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`]
|
||||
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
|
||||
card interactive.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -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() );
|
||||
|
||||
Reference in New Issue
Block a user