Dock: auto-hide per edge, hover only flies out when auto-hiding, menu per edge

- Auto-hide is a setting of the left / right edge rather than of each
  pallet: the pin on any docked pallet turns it on for its whole edge,
  which then shows only its icon strip until an icon is hovered. A
  floating pallet keeps its own auto-hide.
- Hovering the strip icons only shows a group while the edge auto-hides;
  otherwise groups switch on click.
- The + menu checks only the pallets open on that edge. Picking one that
  is open elsewhere (the other edge, floating) moves it to this edge;
  unchecking a checked one hides it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013RyGiMXcsf9HTKfqDxaQGt
This commit is contained in:
Claude committed 2026-10-09 17:44:52 +00:00
1 parent 47c2e2f2fe
commit 258762f992
3 files changed
+115 -21

No files matched your search

+51 -18
View File
@@ -633,7 +633,11 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
iced::Task::none()
}
DockMsg::EdgeMenuToggle(side, id) => {
if self.dock_panel_visible(id) {
// Checked = open and docked on this edge: unchecking hides it.
// Anything else (closed, floating, on the other edge) moves
// here and opens.
let here = self.dock.location(id).map(|(s, _)| s) == Some(side);
if here && self.dock_panel_visible(id) {
let task = self.on_dock(DockMsg::Close(id));
self.save_config();
return task;
@@ -666,8 +670,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
iced::Task::none()
}
DockMsg::AutoCollapseToggle(id) => {
let on = !self.dock.auto_collapse(id);
self.dock.set_auto_collapse(id, on);
// Docked: auto-hide for the whole edge; floating: this pallet.
let on = self.dock.toggle_auto_hide(id);
self.dock_expanded = if on { None } else { Some(id) };
self.save_config();
iced::Task::none()
@@ -722,19 +726,22 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
// 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() {
// Only an auto-hiding edge (or floating pallet) reacts to
// hover: it flies the hovered group out. Otherwise groups
// switch on click.
if self.dock_dragging.is_none()
&& self.dock_resizing.is_none()
&& self.dock.auto_hides(id)
{
self.dock_expanded = Some(id);
// Hovering an auto-hiding group's icon flies it out.
if self.dock.auto_collapse(id) {
self.dock.show_group_of(id);
}
self.dock.show_group_of(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) {
if self.dock.auto_hides(id) {
self.dock_expanded = None;
}
}
@@ -991,14 +998,15 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
}
/// Whether `side` draws its shown group beside the icon strip: always,
/// unless all its pallets auto-hide and none is hovered.
/// 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;
};
let panels = self.dock_group_visible(side, gi);
panels.iter().any(|id| !self.dock.auto_collapse(*id))
|| self.dock_expanded.is_some_and(|e| panels.contains(&e))
!self.dock.edge_auto_hide(side)
|| self
.dock_expanded
.is_some_and(|e| self.dock_group_visible(side, gi).contains(&e))
}
/// Width of group `gi` on `side`: its pallets share the widest one's
@@ -2010,9 +2018,9 @@ mod tests {
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");
assert!(app.dock.auto_hides(id), "pin enables auto-hide");
let _ = app.on_dock(crate::ui::dock::DockMsg::AutoCollapseToggle(id));
assert!(!app.dock.auto_collapse(id), "second pin disables auto-collapse");
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");
}
@@ -2287,18 +2295,37 @@ mod tests {
}
#[test]
fn dock_auto_hiding_group_shows_while_its_icon_is_hovered() {
fn dock_auto_hide_edge_shows_the_hovered_group() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockMsg, PanelId};
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));
let _ = app.on_dock(DockMsg::Hover(PanelId::Properties));
// Hovering an icon flies its group out.
let _ = app.on_dock(DockMsg::Hover(PanelId::Browser));
assert!(app.dock_edge_expanded(DockSide::Left));
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
let _ = app.on_dock(DockMsg::HoverExit);
assert!(!app.dock_edge_expanded(DockSide::Left));
}
#[test]
fn dock_hover_does_not_switch_groups_without_auto_hide() {
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));
let _ = app.on_dock(DockMsg::Hover(PanelId::Browser));
assert_eq!(app.dock_shown_group(DockSide::Left), Some(0));
assert!(app.dock_edge_expanded(DockSide::Left));
}
#[test]
fn dock_float_resize_grows_the_panel() {
use crate::ui::dock::{DockMsg, FloatPanel, PanelId};
@@ -2345,6 +2372,12 @@ mod tests {
// 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]
+7 -3
View File
@@ -1940,9 +1940,13 @@ bg={bg_ms:.1}ms n={view_count}"
// catcher that closes it on any click elsewhere.
if let Some(side) = self.dock_edge_menu {
if !self.dock_visible_groups(side).is_empty() {
// Checked = open and docked on this edge.
let shown: Vec<_> = crate::ui::dock::PanelId::ALL
.iter()
.map(|p| (*p, self.dock_panel_visible(*p)))
.map(|p| {
let here = self.dock.location(*p).map(|(s, _)| s) == Some(side);
(*p, here && self.dock_panel_visible(*p))
})
.collect();
let (_, plus_top) = self.dock_strip_layout(side);
let strip = crate::ui::dock::DOCK_STRIP_W;
@@ -2927,7 +2931,7 @@ impl OpenCADStudio {
tab: &'a DocumentTab,
) -> Element<'a, Message> {
let chrome = crate::ui::dock::Chrome {
auto_collapse: self.dock.auto_collapse(id),
auto_collapse: self.dock.auto_hides(id),
floating,
title_hovered: self.dock_title_hover == Some(id),
};
@@ -3635,7 +3639,7 @@ const DOCK_MENU_W: f32 = 230.0;
/// 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.
/// open on this edge. `shown` holds each pallet with that state.
fn dock_edge_menu(
side: crate::app::config::DockSide,
shown: &[(crate::ui::dock::PanelId, bool)],
+57
View File
@@ -310,6 +310,9 @@ pub struct DockState {
pub right: Vec<DockGroup>,
/// The group each edge shows (left, right).
pub shown: (usize, usize),
/// Auto-hide per edge (left, right): the edge shows only its icon strip
/// until an icon is hovered.
pub auto_hide: (bool, bool),
/// Floating panels, back → front.
pub floating: Vec<FloatPanel>,
/// Per-panel width / auto-collapse settings, keyed by `PanelId`. A
@@ -323,6 +326,7 @@ impl Default for DockState {
left: vec![DockGroup::single(PanelId::Properties)],
right: vec![DockGroup::single(PanelId::BlockPalette)],
shown: (0, 0),
auto_hide: (false, false),
floating: Vec::new(),
panels: BTreeMap::new(),
}
@@ -435,10 +439,41 @@ impl DockState {
.clamp(DOCK_MIN_W, id.max_width().min(win_w * 0.45).max(DOCK_MIN_W))
}
/// The floating auto-hide flag of `id` (a docked pallet follows its
/// edge instead; see [`Self::auto_hides`]).
pub fn auto_collapse(&self, id: PanelId) -> bool {
self.settings(id).auto_collapse
}
/// Whether `side` auto-hides.
pub fn edge_auto_hide(&self, side: DockSide) -> bool {
match side {
DockSide::Left => self.auto_hide.0,
DockSide::Right => self.auto_hide.1,
}
}
/// Whether `id` auto-hides where it is: by its edge's setting when
/// docked, by its own when floating.
pub fn auto_hides(&self, id: PanelId) -> bool {
match self.location(id) {
Some((side, _)) => self.edge_auto_hide(side),
None => self.auto_collapse(id),
}
}
/// Flip auto-hide where `id` is: its whole edge when docked, the pallet
/// itself when floating. Returns the new state.
pub fn toggle_auto_hide(&mut self, id: PanelId) -> bool {
let on = !self.auto_hides(id);
match self.location(id) {
Some((DockSide::Left, _)) => self.auto_hide.0 = on,
Some((DockSide::Right, _)) => self.auto_hide.1 = on,
None => self.set_auto_collapse(id, on),
}
on
}
/// Set the persisted width, clamped.
pub fn set_width(&mut self, id: PanelId, width: f32) {
let entry = self.panels.entry(id).or_insert_with(|| DockPanel::for_id(id));
@@ -1131,6 +1166,28 @@ mod tests {
assert_eq!(state.left[0].weights, vec![1.0, 1.0]);
}
#[test]
fn auto_hide_is_per_edge_for_docked_and_per_pallet_when_floating() {
let mut state = DockState::default();
state.join_group(PanelId::Browser, DockSide::Left, 0, 1);
assert!(state.toggle_auto_hide(PanelId::Properties));
// The whole left edge auto-hides, the right edge does not.
assert!(state.auto_hides(PanelId::Browser));
assert!(!state.auto_hides(PanelId::BlockPalette));
// A floating pallet keeps its own setting.
state.float(FloatPanel {
id: PanelId::Count,
x: 0.0,
y: 0.0,
w: 250.0,
h: 300.0,
});
assert!(!state.auto_hides(PanelId::Count));
assert!(state.toggle_auto_hide(PanelId::Count));
assert!(state.auto_hides(PanelId::Count));
assert!(!state.edge_auto_hide(DockSide::Right));
}
#[test]
fn ensure_settings_drops_duplicates_and_clamps_the_shown_group() {
let mut state = DockState::default();