Dock: floating windows hold one pallet again

Remove floating groups: a floating window is a single pallet once more,
and the Pallets submenu is gone from its title-bar menu. Kept: the menu
opens at the pointer with Allow docking, Dock left and Dock right;
close / pin show only while the bar is hovered; content is clipped to
its frame. A config saved by the floating-group build still loads, its
windows floating their first pallet. Drops the unused Pallets string.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013RyGiMXcsf9HTKfqDxaQGt
This commit is contained in:
Claude committed 2026-10-10 11:32:51 +00:00
1 parent 75fb9e32dd
commit 2135dd499a
27 files changed
+245 -793

No files matched your search

+20 -50
View File
@@ -205,7 +205,7 @@ fn dock_drop_over_the_viewport_floats_the_panel() {
);
assert!(matches!(target, Some(DropTarget::Float { .. })));
assert!(app.dock.right.is_empty());
let f = app.dock.float_rect(PanelId::BlockPalette).expect("floating").clone();
let f = app.dock.float_rect(PanelId::BlockPalette).expect("floating");
assert!(f.x > 600.0 && f.x < 800.0 && f.y > 350.0 && f.y < 400.0);
// Dragging a floating panel keeps the grab point under the pointer.
let target = drag_panel(
@@ -275,53 +275,14 @@ fn dock_grip_drag_moves_a_whole_group() {
app.dock.left[1].panels,
vec![PanelId::Properties, PanelId::Browser]
);
// A group dropped over the viewport floats as one window.
// A group dropped over the viewport does not float or split up.
let before = app.dock.clone();
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!(drag_target(&app), None);
let _ = app.on_dock(DockMsg::DragRelease);
assert_eq!(app.dock.left.len(), 1);
let f = app.dock.float_rect(PanelId::Browser).expect("floats");
assert_eq!(f.group.panels, vec![PanelId::Properties, PanelId::Browser]);
// Dragging the window's title bar onto the strip docks it whole again.
let _ = app.on_dock(DockMsg::FloatGrab(PanelId::Properties));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(800.0, 400.0)));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, plus_top + 10.0)));
let _ = app.on_dock(DockMsg::DragRelease);
assert!(app.dock.floating.is_empty());
assert_eq!(
app.dock.left[1].panels,
vec![PanelId::Properties, PanelId::Browser]
);
}
#[test]
fn floating_menu_adds_pallets_and_docks_the_window() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockMsg, PanelId};
let mut app = dock_app();
let _ = app.on_dock(DockMsg::FloatOut(PanelId::BlockPalette));
let _ = app.on_dock(DockMsg::FloatMenu(Some(PanelId::BlockPalette)));
let _ = app.on_dock(DockMsg::FloatMenuPallets(true));
// Picking a pallet adds it to the window, opens it and closes the menu.
let _ = app.on_dock(DockMsg::FloatMenuToggle(PanelId::BlockPalette, PanelId::Browser));
assert!(app.show_browser);
assert_eq!(app.dock_float_menu, None);
let f = app.dock.float_rect(PanelId::BlockPalette).unwrap();
assert_eq!(f.group.panels, vec![PanelId::BlockPalette, PanelId::Browser]);
// Picking a checked one hides it but keeps its place.
let _ = app.on_dock(DockMsg::FloatMenuToggle(PanelId::BlockPalette, PanelId::Browser));
assert!(!app.show_browser);
assert!(app.dock.float_rect(PanelId::Browser).is_some());
// Dock left takes the whole window to the left edge.
let _ = app.on_dock(DockMsg::DockTo(PanelId::BlockPalette, DockSide::Left));
assert!(app.dock.floating.is_empty());
let (side, gi) = app.dock.location(PanelId::Browser).unwrap();
assert_eq!(side, DockSide::Left);
assert_eq!(
app.dock.groups(side)[gi].panels,
vec![PanelId::BlockPalette, PanelId::Browser]
);
assert_eq!(app.dock, before);
}
#[test]
@@ -405,7 +366,7 @@ fn dock_float_resize_grows_the_panel() {
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(360.0, 400.0)));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(400.0, 450.0)));
let _ = app.on_dock(DockMsg::DragRelease);
let f = app.dock.float_rect(PanelId::BlockPalette).unwrap().clone();
let f = app.dock.float_rect(PanelId::BlockPalette).unwrap();
assert_eq!((f.w, f.h), (300.0, 350.0));
// The bottom-left grip grows the panel leftward, keeping its right edge.
let _ = app.on_dock(DockMsg::FloatResizeGrab(PanelId::BlockPalette, true));
@@ -630,18 +591,27 @@ fn blockpalette_width_reset() {
}
#[test]
fn a_window_without_allow_docking_only_floats_when_dragged() {
fn floating_menu_docks_and_allow_docking_keeps_a_drag_floating() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockMsg, DropTarget, PanelId};
let mut app = dock_app();
let _ = app.on_dock(DockMsg::FloatOut(PanelId::BlockPalette));
// With Allow docking off, a drag onto the left edge's strip stays
// floating.
let _ = app.on_dock(DockMsg::FloatMenu(Some(PanelId::BlockPalette)));
let _ = app.on_dock(DockMsg::FloatDockingToggle(PanelId::BlockPalette));
assert_eq!(app.dock_float_menu, None);
assert!(!app.dock.float_rect(PanelId::BlockPalette).unwrap().docking);
// Dragged onto the left edge's strip, it stays a floating window.
let _ = app.on_dock(DockMsg::FloatGrab(PanelId::BlockPalette));
let _ = app.on_dock(DockMsg::DockGrab(PanelId::BlockPalette));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(500.0, 300.0)));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(10.0, 60.0)));
assert!(matches!(drag_target(&app), Some(DropTarget::Float { .. })));
let _ = app.on_dock(DockMsg::DragRelease);
assert!(app.dock.float_rect(PanelId::BlockPalette).is_some());
assert_eq!(app.dock.location(PanelId::BlockPalette), None);
let f = app.dock.float_rect(PanelId::BlockPalette).expect("still floating");
assert!(!f.docking, "moving it keeps the choice");
// The menu's Dock left still docks it.
let _ = app.on_dock(DockMsg::FloatMenu(Some(PanelId::BlockPalette)));
let _ = app.on_dock(DockMsg::DockTo(PanelId::BlockPalette, DockSide::Left));
assert_eq!(app.dock_float_menu, None);
assert_eq!(app.dock.location(PanelId::BlockPalette).map(|(s, _)| s), Some(DockSide::Left));
}
+26 -165
View File
@@ -102,60 +102,20 @@ impl OpenCADStudio {
self.save_config();
}
}
DockMsg::GroupFloat(side, gi) => {
// Float beside the edge it was docked on, near the top.
let (ww, _) = self.dock_workspace_size();
let w = self.dock_group_width(side, gi);
let strip = crate::ui::dock::DOCK_STRIP_W;
let x = match side {
DockSide::Right => ww - strip - self.dock_column_width(side) - w - 60.0,
DockSide::Left => strip + self.dock_column_width(side) + 40.0,
};
self.dock_drag = None;
let h = self.dock_default_float_h();
if self.dock.float_group(side, gi, x.max(0.0), 40.0, w, h) {
self.save_config();
}
}
DockMsg::DockTo(id, side) => {
self.dock_drag = None;
self.dock_float_menu = None;
if self.dock.dock_window(id, side, usize::MAX) {
if self.dock.dock(id, side, usize::MAX) {
self.save_config();
}
}
DockMsg::FloatGrab(id) => self.dock_begin(DockDrag::moving_window(id), Some(id)),
DockMsg::FloatClose(id) => {
let panels = self
.dock
.float_rect(id)
.map(|f| f.group.panels.clone())
.unwrap_or_default();
let mut tasks = Vec::new();
for p in panels {
if self.dock_panel_visible(p) {
tasks.push(self.dock_set_open(p, false));
}
}
return iced::Task::batch(tasks);
}
DockMsg::FloatSplitGrab(panel, upper, lower) => {
self.dock.raise_float(panel);
self.dock_begin(DockDrag::FloatSplit { panel, upper, lower }, None);
}
DockMsg::FloatSplitReset(id) => {
self.dock.reset_float_splits(id);
self.save_config();
}
DockMsg::FloatMenu(id) => {
self.dock_float_menu = id;
self.dock_float_menu_at = self.dock_float_pointer;
self.dock_float_menu_pallets = false;
if let Some(id) = id {
self.dock.raise_float(id);
}
}
DockMsg::FloatMenuPallets(open) => self.dock_float_menu_pallets = open,
DockMsg::FloatBarPointer(p) => self.dock_float_pointer = p,
DockMsg::FloatDockingToggle(id) => {
self.dock_float_menu = None;
@@ -163,22 +123,6 @@ impl OpenCADStudio {
self.save_config();
}
}
DockMsg::FloatMenuToggle(window, id) => {
// Checked = open in this window: unchecking hides it.
// Anything else (closed, docked, in another window) moves
// into the window and opens; adding ends the menu's job.
let here = self.dock.float_rect(window).is_some_and(|f| f.contains(id));
if here && self.dock_panel_visible(id) {
let task = self.dock_set_open(id, false);
self.save_config();
return task;
}
self.dock.add_to_float(id, window);
self.dock_float_menu = None;
let task = self.dock_set_open(id, true);
self.save_config();
return task;
}
DockMsg::EdgeMenu(side) => self.dock_edge_menu = side,
DockMsg::EdgeMenuToggle(side, id) => {
// Checked = open and docked on this edge: unchecking hides it.
@@ -259,31 +203,6 @@ impl OpenCADStudio {
target: Some(DropTarget::Edge { side: to, index }),
..
}) => changed = self.dock.move_group(side, gi, to, index),
Some(DockDrag::Move {
group: Some((side, gi)),
target: Some(DropTarget::Float { x, y }),
..
}) => {
let w = self.dock_group_width(side, gi);
let h = self.dock_default_float_h();
changed = self.dock.float_group(side, gi, x, y, w, h);
}
Some(DockDrag::Move {
panel,
window: true,
target: Some(target),
..
}) => {
changed = match target {
DropTarget::Edge { side, index } => {
self.dock.dock_window(panel, side, index)
}
DropTarget::Join { side, group, index } => {
self.dock.join_window(panel, side, group, index)
}
DropTarget::Float { x, y } => self.dock.move_float(panel, x, y),
};
}
Some(DockDrag::Move {
panel,
group: None,
@@ -297,8 +216,7 @@ impl OpenCADStudio {
}
DropTarget::Float { x, y } => {
let (w, h) = self.dock_float_size(panel);
// Pulled out of a window, it keeps the
// window's docking choice.
// A floating panel keeps its docking choice.
let docking = self.dock.float_rect(panel).is_none_or(|f| f.docking);
self.dock.float(FloatPanel {
docking,
@@ -308,12 +226,7 @@ impl OpenCADStudio {
};
}
Some(DockDrag::Move { .. } | DockDrag::LayerColumn) | None => {}
Some(
DockDrag::Width(_)
| DockDrag::Split { .. }
| DockDrag::FloatSize { .. }
| DockDrag::FloatSplit { .. },
) => {
Some(DockDrag::Width(_) | DockDrag::Split { .. } | DockDrag::FloatSize { .. }) => {
changed = true;
}
}
@@ -346,7 +259,6 @@ impl OpenCADStudio {
DockDrag::Move {
panel,
group,
window,
origin,
target,
grab,
@@ -354,12 +266,10 @@ impl OpenCADStudio {
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 window where it was grabbed; a docked
// pallet (or one pulled out of a floating group) by its
// title bar.
// Hold a floating panel where it was grabbed; a docked one by
// its title bar.
let grab = if target.is_none() && started {
let whole = |f: &&crate::ui::dock::FloatPanel| window || f.group.panels.len() == 1;
match self.dock.float_rect(panel).filter(whole) {
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),
@@ -370,20 +280,16 @@ impl OpenCADStudio {
grab
};
let target = if started {
// A window that may not dock (or a pallet pulled out of
// one) only floats.
let floats_only = group.is_none()
&& self.dock.float_rect(panel).is_some_and(|f| !f.docking);
let found = if floats_only {
self.dock_float_target(point, grab)
} else {
self.dock_drop_target(point, grab)
// A whole group only moves between group positions: it
// cannot join another group or float.
// A floating panel that may not dock only floats.
let found = match self.dock.float_rect(panel) {
Some(f) if !f.docking => self.dock_float_target(point, grab),
_ => self.dock_drop_target(point, grab),
};
// A whole docked group moves between group positions or
// floats; it cannot join another group.
match (group, found) {
(Some(_), DropTarget::Join { .. }) => None,
(_, found) => Some(found),
(Some(_), found @ DropTarget::Edge { .. }) | (None, found) => Some(found),
(Some(_), _) => None,
}
} else {
target
@@ -391,7 +297,6 @@ impl OpenCADStudio {
self.dock_drag = Some(DockDrag::Move {
panel,
group,
window,
origin: Some(origin),
target,
grab,
@@ -430,30 +335,12 @@ impl OpenCADStudio {
return;
};
let dx = point.x - last.x;
let (w, h) = (if from_left { f.w - dx } else { f.w + dx }, f.h + point.y - last.y);
self.dock.resize_float(panel, w, h, from_left);
}
DockDrag::FloatSplit { panel, upper, lower } => {
let (Some(last), Some(f)) = (last, self.dock.float_rect(panel)) else {
return;
};
let total: f32 = self
.dock_float_visible(f)
.iter()
.filter_map(|p| f.group.panels.iter().position(|q| q == p))
.filter_map(|i| f.group.weights.get(i))
.sum();
let avail = f.h;
if avail > 0.0 && total > 0.0 {
let per_px = total / avail;
self.dock.shift_float_split(
panel,
upper,
lower,
(point.y - last.y) * per_px,
crate::ui::dock::GROUP_MIN_H * per_px,
);
}
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 };
@@ -494,6 +381,8 @@ impl OpenCADStudio {
}
}
/// Bring `id` into view: show its group on its edge and, where it
/// auto-hides, keep it revealed until the pointer leaves.
/// Arm a delayed reveal (`Some`) or hide (`None`); it applies when its
/// timer reports back unless a newer hover superseded it.
fn dock_hover_arm(&mut self, target: Option<crate::ui::dock::PanelId>) -> iced::Task<Message> {
@@ -511,8 +400,6 @@ impl OpenCADStudio {
self.dock_hover_pending = None;
}
/// 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);
@@ -764,37 +651,11 @@ impl OpenCADStudio {
if let Some(f) = self.dock.float_rect(id) {
return (f.w, f.h);
}
(self.dock.width(id, self.win_size.0), self.dock_default_float_h())
}
/// Height a pallet or group floats at first: a comfortable share of the
/// workspace height.
pub(crate) fn dock_default_float_h(&self) -> f32 {
let (_, avail) = self.dock_workspace_size();
(avail * 0.6).clamp(crate::ui::dock::FLOAT_MIN_H, 520.0)
}
/// The pallets floating window `f` draws, top → bottom: its open ones,
/// less one being dragged out of it.
pub(crate) fn dock_float_visible(
&self,
f: &crate::ui::dock::FloatPanel,
) -> Vec<crate::ui::dock::PanelId> {
let pulled = match self.dock_drag {
Some(crate::ui::dock::DockDrag::Move {
panel,
window: false,
target: Some(_),
..
}) => Some(panel),
_ => None,
};
f.group
.panels
.iter()
.copied()
.filter(|p| self.dock_panel_visible(*p) && Some(*p) != pulled)
.collect()
(
self.dock.width(id, self.win_size.0),
(avail * 0.6).clamp(crate::ui::dock::FLOAT_MIN_H, 520.0),
)
}
/// Where a dragged pallet lands if released at workspace point `p`:
+120 -241
View File
@@ -17,19 +17,16 @@ impl OpenCADStudio {
workspace: Element<'a, Message>,
tab: &'a DocumentTab,
) -> Element<'a, Message> {
use crate::ui::dock::DockMsg;
// Floating panels sit over the workspace, back to front.
let mut layers: Vec<Element<'_, Message>> = vec![workspace];
for f in &self.dock.floating {
// Once a window drag is under way only its preview shows it.
let dragged = matches!(
self.dock_drag,
Some(crate::ui::dock::DockDrag::Move { panel, window: true, target: Some(_), .. })
if f.contains(panel)
);
let visible = self.dock_float_visible(f);
if !visible.is_empty() && !dragged {
layers.push(self.floating_panel(f, visible, tab));
// Once a drag is under way only its preview shows the panel.
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));
}
}
// An open pallet menu hangs beside its edge's + button, over a
@@ -54,19 +51,20 @@ impl OpenCADStudio {
};
layers.push(
mouse_area(Space::new().width(Fill).height(Fill))
.on_press(Message::Dock(DockMsg::EdgeMenu(None)))
.on_right_press(Message::Dock(DockMsg::EdgeMenu(None)))
.on_press(Message::Dock(crate::ui::dock::DockMsg::EdgeMenu(None)))
.on_right_press(Message::Dock(crate::ui::dock::DockMsg::EdgeMenu(None)))
.into(),
);
let toggle = move |id| Message::Dock(DockMsg::EdgeMenuToggle(side, id));
layers.push(place_at(dock_pallet_menu(&checked, toggle), x.max(0.0), plus_top));
layers.push(place_at(dock_edge_menu(side, &checked), x.max(0.0), plus_top));
}
}
// An open floating title-bar menu, over its own click catcher.
if let Some(menu) = self.dock_float_menu_overlay() {
let close = Message::Dock(crate::ui::dock::DockMsg::FloatMenu(None));
layers.push(
mouse_area(Space::new().width(Fill).height(Fill))
.on_press(Message::Dock(DockMsg::FloatMenu(None)))
.on_right_press(Message::Dock(DockMsg::FloatMenu(None)))
.on_press(close.clone())
.on_right_press(close)
.into(),
);
layers.push(menu);
@@ -288,7 +286,6 @@ impl OpenCADStudio {
}),
)
.on_press(Message::Dock(DockMsg::GroupGrab(side, gi)))
.on_double_click(Message::Dock(DockMsg::GroupFloat(side, gi)))
.on_enter(Message::Dock(DockMsg::GripHover(side, gi)))
.on_exit(Message::Dock(DockMsg::GripHoverEnd(side, gi)))
.interaction(iced::mouse::Interaction::Grab);
@@ -437,46 +434,19 @@ impl OpenCADStudio {
}
}
/// Where floating window `f` draws: its top-left corner, whether its
/// title bar is on the left, and whether it is hidden down to that bar
/// (auto-hide, not hovered).
fn dock_float_frame(&self, f: &crate::ui::dock::FloatPanel) -> (f32, f32, bool, bool) {
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_drag {
Some(crate::ui::dock::DockDrag::FloatSize { panel, from_left }) if f.contains(panel) => {
!from_left
}
_ => f.x + (f.w + DOCK_FLOAT_BAR_W) * 0.5 < ww * 0.5,
};
let hidden = self.dock.auto_hides(f.id()) && !self.dock_peek.is_some_and(|p| f.contains(p));
let total_w = if hidden { DOCK_FLOAT_BAR_W } else { f.w + DOCK_FLOAT_BAR_W };
// A hidden window keeps its title bar where it sits when shown.
let x = if hidden && !bar_left { f.x + f.w } else { f.x };
// Keep the window on screen when the workspace shrank since it was
// placed.
let x = x.min(ww - total_w).max(0.0);
let y = f.y.min(wh - f.h).max(0.0);
(x, y, bar_left, hidden)
}
/// A floating window at its saved place: a vertical title bar on the
/// side facing the nearer workspace edge, its open pallets (`visible`)
/// stacked beside it, and a corner grip to resize it. With its pin on,
/// the window hides down to the title bar until hovered.
/// A floating panel at its saved place: a vertical title bar on the side
/// facing the nearer workspace edge, the panel beside it, and a corner
/// grip to resize it. With its pin on, the panel hides down to the title
/// bar until hovered.
fn floating_panel<'a>(
&'a self,
f: &'a crate::ui::dock::FloatPanel,
visible: Vec<crate::ui::dock::PanelId>,
f: crate::ui::dock::FloatPanel,
tab: &'a DocumentTab,
) -> Element<'a, Message> {
use crate::ui::dock::DockMsg;
let (x, y, bar_left, hidden) = self.dock_float_frame(f);
// Messages name the window by its first open pallet.
let anchor = visible[0];
let hovered = self.dock_title_hover.is_some_and(|p| f.contains(p));
let bar = floating_title_bar(&visible, self.dock.auto_hides(anchor), bar_left, hovered);
let hovered = self.dock_title_hover == Some(f.id);
let bar = floating_title_bar(f.id, self.dock.auto_collapse(f.id), bar_left, hovered);
let content: Element<'_, Message> = if hidden {
bar
} else {
@@ -495,31 +465,14 @@ impl OpenCADStudio {
.center_x(Length::Fixed(14.0))
.center_y(Length::Fixed(14.0)),
)
.on_press(Message::Dock(DockMsg::FloatResizeGrab(anchor, grip_left)))
.on_press(Message::Dock(DockMsg::FloatResizeGrab(f.id, grip_left)))
.interaction(if grip_left {
iced::mouse::Interaction::ResizingDiagonallyUp
} else {
iced::mouse::Interaction::ResizingDiagonallyDown
});
// The pallets stack like a docked group, with splitters between.
let mut pallets: Vec<Element<'_, Message>> = Vec::new();
let mut prev: Option<usize> = None;
for id in &visible {
let pos = f.group.panels.iter().position(|p| p == id).unwrap_or(0);
if let Some(upper) = prev {
pallets.push(float_splitter(anchor, upper, pos, f.w));
}
let weight = f.group.weights.get(pos).copied().unwrap_or(1.0);
pallets.push(
container(self.panel_body(*id, f.w, true, tab))
.height(Length::FillPortion(crate::ui::dock::portion(weight)))
.clip(true)
.into(),
);
prev = Some(pos);
}
let body = stack![
column(pallets).height(Fill),
self.panel_body(f.id, f.w, true, tab),
container(grip)
.width(Fill)
.height(Fill)
@@ -538,8 +491,8 @@ impl OpenCADStudio {
row![body, bar].into()
}
};
// Clipped, so a pallet squeezed below its content's height never
// draws outside the window.
// Clipped, so a panel squeezed below its content's height never
// draws outside its frame.
let framed = container(content)
.height(Length::Fixed(f.h))
.clip(true)
@@ -556,25 +509,48 @@ impl OpenCADStudio {
},
..Default::default()
});
// The idle cursor marks the window as opaque to the pointer, so
// clicks on its empty areas don't fall through to the drawing below.
// The idle cursor marks the panel as opaque to the pointer, so clicks
// on its empty areas don't fall through to the drawing below.
let panel = mouse_area(framed)
.on_press(Message::Dock(DockMsg::FloatRaise(anchor)))
.on_enter(Message::Dock(DockMsg::Hover(anchor)))
.on_press(Message::Dock(DockMsg::FloatRaise(f.id)))
.on_enter(Message::Dock(DockMsg::Hover(f.id)))
.on_exit(Message::Dock(DockMsg::HoverExit))
.interaction(iced::mouse::Interaction::Idle);
place_at(panel.into(), x, y)
}
/// The open right-click menu of a floating window: Pallets (a submenu
/// to add or hide pallets in the window), Allow docking, Dock left and
/// Dock right. It opens at the pointer, flipped to stay on screen.
/// Where floating panel `f` draws: its top-left corner, whether its
/// title bar is on the left, and whether it is hidden down to that bar
/// (auto-hide, not hovered).
fn dock_float_frame(&self, f: crate::ui::dock::FloatPanel) -> (f32, f32, bool, bool) {
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_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_peek != Some(f.id);
let total_w = if hidden { DOCK_FLOAT_BAR_W } else { f.w + DOCK_FLOAT_BAR_W };
// A hidden panel keeps its title bar where it sits when shown.
let x = if hidden && !bar_left { f.x + f.w } else { f.x };
// Keep the panel on screen when the window shrank since it was placed.
let x = x.min(ww - total_w).max(0.0);
let y = f.y.min(wh - f.h).max(0.0);
(x, y, bar_left, hidden)
}
/// The open right-click menu of a floating title bar: Allow docking,
/// Dock left and Dock right. It opens at the pointer, flipped to stay on
/// screen.
fn dock_float_menu_overlay(&self) -> Option<Element<'_, Message>> {
use crate::app::config::DockSide;
use crate::ui::dock::{DockMsg, PanelId};
let anchor = self.dock_float_menu?;
let f = self.dock.float_rect(anchor)?;
if self.dock_float_visible(f).is_empty() {
use crate::ui::dock::DockMsg;
let id = self.dock_float_menu?;
let f = self.dock.float_rect(id)?;
if !self.dock_panel_visible(id) {
return None;
}
let (ww, wh) = self.dock_workspace_size();
@@ -583,35 +559,17 @@ impl OpenCADStudio {
let bar_x = if bar_left { x } else { x + total_w - DOCK_FLOAT_BAR_W };
// Where the right-click happened, in the workspace.
let at = iced::Point::new(bar_x + self.dock_float_menu_at.x, y + self.dock_float_menu_at.y);
let max_x = (ww - DOCK_MENU_W).max(0.0);
let menu_x = if at.x + DOCK_MENU_W <= ww { at.x } else { at.x - DOCK_MENU_W };
let menu_x = menu_x.clamp(0.0, max_x);
let menu_x = menu_x.clamp(0.0, (ww - DOCK_MENU_W).max(0.0));
let menu_y = at.y.min(wh - FLOAT_MENU_H).max(0.0);
let open = self.dock_float_menu_pallets;
// Hovering a row opens (Pallets) or closes (the others) the submenu.
let item = |label: Element<'static, Message>, press: DockMsg, sub: bool| -> Element<'static, Message> {
mouse_area(
button(label)
.on_press(Message::Dock(press))
.style(menu_row_style)
.padding([5, 8])
.width(Fill),
)
.on_enter(Message::Dock(DockMsg::FloatMenuPallets(sub)))
.into()
let item = |label: Element<'static, Message>, press: DockMsg| -> Element<'static, Message> {
button(label)
.on_press(Message::Dock(press))
.style(menu_row_style)
.padding([5, 8])
.width(Fill)
.into()
};
let pallets = row![
text(t!("Pallets")).size(12).width(Fill),
text("\u{203A}").size(13),
]
.align_y(iced::Center);
let separator = container(Space::new())
.width(Fill)
.height(Length::Fixed(1.0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
});
let check: Element<'static, Message> = if f.docking {
crate::ui::icons::themed(crate::ui::icons::CHECK, 12.0)
} else {
@@ -619,46 +577,22 @@ impl OpenCADStudio {
};
let docking = row![text(t!("Allow docking")).size(12).width(Fill), check]
.align_y(iced::Center);
let main = column![
item(pallets.into(), DockMsg::FloatMenuPallets(!open), true),
let separator = container(Space::new())
.width(Fill)
.height(Length::Fixed(1.0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
});
let menu = column![
item(docking.into(), DockMsg::FloatDockingToggle(id)),
container(separator).padding([3, 4]),
item(docking.into(), DockMsg::FloatDockingToggle(anchor), false),
item(
text(t!("Dock left")).size(12).into(),
DockMsg::DockTo(anchor, DockSide::Left),
false
),
item(
text(t!("Dock right")).size(12).into(),
DockMsg::DockTo(anchor, DockSide::Right),
false
),
item(text(t!("Dock left")).size(12).into(), DockMsg::DockTo(id, DockSide::Left)),
item(text(t!("Dock right")).size(12).into(), DockMsg::DockTo(id, DockSide::Right)),
]
.spacing(1)
.padding(4);
let mut layers = vec![place_at(menu_frame(main.into()), menu_x, menu_y)];
if open {
let checked: Vec<_> = PanelId::ALL
.iter()
.map(|p| (*p, f.contains(*p) && self.dock_panel_visible(*p)))
.collect();
let toggle = move |id| Message::Dock(DockMsg::FloatMenuToggle(anchor, id));
// To the right of the menu, or its left when there is no room.
let sub_x = if menu_x + 2.0 * DOCK_MENU_W + 2.0 <= ww {
menu_x + DOCK_MENU_W + 2.0
} else {
menu_x - DOCK_MENU_W - 2.0
}
.clamp(0.0, max_x);
let sub_y = menu_y.min(wh - PALLET_MENU_H).max(0.0);
layers.push(place_at(dock_pallet_menu(&checked, toggle), sub_x, sub_y));
}
Some(
iced::widget::Stack::with_children(layers)
.width(Fill)
.height(Fill)
.into(),
)
Some(place_at(menu_frame(menu.into()), menu_x, menu_y))
}
/// The live drop preview for dragged panel `id`: a tint over the target
@@ -695,32 +629,18 @@ impl OpenCADStudio {
};
let mut layers: Vec<Element<'_, Message>> = Vec::new();
let group_drag = self.dock_drag.and_then(|d| d.movement()).and_then(|(_, g, _)| g);
// The pallets that move: a docked group's, a floating window's, or
// just `id`.
let window_drag = matches!(
self.dock_drag,
Some(crate::ui::dock::DockDrag::Move { window: true, .. })
);
let moving = match (group_drag, self.dock.float_rect(id)) {
(Some((s, g)), _) => self.dock_group_visible(s, g),
(None, Some(f)) if window_drag => self.dock_float_visible(f),
_ => vec![id],
};
let marks_side = match target {
DropTarget::Edge { side, .. } | DropTarget::Join { side, .. } => Some(side),
DropTarget::Float { x, y } => {
let (w, h) = match group_drag {
Some((s, g)) => (self.dock_group_width(s, g), self.dock_default_float_h()),
None => self.dock_float_size(id),
};
let (w, h) = self.dock_float_size(id);
layers.push(place_at(dock_ghost(id, w + DOCK_FLOAT_BAR_W, h), x, y));
None
}
};
// A single pallet also gets a ghost where it lands in the column;
// several show only the strip feedback, as the column keeps showing
// what is there.
if let (Some(side), None, [_]) = (marks_side, group_drag, moving.as_slice()) {
// A single pallet also gets a ghost where it lands in the column; a
// whole group shows only the strip feedback, as the column keeps
// showing what is there.
if let (Some(side), None) = (marks_side, group_drag) {
if let Some((top, bottom, w)) = self.dock_landing(id, target) {
let x = match side {
DockSide::Left => strip,
@@ -735,7 +655,11 @@ impl OpenCADStudio {
if let Some(side) = marks_side {
layers.extend(self.dock_strip_drop_marks(side, target));
if let Some(p) = self.dock_drag_last {
layers.push(place_at(dock_icon_ghost(&moving), p.x - 16.0, p.y - 15.0));
let icons = match group_drag {
Some((s, g)) => self.dock_group_visible(s, g),
None => vec![id],
};
layers.push(place_at(dock_icon_ghost(&icons), p.x - 16.0, p.y - 15.0));
}
}
iced::widget::Stack::with_children(layers)
@@ -956,73 +880,53 @@ impl canvas::Program<Message> for VBarLabel {
/// Width of a floating panel's vertical title bar.
const DOCK_FLOAT_BAR_W: f32 = 28.0;
/// A floating window's vertical title bar: close then the hide (auto-hide)
/// pin at the top; at the bottom the pallet's icon with its title reading
/// upward, or for a group each pallet's icon (press one to drag that pallet
/// out). Pressing the bar drags the window, double-clicking docks it against
/// the edge the bar faces, right-clicking opens its menu.
/// A floating panel's vertical title bar: close then the hide (auto-collapse)
/// pin at the top, the panel's icon at the bottom with its title reading
/// upward above it. Pressing the bar's free area drags the panel.
fn floating_title_bar(
visible: &[crate::ui::dock::PanelId],
id: crate::ui::dock::PanelId,
auto_collapse: bool,
on_left: bool,
hovered: bool,
) -> Element<'static, Message> {
use crate::ui::dock::DockMsg;
let anchor = visible[0];
let tip = if on_left {
iced::widget::tooltip::Position::Left
} else {
iced::widget::tooltip::Position::Right
};
let title = canvas(VBarLabel {
text: t!(id.title()).into_owned(),
})
.width(Fill)
.height(Fill);
// Close and pin only show while the pointer is over the bar, as on a
// docked title bar.
let mut bar = column![].spacing(2);
if hovered {
bar = bar
.push(crate::ui::dock::close_button(DockMsg::FloatClose(anchor), tip))
.push(crate::ui::dock::pin_button(anchor, auto_collapse, tip));
}
if let [id] = visible {
let title = canvas(VBarLabel {
text: t!(id.title()).into_owned(),
})
.width(Fill)
.height(Fill);
bar = bar
.push(container(title).width(Fill).height(Fill).padding([6, 0]))
.push(container(crate::ui::icons::themed(id.icon(), 16.0)).center_x(Fill));
} else {
bar = bar.push(Space::new().height(Fill));
for id in visible {
let icon = mouse_area(
container(crate::ui::icons::themed(id.icon(), 16.0))
.center_x(Fill)
.center_y(Length::Fixed(24.0)),
)
.on_press(Message::Dock(DockMsg::IconPress(*id)))
.interaction(iced::mouse::Interaction::Grab);
bar = bar.push(
iced::widget::tooltip(icon, text(t!(id.title())).size(10), tip).gap(4),
);
}
.push(crate::ui::dock::close_button(id, tip))
.push(crate::ui::dock::pin_button(id, auto_collapse, tip));
}
let bar = bar
.padding([4, 0])
.align_x(iced::Center)
.width(Length::Fixed(DOCK_FLOAT_BAR_W))
.height(Fill);
.push(container(title).width(Fill).height(Fill).padding([6, 0]))
.push(container(crate::ui::icons::themed(id.icon(), 16.0)).center_x(Fill))
.padding([4, 0])
.align_x(iced::Center)
.width(Length::Fixed(DOCK_FLOAT_BAR_W))
.height(Fill);
mouse_area(container(bar).style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
..Default::default()
}))
.on_press(Message::Dock(DockMsg::FloatGrab(anchor)))
.on_right_press(Message::Dock(DockMsg::FloatMenu(Some(anchor))))
.on_enter(Message::Dock(DockMsg::TitleHover(anchor)))
.on_press(Message::Dock(DockMsg::DockGrab(id)))
.on_right_press(Message::Dock(DockMsg::FloatMenu(Some(id))))
.on_enter(Message::Dock(DockMsg::TitleHover(id)))
.on_move(|p| Message::Dock(DockMsg::FloatBarPointer(p)))
.on_exit(Message::Dock(DockMsg::TitleHoverEnd(anchor)))
.on_exit(Message::Dock(DockMsg::TitleHoverEnd(id)))
// Double-click docks it against the edge its title bar faces.
.on_double_click(Message::Dock(DockMsg::DockTo(
anchor,
id,
if on_left {
crate::app::config::DockSide::Left
} else {
@@ -1033,29 +937,6 @@ fn floating_title_bar(
.into()
}
/// Draggable bar between floating pallets `upper` and `lower` of the window
/// holding `anchor`; double-click evens their heights.
fn float_splitter(
anchor: crate::ui::dock::PanelId,
upper: usize,
lower: usize,
w: f32,
) -> Element<'static, Message> {
use crate::ui::dock::DockMsg;
let line = container(Space::new())
.width(Length::Fixed(w))
.height(Length::Fixed(5.0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
});
mouse_area(line)
.on_press(Message::Dock(DockMsg::FloatSplitGrab(anchor, upper, lower)))
.on_double_click(Message::Dock(DockMsg::FloatSplitReset(anchor)))
.interaction(iced::mouse::Interaction::ResizingVertically)
.into()
}
/// Grabbable separator between a docked pallet and the viewport: drag to
/// size its group, double-click to reset the width.
fn dock_divider(id: crate::ui::dock::PanelId) -> Element<'static, Message> {
@@ -1078,19 +959,17 @@ fn dock_divider(id: crate::ui::dock::PanelId) -> Element<'static, Message> {
/// Width of the icon strip's pallet menu.
const DOCK_MENU_W: f32 = 230.0;
/// Rough heights of a floating window's menu and of a pallet menu, to keep
/// them on screen.
const FLOAT_MENU_H: f32 = 150.0;
const PALLET_MENU_H: f32 = 270.0;
/// Rough height of a floating title bar's menu, to keep it on screen.
const FLOAT_MENU_H: f32 = 110.0;
/// A pallet menu (an edge's + button, a floating window's Pallets
/// submenu), styled like the right-click menu: one row per pallet with its
/// icon, its name and a check mark when it is open there. `checked` holds
/// each pallet with that state; picking one sends `toggle`.
fn dock_pallet_menu(
/// The pallet menu of an edge's + button, styled like the right-click menu:
/// one row per pallet with its icon, its name and a check mark when it is
/// open on this edge. `checked` holds each pallet with that state.
fn dock_edge_menu(
side: crate::app::config::DockSide,
checked: &[(crate::ui::dock::PanelId, bool)],
toggle: impl Fn(crate::ui::dock::PanelId) -> Message,
) -> Element<'static, Message> {
use crate::ui::dock::DockMsg;
let rows: Vec<Element<'static, Message>> = checked
.iter()
.map(|(id, checked)| {
@@ -1109,7 +988,7 @@ fn dock_pallet_menu(
.spacing(8)
.align_y(iced::Center),
)
.on_press(toggle(id))
.on_press(Message::Dock(DockMsg::EdgeMenuToggle(side, id)))
.style(menu_row_style)
.padding([5, 8])
.width(Fill)
+1 -4
View File
@@ -865,10 +865,8 @@ pub(super) struct OpenCADStudio {
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>,
/// Floating window (named by one of its pallets) whose right-click menu
/// is open, and whether its Pallets submenu shows.
/// Floating pallet whose title-bar right-click menu is open.
pub(crate) dock_float_menu: Option<crate::ui::dock::PanelId>,
pub(crate) dock_float_menu_pallets: bool,
/// Pointer position over a floating title bar (bar-local), and where
/// its open menu was asked for.
pub(crate) dock_float_pointer: iced::Point,
@@ -4306,7 +4304,6 @@ impl OpenCADStudio {
dock_hover_pending: None,
dock_hover_gen: 0,
dock_float_menu: None,
dock_float_menu_pallets: false,
dock_float_pointer: iced::Point::ORIGIN,
dock_float_menu_at: iced::Point::ORIGIN,
dock_drag: None,