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:
2026-05-13 19:38:41 +02:00
parent 96f437544a
commit 821037f509
6 changed files with 102 additions and 19 deletions

View File

@@ -138,6 +138,13 @@ pub( crate ) fn layout_and_draw<Msg: Clone>(
let saved_alpha = canvas.global_alpha();
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
// through to `c.background` when the slot is absent (third-
// party theme without the named surface — content still

View File

@@ -83,6 +83,12 @@ pub struct Container<Msg: Clone>
/// container's rounded rectangle, after the fill / surface and
/// before the child draws. `None` leaves the chrome flat.
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>
@@ -101,6 +107,7 @@ impl<Msg: Clone> Container<Msg>
pad_left: 0.0,
opacity: 1.0,
border: None,
max_width: None,
}
}
@@ -237,12 +244,22 @@ impl<Msg: Clone> Container<Msg>
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.
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_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 );
( cw + pad_x, ch + pad_y )
}
@@ -264,6 +281,7 @@ impl<Msg: Clone> Container<Msg>
pad_left: self.pad_left,
opacity: self.opacity,
border: self.border,
max_width: self.max_width,
}
}
}
@@ -378,5 +396,15 @@ mod tests
let c = container::<()>( spacer() ).surface( "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 );
}
}

View File

@@ -213,6 +213,7 @@ pub struct DatePicker<Msg: Clone>
pub on_change: Option<Arc<dyn Fn( Date ) -> Msg>>,
pub on_navigate: Option<Arc<dyn Fn( i32, u8 ) -> Msg>>,
pub locale: Locale,
pub width: Option<f32>,
}
impl<Msg: Clone + 'static> DatePicker<Msg>
@@ -231,6 +232,7 @@ impl<Msg: Clone + 'static> DatePicker<Msg>
on_change: None,
on_navigate: None,
locale: Locale::default(),
width: None,
}
}
@@ -274,6 +276,17 @@ impl<Msg: Clone + 'static> DatePicker<Msg>
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.
pub fn build( self ) -> Element<Msg>
{
@@ -290,6 +303,18 @@ impl<Msg: Clone + 'static> DatePicker<Msg>
let on_nav = self.on_navigate.clone();
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
// (`icons/catalogue/filled/multimedia/{previous,next}.svg`),
// 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
// sit at left / centre / right of the same rect) wrapped in a
// 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>()
.push_aligned( prev, HAlign::Start, VAlign::Center )
.push_aligned(
text( title ).size( theme::HEADER_FS ).color( theme::text() ),
text( title ).size( header_fs ).color( theme::text() ),
HAlign::Center, 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 cell: Element<Msg> = container::<Msg>(
text( dow_short( dow ) )
.size( theme::DOW_FS )
.size( dow_fs )
.color( theme::text_muted() )
.align_center(),
.align_center()
.no_truncate(),
)
.padding_h( 0.0 )
.padding_v( 4.0 )
@@ -404,9 +430,10 @@ impl<Msg: Clone + 'static> DatePicker<Msg>
let mut card = container::<Msg>(
text( format!( "{}", day ) )
.size( theme::DAY_FS )
.size( day_fs )
.color( label_color )
.align_center(),
.align_center()
.no_truncate(),
)
.padding_h( 4.0 )
.padding_v( 8.0 )

View File

@@ -20,11 +20,17 @@ pub enum TextAlign
pub struct Text
{
pub content: String,
pub size: f32,
pub color: Color,
pub align: TextAlign,
pub wrap: bool,
pub content: String,
pub size: f32,
pub color: Color,
pub align: TextAlign,
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
/// the active theme's font registry on every draw. `None` keeps
/// the canvas default font (Sora Regular in `ltk-theme-default`).
@@ -37,15 +43,22 @@ impl Text
{
Self
{
content: content.into(),
size: 16.0,
color: Color::WHITE,
align: TextAlign::Left,
wrap: false,
font: None,
content: content.into(),
size: 16.0,
color: Color::WHITE,
align: TextAlign::Left,
wrap: false,
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.
/// The triple is resolved through [`Canvas::font_for`] at draw
/// 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 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 ell_w = self.measure( ellipsis, canvas, font.as_ref() );