55using System . Threading . Tasks ;
66using System . Runtime . InteropServices ;
77using Microsoft . UI . Dispatching ;
8+ using Microsoft . UI . Windowing ;
89using Microsoft . UI . Xaml ;
910using Microsoft . UI . Xaml . Automation ;
11+ using Microsoft . UI . Xaml . Input ;
1012using Microsoft . Web . WebView2 . Core ;
1113using OpenClaw . Shared ;
1214using OpenClawTray . Helpers ;
1315using OpenClawTray . Services ;
1416using WinUIEx ;
1517using Windows . Foundation ;
1618using Windows . Storage . Streams ;
19+ using Windows . System ;
1720
1821namespace 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 ( ) ;
0 commit comments