From b1d2ac2744ab1a790f6bbce205a9269bcec65c9b Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Thu, 10 Sep 2026 08:18:23 +0300 Subject: [PATCH] Give the Options dialog a tab rail that can hold more pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dialog laid its pages out as a row of buttons across the top, which is what AutoCAD does and what fits while there are four of them. The pages this series adds take it to nine, and the names are translated into 21 languages — a row of nine German or Russian page names does not fit at any dialog width worth having. The rail is now a vertical list down the left, the dialog is 880×620 rather than 540×560, and the content pane scrolls on its own. Which page was open is now remembered. With four pages, reopening on General was a shrug; with nine it means hunting for the one you were last in. The ribbon's Options tool has existed all along — `ToolDef`, icon and all — but was marked `#[allow(dead_code)]` and never added to a group, so the dialog could only be reached by typing OPTIONS or from the start page. It now sits in Manage ▸ Application, beside About. No settings change in this commit; it is only the shell the rest needs. Co-Authored-By: Claude Opus 5 --- src/app/settings.rs | 3 +++ src/app/update/file.rs | 2 ++ src/app/view/modal.rs | 4 +-- src/modules/manage/mod.rs | 8 +++++- src/modules/manage/options.rs | 1 - src/ui/window/options.rs | 49 +++++++++++++++++++++++++---------- 6 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/app/settings.rs b/src/app/settings.rs index bdfab8ff9..c66b0c1bb 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -170,6 +170,8 @@ pub struct UserSettings { /// GRIPOBJLIMIT: past this many selected objects, no grips are drawn at /// all. 0 means no limit. The drawing header carries no slot for it. pub grip_object_limit: i32, + /// Which Options page was showing when the dialog was last closed. + pub options_tab: crate::ui::window::options::OptionsTab, /// CURSORTYPE: crosshair or the platform pointer over the drawing. pub cursor_type: CursorType, /// Explicit crosshair RGB. `None` keeps automatic background contrast. @@ -325,6 +327,7 @@ impl Default for UserSettings { zoom_factor: 60, cursor_size: 5, pick_box: 3, + options_tab: crate::ui::window::options::OptionsTab::General, double_click_block_refedit: false, double_click_block_attedit: true, grip_object_limit: DEFAULT_GRIP_OBJECT_LIMIT, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 771b2603b..bc4c020cd 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -365,6 +365,7 @@ impl OpenCADStudio { zoom_factor: self.zoom_factor, cursor_size: self.cursor_size, pick_box: self.pick_box, + options_tab: self.options_tab, double_click_block_refedit: self.double_click_block_refedit, double_click_block_attedit: self.double_click_block_attedit, grip_object_limit: self.grip_object_limit, @@ -422,6 +423,7 @@ impl OpenCADStudio { self.zoom_factor = s.zoom_factor.clamp(3, 100); self.cursor_size = s.cursor_size.clamp(1, 100); self.pick_box = s.pick_box.clamp(0, 50); + self.options_tab = s.options_tab; self.double_click_block_refedit = s.double_click_block_refedit; self.double_click_block_attedit = s.double_click_block_attedit; self.grip_object_limit = s.grip_object_limit.clamp(0, 32767); diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 9d116889e..0c27f9278 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -175,8 +175,8 @@ impl OpenCADStudio { } super::super::ModalKind::Options => sized_flow( ex, - 540, - 560, + 880, + 620, |flow| { crate::ui::window::options::view_window( &self.default_save_format, diff --git a/src/modules/manage/mod.rs b/src/modules/manage/mod.rs index f1a66e7aa..48fda655e 100644 --- a/src/modules/manage/mod.rs +++ b/src/modules/manage/mod.rs @@ -79,7 +79,13 @@ impl CadModule for ManageModule { // ── Application ─────────────────────────────────────────────────── RibbonGroup { title: "Application", - tools: vec![RibbonItem::LargeTool(about::tool())], + tools: vec![ + // The tool and its icon already existed but were never + // added to a group, so the dialog could only be reached + // by typing OPTIONS or from the start page. + RibbonItem::LargeTool(options::tool()), + RibbonItem::LargeTool(about::tool()), + ], }, ] }) diff --git a/src/modules/manage/options.rs b/src/modules/manage/options.rs index 615f31d0a..75bea9afc 100644 --- a/src/modules/manage/options.rs +++ b/src/modules/manage/options.rs @@ -1,5 +1,4 @@ use crate::modules::{IconKind, ModuleEvent, ToolDef}; -#[allow(dead_code)] pub fn tool() -> ToolDef { ToolDef { id: "OPTIONS", diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs index 7f3aa32d6..86325e844 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -7,7 +7,21 @@ use iced::widget::{ use iced::{Background, Border, Element, Fill, Theme}; use std::fmt; -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +/// Width of the vertical tab rail. Wide enough for the longest translated +/// page name; German and Russian are the ones that set it. +const TAB_RAIL_WIDTH: f32 = 178.0; +/// The dialog's own size when the modal is laid out intrinsically. +const DIALOG_WIDTH: f32 = 880.0; +const DIALOG_HEIGHT: f32 = 620.0; + +/// Which page of the Options dialog is showing. +/// +/// Persisted: with nine pages, reopening on General every time means hunting +/// for the one you were last in. +#[derive( + Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, +)] +#[serde(rename_all = "kebab-case")] pub enum OptionsTab { #[default] General, @@ -841,38 +855,45 @@ pub fn view_window<'a>( OptionsTab::Drawing => drawing.into(), }; + // A vertical rail rather than a horizontal strip: the tab names are + // translated into 21 languages, and a row of them stops fitting long + // before the list of pages is complete. let tab_button = |label, tab| { let selected = active_tab == tab; - button(text(label).size(12)) + button(text(label).size(12.5)) .on_press(Message::OptionsTabChanged(tab)) - .padding([6, 14]) - .style(if selected { button::primary } else { button::secondary }) + .padding([7, 12]) + .width(Fill) + .style(if selected { button::primary } else { button::text }) }; - let tabs = row![ + let tabs = column![ tab_button(crate::t!("General"), OptionsTab::General), tab_button(crate::t!("Display"), OptionsTab::Display), tab_button(crate::t!("Selection"), OptionsTab::Selection), tab_button(crate::t!("Drawing"), OptionsTab::Drawing), ] - .spacing(6); + .spacing(2) + .width(iced::Length::Fixed(TAB_RAIL_WIDTH)); - let body = column![ - tabs, - Space::new().height(12), + let pane = column![ // Keep the scrollbar in its own lane instead of floating over the // controls at the trailing edge of the Options content. - scrollable(content).spacing(8).height(sizing.height), + scrollable(content).spacing(8).height(Fill), Space::new().height(12), - row![Space::new().width(sizing.width), close], + row![Space::new().width(Fill), close], ] - .width(sizing.width) + .width(Fill) .height(sizing.height); + let body = row![tabs, Space::new().width(18), pane] + .width(sizing.width) + .height(sizing.height); + let intrinsic = sizing.width == crate::ui::modal::ModalSizing::INTRINSIC.width; container(body) .style(container::rounded_box) .padding([16, 18]) - .width(if intrinsic { iced::Length::Fixed(540.0) } else { sizing.width }) - .height(if intrinsic { iced::Length::Fixed(560.0) } else { sizing.height }) + .width(if intrinsic { iced::Length::Fixed(DIALOG_WIDTH) } else { sizing.width }) + .height(if intrinsic { iced::Length::Fixed(DIALOG_HEIGHT) } else { sizing.height }) .into() }