diff --git a/src/app/drafting_settings.rs b/src/app/drafting_settings.rs new file mode 100644 index 000000000..0a6a58a6a --- /dev/null +++ b/src/app/drafting_settings.rs @@ -0,0 +1,170 @@ +use super::OpenCADStudio; +use crate::ui::window::drafting_settings::{DraftingSettingsState, DraftingSettingsTab}; + +impl DraftingSettingsState { + pub(crate) fn from_app(app: &OpenCADStudio) -> Self { + Self { + active_tab: DraftingSettingsTab::SnapAndGrid, + snap_on: app.snapper.grid_snap(), + grid_on: app.show_grid, + isometric: app.isometric_drafting, + iso_plane: app.iso_plane, + snap_angle_deg: app.snap_angle_deg, + polar_on: app.polar_mode, + ortho_on: app.ortho_mode, + polar_increment_deg: app.polar_increment_deg, + osnap_on: app.snapper.snap_enabled, + otrack_on: app.snapper.otrack_enabled, + snap_modes: app.snapper.enabled.clone(), + osnap3d_on: false, + dyn_input_on: app.dyn_input, + quick_props_on: app.quick_properties, + selection_cycling_on: app.selection_cycling, + } + } +} + +impl OpenCADStudio { + pub(super) fn drafting_settings_dirty(&self) -> bool { + match (&self.drafting_settings_state, &self.drafting_settings_saved) { + (Some(curr), Some(saved)) => curr.is_dirty(saved), + (Some(_), None) => true, + _ => false, + } + } + + pub(super) fn apply_drafting_settings(&mut self) { + if let Some(state) = &self.drafting_settings_state { + self.show_grid = state.grid_on; + self.snapper.grid_snap_on = state.snap_on; + self.isometric_drafting = state.isometric; + self.iso_plane = state.iso_plane; + self.snap_angle_deg = state.snap_angle_deg; + self.polar_mode = state.polar_on; + self.ortho_mode = state.ortho_on; + self.polar_increment_deg = state.polar_increment_deg; + self.snapper.snap_enabled = state.osnap_on; + self.snapper.otrack_enabled = state.otrack_on; + self.snapper.enabled = state.snap_modes.clone(); + self.dyn_input = state.dyn_input_on; + self.quick_properties = state.quick_props_on; + self.selection_cycling = state.selection_cycling_on; + self.sync_vport_display(self.active_tab); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_drafting_settings_tab_switching_not_dirty() { + let state1 = DraftingSettingsState { + active_tab: DraftingSettingsTab::SnapAndGrid, + snap_on: false, + grid_on: true, + isometric: false, + iso_plane: crate::app::settings::IsoPlane::Left, + snap_angle_deg: 0.0, + polar_on: false, + ortho_on: false, + polar_increment_deg: 15.0, + osnap_on: true, + otrack_on: false, + snap_modes: rustc_hash::FxHashSet::default(), + osnap3d_on: false, + dyn_input_on: false, + quick_props_on: false, + selection_cycling_on: false, + }; + let mut state2 = state1.clone(); + assert!(!state2.is_dirty(&state1)); + + // Switching tabs should NOT mark it dirty + state2.active_tab = DraftingSettingsTab::ObjectSnap; + assert!(!state2.is_dirty(&state1)); + + // Toggling a setting should mark it dirty + state2.grid_on = false; + assert!(state2.is_dirty(&state1)); + } + + #[test] + fn test_drafting_settings_dirty_toggles() { + let base = DraftingSettingsState { + active_tab: DraftingSettingsTab::SnapAndGrid, + snap_on: false, + grid_on: true, + isometric: false, + iso_plane: crate::app::settings::IsoPlane::Left, + snap_angle_deg: 0.0, + polar_on: false, + ortho_on: false, + polar_increment_deg: 15.0, + osnap_on: true, + otrack_on: false, + snap_modes: rustc_hash::FxHashSet::default(), + osnap3d_on: false, + dyn_input_on: false, + quick_props_on: false, + selection_cycling_on: false, + }; + + let mut modded = base.clone(); + modded.snap_on = true; + assert!(modded.is_dirty(&base)); + + let mut modded = base.clone(); + modded.isometric = true; + assert!(modded.is_dirty(&base)); + + let mut modded = base.clone(); + modded.iso_plane = crate::app::settings::IsoPlane::Top; + assert!(modded.is_dirty(&base)); + + let mut modded = base.clone(); + modded.snap_angle_deg = 45.0; + assert!(modded.is_dirty(&base)); + + let mut modded = base.clone(); + modded.polar_on = true; + assert!(modded.is_dirty(&base)); + + let mut modded = base.clone(); + modded.ortho_on = true; + assert!(modded.is_dirty(&base)); + + let mut modded = base.clone(); + modded.polar_increment_deg = 30.0; + assert!(modded.is_dirty(&base)); + + let mut modded = base.clone(); + modded.osnap_on = false; + assert!(modded.is_dirty(&base)); + + let mut modded = base.clone(); + modded.otrack_on = true; + assert!(modded.is_dirty(&base)); + + let mut modded = base.clone(); + modded.snap_modes.insert(crate::snap::SnapType::Endpoint); + assert!(modded.is_dirty(&base)); + + let mut modded = base.clone(); + modded.osnap3d_on = true; + assert!(modded.is_dirty(&base)); + + let mut modded = base.clone(); + modded.dyn_input_on = true; + assert!(modded.is_dirty(&base)); + + let mut modded = base.clone(); + modded.quick_props_on = true; + assert!(modded.is_dirty(&base)); + + let mut modded = base.clone(); + modded.selection_cycling_on = true; + assert!(modded.is_dirty(&base)); + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 6109b4ab9..58dc6f398 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -10,6 +10,7 @@ pub use automation::{export_headless, serve}; mod command_driver; pub(crate) mod commands; mod document; +mod drafting_settings; pub(crate) mod expr_eval; mod find_replace; pub(crate) mod helpers; @@ -414,6 +415,9 @@ pub(super) struct OpenCADStudio { win_size: (f32, f32), snapper: Snapper, snap_popup_open: bool, + drafting_settings_state: Option, + drafting_settings_saved: Option, + drafting_settings_close_confirm: bool, scale_popup_open: bool, /// True while the polar-tracking angle picker is open. polar_popup_open: bool, @@ -2609,6 +2613,29 @@ pub enum Message { SnapSelectAll, /// Disable all snap modes. SnapClearAll, + // ── Drafting Settings Dialog ────────────────────────────────────────── + DraftingSettingsTabChanged(crate::ui::window::drafting_settings::DraftingSettingsTab), + DraftingSettingsToggleGrid, + DraftingSettingsToggleSnap, + DraftingSettingsToggleIsometric, + DraftingSettingsSetIsoPlane(crate::app::settings::IsoPlane), + DraftingSettingsResetRotation, + DraftingSettingsTogglePolar, + DraftingSettingsToggleOrtho, + DraftingSettingsToggleOsnap, + DraftingSettingsToggleOtrack, + DraftingSettingsToggleSnapMode(crate::snap::SnapType), + DraftingSettingsSnapSelectAll, + DraftingSettingsSnapClearAll, + DraftingSettingsToggle3dOsnap, + DraftingSettingsToggleDynInput, + DraftingSettingsToggleQuickProps, + DraftingSettingsToggleSelCycling, + DraftingSettingsApply, + DraftingSettingsOk, + DraftingSettingsClose, + DraftingSettingsCloseDiscard, + DraftingSettingsCloseKeep, /// Toggle a ribbon dropdown open/closed. ToggleRibbonDropdown(String), /// Toggle a collapsed ribbon panel's flyout open/closed (by panel title). @@ -3509,6 +3536,9 @@ impl OpenCADStudio { win_size: (1280.0, 720.0), snapper: Snapper::default(), snap_popup_open: false, + drafting_settings_state: None, + drafting_settings_saved: None, + drafting_settings_close_confirm: false, scale_popup_open: false, polar_popup_open: false, polar_custom_input: String::new(), diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 2782556bf..737e865e6 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -171,6 +171,9 @@ impl OpenCADStudio { } if self.active_modal == Some(DraftingSettings) { self.snap_popup_open = false; + self.drafting_settings_close_confirm = false; + self.drafting_settings_state = None; + self.drafting_settings_saved = None; } #[cfg(not(target_arch = "wasm32"))] if self.active_modal == Some(FileInUse) { @@ -4025,9 +4028,17 @@ impl OpenCADStudio { } Message::ToggleSnapPopup => { if self.active_modal == Some(super::ModalKind::DraftingSettings) { + if !self.drafting_settings_close_confirm && self.drafting_settings_dirty() { + self.drafting_settings_close_confirm = true; + return Task::none(); + } self.close_active_modal(); self.snap_popup_open = false; } else { + let st = crate::ui::window::drafting_settings::DraftingSettingsState::from_app(self); + self.drafting_settings_saved = Some(st.clone()); + self.drafting_settings_state = Some(st); + self.drafting_settings_close_confirm = false; self.active_modal = Some(super::ModalKind::DraftingSettings); self.snap_popup_open = true; } @@ -4049,6 +4060,147 @@ impl OpenCADStudio { Task::none() } + // ── Drafting Settings Dialog ────────────────────────────────── + Message::DraftingSettingsTabChanged(tab) => { + if let Some(state) = &mut self.drafting_settings_state { + state.active_tab = tab; + } + Task::none() + } + Message::DraftingSettingsToggleGrid => { + if let Some(state) = &mut self.drafting_settings_state { + state.grid_on = !state.grid_on; + } + Task::none() + } + Message::DraftingSettingsToggleSnap => { + if let Some(state) = &mut self.drafting_settings_state { + state.snap_on = !state.snap_on; + } + Task::none() + } + Message::DraftingSettingsToggleIsometric => { + if let Some(state) = &mut self.drafting_settings_state { + state.isometric = !state.isometric; + } + Task::none() + } + Message::DraftingSettingsSetIsoPlane(plane) => { + if let Some(state) = &mut self.drafting_settings_state { + state.isometric = true; + state.iso_plane = plane; + } + Task::none() + } + Message::DraftingSettingsResetRotation => { + if let Some(state) = &mut self.drafting_settings_state { + state.snap_angle_deg = 0.0; + } + Task::none() + } + Message::DraftingSettingsTogglePolar => { + if let Some(state) = &mut self.drafting_settings_state { + state.polar_on = !state.polar_on; + } + Task::none() + } + Message::DraftingSettingsToggleOrtho => { + if let Some(state) = &mut self.drafting_settings_state { + state.ortho_on = !state.ortho_on; + } + Task::none() + } + Message::DraftingSettingsToggleOsnap => { + if let Some(state) = &mut self.drafting_settings_state { + state.osnap_on = !state.osnap_on; + } + Task::none() + } + Message::DraftingSettingsToggleOtrack => { + if let Some(state) = &mut self.drafting_settings_state { + state.otrack_on = !state.otrack_on; + } + Task::none() + } + Message::DraftingSettingsToggleSnapMode(snap_type) => { + if let Some(state) = &mut self.drafting_settings_state { + if !state.snap_modes.remove(&snap_type) { + state.snap_modes.insert(snap_type); + } + } + Task::none() + } + Message::DraftingSettingsSnapSelectAll => { + if let Some(state) = &mut self.drafting_settings_state { + for &(snap_type, _, _) in crate::snap::ALL_SNAP_MODES { + state.snap_modes.insert(snap_type); + } + } + Task::none() + } + Message::DraftingSettingsSnapClearAll => { + if let Some(state) = &mut self.drafting_settings_state { + state.snap_modes.clear(); + } + Task::none() + } + Message::DraftingSettingsToggle3dOsnap => { + if let Some(state) = &mut self.drafting_settings_state { + state.osnap3d_on = !state.osnap3d_on; + } + Task::none() + } + Message::DraftingSettingsToggleDynInput => { + if let Some(state) = &mut self.drafting_settings_state { + state.dyn_input_on = !state.dyn_input_on; + } + Task::none() + } + Message::DraftingSettingsToggleQuickProps => { + if let Some(state) = &mut self.drafting_settings_state { + state.quick_props_on = !state.quick_props_on; + } + Task::none() + } + Message::DraftingSettingsToggleSelCycling => { + if let Some(state) = &mut self.drafting_settings_state { + state.selection_cycling_on = !state.selection_cycling_on; + } + Task::none() + } + Message::DraftingSettingsApply => { + self.apply_drafting_settings(); + self.drafting_settings_saved = self.drafting_settings_state.clone(); + self.persist_settings_if_changed(); + Task::none() + } + Message::DraftingSettingsOk => { + self.apply_drafting_settings(); + self.drafting_settings_saved = self.drafting_settings_state.clone(); + self.drafting_settings_close_confirm = false; + self.persist_settings_if_changed(); + self.close_active_modal(); + Task::none() + } + Message::DraftingSettingsClose => { + if !self.drafting_settings_close_confirm && self.drafting_settings_dirty() { + self.drafting_settings_close_confirm = true; + return Task::none(); + } + self.drafting_settings_close_confirm = false; + self.close_active_modal(); + Task::none() + } + Message::DraftingSettingsCloseDiscard => { + self.drafting_settings_close_confirm = false; + self.close_active_modal(); + Task::none() + } + Message::DraftingSettingsCloseKeep => { + self.drafting_settings_close_confirm = false; + Task::none() + } + // ── Ribbon dropdowns ────────────────────────────────────────── Message::ToggleRibbonDropdown(id) => { if self.tabs[self.active_tab].is_start { @@ -6608,6 +6760,14 @@ impl OpenCADStudio { return Task::none(); } self.shortcut_close_confirm = false; + if self.active_modal == Some(super::ModalKind::DraftingSettings) + && !self.drafting_settings_close_confirm + && self.drafting_settings_dirty() + { + self.drafting_settings_close_confirm = true; + return Task::none(); + } + self.drafting_settings_close_confirm = false; let resume_open_queue = self.active_modal == Some(super::ModalKind::Recovery); self.close_active_modal(); if resume_open_queue { diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index dee88a495..13f3ea46e 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -283,25 +283,28 @@ impl OpenCADStudio { ) }, ), - super::super::ModalKind::DraftingSettings => sized_flow( - ex, - 520, - 560, - |flow| { - crate::ui::window::drafting_settings::view_window( - &self.snapper, - self.show_grid, - self.snapper.grid_snap(), - self.ortho_mode, - self.polar_mode, - self.snapper.otrack_enabled, - self.isometric_drafting, - self.iso_plane, - self.snap_angle_deg, - flow, - ) - }, - ), + super::super::ModalKind::DraftingSettings => { + let state = self.drafting_settings_state.as_ref(); + let dirty = self.drafting_settings_dirty(); + let confirm = self.drafting_settings_close_confirm; + sized_flow( + ex, + 700, + 480, + |flow| { + if let Some(state) = state { + crate::ui::window::drafting_settings::view_window( + state, + dirty, + confirm, + flow, + ) + } else { + iced::widget::Space::new().into() + } + }, + ) + } super::super::ModalKind::FindReplace => automatic_flow(ex, |flow| { crate::ui::window::find_replace::view_window( &self.find_replace.search, diff --git a/src/ui/window/drafting_settings.rs b/src/ui/window/drafting_settings.rs index e760ac757..2ea71b51c 100644 --- a/src/ui/window/drafting_settings.rs +++ b/src/ui/window/drafting_settings.rs @@ -1,22 +1,107 @@ use crate::app::settings::IsoPlane; use crate::app::Message; -use crate::snap::{Snapper, ALL_SNAP_MODES}; +use crate::snap::{SnapType, ALL_SNAP_MODES}; use iced::widget::{button, checkbox, column, container, row, scrollable, text, Space}; -use iced::{Element, Fill}; +use iced::{Background, Border, Element, Fill, Length, Theme}; +use std::borrow::Cow; + +/// Active tab in the Drafting Settings dialog. +#[derive( + Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, +)] +#[serde(rename_all = "kebab-case")] +pub enum DraftingSettingsTab { + #[default] + SnapAndGrid, + PolarTracking, + ObjectSnap, + ObjectSnap3D, + DynamicInput, + QuickProperties, + SelectionCycling, +} + +/// Working buffer of drafting settings edited in the dialog. +#[derive(Clone, Debug, PartialEq)] +pub struct DraftingSettingsState { + pub active_tab: DraftingSettingsTab, + // Snap and Grid + pub snap_on: bool, + pub grid_on: bool, + pub isometric: bool, + pub iso_plane: IsoPlane, + pub snap_angle_deg: f32, + // Polar Tracking + pub polar_on: bool, + pub ortho_on: bool, + pub polar_increment_deg: f32, + // Object Snap + pub osnap_on: bool, + pub otrack_on: bool, + pub snap_modes: rustc_hash::FxHashSet, + // 3D Object Snap + pub osnap3d_on: bool, + // Dynamic Input + pub dyn_input_on: bool, + // Quick Properties + pub quick_props_on: bool, + // Selection Cycling + pub selection_cycling_on: bool, +} + +impl DraftingSettingsState { + /// Check whether settings were modified compared to the saved snapshot. + /// Excludes `active_tab` so tab navigation does not mark the dialog dirty. + pub fn is_dirty(&self, saved: &Self) -> bool { + self.snap_on != saved.snap_on + || self.grid_on != saved.grid_on + || self.isometric != saved.isometric + || self.iso_plane != saved.iso_plane + || (self.snap_angle_deg - saved.snap_angle_deg).abs() > 0.001 + || self.polar_on != saved.polar_on + || self.ortho_on != saved.ortho_on + || (self.polar_increment_deg - saved.polar_increment_deg).abs() > 0.001 + || self.osnap_on != saved.osnap_on + || self.otrack_on != saved.otrack_on + || self.snap_modes != saved.snap_modes + || self.osnap3d_on != saved.osnap3d_on + || self.dyn_input_on != saved.dyn_input_on + || self.quick_props_on != saved.quick_props_on + || self.selection_cycling_on != saved.selection_cycling_on + } +} + +/// Helper for grouped sub-panels with a light border and header title. +fn group<'a>(title: impl Into, body: impl Into>) -> Element<'a, Message> { + container( + column![ + text(title.into()).size(11).style(|theme: &Theme| text::Style { + color: Some(theme.palette().background.strongest.text.scale_alpha(0.75)), + }), + body.into(), + ] + .spacing(6), + ) + .padding([8, 10]) + .width(Fill) + .style(|theme: &Theme| container::Style { + border: Border { + width: 1.0, + radius: 4.0.into(), + color: theme.palette().background.strong.color, + }, + ..Default::default() + }) + .into() +} pub fn view_window<'a>( - snapper: &'a Snapper, - grid: bool, - grid_snap: bool, - ortho: bool, - polar: bool, - otrack: bool, - isometric: bool, - iso_plane: IsoPlane, - snap_angle_deg: f32, + state: &'a DraftingSettingsState, + dirty: bool, + close_confirm: bool, sizing: crate::ui::modal::ModalSizing, ) -> Element<'a, Message> { - let toggle = |value: bool, label: std::borrow::Cow<'a, str>, message: Message| { + let toggle = |value: bool, label: Cow<'a, str>, message: Message| { row![ checkbox(value).on_toggle(move |_| message.clone()).size(15), text(label).size(12), @@ -25,111 +110,569 @@ pub fn view_window<'a>( .align_y(iced::Center) }; - let drafting_modes = column![ - text(crate::t!("Drafting modes")).size(15), - Space::new().height(8), - toggle(grid, crate::t!("Grid display"), Message::ToggleGrid), - toggle(grid_snap, crate::t!("Grid snap"), Message::ToggleGridSnap), - toggle(ortho, crate::t!("Ortho"), Message::ToggleOrtho), - toggle(polar, crate::t!("Polar tracking"), Message::TogglePolar), - toggle(otrack, crate::t!("Object snap tracking"), Message::ToggleOTrack), - ] - .spacing(6); + // ── Tab Bar ────────────────────────────────────────────────────────── + let tab_button = |tab: DraftingSettingsTab, label: Cow<'a, str>| { + let is_active = state.active_tab == tab; + button(text(label).size(11.5)) + .on_press(Message::DraftingSettingsTabChanged(tab)) + .style(if is_active { + button::primary + } else { + button::secondary + }) + .padding([5, 9]) + }; - let mut planes = row![].spacing(5); - for plane in IsoPlane::ALL { - planes = planes.push( - button(text(crate::t!(plane.label())).size(11)) - .on_press(Message::SetIsoPlane(plane)) - .style(if isometric && iso_plane == plane { - button::primary - } else { - button::secondary - }) - .padding([5, 12]), + let tab_bar = row![ + tab_button(DraftingSettingsTab::SnapAndGrid, crate::t!("Snap and Grid")), + tab_button(DraftingSettingsTab::PolarTracking, crate::t!("Polar Tracking")), + tab_button(DraftingSettingsTab::ObjectSnap, crate::t!("Object Snap")), + tab_button(DraftingSettingsTab::ObjectSnap3D, crate::t!("3D Object Snap")), + tab_button(DraftingSettingsTab::DynamicInput, crate::t!("Dynamic Input")), + tab_button(DraftingSettingsTab::QuickProperties, crate::t!("Quick Properties")), + tab_button(DraftingSettingsTab::SelectionCycling, crate::t!("Selection Cycling")), + ] + .spacing(4) + .wrap(); + + // ── Tab 1: Snap and Grid ───────────────────────────────────────────── + let snap_and_grid_view = { + // Left Column: Snap settings + let snap_on_toggle = toggle( + state.snap_on, + crate::t!("Snap On (F9)"), + Message::DraftingSettingsToggleSnap, ); - } - let isometric_controls = column![ - text(crate::t!("Isometric drafting")).size(15), - Space::new().height(8), - toggle( - isometric, - crate::t!("Enable isometric drafting"), - Message::ToggleIsometricDrafting, - ), - planes, - text(crate::t!("F5 cycles Left, Top, and Right.")).size(11), - Space::new().height(6), - row![ - text(crate::t!("Rotation: %{angle}°", angle = snap_angle_deg)).size(11), - button(text(crate::t!("Reset rotation")).size(10)) - .on_press(Message::ResetDraftingRotation) - .style(button::secondary) - .padding([4, 10]), + + let snap_spacing_group = group( + crate::t!("Snap spacing"), + column![ + row![ + text(crate::t!("Snap X spacing:")).size(11).width(120), + text("10.00").size(11), + ] + .spacing(8) + .align_y(iced::Center), + row![ + text(crate::t!("Snap Y spacing:")).size(11).width(120), + text("10.00").size(11), + ] + .spacing(8) + .align_y(iced::Center), + row![ + checkbox(true).size(14), + text(crate::t!("Equal X and Y spacing")).size(11), + ] + .spacing(6) + .align_y(iced::Center), + ] + .spacing(6), + ); + + let mut iso_planes = row![].spacing(4); + for plane in IsoPlane::ALL { + iso_planes = iso_planes.push( + button(text(crate::t!(plane.label())).size(11)) + .on_press(Message::DraftingSettingsSetIsoPlane(plane)) + .style(if state.isometric && state.iso_plane == plane { + button::primary + } else { + button::secondary + }) + .padding([4, 10]), + ); + } + + let snap_type_group = group( + crate::t!("Snap type"), + column![ + toggle( + state.isometric, + crate::t!("Enable isometric drafting"), + Message::DraftingSettingsToggleIsometric, + ), + iso_planes, + text(crate::t!("F5 cycles Left, Top, and Right.")).size(10.5), + Space::new().height(4), + row![ + text(crate::t!("Rotation: %{angle}°", angle = state.snap_angle_deg)).size(11), + button(text(crate::t!("Reset rotation")).size(10)) + .on_press(Message::DraftingSettingsResetRotation) + .style(button::secondary) + .padding([3, 8]), + ] + .spacing(8) + .align_y(iced::Center), + ] + .spacing(6), + ); + + let left_col = column![snap_on_toggle, snap_spacing_group, snap_type_group] + .spacing(10) + .width(Fill); + + // Right Column: Grid settings + let grid_on_toggle = toggle( + state.grid_on, + crate::t!("Grid On (F7)"), + Message::DraftingSettingsToggleGrid, + ); + + let grid_spacing_group = group( + crate::t!("Grid spacing"), + column![ + row![ + text(crate::t!("Grid X spacing:")).size(11).width(120), + text("10.00").size(11), + ] + .spacing(8) + .align_y(iced::Center), + row![ + text(crate::t!("Grid Y spacing:")).size(11).width(120), + text("10.00").size(11), + ] + .spacing(8) + .align_y(iced::Center), + row![ + text(crate::t!("Major line every:")).size(11).width(120), + text("5").size(11), + ] + .spacing(8) + .align_y(iced::Center), + ] + .spacing(6), + ); + + let grid_behavior_group = group( + crate::t!("Grid behavior"), + column![ + row![ + checkbox(true).size(14), + text(crate::t!("Adaptive grid")).size(11), + ] + .spacing(6) + .align_y(iced::Center), + row![ + checkbox(true).size(14), + text(crate::t!("Display grid beyond Limits")).size(11), + ] + .spacing(6) + .align_y(iced::Center), + ] + .spacing(6), + ); + + let right_col = column![grid_on_toggle, grid_spacing_group, grid_behavior_group] + .spacing(10) + .width(Fill); + + row![left_col, right_col].spacing(16).width(Fill) + }; + + // ── Tab 2: Polar Tracking ──────────────────────────────────────────── + let polar_tracking_view = { + let polar_on_toggle = toggle( + state.polar_on, + crate::t!("Polar Tracking On (F10)"), + Message::DraftingSettingsTogglePolar, + ); + + let ortho_on_toggle = toggle( + state.ortho_on, + crate::t!("Ortho mode (F8)"), + Message::DraftingSettingsToggleOrtho, + ); + + let polar_angle_group = group( + crate::t!("Polar Angle Settings"), + column![ + row![ + text(crate::t!("Increment angle:")).size(11).width(130), + text(format!("{:.1}°", state.polar_increment_deg)).size(11), + ] + .spacing(8) + .align_y(iced::Center), + text(crate::t!( + "Polar Tracking guides cursor movement along specified angles. Ortho mode constrains movement to orthogonal axes." + )) + .size(10.5) + .style(|theme: &Theme| text::Style { + color: Some(theme.palette().background.strongest.text.scale_alpha(0.7)), + }), + ] + .spacing(6), + ); + + column![ + polar_on_toggle, + ortho_on_toggle, + Space::new().height(4), + polar_angle_group, ] .spacing(10) - .align_y(iced::Center), - ] - .spacing(7); + .width(Fill) + }; - let mut snap_modes = column![ - text(crate::t!("Object snap modes")).size(15), - Space::new().height(8), - toggle( - snapper.snap_enabled, - crate::t!("Enable object snap"), - Message::ToggleSnapEnabled, - ), - row![ - button(text(crate::t!("Select All")).size(10)) - .on_press(Message::SnapSelectAll) + // ── Tab 3: Object Snap ─────────────────────────────────────────────── + let object_snap_view = { + let osnap_on_toggle = toggle( + state.osnap_on, + crate::t!("Object Snap On (F3)"), + Message::DraftingSettingsToggleOsnap, + ); + + let otrack_on_toggle = toggle( + state.otrack_on, + crate::t!("Object Snap Tracking On (F11)"), + Message::DraftingSettingsToggleOtrack, + ); + + let action_buttons = row![ + button(text(crate::t!("Select All")).size(10.5)) + .on_press(Message::DraftingSettingsSnapSelectAll) .style(button::secondary) .padding([4, 10]), - button(text(crate::t!("Clear All")).size(10)) - .on_press(Message::SnapClearAll) + button(text(crate::t!("Clear All")).size(10.5)) + .on_press(Message::DraftingSettingsSnapClearAll) .style(button::secondary) .padding([4, 10]), ] - .spacing(6), - ] - .spacing(5); - for &(snap_type, _, label) in ALL_SNAP_MODES { - snap_modes = snap_modes.push( - row![ - checkbox(snapper.is_on(snap_type)) - .on_toggle(move |_| Message::ToggleSnap(snap_type)) - .size(14), - text(crate::t!(label)).size(11), - ] - .spacing(7) - .align_y(iced::Center), - ); - } + .spacing(8); - let close = button(text(crate::tr!("action", "close")).size(12)) - .on_press(Message::CloseModal) - .padding([6, 18]) - .style(button::secondary); - let content = column![ - drafting_modes, - Space::new().height(20), - isometric_controls, - Space::new().height(20), - snap_modes, + // Two columns of snap modes: + let total = ALL_SNAP_MODES.len(); + let mid = (total + 1) / 2; + let (first_half, second_half) = ALL_SNAP_MODES.split_at(mid); + + let mut col1 = column![].spacing(6).width(Fill); + for &(snap_type, _, label) in first_half { + let is_checked = state.snap_modes.contains(&snap_type); + col1 = col1.push( + row![ + checkbox(is_checked) + .on_toggle(move |_| Message::DraftingSettingsToggleSnapMode(snap_type)) + .size(14), + text(crate::t!(label)).size(11), + ] + .spacing(7) + .align_y(iced::Center), + ); + } + + let mut col2 = column![].spacing(6).width(Fill); + for &(snap_type, _, label) in second_half { + let is_checked = state.snap_modes.contains(&snap_type); + col2 = col2.push( + row![ + checkbox(is_checked) + .on_toggle(move |_| Message::DraftingSettingsToggleSnapMode(snap_type)) + .size(14), + text(crate::t!(label)).size(11), + ] + .spacing(7) + .align_y(iced::Center), + ); + } + + let snap_modes_group = group( + crate::t!("Object Snap modes"), + row![col1, col2].spacing(20).width(Fill), + ); + + column![ + row![osnap_on_toggle, Space::new().width(24), otrack_on_toggle] + .align_y(iced::Center), + action_buttons, + Space::new().height(4), + snap_modes_group, + ] + .spacing(10) + .width(Fill) + }; + + // ── Tab 4: 3D Object Snap ──────────────────────────────────────────── + let object_snap_3d_view = { + let osnap3d_toggle = toggle( + state.osnap3d_on, + crate::t!("3D Object Snap On (F4)"), + Message::DraftingSettingsToggle3dOsnap, + ); + + let placeholder_group = group( + crate::t!("3D Object Snap modes"), + column![ + row![checkbox(false).size(14), text(crate::t!("Vertex")).size(11)] + .spacing(7) + .align_y(iced::Center), + row![ + checkbox(false).size(14), + text(crate::t!("Midpoint on edge")).size(11), + ] + .spacing(7) + .align_y(iced::Center), + row![ + checkbox(false).size(14), + text(crate::t!("Center of face")).size(11), + ] + .spacing(7) + .align_y(iced::Center), + row![checkbox(false).size(14), text(crate::t!("Knot")).size(11)] + .spacing(7) + .align_y(iced::Center), + row![ + checkbox(false).size(14), + text(crate::t!("Perpendicular to face")).size(11), + ] + .spacing(7) + .align_y(iced::Center), + row![ + checkbox(false).size(14), + text(crate::t!("Nearest to face")).size(11), + ] + .spacing(7) + .align_y(iced::Center), + ] + .spacing(6), + ); + + column![osnap3d_toggle, Space::new().height(4), placeholder_group] + .spacing(10) + .width(Fill) + }; + + // ── Tab 5: Dynamic Input ───────────────────────────────────────────── + let dynamic_input_view = { + let dyn_input_toggle = toggle( + state.dyn_input_on, + crate::t!("Enable Pointer Input (F12)"), + Message::DraftingSettingsToggleDynInput, + ); + + let pointer_group = group( + crate::t!("Pointer Input"), + column![ + row![ + checkbox(true).size(14), + text(crate::t!("Display coordinate input near crosshairs")).size(11), + ] + .spacing(7) + .align_y(iced::Center), + row![ + checkbox(true).size(14), + text(crate::t!("Enable dimension input fields")).size(11), + ] + .spacing(7) + .align_y(iced::Center), + ] + .spacing(6), + ); + + let prompt_group = group( + crate::t!("Dynamic Prompts"), + column![ + row![ + checkbox(true).size(14), + text(crate::t!("Show command prompting and command input near crosshairs")) + .size(11), + ] + .spacing(7) + .align_y(iced::Center), + ] + .spacing(6), + ); + + column![ + dyn_input_toggle, + Space::new().height(4), + pointer_group, + prompt_group, + ] + .spacing(10) + .width(Fill) + }; + + // ── Tab 6: Quick Properties ────────────────────────────────────────── + let quick_properties_view = { + let quick_props_toggle = toggle( + state.quick_props_on, + crate::t!("Display Quick Properties palette on selection"), + Message::DraftingSettingsToggleQuickProps, + ); + + let palette_group = group( + crate::t!("Palette Location"), + column![ + row![ + checkbox(true).size(14), + text(crate::t!("Cursor-dependent position")).size(11), + ] + .spacing(7) + .align_y(iced::Center), + row![ + checkbox(false).size(14), + text(crate::t!("Static quadrant location")).size(11), + ] + .spacing(7) + .align_y(iced::Center), + ] + .spacing(6), + ); + + column![ + quick_props_toggle, + Space::new().height(4), + palette_group, + ] + .spacing(10) + .width(Fill) + }; + + // ── Tab 7: Selection Cycling ───────────────────────────────────────── + let selection_cycling_view = { + let sel_cycling_toggle = toggle( + state.selection_cycling_on, + crate::t!("Allow selection cycling"), + Message::DraftingSettingsToggleSelCycling, + ); + + let cycling_group = group( + crate::t!("Display Selection Cycling List Box"), + column![ + row![ + checkbox(true).size(14), + text(crate::t!("Show cycling badge when objects overlap")).size(11), + ] + .spacing(7) + .align_y(iced::Center), + row![ + checkbox(true).size(14), + text(crate::t!("Display selection candidate list box on click")).size(11), + ] + .spacing(7) + .align_y(iced::Center), + ] + .spacing(6), + ); + + column![ + sel_cycling_toggle, + Space::new().height(4), + cycling_group, + ] + .spacing(10) + .width(Fill) + }; + + // ── Active Tab Content ─────────────────────────────────────────────── + let tab_content: Element<'a, Message> = match state.active_tab { + DraftingSettingsTab::SnapAndGrid => snap_and_grid_view.into(), + DraftingSettingsTab::PolarTracking => polar_tracking_view.into(), + DraftingSettingsTab::ObjectSnap => object_snap_view.into(), + DraftingSettingsTab::ObjectSnap3D => object_snap_3d_view.into(), + DraftingSettingsTab::DynamicInput => dynamic_input_view.into(), + DraftingSettingsTab::QuickProperties => quick_properties_view.into(), + DraftingSettingsTab::SelectionCycling => selection_cycling_view.into(), + }; + + // ── Bottom Action Buttons ──────────────────────────────────────────── + let ok_button = button(text(crate::t!("OK")).size(12)) + .on_press(Message::DraftingSettingsOk) + .style(button::primary) + .padding([6, 18]); + + let apply_button = button(text(crate::t!("Apply")).size(12)) + .on_press_maybe(if dirty { + Some(Message::DraftingSettingsApply) + } else { + None + }) + .style(if dirty { + button::secondary + } else { + button::text + }) + .padding([6, 18]); + + let close_button = button(text(crate::t!("Close")).size(12)) + .on_press(Message::DraftingSettingsClose) + .style(button::secondary) + .padding([6, 18]); + + let action_row = row![ + Space::new().width(Fill), + ok_button, + apply_button, + close_button, ] - .width(sizing.width); + .spacing(8) + .align_y(iced::Center); + + // ── Body Layout ────────────────────────────────────────────────────── let body = column![ - scrollable(content).spacing(8).height(sizing.height), + tab_bar, + Space::new().height(10), + scrollable(tab_content).spacing(6).height(Fill), Space::new().height(12), - row![Space::new().width(Fill), close], + action_row, ] .width(sizing.width) .height(sizing.height); - container(body) + let main_content = container(body) .style(container::rounded_box) - .padding([16, 18]) + .padding([14, 16]) .width(sizing.width) - .height(sizing.height) - .into() + .height(sizing.height); + + // ── Unsaved Changes Guard ──────────────────────────────────────────── + if !close_confirm { + return main_content.into(); + } + + let shield = iced::widget::mouse_area( + container(Space::new()) + .width(Length::Fill) + .height(Length::Fill) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme + .palette() + .background + .strongest + .color + .scale_alpha(0.55), + )), + ..Default::default() + }), + ) + .on_press(Message::DraftingSettingsCloseKeep); + + let warning_panel = container( + column![ + text(crate::t!("Unsaved changes will be discarded.")).size(13.5), + Space::new().height(12), + row![ + button(text(crate::t!("Discard && close")).size(12)) + .on_press(Message::DraftingSettingsCloseDiscard) + .padding([5, 14]) + .style(button::danger), + button(text(crate::t!("Keep editing")).size(12)) + .on_press(Message::DraftingSettingsCloseKeep) + .padding([5, 14]) + .style(button::secondary), + ] + .spacing(10), + ] + .spacing(0), + ) + .padding([18, 22]) + .style(container::rounded_box); + + iced::widget::stack![ + main_content, + shield, + container(warning_panel).center_x(Fill).center_y(Fill) + ] + .into() } +