use crate::Theme;
use egui::{FontFamily, Response, RichText, Ui, Widget};
use egui_cha::ViewCtx;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ListItemSize {
Compact,
#[default]
Medium,
Large,
}
pub struct ListItem {
label: String,
icon: Option<&'static str>,
badge: Option<String>,
selected: bool,
disabled: bool,
size: ListItemSize,
}
impl ListItem {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
icon: None,
badge: None,
selected: false,
disabled: false,
size: ListItemSize::default(),
}
}
pub fn compact(mut self) -> Self {
self.size = ListItemSize::Compact;
self
}
pub fn size(mut self, size: ListItemSize) -> Self {
self.size = size;
self
}
pub fn icon(mut self, icon: &'static str) -> Self {
self.icon = Some(icon);
self
}
pub fn badge(mut self, badge: impl Into<String>) -> Self {
self.badge = Some(badge.into());
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn on_click<Msg>(self, ctx: &mut ViewCtx<'_, Msg>, msg: Msg) {
let disabled = self.disabled;
if self.show(ctx.ui).clicked() && !disabled {
ctx.emit(msg);
}
}
pub fn show(self, ui: &mut Ui) -> Response {
ui.add(self)
}
}
impl Widget for ListItem {
fn ui(self, ui: &mut Ui) -> Response {
let theme = Theme::current(ui.ctx());
let (text_color, bg_color) = if self.disabled {
(theme.text_muted, None)
} else if self.selected {
(theme.primary, Some(theme.bg_secondary))
} else {
(theme.text_primary, None)
};
let hover_color = theme.bg_tertiary;
let desired_height = match self.size {
ListItemSize::Compact => theme.spacing_md + theme.spacing_sm, ListItemSize::Medium => theme.spacing_lg + theme.spacing_md, ListItemSize::Large => theme.spacing_xl + theme.spacing_md, };
let available_width = ui.available_width();
let (rect, response) = ui.allocate_exact_size(
egui::vec2(available_width, desired_height),
egui::Sense::click(),
);
if ui.is_rect_visible(rect) {
let painter = ui.painter();
let bg = if response.hovered() && !self.disabled {
Some(hover_color)
} else {
bg_color
};
if let Some(color) = bg {
painter.rect_filled(rect, theme.radius_sm, color);
}
let indicator_width = theme.stroke_width * 3.0;
if self.selected {
let indicator_rect =
egui::Rect::from_min_size(rect.min, egui::vec2(indicator_width, rect.height()));
painter.rect_filled(indicator_rect, 0.0, theme.primary);
}
let padding = theme.spacing_md;
let mut x = rect.min.x + padding + if self.selected { indicator_width } else { 0.0 };
let center_y = rect.center().y;
if let Some(icon) = self.icon {
let icon_text = RichText::new(icon)
.family(FontFamily::Name("icons".into()))
.color(text_color);
let galley = painter.layout_no_wrap(
icon_text.text().to_string(),
egui::FontId::new(theme.font_size_md, FontFamily::Name("icons".into())),
text_color,
);
let icon_pos = egui::pos2(x, center_y - galley.size().y / 2.0);
painter.galley(icon_pos, galley, text_color);
x += theme.font_size_md + theme.spacing_sm;
}
let galley = painter.layout_no_wrap(
self.label.clone(),
egui::FontId::proportional(theme.font_size_sm),
text_color,
);
let label_pos = egui::pos2(x, center_y - galley.size().y / 2.0);
painter.galley(label_pos, galley, text_color);
if let Some(badge) = &self.badge {
let galley = painter.layout_no_wrap(
badge.clone(),
egui::FontId::proportional(theme.font_size_xs),
theme.primary_text,
);
let badge_width = galley.size().x + theme.spacing_sm * 2.0;
let badge_height = galley.size().y + theme.spacing_xs;
let badge_x = rect.max.x - padding - badge_width;
let badge_rect = egui::Rect::from_min_size(
egui::pos2(badge_x, center_y - badge_height / 2.0),
egui::vec2(badge_width, badge_height),
);
painter.rect_filled(badge_rect, theme.radius_sm, theme.primary);
let text_pos =
egui::pos2(badge_x + theme.spacing_sm, center_y - galley.size().y / 2.0);
painter.galley(text_pos, galley, theme.primary_text);
}
}
if !self.disabled && response.hovered() {
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
}
response
}
}