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