From 4b8da6d64dd39bf7e9e081d6856b71f333f5f59d Mon Sep 17 00:00:00 2001 From: MStarha Date: Fri, 6 Dec 2024 19:33:09 +0100 Subject: [PATCH] Implement scroll source --- crates/egui/src/containers/scroll_area.rs | 210 +++++++++++++++++----- 1 file changed, 163 insertions(+), 47 deletions(-) diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index a17b18c7d68..3f5894e87d5 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -1,5 +1,7 @@ #![allow(clippy::needless_range_loop)] +use std::ops::{Add, AddAssign, BitOr, BitOrAssign}; + use crate::{ emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, vec2, Context, CursorIcon, Id, NumExt, Pos2, Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, @@ -133,6 +135,111 @@ impl ScrollBarVisibility { ]; } +/// What is the source of scrolling for a [`ScrollArea`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct ScrollSource { + /// Scroll the area by dragging a scroll bar. + /// + /// By default the scroll bars remain visible to show current position. + /// To hide them use [`ScrollArea::scroll_bar_visibility()`]. + pub scroll_bar: bool, + /// Scroll the area by dragging the contents. + pub drag: bool, + /// Scroll the area by scrolling (or shift scrolling) the mouse wheel with + /// the mouse cursor over the [`ScrollArea`]. + pub mouse_wheel: bool, +} + +impl Default for ScrollSource { + fn default() -> Self { + Self::ALL + } +} + +impl ScrollSource { + pub const NONE: Self = Self { + scroll_bar: false, + drag: false, + mouse_wheel: false, + }; + pub const ALL: Self = Self { + scroll_bar: true, + drag: true, + mouse_wheel: true, + }; + pub const SCROLL_BAR: Self = Self { + scroll_bar: true, + drag: false, + mouse_wheel: false, + }; + pub const DRAG: Self = Self { + scroll_bar: false, + drag: true, + mouse_wheel: false, + }; + pub const MOUSE_WHEEL: Self = Self { + scroll_bar: false, + drag: false, + mouse_wheel: true, + }; + + /// Is everything disabled? + #[inline] + pub fn is_none(&self) -> bool { + self == &Self::NONE + } + + /// Is anything enabled? + #[inline] + pub fn any(&self) -> bool { + self.scroll_bar | self.drag | self.mouse_wheel + } + + /// Is everything enabled? + #[inline] + pub fn is_all(&self) -> bool { + self.scroll_bar & self.drag & self.mouse_wheel + } +} + +impl BitOr for ScrollSource { + type Output = Self; + + #[inline] + fn bitor(self, rhs: Self) -> Self::Output { + Self { + scroll_bar: self.scroll_bar | rhs.scroll_bar, + drag: self.drag | rhs.drag, + mouse_wheel: self.mouse_wheel | rhs.mouse_wheel, + } + } +} + +#[allow(clippy::suspicious_arithmetic_impl)] +impl Add for ScrollSource { + type Output = Self; + + #[inline] + fn add(self, rhs: Self) -> Self::Output { + self | rhs + } +} + +impl BitOrAssign for ScrollSource { + #[inline] + fn bitor_assign(&mut self, rhs: Self) { + *self = *self | rhs; + } +} + +impl AddAssign for ScrollSource { + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + /// Add vertical and/or horizontal scrolling to a contained [`Ui`]. /// /// By default, scroll bars only show up when needed, i.e. when the contents @@ -165,7 +272,7 @@ impl ScrollBarVisibility { #[must_use = "You should call .show()"] pub struct ScrollArea { /// Do we have horizontal/vertical scrolling enabled? - scroll_enabled: Vec2b, + direction_enabled: Vec2b, auto_shrink: Vec2b, max_size: Vec2, @@ -178,9 +285,7 @@ pub struct ScrollArea { on_hover_cursor: Option, on_drag_cursor: Option, - /// If false, we ignore scroll events. - scrolling_enabled: bool, - drag_to_scroll: bool, + scroll_source: ScrollSource, /// If true for vertical or horizontal the scroll wheel will stick to the /// end position until user manually changes position. It will become true @@ -219,9 +324,9 @@ impl ScrollArea { /// Create a scroll area where you decide which axis has scrolling enabled. /// For instance, `ScrollArea::new([true, false])` enables horizontal scrolling. - pub fn new(scroll_enabled: impl Into) -> Self { + pub fn new(direction_enabled: impl Into) -> Self { Self { - scroll_enabled: scroll_enabled.into(), + direction_enabled: direction_enabled.into(), auto_shrink: Vec2b::TRUE, max_size: Vec2::INFINITY, min_scrolled_size: Vec2::splat(64.0), @@ -232,8 +337,7 @@ impl ScrollArea { offset_y: None, on_hover_cursor: None, on_drag_cursor: None, - scrolling_enabled: true, - drag_to_scroll: true, + scroll_source: ScrollSource::default(), stick_to_end: Vec2b::FALSE, animated: true, } @@ -358,7 +462,7 @@ impl ScrollArea { /// Set the cursor used when the mouse pointer is hovering over the [`ScrollArea`]. /// - /// Only applies if [`Self::drag_to_scroll`] is set to `true`. + /// Only applies if [`Self::scroll_source()`] has set [`ScrollSource::drag`] to `true`. /// /// Any changes to the mouse cursor made within the contents of the [`ScrollArea`] will /// override this setting. @@ -370,7 +474,7 @@ impl ScrollArea { /// Set the cursor used when the [`ScrollArea`] is being dragged. /// - /// Only applies if [`Self::drag_to_scroll`] is set to `true`. + /// Only applies if [`Self::scroll_source()`] has set [`ScrollSource::drag`] to `true`. /// /// Any changes to the mouse cursor made within the contents of the [`ScrollArea`] will /// override this setting. @@ -383,14 +487,14 @@ impl ScrollArea { /// Turn on/off scrolling on the horizontal axis. #[inline] pub fn hscroll(mut self, hscroll: bool) -> Self { - self.scroll_enabled[0] = hscroll; + self.direction_enabled[0] = hscroll; self } /// Turn on/off scrolling on the vertical axis. #[inline] pub fn vscroll(mut self, vscroll: bool) -> Self { - self.scroll_enabled[1] = vscroll; + self.direction_enabled[1] = vscroll; self } @@ -398,16 +502,16 @@ impl ScrollArea { /// /// You can pass in `false`, `true`, `[false, true]` etc. #[inline] - pub fn scroll(mut self, scroll_enabled: impl Into) -> Self { - self.scroll_enabled = scroll_enabled.into(); + pub fn scroll(mut self, direction_enabled: impl Into) -> Self { + self.direction_enabled = direction_enabled.into(); self } /// Turn on/off scrolling on the horizontal/vertical axes. #[deprecated = "Renamed to `scroll`"] #[inline] - pub fn scroll2(mut self, scroll_enabled: impl Into) -> Self { - self.scroll_enabled = scroll_enabled.into(); + pub fn scroll2(mut self, direction_enabled: impl Into) -> Self { + self.direction_enabled = direction_enabled.into(); self } @@ -420,9 +524,14 @@ impl ScrollArea { /// is typing text in a [`crate::TextEdit`] widget contained within the scroll area. /// /// This controls both scrolling directions. + #[deprecated = "Use `ScrollArea::scroll_source()"] #[inline] pub fn enable_scrolling(mut self, enable: bool) -> Self { - self.scrolling_enabled = enable; + self.scroll_source = if enable { + ScrollSource::ALL + } else { + ScrollSource::NONE + }; self } @@ -433,9 +542,17 @@ impl ScrollArea { /// If `true`, the [`ScrollArea`] will sense drags. /// /// Default: `true`. + #[deprecated = "Use `ScrollArea::scroll_source()"] #[inline] pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self { - self.drag_to_scroll = drag_to_scroll; + self.scroll_source.drag = drag_to_scroll; + self + } + + /// What sources does the [`ScrollArea`] use for scrolling the contents. + #[inline] + pub fn scroll_source(mut self, scroll_source: ScrollSource) -> Self { + self.scroll_source = scroll_source; self } @@ -462,7 +579,7 @@ impl ScrollArea { /// Is any scrolling enabled? pub(crate) fn is_any_scroll_enabled(&self) -> bool { - self.scroll_enabled[0] || self.scroll_enabled[1] + self.direction_enabled[0] || self.direction_enabled[1] } /// The scroll handle will stick to the rightmost position even while the content size @@ -497,7 +614,7 @@ struct Prepared { auto_shrink: Vec2b, /// Does this `ScrollArea` have horizontal/vertical scrolling enabled? - scroll_enabled: Vec2b, + direction_enabled: Vec2b, /// Smoothly interpolated boolean of whether or not to show the scroll bars. show_bars_factor: Vec2, @@ -525,7 +642,7 @@ struct Prepared { /// `viewport.min == ZERO` means we scrolled to the top. viewport: Rect, - scrolling_enabled: bool, + scroll_source: ScrollSource, stick_to_end: Vec2b, animated: bool, } @@ -533,7 +650,7 @@ struct Prepared { impl ScrollArea { fn begin(self, ui: &mut Ui) -> Prepared { let Self { - scroll_enabled, + direction_enabled, auto_shrink, max_size, min_scrolled_size, @@ -544,14 +661,12 @@ impl ScrollArea { offset_y, on_hover_cursor, on_drag_cursor, - scrolling_enabled, - drag_to_scroll, + scroll_source, stick_to_end, animated, } = self; let ctx = ui.ctx().clone(); - let scrolling_enabled = scrolling_enabled && ui.is_enabled(); let id_salt = id_salt.unwrap_or_else(|| Id::new("scroll_area")); let id = ui.make_persistent_id(id_salt); @@ -568,7 +683,7 @@ impl ScrollArea { let show_bars: Vec2b = match scroll_bar_visibility { ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE, ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll, - ScrollBarVisibility::AlwaysVisible => scroll_enabled, + ScrollBarVisibility::AlwaysVisible => direction_enabled, }; let show_bars_factor = Vec2::new( @@ -590,7 +705,7 @@ impl ScrollArea { // one shouldn't collapse into nothingness. // See https://github.com/emilk/egui/issues/1097 for d in 0..2 { - if scroll_enabled[d] { + if direction_enabled[d] { inner_size[d] = inner_size[d].max(min_scrolled_size[d]); } } @@ -607,7 +722,7 @@ impl ScrollArea { } else { // Tell the inner Ui to use as much space as possible, we can scroll to see it! for d in 0..2 { - if scroll_enabled[d] { + if direction_enabled[d] { content_max_size[d] = f32::INFINITY; } } @@ -625,7 +740,7 @@ impl ScrollArea { let clip_rect_margin = ui.visuals().clip_rect_margin; let mut content_clip_rect = ui.clip_rect(); for d in 0..2 { - if scroll_enabled[d] { + if direction_enabled[d] { content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin; content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin; } else { @@ -641,7 +756,8 @@ impl ScrollArea { let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size); let dt = ui.input(|i| i.stable_dt).at_most(0.1); - if (scrolling_enabled && drag_to_scroll) + if scroll_source.drag + && ui.is_enabled() && (state.content_is_too_large[0] || state.content_is_too_large[1]) { // Drag contents to scroll (for touch screens mostly). @@ -656,7 +772,7 @@ impl ScrollArea { .is_some_and(|response| response.dragged()) { for d in 0..2 { - if scroll_enabled[d] { + if direction_enabled[d] { ui.input(|input| { state.offset[d] -= input.pointer.delta()[d]; }); @@ -671,7 +787,7 @@ impl ScrollArea { .is_some_and(|response| response.drag_stopped()) { state.vel = - scroll_enabled.to_vec2() * ui.input(|input| input.pointer.velocity()); + direction_enabled.to_vec2() * ui.input(|input| input.pointer.velocity()); } for d in 0..2 { // Kinetic scrolling @@ -740,7 +856,7 @@ impl ScrollArea { id, state, auto_shrink, - scroll_enabled, + direction_enabled, show_bars_factor, current_bar_use, scroll_bar_visibility, @@ -748,7 +864,7 @@ impl ScrollArea { inner_rect, content_ui, viewport, - scrolling_enabled, + scroll_source, stick_to_end, animated, } @@ -854,14 +970,14 @@ impl Prepared { mut state, inner_rect, auto_shrink, - scroll_enabled, + direction_enabled, mut show_bars_factor, current_bar_use, scroll_bar_visibility, scroll_bar_rect, content_ui, viewport: _, - scrolling_enabled, + scroll_source, stick_to_end, animated, } = self; @@ -883,7 +999,7 @@ impl Prepared { .ctx() .pass_state_mut(|state| state.scroll_target[d].take()); - if scroll_enabled[d] { + if direction_enabled[d] { if let Some(target) = scroll_target { let pass_state::ScrollTarget { range, @@ -950,7 +1066,7 @@ impl Prepared { let mut inner_size = inner_rect.size(); for d in 0..2 { - inner_size[d] = match (scroll_enabled[d], auto_shrink[d]) { + inner_size[d] = match (direction_enabled[d], auto_shrink[d]) { (true, true) => inner_size[d].min(content_size[d]), // shrink scroll area if content is small (true, false) => inner_size[d], // let scroll area be larger than content; fill with blank space (false, true) => content_size[d], // Follow the content (expand/contract to fit it). @@ -964,18 +1080,18 @@ impl Prepared { let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use); let content_is_too_large = Vec2b::new( - scroll_enabled[0] && inner_rect.width() < content_size.x, - scroll_enabled[1] && inner_rect.height() < content_size.y, + direction_enabled[0] && inner_rect.width() < content_size.x, + direction_enabled[1] && inner_rect.height() < content_size.y, ); let max_offset = content_size - inner_rect.size(); let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect); - if scrolling_enabled && is_hovering_outer_rect { + if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect { let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction - && scroll_enabled[0] != scroll_enabled[1]; + && direction_enabled[0] != direction_enabled[1]; for d in 0..2 { - if scroll_enabled[d] { - let scroll_delta = ui.ctx().input_mut(|input| { + if direction_enabled[d] { + let scroll_delta = ui.ctx().input(|input| { if always_scroll_enabled_direction { // no bidirectional scrolling; allow horizontal scrolling without pressing shift input.smooth_scroll_delta[0] + input.smooth_scroll_delta[1] @@ -1010,7 +1126,7 @@ impl Prepared { let show_scroll_this_frame = match scroll_bar_visibility { ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE, ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large, - ScrollBarVisibility::AlwaysVisible => scroll_enabled, + ScrollBarVisibility::AlwaysVisible => direction_enabled, }; // Avoid frame delay; start showing scroll bar right away: @@ -1126,7 +1242,7 @@ impl Prepared { }; let interact_id = id.with(d); - let sense = if self.scrolling_enabled { + let sense = if scroll_source.scroll_bar && ui.is_enabled() { Sense::click_and_drag() } else { Sense::hover() @@ -1200,7 +1316,7 @@ impl Prepared { ); } - let visuals = if scrolling_enabled { + let visuals = if scroll_source.scroll_bar && ui.is_enabled() { // Pick visuals based on interaction with the handle. // Remember that the response is for the whole scroll bar! let is_hovering_handle = response.hovered()