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
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:
@@ -119,28 +119,30 @@ impl<A: App> AppData<A>
|
|||||||
} );
|
} );
|
||||||
ss.request_redraw();
|
ss.request_redraw();
|
||||||
|
|
||||||
// Sync cursor to end of current value when focusing a text
|
// Sync cursor to the end of the value when focusing a text
|
||||||
// input. Default behaviour collapses the selection to the
|
// input, via the `usize::MAX` sentinel ("end of whatever
|
||||||
// cursor — focus changes always discard any prior
|
// value is rendered" — every consumer clamps with
|
||||||
// selection state. Fields built with `.select_on_focus(
|
// `cursor.min( value.len() )`). A concrete `value.len()`
|
||||||
// true )` instead anchor the selection at `0` so the
|
// snapshot here would go stale if the value keeps growing
|
||||||
// whole value is highlighted, ready to be replaced by the
|
// after focus (e.g. updated externally over IPC), leaving
|
||||||
// next keystroke (typical for numeric pickers).
|
// 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 is_text_input
|
||||||
{
|
{
|
||||||
if let Some( i ) = idx
|
if let Some( i ) = idx
|
||||||
{
|
{
|
||||||
let handler = find_handlers( &ss.frame.widget_rects, i );
|
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
|
let anchor = match handler
|
||||||
{
|
{
|
||||||
Some( WidgetHandlers::TextEdit { select_on_focus: true, .. } ) => 0,
|
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 );
|
ss.frame.selection_anchor.insert( i, anchor );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ pub struct Row<Msg: Clone>
|
|||||||
/// Padding on all sides. [`Length`]; default `0.0` px.
|
/// Padding on all sides. [`Length`]; default `0.0` px.
|
||||||
pub( crate ) padding: Length,
|
pub( crate ) padding: Length,
|
||||||
pub( crate ) align_right: bool,
|
pub( crate ) align_right: bool,
|
||||||
|
pub( crate ) align_top: bool,
|
||||||
|
pub( crate ) fill_height: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Msg: Clone> Row<Msg>
|
impl<Msg: Clone> Row<Msg>
|
||||||
@@ -59,6 +61,8 @@ impl<Msg: Clone> Row<Msg>
|
|||||||
spacing: Length::px( 8.0 ),
|
spacing: Length::px( 8.0 ),
|
||||||
padding: Length::px( 0.0 ),
|
padding: Length::px( 0.0 ),
|
||||||
align_right: false,
|
align_right: false,
|
||||||
|
align_top: false,
|
||||||
|
fill_height: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +108,26 @@ impl<Msg: Clone> Row<Msg>
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pin children to the top edge instead of the default vertical
|
||||||
|
/// centering, so siblings of slightly different heights (e.g. two
|
||||||
|
/// cards whose text metrics differ) share the same top line.
|
||||||
|
pub fn align_top( mut self ) -> Self
|
||||||
|
{
|
||||||
|
self.align_top = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stretch every child to the row's inner height, so siblings share
|
||||||
|
/// both the top and the bottom line regardless of their natural
|
||||||
|
/// heights. The row's own height still comes from the tallest
|
||||||
|
/// child's preferred size; children whose content should track the
|
||||||
|
/// stretch need internal flexible spacers.
|
||||||
|
pub fn fill_height( mut self ) -> Self
|
||||||
|
{
|
||||||
|
self.fill_height = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the preferred `(width, height)` given available `max_width`.
|
/// Return the preferred `(width, height)` given available `max_width`.
|
||||||
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
||||||
{
|
{
|
||||||
@@ -240,8 +264,15 @@ impl<Msg: Clone> Row<Msg>
|
|||||||
Element::Flex( f ) => flex_unit * f.weight as f32,
|
Element::Flex( f ) => flex_unit * f.weight as f32,
|
||||||
_ => w,
|
_ => w,
|
||||||
};
|
};
|
||||||
let y = rect.y + pad + ( inner_h - h ) / 2.0;
|
let ( y, height ) = if self.fill_height && !matches!( child, Element::Spacer( _ ) )
|
||||||
result.push( ( Rect { x, y, width, height: h }, i ) );
|
{
|
||||||
|
( rect.y + pad, inner_h )
|
||||||
|
} else if self.align_top {
|
||||||
|
( rect.y + pad, h )
|
||||||
|
} else {
|
||||||
|
( rect.y + pad + ( inner_h - h ) / 2.0, h )
|
||||||
|
};
|
||||||
|
result.push( ( Rect { x, y, width, height }, i ) );
|
||||||
x += width + spacing;
|
x += width + spacing;
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
@@ -258,6 +289,8 @@ impl<Msg: Clone> Row<Msg>
|
|||||||
spacing: self.spacing,
|
spacing: self.spacing,
|
||||||
padding: self.padding,
|
padding: self.padding,
|
||||||
align_right: self.align_right,
|
align_right: self.align_right,
|
||||||
|
align_top: self.align_top,
|
||||||
|
fill_height: self.fill_height,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,12 +195,16 @@ impl<Msg: Clone> TextEdit<Msg>
|
|||||||
};
|
};
|
||||||
let cursor_x = rect.x + theme::PAD_H + align_x - scroll_x
|
let cursor_x = rect.x + theme::PAD_H + align_x - scroll_x
|
||||||
+ canvas.measure_text( &cursor_text, font_size );
|
+ canvas.measure_text( &cursor_text, font_size );
|
||||||
|
// Caret height follows the text line, not the field box — a
|
||||||
|
// tall field (launcher pill) otherwise grows a caret twice
|
||||||
|
// the glyph height. Matches the multiline caret.
|
||||||
|
let cursor_h = font_size + 4.0;
|
||||||
let cursor_rect = Rect
|
let cursor_rect = Rect
|
||||||
{
|
{
|
||||||
x: cursor_x,
|
x: cursor_x,
|
||||||
y: rect.y + 8.0,
|
y: rect.y + ( rect.height - cursor_h ) / 2.0,
|
||||||
width: 2.0,
|
width: 2.0,
|
||||||
height: rect.height - 16.0,
|
height: cursor_h,
|
||||||
};
|
};
|
||||||
canvas.fill_rect( cursor_rect, theme::cursor(), 0.0 );
|
canvas.fill_rect( cursor_rect, theme::cursor(), 0.0 );
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user