Skip to content

Commit 87caf27

Browse files
committed
Inputs, Scrolling: better selection of scrolling window when hovering nested windows and backend/OS is emitting dual-axis wheeling inputs. (#3795, #4559)
1 parent a5f3596 commit 87caf27

File tree

4 files changed

+95
-34
lines changed

4 files changed

+95
-34
lines changed

docs/CHANGELOG.txt

+4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ Breaking changes:
3939

4040
Other changes:
4141

42+
- Inputs, Scrolling: better selection of scrolling window when hovering nested windows
43+
and when backend/OS is emitting dual-axis wheeling inputs (typically touch pads on macOS).
44+
We now select a primary axis based on recent events, and select a target window based on it.
45+
We expect this behavior to be further improved/tweaked. (#3795, #4559) [@ocornut, @folays]
4246
- Text: fixed layouting of wrapped-text block when the last source line is above the
4347
clipping region. Regression added in 1.89. (#5720, #5919)
4448
- Misc: fixed parameters to IMGUI_DEBUG_LOG() not being dead-stripped when building

imgui.cpp

+85-33
Original file line numberDiff line numberDiff line change
@@ -4367,13 +4367,58 @@ static void LockWheelingWindow(ImGuiWindow* window, float wheel_amount)
43674367
IMGUI_DEBUG_LOG_IO("LockWheelingWindow() \"%s\"\n", window ? window->Name : "NULL");
43684368
g.WheelingWindow = window;
43694369
g.WheelingWindowRefMousePos = g.IO.MousePos;
4370+
if (window == NULL)
4371+
{
4372+
g.WheelingWindowStartFrame = -1;
4373+
g.WheelingAxisAvg = ImVec2(0.0f, 0.0f);
4374+
}
4375+
}
4376+
4377+
static ImGuiWindow* FindBestWheelingWindow(const ImVec2& wheel)
4378+
{
4379+
// For each axis, find window in the hierarchy that may want to use scrolling
4380+
ImGuiContext& g = *GImGui;
4381+
ImGuiWindow* windows[2] = { NULL, NULL };
4382+
for (int axis = 0; axis < 2; axis++)
4383+
if (wheel[axis] != 0.0f)
4384+
for (ImGuiWindow* window = windows[axis] = g.HoveredWindow; window->Flags & ImGuiWindowFlags_ChildWindow; window = windows[axis] = window->ParentWindow)
4385+
{
4386+
// Bubble up into parent window if:
4387+
// - a child window doesn't allow any scrolling.
4388+
// - a child window has the ImGuiWindowFlags_NoScrollWithMouse flag.
4389+
//// - a child window doesn't need scrolling because it is already at the edge for the direction we are going in (FIXME-WIP)
4390+
const bool has_scrolling = (window->ScrollMax[axis] != 0.0f);
4391+
const bool inputs_disabled = (window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs);
4392+
//const bool scrolling_past_limits = (wheel_v < 0.0f) ? (window->Scroll[axis] <= 0.0f) : (window->Scroll[axis] >= window->ScrollMax[axis]);
4393+
if (has_scrolling && !inputs_disabled) // && !scrolling_past_limits)
4394+
break; // select this window
4395+
}
4396+
if (windows[0] == NULL && windows[1] == NULL)
4397+
return NULL;
4398+
4399+
// If there's only one window or only one axis then there's no ambiguity
4400+
if (windows[0] == windows[1] || windows[0] == NULL || windows[1] == NULL)
4401+
return windows[1] ? windows[1] : windows[0];
4402+
4403+
// If candidate are different windows we need to decide which one to prioritize
4404+
// - First frame: only find a winner if one axis is zero.
4405+
// - Subsequent frames: only find a winner when one is more than the other.
4406+
if (g.WheelingWindowStartFrame == -1)
4407+
g.WheelingWindowStartFrame = g.FrameCount;
4408+
if ((g.WheelingWindowStartFrame == g.FrameCount && wheel.x != 0.0f && wheel.y != 0.0f) || (g.WheelingAxisAvg.x == g.WheelingAxisAvg.y))
4409+
{
4410+
g.WheelingWindowWheelRemainder = wheel;
4411+
return NULL;
4412+
}
4413+
return (g.WheelingAxisAvg.x > g.WheelingAxisAvg.y) ? windows[0] : windows[1];
43704414
}
43714415

43724416
void ImGui::UpdateMouseWheel()
43734417
{
43744418
ImGuiContext& g = *GImGui;
43754419

4376-
// Reset the locked window if we move the mouse or after the timer elapses
4420+
// Reset the locked window if we move the mouse or after the timer elapses.
4421+
// FIXME: Ideally we could refactor to have one timer for "changing window w/ same axis" and a shorter timer for "changing window or axis w/ other axis" (#3795)
43774422
if (g.WheelingWindow != NULL)
43784423
{
43794424
g.WheelingWindowReleaseTimer -= g.IO.DeltaTime;
@@ -4386,8 +4431,6 @@ void ImGui::UpdateMouseWheel()
43864431
ImVec2 wheel;
43874432
wheel.x = TestKeyOwner(ImGuiKey_MouseWheelX, ImGuiKeyOwner_None) ? g.IO.MouseWheelH : 0.0f;
43884433
wheel.y = TestKeyOwner(ImGuiKey_MouseWheelY, ImGuiKeyOwner_None) ? g.IO.MouseWheel : 0.0f;
4389-
if (wheel.x == 0.0f && wheel.y == 0.0f)
4390-
return;
43914434

43924435
//IMGUI_DEBUG_LOG("MouseWheel X:%.3f Y:%.3f\n", wheel_x, wheel_y);
43934436
ImGuiWindow* mouse_window = g.WheelingWindow ? g.WheelingWindow : g.HoveredWindow;
@@ -4425,39 +4468,41 @@ void ImGui::UpdateMouseWheel()
44254468
wheel.y = 0.0f;
44264469
}
44274470

4428-
// Vertical Mouse Wheel scrolling
4429-
// Bubble up into parent window if:
4430-
// - a child window doesn't allow any scrolling.
4431-
// - a child window doesn't need scrolling because it is already at the edge for the direction we are going in.
4432-
// - a child window has the ImGuiWindowFlags_NoScrollWithMouse flag.
4433-
if (wheel.y != 0.0f)
4434-
{
4435-
ImGuiWindow* window = mouse_window;
4436-
while ((window->Flags & ImGuiWindowFlags_ChildWindow) && ((window->ScrollMax.y == 0.0f) || ((window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs))))
4437-
window = window->ParentWindow;
4438-
if (!(window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs))
4439-
{
4440-
LockWheelingWindow(mouse_window, wheel.y);
4441-
float max_step = window->InnerRect.GetHeight() * 0.67f;
4442-
float scroll_step = ImFloor(ImMin(5 * window->CalcFontSize(), max_step));
4443-
SetScrollY(window, window->Scroll.y - wheel.y * scroll_step);
4444-
}
4445-
}
4471+
// Maintain a rough average of moving magnitude on both axises
4472+
// FIXME: should by based on wall clock time rather than frame-counter
4473+
g.WheelingAxisAvg.x = ImExponentialMovingAverage(g.WheelingAxisAvg.x, ImAbs(wheel.x), 30);
4474+
g.WheelingAxisAvg.y = ImExponentialMovingAverage(g.WheelingAxisAvg.y, ImAbs(wheel.y), 30);
44464475

4447-
// Horizontal Mouse Wheel scrolling, or Vertical Mouse Wheel w/ Shift held
4448-
if (wheel.x != 0.0f)
4449-
{
4450-
ImGuiWindow* window = mouse_window;
4451-
while ((window->Flags & ImGuiWindowFlags_ChildWindow) && ((window->ScrollMax.x == 0.0f) || ((window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs))))
4452-
window = window->ParentWindow;
4476+
// In the rare situation where FindBestWheelingWindow() had to defer first frame of wheeling due to ambiguous main axis, reinject it now.
4477+
wheel += g.WheelingWindowWheelRemainder;
4478+
g.WheelingWindowWheelRemainder = ImVec2(0.0f, 0.0f);
4479+
if (wheel.x == 0.0f && wheel.y == 0.0f)
4480+
return;
4481+
4482+
// Mouse wheel scrolling: find target and apply
4483+
// - don't renew lock if axis doesn't apply on the window.
4484+
// - select a main axis when both axises are being moved.
4485+
if (ImGuiWindow* window = (g.WheelingWindow ? g.WheelingWindow : FindBestWheelingWindow(wheel)))
44534486
if (!(window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs))
44544487
{
4455-
LockWheelingWindow(mouse_window, wheel.x);
4456-
float max_step = window->InnerRect.GetWidth() * 0.67f;
4457-
float scroll_step = ImFloor(ImMin(2 * window->CalcFontSize(), max_step));
4458-
SetScrollX(window, window->Scroll.x - wheel.x * scroll_step);
4488+
bool do_scroll[2] = { wheel.x != 0.0f && window->ScrollMax.x != 0.0f, wheel.y != 0.0f && window->ScrollMax.y != 0.0f };
4489+
if (do_scroll[ImGuiAxis_X] && do_scroll[ImGuiAxis_Y])
4490+
do_scroll[(g.WheelingAxisAvg.x > g.WheelingAxisAvg.y) ? ImGuiAxis_Y : ImGuiAxis_X] = false;
4491+
if (do_scroll[ImGuiAxis_X])
4492+
{
4493+
LockWheelingWindow(window, wheel.x);
4494+
float max_step = window->InnerRect.GetWidth() * 0.67f;
4495+
float scroll_step = ImFloor(ImMin(2 * window->CalcFontSize(), max_step));
4496+
SetScrollX(window, window->Scroll.x - wheel.x * scroll_step);
4497+
}
4498+
if (do_scroll[ImGuiAxis_Y])
4499+
{
4500+
LockWheelingWindow(window, wheel.y);
4501+
float max_step = window->InnerRect.GetHeight() * 0.67f;
4502+
float scroll_step = ImFloor(ImMin(5 * window->CalcFontSize(), max_step));
4503+
SetScrollY(window, window->Scroll.y - wheel.y * scroll_step);
4504+
}
44594505
}
4460-
}
44614506
}
44624507

44634508
// The reason this is exposed in imgui_internal.h is: on touch-based system that don't have hovering, we want to dispatch inputs to the right target (imgui vs imgui+app)
@@ -8427,7 +8472,7 @@ static void DebugPrintInputEvent(const char* prefix, const ImGuiInputEvent* e)
84278472
ImGuiContext& g = *GImGui;
84288473
if (e->Type == ImGuiInputEventType_MousePos) { if (e->MousePos.PosX == -FLT_MAX && e->MousePos.PosY == -FLT_MAX) IMGUI_DEBUG_LOG_IO("%s: MousePos (-FLT_MAX, -FLT_MAX)\n", prefix); else IMGUI_DEBUG_LOG_IO("%s: MousePos (%.1f, %.1f)\n", prefix, e->MousePos.PosX, e->MousePos.PosY); return; }
84298474
if (e->Type == ImGuiInputEventType_MouseButton) { IMGUI_DEBUG_LOG_IO("%s: MouseButton %d %s\n", prefix, e->MouseButton.Button, e->MouseButton.Down ? "Down" : "Up"); return; }
8430-
if (e->Type == ImGuiInputEventType_MouseWheel) { IMGUI_DEBUG_LOG_IO("%s: MouseWheel (%.1f, %.1f)\n", prefix, e->MouseWheel.WheelX, e->MouseWheel.WheelY); return; }
8475+
if (e->Type == ImGuiInputEventType_MouseWheel) { IMGUI_DEBUG_LOG_IO("%s: MouseWheel (%.3f, %.3f)\n", prefix, e->MouseWheel.WheelX, e->MouseWheel.WheelY); return; }
84318476
if (e->Type == ImGuiInputEventType_Key) { IMGUI_DEBUG_LOG_IO("%s: Key \"%s\" %s\n", prefix, ImGui::GetKeyName(e->Key.Key), e->Key.Down ? "Down" : "Up"); return; }
84328477
if (e->Type == ImGuiInputEventType_Text) { IMGUI_DEBUG_LOG_IO("%s: Text: %c (U+%08X)\n", prefix, e->Text.Char, e->Text.Char); return; }
84338478
if (e->Type == ImGuiInputEventType_Focus) { IMGUI_DEBUG_LOG_IO("%s: AppFocused %d\n", prefix, e->AppFocused.Focused); return; }
@@ -13409,6 +13454,13 @@ void ImGui::ShowMetricsWindow(bool* p_open)
1340913454
Text("NavWindowingTarget: '%s'", g.NavWindowingTarget ? g.NavWindowingTarget->Name : "NULL");
1341013455
Unindent();
1341113456

13457+
Text("MOUSE WHEELING");
13458+
Indent();
13459+
Text("WheelingWindow: '%s'", g.WheelingWindow ? g.WheelingWindow->Name : "NULL");
13460+
Text("WheelingWindowReleaseTimer: %.2f", g.WheelingWindowReleaseTimer);
13461+
Text("WheelingAxisAvg[] = { %.3f, %.3f }, Main Axis: %s", g.WheelingAxisAvg.x, g.WheelingAxisAvg.y, (g.WheelingAxisAvg.x > g.WheelingAxisAvg.y) ? "X" : (g.WheelingAxisAvg.x < g.WheelingAxisAvg.y) ? "Y" : "<none>");
13462+
Unindent();
13463+
1341213464
TreePop();
1341313465
}
1341413466

imgui.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
// Library Version
2424
// (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM > 12345')
2525
#define IMGUI_VERSION "1.89.2 WIP"
26-
#define IMGUI_VERSION_NUM 18912
26+
#define IMGUI_VERSION_NUM 18913
2727
#define IMGUI_HAS_TABLE
2828

2929
/*

imgui_internal.h

+5
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ static inline ImVec2 ImRotate(const ImVec2& v, float cos_a, float sin_a)
477477
static inline float ImLinearSweep(float current, float target, float speed) { if (current < target) return ImMin(current + speed, target); if (current > target) return ImMax(current - speed, target); return current; }
478478
static inline ImVec2 ImMul(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); }
479479
static inline bool ImIsFloatAboveGuaranteedIntegerPrecision(float f) { return f <= -16777216 || f >= 16777216; }
480+
static inline float ImExponentialMovingAverage(float avg, float sample, int n) { avg -= avg / n; avg += sample / n; return avg; }
480481
IM_MSVC_RUNTIME_CHECKS_RESTORE
481482

482483
// Helpers: Geometry
@@ -1764,7 +1765,10 @@ struct ImGuiContext
17641765
ImGuiWindow* MovingWindow; // Track the window we clicked on (in order to preserve focus). The actual window that is moved is generally MovingWindow->RootWindow.
17651766
ImGuiWindow* WheelingWindow; // Track the window we started mouse-wheeling on. Until a timer elapse or mouse has moved, generally keep scrolling the same window even if during the course of scrolling the mouse ends up hovering a child window.
17661767
ImVec2 WheelingWindowRefMousePos;
1768+
int WheelingWindowStartFrame; // This may be set one frame before WheelingWindow is != NULL
17671769
float WheelingWindowReleaseTimer;
1770+
ImVec2 WheelingWindowWheelRemainder;
1771+
ImVec2 WheelingAxisAvg;
17681772

17691773
// Item/widgets state and tracking information
17701774
ImGuiID DebugHookIdInfo; // Will call core hooks: DebugHookIdInfo() from GetID functions, used by Stack Tool [next HoveredId/ActiveId to not pull in an extra cache-line]
@@ -2030,6 +2034,7 @@ struct ImGuiContext
20302034
HoveredWindowUnderMovingWindow = NULL;
20312035
MovingWindow = NULL;
20322036
WheelingWindow = NULL;
2037+
WheelingWindowStartFrame = -1;
20332038
WheelingWindowReleaseTimer = 0.0f;
20342039

20352040
DebugHookIdInfo = 0;

0 commit comments

Comments
 (0)