forked from 1b-agent/opencadstudio-upstream
Dock: floating panels, tab groups and adjustable slot heights
Panels can now float over the workspace, share a docked slot as tabs, and split an edge at heights the user picks. - Dropping a panel away from the edges floats it at the pointer. Floating panels move by their title bar, resize from a corner grip, and come to the front when clicked. - Dropping in the top or bottom quarter of a docked slot docks the panel above or below it, as before. Dropping in the middle adds the panel as a tab of that slot. Pressing a tab shows that panel; dragging a tab pulls the panel out again. - While dragging, a blue ghost previews the result: the new slot, the slot with its future tab row, or the floating window. - A splitter between stacked slots sets their heights; double-click it to make the slots equal again. Heights persist. - A press only becomes a drag after 5 px of movement, so clicking a title bar or tab never moves the panel. The persisted layout now stores each edge as tab groups with a height weight, plus a list of floating panels. Configs with the old bare-panel-id stacks still load as single-panel slots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013RyGiMXcsf9HTKfqDxaQGt
This commit is contained in:
11 files changed
+1452
-417
No files matched your search
+458
-118
@@ -20,6 +20,9 @@ use codec::{EntityType as AcadEntityType, Handle};
|
||||
use iced::time::Instant;
|
||||
use iced::{mouse, Point, Task};
|
||||
|
||||
/// Pointer travel (px) before pressing a panel's title bar or tab becomes a
|
||||
/// drag, so a plain click never moves the panel.
|
||||
const DOCK_DRAG_THRESHOLD: f32 = 5.0;
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(in crate::app) fn open_save_dialog_window(&mut self, tab_idx: usize) -> Task<Message> {
|
||||
@@ -542,14 +545,19 @@ 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, PanelId};
|
||||
use crate::ui::dock::{DockMsg, DropTarget, FloatPanel, PanelId};
|
||||
match m {
|
||||
DockMsg::DockGrab(id) => {
|
||||
self.dock_dragging = Some(id);
|
||||
self.dock_resizing = None;
|
||||
self.dock_drag_last = None;
|
||||
self.dock_drag_target = self.dock.location(id);
|
||||
self.dock_expanded = Some(id);
|
||||
self.dock_begin_drag(id);
|
||||
iced::Task::none()
|
||||
}
|
||||
DockMsg::SelectTab(id) => {
|
||||
// A tab press shows the tab and may also start pulling it out
|
||||
// of the group; a plain click never passes the drag threshold.
|
||||
if self.dock.select_tab(id) {
|
||||
self.save_config();
|
||||
}
|
||||
self.dock_begin_drag(id);
|
||||
iced::Task::none()
|
||||
}
|
||||
DockMsg::ResizeGrab(id) => {
|
||||
@@ -560,6 +568,30 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
self.dock_expanded = Some(id);
|
||||
iced::Task::none()
|
||||
}
|
||||
DockMsg::SplitGrab(side, upper, lower) => {
|
||||
self.dock_split_drag = Some((side, upper, lower));
|
||||
self.dock_dragging = None;
|
||||
self.dock_drag_last = None;
|
||||
self.dock_drag_target = None;
|
||||
iced::Task::none()
|
||||
}
|
||||
DockMsg::SplitReset(side) => {
|
||||
self.dock.reset_splits(side);
|
||||
self.save_config();
|
||||
iced::Task::none()
|
||||
}
|
||||
DockMsg::FloatResizeGrab(id) => {
|
||||
self.dock.raise_float(id);
|
||||
self.dock_float_resizing = Some(id);
|
||||
self.dock_dragging = None;
|
||||
self.dock_drag_last = None;
|
||||
self.dock_drag_target = None;
|
||||
iced::Task::none()
|
||||
}
|
||||
DockMsg::FloatRaise(id) => {
|
||||
self.dock.raise_float(id);
|
||||
iced::Task::none()
|
||||
}
|
||||
DockMsg::WidthReset(id) => {
|
||||
self.dock.reset_width(id);
|
||||
self.save_config();
|
||||
@@ -639,19 +671,50 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
// 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 self.dock_dragging.is_some() {
|
||||
let avail = self.tabs[self.active_tab].scene.selection.borrow().view.vp_size.1;
|
||||
let side = if point.x < self.win_size.0 * 0.5 {
|
||||
DockSide::Left
|
||||
} else {
|
||||
DockSide::Right
|
||||
};
|
||||
let index = crate::ui::dock::drop_index(
|
||||
point.y,
|
||||
std::cmp::max(self.dock_visible_len(side), 1),
|
||||
avail,
|
||||
);
|
||||
self.dock_drag_target = Some((side, index));
|
||||
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 {
|
||||
self.dock_drag_target = Some(self.dock_drop_target(id, point));
|
||||
}
|
||||
} else if let Some((side, 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_expanded_groups(side)
|
||||
.iter()
|
||||
.map(|gi| self.dock.groups(side)[*gi].weight)
|
||||
.sum();
|
||||
if avail > 0.0 && total > 0.0 {
|
||||
let per_px = total / avail;
|
||||
self.dock.shift_split(
|
||||
side,
|
||||
upper,
|
||||
lower,
|
||||
(point.y - last.y) * per_px,
|
||||
crate::ui::dock::GROUP_MIN_H * per_px,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if let Some(id) = self.dock_float_resizing {
|
||||
if let (Some(last), Some(f)) = (self.dock_drag_last, self.dock.float_rect(id)) {
|
||||
self.dock.resize_float(
|
||||
id,
|
||||
f.w + point.x - last.x,
|
||||
f.h + point.y - last.y,
|
||||
);
|
||||
}
|
||||
} else if let Some(id) = self.dock_resizing {
|
||||
if let Some(last) = self.dock_drag_last {
|
||||
let dx = point.x - last.x;
|
||||
@@ -663,22 +726,42 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
self.dock.set_width(id, cur);
|
||||
}
|
||||
}
|
||||
if self.dock_dragging.is_some() || self.dock_resizing.is_some() {
|
||||
if self.dock_dragging.is_some()
|
||||
|| self.dock_resizing.is_some()
|
||||
|| self.dock_split_drag.is_some()
|
||||
|| self.dock_float_resizing.is_some()
|
||||
{
|
||||
self.dock_drag_last = Some(point);
|
||||
}
|
||||
iced::Task::none()
|
||||
}
|
||||
DockMsg::DragRelease => {
|
||||
if let Some(id) = self.dock_dragging {
|
||||
if let Some((side, index)) = self.dock_drag_target {
|
||||
if self.dock.dock(id, side, index) {
|
||||
self.save_config();
|
||||
let mut changed = self.dock_resizing.is_some()
|
||||
|| self.dock_split_drag.is_some()
|
||||
|| self.dock_float_resizing.is_some();
|
||||
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::Tab { side, group } => self.dock.add_tab(id, side, group),
|
||||
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);
|
||||
}
|
||||
changed |= moved;
|
||||
}
|
||||
if changed {
|
||||
self.save_config();
|
||||
}
|
||||
self.dock_dragging = 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;
|
||||
@@ -688,6 +771,20 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_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.dock_expanded = Some(id);
|
||||
}
|
||||
|
||||
/// Whether `id` is currently rendered (not closed, not on the start screen /
|
||||
/// clean-screen viewport). Mirrors the visibility filter the edge column
|
||||
/// renderer applies before building each side's stack.
|
||||
@@ -708,17 +805,150 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of panels currently rendered on `side`. Closed panels keep their
|
||||
/// stack slot but take no vertical space, so drag preview geometry must
|
||||
/// size slots against only the panels that are actually visible.
|
||||
pub(crate) fn dock_visible_len(&self, side: crate::app::config::DockSide) -> usize {
|
||||
let ids: &[crate::ui::dock::PanelId] = match side {
|
||||
crate::app::config::DockSide::Left => &self.dock.left,
|
||||
crate::app::config::DockSide::Right => &self.dock.right,
|
||||
};
|
||||
ids.iter()
|
||||
.filter(|id| self.dock_panel_visible(**id))
|
||||
.count()
|
||||
/// Size of the workspace the dock lays out in: window width by viewport
|
||||
/// height.
|
||||
pub(crate) fn dock_workspace_size(&self) -> (f32, f32) {
|
||||
(
|
||||
self.win_size.0,
|
||||
self.tabs[self.active_tab].scene.selection.borrow().view.vp_size.1,
|
||||
)
|
||||
}
|
||||
|
||||
/// Visible tabs of slot `gi` on `side`. Closed panels keep their tab but
|
||||
/// take no space, so a slot with no visible tab is not rendered at all.
|
||||
pub(crate) fn dock_group_visible_tabs(
|
||||
&self,
|
||||
side: crate::app::config::DockSide,
|
||||
gi: usize,
|
||||
) -> Vec<crate::ui::dock::PanelId> {
|
||||
self.dock.groups(side)[gi]
|
||||
.tabs
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|id| self.dock_panel_visible(*id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The panel slot `gi` on `side` shows: its active tab, or the first
|
||||
/// visible one when the active panel is closed.
|
||||
pub(crate) fn dock_group_shown(
|
||||
&self,
|
||||
side: crate::app::config::DockSide,
|
||||
gi: usize,
|
||||
) -> Option<crate::ui::dock::PanelId> {
|
||||
let active = self.dock.groups(side)[gi].active;
|
||||
if self.dock_panel_visible(active) {
|
||||
return Some(active);
|
||||
}
|
||||
self.dock_group_visible_tabs(side, gi).first().copied()
|
||||
}
|
||||
|
||||
/// Indices of the slots on `side` that render (hold a visible panel).
|
||||
pub(crate) fn dock_visible_groups(&self, side: crate::app::config::DockSide) -> Vec<usize> {
|
||||
(0..self.dock.groups(side).len())
|
||||
.filter(|gi| self.dock_group_shown(side, *gi).is_some())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Visible slots on `side` that are open in the edge column rather than
|
||||
/// reduced to a rail tab: not auto-collapsing, or hovered.
|
||||
pub(crate) fn dock_expanded_groups(&self, side: crate::app::config::DockSide) -> Vec<usize> {
|
||||
self.dock_visible_groups(side)
|
||||
.into_iter()
|
||||
.filter(|gi| {
|
||||
let shown = self.dock_group_shown(side, *gi).expect("visible group");
|
||||
!self.dock.auto_collapse(shown)
|
||||
|| self
|
||||
.dock_expanded
|
||||
.is_some_and(|e| self.dock_group_visible_tabs(side, *gi).contains(&e))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Width of the edge column on `side`: its widest shown panel.
|
||||
pub(crate) fn dock_column_width(&self, side: crate::app::config::DockSide) -> f32 {
|
||||
self.dock_visible_groups(side)
|
||||
.into_iter()
|
||||
.filter_map(|gi| self.dock_group_shown(side, gi))
|
||||
.map(|id| self.dock.width(id, self.win_size.0))
|
||||
.fold(0.0, f32::max)
|
||||
}
|
||||
|
||||
/// Visible slots on `side` with their vertical span in the workspace.
|
||||
pub(crate) fn dock_slot_spans(
|
||||
&self,
|
||||
side: crate::app::config::DockSide,
|
||||
) -> Vec<(usize, f32, f32)> {
|
||||
let (_, avail) = self.dock_workspace_size();
|
||||
let visible = self.dock_visible_groups(side);
|
||||
let weights: Vec<f32> = visible
|
||||
.iter()
|
||||
.map(|gi| self.dock.groups(side)[*gi].weight)
|
||||
.collect();
|
||||
visible
|
||||
.into_iter()
|
||||
.zip(crate::ui::dock::slot_spans(&weights, avail))
|
||||
.map(|(gi, (top, bottom))| (gi, top, bottom))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Size a panel takes when floated: its current floating size, or its
|
||||
/// docked width by a comfortable share of the workspace height.
|
||||
pub(crate) fn dock_float_size(&self, id: crate::ui::dock::PanelId) -> (f32, f32) {
|
||||
if let Some(f) = self.dock.float_rect(id) {
|
||||
return (f.w, f.h);
|
||||
}
|
||||
let (_, avail) = self.dock_workspace_size();
|
||||
(
|
||||
self.dock.width(id, self.win_size.0),
|
||||
(avail * 0.6).clamp(crate::ui::dock::FLOAT_MIN_H, 520.0),
|
||||
)
|
||||
}
|
||||
|
||||
/// Where dragged panel `id` lands if released at workspace point `p`:
|
||||
/// near an edge it docks (above / below a slot, or as a tab in the middle
|
||||
/// of one), anywhere else it floats.
|
||||
pub(crate) fn dock_drop_target(
|
||||
&self,
|
||||
id: crate::ui::dock::PanelId,
|
||||
p: iced::Point,
|
||||
) -> crate::ui::dock::DropTarget {
|
||||
use crate::app::config::DockSide;
|
||||
use crate::ui::dock::{slot_zone, DropTarget, SlotZone, DOCK_EDGE_ZONE};
|
||||
let (ww, wh) = self.dock_workspace_size();
|
||||
for side in [DockSide::Left, DockSide::Right] {
|
||||
let zone = self.dock_column_width(side).max(DOCK_EDGE_ZONE);
|
||||
let inside = match side {
|
||||
DockSide::Left => p.x <= zone,
|
||||
DockSide::Right => p.x >= ww - zone,
|
||||
};
|
||||
if !inside {
|
||||
continue;
|
||||
}
|
||||
let spans = self.dock_slot_spans(side);
|
||||
let Some(&(gi, top, bottom)) = spans
|
||||
.iter()
|
||||
.find(|(_, _, bottom)| p.y < *bottom)
|
||||
.or(spans.last())
|
||||
else {
|
||||
return DropTarget::Edge {
|
||||
side,
|
||||
index: self.dock.groups(side).len(),
|
||||
};
|
||||
};
|
||||
return match slot_zone(p.y, top, bottom) {
|
||||
SlotZone::Above => DropTarget::Edge { side, index: gi },
|
||||
SlotZone::Below => DropTarget::Edge { side, index: gi + 1 },
|
||||
// Tabbing a panel onto its own lone slot changes nothing.
|
||||
SlotZone::Middle if self.dock_group_visible_tabs(side, gi) == [id] => {
|
||||
DropTarget::Edge { side, index: gi }
|
||||
}
|
||||
SlotZone::Middle => DropTarget::Tab { side, group: gi },
|
||||
};
|
||||
}
|
||||
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));
|
||||
DropTarget::Float { x, y }
|
||||
}
|
||||
|
||||
/// Rebuild the Reference Manager's entry list from the active drawing.
|
||||
@@ -1602,112 +1832,222 @@ mod tests {
|
||||
assert!(!app.show_block_palette, "close dismisses the sidebar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blockpalette_dock_moves_to_other_side_and_persists() {
|
||||
/// A hermetic app with a 1600×900 workspace and the default dock layout
|
||||
/// (Properties left, block palette right), both shown.
|
||||
fn dock_app() -> OpenCADStudio {
|
||||
let mut app = fresh();
|
||||
// Tests load the user's persisted config; reset the dock to a known
|
||||
// state so this stays hermetic.
|
||||
app.dock = Default::default();
|
||||
app.dock.ensure_settings();
|
||||
app.show_properties = true;
|
||||
app.show_block_palette = true;
|
||||
let i = app.active_tab;
|
||||
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600.0, 900.0);
|
||||
app.win_size = (1600.0, 900.0).into();
|
||||
app
|
||||
}
|
||||
|
||||
/// Press `id`'s title bar at `from`, drag to `to`, and return the target
|
||||
/// shown just before release.
|
||||
fn drag_panel(
|
||||
app: &mut OpenCADStudio,
|
||||
id: crate::ui::dock::PanelId,
|
||||
from: iced::Point,
|
||||
to: iced::Point,
|
||||
) -> Option<crate::ui::dock::DropTarget> {
|
||||
use crate::ui::dock::DockMsg;
|
||||
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 _ = app.on_dock(DockMsg::DragRelease);
|
||||
target
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blockpalette_dock_moves_to_other_side_and_persists() {
|
||||
use crate::app::config::DockSide;
|
||||
use crate::ui::dock::DropTarget;
|
||||
let mut app = dock_app();
|
||||
let id = crate::ui::dock::PanelId::BlockPalette;
|
||||
assert_eq!(
|
||||
app.dock.location(id),
|
||||
Some((crate::app::config::DockSide::Right, 0))
|
||||
assert_eq!(app.dock.location(id), Some((DockSide::Right, 0)));
|
||||
// Top quarter of the Properties slot: dock above it.
|
||||
let target = drag_panel(
|
||||
&mut app,
|
||||
id,
|
||||
iced::Point::new(1500.0, 10.0),
|
||||
iced::Point::new(100.0, 40.0),
|
||||
);
|
||||
let _ = app.on_dock(crate::ui::dock::DockMsg::DockGrab(id));
|
||||
app.win_size = (1600.0, 900.0).into();
|
||||
let _ = app.on_dock(crate::ui::dock::DockMsg::DragMove(iced::Point::new(100.0, 100.0)));
|
||||
assert_eq!(target, Some(DropTarget::Edge { side: DockSide::Left, index: 0 }));
|
||||
assert_eq!(app.dock.location(id), Some((DockSide::Left, 0)));
|
||||
assert_eq!(
|
||||
app.dock_drag_target,
|
||||
Some((crate::app::config::DockSide::Left, 0))
|
||||
app.dock.location(crate::ui::dock::PanelId::Properties),
|
||||
Some((DockSide::Left, 1))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dock_click_without_drag_changes_nothing() {
|
||||
let mut app = dock_app();
|
||||
let before = app.dock.clone();
|
||||
let id = crate::ui::dock::PanelId::BlockPalette;
|
||||
let p = iced::Point::new(1500.0, 10.0);
|
||||
let target = drag_panel(&mut app, id, p, iced::Point::new(1502.0, 11.0));
|
||||
assert_eq!(target, None, "movement under the threshold is a click");
|
||||
assert_eq!(app.dock, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dock_drop_in_slot_middle_adds_a_tab() {
|
||||
use crate::app::config::DockSide;
|
||||
use crate::ui::dock::{DropTarget, PanelId};
|
||||
let mut app = dock_app();
|
||||
let target = drag_panel(
|
||||
&mut app,
|
||||
PanelId::BlockPalette,
|
||||
iced::Point::new(1500.0, 10.0),
|
||||
iced::Point::new(100.0, 450.0),
|
||||
);
|
||||
assert_eq!(target, Some(DropTarget::Tab { side: DockSide::Left, group: 0 }));
|
||||
assert_eq!(
|
||||
app.dock.left[0].tabs,
|
||||
vec![PanelId::Properties, PanelId::BlockPalette]
|
||||
);
|
||||
assert_eq!(app.dock.left[0].active, PanelId::BlockPalette);
|
||||
// Clicking the Properties tab shows it again.
|
||||
let _ = app.on_dock(crate::ui::dock::DockMsg::SelectTab(PanelId::Properties));
|
||||
let _ = app.on_dock(crate::ui::dock::DockMsg::DragRelease);
|
||||
assert_eq!(
|
||||
app.dock.location(id),
|
||||
Some((crate::app::config::DockSide::Left, 0))
|
||||
);
|
||||
assert_eq!(app.dock.left[0].active, PanelId::Properties);
|
||||
assert_eq!(app.dock.left[0].tabs.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dock_visible_len_counts_only_rendered_panels() {
|
||||
let mut app = fresh();
|
||||
app.dock = Default::default();
|
||||
app.dock.left = vec![
|
||||
crate::ui::dock::PanelId::BlockPalette,
|
||||
crate::ui::dock::PanelId::Properties,
|
||||
];
|
||||
// Left: block palette hidden, properties shown -> 1 visible panel.
|
||||
app.show_block_palette = false;
|
||||
app.show_properties = true;
|
||||
assert_eq!(
|
||||
app.dock_visible_len(crate::app::config::DockSide::Left),
|
||||
1
|
||||
fn dock_drop_in_bottom_quarter_docks_below() {
|
||||
use crate::app::config::DockSide;
|
||||
use crate::ui::dock::{DropTarget, PanelId};
|
||||
let mut app = dock_app();
|
||||
let target = drag_panel(
|
||||
&mut app,
|
||||
PanelId::BlockPalette,
|
||||
iced::Point::new(1500.0, 10.0),
|
||||
iced::Point::new(100.0, 880.0),
|
||||
);
|
||||
// Reveal the block palette -> both count.
|
||||
assert_eq!(target, Some(DropTarget::Edge { side: DockSide::Left, index: 1 }));
|
||||
assert_eq!(app.dock.location(PanelId::BlockPalette), Some((DockSide::Left, 1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dock_drop_over_the_viewport_floats_the_panel() {
|
||||
use crate::ui::dock::{DropTarget, PanelId};
|
||||
let mut app = dock_app();
|
||||
let target = drag_panel(
|
||||
&mut app,
|
||||
PanelId::BlockPalette,
|
||||
iced::Point::new(1500.0, 10.0),
|
||||
iced::Point::new(800.0, 400.0),
|
||||
);
|
||||
assert!(matches!(target, Some(DropTarget::Float { .. })));
|
||||
assert!(app.dock.right.is_empty());
|
||||
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(
|
||||
&mut app,
|
||||
PanelId::BlockPalette,
|
||||
iced::Point::new(f.x + 20.0, f.y + 10.0),
|
||||
iced::Point::new(f.x + 120.0, f.y + 60.0),
|
||||
);
|
||||
assert_eq!(target, Some(DropTarget::Float { x: f.x + 100.0, y: f.y + 50.0 }));
|
||||
// Opening a floating panel must not re-dock it.
|
||||
assert!(app.dock.is_placed(PanelId::BlockPalette));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dock_drop_on_own_lone_slot_is_a_no_op() {
|
||||
use crate::app::config::DockSide;
|
||||
use crate::ui::dock::{DropTarget, PanelId};
|
||||
let mut app = dock_app();
|
||||
let before = app.dock.clone();
|
||||
let target = drag_panel(
|
||||
&mut app,
|
||||
PanelId::Properties,
|
||||
iced::Point::new(100.0, 10.0),
|
||||
iced::Point::new(100.0, 450.0),
|
||||
);
|
||||
assert_eq!(target, Some(DropTarget::Edge { side: DockSide::Left, index: 0 }));
|
||||
assert_eq!(app.dock, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dock_visible_groups_skip_closed_panels() {
|
||||
use crate::app::config::DockSide;
|
||||
use crate::ui::dock::{DockGroup, PanelId};
|
||||
let mut app = dock_app();
|
||||
app.dock.left = vec![
|
||||
DockGroup::single(PanelId::BlockPalette, 1.0),
|
||||
DockGroup::single(PanelId::Properties, 1.0),
|
||||
];
|
||||
app.show_block_palette = false;
|
||||
assert_eq!(app.dock_visible_groups(DockSide::Left), vec![1]);
|
||||
// The single visible slot spans the full height, so the drop geometry
|
||||
// never splits around the hidden one.
|
||||
assert_eq!(app.dock_slot_spans(DockSide::Left), vec![(1, 0.0, 900.0)]);
|
||||
app.show_block_palette = true;
|
||||
assert_eq!(
|
||||
app.dock_visible_len(crate::app::config::DockSide::Left),
|
||||
2
|
||||
);
|
||||
// A hidden (closed) panel counts for nothing even when stacked.
|
||||
app.show_block_palette = false;
|
||||
assert_eq!(
|
||||
app.dock_visible_len(crate::app::config::DockSide::Left),
|
||||
1
|
||||
);
|
||||
assert_eq!(app.dock_visible_groups(DockSide::Left), vec![0, 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dock_drag_target_ignores_hidden_panels_on_the_side() {
|
||||
// Reproduce the persisted layout that exposed a half-height ghost: two
|
||||
// panels live in the left stack but the block palette is hidden
|
||||
// (show_block_palette=false), so only Properties renders. Drag geometry
|
||||
// must count only panels that are actually visible.
|
||||
let mut app = fresh();
|
||||
app.dock = Default::default();
|
||||
app.dock.left = vec![
|
||||
crate::ui::dock::PanelId::BlockPalette,
|
||||
crate::ui::dock::PanelId::Properties,
|
||||
];
|
||||
fn dock_group_shows_a_visible_tab_when_the_active_one_is_closed() {
|
||||
use crate::app::config::DockSide;
|
||||
use crate::ui::dock::PanelId;
|
||||
let mut app = dock_app();
|
||||
app.dock.add_tab(PanelId::BlockPalette, DockSide::Left, 0);
|
||||
assert_eq!(app.dock_group_shown(DockSide::Left, 0), Some(PanelId::BlockPalette));
|
||||
app.show_block_palette = false;
|
||||
app.show_properties = true;
|
||||
let i = app.active_tab;
|
||||
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600.0, 900.0);
|
||||
app.win_size = (1600.0, 900.0).into();
|
||||
let id = crate::ui::dock::PanelId::Properties;
|
||||
let _ = app.on_dock(crate::ui::dock::DockMsg::DockGrab(id));
|
||||
// Pointer near the bottom of the left edge: one visible panel means a
|
||||
// single slot, so every y maps to index 0 (no top/bottom split).
|
||||
let _ =
|
||||
app.on_dock(crate::ui::dock::DockMsg::DragMove(iced::Point::new(100.0, 850.0)));
|
||||
assert_eq!(
|
||||
app.dock_drag_target,
|
||||
Some((crate::app::config::DockSide::Left, 0))
|
||||
);
|
||||
assert_eq!(app.dock_group_shown(DockSide::Left, 0), Some(PanelId::Properties));
|
||||
app.show_properties = false;
|
||||
assert_eq!(app.dock_group_shown(DockSide::Left, 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dock_drag_target_counts_all_visible_panels() {
|
||||
let mut app = fresh();
|
||||
app.dock = Default::default();
|
||||
fn dock_splitter_drag_changes_slot_heights() {
|
||||
use crate::app::config::DockSide;
|
||||
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
|
||||
let mut app = dock_app();
|
||||
app.dock.left = vec![
|
||||
crate::ui::dock::PanelId::BlockPalette,
|
||||
crate::ui::dock::PanelId::Properties,
|
||||
DockGroup::single(PanelId::BlockPalette, 1.0),
|
||||
DockGroup::single(PanelId::Properties, 1.0),
|
||||
];
|
||||
// Both panels shown: two real slots on the left edge. A pointer near
|
||||
// the bottom maps to the append slot (index == 2).
|
||||
app.show_block_palette = true;
|
||||
app.show_properties = true;
|
||||
let i = app.active_tab;
|
||||
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600.0, 900.0);
|
||||
app.win_size = (1600.0, 900.0).into();
|
||||
let id = crate::ui::dock::PanelId::Properties;
|
||||
let _ = app.on_dock(crate::ui::dock::DockMsg::DockGrab(id));
|
||||
let _ =
|
||||
app.on_dock(crate::ui::dock::DockMsg::DragMove(iced::Point::new(100.0, 850.0)));
|
||||
assert_eq!(
|
||||
app.dock_drag_target,
|
||||
Some((crate::app::config::DockSide::Left, 2))
|
||||
);
|
||||
app.dock.right.clear();
|
||||
let _ = app.on_dock(DockMsg::SplitGrab(DockSide::Left, 0, 1));
|
||||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(100.0, 450.0)));
|
||||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(100.0, 675.0)));
|
||||
let _ = app.on_dock(DockMsg::DragRelease);
|
||||
// 225 px of a 900 px edge holding 2 weight units = 0.5 units.
|
||||
assert!((app.dock.left[0].weight - 1.5).abs() < 1e-4);
|
||||
assert!((app.dock.left[1].weight - 0.5).abs() < 1e-4);
|
||||
let _ = app.on_dock(DockMsg::SplitReset(DockSide::Left));
|
||||
assert_eq!(app.dock.left[0].weight, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dock_float_resize_grows_the_panel() {
|
||||
use crate::ui::dock::{DockMsg, FloatPanel, PanelId};
|
||||
let mut app = dock_app();
|
||||
app.dock.float(FloatPanel {
|
||||
id: PanelId::BlockPalette,
|
||||
x: 100.0,
|
||||
y: 100.0,
|
||||
w: 260.0,
|
||||
h: 300.0,
|
||||
});
|
||||
let _ = app.on_dock(DockMsg::FloatResizeGrab(PanelId::BlockPalette));
|
||||
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();
|
||||
assert_eq!((f.w, f.h), (300.0, 350.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2568,7 +2568,7 @@ impl OpenCADStudio {
|
||||
if self.show_external_references {
|
||||
use crate::app::config::DockSide;
|
||||
use crate::ui::dock::PanelId;
|
||||
if self.dock.location(PanelId::ExternalReferences).is_none() {
|
||||
if !self.dock.is_placed(PanelId::ExternalReferences) {
|
||||
self.dock.dock(PanelId::ExternalReferences, DockSide::Right, usize::MAX);
|
||||
}
|
||||
self.dock_expanded = Some(PanelId::ExternalReferences);
|
||||
|
||||
Reference in new issue
Block a user