diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e7caa9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target/ diff --git a/docs/widgets.md b/docs/widgets.md index 8fc010c..c248fc8 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -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. diff --git a/src/draw/layout.rs b/src/draw/layout.rs index cf90cc5..a85d07d 100644 --- a/src/draw/layout.rs +++ b/src/draw/layout.rs @@ -138,6 +138,13 @@ pub( crate ) fn layout_and_draw( 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 diff --git a/src/widget/container.rs b/src/widget/container.rs index e9295c1..2d78f00 100644 --- a/src/widget/container.rs +++ b/src/widget/container.rs @@ -83,6 +83,12 @@ pub struct Container /// 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, } impl Container @@ -101,6 +107,7 @@ impl Container pad_left: 0.0, opacity: 1.0, border: None, + max_width: None, } } @@ -237,12 +244,22 @@ impl Container 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 Container 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 ); + } } diff --git a/src/widget/date_picker.rs b/src/widget/date_picker.rs index f52b187..07d77c0 100644 --- a/src/widget/date_picker.rs +++ b/src/widget/date_picker.rs @@ -213,6 +213,7 @@ pub struct DatePicker pub on_change: Option Msg>>, pub on_navigate: Option Msg>>, pub locale: Locale, + pub width: Option, } impl DatePicker @@ -231,6 +232,7 @@ impl DatePicker on_change: None, on_navigate: None, locale: Locale::default(), + width: None, } } @@ -274,6 +276,17 @@ impl DatePicker 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 { @@ -290,6 +303,18 @@ impl DatePicker 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 DatePicker // `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 = stack::() .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 DatePicker let dow = ( ( slot + first ) % 7 ) as u8; let cell: Element = container::( 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 DatePicker let mut card = container::( 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 ) diff --git a/src/widget/text.rs b/src/widget/text.rs index b93043f..174da1e 100644 --- a/src/widget/text.rs +++ b/src/widget/text.rs @@ -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() );