Line endings: LF everywhere

.gitattributes forces LF (CRLF only for .bat/.cmd), core.autocrlf is off;
files that differed only by CRLF are normalized.
This commit is contained in:
cthangover committed 2026-10-21 13:29:59 +02:00
1 parent 3fb418fa4a
commit c4c90116ed
23 files changed
+1681 -1651

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
# Line endings: LF everywhere, on every OS, whatever core.autocrlf says.
* text=auto eol=lf
# cmd.exe misreads labels and goto in LF batch files, so they stay CRLF.
*.bat text eol=crlf
*.cmd text eol=crlf
# Binaries: never converted, never diffed as text.
*.png binary
*.jpg binary
*.jpeg binary
*.webp binary
*.svg text
*.ogg binary
*.wav binary
*.mp3 binary
*.ttf binary
*.otf binary
*.woff binary
*.woff2 binary
*.dll binary
*.so binary
*.dylib binary
*.pdb binary
*.exe binary
*.zip binary
*.pck binary
*.moc3 binary
*.res binary
*.scn binary
+37 -37
View File
@@ -1,37 +1,37 @@
# Godot 4.x # Godot 4.x
.godot/ .godot/
.idea/ .idea/
sdk/ sdk/
bin/ bin/
obj/ obj/
#ai #ai
.omo/ .omo/
.opencode/ .opencode/
gen/ gen/
obj/ obj/
bin/ bin/
*.tres~ *.tres~
*.tscn~ *.tscn~
# GDScript LSP # GDScript LSP
.godot/ .godot/
# Export # Export
*.exe *.exe
*.dmg *.dmg
*.apk *.apk
*.aab *.aab
*.pck *.pck
*.zip *.zip
*.log *.log
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Editor # Editor
*.import *.import
*.godot.uid *.godot.uid
+21 -21
View File
@@ -1,21 +1,21 @@
using Godot; using Godot;
namespace Cthangover.Mods.Interface.Base namespace Cthangover.Mods.Interface.Base
{ {
/// <summary> /// <summary>
/// Forwarder pattern for Godot input: proxies _Input to a virtual OnInput /// Forwarder pattern for Godot input: proxies _Input to a virtual OnInput
/// so derived classes can handle input events without needing to know about /// so derived classes can handle input events without needing to know about
/// Godot's _Input override mechanics. /// Godot's _Input override mechanics.
/// </summary> /// </summary>
public partial class InputHandlerNode : Node public partial class InputHandlerNode : Node
{ {
/// <summary>Engine entry point: forwards the input event to <see cref="OnInput"/>.</summary> /// <summary>Engine entry point: forwards the input event to <see cref="OnInput"/>.</summary>
public override void _Input(InputEvent @event) public override void _Input(InputEvent @event)
{ {
OnInput(@event); OnInput(@event);
} }
/// <summary>Override in derived classes to receive Godot input events. Called by <see cref="_Input"/> which proxies from the engine.</summary> /// <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) { } protected virtual void OnInput(InputEvent @event) { }
} }
} }
+19 -19
View File
@@ -1,19 +1,19 @@
namespace Cthangover.Mods.Interface.Base.Lists namespace Cthangover.Mods.Interface.Base.Lists
{ {
/// <summary> /// <summary>
/// Serializable margin/padding struct for list layout calculations. /// Serializable margin/padding struct for list layout calculations.
/// Marked [System.Serializable] for potential JSON serialization in mod configs. /// Marked [System.Serializable] for potential JSON serialization in mod configs.
/// </summary> /// </summary>
[System.Serializable] [System.Serializable]
public struct Border public struct Border
{ {
/// <summary>Left margin or padding, in pixels.</summary> /// <summary>Left margin or padding, in pixels.</summary>
public float left; public float left;
/// <summary>Right margin or padding, in pixels.</summary> /// <summary>Right margin or padding, in pixels.</summary>
public float right; public float right;
/// <summary>Top margin or padding, in pixels.</summary> /// <summary>Top margin or padding, in pixels.</summary>
public float top; public float top;
/// <summary>Bottom margin or padding, in pixels.</summary> /// <summary>Bottom margin or padding, in pixels.</summary>
public float bottom; public float bottom;
} }
} }
+22 -22
View File
@@ -1,22 +1,22 @@
using Godot; using Godot;
namespace Cthangover.Mods.Interface.Base.Lists namespace Cthangover.Mods.Interface.Base.Lists
{ {
/// <summary> /// <summary>
/// Extends IListWidget with grid/cell dimensions. CellSize drives item sizing; /// Extends IListWidget with grid/cell dimensions. CellSize drives item sizing;
/// ViewCellsCount determines how many cells fit in the visible area (used for /// ViewCellsCount determines how many cells fit in the visible area (used for
/// scroll/viewport calculations). /// scroll/viewport calculations).
/// </summary> /// </summary>
public interface ICellListWidget<TItem, TModel> : IListWidget<TItem, TModel> public interface ICellListWidget<TItem, TModel> : IListWidget<TItem, TModel>
where TItem : class, IListItem<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> /// <summary>Pixel dimensions of each cell in the grid. Drives item sizing; derived from content width and aspect ratio.</summary>
Vector2 CellSize { get; } Vector2 CellSize { get; }
/// <summary>Number of cells visible in the viewport (columns × rows). Used for scroll range and visibility calculations.</summary> /// <summary>Number of cells visible in the viewport (columns × rows). Used for scroll range and visibility calculations.</summary>
Vector2I ViewCellsCount { get; } Vector2I ViewCellsCount { get; }
} }
} }
+27 -27
View File
@@ -1,27 +1,27 @@
using Godot; using Godot;
namespace Cthangover.Mods.Interface.Base.Lists namespace Cthangover.Mods.Interface.Base.Lists
{ {
/// <summary> /// <summary>
/// Contract for a single item in a list widget. Construct binds a model; /// Contract for a single item in a list widget. Construct binds a model;
/// Destruct tears down (typically QueueFree). The Rect getter exposes the /// Destruct tears down (typically QueueFree). The Rect getter exposes the
/// item's Control for layout positioning by the parent list. /// item's Control for layout positioning by the parent list.
/// </summary> /// </summary>
public interface IListItem<TModel> public interface IListItem<TModel>
{ {
/// <summary>The Control that this item occupies. Used by the parent list to position the item in layout.</summary> /// <summary>The Control that this item occupies. Used by the parent list to position the item in layout.</summary>
Control Rect { get; } Control Rect { get; }
/// <summary>The bound data model. Set via <see cref="Construct"/>.</summary> /// <summary>The bound data model. Set via <see cref="Construct"/>.</summary>
TModel Model { get; } TModel Model { get; }
/// <summary>Binds the item to a data model. Called by the list widget after instantiation, before layout positioning.</summary> /// <summary>Binds the item to a data model. Called by the list widget after instantiation, before layout positioning.</summary>
void Construct(TModel model); void Construct(TModel model);
/// <summary>Cleans up the item — typically disconnects signals and calls QueueFree. Called when the list is destroyed or refreshed.</summary> /// <summary>Cleans up the item — typically disconnects signals and calls QueueFree. Called when the list is destroyed or refreshed.</summary>
void Destruct(); void Destruct();
} }
} }
+43 -43
View File
@@ -1,45 +1,45 @@
using System.Collections.Generic; using System.Collections.Generic;
using Godot; using Godot;
using Cthangover.Core.UI; using Cthangover.Core.UI;
namespace Cthangover.Mods.Interface.Base.Lists namespace Cthangover.Mods.Interface.Base.Lists
{ {
/// <summary> /// <summary>
/// Generic list widget contract: binds a collection of models to Item widgets. /// Generic list widget contract: binds a collection of models to Item widgets.
/// The two-step CreateModels() → CreateContent() pattern separates data sourcing /// The two-step CreateModels() → CreateContent() pattern separates data sourcing
/// from instantiation, so subclasses can pull models from anywhere (runtime state, /// from instantiation, so subclasses can pull models from anywhere (runtime state,
/// config, save data) while the framework handles layout. ConstructUI/DestructUI /// config, save data) while the framework handles layout. ConstructUI/DestructUI
/// manage the full lifecycle of child items. /// manage the full lifecycle of child items.
/// </summary> /// </summary>
public interface IListWidget<TItem, TModel> : IWidget public interface IListWidget<TItem, TModel> : IWidget
where TItem : class, IListItem<TModel> 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> /// <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; } Vector2 ItemsCellPadding { get; }
/// <summary>The container Control that holds all child item nodes. Items are added as children of this node.</summary> /// <summary>The container Control that holds all child item nodes. Items are added as children of this node.</summary>
Control Content { get; } Control Content { get; }
/// <summary>The collection of instantiated item widgets. Populated by <see cref="ConstructUI"/> and cleared by <see cref="DestructUI"/>.</summary> /// <summary>The collection of instantiated item widgets. Populated by <see cref="ConstructUI"/> and cleared by <see cref="DestructUI"/>.</summary>
List<TItem> Items { get; set; } List<TItem> Items { get; set; }
/// <summary>Implementations return the collection of data models to display. Called once per <see cref="ConstructUI"/> cycle.</summary> /// <summary>Implementations return the collection of data models to display. Called once per <see cref="ConstructUI"/> cycle.</summary>
ICollection<TModel> CreateModels(); ICollection<TModel> CreateModels();
/// <summary>Instantiates item widgets from the model collection. Separates model sourcing from instantiation so subclasses control both steps.</summary> /// <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); 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> /// <summary>Calculates the total content size in pixels for <paramref name="count"/> items, used for scroll container sizing.</summary>
Vector2 GetContentSize(int count); Vector2 GetContentSize(int count);
/// <summary>Builds the full item tree: creates models, instantiates items, and positions them via layout.</summary> /// <summary>Builds the full item tree: creates models, instantiates items, and positions them via layout.</summary>
void ConstructUI(); void ConstructUI();
/// <summary>Tears down all child items by calling each item's <see cref="IListItem{TModel}.Destruct"/> and clearing the Items list.</summary> /// <summary>Tears down all child items by calling each item's <see cref="IListItem{TModel}.Destruct"/> and clearing the Items list.</summary>
void DestructUI(); void DestructUI();
} }
} }
+93 -93
View File
@@ -1,93 +1,93 @@
using Godot; using Godot;
namespace Cthangover.Mods.Interface.Base.Lists.Impls namespace Cthangover.Mods.Interface.Base.Lists.Impls
{ {
/// <summary> /// <summary>
/// Grid-based list layout with configurable column count, aspect-ratio cells, /// Grid-based list layout with configurable column count, aspect-ratio cells,
/// and border/padding offsets. Cell dimensions are computed dynamically from /// and border/padding offsets. Cell dimensions are computed dynamically from
/// the content area width, so they scale with the parent. GetContentSize /// the content area width, so they scale with the parent. GetContentSize
/// calculates the total scrollable height from item count and cell geometry, /// calculates the total scrollable height from item count and cell geometry,
/// enabling vertical scrolling through a grid. PutToLayout positions items /// enabling vertical scrolling through a grid. PutToLayout positions items
/// by flat-index-to-grid conversion. The isScaledCanvas flag supports /// by flat-index-to-grid conversion. The isScaledCanvas flag supports
/// resolution-independent layouts. /// resolution-independent layouts.
/// </summary> /// </summary>
public abstract partial class ColumnCellListWidget<TItem, TModel> : ListWidget<TItem, TModel>, ICellListWidget<TItem, TModel> public abstract partial class ColumnCellListWidget<TItem, TModel> : ListWidget<TItem, TModel>, ICellListWidget<TItem, TModel>
where TItem : Control, IListItem<TModel> where TItem : Control, IListItem<TModel>
{ {
[Export] private int columnCount; [Export] private int columnCount;
[Export] private float aspectHeight = 1f; [Export] private float aspectHeight = 1f;
[Export] private float borderLeft; [Export] private float borderLeft;
[Export] private float borderRight; [Export] private float borderRight;
[Export] private float borderTop; [Export] private float borderTop;
[Export] private float borderBottom; [Export] private float borderBottom;
[Export] private bool isScaledCanvas = true; [Export] private bool isScaledCanvas = true;
private Border contentBorder => new() { left = borderLeft, right = borderRight, top = borderTop, bottom = borderBottom }; 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> /// <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); public Vector2 CellSize => GetCellSize(Content.Size);
private Vector2 GetCellSize(Vector2 contentSize) private Vector2 GetCellSize(Vector2 contentSize)
{ {
var size = contentSize; var size = contentSize;
var effectiveWidth = size.X - contentBorder.left - contentBorder.right; var effectiveWidth = size.X - contentBorder.left - contentBorder.right;
var cellWidth = (effectiveWidth - (columnCount - 1) * ItemsCellPadding.X) / columnCount; var cellWidth = (effectiveWidth - (columnCount - 1) * ItemsCellPadding.X) / columnCount;
var cellHeight = cellWidth * aspectHeight; var cellHeight = cellWidth * aspectHeight;
return new Vector2(cellWidth, cellHeight); return new Vector2(cellWidth, cellHeight);
} }
/// <summary>Number of cells visible in the viewport: X = columnCount, Y = floor((content height - borders) / (cell height + padding)).</summary> /// <summary>Number of cells visible in the viewport: X = columnCount, Y = floor((content height - borders) / (cell height + padding)).</summary>
public Vector2I ViewCellsCount public Vector2I ViewCellsCount
{ {
get get
{ {
var size = Content.Size; var size = Content.Size;
return new Vector2I( return new Vector2I(
columnCount, columnCount,
Mathf.FloorToInt((size.Y - contentBorder.top - contentBorder.bottom) / (CellSize.Y + ItemsCellPadding.Y)) 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> /// <summary>Calculates total scrollable content height: top border + bottom border + rows * cellHeight + (rows - 1) * padding.</summary>
public override Vector2 GetContentSize(int count) public override Vector2 GetContentSize(int count)
{ {
if (count == 0) return Vector2.Zero; if (count == 0) return Vector2.Zero;
var rect = Content.Size; var rect = Content.Size;
var effectiveWidth = rect.X - contentBorder.left - contentBorder.right; var effectiveWidth = rect.X - contentBorder.left - contentBorder.right;
var cellWidth = (effectiveWidth - (columnCount - 1) * ItemsCellPadding.X) / columnCount; var cellWidth = (effectiveWidth - (columnCount - 1) * ItemsCellPadding.X) / columnCount;
var cellHeight = cellWidth * aspectHeight; var cellHeight = cellWidth * aspectHeight;
var rows = Mathf.CeilToInt((float)count / columnCount); var rows = Mathf.CeilToInt((float)count / columnCount);
var contentHeight = contentBorder.top + contentBorder.bottom + rows * cellHeight + (rows - 1) * ItemsCellPadding.Y; var contentHeight = contentBorder.top + contentBorder.bottom + rows * cellHeight + (rows - 1) * ItemsCellPadding.Y;
var contentWidth = rect.X; var contentWidth = rect.X;
return new Vector2(contentWidth, contentHeight); return new Vector2(contentWidth, contentHeight);
} }
protected override void PutToLayout(TItem item, int index, Control container, Vector2 contentSize) protected override void PutToLayout(TItem item, int index, Control container, Vector2 contentSize)
{ {
var rectTransform = item; var rectTransform = item;
var cellIndex = GetIndex(index); var cellIndex = GetIndex(index);
var cellSize = GetCellSize(contentSize); var cellSize = GetCellSize(contentSize);
if (rectTransform.GetParent() != container) if (rectTransform.GetParent() != container)
rectTransform.Reparent(container, false); rectTransform.Reparent(container, false);
rectTransform.Position = new Vector2( rectTransform.Position = new Vector2(
contentBorder.left + cellIndex.X * (cellSize.X + ItemsCellPadding.X), contentBorder.left + cellIndex.X * (cellSize.X + ItemsCellPadding.X),
contentBorder.top + cellIndex.Y * (cellSize.Y + ItemsCellPadding.Y) contentBorder.top + cellIndex.Y * (cellSize.Y + ItemsCellPadding.Y)
); );
rectTransform.Size = cellSize; rectTransform.Size = cellSize;
} }
private Vector2I GetIndex(int flatIndex) private Vector2I GetIndex(int flatIndex)
{ {
var count = ViewCellsCount; var count = ViewCellsCount;
return new Vector2I( return new Vector2I(
flatIndex % count.X, flatIndex % count.X,
flatIndex / count.X flatIndex / count.X
); );
} }
} }
} }
+33 -33
View File
@@ -1,33 +1,33 @@
using Godot; using Godot;
namespace Cthangover.Mods.Interface.Base.Lists.Impls namespace Cthangover.Mods.Interface.Base.Lists.Impls
{ {
/// <summary> /// <summary>
/// Simple vertical stack layout. Items are placed top-to-bottom with padding /// Simple vertical stack layout. Items are placed top-to-bottom with padding
/// between them, offset so the list grows upward from the content center. /// 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 /// Content height is the sum of all item heights plus padding — no pooling
/// or virtualization, so it's intended for short lists. /// or virtualization, so it's intended for short lists.
/// </summary> /// </summary>
public abstract partial class VerticalListWidget<TItem, TModel> : ListWidget<TItem, TModel> public abstract partial class VerticalListWidget<TItem, TModel> : ListWidget<TItem, TModel>
where TItem : Control, IListItem<TModel> where TItem : Control, IListItem<TModel>
{ {
protected override void PutToLayout(TItem item, int index, Control container, Vector2 contentSize) protected override void PutToLayout(TItem item, int index, Control container, Vector2 contentSize)
{ {
var posY = index * (PrefabRect.Size.Y + ItemsCellPadding.Y); var posY = index * (PrefabRect.Size.Y + ItemsCellPadding.Y);
var itemRect = item; var itemRect = item;
var pos = new Vector2(itemRect.Position.X, -posY + contentSize.Y * 0.5f); var pos = new Vector2(itemRect.Position.X, -posY + contentSize.Y * 0.5f);
itemRect.Position = pos; itemRect.Position = pos;
} }
/// <summary>Content height = count * (prefab height + Y padding). Width is inherited from the Content container.</summary> /// <summary>Content height = count * (prefab height + Y padding). Width is inherited from the Content container.</summary>
public override Vector2 GetContentSize(int count) public override Vector2 GetContentSize(int count)
{ {
var boxSize = Content.Size; var boxSize = Content.Size;
boxSize.Y = count * (PrefabRect.Size.Y + ItemsCellPadding.Y); boxSize.Y = count * (PrefabRect.Size.Y + ItemsCellPadding.Y);
return boxSize; return boxSize;
} }
} }
} }
+29 -29
View File
@@ -1,35 +1,35 @@
using Godot; using Godot;
namespace Cthangover.Mods.Interface.Base.Lists namespace Cthangover.Mods.Interface.Base.Lists
{ {
/// <summary> /// <summary>
/// Base list item: stores the bound model and provides a Rect for layout. /// Base list item: stores the bound model and provides a Rect for layout.
/// The virtual Construct saves the model reference; Destruct is abstract so /// The virtual Construct saves the model reference; Destruct is abstract so
/// subclasses must explicitly implement cleanup (signal disconnection, QueueFree). /// subclasses must explicitly implement cleanup (signal disconnection, QueueFree).
/// </summary> /// </summary>
public abstract partial class ListItem<TModel> : Control, IListItem<TModel> public abstract partial class ListItem<TModel> : Control, IListItem<TModel>
{ {
/// <summary>The data model bound to this list item. Set by <see cref="Construct"/>.</summary> /// <summary>The data model bound to this list item. Set by <see cref="Construct"/>.</summary>
public TModel Model { get; private set; } public TModel Model { get; private set; }
/// <summary>The Control that this item occupies for layout. Returns itself — ListItem is its own layout root.</summary> /// <summary>The Control that this item occupies for layout. Returns itself — ListItem is its own layout root.</summary>
public Control Rect => this; public Control Rect => this;
/// <summary>Saves the model reference. Override in subclasses to apply model data to UI elements.</summary> /// <summary>Saves the model reference. Override in subclasses to apply model data to UI elements.</summary>
public virtual void Construct(TModel model) public virtual void Construct(TModel model)
{ {
Model = model; Model = model;
} }
/// <summary>Required cleanup method. Subclasses must implement — typically disconnects signals and calls QueueFree.</summary> /// <summary>Required cleanup method. Subclasses must implement — typically disconnects signals and calls QueueFree.</summary>
public abstract void Destruct(); 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> /// <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(); public override void _Ready() => OnReady();
/// <summary>Forwarded from <see cref="_Ready"/>. Override for mod item initialization.</summary> /// <summary>Forwarded from <see cref="_Ready"/>. Override for mod item initialization.</summary>
protected virtual void OnReady() { } protected virtual void OnReady() { }
} }
} }
+134 -134
View File
@@ -1,134 +1,134 @@
using System.Collections.Generic; using System.Collections.Generic;
using Godot; using Godot;
namespace Cthangover.Mods.Interface.Base.Lists namespace Cthangover.Mods.Interface.Base.Lists
{ {
/// <summary> /// <summary>
/// Abstract model-driven list: instantiates items from a PackedScene prefab /// Abstract model-driven list: instantiates items from a PackedScene prefab
/// for each model in CreateModels(), then delegates positioning to the abstract /// for each model in CreateModels(), then delegates positioning to the abstract
/// PutToLayout (defined by layout subclasses like ColumnCellListWidget or /// PutToLayout (defined by layout subclasses like ColumnCellListWidget or
/// VerticalListWidget). Refresh() tears down and rebuilds the list in place /// VerticalListWidget). Refresh() tears down and rebuilds the list in place
/// for reactive updates. The virtual Create/Init hooks let subclasses inject /// for reactive updates. The virtual Create/Init hooks let subclasses inject
/// extra setup between instantiation and model binding without overriding /// extra setup between instantiation and model binding without overriding
/// CreateContent. /// CreateContent.
/// </summary> /// </summary>
public abstract partial class ListWidget<T, M> : Widget, IListWidget<T, M> public abstract partial class ListWidget<T, M> : Widget, IListWidget<T, M>
where T : Control, IListItem<M> where T : Control, IListItem<M>
{ {
[Export] [Export]
private Control content; private Control content;
[Export] [Export]
private PackedScene itemScene; private PackedScene itemScene;
[Export] private Vector2 itemsCellPadding = new(0f, 1f); [Export] private Vector2 itemsCellPadding = new(0f, 1f);
/// <summary>Editor-configured padding between items; exposed for layout size math.</summary> /// <summary>Editor-configured padding between items; exposed for layout size math.</summary>
public Vector2 ItemsCellPadding => itemsCellPadding; public Vector2 ItemsCellPadding => itemsCellPadding;
/// <summary>Prefab instance for layout measurement. Instantiated on each access — not cached. Returns null if itemScene is not set.</summary> /// <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>(); protected T Prefab => itemScene?.Instantiate<T>();
/// <summary>The Control rect of the item prefab, used for layout size calculations.</summary> /// <summary>The Control rect of the item prefab, used for layout size calculations.</summary>
protected Control PrefabRect => Prefab; protected Control PrefabRect => Prefab;
/// <summary>The container Control that holds child items. Set via [Export] in the Godot editor.</summary> /// <summary>The container Control that holds child items. Set via [Export] in the Godot editor.</summary>
public Control Content => content; public Control Content => content;
/// <summary>Calculates the total content size for a given item count. Subclass-specific — different layouts have different formulas.</summary> /// <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); public abstract Vector2 GetContentSize(int count);
/// <summary>The instantiated item collection. Set by <see cref="ConstructUI"/> and cleared by <see cref="DestructUI"/>.</summary> /// <summary>The instantiated item collection. Set by <see cref="ConstructUI"/> and cleared by <see cref="DestructUI"/>.</summary>
public List<T> Items { get; set; } public List<T> Items { get; set; }
/// <summary>Shows the widget and rebuilds the UI tree via <see cref="ConstructUI"/>.</summary> /// <summary>Shows the widget and rebuilds the UI tree via <see cref="ConstructUI"/>.</summary>
public override void Show() public override void Show()
{ {
base.Show(); base.Show();
ConstructUI(); ConstructUI();
} }
/// <summary>Tears down child items via <see cref="DestructUI"/> then hides via base <see cref="Widget.Hide"/>.</summary> /// <summary>Tears down child items via <see cref="DestructUI"/> then hides via base <see cref="Widget.Hide"/>.</summary>
public override void Hide() public override void Hide()
{ {
DestructUI(); DestructUI();
base.Hide(); base.Hide();
} }
/// <summary>Destructs and reconstructs the item tree in place. Use for reactive updates when the model collection changes.</summary> /// <summary>Destructs and reconstructs the item tree in place. Use for reactive updates when the model collection changes.</summary>
public void Refresh() public void Refresh()
{ {
if (!Visible) if (!Visible)
return; return;
DestructUI(); DestructUI();
ConstructUI(); ConstructUI();
} }
/// <summary>Iterates <paramref name="models"/> and calls <see cref="Create"/> for each, building the item widget collection.</summary> /// <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) public List<T> CreateContent(ICollection<M> models)
{ {
var items = new List<T>(); var items = new List<T>();
foreach (var model in models) foreach (var model in models)
{ {
var item = Create(model); var item = Create(model);
item.Construct(model); item.Construct(model);
items.Add(item); items.Add(item);
} }
return items; return items;
} }
/// <summary> /// <summary>
/// Full build pipeline: gets models via <see cref="CreateModels"/>, instantiates items via <see cref="CreateContent"/>, /// 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"/>. /// sets the content container size via <see cref="CallDeferred"/> to size, then positions each item via <see cref="PutToLayout"/>.
/// </summary> /// </summary>
public void ConstructUI() public void ConstructUI()
{ {
var rect = Content; var rect = Content;
var models = CreateModels(); var models = CreateModels();
var items = CreateContent(models); var items = CreateContent(models);
var size = GetContentSize(items.Count); var size = GetContentSize(items.Count);
rect.CallDeferred("set_size", size); rect.CallDeferred("set_size", size);
for (int i = 0; i < items.Count; i++) for (int i = 0; i < items.Count; i++)
PutToLayout(items[i], i, rect, size); PutToLayout(items[i], i, rect, size);
Items = items; Items = items;
} }
/// <summary>Subclasses implement positioning logic. Called for each item with its flat index, the content container, and total content size.</summary> /// <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); 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> /// <summary>Subclasses implement model sourcing. Called by <see cref="ConstructUI"/> before item instantiation.</summary>
public abstract ICollection<M> CreateModels(); public abstract ICollection<M> CreateModels();
/// <summary>Hook for subclasses to inject setup between instantiation and model binding. Called by <see cref="Create"/>.</summary> /// <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) { } 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> /// <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) protected virtual T Create(M model)
{ {
var instance = itemScene.Instantiate<T>(); var instance = itemScene.Instantiate<T>();
Content.AddChild(instance); Content.AddChild(instance);
Init(instance, model); Init(instance, model);
return instance; return instance;
} }
/// <summary>Calls <see cref="IListItem{TModel}.Destruct"/> on each child item, clears the list, and sets Items to null.</summary> /// <summary>Calls <see cref="IListItem{TModel}.Destruct"/> on each child item, clears the list, and sets Items to null.</summary>
public void DestructUI() public void DestructUI()
{ {
if (Items == null || Items.Count == 0) if (Items == null || Items.Count == 0)
return; return;
foreach (var item in Items) foreach (var item in Items)
item.Destruct(); item.Destruct();
Items.Clear(); Items.Clear();
Items = null; Items = null;
} }
} }
} }
+36 -36
View File
@@ -1,38 +1,38 @@
using Godot; using Godot;
using Cthangover.Core.UI; using Cthangover.Core.UI;
namespace Cthangover.Mods.Interface.Base.Lists namespace Cthangover.Mods.Interface.Base.Lists
{ {
/// <summary> /// <summary>
/// Lightweight alternative to Widget for mod-authored controls. Has a simpler /// Lightweight alternative to Widget for mod-authored controls. Has a simpler
/// lifecycle — just Construct() on first access and Destruct() on tree exit — /// lifecycle — just Construct() on first access and Destruct() on tree exit —
/// without the visibility interception and Show/Hide pipeline that Widget /// without the visibility interception and Show/Hide pipeline that Widget
/// carries. Use when the heavy Widget state machine would be overkill or /// carries. Use when the heavy Widget state machine would be overkill or
/// when the control's visibility is managed by Godot's native system. /// when the control's visibility is managed by Godot's native system.
/// </summary> /// </summary>
public abstract partial class ModWidget : Control, IUiLazyConstruct public abstract partial class ModWidget : Control, IUiLazyConstruct
{ {
private bool _constructed; private bool _constructed;
/// <summary>Idempotent construction gate: calls <see cref="Construct"/> exactly once, on the first invocation. Thread-safe for repeated external calls.</summary> /// <summary>Idempotent construction gate: calls <see cref="Construct"/> exactly once, on the first invocation. Thread-safe for repeated external calls.</summary>
public void EnsureConstructed() public void EnsureConstructed()
{ {
if (_constructed) if (_constructed)
return; return;
_constructed = true; _constructed = true;
Construct(); Construct();
} }
/// <summary>Runs <see cref="Destruct"/> once when the constructed control leaves the tree.</summary> /// <summary>Runs <see cref="Destruct"/> once when the constructed control leaves the tree.</summary>
public override void _ExitTree() public override void _ExitTree()
{ {
if (_constructed) if (_constructed)
Destruct(); Destruct();
} }
protected abstract void Construct(); protected abstract void Construct();
protected virtual void Destruct() { } protected virtual void Destruct() { }
} }
} }
+128 -128
View File
@@ -1,128 +1,128 @@
using Cthangover.Core.Mods; using Cthangover.Core.Mods;
using Cthangover.Core.Mods.Resolvers; using Cthangover.Core.Mods.Resolvers;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Godot; using Godot;
namespace Cthangover.Mods.Interface.Base.Lists namespace Cthangover.Mods.Interface.Base.Lists
{ {
/// <summary> /// <summary>
/// Shader-based scene transition: animates a "progress" uniform on the /// Shader-based scene transition: animates a "progress" uniform on the
/// background's ShaderMaterial via a Godot Tween, then reveals content after /// background's ShaderMaterial via a Godot Tween, then reveals content after
/// a configurable delay. The shader is resolved through the mod system /// a configurable delay. The shader is resolved through the mod system
/// (ModManager.ResolveShader), allowing mods to replace the transition effect. /// (ModManager.ResolveShader), allowing mods to replace the transition effect.
/// Uses an easeInOut curve for the progress ramp. Material is duplicated on /// 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, /// first use so each instance gets its own shader parameters. On tree exit,
/// the duplicated material is disposed to avoid leaks. /// the duplicated material is disposed to avoid leaks.
/// </summary> /// </summary>
public abstract partial class TransitionWidget : Widget public abstract partial class TransitionWidget : Widget
{ {
private static readonly StringName ProgressParam = "progress"; private static readonly StringName ProgressParam = "progress";
[Export] private TextureRect imgBackground; [Export] private TextureRect imgBackground;
[Export] private Control contentGroup; [Export] private Control contentGroup;
[Export] private float transitionDuration = 0.8f; [Export] private float transitionDuration = 0.8f;
[Export] private float contentDelay = 0.05f; [Export] private float contentDelay = 0.05f;
private ShaderMaterial materialInstance; private ShaderMaterial materialInstance;
private Tween transitionTween; private Tween transitionTween;
/// <summary> /// <summary>
/// Shows the widget base, then creates or reuses a shader material for the background and starts the transition tween. /// 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. /// Material is duplicated from <c>imgBackground.Material</c> on first call so each instance gets independent shader parameters.
/// </summary> /// </summary>
public override void Show() public override void Show()
{ {
base.Show(); base.Show();
if (materialInstance == null && imgBackground != null) if (materialInstance == null && imgBackground != null)
{ {
materialInstance = imgBackground.Material?.Duplicate() as ShaderMaterial; materialInstance = imgBackground.Material?.Duplicate() as ShaderMaterial;
if (materialInstance == null) if (materialInstance == null)
{ {
materialInstance = new ShaderMaterial(); materialInstance = new ShaderMaterial();
var shader = Shaders.Resolve("scene_transition"); var shader = Shaders.Resolve("scene_transition");
if (shader != null) if (shader != null)
materialInstance.Shader = shader; materialInstance.Shader = shader;
} }
imgBackground.Material = materialInstance; imgBackground.Material = materialInstance;
materialInstance.SetShaderParameter(ProgressParam, 0f); materialInstance.SetShaderParameter(ProgressParam, 0f);
} }
if (transitionTween != null) if (transitionTween != null)
{ {
transitionTween.Kill(); transitionTween.Kill();
transitionTween = null; transitionTween = null;
} }
RunShowAnimation(); RunShowAnimation();
} }
/// <summary>Kills the active transition tween if one is running, then calls base <see cref="Widget.Hide"/>.</summary> /// <summary>Kills the active transition tween if one is running, then calls base <see cref="Widget.Hide"/>.</summary>
public override void Hide() public override void Hide()
{ {
if (transitionTween != null) if (transitionTween != null)
{ {
transitionTween.Kill(); transitionTween.Kill();
transitionTween = null; transitionTween = null;
} }
base.Hide(); base.Hide();
} }
private void RunShowAnimation() private void RunShowAnimation()
{ {
if (contentGroup != null) if (contentGroup != null)
{ {
contentGroup.Modulate = UiTokens.Alpha(UiTokens.White, 0f); contentGroup.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
contentGroup.MouseFilter = MouseFilterEnum.Ignore; contentGroup.MouseFilter = MouseFilterEnum.Ignore;
} }
transitionTween = CreateTween(); transitionTween = CreateTween();
transitionTween.SetProcessMode(Tween.TweenProcessMode.Physics); transitionTween.SetProcessMode(Tween.TweenProcessMode.Physics);
transitionTween.TweenMethod(Callable.From<float>(OnProgressUpdate), 0f, 1f, transitionDuration); transitionTween.TweenMethod(Callable.From<float>(OnProgressUpdate), 0f, 1f, transitionDuration);
transitionTween.Finished += OnTweenFinished; transitionTween.Finished += OnTweenFinished;
} }
private void OnProgressUpdate(float t) private void OnProgressUpdate(float t)
{ {
float curvedT = EaseInOut(t); float curvedT = EaseInOut(t);
if (imgBackground?.Material is ShaderMaterial mat) if (imgBackground?.Material is ShaderMaterial mat)
mat.SetShaderParameter(ProgressParam, curvedT); mat.SetShaderParameter(ProgressParam, curvedT);
} }
private void OnTweenFinished() private void OnTweenFinished()
{ {
if (imgBackground?.Material is ShaderMaterial mat) if (imgBackground?.Material is ShaderMaterial mat)
mat.SetShaderParameter(ProgressParam, 1f); mat.SetShaderParameter(ProgressParam, 1f);
var timer = GetTree().CreateTimer(contentDelay); var timer = GetTree().CreateTimer(contentDelay);
timer.Timeout += () => timer.Timeout += () =>
{ {
if (contentGroup != null) if (contentGroup != null)
{ {
contentGroup.Modulate = UiTokens.White; contentGroup.Modulate = UiTokens.White;
contentGroup.MouseFilter = MouseFilterEnum.Stop; contentGroup.MouseFilter = MouseFilterEnum.Stop;
} }
}; };
transitionTween = null; transitionTween = null;
} }
private static float EaseInOut(float t) private static float EaseInOut(float t)
{ {
return t < 0.5f ? 2f * t * t : 1f - Mathf.Pow(-2f * t + 2f, 2f) / 2f; 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> /// <summary>Disposes the per-instance shader material created for the transition.</summary>
public override void _ExitTree() public override void _ExitTree()
{ {
if (materialInstance != null) if (materialInstance != null)
{ {
materialInstance.Dispose(); materialInstance.Dispose();
materialInstance = null; materialInstance = null;
} }
base._ExitTree(); base._ExitTree();
} }
} }
} }
+192 -192
View File
@@ -1,194 +1,194 @@
using Cthangover.Core.Utils; using Cthangover.Core.Utils;
using Godot; using Godot;
using Cthangover.Core.UI; using Cthangover.Core.UI;
namespace Cthangover.Mods.Interface.Base.Lists namespace Cthangover.Mods.Interface.Base.Lists
{ {
/// <summary> /// <summary>
/// Base widget with a lazy construct/destruct lifecycle around Show/Hide. /// Base widget with a lazy construct/destruct lifecycle around Show/Hide.
/// On first Show(), OnceConstruct() fires once via EnsureConstructed — subsequent /// On first Show(), OnceConstruct() fires once via EnsureConstructed — subsequent
/// Show/Hide calls trigger ShowConstruct/HideDestruct instead, allowing repeated /// Show/Hide calls trigger ShowConstruct/HideDestruct instead, allowing repeated
/// state reset without full reconstruction. Overrides Godot's built-in Visible /// state reset without full reconstruction. Overrides Godot's built-in Visible
/// property to intercept visibility changes and route them through the lifecycle. /// property to intercept visibility changes and route them through the lifecycle.
/// Maintains an independent isVisible flag because Godot's visibility has quirks /// Maintains an independent isVisible flag because Godot's visibility has quirks
/// (e.g. parent visibility propagation) that would break the state machine. /// (e.g. parent visibility propagation) that would break the state machine.
/// The #if TOOLS _ValidateProperty syncs the editor inspector with the override. /// The #if TOOLS _ValidateProperty syncs the editor inspector with the override.
/// </summary> /// </summary>
public abstract partial class Widget : Control, IWidget public abstract partial class Widget : Control, IWidget
{ {
private bool isConstructed; private bool isConstructed;
/// <summary>Syncs the internal visibility flag with the scene-file state and hides properly when the widget starts hidden.</summary> /// <summary>Syncs the internal visibility flag with the scene-file state and hides properly when the widget starts hidden.</summary>
public override void _Ready() public override void _Ready()
{ {
GameLogger.Log("WIDGET", $"_Ready: name={Name}, isVisible={isVisible}, GodotVisible={base.Visible}", LogLevel.Debug); GameLogger.Log("WIDGET", $"_Ready: name={Name}, isVisible={isVisible}, GodotVisible={base.Visible}", LogLevel.Debug);
isVisible = base.Visible; isVisible = base.Visible;
if (!isVisible) if (!isVisible)
Hide(); Hide();
OnReady(); OnReady();
} }
/// <summary>Tears down a constructed widget when it leaves the tree, so hidden-while-freed widgets still release their resources.</summary> /// <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() public override void _ExitTree()
{ {
if (isConstructed) if (isConstructed)
OnceDestruct(); OnceDestruct();
OnExitTree(); OnExitTree();
} }
/// <summary>Engine callback; forwards to <see cref="OnInput"/> so mod subclasses receive input without their own engine bindings.</summary> /// <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); public override void _Input(InputEvent @event) => OnInput(@event);
/// <summary>Forwarded from <see cref="_Ready"/>. Override for mod widget initialization.</summary> /// <summary>Forwarded from <see cref="_Ready"/>. Override for mod widget initialization.</summary>
protected virtual void OnReady() { } protected virtual void OnReady() { }
/// <summary>Forwarded from <see cref="_ExitTree"/>. Override for mod widget teardown.</summary> /// <summary>Forwarded from <see cref="_ExitTree"/>. Override for mod widget teardown.</summary>
protected virtual void OnExitTree() { } protected virtual void OnExitTree() { }
/// <summary>Forwarded from <see cref="_Input"/>. Override to handle input in mod widgets.</summary> /// <summary>Forwarded from <see cref="_Input"/>. Override to handle input in mod widgets.</summary>
protected virtual void OnInput(InputEvent @event) { } protected virtual void OnInput(InputEvent @event) { }
/// <summary>Toggles visibility: calls <see cref="Hide"/> if visible, <see cref="Show"/> otherwise.</summary> /// <summary>Toggles visibility: calls <see cref="Hide"/> if visible, <see cref="Show"/> otherwise.</summary>
public void Switch() public void Switch()
{ {
if (Visible) if (Visible)
Hide(); Hide();
else else
Show(); Show();
} }
/// <summary> /// <summary>
/// Makes visible and runs lifecycle. On first call, <see cref="EnsureConstructed"/> fires <see cref="OnceConstruct"/>. /// 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 /// 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. /// and Godot's native visibility, then explicitly shows the Body canvas item.
/// </summary> /// </summary>
public new virtual void Show() public new virtual void Show()
{ {
GameLogger.Log("WIDGET", $"Show ENTER: name={Name}, isVisible={isVisible}, GodotVisible={base.Visible}", LogLevel.Debug); GameLogger.Log("WIDGET", $"Show ENTER: name={Name}, isVisible={isVisible}, GodotVisible={base.Visible}", LogLevel.Debug);
var wasConstructed = isConstructed; var wasConstructed = isConstructed;
EnsureConstructed(); EnsureConstructed();
if (Visible) if (Visible)
{ {
if (!wasConstructed) if (!wasConstructed)
ShowConstruct(); ShowConstruct();
GameLogger.Log("WIDGET", $"Show SKIP: already visible", LogLevel.Debug); GameLogger.Log("WIDGET", $"Show SKIP: already visible", LogLevel.Debug);
return; return;
} }
ShowConstruct(); ShowConstruct();
isVisible = true; isVisible = true;
base.Visible = true; base.Visible = true;
var canvasBody = Body as CanvasItem; var canvasBody = Body as CanvasItem;
if (canvasBody != null) if (canvasBody != null)
canvasBody.Visible = true; canvasBody.Visible = true;
(Body as Control)?.QueueRedraw(); (Body as Control)?.QueueRedraw();
GameLogger.Log("WIDGET", $"Show EXIT: name={Name}, isVisible={isVisible}, GodotVisible={base.Visible}", LogLevel.Debug); 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> /// <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() public new virtual void Hide()
{ {
if (!Visible) if (!Visible)
return; return;
if (isConstructed) if (isConstructed)
HideDestruct(); HideDestruct();
isVisible = false; isVisible = false;
base.Visible = false; base.Visible = false;
var canvasBody = Body as CanvasItem; var canvasBody = Body as CanvasItem;
if (canvasBody != null) if (canvasBody != null)
canvasBody.Visible = false; canvasBody.Visible = false;
} }
/// <summary>Idempotent construction gate: calls <see cref="OnceConstruct"/> exactly once, on the first invocation.</summary> /// <summary>Idempotent construction gate: calls <see cref="OnceConstruct"/> exactly once, on the first invocation.</summary>
public void EnsureConstructed() public void EnsureConstructed()
{ {
if (!isConstructed) if (!isConstructed)
{ {
isConstructed = true; isConstructed = true;
OnceConstruct(); OnceConstruct();
} }
} }
/// <summary>Called exactly once on the first <see cref="Show"/>. Override to perform one-time initialization.</summary> /// <summary>Called exactly once on the first <see cref="Show"/>. Override to perform one-time initialization.</summary>
protected virtual void OnceConstruct() 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> /// <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() protected virtual void OnceDestruct()
{ {
} }
[Export] private bool isVisible = true; [Export] private bool isVisible = true;
[Export] private Node body; [Export] private Node body;
/// <summary> /// <summary>
/// Shadows Godot's built-in visibility. Setting to true calls <see cref="Show"/>, setting to false calls <see cref="Hide"/>. /// 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, /// 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. /// which would break the explicit lifecycle state machine.
/// </summary> /// </summary>
public new bool Visible public new bool Visible
{ {
get => isVisible; get => isVisible;
set set
{ {
if (isVisible == value) if (isVisible == value)
return; return;
if (value) if (value)
Show(); Show();
else else
Hide(); Hide();
} }
} }
/// <summary>The rendered content subtree. May be a child node separate from the Rect layout root.</summary> /// <summary>The rendered content subtree. May be a child node separate from the Rect layout root.</summary>
public Node Body => body; 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> /// <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; 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> /// <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() 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> /// <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() protected virtual void HideDestruct()
{ {
} }
#if TOOLS #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> /// <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) public override void _ValidateProperty(Godot.Collections.Dictionary property)
{ {
base._ValidateProperty(property); base._ValidateProperty(property);
if (property.TryGetValue("visible", out var _)) if (property.TryGetValue("visible", out var _))
{ {
isVisible = base.Visible; isVisible = base.Visible;
if (isVisible) if (isVisible)
Show(); Show();
else else
Hide(); Hide();
} }
else if (property.TryGetValue("isVisible", out var _)) else if (property.TryGetValue("isVisible", out var _))
{ {
if (isVisible) if (isVisible)
Show(); Show();
else else
Hide(); Hide();
} }
} }
#endif #endif
} }
} }
+137 -137
View File
@@ -1,137 +1,137 @@
using Godot; using Godot;
namespace Cthangover.Mods.Interface.Event namespace Cthangover.Mods.Interface.Event
{ {
/// <summary> /// <summary>
/// Stateless input abstraction that maps mouse to a virtual single-touch source. /// Stateless input abstraction that maps mouse to a virtual single-touch source.
/// Tracks mouse-down state between frames to derive touch phases (Began/Moved/ /// Tracks mouse-down state between frames to derive touch phases (Began/Moved/
/// Ended/Stationary), enabling touch-aware code to work with mouse-only devices. /// Ended/Stationary), enabling touch-aware code to work with mouse-only devices.
/// Provides both UI-space (UITouchPosition) and screen-space (TouchPosition) /// Provides both UI-space (UITouchPosition) and screen-space (TouchPosition)
/// coordinate access. Update() must be called each frame to refresh the /// coordinate access. Update() must be called each frame to refresh the
/// wasMouseDownLastFrame flag. /// wasMouseDownLastFrame flag.
/// </summary> /// </summary>
public static class DeviceInput public static class DeviceInput
{ {
/// <summary> /// <summary>
/// Index of the active touch (always 0 for the mouse-backed single-touch /// Index of the active touch (always 0 for the mouse-backed single-touch
/// model). Exposed for API compatibility with potential multi-touch support. /// model). Exposed for API compatibility with potential multi-touch support.
/// </summary> /// </summary>
public static int CurrentTouchIndex { get; set; } public static int CurrentTouchIndex { get; set; }
/// <summary> /// <summary>
/// Derives a <see cref="TouchPhase"/> from mouse button state across frames. /// Derives a <see cref="TouchPhase"/> from mouse button state across frames.
/// Compares the current left-button state against <c>wasMouseDownLastFrame</c>: /// Compares the current left-button state against <c>wasMouseDownLastFrame</c>:
/// Began on fresh press, Moved on hold, Ended on release. /// Began on fresh press, Moved on hold, Ended on release.
/// </summary> /// </summary>
public static TouchPhase GetTouchPhase public static TouchPhase GetTouchPhase
{ {
get get
{ {
if (Input.IsMouseButtonPressed(MouseButton.Left)) if (Input.IsMouseButtonPressed(MouseButton.Left))
{ {
if (Input.IsMouseButtonPressed(MouseButton.Left) && !wasMouseDownLastFrame) if (Input.IsMouseButtonPressed(MouseButton.Left) && !wasMouseDownLastFrame)
return TouchPhase.Began; return TouchPhase.Began;
if (Input.IsMouseButtonPressed(MouseButton.Left) && wasMouseDownLastFrame) if (Input.IsMouseButtonPressed(MouseButton.Left) && wasMouseDownLastFrame)
return TouchPhase.Moved; return TouchPhase.Moved;
} }
else else
{ {
if (wasMouseDownLastFrame) if (wasMouseDownLastFrame)
return TouchPhase.Ended; return TouchPhase.Ended;
} }
return TouchPhase.Stationary; return TouchPhase.Stationary;
} }
} }
private static bool wasMouseDownLastFrame; private static bool wasMouseDownLastFrame;
/// <summary> /// <summary>
/// Must be called each frame to snapshot the mouse button state. The saved /// Must be called each frame to snapshot the mouse button state. The saved
/// state drives <see cref="GetTouchPhase"/> for press/release transitions. /// state drives <see cref="GetTouchPhase"/> for press/release transitions.
/// </summary> /// </summary>
public static void Update() public static void Update()
{ {
wasMouseDownLastFrame = Input.IsMouseButtonPressed(MouseButton.Left); wasMouseDownLastFrame = Input.IsMouseButtonPressed(MouseButton.Left);
} }
/// <summary> /// <summary>
/// Returns 1 when the left mouse button is pressed, 0 otherwise. /// Returns 1 when the left mouse button is pressed, 0 otherwise.
/// Maps the mouse to a single-touch model. /// Maps the mouse to a single-touch model.
/// </summary> /// </summary>
public static int TouchCount public static int TouchCount
{ {
get get
{ {
return Input.IsMouseButtonPressed(MouseButton.Left) ? 1 : 0; return Input.IsMouseButtonPressed(MouseButton.Left) ? 1 : 0;
} }
} }
/// <summary> /// <summary>
/// Mouse position in UI space (top-left origin, Control coordinate system). /// Mouse position in UI space (top-left origin, Control coordinate system).
/// Used for hit-testing against Control nodes in the UI layer. /// Used for hit-testing against Control nodes in the UI layer.
/// </summary> /// </summary>
public static Vector2 UITouchPosition public static Vector2 UITouchPosition
{ {
get get
{ {
var mp = GetViewport()?.GetMousePosition() ?? Vector2.Zero; var mp = GetViewport()?.GetMousePosition() ?? Vector2.Zero;
// In Godot, UI coordinates match mouse coordinates (top-left origin in Control nodes) // In Godot, UI coordinates match mouse coordinates (top-left origin in Control nodes)
return mp; return mp;
} }
} }
/// <summary> /// <summary>
/// Mouse position in screen space. Used for raycasting against 2D/3D /// Mouse position in screen space. Used for raycasting against 2D/3D
/// world objects. /// world objects.
/// </summary> /// </summary>
public static Vector2 TouchPosition public static Vector2 TouchPosition
{ {
get get
{ {
return GetViewport()?.GetMousePosition() ?? Vector2.Zero; return GetViewport()?.GetMousePosition() ?? Vector2.Zero;
} }
} }
/// <summary> /// <summary>
/// Converts <see cref="TouchPosition"/> to a 3D vector (z=0) for ray-origin /// Converts <see cref="TouchPosition"/> to a 3D vector (z=0) for ray-origin
/// calculations in the 3D input pipeline. /// calculations in the 3D input pipeline.
/// </summary> /// </summary>
public static Vector3 TouchPosition3D public static Vector3 TouchPosition3D
{ {
get get
{ {
var pos = TouchPosition; var pos = TouchPosition;
return new Vector3(pos.X, pos.Y, 0); return new Vector3(pos.X, pos.Y, 0);
} }
} }
/// <summary> /// <summary>
/// Last-frame mouse velocity from Godot's <c>Input.GetLastMouseVelocity()</c>. /// Last-frame mouse velocity from Godot's <c>Input.GetLastMouseVelocity()</c>.
/// Used by drag handlers to compute movement deltas. /// Used by drag handlers to compute movement deltas.
/// </summary> /// </summary>
public static Vector2 TouchDelta public static Vector2 TouchDelta
{ {
get get
{ {
return Input.GetLastMouseVelocity(); return Input.GetLastMouseVelocity();
} }
} }
/// <summary> /// <summary>
/// Returns the current touch position. The <paramref name="index"/> parameter /// Returns the current touch position. The <paramref name="index"/> parameter
/// is ignored in the single-touch model — always returns <see cref="TouchPosition"/>. /// is ignored in the single-touch model — always returns <see cref="TouchPosition"/>.
/// Exists for multi-touch API compatibility. /// Exists for multi-touch API compatibility.
/// </summary> /// </summary>
/// <param name="index">Touch index (ignored in current implementation).</param> /// <param name="index">Touch index (ignored in current implementation).</param>
public static Vector2 GetTouchPosition(int index) public static Vector2 GetTouchPosition(int index)
{ {
return TouchPosition; return TouchPosition;
} }
private static Viewport GetViewport() private static Viewport GetViewport()
{ {
var tree = Godot.Engine.GetMainLoop() as SceneTree; var tree = Godot.Engine.GetMainLoop() as SceneTree;
return tree?.Root?.GetViewport(); return tree?.Root?.GetViewport();
} }
} }
} }
+119 -119
View File
@@ -1,121 +1,121 @@
using Cthangover.Core.Events; using Cthangover.Core.Events;
using Cthangover.Core.Scenes; using Cthangover.Core.Scenes;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Cthangover.Core.Utils; using Cthangover.Core.Utils;
using Godot; using Godot;
using Cthangover.Core.UI.Messages; using Cthangover.Core.UI.Messages;
namespace Cthangover.Mods.Interface.Messages namespace Cthangover.Mods.Interface.Messages
{ {
/// <summary> /// <summary>
/// Static factory for floating UI messages (like "Quest updated" or item /// Static factory for floating UI messages (like "Quest updated" or item
/// pickup notifications). Manages a cycling position index of 0-5 so /// pickup notifications). Manages a cycling position index of 0-5 so
/// consecutive messages stack vertically without overlapping. Each message /// consecutive messages stack vertically without overlapping. Each message
/// is instantiated from message.tscn and parented to the Background node /// is instantiated from message.tscn and parented to the Background node
/// (found via scene tree search). Messages auto-fade and self-destruct /// (found via scene tree search). Messages auto-fade and self-destruct
/// via UiMessage's OnUpdate. /// via UiMessage's OnUpdate.
/// </summary> /// </summary>
public static class MessagesHelper public static class MessagesHelper
{ {
private const int maxMessageIndex = 5; private const int maxMessageIndex = 5;
private static int lastMessageIndex = 0; private static int lastMessageIndex = 0;
private static ModScenePrefab prefab; private static ModScenePrefab prefab;
private static int GetNextIndex(int index) private static int GetNextIndex(int index)
{ {
if (index++ >= maxMessageIndex) if (index++ >= maxMessageIndex)
index = 0; index = 0;
return index; return index;
} }
private static float GetPositionByLastIndex(int index) private static float GetPositionByLastIndex(int index)
{ {
return (index - 1) * 60; return (index - 1) * 60;
} }
/// <summary> /// <summary>
/// Resets the cycling position index to 0. Call when UI state resets /// Resets the cycling position index to 0. Call when UI state resets
/// to prevent messages from accumulating at high Y positions. /// to prevent messages from accumulating at high Y positions.
/// </summary> /// </summary>
public static void ClearLastIndex() public static void ClearLastIndex()
{ {
lastMessageIndex = 0; lastMessageIndex = 0;
} }
/// <summary> /// <summary>
/// Shows a message in the default warning colour at the next vertical slot. /// Shows a message in the default warning colour at the next vertical slot.
/// </summary> /// </summary>
/// <param name="text">The message text to display.</param> /// <param name="text">The message text to display.</param>
/// <param name="speed">Fade speed multiplier (higher = faster fade, default 1).</param> /// <param name="speed">Fade speed multiplier (higher = faster fade, default 1).</param>
public static void AddMessage(string text, float speed = 1f) public static void AddMessage(string text, float speed = 1f)
{ {
AddMessage(text, UiTokens.Warning, speed); AddMessage(text, UiTokens.Warning, speed);
} }
/// <summary> /// <summary>
/// Shows a message with a custom color at the next vertical slot. /// Shows a message with a custom color at the next vertical slot.
/// Messages auto-fade and self-destruct after ~3 seconds of display plus /// Messages auto-fade and self-destruct after ~3 seconds of display plus
/// a fade duration determined by <paramref name="speed"/>. /// a fade duration determined by <paramref name="speed"/>.
/// </summary> /// </summary>
/// <param name="text">The message text to display.</param> /// <param name="text">The message text to display.</param>
/// <param name="color">The text color.</param> /// <param name="color">The text color.</param>
/// <param name="speed">Fade speed multiplier (higher = faster fade, default 1).</param> /// <param name="speed">Fade speed multiplier (higher = faster fade, default 1).</param>
public static void AddMessage(string text, Color color, float speed = 1f) public static void AddMessage(string text, Color color, float speed = 1f)
{ {
var nextIndex = GetNextIndex(lastMessageIndex); var nextIndex = GetNextIndex(lastMessageIndex);
AddMessage(GetPositionByLastIndex(nextIndex), text, color, speed); AddMessage(GetPositionByLastIndex(nextIndex), text, color, speed);
lastMessageIndex = nextIndex; lastMessageIndex = nextIndex;
} }
/// <summary> /// <summary>
/// Shows a message in the default warning colour at a specific vertical /// Shows a message in the default warning colour at a specific vertical
/// position, bypassing the cycling slot system. /// position, bypassing the cycling slot system.
/// </summary> /// </summary>
/// <param name="position">Y-offset in pixels for this message.</param> /// <param name="position">Y-offset in pixels for this message.</param>
/// <param name="text">The message text to display.</param> /// <param name="text">The message text to display.</param>
/// <param name="speed">Fade speed multiplier (default 1).</param> /// <param name="speed">Fade speed multiplier (default 1).</param>
public static void AddMessage(float position, string text, float speed = 1f) public static void AddMessage(float position, string text, float speed = 1f)
{ {
AddMessage(position, text, UiTokens.Warning, speed); AddMessage(position, text, UiTokens.Warning, speed);
} }
/// <summary> /// <summary>
/// Instantiates a <see cref="UiMessage"/> from <c>message.tscn</c>, parents /// Instantiates a <see cref="UiMessage"/> from <c>message.tscn</c>, parents
/// it to the Background node, and configures it for display and auto-fade. /// it to the Background node, and configures it for display and auto-fade.
/// </summary> /// </summary>
/// <param name="position">Y-offset in pixels for this message.</param> /// <param name="position">Y-offset in pixels for this message.</param>
/// <param name="text">The message text to display.</param> /// <param name="text">The message text to display.</param>
/// <param name="color">The text color.</param> /// <param name="color">The text color.</param>
/// <param name="speed">Fade speed multiplier (default 1).</param> /// <param name="speed">Fade speed multiplier (default 1).</param>
public static void AddMessage(float position, string text, Color color, float speed = 1f) public static void AddMessage(float position, string text, Color color, float speed = 1f)
{ {
if (prefab == null) if (prefab == null)
prefab = new ModScenePrefab("scenes/ui/message.tscn"); prefab = new ModScenePrefab("scenes/ui/message.tscn");
var instance = prefab.Instantiate<UiMessage>(); var instance = prefab.Instantiate<UiMessage>();
var background = SceneContextNode.FindNode<Control>("Background"); var background = SceneContextNode.FindNode<Control>("Background");
if (background == null) if (background == null)
{ {
background = SceneContextNode.Instance?.GetTree()?.GetFirstNodeInGroup("Background") as Control; background = SceneContextNode.Instance?.GetTree()?.GetFirstNodeInGroup("Background") as Control;
if (background == null) if (background == null)
{ {
GameLogger.Log("UI", "Background node not found for messages", LogLevel.Error); GameLogger.Log("UI", "Background node not found for messages", LogLevel.Error);
return; return;
} }
} }
background.AddChild(instance); background.AddChild(instance);
instance.Setup(position, text, speed, color); instance.Setup(position, text, speed, color);
ModEventBus.Publish(new NotificationRequestedEvent ModEventBus.Publish(new NotificationRequestedEvent
{ {
Text = text, Text = text,
Category = "message", Category = "message",
}); });
} }
} }
} }
+103 -103
View File
@@ -1,104 +1,104 @@
using Cthangover.Core.Scenes; using Cthangover.Core.Scenes;
using Cthangover.Core.UI.Event; using Cthangover.Core.UI.Event;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Godot; using Godot;
using Cthangover.Core.UI.Messages; using Cthangover.Core.UI.Messages;
namespace Cthangover.Mods.Interface.Messages namespace Cthangover.Mods.Interface.Messages
{ {
/// <summary> /// <summary>
/// Floating text message with auto-fade. Appears at a vertical position, /// Floating text message with auto-fade. Appears at a vertical position,
/// stays visible for 3 seconds, then fades over 2/Speed seconds. Self- /// stays visible for 3 seconds, then fades over 2/Speed seconds. Self-
/// destructs (QueueFree) when fully transparent. Registers with /// destructs (QueueFree) when fully transparent. Registers with
/// SceneEventController for per-frame updates; unregisters on tree exit /// SceneEventController for per-frame updates; unregisters on tree exit
/// to prevent leaks. Speed affects only the fade duration, not the display /// to prevent leaks. Speed affects only the fade duration, not the display
/// duration, so faster speed = shorter fade = message disappears more quickly. /// duration, so faster speed = shorter fade = message disappears more quickly.
/// </summary> /// </summary>
public partial class UiMessage : Control, IOnUpdateEvent public partial class UiMessage : Control, IOnUpdateEvent
{ {
[Export] private Label textField; [Export] private Label textField;
/// <summary>Tint of the message text; defaults to the warning colour.</summary> /// <summary>Tint of the message text; defaults to the warning colour.</summary>
[Export] public Color Color { get; set; } = UiTokens.Warning; [Export] public Color Color { get; set; } = UiTokens.Warning;
/// <summary>Fade speed multiplier — 1 is normal; higher values shorten the fade-out.</summary> /// <summary>Fade speed multiplier — 1 is normal; higher values shorten the fade-out.</summary>
[Export] public float Speed { get; set; } = 1f; [Export] public float Speed { get; set; } = 1f;
private float timestamp; private float timestamp;
/// <summary> /// <summary>
/// The text content displayed by this message. Reads/writes the child /// The text content displayed by this message. Reads/writes the child
/// <c>textField</c> label directly. /// <c>textField</c> label directly.
/// </summary> /// </summary>
public string Text public string Text
{ {
get => textField.Text; get => textField.Text;
set => textField.Text = value; set => textField.Text = value;
} }
/// <summary> /// <summary>
/// Initializes the message: positions it, sets text/speed/color, records /// Initializes the message: positions it, sets text/speed/color, records
/// the start timestamp, and registers with the <see cref="SceneEventController"/> /// the start timestamp, and registers with the <see cref="SceneEventController"/>
/// for per-frame updates. Called by <see cref="MessagesHelper"/>. /// for per-frame updates. Called by <see cref="MessagesHelper"/>.
/// </summary> /// </summary>
/// <param name="pos">Vertical Y-offset in pixels.</param> /// <param name="pos">Vertical Y-offset in pixels.</param>
/// <param name="text">The message text.</param> /// <param name="text">The message text.</param>
/// <param name="speed">Fade speed multiplier.</param> /// <param name="speed">Fade speed multiplier.</param>
/// <param name="color">The text color.</param> /// <param name="color">The text color.</param>
public void Setup(float pos, string text, float speed, Color color) public void Setup(float pos, string text, float speed, Color color)
{ {
OffsetLeft = 50; OffsetLeft = 50;
OffsetRight = -10; OffsetRight = -10;
Size = new Vector2(Size.X, 50); Size = new Vector2(Size.X, 50);
Position = new Vector2(0, pos + 200); Position = new Vector2(0, pos + 200);
Text = text; Text = text;
Speed = speed; Speed = speed;
Color = color; Color = color;
timestamp = (float)Godot.Time.GetTicksMsec() / 1000f; timestamp = (float)Godot.Time.GetTicksMsec() / 1000f;
if (SceneContextNode.Instance != null) if (SceneContextNode.Instance != null)
{ {
var eventController = SceneContextNode.FindNode<SceneEventController>("EventController"); var eventController = SceneContextNode.FindNode<SceneEventController>("EventController");
eventController?.AddUpdateEventListener(this); eventController?.AddUpdateEventListener(this);
} }
} }
/// <summary>Detaches from the event controller so a freed message stops receiving updates.</summary> /// <summary>Detaches from the event controller so a freed message stops receiving updates.</summary>
public override void _ExitTree() public override void _ExitTree()
{ {
if (SceneContextNode.Instance != null) if (SceneContextNode.Instance != null)
{ {
var eventController = SceneContextNode.FindNode<SceneEventController>("EventController"); var eventController = SceneContextNode.FindNode<SceneEventController>("EventController");
eventController?.RemoveUpdateEventListener(this); eventController?.RemoveUpdateEventListener(this);
} }
} }
/// <summary> /// <summary>
/// Priority <c>1</c> — lightweight enough for many concurrent messages. /// Priority <c>1</c> — lightweight enough for many concurrent messages.
/// </summary> /// </summary>
public int Priority => 1; public int Priority => 1;
/// <summary> /// <summary>
/// Per-frame update: displays for 3 seconds, then fades out over /// Per-frame update: displays for 3 seconds, then fades out over
/// <c>2 / <see cref="Speed"/></c> seconds. Self-destructs via /// <c>2 / <see cref="Speed"/></c> seconds. Self-destructs via
/// <c>QueueFree</c> when fully transparent. /// <c>QueueFree</c> when fully transparent.
/// </summary> /// </summary>
public void OnUpdate() public void OnUpdate()
{ {
var currentTime = (float)Godot.Time.GetTicksMsec() / 1000f; var currentTime = (float)Godot.Time.GetTicksMsec() / 1000f;
var elapsed = currentTime - timestamp; var elapsed = currentTime - timestamp;
if (elapsed < 3f) if (elapsed < 3f)
return; return;
var fadeDuration = 2f / Speed; var fadeDuration = 2f / Speed;
var fadeElapsed = elapsed - 3f; var fadeElapsed = elapsed - 3f;
var progress = Mathf.Clamp(fadeElapsed / fadeDuration, 0f, 1f); var progress = Mathf.Clamp(fadeElapsed / fadeDuration, 0f, 1f);
textField.SelfModulate = UiTokens.Alpha(Colors.White, 1f - progress); textField.SelfModulate = UiTokens.Alpha(Colors.White, 1f - progress);
if (progress >= 1f) if (progress >= 1f)
QueueFree(); QueueFree();
} }
} }
} }
+29 -29
View File
@@ -1,30 +1,30 @@
using Cthangover.Core.Factories.Impls; using Cthangover.Core.Factories.Impls;
using Godot; using Godot;
using Cthangover.Core.UI.Tool; using Cthangover.Core.UI.Tool;
namespace Cthangover.Mods.Interface.Tool namespace Cthangover.Mods.Interface.Tool
{ {
/// <summary> /// <summary>
/// Mod-aware TextureButton: resolves IconId through UIIconFactory at _Ready /// Mod-aware TextureButton: resolves IconId through UIIconFactory at _Ready
/// and applies the result to TextureNormal. Allows tool buttons to reference /// and applies the result to TextureNormal. Allows tool buttons to reference
/// icons by logical ID that mods can override. /// icons by logical ID that mods can override.
/// </summary> /// </summary>
public partial class ButtonModIconLoader : TextureButton public partial class ButtonModIconLoader : TextureButton
{ {
/// <summary> /// <summary>
/// Logical icon ID resolved through <see cref="UIIconFactory"/>. Set in the editor /// Logical icon ID resolved through <see cref="UIIconFactory"/>. Set in the editor
/// or via mod to reference a mod-overridable icon texture. /// or via mod to reference a mod-overridable icon texture.
/// </summary> /// </summary>
[Export] public string IconId { get; set; } [Export] public string IconId { get; set; }
public override void _Ready() public override void _Ready()
{ {
if (string.IsNullOrEmpty(IconId)) if (string.IsNullOrEmpty(IconId))
return; return;
var texture = UIIconFactory.Instance.Get(IconId); var texture = UIIconFactory.Instance.Get(IconId);
if (texture != null) if (texture != null)
TextureNormal = texture; TextureNormal = texture;
} }
} }
} }
+29 -29
View File
@@ -1,30 +1,30 @@
using Cthangover.Core.Factories.Impls; using Cthangover.Core.Factories.Impls;
using Godot; using Godot;
using Cthangover.Core.UI.Tool; using Cthangover.Core.UI.Tool;
namespace Cthangover.Mods.Interface.Tool namespace Cthangover.Mods.Interface.Tool
{ {
/// <summary> /// <summary>
/// Mod-aware TextureRect: resolves IconId through UIIconFactory at _Ready /// Mod-aware TextureRect: resolves IconId through UIIconFactory at _Ready
/// and applies the result. Used as a base class for any TextureRect that /// and applies the result. Used as a base class for any TextureRect that
/// needs mod-overridable textures without scene modification. /// needs mod-overridable textures without scene modification.
/// </summary> /// </summary>
public partial class TextureRectModIconLoader : TextureRect public partial class TextureRectModIconLoader : TextureRect
{ {
/// <summary> /// <summary>
/// Logical icon ID resolved through <see cref="UIIconFactory"/>. Set in the editor /// Logical icon ID resolved through <see cref="UIIconFactory"/>. Set in the editor
/// or via mod to reference a mod-overridable icon texture. /// or via mod to reference a mod-overridable icon texture.
/// </summary> /// </summary>
[Export] public string IconId { get; set; } [Export] public string IconId { get; set; }
public override void _Ready() public override void _Ready()
{ {
if (string.IsNullOrEmpty(IconId)) if (string.IsNullOrEmpty(IconId))
return; return;
var texture = UIIconFactory.Instance.Get(IconId); var texture = UIIconFactory.Instance.Get(IconId);
if (texture != null) if (texture != null)
Texture = texture; Texture = texture;
} }
} }
} }
+32 -32
View File
@@ -1,32 +1,32 @@
namespace Cthangover.Mods.Interface.View namespace Cthangover.Mods.Interface.View
{ {
/// <summary> /// <summary>
/// 9-point alignment grid for viewport content positioning. The naming /// 9-point alignment grid for viewport content positioning. The naming
/// uses horizontal-first, vertical-second convention (LeftTop, CenterCenter, /// uses horizontal-first, vertical-second convention (LeftTop, CenterCenter,
/// RightBottom) — matching the intuitive order for CSS-like layout. /// RightBottom) — matching the intuitive order for CSS-like layout.
/// </summary> /// </summary>
public enum AlignType public enum AlignType
{ {
/// <summary>Content aligned to top-left corner of the viewport.</summary> /// <summary>Content aligned to top-left corner of the viewport.</summary>
LeftTop, LeftTop,
/// <summary>Content horizontally left, vertically centered in the viewport.</summary> /// <summary>Content horizontally left, vertically centered in the viewport.</summary>
LeftCenter, LeftCenter,
/// <summary>Content horizontally left, vertically at the bottom of the viewport.</summary> /// <summary>Content horizontally left, vertically at the bottom of the viewport.</summary>
LeftBottom, LeftBottom,
/// <summary>Content horizontally centered, vertically at the top of the viewport.</summary> /// <summary>Content horizontally centered, vertically at the top of the viewport.</summary>
CenterTop, CenterTop,
/// <summary>Content centered both horizontally and vertically in the viewport.</summary> /// <summary>Content centered both horizontally and vertically in the viewport.</summary>
CenterCenter, CenterCenter,
/// <summary>Content horizontally centered, vertically at the bottom of the viewport.</summary> /// <summary>Content horizontally centered, vertically at the bottom of the viewport.</summary>
CenterBottom, CenterBottom,
/// <summary>Content horizontally right, vertically at the top of the viewport.</summary> /// <summary>Content horizontally right, vertically at the top of the viewport.</summary>
RightTop, RightTop,
/// <summary>Content horizontally right, vertically centered in the viewport.</summary> /// <summary>Content horizontally right, vertically centered in the viewport.</summary>
RightCenter, RightCenter,
/// <summary>Content aligned to the bottom-right corner of the viewport.</summary> /// <summary>Content aligned to the bottom-right corner of the viewport.</summary>
RightBottom, RightBottom,
} }
} }
+284 -284
View File
@@ -1,284 +1,284 @@
using Cthangover.Core.Mods; using Cthangover.Core.Mods;
using Cthangover.Core.Mods.Resolvers; using Cthangover.Core.Mods.Resolvers;
using Cthangover.Core.Scenes; using Cthangover.Core.Scenes;
using Cthangover.Core.Utils; using Cthangover.Core.Utils;
using Cthangover.Core.UI; using Cthangover.Core.UI;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Godot; using Godot;
namespace Cthangover.Mods.Interface.View namespace Cthangover.Mods.Interface.View
{ {
/// <summary> /// <summary>
/// Core viewport container: manages background/foreground TextureRects with /// Core viewport container: manages background/foreground TextureRects with
/// a shared shader material (timed_sprite from mods) for time-of-day lighting. /// a shared shader material (timed_sprite from mods) for time-of-day lighting.
/// Supports smooth background transitions via a black ColorRect overlay that /// Supports smooth background transitions via a black ColorRect overlay that
/// fades in, swaps the texture mid-transition, then fades out — a classic /// fades in, swaps the texture mid-transition, then fades out — a classic
/// crossfade approach that doesn't require a second TextureRect. The zoom /// crossfade approach that doesn't require a second TextureRect. The zoom
/// system scales the content Control between minZoom and maxZoom based on /// system scales the content Control between minZoom and maxZoom based on
/// a 0–1 input value. Alignment positions content within the viewport using /// a 0–1 input value. Alignment positions content within the viewport using
/// ViewBoxNavigator's 9-point grid. The #if TOOLS block forces content size /// ViewBoxNavigator's 9-point grid. The #if TOOLS block forces content size
/// to defaultResolution for editor preview. /// to defaultResolution for editor preview.
/// ///
/// Interactive layers are Control containers placed between Background and /// Interactive layers are Control containers placed between Background and
/// Foreground (and above Foreground for UI) that host interactive scene objects. /// Foreground (and above Foreground for UI) that host interactive scene objects.
/// </summary> /// </summary>
public partial class ViewBox : Control, IBackgroundView public partial class ViewBox : Control, IBackgroundView
{ {
[Export] private Vector2I defaultResolution = new(1920, 1024); [Export] private Vector2I defaultResolution = new(1920, 1024);
[Export] private Control content; [Export] private Control content;
[Export] private float minZoom; [Export] private float minZoom;
[Export] private float maxZoom; [Export] private float maxZoom;
/// <summary> /// <summary>
/// The content node that holds all scene visuals. Scaling and alignment are applied /// The content node that holds all scene visuals. Scaling and alignment are applied
/// directly to this node. /// directly to this node.
/// </summary> /// </summary>
public Control Content => content; public Control Content => content;
/// <summary>Logical resolution of the content area (default 1920x1024). Used for normalised coordinate calculations.</summary> /// <summary>Logical resolution of the content area (default 1920x1024). Used for normalised coordinate calculations.</summary>
public Vector2I LogicalSize => defaultResolution; public Vector2I LogicalSize => defaultResolution;
/// <summary> /// <summary>
/// The background <see cref="TextureRect"/> rendered behind all interactive layers. /// The background <see cref="TextureRect"/> rendered behind all interactive layers.
/// Receives a <c>timed_sprite</c> shader material for time-of-day lighting when /// Receives a <c>timed_sprite</c> shader material for time-of-day lighting when
/// no material is already assigned. /// no material is already assigned.
/// </summary> /// </summary>
public TextureRect Background { get; private set; } public TextureRect Background { get; private set; }
/// <summary> /// <summary>
/// The foreground <see cref="TextureRect"/> rendered above interactive layers. /// The foreground <see cref="TextureRect"/> rendered above interactive layers.
/// Receives the same <c>timed_sprite</c> shader as the background. /// Receives the same <c>timed_sprite</c> shader as the background.
/// </summary> /// </summary>
public TextureRect Foreground { get; private set; } public TextureRect Foreground { get; private set; }
/// <summary>Container for interactive objects rendered behind the foreground.</summary> /// <summary>Container for interactive objects rendered behind the foreground.</summary>
public Control InteractiveLayerBackground { get; private set; } public Control InteractiveLayerBackground { get; private set; }
/// <summary>Container for interactive objects rendered above the foreground.</summary> /// <summary>Container for interactive objects rendered above the foreground.</summary>
public Control InteractiveLayerForeground { get; private set; } public Control InteractiveLayerForeground { get; private set; }
/// <summary>Container for interactive objects rendered above all scene visuals (UI layer).</summary> /// <summary>Container for interactive objects rendered above all scene visuals (UI layer).</summary>
public Control InteractiveLayerUI { get; private set; } public Control InteractiveLayerUI { get; private set; }
private ColorRect transitionOverlay; private ColorRect transitionOverlay;
private Tween transitionTween; private Tween transitionTween;
/// <summary>Resolves the background and foreground nodes and assigns the time-of-day shader when no material is set.</summary> /// <summary>Resolves the background and foreground nodes and assigns the time-of-day shader when no material is set.</summary>
public override void _Ready() public override void _Ready()
{ {
Background ??= GetNodeOrNull<TextureRect>("Background"); Background ??= GetNodeOrNull<TextureRect>("Background");
Foreground ??= GetNodeOrNull<TextureRect>("Foreground"); Foreground ??= GetNodeOrNull<TextureRect>("Foreground");
var timedShader = Shaders.Resolve("timed_sprite"); var timedShader = Shaders.Resolve("timed_sprite");
if (timedShader != null) if (timedShader != null)
{ {
if (Background != null && Background.Material == null) if (Background != null && Background.Material == null)
{ {
Background.Material = new ShaderMaterial { Shader = timedShader }; Background.Material = new ShaderMaterial { Shader = timedShader };
GameLogger.Log("LIGHT", "ViewBox._Ready: created ShaderMaterial for Background", LogLevel.Debug); GameLogger.Log("LIGHT", "ViewBox._Ready: created ShaderMaterial for Background", LogLevel.Debug);
} }
if (Foreground != null && Foreground.Material == null) if (Foreground != null && Foreground.Material == null)
{ {
Foreground.Material = new ShaderMaterial { Shader = timedShader }; Foreground.Material = new ShaderMaterial { Shader = timedShader };
GameLogger.Log("LIGHT", "ViewBox._Ready: created ShaderMaterial for Foreground", LogLevel.Debug); GameLogger.Log("LIGHT", "ViewBox._Ready: created ShaderMaterial for Foreground", LogLevel.Debug);
} }
} }
SetZoom(0f); SetZoom(0f);
SetAlign(AlignType.CenterCenter); SetAlign(AlignType.CenterCenter);
} }
/// <summary> /// <summary>
/// Returns the interactive layer container for the given logical layer name. /// Returns the interactive layer container for the given logical layer name.
/// Creates the layer lazily as a direct child of ViewBox if it doesn't exist yet. /// Creates the layer lazily as a direct child of ViewBox if it doesn't exist yet.
/// </summary> /// </summary>
/// <param name="layerName">"background", "foreground" or "ui".</param> /// <param name="layerName">"background", "foreground" or "ui".</param>
/// <returns>The matching Control container.</returns> /// <returns>The matching Control container.</returns>
public Control GetInteractiveLayer(string layerName) public Control GetInteractiveLayer(string layerName)
{ {
var result = layerName?.ToLowerInvariant() switch var result = layerName?.ToLowerInvariant() switch
{ {
"background" => InteractiveLayerBackground ??= CreateLayerDirect("InteractiveLayer_Background", 1), "background" => InteractiveLayerBackground ??= CreateLayerDirect("InteractiveLayer_Background", 1),
"foreground" => InteractiveLayerForeground ??= CreateLayerDirect("InteractiveLayer_Foreground", 3), "foreground" => InteractiveLayerForeground ??= CreateLayerDirect("InteractiveLayer_Foreground", 3),
"ui" => InteractiveLayerUI ??= CreateLayerDirect("InteractiveLayer_UI", 5), "ui" => InteractiveLayerUI ??= CreateLayerDirect("InteractiveLayer_UI", 5),
_ => InteractiveLayerForeground ??= CreateLayerDirect("InteractiveLayer_Foreground", 3) _ => InteractiveLayerForeground ??= CreateLayerDirect("InteractiveLayer_Foreground", 3)
}; };
return result; return result;
} }
private Control CreateLayerDirect(string name, int zIndex) private Control CreateLayerDirect(string name, int zIndex)
{ {
var layer = new Control var layer = new Control
{ {
Name = name, Name = name,
MouseFilter = MouseFilterEnum.Ignore, MouseFilter = MouseFilterEnum.Ignore,
ZIndex = zIndex, ZIndex = zIndex,
Size = new Vector2(defaultResolution.X, defaultResolution.Y) Size = new Vector2(defaultResolution.X, defaultResolution.Y)
}; };
AddChild(layer); AddChild(layer);
return layer; return layer;
} }
/// <summary> /// <summary>
/// Assigns a texture to the background <see cref="TextureRect"/>. This is the /// Assigns a texture to the background <see cref="TextureRect"/>. This is the
/// primary mechanism for scenes to set the backdrop visual. Logs the new background /// primary mechanism for scenes to set the backdrop visual. Logs the new background
/// ID from <see cref="SceneContextNode.LastBackgroundID"/> for debugging. /// ID from <see cref="SceneContextNode.LastBackgroundID"/> for debugging.
/// </summary> /// </summary>
public void SetBackgroundTexture(Texture2D texture) public void SetBackgroundTexture(Texture2D texture)
{ {
if (Background != null) if (Background != null)
{ {
Background.Texture = texture; Background.Texture = texture;
GameLogger.Log("VIEW", $"set background '" + SceneContextNode.LastBackgroundID + "'"); GameLogger.Log("VIEW", $"set background '" + SceneContextNode.LastBackgroundID + "'");
} }
} }
/// <summary> /// <summary>
/// Assigns a texture to the foreground <see cref="TextureRect"/>. Typically used /// Assigns a texture to the foreground <see cref="TextureRect"/>. Typically used
/// for scene overlays rendered above interactive objects but below UI. /// for scene overlays rendered above interactive objects but below UI.
/// </summary> /// </summary>
public void SetForegroundTexture(Texture2D texture) public void SetForegroundTexture(Texture2D texture)
{ {
if (Foreground != null) if (Foreground != null)
{ {
Foreground.Texture = texture; Foreground.Texture = texture;
GameLogger.Log("VIEW", $"set foreground"); GameLogger.Log("VIEW", $"set foreground");
} }
} }
/// <summary> /// <summary>
/// Returns the currently assigned background texture, or <c>null</c> if none is set /// Returns the currently assigned background texture, or <c>null</c> if none is set
/// or the <see cref="Background"/> node is missing. /// or the <see cref="Background"/> node is missing.
/// </summary> /// </summary>
public Texture2D GetBackgroundTexture() public Texture2D GetBackgroundTexture()
{ {
return Background?.Texture; return Background?.Texture;
} }
/// <summary> /// <summary>
/// Smoothly transitions the background to a new texture using a black overlay crossfade: /// Smoothly transitions the background to a new texture using a black overlay crossfade:
/// fades a <see cref="ColorRect"/> to full opacity over the first half of <paramref name="duration"/>, /// fades a <see cref="ColorRect"/> to full opacity over the first half of <paramref name="duration"/>,
/// swaps the background texture mid-transition, then fades back out over the second half. /// swaps the background texture mid-transition, then fades back out over the second half.
/// If the target texture is already set or the background node is missing, no-op. /// If the target texture is already set or the background node is missing, no-op.
/// <see cref="KillTransitionTween"/> cancels any in-progress transition first. /// <see cref="KillTransitionTween"/> cancels any in-progress transition first.
/// </summary> /// </summary>
/// <param name="newTexture">The replacement background texture.</param> /// <param name="newTexture">The replacement background texture.</param>
/// <param name="duration">Total crossfade duration in seconds (default 0.5).</param> /// <param name="duration">Total crossfade duration in seconds (default 0.5).</param>
public void TransitionBackground(Texture2D newTexture) public void TransitionBackground(Texture2D newTexture)
{ {
TransitionBackground(newTexture, 0.5f); TransitionBackground(newTexture, 0.5f);
} }
public void TransitionBackground(Texture2D newTexture, float duration = 0.5f) public void TransitionBackground(Texture2D newTexture, float duration = 0.5f)
{ {
if (Background == null) if (Background == null)
{ {
if (newTexture != null) if (newTexture != null)
SetBackgroundTexture(newTexture); SetBackgroundTexture(newTexture);
return; return;
} }
if (Background.Texture == newTexture) if (Background.Texture == newTexture)
return; return;
KillTransitionTween(); KillTransitionTween();
var overlay = GetOrCreateTransitionOverlay(); var overlay = GetOrCreateTransitionOverlay();
overlay.Visible = true; overlay.Visible = true;
overlay.Modulate = UiTokens.Alpha(Colors.Black, 0f); overlay.Modulate = UiTokens.Alpha(Colors.Black, 0f);
overlay.MouseFilter = MouseFilterEnum.Ignore; overlay.MouseFilter = MouseFilterEnum.Ignore;
var halfDuration = duration / 2f; var halfDuration = duration / 2f;
transitionTween = CreateTween(); transitionTween = CreateTween();
transitionTween.TweenProperty(overlay, "modulate:a", 1f, halfDuration); transitionTween.TweenProperty(overlay, "modulate:a", 1f, halfDuration);
transitionTween.TweenCallback(Callable.From(() => transitionTween.TweenCallback(Callable.From(() =>
{ {
SetBackgroundTexture(newTexture); SetBackgroundTexture(newTexture);
})); }));
transitionTween.TweenProperty(overlay, "modulate:a", 0f, halfDuration); transitionTween.TweenProperty(overlay, "modulate:a", 0f, halfDuration);
transitionTween.TweenCallback(Callable.From(() => transitionTween.TweenCallback(Callable.From(() =>
{ {
overlay.Visible = false; overlay.Visible = false;
transitionTween = null; transitionTween = null;
})); }));
} }
/// <summary> /// <summary>
/// Aborts any in-progress background transition by killing the tween and resetting /// Aborts any in-progress background transition by killing the tween and resetting
/// the overlay to invisible. Safe to call when no transition is active. /// the overlay to invisible. Safe to call when no transition is active.
/// </summary> /// </summary>
public void KillTransitionTween() public void KillTransitionTween()
{ {
if (transitionTween != null) if (transitionTween != null)
{ {
transitionTween.Kill(); transitionTween.Kill();
transitionTween = null; transitionTween = null;
} }
if (transitionOverlay != null) if (transitionOverlay != null)
{ {
transitionOverlay.Visible = false; transitionOverlay.Visible = false;
transitionOverlay.Modulate = UiTokens.Alpha(Colors.Black, 0f); transitionOverlay.Modulate = UiTokens.Alpha(Colors.Black, 0f);
} }
} }
private ColorRect GetOrCreateTransitionOverlay() private ColorRect GetOrCreateTransitionOverlay()
{ {
if (transitionOverlay == null) if (transitionOverlay == null)
{ {
transitionOverlay = new ColorRect transitionOverlay = new ColorRect
{ {
Name = "TransitionOverlay", Name = "TransitionOverlay",
Color = UiTokens.Alpha(Colors.Black, 1f), Color = UiTokens.Alpha(Colors.Black, 1f),
MouseFilter = MouseFilterEnum.Ignore, MouseFilter = MouseFilterEnum.Ignore,
Visible = false Visible = false
}; };
transitionOverlay.AnchorRight = 1f; transitionOverlay.AnchorRight = 1f;
transitionOverlay.AnchorBottom = 1f; transitionOverlay.AnchorBottom = 1f;
transitionOverlay.GrowHorizontal = GrowDirection.Both; transitionOverlay.GrowHorizontal = GrowDirection.Both;
transitionOverlay.GrowVertical = GrowDirection.Both; transitionOverlay.GrowVertical = GrowDirection.Both;
AddChild(transitionOverlay); AddChild(transitionOverlay);
} }
return transitionOverlay; return transitionOverlay;
} }
/// <summary> /// <summary>
/// Applies a zoom level to the content node. The <paramref name="value"/> is a 0–1 lerp /// Applies a zoom level to the content node. The <paramref name="value"/> is a 0–1 lerp
/// factor between <c>minZoom</c> and <c>maxZoom</c>. Scale is uniform (X and Y are equal). /// factor between <c>minZoom</c> and <c>maxZoom</c>. Scale is uniform (X and Y are equal).
/// </summary> /// </summary>
/// <param name="value">Normalized zoom factor: 0 = minZoom, 1 = maxZoom.</param> /// <param name="value">Normalized zoom factor: 0 = minZoom, 1 = maxZoom.</param>
public void SetZoom(float value) public void SetZoom(float value)
{ {
var zoom = minZoom + (maxZoom - minZoom) * value; var zoom = minZoom + (maxZoom - minZoom) * value;
if (content != null) if (content != null)
content.Scale = Vector2.One * zoom; content.Scale = Vector2.One * zoom;
} }
/// <summary> /// <summary>
/// Positions the content node within the viewport using the 9-point alignment grid /// Positions the content node within the viewport using the 9-point alignment grid
/// from <see cref="ViewBoxNavigator.GetPositionByAlign"/>. /// from <see cref="ViewBoxNavigator.GetPositionByAlign"/>.
/// </summary> /// </summary>
/// <param name="align">Which corner/edge/center to align to.</param> /// <param name="align">Which corner/edge/center to align to.</param>
public void SetAlign(AlignType align) public void SetAlign(AlignType align)
{ {
if (content != null) if (content != null)
content.Position = ViewBoxNavigator.GetPositionByAlign(align, content.Size); content.Position = ViewBoxNavigator.GetPositionByAlign(align, content.Size);
} }
#if TOOLS && DEBUG #if TOOLS && DEBUG
/// <summary>Editor-only: forces the content back to the default resolution rect while inspecting properties.</summary> /// <summary>Editor-only: forces the content back to the default resolution rect while inspecting properties.</summary>
public override void _ValidateProperty(Godot.Collections.Dictionary property) public override void _ValidateProperty(Godot.Collections.Dictionary property)
{ {
base._ValidateProperty(property); base._ValidateProperty(property);
if (content != null) if (content != null)
content.Size = defaultResolution; content.Size = defaultResolution;
} }
#endif #endif
} }
} }
+73 -73
View File
@@ -1,73 +1,73 @@
using Godot; using Godot;
namespace Cthangover.Mods.Interface.View namespace Cthangover.Mods.Interface.View
{ {
/// <summary> /// <summary>
/// Static utility that calculates content positioning offsets for the 9-point /// Static utility that calculates content positioning offsets for the 9-point
/// AlignType grid. Uses the viewport's visible rect as the camera/view area /// AlignType grid. Uses the viewport's visible rect as the camera/view area
/// and computes offsets so the content aligns correctly within it. The /// and computes offsets so the content aligns correctly within it. The
/// CenterCenter case negates X because the content position is treated as /// CenterCenter case negates X because the content position is treated as
/// a negative offset from top-left in Godot's coordinate space. /// a negative offset from top-left in Godot's coordinate space.
/// </summary> /// </summary>
public static class ViewBoxNavigator public static class ViewBoxNavigator
{ {
/// <summary> /// <summary>
/// Calculates the content node position offset for the given 9-point <see cref="AlignType"/>. /// Calculates the content node position offset for the given 9-point <see cref="AlignType"/>.
/// Uses the visible viewport rect size as the camera area and computes offsets relative /// Uses the visible viewport rect size as the camera area and computes offsets relative
/// to the top-left corner. Negative X coordinates are intentional: Godot positions content /// to the top-left corner. Negative X coordinates are intentional: Godot positions content
/// relative to the container origin, so aligning center or right requires negative offsets /// relative to the container origin, so aligning center or right requires negative offsets
/// when content is larger than the viewport. /// when content is larger than the viewport.
/// </summary> /// </summary>
/// <param name="align">The target alignment from the 9-point grid.</param> /// <param name="align">The target alignment from the 9-point grid.</param>
/// <param name="contentSize">The size of the content node being positioned.</param> /// <param name="contentSize">The size of the content node being positioned.</param>
/// <returns>A position offset that places the content correctly for the chosen alignment.</returns> /// <returns>A position offset that places the content correctly for the chosen alignment.</returns>
public static Vector2 GetPositionByAlign(AlignType align, Vector2 contentSize) public static Vector2 GetPositionByAlign(AlignType align, Vector2 contentSize)
{ {
var viewport = Godot.Engine.GetMainLoop() as SceneTree; var viewport = Godot.Engine.GetMainLoop() as SceneTree;
var screenSize = viewport?.Root?.GetVisibleRect().Size ?? new Vector2(1920, 1024); var screenSize = viewport?.Root?.GetVisibleRect().Size ?? new Vector2(1920, 1024);
var cameraSize = screenSize; var cameraSize = screenSize;
var alignPos = Vector2.Zero; var alignPos = Vector2.Zero;
switch (align) switch (align)
{ {
case AlignType.LeftTop: case AlignType.LeftTop:
break; break;
case AlignType.LeftCenter: case AlignType.LeftCenter:
alignPos.Y = (contentSize.Y - cameraSize.Y) * 0.5f; alignPos.Y = (contentSize.Y - cameraSize.Y) * 0.5f;
break; break;
case AlignType.LeftBottom: case AlignType.LeftBottom:
alignPos.Y = contentSize.Y - cameraSize.Y; alignPos.Y = contentSize.Y - cameraSize.Y;
break; break;
case AlignType.CenterTop: case AlignType.CenterTop:
alignPos.X = -(contentSize.X - cameraSize.X) * 0.5f; alignPos.X = -(contentSize.X - cameraSize.X) * 0.5f;
break; break;
case AlignType.CenterCenter: case AlignType.CenterCenter:
alignPos = (contentSize - cameraSize) * 0.5f; alignPos = (contentSize - cameraSize) * 0.5f;
alignPos.X = -alignPos.X; alignPos.X = -alignPos.X;
break; break;
case AlignType.CenterBottom: case AlignType.CenterBottom:
alignPos.X = -(contentSize.X - cameraSize.X) * 0.5f; alignPos.X = -(contentSize.X - cameraSize.X) * 0.5f;
alignPos.Y = contentSize.Y - cameraSize.Y; alignPos.Y = contentSize.Y - cameraSize.Y;
break; break;
case AlignType.RightTop: case AlignType.RightTop:
alignPos.X = -(contentSize.X - cameraSize.X); alignPos.X = -(contentSize.X - cameraSize.X);
break; break;
case AlignType.RightCenter: case AlignType.RightCenter:
alignPos.Y = (contentSize.Y - cameraSize.Y) * 0.5f; alignPos.Y = (contentSize.Y - cameraSize.Y) * 0.5f;
alignPos.X = -(contentSize.X - cameraSize.X); alignPos.X = -(contentSize.X - cameraSize.X);
break; break;
case AlignType.RightBottom: case AlignType.RightBottom:
alignPos.X = -(contentSize.X - cameraSize.X); alignPos.X = -(contentSize.X - cameraSize.X);
alignPos.Y = contentSize.Y - cameraSize.Y; alignPos.Y = contentSize.Y - cameraSize.Y;
break; break;
} }
return alignPos; return alignPos;
} }
} }
} }
+31 -31
View File
@@ -1,31 +1,31 @@
using Godot; using Godot;
namespace Cthangover.Mods.Interface.View namespace Cthangover.Mods.Interface.View
{ {
/// <summary> /// <summary>
/// Thin adapter that exposes ViewBox.SetZoom to external callers (e.g. UI /// Thin adapter that exposes ViewBox.SetZoom to external callers (e.g. UI
/// sliders or scroll handlers) through a single method. Exists as a Node /// sliders or scroll handlers) through a single method. Exists as a Node
/// so it can be placed in the scene tree and receive signals. /// so it can be placed in the scene tree and receive signals.
/// </summary> /// </summary>
public partial class ZoomViewBoxController : Node public partial class ZoomViewBoxController : Node
{ {
[Export] private ViewBox viewBox; [Export] private ViewBox viewBox;
/// <summary> /// <summary>
/// Delegates to <see cref="ViewBox.SetZoom"/> on the linked <see cref="ViewBox"/>. /// Delegates to <see cref="ViewBox.SetZoom"/> on the linked <see cref="ViewBox"/>.
/// Intended to be connected to a slider or scroll input signal. No-op if /// Intended to be connected to a slider or scroll input signal. No-op if
/// <c>viewBox</c> is not assigned. /// <c>viewBox</c> is not assigned.
/// </summary> /// </summary>
/// <param name="value">Normalized zoom factor (0–1).</param> /// <param name="value">Normalized zoom factor (0–1).</param>
public void OnChangeZoom(float value) public void OnChangeZoom(float value)
{ {
if (viewBox == null) if (viewBox == null)
return; return;
viewBox.SetZoom(value); viewBox.SetZoom(value);
} }
} }
} }