use egui::{Color32, FontId, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2, Widget, vec2};
use super::{alpha, corner};
use crate::{Icon, RADIUS, SPACING, palette_of};
pub struct NavItem<'a> {
label: &'a str,
icon: Option<Icon>,
selected: bool,
badge: Option<&'a str>,
disabled: bool,
}
impl<'a> NavItem<'a> {
pub fn new(label: &'a str) -> Self {
Self {
label,
icon: None,
selected: false,
badge: None,
disabled: false,
}
}
pub fn icon(mut self, icon: Icon) -> Self {
self.icon = Some(icon);
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn badge(mut self, text: &'a str) -> Self {
self.badge = Some(text);
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
impl<'a> Widget for NavItem<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let palette = palette_of(ui.ctx());
let height = 32.0;
let width = ui.available_width().max(180.0);
let sense = if self.disabled {
Sense::hover()
} else {
Sense::click()
};
let (rect, response) = ui.allocate_exact_size(vec2(width, height), sense);
let bg = if self.disabled {
Color32::TRANSPARENT
} else if self.selected {
alpha(palette.brand_default, 0.12)
} else if response.hovered() {
palette.bg_hover
} else {
Color32::TRANSPARENT
};
ui.painter().rect_filled(rect, corner(RADIUS.md), bg);
if self.selected {
let bar = Rect::from_min_max(
egui::pos2(rect.left(), rect.top() + 6.0),
egui::pos2(rect.left() + 3.0, rect.bottom() - 6.0),
);
ui.painter()
.rect_filled(bar, corner(RADIUS.sm), palette.brand_default);
}
let pad = SPACING.s3;
let mut x = rect.left() + pad;
let cy = rect.center().y;
if let Some(icon) = self.icon {
let size = 16.0;
let r = Rect::from_min_size(egui::pos2(x, cy - size / 2.0), Vec2::splat(size));
let c = if self.disabled {
alpha(palette.text_secondary, 0.45)
} else if self.selected {
palette.brand_default
} else {
palette.text_secondary
};
icon.paint(ui.painter(), r, c);
x += size + SPACING.s2;
}
let text_color = if self.disabled {
alpha(palette.text_primary, 0.45)
} else if self.selected {
palette.text_primary
} else {
palette.text_secondary
};
let font = FontId::new(13.0, egui::FontFamily::Proportional);
let galley = ui
.painter()
.layout_no_wrap(self.label.into(), font, text_color);
ui.painter().galley(
egui::pos2(x, cy - galley.size().y / 2.0),
galley,
text_color,
);
if let Some(badge_text) = self.badge {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.add_space(pad);
let bf = FontId::new(11.0, egui::FontFamily::Proportional);
let bg_galley =
ui.painter()
.layout_no_wrap(badge_text.into(), bf, palette.text_secondary);
let bw = bg_galley.size().x + 10.0;
let bh = 18.0;
let by = cy - bh / 2.0;
let bx = rect.right() - pad - bw;
let badge_rect = Rect::from_min_size(egui::pos2(bx, by), vec2(bw, bh));
ui.painter()
.rect_filled(badge_rect, corner(RADIUS.full), palette.bg_surface_alt);
ui.painter().galley(
egui::pos2(bx + 5.0, cy - bg_galley.size().y / 2.0),
bg_galley,
palette.text_secondary,
);
});
}
if response.has_focus() && !self.disabled {
ui.painter().rect_stroke(
rect.expand(2.0),
corner(RADIUS.md),
Stroke::new(2.0, palette.focus_ring),
StrokeKind::Outside,
);
}
response
}
}
pub struct Tabs<'a, T: Clone + PartialEq> {
selected: &'a mut T,
items: Vec<(T, &'a str, Option<Icon>)>,
}
impl<'a, T: Clone + PartialEq> Tabs<'a, T> {
pub fn new(selected: &'a mut T) -> Self {
Self {
selected,
items: Vec::new(),
}
}
pub fn tab(mut self, value: T, label: &'a str) -> Self {
self.items.push((value, label, None));
self
}
pub fn tab_with_icon(mut self, value: T, icon: Icon, label: &'a str) -> Self {
self.items.push((value, label, Some(icon)));
self
}
pub fn show(self, ui: &mut Ui) -> bool {
let palette = palette_of(ui.ctx());
let mut changed = false;
let row_resp = ui
.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
for (value, label, icon) in self.items.iter() {
let is_selected = self.selected == value;
let font = FontId::new(13.0, egui::FontFamily::Proportional);
let galley =
ui.painter()
.layout_no_wrap((*label).into(), font, palette.text_primary);
let icon_w = if icon.is_some() { 16.0 + 6.0 } else { 0.0 };
let pad = SPACING.s3;
let height = 36.0;
let width = galley.size().x + icon_w + pad * 2.0;
let (rect, resp) = ui.allocate_exact_size(vec2(width, height), Sense::click());
let bg = if is_selected {
Color32::TRANSPARENT
} else if resp.hovered() {
palette.bg_hover
} else {
Color32::TRANSPARENT
};
ui.painter().rect_filled(rect, corner(RADIUS.sm), bg);
let mut x = rect.left() + pad;
let cy = rect.center().y;
let fg = if is_selected {
palette.text_primary
} else {
palette.text_secondary
};
if let Some(icon) = icon {
let r = Rect::from_min_size(egui::pos2(x, cy - 8.0), Vec2::splat(16.0));
icon.paint(ui.painter(), r, fg);
x += icon_w;
}
let galley = ui.painter().layout_no_wrap(
(*label).into(),
FontId::new(13.0, egui::FontFamily::Proportional),
fg,
);
ui.painter()
.galley(egui::pos2(x, cy - galley.size().y / 2.0), galley, fg);
if is_selected {
let bar = Rect::from_min_max(
egui::pos2(rect.left() + pad, rect.bottom() - 2.0),
egui::pos2(rect.right() - pad, rect.bottom()),
);
ui.painter()
.rect_filled(bar, corner(0.0), palette.brand_default);
}
if resp.clicked() && !is_selected {
*self.selected = value.clone();
changed = true;
}
}
})
.response;
let r = row_resp.rect;
ui.painter().line_segment(
[
egui::pos2(r.left(), r.bottom() - 1.0),
egui::pos2(r.right(), r.bottom() - 1.0),
],
Stroke::new(1.0, palette.border_subtle),
);
changed
}
}
pub struct Breadcrumb<'a> {
items: Vec<&'a str>,
}
impl<'a> Breadcrumb<'a> {
pub fn new(items: &[&'a str]) -> Self {
Self {
items: items.to_vec(),
}
}
pub fn item(mut self, label: &'a str) -> Self {
self.items.push(label);
self
}
pub fn show(self, ui: &mut Ui) -> Option<usize> {
let palette = palette_of(ui.ctx());
let mut clicked = None;
ui.horizontal(|ui| {
for (i, label) in self.items.iter().enumerate() {
let is_last = i == self.items.len() - 1;
let color = if is_last {
palette.text_primary
} else {
palette.text_secondary
};
let resp = ui.add(
egui::Label::new(
egui::RichText::new(*label)
.font(FontId::new(13.0, egui::FontFamily::Proportional))
.color(color),
)
.sense(if is_last {
Sense::hover()
} else {
Sense::click()
}),
);
if !is_last && resp.clicked() {
clicked = Some(i);
}
if !is_last {
let (rect, _) = ui.allocate_exact_size(vec2(12.0, 12.0), Sense::hover());
Icon::ChevronRight.paint(ui.painter(), rect, palette.text_tertiary);
}
}
});
clicked
}
}