Skip to content
Merged
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
10 changes: 9 additions & 1 deletion crates/ui/src/input/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -527,9 +527,17 @@ impl TextElement {
_ => 0.85,
} * line_height;

// Match the caret to the deferred scroll target (applied below) that
// the text paints at; otherwise the caret follows the cursor-scroll
// while the text uses the deferred offset, flashing it mid-field.
let cursor_scroll_x = state
.deferred_scroll_offset
.map(|offset| offset.x)
.unwrap_or(scroll_offset.x);

// For Right alignment, clamp cursor within the right edge of bounds so it
// stays visible without having to shift the text via scroll_offset.
let cursor_x = bounds.left() + cursor_pos.x + line_number_width + scroll_offset.x;
let cursor_x = bounds.left() + cursor_pos.x + line_number_width + cursor_scroll_x;
let cursor_x = if last_layout.text_align == TextAlign::Right {
cursor_x.min(bounds.right() - CURSOR_WIDTH)
} else {
Expand Down
68 changes: 64 additions & 4 deletions crates/ui/src/input/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,10 @@ impl InputState {

/// Set the text of the input field.
///
/// And the selection_range will be reset to 0..0.
/// For single-line inputs the caret is placed at the end of the text while
/// the view is scrolled back to the start, so a long value shows its
/// beginning instead of its tail (matching HTML `<input>`). Multi-line
/// inputs reset the selection to `0..0`.
pub fn set_value(
&mut self,
value: impl Into<SharedString>,
Expand All @@ -790,9 +793,11 @@ impl InputState {
self.history.ignore = false;
self.emit_events = true;

// Ensure cursor to start when set text
// Place the caret at the end for single-line inputs (like HTML
// `<input>`); multi-line inputs reset the selection to the start.
if self.mode.is_single_line() {
self.selected_range = (self.text.len()..self.text.len()).into();
let end = self.text.len();
self.selected_range = (end..end).into();
} else {
self.selected_range.clear();
}
Expand All @@ -802,8 +807,13 @@ impl InputState {
self.lsp.reset();
}

// Move scroll to top
// Move scroll to the start. For single-line the caret is at the end, so
// override the cursor-follow scroll for the next painted frame to keep
// the start visible; the deferred offset is consumed during that paint.
self.scroll_handle.set_offset(point(px(0.), px(0.)));
if self.mode.is_single_line() {
self.deferred_scroll_offset = Some(point(px(0.), px(0.)));
}

self.history.clear();
cx.notify();
Expand Down Expand Up @@ -3515,4 +3525,54 @@ ORDER BY id
});
});
}

/// After `set_value` on a single-line input the caret sits at the end (like
/// HTML `<input>`), yet the view is scrolled back to the start so a long
/// value shows its beginning instead of its tail.
#[gpui::test]
fn test_set_value_single_line_caret_at_end_view_at_start(cx: &mut TestAppContext) {
let input_view = InputView::build(cx, |state| state);
let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
let input = input_view.input;

// Long enough to overflow any reasonable single-line input width.
let value = format!("https://example.com/v1/users?{}", "x=1&".repeat(120));
let len = value.len();

// Right after `set_value`, before the next paint consumes the deferred
// offset: caret is at the end, and the view is forced back to the start.
cx.update(|window, cx| {
input.update(cx, |state, cx| {
state.set_value(value.clone(), window, cx);

assert_eq!(
state.selected_range,
Selection::new(len, len),
"single-line caret should be at the end after set_value"
);
assert_eq!(
state.deferred_scroll_offset,
Some(point(px(0.), px(0.))),
"the view should be forced back to the start"
);
});
});

// After a paint, the steady-state view stays at the start (x == 0) even
// though the caret is at the far end.
cx.run_until_parked();
cx.update(|_, cx| {
input.read_with(cx, |state, _| {
assert!(
state.scroll_size.width > state.input_bounds.size.width,
"value must overflow the input width or this test is vacuous"
);
assert_eq!(
state.scroll_handle.offset().x,
px(0.),
"long value should display from its start, not its tail"
);
});
});
}
}
Loading