row, button, widget: distribute a row's width shortfall instead of overflowing, elide button labels into the rect they are granted, icon_size takes a Length
Row deficit distribution (layout/row.rs). `Row` knew how to hand out leftover width but had no notion of a shortfall: `leftover` was floored at zero and every child was laid out at its preferred width, so a cluster wider than its rect simply overflowed — and symmetrically, because the no-spacer branch centres the block, which is why both end labels of a segmented control were clipped at once rather than only the trailing one. The shortfall now comes off the widest children first, by water filling: `width_cap` sorts the flexible widths and returns the largest per-child cap `c` for which `sum( min( w, c ) ) <= available`, so a long title absorbs the whole deficit while an icon button beside it keeps its size, and equal siblings — a segmented control — share it evenly. A uniform scale-down, which was the first attempt, was wrong precisely there: it thinned a header's back arrow along with the title it sat next to. Spacers and flex children stay out of the flexible set, so a pinned gap keeps the width it was given — an explicit spacer is a decision, not slack — and `Row::no_shrink()` opts a row out entirely, for strips deliberately wider than their viewport such as a carousel rail meant to be scrolled. `align_right` and the centring branch now measure the laid width rather than the preferred one, so a shrunken row is positioned against what it actually occupies. Button label elision (widget/button/mod.rs, widget/mod.rs, widget/list_item/mod.rs). Shrinking a row is only half the fix: a leaf handed less width than it asked for still painted its full string, so the deficit came out clipped instead of truncated. `draw_text_button` now elides the label against the rect the layout granted, less the horizontal padding, for all three variants. The truncation rule moves out of `ListItem`, where it was a private helper, into a single crate-internal `widget::elide` the two share. `Text` keeps its own inline copy for now: it measures through an optional font override that this signature does not carry, and folding the two together is a separate change. icon_size as a Length (widget/button/mod.rs). `Button::icon_size` was the one geometry setter that ignored the widget-scaling mode — it took a bare `f32`, pinned it, and used `0.0` as the "unset" sentinel. It now takes `impl Into<Length>` behind an `Option` and resolves through `Canvas::resolve_geom`, the way `height`, `width` and `font_size` already do. A bare number still means `Length::px`, so every existing call keeps its exact size; what it adds is `Length::widget( n )`, which follows the active mode the way stock icons do. Without it a back arrow pinned at 21 px sat next to a `list_item` chevron that fluid sizing had grown well past 21, and read as visibly smaller on the same row.
This commit is contained in:
@@ -32,6 +32,7 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- **`Button::icon_size` takes `impl Into<Length>`** and resolves through `Canvas::resolve_geom`, joining `font_size` / `height` / `width`. A bare `f32` still means `Length::px` — every existing call keeps its exact size — but a caller can now pass `Length::widget( n )` to have an icon button follow the widget-scaling mode the way stock icons do. Without it an explicit `icon_size` was the one geometry setter that ignored the mode, so a 21 px back arrow sat next to a `list_item` chevron that fluid sizing had grown well past 21 and looked visibly smaller.
|
||||||
- **`Length::dp` now applies the density at resolution time, not at construction.** The value carries its design pixels in a new `LengthBase::Dp` variant and `resolve` multiplies by the density in effect when it runs, so a `set_density` change takes effect on the next paint without rebuilding the view's lengths — previously a `dp` value was frozen to the density read when it was constructed. Behaviour is unchanged for code that sets density once at startup.
|
- **`Length::dp` now applies the density at resolution time, not at construction.** The value carries its design pixels in a new `LengthBase::Dp` variant and `resolve` multiplies by the density in effect when it runs, so a `set_density` change takes effect on the next paint without rebuilding the view's lengths — previously a `dp` value was frozen to the density read when it was constructed. Behaviour is unchanged for code that sets density once at startup.
|
||||||
- **The GLES image texture cache is now bounded** to 32 MiB of estimated GPU memory with least-recently-drawn eviction (the in-use entry is never evicted). Previously it grew without limit for the canvas' lifetime, so a stream of distinct buffers (photo carousel, video thumbnails) could exhaust GPU memory.
|
- **The GLES image texture cache is now bounded** to 32 MiB of estimated GPU memory with least-recently-drawn eviction (the in-use entry is never evicted). Previously it grew without limit for the canvas' lifetime, so a stream of distinct buffers (photo carousel, video thumbnails) could exhaust GPU memory.
|
||||||
- **`OverlaySpec::size`** is now `( Length, Length )` (was `( u32, u32 )`), resolved against the main surface when the overlay is materialized; wrap existing sizes in `Length::px( … )` for the old fixed behaviour.
|
- **`OverlaySpec::size`** is now `( Length, Length )` (was `( u32, u32 )`), resolved against the main surface when the overlay is materialized; wrap existing sizes in `Length::px( … )` for the old fixed behaviour.
|
||||||
@@ -42,6 +43,7 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- **Rows no longer spill their children out of the layout.** `Row` distributed leftover width but had no notion of a shortfall: `leftover` floored at zero, so a cluster wider than its rect kept every preferred width and overflowed — symmetrically, since the no-spacer branch centres it, which is why both end labels of a segmented control were clipped at once. The shortfall now comes off the widest children first (water filling), so a long title gives up its width while the icon beside it keeps its size, and equal siblings — a segmented control — share it evenly; pinned spacers keep their width, and `Row::no_shrink()` opts out for strips deliberately wider than their viewport. `Button` elides its label into the rect it is granted, so shrinking degrades to a truncated label rather than clipped glyphs; the truncation rule moves to one crate-internal helper shared with `ListItem`.
|
||||||
- **`ListItem` labels no longer run under the trailing slot.** The label and subtitle were painted with no width limit, so a title longer than the row overlapped the trailing text or the disclosure icon instead of truncating. The trailing slots are now laid out first and both lines are elided with an ellipsis against the space they leave, matching what `Text` already did.
|
- **`ListItem` labels no longer run under the trailing slot.** The label and subtitle were painted with no width limit, so a title longer than the row overlapped the trailing text or the disclosure icon instead of truncating. The trailing slots are now laid out first and both lines are elided with an ellipsis against the space they leave, matching what `Text` already did.
|
||||||
- **Text inputs no longer insert mid-string when the value grows after focus.** Focusing a text input pinned the cursor to a snapshot of `value.len()`; if the value kept growing without the widget seeing keystrokes (a field fed over IPC), the first normally-delivered key inserted at the stale position. The focus-time cursor is now an end-of-value sentinel that every consumer clamps to the current value length, collapsing to a concrete position on the first real keystroke or click.
|
- **Text inputs no longer insert mid-string when the value grows after focus.** Focusing a text input pinned the cursor to a snapshot of `value.len()`; if the value kept growing without the widget seeing keystrokes (a field fed over IPC), the first normally-delivered key inserted at the stale position. The focus-time cursor is now an end-of-value sentinel that every consumer clamps to the current value length, collapsing to a concrete position on the first real keystroke or click.
|
||||||
- **Single-line caret height now follows the text line.** The caret spanned `rect.height - 16`, so a field taller than its text line grew an oversized caret; it now measures `font_size + 4` and is vertically centered like the text, matching the multiline caret.
|
- **Single-line caret height now follows the text line.** The caret spanned `rect.height - 16`, so a field taller than its text line grew an oversized caret; it now measures `font_size + 4` and is vertically centered like the text, matching the multiline caret.
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ icon_button( rgba_bytes, w, h ).on_press( Msg::OpenSearch )
|
|||||||
# }
|
# }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`icon_size( n )` sets the drawn size. A bare number is an absolute pixel
|
||||||
|
count; pass `Length::widget( n )` instead to follow the widget-scaling
|
||||||
|
mode, which is what stock icons (the [`list_item`](#list_item) disclosure
|
||||||
|
arrow) already do — use it when the two sit side by side and should match.
|
||||||
|
|
||||||
**See also**: [`button`](#button), [`window_button`](#window_button)
|
**See also**: [`button`](#button), [`window_button`](#window_button)
|
||||||
(specialised icon button for window decorations).
|
(specialised icon button for window decorations).
|
||||||
|
|
||||||
@@ -954,6 +959,15 @@ row()
|
|||||||
# }
|
# }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When the children do not fit, the row takes the shortfall off the widest
|
||||||
|
ones first, so a long label gives up its width while a narrow neighbour
|
||||||
|
(an icon button beside a title) keeps its size, and equal siblings share
|
||||||
|
it evenly. Leaves that paint a line of text (`button`, `list_item`)
|
||||||
|
truncate with an ellipsis into what they are given. Pinned spacers keep
|
||||||
|
their width — an explicit gap is a decision, not slack. Use
|
||||||
|
`no_shrink()` for strips that are meant to be wider than their viewport
|
||||||
|
and scrolled.
|
||||||
|
|
||||||
### `stack`
|
### `stack`
|
||||||
|
|
||||||
Z-order overlay. Each child gets explicit horizontal and vertical
|
Z-order overlay. Each child gets explicit horizontal and vertical
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ pub struct Row<Msg: Clone>
|
|||||||
pub( crate ) align_right: bool,
|
pub( crate ) align_right: bool,
|
||||||
pub( crate ) align_top: bool,
|
pub( crate ) align_top: bool,
|
||||||
pub( crate ) fill_height: bool,
|
pub( crate ) fill_height: bool,
|
||||||
|
/// When false the row keeps every child at its preferred width even if
|
||||||
|
/// the cluster overflows. Default true.
|
||||||
|
pub( crate ) shrink: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Msg: Clone> Row<Msg>
|
impl<Msg: Clone> Row<Msg>
|
||||||
@@ -63,6 +66,7 @@ impl<Msg: Clone> Row<Msg>
|
|||||||
align_right: false,
|
align_right: false,
|
||||||
align_top: false,
|
align_top: false,
|
||||||
fill_height: false,
|
fill_height: false,
|
||||||
|
shrink: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +105,19 @@ impl<Msg: Clone> Row<Msg>
|
|||||||
canvas.resolve_geom( self.padding )
|
canvas.resolve_geom( self.padding )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Keep every child at its preferred width even when the cluster does not
|
||||||
|
/// fit, letting it overflow the row's rect.
|
||||||
|
///
|
||||||
|
/// The default distributes the shortfall instead, which is what a row of
|
||||||
|
/// labelled buttons wants. Opt out for strips that are deliberately wider
|
||||||
|
/// than their viewport — a carousel rail, a tray meant to be scrolled —
|
||||||
|
/// where shrinking to fit would defeat the point.
|
||||||
|
pub fn no_shrink( mut self ) -> Self
|
||||||
|
{
|
||||||
|
self.shrink = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Push the content block to the right edge of the available width.
|
/// Push the content block to the right edge of the available width.
|
||||||
pub fn align_right( mut self ) -> Self
|
pub fn align_right( mut self ) -> Self
|
||||||
{
|
{
|
||||||
@@ -232,7 +249,27 @@ impl<Msg: Clone> Row<Msg>
|
|||||||
.sum();
|
.sum();
|
||||||
|
|
||||||
let inner_w = ( rect.width - pad * 2.0 ).max( 0.0 );
|
let inner_w = ( rect.width - pad * 2.0 ).max( 0.0 );
|
||||||
let leftover = ( inner_w - fixed_w - gaps ).max( 0.0 );
|
|
||||||
|
// Deficit distribution: cap the widest children first, so a long
|
||||||
|
// label gives up its width before a neighbouring icon does. A uniform
|
||||||
|
// scale-down would thin a header's back arrow along with its title.
|
||||||
|
// Spacers keep the width they were pinned to — an explicit gap is a
|
||||||
|
// decision, not slack.
|
||||||
|
let mut flexible: Vec<f32> = self.children.iter().zip( sizes.iter() )
|
||||||
|
.filter( |( c, _ )| !matches!( c, Element::Flex( _ ) | Element::Spacer( _ ) ) )
|
||||||
|
.map( |( _, ( w, _ ) )| *w )
|
||||||
|
.collect();
|
||||||
|
let flexible_w: f32 = flexible.iter().sum();
|
||||||
|
let rigid_w = ( fixed_w - flexible_w ).max( 0.0 );
|
||||||
|
let cap = if self.shrink
|
||||||
|
{
|
||||||
|
width_cap( &mut flexible, ( inner_w - gaps - rigid_w ).max( 0.0 ) )
|
||||||
|
} else {
|
||||||
|
f32::INFINITY
|
||||||
|
};
|
||||||
|
|
||||||
|
let laid_w = rigid_w + flexible.iter().map( |w| w.min( cap ) ).sum::<f32>();
|
||||||
|
let leftover = ( inner_w - laid_w - gaps ).max( 0.0 );
|
||||||
let has_spacers = total_weight > 0;
|
let has_spacers = total_weight > 0;
|
||||||
|
|
||||||
let ( start_x, flex_unit ) = if has_spacers
|
let ( start_x, flex_unit ) = if has_spacers
|
||||||
@@ -243,11 +280,11 @@ impl<Msg: Clone> Row<Msg>
|
|||||||
}
|
}
|
||||||
else if self.align_right
|
else if self.align_right
|
||||||
{
|
{
|
||||||
( rect.x + rect.width - ( fixed_w + gaps ) - pad, 0.0 )
|
( rect.x + rect.width - ( laid_w + gaps ) - pad, 0.0 )
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
( rect.x + ( rect.width - fixed_w - gaps ) / 2.0, 0.0 )
|
( rect.x + ( rect.width - laid_w - gaps ) / 2.0, 0.0 )
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut x = start_x;
|
let mut x = start_x;
|
||||||
@@ -262,7 +299,7 @@ impl<Msg: Clone> Row<Msg>
|
|||||||
None => flex_unit * s.weight as f32,
|
None => flex_unit * s.weight as f32,
|
||||||
},
|
},
|
||||||
Element::Flex( f ) => flex_unit * f.weight as f32,
|
Element::Flex( f ) => flex_unit * f.weight as f32,
|
||||||
_ => w,
|
_ => w.min( cap ),
|
||||||
};
|
};
|
||||||
let ( y, height ) = if self.fill_height && !matches!( child, Element::Spacer( _ ) )
|
let ( y, height ) = if self.fill_height && !matches!( child, Element::Spacer( _ ) )
|
||||||
{
|
{
|
||||||
@@ -291,10 +328,40 @@ impl<Msg: Clone> Row<Msg>
|
|||||||
align_right: self.align_right,
|
align_right: self.align_right,
|
||||||
align_top: self.align_top,
|
align_top: self.align_top,
|
||||||
fill_height: self.fill_height,
|
fill_height: self.fill_height,
|
||||||
|
shrink: self.shrink,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Largest per-child width `c` for which `sum( min( w, c ) ) <= available`.
|
||||||
|
///
|
||||||
|
/// Water filling: the widest children give up width first and the narrow ones
|
||||||
|
/// are left alone until the wide ones have been levelled down to them. In a
|
||||||
|
/// header that means the title absorbs the whole shortfall while the back
|
||||||
|
/// arrow beside it keeps its size. Sorts `widths` in place; returns infinity
|
||||||
|
/// when everything already fits.
|
||||||
|
fn width_cap( widths: &mut [f32], available: f32 ) -> f32
|
||||||
|
{
|
||||||
|
if widths.iter().sum::<f32>() <= available
|
||||||
|
{
|
||||||
|
return f32::INFINITY;
|
||||||
|
}
|
||||||
|
widths.sort_by( |a, b| a.partial_cmp( b ).unwrap_or( std::cmp::Ordering::Equal ) );
|
||||||
|
|
||||||
|
let mut prefix = 0.0_f32;
|
||||||
|
for ( i, w ) in widths.iter().enumerate()
|
||||||
|
{
|
||||||
|
let remaining = ( widths.len() - i ) as f32;
|
||||||
|
let cap = ( available - prefix ) / remaining;
|
||||||
|
if cap <= *w
|
||||||
|
{
|
||||||
|
return cap.max( 0.0 );
|
||||||
|
}
|
||||||
|
prefix += *w;
|
||||||
|
}
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
|
||||||
/// Create an empty row layout.
|
/// Create an empty row layout.
|
||||||
pub fn row<Msg: Clone>() -> Row<Msg>
|
pub fn row<Msg: Clone>() -> Row<Msg>
|
||||||
{
|
{
|
||||||
@@ -318,6 +385,75 @@ mod tests
|
|||||||
|
|
||||||
fn make_canvas() -> Canvas { Canvas::new( 800, 600 ) }
|
fn make_canvas() -> Canvas { Canvas::new( 800, 600 ) }
|
||||||
|
|
||||||
|
/// Children whose preferred widths overflow the rect are scaled down to
|
||||||
|
/// fit, rather than spilling symmetrically out of it.
|
||||||
|
#[ test ]
|
||||||
|
fn overflowing_children_shrink_to_fit()
|
||||||
|
{
|
||||||
|
let canvas = make_canvas();
|
||||||
|
let build = || row::<()>()
|
||||||
|
.spacing( 0.0 )
|
||||||
|
.padding( 0.0 )
|
||||||
|
.push( crate::toggle( false ) )
|
||||||
|
.push( crate::toggle( false ) );
|
||||||
|
|
||||||
|
let natural = build().preferred_size( 10_000.0, &canvas ).0;
|
||||||
|
assert!( natural > 0.0, "toggles must report a width for this to test anything" );
|
||||||
|
let rect = Rect { x: 0.0, y: 0.0, width: natural / 2.0, height: 48.0 };
|
||||||
|
|
||||||
|
let laid: f32 = build().layout( rect, &canvas )
|
||||||
|
.iter().map( |( r, _ )| r.width ).sum();
|
||||||
|
assert!( laid <= rect.width + 0.5, "laid out {laid} into {}", rect.width );
|
||||||
|
|
||||||
|
let kept: f32 = build().no_shrink().layout( rect, &canvas )
|
||||||
|
.iter().map( |( r, _ )| r.width ).sum();
|
||||||
|
assert!( kept > rect.width, "no_shrink kept {kept}, expected wider than {}", rect.width );
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The shortfall comes off the widest child first: a narrow neighbour —
|
||||||
|
/// a header's back arrow next to its title — keeps its size.
|
||||||
|
#[ test ]
|
||||||
|
fn narrow_child_survives_a_wide_neighbour()
|
||||||
|
{
|
||||||
|
let mut w = vec![ 20.0_f32, 300.0 ];
|
||||||
|
let cap = width_cap( &mut w, 200.0 );
|
||||||
|
assert_eq!( 20.0_f32.min( cap ), 20.0 );
|
||||||
|
assert_eq!( 300.0_f32.min( cap ), 180.0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Equal children share the shortfall equally, which is what a segmented
|
||||||
|
/// control wants.
|
||||||
|
#[ test ]
|
||||||
|
fn equal_children_shrink_equally()
|
||||||
|
{
|
||||||
|
let mut w = vec![ 100.0_f32, 100.0, 100.0 ];
|
||||||
|
let cap = width_cap( &mut w, 150.0 );
|
||||||
|
assert_eq!( cap, 50.0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn nothing_shrinks_when_it_all_fits()
|
||||||
|
{
|
||||||
|
let mut w = vec![ 10.0_f32, 20.0 ];
|
||||||
|
assert_eq!( width_cap( &mut w, 100.0 ), f32::INFINITY );
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pinned spacers are an explicit gap, not slack, so the shrink pass
|
||||||
|
/// leaves them alone.
|
||||||
|
#[ test ]
|
||||||
|
fn pinned_spacers_keep_their_width_when_shrinking()
|
||||||
|
{
|
||||||
|
let canvas = make_canvas();
|
||||||
|
let r = row::<()>()
|
||||||
|
.spacing( 0.0 )
|
||||||
|
.padding( 0.0 )
|
||||||
|
.push( crate::spacer().width( 60.0 ).height( 10.0 ) )
|
||||||
|
.push( crate::toggle( false ) );
|
||||||
|
let rect = Rect { x: 0.0, y: 0.0, width: 70.0, height: 48.0 };
|
||||||
|
let cells = r.layout( rect, &canvas );
|
||||||
|
assert_eq!( cells[ 0 ].0.width, 60.0 );
|
||||||
|
}
|
||||||
|
|
||||||
#[ test ]
|
#[ test ]
|
||||||
fn align_right_returns_full_max_width()
|
fn align_right_returns_full_max_width()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ pub struct Button<Msg: Clone>
|
|||||||
/// Width and height in pixels for icon buttons. The `0.0` default
|
/// Width and height in pixels for icon buttons. The `0.0` default
|
||||||
/// follows the process [`crate::WidgetScaling`] mode (via
|
/// follows the process [`crate::WidgetScaling`] mode (via
|
||||||
/// [`crate::Canvas::geom_px`]); any positive value pins an explicit size.
|
/// [`crate::Canvas::geom_px`]); any positive value pins an explicit size.
|
||||||
pub( crate ) icon_size: f32,
|
pub( crate ) icon_size: Option<Length>,
|
||||||
/// Optional label font size for text buttons. `None` uses the theme's
|
/// Optional label font size for text buttons. `None` uses the theme's
|
||||||
/// default (`theme::FONT_SIZE`); a [`Length`] scales the label with the
|
/// default (`theme::FONT_SIZE`); a [`Length`] scales the label with the
|
||||||
/// surface (e.g. `Length::vmin( 2.2 ).clamp( 14.0, 22.0 )`).
|
/// surface (e.g. `Length::vmin( 2.2 ).clamp( 14.0, 22.0 )`).
|
||||||
@@ -115,7 +115,7 @@ impl<Msg: Clone> Button<Msg>
|
|||||||
on_long_press: None,
|
on_long_press: None,
|
||||||
on_drag_start: None,
|
on_drag_start: None,
|
||||||
variant: ButtonVariant::Primary,
|
variant: ButtonVariant::Primary,
|
||||||
icon_size: 0.0,
|
icon_size: None,
|
||||||
font_size: None,
|
font_size: None,
|
||||||
height: None,
|
height: None,
|
||||||
width: None,
|
width: None,
|
||||||
@@ -153,7 +153,7 @@ impl<Msg: Clone> Button<Msg>
|
|||||||
on_long_press: None,
|
on_long_press: None,
|
||||||
on_drag_start: None,
|
on_drag_start: None,
|
||||||
variant: ButtonVariant::Tertiary,
|
variant: ButtonVariant::Tertiary,
|
||||||
icon_size: 0.0,
|
icon_size: None,
|
||||||
font_size: None,
|
font_size: None,
|
||||||
height: None,
|
height: None,
|
||||||
width: None,
|
width: None,
|
||||||
@@ -237,10 +237,16 @@ impl<Msg: Clone> Button<Msg>
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the display size (width = height) for icon buttons in pixels.
|
/// Set the display size (width = height) for icon buttons.
|
||||||
pub fn icon_size( mut self, size: f32 ) -> Self
|
///
|
||||||
|
/// A bare `f32` is `Length::px`, which pins the icon at that many canvas
|
||||||
|
/// pixels. Pass `Length::widget( n )` to have it follow the active
|
||||||
|
/// widget-scaling mode, which is what stock icons — a `list_item`
|
||||||
|
/// disclosure arrow, for one — already do, so the two match on a surface
|
||||||
|
/// where fluid sizing has grown them.
|
||||||
|
pub fn icon_size( mut self, size: impl Into<Length> ) -> Self
|
||||||
{
|
{
|
||||||
self.icon_size = size;
|
self.icon_size = Some( size.into() );
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,11 +299,17 @@ impl<Msg: Clone> Button<Msg>
|
|||||||
.unwrap_or_else( || canvas.geom_px( theme::HEIGHT ) )
|
.unwrap_or_else( || canvas.geom_px( theme::HEIGHT ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the icon-button size: a positive [`Self::icon_size`] pins it,
|
/// Resolve the icon-button size. Unset follows the widget-scaling mode
|
||||||
/// the `0.0` sentinel follows the widget-scaling mode.
|
/// through `theme::HEIGHT`; an explicit value resolves like any other
|
||||||
|
/// geometry `Length`, so `Length::px` pins a physical size while
|
||||||
|
/// `Length::widget` tracks the surface the way stock icons do.
|
||||||
fn resolved_icon_size( &self, canvas: &Canvas ) -> f32
|
fn resolved_icon_size( &self, canvas: &Canvas ) -> f32
|
||||||
{
|
{
|
||||||
if self.icon_size > 0.0 { self.icon_size } else { canvas.geom_px( theme::HEIGHT ) }
|
match self.icon_size
|
||||||
|
{
|
||||||
|
Some( l ) => canvas.resolve_geom( l ),
|
||||||
|
None => canvas.geom_px( theme::HEIGHT ),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Assign a stable identifier for focus management.
|
/// Assign a stable identifier for focus management.
|
||||||
@@ -389,6 +401,9 @@ impl<Msg: Clone> Button<Msg>
|
|||||||
let is_disabled = self.on_press.is_none();
|
let is_disabled = self.on_press.is_none();
|
||||||
let fs = self.label_font_size( canvas );
|
let fs = self.label_font_size( canvas );
|
||||||
let text_y = rect.y + (rect.height + fs) / 2.0 - 2.0;
|
let text_y = rect.y + (rect.height + fs) / 2.0 - 2.0;
|
||||||
|
// `rect` is what the layout granted, which is not always the
|
||||||
|
// preferred width: a row that cannot fit its children shrinks them.
|
||||||
|
let label = super::elide( canvas, label, fs, rect.width - canvas.geom_px( theme::PAD_H ) * 2.0 );
|
||||||
|
|
||||||
match self.variant
|
match self.variant
|
||||||
{
|
{
|
||||||
@@ -412,9 +427,9 @@ impl<Msg: Clone> Button<Msg>
|
|||||||
theme::RADIUS + theme::FOCUS_W + 2.0,
|
theme::RADIUS + theme::FOCUS_W + 2.0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let text_w = canvas.measure_text( label, fs );
|
let text_w = canvas.measure_text( &label, fs );
|
||||||
canvas.draw_text(
|
canvas.draw_text(
|
||||||
label,
|
&label,
|
||||||
rect.x + (rect.width - text_w) / 2.0,
|
rect.x + (rect.width - text_w) / 2.0,
|
||||||
text_y,
|
text_y,
|
||||||
fs,
|
fs,
|
||||||
@@ -438,9 +453,9 @@ impl<Msg: Clone> Button<Msg>
|
|||||||
theme::RADIUS + theme::FOCUS_W + 2.0,
|
theme::RADIUS + theme::FOCUS_W + 2.0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let text_w = canvas.measure_text( label, fs );
|
let text_w = canvas.measure_text( &label, fs );
|
||||||
canvas.draw_text(
|
canvas.draw_text(
|
||||||
label,
|
&label,
|
||||||
rect.x + (rect.width - text_w) / 2.0,
|
rect.x + (rect.width - text_w) / 2.0,
|
||||||
text_y,
|
text_y,
|
||||||
fs,
|
fs,
|
||||||
@@ -455,9 +470,9 @@ impl<Msg: Clone> Button<Msg>
|
|||||||
let ring = rect.expand( 2.0 );
|
let ring = rect.expand( 2.0 );
|
||||||
canvas.stroke_rect( ring, theme::focus_color(), theme::FOCUS_W, theme::RADIUS );
|
canvas.stroke_rect( ring, theme::focus_color(), theme::FOCUS_W, theme::RADIUS );
|
||||||
}
|
}
|
||||||
let text_w = canvas.measure_text( label, fs );
|
let text_w = canvas.measure_text( &label, fs );
|
||||||
canvas.draw_text(
|
canvas.draw_text(
|
||||||
label,
|
&label,
|
||||||
rect.x + (rect.width - text_w) / 2.0,
|
rect.x + (rect.width - text_w) / 2.0,
|
||||||
text_y,
|
text_y,
|
||||||
fs,
|
fs,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use crate::types::{ Length, Rect, WidgetId };
|
use crate::types::{ Length, Rect, WidgetId };
|
||||||
use crate::render::Canvas;
|
use crate::render::Canvas;
|
||||||
use super::Element;
|
use super::{ elide, Element };
|
||||||
|
|
||||||
mod theme;
|
mod theme;
|
||||||
|
|
||||||
@@ -357,35 +357,6 @@ impl<Msg: Clone> ListItem<Msg>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shorten `text` with a trailing ellipsis so it fits `max_w`, mirroring the
|
|
||||||
/// single-line truncation [`super::text::Text`] applies.
|
|
||||||
fn elide( canvas: &Canvas, text: &str, size: f32, max_w: f32 ) -> String
|
|
||||||
{
|
|
||||||
if max_w <= 0.0
|
|
||||||
{
|
|
||||||
return String::new();
|
|
||||||
}
|
|
||||||
if canvas.measure_text( text, size ) <= max_w
|
|
||||||
{
|
|
||||||
return text.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
let ellipsis = "...";
|
|
||||||
let budget = max_w - canvas.measure_text( ellipsis, size );
|
|
||||||
if budget <= 0.0
|
|
||||||
{
|
|
||||||
return ellipsis.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut accum = 0.0_f32;
|
|
||||||
let kept: String = text.chars().take_while( |ch|
|
|
||||||
{
|
|
||||||
accum += canvas.measure_text( &ch.to_string(), size );
|
|
||||||
accum <= budget
|
|
||||||
} ).collect();
|
|
||||||
format!( "{kept}{ellipsis}" )
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a [`ListItem`] with the given primary label.
|
/// Create a [`ListItem`] with the given primary label.
|
||||||
///
|
///
|
||||||
/// Add detail and behaviour through the chained builders. For a
|
/// Add detail and behaviour through the chained builders. For a
|
||||||
|
|||||||
@@ -108,3 +108,43 @@ pub use factory::{ button, icon_button, text_edit, image, text, container, exter
|
|||||||
/// — the same closure is invoked once per emitted message, regardless
|
/// — the same closure is invoked once per emitted message, regardless
|
||||||
/// of how many leaves the sub-tree has.
|
/// of how many leaves the sub-tree has.
|
||||||
pub( crate ) type MapFn<Msg, U> = std::sync::Arc<dyn Fn( Msg ) -> U>;
|
pub( crate ) type MapFn<Msg, U> = std::sync::Arc<dyn Fn( Msg ) -> U>;
|
||||||
|
|
||||||
|
/// Shorten `text` with a trailing ellipsis so it fits `max_w` at `size`.
|
||||||
|
///
|
||||||
|
/// The single truncation rule for widgets that paint a line of text into a
|
||||||
|
/// rect they do not control: a leaf must stay inside what the layout gave
|
||||||
|
/// it, or a long label runs under its neighbour. Widths accumulate per
|
||||||
|
/// character, which is what [`text::Text`] does with its own inline copy of
|
||||||
|
/// this — folding that one in is pending, as it measures through a font
|
||||||
|
/// override this signature does not carry.
|
||||||
|
pub( crate ) fn elide(
|
||||||
|
canvas: &crate::render::Canvas,
|
||||||
|
text: &str,
|
||||||
|
size: f32,
|
||||||
|
max_w: f32,
|
||||||
|
) -> String
|
||||||
|
{
|
||||||
|
if max_w <= 0.0
|
||||||
|
{
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
if canvas.measure_text( text, size ) <= max_w
|
||||||
|
{
|
||||||
|
return text.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let ellipsis = "...";
|
||||||
|
let budget = max_w - canvas.measure_text( ellipsis, size );
|
||||||
|
if budget <= 0.0
|
||||||
|
{
|
||||||
|
return ellipsis.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut accum = 0.0_f32;
|
||||||
|
let kept: String = text.chars().take_while( |ch|
|
||||||
|
{
|
||||||
|
accum += canvas.measure_text( &ch.to_string(), size );
|
||||||
|
accum <= budget
|
||||||
|
} ).collect();
|
||||||
|
format!( "{kept}{ellipsis}" )
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user