responsive fluid/physical scaling, widget-API stabilization, and perf guardrails
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:
@@ -30,17 +30,22 @@ use crate::render::Canvas;
|
||||
pub struct Image
|
||||
{
|
||||
/// Raw RGBA pixel data (4 bytes per pixel, straight alpha).
|
||||
pub rgba: Arc<Vec<u8>>,
|
||||
pub( crate ) rgba: Arc<Vec<u8>>,
|
||||
/// Pixel width of the source image.
|
||||
pub width: u32,
|
||||
pub( crate ) width: u32,
|
||||
/// Pixel height of the source image.
|
||||
pub height: u32,
|
||||
pub( crate ) height: u32,
|
||||
/// When `true` the image scales to fill the available width (cover mode).
|
||||
pub cover: bool,
|
||||
pub( crate ) cover: bool,
|
||||
/// Optional explicit display size (Length values, resolved at layout time).
|
||||
pub display_size: Option<( Length, Length )>,
|
||||
pub( crate ) display_size: Option<( Length, Length )>,
|
||||
/// Optional extent along the viewport's **short** side (width in portrait,
|
||||
/// height in landscape), with the other axis following the source aspect
|
||||
/// ratio. Resolved at layout time. Takes precedence over `display_size`
|
||||
/// and `cover`.
|
||||
pub( crate ) short_side: Option<Length>,
|
||||
/// Opacity multiplier in `[0.0, 1.0]`. Default: `1.0`.
|
||||
pub opacity: f32,
|
||||
pub( crate ) opacity: f32,
|
||||
}
|
||||
|
||||
impl Image
|
||||
@@ -50,7 +55,7 @@ impl Image
|
||||
/// `width` and `height` must match the dimensions of `rgba`.
|
||||
pub fn new( rgba: Arc<Vec<u8>>, width: u32, height: u32 ) -> Self
|
||||
{
|
||||
Self { rgba, width, height, cover: false, display_size: None, opacity: 1.0 }
|
||||
Self { rgba, width, height, cover: false, display_size: None, short_side: None, opacity: 1.0 }
|
||||
}
|
||||
|
||||
/// Load an image from a file path. Supports PNG, JPEG, and other formats
|
||||
@@ -59,7 +64,7 @@ impl Image
|
||||
{
|
||||
let img = image::open( path )?.into_rgba8();
|
||||
let ( width, height ) = img.dimensions();
|
||||
Ok( Self { rgba: Arc::new( img.into_raw() ), width, height, cover: false, display_size: None, opacity: 1.0 } )
|
||||
Ok( Self { rgba: Arc::new( img.into_raw() ), width, height, cover: false, display_size: None, short_side: None, opacity: 1.0 } )
|
||||
}
|
||||
|
||||
/// Scale the image to fill the available width, preserving aspect ratio (cover mode).
|
||||
@@ -77,6 +82,18 @@ impl Image
|
||||
self
|
||||
}
|
||||
|
||||
/// Size the image by its extent along the viewport's **short** side —
|
||||
/// the width in portrait, the height in landscape — with the other axis
|
||||
/// derived from the source aspect ratio. Pairs with
|
||||
/// [`Length::orient`](crate::Length::orient) to express a rule like "40 %
|
||||
/// of the width in portrait, 5 % of the height in landscape" in one call:
|
||||
/// `img_widget( rgba, w, h ).short_side( Length::orient( 40.0, 5.0 ) )`.
|
||||
pub fn short_side( mut self, extent: impl Into<Length> ) -> Self
|
||||
{
|
||||
self.short_side = Some( extent.into() );
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the opacity multiplier. Clamped to `[0.0, 1.0]`.
|
||||
pub fn opacity( mut self, o: f32 ) -> Self
|
||||
{
|
||||
@@ -88,6 +105,22 @@ impl Image
|
||||
/// `canvas` is used to resolve viewport-relative [`Length`] values.
|
||||
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
||||
{
|
||||
if let Some( extent ) = &self.short_side
|
||||
{
|
||||
let ( vw, vh ) = canvas.viewport_layout();
|
||||
let s = extent.resolve( ( vw, vh ), Length::EM_BASE_DEFAULT ).max( 0.0 );
|
||||
let sw = self.width as f32;
|
||||
let sh = self.height as f32;
|
||||
if sw <= 0.0 || sh <= 0.0 { return ( s, s ); }
|
||||
// Portrait: short side is the width → `s` sets the width.
|
||||
// Landscape: short side is the height → `s` sets the height.
|
||||
if vw <= vh
|
||||
{
|
||||
return ( s, s * sh / sw );
|
||||
} else {
|
||||
return ( s * sw / sh, s );
|
||||
}
|
||||
}
|
||||
if let Some( ( w, h ) ) = &self.display_size
|
||||
{
|
||||
let vp = canvas.viewport_layout();
|
||||
|
||||
Reference in New Issue
Block a user