Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/OpenClaw.Tray.WinUI/Windows/A2UICanvasWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,6 +18,7 @@
using WinUIEx;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
using Windows.System;

namespace OpenClawTray.Windows;

Expand Down Expand Up @@ -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; }

Expand All @@ -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
Expand All @@ -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; }
Expand Down Expand Up @@ -338,6 +359,30 @@ public string GetStateSnapshot()
return snapshot.ToJsonString();
}

// ── Fullscreen ──────────────────────────────────────────────────────────

/// <summary>Toggle borderless fullscreen on F11.</summary>
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
Expand Down
85 changes: 84 additions & 1 deletion src/OpenClaw.Tray.WinUI/Windows/CanvasWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
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;
using OpenClawTray.Services;
using WinUIEx;
using Windows.Foundation;
using Windows.Storage.Streams;
using Windows.System;

namespace OpenClawTray.Windows;

Expand All @@ -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<bool> _webViewReadyTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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) =>
Expand All @@ -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);
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -659,6 +718,30 @@ public void BringToFront(bool keepTopMost)
}
}

// ── Fullscreen ──────────────────────────────────────────────────────────

/// <summary>Toggle borderless fullscreen on F11.</summary>
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();
Expand Down