From 96f2ca5443c3f87a5456cf38320631769952da6b Mon Sep 17 00:00:00 2001 From: Cody Mullins <1738479+codymullins@users.noreply.github.com> Date: Tue, 16 Jun 2026 20:44:07 -0400 Subject: [PATCH] feat(window): window focus/blur (activation) events Add AppWindow.FocusChanged (true = active) so a shell can pause animation, timers, and media while the window is blurred and resume on focus. - GLFW backend forwards Silk.NET's IWindow.FocusChanged. - AppKit backend raises it from windowDidBecomeKey / windowDidResignKey. Addresses #10 (the focus/blur part). The occlusion signal is macOS-only (NSWindow.occlusionState) with no GLFW equivalent, so it does not fit the shared backend seam and is left as follow-up. --- src/Skyline.Apple/AppKitWindowBackend.cs | 7 +++++++ src/Skyline/AppWindow.cs | 7 +++++++ src/Skyline/GlfwWindowBackend.cs | 2 ++ src/Skyline/IWindowBackend.cs | 3 +++ tests/Skyline.Tests/WindowBackendFactoryTests.cs | 1 + 5 files changed, 20 insertions(+) diff --git a/src/Skyline.Apple/AppKitWindowBackend.cs b/src/Skyline.Apple/AppKitWindowBackend.cs index 91524de..09b11dc 100644 --- a/src/Skyline.Apple/AppKitWindowBackend.cs +++ b/src/Skyline.Apple/AppKitWindowBackend.cs @@ -28,6 +28,7 @@ internal sealed class AppKitWindowBackend : IWindowBackend public event Action<(int Width, int Height)>? FramebufferResized; public event Action? MinimizedChanged; + public event Action? FocusChanged; public event Action? Render; public event Action? Pointer; public event Action? Key; @@ -162,6 +163,8 @@ public void Run() internal void OnMinimizedChanged(bool minimized) => MinimizedChanged?.Invoke(minimized); + internal void OnFocusChanged(bool focused) => FocusChanged?.Invoke(focused); + public void Dispose() { _window.Delegate = null; @@ -181,5 +184,9 @@ private sealed class WindowDelegate(AppKitWindowBackend backend) : NSWindowDeleg public override void DidMiniaturize(NSNotification notification) => backend.OnMinimizedChanged(true); public override void DidDeminiaturize(NSNotification notification) => backend.OnMinimizedChanged(false); + + public override void DidBecomeKey(NSNotification notification) => backend.OnFocusChanged(true); + + public override void DidResignKey(NSNotification notification) => backend.OnFocusChanged(false); } } diff --git a/src/Skyline/AppWindow.cs b/src/Skyline/AppWindow.cs index 962a94a..08db5d0 100644 --- a/src/Skyline/AppWindow.cs +++ b/src/Skyline/AppWindow.cs @@ -97,6 +97,7 @@ public AppWindow(AppWindowOptions? options = null) _backend.Pointer += e => PointerInput?.Invoke(e); _backend.Key += e => KeyInput?.Invoke(e); _backend.Text += e => TextInput?.Invoke(e); + _backend.FocusChanged += focused => FocusChanged?.Invoke(focused); } internal void RaisePointer(PointerEventKind kind, float x, float y, int button, float wheelDx, float wheelDy) => @@ -139,6 +140,12 @@ internal void RaiseText(char character) => public event Action? KeyInput; public event Action? TextInput; + /// + /// The window gained or lost focus (true = active). Pause animation, + /// timers, and media while blurred; resume on focus. + /// + public event Action? FocusChanged; + /// /// When set and returning false, the frame is skipped and the loop /// sleeps briefly instead of rendering. Continuous-render apps leave diff --git a/src/Skyline/GlfwWindowBackend.cs b/src/Skyline/GlfwWindowBackend.cs index 3d0d94f..5a5cc35 100644 --- a/src/Skyline/GlfwWindowBackend.cs +++ b/src/Skyline/GlfwWindowBackend.cs @@ -22,6 +22,7 @@ internal sealed class GlfwWindowBackend : IWindowBackend public event Action<(int Width, int Height)>? FramebufferResized; public event Action? MinimizedChanged; + public event Action? FocusChanged; public event Action? Render; public event Action? Pointer; public event Action? Key; @@ -51,6 +52,7 @@ public unsafe GlfwWindowBackend(AppWindowOptions options) _window.FramebufferResize += sz => FramebufferResized?.Invoke((sz.X, sz.Y)); _window.StateChanged += state => MinimizedChanged?.Invoke(state == WindowState.Minimized); + _window.FocusChanged += focused => FocusChanged?.Invoke(focused); _window.Render += delta => Render?.Invoke(delta); _input = _window.CreateInput(); diff --git a/src/Skyline/IWindowBackend.cs b/src/Skyline/IWindowBackend.cs index e1b01c9..b80da32 100644 --- a/src/Skyline/IWindowBackend.cs +++ b/src/Skyline/IWindowBackend.cs @@ -31,6 +31,9 @@ internal interface IWindowBackend : IDisposable /// Minimized state changed; true when minimized. event Action? MinimizedChanged; + /// Focus changed; true when the window became active. + event Action? FocusChanged; + /// A frame tick from the backend's own loop, with the delta seconds. event Action? Render; diff --git a/tests/Skyline.Tests/WindowBackendFactoryTests.cs b/tests/Skyline.Tests/WindowBackendFactoryTests.cs index a7b3ff2..649a793 100644 --- a/tests/Skyline.Tests/WindowBackendFactoryTests.cs +++ b/tests/Skyline.Tests/WindowBackendFactoryTests.cs @@ -21,6 +21,7 @@ private sealed class FakeBackend : IWindowBackend public event Action<(int Width, int Height)>? FramebufferResized; public event Action? MinimizedChanged; + public event Action? FocusChanged; public event Action? Render; public event Action? Pointer; public event Action? Key;