diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 700c2040253..229870db9d4 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -783,7 +783,14 @@ impl Prepared { let content_size = content_ui.min_size(); + let scroll_delta = content_ui + .ctx() + .frame_state_mut(|state| std::mem::take(&mut state.scroll_delta)); + for d in 0..2 { + // FrameState::scroll_delta is inverted from the way we apply the delta, so we need to negate it. + let mut delta = -scroll_delta[d]; + // We always take both scroll targets regardless of which scroll axes are enabled. This // is to avoid them leaking to other scroll areas. let scroll_target = content_ui @@ -791,7 +798,7 @@ impl Prepared { .frame_state_mut(|state| state.scroll_target[d].take()); if scroll_enabled[d] { - if let Some((target_range, align)) = scroll_target { + delta += if let Some((target_range, align)) = scroll_target { let min = content_ui.min_rect().min[d]; let clip_rect = content_ui.clip_rect(); let visible_range = min..=min + clip_rect.size()[d]; @@ -800,7 +807,7 @@ impl Prepared { let clip_end = clip_rect.max[d]; let mut spacing = ui.spacing().item_spacing[d]; - let delta = if let Some(align) = align { + if let Some(align) = align { let center_factor = align.to_factor(); let offset = @@ -817,31 +824,32 @@ impl Prepared { } else { // Ui is already in view, no need to adjust scroll. 0.0 - }; + } + } else { + 0.0 + }; - if delta != 0.0 { - let target_offset = state.offset[d] + delta; + if delta != 0.0 { + let target_offset = state.offset[d] + delta; - if !animated { - state.offset[d] = target_offset; - } else if let Some(animation) = &mut state.offset_target[d] { - // For instance: the user is continuously calling `ui.scroll_to_cursor`, - // so we don't want to reset the animation, but perhaps update the target: - animation.target_offset = target_offset; - } else { - // The further we scroll, the more time we take. - // TODO(emilk): let users configure this in `Style`. - let now = ui.input(|i| i.time); - let points_per_second = 1000.0; - let animation_duration = - (delta.abs() / points_per_second).clamp(0.1, 0.3); - state.offset_target[d] = Some(ScrollTarget { - animation_time_span: (now, now + animation_duration as f64), - target_offset, - }); - } - ui.ctx().request_repaint(); + if !animated { + state.offset[d] = target_offset; + } else if let Some(animation) = &mut state.offset_target[d] { + // For instance: the user is continuously calling `ui.scroll_to_cursor`, + // so we don't want to reset the animation, but perhaps update the target: + animation.target_offset = target_offset; + } else { + // The further we scroll, the more time we take. + // TODO(emilk): let users configure this in `Style`. + let now = ui.input(|i| i.time); + let points_per_second = 1000.0; + let animation_duration = (delta.abs() / points_per_second).clamp(0.1, 0.3); + state.offset_target[d] = Some(ScrollTarget { + animation_time_span: (now, now + animation_duration as f64), + target_offset, + }); } + ui.ctx().request_repaint(); } } } diff --git a/crates/egui/src/frame_state.rs b/crates/egui/src/frame_state.rs index 5f966d117a0..e1414bba166 100644 --- a/crates/egui/src/frame_state.rs +++ b/crates/egui/src/frame_state.rs @@ -38,9 +38,20 @@ pub(crate) struct FrameState { /// Initialized to `None` at the start of each frame. pub(crate) tooltip_state: Option, - /// horizontal, vertical + /// The current scroll area should scroll to this range (horizontal, vertical). pub(crate) scroll_target: [Option<(Rangef, Option)>; 2], + /// The current scroll area should scroll by this much. + /// + /// The delta dictates how the _content_ should move. + /// + /// A positive X-value indicates the content is being moved right, + /// as when swiping right on a touch-screen or track-pad with natural scrolling. + /// + /// A positive Y-value indicates the content is being moved down, + /// as when swiping down on a touch-screen or track-pad with natural scrolling. + pub(crate) scroll_delta: Vec2, + #[cfg(feature = "accesskit")] pub(crate) accesskit_state: Option, @@ -63,6 +74,7 @@ impl Default for FrameState { used_by_panels: Rect::NAN, tooltip_state: None, scroll_target: [None, None], + scroll_delta: Vec2::default(), #[cfg(feature = "accesskit")] accesskit_state: None, highlight_this_frame: Default::default(), @@ -84,6 +96,7 @@ impl FrameState { used_by_panels, tooltip_state, scroll_target, + scroll_delta, #[cfg(feature = "accesskit")] accesskit_state, highlight_this_frame, @@ -99,6 +112,7 @@ impl FrameState { *used_by_panels = Rect::NOTHING; *tooltip_state = None; *scroll_target = [None, None]; + *scroll_delta = Vec2::default(); #[cfg(debug_assertions)] { diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 00b7beef6e4..7343e9ea723 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -1075,6 +1075,8 @@ impl Ui { /// A positive Y-value indicates the content is being moved down, /// as when swiping down on a touch-screen or track-pad with natural scrolling. /// + /// If this is called multiple times per frame for the same [`ScrollArea`], the deltas will be summed. + /// /// /// See also: [`Response::scroll_to_me`], [`Ui::scroll_to_rect`], [`Ui::scroll_to_cursor`] /// /// ``` @@ -1093,8 +1095,9 @@ impl Ui { /// # }); /// ``` pub fn scroll_with_delta(&self, delta: Vec2) { - self.ctx() - .input_mut(|input| input.smooth_scroll_delta += delta); + self.ctx().frame_state_mut(|state| { + state.scroll_delta += delta; + }); } } diff --git a/crates/egui_demo_lib/src/demo/scrolling.rs b/crates/egui_demo_lib/src/demo/scrolling.rs index 9c490d23d8a..71d47f9d84c 100644 --- a/crates/egui_demo_lib/src/demo/scrolling.rs +++ b/crates/egui_demo_lib/src/demo/scrolling.rs @@ -236,6 +236,7 @@ struct ScrollTo { track_item: usize, tack_item_align: Option, offset: f32, + delta: f32, } impl Default for ScrollTo { @@ -244,6 +245,7 @@ impl Default for ScrollTo { track_item: 25, tack_item_align: Some(Align::Center), offset: 0.0, + delta: 64.0, } } } @@ -258,6 +260,7 @@ impl super::View for ScrollTo { let mut go_to_scroll_offset = false; let mut scroll_top = false; let mut scroll_bottom = false; + let mut scroll_delta = None; ui.horizontal(|ui| { ui.label("Scroll to a specific item index:"); @@ -294,6 +297,20 @@ impl super::View for ScrollTo { scroll_bottom |= ui.button("Scroll to bottom").clicked(); }); + ui.horizontal(|ui| { + ui.label("Scroll by"); + DragValue::new(&mut self.delta) + .speed(1.0) + .suffix("px") + .ui(ui); + if ui.button("⬇").clicked() { + scroll_delta = Some(self.delta * Vec2::UP); // scroll down (move contents up) + } + if ui.button("⬆").clicked() { + scroll_delta = Some(self.delta * Vec2::DOWN); // scroll up (move contents down) + } + }); + let mut scroll_area = ScrollArea::vertical().max_height(200.0).auto_shrink(false); if go_to_scroll_offset { scroll_area = scroll_area.vertical_scroll_offset(self.offset); @@ -305,6 +322,10 @@ impl super::View for ScrollTo { if scroll_top { ui.scroll_to_cursor(Some(Align::TOP)); } + if let Some(scroll_delta) = scroll_delta { + ui.scroll_with_delta(scroll_delta); + } + ui.vertical(|ui| { for item in 1..=num_items { if track_item && item == self.track_item {