Bump to 0.2.0: SW/GLES paint parity, shared font resolution, FrameState refactor, docs and packaging fixes
Some checks failed
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

View File

@@ -27,8 +27,9 @@
//! * [`text`] — `SoftwareCanvas::{draw_text, measure_text}`.
//! * [`image`] — `SoftwareCanvas::{draw_image_data,
//! write_to_wayland_buf}`.
//! * [`helpers`] — free functions: `build_rounded_rect`,
//! `find_font`, `find_font_opt`, `SYSTEM_FONT_CANDIDATES`.
//! * [`helpers`] — free functions: `build_ts_path`,
//! `build_rounded_rect`. System-font resolution lives in
//! `crate::system_fonts`.
use std::cell::Cell;
use std::sync::Arc;
@@ -120,7 +121,7 @@ pub struct SoftwareCanvas
/// Kept as the default fallback so widgets that do not yet ask for a
/// specific family through [`SoftwareCanvas::font_for`] keep
/// working. Populated from
/// [`crate::render::helpers::find_font`] at construction time.
/// `crate::system_fonts::default_handle` at construction time.
pub font: Arc<Font>,
/// Raw bytes of the default font. Kept alongside `font` so the
/// HarfBuzz shaper (rustybuzz) can be invoked without re-reading
@@ -869,3 +870,65 @@ mod clip_path_tests
assert_eq!( corner.alpha(), 0, "bbox corner outside the triangle must stay clear" );
}
}
#[ cfg( test ) ]
mod pixel_snap_tests
{
// The GLES backend rounds glyph pen positions and image destinations to
// integer pixels for crisp 1:1 sampling; these check the software backend
// now matches that snapping so both backends place content identically.
use super::Canvas;
use crate::types::{ Color, Rect };
fn red_4x4() -> Vec<u8>
{
[ 255u8, 0, 0, 255 ].repeat( 16 )
}
#[ test ]
fn image_dest_below_half_snaps_to_the_same_pixels_as_integer_dest()
{
let red = red_4x4();
let mut a = Canvas::new( 32, 32 );
a.draw_image_data( &red, 4, 4, Rect { x: 5.0, y: 5.0, width: 4.0, height: 4.0 }, 1.0 );
let mut b = Canvas::new( 32, 32 );
b.draw_image_data( &red, 4, 4, Rect { x: 5.4, y: 5.4, width: 4.0, height: 4.0 }, 1.0 );
let Canvas::Software( sa ) = &a else { panic!( "software canvas" ) };
let Canvas::Software( sb ) = &b else { panic!( "software canvas" ) };
assert_eq!( sa.pixmap.data(), sb.pixmap.data(), "a sub-half fractional dest snaps to the integer origin" );
assert!( sa.pixmap.pixel( 5, 5 ).unwrap().red() > 200, "the block lands on pixel (5,5)" );
assert_eq!( sa.pixmap.pixel( 4, 4 ).unwrap().alpha(), 0, "pixel (4,4) is outside the snapped block" );
}
#[ test ]
fn image_dest_at_or_above_half_rounds_to_the_next_pixel()
{
let red = red_4x4();
let mut c = Canvas::new( 32, 32 );
c.draw_image_data( &red, 4, 4, Rect { x: 5.6, y: 5.6, width: 4.0, height: 4.0 }, 1.0 );
let Canvas::Software( sc ) = &c else { panic!( "software canvas" ) };
assert!( sc.pixmap.pixel( 6, 6 ).unwrap().red() > 200, "round(5.6)=6 moves the block one pixel" );
assert_eq!( sc.pixmap.pixel( 5, 5 ).unwrap().alpha(), 0, "pixel (5,5) is now clear" );
}
#[ test ]
fn text_pen_position_is_rounded_not_truncated()
{
let draw = |x: f32| -> Vec<u8>
{
let mut c = Canvas::new( 64, 64 );
c.draw_text( "l", x, 40.0, 32.0, Color::rgba( 1.0, 1.0, 1.0, 1.0 ) );
let Canvas::Software( sc ) = &c else { panic!( "software canvas" ) };
sc.pixmap.data().to_vec()
};
let at_int = draw( 20.0 );
let at_low = draw( 20.4 );
let at_high = draw( 20.6 );
assert!( at_int.iter().any( |&b| b != 0 ), "the glyph must paint some pixels" );
assert_eq!( at_int, at_low, "x and x+0.4 round to the same pixel column" );
assert_ne!( at_int, at_high, "x+0.6 rounds up to the next column" );
}
}