From c305d53922efa4b5e7d691871e68f239d4ea5c6c Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Tue, 28 Jul 2020 14:39:12 -0400 Subject: [PATCH 01/18] Naive first step to refactor out scroll logic into separate component --- druid/src/lib.rs | 1 + druid/src/scroll_component.rs | 259 +++++++++++++++++++++++ druid/src/widget/scroll.rs | 376 +++++++++------------------------- 3 files changed, 355 insertions(+), 281 deletions(-) create mode 100644 druid/src/scroll_component.rs diff --git a/druid/src/lib.rs b/druid/src/lib.rs index 0bb2244708..85b6c24afb 100644 --- a/druid/src/lib.rs +++ b/druid/src/lib.rs @@ -153,6 +153,7 @@ pub mod lens; mod localization; mod menu; mod mouse; +pub mod scroll_component; #[cfg(not(target_arch = "wasm32"))] #[cfg(test)] mod tests; diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs new file mode 100644 index 0000000000..edd629daca --- /dev/null +++ b/druid/src/scroll_component.rs @@ -0,0 +1,259 @@ +// Copyright 2020 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A component for embedding in another widget to provide consistant and +//! extendable scrolling behavior + +use std::f64::INFINITY; +use std::time::Duration; + +use crate::kurbo::{Point, Rect, RoundedRect, Size, Vec2}; +use crate::theme; +use crate::{BoxConstraints, Env, PaintCtx, RenderContext, TimerToken}; + +pub const SCROLLBAR_MIN_SIZE: f64 = 45.0; + +#[derive(Debug, Clone)] +pub enum ScrollDirection { + Horizontal, + Vertical, + All, +} + +impl ScrollDirection { + /// Return the maximum size the container can be given + /// its scroll direction and box constraints. + /// In practice vertical scrolling will be width limited to + /// box constraints and horizontal will be height limited. + pub fn max_size(&self, bc: &BoxConstraints) -> Size { + match self { + ScrollDirection::Horizontal => Size::new(INFINITY, bc.max().height), + ScrollDirection::Vertical => Size::new(bc.max().width, INFINITY), + ScrollDirection::All => Size::new(INFINITY, INFINITY), + } + } +} + +pub enum BarHoveredState { + None, + Vertical, + Horizontal, +} + +impl BarHoveredState { + pub fn is_hovered(&self) -> bool { + match self { + BarHoveredState::Vertical | BarHoveredState::Horizontal => true, + _ => false, + } + } +} + +pub enum BarHeldState { + None, + /// Vertical scrollbar is being dragged. Contains an `f64` with + /// the initial y-offset of the dragging input + Vertical(f64), + /// Horizontal scrollbar is being dragged. Contains an `f64` with + /// the initial x-offset of the dragging input + Horizontal(f64), +} + +pub struct ScrollbarsState { + pub opacity: f64, + pub timer_id: TimerToken, + pub hovered: BarHoveredState, + pub held: BarHeldState, +} + +impl Default for ScrollbarsState { + fn default() -> Self { + Self { + opacity: 0.0, + timer_id: TimerToken::INVALID, + hovered: BarHoveredState::None, + held: BarHeldState::None, + } + } +} + +impl ScrollbarsState { + /// true if either scrollbar is currently held down/being dragged + pub fn are_held(&self) -> bool { + match self.held { + BarHeldState::None => false, + _ => true, + } + } +} + +pub struct ScrollComponent { + pub content_size: Size, + pub scroll_offset: Vec2, + pub direction: ScrollDirection, + pub scrollbars: ScrollbarsState, +} + +impl ScrollComponent { + pub fn new() -> ScrollComponent { + ScrollComponent { + content_size: Default::default(), + scroll_offset: Vec2::new(0.0, 0.0), + direction: ScrollDirection::All, + scrollbars: ScrollbarsState::default(), + } + } + + /// Update the scroll. + /// + /// Returns `true` if the scroll has been updated. + pub fn scroll(&mut self, delta: Vec2, size: Size) -> bool { + let mut offset = self.scroll_offset + delta; + offset.x = offset.x.min(self.content_size.width - size.width).max(0.0); + offset.y = offset + .y + .min(self.content_size.height - size.height) + .max(0.0); + if (offset - self.scroll_offset).hypot2() > 1e-12 { + self.scroll_offset = offset; + true + } else { + false + } + } + + /// Makes the scrollbars visible, and resets the fade timer. + pub fn reset_scrollbar_fade(&mut self, request_timer: F, env: &Env) + where + F: FnOnce(Duration) -> TimerToken, + { + // Display scroll bars and schedule their disappearance + self.scrollbars.opacity = env.get(theme::SCROLLBAR_MAX_OPACITY); + let fade_delay = env.get(theme::SCROLLBAR_FADE_DELAY); + let deadline = Duration::from_millis(fade_delay); + self.scrollbars.timer_id = request_timer(deadline); + } + + pub fn calc_vertical_bar_bounds(&self, viewport: Rect, env: &Env) -> Rect { + let bar_width = env.get(theme::SCROLLBAR_WIDTH); + let bar_pad = env.get(theme::SCROLLBAR_PAD); + + let percent_visible = viewport.height() / self.content_size.height; + let percent_scrolled = + self.scroll_offset.y / (self.content_size.height - viewport.height()); + + let length = (percent_visible * viewport.height()).ceil(); + let length = length.max(SCROLLBAR_MIN_SIZE); + + let vertical_padding = bar_pad + bar_pad + bar_width; + + let top_y_offset = + ((viewport.height() - length - vertical_padding) * percent_scrolled).ceil(); + let bottom_y_offset = top_y_offset + length; + + let x0 = self.scroll_offset.x + viewport.width() - bar_width - bar_pad; + let y0 = self.scroll_offset.y + top_y_offset + bar_pad; + + let x1 = self.scroll_offset.x + viewport.width() - bar_pad; + let y1 = self.scroll_offset.y + bottom_y_offset; + + Rect::new(x0, y0, x1, y1) + } + + pub fn calc_horizontal_bar_bounds(&self, viewport: Rect, env: &Env) -> Rect { + let bar_width = env.get(theme::SCROLLBAR_WIDTH); + let bar_pad = env.get(theme::SCROLLBAR_PAD); + + let percent_visible = viewport.width() / self.content_size.width; + let percent_scrolled = self.scroll_offset.x / (self.content_size.width - viewport.width()); + + let length = (percent_visible * viewport.width()).ceil(); + let length = length.max(SCROLLBAR_MIN_SIZE); + + let horizontal_padding = bar_pad + bar_pad + bar_width; + + let left_x_offset = + ((viewport.width() - length - horizontal_padding) * percent_scrolled).ceil(); + let right_x_offset = left_x_offset + length; + + let x0 = self.scroll_offset.x + left_x_offset + bar_pad; + let y0 = self.scroll_offset.y + viewport.height() - bar_width - bar_pad; + + let x1 = self.scroll_offset.x + right_x_offset; + let y1 = self.scroll_offset.y + viewport.height() - bar_pad; + + Rect::new(x0, y0, x1, y1) + } + + /// Draw scroll bars. + pub fn draw_bars(&self, ctx: &mut PaintCtx, viewport: Rect, env: &Env) { + if self.scrollbars.opacity <= 0.0 { + return; + } + + let brush = ctx.render_ctx.solid_brush( + env.get(theme::SCROLLBAR_COLOR) + .with_alpha(self.scrollbars.opacity), + ); + let border_brush = ctx.render_ctx.solid_brush( + env.get(theme::SCROLLBAR_BORDER_COLOR) + .with_alpha(self.scrollbars.opacity), + ); + + let radius = env.get(theme::SCROLLBAR_RADIUS); + let edge_width = env.get(theme::SCROLLBAR_EDGE_WIDTH); + + // Vertical bar + if viewport.height() < self.content_size.height { + let bounds = self + .calc_vertical_bar_bounds(viewport, env) + .inset(-edge_width / 2.0); + let rect = RoundedRect::from_rect(bounds, radius); + ctx.render_ctx.fill(rect, &brush); + ctx.render_ctx.stroke(rect, &border_brush, edge_width); + } + + // Horizontal bar + if viewport.width() < self.content_size.width { + let bounds = self + .calc_horizontal_bar_bounds(viewport, env) + .inset(-edge_width / 2.0); + let rect = RoundedRect::from_rect(bounds, radius); + ctx.render_ctx.fill(rect, &brush); + ctx.render_ctx.stroke(rect, &border_brush, edge_width); + } + } + + pub fn point_hits_vertical_bar(&self, viewport: Rect, pos: Point, env: &Env) -> bool { + if viewport.height() < self.content_size.height { + // Stretch hitbox to edge of widget + let mut bounds = self.calc_vertical_bar_bounds(viewport, env); + bounds.x1 = self.scroll_offset.x + viewport.width(); + bounds.contains(pos) + } else { + false + } + } + + pub fn point_hits_horizontal_bar(&self, viewport: Rect, pos: Point, env: &Env) -> bool { + if viewport.width() < self.content_size.width { + // Stretch hitbox to edge of widget + let mut bounds = self.calc_horizontal_bar_bounds(viewport, env); + bounds.y1 = self.scroll_offset.y + viewport.height(); + bounds.contains(pos) + } else { + false + } + } +} diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index 9de234044a..31bd28b4b3 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -14,92 +14,13 @@ //! A container that scrolls its contents. -use std::f64::INFINITY; -use std::time::Duration; - -use crate::kurbo::{Affine, Point, Rect, RoundedRect, Size, Vec2}; +use crate::kurbo::{Affine, Point, Rect, Size, Vec2}; use crate::theme; use crate::{ - BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, - RenderContext, TimerToken, UpdateCtx, Widget, WidgetPod, + scroll_component::*, BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, + LifeCycleCtx, PaintCtx, RenderContext, TimerToken, UpdateCtx, Widget, WidgetPod, }; -const SCROLLBAR_MIN_SIZE: f64 = 45.0; - -#[derive(Debug, Clone)] -enum ScrollDirection { - Horizontal, - Vertical, - All, -} - -impl ScrollDirection { - /// Return the maximum size the container can be given - /// its scroll direction and box constraints. - /// In practice vertical scrolling will be width limited to - /// box constraints and horizontal will be height limited. - pub fn max_size(&self, bc: &BoxConstraints) -> Size { - match self { - ScrollDirection::Horizontal => Size::new(INFINITY, bc.max().height), - ScrollDirection::Vertical => Size::new(bc.max().width, INFINITY), - ScrollDirection::All => Size::new(INFINITY, INFINITY), - } - } -} - -enum BarHoveredState { - None, - Vertical, - Horizontal, -} - -impl BarHoveredState { - fn is_hovered(&self) -> bool { - match self { - BarHoveredState::Vertical | BarHoveredState::Horizontal => true, - _ => false, - } - } -} - -enum BarHeldState { - None, - /// Vertical scrollbar is being dragged. Contains an `f64` with - /// the initial y-offset of the dragging input - Vertical(f64), - /// Horizontal scrollbar is being dragged. Contains an `f64` with - /// the initial x-offset of the dragging input - Horizontal(f64), -} - -struct ScrollbarsState { - opacity: f64, - timer_id: TimerToken, - hovered: BarHoveredState, - held: BarHeldState, -} - -impl Default for ScrollbarsState { - fn default() -> Self { - Self { - opacity: 0.0, - timer_id: TimerToken::INVALID, - hovered: BarHoveredState::None, - held: BarHeldState::None, - } - } -} - -impl ScrollbarsState { - /// true if either scrollbar is currently held down/being dragged - fn are_held(&self) -> bool { - match self.held { - BarHeldState::None => false, - _ => true, - } - } -} - /// A container that scrolls its contents. /// /// This container holds a single child, and uses the wheel to scroll it @@ -108,10 +29,7 @@ impl ScrollbarsState { /// The child is laid out with completely unconstrained layout bounds. pub struct Scroll { child: WidgetPod, - child_size: Size, - scroll_offset: Vec2, - direction: ScrollDirection, - scrollbars: ScrollbarsState, + scroll_component: ScrollComponent, } impl> Scroll { @@ -123,24 +41,21 @@ impl> Scroll { pub fn new(child: W) -> Scroll { Scroll { child: WidgetPod::new(child), - child_size: Default::default(), - scroll_offset: Vec2::new(0.0, 0.0), - direction: ScrollDirection::All, - scrollbars: ScrollbarsState::default(), + scroll_component: ScrollComponent::new(), } } /// Limit scroll behavior to allow only vertical scrolling (Y-axis). /// The child is laid out with constrained width and infinite height. pub fn vertical(mut self) -> Self { - self.direction = ScrollDirection::Vertical; + self.scroll_component.direction = ScrollDirection::Vertical; self } /// Limit scroll behavior to allow only horizontal scrolling (X-axis). /// The child is laid out with constrained height and infinite width. pub fn horizontal(mut self) -> Self { - self.direction = ScrollDirection::Horizontal; + self.scroll_component.direction = ScrollDirection::Horizontal; self } @@ -156,151 +71,12 @@ impl> Scroll { /// Returns the size of the child widget. pub fn child_size(&self) -> Size { - self.child_size - } - - /// Update the scroll. - /// - /// Returns `true` if the scroll has been updated. - pub fn scroll(&mut self, delta: Vec2, size: Size) -> bool { - let mut offset = self.scroll_offset + delta; - offset.x = offset.x.min(self.child_size.width - size.width).max(0.0); - offset.y = offset.y.min(self.child_size.height - size.height).max(0.0); - if (offset - self.scroll_offset).hypot2() > 1e-12 { - self.scroll_offset = offset; - self.child.set_viewport_offset(offset); - true - } else { - false - } - } - - /// Makes the scrollbars visible, and resets the fade timer. - pub fn reset_scrollbar_fade(&mut self, request_timer: F, env: &Env) - where - F: FnOnce(Duration) -> TimerToken, - { - // Display scroll bars and schedule their disappearance - self.scrollbars.opacity = env.get(theme::SCROLLBAR_MAX_OPACITY); - let fade_delay = env.get(theme::SCROLLBAR_FADE_DELAY); - let deadline = Duration::from_millis(fade_delay); - self.scrollbars.timer_id = request_timer(deadline); + self.scroll_component.content_size } /// Returns the current scroll offset. pub fn offset(&self) -> Vec2 { - self.scroll_offset - } - - fn calc_vertical_bar_bounds(&self, viewport: Rect, env: &Env) -> Rect { - let bar_width = env.get(theme::SCROLLBAR_WIDTH); - let bar_pad = env.get(theme::SCROLLBAR_PAD); - - let percent_visible = viewport.height() / self.child_size.height; - let percent_scrolled = self.scroll_offset.y / (self.child_size.height - viewport.height()); - - let length = (percent_visible * viewport.height()).ceil(); - let length = length.max(SCROLLBAR_MIN_SIZE); - - let vertical_padding = bar_pad + bar_pad + bar_width; - - let top_y_offset = - ((viewport.height() - length - vertical_padding) * percent_scrolled).ceil(); - let bottom_y_offset = top_y_offset + length; - - let x0 = self.scroll_offset.x + viewport.width() - bar_width - bar_pad; - let y0 = self.scroll_offset.y + top_y_offset + bar_pad; - - let x1 = self.scroll_offset.x + viewport.width() - bar_pad; - let y1 = self.scroll_offset.y + bottom_y_offset; - - Rect::new(x0, y0, x1, y1) - } - - fn calc_horizontal_bar_bounds(&self, viewport: Rect, env: &Env) -> Rect { - let bar_width = env.get(theme::SCROLLBAR_WIDTH); - let bar_pad = env.get(theme::SCROLLBAR_PAD); - - let percent_visible = viewport.width() / self.child_size.width; - let percent_scrolled = self.scroll_offset.x / (self.child_size.width - viewport.width()); - - let length = (percent_visible * viewport.width()).ceil(); - let length = length.max(SCROLLBAR_MIN_SIZE); - - let horizontal_padding = bar_pad + bar_pad + bar_width; - - let left_x_offset = - ((viewport.width() - length - horizontal_padding) * percent_scrolled).ceil(); - let right_x_offset = left_x_offset + length; - - let x0 = self.scroll_offset.x + left_x_offset + bar_pad; - let y0 = self.scroll_offset.y + viewport.height() - bar_width - bar_pad; - - let x1 = self.scroll_offset.x + right_x_offset; - let y1 = self.scroll_offset.y + viewport.height() - bar_pad; - - Rect::new(x0, y0, x1, y1) - } - - /// Draw scroll bars. - fn draw_bars(&self, ctx: &mut PaintCtx, viewport: Rect, env: &Env) { - if self.scrollbars.opacity <= 0.0 { - return; - } - - let brush = ctx.render_ctx.solid_brush( - env.get(theme::SCROLLBAR_COLOR) - .with_alpha(self.scrollbars.opacity), - ); - let border_brush = ctx.render_ctx.solid_brush( - env.get(theme::SCROLLBAR_BORDER_COLOR) - .with_alpha(self.scrollbars.opacity), - ); - - let radius = env.get(theme::SCROLLBAR_RADIUS); - let edge_width = env.get(theme::SCROLLBAR_EDGE_WIDTH); - - // Vertical bar - if viewport.height() < self.child_size.height { - let bounds = self - .calc_vertical_bar_bounds(viewport, env) - .inset(-edge_width / 2.0); - let rect = RoundedRect::from_rect(bounds, radius); - ctx.render_ctx.fill(rect, &brush); - ctx.render_ctx.stroke(rect, &border_brush, edge_width); - } - - // Horizontal bar - if viewport.width() < self.child_size.width { - let bounds = self - .calc_horizontal_bar_bounds(viewport, env) - .inset(-edge_width / 2.0); - let rect = RoundedRect::from_rect(bounds, radius); - ctx.render_ctx.fill(rect, &brush); - ctx.render_ctx.stroke(rect, &border_brush, edge_width); - } - } - - fn point_hits_vertical_bar(&self, viewport: Rect, pos: Point, env: &Env) -> bool { - if viewport.height() < self.child_size.height { - // Stretch hitbox to edge of widget - let mut bounds = self.calc_vertical_bar_bounds(viewport, env); - bounds.x1 = self.scroll_offset.x + viewport.width(); - bounds.contains(pos) - } else { - false - } - } - - fn point_hits_horizontal_bar(&self, viewport: Rect, pos: Point, env: &Env) -> bool { - if viewport.width() < self.child_size.width { - // Stretch hitbox to edge of widget - let mut bounds = self.calc_horizontal_bar_bounds(viewport, env); - bounds.y1 = self.scroll_offset.y + viewport.height(); - bounds.contains(pos) - } else { - false - } + self.scroll_component.scroll_offset } } @@ -311,43 +87,55 @@ impl> Widget for Scroll { let scrollbar_is_hovered = match event { Event::MouseMove(e) | Event::MouseUp(e) | Event::MouseDown(e) => { - let offset_pos = e.pos + self.scroll_offset; - self.point_hits_vertical_bar(viewport, offset_pos, env) - || self.point_hits_horizontal_bar(viewport, offset_pos, env) + let offset_pos = e.pos + self.scroll_component.scroll_offset; + self.scroll_component + .point_hits_vertical_bar(viewport, offset_pos, env) + || self + .scroll_component + .point_hits_horizontal_bar(viewport, offset_pos, env) } _ => false, }; - if self.scrollbars.are_held() { + if self.scroll_component.scrollbars.are_held() { // if we're dragging a scrollbar match event { Event::MouseMove(event) => { - match self.scrollbars.held { + match self.scroll_component.scrollbars.held { BarHeldState::Vertical(offset) => { - let scale_y = viewport.height() / self.child_size.height; - let bounds = self.calc_vertical_bar_bounds(viewport, env); - let mouse_y = event.pos.y + self.scroll_offset.y; + let scale_y = + viewport.height() / self.scroll_component.content_size.height; + let bounds = self + .scroll_component + .calc_vertical_bar_bounds(viewport, env); + let mouse_y = event.pos.y + self.scroll_component.scroll_offset.y; let delta = mouse_y - bounds.y0 - offset; - self.scroll(Vec2::new(0f64, (delta / scale_y).ceil()), size); + self.scroll_component + .scroll(Vec2::new(0f64, (delta / scale_y).ceil()), size); } BarHeldState::Horizontal(offset) => { - let scale_x = viewport.width() / self.child_size.width; - let bounds = self.calc_horizontal_bar_bounds(viewport, env); - let mouse_x = event.pos.x + self.scroll_offset.x; + let scale_x = + viewport.width() / self.scroll_component.content_size.width; + let bounds = self + .scroll_component + .calc_horizontal_bar_bounds(viewport, env); + let mouse_x = event.pos.x + self.scroll_component.scroll_offset.x; let delta = mouse_x - bounds.x0 - offset; - self.scroll(Vec2::new((delta / scale_x).ceil(), 0f64), size); + self.scroll_component + .scroll(Vec2::new((delta / scale_x).ceil(), 0f64), size); } _ => (), } ctx.request_paint(); } Event::MouseUp(_) => { - self.scrollbars.held = BarHeldState::None; + self.scroll_component.scrollbars.held = BarHeldState::None; ctx.set_active(false); if !scrollbar_is_hovered { - self.scrollbars.hovered = BarHoveredState::None; - self.reset_scrollbar_fade(|d| ctx.request_timer(d), env); + self.scroll_component.scrollbars.hovered = BarHoveredState::None; + self.scroll_component + .reset_scrollbar_fade(|d| ctx.request_timer(d), env); } } _ => (), // other events are a noop @@ -356,29 +144,47 @@ impl> Widget for Scroll { // if we're over a scrollbar but not dragging match event { Event::MouseMove(event) => { - let offset_pos = event.pos + self.scroll_offset; - if self.point_hits_vertical_bar(viewport, offset_pos, env) { - self.scrollbars.hovered = BarHoveredState::Vertical; + let offset_pos = event.pos + self.scroll_component.scroll_offset; + if self + .scroll_component + .point_hits_vertical_bar(viewport, offset_pos, env) + { + self.scroll_component.scrollbars.hovered = BarHoveredState::Vertical; } else { - self.scrollbars.hovered = BarHoveredState::Horizontal; + self.scroll_component.scrollbars.hovered = BarHoveredState::Horizontal; } - self.scrollbars.opacity = env.get(theme::SCROLLBAR_MAX_OPACITY); - self.scrollbars.timer_id = TimerToken::INVALID; // Cancel any fade out in progress + self.scroll_component.scrollbars.opacity = + env.get(theme::SCROLLBAR_MAX_OPACITY); + self.scroll_component.scrollbars.timer_id = TimerToken::INVALID; // Cancel any fade out in progress ctx.request_paint(); } Event::MouseDown(event) => { - let pos = event.pos + self.scroll_offset; + let pos = event.pos + self.scroll_component.scroll_offset; - if self.point_hits_vertical_bar(viewport, pos, env) { + if self + .scroll_component + .point_hits_vertical_bar(viewport, pos, env) + { ctx.set_active(true); - self.scrollbars.held = BarHeldState::Vertical( - pos.y - self.calc_vertical_bar_bounds(viewport, env).y0, + self.scroll_component.scrollbars.held = BarHeldState::Vertical( + pos.y + - self + .scroll_component + .calc_vertical_bar_bounds(viewport, env) + .y0, ); - } else if self.point_hits_horizontal_bar(viewport, pos, env) { + } else if self + .scroll_component + .point_hits_horizontal_bar(viewport, pos, env) + { ctx.set_active(true); - self.scrollbars.held = BarHeldState::Horizontal( - pos.x - self.calc_horizontal_bar_bounds(viewport, env).x0, + self.scroll_component.scrollbars.held = BarHeldState::Horizontal( + pos.x + - self + .scroll_component + .calc_horizontal_bar_bounds(viewport, env) + .x0, ); } } @@ -388,7 +194,8 @@ impl> Widget for Scroll { } } else { let force_event = self.child.is_hot() || self.child.is_active(); - let child_event = event.transform_scroll(self.scroll_offset, viewport, force_event); + let child_event = + event.transform_scroll(self.scroll_component.scroll_offset, viewport, force_event); if let Some(child_event) = child_event { self.child.event(ctx, &child_event, data, env); }; @@ -396,15 +203,18 @@ impl> Widget for Scroll { match event { Event::MouseMove(_) => { // if we have just stopped hovering - if self.scrollbars.hovered.is_hovered() && !scrollbar_is_hovered { - self.scrollbars.hovered = BarHoveredState::None; - self.reset_scrollbar_fade(|d| ctx.request_timer(d), env); + if self.scroll_component.scrollbars.hovered.is_hovered() + && !scrollbar_is_hovered + { + self.scroll_component.scrollbars.hovered = BarHoveredState::None; + self.scroll_component + .reset_scrollbar_fade(|d| ctx.request_timer(d), env); } } - Event::Timer(id) if *id == self.scrollbars.timer_id => { + Event::Timer(id) if *id == self.scroll_component.scrollbars.timer_id => { // Schedule scroll bars animation ctx.request_anim_frame(); - self.scrollbars.timer_id = TimerToken::INVALID; + self.scroll_component.scrollbars.timer_id = TimerToken::INVALID; } _ => (), } @@ -412,10 +222,11 @@ impl> Widget for Scroll { if !ctx.is_handled() { if let Event::Wheel(mouse) = event { - if self.scroll(mouse.wheel_delta, size) { + if self.scroll_component.scroll(mouse.wheel_delta, size) { ctx.request_paint(); ctx.set_handled(); - self.reset_scrollbar_fade(|d| ctx.request_timer(d), env); + self.scroll_component + .reset_scrollbar_fade(|d| ctx.request_timer(d), env); } } } @@ -426,17 +237,19 @@ impl> Widget for Scroll { LifeCycle::AnimFrame(interval) => { // Guard by the timer id being invalid, otherwise the scroll bars would fade // immediately if some other widgeet started animating. - if self.scrollbars.timer_id == TimerToken::INVALID { + if self.scroll_component.scrollbars.timer_id == TimerToken::INVALID { // Animate scroll bars opacity let diff = 2.0 * (*interval as f64) * 1e-9; - self.scrollbars.opacity -= diff; - if self.scrollbars.opacity > 0.0 { + self.scroll_component.scrollbars.opacity -= diff; + if self.scroll_component.scrollbars.opacity > 0.0 { ctx.request_anim_frame(); } } } // Show the scrollbars any time our size changes - LifeCycle::Size(_) => self.reset_scrollbar_fade(|d| ctx.request_timer(d), &env), + LifeCycle::Size(_) => self + .scroll_component + .reset_scrollbar_fade(|d| ctx.request_timer(d), &env), _ => (), } self.child.lifecycle(ctx, event, data, env) @@ -449,14 +262,15 @@ impl> Widget for Scroll { fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { bc.debug_check("Scroll"); - let child_bc = BoxConstraints::new(Size::ZERO, self.direction.max_size(bc)); + let child_bc = + BoxConstraints::new(Size::ZERO, self.scroll_component.direction.max_size(bc)); let size = self.child.layout(ctx, &child_bc, data, env); log_size_warnings(size); - self.child_size = size; + self.scroll_component.content_size = size; self.child.set_layout_rect(ctx, data, env, size.to_rect()); - let self_size = bc.constrain(self.child_size); - let _ = self.scroll(Vec2::new(0.0, 0.0), self_size); + let self_size = bc.constrain(self.scroll_component.content_size); + let _ = self.scroll_component.scroll(Vec2::new(0.0, 0.0), self_size); self_size } @@ -464,12 +278,12 @@ impl> Widget for Scroll { let viewport = ctx.size().to_rect(); ctx.with_save(|ctx| { ctx.clip(viewport); - ctx.transform(Affine::translate(-self.scroll_offset)); + ctx.transform(Affine::translate(-self.scroll_component.scroll_offset)); - let visible = ctx.region().to_rect() + self.scroll_offset; + let visible = ctx.region().to_rect() + self.scroll_component.scroll_offset; ctx.with_child_ctx(visible, |ctx| self.child.paint_raw(ctx, data, env)); - self.draw_bars(ctx, viewport, env); + self.scroll_component.draw_bars(ctx, viewport, env); }); } } From b00cdcabcbf107c0f3d144e7a6577c1528c2192c Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Tue, 28 Jul 2020 15:23:12 -0400 Subject: [PATCH 02/18] Move a bunch of event logic to `scroll_component` from `Scroll` --- druid/src/scroll_component.rs | 118 +++++++++++++++++++++++++++- druid/src/widget/scroll.rs | 143 +--------------------------------- 2 files changed, 120 insertions(+), 141 deletions(-) diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index edd629daca..07796743df 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -20,7 +20,7 @@ use std::time::Duration; use crate::kurbo::{Point, Rect, RoundedRect, Size, Vec2}; use crate::theme; -use crate::{BoxConstraints, Env, PaintCtx, RenderContext, TimerToken}; +use crate::{BoxConstraints, Env, Event, EventCtx, PaintCtx, RenderContext, TimerToken}; pub const SCROLLBAR_MIN_SIZE: f64 = 45.0; @@ -256,4 +256,120 @@ impl ScrollComponent { false } } + + pub fn filter_event(&mut self, ctx: &mut EventCtx, event: &Event, env: &Env) -> bool { + let size = ctx.size(); + let viewport = Rect::from_origin_size(Point::ORIGIN, size); + + let scrollbar_is_hovered = match event { + Event::MouseMove(e) | Event::MouseUp(e) | Event::MouseDown(e) => { + let offset_pos = e.pos + self.scroll_offset; + self.point_hits_vertical_bar(viewport, offset_pos, env) + || self.point_hits_horizontal_bar(viewport, offset_pos, env) + } + _ => false, + }; + + if self.scrollbars.are_held() { + // if we're dragging a scrollbar + match event { + Event::MouseMove(event) => { + match self.scrollbars.held { + BarHeldState::Vertical(offset) => { + let scale_y = viewport.height() / self.content_size.height; + let bounds = self.calc_vertical_bar_bounds(viewport, env); + let mouse_y = event.pos.y + self.scroll_offset.y; + let delta = mouse_y - bounds.y0 - offset; + self.scroll(Vec2::new(0f64, (delta / scale_y).ceil()), size); + } + BarHeldState::Horizontal(offset) => { + let scale_x = viewport.width() / self.content_size.width; + let bounds = self.calc_horizontal_bar_bounds(viewport, env); + let mouse_x = event.pos.x + self.scroll_offset.x; + let delta = mouse_x - bounds.x0 - offset; + self.scroll(Vec2::new((delta / scale_x).ceil(), 0f64), size); + } + _ => (), + } + ctx.request_paint(); + } + Event::MouseUp(_) => { + self.scrollbars.held = BarHeldState::None; + ctx.set_active(false); + + if !scrollbar_is_hovered { + self.scrollbars.hovered = BarHoveredState::None; + self.reset_scrollbar_fade(|d| ctx.request_timer(d), env); + } + } + _ => (), // other events are a noop + } + } else if scrollbar_is_hovered { + // if we're over a scrollbar but not dragging + match event { + Event::MouseMove(event) => { + let offset_pos = event.pos + self.scroll_offset; + if self.point_hits_vertical_bar(viewport, offset_pos, env) { + self.scrollbars.hovered = BarHoveredState::Vertical; + } else { + self.scrollbars.hovered = BarHoveredState::Horizontal; + } + + self.scrollbars.opacity = env.get(theme::SCROLLBAR_MAX_OPACITY); + self.scrollbars.timer_id = TimerToken::INVALID; // Cancel any fade out in progress + ctx.request_paint(); + } + Event::MouseDown(event) => { + let pos = event.pos + self.scroll_offset; + + if self.point_hits_vertical_bar(viewport, pos, env) { + ctx.set_active(true); + self.scrollbars.held = BarHeldState::Vertical( + pos.y - self.calc_vertical_bar_bounds(viewport, env).y0, + ); + } else if self.point_hits_horizontal_bar(viewport, pos, env) { + ctx.set_active(true); + self.scrollbars.held = BarHeldState::Horizontal( + pos.x - self.calc_horizontal_bar_bounds(viewport, env).x0, + ); + } + } + // if the mouse was downed elsewhere, moved over a scroll bar and released: noop. + Event::MouseUp(_) => (), + _ => unreachable!(), + } + } else { + match event { + Event::MouseMove(_) => { + // if we have just stopped hovering + if self.scrollbars.hovered.is_hovered() && !scrollbar_is_hovered { + self.scrollbars.hovered = BarHoveredState::None; + self.reset_scrollbar_fade(|d| ctx.request_timer(d), env); + } + } + Event::Timer(id) if *id == self.scrollbars.timer_id => { + // Schedule scroll bars animation + ctx.request_anim_frame(); + self.scrollbars.timer_id = TimerToken::INVALID; + } + _ => (), + } + + return false; + } + + true + } + + pub fn check_and_scroll(&mut self, ctx: &mut EventCtx, event: &Event, env: &Env) { + if !ctx.is_handled() { + if let Event::Wheel(mouse) = event { + if self.scroll(mouse.wheel_delta, ctx.size()) { + ctx.request_paint(); + ctx.set_handled(); + self.reset_scrollbar_fade(|d| ctx.request_timer(d), env); + } + } + } + } } diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index 31bd28b4b3..660b971b95 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -15,7 +15,6 @@ //! A container that scrolls its contents. use crate::kurbo::{Affine, Point, Rect, Size, Vec2}; -use crate::theme; use crate::{ scroll_component::*, BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, RenderContext, TimerToken, UpdateCtx, Widget, WidgetPod, @@ -82,154 +81,18 @@ impl> Scroll { impl> Widget for Scroll { fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { - let size = ctx.size(); - let viewport = Rect::from_origin_size(Point::ORIGIN, size); + if !self.scroll_component.filter_event(ctx, event, env) { + let viewport = Rect::from_origin_size(Point::ORIGIN, ctx.size()); - let scrollbar_is_hovered = match event { - Event::MouseMove(e) | Event::MouseUp(e) | Event::MouseDown(e) => { - let offset_pos = e.pos + self.scroll_component.scroll_offset; - self.scroll_component - .point_hits_vertical_bar(viewport, offset_pos, env) - || self - .scroll_component - .point_hits_horizontal_bar(viewport, offset_pos, env) - } - _ => false, - }; - - if self.scroll_component.scrollbars.are_held() { - // if we're dragging a scrollbar - match event { - Event::MouseMove(event) => { - match self.scroll_component.scrollbars.held { - BarHeldState::Vertical(offset) => { - let scale_y = - viewport.height() / self.scroll_component.content_size.height; - let bounds = self - .scroll_component - .calc_vertical_bar_bounds(viewport, env); - let mouse_y = event.pos.y + self.scroll_component.scroll_offset.y; - let delta = mouse_y - bounds.y0 - offset; - self.scroll_component - .scroll(Vec2::new(0f64, (delta / scale_y).ceil()), size); - } - BarHeldState::Horizontal(offset) => { - let scale_x = - viewport.width() / self.scroll_component.content_size.width; - let bounds = self - .scroll_component - .calc_horizontal_bar_bounds(viewport, env); - let mouse_x = event.pos.x + self.scroll_component.scroll_offset.x; - let delta = mouse_x - bounds.x0 - offset; - self.scroll_component - .scroll(Vec2::new((delta / scale_x).ceil(), 0f64), size); - } - _ => (), - } - ctx.request_paint(); - } - Event::MouseUp(_) => { - self.scroll_component.scrollbars.held = BarHeldState::None; - ctx.set_active(false); - - if !scrollbar_is_hovered { - self.scroll_component.scrollbars.hovered = BarHoveredState::None; - self.scroll_component - .reset_scrollbar_fade(|d| ctx.request_timer(d), env); - } - } - _ => (), // other events are a noop - } - } else if scrollbar_is_hovered { - // if we're over a scrollbar but not dragging - match event { - Event::MouseMove(event) => { - let offset_pos = event.pos + self.scroll_component.scroll_offset; - if self - .scroll_component - .point_hits_vertical_bar(viewport, offset_pos, env) - { - self.scroll_component.scrollbars.hovered = BarHoveredState::Vertical; - } else { - self.scroll_component.scrollbars.hovered = BarHoveredState::Horizontal; - } - - self.scroll_component.scrollbars.opacity = - env.get(theme::SCROLLBAR_MAX_OPACITY); - self.scroll_component.scrollbars.timer_id = TimerToken::INVALID; // Cancel any fade out in progress - ctx.request_paint(); - } - Event::MouseDown(event) => { - let pos = event.pos + self.scroll_component.scroll_offset; - - if self - .scroll_component - .point_hits_vertical_bar(viewport, pos, env) - { - ctx.set_active(true); - self.scroll_component.scrollbars.held = BarHeldState::Vertical( - pos.y - - self - .scroll_component - .calc_vertical_bar_bounds(viewport, env) - .y0, - ); - } else if self - .scroll_component - .point_hits_horizontal_bar(viewport, pos, env) - { - ctx.set_active(true); - self.scroll_component.scrollbars.held = BarHeldState::Horizontal( - pos.x - - self - .scroll_component - .calc_horizontal_bar_bounds(viewport, env) - .x0, - ); - } - } - // if the mouse was downed elsewhere, moved over a scroll bar and released: noop. - Event::MouseUp(_) => (), - _ => unreachable!(), - } - } else { let force_event = self.child.is_hot() || self.child.is_active(); let child_event = event.transform_scroll(self.scroll_component.scroll_offset, viewport, force_event); if let Some(child_event) = child_event { self.child.event(ctx, &child_event, data, env); }; - - match event { - Event::MouseMove(_) => { - // if we have just stopped hovering - if self.scroll_component.scrollbars.hovered.is_hovered() - && !scrollbar_is_hovered - { - self.scroll_component.scrollbars.hovered = BarHoveredState::None; - self.scroll_component - .reset_scrollbar_fade(|d| ctx.request_timer(d), env); - } - } - Event::Timer(id) if *id == self.scroll_component.scrollbars.timer_id => { - // Schedule scroll bars animation - ctx.request_anim_frame(); - self.scroll_component.scrollbars.timer_id = TimerToken::INVALID; - } - _ => (), - } } - if !ctx.is_handled() { - if let Event::Wheel(mouse) = event { - if self.scroll_component.scroll(mouse.wheel_delta, size) { - ctx.request_paint(); - ctx.set_handled(); - self.scroll_component - .reset_scrollbar_fade(|d| ctx.request_timer(d), env); - } - } - } + self.scroll_component.check_and_scroll(ctx, event, env); } fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { From d4f390efde2c6785f496338adc6bf9a1610905d2 Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Tue, 28 Jul 2020 15:41:09 -0400 Subject: [PATCH 03/18] Add method to coordinate painting with `scroll_component` --- druid/src/scroll_component.rs | 28 +++++++++++++++++++++++++--- druid/src/widget/scroll.rs | 18 ++++++------------ 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index 07796743df..c0935817ee 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -18,13 +18,13 @@ use std::f64::INFINITY; use std::time::Duration; -use crate::kurbo::{Point, Rect, RoundedRect, Size, Vec2}; +use crate::kurbo::{Affine, Point, Rect, RoundedRect, Size, Vec2}; use crate::theme; -use crate::{BoxConstraints, Env, Event, EventCtx, PaintCtx, RenderContext, TimerToken}; +use crate::{BoxConstraints, Env, Event, EventCtx, PaintCtx, Region, RenderContext, TimerToken}; pub const SCROLLBAR_MIN_SIZE: f64 = 45.0; -#[derive(Debug, Clone)] +#[derive(Debug, Copy, Clone)] pub enum ScrollDirection { Horizontal, Vertical, @@ -45,6 +45,7 @@ impl ScrollDirection { } } +#[derive(Debug, Copy, Clone)] pub enum BarHoveredState { None, Vertical, @@ -60,6 +61,7 @@ impl BarHoveredState { } } +#[derive(Debug, Copy, Clone)] pub enum BarHeldState { None, /// Vertical scrollbar is being dragged. Contains an `f64` with @@ -70,6 +72,7 @@ pub enum BarHeldState { Horizontal(f64), } +#[derive(Debug, Copy, Clone)] pub struct ScrollbarsState { pub opacity: f64, pub timer_id: TimerToken, @@ -98,6 +101,7 @@ impl ScrollbarsState { } } +#[derive(Debug, Copy, Clone)] pub struct ScrollComponent { pub content_size: Size, pub scroll_offset: Vec2, @@ -372,4 +376,22 @@ impl ScrollComponent { } } } + + pub fn draw_content( + self, + ctx: &mut PaintCtx, + env: &Env, + f: impl FnOnce(Region, &mut PaintCtx), + ) { + let viewport = ctx.size().to_rect(); + ctx.with_save(|ctx| { + ctx.clip(viewport); + ctx.transform(Affine::translate(-self.scroll_offset)); + + let visible = ctx.region().to_rect() + self.scroll_offset; + f(visible.into(), ctx); + + self.draw_bars(ctx, viewport, env); + }); + } } diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index 660b971b95..ff345f71b9 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -14,10 +14,10 @@ //! A container that scrolls its contents. -use crate::kurbo::{Affine, Point, Rect, Size, Vec2}; +use crate::kurbo::{Point, Rect, Size, Vec2}; use crate::{ scroll_component::*, BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, - LifeCycleCtx, PaintCtx, RenderContext, TimerToken, UpdateCtx, Widget, WidgetPod, + LifeCycleCtx, PaintCtx, TimerToken, UpdateCtx, Widget, WidgetPod, }; /// A container that scrolls its contents. @@ -138,16 +138,10 @@ impl> Widget for Scroll { } fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { - let viewport = ctx.size().to_rect(); - ctx.with_save(|ctx| { - ctx.clip(viewport); - ctx.transform(Affine::translate(-self.scroll_component.scroll_offset)); - - let visible = ctx.region().to_rect() + self.scroll_component.scroll_offset; - ctx.with_child_ctx(visible, |ctx| self.child.paint_raw(ctx, data, env)); - - self.scroll_component.draw_bars(ctx, viewport, env); - }); + self.scroll_component + .draw_content(ctx, env, |visible, ctx| { + ctx.with_child_ctx(visible, |ctx| self.child.paint_raw(ctx, data, env)); + }); } } From a19f58ec74d06fd851946f58ae7cdcfc80ca2364 Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Tue, 28 Jul 2020 15:56:37 -0400 Subject: [PATCH 04/18] Move a bunch of lifecycle logic to `scroll_component` from `Scroll` --- druid/src/scroll_component.rs | 37 ++++++++++++++++++++++++++++++++++- druid/src/widget/scroll.rs | 23 +++------------------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index c0935817ee..4b76794e42 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -20,7 +20,10 @@ use std::time::Duration; use crate::kurbo::{Affine, Point, Rect, RoundedRect, Size, Vec2}; use crate::theme; -use crate::{BoxConstraints, Env, Event, EventCtx, PaintCtx, Region, RenderContext, TimerToken}; +use crate::{ + BoxConstraints, Env, Event, EventCtx, LifeCycle, LifeCycleCtx, PaintCtx, Region, RenderContext, + TimerToken, +}; pub const SCROLLBAR_MIN_SIZE: f64 = 45.0; @@ -377,6 +380,38 @@ impl ScrollComponent { } } + pub fn filter_lifecycle( + &mut self, + ctx: &mut LifeCycleCtx, + event: &LifeCycle, + env: &Env, + ) -> bool { + match event { + LifeCycle::AnimFrame(interval) => { + // Guard by the timer id being invalid, otherwise the scroll bars would fade + // immediately if some other widget started animating. + if self.scrollbars.timer_id == TimerToken::INVALID { + // Animate scroll bars opacity + let diff = 2.0 * (*interval as f64) * 1e-9; + self.scrollbars.opacity -= diff; + if self.scrollbars.opacity > 0.0 { + ctx.request_anim_frame(); + } + + return true; + } + } + // Show the scrollbars any time our size changes + LifeCycle::Size(_) => { + self.reset_scrollbar_fade(|d| ctx.request_timer(d), &env); + return true; + } + _ => (), + } + + false + } + pub fn draw_content( self, ctx: &mut PaintCtx, diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index ff345f71b9..1a46c149e3 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -17,7 +17,7 @@ use crate::kurbo::{Point, Rect, Size, Vec2}; use crate::{ scroll_component::*, BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, - LifeCycleCtx, PaintCtx, TimerToken, UpdateCtx, Widget, WidgetPod, + LifeCycleCtx, PaintCtx, UpdateCtx, Widget, WidgetPod, }; /// A container that scrolls its contents. @@ -96,26 +96,9 @@ impl> Widget for Scroll { } fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { - match event { - LifeCycle::AnimFrame(interval) => { - // Guard by the timer id being invalid, otherwise the scroll bars would fade - // immediately if some other widgeet started animating. - if self.scroll_component.scrollbars.timer_id == TimerToken::INVALID { - // Animate scroll bars opacity - let diff = 2.0 * (*interval as f64) * 1e-9; - self.scroll_component.scrollbars.opacity -= diff; - if self.scroll_component.scrollbars.opacity > 0.0 { - ctx.request_anim_frame(); - } - } - } - // Show the scrollbars any time our size changes - LifeCycle::Size(_) => self - .scroll_component - .reset_scrollbar_fade(|d| ctx.request_timer(d), &env), - _ => (), + if !self.scroll_component.filter_lifecycle(ctx, event, env) { + self.child.lifecycle(ctx, event, data, env); } - self.child.lifecycle(ctx, event, data, env) } fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) { From b5703a97934837a6e6b0f73520ba815ad6b28574 Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Tue, 28 Jul 2020 17:18:27 -0400 Subject: [PATCH 05/18] Documentation pass on `scroll_component` --- druid/src/scroll_component.rs | 30 +++++++++++++++++++++++++++++- druid/src/widget/scroll.rs | 2 +- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index 4b76794e42..982e53ea62 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -152,7 +152,14 @@ impl ScrollComponent { self.scrollbars.timer_id = request_timer(deadline); } + /// Calculates the paint rect of the vertical scrollbar. + /// + /// Returns `Rect::ZERO` if the vertical scrollbar is not visible. pub fn calc_vertical_bar_bounds(&self, viewport: Rect, env: &Env) -> Rect { + if viewport.height() >= self.content_size.height { + return Rect::ZERO; + } + let bar_width = env.get(theme::SCROLLBAR_WIDTH); let bar_pad = env.get(theme::SCROLLBAR_PAD); @@ -178,7 +185,14 @@ impl ScrollComponent { Rect::new(x0, y0, x1, y1) } + /// Calculates the paint rect of the horizontal scrollbar. + /// + /// Returns `Rect::ZERO` if the horizontal scrollbar is not visible. pub fn calc_horizontal_bar_bounds(&self, viewport: Rect, env: &Env) -> Rect { + if viewport.width() >= self.content_size.width { + return Rect::ZERO; + } + let bar_width = env.get(theme::SCROLLBAR_WIDTH); let bar_pad = env.get(theme::SCROLLBAR_PAD); @@ -242,6 +256,9 @@ impl ScrollComponent { } } + /// Tests if the specified point overlaps the vertical scrollbar + /// + /// Returns false if the vertical scrollbar is not visible pub fn point_hits_vertical_bar(&self, viewport: Rect, pos: Point, env: &Env) -> bool { if viewport.height() < self.content_size.height { // Stretch hitbox to edge of widget @@ -253,6 +270,9 @@ impl ScrollComponent { } } + /// Tests if the specified point overlaps the horizontal scrollbar + /// + /// Returns false if the horizontal scrollbar is not visible pub fn point_hits_horizontal_bar(&self, viewport: Rect, pos: Point, env: &Env) -> bool { if viewport.width() < self.content_size.width { // Stretch hitbox to edge of widget @@ -264,6 +284,9 @@ impl ScrollComponent { } } + /// Checks if the event applies to the scroll behavior, uses it and returns true if so + /// + /// Returns false if the event was not used pub fn filter_event(&mut self, ctx: &mut EventCtx, event: &Event, env: &Env) -> bool { let size = ctx.size(); let viewport = Rect::from_origin_size(Point::ORIGIN, size); @@ -368,7 +391,8 @@ impl ScrollComponent { true } - pub fn check_and_scroll(&mut self, ctx: &mut EventCtx, event: &Event, env: &Env) { + /// Applies mousewheel scrolling if the event has not already been handled + pub fn handle_scroll(&mut self, ctx: &mut EventCtx, event: &Event, env: &Env) { if !ctx.is_handled() { if let Event::Wheel(mouse) = event { if self.scroll(mouse.wheel_delta, ctx.size()) { @@ -380,6 +404,9 @@ impl ScrollComponent { } } + /// Checks if the lifecycle event applies to the scroll behavior, uses it and returns true if so + /// + /// Returns false if the lifecycle event was not used pub fn filter_lifecycle( &mut self, ctx: &mut LifeCycleCtx, @@ -412,6 +439,7 @@ impl ScrollComponent { false } + /// Helper function to draw a closure at the correct offset with clipping and scrollbars pub fn draw_content( self, ctx: &mut PaintCtx, diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index 1a46c149e3..85bb853e22 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -92,7 +92,7 @@ impl> Widget for Scroll { }; } - self.scroll_component.check_and_scroll(ctx, event, env); + self.scroll_component.handle_scroll(ctx, event, env); } fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { From 3a230a3181d182ba000062a46148019d4172f96c Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Tue, 28 Jul 2020 19:54:50 -0400 Subject: [PATCH 06/18] Rename `Scroll` to `AbsoluteScroll` & add scroll functionality to `List` --- druid/examples/invalidation.rs | 8 ++- druid/examples/list.rs | 12 ++-- druid/examples/scroll.rs | 4 +- druid/examples/scroll_colors.rs | 4 +- druid/src/core.rs | 4 +- druid/src/scroll_component.rs | 4 +- .../widget/{scroll.rs => absolute_scroll.rs} | 29 ++------ druid/src/widget/list.rs | 70 ++++++++++++------- druid/src/widget/mod.rs | 4 +- 9 files changed, 74 insertions(+), 65 deletions(-) rename druid/src/widget/{scroll.rs => absolute_scroll.rs} (81%) diff --git a/druid/examples/invalidation.rs b/druid/examples/invalidation.rs index 5ad18fb336..1ab3f8a10b 100644 --- a/druid/examples/invalidation.rs +++ b/druid/examples/invalidation.rs @@ -17,7 +17,7 @@ use druid::kurbo::{Circle, Shape}; use druid::widget::prelude::*; -use druid::widget::{Button, Flex, Scroll, Split, TextBox}; +use druid::widget::{AbsoluteScroll, Button, Flex, Split, TextBox}; use druid::{AppLauncher, Color, Data, Lens, LocalizedString, Point, WidgetExt, WindowDesc}; pub fn main() { @@ -46,7 +46,11 @@ fn build_widget() -> impl Widget { for i in 0..30 { col.add_child(Button::new(format!("Button {}", i)).padding(3.0)); } - Split::columns(Scroll::new(col), CircleView.lens(AppState::circle_pos)).debug_invalidation() + Split::columns( + AbsoluteScroll::new(col), + CircleView.lens(AppState::circle_pos), + ) + .debug_invalidation() } struct CircleView; diff --git a/druid/examples/list.rs b/druid/examples/list.rs index 7c9f04fe3c..d6f71f4937 100644 --- a/druid/examples/list.rs +++ b/druid/examples/list.rs @@ -16,7 +16,7 @@ use druid::im::{vector, Vector}; use druid::lens::{self, LensExt}; -use druid::widget::{Button, CrossAxisAlignment, Flex, Label, List, Scroll}; +use druid::widget::{Button, CrossAxisAlignment, Flex, Label, List}; use druid::{ AppLauncher, Color, Data, Lens, LocalizedString, UnitPoint, Widget, WidgetExt, WindowDesc, }; @@ -64,22 +64,21 @@ fn ui_builder() -> impl Widget { // Build a simple list lists.add_flex_child( - Scroll::new(List::new(|| { + List::new(|| { Label::new(|item: &u32, _env: &_| format!("List item #{}", item)) .align_vertical(UnitPoint::LEFT) .padding(10.0) .expand() .height(50.0) .background(Color::rgb(0.5, 0.5, 0.5)) - })) - .vertical() + }) .lens(AppData::left), 1.0, ); // Build a list with shared data lists.add_flex_child( - Scroll::new(List::new(|| { + List::new(|| { Flex::row() .with_child( Label::new(|(_, item): &(Vector, u32), _env: &_| { @@ -101,8 +100,7 @@ fn ui_builder() -> impl Widget { .padding(10.0) .background(Color::rgb(0.5, 0.0, 0.5)) .fix_height(50.0) - })) - .vertical() + }) .lens(lens::Id.map( // Expose shared data with children data |d: &AppData| (d.right.clone(), d.right.clone()), diff --git a/druid/examples/scroll.rs b/druid/examples/scroll.rs index 14dd7de888..97d8542d81 100644 --- a/druid/examples/scroll.rs +++ b/druid/examples/scroll.rs @@ -18,7 +18,7 @@ use druid::kurbo::Circle; use druid::piet::RadialGradient; use druid::widget::prelude::*; -use druid::widget::{Flex, Padding, Scroll}; +use druid::widget::{AbsoluteScroll, Flex, Padding}; use druid::{AppLauncher, Data, Insets, LocalizedString, Rect, WindowDesc}; pub fn main() { @@ -35,7 +35,7 @@ fn build_widget() -> impl Widget { for i in 0..30 { col.add_child(Padding::new(3.0, OverPainter(i))); } - Scroll::new(col) + AbsoluteScroll::new(col) } /// A widget that paints outside of its bounds. diff --git a/druid/examples/scroll_colors.rs b/druid/examples/scroll_colors.rs index f02389fdc3..e90d86573f 100644 --- a/druid/examples/scroll_colors.rs +++ b/druid/examples/scroll_colors.rs @@ -14,7 +14,7 @@ //! This example allows to play with scroll bars over different color tones. -use druid::widget::{Container, Flex, Scroll, SizedBox}; +use druid::widget::{AbsoluteScroll, Container, Flex, SizedBox}; use druid::{AppLauncher, Color, LocalizedString, Widget, WindowDesc}; fn build_app() -> impl Widget { @@ -38,7 +38,7 @@ fn build_app() -> impl Widget { col.add_child(row); } - Scroll::new(col) + AbsoluteScroll::new(col) } pub fn main() { diff --git a/druid/src/core.rs b/druid/src/core.rs index e9a024bc4f..087b7abed9 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -951,7 +951,7 @@ impl WidgetState { #[cfg(test)] mod tests { use super::*; - use crate::widget::{Flex, Scroll, Split, TextBox}; + use crate::widget::{AbsoluteScroll, Flex, Split, TextBox}; use crate::{WidgetExt, WindowHandle, WindowId}; const ID_1: WidgetId = WidgetId::reserved(0); @@ -966,7 +966,7 @@ mod tests { .with_child(TextBox::new().with_id(ID_1).parse()) .with_child(TextBox::new().with_id(ID_2).parse()) .with_child(TextBox::new().with_id(ID_3).parse()), - Scroll::new(TextBox::new().parse()), + AbsoluteScroll::new(TextBox::new().parse()), ) } diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index 982e53ea62..279ee09d00 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -439,8 +439,8 @@ impl ScrollComponent { false } - /// Helper function to draw a closure at the correct offset with clipping and scrollbars - pub fn draw_content( + /// Helper function to paint a closure at the correct offset with clipping and scrollbars + pub fn paint_content( self, ctx: &mut PaintCtx, env: &Env, diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/absolute_scroll.rs similarity index 81% rename from druid/src/widget/scroll.rs rename to druid/src/widget/absolute_scroll.rs index 85bb853e22..48d1af9661 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/absolute_scroll.rs @@ -26,38 +26,23 @@ use crate::{ /// when the child's bounds are larger than the viewport. /// /// The child is laid out with completely unconstrained layout bounds. -pub struct Scroll { +pub struct AbsoluteScroll { child: WidgetPod, scroll_component: ScrollComponent, } -impl> Scroll { +impl> AbsoluteScroll { /// Create a new scroll container. /// /// This method will allow scrolling in all directions if child's bounds - /// are larger than the viewport. Use [vertical](#method.vertical) - /// and [horizontal](#method.horizontal) methods to limit scroll behavior. - pub fn new(child: W) -> Scroll { - Scroll { + /// are larger than the viewport. + pub fn new(child: W) -> AbsoluteScroll { + AbsoluteScroll { child: WidgetPod::new(child), scroll_component: ScrollComponent::new(), } } - /// Limit scroll behavior to allow only vertical scrolling (Y-axis). - /// The child is laid out with constrained width and infinite height. - pub fn vertical(mut self) -> Self { - self.scroll_component.direction = ScrollDirection::Vertical; - self - } - - /// Limit scroll behavior to allow only horizontal scrolling (X-axis). - /// The child is laid out with constrained height and infinite width. - pub fn horizontal(mut self) -> Self { - self.scroll_component.direction = ScrollDirection::Horizontal; - self - } - /// Returns a reference to the child widget. pub fn child(&self) -> &W { self.child.widget() @@ -79,7 +64,7 @@ impl> Scroll { } } -impl> Widget for Scroll { +impl> Widget for AbsoluteScroll { fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { if !self.scroll_component.filter_event(ctx, event, env) { let viewport = Rect::from_origin_size(Point::ORIGIN, ctx.size()); @@ -122,7 +107,7 @@ impl> Widget for Scroll { fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { self.scroll_component - .draw_content(ctx, env, |visible, ctx| { + .paint_content(ctx, env, |visible, ctx| { ctx.with_child_ctx(visible, |ctx| self.child.paint_raw(ctx, data, env)); }); } diff --git a/druid/src/widget/list.rs b/druid/src/widget/list.rs index c3c84c9542..248f1d9323 100644 --- a/druid/src/widget/list.rs +++ b/druid/src/widget/list.rs @@ -23,14 +23,15 @@ use crate::im::Vector; use crate::kurbo::{Point, Rect, Size}; use crate::{ - BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, - UpdateCtx, Widget, WidgetPod, + scroll_component::*, BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, + LifeCycleCtx, PaintCtx, UpdateCtx, Widget, WidgetPod, }; /// A list widget for a variable-size collection of items. pub struct List { closure: Box Box>>, children: Vec>>>, + scroll_component: ScrollComponent, } impl List { @@ -40,6 +41,7 @@ impl List { List { closure: Box::new(move || Box::new(closure())), children: Vec::new(), + scroll_component: ScrollComponent::new(), } } @@ -195,27 +197,40 @@ impl ListIter<(S, T)> for (S, Arc>) { impl> Widget for List { fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { - let mut children = self.children.iter_mut(); - data.for_each_mut(|child_data, _| { - if let Some(child) = children.next() { - child.event(ctx, event, child_data, env); - } - }); + if !self.scroll_component.filter_event(ctx, event, env) { + let viewport = Rect::from_origin_size(Point::ORIGIN, ctx.size()); + let scroll_offset = self.scroll_component.scroll_offset; + let mut children = self.children.iter_mut(); + + data.for_each_mut(|child_data, _| { + if let Some(child) = children.next() { + let force_event = child.is_hot() || child.is_active(); + let child_event = event.transform_scroll(scroll_offset, viewport, force_event); + if let Some(child_event) = child_event { + child.event(ctx, &child_event, child_data, env); + } + } + }); + } + + self.scroll_component.handle_scroll(ctx, event, env); } fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { - if let LifeCycle::WidgetAdded = event { - if self.update_child_count(data, env) { - ctx.children_changed(); + if !self.scroll_component.filter_lifecycle(ctx, event, env) { + if let LifeCycle::WidgetAdded = event { + if self.update_child_count(data, env) { + ctx.children_changed(); + } } - } - let mut children = self.children.iter_mut(); - data.for_each(|child_data, _| { - if let Some(child) = children.next() { - child.lifecycle(ctx, event, child_data, env); - } - }); + let mut children = self.children.iter_mut(); + data.for_each(|child_data, _| { + if let Some(child) = children.next() { + child.lifecycle(ctx, event, child_data, env); + } + }); + } } fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) { @@ -259,6 +274,8 @@ impl> Widget for List { y += child_size.height; }); + self.scroll_component.content_size = Size::new(width, y); + let my_size = bc.constrain(Size::new(width, y)); let insets = paint_rect - Rect::ZERO.with_size(my_size); ctx.set_paint_insets(insets); @@ -266,11 +283,16 @@ impl> Widget for List { } fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { - let mut children = self.children.iter_mut(); - data.for_each(|child_data, _| { - if let Some(child) = children.next() { - child.paint(ctx, child_data, env); - } - }); + self.scroll_component + .paint_content(ctx, env, |visible, ctx| { + let mut children = self.children.iter_mut(); + data.for_each(|child_data, _| { + if let Some(child) = children.next() { + ctx.with_child_ctx(visible.clone(), |ctx| { + child.paint(ctx, child_data, env) + }); + } + }) + }); } } diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 27807da3f8..b7bb8b6f2a 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -14,6 +14,7 @@ //! Common widgets. +mod absolute_scroll; mod align; mod button; mod checkbox; @@ -34,7 +35,6 @@ mod painter; mod parse; mod progress_bar; mod radio; -mod scroll; mod sized_box; mod slider; mod spinner; @@ -51,6 +51,7 @@ mod widget; mod widget_ext; pub use self::image::{Image, ImageData}; +pub use absolute_scroll::AbsoluteScroll; pub use align::Align; pub use button::Button; pub use checkbox::Checkbox; @@ -69,7 +70,6 @@ pub use painter::{BackgroundBrush, Painter}; pub use parse::Parse; pub use progress_bar::ProgressBar; pub use radio::{Radio, RadioGroup}; -pub use scroll::Scroll; pub use sized_box::SizedBox; pub use slider::Slider; pub use spinner::Spinner; From bab2157ec8abd98ab6930ceab5ca396b6046da64 Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Tue, 28 Jul 2020 22:29:36 -0400 Subject: [PATCH 07/18] Add ability to change layout axis for `List` with builder fn --- druid/src/scroll_component.rs | 27 +---------- druid/src/widget/absolute_scroll.rs | 5 ++- druid/src/widget/list.rs | 70 +++++++++++++++++++++++------ 3 files changed, 60 insertions(+), 42 deletions(-) diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index 279ee09d00..eedd2df13e 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -15,39 +15,16 @@ //! A component for embedding in another widget to provide consistant and //! extendable scrolling behavior -use std::f64::INFINITY; use std::time::Duration; use crate::kurbo::{Affine, Point, Rect, RoundedRect, Size, Vec2}; use crate::theme; use crate::{ - BoxConstraints, Env, Event, EventCtx, LifeCycle, LifeCycleCtx, PaintCtx, Region, RenderContext, - TimerToken, + Env, Event, EventCtx, LifeCycle, LifeCycleCtx, PaintCtx, Region, RenderContext, TimerToken, }; pub const SCROLLBAR_MIN_SIZE: f64 = 45.0; -#[derive(Debug, Copy, Clone)] -pub enum ScrollDirection { - Horizontal, - Vertical, - All, -} - -impl ScrollDirection { - /// Return the maximum size the container can be given - /// its scroll direction and box constraints. - /// In practice vertical scrolling will be width limited to - /// box constraints and horizontal will be height limited. - pub fn max_size(&self, bc: &BoxConstraints) -> Size { - match self { - ScrollDirection::Horizontal => Size::new(INFINITY, bc.max().height), - ScrollDirection::Vertical => Size::new(bc.max().width, INFINITY), - ScrollDirection::All => Size::new(INFINITY, INFINITY), - } - } -} - #[derive(Debug, Copy, Clone)] pub enum BarHoveredState { None, @@ -108,7 +85,6 @@ impl ScrollbarsState { pub struct ScrollComponent { pub content_size: Size, pub scroll_offset: Vec2, - pub direction: ScrollDirection, pub scrollbars: ScrollbarsState, } @@ -117,7 +93,6 @@ impl ScrollComponent { ScrollComponent { content_size: Default::default(), scroll_offset: Vec2::new(0.0, 0.0), - direction: ScrollDirection::All, scrollbars: ScrollbarsState::default(), } } diff --git a/druid/src/widget/absolute_scroll.rs b/druid/src/widget/absolute_scroll.rs index 48d1af9661..93279eb769 100644 --- a/druid/src/widget/absolute_scroll.rs +++ b/druid/src/widget/absolute_scroll.rs @@ -14,6 +14,8 @@ //! A container that scrolls its contents. +use std::f64::INFINITY; + use crate::kurbo::{Point, Rect, Size, Vec2}; use crate::{ scroll_component::*, BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, @@ -93,8 +95,7 @@ impl> Widget for AbsoluteScroll { fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { bc.debug_check("Scroll"); - let child_bc = - BoxConstraints::new(Size::ZERO, self.scroll_component.direction.max_size(bc)); + let child_bc = BoxConstraints::new(Size::ZERO, Size::new(INFINITY, INFINITY)); let size = self.child.layout(ctx, &child_bc, data, env); log_size_warnings(size); diff --git a/druid/src/widget/list.rs b/druid/src/widget/list.rs index 248f1d9323..78ea39dcc5 100644 --- a/druid/src/widget/list.rs +++ b/druid/src/widget/list.rs @@ -27,24 +27,38 @@ use crate::{ LifeCycleCtx, PaintCtx, UpdateCtx, Widget, WidgetPod, }; +#[derive(Debug, Copy, Clone)] +pub enum ListDirection { + Vertical, + Horizontal, +} + /// A list widget for a variable-size collection of items. pub struct List { closure: Box Box>>, children: Vec>>>, + direction: ListDirection, scroll_component: ScrollComponent, } impl List { /// Create a new list widget. Closure will be called every time when a new child - /// needs to be constructed. + /// needs to be constructed. Children will layed out vertically Use + /// [horizontal](#method.horizontal) to lay out items horizontally. pub fn new + 'static>(closure: impl Fn() -> W + 'static) -> Self { List { closure: Box::new(move || Box::new(closure())), children: Vec::new(), + direction: ListDirection::Vertical, scroll_component: ScrollComponent::new(), } } + pub fn horizontal(mut self) -> Self { + self.direction = ListDirection::Horizontal; + self + } + /// When the widget is created or the data changes, create or remove children as needed /// /// Returns `true` if children were added or removed. @@ -250,8 +264,12 @@ impl> Widget for List { } fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { - let mut width = bc.min().width; - let mut y = 0.0; + let my_size = bc.constrain(bc.max()); + let layout_direction = self.direction; + let (mut width, mut height) = match layout_direction { + ListDirection::Vertical => (bc.min().width, 0.0), + ListDirection::Horizontal => (0.0, bc.min().height), + }; let mut paint_rect = Rect::ZERO; let mut children = self.children.iter_mut(); @@ -262,21 +280,45 @@ impl> Widget for List { return; } }; - let child_bc = BoxConstraints::new( - Size::new(bc.min().width, 0.0), - Size::new(bc.max().width, std::f64::INFINITY), - ); - let child_size = child.layout(ctx, &child_bc, child_data, env); - let rect = Rect::from_origin_size(Point::new(0.0, y), child_size); + let child_bc = match layout_direction { + ListDirection::Vertical => BoxConstraints::new( + Size::new(bc.min().width, 0.0), + Size::new(bc.max().width, std::f64::INFINITY), + ), + ListDirection::Horizontal => BoxConstraints::new( + Size::new(0.0, bc.min().height), + Size::new(std::f64::INFINITY, bc.max().height), + ), + }; + let child_size = match layout_direction { + ListDirection::Vertical => Size::new( + my_size.width, + child.layout(ctx, &child_bc, child_data, env).height, + ), + ListDirection::Horizontal => Size::new( + child.layout(ctx, &child_bc, child_data, env).width, + my_size.height, + ), + }; + let rect = match layout_direction { + ListDirection::Vertical => { + Rect::from_origin_size(Point::new(0.0, height), child_size) + } + + ListDirection::Horizontal => { + Rect::from_origin_size(Point::new(width, 0.0), child_size) + } + }; child.set_layout_rect(ctx, child_data, env, rect); paint_rect = paint_rect.union(child.paint_rect()); - width = width.max(child_size.width); - y += child_size.height; - }); - self.scroll_component.content_size = Size::new(width, y); + match layout_direction { + ListDirection::Vertical => height += child_size.height, + ListDirection::Horizontal => width += child_size.width, + } + }); - let my_size = bc.constrain(Size::new(width, y)); + self.scroll_component.content_size = Size::new(width, height); let insets = paint_rect - Rect::ZERO.with_size(my_size); ctx.set_paint_insets(insets); my_size From 803ab74ecf16c8b58309130867ee525401291f03 Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Tue, 28 Jul 2020 23:07:21 -0400 Subject: [PATCH 08/18] Scroll behavior component changelog --- AUTHORS | 1 + CHANGELOG.md | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/AUTHORS b/AUTHORS index 11b8a359e6..0b21e74031 100644 --- a/AUTHORS +++ b/AUTHORS @@ -11,3 +11,4 @@ Kaiyin Zhong Kaur Kuut Leopold Luley Andrey Kabylin +Garrett Risley diff --git a/CHANGELOG.md b/CHANGELOG.md index 3edbe02397..76c12b87d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,15 @@ You can find its changes [documented below](#060---2020-06-01). - Re-export `druid_shell::Scalable` under `druid` namespace. ([#1075] by [@ForLoveOfCats]) - `TextBox` now supports ctrl and shift hotkeys. ([#1076] by [@vkahl]) - Added selection text color to textbox. ([#1093] by [@sysint64]) +- `scroll_component` for ease of adding consistant, customized, scrolling behavior to a widget. ([#1107] by [@ForLoveOfCats]) ### Changed - `Scale::from_scale` to `Scale::new`, and `Scale` methods `scale_x` / `scale_y` to `x` / `y`. ([#1042] by [@xStrom]) - Major rework of keyboard event handling. ([#1049] by [@raphlinus]) - `Container::rounded` takes `KeyOrValue` instead of `f64`. ([#1054] by [@binomial0]) +- `Scroll` renamed to `AbsoluteScroll`. ([#1107] by [@ForLoveOfCats]) +- `List` handles its own scrolling on either the vertical or horizontal axis. ([#1107] by [@ForLoveOfCats]) ### Deprecated @@ -27,6 +30,7 @@ You can find its changes [documented below](#060---2020-06-01). - `Scale::from_dpi`, `Scale::dpi_x`, and `Scale::dpi_y`. ([#1042] by [@xStrom]) - `Scale::to_px` and `Scale::to_dp`. ([#1075] by [@ForLoveOfCats]) +- Specific axis scrolling behavior from `AbsoluteScroll`. ([#1107] by [@ForLoveOfCats]) ### Fixed @@ -375,6 +379,7 @@ Last release without a changelog :( [#1093]: https://github.com/linebender/druid/pull/1093 [#1100]: https://github.com/linebender/druid/pull/1100 [#1103]: https://github.com/linebender/druid/pull/1103 +[#1107]: https://github.com/linebender/druid/pull/1107 [Unreleased]: https://github.com/linebender/druid/compare/v0.6.0...master From 7f6ea8f9efd796529b5a36f955eec1c1fecff5fa Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Tue, 28 Jul 2020 23:15:07 -0400 Subject: [PATCH 09/18] Pass `BarHoveredState` by value and add impl Default for ScrollComponent --- druid/src/scroll_component.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index eedd2df13e..ffc75f9307 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -33,7 +33,7 @@ pub enum BarHoveredState { } impl BarHoveredState { - pub fn is_hovered(&self) -> bool { + pub fn is_hovered(self) -> bool { match self { BarHoveredState::Vertical | BarHoveredState::Horizontal => true, _ => false, @@ -88,6 +88,12 @@ pub struct ScrollComponent { pub scrollbars: ScrollbarsState, } +impl Default for ScrollComponent { + fn default() -> Self { + ScrollComponent::new() + } +} + impl ScrollComponent { pub fn new() -> ScrollComponent { ScrollComponent { From 47077a9d1951ac16eb7fcb5d56beb8ff4a4d2df4 Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Wed, 12 Aug 2020 22:24:40 -0400 Subject: [PATCH 10/18] `ScrollComponent` review changes --- CHANGELOG.md | 1 - druid/Cargo.toml | 5 +- druid/examples/invalidation.rs | 8 +-- druid/examples/scroll.rs | 4 +- druid/examples/scroll_colors.rs | 4 +- druid/src/core.rs | 4 +- druid/src/scroll_component.rs | 70 +++++++++++-------- druid/src/widget/list.rs | 38 +++++----- druid/src/widget/mod.rs | 4 +- .../widget/{absolute_scroll.rs => scroll.rs} | 66 ++++++++++++----- 10 files changed, 125 insertions(+), 79 deletions(-) rename druid/src/widget/{absolute_scroll.rs => scroll.rs} (65%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76c12b87d8..fb88d0a84f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,6 @@ You can find its changes [documented below](#060---2020-06-01). - `Scale::from_scale` to `Scale::new`, and `Scale` methods `scale_x` / `scale_y` to `x` / `y`. ([#1042] by [@xStrom]) - Major rework of keyboard event handling. ([#1049] by [@raphlinus]) - `Container::rounded` takes `KeyOrValue` instead of `f64`. ([#1054] by [@binomial0]) -- `Scroll` renamed to `AbsoluteScroll`. ([#1107] by [@ForLoveOfCats]) - `List` handles its own scrolling on either the vertical or horizontal axis. ([#1107] by [@ForLoveOfCats]) ### Deprecated diff --git a/druid/Cargo.toml b/druid/Cargo.toml index f335c82825..6caec8ccb3 100644 --- a/druid/Cargo.toml +++ b/druid/Cargo.toml @@ -21,7 +21,7 @@ default-target = "x86_64-pc-windows-msvc" [features] x11 = ["druid-shell/x11"] -svg = ["usvg"] +svg = ["usvg", "harfbuzz-sys"] [dependencies] druid-shell = { version = "0.6.0", path = "../druid-shell" } @@ -41,6 +41,9 @@ instant = { version = "0.1.4", features = ["wasm-bindgen"] } # Optional dependencies im = { version = "15.0.0", optional = true } usvg = { version = "0.9.0", optional = true } +# We don't depend directly on harfbuzz-sys, it comes through usvg. But we need to pin +# the version, because the latest release of harfbuzz_rs is broken with harfbuzz-sys 0.5. +harfbuzz-sys = {version = "0.4", optional = true } image = { version = "0.23.4", optional = true } [target.'cfg(target_arch="wasm32")'.dependencies] diff --git a/druid/examples/invalidation.rs b/druid/examples/invalidation.rs index 1ab3f8a10b..5ad18fb336 100644 --- a/druid/examples/invalidation.rs +++ b/druid/examples/invalidation.rs @@ -17,7 +17,7 @@ use druid::kurbo::{Circle, Shape}; use druid::widget::prelude::*; -use druid::widget::{AbsoluteScroll, Button, Flex, Split, TextBox}; +use druid::widget::{Button, Flex, Scroll, Split, TextBox}; use druid::{AppLauncher, Color, Data, Lens, LocalizedString, Point, WidgetExt, WindowDesc}; pub fn main() { @@ -46,11 +46,7 @@ fn build_widget() -> impl Widget { for i in 0..30 { col.add_child(Button::new(format!("Button {}", i)).padding(3.0)); } - Split::columns( - AbsoluteScroll::new(col), - CircleView.lens(AppState::circle_pos), - ) - .debug_invalidation() + Split::columns(Scroll::new(col), CircleView.lens(AppState::circle_pos)).debug_invalidation() } struct CircleView; diff --git a/druid/examples/scroll.rs b/druid/examples/scroll.rs index 97d8542d81..14dd7de888 100644 --- a/druid/examples/scroll.rs +++ b/druid/examples/scroll.rs @@ -18,7 +18,7 @@ use druid::kurbo::Circle; use druid::piet::RadialGradient; use druid::widget::prelude::*; -use druid::widget::{AbsoluteScroll, Flex, Padding}; +use druid::widget::{Flex, Padding, Scroll}; use druid::{AppLauncher, Data, Insets, LocalizedString, Rect, WindowDesc}; pub fn main() { @@ -35,7 +35,7 @@ fn build_widget() -> impl Widget { for i in 0..30 { col.add_child(Padding::new(3.0, OverPainter(i))); } - AbsoluteScroll::new(col) + Scroll::new(col) } /// A widget that paints outside of its bounds. diff --git a/druid/examples/scroll_colors.rs b/druid/examples/scroll_colors.rs index e90d86573f..f02389fdc3 100644 --- a/druid/examples/scroll_colors.rs +++ b/druid/examples/scroll_colors.rs @@ -14,7 +14,7 @@ //! This example allows to play with scroll bars over different color tones. -use druid::widget::{AbsoluteScroll, Container, Flex, SizedBox}; +use druid::widget::{Container, Flex, Scroll, SizedBox}; use druid::{AppLauncher, Color, LocalizedString, Widget, WindowDesc}; fn build_app() -> impl Widget { @@ -38,7 +38,7 @@ fn build_app() -> impl Widget { col.add_child(row); } - AbsoluteScroll::new(col) + Scroll::new(col) } pub fn main() { diff --git a/druid/src/core.rs b/druid/src/core.rs index 087b7abed9..e9a024bc4f 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -951,7 +951,7 @@ impl WidgetState { #[cfg(test)] mod tests { use super::*; - use crate::widget::{AbsoluteScroll, Flex, Split, TextBox}; + use crate::widget::{Flex, Scroll, Split, TextBox}; use crate::{WidgetExt, WindowHandle, WindowId}; const ID_1: WidgetId = WidgetId::reserved(0); @@ -966,7 +966,7 @@ mod tests { .with_child(TextBox::new().with_id(ID_1).parse()) .with_child(TextBox::new().with_id(ID_2).parse()) .with_child(TextBox::new().with_id(ID_3).parse()), - AbsoluteScroll::new(TextBox::new().parse()), + Scroll::new(TextBox::new().parse()), ) } diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index ffc75f9307..d15550d601 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -23,6 +23,7 @@ use crate::{ Env, Event, EventCtx, LifeCycle, LifeCycleCtx, PaintCtx, Region, RenderContext, TimerToken, }; +//TODO: Add this to env pub const SCROLLBAR_MIN_SIZE: f64 = 45.0; #[derive(Debug, Copy, Clone)] @@ -34,10 +35,10 @@ pub enum BarHoveredState { impl BarHoveredState { pub fn is_hovered(self) -> bool { - match self { - BarHoveredState::Vertical | BarHoveredState::Horizontal => true, - _ => false, - } + matches!( + self, + BarHoveredState::Vertical | BarHoveredState::Horizontal + ) } } @@ -74,15 +75,37 @@ impl Default for ScrollbarsState { impl ScrollbarsState { /// true if either scrollbar is currently held down/being dragged pub fn are_held(&self) -> bool { - match self.held { - BarHeldState::None => false, - _ => true, - } + !matches!(self.held, BarHeldState::None) } } +/// Embedable component exposing reusable scroll handling logic. +/// +/// In most situations composing [`Scroll`] or [`List`] is a better idea +/// for general UI construction. However some cases are not covered by +/// composing those widgets, such as when a widget needs fine grained +/// control over its scrolling state or doesn't make sense to exist alone +/// without scrolling behavior. +/// +/// `ScrollComponent` contains the unified and consistant scroll logic +/// used by both [`Scroll`] and [`List`]. This can be used to add this +/// logic to a custom widget when the need arises. +/// +/// To use, instance in your widget's new fn and place in a field, keep +/// the [`content_size`] field updated as scrollable content size changes, +/// call [`event`] and [`lifecycle`] with all event and lifecycle events, +/// and finally perform painting from within a closure provided to [`paint_content`]. +/// +/// [`Scroll`]: ../widget/struct.Scroll.html +/// [`List`]: ../widget/struct.List.html +/// [`content_size`]: struct.ScrollComponent.html#field.content_size +/// [`event`]: struct.ScrollComponent.html#method.event +/// [`lifecycle`]: struct.ScrollComponent.html#method.lifecycle +/// [`paint_content`]: struct.ScrollComponent.html#method.paint_content #[derive(Debug, Copy, Clone)] pub struct ScrollComponent { + /// The size of the scrollable content, make sure to keep up this + /// accurate to the content being scrolled pub content_size: Size, pub scroll_offset: Vec2, pub scrollbars: ScrollbarsState, @@ -265,10 +288,10 @@ impl ScrollComponent { } } - /// Checks if the event applies to the scroll behavior, uses it and returns true if so + /// Checks if the event applies to the scroll behavior, uses it, and marks it handled /// - /// Returns false if the event was not used - pub fn filter_event(&mut self, ctx: &mut EventCtx, event: &Event, env: &Env) -> bool { + /// Make sure to call on every event + pub fn event(&mut self, ctx: &mut EventCtx, event: &Event, env: &Env) { let size = ctx.size(); let viewport = Rect::from_origin_size(Point::ORIGIN, size); @@ -362,14 +385,11 @@ impl ScrollComponent { // Schedule scroll bars animation ctx.request_anim_frame(); self.scrollbars.timer_id = TimerToken::INVALID; + ctx.set_handled(); } _ => (), } - - return false; } - - true } /// Applies mousewheel scrolling if the event has not already been handled @@ -385,15 +405,10 @@ impl ScrollComponent { } } - /// Checks if the lifecycle event applies to the scroll behavior, uses it and returns true if so + /// Perform any nessesary action prompted by a lifecycle event /// - /// Returns false if the lifecycle event was not used - pub fn filter_lifecycle( - &mut self, - ctx: &mut LifeCycleCtx, - event: &LifeCycle, - env: &Env, - ) -> bool { + /// Make sure to call on every lifecycle event + pub fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, env: &Env) { match event { LifeCycle::AnimFrame(interval) => { // Guard by the timer id being invalid, otherwise the scroll bars would fade @@ -405,19 +420,16 @@ impl ScrollComponent { if self.scrollbars.opacity > 0.0 { ctx.request_anim_frame(); } - - return true; } } + // Show the scrollbars any time our size changes LifeCycle::Size(_) => { self.reset_scrollbar_fade(|d| ctx.request_timer(d), &env); - return true; } - _ => (), - } - false + _ => {} + } } /// Helper function to paint a closure at the correct offset with clipping and scrollbars diff --git a/druid/src/widget/list.rs b/druid/src/widget/list.rs index 78ea39dcc5..0c13eb484c 100644 --- a/druid/src/widget/list.rs +++ b/druid/src/widget/list.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Simple list view widget. +//! Simple scrolling list view widget. use std::cmp::Ordering; use std::sync::Arc; @@ -33,7 +33,7 @@ pub enum ListDirection { Horizontal, } -/// A list widget for a variable-size collection of items. +/// A scrollable list widget for a variable-size collection of items. pub struct List { closure: Box Box>>, children: Vec>>>, @@ -43,8 +43,8 @@ pub struct List { impl List { /// Create a new list widget. Closure will be called every time when a new child - /// needs to be constructed. Children will layed out vertically Use - /// [horizontal](#method.horizontal) to lay out items horizontally. + /// needs to be constructed. Children will layed out vertically while locking width. + /// Use [horizontal](#method.horizontal) to lay out items horizontally. pub fn new + 'static>(closure: impl Fn() -> W + 'static) -> Self { List { closure: Box::new(move || Box::new(closure())), @@ -54,6 +54,7 @@ impl List { } } + /// Lay out items on the horizontal axis while locking item height pub fn horizontal(mut self) -> Self { self.direction = ListDirection::Horizontal; self @@ -211,7 +212,8 @@ impl ListIter<(S, T)> for (S, Arc>) { impl> Widget for List { fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { - if !self.scroll_component.filter_event(ctx, event, env) { + self.scroll_component.event(ctx, event, env); + if !ctx.is_handled { let viewport = Rect::from_origin_size(Point::ORIGIN, ctx.size()); let scroll_offset = self.scroll_component.scroll_offset; let mut children = self.children.iter_mut(); @@ -231,20 +233,20 @@ impl> Widget for List { } fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { - if !self.scroll_component.filter_lifecycle(ctx, event, env) { - if let LifeCycle::WidgetAdded = event { - if self.update_child_count(data, env) { - ctx.children_changed(); - } - } + self.scroll_component.lifecycle(ctx, event, env); - let mut children = self.children.iter_mut(); - data.for_each(|child_data, _| { - if let Some(child) = children.next() { - child.lifecycle(ctx, event, child_data, env); - } - }); + if let LifeCycle::WidgetAdded = event { + if self.update_child_count(data, env) { + ctx.children_changed(); + } } + + let mut children = self.children.iter_mut(); + data.for_each(|child_data, _| { + if let Some(child) = children.next() { + child.lifecycle(ctx, event, child_data, env); + } + }); } fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) { @@ -319,7 +321,7 @@ impl> Widget for List { }); self.scroll_component.content_size = Size::new(width, height); - let insets = paint_rect - Rect::ZERO.with_size(my_size); + let insets = paint_rect - my_size.to_rect(); ctx.set_paint_insets(insets); my_size } diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index b7bb8b6f2a..27807da3f8 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -14,7 +14,6 @@ //! Common widgets. -mod absolute_scroll; mod align; mod button; mod checkbox; @@ -35,6 +34,7 @@ mod painter; mod parse; mod progress_bar; mod radio; +mod scroll; mod sized_box; mod slider; mod spinner; @@ -51,7 +51,6 @@ mod widget; mod widget_ext; pub use self::image::{Image, ImageData}; -pub use absolute_scroll::AbsoluteScroll; pub use align::Align; pub use button::Button; pub use checkbox::Checkbox; @@ -70,6 +69,7 @@ pub use painter::{BackgroundBrush, Painter}; pub use parse::Parse; pub use progress_bar::ProgressBar; pub use radio::{Radio, RadioGroup}; +pub use scroll::Scroll; pub use sized_box::SizedBox; pub use slider::Slider; pub use spinner::Spinner; diff --git a/druid/src/widget/absolute_scroll.rs b/druid/src/widget/scroll.rs similarity index 65% rename from druid/src/widget/absolute_scroll.rs rename to druid/src/widget/scroll.rs index 93279eb769..86b48c9f88 100644 --- a/druid/src/widget/absolute_scroll.rs +++ b/druid/src/widget/scroll.rs @@ -22,29 +22,56 @@ use crate::{ LifeCycleCtx, PaintCtx, UpdateCtx, Widget, WidgetPod, }; +#[derive(Debug, Clone)] +enum ScrollDirection { + Bidirectional, + Vertical, + Horizontal, +} + /// A container that scrolls its contents. /// /// This container holds a single child, and uses the wheel to scroll it /// when the child's bounds are larger than the viewport. /// -/// The child is laid out with completely unconstrained layout bounds. -pub struct AbsoluteScroll { +/// The child is laid out with completely unconstrained layout bounds by +/// default. Restrict to a specific axis with [`vertical`] or [`horizontal`]. +/// When restricted to scrolling on a specific axis the child's size is +/// locked on the opposite axis. +/// +/// [`vertical`]: struct.Scroll.html#method.vertical +/// [`horizontal`]: struct.Scroll.html#method.horizontal +pub struct Scroll { child: WidgetPod, scroll_component: ScrollComponent, + direction: ScrollDirection, } -impl> AbsoluteScroll { +impl> Scroll { /// Create a new scroll container. /// /// This method will allow scrolling in all directions if child's bounds /// are larger than the viewport. - pub fn new(child: W) -> AbsoluteScroll { - AbsoluteScroll { + pub fn new(child: W) -> Scroll { + Scroll { child: WidgetPod::new(child), scroll_component: ScrollComponent::new(), + direction: ScrollDirection::Bidirectional, } } + /// Restrict scrolling to the vertical axis while locking child width + pub fn vertical(mut self) -> Self { + self.direction = ScrollDirection::Vertical; + self + } + + /// Restrict scrolling to the horizontal axis while locking child height + pub fn horizontal(mut self) -> Self { + self.direction = ScrollDirection::Horizontal; + self + } + /// Returns a reference to the child widget. pub fn child(&self) -> &W { self.child.widget() @@ -66,9 +93,10 @@ impl> AbsoluteScroll { } } -impl> Widget for AbsoluteScroll { +impl> Widget for Scroll { fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { - if !self.scroll_component.filter_event(ctx, event, env) { + self.scroll_component.event(ctx, event, env); + if !ctx.is_handled { let viewport = Rect::from_origin_size(Point::ORIGIN, ctx.size()); let force_event = self.child.is_hot() || self.child.is_active(); @@ -83,9 +111,8 @@ impl> Widget for AbsoluteScroll { } fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { - if !self.scroll_component.filter_lifecycle(ctx, event, env) { - self.child.lifecycle(ctx, event, data, env); - } + self.scroll_component.lifecycle(ctx, event, env); + self.child.lifecycle(ctx, event, data, env); } fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) { @@ -95,13 +122,20 @@ impl> Widget for AbsoluteScroll { fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { bc.debug_check("Scroll"); - let child_bc = BoxConstraints::new(Size::ZERO, Size::new(INFINITY, INFINITY)); - let size = self.child.layout(ctx, &child_bc, data, env); - log_size_warnings(size); + let max_bc = match self.direction { + ScrollDirection::Bidirectional => Size::new(INFINITY, INFINITY), + ScrollDirection::Vertical => Size::new(bc.min().width, INFINITY), + ScrollDirection::Horizontal => Size::new(INFINITY, bc.min().height), + }; + + let child_bc = BoxConstraints::new(Size::ZERO, max_bc); + let child_size = self.child.layout(ctx, &child_bc, data, env); + log_size_warnings(child_size); + self.scroll_component.content_size = child_size; + self.child + .set_layout_rect(ctx, data, env, child_size.to_rect()); - self.scroll_component.content_size = size; - self.child.set_layout_rect(ctx, data, env, size.to_rect()); - let self_size = bc.constrain(self.scroll_component.content_size); + let self_size = bc.constrain(max_bc); let _ = self.scroll_component.scroll(Vec2::new(0.0, 0.0), self_size); self_size } From 26a9ea51272698d0569dddf396131af720bc691b Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Wed, 12 Aug 2020 23:13:18 -0400 Subject: [PATCH 11/18] Add some missing information to `ScrollComponent` documentation --- druid/src/scroll_component.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index d15550d601..27b4def391 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -94,12 +94,15 @@ impl ScrollbarsState { /// To use, instance in your widget's new fn and place in a field, keep /// the [`content_size`] field updated as scrollable content size changes, /// call [`event`] and [`lifecycle`] with all event and lifecycle events, -/// and finally perform painting from within a closure provided to [`paint_content`]. +/// call [`handle_scroll`] with all events after all other event handling +/// code and finally perform painting from within a closure provided to +/// [`paint_content`]. /// /// [`Scroll`]: ../widget/struct.Scroll.html /// [`List`]: ../widget/struct.List.html /// [`content_size`]: struct.ScrollComponent.html#field.content_size /// [`event`]: struct.ScrollComponent.html#method.event +/// [`handle_scroll`]: struct.ScrollComponent.html#method.handle_scroll /// [`lifecycle`]: struct.ScrollComponent.html#method.lifecycle /// [`paint_content`]: struct.ScrollComponent.html#method.paint_content #[derive(Debug, Copy, Clone)] From c4f8a3296f302b4854912011fbdb8541c5d71f2b Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Wed, 26 Aug 2020 20:54:15 -0400 Subject: [PATCH 12/18] `ScrollContainer` review take 2 --- CHANGELOG.md | 2 +- druid/src/scroll_component.rs | 38 +++++++++++++++++++---------------- druid/src/widget/list.rs | 2 +- druid/src/widget/scroll.rs | 9 +++++---- 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd15a51505..bdda1fe2e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ You can find its changes [documented below](#060---2020-06-01). - Re-export `druid_shell::Scalable` under `druid` namespace. ([#1075] by [@ForLoveOfCats]) - `TextBox` now supports ctrl and shift hotkeys. ([#1076] by [@vkahl]) - Added selection text color to textbox. ([#1093] by [@sysint64]) -- `scroll_component` for ease of adding consistant, customized, scrolling behavior to a widget. ([#1107] by [@ForLoveOfCats]) +- `ScrollComponent` for ease of adding consistent, customized, scrolling behavior to a widget. ([#1107] by [@ForLoveOfCats]) ### Changed diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index 27b4def391..143bc7f588 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! A component for embedding in another widget to provide consistant and +//! A component for embedding in another widget to provide consistent and //! extendable scrolling behavior use std::time::Duration; @@ -87,20 +87,22 @@ impl ScrollbarsState { /// control over its scrolling state or doesn't make sense to exist alone /// without scrolling behavior. /// -/// `ScrollComponent` contains the unified and consistant scroll logic +/// `ScrollComponent` contains the unified and consistent scroll logic /// used by both [`Scroll`] and [`List`]. This can be used to add this /// logic to a custom widget when the need arises. /// -/// To use, instance in your widget's new fn and place in a field, keep -/// the [`content_size`] field updated as scrollable content size changes, -/// call [`event`] and [`lifecycle`] with all event and lifecycle events, -/// call [`handle_scroll`] with all events after all other event handling -/// code and finally perform painting from within a closure provided to -/// [`paint_content`]. +/// It should be used like this: +/// - Store an instance of `ScrollComponent` in your widget's struct. +/// - During layout, set the [`content_size`] field to the child's size. +/// - Call [`event`] and [`lifecycle`] with all event and lifecycle events before propagating them to children. +/// - Call [`handle_scroll`] with all events after handling / propagating them. +/// - And finally perform painting using the provided [`paint_content`] function. +/// +/// Also, taking a look at the [`Scroll`] source code can be helpful. /// /// [`Scroll`]: ../widget/struct.Scroll.html /// [`List`]: ../widget/struct.List.html -/// [`content_size`]: struct.ScrollComponent.html#field.content_size +/// [`content_size`]: struct.ScrollComponent.html#structfield.content_size /// [`event`]: struct.ScrollComponent.html#method.event /// [`handle_scroll`]: struct.ScrollComponent.html#method.handle_scroll /// [`lifecycle`]: struct.ScrollComponent.html#method.lifecycle @@ -123,21 +125,24 @@ impl Default for ScrollComponent { impl ScrollComponent { pub fn new() -> ScrollComponent { ScrollComponent { - content_size: Default::default(), + content_size: Size::default(), scroll_offset: Vec2::new(0.0, 0.0), scrollbars: ScrollbarsState::default(), } } - /// Update the scroll. + /// Scroll `delta` units. /// - /// Returns `true` if the scroll has been updated. - pub fn scroll(&mut self, delta: Vec2, size: Size) -> bool { + /// Returns `true` if the scroll offset has changed. + pub fn scroll(&mut self, delta: Vec2, layout_size: Size) -> bool { let mut offset = self.scroll_offset + delta; - offset.x = offset.x.min(self.content_size.width - size.width).max(0.0); + offset.x = offset + .x + .min(self.content_size.width - layout_size.width) + .max(0.0); offset.y = offset .y - .min(self.content_size.height - size.height) + .min(self.content_size.height - layout_size.height) .max(0.0); if (offset - self.scroll_offset).hypot2() > 1e-12 { self.scroll_offset = offset; @@ -152,7 +157,6 @@ impl ScrollComponent { where F: FnOnce(Duration) -> TimerToken, { - // Display scroll bars and schedule their disappearance self.scrollbars.opacity = env.get(theme::SCROLLBAR_MAX_OPACITY); let fade_delay = env.get(theme::SCROLLBAR_FADE_DELAY); let deadline = Duration::from_millis(fade_delay); @@ -408,7 +412,7 @@ impl ScrollComponent { } } - /// Perform any nessesary action prompted by a lifecycle event + /// Perform any necessary action prompted by a lifecycle event /// /// Make sure to call on every lifecycle event pub fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, env: &Env) { diff --git a/druid/src/widget/list.rs b/druid/src/widget/list.rs index 0c13eb484c..83f2e62fb6 100644 --- a/druid/src/widget/list.rs +++ b/druid/src/widget/list.rs @@ -213,7 +213,7 @@ impl ListIter<(S, T)> for (S, Arc>) { impl> Widget for List { fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { self.scroll_component.event(ctx, event, env); - if !ctx.is_handled { + if !ctx.is_handled() { let viewport = Rect::from_origin_size(Point::ORIGIN, ctx.size()); let scroll_offset = self.scroll_component.scroll_offset; let mut children = self.children.iter_mut(); diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index 86b48c9f88..681eed2898 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -51,7 +51,8 @@ impl> Scroll { /// Create a new scroll container. /// /// This method will allow scrolling in all directions if child's bounds - /// are larger than the viewport. + /// are larger than the viewport. Use [vertical](#method.vertical) and + /// [horizontal](#method.horizontal) methods to limit scrolling to a specific axis. pub fn new(child: W) -> Scroll { Scroll { child: WidgetPod::new(child), @@ -60,13 +61,13 @@ impl> Scroll { } } - /// Restrict scrolling to the vertical axis while locking child width + /// Restrict scrolling to the vertical axis while locking child width. pub fn vertical(mut self) -> Self { self.direction = ScrollDirection::Vertical; self } - /// Restrict scrolling to the horizontal axis while locking child height + /// Restrict scrolling to the horizontal axis while locking child height. pub fn horizontal(mut self) -> Self { self.direction = ScrollDirection::Horizontal; self @@ -96,7 +97,7 @@ impl> Scroll { impl> Widget for Scroll { fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { self.scroll_component.event(ctx, event, env); - if !ctx.is_handled { + if !ctx.is_handled() { let viewport = Rect::from_origin_size(Point::ORIGIN, ctx.size()); let force_event = self.child.is_hot() || self.child.is_active(); From 5f7c9851a3fc29259d2b61f649c011101634381e Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Wed, 26 Aug 2020 21:02:29 -0400 Subject: [PATCH 13/18] Changes from #1057 --- druid/src/scroll_component.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index 143bc7f588..beaa7add94 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -427,6 +427,18 @@ impl ScrollComponent { if self.scrollbars.opacity > 0.0 { ctx.request_anim_frame(); } + + let viewport = ctx.size().to_rect(); + if viewport.width() < self.content_size.width { + ctx.request_paint_rect( + self.calc_horizontal_bar_bounds(viewport, env) - self.scroll_offset, + ); + } + if viewport.height() < self.content_size.height { + ctx.request_paint_rect( + self.calc_vertical_bar_bounds(viewport, env) - self.scroll_offset, + ); + } } } @@ -451,7 +463,8 @@ impl ScrollComponent { ctx.clip(viewport); ctx.transform(Affine::translate(-self.scroll_offset)); - let visible = ctx.region().to_rect() + self.scroll_offset; + let mut visible = ctx.region().clone(); + visible += self.scroll_offset; f(visible.into(), ctx); self.draw_bars(ctx, viewport, env); From 49b792835880537f155feef0aefd40657ca7883e Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Wed, 26 Aug 2020 22:34:03 -0400 Subject: [PATCH 14/18] ScrollComponent clippy --- druid/src/scroll_component.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index beaa7add94..4fbcea1625 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -465,7 +465,7 @@ impl ScrollComponent { let mut visible = ctx.region().clone(); visible += self.scroll_offset; - f(visible.into(), ctx); + f(visible, ctx); self.draw_bars(ctx, viewport, env); }); From 2c3131492cd95b5833bc1663274f62135503f132 Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Sat, 5 Sep 2020 21:01:55 -0400 Subject: [PATCH 15/18] Remove List changes (updated to master) & fix Scroll regression --- CHANGELOG.md | 1 - druid/examples/list.rs | 30 +++++---- druid/src/widget/list.rs | 124 +++++++++---------------------------- druid/src/widget/scroll.rs | 4 +- 4 files changed, 50 insertions(+), 109 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74354e60d1..d19c29559a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,6 @@ You can find its changes [documented below](#060---2020-06-01). - `Scale::from_scale` to `Scale::new`, and `Scale` methods `scale_x` / `scale_y` to `x` / `y`. ([#1042] by [@xStrom]) - Major rework of keyboard event handling. ([#1049] by [@raphlinus]) - `Container::rounded` takes `KeyOrValue` instead of `f64`. ([#1054] by [@binomial0]) -- `List` handles its own scrolling on either the vertical or horizontal axis. ([#1107] by [@ForLoveOfCats]) - `request_anim_frame` no longer invalidates the entire window. ([#1057] by [@jneem]) - Use new Piet text api ([#1143] by [@cmyr]) diff --git a/druid/examples/list.rs b/druid/examples/list.rs index d6f71f4937..9b9fdd3846 100644 --- a/druid/examples/list.rs +++ b/druid/examples/list.rs @@ -16,7 +16,7 @@ use druid::im::{vector, Vector}; use druid::lens::{self, LensExt}; -use druid::widget::{Button, CrossAxisAlignment, Flex, Label, List}; +use druid::widget::{Button, CrossAxisAlignment, Flex, Label, List, Scroll}; use druid::{ AppLauncher, Color, Data, Lens, LocalizedString, UnitPoint, Widget, WidgetExt, WindowDesc, }; @@ -25,15 +25,21 @@ use druid::{ struct AppData { left: Vector, right: Vector, + l_index: usize, + r_index: usize, } pub fn main() { let main_window = WindowDesc::new(ui_builder) .title(LocalizedString::new("list-demo-window-title").with_placeholder("List Demo")); // Set our initial data + let left = vector![1, 2]; + let right = vector![1, 2, 3]; let data = AppData { - left: vector![1, 2], - right: vector![1, 2, 3], + l_index: left.len(), + r_index: right.len(), + left, + right, }; AppLauncher::with_window(main_window) .use_simple_logger() @@ -49,12 +55,12 @@ fn ui_builder() -> impl Widget { Button::new("Add") .on_click(|_, data: &mut AppData, _| { // Add child to left list - let value = data.left.len() + 1; - data.left.push_back(value as u32); + data.l_index += 1; + data.left.push_back(data.l_index as u32); // Add child to right list - let value = data.right.len() + 1; - data.right.push_back(value as u32); + data.r_index += 1; + data.right.push_back(data.r_index as u32); }) .fix_height(30.0) .expand_width(), @@ -64,21 +70,22 @@ fn ui_builder() -> impl Widget { // Build a simple list lists.add_flex_child( - List::new(|| { + Scroll::new(List::new(|| { Label::new(|item: &u32, _env: &_| format!("List item #{}", item)) .align_vertical(UnitPoint::LEFT) .padding(10.0) .expand() .height(50.0) .background(Color::rgb(0.5, 0.5, 0.5)) - }) + })) + .vertical() .lens(AppData::left), 1.0, ); // Build a list with shared data lists.add_flex_child( - List::new(|| { + Scroll::new(List::new(|| { Flex::row() .with_child( Label::new(|(_, item): &(Vector, u32), _env: &_| { @@ -100,7 +107,8 @@ fn ui_builder() -> impl Widget { .padding(10.0) .background(Color::rgb(0.5, 0.0, 0.5)) .fix_height(50.0) - }) + })) + .vertical() .lens(lens::Id.map( // Expose shared data with children data |d: &AppData| (d.right.clone(), d.right.clone()), diff --git a/druid/src/widget/list.rs b/druid/src/widget/list.rs index 83f2e62fb6..c3c84c9542 100644 --- a/druid/src/widget/list.rs +++ b/druid/src/widget/list.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Simple scrolling list view widget. +//! Simple list view widget. use std::cmp::Ordering; use std::sync::Arc; @@ -23,43 +23,26 @@ use crate::im::Vector; use crate::kurbo::{Point, Rect, Size}; use crate::{ - scroll_component::*, BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, - LifeCycleCtx, PaintCtx, UpdateCtx, Widget, WidgetPod, + BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, + UpdateCtx, Widget, WidgetPod, }; -#[derive(Debug, Copy, Clone)] -pub enum ListDirection { - Vertical, - Horizontal, -} - -/// A scrollable list widget for a variable-size collection of items. +/// A list widget for a variable-size collection of items. pub struct List { closure: Box Box>>, children: Vec>>>, - direction: ListDirection, - scroll_component: ScrollComponent, } impl List { /// Create a new list widget. Closure will be called every time when a new child - /// needs to be constructed. Children will layed out vertically while locking width. - /// Use [horizontal](#method.horizontal) to lay out items horizontally. + /// needs to be constructed. pub fn new + 'static>(closure: impl Fn() -> W + 'static) -> Self { List { closure: Box::new(move || Box::new(closure())), children: Vec::new(), - direction: ListDirection::Vertical, - scroll_component: ScrollComponent::new(), } } - /// Lay out items on the horizontal axis while locking item height - pub fn horizontal(mut self) -> Self { - self.direction = ListDirection::Horizontal; - self - } - /// When the widget is created or the data changes, create or remove children as needed /// /// Returns `true` if children were added or removed. @@ -212,29 +195,15 @@ impl ListIter<(S, T)> for (S, Arc>) { impl> Widget for List { fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { - self.scroll_component.event(ctx, event, env); - if !ctx.is_handled() { - let viewport = Rect::from_origin_size(Point::ORIGIN, ctx.size()); - let scroll_offset = self.scroll_component.scroll_offset; - let mut children = self.children.iter_mut(); - - data.for_each_mut(|child_data, _| { - if let Some(child) = children.next() { - let force_event = child.is_hot() || child.is_active(); - let child_event = event.transform_scroll(scroll_offset, viewport, force_event); - if let Some(child_event) = child_event { - child.event(ctx, &child_event, child_data, env); - } - } - }); - } - - self.scroll_component.handle_scroll(ctx, event, env); + let mut children = self.children.iter_mut(); + data.for_each_mut(|child_data, _| { + if let Some(child) = children.next() { + child.event(ctx, event, child_data, env); + } + }); } fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { - self.scroll_component.lifecycle(ctx, event, env); - if let LifeCycle::WidgetAdded = event { if self.update_child_count(data, env) { ctx.children_changed(); @@ -266,12 +235,8 @@ impl> Widget for List { } fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { - let my_size = bc.constrain(bc.max()); - let layout_direction = self.direction; - let (mut width, mut height) = match layout_direction { - ListDirection::Vertical => (bc.min().width, 0.0), - ListDirection::Horizontal => (0.0, bc.min().height), - }; + let mut width = bc.min().width; + let mut y = 0.0; let mut paint_rect = Rect::ZERO; let mut children = self.children.iter_mut(); @@ -282,61 +247,30 @@ impl> Widget for List { return; } }; - let child_bc = match layout_direction { - ListDirection::Vertical => BoxConstraints::new( - Size::new(bc.min().width, 0.0), - Size::new(bc.max().width, std::f64::INFINITY), - ), - ListDirection::Horizontal => BoxConstraints::new( - Size::new(0.0, bc.min().height), - Size::new(std::f64::INFINITY, bc.max().height), - ), - }; - let child_size = match layout_direction { - ListDirection::Vertical => Size::new( - my_size.width, - child.layout(ctx, &child_bc, child_data, env).height, - ), - ListDirection::Horizontal => Size::new( - child.layout(ctx, &child_bc, child_data, env).width, - my_size.height, - ), - }; - let rect = match layout_direction { - ListDirection::Vertical => { - Rect::from_origin_size(Point::new(0.0, height), child_size) - } - - ListDirection::Horizontal => { - Rect::from_origin_size(Point::new(width, 0.0), child_size) - } - }; + let child_bc = BoxConstraints::new( + Size::new(bc.min().width, 0.0), + Size::new(bc.max().width, std::f64::INFINITY), + ); + let child_size = child.layout(ctx, &child_bc, child_data, env); + let rect = Rect::from_origin_size(Point::new(0.0, y), child_size); child.set_layout_rect(ctx, child_data, env, rect); paint_rect = paint_rect.union(child.paint_rect()); - - match layout_direction { - ListDirection::Vertical => height += child_size.height, - ListDirection::Horizontal => width += child_size.width, - } + width = width.max(child_size.width); + y += child_size.height; }); - self.scroll_component.content_size = Size::new(width, height); - let insets = paint_rect - my_size.to_rect(); + let my_size = bc.constrain(Size::new(width, y)); + let insets = paint_rect - Rect::ZERO.with_size(my_size); ctx.set_paint_insets(insets); my_size } fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { - self.scroll_component - .paint_content(ctx, env, |visible, ctx| { - let mut children = self.children.iter_mut(); - data.for_each(|child_data, _| { - if let Some(child) = children.next() { - ctx.with_child_ctx(visible.clone(), |ctx| { - child.paint(ctx, child_data, env) - }); - } - }) - }); + let mut children = self.children.iter_mut(); + data.for_each(|child_data, _| { + if let Some(child) = children.next() { + child.paint(ctx, child_data, env); + } + }); } } diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index 681eed2898..e1453c6c6e 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -125,8 +125,8 @@ impl> Widget for Scroll { let max_bc = match self.direction { ScrollDirection::Bidirectional => Size::new(INFINITY, INFINITY), - ScrollDirection::Vertical => Size::new(bc.min().width, INFINITY), - ScrollDirection::Horizontal => Size::new(INFINITY, bc.min().height), + ScrollDirection::Vertical => Size::new(bc.max().width, INFINITY), + ScrollDirection::Horizontal => Size::new(INFINITY, bc.max().height), }; let child_bc = BoxConstraints::new(Size::ZERO, max_bc); From 4ba7fc8cebb0a2e4f8eb01ac337a62bb21a2fe0c Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Sat, 5 Sep 2020 22:36:17 -0400 Subject: [PATCH 16/18] Add missing doc comments --- druid/src/scroll_component.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index 4fbcea1625..6aea8e5d8e 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -24,16 +24,24 @@ use crate::{ }; //TODO: Add this to env +/// Minimum length for any scrollbar to be when measured on that +/// scrollbar's primary axis. pub const SCROLLBAR_MIN_SIZE: f64 = 45.0; +/// Denotes which scrollbar, if any, is currently being hovered over +/// by the mouse. #[derive(Debug, Copy, Clone)] pub enum BarHoveredState { + /// Neither scrollbar is being hovered by the mouse. None, + /// The vertical scrollbar is being hovered by the mouse. Vertical, + /// The horizontal scrollbar is being hovered by the mouse. Horizontal, } impl BarHoveredState { + /// Determines if any scrollbar is currently being hovered by the mouse. pub fn is_hovered(self) -> bool { matches!( self, @@ -42,22 +50,29 @@ impl BarHoveredState { } } +/// Denotes which scrollbar, if any, is currently being dragged. #[derive(Debug, Copy, Clone)] pub enum BarHeldState { + /// Neither scrollbar is being dragged. None, /// Vertical scrollbar is being dragged. Contains an `f64` with - /// the initial y-offset of the dragging input + /// the initial y-offset of the dragging input. Vertical(f64), /// Horizontal scrollbar is being dragged. Contains an `f64` with - /// the initial x-offset of the dragging input + /// the initial x-offset of the dragging input. Horizontal(f64), } +/// Backing struct for storing scrollbar state #[derive(Debug, Copy, Clone)] pub struct ScrollbarsState { + /// Current opacity for both scrollbars pub opacity: f64, + /// ID for the timer which schedules scrollbar fade out pub timer_id: TimerToken, + /// Which if any scrollbar is currently hovered by the mouse pub hovered: BarHoveredState, + /// Which if any scrollbar is currently being dragged by the mouse pub held: BarHeldState, } @@ -112,7 +127,9 @@ pub struct ScrollComponent { /// The size of the scrollable content, make sure to keep up this /// accurate to the content being scrolled pub content_size: Size, + /// Current offset of the scrolling content pub scroll_offset: Vec2, + /// Current state of both scrollbars pub scrollbars: ScrollbarsState, } @@ -123,6 +140,7 @@ impl Default for ScrollComponent { } impl ScrollComponent { + /// Constructs a new [`ScrollComponent`](struct.ScrollComponent.html) for use. pub fn new() -> ScrollComponent { ScrollComponent { content_size: Size::default(), From 16b584633403a9fdf9970ac08d92b2e44b58eeb7 Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Sun, 6 Sep 2020 13:51:45 -0400 Subject: [PATCH 17/18] Be a bit more careful about event consuming --- druid/src/scroll_component.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index 6aea8e5d8e..526822fde2 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -340,6 +340,7 @@ impl ScrollComponent { let mouse_y = event.pos.y + self.scroll_offset.y; let delta = mouse_y - bounds.y0 - offset; self.scroll(Vec2::new(0f64, (delta / scale_y).ceil()), size); + ctx.set_handled(); } BarHeldState::Horizontal(offset) => { let scale_x = viewport.width() / self.content_size.width; @@ -347,6 +348,7 @@ impl ScrollComponent { let mouse_x = event.pos.x + self.scroll_offset.x; let delta = mouse_x - bounds.x0 - offset; self.scroll(Vec2::new((delta / scale_x).ceil(), 0f64), size); + ctx.set_handled(); } _ => (), } @@ -360,6 +362,8 @@ impl ScrollComponent { self.scrollbars.hovered = BarHoveredState::None; self.reset_scrollbar_fade(|d| ctx.request_timer(d), env); } + + ctx.set_handled(); } _ => (), // other events are a noop } @@ -370,13 +374,16 @@ impl ScrollComponent { let offset_pos = event.pos + self.scroll_offset; if self.point_hits_vertical_bar(viewport, offset_pos, env) { self.scrollbars.hovered = BarHoveredState::Vertical; - } else { + } else if self.point_hits_horizontal_bar(viewport, offset_pos, env) { self.scrollbars.hovered = BarHoveredState::Horizontal; + } else { + unreachable!(); } self.scrollbars.opacity = env.get(theme::SCROLLBAR_MAX_OPACITY); self.scrollbars.timer_id = TimerToken::INVALID; // Cancel any fade out in progress ctx.request_paint(); + ctx.set_handled(); } Event::MouseDown(event) => { let pos = event.pos + self.scroll_offset; @@ -391,7 +398,11 @@ impl ScrollComponent { self.scrollbars.held = BarHeldState::Horizontal( pos.x - self.calc_horizontal_bar_bounds(viewport, env).x0, ); + } else { + unreachable!(); } + + ctx.set_handled(); } // if the mouse was downed elsewhere, moved over a scroll bar and released: noop. Event::MouseUp(_) => (), From d027d27f938268d97fc40e159e3ab533d5be5e61 Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Sun, 6 Sep 2020 13:53:19 -0400 Subject: [PATCH 18/18] Fix dup changelog entry & doc comment misspelling --- CHANGELOG.md | 1 - druid/src/scroll_component.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69a8765259..b584ef6a94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,6 @@ You can find its changes [documented below](#060---2020-06-01). - Export `Image` and `ImageData` by default. ([#1011] by [@covercash2]) - Re-export `druid_shell::Scalable` under `druid` namespace. ([#1075] by [@ForLoveOfCats]) - `TextBox` now supports ctrl and shift hotkeys. ([#1076] by [@vkahl]) -- Added selection text color to textbox. ([#1093] by [@sysint64]) - `ScrollComponent` for ease of adding consistent, customized, scrolling behavior to a widget. ([#1107] by [@ForLoveOfCats]) - Selection text color to textbox. ([#1093] by [@sysint64]) - `BoxConstraints::UNBOUNDED` constant. ([#1126] by [@danieldulaney]) diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index 526822fde2..aa862b8ab7 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -94,7 +94,7 @@ impl ScrollbarsState { } } -/// Embedable component exposing reusable scroll handling logic. +/// Embeddable component exposing reusable scroll handling logic. /// /// In most situations composing [`Scroll`] or [`List`] is a better idea /// for general UI construction. However some cases are not covered by