container, text, date_picker: width-aware sizing pass
`Container::max_width(px)` mirrors the same flag on `Column` / `Row` — the container reports `min( offered, px )` upwards and the draw pass caps `rect.width` to `px`, so a decorated child wrapped in `container().max_width(260)` no longer needs a `column()`-of-one shell just to access the cap. Propagated through `map_msg`, covered by a test, and documented under the `container` section of `docs/widgets.md`.
`Text::no_truncate()` opts out of the default ellipsis behaviour: when `truncate = false` the draw pass paints the full string even if `measure(text) > rect.width`. Useful for very short labels (calendar days "1"–"31", day-of-week stubs "Lu" / "Mi" / "Sá") inside grid slots whose width is dictated by the parent — a couple of pixels of overflow centred in the rect is invisible, while "..." in place of a single-digit number is loud.
`DatePicker::width(px)` is a layout hint: when set, `build()` derives `header_fs`, `dow_fs` and `day_fs` from the slot width so the worst-case label in each row ("September 2026" in the header, "Mié" / "Sáb" in the DOW row, "30" in the day cell) fits at a sensible size; the design defaults stay as upper bounds. Day and DOW cells also flip on `Text::no_truncate()` as a safety net — heuristic font sizing can't perfectly predict glyph widths across families, and overflowing one pixel beats truncating to "...".
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Cargo.lock
|
||||||
|
target/
|
||||||
@@ -384,6 +384,12 @@ Per-edge padding is available with `padding_top` / `padding_right` /
|
|||||||
`padding_bottom` / `padding_left`. Per-corner radius takes a [`Corners`]
|
`padding_bottom` / `padding_left`. Per-corner radius takes a [`Corners`]
|
||||||
struct or a tuple.
|
struct or a tuple.
|
||||||
|
|
||||||
|
`max_width(px)` caps the container's outer width — when the parent
|
||||||
|
offers a wider rect, the container reports `min( offered, px )` as its
|
||||||
|
preferred width instead of stretching to fill. Mirrors the same flag on
|
||||||
|
[`column`](#column) and [`row`](#row), so a single decorated child no
|
||||||
|
longer needs a `column`-of-one wrap just to access a width cap.
|
||||||
|
|
||||||
**See also**: [`pressable`](#pressable) wrapping a `container` makes a
|
**See also**: [`pressable`](#pressable) wrapping a `container` makes a
|
||||||
card interactive.
|
card interactive.
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,13 @@ pub( crate ) fn layout_and_draw<Msg: Clone>(
|
|||||||
let saved_alpha = canvas.global_alpha();
|
let saved_alpha = canvas.global_alpha();
|
||||||
canvas.set_global_alpha( saved_alpha * c.opacity );
|
canvas.set_global_alpha( saved_alpha * c.opacity );
|
||||||
|
|
||||||
|
let rect = if let Some( mw ) = c.max_width
|
||||||
|
{
|
||||||
|
crate::types::Rect { width: rect.width.min( mw ), ..rect }
|
||||||
|
} else {
|
||||||
|
rect
|
||||||
|
};
|
||||||
|
|
||||||
// Surface slot takes precedence over flat background; falls
|
// Surface slot takes precedence over flat background; falls
|
||||||
// through to `c.background` when the slot is absent (third-
|
// through to `c.background` when the slot is absent (third-
|
||||||
// party theme without the named surface — content still
|
// party theme without the named surface — content still
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ pub struct Container<Msg: Clone>
|
|||||||
/// container's rounded rectangle, after the fill / surface and
|
/// container's rounded rectangle, after the fill / surface and
|
||||||
/// before the child draws. `None` leaves the chrome flat.
|
/// before the child draws. `None` leaves the chrome flat.
|
||||||
pub border: Option<( Color, f32 )>,
|
pub border: Option<( Color, f32 )>,
|
||||||
|
/// Optional hard cap on the container's outer width. When the parent
|
||||||
|
/// offers more, the container reports its preferred width as
|
||||||
|
/// `min( offered, max_width )` so it does not stretch to fill.
|
||||||
|
/// Mirrors the same flag on [`Column`](crate::layout::column::Column)
|
||||||
|
/// and [`Row`](crate::layout::row::Row).
|
||||||
|
pub max_width: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Msg: Clone> Container<Msg>
|
impl<Msg: Clone> Container<Msg>
|
||||||
@@ -101,6 +107,7 @@ impl<Msg: Clone> Container<Msg>
|
|||||||
pad_left: 0.0,
|
pad_left: 0.0,
|
||||||
opacity: 1.0,
|
opacity: 1.0,
|
||||||
border: None,
|
border: None,
|
||||||
|
max_width: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,12 +244,22 @@ impl<Msg: Clone> Container<Msg>
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cap the container's outer width in logical px. When the parent
|
||||||
|
/// offers a wider rect, the container reports its preferred width as
|
||||||
|
/// `min( offered, w )` so it does not stretch to fill.
|
||||||
|
pub fn max_width( mut self, w: f32 ) -> Self
|
||||||
|
{
|
||||||
|
self.max_width = Some( w );
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the preferred `(width, height)` accounting for padding.
|
/// Return the preferred `(width, height)` accounting for padding.
|
||||||
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 avail = self.max_width.map( |m| max_width.min( m ) ).unwrap_or( max_width );
|
||||||
let pad_x = self.pad_left + self.pad_right;
|
let pad_x = self.pad_left + self.pad_right;
|
||||||
let pad_y = self.pad_top + self.pad_bottom;
|
let pad_y = self.pad_top + self.pad_bottom;
|
||||||
let inner_w = ( max_width - pad_x ).max( 0.0 );
|
let inner_w = ( avail - pad_x ).max( 0.0 );
|
||||||
let ( cw, ch ) = self.child.preferred_size( inner_w, canvas );
|
let ( cw, ch ) = self.child.preferred_size( inner_w, canvas );
|
||||||
( cw + pad_x, ch + pad_y )
|
( cw + pad_x, ch + pad_y )
|
||||||
}
|
}
|
||||||
@@ -264,6 +281,7 @@ impl<Msg: Clone> Container<Msg>
|
|||||||
pad_left: self.pad_left,
|
pad_left: self.pad_left,
|
||||||
opacity: self.opacity,
|
opacity: self.opacity,
|
||||||
border: self.border,
|
border: self.border,
|
||||||
|
max_width: self.max_width,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,5 +396,15 @@ mod tests
|
|||||||
let c = container::<()>( spacer() ).surface( "surface-card" );
|
let c = container::<()>( spacer() ).surface( "surface-card" );
|
||||||
assert_eq!( c.surface.as_deref(), Some( "surface-card" ) );
|
assert_eq!( c.surface.as_deref(), Some( "surface-card" ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn max_width_caps_preferred_width()
|
||||||
|
{
|
||||||
|
use crate::render::Canvas;
|
||||||
|
let canvas = Canvas::new( 800, 600 );
|
||||||
|
let c = container::<()>( spacer() ).max_width( 200.0 );
|
||||||
|
let ( w, _ ) = c.preferred_size( 800.0, &canvas );
|
||||||
|
assert!( w <= 200.0 );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ pub struct DatePicker<Msg: Clone>
|
|||||||
pub on_change: Option<Arc<dyn Fn( Date ) -> Msg>>,
|
pub on_change: Option<Arc<dyn Fn( Date ) -> Msg>>,
|
||||||
pub on_navigate: Option<Arc<dyn Fn( i32, u8 ) -> Msg>>,
|
pub on_navigate: Option<Arc<dyn Fn( i32, u8 ) -> Msg>>,
|
||||||
pub locale: Locale,
|
pub locale: Locale,
|
||||||
|
pub width: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Msg: Clone + 'static> DatePicker<Msg>
|
impl<Msg: Clone + 'static> DatePicker<Msg>
|
||||||
@@ -231,6 +232,7 @@ impl<Msg: Clone + 'static> DatePicker<Msg>
|
|||||||
on_change: None,
|
on_change: None,
|
||||||
on_navigate: None,
|
on_navigate: None,
|
||||||
locale: Locale::default(),
|
locale: Locale::default(),
|
||||||
|
width: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +276,17 @@ impl<Msg: Clone + 'static> DatePicker<Msg>
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Outer width the picker will be laid out at. Used to shrink the
|
||||||
|
/// header / day-of-week / day-cell font sizes so the widest labels
|
||||||
|
/// ("30", "September 2026", "Mié") still fit without ellipsis. When
|
||||||
|
/// unset, the picker uses the design defaults and may truncate
|
||||||
|
/// inside very narrow rects.
|
||||||
|
pub fn width( mut self, w: f32 ) -> Self
|
||||||
|
{
|
||||||
|
self.width = Some( w );
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Build the `Element` tree representing this date picker.
|
/// Build the `Element` tree representing this date picker.
|
||||||
pub fn build( self ) -> Element<Msg>
|
pub fn build( self ) -> Element<Msg>
|
||||||
{
|
{
|
||||||
@@ -290,6 +303,18 @@ impl<Msg: Clone + 'static> DatePicker<Msg>
|
|||||||
let on_nav = self.on_navigate.clone();
|
let on_nav = self.on_navigate.clone();
|
||||||
let first = self.locale.first_dow;
|
let first = self.locale.first_dow;
|
||||||
|
|
||||||
|
let ( header_fs, dow_fs, day_fs ) = if let Some( w ) = self.width
|
||||||
|
{
|
||||||
|
let inner_w = ( w - 2.0 * theme::PADDING ).max( 8.0 );
|
||||||
|
let slot_w = ( ( inner_w - 6.0 * theme::SPACING ) / 7.0 ).max( 4.0 );
|
||||||
|
let day = ( ( slot_w - 8.0 ) / 1.6 ).clamp( 8.0, theme::DAY_FS );
|
||||||
|
let dow = ( slot_w / 2.0 ).clamp( 8.0, theme::DOW_FS );
|
||||||
|
let head = ( ( inner_w - 52.0 ) / ( 14.0 * 0.7 ) ).clamp( 10.0, theme::HEADER_FS );
|
||||||
|
( head, dow, day )
|
||||||
|
} else {
|
||||||
|
( theme::HEADER_FS, theme::DOW_FS, theme::DAY_FS )
|
||||||
|
};
|
||||||
|
|
||||||
// Header chevrons load from the active theme as SVG icons
|
// Header chevrons load from the active theme as SVG icons
|
||||||
// (`icons/catalogue/filled/multimedia/{previous,next}.svg`),
|
// (`icons/catalogue/filled/multimedia/{previous,next}.svg`),
|
||||||
// tinted to the primary text colour so they read against
|
// tinted to the primary text colour so they read against
|
||||||
@@ -333,11 +358,11 @@ impl<Msg: Clone + 'static> DatePicker<Msg>
|
|||||||
// `Stack` (the only layout that lets independent children
|
// `Stack` (the only layout that lets independent children
|
||||||
// sit at left / centre / right of the same rect) wrapped in a
|
// sit at left / centre / right of the same rect) wrapped in a
|
||||||
// container that fixes the row height.
|
// container that fixes the row height.
|
||||||
let header_height = ( CHEVRON_PX as f32 + 16.0 ).max( theme::HEADER_FS + 16.0 );
|
let header_height = ( CHEVRON_PX as f32 + 16.0 ).max( header_fs + 16.0 );
|
||||||
let header_inner: Element<Msg> = stack::<Msg>()
|
let header_inner: Element<Msg> = stack::<Msg>()
|
||||||
.push_aligned( prev, HAlign::Start, VAlign::Center )
|
.push_aligned( prev, HAlign::Start, VAlign::Center )
|
||||||
.push_aligned(
|
.push_aligned(
|
||||||
text( title ).size( theme::HEADER_FS ).color( theme::text() ),
|
text( title ).size( header_fs ).color( theme::text() ),
|
||||||
HAlign::Center, VAlign::Center,
|
HAlign::Center, VAlign::Center,
|
||||||
)
|
)
|
||||||
.push_aligned( next, HAlign::End, VAlign::Center )
|
.push_aligned( next, HAlign::End, VAlign::Center )
|
||||||
@@ -360,9 +385,10 @@ impl<Msg: Clone + 'static> DatePicker<Msg>
|
|||||||
let dow = ( ( slot + first ) % 7 ) as u8;
|
let dow = ( ( slot + first ) % 7 ) as u8;
|
||||||
let cell: Element<Msg> = container::<Msg>(
|
let cell: Element<Msg> = container::<Msg>(
|
||||||
text( dow_short( dow ) )
|
text( dow_short( dow ) )
|
||||||
.size( theme::DOW_FS )
|
.size( dow_fs )
|
||||||
.color( theme::text_muted() )
|
.color( theme::text_muted() )
|
||||||
.align_center(),
|
.align_center()
|
||||||
|
.no_truncate(),
|
||||||
)
|
)
|
||||||
.padding_h( 0.0 )
|
.padding_h( 0.0 )
|
||||||
.padding_v( 4.0 )
|
.padding_v( 4.0 )
|
||||||
@@ -404,9 +430,10 @@ impl<Msg: Clone + 'static> DatePicker<Msg>
|
|||||||
|
|
||||||
let mut card = container::<Msg>(
|
let mut card = container::<Msg>(
|
||||||
text( format!( "{}", day ) )
|
text( format!( "{}", day ) )
|
||||||
.size( theme::DAY_FS )
|
.size( day_fs )
|
||||||
.color( label_color )
|
.color( label_color )
|
||||||
.align_center(),
|
.align_center()
|
||||||
|
.no_truncate(),
|
||||||
)
|
)
|
||||||
.padding_h( 4.0 )
|
.padding_h( 4.0 )
|
||||||
.padding_v( 8.0 )
|
.padding_v( 8.0 )
|
||||||
|
|||||||
@@ -20,11 +20,17 @@ pub enum TextAlign
|
|||||||
|
|
||||||
pub struct Text
|
pub struct Text
|
||||||
{
|
{
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub size: f32,
|
pub size: f32,
|
||||||
pub color: Color,
|
pub color: Color,
|
||||||
pub align: TextAlign,
|
pub align: TextAlign,
|
||||||
pub wrap: bool,
|
pub wrap: bool,
|
||||||
|
/// When `true` (default), overflowing single-line text is truncated
|
||||||
|
/// with an ellipsis. When `false`, the full string is painted even
|
||||||
|
/// if it extends past the layout rect — useful for very short
|
||||||
|
/// labels (calendar day numbers, day-of-week stubs) where a couple
|
||||||
|
/// of pixels of overflow is invisible but "..." is noisy.
|
||||||
|
pub truncate: bool,
|
||||||
/// Optional `(family, weight, style)` override resolved through
|
/// Optional `(family, weight, style)` override resolved through
|
||||||
/// the active theme's font registry on every draw. `None` keeps
|
/// the active theme's font registry on every draw. `None` keeps
|
||||||
/// the canvas default font (Sora Regular in `ltk-theme-default`).
|
/// the canvas default font (Sora Regular in `ltk-theme-default`).
|
||||||
@@ -37,15 +43,22 @@ impl Text
|
|||||||
{
|
{
|
||||||
Self
|
Self
|
||||||
{
|
{
|
||||||
content: content.into(),
|
content: content.into(),
|
||||||
size: 16.0,
|
size: 16.0,
|
||||||
color: Color::WHITE,
|
color: Color::WHITE,
|
||||||
align: TextAlign::Left,
|
align: TextAlign::Left,
|
||||||
wrap: false,
|
wrap: false,
|
||||||
font: None,
|
truncate: true,
|
||||||
|
font: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn no_truncate( mut self ) -> Self
|
||||||
|
{
|
||||||
|
self.truncate = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set a `(family, weight, style)` override for this text node.
|
/// Set a `(family, weight, style)` override for this text node.
|
||||||
/// The triple is resolved through [`Canvas::font_for`] at draw
|
/// The triple is resolved through [`Canvas::font_for`] at draw
|
||||||
/// time, so missing weights fall through the registry's nearest-
|
/// time, so missing weights fall through the registry's nearest-
|
||||||
@@ -187,7 +200,7 @@ impl Text
|
|||||||
|
|
||||||
let text_w = self.measure( &self.content, canvas, font.as_ref() );
|
let text_w = self.measure( &self.content, canvas, font.as_ref() );
|
||||||
|
|
||||||
let display = if text_w > rect.width && rect.width > 0.0
|
let display = if self.truncate && text_w > rect.width && rect.width > 0.0
|
||||||
{
|
{
|
||||||
let ellipsis = "...";
|
let ellipsis = "...";
|
||||||
let ell_w = self.measure( ellipsis, canvas, font.as_ref() );
|
let ell_w = self.measure( ellipsis, canvas, font.as_ref() );
|
||||||
|
|||||||
Reference in New Issue
Block a user