feat(ui): add decision-log reader and overlay components
This commit is contained in:
parent
c1259ecba4
commit
2dc711a55c
9 changed files with 469 additions and 0 deletions
184
ui/JevOverlay/UI/OverlayView.cs
Normal file
184
ui/JevOverlay/UI/OverlayView.cs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
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);
|
||||
summary.Text = entries[index].Summary();
|
||||
evidence.Text = entries[index].Evidence();
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue