From 5799758c2be59d0f06c530d8fd0bf01d78de96a4 Mon Sep 17 00:00:00 2001 From: Ben Postlethwaite Date: Sat, 9 Oct 2021 03:59:42 -0700 Subject: [PATCH] implement stick-to-end scroll (#765) * implement stick-to-end scroll * improve comment grammar * accept emilk suggestion for demo text tweak Co-authored-by: Emil Ernerfeldt * request repaint on each frame to show incoming scroll demo rows * simplify pub api + doc strings * disable scroll_stuck_to_end when wheel-scrolling or dragging Co-authored-by: Emil Ernerfeldt --- egui/src/containers/scroll_area.rs | 61 +++++++++++++++++++++++- egui_demo_lib/src/apps/demo/scrolling.rs | 44 +++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/egui/src/containers/scroll_area.rs b/egui/src/containers/scroll_area.rs index d408b78c..581f7028 100644 --- a/egui/src/containers/scroll_area.rs +++ b/egui/src/containers/scroll_area.rs @@ -22,6 +22,11 @@ pub(crate) struct State { /// Mouse offset relative to the top of the handle when started moving the handle. scroll_start_offset_from_top_left: [Option; 2], + + /// Is the scroll sticky. This is true while scroll handle is in the end position + /// and remains that way until the user moves the scroll_handle. Once unstuck (false) + /// it remains false until the scroll touches the end position, which reenables stickiness. + scroll_stuck_to_end: [bool; 2], } impl Default for State { @@ -31,6 +36,7 @@ impl Default for State { show_scroll: [false; 2], vel: Vec2::ZERO, scroll_start_offset_from_top_left: [None; 2], + scroll_stuck_to_end: [true; 2], } } } @@ -54,6 +60,11 @@ pub struct ScrollArea { offset: Option, /// If false, we ignore scroll events. scrolling_enabled: bool, + + /// If true for vertical or horizontal the scroll wheel will stick to the + /// end position until user manually changes position. It will become true + /// again once scroll handle makes contact with end. + stick_to_end: [bool; 2], } impl ScrollArea { @@ -89,6 +100,7 @@ impl ScrollArea { id_source: None, offset: None, scrolling_enabled: true, + stick_to_end: [false; 2], } } @@ -188,6 +200,28 @@ impl ScrollArea { pub(crate) fn has_any_bar(&self) -> bool { self.has_bar[0] || self.has_bar[1] } + + /// The scroll handle will stick to the rightmost position even while the content size + /// changes dynamically. This can be useful to simulate text scrollers coming in from right + /// hand side. The scroll handle remains stuck until user manually changes position. Once "unstuck" + /// it will remain focused on whatever content viewport the user left it on. If the scroll + /// handle is dragged all the way to the right it will again become stuck and remain there + /// until manually pulled from the end position. + pub fn stick_to_right(mut self) -> Self { + self.stick_to_end[0] = true; + self + } + + /// The scroll handle will stick to the bottom position even while the content size + /// changes dynamically. This can be useful to simulate terminal UIs or log/info scrollers. + /// The scroll handle remains stuck until user manually changes position. Once "unstuck" + /// it will remain focused on whatever content viewport the user left it on. If the scroll + /// handle is dragged to the bottom it will again become stuck and remain there until manually + /// pulled from the end position. + pub fn stick_to_bottom(mut self) -> Self { + self.stick_to_end[1] = true; + self + } } struct Prepared { @@ -205,6 +239,7 @@ struct Prepared { /// `viewport.min == ZERO` means we scrolled to the top. viewport: Rect, scrolling_enabled: bool, + stick_to_end: [bool; 2], } impl ScrollArea { @@ -217,6 +252,7 @@ impl ScrollArea { id_source, offset, scrolling_enabled, + stick_to_end, } = self; let ctx = ui.ctx().clone(); @@ -297,6 +333,7 @@ impl ScrollArea { content_ui, viewport, scrolling_enabled, + stick_to_end, } } @@ -385,6 +422,7 @@ impl Prepared { content_ui, viewport: _, scrolling_enabled, + stick_to_end, } = self; let content_size = content_ui.min_size(); @@ -460,6 +498,7 @@ impl Prepared { if has_bar[d] { state.offset[d] -= input.pointer.delta()[d]; state.vel[d] = input.pointer.velocity()[d]; + state.scroll_stuck_to_end[d] = false; } else { state.vel[d] = 0.0; } @@ -496,6 +535,7 @@ impl Prepared { state.offset[d] -= scroll_delta[d]; // Clear scroll delta so no parent scroll will use it. frame_state.scroll_delta[d] = 0.0; + state.scroll_stuck_to_end[d] = false; } } } @@ -542,6 +582,11 @@ impl Prepared { ) }; + // maybe force increase in offset to keep scroll stuck to end position + if stick_to_end[d] && state.scroll_stuck_to_end[d] { + state.offset[d] = content_size[d] - inner_rect.size()[d]; + } + let from_content = |content| remap_clamp(content, 0.0..=content_size[d], min_main..=max_main); @@ -584,6 +629,9 @@ impl Prepared { let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left; state.offset[d] = remap(new_handle_top, min_main..=max_main, 0.0..=content_size[d]); + + // some manual action taken, scroll not stuck + state.scroll_stuck_to_end[d] = false; } else { state.scroll_start_offset_from_top_left[d] = None; } @@ -648,8 +696,19 @@ impl Prepared { ui.ctx().request_repaint(); } - state.offset = state.offset.min(content_size - inner_rect.size()); + let available_offset = content_size - inner_rect.size(); + state.offset = state.offset.min(available_offset); state.offset = state.offset.max(Vec2::ZERO); + + // Is scroll handle at end of content? If so enter sticky mode. + // Only has an effect if stick_to_end is enabled but we save in + // state anyway so that entering sticky mode at an arbitrary time + // has appropriate effect. + state.scroll_stuck_to_end = [ + state.offset[0] == available_offset[0], + state.offset[1] == available_offset[1], + ]; + state.show_scroll = show_scroll_this_frame; ui.memory().id_data.insert(id, state); diff --git a/egui_demo_lib/src/apps/demo/scrolling.rs b/egui_demo_lib/src/apps/demo/scrolling.rs index 7812de68..11882caf 100644 --- a/egui_demo_lib/src/apps/demo/scrolling.rs +++ b/egui_demo_lib/src/apps/demo/scrolling.rs @@ -6,6 +6,7 @@ enum ScrollDemo { ScrollTo, ManyLines, LargeCanvas, + StickToEnd, } impl Default for ScrollDemo { @@ -20,6 +21,7 @@ impl Default for ScrollDemo { pub struct Scrolling { demo: ScrollDemo, scroll_to: ScrollTo, + scroll_stick_to: ScrollStickTo, } impl super::Demo for Scrolling { @@ -52,6 +54,7 @@ impl super::View for Scrolling { ScrollDemo::LargeCanvas, "Scroll a large canvas", ); + ui.selectable_value(&mut self.demo, ScrollDemo::StickToEnd, "Stick to end"); }); ui.separator(); match self.demo { @@ -64,6 +67,9 @@ impl super::View for Scrolling { ScrollDemo::LargeCanvas => { huge_content_painter(ui); } + ScrollDemo::StickToEnd => { + self.scroll_stick_to.ui(ui); + } } } } @@ -244,3 +250,41 @@ impl super::View for ScrollTo { }); } } + +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +#[derive(PartialEq)] +struct ScrollStickTo { + n_items: usize, +} + +impl Default for ScrollStickTo { + fn default() -> Self { + Self { n_items: 0 } + } +} + +impl super::View for ScrollStickTo { + fn ui(&mut self, ui: &mut Ui) { + ui.label("Rows enter from the bottom, we want the scroll handle to start and stay at bottom unless moved"); + + ui.add_space(4.0); + + let text_style = TextStyle::Body; + let row_height = ui.fonts()[text_style].row_height(); + ScrollArea::vertical().stick_to_bottom().show_rows( + ui, + row_height, + self.n_items, + |ui, row_range| { + for row in row_range { + let text = format!("This is row {}", row + 1); + ui.label(text); + } + }, + ); + + self.n_items += 1; + ui.ctx().request_repaint(); + } +}