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`.
253 lines
7.5 KiB
Rust
253 lines
7.5 KiB
Rust
// SPDX-License-Identifier: LGPL-2.1-only
|
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
|
|
|
use crate::types::Rect;
|
|
use crate::render::Canvas;
|
|
use crate::widget::Element;
|
|
|
|
/// Horizontal alignment of a child within a [`Stack`] rect.
|
|
#[ derive( Debug, Clone, Copy, PartialEq ) ]
|
|
pub enum HAlign
|
|
{
|
|
/// Align to the left edge.
|
|
Start,
|
|
/// Center horizontally.
|
|
Center,
|
|
/// Align to the right edge.
|
|
End,
|
|
/// Stretch to fill the full width.
|
|
Fill,
|
|
}
|
|
|
|
/// Vertical alignment of a child within a [`Stack`] rect.
|
|
#[ derive( Debug, Clone, Copy, PartialEq ) ]
|
|
pub enum VAlign
|
|
{
|
|
/// Align to the top edge.
|
|
Top,
|
|
/// Center vertically.
|
|
Center,
|
|
/// Align to the bottom edge.
|
|
Bottom,
|
|
/// Stretch to fill the full height.
|
|
Fill,
|
|
}
|
|
|
|
/// A layout that draws all its children stacked on top of each other.
|
|
/// Each child can be positioned within the Stack rect via [`HAlign`]/[`VAlign`].
|
|
///
|
|
/// Useful for overlaying a foreground widget on top of a background image:
|
|
///
|
|
/// ```rust,no_run
|
|
/// # use std::sync::Arc;
|
|
/// # use ltk::{ column, img_widget, stack, text, Element, HAlign, VAlign };
|
|
/// # #[ derive( Clone ) ] enum Msg {}
|
|
/// # fn _ex( bg_rgba: Arc<Vec<u8>>, w: u32, h: u32 ) -> Element<Msg> {
|
|
/// stack()
|
|
/// .push( img_widget( bg_rgba, w, h ) )
|
|
/// .push_aligned( column().push( text( "Bottom right" ) ), HAlign::End, VAlign::Bottom )
|
|
/// .into()
|
|
/// # }
|
|
/// ```
|
|
pub struct Stack<Msg: Clone>
|
|
{
|
|
/// Children with their alignment, margin, and extra `(x, y)` translation
|
|
/// applied after alignment. Drawn in order — last child is on top. The 7th
|
|
/// `Option<Rect>` overrides alignment/sizing with an exact rect (see
|
|
/// [`push_placed`](Self::push_placed)); the 8th clips the child's draw to a
|
|
/// rect (Android clipChildren — see [`push_placed_clipped`](Self::push_placed_clipped)).
|
|
pub( crate ) children: Vec<( Element<Msg>, HAlign, VAlign, f32, f32, f32, Option<Rect>, Option<Rect> )>,
|
|
/// When `true`, [`preferred_size`](Self::preferred_size) reports the max of
|
|
/// children's intrinsic widths and heights instead of `(max_width, tallest)`.
|
|
pub( crate ) fit_content: bool,
|
|
}
|
|
|
|
impl<Msg: Clone> Stack<Msg>
|
|
{
|
|
/// Create an empty stack.
|
|
pub fn new() -> Self
|
|
{
|
|
Self { children: Vec::new(), fit_content: false }
|
|
}
|
|
|
|
/// Enable [`Self::fit_content`].
|
|
pub fn fit_content( mut self ) -> Self
|
|
{
|
|
self.fit_content = true;
|
|
self
|
|
}
|
|
|
|
/// Append a child that fills the entire Stack rect (Android FrameLayout default).
|
|
pub fn push( self, e: impl Into<Element<Msg>> ) -> Self
|
|
{
|
|
self.push_aligned_margin( e, HAlign::Fill, VAlign::Fill, 0.0 )
|
|
}
|
|
|
|
/// Append a child with explicit horizontal and vertical alignment.
|
|
pub fn push_aligned(
|
|
self,
|
|
e: impl Into<Element<Msg>>,
|
|
h_align: HAlign,
|
|
v_align: VAlign,
|
|
) -> Self
|
|
{
|
|
self.push_aligned_margin( e, h_align, v_align, 0.0 )
|
|
}
|
|
|
|
/// Append a child with alignment and a uniform margin (inset from the Stack edges).
|
|
pub fn push_aligned_margin(
|
|
mut self,
|
|
e: impl Into<Element<Msg>>,
|
|
h_align: HAlign,
|
|
v_align: VAlign,
|
|
margin: f32,
|
|
) -> Self
|
|
{
|
|
self.children.push( ( e.into(), h_align, v_align, margin, 0.0, 0.0, None, None ) );
|
|
self
|
|
}
|
|
|
|
/// Append a child with alignment plus an extra `(x, y)` translation in
|
|
/// logical pixels. Useful when a child needs to shift outside the normal
|
|
/// alignment grid without giving up the margin or alignment shorthand.
|
|
/// Positive `x` / `y` move the child right / down.
|
|
pub fn push_translated(
|
|
mut self,
|
|
e: impl Into<Element<Msg>>,
|
|
h_align: HAlign,
|
|
v_align: VAlign,
|
|
offset_x: f32,
|
|
offset_y: f32,
|
|
) -> Self
|
|
{
|
|
self.children.push( ( e.into(), h_align, v_align, 0.0, offset_x, offset_y, None, None ) );
|
|
self
|
|
}
|
|
|
|
/// Append a child placed at an exact `rect` (in the Stack's coordinate space),
|
|
/// bypassing alignment and intrinsic sizing. This is what hosts a view tree
|
|
/// whose geometry is computed elsewhere — e.g. Android's measure/layout pass,
|
|
/// which yields absolute rects per view.
|
|
pub fn push_placed(
|
|
mut self,
|
|
e: impl Into<Element<Msg>>,
|
|
rect: Rect,
|
|
) -> Self
|
|
{
|
|
self.children.push( ( e.into(), HAlign::Start, VAlign::Top, 0.0, 0.0, 0.0, Some( rect ), None ) );
|
|
self
|
|
}
|
|
|
|
/// Like [`push_placed`](Self::push_placed) but clips the child's drawing to
|
|
/// `clip` (in the Stack's coordinate space). Mirrors Android's clipChildren:
|
|
/// content that overflows the clip — e.g. a scrolled list row reaching above
|
|
/// the list, or an inner card past a rounded bubble — is not painted.
|
|
pub fn push_placed_clipped(
|
|
mut self,
|
|
e: impl Into<Element<Msg>>,
|
|
rect: Rect,
|
|
clip: Rect,
|
|
) -> Self
|
|
{
|
|
self.children.push( ( e.into(), HAlign::Start, VAlign::Top, 0.0, 0.0, 0.0, Some( rect ), Some( clip ) ) );
|
|
self
|
|
}
|
|
|
|
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
|
{
|
|
if self.fit_content
|
|
{
|
|
let content_w = self.children.iter()
|
|
.map( |( c, .. )| match c
|
|
{
|
|
Element::Spacer( s ) => s.resolved_width( canvas ).unwrap_or( 0.0 ),
|
|
Element::Separator( _ ) => 0.0,
|
|
Element::Scroll( _ ) => 0.0,
|
|
Element::ProgressBar( _ ) => 0.0,
|
|
Element::Slider( _ ) => 0.0,
|
|
Element::TextEdit( t ) => if t.fixed_width.is_some()
|
|
{
|
|
t.preferred_size( max_width, canvas ).0
|
|
} else { 0.0 },
|
|
other => other.preferred_size( max_width, canvas ).0,
|
|
} )
|
|
.fold( 0.0_f32, f32::max );
|
|
let max_h = self.children.iter()
|
|
.map( |( c, .. )| c.preferred_size( max_width, canvas ).1 )
|
|
.fold( 0.0_f32, f32::max );
|
|
return ( content_w.min( max_width ), max_h );
|
|
}
|
|
|
|
let max_h = self.children.iter()
|
|
.map( |( c, .. )| c.preferred_size( max_width, canvas ).1 )
|
|
.fold( 0.0_f32, f32::max );
|
|
( max_width, max_h )
|
|
}
|
|
|
|
/// Return `(rect, child_index)` pairs, computing each child's rect from its alignment.
|
|
pub fn layout( &self, rect: Rect, canvas: &Canvas ) -> Vec<(Rect, usize)>
|
|
{
|
|
self.children.iter().enumerate().map( |( i, ( child, h_align, v_align, margin, ox, oy, placed, .. ) )|
|
|
{
|
|
if let Some( p ) = placed
|
|
{
|
|
return ( Rect { x: rect.x + p.x, y: rect.y + p.y, width: p.width, height: p.height }, i );
|
|
}
|
|
|
|
let inner_w = ( rect.width - margin * 2.0 ).max( 0.0 );
|
|
let inner_h = ( rect.height - margin * 2.0 ).max( 0.0 );
|
|
let ( pref_w, pref_h ) = child.preferred_size( inner_w, canvas );
|
|
|
|
let ( x, width ) = match h_align
|
|
{
|
|
HAlign::Start => ( rect.x + margin, pref_w ),
|
|
HAlign::Center => ( rect.x + ( rect.width - pref_w ) / 2.0, pref_w ),
|
|
HAlign::End => ( rect.x + rect.width - pref_w - margin, pref_w ),
|
|
HAlign::Fill => ( rect.x + margin, inner_w ),
|
|
};
|
|
|
|
let ( y, height ) = match v_align
|
|
{
|
|
VAlign::Top => ( rect.y + margin, pref_h ),
|
|
VAlign::Center => ( rect.y + ( rect.height - pref_h ) / 2.0, pref_h ),
|
|
VAlign::Bottom => ( rect.y + rect.height - pref_h - margin, pref_h ),
|
|
VAlign::Fill => ( rect.y + margin, inner_h ),
|
|
};
|
|
|
|
( Rect { x: x + ox, y: y + oy, width, height }, i )
|
|
} ).collect()
|
|
}
|
|
|
|
/// No-op — children are drawn directly by the event loop during layout.
|
|
pub fn draw( &self, _canvas: &mut Canvas, _rect: Rect, _focused: bool ) {}
|
|
|
|
pub( crate ) fn map_msg<U>( self, f: &crate::widget::MapFn<Msg, U> ) -> Stack<U>
|
|
where
|
|
U: Clone + 'static,
|
|
Msg: 'static,
|
|
{
|
|
Stack
|
|
{
|
|
children: self.children.into_iter()
|
|
.map( |( child, ha, va, margin, ox, oy, placed, clip )|
|
|
( child.map_arc( f ), ha, va, margin, ox, oy, placed, clip ) )
|
|
.collect(),
|
|
fit_content: self.fit_content,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<Msg: Clone> Default for Stack<Msg>
|
|
{
|
|
fn default() -> Self
|
|
{
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Create an empty [`Stack`].
|
|
pub fn stack<Msg: Clone>() -> Stack<Msg>
|
|
{
|
|
Stack::new()
|
|
}
|