use crate::get_global_color;
use egui::{self, Color32, FontId, Pos2, Rect, Response, Sense, Ui, Vec2, Widget};
use egui::epaint::CornerRadius;
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct MaterialTabs<'a> {
selected: &'a mut usize,
tabs: Vec<TabItem>,
enabled: bool,
variant: TabVariant,
id_salt: Option<String>,
height: Option<f32>,
}
pub struct TabItem {
label: String,
icon: Option<String>,
}
#[derive(Clone, Copy, PartialEq)]
pub enum TabVariant {
Primary,
Secondary,
}
impl<'a> MaterialTabs<'a> {
pub fn new(selected: &'a mut usize, variant: TabVariant) -> Self {
Self {
selected,
tabs: Vec::new(),
enabled: true,
variant,
id_salt: None,
height: None,
}
}
pub fn primary(selected: &'a mut usize) -> Self {
Self::new(selected, TabVariant::Primary)
}
pub fn secondary(selected: &'a mut usize) -> Self {
Self::new(selected, TabVariant::Secondary)
}
pub fn tab(mut self, label: impl Into<String>) -> Self {
self.tabs.push(TabItem {
label: label.into(),
icon: None,
});
self
}
pub fn tab_with_icon(mut self, label: impl Into<String>, icon: impl Into<String>) -> Self {
self.tabs.push(TabItem {
label: label.into(),
icon: Some(icon.into()),
});
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn id_salt(mut self, salt: impl Into<String>) -> Self {
self.id_salt = Some(salt.into());
self
}
pub fn height(mut self, height: f32) -> Self {
self.height = Some(height);
self
}
}
const TAB_HEIGHT_TEXT_ONLY: f32 = 46.0;
const TAB_HEIGHT_WITH_ICON: f32 = 72.0;
const PRIMARY_INDICATOR_HEIGHT: f32 = 3.0;
const SECONDARY_INDICATOR_HEIGHT: f32 = 2.0;
const INDICATOR_TOP_ROUNDING: f32 = 3.0;
const DIVIDER_HEIGHT: f32 = 1.0;
const LABEL_FONT_SIZE: f32 = 14.0;
const ICON_FONT_SIZE: f32 = 18.0;
impl<'a> Widget for MaterialTabs<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let has_icons = self.tabs.iter().any(|t| t.icon.is_some());
let tab_height = self
.height
.unwrap_or(if has_icons { TAB_HEIGHT_WITH_ICON } else { TAB_HEIGHT_TEXT_ONLY });
let tab_width = ui.available_width() / self.tabs.len().max(1) as f32;
let desired_size = Vec2::new(ui.available_width(), tab_height);
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::hover());
let primary_color = get_global_color("primary");
let surface_container = get_global_color("surfaceContainer");
let surface = get_global_color("surface");
let on_surface = get_global_color("onSurface");
let on_surface_variant = get_global_color("onSurfaceVariant");
let outline_variant = get_global_color("outlineVariant");
let bg_color = match self.variant {
TabVariant::Primary => surface_container,
TabVariant::Secondary => surface,
};
ui.painter().rect_filled(rect, 0.0, bg_color);
let mut any_clicked = false;
let label_font = FontId::proportional(LABEL_FONT_SIZE);
let icon_font = FontId::proportional(ICON_FONT_SIZE);
for (index, tab) in self.tabs.iter().enumerate() {
let tab_rect = Rect::from_min_size(
Pos2::new(rect.min.x + index as f32 * tab_width, rect.min.y),
Vec2::new(tab_width, tab_height),
);
let tab_id = if let Some(ref salt) = self.id_salt {
egui::Id::new((salt, "tab", index))
} else {
egui::Id::new(("tab", index))
};
let tab_response = ui.interact(tab_rect, tab_id, Sense::click());
let is_selected = *self.selected == index;
let is_hovered = tab_response.hovered();
let text_color = match self.variant {
TabVariant::Primary => {
if is_selected {
primary_color
} else {
on_surface_variant
}
}
TabVariant::Secondary => {
if is_selected {
on_surface
} else {
on_surface_variant
}
}
};
let text_color = if self.enabled {
text_color
} else {
text_color.linear_multiply(0.38)
};
if is_hovered && self.enabled {
let state_layer_color = match self.variant {
TabVariant::Primary => primary_color,
TabVariant::Secondary => on_surface,
};
let hover_color = Color32::from_rgba_unmultiplied(
state_layer_color.r(),
state_layer_color.g(),
state_layer_color.b(),
20, );
ui.painter().rect_filled(tab_rect, 0.0, hover_color);
}
if tab_response.clicked() && self.enabled {
*self.selected = index;
any_clicked = true;
}
if let Some(icon) = &tab.icon {
let icon_y = tab_rect.center().y - 10.0;
let label_y = tab_rect.center().y + 12.0;
ui.painter().text(
Pos2::new(tab_rect.center().x, icon_y),
egui::Align2::CENTER_CENTER,
icon,
icon_font.clone(),
text_color,
);
ui.painter().text(
Pos2::new(tab_rect.center().x, label_y),
egui::Align2::CENTER_CENTER,
&tab.label,
label_font.clone(),
text_color,
);
} else {
ui.painter().text(
tab_rect.center(),
egui::Align2::CENTER_CENTER,
&tab.label,
label_font.clone(),
text_color,
);
}
if is_selected && self.enabled {
match self.variant {
TabVariant::Primary => {
let galley = ui.painter().layout_no_wrap(
tab.label.clone(),
label_font.clone(),
text_color,
);
let label_width = galley.size().x + 16.0; let indicator_x =
tab_rect.center().x - label_width / 2.0;
let indicator_rect = Rect::from_min_size(
Pos2::new(indicator_x, tab_rect.max.y - PRIMARY_INDICATOR_HEIGHT),
Vec2::new(label_width, PRIMARY_INDICATOR_HEIGHT),
);
let rounding = CornerRadius {
nw: INDICATOR_TOP_ROUNDING as u8,
ne: INDICATOR_TOP_ROUNDING as u8,
sw: 0,
se: 0,
};
ui.painter()
.rect_filled(indicator_rect, rounding, primary_color);
}
TabVariant::Secondary => {
let indicator_rect = Rect::from_min_size(
Pos2::new(tab_rect.min.x, tab_rect.max.y - SECONDARY_INDICATOR_HEIGHT),
Vec2::new(tab_width, SECONDARY_INDICATOR_HEIGHT),
);
ui.painter()
.rect_filled(indicator_rect, 0.0, primary_color);
}
}
}
}
let divider_rect = Rect::from_min_size(
Pos2::new(rect.min.x, rect.max.y - DIVIDER_HEIGHT),
Vec2::new(rect.width(), DIVIDER_HEIGHT),
);
ui.painter().rect_filled(divider_rect, 0.0, outline_variant);
if any_clicked {
response.mark_changed();
}
response
}
}
pub fn tabs_primary<'a>(selected: &'a mut usize) -> MaterialTabs<'a> {
MaterialTabs::primary(selected)
}
pub fn tabs_secondary<'a>(selected: &'a mut usize) -> MaterialTabs<'a> {
MaterialTabs::secondary(selected)
}