diff --git a/src/app/dock/update.rs b/src/app/dock/update.rs index 514ab2f50..bbbf46ff5 100644 --- a/src/app/dock/update.rs +++ b/src/app/dock/update.rs @@ -207,12 +207,12 @@ impl OpenCADStudio { Some(HoverPending::Hide) if self.dock_peek.is_some_and(|id| self.dock.auto_hides(id)) => { - // A dropdown or the colour window opened from the - // panel draws as an overlay, which the edge's - // mouse area reads as the pointer leaving. Keep - // the panel out until it closes, so a pick can - // still land; then hide unless the pointer is - // back (its re-entry cancels this). + // The edge's hover zone keeps a dropdown inside + // it from reading as a leave; a colour popup can + // still reach past it (or be the Select Color + // window). Keep the panel out until it closes, so + // a pick can still land; then hide unless the + // pointer is back (its re-entry cancels this). if self.dock_popup_open() { return self.dock_hover_arm(HoverPending::Hide); } diff --git a/src/app/dock/view.rs b/src/app/dock/view.rs index a1735fd93..2bb0e5e5f 100644 --- a/src/app/dock/view.rs +++ b/src/app/dock/view.rs @@ -164,12 +164,13 @@ impl OpenCADStudio { children.push(strip); } } - 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(), - ) + // Not a `mouse_area`: the pointer on one of the pallet's own + // dropdowns must not read as leaving the edge. + Some(crate::ui::hover_zone::hover_zone( + row(children).height(Fill), + Message::Dock(crate::ui::dock::DockMsg::HoverStay), + Message::Dock(crate::ui::dock::DockMsg::HoverExit), + )) } /// An edge's vertical icon strip: per group its open pallets' icons in a @@ -555,10 +556,14 @@ impl OpenCADStudio { // on its empty areas don't fall through to the drawing below. let panel = mouse_area(framed) .on_press(Message::Dock(DockMsg::FloatRaise(f.id))) - .on_enter(Message::Dock(DockMsg::Hover(f.id))) - .on_exit(Message::Dock(DockMsg::HoverExit)) .interaction(iced::mouse::Interaction::Idle); - place_at(panel.into(), x, y) + // Enter / leave as on a docked edge: its dropdowns keep it shown. + let panel = crate::ui::hover_zone::hover_zone( + panel, + Message::Dock(DockMsg::Hover(f.id)), + Message::Dock(DockMsg::HoverExit), + ); + place_at(panel, x, y) } /// Where floating panel `f` draws. diff --git a/src/ui/hover_zone.rs b/src/ui/hover_zone.rs new file mode 100644 index 000000000..38c4a17e7 --- /dev/null +++ b/src/ui/hover_zone.rs @@ -0,0 +1,175 @@ +//! A pointer-enter / leave area that keeps counting the pointer as inside +//! while it rests on an overlay (a dropdown, a combo box list) drawn over +//! its bounds. +//! +//! `mouse_area` reads such a pointer as gone: iced hands the layer below an +//! overlay a levitating cursor, which has no position. An auto-hiding dock +//! edge then hides as soon as one of its own dropdowns opens under the +//! pointer, taking the dropdown with it. Here the cursor is landed before the +//! bounds test, so only a pointer that really leaves the area exits it. + +use iced::advanced::layout::{self, Layout}; +use iced::advanced::widget::{self, tree, Tree, Widget}; +use iced::advanced::{mouse, overlay, renderer, Shell}; +use iced::{Element, Event, Length, Rectangle, Renderer, Size, Theme, Vector}; + +use crate::app::Message; + +/// Wrap `content`, publishing `on_enter` / `on_exit` as the pointer enters +/// and leaves its bounds, overlays drawn over them included. +pub fn hover_zone<'a>( + content: impl Into>, + on_enter: Message, + on_exit: Message, +) -> Element<'a, Message> { + Element::new(HoverZone { + content: content.into(), + on_enter, + on_exit, + }) +} + +struct HoverZone<'a> { + content: Element<'a, Message>, + on_enter: Message, + on_exit: Message, +} + +#[derive(Default)] +struct State { + hovered: bool, +} + +impl Widget for HoverZone<'_> { + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)); + } + + fn size(&self) -> Size { + self.content.as_widget().size() + } + + fn layout( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content + .as_widget_mut() + .layout(&mut tree.children[0], renderer, limits) + } + + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.content + .as_widget_mut() + .operate(&mut tree.children[0], layout, renderer, operation); + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + shell, + viewport, + ); + // Unlike `mouse_area`, this runs for captured events too: a pointer + // move over a dropdown is captured by the dropdown but must still + // not read as leaving. + let hovered = cursor.land().is_over(layout.bounds()); + let state = tree.state.downcast_mut::(); + if hovered != state.hovered { + state.hovered = hovered; + shell.publish(if hovered { + self.on_enter.clone() + } else { + self.on_exit.clone() + }); + } + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'b>, + renderer: &Renderer, + viewport: &Rectangle, + translation: Vector, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + viewport, + translation, + ) + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 4b7608424..89c69faa6 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -5,6 +5,7 @@ pub const ROW_H: f32 = 26.0; pub mod color_select; pub mod command_line; pub mod dock; +pub mod hover_zone; pub mod icons; pub mod menu_bar; pub mod modal;