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,