diff --git a/src/core/fullscreenui_widgets.cpp b/src/core/fullscreenui_widgets.cpp index bc131607f..c4b0c35ec 100644 --- a/src/core/fullscreenui_widgets.cpp +++ b/src/core/fullscreenui_widgets.cpp @@ -54,6 +54,13 @@ static constexpr float MENU_BACKGROUND_ANIMATION_TIME = 0.5f; static constexpr float SMOOTH_SCROLLING_SPEED = 3.5f; static constexpr u32 LOADING_PROGRESS_SAMPLE_COUNT = 30; +enum class SplitWindowFocusChange : u8 +{ + None, + FocusSidebar, + FocusContent, +}; + static std::optional LoadTextureImage(std::string_view path, u32 svg_width, u32 svg_height); static std::shared_ptr UploadTexture(std::string_view path, const Image& image); @@ -329,6 +336,7 @@ struct WidgetsState s32 enum_choice_button_value = 0; bool enum_choice_button_set = false; + SplitWindowFocusChange split_window_focus_change = SplitWindowFocusChange::None; bool had_hovered_menu_item = false; bool has_hovered_menu_item = false; bool rendered_menu_item_border = false; @@ -1188,6 +1196,11 @@ void FullscreenUI::QueueResetFocus(FocusResetType type) GImGui->NavScoringNoClipRect = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX); } +void FullscreenUI::CancelResetFocus() +{ + s_state.focus_reset_queued = FocusResetType::None; +} + bool FullscreenUI::ResetFocusHere() { if (s_state.focus_reset_queued == FocusResetType::None) @@ -1210,10 +1223,15 @@ bool FullscreenUI::ResetFocusHere() ImGui::SetWindowFocus(); // If this is a popup closing, we don't want to reset the current nav item, since we were presumably opened by one. - if (s_state.focus_reset_queued != FocusResetType::PopupClosed) + if (s_state.focus_reset_queued != FocusResetType::PopupClosed && + s_state.focus_reset_queued != FocusResetType::SplitWindowChanged) + { ImGui::NavInitWindow(window, true); + } else + { ImGui::SetNavWindow(window); + } // prevent any sound from playing on the nav change s_state.had_focus_reset = true; @@ -1234,7 +1252,8 @@ bool FullscreenUI::IsFocusResetFromWindowChange() { return (s_state.focus_reset_queued != FocusResetType::None && s_state.focus_reset_queued != FocusResetType::PopupOpened && - s_state.focus_reset_queued != FocusResetType::PopupClosed); + s_state.focus_reset_queued != FocusResetType::PopupClosed && + s_state.focus_reset_queued != FocusResetType::SplitWindowChanged); } FullscreenUI::FocusResetType FullscreenUI::GetQueuedFocusResetType() @@ -3263,6 +3282,216 @@ bool FullscreenUI::HorizontalMenuItem(GPUTexture* icon, std::string_view title, return pressed; } +bool FullscreenUI::BeginSplitWindow(const ImVec2& position, const ImVec2& size, const char* name, + const ImVec4& background /*= UIStyle.BackgroundColor*/, float rounding /*= 0.0f*/, + const ImVec2& padding /*= ImVec2()*/, ImGuiWindowFlags flags /*= 0*/) +{ + const bool ret = BeginFullscreenWindow(position, size, name, background, rounding, padding, flags); + BeginInnerSplitWindow(); + return ret; +} + +void FullscreenUI::EndSplitWindow() +{ + EndInnerSplitWindow(); + EndFullscreenWindow(); +} + +void FullscreenUI::BeginInnerSplitWindow() +{ + ImGuiWindow* const window = ImGui::GetCurrentWindow(); + window->DC.LayoutType = ImGuiLayoutType_Horizontal; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, LayoutScale(10.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, LayoutScale(LAYOUT_MENU_ITEM_BORDER_ROUNDING)); + + s_state.split_window_focus_change = SplitWindowFocusChange::None; + + if (!ImGui::IsPopupOpen(0u, ImGuiPopupFlags_AnyPopup)) + { + if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadLeft, true) || ImGui::IsKeyPressed(ImGuiKey_LeftArrow, true)) + { + s_state.split_window_focus_change = SplitWindowFocusChange::FocusSidebar; + QueueResetFocus(FocusResetType::SplitWindowChanged); + } + else if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadRight, true) || ImGui::IsKeyPressed(ImGuiKey_RightArrow, true)) + { + s_state.split_window_focus_change = SplitWindowFocusChange::FocusContent; + QueueResetFocus(FocusResetType::SplitWindowChanged); + } + } +} + +void FullscreenUI::EndInnerSplitWindow() +{ + ImGui::PopStyleVar(4); +} + +bool FullscreenUI::BeginSplitWindowSidebar(float sidebar_width /*= 0.2f*/) +{ + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(10.0f, 10.0f)); + ImGui::PushStyleColor(ImGuiCol_ChildBg, DarkerColor(ImGui::GetStyle().Colors[ImGuiCol_WindowBg], 1.2f)); + + if (IsFocusResetFromWindowChange()) + ImGui::SetNextWindowScroll(ImVec2(0.0f, 0.0f)); + + const float sidebar_width_px = ImFloor(ImGui::GetCurrentWindowRead()->WorkRect.GetWidth() * sidebar_width); + if (!ImGui::BeginChild("sidebar", ImVec2(sidebar_width_px, 0.0f))) + return false; + + if (ImGui::IsWindowFocused()) + { + // cancel any pending focus change + if (s_state.split_window_focus_change == SplitWindowFocusChange::FocusSidebar) + { + s_state.split_window_focus_change = SplitWindowFocusChange::None; + CancelResetFocus(); + } + } + + // todo: use own splitter + ImDrawList* const dl = ImGui::GetWindowDrawList(); + dl->ChannelsSplit(2); + dl->ChannelsSetCurrent(1); + + BeginMenuButtons(); + + // todo: do we need to move this down if our first item is a heading? + if (s_state.split_window_focus_change == SplitWindowFocusChange::FocusSidebar) + ResetFocusHere(); + if (ImGui::IsWindowFocused() && GImGui->NavId == 0) + ImGui::NavInitWindow(ImGui::GetCurrentWindow(), true); + + return true; +} + +void FullscreenUI::EndSplitWindowSidebar() +{ + EndMenuButtons(); + + ImGui::GetWindowDrawList()->ChannelsMerge(); + ImGui::PopStyleColor(1); + ImGui::PopStyleVar(1); + ImGui::EndChild(); +} + +bool FullscreenUI::SplitWindowSidebarItem(std::string_view title, bool active /*= false*/, bool enabled /*= true*/) +{ + if (active) + { + // todo: use own splitter + ImDrawList* const dl = ImGui::GetWindowDrawList(); + dl->ChannelsSetCurrent(0); + + const MenuButtonBounds bb(title, std::string_view(), std::string_view()); + dl->AddRectFilled(bb.frame_bb.Min, bb.frame_bb.Max, ImGui::GetColorU32(DarkerColor(UIStyle.BackgroundColor, 0.6f)), + LayoutScale(LAYOUT_MENU_ITEM_BORDER_ROUNDING)); + + dl->ChannelsSetCurrent(1); + } + + return MenuButtonWithoutSummary(title, enabled); +} + +void FullscreenUI::SplitWindowSidebarSeparator() +{ + const float line_width = MenuButtonBounds::CalcAvailWidth(); + + const float line_thickness = LayoutScale(1.0f); + const float line_padding = LayoutScale(2.0f); + + const ImVec2 line_start = + ImGui::GetCurrentWindowRead()->DC.CursorPos + ImVec2(ImGui::GetStyle().FramePadding.x, line_padding); + const ImVec2 line_end = ImVec2(line_start.x + line_width, line_start.y); + const ImVec2 shadow_offset = LayoutScale(LAYOUT_SHADOW_OFFSET, LAYOUT_SHADOW_OFFSET); + const u32 color = ImGui::GetColorU32(ImGuiCol_TextDisabled); + +#if 0 + ImGui::GetWindowDrawList()->AddLine(line_start + shadow_offset, line_end + shadow_offset, UIStyle.ShadowColor, + line_thickness); +#endif + ImGui::GetWindowDrawList()->AddLine(line_start, line_end, color, line_thickness); + + ImGui::Dummy(ImVec2(0.0f, line_padding * 2.0f + line_thickness)); +} + +bool FullscreenUI::SplitWindowSidebarItem(std::string_view title, std::string_view summary, bool active /*= false*/, + bool enabled /*= true*/) +{ + if (active) + { + // todo: use own splitter + ImDrawList* const dl = ImGui::GetWindowDrawList(); + dl->ChannelsSetCurrent(0); + + const MenuButtonBounds bb(title, std::string_view(), summary); + dl->AddRectFilled(bb.frame_bb.Min, bb.frame_bb.Max, + ImGui::GetColorU32(DarkerColor(ImGui::GetStyle().Colors[ImGuiCol_WindowBg], 0.6f)), + LayoutScale(LAYOUT_MENU_ITEM_BORDER_ROUNDING)); + + dl->ChannelsSetCurrent(1); + } + + return MenuButton(title, summary, enabled); +} + +bool FullscreenUI::BeginSplitWindowContent(bool background) +{ + ImGui::PushStyleColor(ImGuiCol_ChildBg, DarkerColor(ImGui::GetStyle().Colors[ImGuiCol_WindowBg], 1.1f)); + + if (IsFocusResetFromWindowChange()) + ImGui::SetNextWindowScroll(ImVec2(0.0f, 0.0f)); + + ImGuiWindow* const window = ImGui::GetCurrentWindow(); + const float content_width = window->WorkRect.Max.x - window->DC.CursorPos.x; + + const bool ret = + ImGui::BeginChild("content", ImVec2(content_width, 0.0f), 0, background ? 0 : ImGuiWindowFlags_NoBackground); + + if (ImGui::IsWindowFocused()) + { + // cancel any pending focus change + if (s_state.split_window_focus_change == SplitWindowFocusChange::FocusContent) + { + s_state.split_window_focus_change = SplitWindowFocusChange::None; + CancelResetFocus(); + } + } + + return ret; +} + +void FullscreenUI::ResetSplitWindowContentFocusHere() +{ + if (s_state.split_window_focus_change == SplitWindowFocusChange::FocusSidebar || + (s_state.split_window_focus_change == SplitWindowFocusChange::None && IsFocusResetQueued())) + { + ResetFocusHere(); + } + + if (ImGui::IsWindowFocused() && GImGui->NavId == 0) + ImGui::NavInitWindow(ImGui::GetCurrentWindow(), true); +} + +void FullscreenUI::EndSplitWindowContent() +{ + ImGui::PopStyleColor(1); + ImGui::EndChild(); +} + +bool FullscreenUI::WasSplitWindowChanged() +{ + return (s_state.split_window_focus_change != SplitWindowFocusChange::None); +} + +void FullscreenUI::FocusSplitWindowContent(bool reset_content_nav) +{ + s_state.split_window_focus_change = SplitWindowFocusChange::FocusContent; + QueueResetFocus(reset_content_nav ? FocusResetType::Other : FocusResetType::SplitWindowChanged); +} + FullscreenUI::PopupDialog::PopupDialog() = default; FullscreenUI::PopupDialog::~PopupDialog() = default; diff --git a/src/core/fullscreenui_widgets.h b/src/core/fullscreenui_widgets.h index c6dbea482..78a271eb9 100644 --- a/src/core/fullscreenui_widgets.h +++ b/src/core/fullscreenui_widgets.h @@ -296,9 +296,11 @@ enum class FocusResetType : u8 PopupOpened, PopupClosed, ViewChanged, + SplitWindowChanged, Other, }; void QueueResetFocus(FocusResetType type); +void CancelResetFocus(); bool ResetFocusHere(); bool IsFocusResetQueued(); bool IsFocusResetFromWindowChange(); @@ -461,6 +463,25 @@ void EndHorizontalMenu(); bool HorizontalMenuItem(GPUTexture* icon, std::string_view title, std::string_view description, u32 color = IM_COL32(255, 255, 255, 255)); +// Sidebars +bool BeginSplitWindow(const ImVec2& position, const ImVec2& size, const char* name, + const ImVec4& background = UIStyle.BackgroundColor, float rounding = 0.0f, + const ImVec2& padding = ImVec2(LAYOUT_MENU_WINDOW_X_PADDING, LAYOUT_MENU_WINDOW_Y_PADDING), + ImGuiWindowFlags flags = 0); +void EndSplitWindow(); +void BeginInnerSplitWindow(); +void EndInnerSplitWindow(); +bool BeginSplitWindowSidebar(float sidebar_width = 0.2f); +void EndSplitWindowSidebar(); +bool SplitWindowSidebarItem(std::string_view title, bool active = false, bool enabled = true); +bool SplitWindowSidebarItem(std::string_view title, std::string_view summary, bool active = false, bool enabled = true); +void SplitWindowSidebarSeparator(); +bool BeginSplitWindowContent(bool background); +void ResetSplitWindowContentFocusHere(); +void EndSplitWindowContent(); +bool WasSplitWindowChanged(); +void FocusSplitWindowContent(bool reset_content_nav); + using FileSelectorCallback = std::function; using FileSelectorFilters = std::vector; bool IsFileSelectorOpen();