Commit pending working-tree changes (local edits, line endings)
This commit is contained in:
1 parent
9ecbbb4056
commit
3fb418fa4a
22 files changed
+1651
-1651
No files matched your search
+37
-37
@@ -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
|
||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user