upd: sync local changes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7a30f4b8c5
commit
6884cd5374
36 files changed
+3363
-6
No files matched your search
@@ -0,0 +1,21 @@
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Mods.Interface.Base
|
||||
{
|
||||
/// <summary>
|
||||
/// Forwarder pattern for Godot input: proxies _Input to a virtual OnInput
|
||||
/// so derived classes can handle input events without needing to know about
|
||||
/// Godot's _Input override mechanics.
|
||||
/// </summary>
|
||||
public partial class InputHandlerNode : Node
|
||||
{
|
||||
/// <summary>Engine entry point: forwards the input event to <see cref="OnInput"/>.</summary>
|
||||
public override void _Input(InputEvent @event)
|
||||
{
|
||||
OnInput(@event);
|
||||
}
|
||||
|
||||
/// <summary>Override in derived classes to receive Godot input events. Called by <see cref="_Input"/> which proxies from the engine.</summary>
|
||||
protected virtual void OnInput(InputEvent @event) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Cthangover.Mods.Interface.Base.Lists
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializable margin/padding struct for list layout calculations.
|
||||
/// Marked [System.Serializable] for potential JSON serialization in mod configs.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public struct Border
|
||||
{
|
||||
/// <summary>Left margin or padding, in pixels.</summary>
|
||||
public float left;
|
||||
/// <summary>Right margin or padding, in pixels.</summary>
|
||||
public float right;
|
||||
/// <summary>Top margin or padding, in pixels.</summary>
|
||||
public float top;
|
||||
/// <summary>Bottom margin or padding, in pixels.</summary>
|
||||
public float bottom;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Mods.Interface.Base.Lists
|
||||
{
|
||||
/// <summary>
|
||||
/// Extends IListWidget with grid/cell dimensions. CellSize drives item sizing;
|
||||
/// ViewCellsCount determines how many cells fit in the visible area (used for
|
||||
/// scroll/viewport calculations).
|
||||
/// </summary>
|
||||
public interface ICellListWidget<TItem, TModel> : IListWidget<TItem, TModel>
|
||||
where TItem : class, IListItem<TModel>
|
||||
{
|
||||
|
||||
/// <summary>Pixel dimensions of each cell in the grid. Drives item sizing; derived from content width and aspect ratio.</summary>
|
||||
Vector2 CellSize { get; }
|
||||
|
||||
/// <summary>Number of cells visible in the viewport (columns × rows). Used for scroll range and visibility calculations.</summary>
|
||||
Vector2I ViewCellsCount { get; }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Mods.Interface.Base.Lists
|
||||
{
|
||||
/// <summary>
|
||||
/// Contract for a single item in a list widget. Construct binds a model;
|
||||
/// Destruct tears down (typically QueueFree). The Rect getter exposes the
|
||||
/// item's Control for layout positioning by the parent list.
|
||||
/// </summary>
|
||||
public interface IListItem<TModel>
|
||||
{
|
||||
|
||||
/// <summary>The Control that this item occupies. Used by the parent list to position the item in layout.</summary>
|
||||
Control Rect { get; }
|
||||
|
||||
/// <summary>The bound data model. Set via <see cref="Construct"/>.</summary>
|
||||
TModel Model { get; }
|
||||
|
||||
/// <summary>Binds the item to a data model. Called by the list widget after instantiation, before layout positioning.</summary>
|
||||
void Construct(TModel model);
|
||||
|
||||
/// <summary>Cleans up the item — typically disconnects signals and calls QueueFree. Called when the list is destroyed or refreshed.</summary>
|
||||
void Destruct();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Collections.Generic;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Core.UI;
|
||||
|
||||
namespace Cthangover.Mods.Interface.Base.Lists
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic list widget contract: binds a collection of models to Item widgets.
|
||||
/// The two-step CreateModels() → CreateContent() pattern separates data sourcing
|
||||
/// from instantiation, so subclasses can pull models from anywhere (runtime state,
|
||||
/// config, save data) while the framework handles layout. ConstructUI/DestructUI
|
||||
/// manage the full lifecycle of child items.
|
||||
/// </summary>
|
||||
public interface IListWidget<TItem, TModel> : IWidget
|
||||
where TItem : class, IListItem<TModel>
|
||||
{
|
||||
|
||||
/// <summary>Padding between items in pixels, stored as Vector2(X, Y). The X component is horizontal gap, Y is vertical gap.</summary>
|
||||
Vector2 ItemsCellPadding { get; }
|
||||
|
||||
/// <summary>The container Control that holds all child item nodes. Items are added as children of this node.</summary>
|
||||
Control Content { get; }
|
||||
|
||||
/// <summary>The collection of instantiated item widgets. Populated by <see cref="ConstructUI"/> and cleared by <see cref="DestructUI"/>.</summary>
|
||||
List<TItem> Items { get; set; }
|
||||
|
||||
/// <summary>Implementations return the collection of data models to display. Called once per <see cref="ConstructUI"/> cycle.</summary>
|
||||
ICollection<TModel> CreateModels();
|
||||
|
||||
/// <summary>Instantiates item widgets from the model collection. Separates model sourcing from instantiation so subclasses control both steps.</summary>
|
||||
List<TItem> CreateContent(ICollection<TModel> models);
|
||||
|
||||
/// <summary>Calculates the total content size in pixels for <paramref name="count"/> items, used for scroll container sizing.</summary>
|
||||
Vector2 GetContentSize(int count);
|
||||
|
||||
/// <summary>Builds the full item tree: creates models, instantiates items, and positions them via layout.</summary>
|
||||
void ConstructUI();
|
||||
|
||||
/// <summary>Tears down all child items by calling each item's <see cref="IListItem{TModel}.Destruct"/> and clearing the Items list.</summary>
|
||||
void DestructUI();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Mods.Interface.Base.Lists.Impls
|
||||
{
|
||||
/// <summary>
|
||||
/// Grid-based list layout with configurable column count, aspect-ratio cells,
|
||||
/// and border/padding offsets. Cell dimensions are computed dynamically from
|
||||
/// the content area width, so they scale with the parent. GetContentSize
|
||||
/// calculates the total scrollable height from item count and cell geometry,
|
||||
/// enabling vertical scrolling through a grid. PutToLayout positions items
|
||||
/// by flat-index-to-grid conversion. The isScaledCanvas flag supports
|
||||
/// resolution-independent layouts.
|
||||
/// </summary>
|
||||
public abstract partial class ColumnCellListWidget<TItem, TModel> : ListWidget<TItem, TModel>, ICellListWidget<TItem, TModel>
|
||||
where TItem : Control, IListItem<TModel>
|
||||
{
|
||||
[Export] private int columnCount;
|
||||
[Export] private float aspectHeight = 1f;
|
||||
[Export] private float borderLeft;
|
||||
[Export] private float borderRight;
|
||||
[Export] private float borderTop;
|
||||
[Export] private float borderBottom;
|
||||
[Export] private bool isScaledCanvas = true;
|
||||
|
||||
private Border contentBorder => new() { left = borderLeft, right = borderRight, top = borderTop, bottom = borderBottom };
|
||||
|
||||
/// <summary>Dynamically computed cell dimensions in pixels. Width = (content width - borders - padding * (columns - 1)) / columns; height = width * aspectHeight.</summary>
|
||||
public Vector2 CellSize => GetCellSize(Content.Size);
|
||||
|
||||
private Vector2 GetCellSize(Vector2 contentSize)
|
||||
{
|
||||
var size = contentSize;
|
||||
var effectiveWidth = size.X - contentBorder.left - contentBorder.right;
|
||||
var cellWidth = (effectiveWidth - (columnCount - 1) * ItemsCellPadding.X) / columnCount;
|
||||
var cellHeight = cellWidth * aspectHeight;
|
||||
return new Vector2(cellWidth, cellHeight);
|
||||
}
|
||||
|
||||
/// <summary>Number of cells visible in the viewport: X = columnCount, Y = floor((content height - borders) / (cell height + padding)).</summary>
|
||||
public Vector2I ViewCellsCount
|
||||
{
|
||||
get
|
||||
{
|
||||
var size = Content.Size;
|
||||
return new Vector2I(
|
||||
columnCount,
|
||||
Mathf.FloorToInt((size.Y - contentBorder.top - contentBorder.bottom) / (CellSize.Y + ItemsCellPadding.Y))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Calculates total scrollable content height: top border + bottom border + rows * cellHeight + (rows - 1) * padding.</summary>
|
||||
public override Vector2 GetContentSize(int count)
|
||||
{
|
||||
if (count == 0) return Vector2.Zero;
|
||||
|
||||
var rect = Content.Size;
|
||||
var effectiveWidth = rect.X - contentBorder.left - contentBorder.right;
|
||||
var cellWidth = (effectiveWidth - (columnCount - 1) * ItemsCellPadding.X) / columnCount;
|
||||
var cellHeight = cellWidth * aspectHeight;
|
||||
|
||||
var rows = Mathf.CeilToInt((float)count / columnCount);
|
||||
var contentHeight = contentBorder.top + contentBorder.bottom + rows * cellHeight + (rows - 1) * ItemsCellPadding.Y;
|
||||
var contentWidth = rect.X;
|
||||
|
||||
return new Vector2(contentWidth, contentHeight);
|
||||
}
|
||||
|
||||
protected override void PutToLayout(TItem item, int index, Control container, Vector2 contentSize)
|
||||
{
|
||||
var rectTransform = item;
|
||||
var cellIndex = GetIndex(index);
|
||||
var cellSize = GetCellSize(contentSize);
|
||||
|
||||
if (rectTransform.GetParent() != container)
|
||||
rectTransform.Reparent(container, false);
|
||||
rectTransform.Position = new Vector2(
|
||||
contentBorder.left + cellIndex.X * (cellSize.X + ItemsCellPadding.X),
|
||||
contentBorder.top + cellIndex.Y * (cellSize.Y + ItemsCellPadding.Y)
|
||||
);
|
||||
rectTransform.Size = cellSize;
|
||||
}
|
||||
|
||||
private Vector2I GetIndex(int flatIndex)
|
||||
{
|
||||
var count = ViewCellsCount;
|
||||
return new Vector2I(
|
||||
flatIndex % count.X,
|
||||
flatIndex / count.X
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Mods.Interface.Base.Lists.Impls
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple vertical stack layout. Items are placed top-to-bottom with padding
|
||||
/// between them, offset so the list grows upward from the content center.
|
||||
/// Content height is the sum of all item heights plus padding — no pooling
|
||||
/// or virtualization, so it's intended for short lists.
|
||||
/// </summary>
|
||||
public abstract partial class VerticalListWidget<TItem, TModel> : ListWidget<TItem, TModel>
|
||||
where TItem : Control, IListItem<TModel>
|
||||
{
|
||||
|
||||
protected override void PutToLayout(TItem item, int index, Control container, Vector2 contentSize)
|
||||
{
|
||||
var posY = index * (PrefabRect.Size.Y + ItemsCellPadding.Y);
|
||||
var itemRect = item;
|
||||
var pos = new Vector2(itemRect.Position.X, -posY + contentSize.Y * 0.5f);
|
||||
itemRect.Position = pos;
|
||||
}
|
||||
|
||||
/// <summary>Content height = count * (prefab height + Y padding). Width is inherited from the Content container.</summary>
|
||||
public override Vector2 GetContentSize(int count)
|
||||
{
|
||||
var boxSize = Content.Size;
|
||||
boxSize.Y = count * (PrefabRect.Size.Y + ItemsCellPadding.Y);
|
||||
return boxSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Mods.Interface.Base.Lists
|
||||
{
|
||||
/// <summary>
|
||||
/// Base list item: stores the bound model and provides a Rect for layout.
|
||||
/// The virtual Construct saves the model reference; Destruct is abstract so
|
||||
/// subclasses must explicitly implement cleanup (signal disconnection, QueueFree).
|
||||
/// </summary>
|
||||
public abstract partial class ListItem<TModel> : Control, IListItem<TModel>
|
||||
{
|
||||
|
||||
/// <summary>The data model bound to this list item. Set by <see cref="Construct"/>.</summary>
|
||||
public TModel Model { get; private set; }
|
||||
/// <summary>The Control that this item occupies for layout. Returns itself — ListItem is its own layout root.</summary>
|
||||
public Control Rect => this;
|
||||
|
||||
/// <summary>Saves the model reference. Override in subclasses to apply model data to UI elements.</summary>
|
||||
public virtual void Construct(TModel model)
|
||||
{
|
||||
Model = model;
|
||||
}
|
||||
|
||||
/// <summary>Required cleanup method. Subclasses must implement — typically disconnects signals and calls QueueFree.</summary>
|
||||
public abstract void Destruct();
|
||||
|
||||
/// <summary>Engine callback; forwards to <see cref="OnReady"/> so mod subclasses receive the ready signal without their own engine bindings.</summary>
|
||||
public override void _Ready() => OnReady();
|
||||
|
||||
/// <summary>Forwarded from <see cref="_Ready"/>. Override for mod item initialization.</summary>
|
||||
protected virtual void OnReady() { }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using System.Collections.Generic;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Mods.Interface.Base.Lists
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstract model-driven list: instantiates items from a PackedScene prefab
|
||||
/// for each model in CreateModels(), then delegates positioning to the abstract
|
||||
/// PutToLayout (defined by layout subclasses like ColumnCellListWidget or
|
||||
/// VerticalListWidget). Refresh() tears down and rebuilds the list in place
|
||||
/// for reactive updates. The virtual Create/Init hooks let subclasses inject
|
||||
/// extra setup between instantiation and model binding without overriding
|
||||
/// CreateContent.
|
||||
/// </summary>
|
||||
public abstract partial class ListWidget<T, M> : Widget, IListWidget<T, M>
|
||||
where T : Control, IListItem<M>
|
||||
{
|
||||
|
||||
[Export]
|
||||
private Control content;
|
||||
|
||||
[Export]
|
||||
private PackedScene itemScene;
|
||||
|
||||
[Export] private Vector2 itemsCellPadding = new(0f, 1f);
|
||||
|
||||
/// <summary>Editor-configured padding between items; exposed for layout size math.</summary>
|
||||
public Vector2 ItemsCellPadding => itemsCellPadding;
|
||||
|
||||
/// <summary>Prefab instance for layout measurement. Instantiated on each access — not cached. Returns null if itemScene is not set.</summary>
|
||||
protected T Prefab => itemScene?.Instantiate<T>();
|
||||
/// <summary>The Control rect of the item prefab, used for layout size calculations.</summary>
|
||||
protected Control PrefabRect => Prefab;
|
||||
|
||||
/// <summary>The container Control that holds child items. Set via [Export] in the Godot editor.</summary>
|
||||
public Control Content => content;
|
||||
|
||||
/// <summary>Calculates the total content size for a given item count. Subclass-specific — different layouts have different formulas.</summary>
|
||||
public abstract Vector2 GetContentSize(int count);
|
||||
|
||||
/// <summary>The instantiated item collection. Set by <see cref="ConstructUI"/> and cleared by <see cref="DestructUI"/>.</summary>
|
||||
public List<T> Items { get; set; }
|
||||
|
||||
/// <summary>Shows the widget and rebuilds the UI tree via <see cref="ConstructUI"/>.</summary>
|
||||
public override void Show()
|
||||
{
|
||||
base.Show();
|
||||
ConstructUI();
|
||||
}
|
||||
|
||||
/// <summary>Tears down child items via <see cref="DestructUI"/> then hides via base <see cref="Widget.Hide"/>.</summary>
|
||||
public override void Hide()
|
||||
{
|
||||
DestructUI();
|
||||
base.Hide();
|
||||
}
|
||||
|
||||
/// <summary>Destructs and reconstructs the item tree in place. Use for reactive updates when the model collection changes.</summary>
|
||||
public void Refresh()
|
||||
{
|
||||
if (!Visible)
|
||||
return;
|
||||
|
||||
DestructUI();
|
||||
ConstructUI();
|
||||
}
|
||||
|
||||
/// <summary>Iterates <paramref name="models"/> and calls <see cref="Create"/> for each, building the item widget collection.</summary>
|
||||
public List<T> CreateContent(ICollection<M> models)
|
||||
{
|
||||
var items = new List<T>();
|
||||
|
||||
foreach (var model in models)
|
||||
{
|
||||
var item = Create(model);
|
||||
item.Construct(model);
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full build pipeline: gets models via <see cref="CreateModels"/>, instantiates items via <see cref="CreateContent"/>,
|
||||
/// sets the content container size via <see cref="CallDeferred"/> to size, then positions each item via <see cref="PutToLayout"/>.
|
||||
/// </summary>
|
||||
public void ConstructUI()
|
||||
{
|
||||
var rect = Content;
|
||||
var models = CreateModels();
|
||||
|
||||
var items = CreateContent(models);
|
||||
var size = GetContentSize(items.Count);
|
||||
rect.CallDeferred("set_size", size);
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
PutToLayout(items[i], i, rect, size);
|
||||
|
||||
Items = items;
|
||||
}
|
||||
|
||||
/// <summary>Subclasses implement positioning logic. Called for each item with its flat index, the content container, and total content size.</summary>
|
||||
protected abstract void PutToLayout(T item, int index, Control container, Vector2 contentSize);
|
||||
|
||||
/// <summary>Subclasses implement model sourcing. Called by <see cref="ConstructUI"/> before item instantiation.</summary>
|
||||
public abstract ICollection<M> CreateModels();
|
||||
|
||||
/// <summary>Hook for subclasses to inject setup between instantiation and model binding. Called by <see cref="Create"/>.</summary>
|
||||
protected virtual void Init(T item, M model) { }
|
||||
|
||||
/// <summary>Instantiates the PackedScene prefab, adds it to <see cref="Content"/>, and calls <see cref="Init"/> for subclass setup.</summary>
|
||||
protected virtual T Create(M model)
|
||||
{
|
||||
var instance = itemScene.Instantiate<T>();
|
||||
Content.AddChild(instance);
|
||||
Init(instance, model);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// <summary>Calls <see cref="IListItem{TModel}.Destruct"/> on each child item, clears the list, and sets Items to null.</summary>
|
||||
public void DestructUI()
|
||||
{
|
||||
if (Items == null || Items.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var item in Items)
|
||||
item.Destruct();
|
||||
|
||||
Items.Clear();
|
||||
Items = null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Core.UI;
|
||||
|
||||
namespace Cthangover.Mods.Interface.Base.Lists
|
||||
{
|
||||
/// <summary>
|
||||
/// Lightweight alternative to Widget for mod-authored controls. Has a simpler
|
||||
/// lifecycle — just Construct() on first access and Destruct() on tree exit —
|
||||
/// without the visibility interception and Show/Hide pipeline that Widget
|
||||
/// carries. Use when the heavy Widget state machine would be overkill or
|
||||
/// when the control's visibility is managed by Godot's native system.
|
||||
/// </summary>
|
||||
public abstract partial class ModWidget : Control, IUiLazyConstruct
|
||||
{
|
||||
private bool _constructed;
|
||||
|
||||
/// <summary>Idempotent construction gate: calls <see cref="Construct"/> exactly once, on the first invocation. Thread-safe for repeated external calls.</summary>
|
||||
public void EnsureConstructed()
|
||||
{
|
||||
if (_constructed)
|
||||
return;
|
||||
_constructed = true;
|
||||
|
||||
Construct();
|
||||
}
|
||||
|
||||
/// <summary>Runs <see cref="Destruct"/> once when the constructed control leaves the tree.</summary>
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (_constructed)
|
||||
Destruct();
|
||||
}
|
||||
|
||||
protected abstract void Construct();
|
||||
protected virtual void Destruct() { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Mods.Interface.Base.Lists
|
||||
{
|
||||
/// <summary>
|
||||
/// Shader-based scene transition: animates a "progress" uniform on the
|
||||
/// background's ShaderMaterial via a Godot Tween, then reveals content after
|
||||
/// a configurable delay. The shader is resolved through the mod system
|
||||
/// (ModManager.ResolveShader), allowing mods to replace the transition effect.
|
||||
/// Uses an easeInOut curve for the progress ramp. Material is duplicated on
|
||||
/// first use so each instance gets its own shader parameters. On tree exit,
|
||||
/// the duplicated material is disposed to avoid leaks.
|
||||
/// </summary>
|
||||
public abstract partial class TransitionWidget : Widget
|
||||
{
|
||||
private static readonly StringName ProgressParam = "progress";
|
||||
|
||||
[Export] private TextureRect imgBackground;
|
||||
[Export] private Control contentGroup;
|
||||
[Export] private float transitionDuration = 0.8f;
|
||||
[Export] private float contentDelay = 0.05f;
|
||||
|
||||
private ShaderMaterial materialInstance;
|
||||
private Tween transitionTween;
|
||||
|
||||
/// <summary>
|
||||
/// Shows the widget base, then creates or reuses a shader material for the background and starts the transition tween.
|
||||
/// Material is duplicated from <c>imgBackground.Material</c> on first call so each instance gets independent shader parameters.
|
||||
/// </summary>
|
||||
public override void Show()
|
||||
{
|
||||
base.Show();
|
||||
|
||||
if (materialInstance == null && imgBackground != null)
|
||||
{
|
||||
materialInstance = imgBackground.Material?.Duplicate() as ShaderMaterial;
|
||||
if (materialInstance == null)
|
||||
{
|
||||
materialInstance = new ShaderMaterial();
|
||||
var shader = Shaders.Resolve("scene_transition");
|
||||
if (shader != null)
|
||||
materialInstance.Shader = shader;
|
||||
}
|
||||
imgBackground.Material = materialInstance;
|
||||
materialInstance.SetShaderParameter(ProgressParam, 0f);
|
||||
}
|
||||
|
||||
if (transitionTween != null)
|
||||
{
|
||||
transitionTween.Kill();
|
||||
transitionTween = null;
|
||||
}
|
||||
|
||||
RunShowAnimation();
|
||||
}
|
||||
|
||||
/// <summary>Kills the active transition tween if one is running, then calls base <see cref="Widget.Hide"/>.</summary>
|
||||
public override void Hide()
|
||||
{
|
||||
if (transitionTween != null)
|
||||
{
|
||||
transitionTween.Kill();
|
||||
transitionTween = null;
|
||||
}
|
||||
base.Hide();
|
||||
}
|
||||
|
||||
private void RunShowAnimation()
|
||||
{
|
||||
if (contentGroup != null)
|
||||
{
|
||||
contentGroup.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||
contentGroup.MouseFilter = MouseFilterEnum.Ignore;
|
||||
}
|
||||
|
||||
transitionTween = CreateTween();
|
||||
transitionTween.SetProcessMode(Tween.TweenProcessMode.Physics);
|
||||
transitionTween.TweenMethod(Callable.From<float>(OnProgressUpdate), 0f, 1f, transitionDuration);
|
||||
transitionTween.Finished += OnTweenFinished;
|
||||
}
|
||||
|
||||
private void OnProgressUpdate(float t)
|
||||
{
|
||||
float curvedT = EaseInOut(t);
|
||||
if (imgBackground?.Material is ShaderMaterial mat)
|
||||
mat.SetShaderParameter(ProgressParam, curvedT);
|
||||
}
|
||||
|
||||
private void OnTweenFinished()
|
||||
{
|
||||
if (imgBackground?.Material is ShaderMaterial mat)
|
||||
mat.SetShaderParameter(ProgressParam, 1f);
|
||||
|
||||
var timer = GetTree().CreateTimer(contentDelay);
|
||||
timer.Timeout += () =>
|
||||
{
|
||||
if (contentGroup != null)
|
||||
{
|
||||
contentGroup.Modulate = UiTokens.White;
|
||||
contentGroup.MouseFilter = MouseFilterEnum.Stop;
|
||||
}
|
||||
};
|
||||
|
||||
transitionTween = null;
|
||||
}
|
||||
|
||||
private static float EaseInOut(float t)
|
||||
{
|
||||
return t < 0.5f ? 2f * t * t : 1f - Mathf.Pow(-2f * t + 2f, 2f) / 2f;
|
||||
}
|
||||
|
||||
/// <summary>Disposes the per-instance shader material created for the transition.</summary>
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (materialInstance != null)
|
||||
{
|
||||
materialInstance.Dispose();
|
||||
materialInstance = null;
|
||||
}
|
||||
base._ExitTree();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Core.UI;
|
||||
|
||||
namespace Cthangover.Mods.Interface.Base.Lists
|
||||
{
|
||||
/// <summary>
|
||||
/// Base widget with a lazy construct/destruct lifecycle around Show/Hide.
|
||||
/// On first Show(), OnceConstruct() fires once via EnsureConstructed — subsequent
|
||||
/// Show/Hide calls trigger ShowConstruct/HideDestruct instead, allowing repeated
|
||||
/// state reset without full reconstruction. Overrides Godot's built-in Visible
|
||||
/// property to intercept visibility changes and route them through the lifecycle.
|
||||
/// Maintains an independent isVisible flag because Godot's visibility has quirks
|
||||
/// (e.g. parent visibility propagation) that would break the state machine.
|
||||
/// The #if TOOLS _ValidateProperty syncs the editor inspector with the override.
|
||||
/// </summary>
|
||||
public abstract partial class Widget : Control, IWidget
|
||||
{
|
||||
private bool isConstructed;
|
||||
|
||||
/// <summary>Syncs the internal visibility flag with the scene-file state and hides properly when the widget starts hidden.</summary>
|
||||
public override void _Ready()
|
||||
{
|
||||
GameLogger.Log("WIDGET", $"_Ready: name={Name}, isVisible={isVisible}, GodotVisible={base.Visible}", LogLevel.Debug);
|
||||
isVisible = base.Visible;
|
||||
if (!isVisible)
|
||||
Hide();
|
||||
OnReady();
|
||||
}
|
||||
|
||||
/// <summary>Tears down a constructed widget when it leaves the tree, so hidden-while-freed widgets still release their resources.</summary>
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (isConstructed)
|
||||
OnceDestruct();
|
||||
OnExitTree();
|
||||
}
|
||||
|
||||
/// <summary>Engine callback; forwards to <see cref="OnInput"/> so mod subclasses receive input without their own engine bindings.</summary>
|
||||
public override void _Input(InputEvent @event) => OnInput(@event);
|
||||
|
||||
/// <summary>Forwarded from <see cref="_Ready"/>. Override for mod widget initialization.</summary>
|
||||
protected virtual void OnReady() { }
|
||||
|
||||
/// <summary>Forwarded from <see cref="_ExitTree"/>. Override for mod widget teardown.</summary>
|
||||
protected virtual void OnExitTree() { }
|
||||
|
||||
/// <summary>Forwarded from <see cref="_Input"/>. Override to handle input in mod widgets.</summary>
|
||||
protected virtual void OnInput(InputEvent @event) { }
|
||||
|
||||
/// <summary>Toggles visibility: calls <see cref="Hide"/> if visible, <see cref="Show"/> otherwise.</summary>
|
||||
public void Switch()
|
||||
{
|
||||
if (Visible)
|
||||
Hide();
|
||||
else
|
||||
Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes visible and runs lifecycle. On first call, <see cref="EnsureConstructed"/> fires <see cref="OnceConstruct"/>.
|
||||
/// Subsequent calls trigger <see cref="ShowConstruct"/> without reconstruction. Sets both the internal isVisible flag
|
||||
/// and Godot's native visibility, then explicitly shows the Body canvas item.
|
||||
/// </summary>
|
||||
public new virtual void Show()
|
||||
{
|
||||
GameLogger.Log("WIDGET", $"Show ENTER: name={Name}, isVisible={isVisible}, GodotVisible={base.Visible}", LogLevel.Debug);
|
||||
var wasConstructed = isConstructed;
|
||||
EnsureConstructed();
|
||||
|
||||
if (Visible)
|
||||
{
|
||||
if (!wasConstructed)
|
||||
ShowConstruct();
|
||||
GameLogger.Log("WIDGET", $"Show SKIP: already visible", LogLevel.Debug);
|
||||
return;
|
||||
}
|
||||
|
||||
ShowConstruct();
|
||||
|
||||
isVisible = true;
|
||||
base.Visible = true;
|
||||
|
||||
var canvasBody = Body as CanvasItem;
|
||||
if (canvasBody != null)
|
||||
canvasBody.Visible = true;
|
||||
|
||||
(Body as Control)?.QueueRedraw();
|
||||
|
||||
GameLogger.Log("WIDGET", $"Show EXIT: name={Name}, isVisible={isVisible}, GodotVisible={base.Visible}", LogLevel.Debug);
|
||||
}
|
||||
|
||||
/// <summary>Hides the widget, fires <see cref="HideDestruct"/> if constructed, sets internal and Godot visibility to false, and hides the Body canvas item.</summary>
|
||||
public new virtual void Hide()
|
||||
{
|
||||
if (!Visible)
|
||||
return;
|
||||
|
||||
if (isConstructed)
|
||||
HideDestruct();
|
||||
|
||||
isVisible = false;
|
||||
base.Visible = false;
|
||||
|
||||
var canvasBody = Body as CanvasItem;
|
||||
if (canvasBody != null)
|
||||
canvasBody.Visible = false;
|
||||
}
|
||||
|
||||
/// <summary>Idempotent construction gate: calls <see cref="OnceConstruct"/> exactly once, on the first invocation.</summary>
|
||||
public void EnsureConstructed()
|
||||
{
|
||||
if (!isConstructed)
|
||||
{
|
||||
isConstructed = true;
|
||||
OnceConstruct();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called exactly once on the first <see cref="Show"/>. Override to perform one-time initialization.</summary>
|
||||
protected virtual void OnceConstruct()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Called once on tree exit if constructed. Override for one-time cleanup of resources allocated in <see cref="OnceConstruct"/>.</summary>
|
||||
protected virtual void OnceDestruct()
|
||||
{
|
||||
}
|
||||
|
||||
[Export] private bool isVisible = true;
|
||||
|
||||
[Export] private Node body;
|
||||
|
||||
/// <summary>
|
||||
/// Shadows Godot's built-in visibility. Setting to true calls <see cref="Show"/>, setting to false calls <see cref="Hide"/>.
|
||||
/// Maintains an independent <c>isVisible</c> flag because Godot's native visibility propagates through the parent chain,
|
||||
/// which would break the explicit lifecycle state machine.
|
||||
/// </summary>
|
||||
public new bool Visible
|
||||
{
|
||||
get => isVisible;
|
||||
set
|
||||
{
|
||||
if (isVisible == value)
|
||||
return;
|
||||
|
||||
if (value)
|
||||
Show();
|
||||
else
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The rendered content subtree. May be a child node separate from the Rect layout root.</summary>
|
||||
public Node Body => body;
|
||||
|
||||
/// <summary>The Control used for layout. For Widget this is the node itself, enabling it to serve as both layout root and widget.</summary>
|
||||
public Control Rect => this;
|
||||
|
||||
/// <summary>Called each time <see cref="Show"/> runs after initial construction. Override to refresh state (e.g. reload data, re-enable input).</summary>
|
||||
protected virtual void ShowConstruct()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Called each time <see cref="Hide"/> runs when constructed. Override to clean up display-only state without destroying permanent resources.</summary>
|
||||
protected virtual void HideDestruct()
|
||||
{
|
||||
}
|
||||
|
||||
#if TOOLS
|
||||
/// <summary>Keeps the inspector in sync with the overridden visibility: touching <c>visible</c> or <c>isVisible</c> in the editor routes through the lifecycle.</summary>
|
||||
public override void _ValidateProperty(Godot.Collections.Dictionary property)
|
||||
{
|
||||
base._ValidateProperty(property);
|
||||
if (property.TryGetValue("visible", out var _))
|
||||
{
|
||||
isVisible = base.Visible;
|
||||
if (isVisible)
|
||||
Show();
|
||||
else
|
||||
Hide();
|
||||
}
|
||||
else if (property.TryGetValue("isVisible", out var _))
|
||||
{
|
||||
if (isVisible)
|
||||
Show();
|
||||
else
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Mods.Interface.Base
|
||||
{
|
||||
/// <summary>
|
||||
/// Lifecycle forwarder for Control scripts compiled from mods — the Control
|
||||
/// counterpart of <see cref="ModNode"/>. Godot's C# source generator only
|
||||
/// runs in the main game assembly, so engine callbacks overridden inside a
|
||||
/// runtime-compiled mod are never registered with the engine. Mod controls
|
||||
/// derive from this base (which has bindings) and override the OnXxx methods.
|
||||
/// </summary>
|
||||
public partial class ModControl : Control
|
||||
{
|
||||
/// <summary>Engine callback; forwards to <see cref="OnEnterTree"/>.</summary>
|
||||
public override void _EnterTree() => OnEnterTree();
|
||||
|
||||
/// <summary>Engine callback; forwards to <see cref="OnExitTree"/>.</summary>
|
||||
public override void _ExitTree() => OnExitTree();
|
||||
|
||||
/// <summary>Engine callback; forwards to <see cref="OnReady"/>.</summary>
|
||||
public override void _Ready() => OnReady();
|
||||
|
||||
/// <summary>Engine callback; forwards to <see cref="OnProcess"/>.</summary>
|
||||
public override void _Process(double delta) => OnProcess(delta);
|
||||
|
||||
/// <summary>Forwarded from <see cref="Node._EnterTree"/>. Override in mod controls.</summary>
|
||||
protected virtual void OnEnterTree() { }
|
||||
|
||||
/// <summary>Forwarded from <see cref="Node._ExitTree"/>. Override in mod controls.</summary>
|
||||
protected virtual void OnExitTree() { }
|
||||
|
||||
/// <summary>Forwarded from <see cref="Node._Ready"/>. Override in mod controls.</summary>
|
||||
protected virtual void OnReady() { }
|
||||
|
||||
/// <summary>Forwarded from <see cref="Node._Process"/>. Override in mod controls.</summary>
|
||||
protected virtual void OnProcess(double delta) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Mods.Interface.Base
|
||||
{
|
||||
/// <summary>
|
||||
/// Lifecycle forwarder for Node scripts compiled from mods. Godot's C#
|
||||
/// source generator only runs in the main game assembly, so virtual methods
|
||||
/// overridden inside a runtime-compiled mod (e.g. <c>_Ready</c>, <c>_Process</c>)
|
||||
/// are never registered with the engine and therefore never called. A mod node
|
||||
/// must instead derive from a core base class — the engine drives the base
|
||||
/// (which has bindings) and the base forwards every lifecycle callback to
|
||||
/// virtual <c>OnXxx</c> methods the mod can override.
|
||||
/// <para/>
|
||||
/// The same trick is used by <see cref="InputHandlerNode"/> for input events
|
||||
/// and by <see cref="ModWidget"/> for Controls. Use this wrapper for plain
|
||||
/// <see cref="Node"/> scripts that need enter/exit/ready/process hooks.
|
||||
/// </summary>
|
||||
public partial class ModNode : Node
|
||||
{
|
||||
/// <summary>Engine callback; forwards to <see cref="OnEnterTree"/>.</summary>
|
||||
public override void _EnterTree() => OnEnterTree();
|
||||
|
||||
/// <summary>Engine callback; forwards to <see cref="OnExitTree"/>.</summary>
|
||||
public override void _ExitTree() => OnExitTree();
|
||||
|
||||
/// <summary>Engine callback; forwards to <see cref="OnReady"/>.</summary>
|
||||
public override void _Ready() => OnReady();
|
||||
|
||||
/// <summary>Engine callback; forwards to <see cref="OnProcess"/>.</summary>
|
||||
public override void _Process(double delta) => OnProcess(delta);
|
||||
|
||||
/// <summary>Forwarded from <see cref="Node._EnterTree"/>. Override in mod nodes.</summary>
|
||||
protected virtual void OnEnterTree() { }
|
||||
|
||||
/// <summary>Forwarded from <see cref="Node._ExitTree"/>. Override in mod nodes.</summary>
|
||||
protected virtual void OnExitTree() { }
|
||||
|
||||
/// <summary>Forwarded from <see cref="Node._Ready"/>. Override in mod nodes.</summary>
|
||||
protected virtual void OnReady() { }
|
||||
|
||||
/// <summary>Forwarded from <see cref="Node._Process"/>. Override in mod nodes.</summary>
|
||||
protected virtual void OnProcess(double delta) { }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user