diff --git a/imgui.cpp b/imgui.cpp index a603ad880968..e065d968427d 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -136,7 +136,8 @@ CODE - Click [X]: Close a window, available when 'bool* p_open' is passed to ImGui::Begin(). - Click ^, Double-Click title: Collapse window. - Drag on corner/border: Resize window (double-click to auto fit window to its contents). - - Drag on any empty space: Move window (unless io.ConfigWindowsMoveFromTitleBarOnly = true). + - Drag on any empty space: Move window (unless io.ConfigWindowsMoveFromTitleBarOnly = true) + - Drag inside window: Scroll contents (when io.ConfigDragScroll = true) unless drag move is possible. - Left-click outside popup: Close popup stack (right-click over underlying popup: Partially close popup stack). - TEXT EDITOR @@ -1661,6 +1662,12 @@ ImGuiIO::ImGuiIO() KeyRepeatDelay = 0.275f; KeyRepeatRate = 0.050f; + // Drag scroll options + ConfigDragScroll = false; + DragScrollButton = ImGuiMouseButton_Left; + DragScrollDecel = 5000.0f; + DragScrollMinSpeed = 300.0f; + // Platform Functions // Note: Initialize() will setup default clipboard/ime handlers. BackendPlatformName = BackendRendererName = NULL; @@ -4166,6 +4173,9 @@ ImGuiContext::ImGuiContext(ImFontAtlas* shared_font_atlas) WheelingWindow = NULL; WheelingWindowStartFrame = WheelingWindowScrolledFrame = -1; WheelingWindowReleaseTimer = 0.0f; + DragScrollWindow = NULL; + DragScrollOldValue = ImVec2(0.0f, 0.0f); + DragScrollVelocity = ImVec2(0.0f, 0.0f); DebugDrawIdConflictsId = 0; DebugHookIdInfoId = 0; @@ -4195,6 +4205,7 @@ ImGuiContext::ImGuiContext(ImFontAtlas* shared_font_atlas) memset(&ActiveIdValueOnActivation, 0, sizeof(ActiveIdValueOnActivation)); LastActiveId = 0; LastActiveIdTimer = 0.0f; + DragAction = false; LastKeyboardKeyPressTime = LastKeyModsChangeTime = LastKeyModsChangeFromNoneTime = -1.0; @@ -5588,6 +5599,7 @@ void ImGui::NewFrame() if (g.DeactivatedItemData.ElapseFrame < g.FrameCount) g.DeactivatedItemData.ID = 0; g.DeactivatedItemData.IsAlive = false; + g.DragAction = false; // Record when we have been stationary as this state is preserved while over same item. // FIXME: The way this is expressed means user cannot alter HoverStationaryDelay during the frame to use varying values. @@ -6055,6 +6067,8 @@ void ImGui::EndFrame() for (ImFontAtlas* atlas : g.FontAtlases) atlas->Locked = false; + HandleDragScroll(); + // Clear Input data for next frame g.IO.MousePosPrev = g.IO.MousePos; g.IO.AppFocusLost = false; @@ -6360,6 +6374,184 @@ void ImGui::SetActiveIdUsingAllKeyboardKeys() NavMoveRequestCancel(); } +// Walk up the window hierarchy (up to a root window) until a scrollable window is found. +static ImGuiWindow* FindScrollableWindow(ImGuiWindow* win) +{ + for (ImGuiWindow* target = win; target; target = target->ParentWindow) + { + const bool mouse_inputs_forbidden = target->Flags & ImGuiWindowFlags_NoMouseInputs; + const bool mouse_scroll_forbidden = target->Flags & ImGuiWindowFlags_NoScrollWithMouse; + const bool is_scrollable = target->ScrollMax.x > 0 || target->ScrollMax.y > 0; + if (!mouse_inputs_forbidden && !mouse_scroll_forbidden && is_scrollable) + return target; + // Stop if target is a root window. + if (target->ParentWindow == target) + return NULL; + } + return NULL; +} + +void ImGui::HandleDragScroll() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + // Bail out if DragScroll is disabled. + if (!io.ConfigDragScroll) + { + g.DragScrollWindow = NULL; + return; + } + + // Bail out if a widget is performing a drag action. + if (IsDragAction()) + { + g.DragScrollWindow = NULL; + return; + } + + // Bail out if a drag-and-drop operation is ongoing. + if (IsDragDropActive()) + { + g.DragScrollWindow = NULL; + return; + } + + // Bail out if a window is being moved. + if (g.MovingWindow) + { + g.DragScrollWindow = NULL; + return; + } + + if (g.DragScrollWindow) + { + // Bail out if it was garbage-collected. + if (g.DragScrollWindow->MemoryCompacted) + { + g.DragScrollWindow = NULL; + return; + } + + // Bail out if the window is collapsed. + if (g.DragScrollWindow->Collapsed) + { + g.DragScrollWindow = NULL; + return; + } + + // Bail out when drag move conflicts with drag scroll. + const bool is_movable = !(g.DragScrollWindow->Flags & ImGuiWindowFlags_NoMove); + const bool is_drag_movable = g.DragScrollWindow->BgClickFlags & ImGuiWindowBgClickFlags_Move; + if (is_movable && is_drag_movable) + { + g.DragScrollWindow = NULL; + return; + } + + // Bail out if window content is not hoverable (e.g. modal on top.) + if (!IsWindowContentHoverable(g.DragScrollWindow)) + { + g.DragScrollWindow = NULL; + return; + } + } + + if (IsMouseDown(io.DragScrollButton)) + { + // Button is down. + + // Never allow gliding while the drag scroll button is down. + g.DragScrollVelocity = ImVec2(0.0f, 0.0f); + + if (IsMouseClicked(io.DragScrollButton)) + { + // Just clicked. + const ImVec2 clicked_pos = io.MouseClickedPos[io.DragScrollButton]; + + // Bail out if clicked position is not valid. + if (!IsMousePosValid(&clicked_pos)) + return; + + ImGuiWindow* pointed_window = NULL; + FindHoveredWindowEx(clicked_pos, false, &pointed_window, NULL); + + g.DragScrollWindow = FindScrollableWindow(pointed_window); + // Save original scroll value. + if (g.DragScrollWindow) + g.DragScrollOldValue = g.DragScrollWindow->Scroll; + } + + // Bail out if there's no window to scroll. + if (!g.DragScrollWindow) + return; + + // Bail out if not (yet) in a dragging state. + if (!IsMouseDragging(io.DragScrollButton)) + return; + + // Perform drag scroll. + ImVec2 drag_delta = GetMouseDragDelta(io.DragScrollButton); + SetScrollX(g.DragScrollWindow, g.DragScrollOldValue.x - drag_delta.x); + SetScrollY(g.DragScrollWindow, g.DragScrollOldValue.y - drag_delta.y); + + // Remember velocity for when the button is released. + g.DragScrollVelocity = - io.MouseDelta / io.DeltaTime; + + // Ensure no widget is active, to avoid activating buttons, menus,etc. + ClearActiveID(); + } + else + { + // Button is not down. + + // Bail out if no window to scroll. + if (!g.DragScrollWindow) + return; + + const float min_speed_2 = io.DragScrollMinSpeed * io.DragScrollMinSpeed; + ImVec2& vel = g.DragScrollVelocity; + const float speed_2 = ImLengthSqr(vel); + + // Check if speed high is enough to keep gliding. + const bool is_gliding = speed_2 > min_speed_2; + + // Perform kinetic scrolling if gliding. + if (is_gliding) + { + const ImVec2 old_pos = g.DragScrollWindow->Scroll; + const ImVec2 new_pos = old_pos + vel * io.DeltaTime; + SetScrollX(g.DragScrollWindow, new_pos.x); + SetScrollY(g.DragScrollWindow, new_pos.y); + + // Decelerate scroll velocity. + // integrate deceleration over the delta time + const float decel_speed = io.DragScrollDecel * io.DeltaTime; + const float speed = ImSqrt(speed_2); + if (speed <= decel_speed) + vel = ImVec2(0.0f, 0.0f); + else + // deceleration velocity is always opposed to velocity (vel / speed == normalized(vel)) + vel -= vel * decel_speed / speed; + + // Cancel velocity when hitting a scroll boundary. + const ImVec2 max = g.DragScrollWindow->ScrollMax; + if ((new_pos.x <= 0 && vel.x < 0) || (new_pos.x >= max.x && vel.x > 0)) + vel.x = 0; + if ((new_pos.y <= 0 && vel.y < 0) || (new_pos.y >= max.y && vel.y > 0)) + vel.y = 0; + + // When gliding, we don't want any hover events. + ClearActiveID(); + // If Touchscreen, invalidate mouse position and drag delta, so we don't generate hover events. + if (io.MouseSource == ImGuiMouseSource_TouchScreen) { + io.MousePos = io.MousePosPrev = ImVec2(-FLT_MAX, -FLT_MAX); + ResetMouseDragDelta(io.DragScrollButton); + } + } + } +} + ImGuiID ImGui::GetItemID() { ImGuiContext& g = *GImGui; @@ -6949,6 +7141,7 @@ static int ImGui::UpdateWindowManualResize(ImGuiWindow* window, int* border_hove ImVec2 corner_target = g.IO.MousePos - g.ActiveIdClickOffset + ImLerp(def.InnerDir * grip_hover_outer_size, def.InnerDir * -grip_hover_inner_size, def.CornerPosN); // Corner of the window corresponding to our corner grip corner_target = ImClamp(corner_target, clamp_min, clamp_max); CalcResizePosSizeFromAnyCorner(window, corner_target, def.CornerPosN, &pos_target, &size_target); + SetDragAction(); } // Only lower-left grip is visible before hovering/activating @@ -7028,8 +7221,10 @@ static int ImGui::UpdateWindowManualResize(ImGuiWindow* window, int* border_hove ImVec2 clamp_min(border_n == ImGuiDir_Right ? clamp_rect.Min.x : -FLT_MAX, border_n == ImGuiDir_Down || (border_n == ImGuiDir_Up && window_move_from_title_bar) ? clamp_rect.Min.y : -FLT_MAX); ImVec2 clamp_max(border_n == ImGuiDir_Left ? clamp_rect.Max.x : +FLT_MAX, border_n == ImGuiDir_Up ? clamp_rect.Max.y : +FLT_MAX); border_target = ImClamp(border_target, clamp_min, clamp_max); - if (!ignore_resize) + if (!ignore_resize) { CalcResizePosSizeFromAnyCorner(window, border_target, ImMin(def.SegmentN1, def.SegmentN2), &pos_target, &size_target); + SetDragAction(); + } } if (hovered) *border_hovered = border_n; @@ -9331,7 +9526,7 @@ IM_MSVC_RUNTIME_CHECKS_RESTORE // - IsMouseDragPastThreshold() [Internal] // - IsMouseDragging() // - GetMousePos() -// - SetMousePos() [Internal] +// - TeleportMousePos() [Internal] // - GetMousePosOnOpeningCurrentPopup() // - IsMousePosValid() // - IsAnyMouseDown() diff --git a/imgui.h b/imgui.h index 35adb2dc1287..caed3d1e152d 100644 --- a/imgui.h +++ b/imgui.h @@ -2447,6 +2447,12 @@ struct ImGuiIO float KeyRepeatDelay; // = 0.275f // When holding a key/button, time before it starts repeating, in seconds (for buttons in Repeat mode, etc.). float KeyRepeatRate; // = 0.050f // When holding a key/button, rate at which it repeats, in seconds. + // Drag scrolling behavior. + bool ConfigDragScroll; // = false // Dragging with a mouse button will scroll the content. + ImGuiMouseButton DragScrollButton; // ImGuiMouseButton_Left // What mouse button is used to detect drag scrolling. See ImGuiConfigFlags_DragScroll. + float DragScrollDecel; // = 5000.0f // How much of the scroll speed decelerates, in pixels per second. + float DragScrollMinSpeed; // = 300.0f // Minimum kinetic scroll speed, in pixels per second, before the scroll is stopped. + //------------------------------------------------------------------ // Debug options //------------------------------------------------------------------ diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 461b2cd83f2a..d82acc3984b1 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -550,6 +550,18 @@ void ImGui::ShowDemoWindow(bool* p_open) if (!io.ConfigErrorRecoveryEnableAssert && !io.ConfigErrorRecoveryEnableDebugLog && !io.ConfigErrorRecoveryEnableTooltip) io.ConfigErrorRecoveryEnableAssert = io.ConfigErrorRecoveryEnableDebugLog = io.ConfigErrorRecoveryEnableTooltip = true; + ImGui::SeparatorText("Dragging and scrolling"); + ImGui::Checkbox("io.ConfigDragScroll", &io.ConfigDragScroll); + ImGui::SameLine(); HelpMarker("Enable drag-to-scroll interactions."); + ImGui::PushItemWidth(-ImGui::GetContentRegionAvail().x * 0.5f); + ImGui::DragFloat("io.MouseDragThreshold", &io.MouseDragThreshold, 0.5f, 0.0f, 100.0f, "%.0f"); + ImGui::SameLine(); HelpMarker("Distance threshold before considering we are dragging."); + ImGui::DragFloat("io.DragScrollDecel", &io.DragScrollDecel, 10.0f, 0.0f, 10000.0f, "%.0f"); + ImGui::SameLine(); HelpMarker("How much of the scroll speed decelerates, in pixels per second."); + ImGui::DragFloat("io.DragScrollMinSpeed", &io.DragScrollMinSpeed, 1.0f, 0.0f, 1000.0f, "%.0f"); + ImGui::SameLine(); HelpMarker("Minimum kinetic scroll speed, in pixels per second, before the scroll is stopped."); + ImGui::PopItemWidth(); + // Also read: https://github.com/ocornut/imgui/wiki/Debug-Tools ImGui::SeparatorText("Debug"); ImGui::Checkbox("io.ConfigDebugIsDebuggerPresent", &io.ConfigDebugIsDebuggerPresent); diff --git a/imgui_internal.h b/imgui_internal.h index 9cf35194bb60..9ed1f47f512a 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -278,7 +278,7 @@ extern IMGUI_API ImGuiContext* GImGui; // Current implicit context pointer #define IM_F32_TO_INT8_UNBOUND(_VAL) ((int)((_VAL) * 255.0f + ((_VAL)>=0 ? 0.5f : -0.5f))) // Unsaturated, for display purpose #define IM_F32_TO_INT8_SAT(_VAL) ((int)(ImSaturate(_VAL) * 255.0f + 0.5f)) // Saturated, always output 0..255 #define IM_TRUNC(_VAL) ((float)(int)(_VAL)) // Positive values only! ImTrunc() is not inlined in MSVC debug builds -#define IM_ROUND(_VAL) ((float)(int)((_VAL) + 0.5f)) // Positive values only! +#define IM_ROUND(_VAL) ((float)(int)((_VAL) + 0.5f)) // Positive values only! //#define IM_FLOOR IM_TRUNC // [OBSOLETE] Renamed in 1.90.0 (Sept 2023) // Hint for branch prediction @@ -2242,6 +2242,9 @@ struct ImGuiContext float WheelingWindowReleaseTimer; ImVec2 WheelingWindowWheelRemainder; ImVec2 WheelingAxisAvg; + ImGuiWindow* DragScrollWindow; // Track which window is the target of a drag scroll. + ImVec2 DragScrollOldValue; // Store original scroll value before a drag scroll starts. + ImVec2 DragScrollVelocity; // Item/widgets state and tracking information ImGuiID DebugDrawIdConflictsId; // Set when we detect multiple items with the same identifier @@ -2274,6 +2277,7 @@ struct ImGuiContext ImGuiDataTypeStorage ActiveIdValueOnActivation; // Backup of initial value at the time of activation. ONLY SET BY SPECIFIC WIDGETS: DragXXX and SliderXXX. ImGuiID LastActiveId; // Store the last non-zero ActiveId, useful for animation. float LastActiveIdTimer; // Store the last non-zero ActiveId timer since the beginning of activation, useful for animation. + bool DragAction; // True when a widget is handling a drag-like action, to avoid conflicts with the drag scrolling code. // Key/Input Ownership + Shortcut Routing system // - The idea is that instead of "eating" a given key, we can link to an owner. @@ -3411,6 +3415,10 @@ namespace ImGui IMGUI_API void SetActiveIdUsingAllKeyboardKeys(); inline bool IsActiveIdUsingNavDir(ImGuiDir dir) { ImGuiContext& g = *GImGui; return (g.ActiveIdUsingNavDirMask & (1 << dir)) != 0; } + inline void SetDragAction(bool state = true) { ImGuiContext& g = *GImGui; g.DragAction = state; } // Set to true when a widget is using a mouse drag interaction. + inline bool IsDragAction() { ImGuiContext& g = *GImGui; return g.DragAction; } // Query if a widget is using a mouse drag interaction. + IMGUI_API void HandleDragScroll(); + // [EXPERIMENTAL] Low-Level: Key/Input Ownership // - The idea is that instead of "eating" a given input, we can link to an owner id. // - Ownership is most often claimed as a result of reacting to a press/down event (but occasionally may be claimed ahead). diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index d96c5711ee58..01ec112c26a6 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -1108,6 +1108,8 @@ bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, ImS6 // Update distance to grab now that we have seek'ed and saturated //if (seek_absolute) // g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f; + + SetDragAction(); } // Render @@ -2536,6 +2538,7 @@ bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const adjust_delta *= 1.0f / 100.0f; if (g.IO.KeyShift && !(flags & ImGuiSliderFlags_NoSpeedTweaks)) adjust_delta *= 10.0f; + SetDragAction(); } else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) { @@ -3144,6 +3147,7 @@ bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_typ if (axis == ImGuiAxis_Y) clicked_t = 1.0f - clicked_t; set_new_value = true; + SetDragAction(); } } else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) @@ -5029,6 +5033,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ stb_textedit_drag(state, state->Stb, mouse_x, mouse_y); state->CursorAnimReset(); state->CursorFollow = true; + SetDragAction(); } if (state->SelectedAllMouseLock && !io.MouseDown[0]) state->SelectedAllMouseLock = false; @@ -6404,6 +6409,9 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl g.ColorEditCurrentID = 0; PopID(); + if (value_changed) + SetDragAction(); + return value_changed; } @@ -6965,7 +6973,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l // Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags. // Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support. - // - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0) + // - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0), or MouseDown (IO.ConfigDragScroll = true) // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0) // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1) // - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1) @@ -6973,7 +6981,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l // It is rather standard that arrow click react on Down rather than Up. // We set ImGuiButtonFlags_PressedOnClickRelease on OpenOnDoubleClick because we want the item to be active on the initial MouseDown in order for drag and drop to work. if (is_mouse_x_over_arrow) - button_flags |= ImGuiButtonFlags_PressedOnClick; + button_flags |= g.IO.ConfigDragScroll ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick; else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; else @@ -7730,6 +7738,7 @@ static void BoxSelectPreStartDrag(ImGuiID id, ImGuiSelectionUserData clicked_ite bs->KeyMods = g.IO.KeyMods; bs->StartPosRel = bs->EndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos); bs->ScrollAccum = ImVec2(0.0f, 0.0f); + ImGui::SetDragAction(); } static void BoxSelectActivateDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window) @@ -9266,7 +9275,10 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled) bool pressed; // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another. - const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SelectOnClick | ImGuiSelectableFlags_NoAutoClosePopups; + ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_NoAutoClosePopups; + // SelectOnClick is not compatible with drag scrolling. + if (!g.IO.ConfigDragScroll) + selectable_flags |= ImGuiSelectableFlags_SelectOnClick; ImGuiMenuColumns* offsets = &window->DC.MenuColumns; if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) { @@ -9475,7 +9487,10 @@ bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut BeginDisabled(); // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another. - const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_SelectOnRelease | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SetNavIdOnHover; + ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SetNavIdOnHover; + // SelectOnRelease is not compatible with drag scrolling. + if (!g.IO.ConfigDragScroll) + selectable_flags |= ImGuiSelectableFlags_SelectOnRelease; ImGuiMenuColumns* offsets = &window->DC.MenuColumns; if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) {