using Godot; using JevOverlay.Core; namespace JevOverlay.UI; /// Godot controls only. This view cannot read files or call the game API. 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 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, }; }