use super::action::ActionInfo;
use super::icons::{ICON_SIZE, IconCache, icon_render_size};
use egui::{Color32, Pos2, Rect, Ui, Vec2};
const BUTTON_SIZE: f32 = 28.0;
#[allow(dead_code)]
const BUTTON_PADDING: f32 = 4.0;
const BUTTON_SPACING: f32 = 2.0;
const BAR_PADDING: f32 = 6.0;
const BAR_CORNER_RADIUS: f32 = 6.0;
const SEPARATOR_WIDTH: f32 = 1.0;
const SEPARATOR_MARGIN: f32 = 6.0;
const DRAG_HANDLE_WIDTH: f32 = 16.0;
#[allow(dead_code)]
const ROW_GAP: f32 = 4.0;
const TOOLBAR_MARGIN: f32 = 8.0;
const BAR_BG_COLOR: Color32 = Color32::from_rgba_premultiplied(35, 35, 35, 245);
const BUTTON_DEFAULT_COLOR: Color32 = Color32::TRANSPARENT;
const BUTTON_HOVER_COLOR: Color32 = Color32::from_rgba_premultiplied(60, 60, 60, 255);
const BUTTON_ACTIVE_COLOR: Color32 = Color32::from_rgb(0, 120, 215);
const SEPARATOR_COLOR: Color32 = Color32::from_gray(60);
const ICON_COLOR: Color32 = Color32::WHITE;
const ICON_DISABLED_COLOR: Color32 = Color32::from_gray(100);
pub const DRAWING_TOOLS: &[&str] = &[
"rectangle",
"ellipse",
"polyline",
"arrow",
"annotate",
"highlighter",
"mosaic",
"text",
"sequence",
];
pub const EDIT_TOOLS: &[&str] = &["undo", "redo"];
pub const ACTION_TOOLS: &[&str] = &["cancel", "save", "copy"];
#[derive(Clone)]
pub struct ToolButton {
pub id: String,
pub tooltip: String,
pub is_toggle: bool,
pub bounds: Rect,
}
impl ToolButton {
pub fn new(id: &str, tooltip: &str, is_toggle: bool) -> Self {
Self { id: id.to_string(), tooltip: tooltip.to_string(), is_toggle, bounds: Rect::NOTHING }
}
}
pub struct ActionBar {
position: Pos2,
drag_handle_bounds: Rect,
drawing_buttons: Vec<ToolButton>,
edit_buttons: Vec<ToolButton>,
action_buttons: Vec<ToolButton>,
main_bar_size: Vec2,
#[allow(dead_code)]
options_panel_visible: bool,
icon_cache: IconCache,
is_dragging: bool,
drag_offset: Vec2,
hovered_button: Option<String>,
scale_factor: f32,
}
impl ActionBar {
pub fn new(
actions: &[ActionInfo],
selection_bounds: (Pos2, Pos2),
screen_size: Vec2,
scale_factor: f32,
) -> Self {
let mut drawing_buttons: Vec<ToolButton> = DRAWING_TOOLS
.iter()
.filter(|id| actions.iter().any(|a| a.id == **id))
.map(|id| {
let tooltip = actions
.iter()
.find(|a| a.id == *id)
.map(|a| a.name.clone())
.unwrap_or_default();
ToolButton::new(id, &tooltip, true)
})
.collect();
let edit_buttons: Vec<ToolButton> =
EDIT_TOOLS.iter().map(|id| ToolButton::new(id, id, false)).collect();
let mut action_buttons: Vec<ToolButton> = ACTION_TOOLS
.iter()
.filter(|id| actions.iter().any(|a| a.id == **id))
.map(|id| {
let tooltip = actions
.iter()
.find(|a| a.id == *id)
.map(|a| a.name.clone())
.unwrap_or_default();
ToolButton::new(id, &tooltip, false)
})
.collect();
for action in actions {
let id = &action.id;
if !DRAWING_TOOLS.contains(&id.as_str())
&& !EDIT_TOOLS.contains(&id.as_str())
&& !ACTION_TOOLS.contains(&id.as_str())
{
let is_toggle = matches!(
action.category,
super::action::ToolCategory::Drawing | super::action::ToolCategory::Privacy
);
if is_toggle {
drawing_buttons.push(ToolButton::new(id, &action.name, true));
} else {
action_buttons.push(ToolButton::new(id, &action.name, false));
}
}
}
let drawing_width = drawing_buttons.len() as f32 * BUTTON_SIZE
+ (drawing_buttons.len().saturating_sub(1)) as f32 * BUTTON_SPACING;
let edit_width = edit_buttons.len() as f32 * BUTTON_SIZE
+ (edit_buttons.len().saturating_sub(1)) as f32 * BUTTON_SPACING;
let action_width = action_buttons.len() as f32 * BUTTON_SIZE
+ (action_buttons.len().saturating_sub(1)) as f32 * BUTTON_SPACING;
let separator_count = 2; let separators_width = separator_count as f32 * (SEPARATOR_WIDTH + 2.0 * SEPARATOR_MARGIN);
let total_width = DRAG_HANDLE_WIDTH
+ BAR_PADDING
+ drawing_width
+ separators_width
+ edit_width
+ action_width
+ BAR_PADDING;
let bar_height = BUTTON_SIZE + 2.0 * BAR_PADDING;
let main_bar_size = Vec2::new(total_width, bar_height);
let (_min_pos, max_pos) = selection_bounds;
let mut toolbar_x = max_pos.x - total_width;
let mut toolbar_y = max_pos.y + TOOLBAR_MARGIN;
toolbar_x = toolbar_x.max(TOOLBAR_MARGIN);
if toolbar_x + total_width > screen_size.x - TOOLBAR_MARGIN {
toolbar_x = screen_size.x - total_width - TOOLBAR_MARGIN;
}
if toolbar_y + bar_height > screen_size.y - TOOLBAR_MARGIN {
toolbar_y = screen_size.y - bar_height - TOOLBAR_MARGIN;
}
toolbar_y = toolbar_y.max(TOOLBAR_MARGIN);
log::debug!(
"ActionBar: selection_max=({:.0},{:.0}), screen=({:.0},{:.0}), pos=({:.0},{:.0})",
max_pos.x,
max_pos.y,
screen_size.x,
screen_size.y,
toolbar_x,
toolbar_y
);
let position = Pos2::new(toolbar_x, toolbar_y);
let mut bar = Self {
position,
drag_handle_bounds: Rect::NOTHING,
drawing_buttons,
edit_buttons,
action_buttons,
main_bar_size,
options_panel_visible: false,
icon_cache: IconCache::new(),
is_dragging: false,
drag_offset: Vec2::ZERO,
hovered_button: None,
scale_factor,
};
bar.update_button_bounds();
bar
}
fn update_button_bounds(&mut self) {
let mut x = self.position.x + DRAG_HANDLE_WIDTH + BAR_PADDING;
let y = self.position.y + BAR_PADDING;
self.drag_handle_bounds = Rect::from_min_size(
Pos2::new(self.position.x, self.position.y),
Vec2::new(DRAG_HANDLE_WIDTH, self.main_bar_size.y),
);
for button in &mut self.drawing_buttons {
button.bounds = Rect::from_min_size(Pos2::new(x, y), Vec2::splat(BUTTON_SIZE));
x += BUTTON_SIZE + BUTTON_SPACING;
}
x += SEPARATOR_MARGIN;
x += SEPARATOR_WIDTH;
x += SEPARATOR_MARGIN;
for button in &mut self.edit_buttons {
button.bounds = Rect::from_min_size(Pos2::new(x, y), Vec2::splat(BUTTON_SIZE));
x += BUTTON_SIZE + BUTTON_SPACING;
}
x += SEPARATOR_MARGIN;
x += SEPARATOR_WIDTH;
x += SEPARATOR_MARGIN;
for button in &mut self.action_buttons {
button.bounds = Rect::from_min_size(Pos2::new(x, y), Vec2::splat(BUTTON_SIZE));
x += BUTTON_SIZE + BUTTON_SPACING;
}
}
pub fn bounds(&self) -> Rect {
Rect::from_min_size(self.position, self.main_bar_size)
}
pub fn contains(&self, pos: Pos2) -> bool {
self.bounds().contains(pos)
}
pub fn is_in_drag_handle(&self, pos: Pos2) -> bool {
self.drag_handle_bounds.contains(pos)
}
pub fn start_drag(&mut self, mouse_pos: Pos2) {
self.is_dragging = true;
self.drag_offset = mouse_pos - self.position;
}
pub fn update_drag(&mut self, mouse_pos: Pos2, screen_size: Vec2) {
if self.is_dragging {
let mut new_pos = mouse_pos - self.drag_offset;
let min_x = 0.0;
let min_y = 0.0;
let max_x = (screen_size.x - self.main_bar_size.x).max(0.0);
let max_y = (screen_size.y - self.main_bar_size.y).max(0.0);
new_pos.x = new_pos.x.clamp(min_x, max_x);
new_pos.y = new_pos.y.clamp(min_y, max_y);
self.position = new_pos;
self.update_button_bounds();
}
}
pub fn stop_drag(&mut self) {
self.is_dragging = false;
}
pub fn is_dragging(&self) -> bool {
self.is_dragging
}
pub fn update_hover(&mut self, pos: Pos2) {
self.hovered_button = None;
for button in self
.drawing_buttons
.iter()
.chain(self.edit_buttons.iter())
.chain(self.action_buttons.iter())
{
if button.bounds.contains(pos) {
self.hovered_button = Some(button.id.clone());
break;
}
}
}
pub fn check_click(&self, pos: Pos2) -> Option<&str> {
for button in self
.drawing_buttons
.iter()
.chain(self.edit_buttons.iter())
.chain(self.action_buttons.iter())
{
if button.bounds.contains(pos) {
return Some(&button.id);
}
}
None
}
pub fn hovered_button(&self) -> Option<&str> {
self.hovered_button.as_deref()
}
pub fn render(
&mut self,
ui: &mut Ui,
active_tool: Option<&str>,
undo_enabled: bool,
redo_enabled: bool,
) {
let main_rect = self.bounds();
ui.painter().rect_filled(main_rect, BAR_CORNER_RADIUS, BAR_BG_COLOR);
Self::render_drag_handle_static(ui, self.drag_handle_bounds);
let drawing_render_info: Vec<_> = self
.drawing_buttons
.iter()
.map(|button| {
let is_active = active_tool == Some(button.id.as_str());
let is_hovered = self.hovered_button.as_deref() == Some(button.id.as_str());
(button.id.clone(), button.bounds, is_active, is_hovered, true)
})
.collect();
let edit_render_info: Vec<_> = self
.edit_buttons
.iter()
.map(|button| {
let enabled = if button.id == "undo" {
undo_enabled
} else if button.id == "redo" {
redo_enabled
} else {
true
};
let is_hovered = self.hovered_button.as_deref() == Some(button.id.as_str());
(button.id.clone(), button.bounds, false, is_hovered && enabled, enabled)
})
.collect();
let action_render_info: Vec<_> = self
.action_buttons
.iter()
.map(|button| {
let is_hovered = self.hovered_button.as_deref() == Some(button.id.as_str());
(button.id.clone(), button.bounds, false, is_hovered, true)
})
.collect();
for (id, bounds, is_active, is_hovered, enabled) in &drawing_render_info {
self.render_button_by_info(ui, id, *bounds, *is_active, *is_hovered, *enabled);
}
let sep1_x = self
.drawing_buttons
.last()
.map(|b| b.bounds.max.x + SEPARATOR_MARGIN)
.unwrap_or(self.position.x);
Self::render_separator_static(ui, sep1_x, self.position.y, self.main_bar_size.y);
for (id, bounds, is_active, is_hovered, enabled) in &edit_render_info {
self.render_button_by_info(ui, id, *bounds, *is_active, *is_hovered, *enabled);
}
let sep2_x =
self.edit_buttons.last().map(|b| b.bounds.max.x + SEPARATOR_MARGIN).unwrap_or(sep1_x);
Self::render_separator_static(ui, sep2_x, self.position.y, self.main_bar_size.y);
for (id, bounds, is_active, is_hovered, enabled) in &action_render_info {
self.render_button_by_info(ui, id, *bounds, *is_active, *is_hovered, *enabled);
}
if let Some(ref hovered_id) = self.hovered_button.clone() {
if let Some(button) = self.find_button(hovered_id) {
Self::render_tooltip_static(ui, button.bounds, &button.tooltip, &button.id);
}
}
}
fn find_button(&self, id: &str) -> Option<&ToolButton> {
self.drawing_buttons
.iter()
.chain(self.edit_buttons.iter())
.chain(self.action_buttons.iter())
.find(|b| b.id == id)
}
fn render_drag_handle_static(ui: &mut Ui, rect: Rect) {
let center = rect.center();
let dot_radius = 1.5;
let dot_spacing_x = 4.0;
let dot_spacing_y = 5.0;
let dot_color = Color32::from_gray(120);
for col in 0..2 {
for row in 0..3 {
let x = center.x + (col as f32 - 0.5) * dot_spacing_x;
let y = center.y + (row as f32 - 1.0) * dot_spacing_y;
ui.painter().circle_filled(Pos2::new(x, y), dot_radius, dot_color);
}
}
}
fn render_separator_static(ui: &mut Ui, x: f32, position_y: f32, bar_height: f32) {
let top = position_y + BAR_PADDING + 4.0;
let bottom = position_y + bar_height - BAR_PADDING - 4.0;
ui.painter().line_segment(
[Pos2::new(x, top), Pos2::new(x, bottom)],
egui::Stroke::new(SEPARATOR_WIDTH, SEPARATOR_COLOR),
);
}
fn render_button_by_info(
&mut self,
ui: &mut Ui,
id: &str,
bounds: Rect,
is_active: bool,
is_hovered: bool,
enabled: bool,
) {
let bg_color = if is_active {
BUTTON_ACTIVE_COLOR
} else if is_hovered {
BUTTON_HOVER_COLOR
} else {
BUTTON_DEFAULT_COLOR
};
if bg_color != BUTTON_DEFAULT_COLOR {
ui.painter().rect_filled(bounds, 4.0, bg_color);
}
let icon_color = if enabled { ICON_COLOR } else { ICON_DISABLED_COLOR };
let render_size = icon_render_size(self.scale_factor);
if let Some(texture) = self.icon_cache.get_or_create(ui.ctx(), id, render_size, icon_color)
{
let display_size = Vec2::splat(ICON_SIZE as f32);
let icon_pos = bounds.center() - display_size / 2.0;
let icon_rect = Rect::from_min_size(icon_pos, display_size);
ui.painter().image(
texture.id(),
icon_rect,
Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)),
Color32::WHITE,
);
} else {
ui.painter().text(
bounds.center(),
egui::Align2::CENTER_CENTER,
id[..1].to_uppercase(),
egui::FontId::proportional(14.0),
icon_color,
);
}
}
fn render_tooltip_static(ui: &mut Ui, bounds: Rect, tooltip: &str, id: &str) {
let tooltip_text = if tooltip.is_empty() { id } else { tooltip };
let tooltip_pos = Pos2::new(bounds.center().x, bounds.max.y + 4.0);
let font = egui::FontId::proportional(12.0);
let galley =
ui.painter().layout_no_wrap(tooltip_text.to_string(), font.clone(), Color32::WHITE);
let text_size = galley.size();
let padding = Vec2::new(6.0, 3.0);
let bg_rect = Rect::from_min_size(
Pos2::new(tooltip_pos.x - text_size.x / 2.0 - padding.x, tooltip_pos.y),
text_size + padding * 2.0,
);
ui.painter().rect_filled(bg_rect, 4.0, Color32::from_rgba_premultiplied(20, 20, 20, 230));
ui.painter().text(
bg_rect.center(),
egui::Align2::CENTER_CENTER,
tooltip_text,
font,
Color32::WHITE,
);
}
pub fn position(&self) -> Pos2 {
self.position
}
pub fn size(&self) -> Vec2 {
self.main_bar_size
}
}