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:
@@ -27,11 +27,13 @@ pub enum SliderAxis
|
||||
/// [`Slider`] and [`crate::widget::vslider::VSlider`] through the same call
|
||||
/// site by consulting the [`SliderAxis`] stored in the widget's handler
|
||||
/// snapshot.
|
||||
pub fn value_from_pos_in_rect( rect: Rect, pos: Point, axis: SliderAxis ) -> f32
|
||||
pub fn value_from_pos_in_rect( rect: Rect, pos: Point, axis: SliderAxis, thumb_px: f32 ) -> f32
|
||||
{
|
||||
match axis
|
||||
{
|
||||
SliderAxis::Horizontal => value_from_x_in_rect( rect, pos.x ),
|
||||
// The vertical branch maps against the full rect height and takes no
|
||||
// thumb inset, so `thumb_px` is unused there.
|
||||
SliderAxis::Horizontal => value_from_x_in_rect( rect, pos.x, thumb_px ),
|
||||
SliderAxis::Vertical => crate::widget::vslider::value_from_y_in_rect( rect, pos.y ),
|
||||
}
|
||||
}
|
||||
@@ -83,15 +85,29 @@ pub( crate ) fn intersect_clip( saved: &[ Rect ], inner: Rect ) -> Vec<Rect>
|
||||
/// slider's layout rect. Pure — depends only on `theme::THUMB_SIZE`. Lifted
|
||||
/// out of [`Slider`] so input handlers can call it directly from
|
||||
/// [`crate::widget::LaidOutWidget`] without needing the [`Element`] tree.
|
||||
pub fn value_from_x_in_rect( rect: Rect, x: f32 ) -> f32
|
||||
pub fn value_from_x_in_rect( rect: Rect, x: f32, thumb_px: f32 ) -> f32
|
||||
{
|
||||
let pad = theme::THUMB_SIZE / 2.0;
|
||||
let pad = thumb_px / 2.0;
|
||||
let track_start = rect.x + pad;
|
||||
let track_end = rect.x + rect.width - pad;
|
||||
let track_w = ( track_end - track_start ).max( 1.0 );
|
||||
( ( x - track_start ) / track_w ).clamp( 0.0, 1.0 )
|
||||
}
|
||||
|
||||
/// The slider thumb's design pixel size — the raw theme constant, used as a
|
||||
/// fallback when a widget-scaling-resolved size is not available (e.g. a
|
||||
/// handler snapshot built outside layout). Public for `test_support`, so the
|
||||
/// pure `value_from_*` helpers can be exercised with the real design thumb.
|
||||
pub fn thumb_design_px() -> f32 { theme::THUMB_SIZE }
|
||||
|
||||
/// Resolve the thumb size through the widget-scaling mode. The layout pass
|
||||
/// stores this in the [`crate::widget::WidgetHandlers::Slider`] snapshot so
|
||||
/// the input path maps clicks against the same thumb the renderer drew.
|
||||
pub( crate ) fn resolved_thumb_px( canvas: &Canvas ) -> f32
|
||||
{
|
||||
canvas.geom_px( theme::THUMB_SIZE )
|
||||
}
|
||||
|
||||
/// A horizontal slider for selecting a value in a range.
|
||||
///
|
||||
/// The track defaults to a theme-resolved surface; pass
|
||||
@@ -113,31 +129,31 @@ pub fn value_from_x_in_rect( rect: Rect, x: f32 ) -> f32
|
||||
pub struct Slider<Msg: Clone>
|
||||
{
|
||||
/// Current value in `[0.0, 1.0]`.
|
||||
pub value: f32,
|
||||
pub( crate ) value: f32,
|
||||
/// Callback invoked with the new value when the slider is dragged.
|
||||
/// `Arc` (not `Box`) so the layout pass can clone it into the per-leaf
|
||||
/// handler snapshot for O(1) dispatch on input events.
|
||||
pub on_change: Option<Arc<dyn Fn(f32) -> Msg>>,
|
||||
pub( crate ) on_change: Option<Arc<dyn Fn(f32) -> Msg>>,
|
||||
/// Theme slot id for the track background pill. Defaults to the
|
||||
/// generic `surface-slider-track`; override per-instance to opt
|
||||
/// into the `-flat` (no per-surface backdrop) variant when the
|
||||
/// slider already lives inside a panel-wide blur, or any other
|
||||
/// custom slot.
|
||||
pub track_surface: &'static str,
|
||||
pub( crate ) track_surface: &'static str,
|
||||
/// Theme slot id for the rising / leftward fill. Same shape as
|
||||
/// [`Self::track_surface`] but for the active portion.
|
||||
pub fill_surface: &'static str,
|
||||
pub( crate ) fill_surface: &'static str,
|
||||
/// Paint the thumb with the active palette's `accent` colour and
|
||||
/// a thicker white border (accent inner pill + 4 px white ring)
|
||||
/// instead of the default white-on-text-primary thumb.
|
||||
pub accent_thumb: bool,
|
||||
pub( crate ) accent_thumb: bool,
|
||||
/// Override the track paint with a custom
|
||||
/// [`Paint`](crate::theme::Paint) — typically a
|
||||
/// [`Paint::Linear`](crate::theme::Paint::Linear) for spectrum /
|
||||
/// hue pickers. When set, the active-side fill is suppressed
|
||||
/// because a spectrum already conveys position information
|
||||
/// through colour and the thumb shows where the user is.
|
||||
pub track_paint: Option<crate::theme::Paint>,
|
||||
pub( crate ) track_paint: Option<crate::theme::Paint>,
|
||||
}
|
||||
|
||||
impl<Msg: Clone> Slider<Msg>
|
||||
@@ -204,15 +220,17 @@ impl<Msg: Clone> Slider<Msg>
|
||||
}
|
||||
|
||||
/// Return the preferred `(width, height)`.
|
||||
pub fn preferred_size( &self, max_width: f32, _canvas: &Canvas ) -> (f32, f32)
|
||||
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
||||
{
|
||||
( max_width, theme::HEIGHT )
|
||||
( max_width, canvas.geom_px( theme::HEIGHT ) )
|
||||
}
|
||||
|
||||
/// Compute the value `[0.0, 1.0]` from a tap/drag x position within `rect`.
|
||||
/// Uses the design thumb size; the live input path resolves the thumb
|
||||
/// through the widget-scaling mode via the handler snapshot instead.
|
||||
pub fn value_from_x( &self, rect: Rect, x: f32 ) -> f32
|
||||
{
|
||||
value_from_x_in_rect( rect, x )
|
||||
value_from_x_in_rect( rect, x, thumb_design_px() )
|
||||
}
|
||||
|
||||
/// Thumb border stroke is centered on the thumb edge (which touches the
|
||||
@@ -237,12 +255,17 @@ impl<Msg: Clone> Slider<Msg>
|
||||
/// theme still paints a usable slider.
|
||||
pub fn draw( &self, canvas: &mut Canvas, rect: Rect, _focused: bool )
|
||||
{
|
||||
let pad = theme::THUMB_SIZE / 2.0;
|
||||
let track_y = rect.y + (rect.height - theme::TRACK_H) / 2.0;
|
||||
// Resolve the thumb / track through the widget-scaling mode. The
|
||||
// pad MUST match `value_from_x_in_rect`, which the input path feeds
|
||||
// the same mode-resolved thumb size via the handler snapshot.
|
||||
let thumb = canvas.geom_px( theme::THUMB_SIZE );
|
||||
let track_h = canvas.geom_px( theme::TRACK_H );
|
||||
let pad = thumb / 2.0;
|
||||
let track_y = rect.y + (rect.height - track_h) / 2.0;
|
||||
let track_start = rect.x + pad;
|
||||
let track_end = rect.x + rect.width - pad;
|
||||
let track_w = track_end - track_start;
|
||||
let radius_bg = theme::TRACK_H / 2.0;
|
||||
let radius_bg = track_h / 2.0;
|
||||
|
||||
// Background track — full pill across the inner span.
|
||||
let track_rect = Rect
|
||||
@@ -250,7 +273,7 @@ impl<Msg: Clone> Slider<Msg>
|
||||
x: track_start,
|
||||
y: track_y,
|
||||
width: track_w,
|
||||
height: theme::TRACK_H,
|
||||
height: track_h,
|
||||
};
|
||||
if let Some( paint ) = self.track_paint.as_ref()
|
||||
{
|
||||
@@ -294,7 +317,7 @@ impl<Msg: Clone> Slider<Msg>
|
||||
x: track_start,
|
||||
y: track_y,
|
||||
width: fill_w,
|
||||
height: theme::TRACK_H,
|
||||
height: track_h,
|
||||
};
|
||||
let saved_clip = canvas.clip_bounds();
|
||||
let band = intersect_clip( &saved_clip, visible );
|
||||
@@ -357,13 +380,13 @@ impl<Msg: Clone> Slider<Msg>
|
||||
}
|
||||
else
|
||||
{
|
||||
let thumb_r = theme::THUMB_SIZE / 2.0;
|
||||
let thumb_r = thumb / 2.0;
|
||||
let thumb_rect = Rect
|
||||
{
|
||||
x: thumb_cx - thumb_r,
|
||||
y: thumb_cy - thumb_r,
|
||||
width: theme::THUMB_SIZE,
|
||||
height: theme::THUMB_SIZE,
|
||||
width: thumb,
|
||||
height: thumb,
|
||||
};
|
||||
canvas.fill_rect( thumb_rect, theme::thumb(), thumb_r );
|
||||
canvas.stroke_rect( thumb_rect, theme::thumb_border(), theme::THUMB_BORDER_W, thumb_r );
|
||||
|
||||
Reference in New Issue
Block a user