Skip to content

Commit

Permalink
Nav: Record/restore preferred position on each given axis.
Browse files Browse the repository at this point in the history
Tagging #6344 #6003 #2694 #1688 as it relates to scoring, however this doesn't technically fix any of them fully yet.
But e.g. once we restore axial path for #2694 this commit will allow going back and forth to initial location.
  • Loading branch information
ocornut committed May 9, 2023
1 parent 39f7248 commit 6656553
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 15 deletions.
4 changes: 4 additions & 0 deletions docs/CHANGELOG.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ Other changes:
- Drag, Sliders: if the format string doesn't contain any %, CTRL+Click to input text will
use the default format specifier for the type. Allow display/input of raw value when using
"enums" patterns (display label instead of value) + allow using when value is hidden. (#6405)
- Nav: Record/restore preferred position on each given axis after a movement on that axis,
then score movement on the other axis using this as a bias. This allows going up and down
between e.g. a large header spanning horizontal space and three-ways-columns, landing
on the same column as before.
- Nav: Fixed navigation within tables/columns where item boundaries goes beyond columns limits,
unclipped bounding boxes would interfere with other columns. (#2221) [@zzzyap, @ocornut]
- Nav: Fixed CTRL+Tab into a root window with only childs with _NavFlattened flags
Expand Down
87 changes: 73 additions & 14 deletions imgui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3688,6 +3688,7 @@ ImGuiWindow::ImGuiWindow(ImGuiContext* ctx, const char* name) : DrawListInst(NUL
DrawList = &DrawListInst;
DrawList->_Data = &Ctx->DrawListSharedData;
DrawList->_OwnerName = Name;
NavPreferredScoringPosRel[0] = NavPreferredScoringPosRel[1] = ImVec2(FLT_MAX, FLT_MAX);
}

ImGuiWindow::~ImGuiWindow()
Expand Down Expand Up @@ -10620,6 +10621,12 @@ void ImGui::SetNavWindow(ImGuiWindow* window)
NavUpdateAnyRequestFlag();
}

void ImGui::NavClearPreferredPosForAxis(ImGuiAxis axis)
{
ImGuiContext& g = *GImGui;
g.NavWindow->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer][axis] = FLT_MAX;
}

void ImGui::SetNavID(ImGuiID id, ImGuiNavLayer nav_layer, ImGuiID focus_scope_id, const ImRect& rect_rel)
{
ImGuiContext& g = *GImGui;
Expand All @@ -10630,6 +10637,10 @@ void ImGui::SetNavID(ImGuiID id, ImGuiNavLayer nav_layer, ImGuiID focus_scope_id
g.NavFocusScopeId = focus_scope_id;
g.NavWindow->NavLastIds[nav_layer] = id;
g.NavWindow->NavRectRel[nav_layer] = rect_rel;

// Clear preferred scoring position (NavMoveRequestApplyResult() will tend to restore it)
NavClearPreferredPosForAxis(ImGuiAxis_X);
NavClearPreferredPosForAxis(ImGuiAxis_Y);
}

void ImGui::SetFocusID(ImGuiID id, ImGuiWindow* window)
Expand All @@ -10654,6 +10665,10 @@ void ImGui::SetFocusID(ImGuiID id, ImGuiWindow* window)
g.NavDisableMouseHover = true;
else
g.NavDisableHighlight = true;

// Clear preferred scoring position (NavMoveRequestApplyResult() will tend to restore it)
NavClearPreferredPosForAxis(ImGuiAxis_X);
NavClearPreferredPosForAxis(ImGuiAxis_Y);
}

ImGuiDir ImGetDirQuadrantFromDelta(float dx, float dy)
Expand Down Expand Up @@ -10746,16 +10761,22 @@ static bool ImGui::NavScoreItem(ImGuiNavItemData* result)
draw_list->AddText(cand.Min, IM_COL32(255, 255, 255, 255), buf);
}
}
if (IsMouseHoveringRect(cand.Min, cand.Max))
const bool debug_hovering = IsMouseHoveringRect(cand.Min, cand.Max);
const bool debug_tty = (g.IO.KeyCtrl && IsKeyPressed(ImGuiKey_Space));
if (debug_hovering || debug_tty)
{
ImFormatString(buf, IM_ARRAYSIZE(buf),
"d-box (%7.3f,%7.3f) -> %7.3f\nd-center (%7.3f,%7.3f) -> %7.3f\nd-axial (%7.3f,%7.3f) -> %7.3f\nnav %c, quadrant %c",
dbx, dby, dist_box, dcx, dcy, dist_center, dax, day, dist_axial, "WENS"[g.NavMoveDir], "WENS"[quadrant]);
ImDrawList* draw_list = GetForegroundDrawList(window);
draw_list->AddRect(curr.Min, curr.Max, IM_COL32(255,200,0,100));
draw_list->AddRect(cand.Min, cand.Max, IM_COL32(255,255,0,200));
draw_list->AddRectFilled(cand.Max - ImVec2(4, 4), cand.Max + CalcTextSize(buf) + ImVec2(4, 4), IM_COL32(40,0,0,200));
draw_list->AddText(cand.Max, ~0U, buf);
dbx, dby, dist_box, dcx, dcy, dist_center, dax, day, dist_axial, "-WENS"[move_dir+1], "-WENS"[quadrant+1]);
if (debug_hovering)
{
ImDrawList* draw_list = GetForegroundDrawList(window);
draw_list->AddRect(curr.Min, curr.Max, IM_COL32(255, 200, 0, 100));
draw_list->AddRect(cand.Min, cand.Max, IM_COL32(255, 255, 0, 200));
draw_list->AddRectFilled(cand.Max - ImVec2(4, 4), cand.Max + CalcTextSize(buf) + ImVec2(4, 4), IM_COL32(40, 0, 0, 200));
draw_list->AddText(cand.Max, ~0U, buf);
}
if (debug_tty) { IMGUI_DEBUG_LOG_NAV("id 0x%08X\n%s\n", g.LastItemData.ID, buf); }
}
#endif

Expand Down Expand Up @@ -11322,11 +11343,11 @@ static void ImGui::NavUpdate()
// [DEBUG]
g.NavScoringDebugCount = 0;
#if IMGUI_DEBUG_NAV_RECTS
if (g.NavWindow)
if (ImGuiWindow* debug_window = g.NavWindow)
{
ImDrawList* draw_list = GetForegroundDrawList(g.NavWindow);
if (1) { for (int layer = 0; layer < 2; layer++) { ImRect r = WindowRectRelToAbs(g.NavWindow, g.NavWindow->NavRectRel[layer]); draw_list->AddRect(r.Min, r.Max, IM_COL32(255,200,0,255)); } } // [DEBUG]
if (1) { ImU32 col = (!g.NavWindow->Hidden) ? IM_COL32(255,0,255,255) : IM_COL32(255,0,0,255); ImVec2 p = NavCalcPreferredRefPos(); char buf[32]; ImFormatString(buf, 32, "%d", g.NavLayer); draw_list->AddCircleFilled(p, 3.0f, col); draw_list->AddText(NULL, 13.0f, p + ImVec2(8,-4), col, buf); }
ImDrawList* draw_list = GetForegroundDrawList(debug_window);
int layer = g.NavLayer; /* for (int layer = 0; layer < 2; layer++)*/ { ImRect r = WindowRectRelToAbs(debug_window, debug_window->NavRectRel[layer]); draw_list->AddRect(r.Min, r.Max, IM_COL32(255, 200, 0, 255)); }
//if (1) { ImU32 col = (!debug_window->Hidden) ? IM_COL32(255,0,255,255) : IM_COL32(255,0,0,255); ImVec2 p = NavCalcPreferredRefPos(); char buf[32]; ImFormatString(buf, 32, "%d", g.NavLayer); draw_list->AddCircleFilled(p, 3.0f, col); draw_list->AddText(NULL, 13.0f, p + ImVec2(8,-4), col, buf); }
}
#endif
}
Expand All @@ -11347,6 +11368,28 @@ void ImGui::NavInitRequestApplyResult()
NavRestoreHighlightAfterMove();
}

// Bias scoring rect ahead of scoring + update preferred pos (if missing) using source position
static void NavBiasScoringRect(ImRect& r, ImVec2& preferred_pos_rel, ImGuiDir move_dir)
{
// Bias initial rect
ImGuiContext& g = *GImGui;
const ImVec2 rel_to_abs_offset = g.NavWindow->DC.CursorStartPos;

// Initialize bias on departure if we don't have any. So mouse-click + arrow will record bias.
// - We default to L/U bias, so moving down from a large source item into several columns will land on left-most column.
// - But each successful move sets new bias on one axis, only cleared when using mouse.
if (preferred_pos_rel.x == FLT_MAX)
preferred_pos_rel.x = ImMin(r.Min.x + 1.0f, r.Max.x) - rel_to_abs_offset.x;
if (preferred_pos_rel.y == FLT_MAX)
preferred_pos_rel.y = r.GetCenter().y - rel_to_abs_offset.y;

// Apply general bias on the other axis
if (move_dir == ImGuiDir_Up || move_dir == ImGuiDir_Down)
r.Min.x = r.Max.x = preferred_pos_rel.x + rel_to_abs_offset.x;
else
r.Min.y = r.Max.y = preferred_pos_rel.y + rel_to_abs_offset.y;
}

void ImGui::NavUpdateCreateMoveRequest()
{
ImGuiContext& g = *GImGui;
Expand Down Expand Up @@ -11453,8 +11496,8 @@ void ImGui::NavUpdateCreateMoveRequest()
ImRect nav_rect_rel = !window->NavRectRel[g.NavLayer].IsInverted() ? window->NavRectRel[g.NavLayer] : ImRect(0, 0, 0, 0);
scoring_rect = WindowRectRelToAbs(window, nav_rect_rel);
scoring_rect.TranslateY(scoring_rect_offset_y);
scoring_rect.Min.x = ImMin(scoring_rect.Min.x + 1.0f, scoring_rect.Max.x);
scoring_rect.Max.x = scoring_rect.Min.x;
if (g.NavMoveSubmitted)
NavBiasScoringRect(scoring_rect, window->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer], g.NavMoveDir);
IM_ASSERT(!scoring_rect.IsInverted()); // Ensure if we have a finite, non-inverted bounding box here will allow us to remove extraneous ImFabs() calls in NavScoreItem().
//GetForegroundDrawList()->AddRect(scoring_rect.Min, scoring_rect.Max, IM_COL32(255,200,0,255)); // [DEBUG]
//if (!g.NavScoringNoClipRect.IsInverted()) { GetForegroundDrawList()->AddRect(g.NavScoringNoClipRect.Min, g.NavScoringNoClipRect.Max, IM_COL32(255, 200, 0, 255)); } // [DEBUG]
Expand Down Expand Up @@ -11508,12 +11551,14 @@ void ImGui::NavMoveRequestApplyResult()
result = &g.NavTabbingResultFirst;

// In a situation when there are no results but NavId != 0, re-enable the Navigation highlight (because g.NavId is not considered as a possible result)
const ImGuiAxis axis = (g.NavMoveDir == ImGuiDir_Up || g.NavMoveDir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X;
if (result == NULL)
{
if (g.NavMoveFlags & ImGuiNavMoveFlags_Tabbing)
g.NavMoveFlags |= ImGuiNavMoveFlags_DontSetNavHighlight;
if (g.NavId != 0 && (g.NavMoveFlags & ImGuiNavMoveFlags_DontSetNavHighlight) == 0)
NavRestoreHighlightAfterMove();
NavClearPreferredPosForAxis(axis); // On a failed move, clear preferred pos for this axis.
IMGUI_DEBUG_LOG_NAV("[nav] NavMoveSubmitted but not led to a result!\n");
return;
}
Expand Down Expand Up @@ -11558,10 +11603,19 @@ void ImGui::NavMoveRequestApplyResult()
g.NavJustMovedToKeyMods = g.NavMoveKeyMods;
}

// Focus
// Apply new NavID/Focus
IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequest: result NavID 0x%08X in Layer %d Window \"%s\"\n", result->ID, g.NavLayer, g.NavWindow->Name);
ImVec2 preferred_scoring_pos_rel = g.NavWindow->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer];
SetNavID(result->ID, g.NavLayer, result->FocusScopeId, result->RectRel);

// Restore last preferred position for current axis
// (storing in RootWindowForNav-> as the info is desirable at the beginning of a Move Request. In theory all storage should use RootWindowForNav..)
if ((g.NavMoveFlags & ImGuiNavMoveFlags_Tabbing) == 0)
{
preferred_scoring_pos_rel[axis] = result->RectRel.GetCenter()[axis];
g.NavWindow->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer] = preferred_scoring_pos_rel;
}

// Tabbing: Activates Inputable or Focus non-Inputable
if ((g.NavMoveFlags & ImGuiNavMoveFlags_Tabbing) && (result->InFlags & ImGuiItemFlags_Inputable))
{
Expand Down Expand Up @@ -11775,6 +11829,8 @@ static void ImGui::NavUpdateCreateWrappingRequest()
if (!do_forward)
return;
window->NavRectRel[g.NavLayer] = bb_rel;
NavClearPreferredPosForAxis(ImGuiAxis_X);
NavClearPreferredPosForAxis(ImGuiAxis_Y);
NavMoveRequestForward(g.NavMoveDir, clip_dir, move_flags, g.NavMoveScrollFlags);
}

Expand Down Expand Up @@ -14285,6 +14341,9 @@ void ImGui::DebugNodeWindow(ImGuiWindow* window, const char* label)
BulletText("NavLastIds[%d]: 0x%08X at +(%.1f,%.1f)(%.1f,%.1f)", layer, window->NavLastIds[layer], r.Min.x, r.Min.y, r.Max.x, r.Max.y);
DebugLocateItemOnHover(window->NavLastIds[layer]);
}
const ImVec2* pr = window->NavPreferredScoringPosRel;
for (int layer = 0; layer < ImGuiNavLayer_COUNT; layer++)
BulletText("NavPreferredScoringPosRel[%d] = {%.1f,%.1f)", layer, (pr[layer].x == FLT_MAX ? -99999.0f : pr[layer].x), (pr[layer].y == FLT_MAX ? -99999.0f : pr[layer].y)); // Display as 99999.0f so it looks neater.
BulletText("NavLayersActiveMask: %X, NavLastChildNavWindow: %s", window->DC.NavLayersActiveMask, window->NavLastChildNavWindow ? window->NavLastChildNavWindow->Name : "NULL");
if (window->RootWindow != window) { DebugNodeWindow(window->RootWindow, "RootWindow"); }
if (window->ParentWindow != NULL) { DebugNodeWindow(window->ParentWindow, "ParentWindow"); }
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.6 WIP"
#define IMGUI_VERSION_NUM 18955
#define IMGUI_VERSION_NUM 18956
#define IMGUI_HAS_TABLE

/*
Expand Down
3 changes: 3 additions & 0 deletions imgui_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -2366,6 +2366,7 @@ struct IMGUI_API ImGuiWindow
ImGuiWindow* NavLastChildNavWindow; // When going to the menu bar, we remember the child window we came from. (This could probably be made implicit if we kept g.Windows sorted by last focused including child window.)
ImGuiID NavLastIds[ImGuiNavLayer_COUNT]; // Last known NavId for this window, per layer (0/1)
ImRect NavRectRel[ImGuiNavLayer_COUNT]; // Reference rectangle, in window relative space
ImVec2 NavPreferredScoringPosRel[ImGuiNavLayer_COUNT]; // Preferred X/Y position updated when moving on a given axis, reset to FLT_MAX.
ImGuiID NavRootFocusScopeId; // Focus Scope ID at the time of Begin()

int MemoryDrawListIdxCapacity; // Backup of last idx/vtx count, so when waking up the window we can preallocate and avoid iterative alloc/copy
Expand Down Expand Up @@ -2761,6 +2762,7 @@ namespace ImGui
IMGUI_API void SetWindowHiddendAndSkipItemsForCurrentFrame(ImGuiWindow* window);
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); }

// Windows: Display Order and Focus Order
IMGUI_API void FocusWindow(ImGuiWindow* window, ImGuiFocusRequestFlags flags = 0);
Expand Down Expand Up @@ -2908,6 +2910,7 @@ namespace ImGui
IMGUI_API void NavMoveRequestCancel();
IMGUI_API void NavMoveRequestApplyResult();
IMGUI_API void NavMoveRequestTryWrapping(ImGuiWindow* window, ImGuiNavMoveFlags move_flags);
IMGUI_API void NavClearPreferredPosForAxis(ImGuiAxis axis);
IMGUI_API void NavUpdateCurrentWindowIsScrollPushableX();
IMGUI_API void ActivateItem(ImGuiID id); // Remotely activate a button, checkbox, tree node etc. given its unique ID. activation is queued and processed on the next frame when the item is encountered again.
IMGUI_API void SetNavWindow(ImGuiWindow* window);
Expand Down
2 changes: 2 additions & 0 deletions imgui_widgets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6219,11 +6219,13 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l
if (g.NavId == id && g.NavMoveDir == ImGuiDir_Left && is_open)
{
toggled = true;
NavClearPreferredPosForAxis(ImGuiAxis_X);
NavMoveRequestCancel();
}
if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right && !is_open) // If there's something upcoming on the line we may want to give it the priority?
{
toggled = true;
NavClearPreferredPosForAxis(ImGuiAxis_X);
NavMoveRequestCancel();
}

Expand Down

0 comments on commit 6656553

Please sign in to comment.