1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40:03 -04:00

Drag-to-scroll: now only on touch screens (#8181)

* Part of https://github.com/emilk/egui/issues/8180

Drag-to-scroll is a must-have on touch-screens, since there is no other
way to scroll.

However, when you are not on a touch screens, it is more surprising than
useful.
This commit is contained in:
Emil Ernerfeldt
2026-05-20 10:41:51 +02:00
committed by GitHub
parent 07c6e0de0f
commit e64b7683a2
3 changed files with 135 additions and 75 deletions

View File

@@ -141,6 +141,51 @@ impl ScrollBarVisibility {
]; ];
} }
/// When [`ScrollArea`] should let the user scroll by dragging the content.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum DragScroll {
/// Never scroll on pointer drag.
Never,
/// Only allow drag-to-scroll when a touch screen is detected
/// (see [`crate::InputState::has_touch_screen`]). The recommended default.
#[default]
OnTouch,
/// Always allow drag-to-scroll, even with a mouse.
Always,
}
impl DragScroll {
/// Whether drag-to-scroll is currently active.
///
/// Checks if we have a touch screen (via [`crate::InputState::has_touch_screen`])
/// when `self` is [`Self::OnTouch`].
pub fn enabled(self, ctx: &Context) -> bool {
match self {
Self::Never => false,
Self::OnTouch => ctx.input(|i| i.has_touch_screen()),
Self::Always => true,
}
}
}
impl BitOr for DragScroll {
type Output = Self;
/// Combine two settings, picking the more permissive one.
/// `Always > OnTouch > Never`.
#[inline]
fn bitor(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(Self::Always, _) | (_, Self::Always) => Self::Always,
(Self::OnTouch, _) | (_, Self::OnTouch) => Self::OnTouch,
(Self::Never, Self::Never) => Self::Never,
}
}
}
/// What is the source of scrolling for a [`ScrollArea`]. /// What is the source of scrolling for a [`ScrollArea`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
@@ -152,7 +197,11 @@ pub struct ScrollSource {
pub scroll_bar: bool, pub scroll_bar: bool,
/// Scroll the area by dragging the contents. /// Scroll the area by dragging the contents.
pub drag: bool, ///
/// Defaults to [`DragScroll::OnTouch`]: only active when a touch screen is
/// detected. Set to [`DragScroll::Always`] to force it on, or
/// [`DragScroll::Never`] to disable.
pub drag: DragScroll,
/// Scroll the area by scrolling (or shift scrolling) the mouse wheel with /// Scroll the area by scrolling (or shift scrolling) the mouse wheel with
/// the mouse cursor over the [`ScrollArea`]. /// the mouse cursor over the [`ScrollArea`].
@@ -160,35 +209,40 @@ pub struct ScrollSource {
} }
impl Default for ScrollSource { impl Default for ScrollSource {
/// `scroll_bar` and `mouse_wheel` enabled; `drag` set to [`DragScroll::OnTouch`].
fn default() -> Self { fn default() -> Self {
Self::ALL Self {
scroll_bar: true,
drag: DragScroll::OnTouch,
mouse_wheel: true,
}
} }
} }
impl ScrollSource { impl ScrollSource {
pub const NONE: Self = Self { pub const NONE: Self = Self {
scroll_bar: false, scroll_bar: false,
drag: false, drag: DragScroll::Never,
mouse_wheel: false, mouse_wheel: false,
}; };
pub const ALL: Self = Self { pub const ALL: Self = Self {
scroll_bar: true, scroll_bar: true,
drag: true, drag: DragScroll::Always,
mouse_wheel: true, mouse_wheel: true,
}; };
pub const SCROLL_BAR: Self = Self { pub const SCROLL_BAR: Self = Self {
scroll_bar: true, scroll_bar: true,
drag: false, drag: DragScroll::Never,
mouse_wheel: false, mouse_wheel: false,
}; };
pub const DRAG: Self = Self { pub const DRAG: Self = Self {
scroll_bar: false, scroll_bar: false,
drag: true, drag: DragScroll::Always,
mouse_wheel: false, mouse_wheel: false,
}; };
pub const MOUSE_WHEEL: Self = Self { pub const MOUSE_WHEEL: Self = Self {
scroll_bar: false, scroll_bar: false,
drag: false, drag: DragScroll::Never,
mouse_wheel: true, mouse_wheel: true,
}; };
@@ -201,13 +255,13 @@ impl ScrollSource {
/// Is anything enabled? /// Is anything enabled?
#[inline] #[inline]
pub fn any(&self) -> bool { pub fn any(&self) -> bool {
self.scroll_bar | self.drag | self.mouse_wheel self.scroll_bar || self.drag != DragScroll::Never || self.mouse_wheel
} }
/// Is everything enabled? /// Is everything enabled?
#[inline] #[inline]
pub fn is_all(&self) -> bool { pub fn is_all(&self) -> bool {
self.scroll_bar & self.drag & self.mouse_wheel self.scroll_bar && self.drag == DragScroll::Always && self.mouse_wheel
} }
} }
@@ -770,72 +824,74 @@ impl ScrollArea {
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size); let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
let dt = ui.input(|i| i.stable_dt).at_most(0.1); let dt = ui.input(|i| i.stable_dt).at_most(0.1);
let background_drag_response = let background_drag_response = if scroll_source.drag.enabled(ui.ctx())
if scroll_source.drag && ui.is_enabled() && state.content_is_too_large.any() { && ui.is_enabled()
// Drag contents to scroll (for touch screens mostly). && state.content_is_too_large.any()
// We must do this BEFORE adding content to the `ScrollArea`, {
// or we will steal input from the widgets we contain. // Drag contents to scroll (for touch screens mostly).
let content_response_option = state // We must do this BEFORE adding content to the `ScrollArea`,
.interact_rect // or we will steal input from the widgets we contain.
.map(|rect| ui.interact(rect, id.with("area"), Sense::DRAG)); let content_response_option = state
.interact_rect
.map(|rect| ui.interact(rect, id.with("area"), Sense::DRAG));
if content_response_option
.as_ref()
.is_some_and(|response| response.dragged())
{
for d in 0..2 {
if direction_enabled[d] {
ui.input(|input| {
state.offset[d] -= input.pointer.delta()[d];
});
state.scroll_stuck_to_end[d] = false;
state.offset_target[d] = None;
}
}
} else {
// Apply the cursor velocity to the scroll area when the user releases the drag.
if content_response_option if content_response_option
.as_ref() .as_ref()
.is_some_and(|response| response.dragged()) .is_some_and(|response| response.drag_stopped())
{ {
for d in 0..2 { state.vel =
if direction_enabled[d] { direction_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
ui.input(|input| { }
state.offset[d] -= input.pointer.delta()[d]; for d in 0..2 {
}); // Kinetic scrolling
state.scroll_stuck_to_end[d] = false; let stop_speed = 20.0; // Pixels per second.
state.offset_target[d] = None; let friction_coeff = 1000.0; // Pixels per second squared.
}
}
} else {
// Apply the cursor velocity to the scroll area when the user releases the drag.
if content_response_option
.as_ref()
.is_some_and(|response| response.drag_stopped())
{
state.vel = direction_enabled.to_vec2()
* ui.input(|input| input.pointer.velocity());
}
for d in 0..2 {
// Kinetic scrolling
let stop_speed = 20.0; // Pixels per second.
let friction_coeff = 1000.0; // Pixels per second squared.
let friction = friction_coeff * dt; let friction = friction_coeff * dt;
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed { if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
state.vel[d] = 0.0; state.vel[d] = 0.0;
} else { } else {
state.vel[d] -= friction * state.vel[d].signum(); state.vel[d] -= friction * state.vel[d].signum();
// Offset has an inverted coordinate system compared to // Offset has an inverted coordinate system compared to
// the velocity, so we subtract it instead of adding it // the velocity, so we subtract it instead of adding it
state.offset[d] -= state.vel[d] * dt; state.offset[d] -= state.vel[d] * dt;
ctx.request_repaint(); ctx.request_repaint();
}
} }
} }
}
// Set the desired mouse cursors. // Set the desired mouse cursors.
if let Some(response) = &content_response_option { if let Some(response) = &content_response_option {
if response.dragged() if response.dragged()
&& let Some(cursor) = on_drag_cursor && let Some(cursor) = on_drag_cursor
{ {
ui.set_cursor_icon(cursor); ui.set_cursor_icon(cursor);
} else if response.hovered() } else if response.hovered()
&& let Some(cursor) = on_hover_cursor && let Some(cursor) = on_hover_cursor
{ {
ui.set_cursor_icon(cursor); ui.set_cursor_icon(cursor);
}
} }
}
content_response_option content_response_option
} else { } else {
None None
}; };
// Scroll with an animation if we have a target offset (that hasn't been cleared by the code // Scroll with an animation if we have a target offset (that hasn't been cleared by the code
// above). // above).

View File

@@ -6,7 +6,7 @@ use epaint::CornerRadiusF32;
use crate::collapsing_header::CollapsingState; use crate::collapsing_header::CollapsingState;
use crate::*; use crate::*;
use super::scroll_area::{ScrollBarVisibility, ScrollSource}; use super::scroll_area::{DragScroll, ScrollBarVisibility, ScrollSource};
use super::{Area, Frame, Resize, ScrollArea, area, resize}; use super::{Area, Frame, Resize, ScrollArea, area, resize};
/// Builder for a floating window which can be dragged, closed, collapsed, resized and scrolled (off by default). /// Builder for a floating window which can be dragged, closed, collapsed, resized and scrolled (off by default).
@@ -438,11 +438,13 @@ impl<'a> Window<'a> {
self self
} }
/// Enable/disable scrolling on the window by dragging with the pointer. `true` by default. /// Controls scrolling the window by dragging the contents with the pointer.
/// ///
/// See [`ScrollArea::scroll_source`] for more. /// Defaults to [`DragScroll::OnTouch`] — only active when a touch screen is detected.
///
/// See [`ScrollArea::scroll_source`] and [`DragScroll`] for more.
#[inline] #[inline]
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self { pub fn drag_to_scroll(mut self, drag_to_scroll: DragScroll) -> Self {
self.scroll = self.scroll.scroll_source(ScrollSource { self.scroll = self.scroll.scroll_source(ScrollSource {
drag: drag_to_scroll, drag: drag_to_scroll,
..Default::default() ..Default::default()

View File

@@ -5,7 +5,7 @@
use egui::{ use egui::{
Align, Id, NumExt as _, Rangef, Rect, Response, ScrollArea, Ui, Vec2, Vec2b, Align, Id, NumExt as _, Rangef, Rect, Response, ScrollArea, Ui, Vec2, Vec2b,
scroll_area::{ScrollAreaOutput, ScrollBarVisibility, ScrollSource}, scroll_area::{DragScroll, ScrollAreaOutput, ScrollBarVisibility, ScrollSource},
}; };
use crate::{ use crate::{
@@ -180,7 +180,7 @@ fn to_sizing(columns: &[Column]) -> crate::sizing::Sizing {
struct TableScrollOptions { struct TableScrollOptions {
vscroll: bool, vscroll: bool,
drag_to_scroll: bool, drag_to_scroll: DragScroll,
stick_to_bottom: bool, stick_to_bottom: bool,
scroll_to_row: Option<(usize, Option<Align>)>, scroll_to_row: Option<(usize, Option<Align>)>,
scroll_offset_y: Option<f32>, scroll_offset_y: Option<f32>,
@@ -195,7 +195,7 @@ impl Default for TableScrollOptions {
fn default() -> Self { fn default() -> Self {
Self { Self {
vscroll: true, vscroll: true,
drag_to_scroll: true, drag_to_scroll: DragScroll::OnTouch,
stick_to_bottom: false, stick_to_bottom: false,
scroll_to_row: None, scroll_to_row: None,
scroll_offset_y: None, scroll_offset_y: None,
@@ -318,11 +318,13 @@ impl<'a> TableBuilder<'a> {
self self
} }
/// Enables scrolling the table's contents using mouse drag (default: `true`). /// Controls scrolling the table's contents by dragging with the pointer.
/// ///
/// See [`ScrollArea::scroll_source`] for more. /// Defaults to [`DragScroll::OnTouch`] — only active when a touch screen is detected.
///
/// See [`ScrollArea::scroll_source`] and [`DragScroll`] for more.
#[inline] #[inline]
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self { pub fn drag_to_scroll(mut self, drag_to_scroll: DragScroll) -> Self {
self.scroll_options.drag_to_scroll = drag_to_scroll; self.scroll_options.drag_to_scroll = drag_to_scroll;
self self
} }