Prechádzať zdrojové kódy

Merge pull request #137 from saulob/feat/main-overlay-ux

Improve main overlay UX and startup guidance
stan 4 dní pred
rodič
commit
89db701091

+ 8 - 0
README.md

@@ -12,6 +12,14 @@ Destiny 2 Offline Preservation Mod
 - [Documentation](https://projectsunrise.dev/docs/)
 - [Discord](https://discord.gg/22JS6et5k9)
 
+## Opening the overlay
+
+After launching Destiny 2, press Insert to open or close the Sunrise overlay
+
+Insert is the default key and can be changed in `bin\x64\Sunrise\settings.json`
+
+See the [FAQ](https://projectsunrise.dev/faq/) for more information
+
 ## Features
 - Load into any Destination (matchmade activities are currently broken)
 - Script Missions

+ 2 - 0
Sunrise/Sunrise.vcxproj

@@ -217,6 +217,7 @@
     <ClCompile Include="src\core\ui\hud\overlays\ui_hud_mission_script_overlay.cpp" />
     <ClCompile Include="src\core\ui\hud\overlays\ui_hud_sensor_events_overlay.cpp" />
     <ClCompile Include="src\core\ui\hud\overlays\ui_hud_session_overlay.cpp" />
+    <ClCompile Include="src\core\ui\hud\overlays\ui_hud_startup_hint_overlay.cpp" />
     <ClCompile Include="src\core\ui\hud\overlays\ui_hud_status_overlay.cpp" />
     <ClCompile Include="src\core\ui\hud\store\hud_settings_store.cpp" />
     <ClCompile Include="src\core\ui\textures\ui_texture_slots.cpp" />
@@ -1404,6 +1405,7 @@
     <ClInclude Include="src\core\ui\hud\overlays\ui_hud_mission_script_overlay.h" />
     <ClInclude Include="src\core\ui\hud\overlays\ui_hud_sensor_events_overlay.h" />
     <ClInclude Include="src\core\ui\hud\overlays\ui_hud_session_overlay.h" />
+    <ClInclude Include="src\core\ui\hud\overlays\ui_hud_startup_hint_overlay.h" />
     <ClInclude Include="src\core\ui\hud\overlays\ui_hud_status_overlay.h" />
     <ClInclude Include="src\core\ui\hud\store\hud_settings_store.h" />
     <ClInclude Include="src\core\ui\textures\ui_texture_slots.h" />

+ 10 - 0
Sunrise/src/core/settings/client/client_ui_settings_parser.cpp

@@ -46,6 +46,16 @@ bool Parser::ui_toggle_key_value(std::string_view name, UINT& output) noexcept {
     return false;
 }
 
+/** Names one Windows SDK virtual key with the setting that selects it. */
+std::string_view Parser::ui_toggle_key_name(UINT virtualKey) noexcept {
+    for (const KeyName& entry : kToggleKeys) {
+        if (entry.virtualKey == virtualKey) {
+            return entry.name;
+        }
+    }
+    return {};
+}
+
 /** Parses the in-game UI boot and input policy. */
 bool Parser::client_ui_settings(ui::runtime::Settings& output) noexcept {
     if (!consume('{')) {

+ 3 - 0
Sunrise/src/core/settings/parser.h

@@ -28,6 +28,9 @@ public:
     /** Reads only the root version; a missing version is zero. */
     [[nodiscard]] bool parse_version(std::uint32_t& output) noexcept;
 
+    /** @return The settings name of a supported menu toggle key, or empty when it is not one. */
+    [[nodiscard]] static std::string_view ui_toggle_key_name(UINT virtualKey) noexcept;
+
 private:
     /** Parses the Core settings object. */
     [[nodiscard]] bool core(Settings& output) noexcept;

+ 2 - 0
Sunrise/src/core/ui/hud/overlay.h

@@ -8,6 +8,8 @@ namespace sunrise::core::ui::hud {
 enum class Overlay : std::uint8_t {
     /** The Sunrise name, version and animated logo. */
     logoCard,
+    /** How to open the menu, shown once beside the card shortly after start. */
+    startupHint,
     /** Where the player is: activity, bubble, slice set and closest spawn. */
     currentStatus,
     /** The instances of the session the player is in. */

+ 82 - 0
Sunrise/src/core/ui/hud/overlays/ui_hud_startup_hint_overlay.cpp

@@ -0,0 +1,82 @@
+#include "ui_hud_startup_hint_overlay.h"
+
+#include <algorithm>
+#include <array>
+#include <cctype>
+#include <cstddef>
+#include <imgui.h>
+#include <string_view>
+
+#include "../../../settings/parser.h"
+#include "../../runtime/ui_visibility_runtime.h"
+
+namespace sunrise::core::ui::hud::overlays::startup_hint {
+namespace {
+
+/** 2 seconds let the game settle before the hint asks for attention. */
+constexpr float kDelaySeconds = 2.0F;
+/** 30 seconds is long enough to notice the hint and short enough to clear itself. */
+constexpr float kVisibleSeconds = 30.0F;
+/** Half a second to fade in or out, slower than the menus so the hint stays subtle. */
+constexpr float kFadeSeconds = 0.5F;
+/** A hidden hint has finished fading out and draws nothing. */
+constexpr float kHiddenOpacity = 0.0F;
+/** A shown hint has finished fading in. */
+constexpr float kShownOpacity = 1.0F;
+/** Longest supported toggle key name, plus the null. */
+constexpr std::size_t kKeyNameCapacity = 16;
+
+/** Seconds since the first HUD frame, counted until the hint is dismissed. */
+float g_elapsedSeconds = 0.0F;
+/** Set once the surface opened or the stay ran out. The hint never returns in this run. */
+bool g_dismissed = false;
+/** The hint's own fade, so it needs no shared animation key. */
+float g_opacity = kHiddenOpacity;
+
+/**
+ * Names the configured toggle key in upper case, so it stands out from the muted text around it.
+ * The key always comes from the parser's table, because the settings reject any other one.
+ * @return Null-terminated key name.
+ */
+[[nodiscard]] std::array<char, kKeyNameCapacity> key_label() noexcept {
+    std::array<char, kKeyNameCapacity> label{};
+    const std::string_view name =
+        settings::parser::Parser::ui_toggle_key_name(runtime::snapshot().toggleVirtualKey);
+    const std::size_t length = (std::min)(name.size(), label.size() - 1);
+    for (std::size_t index = 0; index < length; ++index) {
+        label[index] = static_cast<char>(std::toupper(static_cast<unsigned char>(name[index])));
+    }
+    return label;
+}
+
+} // namespace
+
+/** Advances the hint's one-time lifetime by this frame. */
+float progress() noexcept {
+    const float deltaSeconds = ImGui::GetIO().DeltaTime;
+    if (!g_dismissed) {
+        g_elapsedSeconds += deltaSeconds;
+        // An open surface has taught the key, and a hint that came back later would nag.
+        if (runtime::snapshot().visible || g_elapsedSeconds >= kDelaySeconds + kVisibleSeconds) {
+            g_dismissed = true;
+        }
+    }
+    const bool shown = !g_dismissed && g_elapsedSeconds >= kDelaySeconds;
+    const float step = deltaSeconds / kFadeSeconds;
+    g_opacity = shown ? (std::min)(g_opacity + step, kShownOpacity)
+                      : (std::max)(g_opacity - step, kHiddenOpacity);
+    return g_opacity;
+}
+
+/** Draws the startup hint inside the overlay window the stack has already started. */
+void draw() noexcept {
+    const std::array<char, kKeyNameCapacity> key = key_label();
+    // The sentence is muted like the card's version line; the key is white like its wordmark.
+    ImGui::TextDisabled("Press");
+    ImGui::SameLine();
+    ImGui::TextUnformatted(key.data());
+    ImGui::SameLine();
+    ImGui::TextDisabled("to open the Sunrise overlay");
+}
+
+} // namespace sunrise::core::ui::hud::overlays::startup_hint

+ 15 - 0
Sunrise/src/core/ui/hud/overlays/ui_hud_startup_hint_overlay.h

@@ -0,0 +1,15 @@
+#pragma once
+
+namespace sunrise::core::ui::hud::overlays::startup_hint {
+
+/**
+ * Advances the hint's one-time lifetime by this frame: a short wait after the first HUD frame, a
+ * bounded stay, and an early end once the main surface opens. It never comes back in the same run.
+ * @return Current fade, from 0 (nothing to draw) to 1 (fully shown).
+ */
+[[nodiscard]] float progress() noexcept;
+
+/** Draws the startup hint inside the overlay window the stack has already started. */
+void draw() noexcept;
+
+} // namespace sunrise::core::ui::hud::overlays::startup_hint

+ 31 - 6
Sunrise/src/core/ui/hud/ui_hud_overlay_stack.cpp

@@ -13,6 +13,7 @@
 #include "overlays/ui_hud_mission_script_overlay.h"
 #include "overlays/ui_hud_sensor_events_overlay.h"
 #include "overlays/ui_hud_session_overlay.h"
+#include "overlays/ui_hud_startup_hint_overlay.h"
 #include "overlays/ui_hud_status_overlay.h"
 #include "store/hud_settings_store.h"
 
@@ -48,6 +49,11 @@ constexpr std::size_t kSwitchCount = kOverlayCount + kStatusLineCount;
 /** Every overlay, in Overlay order. The menu lists them and the corner stacks them in it. */
 constexpr std::array<Entry, kOverlayCount> kOverlays{
     Entry{"Sunrise Card", "sunrise_card", "##sunrise_hud_card", &overlays::logo::draw, true},
+    Entry{"Startup Hint",
+          "startup_hint",
+          "##sunrise_hud_startup_hint",
+          &overlays::startup_hint::draw,
+          true},
     // Diagnostic overlays start off because an ordinary run does not need them on screen.
     Entry{
         "Current Status", "current_status", "##sunrise_hud_status", &overlays::status::draw, false},
@@ -136,18 +142,18 @@ void save_switches() noexcept {
  * Draws one overlay at a fixed position.
  * @param entry Overlay to draw.
  * @param position Top-left corner, in final framebuffer pixels.
- * @return Height the window took, which is known only after its content is submitted.
+ * @return Size the window took, which is known only after its content is submitted.
  */
-[[nodiscard]] float draw_overlay(const Entry& entry, const ImVec2& position) noexcept {
+[[nodiscard]] ImVec2 draw_overlay(const Entry& entry, const ImVec2& position) noexcept {
     ImGui::SetNextWindowPos(position, ImGuiCond_Always);
     const bool submitContents = ImGui::Begin(entry.windowId, nullptr, kOverlayFlags);
     if (submitContents) {
         entry.draw();
     }
     // Read inside the window, because the size belongs to it and not to the caller's window.
-    const float height = ImGui::GetWindowSize().y;
+    const ImVec2 size = ImGui::GetWindowSize();
     ImGui::End();
-    return height;
+    return size;
 }
 
 } // namespace
@@ -217,14 +223,33 @@ bool draw(bool interfaceEnabled) noexcept {
     const float margin = scaling::dpi::pixels(kViewportMargin);
     const float gap = scaling::dpi::pixels(kOverlayGap);
     ImVec2 position{viewport->WorkPos.x + margin, viewport->WorkPos.y + margin};
+    // The startup hint sits beside whichever overlay owns the top row, or on the corner when
+    // none does. It is drawn after the stack, once that row's width is known, and never moves
+    // the overlays stacked below.
+    ImVec2 hintPosition = position;
     bool drawn = false;
     for (std::size_t index = 0; index < kOverlayCount; ++index) {
-        if (!g_enabled[index]) {
+        if (!g_enabled[index] || static_cast<Overlay>(index) == Overlay::startupHint) {
             continue;
         }
-        position.y += draw_overlay(kOverlays[index], position) + gap;
+        const ImVec2 size = draw_overlay(kOverlays[index], position);
+        if (!drawn) {
+            hintPosition.x += size.x + gap;
+        }
+        position.y += size.y + gap;
         drawn = true;
     }
+    constexpr auto hintIndex = static_cast<std::size_t>(Overlay::startupHint);
+    if (g_enabled[hintIndex]) {
+        const float progress = overlays::startup_hint::progress();
+        if (progress > 0.0F) {
+            // One style alpha fades the hint and everything drawn inside it together.
+            ImGui::PushStyleVar(ImGuiStyleVar_Alpha, progress);
+            (void)draw_overlay(kOverlays[hintIndex], hintPosition);
+            ImGui::PopStyleVar();
+            drawn = true;
+        }
+    }
     return drawn;
 }
 

+ 45 - 3
Sunrise/src/core/ui/layout/ui_layout_render.cpp

@@ -9,6 +9,7 @@
 #include "../components/logo/ui_logo_component.h"
 #include "../components/section/ui_section_component.h"
 #include "../modules/registry/ui_module_registry.h"
+#include "../runtime/ui_visibility_runtime.h"
 #include "../scaling/dpi/ui_dpi_scaling.h"
 #include "navigation/ui_layout_navigation.h"
 #include "ui_layout_lifecycle.h"
@@ -58,6 +59,12 @@ constexpr float kTitleTextRatio = 1.5F;
 constexpr float kHalfExtent = 2.0F;
 /** The surface names the tool with the same wordmark the HUD card carries. */
 constexpr char kTitle[] = "SUNRISE";
+/** The hidden close action label is scoped by the main window. */
+constexpr char kCloseWidgetId[] = "##close";
+/** The close cross spans this fraction of its button, so it reads as a glyph and not a frame. */
+constexpr float kCloseCrossRatio = 0.5F;
+/** One pixel keeps the close cross as thin as the body text. */
+constexpr float kCloseCrossThickness = 1.0F;
 
 /**
  * Copies one display name into null-terminated component storage.
@@ -119,8 +126,15 @@ void draw_companion_windows() noexcept {
     }
 }
 
-/** Draws the animated logo, then the name and version, on one title row. */
-void draw_title() noexcept {
+/**
+ * Draws the animated logo, then the name and version, on one title row, with a close action
+ * at its far end.
+ * @return True when the close action was pressed.
+ */
+[[nodiscard]] bool draw_title() noexcept {
+    // The row edges are taken before it opens, so the close action can sit at its far end.
+    const ImVec2 rowOrigin = ImGui::GetCursorScreenPos();
+    const float rowRight = rowOrigin.x + ImGui::GetContentRegionAvail().x;
     const float extent = scaling::dpi::pixels(kTitleLogoExtent);
     const bool logoDrawn = components::logo::draw(extent);
     if (logoDrawn) {
@@ -144,6 +158,31 @@ void draw_title() noexcept {
     ImGui::SetCursorPosY(
         titleY + ((std::max)(titleHeight - ImGui::GetTextLineHeight(), 0.0F) / kHalfExtent));
     ImGui::TextDisabled(SUNRISE_VER_STRING);
+
+    // A cross at the right edge of the title row, centered on it. It stays on the row the logo
+    // opened, like the version, so the row and the separator below keep their places.
+    ImGui::SameLine();
+    const float buttonExtent = ImGui::GetFontSize();
+    const float rowHeight = logoDrawn ? extent : titleHeight;
+    const ImVec2 buttonPosition{
+        rowRight - buttonExtent,
+        rowOrigin.y + ((std::max)(rowHeight - buttonExtent, 0.0F) / kHalfExtent)};
+    ImGui::SetCursorScreenPos(buttonPosition);
+    const bool pressed = ImGui::InvisibleButton(kCloseWidgetId, {buttonExtent, buttonExtent});
+
+    // Muted like the version until hovered, then white like the title.
+    const ImU32 color =
+        ImGui::GetColorU32(ImGui::IsItemHovered() ? ImGuiCol_Text : ImGuiCol_TextDisabled);
+    const float half = buttonExtent * kCloseCrossRatio / kHalfExtent;
+    const ImVec2 center{buttonPosition.x + (buttonExtent / kHalfExtent),
+                        buttonPosition.y + (buttonExtent / kHalfExtent)};
+    ImDrawList* drawList = ImGui::GetWindowDrawList();
+    const float thickness = scaling::dpi::pixels(kCloseCrossThickness);
+    drawList->AddLine(
+        {center.x - half, center.y - half}, {center.x + half, center.y + half}, color, thickness);
+    drawList->AddLine(
+        {center.x + half, center.y - half}, {center.x - half, center.y + half}, color, thickness);
+    return pressed;
 }
 
 } // namespace
@@ -186,7 +225,10 @@ bool render(bool visible) noexcept {
     ImGui::PushStyleVar(ImGuiStyleVar_Alpha, progress);
     const bool submitContents = ImGui::Begin("Sunrise", nullptr, kMainWindowFlags);
     if (submitContents) {
-        draw_title();
+        // The close action only ever hides, so a surface already closing stays closed.
+        if (draw_title()) {
+            runtime::hide();
+        }
         ImGui::Separator();
 
         const StateSnapshot state = snapshot();

+ 7 - 0
Sunrise/src/core/ui/runtime/ui_visibility_runtime.cpp

@@ -63,4 +63,11 @@ bool toggle_for_key(UINT virtualKey) noexcept {
     return handled;
 }
 
+/** Closes the menu. A menu that is already closed or closing stays that way. */
+void hide() noexcept {
+    AcquireSRWLockExclusive(&g_visibilityLock);
+    g_state.visible = false;
+    ReleaseSRWLockExclusive(&g_visibilityLock);
+}
+
 } // namespace sunrise::core::ui::runtime

+ 3 - 0
Sunrise/src/core/ui/runtime/ui_visibility_runtime.h

@@ -35,4 +35,7 @@ void shutdown() noexcept;
  */
 [[nodiscard]] bool toggle_for_key(UINT virtualKey) noexcept;
 
+/** Closes the menu. A menu that is already closed or closing stays that way. */
+void hide() noexcept;
+
 } // namespace sunrise::core::ui::runtime