widget/toggle, widget/radio: height() builder to override the theme row height, floored at the control's visual so it never clips
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

Toggle and Radio report a fixed preferred height of 48 design px (theme::HEIGHT), which the fluid widget-scaling mode inflates to 72 px on large surfaces. That makes dense settings-style lists — several rows of label + control — the dominant consumer of vertical space on short landscape windows, with no way for the application to trade the built-in touch-target generosity for row density.
Both widgets gain a `height( impl Into<Length> )` builder storing an optional override that `preferred_size()` resolves through `canvas.resolve_geom()`, so callers can pass viewport-relative lengths (e.g. `Length::vh( 5.0 ).clamp( 30.0, 48.0 )`) and have the row height track the surface. The resolved value is floored at the control's visual size — the track height for Toggle, the outer circle for Radio — so the pill or ring never clips regardless of how aggressive the override is; the visual itself keeps its theme size and stays vertically centred in whatever rect the layout assigns, exactly as before. Without the builder both widgets behave identically to the previous fixed-height code.
This commit is contained in:
2026-08-01 12:07:56 +02:00
parent 806dee5167
commit 4247613bb0
2 changed files with 44 additions and 6 deletions

View File

@@ -1,7 +1,7 @@
// SPDX-License-Identifier: LGPL-2.1-only // SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net> // Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
use crate::types::{ Rect, WidgetId }; use crate::types::{ Length, Rect, WidgetId };
use crate::render::Canvas; use crate::render::Canvas;
use super::Element; use super::Element;
@@ -48,6 +48,8 @@ pub struct Radio<Msg: Clone>
pub( crate ) label: Option<String>, pub( crate ) label: Option<String>,
/// Optional stable identifier for focus management. /// Optional stable identifier for focus management.
pub( crate ) id: Option<WidgetId>, pub( crate ) id: Option<WidgetId>,
/// Optional override of the theme row height.
pub( crate ) height: Option<Length>,
} }
impl<Msg: Clone> Radio<Msg> impl<Msg: Clone> Radio<Msg>
@@ -55,7 +57,7 @@ impl<Msg: Clone> Radio<Msg>
/// Create a radio in the given state, with no label and no callback. /// Create a radio in the given state, with no label and no callback.
pub fn new( selected: bool ) -> Self pub fn new( selected: bool ) -> Self
{ {
Self { selected, on_select: None, label: None, id: None } Self { selected, on_select: None, label: None, id: None, height: None }
} }
/// Set the message emitted when this option is picked. The /// Set the message emitted when this option is picked. The
@@ -81,6 +83,17 @@ impl<Msg: Clone> Radio<Msg>
self self
} }
/// Override the preferred height (default: the theme row height,
/// [`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.
pub fn height( mut self, h: impl Into<Length> ) -> Self
{
self.height = Some( h.into() );
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 outer = canvas.geom_px( theme::OUTER_SIZE ); let outer = canvas.geom_px( theme::OUTER_SIZE );
@@ -91,7 +104,12 @@ impl<Msg: Clone> Radio<Msg>
} else { } else {
outer.min( max_width ) outer.min( max_width )
}; };
( w, canvas.geom_px( theme::HEIGHT ) ) let h = match self.height
{
Some( l ) => canvas.resolve_geom( l ).max( outer ),
None => canvas.geom_px( theme::HEIGHT ),
};
( w, h )
} }
/// Focus ring on the outer circle extends `FOCUS_W + 2 + FOCUS_W/2 ≈ 6.5 px` /// Focus ring on the outer circle extends `FOCUS_W + 2 + FOCUS_W/2 ≈ 6.5 px`
@@ -159,6 +177,7 @@ impl<Msg: Clone> Radio<Msg>
on_select: self.on_select.map( |m| ( *f )( m ) ), on_select: self.on_select.map( |m| ( *f )( m ) ),
label: self.label, label: self.label,
id: self.id, id: self.id,
height: self.height,
} }
} }
} }

View File

@@ -1,7 +1,7 @@
// SPDX-License-Identifier: LGPL-2.1-only // SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net> // Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
use crate::types::{ Rect, WidgetId }; use crate::types::{ Length, Rect, WidgetId };
use crate::render::Canvas; use crate::render::Canvas;
use super::Element; use super::Element;
@@ -46,6 +46,8 @@ pub struct Toggle<Msg: Clone>
pub( crate ) label: Option<String>, pub( crate ) label: Option<String>,
/// Optional stable identifier for focus management. /// Optional stable identifier for focus management.
pub( crate ) id: Option<WidgetId>, pub( crate ) id: Option<WidgetId>,
/// Optional override of the theme row height.
pub( crate ) height: Option<Length>,
} }
impl<Msg: Clone> Toggle<Msg> impl<Msg: Clone> Toggle<Msg>
@@ -56,7 +58,7 @@ impl<Msg: Clone> Toggle<Msg>
/// widget tree, otherwise the toggle is decorative. /// widget tree, otherwise the toggle is decorative.
pub fn new( value: bool ) -> Self pub fn new( value: bool ) -> Self
{ {
Self { value, on_toggle: None, label: None, id: None } Self { value, on_toggle: None, label: None, id: None, height: None }
} }
/// Set the message emitted when the toggle is activated (tap, Enter or /// Set the message emitted when the toggle is activated (tap, Enter or
@@ -85,6 +87,17 @@ impl<Msg: Clone> Toggle<Msg>
self self
} }
/// Override the preferred height (default: the theme row height,
/// [`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.
pub fn height( mut self, h: impl Into<Length> ) -> Self
{
self.height = Some( h.into() );
self
}
/// Return the preferred `(width, height)` given available `max_width`. /// Return the preferred `(width, height)` given available `max_width`.
/// ///
/// Width is `track_width` for an unlabelled toggle, or /// Width is `track_width` for an unlabelled toggle, or
@@ -100,7 +113,12 @@ impl<Msg: Clone> Toggle<Msg>
} else { } else {
track_w.min( max_width ) track_w.min( max_width )
}; };
( w, canvas.geom_px( theme::HEIGHT ) ) let h = match self.height
{
Some( l ) => canvas.resolve_geom( l ).max( canvas.geom_px( theme::TRACK_H ) ),
None => canvas.geom_px( theme::HEIGHT ),
};
( w, h )
} }
/// Bounding box of everything painted at `rect` across all states. The /// Bounding box of everything painted at `rect` across all states. The
@@ -179,6 +197,7 @@ impl<Msg: Clone> Toggle<Msg>
on_toggle: self.on_toggle.map( |m| ( *f )( m ) ), on_toggle: self.on_toggle.map( |m| ( *f )( m ) ),
label: self.label, label: self.label,
id: self.id, id: self.id,
height: self.height,
} }
} }
} }