diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index d394215e4..f799a40b1 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -649,6 +649,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven } let task = self.dock_open_panel(id); self.dock.show_group_of(id); + // Adding a pallet is the end of the menu's job. + self.dock_edge_menu = None; self.save_config(); task } @@ -2438,6 +2440,8 @@ mod tests { 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); @@ -2489,6 +2493,72 @@ mod tests { assert!(app.dock.float_rect(PanelId::Layers).is_some()); } + /// Drive the real view through iced's `UserInterface`: point at the + /// first text `label`, 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, label: &str, events: &[iced::Event]) { + use iced_test::core::{mouse, shell, widget, window, Size}; + use iced_test::runtime::user_interface::{Cache, UserInterface}; + use iced_test::selector::Bounded; + use iced_test::Selector; + let mut renderer = iced_test::futures::futures::executor::block_on( + ::new( + iced_test::core::renderer::Settings::default(), + None, + ), + ) + .expect("headless renderer"); + let size = Size::new(1600.0, 900.0); + let mut cache = Cache::default(); + let mut cursor = mouse::Cursor::Unavailable; + { + let mut ui = UserInterface::build(app.view_main(), size, cache, &mut renderer); + let mut find = Selector::find(label); + ui.operate(&renderer, &mut widget::operation::black_box(&mut find)); + if let widget::operation::Outcome::Some(Some(target)) = widget::Operation::finish(&find) { + if let Some(bounds) = target.visible_bounds() { + cursor = mouse::Cursor::Available(bounds.center()); + } + } + cache = ui.into_cache(); + } + assert!(matches!(cursor, mouse::Cursor::Available(_)), "{label} not found"); + for event in events { + let mut messages = Vec::new(); + { + let mut ui = UserInterface::build(app.view_main(), 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. + drive_view(&mut app, "Blocks", &[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 dock_double_clicks_float_and_dock_panels() { use crate::app::config::DockSide; diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index eb069b62d..160c1683e 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1969,14 +1969,14 @@ bg={bg_ms:.1}ms n={view_count}" if let (Some(id), Some(target)) = (self.dock_dragging, self.dock_drag_target) { layers.push(self.dock_drop_preview(id, target)); } - let workspace: Element<'_, Message> = if layers.len() == 1 { - layers.pop().expect("workspace layer") - } else { - iced::widget::Stack::with_children(layers) - .width(Fill) - .height(Fill) - .into() - }; + // Always a stack (and, below, always a mouse area), even with nothing + // on top: iced keeps a widget's state only while the tree keeps its + // shape, and a title bar's double-click needs its first click to + // survive the drag that pressing the bar starts. + let workspace: Element<'_, Message> = iced::widget::Stack::with_children(layers) + .width(Fill) + .height(Fill) + .into(); let any_dragging = self.dock_dragging.is_some(); let any_splitting = self.dock_split_drag.is_some(); let any_resizing = self.dock_resizing.is_some() @@ -1984,8 +1984,10 @@ bg={bg_ms:.1}ms n={view_count}" || self.layer_col_dragging || self.xref_col_drag.is_some() || self.xref_split_drag; - let workspace: Element<'_, Message> = if any_dragging || any_resizing || any_splitting { - mouse_area(workspace) + let capturing = any_dragging || any_resizing || any_splitting; + let mut capture = mouse_area(workspace); + if capturing { + capture = capture .on_move(move |p| Message::Dock(crate::ui::dock::DockMsg::DragMove(p))) .on_release(Message::Dock(crate::ui::dock::DockMsg::DragRelease)) .interaction(if let Some((_, from_left)) = self.dock_float_resizing { @@ -2000,11 +2002,9 @@ bg={bg_ms:.1}ms n={view_count}" iced::mouse::Interaction::ResizingVertically } else { iced::mouse::Interaction::Grabbing - }) - .into() - } else { - workspace - }; + }); + } + let workspace: Element<'_, Message> = capture.into(); let command_line = self.command_line.view( allow_autocomplete, dyn_capturing, @@ -2905,8 +2905,16 @@ impl OpenCADStudio { } // The group grip: a band on the window side holding the edge bar. + // Explicit heights: the tooltip around the band sizes it to its + // content, so a `Fill` bar would stop short of the row's bottom. let bar_w = if lit { 5.0 } else { 3.0 }; - let bar = container(Space::new().width(bar_w).height(Fill)).style(move |theme: &Theme| { + let bar_inset = 5.0; + let bar = container( + Space::new() + .width(bar_w) + .height(Length::Fixed(row_h - 2.0 * bar_inset)), + ) + .style(move |theme: &Theme| { let palette = theme.palette(); let (color, glow) = if lit { (Some(palette.primary.weak.color), true) @@ -2939,13 +2947,8 @@ impl OpenCADStudio { let band = mouse_area( container(bar) .width(Length::Fixed(STRIP_GRIP_W)) - .height(Fill) - .padding(iced::Padding { - top: 5.0, - bottom: 5.0, - left: 0.0, - right: 0.0, - }) + .height(Length::Fixed(row_h)) + .align_y(iced::alignment::Vertical::Center) .align_x(match side { DockSide::Left => iced::alignment::Horizontal::Left, DockSide::Right => iced::alignment::Horizontal::Right, @@ -2963,7 +2966,7 @@ impl OpenCADStudio { .gap(4); let band = container(band) .width(Fill) - .height(Fill) + .height(Length::Fixed(row_h)) .align_x(match side { DockSide::Left => iced::alignment::Horizontal::Left, DockSide::Right => iced::alignment::Horizontal::Right,