viewport: local_viewport() opt-out of root-viewport inheritance; list_item: height/font_size builders
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

`Viewport::local_viewport()` resolves the child's viewport-relative (vw / vh / vmin) and fluid Lengths against the viewport's own rect instead of the root layout viewport the sub-canvas inherits since the fluid-resolution inheritance change. That inheritance is right for scroll-like clips (content renders the same size inside and outside), but it is exactly wrong for a fixed-size floating mini-UI — crustace's phone-shaped quick-settings pill pinned to a corner of a desktop-wide surface resolved its vw text and fluid stock geometry against the whole monitor, inflating the content past the pill's fixed clip and cutting off the bottom stripe. The flag pins the sub-canvas's layout viewport to its own size via the new crate-internal Canvas::set_local_layout_viewport, nested sub-canvases keep propagating the pinned value, and a unit test covers the override plus propagation.
`ListItem::height( impl Into<Length> )` and `ListItem::font_size( impl Into<Length> )` mirror the Toggle / Radio height() builders: override the theme row height (floored at the label's rendered height so the text never clips) and the primary-label font size (subtitle and trailing keep their theme sizes), letting dense context menus trade the stock touch-target generosity for row density.
Docs: widgets.md gains both builder sets, architecture.md documents the layout-viewport inheritance model next to the per-canvas density it parallels, the cookbook slide-in panel recipe notes when the pill needs local_viewport(), and the toggle / radio height() rustdoc demotes its private theme::HEIGHT link to a code span so cargo doc is warning-free again. CHANGELOG entries added.
This commit is contained in:
2026-08-02 13:38:16 +02:00
parent 4247613bb0
commit e343142347
10 changed files with 118 additions and 13 deletions

View File

@@ -67,6 +67,10 @@ pub struct ListItem<Msg: Clone>
/// Optional override of the horizontal content inset. `None`
/// falls back to `theme::PAD_H`.
pub( crate ) pad_h: Option<Length>,
/// Optional override of the theme row height.
pub( crate ) height: Option<Length>,
/// Optional override of the primary-label font size.
pub( crate ) font_size: Option<Length>,
}
impl<Msg: Clone> ListItem<Msg>
@@ -86,6 +90,8 @@ impl<Msg: Clone> ListItem<Msg>
selected: false,
icon: None,
pad_h: None,
height: None,
font_size: None,
}
}
@@ -143,6 +149,26 @@ impl<Msg: Clone> ListItem<Msg>
self
}
/// Override the preferred row height (default: the theme row
/// height, `theme::HEIGHT` / `theme::HEIGHT_SUB` design px).
/// Accepts any [`Length`] so dense menus can trade the touch-target
/// generosity for row density. The resolved value is floored at the
/// label's rendered height so the text never clips.
pub fn height( mut self, h: impl Into<Length> ) -> Self
{
self.height = Some( h.into() );
self
}
/// Override the primary-label font size (default: `theme::LABEL_SIZE`
/// design px, resolved through the widget-scaling mode). Subtitle and
/// trailing text keep their theme sizes.
pub fn font_size( mut self, s: impl Into<Length> ) -> Self
{
self.font_size = Some( s.into() );
self
}
/// Set the message emitted when the row is tapped.
pub fn on_press( mut self, msg: Msg ) -> Self
{
@@ -157,13 +183,30 @@ impl<Msg: Clone> ListItem<Msg>
self
}
/// Effective primary-label font size on `canvas`, honouring the
/// [`Self::font_size`] override.
fn label_px( &self, canvas: &Canvas ) -> f32
{
match self.font_size
{
Some( l ) => canvas.resolve_font( l ),
None => canvas.font_px( theme::LABEL_SIZE ),
}
}
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
{
let h = if self.subtitle.is_some()
let h = match self.height
{
canvas.geom_px( theme::HEIGHT_SUB )
} else {
canvas.geom_px( theme::HEIGHT )
Some( l ) =>
{
// Floor at the label's physical height so the row can be
// squeezed but the text never clips.
let label_phys = self.label_px( canvas ) * canvas.dpi_scale();
canvas.resolve_geom( l ).max( label_phys + 4.0 )
}
None if self.subtitle.is_some() => canvas.geom_px( theme::HEIGHT_SUB ),
None => canvas.geom_px( theme::HEIGHT ),
};
( max_width, h )
}
@@ -206,7 +249,7 @@ 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 label_size = self.label_px( canvas );
let pad_h = self.pad_h
.map( |l| canvas.resolve_geom( l ) )
.unwrap_or_else( || canvas.geom_px( theme::PAD_H ) );
@@ -288,6 +331,8 @@ impl<Msg: Clone> ListItem<Msg>
selected: self.selected,
icon: self.icon,
pad_h: self.pad_h,
height: self.height,
font_size: self.font_size,
}
}
}

View File

@@ -84,7 +84,7 @@ impl<Msg: Clone> Radio<Msg>
}
/// Override the preferred height (default: the theme row height,
/// [`theme::HEIGHT`] design px). Accepts any [`Length`] so dense
/// `theme::HEIGHT` design px). Accepts any [`Length`] so dense
/// layouts can tie the row height to the viewport. The resolved
/// value is floored at the outer circle size so the ring never
/// clips; the circle keeps its theme size and stays centred.

View File

@@ -88,7 +88,7 @@ impl<Msg: Clone> Toggle<Msg>
}
/// Override the preferred height (default: the theme row height,
/// [`theme::HEIGHT`] design px). Accepts any [`Length`] so dense
/// `theme::HEIGHT` design px). Accepts any [`Length`] so dense
/// layouts can tie the row height to the viewport. The resolved
/// value is floored at the track height so the pill never clips;
/// the track keeps its theme size and stays vertically centred.

View File

@@ -30,13 +30,17 @@ pub struct Viewport<Msg: Clone>
/// the leading edge of the animation does not knife-cut against the layer
/// below it.
pub( crate ) fade_bottom: f32,
/// Resolve the child's viewport-relative and fluid `Length`s against
/// this viewport's own rect instead of the inherited root layout
/// viewport. See [`Self::local_viewport`].
pub( crate ) local_viewport: bool,
}
impl<Msg: Clone> Viewport<Msg>
{
pub fn new( child: impl Into<Element<Msg>> ) -> Self
{
Self { child: Box::new( child.into() ), width: None, height: None, fade_bottom: 0.0 }
Self { child: Box::new( child.into() ), width: None, height: None, fade_bottom: 0.0, local_viewport: false }
}
/// Set a fixed viewport width in logical pixels. Mirrors
@@ -66,6 +70,20 @@ impl<Msg: Clone> Viewport<Msg>
self
}
/// Resolve the child's viewport-relative (`vw` / `vh` / `vmin`) and
/// fluid `Length`s against this viewport's own rect instead of the
/// root surface the sub-canvas normally inherits. For a fixed-size
/// floating mini-UI (e.g. a phone-shaped panel pinned to a corner of
/// a desktop-wide surface) whose content is calibrated against the
/// panel rect and must not scale with the host surface. Scroll-like
/// clips should keep the default inheritance so content renders at
/// the same size inside and outside the clip.
pub fn local_viewport( mut self ) -> Self
{
self.local_viewport = true;
self
}
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> ( f32, f32 )
{
let inner_w = self.width.unwrap_or( max_width );
@@ -83,10 +101,11 @@ impl<Msg: Clone> Viewport<Msg>
{
Viewport
{
child: Box::new( self.child.map_arc( f ) ),
width: self.width,
height: self.height,
fade_bottom: self.fade_bottom,
child: Box::new( self.child.map_arc( f ) ),
width: self.width,
height: self.height,
fade_bottom: self.fade_bottom,
local_viewport: self.local_viewport,
}
}
}