1use egui::{
25 pos2, vec2, Color32, FontId, Frame, InnerResponse, Margin, Response, Sense, Stroke, Ui,
26 Vec2, Widget, WidgetText,
27};
28use egui_components_theme::{mix, Theme};
29
30pub struct ListItem {
32 label: WidgetText,
33 secondary: Option<WidgetText>,
34 selected: bool,
35 disabled: bool,
36 confirmed: bool,
37}
38
39impl ListItem {
40 pub fn new(label: impl Into<WidgetText>) -> Self {
41 Self {
42 label: label.into(),
43 secondary: None,
44 selected: false,
45 disabled: false,
46 confirmed: false,
47 }
48 }
49
50 pub fn secondary(mut self, text: impl Into<WidgetText>) -> Self {
52 self.secondary = Some(text.into());
53 self
54 }
55
56 pub fn selected(mut self, b: bool) -> Self {
57 self.selected = b;
58 self
59 }
60 pub fn disabled(mut self, b: bool) -> Self {
61 self.disabled = b;
62 self
63 }
64 pub fn confirmed(mut self, b: bool) -> Self {
66 self.confirmed = b;
67 self
68 }
69}
70
71impl Widget for ListItem {
72 fn ui(self, ui: &mut Ui) -> Response {
73 let theme = Theme::get(ui.ctx());
74 let m = theme.metrics;
75 let c = &theme.colors;
76 let font = FontId::proportional(m.font_size_md);
77
78 let row_h = m.button_height_sm.max(28.0);
79 let pad_x = 10.0;
80 let gap = 8.0;
81
82 let label_galley = self.label.clone().into_galley(
83 ui,
84 Some(egui::TextWrapMode::Truncate),
85 ui.available_width(),
86 font.clone(),
87 );
88 let secondary_galley = self.secondary.as_ref().map(|t| {
89 t.clone().into_galley(
90 ui,
91 Some(egui::TextWrapMode::Extend),
92 f32::INFINITY,
93 font.clone(),
94 )
95 });
96
97 let secondary_w = secondary_galley.as_ref().map(|g| g.size().x + gap).unwrap_or(0.0);
98 let check_w = if self.confirmed { row_h * 0.6 + gap } else { 0.0 };
99 let total_w = ui.available_width();
100 let desired = vec2(total_w, row_h);
101 let sense = if self.disabled { Sense::hover() } else { Sense::click() };
102 let (rect, response) = ui.allocate_exact_size(desired, sense);
103
104 if !ui.is_rect_visible(rect) {
105 return response;
106 }
107
108 let painter = ui.painter();
109
110 let bg = if self.disabled {
112 Color32::TRANSPARENT
113 } else if self.selected {
114 c.secondary_background
115 } else if response.is_pointer_button_down_on() {
116 c.accent_background
117 } else if response.hovered() {
118 c.accent_background
119 } else {
120 Color32::TRANSPARENT
121 };
122 if bg != Color32::TRANSPARENT {
123 painter.rect_filled(rect, theme.corner_sm(), bg);
124 }
125 if self.selected {
126 painter.rect_filled(
128 egui::Rect::from_min_size(
129 pos2(rect.left(), rect.top() + 4.0),
130 vec2(2.0, rect.height() - 8.0),
131 ),
132 egui::CornerRadius::same(1),
133 c.primary_background,
134 );
135 }
136
137 let label_color = if self.disabled {
139 mix(c.muted_foreground, Color32::TRANSPARENT, 0.3)
140 } else {
141 c.foreground
142 };
143 let label_x = rect.left() + pad_x;
144 let label_y = rect.center().y - label_galley.size().y * 0.5;
145 painter.galley_with_override_text_color(
146 pos2(label_x, label_y),
147 label_galley.clone(),
148 label_color,
149 );
150
151 let mut right = rect.right() - pad_x;
153 if self.confirmed {
154 let cx = right - check_w * 0.5 + gap * 0.5;
155 let cy = rect.center().y;
156 let size = row_h * 0.5;
157 draw_check(painter, pos2(cx, cy), size, c.primary_background);
158 right -= check_w;
159 }
160 if let Some(g) = secondary_galley {
161 let pos = pos2(right - g.size().x, rect.center().y - g.size().y * 0.5);
162 painter.galley_with_override_text_color(pos, g, c.muted_foreground);
163 let _ = secondary_w; }
165
166 if response.has_focus() {
167 painter.rect_stroke(
168 rect.expand(1.0),
169 theme.corner_sm(),
170 theme.focus_ring(),
171 egui::StrokeKind::Outside,
172 );
173 }
174
175 if !self.disabled && response.hovered() {
176 ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
177 }
178
179 response
180 }
181}
182
183fn draw_check(painter: &egui::Painter, center: egui::Pos2, size: f32, color: Color32) {
184 let stroke = Stroke::new(1.8, color);
185 let half = size * 0.5;
186 let p1 = pos2(center.x - half * 0.6, center.y);
187 let p2 = pos2(center.x - half * 0.15, center.y + half * 0.45);
188 let p3 = pos2(center.x + half * 0.6, center.y - half * 0.5);
189 painter.line_segment([p1, p2], stroke);
190 painter.line_segment([p2, p3], stroke);
191}
192
193pub struct List {
197 id_salt: egui::Id,
198 max_height: Option<f32>,
199 padding: f32,
200}
201
202impl List {
203 pub fn new(id_source: impl std::hash::Hash) -> Self {
206 Self {
207 id_salt: egui::Id::new(id_source),
208 max_height: None,
209 padding: 4.0,
210 }
211 }
212 pub fn max_height(mut self, h: f32) -> Self {
214 self.max_height = Some(h);
215 self
216 }
217 pub fn padding(mut self, p: f32) -> Self {
218 self.padding = p;
219 self
220 }
221
222 pub fn show<R>(self, ui: &mut Ui, body: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
223 let theme = Theme::get(ui.ctx());
224 Frame::new()
225 .fill(theme.colors.background)
226 .stroke(theme.border_stroke())
227 .corner_radius(theme.corner())
228 .inner_margin(Margin::same(self.padding as i8))
229 .show(ui, |ui| {
230 if let Some(h) = self.max_height {
231 egui::ScrollArea::vertical()
232 .id_salt(self.id_salt)
233 .max_height(h)
234 .show(ui, |ui| {
235 ui.set_width(ui.available_width());
236 body(ui)
237 })
238 .inner
239 } else {
240 body(ui)
241 }
242 })
243 }
244}
245
246#[allow(dead_code)]
248fn _ensure_vec_used(_: Vec2) {}