1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
use egui::{NumExt as _, Ui, Widget};
use crate::egui_ext::boxed_widget::{BoxedWidgetLocal, BoxedWidgetLocalExt as _};
use crate::list_item::{ContentContext, DesiredWidth, ListItemContent};
use crate::{OnResponseExt as _, UiExt as _};
/// Control how the [`CustomContent`] advertises its width.
#[derive(Debug, Clone, Copy)]
enum CustomContentDesiredWidth {
/// Use the provided [`DesiredWidth`].
DesiredWidth(DesiredWidth),
/// Use [`DesiredWidth::AtLeast`] with a width computed from the provided content, plus any
/// extras such as a button.
ContentWidth(f32),
}
impl Default for CustomContentDesiredWidth {
fn default() -> Self {
Self::DesiredWidth(Default::default())
}
}
/// [`ListItemContent`] that mostly delegates to a closure.
#[expect(clippy::type_complexity)]
pub struct CustomContent<'a> {
ui: Box<dyn FnOnce(&mut egui::Ui, &ContentContext<'_>) + 'a>,
desired_width: CustomContentDesiredWidth,
//TODO(ab): in the future, that should be a `Vec`, with some auto expanding mini-toolbar
button: Option<BoxedWidgetLocal<'a>>,
}
impl<'a> CustomContent<'a> {
/// Create a content with a custom UI closure.
///
/// The closure will be called from within a [`egui::Ui`] with its maximum width set as per the
/// list item geometry. Note that this may differ from [`ContentContext::rect`] if a button is
/// set.
pub fn new(ui: impl FnOnce(&mut egui::Ui, &ContentContext<'_>) + 'a) -> Self {
Self {
ui: Box::new(ui),
desired_width: Default::default(),
button: None,
}
}
/// Set the desired width for the entire content.
#[inline]
pub fn with_desired_width(mut self, desired_width: DesiredWidth) -> Self {
self.desired_width = CustomContentDesiredWidth::DesiredWidth(desired_width);
self
}
/// Set the desired width based on the provided content width. If a button is set, its width
/// will be taken into account and added to the content width.
#[inline]
pub fn with_content_width(mut self, desired_content_width: f32) -> Self {
self.desired_width = CustomContentDesiredWidth::ContentWidth(desired_content_width);
self
}
/// Add a right-aligned button.
///
/// Note: for aesthetics, space is always reserved for the action button.
// TODO(ab): accept multiple calls for this function for multiple actions.
#[inline]
pub fn button(mut self, button: impl Widget + 'a) -> Self {
// TODO(ab): support multiple action buttons
assert!(
self.button.is_none(),
"Only one action button is supported right now"
);
self.button = Some(button.boxed_local());
self
}
/// Helper to add a button to the right of the item.
///
/// The `alt_text` will be used for accessibility (e.g. read by screen readers),
/// and is also how we can query the button in tests.
///
/// See [`Self::button`] for more information.
#[inline]
pub fn action_button(
self,
icon: &'static crate::icons::Icon,
alt_text: impl Into<String>,
on_click: impl FnOnce() + 'a,
) -> Self {
self.action_button_with_enabled(icon, alt_text, true, on_click)
}
/// Helper to add an enabled/disabled button to the right of the item.
///
/// The `alt_text` will be used for accessibility (e.g. read by screen readers),
/// and is also how we can query the button in tests.
///
/// See [`Self::button`] for more information.
#[inline]
pub fn action_button_with_enabled(
self,
icon: &'static crate::icons::Icon,
alt_text: impl Into<String>,
enabled: bool,
on_click: impl FnOnce() + 'a,
) -> Self {
let alt_text = alt_text.into();
self.button(move |ui: &mut Ui| {
ui.add(
ui.small_icon_button_widget(icon, &alt_text)
.on_click(on_click)
.enabled(enabled)
.on_hover_text(alt_text),
)
})
}
/// Helper to add a menu button to the right of the item.
///
/// The `alt_text` will be used for accessibility (e.g. read by screen readers),
/// and is also how we can query the button in tests.
///
/// See [`Self::button`] for more information.
#[inline]
pub fn menu_button(
self,
icon: &'static crate::icons::Icon,
alt_text: impl Into<String>,
add_contents: impl FnOnce(&mut egui::Ui) + 'a,
) -> Self {
let alt_text = alt_text.into();
self.button(|ui: &mut egui::Ui| {
ui.add(
ui.small_icon_button_widget(icon, &alt_text)
.on_menu(add_contents)
.on_hover_text(alt_text),
)
})
}
}
impl ListItemContent for CustomContent<'_> {
fn ui(self: Box<Self>, ui: &mut egui::Ui, context: &ContentContext<'_>) {
let Self {
ui: content_ui,
desired_width: _,
button,
} = *self;
let tokens = ui.tokens();
let button_dimension = tokens.small_icon_size.x + 2.0 * ui.spacing().button_padding.x;
let content_width = if button.is_some() {
(context.rect.width() - button_dimension - tokens.text_to_icon_padding()).at_least(0.0)
} else {
context.rect.width()
};
let content_rect = egui::Rect::from_min_size(
context.rect.min,
egui::vec2(content_width, context.rect.height()),
);
ui.scope_builder(
egui::UiBuilder::new()
.max_rect(content_rect)
.layout(egui::Layout::left_to_right(egui::Align::Center)),
|ui| {
// When selected we override the text color so e.g. syntax highlighted code
// doesn't become unreadable
if context.visuals.selected {
ui.visuals_mut().override_text_color = Some(context.visuals.text_color());
}
content_ui(ui, context);
},
);
if let Some(button) = button {
let action_button_rect = egui::Rect::from_center_size(
context.rect.right_center() - egui::vec2(button_dimension / 2.0, 0.0),
egui::Vec2::splat(button_dimension),
);
// the right to left layout is used to mimic LabelContent's buttons behavior and get a
// better alignment
let mut child_ui = ui.new_child(
egui::UiBuilder::new()
.max_rect(action_button_rect)
.layout(egui::Layout::right_to_left(egui::Align::Center)),
);
button.ui(&mut child_ui);
}
}
fn desired_width(&self, ui: &Ui) -> DesiredWidth {
match self.desired_width {
CustomContentDesiredWidth::DesiredWidth(desired_width) => desired_width,
CustomContentDesiredWidth::ContentWidth(mut content_width) => {
if self.button.is_some() {
let tokens = ui.tokens();
content_width += tokens.small_icon_size.x
+ 2.0 * ui.spacing().button_padding.x
+ tokens.text_to_icon_padding();
}
DesiredWidth::AtLeast(content_width)
}
}
}
}