Skip to content

Commit

Permalink
RangeSelect/MultiSelect: WIP range-select (#1861)
Browse files Browse the repository at this point in the history
  • Loading branch information
ocornut committed Jan 31, 2019
1 parent 2d363fa commit ceeedc5
Show file tree
Hide file tree
Showing 5 changed files with 524 additions and 35 deletions.
21 changes: 20 additions & 1 deletion imgui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,7 @@ CODE
// Debug options
#define IMGUI_DEBUG_NAV_SCORING 0
#define IMGUI_DEBUG_NAV_RECTS 0
#define IMGUI_DEBUG_MULTISELECT 0

// Visual Studio warnings
#ifdef _MSC_VER
Expand Down Expand Up @@ -1095,6 +1096,7 @@ ImGuiStyle::ImGuiStyle()
FrameBorderSize = 0.0f; // Thickness of border around frames. Generally set to 0.0f or 1.0f. Other values not well tested.
ItemSpacing = ImVec2(8,4); // Horizontal and vertical spacing between widgets/lines
ItemInnerSpacing = ImVec2(4,4); // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label)
SelectableSpacing = ImVec2(0,0); // Horizontal and vertical spacing between selectables (by default they are canceling out the effect of ItemSpacing).
TouchExtraPadding = ImVec2(0,0); // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much!
IndentSpacing = 21.0f; // Horizontal spacing when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2).
ColumnsMinSpacing = 6.0f; // Minimum horizontal spacing between two columns
Expand Down Expand Up @@ -1130,6 +1132,7 @@ void ImGuiStyle::ScaleAllSizes(float scale_factor)
TabRounding = ImFloor(TabRounding * scale_factor);
ItemSpacing = ImFloor(ItemSpacing * scale_factor);
ItemInnerSpacing = ImFloor(ItemInnerSpacing * scale_factor);
SelectableSpacing = ImFloor(SelectableSpacing * scale_factor);
TouchExtraPadding = ImFloor(TouchExtraPadding * scale_factor);
IndentSpacing = ImFloor(IndentSpacing * scale_factor);
ColumnsMinSpacing = ImFloor(ColumnsMinSpacing * scale_factor);
Expand Down Expand Up @@ -2635,6 +2638,7 @@ void ImGui::SetActiveID(ImGuiID id, ImGuiWindow* window)
if (g.ActiveIdIsJustActivated)
{
g.ActiveIdTimer = 0.0f;
g.ActiveIdPressed = false;
g.ActiveIdHasBeenEdited = false;
if (id != 0)
{
Expand Down Expand Up @@ -3600,6 +3604,7 @@ void ImGui::Shutdown(ImGuiContext* context)
g.InputTextState.TextW.clear();
g.InputTextState.InitialText.clear();
g.InputTextState.TempBuffer.clear();
g.MultiSelectScopeWindow = NULL;

for (int i = 0; i < g.SettingsWindows.Size; i++)
IM_DELETE(g.SettingsWindows[i].Name);
Expand Down Expand Up @@ -4235,6 +4240,12 @@ bool ImGui::IsItemClicked(int mouse_button)
return IsMouseClicked(mouse_button) && IsItemHovered(ImGuiHoveredFlags_None);
}

bool ImGui::IsItemToggledSelection()
{
ImGuiContext& g = *GImGui;
return (g.CurrentWindow->DC.LastItemStatusFlags & ImGuiItemStatusFlags_ToggledSelection) ? true : false;
}

bool ImGui::IsAnyItemHovered()
{
ImGuiContext& g = *GImGui;
Expand Down Expand Up @@ -5777,6 +5788,7 @@ static const ImGuiStyleVarInfo GStyleVarInfo[] =
{ ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, FrameBorderSize) }, // ImGuiStyleVar_FrameBorderSize
{ ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, ItemSpacing) }, // ImGuiStyleVar_ItemSpacing
{ ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, ItemInnerSpacing) }, // ImGuiStyleVar_ItemInnerSpacing
{ ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, SelectableSpacing) }, // ImGuiStyleVar_SelectableSpacing
{ ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, IndentSpacing) }, // ImGuiStyleVar_IndentSpacing
{ ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ScrollbarSize) }, // ImGuiStyleVar_ScrollbarSize
{ ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ScrollbarRounding) }, // ImGuiStyleVar_ScrollbarRounding
Expand Down Expand Up @@ -7295,6 +7307,7 @@ static void ImGui::NavProcessItem(ImGuiWindow* window, const ImRect& nav_bb, con
if (new_best)
{
result->ID = id;
result->SelectScopeId = g.MultiSelectScopeId;
result->Window = window;
result->RectRel = nav_bb_rel;
}
Expand All @@ -7306,6 +7319,7 @@ static void ImGui::NavProcessItem(ImGuiWindow* window, const ImRect& nav_bb, con
{
result = &g.NavMoveResultLocalVisibleSet;
result->ID = id;
result->SelectScopeId = g.MultiSelectScopeId;
result->Window = window;
result->RectRel = nav_bb_rel;
}
Expand Down Expand Up @@ -7846,8 +7860,13 @@ static void ImGui::NavUpdateMoveResult()

ClearActiveID();
g.NavWindow = result->Window;
if (g.NavId != result->ID)
{
// Don't set NavJustMovedToId if just landed on the same spot (which may happen with ImGuiNavMoveFlags_AllowCurrentNavId)
g.NavJustMovedToId = result->ID;
g.NavJustMovedToSelectScopeId = result->SelectScopeId;
}
SetNavIDWithRectRel(result->ID, g.NavLayer, result->RectRel);
g.NavJustMovedToId = result->ID;
g.NavMoveFromClampedRefRect = false;
}

Expand Down
73 changes: 73 additions & 0 deletions imgui.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ struct ImGuiContext; // Dear ImGui context (opaque structure, unl
struct ImGuiIO; // Main configuration and I/O between your application and ImGui
struct ImGuiInputTextCallbackData; // Shared state of InputText() when using custom ImGuiInputTextCallback (rare/advanced use)
struct ImGuiListClipper; // Helper to manually clip large list of items
struct ImGuiMultiSelectData; // State for a BeginMultiSelect() block
struct ImGuiOnceUponAFrame; // Helper for running a block of code not more than once a frame, used by IMGUI_ONCE_UPON_A_FRAME macro
struct ImGuiPayload; // User data payload for drag and drop operations
struct ImGuiSizeCallbackData; // Callback data when using SetNextWindowSizeConstraints() (rare/advanced use)
Expand Down Expand Up @@ -141,6 +142,7 @@ typedef int ImGuiDragDropFlags; // -> enum ImGuiDragDropFlags_ // Flags: f
typedef int ImGuiFocusedFlags; // -> enum ImGuiFocusedFlags_ // Flags: for IsWindowFocused()
typedef int ImGuiHoveredFlags; // -> enum ImGuiHoveredFlags_ // Flags: for IsItemHovered(), IsWindowHovered() etc.
typedef int ImGuiInputTextFlags; // -> enum ImGuiInputTextFlags_ // Flags: for InputText*()
typedef int ImGuiMultiSelectFlags; // -> enum ImGuiMultiSelectFlags_// Flags: for BeginMultiSelect()
typedef int ImGuiSelectableFlags; // -> enum ImGuiSelectableFlags_ // Flags: for Selectable()
typedef int ImGuiTabBarFlags; // -> enum ImGuiTabBarFlags_ // Flags: for BeginTabBar()
typedef int ImGuiTabItemFlags; // -> enum ImGuiTabItemFlags_ // Flags: for BeginTabItem()
Expand Down Expand Up @@ -492,6 +494,14 @@ namespace ImGui
IMGUI_API bool Selectable(const char* label, bool selected = false, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0,0)); // "bool selected" carry the selection state (read-only). Selectable() is clicked is returns true so you can modify your selection state. size.x==0.0: use remaining width, size.x>0.0: specify width. size.y==0.0: use label height, size.y>0.0: specify height
IMGUI_API bool Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0,0)); // "bool* p_selected" point to the selection state (read-write), as a convenient helper.

// Multi-selection system for Selectable() and TreeNode() functions.
// This enables standard multi-selection/range-selection idioms (CTRL+Click/Arrow, SHIFT+Click/Arrow, etc) in a way that allow items to be fully clipped (= not submitted at all) when not visible.
// Read comments near ImGuiMultiSelectData for details.
// When enabled, Selectable() and TreeNode() functions will return true when selection needs toggling.
IMGUI_API ImGuiMultiSelectData* BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected);
IMGUI_API ImGuiMultiSelectData* EndMultiSelect();
IMGUI_API void SetNextItemMultiSelectData(void* item_data);

// Widgets: List Boxes
// - FIXME: To be consistent with all the newer API, ListBoxHeader/ListBoxFooter should in reality be called BeginListBox/EndListBox. Will rename them.
IMGUI_API bool ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items = -1);
Expand Down Expand Up @@ -607,6 +617,7 @@ namespace ImGui
IMGUI_API bool IsItemEdited(); // did the last item modify its underlying value this frame? or was pressed? This is generally the same as the "bool" return value of many widgets.
IMGUI_API bool IsItemDeactivated(); // was the last item just made inactive (item was previously active). Useful for Undo/Redo patterns with widgets that requires continuous editing.
IMGUI_API bool IsItemDeactivatedAfterEdit(); // was the last item just made inactive and made a value change when it was active? (e.g. Slider/Drag moved). Useful for Undo/Redo patterns with widgets that requires continuous editing. Note that you may get false positives (some widgets such as Combo()/ListBox()/Selectable() will return true even when clicking an already selected item).
IMGUI_API bool IsItemToggledSelection(); // was the last item selection toggled? (after Selectable(), TreeNode() etc. We only returns toggle _event_ in order to handle clipping correctly)
IMGUI_API bool IsAnyItemHovered();
IMGUI_API bool IsAnyItemActive();
IMGUI_API bool IsAnyItemFocused();
Expand Down Expand Up @@ -1073,6 +1084,7 @@ enum ImGuiStyleVar_
ImGuiStyleVar_ItemSpacing, // ImVec2 ItemSpacing
ImGuiStyleVar_ItemInnerSpacing, // ImVec2 ItemInnerSpacing
ImGuiStyleVar_IndentSpacing, // float IndentSpacing
ImGuiStyleVar_SelectableSpacing, // ImVec2 SelectableSpacing
ImGuiStyleVar_ScrollbarSize, // float ScrollbarSize
ImGuiStyleVar_ScrollbarRounding, // float ScrollbarRounding
ImGuiStyleVar_GrabMinSize, // float GrabMinSize
Expand Down Expand Up @@ -1142,6 +1154,17 @@ enum ImGuiMouseCursor_
#endif
};

// Flags for BeginMultiSelect().
// This system is designed to allow mouse/keyboard multi-selection, including support for range-selection (SHIFT + click) which is difficult to re-implement manually.
// If you disable multi-selection with ImGuiMultiSelectFlags_NoMultiSelect (which is provided for consistency and flexibility), the whole BeginMultiSelect() system
// becomes largely overkill as you can handle single-selection in a simpler manner by just calling Selectable() and reacting on clicks yourself.
enum ImGuiMultiSelectFlags_
{
ImGuiMultiSelectFlags_NoMultiSelect = 1 << 0,
ImGuiMultiSelectFlags_NoUnselect = 1 << 1, // Disable unselecting items with CTRL+Click, CTRL+Space etc.
ImGuiMultiSelectFlags_NoSelectAll = 1 << 2 // Disable CTRL+A shortcut to set RequestSelectAll
};

// Enumateration for ImGui::SetWindow***(), SetNextWindow***(), SetNextTreeNode***() functions
// Represent a condition.
// Important: Treat as a regular enum! Do NOT combine multiple values using binary operators! All the functions above treat 0 as a shortcut to ImGuiCond_Always.
Expand Down Expand Up @@ -1244,6 +1267,7 @@ struct ImGuiStyle
float FrameBorderSize; // Thickness of border around frames. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly).
ImVec2 ItemSpacing; // Horizontal and vertical spacing between widgets/lines.
ImVec2 ItemInnerSpacing; // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label).
ImVec2 SelectableSpacing; // Horizontal and vertical spacing between selectables (by default they are canceling out the effect of ItemSpacing).
ImVec2 TouchExtraPadding; // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much!
float IndentSpacing; // Horizontal indentation when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2).
float ColumnsMinSpacing; // Minimum horizontal spacing between two columns.
Expand Down Expand Up @@ -1672,6 +1696,55 @@ struct ImGuiListClipper
IMGUI_API void End(); // Automatically called on the last call of Step() that returns false.
};

// Abstract:
// - This system implements standard multi-selection idioms (CTRL+Click/Arrow, SHIFT+Click/Arrow, etc) in a way that allow items to be
// fully clipped (= not submitted at all) when not visible. Clipping is typically provided by ImGuiListClipper.
// Handling all of this in a single pass imgui is a little tricky, and this is why we provide those functionalities.
// Note however that if you don't need SHIFT+Click/Arrow range-select, you can handle a simpler form of multi-selection yourself,
// by reacting to click/presses on Selectable() items and checking keyboard modifiers.
// The complexity of this system here is mostly caused by the handling of range-select while optionally allowing to clip elements.
// - The work involved to deal with multi-selection differs whether you want to only submit visible items (and clip others) or submit all items
// regardless of their visibility. Clipping items is more efficient and will allow you to deal with large lists (1k~100k items) with near zero
// performance penalty, but requires a little more work on the code. If you only have a few hundreds elements in your possible selection set,
// you may as well not bother with clipping, as the cost should be negligible (as least on imgui side).
// If you are not sure, always start without clipping and you can work your way to the more optimized version afterwards.
// - The void* Src/Dst value represent a selectable object. They are the values you pass to SetNextItemMultiSelectData().
// Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points. But the code never assume that sortable integers are used.
// - In the spirit of imgui design, your code own the selection data. So this is designed to handle all kind of selection data: instructive (store a bool inside each object),
// external array (store an array aside from your objects), set (store only selected items in a hash/map/set), using intervals (store indices in an interval tree), etc.
// Usage flow:
// 1) Call BeginMultiSelect() with the last saved value of ->RangeSrc and its selection status. As a default value for the initial frame or when,
// resetting your selection state: you may use the value for your first item or a "null" value that matches the type stored in your void*.
// 2) Honor Clear/SelectAll requests by updating your selection data. [Only required if you are using a clipper in step 4]
// 3) Set RangeSrcPassedBy=true if the RangeSrc item is part of the items clipped before the first submitted/visible item. [Only required if you are using a clipper in step 4]
// This is because for range-selection we need to know if we are currently "inside" or "outside" the range.
// If you are using integer indices everywhere, this is easy to compute: if (clipper.DisplayStart > (int)data->RangeSrc) { data->RangeSrcPassedBy = true; }
// 4) Submit your items with SetNextItemMultiSelectData() + Selectable()/TreeNode() calls.
// Call IsItemSelectionToggled() to query with the selection state has been toggled, in which you need the info immediately (before EndMultiSelect()) for your display.
// When cannot reliably return a "IsItemSelected()" value because we need to consider clipped (unprocessed) item, this is why we return a toggle event instead.
// 5) Call EndMultiSelect(). Save the value of ->RangeSrc for the next frame (you may convert the value in a format that is safe for persistance)
// 6) Honor Clear/SelectAll/SetRange requests by updating your selection data. Always process them in this order (as you will receive Clear+SetRange request simultaneously)
// If you submit all items (no clipper), Step 2 and 3 and will be handled by Selectable() on a per-item basis.
struct ImGuiMultiSelectData
{
bool RequestClear; // Begin, End // Request user to clear selection
bool RequestSelectAll; // Begin, End // Request user to select all
bool RequestSetRange; // End // Request user to set or clear selection in the [RangeSrc..RangeDst] range
bool RangeSrcPassedBy; // After Begin // Need to be set by user is RangeSrc was part of the clipped set before submitting the visible items. Ignore if not clipping.
bool RangeValue; // End // End: parameter from RequestSetRange request. True = Select Range, False = Unselect range.
void* RangeSrc; // Begin, End // End: parameter from RequestSetRange request + you need to save this value so you can pass it again next frame. / Begin: this is the value you passed to BeginMultiSelect()
void* RangeDst; // End // End: parameter from RequestSetRange request.
int RangeDirection; // End // End: parameter from RequestSetRange request. +1 if RangeSrc came before RangeDst, -1 otherwise. Available as an indicator in case you cannot infer order from the void* values.

ImGuiMultiSelectData() { Clear(); }
void Clear()
{
RequestClear = RequestSelectAll = RequestSetRange = RangeSrcPassedBy = RangeValue = false;
RangeSrc = RangeDst = NULL;
RangeDirection = 0;
}
};

// Helpers macros to generate 32-bits encoded colors
#ifdef IMGUI_USE_BGRA_PACKED_COLOR
#define IM_COL32_R_SHIFT 16
Expand Down
Loading

0 comments on commit ceeedc5

Please sign in to comment.