From a262ee5c9e59cfe07510f3b0f2546c0a4901b844 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 10 Oct 2026 02:32:52 +0700 Subject: [PATCH] Fix #807: All Tools shows every tool with its icon, under headings The toolbar's All Tools drawer listed the tools' names as plain buttons. It now shows each tool with its icon and name in a grid, under the toolbar's group headings (Select, Shapes, Draw, Modify, Type, Navigate, Color) and headings for the tools only the Advanced toolbar shows (Perspective, Symbols, Graph, Artboards, Slices). The active tool is highlighted, a tool's tooltip gives its name and shortcut, and picking one selects it and closes the drawer. The headings use labels translated already. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs/all_tools.rs | 116 +++++++++++++++++++++--- crates/ui-egui/src/toolbar.rs | 2 +- 2 files changed, 102 insertions(+), 16 deletions(-) diff --git a/crates/ui-egui/src/dialogs/all_tools.rs b/crates/ui-egui/src/dialogs/all_tools.rs index a40c777b..402e6f44 100644 --- a/crates/ui-egui/src/dialogs/all_tools.rs +++ b/crates/ui-egui/src/dialogs/all_tools.rs @@ -1,24 +1,110 @@ -//! The All Tools drawer: every tool as a button; picking one selects it and closes the drawer. +//! The All Tools drawer: every tool with its icon and name, under the headings of the toolbar's +//! groups (Select, Shapes, Draw, Modify, Type, Navigate, Color) and of the tools the Basic toolbar +//! leaves out (#807). The active tool is highlighted; picking one selects it and closes the drawer. + +use egui::{Sense, vec2}; +use vectorcraft_tools::{TOOL_GROUPS, ToolInfo}; use super::DialogSpec; -use crate::VectorcraftApp; use crate::state::Dialog; +use crate::theme::Tokens; +use crate::{VectorcraftApp, icons, widgets}; -pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("All Tools").into(), body, ok: None, ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("All Tools").into(), body, ok: None, min_width: 560.0, ..DialogSpec::FORM }; + +/// A tool's cell: its icon and name. +const CELL: egui::Vec2 = vec2(176.0, 28.0); +const ICON: f32 = 20.0; + +/// The heading a tool is listed under: its Basic toolbar group, else one for the tools only the +/// Advanced toolbar shows. +fn heading(id: &str) -> &'static str { + if let Some((cat, _)) = crate::toolbar::BASIC.iter().find(|(_, slots)| slots.iter().any(|s| s.contains(&id))) { + return cat; + } + match id { + _ if id.starts_with("symbol") => "Symbols", + _ if id.ends_with("Graph") => "Graph", + _ if id.starts_with("perspective") => "Perspective", + "artboard" => "Artboards", + "slice" | "sliceSelection" => "Slices", + _ => "More", + } +} + +/// The headings in order, each with its tools in the toolbar's order. +fn sections() -> Vec<(&'static str, Vec<&'static ToolInfo>)> { + let mut out: Vec<(&'static str, Vec<&'static ToolInfo>)> = crate::toolbar::BASIC.iter().map(|(cat, _)| (*cat, vec![])).collect(); + for tool in TOOL_GROUPS.iter().flat_map(|g| g.iter()) { + let h = heading(tool.id); + match out.iter_mut().find(|(c, _)| *c == h) { + Some((_, tools)) => tools.push(tool), + None => out.push((h, vec![tool])), + } + } + out.retain(|(_, tools)| !tools.is_empty()); + out +} fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, _: &mut Dialog) -> bool { - let mut picked = false; - for g in vectorcraft_tools::TOOL_GROUPS { - ui.horizontal_wrapped(|ui| { - for tool in g.iter() { - let shown = tl!(tool.label); - let shown = if crate::i18n::current() == crate::i18n::Lang::EN { shown.trim_end_matches(" Tool") } else { shown }; - if ui.button(shown).clicked() { - app.select_tool(tool.id); - picked = true; + let t = Tokens::get(ui.ctx()); + let active = app.session.tool_id(); + let mut picked = None; + let room = (ui.ctx().content_rect().height() - 180.0).max(200.0); + egui::ScrollArea::vertical().max_height(room).auto_shrink([false, true]).show(ui, |ui| { + for (cat, tools) in sections() { + widgets::subheader(ui, tl!(cat)); + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing = vec2(4.0, 2.0); + for tool in tools { + let (rect, resp) = ui.allocate_exact_size(CELL, Sense::click()); + let on = tool.id == active; + if on { + ui.painter().rect_filled(rect, 3.0, t.selection); + } else if resp.hovered() { + ui.painter().rect_filled(rect, 3.0, t.hover); + } + let icon = egui::Rect::from_center_size(rect.left_center() + vec2(6.0 + ICON / 2.0, 0.0), vec2(ICON, ICON)); + icons::paint(ui, icons::tool_icon(tool.icon), icon, if on { t.text_strong } else { t.icon }); + let name = tl!(tool.label); + let name = if crate::i18n::current() == crate::i18n::Lang::EN { name.trim_end_matches(" Tool") } else { name }; + let text_at = egui::pos2(icon.right() + 8.0, rect.center().y); + ui.painter().with_clip_rect(rect).text( + text_at, + egui::Align2::LEFT_CENTER, + name, + egui::FontId::proportional(12.0), + if on { t.text_strong } else { t.text }, + ); + if resp.on_hover_text(crate::toolbar::tip(tool)).clicked() { + picked = Some(tool.id); + } } - } - }); + }); + ui.add_space(6.0); + } + }); + if let Some(id) = picked { + app.select_tool(id); + } + picked.is_some() +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Every tool is listed once, under a heading; the Basic toolbar's groups come first, in its order. + #[test] + fn every_tool_is_listed_once_under_a_heading() { + let s = sections(); + let listed: Vec<&str> = s.iter().flat_map(|(_, tools)| tools.iter().map(|t| t.id)).collect(); + let all: Vec<&str> = TOOL_GROUPS.iter().flat_map(|g| g.iter().map(|t| t.id)).collect(); + assert_eq!(listed.len(), all.len()); + assert!(all.iter().all(|id| listed.contains(id))); + let heads: Vec<&str> = s.iter().map(|(c, _)| *c).collect(); + assert_eq!(&heads[..3], ["Select", "Shapes", "Draw"]); + assert!(heads.contains(&"Symbols") && heads.contains(&"Graph"), "{heads:?}"); + assert!(!heads.contains(&"More"), "every tool has a named heading: {heads:?}"); } - picked } diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index c543618e..e8cfe8c3 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -62,7 +62,7 @@ pub const BASIC: &[(&str, &[&[&str]])] = &[ ("Color", &[&["gradient", "mesh"], &["eyedropper", "measure"]]), ]; -fn tip(t: &ToolInfo) -> String { +pub(crate) fn tip(t: &ToolInfo) -> String { match crate::shortcut_editor::tool_shortcut(t.id) { Some(s) => format!("{} ({})", tl!(t.label), s), None => tl!(t.label).to_string(),