egui_charts/ui_kit/list_item/
mod.rs1use crate::theme::ThemeExt;
45mod button_content;
46mod content;
47mod label_content;
48mod property_content;
49mod visuals;
50
51pub use button_content::{ButtonContent, ButtonStyle};
52pub use content::{ContentContext, ListItem, ListItemContent};
53pub use label_content::LabelContent;
54pub use property_content::PropertyContent;
55pub use visuals::ListVisuals;
56
57use egui::{Color32, Response, Ui, Vec2};
58
59use crate::tokens::DESIGN_TOKENS;
60
61fn default_height() -> f32 {
63 DESIGN_TOKENS.sizing.list_item_height
64}
65
66impl ListItem {
67 pub fn show<C: ListItemContent>(self, ui: &mut Ui, content: C) -> Response {
69 let height = self.height.unwrap_or_else(default_height);
70 let width = ui.available_width();
71 let interactive = self.interactive && content.interactive();
72
73 let spacing = std::mem::take(&mut ui.spacing_mut().item_spacing.y);
75 let (rect, mut response) = ui.allocate_exact_size(
76 Vec2::new(width, height),
77 if interactive {
78 egui::Sense::click()
79 } else {
80 egui::Sense::hover()
81 },
82 );
83 ui.spacing_mut().item_spacing.y = spacing;
84
85 if ui.is_rect_visible(rect) {
86 let egui_visuals = ui.style().visuals.clone();
87
88 let list_visuals = ListVisuals::new()
89 .with_hovered(response.hovered())
90 .with_active(interactive && response.is_pointer_button_down_on())
91 .with_selected(self.selected)
92 .with_interactive(interactive);
93
94 let bg_color = list_visuals
96 .bg_color(&egui_visuals)
97 .unwrap_or(Color32::TRANSPARENT);
98
99 if bg_color != Color32::TRANSPARENT {
101 ui.painter()
102 .rect_filled(rect, ui.active_theme().radius.sm, bg_color);
103 }
104
105 let content_context = ContentContext {
107 rect: rect.shrink2(Vec2::new(ui.active_theme().spacing.lg, 0.0)),
108 bg_color,
109 selected: self.selected,
110 hovered: response.hovered(),
111 list_item: &self,
112 response: &response,
113 visuals: list_visuals,
114 };
115
116 content.ui(ui, &content_context);
117
118 if self.show_separator {
120 let sep_y = rect.bottom();
121 let sep_color = egui_visuals.widgets.noninteractive.bg_stroke.color;
122 ui.painter().hline(
123 rect.x_range(),
124 sep_y,
125 egui::Stroke::new(DESIGN_TOKENS.stroke.hairline, sep_color),
126 );
127 }
128 }
129
130 crate::styles::focus::draw_focus_ring(
132 ui,
133 &response
134 .clone()
135 .with_new_rect(rect.shrink(ui.active_theme().spacing.xs)),
136 ui.active_theme().radius.sm - ui.active_theme().spacing.xs,
137 );
138
139 if response.clicked() && interactive {
141 response.mark_changed();
142 }
143
144 response
145 }
146
147 pub fn show_text(self, ui: &mut Ui, text: impl Into<String>) -> Response {
149 self.show(ui, LabelContent::new(text))
150 }
151
152 pub fn show_with_icon(
154 self,
155 ui: &mut Ui,
156 text: impl Into<String>,
157 icon: &crate::icons::Icon,
158 ) -> Response {
159 self.show(ui, LabelContent::new(text).with_icon(icon))
160 }
161
162 pub fn show_property(
164 self,
165 ui: &mut Ui,
166 label: impl Into<String>,
167 value: impl Into<String>,
168 ) -> Response {
169 self.show(ui, PropertyContent::new(label, value))
170 }
171}
172
173pub fn simple_list<T, F>(ui: &mut Ui, items: &[T], selected: &mut Option<usize>, mut show_item: F)
190where
191 F: FnMut(&mut Ui, &T, bool) -> Response,
192{
193 for (idx, item) in items.iter().enumerate() {
194 let is_selected = *selected == Some(idx);
195 let response = show_item(ui, item, is_selected);
196 if response.clicked() {
197 *selected = Some(idx);
198 }
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn show_advances_by_exactly_one_row() {
208 let ctx = egui::Context::default();
209 ctx.run_ui(egui::RawInput::default(), |ui| {
210 egui::CentralPanel::default().show(ui, |ui| {
211 let top = ui.cursor().top();
212 let row = ListItem::new().show_text(ui, "Row");
213 assert_eq!(row.rect.height(), default_height());
214 assert_eq!(ui.cursor().top() - top, default_height());
215 });
216 })
217 .drop_without_applying_deltas();
218 }
219
220 #[test]
221 fn subdued_and_property_labels_keep_their_weak_text_colour() {
222 for (name, theme) in crate::theme::presets() {
223 let ctx = egui::Context::default();
224 crate::theme::apply_to_egui(&ctx, &theme);
225 let output = ctx.run_ui(Default::default(), |ui| {
226 ListItem::new().show_text(ui, "Plain");
227 ListItem::new().show(ui, LabelContent::new("Subdued").subdued(true));
228 ListItem::new().show_property(ui, "Open", "19,126.06");
229 });
230 let text: Vec<_> = output
231 .shapes
232 .iter()
233 .filter_map(|s| {
234 if let egui::Shape::Text(t) = &s.shape {
235 Some((
236 t.galley.job.text.clone(),
237 t.override_text_color
238 .unwrap_or(t.galley.job.sections[0].format.color),
239 ))
240 } else {
241 None
242 }
243 })
244 .collect();
245 output.drop_without_applying_deltas();
246 for (label, expected) in [
247 ("Plain", theme.ui.text),
248 ("Subdued", theme.ui.text_muted),
249 ("Open", theme.ui.text_muted),
250 ("19,126.06", theme.ui.text),
251 ] {
252 assert_eq!(
253 text.iter().find(|(s, _)| s == label).unwrap().1,
254 expected,
255 "{name}: {label}"
256 );
257 }
258 assert_ne!(theme.ui.text_muted, theme.ui.text);
259 }
260 }
261
262 #[test]
263 fn tab_reaches_list_rows_in_visual_order_and_draws_their_ring() {
264 let ctx = egui::Context::default();
265 let frame = |events| {
266 let mut rows = Vec::new();
267 let output = ctx.run_ui(
268 egui::RawInput {
269 events,
270 ..Default::default()
271 },
272 |ui| {
273 for label in ["First", "Second", "Third"] {
274 rows.push(ListItem::new().show_text(ui, label));
275 }
276 },
277 );
278 let rings: Vec<_> = output
279 .shapes
280 .iter()
281 .filter_map(|s| match &s.shape {
282 egui::Shape::Rect(r) if r.stroke.width == 2.0 => Some(r.rect),
283 _ => None,
284 })
285 .collect();
286 output.drop_without_applying_deltas();
287 (rows, rings)
288 };
289 frame(vec![]);
290 for index in 0..3 {
291 let (rows, rings) = frame(vec![egui::Event::Key {
292 key: egui::Key::Tab,
293 physical_key: None,
294 pressed: true,
295 repeat: false,
296 modifiers: egui::Modifiers::NONE,
297 }]);
298 assert!(rows[index].has_focus(), "row {index}");
299 assert_eq!(rings, vec![rows[index].rect]);
300 }
301 }
302}