Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ You can find its changes [documented below](#070---2021-01-01).
- `RangeSlider` and `Annotated` ([#1979] by [@xarvic])
- Add `Checkbox::from_label` constructor ([#2111] by [@maurerdietmar])
- fix content_insets for gtk backend ([#2117] by [@maurerdietmar])
- `ClipBox::managed`, `Notification::warn_if_ununsed` and `Notification::warn_if_ununsed_set` ([#2141] by [@xarvic])
- `ClipBox` and `Tabs` handle SCROLL_TO_VIEW ([#2141] by [@xarvic])
- `EventCtx::submit_notification_without_warning` ([#2141] by [@xarvic])

### Changed

Expand All @@ -91,6 +94,7 @@ You can find its changes [documented below](#070---2021-01-01).
- Closures passed to `Label::new` can now return any type that implements `Into<ArcStr>` ([#2064] by [@jplatte])
- `AppDelegate::window_added` now receives the new window's `WindowHandle`. ([#2119] by [@zedseven])
- Removed line of code that prevented window miximalization. ([#2113] by [@Pavel-N])
- Dont warn about unhandled `Notification`s which have `known_target` set to false ([#2141] by [@xarvic])

### Deprecated

Expand Down Expand Up @@ -826,6 +830,7 @@ Last release without a changelog :(
[#2119]: https://github.com/linebender/druid/pull/2119
[#2111]: https://github.com/linebender/druid/pull/2111
[#2117]: https://github.com/linebender/druid/pull/2117
[#2117]: https://github.com/linebender/druid/pull/2141

[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
23 changes: 21 additions & 2 deletions druid/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ pub struct Notification {
payload: Arc<dyn Any>,
source: WidgetId,
route: WidgetId,
warn_if_unused: bool,
}

/// A wrapper type for [`Command`] payloads that should only be used once.
Expand Down Expand Up @@ -343,11 +344,15 @@ pub mod sys {
/// 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.
/// a new `SCROLL_TO_VIEW` notification with the same region relative to the new child position.
///
/// When building a new widget using ClipBox take a look at [`ClipBox::managed`] and
/// [`Viewport::default_scroll_to_view_handling`].
///
/// [`scroll_to_view`]: crate::EventCtx::scroll_to_view()
/// [`scroll_area_to_view`]: crate::EventCtx::scroll_area_to_view()
/// [`ClipBox::managed`]: crate::widget::ClipBox::managed()
/// [`Viewport::default_scroll_to_view_handling`]: crate::widget::Viewport::default_scroll_to_view_handling()
pub const SCROLL_TO_VIEW: Selector<Rect> = Selector::new("druid-builtin.scroll-to");

/// A change that has occured to text state, and needs to be
Expand Down Expand Up @@ -430,6 +435,7 @@ impl Command {
payload: self.payload,
source,
route: source,
warn_if_unused: true,
}
}

Expand Down Expand Up @@ -552,6 +558,19 @@ impl Notification {
self.source
}

/// Builder-style method to set warn_if_unused.
///
/// The default is true.
pub fn warn_if_unused(mut self, warn_if_unused: bool) -> Self {
self.warn_if_unused = warn_if_unused;
self
}

/// Returns whether there should be a warning when no widget handles this notification.
pub fn warn_if_unused_set(&self) -> bool {
self.warn_if_unused
}

/// Change the route id
pub(crate) fn with_route(mut self, widget_id: WidgetId) -> Self {
self.route = widget_id;
Expand Down
24 changes: 23 additions & 1 deletion druid/src/contexts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,8 +465,11 @@ impl_context_method!(EventCtx<'_, '_>, UpdateCtx<'_, '_>, LifeCycleCtx<'_, '_>,
///
/// If the widget is [`hidden`], this method has no effect.
///
/// This functionality is achieved by sending a [`SCROLL_TO_VIEW`] notification.
///
/// [`Scroll`]: crate::widget::Scroll
/// [`hidden`]: crate::Event::should_propagate_to_hidden
/// [`SCROLL_TO_VIEW`]: crate::commands::SCROLL_TO_VIEW
pub fn scroll_to_view(&mut self) {
self.scroll_area_to_view(self.size().to_rect())
}
Expand Down Expand Up @@ -545,6 +548,23 @@ impl EventCtx<'_, '_> {
self.notifications.push_back(note);
}

/// Submit a [`Notification`] without warning.
///
/// In contrast to [`submit_notification`], calling this method will not result in an
/// "unhandled notification" warning.
///
/// [`submit_notification`]: crate::EventCtx::submit_notification
//TODO: decide if we should use a known_target flag on submit_notification instead,
// which would be a breaking change.
pub fn submit_notification_without_warning(&mut self, note: impl Into<Command>) {
trace!("submit_notification");
let note = note
.into()
.into_notification(self.widget_state.id)
.warn_if_unused(false);
self.notifications.push_back(note);
}

/// Set the "active" state of the widget.
///
/// See [`EventCtx::is_active`](struct.EventCtx.html#method.is_active).
Expand Down Expand Up @@ -716,7 +736,9 @@ impl EventCtx<'_, '_> {
/// [`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()));
self.submit_notification_without_warning(
SCROLL_TO_VIEW.with(area + self.window_origin().to_vec2()),
);
}
}

Expand Down
2 changes: 1 addition & 1 deletion druid/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
// 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));
inner_ctx.submit_notification_without_warning(SCROLL_TO_VIEW.with(*rect));
ctx.is_handled = true;
}
_ => {
Expand Down
155 changes: 115 additions & 40 deletions druid/src/widget/clip_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ 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 tracing::{info, instrument, trace, warn};

/// Represents the size and position of a rectangular "viewport" into a larger area.
#[derive(Clone, Copy, Default, Debug, PartialEq)]
Expand Down Expand Up @@ -123,6 +123,68 @@ impl Viewport {
let new_origin = self.view_origin + Vec2::new(delta_x, delta_y);
self.pan_to(new_origin)
}

/// 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;
let global_content_offset = ctx.window_origin().to_vec2() - self.view_origin.to_vec2();
let content_highlight_rect = global_highlight_rect - global_content_offset;

if self
.content_size
.to_rect()
.intersect(content_highlight_rect)
!= content_highlight_rect
{
warn!("tried to bring area outside of the content to view!");
}

if self.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() - self.view_origin.to_vec2();
ctx.submit_notification_without_warning(
SCROLL_TO_VIEW.with(content_highlight_rect + global_content_offset),
);
viewport_changed
}

/// This method handles SCROLL_TO_VIEW by clipping the view_rect to the content rect.
///
/// 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 fixed_scroll_to_view_handling(
&self,
ctx: &mut EventCtx,
global_highlight_rect: Rect,
source: WidgetId,
) {
let global_viewport_rect = self.view_rect() + ctx.window_origin().to_vec2();
let clipped_highlight_rect = global_highlight_rect.intersect(global_viewport_rect);

if clipped_highlight_rect.area() > 0.0 {
ctx.submit_notification_without_warning(SCROLL_TO_VIEW.with(clipped_highlight_rect));
} else {
info!("Hidden Widget({}) in unmanaged clip requested SCROLL_TO_VIEW. The request is ignored.", source.to_raw());
}
}
}

/// A widget exposing a rectangular view into its child, which can be used as a building block for
Expand All @@ -133,6 +195,9 @@ pub struct ClipBox<T, W> {
constrain_horizontal: bool,
constrain_vertical: bool,
must_fill: bool,

//This ClipBox is wrapped by a widget which manages the viewport_offset
managed: bool,
}

impl<T, W> ClipBox<T, W> {
Expand Down Expand Up @@ -237,13 +302,37 @@ impl<T, W> ClipBox<T, W> {

impl<T, W: Widget<T>> ClipBox<T, W> {
/// Creates a new `ClipBox` wrapping `child`.
pub fn new(child: W) -> Self {
///
/// This method should only be used when creating your own widget, which uses ClipBox
/// internally.
///
/// `ClipBox` will forward [`SCROLL_TO_VIEW`] notifications to its parent unchanged.
/// In this case the parent has to handle said notification itself. By default the ClipBox will
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no more "by default", right? The choice is now explicit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

/// filter out [`SCROLL_TO_VIEW`] notifications which refer to areas not visible.
///
/// [`SCROLL_TO_VIEW`]: crate::commands::SCROLL_TO_VIEW
pub fn managed(child: W) -> Self {
ClipBox {
child: WidgetPod::new(child),
port: Default::default(),
constrain_horizontal: false,
constrain_vertical: false,
must_fill: false,
managed: true,
}
}

/// Creates a new unmanaged `ClipBox` wrapping `child`.
///
/// This method should be used when you are using ClipBox in the widget-hierachie directly.
pub fn unmanaged(child: W) -> Self {
ClipBox {
child: WidgetPod::new(child),
port: Default::default(),
constrain_horizontal: false,
constrain_vertical: false,
must_fill: false,
managed: false,
}
}

Expand Down Expand Up @@ -309,49 +398,35 @@ impl<T, W: Widget<T>> ClipBox<T, W> {
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<T: Data, W: Widget<T>> Widget<T> for ClipBox<T, W> {
#[instrument(name = "ClipBox", level = "trace", skip(self, ctx, event, data, env))]
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
let viewport = ctx.size().to_rect();
let force_event = self.child.is_hot() || self.child.has_active();
if let Some(child_event) =
event.transform_scroll(self.viewport_origin().to_vec2(), viewport, force_event)
{
self.child.event(ctx, &child_event, data, env);
if let Event::Notification(notification) = event {
if let Some(global_highlight_rect) = notification.get(SCROLL_TO_VIEW) {
if !self.managed {
// If the parent widget does not handle SCROLL_TO_VIEW notifications, we
// prevent unexpected behaviour, by clipping SCROLL_TO_VIEW notifications
// to this ClipBox's viewport.
ctx.set_handled();
self.with_port(|port| {
port.fixed_scroll_to_view_handling(
ctx,
*global_highlight_rect,
notification.source(),
);
});
}
}
} else {
let viewport = ctx.size().to_rect();
let force_event = self.child.is_hot() || self.child.has_active();
if let Some(child_event) =
event.transform_scroll(self.viewport_origin().to_vec2(), viewport, force_event)
{
self.child.event(ctx, &child_event, data, env);
}
}
}

Expand Down
27 changes: 13 additions & 14 deletions druid/src/widget/scroll.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ impl<T, W: Widget<T>> Scroll<T, W> {
/// [horizontal](#method.horizontal) methods to limit scrolling to a specific axis.
pub fn new(child: W) -> Scroll<T, W> {
Scroll {
clip: ClipBox::new(child),
clip: ClipBox::managed(child),
scroll_component: ScrollComponent::new(),
}
}
Expand Down Expand Up @@ -189,23 +189,22 @@ impl<T: Data, W: Widget<T>> Widget<T> for Scroll<T, W> {
// 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);
if !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 =
port.default_scroll_to_view_handling(ctx, global_highlight_rect);
if view_port_changed {
scroll_component
.reset_scrollbar_fade(|duration| ctx.request_timer(duration), env);
}
}
}
}
}
});
}

#[instrument(name = "Scroll", level = "trace", skip(self, ctx, event, data, env))]
Expand Down
Loading