diff --git a/src/app/mod.rs b/src/app/mod.rs index 2b311596e..899d0d2c8 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -814,6 +814,8 @@ pub(super) struct OpenCADStudio { render_mode_preview: Option, /// Whether the Properties panel is shown on the left (PROPERTIES). show_properties: bool, + /// The Layer Manager pallet (LAYERS) is open. + pub(crate) show_layers: bool, /// Docked Insert Block panel visibility. pub(crate) show_block_palette: bool, /// Node graph overlay over the viewport. @@ -1963,7 +1965,6 @@ pub enum ModalKind { PluginManager, UpdateNotice, DonationPrompt, - Layers, LayerStateManager, LayerTranslator, DrawingUnits, @@ -4285,6 +4286,7 @@ impl OpenCADStudio { render_mode_menu_open: false, render_mode_preview: None, show_properties: true, + show_layers: false, show_block_palette: false, show_node_graph: false, property_target_override: None, diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index d8715a2e5..613422a0a 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -592,6 +592,38 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven self.dock_title_hover = id; iced::Task::none() } + DockMsg::FloatOut(id) => { + // Float beside the edge it was docked on, near the top. + let (ww, _) = self.dock_workspace_size(); + let (w, h) = self.dock_float_size(id); + let x = match self.dock.location(id) { + Some((DockSide::Right, _)) => { + ww - self.dock_column_width(DockSide::Right) - w - 60.0 + } + _ => self.dock_column_width(DockSide::Left) + 40.0, + }; + self.dock_dragging = None; + self.dock_drag_target = None; + if self.dock.float(FloatPanel { + id, + x: x.max(0.0), + y: 40.0, + w, + h, + }) { + self.save_config(); + } + iced::Task::none() + } + DockMsg::DockTo(id, side) => { + self.dock_dragging = None; + self.dock_drag_target = None; + if self.dock.dock(id, side, usize::MAX) { + self.dock_expanded = Some(id); + self.save_config(); + } + iced::Task::none() + } DockMsg::TabMenu(slot) => { self.dock_tab_menu = slot; iced::Task::none() @@ -666,6 +698,10 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven self.show_properties = false; self.ribbon.set_properties(false); } + PanelId::Layers => { + self.show_layers = false; + self.ribbon.deactivate_tool_if("LAYERS"); + } } if self.dock_expanded == Some(id) { self.dock_expanded = None; @@ -749,6 +785,11 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven from_left, ); } + } else if self.layer_col_dragging { + if let Some(last) = self.dock_drag_last { + self.layer_name_col_w = + (self.layer_name_col_w + point.x - last.x).clamp(60.0, 640.0); + } } else if let Some(id) = self.dock_resizing { if let Some(last) = self.dock_drag_last { let dx = point.x - last.x; @@ -768,6 +809,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven || self.dock_resizing.is_some() || self.dock_split_drag.is_some() || self.dock_float_resizing.is_some() + || self.layer_col_dragging { self.dock_drag_last = Some(point); } @@ -806,6 +848,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven self.xref_col_drag = None; self.xref_col_last = None; self.xref_split_drag = false; + self.layer_col_dragging = false; iced::Task::none() } } @@ -835,6 +878,10 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven PanelId::PointCloudManager => self.pc_manager.show = true, PanelId::Count => self.set_count_palette(true), PanelId::SheetSetManager => self.show_sheet_set_manager(true), + PanelId::Layers => { + self.sync_ribbon_layers(); + self.show_layers = true; + } } self.dock_expanded = Some(id); iced::Task::none() @@ -871,6 +918,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven PanelId::PointCloudManager => self.pc_manager.show, PanelId::Count => self.count_palette.show, PanelId::SheetSetManager => self.sheet_set.show, + PanelId::Layers => self.show_layers, } } @@ -2108,6 +2156,35 @@ mod tests { assert_eq!(app.dock_tab_menu, Some((DockSide::Left, 0))); } + #[test] + fn layers_command_opens_a_floating_pallet_and_toggles_it_closed() { + use crate::ui::dock::PanelId; + let mut app = dock_app(); + let _ = app.update(Message::ToggleLayers); + assert!(app.show_layers); + assert!(app.active_modal.is_none(), "no modal backdrop"); + let f = app.dock.float_rect(PanelId::Layers).expect("floats on first use"); + assert!(f.x > 0.0 && f.y >= 0.0); + let _ = app.update(Message::ToggleLayers); + assert!(!app.show_layers); + // Its place is kept for next time. + assert!(app.dock.float_rect(PanelId::Layers).is_some()); + } + + #[test] + fn dock_double_clicks_float_and_dock_panels() { + use crate::app::config::DockSide; + use crate::ui::dock::{DockMsg, PanelId}; + let mut app = dock_app(); + let _ = app.on_dock(DockMsg::FloatOut(PanelId::Properties)); + assert!(app.dock.left.is_empty()); + let f = app.dock.float_rect(PanelId::Properties).expect("floating"); + assert!(f.x > 0.0, "floats clear of the left edge"); + let _ = app.on_dock(DockMsg::DockTo(PanelId::Properties, DockSide::Right)); + assert!(app.dock.floating.is_empty()); + assert_eq!(app.dock.location(PanelId::Properties), Some((DockSide::Right, 1))); + } + #[test] fn dock_tab_group_resize_applies_to_every_tab() { use crate::app::config::DockSide; diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 187d653d0..4f39e772c 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -2552,14 +2552,28 @@ impl OpenCADStudio { Message::ScriptLine(line) => self.feed_script_line(&line), Message::ToggleLayers => { - if self.active_modal == Some(super::ModalKind::Layers) { - self.ribbon.deactivate_tool_if("LAYERS"); - self.active_modal = None; - self.reset_modal_geometry(); - } else { - self.sync_ribbon_layers(); - self.active_modal = Some(super::ModalKind::Layers); + use crate::ui::dock::PanelId; + if self.show_layers { + return self.on_dock(crate::ui::dock::DockMsg::Close(PanelId::Layers)); } + self.sync_ribbon_layers(); + self.show_layers = true; + if !self.dock.is_placed(PanelId::Layers) { + // First use: float it over the middle of the drawing, + // where the dialog used to open. + let (ww, wh) = self.dock_workspace_size(); + let (w, h) = self.dock_float_size(PanelId::Layers); + self.dock.float(crate::ui::dock::FloatPanel { + id: PanelId::Layers, + x: ((ww - w) * 0.5).max(0.0), + y: ((wh - h) * 0.5).max(0.0), + w, + h, + }); + self.save_config(); + } + self.dock.raise_float(PanelId::Layers); + self.dock_expanded = Some(PanelId::Layers); Task::none() } @@ -6261,8 +6275,11 @@ impl OpenCADStudio { // Ctrl+A in the MText editor selects all of its text. self.mtext_select_all(); Task::none() - } else if self.active_modal == Some(super::ModalKind::Layers) { - // Select every row in the Layer Manager (#236). + } else if self.dock_panel_visible(crate::ui::dock::PanelId::Layers) + && self.tabs[i].layers.selected.is_some() + { + // Select every row in the Layer Manager (#236) while the + // user is working in it (a layer row is selected). let n = self.tabs[i].layers.layers.len(); self.tabs[i].layers.selected_multi = (0..n).collect(); self.tabs[i].layers.selected = (n > 0).then_some(0); @@ -9024,17 +9041,16 @@ impl OpenCADStudio { Task::none() } Message::LayerNameColGrab => { - // Start a Name-column divider drag; rides ModalDragMove. + // Start a Name-column divider drag; the dock's pointer capture + // feeds it (see `on_dock`). self.layer_col_dragging = true; - self.modal_drag_last = None; + self.dock_drag_last = None; Task::none() } Message::ModalDragMove(p) => { if let Some(last) = self.modal_drag_last { let (dx, dy) = (p.x - last.x, p.y - last.y); - if self.layer_col_dragging { - self.layer_name_col_w = (self.layer_name_col_w + dx).clamp(60.0, 640.0); - } else if self.modal_resizing { + if self.modal_resizing { // The grip sits bottom-right, so dragging out grows the // box. The delta is added to each dialog's natural size, // so clamp it at zero — dragging in past the natural size @@ -9068,7 +9084,7 @@ impl OpenCADStudio { } } } - if self.modal_dragging || self.modal_resizing || self.layer_col_dragging { + if self.modal_dragging || self.modal_resizing { self.modal_drag_last = Some(p); } Task::none() @@ -9076,7 +9092,6 @@ impl OpenCADStudio { Message::ModalDragRelease => { self.modal_dragging = false; self.modal_resizing = false; - self.layer_col_dragging = false; self.modal_drag_last = None; Task::none() } diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 08d373cea..1e02d092a 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1952,6 +1952,7 @@ bg={bg_ms:.1}ms n={view_count}" let any_splitting = self.dock_split_drag.is_some(); let any_resizing = self.dock_resizing.is_some() || self.dock_float_resizing.is_some() + || self.layer_col_dragging || self.xref_col_drag.is_some() || self.xref_split_drag; let workspace: Element<'_, Message> = if any_dragging || any_resizing || any_splitting { @@ -2935,6 +2936,9 @@ impl OpenCADStudio { crate::ui::dock::PanelId::SheetSetManager => { crate::ui::window::sheet_set::view(&self.sheet_set, width, chrome) } + crate::ui::dock::PanelId::Layers => { + tab.layers.view_panel(self.layer_name_col_w, width, chrome) + } } } @@ -3620,6 +3624,15 @@ fn floating_title_bar( ..Default::default() })) .on_press(Message::Dock(DockMsg::DockGrab(id))) + // Double-click docks it against the edge its title bar faces. + .on_double_click(Message::Dock(DockMsg::DockTo( + id, + if on_left { + crate::app::config::DockSide::Left + } else { + crate::app::config::DockSide::Right + }, + ))) .interaction(iced::mouse::Interaction::Grab) .into() } @@ -3643,7 +3656,7 @@ fn dock_divider(id: crate::ui::dock::PanelId) -> Element<'static, Message> { .into() } -use crate::ui::dock::{DOCK_DIVIDER_W, DOCK_RAIL_W, DOCK_TAB_H}; +use crate::ui::dock::{DOCK_DIVIDER_W, DOCK_RAIL_W}; /// The tab row above a docked slot: one icon tab per shown pallet, from the /// left, and a menu button on the right listing every pallet. Pressing a tab @@ -3656,7 +3669,7 @@ fn dock_tab_strip( width: f32, side: crate::app::config::DockSide, gi: usize, - menu: Option<[(crate::ui::dock::PanelId, bool); 8]>, + menu: Option<[(crate::ui::dock::PanelId, bool); crate::ui::dock::PanelId::ALL.len()]>, ) -> Element<'static, Message> { use crate::ui::dock::{DockMsg, DOCK_TAB_CELL_W, DOCK_TAB_INSET}; const TAB_GAP: f32 = 2.0; diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index bb2a95378..7f2c286b9 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -19,7 +19,6 @@ impl OpenCADStudio { Some(K::PluginManager) => crate::tr!("modal", "plugin-manager"), Some(K::UpdateNotice) => crate::tr!("modal", "update-available"), Some(K::DonationPrompt) => crate::tr!("donation", "title"), - Some(K::Layers) => crate::tr!("modal", "layer-manager"), Some(K::LayerStateManager) => crate::tr!("modal", "layer-state-manager"), Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(), Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(), @@ -507,15 +506,6 @@ impl OpenCADStudio { |flow| crate::ui::window::update_notice::view_window(latest, body, flow), ) } - super::super::ModalKind::Layers => { - let tab = &self.tabs[self.active_tab]; - sized_flow( - ex, - 900, - 360, - |flow| tab.layers.view_window(self.layer_name_col_w, flow), - ) - } super::super::ModalKind::LayerTranslator => { use crate::modules::draw::layers::laytrans; let i = self.active_tab; diff --git a/src/ui/dock.rs b/src/ui/dock.rs index 167ee0f16..26c77bf41 100644 --- a/src/ui/dock.rs +++ b/src/ui/dock.rs @@ -49,6 +49,10 @@ pub enum DockMsg { /// Pallet menu pick: show `panel` as a tab of the slot, or hide it when /// it already shows there. TabMenuToggle(DockSide, usize, PanelId), + /// Double-click on a docked title bar: float the panel. + FloatOut(PanelId), + /// Double-click on a floating title bar: dock the panel on that side. + DockTo(PanelId, DockSide), /// Bring floating `panel` to the front. FloatRaise(PanelId), /// The pointer left the edge column; collapse any auto-collapsing panel. @@ -72,11 +76,13 @@ pub enum PanelId { Count, /// The open sheet sets: subsets and sheets. SheetSetManager, + /// The Layer Manager (LAYERS). + Layers, } impl PanelId { /// Every panel, in the order the pallet menu lists them. - pub const ALL: [PanelId; 8] = [ + pub const ALL: [PanelId; 9] = [ PanelId::Properties, PanelId::BlockPalette, PanelId::ExternalReferences, @@ -85,6 +91,7 @@ impl PanelId { PanelId::PointCloudManager, PanelId::Count, PanelId::SheetSetManager, + PanelId::Layers, ]; /// Localized-friendly display name used by the collapsed/edge chrome. @@ -98,6 +105,7 @@ impl PanelId { PanelId::PointCloudManager => "Point Cloud Manager", PanelId::Count => "Count", PanelId::SheetSetManager => "Sheet Set Manager", + PanelId::Layers => "Layer Manager", } } @@ -112,6 +120,7 @@ impl PanelId { PanelId::PointCloudManager => include_bytes!("../../assets/icons/pc_attach.svg"), PanelId::Count => include_bytes!("../../assets/icons/data_extract.svg"), PanelId::SheetSetManager => include_bytes!("../../assets/icons/sheetset.svg"), + PanelId::Layers => include_bytes!("../../assets/icons/layers/panel.svg"), } } @@ -126,6 +135,7 @@ impl PanelId { PanelId::PointCloudManager => 280.0, PanelId::Count => 280.0, PanelId::SheetSetManager => 280.0, + PanelId::Layers => 560.0, } } @@ -133,7 +143,7 @@ impl PanelId { /// column-rich, so it allows double the shared maximum. fn max_width(self) -> f32 { match self { - PanelId::ExternalReferences => DOCK_MAX_W * 2.0, + PanelId::ExternalReferences | PanelId::Layers => DOCK_MAX_W * 2.0, _ => DOCK_MAX_W, } } @@ -699,6 +709,7 @@ pub fn title_bar<'a>(id: PanelId, title: String, chrome: Chrome) -> Element<'a, .padding([3, 6]), ) .on_press(Message::Dock(DockMsg::DockGrab(id))) + .on_double_click(Message::Dock(DockMsg::FloatOut(id))) .on_enter(Message::Dock(DockMsg::TitleHover(Some(id)))) .on_exit(Message::Dock(DockMsg::TitleHover(None))) .interaction(iced::mouse::Interaction::Grab) @@ -1098,16 +1109,7 @@ mod tests { #[test] fn every_panel_has_its_own_icon() { - let ids = [ - PanelId::Properties, - PanelId::BlockPalette, - PanelId::ExternalReferences, - PanelId::Browser, - PanelId::NodeGraph, - PanelId::PointCloudManager, - PanelId::Count, - PanelId::SheetSetManager, - ]; + let ids = PanelId::ALL; for (i, a) in ids.iter().enumerate() { assert!(a.icon().starts_with(b" Element<'_, Message> { - self.view_content(name_col_w, sizing) + use crate::ui::dock::PanelId; + let title_bar = crate::ui::dock::title_bar( + PanelId::Layers, + crate::tr!("modal", "layer-manager"), + chrome, + ); + let table = self.view_content( + name_col_w, + crate::ui::modal::ModalSizing { + width: Length::Shrink, + height: Length::Fill, + }, + ); + let body = scrollable(table) + .direction(iced::widget::scrollable::Direction::Horizontal( + iced::widget::scrollable::Scrollbar::new(), + )) + .width(Length::Fill) + .height(Length::Fill); + container(column![title_bar, body]) + .width(Length::Fixed(width)) + .height(Length::Fill) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.base.color)), + border: Border { + color: theme.palette().background.neutral.color, + width: 1.0, + radius: 0.0.into(), + }, + ..Default::default() + }) + .into() } fn view_content(