From 5d301305c97779b92ebac9d1e89cf30aec52d404 Mon Sep 17 00:00:00 2001 From: Hal Goff Date: Sun, 11 Oct 2026 18:05:53 +0100 Subject: [PATCH] Issues #444, #186: a mouse wheel scroll speed preference, faster by default on Windows and Linux (#514) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Co-authored-by: Brandon Thomas --- ATTRIBUTION.toml | 30 +++---- crates/ui-egui/src/i18n/ar.tsv | 4 + crates/ui-egui/src/i18n/bg.tsv | 4 + crates/ui-egui/src/i18n/de.tsv | 4 + crates/ui-egui/src/i18n/es.tsv | 4 + crates/ui-egui/src/i18n/fr.tsv | 4 + crates/ui-egui/src/i18n/hu.tsv | 4 + crates/ui-egui/src/i18n/it.tsv | 4 + crates/ui-egui/src/i18n/ja.tsv | 4 + crates/ui-egui/src/i18n/pl.tsv | 4 + crates/ui-egui/src/i18n/pt-br.tsv | 4 + crates/ui-egui/src/i18n/ru.tsv | 4 + crates/ui-egui/src/i18n/te.tsv | 4 + crates/ui-egui/src/i18n/uk.tsv | 4 + crates/ui-egui/src/i18n/zh-hans.tsv | 4 + crates/ui-egui/src/i18n/zh-hant.tsv | 4 + crates/ui-egui/src/js_ui.rs | 6 ++ crates/ui-egui/src/lib.rs | 15 ++++ crates/ui-egui/src/scroll_speed.rs | 119 ++++++++++++++++++++++++++++ crates/ui-egui/src/wheel_pager.rs | 7 +- crates/ui-egui/tests/viewer.rs | 34 ++++++++ 21 files changed, 253 insertions(+), 18 deletions(-) create mode 100644 crates/ui-egui/src/scroll_speed.rs diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 7835c72ed..723a36e21 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -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" diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index 26c88bdb0..bf23b8f7c 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -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 تحسين الصفحة diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 8cb3d9a88..a1501789d 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -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 Подобри страницата diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 46e4f5164..cc8b954d9 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index fd5d62958..74bcde928 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index c334375d2..ab1aa5e12 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 03e51c58c..c7f4456aa 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 774756dfd..8e29b80a8 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index f8dead105..31a33d33b 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -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 ページを補正 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index e4b574c04..407282647 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -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ę diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index a3b7e9d07..59290d0a4 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 4358eb3fc..448c0f96b 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -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 Улучшить страницу diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index 8eefeceaf..16d1de389 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -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 పేజీని మెరుగుపరచు diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 018a6ee73..779fc7327 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -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 Поліпшити сторінку diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index b6f83a99b..7bc74bee9 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -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 增强页面 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index b1a975608..d22baa78a 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -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. 將四個角拖曳到文件邊緣,然後選擇「增強頁面」。 diff --git a/crates/ui-egui/src/js_ui.rs b/crates/ui-egui/src/js_ui.rs index 9f5ffdfa1..545c3b013 100644 --- a/crates/ui-egui/src/js_ui.rs +++ b/crates/ui-egui/src/js_ui.rs @@ -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))); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 2b9fd04f7..77145db54 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -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, + /// 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::(v["theme"].clone()) { self.set_theme_preference(preference); } + if let Ok(speed) = serde_json::from_value::(v["scroll_speed"].clone()) { + self.scroll_speed = speed; + } if let Ok(mode) = serde_json::from_value::(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); diff --git a/crates/ui-egui/src/scroll_speed.rs b/crates/ui-egui/src/scroll_speed.rs new file mode 100644 index 000000000..af8d0fe33 --- /dev/null +++ b/crates/ui-egui/src/scroll_speed.rs @@ -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::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 = + 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()), "Низкая"); + } +} diff --git a/crates/ui-egui/src/wheel_pager.rs b/crates/ui-egui/src/wheel_pager.rs index 423ac1d0d..a05af23bf 100644 --- a/crates/ui-egui/src/wheel_pager.rs +++ b/crates/ui-egui/src/wheel_pager.rs @@ -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. diff --git a/crates/ui-egui/tests/viewer.rs b/crates/ui-egui/tests/viewer.rs index 22eaa744c..6fdcea470 100644 --- a/crates/ui-egui/tests/viewer.rs +++ b/crates/ui-egui/tests/viewer.rs @@ -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); +}