From 3ec7ce8aae949edd3d553fdbe24a237a14600512 Mon Sep 17 00:00:00 2001 From: alehel Date: Thu, 15 Oct 2026 10:41:14 +0200 Subject: [PATCH 1/7] M3: All tools expands to a full, searchable view An expand button at the top-left of the All tools sidebar opens a view that fills the workspace, like Acrobat's tools page: every tool as a tile, filed under five categories (Create & Edit, Review & Compare, Forms & Signatures, Protect & Standardize, Customize & Automate), with a search field that matches tool names and the items inside each tool, in English or the UI language. Choosing a tile opens that tool's panel; Collapse, Esc or another workspace puts the view away. `--tools full` opens it from the command line and the control channel. The categories live in the engine catalogue (TOOL_CATEGORIES), with a test that every tool group is in exactly one. Six new strings are translated in all 22 catalogues. Co-Authored-By: Claude Opus 5.5 --- ATTRIBUTION.toml | 44 ++++---- crates/engine/src/catalog.rs | 29 +++++ crates/ui-egui/src/all_tools_ui.rs | 158 ++++++++++++++++++++++++++++ crates/ui-egui/src/home.rs | 2 +- crates/ui-egui/src/i18n/ar.tsv | 6 ++ crates/ui-egui/src/i18n/bg.tsv | 6 ++ crates/ui-egui/src/i18n/cs.tsv | 6 ++ crates/ui-egui/src/i18n/de.tsv | 6 ++ crates/ui-egui/src/i18n/el.tsv | 6 ++ crates/ui-egui/src/i18n/es.tsv | 6 ++ crates/ui-egui/src/i18n/fr.tsv | 6 ++ crates/ui-egui/src/i18n/hu.tsv | 6 ++ crates/ui-egui/src/i18n/it.tsv | 6 ++ crates/ui-egui/src/i18n/ja.tsv | 6 ++ crates/ui-egui/src/i18n/kk.tsv | 6 ++ crates/ui-egui/src/i18n/lt.tsv | 6 ++ crates/ui-egui/src/i18n/mod.rs | 3 + crates/ui-egui/src/i18n/pl.tsv | 6 ++ crates/ui-egui/src/i18n/pt-br.tsv | 6 ++ crates/ui-egui/src/i18n/ru.tsv | 6 ++ crates/ui-egui/src/i18n/sl.tsv | 6 ++ crates/ui-egui/src/i18n/te.tsv | 6 ++ crates/ui-egui/src/i18n/tr.tsv | 6 ++ crates/ui-egui/src/i18n/uk.tsv | 6 ++ crates/ui-egui/src/i18n/vi.tsv | 6 ++ crates/ui-egui/src/i18n/zh-hans.tsv | 6 ++ crates/ui-egui/src/i18n/zh-hant.tsv | 6 ++ crates/ui-egui/src/lib.rs | 21 +++- crates/ui-egui/src/panels.rs | 10 +- crates/ui-egui/tests/ui.rs | 44 ++++++++ 30 files changed, 417 insertions(+), 26 deletions(-) create mode 100644 crates/ui-egui/src/all_tools_ui.rs diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 3f3791f719..180bbd6ba8 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "cefa955aa92790d6773cb1d1e15e19019b3165a00528d57d6617362e1d1988ab" +sha256 = "d779dbb265c576d68cd804d0a0505ac5821d74f2d4e5afb663516d8801e0fbbc" # ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences. # ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`). @@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "1de29a6cc089be31cb6206bf41e0715f8ddcf74ef887b45ccee235d743bed223" +sha256 = "69a7a36b9b8fbe6cd04d7f287a77deb01c243cc0f1a438511cf35748dde6b3df" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "8c6d5b1505d76bc7ba606c98660afa60b1152c5b4c2e7d345104a6ef3d280ed2" +sha256 = "12a568bf7a626ff3e34009150894eabe3c0927589756d8635fc9bae19638a512" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" @@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "8a8aed18bd8fceabd1389580361ba75b0dccf141d845650a353d8dfe19c95536" +sha256 = "0fd03fafffc2d75bf484186a7dd6ab15ab95d79448e0e798f57519515d73ac4e" [[asset]] path = "crates/ui-egui/src/i18n/pt-br.tsv" @@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "1c197ac045810950aceb733152a296f2224f38857b1169032fdc7092ab4aa3f7" +sha256 = "d3292e186768f75b9cb87097688ff4909c26c9810a7e6542c127aad9fbce5af3" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" @@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "a9cf0e062b006b40f549ce2316b88d4b0196d70bd9cecd66c12d980d699bf679" +sha256 = "14c9f2ba61ffc925b01e6bf31c7f7761fb5e7adcda1751f6768829341a8e3757" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" @@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "ef5c84bd13ef8318f7deab7f42136cfe3c14cbef6fd5231014b94167baaecd2a" +sha256 = "d2dc6bd5de197eff60a440a05c58c6b7307009ecb08ddf8fbb31ff616633e36e" [[asset]] path = "crates/ui-egui/src/i18n/de.tsv" @@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "be689af8bb22537879269ad9ab9bdcc4b730665cf0b27c6bf4bad560cacf3b69" +sha256 = "b6ffc5eda71ea13eaea79f5f3368811cb317f0a43e6f6be167bb4742d8e1ae77" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" @@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "989d2e5f57cf22c21a85951835ca9fbd05e314da67eb4c17332df1f03679fa00" +sha256 = "37b60dda614ebc0e2b141cb27c9b2b7d13b49b347c1a5acb4d395147fbbdd246" [[asset]] path = "crates/ui-egui/src/i18n/it.tsv" @@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "425c42acb8d98ac31b216b13c05a285e0676318dd379dcfb9d57cc7329e9d7a5" +sha256 = "0055dfb04a35abd2c8da01bf3836855074e17bbd2b6332b2d1cecd16fd816938" [[asset]] path = "crates/ui-egui/src/i18n/pl.tsv" @@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "988bcf2e0322a46c0464ff7b91ab5d90193b0ea7f454812d12595eb843de6fc7" +sha256 = "e1c4801f9633f5873f5dc07fa6de01375369b636282fa22e8cd223adb9737b42" [[asset]] path = "crates/ui-egui/src/i18n/ru.tsv" @@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "ebe7aa9152baff38ae7e36aeb4f6a7ed2198166ab93425a612a66d53e43cb363" +sha256 = "36bd183e5bb36e76e2eb40ac68d06f2d0dcd962b42998917ba3ad5e7d8ca2b19" [[asset]] path = "crates/ui-egui/src/i18n/hu.tsv" @@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "69d2da21949f02c3c40f71159e377eef2fdd1a87f749d6c504e1d151ef33f643" +sha256 = "c10dab015d7cabc571dd2a462a0ca22a1b1461b9b747c4998e7a7e4e1f9b6b71" [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" @@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "98359685aa555f391846f2259ac302ce1234c7a180fb4abe07c36e0c1c62211e" +sha256 = "7b1db4f5f3ce50c8b995037d84cce86088e3af17942b2e1d0197234a2454ecdd" [[asset]] path = "crates/ui-egui/src/i18n/kk.tsv" @@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9f6296d9c79c848167fc80bb4cfaacc9e60f72edb70cc1d70f7b6088ada83578" +sha256 = "3ef5f6e5c28c8060017b1219d0ae3fe6308eaa4f69baf821816da1ac2b4468cd" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" @@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "3528e85afd4117de2c22b9f37d32a380a073a49834fa5a3063d21b803606ffbf" +sha256 = "d2b91bcf778610861b515865339a487f279f68188617fd2729e0d8d8455d7a2b" [[asset]] path = "crates/ui-egui/src/i18n/uk.tsv" @@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "4db7e23074d44305f4584e2cd8b9a0fc35b433cb187dad493c8f7d78a8cd4ad2" +sha256 = "b3a0c1563ba9c67ffbebf805769bae9c5a809cd14fd37a0441a30e19cacb28c8" [[asset]] path = "crates/ui-egui/src/i18n/ar.tsv" @@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "2af01de9509db4b4ae61193f41ff0f7f3b70ca5f91cd2afda667e90d34980b25" +sha256 = "15dde8ef0f0d20a6e99b82c7f5297b41130e9f0d8f11494fcdc09c69bcbfb43c" [[asset]] path = "crates/ui-egui/src/i18n/tr.tsv" @@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "de988b6959572931c4878a0588109c236bf8c5d46ef57c7a406b6ab42de51339" +sha256 = "9d879f455313d528d2d8c91e080a1fa199f50d901c7691f7b6dc3f15be73fd1a" [[asset]] path = "crates/ui-egui/src/i18n/lt.tsv" @@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "63955758227bd57329b22dc7c16ed9cee8b0a15df9453477755849c886424dd5" +sha256 = "b300a148c46c122b84ee9b978979ad41c3b4fbb220a488989bb991e0062d9885" [[asset]] @@ -240,7 +240,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "b54adaa6515dbfb0de3859b30989054e5fcc4f56e66e9cc713c7bf509bdbbac3" +sha256 = "4c09041b54acbdb7c5fe16409b2fa699d260200fec8600000d668c9af7636f8c" [[asset]] path = "assets/icons/accessibility.svg" @@ -3363,7 +3363,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "5881cc7fbfe9da5f5f611309dacf267f95db3a143b916a113b649839019b292c" +sha256 = "bb3a8708b1a2f72782ab41b0ae43e902f089caffd545004f5d1b2112d20df78c" [[asset]] path = "vendor/vello_common/assets/probe.png" diff --git a/crates/engine/src/catalog.rs b/crates/engine/src/catalog.rs index b46d88acf1..7a63beef89 100644 --- a/crates/engine/src/catalog.rs +++ b/crates/engine/src/catalog.rs @@ -547,6 +547,23 @@ pub fn group(id: &str) -> Option<&'static ToolGroup> { TOOL_GROUPS.iter().find(|g| g.id == id) } +/// A heading of the expanded All tools view, as Acrobat files its tools under "Create & Edit", +/// "Forms & Signatures" and so on. Every group belongs to exactly one category. +#[derive(Clone, Copy, Debug)] +pub struct ToolCategory { + pub label: &'static str, + /// [`ToolGroup::id`]s, in display order. + pub groups: &'static [&'static str], +} + +pub static TOOL_CATEGORIES: &[ToolCategory] = &[ + ToolCategory { label: "Create & Edit", groups: &["create", "edit", "export", "combine", "organize", "scan", "compress"] }, + ToolCategory { label: "Review & Compare", groups: &["comment", "stamp", "compare", "measure"] }, + ToolCategory { label: "Forms & Signatures", groups: &["fill_sign", "form", "certificate"] }, + ToolCategory { label: "Protect & Standardize", groups: &["protect", "redact", "accessibility", "standards", "print_production"] }, + ToolCategory { label: "Customize & Automate", groups: &["actions", "javascript", "search_index", "ai"] }, +]; + #[cfg(test)] mod tests { use super::*; @@ -561,6 +578,18 @@ mod tests { } } + #[test] + fn every_group_is_in_exactly_one_category() { + let mut seen = HashSet::new(); + for c in TOOL_CATEGORIES { + for id in c.groups { + assert!(group(id).is_some(), "{} lists unknown group {id}", c.label); + assert!(seen.insert(*id), "{id} is in two categories"); + } + } + assert_eq!(seen.len(), TOOL_GROUPS.len(), "a group is in no category"); + } + #[test] fn commands_are_namespaced() { for g in TOOL_GROUPS { diff --git a/crates/ui-egui/src/all_tools_ui.rs b/crates/ui-egui/src/all_tools_ui.rs new file mode 100644 index 0000000000..e46ec6c723 --- /dev/null +++ b/crates/ui-egui/src/all_tools_ui.rs @@ -0,0 +1,158 @@ +//! The expanded All tools view: every tool, filed under Acrobat-style categories, with a search +//! field, filling the workspace instead of the sidebar. The sidebar's expand button opens it; +//! its collapse button, Esc or choosing a tool closes it again. + +use egui::{Align2, Color32, CornerRadius, Rect, Sense, Stroke, vec2}; +use pdfcraft_engine::catalog::{self, Availability, TOOL_CATEGORIES, ToolGroup}; + +use crate::theme::{self, Tokens}; +use crate::{PdfCraftApp, icons, widgets}; + +const TILE: egui::Vec2 = vec2(148.0, 118.0); +/// Item names a tile lists when the search found the tool by them. +const MATCHES_SHOWN: usize = 2; + +/// Whether `group` matches `query` (already trimmed and lower-cased), in English or the UI +/// language. `None` is no match; otherwise the items that matched, empty when the tool's own +/// name did. +fn matches(query: &str, group: &ToolGroup) -> Option> { + let hit = |label: &str| label.to_lowercase().contains(query) || tl!(label).to_lowercase().contains(query); + if query.is_empty() || hit(group.label) { + return Some(Vec::new()); + } + let items: Vec<_> = group.sections.iter().flat_map(|s| s.items.iter()).map(|i| i.label).filter(|l| hit(l)).collect(); + (!items.is_empty()).then_some(items) +} + +pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) { + let t = Tokens::get(ui.ctx()); + if ui.input(|i| i.key_pressed(egui::Key::Escape)) { + app.all_tools_full = false; + return; + } + let mut chosen = None; + egui::Frame::NONE.fill(t.panel).show(ui, |ui| { + ui.set_min_size(ui.available_size()); + egui::Frame::NONE.inner_margin(egui::Margin { left: 36, right: 28, top: 20, bottom: 12 }).show(ui, |ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new(tl!("All tools")).font(theme::semibold(22.0)).color(t.text)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if icons::button(ui, "minimize-2", 28.0, false, tl!("Collapse")).clicked() { + app.all_tools_full = false; + } + }); + }); + ui.add_space(10.0); + let field = ui.add( + widgets::line(&mut app.all_tools_query).hint_text(tl!("Search tools and commands…")).desired_width(ui.available_width().min(520.0)), + ); + // Typing goes straight to the search, as in Acrobat's tools view. + if !ui.memory(|m| m.focused().is_some()) { + field.request_focus(); + } + }); + ui.separator(); + let query = app.all_tools_query.trim().to_lowercase(); + egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| { + egui::Frame::NONE.inner_margin(egui::Margin { left: 36, right: 28, top: 8, bottom: 28 }).show(ui, |ui| { + ui.set_width(ui.available_width()); + let mut any = false; + for category in TOOL_CATEGORIES { + let found: Vec<_> = + category.groups.iter().filter_map(|id| catalog::group(id)).filter_map(|g| matches(&query, g).map(|m| (g, m))).collect(); + if found.is_empty() { + continue; + } + any = true; + ui.add_space(12.0); + ui.label(egui::RichText::new(tl!(category.label)).font(theme::semibold(16.0)).color(t.text)); + ui.add_space(10.0); + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing = vec2(14.0, 14.0); + for (g, items) in found { + if tile(ui, &t, g, &items).clicked() { + chosen = Some(g.id); + } + } + }); + ui.add_space(8.0); + } + if !any { + ui.add_space(24.0); + ui.label(egui::RichText::new(tl!("No tools match your search.")).color(t.text_muted)); + } + }); + }); + }); + if let Some(id) = chosen { + app.all_tools_full = false; + app.left_open = true; + app.select_tool(id); + } +} + +fn tile(ui: &mut egui::Ui, t: &Tokens, g: &ToolGroup, items: &[&'static str]) -> egui::Response { + let (rect, resp) = ui.allocate_exact_size(TILE, Sense::click()); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tl!(g.label))); + let press = theme::Press::track(ui, &resp); + crate::home::card_lift(ui, t, rect, press); + ui.painter().rect(rect, CornerRadius::same(10), press.fill(t.card, t.hover, t.pressed), Stroke::new(1.0, t.divider), egui::StrokeKind::Inside); + let body = rect.translate(press.offset()); + let ready = g.availability == Availability::Ready; + let hue = Color32::from_rgb(g.hue[0], g.hue[1], g.hue[2]); + icons::paint(ui, Rect::from_center_size(body.center_top() + vec2(0.0, 34.0), vec2(32.0, 32.0)), g.icon, 30.0, hue); + let mut job = + egui::text::LayoutJob::simple(tl!(g.label).to_owned(), theme::medium(13.0), if ready { t.text } else { t.text_muted }, body.width() - 16.0); + job.halign = egui::Align::Center; + job.wrap.max_rows = 2; + let title = ui.fonts_mut(|f| f.layout_job(job)); + let mut elided = title.elided; + let title_h = title.size().y; + ui.painter().galley(body.center_top() + vec2(0.0, 60.0), title, t.text); + // What found the tool, or else whether it works today. + let note = if items.is_empty() { + match g.availability { + Availability::Ready => String::new(), + Availability::Planned(m) => format!("{} · {m}", tl!("Planned")), + Availability::Provider => g.badge.map(|b| tl!(b).to_owned()).unwrap_or_default(), + } + } else { + items.iter().take(MATCHES_SHOWN).map(|l| tl!(l)).collect::>().join(" · ") + }; + if !note.is_empty() { + let color = if items.is_empty() { t.text_faint } else { t.accent_text }; + let mut job = egui::text::LayoutJob::simple_singleline(note.clone(), theme::regular(11.0), color); + job.halign = egui::Align::Center; + job.wrap = egui::text::TextWrapping { max_width: body.width() - 16.0, max_rows: 1, break_anywhere: true, ..Default::default() }; + let galley = ui.fonts_mut(|f| f.layout_job(job)); + elided |= galley.elided; + ui.painter().galley(body.center_top() + vec2(0.0, 64.0 + title_h), galley, color); + } + if let Some(b) = g.badge.filter(|_| g.availability == Availability::Ready) { + let font = theme::semibold(9.5); + let w = widgets::text_width(ui, tl!(b), &font); + let r = Rect::from_min_size(body.right_top() + vec2(-w - 18.0, 8.0), vec2(w + 10.0, 16.0)); + ui.painter().rect_filled(r, CornerRadius::same(4), t.badge_new); + ui.painter().text(r.center(), Align2::CENTER_CENTER, tl!(b), font, Color32::WHITE); + } + let resp = if elided { resp.on_hover_text(format!("{}\n{note}", tl!(g.label))) } else { resp }; + theme::hand(resp) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn group(id: &str) -> &'static ToolGroup { + catalog::group(id).expect("catalogue group") + } + + #[test] + fn search_matches_tool_names_and_their_items() { + assert_eq!(matches("", group("organize")), Some(Vec::new()), "an empty query shows everything"); + assert_eq!(matches("organize", group("organize")), Some(Vec::new()), "the tool's own name"); + let rotate = matches("rotate", group("organize")).expect("Organize pages lists Rotate"); + assert!(!rotate.is_empty() && rotate.iter().all(|l| l.to_lowercase().contains("rotate")), "{rotate:?}"); + assert_eq!(matches("no such tool anywhere", group("organize")), None); + } +} diff --git a/crates/ui-egui/src/home.rs b/crates/ui-egui/src/home.rs index 3e70f34e42..42a21900d5 100644 --- a/crates/ui-egui/src/home.rs +++ b/crates/ui-egui/src/home.rs @@ -10,7 +10,7 @@ use crate::{PdfCraftApp, icons, panels::human_size, widgets}; const RECOMMENDED: [&str; 5] = ["organize", "comment", "form", "edit", "protect"]; /// A soft shadow that rises on hover and settles when the card is pressed. -fn card_lift(ui: &egui::Ui, t: &Tokens, rect: Rect, press: theme::Press) { +pub(crate) fn card_lift(ui: &egui::Ui, t: &Tokens, rect: Rect, press: theme::Press) { let lift = press.hover * (1.0 - press.down); if lift > 0.04 { let shadow = egui::Shadow { offset: [0, 3], blur: (14.0 * lift) as u8, spread: 0, color: t.page_shadow.gamma_multiply(0.85 * lift) }; diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index 01bb1679e4..1915ee5fc6 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -2706,3 +2706,9 @@ certificate Version الإصدار Make properties default جعل الخصائص افتراضية Pages selected الصفحات المحددة Page properties خصائص الصفحة + Create & Edit الإنشاء والتحرير + Review & Compare المراجعة والمقارنة + Forms & Signatures النماذج والتوقيعات + Protect & Standardize الحماية والتوحيد القياسي + Customize & Automate التخصيص والأتمتة + No tools match your search. لا توجد أدوات تطابق بحثك. diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 8f2fea70e0..57bbffaabb 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2391,3 +2391,9 @@ animation effects Off Изключени Make properties default Задаване като свойства по подразбиране Pages selected Избрани страници Page properties Свойства на страницата + Create & Edit Създаване и редактиране + Review & Compare Преглед и сравнение + Forms & Signatures Формуляри и подписи + Protect & Standardize Защита и стандартизация + Customize & Automate Персонализиране и автоматизация + No tools match your search. Няма инструменти, отговарящи на търсенето. diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index bad5c497e8..c52ec8b20d 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2502,3 +2502,9 @@ animation effects Off Vypnuto Make properties default Nastavit vlastnosti jako výchozí Pages selected Vybrané stránky Page properties Vlastnosti stránky + Create & Edit Vytváření a úpravy + Review & Compare Revize a porovnání + Forms & Signatures Formuláře a podpisy + Protect & Standardize Ochrana a standardizace + Customize & Automate Přizpůsobení a automatizace + No tools match your search. Hledání neodpovídá žádný nástroj. diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index b5baed7ba1..94863e0743 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2429,3 +2429,9 @@ animation effects Off Aus Make properties default Eigenschaften als Standard festlegen Pages selected Ausgewählte Seiten Page properties Seiteneigenschaften + Create & Edit Erstellen & Bearbeiten + Review & Compare Prüfen & Vergleichen + Forms & Signatures Formulare & Signaturen + Protect & Standardize Schützen & Standardisieren + Customize & Automate Anpassen & Automatisieren + No tools match your search. Keine Werkzeuge entsprechen Ihrer Suche. diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 57de6eef07..b22b3cb2a6 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2438,3 +2438,9 @@ animation effects Off Ανενεργά Make properties default Ορισμός ιδιοτήτων ως προεπιλογή Pages selected Επιλεγμένες σελίδες Page properties Ιδιότητες σελίδας + Create & Edit Δημιουργία και επεξεργασία + Review & Compare Αναθεώρηση και σύγκριση + Forms & Signatures Φόρμες και υπογραφές + Protect & Standardize Προστασία και τυποποίηση + Customize & Automate Προσαρμογή και αυτοματοποίηση + No tools match your search. Κανένα εργαλείο δεν ταιριάζει με την αναζήτησή σας. diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 548daf0caa..332cd43f06 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2568,3 +2568,9 @@ animation effects Off Desactivados Make properties default Usar como propiedades predeterminadas Pages selected Páginas seleccionadas Page properties Propiedades de página + Create & Edit Crear y editar + Review & Compare Revisar y comparar + Forms & Signatures Formularios y firmas + Protect & Standardize Proteger y estandarizar + Customize & Automate Personalizar y automatizar + No tools match your search. Ninguna herramienta coincide con la búsqueda. diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index c082789bc1..0e69171c6f 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2428,3 +2428,9 @@ animation effects Off Désactivés Make properties default Définir comme propriétés par défaut Pages selected Pages sélectionnées Page properties Propriétés de la page + Create & Edit Créer et modifier + Review & Compare Réviser et comparer + Forms & Signatures Formulaires et signatures + Protect & Standardize Protéger et normaliser + Customize & Automate Personnaliser et automatiser + No tools match your search. Aucun outil ne correspond à votre recherche. diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index b47da5b412..4facca6c56 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2427,3 +2427,9 @@ animation effects Off Ki Make properties default Tulajdonságok beállítása alapértelmezettként Pages selected Kijelölt oldalak Page properties Oldaltulajdonságok + Create & Edit Létrehozás és szerkesztés + Review & Compare Áttekintés és összehasonlítás + Forms & Signatures Űrlapok és aláírások + Protect & Standardize Védelem és szabványosítás + Customize & Automate Testreszabás és automatizálás + No tools match your search. Egy eszköz sem felel meg a keresésnek. diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 9d54a4bead..6420b720e2 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2640,3 +2640,9 @@ certificate Key usage Utilizzo chiave certificate Self-signed Autofirmato certificate SHA-1 digest Impronta SHA-1 certificate SHA-256 digest Impronta SHA-256 + Create & Edit Crea e modifica + Review & Compare Revisione e confronto + Forms & Signatures Moduli e firme + Protect & Standardize Proteggi e standardizza + Customize & Automate Personalizza e automatizza + No tools match your search. Nessuno strumento corrisponde alla ricerca. diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 461e074935..0fec03dad2 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2428,3 +2428,9 @@ animation effects Off オフ Make properties default プロパティをデフォルトに設定 Pages selected 選択したページ Page properties ページのプロパティ + Create & Edit 作成と編集 + Review & Compare レビューと比較 + Forms & Signatures フォームと署名 + Protect & Standardize 保護と標準化 + Customize & Automate カスタマイズと自動化 + No tools match your search. 検索に一致するツールはありません。 diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index af140d3a07..4b6ef6795b 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -2426,3 +2426,9 @@ animation effects Off Өшірулі Make properties default Қасиеттерді әдепкі ету Pages selected Таңдалған беттер Page properties Бет қасиеттері + Create & Edit Жасау және өңдеу + Review & Compare Шолу және салыстыру + Forms & Signatures Пішіндер және қолтаңбалар + Protect & Standardize Қорғау және стандарттау + Customize & Automate Баптау және автоматтандыру + No tools match your search. Іздеуге сәйкес құралдар жоқ. diff --git a/crates/ui-egui/src/i18n/lt.tsv b/crates/ui-egui/src/i18n/lt.tsv index 247b6cff95..9cc5927d6d 100644 --- a/crates/ui-egui/src/i18n/lt.tsv +++ b/crates/ui-egui/src/i18n/lt.tsv @@ -2672,3 +2672,9 @@ key Space Tarpas Make properties default Nustatyti ypatybes kaip numatytąsias Pages selected Pasirinkti puslapiai Page properties Puslapio ypatybės + Create & Edit Kūrimas ir redagavimas + Review & Compare Peržiūra ir palyginimas + Forms & Signatures Formos ir parašai + Protect & Standardize Apsauga ir standartizavimas + Customize & Automate Pritaikymas ir automatizavimas + No tools match your search. Paieškos neatitinka joks įrankis. diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index 5bb2889c9d..c322e2d7c5 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -811,6 +811,9 @@ mod tests { for command in pdfcraft_engine::commands::COMMANDS { assert!(has(JA(), command.label), "missing command: {}", command.label); } + for category in pdfcraft_engine::catalog::TOOL_CATEGORIES { + assert!(has(JA(), category.label), "missing category: {}", category.label); + } for group in pdfcraft_engine::catalog::TOOL_GROUPS { assert!(has(JA(), group.label), "missing group: {}", group.label); for section in group.sections { diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 3dbb247056..c04c83f673 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2408,3 +2408,9 @@ animation effects Off Wyłączone Make properties default Ustaw właściwości jako domyślne Pages selected Zaznaczone strony Page properties Właściwości strony + Create & Edit Tworzenie i edycja + Review & Compare Recenzja i porównanie + Forms & Signatures Formularze i podpisy + Protect & Standardize Ochrona i standaryzacja + Customize & Automate Dostosowanie i automatyzacja + No tools match your search. Żadne narzędzie nie pasuje do wyszukiwania. diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 3e55173169..266aa57c9f 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2442,3 +2442,9 @@ animation effects Off Desativados Make properties default Tornar propriedades padrão Pages selected Páginas selecionadas Page properties Propriedades da página + Create & Edit Criar e editar + Review & Compare Revisar e comparar + Forms & Signatures Formulários e assinaturas + Protect & Standardize Proteger e padronizar + Customize & Automate Personalizar e automatizar + No tools match your search. Nenhuma ferramenta corresponde à pesquisa. diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 1ac9c10ba1..cf6248983a 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2341,3 +2341,9 @@ animation effects Off Выкл Make properties default Сделать свойства стандартными Pages selected Выделенные страницы Page properties Свойства страницы + Create & Edit Создание и редактирование + Review & Compare Рецензирование и сравнение + Forms & Signatures Формы и подписи + Protect & Standardize Защита и стандартизация + Customize & Automate Настройка и автоматизация + No tools match your search. Нет инструментов, соответствующих запросу. diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index 4165909995..3970769429 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -2433,3 +2433,9 @@ animation effects Off Izklopljeni Make properties default Nastavi lastnosti kot privzete Pages selected Izbrane strani Page properties Lastnosti strani + Create & Edit Ustvarjanje in urejanje + Review & Compare Pregled in primerjava + Forms & Signatures Obrazci in podpisi + Protect & Standardize Zaščita in standardizacija + Customize & Automate Prilagajanje in avtomatizacija + No tools match your search. Iskanju ne ustreza nobeno orodje. diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index 07b716eac1..b0f5dba9ef 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -2341,3 +2341,9 @@ animation effects Off ఆఫ్ Make properties default లక్షణాలను డిఫాల్ట్‌గా చేయి Pages selected ఎంచుకున్న పేజీలు Page properties పేజీ లక్షణాలు + Create & Edit సృష్టించు & సవరించు + Review & Compare సమీక్షించు & పోల్చు + Forms & Signatures ఫారమ్‌లు & సంతకాలు + Protect & Standardize రక్షించు & ప్రామాణీకరించు + Customize & Automate అనుకూలీకరించు & ఆటోమేట్ చేయి + No tools match your search. మీ శోధనకు సరిపోలే సాధనాలు లేవు. diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 9fbd4dc966..5842a2f252 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2424,3 +2424,9 @@ animation effects Off Kapalı Make properties default Özellikleri varsayılan yap Pages selected Seçilen sayfalar Page properties Sayfa özellikleri + Create & Edit Oluştur ve Düzenle + Review & Compare İncele ve Karşılaştır + Forms & Signatures Formlar ve İmzalar + Protect & Standardize Koru ve Standartlaştır + Customize & Automate Özelleştir ve Otomatikleştir + No tools match your search. Aramanızla eşleşen araç yok. diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 53bc832f55..0e16e392f2 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2450,3 +2450,9 @@ animation effects Off Вимкнено Make properties default Зробити властивості типовими Pages selected Вибрані сторінки Page properties Властивості сторінки + Create & Edit Створення й редагування + Review & Compare Рецензування й порівняння + Forms & Signatures Форми й підписи + Protect & Standardize Захист і стандартизація + Customize & Automate Налаштування й автоматизація + No tools match your search. Немає інструментів, що відповідають пошуку. diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index a01b47f8a1..76052dc48f 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2615,3 +2615,9 @@ key Space Space Redact Image Xóa nội dung nhạy cảm của hình ảnh Create Link… Tạo liên kết… Recognize Text… Nhận dạng văn bản… + Create & Edit Tạo và chỉnh sửa + Review & Compare Xem xét và so sánh + Forms & Signatures Biểu mẫu và chữ ký + Protect & Standardize Bảo vệ và chuẩn hóa + Customize & Automate Tùy chỉnh và tự động hóa + No tools match your search. Không có công cụ nào khớp với tìm kiếm. diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 3274ff4731..76736ff144 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2392,3 +2392,9 @@ animation effects Off 关 Make properties default 将属性设为默认 Pages selected 选定的页面 Page properties 页面属性 + Create & Edit 创建和编辑 + Review & Compare 审阅和比较 + Forms & Signatures 表单和签名 + Protect & Standardize 保护和标准化 + Customize & Automate 自定义和自动化 + No tools match your search. 没有与搜索匹配的工具。 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 0ea252e56c..ec1f188f44 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2384,3 +2384,9 @@ animation effects Off 關 Make properties default 將內容設為預設 Pages selected 選取的頁面 Page properties 頁面屬性 + Create & Edit 建立與編輯 + Review & Compare 審閱與比較 + Forms & Signatures 表單與簽名 + Protect & Standardize 保護與標準化 + Customize & Automate 自訂與自動化 + No tools match your search. 沒有符合搜尋的工具。 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index d101d812e0..3f3f6e9913 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -30,6 +30,7 @@ macro_rules! tl_ctx { mod a11y_ui; mod actions_ui; +mod all_tools_ui; mod appearance; pub mod canvas; mod chrome; @@ -563,6 +564,9 @@ pub struct PdfCraftApp { pub palette_open: bool, pub palette_query: String, pub all_tools_expanded: bool, + /// The All tools view fills the workspace (its sidebar's expand button). + pub all_tools_full: bool, + pub all_tools_query: String, pub recent: Vec, /// Preferences: reopen the files that were open when PdfCraft last closed (#442). pub reopen_last_session: bool, @@ -929,6 +933,8 @@ impl PdfCraftApp { palette_open: false, palette_query: String::new(), all_tools_expanded: false, + all_tools_full: false, + all_tools_query: String::new(), recent: Vec::new(), reopen_last_session: false, last_session: Default::default(), @@ -2171,6 +2177,10 @@ impl PdfCraftApp { /// that panel. The rail stays in Read mode, and its buttons open panels there too (#932). pub(crate) fn switch_mode(&mut self, mode: Mode, select_tools: bool) { let was_read = self.mode == Mode::Read; + // Another workspace puts the expanded All tools view away. + if mode != Mode::AllTools { + self.all_tools_full = false; + } if select_tools { self.select_mode(mode); } else { @@ -2549,7 +2559,14 @@ impl PdfCraftApp { _ => Some(Dialog::About), } } - ("tools", _) => self.all_tools_expanded = value != "collapsed", + ("tools", _) if value == "full" => { + self.mode = Mode::AllTools; + self.all_tools_full = true; + } + ("tools", _) => { + self.all_tools_full = false; + self.all_tools_expanded = value != "collapsed"; + } ("palette", _) => { self.palette_open = true; self.palette_query = value.to_string(); @@ -3145,6 +3162,8 @@ impl eframe::App for PdfCraftApp { None => canvas::document_area(self, index, ui), }, } + } else if self.mode == Mode::AllTools && self.all_tools_full { + all_tools_ui::show(self, ui); } else { docking::show(self, ui); } diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index 92071c2493..d9ca218e78 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -35,11 +35,13 @@ pub fn left_panel(app: &mut PdfCraftApp, ui: &mut egui::Ui) { ); } +/// Returns whether the leading button (Back, or Expand on All tools) and Close were clicked. fn panel_header(ui: &mut egui::Ui, t: &Tokens, title: &str, back: bool) -> (bool, bool) { let mut go_back = false; let mut close = false; ui.horizontal(|ui| { - if back && icons::button(ui, "chevron-left", 26.0, false, tl!("Back to all tools")).clicked() { + let (icon, tip) = if back { ("chevron-left", "Back to all tools") } else { ("maximize-2", "Expand") }; + if icons::button(ui, icon, 26.0, false, tl!(tip)).clicked() { go_back = true; } // Reserve space for Close before laying out the translated title. Long tool names @@ -58,7 +60,11 @@ fn panel_header(ui: &mut egui::Ui, t: &Tokens, title: &str, back: bool) -> (bool } fn all_tools(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) { - let (_, close) = panel_header(ui, t, "All tools", false); + let (expand, close) = panel_header(ui, t, "All tools", false); + if expand { + app.all_tools_full = true; + app.mode = crate::Mode::AllTools; + } if close { app.left_open = false; } diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index 1b6276d7c4..731ae3a79d 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -125,6 +125,50 @@ fn every_catalog_tool_is_listed_after_view_more() { } } +#[test] +fn expanded_all_tools_lists_every_tool_by_category_and_searches() { + let mut h = harness(|_| {}); + h.get_by_label("Expand").click(); + h.run_steps(3); + assert!(h.state().all_tools_full); + for c in pdfcraft_engine::catalog::TOOL_CATEGORIES { + h.get_by_label(c.label); + } + for g in pdfcraft_engine::catalog::TOOL_GROUPS { + assert!(h.query_all_by_label(g.label).count() >= 1, "tool {} missing from the expanded view", g.label); + } + // An item name finds its tool; the other categories drop out. + h.state_mut().all_tools_query = "rotate".into(); + h.run_steps(3); + assert_eq!(h.query_all_by_label("Organize pages").count(), 1, "only the expanded view's tile; the sidebar is gone"); + assert!(h.query_by_label("Forms & Signatures").is_none()); + h.state_mut().all_tools_query = "no such tool".into(); + h.run_steps(3); + h.get_by_label("No tools match your search."); + // Choosing a tool goes back to the workspace with that tool's panel. + h.state_mut().all_tools_query = "organize".into(); + h.run_steps(3); + h.get_by_label("Organize pages").click(); + h.run_steps(3); + assert!(!h.state().all_tools_full); + assert_eq!(h.state().left, pdfcraft_ui_egui::LeftPanel::Tool("organize")); + h.get_by_label("Back to all tools"); +} + +#[test] +fn collapse_and_escape_close_the_expanded_all_tools() { + let mut h = harness(|app| app.set_option("tools", "full").unwrap()); + h.get_by_label("Collapse").click(); + h.run_steps(3); + assert!(!h.state().all_tools_full); + h.get_by_label("View more"); + h.get_by_label("Expand").click(); + h.run_steps(3); + h.key_press(egui::Key::Escape); + h.run_steps(3); + assert!(!h.state().all_tools_full); +} + #[test] fn opening_a_pdf_shows_comments_and_bookmarks() { let mut h = harness(|app| app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens")); From b9e1b06b9456174e4e5aef266f74e5072ef1acaf Mon Sep 17 00:00:00 2001 From: alehel Date: Thu, 15 Oct 2026 17:58:11 +0200 Subject: [PATCH 2/7] M3: name the All tools expand and collapse buttons for what they do The sidebar's expand button was labelled "Expand", the same as the Combine grid's per-file Expand on its hover bar. With both on screen, the Combine grid tests' lookup by that label found two buttons and 13 of them failed. The buttons are now "Expand all tools" and "Collapse all tools", translated in all 22 catalogues, and the labels are tl! literals the coverage scan sees. Co-Authored-By: Claude Opus 5.5 --- ATTRIBUTION.toml | 44 ++++++++++++++--------------- crates/ui-egui/src/all_tools_ui.rs | 2 +- crates/ui-egui/src/i18n/ar.tsv | 2 ++ crates/ui-egui/src/i18n/bg.tsv | 2 ++ crates/ui-egui/src/i18n/cs.tsv | 2 ++ crates/ui-egui/src/i18n/de.tsv | 2 ++ crates/ui-egui/src/i18n/el.tsv | 2 ++ crates/ui-egui/src/i18n/es.tsv | 2 ++ crates/ui-egui/src/i18n/fr.tsv | 2 ++ crates/ui-egui/src/i18n/hu.tsv | 2 ++ crates/ui-egui/src/i18n/it.tsv | 2 ++ crates/ui-egui/src/i18n/ja.tsv | 2 ++ crates/ui-egui/src/i18n/kk.tsv | 2 ++ crates/ui-egui/src/i18n/lt.tsv | 2 ++ crates/ui-egui/src/i18n/pl.tsv | 2 ++ crates/ui-egui/src/i18n/pt-br.tsv | 2 ++ crates/ui-egui/src/i18n/ru.tsv | 2 ++ crates/ui-egui/src/i18n/sl.tsv | 2 ++ crates/ui-egui/src/i18n/te.tsv | 2 ++ crates/ui-egui/src/i18n/tr.tsv | 2 ++ crates/ui-egui/src/i18n/uk.tsv | 2 ++ crates/ui-egui/src/i18n/vi.tsv | 2 ++ crates/ui-egui/src/i18n/zh-hans.tsv | 2 ++ crates/ui-egui/src/i18n/zh-hant.tsv | 2 ++ crates/ui-egui/src/panels.rs | 5 ++-- crates/ui-egui/tests/ui.rs | 6 ++-- 26 files changed, 73 insertions(+), 28 deletions(-) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index db7dc3ebd0..2a094f27ec 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "d779dbb265c576d68cd804d0a0505ac5821d74f2d4e5afb663516d8801e0fbbc" +sha256 = "e89a37e6597ce2c9546ba4ab257dbbd0d6509311d061e0da481ad493edfc5fac" # ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences. # ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`). @@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "69a7a36b9b8fbe6cd04d7f287a77deb01c243cc0f1a438511cf35748dde6b3df" +sha256 = "65ed776368317165b0e77d339522d67d56df15a3446e2e75a88f7f6514fd6c1a" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "08e35bcca452daaa26efc66a6cbbf75794673cc59ebf42a97ce4180cc6ecfa0c" +sha256 = "5517f7d5c1a5eb75e33229d8b6a4d1014fb36b8dda518c4f9e2a012312198da5" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" @@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "0fd03fafffc2d75bf484186a7dd6ab15ab95d79448e0e798f57519515d73ac4e" +sha256 = "15ee8163cb5ffcc3f337b7015c33c31df14308d0cd2cdd8b515ba2b129bbed68" [[asset]] path = "crates/ui-egui/src/i18n/pt-br.tsv" @@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "d3292e186768f75b9cb87097688ff4909c26c9810a7e6542c127aad9fbce5af3" +sha256 = "895a2233d27a77e3debeaf2d12f7552eab0d552eb9fed8bf06cac319bb2e3565" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" @@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "14c9f2ba61ffc925b01e6bf31c7f7761fb5e7adcda1751f6768829341a8e3757" +sha256 = "107b4ba6fdfdb22bb87bf834ea57078a83e73881beaf197160496a0c60bfae59" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" @@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "d2dc6bd5de197eff60a440a05c58c6b7307009ecb08ddf8fbb31ff616633e36e" +sha256 = "b5abd6afc8188025331a75e9dd4bb9aa486e39560620f58bd25caf77a9dc5031" [[asset]] path = "crates/ui-egui/src/i18n/de.tsv" @@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "b6ffc5eda71ea13eaea79f5f3368811cb317f0a43e6f6be167bb4742d8e1ae77" +sha256 = "7bd86b52d9944de8496a769d66f279152c2fe52b89dcada393c5a4e9f7cb9020" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" @@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "37b60dda614ebc0e2b141cb27c9b2b7d13b49b347c1a5acb4d395147fbbdd246" +sha256 = "8c18e15224f961b19867076502fac48d5f22a9bd00330ce3a5b18b435e9c06a3" [[asset]] path = "crates/ui-egui/src/i18n/it.tsv" @@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "0055dfb04a35abd2c8da01bf3836855074e17bbd2b6332b2d1cecd16fd816938" +sha256 = "bff74bf37446325d9d9fbc99dbfda92e08af1dee63b3781b0d4eabbf687aa7d9" [[asset]] path = "crates/ui-egui/src/i18n/pl.tsv" @@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "e1c4801f9633f5873f5dc07fa6de01375369b636282fa22e8cd223adb9737b42" +sha256 = "4423f887698713d1ed2880e8e38dc9fa8022d2b4906f88a038c17bfe79072cee" [[asset]] path = "crates/ui-egui/src/i18n/ru.tsv" @@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "36bd183e5bb36e76e2eb40ac68d06f2d0dcd962b42998917ba3ad5e7d8ca2b19" +sha256 = "7641856dbc25181ff3290d8253b2abba3bd903b11d37b0537bb07cba0d0d18dc" [[asset]] path = "crates/ui-egui/src/i18n/hu.tsv" @@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "c10dab015d7cabc571dd2a462a0ca22a1b1461b9b747c4998e7a7e4e1f9b6b71" +sha256 = "69d3b6ab0e3435e0a142ab84c8c9348ce33395d5d68af121481e631947361873" [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" @@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "7b1db4f5f3ce50c8b995037d84cce86088e3af17942b2e1d0197234a2454ecdd" +sha256 = "8f404ffaaec75c4e024cfa3ec41c1d94cb14291fb8937ce79e0b5d214423505a" [[asset]] path = "crates/ui-egui/src/i18n/kk.tsv" @@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "3ef5f6e5c28c8060017b1219d0ae3fe6308eaa4f69baf821816da1ac2b4468cd" +sha256 = "209b739e765adb88c1e14517cb782e9622ef9fa6f748ba17c7f8e04957009501" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" @@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "d2b91bcf778610861b515865339a487f279f68188617fd2729e0d8d8455d7a2b" +sha256 = "450647a98ee292e84f281fd1aa09170e3747fbdac0259dcc75e173d0e8af88aa" [[asset]] path = "crates/ui-egui/src/i18n/uk.tsv" @@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "b3a0c1563ba9c67ffbebf805769bae9c5a809cd14fd37a0441a30e19cacb28c8" +sha256 = "24827470a54676a9819dc8c5ea56aba830b808586d0cda22b368be4747126022" [[asset]] path = "crates/ui-egui/src/i18n/ar.tsv" @@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "15dde8ef0f0d20a6e99b82c7f5297b41130e9f0d8f11494fcdc09c69bcbfb43c" +sha256 = "4d5d708e54b5e1534b4d11aa30ac3aebbb591993fb6135beb05015fd9ed1ec2b" [[asset]] path = "crates/ui-egui/src/i18n/tr.tsv" @@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9d879f455313d528d2d8c91e080a1fa199f50d901c7691f7b6dc3f15be73fd1a" +sha256 = "8c074e0fc0dc7c6da1ffea2a87b8ebbc2abb16915e3d3ebd137b775e6267890f" [[asset]] path = "crates/ui-egui/src/i18n/lt.tsv" @@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "e9ecf10658e7c02e2c59e3fb354e561ade437ce9938cb08ebdc9488184efb59e" +sha256 = "476ec3ac3ba98e5fe243a2e66379d58cd95a0b1d6488730346d934af0212ff10" [[asset]] @@ -240,7 +240,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "29677be4697aa6feeb1d4251795960e525a3a47ce3381a32b0157f488542b30e" +sha256 = "9f417e74d6590fbf68e63ad1bed8f808c92900548633c8dbe63b0f7d70f271b7" [[asset]] path = "assets/icons/accessibility.svg" @@ -3363,7 +3363,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "bb3a8708b1a2f72782ab41b0ae43e902f089caffd545004f5d1b2112d20df78c" +sha256 = "0e6d29aadf9d09f0acba0694a3fbe3bb29ee8e52c1027adc02cc9f6aa47b6358" [[asset]] path = "vendor/vello_common/assets/probe.png" diff --git a/crates/ui-egui/src/all_tools_ui.rs b/crates/ui-egui/src/all_tools_ui.rs index e46ec6c723..70be7747c4 100644 --- a/crates/ui-egui/src/all_tools_ui.rs +++ b/crates/ui-egui/src/all_tools_ui.rs @@ -37,7 +37,7 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) { ui.horizontal(|ui| { ui.label(egui::RichText::new(tl!("All tools")).font(theme::semibold(22.0)).color(t.text)); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if icons::button(ui, "minimize-2", 28.0, false, tl!("Collapse")).clicked() { + if icons::button(ui, "minimize-2", 28.0, false, tl!("Collapse all tools")).clicked() { app.all_tools_full = false; } }); diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index 1915ee5fc6..0e847fd4c3 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -2712,3 +2712,5 @@ certificate Version الإصدار Protect & Standardize الحماية والتوحيد القياسي Customize & Automate التخصيص والأتمتة No tools match your search. لا توجد أدوات تطابق بحثك. + Expand all tools توسيع كل الأدوات + Collapse all tools طي كل الأدوات diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 57bbffaabb..5b93bd1425 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2397,3 +2397,5 @@ animation effects Off Изключени Protect & Standardize Защита и стандартизация Customize & Automate Персонализиране и автоматизация No tools match your search. Няма инструменти, отговарящи на търсенето. + Expand all tools Разгъване на всички инструменти + Collapse all tools Свиване на всички инструменти diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index c52ec8b20d..97f8017326 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2508,3 +2508,5 @@ animation effects Off Vypnuto Protect & Standardize Ochrana a standardizace Customize & Automate Přizpůsobení a automatizace No tools match your search. Hledání neodpovídá žádný nástroj. + Expand all tools Rozbalit všechny nástroje + Collapse all tools Sbalit všechny nástroje diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 94863e0743..87b8cefad8 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2435,3 +2435,5 @@ animation effects Off Aus Protect & Standardize Schützen & Standardisieren Customize & Automate Anpassen & Automatisieren No tools match your search. Keine Werkzeuge entsprechen Ihrer Suche. + Expand all tools Alle Werkzeuge ausklappen + Collapse all tools Alle Werkzeuge einklappen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index b22b3cb2a6..7df73c841c 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2444,3 +2444,5 @@ animation effects Off Ανενεργά Protect & Standardize Προστασία και τυποποίηση Customize & Automate Προσαρμογή και αυτοματοποίηση No tools match your search. Κανένα εργαλείο δεν ταιριάζει με την αναζήτησή σας. + Expand all tools Ανάπτυξη όλων των εργαλείων + Collapse all tools Σύμπτυξη όλων των εργαλείων diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 332cd43f06..f14153bb8a 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2574,3 +2574,5 @@ animation effects Off Desactivados Protect & Standardize Proteger y estandarizar Customize & Automate Personalizar y automatizar No tools match your search. Ninguna herramienta coincide con la búsqueda. + Expand all tools Expandir todas las herramientas + Collapse all tools Contraer todas las herramientas diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 0e69171c6f..01531f0b34 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2434,3 +2434,5 @@ animation effects Off Désactivés Protect & Standardize Protéger et normaliser Customize & Automate Personnaliser et automatiser No tools match your search. Aucun outil ne correspond à votre recherche. + Expand all tools Développer tous les outils + Collapse all tools Réduire tous les outils diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 4facca6c56..ef4b84257f 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2433,3 +2433,5 @@ animation effects Off Ki Protect & Standardize Védelem és szabványosítás Customize & Automate Testreszabás és automatizálás No tools match your search. Egy eszköz sem felel meg a keresésnek. + Expand all tools Az összes eszköz kibontása + Collapse all tools Az összes eszköz összecsukása diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 6420b720e2..2d9b25baab 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2646,3 +2646,5 @@ certificate SHA-256 digest Impronta SHA-256 Protect & Standardize Proteggi e standardizza Customize & Automate Personalizza e automatizza No tools match your search. Nessuno strumento corrisponde alla ricerca. + Expand all tools Espandi tutti gli strumenti + Collapse all tools Comprimi tutti gli strumenti diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 0fec03dad2..7c5ea80bcf 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2434,3 +2434,5 @@ animation effects Off オフ Protect & Standardize 保護と標準化 Customize & Automate カスタマイズと自動化 No tools match your search. 検索に一致するツールはありません。 + Expand all tools すべてのツールを展開 + Collapse all tools すべてのツールを折りたたむ diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index 4b6ef6795b..a8f4d51af6 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -2432,3 +2432,5 @@ animation effects Off Өшірулі Protect & Standardize Қорғау және стандарттау Customize & Automate Баптау және автоматтандыру No tools match your search. Іздеуге сәйкес құралдар жоқ. + Expand all tools Барлық құралдарды жаю + Collapse all tools Барлық құралдарды жию diff --git a/crates/ui-egui/src/i18n/lt.tsv b/crates/ui-egui/src/i18n/lt.tsv index 2315154728..4d5afa71ed 100644 --- a/crates/ui-egui/src/i18n/lt.tsv +++ b/crates/ui-egui/src/i18n/lt.tsv @@ -2678,6 +2678,8 @@ key Space Tarpas Protect & Standardize Apsauga ir standartizavimas Customize & Automate Pritaikymas ir automatizavimas No tools match your search. Paieškos neatitinka joks įrankis. + Expand all tools Išskleisti visus įrankius + Collapse all tools Suskleisti visus įrankius # AI-assisted additions for concurrently merged UI features (2026-10-15). # These additions have not had separate native-speaker review. diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index c04c83f673..9a2c5cf53d 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2414,3 +2414,5 @@ animation effects Off Wyłączone Protect & Standardize Ochrona i standaryzacja Customize & Automate Dostosowanie i automatyzacja No tools match your search. Żadne narzędzie nie pasuje do wyszukiwania. + Expand all tools Rozwiń wszystkie narzędzia + Collapse all tools Zwiń wszystkie narzędzia diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 266aa57c9f..6a09d38e46 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2448,3 +2448,5 @@ animation effects Off Desativados Protect & Standardize Proteger e padronizar Customize & Automate Personalizar e automatizar No tools match your search. Nenhuma ferramenta corresponde à pesquisa. + Expand all tools Expandir todas as ferramentas + Collapse all tools Recolher todas as ferramentas diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index cf6248983a..de69cb2848 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2347,3 +2347,5 @@ animation effects Off Выкл Protect & Standardize Защита и стандартизация Customize & Automate Настройка и автоматизация No tools match your search. Нет инструментов, соответствующих запросу. + Expand all tools Развернуть все инструменты + Collapse all tools Свернуть все инструменты diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index 3970769429..bbc008db7d 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -2439,3 +2439,5 @@ animation effects Off Izklopljeni Protect & Standardize Zaščita in standardizacija Customize & Automate Prilagajanje in avtomatizacija No tools match your search. Iskanju ne ustreza nobeno orodje. + Expand all tools Razširi vsa orodja + Collapse all tools Strni vsa orodja diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index b0f5dba9ef..61352bcea9 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -2347,3 +2347,5 @@ animation effects Off ఆఫ్ Protect & Standardize రక్షించు & ప్రామాణీకరించు Customize & Automate అనుకూలీకరించు & ఆటోమేట్ చేయి No tools match your search. మీ శోధనకు సరిపోలే సాధనాలు లేవు. + Expand all tools అన్ని సాధనాలను విస్తరించు + Collapse all tools అన్ని సాధనాలను కుదించు diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 5842a2f252..355a96e647 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2430,3 +2430,5 @@ animation effects Off Kapalı Protect & Standardize Koru ve Standartlaştır Customize & Automate Özelleştir ve Otomatikleştir No tools match your search. Aramanızla eşleşen araç yok. + Expand all tools Tüm araçları genişlet + Collapse all tools Tüm araçları daralt diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 0e16e392f2..b2b3aa6ab0 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2456,3 +2456,5 @@ animation effects Off Вимкнено Protect & Standardize Захист і стандартизація Customize & Automate Налаштування й автоматизація No tools match your search. Немає інструментів, що відповідають пошуку. + Expand all tools Розгорнути всі інструменти + Collapse all tools Згорнути всі інструменти diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 43aaf035c2..78c53101ac 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2621,6 +2621,8 @@ key Space Space Protect & Standardize Bảo vệ và chuẩn hóa Customize & Automate Tùy chỉnh và tự động hóa No tools match your search. Không có công cụ nào khớp với tìm kiếm. + Expand all tools Mở rộng tất cả công cụ + Collapse all tools Thu gọn tất cả công cụ # AI-assisted additions for concurrently merged UI features (2026-10-15). # These additions have not had separate native-speaker review. diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 76736ff144..2af48b2786 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2398,3 +2398,5 @@ animation effects Off 关 Protect & Standardize 保护和标准化 Customize & Automate 自定义和自动化 No tools match your search. 没有与搜索匹配的工具。 + Expand all tools 展开所有工具 + Collapse all tools 折叠所有工具 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 5d5f038b81..e783a83be1 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2388,3 +2388,5 @@ animation effects Off 關 Protect & Standardize 保護與標準化 Customize & Automate 自訂與自動化 No tools match your search. 沒有符合搜尋的工具。 + Expand all tools 展開所有工具 + Collapse all tools 摺疊所有工具 diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index d9ca218e78..7060610d51 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -40,8 +40,9 @@ fn panel_header(ui: &mut egui::Ui, t: &Tokens, title: &str, back: bool) -> (bool let mut go_back = false; let mut close = false; ui.horizontal(|ui| { - let (icon, tip) = if back { ("chevron-left", "Back to all tools") } else { ("maximize-2", "Expand") }; - if icons::button(ui, icon, 26.0, false, tl!(tip)).clicked() { + // Labels distinct from the Combine grid's Expand and Collapse, which can show beside them. + let (icon, tip) = if back { ("chevron-left", tl!("Back to all tools")) } else { ("maximize-2", tl!("Expand all tools")) }; + if icons::button(ui, icon, 26.0, false, tip).clicked() { go_back = true; } // Reserve space for Close before laying out the translated title. Long tool names diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index 731ae3a79d..a8ff0ed03c 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -128,7 +128,7 @@ fn every_catalog_tool_is_listed_after_view_more() { #[test] fn expanded_all_tools_lists_every_tool_by_category_and_searches() { let mut h = harness(|_| {}); - h.get_by_label("Expand").click(); + h.get_by_label("Expand all tools").click(); h.run_steps(3); assert!(h.state().all_tools_full); for c in pdfcraft_engine::catalog::TOOL_CATEGORIES { @@ -158,11 +158,11 @@ fn expanded_all_tools_lists_every_tool_by_category_and_searches() { #[test] fn collapse_and_escape_close_the_expanded_all_tools() { let mut h = harness(|app| app.set_option("tools", "full").unwrap()); - h.get_by_label("Collapse").click(); + h.get_by_label("Collapse all tools").click(); h.run_steps(3); assert!(!h.state().all_tools_full); h.get_by_label("View more"); - h.get_by_label("Expand").click(); + h.get_by_label("Expand all tools").click(); h.run_steps(3); h.key_press(egui::Key::Escape); h.run_steps(3); From 23ed4cca657327e849f3bc5e571df5dcd3f5e2f8 Mon Sep 17 00:00:00 2001 From: alehel Date: Sun, 18 Oct 2026 16:33:12 +0200 Subject: [PATCH 3/7] M3: All tools view keeps Escape for dialogs, leaves with the mode option, and searches Arabic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups for the expanded All tools view: - Escape closed the view even when a dialog or prompt over it owned the key. It now checks modal_open() first. - set_option("mode", …) changed workspace without clearing the view, so leaving and coming back to All tools reopened it. Another workspace now puts it away, as switch_mode does. - Right-to-left translations are stored in display order, so a multi-word Arabic query typed in logical order missed its tool. Such a query now matches word by word, as the command palette's search does. Each test was seen to fail with its fix reverted. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/all_tools_ui.rs | 27 +++++++++++++++++++++++++-- crates/ui-egui/src/lib.rs | 4 ++++ crates/ui-egui/tests/ui.rs | 21 +++++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/all_tools_ui.rs b/crates/ui-egui/src/all_tools_ui.rs index 70be7747c4..e9e0f6128a 100644 --- a/crates/ui-egui/src/all_tools_ui.rs +++ b/crates/ui-egui/src/all_tools_ui.rs @@ -12,11 +12,23 @@ const TILE: egui::Vec2 = vec2(148.0, 118.0); /// Item names a tile lists when the search found the tool by them. const MATCHES_SHOWN: usize = 2; +/// Whether `query` (already trimmed and lower-cased) is in `label` or its translation. +/// A right-to-left language's translations are in display order (their words run the other way) +/// while the query is in typing order, so there several words match one by one, as in the +/// command palette. +fn label_hit(label: &str, translated: &str, query: &str, rtl: bool) -> bool { + let translated = translated.to_lowercase(); + label.to_lowercase().contains(query) + || translated.contains(query) + || (rtl && query.contains(char::is_whitespace) && query.split_whitespace().all(|word| translated.contains(word))) +} + /// Whether `group` matches `query` (already trimmed and lower-cased), in English or the UI /// language. `None` is no match; otherwise the items that matched, empty when the tool's own /// name did. fn matches(query: &str, group: &ToolGroup) -> Option> { - let hit = |label: &str| label.to_lowercase().contains(query) || tl!(label).to_lowercase().contains(query); + let rtl = crate::i18n::current().rtl(); + let hit = |label: &str| label_hit(label, tl!(label), query, rtl); if query.is_empty() || hit(group.label) { return Some(Vec::new()); } @@ -26,7 +38,8 @@ fn matches(query: &str, group: &ToolGroup) -> Option> { pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); - if ui.input(|i| i.key_pressed(egui::Key::Escape)) { + // A dialog or prompt over the view owns Escape. + if !app.modal_open() && ui.input(|i| i.key_pressed(egui::Key::Escape)) { app.all_tools_full = false; return; } @@ -155,4 +168,14 @@ mod tests { assert!(!rotate.is_empty() && rotate.iter().all(|l| l.to_lowercase().contains("rotate")), "{rotate:?}"); assert_eq!(matches("no such tool anywhere", group("organize")), None); } + + #[test] + fn right_to_left_labels_match_a_query_typed_in_logical_order() { + let shown = crate::bidi::display_rtl("تنظيم الصفحات"); + assert_ne!(shown, "تنظيم الصفحات", "the catalogue reverses the words for display"); + assert!(label_hit("Organize pages", &shown, "تنظيم الصفحات", true)); + assert!(label_hit("Organize pages", &shown, "الصفحات", true), "one word"); + assert!(!label_hit("Organize pages", &shown, "تنظيم الملفات", true), "every word must be there"); + assert!(!label_hit("Organize pages", &shown, "تنظيم الصفحات", false), "left-to-right labels stay exact"); + } } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 63c5a37d58..b6275cce2e 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -2515,6 +2515,10 @@ impl PdfCraftApp { let mode = Mode::parse(value).unwrap_or(Mode::AllTools); self.mode_override = Some(mode); self.mode = mode; + // As switch_mode: another workspace puts the expanded All tools view away. + if mode != Mode::AllTools { + self.all_tools_full = false; + } } ("default-mode", _) => { self.default_mode = Mode::parse(value).ok_or("default-mode must be all, read, edit, convert or sign")?; diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index 8f8916749b..6c51eae18f 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -169,6 +169,27 @@ fn collapse_and_escape_close_the_expanded_all_tools() { assert!(!h.state().all_tools_full); } +#[test] +fn escape_in_a_dialog_over_the_expanded_all_tools_leaves_the_view_open() { + let mut h = harness(|app| app.set_option("tools", "full").unwrap()); + h.state_mut().dialog = Some(pdfcraft_ui_egui::Dialog::About); + h.run_steps(3); + h.key_press(egui::Key::Escape); + h.run_steps(3); + assert!(h.state().all_tools_full, "Escape belonged to the dialog"); +} + +#[test] +fn another_workspace_from_the_mode_option_puts_the_expanded_all_tools_away() { + let mut app = PdfCraftApp::new(); + app.set_option("tools", "full").unwrap(); + app.set_option("mode", "all").unwrap(); + assert!(app.all_tools_full, "All tools itself keeps the view"); + app.set_option("mode", "read").unwrap(); + app.set_option("mode", "all").unwrap(); + assert!(!app.all_tools_full, "coming back shows the sidebar, as the mode bar does"); +} + #[test] fn opening_a_pdf_shows_comments_and_bookmarks() { let mut h = harness(|app| app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens")); From cb21b2cce4c4f848b92b9427c67ac99f96c633d6 Mon Sep 17 00:00:00 2001 From: alehel Date: Thu, 22 Oct 2026 16:47:14 +0200 Subject: [PATCH 4/7] M3: opening a file and expanding All tools take the usual workspace path Review follow-ups for the expanded All tools view: - Opening a document left the view on, either over the new document (All tools as the default workspace) or waiting to reappear on the next switch back (Read as the default, or a mode override). Activating a document now puts it away, and the reset lives in enter_mode, which every workspace change goes through. - The sidebar's expand button and `--tools full` assigned the mode directly. From Read the right panel saved on entry was not restored, and from Edit the Edit text tool stayed active. Both now go through switch_mode, as the mode bar does, so the sidebar shows the All tools list again on collapse. Each new test was seen to fail with the fix reverted. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/lib.rs | 26 ++++++++++++++------- crates/ui-egui/src/panels.rs | 3 +-- crates/ui-egui/tests/ui.rs | 45 ++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 81184ce686..0cd9a29344 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1426,6 +1426,10 @@ impl PdfCraftApp { { view.open_organize(); } + // The document shows, not the expanded All tools view over it (whichever workspace opens). + if activate { + self.all_tools_full = false; + } if activate && let Some(mode) = self.mode_override { // Explicit mode options do not reset independent --tool / --left choices. self.mode = mode; @@ -2286,6 +2290,10 @@ impl PdfCraftApp { /// again and applies a paragraph still open in the editor. fn enter_mode(&mut self, mode: Mode) { self.mode = mode; + // Another workspace puts the expanded All tools view away. + if mode != Mode::AllTools { + self.all_tools_full = false; + } if mode == Mode::Edit { self.quick_tool = QuickTool::EditText; } else if matches!(self.quick_tool, QuickTool::EditText | QuickTool::AddText) { @@ -2298,6 +2306,15 @@ impl PdfCraftApp { } } + /// Fill the workspace with the expanded All tools view, entering All tools the way the mode + /// bar does: leaving Read restores its right panel and leaving Edit puts the editing tool down. + pub(crate) fn expand_all_tools(&mut self) { + if self.mode != Mode::AllTools { + self.switch_mode(Mode::AllTools, true); + } + self.all_tools_full = true; + } + /// Back to the Select tool from an editing tool. A paragraph still open in the editor is /// applied, as clicking away from it does; left open, its page would keep showing the /// paragraph's text removed with no editor over it. @@ -2316,10 +2333,6 @@ impl PdfCraftApp { /// Read mode hides the quick toolbar, so entering it also puts the current tool down. pub(crate) fn switch_mode(&mut self, mode: Mode, select_tools: bool) { let was_read = self.mode == Mode::Read; - // Another workspace puts the expanded All tools view away. - if mode != Mode::AllTools { - self.all_tools_full = false; - } if select_tools { self.select_mode(mode); } else { @@ -2730,10 +2743,7 @@ impl PdfCraftApp { _ => Some(Dialog::About), } } - ("tools", _) if value == "full" => { - self.mode = Mode::AllTools; - self.all_tools_full = true; - } + ("tools", _) if value == "full" => self.expand_all_tools(), ("tools", _) => { self.all_tools_full = false; self.all_tools_expanded = value != "collapsed"; diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index f4fbb397a6..ebddd320b5 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -66,8 +66,7 @@ fn all_tools(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) { // Labels distinct from the Combine grid's Expand and Collapse, which can show beside them. let (expand, close) = panel_header(ui, t, "All tools", Some(("maximize-2", tl!("Expand all tools")))); if expand { - app.all_tools_full = true; - app.mode = crate::Mode::AllTools; + app.expand_all_tools(); } if close { app.left_open = false; diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index 8416e34607..f65da9ac6a 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -190,6 +190,51 @@ fn another_workspace_from_the_mode_option_puts_the_expanded_all_tools_away() { assert!(!app.all_tools_full, "coming back shows the sidebar, as the mode bar does"); } +#[test] +fn opening_a_document_puts_the_expanded_all_tools_away() { + use pdfcraft_ui_egui::Mode; + // All tools as the default workspace: no workspace change, but the document must show. + let mut app = PdfCraftApp::new(); + app.set_option("tools", "full").unwrap(); + app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens"); + assert!(!app.all_tools_full, "the document shows, not the catalogue over it"); + // Read as the default workspace: coming back to All tools later shows the sidebar. + let mut app = PdfCraftApp::new(); + app.set_option("default-mode", "read").unwrap(); + app.set_option("tools", "full").unwrap(); + app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens"); + assert_eq!(app.mode, Mode::Read); + assert!(!app.all_tools_full); +} + +#[test] +fn expanding_all_tools_from_read_mode_restores_the_right_panel() { + use pdfcraft_ui_egui::{Mode, RightPanel}; + let mut h = harness(|app| app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens")); + h.state_mut().right = Some(RightPanel::Bookmarks); + h.state_mut().execute("view.read_mode"); + h.run_steps(3); + assert_eq!((h.state().mode, h.state().right), (Mode::Read, None), "Read mode hides the panel"); + h.state_mut().set_option("tools", "full").unwrap(); + h.run_steps(3); + assert!(h.state().all_tools_full); + assert_eq!((h.state().mode, h.state().right), (Mode::AllTools, Some(RightPanel::Bookmarks))); +} + +#[test] +fn expanding_all_tools_from_edit_puts_edit_text_down() { + use pdfcraft_ui_egui::{LeftPanel, Mode, QuickTool}; + let mut h = harness(|app| app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens")); + h.get_by_label("Edit").click(); + h.run_steps(3); + assert_eq!(h.state().quick_tool, QuickTool::EditText); + h.state_mut().set_option("tools", "full").unwrap(); + h.run_steps(3); + assert!(h.state().all_tools_full); + assert_eq!((h.state().mode, h.state().quick_tool), (Mode::AllTools, QuickTool::Select)); + assert_eq!(h.state().left, LeftPanel::AllTools, "collapsing goes back to the All tools list"); +} + #[test] fn opening_a_pdf_shows_comments_and_bookmarks() { let mut h = harness(|app| app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens")); From 8a8bb92d33f028a4e2b92c4412e8c7a335de2c8a Mon Sep 17 00:00:00 2001 From: alehel Date: Thu, 22 Oct 2026 17:00:41 +0200 Subject: [PATCH 5/7] M3: choosing a tool or another tab puts the expanded All tools away The expanded view kept covering the workspace after a control client chose a tool (`tool=organize`) or went Home (`home`), so the chosen panel or the Home view never showed. The same held for a document tab click, Open Recent on an open file, and the Combine files tab. select_tool now clears the view, so every way of choosing a tool does. The view also remembers which tab was in front when it opened, and another tab coming forward puts it away, whichever path brought it there. Each new test was seen to fail with the fix reverted. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/all_tools_ui.rs | 1 - crates/ui-egui/src/lib.rs | 16 +++++++++++++++ crates/ui-egui/tests/ui.rs | 33 ++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/all_tools_ui.rs b/crates/ui-egui/src/all_tools_ui.rs index e9e0f6128a..71b4c16845 100644 --- a/crates/ui-egui/src/all_tools_ui.rs +++ b/crates/ui-egui/src/all_tools_ui.rs @@ -98,7 +98,6 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) { }); }); if let Some(id) = chosen { - app.all_tools_full = false; app.left_open = true; app.select_tool(id); } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 0cd9a29344..8272978545 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -583,6 +583,9 @@ pub struct PdfCraftApp { /// The All tools view fills the workspace (its sidebar's expand button). pub all_tools_full: bool, pub all_tools_query: String, + /// The tab that showed when the expanded All tools view opened ([`Self::shown_tab`]). Another + /// tab coming to the front (Home, a document, Combine files) puts the view away. + all_tools_tab: (Option, bool), pub recent: Vec, /// Preferences: reopen the files that were open when PdfCraft last closed (#442). pub reopen_last_session: bool, @@ -987,6 +990,7 @@ impl PdfCraftApp { all_tools_expanded: false, all_tools_full: false, all_tools_query: String::new(), + all_tools_tab: (None, false), recent: Vec::new(), reopen_last_session: false, last_session: Default::default(), @@ -2256,6 +2260,8 @@ impl PdfCraftApp { /// Enter a tool from the catalogue, Home, palette or control channel. Organize is a /// workspace, so opening its panel also opens the page grid (without toggling it off). pub(crate) fn select_tool(&mut self, id: &'static str) { + // The tool's panel shows, not the expanded All tools view over it. + self.all_tools_full = false; self.left = LeftPanel::Tool(id); self.reveal_tools(); if let Some(view) = self.active.and_then(|i| self.views.get_mut(i)) { @@ -2313,6 +2319,12 @@ impl PdfCraftApp { self.switch_mode(Mode::AllTools, true); } self.all_tools_full = true; + self.all_tools_tab = self.shown_tab(); + } + + /// The tab at the front: the active document, or else whether Combine files shows over Home. + fn shown_tab(&self) -> (Option, bool) { + (self.active.and_then(|i| self.views.get(i)).map(|v| v.id), self.combine_showing()) } /// Back to the Select tool from an editing tool. A paragraph still open in the editor is @@ -3400,6 +3412,10 @@ impl eframe::App for PdfCraftApp { .show(ui, |ui| panels::right_panel(self, ui)); } } + // Whichever way another tab came to the front, it shows instead of the expanded view. + if self.all_tools_full && self.all_tools_tab != self.shown_tab() { + self.all_tools_full = false; + } let t = theme::Tokens::get(&ctx); egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.pasteboard)).show(ui, |ui| { if self.mode == Mode::Read { diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index f65da9ac6a..ae32820fc8 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -207,6 +207,39 @@ fn opening_a_document_puts_the_expanded_all_tools_away() { assert!(!app.all_tools_full); } +#[test] +fn choosing_a_tool_puts_the_expanded_all_tools_away() { + use pdfcraft_ui_egui::LeftPanel; + let mut h = harness(|app| app.set_option("tools", "full").unwrap()); + h.state_mut().set_option("tool", "organize").unwrap(); + h.run_steps(3); + assert!(!h.state().all_tools_full, "the tool's panel shows, not the catalogue"); + assert_eq!(h.state().left, LeftPanel::Tool("organize")); +} + +#[test] +fn another_tab_coming_forward_puts_the_expanded_all_tools_away() { + // Home, through the control option. + let mut h = harness(|app| app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens")); + h.state_mut().set_option("tools", "full").unwrap(); + h.run_steps(3); + assert!(h.state().all_tools_full, "the document's own tab keeps it"); + h.state_mut().set_option("home", "").unwrap(); + h.run_steps(3); + assert!(!h.state().all_tools_full); + h.get_by_label_contains("Welcome to PdfCraft"); + // Another document's tab. + let mut h = harness(|app| { + app.open_bytes("one.pdf", None, FIXTURE.to_vec()).expect("fixture opens"); + app.open_bytes("two.pdf", None, FIXTURE.to_vec()).expect("fixture opens"); + }); + h.state_mut().set_option("tools", "full").unwrap(); + h.run_steps(3); + h.state_mut().active = Some(0); + h.run_steps(3); + assert!(!h.state().all_tools_full, "one.pdf shows, not the catalogue over it"); +} + #[test] fn expanding_all_tools_from_read_mode_restores_the_right_panel() { use pdfcraft_ui_egui::{Mode, RightPanel}; From fd971559e7ea07349e88c77a72773f794c4edc1d Mon Sep 17 00:00:00 2001 From: alehel Date: Thu, 22 Oct 2026 17:29:03 +0200 Subject: [PATCH 6/7] M3: going Home puts the expanded All tools away with no document open With no document open the expanded view covers Home itself, so the control option `home` changed no tab and the view stayed up (CodeRabbit's remaining merge-risk note on 8a8bb92d). The option now puts the view away itself. `going_home_with_no_document_puts_the_expanded_all_tools_away` was seen to fail with the fix reverted. Also doc comments on the view's `show` and `tile` and the sidebar's `all_tools`, for CodeRabbit's docstring-coverage check. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/all_tools_ui.rs | 5 +++++ crates/ui-egui/src/lib.rs | 7 ++++++- crates/ui-egui/src/panels.rs | 2 ++ crates/ui-egui/tests/ui.rs | 10 ++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/all_tools_ui.rs b/crates/ui-egui/src/all_tools_ui.rs index 71b4c16845..53aa7ebbd7 100644 --- a/crates/ui-egui/src/all_tools_ui.rs +++ b/crates/ui-egui/src/all_tools_ui.rs @@ -36,6 +36,8 @@ fn matches(query: &str, group: &ToolGroup) -> Option> { (!items.is_empty()).then_some(items) } +/// Draw the view over the workspace: the title with Collapse, the search field, and each +/// category's tiles that match the search. Choosing a tile opens that tool's panel. pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); // A dialog or prompt over the view owns Escape. @@ -103,6 +105,9 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) { } } +/// One tool's tile: its icon and name, then the `items` the search found it by or, when there are +/// none, its availability (a planned tool is greyed out with its milestone). A name or note cut +/// short shows in full on hover. fn tile(ui: &mut egui::Ui, t: &Tokens, g: &ToolGroup, items: &[&'static str]) -> egui::Response { let (rect, resp) = ui.allocate_exact_size(TILE, Sense::click()); resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tl!(g.label))); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 8272978545..0e8a5a58c6 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -2712,7 +2712,12 @@ impl PdfCraftApp { self.select_tool(g.id); } ("left", _) => self.left_open = value != "closed", - ("home", _) => self.active = None, + ("home", _) => { + self.active = None; + // With no document open the expanded All tools view covers Home itself, so no + // other tab comes forward to put it away. + self.all_tools_full = false; + } ("dialog", _) => { self.dialog = match value { "properties" => Some(Dialog::Properties(PropsTab::Description)), diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index ebddd320b5..c36b442410 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -62,6 +62,8 @@ pub(crate) fn panel_header(ui: &mut egui::Ui, t: &Tokens, title: &str, lead: Opt (go_back, close) } +/// The All tools sidebar: Expand (the full view) and Close, then the tool list, the first few +/// tools until View more shows them all. fn all_tools(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) { // Labels distinct from the Combine grid's Expand and Collapse, which can show beside them. let (expand, close) = panel_header(ui, t, "All tools", Some(("maximize-2", tl!("Expand all tools")))); diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index ae32820fc8..4ee1f06582 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -240,6 +240,16 @@ fn another_tab_coming_forward_puts_the_expanded_all_tools_away() { assert!(!h.state().all_tools_full, "one.pdf shows, not the catalogue over it"); } +#[test] +fn going_home_with_no_document_puts_the_expanded_all_tools_away() { + // Home is already the tab in front, so only the option itself can put the view away. + let mut h = harness(|app| app.set_option("tools", "full").unwrap()); + h.state_mut().set_option("home", "").unwrap(); + h.run_steps(3); + assert!(!h.state().all_tools_full); + h.get_by_label_contains("Welcome to PdfCraft"); +} + #[test] fn expanding_all_tools_from_read_mode_restores_the_right_panel() { use pdfcraft_ui_egui::{Mode, RightPanel}; From 45dccb22d03ea2aa9b59b387b47196a6edf5f4ce Mon Sep 17 00:00:00 2001 From: alehel Date: Fri, 23 Oct 2026 17:26:53 +0200 Subject: [PATCH 7/7] M3: All tools search finds menu commands; Escape stays with the palette Local CodeRabbit review follow-ups for the expanded All tools view: - The search field promises tools and commands, but an entry with a menu (Watermark: Add, Update, Remove) matched only by its own name, so "remove watermark" found nothing. Entries now also match by their menu commands' names, in English or the UI language. - With the command palette open over the view, Escape closed the view as well as the palette. The palette now owns Escape, as dialogs do. Each new test was seen to fail with its fix reverted. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/all_tools_ui.rs | 19 +++++++++++++++---- crates/ui-egui/tests/it/ui.rs | 11 +++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/all_tools_ui.rs b/crates/ui-egui/src/all_tools_ui.rs index 53aa7ebbd7..c8c510a11c 100644 --- a/crates/ui-egui/src/all_tools_ui.rs +++ b/crates/ui-egui/src/all_tools_ui.rs @@ -25,14 +25,19 @@ fn label_hit(label: &str, translated: &str, query: &str, rtl: bool) -> bool { /// Whether `group` matches `query` (already trimmed and lower-cased), in English or the UI /// language. `None` is no match; otherwise the items that matched, empty when the tool's own -/// name did. +/// name did. An entry with a menu (Watermark: Add, Update, Remove) also matches by its commands' +/// names, so "remove watermark" finds it. fn matches(query: &str, group: &ToolGroup) -> Option> { let rtl = crate::i18n::current().rtl(); let hit = |label: &str| label_hit(label, tl!(label), query, rtl); if query.is_empty() || hit(group.label) { return Some(Vec::new()); } - let items: Vec<_> = group.sections.iter().flat_map(|s| s.items.iter()).map(|i| i.label).filter(|l| hit(l)).collect(); + let action_hit = |id: &&str| { + pdfcraft_engine::commands::command(id).is_some_and(|spec| label_hit(spec.label, &crate::i18n::menu_label(spec.id, spec.label), query, rtl)) + }; + let items: Vec<_> = + group.sections.iter().flat_map(|s| s.items.iter()).filter(|i| hit(i.label) || i.actions.iter().any(action_hit)).map(|i| i.label).collect(); (!items.is_empty()).then_some(items) } @@ -40,8 +45,8 @@ fn matches(query: &str, group: &ToolGroup) -> Option> { /// category's tiles that match the search. Choosing a tile opens that tool's panel. pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); - // A dialog or prompt over the view owns Escape. - if !app.modal_open() && ui.input(|i| i.key_pressed(egui::Key::Escape)) { + // A dialog, prompt or the command palette over the view owns Escape. + if !app.modal_open() && !app.palette_open && ui.input(|i| i.key_pressed(egui::Key::Escape)) { app.all_tools_full = false; return; } @@ -173,6 +178,12 @@ mod tests { assert_eq!(matches("no such tool anywhere", group("organize")), None); } + #[test] + fn search_finds_an_entry_by_its_menus_commands() { + let found = matches("remove watermark", group("edit")).expect("Watermark's menu has Remove watermark"); + assert!(found.contains(&"Watermark"), "{found:?}"); + } + #[test] fn right_to_left_labels_match_a_query_typed_in_logical_order() { let shown = crate::bidi::display_rtl("تنظيم الصفحات"); diff --git a/crates/ui-egui/tests/it/ui.rs b/crates/ui-egui/tests/it/ui.rs index 056a7c3ddd..52e22017b3 100644 --- a/crates/ui-egui/tests/it/ui.rs +++ b/crates/ui-egui/tests/it/ui.rs @@ -179,6 +179,17 @@ fn escape_in_a_dialog_over_the_expanded_all_tools_leaves_the_view_open() { assert!(h.state().all_tools_full, "Escape belonged to the dialog"); } +#[test] +fn escape_in_the_command_palette_over_the_expanded_all_tools_leaves_the_view_open() { + let mut h = harness(|app| app.set_option("tools", "full").unwrap()); + h.state_mut().palette_open = true; + h.run_steps(3); + h.key_press(egui::Key::Escape); + h.run_steps(3); + assert!(!h.state().palette_open, "Escape closed the palette"); + assert!(h.state().all_tools_full, "and only the palette"); +} + #[test] fn another_workspace_from_the_mode_option_puts_the_expanded_all_tools_away() { let mut app = PdfCraftApp::new();