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`.
102 lines
3.2 KiB
Rust
102 lines
3.2 KiB
Rust
// SPDX-License-Identifier: LGPL-2.1-only
|
||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||
|
||
//! Position a child element below the on-screen rect of another widget,
|
||
//! looked up from the *previous* frame's [`LaidOutWidget`] snapshot.
|
||
//!
|
||
//! The classic use case is a combo / dropdown popup that should appear
|
||
//! flush below its trigger. The trigger's rect is not known in `view()`
|
||
//! time — it's assigned during the layout pass — so the popup widget
|
||
//! cannot place itself there directly. `AnchoredOverlay` solves this
|
||
//! with a one-frame-old anchor: the trigger carries a stable
|
||
//! [`WidgetId`], the popup wraps in `AnchoredOverlay` referencing the
|
||
//! same id, and at draw time the wrapper looks up the trigger's rect
|
||
//! from the runtime's persisted `widget_rects` (frame N − 1) and
|
||
//! overrides the rect that its parent gave it.
|
||
//!
|
||
//! When the anchor is not found (first frame after open, missing id,
|
||
//! widget went off-screen) the wrapper falls back to drawing the child
|
||
//! in the parent-supplied rect — which, for a typical Stack-overlay
|
||
//! root in a `view()`, means the full surface, giving a sane modal
|
||
//! fallback until the next frame fixes the position.
|
||
|
||
use crate::types::{ Rect, WidgetId };
|
||
|
||
use super::Element;
|
||
|
||
#[ cfg( test ) ]
|
||
mod tests;
|
||
|
||
/// A wrapper that re-positions its child relative to an anchor widget
|
||
/// found in the previous frame's layout snapshot.
|
||
pub struct AnchoredOverlay<Msg: Clone>
|
||
{
|
||
/// The element to draw at the anchored position.
|
||
pub( crate ) child: Box<Element<Msg>>,
|
||
/// Stable identifier of the widget whose rect provides the anchor.
|
||
pub( crate ) anchor_id: WidgetId,
|
||
/// Vertical pixel gap between the bottom of the anchor and the top
|
||
/// of the child.
|
||
pub( crate ) gap: f32,
|
||
}
|
||
|
||
impl<Msg: Clone> AnchoredOverlay<Msg>
|
||
{
|
||
/// Wrap `child` so it draws anchored below the widget that carries
|
||
/// `anchor_id` in its `.id( … )` builder. `gap` is the vertical
|
||
/// space (logical pixels) between the anchor's bottom edge and the
|
||
/// child's top edge.
|
||
pub fn new( child: impl Into<Element<Msg>>, anchor_id: WidgetId, gap: f32 ) -> Self
|
||
{
|
||
Self
|
||
{
|
||
child: Box::new( child.into() ),
|
||
anchor_id,
|
||
gap,
|
||
}
|
||
}
|
||
|
||
/// Compute the draw rect for the child, given the anchor rect (when
|
||
/// available) and the parent-supplied fallback.
|
||
///
|
||
/// Anchor available → place the child flush below the anchor with
|
||
/// `gap` spacing, preserving the anchor's width.
|
||
/// Anchor missing → return the parent rect verbatim, so the child
|
||
/// renders modal-style as a fallback.
|
||
pub fn resolve_rect( anchor: Option<Rect>, gap: f32, fallback: Rect ) -> Rect
|
||
{
|
||
match anchor
|
||
{
|
||
Some( a ) => Rect
|
||
{
|
||
x: a.x,
|
||
y: a.y + a.height + gap,
|
||
width: a.width,
|
||
height: fallback.height,
|
||
},
|
||
None => fallback,
|
||
}
|
||
}
|
||
|
||
pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> AnchoredOverlay<U>
|
||
where
|
||
U: Clone + 'static,
|
||
Msg: 'static,
|
||
{
|
||
AnchoredOverlay
|
||
{
|
||
child: Box::new( self.child.map_arc( f ) ),
|
||
anchor_id: self.anchor_id,
|
||
gap: self.gap,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl<Msg: Clone + 'static> From<AnchoredOverlay<Msg>> for Element<Msg>
|
||
{
|
||
fn from( a: AnchoredOverlay<Msg> ) -> Self
|
||
{
|
||
Element::AnchoredOverlay( a )
|
||
}
|
||
}
|