Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions crates/ui/src/input/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,16 @@ impl TextElement {
let mut cursor_bounds = None;

// If the input has a fixed height (Otherwise is auto-grow), we need to add a bottom margin to the input.
// Cap to bounds height: taffy edge-rounding can shrink bounds.height below
// line_height, and an uncapped margin would falsely trigger scroll adjustments.
let top_bottom_margin = if state.mode.is_auto_grow() {
line_height
} else if visible_range.len() < BOTTOM_MARGIN_ROWS * 8 {
line_height
} else {
BOTTOM_MARGIN_ROWS * line_height
};
}
.min(bounds.size.height);

// The cursor corresponds to the current cursor position in the text no only the line.
let mut cursor_pos = None;
Expand Down Expand Up @@ -1842,17 +1845,23 @@ impl Element for TextElement {
cx,
);

let scale_factor = window.scale_factor();
self.state.update(cx, |state, cx| {
state.last_layout = Some(prepaint.last_layout.clone());
state.last_bounds = Some(bounds);
state.last_cursor = Some(state.cursor());
state.set_input_bounds(input_bounds, cx);
state.set_input_bounds(input_bounds, scale_factor, cx);
state.last_selected_range = Some(selected_range);
state.scroll_size = prepaint.scroll_size;
state.update_scroll_offset(Some(prepaint.cursor_scroll_offset), cx);
state.update_scroll_offset(
Some(prepaint.cursor_scroll_offset),
scale_factor,
cx,
);
state.deferred_scroll_offset = None;

cx.notify();
// No unconditional cx.notify() here — paint is a read-only observation endpoint.
// set_input_bounds and update_scroll_offset notify internally only when
// values truly change at physical-pixel granularity.
});

if let Some(hitbox) = prepaint.hover_definition_hitbox.as_ref() {
Expand Down
30 changes: 26 additions & 4 deletions crates/ui/src/input/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1442,7 +1442,7 @@ impl InputState {
let delta = event.delta.pixel_delta(line_height);

let old_offset = self.scroll_handle.offset();
self.update_scroll_offset(Some(old_offset + delta), cx);
self.update_scroll_offset(Some(old_offset + delta), window.scale_factor(), cx);

// Only stop propagation if the offset actually changed
if self.scroll_handle.offset() != old_offset {
Expand All @@ -1455,6 +1455,7 @@ impl InputState {
pub(super) fn update_scroll_offset(
&mut self,
offset: Option<Point<Pixels>>,
scale_factor: f32,
cx: &mut Context<Self>,
) {
let mut offset = offset.unwrap_or(self.scroll_handle.offset());
Expand All @@ -1476,8 +1477,16 @@ impl InputState {
offset.y.clamp(safe_y_range.start, safe_y_range.end)
};
offset.x = offset.x.clamp(safe_x_range.start, safe_x_range.end);

// Compare at physical-pixel granularity to avoid sub-pixel oscillation
// that would cause a perpetual notify→repaint loop.
let old = self.scroll_handle.offset();
self.scroll_handle.set_offset(offset);
cx.notify();

let snap = |v: Pixels| (v.as_f32() * scale_factor).round() as i32;
if snap(old.x) != snap(offset.x) || snap(old.y) != snap(offset.y) {
cx.notify();
}
}

/// Scroll to make the given offset visible.
Expand Down Expand Up @@ -1546,6 +1555,10 @@ impl InputState {
} else {
line_height
};
// Cap margin to viewport height: taffy edge-rounding can shrink bounds.height
// below line_height (e.g. 34.5 phys → 34 phys at 1.5× scale), and an
// uncapped margin would falsely trigger scroll on every cursor movement.
let edge_height = edge_height.min(bounds.size.height);
if row_offset_y - edge_height + line_height < -scroll_offset.y {
// Scroll up
scroll_offset.y = -row_offset_y + edge_height - line_height;
Expand Down Expand Up @@ -1979,8 +1992,17 @@ impl InputState {
cx.notify();
}

pub(super) fn set_input_bounds(&mut self, new_bounds: Bounds<Pixels>, cx: &mut Context<Self>) {
let wrap_width_changed = self.input_bounds.size.width != new_bounds.size.width;
pub(super) fn set_input_bounds(
&mut self,
new_bounds: Bounds<Pixels>,
scale_factor: f32,
cx: &mut Context<Self>,
) {
// Compare at physical-pixel granularity to avoid sub-pixel float noise
// from taffy rounding causing a perpetual notify→repaint loop.
let snap = |v: Pixels| (v.as_f32() * scale_factor).round() as i32;
let wrap_width_changed =
snap(self.input_bounds.size.width) != snap(new_bounds.size.width);
self.input_bounds = new_bounds;

// Update display_map wrap_width if changed.
Expand Down
Loading