namespace Cthangover.Mods.ScenarioStudio
{
using System;
using System.Collections.Generic;
using Cthangover.Core.UI.Theming;
using Cthangover.Mods.Interface.Theming;
using Godot;
///
/// "Now" tab in the author mode: the running scenario as script with a
/// gutter () and the insert bar for a line
/// after the current one. It ends with the "this change affects" plate
/// naming the mod that receives the edit and the mod it overrides, so
/// nobody changes someone else's content unknowingly; the novice mode
/// () shows the same plate.
///
/// Dragging the ▶ marker of the current line highlights the line under the
/// mouse when the running dialog can continue from it
/// () and on release asks for it
/// with ; a drop anywhere else does nothing.
///
/// One instance lives as long as the dock; refills the
/// body and wires the controls to this instance's handlers.
///
public sealed class StudioNowPage
{
/// A gutter click changed a breakpoint.
public event Action BreakpointToggled;
/// One of the insert buttons was pressed.
public event Action InsertRequested;
/// The ▶ marker was dropped on a line the dialog can continue from.
public event Action NextLineRequested;
private readonly List _views = new();
private StudioCodeLineView _dropTarget;
private bool _dragging;
/// True while the ▶ marker is dragged: a redraw would drop the drag.
public bool IsDragging => _dragging;
public void Build(VBoxContainer content, StudioSession session)
{
_views.Clear();
_dropTarget = null;
_dragging = false;
if (!session.HasScenario)
{
content.AddChild(UiKit.EmptyState(TranslationServer.Translate("studio/no_scenario"), UiTokens.FontBody));
return;
}
var well = new PanelContainer { MouseFilter = Control.MouseFilterEnum.Ignore };
well.AddThemeStyleboxOverride("panel", StudioStyles.CodeWell());
content.AddChild(well);
var lines = UiKit.Column(0);
well.AddChild(lines);
foreach (var line in session.Lines)
{
var view = new StudioCodeLineView(line);
view.BreakpointToggled += OnBreakpointToggled;
view.MarkerDragged += OnMarkerDragged;
view.MarkerDropped += OnMarkerDropped;
lines.AddChild(view);
_views.Add(view);
}
var at = session.InsertionTextIndexAfterCurrent();
var caption = at <= 0 ? string.Empty : string.Format(TranslationServer.Translate("studio/after_line"), at);
content.AddChild(BuildInsertBar(caption));
content.AddChild(BuildScopeBanner(session));
}
private Control BuildInsertBar(string caption)
{
var bar = new PanelContainer { MouseFilter = Control.MouseFilterEnum.Ignore };
bar.AddThemeStyleboxOverride("panel", StudioStyles.InsertBar());
var row = new HFlowContainer { MouseFilter = Control.MouseFilterEnum.Ignore };
row.AddThemeConstantOverride("h_separation", UiTokens.Space2);
row.AddThemeConstantOverride("v_separation", UiTokens.Space2);
bar.AddChild(row);
if (!string.IsNullOrEmpty(caption))
{
var label = StudioStyles.Text(caption, UiTokens.FontSmall, UiTokens.Faint);
label.VerticalAlignment = VerticalAlignment.Center;
label.SizeFlagsVertical = Control.SizeFlags.ShrinkCenter;
row.AddChild(label);
}
row.AddChild(InsertButton("studio/add_line", OnAddLine));
row.AddChild(InsertButton("studio/add_choice", OnAddChoice));
row.AddChild(InsertButton("studio/add_jump", OnAddJump));
return bar;
}
private static Button InsertButton(string key, Action handler)
{
var button = StudioStyles.SmallButton(new Button(), TranslationServer.Translate(key));
button.Pressed += handler;
return button;
}
///
/// Inline editor of the whole scenario text (the footer's "Edit text"),
/// filling the body; the dock reads the text back when editing ends.
///
public static CodeEdit BuildEditor(VBoxContainer content, string text)
{
var editor = new CodeEdit
{
Text = text ?? string.Empty,
GuttersDrawLineNumbers = true,
HighlightCurrentLine = true,
WrapMode = TextEdit.LineWrappingMode.None,
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
CustomMinimumSize = new Vector2(0, 560),
SyntaxHighlighter = StudioSyntax.CreateHighlighter(),
};
editor.AddThemeFontOverride("font", StudioStyles.Mono);
editor.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
content.AddChild(editor);
return editor;
}
/// "This change affects" plate: the edit scope and, when it differs, the mod it overrides.
public static Control BuildScopeBanner(StudioSession session)
{
var banner = new PanelContainer { MouseFilter = Control.MouseFilterEnum.Ignore };
banner.AddThemeStyleboxOverride("panel", StudioStyles.ScopeBanner());
var column = UiKit.Column(UiTokens.Space2);
banner.AddChild(column);
var title = StudioStyles.Text(TranslationServer.Translate("studio/affects"), UiTokens.FontTiny, UiTokens.Warning);
title.Uppercase = true;
column.AddChild(title);
var row = new HFlowContainer { MouseFilter = Control.MouseFilterEnum.Ignore };
row.AddThemeConstantOverride("h_separation", UiTokens.Space3);
column.AddChild(row);
if (session.EditScope == null)
return banner;
row.AddChild(StudioStyles.Scope(session.EditScope));
if (session.EditScope.ModId != session.FileScope.ModId)
{
row.AddChild(StudioStyles.Text(TranslationServer.Translate("studio/overrides"), UiTokens.FontSmall, UiTokens.Faint));
row.AddChild(StudioStyles.Scope(session.FileScope));
}
return banner;
}
private void OnBreakpointToggled(StudioCodeLine line) => BreakpointToggled?.Invoke(line);
private void OnMarkerDragged(Vector2 at)
{
_dragging = true;
var target = DropTargetAt(at);
if (target == _dropTarget)
return;
_dropTarget?.ShowDropTarget(false);
_dropTarget = target;
_dropTarget?.ShowDropTarget(true);
}
private void OnMarkerDropped(Vector2 at)
{
_dragging = false;
_dropTarget?.ShowDropTarget(false);
_dropTarget = null;
var target = DropTargetAt(at);
if (target != null)
NextLineRequested?.Invoke(target.Line);
}
/// The row under (canvas coordinates) the dialog can continue from, or null.
private StudioCodeLineView DropTargetAt(Vector2 at)
{
foreach (var view in _views)
{
if (view.Line.CanRunNext && view.GetGlobalRect().HasPoint(at))
return view;
}
return null;
}
private void OnAddLine() => InsertRequested?.Invoke(StudioLineKind.Line);
private void OnAddChoice() => InsertRequested?.Invoke(StudioLineKind.Choice);
private void OnAddJump() => InsertRequested?.Invoke(StudioLineKind.Jump);
}
}