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 <emil.ernerfeldt@gmail.com>

* 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 <emil.ernerfeldt@gmail.com>
This commit is contained in:
Ben Postlethwaite 2021-10-09 03:59:42 -07:00 committed by GitHub
parent 1dfc399d98
commit 5799758c2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 104 additions and 1 deletions

View file

@ -22,6 +22,11 @@ pub(crate) struct State {
/// Mouse offset relative to the top of the handle when started moving the handle. /// Mouse offset relative to the top of the handle when started moving the handle.
scroll_start_offset_from_top_left: [Option<f32>; 2], scroll_start_offset_from_top_left: [Option<f32>; 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 { impl Default for State {
@ -31,6 +36,7 @@ impl Default for State {
show_scroll: [false; 2], show_scroll: [false; 2],
vel: Vec2::ZERO, vel: Vec2::ZERO,
scroll_start_offset_from_top_left: [None; 2], scroll_start_offset_from_top_left: [None; 2],
scroll_stuck_to_end: [true; 2],
} }
} }
} }
@ -54,6 +60,11 @@ pub struct ScrollArea {
offset: Option<Vec2>, offset: Option<Vec2>,
/// If false, we ignore scroll events. /// If false, we ignore scroll events.
scrolling_enabled: bool, 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 { impl ScrollArea {
@ -89,6 +100,7 @@ impl ScrollArea {
id_source: None, id_source: None,
offset: None, offset: None,
scrolling_enabled: true, scrolling_enabled: true,
stick_to_end: [false; 2],
} }
} }
@ -188,6 +200,28 @@ impl ScrollArea {
pub(crate) fn has_any_bar(&self) -> bool { pub(crate) fn has_any_bar(&self) -> bool {
self.has_bar[0] || self.has_bar[1] 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 { struct Prepared {
@ -205,6 +239,7 @@ struct Prepared {
/// `viewport.min == ZERO` means we scrolled to the top. /// `viewport.min == ZERO` means we scrolled to the top.
viewport: Rect, viewport: Rect,
scrolling_enabled: bool, scrolling_enabled: bool,
stick_to_end: [bool; 2],
} }
impl ScrollArea { impl ScrollArea {
@ -217,6 +252,7 @@ impl ScrollArea {
id_source, id_source,
offset, offset,
scrolling_enabled, scrolling_enabled,
stick_to_end,
} = self; } = self;
let ctx = ui.ctx().clone(); let ctx = ui.ctx().clone();
@ -297,6 +333,7 @@ impl ScrollArea {
content_ui, content_ui,
viewport, viewport,
scrolling_enabled, scrolling_enabled,
stick_to_end,
} }
} }
@ -385,6 +422,7 @@ impl Prepared {
content_ui, content_ui,
viewport: _, viewport: _,
scrolling_enabled, scrolling_enabled,
stick_to_end,
} = self; } = self;
let content_size = content_ui.min_size(); let content_size = content_ui.min_size();
@ -460,6 +498,7 @@ impl Prepared {
if has_bar[d] { if has_bar[d] {
state.offset[d] -= input.pointer.delta()[d]; state.offset[d] -= input.pointer.delta()[d];
state.vel[d] = input.pointer.velocity()[d]; state.vel[d] = input.pointer.velocity()[d];
state.scroll_stuck_to_end[d] = false;
} else { } else {
state.vel[d] = 0.0; state.vel[d] = 0.0;
} }
@ -496,6 +535,7 @@ impl Prepared {
state.offset[d] -= scroll_delta[d]; state.offset[d] -= scroll_delta[d];
// Clear scroll delta so no parent scroll will use it. // Clear scroll delta so no parent scroll will use it.
frame_state.scroll_delta[d] = 0.0; 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 = let from_content =
|content| remap_clamp(content, 0.0..=content_size[d], min_main..=max_main); |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; 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]); 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 { } else {
state.scroll_start_offset_from_top_left[d] = None; state.scroll_start_offset_from_top_left[d] = None;
} }
@ -648,8 +696,19 @@ impl Prepared {
ui.ctx().request_repaint(); 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); 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; state.show_scroll = show_scroll_this_frame;
ui.memory().id_data.insert(id, state); ui.memory().id_data.insert(id, state);

View file

@ -6,6 +6,7 @@ enum ScrollDemo {
ScrollTo, ScrollTo,
ManyLines, ManyLines,
LargeCanvas, LargeCanvas,
StickToEnd,
} }
impl Default for ScrollDemo { impl Default for ScrollDemo {
@ -20,6 +21,7 @@ impl Default for ScrollDemo {
pub struct Scrolling { pub struct Scrolling {
demo: ScrollDemo, demo: ScrollDemo,
scroll_to: ScrollTo, scroll_to: ScrollTo,
scroll_stick_to: ScrollStickTo,
} }
impl super::Demo for Scrolling { impl super::Demo for Scrolling {
@ -52,6 +54,7 @@ impl super::View for Scrolling {
ScrollDemo::LargeCanvas, ScrollDemo::LargeCanvas,
"Scroll a large canvas", "Scroll a large canvas",
); );
ui.selectable_value(&mut self.demo, ScrollDemo::StickToEnd, "Stick to end");
}); });
ui.separator(); ui.separator();
match self.demo { match self.demo {
@ -64,6 +67,9 @@ impl super::View for Scrolling {
ScrollDemo::LargeCanvas => { ScrollDemo::LargeCanvas => {
huge_content_painter(ui); 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();
}
}