diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index e6b4f88..ef9b749 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -572,6 +572,12 @@ public sealed class GameWindow : IDisposable _window.Update += OnUpdate; _window.Render += OnRender; _window.Closing += OnClosing; + // L.0 Display tab: keep the GL viewport + camera aspect in sync + // with the window framebuffer. Without this handler, resizing + // the window (or applying a Display-tab Resolution change at + // startup) leaves the viewport pinned to the original size — + // user sees a small render in the corner of a big window. + _window.FramebufferResize += OnFramebufferResize; _window.Run(); } @@ -1058,6 +1064,14 @@ public sealed class GameWindow : IDisposable } Console.WriteLine("devtools: ImGui panel host ready (VitalsPanel + ChatPanel + DebugPanel + SettingsPanel registered)"); + + // L.0 Display tab: seed sensible default positions for + // every registered panel. cond=FirstUseEver means imgui.ini + // takes precedence on subsequent launches — the user's + // dragged positions persist. Without this, the first-run + // experience stacks every panel at (0,0) which looks + // broken. + ResetPanelLayout(ImGuiNET.ImGuiCond.FirstUseEver); } catch (Exception ex) { @@ -4515,6 +4529,15 @@ public sealed class GameWindow : IDisposable if (_debugPanel is not null && ImGuiNET.ImGui.MenuItem("Debug", "Ctrl+F1")) _debugPanel.IsVisible = !_debugPanel.IsVisible; + ImGuiNET.ImGui.Separator(); + // L.0 Display tab: a manual reset for users whose + // imgui.ini has saved a panel position that's now + // off-screen (after a window shrink, monitor swap, + // or a malformed save). Force-resets every panel + // to its default landing position. The same code + // path runs automatically on FramebufferResize. + if (ImGuiNET.ImGui.MenuItem("Reset window layout")) + ResetPanelLayout(ImGuiNET.ImGuiCond.Always); ImGuiNET.ImGui.EndMenu(); } // K-fix2 (2026-04-26): Camera submenu — discoverable @@ -5408,6 +5431,78 @@ public sealed class GameWindow : IDisposable private AcDream.UI.Abstractions.Panels.Settings.SettingsStore? _settingsStore; private string _activeToonKey = "default"; + /// + /// L.0 Display tab: framebuffer-resize handler — update GL viewport + /// + camera aspect when the window is resized (by the user dragging + /// the corner OR by ApplyDisplayWindowState applying a saved + /// Resolution). Without this, the viewport stays pinned at the + /// startup size, producing a small render inside a big window. + /// Also force-resets ImGui panel layout so panels that were + /// previously off the new viewport snap back to default positions. + /// + private void OnFramebufferResize(Silk.NET.Maths.Vector2D newSize) + { + if (newSize.X <= 0 || newSize.Y <= 0) return; + _gl?.Viewport(0, 0, (uint)newSize.X, (uint)newSize.Y); + _cameraController?.SetAspect(newSize.X / (float)newSize.Y); + // Resize is always a force-reset — the alternative ("clamp + // existing positions") would require tracking each panel's + // current pos+size, which ImGuiNET doesn't expose by name. + // Force-reset is acceptable UX because resizing happens rarely + // and the user can always drag panels back where they want. + if (DevToolsEnabled && _imguiBootstrap is not null) + ResetPanelLayout(ImGuiNET.ImGuiCond.Always); + } + + /// + /// L.0 Display tab: position every registered panel to its default + /// landing spot, computed relative to the current window size so + /// the layout adapts to any resolution. Called from: + /// + /// OnFramebufferResize (cond=Always — force-reset on resize). + /// The View → "Reset window layout" menu item (cond=Always). + /// OnLoad after panel registration (cond=FirstUseEver — only + /// applies when imgui.ini has no saved position for that + /// panel; on subsequent launches the saved positions win). + /// + /// + private void ResetPanelLayout(ImGuiNET.ImGuiCond cond) + { + if (_window is null) return; + float w = _window.Size.X; + float h = _window.Size.Y; + // Sane minimums so the math doesn't blow up on a tiny window. + if (w < 480) w = 480; + if (h < 320) h = 320; + + // Panel positions chosen to be classic-MMO discoverable on a + // 1280x720 window: vitals top-left under the menu bar, chat + // bottom-left, debug top-right, settings centered. All sizes + // are reasonable defaults the user can resize from. + SetPanelLayout(_vitalsPanel?.Title, new System.Numerics.Vector2(10f, 30f), + new System.Numerics.Vector2(220f, 110f), cond); + SetPanelLayout(_chatPanel?.Title, new System.Numerics.Vector2(10f, h - 320f), + new System.Numerics.Vector2(450f, 300f), cond); + SetPanelLayout(_debugPanel?.Title, new System.Numerics.Vector2(w - 380f, 30f), + new System.Numerics.Vector2(370f, 520f), cond); + SetPanelLayout(_settingsPanel?.Title, new System.Numerics.Vector2((w - 700f) * 0.5f, (h - 500f) * 0.5f), + new System.Numerics.Vector2(700f, 500f), cond); + } + + private static void SetPanelLayout( + string? title, + System.Numerics.Vector2 pos, + System.Numerics.Vector2 size, + ImGuiNET.ImGuiCond cond) + { + if (string.IsNullOrEmpty(title)) return; + // SetWindowPos/SetWindowSize by name work even when the window + // has never been Begin'd — ImGui stores the value for next + // appearance. + ImGuiNET.ImGui.SetWindowPos(title, pos, cond); + ImGuiNET.ImGui.SetWindowSize(title, size, cond); + } + /// /// L.0 Display tab: apply the window-state-dependent settings /// (Resolution + Fullscreen) from a diff --git a/src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs b/src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs index 505a2d3..05438b0 100644 --- a/src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs +++ b/src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs @@ -23,11 +23,14 @@ public sealed record DisplaySettings( bool ShowFps) { /// Values used on first launch / when settings.json is absent. - /// FieldOfView (60°) and VSync (false) match the camera + window - /// defaults that shipped before L.0, so opening Display + Save - /// without touching anything is a visual no-op. + /// All defaults pinned to the pre-L.0 runtime state — Resolution + /// matches the WindowOptions startup size (1280×720), FieldOfView + /// matches camera FovY (60°), VSync matches WindowOptions (false), + /// ShowFps preserves the perf string in the title bar. Net effect: + /// opening Display + Save without touching anything is a complete + /// visual no-op. public static DisplaySettings Default { get; } = new( - Resolution: "1920x1080", + Resolution: "1280x720", Fullscreen: false, VSync: false, FieldOfView: 60f,