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
+14 -13
View File
@@ -38,6 +38,7 @@ use std::sync::Arc;
use crate::layout::column::column;
use crate::layout::row::row;
use crate::layout::spacer::spacer;
use crate::types::Length;
use super::Element;
@@ -152,12 +153,12 @@ fn snap_step_delta( value: i32, step: i32, dir: i32 ) -> i32
/// Time-of-day selector with up / down steppers per unit.
pub struct TimePicker<Msg: Clone>
{
pub value: Time,
pub on_change: Option<Arc<dyn Fn( Time ) -> Msg>>,
pub minute_step: u8,
pub second_step: u8,
pub seconds: bool,
pub twelve_hour: bool,
pub( crate ) value: Time,
pub( crate ) on_change: Option<Arc<dyn Fn( Time ) -> Msg>>,
pub( crate ) minute_step: u8,
pub( crate ) second_step: u8,
pub( crate ) seconds: bool,
pub( crate ) twelve_hour: bool,
}
impl<Msg: Clone + 'static> TimePicker<Msg>
@@ -305,7 +306,7 @@ impl<Msg: Clone + 'static> TimePicker<Msg>
None =>
{
text( display )
.size( theme::VAL_FS )
.size( Length::widget( theme::VAL_FS ) )
.color( theme::text() )
.align_center()
.into()
@@ -317,7 +318,7 @@ impl<Msg: Clone + 'static> TimePicker<Msg>
text_edit::<Msg>( "", display )
.borderless( true )
.fixed_width( 72.0 )
.font_size( theme::VAL_FS )
.font_size_fluid( theme::VAL_FS )
.align( TextAlign::Center )
.select_on_focus( true )
.on_change( move |s: String|
@@ -386,9 +387,9 @@ impl<Msg: Clone + 'static> TimePicker<Msg>
);
let minute_col = stepper( minute_field, minute_up, minute_dn );
let mut units = row::<Msg>().spacing( theme::SPACING )
let mut units = row::<Msg>().spacing( Length::widget( theme::SPACING ) )
.push( hour_col )
.push( text( ":" ).size( theme::SEP_FS ).color( theme::text_muted() ) )
.push( text( ":" ).size( Length::widget( theme::SEP_FS ) ).color( theme::text_muted() ) )
.push( minute_col );
if self.seconds
@@ -406,7 +407,7 @@ impl<Msg: Clone + 'static> TimePicker<Msg>
);
let second_col = stepper( second_field, second_up, second_dn );
units = units
.push( text( ":" ).size( theme::SEP_FS ).color( theme::text_muted() ) )
.push( text( ":" ).size( Length::widget( theme::SEP_FS ) ).color( theme::text_muted() ) )
.push( second_col );
}
@@ -427,7 +428,7 @@ impl<Msg: Clone + 'static> TimePicker<Msg>
// the whole hh:mm AM block inside the container. A flex
// spacer here would defeat the auto-centre and pin AM/PM
// to the right.
units = units.push( spacer().width( theme::SPACING * 2.0 ) ).push( ampm );
units = units.push( spacer().width( Length::widget( theme::SPACING * 2.0 ) ) ).push( ampm );
}
// `Row::layout` centres a cluster horizontally when there are
@@ -438,7 +439,7 @@ impl<Msg: Clone + 'static> TimePicker<Msg>
// AM/PM), so they centre automatically inside the container.
container::<Msg>( units )
.background( theme::surface_alt() )
.padding( theme::PADDING )
.padding( Length::widget( theme::PADDING ) )
.radius( theme::RADIUS )
.into()
}