using System.Collections.Concurrent; using System.Collections.Generic; using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Game.Gui; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; namespace Dalamud.Interface.ImGuiNotification.Internal; /// Class handling notifications/toasts in ImGui. [ServiceManager.EarlyLoadedService] internal class NotificationManager : INotificationManager, IInternalDisposableService { [ServiceManager.ServiceDependency] private readonly GameGui gameGui = Service.Get(); [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); private readonly List notifications = []; private readonly ConcurrentBag pendingNotifications = []; private NotificationPositionChooser? positionChooser; [ServiceManager.ServiceConstructor] private NotificationManager(FontAtlasFactory fontAtlasFactory) { this.PrivateAtlas = fontAtlasFactory.CreateFontAtlas( nameof(NotificationManager), FontAtlasAutoRebuildMode.Async); this.IconAxisFontHandle = this.PrivateAtlas.NewGameFontHandle(new(GameFontFamily.Axis, NotificationConstants.IconSize)); this.IconFontAwesomeFontHandle = this.PrivateAtlas.NewDelegateFontHandle( e => e.OnPreBuild( tk => tk.AddFontAwesomeIconFont(new() { SizePx = NotificationConstants.IconSize }))); } /// Gets the handle to AXIS fonts, sized for use as an icon. public IFontHandle IconAxisFontHandle { get; } /// Gets the handle to FontAwesome fonts, sized for use as an icon. public IFontHandle IconFontAwesomeFontHandle { get; } /// Gets the private atlas for use with notification windows. private IFontAtlas PrivateAtlas { get; } /// /// Calculate the width to be used to draw notifications. /// /// The width. public static float CalculateNotificationWidth() { var viewportSize = ImGuiHelpers.MainViewport.WorkSize; var width = ImGui.CalcTextSize(NotificationConstants.NotificationWidthMeasurementString).X; width += NotificationConstants.ScaledWindowPadding * 3; width += NotificationConstants.ScaledIconSize; return Math.Min(width, viewportSize.X * NotificationConstants.MaxNotificationWindowWidthWrtMainViewportWidth); } /// /// Check if notifications should scroll downwards on the screen, based on the anchor position. /// /// Where notifications are anchored to. /// A value indicating wether notifications should scroll downwards. public static bool ShouldScrollDownwards(Vector2 anchorPosition) { return anchorPosition.Y < 0.5f; } /// /// Choose the snap position for a notification based on the anchor position. /// /// Where notifications are anchored to. /// The snap position. public static NotificationSnapDirection ChooseSnapDirection(Vector2 anchorPosition) { if (anchorPosition.Y <= NotificationConstants.NotificationTopBottomSnapMargin) return NotificationSnapDirection.Top; if (anchorPosition.Y >= 1f - NotificationConstants.NotificationTopBottomSnapMargin) return NotificationSnapDirection.Bottom; if (anchorPosition.X <= 0.5f) return NotificationSnapDirection.Left; return NotificationSnapDirection.Right; } /// public void DisposeService() { this.PrivateAtlas.Dispose(); foreach (var n in this.pendingNotifications) n.DisposeInternal(); foreach (var n in this.notifications) n.DisposeInternal(); this.pendingNotifications.Clear(); this.notifications.Clear(); } /// public IActiveNotification AddNotification(Notification notification) { var an = new ActiveNotification(notification, null); this.pendingNotifications.Add(an); return an; } /// Adds a notification originating from a plugin. /// The notification. /// The source plugin. /// The added notification. public IActiveNotification AddNotification(Notification notification, LocalPlugin plugin) { var an = new ActiveNotification(notification, plugin); this.pendingNotifications.Add(an); return an; } /// Add a notification to the notification queue. /// The content of the notification. /// The title of the notification. /// The type of the notification. public void AddNotification( string content, string? title = null, NotificationType type = NotificationType.None) => this.AddNotification( new() { Content = content, Title = title, Type = type, }); /// Draw all currently queued notifications. public void Draw() { var height = 0f; var uiHidden = this.gameGui.GameUiHidden; while (this.pendingNotifications.TryTake(out var newNotification)) this.notifications.Add(newNotification); var width = CalculateNotificationWidth(); this.notifications.RemoveAll(static x => x.UpdateOrDisposeInternal()); var scrollsDownwards = ShouldScrollDownwards(this.configuration.NotificationAnchorPosition); var snapDirection = ChooseSnapDirection(this.configuration.NotificationAnchorPosition); foreach (var tn in this.notifications) { if (uiHidden && tn.RespectUiHidden) continue; height += tn.Draw(width, height, this.configuration.NotificationAnchorPosition, snapDirection); height += scrollsDownwards ? -NotificationConstants.ScaledWindowGap : NotificationConstants.ScaledWindowGap; } this.positionChooser?.Draw(); } /// /// Starts the position chooser for notifications. Will block the UI until the user makes a selection. /// public void StartPositionChooser() { this.positionChooser = new NotificationPositionChooser(this.configuration); this.positionChooser.SelectionMade += () => { this.positionChooser = null; }; } } /// Plugin-scoped version of a service. [PluginInterface] [ServiceManager.ScopedService] #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 internal class NotificationManagerPluginScoped : INotificationManager, IInternalDisposableService { private readonly LocalPlugin localPlugin; private readonly ConcurrentDictionary notifications = new(); [ServiceManager.ServiceDependency] private readonly NotificationManager notificationManagerService = Service.Get(); [ServiceManager.ServiceConstructor] private NotificationManagerPluginScoped(LocalPlugin localPlugin) => this.localPlugin = localPlugin; /// public IActiveNotification AddNotification(Notification notification) { var an = this.notificationManagerService.AddNotification(notification, this.localPlugin); _ = this.notifications.TryAdd(an, 0); an.Dismiss += a => this.notifications.TryRemove(a.Notification, out _); return an; } /// public void DisposeService() { while (!this.notifications.IsEmpty) { foreach (var n in this.notifications.Keys) { this.notifications.TryRemove(n, out _); ((ActiveNotification)n).RemoveNonDalamudInvocations(); } } } }