Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scroll to view #1976

Merged
merged 21 commits into from
Oct 2, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -789,6 +790,7 @@ Last release without a changelog :(
[#1929]: https://github.com/linebender/druid/pull/1929
[#1947]: https://github.com/linebender/druid/pull/1947
[#1967]: https://github.com/linebender/druid/pull/1967
[#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
Expand Down
18 changes: 17 additions & 1 deletion druid/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,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.
Expand Down Expand Up @@ -328,6 +328,22 @@ pub mod sys {
pub(crate) const INVALIDATE_IME: Selector<ImeInvalidation> =
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<Rect> = Selector::new("druid_builtin.scroll_to");
xarvic marked this conversation as resolved.
Show resolved Hide resolved

/// A change that has occured to text state, and needs to be
/// communicated to the platform.
pub(crate) struct ImeInvalidation {
Expand Down
68 changes: 68 additions & 0 deletions druid/src/contexts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -453,6 +454,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
Expand Down Expand Up @@ -686,6 +701,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<'_, '_> {
Expand Down Expand Up @@ -725,6 +755,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<'_, '_> {
Expand Down Expand Up @@ -760,6 +809,25 @@ 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`]: 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(),
));
xarvic marked this conversation as resolved.
Show resolved Hide resolved
}
}

impl LayoutCtx<'_, '_> {
Expand Down
13 changes: 13 additions & 0 deletions druid/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -849,6 +850,13 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
}
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);

Expand Down Expand Up @@ -968,6 +976,11 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
if let Some(change) = this_changed {
self.state.has_focus = change;
extra_event = Some(LifeCycle::FocusChanged(change));

if change {
//TODO: decide whether this should be done manually
xarvic marked this conversation as resolved.
Show resolved Hide resolved
ctx.scroll_to_view();
}
} else {
self.state.has_focus = false;
}
Expand Down
53 changes: 52 additions & 1 deletion druid/src/scroll_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

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};
Expand Down Expand Up @@ -515,13 +516,63 @@ 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(
xarvic marked this conversation as resolved.
Show resolved Hide resolved
ctx: &mut EventCtx,
port: &mut Viewport,
global_highlight_rect: Rect,
) {
let global_content_offset = ctx.window_origin().to_vec2() - port.view_origin.to_vec2();
maan2003 marked this conversation as resolved.
Show resolved Hide resolved
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;
}

xarvic marked this conversation as resolved.
Show resolved Hide resolved
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::*;
maan2003 marked this conversation as resolved.
Show resolved Hide resolved

const TEST_SCROLLBAR_WIDTH: f64 = 11.0;
const TEST_SCROLLBAR_PAD: f64 = 3.0;
const TEST_SCROLLBAR_MIN_SIZE: f64 = 17.0;
Expand Down
2 changes: 1 addition & 1 deletion druid/src/widget/clip_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
93 changes: 48 additions & 45 deletions druid/src/widget/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,54 @@

//! Common widgets.

pub use added::Added;
maan2003 marked this conversation as resolved.
Show resolved Hide resolved
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;
Expand Down Expand Up @@ -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;
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
Expand Down
Loading