1use crate::controls::MacControlSize;
8use crate::layout::subpixel_snap_rect;
9use crate::theme::MacTheme;
10use egui::{
11 CornerRadius, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui, UiBuilder,
12 Vec2, WidgetText,
13};
14
15#[derive(Debug, Clone, Copy, PartialEq, Default)]
17pub struct MacInsets {
18 pub top: f32,
20 pub bottom: f32,
22 pub left: f32,
24 pub right: f32,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30pub enum MacTabStyle {
31 #[default]
33 TopTabsBezelBorder,
34 NoTabsNoBorder,
36}
37
38type TabContentFn<'a> = Box<dyn FnOnce(&mut Ui) + 'a>;
39
40pub struct MacTabItem<'a> {
42 title: WidgetText,
43 content: Option<TabContentFn<'a>>,
44}
45
46pub struct MacTabView<'a> {
73 selected_index: &'a mut usize,
74 style: MacTabStyle,
75 control_size: MacControlSize,
76 theme: Option<&'a MacTheme>,
77 tabs: Vec<MacTabItem<'a>>,
78}
79
80impl<'a> MacTabView<'a> {
81 pub fn new(selected_index: &'a mut usize) -> Self {
83 Self {
84 selected_index,
85 style: MacTabStyle::TopTabsBezelBorder,
86 control_size: MacControlSize::Regular,
87 theme: None,
88 tabs: Vec::new(),
89 }
90 }
91
92 pub fn style(mut self, style: MacTabStyle) -> Self {
94 self.style = style;
95 self
96 }
97
98 pub fn control_size(mut self, size: MacControlSize) -> Self {
100 self.control_size = size;
101 self
102 }
103
104 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
106 self.theme = Some(theme);
107 self
108 }
109
110 pub fn tab(mut self, title: impl Into<WidgetText>, content: impl FnOnce(&mut Ui) + 'a) -> Self {
112 self.tabs.push(MacTabItem {
113 title: title.into(),
114 content: Some(Box::new(content)),
115 });
116 self
117 }
118
119 pub fn tab_bar_height(&self) -> f32 {
121 if self.style == MacTabStyle::NoTabsNoBorder {
122 return 0.0;
123 }
124 let theme_cow = MacTheme::resolve_or_default(self.theme);
125 let m = theme_cow.tab_view();
126 match self.control_size {
127 MacControlSize::Regular | MacControlSize::Large => m.regular_bar_height.points(),
128 MacControlSize::Small | MacControlSize::CompactInfobox => m.small_bar_height.points(),
129 MacControlSize::Mini => m.mini_bar_height.points(),
130 }
131 }
132
133 pub fn content_insets(&self) -> MacInsets {
135 if self.style == MacTabStyle::NoTabsNoBorder {
136 return MacInsets {
137 top: 0.0,
138 bottom: 0.0,
139 left: 0.0,
140 right: 0.0,
141 };
142 }
143 let theme_cow = MacTheme::resolve_or_default(self.theme);
144 let m = theme_cow.tab_view();
145 match self.control_size {
146 MacControlSize::Regular | MacControlSize::Large => MacInsets {
147 top: m.regular_inset_top.points(),
148 bottom: m.regular_inset_bottom.points(),
149 left: m.regular_inset_horizontal.points(),
150 right: m.regular_inset_horizontal.points(),
151 },
152 MacControlSize::Small | MacControlSize::CompactInfobox => MacInsets {
153 top: m.small_inset_top.points(),
154 bottom: m.small_inset_bottom.points(),
155 left: m.small_inset_horizontal.points(),
156 right: m.small_inset_horizontal.points(),
157 },
158 MacControlSize::Mini => MacInsets {
159 top: m.mini_inset_top.points(),
160 bottom: m.mini_inset_bottom.points(),
161 left: m.mini_inset_horizontal.points(),
162 right: m.mini_inset_horizontal.points(),
163 },
164 }
165 }
166
167 pub fn min_container_size(&self) -> Vec2 {
169 let theme_cow = MacTheme::resolve_or_default(self.theme);
170 let m = theme_cow.tab_view();
171 match self.control_size {
172 MacControlSize::Regular | MacControlSize::Large => {
173 Vec2::new(m.regular_min_width.points(), m.regular_min_height.points())
174 }
175 MacControlSize::Small | MacControlSize::CompactInfobox => {
176 Vec2::new(m.small_min_width.points(), m.small_min_height.points())
177 }
178 MacControlSize::Mini => {
179 Vec2::new(m.mini_min_width.points(), m.mini_min_height.points())
180 }
181 }
182 }
183
184 pub fn tab_bar_rect(&self, container_rect: Rect, ui: &Ui) -> Rect {
186 let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
187 let metrics = theme_cow.tab_view();
188 let bar_h = self.tab_bar_height();
189 let min_size = self.min_container_size();
190 let pad = metrics.tab_padding_horizontal.points();
191 let mut total_intrinsic_w = 0.0f32;
192 for tab in &self.tabs {
193 let g = tab.title.clone().into_galley(
194 ui,
195 Some(egui::TextWrapMode::Extend),
196 f32::INFINITY,
197 egui::TextStyle::Button,
198 );
199 total_intrinsic_w += g.size().x + pad;
200 }
201 let bar_w = total_intrinsic_w.clamp(min_size.x.min(container_rect.width()), container_rect.width());
202 Rect::from_center_size(
203 Pos2::new(container_rect.center().x, container_rect.min.y + bar_h * 0.5),
204 Vec2::new(bar_w, bar_h),
205 )
206 }
207
208 pub fn show(mut self, ui: &mut Ui) -> Response {
210 let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
211 let theme: &MacTheme = &theme_cow;
212 let metrics = theme.tab_view();
213
214 let available_w = ui.available_width();
215 let start_pos = ui.cursor().min;
216 let bar_h = self.tab_bar_height();
217 let insets = self.content_insets();
218 let min_size = self.min_container_size();
219
220 let tab_count = self.tabs.len();
221 let active_idx = *self.selected_index;
222
223 let container_w = available_w.max(min_size.x);
224 let content_start_y = start_pos.y + insets.top;
225 let content_rect = Rect::from_min_max(
226 Pos2::new(start_pos.x + insets.left, content_start_y),
227 Pos2::new(start_pos.x + container_w - insets.right, f32::INFINITY),
228 );
229
230 let bg_shape_idx = ui.painter().add(egui::Shape::Noop);
232 let tab_scope_id = ui.next_auto_id().with("tab_content").with(active_idx);
233
234 let mut child_ui = ui.new_child(
235 UiBuilder::new()
236 .id_salt(tab_scope_id)
237 .max_rect(content_rect),
238 );
239
240 if active_idx < tab_count {
241 if let Some(content_fn) = self.tabs[active_idx].content.take() {
242 content_fn(&mut child_ui);
243 }
244 }
245
246 let child_min_rect = child_ui.min_rect();
247 let content_h = if active_idx < tab_count {
248 child_min_rect.height()
249 } else {
250 0.0
251 };
252
253 let container_h = (if self.style == MacTabStyle::TopTabsBezelBorder {
254 insets.top + content_h + insets.bottom
255 } else {
256 content_h
257 }).max(min_size.y);
258
259 let container_rect = Rect::from_min_size(start_pos, Vec2::new(container_w, container_h));
260 let container_rect = subpixel_snap_rect(container_rect, ui.ctx().pixels_per_point());
261
262 let (rect, mut response) = ui.allocate_exact_size(container_rect.size(), Sense::click());
263
264 if self.style == MacTabStyle::TopTabsBezelBorder {
265 let corner_r = metrics.corner_radius.points();
266
267 let body_rect = Rect::from_min_max(
269 Pos2::new(rect.min.x, rect.min.y + bar_h * 0.5),
270 rect.max,
271 );
272 ui.painter().set(
273 bg_shape_idx,
274 egui::epaint::RectShape::new(
275 body_rect,
276 CornerRadius::same(corner_r as u8),
277 theme.control_background(),
278 Stroke::new(1.0, theme.separator()),
279 StrokeKind::Inside,
280 ),
281 );
282
283 let painter = ui.painter_at(rect);
284
285 if tab_count > 0 {
287 let bar_rect = self.tab_bar_rect(rect, ui);
288 painter.rect(
289 bar_rect,
290 CornerRadius::same(corner_r as u8),
291 theme.window_background(),
292 Stroke::new(1.0, theme.separator()),
293 StrokeKind::Inside,
294 );
295
296 let tab_w = bar_rect.width() / (tab_count as f32);
297 for i in 0..tab_count {
298 let tab_rect = Rect::from_min_size(
299 Pos2::new(bar_rect.min.x + (i as f32) * tab_w, bar_rect.min.y),
300 Vec2::new(tab_w, bar_h),
301 );
302 let tab_hovered = ui
303 .input(|inp| inp.pointer.hover_pos())
304 .is_some_and(|p| tab_rect.contains(p));
305
306 let is_active = *self.selected_index == i;
307 if is_active || tab_hovered {
308 let tab_fill = if is_active {
309 theme.control_background()
310 } else {
311 theme.control()
312 };
313
314 painter.rect(
315 tab_rect,
316 CornerRadius::same(corner_r as u8),
317 tab_fill,
318 Stroke::new(1.0, theme.separator()),
319 StrokeKind::Inside,
320 );
321 }
322
323 let galley = self.tabs[i].title.clone().into_galley(
324 ui,
325 Some(egui::TextWrapMode::Extend),
326 tab_w,
327 egui::TextStyle::Button,
328 );
329 let text_pos = Pos2::new(
330 tab_rect.center().x - galley.size().x * 0.5,
331 tab_rect.center().y - galley.size().y * 0.5,
332 );
333 let text_color = if is_active {
334 theme.label()
335 } else {
336 theme.secondary_label()
337 };
338 painter.galley(text_pos, galley, text_color);
339
340 if tab_hovered && ui.input(|inp| inp.pointer.primary_clicked()) {
341 *self.selected_index = i;
342 response.mark_changed();
343 }
344 }
345 }
346 }
347
348 response
349 }
350}