From 81b07bea0c620357af558f6ec4be506d1f1bbfdf Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 10 Jun 2026 13:59:10 +0000
Subject: [PATCH] feat(canvas): F11 borderless fullscreen toggle for Canvas
windows
Add F11/Escape keyboard shortcut to toggle borderless fullscreen in
both Canvas window types (WebView2-based CanvasWindow and native
A2UICanvasWindow).
Implementation:
- Use AppWindow.SetPresenter(AppWindowPresenterKind.FullScreen) to
enter/exit fullscreen (WinUI 3 AppWindow API via Microsoft.UI.Windowing)
- A2UICanvasWindow: KeyboardAccelerators on RootGrid (XAML tree handles
F11/Esc natively)
- CanvasWindow: KeyboardAccelerators on content root for XAML-focused
state, plus JavaScript injection via AddScriptToExecuteOnDocumentCreatedAsync
so F11/Escape are also captured when WebView2 content has focus and
routed via the existing web bridge (postMessage)
- Bridge messages 'fullscreen-toggle' and 'fullscreen-exit' are intercepted
before forwarding to external BridgeMessageReceived subscribers
- ExitFullScreen() called on window close to restore normal presenter state
Closes #669
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Windows/A2UICanvasWindow.xaml.cs | 45 ++++++++++
.../Windows/CanvasWindow.xaml.cs | 85 ++++++++++++++++++-
2 files changed, 129 insertions(+), 1 deletion(-)
diff --git a/src/OpenClaw.Tray.WinUI/Windows/A2UICanvasWindow.xaml.cs b/src/OpenClaw.Tray.WinUI/Windows/A2UICanvasWindow.xaml.cs
index 4188ab23..0fe005fb 100644
--- a/src/OpenClaw.Tray.WinUI/Windows/A2UICanvasWindow.xaml.cs
+++ b/src/OpenClaw.Tray.WinUI/Windows/A2UICanvasWindow.xaml.cs
@@ -5,8 +5,10 @@
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Microsoft.UI.Dispatching;
+using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Imaging;
using OpenClaw.Shared;
using OpenClawTray.A2UI.Actions;
@@ -16,6 +18,7 @@
using WinUIEx;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
+using Windows.System;
namespace OpenClawTray.Windows;
@@ -45,6 +48,7 @@ public sealed partial class A2UICanvasWindow : WindowEx
private readonly DispatcherQueue _dispatcher;
private readonly A2UIRouter _router;
private readonly DataModelStore _dataModel;
+ private bool _isFullScreen;
public bool IsClosed { get; private set; }
@@ -69,6 +73,22 @@ public A2UICanvasWindow(IActionSink actions, MediaResolver media, IOpenClawLogge
_router.SurfaceCreated += OnSurfaceCreated;
_router.SurfaceDeleted += OnSurfaceDeleted;
+ // F11 toggles borderless fullscreen; Escape exits it.
+ var f11Accel = new KeyboardAccelerator { Key = VirtualKey.F11 };
+ f11Accel.Invoked += (_, args) =>
+ {
+ args.Handled = true;
+ ToggleFullScreen();
+ };
+ var escAccel = new KeyboardAccelerator { Key = VirtualKey.Escape };
+ escAccel.Invoked += (_, args) =>
+ {
+ if (_isFullScreen) { args.Handled = true; ExitFullScreen(); }
+ };
+ RootGrid.KeyboardAccelerators.Add(f11Accel);
+ RootGrid.KeyboardAccelerators.Add(escAccel);
+ RootGrid.KeyboardAcceleratorPlacementMode = KeyboardAcceleratorPlacementMode.Hidden;
+
// Explicit teardown: unsubscribe router events and reset surfaces so
// the router's component subscriptions don't outlive the window. The
// router holds back-references via event delegates; without this, a
@@ -77,6 +97,7 @@ public A2UICanvasWindow(IActionSink actions, MediaResolver media, IOpenClawLogge
Closed += (_, _) =>
{
IsClosed = true;
+ ExitFullScreen();
try { _router.SurfaceCreated -= OnSurfaceCreated; }
catch (Exception ex) { OpenClawTray.Services.Logger.Debug($"A2UICanvasWindow: unsubscribe SurfaceCreated failed: {ex.Message}"); }
try { _router.SurfaceDeleted -= OnSurfaceDeleted; }
@@ -338,6 +359,30 @@ public string GetStateSnapshot()
return snapshot.ToJsonString();
}
+ // ── Fullscreen ──────────────────────────────────────────────────────────
+
+ /// Toggle borderless fullscreen on F11.
+ public void ToggleFullScreen()
+ {
+ if (_isFullScreen) ExitFullScreen();
+ else EnterFullScreen();
+ }
+
+ private void EnterFullScreen()
+ {
+ _isFullScreen = true;
+ try { AppWindow.SetPresenter(AppWindowPresenterKind.FullScreen); }
+ catch (Exception ex) { OpenClawTray.Services.Logger.Debug($"A2UICanvasWindow: EnterFullScreen failed: {ex.Message}"); }
+ }
+
+ private void ExitFullScreen()
+ {
+ if (!_isFullScreen) return;
+ _isFullScreen = false;
+ try { AppWindow.SetPresenter(AppWindowPresenterKind.Default); }
+ catch (Exception ex) { OpenClawTray.Services.Logger.Debug($"A2UICanvasWindow: ExitFullScreen failed: {ex.Message}"); }
+ }
+
public void BringToFront(bool keepTopMost = false)
{
try
diff --git a/src/OpenClaw.Tray.WinUI/Windows/CanvasWindow.xaml.cs b/src/OpenClaw.Tray.WinUI/Windows/CanvasWindow.xaml.cs
index 02f08666..15256a78 100644
--- a/src/OpenClaw.Tray.WinUI/Windows/CanvasWindow.xaml.cs
+++ b/src/OpenClaw.Tray.WinUI/Windows/CanvasWindow.xaml.cs
@@ -5,8 +5,10 @@
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using Microsoft.UI.Dispatching;
+using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
+using Microsoft.UI.Xaml.Input;
using Microsoft.Web.WebView2.Core;
using OpenClaw.Shared;
using OpenClawTray.Helpers;
@@ -14,6 +16,7 @@
using WinUIEx;
using Windows.Foundation;
using Windows.Storage.Streams;
+using Windows.System;
namespace OpenClawTray.Windows;
@@ -39,6 +42,7 @@ public sealed partial class CanvasWindow : WindowEx
private const uint SWP_SHOWWINDOW = 0x0040;
private bool _isWebViewInitialized;
+ private bool _isFullScreen;
private string? _pendingUrl;
private string? _pendingHtml;
private readonly TaskCompletionSource _webViewReadyTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -235,7 +239,30 @@ public CanvasWindow()
this.SetIcon("Assets\\openclaw.ico");
_dispatcherQueue = DispatcherQueue;
this.Closed += OnWindowClosed;
-
+
+ // F11 toggles borderless fullscreen; Escape exits it.
+ // KeyboardAccelerators on the root content get first-class handling
+ // when focus is in the XAML tree (title bar, toolbar, etc.).
+ // When focus is inside the WebView2, F11/Escape are also intercepted
+ // via an injected content script that posts bridge messages.
+ if (this.Content is FrameworkElement contentRoot)
+ {
+ var f11Accel = new KeyboardAccelerator { Key = VirtualKey.F11 };
+ f11Accel.Invoked += (_, args) =>
+ {
+ args.Handled = true;
+ ToggleFullScreen();
+ };
+ var escAccel = new KeyboardAccelerator { Key = VirtualKey.Escape };
+ escAccel.Invoked += (_, args) =>
+ {
+ if (_isFullScreen) { args.Handled = true; ExitFullScreen(); }
+ };
+ contentRoot.KeyboardAccelerators.Add(f11Accel);
+ contentRoot.KeyboardAccelerators.Add(escAccel);
+ contentRoot.KeyboardAcceleratorPlacementMode = KeyboardAcceleratorPlacementMode.Hidden;
+ }
+
// Initialize WebView2
InitializeWebViewAsync();
}
@@ -284,6 +311,24 @@ private async Task InitializeWebViewCoreAsync()
CanvasWebView.CoreWebView2.Settings.IsStatusBarEnabled = false;
CanvasWebView.CoreWebView2.Settings.AreDevToolsEnabled = false;
+ // Inject F11/Escape fullscreen bridge: intercepts these keys when
+ // WebView2 content has focus and routes them as bridge messages so
+ // the native window can toggle its presenter the same way the XAML
+ // keyboard accelerators do when focus is in the title bar.
+ await CanvasWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("""
+ (function () {
+ document.addEventListener('keydown', function (e) {
+ if (!window.chrome || !window.chrome.webview) return;
+ if (e.key === 'F11') {
+ e.preventDefault();
+ window.chrome.webview.postMessage(JSON.stringify({ type: 'fullscreen-toggle' }));
+ } else if (e.key === 'Escape') {
+ window.chrome.webview.postMessage(JSON.stringify({ type: 'fullscreen-exit' }));
+ }
+ }, true);
+ })();
+ """);
+
// Wire the bidirectional native↔SPA bridge
// SPA → native: window.chrome.webview.postMessage({ type, payload })
_webMessageReceivedHandler = (s, e) =>
@@ -297,6 +342,19 @@ private async Task InitializeWebViewCoreAsync()
var msg = WebBridgeMessage.TryParse(e.WebMessageAsJson);
if (msg != null)
{
+ // Fullscreen control messages are handled natively and not
+ // forwarded to external bridge subscribers.
+ if (msg.Type == "fullscreen-toggle")
+ {
+ _dispatcherQueue?.TryEnqueue(ToggleFullScreen);
+ return;
+ }
+ if (msg.Type == "fullscreen-exit")
+ {
+ _dispatcherQueue?.TryEnqueue(ExitFullScreen);
+ return;
+ }
+
Logger.Debug($"[Canvas] bridge message from SPA, type={SanitizeBridgeLogValue(msg.Type)}");
BridgeMessageReceived?.Invoke(this, msg);
}
@@ -473,6 +531,7 @@ private void OnCanvasFileChanged(object sender, FileSystemEventArgs e)
private void OnWindowClosed(object sender, WindowEventArgs args)
{
IsClosed = true;
+ ExitFullScreen();
_gatewayToken = null;
if (CanvasWebView.CoreWebView2 != null)
@@ -659,6 +718,30 @@ public void BringToFront(bool keepTopMost)
}
}
+ // ── Fullscreen ──────────────────────────────────────────────────────────
+
+ /// Toggle borderless fullscreen on F11.
+ public void ToggleFullScreen()
+ {
+ if (_isFullScreen) ExitFullScreen();
+ else EnterFullScreen();
+ }
+
+ private void EnterFullScreen()
+ {
+ _isFullScreen = true;
+ try { AppWindow.SetPresenter(AppWindowPresenterKind.FullScreen); }
+ catch (Exception ex) { Logger.Debug($"[Canvas] EnterFullScreen failed: {ex.Message}"); }
+ }
+
+ private void ExitFullScreen()
+ {
+ if (!_isFullScreen) return;
+ _isFullScreen = false;
+ try { AppWindow.SetPresenter(AppWindowPresenterKind.Default); }
+ catch (Exception ex) { Logger.Debug($"[Canvas] ExitFullScreen failed: {ex.Message}"); }
+ }
+
public async Task EnsureA2UIHostAsync(string url)
{
await EnsureWebViewReadyAsync();