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.
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
// SPDX-License-Identifier: LGPL-2.1-only
|
// SPDX-License-Identifier: LGPL-2.1-only
|
||||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::types::{ Rect, WidgetId };
|
use crate::types::{ Rect, WidgetId };
|
||||||
use crate::render::Canvas;
|
use crate::render::Canvas;
|
||||||
use super::Element;
|
use super::Element;
|
||||||
@@ -49,9 +51,14 @@ pub struct ListItem<Msg: Clone>
|
|||||||
pub id: Option<WidgetId>,
|
pub id: Option<WidgetId>,
|
||||||
/// `true` paints the row with the dark selected surface and white
|
/// `true` paints the row with the dark selected surface and white
|
||||||
/// text, regardless of hover / press state. Use to indicate the
|
/// text, regardless of hover / press state. Use to indicate the
|
||||||
/// active item in a list of choices (combo dropdown, segmented
|
/// active item in a list of choices (combo dropdown, settings
|
||||||
/// picker, settings group with a single active value).
|
/// group with a single active value).
|
||||||
pub selected: bool,
|
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<Vec<u8>>, u32, u32 )>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Msg: Clone> ListItem<Msg>
|
impl<Msg: Clone> ListItem<Msg>
|
||||||
@@ -68,9 +75,21 @@ impl<Msg: Clone> ListItem<Msg>
|
|||||||
on_press: None,
|
on_press: None,
|
||||||
id: None,
|
id: None,
|
||||||
selected: false,
|
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<Vec<u8>>, w: u32, h: u32 ) -> Self
|
||||||
|
{
|
||||||
|
self.icon = Some( ( rgba, w, h ) );
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Mark this row as the currently-selected option in its list.
|
/// Mark this row as the currently-selected option in its list.
|
||||||
/// Selected rows paint with a dark surface and white text and
|
/// Selected rows paint with a dark surface and white text and
|
||||||
/// override hover / press visuals.
|
/// override hover / press visuals.
|
||||||
@@ -161,12 +180,29 @@ impl<Msg: Clone> ListItem<Msg>
|
|||||||
rect.y + ( rect.height + theme::LABEL_SIZE ) / 2.0 - 2.0
|
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
|
if let Some( ref sub ) = self.subtitle
|
||||||
{
|
{
|
||||||
let sub_y = rect.y + rect.height * 0.62 + theme::SUBTITLE_SIZE * 0.3;
|
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
|
if let Some( ref trail ) = self.trailing
|
||||||
@@ -191,6 +227,7 @@ impl<Msg: Clone> ListItem<Msg>
|
|||||||
on_press: self.on_press.map( |m| ( *f )( m ) ),
|
on_press: self.on_press.map( |m| ( *f )( m ) ),
|
||||||
id: self.id,
|
id: self.id,
|
||||||
selected: self.selected,
|
selected: self.selected,
|
||||||
|
icon: self.icon,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,3 +31,7 @@ pub const HEIGHT_SUB: f32 = 68.0;
|
|||||||
pub const PAD_H: f32 = 16.0;
|
pub const PAD_H: f32 = 16.0;
|
||||||
pub const RADIUS: f32 = 12.0;
|
pub const RADIUS: f32 = 12.0;
|
||||||
pub const FOCUS_W: f32 = 2.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;
|
||||||
|
|||||||
Reference in New Issue
Block a user