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
+110 -25
View File
@@ -112,34 +112,108 @@
//!
//! - [`Color`], [`Rect`], [`Point`], [`Size`], [`Corners`], [`WidgetId`].
//! - [`Length`] — a size/distance that may be absolute pixels
//! ([`LengthBase::Px`]) or relative to the surface viewport
//! ([`LengthBase::Px`]), relative to the surface viewport
//! ([`LengthBase::Vw`] / [`LengthBase::Vh`] / [`LengthBase::Vmin`] /
//! [`LengthBase::Vmax`]) or to the root font size ([`LengthBase::Em`]).
//! Every setter that takes a size, padding, spacing or font height
//! now accepts `impl Into<Length>`, so legacy `.size( 24.0 )` keeps
//! working while new code can write `.size( Length::vmin( 4.0 )
//! .clamp( 16.0, 32.0 ) )` for a typeface that scales with the screen.
//! [`LengthBase::Vmax`] / [`LengthBase::Orient`]) or to the root font
//! size ([`LengthBase::Em`]). Every setter that takes a size, padding,
//! spacing or font height now accepts `impl Into<Length>`, so legacy
//! `.size( 24.0 )` keeps working while new code can write
//! `.size( Length::vmin( 4.0 ).clamp( 16.0, 32.0 ) )` for a typeface
//! that scales with the screen. See **Responsive design** below.
//!
//! See [`types`] for the full module with `//!` description.
//!
//! ## Designing for multiple resolutions
//! ## Responsive design: one UI from phone to desktop
//!
//! ltk supports three approaches to making a layout adapt to the screen,
//! ordered from most to least common:
//! A core goal of ltk is that a **single view** reads coherently across
//! a portrait phone, a tablet and a landscape desktop window — no
//! per-target forks, no media-query soup. The mechanism is **fluid
//! sizing**: express sizes as a fraction of the surface so the whole
//! design breathes with the screen, instead of freezing at one pixel
//! size that only looks right on one device.
//!
//! 1. **Relative [`Length`] values** for font sizes, padding, spacing and
//! spacer height/width. Default to `Length::vmin( pct ).clamp( lo, hi )`:
//! the percentage tracks the surface's smaller side (so portrait and
//! landscape behave coherently), and the px clamps protect both ends
//! of the spectrum from breaking the design.
//! 2. **Responsive typographic scale** via [`theme::typography::h0`]…
//! [`theme::typography::body_xs`] — pre-calibrated [`Length`] values
//! coherent with the default Sora-based theme. Calibrated so a
//! 1000-px smaller side reproduces the legacy px constants exactly.
//! 3. **`view()`-level branching on `surface_width` / `surface_height`**
//! when the layout structure itself must change (e.g. sidebar →
//! bottom-tabs below a breakpoint). Keep this for genuine restructuring,
//! not for sizing — `Length` covers sizing.
//! ### The core rule — fluid, but clamped
//!
//! Default to `Length::vmin( pct ).clamp( lo, hi )` for every font size,
//! padding, spacing and spacer:
//!
//! - the percentage tracks the surface's **smaller** side, so portrait
//! and landscape stay coherent — an element keeps the same fraction of
//! the narrow axis whichever way the device is held;
//! - the px `clamp` bounds the fluid range so the design never collapses
//! on a watch-sized surface nor balloons on a 4K monitor.
//!
//! The clamp is not optional polish: it is what turns *pure* proportional
//! sizing (fragile at the extremes) into *bounded* proportional sizing
//! (robust everywhere). Treat "always clamp a fluid value" as the rule,
//! not the exception.
//!
//! ### Orientation-dependent proportion — [`Length::orient`]
//!
//! Sometimes the right *proportion* differs by orientation, not merely
//! the reference axis. A logo may want 40 % of the width in portrait —
//! there is vertical room to spare — but only 5 % of the height in
//! landscape, where vertical room is scarce.
//! [`Length::orient`]`( portrait, landscape )` expresses exactly that:
//! `portrait` % of the **width** when the surface is portrait,
//! `landscape` % of the **height** when it is landscape (the short side
//! of each orientation, but with its own proportion).
//!
//! For images, pair it with
//! [`Image::short_side`](widget::image::Image::short_side), which sizes
//! the image along the screen's short side and lets the other axis follow
//! the source aspect ratio:
//!
//! ```rust,no_run
//! # use std::sync::Arc;
//! # use ltk::{ img_widget, Length, Element };
//! # #[ derive( Clone ) ] enum Msg {}
//! # fn _ex( rgba: Arc<Vec<u8>>, w: u32, h: u32 ) -> Element<Msg> {
//! // 40 % of the width in portrait, 5 % of the height in landscape.
//! img_widget( rgba, w, h ).short_side( Length::orient( 40.0, 5.0 ) ).into()
//! # }
//! ```
//!
//! ### When constant *physical* size matters instead
//!
//! Fluid units scale with the screen's pixels, not with real-world
//! millimetres — and legibility is a function of physical (angular) size,
//! not of what fraction of the screen a glyph fills. When a size must stay
//! a **constant physical size** across very different displays, use the
//! other mode: [`Length::dp`] (a density-independent pixel — `n ×`
//! [`density`], the mainstream HiDPI unit), or [`LengthBase::Em`] for text
//! relative to the root font size. The pre-calibrated
//! [`theme::typography`] scale
//! ([`theme::typography::h0`]…[`theme::typography::body_xs`]) is built on
//! clamped `vmin`, so it stays fluid while respecting a readable px floor
//! and ceiling — a good default for running text.
//!
//! ### The two modes, and choosing per app
//!
//! ltk offers both strategies as first-class citizens and lets the app
//! pick — per value, or process-wide for every stock widget:
//!
//! - **Fluid** — [`Length::fluid`] and the raw `vmin` / `vmax` / `vw` /
//! `vh` / `orient` units. Surface-proportional; tracks the short side
//! (width in portrait, height in landscape). Best for full-screen system
//! surfaces on known hardware (lock screen, greeter, splash, kiosk,
//! launcher).
//! - **Physical** — [`Length::dp`] plus [`set_density`] / [`density`].
//! Constant real-world size, HiDPI-aware. Best for conventional windowed
//! apps across an open-ended device set.
//!
//! Stock widgets carry one design pixel per dimension and resolve it
//! through the process-wide [`WidgetScaling`] mode
//! ([`set_widget_scaling`]): [`WidgetScaling::Fluid`] (the default) reads
//! it as [`Length::fluid`], [`WidgetScaling::Physical`] as [`Length::dp`].
//! An explicit [`Length`] on a widget always overrides the mode.
//!
//! ### Structural changes — branch in `view()`
//!
//! When the layout *structure* must change (sidebar → bottom tabs, two
//! columns → one) rather than merely resize, branch in `view()` on
//! `surface_width` / `surface_height`. Keep this for genuine
//! restructuring; [`Length`] covers all pure sizing.
//!
//! ## Runtime-free embedding
//!
@@ -187,6 +261,12 @@
// English entry.
rust_i18n::i18n!( "locales", fallback = "en" );
/// Serialises tests across modules that read or mutate the process-wide
/// density / [`WidgetScaling`] globals, so parallel test threads never
/// observe each other's transient state.
#[ cfg( test ) ]
pub( crate ) static TEST_GLOBALS_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new( () );
pub mod types;
pub( crate ) mod render;
pub( crate ) mod system_fonts;
@@ -248,7 +328,10 @@ pub use render::Canvas;
pub use wallpaper::{ WallpaperBundle, ImageData };
pub use chassis::{ set_default_theme, theme_logo_rgba, theme_icon_tinted, branding_bundle_or_solid, wallpaper_bundle_or_solid, backdrop };
pub use types::{ Color, Corners, CursorShape, Length, LengthBase, PathCmd, Point, Rect, Size, WidgetId };
pub use types::{ design_reference, set_design_reference };
pub use types::{ WidgetScaling, FLUID_MIN, FLUID_MAX };
pub use types::{ fluid_reference, set_fluid_reference };
pub use types::{ density, set_density };
pub use types::{ widget_scaling, set_widget_scaling };
pub use widget::{ Element, button, icon_button, text_edit, image as img_widget, text, container };
pub use text_shaping::measure_text;
pub use widget::rich_text::{ rich_text, RichText, LinkSpan };
@@ -468,13 +551,15 @@ pub mod runtime
///
/// **Not part of the stable public API.** Anything in here may change between
/// patch releases without notice. Hidden from generated docs via
/// `#[doc(hidden)]` for the same reason.
/// `#[doc(hidden)]`, and gated behind the `test-support` feature so it is
/// absent from third-party builds entirely (ltk's own `make test` enables it).
#[ cfg( feature = "test-support" ) ]
#[ doc( hidden ) ]
pub mod test_support
{
pub use crate::tree::{ find_widget_at, find_widget, find_handlers, next_focusable_index };
pub use crate::widget::{ LaidOutWidget, WidgetHandlers };
pub use crate::widget::slider::{ value_from_x_in_rect, value_from_pos_in_rect, SliderAxis };
pub use crate::widget::slider::{ value_from_x_in_rect, value_from_pos_in_rect, thumb_design_px, SliderAxis };
pub use crate::widget::vslider::value_from_y_in_rect;
pub use crate::event_loop::diff_overlay_ids;
}