Skip to content

Commit 68fddf0

Browse files
ranjeshjCopilot
authored andcommitted
fix(canvas): repair fullscreen WebView bridge
Send fullscreen shortcut messages as object-root WebView bridge payloads, guard bridge parsing against non-object roots, and use shared fullscreen message constants. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 234fa8f commit 68fddf0

4 files changed

Lines changed: 153 additions & 4 deletions

File tree

src/OpenClaw.Shared/WebBridgeMessage.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ public WebBridgeMessage(string type, string? payloadJson = null)
4646
/// <summary>Sent SPA→native when the SPA is fully initialised and ready for messages.</summary>
4747
public const string TypeReady = "ready";
4848

49+
/// <summary>Sent SPA→native to toggle the owning canvas window fullscreen state.</summary>
50+
public const string TypeFullscreenToggle = "fullscreen-toggle";
51+
52+
/// <summary>Sent SPA→native to exit fullscreen on the owning canvas window.</summary>
53+
public const string TypeFullscreenExit = "fullscreen-exit";
54+
4955
// ── parsing ────────────────────────────────────────────────────────────
5056

5157
/// <summary>
@@ -62,6 +68,9 @@ public WebBridgeMessage(string type, string? payloadJson = null)
6268
using var doc = JsonDocument.Parse(json);
6369
var root = doc.RootElement;
6470

71+
if (root.ValueKind != JsonValueKind.Object)
72+
return null;
73+
6574
if (!root.TryGetProperty("type", out var typeEl) || typeEl.ValueKind != JsonValueKind.String)
6675
return null;
6776

src/OpenClaw.Tray.WinUI/Windows/A2UICanvasWindow.xaml.cs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
using System.Text.Json.Nodes;
66
using System.Threading.Tasks;
77
using Microsoft.UI.Dispatching;
8+
using Microsoft.UI.Windowing;
89
using Microsoft.UI.Xaml;
910
using Microsoft.UI.Xaml.Controls;
11+
using Microsoft.UI.Xaml.Input;
1012
using Microsoft.UI.Xaml.Media.Imaging;
1113
using OpenClaw.Shared;
1214
using OpenClawTray.A2UI.Actions;
@@ -16,6 +18,7 @@
1618
using WinUIEx;
1719
using Windows.Graphics.Imaging;
1820
using Windows.Storage.Streams;
21+
using Windows.System;
1922

2023
namespace OpenClawTray.Windows;
2124

@@ -42,9 +45,10 @@ public sealed partial class A2UICanvasWindow : WindowEx
4245
private const uint SWP_NOSIZE = 0x0001;
4346
private const uint SWP_SHOWWINDOW = 0x0040;
4447

45-
private readonly DispatcherQueue _dispatcher;
48+
private readonly Microsoft.UI.Dispatching.DispatcherQueue _dispatcher;
4649
private readonly A2UIRouter _router;
4750
private readonly DataModelStore _dataModel;
51+
private bool _isFullScreen;
4852

4953
public bool IsClosed { get; private set; }
5054

@@ -61,14 +65,30 @@ public A2UICanvasWindow(IActionSink actions, MediaResolver media, IOpenClawLogge
6165
Title = OpenClawTray.Helpers.LocalizationHelper.GetString("A2UI_CanvasTitle");
6266
WaitingForContentText.Text = OpenClawTray.Helpers.LocalizationHelper.GetString("Canvas_WaitingForContent");
6367

64-
_dispatcher = DispatcherQueue.GetForCurrentThread();
68+
_dispatcher = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread();
6569
_dataModel = new DataModelStore(_dispatcher);
6670
var registry = ComponentRendererRegistry.BuildDefault(media);
6771
_router = new A2UIRouter(_dispatcher, _dataModel, registry, actions, logger);
6872

6973
_router.SurfaceCreated += OnSurfaceCreated;
7074
_router.SurfaceDeleted += OnSurfaceDeleted;
7175

76+
// F11 toggles borderless fullscreen; Escape exits it.
77+
var f11Accel = new KeyboardAccelerator { Key = VirtualKey.F11 };
78+
f11Accel.Invoked += (_, args) =>
79+
{
80+
args.Handled = true;
81+
ToggleFullScreen();
82+
};
83+
var escAccel = new KeyboardAccelerator { Key = VirtualKey.Escape };
84+
escAccel.Invoked += (_, args) =>
85+
{
86+
if (_isFullScreen) { args.Handled = true; ExitFullScreen(); }
87+
};
88+
RootGrid.KeyboardAccelerators.Add(f11Accel);
89+
RootGrid.KeyboardAccelerators.Add(escAccel);
90+
RootGrid.KeyboardAcceleratorPlacementMode = KeyboardAcceleratorPlacementMode.Hidden;
91+
7292
// Explicit teardown: unsubscribe router events and reset surfaces so
7393
// the router's component subscriptions don't outlive the window. The
7494
// router holds back-references via event delegates; without this, a
@@ -77,6 +97,7 @@ public A2UICanvasWindow(IActionSink actions, MediaResolver media, IOpenClawLogge
7797
Closed += (_, _) =>
7898
{
7999
IsClosed = true;
100+
ExitFullScreen();
80101
try { _router.SurfaceCreated -= OnSurfaceCreated; }
81102
catch (Exception ex) { OpenClawTray.Services.Logger.Debug($"A2UICanvasWindow: unsubscribe SurfaceCreated failed: {ex.Message}"); }
82103
try { _router.SurfaceDeleted -= OnSurfaceDeleted; }
@@ -338,6 +359,30 @@ public string GetStateSnapshot()
338359
return snapshot.ToJsonString();
339360
}
340361

362+
// ── Fullscreen ──────────────────────────────────────────────────────────
363+
364+
/// <summary>Toggle borderless fullscreen on F11.</summary>
365+
public void ToggleFullScreen()
366+
{
367+
if (_isFullScreen) ExitFullScreen();
368+
else EnterFullScreen();
369+
}
370+
371+
private void EnterFullScreen()
372+
{
373+
_isFullScreen = true;
374+
try { AppWindow.SetPresenter(AppWindowPresenterKind.FullScreen); }
375+
catch (Exception ex) { OpenClawTray.Services.Logger.Debug($"A2UICanvasWindow: EnterFullScreen failed: {ex.Message}"); }
376+
}
377+
378+
private void ExitFullScreen()
379+
{
380+
if (!_isFullScreen) return;
381+
_isFullScreen = false;
382+
try { AppWindow.SetPresenter(AppWindowPresenterKind.Default); }
383+
catch (Exception ex) { OpenClawTray.Services.Logger.Debug($"A2UICanvasWindow: ExitFullScreen failed: {ex.Message}"); }
384+
}
385+
341386
public void BringToFront(bool keepTopMost = false)
342387
{
343388
try

src/OpenClaw.Tray.WinUI/Windows/CanvasWindow.xaml.cs

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@
55
using System.Threading.Tasks;
66
using System.Runtime.InteropServices;
77
using Microsoft.UI.Dispatching;
8+
using Microsoft.UI.Windowing;
89
using Microsoft.UI.Xaml;
910
using Microsoft.UI.Xaml.Automation;
11+
using Microsoft.UI.Xaml.Input;
1012
using Microsoft.Web.WebView2.Core;
1113
using OpenClaw.Shared;
1214
using OpenClawTray.Helpers;
1315
using OpenClawTray.Services;
1416
using WinUIEx;
1517
using Windows.Foundation;
1618
using Windows.Storage.Streams;
19+
using Windows.System;
1720

1821
namespace OpenClawTray.Windows;
1922

@@ -39,6 +42,7 @@ public sealed partial class CanvasWindow : WindowEx
3942
private const uint SWP_SHOWWINDOW = 0x0040;
4043

4144
private bool _isWebViewInitialized;
45+
private bool _isFullScreen;
4246
private string? _pendingUrl;
4347
private string? _pendingHtml;
4448
private readonly TaskCompletionSource<bool> _webViewReadyTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -50,7 +54,7 @@ public sealed partial class CanvasWindow : WindowEx
5054
private FileSystemWatcher? _canvasWatcher;
5155
private long _lastReloadTicks = 0;
5256

53-
private readonly DispatcherQueue? _dispatcherQueue;
57+
private readonly Microsoft.UI.Dispatching.DispatcherQueue? _dispatcherQueue;
5458
private TypedEventHandler<CoreWebView2, CoreWebView2WebMessageReceivedEventArgs>? _webMessageReceivedHandler;
5559
private TypedEventHandler<CoreWebView2, CoreWebView2WebResourceRequestedEventArgs>? _webResourceRequestedHandler;
5660
private string? _webResourceRequestedFilter;
@@ -235,7 +239,30 @@ public CanvasWindow()
235239
this.SetIcon("Assets\\openclaw.ico");
236240
_dispatcherQueue = DispatcherQueue;
237241
this.Closed += OnWindowClosed;
238-
242+
243+
// F11 toggles borderless fullscreen; Escape exits it.
244+
// KeyboardAccelerators on the root content get first-class handling
245+
// when focus is in the XAML tree (title bar, toolbar, etc.).
246+
// When focus is inside the WebView2, F11/Escape are also intercepted
247+
// via an injected content script that posts bridge messages.
248+
if (this.Content is FrameworkElement contentRoot)
249+
{
250+
var f11Accel = new KeyboardAccelerator { Key = VirtualKey.F11 };
251+
f11Accel.Invoked += (_, args) =>
252+
{
253+
args.Handled = true;
254+
ToggleFullScreen();
255+
};
256+
var escAccel = new KeyboardAccelerator { Key = VirtualKey.Escape };
257+
escAccel.Invoked += (_, args) =>
258+
{
259+
if (_isFullScreen) { args.Handled = true; ExitFullScreen(); }
260+
};
261+
contentRoot.KeyboardAccelerators.Add(f11Accel);
262+
contentRoot.KeyboardAccelerators.Add(escAccel);
263+
contentRoot.KeyboardAcceleratorPlacementMode = KeyboardAcceleratorPlacementMode.Hidden;
264+
}
265+
239266
// Initialize WebView2
240267
InitializeWebViewAsync();
241268
}
@@ -284,6 +311,24 @@ private async Task InitializeWebViewCoreAsync()
284311
CanvasWebView.CoreWebView2.Settings.IsStatusBarEnabled = false;
285312
CanvasWebView.CoreWebView2.Settings.AreDevToolsEnabled = false;
286313

314+
// Inject F11/Escape fullscreen bridge: intercepts these keys when
315+
// WebView2 content has focus and routes them as bridge messages so
316+
// the native window can toggle its presenter the same way the XAML
317+
// keyboard accelerators do when focus is in the title bar.
318+
await CanvasWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("""
319+
(function () {
320+
document.addEventListener('keydown', function (e) {
321+
if (!window.chrome || !window.chrome.webview) return;
322+
if (e.key === 'F11') {
323+
e.preventDefault();
324+
window.chrome.webview.postMessage({ type: 'fullscreen-toggle' });
325+
} else if (e.key === 'Escape') {
326+
window.chrome.webview.postMessage({ type: 'fullscreen-exit' });
327+
}
328+
}, true);
329+
})();
330+
""");
331+
287332
// Wire the bidirectional native↔SPA bridge
288333
// SPA → native: window.chrome.webview.postMessage({ type, payload })
289334
_webMessageReceivedHandler = (s, e) =>
@@ -297,6 +342,19 @@ private async Task InitializeWebViewCoreAsync()
297342
var msg = WebBridgeMessage.TryParse(e.WebMessageAsJson);
298343
if (msg != null)
299344
{
345+
// Fullscreen control messages are handled natively and not
346+
// forwarded to external bridge subscribers.
347+
if (msg.Type == WebBridgeMessage.TypeFullscreenToggle)
348+
{
349+
_dispatcherQueue?.TryEnqueue(ToggleFullScreen);
350+
return;
351+
}
352+
if (msg.Type == WebBridgeMessage.TypeFullscreenExit)
353+
{
354+
_dispatcherQueue?.TryEnqueue(ExitFullScreen);
355+
return;
356+
}
357+
300358
Logger.Debug($"[Canvas] bridge message from SPA, type={SanitizeBridgeLogValue(msg.Type)}");
301359
BridgeMessageReceived?.Invoke(this, msg);
302360
}
@@ -473,6 +531,7 @@ private void OnCanvasFileChanged(object sender, FileSystemEventArgs e)
473531
private void OnWindowClosed(object sender, WindowEventArgs args)
474532
{
475533
IsClosed = true;
534+
ExitFullScreen();
476535
_gatewayToken = null;
477536

478537
if (CanvasWebView.CoreWebView2 != null)
@@ -659,6 +718,30 @@ public void BringToFront(bool keepTopMost)
659718
}
660719
}
661720

721+
// ── Fullscreen ──────────────────────────────────────────────────────────
722+
723+
/// <summary>Toggle borderless fullscreen on F11.</summary>
724+
public void ToggleFullScreen()
725+
{
726+
if (_isFullScreen) ExitFullScreen();
727+
else EnterFullScreen();
728+
}
729+
730+
private void EnterFullScreen()
731+
{
732+
_isFullScreen = true;
733+
try { AppWindow.SetPresenter(AppWindowPresenterKind.FullScreen); }
734+
catch (Exception ex) { Logger.Debug($"[Canvas] EnterFullScreen failed: {ex.Message}"); }
735+
}
736+
737+
private void ExitFullScreen()
738+
{
739+
if (!_isFullScreen) return;
740+
_isFullScreen = false;
741+
try { AppWindow.SetPresenter(AppWindowPresenterKind.Default); }
742+
catch (Exception ex) { Logger.Debug($"[Canvas] ExitFullScreen failed: {ex.Message}"); }
743+
}
744+
662745
public async Task EnsureA2UIHostAsync(string url)
663746
{
664747
await EnsureWebViewReadyAsync();

tests/OpenClaw.Shared.Tests/WebBridgeMessageTests.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ public void TryParse_InvalidJson_ReturnsNull()
5353
Assert.Null(WebBridgeMessage.TryParse("{bad json"));
5454
}
5555

56+
[Fact]
57+
public void TryParse_StringRoot_ReturnsNull()
58+
{
59+
var jsonStringRoot = """
60+
"{\"type\":\"fullscreen-toggle\"}"
61+
""";
62+
63+
Assert.Null(WebBridgeMessage.TryParse(jsonStringRoot));
64+
}
65+
5666
[Fact]
5767
public void TryParse_TypeIsNotString_ReturnsNull()
5868
{
@@ -129,6 +139,8 @@ public void ToJson_PassedPayloadOverridesStoredPayloadJson()
129139
[InlineData(WebBridgeMessage.TypeVoiceStart)]
130140
[InlineData(WebBridgeMessage.TypeVoiceStop)]
131141
[InlineData(WebBridgeMessage.TypeReady)]
142+
[InlineData(WebBridgeMessage.TypeFullscreenToggle)]
143+
[InlineData(WebBridgeMessage.TypeFullscreenExit)]
132144
public void RoundTrip_WellKnownTypes_PreserveType(string type)
133145
{
134146
var original = new WebBridgeMessage(type);

0 commit comments

Comments
 (0)