diff --git a/src/app/commands/blocks.rs b/src/app/commands/blocks.rs index 99a86a5c2..f41150459 100644 --- a/src/app/commands/blocks.rs +++ b/src/app/commands/blocks.rs @@ -921,7 +921,7 @@ impl OpenCADStudio { usize::MAX, ); } - self.dock_expanded = Some(crate::ui::dock::PanelId::ExternalReferences); + self.dock_reveal(crate::ui::dock::PanelId::ExternalReferences); self.refresh_xref_manager(); } } @@ -1766,7 +1766,7 @@ mod tests { let _ = app.run_command_line("BLOCKPALETTE"); assert!(app.show_block_palette); assert!( - app.dock_expanded == Some(crate::ui::dock::PanelId::BlockPalette), + app.dock_peek == Some(crate::ui::dock::PanelId::BlockPalette), "palette must open expanded, not as the collapsed bar" ); // BLOCKSPALETTE only opens; BLOCKSPALETTECLOSE closes. diff --git a/src/app/commands/count.rs b/src/app/commands/count.rs index 5d54c42a0..ff3bf4854 100644 --- a/src/app/commands/count.rs +++ b/src/app/commands/count.rs @@ -239,9 +239,9 @@ impl OpenCADStudio { if !self.dock.is_placed(id) { self.dock.dock(id, crate::app::config::DockSide::Right, usize::MAX); } - self.dock_expanded = Some(id); - } else if self.dock_expanded == Some(id) { - self.dock_expanded = None; + self.dock_reveal(id); + } else if self.dock_peek == Some(id) { + self.dock_peek = None; } } diff --git a/src/app/commands/layerprops.rs b/src/app/commands/layerprops.rs index 828140bef..3c97b2ad3 100644 --- a/src/app/commands/layerprops.rs +++ b/src/app/commands/layerprops.rs @@ -687,7 +687,7 @@ impl OpenCADStudio { } // Open expanded so it is usable straight away; the pin // button still collapses it. - self.dock_expanded = Some(id); + self.dock_reveal(id); self.command_line .push_output(crate::t!("Browser opened.").as_ref()); } else { diff --git a/src/app/commands/pdf_underlay.rs b/src/app/commands/pdf_underlay.rs index ea01d01c4..67f584b14 100644 --- a/src/app/commands/pdf_underlay.rs +++ b/src/app/commands/pdf_underlay.rs @@ -377,12 +377,12 @@ impl OpenCADStudio { if !self.dock.is_placed(id) { self.dock.dock(id, crate::app::config::DockSide::Right, usize::MAX); } - self.dock_expanded = Some(id); + self.dock_reveal(id); } "POINTCLOUDMANAGERCLOSE" => { self.pc_manager.show = false; - if self.dock_expanded == Some(crate::ui::dock::PanelId::PointCloudManager) { - self.dock_expanded = None; + if self.dock_peek == Some(crate::ui::dock::PanelId::PointCloudManager) { + self.dock_peek = None; } } "_PCUNCROP" =>self.edit_selected_point_clouds(i, "POINTCLOUDUNCROP", |data| data.croppings.clear()), diff --git a/src/app/commands/sheet_set.rs b/src/app/commands/sheet_set.rs index 372ab1554..73fa949fa 100644 --- a/src/app/commands/sheet_set.rs +++ b/src/app/commands/sheet_set.rs @@ -580,9 +580,9 @@ impl OpenCADStudio { if !self.dock.is_placed(id) { self.dock.dock(id, crate::app::config::DockSide::Right, usize::MAX); } - self.dock_expanded = Some(id); - } else if self.dock_expanded == Some(id) { - self.dock_expanded = None; + self.dock_reveal(id); + } else if self.dock_peek == Some(id) { + self.dock_peek = None; } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 9f989d392..56c0e8628 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -845,41 +845,23 @@ pub(super) struct OpenCADStudio { pub(crate) bg_picker: Option, /// General edge-stack dock layout for the side panels. pub(crate) dock: crate::ui::dock::DockState, - /// Which panel is currently floated at full height (hovered, or a pinned - /// panel on top). - pub(crate) dock_expanded: Option, - /// Panel currently being dragged between sides / reordered. - pub(crate) dock_dragging: Option, - /// Panel currently being width-resized. - pub(crate) dock_resizing: Option, - /// Last pointer position during a drag / resize. + /// Pallet revealed although it auto-hides (its edge's group, or its + /// floating frame): hovered, being dragged, or just opened. Cleared when + /// the pointer leaves. + pub(crate) dock_peek: Option, + /// What the pointer is dragging in the dock (pallet, group, width, + /// splitter, floating size, Layer Manager column). + pub(crate) dock_drag: Option, + /// Pallet whose strip icon (or floating frame) the pointer is over. + pub(crate) dock_icon_hover: Option, + /// Last pointer position during a dock drag. pub(crate) dock_drag_last: Option, - /// Live drop target, shown as a preview while dragging. `None` until the - /// pointer has moved far enough to count as a drag, so a click on a title - /// bar or tab never moves the panel. - pub(crate) dock_drag_target: Option, - /// First pointer position of the current panel drag. - pub(crate) dock_drag_origin: Option, - /// Pointer offset from a dragged panel's top-left corner, so a floated - /// panel lands where it is held rather than jumping to the pointer. - pub(crate) dock_grab_offset: iced::Vector, - /// Splitter being dragged between two pallets of a group (side, group, - /// upper, lower). - pub(crate) dock_split_drag: Option<(crate::app::config::DockSide, usize, usize, usize)>, - /// Floating panel being resized from its corner grip, and whether from - /// the bottom-left corner. - pub(crate) dock_float_resizing: Option<(crate::ui::dock::PanelId, bool)>, /// Docked panel whose title bar the pointer is over (shows pin / close). pub(crate) dock_title_hover: Option, /// Edge whose icon-strip pallet menu (+) is open. pub(crate) dock_edge_menu: Option, - /// Group being dragged by its strip grip (side, slot index). While set, - /// `dock_dragging` holds the group's shown panel for the preview. - pub(crate) dock_dragging_group: Option<(crate::app::config::DockSide, usize)>, /// Group whose edge band (group grip) the pointer is over. pub(crate) dock_grip_hover: Option<(crate::app::config::DockSide, usize)>, - /// Pallet whose strip icon the pointer is over. - pub(crate) dock_icon_hover: Option, /// Reference-table column currently being width-resized (column index), /// with the last pointer position. Mirrors the Layers Name-column drag. pub(crate) xref_col_drag: Option, @@ -968,9 +950,6 @@ pub(super) struct OpenCADStudio { modal_drag_last: Option, /// True while the modal title bar is held (a drag is in progress). modal_dragging: bool, - /// Layer Manager: dragging the Name-column divider (width follows the - /// shared ModalDragMove flow). - layer_col_dragging: bool, /// Layer Manager Name column width in px, adjusted by the divider drag. layer_name_col_w: f32, /// How far the user has dragged the modal's corner resize grip from the @@ -4310,18 +4289,11 @@ impl OpenCADStudio { block_palette: Default::default(), xref_manager: Default::default(), dock: Default::default(), - dock_expanded: None, - dock_dragging: None, - dock_resizing: None, + dock_peek: None, + dock_drag: None, dock_drag_last: None, - dock_drag_target: None, - dock_drag_origin: None, - dock_grab_offset: iced::Vector::new(0.0, 0.0), - dock_split_drag: None, - dock_float_resizing: None, dock_title_hover: None, dock_edge_menu: None, - dock_dragging_group: None, dock_grip_hover: None, dock_icon_hover: None, xref_col_drag: None, @@ -4360,7 +4332,6 @@ impl OpenCADStudio { modal_offset: iced::Vector::ZERO, modal_drag_last: None, modal_dragging: false, - layer_col_dragging: false, layer_name_col_w: 130.0, modal_resize: iced::Vector::ZERO, modal_content_size: None, diff --git a/src/app/node_graph.rs b/src/app/node_graph.rs index 7a7e9bd34..3af60f9d7 100644 --- a/src/app/node_graph.rs +++ b/src/app/node_graph.rs @@ -177,7 +177,7 @@ impl OpenCADStudio { self.dock .dock(id, crate::app::config::DockSide::Right, usize::MAX); } - self.dock_expanded = Some(id); + self.dock_reveal(id); return self.graph_run(i); } } diff --git a/src/app/update/blocks_palette.rs b/src/app/update/blocks_palette.rs index 003c97c78..8eb913c8e 100644 --- a/src/app/update/blocks_palette.rs +++ b/src/app/update/blocks_palette.rs @@ -124,7 +124,7 @@ impl OpenCADStudio { /// BLOCKSPALETTE: open (expanded) on `tab`, or on the tab it was left on. pub(crate) fn open_blocks_palette(&mut self, tab: Option) { self.show_block_palette = true; - self.dock_expanded = Some(crate::ui::dock::PanelId::BlockPalette); + self.dock_reveal(crate::ui::dock::PanelId::BlockPalette); self.show_block_palette_tab(tab.unwrap_or(self.block_palette.tab)); } diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index f799a40b1..0ccc36442 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -545,12 +545,9 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven /// whichever panel the message names. pub(super) fn on_dock(&mut self, m: crate::ui::dock::DockMsg) -> iced::Task { use crate::app::config::DockSide; - use crate::ui::dock::{DockMsg, DropTarget, FloatPanel, PanelId}; + use crate::ui::dock::{DockDrag, DockMsg, DropTarget, FloatPanel}; match m { - DockMsg::DockGrab(id) => { - self.dock_begin_drag(id); - iced::Task::none() - } + DockMsg::DockGrab(id) => self.dock_begin(DockDrag::moving(id, None), Some(id)), DockMsg::IconPress(id) => { // An icon press shows the icon's group and may also start // dragging the pallet; a plain click never passes the drag @@ -558,56 +555,54 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven if self.dock.show_group_of(id) { self.save_config(); } - self.dock_begin_drag(id); - iced::Task::none() + self.dock_begin(DockDrag::moving(id, None), Some(id)); } - DockMsg::ResizeGrab(id) => { - self.dock_resizing = Some(id); - self.dock_dragging = None; - self.dock_drag_last = None; - self.dock_drag_target = None; - self.dock_expanded = Some(id); - iced::Task::none() + DockMsg::GroupGrab(side, gi) => { + if let Some(&id) = self.dock_group_visible(side, gi).first() { + self.dock_begin(DockDrag::moving(id, Some((side, gi))), Some(id)); + } } - DockMsg::SplitGrab(side, gi, upper, lower) => { - self.dock_split_drag = Some((side, gi, upper, lower)); - self.dock_dragging = None; - self.dock_drag_last = None; - self.dock_drag_target = None; - iced::Task::none() + DockMsg::ResizeGrab(id) => self.dock_begin(DockDrag::Width(id), Some(id)), + DockMsg::SplitGrab(side, group, upper, lower) => self.dock_begin( + DockDrag::Split { + side, + group, + upper, + lower, + }, + None, + ), + DockMsg::FloatResizeGrab(panel, from_left) => { + self.dock.raise_float(panel); + self.dock_begin(DockDrag::FloatSize { panel, from_left }, None); } DockMsg::SplitReset(side, gi) => { self.dock.reset_splits(side, gi); self.save_config(); - iced::Task::none() } - DockMsg::FloatResizeGrab(id, from_left) => { - self.dock.raise_float(id); - self.dock_float_resizing = Some((id, from_left)); - self.dock_dragging = None; - self.dock_drag_last = None; - self.dock_drag_target = None; - iced::Task::none() - } - DockMsg::TitleHover(id) => { - self.dock_title_hover = id; - iced::Task::none() + DockMsg::WidthReset(id) => { + if let Some((side, gi)) = self.dock.location(id) { + self.dock.reset_group_width(side, gi); + } else { + self.dock.reset_width(id); + } + self.save_config(); } + DockMsg::TitleHover(id) => self.dock_title_hover = id, + DockMsg::GripHover(grip) => self.dock_grip_hover = grip, + DockMsg::FloatRaise(id) => self.dock.raise_float(id), 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 strip = crate::ui::dock::DOCK_STRIP_W; let x = match self.dock.location(id) { Some((DockSide::Right, _)) => { - ww - crate::ui::dock::DOCK_STRIP_W - - self.dock_column_width(DockSide::Right) - - w - - 60.0 + ww - strip - self.dock_column_width(DockSide::Right) - w - 60.0 } - _ => crate::ui::dock::DOCK_STRIP_W + self.dock_column_width(DockSide::Left) + 40.0, + _ => strip + self.dock_column_width(DockSide::Left) + 40.0, }; - self.dock_dragging = None; - self.dock_drag_target = None; + self.dock_drag = None; if self.dock.float(FloatPanel { id, x: x.max(0.0), @@ -617,328 +612,344 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven }) { self.save_config(); } - iced::Task::none() } DockMsg::DockTo(id, side) => { - self.dock_dragging = None; - self.dock_drag_target = None; + self.dock_drag = None; if self.dock.dock(id, side, usize::MAX) { - self.dock_expanded = Some(id); self.save_config(); } - iced::Task::none() - } - DockMsg::EdgeMenu(side) => { - self.dock_edge_menu = side; - iced::Task::none() } + DockMsg::EdgeMenu(side) => self.dock_edge_menu = side, DockMsg::EdgeMenuToggle(side, id) => { // Checked = open and docked on this edge: unchecking hides it. // Anything else (closed, floating, on the other edge) moves - // here and opens. + // here and opens, in its own group here if it has one, else + // as a new group; adding a pallet ends the menu's job. let here = self.dock.location(id).map(|(s, _)| s) == Some(side); - if here && self.dock_panel_visible(id) { - let task = self.on_dock(DockMsg::Close(id)); + if !here { + self.dock.dock(id, side, usize::MAX); + } else if self.dock_panel_visible(id) { + let task = self.dock_set_open(id, false); self.save_config(); return task; } - // A pallet opened from an edge's menu lands on that edge: in - // its own group there if it has one, else as a new group. - if self.dock.location(id).map(|(s, _)| s) != Some(side) { - self.dock.dock(id, side, usize::MAX); - } - let task = self.dock_open_panel(id); - self.dock.show_group_of(id); - // Adding a pallet is the end of the menu's job. self.dock_edge_menu = None; + let task = self.dock_set_open(id, true); self.save_config(); - task - } - DockMsg::GripHover(grip) => { - self.dock_grip_hover = grip; - iced::Task::none() - } - DockMsg::IconHover(id) => { - self.dock_icon_hover = id; - iced::Task::none() - } - DockMsg::GroupGrab(side, gi) => { - if let Some(&id) = self.dock_group_visible(side, gi).first() { - self.dock_begin_drag(id); - self.dock_dragging_group = Some((side, gi)); - } - iced::Task::none() - } - DockMsg::FloatRaise(id) => { - self.dock.raise_float(id); - iced::Task::none() - } - DockMsg::WidthReset(id) => { - self.dock.reset_width(id); - self.dock.set_group_width(id, self.dock.settings(id).width); - self.save_config(); - iced::Task::none() + return task; } DockMsg::AutoCollapseToggle(id) => { // Docked: auto-hide for the whole edge; floating: this pallet. - let on = self.dock.toggle_auto_hide(id); - self.dock_expanded = if on { None } else { Some(id) }; + if self.dock.toggle_auto_hide(id) { + self.dock_peek = None; + } self.save_config(); - iced::Task::none() - } - DockMsg::Close(id) => { - match id { - PanelId::BlockPalette => { - self.show_block_palette = false; - self.block_palette.placing = None; - } - PanelId::ExternalReferences => { - self.show_external_references = false; - } - PanelId::Browser => { - self.show_browser = false; - } - PanelId::NodeGraph => { - self.show_node_graph = false; - } - PanelId::PointCloudManager => { - self.pc_manager.show = false; - } - PanelId::Count => { - self.count_palette.show = false; - self.ribbon.set_count_palette(false); - } - PanelId::SheetSetManager => { - self.sheet_set.show = false; - self.ribbon.set_sheet_set(false); - } - PanelId::Properties => { - 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; - } - if self.dock_dragging == Some(id) { - self.dock_dragging = None; - } - if self.dock_resizing == Some(id) { - self.dock_resizing = None; - } - iced::Task::none() } + DockMsg::Close(id) => return self.dock_set_open(id, false), DockMsg::Hover(id) => { - // Ignored while dragging/resizing: the pointer is over the - // drag preview, not a rail, so a hover must not collapse the - // panel being dragged or repoint the resize target. + self.dock_icon_hover = id; // Only an auto-hiding edge (or floating pallet) reacts to - // hover: it flies the hovered group out. Otherwise groups - // switch on click. - if self.dock_dragging.is_none() - && self.dock_resizing.is_none() - && self.dock.auto_hides(id) - { - self.dock_expanded = Some(id); - self.dock.show_group_of(id); + // hover: it reveals the hovered pallet's group. Otherwise + // groups switch on click. Ignored mid-drag, when the pointer + // is over the preview rather than the strip. + if let Some(id) = id { + if self.dock_drag.is_none() && self.dock.auto_hides(id) { + self.dock_reveal(id); + } } - iced::Task::none() } DockMsg::HoverExit => { - if self.dock_dragging.is_none() && self.dock_resizing.is_none() { - if let Some(id) = self.dock_expanded { - if self.dock.auto_hides(id) { - self.dock_expanded = None; - } - } - } - iced::Task::none() - } - DockMsg::DragMove(point) => { - // NOTE: xref column drags intentionally do NOT ride this - // path: its points live in workspace space while the header - // tracker reports header-local points, and mixing the two - // produced a one-time jump plus a stuck drag. Column moves - // arrive via Message::XrefColMove only. - if let Some(id) = self.dock_dragging { - let origin = *self.dock_drag_origin.get_or_insert(point); - let moved = (point.x - origin.x).hypot(point.y - origin.y); - if self.dock_drag_target.is_none() && moved >= DOCK_DRAG_THRESHOLD { - // Hold a floating panel where it was grabbed; a docked - // one by its title bar. - self.dock_grab_offset = match self.dock.float_rect(id) { - Some(f) => iced::Vector::new(origin.x - f.x, origin.y - f.y), - None => iced::Vector::new( - (self.dock.width(id, self.win_size.0) * 0.5).min(120.0), - 14.0, - ), - }; - } - if self.dock_drag_target.is_some() || moved >= DOCK_DRAG_THRESHOLD { - let target = self.dock_drop_target(point); - // A whole group only moves between group positions: - // it cannot join another group or float. - self.dock_drag_target = match (self.dock_dragging_group, target) { - (Some(_), DropTarget::Edge { .. }) | (None, _) => Some(target), - (Some(_), _) => None, - }; - } - } else if let Some((side, gi, upper, lower)) = self.dock_split_drag { - if let Some(last) = self.dock_drag_last { - let (_, avail) = self.dock_workspace_size(); - let total: f32 = self - .dock_slot_spans(side) - .iter() - .map(|(i, _, _)| self.dock.groups(side)[gi].weights[*i]) - .sum(); - if avail > 0.0 && total > 0.0 { - let per_px = total / avail; - self.dock.shift_split( - side, - gi, - upper, - lower, - (point.y - last.y) * per_px, - crate::ui::dock::GROUP_MIN_H * per_px, - ); - } - } - } else if let Some((id, from_left)) = self.dock_float_resizing { - if let (Some(last), Some(f)) = (self.dock_drag_last, self.dock.float_rect(id)) { - let dx = point.x - last.x; - self.dock.resize_float( - id, - if from_left { f.w - dx } else { f.w + dx }, - f.h + point.y - last.y, - 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; - let delta = match self.dock.location(id) { - Some((DockSide::Left, _)) => dx, - _ => -dx, - }; - // The divider sizes the panel's group; every group - // keeps its own width. - let cur = match self.dock.location(id) { - Some((side, gi)) => self.dock_group_width(side, gi), - None => self.dock.settings(id).width, - } + delta; - self.dock.set_group_width(id, cur); - } - } - if self.dock_dragging.is_some() - || self.dock_resizing.is_some() - || self.dock_split_drag.is_some() - || self.dock_float_resizing.is_some() - || self.layer_col_dragging + if self.dock_drag.is_none() + && self.dock_peek.is_some_and(|id| self.dock.auto_hides(id)) { - self.dock_drag_last = Some(point); + self.dock_peek = None; } - iced::Task::none() } + DockMsg::DragMove(point) => self.dock_drag_move(point), DockMsg::DragRelease => { - let mut changed = self.dock_resizing.is_some() - || self.dock_split_drag.is_some() - || self.dock_float_resizing.is_some(); - if let (Some((side, gi)), Some(DropTarget::Edge { side: to, index })) = - (self.dock_dragging_group, self.dock_drag_target) - { - changed |= self.dock.move_group(side, gi, to, index); - } else if let (Some(id), Some(target)) = (self.dock_dragging, self.dock_drag_target) { - let moved = match target { - DropTarget::Edge { side, index } => self.dock.dock(id, side, index), - DropTarget::Join { side, group, index } => { - self.dock.join_group(id, side, group, index) - } - DropTarget::Float { x, y } => { - let (w, h) = self.dock_float_size(id); - self.dock.float(FloatPanel { id, x, y, w, h }) - } - }; - if moved { - self.dock_expanded = Some(id); + let mut changed = false; + match self.dock_drag.take() { + Some(DockDrag::Move { + group: Some((side, gi)), + target: Some(DropTarget::Edge { side: to, index }), + .. + }) => changed = self.dock.move_group(side, gi, to, index), + Some(DockDrag::Move { + panel, + group: None, + target: Some(target), + .. + }) => { + changed = match target { + DropTarget::Edge { side, index } => self.dock.dock(panel, side, index), + DropTarget::Join { side, group, index } => { + self.dock.join_group(panel, side, group, index) + } + DropTarget::Float { x, y } => { + let (w, h) = self.dock_float_size(panel); + self.dock.float(FloatPanel { + id: panel, + x, + y, + w, + h, + }) + } + }; + } + Some(DockDrag::Move { .. } | DockDrag::LayerColumn) | None => {} + Some(DockDrag::Width(_) | DockDrag::Split { .. } | DockDrag::FloatSize { .. }) => { + changed = true; } - changed |= moved; } if changed { self.save_config(); } - self.dock_dragging = None; - self.dock_dragging_group = None; - self.dock_resizing = None; - self.dock_split_drag = None; - self.dock_float_resizing = None; self.dock_drag_last = None; - self.dock_drag_origin = None; - self.dock_drag_target = None; self.xref_col_drag = None; self.xref_col_last = None; self.xref_split_drag = false; - self.layer_col_dragging = false; - iced::Task::none() } } - } - - /// Show panel `id` where the layout has it, running its usual open path - /// (refreshes, ribbon highlight) when it was closed. - fn dock_open_panel(&mut self, id: crate::ui::dock::PanelId) -> iced::Task { - use crate::ui::dock::PanelId; - if self.dock_panel_visible(id) { - return iced::Task::none(); - } - match id { - PanelId::Properties => { - self.show_properties = true; - self.ribbon.set_properties(true); - } - PanelId::BlockPalette => self.open_blocks_palette(None), - PanelId::ExternalReferences => { - self.show_external_references = true; - self.refresh_xref_manager(); - } - PanelId::Browser => self.show_browser = true, - PanelId::NodeGraph => { - return self.on_graph(crate::ui::node_graph::GraphMsg::Toggle); - } - 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() } - /// Start a (not yet confirmed) drag of `id`: it only moves once the - /// pointer travels past `DOCK_DRAG_THRESHOLD`. - fn dock_begin_drag(&mut self, id: crate::ui::dock::PanelId) { - self.dock.raise_float(id); - self.dock_dragging = Some(id); - self.dock_dragging_group = None; - self.dock_edge_menu = None; - self.dock_resizing = None; - self.dock_split_drag = None; - self.dock_float_resizing = None; + /// Pointer motion while something in the dock is dragged. + fn dock_drag_move(&mut self, point: iced::Point) { + // NOTE: xref column drags intentionally do NOT ride this path: its + // points live in workspace space while the header tracker reports + // header-local points, and mixing the two produced a one-time jump + // plus a stuck drag. Column moves arrive via Message::XrefColMove + // only. + use crate::app::config::DockSide; + use crate::ui::dock::{DockDrag, DropTarget}; + let last = self.dock_drag_last.replace(point); + let Some(drag) = self.dock_drag else { + return; + }; + match drag { + DockDrag::Move { + panel, + group, + origin, + target, + grab, + } => { + let origin = origin.unwrap_or(point); + let moved = (point.x - origin.x).hypot(point.y - origin.y); + let started = target.is_some() || moved >= DOCK_DRAG_THRESHOLD; + // Hold a floating panel where it was grabbed; a docked one by + // its title bar. + let grab = if target.is_none() && started { + match self.dock.float_rect(panel) { + Some(f) => iced::Vector::new(origin.x - f.x, origin.y - f.y), + None => iced::Vector::new( + (self.dock.width(panel, self.win_size.0) * 0.5).min(120.0), + 14.0, + ), + } + } else { + grab + }; + // Store the grab first: the drop target reads it. + self.dock_drag = Some(DockDrag::Move { + panel, + group, + origin: Some(origin), + target, + grab, + }); + if !started { + return; + } + let found = self.dock_drop_target(point); + // A whole group only moves between group positions: it + // cannot join another group or float. + let target = match (group, found) { + (Some(_), DropTarget::Edge { .. }) | (None, _) => Some(found), + (Some(_), _) => None, + }; + self.dock_drag = Some(DockDrag::Move { + panel, + group, + origin: Some(origin), + target, + grab, + }); + } + DockDrag::Split { + side, + group, + upper, + lower, + } => { + let Some(last) = last else { return }; + let (_, avail) = self.dock_workspace_size(); + let total: f32 = self + .dock_slot_spans(side) + .iter() + .map(|(i, _, _)| self.dock.groups(side)[group].weights[*i]) + .sum(); + if avail > 0.0 && total > 0.0 { + let per_px = total / avail; + self.dock.shift_split( + side, + group, + upper, + lower, + (point.y - last.y) * per_px, + crate::ui::dock::GROUP_MIN_H * per_px, + ); + } + } + DockDrag::FloatSize { panel, from_left } => { + let (Some(last), Some(f)) = (last, self.dock.float_rect(panel)) else { + return; + }; + let dx = point.x - last.x; + self.dock.resize_float( + panel, + if from_left { f.w - dx } else { f.w + dx }, + f.h + point.y - last.y, + from_left, + ); + } + DockDrag::LayerColumn => { + let Some(last) = last else { return }; + self.layer_name_col_w = + (self.layer_name_col_w + point.x - last.x).clamp(60.0, 640.0); + } + DockDrag::Width(id) => { + let Some(last) = last else { return }; + let dx = point.x - last.x; + match self.dock.location(id) { + // The divider sizes the panel's group. + Some((side, gi)) => { + let delta = if side == DockSide::Left { dx } else { -dx }; + let cur = self.dock_group_width(side, gi) + delta; + self.dock.set_group_width(side, gi, cur); + } + None => { + let cur = self.dock.settings(id).width + dx; + self.dock.set_width(id, cur); + } + } + } + } + } + + /// Start `drag`, peeking `peek` (a pallet being dragged or resized stays + /// shown on an auto-hiding edge). + fn dock_begin(&mut self, drag: crate::ui::dock::DockDrag, peek: Option) { + if let crate::ui::dock::DockDrag::Move { panel, .. } = drag { + self.dock.raise_float(panel); + } + self.dock_drag = Some(drag); self.dock_drag_last = None; - self.dock_drag_origin = None; - self.dock_drag_target = None; - self.dock_expanded = Some(id); + self.dock_edge_menu = None; + if peek.is_some() { + self.dock_peek = peek; + } + } + + /// Bring `id` into view: show its group on its edge and, where it + /// auto-hides, keep it revealed until the pointer leaves. + pub(crate) fn dock_reveal(&mut self, id: crate::ui::dock::PanelId) { + self.dock.show_group_of(id); + self.dock_peek = Some(id); + } + + /// Open or close pallet `id`, running its usual open path (refreshes, + /// ribbon highlight) or close path. An opened pallet is revealed where + /// the layout has it. + pub(crate) fn dock_set_open( + &mut self, + id: crate::ui::dock::PanelId, + open: bool, + ) -> iced::Task { + use crate::ui::dock::PanelId; + if open { + let task = if self.dock_panel_visible(id) { + iced::Task::none() + } else { + match id { + PanelId::Properties => { + self.show_properties = true; + self.ribbon.set_properties(true); + iced::Task::none() + } + PanelId::BlockPalette => { + self.open_blocks_palette(None); + iced::Task::none() + } + PanelId::ExternalReferences => { + self.show_external_references = true; + self.refresh_xref_manager(); + iced::Task::none() + } + PanelId::Browser => { + self.show_browser = true; + iced::Task::none() + } + PanelId::NodeGraph => self.on_graph(crate::ui::node_graph::GraphMsg::Toggle), + PanelId::PointCloudManager => { + self.pc_manager.show = true; + iced::Task::none() + } + PanelId::Count => { + self.set_count_palette(true); + iced::Task::none() + } + PanelId::SheetSetManager => { + self.show_sheet_set_manager(true); + iced::Task::none() + } + PanelId::Layers => { + self.sync_ribbon_layers(); + self.show_layers = true; + iced::Task::none() + } + } + }; + self.dock_reveal(id); + return task; + } + match id { + PanelId::BlockPalette => { + self.show_block_palette = false; + self.block_palette.placing = None; + } + PanelId::ExternalReferences => self.show_external_references = false, + PanelId::Browser => self.show_browser = false, + PanelId::NodeGraph => self.show_node_graph = false, + PanelId::PointCloudManager => self.pc_manager.show = false, + PanelId::Count => { + self.count_palette.show = false; + self.ribbon.set_count_palette(false); + } + PanelId::SheetSetManager => { + self.sheet_set.show = false; + self.ribbon.set_sheet_set(false); + } + PanelId::Properties => { + self.show_properties = false; + self.ribbon.set_properties(false); + } + PanelId::Layers => { + self.show_layers = false; + self.ribbon.deactivate_tool_if("LAYERS"); + } + } + if self.dock_peek == Some(id) { + self.dock_peek = None; + } + if self + .dock_drag + .is_some_and(|d| d.movement().is_some_and(|(p, _, _)| p == id) || d == crate::ui::dock::DockDrag::Width(id)) + { + self.dock_drag = None; + } + iced::Task::none() } /// Whether `id` is currently rendered (not closed, not on the start screen / @@ -1015,18 +1026,13 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven }; !self.dock.edge_auto_hide(side) || self - .dock_expanded + .dock_peek .is_some_and(|e| self.dock_group_visible(side, gi).contains(&e)) } - /// Width of group `gi` on `side`: its pallets share the widest one's - /// width. + /// On-screen width of group `gi` on `side`. pub(crate) fn dock_group_width(&self, side: crate::app::config::DockSide, gi: usize) -> f32 { - self.dock.groups(side)[gi] - .panels - .iter() - .map(|id| self.dock.width(*id, self.win_size.0)) - .fold(0.0, f32::max) + self.dock.group_width_px(side, gi, self.win_size.0) } /// Width of the pallet column beside `side`'s icon strip: the shown @@ -1149,8 +1155,12 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven } } } - let x = (p.x - self.dock_grab_offset.x).clamp(0.0, (ww - 60.0).max(0.0)); - let y = (p.y - self.dock_grab_offset.y).clamp(0.0, (wh - 30.0).max(0.0)); + let grab = match self.dock_drag { + Some(crate::ui::dock::DockDrag::Move { grab, .. }) => grab, + _ => iced::Vector::new(0.0, 0.0), + }; + let x = (p.x - grab.x).clamp(0.0, (ww - 60.0).max(0.0)); + let y = (p.y - grab.y).clamp(0.0, (wh - 30.0).max(0.0)); DropTarget::Float { x, y } } @@ -2051,6 +2061,11 @@ mod tests { app } + /// Where the current dock drag would land, if it has started. + fn drag_target(app: &OpenCADStudio) -> Option { + app.dock_drag.and_then(|d| d.movement()).and_then(|(_, _, target)| target) + } + /// Press `id`'s title bar at `from`, drag to `to`, and return the target /// shown just before release. fn drag_panel( @@ -2063,7 +2078,7 @@ mod tests { let _ = app.on_dock(DockMsg::DockGrab(id)); let _ = app.on_dock(DockMsg::DragMove(from)); let _ = app.on_dock(DockMsg::DragMove(to)); - let target = app.dock_drag_target; + let target = drag_target(app); let _ = app.on_dock(DockMsg::DragRelease); target } @@ -2252,7 +2267,7 @@ mod tests { top + 2.0 * STRIP_CELL_H - 10.0, ))); assert_eq!( - app.dock_drag_target, + drag_target(&app), Some(DropTarget::Join { side: DockSide::Left, group: 0, @@ -2286,7 +2301,7 @@ mod tests { layout[0].icons_top + STRIP_CELL_H + 2.0, ))); assert_eq!( - app.dock_drag_target, + drag_target(&app), Some(DropTarget::Join { side: DockSide::Left, group: 0, @@ -2327,7 +2342,7 @@ mod tests { let _ = app.on_dock(DockMsg::GroupGrab(DockSide::Left, 1)); let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, 300.0))); let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(800.0, 400.0))); - assert_eq!(app.dock_drag_target, None); + assert_eq!(drag_target(&app), None); let _ = app.on_dock(DockMsg::DragRelease); assert_eq!(app.dock, before); } @@ -2382,7 +2397,7 @@ mod tests { assert!(!app.dock.edge_auto_hide(DockSide::Right)); assert!(!app.dock_edge_expanded(DockSide::Left)); // Hovering an icon flies its group out. - let _ = app.on_dock(DockMsg::Hover(PanelId::Browser)); + let _ = app.on_dock(DockMsg::Hover(Some(PanelId::Browser))); assert!(app.dock_edge_expanded(DockSide::Left)); assert_eq!(app.dock_shown_group(DockSide::Left), Some(1)); let _ = app.on_dock(DockMsg::HoverExit); @@ -2396,7 +2411,7 @@ mod tests { let mut app = dock_app(); app.show_browser = true; app.dock.left.push(DockGroup::single(PanelId::Browser)); - let _ = app.on_dock(DockMsg::Hover(PanelId::Browser)); + let _ = app.on_dock(DockMsg::Hover(Some(PanelId::Browser))); assert_eq!(app.dock_shown_group(DockSide::Left), Some(0)); assert!(app.dock_edge_expanded(DockSide::Left)); } @@ -2471,11 +2486,11 @@ mod tests { let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(300.0, 100.0))); let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(330.0, 100.0))); let _ = app.on_dock(DockMsg::DragRelease); - // The group was 260 wide (its widest pallet); both pallets take 290. - assert_eq!(app.dock.settings(PanelId::Properties).width, 290.0); - assert_eq!(app.dock.settings(PanelId::BlockPalette).width, 290.0); - assert_eq!(app.dock.settings(PanelId::Count).width, 280.0); + // The group was 260 wide (its widest pallet); it is now 290, while + // the other group and the pallets' own widths are untouched. assert_eq!(app.dock_group_width(DockSide::Left, 0), 290.0); + assert_eq!(app.dock_group_width(DockSide::Left, 1), 280.0); + assert_eq!(app.dock.settings(PanelId::Properties).width, 250.0); } #[test] @@ -2493,41 +2508,53 @@ mod tests { assert!(app.dock.float_rect(PanelId::Layers).is_some()); } - /// Drive the real view through iced's `UserInterface`: point at the - /// first text `label`, feed `events` one at a time, and between events - /// hand the published messages to the app and rebuild the view while - /// keeping the widget-tree cache, as the runtime does. - fn drive_view(app: &mut OpenCADStudio, label: &str, events: &[iced::Event]) { - use iced_test::core::{mouse, shell, widget, window, Size}; - use iced_test::runtime::user_interface::{Cache, UserInterface}; - use iced_test::selector::Bounded; - use iced_test::Selector; - let mut renderer = iced_test::futures::futures::executor::block_on( + /// A headless renderer for driving the real view. + fn headless_renderer() -> iced::Renderer { + iced_test::futures::futures::executor::block_on( ::new( iced_test::core::renderer::Settings::default(), None, ), ) - .expect("headless renderer"); - let size = Size::new(1600.0, 900.0); - let mut cache = Cache::default(); - let mut cursor = mouse::Cursor::Unavailable; - { - let mut ui = UserInterface::build(app.view_main(), size, cache, &mut renderer); - let mut find = Selector::find(label); - ui.operate(&renderer, &mut widget::operation::black_box(&mut find)); - if let widget::operation::Outcome::Some(Some(target)) = widget::Operation::finish(&find) { - if let Some(bounds) = target.visible_bounds() { - cursor = mouse::Cursor::Available(bounds.center()); - } - } - cache = ui.into_cache(); + .expect("headless renderer") + } + + /// Size the real view is laid out at. + const VIEW_SIZE: iced::Size = iced::Size::new(1600.0, 900.0); + + /// Centre of the first text `label` in the real view. + fn find_in_view(app: &OpenCADStudio, label: &str) -> iced::Point { + use iced_test::core::widget; + use iced_test::runtime::user_interface::{Cache, UserInterface}; + use iced_test::selector::Bounded; + use iced_test::Selector; + let mut renderer = headless_renderer(); + let mut ui = UserInterface::build(app.view_main(), VIEW_SIZE, Cache::default(), &mut renderer); + let mut find = Selector::find(label); + ui.operate(&renderer, &mut widget::operation::black_box(&mut find)); + match widget::Operation::finish(&find) { + widget::operation::Outcome::Some(Some(target)) => target + .visible_bounds() + .expect("visible") + .center(), + _ => panic!("{label} not found"), } - assert!(matches!(cursor, mouse::Cursor::Available(_)), "{label} not found"); + } + + /// Drive the real view through iced's `UserInterface` with the pointer at + /// `at`: feed `events` one at a time, and between events hand the + /// published messages to the app and rebuild the view while keeping the + /// widget-tree cache, as the runtime does. + fn drive_view(app: &mut OpenCADStudio, at: iced::Point, events: &[iced::Event]) { + use iced_test::core::{mouse, shell, window}; + use iced_test::runtime::user_interface::{Cache, UserInterface}; + let mut renderer = headless_renderer(); + let mut cache = Cache::default(); + let cursor = mouse::Cursor::Available(at); for event in events { let mut messages = Vec::new(); { - let mut ui = UserInterface::build(app.view_main(), size, cache, &mut renderer); + let mut ui = UserInterface::build(app.view_main(), VIEW_SIZE, cache, &mut renderer); let _ = ui.update( &window::Headless, &shell::Waker::noop(), @@ -2552,13 +2579,42 @@ mod tests { let release = iced::Event::Mouse(iced::mouse::Event::ButtonReleased(iced::mouse::Button::Left)); // The block palette's title text sits in its docked title bar. - drive_view(&mut app, "Blocks", &[press.clone(), release.clone(), press, release]); + let title = find_in_view(&app, "Blocks"); + drive_view(&mut app, title, &[press.clone(), release.clone(), press, release]); assert!( app.dock.float_rect(PanelId::BlockPalette).is_some(), "a double-click on a docked title bar floats the pallet" ); } + #[test] + fn hovering_a_strip_icon_reveals_an_auto_hiding_edge_in_the_real_view() { + use crate::app::config::DockSide; + use crate::ui::dock::{DockMsg, PanelId, STRIP_CELL_H, STRIP_PAD}; + let mut app = dock_app(); + // Mirror the layout onto the left so the block palette's title marks + // where the workspace starts, then auto-hide the right edge. + let title = find_in_view(&app, "Blocks"); + let _ = app.on_dock(DockMsg::AutoCollapseToggle(PanelId::BlockPalette)); + assert!(!app.dock_edge_expanded(DockSide::Right)); + // The title bar text sits ~13 px below the workspace top; the strip's + // first icon is centred STRIP_PAD + half a cell below it. + let workspace_top = title.y - 13.0; + let icon = iced::Point::new( + VIEW_SIZE.width - crate::ui::dock::DOCK_STRIP_W / 2.0, + workspace_top + STRIP_PAD + STRIP_CELL_H / 2.0, + ); + drive_view( + &mut app, + icon, + &[iced::Event::Mouse(iced::mouse::Event::CursorMoved { position: icon })], + ); + assert!( + app.dock_edge_expanded(DockSide::Right), + "hovering the icon reveals the auto-hiding edge" + ); + } + #[test] fn dock_double_clicks_float_and_dock_panels() { use crate::app::config::DockSide; @@ -2575,11 +2631,12 @@ mod tests { #[test] fn blockpalette_width_reset() { - let mut app = fresh(); + use crate::app::config::DockSide; + let mut app = dock_app(); let id = crate::ui::dock::PanelId::BlockPalette; - app.dock.set_width(id, 500.0); + app.dock.set_group_width(DockSide::Right, 0, 500.0); let _ = app.on_dock(crate::ui::dock::DockMsg::WidthReset(id)); - assert_eq!(app.dock.settings(id).width, 260.0); + assert_eq!(app.dock.group_width(DockSide::Right, 0), 260.0); } #[test] diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 4f39e772c..2da46524e 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -2573,7 +2573,7 @@ impl OpenCADStudio { self.save_config(); } self.dock.raise_float(PanelId::Layers); - self.dock_expanded = Some(PanelId::Layers); + self.dock_reveal(PanelId::Layers); Task::none() } @@ -2585,7 +2585,7 @@ impl OpenCADStudio { if !self.dock.is_placed(PanelId::ExternalReferences) { self.dock.dock(PanelId::ExternalReferences, DockSide::Right, usize::MAX); } - self.dock_expanded = Some(PanelId::ExternalReferences); + self.dock_reveal(PanelId::ExternalReferences); self.refresh_xref_manager(); } Task::none() @@ -9043,7 +9043,7 @@ impl OpenCADStudio { Message::LayerNameColGrab => { // Start a Name-column divider drag; the dock's pointer capture // feeds it (see `on_dock`). - self.layer_col_dragging = true; + self.dock_drag = Some(crate::ui::dock::DockDrag::LayerColumn); self.dock_drag_last = None; Task::none() } diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 160c1683e..0b1daa9f9 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1931,7 +1931,10 @@ bg={bg_ms:.1}ms n={view_count}" let mut layers: Vec> = vec![workspace]; for f in &self.dock.floating { // Once a drag is under way only its preview shows the panel. - let dragged = self.dock_dragging == Some(f.id) && self.dock_drag_target.is_some(); + let dragged = self + .dock_drag + .and_then(|d| d.movement()) + .is_some_and(|(panel, _, target)| panel == f.id && target.is_some()); if self.dock_panel_visible(f.id) && !dragged { layers.push(self.floating_panel(*f, tab)); } @@ -1966,7 +1969,7 @@ bg={bg_ms:.1}ms n={view_count}" } } // While a panel is dragged, preview where it lands. - if let (Some(id), Some(target)) = (self.dock_dragging, self.dock_drag_target) { + if let Some((id, _, Some(target))) = self.dock_drag.and_then(|d| d.movement()) { layers.push(self.dock_drop_preview(id, target)); } // Always a stack (and, below, always a mouse area), even with nothing @@ -1977,31 +1980,26 @@ bg={bg_ms:.1}ms n={view_count}" .width(Fill) .height(Fill) .into(); - let any_dragging = self.dock_dragging.is_some(); - 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 + use crate::ui::dock::DockDrag; + let capturing = self.dock_drag.is_some() || self.xref_col_drag.is_some() || self.xref_split_drag; - let capturing = any_dragging || any_resizing || any_splitting; let mut capture = mouse_area(workspace); if capturing { capture = capture .on_move(move |p| Message::Dock(crate::ui::dock::DockMsg::DragMove(p))) .on_release(Message::Dock(crate::ui::dock::DockMsg::DragRelease)) - .interaction(if let Some((_, from_left)) = self.dock_float_resizing { - if from_left { + .interaction(match self.dock_drag { + Some(DockDrag::FloatSize { from_left: true, .. }) => { iced::mouse::Interaction::ResizingDiagonallyUp - } else { + } + Some(DockDrag::FloatSize { from_left: false, .. }) => { iced::mouse::Interaction::ResizingDiagonallyDown } - } else if any_resizing { - iced::mouse::Interaction::ResizingHorizontally - } else if any_splitting { - iced::mouse::Interaction::ResizingVertically - } else { - iced::mouse::Interaction::Grabbing + Some(DockDrag::Split { .. }) => iced::mouse::Interaction::ResizingVertically, + Some(DockDrag::Move { .. }) => iced::mouse::Interaction::Grabbing, + // Widths, the Layer Manager column and the xref drags. + _ => iced::mouse::Interaction::ResizingHorizontally, }); } let workspace: Element<'_, Message> = capture.into(); @@ -2844,8 +2842,10 @@ impl OpenCADStudio { STRIP_PAD, STRIP_PLUS_GAP, }; let shown = self.dock_shown_group(side); - let dragging = self.dock_drag_target.is_some(); - let target_group = match self.dock_drag_target { + let movement = self.dock_drag.and_then(|d| d.movement()); + let move_target = movement.and_then(|(_, _, target)| target); + let dragging = move_target.is_some(); + let target_group = match move_target { Some(DropTarget::Join { side: s, group, .. }) if s == side => Some(group), _ => None, }; @@ -2858,7 +2858,8 @@ impl OpenCADStudio { let is_shown = shown == Some(gi); let lit = target_group == Some(gi) || (!dragging && self.dock_grip_hover == Some((side, gi))); - let group_dragged = dragging && self.dock_dragging_group == Some((side, gi)); + let group_dragged = + dragging && movement.is_some_and(|(_, group, _)| group == Some((side, gi))); let panels = self.dock_group_visible(side, gi); let row_h = 2.0 * STRIP_PAD + panels.len() as f32 * STRIP_CELL_H; @@ -2866,8 +2867,7 @@ impl OpenCADStudio { for id in panels { let faded = group_dragged || (dragging - && self.dock_dragging_group.is_none() - && self.dock_dragging == Some(id)); + && movement.is_some_and(|(panel, group, _)| group.is_none() && panel == id)); let glyph = if faded { crate::ui::icons::themed_disabled(id.icon(), 20.0) } else if is_shown { @@ -2894,8 +2894,8 @@ impl OpenCADStudio { .center_y(Length::Fixed(STRIP_CELL_H)), ) .on_press(Message::Dock(DockMsg::IconPress(id))) - .on_enter(Message::Dock(DockMsg::IconHover(Some(id)))) - .on_exit(Message::Dock(DockMsg::IconHover(None))) + .on_enter(Message::Dock(DockMsg::Hover(Some(id)))) + .on_exit(Message::Dock(DockMsg::Hover(None))) .interaction(iced::mouse::Interaction::Pointer); icons.push( iced::widget::tooltip(cell, text(id.title()).size(10), tip_side) @@ -3120,11 +3120,13 @@ impl OpenCADStudio { let (ww, wh) = self.dock_workspace_size(); // The bar faces the nearer workspace edge; while resizing, it stays // opposite the grip being dragged so it cannot flip mid-drag. - let bar_left = match self.dock_float_resizing { - Some((id, from_left)) if id == f.id => !from_left, + let bar_left = match self.dock_drag { + Some(crate::ui::dock::DockDrag::FloatSize { panel, from_left }) if panel == f.id => { + !from_left + } _ => f.x + (f.w + DOCK_FLOAT_BAR_W) * 0.5 < ww * 0.5, }; - let hidden = self.dock.auto_collapse(f.id) && self.dock_expanded != Some(f.id); + let hidden = self.dock.auto_collapse(f.id) && self.dock_peek != Some(f.id); let bar = floating_title_bar(f.id, self.dock.auto_collapse(f.id), bar_left); let content: Element<'_, Message> = if hidden { bar @@ -3190,7 +3192,7 @@ impl OpenCADStudio { // on its empty areas don't fall through to the drawing below. let panel = mouse_area(framed) .on_press(Message::Dock(DockMsg::FloatRaise(f.id))) - .on_enter(Message::Dock(DockMsg::Hover(f.id))) + .on_enter(Message::Dock(DockMsg::Hover(Some(f.id)))) .on_exit(Message::Dock(DockMsg::HoverExit)) .interaction(iced::mouse::Interaction::Idle); // A hidden panel keeps its title bar where it sits when shown. @@ -3248,7 +3250,7 @@ impl OpenCADStudio { DropTarget::Float { x, y } => { let (w, h) = self.dock_float_size(id); layers.push(place_at( - dock_ghost(&[id], id, w + DOCK_FLOAT_BAR_W, h), + dock_ghost(id, w + DOCK_FLOAT_BAR_W, h), x, y, )); @@ -3257,7 +3259,7 @@ impl OpenCADStudio { }; // A whole group shows only the strip feedback and its ghost; the // column keeps showing what is there. - let group_drag = self.dock_dragging_group; + let group_drag = self.dock_drag.and_then(|d| d.movement()).and_then(|(_, g, _)| g); let docked = docked.map(|(side, after)| (side, group_drag.is_none().then_some(after))); let marks_side = docked.as_ref().map(|(side, _)| *side); if let Some((side, Some(after))) = docked { @@ -3284,7 +3286,7 @@ impl OpenCADStudio { DockSide::Right => (ww - w - strip).max(0.0), }; layers.push(tint(side, w)); - layers.push(place_at(dock_ghost(&[id], id, w, bottom - top), x, top)); + layers.push(place_at(dock_ghost(id, w, bottom - top), x, top)); } } // On top: the strip's drop marks and a ghost of what is dragged @@ -3329,7 +3331,7 @@ impl OpenCADStudio { }; // Icons above the insertion point, counted as the strip shows // them (the dragged icon still sits in its old place). - let dragged = self.dock_dragging; + let dragged = self.dock_drag.and_then(|d| d.movement()).map(|(p, _, _)| p); let above = self.dock.groups(side)[group].panels[..index] .iter() .filter(|p| self.dock_panel_visible(**p) || Some(**p) == dragged) @@ -3992,47 +3994,27 @@ fn dock_splitter( .into() } -/// The blue drop ghost: a `w`×`h` panel outline whose header shows the tabs -/// the slot will hold, with the dragged panel's tab highlighted. -fn dock_ghost( - tabs: &[crate::ui::dock::PanelId], - dragged: crate::ui::dock::PanelId, - w: f32, - h: f32, -) -> Element<'static, Message> { - let cells: Vec> = tabs - .iter() - .map(|id| { - let is_dragged = *id == dragged; - container( - text(id.title()) - .size(12) - .wrapping(iced::widget::text::Wrapping::None) - .ellipsis(iced::advanced::text::Ellipsis::End), - ) - .width(Fill) - .padding([5, 8]) - .clip(true) - .style(move |theme: &Theme| { - let palette = theme.palette(); - container::Style { - background: Some(Background::Color(if is_dragged { - palette.primary.base.color - } else { - palette.primary.weak.color - })), - text_color: Some(if is_dragged { - palette.primary.base.text - } else { - palette.primary.weak.text - }), - ..Default::default() - } - }) - .into() - }) - .collect(); - container(column![row(cells).spacing(2), Space::new()]) +/// The blue drop ghost: a `w`×`h` outline of where pallet `id` lands, with +/// its name as the header. +fn dock_ghost(id: crate::ui::dock::PanelId, w: f32, h: f32) -> Element<'static, Message> { + let header = container( + text(id.title()) + .size(12) + .wrapping(iced::widget::text::Wrapping::None) + .ellipsis(iced::advanced::text::Ellipsis::End), + ) + .width(Fill) + .padding([5, 8]) + .clip(true) + .style(|theme: &Theme| { + let palette = theme.palette(); + container::Style { + background: Some(Background::Color(palette.primary.base.color)), + text_color: Some(palette.primary.base.text), + ..Default::default() + } + }); + container(column![header, Space::new()]) .width(Length::Fixed(w.max(1.0))) .height(Length::Fixed(h.max(1.0))) .clip(true) diff --git a/src/ui/dock.rs b/src/ui/dock.rs index 7909afc58..480c2c8dc 100644 --- a/src/ui/dock.rs +++ b/src/ui/dock.rs @@ -1,12 +1,12 @@ -//! General edge-stack docking for side panels. +//! Docking for side panels (pallets). //! -//! Any number of dockable panels (Properties, the block palette, future -//! palettes) live in an ordered vertical stack of slots on the left or right -//! edge of the drawing view, or float over it. A slot holds one panel or -//! several sharing it as tabs, and owns an adjustable share of the edge -//! height. This module owns the persisted layout (which panels are docked or -//! floating, where, at what size, and whether each auto-collapses) plus the -//! pure geometry used to render and hover an edge's stacked slots. +//! Pallets (Properties, the block palette, the Layer Manager, …) dock on the +//! left or right edge of the drawing view, or float over it. Each edge holds +//! an ordered list of groups and shows one group at a time; a group's pallets +//! stack top to bottom, each with an adjustable share of the height, and the +//! group has its own width. An icon strip along the edge lists every group. +//! This module owns the persisted layout and the pure geometry used to draw +//! and hit-test it; the app owns the transient drag / hover state. use crate::app::config::DockSide; use serde::{Deserialize, Serialize}; @@ -26,8 +26,10 @@ pub enum DockMsg { AutoCollapseToggle(PanelId), /// Close / hide `panel`. Close(PanelId), - /// The pointer is over `panel`, raising it to full height. - Hover(PanelId), + /// The pointer entered (`Some`) or left (`None`) a pallet's strip icon or + /// floating frame: highlights the icon and, where the pallet auto-hides, + /// reveals it. + Hover(Option), /// Pointer moved while a panel is dragging or resizing. DragMove(iced::Point), /// Pointer released after a drag / resize. @@ -55,8 +57,7 @@ pub enum DockMsg { GroupGrab(DockSide, usize), /// The pointer entered (`Some`) or left (`None`) a group's edge band. GripHover(Option<(DockSide, usize)>), - /// The pointer entered (`Some`) or left (`None`) a pallet's strip icon. - IconHover(Option), + /// 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. @@ -192,19 +193,24 @@ pub struct DockGroup { pub panels: Vec, /// Relative height of each pallet, parallel to `panels`. pub weights: Vec, + /// The group's docked width; 0 until first sized, meaning "the widest + /// of its pallets' own widths" (see [`DockState::group_width`]). + #[serde(default)] + pub width: f32, } impl DockGroup { pub fn single(id: PanelId) -> Self { - Self { - panels: vec![id], - weights: vec![1.0], - } + Self::stack(vec![id]) } pub fn stack(panels: Vec) -> Self { let weights = vec![1.0; panels.len()]; - Self { panels, weights } + Self { + panels, + weights, + width: 0.0, + } } /// Keep one sane weight per pallet (configs edited by hand, or written @@ -242,8 +248,7 @@ impl DockGroup { } /// On-disk forms of one entry of an edge list. Configs from before groups -/// stored each stacked pallet as a bare id; this branch's earlier builds -/// stored tab groups. +/// stored each stacked pallet as a bare id. #[derive(Deserialize)] #[serde(untagged)] enum GroupRepr { @@ -252,8 +257,9 @@ enum GroupRepr { panels: Vec, #[serde(default)] weights: Vec, + #[serde(default)] + width: f32, }, - Tabs { tabs: Vec }, } /// Read an edge list. Bare ids were pallets stacked on the edge, all shown @@ -268,8 +274,15 @@ where for repr in reprs { match repr { GroupRepr::Single(id) => legacy.push(id), - GroupRepr::Stack { panels, weights } => groups.push(DockGroup { panels, weights }), - GroupRepr::Tabs { tabs } => groups.push(DockGroup::stack(tabs)), + GroupRepr::Stack { + panels, + weights, + width, + } => groups.push(DockGroup { + panels, + weights, + width, + }), } } if !legacy.is_empty() { @@ -319,8 +332,8 @@ pub struct DockState { pub auto_hide: (bool, bool), /// Floating panels, back → front. pub floating: Vec, - /// Per-panel width / auto-collapse settings, keyed by `PanelId`. A - /// group's pallets share one width, so each group keeps its own. + /// Per-panel settings, keyed by `PanelId`: the width a pallet floats at + /// (and starts a new group with) and its floating auto-hide. pub panels: BTreeMap, } @@ -495,27 +508,53 @@ impl DockState { entry.auto_collapse = on; } - /// The saved width a group's pallets share: the widest of them, so a - /// group whose pallets were sized apart renders at one width. + /// Group `group`'s docked width: its own once sized, else the widest of + /// its pallets' own widths. pub fn group_width(&self, side: DockSide, group: usize) -> f32 { - self.groups(side)[group] - .panels + let g = &self.groups(side)[group]; + if g.width > 0.0 { + return g.width; + } + g.panels .iter() .map(|id| self.settings(*id).width) .fold(DOCK_MIN_W, f32::max) } - /// Set the width of `id` and of every other pallet in its group. - pub fn set_group_width(&mut self, id: PanelId, width: f32) { - let panels = match self.location(id) { - Some((side, gi)) => self.groups(side)[gi].panels.clone(), - None => vec![id], - }; - for p in panels { - self.set_width(p, width); + /// The widest group `group` may be: the most any of its pallets allows. + fn group_max_width(&self, side: DockSide, group: usize) -> f32 { + self.groups(side)[group] + .panels + .iter() + .map(|id| id.max_width()) + .fold(DOCK_MIN_W, f32::max) + } + + /// Group `group`'s on-screen width in a `win_w` wide window, clamped like + /// [`Self::width`]. + pub fn group_width_px(&self, side: DockSide, group: usize, win_w: f32) -> f32 { + let max = self.group_max_width(side, group).min(win_w * 0.45).max(DOCK_MIN_W); + self.group_width(side, group).clamp(DOCK_MIN_W, max) + } + + /// Size group `group` on `side`, clamped. + pub fn set_group_width(&mut self, side: DockSide, group: usize, width: f32) { + let max = self.group_max_width(side, group); + if let Some(g) = self.groups_mut(side).get_mut(group) { + g.width = width.clamp(DOCK_MIN_W, max); } } + /// Give group `group` the widest default width of its pallets. + pub fn reset_group_width(&mut self, side: DockSide, group: usize) { + let width = self.groups(side)[group] + .panels + .iter() + .map(|id| id.default_width()) + .fold(DOCK_MIN_W, f32::max); + self.set_group_width(side, group, width); + } + /// Take `id` out of the layout. Returns the group it left, its position /// and weight there, and whether the group disappeared (it held nothing /// else). @@ -555,8 +594,8 @@ impl DockState { /// Stack `id` into group `group` on `side` at position `index` (0 = top, /// `len` = bottom), both counted before the move, and show the group. A - /// pallet already in the group moves within it. A joining pallet takes on - /// the group's width. Returns whether the layout changed. + /// pallet already in the group moves within it; the group keeps its + /// width. Returns whether the layout changed. pub fn join_group(&mut self, id: PanelId, side: DockSide, group: usize, index: usize) -> bool { if group >= self.groups(side).len() { return false; @@ -567,8 +606,6 @@ impl DockState { return self.show_group(side, group); } let before = self.clone(); - let width = self.group_width(side, group); - let joining = !self.groups(side)[group].panels.contains(&id); let mut group = group; let mut index = index; let mut weight = self.groups(side)[group].mean_weight(); @@ -584,9 +621,6 @@ impl DockState { } } self.groups_mut(side)[group].insert(index, id, weight); - if joining { - self.set_width(id, width); - } self.show_group(side, group); *self != before } @@ -683,6 +717,64 @@ impl DockState { } } +/// What the pointer is dragging in the dock, if anything. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DockDrag { + /// Moving pallet `panel`, or with `group` that whole group (`panel` is + /// then its first open pallet, used for the preview). Nothing moves + /// until the pointer has travelled past the drag threshold from + /// `origin`; from then on `target` is where it would land and `grab` the + /// pointer's offset from the dragged frame's top-left corner. + Move { + panel: PanelId, + group: Option<(DockSide, usize)>, + origin: Option, + target: Option, + grab: iced::Vector, + }, + /// Sizing the width of `panel`'s group (or of `panel`, floating). + Width(PanelId), + /// Moving the splitter between pallets `upper` and `lower` of a group. + Split { + side: DockSide, + group: usize, + upper: usize, + lower: usize, + }, + /// Sizing floating `panel` from its bottom-left (`from_left`) or + /// bottom-right corner. + FloatSize { panel: PanelId, from_left: bool }, + /// Sizing the Layer Manager's Name column. + LayerColumn, +} + +impl DockDrag { + /// Start moving `panel` (a whole group with `group`). + pub fn moving(panel: PanelId, group: Option<(DockSide, usize)>) -> Self { + DockDrag::Move { + panel, + group, + origin: None, + target: None, + grab: iced::Vector::new(0.0, 0.0), + } + } + + /// The pallet being moved, the group if a whole group moves, and where + /// it would land (once past the drag threshold). + pub fn movement(&self) -> Option<(PanelId, Option<(DockSide, usize)>, Option)> { + match *self { + DockDrag::Move { + panel, + group, + target, + .. + } => Some((panel, group, target)), + _ => None, + } + } +} + /// Where a dragged panel lands on release. #[derive(Debug, Clone, Copy, PartialEq)] pub enum DropTarget { @@ -699,8 +791,8 @@ pub enum DropTarget { Float { x: f32, y: f32 }, } -/// Split `avail` pixels of height between slots of `weights`, returning each -/// slot's `(top, bottom)`. +/// Split `avail` pixels of height between stacked pallets of `weights`, +/// returning each one's `(top, bottom)`. pub fn slot_spans(weights: &[f32], avail: f32) -> Vec<(f32, f32)> { let total: f32 = weights.iter().sum(); if total <= 0.0 { @@ -717,7 +809,7 @@ pub fn slot_spans(weights: &[f32], avail: f32) -> Vec<(f32, f32)> { .collect() } -/// Integer share for iced's `FillPortion` from a slot weight. +/// Integer share for iced's `FillPortion` from a pallet's weight. pub fn portion(weight: f32) -> u16 { (weight * 1000.0).round().clamp(1.0, u16::MAX as f32) as u16 } @@ -728,9 +820,9 @@ pub const DOCK_MIN_W: f32 = 200.0; pub const DOCK_MAX_W: f32 = 600.0; /// Width of the band along an empty edge that docks a dragged panel there. pub const DOCK_EDGE_ZONE: f32 = 48.0; -/// Width of the grabbable divider between a docked slot and the viewport. +/// Width of the grabbable divider between a docked group and the viewport. pub const DOCK_DIVIDER_W: f32 = 5.0; -/// Width of an edge's vertical icon strip (the groups' tabs). +/// Width of an edge's vertical icon strip. pub const DOCK_STRIP_W: f32 = 40.0; /// Height of one pallet icon in the strip. pub const STRIP_CELL_H: f32 = 32.0; @@ -1106,8 +1198,8 @@ mod tests { vec![PanelId::Properties, PanelId::BlockPalette] ); assert_eq!(state.left[0].weights.len(), 2); - // It takes on the group's width. - assert_eq!(state.settings(PanelId::BlockPalette).width, 320.0); + // The group keeps its width. + assert_eq!(state.group_width(DockSide::Left, 0), 320.0); // Moving it above Properties keeps it in the group. assert!(state.join_group(PanelId::BlockPalette, DockSide::Left, 0, 0)); assert_eq!(state.left[0].panels[0], PanelId::BlockPalette); @@ -1199,6 +1291,23 @@ mod tests { assert_eq!(state.left[0].weights, vec![1.0, 1.0]); } + #[test] + fn group_width_is_its_own_once_set() { + let mut state = DockState::default(); + state.join_group(PanelId::Browser, DockSide::Left, 0, 1); + // Unset: the widest pallet (Properties 250 > Browser 230). + assert_eq!(state.group_width(DockSide::Left, 0), 250.0); + state.set_group_width(DockSide::Left, 0, 333.0); + assert_eq!(state.group_width(DockSide::Left, 0), 333.0); + // Pallet widths are untouched; other groups keep theirs. + assert_eq!(state.settings(PanelId::Properties).width, 250.0); + assert_eq!(state.group_width(DockSide::Right, 0), 260.0); + state.set_group_width(DockSide::Left, 0, 5000.0); + assert_eq!(state.group_width(DockSide::Left, 0), DOCK_MAX_W); + state.reset_group_width(DockSide::Left, 0); + assert_eq!(state.group_width(DockSide::Left, 0), 250.0); + } + #[test] fn auto_hide_is_per_edge_for_docked_and_per_pallet_when_floating() { let mut state = DockState::default();