1use egui::{pos2, vec2, Align, FontId, Layout, Rect, Sense, Ui, UiBuilder};
23use egui_components_theme::Theme;
24
25use crate::icon::{paint_icon, IconKind};
26use crate::tooltip::Tooltip;
27
28pub struct Sidebar {
29 width: f32,
30 collapsed_width: f32,
31 collapsed: bool,
32 selected: Option<usize>,
33}
34
35impl Default for Sidebar {
36 fn default() -> Self {
37 Self::new("sidebar")
38 }
39}
40
41impl Sidebar {
42 pub fn new(_id_salt: impl std::hash::Hash) -> Self {
43 Self {
44 width: 220.0,
45 collapsed_width: 56.0,
46 collapsed: false,
47 selected: None,
48 }
49 }
50 pub fn width(mut self, w: f32) -> Self {
51 self.width = w;
52 self
53 }
54 pub fn collapsed(mut self, c: bool) -> Self {
55 self.collapsed = c;
56 self
57 }
58 pub fn selected(mut self, idx: Option<usize>) -> Self {
59 self.selected = idx;
60 self
61 }
62
63 pub fn show(self, ui: &mut Ui, build: impl FnOnce(&mut SidebarUi)) -> Option<usize> {
64 let theme = Theme::get(ui.ctx());
65 let c = theme.colors;
66 let width = if self.collapsed {
67 self.collapsed_width
68 } else {
69 self.width
70 };
71 let height = ui.available_height();
72
73 let (rect, _) = ui.allocate_exact_size(vec2(width, height), Sense::hover());
74 ui.painter().rect_filled(rect, 0.0, c.muted_background);
76 ui.painter().line_segment(
77 [rect.right_top(), rect.right_bottom()],
78 theme.border_stroke(),
79 );
80
81 let mut content = ui.new_child(
82 UiBuilder::new()
83 .max_rect(rect.shrink(8.0))
84 .layout(Layout::top_down(Align::Min)),
85 );
86
87 let mut sb = SidebarUi {
88 ui: &mut content,
89 theme,
90 collapsed: self.collapsed,
91 selected: self.selected,
92 next_index: 0,
93 clicked: None,
94 };
95 build(&mut sb);
96 sb.clicked
97 }
98}
99
100pub struct SidebarUi<'a> {
102 ui: &'a mut Ui,
103 theme: Theme,
104 collapsed: bool,
105 selected: Option<usize>,
106 next_index: usize,
107 clicked: Option<usize>,
108}
109
110impl SidebarUi<'_> {
111 pub fn header(&mut self, text: impl Into<String>) {
113 if self.collapsed {
114 self.ui.add_space(8.0);
115 return;
116 }
117 self.ui.add_space(8.0);
118 self.ui.add(
119 crate::label::Label::new(text.into())
120 .muted()
121 .size(crate::common::Size::Small),
122 );
123 self.ui.add_space(2.0);
124 }
125
126 pub fn separator(&mut self) {
127 self.ui.add_space(6.0);
128 self.ui.add(crate::separator::Separator::horizontal());
129 self.ui.add_space(6.0);
130 }
131
132 pub fn item(&mut self, icon: IconKind, label: impl Into<String>) -> bool {
134 let index = self.next_index;
135 self.next_index += 1;
136 let selected = self.selected == Some(index);
137 let label = label.into();
138
139 let c = self.theme.colors;
140 let m = self.theme.metrics;
141 let row_h = m.button_height_md;
142 let resp = {
143 let ui = &mut self.ui;
144 let (rect, resp) =
145 ui.allocate_exact_size(vec2(ui.available_width(), row_h), Sense::click());
146
147 if ui.is_rect_visible(rect) {
148 let painter = ui.painter();
149 let bg = if selected {
150 c.secondary_background
151 } else if resp.hovered() {
152 c.accent_background
153 } else {
154 egui::Color32::TRANSPARENT
155 };
156 if bg != egui::Color32::TRANSPARENT {
157 painter.rect_filled(rect, self.theme.corner_sm(), bg);
158 }
159 if selected {
160 painter.rect_filled(
161 Rect::from_min_size(
162 pos2(rect.left(), rect.top() + 5.0),
163 vec2(2.5, rect.height() - 10.0),
164 ),
165 egui::CornerRadius::same(1),
166 c.primary_background,
167 );
168 }
169
170 let fg = if selected { c.foreground } else { c.muted_foreground };
171 let icon_size = 18.0;
172 if self.collapsed {
173 let ir = Rect::from_center_size(rect.center(), vec2(icon_size, icon_size));
174 paint_icon(painter, icon, ir, fg, 1.7);
175 } else {
176 let ir = Rect::from_center_size(
177 pos2(rect.left() + 10.0 + icon_size * 0.5, rect.center().y),
178 vec2(icon_size, icon_size),
179 );
180 paint_icon(painter, icon, ir, fg, 1.7);
181 painter.text(
182 pos2(ir.right() + 10.0, rect.center().y),
183 egui::Align2::LEFT_CENTER,
184 &label,
185 FontId::proportional(m.font_size_md),
186 fg,
187 );
188 }
189 if resp.hovered() {
190 ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
191 }
192 }
193 resp
194 };
195
196 if self.collapsed {
198 Tooltip::new(label).attach(resp.clone());
199 }
200
201 if resp.clicked() {
202 self.clicked = Some(index);
203 true
204 } else {
205 false
206 }
207 }
208}
209
210pub struct Rail {
229 width: f32,
230 selected: Option<usize>,
231}
232
233impl Default for Rail {
234 fn default() -> Self {
235 Self::new("rail")
236 }
237}
238
239impl Rail {
240 pub fn new(_id_salt: impl std::hash::Hash) -> Self {
241 Self {
242 width: 56.0,
243 selected: None,
244 }
245 }
246 pub fn width(mut self, w: f32) -> Self {
247 self.width = w;
248 self
249 }
250 pub fn selected(mut self, idx: Option<usize>) -> Self {
251 self.selected = idx;
252 self
253 }
254
255 pub fn show(self, ui: &mut Ui, build: impl FnOnce(&mut RailUi)) -> Option<usize> {
256 let theme = Theme::get(ui.ctx());
257 let c = theme.colors;
258 let height = ui.available_height();
259
260 let (rect, _) = ui.allocate_exact_size(vec2(self.width, height), Sense::hover());
261 ui.painter().rect_filled(rect, 0.0, c.muted_background);
262 ui.painter().line_segment(
263 [rect.right_top(), rect.right_bottom()],
264 theme.border_stroke(),
265 );
266
267 let mut spec = RailUi {
270 theme,
271 next_index: 0,
272 in_footer: false,
273 top: Vec::new(),
274 bottom: Vec::new(),
275 };
276 build(&mut spec);
277
278 let inner = rect.shrink(8.0);
279 let row_h = 44.0;
280 let mut clicked = None;
281
282 let mut top_ui = ui.new_child(
284 UiBuilder::new()
285 .max_rect(inner)
286 .layout(Layout::top_down(Align::Center)),
287 );
288 for it in &spec.top {
289 if paint_rail_item(&mut top_ui, theme, it, self.selected, row_h) {
290 clicked = Some(it.index);
291 }
292 }
293
294 if !spec.bottom.is_empty() {
296 let footer_h = spec.bottom.len() as f32 * (row_h + top_ui.spacing().item_spacing.y);
297 let footer_rect = Rect::from_min_size(
298 pos2(inner.left(), inner.bottom() - footer_h),
299 vec2(inner.width(), footer_h),
300 );
301 let mut bottom_ui = ui.new_child(
302 UiBuilder::new()
303 .max_rect(footer_rect)
304 .layout(Layout::top_down(Align::Center)),
305 );
306 for it in &spec.bottom {
307 if paint_rail_item(&mut bottom_ui, theme, it, self.selected, row_h) {
308 clicked = Some(it.index);
309 }
310 }
311 }
312
313 clicked
314 }
315}
316
317struct RailItem {
318 icon: IconKind,
319 label: String,
320 index: usize,
321}
322
323pub struct RailUi {
325 theme: Theme,
326 next_index: usize,
327 in_footer: bool,
328 top: Vec<RailItem>,
329 bottom: Vec<RailItem>,
330}
331
332impl RailUi {
333 pub fn item(&mut self, icon: IconKind, label: impl Into<String>) {
336 let item = RailItem {
337 icon,
338 label: label.into(),
339 index: self.next_index,
340 };
341 self.next_index += 1;
342 if self.in_footer {
343 self.bottom.push(item);
344 } else {
345 self.top.push(item);
346 }
347 }
348
349 pub fn footer(&mut self) {
351 self.in_footer = true;
352 }
353
354 pub fn theme(&self) -> Theme {
356 self.theme
357 }
358}
359
360fn paint_rail_item(
361 ui: &mut Ui,
362 theme: Theme,
363 item: &RailItem,
364 selected: Option<usize>,
365 row_h: f32,
366) -> bool {
367 let c = theme.colors;
368 let selected = selected == Some(item.index);
369 let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), row_h), Sense::click());
370
371 if ui.is_rect_visible(rect) {
372 let painter = ui.painter();
373 let bg = if selected {
374 c.secondary_background
375 } else if resp.hovered() {
376 c.accent_background
377 } else {
378 egui::Color32::TRANSPARENT
379 };
380 if bg != egui::Color32::TRANSPARENT {
381 painter.rect_filled(rect, theme.corner_sm(), bg);
382 }
383 if selected {
384 painter.rect_filled(
385 Rect::from_min_size(
386 pos2(rect.left(), rect.top() + 6.0),
387 vec2(2.5, rect.height() - 12.0),
388 ),
389 egui::CornerRadius::same(1),
390 c.primary_background,
391 );
392 }
393 let fg = if selected { c.foreground } else { c.muted_foreground };
394 let icon_size = 20.0;
395 let ir = Rect::from_center_size(rect.center(), vec2(icon_size, icon_size));
396 paint_icon(painter, item.icon, ir, fg, 1.7);
397 if resp.hovered() {
398 ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
399 }
400 }
401
402 Tooltip::new(item.label.clone()).attach(resp.clone());
403 resp.clicked()
404}