responsive fluid/physical scaling, widget-API stabilization, and perf guardrails
Some checks failed
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

View File

@@ -35,8 +35,8 @@ pub fn value_from_y_in_rect( rect: Rect, y: f32 ) -> f32
/// offers — it is intrinsically sized, not filler.
///
/// The widget renders a rounded track in `palette.surface_alt` and, on top,
/// a rising pill in `palette.accent` whose height is proportional to
/// [`VSlider::value`]. No separate thumb is drawn; the top edge of the fill
/// a rising pill in `palette.accent` whose height is proportional to its
/// `value`. No separate thumb is drawn; the top edge of the fill
/// itself acts as the value indicator.
///
/// ```rust,no_run
@@ -64,25 +64,25 @@ pub struct VSlider<Msg: Clone>
{
/// Current value in `[0.0, 1.0]`. `0.0` paints no fill; `1.0` fills the
/// whole pill.
pub value: f32,
pub( crate ) value: f32,
/// Width of the pill. Defaults to 56 px; accepts any [`Length`].
pub width: Length,
pub( crate ) width: Length,
/// Height of the pill. Defaults to 160 px; accepts any [`Length`].
pub height: Length,
pub( crate ) height: Length,
/// Callback invoked with the new value when the slider is tapped or
/// dragged. `Arc` (not `Box`) so the layout pass can clone it into the
/// per-leaf handler snapshot for O(1) dispatch on input events.
pub on_change: Option<Arc<dyn Fn(f32) -> Msg>>,
pub( crate ) on_change: Option<Arc<dyn Fn(f32) -> Msg>>,
/// Theme slot id for the unfilled track. Defaults to
/// `surface-slider-track`. Override with [`VSlider::track_surface`]
/// when the slider lives inside a panel that already provides its
/// own backdrop blur — point the slot at a `*-flat` variant
/// (no `backdrop` field) so the pipeline does not run a redundant
/// backdrop snapshot per slider per frame.
pub track_surface: &'static str,
pub( crate ) track_surface: &'static str,
/// Theme slot id for the filled portion. Same role as
/// [`Self::track_surface`] but for the rising fill.
pub fill_surface: &'static str,
pub( crate ) fill_surface: &'static str,
}
impl<Msg: Clone> VSlider<Msg>
@@ -93,8 +93,8 @@ impl<Msg: Clone> VSlider<Msg>
Self
{
value: value.clamp( 0.0, 1.0 ),
width: Length::px( theme::WIDTH ),
height: Length::px( theme::HEIGHT ),
width: Length::widget( theme::WIDTH ),
height: Length::widget( theme::HEIGHT ),
on_change: None,
track_surface: theme::SURFACE_TRACK,
fill_surface: theme::SURFACE_FILL,

View File

@@ -88,20 +88,23 @@ fn preferred_size_ignores_max_width()
{
// A VSlider is intrinsically sized — the parent's max_width doesn't
// change what we return.
let _g = crate::TEST_GLOBALS_LOCK.lock().unwrap_or_else( |e| e.into_inner() );
let canvas = make_canvas();
let s: VSlider<()> = vslider( 0.5 );
let ( w_small, _ ) = s.preferred_size( 10.0, &canvas );
let ( w_big, _ ) = s.preferred_size( 9_999.0, &canvas );
assert_eq!( w_small, theme::WIDTH );
assert_eq!( w_big, theme::WIDTH );
// The intrinsic width now follows the widget-scaling mode.
assert_eq!( w_small, canvas.geom_px( theme::WIDTH ) );
assert_eq!( w_big, canvas.geom_px( theme::WIDTH ) );
}
#[ test ]
fn default_dimensions_are_the_theme_constants()
fn default_dimensions_follow_widget_scaling()
{
let _g = crate::TEST_GLOBALS_LOCK.lock().unwrap_or_else( |e| e.into_inner() );
let s: VSlider<()> = vslider( 0.0 );
assert_eq!( s.width, Length::px( theme::WIDTH ) );
assert_eq!( s.height, Length::px( theme::HEIGHT ) );
assert_eq!( s.width, Length::widget( theme::WIDTH ) );
assert_eq!( s.height, Length::widget( theme::HEIGHT ) );
}
#[ test ]