diff --git a/ImGui.App/ImGui.App.csproj b/ImGui.App/ImGui.App.csproj
index 185c956..ea90fac 100644
--- a/ImGui.App/ImGui.App.csproj
+++ b/ImGui.App/ImGui.App.csproj
@@ -48,6 +48,7 @@
+
diff --git a/ImGui.App/ImGuiApp.cs b/ImGui.App/ImGuiApp.cs
index 03a397d..372e093 100644
--- a/ImGui.App/ImGuiApp.cs
+++ b/ImGui.App/ImGuiApp.cs
@@ -105,6 +105,16 @@ public static ImGuiAppWindowState WindowState
internal static readonly PidFrameLimiter frameLimiter = new();
internal static double previousTargetFrameTimeMs = 1000.0 / 30.0;
+ /// Drives the native window styling that backs overlay mode (see ).
+ internal static readonly OverlayChrome overlayChrome = new();
+
+ ///
+ /// Gets a value indicating whether the window is currently in overlay mode (borderless,
+ /// always-on-top, and translucent). Toggled via and
+ /// .
+ ///
+ public static bool IsOverlayActive => overlayChrome.IsActive;
+
///
/// Updates the last input time to the current time. Called by the input system when user input is detected.
///
@@ -372,6 +382,43 @@ public static void Hide()
ApplyNativeVisibility(false);
}
+ ///
+ /// Switches the window into overlay mode: borderless, always-on-top, and whole-window
+ /// translucent, with optional click-through. Safe to call every frame to keep the opacity
+ /// and click-through in sync with live settings; the underlying window styles are only
+ /// touched when something actually changes.
+ ///
+ /// Overlay styling is implemented on Windows. On other platforms
+ /// still reflects the request (so overlay frame-rate throttling via
+ /// still applies) but the window is not
+ /// restyled. Call from the thread that owns the render window (e.g. from ).
+ ///
+ ///
+ /// Whole-window opacity, clamped to 0.2 (faint) – 1.0 (opaque).
+ /// When true, mouse input passes through to whatever is behind the overlay.
+ public static void EnableOverlay(float opacity = 1.0f, bool clickThrough = false) =>
+ overlayChrome.Enable(TryGetWindowHandle(), opacity, clickThrough);
+
+ ///
+ /// Locks the overlay to a corner of the active monitor's work area at the given offset and
+ /// size. No-op unless overlay mode is active (see ). Re-applies
+ /// only when the geometry changes, so it is cheap to call every frame. Windows-only;
+ /// elsewhere the consumer is responsible for positioning.
+ ///
+ /// Which work-area corner to anchor the overlay to.
+ /// Horizontal inset (px) from the anchored corner.
+ /// Vertical inset (px) from the anchored corner.
+ /// Overlay width in pixels (minimum 200).
+ /// Overlay height in pixels (minimum 140).
+ public static void SetOverlayGeometry(OverlayCorner corner, int offsetX, int offsetY, int width, int height) =>
+ overlayChrome.SetGeometry(TryGetWindowHandle(), corner, offsetX, offsetY, width, height);
+
+ ///
+ /// Leaves overlay mode and restores the decorated, non-topmost, opaque window. Safe to call
+ /// repeatedly (including when overlay mode was never entered).
+ ///
+ public static void DisableOverlay() => overlayChrome.Disable();
+
internal static void SetupWindowResizeHandler(ImGuiAppConfig config)
{
window!.FramebufferResize += s =>
@@ -419,6 +466,18 @@ internal static void UpdateWindowPerformance()
return;
}
+ // Overlay mode runs at its own dedicated rate. An always-on-top overlay is usually
+ // unfocused and may report as "not visible" to some backends, which would otherwise
+ // throttle it to a crawl — yet it typically shows live, continuously-updating data.
+ // So while overlay mode is active we bypass the focus/idle/visibility reductions and
+ // use OverlayFps directly.
+ if (IsOverlayActive)
+ {
+ IsIdle = false;
+ targetFrameTimeMs = settings.OverlayFps > 0 ? (1000.0 / settings.OverlayFps) : 10000.0;
+ return;
+ }
+
// Update idle state if idle detection is enabled
if (settings.EnableIdleDetection)
{
@@ -1733,6 +1792,7 @@ internal static void Reset()
ScaleFactor = 1;
GlobalScale = 1.0f;
Textures.Clear();
+ overlayChrome.ResetState();
Config = new();
}
diff --git a/ImGui.App/ImGuiAppPerformanceSettings.cs b/ImGui.App/ImGuiAppPerformanceSettings.cs
index ec0918c..6265be1 100644
--- a/ImGui.App/ImGuiAppPerformanceSettings.cs
+++ b/ImGui.App/ImGuiAppPerformanceSettings.cs
@@ -35,6 +35,15 @@ public class ImGuiAppPerformanceSettings
///
public double NotVisibleFps { get; init; } = 2.0;
+ ///
+ /// Gets or sets the target frame rate (FPS) used while the window is in overlay mode
+ /// (entered via ImGuiApp.EnableOverlay). Because an always-on-top overlay is usually
+ /// unfocused — and may even report as not visible — the normal focus/idle/visibility
+ /// throttling would make it sluggish despite showing live data. While overlay mode is
+ /// active this rate is used instead, bypassing those reductions. Defaults to 30 FPS.
+ ///
+ public double OverlayFps { get; init; } = 30.0;
+
///
/// Gets or sets a value indicating whether idle detection is enabled.
/// When true, the application will detect when there's no user input and reduce frame rate further.
diff --git a/ImGui.App/NativeMethods.cs b/ImGui.App/NativeMethods.cs
index f191d52..cd90b46 100644
--- a/ImGui.App/NativeMethods.cs
+++ b/ImGui.App/NativeMethods.cs
@@ -32,6 +32,54 @@ internal static partial class NativeMethods
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
internal static partial nint SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong);
+ /// Reads a window's style/extended-style word (used to enter and restore overlay mode).
+ [LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ internal static partial nint GetWindowLongPtr(nint hWnd, int nIndex);
+
+ /// Sets the per-pixel/whole-window alpha for a layered window (overlay translucency).
+ [LibraryImport("user32.dll")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool SetLayeredWindowAttributes(nint hWnd, uint crKey, byte bAlpha, uint dwFlags);
+
+ /// Changes a window's size, position, and Z-order (used to make the overlay topmost and corner-locked).
+ [LibraryImport("user32.dll")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool SetWindowPos(nint hWnd, nint hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
+
+ /// Returns the monitor that contains (or is nearest to) the given window.
+ [LibraryImport("user32.dll")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ internal static partial nint MonitorFromWindow(nint hWnd, uint dwFlags);
+
+ /// Retrieves monitor geometry, including the work area excluding the taskbar.
+ [LibraryImport("user32.dll", EntryPoint = "GetMonitorInfoW")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool GetMonitorInfo(nint hMonitor, ref MONITORINFO lpmi);
+
+ /// A rectangle defined by its edges, matching the Win32 RECT structure.
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct RECT
+ {
+ public int Left;
+ public int Top;
+ public int Right;
+ public int Bottom;
+ }
+
+ /// Monitor geometry returned by , matching the Win32 MONITORINFO structure.
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct MONITORINFO
+ {
+ public int cbSize;
+ public RECT rcMonitor;
+ public RECT rcWork;
+ public uint dwFlags;
+ }
+
/// Forwards a message to the previously installed window procedure.
[LibraryImport("user32.dll", EntryPoint = "CallWindowProcW")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
diff --git a/ImGui.App/OverlayWindow.cs b/ImGui.App/OverlayWindow.cs
new file mode 100644
index 0000000..28b2836
--- /dev/null
+++ b/ImGui.App/OverlayWindow.cs
@@ -0,0 +1,260 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGui.App;
+
+using System.Runtime.Versioning;
+
+///
+/// Identifies which corner of the active monitor's work area an overlay window locks to
+/// when its geometry is managed via .
+///
+public enum OverlayCorner
+{
+ /// Top-left corner of the work area.
+ TopLeft,
+
+ /// Top-right corner of the work area.
+ TopRight,
+
+ /// Bottom-left corner of the work area.
+ BottomLeft,
+
+ /// Bottom-right corner of the work area.
+ BottomRight,
+}
+
+///
+/// Turns the application's native window into an "overlay": borderless, always-on-top, and
+/// whole-window translucent, with optional click-through and corner-locked geometry. The
+/// original window styles are cached so the decorated window can be restored exactly.
+///
+/// Native styling is implemented on Windows; on other platforms the logical
+/// state is still tracked (so frame-rate throttling and consumer
+/// logic behave consistently) but no window styles are changed. All calls are best-effort
+/// and no-op when the native handle is unavailable (e.g. before the window is created) or
+/// when the platform does not export the required APIs.
+///
+/// Per-frame calls are cheap: styles are only re-applied when something actually changed.
+/// Must be driven from the thread that owns the render window.
+///
+internal sealed class OverlayChrome
+{
+ private const int GWL_STYLE = -16;
+ private const int GWL_EXSTYLE = -20;
+
+ private const int WS_CAPTION = 0x00C00000;
+ private const int WS_THICKFRAME = 0x00040000;
+ private const int WS_MINIMIZEBOX = 0x00020000;
+ private const int WS_MAXIMIZEBOX = 0x00010000;
+ private const int WS_SYSMENU = 0x00080000;
+
+ private const int WS_EX_LAYERED = 0x00080000;
+ private const int WS_EX_TRANSPARENT = 0x00000020;
+
+ private const uint LWA_ALPHA = 0x2;
+
+ private static readonly nint HWND_TOPMOST = new(-1);
+ private static readonly nint HWND_NOTOPMOST = new(-2);
+
+ private const uint SWP_NOSIZE = 0x0001;
+ private const uint SWP_NOMOVE = 0x0002;
+ private const uint SWP_NOACTIVATE = 0x0010;
+ private const uint SWP_FRAMECHANGED = 0x0020;
+
+ private const uint MONITOR_DEFAULTTONEAREST = 2;
+
+ private nint hwnd;
+ private bool styled;
+ private nint originalStyle;
+ private nint originalExStyle;
+
+ // Set once if the platform doesn't export the window-style APIs (e.g. 32-bit Windows),
+ // so we stop retrying the native calls every frame.
+ private bool nativeUnavailable;
+
+ // Last-applied values, so per-frame calls only touch Win32 on a real change.
+ private bool lastClickThrough;
+ private byte lastAlpha;
+ private (OverlayCorner Corner, int OffsetX, int OffsetY, int Width, int Height) lastGeometry;
+ private bool geometryApplied;
+
+ ///
+ /// Gets a value indicating whether overlay mode is logically active (i.e.
+ /// has been called more recently than ). Tracked on all platforms,
+ /// independently of whether native styling could be applied.
+ ///
+ public bool IsActive { get; private set; }
+
+ ///
+ /// Switches the window into overlay mode (borderless, topmost, layered) and keeps its
+ /// click-through and opacity in sync. Safe to call every frame.
+ ///
+ /// The native window handle, or zero if unavailable.
+ /// Whole-window opacity in the range 0.2–1.0.
+ /// When true, mouse input passes through to whatever is behind the overlay.
+ public void Enable(nint windowHandle, float opacity, bool clickThrough)
+ {
+ IsActive = true;
+
+ if (!OperatingSystem.IsWindows() || windowHandle == 0 || nativeUnavailable)
+ {
+ return;
+ }
+
+ ApplyWindowsStyles(windowHandle, opacity, clickThrough);
+ }
+
+ ///
+ /// Locks the overlay to the given corner of its monitor's work area at the given offset
+ /// and size. Re-applies only when something changed (or on the first call after entering
+ /// overlay mode). No-op unless overlay mode is active and native styling has been applied.
+ ///
+ public void SetGeometry(nint windowHandle, OverlayCorner corner, int offsetX, int offsetY, int width, int height)
+ {
+ if (!IsActive || !styled || !OperatingSystem.IsWindows() || windowHandle == 0)
+ {
+ return;
+ }
+
+ ApplyWindowsGeometry(windowHandle, corner, offsetX, offsetY, width, height);
+ }
+
+ /// Restores the original decorated, non-topmost, opaque window. Safe to call repeatedly.
+ public void Disable()
+ {
+ if (styled && OperatingSystem.IsWindows())
+ {
+ RestoreWindowsStyles();
+ }
+
+ IsActive = false;
+ }
+
+ /// Clears all logical and cached state without touching the native window (test/reset hook).
+ internal void ResetState()
+ {
+ IsActive = false;
+ styled = false;
+ geometryApplied = false;
+ nativeUnavailable = false;
+ hwnd = 0;
+ originalStyle = 0;
+ originalExStyle = 0;
+ lastAlpha = 0;
+ lastClickThrough = false;
+ lastGeometry = default;
+ }
+
+ [SupportedOSPlatform("windows")]
+ private void ApplyWindowsStyles(nint windowHandle, float opacity, bool clickThrough)
+ {
+ byte alpha = (byte)Math.Clamp(opacity * 255f, 51f, 255f);
+
+ try
+ {
+ if (!styled || hwnd != windowHandle)
+ {
+ hwnd = windowHandle;
+ originalStyle = NativeMethods.GetWindowLongPtr(windowHandle, GWL_STYLE);
+ originalExStyle = NativeMethods.GetWindowLongPtr(windowHandle, GWL_EXSTYLE);
+
+ nint style = originalStyle & ~(nint)(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU);
+ _ = NativeMethods.SetWindowLongPtr(windowHandle, GWL_STYLE, style);
+ _ = NativeMethods.SetWindowLongPtr(windowHandle, GWL_EXSTYLE, originalExStyle | WS_EX_LAYERED);
+ _ = NativeMethods.SetWindowPos(windowHandle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_FRAMECHANGED);
+
+ styled = true;
+ // Force the click-through and alpha branches below to run for the fresh window.
+ lastClickThrough = !clickThrough;
+ lastAlpha = 0;
+ }
+
+ if (clickThrough != lastClickThrough)
+ {
+ nint ex = NativeMethods.GetWindowLongPtr(windowHandle, GWL_EXSTYLE) | WS_EX_LAYERED;
+ ex = clickThrough ? (ex | WS_EX_TRANSPARENT) : (ex & ~(nint)WS_EX_TRANSPARENT);
+ _ = NativeMethods.SetWindowLongPtr(windowHandle, GWL_EXSTYLE, ex);
+ lastClickThrough = clickThrough;
+ }
+
+ if (alpha != lastAlpha)
+ {
+ _ = NativeMethods.SetLayeredWindowAttributes(windowHandle, 0, alpha, LWA_ALPHA);
+ lastAlpha = alpha;
+ }
+ }
+ catch (Exception ex) when (ex is EntryPointNotFoundException or DllNotFoundException)
+ {
+ // Older/32-bit Windows may not export the *Ptr window-style APIs; degrade to a
+ // normal decorated window rather than throwing every frame.
+ nativeUnavailable = true;
+ styled = false;
+ DebugLogger.Log($"OverlayChrome: native styling unavailable ({ex.Message}); overlay will render as a normal window.");
+ }
+ }
+
+ [SupportedOSPlatform("windows")]
+ private void ApplyWindowsGeometry(nint windowHandle, OverlayCorner corner, int offsetX, int offsetY, int width, int height)
+ {
+ width = Math.Max(200, width);
+ height = Math.Max(140, height);
+
+ (OverlayCorner corner, int offsetX, int offsetY, int width, int height) geometry = (corner, offsetX, offsetY, width, height);
+ if (geometryApplied && geometry == lastGeometry)
+ {
+ return;
+ }
+
+ if (!TryGetWorkArea(windowHandle, out NativeMethods.RECT work))
+ {
+ return;
+ }
+
+ bool right = corner is OverlayCorner.TopRight or OverlayCorner.BottomRight;
+ bool bottom = corner is OverlayCorner.BottomLeft or OverlayCorner.BottomRight;
+
+ int x = right ? work.Right - width - offsetX : work.Left + offsetX;
+ int y = bottom ? work.Bottom - height - offsetY : work.Top + offsetY;
+
+ // The explicit resize also forces the renderer to refresh its framebuffer after the
+ // title bar was removed — without it the top of the content can be clipped.
+ _ = NativeMethods.SetWindowPos(windowHandle, HWND_TOPMOST, x, y, width, height, SWP_NOACTIVATE);
+ lastGeometry = geometry;
+ geometryApplied = true;
+ }
+
+ [SupportedOSPlatform("windows")]
+ private void RestoreWindowsStyles()
+ {
+ if (hwnd == 0)
+ {
+ return;
+ }
+
+ _ = NativeMethods.SetWindowLongPtr(hwnd, GWL_STYLE, originalStyle);
+ _ = NativeMethods.SetWindowLongPtr(hwnd, GWL_EXSTYLE, originalExStyle);
+ _ = NativeMethods.SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_FRAMECHANGED);
+
+ styled = false;
+ geometryApplied = false;
+ lastAlpha = 0;
+ lastClickThrough = false;
+ }
+
+ [SupportedOSPlatform("windows")]
+ private static bool TryGetWorkArea(nint windowHandle, out NativeMethods.RECT work)
+ {
+ nint monitor = NativeMethods.MonitorFromWindow(windowHandle, MONITOR_DEFAULTTONEAREST);
+ NativeMethods.MONITORINFO info = new() { cbSize = System.Runtime.InteropServices.Marshal.SizeOf() };
+ if (monitor != 0 && NativeMethods.GetMonitorInfo(monitor, ref info))
+ {
+ work = info.rcWork;
+ return true;
+ }
+
+ work = default;
+ return false;
+ }
+}
diff --git a/README.md b/README.md
index 04dd3e5..b1554b9 100644
--- a/README.md
+++ b/README.md
@@ -150,6 +150,44 @@ ImGuiApp.Start(new ImGuiAppConfig
});
```
+### Overlay Mode (Always-On-Top HUD)
+
+Overlay mode turns the application window into a borderless, always-on-top, translucent HUD
+with optional click-through — ideal for live status displays that sit over other apps. Drive
+it from your render callback; the calls are cheap to make every frame (the native window is
+only restyled when something actually changes). A dedicated `OverlayFps` keeps the overlay
+animating smoothly even while unfocused, bypassing the normal focus/idle/visibility throttling.
+
+```csharp
+bool overlayEnabled = true;
+
+ImGuiApp.Start(new ImGuiAppConfig
+{
+ Title = "Status HUD",
+ PerformanceSettings = new ImGuiAppPerformanceSettings { OverlayFps = 60.0 },
+ OnRender = delta =>
+ {
+ if (overlayEnabled)
+ {
+ // opacity 0.2–1.0, plus optional click-through (mouse passes through to apps behind it).
+ ImGuiApp.EnableOverlay(opacity: 0.9f, clickThrough: false);
+ // Optional: lock it to a corner of the monitor work area (Windows).
+ ImGuiApp.SetOverlayGeometry(OverlayCorner.TopRight, offsetX: 24, offsetY: 24, width: 360, height: 280);
+ }
+ else
+ {
+ ImGuiApp.DisableOverlay(); // restores the decorated window
+ }
+
+ ImGui.Text("Live status…");
+ }
+});
+```
+
+Overlay window styling (borderless / topmost / translucency / click-through) is implemented on
+Windows. On other platforms `IsOverlayActive` and `OverlayFps` still apply, but the window is
+not restyled.
+
### Widgets
```csharp
@@ -312,6 +350,10 @@ Application lifecycle and utilities.
| ---- | ----------- | ----------- |
| `Start(ImGuiAppConfig)` | `void` | Initialize and run the application |
| `Stop()` | `void` | Close the application window |
+| `Show()` / `Hide()` | `void` | Show or hide the window without stopping the render loop |
+| `EnableOverlay(float, bool)` | `void` | Enter overlay mode: borderless, always-on-top, translucent, optional click-through |
+| `SetOverlayGeometry(OverlayCorner, int, int, int, int)` | `void` | Lock the overlay to a work-area corner at an offset and size |
+| `DisableOverlay()` | `void` | Restore the decorated, non-topmost, opaque window |
| `SetGlobalScale(float)` | `void` | Set accessibility UI scale (0.5-3.0) |
| `SetWindowIcon(string)` | `void` | Set window icon from image file |
| `GetOrLoadTexture(AbsoluteFilePath)` | `ImGuiAppTextureInfo` | Load or retrieve cached GPU texture |
@@ -326,6 +368,7 @@ Application lifecycle and utilities.
| `IsFocused` | `bool` | Whether the window has focus |
| `IsVisible` | `bool` | Whether the window is visible |
| `IsIdle` | `bool` | Whether the app is idle |
+| `IsOverlayActive` | `bool` | Whether the window is in overlay mode |
| `ScaleFactor` | `float` | DPI-based scale factor |
| `GlobalScale` | `float` | User-adjustable UI scale |
| `Invoker` | `Invoker` | Delegate invocation for window thread |
@@ -363,6 +406,7 @@ Frame rate throttling configuration.
| `UnfocusedFps` | `double` | Target FPS when unfocused (default: 5) |
| `IdleFps` | `double` | Target FPS when idle (default: 10) |
| `NotVisibleFps` | `double` | Target FPS when minimized (default: 2) |
+| `OverlayFps` | `double` | Target FPS in overlay mode, bypassing focus/idle/visibility throttling (default: 30) |
| `IdleTimeoutSeconds` | `double` | Seconds before idle state (default: 30) |
### `ImGuiWidgets` (Static)
diff --git a/examples/ImGuiAppDemo/ImGuiAppDemo.cs b/examples/ImGuiAppDemo/ImGuiAppDemo.cs
index 9a5f4c7..e52ee53 100644
--- a/examples/ImGuiAppDemo/ImGuiAppDemo.cs
+++ b/examples/ImGuiAppDemo/ImGuiAppDemo.cs
@@ -13,6 +13,13 @@ namespace ktsu.ImGui.Examples.App;
internal static class ImGuiAppDemo
{
private static bool showAbout;
+
+ // Overlay-mode demo state: a borderless, always-on-top, translucent window locked to a corner.
+ private static bool overlayEnabled;
+ private static float overlayOpacity = 0.9f;
+ private static bool overlayClickThrough;
+ private static OverlayCorner overlayCorner = OverlayCorner.TopRight;
+
private static readonly List demoTabs = [];
static ImGuiAppDemo()
@@ -53,6 +60,9 @@ private static void Main() => ImGuiApp.Start(new()
PerformanceSettings = new()
{
EnableThrottledRendering = true,
+ // Overlay mode keeps animating at 60 FPS even while unfocused (it's always-on-top
+ // and shows live content), bypassing the focus/idle/visibility throttling.
+ OverlayFps = 60.0,
// Using default values: Focused=30, Unfocused=5, Idle=10 FPS
// But with a shorter idle timeout for demo purposes
IdleTimeoutSeconds = 5.0, // Consider idle after 5 seconds (default is 30)
@@ -61,14 +71,33 @@ private static void Main() => ImGuiApp.Start(new()
private static void OnRender(float dt)
{
+ // Keep the native window styling in sync with the overlay-demo toggle. Calling these
+ // every frame is cheap — the underlying window is only restyled when something changes.
+ if (overlayEnabled)
+ {
+ ImGuiApp.EnableOverlay(overlayOpacity, overlayClickThrough);
+ ImGuiApp.SetOverlayGeometry(overlayCorner, offsetX: 24, offsetY: 24, width: 380, height: 320);
+ }
+ else
+ {
+ ImGuiApp.DisableOverlay();
+ }
+
// Update all demo tabs
foreach (IDemoTab demo in demoTabs)
{
demo.Update(dt);
}
- // Render main demo window
- RenderMainDemoWindow();
+ // In overlay mode show a compact control strip instead of the full tabbed UI.
+ if (overlayEnabled)
+ {
+ RenderOverlayControls();
+ }
+ else
+ {
+ RenderMainDemoWindow();
+ }
// Show about window if requested
if (showAbout)
@@ -77,6 +106,32 @@ private static void OnRender(float dt)
}
}
+ // Demonstrates the canonical overlay API: toggle, opacity, click-through, and corner anchor.
+ private static void RenderOverlayControls()
+ {
+ ImGui.TextUnformatted("Overlay mode");
+ ImGui.Separator();
+
+ _ = ImGui.SliderFloat("Opacity", ref overlayOpacity, 0.2f, 1.0f, "%.2f");
+ _ = ImGui.Checkbox("Click-through", ref overlayClickThrough);
+
+ int corner = (int)overlayCorner;
+ if (ImGui.Combo("Corner", ref corner, "Top-left\0Top-right\0Bottom-left\0Bottom-right\0"))
+ {
+ overlayCorner = (OverlayCorner)corner;
+ }
+
+ if (overlayClickThrough)
+ {
+ ImGui.TextDisabled("Click-through is on — toggle it from the View menu to interact.");
+ }
+
+ if (ImGui.Button("Exit overlay"))
+ {
+ overlayEnabled = false;
+ }
+ }
+
private static void RenderMainDemoWindow()
{
// Create tabs for different demo sections
@@ -118,6 +173,16 @@ private static void RenderAboutWindow()
private static void OnAppMenu()
{
+ if (ImGui.BeginMenu("View"))
+ {
+ ImGui.MenuItem("Overlay mode", string.Empty, ref overlayEnabled);
+ if (overlayEnabled)
+ {
+ ImGui.MenuItem("Overlay click-through", string.Empty, ref overlayClickThrough);
+ }
+ ImGui.EndMenu();
+ }
+
if (ImGui.BeginMenu("Help"))
{
ImGui.MenuItem("About", string.Empty, ref showAbout);
diff --git a/tests/ImGui.App.Tests/ImGuiAppTests.cs b/tests/ImGui.App.Tests/ImGuiAppTests.cs
index c148b83..dd08543 100644
--- a/tests/ImGui.App.Tests/ImGuiAppTests.cs
+++ b/tests/ImGui.App.Tests/ImGuiAppTests.cs
@@ -467,6 +467,7 @@ public void PerformanceSettings_DefaultValues_AreCorrect()
Assert.AreEqual(5.0, settings.UnfocusedFps);
Assert.AreEqual(10.0, settings.IdleFps);
Assert.AreEqual(2.0, settings.NotVisibleFps);
+ Assert.AreEqual(30.0, settings.OverlayFps);
Assert.IsTrue(settings.EnableIdleDetection, "EnableIdleDetection should default to true");
Assert.AreEqual(30.0, settings.IdleTimeoutSeconds);
}
diff --git a/tests/ImGui.App.Tests/ImGuiAppWindowManagementTests.cs b/tests/ImGui.App.Tests/ImGuiAppWindowManagementTests.cs
index 7bf74b8..a4ab7d1 100644
--- a/tests/ImGui.App.Tests/ImGuiAppWindowManagementTests.cs
+++ b/tests/ImGui.App.Tests/ImGuiAppWindowManagementTests.cs
@@ -422,6 +422,98 @@ public void UpdateWindowPerformance_WithUnfocusedWindow_DoesNotThrow()
}
}
+ [TestMethod]
+ public void UpdateWindowPerformance_WhenOverlayActive_UsesOverlayFps()
+ {
+ ImGuiAppConfig config = new()
+ {
+ Title = "Test",
+ InitialWindowState = new ImGuiAppWindowState
+ {
+ Size = new Vector2(800, 600),
+ Pos = new Vector2(100, 100),
+ LayoutState = WindowState.Normal
+ },
+ PerformanceSettings = new ImGuiAppPerformanceSettings
+ {
+ FocusedFps = 30.0,
+ OverlayFps = 60.0
+ }
+ };
+ ImGuiApp.Config = config;
+
+ // No native window in tests, but overlay mode is logically active regardless of platform.
+ ImGuiApp.EnableOverlay(opacity: 0.85f);
+ Assert.IsTrue(ImGuiApp.IsOverlayActive, "Overlay mode should be active after EnableOverlay.");
+
+ ImGuiApp.UpdateWindowPerformance();
+
+ Assert.AreEqual(1000.0 / 60.0, ImGuiApp.targetFrameTimeMs, 0.0001,
+ "Overlay mode should drive the frame rate from OverlayFps, bypassing focus/idle/visibility throttling.");
+
+ ImGuiApp.DisableOverlay();
+ Assert.IsFalse(ImGuiApp.IsOverlayActive, "Overlay mode should be inactive after DisableOverlay.");
+ }
+
+ [TestMethod]
+ public void UpdateWindowPerformance_WhenOverlayActive_ClearsIdleState()
+ {
+ ImGuiAppConfig config = new()
+ {
+ Title = "Test",
+ PerformanceSettings = new ImGuiAppPerformanceSettings
+ {
+ OverlayFps = 30.0,
+ EnableIdleDetection = true,
+ IdleTimeoutSeconds = 0.0
+ }
+ };
+ ImGuiApp.Config = config;
+ ImGuiApp.EnableOverlay();
+
+ ImGuiApp.UpdateWindowPerformance();
+
+ Assert.IsFalse(ImGuiApp.IsIdle, "Overlay mode shows live data, so it should not be throttled to the idle rate.");
+
+ ImGuiApp.DisableOverlay();
+ }
+
+ [TestMethod]
+ public void EnableOverlay_ThenDisableOverlay_TogglesIsOverlayActive()
+ {
+ Assert.IsFalse(ImGuiApp.IsOverlayActive, "Overlay mode should be inactive by default.");
+
+ ImGuiApp.EnableOverlay(opacity: 0.5f, clickThrough: true);
+ Assert.IsTrue(ImGuiApp.IsOverlayActive);
+
+ // Idempotent: re-enabling (e.g. every frame) keeps it active.
+ ImGuiApp.EnableOverlay(opacity: 0.9f, clickThrough: false);
+ Assert.IsTrue(ImGuiApp.IsOverlayActive);
+
+ ImGuiApp.DisableOverlay();
+ Assert.IsFalse(ImGuiApp.IsOverlayActive);
+
+ // Idempotent: disabling again is safe.
+ ImGuiApp.DisableOverlay();
+ Assert.IsFalse(ImGuiApp.IsOverlayActive);
+ }
+
+ [TestMethod]
+ public void SetOverlayGeometry_WhenOverlayInactive_DoesNotThrow()
+ {
+ try
+ {
+ // No overlay active and no native window: should be a safe no-op.
+ ImGuiApp.SetOverlayGeometry(OverlayCorner.TopRight, 24, 24, 460, 320);
+ }
+#pragma warning disable CA1031 // Do not catch general exception types
+ catch (Exception ex)
+#pragma warning restore CA1031 // Do not catch general exception types
+ {
+ Assert.Fail($"Expected no exception, but got: {ex.Message}");
+ }
+ }
+
#endregion
#region Window Icon Tests