event_loop, text_edit, layout: focus-time cursor as an end-of-value sentinel, caret height tied to the text line, row align_top/fill_height modes
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

Focus-time cursor (event_loop/focus.rs): focusing a text input pinned the cursor to a concrete `value.len()` snapshot. When the value keeps growing after focus without the widget seeing keystrokes — crustace's launcher search field is fed over IPC while forge routes the type-to-search keys around Wayland — that snapshot goes stale, and the first key delivered normally afterwards inserts mid-string. The cursor is now seeded with the `usize::MAX` sentinel ("end of whatever value is rendered"), the same convention `select_on_focus` fields already rely on; every consumer (insert/delete, arrow keys, draw, hit-test, context-menu paste offset, a11y tree) clamps via `cursor.min( value.len() )`, so the cursor tracks external growth and collapses to a concrete position on the first real keystroke or click. Click placement is untouched — the pointer path writes its own hit-tested offset.
Caret height (widget/text_edit/draw.rs): the single-line caret spanned `rect.height - 16`, so a field taller than its text line (the launcher's borderless search pill) grew a caret about twice the glyph height. It now measures `font_size + 4`, vertically centered like the text — matching what the multiline caret already did.
Row alignment modes (layout/row.rs): `Row` gains `align_top()` (children pinned to the top edge instead of the default vertical centering) and `fill_height()` (every non-spacer child stretched to the row's inner height, the row itself still sized by its tallest child). Both exist for siblings whose natural heights differ by a few font-metric pixels — like the QS media card next to the wifi/bluetooth chip column — where equal-height layouts cannot be achieved by estimating text heights: `new_line_size` is font-dependent, so px arithmetic in the app always drifts. Containers paint their chrome over the full rect they receive and columns absorb the extra in weighted spacers, so a stretched card keeps its content anchored where its internal spacers put it.
This commit is contained in:
2026-07-30 01:02:30 +02:00
parent 1234943e86
commit 16c04f4159
3 changed files with 56 additions and 17 deletions
+15 -13
View File
@@ -119,28 +119,30 @@ impl<A: App> AppData<A>
} );
ss.request_redraw();
// Sync cursor to end of current value when focusing a text
// input. Default behaviour collapses the selection to the
// cursor — focus changes always discard any prior
// selection state. Fields built with `.select_on_focus(
// true )` instead anchor the selection at `0` so the
// whole value is highlighted, ready to be replaced by the
// next keystroke (typical for numeric pickers).
// Sync cursor to the end of the value when focusing a text
// input, via the `usize::MAX` sentinel ("end of whatever
// value is rendered" — every consumer clamps with
// `cursor.min( value.len() )`). A concrete `value.len()`
// snapshot here would go stale if the value keeps growing
// after focus (e.g. updated externally over IPC), leaving
// later insertions mid-string. Default behaviour collapses
// the selection to the cursor — focus changes always
// discard any prior selection state. Fields built with
// `.select_on_focus( true )` instead anchor the selection
// at `0` so the whole value is highlighted, ready to be
// replaced by the next keystroke (typical for numeric
// pickers).
if is_text_input
{
if let Some( i ) = idx
{
let handler = find_handlers( &ss.frame.widget_rects, i );
let cursor = handler
.and_then( |h| h.current_value() )
.map( |v| v.len() )
.unwrap_or( 0 );
let anchor = match handler
{
Some( WidgetHandlers::TextEdit { select_on_focus: true, .. } ) => 0,
_ => cursor,
_ => usize::MAX,
};
ss.frame.cursor_state.insert( i, cursor );
ss.frame.cursor_state.insert( i, usize::MAX );
ss.frame.selection_anchor.insert( i, anchor );
}
}