This commit is contained in:
Blair 2025-11-30 16:34:35 +10:00 committed by GitHub
commit f4b92dc68d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1138 additions and 946 deletions

View file

@ -0,0 +1,216 @@
using System.Collections.Generic;
using System.Numerics;
using Dalamud.Bindings.ImGui;
namespace Dalamud.Interface.Windowing;
/// <summary>
/// Represents a ImGui window for use with the built-in <see cref="WindowSystem"/>.
/// </summary>
public interface IWindow
{
/// <summary>
/// Gets or sets the namespace of the window.
/// </summary>
string? Namespace { get; set; }
/// <summary>
/// Gets or sets the name of the window.
/// If you have multiple windows with the same name, you will need to
/// append an unique ID to it by specifying it after "###" behind the window title.
/// </summary>
string WindowName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the window is focused.
/// </summary>
bool IsFocused { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this window is to be closed with a hotkey, like Escape, and keep game addons open in turn if it is closed.
/// </summary>
bool RespectCloseHotkey { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this window should not generate sound effects when opening and closing.
/// </summary>
bool DisableWindowSounds { get; set; }
/// <summary>
/// Gets or sets a value representing the sound effect id to be played when the window is opened.
/// </summary>
uint OnOpenSfxId { get; set; }
/// <summary>
/// Gets or sets a value representing the sound effect id to be played when the window is closed.
/// </summary>
uint OnCloseSfxId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this window should not fade in and out, regardless of the users'
/// preference.
/// </summary>
bool DisableFadeInFadeOut { get; set; }
/// <summary>
/// Gets or sets the position of this window.
/// </summary>
Vector2? Position { get; set; }
/// <summary>
/// Gets or sets the condition that defines when the position of this window is set.
/// </summary>
ImGuiCond PositionCondition { get; set; }
/// <summary>
/// Gets or sets the size of the window. The size provided will be scaled by the global scale.
/// </summary>
Vector2? Size { get; set; }
/// <summary>
/// Gets or sets the condition that defines when the size of this window is set.
/// </summary>
ImGuiCond SizeCondition { get; set; }
/// <summary>
/// Gets or sets the size constraints of the window. The size constraints provided will be scaled by the global scale.
/// </summary>
WindowSizeConstraints? SizeConstraints { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this window is collapsed.
/// </summary>
bool? Collapsed { get; set; }
/// <summary>
/// Gets or sets the condition that defines when the collapsed state of this window is set.
/// </summary>
ImGuiCond CollapsedCondition { get; set; }
/// <summary>
/// Gets or sets the window flags.
/// </summary>
ImGuiWindowFlags Flags { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this ImGui window will be forced to stay inside the main game window.
/// </summary>
bool ForceMainWindow { get; set; }
/// <summary>
/// Gets or sets this window's background alpha value.
/// </summary>
float? BgAlpha { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this ImGui window should display a close button in the title bar.
/// </summary>
bool ShowCloseButton { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this window should offer to be pinned via the window's titlebar context menu.
/// </summary>
bool AllowPinning { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this window should offer to be made click-through via the window's titlebar context menu.
/// </summary>
bool AllowClickthrough { get; set; }
/// <summary>
/// Gets a value indicating whether this window is pinned.
/// </summary>
public bool IsPinned { get; set; }
/// <summary>
/// Gets a value indicating whether this window is click-through.
/// </summary>
public bool IsClickthrough { get; set; }
/// <summary>
/// Gets or sets a list of available title bar buttons.
///
/// If <see cref="AllowPinning"/> or <see cref="AllowClickthrough"/> are set to true, and this features is not
/// disabled globally by the user, an internal title bar button to manage these is added when drawing, but it will
/// not appear in this collection. If you wish to remove this button, set both of these values to false.
/// </summary>
List<TitleBarButton> TitleBarButtons { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this window will stay open.
/// </summary>
bool IsOpen { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this window will request focus from the window system next frame.
/// </summary>
public bool RequestFocus { get; set; }
/// <summary>
/// Toggle window is open state.
/// </summary>
void Toggle();
/// <summary>
/// Bring this window to the front.
/// </summary>
void BringToFront();
/// <summary>
/// Code to always be executed before the open-state of the window is checked.
/// </summary>
void PreOpenCheck();
/// <summary>
/// Additional conditions for the window to be drawn, regardless of its open-state.
/// </summary>
/// <returns>
/// True if the window should be drawn, false otherwise.
/// </returns>
/// <remarks>
/// Not being drawn due to failing this condition will not change focus or trigger OnClose.
/// This is checked before PreDraw, but after Update.
/// </remarks>
bool DrawConditions();
/// <summary>
/// Code to be executed before conditionals are applied and the window is drawn.
/// </summary>
void PreDraw();
/// <summary>
/// Code to be executed after the window is drawn.
/// </summary>
void PostDraw();
/// <summary>
/// Code to be executed every time the window renders.
/// </summary>
/// <remarks>
/// In this method, implement your drawing code.
/// You do NOT need to ImGui.Begin your window.
/// </remarks>
void Draw();
/// <summary>
/// Code to be executed when the window is opened.
/// </summary>
void OnOpen();
/// <summary>
/// Code to be executed when the window is closed.
/// </summary>
void OnClose();
/// <summary>
/// Code to be executed when the window is safe to be disposed or removed from the window system.
/// Doing so in <see cref="IWindow.OnClose"/> may result in animations not playing correctly.
/// </summary>
void OnSafeToRemove();
/// <summary>
/// Code to be executed every frame, even when the window is collapsed.
/// </summary>
void Update();
}

View file

@ -0,0 +1,51 @@
using System.Collections.Generic;
namespace Dalamud.Interface.Windowing;
/// <summary>
/// Class running a WindowSystem using <see cref="IWindow"/> implementations to simplify ImGui windowing.
/// </summary>
public interface IWindowSystem
{
/// <summary>
/// Gets a read-only list of all <see cref="IWindow"/>s in this <see cref="WindowSystem"/>.
/// </summary>
IReadOnlyList<IWindow> Windows { get; }
/// <summary>
/// Gets a value indicating whether any window in this <see cref="WindowSystem"/> has focus and is
/// not marked to be excluded from consideration.
/// </summary>
bool HasAnyFocus { get; }
/// <summary>
/// Gets or sets the name/ID-space of this <see cref="WindowSystem"/>.
/// </summary>
string? Namespace { get; set; }
/// <summary>
/// Add a window to this <see cref="WindowSystem"/>.
/// The window system doesn't own your window, it just renders it
/// You need to store a reference to it to use it later.
/// </summary>
/// <param name="window">The window to add.</param>
void AddWindow(IWindow window);
/// <summary>
/// Remove a window from this <see cref="WindowSystem"/>.
/// Will not dispose your window, if it is disposable.
/// </summary>
/// <param name="window">The window to remove.</param>
void RemoveWindow(IWindow window);
/// <summary>
/// Remove all windows from this <see cref="WindowSystem"/>.
/// Will not dispose your windows, if they are disposable.
/// </summary>
void RemoveAllWindows();
/// <summary>
/// Draw all registered windows using ImGui.
/// </summary>
void Draw();
}

View file

@ -0,0 +1,45 @@
using System.Numerics;
using Dalamud.Bindings.ImGui;
namespace Dalamud.Interface.Windowing;
/// <summary>
/// Structure describing a title bar button.
/// </summary>
public class TitleBarButton
{
/// <summary>
/// Gets or sets the icon of the button.
/// </summary>
public FontAwesomeIcon Icon { get; set; }
/// <summary>
/// Gets or sets a vector by which the position of the icon within the button shall be offset.
/// Automatically scaled by the global font scale for you.
/// </summary>
public Vector2 IconOffset { get; set; }
/// <summary>
/// Gets or sets an action that is called when a tooltip shall be drawn.
/// May be null if no tooltip shall be drawn.
/// </summary>
public Action? ShowTooltip { get; set; }
/// <summary>
/// Gets or sets an action that is called when the button is clicked.
/// </summary>
public Action<ImGuiMouseButton> Click { get; set; }
/// <summary>
/// Gets or sets the priority the button shall be shown in.
/// Lower = closer to ImGui default buttons.
/// </summary>
public int Priority { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the button shall be clickable
/// when the respective window is set to clickthrough.
/// </summary>
public bool AvailableClickthrough { get; set; }
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,721 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using CheapLoc;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Internal;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing.Persistence;
using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Client.UI;
namespace Dalamud.Interface.Windowing;
/// <summary>
/// Base class you can use to implement an ImGui window for use with the built-in <see cref="WindowSystem"/>.
/// </summary>
public class WindowHost
{
private const float FadeInOutTime = 0.072f;
private static readonly ModuleLog Log = new("WindowSystem");
private static bool wasEscPressedLastFrame = false;
private bool internalLastIsOpen = false;
private bool didPushInternalAlpha = false;
private float? internalAlpha = null;
private bool hasInitializedFromPreset = false;
private PresetModel.PresetWindow? presetWindow;
private bool presetDirty = false;
private bool pushedFadeInAlpha = false;
private float fadeInTimer = 0f;
private float fadeOutTimer = 0f;
private IDrawListTextureWrap? fadeOutTexture = null;
private Vector2 fadeOutSize = Vector2.Zero;
private Vector2 fadeOutOrigin = Vector2.Zero;
private bool hasError = false;
private Exception? lastError;
/// <summary>
/// Initializes a new instance of the <see cref="WindowHost"/> class.
/// </summary>
/// <param name="window">A plugin provided window.</param>
internal WindowHost(IWindow window)
{
this.Window = window;
}
/// <summary>
/// Flags to control window behavior.
/// </summary>
[Flags]
internal enum WindowDrawFlags
{
/// <summary>
/// Nothing.
/// </summary>
None = 0,
/// <summary>
/// Enable window opening/closing sound effects.
/// </summary>
UseSoundEffects = 1 << 0,
/// <summary>
/// Hook into the game's focus management.
/// </summary>
UseFocusManagement = 1 << 1,
/// <summary>
/// Enable the built-in "additional options" menu on the title bar.
/// </summary>
UseAdditionalOptions = 1 << 2,
/// <summary>
/// Do not draw non-critical animations.
/// </summary>
IsReducedMotion = 1 << 3,
}
/// <summary>
/// Gets or sets the backing window provided by the plugin.
/// </summary>
public IWindow Window { get; set; }
private bool CanShowCloseButton => this.Window.ShowCloseButton && !this.Window.IsClickthrough;
/// <summary>
/// Draw the window via ImGui.
/// </summary>
/// <param name="internalDrawFlags">Flags controlling window behavior.</param>
/// <param name="persistence">Handler for window persistence data.</param>
internal void DrawInternal(WindowDrawFlags internalDrawFlags, WindowSystemPersistence? persistence)
{
this.Window.PreOpenCheck();
var doFades = !internalDrawFlags.HasFlag(WindowDrawFlags.IsReducedMotion) && !this.Window.DisableFadeInFadeOut;
if (!this.Window.IsOpen)
{
if (this.Window.IsOpen != this.internalLastIsOpen)
{
this.internalLastIsOpen = this.Window.IsOpen;
this.Window.OnClose();
this.Window.IsFocused = false;
if (internalDrawFlags.HasFlag(WindowDrawFlags.UseSoundEffects) && !this.Window.DisableWindowSounds)
UIGlobals.PlaySoundEffect(this.Window.OnCloseSfxId);
}
if (this.fadeOutTexture != null)
{
this.fadeOutTimer -= ImGui.GetIO().DeltaTime;
if (this.fadeOutTimer <= 0f)
{
this.fadeOutTexture.Dispose();
this.fadeOutTexture = null;
this.Window.OnSafeToRemove();
}
else
{
this.DrawFakeFadeOutWindow();
}
}
this.fadeInTimer = doFades ? 0f : FadeInOutTime;
return;
}
this.fadeInTimer += ImGui.GetIO().DeltaTime;
if (this.fadeInTimer > FadeInOutTime)
this.fadeInTimer = FadeInOutTime;
this.Window.Update();
if (!this.Window.DrawConditions())
return;
var hasNamespace = !string.IsNullOrEmpty(this.Window.Namespace);
if (hasNamespace)
ImGui.PushID(this.Window.Namespace);
this.PreHandlePreset(persistence);
if (this.internalLastIsOpen != this.Window.IsOpen && this.Window.IsOpen)
{
this.internalLastIsOpen = this.Window.IsOpen;
this.Window.OnOpen();
if (internalDrawFlags.HasFlag(WindowDrawFlags.UseSoundEffects) && !this.Window.DisableWindowSounds)
UIGlobals.PlaySoundEffect(this.Window.OnOpenSfxId);
}
// TODO: We may have to allow for windows to configure if they should fade
if (this.internalAlpha.HasValue)
{
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, this.internalAlpha.Value);
this.didPushInternalAlpha = true;
}
this.Window.PreDraw();
this.ApplyConditionals();
if (this.Window.ForceMainWindow)
ImGuiHelpers.ForceNextWindowMainViewport();
var wasFocused = this.Window.IsFocused;
if (wasFocused)
{
var style = ImGui.GetStyle();
var focusedHeaderColor = style.Colors[(int)ImGuiCol.TitleBgActive];
ImGui.PushStyleColor(ImGuiCol.TitleBgCollapsed, focusedHeaderColor);
}
if (this.Window.RequestFocus)
{
ImGui.SetNextWindowFocus();
this.Window.RequestFocus = false;
}
var flags = this.Window.Flags;
if (this.Window.IsPinned || this.Window.IsClickthrough)
flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize;
if (this.Window.IsClickthrough)
flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs;
var isWindowOpen = this.Window.IsOpen;
if (this.CanShowCloseButton ? ImGui.Begin(this.Window.WindowName, ref isWindowOpen, flags) : ImGui.Begin(this.Window.WindowName, flags))
{
if (this.Window.IsOpen != isWindowOpen)
{
this.Window.IsOpen = isWindowOpen;
}
var context = ImGui.GetCurrentContext();
if (!context.IsNull)
{
ImGuiP.GetCurrentWindow().InheritNoInputs = this.Window.IsClickthrough;
}
// Not supported yet on non-main viewports
if ((this.Window.IsPinned || this.Window.IsClickthrough || this.internalAlpha.HasValue) &&
ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
{
this.internalAlpha = null;
this.Window.IsPinned = false;
this.Window.IsClickthrough = false;
this.presetDirty = true;
}
// Draw the actual window contents
if (this.hasError)
{
this.DrawErrorMessage();
}
else
{
// Draw the actual window contents
try
{
this.Window.Draw();
}
catch (Exception ex)
{
Log.Error(ex, "Error during Draw(): {WindowName}", this.Window.WindowName);
this.hasError = true;
this.lastError = ex;
}
}
}
const string additionsPopupName = "WindowSystemContextActions";
var flagsApplicableForTitleBarIcons = !flags.HasFlag(ImGuiWindowFlags.NoDecoration) &&
!flags.HasFlag(ImGuiWindowFlags.NoTitleBar);
var showAdditions = (this.Window.AllowPinning || this.Window.AllowClickthrough) &&
internalDrawFlags.HasFlag(WindowDrawFlags.UseAdditionalOptions) &&
flagsApplicableForTitleBarIcons;
var printWindow = false;
if (showAdditions)
{
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f);
if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove))
{
var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport();
if (!isAvailable)
ImGui.BeginDisabled();
if (this.Window.IsClickthrough)
ImGui.BeginDisabled();
if (this.Window.AllowPinning)
{
var showAsPinned = this.Window.IsPinned || this.Window.IsClickthrough;
if (ImGui.Checkbox(Loc.Localize("WindowSystemContextActionPin", "Pin Window"), ref showAsPinned))
{
this.Window.IsPinned = showAsPinned;
this.presetDirty = true;
}
ImGuiComponents.HelpMarker(
Loc.Localize("WindowSystemContextActionPinHint", "Pinned windows will not move or resize when you click and drag them, nor will they close when escape is pressed."));
}
if (this.Window.IsClickthrough)
ImGui.EndDisabled();
if (this.Window.AllowClickthrough)
{
var isClickthrough = this.Window.IsClickthrough;
if (ImGui.Checkbox(
Loc.Localize("WindowSystemContextActionClickthrough", "Make clickthrough"),
ref isClickthrough))
{
this.Window.IsClickthrough = isClickthrough;
this.presetDirty = true;
}
ImGuiComponents.HelpMarker(
Loc.Localize("WindowSystemContextActionClickthroughHint", "Clickthrough windows will not receive mouse input, move or resize. They are completely inert."));
}
var alpha = (this.internalAlpha ?? ImGui.GetStyle().Alpha) * 100f;
if (ImGui.SliderFloat(Loc.Localize("WindowSystemContextActionAlpha", "Opacity"), ref alpha, 20f,
100f))
{
this.internalAlpha = Math.Clamp(alpha / 100f, 0.2f, 1f);
this.presetDirty = true;
}
ImGui.SameLine();
if (ImGui.Button(Loc.Localize("WindowSystemContextActionReset", "Reset")))
{
this.internalAlpha = null;
this.presetDirty = true;
}
if (isAvailable)
{
ImGui.TextColored(ImGuiColors.DalamudGrey,
Loc.Localize("WindowSystemContextActionClickthroughDisclaimer",
"Open this menu again by clicking the three dashes to disable clickthrough."));
}
else
{
ImGui.TextColored(ImGuiColors.DalamudGrey,
Loc.Localize("WindowSystemContextActionViewportDisclaimer",
"These features are only available if this window is inside the game window."));
}
if (!isAvailable)
ImGui.EndDisabled();
if (ImGui.Button(Loc.Localize("WindowSystemContextActionPrintWindow", "Print window")))
printWindow = true;
ImGui.EndPopup();
}
ImGui.PopStyleVar();
}
unsafe
{
var window = ImGuiP.GetCurrentWindow();
ImRect outRect;
ImGuiP.TitleBarRect(&outRect, window);
var additionsButton = new TitleBarButton
{
Icon = FontAwesomeIcon.Bars,
IconOffset = new Vector2(2.5f, 1),
Click = _ =>
{
this.Window.IsClickthrough = false;
this.presetDirty = false;
ImGui.OpenPopup(additionsPopupName);
},
Priority = int.MinValue,
AvailableClickthrough = true,
};
if (flagsApplicableForTitleBarIcons)
{
this.DrawTitleBarButtons(window, flags, outRect,
showAdditions
? this.Window.TitleBarButtons.Append(additionsButton)
: this.Window.TitleBarButtons);
}
}
if (wasFocused)
{
ImGui.PopStyleColor();
}
this.Window.IsFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
if (internalDrawFlags.HasFlag(WindowDrawFlags.UseFocusManagement) && !this.Window.IsPinned)
{
var escapeDown = Service<KeyState>.Get()[VirtualKey.ESCAPE];
if (escapeDown && this.Window.IsFocused && !wasEscPressedLastFrame && this.Window.RespectCloseHotkey)
{
this.Window.IsOpen = false;
wasEscPressedLastFrame = true;
}
else if (!escapeDown && wasEscPressedLastFrame)
{
wasEscPressedLastFrame = false;
}
}
this.fadeOutSize = ImGui.GetWindowSize();
this.fadeOutOrigin = ImGui.GetWindowPos();
var isCollapsed = ImGui.IsWindowCollapsed();
var isDocked = ImGui.IsWindowDocked();
ImGui.End();
if (this.pushedFadeInAlpha)
{
ImGui.PopStyleVar();
this.pushedFadeInAlpha = false;
}
// TODO: No fade-out if the window is collapsed. We could do this if we knew the "FullSize" of the window
// from the internal ImGuiWindow, but I don't want to mess with that here for now. We can do this a lot
// easier with the new bindings.
// TODO: No fade-out if docking is enabled and the window is docked, since this makes them "unsnap".
// Ideally we should get rid of this "fake window" thing and just insert a new drawlist at the correct spot.
if (!this.Window.IsOpen && this.fadeOutTexture == null && doFades && !isCollapsed && !isDocked)
{
this.fadeOutTexture = Service<TextureManager>.Get().CreateDrawListTexture(
"WindowFadeOutTexture");
Log.Verbose("Attempting to fade out {WindowName}", this.Window.WindowName);
this.fadeOutTexture.ResizeAndDrawWindow(this.Window.WindowName, Vector2.One);
this.fadeOutTimer = FadeInOutTime;
}
if (printWindow)
{
var tex = Service<TextureManager>.Get().CreateDrawListTexture(
Loc.Localize("WindowSystemContextActionPrintWindow", "Print window"));
tex.ResizeAndDrawWindow(this.Window.WindowName, Vector2.One);
_ = Service<DevTextureSaveMenu>.Get().ShowTextureSaveMenuAsync(
this.Window.WindowName,
this.Window.WindowName,
Task.FromResult<IDalamudTextureWrap>(tex));
}
if (this.didPushInternalAlpha)
{
ImGui.PopStyleVar();
this.didPushInternalAlpha = false;
}
this.Window.PostDraw();
this.PostHandlePreset(persistence);
if (hasNamespace)
ImGui.PopID();
}
private unsafe void ApplyConditionals()
{
if (this.Window.Position.HasValue)
{
var pos = this.Window.Position.Value;
if (this.Window.ForceMainWindow)
pos += ImGuiHelpers.MainViewport.Pos;
ImGui.SetNextWindowPos(pos, this.Window.PositionCondition);
}
if (this.Window.Size.HasValue)
{
ImGui.SetNextWindowSize(this.Window.Size.Value * ImGuiHelpers.GlobalScale, this.Window.SizeCondition);
}
if (this.Window.Collapsed.HasValue)
{
ImGui.SetNextWindowCollapsed(this.Window.Collapsed.Value, this.Window.CollapsedCondition);
}
if (this.Window.SizeConstraints.HasValue)
{
var (min, max) = this.GetValidatedConstraints(this.Window.SizeConstraints.Value);
ImGui.SetNextWindowSizeConstraints(
min * ImGuiHelpers.GlobalScale,
max * ImGuiHelpers.GlobalScale);
}
var maxBgAlpha = this.internalAlpha ?? this.Window.BgAlpha;
var fadeInAlpha = this.fadeInTimer / FadeInOutTime;
if (fadeInAlpha < 1f)
{
maxBgAlpha = maxBgAlpha.HasValue ?
Math.Clamp(maxBgAlpha.Value * fadeInAlpha, 0f, 1f) :
(*ImGui.GetStyleColorVec4(ImGuiCol.WindowBg)).W * fadeInAlpha;
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * fadeInAlpha);
this.pushedFadeInAlpha = true;
}
if (maxBgAlpha.HasValue)
{
ImGui.SetNextWindowBgAlpha(maxBgAlpha.Value);
}
}
private (Vector2 Min, Vector2 Max) GetValidatedConstraints(WindowSizeConstraints constraints)
{
var min = constraints.MinimumSize;
var max = constraints.MaximumSize;
// If max < min, treat as "no constraint" (float.MaxValue)
if (max.X < min.X || max.Y < min.Y)
max = new Vector2(float.MaxValue);
return (min, max);
}
private void PreHandlePreset(WindowSystemPersistence? persistence)
{
if (persistence == null || this.hasInitializedFromPreset)
return;
var id = ImGui.GetID(this.Window.WindowName);
this.presetWindow = persistence.GetWindow(id);
this.hasInitializedFromPreset = true;
// Fresh preset - don't apply anything
if (this.presetWindow == null)
{
this.presetWindow = new PresetModel.PresetWindow();
this.presetDirty = true;
return;
}
this.Window.IsPinned = this.presetWindow.IsPinned;
this.Window.IsClickthrough = this.presetWindow.IsClickThrough;
this.internalAlpha = this.presetWindow.Alpha;
}
private void PostHandlePreset(WindowSystemPersistence? persistence)
{
if (persistence == null)
return;
Debug.Assert(this.presetWindow != null, "this.presetWindow != null");
if (this.presetDirty)
{
this.presetWindow.IsPinned = this.Window.IsPinned;
this.presetWindow.IsClickThrough = this.Window.IsClickthrough;
this.presetWindow.Alpha = this.internalAlpha;
var id = ImGui.GetID(this.Window.WindowName);
persistence.SaveWindow(id, this.presetWindow!);
this.presetDirty = false;
Log.Verbose("Saved preset for {WindowName}", this.Window.WindowName);
}
}
private unsafe void DrawTitleBarButtons(ImGuiWindowPtr window, ImGuiWindowFlags flags, ImRect titleBarRect, IEnumerable<TitleBarButton> buttons)
{
ImGui.PushClipRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize(), false);
var style = ImGui.GetStyle();
var fontSize = ImGui.GetFontSize();
var drawList = ImGui.GetWindowDrawList();
var padR = 0f;
var buttonSize = ImGui.GetFontSize();
var numNativeButtons = 0;
if (this.CanShowCloseButton)
numNativeButtons++;
if (!flags.HasFlag(ImGuiWindowFlags.NoCollapse) && style.WindowMenuButtonPosition == ImGuiDir.Right)
numNativeButtons++;
// If there are no native buttons, pad from the right to make some space
if (numNativeButtons == 0)
padR += style.FramePadding.X;
// Pad to the left, to get out of the way of the native buttons
padR += numNativeButtons * (buttonSize + style.ItemInnerSpacing.X);
Vector2 GetCenter(ImRect rect) => new((rect.Min.X + rect.Max.X) * 0.5f, (rect.Min.Y + rect.Max.Y) * 0.5f);
var numButtons = 0;
bool DrawButton(TitleBarButton button, Vector2 pos)
{
var id = ImGui.GetID($"###CustomTbButton{numButtons}");
numButtons++;
var max = pos + new Vector2(fontSize, fontSize);
ImRect bb = new(pos, max);
var isClipped = !ImGuiP.ItemAdd(bb, id, null, 0);
bool hovered, held;
var pressed = false;
if (this.Window.IsClickthrough)
{
hovered = false;
held = false;
// ButtonBehavior does not function if the window is clickthrough, so we have to do it ourselves
if (ImGui.IsMouseHoveringRect(pos, max))
{
hovered = true;
// We can't use ImGui native functions here, because they don't work with clickthrough
if ((global::Windows.Win32.PInvoke.GetKeyState((int)VirtualKey.LBUTTON) & 0x8000) != 0)
{
held = true;
pressed = true;
}
}
}
else
{
pressed = ImGuiP.ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags.None);
}
if (isClipped)
return pressed;
// Render
var bgCol = ImGui.GetColorU32((held && hovered) ? ImGuiCol.ButtonActive : hovered ? ImGuiCol.ButtonHovered : ImGuiCol.Button);
var textCol = ImGui.GetColorU32(ImGuiCol.Text);
if (hovered || held)
drawList.AddCircleFilled(GetCenter(bb) + new Vector2(0.0f, -0.5f), (fontSize * 0.5f) + 1.0f, bgCol);
var offset = button.IconOffset * ImGuiHelpers.GlobalScale;
drawList.AddText(InterfaceManager.IconFont, (float)(fontSize * 0.8), new Vector2(bb.Min.X + offset.X, bb.Min.Y + offset.Y), textCol, button.Icon.ToIconString());
if (hovered)
button.ShowTooltip?.Invoke();
// Switch to moving the window after mouse is moved beyond the initial drag threshold
if (ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left) && !this.Window.IsClickthrough)
ImGuiP.StartMouseMovingWindow(window);
return pressed;
}
foreach (var button in buttons.OrderBy(x => x.Priority))
{
if (this.Window.IsClickthrough && !button.AvailableClickthrough)
return;
Vector2 position = new(titleBarRect.Max.X - padR - buttonSize, titleBarRect.Min.Y + style.FramePadding.Y);
padR += buttonSize + style.ItemInnerSpacing.X;
if (DrawButton(button, position))
button.Click?.Invoke(ImGuiMouseButton.Left);
}
ImGui.PopClipRect();
}
private void DrawFakeFadeOutWindow()
{
// Draw a fake window to fade out, so that the fade out texture stays in the right place in the
// focus order
ImGui.SetNextWindowPos(this.fadeOutOrigin);
ImGui.SetNextWindowSize(this.fadeOutSize);
using var style = ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, Vector2.Zero);
style.Push(ImGuiStyleVar.WindowBorderSize, 0);
style.Push(ImGuiStyleVar.FrameBorderSize, 0);
const ImGuiWindowFlags flags = ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav |
ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs |
ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoBackground;
if (ImGui.Begin(this.Window.WindowName, flags))
{
var dl = ImGui.GetWindowDrawList();
dl.AddImage(
this.fadeOutTexture!.Handle,
this.fadeOutOrigin,
this.fadeOutOrigin + this.fadeOutSize,
Vector2.Zero,
Vector2.One,
ImGui.ColorConvertFloat4ToU32(new(1f, 1f, 1f, Math.Clamp(this.fadeOutTimer / FadeInOutTime, 0f, 1f))));
}
ImGui.End();
}
private void DrawErrorMessage()
{
// TODO: Once window systems are services, offer to reload the plugin
ImGui.TextColoredWrapped(ImGuiColors.DalamudRed,Loc.Localize("WindowSystemErrorOccurred", "An error occurred while rendering this window. Please contact the developer for details."));
ImGuiHelpers.ScaledDummy(5);
if (ImGui.Button(Loc.Localize("WindowSystemErrorRecoverButton", "Attempt to retry")))
{
this.hasError = false;
this.lastError = null;
}
ImGui.SameLine();
if (ImGui.Button(Loc.Localize("WindowSystemErrorClose", "Close Window")))
{
this.Window.IsOpen = false;
this.hasError = false;
this.lastError = null;
}
ImGuiHelpers.ScaledDummy(10);
if (this.lastError != null)
{
using var child = ImRaii.Child("##ErrorDetails", new Vector2(0, 200 * ImGuiHelpers.GlobalScale), true);
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
{
ImGui.TextWrapped(Loc.Localize("WindowSystemErrorDetails", "Error Details:"));
ImGui.Separator();
ImGui.TextWrapped(this.lastError.ToString());
}
var childWindowSize = ImGui.GetWindowSize();
var copyText = Loc.Localize("WindowSystemErrorCopy", "Copy");
var buttonWidth = ImGuiComponents.GetIconButtonWithTextWidth(FontAwesomeIcon.Copy, copyText);
ImGui.SetCursorPos(new Vector2(childWindowSize.X - buttonWidth - ImGui.GetStyle().FramePadding.X,
ImGui.GetStyle().FramePadding.Y));
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Copy, copyText))
{
ImGui.SetClipboardText(this.lastError.ToString());
}
}
}
}

View file

@ -0,0 +1,26 @@
using System.Numerics;
namespace Dalamud.Interface.Windowing;
/// <summary>
/// Structure detailing the size constraints of a window.
/// </summary>
public struct WindowSizeConstraints
{
/// <summary>
/// Initializes a new instance of the <see cref="WindowSizeConstraints"/> struct.
/// </summary>
public WindowSizeConstraints()
{
}
/// <summary>
/// Gets or sets the minimum size of the window.
/// </summary>
public Vector2 MinimumSize { get; set; }
/// <summary>
/// Gets or sets the maximum size of the window.
/// </summary>
public Vector2 MaximumSize { get; set; }
}

View file

@ -8,14 +8,12 @@ using Serilog;
namespace Dalamud.Interface.Windowing;
/// <summary>
/// Class running a WindowSystem using <see cref="Window"/> implementations to simplify ImGui windowing.
/// </summary>
public class WindowSystem
/// <inheritdoc/>
public class WindowSystem : IWindowSystem
{
private static DateTimeOffset lastAnyFocus;
private readonly List<Window> windows = new();
private readonly List<WindowHost> windows = new();
private string lastFocusedWindowName = string.Empty;
@ -29,7 +27,7 @@ public class WindowSystem
}
/// <summary>
/// Gets a value indicating whether any <see cref="WindowSystem"/> contains any <see cref="Window"/>
/// Gets a value indicating whether any <see cref="WindowSystem"/> contains any <see cref="IWindow"/>
/// that has focus and is not marked to be excluded from consideration.
/// </summary>
public static bool HasAnyWindowSystemFocus { get; internal set; } = false;
@ -44,20 +42,13 @@ public class WindowSystem
/// </summary>
public static TimeSpan TimeSinceLastAnyFocus => DateTimeOffset.Now - lastAnyFocus;
/// <summary>
/// Gets a read-only list of all <see cref="Window"/>s in this <see cref="WindowSystem"/>.
/// </summary>
public IReadOnlyList<Window> Windows => this.windows;
/// <inheritdoc/>
public IReadOnlyList<IWindow> Windows => this.windows.Select(c => c.Window).ToList();
/// <summary>
/// Gets a value indicating whether any window in this <see cref="WindowSystem"/> has focus and is
/// not marked to be excluded from consideration.
/// </summary>
/// <inheritdoc/>
public bool HasAnyFocus { get; private set; }
/// <summary>
/// Gets or sets the name/ID-space of this <see cref="WindowSystem"/>.
/// </summary>
/// <inheritdoc/>
public string? Namespace { get; set; }
/// <summary>
@ -66,42 +57,28 @@ public class WindowSystem
/// </summary>
internal static bool ShouldInhibitAtkCloseEvents { get; set; }
/// <summary>
/// Add a window to this <see cref="WindowSystem"/>.
/// The window system doesn't own your window, it just renders it
/// You need to store a reference to it to use it later.
/// </summary>
/// <param name="window">The window to add.</param>
public void AddWindow(Window window)
/// <inheritdoc/>
public void AddWindow(IWindow window)
{
if (this.windows.Any(x => x.WindowName == window.WindowName))
if (this.windows.Any(x => x.Window.WindowName == window.WindowName))
throw new ArgumentException("A window with this name/ID already exists.");
this.windows.Add(window);
this.windows.Add(new WindowHost(window));
}
/// <summary>
/// Remove a window from this <see cref="WindowSystem"/>.
/// Will not dispose your window, if it is disposable.
/// </summary>
/// <param name="window">The window to remove.</param>
public void RemoveWindow(Window window)
/// <inheritdoc/>
public void RemoveWindow(IWindow window)
{
if (!this.windows.Contains(window))
if (this.windows.All(c => c.Window != window))
throw new ArgumentException("This window is not registered on this WindowSystem.");
this.windows.Remove(window);
this.windows.RemoveAll(c => c.Window == window);
}
/// <summary>
/// Remove all windows from this <see cref="WindowSystem"/>.
/// Will not dispose your windows, if they are disposable.
/// </summary>
/// <inheritdoc/>
public void RemoveAllWindows() => this.windows.Clear();
/// <summary>
/// Draw all registered windows using ImGui.
/// </summary>
/// <inheritdoc/>
public void Draw()
{
var hasNamespace = !string.IsNullOrEmpty(this.Namespace);
@ -113,19 +90,19 @@ public class WindowSystem
var config = Service<DalamudConfiguration>.GetNullable();
var persistence = Service<WindowSystemPersistence>.GetNullable();
var flags = Window.WindowDrawFlags.None;
var flags = WindowHost.WindowDrawFlags.None;
if (config?.EnablePluginUISoundEffects ?? false)
flags |= Window.WindowDrawFlags.UseSoundEffects;
flags |= WindowHost.WindowDrawFlags.UseSoundEffects;
if (config?.EnablePluginUiAdditionalOptions ?? false)
flags |= Window.WindowDrawFlags.UseAdditionalOptions;
flags |= WindowHost.WindowDrawFlags.UseAdditionalOptions;
if (config?.IsFocusManagementEnabled ?? false)
flags |= Window.WindowDrawFlags.UseFocusManagement;
flags |= WindowHost.WindowDrawFlags.UseFocusManagement;
if (config?.ReduceMotions ?? false)
flags |= Window.WindowDrawFlags.IsReducedMotion;
flags |= WindowHost.WindowDrawFlags.IsReducedMotion;
// Shallow clone the list of windows so that we can edit it without modifying it while the loop is iterating
foreach (var window in this.windows.ToArray())
@ -136,15 +113,15 @@ public class WindowSystem
window.DrawInternal(flags, persistence);
}
var focusedWindow = this.windows.FirstOrDefault(window => window.IsFocused);
var focusedWindow = this.windows.FirstOrDefault(window => window.Window.IsFocused);
this.HasAnyFocus = focusedWindow != default;
if (this.HasAnyFocus)
{
if (this.lastFocusedWindowName != focusedWindow.WindowName)
if (this.lastFocusedWindowName != focusedWindow.Window.WindowName)
{
Log.Verbose($"WindowSystem \"{this.Namespace}\" Window \"{focusedWindow.WindowName}\" has focus now");
this.lastFocusedWindowName = focusedWindow.WindowName;
Log.Verbose($"WindowSystem \"{this.Namespace}\" Window \"{focusedWindow.Window.WindowName}\" has focus now");
this.lastFocusedWindowName = focusedWindow.Window.WindowName;
}
HasAnyWindowSystemFocus = true;
@ -161,10 +138,10 @@ public class WindowSystem
}
}
ShouldInhibitAtkCloseEvents |= this.windows.Any(w => w.IsFocused &&
w.RespectCloseHotkey &&
!w.IsPinned &&
!w.IsClickthrough);
ShouldInhibitAtkCloseEvents |= this.windows.Any(w => w.Window.IsFocused &&
w.Window.RespectCloseHotkey &&
!w.Window.IsPinned &&
!w.Window.IsClickthrough);
if (hasNamespace)
ImGui.PopID();