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)
258 lines
12 KiB
C#
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;
|
|
}
|
|
}
|