fix(ribbon): highlight toggle buttons by their actual state

The UCS Icon, Properties, File Tabs, and Layout Tabs buttons were highlighted unconditionally because their ids fell through to an active_tool equality check in is_active_tool, which activate_tool sets on every click and never clears. Each id is now mapped to its corresponding visibility flag (show_ucs_icon, show_properties, show_file_tabs, show_layout_tabs), and those flags are kept in sync from their toggle handlers so the highlight reflects live state. The per-toggle booleans previously threaded through the render path are consolidated into a single ToggleState struct read from one place, so introducing a further toggle no longer requires changing every render-function signature and call site.
This commit is contained in:
Karim Jerbi 2026-07-12 00:56:15 +01:00 committed by Hakan Seven
commit 8ef65f59f8
4 changed files with 88 additions and 54 deletions

View file

@ -81,6 +81,7 @@ impl OpenCADStudio {
let at_origin = sub == "ORIGIN";
// Update model-space icon flags.
self.show_ucs_icon = visible;
self.ribbon.set_ucs_icon(visible);
if sub == "NOORIGIN" || sub == "ORIGIN" {
self.ucs_icon_at_origin = at_origin;
}
@ -104,6 +105,7 @@ impl OpenCADStudio {
self.push_undo_snapshot(i, "UCSICON");
let visible = !self.show_ucs_icon;
self.show_ucs_icon = visible;
self.ribbon.set_ucs_icon(visible);
for entity in self.tabs[i].scene.document.entities_mut() {
if let acadrust::EntityType::Viewport(vp) = entity {
vp.status.ucs_icon_visible = visible;

View file

@ -1567,14 +1567,17 @@ impl OpenCADStudio {
}
Message::ToggleProperties => {
self.show_properties ^= true;
self.ribbon.set_properties(self.show_properties);
Task::none()
}
Message::ToggleFileTabs => {
self.show_file_tabs ^= true;
self.ribbon.set_file_tabs(self.show_file_tabs);
Task::none()
}
Message::ToggleLayoutTabs => {
self.show_layout_tabs ^= true;
self.ribbon.set_layout_tabs(self.show_layout_tabs);
Task::none()
}
Message::ToggleOTrack => {

View file

@ -37,6 +37,14 @@ pub struct Ribbon {
pub ortho_mode: bool,
/// ViewCube (NAVVCUBE) visibility — drives the View Cube button highlight.
pub show_viewcube: bool,
/// UCS icon (UCSICON) visibility — drives the UCS Icon button highlight.
pub show_ucs_icon: bool,
/// Properties panel (PROPERTIES) visibility — drives the Properties button highlight.
pub show_properties: bool,
/// File tabs (FILETAB) visibility — drives the File Tabs button highlight.
pub show_file_tabs: bool,
/// Layout tabs (LAYOUTTAB) visibility — drives the Layout Tabs button highlight.
pub show_layout_tabs: bool,
pub open_dropdown: Option<String>,
/// Title of the collapsed panel whose flyout is currently open, if any.
pub collapsed_open: Option<String>,
@ -136,6 +144,10 @@ impl Ribbon {
wireframe: false,
ortho_mode: true,
show_viewcube: true,
show_ucs_icon: true,
show_properties: true,
show_file_tabs: true,
show_layout_tabs: true,
open_dropdown: None,
collapsed_open: None,
last_panel_tool: HashMap::default(),
@ -280,6 +292,31 @@ impl Ribbon {
pub fn set_viewcube(&mut self, on: bool) {
self.show_viewcube = on;
}
pub fn set_ucs_icon(&mut self, on: bool) {
self.show_ucs_icon = on;
}
pub fn set_properties(&mut self, on: bool) {
self.show_properties = on;
}
pub fn set_file_tabs(&mut self, on: bool) {
self.show_file_tabs = on;
}
pub fn set_layout_tabs(&mut self, on: bool) {
self.show_layout_tabs = on;
}
/// Snapshot of every ribbon toggle's live state, for the render path.
fn toggle_state(&self) -> widgets::ToggleState {
use widgets::ToggleState;
ToggleState {
wireframe: self.wireframe,
ortho_mode: self.ortho_mode,
show_viewcube: self.show_viewcube,
show_ucs_icon: self.show_ucs_icon,
show_properties: self.show_properties,
show_file_tabs: self.show_file_tabs,
show_layout_tabs: self.show_layout_tabs,
}
}
pub fn toggle_dropdown(&mut self, id: &str) {
if self.open_dropdown.as_deref() == Some(id) {
@ -532,7 +569,9 @@ impl Ribbon {
// button. See `CollapsePanels`.
let panels: Vec<Panel<'_>> = groups
.iter()
.map(|g| Panel {
.map(|g| {
let ts = self.toggle_state();
Panel {
id: g.title.to_string(),
full: render_group(
false,
@ -540,9 +579,7 @@ impl Ribbon {
&self.active_tool,
&self.open_dropdown,
&self.last_cmd,
self.wireframe,
self.ortho_mode,
self.show_viewcube,
ts,
&self.layer_infos,
&self.active_layer,
self.active_color,
@ -556,9 +593,7 @@ impl Ribbon {
&self.active_tool,
&self.open_dropdown,
&self.last_cmd,
self.wireframe,
self.ortho_mode,
self.show_viewcube,
ts,
&self.layer_infos,
&self.active_layer,
self.active_color,
@ -572,9 +607,7 @@ impl Ribbon {
&self.active_tool,
&self.open_dropdown,
&self.last_cmd,
self.wireframe,
self.ortho_mode,
self.show_viewcube,
ts,
&self.layer_infos,
&self.active_layer,
self.active_color,
@ -589,9 +622,7 @@ impl Ribbon {
&self.active_tool,
&self.open_dropdown,
&self.last_cmd,
self.wireframe,
self.ortho_mode,
self.show_viewcube,
ts,
&self.layer_infos,
&self.active_layer,
self.active_color,
@ -606,9 +637,7 @@ impl Ribbon {
&self.active_tool,
&self.open_dropdown,
&self.last_cmd,
self.wireframe,
self.ortho_mode,
self.show_viewcube,
ts,
&self.layer_infos,
&self.active_layer,
self.active_color,
@ -626,6 +655,7 @@ impl Ribbon {
..Default::default()
})
.into(),
}
})
.collect();
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H, BORDER_DARK)
@ -1300,9 +1330,7 @@ fn render_group<'a>(
active_tool: &Option<String>,
open_dd: &Option<String>,
last_cmd: &HashMap<&'static str, &'static str>,
wireframe: bool,
ortho_mode: bool,
show_viewcube: bool,
state: widgets::ToggleState,
layer_infos: &'a [LayerInfo],
active_layer: &'a str,
active_color: AcadColor,
@ -1329,9 +1357,7 @@ fn render_group<'a>(
active_tool,
open_dd,
last_cmd,
wireframe,
ortho_mode,
show_viewcube,
state,
layer_infos,
active_layer,
active_color,
@ -1346,9 +1372,7 @@ fn render_group<'a>(
active_tool,
open_dd,
last_cmd,
wireframe,
ortho_mode,
show_viewcube,
state,
));
if small_buf.len() == 3 {
flush_small_col(&mut small_buf, &mut items_row);
@ -1426,9 +1450,7 @@ fn collapse_button<'a>(
active_tool: &Option<String>,
open_dd: &Option<String>,
last_cmd: &HashMap<&'static str, &'static str>,
wireframe: bool,
ortho_mode: bool,
show_viewcube: bool,
state: widgets::ToggleState,
layer_infos: &'a [LayerInfo],
active_layer: &'a str,
active_color: AcadColor,
@ -1494,9 +1516,7 @@ fn collapse_button<'a>(
active_tool,
open_dd,
last_cmd,
wireframe,
ortho_mode,
show_viewcube,
state,
layer_infos,
active_layer,
active_color,
@ -1510,9 +1530,7 @@ fn collapse_button<'a>(
active_tool,
open_dd,
last_cmd,
wireframe,
ortho_mode,
show_viewcube,
state,
layer_infos,
active_layer,
active_color,

View file

@ -19,6 +19,21 @@ use crate::ui::properties::{acad_color_display, LwItem};
use super::LayerInfo;
/// Live on/off state of every ribbon toggle button. Passed as a single value
/// through the render path so adding a new toggle only touches `is_active_tool`
/// plus the `Ribbon::toggle_state` builder — not every render-function signature
/// and call site.
#[derive(Clone, Copy)]
pub(super) struct ToggleState {
pub wireframe: bool,
pub ortho_mode: bool,
pub show_viewcube: bool,
pub show_ucs_icon: bool,
pub show_properties: bool,
pub show_file_tabs: bool,
pub show_layout_tabs: bool,
}
// ── Layout constants (single source of truth: ROW_H from ui::mod) ─────────
use crate::ui::ROW_H;
@ -287,16 +302,18 @@ pub(super) fn make_icon(icon: IconKind, size: f32) -> Element<'static, Message>
pub(super) fn is_active_tool(
id: &str,
active_tool: &Option<String>,
wireframe: bool,
ortho_mode: bool,
show_viewcube: bool,
state: &ToggleState,
) -> bool {
match id {
"WIREFRAME" => wireframe,
"SOLID" => !wireframe,
"ORTHO" => ortho_mode,
"PERSP" => !ortho_mode,
"NAVVCUBE" => show_viewcube,
"WIREFRAME" => state.wireframe,
"SOLID" => !state.wireframe,
"ORTHO" => state.ortho_mode,
"PERSP" => !state.ortho_mode,
"NAVVCUBE" => state.show_viewcube,
"UCSICON" => state.show_ucs_icon,
"PROPERTIES" => state.show_properties,
"FILETAB" => state.show_file_tabs,
"LAYOUTTAB" => state.show_layout_tabs,
id => active_tool.as_deref() == Some(id),
}
}
@ -349,15 +366,13 @@ pub(super) fn render_small<'a>(
active_tool: &Option<String>,
open_dd: &Option<String>,
last_cmd: &HashMap<&'static str, &'static str>,
wireframe: bool,
ortho_mode: bool,
show_viewcube: bool,
state: ToggleState,
) -> Element<'a, Message> {
match item {
// Large variants render small too, so the ribbon can shrink a panel of
// large buttons to icon-only columns when the width is tight.
RibbonItem::Tool(t) | RibbonItem::LargeTool(t) => {
let active = is_active_tool(t.id, active_tool, wireframe, ortho_mode, show_viewcube);
let active = is_active_tool(t.id, active_tool, &state);
let event = t.event.clone();
let tool_id = t.id.to_string();
let tip_text = format!("{}\nCommand: {}", t.label, t.id);
@ -585,9 +600,7 @@ pub(super) fn render_large<'a>(
active_tool: &Option<String>,
open_dd: &Option<String>,
last_cmd: &HashMap<&'static str, &'static str>,
wireframe: bool,
ortho_mode: bool,
show_viewcube: bool,
state: ToggleState,
layer_infos: &'a [LayerInfo],
active_layer: &'a str,
active_color: AcadColor,
@ -601,7 +614,7 @@ pub(super) fn render_large<'a>(
// A plain Tool renders large too, so a collapsed panel can show its
// representative tool as a big icon.
RibbonItem::LargeTool(t) | RibbonItem::Tool(t) => {
let active = is_active_tool(t.id, active_tool, wireframe, ortho_mode, show_viewcube);
let active = is_active_tool(t.id, active_tool, &state);
let event = t.event.clone();
let tool_id = t.id.to_string();
let tip_text = format!("{}\nCommand: {}", t.label, t.id);
@ -785,12 +798,10 @@ pub(super) fn render_large<'a>(
active_tool,
open_dd,
last_cmd,
wireframe,
ortho_mode,
show_viewcube,
state,
)
} else {
let mp_active = is_active_tool(match_prop.id, active_tool, wireframe, ortho_mode, show_viewcube);
let mp_active = is_active_tool(match_prop.id, active_tool, &state);
let mp_event = match_prop.event.clone();
let mp_id = match_prop.id.to_string();
let mp_tip = format!("{}\nCommand: {}", match_prop.label, match_prop.id);