egui_charts/ui_kit/sidebar_layout/
mod.rs1use crate::localization as l10n;
19use crate::theme::ThemeExt;
20use crate::tokens::DESIGN_TOKENS;
21use egui::{Color32, Pos2, Rect, Role, Sense, Ui, Vec2, WidgetInfo};
22use egui_extras::{Size, StripBuilder};
23
24pub struct SidebarLayout {
28 sidebar_width: f32,
29}
30
31impl SidebarLayout {
32 pub fn new(sidebar_width: f32) -> Self {
34 Self { sidebar_width }
35 }
36
37 pub fn show(self, ui: &mut Ui, sidebar: impl FnOnce(&mut Ui), content: impl FnOnce(&mut Ui)) {
42 StripBuilder::new(ui)
43 .size(Size::exact(self.sidebar_width))
44 .size(Size::remainder())
45 .horizontal(|mut strip| {
46 strip.cell(|ui| {
48 sidebar(ui);
49 });
50
51 strip.cell(|ui| {
53 let inset = ui.active_theme().spacing.xxl;
54 let left = inset - ui.spacing().item_spacing.x;
55 egui::Frame::new()
56 .inner_margin(egui::Margin {
57 left: left as i8,
58 top: inset as i8,
59 ..Default::default()
60 })
61 .show(ui, content);
62 });
63 });
64 }
65}
66
67pub struct SidebarTab<'a> {
87 label: &'a str,
88 icon: Option<&'a crate::icons::Icon>,
89 is_active: bool,
90 fill: Option<Color32>,
91}
92
93impl<'a> SidebarTab<'a> {
94 pub fn new(label: &'a str, is_active: bool) -> Self {
96 Self {
97 label,
98 icon: None,
99 is_active,
100 fill: None,
101 }
102 }
103
104 pub fn icon(mut self, icon: &'a crate::icons::Icon) -> Self {
106 self.icon = Some(icon);
107 self
108 }
109
110 pub(crate) fn fill(mut self, fill: Color32) -> Self {
112 self.fill = Some(fill);
113 self
114 }
115
116 pub fn show(self, ui: &mut Ui) -> bool {
120 let theme = ui.active_theme();
121 let width = ui.available_width();
122 let height = DESIGN_TOKENS.sizing.button_xl;
123 let spacing = std::mem::take(&mut ui.spacing_mut().item_spacing.y);
125 let (rect, response) = ui.allocate_exact_size(Vec2::new(width, height), Sense::click());
126 ui.spacing_mut().item_spacing.y = spacing;
127 let translated_label = l10n::text(ui.ctx(), self.label);
128 response.widget_info(|| WidgetInfo::labeled(Role::Tab, ui.is_enabled(), &translated_label));
129 ui.ctx()
130 .accesskit_node_builder(response.id, |node| node.set_selected(self.is_active));
131
132 if self.is_active || response.hovered() {
133 ui.painter().rect_filled(
134 rect,
135 theme.radius.md,
136 if let Some(fill) = self.fill.filter(|_| self.is_active) {
137 fill
138 } else if self.is_active && !theme.dark {
139 theme.ui.text.gamma_multiply(0.06)
140 } else {
141 ui.style().visuals.widgets.hovered.bg_fill
142 },
143 );
144 }
145
146 let text_color = ui.style().visuals.text_color();
147
148 let icon_size = DESIGN_TOKENS.sizing.icon_md;
150 let icon_x = rect.min.x + theme.spacing.xxl + theme.spacing.sm;
151 if let Some(icon) = self.icon {
152 let icon_rect = Rect::from_min_size(
153 Pos2::new(icon_x, rect.center().y - icon_size / 2.0),
154 Vec2::splat(icon_size),
155 );
156 icon.as_image_tinted(icon_rect.size(), text_color)
157 .paint_at(ui, icon_rect);
158 }
159
160 let mut format =
162 egui::TextFormat::simple(egui::FontId::proportional(theme.text.title), text_color);
163 if self.is_active {
164 format.coords.push("wght", 600.0);
165 }
166 let label_x = icon_x + icon_size + theme.spacing.sm;
167 let mut job = egui::text::LayoutJob::single_section(translated_label, format);
168 job.wrap = egui::text::TextWrapping::truncate_at_width((rect.right() - label_x).max(0.0));
169 let galley = ui.painter().layout_job(job);
170 let label_pos = Pos2::new(label_x, rect.center().y - galley.size().y / 2.0);
171 ui.painter().galley(label_pos, galley, text_color);
172 crate::styles::focus::draw_focus_ring(ui, &response, theme.radius.md);
173
174 response.clicked()
175 }
176}