Dock: simplify drag, peek, width and open/close state

No intended behaviour change except the fixes noted below.

- One drag state: a single `DockDrag` (move pallet / group, width,
  splitter, floating size, Layer Manager column) replaces nine fields
  (dragging, dragging group, resizing, split, float resizing, origin,
  target, grab offset, layer column flag) that had to be kept in sync.
- `dock_expanded` becomes `dock_peek` with one meaning: a pallet revealed
  although it auto-hides. `dock_reveal(id)` shows a pallet's group and
  peeks it; commands that open a pallet now use it, so opening a pallet
  also switches its edge to that pallet's group.
- Each group stores its own width (falling back to its widest pallet's
  width until first sized), instead of every pallet's width being kept in
  sync. Pallet widths now only size floating pallets and new groups.
- One `dock_set_open(id, open)` holds every pallet's open and close path.
- Leftovers removed: the tab-group config format that never reached main,
  the multi-tab drag ghost header, the separate icon-hover message.

Fixes:
- Hovering a strip icon reveals an auto-hiding edge again (the icons had
  lost the hover message in the option B strip). Covered by a test that
  moves the pointer over the icon in the real view.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013RyGiMXcsf9HTKfqDxaQGt
This commit is contained in:
Claude committed 2026-10-09 20:15:09 +00:00
1 parent 6c00b8bc91
commit 4db24b3bbd
12 files changed
+667 -548

No files matched your search

+2 -2
View File
@@ -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.
+3 -3
View File
@@ -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;
}
}
+1 -1
View File
@@ -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 {
+3 -3
View File
@@ -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()),
+3 -3
View File
@@ -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;
}
}
+12 -41
View File
@@ -845,41 +845,23 @@ pub(super) struct OpenCADStudio {
pub(crate) bg_picker: Option<BgTarget>,
/// 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<crate::ui::dock::PanelId>,
/// Panel currently being dragged between sides / reordered.
pub(crate) dock_dragging: Option<crate::ui::dock::PanelId>,
/// Panel currently being width-resized.
pub(crate) dock_resizing: Option<crate::ui::dock::PanelId>,
/// 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<crate::ui::dock::PanelId>,
/// What the pointer is dragging in the dock (pallet, group, width,
/// splitter, floating size, Layer Manager column).
pub(crate) dock_drag: Option<crate::ui::dock::DockDrag>,
/// Pallet whose strip icon (or floating frame) the pointer is over.
pub(crate) dock_icon_hover: Option<crate::ui::dock::PanelId>,
/// Last pointer position during a dock drag.
pub(crate) dock_drag_last: Option<iced::Point>,
/// 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<crate::ui::dock::DropTarget>,
/// First pointer position of the current panel drag.
pub(crate) dock_drag_origin: Option<iced::Point>,
/// 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<crate::ui::dock::PanelId>,
/// Edge whose icon-strip pallet menu (+) is open.
pub(crate) dock_edge_menu: Option<crate::app::config::DockSide>,
/// 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<crate::ui::dock::PanelId>,
/// 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<usize>,
@@ -968,9 +950,6 @@ pub(super) struct OpenCADStudio {
modal_drag_last: Option<Point>,
/// 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,
+1 -1
View File
@@ -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);
}
}
+1 -1
View File
@@ -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<Tab>) {
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));
}
+424 -367
View File
@@ -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<Message> {
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<Message> {
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<crate::ui::dock::PanelId>) {
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<Message> {
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<crate::ui::dock::DropTarget> {
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(
<iced::Renderer as iced_test::core::renderer::Headless>::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]
+3 -3
View File
@@ -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()
}
+55 -73
View File
@@ -1931,7 +1931,10 @@ bg={bg_ms:.1}ms n={view_count}"
let mut layers: Vec<Element<'_, Message>> = 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<Element<'static, Message>> = 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)
+159 -50
View File
@@ -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<PanelId>),
/// 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<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.
@@ -192,19 +193,24 @@ pub struct DockGroup {
pub panels: Vec<PanelId>,
/// Relative height of each pallet, parallel to `panels`.
pub weights: Vec<f32>,
/// 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<PanelId>) -> 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<PanelId>,
#[serde(default)]
weights: Vec<f32>,
#[serde(default)]
width: f32,
},
Tabs { tabs: Vec<PanelId> },
}
/// 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<FloatPanel>,
/// 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<PanelId, DockPanel>,
}
@@ -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<iced::Point>,
target: Option<DropTarget>,
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<DropTarget>)> {
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();