From 71837895b019983be1ffd7651a26feb470d8098f Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 12 Jun 2026 02:07:26 +0300 Subject: [PATCH] feat(ui): right-edge side toolbar; drop the paper-space ribbon tab Switching to a paper layout used to hijack the ribbon with a contextual "Layout" tab. Replace that with a reusable right-edge vertical toolbar: a single centred column of icon buttons floated over the canvas, built from a flat ToolDef list and dispatching through the existing RibbonToolClick path (ui::side_toolbar). Paper space now shows Viewport / Page Setup / Export PDF there and leaves the ribbon on the user's tab; the contextual layout tab and its three auto-switch hooks are removed. The toolbar is generic infrastructure for future context action sets. Co-Authored-By: Claude Opus 4.8 --- src/app/update.rs | 19 ++----- src/app/view.rs | 10 ++++ src/modules/layout/mod.rs | 20 +++++++ src/ui/mod.rs | 1 + src/ui/ribbon/mod.rs | 4 +- src/ui/side_toolbar.rs | 108 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 145 insertions(+), 17 deletions(-) create mode 100644 src/ui/side_toolbar.rs diff --git a/src/app/update.rs b/src/app/update.rs index b14b6d6d3..fdcdd3987 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -5005,13 +5005,9 @@ impl OpenCADStudio { self.tabs[i].scene.deselect_all(); self.tabs[i].scene.restore_saved_camera(); self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation; - if going_to_paper { - if let Some(idx) = self.ribbon.layout_module_index() { - self.ribbon.select(idx); - } - } else if self.ribbon.active_is_layout() { - self.ribbon.select(0); - } + // Paper-space tools live in the right-edge side toolbar now, so + // entering/leaving a layout no longer hijacks the ribbon tab. + let _ = going_to_paper; // Refresh VP freeze columns for the new layout. let doc_layers = self.tabs[i].scene.document.layers.clone(); let vp_info = self.tabs[i].scene.viewport_list(); @@ -5054,9 +5050,6 @@ impl OpenCADStudio { self.tabs[i].scene.ensure_sheet_viewport(&new_name); self.tabs[i].scene.deselect_all(); self.tabs[i].scene.fit_all(); - if let Some(idx) = self.ribbon.layout_module_index() { - self.ribbon.select(idx); - } self.command_line.push_output(&format!( "Layout \"{new_name}\" created — use MVIEW to add a viewport" )); @@ -5075,12 +5068,6 @@ impl OpenCADStudio { if self.tabs[i].scene.delete_layout(&name) { self.layout_context_menu = None; self.layout_rename_state = None; - // If we fell back to Model space, update ribbon. - if self.tabs[i].scene.current_layout == "Model" - && self.ribbon.active_is_layout() - { - self.ribbon.select(0); - } self.command_line .push_output(&format!("Layout \"{name}\" silindi")); self.tabs[i].dirty = true; diff --git a/src/app/view.rs b/src/app/view.rs index b4642a472..19a6a61f1 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -1119,6 +1119,16 @@ impl OpenCADStudio { } } + // Paper-space context actions: a right-edge vertical toolbar + // (viewport / page setup / plot) instead of a contextual ribbon tab. + if is_paper && !tab.is_start { + if let Some(tb) = crate::ui::side_toolbar::view( + &crate::modules::layout::paper_space_tools(), + ) { + viewport_stack = viewport_stack.push(tb); + } + } + // Quick Properties: compact floating property panel on selection, // anchored at the canvas top-left so it doesn't track the cursor. if self.quick_properties && !tab.is_start { diff --git a/src/modules/layout/mod.rs b/src/modules/layout/mod.rs index 3acda001b..b126ff1bd 100644 --- a/src/modules/layout/mod.rs +++ b/src/modules/layout/mod.rs @@ -6,6 +6,26 @@ pub mod vplayer; use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, ToolDef}; +/// Paper-space context tools, as a flat list for the right-edge side toolbar +/// (the contextual ribbon tab is no longer shown). Viewport + plot actions. +pub fn paper_space_tools() -> Vec { + vec![ + mview::tool(), + ToolDef { + id: "PAGESETUP", + label: "Page Setup", + icon: IconKind::Glyph("📋"), + event: ModuleEvent::Command("PAGESETUP".to_string()), + }, + ToolDef { + id: "PLOT", + label: "Export PDF", + icon: IconKind::Glyph("🖨"), + event: ModuleEvent::Command("PLOT".to_string()), + }, + ] +} + pub struct LayoutModule; impl CadModule for LayoutModule { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index c1fe9fba9..6bfc3d1b0 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -20,6 +20,7 @@ pub mod properties; pub mod ribbon; pub mod scale_popup; pub mod selection_filter_popup; +pub mod side_toolbar; pub mod shortcuts; pub mod snap_popup; pub mod statusbar; diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index 1f057288b..58a6dad81 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -246,7 +246,9 @@ impl Ribbon { .align_y(iced::Center) .spacing(6), |row_acc, (i, module)| { - if module.id() == "layout" && !is_paper { + // The Layout module no longer has a ribbon tab — its paper-space + // tools live in the right-edge side toolbar (see ui::side_toolbar). + if module.id() == "layout" { return row_acc; } diff --git a/src/ui/side_toolbar.rs b/src/ui/side_toolbar.rs new file mode 100644 index 000000000..9a3349c7f --- /dev/null +++ b/src/ui/side_toolbar.rs @@ -0,0 +1,108 @@ +//! Reusable right-edge vertical toolbar. +//! +//! A single centred column of icon buttons floated over the right edge of the +//! canvas — a lightweight, context-specific alternative to a contextual ribbon +//! tab. It is built from a flat list of [`ToolDef`]s and dispatches each tool's +//! command through the existing [`Message::RibbonToolClick`] path, so any +//! module's tools can drive it. First used for paper-space viewport / plot +//! actions; reusable for any future context action set. + +use iced::widget::{button, column, container, svg, text, tooltip}; +use iced::{Background, Border, Color, Element, Length, Theme}; + +use crate::app::Message; +use crate::modules::{IconKind, ToolDef}; + +const PANEL_BG: Color = Color { r: 0.13, g: 0.13, b: 0.13, a: 0.96 }; +const PANEL_BORDER: Color = Color { r: 0.32, g: 0.32, b: 0.32, a: 1.0 }; +const BTN_HOVER: Color = Color { r: 0.24, g: 0.24, b: 0.24, a: 1.0 }; +const ICON_SIZE: f32 = 22.0; +const BTN_SIZE: f32 = 38.0; +/// Gap between the toolbar and the right edge of the canvas. +const EDGE_MARGIN: f32 = 8.0; + +fn icon_el(icon: IconKind) -> Element<'static, Message> { + match icon { + IconKind::Glyph(s) => text(s).size(ICON_SIZE * 0.85).color(Color::WHITE).into(), + IconKind::Svg(bytes) => svg(svg::Handle::from_memory(bytes)) + .width(Length::Fixed(ICON_SIZE)) + .height(Length::Fixed(ICON_SIZE)) + .into(), + } +} + +fn tip_panel(label: &'static str) -> Element<'static, Message> { + container(text(label).size(11).color(Color::WHITE)) + .padding([2, 6]) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(PANEL_BG)), + border: Border { + color: PANEL_BORDER, + width: 1.0, + radius: 3.0.into(), + }, + ..Default::default() + }) + .into() +} + +/// Build the floating right-edge vertical toolbar from `tools`, vertically +/// centred over the canvas. Returns `None` when `tools` is empty so the caller +/// can skip pushing an overlay. +pub fn view(tools: &[ToolDef]) -> Option> { + if tools.is_empty() { + return None; + } + + let mut col = column![].spacing(4).align_x(iced::Center); + for t in tools { + let btn = button(icon_el(t.icon)) + .on_press(Message::RibbonToolClick { + tool_id: t.id.to_string(), + event: t.event.clone(), + }) + .width(Length::Fixed(BTN_SIZE)) + .height(Length::Fixed(BTN_SIZE)) + .style(|_: &Theme, status| button::Style { + background: Some(Background::Color(match status { + button::Status::Hovered | button::Status::Pressed => BTN_HOVER, + _ => Color::TRANSPARENT, + })), + border: Border { + radius: 3.0.into(), + ..Default::default() + }, + text_color: Color::WHITE, + ..Default::default() + }); + // Label tooltip on the left so it never runs off the right edge. + col = col.push( + tooltip(btn, tip_panel(t.label), tooltip::Position::Left).gap(6), + ); + } + + let panel = container(col).padding(4).style(|_: &Theme| container::Style { + background: Some(Background::Color(PANEL_BG)), + border: Border { + color: PANEL_BORDER, + width: 1.0, + radius: 5.0.into(), + }, + ..Default::default() + }); + + // Fill the canvas, pin the panel to the right edge and centre it + // vertically — no manual size/position math needed. + Some( + container(iced::widget::opaque(panel)) + .width(Length::Fill) + .height(Length::Fill) + .align_x(iced::Right) + .align_y(iced::Center) + .padding(iced::Padding { + right: EDGE_MARGIN, + ..iced::Padding::ZERO + }) + .into(), + ) +}