From f7ef932976d438f72f25c9828cb37cb4155a53c3 Mon Sep 17 00:00:00 2001 From: yamabush1 Date: Mon, 25 May 2026 09:00:00 +0200 Subject: [PATCH] list_item: optional leading icon Add a leading-icon slot to ListItem so settings-style rows can pair a 24 px symbolic glyph with the label. `.icon( rgba, w, h )` takes the same shape the rest of the toolkit uses for raw pixmaps; the draw path reserves `ICON_SIZE + ICON_GAP` on the left and shifts the label / subtitle text origin so existing icon-less rows render unchanged. --- src/widget/list_item/mod.rs | 45 +++++++++++++++++++++++++++++++---- src/widget/list_item/theme.rs | 4 ++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/widget/list_item/mod.rs b/src/widget/list_item/mod.rs index 3e691cc..a81bb94 100644 --- a/src/widget/list_item/mod.rs +++ b/src/widget/list_item/mod.rs @@ -1,6 +1,8 @@ // SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. +use std::sync::Arc; + use crate::types::{ Rect, WidgetId }; use crate::render::Canvas; use super::Element; @@ -49,9 +51,14 @@ pub struct ListItem pub id: Option, /// `true` paints the row with the dark selected surface and white /// text, regardless of hover / press state. Use to indicate the - /// active item in a list of choices (combo dropdown, segmented - /// picker, settings group with a single active value). + /// active item in a list of choices (combo dropdown, settings + /// group with a single active value). pub selected: bool, + /// Optional leading icon — RGBA bytes + native dimensions. The + /// row reserves `theme::ICON_SIZE + theme::ICON_GAP` on the left + /// when this is set, and offsets the label / subtitle by the + /// same amount. Pass `None` to keep the icon-less layout. + pub icon: Option<( Arc>, u32, u32 )>, } impl ListItem @@ -68,9 +75,21 @@ impl ListItem on_press: None, id: None, selected: false, + icon: None, } } + /// Attach a leading icon. Pass the decoded RGBA buffer alongside + /// the image's native width and height; the draw path scales it + /// down to [`theme::ICON_SIZE`] on the same row baseline as the + /// label. Symbolic icons should be pre-tinted by the caller (see + /// [`crate::tint_symbolic`]). + pub fn icon( mut self, rgba: Arc>, w: u32, h: u32 ) -> Self + { + self.icon = Some( ( rgba, w, h ) ); + self + } + /// Mark this row as the currently-selected option in its list. /// Selected rows paint with a dark surface and white text and /// override hover / press visuals. @@ -161,12 +180,29 @@ impl ListItem rect.y + ( rect.height + theme::LABEL_SIZE ) / 2.0 - 2.0 }; - canvas.draw_text( &self.label, rect.x + theme::PAD_H, label_y, theme::LABEL_SIZE, label_color ); + // Leading-icon column shifts the text right by + // `ICON_SIZE + ICON_GAP`. Symbolic icons (single channel + // pre-tinted) draw at full opacity; the colour comes from + // the bytes the caller passed in, which is why the helper + // is in `theme::icon_rgba` rather than baked here. + let text_x = if let Some( ( rgba, w, h ) ) = &self.icon + { + let icon_y = rect.y + ( rect.height - theme::ICON_SIZE ) / 2.0; + let icon_rect = Rect { x: rect.x + theme::PAD_H, y: icon_y, width: theme::ICON_SIZE, height: theme::ICON_SIZE }; + canvas.draw_image_data( rgba, *w, *h, icon_rect, 1.0 ); + rect.x + theme::PAD_H + theme::ICON_SIZE + theme::ICON_GAP + } + else + { + rect.x + theme::PAD_H + }; + + canvas.draw_text( &self.label, text_x, label_y, theme::LABEL_SIZE, label_color ); if let Some( ref sub ) = self.subtitle { let sub_y = rect.y + rect.height * 0.62 + theme::SUBTITLE_SIZE * 0.3; - canvas.draw_text( sub, rect.x + theme::PAD_H, sub_y, theme::SUBTITLE_SIZE, subtitle_color ); + canvas.draw_text( sub, text_x, sub_y, theme::SUBTITLE_SIZE, subtitle_color ); } if let Some( ref trail ) = self.trailing @@ -191,6 +227,7 @@ impl ListItem on_press: self.on_press.map( |m| ( *f )( m ) ), id: self.id, selected: self.selected, + icon: self.icon, } } } diff --git a/src/widget/list_item/theme.rs b/src/widget/list_item/theme.rs index c030f68..52dd301 100644 --- a/src/widget/list_item/theme.rs +++ b/src/widget/list_item/theme.rs @@ -31,3 +31,7 @@ pub const HEIGHT_SUB: f32 = 68.0; pub const PAD_H: f32 = 16.0; 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; +/// Gap between the leading icon's right edge and the label baseline. +pub const ICON_GAP: f32 = 12.0;