Skip to content

Commit

Permalink
Inputs: added wip/experiment routing system: Shortcut(), RoutingFocus…
Browse files Browse the repository at this point in the history
…ed policy, SetShortcutRouting(). (#456, #2637, #3724)

- InputText() uses Shortcut().
  • Loading branch information
ocornut committed Nov 8, 2022
1 parent c59ebb2 commit 7ff2d33
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 18 deletions.
201 changes: 199 additions & 2 deletions imgui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4116,6 +4116,45 @@ static void UpdateAliasKey(ImGuiKey key, bool v, float analog_value)
key_data->AnalogValue = analog_value;
}

// Rewrite routing data buffers to strip old entries + sort by key to make queries not touch scattered data.
// Entries D,A,B,B,A,C,B --> A,A,B,B,B,C,D
// Index A:1 B:2 C:5 D:0 --> A:0 B:2 C:5 D:6
// See 'Metrics->Key Owners & Shortcut Routing' to visualize the result of that operation.
static void UpdateKeyRoutingTable(ImGuiKeyRoutingTable* rt)
{
ImGuiContext& g = *GImGui;
rt->EntriesNext.resize(0);
for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1))
{
const int new_routing_start_idx = rt->EntriesNext.Size;
ImGuiKeyRoutingData* routing_entry;
for (int old_routing_idx = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; old_routing_idx != -1; old_routing_idx = routing_entry->NextEntryIndex)
{
routing_entry = &rt->Entries[old_routing_idx];
routing_entry->RoutingCurr = routing_entry->RoutingNext; // Update entry
routing_entry->RoutingNext = ImGuiKeyOwner_None;
routing_entry->RoutingNextScore = 255;
if (routing_entry->RoutingCurr == ImGuiKeyOwner_None)
continue;
rt->EntriesNext.push_back(*routing_entry); // Write alive ones into new buffer

// Apply routing to owner if there's no owner already (RoutingCurr == None at this point)
if (routing_entry->Mods == g.IO.KeyMods)
{
ImGuiKeyOwnerData* owner_data = ImGui::GetKeyOwnerData(key);
if (owner_data->OwnerCurr == ImGuiKeyOwner_None)
owner_data->OwnerCurr = routing_entry->RoutingCurr;
}
}

// Rewrite linked-list
rt->Index[key - ImGuiKey_NamedKey_BEGIN] = (ImGuiKeyRoutingIndex)(new_routing_start_idx < rt->EntriesNext.Size ? new_routing_start_idx : -1);
for (int n = new_routing_start_idx; n < rt->EntriesNext.Size; n++)
rt->EntriesNext[n].NextEntryIndex = (ImGuiKeyRoutingIndex)((n + 1 < rt->EntriesNext.Size) ? n + 1 : -1);
}
rt->Entries.swap(rt->EntriesNext); // Swap new and old indexes
}

// [Internal] Do not use directly (should read io.KeyMods instead)
static ImGuiKeyChord GetMergedModsFromBools()
{
Expand Down Expand Up @@ -4234,6 +4273,8 @@ static void ImGui::UpdateKeyboardInputs()
owner_data->OwnerNext = ImGuiKeyOwner_None;
owner_data->LockThisFrame = owner_data->LockUntilRelease = owner_data->LockUntilRelease && key_data->Down; // Clear LockUntilRelease when key is not Down anymore
}

UpdateKeyRoutingTable(&g.KeysRoutingTable);
}

static void ImGui::UpdateMouseInputs()
Expand Down Expand Up @@ -4794,6 +4835,9 @@ void ImGui::Shutdown()
g.HoveredWindow = g.HoveredWindowUnderMovingWindow = NULL;
g.ActiveIdWindow = g.ActiveIdPreviousFrameWindow = NULL;
g.MovingWindow = NULL;

g.KeysRoutingTable.Clear();

g.ColorStack.clear();
g.StyleVarStack.clear();
g.FontStack.clear();
Expand Down Expand Up @@ -7955,6 +7999,128 @@ ImVec2 ImGui::GetKeyVector2d(ImGuiKey key_left, ImGuiKey key_right, ImGuiKey key
GetKeyData(key_down)->AnalogValue - GetKeyData(key_up)->AnalogValue);
}

// owner_id may be None/Any, but routing_id needs to be always be set, so we default to GetID("") aka current ID stack.
static inline ImGuiID GetRoutingIdFromOwnerId(ImGuiID owner_id)
{
ImGuiContext& g = *GImGui;
return (owner_id != ImGuiKeyOwner_None && owner_id != ImGuiKeyOwner_Any) ? owner_id : g.CurrentWindow->IDStack.back();
}

ImGuiKeyRoutingData* ImGui::GetShortcutRoutingData(ImGuiKeyChord key_chord)
{
// Majority of shortcuts will be Key + any number of Mods
// We accept _Single_ mod with ImGuiKey_None.
// - Shortcut(ImGuiKey_S | ImGuiMod_Ctrl); // Legal
// - Shortcut(ImGuiKey_S | ImGuiMod_Ctrl | ImGuiMod_Shift); // Legal
// - Shortcut(ImGuiMod_Ctrl); // Legal
// - Shortcut(ImGuiMod_Ctrl | ImGuiMod_Shift); // Not legal
ImGuiContext& g = *GImGui;
ImGuiKeyRoutingTable* rt = &g.KeysRoutingTable;
ImGuiKeyRoutingData* routing_data;
ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_);
ImGuiKey mods = (ImGuiKey)(key_chord & ImGuiMod_Mask_);
if (key == ImGuiKey_None)
key = ConvertSingleModFlagToKey(mods);
IM_ASSERT(IsNamedKey(key));

// Get (in the majority of case, the linked list will have one element so this should be 2 reads.
// Subsequent elements will be contiguous in memory as list is sorted/rebuilt in NewFrame).
for (ImGuiKeyRoutingIndex idx = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; idx != -1; idx = routing_data->NextEntryIndex)
{
routing_data = &rt->Entries[idx];
if (routing_data->Mods == mods)
return routing_data;
}

// Add
ImGuiKeyRoutingIndex idx = (ImGuiKeyRoutingIndex)rt->Entries.Size;
rt->Entries.push_back(ImGuiKeyRoutingData());
routing_data = &rt->Entries[idx];
routing_data->Mods = (ImU16)mods;
routing_data->NextEntryIndex = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; // Setup linked list
rt->Index[key - ImGuiKey_NamedKey_BEGIN] = idx;
return routing_data;
}

// Request a desired route for an input chord (key + mods).
// Return true if the route is available this frame.
// - Routes and key ownership are attributed at the beginning of next frame based on best score and mod state.
// (Conceptually this does a "Submit for next frame" + "Test for current frame".
// As such, it could be called TrySetXXX or SubmitXXX, or the Submit and Test operations should be separate.)
// - Using 'owner_id == ImGuiKeyOwner_Any/0': auto-assign an owner based on current focus scope (each window has its focus scope by default)
// - Using 'owner_id == ImGuiKeyOwner_None': allows disabling/locking a shortcut.
bool ImGui::SetShortcutRouting(ImGuiKeyChord key_chord, ImGuiID owner_id, ImGuiInputFlags flags, ImGuiWindow* location)
{
ImGuiContext& g = *GImGui;
IM_ASSERT(owner_id != ImGuiKeyOwner_None);

// Calculate our score
int score = 255;
bool always_set_next_route = false;
if (flags & ImGuiInputFlags_RouteFocused)
{
if (location == NULL)
location = g.CurrentWindow;
ImGuiWindow* focused = g.NavWindow;

if (g.ActiveId != 0 && g.ActiveId == owner_id)
{
// ActiveID gets top priority (0)
// (we don't check g.ActiveIdUsingAllKeys here. Routing is applied but if input ownership is tested later it may discard it)
score = 0;
}
else if (focused != NULL && focused->RootWindow == location->RootWindow) // Early out
{
// Score based on distance to focused window (lower is better)
// Assuming both windows are submitting a routing request,
// - When WindowA...... is focused -> WindowA scores 1 (best), WindowA/ChildB scores 255 (no match)
// - When Window/ChildB is focused -> WindowA scores 2, WindowA/ChildB scores 1 (best)
// Assuming only WindowA is submitting a routing request,
// - When Window/ChildB is focused -> WindowA scores 2 (best), WindowA/ChildB doesn't have a scoe.
for (int next_score = 1; focused != NULL; next_score++)
{
if (focused == location)
{
score = (ImU8)next_score;
break;
}
focused = (focused->RootWindow != focused) ? focused->ParentWindow : NULL; // FIXME: This could be later abstracted as a focus path
}
}

if (score == 255)
return false;
}
else
{
score = 0;
always_set_next_route = true;
}

// Submit routing for NEXT frame (assuming score is sufficient)
// FIXME: Could expose a way to use a "serve last" policy for same score resolution (using <= instead of <).
ImGuiKeyRoutingData* routing_data = GetShortcutRoutingData(key_chord);
const ImGuiID routing_id = GetRoutingIdFromOwnerId(owner_id); // FIXME: Location
//const bool set_route = (flags & ImGuiInputFlags_ServeLast) ? (score <= routing_data->RoutingNextScore) : (score < routing_data->RoutingNextScore);
if (score < routing_data->RoutingNextScore || always_set_next_route)
{
routing_data->RoutingNext = routing_id;
routing_data->RoutingNextScore = (ImU8)score;
}

// Return routing state for CURRENT frame
return routing_data->RoutingCurr == routing_id;
}

// Currently unused by core (but used by tests)
// Note: this cannot be turned into GetShortcutRouting() because we do the owner_id->routing_id translation, name would be more misleading.
bool ImGui::TestShortcutRouting(ImGuiKeyChord key_chord, ImGuiID owner_id)
{
const ImGuiID routing_id = GetRoutingIdFromOwnerId(owner_id);
ImGuiKeyRoutingData* routing_data = GetShortcutRoutingData(key_chord);
return routing_data->RoutingCurr == routing_id;
}

// Note that Dear ImGui doesn't know the meaning/semantic of ImGuiKey from 0..511: they are legacy native keycodes.
// Consider transitioning from 'IsKeyDown(MY_ENGINE_KEY_A)' (<1.87) to IsKeyDown(ImGuiKey_A) (>= 1.87)
bool ImGui::IsKeyDown(ImGuiKey key)
Expand Down Expand Up @@ -7986,6 +8152,7 @@ bool ImGui::IsKeyPressed(ImGuiKey key, ImGuiID owner_id, ImGuiInputFlags flags)
const float t = key_data->DownDuration;
if (t < 0.0f)
return false;
IM_ASSERT((flags & ~ImGuiInputFlags_SupportedByIsKeyPressed) == 0); // Passing flags not supported by this function!

bool pressed = (t == 0.0f);
if (!pressed && ((flags & ImGuiInputFlags_Repeat) != 0))
Expand Down Expand Up @@ -8044,6 +8211,7 @@ bool ImGui::IsMouseClicked(ImGuiMouseButton button, ImGuiID owner_id, ImGuiInput
const float t = g.IO.MouseDownDuration[button];
if (t < 0.0f)
return false;
IM_ASSERT((flags & ~ImGuiInputFlags_SupportedByIsKeyPressed) == 0); // Passing flags not supported by this function!

const bool repeat = (flags & ImGuiInputFlags_Repeat) != 0;
const bool pressed = (t == 0.0f) || (repeat && t > g.IO.KeyRepeatDelay && CalcTypematicRepeatAmount(t - g.IO.DeltaTime, t, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0);
Expand Down Expand Up @@ -8392,6 +8560,7 @@ bool ImGui::TestKeyOwner(ImGuiKey key, ImGuiID owner_id)
void ImGui::SetKeyOwner(ImGuiKey key, ImGuiID owner_id, ImGuiInputFlags flags)
{
IM_ASSERT(IsNamedKeyOrModKey(key) && (owner_id != ImGuiKeyOwner_Any || (flags & (ImGuiInputFlags_LockThisFrame | ImGuiInputFlags_LockUntilRelease)))); // Can only use _Any with _LockXXX flags (to eat a key away without an ID to retrieve it)
IM_ASSERT((flags & ~ImGuiInputFlags_SupportedBySetKeyOwner) == 0); // Passing flags not supported by this function!

ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(key);
owner_data->OwnerCurr = owner_data->OwnerNext = owner_id;
Expand All @@ -8417,14 +8586,22 @@ void ImGui::SetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags)
if ((flags & ImGuiInputFlags_CondMask_) == 0)
flags |= ImGuiInputFlags_CondDefault_;
if ((g.HoveredId == id && (flags & ImGuiInputFlags_CondHovered)) || (g.ActiveId == id && (flags & ImGuiInputFlags_CondActive)))
SetKeyOwner(key, id, flags);
{
IM_ASSERT((flags & ~ImGuiInputFlags_SupportedBySetItemKeyOwner) == 0); // Passing flags not supported by this function!
SetKeyOwner(key, id, flags & ~ImGuiInputFlags_CondMask_);
}
}

// - Need to decide how to handle shortcut translations for Non-Mac <> Mac
// - Ideas: https://github.com/ocornut/imgui/issues/456#issuecomment-264390864
bool ImGui::Shortcut(ImGuiKeyChord key_chord, ImGuiID owner_id, ImGuiInputFlags flags)
{
ImGuiContext& g = *GImGui;

if (flags & ImGuiInputFlags_RouteFocused)
if (!SetShortcutRouting(key_chord, owner_id, ImGuiInputFlags_RouteFocused, g.CurrentWindow))
return false;

ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_);
ImGuiKey mods = (ImGuiKey)(key_chord & ImGuiMod_Mask_);
if (g.IO.KeyMods != mods)
Expand All @@ -8436,6 +8613,8 @@ bool ImGui::Shortcut(ImGuiKeyChord key_chord, ImGuiID owner_id, ImGuiInputFlags

if (!IsKeyPressed(key, owner_id, (flags & (ImGuiInputFlags_Repeat | ImGuiInputFlags_RepeatRateMask_))))
return false;
IM_ASSERT((flags & ~ImGuiInputFlags_SupportedByShortcut) == 0); // Passing flags not supported by this function!

return true;
}

Expand Down Expand Up @@ -13080,7 +13259,7 @@ void ImGui::ShowMetricsWindow(bool* p_open)
TreePop();
}

if (TreeNode("Key Owners"))
if (TreeNode("Key Owners & Shortcut Routing"))
{
TextUnformatted("Key Owners:");
if (BeginListBox("##owners", ImVec2(-FLT_MIN, GetTextLineHeightWithSpacing() * 8)))
Expand All @@ -13096,6 +13275,24 @@ void ImGui::ShowMetricsWindow(bool* p_open)
}
EndListBox();
}
TextUnformatted("Shortcut Routing:");
if (BeginListBox("##routes", ImVec2(-FLT_MIN, GetTextLineHeightWithSpacing() * 8)))
{
for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1))
{
ImGuiKeyRoutingTable* rt = &g.KeysRoutingTable;
for (ImGuiKeyRoutingIndex idx = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; idx != -1; )
{
char key_chord_name[64];
ImGuiKeyRoutingData* routing_data = &rt->Entries[idx];
GetKeyChordName(key | routing_data->Mods, key_chord_name, IM_ARRAYSIZE(key_chord_name));
Text("%s: 0x%08X", key_chord_name, routing_data->RoutingCurr);
DebugLocateItemOnHover(routing_data->RoutingCurr);
idx = routing_data->NextEntryIndex;
}
}
EndListBox();
}
Text("(ActiveIdUsing: AllKeyboardKeys: %d, NavDirMask: 0x%X)", g.ActiveIdUsingAllKeyboardKeys, g.ActiveIdUsingNavDirMask);
TreePop();
}
Expand Down
2 changes: 1 addition & 1 deletion imgui.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
// Library Version
// (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM > 12345')
#define IMGUI_VERSION "1.89 WIP"
#define IMGUI_VERSION_NUM 18836
#define IMGUI_VERSION_NUM 18837
#define IMGUI_HAS_TABLE

/*
Expand Down
Loading

0 comments on commit 7ff2d33

Please sign in to comment.