use crate::ext::HasDesignTokens;
use crate::icons::{Icon, icons as embedded_icons};
use crate::styles::typography;
use crate::theming;
use crate::tokens::DESIGN_TOKENS;
use egui::{Pos2, Rect, Sense, Ui, Vec2};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WidgetBarSection {
Top,
Bottom,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum RightBarIcon {
Alerts,
ObjectTree,
Help,
}
impl RightBarIcon {
pub fn icon(&self) -> &'static Icon {
match self {
RightBarIcon::Alerts => &embedded_icons::WIDGET_BAR_ALERTS,
RightBarIcon::ObjectTree => &embedded_icons::OBJECT_TREE,
RightBarIcon::Help => &embedded_icons::WIDGET_BAR_HELP,
}
}
pub fn tooltip(&self) -> &'static str {
match self {
RightBarIcon::Alerts => "Alerts",
RightBarIcon::ObjectTree => "Object Tree",
RightBarIcon::Help => "Help",
}
}
pub fn section(&self) -> WidgetBarSection {
match self {
RightBarIcon::Alerts | RightBarIcon::ObjectTree => WidgetBarSection::Top,
RightBarIcon::Help => WidgetBarSection::Bottom,
}
}
}
#[derive(Clone, Debug)]
pub struct WidgetBarConfig {
pub width: f32,
pub btn_size: f32,
pub icon_size: f32,
pub btn_spacing: f32,
pub hover_margin: f32,
pub hover_rounding: f32,
pub top_padding: f32,
}
impl Default for WidgetBarConfig {
fn default() -> Self {
Self {
width: DESIGN_TOKENS.sizing.toolbar.right_sidebar_width, btn_size: DESIGN_TOKENS.sizing.toolbar.right_btn_size, icon_size: DESIGN_TOKENS.sizing.toolbar.right_icon_size, btn_spacing: DESIGN_TOKENS.sizing.toolbar.right_btn_spacing, hover_margin: DESIGN_TOKENS.sizing.toolbar.right_hover_margin, hover_rounding: DESIGN_TOKENS.sizing.toolbar.right_hover_rounding, top_padding: DESIGN_TOKENS.spacing.md,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum WidgetBarAction {
None,
Clicked(RightBarIcon),
}
pub struct WidgetBar {
pub config: WidgetBarConfig,
pub icons: Vec<RightBarIcon>,
pub active_icon: Option<RightBarIcon>,
pub notification_counts: std::collections::HashMap<RightBarIcon, u32>,
}
impl Default for WidgetBar {
fn default() -> Self {
Self {
config: WidgetBarConfig::default(),
icons: vec![
RightBarIcon::Alerts,
RightBarIcon::ObjectTree,
RightBarIcon::Help,
],
active_icon: None,
notification_counts: std::collections::HashMap::new(),
}
}
}
impl WidgetBar {
pub fn new() -> Self {
Self::default()
}
pub fn with_config(mut self, config: WidgetBarConfig) -> Self {
self.config = config;
self
}
pub fn set_notification_cnt(&mut self, icon: RightBarIcon, count: u32) {
self.notification_counts.insert(icon, count);
}
pub fn show(&mut self, ui: &mut Ui) -> WidgetBarAction {
let mut action = WidgetBarAction::None;
let bg_color = ui.style().visuals.panel_fill;
let icon_color = ui.icon_color();
let icon_hover_color = ui.icon_hover();
let active_color = ui.accent_color();
let hover_bg = ui.hover_bg();
let pressed_bg = ui.active_bg();
let error_color = ui.style().visuals.error_fg_color;
let separator_color = ui.style().visuals.widgets.noninteractive.bg_fill;
let available_height = ui.available_height();
let (response, painter) = ui.allocate_painter(
Vec2::new(self.config.width, available_height),
Sense::hover(),
);
let rect = response.rect;
if ui.is_rect_visible(rect) {
painter.rect_filled(rect, DESIGN_TOKENS.rounding.panel, bg_color);
}
let mut y = rect.min.y + self.config.top_padding;
let center_x = rect.center().x;
let top_icons: Vec<_> = self
.icons
.iter()
.filter(|i| i.section() == WidgetBarSection::Top)
.copied()
.collect();
let bottom_icons: Vec<_> = self
.icons
.iter()
.filter(|i| i.section() == WidgetBarSection::Bottom)
.copied()
.collect();
let bottom_section_height = (bottom_icons.len() as f32)
* (self.config.btn_size + self.config.btn_spacing)
+ DESIGN_TOKENS.spacing.lg;
for icon in &top_icons {
y = self.render_icon_button(
ui,
&painter,
icon,
center_x,
y,
&mut action,
icon_color,
icon_hover_color,
active_color,
hover_bg,
pressed_bg,
error_color,
);
}
let remaining_space = rect.max.y - y - bottom_section_height;
if remaining_space > 0.0 {
y += remaining_space;
}
let sep_margin = DESIGN_TOKENS.sizing.toolbar.separator_margin;
let sep_width = self.config.width - sep_margin * 2.0;
let sep_height = DESIGN_TOKENS.sizing.toolbar.separator_width;
let sep_rect = Rect::from_min_size(
Pos2::new(rect.min.x + sep_margin, y),
Vec2::new(sep_width, sep_height),
);
if ui.is_rect_visible(sep_rect) {
painter.rect_filled(sep_rect, DESIGN_TOKENS.rounding.none, separator_color);
}
y += sep_height + DESIGN_TOKENS.spacing.md;
for icon in &bottom_icons {
y = self.render_icon_button(
ui,
&painter,
icon,
center_x,
y,
&mut action,
icon_color,
icon_hover_color,
active_color,
hover_bg,
pressed_bg,
error_color,
);
}
action
}
fn render_icon_button(
&mut self,
ui: &mut Ui,
painter: &egui::Painter,
icon: &RightBarIcon,
center_x: f32,
y: f32,
action: &mut WidgetBarAction,
icon_color: egui::Color32,
icon_hover_color: egui::Color32,
active_color: egui::Color32,
hover_bg: egui::Color32,
pressed_bg: egui::Color32,
error_color: egui::Color32,
) -> f32 {
let btn_rect = Rect::from_center_size(
Pos2::new(center_x, y + self.config.btn_size / 2.0),
Vec2::splat(self.config.btn_size),
);
let hover_rect = Rect::from_min_max(
Pos2::new(
btn_rect.min.x + self.config.hover_margin,
btn_rect.min.y + self.config.hover_margin,
),
Pos2::new(
btn_rect.max.x - self.config.hover_margin,
btn_rect.max.y - self.config.hover_margin,
),
);
let btn_res = ui.allocate_rect(btn_rect, Sense::click());
let is_hovered = btn_res.hovered();
let is_pressed = btn_res.is_pointer_button_down_on();
let is_active = self.active_icon == Some(*icon);
if ui.is_rect_visible(hover_rect) {
if is_active {
painter.rect_filled(hover_rect, self.config.hover_rounding, hover_bg);
} else if is_pressed {
painter.rect_filled(hover_rect, self.config.hover_rounding, pressed_bg);
} else if is_hovered {
painter.rect_filled(hover_rect, self.config.hover_rounding, hover_bg);
}
}
let curr_icon_color = if is_active {
active_color
} else if is_pressed || is_hovered {
icon_hover_color
} else {
icon_color
};
let icon_rect =
Rect::from_center_size(btn_rect.center(), Vec2::splat(self.config.icon_size));
if ui.is_rect_visible(icon_rect) {
icon.icon()
.as_image_tinted(Vec2::splat(self.config.icon_size), curr_icon_color)
.paint_at(ui, icon_rect);
}
if let Some(&count) = self.notification_counts.get(icon)
&& count > 0
{
let badge_radius = DESIGN_TOKENS.spacing.md;
let badge_pos = Pos2::new(btn_rect.max.x - badge_radius, btn_rect.min.y + badge_radius);
let badge_rect = Rect::from_center_size(badge_pos, Vec2::splat(badge_radius * 2.0));
if ui.is_rect_visible(badge_rect) {
painter.circle_filled(badge_pos, badge_radius, error_color);
let cnt_text = if count > 9 {
"9+".to_string()
} else {
count.to_string()
};
let badge_galley = painter.layout_no_wrap(
cnt_text,
egui::FontId::proportional(typography::XS),
theming::badge_text_color(ui),
);
painter.galley(
Pos2::new(
badge_pos.x - badge_galley.size().x / 2.0,
badge_pos.y - badge_galley.size().y / 2.0,
),
badge_galley,
egui::Color32::TRANSPARENT,
);
}
}
if btn_res.clicked() {
self.active_icon = if is_active { None } else { Some(*icon) };
*action = WidgetBarAction::Clicked(*icon);
}
if is_hovered {
btn_res.on_hover_text(icon.tooltip());
}
y + self.config.btn_size + self.config.btn_spacing
}
}