Bump to 0.2.0: SW/GLES paint parity, shared font resolution, FrameState refactor, docs and packaging fixes
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:
@@ -23,7 +23,7 @@ impl<A: App> AppData<A>
|
||||
pos: Point,
|
||||
) -> bool
|
||||
{
|
||||
let toggle_msg = self.surface( focus ).widget_rects.iter()
|
||||
let toggle_msg = self.surface( focus ).frame.widget_rects.iter()
|
||||
.find( |w| w.flat_idx == idx )
|
||||
.and_then( |w| match &w.handlers
|
||||
{
|
||||
|
||||
@@ -21,10 +21,10 @@ impl<A: App> AppData<A>
|
||||
// Find the topmost scroll that has a navigable item list.
|
||||
let scroll_meta = {
|
||||
let ss = self.surface( focus );
|
||||
ss.scroll_rects.iter().rev()
|
||||
ss.frame.scroll_rects.iter().rev()
|
||||
.find_map( |( rect, idx, _ax )|
|
||||
{
|
||||
ss.scroll_navigable_items.get( idx )
|
||||
ss.frame.scroll_navigable_items.get( idx )
|
||||
.filter( |list| !list.is_empty() )
|
||||
.map( |list| ( *rect, *idx, list.clone() ) )
|
||||
} )
|
||||
@@ -50,7 +50,7 @@ impl<A: App> AppData<A>
|
||||
// list-style); the X offset is preserved as-is.
|
||||
let viewport_h = scroll_rect.height;
|
||||
let ( current_x, current_y ) = self.surface( focus )
|
||||
.scroll_offsets.get( &scroll_idx )
|
||||
.frame.scroll_offsets.get( &scroll_idx )
|
||||
.copied().unwrap_or( ( 0.0, 0.0 ) );
|
||||
let new_y = if content_y < current_y
|
||||
{
|
||||
@@ -67,7 +67,7 @@ impl<A: App> AppData<A>
|
||||
|
||||
let ss = self.surface_mut( focus );
|
||||
ss.hovered_idx = Some( new_idx );
|
||||
ss.scroll_offsets.insert( scroll_idx, ( current_x, new_y ) );
|
||||
ss.frame.scroll_offsets.insert( scroll_idx, ( current_x, new_y ) );
|
||||
ss.request_redraw();
|
||||
true
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ impl<A: App> AppData<A>
|
||||
{
|
||||
let focused = self.surface( focus ).focused_idx;
|
||||
let is_text = focused.and_then( |idx|
|
||||
find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||
find_handlers( &self.surface( focus ).frame.widget_rects, idx )
|
||||
.map( |h| h.is_text_input() ) ).unwrap_or( false );
|
||||
if is_text { self.handle_select_all( focus ); }
|
||||
}
|
||||
@@ -23,7 +23,7 @@ impl<A: App> AppData<A>
|
||||
{
|
||||
let focused = self.surface( focus ).focused_idx;
|
||||
let is_text = focused.and_then( |idx|
|
||||
find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||
find_handlers( &self.surface( focus ).frame.widget_rects, idx )
|
||||
.map( |h| h.is_text_input() ) ).unwrap_or( false );
|
||||
if is_text { self.handle_copy( focus ); }
|
||||
}
|
||||
@@ -32,7 +32,7 @@ impl<A: App> AppData<A>
|
||||
{
|
||||
let focused = self.surface( focus ).focused_idx;
|
||||
let is_text = focused.and_then( |idx|
|
||||
find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||
find_handlers( &self.surface( focus ).frame.widget_rects, idx )
|
||||
.map( |h| h.is_text_input() ) ).unwrap_or( false );
|
||||
if is_text { self.handle_cut( focus ); }
|
||||
}
|
||||
@@ -41,7 +41,7 @@ impl<A: App> AppData<A>
|
||||
{
|
||||
let focused = self.surface( focus ).focused_idx;
|
||||
let is_text = focused.and_then( |idx|
|
||||
find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||
find_handlers( &self.surface( focus ).frame.widget_rects, idx )
|
||||
.map( |h| h.is_text_input() ) ).unwrap_or( false );
|
||||
if is_text { self.handle_paste( focus ); }
|
||||
}
|
||||
@@ -57,7 +57,7 @@ impl<A: App> AppData<A>
|
||||
else
|
||||
{
|
||||
let ss = self.surface( focus );
|
||||
let next_idx = next_focusable_index( &ss.widget_rects, ss.focused_idx, reverse );
|
||||
let next_idx = next_focusable_index( &ss.frame.widget_rects, ss.focused_idx, reverse );
|
||||
if let Some( next_idx ) = next_idx
|
||||
{
|
||||
self.set_focus( focus, Some( next_idx ), qh );
|
||||
@@ -84,7 +84,7 @@ impl<A: App> AppData<A>
|
||||
} else if self.collapse_selection_if_any( focus )
|
||||
{
|
||||
// Selection collapsed — keep focus, no app msg.
|
||||
} else if let Some( msg ) = self.surface( focus ).widget_rects.iter()
|
||||
} else if let Some( msg ) = self.surface( focus ).frame.widget_rects.iter()
|
||||
.rev()
|
||||
.find_map( |w| w.handlers.escape_msg() )
|
||||
{
|
||||
|
||||
@@ -47,7 +47,7 @@ impl<A: App> AppData<A>
|
||||
// literal `\n` into the buffer instead of submitting, so
|
||||
// the user can compose paragraphs.
|
||||
let is_multi = focused.and_then( |idx|
|
||||
find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||
find_handlers( &self.surface( focus ).frame.widget_rects, idx )
|
||||
.map( |h| h.is_multiline_text_input() ) ).unwrap_or( false );
|
||||
if is_multi
|
||||
{
|
||||
@@ -57,7 +57,7 @@ impl<A: App> AppData<A>
|
||||
let mut handled = false;
|
||||
if let Some( idx ) = target
|
||||
{
|
||||
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.submit_msg().or_else( || h.press_msg() ) );
|
||||
if let Some( m ) = msg
|
||||
{
|
||||
@@ -79,7 +79,7 @@ impl<A: App> AppData<A>
|
||||
{
|
||||
let focused = self.surface( focus ).focused_idx;
|
||||
let is_text = focused.and_then( |idx|
|
||||
find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||
find_handlers( &self.surface( focus ).frame.widget_rects, idx )
|
||||
.map( |h| h.is_text_input() ) ).unwrap_or( false );
|
||||
let reverse = event.keysym == Keysym::Up;
|
||||
let extend = self.shift_pressed;
|
||||
@@ -106,7 +106,7 @@ impl<A: App> AppData<A>
|
||||
{
|
||||
let focused = self.surface( focus ).focused_idx;
|
||||
let is_text = focused.and_then( |idx|
|
||||
find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||
find_handlers( &self.surface( focus ).frame.widget_rects, idx )
|
||||
.map( |h| h.is_text_input() ) ).unwrap_or( false );
|
||||
let extend = self.shift_pressed;
|
||||
let intercepted = self.app.on_key_with_modifiers( event.keysym, self.ctrl_pressed, self.shift_pressed );
|
||||
@@ -129,7 +129,7 @@ impl<A: App> AppData<A>
|
||||
{
|
||||
let focused = self.surface( focus ).focused_idx;
|
||||
let is_text = focused.and_then( |idx|
|
||||
find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||
find_handlers( &self.surface( focus ).frame.widget_rects, idx )
|
||||
.map( |h| h.is_text_input() ) ).unwrap_or( false );
|
||||
if is_text { self.handle_cursor_home( focus, self.shift_pressed ); }
|
||||
}
|
||||
@@ -138,7 +138,7 @@ impl<A: App> AppData<A>
|
||||
{
|
||||
let focused = self.surface( focus ).focused_idx;
|
||||
let is_text = focused.and_then( |idx|
|
||||
find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||
find_handlers( &self.surface( focus ).frame.widget_rects, idx )
|
||||
.map( |h| h.is_text_input() ) ).unwrap_or( false );
|
||||
if is_text { self.handle_cursor_end( focus, self.shift_pressed ); }
|
||||
}
|
||||
@@ -148,7 +148,7 @@ impl<A: App> AppData<A>
|
||||
let focused = self.surface( focus ).focused_idx;
|
||||
if let Some( idx ) = focused
|
||||
{
|
||||
let press = find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||
let press = find_handlers( &self.surface( focus ).frame.widget_rects, idx )
|
||||
.and_then( |h| h.press_msg() );
|
||||
if let Some( msg ) = press
|
||||
{
|
||||
|
||||
@@ -33,12 +33,12 @@ impl<A: App> AppData<A>
|
||||
// Hover tracking — pointer-only (touch has no hover).
|
||||
// Runs before the gesture motion so the cache-dirty
|
||||
// below picks up any hover-dependent redraw request.
|
||||
let new_hover = find_widget_at( &self.surface( focus ).widget_rects, pp );
|
||||
let new_hover = find_widget_at( &self.surface( focus ).frame.widget_rects, pp );
|
||||
let old_hover = self.surface( focus ).hovered_idx;
|
||||
if new_hover != old_hover
|
||||
{
|
||||
let redraw = hover_affects_paint( &self.surface( focus ).widget_rects, old_hover )
|
||||
|| hover_affects_paint( &self.surface( focus ).widget_rects, new_hover );
|
||||
let redraw = hover_affects_paint( &self.surface( focus ).frame.widget_rects, old_hover )
|
||||
|| hover_affects_paint( &self.surface( focus ).frame.widget_rects, new_hover );
|
||||
{
|
||||
let ss = self.surface_mut( focus );
|
||||
ss.hovered_idx = new_hover;
|
||||
@@ -115,7 +115,7 @@ impl<A: App> AppData<A>
|
||||
let outcome =
|
||||
{
|
||||
let ss = self.surface_mut( focus );
|
||||
ss.gesture.on_move( pp, &ss.widget_rects, &mut ss.scroll_offsets, &swipe, global_drag )
|
||||
ss.gesture.on_move( pp, &ss.frame.widget_rects, &mut ss.frame.scroll_offsets, &swipe, global_drag )
|
||||
};
|
||||
self.apply_move_outcome( focus, outcome );
|
||||
|
||||
@@ -129,7 +129,7 @@ impl<A: App> AppData<A>
|
||||
let pressed_text = self.surface( focus ).gesture.pressed_idx
|
||||
.and_then( |idx|
|
||||
{
|
||||
let is_text = find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||
let is_text = find_handlers( &self.surface( focus ).frame.widget_rects, idx )
|
||||
.map( |h| h.is_text_input() ).unwrap_or( false );
|
||||
if is_text { Some( idx ) } else { None }
|
||||
} );
|
||||
|
||||
@@ -43,9 +43,9 @@ impl<A: App> AppData<A>
|
||||
let pos = self.surface( focus ).to_physical( event.position.0, event.position.1 );
|
||||
self.pointer_pos = pos;
|
||||
self.app.on_pointer_move( pos.x, pos.y );
|
||||
let hit_idx = find_widget_at( &self.surface( focus ).widget_rects, pos );
|
||||
let hit_idx = find_widget_at( &self.surface( focus ).frame.widget_rects, pos );
|
||||
let lp_msg = hit_idx.and_then( |idx|
|
||||
find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||
find_handlers( &self.surface( focus ).frame.widget_rects, idx )
|
||||
.and_then( |h| h.long_press_msg() ) );
|
||||
if let Some( msg ) = lp_msg
|
||||
{
|
||||
@@ -53,7 +53,7 @@ impl<A: App> AppData<A>
|
||||
self.surface_mut( focus ).request_redraw();
|
||||
} else {
|
||||
let is_text = hit_idx.and_then( |idx|
|
||||
find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||
find_handlers( &self.surface( focus ).frame.widget_rects, idx )
|
||||
.map( |h| h.is_text_input() ) ).unwrap_or( false );
|
||||
if is_text
|
||||
{
|
||||
@@ -170,7 +170,7 @@ impl<A: App> AppData<A>
|
||||
let outcome =
|
||||
{
|
||||
let ss = self.surface_mut( focus );
|
||||
let result = ss.gesture.on_press( pos, &ss.widget_rects, &ss.scroll_rects );
|
||||
let result = ss.gesture.on_press( pos, &ss.frame.widget_rects, &ss.frame.scroll_rects );
|
||||
// Mark this gesture as mouse-driven so the
|
||||
// gesture machine's 6 px stray-cancel skips
|
||||
// the drag-start / long-press slots — mouse
|
||||
@@ -199,7 +199,7 @@ impl<A: App> AppData<A>
|
||||
if let Some( idx ) = outcome.hit_idx
|
||||
{
|
||||
let immediate = {
|
||||
let handlers = find_handlers( &self.surface( focus ).widget_rects, idx );
|
||||
let handlers = find_handlers( &self.surface( focus ).frame.widget_rects, idx );
|
||||
if matches!( handlers, Some( WidgetHandlers::Button { repeating: true, .. } ) )
|
||||
{
|
||||
handlers.and_then( |h| h.press_msg() )
|
||||
@@ -222,7 +222,7 @@ impl<A: App> AppData<A>
|
||||
// under the cursor instead of just positioning.
|
||||
if let Some( idx ) = outcome.hit_idx
|
||||
{
|
||||
let is_text = find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||
let is_text = find_handlers( &self.surface( focus ).frame.widget_rects, idx )
|
||||
.map( |h| h.is_text_input() ).unwrap_or( false );
|
||||
if is_text
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ impl<A: App> AppData<A>
|
||||
{
|
||||
let ss = self.surface_mut( focus );
|
||||
ss.needs_redraw = true;
|
||||
ss.gesture.on_release( pos, &ss.widget_rects, &swipe, global_drag )
|
||||
ss.gesture.on_release( pos, &ss.frame.widget_rects, &swipe, global_drag )
|
||||
};
|
||||
self.apply_release_events( focus, events_out );
|
||||
// Cancel any held-button repeat — the press is
|
||||
|
||||
@@ -33,7 +33,7 @@ impl<A: App> AppData<A>
|
||||
let scroll_hit =
|
||||
{
|
||||
let ss = self.surface( focus );
|
||||
ss.scroll_rects.iter()
|
||||
ss.frame.scroll_rects.iter()
|
||||
.find( |( r, _, _ )| r.contains( pos ) )
|
||||
.map( |( _, idx, ax )| ( *idx, *ax ) )
|
||||
};
|
||||
@@ -47,7 +47,7 @@ impl<A: App> AppData<A>
|
||||
let step_x = horizontal.absolute as f32 * multiplier;
|
||||
let step_y = vertical.absolute as f32 * multiplier;
|
||||
let ss = self.surface_mut( focus );
|
||||
let entry = ss.scroll_offsets.entry( scroll_idx ).or_insert( ( 0.0, 0.0 ) );
|
||||
let entry = ss.frame.scroll_offsets.entry( scroll_idx ).or_insert( ( 0.0, 0.0 ) );
|
||||
// Wheels report on a single axis at a time; route to
|
||||
// whichever axis the viewport allows. A pure horizontal
|
||||
// viewport translates a vertical wheel into horizontal
|
||||
|
||||
@@ -51,7 +51,7 @@ impl<A: App> AppData<A>
|
||||
let token = self.loop_handle.insert_source( timer, move |_, _, data: &mut AppData<A>|
|
||||
{
|
||||
let live_msg = crate::tree::find_handlers(
|
||||
&data.surface( focus ).widget_rects,
|
||||
&data.surface( focus ).frame.widget_rects,
|
||||
idx,
|
||||
)
|
||||
.and_then( |h| h.press_msg() );
|
||||
|
||||
@@ -109,7 +109,7 @@ impl<A: App> TouchHandler for AppData<A>
|
||||
let outcome =
|
||||
{
|
||||
let ss = self.surface_mut( focus );
|
||||
let result = ss.gesture.on_press( pos, &ss.widget_rects, &ss.scroll_rects );
|
||||
let result = ss.gesture.on_press( pos, &ss.frame.widget_rects, &ss.frame.scroll_rects );
|
||||
ss.needs_redraw = true;
|
||||
result
|
||||
};
|
||||
@@ -122,7 +122,7 @@ impl<A: App> TouchHandler for AppData<A>
|
||||
if let Some( idx ) = outcome.hit_idx
|
||||
{
|
||||
let immediate = {
|
||||
let handlers = find_handlers( &self.surface( focus ).widget_rects, idx );
|
||||
let handlers = find_handlers( &self.surface( focus ).frame.widget_rects, idx );
|
||||
if matches!( handlers, Some( crate::widget::WidgetHandlers::Button { repeating: true, .. } ) )
|
||||
{
|
||||
handlers.and_then( |h| h.press_msg() )
|
||||
@@ -138,7 +138,7 @@ impl<A: App> TouchHandler for AppData<A>
|
||||
// and double-tap selects the word under the press.
|
||||
if let Some( idx ) = outcome.hit_idx
|
||||
{
|
||||
let is_text = find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||
let is_text = find_handlers( &self.surface( focus ).frame.widget_rects, idx )
|
||||
.map( |h| h.is_text_input() ).unwrap_or( false );
|
||||
if is_text
|
||||
{
|
||||
@@ -208,7 +208,7 @@ impl<A: App> TouchHandler for AppData<A>
|
||||
let ss = self.surface_mut( focus );
|
||||
ss.needs_redraw = true;
|
||||
ss.primary_touch_id = None;
|
||||
ss.gesture.on_release( pos, &ss.widget_rects, &swipe, global_drag )
|
||||
ss.gesture.on_release( pos, &ss.frame.widget_rects, &swipe, global_drag )
|
||||
};
|
||||
self.apply_release_events( focus, events_out );
|
||||
self.stop_button_repeat();
|
||||
@@ -250,7 +250,7 @@ impl<A: App> TouchHandler for AppData<A>
|
||||
let outcome =
|
||||
{
|
||||
let ss = self.surface_mut( focus );
|
||||
ss.gesture.on_move( pp, &ss.widget_rects, &mut ss.scroll_offsets, &swipe, global_drag )
|
||||
ss.gesture.on_move( pp, &ss.frame.widget_rects, &mut ss.frame.scroll_offsets, &swipe, global_drag )
|
||||
};
|
||||
self.apply_move_outcome( focus, outcome );
|
||||
|
||||
@@ -258,7 +258,7 @@ impl<A: App> TouchHandler for AppData<A>
|
||||
let pressed_text = self.surface( focus ).gesture.pressed_idx
|
||||
.and_then( |idx|
|
||||
{
|
||||
let is_text = find_handlers( &self.surface( focus ).widget_rects, idx )
|
||||
let is_text = find_handlers( &self.surface( focus ).frame.widget_rects, idx )
|
||||
.map( |h| h.is_text_input() ).unwrap_or( false );
|
||||
if is_text { Some( idx ) } else { None }
|
||||
} );
|
||||
|
||||
Reference in New Issue
Block a user