From 873dd54ca5f865f8f7264a37c76a739545329971 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 18 Sep 2021 12:05:20 +0200 Subject: [PATCH 01/17] added SCROLL_TO selector, implemented handling of SCROLL_TO notification in Scroll, improved documentation Signed-off-by: Christoph --- druid/src/widget/clip_box.rs | 2 +- druid/src/widget/scroll.rs | 45 +++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/druid/src/widget/clip_box.rs b/druid/src/widget/clip_box.rs index df2a73d49e..aa63de06f1 100644 --- a/druid/src/widget/clip_box.rs +++ b/druid/src/widget/clip_box.rs @@ -24,7 +24,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, diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index 50e333b084..785fdc8a6b 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -17,7 +17,7 @@ use crate::debug_state::DebugState; use crate::widget::prelude::*; use crate::widget::{Axis, ClipBox}; -use crate::{scroll_component::*, Data, Rect, Vec2}; +use crate::{scroll_component::*, Data, Rect, Vec2, Selector}; use tracing::{instrument, trace}; /// A container that scrolls its contents. @@ -38,6 +38,8 @@ pub struct Scroll { } impl> Scroll { + const SCROLL_TO: Selector = Selector::new("org.linebender.druid.scroll.scroll_to"); + /// Create a new scroll container. /// /// This method will allow scrolling in all directions if child's bounds @@ -184,9 +186,50 @@ 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(Self::SCROLL_TO) { + ctx.set_handled(); + + self.clip.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; + let view_rect = Rect::from_origin_size(port.view_origin, port.view_size); + + let mut new_origin = port.view_origin; + if content_highlight_rect.x0 < view_rect.x0 || content_highlight_rect.size().width > port.view_size.width { + //Prefer the left over the right side if the scroll_to content is bigger than the view_size + new_origin.x = content_highlight_rect.x0; + } else if content_highlight_rect.x1 > view_rect.x1 { + new_origin.x = content_highlight_rect.x1 - port.view_size.width; + } + if content_highlight_rect.y0 < view_rect.y0 || content_highlight_rect.size().height > port.view_size.height { + //Prefer the upper over the lower side if the scroll_to content is bigger than the view_size + new_origin.y = content_highlight_rect.y0; + } else if content_highlight_rect.y1 > view_rect.y1 { + new_origin.y = content_highlight_rect.y1 - port.view_size.height; + } + + if port.pan_to(new_origin) { + ctx.request_paint(); + } + + // 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(Self::SCROLL_TO.with(content_highlight_rect + global_content_offset)); + }); + } + } + } } #[instrument(name = "Scroll", level = "trace", skip(self, ctx, event, data, env))] From 8c2fe0038c3ef297bf244b17138322ed9ec623b0 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 18 Sep 2021 12:43:58 +0200 Subject: [PATCH 02/17] added context methods `scroll_to_view` and `scroll_area_to_view` Signed-off-by: Christoph --- druid/src/contexts.rs | 30 ++++++++++++++++++++++++++++++ druid/src/core.rs | 11 +++++++++++ druid/src/widget/scroll.rs | 5 ++++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index f9fad558c5..bf05e82d81 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -35,6 +35,7 @@ use crate::{ ExtEventSink, Insets, Menu, Notification, Point, Rect, SingleUse, Size, Target, TimerToken, Vec2, WidgetId, WindowConfig, WindowDesc, WindowHandle, WindowId, }; +use crate::widget::Scroll; /// A macro for implementing methods on multiple contexts. /// @@ -478,6 +479,35 @@ impl_context_method!( self.state.submit_command(cmd.into()) } + /// 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`]: widget::Scroll + /// [`hidden`]: Event::should_propagate_to_hidden + pub fn scroll_to_view(&mut self) { + self.scroll_area_to_view(self.size().to_rect()) + } + + /// 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`]: widget::Scroll + /// [`hidden`]: 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::SCROLL_TO.with(area + self.window_origin().to_vec2())); + } + /// Returns an [`ExtEventSink`] that can be moved between threads, /// and can be used to submit commands back to the application. /// diff --git a/druid/src/core.rs b/druid/src/core.rs index 149ab82832..0f40c31e1f 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -29,6 +29,7 @@ use crate::{ InternalLifeCycle, LayoutCtx, LifeCycle, LifeCycleCtx, Notification, PaintCtx, Region, RenderContext, Target, TextLayout, TimerToken, UpdateCtx, Widget, WidgetId, WindowId, }; +use crate::widget::Scroll; /// Our queue type pub(crate) type CommandQueue = VecDeque; @@ -849,6 +850,16 @@ impl> WidgetPod { } ctx.is_handled = true } + Event::Command(cmd) if cmd.is(Scroll::SCROLL_TO) => { + if let Some(rect) = cmd + .get_unchecked(Scroll::SCROLL_TO) + .downcast_ref::() + { + *data = (*update).clone(); + } + ctx.is_handled = true; + inner_ctx.submit_notification(Scroll::SCROLL_TO.with(*rect)); + } _ => { self.inner.event(&mut inner_ctx, inner_event, data, env); diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index 785fdc8a6b..a3d96edf34 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -38,7 +38,7 @@ pub struct Scroll { } impl> Scroll { - const SCROLL_TO: Selector = Selector::new("org.linebender.druid.scroll.scroll_to"); + pub const SCROLL_TO: Selector = Selector::new("org.linebender.druid.scroll.scroll_to"); /// Create a new scroll container. /// @@ -204,6 +204,9 @@ impl> Widget for Scroll { let view_rect = Rect::from_origin_size(port.view_origin, port.view_size); let mut new_origin = port.view_origin; + //TODO: decide whether the scroll should pan to the upper-left corner of the + // requested area if the view area is smaller than the requested area and + // already inside of it. if content_highlight_rect.x0 < view_rect.x0 || content_highlight_rect.size().width > port.view_size.width { //Prefer the left over the right side if the scroll_to content is bigger than the view_size new_origin.x = content_highlight_rect.x0; From e61e75704e12cd5da3565565d7d32859b7261a30 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 18 Sep 2021 12:55:07 +0200 Subject: [PATCH 03/17] removed scroll_to_* methods from LayoutCtx Signed-off-by: Christoph --- druid/src/contexts.rs | 58 +++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index bf05e82d81..0544b38ed1 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -454,6 +454,35 @@ 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`]: widget::Scroll + /// [`hidden`]: Event::should_propagate_to_hidden + pub fn scroll_to_view(&mut self) { + self.scroll_area_to_view(self.size().to_rect()) + } + + /// 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`]: widget::Scroll + /// [`hidden`]: 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::SCROLL_TO.with(area + self.window_origin().to_vec2())); + } }); // methods on everyone but paintctx @@ -479,35 +508,6 @@ impl_context_method!( self.state.submit_command(cmd.into()) } - /// 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`]: widget::Scroll - /// [`hidden`]: Event::should_propagate_to_hidden - pub fn scroll_to_view(&mut self) { - self.scroll_area_to_view(self.size().to_rect()) - } - - /// 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`]: widget::Scroll - /// [`hidden`]: 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::SCROLL_TO.with(area + self.window_origin().to_vec2())); - } - /// Returns an [`ExtEventSink`] that can be moved between threads, /// and can be used to submit commands back to the application. /// From 7f9f82ce97c5139686415e995e09e924874f9428 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 18 Sep 2021 15:42:18 +0200 Subject: [PATCH 04/17] fixed logic errors, refactored scroll.rs Signed-off-by: Christoph --- druid/src/command.rs | 7 ++-- druid/src/contexts.rs | 3 +- druid/src/core.rs | 19 +++++----- druid/src/widget/scroll.rs | 73 ++++++++++++++++++++------------------ 4 files changed, 54 insertions(+), 48 deletions(-) diff --git a/druid/src/command.rs b/druid/src/command.rs index fa40d58dff..b021e1134f 100644 --- a/druid/src/command.rs +++ b/druid/src/command.rs @@ -175,10 +175,7 @@ pub mod sys { use std::any::Any; use super::Selector; - use crate::{ - sub_window::{SubWindowDesc, SubWindowUpdate}, - FileDialogOptions, FileInfo, SingleUse, WidgetId, WindowConfig, - }; + use crate::{sub_window::{SubWindowDesc, SubWindowUpdate}, FileDialogOptions, FileInfo, SingleUse, WidgetId, WindowConfig, Rect}; /// Quit the running application. This command is handled by the druid library. pub const QUIT_APP: Selector = Selector::new("druid-builtin.quit-app"); @@ -328,6 +325,8 @@ pub mod sys { pub(crate) const INVALIDATE_IME: Selector = Selector::new("druid-builtin.invalidate-ime"); + pub const SCROLL_TO: 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 0544b38ed1..701033ebf4 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -36,6 +36,7 @@ use crate::{ Vec2, WidgetId, WindowConfig, WindowDesc, WindowHandle, WindowId, }; use crate::widget::Scroll; +use crate::commands::SCROLL_TO; /// A macro for implementing methods on multiple contexts. /// @@ -481,7 +482,7 @@ impl_context_method!(EventCtx<'_, '_>, UpdateCtx<'_, '_>, LifeCycleCtx<'_, '_>, /// [`hidden`]: 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::SCROLL_TO.with(area + self.window_origin().to_vec2())); + self.submit_command(Command::new(SCROLL_TO, area + self.window_origin().to_vec2(), self.widget_id())); } }); diff --git a/druid/src/core.rs b/druid/src/core.rs index 0f40c31e1f..978dcbf559 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -30,6 +30,7 @@ use crate::{ RenderContext, Target, TextLayout, TimerToken, UpdateCtx, Widget, WidgetId, WindowId, }; use crate::widget::Scroll; +use crate::commands::SCROLL_TO; /// Our queue type pub(crate) type CommandQueue = VecDeque; @@ -850,15 +851,12 @@ impl> WidgetPod { } ctx.is_handled = true } - Event::Command(cmd) if cmd.is(Scroll::SCROLL_TO) => { - if let Some(rect) = cmd - .get_unchecked(Scroll::SCROLL_TO) - .downcast_ref::() - { - *data = (*update).clone(); - } + Event::Command(cmd) if cmd.is(SCROLL_TO) => { + // Submit the SCROLL_TO notification if it was used from a update or lifecycle + // call. + let rect = cmd.get_unchecked(SCROLL_TO); + inner_ctx.submit_notification(SCROLL_TO.with(*rect)); ctx.is_handled = true; - inner_ctx.submit_notification(Scroll::SCROLL_TO.with(*rect)); } _ => { self.inner.event(&mut inner_ctx, inner_event, data, env); @@ -979,6 +977,11 @@ impl> WidgetPod { if let Some(change) = this_changed { self.state.has_focus = change; extra_event = Some(LifeCycle::FocusChanged(change)); + + if change == true { + //TODO: decide whether this should be done manually + ctx.scroll_to_view(); + } } else { self.state.has_focus = false; } diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index a3d96edf34..1156e228e5 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -16,9 +16,10 @@ use crate::debug_state::DebugState; use crate::widget::prelude::*; -use crate::widget::{Axis, ClipBox}; -use crate::{scroll_component::*, Data, Rect, Vec2, Selector}; +use crate::widget::{Axis, ClipBox, Viewport}; +use crate::{scroll_component::*, Data, Rect, Vec2, Selector, Key}; use tracing::{instrument, trace}; +use crate::commands::SCROLL_TO; /// A container that scrolls its contents. /// @@ -38,8 +39,6 @@ pub struct Scroll { } impl> Scroll { - pub const SCROLL_TO: Selector = Selector::new("org.linebender.druid.scroll.scroll_to"); - /// Create a new scroll container. /// /// This method will allow scrolling in all directions if child's bounds @@ -195,40 +194,11 @@ impl> Widget for Scroll { 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(Self::SCROLL_TO) { + if let Some(&global_highlight_rect) = notification.get(SCROLL_TO) { ctx.set_handled(); self.clip.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; - let view_rect = Rect::from_origin_size(port.view_origin, port.view_size); - - let mut new_origin = port.view_origin; - //TODO: decide whether the scroll should pan to the upper-left corner of the - // requested area if the view area is smaller than the requested area and - // already inside of it. - if content_highlight_rect.x0 < view_rect.x0 || content_highlight_rect.size().width > port.view_size.width { - //Prefer the left over the right side if the scroll_to content is bigger than the view_size - new_origin.x = content_highlight_rect.x0; - } else if content_highlight_rect.x1 > view_rect.x1 { - new_origin.x = content_highlight_rect.x1 - port.view_size.width; - } - if content_highlight_rect.y0 < view_rect.y0 || content_highlight_rect.size().height > port.view_size.height { - //Prefer the upper over the lower side if the scroll_to content is bigger than the view_size - new_origin.y = content_highlight_rect.y0; - } else if content_highlight_rect.y1 > view_rect.y1 { - new_origin.y = content_highlight_rect.y1 - port.view_size.height; - } - - if port.pan_to(new_origin) { - ctx.request_paint(); - } - - // 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(Self::SCROLL_TO.with(content_highlight_rect + global_content_offset)); + default_scroll_to_view_handling(ctx, port, global_highlight_rect); }); } } @@ -292,3 +262,36 @@ fn log_size_warnings(size: Size) { tracing::warn!("Scroll widget's child has an infinite height."); } } + +pub fn default_scroll_to_view_handling(ctx: &mut EventCtx, port: &mut Viewport, global_highlight_rect: Rect) { + let global_content_offset = ctx.window_origin().to_vec2() - port.view_origin.to_vec2(); + let content_highlight_rect = global_highlight_rect - global_content_offset; + let view_rect = Rect::from_origin_size(port.view_origin, port.view_size); + + let mut new_origin = port.view_origin; + //TODO: decide whether the scroll should pan to the upper-left corner of the + // requested area if the view area is smaller than the requested area and + // already inside of it. + if content_highlight_rect.x0 < view_rect.x0 || content_highlight_rect.size().width > port.view_size.width { + //Prefer the left over the right side if the scroll_to content is bigger than the view_size + new_origin.x = content_highlight_rect.x0; + } else if content_highlight_rect.x1 > view_rect.x1 { + new_origin.x = content_highlight_rect.x1 - port.view_size.width; + } + if content_highlight_rect.y0 < view_rect.y0 || content_highlight_rect.size().height > port.view_size.height { + //Prefer the upper over the lower side if the scroll_to content is bigger than the view_size + new_origin.y = content_highlight_rect.y0; + } else if content_highlight_rect.y1 > view_rect.y1 { + new_origin.y = content_highlight_rect.y1 - port.view_size.height; + } + + if port.pan_to(new_origin) { + ctx.request_paint(); + } + + // 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.with(content_highlight_rect + global_content_offset)); +} From 5f4f02fda33e8ccc7b2ac42a6d34ff80702bc978 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 18 Sep 2021 16:05:03 +0200 Subject: [PATCH 05/17] updated CHANGELOG.md Signed-off-by: Christoph --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d1bb0d567..85a9e5dadf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,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]) ### Changed @@ -784,6 +785,7 @@ Last release without a changelog :( [#1886]: https://github.com/linebender/druid/pull/1886 [#1907]: https://github.com/linebender/druid/pull/1907 [#1929]: https://github.com/linebender/druid/pull/1929 +[#1976]: https://github.com/linebender/druid/pull/1976 [Unreleased]: https://github.com/linebender/druid/compare/v0.7.0...master [0.7.0]: https://github.com/linebender/druid/compare/v0.6.0...v0.7.0 From 33965b24149318df3a8e519f631148570a85d08d Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 18 Sep 2021 16:07:45 +0200 Subject: [PATCH 06/17] import default_scroll_to_view_handling Signed-off-by: Christoph --- druid/src/widget/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 8d0879d43f..19d2b49b8f 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -89,7 +89,7 @@ pub use parse::Parse; pub use progress_bar::ProgressBar; pub use radio::{Radio, RadioGroup}; pub use scope::{DefaultScopePolicy, LensScopeTransfer, Scope, ScopePolicy, ScopeTransfer}; -pub use scroll::Scroll; +pub use scroll::{Scroll, default_scroll_to_view_handling}; pub use sized_box::SizedBox; pub use slider::Slider; pub use spinner::Spinner; From ffd026a6e5279a0f0afd10e504ed11ffcbb25b21 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 18 Sep 2021 16:12:07 +0200 Subject: [PATCH 07/17] add a different implementation for `scroll_area_to_view` in EventCtx, to make it more efficient Signed-off-by: Christoph --- druid/src/contexts.rs | 60 ++++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index 701033ebf4..25c91aff8f 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -469,21 +469,6 @@ impl_context_method!(EventCtx<'_, '_>, UpdateCtx<'_, '_>, LifeCycleCtx<'_, '_>, pub fn scroll_to_view(&mut self) { self.scroll_area_to_view(self.size().to_rect()) } - - /// 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`]: widget::Scroll - /// [`hidden`]: 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, area + self.window_origin().to_vec2(), self.widget_id())); - } }); // methods on everyone but paintctx @@ -717,6 +702,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`]: widget::Scroll + /// [`hidden`]: 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.with(area + self.window_origin().to_vec2())); + } } impl UpdateCtx<'_, '_> { @@ -756,6 +756,21 @@ 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`]: widget::Scroll + /// [`hidden`]: 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, area + self.window_origin().to_vec2(), self.widget_id())); + } } impl LifeCycleCtx<'_, '_> { @@ -791,6 +806,21 @@ impl LifeCycleCtx<'_, '_> { }; self.widget_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`]: widget::Scroll + /// [`hidden`]: 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, area + self.window_origin().to_vec2(), self.widget_id())); + } } impl LayoutCtx<'_, '_> { From c93ffbecd6db74f4b773099ab6c0b4fa1ed74e08 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 18 Sep 2021 17:03:04 +0200 Subject: [PATCH 08/17] docs and refactoring Signed-off-by: Christoph --- druid/src/command.rs | 21 +++++++- druid/src/contexts.rs | 23 ++++++--- druid/src/core.rs | 11 ++--- druid/src/scroll_component.rs | 58 ++++++++++++++++++++-- druid/src/widget/mod.rs | 93 ++++++++++++++++++----------------- druid/src/widget/scroll.rs | 44 +++-------------- 6 files changed, 148 insertions(+), 102 deletions(-) diff --git a/druid/src/command.rs b/druid/src/command.rs index b021e1134f..8b7f7e5abc 100644 --- a/druid/src/command.rs +++ b/druid/src/command.rs @@ -175,7 +175,10 @@ pub mod sys { use std::any::Any; use super::Selector; - use crate::{sub_window::{SubWindowDesc, SubWindowUpdate}, FileDialogOptions, FileInfo, SingleUse, WidgetId, WindowConfig, Rect}; + use crate::{ + sub_window::{SubWindowDesc, SubWindowUpdate}, + FileDialogOptions, FileInfo, Rect, SingleUse, WidgetId, WindowConfig, + }; /// Quit the running application. This command is handled by the druid library. pub const QUIT_APP: Selector = Selector::new("druid-builtin.quit-app"); @@ -325,7 +328,21 @@ pub mod sys { pub(crate) const INVALIDATE_IME: Selector = Selector::new("druid-builtin.invalidate-ime"); - pub const SCROLL_TO: Selector = Selector::new("druid_builtin.scroll_to"); + /// 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_are_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. diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index 25c91aff8f..4aa62f0bd3 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; @@ -35,8 +36,6 @@ use crate::{ ExtEventSink, Insets, Menu, Notification, Point, Rect, SingleUse, Size, Target, TimerToken, Vec2, WidgetId, WindowConfig, WindowDesc, WindowHandle, WindowId, }; -use crate::widget::Scroll; -use crate::commands::SCROLL_TO; /// A macro for implementing methods on multiple contexts. /// @@ -466,9 +465,9 @@ impl_context_method!(EventCtx<'_, '_>, UpdateCtx<'_, '_>, LifeCycleCtx<'_, '_>, /// /// [`Scroll`]: widget::Scroll /// [`hidden`]: Event::should_propagate_to_hidden - pub fn scroll_to_view(&mut self) { - self.scroll_area_to_view(self.size().to_rect()) - } + pub fn scroll_to_view(&mut self) { + self.scroll_area_to_view(self.size().to_rect()) + } }); // methods on everyone but paintctx @@ -715,7 +714,7 @@ impl EventCtx<'_, '_> { /// [`hidden`]: 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.with(area + self.window_origin().to_vec2())); + self.submit_notification(SCROLL_TO_VIEW.with(area + self.window_origin().to_vec2())); } } @@ -769,7 +768,11 @@ impl UpdateCtx<'_, '_> { /// [`hidden`]: 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, area + self.window_origin().to_vec2(), self.widget_id())); + self.submit_command(Command::new( + SCROLL_TO_VIEW, + area + self.window_origin().to_vec2(), + self.widget_id(), + )); } } @@ -819,7 +822,11 @@ impl LifeCycleCtx<'_, '_> { /// [`hidden`]: 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, area + self.window_origin().to_vec2(), self.widget_id())); + self.submit_command(Command::new( + SCROLL_TO_VIEW, + area + self.window_origin().to_vec2(), + self.widget_id(), + )); } } diff --git a/druid/src/core.rs b/druid/src/core.rs index 978dcbf559..315d091d23 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; @@ -29,8 +30,6 @@ use crate::{ InternalLifeCycle, LayoutCtx, LifeCycle, LifeCycleCtx, Notification, PaintCtx, Region, RenderContext, Target, TextLayout, TimerToken, UpdateCtx, Widget, WidgetId, WindowId, }; -use crate::widget::Scroll; -use crate::commands::SCROLL_TO; /// Our queue type pub(crate) type CommandQueue = VecDeque; @@ -851,11 +850,11 @@ impl> WidgetPod { } ctx.is_handled = true } - Event::Command(cmd) if cmd.is(SCROLL_TO) => { + 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); - inner_ctx.submit_notification(SCROLL_TO.with(*rect)); + let rect = cmd.get_unchecked(SCROLL_TO_VIEW); + inner_ctx.submit_notification(SCROLL_TO_VIEW.with(*rect)); ctx.is_handled = true; } _ => { @@ -978,7 +977,7 @@ impl> WidgetPod { self.state.has_focus = change; extra_event = Some(LifeCycle::FocusChanged(change)); - if change == true { + if change { //TODO: decide whether this should be done manually ctx.scroll_to_view(); } diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index b7768faf69..a93f880d91 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -17,10 +17,11 @@ use std::time::Duration; +use crate::{Env, Event, EventCtx, LifeCycle, LifeCycleCtx, PaintCtx, RenderContext, TimerToken}; +use crate::command::sys::SCROLL_TO_VIEW; use crate::kurbo::{Point, Rect, Vec2}; use crate::theme; use crate::widget::{Axis, Viewport}; -use crate::{Env, Event, EventCtx, LifeCycle, LifeCycleCtx, PaintCtx, RenderContext, TimerToken}; #[derive(Debug, Copy, Clone)] /// Which scroll bars of a scroll area are currently enabled. @@ -515,13 +516,64 @@ impl ScrollComponent { } } +/// 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( + ctx: &mut EventCtx, + port: &mut Viewport, + global_highlight_rect: Rect, +) { + let global_content_offset = ctx.window_origin().to_vec2() - port.view_origin.to_vec2(); + let content_highlight_rect = global_highlight_rect - global_content_offset; + let view_rect = Rect::from_origin_size(port.view_origin, port.view_size); + + let mut new_origin = port.view_origin; + //TODO: decide whether the scroll should pan to the upper-left corner of the + // requested area if the view area is smaller than the requested area and + // already inside of it. + if content_highlight_rect.x0 < view_rect.x0 + || content_highlight_rect.size().width > port.view_size.width + { + //Prefer the left over the right side if the scroll_to content is bigger than the view_size + new_origin.x = content_highlight_rect.x0; + } else if content_highlight_rect.x1 > view_rect.x1 { + new_origin.x = content_highlight_rect.x1 - port.view_size.width; + } + if content_highlight_rect.y0 < view_rect.y0 + || content_highlight_rect.size().height > port.view_size.height + { + //Prefer the upper over the lower side if the scroll_to content is bigger than the view_size + new_origin.y = content_highlight_rect.y0; + } else if content_highlight_rect.y1 > view_rect.y1 { + new_origin.y = content_highlight_rect.y1 - port.view_size.height; + } + + if port.pan_to(new_origin) { + ctx.request_paint(); + } + + // 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)); +} + + #[cfg(test)] mod tests { use float_cmp::approx_eq; - use super::*; use crate::kurbo::Size; + use super::*; + const TEST_SCROLLBAR_WIDTH: f64 = 11.0; const TEST_SCROLLBAR_PAD: f64 = 3.0; const TEST_SCROLLBAR_MIN_SIZE: f64 = 17.0; @@ -753,4 +805,4 @@ mod tests { .adding(theme::SCROLLBAR_PAD, TEST_SCROLLBAR_PAD) .adding(theme::SCROLLBAR_MIN_SIZE, TEST_SCROLLBAR_MIN_SIZE) } -} +} \ No newline at end of file diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 19d2b49b8f..9164895cbd 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -14,6 +14,54 @@ //! Common widgets. +pub use added::Added; +pub use align::Align; +pub use aspect_ratio_box::AspectRatioBox; +pub use button::Button; +pub use checkbox::Checkbox; +pub use click::Click; +pub use clip_box::{ClipBox, Viewport}; +pub use common::FillStrat; +pub use container::Container; +pub use controller::{Controller, ControllerHost}; +pub use disable_if::DisabledIf; +pub use either::Either; +pub use env_scope::EnvScope; +pub use flex::{Axis, CrossAxisAlignment, Flex, FlexParams, MainAxisAlignment}; +pub use identity_wrapper::IdentityWrapper; +pub use label::{Label, LabelText, LineBreaking, RawLabel}; +pub use lens_wrap::LensWrap; +pub use list::{List, ListIter}; +pub use maybe::Maybe; +pub use padding::Padding; +pub use painter::{BackgroundBrush, Painter}; +pub use parse::Parse; +pub use progress_bar::ProgressBar; +pub use radio::{Radio, RadioGroup}; +pub use scope::{DefaultScopePolicy, LensScopeTransfer, Scope, ScopePolicy, ScopeTransfer}; +pub use scroll::Scroll; +pub use sized_box::SizedBox; +pub use slider::Slider; +pub use spinner::Spinner; +pub use split::Split; +pub use stepper::Stepper; +#[cfg(feature = "svg")] +pub use svg::{Svg, SvgData}; +pub use switch::Switch; +pub use tabs::{TabInfo, Tabs, TabsEdge, TabsPolicy, TabsState, TabsTransition}; +pub use textbox::TextBox; +pub use value_textbox::{TextBoxEvent, ValidationDelegate, ValueTextBox}; +pub use view_switcher::ViewSwitcher; +#[doc(hidden)] +pub use widget::{Widget, WidgetId}; +#[doc(hidden)] +pub use widget_ext::WidgetExt; +pub use widget_wrapper::WidgetWrapper; + +pub use crate::scroll_component::default_scroll_to_view_handling; + +pub use self::image::Image; + // First as it defines macros #[macro_use] mod widget_wrapper; @@ -63,51 +111,6 @@ mod view_switcher; mod widget; mod widget_ext; -pub use self::image::Image; -pub use added::Added; -pub use align::Align; -pub use aspect_ratio_box::AspectRatioBox; -pub use button::Button; -pub use checkbox::Checkbox; -pub use click::Click; -pub use clip_box::{ClipBox, Viewport}; -pub use common::FillStrat; -pub use container::Container; -pub use controller::{Controller, ControllerHost}; -pub use disable_if::DisabledIf; -pub use either::Either; -pub use env_scope::EnvScope; -pub use flex::{Axis, CrossAxisAlignment, Flex, FlexParams, MainAxisAlignment}; -pub use identity_wrapper::IdentityWrapper; -pub use label::{Label, LabelText, LineBreaking, RawLabel}; -pub use lens_wrap::LensWrap; -pub use list::{List, ListIter}; -pub use maybe::Maybe; -pub use padding::Padding; -pub use painter::{BackgroundBrush, Painter}; -pub use parse::Parse; -pub use progress_bar::ProgressBar; -pub use radio::{Radio, RadioGroup}; -pub use scope::{DefaultScopePolicy, LensScopeTransfer, Scope, ScopePolicy, ScopeTransfer}; -pub use scroll::{Scroll, default_scroll_to_view_handling}; -pub use sized_box::SizedBox; -pub use slider::Slider; -pub use spinner::Spinner; -pub use split::Split; -pub use stepper::Stepper; -#[cfg(feature = "svg")] -pub use svg::{Svg, SvgData}; -pub use switch::Switch; -pub use tabs::{TabInfo, Tabs, TabsEdge, TabsPolicy, TabsState, TabsTransition}; -pub use textbox::TextBox; -pub use value_textbox::{TextBoxEvent, ValidationDelegate, ValueTextBox}; -pub use view_switcher::ViewSwitcher; -#[doc(hidden)] -pub use widget::{Widget, WidgetId}; -#[doc(hidden)] -pub use widget_ext::WidgetExt; -pub use widget_wrapper::WidgetWrapper; - /// The types required to implement a `Widget`. /// /// # Structs diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index 1156e228e5..de73a8fd42 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -14,12 +14,13 @@ //! A container that scrolls its contents. +use tracing::{instrument, trace}; + +use crate::{Data, Rect, scroll_component::*, Vec2}; +use crate::commands::SCROLL_TO_VIEW; use crate::debug_state::DebugState; +use crate::widget::{Axis, ClipBox}; use crate::widget::prelude::*; -use crate::widget::{Axis, ClipBox, Viewport}; -use crate::{scroll_component::*, Data, Rect, Vec2, Selector, Key}; -use tracing::{instrument, trace}; -use crate::commands::SCROLL_TO; /// A container that scrolls its contents. /// @@ -194,7 +195,7 @@ impl> Widget for Scroll { 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) { + if let Some(&global_highlight_rect) = notification.get(SCROLL_TO_VIEW) { ctx.set_handled(); self.clip.with_port(|port| { @@ -262,36 +263,3 @@ fn log_size_warnings(size: Size) { tracing::warn!("Scroll widget's child has an infinite height."); } } - -pub fn default_scroll_to_view_handling(ctx: &mut EventCtx, port: &mut Viewport, global_highlight_rect: Rect) { - let global_content_offset = ctx.window_origin().to_vec2() - port.view_origin.to_vec2(); - let content_highlight_rect = global_highlight_rect - global_content_offset; - let view_rect = Rect::from_origin_size(port.view_origin, port.view_size); - - let mut new_origin = port.view_origin; - //TODO: decide whether the scroll should pan to the upper-left corner of the - // requested area if the view area is smaller than the requested area and - // already inside of it. - if content_highlight_rect.x0 < view_rect.x0 || content_highlight_rect.size().width > port.view_size.width { - //Prefer the left over the right side if the scroll_to content is bigger than the view_size - new_origin.x = content_highlight_rect.x0; - } else if content_highlight_rect.x1 > view_rect.x1 { - new_origin.x = content_highlight_rect.x1 - port.view_size.width; - } - if content_highlight_rect.y0 < view_rect.y0 || content_highlight_rect.size().height > port.view_size.height { - //Prefer the upper over the lower side if the scroll_to content is bigger than the view_size - new_origin.y = content_highlight_rect.y0; - } else if content_highlight_rect.y1 > view_rect.y1 { - new_origin.y = content_highlight_rect.y1 - port.view_size.height; - } - - if port.pan_to(new_origin) { - ctx.request_paint(); - } - - // 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.with(content_highlight_rect + global_content_offset)); -} From 5bd2a54cbfab425ee75d628efca196bb98545a7e Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 18 Sep 2021 19:50:41 +0200 Subject: [PATCH 09/17] reformat Signed-off-by: Christoph --- druid/src/scroll_component.rs | 5 ++--- druid/src/widget/scroll.rs | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index a93f880d91..e2563e7cbd 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -17,11 +17,11 @@ use std::time::Duration; -use crate::{Env, Event, EventCtx, LifeCycle, LifeCycleCtx, PaintCtx, RenderContext, TimerToken}; use crate::command::sys::SCROLL_TO_VIEW; use crate::kurbo::{Point, Rect, Vec2}; use crate::theme; use crate::widget::{Axis, Viewport}; +use crate::{Env, Event, EventCtx, LifeCycle, LifeCycleCtx, PaintCtx, RenderContext, TimerToken}; #[derive(Debug, Copy, Clone)] /// Which scroll bars of a scroll area are currently enabled. @@ -565,7 +565,6 @@ pub fn default_scroll_to_view_handling( ctx.submit_notification(SCROLL_TO_VIEW.with(content_highlight_rect + global_content_offset)); } - #[cfg(test)] mod tests { use float_cmp::approx_eq; @@ -805,4 +804,4 @@ mod tests { .adding(theme::SCROLLBAR_PAD, TEST_SCROLLBAR_PAD) .adding(theme::SCROLLBAR_MIN_SIZE, TEST_SCROLLBAR_MIN_SIZE) } -} \ No newline at end of file +} diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index de73a8fd42..5e1a4997f2 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -16,11 +16,11 @@ use tracing::{instrument, trace}; -use crate::{Data, Rect, scroll_component::*, Vec2}; use crate::commands::SCROLL_TO_VIEW; use crate::debug_state::DebugState; -use crate::widget::{Axis, ClipBox}; use crate::widget::prelude::*; +use crate::widget::{Axis, ClipBox}; +use crate::{scroll_component::*, Data, Rect, Vec2}; /// A container that scrolls its contents. /// From 99bad6319b6aba26d7f443345e7cc7ea37f08556 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 18 Sep 2021 20:06:30 +0200 Subject: [PATCH 10/17] fixed docs Signed-off-by: Christoph --- druid/src/command.rs | 2 +- druid/src/contexts.rs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/druid/src/command.rs b/druid/src/command.rs index 8b7f7e5abc..63f82ac158 100644 --- a/druid/src/command.rs +++ b/druid/src/command.rs @@ -341,7 +341,7 @@ pub mod sys { /// translated. /// /// [`scroll_to_view`]: crate::EventCtx::scroll_to_view() - /// [`scroll_area_to_view`]: crate::EventCtx::scroll_are_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 diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index 4aa62f0bd3..2899c7914e 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -463,8 +463,8 @@ impl_context_method!(EventCtx<'_, '_>, UpdateCtx<'_, '_>, LifeCycleCtx<'_, '_>, /// /// If the widget is [`hidden`], this method has no effect. /// - /// [`Scroll`]: widget::Scroll - /// [`hidden`]: Event::should_propagate_to_hidden + /// [`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()) } @@ -710,8 +710,8 @@ impl EventCtx<'_, '_> { /// /// If the widget is [`hidden`], this method has no effect. /// - /// [`Scroll`]: widget::Scroll - /// [`hidden`]: Event::should_propagate_to_hidden + /// [`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())); @@ -764,8 +764,8 @@ impl UpdateCtx<'_, '_> { /// /// If the widget is [`hidden`], this method has no effect. /// - /// [`Scroll`]: widget::Scroll - /// [`hidden`]: Event::should_propagate_to_hidden + /// [`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( @@ -818,8 +818,8 @@ impl LifeCycleCtx<'_, '_> { /// /// If the widget is [`hidden`], this method has no effect. /// - /// [`Scroll`]: widget::Scroll - /// [`hidden`]: Event::should_propagate_to_hidden + /// [`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( From e94c22387aab983f74ee308e56792f92d5bf346f Mon Sep 17 00:00:00 2001 From: Christoph Date: Fri, 24 Sep 2021 17:05:02 +0200 Subject: [PATCH 11/17] - refactored - show scrollbar when clip is moved Signed-off-by: Christoph --- druid/src/scroll_component.rs | 50 ----------------------------------- druid/src/widget/clip_box.rs | 31 ++++++++++++++++++++++ druid/src/widget/mod.rs | 2 -- druid/src/widget/scroll.rs | 8 +++--- 4 files changed, 35 insertions(+), 56 deletions(-) diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index e2563e7cbd..72b59eea95 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -17,7 +17,6 @@ use std::time::Duration; -use crate::command::sys::SCROLL_TO_VIEW; use crate::kurbo::{Point, Rect, Vec2}; use crate::theme; use crate::widget::{Axis, Viewport}; @@ -516,55 +515,6 @@ impl ScrollComponent { } } -/// 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( - ctx: &mut EventCtx, - port: &mut Viewport, - global_highlight_rect: Rect, -) { - let global_content_offset = ctx.window_origin().to_vec2() - port.view_origin.to_vec2(); - let content_highlight_rect = global_highlight_rect - global_content_offset; - let view_rect = Rect::from_origin_size(port.view_origin, port.view_size); - - let mut new_origin = port.view_origin; - //TODO: decide whether the scroll should pan to the upper-left corner of the - // requested area if the view area is smaller than the requested area and - // already inside of it. - if content_highlight_rect.x0 < view_rect.x0 - || content_highlight_rect.size().width > port.view_size.width - { - //Prefer the left over the right side if the scroll_to content is bigger than the view_size - new_origin.x = content_highlight_rect.x0; - } else if content_highlight_rect.x1 > view_rect.x1 { - new_origin.x = content_highlight_rect.x1 - port.view_size.width; - } - if content_highlight_rect.y0 < view_rect.y0 - || content_highlight_rect.size().height > port.view_size.height - { - //Prefer the upper over the lower side if the scroll_to content is bigger than the view_size - new_origin.y = content_highlight_rect.y0; - } else if content_highlight_rect.y1 > view_rect.y1 { - new_origin.y = content_highlight_rect.y1 - port.view_size.height; - } - - if port.pan_to(new_origin) { - ctx.request_paint(); - } - - // 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)); -} - #[cfg(test)] mod tests { use float_cmp::approx_eq; diff --git a/druid/src/widget/clip_box.rs b/druid/src/widget/clip_box.rs index aa63de06f1..45445d55a4 100644 --- a/druid/src/widget/clip_box.rs +++ b/druid/src/widget/clip_box.rs @@ -18,6 +18,7 @@ use crate::widget::prelude::*; use crate::widget::Axis; use crate::{Data, WidgetPod}; use tracing::{instrument, trace}; +use crate::commands::SCROLL_TO_VIEW; /// Represents the size and position of a rectangular "viewport" into a larger area. #[derive(Clone, Copy, Default, Debug, PartialEq)] @@ -308,6 +309,36 @@ 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/mod.rs b/druid/src/widget/mod.rs index 9164895cbd..295f91e05a 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -58,8 +58,6 @@ pub use widget::{Widget, WidgetId}; pub use widget_ext::WidgetExt; pub use widget_wrapper::WidgetWrapper; -pub use crate::scroll_component::default_scroll_to_view_handling; - pub use self::image::Image; // First as it defines macros diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index 5e1a4997f2..25b225f8be 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -197,10 +197,10 @@ impl> Widget for Scroll { if let Event::Notification(notification) = event { if let Some(&global_highlight_rect) = notification.get(SCROLL_TO_VIEW) { ctx.set_handled(); - - self.clip.with_port(|port| { - default_scroll_to_view_handling(ctx, port, global_highlight_rect); - }); + 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); + } } } } From b3dccd2a69ae268a61ab15667eda0d116a1c3ccc Mon Sep 17 00:00:00 2001 From: Christoph Date: Fri, 24 Sep 2021 17:16:01 +0200 Subject: [PATCH 12/17] changing id to kebab case Signed-off-by: Christoph --- druid/src/command.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/druid/src/command.rs b/druid/src/command.rs index 63f82ac158..646c0244fb 100644 --- a/druid/src/command.rs +++ b/druid/src/command.rs @@ -342,7 +342,7 @@ pub mod sys { /// /// [`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"); + 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. From 8c1f211d11babb378e6fb79b464c794cfb78707f Mon Sep 17 00:00:00 2001 From: Christoph Date: Fri, 24 Sep 2021 17:21:17 +0200 Subject: [PATCH 13/17] changed back ordering Signed-off-by: Christoph --- druid/src/scroll_component.rs | 2 +- druid/src/widget/mod.rs | 91 +++++++++++++++++------------------ druid/src/widget/scroll.rs | 3 +- 3 files changed, 47 insertions(+), 49 deletions(-) diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index 72b59eea95..a4eafe76ce 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -519,9 +519,9 @@ impl ScrollComponent { mod tests { use float_cmp::approx_eq; + use super::*; use crate::kurbo::Size; - use super::*; const TEST_SCROLLBAR_WIDTH: f64 = 11.0; const TEST_SCROLLBAR_PAD: f64 = 3.0; diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 295f91e05a..8d0879d43f 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -14,52 +14,6 @@ //! Common widgets. -pub use added::Added; -pub use align::Align; -pub use aspect_ratio_box::AspectRatioBox; -pub use button::Button; -pub use checkbox::Checkbox; -pub use click::Click; -pub use clip_box::{ClipBox, Viewport}; -pub use common::FillStrat; -pub use container::Container; -pub use controller::{Controller, ControllerHost}; -pub use disable_if::DisabledIf; -pub use either::Either; -pub use env_scope::EnvScope; -pub use flex::{Axis, CrossAxisAlignment, Flex, FlexParams, MainAxisAlignment}; -pub use identity_wrapper::IdentityWrapper; -pub use label::{Label, LabelText, LineBreaking, RawLabel}; -pub use lens_wrap::LensWrap; -pub use list::{List, ListIter}; -pub use maybe::Maybe; -pub use padding::Padding; -pub use painter::{BackgroundBrush, Painter}; -pub use parse::Parse; -pub use progress_bar::ProgressBar; -pub use radio::{Radio, RadioGroup}; -pub use scope::{DefaultScopePolicy, LensScopeTransfer, Scope, ScopePolicy, ScopeTransfer}; -pub use scroll::Scroll; -pub use sized_box::SizedBox; -pub use slider::Slider; -pub use spinner::Spinner; -pub use split::Split; -pub use stepper::Stepper; -#[cfg(feature = "svg")] -pub use svg::{Svg, SvgData}; -pub use switch::Switch; -pub use tabs::{TabInfo, Tabs, TabsEdge, TabsPolicy, TabsState, TabsTransition}; -pub use textbox::TextBox; -pub use value_textbox::{TextBoxEvent, ValidationDelegate, ValueTextBox}; -pub use view_switcher::ViewSwitcher; -#[doc(hidden)] -pub use widget::{Widget, WidgetId}; -#[doc(hidden)] -pub use widget_ext::WidgetExt; -pub use widget_wrapper::WidgetWrapper; - -pub use self::image::Image; - // First as it defines macros #[macro_use] mod widget_wrapper; @@ -109,6 +63,51 @@ mod view_switcher; mod widget; mod widget_ext; +pub use self::image::Image; +pub use added::Added; +pub use align::Align; +pub use aspect_ratio_box::AspectRatioBox; +pub use button::Button; +pub use checkbox::Checkbox; +pub use click::Click; +pub use clip_box::{ClipBox, Viewport}; +pub use common::FillStrat; +pub use container::Container; +pub use controller::{Controller, ControllerHost}; +pub use disable_if::DisabledIf; +pub use either::Either; +pub use env_scope::EnvScope; +pub use flex::{Axis, CrossAxisAlignment, Flex, FlexParams, MainAxisAlignment}; +pub use identity_wrapper::IdentityWrapper; +pub use label::{Label, LabelText, LineBreaking, RawLabel}; +pub use lens_wrap::LensWrap; +pub use list::{List, ListIter}; +pub use maybe::Maybe; +pub use padding::Padding; +pub use painter::{BackgroundBrush, Painter}; +pub use parse::Parse; +pub use progress_bar::ProgressBar; +pub use radio::{Radio, RadioGroup}; +pub use scope::{DefaultScopePolicy, LensScopeTransfer, Scope, ScopePolicy, ScopeTransfer}; +pub use scroll::Scroll; +pub use sized_box::SizedBox; +pub use slider::Slider; +pub use spinner::Spinner; +pub use split::Split; +pub use stepper::Stepper; +#[cfg(feature = "svg")] +pub use svg::{Svg, SvgData}; +pub use switch::Switch; +pub use tabs::{TabInfo, Tabs, TabsEdge, TabsPolicy, TabsState, TabsTransition}; +pub use textbox::TextBox; +pub use value_textbox::{TextBoxEvent, ValidationDelegate, ValueTextBox}; +pub use view_switcher::ViewSwitcher; +#[doc(hidden)] +pub use widget::{Widget, WidgetId}; +#[doc(hidden)] +pub use widget_ext::WidgetExt; +pub use widget_wrapper::WidgetWrapper; + /// The types required to implement a `Widget`. /// /// # Structs diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index 25b225f8be..941909d77b 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -14,13 +14,12 @@ //! A container that scrolls its contents. -use tracing::{instrument, trace}; - use crate::commands::SCROLL_TO_VIEW; use crate::debug_state::DebugState; use crate::widget::prelude::*; use crate::widget::{Axis, ClipBox}; use crate::{scroll_component::*, Data, Rect, Vec2}; +use tracing::{instrument, trace}; /// A container that scrolls its contents. /// From e26a4ddd3ca8fb150819604bb0549d729c6ab2c7 Mon Sep 17 00:00:00 2001 From: Christoph Date: Fri, 24 Sep 2021 17:28:37 +0200 Subject: [PATCH 14/17] make scroll to view on FocusChange manual Signed-off-by: Christoph --- druid/src/core.rs | 4 ---- druid/src/scroll_component.rs | 1 - druid/src/widget/clip_box.rs | 6 ++++-- druid/src/widget/scroll.rs | 7 +++++-- druid/src/widget/textbox.rs | 1 + 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/druid/src/core.rs b/druid/src/core.rs index 315d091d23..8b8b43933f 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -977,10 +977,6 @@ impl> WidgetPod { self.state.has_focus = change; extra_event = Some(LifeCycle::FocusChanged(change)); - if change { - //TODO: decide whether this should be done manually - ctx.scroll_to_view(); - } } else { self.state.has_focus = false; } diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index a4eafe76ce..b7768faf69 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -522,7 +522,6 @@ mod tests { use super::*; use crate::kurbo::Size; - const TEST_SCROLLBAR_WIDTH: f64 = 11.0; const TEST_SCROLLBAR_PAD: f64 = 3.0; const TEST_SCROLLBAR_MIN_SIZE: f64 = 17.0; diff --git a/druid/src/widget/clip_box.rs b/druid/src/widget/clip_box.rs index 45445d55a4..eb86285e30 100644 --- a/druid/src/widget/clip_box.rs +++ b/druid/src/widget/clip_box.rs @@ -12,13 +12,13 @@ // 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::*; use crate::widget::Axis; use crate::{Data, WidgetPod}; use tracing::{instrument, trace}; -use crate::commands::SCROLL_TO_VIEW; /// Represents the size and position of a rectangular "viewport" into a larger area. #[derive(Clone, Copy, Default, Debug, PartialEq)] @@ -335,7 +335,9 @@ impl> ClipBox { // 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)); + ctx.submit_notification( + SCROLL_TO_VIEW.with(content_highlight_rect + global_content_offset), + ); }); viewport_changed } diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index 941909d77b..c0a9c0165e 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -196,9 +196,12 @@ impl> Widget for Scroll { 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); + 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); + self.scroll_component + .reset_scrollbar_fade(|duration| ctx.request_timer(duration), env); } } } diff --git a/druid/src/widget/textbox.rs b/druid/src/widget/textbox.rs index 54a1ba2966..5d86fef5f0 100644 --- a/druid/src/widget/textbox.rs +++ b/druid/src/widget/textbox.rs @@ -511,6 +511,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 && !self.multiline { From 4dabaf0befc776da7996fdbb25cf62f5275c06de Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 25 Sep 2021 16:34:52 +0200 Subject: [PATCH 15/17] reformat Signed-off-by: Christoph --- druid/src/core.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/druid/src/core.rs b/druid/src/core.rs index 8b8b43933f..4b8bafb867 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -976,7 +976,6 @@ impl> WidgetPod { if let Some(change) = this_changed { self.state.has_focus = change; extra_event = Some(LifeCycle::FocusChanged(change)); - } else { self.state.has_focus = false; } From da55e5a505ef7a0a22ebe7388b55598bbf9c8bb8 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 2 Oct 2021 07:06:23 +0000 Subject: [PATCH 16/17] Update druid/src/contexts.rs Co-authored-by: Manmeet Maan <49202620+Maan2003@users.noreply.github.com> --- druid/src/contexts.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index 2899c7914e..7d8bf70b8b 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -822,11 +822,11 @@ impl LifeCycleCtx<'_, '_> { /// [`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(), - )); + self.submit_command( + SCROLL_TO_VIEW + .with(area + self.window_origin().to_vec2()) + .to(self.widget_id()) + ); } } From 0626a38e655b932b612a4976938970280384c1b5 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 2 Oct 2021 09:30:45 +0200 Subject: [PATCH 17/17] reformat Signed-off-by: Christoph --- druid/src/contexts.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index f091289851..6d71a3223e 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -827,7 +827,7 @@ impl LifeCycleCtx<'_, '_> { self.submit_command( SCROLL_TO_VIEW .with(area + self.window_origin().to_vec2()) - .to(self.widget_id()) + .to(self.widget_id()), ); } }