From aa4d64be925da3575bce51111114d5cb0ebe5433 Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 26 Sep 2023 13:44:10 +0200 Subject: [PATCH] MultiSelect: Box-Select: added scroll support. --- imgui.h | 1 + imgui_demo.cpp | 3 ++- imgui_internal.h | 1 + imgui_widgets.cpp | 42 ++++++++++++++++++++++++++++++++++-------- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/imgui.h b/imgui.h index 701d995dd..bb6ab85fc 100644 --- a/imgui.h +++ b/imgui.h @@ -2734,6 +2734,7 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to use same code/logic is desired, but may not be very useful. ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut to set RequestSelectAll ImGuiMultiSelectFlags_BoxSelect = 1 << 2, // Enable box-selection. Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_NoBoxSelectScroll = 1 << 3, // Disable scrolling when box-selecting near edges of scope. ImGuiMultiSelectFlags_ClearOnEscape = 1 << 4, // Clear selection when pressing Escape while scope is focused. ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 5, // Clear selection when clicking on empty location within scope. ImGuiMultiSelectFlags_ScopeWindow = 1 << 6, // Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). Use if (use if BeginMultiSelect() covers a whole window. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index a97972648..46ccf7dd7 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3362,6 +3362,7 @@ static void ShowDemoWindowMultiSelect() ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SingleSelect", &flags, ImGuiMultiSelectFlags_SingleSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoSelectAll", &flags, ImGuiMultiSelectFlags_NoSelectAll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoBoxSelectScroll", &flags, ImGuiMultiSelectFlags_NoBoxSelectScroll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickVoid); if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeWindow", &flags, ImGuiMultiSelectFlags_ScopeWindow) && (flags & ImGuiMultiSelectFlags_ScopeWindow)) @@ -3372,7 +3373,7 @@ static void ShowDemoWindowMultiSelect() flags &= ~ImGuiMultiSelectFlags_SelectOnClickRelease; if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SelectOnClickRelease", &flags, ImGuiMultiSelectFlags_SelectOnClickRelease) && (flags & ImGuiMultiSelectFlags_SelectOnClickRelease)) flags &= ~ImGuiMultiSelectFlags_SelectOnClick; - ImGui::SameLine(); HelpMarker("Allow dragging an unselected item without altering selection."); + ImGui::SameLine(); HelpMarker("Allow dragging an unselected item without altering selection."); // Initialize default list with 1000 items. static ImVector items; diff --git a/imgui_internal.h b/imgui_internal.h index 2ce16fb1a..271946569 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -490,6 +490,7 @@ static inline int ImModPositive(int a, int b) static inline float ImDot(const ImVec2& a, const ImVec2& b) { return a.x * b.x + a.y * b.y; } static inline ImVec2 ImRotate(const ImVec2& v, float cos_a, float sin_a) { return ImVec2(v.x * cos_a - v.y * sin_a, v.x * sin_a + v.y * cos_a); } 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; } +static inline float ImLinearRemapClamp(float s0, float s1, float d0, float d1, float x) { return ImSaturate((x - s0) / (s1 - s0)) * (d1 - d0) + d0; } static inline ImVec2 ImMul(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); } static inline bool ImIsFloatAboveGuaranteedIntegerPrecision(float f) { return f <= -16777216 || f >= 16777216; } static inline float ImExponentialMovingAverage(float avg, float sample, int n) { avg -= avg / n; avg += sample / n; return avg; } diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index ec4f53744..b8b7032aa 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7105,6 +7105,7 @@ void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) //------------------------------------------------------------------------- // - DebugLogMultiSelectRequests() [Internal] // - BoxSelectStart() [Internal] +// - BoxSelectScrollWithMouseDrag() [Internal] // - BeginMultiSelect() // - EndMultiSelect() // - SetNextItemSelectionUserData() @@ -7133,6 +7134,23 @@ static void BoxSelectStart(ImGuiMultiSelectState* storage, ImGuiSelectionUserDat storage->BoxSelectStartPosRel = storage->BoxSelectEndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos); } +static void BoxSelectScrollWithMouseDrag(ImGuiWindow* window, const ImRect& r) +{ + ImGuiContext& g = *GImGui; + for (int n = 0; n < 2; n++) + { + float dist = (g.IO.MousePos[n] > r.Max[n]) ? g.IO.MousePos[n] - r.Max[n] : (g.IO.MousePos[n] < r.Min[n]) ? g.IO.MousePos[n] - r.Min[n] : 0.0f; + if (dist == 0.0f || (dist < 0.0f && window->Scroll[n] < 0.0f) || (dist > 0.0f && window->Scroll[n] >= window->ScrollMax[n])) + continue; + float speed_multiplier = ImLinearRemapClamp(g.FontSize, g.FontSize * 5.0f, 1.0f, 4.0f, ImAbs(dist)); // x1 to x4 depending on distance + float scroll_step = IM_ROUND(g.FontSize * 35.0f * speed_multiplier * ImSign(dist) * g.IO.DeltaTime); + if (n == 0) + ImGui::SetScrollX(window, window->Scroll[n] + scroll_step); + else + ImGui::SetScrollY(window, window->Scroll[n] + scroll_step); + } +} + // Return ImGuiMultiSelectIO structure. Lifetime: valid until corresponding call to EndMultiSelect(). ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) { @@ -7142,6 +7160,8 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) IM_ASSERT(g.CurrentMultiSelect == NULL); // No recursion allowed yet (we could allow it if we deem it useful) IM_STATIC_ASSERT(offsetof(ImGuiMultiSelectTempData, IO) == 0); // Clear() relies on that. g.CurrentMultiSelect = ms; + if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0) + flags |= ImGuiMultiSelectFlags_ScopeWindow; // FIXME: BeginFocusScope() const ImGuiID id = window->IDStack.back(); @@ -7257,6 +7277,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() IM_ASSERT(ms->FocusScopeId == g.CurrentFocusScopeId); IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow); + const ImRect scope_rect = (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) ? ImRect(ms->ScopeRectMin, ImMax(window->DC.CursorMaxPos, ms->ScopeRectMin)) : window->InnerClipRect; if (ms->IsFocused) { // We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here. @@ -7272,25 +7293,30 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() storage->NavIdSelected = -1; } - // Box-select: render selection rectangle - // FIXME-MULTISELECT: Scroll on box-select if ((ms->Flags & ImGuiMultiSelectFlags_BoxSelect) && storage->BoxSelectActive) { + // Box-select: render selection rectangle ms->Storage->BoxSelectEndPosRel = WindowPosAbsToRel(window, g.IO.MousePos); - window->DrawList->AddRectFilled(ms->BoxSelectRectCurr.Min, ms->BoxSelectRectCurr.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling - window->DrawList->AddRect(ms->BoxSelectRectCurr.Min, ms->BoxSelectRectCurr.Max, GetColorU32(ImGuiCol_NavHighlight)); // FIXME-MULTISELECT: Styling + ImRect box_select_r = ms->BoxSelectRectCurr; + box_select_r.ClipWith(scope_rect); + window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling + window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavHighlight)); // FIXME-MULTISELECT: Styling + + // Box-select: scroll + ImRect scroll_r = scope_rect; + scroll_r.Expand(g.Style.FramePadding); + if ((ms->Flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms->Flags & ImGuiMultiSelectFlags_NoBoxSelectScroll) == 0 && !scroll_r.Contains(g.IO.MousePos)) + BoxSelectScrollWithMouseDrag(window, scroll_r); } } if (ms->IsEndIO == false) ms->IO.Requests.resize(0); - const ImRect scope_rect(ms->ScopeRectMin, ImMax(window->DC.CursorMaxPos, ms->ScopeRectMin)); - const bool scope_hovered = (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) ? IsMouseHoveringRect(scope_rect.Min, scope_rect.Max) : IsWindowHovered(); - // Clear selection when clicking void? // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection! - if (scope_hovered && g.HoveredId == 0) + const bool scope_hovered = (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) ? scope_rect.Contains(g.IO.MousePos) : IsWindowHovered(); + if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0) { if (ms->Flags & ImGuiMultiSelectFlags_BoxSelect) if (!storage->BoxSelectActive && !storage->BoxSelectStarting && g.IO.MouseClickedCount[0] == 1)