From deaecdfb1e042ff16b792a9a224c102c6ff7d7d5 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 5 Aug 2026 00:52:52 +0300 Subject: [PATCH] fix(ribbon): mask tools on start Refs #494 Refs #507 --- src/app/update/mod.rs | 25 ++++++++-- src/app/view/mod.rs | 1 + src/ui/icons.rs | 12 ----- src/ui/ribbon/mod.rs | 59 ++++++++++++++++++---- src/ui/ribbon/widgets.rs | 104 ++++++--------------------------------- 5 files changed, 88 insertions(+), 113 deletions(-) diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 0c918b4f8..2cbaf8b56 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -1251,7 +1251,11 @@ impl OpenCADStudio { } Message::RibbonToolClick { tool_id, event } => { - self.on_ribbon_tool_click(tool_id, event) + if self.tabs[self.active_tab].is_start { + Task::none() + } else { + self.on_ribbon_tool_click(tool_id, event) + } } Message::PluginFileDialogResult { command, path } => { if let Some(path) = path { @@ -1310,6 +1314,9 @@ impl OpenCADStudio { self.stamp_header_sysvars(prev); } self.active_tab = idx; + if self.tabs[idx].is_start { + self.ribbon.close_dropdown(); + } if self.tabs[idx].is_start && matches!( self.active_modal, @@ -3430,11 +3437,19 @@ impl OpenCADStudio { // ── Ribbon dropdowns ────────────────────────────────────────── Message::ToggleRibbonDropdown(id) => { - self.ribbon.toggle_dropdown(&id); + if self.tabs[self.active_tab].is_start { + self.ribbon.close_dropdown(); + } else { + self.ribbon.toggle_dropdown(&id); + } Task::none() } Message::ToggleRibbonPanel(id) => { - self.ribbon.toggle_collapsed_panel(&id); + if self.tabs[self.active_tab].is_start { + self.ribbon.close_dropdown(); + } else { + self.ribbon.toggle_collapsed_panel(&id); + } Task::none() } Message::CloseRibbonDropdown => { @@ -3442,6 +3457,10 @@ impl OpenCADStudio { Task::none() } Message::DropdownSelectItem { dropdown_id, cmd } => { + if self.tabs[self.active_tab].is_start { + self.ribbon.close_dropdown(); + return Task::none(); + } self.ribbon.select_dropdown_item(dropdown_id, cmd); self.ribbon.activate_tool(cmd); self.dispatch_command(cmd) diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index d9bc2021a..56989b3b1 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1708,6 +1708,7 @@ impl OpenCADStudio { &history_dropdown_labels(&self.tabs[self.active_tab].history.undo_stack), &history_dropdown_labels(&self.tabs[self.active_tab].history.redo_stack), self.win_size, + self.tabs[self.active_tab].is_start, ) .unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into()); diff --git a/src/ui/icons.rs b/src/ui/icons.rs index 1bf048164..7c0828aae 100644 --- a/src/ui/icons.rs +++ b/src/ui/icons.rs @@ -223,18 +223,6 @@ pub fn semantic<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> { }) } -/// Render a disabled multi-colour tool icon without flattening its colour roles. -pub fn semantic_disabled<'a, M: 'a>( - bytes: &'static [u8], - size: f32, -) -> Element<'a, M> { - Element::new(SemanticIcon { - bytes, - size, - opacity: 0.42, - }) -} - fn semantic_handle(bytes: &'static [u8], theme: &Theme) -> svg::Handle { let key = SemanticCacheKey { address: bytes.as_ptr() as usize, diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index a002cf644..47a430539 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -321,7 +321,6 @@ impl Ribbon { fn toggle_state(&self) -> widgets::ToggleState { use widgets::ToggleState; ToggleState { - start_mode: false, wireframe: self.wireframe, ortho_mode: self.ortho_mode, show_viewcube: self.show_viewcube, @@ -403,11 +402,11 @@ impl Ribbon { ) -> Element<'_, Message> { // ── Quick-access file commands + undo/redo, one merged flow ──────── let lead = iced::widget::Row::with_children(vec![ - quick_access_btn(crate::ui::icons::DOC_NEW, "New", "NEW", is_start).into(), - quick_access_btn(crate::ui::icons::FOLDER_OPEN, "Open", "OPEN", is_start).into(), - quick_access_btn(crate::ui::icons::SAVE, "Save", "SAVE", is_start).into(), - quick_access_btn(crate::ui::icons::FILE_EXPORT, "Save As", "SAVEAS", is_start).into(), - quick_access_btn(crate::ui::icons::PRINT, "Print", "PRINT", is_start).into(), + quick_access_btn(crate::ui::icons::DOC_NEW, "New", "NEW").into(), + quick_access_btn(crate::ui::icons::FOLDER_OPEN, "Open", "OPEN").into(), + quick_access_btn(crate::ui::icons::SAVE, "Save", "SAVE").into(), + quick_access_btn(crate::ui::icons::FILE_EXPORT, "Save As", "SAVEAS").into(), + quick_access_btn(crate::ui::icons::PRINT, "Print", "PRINT").into(), render_history_control("Undo", UNDO_HISTORY_ID, undo_count, &self.open_dropdown).into(), render_history_control("Redo", REDO_HISTORY_ID, redo_count, &self.open_dropdown).into(), ]) @@ -580,8 +579,7 @@ impl Ribbon { let panels: Vec> = groups .iter() .map(|g| { - let mut ts = self.toggle_state(); - ts.start_mode = is_start; + let ts = self.toggle_state(); Panel { id: g.title.to_string(), full: render_group( @@ -680,7 +678,7 @@ impl Ribbon { text("").into() }; - let tool_bar = container(tool_area) + let tool_bar: Element<'_, Message> = container(tool_area) .style(|theme: &Theme| container::Style { background: Some(Background::Color( theme.palette().background.weakest.color, @@ -692,7 +690,44 @@ impl Ribbon { }, ..Default::default() }) - .width(Length::Fill); + .width(Length::Fill) + .into(); + + let tool_bar = if is_start { + // The Start tab has no drawing context. One theme-aware shield + // fades and blocks only the tool area; the tabs and application + // controls above it remain available and clearly identifiable. + let shield = mouse_area( + container(iced::widget::Space::new()) + .width(Fill) + .height(Fill) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme + .palette() + .background + .strongest + .color + .scale_alpha(0.58), + )), + ..Default::default() + }), + ) + .on_press(Message::CloseRibbonDropdown) + .interaction(iced::mouse::Interaction::Idle); + let shield = iced::widget::tooltip( + shield, + make_tip(t!("Open or create a drawing to use ribbon commands.").into_owned()), + iced::widget::tooltip::Position::Bottom, + ) + .gap(6.0) + .delay(std::time::Duration::from_millis(400)) + .style(tip_style); + + iced::widget::stack![tool_bar, iced::widget::opaque(shield)].into() + } else { + tool_bar + }; column![tab_bar, tool_bar].into() } @@ -704,7 +739,11 @@ impl Ribbon { undo_labels: &[String], redo_labels: &[String], win: (f32, f32), + is_start: bool, ) -> Option> { + if is_start { + return None; + } let open_id = self.open_dropdown.as_deref()?; if open_id == UNDO_HISTORY_ID || open_id == REDO_HISTORY_ID { diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs index 2f1550806..01ffd7886 100644 --- a/src/ui/ribbon/widgets.rs +++ b/src/ui/ribbon/widgets.rs @@ -33,9 +33,6 @@ use super::LayerInfo; /// and call site. #[derive(Clone, Copy)] pub(super) struct ToggleState { - /// Start (welcome) tab is active — tools whose command the start-tab - /// gate refuses render dimmed and read as unusable. - pub start_mode: bool, pub wireframe: bool, pub ortho_mode: bool, pub show_viewcube: bool, @@ -352,29 +349,6 @@ pub(super) fn make_icon(icon: IconKind, size: f32) -> Element<'static, Message> } } -/// Unusable on the Start tab: dim it. Mirrors the dispatch gate — the single -/// authority is `crate::app::commands::start_allowed`. -pub(super) fn start_dimmed(state: &ToggleState, event: &ModuleEvent) -> bool { - state.start_mode - && !matches!(event, ModuleEvent::Command(c) if crate::app::commands::start_allowed(c)) -} - -/// `make_icon`, faded when `dim` without flattening multi-colour SVGs. -pub(super) fn make_icon_dim(icon: IconKind, size: f32, dim: bool) -> Element<'static, Message> { - if !dim { - return make_icon(icon, size); - } - match icon { - IconKind::Glyph(s) => text(s) - .size(size * 0.7) - .style(|theme: &Theme| iced::widget::text::Style { - color: Some(theme.palette().background.base.text.scale_alpha(0.42)), - }) - .into(), - IconKind::Svg(bytes) => icons::semantic_disabled(bytes, size), - } -} - pub(super) fn is_active_tool( id: &str, active_tool: &Option, @@ -487,14 +461,6 @@ pub(super) fn muted_text_style(theme: &Theme) -> iced::widget::text::Style { } } -pub(super) fn tool_label_style(theme: &Theme, dim: bool) -> iced::widget::text::Style { - iced::widget::text::Style { - color: dim.then_some( - theme.palette().background.base.text.scale_alpha(0.42), - ), - } -} - // ── Tooltip helpers ──────────────────────────────────────────────────────── pub(super) fn make_tip(tip: String) -> Element<'static, Message> { @@ -530,11 +496,10 @@ pub(super) fn render_small<'a>( // 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, &state); - let dim = start_dimmed(&state, &t.event); let event = t.event.clone(); let tool_id = t.id.to_string(); let tip_text = format!("{}\n{} {}", t!(t.label), t!("Command:"), t.id); - let btn = button(make_icon_dim(t.icon, SMALL_ICON, dim)) + let btn = button(make_icon(t.icon, SMALL_ICON)) .on_press(Message::RibbonToolClick { tool_id, event }) .style(move |theme: &Theme, status| tool_btn_style(theme, active, status)) .width(Length::Fixed(SMALL_W)) @@ -561,15 +526,10 @@ pub(super) fn render_small<'a>( default, .. } => { - let dim = state.start_mode - && !items + let active = active_tool.as_deref() == Some(*id) + || items .iter() - .any(|(cmd, _, _)| crate::app::commands::start_allowed(cmd)); - let active = !dim - && (active_tool.as_deref() == Some(*id) - || items - .iter() - .any(|(cmd, _, _)| active_tool.as_deref() == Some(*cmd))); + .any(|(cmd, _, _)| active_tool.as_deref() == Some(*cmd)); let dd_open = open_dd.as_deref() == Some(*id); let last = last_cmd.get(id).copied().unwrap_or(*default); let cur_icon = last_cmd @@ -597,7 +557,7 @@ pub(super) fn render_small<'a>( .unwrap_or(*id); let tip_text = format!("{}\n{} {}", t!(cur_label), t!("Command:"), last); - let icon_btn = button(make_icon_dim(cur_icon, SMALL_ICON, dim)) + let icon_btn = button(make_icon(cur_icon, SMALL_ICON)) .on_press(Message::RibbonToolClick { tool_id: last.to_string(), event: ModuleEvent::Command(last.to_string()), @@ -659,13 +619,11 @@ pub(super) fn render_large_dropdown<'a>( active_tool: &Option, open_dd: &Option, last_cmd: &HashMap<&'static str, &'static str>, - dim: bool, ) -> Element<'a, Message> { - let active = !dim - && (active_tool.as_deref() == Some(id) - || items - .iter() - .any(|(cmd, _, _)| active_tool.as_deref() == Some(*cmd))); + let active = active_tool.as_deref() == Some(id) + || items + .iter() + .any(|(cmd, _, _)| active_tool.as_deref() == Some(*cmd)); let dd_open = open_dd.as_deref() == Some(id); let last = last_cmd.get(id).copied().unwrap_or(default); let cur_icon = last_cmd @@ -688,7 +646,7 @@ pub(super) fn render_large_dropdown<'a>( // it in all remaining space between the button top and the label. let top_btn = button( column![ - container(make_icon_dim(cur_icon, LARGE_ICON, dim)) + container(make_icon(cur_icon, LARGE_ICON)) .width(Fill) .height(Fill) .align_x(iced::Center) @@ -697,8 +655,7 @@ pub(super) fn render_large_dropdown<'a>( .size(10) .width(Fill) .align_x(iced::Center) - .wrapping(advanced_text::Wrapping::WordOrGlyph) - .style(move |theme: &Theme| tool_label_style(theme, dim)), + .wrapping(advanced_text::Wrapping::WordOrGlyph), ] .align_x(iced::Center) .spacing(0) @@ -773,14 +730,13 @@ pub(super) fn render_large<'a>( // representative tool as a big icon. RibbonItem::LargeTool(t) | RibbonItem::Tool(t) => { let active = is_active_tool(t.id, active_tool, &state); - let dim = start_dimmed(&state, &t.event); let event = t.event.clone(); let tool_id = t.id.to_string(); let label = t!(t.label).into_owned(); let tip_text = format!("{}\n{} {}", label, t!("Command:"), t.id); let btn = button( column![ - container(make_icon_dim(t.icon, LARGE_ICON, dim)) + container(make_icon(t.icon, LARGE_ICON)) .width(Fill) .height(Fill) .align_x(iced::Center) @@ -789,8 +745,7 @@ pub(super) fn render_large<'a>( .size(10) .width(Fill) .align_x(iced::Center) - .wrapping(advanced_text::Wrapping::WordOrGlyph) - .style(move |theme: &Theme| tool_label_style(theme, dim)), + .wrapping(advanced_text::Wrapping::WordOrGlyph), ] .align_x(iced::Center) .spacing(0) @@ -825,10 +780,6 @@ pub(super) fn render_large<'a>( items, default, } => { - let dim = state.start_mode - && !items - .iter() - .any(|(cmd, _, _)| crate::app::commands::start_allowed(cmd)); render_large_dropdown( *id, *icon, @@ -838,7 +789,6 @@ pub(super) fn render_large<'a>( active_tool, open_dd, last_cmd, - dim, ) } @@ -850,13 +800,8 @@ pub(super) fn render_large<'a>( items, default, } => { - let dim = state.start_mode - && !items - .iter() - .any(|(cmd, _, _)| crate::app::commands::start_allowed(cmd)); render_large_dropdown( *id, *icon, None, items, *default, active_tool, open_dd, last_cmd, - dim, ) } @@ -929,14 +874,9 @@ pub(super) fn render_large<'a>( .iter() .map(|t| { let is_active = active_tool.as_deref() == Some(t.id); - let dim = start_dimmed(&state, &t.event); let tip = t!(t.label); let event = t.event.clone(); - let icon_el: Element = if dim { - make_icon_dim(t.icon, 16.0, true) - } else { - make_icon(t.icon, 16.0) - }; + let icon_el: Element = make_icon(t.icon, 16.0); let msg = module_event_to_message(event); tooltip( button(icon_el) @@ -1131,14 +1071,9 @@ pub(super) fn render_large<'a>( .iter() .map(|t| { let is_active = active_tool.as_deref() == Some(t.id); - let dim = start_dimmed(&state, &t.event); let tip = t!(t.label); let event = t.event.clone(); - let icon_el: Element = if dim { - make_icon_dim(t.icon, 16.0, true) - } else { - make_icon(t.icon, 16.0) - }; + let icon_el: Element = make_icon(t.icon, 16.0); let msg = module_event_to_message(event); tooltip( button(icon_el) @@ -1209,17 +1144,10 @@ pub(super) fn quick_access_btn<'a>( icon_bytes: &'static [u8], label: &'static str, cmd: &'static str, - is_start: bool, ) -> Element<'a, Message> { // The bundled UI SVGs are black-stroked; tint them to a light chrome grey so // they read on the dark top strip (raw black is invisible there). - // On the Start tab, commands the start gate refuses render dimmed. - let dim = is_start && !crate::app::commands::start_allowed(cmd); - let icon = if dim { - icons::themed_disabled(icon_bytes, 16.0) - } else { - icons::themed(icon_bytes, 16.0) - }; + let icon = icons::themed(icon_bytes, 16.0); let btn = button( container(icon) .width(Fill)