diff --git a/src/app/dock/tests.rs b/src/app/dock/tests.rs index 2e3df1aa1..b8858fa2c 100644 --- a/src/app/dock/tests.rs +++ b/src/app/dock/tests.rs @@ -661,6 +661,62 @@ fn an_open_dropdown_in_a_hover_zone_does_not_read_as_leaving() { }); } +/// Bounds of the field (text `ByLayer`) and of its open list (text `Opaque`) +/// for a `drop_down_below` in a scrollable that, like a pallet, sits away +/// from the window's top-left corner. +fn drop_down_below_in_an_offset_scrollable() -> (iced::Rectangle, iced::Rectangle) { + use iced::widget::{button, column, container, row, scrollable, text, Space}; + use iced_test::core::widget; + use iced_test::runtime::user_interface::{Cache, UserInterface}; + use iced_test::Selector; + let view = || -> iced::Element<'static, Message> { + let field = container(text("ByLayer")).width(200).height(20); + let list = column![ + button(text("Opaque")).width(iced::Fill), + button(text("50")).width(iced::Fill), + ]; + let dd = crate::ui::color_select::drop_down_below( + field.into(), + list.into(), + None, + iced::Length::Shrink, + Message::PropEditChoiceToggle, + ); + let pallet = scrollable(column![Space::new().height(300), dd]).height(500); + row![ + Space::new().width(1000), + container(pallet).width(300).height(iced::Fill) + ] + .into() + }; + let mut renderer = headless_renderer(); + let mut ui = UserInterface::build(view(), VIEW_SIZE, Cache::default(), &mut renderer); + let mut find = |label: &str| { + let mut find = Selector::find(label); + ui.operate(&renderer, &mut widget::operation::black_box(&mut find)); + match widget::Operation::finish(&find) { + widget::operation::Outcome::Some(Some(target)) => target.bounds(), + _ => panic!("{label} not found"), + } + }; + (find("ByLayer"), find("Opaque")) +} + +#[test] +fn a_drop_down_in_a_pallet_opens_right_below_its_field() { + with_stack(|| { + let (field, item) = drop_down_below_in_an_offset_scrollable(); + assert!( + item.y >= field.y + field.height && item.y < field.y + field.height + 20.0, + "list starts below the field: field {field:?}, first item {item:?}" + ); + assert!( + item.x >= field.x - 1.0 && item.x < field.x + field.width, + "list lines up with the field: field {field:?}, first item {item:?}" + ); + }); +} + #[test] fn docked_title_bar_double_click_floats_the_pallet_in_the_real_view() { // Building the real view is deeper than a default test thread's stack. diff --git a/src/ui/color_select/mod.rs b/src/ui/color_select/mod.rs index 445a52bbc..2eb812bf8 100644 --- a/src/ui/color_select/mod.rs +++ b/src/ui/color_select/mod.rs @@ -322,5 +322,5 @@ pub fn drop_down_below<'a>( if let Some(w) = popup_width { dd = dd.width(w); } - dd.into() + crate::ui::drop_down_fit::drop_down_fit(dd) } diff --git a/src/ui/drop_down_fit.rs b/src/ui/drop_down_fit.rs new file mode 100644 index 000000000..127ef93e1 --- /dev/null +++ b/src/ui/drop_down_fit.rs @@ -0,0 +1,131 @@ +//! Keeps an `iced_aw::DropDown` list under its field inside a scrollable. +//! +//! The drop-down keeps its list on screen by testing it against the viewport +//! it is handed, but reads only that viewport's width and height, as if it +//! started at the window's top-left corner. Inside a scrollable the viewport +//! is the scrollable's visible area in window coordinates, so a field in a +//! dock pallet (which does not start at the corner) sees its list "overflow", +//! and the list jumps left by its own width or up over the field. Wrapped +//! here, the drop-down gets a viewport reaching from the window corner to the +//! visible area's far edges: the test then trips only where the list really +//! leaves the visible area. + +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 a drop-down so its list is placed against the visible area's real +/// edges. +pub fn drop_down_fit<'a>(drop_down: impl Into>) -> Element<'a, Message> { + Element::new(DropDownFit { + content: drop_down.into(), + }) +} + +struct DropDownFit<'a> { + content: Element<'a, Message>, +} + +impl Widget for DropDownFit<'_> { + fn tag(&self) -> tree::Tag { + self.content.as_widget().tag() + } + + fn state(&self) -> tree::State { + self.content.as_widget().state() + } + + fn diff(&mut self, tree: &mut Tree) { + self.content.as_widget_mut().diff(tree); + } + + 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(tree, renderer, limits) + } + + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.content + .as_widget_mut() + .operate(tree, 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(tree, event, layout, cursor, renderer, shell, viewport); + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content + .as_widget() + .mouse_interaction(tree, 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, 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> { + let from_corner = Rectangle { + x: 0.0, + y: 0.0, + width: viewport.x + viewport.width, + height: viewport.y + viewport.height, + }; + self.content + .as_widget_mut() + .overlay(tree, layout, renderer, &from_corner, translation) + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 89c69faa6..c894b3abf 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 drop_down_fit; pub mod hover_zone; pub mod icons; pub mod menu_bar;