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:
parent
1dfc399d98
commit
5799758c2b
2 changed files with 104 additions and 1 deletions
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue