diff --git a/src/Canvas.cpp b/src/Canvas.cpp index 4d7a7dfb7c1..4155e246052 100644 --- a/src/Canvas.cpp +++ b/src/Canvas.cpp @@ -61,13 +61,10 @@ bool gNoFlickerRender = true; Kind kNotifAnnotation = "notifAnnotation"; -// Timer for mouse wheel smooth scrolling -constexpr UINT_PTR kSmoothScrollTimerID = 6; - // Smooth scrolling factor. This is a value between 0 and 1. // Each step, we scroll the needed delta times this factor. // Therefore, a higher factor makes smooth scrolling faster. -static const double gSmoothScrollingFactor = 0.2; +static const double gSmoothScrollingFactor = 0.3; // these can be global, as the mouse wheel can't affect more than one window at once static int gDeltaPerLine = 0; @@ -151,7 +148,7 @@ static void OnVScroll(MainWindow* win, WPARAM wp) { if (si.nPos != currPos || msg == SB_THUMBTRACK) { if (gGlobalPrefs->smoothScroll) { win->scrollTargetY = si.nPos; - SetTimer(win->hwndCanvas, kSmoothScrollTimerID, USER_TIMER_MINIMUM, nullptr); + SetEvent(win->scrollTimer); } else { win->AsFixed()->ScrollYTo(si.nPos); } @@ -392,8 +389,8 @@ static void OnMouseMove(MainWindow* win, int x, int y, WPARAM) { case MouseAction::Scrolling: { win->annotationUnderCursor = nullptr; - win->yScrollSpeed = (y - win->dragStart.y) / SMOOTHSCROLL_SLOW_DOWN_FACTOR; - win->xScrollSpeed = (x - win->dragStart.x) / SMOOTHSCROLL_SLOW_DOWN_FACTOR; + win->yScrollSpeed = (y - win->dragStart.y) * SMOOTHSCROLL_FACTOR; + win->xScrollSpeed = (x - win->dragStart.x) * SMOOTHSCROLL_FACTOR; break; } case MouseAction::SelectingText: @@ -1211,7 +1208,7 @@ static void ZoomByMouseWheel(MainWindow* win, WPARAM wp) { win->dragStartPending = false; // Kill the smooth scroll timer when zooming // We don't want to move to the new updated y offset after zooming - KillTimer(win->hwndCanvas, kSmoothScrollTimerID); + ResetEvent(win->scrollTimer); short delta = GET_WHEEL_DELTA_WPARAM(wp); Point pt = HwndGetCursorPos(win->hwndCanvas); @@ -1639,7 +1636,7 @@ static LRESULT WndProcCanvasFixedPageUI(MainWindow* win, HWND hwnd, UINT msg, WP return 0; case WM_MBUTTONDOWN: - SetTimer(hwnd, SMOOTHSCROLL_TIMER_ID, SMOOTHSCROLL_DELAY_IN_MS, nullptr); + SetEvent(win->smoothscrollTimer); // TODO: Create window that shows location of initial click for reference OnMouseMiddleButtonDown(win, x, y, wp); return 0; @@ -1819,14 +1816,15 @@ static void OnTimer(MainWindow* win, HWND hwnd, WPARAM timerId) { case SMOOTHSCROLL_TIMER_ID: if (MouseAction::Scrolling == win->mouseAction) { - win->MoveDocBy(win->xScrollSpeed, win->yScrollSpeed); + double factor = win->scrollTimerDeltaTime / 1000.0; + win->MoveDocBy((int)(win->xScrollSpeed * factor), (int)(win->yScrollSpeed * factor)); } else if (MouseAction::Selecting == win->mouseAction || MouseAction::SelectingText == win->mouseAction) { pt = HwndGetCursorPos(win->hwndCanvas); if (NeedsSelectionEdgeAutoscroll(win, pt.x, pt.y)) { OnMouseMove(win, pt.x, pt.y, MK_CONTROL); } } else { - KillTimer(hwnd, SMOOTHSCROLL_TIMER_ID); + ResetEvent(win->smoothscrollTimer); win->yScrollSpeed = 0; win->xScrollSpeed = 0; } @@ -1875,11 +1873,12 @@ static void OnTimer(MainWindow* win, HWND hwnd, WPARAM timerId) { int delta = target - current; if (delta == 0) { - KillTimer(hwnd, kSmoothScrollTimerID); + ResetEvent(win->scrollTimer); } else { // logf("Smooth scrolling from %d to %d (delta %d)\n", current, target, delta); - double step = delta * gSmoothScrollingFactor; + double a = pow(1.0 - gSmoothScrollingFactor, 60 * win->scrollTimerDeltaTime / 1000.0); + double step = delta * (1.0 - a); // Round away from zero int dy = step < 0 ? (int)floor(step) : (int)ceil(step); diff --git a/src/Canvas.h b/src/Canvas.h index 08c8b525360..52b15a9f2a1 100644 --- a/src/Canvas.h +++ b/src/Canvas.h @@ -1,6 +1,9 @@ /* Copyright 2022 the SumatraPDF project authors (see AUTHORS file). License: GPLv3 */ +// Timer for mouse wheel smooth scrolling +constexpr UINT_PTR kSmoothScrollTimerID = 6; + void UpdateDeltaPerLine(); LRESULT CALLBACK WndProcCanvas(HWND, UINT, WPARAM, LPARAM); diff --git a/src/MainWindow.h b/src/MainWindow.h index e32ac1c0b20..d8f3e3cbb6f 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -212,6 +212,15 @@ struct MainWindow { DocControllerCallback* cbHandler = nullptr; + // Seprate scroll thread for higher precision scrolling updates + HANDLE scrollTimerThread = nullptr; + bool scrollTimerCancelled = false; + // Scroll thread delta time (e.g. frame time) in ms + int scrollTimerDeltaTime = 0; + + HANDLE scrollTimer = nullptr; + HANDLE smoothscrollTimer = nullptr; + // The target y offset for smooth scrolling. // We use a timer to gradually scroll there. int scrollTargetY = 0; diff --git a/src/Selection.cpp b/src/Selection.cpp index 59b6cda384a..e4c64cd9030 100644 --- a/src/Selection.cpp +++ b/src/Selection.cpp @@ -396,7 +396,7 @@ void OnSelectionStart(MainWindow* win, int x, int y, WPARAM) { } SetCapture(win->hwndCanvas); - SetTimer(win->hwndCanvas, SMOOTHSCROLL_TIMER_ID, SMOOTHSCROLL_DELAY_IN_MS, nullptr); + SetEvent(win->smoothscrollTimer); ScheduleRepaint(win, 0); } @@ -404,7 +404,7 @@ void OnSelectionStop(MainWindow* win, int x, int y, bool aborted) { if (GetCapture() == win->hwndCanvas) { ReleaseCapture(); } - KillTimer(win->hwndCanvas, SMOOTHSCROLL_TIMER_ID); + ResetEvent(win->smoothscrollTimer); // update the text selection before changing the selectionRect if (MouseAction::SelectingText == win->mouseAction) { diff --git a/src/Selection.h b/src/Selection.h index caa248eda9a..e43b35ffd37 100644 --- a/src/Selection.h +++ b/src/Selection.h @@ -2,8 +2,7 @@ License: GPLv3 */ #define SMOOTHSCROLL_TIMER_ID 2 -#define SMOOTHSCROLL_DELAY_IN_MS 20 -#define SMOOTHSCROLL_SLOW_DOWN_FACTOR 10 +#define SMOOTHSCROLL_FACTOR 10 /* Represents selected area on given page */ struct SelectionOnPage { diff --git a/src/SumatraPDF.cpp b/src/SumatraPDF.cpp index 6ec0c460b37..60de9d253fa 100644 --- a/src/SumatraPDF.cpp +++ b/src/SumatraPDF.cpp @@ -1464,6 +1464,50 @@ static void UpdateToolbarSidebarText(MainWindow* win) { win->favLabelWithClose->SetLabel(_TRA("Favorites")); } +static void ScrollTimerThread(MainWindow* win) { + std::array timers = { + win->scrollTimer, + win->smoothscrollTimer, + }; + + std::array ids = { + kSmoothScrollTimerID, + SMOOTHSCROLL_TIMER_ID, + }; + + while (WaitForMultipleObjects(timers.size(), timers.data(), false, INFINITE) <= WAIT_OBJECT_0 + timers.size()) { + if (win->scrollTimerCancelled) { + break; + } + + for (int i = 0; i < timers.size(); i++) { + if (WaitForSingleObject(timers[i], 0) == WAIT_OBJECT_0) + SendMessage(win->hwndCanvas, WM_TIMER, ids[i], 0); + } + + // Re-fetch the monitor refresh rate in case window is moved to + // new window, or monitor refresh rate changed. Default to 60fps if + // anything goes wrong. + + win->scrollTimerDeltaTime = 1000 / 60; + + HMONITOR monitor = MonitorFromWindow(win->hwndCanvas, MONITOR_DEFAULTTOPRIMARY); + MONITORINFOEX info; + info.cbSize = sizeof(MONITORINFOEX); + if (GetMonitorInfo(monitor, &info)) { + DEVMODE mode; + mode.dmSize = sizeof(DEVMODE); + mode.dmDriverExtra = 0; + + if (EnumDisplaySettings(info.szDevice, ENUM_CURRENT_SETTINGS, &mode) && mode.dmDisplayFrequency > 0) { + win->scrollTimerDeltaTime = 1000 / mode.dmDisplayFrequency; + } + } + + Sleep(win->scrollTimerDeltaTime); + } +} + static MainWindow* CreateMainWindow() { Rect windowPos = gGlobalPrefs->windowPos; if (!windowPos.IsEmpty()) { @@ -1513,6 +1557,11 @@ static MainWindow* CreateMainWindow() { // hide scrollbars to avoid showing/hiding on empty window ShowScrollBar(win->hwndCanvas, SB_BOTH, FALSE); + win->scrollTimer = CreateEvent(nullptr, true, false, nullptr); + win->smoothscrollTimer = CreateEvent(nullptr, true, false, nullptr); + Func0 fn = MkFunc0(ScrollTimerThread, win); + win->scrollTimerThread = StartThread(fn, "Scroll Thread"); + ReportIf(win->menu); win->menu = BuildMenu(win); win->isMenuHidden = !gGlobalPrefs->showMenubar; @@ -2728,6 +2777,12 @@ void CloseWindow(MainWindow* win, bool quitIfLast, bool forceClose) { AbortFinding(win, true); AbortPrinting(win); + if (win->scrollTimerThread) { + win->scrollTimerCancelled = true; + SetEvent(win->scrollTimer); + WaitForSingleObject(win->scrollTimerThread, INFINITE); + } + win->scrollTimerCancelled = false; for (auto& tab : win->Tabs()) { if (tab->AsFixed()) {