Skip to content
Merged
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
12 changes: 12 additions & 0 deletions docs/en/wework/developer-guide/wework-macos-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ Transparent window edges are composed differently across macOS versions and
graphics environments, which can expose the desktop or appear as translucent
or gray borders.

The root shell in the main React WebView must fill the current WebView through
CSS viewport constraints such as `fixed inset-0`. Do not listen for native
Tauri resize events and write asynchronously read and converted `innerSize` and
`scaleFactor` values back as inline root-shell dimensions. Native events, scale
factors, and WebView layout commits can arrive out of order, allowing a stale
inline size to override the current viewport and leave an unfilled region after
a resize. Native window dimensions are for window creation, restoration, and
system-level window management, not as a source for the React root layout.
On Linux, embedded browser child WebViews must live in an absolute host built
from `GtkOverlay` and `GtkFixed`; adding them directly to the main window's
`GtkBox` lets them participate in layout and compress the main React WebView.

The system drag panel and Popout Window are separate lightweight overlays and
are not covered by this rule. Changes to ordinary window backgrounds, title
bars, or creation options must verify both the main window and detached
Expand Down
8 changes: 8 additions & 0 deletions docs/zh/wework/developer-guide/wework-macos-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ Wework 主窗口和独立工作区窗口必须使用不透明的 Tauri 窗口,
或原生 vibrancy 材质;不同 macOS 版本和图形环境对透明窗口边缘的合成结果不一致,
可能显示为透出桌面、半透明描边或灰色边框。

主 React WebView 的根壳必须直接使用 CSS viewport 约束(例如 `fixed inset-0`)
填满当前 WebView。不要监听 Tauri 原生 resize 事件,再把异步读取和换算后的
`innerSize`、`scaleFactor` 写成根壳的内联宽高;原生事件、缩放因子和 WebView
布局提交可能乱序,旧的内联尺寸会覆盖当前 viewport,并在窗口调整后留下未填充区域。
原生窗口尺寸只用于窗口创建、恢复和系统级窗口管理,不作为 React 根布局的尺寸来源。
Linux 上的内置浏览器子 WebView 必须放在 `GtkOverlay` 和 `GtkFixed` 组成的绝对定位
宿主中,不能直接加入主窗口的 `GtkBox` 参与布局,否则会挤压主 React WebView。

系统拖拽面板和 Popout Window 是独立的轻量浮层,不受该约束。修改普通窗口的背景、
标题栏或创建参数时,需要同时验证主窗口和独立工作区窗口,并保留自动化断言,确保
两者不会重新启用原生透明效果。
Expand Down
36 changes: 35 additions & 1 deletion wework/src-tauri/src/embedded_browser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,21 @@ use serde_json::{json, Value};
use tauri::{
async_runtime::Mutex as AsyncMutex,
webview::{DownloadEvent, NewWindowResponse, PageLoadEvent},
Emitter, LogicalPosition, LogicalSize, Manager, Position, Rect, Size, Webview, WebviewUrl, Wry,
Emitter, LogicalPosition, LogicalSize, Manager, Webview, WebviewUrl, Wry,
};

#[cfg(target_os = "macos")]
use objc2_web_kit::WKWebView;
#[cfg(not(target_os = "linux"))]
use tauri::{Position, Rect, Size};

mod agent_control;
mod bridge_security;
mod bridge_server;
mod browser_runtime;
mod data_clearing;
#[cfg(target_os = "linux")]
mod linux_host;
mod local_file_preview;
mod popup;
mod screenshot;
Expand Down Expand Up @@ -328,6 +332,7 @@ struct NormalizedBounds {
}

impl NormalizedBounds {
#[cfg(not(target_os = "linux"))]
fn rect(&self) -> Rect {
Rect {
position: Position::Logical(self.position),
Expand All @@ -343,6 +348,12 @@ fn normalize_bounds(bounds: EmbeddedBrowserBounds) -> NormalizedBounds {
}
}

#[cfg(target_os = "linux")]
fn apply_webview_bounds(webview: &Webview<Wry>, bounds: NormalizedBounds) -> Result<(), String> {
linux_host::apply_bounds(webview, bounds.position, bounds.size)
}

#[cfg(not(target_os = "linux"))]
fn apply_webview_bounds(webview: &Webview<Wry>, bounds: NormalizedBounds) -> Result<(), String> {
webview
.set_bounds(bounds.rect())
Expand Down Expand Up @@ -1941,6 +1952,29 @@ pub async fn embedded_browser_open(
}
};

#[cfg(target_os = "linux")]
if let Err(error) = apply_webview_bounds(&webview, normalized_bounds) {
log_embedded_browser_diagnostic(
&state,
&label,
"open_host_failed",
json!({
"nativeLabel": &native_label,
"error": &error,
}),
);
if let Ok(mut webviews) = state.webviews.lock() {
remove_logical_entry_if_native_matches(
&mut webviews,
&label,
&native_label,
|current| current.native_label.as_str(),
);
}
let _ = webview.close();
return Err(error);
}

log_embedded_browser_diagnostic(
&state,
&label,
Expand Down
117 changes: 117 additions & 0 deletions wework/src-tauri/src/embedded_browser/linux_host.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use std::time::Duration;

use gtk::prelude::*;
use tauri::{LogicalPosition, LogicalSize, Webview, Wry};

const HOST_NAME: &str = "wework-embedded-browser-host";

fn find_host(container: &gtk::Box) -> Option<gtk::Fixed> {
container.children().into_iter().find_map(|child| {
let overlay = child.downcast::<gtk::Overlay>().ok()?;
overlay.children().into_iter().find_map(|overlay_child| {
if overlay_child.widget_name() != HOST_NAME {
return None;
}
overlay_child.downcast::<gtk::Fixed>().ok()
})
})
}

fn create_host(
container: &gtk::Box,
embedded_webview: &webkit2gtk::WebView,
) -> Result<gtk::Fixed, String> {
let embedded_widget = embedded_webview.clone().upcast::<gtk::Widget>();
let (main_webview_index, main_webview) = container
.children()
.into_iter()
.enumerate()
.find_map(|(index, child)| {
if child == embedded_widget {
return None;
}
child
.downcast::<webkit2gtk::WebView>()
.ok()
.map(|webview| (index, webview))
})
.ok_or_else(|| "Main webview was not found in the GTK window container".to_string())?;

container.remove(&main_webview);

let overlay = gtk::Overlay::new();
overlay.set_hexpand(true);
overlay.set_vexpand(true);
overlay.add(&main_webview);

let host = gtk::Fixed::new();
host.set_widget_name(HOST_NAME);
host.set_hexpand(true);
host.set_vexpand(true);
host.set_halign(gtk::Align::Fill);
host.set_valign(gtk::Align::Fill);
overlay.add_overlay(&host);

container.pack_start(&overlay, true, true, 0);
container.reorder_child(&overlay, main_webview_index as i32);
main_webview.show();
host.show();
overlay.show();

Ok(host)
}

fn place_webview(
embedded_webview: webkit2gtk::WebView,
position: LogicalPosition<f64>,
size: LogicalSize<f64>,
) -> Result<(), String> {
let host = match embedded_webview
.parent()
.and_then(|parent| parent.downcast::<gtk::Fixed>().ok())
{
Some(host) => host,
None => {
let container = embedded_webview
.parent()
.and_then(|parent| parent.downcast::<gtk::Box>().ok())
.ok_or_else(|| {
"Embedded browser webview is not attached to the GTK window container"
.to_string()
})?;
let host = find_host(&container)
.map(Ok)
.unwrap_or_else(|| create_host(&container, &embedded_webview))?;
container.remove(&embedded_webview);
host.put(&embedded_webview, 0, 0);
host
}
};

embedded_webview.set_size_request(size.width.round() as i32, size.height.round() as i32);
host.move_(
&embedded_webview,
position.x.round() as i32,
position.y.round() as i32,
);
Ok(())
}

pub fn apply_bounds(
webview: &Webview<Wry>,
position: LogicalPosition<f64>,
size: LogicalSize<f64>,
) -> Result<(), String> {
// Tauri packs Linux child webviews into the main GtkBox, so their requested
// overlay bounds are ignored and they can collapse the app webview. Move
// them into an absolute overlay host before exposing them to the frontend.
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
webview
.with_webview(move |platform_webview| {
let _ = sender.send(place_webview(platform_webview.inner(), position, size));
})
.map_err(|error| format!("Failed to access embedded browser webview: {error}"))?;
receiver
.recv_timeout(Duration::from_secs(5))
.map_err(|error| format!("Timed out positioning embedded browser webview: {error}"))?
}
3 changes: 3 additions & 0 deletions wework/src/App.plugins.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1737,6 +1737,9 @@ describe('App plugins route', () => {
expect(screen.getByTestId('macos-traffic-light-spacer')).toBeInTheDocument()
expect(screen.getByTestId('plugins-page-content')).toHaveClass('md:pl-6')
expect(screen.getByTestId('plugins-page-content').style.paddingLeft).toBe('')
expect(screen.getByTestId('app-shell')).toHaveClass('fixed', 'inset-0')
expect(screen.getByTestId('app-shell').style.width).toBe('')
expect(screen.getByTestId('app-shell').style.height).toBe('')
})

test('uses the mobile shell for plugins route at the shared mobile breakpoint', async () => {
Expand Down
11 changes: 0 additions & 11 deletions wework/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ import { TelemetryBridge } from '@/telemetry/TelemetryBridge'
import { track, useTelemetryEnabled } from '@/telemetry/client'
import { WorkspaceTabPortalOwner } from '@/components/topnav/TitlebarActionsPortal'
import { setActiveWorkspaceTabPortalOwner } from '@/components/topnav/workspaceTabPortalOwnership'
import { useTauriViewportSize } from '@/hooks/useTauriViewportSize'

const WORKBENCH_STARTUP_REVEAL_TIMEOUT_MS = 6000
const POPOUT_WINDOW_LABEL = 'popout-window'
Expand Down Expand Up @@ -450,7 +449,6 @@ function AppShell() {
}
const { activeAppKey, navigateToApp } = useChromeTabs(path)
const isTauri = isTauriRuntime()
const tauriViewportSize = useTauriViewportSize(isTauri)
const isPopoutWindow = isPopoutWindowRuntime()
const isWorkspaceWindow = isTauri && getCurrentWindow().label?.startsWith('workspace-') === true
const titlebarOverlaysContent = false
Expand Down Expand Up @@ -666,7 +664,6 @@ function AppShell() {
>
<div
data-testid="app-shell"
data-tauri-viewport-height={tauriViewportSize?.height}
className={cn(
isTauri ? 'fixed inset-0' : 'h-dvh',
isPopoutWindow
Expand All @@ -676,14 +673,6 @@ function AppShell() {
: 'overflow-hidden bg-surface',
titlebarOverlaysContent ? 'relative' : 'flex flex-col'
)}
style={
tauriViewportSize
? {
width: `${tauriViewportSize.width}px`,
height: `${tauriViewportSize.height}px`,
}
: undefined
}
>
{showChromeTitlebar && (
<ChromeTitlebar
Expand Down
73 changes: 0 additions & 73 deletions wework/src/App.viewport.test.tsx

This file was deleted.

Loading
Loading