diff --git a/imgui.cpp b/imgui.cpp index 3128892f0..bee410a72 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -3079,9 +3079,23 @@ static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper) data->Ranges.push_back(ImGuiListClipperRange::FromPositions(nav_rect_abs.Min.y, nav_rect_abs.Max.y, 0, 0)); // Add visible range + float min_y = window->ClipRect.Min.y; + float max_y = window->ClipRect.Max.y; + + // Add box selection range + if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect) + if (ms->Storage->Window == window && ms->Storage->BoxSelectActive) + { + // FIXME: Selectable() use of half-ItemSpacing isn't consistent in matter of layout, as ItemAdd(bb) stray above ItemSize()'s CursorPos. + // RangeSelect's BoxSelect relies on comparing overlap of previous and current rectangle and is sensitive to that. + // As a workaround we currently half ItemSpacing worth on each side. + min_y -= g.Style.ItemSpacing.y; + max_y += g.Style.ItemSpacing.y; + } + const int off_min = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Up) ? -1 : 0; const int off_max = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Down) ? 1 : 0; - data->Ranges.push_back(ImGuiListClipperRange::FromPositions(window->ClipRect.Min.y, window->ClipRect.Max.y, off_min, off_max)); + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(min_y, max_y, off_min, off_max)); } // Convert position ranges to item index ranges @@ -13438,7 +13452,7 @@ bool ImGui::BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id) IM_ASSERT(g.DragDropWithinTarget == false && g.DragDropWithinSource == false); // Can't nest BeginDragDropSource() and BeginDragDropTarget() g.DragDropTargetRect = bb; - g.DragDropTargetClipRect = window->ClipRect; // May want to be overriden by user depending on use case? + g.DragDropTargetClipRect = window->ClipRect; // May want to be overridden by user depending on use case? g.DragDropTargetId = id; g.DragDropWithinTarget = true; return true; diff --git a/imgui.h b/imgui.h index 6de5cec7f..63159a14b 100644 --- a/imgui.h +++ b/imgui.h @@ -2774,8 +2774,8 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_None = 0, ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to share same code/logic if desired. It essentially disables the main purpose of BeginMultiSelect() tho! ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut sending a SelectAll request. - 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_BoxSelect = 1 << 2, // Enable box-selection. Box-selection + clipper is currently only supported for 1D list (not with 2D grid). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_BoxSelectNoScroll = 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 BeginMultiSelect() covers a whole window. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 45b758436..004f89ca1 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3031,6 +3031,7 @@ static void ShowDemoWindowMultiSelect() ImGui::BulletText("Shift modifier for range selection."); ImGui::BulletText("CTRL+A to select all."); ImGui::BulletText("Escape to clear selection."); + ImGui::BulletText("Click and drag to box-select."); ImGui::Text("Tip: Use 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen."); // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection @@ -3041,7 +3042,7 @@ static void ShowDemoWindowMultiSelect() // The BeginListBox() has no actual purpose for selection logic (other that offering a scrolling region). if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { - ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); selection.ApplyRequests(ms_io, ITEMS_COUNT); @@ -3076,7 +3077,7 @@ static void ShowDemoWindowMultiSelect() ImGui::Text("Selection: %d/%d", selection.Size, ITEMS_COUNT); if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { - ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); selection.ApplyRequests(ms_io, ITEMS_COUNT); @@ -3142,7 +3143,7 @@ static void ShowDemoWindowMultiSelect() if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { - ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); selection.ApplyRequests(ms_io, items.Size); @@ -3259,7 +3260,7 @@ static void ShowDemoWindowMultiSelect() static bool use_drag_drop = true; static bool show_in_table = false; static bool show_color_button = false; - static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_None; + static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; static WidgetType widget_type = WidgetType_Selectable; if (ImGui::RadioButton("Selectables", widget_type == WidgetType_Selectable)) { widget_type = WidgetType_Selectable; } @@ -3273,7 +3274,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_BoxSelectNoScroll", &flags, ImGuiMultiSelectFlags_BoxSelectNoScroll); 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)) @@ -9621,6 +9622,7 @@ struct ExampleAssetsBrowser // Options bool ShowTypeOverlay = true; bool AllowDragUnselected = false; + bool AllowBoxSelect = false; // Unsupported for 2D selection for now. float IconSize = 32.0f; int IconSpacing = 10; int IconHitSpacing = 4; // Increase hit-spacing if you want to make it possible to clear or box-select from gaps. Some spacing is required to able to amend with Shift+box-select. Value is small in Explorer. @@ -9724,6 +9726,9 @@ struct ExampleAssetsBrowser ImGui::SeparatorText("Selection Behavior"); ImGui::Checkbox("Allow dragging unselected item", &AllowDragUnselected); + ImGui::BeginDisabled(); // Unsupported for 2D selection for now. + ImGui::Checkbox("Allow box-selection", &AllowBoxSelect); + ImGui::EndDisabled(); ImGui::SeparatorText("Layout"); ImGui::SliderFloat("Icon Size", &IconSize, 16.0f, 128.0f, "%.0f"); @@ -9772,6 +9777,8 @@ struct ExampleAssetsBrowser ImGuiMultiSelectFlags ms_flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_ClearOnClickVoid; if (AllowDragUnselected) ms_flags |= ImGuiMultiSelectFlags_SelectOnClickRelease; // To allow dragging an unselected item without altering selection. + if (AllowBoxSelect) + ms_flags |= ImGuiMultiSelectFlags_BoxSelect; // FIXME-MULTISELECT: Box-select not yet supported for 2D selection when using clipper. ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags); // Use custom selection adapter: store ID in selection (recommended) diff --git a/imgui_internal.h b/imgui_internal.h index 65eb5eb89..0b05404e1 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1724,8 +1724,8 @@ struct IMGUI_API ImGuiMultiSelectTempData ImVec2 ScopeRectMin; ImVec2 BackupCursorMaxPos; ImGuiID BoxSelectId; - ImRect BoxSelectRectCurr; // Selection rectangle in absolute coordinates (derived from Storage->BoxSelectStartPosRel + MousePos) ImRect BoxSelectRectPrev; + ImRect BoxSelectRectCurr; // Selection rectangle in absolute coordinates (derived every frame from Storage->BoxSelectStartPosRel + MousePos) ImGuiSelectionUserData BoxSelectLastitem; ImGuiKeyChord KeyMods; bool LoopRequestClear; @@ -3102,7 +3102,7 @@ namespace ImGui inline ImRect WindowRectAbsToRel(ImGuiWindow* window, const ImRect& r) { ImVec2 off = window->DC.CursorStartPos; return ImRect(r.Min.x - off.x, r.Min.y - off.y, r.Max.x - off.x, r.Max.y - off.y); } inline ImRect WindowRectRelToAbs(ImGuiWindow* window, const ImRect& r) { ImVec2 off = window->DC.CursorStartPos; return ImRect(r.Min.x + off.x, r.Min.y + off.y, r.Max.x + off.x, r.Max.y + off.y); } inline ImVec2 WindowPosRelToAbs(ImGuiWindow* window, const ImVec2& p) { ImVec2 off = window->DC.CursorStartPos; return ImVec2(p.x + off.x, p.y + off.y); } - inline ImVec2 WindowPosAbsToRel(ImGuiWindow* window, const ImVec2& p) { ImVec2 off = window->DC.CursorStartPos; return ImVec2(p.x - off.x, p.y - off.y); } + inline ImVec2 WindowPosAbsToRel(ImGuiWindow* window, const ImVec2& p) { ImVec2 off = window->DC.CursorStartPos; return ImVec2(p.x - off.x, p.y - off.y); } // Windows: Display Order and Focus Order IMGUI_API void FocusWindow(ImGuiWindow* window, ImGuiFocusRequestFlags flags = 0); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index e0b32ac86..52a342e5a 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6749,6 +6749,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl const ImVec2 text_max(min_x + size.x, pos.y + size.y); // Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable. + // FIXME: Not part of layout so not included in clipper calculation, but ItemSize currenty doesn't allow offsetting CursorPos. ImRect bb(min_x, pos.y, text_max.x, text_max.y); if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0) { @@ -7135,20 +7136,22 @@ 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) +static void BoxSelectScrollWithMouseDrag(ImGuiWindow* window, const ImRect& inner_r) { ImGuiContext& g = *GImGui; - for (int n = 0; n < 2; n++) + for (int n = 0; n < 2; n++) // each axis { - 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])) + const float mouse_pos = g.IO.MousePos[n]; + const float dist = (mouse_pos > inner_r.Max[n]) ? mouse_pos - inner_r.Max[n] : (mouse_pos < inner_r.Min[n]) ? mouse_pos - inner_r.Min[n] : 0.0f; + const float scroll_curr = window->Scroll[n]; + if (dist == 0.0f || (dist < 0.0f && scroll_curr < 0.0f) || (dist > 0.0f && scroll_curr >= 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); + const float speed_multiplier = ImLinearRemapClamp(g.FontSize, g.FontSize * 5.0f, 1.0f, 4.0f, ImAbs(dist)); // x1 to x4 depending on distance + const 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); + ImGui::SetScrollX(window, scroll_curr + scroll_step); else - ImGui::SetScrollY(window, window->Scroll[n] + scroll_step); + ImGui::SetScrollY(window, scroll_curr + scroll_step); } } @@ -7165,6 +7168,8 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) g.CurrentMultiSelect = ms; if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0) flags |= ImGuiMultiSelectFlags_ScopeWindow; + if (flags & ImGuiMultiSelectFlags_SingleSelect) + flags &= ~ImGuiMultiSelectFlags_BoxSelect; // FIXME: BeginFocusScope() const ImGuiID id = window->IDStack.back(); @@ -7250,12 +7255,20 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) } if (storage->BoxSelectActive) { + // Current frame absolute prev/current rectangles are used to toggle selection. + // They are derived from positions relative to scrolling space. + const ImRect scope_rect = window->InnerClipRect; ImVec2 start_pos_abs = WindowPosRelToAbs(window, storage->BoxSelectStartPosRel); - ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, storage->BoxSelectEndPosRel); + ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, storage->BoxSelectEndPosRel); // Clamped already + ImVec2 curr_end_pos_abs = g.IO.MousePos; + if (ms->Flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow + curr_end_pos_abs = ImClamp(curr_end_pos_abs, scope_rect.Min, scope_rect.Max); ms->BoxSelectRectPrev.Min = ImMin(start_pos_abs, prev_end_pos_abs); ms->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs); - ms->BoxSelectRectCurr.Min = ImMin(start_pos_abs, g.IO.MousePos); - ms->BoxSelectRectCurr.Max = ImMax(start_pos_abs, g.IO.MousePos); + ms->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs); + ms->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs); + //GetForegroundDrawList()->AddRect(ms->BoxSelectRectPrev.Min, ms->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); + //GetForegroundDrawList()->AddRect(ms->BoxSelectRectCurr.Min, ms->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f); } } @@ -7303,7 +7316,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if ((ms->Flags & ImGuiMultiSelectFlags_BoxSelect) && storage->BoxSelectActive) { // Box-select: render selection rectangle - ms->Storage->BoxSelectEndPosRel = WindowPosAbsToRel(window, g.IO.MousePos); + ms->Storage->BoxSelectEndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view 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 @@ -7311,8 +7324,9 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() // 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)) + scroll_r.Expand(-g.FontSize); + //GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255)); + if ((ms->Flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms->Flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0 && !scroll_r.Contains(g.IO.MousePos)) BoxSelectScrollWithMouseDrag(window, scroll_r); } }