Skip to content

scroll area moves in jitters/jumps on windows with trackpad scrolls #8215

@Jaysmito101

Description

@Jaysmito101

Describe the bug

I have a simple egui scroll view with a bunch of rects in it, when I just make a single fast swipe on the trackpad the scroll moves in jumps/jitters (as shown in the video, it was a single scroll swipe).

To Reproduce

use eframe::egui;

const COLS: usize = 8;
const ROWS: usize = 200;
const RECT_SIZE: f32 = 60.0;
const GAP: f32 = 4.0;

struct App;

impl eframe::App for App {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ctx, |ui| {
            egui::ScrollArea::vertical()
                .auto_shrink([false; 2])
                .show(ui, |ui| {
                    let cell = RECT_SIZE + GAP;
                    let total_w = COLS as f32 * cell - GAP;
                    let total_h = ROWS as f32 * cell - GAP;

                    let (rect, _) =
                        ui.allocate_exact_size(egui::vec2(total_w, total_h), egui::Sense::hover());

                    let painter = ui.painter();
                    for row in 0..ROWS {
                        for col in 0..COLS {
                            let idx = row * COLS + col;
                            let hue = idx as f32 / (ROWS * COLS) as f32;
                            let color =
                                egui::Color32::from(egui::ecolor::Hsva::new(hue, 0.75, 0.85, 1.0));

                            let min = rect.min + egui::vec2(col as f32 * cell, row as f32 * cell);
                            let tile =
                                egui::Rect::from_min_size(min, egui::vec2(RECT_SIZE, RECT_SIZE));

                            painter.rect_filled(tile, 4.0, color);
                            painter.text(
                                tile.center(),
                                egui::Align2::CENTER_CENTER,
                                idx.to_string(),
                                egui::FontId::proportional(14.0),
                                egui::Color32::WHITE,
                            );
                        }
                    }
                });
        });
    }
}

fn main() -> eframe::Result<()> {
    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_inner_size([640.0, 480.0])
            .with_title("egui scroll repro"),
        ..Default::default()
    };
    eframe::run_native("repro", options, Box::new(|_cc| Ok(Box::new(App))))
}

Run this example and scroll fast with a trackpad.
For me this is consistently reproducible.

Expected behavior

Screenshots

20260602-0645-36.5288603.mp4

Desktop (please complete the following information):

  • OS: Windows 11

Additional context

I tried to fix it for myself by driving the scroll offset the raw input data, but I am not sure if this is a good way of doing it.

use eframe::egui;

const COLS: usize = 8;
const ROWS: usize = 200;
const RECT_SIZE: f32 = 60.0;
const GAP: f32 = 4.0;
const EASE_RATE: f32 = 12.0;
const SNAP: f32 = 0.3;

#[derive(Default)]
struct App {
    target: f32,
    pos: f32,
    max_off: f32,
    velocity: f32,
    had_scroll: bool,
}

impl eframe::App for App {
    fn raw_input_hook(&mut self, _ctx: &egui::Context, raw_input: &mut egui::RawInput) {
        raw_input.events.retain(|event| {
            if let egui::Event::MouseWheel { unit, delta, .. } = event {
                let dy = match unit {
                    egui::MouseWheelUnit::Point => delta.y,
                    egui::MouseWheelUnit::Line => delta.y * (RECT_SIZE + GAP),
                    egui::MouseWheelUnit::Page => delta.y * self.max_off,
                };
                self.target = (self.target - dy).clamp(0.0, self.max_off);
                self.velocity = -dy;
                self.had_scroll = true;
                false
            } else {
                true
            }
        });
    }

    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ctx, |ui| {
            let cell = RECT_SIZE + GAP;
            let total_h = ROWS as f32 * cell - GAP;

            self.max_off = (total_h - ui.available_height()).max(0.0);
            self.target = self.target.clamp(0.0, self.max_off);
            self.pos = self.pos.clamp(0.0, self.max_off);

            let had_scroll = self.had_scroll;
            self.had_scroll = false;

            if !had_scroll {
                self.target = (self.target + self.velocity).clamp(0.0, self.max_off);
                self.velocity *= 0.9;
                if self.velocity.abs() < 0.5 {
                    self.velocity = 0.0;
                }
            }

            let dt = ctx.input(|i| i.unstable_dt).min(0.05);
            let diff = self.target - self.pos;
            self.pos += diff * (1.0 - (-EASE_RATE * dt).exp());
            if diff.abs() < SNAP {
                self.pos = self.target;
            }

            egui::ScrollArea::vertical()
                .vertical_scroll_offset(self.pos)
                .auto_shrink(false)
                .animated(false)
                .show(ui, |ui| {
                    let total_w = COLS as f32 * cell - GAP;
                    let (rect, _) =
                        ui.allocate_exact_size(egui::vec2(total_w, total_h), egui::Sense::hover());

                    let painter = ui.painter();
                    for row in 0..ROWS {
                        for col in 0..COLS {
                            let idx = row * COLS + col;
                            let hue = idx as f32 / (ROWS * COLS) as f32;
                            let color =
                                egui::Color32::from(egui::ecolor::Hsva::new(hue, 0.75, 0.85, 1.0));

                            let min = rect.min + egui::vec2(col as f32 * cell, row as f32 * cell);
                            let tile =
                                egui::Rect::from_min_size(min, egui::vec2(RECT_SIZE, RECT_SIZE));

                            painter.rect_filled(tile, 4.0, color);
                            painter.text(
                                tile.center(),
                                egui::Align2::CENTER_CENTER,
                                idx.to_string(),
                                egui::FontId::proportional(14.0),
                                egui::Color32::WHITE,
                            );
                        }
                    }
                });
        });

        ctx.request_repaint();
    }
}

fn main() -> eframe::Result {
    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_inner_size([640.0, 480.0])
            .with_title("egui scroll repro"),
        ..Default::default()
    };
    eframe::run_native(
        "repro",
        options,
        Box::new(|_cc| Ok(Box::new(App::default()))),
    )
}
20260602-0656-12.9729125.mp4

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions