diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ab4874c8e..249653d52c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ You can find its changes [documented below](#070---2021-01-01). - Add #[data(eq)] shorthand attribute for Data derive macro ([#1884] by [@Maan2003]) - X11: detect keyboard layout ([#1779] by [@Maan2003]) - WindowDesc::with_config ([#1929] by [@Maan2003]) +- `scroll_to_view` and `scroll_area_to_view` methods on `UpdateCtx`, `LifecycleCtx` and `EventCtx` ([#1976] by [@xarvic]) - `Notification::route` ([#1978] by [@xarvic]) - Build on OpenBSD ([#1993] by [@klemensn]) @@ -798,6 +799,7 @@ Last release without a changelog :( [#1947]: https://github.com/linebender/druid/pull/1947 [#1953]: https://github.com/linebender/druid/pull/1953 [#1967]: https://github.com/linebender/druid/pull/1967 +[#1976]: https://github.com/linebender/druid/pull/1976 [#1978]: https://github.com/linebender/druid/pull/1978 [#1993]: https://github.com/linebender/druid/pull/1993 diff --git a/druid/src/command.rs b/druid/src/command.rs index 2cb9129793..07c7027f7a 100644 --- a/druid/src/command.rs +++ b/druid/src/command.rs @@ -178,7 +178,7 @@ pub mod sys { use super::Selector; use crate::{ sub_window::{SubWindowDesc, SubWindowUpdate}, - FileDialogOptions, FileInfo, SingleUse, WidgetId, WindowConfig, + FileDialogOptions, FileInfo, Rect, SingleUse, WidgetId, WindowConfig, }; /// Quit the running application. This command is handled by the druid library. @@ -329,6 +329,22 @@ pub mod sys { pub(crate) const INVALIDATE_IME: Selector = Selector::new("druid-builtin.invalidate-ime"); + /// Informs this widget, that a child wants a specific region to be shown. The payload is the + /// requested region in global coordinates. + /// + /// This notification is send when [`scroll_to_view`] or [`scroll_area_to_view`] + /// are called. + /// + /// Widgets which hide their children, should always call `ctx.set_handled()` in response to + /// avoid unintended behaviour from widgets further down the tree. + /// If possible the widget should move its children to bring the area into view and then submit + /// a new notification with the region translated by the amount, the child it contained was + /// translated. + /// + /// [`scroll_to_view`]: crate::EventCtx::scroll_to_view() + /// [`scroll_area_to_view`]: crate::EventCtx::scroll_area_to_view() + pub const SCROLL_TO_VIEW: Selector = Selector::new("druid-builtin.scroll-to"); + /// A change that has occured to text state, and needs to be /// communicated to the platform. pub(crate) struct ImeInvalidation { diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index 4f273c1c7d..6d71a3223e 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -23,6 +23,7 @@ use std::{ }; use tracing::{error, trace, warn}; +use crate::commands::SCROLL_TO_VIEW; use crate::core::{CommandQueue, CursorChange, FocusChange, WidgetState}; use crate::env::KeyLike; use crate::menu::ContextMenu; @@ -455,6 +456,20 @@ impl_context_method!(EventCtx<'_, '_>, UpdateCtx<'_, '_>, LifeCycleCtx<'_, '_>, self.submit_command(commands::NEW_SUB_WINDOW.with(SingleUse::new(req))); window_id } + + /// Scrolls this widget into view. + /// + /// If this widget is only partially visible or not visible at all because of [`Scroll`]s + /// it is wrapped in, they will do the minimum amount of scrolling necessary to bring this + /// widget fully into view. + /// + /// If the widget is [`hidden`], this method has no effect. + /// + /// [`Scroll`]: crate::widget::Scroll + /// [`hidden`]: crate::Event::should_propagate_to_hidden + pub fn scroll_to_view(&mut self) { + self.scroll_area_to_view(self.size().to_rect()) + } }); // methods on everyone but paintctx @@ -688,6 +703,21 @@ impl EventCtx<'_, '_> { trace!("request_update"); self.widget_state.request_update = true; } + + /// Scrolls the area into view. + /// + /// If the area is only partially visible or not visible at all because of [`Scroll`]s + /// this widget is wrapped in, they will do the minimum amount of scrolling necessary to + /// bring the area fully into view. + /// + /// If the widget is [`hidden`], this method has no effect. + /// + /// [`Scroll`]: crate::widget::Scroll + /// [`hidden`]: crate::Event::should_propagate_to_hidden + pub fn scroll_area_to_view(&mut self, area: Rect) { + //TODO: only do something if this widget is not hidden + self.submit_notification(SCROLL_TO_VIEW.with(area + self.window_origin().to_vec2())); + } } impl UpdateCtx<'_, '_> { @@ -727,6 +757,25 @@ impl UpdateCtx<'_, '_> { None => false, } } + + /// Scrolls the area into view. + /// + /// If the area is only partially visible or not visible at all because of [`Scroll`]s + /// this widget is wrapped in, they will do the minimum amount of scrolling necessary to + /// bring the area fully into view. + /// + /// If the widget is [`hidden`], this method has no effect. + /// + /// [`Scroll`]: crate::widget::Scroll + /// [`hidden`]: crate::Event::should_propagate_to_hidden + pub fn scroll_area_to_view(&mut self, area: Rect) { + //TODO: only do something if this widget is not hidden + self.submit_command(Command::new( + SCROLL_TO_VIEW, + area + self.window_origin().to_vec2(), + self.widget_id(), + )); + } } impl LifeCycleCtx<'_, '_> { @@ -762,6 +811,25 @@ impl LifeCycleCtx<'_, '_> { }; self.state.text_registrations.push(registration); } + + /// Scrolls the area into view. + /// + /// If the area is only partially visible or not visible at all because of [`Scroll`]s + /// this widget is wrapped in, they will do the minimum amount of scrolling necessary to + /// bring the area fully into view. + /// + /// If the widget is [`hidden`], this method has no effect. + /// + /// [`Scroll`]: crate::widget::Scroll + /// [`hidden`]: crate::Event::should_propagate_to_hidden + pub fn scroll_area_to_view(&mut self, area: Rect) { + //TODO: only do something if this widget is not hidden + self.submit_command( + SCROLL_TO_VIEW + .with(area + self.window_origin().to_vec2()) + .to(self.widget_id()), + ); + } } impl LayoutCtx<'_, '_> { diff --git a/druid/src/core.rs b/druid/src/core.rs index 7553b20f02..a06975d7a3 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -19,6 +19,7 @@ use tracing::{info_span, trace, warn}; use crate::bloom::Bloom; use crate::command::sys::{CLOSE_WINDOW, SUB_WINDOW_HOST_TO_PARENT, SUB_WINDOW_PARENT_TO_HOST}; +use crate::commands::SCROLL_TO_VIEW; use crate::contexts::ContextState; use crate::kurbo::{Affine, Insets, Point, Rect, Shape, Size, Vec2}; use crate::sub_window::SubWindowUpdate; @@ -843,6 +844,13 @@ impl> WidgetPod { } ctx.is_handled = true } + Event::Command(cmd) if cmd.is(SCROLL_TO_VIEW) => { + // Submit the SCROLL_TO notification if it was used from a update or lifecycle + // call. + let rect = cmd.get_unchecked(SCROLL_TO_VIEW); + inner_ctx.submit_notification(SCROLL_TO_VIEW.with(*rect)); + ctx.is_handled = true; + } _ => { self.inner.event(&mut inner_ctx, inner_event, data, env); diff --git a/druid/src/widget/clip_box.rs b/druid/src/widget/clip_box.rs index df2a73d49e..eb86285e30 100644 --- a/druid/src/widget/clip_box.rs +++ b/druid/src/widget/clip_box.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::commands::SCROLL_TO_VIEW; use crate::debug_state::DebugState; use crate::kurbo::{Affine, Point, Rect, Size, Vec2}; use crate::widget::prelude::*; @@ -24,7 +25,7 @@ use tracing::{instrument, trace}; pub struct Viewport { /// The size of the area that we have a viewport into. pub content_size: Size, - /// The origin of the view rectangle. + /// The origin of the view rectangle, relative to the content. pub view_origin: Point, /// The size of the view rectangle. pub view_size: Size, @@ -308,6 +309,38 @@ impl> ClipBox { self.child .set_viewport_offset(self.viewport_origin().to_vec2()); } + + /// The default handling of the [`SCROLL_TO_VIEW`] notification for a scrolling container. + /// + /// The [`SCROLL_TO_VIEW`] notification is send when [`scroll_to_view`] or [`scroll_area_to_view`] + /// are called. + /// + /// [`SCROLL_TO_VIEW`]: crate::commands::SCROLL_TO_VIEW + /// [`scroll_to_view`]: crate::EventCtx::scroll_to_view() + /// [`scroll_area_to_view`]: crate::EventCtx::scroll_area_to_view() + pub fn default_scroll_to_view_handling( + &mut self, + ctx: &mut EventCtx, + global_highlight_rect: Rect, + ) -> bool { + let mut viewport_changed = false; + self.with_port(|port| { + let global_content_offset = ctx.window_origin().to_vec2() - port.view_origin.to_vec2(); + let content_highlight_rect = global_highlight_rect - global_content_offset; + + if port.pan_to_visible(content_highlight_rect) { + ctx.request_paint(); + viewport_changed = true; + } + + // This is a new value since view_origin has changed in the meantime + let global_content_offset = ctx.window_origin().to_vec2() - port.view_origin.to_vec2(); + ctx.submit_notification( + SCROLL_TO_VIEW.with(content_highlight_rect + global_content_offset), + ); + }); + viewport_changed + } } impl> Widget for ClipBox { diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index 50e333b084..c0a9c0165e 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -14,6 +14,7 @@ //! A container that scrolls its contents. +use crate::commands::SCROLL_TO_VIEW; use crate::debug_state::DebugState; use crate::widget::prelude::*; use crate::widget::{Axis, ClipBox}; @@ -184,9 +185,27 @@ impl> Widget for Scroll { self.clip.event(ctx, event, data, env); } + // Handle scroll after the inner widget processed the events, to prefer inner widgets while + // scrolling. self.clip.with_port(|port| { scroll_component.handle_scroll(port, ctx, event, env); }); + + if !self.scroll_component.are_bars_held() { + // We only scroll to the component if the user is not trying to move the scrollbar. + if let Event::Notification(notification) = event { + if let Some(&global_highlight_rect) = notification.get(SCROLL_TO_VIEW) { + ctx.set_handled(); + let view_port_changed = self + .clip + .default_scroll_to_view_handling(ctx, global_highlight_rect); + if view_port_changed { + self.scroll_component + .reset_scrollbar_fade(|duration| ctx.request_timer(duration), env); + } + } + } + } } #[instrument(name = "Scroll", level = "trace", skip(self, ctx, event, data, env))] diff --git a/druid/src/widget/textbox.rs b/druid/src/widget/textbox.rs index 3c2fad75eb..7303508e62 100644 --- a/druid/src/widget/textbox.rs +++ b/druid/src/widget/textbox.rs @@ -515,6 +515,7 @@ impl Widget for TextBox { self.reset_cursor_blink(ctx.request_timer(CURSOR_BLINK_DURATION)); self.was_focused_from_click = false; ctx.request_paint(); + ctx.scroll_to_view(); } LifeCycle::FocusChanged(false) => { if self.text().can_write() && MAC_OR_LINUX_OR_OBSD && !self.multiline {