list_item: trailing icon slot and horizontal inset override
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

Disclosure arrows in list rows could only be text: the trailing slot draws a string, so apps fell back to the "›" glyph at 14 px, which cannot use the theme's arrow SVGs and looks thin next to 24 px leading icons. Add ListItem::trailing_icon( rgba, w, h ): a right-aligned icon drawn at the new theme::TRAILING_ICON_SIZE (21 px, 1.5x the old glyph em box), vertically centered. It coexists with trailing text, which shifts to the icon's left. Like the leading icon, symbolic assets are pre-tinted by the caller (tint_symbolic), so the widget stays colour-agnostic.
Add ListItem::pad_h( impl Into<Length> ), a per-item override of the horizontal inset between the row edge and its content (leading icon/label on the left, trailing text/icon on the right). Resolution follows the Separator pattern: an explicit Length wins, otherwise the theme default (16 px through geom_px) applies, so existing rows are unaffected. This lets an app whose enclosing view already provides the margin bring row content flush instead of stacking both insets.
Document both builders in docs/widgets.md and the changelog, and rework the list_item rustdoc example to point at trailing_icon with a theme icon for disclosure arrows instead of the text glyph.
This commit is contained in:
2026-07-30 11:19:37 +02:00
parent 16c04f4159
commit d31e7222da
4 changed files with 97 additions and 26 deletions

View File

@@ -3,7 +3,7 @@
use std::sync::Arc;
use crate::types::{ Rect, WidgetId };
use crate::types::{ Length, Rect, WidgetId };
use crate::render::Canvas;
use super::Element;
@@ -42,9 +42,14 @@ pub struct ListItem<Msg: Clone>
/// Optional secondary line drawn below the label in muted colour.
/// Doubles the row height when set.
pub( crate ) subtitle: Option<String>,
/// Optional right-aligned text (current setting, badge count, ""
/// disclosure). Drawn in muted colour.
/// Optional right-aligned text (current setting, badge count).
/// Drawn in muted colour.
pub( crate ) trailing: Option<String>,
/// Optional right-aligned icon — RGBA bytes + native dimensions.
/// Drawn at `theme::TRAILING_ICON_SIZE`, to the right of the
/// trailing text when both are set. Symbolic icons should be
/// pre-tinted by the caller (see [`crate::tint_symbolic`]).
pub( crate ) trailing_icon: Option<( Arc<Vec<u8>>, u32, u32 )>,
/// Message emitted on tap. `None` keeps the item visible but inert.
pub( crate ) on_press: Option<Msg>,
/// Optional stable identifier for focus management.
@@ -59,6 +64,9 @@ pub struct ListItem<Msg: Clone>
/// when this is set, and offsets the label / subtitle by the
/// same amount. Pass `None` to keep the icon-less layout.
pub( crate ) icon: Option<( Arc<Vec<u8>>, u32, u32 )>,
/// Optional override of the horizontal content inset. `None`
/// falls back to `theme::PAD_H`.
pub( crate ) pad_h: Option<Length>,
}
impl<Msg: Clone> ListItem<Msg>
@@ -69,13 +77,15 @@ impl<Msg: Clone> ListItem<Msg>
{
Self
{
label: label.into(),
subtitle: None,
trailing: None,
on_press: None,
id: None,
selected: false,
icon: None,
label: label.into(),
subtitle: None,
trailing: None,
trailing_icon: None,
on_press: None,
id: None,
selected: false,
icon: None,
pad_h: None,
}
}
@@ -114,6 +124,25 @@ impl<Msg: Clone> ListItem<Msg>
self
}
/// Attach a right-aligned icon (disclosure arrow). Pass the decoded
/// RGBA buffer alongside the image's native width and height; the
/// draw path scales it down to `theme::TRAILING_ICON_SIZE`.
pub fn trailing_icon( mut self, rgba: Arc<Vec<u8>>, w: u32, h: u32 ) -> Self
{
self.trailing_icon = Some( ( rgba, w, h ) );
self
}
/// Override the horizontal inset between the row edge and its
/// content (leading icon / label on the left, trailing text or
/// icon on the right). Accepts logical `f32` pixels or any
/// [`Length`]; without this it uses the theme default.
pub fn pad_h( mut self, p: impl Into<Length> ) -> Self
{
self.pad_h = Some( p.into() );
self
}
/// Set the message emitted when the row is tapped.
pub fn on_press( mut self, msg: Msg ) -> Self
{
@@ -178,7 +207,9 @@ impl<Msg: Clone> ListItem<Msg>
}
let label_size = canvas.font_px( theme::LABEL_SIZE );
let pad_h = canvas.geom_px( theme::PAD_H );
let pad_h = self.pad_h
.map( |l| l.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT ) )
.unwrap_or_else( || canvas.geom_px( theme::PAD_H ) );
let has_sub = self.subtitle.is_some();
let label_y = if has_sub
{
@@ -215,11 +246,27 @@ impl<Msg: Clone> ListItem<Msg>
canvas.draw_text( sub, text_x, sub_y, sub_size, subtitle_color );
}
let mut trail_right = rect.x + rect.width - pad_h;
if let Some( ( rgba, w, h ) ) = &self.trailing_icon
{
let icon_size = canvas.geom_px( theme::TRAILING_ICON_SIZE );
let icon_rect = Rect
{
x: trail_right - icon_size,
y: rect.y + ( rect.height - icon_size ) / 2.0,
width: icon_size,
height: icon_size,
};
canvas.draw_image_data( rgba, *w, *h, icon_rect, 1.0 );
trail_right = icon_rect.x - canvas.geom_px( theme::ICON_GAP );
}
if let Some( ref trail ) = self.trailing
{
let trail_size = canvas.font_px( theme::TRAILING_SIZE );
let tw = canvas.measure_text( trail, trail_size );
let tx = rect.x + rect.width - pad_h - tw;
let tx = trail_right - tw;
let ty = rect.y + ( rect.height + trail_size ) / 2.0 - 2.0;
canvas.draw_text( trail, tx, ty, trail_size, trailing_color );
}
@@ -232,20 +279,24 @@ impl<Msg: Clone> ListItem<Msg>
{
ListItem
{
label: self.label,
subtitle: self.subtitle,
trailing: self.trailing,
on_press: self.on_press.map( |m| ( *f )( m ) ),
id: self.id,
selected: self.selected,
icon: self.icon,
label: self.label,
subtitle: self.subtitle,
trailing: self.trailing,
trailing_icon: self.trailing_icon,
on_press: self.on_press.map( |m| ( *f )( m ) ),
id: self.id,
selected: self.selected,
icon: self.icon,
pad_h: self.pad_h,
}
}
}
/// Create a [`ListItem`] with the given primary label.
///
/// Add detail and behaviour through the chained builders:
/// Add detail and behaviour through the chained builders. For a
/// disclosure arrow use [`ListItem::trailing_icon`] with a theme icon
/// (e.g. `"general/right"`) rather than a text glyph:
///
/// ```rust,no_run
/// # use ltk::{ list_item, ListItem };
@@ -253,7 +304,7 @@ impl<Msg: Clone> ListItem<Msg>
/// # fn _ex() -> ListItem<Msg> {
/// list_item( "Display" )
/// .subtitle( "Resolution, brightness, night mode" )
/// .trailing( "" )
/// .trailing( "Light" )
/// .on_press( Msg::OpenDisplay )
/// # }
/// ```

View File

@@ -33,5 +33,7 @@ pub const RADIUS: f32 = 12.0;
pub const FOCUS_W: f32 = 2.0;
/// Visible side of the optional leading icon (square).
pub const ICON_SIZE: f32 = 24.0;
/// Visible side of the optional trailing icon (square).
pub const TRAILING_ICON_SIZE: f32 = 21.0;
/// Gap between the leading icon's right edge and the label baseline.
pub const ICON_GAP: f32 = 12.0;