use crate::controls::MacControlSize;
use crate::layout::subpixel_snap_rect;
use crate::theme::MacTheme;
use egui::{
CornerRadius, Id, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui, UiBuilder,
Vec2, WidgetText,
};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct MacInsets {
pub top: f32,
pub bottom: f32,
pub left: f32,
pub right: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MacTabStyle {
#[default]
TopTabsBezelBorder,
NoTabsNoBorder,
}
type TabContentFn<'a> = Box<dyn FnOnce(&mut Ui) + 'a>;
pub struct MacTabItem<'a> {
title: WidgetText,
content: Option<TabContentFn<'a>>,
}
pub struct MacTabView<'a> {
selected_index: &'a mut usize,
style: MacTabStyle,
control_size: MacControlSize,
theme: Option<&'a MacTheme>,
tabs: Vec<MacTabItem<'a>>,
}
impl<'a> MacTabView<'a> {
pub fn new(selected_index: &'a mut usize) -> Self {
Self {
selected_index,
style: MacTabStyle::TopTabsBezelBorder,
control_size: MacControlSize::Regular,
theme: None,
tabs: Vec::new(),
}
}
pub fn style(mut self, style: MacTabStyle) -> Self {
self.style = style;
self
}
pub fn control_size(mut self, size: MacControlSize) -> Self {
self.control_size = size;
self
}
pub fn theme(mut self, theme: &'a MacTheme) -> Self {
self.theme = Some(theme);
self
}
pub fn tab(mut self, title: impl Into<WidgetText>, content: impl FnOnce(&mut Ui) + 'a) -> Self {
self.tabs.push(MacTabItem {
title: title.into(),
content: Some(Box::new(content)),
});
self
}
pub fn tab_bar_height(&self) -> f32 {
if self.style == MacTabStyle::NoTabsNoBorder {
return 0.0;
}
let theme_cow = MacTheme::resolve(self.theme, &egui::Context::default());
let m = theme_cow.tab_view();
match self.control_size {
MacControlSize::Regular | MacControlSize::Large => m.regular_bar_height.points(),
MacControlSize::Small | MacControlSize::CompactInfobox => m.small_bar_height.points(),
MacControlSize::Mini => m.mini_bar_height.points(),
}
}
pub fn content_insets(&self) -> MacInsets {
if self.style == MacTabStyle::NoTabsNoBorder {
return MacInsets {
top: 0.0,
bottom: 0.0,
left: 0.0,
right: 0.0,
};
}
let theme_cow = MacTheme::resolve(self.theme, &egui::Context::default());
let m = theme_cow.tab_view();
match self.control_size {
MacControlSize::Regular | MacControlSize::Large => MacInsets {
top: m.regular_inset_top.points(),
bottom: m.regular_inset_bottom.points(),
left: m.regular_inset_horizontal.points(),
right: m.regular_inset_horizontal.points(),
},
MacControlSize::Small | MacControlSize::CompactInfobox => MacInsets {
top: m.small_inset_top.points(),
bottom: m.small_inset_bottom.points(),
left: m.small_inset_horizontal.points(),
right: m.small_inset_horizontal.points(),
},
MacControlSize::Mini => MacInsets {
top: m.mini_inset_top.points(),
bottom: m.mini_inset_bottom.points(),
left: m.mini_inset_horizontal.points(),
right: m.mini_inset_horizontal.points(),
},
}
}
pub fn show(mut self, ui: &mut Ui) -> Response {
let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
let theme: &MacTheme = &theme_cow;
let available_w = ui.available_width();
let start_pos = ui.cursor().min;
let bar_h = self.tab_bar_height();
let insets = self.content_insets();
let tab_count = self.tabs.len();
let active_idx = *self.selected_index;
let content_start_y = start_pos.y + insets.top;
let content_rect = Rect::from_min_max(
Pos2::new(start_pos.x + insets.left, content_start_y),
Pos2::new(start_pos.x + available_w - insets.right, f32::INFINITY),
);
let mut child_ui = ui.new_child(
UiBuilder::new()
.id_salt(Id::NULL.with("tab_content").with(active_idx))
.max_rect(content_rect),
);
if active_idx < tab_count {
if let Some(content_fn) = self.tabs[active_idx].content.take() {
content_fn(&mut child_ui);
}
}
let child_min_rect = child_ui.min_rect();
let content_h = if active_idx < tab_count {
child_min_rect.height()
} else {
0.0
};
let container_h = if self.style == MacTabStyle::TopTabsBezelBorder {
insets.top + content_h + insets.bottom
} else {
content_h
};
let container_rect = Rect::from_min_size(start_pos, Vec2::new(available_w, container_h));
let container_rect = subpixel_snap_rect(container_rect, ui.ctx().pixels_per_point());
let (rect, mut response) = ui.allocate_exact_size(container_rect.size(), Sense::click());
if self.style == MacTabStyle::TopTabsBezelBorder {
let painter = ui.painter_at(rect);
let corner_r = theme.tab_view().corner_radius.points();
let body_rect = Rect::from_min_max(
Pos2::new(rect.min.x, rect.min.y + bar_h * 0.5),
rect.max,
);
painter.rect(
body_rect,
CornerRadius::same(corner_r as u8),
theme.control_background(),
Stroke::new(1.0, theme.separator()),
StrokeKind::Inside,
);
if tab_count > 0 {
let tab_w = available_w / (tab_count as f32);
for i in 0..tab_count {
let tab_rect = Rect::from_min_size(
Pos2::new(rect.min.x + (i as f32) * tab_w, rect.min.y),
Vec2::new(tab_w, bar_h),
);
let tab_hovered = ui
.input(|inp| inp.pointer.hover_pos())
.is_some_and(|p| tab_rect.contains(p));
let is_active = *self.selected_index == i;
let tab_fill = if is_active {
theme.control_background()
} else if tab_hovered {
theme.control()
} else {
theme.window_background()
};
painter.rect(
tab_rect,
CornerRadius {
nw: corner_r as u8,
ne: corner_r as u8,
sw: 0,
se: 0,
},
tab_fill,
Stroke::new(1.0, theme.separator()),
StrokeKind::Inside,
);
let galley = self.tabs[i].title.clone().into_galley(
ui,
Some(egui::TextWrapMode::Extend),
tab_w,
egui::TextStyle::Button,
);
let text_pos = Pos2::new(
tab_rect.center().x - galley.size().x * 0.5,
tab_rect.center().y - galley.size().y * 0.5,
);
let text_color = if is_active {
theme.label()
} else {
theme.secondary_label()
};
painter.galley(text_pos, galley, text_color);
if tab_hovered && ui.input(|inp| inp.pointer.primary_clicked()) {
*self.selected_index = i;
response.mark_changed();
}
}
}
}
response
}
}