Merge pull request #1702 from MarcelP102/Pallet-placement

[UX] Pallets you can dock, group and float (including LAYERS panel)
This commit is contained in:
Hakan Seven committed 2026-10-12 00:27:53 +03:00
commit 7ca457a6cc
54 files changed
+4245 -1098

No files matched your search

+2 -13
View File
@@ -929,18 +929,7 @@ impl OpenCADStudio {
if self.show_external_references {
// Always open expanded so the panel is immediately usable;
// dock it on the right if the user closed it from the layout.
if self
.dock
.location(crate::ui::dock::PanelId::ExternalReferences)
.is_none()
{
self.dock.dock(
crate::ui::dock::PanelId::ExternalReferences,
crate::app::config::DockSide::Right,
usize::MAX,
);
}
self.dock_expanded = Some(crate::ui::dock::PanelId::ExternalReferences);
self.dock_open_at_default(crate::ui::dock::PanelId::ExternalReferences);
self.refresh_xref_manager();
}
}
@@ -1784,7 +1773,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 -7
View File
@@ -259,13 +259,9 @@ impl OpenCADStudio {
self.count_palette.show = open;
self.ribbon.set_count_palette(open);
if open {
if self.dock.location(id).is_none() {
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_open_at_default(id);
} else {
self.dock_unpeek(id);
}
}
+3 -1
View File
@@ -384,7 +384,9 @@ impl OpenCADStudio {
"ORTHO" => return Some(Task::done(Message::ToggleOrtho)),
"PARALLEL" => return Some(Task::done(Message::SetProjection(true))),
"PERSP" => return Some(Task::done(Message::SetProjection(false))),
"LAYERS" => return Some(Task::done(Message::ToggleLayers)),
// LAYERS opens the Layer Manager pallet (its close button or
// the toggle message close it).
"LAYERS" => self.open_layers_pallet(),
// SCRIPT <path> — run a command script: each non-comment
// line is fed through the same command path the `--script` startup
+1 -9
View File
@@ -704,15 +704,7 @@ impl OpenCADStudio {
// rearrange themselves on upgrade for a panel they have
// not asked for.
let id = crate::ui::dock::PanelId::Browser;
if self.dock.location(id).is_none() {
// dock() clamps an out-of-range index to the end of
// the edge, so this appends below whatever is there.
self.dock
.dock(id, crate::app::config::DockSide::Right, usize::MAX);
}
// Open expanded so it is usable straight away; the pin
// button still collapses it.
self.dock_expanded = Some(id);
self.dock_open_at_default(id);
self.command_line
.push_output(crate::t!("Browser opened.").as_ref());
} else {
+2 -8
View File
@@ -444,17 +444,11 @@ impl OpenCADStudio {
"POINTCLOUDMANAGER" => {
let id = crate::ui::dock::PanelId::PointCloudManager;
self.pc_manager.show = true;
if self.dock.location(id).is_none() {
self.dock
.dock(id, crate::app::config::DockSide::Right, usize::MAX);
}
self.dock_expanded = Some(id);
self.dock_open_at_default(id);
}
"POINTCLOUDMANAGERCLOSE" => {
self.pc_manager.show = false;
if self.dock_expanded == Some(crate::ui::dock::PanelId::PointCloudManager) {
self.dock_expanded = None;
}
self.dock_unpeek(crate::ui::dock::PanelId::PointCloudManager);
}
"_PCUNCROP" => self
.edit_selected_point_clouds(i, "POINTCLOUDUNCROP", |data| data.croppings.clear()),
+3 -7
View File
@@ -660,13 +660,9 @@ impl OpenCADStudio {
self.sheet_set.show = on;
self.ribbon.set_sheet_set(on);
if on {
if self.dock.location(id).is_none() {
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_open_at_default(id);
} else {
self.dock_unpeek(id);
}
}
+12
View File
@@ -0,0 +1,12 @@
//! The app side of docking: pallets docked on the drawing view's edges (one
//! group shown per edge, listed in an icon strip) or floating over it.
//!
//! The persisted layout and its pure geometry live in [`crate::ui::dock`];
//! this module holds the transient state handling (`update`) and drawing
//! (`view`).
mod update;
mod view;
#[cfg(test)]
mod tests;
+670
View File
@@ -0,0 +1,670 @@
//! Dock behaviour through the app: drags, hover, menus, and the real view.
use crate::app::{Message, OpenCADStudio};
fn fresh() -> OpenCADStudio {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
app
}
#[test]
fn blockpalette_pin_toggles_autocollapse_and_close_hides() {
let mut app = fresh();
app.show_block_palette = true;
let id = crate::ui::dock::PanelId::BlockPalette;
let _ = app.on_dock(crate::ui::dock::DockMsg::AutoCollapseToggle(id));
assert!(app.dock.auto_hides(id), "pin enables auto-hide");
let _ = app.on_dock(crate::ui::dock::DockMsg::AutoCollapseToggle(id));
assert!(!app.dock.auto_hides(id), "second pin disables auto-hide");
let _ = app.on_dock(crate::ui::dock::DockMsg::Close(id));
assert!(!app.show_block_palette, "close dismisses the sidebar");
}
/// 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
}
/// 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(
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 = drag_target(app);
let _ = app.on_dock(DockMsg::DragRelease);
target
}
#[test]
fn dropping_on_a_pallets_top_or_bottom_half_stacks_above_or_below_it() {
use crate::app::config::DockSide;
use crate::ui::dock::{DropTarget, PanelId};
let mut app = dock_app();
let id = PanelId::BlockPalette;
assert_eq!(app.dock.location(id), Some((DockSide::Right, 0)));
let target = drag_panel(
&mut app,
id,
iced::Point::new(1500.0, 10.0),
iced::Point::new(150.0, 300.0),
);
assert_eq!(
target,
Some(DropTarget::Join {
side: DockSide::Left,
group: 0,
index: 0
})
);
assert_eq!(
app.dock.left[0].panels,
vec![PanelId::BlockPalette, PanelId::Properties]
);
assert!(app.dock.right.is_empty());
// The bottom half of a pallet stacks below it.
let target = drag_panel(
&mut app,
id,
iced::Point::new(150.0, 100.0),
iced::Point::new(150.0, 800.0),
);
assert_eq!(
target,
Some(DropTarget::Join {
side: DockSide::Left,
group: 0,
index: 2
})
);
assert_eq!(
app.dock.left[0].panels,
vec![PanelId::Properties, PanelId::BlockPalette]
);
}
#[test]
fn dock_dragging_a_lone_pallet_over_its_own_group_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();
// The right edge's only group holds just the block palette; drag it
// around over itself.
let target = drag_panel(
&mut app,
PanelId::BlockPalette,
iced::Point::new(1500.0, 10.0),
iced::Point::new(1450.0, 600.0),
);
assert_eq!(
target,
Some(DropTarget::Join {
side: DockSide::Right,
group: 0,
index: 1
})
);
assert_eq!(app.dock, before);
}
#[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_below_the_strip_groups_starts_a_new_shown_group() {
use crate::app::config::DockSide;
use crate::ui::dock::{DropTarget, PanelId};
let mut app = dock_app();
let (_, plus_top) = app.dock_strip_layout(DockSide::Left);
let target = drag_panel(
&mut app,
PanelId::BlockPalette,
iced::Point::new(1500.0, 10.0),
iced::Point::new(18.0, plus_top + 10.0),
);
assert_eq!(
target,
Some(DropTarget::Edge {
side: DockSide::Left,
index: 1
})
);
assert_eq!(app.dock.left.len(), 2);
assert_eq!(app.dock.left[1].panels, vec![PanelId::BlockPalette]);
// The new group is the one shown: the column holds only the palette.
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
assert_eq!(app.dock_slot_spans(DockSide::Left), vec![(0, 0.0, 900.0)]);
}
#[test]
fn dock_icon_press_switches_the_shown_group() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
let mut app = dock_app();
app.show_browser = true;
app.dock.left = vec![
DockGroup::stack(vec![PanelId::Properties, PanelId::BlockPalette]),
DockGroup::single(PanelId::Browser),
];
app.dock.right.clear();
// Group 0 shows both stacked pallets.
assert_eq!(app.dock_slot_spans(DockSide::Left).len(), 2);
let _ = app.on_dock(DockMsg::IconPress(PanelId::Browser));
let _ = app.on_dock(DockMsg::DragRelease);
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
assert_eq!(app.dock_slot_spans(DockSide::Left), vec![(0, 0.0, 900.0)]);
// Each group keeps its own width.
app.dock.set_group_width(DockSide::Left, 1, 400.0);
assert_eq!(app.dock_column_width(DockSide::Left), 400.0);
let _ = app.on_dock(DockMsg::IconPress(PanelId::BlockPalette));
let _ = app.on_dock(DockMsg::DragRelease);
assert_eq!(app.dock_column_width(DockSide::Left), 260.0);
}
#[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_dragging_an_icon_into_another_group_lands_between_its_icons() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockGroup, DockMsg, DropTarget, PanelId, STRIP_CELL_H};
let mut app = dock_app();
app.show_browser = true;
app.dock.left = vec![
DockGroup::stack(vec![PanelId::Properties, PanelId::Browser]),
DockGroup::single(PanelId::BlockPalette),
];
app.dock.right.clear();
let (layout, _) = app.dock_strip_layout(DockSide::Left);
// Drag the Blocks icon (group 1) to between Properties and Browser.
let _ = app.on_dock(DockMsg::IconPress(PanelId::BlockPalette));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(
20.0,
layout[1].icons_top + 10.0,
)));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(
20.0,
layout[0].icons_top + STRIP_CELL_H + 2.0,
)));
assert_eq!(
drag_target(&app),
Some(DropTarget::Join {
side: DockSide::Left,
group: 0,
index: 1
})
);
let _ = app.on_dock(DockMsg::DragRelease);
assert_eq!(app.dock.left.len(), 1, "the emptied group goes away");
assert_eq!(
app.dock.left[0].panels,
vec![PanelId::Properties, PanelId::BlockPalette, PanelId::Browser]
);
}
#[test]
fn dock_grip_drag_moves_a_whole_group() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
let mut app = dock_app();
app.show_browser = true;
app.dock.left = vec![
DockGroup::stack(vec![PanelId::Properties, PanelId::Browser]),
DockGroup::single(PanelId::BlockPalette),
];
app.dock.right.clear();
let (layout, plus_top) = app.dock_strip_layout(DockSide::Left);
let _ = app.on_dock(DockMsg::GroupGrab(DockSide::Left, 0));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(
18.0,
layout[0].top + 2.0,
)));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, plus_top + 10.0)));
let _ = app.on_dock(DockMsg::DragRelease);
assert_eq!(app.dock.left[0].panels, vec![PanelId::BlockPalette]);
assert_eq!(
app.dock.left[1].panels,
vec![PanelId::Properties, PanelId::Browser]
);
// 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, before);
}
#[test]
fn dock_closed_pallets_take_no_room_and_empty_groups_hide() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockGroup, PanelId};
let mut app = dock_app();
app.dock.left = vec![
DockGroup::stack(vec![PanelId::BlockPalette, PanelId::Properties]),
DockGroup::single(PanelId::Browser),
];
app.show_block_palette = false;
app.show_browser = false;
// Only Properties is open: one listed group, full height.
assert_eq!(app.dock_visible_groups(DockSide::Left), vec![0]);
assert_eq!(app.dock_slot_spans(DockSide::Left), vec![(1, 0.0, 900.0)]);
// A shown group with nothing open falls back to one that has.
app.dock.show_group(DockSide::Left, 1);
assert_eq!(app.dock_shown_group(DockSide::Left), Some(0));
}
#[test]
fn dock_splitter_drag_changes_pallet_heights() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
let mut app = dock_app();
app.dock.left = vec![DockGroup::stack(vec![
PanelId::BlockPalette,
PanelId::Properties,
])];
app.dock.right.clear();
let _ = app.on_dock(DockMsg::SplitGrab(DockSide::Left, 0, 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.
let w = &app.dock.left[0].weights;
assert!((w[0] - 1.5).abs() < 1e-4 && (w[1] - 0.5).abs() < 1e-4);
let _ = app.on_dock(DockMsg::SplitReset(DockSide::Left, 0));
assert_eq!(app.dock.left[0].weights, vec![1.0, 1.0]);
}
#[test]
fn dock_hover_reveals_groups_only_on_an_auto_hiding_edge() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
let mut app = dock_app();
app.show_browser = true;
app.dock.left.push(DockGroup::single(PanelId::Browser));
// The pin on any docked pallet turns auto-hide on for its edge.
let _ = app.on_dock(DockMsg::AutoCollapseToggle(PanelId::Properties));
assert!(app.dock.edge_auto_hide(DockSide::Left));
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));
// ...but only once the pointer rests: the reveal waits for the delay.
assert!(!app.dock_edge_expanded(DockSide::Left));
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
assert!(app.dock_edge_expanded(DockSide::Left));
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
// Leaving and coming back within the delay keeps it open.
let _ = app.on_dock(DockMsg::HoverExit);
let stale = app.dock_hover_gen;
let _ = app.on_dock(DockMsg::HoverStay);
let _ = app.on_dock(DockMsg::HoverSettled(stale));
assert!(app.dock_edge_expanded(DockSide::Left));
let _ = app.on_dock(DockMsg::HoverExit);
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
assert!(!app.dock_edge_expanded(DockSide::Left));
// Without auto-hide, hovering does not switch groups (clicks do).
let _ = app.on_dock(DockMsg::AutoCollapseToggle(PanelId::Properties));
let _ = app.on_dock(DockMsg::Hover(PanelId::Properties));
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
assert!(app.dock_edge_expanded(DockSide::Left));
}
#[test]
fn dock_float_resize_grows_the_panel() {
use crate::ui::dock::{DockMsg, FloatPanel, PanelId};
let mut app = dock_app();
app.dock.float(FloatPanel::new(
PanelId::BlockPalette,
100.0,
100.0,
260.0,
300.0,
));
let _ = app.on_dock(DockMsg::FloatResizeGrab(PanelId::BlockPalette, false));
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));
// The bottom-left grip grows the panel leftward, keeping its right edge.
let _ = app.on_dock(DockMsg::FloatResizeGrab(PanelId::BlockPalette, true));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(100.0, 450.0)));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(60.0, 450.0)));
let _ = app.on_dock(DockMsg::DragRelease);
let g = app.dock.float_rect(PanelId::BlockPalette).unwrap();
assert_eq!((g.x, g.w, g.h), (f.x - 40.0, 340.0, 350.0));
}
#[test]
fn dock_edge_menu_adds_and_hides_pallets() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockMsg, PanelId};
let mut app = dock_app();
app.show_browser = false;
let _ = app.on_dock(DockMsg::EdgeMenu(Some(DockSide::Left)));
// Checking a closed pallet opens it as a new, shown group here.
let _ = app.on_dock(DockMsg::EdgeMenuToggle(DockSide::Left, PanelId::Browser));
assert!(app.show_browser);
assert_eq!(app.dock.left.len(), 2);
assert_eq!(app.dock.left[1].panels, vec![PanelId::Browser]);
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
assert_eq!(app.dock_edge_menu, None, "the menu closes after adding");
let _ = app.on_dock(DockMsg::EdgeMenu(Some(DockSide::Left)));
// Unchecking hides it but keeps its place.
let _ = app.on_dock(DockMsg::EdgeMenuToggle(DockSide::Left, PanelId::Browser));
assert!(!app.show_browser);
assert_eq!(
app.dock.location(PanelId::Browser),
Some((DockSide::Left, 1))
);
// Opening it again from the same edge reuses that place.
let _ = app.on_dock(DockMsg::EdgeMenuToggle(DockSide::Left, PanelId::Browser));
assert_eq!(app.dock.left.len(), 2);
// A pallet open on the other edge is not checked here: picking it
// moves it over instead of hiding it.
let _ = app.on_dock(DockMsg::EdgeMenuToggle(
DockSide::Left,
PanelId::BlockPalette,
));
assert!(app.show_block_palette);
assert!(app.dock.right.is_empty());
assert_eq!(
app.dock.location(PanelId::BlockPalette),
Some((DockSide::Left, 2))
);
}
#[test]
fn dock_divider_resizes_only_its_group() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
let mut app = dock_app();
app.dock.left = vec![
DockGroup::stack(vec![PanelId::Properties, PanelId::BlockPalette]),
DockGroup::single(PanelId::Count),
];
app.dock.right.clear();
let _ = app.on_dock(DockMsg::ResizeGrab(PanelId::Properties));
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); 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]
fn layers_command_opens_a_floating_pallet_and_toggles_it_closed() {
use crate::ui::dock::PanelId;
let mut app = dock_app();
let _ = app.update(Message::ToggleLayers);
assert!(app.show_layers);
assert!(app.active_modal.is_none(), "no modal backdrop");
let f = app
.dock
.float_rect(PanelId::Layers)
.expect("floats on first use");
assert!(f.x > 0.0 && f.y >= 0.0);
let _ = app.update(Message::ToggleLayers);
assert!(!app.show_layers);
// Its place is kept for next time.
assert!(app.dock.float_rect(PanelId::Layers).is_some());
}
/// 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")
}
/// 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;
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"),
}
}
/// 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(), VIEW_SIZE, cache, &mut renderer);
let _ = ui.update(
&window::Headless,
&shell::Waker::noop(),
std::slice::from_ref(event),
cursor,
&mut renderer,
&mut messages,
);
cache = ui.into_cache();
}
for message in messages {
let _ = app.update(message);
}
}
}
#[test]
fn docked_title_bar_double_click_floats_the_pallet_in_the_real_view() {
use crate::ui::dock::PanelId;
let mut app = dock_app();
let press = iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left));
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.
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,
})],
);
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
assert!(
app.dock_edge_expanded(DockSide::Right),
"hovering the icon reveals the auto-hiding edge"
);
}
#[test]
fn layers_ribbon_button_opens_the_pallet_without_staying_highlighted() {
let mut app = dock_app();
// The ribbon's LAYERS button runs the LAYERS command.
let click = || Message::RibbonToolClick {
tool_id: "LAYERS".to_string(),
event: crate::modules::ModuleEvent::Command("LAYERS".to_string()),
};
let _ = app.update(click());
assert!(app.show_layers);
assert_eq!(app.ribbon.active_tool(), None);
// Pressing it again keeps it open, like the Blocks palette's button.
let _ = app.update(click());
assert!(app.show_layers);
}
#[test]
fn dock_double_clicks_float_and_dock_panels() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockMsg, PanelId};
let mut app = dock_app();
let _ = app.on_dock(DockMsg::FloatOut(PanelId::Properties));
assert!(app.dock.left.is_empty());
let f = app.dock.float_rect(PanelId::Properties).expect("floating");
assert!(f.x > 0.0, "floats clear of the left edge");
let _ = app.on_dock(DockMsg::DockTo(PanelId::Properties, DockSide::Right));
assert!(app.dock.floating.is_empty());
assert_eq!(
app.dock.location(PanelId::Properties),
Some((DockSide::Right, 1))
);
}
#[test]
fn blockpalette_width_reset() {
use crate::app::config::DockSide;
let mut app = dock_app();
let id = crate::ui::dock::PanelId::BlockPalette;
app.dock.set_group_width(DockSide::Right, 0, 500.0);
let _ = app.on_dock(crate::ui::dock::DockMsg::WidthReset(id));
assert_eq!(app.dock.group_width(DockSide::Right, 0), 260.0);
}
#[test]
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);
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);
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)
);
}
+788
View File
@@ -0,0 +1,788 @@
//! Dock state changes: drags, hover, open / close, and the queries the view
//! and drop targeting share.
use crate::app::{Message, OpenCADStudio};
/// Pointer travel (px) before pressing a title bar or icon becomes a drag,
/// so a plain click never moves anything.
const DOCK_DRAG_THRESHOLD: f32 = 5.0;
/// Resolves once the dock hover delay has passed (a sleeping helper thread;
/// the web build has none and resolves at once).
fn hover_delay() -> impl std::future::Future<Output = ()> {
#[cfg(not(target_arch = "wasm32"))]
let rx = {
let (tx, rx) = iced::futures::channel::oneshot::channel::<()>();
std::thread::spawn(move || {
std::thread::sleep(crate::ui::dock::DOCK_HOVER_DELAY);
let _ = tx.send(());
});
rx
};
async move {
#[cfg(not(target_arch = "wasm32"))]
let _ = rx.await;
}
}
impl OpenCADStudio {
/// Dock chrome interaction (grab / pin / resize / hover / move) applied to
/// whichever panel the message names.
pub(crate) fn on_dock(&mut self, m: crate::ui::dock::DockMsg) -> iced::Task<Message> {
use crate::app::config::DockSide;
use crate::ui::dock::{DockDrag, DockMsg, FloatPanel};
match m {
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
// threshold.
if self.dock.show_group_of(id) {
self.save_config();
}
self.dock_begin(DockDrag::moving(id, None), Some(id));
}
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::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();
}
DockMsg::WidthReset(id) => {
// Only docked pallets have the divider that sends this.
if let Some((side, gi)) = self.dock.location(id) {
self.dock.reset_group_width(side, gi);
self.save_config();
}
}
DockMsg::TitleHover(id) => self.dock_title_hover = Some(id),
DockMsg::TitleHoverEnd(id) => {
if self.dock_title_hover == Some(id) {
self.dock_title_hover = None;
}
}
DockMsg::GripHover(side, gi) => self.dock_grip_hover = Some((side, gi)),
DockMsg::GripHoverEnd(side, gi) => {
if self.dock_grip_hover == Some((side, gi)) {
self.dock_grip_hover = None;
}
}
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 - strip - self.dock_column_width(DockSide::Right) - w - 60.0
}
_ => strip + self.dock_column_width(DockSide::Left) + 40.0,
};
self.dock_drag = None;
if self.dock.float(FloatPanel::new(id, 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(id, side, usize::MAX) {
self.save_config();
}
}
DockMsg::FloatMenu(id) => {
self.dock_float_menu = id;
self.dock_float_menu_at = self.dock_float_pointer;
if let Some(id) = id {
self.dock.raise_float(id);
}
}
DockMsg::FloatBarPointer(p) => self.dock_float_pointer = p,
DockMsg::FloatDockingToggle(id) => {
self.dock_float_menu = None;
if self.dock.toggle_float_docking(id) {
self.save_config();
}
}
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, 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.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;
}
self.dock_edge_menu = None;
let task = self.dock_set_open(id, true);
self.save_config();
return task;
}
DockMsg::AutoCollapseToggle(id) => {
// Docked: auto-hide for the whole edge; floating: this pallet.
if self.dock.toggle_auto_hide(id) {
self.dock_peek = None;
}
self.save_config();
}
DockMsg::Close(id) => return self.dock_set_open(id, false),
DockMsg::Hover(_)
| DockMsg::HoverEnd(_)
| DockMsg::HoverExit
| DockMsg::HoverStay
| DockMsg::HoverSettled(_) => return self.dock_hover(m),
DockMsg::DragMove(point) => self.dock_drag_move(point),
DockMsg::DragRelease => self.dock_drag_release(),
}
iced::Task::none()
}
/// Pointer entering / leaving strip icons, edge columns and floating
/// frames, and the delayed auto-hide reveal / hide that follows.
fn dock_hover(&mut self, m: crate::ui::dock::DockMsg) -> iced::Task<Message> {
use crate::ui::dock::{DockMsg, HoverPending};
match m {
DockMsg::Hover(id) => {
self.dock_icon_hover = Some(id);
// Only an auto-hiding edge (or floating pallet) reacts to
// hover: after a short rest 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 self.dock_drag.is_none() && self.dock.auto_hides(id) {
if self.dock_peek == Some(id) {
self.dock_hover_cancel();
} else {
return self.dock_hover_arm(HoverPending::Reveal(id));
}
}
}
DockMsg::HoverEnd(id) => {
if self.dock_icon_hover == Some(id) {
self.dock_icon_hover = None;
}
}
DockMsg::HoverExit => {
if self.dock_drag.is_none()
&& self.dock_peek.is_some_and(|id| self.dock.auto_hides(id))
{
return self.dock_hover_arm(HoverPending::Hide);
}
// Left before a pending reveal ran out: it never happens.
self.dock_hover_cancel();
}
DockMsg::HoverStay => {
if self.dock_hover_pending == Some(HoverPending::Hide) {
self.dock_hover_cancel();
}
}
DockMsg::HoverSettled(gen) => {
if gen == self.dock_hover_gen && self.dock_drag.is_none() {
match self.dock_hover_pending.take() {
Some(HoverPending::Reveal(id)) if self.dock.auto_hides(id) => {
self.dock_reveal(id)
}
Some(HoverPending::Hide)
if self.dock_peek.is_some_and(|id| self.dock.auto_hides(id)) =>
{
self.dock_peek = None;
}
_ => {}
}
}
}
_ => {}
}
iced::Task::none()
}
/// Pointer released: land what was dragged and save a changed layout.
fn dock_drag_release(&mut self) {
use crate::ui::dock::{DockDrag, DropTarget, FloatPanel};
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);
// 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,
..FloatPanel::new(panel, x, y, w, h)
})
}
};
}
Some(DockDrag::Move { .. } | DockDrag::LayerColumn) | None => {}
Some(DockDrag::Width(_) | DockDrag::Split { .. } | DockDrag::FloatSize { .. }) => {
changed = true;
}
}
if changed {
self.save_config();
}
self.dock_drag_last = None;
self.xref_col_drag = None;
self.xref_col_last = None;
self.xref_split_drag = false;
}
/// 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
};
let target = if started {
// 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 group only moves between group positions: it
// cannot join another group or float.
match (group, found) {
(Some(_), found @ DropTarget::Edge { .. }) | (None, found) => Some(found),
(Some(_), _) => None,
}
} else {
target
};
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 Some(weights) = self.dock.groups(side).get(group).map(|g| &g.weights) else {
return;
};
let (_, avail) = self.dock_workspace_size();
let total: f32 = self
.dock_slot_spans(side)
.iter()
.filter_map(|(i, _, _)| weights.get(*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 };
// The divider sizes the docked pallet's group.
if let Some((side, gi)) = self.dock.location(id) {
let dx = point.x - last.x;
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);
}
}
}
}
/// 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_edge_menu = None;
self.dock_float_menu = None;
if peek.is_some() {
self.dock_peek = peek;
}
}
/// Arm a delayed reveal or hide; it applies when its timer reports back
/// unless a newer hover superseded it.
fn dock_hover_arm(&mut self, target: crate::ui::dock::HoverPending) -> iced::Task<Message> {
self.dock_hover_gen = self.dock_hover_gen.wrapping_add(1);
self.dock_hover_pending = Some(target);
let gen = self.dock_hover_gen;
iced::Task::perform(hover_delay(), move |()| {
Message::Dock(crate::ui::dock::DockMsg::HoverSettled(gen))
})
}
/// Drop any pending delayed reveal / hide.
fn dock_hover_cancel(&mut self) {
self.dock_hover_gen = self.dock_hover_gen.wrapping_add(1);
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);
}
/// 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::{DockDrag, PanelId};
let mut task = iced::Task::none();
if open {
if !self.dock_panel_visible(id) {
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 => {
task = 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_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");
}
}
self.dock_unpeek(id);
// A closed pallet cannot stay held by the pointer.
let held = match self.dock_drag {
Some(DockDrag::Move { panel, .. } | DockDrag::Width(panel)) => panel == id,
_ => false,
};
if held {
self.dock_drag = None;
}
task
}
/// Show `id` where the layout has it, first docking it on the right edge
/// when it has no place yet (opened for the first time, or never moved).
pub(crate) fn dock_open_at_default(&mut self, id: crate::ui::dock::PanelId) {
if !self.dock.is_placed(id) {
self.dock
.dock(id, crate::app::config::DockSide::Right, usize::MAX);
}
self.dock_reveal(id);
}
/// Open the Layer Manager pallet (LAYERS). The first time it floats over
/// the middle of the drawing, where the dialog it replaced used to open;
/// after that it opens wherever the user left it.
pub(crate) fn open_layers_pallet(&mut self) {
use crate::ui::dock::{FloatPanel, PanelId};
self.sync_ribbon_layers();
self.show_layers = true;
if !self.dock.is_placed(PanelId::Layers) {
let (ww, wh) = self.dock_workspace_size();
let (w, h) = self.dock_float_size(PanelId::Layers);
let (x, y) = (((ww - w) * 0.5).max(0.0), ((wh - h) * 0.5).max(0.0));
self.dock
.float(FloatPanel::new(PanelId::Layers, x, y, w, h));
self.save_config();
}
self.dock.raise_float(PanelId::Layers);
self.dock_reveal(PanelId::Layers);
}
/// Stop revealing `id` (it closed).
pub(crate) fn dock_unpeek(&mut self, id: crate::ui::dock::PanelId) {
if self.dock_peek == Some(id) {
self.dock_peek = None;
}
}
/// 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.
pub(crate) fn dock_panel_visible(&self, id: crate::ui::dock::PanelId) -> bool {
use crate::ui::dock::PanelId;
if self.tabs[self.active_tab].is_start || self.clean_screen {
return false;
}
match id {
PanelId::Properties => self.show_properties,
PanelId::BlockPalette => self.show_block_palette,
PanelId::ExternalReferences => self.show_external_references,
PanelId::Browser => self.show_browser,
PanelId::NodeGraph => self.show_node_graph,
PanelId::PointCloudManager => self.pc_manager.show,
PanelId::Count => self.count_palette.show,
PanelId::SheetSetManager => self.sheet_set.show,
PanelId::Layers => self.show_layers,
}
}
/// 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,
)
}
/// The open pallets of group `gi` on `side`, top → bottom. Closed
/// pallets keep their place but take no space, so a group with none open
/// is not drawn at all.
pub(crate) fn dock_group_visible(
&self,
side: crate::app::config::DockSide,
gi: usize,
) -> Vec<crate::ui::dock::PanelId> {
// A stale index (a message from before the layout changed) reads as
// an empty group rather than a crash.
self.dock
.groups(side)
.get(gi)
.into_iter()
.flat_map(|g| g.panels.iter().copied())
.filter(|id| self.dock_panel_visible(*id))
.collect()
}
/// Indices of the groups on `side` with an open pallet (listed in the
/// icon strip).
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_visible(side, *gi).is_empty())
.collect()
}
/// The group `side` draws: the one it shows, or the first with an open
/// pallet when that one has none.
pub(crate) fn dock_shown_group(&self, side: crate::app::config::DockSide) -> Option<usize> {
let visible = self.dock_visible_groups(side);
let shown = self.dock.shown(side);
if visible.contains(&shown) {
Some(shown)
} else {
visible.first().copied()
}
}
/// Whether `side` draws its shown group beside the icon strip: always,
/// unless the edge auto-hides and none of the group's icons is hovered.
pub(crate) fn dock_edge_expanded(&self, side: crate::app::config::DockSide) -> bool {
let Some(gi) = self.dock_shown_group(side) else {
return false;
};
!self.dock.edge_auto_hide(side)
|| self
.dock_peek
.is_some_and(|e| self.dock_group_visible(side, gi).contains(&e))
}
/// 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.group_width_px(side, gi, self.win_size.0)
}
/// Width of the pallet column beside `side`'s icon strip: the shown
/// group's width, or nothing when it is hidden.
pub(crate) fn dock_column_width(&self, side: crate::app::config::DockSide) -> f32 {
match self.dock_shown_group(side) {
Some(gi) if self.dock_edge_expanded(side) => self.dock_group_width(side, gi),
_ => 0.0,
}
}
/// The icon strip of `side`: each listed group's place, and the top of
/// the + button.
pub(crate) fn dock_strip_layout(
&self,
side: crate::app::config::DockSide,
) -> (Vec<crate::ui::dock::StripGroup>, f32) {
let groups: Vec<(usize, usize)> = self
.dock_visible_groups(side)
.into_iter()
.map(|gi| (gi, self.dock_group_visible(side, gi).len()))
.collect();
crate::ui::dock::strip_layout(&groups)
}
/// The open pallets of `side`'s shown group, each as (position in the
/// group, top, bottom) in the workspace.
pub(crate) fn dock_slot_spans(
&self,
side: crate::app::config::DockSide,
) -> Vec<(usize, f32, f32)> {
match self.dock_shown_group(side) {
Some(gi) => self.dock_open_spans(&self.dock.groups(side)[gi], None),
None => Vec::new(),
}
}
/// The open pallets of `group` (plus `extra`, a pallet about to land
/// there), each as (position in the group, top, bottom) down the
/// workspace height.
pub(crate) fn dock_open_spans(
&self,
group: &crate::ui::dock::DockGroup,
extra: Option<crate::ui::dock::PanelId>,
) -> Vec<(usize, f32, f32)> {
let (_, avail) = self.dock_workspace_size();
let open: Vec<usize> = (0..group.panels.len())
.filter(|i| {
let id = group.panels[*i];
Some(id) == extra || self.dock_panel_visible(id)
})
.collect();
let weights: Vec<f32> = open.iter().map(|i| group.weights[*i]).collect();
open.into_iter()
.zip(crate::ui::dock::slot_spans(&weights, avail))
.map(|(i, (top, bottom))| (i, 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 a dragged pallet lands if released at workspace point `p`:
/// on an edge's icon strip it joins a group between its icons or starts a
/// new group; over the shown group's pallets it stacks above / below the
/// one under the pointer; on an empty edge it starts a group; anywhere
/// else it floats, held at `grab` from its top-left corner.
pub(crate) fn dock_drop_target(
&self,
p: iced::Point,
grab: iced::Vector,
) -> crate::ui::dock::DropTarget {
use crate::app::config::DockSide;
use crate::ui::dock::{DropTarget, StripHit, DOCK_EDGE_ZONE, DOCK_STRIP_W};
let (ww, _) = self.dock_workspace_size();
for side in [DockSide::Left, DockSide::Right] {
let listed = !self.dock_visible_groups(side).is_empty();
let from_edge = match side {
DockSide::Left => p.x,
DockSide::Right => ww - p.x,
};
if !listed {
if from_edge <= DOCK_EDGE_ZONE {
return DropTarget::Edge {
side,
index: self.dock.groups(side).len(),
};
}
continue;
}
// Over the icon strip.
if from_edge <= DOCK_STRIP_W {
let (layout, _) = self.dock_strip_layout(side);
return match crate::ui::dock::strip_hit(p.y, &layout) {
StripHit::NewGroup(at) => DropTarget::Edge {
side,
index: at.unwrap_or(self.dock.groups(side).len()),
},
StripHit::Join { group, index } => {
// Map the gap among open pallets to a position among
// all of the group's pallets.
let open = self.dock_group_visible(side, group);
let all = &self.dock.groups(side)[group].panels;
let index = open
.get(index)
.and_then(|t| all.iter().position(|a| a == t))
.unwrap_or(all.len());
DropTarget::Join { side, group, index }
}
};
}
// Over the shown group's pallets: above or below the one there.
if from_edge <= DOCK_STRIP_W + self.dock_column_width(side) {
let gi = self.dock_shown_group(side).expect("listed edge");
let spans = self.dock_slot_spans(side);
if let Some(&(pos, top, bottom)) = spans
.iter()
.find(|(_, _, bottom)| p.y < *bottom)
.or(spans.last())
{
let index = if p.y < (top + bottom) * 0.5 {
pos
} else {
pos + 1
};
return DropTarget::Join {
side,
group: gi,
index,
};
}
}
}
self.dock_float_target(p, grab)
}
/// Floating at workspace point `p`, held at `grab` from its top-left
/// corner (kept on the workspace).
pub(crate) fn dock_float_target(
&self,
p: iced::Point,
grab: iced::Vector,
) -> crate::ui::dock::DropTarget {
let (ww, wh) = self.dock_workspace_size();
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));
crate::ui::dock::DropTarget::Float { x, y }
}
}
+1183
View File
File diff suppressed because it is too large. Load diff
+41 -19
View File
@@ -14,6 +14,7 @@ pub(crate) mod commands;
pub(crate) mod dim_viewport;
#[cfg(test)]
mod dimension_preview_tests;
mod dock;
mod document;
mod drafting_settings;
pub(crate) mod expr_eval;
@@ -814,6 +815,8 @@ pub(super) struct OpenCADStudio {
render_mode_preview: Option<codec::entities::ViewportRenderMode>,
/// Whether the Properties panel is shown on the left (PROPERTIES).
show_properties: bool,
/// The Layer Manager pallet (LAYERS) is open.
pub(crate) show_layers: bool,
/// Docked Insert Block panel visibility.
pub(crate) show_block_palette: bool,
/// Node graph overlay over the viewport.
@@ -843,17 +846,33 @@ 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>,
/// Reveal or hide waiting out the hover delay, with the generation its
/// timer carries; any newer hover supersedes it.
pub(crate) dock_hover_pending: Option<crate::ui::dock::HoverPending>,
pub(crate) dock_hover_gen: u64,
/// 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 drag target (side + index), shown as a highlight while dragging.
pub(crate) dock_drag_target: Option<(crate::app::config::DockSide, usize)>,
/// 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>,
/// Floating pallet whose title-bar right-click menu is open.
pub(crate) dock_float_menu: Option<crate::ui::dock::PanelId>,
/// Pointer position over a floating title bar (bar-local), and where
/// its open menu was asked for.
pub(crate) dock_float_pointer: iced::Point,
pub(crate) dock_float_menu_at: iced::Point,
/// Group whose edge band (group grip) the pointer is over.
pub(crate) dock_grip_hover: Option<(crate::app::config::DockSide, usize)>,
/// 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>,
@@ -942,9 +961,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
@@ -1951,7 +1967,6 @@ pub enum ModalKind {
PluginManager,
UpdateNotice,
DonationPrompt,
Layers,
LayerStateManager,
LayerTranslator,
DrawingUnits,
@@ -4293,6 +4308,7 @@ impl OpenCADStudio {
render_mode_menu_open: false,
render_mode_preview: None,
show_properties: true,
show_layers: false,
show_block_palette: false,
show_node_graph: false,
property_target_override: None,
@@ -4308,11 +4324,18 @@ 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_hover_pending: None,
dock_hover_gen: 0,
dock_float_menu: None,
dock_float_pointer: iced::Point::ORIGIN,
dock_float_menu_at: iced::Point::ORIGIN,
dock_drag: None,
dock_drag_last: None,
dock_drag_target: None,
dock_title_hover: None,
dock_edge_menu: None,
dock_grip_hover: None,
dock_icon_hover: None,
xref_col_drag: None,
xref_col_last: None,
xref_split_drag: false,
@@ -4349,7 +4372,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 -5
View File
@@ -188,11 +188,7 @@ impl OpenCADStudio {
// The node library docks on the right on first use; after
// that it stays wherever the user moved it.
let id = crate::ui::dock::PanelId::NodeGraph;
if self.dock.location(id).is_none() {
self.dock
.dock(id, crate::app::config::DockSide::Right, usize::MAX);
}
self.dock_expanded = Some(id);
self.dock_open_at_default(id);
return self.graph_run(i);
}
}
+1 -1
View File
@@ -134,7 +134,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));
}
-317
View File
@@ -542,195 +542,6 @@ impl OpenCADStudio {
Ok(name)
}
/// Dock chrome interaction (grab / pin / resize / hover / move) applied to
/// 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};
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);
iced::Task::none()
}
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::WidthReset(id) => {
self.dock.reset_width(id);
self.save_config();
iced::Task::none()
}
DockMsg::AutoCollapseToggle(id) => {
let on = !self.dock.auto_collapse(id);
self.dock.set_auto_collapse(id, on);
self.dock_expanded = if on { None } else { Some(id) };
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);
}
}
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::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.
if self.dock_dragging.is_none() && self.dock_resizing.is_none() {
self.dock_expanded = Some(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_collapse(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 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));
} 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,
};
let cur = self.dock.settings(id).width + delta;
self.dock.set_width(id, cur);
}
}
if self.dock_dragging.is_some() || self.dock_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();
}
}
}
self.dock_dragging = None;
self.dock_resizing = None;
self.dock_drag_last = None;
self.dock_drag_target = None;
self.xref_col_drag = None;
self.xref_col_last = None;
self.xref_split_drag = false;
iced::Task::none()
}
}
}
/// 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.
pub(crate) fn dock_panel_visible(&self, id: crate::ui::dock::PanelId) -> bool {
use crate::ui::dock::PanelId;
if self.tabs[self.active_tab].is_start || self.clean_screen {
return false;
}
match id {
PanelId::Properties => self.show_properties,
PanelId::BlockPalette => self.show_block_palette,
PanelId::ExternalReferences => self.show_external_references,
PanelId::Browser => self.show_browser,
PanelId::NodeGraph => self.show_node_graph,
PanelId::PointCloudManager => self.pc_manager.show,
PanelId::Count => self.count_palette.show,
PanelId::SheetSetManager => self.sheet_set.show,
}
}
/// 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()
}
/// Rebuild the Reference Manager's entry list from the active drawing.
/// Uses the tab's session sets (unloaded + stat baseline) so CLI and
/// palette agree; writes fresh load-time mtimes back into the tab's stat
@@ -1605,134 +1416,6 @@ mod tests {
);
}
#[test]
fn blockpalette_pin_toggles_autocollapse_and_close_hides() {
let mut app = fresh();
app.show_block_palette = true;
let id = crate::ui::dock::PanelId::BlockPalette;
let _ = app.on_dock(crate::ui::dock::DockMsg::AutoCollapseToggle(id));
assert!(app.dock.auto_collapse(id), "pin enables auto-collapse");
let _ = app.on_dock(crate::ui::dock::DockMsg::AutoCollapseToggle(id));
assert!(
!app.dock.auto_collapse(id),
"second pin disables auto-collapse"
);
let _ = app.on_dock(crate::ui::dock::DockMsg::Close(id));
assert!(!app.show_block_palette, "close dismisses the sidebar");
}
#[test]
fn blockpalette_dock_moves_to_other_side_and_persists() {
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();
let id = crate::ui::dock::PanelId::BlockPalette;
assert_eq!(
app.dock.location(id),
Some((crate::app::config::DockSide::Right, 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!(
app.dock_drag_target,
Some((crate::app::config::DockSide::Left, 0))
);
let _ = app.on_dock(crate::ui::dock::DockMsg::DragRelease);
assert_eq!(
app.dock.location(id),
Some((crate::app::config::DockSide::Left, 0))
);
}
#[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);
// Reveal the block palette -> both count.
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);
}
#[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,
];
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))
);
}
#[test]
fn dock_drag_target_counts_all_visible_panels() {
let mut app = fresh();
app.dock = Default::default();
app.dock.left = vec![
crate::ui::dock::PanelId::BlockPalette,
crate::ui::dock::PanelId::Properties,
];
// 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))
);
}
#[test]
fn blockpalette_width_reset() {
let mut app = fresh();
let id = crate::ui::dock::PanelId::BlockPalette;
app.dock.set_width(id, 500.0);
let _ = app.on_dock(crate::ui::dock::DockMsg::WidthReset(id));
assert_eq!(app.dock.settings(id).width, 260.0);
}
#[test]
fn block_name_from_file_avoids_collisions() {
let mut app = fresh();
+17 -24
View File
@@ -2608,27 +2608,19 @@ impl OpenCADStudio {
Message::ScriptLine(line) => self.feed_script_line(&line),
Message::ToggleLayers => {
if self.active_modal == Some(super::ModalKind::Layers) {
self.ribbon.deactivate_tool_if("LAYERS");
self.active_modal = None;
self.reset_modal_geometry();
} else {
self.sync_ribbon_layers();
self.active_modal = Some(super::ModalKind::Layers);
if self.show_layers {
return self.on_dock(crate::ui::dock::DockMsg::Close(
crate::ui::dock::PanelId::Layers,
));
}
self.open_layers_pallet();
Task::none()
}
Message::ToggleXrefManager => {
self.show_external_references ^= true;
if self.show_external_references {
use crate::app::config::DockSide;
use crate::ui::dock::PanelId;
if self.dock.location(PanelId::ExternalReferences).is_none() {
self.dock
.dock(PanelId::ExternalReferences, DockSide::Right, usize::MAX);
}
self.dock_expanded = Some(PanelId::ExternalReferences);
self.dock_open_at_default(crate::ui::dock::PanelId::ExternalReferences);
self.refresh_xref_manager();
}
Task::none()
@@ -6362,8 +6354,11 @@ impl OpenCADStudio {
// Ctrl+A in the MText editor selects all of its text.
self.mtext_select_all();
Task::none()
} else if self.active_modal == Some(super::ModalKind::Layers) {
// Select every row in the Layer Manager (#236).
} else if self.dock_panel_visible(crate::ui::dock::PanelId::Layers)
&& self.tabs[i].layers.selected.is_some()
{
// Select every row in the Layer Manager (#236) while the
// user is working in it (a layer row is selected).
let n = self.tabs[i].layers.layers.len();
self.tabs[i].layers.selected_multi = (0..n).collect();
self.tabs[i].layers.selected = (n > 0).then_some(0);
@@ -9144,17 +9139,16 @@ impl OpenCADStudio {
Task::none()
}
Message::LayerNameColGrab => {
// Start a Name-column divider drag; rides ModalDragMove.
self.layer_col_dragging = true;
self.modal_drag_last = None;
// Start a Name-column divider drag; the dock's pointer capture
// feeds it (see `on_dock`).
self.dock_drag = Some(crate::ui::dock::DockDrag::LayerColumn);
self.dock_drag_last = None;
Task::none()
}
Message::ModalDragMove(p) => {
if let Some(last) = self.modal_drag_last {
let (dx, dy) = (p.x - last.x, p.y - last.y);
if self.layer_col_dragging {
self.layer_name_col_w = (self.layer_name_col_w + dx).clamp(60.0, 640.0);
} else if self.modal_resizing {
if self.modal_resizing {
// The grip sits bottom-right, so dragging out grows the
// box. The delta is added to each dialog's natural size,
// so clamp it at zero — dragging in past the natural size
@@ -9188,7 +9182,7 @@ impl OpenCADStudio {
}
}
}
if self.modal_dragging || self.modal_resizing || self.layer_col_dragging {
if self.modal_dragging || self.modal_resizing {
self.modal_drag_last = Some(p);
}
Task::none()
@@ -9196,7 +9190,6 @@ impl OpenCADStudio {
Message::ModalDragRelease => {
self.modal_dragging = false;
self.modal_resizing = false;
self.layer_col_dragging = false;
self.modal_drag_last = None;
Task::none()
}
+8 -432
View File
@@ -9,11 +9,11 @@ use crate::t;
use crate::ui::wrap_bar::DensitySwap;
use crate::ui::wrap_bar::WrapFlow;
use iced::widget::{
button, canvas, column, container, mouse_area, pane_grid, responsive, row, scrollable, shader,
stack, text, Row, Space,
button, column, container, mouse_area, pane_grid, responsive, row, scrollable, shader, stack,
text, Row, Space,
};
use iced::window;
use iced::{keyboard, Background, Border, Color, Element, Fill, Length, Subscription, Task, Theme};
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
use iced_aw::ContextMenu;
use std::sync::Arc;
@@ -1889,29 +1889,10 @@ bg={bg_ms:.1}ms n={view_count}"
viewport_stack = viewport_stack.push(tab.graph.view(sections));
}
// Docked side panels (Properties, block palette, future palettes) live
// in an ordered vertical stack on the left/right edge of the drawing
// view. Auto-collapsing (pinned) panels that aren't being hovered
// reduce to a rail sharing 1/N of the edge column's height; the
// hovered or unpinned panel expands to the full column height on top.
// Single source of truth with dock_visible_len (#57): one new panel
// updated in only one place used to break drag math vs rendering.
let visible_panel = |id: crate::ui::dock::PanelId| -> bool { self.dock_panel_visible(id) };
let edge_stack = |side: crate::app::config::DockSide| -> Option<Element<'_, Message>> {
let ids: Vec<crate::ui::dock::PanelId> = match side {
crate::app::config::DockSide::Left => self.dock.left.clone(),
crate::app::config::DockSide::Right => self.dock.right.clone(),
}
.into_iter()
.filter(|id| visible_panel(*id))
.collect();
if ids.is_empty() {
return None;
}
Some(self.build_edge_stack(side, &ids, tab))
};
let left_edge = edge_stack(crate::app::config::DockSide::Left);
let right_edge = edge_stack(crate::app::config::DockSide::Right);
// Docked pallets: each edge's icon strip and shown group (see
// `app::dock`).
let left_edge = self.build_edge_stack(crate::app::config::DockSide::Left, tab);
let right_edge = self.build_edge_stack(crate::app::config::DockSide::Right, tab);
// Command-line sits as a bottom-centre overlay on top of the
// viewport stack rather than as a separate row in the main
@@ -1957,158 +1938,7 @@ bg={bg_ms:.1}ms n={view_count}"
}
let workspace: Element<'_, Message> = row(parts).width(Fill).height(Fill).into();
let any_dragging = self.dock_dragging.is_some();
let any_resizing =
self.dock_resizing.is_some() || self.xref_col_drag.is_some() || self.xref_split_drag;
let workspace = if any_dragging {
let id = self.dock_dragging.expect("guarded by any_dragging");
let side = self.dock_drag_target.map(|(s, _)| s).unwrap_or(
self.dock
.location(id)
.map(|(s, _)| s)
.unwrap_or(crate::app::config::DockSide::Right),
);
// Dropping onto an edge joins that edge's stack, whose column is as
// wide as its widest currently-shown panel (the dragged panel
// included). Hidden (closed) panels don't render, so they can't
// widen the column being previewed.
let preview_width = {
let mut widths: Vec<f32> = match side {
crate::app::config::DockSide::Left => &self.dock.left,
crate::app::config::DockSide::Right => &self.dock.right,
}
.iter()
.copied()
.filter(|&pid| self.dock_panel_visible(pid))
.map(|pid| self.dock.width(pid, self.win_size.0))
.collect();
widths.push(self.dock.width(id, self.win_size.0));
widths.into_iter().fold(0.0, f32::max)
};
// Faint tint over the whole edge column so the target edge reads
// even before the panel-sized ghost snaps to a slot.
let edge_tint = container(Space::new())
.width(Length::Fixed(preview_width))
.height(Fill)
.style(|theme: &Theme| {
let palette = theme.palette();
container::Style {
background: Some(Background::Color(
palette.primary.weak.color.scale_alpha(0.35),
)),
..Default::default()
}
});
// The ghost is the dragged panel at its real size: its own saved
// width and its 1/N share of the edge (N = number of panels shown
// on the edge after the drop), with a header naming which panel is
// being moved. Only visibly-docked panels share the column height:
// a hidden (closed) panel keeps its stack slot but no screen space,
// so it must not split the preview.
let was_here = self.dock.location(id).map(|(s, _)| s) == Some(side);
let visible = self.dock_visible_len(side);
let final_count = if was_here {
std::cmp::max(visible, 1)
} else {
visible + 1
};
let edge_h = tab.scene.selection.borrow().view.vp_size.1;
let slot_h = edge_h / final_count as f32;
let index = self.dock_drag_target.map(|(_, i)| i).unwrap_or(0);
let slot = index.min(final_count.saturating_sub(1));
let ghost_top = slot as f32 * slot_h;
let ghost_header = container(text(id.title()).size(12))
.width(Fill)
.padding(iced::Padding {
top: 5.0,
right: 8.0,
bottom: 5.0,
left: 8.0,
})
.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()
}
});
let ghost_panel = container(column![ghost_header, Space::new()])
.width(Length::Fixed(self.dock.width(id, self.win_size.0)))
.height(Length::Fixed(slot_h))
.style(|theme: &Theme| {
let palette = theme.palette();
container::Style {
background: Some(Background::Color(palette.background.base.color)),
border: Border {
color: palette.primary.base.color,
width: 2.0,
radius: 0.0.into(),
},
..Default::default()
}
});
// Position the ghost at its drop slot, flush against the edge.
let ghost = container(ghost_panel)
.width(Fill)
.height(Fill)
.align_x(match side {
crate::app::config::DockSide::Left => iced::alignment::Horizontal::Left,
crate::app::config::DockSide::Right => iced::alignment::Horizontal::Right,
})
.align_y(iced::alignment::Vertical::Top)
.padding(iced::Padding {
top: ghost_top,
right: 0.0,
bottom: 0.0,
left: 0.0,
});
// Thin line across the column at the drop slot's top boundary.
let drop_line = container(
container(Space::new())
.width(Fill)
.height(Length::Fixed(3.0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().primary.base.color)),
..Default::default()
}),
)
.width(Fill)
.height(Fill)
.align_y(iced::alignment::Vertical::Top)
.padding(iced::Padding {
top: ghost_top,
right: 0.0,
bottom: 0.0,
left: 0.0,
});
let preview = iced::widget::stack![edge_tint, ghost, drop_line]
.width(Length::Fixed(preview_width))
.height(Fill);
let preview = container(preview)
.width(Fill)
.height(Fill)
.align_x(match side {
crate::app::config::DockSide::Left => iced::alignment::Horizontal::Left,
crate::app::config::DockSide::Right => iced::alignment::Horizontal::Right,
});
stack![workspace, preview].width(Fill).height(Fill).into()
} else {
workspace
};
let workspace: Element<'_, Message> = if any_dragging || any_resizing {
mouse_area(workspace)
.on_move(move |p| Message::Dock(crate::ui::dock::DockMsg::DragMove(p)))
.on_release(Message::Dock(crate::ui::dock::DockMsg::DragRelease))
.interaction(if any_resizing {
iced::mouse::Interaction::ResizingHorizontally
} else {
iced::mouse::Interaction::Grabbing
})
.into()
} else {
workspace
};
let workspace = self.dock_overlay(workspace, tab);
let command_line = self.command_line.view(
allow_autocomplete,
dyn_capturing,
@@ -2900,198 +2730,6 @@ impl OpenCADStudio {
pub(super) fn unfocus_widgets(&self) -> Task<Message> {
iced::advanced::widget::operate(iced::advanced::widget::operation::focusable::unfocus())
}
/// Build one edge: a narrow tab strip at the very edge plus the expanded
/// panels sliding out beside it. Auto-collapsing (pinned) panels are always
/// visible as equal-height 1/N tabs in the strip (via `Fill` distribution);
/// hovering a tab raises its panel to full column height beside the strip,
/// so switching between stacked panels is a direct tab-to-tab hover with no
/// width-jumping collapse dance. Unpinned panels are always expanded. The
/// whole edge is one hover region: leaving it collapses any auto-collapsing
/// panel, but moving between the tabs and the expanded panel does not.
fn build_edge_stack<'a>(
&'a self,
side: crate::app::config::DockSide,
ids: &[crate::ui::dock::PanelId],
tab: &'a DocumentTab,
) -> Element<'a, Message> {
let pinned: Vec<crate::ui::dock::PanelId> = ids
.iter()
.copied()
.filter(|id| self.dock.auto_collapse(*id))
.collect();
// Expanded = unpinned panels (always open) in stack order, then the
// hovered auto-collapsing one last so it floats on top.
let mut expanded: Vec<crate::ui::dock::PanelId> = ids
.iter()
.copied()
.filter(|id| !self.dock.auto_collapse(*id))
.collect();
if let Some(id) = self.dock_expanded {
if ids.contains(&id) && self.dock.auto_collapse(id) {
expanded.push(id);
}
}
// Each expanded panel renders at its own saved width; the column is as
// wide as the widest one currently showing.
let col_w = expanded
.iter()
.map(|id| self.dock.width(*id, self.win_size.0))
.fold(0.0, f32::max);
let tab_strip = (!pinned.is_empty()).then(|| {
let tabs: Vec<Element<'_, Message>> = pinned
.iter()
.map(|id| {
self.rail_slice(
*id,
side,
self.dock_expanded == Some(*id),
self.dock_dragging == Some(*id),
)
})
.collect();
column(tabs).width(DOCK_RAIL_W).height(Fill).into()
});
let expanded_stack = (!expanded.is_empty()).then(|| {
// Multiple simultaneously-expanded panels (e.g. two unpinned panels
// on the same edge) stack one below the other, each taking 1/N of
// the column height via equal `Fill` shares — never overlapping.
let layers: Vec<Element<'_, Message>> = expanded
.iter()
.map(|id| self.expanded_panel(*id, side, col_w, tab))
.collect();
column(layers).height(Fill).into()
});
let mut children: Vec<Element<'_, Message>> = Vec::new();
match side {
crate::app::config::DockSide::Left => {
if let Some(t) = tab_strip {
children.push(t);
}
if let Some(e) = expanded_stack {
children.push(e);
}
}
crate::app::config::DockSide::Right => {
if let Some(e) = expanded_stack {
children.push(e);
}
if let Some(t) = tab_strip {
children.push(t);
}
}
}
mouse_area(row(children).height(Fill))
.on_exit(Message::Dock(crate::ui::dock::DockMsg::HoverExit))
.into()
}
/// One narrow tab in the edge strip. Equal `Fill` heights across the tabs
/// give each 1/N of the strip; hovering or clicking it raises its panel.
fn rail_slice(
&self,
id: crate::ui::dock::PanelId,
side: crate::app::config::DockSide,
is_active: bool,
is_dragging: bool,
) -> Element<'_, Message> {
let label = canvas(VBarLabel {
text: id.title().to_string(),
clockwise: side == crate::app::config::DockSide::Left,
})
.width(Fill)
.height(Fill);
let bg = move |theme: &Theme| {
let palette = theme.palette();
if is_active {
palette.primary.weak.color
} else if is_dragging {
palette.primary.weak.color.scale_alpha(0.55)
} else {
palette.background.base.color
}
};
let tab = container(label)
.width(Length::Fixed(DOCK_RAIL_W))
.height(Fill)
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(bg(theme))),
border: Border {
color: if is_active {
theme.palette().primary.base.color
} else {
theme.palette().background.neutral.color
},
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
});
mouse_area(tab)
.interaction(iced::mouse::Interaction::Pointer)
.on_press(Message::Dock(crate::ui::dock::DockMsg::DockGrab(id)))
.on_enter(Message::Dock(crate::ui::dock::DockMsg::Hover(id)))
.into()
}
/// A panel expanded to the full edge column: the panel body plus a
/// grabbable divider against the viewport. Hovering is handled by the
/// enclosing edge region (see `build_edge_stack`), so the body stays fully
/// interactive without fighting the region's hover tracking.
fn expanded_panel<'a>(
&'a self,
id: crate::ui::dock::PanelId,
side: crate::app::config::DockSide,
width: f32,
tab: &'a DocumentTab,
) -> Element<'a, Message> {
let auto_collapse = self.dock.auto_collapse(id);
let panel: Element<'_, Message> = match id {
crate::ui::dock::PanelId::Properties => tab.properties.view(width, auto_collapse),
crate::ui::dock::PanelId::BlockPalette => {
crate::ui::window::block_palette::view(&self.block_palette, width, auto_collapse)
}
crate::ui::dock::PanelId::ExternalReferences => {
self.xref_manager
.view(width, auto_collapse, tab.xref_missing, &tab.scene.document)
}
crate::ui::dock::PanelId::Browser => crate::ui::window::browser::view(
&tab.scene.document,
tab.sketch_session
.as_ref()
.map(|session| session.name.as_str()),
width,
auto_collapse,
),
crate::ui::dock::PanelId::NodeGraph => tab.graph.panel(width, auto_collapse),
crate::ui::dock::PanelId::Count => crate::ui::window::count_palette::view(
&self.count_palette,
tab.count.as_ref(),
&tab.scene.document,
tab.id,
tab.scene.geometry_epoch,
width,
auto_collapse,
),
crate::ui::dock::PanelId::PointCloudManager => crate::ui::window::pc_manager::view(
&self.pc_manager,
&tab.scene.document,
&tab.scene.selected_handles_in_order(),
width,
auto_collapse,
),
crate::ui::dock::PanelId::SheetSetManager => {
crate::ui::window::sheet_set::view(&self.sheet_set, width, auto_collapse)
}
};
let divider = dock_divider(id);
match side {
crate::app::config::DockSide::Left => row![panel, divider].height(Fill).into(),
crate::app::config::DockSide::Right => row![divider, panel].height(Fill).into(),
}
}
}
// ── Document tab bar ───────────────────────────────────────────────────────
@@ -3420,68 +3058,6 @@ fn pane_mouse_area<'a>(idx: usize) -> Element<'a, Message> {
.into()
}
/// Canvas that draws a label rotated 90° (for a collapsed panel's bar).
struct VBarLabel {
text: String,
clockwise: bool,
}
impl canvas::Program<Message> for VBarLabel {
type State = ();
fn draw(
&self,
_state: &(),
renderer: &iced::Renderer,
theme: &Theme,
bounds: iced::Rectangle,
_cursor: iced::advanced::mouse::Cursor,
) -> Vec<canvas::Geometry> {
let mut frame = canvas::Frame::new(renderer, bounds.size());
frame.with_save(|frame| {
frame.translate(iced::Vector::new(bounds.width / 2.0, bounds.height / 2.0));
frame.rotate(iced::Radians(if self.clockwise {
std::f32::consts::FRAC_PI_2
} else {
-std::f32::consts::FRAC_PI_2
}));
frame.fill_text(canvas::Text {
content: self.text.clone(),
position: iced::Point::ORIGIN,
color: theme.palette().background.base.text.scale_alpha(0.72),
size: iced::Pixels(13.0),
align_x: iced::advanced::text::Alignment::Center,
align_y: iced::alignment::Vertical::Center,
shaping: iced::advanced::text::Shaping::Advanced,
..Default::default()
});
});
vec![frame.into_geometry()]
}
}
/// Width of a collapsed (auto-collapsing) panel's tab in the edge strip.
const DOCK_RAIL_W: f32 = 28.0;
/// Grabbable separator for a docked panel managed by the general dock. Same
/// visual as the previous per-panel divider but emits generic dock messages.
fn dock_divider(id: crate::ui::dock::PanelId) -> Element<'static, Message> {
let line = container(Space::new())
.width(Length::Fixed(5.0))
.height(Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
});
let grab = Message::Dock(crate::ui::dock::DockMsg::ResizeGrab(id));
let reset = Message::Dock(crate::ui::dock::DockMsg::WidthReset(id));
mouse_area(line)
.on_press(grab)
.on_double_click(reset)
.interaction(iced::mouse::Interaction::ResizingHorizontally)
.into()
}
const START_ACTION_RADIUS: f32 = 6.0;
fn start_action_shape(mut style: button::Style) -> button::Style {
-7
View File
@@ -19,7 +19,6 @@ impl OpenCADStudio {
Some(K::PluginManager) => crate::tr!("modal", "plugin-manager"),
Some(K::UpdateNotice) => crate::tr!("modal", "update-available"),
Some(K::DonationPrompt) => crate::tr!("donation", "title"),
Some(K::Layers) => crate::tr!("modal", "layer-manager"),
Some(K::LayerStateManager) => crate::tr!("modal", "layer-state-manager"),
Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(),
Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(),
@@ -465,12 +464,6 @@ impl OpenCADStudio {
crate::ui::window::update_notice::view_window(latest, body, flow)
})
}
super::super::ModalKind::Layers => {
let tab = &self.tabs[self.active_tab];
sized_flow(ex, 900, 360, |flow| {
tab.layers.view_window(self.layer_name_col_w, flow)
})
}
super::super::ModalKind::LayerTranslator => {
use crate::modules::draw::layers::laytrans;
let i = self.active_tab;
+7
View File
@@ -5765,6 +5765,13 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Invalid count area boundary object. Select a closed polyline consisting of line segments and does not intersect itself." => Some(("count-feature", "invalid-boundary")),
"Block \"{part}\" cannot be found." => Some(("count-feature", "block-not-found")),
"Invalid point." => Some(("count-feature", "invalid-point")),
"Auto-hide" => Some(("dock", "auto-hide")),
"Allow docking" => Some(("dock", "allow-docking")),
"Dock left" => Some(("dock", "dock-left")),
"Dock right" => Some(("dock", "dock-right")),
"New group" => Some(("dock", "new-group")),
"Browser" => Some(("dock", "browser")),
"Node Graph" => Some(("dock", "node-graph")),
_ => None,
}
}
+4 -2
View File
@@ -1,4 +1,4 @@
// Layers panel toggle — ribbon definition.
// Layer Manager pallet — ribbon definition.
use crate::modules::{IconKind, ModuleEvent, ToolDef};
@@ -7,6 +7,8 @@ pub fn tool() -> ToolDef {
id: "LAYERS",
label: "Layers",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/layers/panel.svg")),
event: ModuleEvent::ToggleLayers,
// A command like the Blocks palette's: it opens the pallet, and the
// ribbon drops the button's highlight once it has run.
event: ModuleEvent::Command("LAYERS".to_string()),
}
}
+1211 -181
View File
File diff suppressed because it is too large. Load diff
+4
View File
@@ -72,7 +72,11 @@ pub static COPY: &[u8] = include_bytes!("../../assets/icons/ui/copy.svg");
pub static MENU: &[u8] = include_bytes!("../../assets/icons/ui/menu.svg");
pub static MOVE: &[u8] = include_bytes!("../../assets/icons/ui/move.svg");
pub static RESIZE: &[u8] = include_bytes!("../../assets/icons/ui/resize.svg");
/// `RESIZE` mirrored for a bottom-left corner grip.
pub static RESIZE_LEFT: &[u8] = include_bytes!("../../assets/icons/ui/resize_left.svg");
pub static PIN: &[u8] = include_bytes!("../../assets/icons/ui/pin.svg");
/// `PIN` tilted 30°: the pin while auto-hide is on.
pub static PIN_ACTIVE: &[u8] = include_bytes!("../../assets/icons/ui/pin_active.svg");
pub static SPLIT_V: &[u8] = include_bytes!("../../assets/icons/ui/split_v.svg");
pub static SPLIT_H: &[u8] = include_bytes!("../../assets/icons/ui/split_h.svg");
pub static GRID: &[u8] = include_bytes!("../../assets/icons/ui/grid.svg");
+3 -11
View File
@@ -658,7 +658,7 @@ impl Graph {
/// The node library as a docked panel: title bar, a search box with the
/// graph's New / Open / Save beside it, then the categories. Entries are
/// clicked or dragged onto the canvas.
pub fn panel(&self, width: f32, auto_collapse: bool) -> Element<'_, Message> {
pub fn panel(&self, width: f32, chrome: crate::ui::dock::Chrome) -> Element<'_, Message> {
use crate::ui::dock::{self, PanelId};
let tool = |icon: &'static [u8], tip: &str, message: GraphMsg| {
dock::tool_button(
@@ -740,16 +740,8 @@ impl Graph {
.height(Length::Fill);
dock::frame(
column![
dock::title_bar(
PanelId::NodeGraph,
t!("Node Graph").into_owned(),
auto_collapse
),
toolbar,
body
]
.spacing(6),
dock::title_bar(PanelId::NodeGraph, t!("Node Graph").into_owned(), chrome),
column![toolbar, body].spacing(6),
width,
)
}
+3 -6
View File
@@ -544,14 +544,11 @@ impl PropertiesPanel {
.unwrap_or_default()
}
pub fn view(&self, width: f32, auto_collapse: bool) -> Element<'_, Message> {
pub fn view(&self, width: f32, chrome: crate::ui::dock::Chrome) -> Element<'_, Message> {
use crate::ui::dock::PanelId;
// ── Header ──────────────────────────────────────────────────────────
let header = crate::ui::dock::title_bar(
PanelId::Properties,
t!("Properties").into_owned(),
auto_collapse,
);
let header =
crate::ui::dock::title_bar(PanelId::Properties, t!("Properties").into_owned(), chrome);
// ── Title bar (entity type / "No Selection") ─────────────────────
let title_content: Element<'_, Message> = if self.selection_groups.is_empty() {
+8 -3
View File
@@ -800,11 +800,15 @@ fn options_card<'a>(palette: &'a BlockPalette) -> Element<'a, Message> {
}
/// Build the docked panel element from the palette state.
pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element<'_, Message> {
pub fn view(
palette: &BlockPalette,
width: f32,
chrome: crate::ui::dock::Chrome,
) -> Element<'_, Message> {
let title_bar = crate::ui::dock::title_bar(
PanelId::BlockPalette,
crate::t!("Blocks").into_owned(),
auto_collapse,
chrome,
);
let tabs = {
@@ -991,7 +995,8 @@ pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element<
};
crate::ui::dock::frame(
column![title_bar, top, body, options_card(palette)].spacing(6),
title_bar,
column![top, body, options_card(palette)].spacing(6),
width,
)
}
+4 -7
View File
@@ -100,13 +100,10 @@ pub fn view<'a>(
document: &'a CadDocument,
open_sketch: Option<&'a str>,
width: f32,
auto_collapse: bool,
chrome: crate::ui::dock::Chrome,
) -> Element<'a, Message> {
let title_bar = crate::ui::dock::title_bar(
PanelId::Browser,
crate::t!("Browser").into_owned(),
auto_collapse,
);
let title_bar =
crate::ui::dock::title_bar(PanelId::Browser, crate::t!("Browser").into_owned(), chrome);
let mut tree = column![].spacing(1);
@@ -151,5 +148,5 @@ pub fn view<'a>(
.width(Fill)
.height(Fill);
crate::ui::dock::frame(column![title_bar, body].spacing(6), width)
crate::ui::dock::frame(title_bar, column![body].spacing(6), width)
}
+4 -7
View File
@@ -660,13 +660,10 @@ pub fn view<'a>(
doc_id: u64,
epoch: u64,
width: f32,
auto_collapse: bool,
chrome: crate::ui::dock::Chrome,
) -> Element<'a, Message> {
let title_bar = crate::ui::dock::title_bar(
PanelId::Count,
crate::t!("Count").into_owned(),
auto_collapse,
);
let title_bar =
crate::ui::dock::title_bar(PanelId::Count, crate::t!("Count").into_owned(), chrome);
let body = match mode {
Some(mode) if mode.target.is_some() => mode_view(palette, mode, doc),
_ => {
@@ -685,7 +682,7 @@ pub fn view<'a>(
list_view(palette, &instances)
}
};
crate::ui::dock::frame(column![title_bar, body].spacing(6), width)
crate::ui::dock::frame(title_bar, column![body].spacing(6), width)
}
fn bar_button<'a>(
+53 -23
View File
@@ -337,20 +337,40 @@ impl LayerPanel {
.collect()
}
/// Render the layer panel as the full content of its own OS window.
pub fn view_window(
/// The Layer Manager as a dock pallet: its title bar, the toolbar across
/// the pallet (search box on the right), then the table, at least as wide
/// as the pallet and scrolling sideways when the pallet is narrower.
pub fn view_panel(
&self,
name_col_w: f32,
sizing: crate::ui::modal::ModalSizing,
width: f32,
chrome: crate::ui::dock::Chrome,
) -> Element<'_, Message> {
self.view_content(name_col_w, sizing)
use crate::ui::dock::PanelId;
let title_bar = crate::ui::dock::title_bar(
PanelId::Layers,
crate::tr!("modal", "layer-manager"),
chrome,
);
// Inside the pallet's 1 px border on either side.
let body = self.view_content(name_col_w, (width - 2.0).max(0.0));
container(column![title_bar, body])
.width(Length::Fixed(width))
.height(Length::Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.base.color)),
border: Border {
color: theme.palette().background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
})
.into()
}
fn view_content(
&self,
name_col_w: f32,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'_, Message> {
/// Toolbar and table for a pallet `inner_w` wide.
fn view_content(&self, name_col_w: f32, inner_w: f32) -> Element<'_, Message> {
let has_sel = self.selected.is_some();
let sel_is_zero = self
.selected
@@ -377,7 +397,7 @@ impl LayerPanel {
Message::LayerSetCurrent,
can_set_current,
),
iced::widget::Space::new().width(sizing.width),
iced::widget::Space::new().width(Length::Fill),
// Search box: filters rows by name as the user types (#343).
text_input(t!("Search…").as_ref(), &self.filter)
.on_input(Message::LayerManagerFilterChanged)
@@ -393,7 +413,7 @@ impl LayerPanel {
background: Some(Background::Color(theme.palette().background.weak.color)),
..Default::default()
})
.width(sizing.width)
.width(Length::Fill)
.padding([4, 8]);
// ── Column header ─────────────────────────────────────────────────
@@ -473,7 +493,6 @@ impl LayerPanel {
),
]
.spacing(4)
.width(sizing.width)
.align_y(iced::Center);
for vp in &self.vp_cols {
@@ -499,7 +518,7 @@ impl LayerPanel {
}
})
.padding([4, 8])
.width(sizing.width);
.width(Length::Fill);
// ── Layer rows ────────────────────────────────────────────────────
let mut rows_col = column![].spacing(0);
@@ -536,17 +555,28 @@ impl LayerPanel {
let table = scrollable(rows_col)
.id(iced::advanced::widget::Id::new(LAYER_TABLE_SCROLL_ID))
.height(sizing.height.min(240.0));
.width(Length::Fill)
.height(Length::Fill);
// ── Full-window frame ─────────────────────────────────────────────
container(column![toolbar, col_header, table].spacing(0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.base.color)),
..Default::default()
})
.width(sizing.width)
.height(sizing.height)
.into()
// The header and table stretch to the wider of their columns and the
// pallet (the zero-height spacer), so the header's grey band and
// borders always run to the pallet's edge; a narrower pallet scrolls
// the table sideways while the toolbar stays put.
let grid = column![
col_header,
table,
iced::widget::Space::new()
.width(Length::Fixed(inner_w))
.height(0),
]
.height(Length::Fill);
let grid = scrollable(grid)
.direction(iced::widget::scrollable::Direction::Horizontal(
iced::widget::scrollable::Scrollbar::new(),
))
.width(Length::Fill)
.height(Length::Fill);
column![toolbar, grid].height(Length::Fill).into()
}
}
+3 -3
View File
@@ -277,12 +277,12 @@ pub fn view<'a>(
document: &'a CadDocument,
selected: &[Handle],
width: f32,
auto_collapse: bool,
chrome: crate::ui::dock::Chrome,
) -> Element<'a, Message> {
let title_bar = crate::ui::dock::title_bar(
PanelId::PointCloudManager,
crate::t!("Point Cloud Manager").into_owned(),
auto_collapse,
chrome,
);
let query = state.search.trim().to_lowercase();
let matches = |label: &str| query.is_empty() || label.to_lowercase().contains(&query);
@@ -416,5 +416,5 @@ pub fn view<'a>(
.padding([4, 8])
.size(12);
crate::ui::dock::frame(column![title_bar, card, search].spacing(6), width)
crate::ui::dock::frame(title_bar, column![card, search].spacing(6), width)
}
+4 -3
View File
@@ -1153,12 +1153,12 @@ fn heading(label: &str) -> Element<'static, Message> {
pub fn view<'a>(
state: &'a SheetSetManager,
width: f32,
auto_collapse: bool,
chrome: crate::ui::dock::Chrome,
) -> Element<'a, Message> {
let title_bar = crate::ui::dock::title_bar(
PanelId::SheetSetManager,
t!("Sheet Set Manager").into_owned(),
auto_collapse,
chrome,
);
let mut picks: Vec<Pick> = state
.sets
@@ -1305,7 +1305,8 @@ pub fn view<'a>(
.size(10)
.style(muted_style);
crate::ui::dock::frame(
column![title_bar, combo, tabs, card, hint].spacing(6),
title_bar,
column![combo, tabs, card, hint].spacing(6),
width,
)
}
+2 -2
View File
@@ -590,7 +590,7 @@ impl XrefManagerPanel {
pub fn view<'a>(
&'a self,
width: f32,
auto_collapse: bool,
chrome: crate::ui::dock::Chrome,
missing: usize,
doc: &'a CadDocument,
) -> Element<'a, Message> {
@@ -602,7 +602,7 @@ impl XrefManagerPanel {
crate::t!("External References").as_ref(),
self.entries.len()
),
auto_collapse,
chrome,
);
// Table content width: column widths plus gutters, stretched to the
// dock when wider so rows fill the panel; narrower docks sidescroll.