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 } }