Tool Sets: the gear on a category header opens that category's options (#620)

Squash of PR #620 by @switch87.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Gert Pellin - GPEandClaude Opus 5.5 authored and GitHub committed 2026-10-11 19:53:19 -04:00
1 parent 9b3674e01f
commit 4e276db858
5 files changed
+126 -2

No files matched your search

+5
View File
@@ -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
+2
View File
@@ -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 Показати/приховати товщину ліній
1 # CADCraft Ukrainian interface catalog — original translations by @dmatviichuk.
840
841
842
843
844
845
846
847
+4
View File
@@ -77,6 +77,9 @@ pub struct UiState {
/// "Drafting" or "Modeling".
pub toolset_tab: String,
pub collapsed_groups: Vec<String>,
/// Tools the user took out of the Tool Sets palette (command ids); each category's gear
/// menu puts them back.
pub hidden_tools: Vec<String>,
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,
+35 -2
View File
@@ -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<String> = 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<Tool> = 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);
+80
View File
@@ -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");
}