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

@@ -51,11 +51,17 @@ impl GlesCanvas
r_x1 <= clip.x || c_x1 <= r_x0 || r_y1 <= clip.y || c_y1 <= r_y0
}
/// Fill an arbitrary vector path (commands in surface coordinates) with a
/// solid colour. CPU fallback: rasterised with tiny-skia into a bbox-sized
/// pixmap and blitted as a transient texture — there is no GPU path shader.
pub fn fill_path( &mut self, cmds: &[ PathCmd ], color: Color )
{
self.rasterise_path( cmds, color, None );
}
/// Stroke an arbitrary vector path (commands in surface coordinates) with a
/// centered stroke of `width` px. Same tiny-skia-into-texture CPU fallback as
/// [`Self::fill_path`].
pub fn stroke_path( &mut self, cmds: &[ PathCmd ], color: Color, width: f32 )
{
self.rasterise_path( cmds, color, Some( width ) );
@@ -114,6 +120,10 @@ impl GlesCanvas
unsafe { self.gl.delete_texture( tex ); }
}
/// Fill `rect` with a solid colour, with per-corner rounding from `corners`.
/// Coverage (including the rounded corners) comes from an SDF in the rect
/// shader; `color.a` is multiplied by `global_alpha`. Culled early when the
/// rect falls entirely outside the active scissor.
pub fn fill_rect( &mut self, rect: Rect, color: Color, corners: Corners )
{
if self.rect_culled( rect, 1.0 ) { return; }
@@ -124,13 +134,7 @@ impl GlesCanvas
// stay anchored to the original rect — `u_pad` lets the shader
// remap `v_uv` from the larger quad back into rect-local space.
let pad = 1.0_f32;
let expanded = Rect
{
x: rect.x - pad,
y: rect.y - pad,
width: rect.width + 2.0 * pad,
height: rect.height + 2.0 * pad,
};
let expanded = rect.expand( pad );
let mvp = ortho_rect( self.width, self.height, expanded );
let alpha = color.a * self.global_alpha;
// SAFETY: see module doc. `u_rect_stroke = 0.0` triggers the fill
@@ -221,13 +225,7 @@ impl GlesCanvas
self.activate_target();
// See fill_rect for the rationale on the 1 px quad pad.
let pad = 1.0_f32;
let expanded = Rect
{
x: rect.x - pad,
y: rect.y - pad,
width: rect.width + 2.0 * pad,
height: rect.height + 2.0 * pad,
};
let expanded = rect.expand( pad );
let mvp = ortho_rect( self.width, self.height, expanded );
// SAFETY: see module doc. `tex` is the cached LUT for `g.stops`
// produced by `ensure_lut_texture` above (RGBA8, sampler unit 0);
@@ -269,13 +267,7 @@ impl GlesCanvas
self.activate_target();
// See fill_rect for the rationale on the 1 px quad pad.
let pad = 1.0_f32;
let expanded = Rect
{
x: rect.x - pad,
y: rect.y - pad,
width: rect.width + 2.0 * pad,
height: rect.height + 2.0 * pad,
};
let expanded = rect.expand( pad );
let mvp = ortho_rect( self.width, self.height, expanded );
// SAFETY: see module doc. Same LUT contract as the linear path
// above. `g.center` and `g.radius` are finite fractional values
@@ -422,22 +414,10 @@ impl GlesCanvas
// active scissor is culled before the shader runs, so the
// snapshot only needs to cover the intersection.
let pad = 1.0_f32;
let snap_rect = Rect
{
x: target.x - pad,
y: target.y - pad,
width: target.width + 2.0 * pad,
height: target.height + 2.0 * pad,
};
let snap_rect = target.expand( pad );
self.snapshot_fbo_region_tight( snap_rect );
self.activate_target();
let expanded = Rect
{
x: target.x - pad,
y: target.y - pad,
width: target.width + 2.0 * pad,
height: target.height + 2.0 * pad,
};
let expanded = target.expand( pad );
let mvp = ortho_rect( self.width, self.height, expanded );
let aux_tex = self.aux_a.expect( "snapshotted" ).1;
// SAFETY: see module doc. `aux_tex` was just populated by
@@ -475,13 +455,7 @@ impl GlesCanvas
// of terminating at the surface rect. Same rationale as
// fill_rect. `u_size` / `u_radii` stay anchored to `target`.
let pad = 1.0_f32;
let expanded = Rect
{
x: target.x - pad,
y: target.y - pad,
width: target.width + 2.0 * pad,
height: target.height + 2.0 * pad,
};
let expanded = target.expand( pad );
let mvp = ortho_rect( self.width, self.height, expanded );
// SAFETY: see module doc. We swap the global blend state for the
// duration of one draw and restore the canvas-wide default
@@ -550,13 +524,7 @@ impl GlesCanvas
// would shift the zero-line outward and, in the circle case
// (radius = size/2), turn the result into a rounded square.
let pad = half + 1.0;
let expanded = Rect
{
x: rect.x - pad,
y: rect.y - pad,
width: rect.width + 2.0 * pad,
height: rect.height + 2.0 * pad,
};
let expanded = rect.expand( pad );
let mvp = ortho_rect( self.width, self.height, expanded );
let alpha = color.a * self.global_alpha;
// SAFETY: see module doc. `u_rect_stroke = width > 0.0` triggers