railroader-setons-special-s.../src/Modules/Popout/PopoutSettingsUI.cs
seton 97eb320e0f v0.2.2 - map module themes, custom colors, in-game overlay
Map module:
- In-game overlay (UiService) with full map chrome matching the popout
- Six themes (S3 Dark, Railroader Classic, Night Mode, Hi-Vis, Retro Terminal, Custom)
- Per-element custom color editor with hex input and live R/G/B/A sliders
- Independent window and map opacity controls
- Theme and opacity changes apply from both the in-game overlay and the popout window
- Renamed module display name from "Map Popout" to "Map Module"

Native engine:
- SnapshotTheme() helper reduces per-frame mutex acquisitions from two to one
- Toolbar button text color auto-contrasts against the button background
- Compass colors driven by theme data

README:
- Logo, screenshots, and videos for all features
- Physics Optimizer before/after profiler comparison
- Correct MapEnhancer link (maintained fork)
2026-06-18 21:30:53 -04:00

258 lines
12 KiB
C#

using System;
using UnityEngine;
namespace S3.Modules.Popout;
// Renders the Map Popout settings: hotkey, behaviour toggles, theme preset buttons,
// a live per-element color editor for the Custom theme, and overlay opacity sliders.
static class PopoutSettingsUI
{
private static bool _capturing;
// ---------------------------------------------------------------------------
// Color editor state — one hex string per theme element, kept in sync with
// the sliders. Reset by InitColorEditor when a preset is copied or on first draw.
// ---------------------------------------------------------------------------
private static bool _colorEditorInit;
private static string _hexWBg = "1F1F1F";
private static string _hexAcc = "4296FA";
private static string _hexTxt = "FFFFFF";
private static string _hexPop = "141414";
private static string _hexMap = "FFFFFF";
private static string _hexCmp = "161616";
private static string _hexNN = "CD3232";
private static string _hexNS = "CDCDCD";
private static string _hexMapBg = "151F28";
private static bool _showColorEditor = true; // expanded by default
private static readonly string[] s_themeNames = { "S3 Dark", "Classic", "Night", "Hi-Vis", "Retro", "Custom" };
private static readonly string[] s_presetNames = { "S3 Dark", "Classic", "Night", "Hi-Vis", "Retro" };
private const float kLabelW = 92f;
private const float kThemeW = 68f; // wide enough for "S3 Dark" / "Classic"
private const float kHexW = 60f;
private const float kSwatchW = 20f;
private const float kChanL = 12f;
private const float kSliderW = 52f;
private const float kValW = 26f;
// ---------------------------------------------------------------------------
public static void Draw()
{
PopoutSettings s = PopoutModule.Settings;
// ── Hotkey ──────────────────────────────────────────────────────────────
GUILayout.BeginHorizontal();
GUILayout.Label("Pop-out hotkey:", GUILayout.Width(110f));
if (_capturing)
{
GUILayout.Label("Press a key… (Esc to cancel)");
Event e = Event.current;
if (e.type == EventType.KeyDown)
{
if (e.keyCode != KeyCode.Escape && e.keyCode != KeyCode.None)
{
s.hotkeyKeyCode = (int)e.keyCode;
s.hotkeyModifiers = (e.shift ? 1 : 0) | (e.control ? 2 : 0) | (e.alt ? 4 : 0);
PopoutModule.RebuildHotkey();
PopoutModule.Persist();
}
_capturing = false;
e.Use();
}
}
else
{
if (GUILayout.Button($"{HotkeyLabel(s)} (click to change)", GUILayout.Width(220f)))
_capturing = true;
}
GUILayout.EndHorizontal();
GUILayout.Space(8f);
// ── Behaviour ───────────────────────────────────────────────────────────
bool pan = GUILayout.Toggle(s.panDisablesFollow, " Dragging the map cancels follow mode");
if (pan != s.panDisablesFollow) { s.panDisablesFollow = pan; PopoutModule.Persist(); }
GUILayout.Space(8f);
// ── Theme preset quick-switch ────────────────────────────────────────────
GUILayout.BeginHorizontal();
GUILayout.Label("Theme:", GUILayout.Width(kLabelW));
for (int i = 0; i < s_themeNames.Length; i++)
{
GUI.color = s.mapTheme == i ? new Color(0.5f, 0.9f, 1.0f) : Color.white;
if (GUILayout.Button(s_themeNames[i], GUILayout.Width(kThemeW)))
MapThemes.Apply((MapTheme)i);
}
GUI.color = Color.white;
GUILayout.EndHorizontal();
GUILayout.Space(4f);
// ── Overlay opacity ──────────────────────────────────────────────────────
OpacityRow("Window opacity:", ref s.overlayAlpha, 0.1f, 1.0f, Native.RRPOPOUT_SetOverlayAlpha);
OpacityRow("Map opacity:", ref s.overlayMapAlpha, 0.0f, 1.0f, Native.RRPOPOUT_SetOverlayMapAlpha);
GUILayout.Space(8f);
// ── Status + detach button ──────────────────────────────────────────────
bool det = PopoutModule.IsDetached;
GUILayout.Label(det ? "Status: Detached" : "Status: Docked");
if (GUILayout.Button(det ? "Re-attach map" : "Detach map", GUILayout.Width(160f)))
PopoutModule.Toggle();
GUILayout.Space(8f);
// ── Custom color editor (collapsible) ────────────────────────────────────
string editorHeader = _showColorEditor ? "▲ Custom Colors (click to collapse)" : "▼ Custom Colors (click to expand)";
if (GUILayout.Button(editorHeader, GUILayout.Width(300f)))
_showColorEditor = !_showColorEditor;
if (_showColorEditor)
{
GUILayout.Space(4f);
GUILayout.Label("Hex = RRGGBB (no #). Drag sliders to tune live; changes switch to Custom.");
// "Copy from:" seeds the editor from a named preset and applies it as Custom
GUILayout.BeginHorizontal();
GUILayout.Label("Copy from:", GUILayout.Width(kLabelW));
for (int i = 0; i < s_presetNames.Length; i++)
{
if (GUILayout.Button(s_presetNames[i], GUILayout.Width(kThemeW)))
{
MapThemeData pd = MapThemes.GetPresetData((MapTheme)i);
s.customTheme = pd;
InitColorEditor(ref s.customTheme);
MapThemes.Apply(MapTheme.Custom);
}
}
GUILayout.EndHorizontal();
GUILayout.Space(4f);
if (!_colorEditorInit)
InitColorEditor(ref s.customTheme);
// Each row writes directly through ref to the struct field on the heap-allocated
// PopoutSettings class, so no copy-modify-write boilerplate is needed.
bool anyChanged = false;
anyChanged |= ColorRow("Window BG", ref s.customTheme.wBgR, ref s.customTheme.wBgG, ref s.customTheme.wBgB, ref s.customTheme.wBgA, showAlpha: true, ref _hexWBg);
anyChanged |= ColorRow("Accent", ref s.customTheme.accR, ref s.customTheme.accG, ref s.customTheme.accB, ref s.customTheme.accA, showAlpha: false, ref _hexAcc);
anyChanged |= ColorRow("Text", ref s.customTheme.txtR, ref s.customTheme.txtG, ref s.customTheme.txtB, ref s.customTheme.txtA, showAlpha: false, ref _hexTxt);
anyChanged |= ColorRow("Popup BG", ref s.customTheme.popR, ref s.customTheme.popG, ref s.customTheme.popB, ref s.customTheme.popA, showAlpha: true, ref _hexPop);
anyChanged |= ColorRow("Map Tint", ref s.customTheme.mapR, ref s.customTheme.mapG, ref s.customTheme.mapB, ref s.customTheme.mapA, showAlpha: true, ref _hexMap);
anyChanged |= ColorRow("Compass BG", ref s.customTheme.cmpR, ref s.customTheme.cmpG, ref s.customTheme.cmpB, ref s.customTheme.cmpA, showAlpha: true, ref _hexCmp);
anyChanged |= ColorRow("N Needle", ref s.customTheme.nNR, ref s.customTheme.nNG, ref s.customTheme.nNB, ref s.customTheme.nNA, showAlpha: false, ref _hexNN);
anyChanged |= ColorRow("S Needle", ref s.customTheme.nSR, ref s.customTheme.nSG, ref s.customTheme.nSB, ref s.customTheme.nSA, showAlpha: false, ref _hexNS);
anyChanged |= ColorRow("Map Camera", ref s.customTheme.mapBgR, ref s.customTheme.mapBgG, ref s.customTheme.mapBgB, ref s.customTheme.mapBgA, showAlpha: false, ref _hexMapBg);
if (anyChanged)
MapThemes.Apply(MapTheme.Custom);
}
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
private static void OpacityRow(string label, ref float field, float min, float max, Action<float> setNative)
{
GUILayout.BeginHorizontal();
GUILayout.Label(label, GUILayout.Width(110f));
float nv = GUILayout.HorizontalSlider(field, min, max, GUILayout.Width(180f));
GUILayout.Label($"{field:P0}", GUILayout.Width(44f));
GUILayout.EndHorizontal();
if (Mathf.Abs(nv - field) > 0.001f)
{
field = nv;
setNative(nv);
PopoutModule.Persist();
}
}
// Syncs all nine hex strings from the given theme data snapshot.
// Call after "Copy from preset" or on first draw.
private static void InitColorEditor(ref MapThemeData t)
{
_hexWBg = ToHex(t.wBgR, t.wBgG, t.wBgB);
_hexAcc = ToHex(t.accR, t.accG, t.accB);
_hexTxt = ToHex(t.txtR, t.txtG, t.txtB);
_hexPop = ToHex(t.popR, t.popG, t.popB);
_hexMap = ToHex(t.mapR, t.mapG, t.mapB);
_hexCmp = ToHex(t.cmpR, t.cmpG, t.cmpB);
_hexNN = ToHex(t.nNR, t.nNG, t.nNB);
_hexNS = ToHex(t.nSR, t.nSG, t.nSB);
_hexMapBg = ToHex(t.mapBgR, t.mapBgG, t.mapBgB);
_colorEditorInit = true;
}
// Renders one theme element row and returns true if any value changed.
// hexStr shows RRGGBB (no #). Typing a valid 6-char hex updates R/G/B;
// moving a slider updates R/G/B and also rebuilds hexStr.
private static bool ColorRow(
string label,
ref float r, ref float g, ref float b, ref float a,
bool showAlpha, ref string hexStr)
{
bool changed = false;
GUILayout.BeginHorizontal();
GUILayout.Label(label, GUILayout.Width(kLabelW));
// Hex input
string newHex = GUILayout.TextField(hexStr, 6, GUILayout.Width(kHexW));
if (newHex != hexStr)
{
hexStr = newHex;
if (newHex.Length == 6 && ColorUtility.TryParseHtmlString("#" + newHex, out Color hc))
{
r = hc.r; g = hc.g; b = hc.b;
changed = true;
}
}
// Live color swatch
GUI.color = new Color(r, g, b, 1f);
GUILayout.Box("", GUILayout.Width(kSwatchW), GUILayout.Height(16f));
GUI.color = Color.white;
// R, G, B sliders; rebuild hex if any change
bool rC = ChanSlider("R", ref r);
bool gC = ChanSlider("G", ref g);
bool bC = ChanSlider("B", ref b);
if (rC || gC || bC) { hexStr = ToHex(r, g, b); changed = true; }
// Optional alpha slider (no hex involvement)
if (showAlpha && ChanSlider("A", ref a))
changed = true;
GUILayout.EndHorizontal();
return changed;
}
// Renders a single channel: letter label + slider (0-1) + integer value (0-255).
// Returns true if the value moved more than the movement threshold.
private static bool ChanSlider(string label, ref float v)
{
GUILayout.Label(label, GUILayout.Width(kChanL));
float nv = GUILayout.HorizontalSlider(v, 0f, 1f, GUILayout.Width(kSliderW));
GUILayout.Label(Mathf.RoundToInt(nv * 255).ToString(), GUILayout.Width(kValW));
if (Mathf.Abs(nv - v) > 0.002f) { v = nv; return true; }
return false;
}
private static string ToHex(float r, float g, float b) =>
$"{Mathf.RoundToInt(r * 255):X2}{Mathf.RoundToInt(g * 255):X2}{Mathf.RoundToInt(b * 255):X2}";
private static string HotkeyLabel(PopoutSettings s)
{
string prefix = "";
if ((s.hotkeyModifiers & 2) != 0) prefix += "Ctrl+";
if ((s.hotkeyModifiers & 1) != 0) prefix += "Shift+";
if ((s.hotkeyModifiers & 4) != 0) prefix += "Alt+";
return prefix + (KeyCode)s.hotkeyKeyCode;
}
}