diff --git a/contributors/people.toml b/contributors/people.toml index 1124c7f2..e627c0f8 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -22,6 +22,11 @@ real_name = "Brandon Thomas" display_name = "Brandon Thomas" sync_github_name = false +[people.switch87] +real_name = "Gert Pellin" +display_name = "Gert Pellin" +sync_github_name = false + [people.rangoDJ] sync_github_name = true diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 7af9cb83..fe9114d0 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -840,6 +840,8 @@ Show All Показати всі Show All Constraint Bars Показати всі панелі обмежень Show All Dynamic Dimensions Показати всі динамічні розміри + Show and hide the tools of this category Показати й сховати інструменти цієї категорії + Show in Tool Sets Показувати в наборах інструментів Show command prompting near the crosshairs Показувати запити команд біля перехрестя Show Layer List Показати список шарів Show/Hide Lineweight Показати/приховати товщину ліній diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index bb6d2f8e..925457c3 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -77,6 +77,9 @@ pub struct UiState { /// "Drafting" or "Modeling". pub toolset_tab: String, pub collapsed_groups: Vec, + /// Tools the user took out of the Tool Sets palette (command ids); each category's gear + /// menu puts them back. + pub hidden_tools: Vec, pub properties_all: bool, /// Show an in-window menu bar (platforms without a native menu bar, or on request). pub in_window_menu: bool, @@ -150,6 +153,7 @@ impl Default for UiState { system_cursor: std::env::var("CADCRAFT_SYSTEM_CURSOR").is_ok_and(|v| !matches!(v.trim(), "" | "0" | "false" | "off")), toolset_tab: "Drafting".into(), collapsed_groups: Vec::new(), + hidden_tools: Vec::new(), properties_all: true, in_window_menu: !cfg!(target_os = "macos"), start_tab: false, diff --git a/crates/ui-egui/src/palettes.rs b/crates/ui-egui/src/palettes.rs index 26d929f5..c7abed63 100644 --- a/crates/ui-egui/src/palettes.rs +++ b/crates/ui-egui/src/palettes.rs @@ -160,6 +160,8 @@ pub fn toolsets(app: &mut CadApp, ui: &mut egui::Ui) { let body = Rect::from_min_max(pos2(r.left(), r.top() + tab_h), r.max); let mut clicked: Option<&'static str> = None; let mut toggle_group = None; + let mut tool_visible: Option<(String, bool)> = None; + let mut restore: Vec = Vec::new(); ui.scope_builder(egui::UiBuilder::new().max_rect(body), |ui| { egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| { ui.spacing_mut().item_spacing = vec2(0.0, 0.0); @@ -182,11 +184,28 @@ pub fn toolsets(app: &mut CadApp, ui: &mut egui::Ui) { egui::FontId::proportional(12.5), t.text, ); - icons::paint(pp, Rect::from_center_size(pos2(hr.right() - 12.0, hr.center().y), vec2(11.0, 11.0)), Icon::Gear, false); + let gear = Rect::from_center_size(pos2(hr.right() - 12.0, hr.center().y), vec2(13.0, 13.0)); + let gresp = ui.interact(gear, ui.id().with(("ts_gear", name)), Sense::click()); + icons::paint(pp, Rect::from_center_size(gear.center(), vec2(11.0, 11.0)), Icon::Gear, false); icons::describe_control(ui, &hresp, name, Some(!collapsed), false); + icons::describe_control(ui, &gresp, &format!("{name} options"), None, false); + let gresp = gresp.on_hover_text(crate::tl!("Show and hide the tools of this category")); if hresp.clicked() { toggle_group = Some(name.to_string()); } + let hidden = |cmd: &str| app.ui.hidden_tools.iter().any(|h| h == cmd); + egui::Popup::from_toggle_button_response(&gresp).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| { + ui.label(crate::tl!("Show in Tool Sets")); + for (_, cmd, tip) in large.iter().chain(small.iter()) { + let mut shown = !hidden(cmd); + if ui.checkbox(&mut shown, *tip).changed() { + tool_visible = Some((cmd.to_string(), shown)); + } + } + if ui.selectable_label(false, crate::tl!("Show All")).clicked() { + restore = large.iter().chain(small.iter()).map(|(_, cmd, _)| cmd.to_string()).collect(); + } + }); if !collapsed { ui.add_space(4.0); // Large icons. @@ -195,6 +214,9 @@ pub fn toolsets(app: &mut CadApp, ui: &mut egui::Ui) { ui.add_space(8.0); ui.spacing_mut().item_spacing = vec2(8.0, 0.0); for (icon, cmd, tip) in large.iter() { + if hidden(cmd) { + continue; + } ui.add_enabled_ui(command_known(cmd), |ui| { if icons::button(ui, *icon, 40.0, tip, false).clicked() { clicked = Some(cmd); @@ -204,7 +226,8 @@ pub fn toolsets(app: &mut CadApp, ui: &mut egui::Ui) { }); } // Small icons, 8 per row. - for row in small.chunks(8) { + let visible: Vec = small.iter().copied().filter(|(_, cmd, _)| !hidden(cmd)).collect(); + for row in visible.chunks(8) { ui.add_space(2.0); ui.horizontal(|ui| { ui.add_space(8.0); @@ -225,6 +248,16 @@ pub fn toolsets(app: &mut CadApp, ui: &mut egui::Ui) { } }); }); + if let Some((cmd, shown)) = tool_visible { + if shown { + app.ui.hidden_tools.retain(|h| h != &cmd); + } else if !app.ui.hidden_tools.contains(&cmd) { + app.ui.hidden_tools.push(cmd); + } + } + if !restore.is_empty() { + app.ui.hidden_tools.retain(|h| !restore.contains(h)); + } if let Some(g) = toggle_group { if let Some(i) = app.ui.collapsed_groups.iter().position(|x| *x == g) { app.ui.collapsed_groups.remove(i); diff --git a/crates/ui-egui/tests/toolsets_gear.rs b/crates/ui-egui/tests/toolsets_gear.rs new file mode 100644 index 00000000..216619e0 --- /dev/null +++ b/crates/ui-egui/tests/toolsets_gear.rs @@ -0,0 +1,80 @@ +//! The gear on a Tool Sets category header opens that category's options menu and no longer +//! collapses the category it sits on. + +use cadcraft_engine::Session; +use cadcraft_ui_egui::{CadApp, Services}; +use egui::vec2; +use egui_kittest::Harness; +use egui_kittest::kittest::Queryable; + +/// A tool name that appears exactly once in the Drafting palette, so the accessibility tree can +/// be queried for it without running into a toolbar button of the same name. +const HIDDEN: &str = "Revision Cloud"; + +/// The same tool as a command id, which is what the UI state stores. +const HIDDEN_CMD: &str = "revcloud"; + +fn harness() -> Harness<'static, CadApp> { + let mut app = CadApp::new(Session::default(), Services::default()); + app.ui.in_window_menu = false; + app.integrated_titlebar = true; + let mut harness = Harness::builder().with_size(vec2(1280.0, 760.0)).build_ui_state( + |ui, app: &mut CadApp| { + app.logic(&ui.ctx().clone()); + app.ui(ui); + }, + app, + ); + harness.run_steps(5); + harness +} + +#[test] +fn the_gear_opens_the_options_and_the_header_keeps_collapsing() { + let mut harness = harness(); + let header = harness.get_by_label("Draw"); + let gear = harness.get_by_label("Draw options"); + assert!(gear.rect().width() < header.rect().width() / 2.0, "the gear is a target of its own, not part of the header row's"); + + harness.get_by_label("Draw").click(); + harness.run_steps(5); + assert!(harness.state().ui.collapsed_groups.iter().any(|g| g == "Draw"), "clicking the header collapses the category"); + harness.get_by_label("Draw").click(); + harness.run_steps(5); + assert!(harness.state().ui.collapsed_groups.iter().all(|g| g != "Draw"), "clicking it again expands the category"); + + harness.get_by_label("Draw options").click(); + harness.run_steps(5); + assert!(harness.query_by_role_and_label(egui::accesskit::Role::CheckBox, HIDDEN).is_some(), "the gear opens the category's tool list"); + assert!(harness.state().ui.collapsed_groups.is_empty(), "the gear itself must not collapse the category"); +} + +#[test] +fn the_options_menu_hides_a_tool_and_remembers_the_choice() { + let mut harness = harness(); + assert!(harness.query_by_label(HIDDEN).is_some(), "the palette starts with every tool of the category"); + + harness.get_by_label("Draw options").click(); + harness.run_steps(5); + harness.get_by_role_and_label(egui::accesskit::Role::CheckBox, HIDDEN).click(); + harness.run_steps(5); + + assert_eq!(harness.state().ui.hidden_tools, vec!["revcloud".to_string()], "the choice is kept in the UI state"); +} + +#[test] +fn a_hidden_tool_leaves_the_palette_and_show_all_puts_it_back() { + let mut harness = harness(); + harness.state_mut().ui.hidden_tools.push(HIDDEN_CMD.to_string()); + harness.run_steps(5); + assert!(harness.query_by_label(HIDDEN).is_none(), "a hidden tool leaves the palette"); + assert!(harness.query_by_label("Construction Line").is_some(), "the rest of the category stays"); + + harness.get_by_label("Draw options").click(); + harness.run_steps(5); + harness.get_by_label("Show All").click(); + harness.run_steps(5); + + assert!(harness.state().ui.hidden_tools.is_empty(), "Show all empties the list"); + assert!(harness.query_by_role_and_label(egui::accesskit::Role::Button, HIDDEN).is_some(), "the tool is back in the palette"); +}