sts2-bot/ui/JevOverlay/UI/OverlayView.cs
0xrsydn 130273570b feat(ui): integrate read-only Jev decision overlay
Initialize the Godot panel, poll the decision log in the background, and preserve inspection state during updates. Use a .conf file to keep local settings separate from mod manifests.
2026-09-23 13:40:02 +07:00

187 lines
7.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Godot;
using JevOverlay.Core;
namespace JevOverlay.UI;
/// <summary>Godot controls only. This view cannot read files or call the game API.</summary>
public sealed class OverlayView
{
private readonly CanvasLayer layer = new() { Name = "JevDecisionOverlay", Layer = 100 };
private readonly PanelContainer panel = new() { MouseFilter = Control.MouseFilterEnum.Stop };
private readonly VBoxContainer body = new();
private readonly Label status = new() { AutowrapMode = TextServer.AutowrapMode.WordSmart };
private readonly RichTextLabel summary = TextArea();
private readonly RichTextLabel evidence = TextArea();
private readonly ItemList history = new()
{
CustomMinimumSize = new(0, 110),
FocusMode = Control.FocusModeEnum.None,
SizeFlagsVertical = Control.SizeFlags.ExpandFill,
};
private readonly Button collapse = Button("", "Collapse panel (F8)");
private readonly Button details = Button("Show questions and answers");
private readonly Button follow = Button("Follow latest");
private IReadOnlyList<DecisionEntry> entries = [];
private DecisionKey? selected;
private bool following = true;
private bool collapsed;
private bool expanded;
public OverlayView(SceneTree tree)
{
var root = new Control { MouseFilter = Control.MouseFilterEnum.Ignore };
layer.AddChild(root);
tree.Root.AddChild(layer);
root.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.FullRect);
root.AddChild(panel);
panel.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.TopRight);
var style = new StyleBoxFlat
{
BgColor = new Color(0.055f, 0.065f, 0.085f, 0.96f),
BorderColor = new Color(0.3f, 0.4f, 0.5f),
BorderWidthLeft = 1, BorderWidthRight = 1, BorderWidthTop = 1, BorderWidthBottom = 1,
CornerRadiusTopLeft = 8, CornerRadiusTopRight = 8,
CornerRadiusBottomLeft = 8, CornerRadiusBottomRight = 8,
ContentMarginLeft = 12, ContentMarginRight = 12,
ContentMarginTop = 10, ContentMarginBottom = 10,
};
panel.AddThemeStyleboxOverride("panel", style);
panel.Theme = new Theme { DefaultFontSize = 16 };
panel.AddThemeColorOverride("font_color", new Color(0.92f, 0.94f, 0.96f));
var layout = new VBoxContainer();
layout.AddThemeConstantOverride("separation", 8);
panel.AddChild(layout);
var header = new HBoxContainer();
layout.AddChild(header);
header.AddChild(new Label
{
Text = "Jev decisions · read only",
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
MouseFilter = Control.MouseFilterEnum.Ignore,
});
header.AddChild(collapse);
collapse.Pressed += ToggleCollapsed;
layout.AddChild(body);
body.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
body.AddThemeConstantOverride("separation", 8);
body.AddChild(status);
body.AddChild(summary);
body.AddChild(details);
body.AddChild(evidence);
evidence.Visible = false;
details.Pressed += () =>
{
expanded = !expanded;
evidence.Visible = expanded;
details.Text = expanded ? "Hide questions and answers" : "Show questions and answers";
};
body.AddChild(new Label { Text = "Recent decisions · select to inspect" });
body.AddChild(history);
body.AddChild(follow);
follow.ToggleMode = true;
follow.ButtonPressed = true;
follow.Toggled += enabled =>
{
following = enabled;
if (enabled) SelectLatest();
};
history.ItemSelected += index =>
{
if (index < 0 || index >= entries.Count) return;
following = false;
follow.SetPressedNoSignal(false);
selected = entries[(int)index].Key;
RenderSelection();
};
summary.Text = "Waiting for decision records.\nF8 collapses or expands this panel.";
}
public void ToggleCollapsed()
{
collapsed = !collapsed;
body.Visible = !collapsed;
collapse.Text = collapsed ? "+" : "";
collapse.TooltipText = collapsed ? "Expand panel (F8)" : "Collapse panel (F8)";
}
public void Resize(Vector2 viewport)
{
panel.OffsetLeft = -Math.Min(440, Math.Max(280, viewport.X - 32));
panel.OffsetRight = -16;
panel.OffsetTop = 64;
panel.OffsetBottom = collapsed ? 112 : Math.Min(viewport.Y - 16, expanded ? 764 : 584);
}
public void SetStatus(string text, string? tooltip = null)
{
status.Text = text;
status.TooltipText = tooltip ?? text;
}
public void Render(DecisionHistory model)
{
// Copy the list; selection must refer to the same ordering as the visible rows.
entries = model.Entries.ToArray();
history.Clear();
foreach (var entry in entries)
{
int index = history.AddItem($"#{entry.Key.Step} {entry.Source.ToUpperInvariant()} {entry.Action}");
history.SetItemTooltip(index, $"Session: {entry.Key.Session}\n{entry.Status}");
history.SetItemCustomFgColor(index, entry.Source switch
{
"jev" => new Color(0.55f, 0.8f, 1f),
"fallback" => new Color(1f, 0.75f, 0.4f),
_ => new Color(0.8f, 0.85f, 0.85f),
});
}
if (following) SelectLatest();
else RenderSelection();
}
private void SelectLatest()
{
selected = entries.Count == 0 ? null : entries[0].Key;
RenderSelection();
if (entries.Count > 0) history.EnsureCurrentIsVisible();
}
private void RenderSelection()
{
int index = -1;
for (int i = 0; i < entries.Count; i++)
if (entries[i].Key == selected) { index = i; break; }
if (index < 0)
{
summary.Text = selected is null ? "Waiting for decision records."
: "This decision is no longer in the recent history. Select Follow latest.";
evidence.Text = "No decision selected.";
}
else
{
history.Select(index);
// Do not reset text selection or scroll when another decision arrives.
string nextSummary = entries[index].Summary();
string nextEvidence = entries[index].Evidence();
if (summary.Text != nextSummary) summary.Text = nextSummary;
if (evidence.Text != nextEvidence) evidence.Text = nextEvidence;
}
}
public void Dispose() => layer.QueueFree();
private static Button Button(string text, string tooltip = "") => new()
{
Text = text, TooltipText = tooltip, FocusMode = Control.FocusModeEnum.None,
};
private static RichTextLabel TextArea() => new()
{
BbcodeEnabled = false,
SelectionEnabled = true,
ScrollActive = true,
CustomMinimumSize = new(0, 90),
SizeFlagsVertical = Control.SizeFlags.ExpandFill,
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
FocusMode = Control.FocusModeEnum.None,
};
}