Dock: LAYERS opens its pallet like Insert Block, Layer Manager layout fixes, 150 ms auto-hide hover delay

- LAYERS ribbon button runs a command that opens the Layer Manager pallet;
  the ribbon no longer keeps it highlighted.
- Layer Manager: search box stays right-aligned, the column header fills
  the row, no stray contour lines when the pallet is taller than its rows.
- Auto-hidden edges don't show the shown-group accent bar.
- Auto-hide reveal and hide wait 150 ms of rest, so passing over the
  strip doesn't flash groups open.

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-10 10:22:47 +00:00
1 parent b12a29b233
commit 4e210f65b5
9 files changed
+172 -65

No files matched your search

+3 -1
View File
@@ -354,7 +354,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
+27
View File
@@ -336,9 +336,19 @@ fn dock_hover_reveals_groups_only_on_an_auto_hiding_edge() {
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));
@@ -539,12 +549,29 @@ fn hovering_a_strip_icon_reveals_an_auto_hiding_edge_in_the_real_view() {
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;
+85 -5
View File
@@ -7,6 +7,24 @@ use crate::app::{Message, OpenCADStudio};
/// 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.
@@ -126,11 +144,15 @@ impl OpenCADStudio {
DockMsg::Hover(id) => {
self.dock_icon_hover = Some(id);
// Only an auto-hiding edge (or floating pallet) reacts to
// hover: 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.
// 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) {
self.dock_reveal(id);
if self.dock_peek == Some(id) {
self.dock_hover_cancel();
} else {
return self.dock_hover_arm(Some(id));
}
}
}
DockMsg::HoverEnd(id) => {
@@ -142,7 +164,25 @@ impl OpenCADStudio {
if self.dock_drag.is_none()
&& self.dock_peek.is_some_and(|id| self.dock.auto_hides(id))
{
self.dock_peek = None;
return self.dock_hover_arm(None);
}
// Left before a pending reveal ran out: it never happens.
self.dock_hover_cancel();
}
DockMsg::HoverStay => {
if self.dock_hover_pending == Some(None) {
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(Some(id)) if self.dock.auto_hides(id) => self.dock_reveal(id),
Some(None) if self.dock_peek.is_some_and(|id| self.dock.auto_hides(id)) => {
self.dock_peek = None;
}
_ => {}
}
}
}
DockMsg::DragMove(point) => self.dock_drag_move(point),
@@ -329,6 +369,23 @@ impl OpenCADStudio {
/// Bring `id` into view: show its group on its edge and, where it
/// auto-hides, keep it revealed until the pointer leaves.
/// Arm a delayed reveal (`Some`) or hide (`None`); it applies when its
/// timer reports back unless a newer hover superseded it.
fn dock_hover_arm(&mut self, target: Option<crate::ui::dock::PanelId>) -> iced::Task<Message> {
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;
}
pub(crate) fn dock_reveal(&mut self, id: crate::ui::dock::PanelId) {
self.dock.show_group_of(id);
self.dock_peek = Some(id);
@@ -419,6 +476,29 @@ impl OpenCADStudio {
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);
self.dock.float(FloatPanel {
id: PanelId::Layers,
x: ((ww - w) * 0.5).max(0.0),
y: ((wh - h) * 0.5).max(0.0),
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) {
+6 -1
View File
@@ -147,6 +147,7 @@ impl OpenCADStudio {
}
Some(
mouse_area(row(children).height(Fill))
.on_enter(Message::Dock(crate::ui::dock::DockMsg::HoverStay))
.on_exit(Message::Dock(crate::ui::dock::DockMsg::HoverExit))
.into(),
)
@@ -165,7 +166,11 @@ impl OpenCADStudio {
DockMsg, DropTarget, DOCK_STRIP_W, STRIP_CELL_H, STRIP_DIVIDER_H, STRIP_GRIP_W,
STRIP_PAD, STRIP_PLUS_GAP,
};
let shown = self.dock_shown_group(side);
// Only a group actually on screen is marked as shown: on an
// auto-hiding edge nothing is, until a group is revealed.
let shown = self
.dock_shown_group(side)
.filter(|_| self.dock_edge_expanded(side));
let movement = self.dock_drag.and_then(|d| d.movement());
let move_target = movement.and_then(|(_, _, target)| target);
let dragging = move_target.is_some();
+6
View File
@@ -850,6 +850,10 @@ pub(super) struct OpenCADStudio {
/// floating frame): hovered, being dragged, or just opened. Cleared when
/// the pointer leaves.
pub(crate) dock_peek: Option<crate::ui::dock::PanelId>,
/// Reveal (`Some(pallet)`) or hide (`None`) waiting out the hover delay,
/// with the generation its timer carries; any newer hover supersedes it.
pub(crate) dock_hover_pending: Option<Option<crate::ui::dock::PanelId>>,
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>,
@@ -4291,6 +4295,8 @@ impl OpenCADStudio {
xref_manager: Default::default(),
dock: Default::default(),
dock_peek: None,
dock_hover_pending: None,
dock_hover_gen: 0,
dock_drag: None,
dock_drag_last: None,
dock_title_hover: None,
+4 -20
View File
@@ -2552,28 +2552,12 @@ impl OpenCADStudio {
Message::ScriptLine(line) => self.feed_script_line(&line),
Message::ToggleLayers => {
use crate::ui::dock::PanelId;
if self.show_layers {
return self.on_dock(crate::ui::dock::DockMsg::Close(PanelId::Layers));
return self.on_dock(crate::ui::dock::DockMsg::Close(
crate::ui::dock::PanelId::Layers,
));
}
self.sync_ribbon_layers();
self.show_layers = true;
if !self.dock.is_placed(PanelId::Layers) {
// First use: float it over the middle of the drawing,
// where the dialog used to open.
let (ww, wh) = self.dock_workspace_size();
let (w, h) = self.dock_float_size(PanelId::Layers);
self.dock.float(crate::ui::dock::FloatPanel {
id: PanelId::Layers,
x: ((ww - w) * 0.5).max(0.0),
y: ((wh - h) * 0.5).max(0.0),
w,
h,
});
self.save_config();
}
self.dock.raise_float(PanelId::Layers);
self.dock_reveal(PanelId::Layers);
self.open_layers_pallet();
Task::none()
}