using System.Collections.Generic;
using Cthangover.Core.Events;
using Cthangover.Core.Scenes;
using Cthangover.Core.UI.Event;
using Cthangover.Core.UI.Theming;
using Godot;
using Cthangover.Mods.Interface.Theming;
using Cthangover.Core.UI.Messages;
namespace Cthangover.Mods.Interface.Messages
{
///
/// Manages the visual notification list in the top-left corner of the scene.
/// Displays up to MaxVisibleSlots notification banners simultaneously.
/// Pending items wait in and are pulled
/// into visible slots as they become free. Each visible notification has a
/// 10-second lifetime; when it expires, it fades out and the remaining
/// notifications shift upward with a smooth animation.
///
public partial class NotificationPanel : Control, IOnUpdateEvent
{
private const int MaxVisibleSlots = 4;
private const float SlotHeight = 40f;
private const float SlotSpacing = 6f;
private const float DisplayDuration = 10f;
private const float FadeInDuration = 0.3f;
private const float FadeOutDuration = 0.5f;
private readonly List _slots = new();
private readonly List _dyingIndices = new();
/// Update priority 1 — runs after the highest-priority listeners.
public int Priority => 1;
/// Subscribes to notification requests; text is queued in , not shown immediately.
public override void _EnterTree()
{
ModEventBus.Subscribe(OnNotificationRequested);
}
/// Registers with the event controller for per-frame slot updates.
public override void _Ready()
{
MouseFilter = MouseFilterEnum.Ignore;
if (SceneContextNode.Instance != null)
{
var eventController = SceneContextNode.FindNode("EventController");
eventController?.AddUpdateEventListener(this);
}
}
/// Unsubscribes from notification requests and detaches from the update controller.
public override void _ExitTree()
{
ModEventBus.Unsubscribe(OnNotificationRequested);
if (SceneContextNode.Instance != null)
{
var eventController = SceneContextNode.FindNode("EventController");
eventController?.RemoveUpdateEventListener(this);
}
}
///
/// Per-frame update: expires old slots, collects dead ones,
/// pulls new notifications from the buffer, and repositions
/// all visible slots with a smooth slide animation.
///
public void OnUpdate()
{
var currentTime = (float)Godot.Time.GetTicksMsec() / 1000f;
for (int i = _slots.Count - 1; i >= 0; i--)
{
var slot = _slots[i];
var elapsed = currentTime - slot.DisplayedAt;
if (elapsed >= DisplayDuration && !slot.IsFadingOut)
{
FadeOutSlot(i);
}
}
_dyingIndices.Clear();
for (int i = 0; i < _slots.Count; i++)
{
if (_slots[i].IsDead)
_dyingIndices.Add(i);
}
for (int j = _dyingIndices.Count - 1; j >= 0; j--)
{
var idx = _dyingIndices[j];
_slots[idx].Container.QueueFree();
_slots.RemoveAt(idx);
}
while (_slots.Count < MaxVisibleSlots && NotificationBuffer.TryDequeue(out var text))
{
CreateSlot(text, currentTime);
}
RepositionSlots();
}
private void CreateSlot(string text, float time)
{
var container = new Panel
{
SizeFlagsHorizontal = SizeFlags.ShrinkBegin,
MouseFilter = MouseFilterEnum.Ignore,
};
var style = UiKit.Frame(UiTokens.RadiusMd, UiTokens.ToastBg, UiTokens.Line);
style.ContentMarginLeft = UiTokens.Space4;
style.ContentMarginRight = UiTokens.Space4;
style.ContentMarginTop = UiTokens.Space2;
style.ContentMarginBottom = UiTokens.Space2;
container.AddThemeStyleboxOverride("panel", style);
var label = new Label
{
Text = text,
Modulate = UiTokens.Alpha(Colors.White, 0f),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
};
label.AddThemeFontSizeOverride("font_size", UiTokens.FontLead);
label.AddThemeColorOverride("font_color", UiTokens.Ink);
container.AddChild(label);
AddChild(container);
var slot = new NotificationSlot
{
Container = container,
Text = text,
DisplayedAt = time,
};
_slots.Add(slot);
var tween = CreateTween();
tween.TweenProperty(label, "modulate", Colors.White, FadeInDuration);
}
private void FadeOutSlot(int index)
{
var slot = _slots[index];
slot.IsFadingOut = true;
var label = slot.Container.GetChildOrNull