Issues #444, #186: a mouse wheel scroll speed preference, faster by default on Windows and Linux (#514)

egui scrolls 40 points per wheel line on the desktop. winit reports one line
per notch on Windows and Linux, where browsers scroll about 100 pixels, so
the wheel felt slow there. Preferences ▸ Documents and view gets "Mouse wheel
scroll speed": Slow / Normal / Fast (0.5×, 1×, 2× of the platform's normal:
100 pt per line on Windows and Linux, 40 on macOS and 8 on the web, both
egui's defaults). It is saved, and `set_option("scroll_speed", …)` sets it
over the control channel.

- Applied in `raw_input_hook`, before egui reads the frame's input (and in
  `logic` for hosts without the hook).
- ⌘/Ctrl-wheel zoom keeps egui's rate: those line deltas are scaled back,
  since egui scales lines before turning them into zoom.
- Point deltas (most trackpads) keep their speed; Single page wheel paging
  still turns one page per notch (its notch size is fixed).
- Labels translated (fr, ru, zh-hans, zh-hant, es, ja, bg, de), the speed names in
  a "scroll speed" context; catalog hashes updated.


Claude-Session: https://claude.ai/code/session_01VNJqS2dEQawHJn1qTRJiKp

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Hal Goff
2026-10-11 13:05:53 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent 10f801e752
commit 5d301305c9
21 changed files with 253 additions and 18 deletions
+15 -15
View File
@@ -19,7 +19,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "1494c20c21ac3491d22531c7b05b6e718b361c6e587ae4df8d7fa11a3f5ea484"
sha256 = "1cc32f906351568117e0bf64d67cc4aac364772b1fb1cb884bff4b552f7c545d"
[[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
@@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "7d0770a42541d1c3f1c268450b81ca1d1cf49212f14104ffce25f55fdfbb3d51"
sha256 = "472f3ebfa5e9831a92aabf469acab5cdb6ae8af6add95611f50f3b74c420575c"
[[asset]]
path = "crates/ui-egui/src/i18n/cs.tsv"
@@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "935be8513694b4bec72b2b998f8061b2c759656d409fa91d7909726d2a2b6252"
sha256 = "e0ac6eaa707d1e52505d55f401a442af5674336596f27a4a271b48b91337551e"
[[asset]]
path = "crates/ui-egui/src/i18n/es.tsv"
@@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "e78ed8e2cbc06843be00788142d1d14448f51b7fe19e5c264a7757c1e041eda6"
sha256 = "db87ab4ba9b7cd2e8547b24cc334ba01af6dbb2a5b1fbb33dd51a8fb44157198"
[[asset]]
path = "crates/ui-egui/src/i18n/fr.tsv"
@@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "d72057bf5178e6847303c859b21ef9173f1a0f23ff6f8af4bbe65ffb5d31c5c2"
sha256 = "6f1cd0b54493510fb22566b0659476f98f5937bffe9742de91092edc3053f68f"
[[asset]]
path = "crates/ui-egui/src/i18n/de.tsv"
@@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "fa4ec97ae6e5c9a21e420db46da3a99e135eea4cf7065c3514e817eb85003ece"
sha256 = "d95feff5318f449d7a438a3100f4190d0d570afc1ba40b55ecebd0e7edfc3288"
[[asset]]
path = "crates/ui-egui/src/i18n/te.tsv"
@@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "a10ac20172d220e4981f122f7c68c5310f7e6e53e6d3c128ea2e0928d58efe84"
sha256 = "41c9cc2664abb82149d88460b35494022dd72f8f10b074c7972154c556cf5e16"
[[asset]]
path = "crates/ui-egui/src/i18n/it.tsv"
@@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "33d7f73c6b8837807ff12451a00a5359546ffef57e4ce75a2ee5fe53578888c8"
sha256 = "15736e8b0b22c34d151d3998fb342f0dc1e3d0ebda6baefd56c27581018ad07e"
[[asset]]
path = "crates/ui-egui/src/i18n/pl.tsv"
@@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "e30e23bafce57e494ab99b8da0658292aa3da656c6d8f4470636490ce1a998f4"
sha256 = "2e36c61bb681d1d357604254d7b38c86564f8b3b52d72382f050529ac224285e"
[[asset]]
path = "crates/ui-egui/src/i18n/ru.tsv"
@@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "c32312580b8c398cf22ad281dab6cb73acc6d11395e238569e00192254bbf07a"
sha256 = "4b253cfe16540a7f2a24f50be28edf3ceef8a4e2db092ebecb97cc325be5a50a"
[[asset]]
path = "crates/ui-egui/src/i18n/hu.tsv"
@@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "e82c1076711ad819892a21e9dfc836f27c5545d8acd27a961644477949a3039d"
sha256 = "1bebc7f5441412596124e46f304f446419e55cb8468e18f7ab52dc5562950484"
[[asset]]
path = "crates/ui-egui/src/i18n/bg.tsv"
@@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "d92f7f598b61cc2874f75ceb247d6df92c715a0c3bb1cb51574343d2e212fda1"
sha256 = "3f8908a3577fa118300e47cbfe25b3d792775365cbfae0aa07e6178137d4beee"
[[asset]]
path = "crates/ui-egui/src/i18n/ja.tsv"
@@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "5828c26d65ded4d272bdf1abd8ce56fc71c963953d7c4ea10021fcea02eda6fb"
sha256 = "0c522c8292bc55c2ba8065378202feda081f02f03029dfe06a9b65aba625c677"
[[asset]]
path = "crates/ui-egui/src/i18n/uk.tsv"
@@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "0555902c3f2ec6269121c033050055374cf9afd4de8f64d7857c06cba841c179"
sha256 = "9cc716897f6d3e6a4da91eaadb0aff82117d5c6145575c5f33fb943b557fffe2"
[[asset]]
path = "crates/ui-egui/src/i18n/ar.tsv"
@@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "cd168b8ca799fd4552b09d791e762b2cf05ec37a6eb7aeb5d304281d812c5411"
sha256 = "c6c85ed20d1135f0ff605d28e2fe0c9b0ecf03bde84753d184f2401965bc9bdb"
[[asset]]
path = "assets/icons/accessibility.svg"
+4
View File
@@ -2221,6 +2221,10 @@ key Space مسافة
Panels اللوحات
Resize panels تغيير حجم اللوحات
Resize panel window تغيير حجم نافذة اللوحة
Mouse wheel scroll speed سرعة التمرير بعجلة الماوس
scroll speed Slow بطيئة
scroll speed Normal عادية
scroll speed Fast سريعة
Enhance camera image تحسين صورة الكاميرا
Drag the corners onto the document's edges, then choose Enhance page. اسحب الزوايا إلى حواف المستند، ثم اختر تحسين الصفحة.
Enhance page تحسين الصفحة
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -2163,6 +2163,10 @@ key Space Интервал
Resize panel window Преоразмери прозореца на панела
Tools Инструменти
Inspector Инспектор
Mouse wheel scroll speed Скорост на превъртане с колелцето на мишката
scroll speed Slow Ниска
scroll speed Normal Нормална
scroll speed Fast Висока
Enhance camera image Подобри снимка от камера
Drag the corners onto the document's edges, then choose Enhance page. Плъзнете ъглите до краищата на документа, след това изберете „Подобри страницата“.
Enhance page Подобри страницата
Can't render this file because it contains an unexpected character in line 8 and column 16.
+4
View File
@@ -2201,6 +2201,10 @@ key Space Leertaste
Resize panel window Größe des Bereichsfensters ändern
Tools Werkzeuge
Inspector Inspektor
Mouse wheel scroll speed Scrollgeschwindigkeit des Mausrads
scroll speed Slow Langsam
scroll speed Normal Normal
scroll speed Fast Schnell
Enhance camera image Kamerafoto verbessern
Drag the corners onto the document's edges, then choose Enhance page. Ziehen Sie die Ecken auf die Kanten des Dokuments und wählen Sie dann Seite verbessern.
Enhance page Seite verbessern
Can't render this file because it contains an unexpected character in line 6 and column 51.
+4
View File
@@ -2116,6 +2116,10 @@ key Space Espacio
Resize panel window Cambiar tamaño de la ventana del panel
Tools Herramientas
Inspector Inspector
Mouse wheel scroll speed Velocidad de desplazamiento de la rueda del ratón
scroll speed Slow Lenta
scroll speed Normal Normal
scroll speed Fast Rápida
Enhance camera image Mejorar foto de cámara
Drag the corners onto the document's edges, then choose Enhance page. Arrastre las esquinas hasta los bordes del documento y elija Mejorar página.
Enhance page Mejorar página
Can't render this file because it contains an unexpected character in line 6 and column 60.
+4
View File
@@ -2201,6 +2201,10 @@ key Space Espace
Resize panel window Redimensionner la fenêtre du panneau
Tools Outils
Inspector Inspecteur
Mouse wheel scroll speed Vitesse de défilement de la molette
scroll speed Slow Lente
scroll speed Normal Normale
scroll speed Fast Rapide
Enhance camera image Améliorer la photo d'un document
Drag the corners onto the document's edges, then choose Enhance page. Faites glisser les coins sur les bords du document, puis choisissez Améliorer la page.
Enhance page Améliorer la page
Can't render this file because it contains an unexpected character in line 6 and column 39.
+4
View File
@@ -2199,6 +2199,10 @@ key Space Szóköz
Resize panel window Panelablak átméretezése
Tools Eszközök
Inspector Tulajdonságvizsgáló
Mouse wheel scroll speed Egérgörgő görgetési sebessége
scroll speed Slow Lassú
scroll speed Normal Normál
scroll speed Fast Gyors
Enhance camera image Kamerakép javítása
Drag the corners onto the document's edges, then choose Enhance page. Húzza a sarkokat a dokumentum széleire, majd válassza az Oldal javítása lehetőséget.
Enhance page Oldal javítása
Can't render this file because it contains an unexpected character in line 7 and column 44.
+4
View File
@@ -2208,6 +2208,10 @@ key Space Spazio
Panels Pannelli
Resize panels Ridimensiona pannelli
Resize panel window Ridimensiona finestra pannello
Mouse wheel scroll speed Velocità di scorrimento della rotellina del mouse
scroll speed Slow Lenta
scroll speed Normal Normale
scroll speed Fast Veloce
Enhance camera image Migliora foto della fotocamera
Drag the corners onto the document's edges, then choose Enhance page. Trascina gli angoli sui bordi del documento, poi scegli Migliora la pagina.
Enhance page Migliora la pagina
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -2201,6 +2201,10 @@ key Space スペース
Resize panel window パネルウィンドウのサイズを変更
Tools ツール
Inspector インスペクター
Mouse wheel scroll speed マウスホイールのスクロール速度
scroll speed Slow 遅い
scroll speed Normal 標準
scroll speed Fast 速い
Enhance camera image カメラ画像を補正
Drag the corners onto the document's edges, then choose Enhance page. 四隅を文書の端までドラッグしてから、「ページを補正」を選択します。
Enhance page ページを補正
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -2195,6 +2195,10 @@ key Space Spacja
Text used as a clipping path cannot be moved with this selection. Tekstu używanego jako ścieżka przycinania nie można przenieść razem z tym zaznaczeniem.
These objects could not be moved. Select them again and try a smaller move. Nie udało się przenieść tych obiektów. Zaznacz je ponownie i spróbuj mniejszego przesunięcia.
This page contains objects that cannot be selected together. Ta strona zawiera obiekty, których nie można zaznaczyć razem.
Mouse wheel scroll speed Szybkość przewijania kółkiem myszy
scroll speed Slow Wolna
scroll speed Normal Normalna
scroll speed Fast Szybka
Enhance camera image Popraw zdjęcie z aparatu
Drag the corners onto the document's edges, then choose Enhance page. Przeciągnij narożniki na krawędzie dokumentu, a następnie wybierz Popraw stronę.
Enhance page Popraw stronę
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -2202,6 +2202,10 @@ key Space Espaço
Resize panel window Redimensionar janela do painel
Tools Ferramentas
Inspector Inspetor
Mouse wheel scroll speed Velocidade de rolagem da roda do mouse
scroll speed Slow Lenta
scroll speed Normal Normal
scroll speed Fast Rápida
Enhance camera image Aprimorar foto da câmera
Drag the corners onto the document's edges, then choose Enhance page. Arraste os cantos até as bordas do documento e escolha Aprimorar página.
Enhance page Aprimorar página
Can't render this file because it contains an unexpected character in line 9 and column 16.
+4
View File
@@ -2114,6 +2114,10 @@ key Space Пробел
Resize panel window Изменить размер окна панели
Tools Инструменты
Inspector Инспектор
Mouse wheel scroll speed Скорость прокрутки колесом мыши
scroll speed Slow Низкая
scroll speed Normal Обычная
scroll speed Fast Высокая
Enhance camera image Улучшить снимок с камеры
Drag the corners onto the document's edges, then choose Enhance page. Перетащите углы к краям документа, затем выберите «Улучшить страницу».
Enhance page Улучшить страницу
Can't render this file because it contains an unexpected character in line 9 and column 16.
+4
View File
@@ -2130,6 +2130,10 @@ key Space స్పేస్
Resize panel window ప్యానెల్ విండో పరిమాణం మార్చు
Tools సాధనాలు
Inspector పరిశీలకుడు
Mouse wheel scroll speed మౌస్ వీల్ స్క్రోల్ వేగం
scroll speed Slow నెమ్మదిగా
scroll speed Normal సాధారణం
scroll speed Fast వేగంగా
Enhance camera image కెమెరా చిత్రాన్ని మెరుగుపరచు
Drag the corners onto the document's edges, then choose Enhance page. మూలలను పత్రం అంచుల వరకు లాగి, ఆపై పేజీని మెరుగుపరచు ఎంచుకోండి.
Enhance page పేజీని మెరుగుపరచు
Can't render this file because it contains an unexpected character in line 13 and column 16.
+4
View File
@@ -2222,6 +2222,10 @@ key Space Пробіл
Panels Панелі
Resize panels Змінити розмір панелей
Resize panel window Змінити розмір вікна панелі
Mouse wheel scroll speed Швидкість прокручування коліщатком миші
scroll speed Slow Повільна
scroll speed Normal Звичайна
scroll speed Fast Швидка
Enhance camera image Поліпшити знімок з камери
Drag the corners onto the document's edges, then choose Enhance page. Перетягніть кути до країв документа, потім виберіть «Поліпшити сторінку».
Enhance page Поліпшити сторінку
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -2165,6 +2165,10 @@ key Space 空格
Resize panel window 调整面板窗口大小
Tools 工具
Inspector 检查器
Mouse wheel scroll speed 鼠标滚轮滚动速度
scroll speed Slow 慢
scroll speed Normal 正常
scroll speed Fast 快
Enhance camera image 增强相机图像
Drag the corners onto the document's edges, then choose Enhance page. 将四个角拖到文档边缘,然后选择“增强页面”。
Enhance page 增强页面
Can't render this file because it contains an unexpected character in line 9 and column 16.
+4
View File
@@ -2086,6 +2086,10 @@ key Space 空格
Resize panel window 調整面板視窗大小
Tools 工具
Inspector 檢查器
Mouse wheel scroll speed 滑鼠滾輪捲動速度
scroll speed Slow 慢
scroll speed Normal 正常
scroll speed Fast 快
Table rows belong directly in a table or its head, body or foot. Fix the table's tags. 表格列應直接位於表格或其表頭、主體、表尾內。請修正表格標記。
Enhance camera image 增強相機影像
Drag the corners onto the document's edges, then choose Enhance page. 將四個角拖曳到文件邊緣,然後選擇「增強頁面」。
Can't render this file because it contains an unexpected character in line 9 and column 16.
+6
View File
@@ -322,6 +322,12 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok
// The web build has no file paths to reopen.
#[cfg(not(target_arch = "wasm32"))]
ui.checkbox(&mut app.reopen_last_session, tl!("Reopen the files that were open when PdfCraft last closed"));
ui.horizontal(|ui| {
ui.label(tl!("Mouse wheel scroll speed"));
for speed in crate::scroll_speed::ScrollSpeed::ALL {
ui.radio_value(&mut app.scroll_speed, speed, tl_ctx!("scroll speed", speed.label()));
}
});
ui.add_space(8.0);
// Identity: the author of new comments (Acrobat: Preferences ▸ Identity).
ui.label(egui::RichText::new(tl!("Identity")).font(theme::semibold(13.0)));
+15
View File
@@ -111,6 +111,7 @@ pub(crate) const MAX_AUTHOR_CHARS: usize = 200;
pub mod portable;
mod protect;
mod recovery;
pub mod scroll_speed;
#[cfg(not(target_arch = "wasm32"))]
mod system_fonts;
pub mod theme;
@@ -421,6 +422,8 @@ pub struct PdfCraftApp {
desktop_theme: pdfcraft_platform::desktop_theme::DesktopTheme,
/// The last answer [`desktop_theme`] gave, so a frame that asks costs nothing.
desktop_dark: Option<bool>,
/// How far a mouse-wheel notch scrolls.
pub scroll_speed: scroll_speed::ScrollSpeed,
/// Interface language preference: `auto` (follow the system) or a code from [`i18n::LANGUAGES`].
pub language: String,
/// Preferences ▸ Interface size as a factor, or `None` to follow [`Self::system_text_scale`].
@@ -734,6 +737,7 @@ impl PdfCraftApp {
theme_preference: ThemePreference::Light,
desktop_theme: pdfcraft_platform::desktop_theme::DesktopTheme::start(),
desktop_dark: None,
scroll_speed: scroll_speed::ScrollSpeed::default(),
language: i18n::AUTO.to_string(),
ui_scale: None,
system_text_scale: 1.0,
@@ -1443,6 +1447,7 @@ impl PdfCraftApp {
"last_session": self.reopen_last_session.then(|| self.session_to_save()),
"pinned_folders": self.pinned.folders,
"theme": self.theme_preference,
"scroll_speed": self.scroll_speed,
"default_mode": self.default_mode,
"default_layout": self.view_defaults.layout.as_str(),
"default_zoom": self.view_defaults.zoom_name(),
@@ -1520,6 +1525,9 @@ impl PdfCraftApp {
if let Ok(preference) = serde_json::from_value::<ThemePreference>(v["theme"].clone()) {
self.set_theme_preference(preference);
}
if let Ok(speed) = serde_json::from_value::<scroll_speed::ScrollSpeed>(v["scroll_speed"].clone()) {
self.scroll_speed = speed;
}
if let Ok(mode) = serde_json::from_value::<Mode>(v["default_mode"].clone()) {
self.default_mode = mode;
}
@@ -1638,6 +1646,9 @@ impl PdfCraftApp {
};
self.set_theme_preference(preference);
}
("scroll_speed", _) => {
self.scroll_speed = scroll_speed::ScrollSpeed::parse(value).ok_or("scroll_speed must be slow, normal, or fast")?;
}
("panel", _) => {
self.right = match value {
"comments" => Some(RightPanel::Comments),
@@ -1959,6 +1970,8 @@ impl eframe::App for PdfCraftApp {
/// would keep the place the pointer entered at: tell it where the pointer really is, so the
/// page grid can show (and use) the gap under it.
fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
// Before egui reads this frame's wheel input.
self.scroll_speed.apply(ctx, raw);
if raw.hovered_files.is_empty() && raw.dropped_files.is_empty() {
return;
}
@@ -1998,6 +2011,8 @@ impl eframe::App for PdfCraftApp {
ui_scale::sync(ctx, ui_scale::factor(self.ui_scale, self.system_text_scale), &mut self.ui_scale_applied);
// Remember user work even if its tab is subsequently closed or Home is selected.
self.startup_superseded |= !self.views.is_empty() || self.combine_showing() || self.dialog.is_some() || self.password_prompt.is_some();
// Also done in `raw_input_hook`, before input; here for hosts that don't call the hook.
self.scroll_speed.set_line_speed(ctx);
// Before taking this frame's drop: the grid must be drawn once with the pointer where
// the files were let go before the gap is read.
self.finish_grid_drop(ctx);
+119
View File
@@ -0,0 +1,119 @@
//! How far a mouse-wheel notch scrolls (#444, #186).
//!
//! egui scrolls `line_scroll_speed` points per wheel line: 40 on the desktop, 8 on the web.
//! macOS already accelerates line deltas, and browsers there scroll about that far per line,
//! but on Windows and Linux winit reports one line per notch, where browsers scroll about
//! 100 pixels, so 40 felt slow. Wheel input reported in points (most trackpads) keeps its own
//! speed, ⌘/Ctrl-wheel zoom keeps egui's rate, and wheel paging in Single page view still turns
//! one page per notch.
/// The user's choice, relative to the platform's normal speed.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ScrollSpeed {
Slow,
#[default]
Normal,
Fast,
}
/// egui's own `line_scroll_speed`, which its wheel zoom rate is tuned against.
const EGUI_LINE_POINTS: f32 = if cfg!(target_arch = "wasm32") { 8.0 } else { 40.0 };
/// Points one wheel line scrolls at Normal speed.
const NORMAL_LINE_POINTS: f32 = if cfg!(target_arch = "wasm32") {
// The browser's line deltas, which egui already scales for the web.
8.0
} else if cfg!(target_os = "macos") {
40.0
} else {
100.0
};
impl ScrollSpeed {
pub const ALL: [Self; 3] = [Self::Slow, Self::Normal, Self::Fast];
pub fn label(self) -> &'static str {
match self {
Self::Slow => "Slow",
Self::Normal => "Normal",
Self::Fast => "Fast",
}
}
/// Points one wheel line scrolls.
pub fn line_points(self) -> f32 {
NORMAL_LINE_POINTS
* match self {
Self::Slow => 0.5,
Self::Normal => 1.0,
Self::Fast => 2.0,
}
}
/// Makes egui scroll this far per wheel line, from this frame's input on (call it before egui
/// reads `raw`). egui scales line deltas before turning ⌘/Ctrl-wheel motion into zoom, so
/// those events are scaled back to keep zooming at egui's own rate.
pub(crate) fn apply(self, ctx: &egui::Context, raw: &mut egui::RawInput) {
self.set_line_speed(ctx);
let points = self.line_points();
let zoom_scale = EGUI_LINE_POINTS / points;
for event in &mut raw.events {
if let egui::Event::MouseWheel { unit: egui::MouseWheelUnit::Line, delta, modifiers, .. } = event
&& modifiers.command
{
*delta *= zoom_scale;
}
}
}
/// Makes egui scroll this far per wheel line (from the next frame when called during one).
pub(crate) fn set_line_speed(self, ctx: &egui::Context) {
let points = self.line_points();
if ctx.options(|o| o.input_options.line_scroll_speed) != points {
ctx.options_mut(|o| o.input_options.line_scroll_speed = points);
}
}
pub fn parse(value: &str) -> Option<Self> {
Self::ALL.into_iter().find(|s| s.label().eq_ignore_ascii_case(value))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn wheel(unit: egui::MouseWheelUnit, dy: f32, command: bool) -> egui::Event {
let modifiers = egui::Modifiers { command, ..egui::Modifiers::NONE };
egui::Event::MouseWheel { unit, delta: egui::vec2(0.0, dy), phase: egui::TouchPhase::Move, modifiers }
}
#[test]
fn zoom_keeps_egui_rate_and_scrolling_follows_the_choice() {
let ctx = egui::Context::default();
for speed in ScrollSpeed::ALL {
let mut raw = egui::RawInput {
events: vec![
wheel(egui::MouseWheelUnit::Line, 1.0, true),
wheel(egui::MouseWheelUnit::Line, 1.0, false),
wheel(egui::MouseWheelUnit::Point, 30.0, true),
],
..Default::default()
};
speed.apply(&ctx, &mut raw);
assert_eq!(ctx.options(|o| o.input_options.line_scroll_speed), speed.line_points());
let dy: Vec<f32> =
raw.events.iter().filter_map(|e| if let egui::Event::MouseWheel { delta, .. } = e { Some(delta.y) } else { None }).collect();
// ⌘/Ctrl lines scaled back so egui's zoom sees EGUI_LINE_POINTS per line; plain lines
// and point deltas untouched.
assert!((dy[0] * speed.line_points() - EGUI_LINE_POINTS).abs() < 1e-3, "{speed:?}: {dy:?}");
assert_eq!((dy[1], dy[2]), (1.0, 30.0));
}
assert_eq!(ScrollSpeed::parse("FAST"), Some(ScrollSpeed::Fast));
assert_eq!(ScrollSpeed::parse("warp"), None);
// The labels are translated in their own context: they qualify "speed".
let ru = crate::i18n::Lang::from_code("ru").expect("Russian ships");
assert_eq!(crate::i18n::tr_ctx(ru, "scroll speed", ScrollSpeed::Slow.label()), "Низкая");
}
}
+4 -3
View File
@@ -11,9 +11,10 @@
use egui::{MouseWheelUnit, TouchPhase};
/// How far one wheel notch scrolls, in points (egui's native `line_scroll_speed`). Smooth
/// input turns a page once it has moved this far, so a light swipe turns and resting fingers
/// don't; smooth line deltas count this many points per line.
/// One wheel notch, in points (egui's default desktop `line_scroll_speed`, fixed so paging
/// doesn't change with the scroll speed preference). Smooth input turns a page once it has
/// moved this far, so a light swipe turns and resting fingers don't; smooth line deltas count
/// this many points per line.
const NOTCH_POINTS: f32 = 40.0;
/// A phase-less point delta at least this big is a wheel notch: egui draws the same line
/// between wheel steps, which it smooths, and trackpad input, which it passes through.
+34
View File
@@ -794,3 +794,37 @@ fn pages_panel_thumbnails_have_a_context_menu() {
h.run_steps(3);
assert_eq!(h.state().page_clipboard.as_ref().map(|c| c.pages.clone()), Some(vec![2]));
}
/// #444, #186: a mouse-wheel notch scrolls as far as the scroll speed preference says;
/// trackpad (point) scrolling keeps its own speed. The choice is saved.
#[test]
fn the_scroll_speed_preference_scales_mouse_wheel_notches() {
use pdfcraft_ui_egui::scroll_speed::ScrollSpeed;
let scrolled = |speed: ScrollSpeed, unit: egui::MouseWheelUnit, dy: f32| -> f32 {
let mut h = harness();
h.state_mut().scroll_speed = speed;
h.run_steps(2);
let i = h.state().active.expect("a document is shown");
let top = |h: &Harness<'static, PdfCraftApp>| h.state().views[i].page_screen_rect(0).expect("page 1 on screen").min.y;
let before = top(&h);
h.event(egui::Event::PointerMoved(h.state().views[i].viewport_rect().center()));
h.event(egui::Event::MouseWheel { unit, delta: egui::vec2(0.0, -dy), phase: egui::TouchPhase::Move, modifiers: egui::Modifiers::NONE });
// egui spreads a wheel notch over a few frames.
h.run_steps(30);
before - top(&h)
};
let line = egui::MouseWheelUnit::Line;
let normal = scrolled(ScrollSpeed::Normal, line, 1.0);
assert!((normal - ScrollSpeed::Normal.line_points()).abs() < 1.0, "one notch at Normal scrolled {normal}");
let (slow, fast) = (scrolled(ScrollSpeed::Slow, line, 1.0), scrolled(ScrollSpeed::Fast, line, 1.0));
assert!((slow * 2.0 - normal).abs() < 1.0 && (fast - normal * 2.0).abs() < 1.0, "slow {slow}, normal {normal}, fast {fast}");
let point = egui::MouseWheelUnit::Point;
let (normal_points, fast_points) = (scrolled(ScrollSpeed::Normal, point, 30.0), scrolled(ScrollSpeed::Fast, point, 30.0));
assert!(normal_points > 0.0 && (normal_points - fast_points).abs() < 1.0, "trackpad: {normal_points} vs {fast_points}");
let mut app = PdfCraftApp::new();
app.set_option("scroll_speed", "fast").unwrap();
assert!(app.set_option("scroll_speed", "warp").unwrap_err().contains("slow, normal, or fast"));
let mut again = PdfCraftApp::new();
again.restore(&app.persist());
assert_eq!(again.scroll_speed, ScrollSpeed::Fast);
}