use eframe::egui::{self, Align2, Color32, CornerRadius, FontId, Response, RichText, Sense, vec2};
use super::icons::{self, Icon};
use crate::theme::Palette;
pub fn logo(ui: &mut egui::Ui, size: f32) {
const RUST: Color32 = Color32::from_rgb(0xd8, 0x50, 0x30);
const STEEL: Color32 = Color32::from_rgb(0x17, 0x19, 0x1f);
const CREAM: Color32 = Color32::from_rgb(0xf7, 0xf0, 0xe8);
let (rect, _) = ui.allocate_exact_size(vec2(size, size), Sense::hover());
let painter = ui.painter();
painter.rect_filled(rect, CornerRadius::same((size * 0.22) as u8), RUST);
let center = rect.center();
let r = size * 0.4;
let nut: Vec<egui::Pos2> = (0..6)
.map(|i| {
let angle = (60.0 * i as f32 - 90.0).to_radians();
center + vec2(angle.cos(), angle.sin()) * r
})
.collect();
painter.add(egui::Shape::convex_polygon(nut, STEEL, egui::Stroke::NONE));
let container = egui::Rect::from_center_size(center, vec2(r * 1.14, r * 0.66));
painter.rect_filled(container, CornerRadius::same(1), CREAM);
}
pub fn dot(ui: &mut egui::Ui, color: Color32) {
let (rect, _) = ui.allocate_exact_size(vec2(8.0, 8.0), Sense::hover());
ui.painter().circle_filled(rect.center(), 4.0, color);
}
pub fn nav_item(
ui: &mut egui::Ui,
p: &Palette,
icon: Icon,
label: &str,
badge: &str,
selected: bool,
) -> Response {
let (rect, response) = ui.allocate_exact_size(vec2(ui.available_width(), 32.0), Sense::click());
let painter = ui.painter();
if selected {
painter.rect_filled(rect, CornerRadius::same(8), p.accent.gamma_multiply(0.13));
} else if response.hovered() {
painter.rect_filled(rect, CornerRadius::same(8), p.raised);
}
let icon_rect =
egui::Rect::from_center_size(rect.left_center() + vec2(19.0, 0.0), vec2(17.0, 17.0));
icons::paint(
painter,
icon_rect,
icon,
if selected { p.accent } else { p.muted },
);
painter.text(
rect.left_center() + vec2(36.0, 0.0),
Align2::LEFT_CENTER,
label,
if selected {
FontId::new(13.5, crate::theme::semibold())
} else {
FontId::proportional(13.5)
},
p.text,
);
painter.text(
rect.right_center() - vec2(10.0, 0.0),
Align2::RIGHT_CENTER,
badge,
FontId::proportional(12.0),
p.muted,
);
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Tab, true, selected, label));
response.on_hover_cursor(egui::CursorIcon::PointingHand)
}
pub fn segmented<R>(ui: &mut egui::Ui, p: &Palette, add: impl FnOnce(&mut egui::Ui) -> R) -> R {
egui::Frame::new()
.fill(p.raised)
.corner_radius(CornerRadius::same(9))
.inner_margin(egui::Margin::same(3))
.show(ui, |ui| {
ui.spacing_mut().item_spacing.x = 2.0;
ui.horizontal(add).inner
})
.inner
}
pub fn tab(
ui: &mut egui::Ui,
p: &Palette,
label: &str,
count: Option<usize>,
selected: bool,
) -> Response {
let text = match count {
Some(count) => format!("{label} {count}"),
None => label.to_owned(),
};
let font = if selected {
FontId::new(13.0, crate::theme::semibold())
} else {
FontId::proportional(13.0)
};
let galley = ui.painter().layout_no_wrap(text, font, p.text);
let (rect, response) = ui.allocate_exact_size(galley.size() + vec2(22.0, 10.0), Sense::click());
let painter = ui.painter();
if selected {
painter.add(egui::Shape::rect_filled(
rect.translate(vec2(0.0, 0.5)),
CornerRadius::same(7),
Color32::from_black_alpha(18),
));
painter.rect_filled(rect, CornerRadius::same(7), p.bg);
} else if response.hovered() {
painter.rect_filled(rect, CornerRadius::same(7), p.border.gamma_multiply(0.6));
}
let color = if selected { p.text } else { p.muted };
painter.galley_with_override_text_color(rect.min + vec2(11.0, 5.0), galley, color);
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Tab, true, selected, label));
response
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Mark {
Dot,
Ring,
Square,
Triangle,
}
pub fn pill(ui: &mut egui::Ui, label: &str, color: Color32) {
pill_with(ui, label, color, Mark::Dot);
}
pub fn pill_with(ui: &mut egui::Ui, label: &str, color: Color32, mark: Mark) {
let galley = ui
.painter()
.layout_no_wrap(label.to_owned(), FontId::proportional(12.0), color);
let padding = vec2(8.0, 3.0);
let dot_space = 12.0;
let size = galley.size() + padding * 2.0 + vec2(dot_space, 0.0);
let (rect, _) = ui.allocate_exact_size(size, Sense::hover());
let painter = ui.painter();
painter.rect_filled(rect, CornerRadius::same(99), color.gamma_multiply(0.15));
let center = egui::pos2(rect.left() + padding.x + 3.0, rect.center().y);
match mark {
Mark::Dot => {
painter.circle_filled(center, 3.0, color);
}
Mark::Ring => {
painter.circle_stroke(center, 2.6, egui::Stroke::new(1.2, color));
}
Mark::Square => {
let square = egui::Rect::from_center_size(center, vec2(5.5, 5.5));
painter.rect_filled(square, CornerRadius::same(1), color);
}
Mark::Triangle => {
let points = vec![
center + vec2(0.0, -3.6),
center + vec2(3.4, 2.6),
center + vec2(-3.4, 2.6),
];
painter.add(egui::Shape::convex_polygon(
points,
color,
egui::Stroke::NONE,
));
}
}
painter.galley(rect.min + padding + vec2(dot_space, 0.0), galley, color);
}
pub fn icon_button(ui: &mut egui::Ui, icon: Icon, tooltip: &str) -> Response {
let p = crate::theme::palette(ui);
let (rect, response) = ui.allocate_exact_size(vec2(28.0, 28.0), Sense::click());
let painter = ui.painter();
if response.hovered() {
painter.rect_filled(rect, CornerRadius::same(7), p.raised);
}
let color = if response.hovered() { p.text } else { p.muted };
icons::paint(
painter,
egui::Rect::from_center_size(rect.center(), vec2(15.0, 15.0)),
icon,
color,
);
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, tooltip));
response
.on_hover_text(tooltip)
.on_hover_cursor(egui::CursorIcon::PointingHand)
}
pub fn button(ui: &mut egui::Ui, icon: Icon, label: &str) -> Response {
let p = crate::theme::palette(ui);
let galley = ui
.painter()
.layout_no_wrap(label.to_owned(), FontId::proportional(13.0), p.text);
let size = vec2(galley.size().x + 44.0, 28.0);
let (rect, response) = ui.allocate_exact_size(size, Sense::click());
let painter = ui.painter();
let fill = if response.is_pointer_button_down_on() {
p.border
} else if response.hovered() {
p.border.gamma_multiply(0.7)
} else {
p.raised
};
painter.rect_filled(rect, CornerRadius::same(8), fill);
icons::paint(
painter,
egui::Rect::from_center_size(rect.left_center() + vec2(19.0, 0.0), vec2(14.0, 14.0)),
icon,
p.text,
);
painter.galley(
egui::pos2(rect.left() + 32.0, rect.center().y - galley.size().y / 2.0),
galley,
p.text,
);
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, label));
response.on_hover_cursor(egui::CursorIcon::PointingHand)
}
pub fn primary_button(ui: &mut egui::Ui, label: &str) -> Response {
let p = crate::theme::palette(ui);
let enabled = ui.is_enabled();
let galley = ui.painter().layout_no_wrap(
label.to_owned(),
FontId::new(13.5, crate::theme::semibold()),
Color32::WHITE,
);
let size = vec2(galley.size().x + 32.0, 30.0);
let (rect, response) = ui.allocate_exact_size(size, Sense::click());
let fill = if !enabled {
p.accent.gamma_multiply(0.4)
} else if response.is_pointer_button_down_on() {
p.accent.gamma_multiply(0.85)
} else if response.hovered() {
p.accent.gamma_multiply(0.92)
} else {
p.accent
};
ui.painter().rect_filled(rect, CornerRadius::same(8), fill);
ui.painter()
.galley(rect.center() - galley.size() / 2.0, galley, Color32::WHITE);
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, enabled, label));
response.on_hover_cursor(egui::CursorIcon::PointingHand)
}
pub fn danger_button(ui: &mut egui::Ui, p: &Palette, label: &str) -> Response {
ui.add(
egui::Button::new(RichText::new(label).color(p.danger)).fill(p.danger.gamma_multiply(0.15)),
)
}
pub fn empty_state(
ui: &mut egui::Ui,
p: &Palette,
icon: Icon,
title: &str,
hint: &str,
action: Option<&str>,
) -> bool {
let mut clicked = false;
ui.add_space(56.0);
ui.vertical_centered(|ui| {
let (rect, _) = ui.allocate_exact_size(vec2(64.0, 64.0), Sense::hover());
ui.painter()
.rect_filled(rect, CornerRadius::same(18), p.accent.gamma_multiply(0.1));
icons::paint(ui.painter(), rect.shrink(18.0), icon, p.accent);
ui.add_space(14.0);
ui.label(
RichText::new(title)
.font(FontId::new(17.0, crate::theme::semibold()))
.color(p.text),
);
ui.add_space(4.0);
ui.label(RichText::new(hint).color(p.muted));
if let Some(label) = action {
ui.add_space(14.0);
clicked = primary_button(ui, label).clicked();
}
});
clicked
}
pub fn join_or_dash(items: &[impl AsRef<str>]) -> String {
if items.is_empty() {
"—".into()
} else {
items
.iter()
.map(AsRef::as_ref)
.collect::<Vec<_>>()
.join("\n")
}
}