Bump to 0.2.0: SW/GLES paint parity, shared font resolution, FrameState refactor, docs and packaging fixes
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

Rendering parity (software ↔ GLES). The software backend now rounds glyph pen positions and image destinations to the nearest integer pixel, matching what the GLES backend already did; previously it truncated, so text and 1:1 images could land up to half a pixel off between the two backends and the bilinear sample read ~1 px softer than the source. Gradients and shadows are deliberately left unimplemented on the software backend, and the GLES multi-rect `glScissor` clip is left coarse on purpose: making it exact would need stencil bits the EGL config does not carry, or routing the partial-redraw path through the offscreen clip layer, which would break its `fill` / `clear_rects_transparent` scissor semantics. Adds software-backend pixel tests covering the snapping.
Font resolution unification. The system-font candidate chain, `find_font_opt` and `load_default_font_bytes` lived in two copies (`render/helpers` and `gles_render/helpers`) that had already diverged — one resolved through `find_font_opt`, the other inlined the candidate loop — and now live once in `system_fonts`. The two per-backend `OnceLock` default-font caches and `primary_handle` collapse into a single `system_fonts::default_handle`. Module docs and the `font_registry` caller are updated accordingly.
Shared image validation and rect inflation. `draw_image_data`'s dimension check and its one-line warning were byte-duplicated across both backends and are now `render::helpers::validate_rgba_dims`. The six manual symmetric `Rect`-inflate literals in the GLES primitives reuse the existing `Rect::expand`.
FrameState and DrawCtx de-duplication. The eleven `SurfaceState` fields the draw pass owns and threads through `DrawCtx` — `widget_rects`, the cursor / selection maps, the scroll state, `accessible_extras`, `prev_focused` / `prev_hovered` / `prev_pressed` — move into a `FrameState` sub-struct. The per-frame `build_draw_ctx` / `commit_draw_ctx` helpers can then borrow `&mut ss.frame`, disjoint from `ss.canvas` and `ss.pool`, so the four frame paths (software / GLES × full / partial) replace their duplicated `DrawCtx` construction and write-back with a single helper call each. A whole-`SurfaceState` borrow could not express this (partial borrows do not cross function boundaries), which is why the helpers take the sub-struct. `content_dirty` stays on `SurfaceState` — it is an invalidation flag, not frame state — and is reset at the call site.
rich_text tests. Adds the previously-missing `tests.rs` for the `RichText` widget: one hit rect per visual line a link spans (the widget's core invariant), the single-line and no-link cases, preferred-size growth with hard line breaks, and `map_msg` range preservation — all headless against a software `Canvas`, with line counts forced by `\n` so they do not depend on any system font's measured width.
Documentation. Fills the rustdoc gaps on the embedder-facing surface: `core::UiSurface` accessors, the `egl_context` public API, the `GlesCanvas` methods, `theme::typography` and `theme::error`, and the `RichText` / `Text` builders; adds a crate-level "Rendering backends" overview. `CHANGELOG.md` is added (0.2.0 / 0.1.0), and `docs/widgets.md` / `docs/cookbook.md` gain `rich_text`, `external`, and the CPU-draw / path-clip / externally-laid-out-tree recipes. `debian/changelog` gets the 0.2.0-1 entry. Private intra-doc links to `system_fonts` are demoted to code spans so `cargo doc` is warning-free.
Packaging. The `libltk-dev` registry crate shipped a `Cargo.toml` declaring the `lookup` bench while the `Makefile` install copied only `src/`, so Cargo refused to parse the manifest over a missing `benches/lookup.rs`; the install now ships `benches/` as well (the file alone satisfies the parse — criterion is a dev-dependency and is not resolved when the crate is consumed as a library). `Cargo.toml` is bumped to 0.2.0 to match the package version and the `ltk-0.2.0` registry directory.
This commit is contained in:
2026-06-25 12:43:40 +02:00
parent fb3552e9f7
commit d4d7ee742e
56 changed files with 936 additions and 549 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ impl<A: App> AppData<A>
}
if let Some( new_value ) = self.delete_selection( focus )
{
let msg = find_handlers( &self.surface( focus ).widget_rects, idx )
let msg = find_handlers( &self.surface( focus ).frame.widget_rects, idx )
.and_then( |h| h.text_change_msg( &new_value ) );
if let Some( m ) = msg { self.pending_msgs.push( m ); }
}
+4 -4
View File
@@ -88,7 +88,7 @@ impl<A: App> AppData<A>
let paste_offset = self.text_input_geometry( focus, widget_idx )
.and_then( |( rect, value, multiline, secure, align, font_size )|
{
let cursor_pos = self.surface( focus ).cursor_state.get( &widget_idx ).copied().unwrap_or( 0 );
let cursor_pos = self.surface( focus ).frame.cursor_state.get( &widget_idx ).copied().unwrap_or( 0 );
let canvas = self.surface( focus ).canvas.as_ref()?;
Some( crate::widget::text_edit::byte_offset_at(
canvas, rect, pos, &value, multiline, secure, cursor_pos, align, font_size,
@@ -163,8 +163,8 @@ impl<A: App> AppData<A>
if let Some( ofs ) = menu.paste_offset
{
let ss = self.surface_mut( focus );
ss.cursor_state.insert( menu.widget_idx, ofs );
ss.selection_anchor.insert( menu.widget_idx, ofs );
ss.frame.cursor_state.insert( menu.widget_idx, ofs );
ss.frame.selection_anchor.insert( menu.widget_idx, ofs );
}
self.handle_paste( focus );
}
@@ -176,7 +176,7 @@ impl<A: App> AppData<A>
if !menu.has_selection { return true; }
if let Some( new_value ) = self.delete_selection( focus )
{
let msg = find_handlers( &self.surface( focus ).widget_rects, menu.widget_idx )
let msg = find_handlers( &self.surface( focus ).frame.widget_rects, menu.widget_idx )
.and_then( |h| h.text_change_msg( &new_value ) );
if let Some( m ) = msg { self.pending_msgs.push( m ); }
}
+1 -1
View File
@@ -115,7 +115,7 @@ impl<A: App> AppData<A>
let app_override = self.app.cursor_override();
let dragging = self.surface( focus ).gesture.dragging_slider.is_some();
let hover_cursor = self.surface( focus ).hovered_idx
.and_then( |idx| crate::tree::find_widget( &self.surface( focus ).widget_rects, idx ) )
.and_then( |idx| crate::tree::find_widget( &self.surface( focus ).frame.widget_rects, idx ) )
.map( |w| w.cursor )
.unwrap_or( CursorShape::Default );
let resize_cursor = self.resize_edge_under_pointer( focus ).map( resize_edge_to_cursor );
+8 -8
View File
@@ -35,7 +35,7 @@ impl<A: App> AppData<A>
// dismiss that races other intents (e.g. forge's topbar
// taps that route a separate IPC into the same overlay).
let Some( anchor_id ) = spec.anchor_widget_id else { continue; };
let anchor_rect = self.main.widget_rects.iter()
let anchor_rect = self.main.frame.widget_rects.iter()
.find( | w | w.id == Some( anchor_id ) )
.map( | w | w.rect );
let on_anchor = anchor_rect
@@ -89,15 +89,15 @@ impl<A: App> AppData<A>
let ss = self.surface_mut( focus );
is_text_input = idx
.and_then( |i| find_handlers( &ss.widget_rects, i ) )
.and_then( |i| find_handlers( &ss.frame.widget_rects, i ) )
.map( |h| h.is_text_input() )
.unwrap_or( false );
secure = idx
.and_then( |i| find_handlers( &ss.widget_rects, i ) )
.and_then( |i| find_handlers( &ss.frame.widget_rects, i ) )
.map( |h| matches!( h, WidgetHandlers::TextEdit { secure: true, .. } ) )
.unwrap_or( false );
was_text_input = ss.focused_idx
.and_then( |i| find_handlers( &ss.widget_rects, i ) )
.and_then( |i| find_handlers( &ss.frame.widget_rects, i ) )
.map( |h| h.is_text_input() )
.unwrap_or( false );
@@ -113,7 +113,7 @@ impl<A: App> AppData<A>
ss.focused_idx = idx;
ss.focused_id = idx.and_then( |i|
{
ss.widget_rects.iter()
ss.frame.widget_rects.iter()
.find( |w| w.flat_idx == i )
.and_then( |w| w.id )
} );
@@ -130,7 +130,7 @@ impl<A: App> AppData<A>
{
if let Some( i ) = idx
{
let handler = find_handlers( &ss.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() )
@@ -140,8 +140,8 @@ impl<A: App> AppData<A>
Some( WidgetHandlers::TextEdit { select_on_focus: true, .. } ) => 0,
_ => cursor,
};
ss.cursor_state.insert( i, cursor );
ss.selection_anchor.insert( i, anchor );
ss.frame.cursor_state.insert( i, cursor );
ss.frame.selection_anchor.insert( i, anchor );
}
}
}
+3 -3
View File
@@ -124,13 +124,13 @@ fn draw_surface<Msg: Clone>(
.unwrap_or( false );
let partial_eligible = !ss.content_dirty
&& canvas_ready
&& !ss.widget_rects.is_empty();
&& !ss.frame.widget_rects.is_empty();
if partial_eligible
{
let dirty_rects = compute_interaction_dirty_rects(
&ss.widget_rects,
ss.prev_focused, ss.prev_hovered, ss.prev_pressed,
&ss.frame.widget_rects,
ss.frame.prev_focused, ss.frame.prev_hovered, ss.frame.prev_pressed,
ss.focused_idx, ss.hovered_idx, ss.gesture.pressed_idx,
pw, ph,
);
+1 -1
View File
@@ -22,7 +22,7 @@ pub( crate ) mod invalidation;
pub( crate ) mod overlays_reconcile;
pub( crate ) use app_data::AppData;
pub( crate ) use surface::{ LayerConfig, SurfaceFocus, SurfaceKind, SurfaceState };
pub( crate ) use surface::{ FrameState, LayerConfig, SurfaceFocus, SurfaceKind, SurfaceState };
pub use error::RunError;
pub( crate ) use run::{ run, try_run };
pub use invalidation::diff_overlay_ids;
+1 -1
View File
@@ -120,7 +120,7 @@ pub( super ) fn reconcile_overlays<A: App>( data: &mut AppData<A> )
// Snapshot the previous-frame anchor lookup table so we can resolve
// `anchor_widget_id` → `Rect` without holding a borrow on `data.main`
// across the overlay-mut loop below.
let main_widget_rects = &data.main.widget_rects;
let main_widget_rects = &data.main.frame.widget_rects;
let overlays_m = &mut data.overlays;
for spec in &specs
{
+15 -15
View File
@@ -638,12 +638,12 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
focus_id: 0,
is_main: true,
label: None,
widget_rects: &data.main.widget_rects,
extras: &data.main.accessible_extras,
widget_rects: &data.main.frame.widget_rects,
extras: &data.main.frame.accessible_extras,
focused_idx: data.main.focused_idx,
pressed_idx: data.main.gesture.pressed_idx,
pending_text_values: &data.main.pending_text_values,
cursor_state: &data.main.cursor_state,
cursor_state: &data.main.frame.cursor_state,
width: main_w,
height: main_h,
offset_x: 0.0,
@@ -657,12 +657,12 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
focus_id: *fid,
is_main: false,
label: None,
widget_rects: &ss.widget_rects,
extras: &ss.accessible_extras,
widget_rects: &ss.frame.widget_rects,
extras: &ss.frame.accessible_extras,
focused_idx: ss.focused_idx,
pressed_idx: ss.gesture.pressed_idx,
pending_text_values: &ss.pending_text_values,
cursor_state: &ss.cursor_state,
cursor_state: &ss.frame.cursor_state,
width: ss.width as f32,
height: ss.height as f32,
offset_x: *ox,
@@ -711,8 +711,8 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
};
let widget = match focus_target
{
SurfaceFocus::Main => data.main.widget_rects.iter().find( |w| w.flat_idx == idx ).cloned(),
SurfaceFocus::Overlay( id ) => data.overlays.get( &id ).and_then( |ss| ss.widget_rects.iter().find( |w| w.flat_idx == idx ).cloned() ),
SurfaceFocus::Main => data.main.frame.widget_rects.iter().find( |w| w.flat_idx == idx ).cloned(),
SurfaceFocus::Overlay( id ) => data.overlays.get( &id ).and_then( |ss| ss.frame.widget_rects.iter().find( |w| w.flat_idx == idx ).cloned() ),
};
match req.action
{
@@ -786,10 +786,10 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
};
if let Some( ss ) = ss
{
if let Some( ( _, _, _ ) ) = ss.scroll_rects.last().copied()
if let Some( ( _, _, _ ) ) = ss.frame.scroll_rects.last().copied()
{
let scroll_idx = ss.scroll_rects.last().unwrap().1;
let entry = ss.scroll_offsets.entry( scroll_idx ).or_insert( ( 0.0, 0.0 ) );
let scroll_idx = ss.frame.scroll_rects.last().unwrap().1;
let entry = ss.frame.scroll_offsets.entry( scroll_idx ).or_insert( ( 0.0, 0.0 ) );
entry.0 = ( entry.0 + dx ).max( 0.0 );
entry.1 = ( entry.1 + dy ).max( 0.0 );
ss.request_redraw();
@@ -808,12 +808,12 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
{
let target = w.rect;
let probe = crate::types::Point { x: target.x + 1.0, y: target.y + 1.0 };
let container = ss.scroll_rects.iter().rev()
let container = ss.frame.scroll_rects.iter().rev()
.find( |( r, _, _ )| r.contains( probe ) )
.copied();
if let Some( ( r, idx, _ ) ) = container
{
let entry = ss.scroll_offsets.entry( idx ).or_insert( ( 0.0, 0.0 ) );
let entry = ss.frame.scroll_offsets.entry( idx ).or_insert( ( 0.0, 0.0 ) );
if target.y < r.y { entry.1 -= r.y - target.y; }
if target.y + target.height > r.y + r.height
{
@@ -842,14 +842,14 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
let focus_id = data.app.take_focus_request().or_else( || data.focus_retry.take() );
if let Some( id ) = focus_id
{
let mut hit: Option<( SurfaceFocus, usize )> = data.main.widget_rects.iter()
let mut hit: Option<( SurfaceFocus, usize )> = data.main.frame.widget_rects.iter()
.find( |w| w.id == Some( id ) )
.map( |w| ( SurfaceFocus::Main, w.flat_idx ) );
if hit.is_none()
{
for ( ov_id, surf ) in &data.overlays
{
if let Some( w ) = surf.widget_rects.iter().find( |w| w.id == Some( id ) )
if let Some( w ) = surf.frame.widget_rects.iter().find( |w| w.id == Some( id ) )
{
hit = Some( ( SurfaceFocus::Overlay( *ov_id ), w.flat_idx ) );
break;
+60 -39
View File
@@ -149,6 +149,61 @@ impl SurfaceKind
}
}
/// The subset of per-surface state the draw pass owns and threads through
/// [`crate::draw::DrawCtx`]: the laid-out widget rects it produces, the scroll /
/// text-edit / a11y state carried across frames, and the previous frame's
/// interaction indices for damage tracking. Grouped into its own struct so the
/// draw helpers can borrow it as one unit, disjoint from `canvas` / `pool`.
pub( crate ) struct FrameState<Msg: Clone>
{
pub widget_rects: Vec<LaidOutWidget<Msg>>,
pub cursor_state: HashMap<usize, usize>,
/// Selection anchor (byte offset). When equal to the cursor, there is no
/// active selection. Mutated jointly with `cursor_state`: plain arrow keys
/// collapse the selection by setting anchor = cursor; Shift+arrow extends by
/// leaving anchor put while moving cursor; pointer drags set both ends.
pub selection_anchor: HashMap<usize, usize>,
pub accessible_extras: Vec<crate::a11y::tree::AccessibleExtra>,
/// Per-scroll-viewport offset, indexed by `flat_idx` of the `Scroll` widget.
/// Tuple is `(x, y)` in physical pixels: x is applied only when the widget's
/// axis allows horizontal scroll (and stays at `0` otherwise), y mirrors the
/// historic single-f32 behaviour for vertical scrolls.
pub scroll_offsets: HashMap<usize, ( f32, f32 )>,
pub scroll_rects: Vec<( Rect, usize, crate::widget::scroll::ScrollAxis )>,
pub scroll_canvases: HashMap<usize, Canvas>,
/// Per-scroll list of `(flat_idx, content_y, height)` entries for every
/// interactive item the scroll's child laid out, in document order. Includes
/// items currently scrolled off-screen so keyboard arrow handlers can step
/// `hovered_idx` item-by-item without regenerating the layout pass. Y is in
/// pre-translation, pre-offset content coordinates.
pub scroll_navigable_items: HashMap<usize, Vec<( usize, f32, f32 )>>,
/// Previous frame interaction state for damage tracking.
pub prev_focused: Option<usize>,
pub prev_hovered: Option<usize>,
pub prev_pressed: Option<usize>,
}
impl<Msg: Clone> FrameState<Msg>
{
pub( crate ) fn new() -> Self
{
Self
{
widget_rects: Vec::new(),
cursor_state: HashMap::new(),
selection_anchor: HashMap::new(),
accessible_extras: Vec::new(),
scroll_offsets: HashMap::new(),
scroll_rects: Vec::new(),
scroll_canvases: HashMap::new(),
scroll_navigable_items: HashMap::new(),
prev_focused: None,
prev_hovered: None,
prev_pressed: None,
}
}
}
/// Per-surface render + interaction state.
///
/// Each Wayland surface managed by the event loop (main surface and any
@@ -177,31 +232,11 @@ pub( crate ) struct SurfaceState<Msg: Clone>
pub focused_idx: Option<usize>,
pub focused_id: Option<WidgetId>,
pub hovered_idx: Option<usize>,
pub widget_rects: Vec<LaidOutWidget<Msg>>,
pub cursor_state: HashMap<usize, usize>,
/// Selection anchor (byte offset). When equal to the cursor, there
/// is no active selection. Mutated jointly with `cursor_state`:
/// plain arrow keys collapse the selection by setting anchor =
/// cursor; Shift+arrow extends by leaving anchor put while moving
/// cursor; pointer drags set both ends.
pub selection_anchor: HashMap<usize, usize>,
pub pending_text_values: HashMap<usize, String>,
/// Per-scroll-viewport offset, indexed by `flat_idx` of the
/// `Scroll` widget. Tuple is `(x, y)` in physical pixels: x is
/// applied only when the widget's axis allows horizontal scroll
/// (and stays at `0` otherwise), y mirrors the historic single-f32
/// behaviour for vertical scrolls.
pub accessible_extras: Vec<crate::a11y::tree::AccessibleExtra>,
pub scroll_offsets: HashMap<usize, ( f32, f32 )>,
pub scroll_rects: Vec<( Rect, usize, crate::widget::scroll::ScrollAxis )>,
pub scroll_canvases: HashMap<usize, Canvas>,
/// Per-scroll list of `(flat_idx, content_y, height)` entries for
/// every interactive item the scroll's child laid out, in document
/// order. Includes items currently scrolled off-screen so keyboard
/// arrow handlers can step `hovered_idx` item-by-item without
/// regenerating the layout pass. Y is in pre-translation,
/// pre-offset content coordinates.
pub scroll_navigable_items: HashMap<usize, Vec<( usize, f32, f32 )>>,
/// Per-frame draw state: laid-out rects, carried scroll / text-edit / a11y
/// state, and the previous frame's interaction indices. Grouped so the draw
/// helpers can borrow it as one unit, disjoint from `canvas` / `pool`.
pub frame: FrameState<Msg>,
/// Built-in Copy / Cut / Paste context menu when shown on this
/// surface. `None` means no menu is up.
pub context_menu: Option<super::context_menu::ContextMenu>,
@@ -223,10 +258,6 @@ pub( crate ) struct SurfaceState<Msg: Clone>
/// `wl_touch.up` does not carry a position) and to keep state
/// across `motion` events between dispatchers.
pub touch_slots: HashMap<i32, crate::types::Point>,
/// Previous frame interaction state for damage tracking
pub prev_focused: Option<usize>,
pub prev_hovered: Option<usize>,
pub prev_pressed: Option<usize>,
/// Height of the client-side title bar (0 for layer-shell surfaces).
pub titlebar_height: f32,
/// Title text shown in the client-side title bar.
@@ -275,22 +306,12 @@ impl<Msg: Clone> SurfaceState<Msg>
focused_idx: None,
focused_id: None,
hovered_idx: None,
widget_rects: Vec::new(),
cursor_state: HashMap::new(),
selection_anchor: HashMap::new(),
pending_text_values: HashMap::new(),
accessible_extras: Vec::new(),
scroll_offsets: HashMap::new(),
scroll_rects: Vec::new(),
scroll_navigable_items: HashMap::new(),
frame: FrameState::new(),
primary_touch_id: None,
touch_slots: HashMap::new(),
context_menu: None,
scroll_canvases: HashMap::new(),
gesture: GestureState::new(),
prev_focused: None,
prev_hovered: None,
prev_pressed: None,
titlebar_height,
titlebar_title,
titlebar_close_rect: Rect::default(),
+25 -25
View File
@@ -17,7 +17,7 @@ impl<A: App> AppData<A>
{
pending.clone()
} else {
crate::tree::find_handlers( &self.surface( focus ).widget_rects, idx )?
crate::tree::find_handlers( &self.surface( focus ).frame.widget_rects, idx )?
.current_value()
.map( |s| s.to_string() )
.unwrap_or_default()
@@ -32,17 +32,17 @@ impl<A: App> AppData<A>
pub( crate ) fn handle_cursor_left( &mut self, focus: SurfaceFocus, extend: bool )
{
let ( idx, value ) = match self.focused_text_value( focus ) { Some( v ) => v, None => return };
let cursor = self.surface( focus ).cursor_state.get( &idx ).copied()
let cursor = self.surface( focus ).frame.cursor_state.get( &idx ).copied()
.unwrap_or( value.len() ).min( value.len() );
let anchor = self.surface( focus ).selection_anchor.get( &idx ).copied()
let anchor = self.surface( focus ).frame.selection_anchor.get( &idx ).copied()
.unwrap_or( cursor ).min( value.len() );
// Without shift: if a selection exists, collapse to its start.
if !extend && anchor != cursor
{
let s = cursor.min( anchor );
let ss = self.surface_mut( focus );
*ss.cursor_state.entry( idx ).or_insert( 0 ) = s;
*ss.selection_anchor.entry( idx ).or_insert( 0 ) = s;
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = s;
*ss.frame.selection_anchor.entry( idx ).or_insert( 0 ) = s;
ss.request_redraw();
return;
}
@@ -51,8 +51,8 @@ impl<A: App> AppData<A>
let step = prev_char.map( |c| c.len_utf8() ).unwrap_or( 1 );
let new_cursor = cursor.saturating_sub( step );
let ss = self.surface_mut( focus );
*ss.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
if !extend { *ss.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
if !extend { *ss.frame.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
ss.request_redraw();
}
@@ -60,16 +60,16 @@ impl<A: App> AppData<A>
pub( crate ) fn handle_cursor_right( &mut self, focus: SurfaceFocus, extend: bool )
{
let ( idx, value ) = match self.focused_text_value( focus ) { Some( v ) => v, None => return };
let cursor = self.surface( focus ).cursor_state.get( &idx ).copied()
let cursor = self.surface( focus ).frame.cursor_state.get( &idx ).copied()
.unwrap_or( value.len() ).min( value.len() );
let anchor = self.surface( focus ).selection_anchor.get( &idx ).copied()
let anchor = self.surface( focus ).frame.selection_anchor.get( &idx ).copied()
.unwrap_or( cursor ).min( value.len() );
if !extend && anchor != cursor
{
let e = cursor.max( anchor );
let ss = self.surface_mut( focus );
*ss.cursor_state.entry( idx ).or_insert( 0 ) = e;
*ss.selection_anchor.entry( idx ).or_insert( 0 ) = e;
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = e;
*ss.frame.selection_anchor.entry( idx ).or_insert( 0 ) = e;
ss.request_redraw();
return;
}
@@ -78,8 +78,8 @@ impl<A: App> AppData<A>
let step = next_char.map( |c| c.len_utf8() ).unwrap_or( 1 );
let new_cursor = ( cursor + step ).min( value.len() );
let ss = self.surface_mut( focus );
*ss.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
if !extend { *ss.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
if !extend { *ss.frame.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
ss.request_redraw();
}
@@ -96,7 +96,7 @@ impl<A: App> AppData<A>
pub( crate ) fn handle_cursor_up( &mut self, focus: SurfaceFocus, extend: bool ) -> bool
{
let ( idx, value ) = match self.focused_text_value( focus ) { Some( v ) => v, None => return false };
let cursor = self.surface( focus ).cursor_state.get( &idx ).copied()
let cursor = self.surface( focus ).frame.cursor_state.get( &idx ).copied()
.unwrap_or( value.len() ).min( value.len() );
let ( rect, _, multiline, secure, _align, _font_size ) = match self.text_input_geometry( focus, idx )
{
@@ -111,8 +111,8 @@ impl<A: App> AppData<A>
None => return false,
};
let ss = self.surface_mut( focus );
*ss.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
if !extend { *ss.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
if !extend { *ss.frame.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
ss.request_redraw();
true
}
@@ -122,7 +122,7 @@ impl<A: App> AppData<A>
pub( crate ) fn handle_cursor_down( &mut self, focus: SurfaceFocus, extend: bool ) -> bool
{
let ( idx, value ) = match self.focused_text_value( focus ) { Some( v ) => v, None => return false };
let cursor = self.surface( focus ).cursor_state.get( &idx ).copied()
let cursor = self.surface( focus ).frame.cursor_state.get( &idx ).copied()
.unwrap_or( value.len() ).min( value.len() );
let ( rect, _, multiline, secure, _align, _font_size ) = match self.text_input_geometry( focus, idx )
{
@@ -137,8 +137,8 @@ impl<A: App> AppData<A>
None => return false,
};
let ss = self.surface_mut( focus );
*ss.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
if !extend { *ss.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
if !extend { *ss.frame.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
ss.request_redraw();
true
}
@@ -151,7 +151,7 @@ impl<A: App> AppData<A>
pub( crate ) fn handle_cursor_home( &mut self, focus: SurfaceFocus, extend: bool )
{
let ( idx, value ) = match self.focused_text_value( focus ) { Some( v ) => v, None => return };
let cursor = self.surface( focus ).cursor_state.get( &idx ).copied()
let cursor = self.surface( focus ).frame.cursor_state.get( &idx ).copied()
.unwrap_or( value.len() ).min( value.len() );
let new_cursor = match self.text_input_geometry( focus, idx )
{
@@ -163,8 +163,8 @@ impl<A: App> AppData<A>
_ => value[..cursor].rfind( '\n' ).map( |p| p + 1 ).unwrap_or( 0 ),
};
let ss = self.surface_mut( focus );
*ss.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
if !extend { *ss.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
if !extend { *ss.frame.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
ss.request_redraw();
}
@@ -174,7 +174,7 @@ impl<A: App> AppData<A>
pub( crate ) fn handle_cursor_end( &mut self, focus: SurfaceFocus, extend: bool )
{
let ( idx, value ) = match self.focused_text_value( focus ) { Some( v ) => v, None => return };
let cursor = self.surface( focus ).cursor_state.get( &idx ).copied()
let cursor = self.surface( focus ).frame.cursor_state.get( &idx ).copied()
.unwrap_or( value.len() ).min( value.len() );
let new_cursor = match self.text_input_geometry( focus, idx )
{
@@ -186,8 +186,8 @@ impl<A: App> AppData<A>
_ => value[cursor..].find( '\n' ).map( |p| cursor + p ).unwrap_or( value.len() ),
};
let ss = self.surface_mut( focus );
*ss.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
if !extend { *ss.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = new_cursor;
if !extend { *ss.frame.selection_anchor.entry( idx ).or_insert( 0 ) = new_cursor; }
ss.request_redraw();
}
}
+1 -1
View File
@@ -77,7 +77,7 @@ impl<A: App> AppData<A>
) -> Option<( Rect, String, bool, bool, crate::widget::text::TextAlign, f32 )>
{
let ss = self.surface( focus );
let widget = find_widget( &ss.widget_rects, idx )?;
let widget = find_widget( &ss.frame.widget_rects, idx )?;
let ( value_handler, multiline, secure, align, font_size ) = match &widget.handlers
{
WidgetHandlers::TextEdit { value, multiline, secure, align, font_size, .. } =>
+26 -26
View File
@@ -22,7 +22,7 @@ impl<A: App> AppData<A>
{
pending.clone()
} else {
find_handlers( &self.surface( focus ).widget_rects, idx )
find_handlers( &self.surface( focus ).frame.widget_rects, idx )
.and_then( |h| h.current_value() )
.map( |s| s.to_string() )
.unwrap_or_default()
@@ -41,26 +41,26 @@ impl<A: App> AppData<A>
// a second digit would insert into the middle of the
// normalised string ("0|2" + "3" → "032" → 32 instead of 23).
let select_on_focus = matches!(
find_handlers( &self.surface( focus ).widget_rects, idx ),
find_handlers( &self.surface( focus ).frame.widget_rects, idx ),
Some( WidgetHandlers::TextEdit { select_on_focus: true, .. } ),
);
let new_value;
{
let ss = self.surface_mut( focus );
let cursor = ss.cursor_state.entry( idx ).or_insert( current_value.len() );
let cursor = ss.frame.cursor_state.entry( idx ).or_insert( current_value.len() );
let safe_cursor = (*cursor).min( current_value.len() );
let mut v = current_value.clone();
v.insert_str( safe_cursor, text );
let next_cursor = if select_on_focus { usize::MAX } else { safe_cursor + text.len() };
*ss.cursor_state.entry( idx ).or_insert( 0 ) = next_cursor;
*ss.selection_anchor.entry( idx ).or_insert( 0 ) = next_cursor;
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = next_cursor;
*ss.frame.selection_anchor.entry( idx ).or_insert( 0 ) = next_cursor;
ss.pending_text_values.insert( idx, v.clone() );
ss.request_redraw();
new_value = v;
}
let msg = find_handlers( &self.surface( focus ).widget_rects, idx )
let msg = find_handlers( &self.surface( focus ).frame.widget_rects, idx )
.and_then( |h| h.text_change_msg( &new_value ) );
if let Some( m ) = msg
{
@@ -78,7 +78,7 @@ impl<A: App> AppData<A>
if let Some( new_value ) = self.delete_selection( focus )
{
let msg = find_handlers( &self.surface( focus ).widget_rects, idx )
let msg = find_handlers( &self.surface( focus ).frame.widget_rects, idx )
.and_then( |h| h.text_change_msg( &new_value ) );
if let Some( m ) = msg { self.pending_msgs.push( m ); }
return;
@@ -88,13 +88,13 @@ impl<A: App> AppData<A>
{
pending.clone()
} else {
find_handlers( &self.surface( focus ).widget_rects, idx )
find_handlers( &self.surface( focus ).frame.widget_rects, idx )
.and_then( |h| h.current_value() )
.map( |s| s.to_string() )
.unwrap_or_default()
};
let cursor_val = self.surface( focus ).cursor_state.get( &idx ).copied()
let cursor_val = self.surface( focus ).frame.cursor_state.get( &idx ).copied()
.unwrap_or( current_value.len() );
let safe_cursor_pre = cursor_val.min( current_value.len() );
if safe_cursor_pre >= current_value.len() { return; }
@@ -108,7 +108,7 @@ impl<A: App> AppData<A>
// post-update displayed value via the `usize::MAX` sentinel —
// see `handle_text_insert` for the reasoning.
let select_on_focus = matches!(
find_handlers( &self.surface( focus ).widget_rects, idx ),
find_handlers( &self.surface( focus ).frame.widget_rects, idx ),
Some( WidgetHandlers::TextEdit { select_on_focus: true, .. } ),
);
let next_cursor = if select_on_focus { usize::MAX } else { safe_cursor_pre };
@@ -116,13 +116,13 @@ impl<A: App> AppData<A>
let ss = self.surface_mut( focus );
// Cursor stays put — the char to its right is gone, so the
// remaining tail shifts left under it.
*ss.cursor_state.entry( idx ).or_insert( 0 ) = next_cursor;
*ss.selection_anchor.entry( idx ).or_insert( 0 ) = next_cursor;
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = next_cursor;
*ss.frame.selection_anchor.entry( idx ).or_insert( 0 ) = next_cursor;
ss.pending_text_values.insert( idx, new_value.clone() );
ss.request_redraw();
}
let msg = find_handlers( &self.surface( focus ).widget_rects, idx )
let msg = find_handlers( &self.surface( focus ).frame.widget_rects, idx )
.and_then( |h| h.text_change_msg( &new_value ) );
if let Some( m ) = msg
{
@@ -148,13 +148,13 @@ impl<A: App> AppData<A>
{
pending.clone()
} else {
find_handlers( &self.surface( focus ).widget_rects, idx )
find_handlers( &self.surface( focus ).frame.widget_rects, idx )
.and_then( |h| h.current_value() )
.map( |s| s.to_string() )
.unwrap_or_default()
};
let cursor_val = self.surface( focus ).cursor_state.get( &idx ).copied()
let cursor_val = self.surface( focus ).frame.cursor_state.get( &idx ).copied()
.unwrap_or( current_value.len() );
let safe_cursor = cursor_val.min( current_value.len() );
@@ -190,20 +190,20 @@ impl<A: App> AppData<A>
new_value.replace_range( start_byte..end_byte, "" );
let select_on_focus = matches!(
find_handlers( &self.surface( focus ).widget_rects, idx ),
find_handlers( &self.surface( focus ).frame.widget_rects, idx ),
Some( WidgetHandlers::TextEdit { select_on_focus: true, .. } ),
);
let next_cursor = if select_on_focus { usize::MAX } else { start_byte };
{
let ss = self.surface_mut( focus );
*ss.cursor_state.entry( idx ).or_insert( 0 ) = next_cursor;
*ss.selection_anchor.entry( idx ).or_insert( 0 ) = next_cursor;
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = next_cursor;
*ss.frame.selection_anchor.entry( idx ).or_insert( 0 ) = next_cursor;
ss.pending_text_values.insert( idx, new_value.clone() );
ss.request_redraw();
}
let msg = find_handlers( &self.surface( focus ).widget_rects, idx )
let msg = find_handlers( &self.surface( focus ).frame.widget_rects, idx )
.and_then( |h| h.text_change_msg( &new_value ) );
if let Some( m ) = msg
{
@@ -220,7 +220,7 @@ impl<A: App> AppData<A>
// here so the app sees a single text update.
if let Some( new_value ) = self.delete_selection( focus )
{
let msg = find_handlers( &self.surface( focus ).widget_rects, idx )
let msg = find_handlers( &self.surface( focus ).frame.widget_rects, idx )
.and_then( |h| h.text_change_msg( &new_value ) );
if let Some( m ) = msg { self.pending_msgs.push( m ); }
return;
@@ -230,7 +230,7 @@ impl<A: App> AppData<A>
{
pending.clone()
} else {
find_handlers( &self.surface( focus ).widget_rects, idx )
find_handlers( &self.surface( focus ).frame.widget_rects, idx )
.and_then( |h| h.current_value() )
.map( |s| s.to_string() )
.unwrap_or_default()
@@ -238,7 +238,7 @@ impl<A: App> AppData<A>
// Scoped block to release the immutable borrow of `self` (via
// `self.surface( focus )`) before taking a mutable one below.
let cursor_val = self.surface( focus ).cursor_state.get( &idx ).copied()
let cursor_val = self.surface( focus ).frame.cursor_state.get( &idx ).copied()
.unwrap_or( current_value.len() );
if cursor_val == 0 { return; }
let safe_cursor = cursor_val.min( current_value.len() );
@@ -253,19 +253,19 @@ impl<A: App> AppData<A>
// post-update displayed value via the `usize::MAX` sentinel —
// see `handle_text_insert` for the reasoning.
let select_on_focus = matches!(
find_handlers( &self.surface( focus ).widget_rects, idx ),
find_handlers( &self.surface( focus ).frame.widget_rects, idx ),
Some( WidgetHandlers::TextEdit { select_on_focus: true, .. } ),
);
let next_cursor = if select_on_focus { usize::MAX } else { new_cursor };
{
let ss = self.surface_mut( focus );
*ss.cursor_state.entry( idx ).or_insert( 0 ) = next_cursor;
*ss.selection_anchor.entry( idx ).or_insert( 0 ) = next_cursor;
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = next_cursor;
*ss.frame.selection_anchor.entry( idx ).or_insert( 0 ) = next_cursor;
ss.pending_text_values.insert( idx, new_value.clone() );
ss.request_redraw();
}
let msg = find_handlers( &self.surface( focus ).widget_rects, idx )
let msg = find_handlers( &self.surface( focus ).frame.widget_rects, idx )
.and_then( |h| h.text_change_msg( &new_value ) );
if let Some( m ) = msg
{
+23 -23
View File
@@ -87,7 +87,7 @@ impl<A: App> AppData<A>
// already the natural selection unit, so a double-click does
// not need to add a word-bound selection on top.
let select_on_focus = matches!(
find_handlers( &self.surface( focus ).widget_rects, idx ),
find_handlers( &self.surface( focus ).frame.widget_rects, idx ),
Some( WidgetHandlers::TextEdit { select_on_focus: true, .. } ),
);
if select_on_focus { return; }
@@ -97,7 +97,7 @@ impl<A: App> AppData<A>
None => return,
};
if value.is_empty() { return; }
let cursor_pos = self.surface( focus ).cursor_state.get( &idx ).copied().unwrap_or( 0 );
let cursor_pos = self.surface( focus ).frame.cursor_state.get( &idx ).copied().unwrap_or( 0 );
let click_byte = {
let ss = self.surface( focus );
let canvas = match ss.canvas.as_ref() { Some( c ) => c, None => return };
@@ -107,8 +107,8 @@ impl<A: App> AppData<A>
};
let ( start, end ) = word_bounds_at( &value, click_byte );
let ss = self.surface_mut( focus );
ss.selection_anchor.insert( idx, start );
ss.cursor_state.insert( idx, end );
ss.frame.selection_anchor.insert( idx, start );
ss.frame.cursor_state.insert( idx, end );
ss.request_redraw();
}
@@ -124,7 +124,7 @@ impl<A: App> AppData<A>
// this guard the click-to-position below would collapse the
// selection right after `set_focus` produced it.
let select_on_focus = matches!(
find_handlers( &self.surface( focus ).widget_rects, idx ),
find_handlers( &self.surface( focus ).frame.widget_rects, idx ),
Some( WidgetHandlers::TextEdit { select_on_focus: true, .. } ),
);
if select_on_focus { return; }
@@ -133,7 +133,7 @@ impl<A: App> AppData<A>
Some( v ) => v,
None => return,
};
let cursor_pos = self.surface( focus ).cursor_state.get( &idx ).copied().unwrap_or( 0 );
let cursor_pos = self.surface( focus ).frame.cursor_state.get( &idx ).copied().unwrap_or( 0 );
let byte_offset =
{
let ss = self.surface( focus );
@@ -143,8 +143,8 @@ impl<A: App> AppData<A>
)
};
let ss = self.surface_mut( focus );
ss.cursor_state.insert( idx, byte_offset );
ss.selection_anchor.insert( idx, byte_offset );
ss.frame.cursor_state.insert( idx, byte_offset );
ss.frame.selection_anchor.insert( idx, byte_offset );
ss.request_redraw();
}
@@ -158,7 +158,7 @@ impl<A: App> AppData<A>
// short-form field stays whole-selected even if the user
// drags the pointer over the digits.
let select_on_focus = matches!(
find_handlers( &self.surface( focus ).widget_rects, idx ),
find_handlers( &self.surface( focus ).frame.widget_rects, idx ),
Some( WidgetHandlers::TextEdit { select_on_focus: true, .. } ),
);
if select_on_focus { return; }
@@ -167,7 +167,7 @@ impl<A: App> AppData<A>
Some( v ) => v,
None => return,
};
let cursor_pos = self.surface( focus ).cursor_state.get( &idx ).copied().unwrap_or( 0 );
let cursor_pos = self.surface( focus ).frame.cursor_state.get( &idx ).copied().unwrap_or( 0 );
let byte_offset =
{
let ss = self.surface( focus );
@@ -177,7 +177,7 @@ impl<A: App> AppData<A>
)
};
let ss = self.surface_mut( focus );
ss.cursor_state.insert( idx, byte_offset );
ss.frame.cursor_state.insert( idx, byte_offset );
ss.request_redraw();
}
@@ -187,8 +187,8 @@ impl<A: App> AppData<A>
pub( crate ) fn collapse_selection_if_any( &mut self, focus: SurfaceFocus ) -> bool
{
let idx = match self.surface( focus ).focused_idx { Some( i ) => i, None => return false };
let cursor = self.surface( focus ).cursor_state.get( &idx ).copied();
let anchor = self.surface( focus ).selection_anchor.get( &idx ).copied();
let cursor = self.surface( focus ).frame.cursor_state.get( &idx ).copied();
let anchor = self.surface( focus ).frame.selection_anchor.get( &idx ).copied();
let ( c, a ) = match ( cursor, anchor )
{
( Some( c ), Some( a ) ) if c != a => ( c, a ),
@@ -196,7 +196,7 @@ impl<A: App> AppData<A>
};
let _ = a;
let ss = self.surface_mut( focus );
ss.selection_anchor.insert( idx, c );
ss.frame.selection_anchor.insert( idx, c );
ss.request_redraw();
true
}
@@ -207,8 +207,8 @@ impl<A: App> AppData<A>
{
let ( idx, value ) = match self.focused_text_value( focus ) { Some( v ) => v, None => return };
let ss = self.surface_mut( focus );
*ss.cursor_state.entry( idx ).or_insert( 0 ) = value.len();
*ss.selection_anchor.entry( idx ).or_insert( 0 ) = 0;
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = value.len();
*ss.frame.selection_anchor.entry( idx ).or_insert( 0 ) = 0;
ss.request_redraw();
}
@@ -218,9 +218,9 @@ impl<A: App> AppData<A>
pub( crate ) fn focused_selection_text( &self, focus: SurfaceFocus ) -> Option<String>
{
let ( idx, value ) = self.focused_text_value( focus )?;
let cursor = self.surface( focus ).cursor_state.get( &idx ).copied()
let cursor = self.surface( focus ).frame.cursor_state.get( &idx ).copied()
.unwrap_or( value.len() ).min( value.len() );
let anchor = self.surface( focus ).selection_anchor.get( &idx ).copied()
let anchor = self.surface( focus ).frame.selection_anchor.get( &idx ).copied()
.unwrap_or( cursor ).min( value.len() );
if anchor == cursor { return None; }
let s = cursor.min( anchor );
@@ -236,9 +236,9 @@ impl<A: App> AppData<A>
pub( crate ) fn delete_selection( &mut self, focus: SurfaceFocus ) -> Option<String>
{
let ( idx, value ) = self.focused_text_value( focus )?;
let cursor = self.surface( focus ).cursor_state.get( &idx ).copied()
let cursor = self.surface( focus ).frame.cursor_state.get( &idx ).copied()
.unwrap_or( value.len() ).min( value.len() );
let anchor = self.surface( focus ).selection_anchor.get( &idx ).copied()
let anchor = self.surface( focus ).frame.selection_anchor.get( &idx ).copied()
.unwrap_or( cursor ).min( value.len() );
if anchor == cursor { return None; }
let s = cursor.min( anchor );
@@ -249,13 +249,13 @@ impl<A: App> AppData<A>
// post-update displayed value via the `usize::MAX` sentinel —
// see `handle_text_insert` for the reasoning.
let select_on_focus = matches!(
find_handlers( &self.surface( focus ).widget_rects, idx ),
find_handlers( &self.surface( focus ).frame.widget_rects, idx ),
Some( WidgetHandlers::TextEdit { select_on_focus: true, .. } ),
);
let next_cursor = if select_on_focus { usize::MAX } else { s };
let ss = self.surface_mut( focus );
*ss.cursor_state.entry( idx ).or_insert( 0 ) = next_cursor;
*ss.selection_anchor.entry( idx ).or_insert( 0 ) = next_cursor;
*ss.frame.cursor_state.entry( idx ).or_insert( 0 ) = next_cursor;
*ss.frame.selection_anchor.entry( idx ).or_insert( 0 ) = next_cursor;
ss.pending_text_values.insert( idx, new_value.clone() );
ss.request_redraw();
Some( new_value )
+1 -1
View File
@@ -31,7 +31,7 @@ impl<A: App> AppData<A>
pub( crate ) fn arm_tooltip( &mut self, focus: SurfaceFocus, flat_idx: usize )
{
let Some( ss ) = self.try_surface( focus ) else { return };
let Some( w ) = crate::tree::find_widget( &ss.widget_rects, flat_idx ) else { return };
let Some( w ) = crate::tree::find_widget( &ss.frame.widget_rects, flat_idx ) else { return };
let Some( text ) = w.tooltip.clone() else
{
self.tooltip_pending = None;