forked from 1b-agent/opencadstudio-upstream
Dock: Photoshop-style vertical icon strip for docked groups
Each dock edge now has a vertical icon strip along the window edge in place of the horizontal tab row and the auto-hide rail. - Every shown group gets a grip bar with its pallets' icons below it; the shown pallet's icon has a grey background. Clicking an icon shows that pallet in its group; hovering the icons of an auto-hiding group opens it. - Dragging an icon within the strip reorders it or moves it into another group; dropping it on a grip or below the last group makes it a new group there. On the pallet area, the top / bottom bands still start a new group above / below and the middle joins the group. - Dragging a grip moves the whole group to another position or edge. - A + button under the groups opens the pallet menu (icon, name, check mark). Opening a pallet from it docks it on that edge as a new group, or in its existing group there; a checked pallet is hidden. - Groups keep their width when moved; the divider against the viewport sizes the whole edge column. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013RyGiMXcsf9HTKfqDxaQGt
This commit is contained in:
4 files changed
+455
-319
No files matched your search
+7
-3
@@ -870,8 +870,11 @@ pub(super) struct OpenCADStudio {
|
||||
pub(crate) dock_float_resizing: Option<(crate::ui::dock::PanelId, bool)>,
|
||||
/// Docked panel whose title bar the pointer is over (shows pin / close).
|
||||
pub(crate) dock_title_hover: Option<crate::ui::dock::PanelId>,
|
||||
/// Slot (side, index) whose pallet menu is open.
|
||||
pub(crate) dock_tab_menu: Option<(crate::app::config::DockSide, usize)>,
|
||||
/// Edge whose icon-strip pallet menu (+) is open.
|
||||
pub(crate) dock_edge_menu: Option<crate::app::config::DockSide>,
|
||||
/// Group being dragged by its strip grip (side, slot index). While set,
|
||||
/// `dock_dragging` holds the group's shown panel for the preview.
|
||||
pub(crate) dock_dragging_group: Option<(crate::app::config::DockSide, usize)>,
|
||||
/// 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>,
|
||||
@@ -4312,7 +4315,8 @@ impl OpenCADStudio {
|
||||
dock_split_drag: None,
|
||||
dock_float_resizing: None,
|
||||
dock_title_hover: None,
|
||||
dock_tab_menu: None,
|
||||
dock_edge_menu: None,
|
||||
dock_dragging_group: None,
|
||||
xref_col_drag: None,
|
||||
xref_col_last: None,
|
||||
xref_split_drag: false,
|
||||
|
||||
+186
-91
@@ -624,33 +624,33 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
}
|
||||
iced::Task::none()
|
||||
}
|
||||
DockMsg::TabMenu(slot) => {
|
||||
self.dock_tab_menu = slot;
|
||||
DockMsg::EdgeMenu(side) => {
|
||||
self.dock_edge_menu = side;
|
||||
iced::Task::none()
|
||||
}
|
||||
DockMsg::TabMenuToggle(side, gi, id) => {
|
||||
if gi >= self.dock.groups(side).len() {
|
||||
self.dock_tab_menu = None;
|
||||
return iced::Task::none();
|
||||
}
|
||||
if self.dock_group_visible_tabs(side, gi).contains(&id) {
|
||||
// Unchecking hides the pallet; it keeps its tab, so it
|
||||
// comes back here when opened again.
|
||||
DockMsg::EdgeMenuToggle(side, id) => {
|
||||
if self.dock_panel_visible(id) {
|
||||
let task = self.on_dock(DockMsg::Close(id));
|
||||
if self.dock_group_shown(side, gi).is_none() {
|
||||
self.dock_tab_menu = None;
|
||||
}
|
||||
self.save_config();
|
||||
return task;
|
||||
}
|
||||
self.dock.add_tab(id, side, gi, None);
|
||||
// Joining may have removed an earlier slot on this side.
|
||||
self.dock_tab_menu = self.dock.location(id);
|
||||
// A pallet opened from an edge's menu lands on that edge: in
|
||||
// its own group there if it has one, else as a new group.
|
||||
if self.dock.location(id).map(|(s, _)| s) != Some(side) {
|
||||
self.dock.dock(id, side, usize::MAX);
|
||||
}
|
||||
let task = self.dock_open_panel(id);
|
||||
self.dock.select_tab(id);
|
||||
self.save_config();
|
||||
task
|
||||
}
|
||||
DockMsg::GroupGrab(side, gi) => {
|
||||
if let Some(id) = self.dock_group_shown(side, gi) {
|
||||
self.dock_begin_drag(id);
|
||||
self.dock_dragging_group = Some((side, gi));
|
||||
}
|
||||
iced::Task::none()
|
||||
}
|
||||
DockMsg::FloatRaise(id) => {
|
||||
self.dock.raise_float(id);
|
||||
iced::Task::none()
|
||||
@@ -754,7 +754,13 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
};
|
||||
}
|
||||
if self.dock_drag_target.is_some() || moved >= DOCK_DRAG_THRESHOLD {
|
||||
self.dock_drag_target = Some(self.dock_drop_target(id, point));
|
||||
let target = self.dock_drop_target(id, point);
|
||||
// A whole group only moves between slots: it cannot
|
||||
// join another group or float.
|
||||
self.dock_drag_target = match (self.dock_dragging_group, target) {
|
||||
(Some(_), DropTarget::Edge { .. }) | (None, _) => Some(target),
|
||||
(Some(_), _) => None,
|
||||
};
|
||||
}
|
||||
} else if let Some((side, upper, lower)) = self.dock_split_drag {
|
||||
if let Some(last) = self.dock_drag_last {
|
||||
@@ -797,12 +803,22 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
Some((DockSide::Left, _)) => dx,
|
||||
_ => -dx,
|
||||
};
|
||||
// Every tab of the slot shares one width.
|
||||
let cur = match self.dock.location(id) {
|
||||
Some((side, gi)) => self.dock.group_width(side, gi),
|
||||
None => self.dock.settings(id).width,
|
||||
} + delta;
|
||||
self.dock.set_group_width(id, cur);
|
||||
// The divider sizes the whole edge column: every
|
||||
// shown group on the edge takes the new width (and
|
||||
// keeps it if it moves elsewhere later).
|
||||
match self.dock.location(id) {
|
||||
Some((side, _)) => {
|
||||
let cur = self.dock_column_width(side) + delta;
|
||||
for gi in self.dock_visible_groups(side) {
|
||||
let shown = self.dock.groups(side)[gi].active;
|
||||
self.dock.set_group_width(shown, cur);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let cur = self.dock.settings(id).width + delta;
|
||||
self.dock.set_group_width(id, cur);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.dock_dragging.is_some()
|
||||
@@ -819,7 +835,11 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
let mut changed = self.dock_resizing.is_some()
|
||||
|| self.dock_split_drag.is_some()
|
||||
|| self.dock_float_resizing.is_some();
|
||||
if let (Some(id), Some(target)) = (self.dock_dragging, self.dock_drag_target) {
|
||||
if let (Some((side, gi)), Some(DropTarget::Edge { side: to, index })) =
|
||||
(self.dock_dragging_group, self.dock_drag_target)
|
||||
{
|
||||
changed |= self.dock.move_group(side, gi, to, index);
|
||||
} else if let (Some(id), Some(target)) = (self.dock_dragging, self.dock_drag_target) {
|
||||
let moved = match target {
|
||||
DropTarget::Edge { side, index } => self.dock.dock(id, side, index),
|
||||
DropTarget::Tab { side, group, index } => {
|
||||
@@ -839,6 +859,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
self.save_config();
|
||||
}
|
||||
self.dock_dragging = None;
|
||||
self.dock_dragging_group = None;
|
||||
self.dock_resizing = None;
|
||||
self.dock_split_drag = None;
|
||||
self.dock_float_resizing = None;
|
||||
@@ -892,6 +913,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
fn dock_begin_drag(&mut self, id: crate::ui::dock::PanelId) {
|
||||
self.dock.raise_float(id);
|
||||
self.dock_dragging = Some(id);
|
||||
self.dock_dragging_group = None;
|
||||
self.dock_edge_menu = None;
|
||||
self.dock_resizing = None;
|
||||
self.dock_split_drag = None;
|
||||
self.dock_float_resizing = None;
|
||||
@@ -1000,24 +1023,18 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
.fold(0.0, f32::max)
|
||||
}
|
||||
|
||||
/// Left x of the expanded slots on `side` (past the auto-hide rail, if
|
||||
/// the edge has one) in workspace pixels.
|
||||
pub(crate) fn dock_column_x0(&self, side: crate::app::config::DockSide) -> f32 {
|
||||
use crate::app::config::DockSide;
|
||||
let has_rail = self.dock_visible_groups(side).into_iter().any(|gi| {
|
||||
self.dock_group_shown(side, gi)
|
||||
.is_some_and(|id| self.dock.auto_collapse(id))
|
||||
});
|
||||
let rail = if has_rail { crate::ui::dock::DOCK_RAIL_W } else { 0.0 };
|
||||
match side {
|
||||
DockSide::Left => rail,
|
||||
DockSide::Right => {
|
||||
self.dock_workspace_size().0
|
||||
- rail
|
||||
- self.dock_column_width(side)
|
||||
- crate::ui::dock::DOCK_DIVIDER_W
|
||||
}
|
||||
}
|
||||
/// The icon strip of `side`: each shown 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_tabs(side, gi).len()))
|
||||
.collect();
|
||||
crate::ui::dock::strip_layout(&groups)
|
||||
}
|
||||
|
||||
/// Visible slots on `side` with their vertical span in the workspace.
|
||||
@@ -1063,7 +1080,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
use crate::ui::dock::{slot_zone, DropTarget, SlotZone, DOCK_EDGE_ZONE};
|
||||
let (ww, wh) = self.dock_workspace_size();
|
||||
for side in [DockSide::Left, DockSide::Right] {
|
||||
let zone = self.dock_column_width(side).max(DOCK_EDGE_ZONE);
|
||||
let zone = (self.dock_column_width(side) + crate::ui::dock::DOCK_STRIP_W)
|
||||
.max(DOCK_EDGE_ZONE);
|
||||
let inside = match side {
|
||||
DockSide::Left => p.x <= zone,
|
||||
DockSide::Right => p.x >= ww - zone,
|
||||
@@ -1071,6 +1089,37 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
if !inside {
|
||||
continue;
|
||||
}
|
||||
// Over the icon strip: join a group between its icons, or start
|
||||
// a new group at a grip / below the last group.
|
||||
let on_strip = !self.dock_visible_groups(side).is_empty()
|
||||
&& match side {
|
||||
DockSide::Left => p.x <= crate::ui::dock::DOCK_STRIP_W,
|
||||
DockSide::Right => p.x >= ww - crate::ui::dock::DOCK_STRIP_W,
|
||||
};
|
||||
if on_strip {
|
||||
let (layout, _) = self.dock_strip_layout(side);
|
||||
return match crate::ui::dock::strip_hit(p.y, &layout) {
|
||||
crate::ui::dock::StripHit::NewGroup(at) => DropTarget::Edge {
|
||||
side,
|
||||
index: at.unwrap_or(self.dock.groups(side).len()),
|
||||
},
|
||||
crate::ui::dock::StripHit::Tab { group, index } => {
|
||||
// Map the gap among visible icons to a position in
|
||||
// all of the group's tabs.
|
||||
let tabs = self.dock_group_visible_tabs(side, group);
|
||||
let all = &self.dock.groups(side)[group].tabs;
|
||||
let index = tabs
|
||||
.get(index)
|
||||
.and_then(|t| all.iter().position(|a| a == t))
|
||||
.unwrap_or(all.len());
|
||||
DropTarget::Tab {
|
||||
side,
|
||||
group,
|
||||
index: Some(index),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
let spans = self.dock_slot_spans(side);
|
||||
let Some(&(gi, top, bottom)) = spans
|
||||
.iter()
|
||||
@@ -1082,33 +1131,6 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
index: self.dock.groups(side).len(),
|
||||
};
|
||||
};
|
||||
// Over a tab strip: drop between its tabs (reorders a member).
|
||||
let tabs = self.dock_group_visible_tabs(side, gi);
|
||||
if tabs.len() > 1
|
||||
&& self.dock_expanded_groups(side).contains(&gi)
|
||||
&& p.y < top + crate::ui::dock::DOCK_TAB_H
|
||||
{
|
||||
let x0 = self.dock_column_x0(side)
|
||||
+ if side == DockSide::Right { crate::ui::dock::DOCK_DIVIDER_W } else { 0.0 }
|
||||
+ crate::ui::dock::DOCK_TAB_INSET;
|
||||
let visible_at = crate::ui::dock::tab_insert_index(
|
||||
p.x,
|
||||
x0,
|
||||
crate::ui::dock::DOCK_TAB_CELL_W,
|
||||
tabs.len(),
|
||||
);
|
||||
// Map the gap among visible tabs to a position in all tabs.
|
||||
let all = &self.dock.groups(side)[gi].tabs;
|
||||
let index = tabs
|
||||
.get(visible_at)
|
||||
.and_then(|t| all.iter().position(|a| a == t))
|
||||
.unwrap_or(all.len());
|
||||
return DropTarget::Tab {
|
||||
side,
|
||||
group: gi,
|
||||
index: Some(index),
|
||||
};
|
||||
}
|
||||
return match slot_zone(p.y, top, bottom) {
|
||||
SlotZone::Above => DropTarget::Edge { side, index: gi },
|
||||
SlotZone::Below => DropTarget::Edge { side, index: gi + 1 },
|
||||
@@ -2107,17 +2129,19 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dock_dragging_a_tab_along_the_strip_reorders_it() {
|
||||
fn dock_dragging_an_icon_along_the_strip_reorders_it() {
|
||||
use crate::app::config::DockSide;
|
||||
use crate::ui::dock::{DockMsg, DropTarget, PanelId};
|
||||
use crate::ui::dock::{DockMsg, DropTarget, PanelId, STRIP_CELL_H};
|
||||
let mut app = dock_app();
|
||||
app.dock.add_tab(PanelId::BlockPalette, DockSide::Left, 0, None);
|
||||
app.dock.right.clear();
|
||||
// Icon tabs: Properties | Blocks from the left. Drag the Properties
|
||||
// tab past the Blocks tab.
|
||||
// Icons: Properties above Blocks. Drag the Properties icon below the
|
||||
// Blocks icon.
|
||||
let (layout, _) = app.dock_strip_layout(DockSide::Left);
|
||||
let top = layout[0].icons_top;
|
||||
let _ = app.on_dock(DockMsg::SelectTab(PanelId::Properties));
|
||||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(10.0, 12.0)));
|
||||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(78.0, 12.0)));
|
||||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, top + 10.0)));
|
||||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, top + 2.0 * STRIP_CELL_H)));
|
||||
assert_eq!(
|
||||
app.dock_drag_target,
|
||||
Some(DropTarget::Tab {
|
||||
@@ -2131,30 +2155,101 @@ mod tests {
|
||||
app.dock.left[0].tabs,
|
||||
vec![PanelId::BlockPalette, PanelId::Properties]
|
||||
);
|
||||
assert_eq!(app.dock.left[0].active, PanelId::Properties);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dock_tab_menu_adds_and_hides_pallets() {
|
||||
fn dock_dropping_an_icon_below_the_strip_groups_starts_a_new_group() {
|
||||
use crate::app::config::DockSide;
|
||||
use crate::ui::dock::{DockMsg, DropTarget, PanelId};
|
||||
let mut app = dock_app();
|
||||
app.dock.add_tab(PanelId::BlockPalette, DockSide::Left, 0, None);
|
||||
let (layout, plus_top) = app.dock_strip_layout(DockSide::Left);
|
||||
let _ = app.on_dock(DockMsg::SelectTab(PanelId::BlockPalette));
|
||||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, layout[0].icons_top + 40.0)));
|
||||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, plus_top + 10.0)));
|
||||
assert_eq!(
|
||||
app.dock_drag_target,
|
||||
Some(DropTarget::Edge {
|
||||
side: DockSide::Left,
|
||||
index: 1
|
||||
})
|
||||
);
|
||||
let _ = app.on_dock(DockMsg::DragRelease);
|
||||
assert_eq!(app.dock.left.len(), 2);
|
||||
assert_eq!(app.dock.left[1].tabs, vec![PanelId::BlockPalette]);
|
||||
}
|
||||
|
||||
#[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.dock.left = vec![
|
||||
DockGroup {
|
||||
tabs: vec![PanelId::Properties, PanelId::Browser],
|
||||
active: PanelId::Properties,
|
||||
weight: 1.0,
|
||||
},
|
||||
DockGroup::single(PanelId::BlockPalette, 1.0),
|
||||
];
|
||||
app.dock.right.clear();
|
||||
app.show_browser = true;
|
||||
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].grip_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].tabs, vec![PanelId::BlockPalette]);
|
||||
assert_eq!(
|
||||
app.dock.left[1].tabs,
|
||||
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!(app.dock_drag_target, None);
|
||||
let _ = app.on_dock(DockMsg::DragRelease);
|
||||
assert_eq!(app.dock, before);
|
||||
}
|
||||
|
||||
#[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::TabMenu(Some((DockSide::Left, 0))));
|
||||
// Checking a closed pallet opens it as a tab of this slot.
|
||||
let _ = app.on_dock(DockMsg::TabMenuToggle(DockSide::Left, 0, PanelId::Browser));
|
||||
let _ = app.on_dock(DockMsg::EdgeMenu(Some(DockSide::Left)));
|
||||
// Checking a closed pallet opens it as a new group on this edge.
|
||||
let _ = app.on_dock(DockMsg::EdgeMenuToggle(DockSide::Left, PanelId::Browser));
|
||||
assert!(app.show_browser);
|
||||
assert_eq!(app.dock.left[0].tabs, vec![PanelId::Properties, PanelId::Browser]);
|
||||
assert_eq!(app.dock.left[0].active, PanelId::Browser);
|
||||
// Checking a pallet docked elsewhere moves it here.
|
||||
let _ = app.on_dock(DockMsg::TabMenuToggle(DockSide::Left, 0, PanelId::BlockPalette));
|
||||
assert!(app.dock.right.is_empty());
|
||||
assert_eq!(app.dock.left[0].tabs.len(), 3);
|
||||
// Unchecking hides it but keeps its tab.
|
||||
let _ = app.on_dock(DockMsg::TabMenuToggle(DockSide::Left, 0, PanelId::Browser));
|
||||
assert_eq!(app.dock.left.len(), 2);
|
||||
assert_eq!(app.dock.left[1].tabs, vec![PanelId::Browser]);
|
||||
// Unchecking hides it but keeps its place.
|
||||
let _ = app.on_dock(DockMsg::EdgeMenuToggle(DockSide::Left, PanelId::Browser));
|
||||
assert!(!app.show_browser);
|
||||
assert!(app.dock.left[0].tabs.contains(&PanelId::Browser));
|
||||
assert_eq!(app.dock_tab_menu, Some((DockSide::Left, 0)));
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dock_divider_resizes_the_whole_column() {
|
||||
use crate::app::config::DockSide;
|
||||
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
|
||||
let mut app = dock_app();
|
||||
app.dock.left.push(DockGroup::single(PanelId::BlockPalette, 1.0));
|
||||
app.dock.right.clear();
|
||||
app.dock.set_width(PanelId::BlockPalette, 300.0);
|
||||
let _ = app.on_dock(DockMsg::ResizeGrab(PanelId::Properties));
|
||||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(330.0, 100.0)));
|
||||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(350.0, 100.0)));
|
||||
let _ = app.on_dock(DockMsg::DragRelease);
|
||||
// The column was 300 (the widest group); both groups now take 320.
|
||||
assert_eq!(app.dock.settings(PanelId::Properties).width, 320.0);
|
||||
assert_eq!(app.dock.settings(PanelId::BlockPalette).width, 320.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+130
-199
@@ -1936,39 +1936,29 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
layers.push(self.floating_panel(*f, tab));
|
||||
}
|
||||
}
|
||||
// An open pallet menu hangs under its slot's menu button, over a
|
||||
// An open pallet menu hangs beside its edge's + button, over a
|
||||
// catcher that closes it on any click elsewhere.
|
||||
if let Some((side, gi)) = self.dock_tab_menu {
|
||||
let span = self
|
||||
.dock_slot_spans(side)
|
||||
.into_iter()
|
||||
.find(|(g, _, _)| *g == gi);
|
||||
if let Some((_, top, _)) = span {
|
||||
let tabs = self.dock_group_visible_tabs(side, gi);
|
||||
if let Some(side) = self.dock_edge_menu {
|
||||
if !self.dock_visible_groups(side).is_empty() {
|
||||
let shown: Vec<_> = crate::ui::dock::PanelId::ALL
|
||||
.iter()
|
||||
.map(|p| (*p, tabs.contains(p)))
|
||||
.map(|p| (*p, self.dock_panel_visible(*p)))
|
||||
.collect();
|
||||
let right_pad = 2.0;
|
||||
let button_right = self.dock_column_x0(side)
|
||||
+ if side == crate::app::config::DockSide::Right {
|
||||
DOCK_DIVIDER_W
|
||||
} else {
|
||||
0.0
|
||||
let (_, plus_top) = self.dock_strip_layout(side);
|
||||
let strip = crate::ui::dock::DOCK_STRIP_W;
|
||||
let x = match side {
|
||||
crate::app::config::DockSide::Left => strip + 4.0,
|
||||
crate::app::config::DockSide::Right => {
|
||||
self.dock_workspace_size().0 - strip - DOCK_MENU_W - 4.0
|
||||
}
|
||||
+ self.dock_column_width(side)
|
||||
- right_pad;
|
||||
};
|
||||
layers.push(
|
||||
mouse_area(Space::new().width(Fill).height(Fill))
|
||||
.on_press(Message::Dock(crate::ui::dock::DockMsg::TabMenu(None)))
|
||||
.on_right_press(Message::Dock(crate::ui::dock::DockMsg::TabMenu(None)))
|
||||
.on_press(Message::Dock(crate::ui::dock::DockMsg::EdgeMenu(None)))
|
||||
.on_right_press(Message::Dock(crate::ui::dock::DockMsg::EdgeMenu(None)))
|
||||
.into(),
|
||||
);
|
||||
layers.push(place_at(
|
||||
dock_tab_menu(side, gi, &shown),
|
||||
(button_right - DOCK_MENU_W).max(0.0),
|
||||
top + crate::ui::dock::DOCK_TAB_H,
|
||||
));
|
||||
layers.push(place_at(dock_edge_menu(side, &shown), x.max(0.0), plus_top));
|
||||
}
|
||||
}
|
||||
// While a panel is dragged, preview where it lands.
|
||||
@@ -2779,56 +2769,32 @@ impl OpenCADStudio {
|
||||
iced::advanced::widget::operate(iced::advanced::widget::operation::focusable::unfocus())
|
||||
}
|
||||
|
||||
/// Build one edge: a narrow tab strip at the very edge plus the expanded
|
||||
/// slots sliding out beside it. Auto-collapsing (pinned) slots are always
|
||||
/// visible as equal-height tabs in the strip (via `Fill` distribution);
|
||||
/// hovering a tab raises its slot beside the strip, so switching between
|
||||
/// stacked panels is a direct tab-to-tab hover with no width-jumping
|
||||
/// collapse dance. Unpinned slots are always expanded and split the column
|
||||
/// height by their weights, with a draggable splitter between neighbours.
|
||||
/// The whole edge is one hover region: leaving it collapses any
|
||||
/// auto-collapsing slot, but moving between the tabs and the expanded slot
|
||||
/// does not. `None` when nothing on the edge is shown.
|
||||
/// Build one edge: the vertical icon strip at the very edge plus the
|
||||
/// expanded groups beside it. The strip lists every shown group's pallets
|
||||
/// as icons under a grip bar, with a + button below them; clicking an
|
||||
/// icon shows that pallet in its group. Unpinned groups are always
|
||||
/// expanded and split the column height by their weights, with a
|
||||
/// draggable splitter between neighbours. An auto-hiding (pinned) group
|
||||
/// lives only in the strip until one of its icons is hovered. The whole
|
||||
/// edge is one hover region: leaving it collapses any auto-hiding group.
|
||||
/// `None` when nothing on the edge is shown.
|
||||
fn build_edge_stack<'a>(
|
||||
&'a self,
|
||||
side: crate::app::config::DockSide,
|
||||
tab: &'a DocumentTab,
|
||||
) -> Option<Element<'a, Message>> {
|
||||
let visible = self.dock_visible_groups(side);
|
||||
if visible.is_empty() {
|
||||
if self.dock_visible_groups(side).is_empty() {
|
||||
return None;
|
||||
}
|
||||
let shown = |gi: usize| self.dock_group_shown(side, gi).expect("visible group");
|
||||
let pinned: Vec<usize> = visible
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|gi| self.dock.auto_collapse(shown(*gi)))
|
||||
.collect();
|
||||
let expanded = self.dock_expanded_groups(side);
|
||||
// Each expanded slot renders at its shown panel's saved width; the
|
||||
// column is as wide as the widest one currently showing.
|
||||
let col_w = expanded
|
||||
.iter()
|
||||
.map(|gi| self.dock_group_width(side, *gi))
|
||||
.fold(0.0, f32::max);
|
||||
|
||||
let tab_strip = (!pinned.is_empty()).then(|| {
|
||||
let tabs: Vec<Element<'_, Message>> = pinned
|
||||
.iter()
|
||||
.map(|gi| {
|
||||
let tabs = self.dock_group_visible_tabs(side, *gi);
|
||||
self.rail_slice(
|
||||
shown(*gi),
|
||||
&tabs,
|
||||
side,
|
||||
self.dock_dragging.is_some_and(|d| tabs.contains(&d)),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
column(tabs).width(DOCK_RAIL_W).height(Fill).into()
|
||||
});
|
||||
let strip = self.dock_icon_strip(side);
|
||||
let expanded_stack = (!expanded.is_empty()).then(|| {
|
||||
// Expanded slots stack one below the other, each taking its
|
||||
// Expanded groups stack one below the other, each taking its
|
||||
// weighted share of the column height — never overlapping.
|
||||
let mut layers: Vec<Element<'_, Message>> = Vec::new();
|
||||
for (n, gi) in expanded.iter().enumerate() {
|
||||
@@ -2848,20 +2814,12 @@ impl OpenCADStudio {
|
||||
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);
|
||||
}
|
||||
children.push(strip);
|
||||
children.extend(expanded_stack);
|
||||
}
|
||||
crate::app::config::DockSide::Right => {
|
||||
if let Some(e) = expanded_stack {
|
||||
children.push(e);
|
||||
}
|
||||
if let Some(t) = tab_strip {
|
||||
children.push(t);
|
||||
}
|
||||
children.extend(expanded_stack);
|
||||
children.push(strip);
|
||||
}
|
||||
}
|
||||
Some(
|
||||
@@ -2871,52 +2829,98 @@ impl OpenCADStudio {
|
||||
)
|
||||
}
|
||||
|
||||
/// One narrow tab in the edge strip, naming every panel of its slot.
|
||||
/// Equal `Fill` heights across the tabs give each 1/N of the strip;
|
||||
/// hovering or clicking it raises its slot.
|
||||
fn rail_slice(
|
||||
&self,
|
||||
id: crate::ui::dock::PanelId,
|
||||
tabs: &[crate::ui::dock::PanelId],
|
||||
side: crate::app::config::DockSide,
|
||||
is_dragging: bool,
|
||||
) -> Element<'_, Message> {
|
||||
let label = canvas(VBarLabel {
|
||||
text: tabs
|
||||
.iter()
|
||||
.map(|t| t.title())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" · "),
|
||||
clockwise: side == crate::app::config::DockSide::Left,
|
||||
from_start: false,
|
||||
})
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
// Hovering raises the slot but leaves the rail looking as it is.
|
||||
let bg = move |theme: &Theme| {
|
||||
let palette = theme.palette();
|
||||
if is_dragging {
|
||||
palette.primary.weak.color.scale_alpha(0.55)
|
||||
} else {
|
||||
palette.background.base.color
|
||||
/// An edge's vertical icon strip: per shown group a grip bar (drag it to
|
||||
/// move the group) and its pallets' icons (the group's tabs; the shown one
|
||||
/// on a grey background), then a + button opening the pallet menu.
|
||||
/// Heights follow `dock::strip_layout`, which drop targeting uses too.
|
||||
fn dock_icon_strip(&self, side: crate::app::config::DockSide) -> Element<'_, Message> {
|
||||
use crate::ui::dock::{DockMsg, DOCK_STRIP_W, STRIP_CELL_H, STRIP_GAP, STRIP_GRIP_H};
|
||||
let mut items: Vec<Element<'_, Message>> = Vec::new();
|
||||
for gi in self.dock_visible_groups(side) {
|
||||
items.push(Space::new().height(STRIP_GAP).into());
|
||||
let grip = container(
|
||||
container(Space::new())
|
||||
.width(Length::Fixed(18.0))
|
||||
.height(Length::Fixed(3.0))
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(
|
||||
theme.palette().background.strong.color,
|
||||
)),
|
||||
border: Border {
|
||||
radius: 1.5.into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.center_x(Length::Fixed(DOCK_STRIP_W))
|
||||
.center_y(Length::Fixed(STRIP_GRIP_H));
|
||||
items.push(
|
||||
mouse_area(grip)
|
||||
.on_press(Message::Dock(DockMsg::GroupGrab(side, gi)))
|
||||
.interaction(iced::mouse::Interaction::Grab)
|
||||
.into(),
|
||||
);
|
||||
let shown = self.dock_group_shown(side, gi);
|
||||
for id in self.dock_group_visible_tabs(side, gi) {
|
||||
let is_active = Some(id) == shown;
|
||||
let icon = container(crate::ui::icons::themed(id.icon(), 20.0))
|
||||
.center_x(Length::Fixed(STRIP_CELL_H - 4.0))
|
||||
.center_y(Length::Fixed(STRIP_CELL_H - 4.0))
|
||||
.style(move |theme: &Theme| container::Style {
|
||||
// Only the shown pallet of the group gets a background.
|
||||
background: is_active
|
||||
.then(|| Background::Color(theme.palette().background.weak.color)),
|
||||
border: Border {
|
||||
radius: 4.0.into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let cell = mouse_area(
|
||||
container(icon)
|
||||
.center_x(Length::Fixed(DOCK_STRIP_W))
|
||||
.center_y(Length::Fixed(STRIP_CELL_H)),
|
||||
)
|
||||
.on_press(Message::Dock(DockMsg::SelectTab(id)))
|
||||
.on_enter(Message::Dock(DockMsg::Hover(id)))
|
||||
.interaction(iced::mouse::Interaction::Pointer);
|
||||
items.push(
|
||||
iced::widget::tooltip(
|
||||
cell,
|
||||
text(id.title()).size(10),
|
||||
match side {
|
||||
crate::app::config::DockSide::Left => {
|
||||
iced::widget::tooltip::Position::Right
|
||||
}
|
||||
crate::app::config::DockSide::Right => {
|
||||
iced::widget::tooltip::Position::Left
|
||||
}
|
||||
},
|
||||
)
|
||||
.gap(4)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let tab = container(label)
|
||||
.width(Length::Fixed(DOCK_RAIL_W))
|
||||
}
|
||||
items.push(Space::new().height(STRIP_GAP).into());
|
||||
let plus = button(crate::ui::icons::themed_secondary(crate::ui::icons::PLUS, 14.0))
|
||||
.on_press(Message::Dock(DockMsg::EdgeMenu(Some(side))))
|
||||
.style(button::subtle)
|
||||
.padding([5, 8]);
|
||||
items.push(container(plus).center_x(Length::Fixed(DOCK_STRIP_W)).into());
|
||||
container(column(items))
|
||||
.width(Length::Fixed(DOCK_STRIP_W))
|
||||
.height(Fill)
|
||||
.style(move |theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(bg(theme))),
|
||||
.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()
|
||||
});
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -2977,8 +2981,8 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
/// An expanded slot: its tab strip (when it holds more than one shown
|
||||
/// panel), the shown panel, and a grabbable divider against the viewport.
|
||||
/// An expanded slot: its shown panel (the group's tabs live in the icon
|
||||
/// strip) and 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.
|
||||
@@ -2989,16 +2993,8 @@ impl OpenCADStudio {
|
||||
width: f32,
|
||||
tab: &'a DocumentTab,
|
||||
) -> Element<'a, Message> {
|
||||
let tabs = self.dock_group_visible_tabs(side, gi);
|
||||
let id = self.dock_group_shown(side, gi).expect("visible group");
|
||||
let body = self.panel_body(id, width, false, tab);
|
||||
let panel: Element<'_, Message> = if tabs.len() > 1 {
|
||||
column![dock_tab_strip(&tabs, id, width, side, gi), body]
|
||||
.height(Fill)
|
||||
.into()
|
||||
} else {
|
||||
body
|
||||
};
|
||||
let panel = self.panel_body(id, width, false, tab);
|
||||
let divider = dock_divider(id);
|
||||
match side {
|
||||
crate::app::config::DockSide::Left => row![panel, divider].height(Fill).into(),
|
||||
@@ -3113,8 +3109,11 @@ impl OpenCADStudio {
|
||||
let (ww, _) = self.dock_workspace_size();
|
||||
let id_w = self.dock.width(id, self.win_size.0);
|
||||
let edge_w = |side: DockSide| self.dock_column_width(side).max(id_w);
|
||||
let strip = crate::ui::dock::DOCK_STRIP_W;
|
||||
// The tint covers the edge's icon strip and column; the ghost sits in
|
||||
// the column beside the strip.
|
||||
let tint = |side: DockSide| -> Element<'_, Message> {
|
||||
let w = edge_w(side);
|
||||
let w = edge_w(side) + strip;
|
||||
let band = container(Space::new())
|
||||
.width(Length::Fixed(w))
|
||||
.height(Fill)
|
||||
@@ -3134,8 +3133,8 @@ impl OpenCADStudio {
|
||||
.into()
|
||||
};
|
||||
let edge_x = |side: DockSide, w: f32| match side {
|
||||
DockSide::Left => 0.0,
|
||||
DockSide::Right => (ww - w).max(0.0),
|
||||
DockSide::Left => strip,
|
||||
DockSide::Right => (ww - w - strip).max(0.0),
|
||||
};
|
||||
let mut layers: Vec<Element<'_, Message>> = Vec::new();
|
||||
match target {
|
||||
@@ -3689,84 +3688,16 @@ fn dock_divider(id: crate::ui::dock::PanelId) -> Element<'static, Message> {
|
||||
.into()
|
||||
}
|
||||
|
||||
use crate::ui::dock::{DOCK_DIVIDER_W, DOCK_RAIL_W};
|
||||
use crate::ui::dock::DOCK_DIVIDER_W;
|
||||
|
||||
/// The tab row above a docked slot holding several pallets: one icon tab per
|
||||
/// shown pallet, from the left, and a menu button on the right that lists
|
||||
/// every pallet (drawn by `dock_tab_menu`). Pressing a tab shows its pallet;
|
||||
/// dragging it reorders the tabs or pulls the pallet out.
|
||||
fn dock_tab_strip(
|
||||
tabs: &[crate::ui::dock::PanelId],
|
||||
active: crate::ui::dock::PanelId,
|
||||
width: f32,
|
||||
side: crate::app::config::DockSide,
|
||||
gi: usize,
|
||||
) -> Element<'static, Message> {
|
||||
use crate::ui::dock::{DockMsg, DOCK_TAB_CELL_W, DOCK_TAB_INSET};
|
||||
const TAB_GAP: f32 = 2.0;
|
||||
let mut cells: Vec<Element<'static, Message>> = tabs
|
||||
.iter()
|
||||
.map(|id| {
|
||||
let id = *id;
|
||||
let is_active = id == active;
|
||||
let cell = container(crate::ui::icons::themed(id.icon(), 18.0))
|
||||
.center_x(Length::Fixed(DOCK_TAB_CELL_W - TAB_GAP))
|
||||
.center_y(Fill)
|
||||
.style(move |theme: &Theme| container::Style {
|
||||
// Only the shown tab gets a background.
|
||||
background: is_active
|
||||
.then(|| Background::Color(theme.palette().background.weak.color)),
|
||||
border: Border {
|
||||
radius: iced::border::Radius::default().top(4.0),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let cell = mouse_area(cell)
|
||||
.on_press(Message::Dock(DockMsg::SelectTab(id)))
|
||||
.interaction(iced::mouse::Interaction::Pointer);
|
||||
iced::widget::tooltip(
|
||||
cell,
|
||||
text(id.title()).size(10),
|
||||
iced::widget::tooltip::Position::Bottom,
|
||||
)
|
||||
.gap(4)
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
cells.push(Space::new().width(Fill).into());
|
||||
cells.push(
|
||||
button(crate::ui::icons::themed_secondary(crate::ui::icons::MENU, 14.0))
|
||||
.on_press(Message::Dock(DockMsg::TabMenu(Some((side, gi)))))
|
||||
.style(button::subtle)
|
||||
.padding([3, 5])
|
||||
.into(),
|
||||
);
|
||||
container(row(cells).spacing(TAB_GAP).height(Fill).align_y(iced::Center))
|
||||
.width(Length::Fixed(width))
|
||||
.height(Length::Fixed(crate::ui::dock::DOCK_TAB_H))
|
||||
.padding(iced::Padding {
|
||||
top: 3.0,
|
||||
right: 2.0,
|
||||
bottom: 0.0,
|
||||
left: DOCK_TAB_INSET,
|
||||
})
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.strong.color)),
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Width of the tab row's pallet menu.
|
||||
/// Width of the icon strip's pallet menu.
|
||||
const DOCK_MENU_W: f32 = 230.0;
|
||||
|
||||
/// The pallet menu of slot (`side`, `gi`), styled like the right-click menu:
|
||||
/// one row per pallet with its icon, its name and a check mark when it shows
|
||||
/// in this slot. `shown` holds each pallet with that state.
|
||||
fn dock_tab_menu(
|
||||
/// The pallet menu of an edge's + button, styled like the right-click menu:
|
||||
/// one row per pallet with its icon, its name and a check mark when it is
|
||||
/// shown. `shown` holds each pallet with that state.
|
||||
fn dock_edge_menu(
|
||||
side: crate::app::config::DockSide,
|
||||
gi: usize,
|
||||
shown: &[(crate::ui::dock::PanelId, bool)],
|
||||
) -> Element<'static, Message> {
|
||||
use crate::ui::dock::DockMsg;
|
||||
@@ -3788,7 +3719,7 @@ fn dock_tab_menu(
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.on_press(Message::Dock(DockMsg::TabMenuToggle(side, gi, id)))
|
||||
.on_press(Message::Dock(DockMsg::EdgeMenuToggle(side, id)))
|
||||
.style(|theme: &Theme, status| {
|
||||
let palette = theme.palette();
|
||||
button::Style {
|
||||
|
||||
+132
-26
@@ -43,12 +43,14 @@ pub enum DockMsg {
|
||||
FloatResizeGrab(PanelId, bool),
|
||||
/// The pointer entered (`Some`) or left (`None`) a docked title bar.
|
||||
TitleHover(Option<PanelId>),
|
||||
/// Open (`Some`) the pallet menu of a slot (side, slot index), or close
|
||||
/// it (`None`).
|
||||
TabMenu(Option<(DockSide, usize)>),
|
||||
/// Pallet menu pick: show `panel` as a tab of the slot, or hide it when
|
||||
/// it already shows there.
|
||||
TabMenuToggle(DockSide, usize, PanelId),
|
||||
/// Open (`Some`) the pallet menu of an edge's icon strip (its + button),
|
||||
/// or close it (`None`).
|
||||
EdgeMenu(Option<DockSide>),
|
||||
/// Pallet menu pick: hide `panel` when it shows, else open it docked on
|
||||
/// the edge (as a new group unless it already has a group there).
|
||||
EdgeMenuToggle(DockSide, PanelId),
|
||||
/// Begin dragging a whole group (slot) by its grip in the icon strip.
|
||||
GroupGrab(DockSide, usize),
|
||||
/// Double-click on a docked title bar: float the panel.
|
||||
FloatOut(PanelId),
|
||||
/// Double-click on a floating title bar: dock the panel on that side.
|
||||
@@ -503,6 +505,28 @@ impl DockState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Move slot `group` on `side` to insertion `index` on `to` (counted
|
||||
/// before the move). Returns whether the layout changed.
|
||||
pub fn move_group(&mut self, side: DockSide, group: usize, to: DockSide, index: usize) -> bool {
|
||||
if group >= self.groups(side).len() {
|
||||
return false;
|
||||
}
|
||||
let mut index = index;
|
||||
if side == to {
|
||||
if index == group || index == group + 1 {
|
||||
return false;
|
||||
}
|
||||
if group < index {
|
||||
index -= 1;
|
||||
}
|
||||
}
|
||||
let g = self.groups_mut(side).remove(group);
|
||||
let dest = self.groups_mut(to);
|
||||
let index = index.min(dest.len());
|
||||
dest.insert(index, g);
|
||||
true
|
||||
}
|
||||
|
||||
/// Float `id` at `rect`, on top of the other floating panels.
|
||||
pub fn float(&mut self, rect: FloatPanel) -> bool {
|
||||
let before = self.clone();
|
||||
@@ -641,25 +665,79 @@ pub const DOCK_MIN_W: f32 = 200.0;
|
||||
pub const DOCK_MAX_W: f32 = 600.0;
|
||||
/// Width of the band along an empty edge that docks a dragged panel there.
|
||||
pub const DOCK_EDGE_ZONE: f32 = 48.0;
|
||||
/// Width of a collapsed (auto-collapsing) slot's tab in the edge strip.
|
||||
pub const DOCK_RAIL_W: f32 = 28.0;
|
||||
/// Width of the grabbable divider between a docked slot and the viewport.
|
||||
pub const DOCK_DIVIDER_W: f32 = 5.0;
|
||||
/// Height of a docked slot's tab strip.
|
||||
pub const DOCK_TAB_H: f32 = 28.0;
|
||||
/// Width of an edge's vertical icon strip (the groups' tabs).
|
||||
pub const DOCK_STRIP_W: f32 = 36.0;
|
||||
/// Height of one pallet icon in the strip.
|
||||
pub const STRIP_CELL_H: f32 = 32.0;
|
||||
/// Height of the grip bar above each group's icons.
|
||||
pub const STRIP_GRIP_H: f32 = 10.0;
|
||||
/// Space above the first group and between groups in the strip.
|
||||
pub const STRIP_GAP: f32 = 6.0;
|
||||
|
||||
/// Pitch of one icon tab in a tab strip (tab plus the gap after it).
|
||||
pub const DOCK_TAB_CELL_W: f32 = 34.0;
|
||||
/// Left inset of the first tab in a tab strip.
|
||||
pub const DOCK_TAB_INSET: f32 = 4.0;
|
||||
/// Where one group sits in an edge's icon strip, top to bottom.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct StripGroup {
|
||||
/// Slot index on the edge.
|
||||
pub group: usize,
|
||||
/// Top of the group's grip bar.
|
||||
pub grip_top: f32,
|
||||
/// Top of the group's first icon.
|
||||
pub icons_top: f32,
|
||||
/// Number of icons (visible tabs).
|
||||
pub icons: usize,
|
||||
}
|
||||
|
||||
/// Insertion position (0..=`count`) for a tab dropped at `x` on a strip of
|
||||
/// `count` left-aligned tabs of pitch `cell` starting at `x0`.
|
||||
pub fn tab_insert_index(x: f32, x0: f32, cell: f32, count: usize) -> usize {
|
||||
if count == 0 || cell <= 0.0 {
|
||||
return 0;
|
||||
/// Lay out the icon strip for `groups` = (slot index, visible tab count),
|
||||
/// returning each group's place and the top of the + button below them.
|
||||
pub fn strip_layout(groups: &[(usize, usize)]) -> (Vec<StripGroup>, f32) {
|
||||
let mut y = STRIP_GAP;
|
||||
let placed = groups
|
||||
.iter()
|
||||
.map(|&(group, icons)| {
|
||||
let grip_top = y;
|
||||
let icons_top = grip_top + STRIP_GRIP_H;
|
||||
y = icons_top + icons as f32 * STRIP_CELL_H + STRIP_GAP;
|
||||
StripGroup {
|
||||
group,
|
||||
grip_top,
|
||||
icons_top,
|
||||
icons,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
(placed, y)
|
||||
}
|
||||
|
||||
/// What a dragged pallet released at height `y` over the icon strip joins.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StripHit {
|
||||
/// A new group before slot `group`; `None` = after the last group.
|
||||
NewGroup(Option<usize>),
|
||||
/// Group `group`, at position `index` among its visible icons.
|
||||
Tab { group: usize, index: usize },
|
||||
}
|
||||
|
||||
/// Classify a pointer at strip height `y`: over a group's grip (or the gap
|
||||
/// above it) starts a new group there, over its icons joins it between the
|
||||
/// nearest icons, below every group starts a new last group.
|
||||
pub fn strip_hit(y: f32, layout: &[StripGroup]) -> StripHit {
|
||||
for g in layout {
|
||||
if y < g.icons_top {
|
||||
return StripHit::NewGroup(Some(g.group));
|
||||
}
|
||||
let bottom = g.icons_top + g.icons as f32 * STRIP_CELL_H + STRIP_GAP * 0.5;
|
||||
if y < bottom {
|
||||
let index = (((y - g.icons_top) / STRIP_CELL_H).round().max(0.0) as usize)
|
||||
.min(g.icons);
|
||||
return StripHit::Tab {
|
||||
group: g.group,
|
||||
index,
|
||||
};
|
||||
}
|
||||
}
|
||||
(((x - x0) / cell).round().max(0.0) as usize).min(count)
|
||||
StripHit::NewGroup(None)
|
||||
}
|
||||
|
||||
// ── Shared panel chrome ─────────────────────────────────────────────────
|
||||
@@ -978,12 +1056,40 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_insert_index_rounds_to_the_nearest_gap() {
|
||||
assert_eq!(tab_insert_index(0.0, 0.0, 30.0, 3), 0);
|
||||
assert_eq!(tab_insert_index(40.0, 0.0, 30.0, 3), 1);
|
||||
assert_eq!(tab_insert_index(85.0, 0.0, 30.0, 3), 3);
|
||||
assert_eq!(tab_insert_index(250.0, 0.0, 30.0, 3), 3);
|
||||
assert_eq!(tab_insert_index(-20.0, 0.0, 30.0, 3), 0);
|
||||
fn strip_hit_maps_grips_icons_and_the_end() {
|
||||
// Two groups: three icons, then one.
|
||||
let (layout, plus_top) = strip_layout(&[(0, 3), (2, 1)]);
|
||||
let g0 = layout[0];
|
||||
let g1 = layout[1];
|
||||
assert_eq!(g0.grip_top, STRIP_GAP);
|
||||
assert_eq!(g1.icons_top, g1.grip_top + STRIP_GRIP_H);
|
||||
assert_eq!(plus_top, g1.icons_top + STRIP_CELL_H + STRIP_GAP);
|
||||
// On the first grip: a new group before slot 0.
|
||||
assert_eq!(strip_hit(g0.grip_top + 2.0, &layout), StripHit::NewGroup(Some(0)));
|
||||
// Between the first and second icon of group 0.
|
||||
assert_eq!(
|
||||
strip_hit(g0.icons_top + STRIP_CELL_H + 4.0, &layout),
|
||||
StripHit::Tab { group: 0, index: 1 }
|
||||
);
|
||||
// On the second group's grip: a new group before slot 2.
|
||||
assert_eq!(strip_hit(g1.grip_top + 1.0, &layout), StripHit::NewGroup(Some(2)));
|
||||
// Below everything: a new last group.
|
||||
assert_eq!(strip_hit(plus_top + 20.0, &layout), StripHit::NewGroup(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_group_reorders_and_crosses_edges() {
|
||||
let mut state = DockState::default();
|
||||
state.left.push(DockGroup::single(PanelId::Browser, 1.0));
|
||||
state.left.push(DockGroup::single(PanelId::Count, 1.0));
|
||||
// Same spot (before itself or after itself) changes nothing.
|
||||
assert!(!state.move_group(DockSide::Left, 1, DockSide::Left, 1));
|
||||
assert!(!state.move_group(DockSide::Left, 1, DockSide::Left, 2));
|
||||
assert!(state.move_group(DockSide::Left, 0, DockSide::Left, 3));
|
||||
assert_eq!(state.left[2].tabs, vec![PanelId::Properties]);
|
||||
assert!(state.move_group(DockSide::Left, 2, DockSide::Right, 0));
|
||||
assert_eq!(state.right[0].tabs, vec![PanelId::Properties]);
|
||||
assert_eq!(state.left.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user