Merge branch 'feature/settings-retail' — Phase L.0 Settings interface
Lands the full retail-style Settings interface developed in the .worktrees/settings-retail worktree. 17 commits delivering: == Phase L.0 — Settings interface ==7665cdftabbed Settings shell + IPanelRenderer tab API extension382f0adDisplay tab + settings.json persistence layer53b1878Audio tab + live volume sliders driving OpenAL engineb7165e5Gameplay tab — 14 retail CharacterOption-derived toggles356b5f2Chat tab — channel filters + display prefs + font slider73749d1Character tab — per-toon settings; Phase L.0 completefc1e193wire Display GL knobs + per-toon Character key4c75cedchat Copy mode — read-only multi-line for select + Ctrl+C == Drag-fix iteration ==6273255first attempt at title-bar-only drag (Begin-level absorber)2818fccscope drag absorber to BeginChild (fixed Settings tabs)df9f2fdwrap chat panel body in outer BeginChild (fixed chat drag) == Pre-merge code review fixes ==944a036rescue commit — orphaned FramebufferResize + ResetPanelLayout (working-tree changes that never got committed in the cwd shenanigans during earlier iteration)a37ebdeapply persisted Display + Audio settings without devtools gate (settings are runtime state, not devtools state); hide Music + Ambient sliders that were inert (R5 MIDI not shipped)23aa017docs/plans/roadmap shipped table updated for K + L.0 == Net delivered == · 6-tab F11 Settings panel: Keybinds (existing) + Display + Audio + Gameplay + Chat + Character · settings.json at %LOCALAPPDATA%\acdream\ — five sections coexist non-destructively, per-toon Character keying · Display: Resolution / Fullscreen / VSync / FOV / ShowFps live-wired to Silk.NET window + camera FovY + title-bar perf string · Audio: Master + SFX volume live-driving OpenAL engine · Gameplay/Chat/Character: persist for forthcoming server-sync wiring · Chat panel Copy mode (Ctrl+C selectable text) · Title-bar-only window drag (BeginChild absorber) · FramebufferResize handler — GL viewport + camera aspect + panel layout stay in sync on window resize · "Reset window layout" View menu item · IPanelRenderer extensions: tab API + TextMultilineReadOnly dotnet build green (0 warnings); dotnet test 1,309 / 1,309 green (243 Core.Net + 393 UI.Abstractions + 673 Core; +87 net new tests since fork). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
a2e0bb5e2f
23 changed files with 3101 additions and 60 deletions
|
|
@ -595,6 +595,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();
|
||||
}
|
||||
|
|
@ -833,6 +839,17 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// L.0 follow-up — load + apply persisted Display / Audio settings
|
||||
// BEFORE the DevToolsEnabled block. The settings.json values
|
||||
// (resolution, vsync, FOV, master volume, etc) are runtime
|
||||
// settings, not devtools settings — a user running without
|
||||
// ACDREAM_DEVTOOLS=1 still expects their saved values to take
|
||||
// effect. The Settings PANEL (editing UI) is gated on devtools;
|
||||
// the persisted state is not. Caches values into fields so the
|
||||
// SettingsVM construction in the devtools block reads them
|
||||
// without re-loading.
|
||||
LoadAndApplyPersistedSettings();
|
||||
|
||||
// Phase D.2a — ImGui devtools overlay. Zero cost when the env var
|
||||
// isn't set: no context creation, no per-frame branches hit.
|
||||
// See docs/plans/2026-04-24-ui-framework.md + memory/project_ui_architecture.md.
|
||||
|
|
@ -922,8 +939,15 @@ public sealed class GameWindow : IDisposable
|
|||
// the draft. Construction is null-safe vs. the
|
||||
// dispatcher because the dispatcher is built earlier in
|
||||
// the same OnLoad path (see _inputDispatcher field).
|
||||
if (_inputDispatcher is not null)
|
||||
if (_inputDispatcher is not null && _settingsStore is not null)
|
||||
{
|
||||
// L.0 — SettingsStore + persisted-settings load + apply
|
||||
// happened earlier in OnLoad via
|
||||
// LoadAndApplyPersistedSettings (settings are runtime
|
||||
// state, not devtools state — they take effect even
|
||||
// when ACDREAM_DEVTOOLS=0). Here we just construct the
|
||||
// Settings PANEL on top of the already-loaded values.
|
||||
var settingsStore = _settingsStore;
|
||||
_settingsVm = new AcDream.UI.Abstractions.Panels.Settings.SettingsVM(
|
||||
persisted: _keyBindings,
|
||||
dispatcher: _inputDispatcher,
|
||||
|
|
@ -942,12 +966,113 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
Console.WriteLine($"keybinds: save failed: {ex.Message}");
|
||||
}
|
||||
},
|
||||
persistedDisplay: _persistedDisplay,
|
||||
onSaveDisplay: display =>
|
||||
{
|
||||
try
|
||||
{
|
||||
settingsStore.SaveDisplay(display);
|
||||
Console.WriteLine(
|
||||
"settings: display saved to "
|
||||
+ AcDream.UI.Abstractions.Panels.Settings.SettingsStore.DefaultPath());
|
||||
// Apply window-level changes that are too
|
||||
// jarring to live-preview (resolution +
|
||||
// fullscreen). VSync / FOV / ShowFps
|
||||
// already track DisplayDraft via the
|
||||
// per-frame push.
|
||||
ApplyDisplayWindowState(display);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"settings: display save failed: {ex.Message}");
|
||||
}
|
||||
},
|
||||
persistedAudio: _persistedAudio,
|
||||
onSaveAudio: audio =>
|
||||
{
|
||||
try
|
||||
{
|
||||
settingsStore.SaveAudio(audio);
|
||||
Console.WriteLine(
|
||||
"settings: audio saved to "
|
||||
+ AcDream.UI.Abstractions.Panels.Settings.SettingsStore.DefaultPath());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"settings: audio save failed: {ex.Message}");
|
||||
}
|
||||
},
|
||||
persistedGameplay: _persistedGameplay,
|
||||
onSaveGameplay: gameplay =>
|
||||
{
|
||||
try
|
||||
{
|
||||
settingsStore.SaveGameplay(gameplay);
|
||||
Console.WriteLine(
|
||||
"settings: gameplay saved to "
|
||||
+ AcDream.UI.Abstractions.Panels.Settings.SettingsStore.DefaultPath());
|
||||
// Local-only this phase. Server-sync packet
|
||||
// (CharacterOption bitmask) goes in here when
|
||||
// the protocol round-trip is in place.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"settings: gameplay save failed: {ex.Message}");
|
||||
}
|
||||
},
|
||||
persistedChat: _persistedChat,
|
||||
onSaveChat: chat =>
|
||||
{
|
||||
try
|
||||
{
|
||||
settingsStore.SaveChat(chat);
|
||||
Console.WriteLine(
|
||||
"settings: chat saved to "
|
||||
+ AcDream.UI.Abstractions.Panels.Settings.SettingsStore.DefaultPath());
|
||||
// Channel filters affect client-side display
|
||||
// only this phase. ChatPanel will read them
|
||||
// off SettingsVM.ChatDraft when filtering is
|
||||
// wired into the chat-line render path.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"settings: chat save failed: {ex.Message}");
|
||||
}
|
||||
},
|
||||
persistedCharacter: _persistedCharacter,
|
||||
onSaveCharacter: character =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// _activeToonKey is updated by
|
||||
// BeginLiveSessionAsync after EnterWorld
|
||||
// so saving character settings always
|
||||
// writes under the chosen character's
|
||||
// name (or "default" pre-login).
|
||||
settingsStore.SaveCharacter(_activeToonKey, character);
|
||||
Console.WriteLine(
|
||||
$"settings: character[{_activeToonKey}] saved to "
|
||||
+ AcDream.UI.Abstractions.Panels.Settings.SettingsStore.DefaultPath());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"settings: character save failed: {ex.Message}");
|
||||
}
|
||||
});
|
||||
_settingsPanel = new AcDream.UI.Abstractions.Panels.Settings.SettingsPanel(_settingsVm);
|
||||
_panelHost.Register(_settingsPanel);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
|
@ -1464,6 +1589,20 @@ public sealed class GameWindow : IDisposable
|
|||
_worldState.MarkPersistent(chosen.Id); // player entity survives landblock unloads
|
||||
Console.WriteLine($"live: entering world as 0x{chosen.Id:X8} {chosen.Name}");
|
||||
_liveSession.EnterWorld(user, characterIndex: 0);
|
||||
|
||||
// L.0 Character tab: swap the SettingsVM's character bag
|
||||
// from the "default" pre-login bag to the actual chosen
|
||||
// toon's bag. Every Save from now on writes under the
|
||||
// chosen toon's name. LoadCharacterContext rebinds BOTH
|
||||
// persisted + draft so HasUnsavedChanges doesn't flag the
|
||||
// swap as a pending edit.
|
||||
_activeToonKey = chosen.Name;
|
||||
if (_settingsStore is not null && _settingsVm is not null)
|
||||
{
|
||||
var toonBag = _settingsStore.LoadCharacter(_activeToonKey);
|
||||
_settingsVm.LoadCharacterContext(toonBag);
|
||||
Console.WriteLine($"settings: loaded character[{_activeToonKey}] preferences");
|
||||
}
|
||||
// Phase K.2: arm auto-entry. The guard's predicates won't
|
||||
// pass yet — the entity stream hasn't started — but the
|
||||
// OnUpdate tick re-checks every frame and fires once
|
||||
|
|
@ -4260,6 +4399,41 @@ public sealed class GameWindow : IDisposable
|
|||
System.Numerics.Matrix4x4.Invert(camera.View, out var invView);
|
||||
var camPos = new System.Numerics.Vector3(invView.M41, invView.M42, invView.M43);
|
||||
|
||||
// L.0 Audio tab: push the SettingsVM's live AudioDraft into the
|
||||
// engine each frame, so volume sliders preview audibly while
|
||||
// the user drags. Cancel reverts the draft and the engine
|
||||
// catches up on the very next frame; Save persists to
|
||||
// settings.json without changing engine state (already
|
||||
// applied). Cheap enough to run unconditionally on every
|
||||
// tick — four float assignments.
|
||||
if (_audioEngine is not null && _audioEngine.IsAvailable && _settingsVm is not null)
|
||||
{
|
||||
var a = _settingsVm.AudioDraft;
|
||||
_audioEngine.MasterVolume = a.Master;
|
||||
_audioEngine.MusicVolume = a.Music;
|
||||
_audioEngine.SfxVolume = a.Sfx;
|
||||
_audioEngine.AmbientVolume = a.Ambient;
|
||||
}
|
||||
|
||||
// L.0 Display tab: push the live DisplayDraft into the
|
||||
// active rendering surfaces each frame. FOV is the live-
|
||||
// preview slider per the brainstorm — dragging it changes
|
||||
// camera FovY immediately. VSync change-detected to avoid
|
||||
// spamming the window. Resolution + Fullscreen apply on
|
||||
// Save (handled by ApplyDisplayWindowState — too jarring
|
||||
// to live-preview a resize).
|
||||
if (_settingsVm is not null && _cameraController is not null)
|
||||
{
|
||||
var d = _settingsVm.DisplayDraft;
|
||||
float fovYRad = d.FieldOfView * (MathF.PI / 180f);
|
||||
_cameraController.Orbit.FovY = fovYRad;
|
||||
_cameraController.Fly.FovY = fovYRad;
|
||||
if (_cameraController.Chase is not null)
|
||||
_cameraController.Chase.FovY = fovYRad;
|
||||
if (_window is not null && _window.VSync != d.VSync)
|
||||
_window.VSync = d.VSync;
|
||||
}
|
||||
|
||||
// Phase E.2 audio: update listener pose so 3D sounds pan/attenuate
|
||||
// correctly relative to where we're looking.
|
||||
if (_audioEngine is not null && _audioEngine.IsAvailable)
|
||||
|
|
@ -4510,6 +4684,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
|
||||
|
|
@ -4543,9 +4726,16 @@ public sealed class GameWindow : IDisposable
|
|||
int entityCount = _worldState.Entities.Count;
|
||||
int animatedCount = _animatedEntities.Count;
|
||||
|
||||
_window!.Title = $"acdream | {fps:F0} fps | {avgFrameTime:F1} ms | " +
|
||||
$"lb {visibleLandblocks}/{totalLandblocks} visible | " +
|
||||
$"ent {entityCount} | anim {animatedCount}";
|
||||
// L.0 Display tab: ShowFps gates the perf string in the
|
||||
// title bar. Default is true (matches pre-L.0 behaviour);
|
||||
// unchecking the toggle in Display tab collapses the title
|
||||
// to just "acdream" for a cleaner alt-tab experience.
|
||||
bool showFps = _settingsVm?.DisplayDraft.ShowFps ?? true;
|
||||
_window!.Title = showFps
|
||||
? $"acdream | {fps:F0} fps | {avgFrameTime:F1} ms | "
|
||||
+ $"lb {visibleLandblocks}/{totalLandblocks} visible | "
|
||||
+ $"ent {entityCount} | anim {animatedCount}"
|
||||
: "acdream";
|
||||
_lastFps = fps;
|
||||
_lastFrameMs = avgFrameTime;
|
||||
_perfAccum = 0;
|
||||
|
|
@ -5478,6 +5668,188 @@ public sealed class GameWindow : IDisposable
|
|||
// default; F11 / View → Settings toggles. Null when devtools are off.
|
||||
private AcDream.UI.Abstractions.Panels.Settings.SettingsPanel? _settingsPanel;
|
||||
private AcDream.UI.Abstractions.Panels.Settings.SettingsVM? _settingsVm;
|
||||
// L.0: settings.json store + active toon key. The store is held as
|
||||
// a field so BeginLiveSessionAsync can re-load the chosen toon's
|
||||
// bag once we know its name (post-EnterWorld). Toon key starts as
|
||||
// "default" and gets swapped to the actual character name on the
|
||||
// first EnterWorld.
|
||||
private AcDream.UI.Abstractions.Panels.Settings.SettingsStore? _settingsStore;
|
||||
private string _activeToonKey = "default";
|
||||
// L.0 follow-up: persisted-settings cache populated by
|
||||
// LoadAndApplyPersistedSettings (runs unconditionally in OnLoad,
|
||||
// not gated on DevToolsEnabled). The Settings PANEL construction
|
||||
// — which IS gated on devtools — reads these fields when wiring
|
||||
// SettingsVM. Defaults are placeholders; LoadAndApplyPersistedSettings
|
||||
// overwrites them with values from settings.json (or per-section
|
||||
// defaults when the file is missing/corrupt).
|
||||
private AcDream.UI.Abstractions.Panels.Settings.DisplaySettings _persistedDisplay
|
||||
= AcDream.UI.Abstractions.Panels.Settings.DisplaySettings.Default;
|
||||
private AcDream.UI.Abstractions.Panels.Settings.AudioSettings _persistedAudio
|
||||
= AcDream.UI.Abstractions.Panels.Settings.AudioSettings.Default;
|
||||
private AcDream.UI.Abstractions.Panels.Settings.GameplaySettings _persistedGameplay
|
||||
= AcDream.UI.Abstractions.Panels.Settings.GameplaySettings.Default;
|
||||
private AcDream.UI.Abstractions.Panels.Settings.ChatSettings _persistedChat
|
||||
= AcDream.UI.Abstractions.Panels.Settings.ChatSettings.Default;
|
||||
private AcDream.UI.Abstractions.Panels.Settings.CharacterSettings _persistedCharacter
|
||||
= AcDream.UI.Abstractions.Panels.Settings.CharacterSettings.Default;
|
||||
|
||||
/// <summary>
|
||||
/// L.0 follow-up: load every section from settings.json + apply the
|
||||
/// runtime-affecting ones (Display window state + Audio engine
|
||||
/// volumes) at startup. Runs unconditionally — settings are runtime
|
||||
/// state, not devtools state. Without this, a user running with
|
||||
/// <c>ACDREAM_DEVTOOLS=0</c> would silently get WindowOptions
|
||||
/// defaults instead of their saved Display/Audio preferences.
|
||||
/// </summary>
|
||||
private void LoadAndApplyPersistedSettings()
|
||||
{
|
||||
_settingsStore = new AcDream.UI.Abstractions.Panels.Settings.SettingsStore(
|
||||
AcDream.UI.Abstractions.Panels.Settings.SettingsStore.DefaultPath());
|
||||
_persistedDisplay = _settingsStore.LoadDisplay();
|
||||
_persistedAudio = _settingsStore.LoadAudio();
|
||||
_persistedGameplay = _settingsStore.LoadGameplay();
|
||||
_persistedChat = _settingsStore.LoadChat();
|
||||
// _activeToonKey is "default" pre-EnterWorld; the post-login
|
||||
// branch in BeginLiveSessionAsync swaps to the chosen toon's
|
||||
// name and re-loads via SettingsVM.LoadCharacterContext.
|
||||
_persistedCharacter = _settingsStore.LoadCharacter(_activeToonKey);
|
||||
|
||||
// Apply Display to the Silk.NET window. VSync goes via the
|
||||
// window property; resolution + fullscreen go through
|
||||
// ApplyDisplayWindowState which is shared with the on-Save path.
|
||||
if (_window is not null)
|
||||
{
|
||||
if (_window.VSync != _persistedDisplay.VSync)
|
||||
_window.VSync = _persistedDisplay.VSync;
|
||||
ApplyDisplayWindowState(_persistedDisplay);
|
||||
}
|
||||
|
||||
// Apply Audio to the OpenAL engine. Master + Sfx are wired
|
||||
// through to the engine; Music + Ambient are stored but inert
|
||||
// until R5 MIDI/ambient-loop engines exist (assigning them is
|
||||
// harmless — the engine just doesn't read them yet).
|
||||
if (_audioEngine is not null && _audioEngine.IsAvailable)
|
||||
{
|
||||
_audioEngine.MasterVolume = _persistedAudio.Master;
|
||||
_audioEngine.MusicVolume = _persistedAudio.Music;
|
||||
_audioEngine.SfxVolume = _persistedAudio.Sfx;
|
||||
_audioEngine.AmbientVolume = _persistedAudio.Ambient;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private void OnFramebufferResize(Silk.NET.Maths.Vector2D<int> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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:
|
||||
/// <list type="bullet">
|
||||
/// <item>OnFramebufferResize (cond=Always — force-reset on resize).</item>
|
||||
/// <item>The View → "Reset window layout" menu item (cond=Always).</item>
|
||||
/// <item>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).</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// L.0 Display tab: apply the window-state-dependent settings
|
||||
/// (Resolution + Fullscreen) from a <see cref="AcDream.UI.Abstractions.Panels.Settings.DisplaySettings"/>
|
||||
/// to the live Silk.NET window. Called at startup (with persisted
|
||||
/// values) and on every Save (with the saved values). Resolution
|
||||
/// parses "<c>WIDTHxHEIGHT</c>" (e.g. <c>"1920x1080"</c>); a malformed
|
||||
/// or unparseable string is silently ignored to avoid crashing the
|
||||
/// client mid-session.
|
||||
/// </summary>
|
||||
private void ApplyDisplayWindowState(
|
||||
AcDream.UI.Abstractions.Panels.Settings.DisplaySettings display)
|
||||
{
|
||||
if (_window is null) return;
|
||||
|
||||
// Resolution: parse and resize if changed.
|
||||
if (TryParseResolution(display.Resolution, out int w, out int h))
|
||||
{
|
||||
if (_window.Size.X != w || _window.Size.Y != h)
|
||||
_window.Size = new Silk.NET.Maths.Vector2D<int>(w, h);
|
||||
}
|
||||
|
||||
// Fullscreen: borderless via Silk.NET's WindowState.Fullscreen
|
||||
// (no exclusive-mode DXGI dance needed).
|
||||
var desiredState = display.Fullscreen
|
||||
? Silk.NET.Windowing.WindowState.Fullscreen
|
||||
: Silk.NET.Windowing.WindowState.Normal;
|
||||
if (_window.WindowState != desiredState)
|
||||
_window.WindowState = desiredState;
|
||||
}
|
||||
|
||||
private static bool TryParseResolution(string spec, out int width, out int height)
|
||||
{
|
||||
width = height = 0;
|
||||
if (string.IsNullOrWhiteSpace(spec)) return false;
|
||||
var parts = spec.Split('x', 2);
|
||||
if (parts.Length != 2) return false;
|
||||
return int.TryParse(parts[0], out width)
|
||||
&& int.TryParse(parts[1], out height)
|
||||
&& width > 0
|
||||
&& height > 0;
|
||||
}
|
||||
// Vitals panel reference cached for the View menu's toggle entry.
|
||||
private AcDream.UI.Abstractions.Panels.Vitals.VitalsPanel? _vitalsPanel;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue