responsive fluid/physical scaling, widget-API stabilization, and perf guardrails
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

Responsive scaling. ltk now offers two first-class ways to size a UI so it adapts across screens, chosen per process via `WidgetScaling { Fluid, Physical }` (`set_widget_scaling` / `widget_scaling`, default `Fluid`). Fluid sizing (`Length::fluid( px )`) makes a design pixel a proportion of the surface's smaller side, calibrated against a reference width (`set_fluid_reference` / `fluid_reference`, 412 px default) and bounded by `FLUID_MIN` / `FLUID_MAX`; physical sizing (`Length::dp( px )`) is a constant-physical-size pixel scaled by display density (`set_density` / `density`). `Length` gains `orient( portrait, landscape )` — resolve one value in portrait, another in landscape — plus `widget( px )`, which picks fluid or dp per the active mode. Canvas exposes `geom_px` (geometry, resolved in physical layout space) and `font_px` (font size, bridging logical / physical per mode) so widgets and apps share one resolution path. Note the rename: `set_design_reference` / `design_reference` became `set_fluid_reference` / `fluid_reference`, and `Length::dp` changed meaning — the old surface-proportional behaviour now lives on `Length::fluid`.
Widgets. Every stock widget resolves its default geometry and font through the widget-scaling mode instead of frozen pixels, so a whole UI scales coherently without per-call units. New size builders where they were missing: `button` gains `font_size` / `height`, `text_edit` gains `height` / `font_size_fluid`, `separator` gains `pad_v`, and assorted widgets accept a `Length` where they previously took only `f32`.
Overlays. `OverlaySpec::size` is now `( Length, Length )` instead of `( u32, u32 )`, resolved against the main surface when the overlay is materialized, so overlays can scale with the display; `Length::px( … )` reproduces the old fixed sizing.
API stabilization (toward 1.0). Widget struct fields are now `pub( crate )` — they are configured through builders, not field access — except the value / state types apps genuinely read or construct (`Time`, `Date`, `ComboState`), which stay public. The internal `test_support` helpers move behind a `test-support` Cargo feature (off by default, so third-party builds never see them; ltk's own `make test` enables it). `Separator` drops its `0.0`-means-mode sentinel for `Option<Length>`, so an explicit `pad_v( 0.0 )` is a real flush divider distinct from the mode-following default.
Performance guardrails. Opt-in diagnostics via `LTK_PERF_WARN=1` warn about stuck animations, sustained software-render animation, and low `poll_interval`; software-rendered animation is capped near 30 Hz to spare CPU on machines that fall back off EGL. Apps can override the cap with `App::cap_software_animation`.
Docs and build. The two scaling modes are documented in README, onboarding and architecture, with the earlier gradient / backdrop doc drift cleaned up. The Makefile now ships the `locales/` directory into the packaged crate (fixing i18n keys rendering raw for downstreams), builds the new `responsive` example, and runs tests with `--features test-support`.
This commit is contained in:
2026-07-07 17:40:33 +02:00
parent d4d7ee742e
commit ce893ac776
83 changed files with 1850 additions and 526 deletions
+29 -23
View File
@@ -38,14 +38,14 @@ pub struct Toggle<Msg: Clone>
{
/// Current on / off state. Drawn from this field every frame; the
/// runtime never mutates it.
pub value: bool,
pub( crate ) value: bool,
/// Message emitted on activation. `None` leaves the toggle inert (it
/// still renders and takes focus, but does nothing on press).
pub on_toggle: Option<Msg>,
pub( crate ) on_toggle: Option<Msg>,
/// Optional label drawn to the left of the track.
pub label: Option<String>,
pub( crate ) label: Option<String>,
/// Optional stable identifier for focus management.
pub id: Option<WidgetId>,
pub( crate ) id: Option<WidgetId>,
}
impl<Msg: Clone> Toggle<Msg>
@@ -92,14 +92,15 @@ impl<Msg: Clone> Toggle<Msg>
/// label is set. Height is the theme-defined row height.
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
{
let track_w = canvas.geom_px( theme::TRACK_W );
let w = if let Some( ref label ) = self.label
{
let text_w = canvas.measure_text( label, theme::FONT_SIZE );
( text_w + theme::GAP + theme::TRACK_W ).min( max_width )
let text_w = canvas.measure_text( label, canvas.font_px( theme::FONT_SIZE ) );
( text_w + canvas.geom_px( theme::GAP ) + track_w ).min( max_width )
} else {
theme::TRACK_W.min( max_width )
track_w.min( max_width )
};
( w, theme::HEIGHT )
( w, canvas.geom_px( theme::HEIGHT ) )
}
/// Bounding box of everything painted at `rect` across all states. The
@@ -114,43 +115,48 @@ impl<Msg: Clone> Toggle<Msg>
pub fn draw( &self, canvas: &mut Canvas, rect: Rect, focused: bool )
{
let track_w = canvas.geom_px( theme::TRACK_W );
let track_h = canvas.geom_px( theme::TRACK_H );
let thumb_size = canvas.geom_px( theme::THUMB_SIZE );
let track_x = if let Some( ref label ) = self.label
{
let text_y = rect.y + ( rect.height + theme::FONT_SIZE ) / 2.0 - 2.0;
canvas.draw_text( label, rect.x, text_y, theme::FONT_SIZE, theme::label_color() );
rect.x + rect.width - theme::TRACK_W
let fs = canvas.font_px( theme::FONT_SIZE );
let text_y = rect.y + ( rect.height + fs ) / 2.0 - 2.0;
canvas.draw_text( label, rect.x, text_y, fs, theme::label_color() );
rect.x + rect.width - track_w
} else {
rect.x + ( rect.width - theme::TRACK_W ) / 2.0
rect.x + ( rect.width - track_w ) / 2.0
};
let track_y = rect.y + ( rect.height - theme::TRACK_H ) / 2.0;
let track_r = theme::TRACK_H / 2.0;
let track_y = rect.y + ( rect.height - track_h ) / 2.0;
let track_r = track_h / 2.0;
let track_rect = Rect
{
x: track_x,
y: track_y,
width: theme::TRACK_W,
height: theme::TRACK_H,
width: track_w,
height: track_h,
};
let track_color = if self.value { theme::track_on() } else { theme::track_off() };
canvas.fill_rect( track_rect, track_color, track_r );
let thumb_pad = ( theme::TRACK_H - theme::THUMB_SIZE ) / 2.0;
let thumb_pad = ( track_h - thumb_size ) / 2.0;
let thumb_cx = if self.value
{
track_x + theme::TRACK_W - thumb_pad - theme::THUMB_SIZE / 2.0
track_x + track_w - thumb_pad - thumb_size / 2.0
} else {
track_x + thumb_pad + theme::THUMB_SIZE / 2.0
track_x + thumb_pad + thumb_size / 2.0
};
let thumb_cy = track_y + theme::TRACK_H / 2.0;
let thumb_r = theme::THUMB_SIZE / 2.0;
let thumb_cy = track_y + track_h / 2.0;
let thumb_r = thumb_size / 2.0;
let thumb_rect = Rect
{
x: thumb_cx - thumb_r,
y: thumb_cy - thumb_r,
width: theme::THUMB_SIZE,
height: theme::THUMB_SIZE,
width: thumb_size,
height: thumb_size,
};
canvas.fill_rect( thumb_rect, theme::thumb(), thumb_r );
canvas.stroke_rect( thumb_rect, theme::thumb_border(), theme::THUMB_BORDER_W, thumb_r );