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:
+33
-22
@@ -38,27 +38,27 @@ pub struct ListItem<Msg: Clone>
|
||||
{
|
||||
/// Primary label (always visible, top-aligned when a subtitle is
|
||||
/// present).
|
||||
pub label: String,
|
||||
pub( crate ) label: String,
|
||||
/// Optional secondary line drawn below the label in muted colour.
|
||||
/// Doubles the row height when set.
|
||||
pub subtitle: Option<String>,
|
||||
pub( crate ) subtitle: Option<String>,
|
||||
/// Optional right-aligned text (current setting, badge count, "›"
|
||||
/// disclosure). Drawn in muted colour.
|
||||
pub trailing: Option<String>,
|
||||
pub( crate ) trailing: Option<String>,
|
||||
/// Message emitted on tap. `None` keeps the item visible but inert.
|
||||
pub on_press: Option<Msg>,
|
||||
pub( crate ) on_press: Option<Msg>,
|
||||
/// Optional stable identifier for focus management.
|
||||
pub id: Option<WidgetId>,
|
||||
pub( crate ) id: Option<WidgetId>,
|
||||
/// `true` paints the row with the dark selected surface and white
|
||||
/// text, regardless of hover / press state. Use to indicate the
|
||||
/// active item in a list of choices (combo dropdown, settings
|
||||
/// group with a single active value).
|
||||
pub selected: bool,
|
||||
pub( crate ) selected: bool,
|
||||
/// Optional leading icon — RGBA bytes + native dimensions. The
|
||||
/// row reserves `theme::ICON_SIZE + theme::ICON_GAP` on the left
|
||||
/// when this is set, and offsets the label / subtitle by the
|
||||
/// same amount. Pass `None` to keep the icon-less layout.
|
||||
pub icon: Option<( Arc<Vec<u8>>, u32, u32 )>,
|
||||
pub( crate ) icon: Option<( Arc<Vec<u8>>, u32, u32 )>,
|
||||
}
|
||||
|
||||
impl<Msg: Clone> ListItem<Msg>
|
||||
@@ -128,9 +128,14 @@ impl<Msg: Clone> ListItem<Msg>
|
||||
self
|
||||
}
|
||||
|
||||
pub fn preferred_size( &self, max_width: f32, _canvas: &Canvas ) -> (f32, f32)
|
||||
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
||||
{
|
||||
let h = if self.subtitle.is_some() { theme::HEIGHT_SUB } else { theme::HEIGHT };
|
||||
let h = if self.subtitle.is_some()
|
||||
{
|
||||
canvas.geom_px( theme::HEIGHT_SUB )
|
||||
} else {
|
||||
canvas.geom_px( theme::HEIGHT )
|
||||
};
|
||||
( max_width, h )
|
||||
}
|
||||
|
||||
@@ -172,12 +177,14 @@ impl<Msg: Clone> ListItem<Msg>
|
||||
canvas.stroke_rect( rect, theme::focus_color(), theme::FOCUS_W, theme::RADIUS );
|
||||
}
|
||||
|
||||
let label_size = canvas.font_px( theme::LABEL_SIZE );
|
||||
let pad_h = canvas.geom_px( theme::PAD_H );
|
||||
let has_sub = self.subtitle.is_some();
|
||||
let label_y = if has_sub
|
||||
{
|
||||
rect.y + rect.height * 0.35 + theme::LABEL_SIZE * 0.3
|
||||
rect.y + rect.height * 0.35 + label_size * 0.3
|
||||
} else {
|
||||
rect.y + ( rect.height + theme::LABEL_SIZE ) / 2.0 - 2.0
|
||||
rect.y + ( rect.height + label_size ) / 2.0 - 2.0
|
||||
};
|
||||
|
||||
// Leading-icon column shifts the text right by
|
||||
@@ -187,30 +194,34 @@ impl<Msg: Clone> ListItem<Msg>
|
||||
// is in `theme::icon_rgba` rather than baked here.
|
||||
let text_x = if let Some( ( rgba, w, h ) ) = &self.icon
|
||||
{
|
||||
let icon_y = rect.y + ( rect.height - theme::ICON_SIZE ) / 2.0;
|
||||
let icon_rect = Rect { x: rect.x + theme::PAD_H, y: icon_y, width: theme::ICON_SIZE, height: theme::ICON_SIZE };
|
||||
let icon_size = canvas.geom_px( theme::ICON_SIZE );
|
||||
let icon_gap = canvas.geom_px( theme::ICON_GAP );
|
||||
let icon_y = rect.y + ( rect.height - icon_size ) / 2.0;
|
||||
let icon_rect = Rect { x: rect.x + pad_h, y: icon_y, width: icon_size, height: icon_size };
|
||||
canvas.draw_image_data( rgba, *w, *h, icon_rect, 1.0 );
|
||||
rect.x + theme::PAD_H + theme::ICON_SIZE + theme::ICON_GAP
|
||||
rect.x + pad_h + icon_size + icon_gap
|
||||
}
|
||||
else
|
||||
{
|
||||
rect.x + theme::PAD_H
|
||||
rect.x + pad_h
|
||||
};
|
||||
|
||||
canvas.draw_text( &self.label, text_x, label_y, theme::LABEL_SIZE, label_color );
|
||||
canvas.draw_text( &self.label, text_x, label_y, label_size, label_color );
|
||||
|
||||
if let Some( ref sub ) = self.subtitle
|
||||
{
|
||||
let sub_y = rect.y + rect.height * 0.62 + theme::SUBTITLE_SIZE * 0.3;
|
||||
canvas.draw_text( sub, text_x, sub_y, theme::SUBTITLE_SIZE, subtitle_color );
|
||||
let sub_size = canvas.font_px( theme::SUBTITLE_SIZE );
|
||||
let sub_y = rect.y + rect.height * 0.62 + sub_size * 0.3;
|
||||
canvas.draw_text( sub, text_x, sub_y, sub_size, subtitle_color );
|
||||
}
|
||||
|
||||
if let Some( ref trail ) = self.trailing
|
||||
{
|
||||
let tw = canvas.measure_text( trail, theme::TRAILING_SIZE );
|
||||
let tx = rect.x + rect.width - theme::PAD_H - tw;
|
||||
let ty = rect.y + ( rect.height + theme::TRAILING_SIZE ) / 2.0 - 2.0;
|
||||
canvas.draw_text( trail, tx, ty, theme::TRAILING_SIZE, trailing_color );
|
||||
let trail_size = canvas.font_px( theme::TRAILING_SIZE );
|
||||
let tw = canvas.measure_text( trail, trail_size );
|
||||
let tx = rect.x + rect.width - pad_h - tw;
|
||||
let ty = rect.y + ( rect.height + trail_size ) / 2.0 - 2.0;
|
||||
canvas.draw_text( trail, tx, ty, trail_size, trailing_color );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user