From 795f8e85f1598ce1c7f4a35e60db9df7e63b27ef Mon Sep 17 00:00:00 2001 From: arookieofc <2128194521@qq.com> Date: Sun, 1 Nov 2026 02:25:48 +0800 Subject: [PATCH 1/6] fix(windows): de-parent settings window on Windows so DWM paints its shadow An owned child (WS_POPUP) of a borderless parent gets no DWM shadow, leaving the frameless settings window flat against its project window. Ownership is already tracked by the settings- label (parent-close cleanup in on_window_event, menu modality), so make the settings window top-level on Windows; macOS and Linux keep the native parent link. --- src-tauri/src/windows.rs | 10 +++++++--- src/features/settings/window/bridge.ts | 6 ++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/windows.rs b/src-tauri/src/windows.rs index b9d14c122..a47340ade 100644 --- a/src-tauri/src/windows.rs +++ b/src-tauri/src/windows.rs @@ -796,9 +796,13 @@ pub fn open_settings_window( #[cfg(target_os = "linux")] crate::linux_window_state::prepare(app, &mut config); let builder = WebviewWindowBuilder::from_config(app, &config); - // AppKit child windows can disappear when dragged to another display's Space. - // The settings label already tracks ownership and parent-close cleanup. - #[cfg(not(target_os = "macos"))] + // Windows: an owned child (WS_POPUP) of a borderless parent gets no DWM + // shadow, leaving the frameless settings window flat against its parent. + // Ownership is tracked by the `settings-` label instead + // (parent-close cleanup, menu modality), so only Linux keeps the native + // parent link. AppKit child windows can disappear when dragged to another + // display's Space, so macOS keeps it too. + #[cfg(target_os = "linux")] let builder = builder.and_then(|b| b.parent(&parent)); let window = builder .and_then(|b| dev_font_preferences(b).build()) diff --git a/src/features/settings/window/bridge.ts b/src/features/settings/window/bridge.ts index 3aa202099..e78722cf2 100644 --- a/src/features/settings/window/bridge.ts +++ b/src/features/settings/window/bridge.ts @@ -1,8 +1,10 @@ /** * Settings window ↔ its project window. * - * Settings live in a native child window `settings-` (see - * src-tauri/src/windows.rs) that only runs the settings pages. The action + * Settings live in a separate window `settings-` (see + * src-tauri/src/windows.rs) that only runs the settings pages (a native + * child of the parent on macOS/Linux; top-level on Windows so DWM paints + * its shadow). The action * registry stays in the project window: it sends the settings window a * description of every action (Keymap page, links that run actions) and runs * the actions the settings window forwards. From 58ff3e93d04dbce5091ff2b9984147086d9a4ed7 Mon Sep 17 00:00:00 2001 From: arookieofc <2128194521@qq.com> Date: Mon, 2 Nov 2026 19:04:25 +0800 Subject: [PATCH 2/6] fix(linux): paint a hairline edge on the frameless settings window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Linux compositors draw no shadow for undecorated windows (Wayland has no server-side decorations at all), so the frameless settings window is indistinguishable from the desktop there — #595's follow-up after the Windows DWM-shadow fix. Paint a 1px --border-window inset ring around the window root: the webview draws the same hairline on every windowing system. --- .../settings/__tests__/dialog.test.tsx | 20 +++++++++++++++++++ .../settings/dialog/SettingsDialog.module.css | 5 +++++ .../settings/window/SettingsWindow.tsx | 6 ++++-- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/features/settings/__tests__/dialog.test.tsx b/src/features/settings/__tests__/dialog.test.tsx index a72f47525..a187bf413 100644 --- a/src/features/settings/__tests__/dialog.test.tsx +++ b/src/features/settings/__tests__/dialog.test.tsx @@ -22,6 +22,7 @@ vi.mock("@/platform/ipc", async (importOriginal) => { }); import { registerAction, executeAction } from "@/platform/actions"; +import { usePlatform } from "@/platform/platformInfo"; import { useProject } from "@/platform/project"; import { useKeymap } from "@/platform/keymap"; import { ContextMenuHost } from "@/ui"; @@ -29,6 +30,8 @@ import { PageContext, useSetting } from "../components/context"; import { DEFAULT_SETTINGS, setAt } from "../model/schema"; import { openSettings, stage, useSettingsDialog } from "../model/staging"; import { SettingsDialog } from "../dialog/SettingsDialog"; +import { SettingsWindow } from "../window/SettingsWindow"; +import dialogStyles from "../dialog/SettingsDialog.module.css"; import { activate } from ".."; import { MessagesHost } from "@/workbench/messages"; @@ -471,6 +474,23 @@ describe("Settings dialog", () => { }); }); +describe("native Settings window root", () => { + it("marks the window edge on Linux, whose frameless windows get no compositor shadow", () => { + const saved = usePlatform.getState(); + usePlatform.setState({ os: "linux" }); + let view = render(); + let root = document.querySelector(`.${dialogStyles.window}`); + expect(root).toHaveClass(dialogStyles.windowEdge!); + view.unmount(); + usePlatform.setState({ os: "windows" }); + view = render(); + root = document.querySelector(`.${dialogStyles.window}`); + expect(root).not.toHaveClass(dialogStyles.windowEdge!); + view.unmount(); + usePlatform.setState(saved); + }); +}); + describe("native Settings window background", () => { it.each([ ["a hex frame", "#26282c", "", "#26282c"], diff --git a/src/features/settings/dialog/SettingsDialog.module.css b/src/features/settings/dialog/SettingsDialog.module.css index 24fdea935..0a1105abb 100644 --- a/src/features/settings/dialog/SettingsDialog.module.css +++ b/src/features/settings/dialog/SettingsDialog.module.css @@ -203,6 +203,11 @@ background: var(--bg-dialog, var(--bg-elev)); color: var(--fg); } +/* Linux draws no shadow for a frameless window (Wayland/KDE give undecorated + windows no CSD), so mark its edge in CSS instead. */ +.windowEdge { + box-shadow: inset 0 0 0 1px var(--border-window); +} .windowBody { flex: 1; min-height: 0; diff --git a/src/features/settings/window/SettingsWindow.tsx b/src/features/settings/window/SettingsWindow.tsx index e55094905..231a60640 100644 --- a/src/features/settings/window/SettingsWindow.tsx +++ b/src/features/settings/window/SettingsWindow.tsx @@ -1,4 +1,5 @@ /** Content of the native Settings window: title bar, page tree + page, buttons. */ +import clsx from "clsx"; import { useEffect, useRef } from "react"; import { OverlayHost } from "@/workbench/OverlayHost"; import { usePlatform } from "@/platform/platformInfo"; @@ -10,6 +11,7 @@ import styles from "../dialog/SettingsDialog.module.css"; export function SettingsWindow() { useApplyTheme(); + const os = usePlatform((s) => s.os); const ownTitleBar = usePlatform((s) => s.overlayTitleBar || s.framelessTitleBar); const open = useSettingsDialog((s) => s.open); const title = useSettingsTitle(); @@ -20,8 +22,8 @@ export function SettingsWindow() { }, [open]); return ( -
- {/* macOS overlays traffic lights on it; Windows is frameless. Linux keeps the OS title bar. */} +
+ {/* macOS overlays traffic lights on it; Windows and Linux are frameless (tauri..conf.json). */} {ownTitleBar && } {open && ( <> From bbb1be69a2d07c6d09c21609d28be03d04b1806f Mon Sep 17 00:00:00 2001 From: arookieofc <2128194521@qq.com> Date: Tue, 3 Nov 2026 07:05:16 +0800 Subject: [PATCH 3/6] fix(linux): make the frameless settings window edge readable #595 follow-up: the 1px --border-window hairline is nearly invisible on the dark theme (#40434a against a dark desktop), which fuqiuluo confirmed on Fedora 44 GNOME Wayland after the first edge commit. Blend the foreground tone into the ring so the boundary reads on every theme; still CSS-only, since Wayland gives undecorated windows no compositor shadow to paint outside the surface. --- src/features/settings/dialog/SettingsDialog.module.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/features/settings/dialog/SettingsDialog.module.css b/src/features/settings/dialog/SettingsDialog.module.css index 0a1105abb..a9ab42928 100644 --- a/src/features/settings/dialog/SettingsDialog.module.css +++ b/src/features/settings/dialog/SettingsDialog.module.css @@ -204,9 +204,10 @@ color: var(--fg); } /* Linux draws no shadow for a frameless window (Wayland/KDE give undecorated - windows no CSD), so mark its edge in CSS instead. */ + windows no CSD), so mark its edge in CSS instead. --border-window alone is + near-invisible on the dark theme, so blend the foreground tone into it. */ .windowEdge { - box-shadow: inset 0 0 0 1px var(--border-window); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 32%, var(--border-window)); } .windowBody { flex: 1; From fd8161a5a70f2abd322184f2cc18c126b07dd43c Mon Sep 17 00:00:00 2001 From: fuqiuluo Date: Tue, 3 Nov 2026 07:15:24 +0800 Subject: [PATCH 4/6] Revert "fix(linux): make the frameless settings window edge readable" This reverts commit bbb1be69a2d07c6d09c21609d28be03d04b1806f. --- src/features/settings/dialog/SettingsDialog.module.css | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/features/settings/dialog/SettingsDialog.module.css b/src/features/settings/dialog/SettingsDialog.module.css index a9ab42928..0a1105abb 100644 --- a/src/features/settings/dialog/SettingsDialog.module.css +++ b/src/features/settings/dialog/SettingsDialog.module.css @@ -204,10 +204,9 @@ color: var(--fg); } /* Linux draws no shadow for a frameless window (Wayland/KDE give undecorated - windows no CSD), so mark its edge in CSS instead. --border-window alone is - near-invisible on the dark theme, so blend the foreground tone into it. */ + windows no CSD), so mark its edge in CSS instead. */ .windowEdge { - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 32%, var(--border-window)); + box-shadow: inset 0 0 0 1px var(--border-window); } .windowBody { flex: 1; From d830d21d466801d3affc10fb423cf4c03d150620 Mon Sep 17 00:00:00 2001 From: fuqiuluo Date: Tue, 3 Nov 2026 07:20:03 +0800 Subject: [PATCH 5/6] fix(linux): keep the settings edge above content --- src/features/settings/dialog/SettingsDialog.module.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/features/settings/dialog/SettingsDialog.module.css b/src/features/settings/dialog/SettingsDialog.module.css index 0a1105abb..7661a3a67 100644 --- a/src/features/settings/dialog/SettingsDialog.module.css +++ b/src/features/settings/dialog/SettingsDialog.module.css @@ -206,6 +206,14 @@ /* Linux draws no shadow for a frameless window (Wayland/KDE give undecorated windows no CSD), so mark its edge in CSS instead. */ .windowEdge { + position: relative; +} +.windowEdge::after { + content: ""; + position: absolute; + inset: 0; + z-index: 1; + pointer-events: none; box-shadow: inset 0 0 0 1px var(--border-window); } .windowBody { From ec5302d8385da9c68f13975c8733165818374a4d Mon Sep 17 00:00:00 2001 From: Piettro Castro Date: Tue, 3 Nov 2026 00:14:14 -0500 Subject: [PATCH 6/6] fix(linux): draw GTK's CSD shadow on frameless windows under Wayland tao makes frameless Wayland windows client-side decorated (an empty GTK title bar, so the compositor adds no frame) but leaves them undecorated, so GTK draws no shadow or resize margin and dark windows blend into whatever is behind them. Decorating the window after build keeps that empty title bar: GTK adds the theme's shadow and resize edges, nothing else. Applies to project windows and the Settings window; X11 is unchanged (decorating there would bring back the WM frame). - Detect Wayland from GDK's backend (tao's own test), not env vars. - Measure windows by GTK's content size, which excludes the shadow margin, so remembered geometry and Settings placement don't grow. - Skip the in-page resize handles on Wayland; GTK provides the edges. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 1 + src-tauri/Cargo.toml | 4 ++ src-tauri/src/commands/app.rs | 6 +++ src-tauri/src/linux_window_state.rs | 42 ++++++++++++++++++- src-tauri/src/window_groups.rs | 4 ++ src-tauri/src/windows.rs | 22 +++++++++- src/bindings.ts | 2 + .../settings/dialog/SettingsDialog.module.css | 4 +- src/platform/platformInfo.ts | 4 ++ src/workbench/WindowResizeHandles.tsx | 12 +++--- 10 files changed, 91 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5275a63ff..91fb0a947 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6152,6 +6152,7 @@ dependencies = [ "block2 0.6.2", "dirs 6.0.0", "dunce", + "gtk", "ide-agent", "ide-analysis", "ide-build", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ab942c551..633a43b9e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -79,6 +79,10 @@ objc2-foundation = { version = "0.3", default-features = false, features = ["std # Used only by the debug font-restriction override; Cargo dependency cfg cannot test debug_assertions. objc2-web-kit = { version = "0.3", default-features = false, features = ["std", "WKWebViewConfiguration", "WKPreferences"] } +[target.'cfg(target_os = "linux")'.dependencies] +# Wayland detection and the client size without GTK's CSD shadow (linux_window_state.rs). +gtk = "0.18" + [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61", features = [ "Win32_Foundation", diff --git a/src-tauri/src/commands/app.rs b/src-tauri/src/commands/app.rs index e68485c61..e383c9fa4 100644 --- a/src-tauri/src/commands/app.rs +++ b/src-tauri/src/commands/app.rs @@ -15,6 +15,8 @@ pub struct AppInfo { pub version: String, pub platform: String, pub config_dir: ide_core::IpcPath, + /// Linux on a Wayland display: GTK draws the frameless windows' shadow and resize edges. + pub wayland: bool, } pub use ide_host::ops::project::ProjectInfo; @@ -27,6 +29,10 @@ pub fn app_info(state: State<'_, AppState>) -> AppInfo { version: env!("CARGO_PKG_VERSION").into(), platform: std::env::consts::OS.into(), config_dir: state.config_dir.clone().into(), + #[cfg(target_os = "linux")] + wayland: crate::linux_window_state::is_wayland(), + #[cfg(not(target_os = "linux"))] + wayland: false, } } diff --git a/src-tauri/src/linux_window_state.rs b/src-tauri/src/linux_window_state.rs index 3d9816dce..a05a6f7e6 100644 --- a/src-tauri/src/linux_window_state.rs +++ b/src-tauri/src/linux_window_state.rs @@ -14,6 +14,7 @@ mod runtime { use super::*; use parking_lot::Mutex; use std::collections::HashMap; + use std::sync::OnceLock; use tauri::{AppHandle, Listener, Manager, Window, WindowEvent, plugin::TauriPlugin}; const FILE: &str = tauri_plugin_window_state::DEFAULT_FILENAME; @@ -63,8 +64,42 @@ mod runtime { visible: bool, } + static WAYLAND: OnceLock = OnceLock::new(); + + /// GDK's backend, as tao tests it (`GDK_BACKEND=x11` under a Wayland session is X11). + /// Known once the plugin is set up, before any window exists. + pub fn is_wayland() -> bool { + WAYLAND.get().copied().unwrap_or(false) + } + + /// The client size that `set_size` (`gtk_window_resize`) takes. On Wayland, decorated windows + /// get a GTK CSD shadow margin that tao's inner and outer sizes (the configure size) include. + pub fn content_size(window: &Window) -> tauri::Result> { + let inner = window.inner_size()?; + if !is_wayland() { + return Ok(inner); + } + // GTK objects belong to the main thread. `gtk_window_get_size` follows the configure + // synchronously; the webview's allocation only catches up on the next layout. + let (tx, rx) = std::sync::mpsc::sync_channel(1); + let cloned = window.clone(); + window.run_on_main_thread(move || { + let sizes = cloned.gtk_window().ok().zip(cloned.inner_size().ok()).zip(cloned.scale_factor().ok()); + let _ = tx.send(sizes.map(|((gtk, inner), scale)| { + let (width, height) = gtk::prelude::GtkWindowExt::size(>k); + let content = tauri::LogicalSize::new(width.max(0) as u32, height.max(0) as u32); + (content.to_physical::(scale), inner) + })); + })?; + let Some((content, inner)) = rx.recv().ok().flatten() else { return Ok(inner) }; + // An unmapped window reports its size request; never exceed the configure size. + let usable = + content.width > 1 && content.height > 1 && content.width <= inner.width && content.height <= inner.height; + Ok(if usable { content } else { inner }) + } + fn frame(window: &Window) -> Option { - let size = window.inner_size().ok()?; + let size = content_size(window).ok()?; Some(Frame { visible: window.is_visible().ok()?, size: Size(size.width, size.height), @@ -381,6 +416,9 @@ mod runtime { pub fn plugin() -> TauriPlugin { tauri::plugin::Builder::new("linux-window-state") .setup(|app, _| { + // Plugins are set up on the main thread once GTK is initialised, before any window exists. + use gtk::gdk::prelude::DisplayExtManual; + let _ = WAYLAND.set(gtk::gdk::Display::default().is_some_and(|d| d.backend().is_wayland())); let saved = app .path() .app_config_dir() @@ -466,7 +504,7 @@ mod runtime { } #[cfg(target_os = "linux")] -pub use runtime::{normal_size_requested, plugin, prepare, save_before_close}; +pub use runtime::{content_size, is_wayland, normal_size_requested, plugin, prepare, save_before_close}; #[cfg(test)] mod tests { diff --git a/src-tauri/src/window_groups.rs b/src-tauri/src/window_groups.rs index fef00fa14..64344e0d8 100644 --- a/src-tauri/src/window_groups.rs +++ b/src-tauri/src/window_groups.rs @@ -65,6 +65,10 @@ fn copy_frame(from: &WebviewWindow, to: &WebviewWindow) -> IdeResult<()> { if !maximized { to.unmaximize().map_err(err)?; to.set_position(from.outer_position().map_err(err)?).map_err(err)?; + // `set_size` excludes GTK's CSD shadow, which tao's inner size includes on Wayland. + #[cfg(target_os = "linux")] + let size = crate::linux_window_state::content_size(&from.as_ref().window()).map_err(err)?; + #[cfg(not(target_os = "linux"))] let size = from.inner_size().map_err(err)?; #[cfg(target_os = "linux")] crate::linux_window_state::normal_size_requested(to.app_handle(), to.label(), size); diff --git a/src-tauri/src/windows.rs b/src-tauri/src/windows.rs index a47340ade..14e84c208 100644 --- a/src-tauri/src/windows.rs +++ b/src-tauri/src/windows.rs @@ -303,6 +303,8 @@ fn build_project_window(app: &AppHandle, mut config: WindowConfig) -> IdeResult< let window = WebviewWindowBuilder::from_config(app, &config) .and_then(|b| dev_font_preferences(b).build()) .map_err(|e| IdeError::internal(format!("cannot create window: {e}")))?; + #[cfg(target_os = "linux")] + decorate_csd(&window); apply_zoom(app, &window); #[cfg(target_os = "macos")] align_titlebar(app, &window); @@ -325,6 +327,17 @@ fn build_project_window(app: &AppHandle, mut config: WindowConfig) -> IdeResult< Ok(window) } +/// Wayland: tao already makes frameless windows client-side decorated (an empty GTK title bar, so +/// the compositor adds no frame), but undecorated GTK windows get no shadow or resize margin. +/// Decorating keeps that empty title bar, so GTK adds the theme's shadow and resize edges and +/// nothing else. X11 has no such title bar: decorating there would bring back the WM frame. +#[cfg(target_os = "linux")] +fn decorate_csd(window: &WebviewWindow) { + if crate::linux_window_state::is_wayland() { + let _ = window.set_decorations(true); + } +} + /// Sets the saved IDE zoom before the page loads; applied later by the frontend, /// it would visibly scale up the splash. fn apply_zoom(app: &AppHandle, window: &WebviewWindow) { @@ -807,6 +820,8 @@ pub fn open_settings_window( let window = builder .and_then(|b| dev_font_preferences(b).build()) .map_err(|e| IdeError::internal(format!("cannot create settings window: {e}")))?; + #[cfg(target_os = "linux")] + decorate_csd(&window); apply_zoom(app, &window); #[cfg(target_os = "macos")] align_titlebar(app, &window); @@ -887,7 +902,12 @@ pub(super) fn place_settings_window(window: &WebviewWindow, parent: &WebviewWind let size = parent.outer_size()?.to_logical::(scale); let own_scale = window.scale_factor()?; let own = window.outer_size()?.to_logical::(own_scale); - let inner = window.inner_size()?.to_logical::(own_scale); + // On Wayland, the frame is GTK's CSD shadow margin, which tao's inner size includes. + #[cfg(target_os = "linux")] + let inner = crate::linux_window_state::content_size(&window.as_ref().window())?; + #[cfg(not(target_os = "linux"))] + let inner = window.inner_size()?; + let inner = inner.to_logical::(own_scale); let frame = (own.width - inner.width, own.height - inner.height); let bounds = fit_bounds( (pos.x, pos.y, size.width, size.height), diff --git a/src/bindings.ts b/src/bindings.ts index 7c4a2d955..eed10ae62 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -1379,6 +1379,8 @@ export type AppInfo = { version: string, platform: string, configDir: IpcPath, + /** Linux on a Wayland display: GTK draws the frameless windows' shadow and resize edges. */ + wayland: boolean, }; export type AppMetrics = { diff --git a/src/features/settings/dialog/SettingsDialog.module.css b/src/features/settings/dialog/SettingsDialog.module.css index 7661a3a67..d7bad0428 100644 --- a/src/features/settings/dialog/SettingsDialog.module.css +++ b/src/features/settings/dialog/SettingsDialog.module.css @@ -203,8 +203,8 @@ background: var(--bg-dialog, var(--bg-elev)); color: var(--fg); } -/* Linux draws no shadow for a frameless window (Wayland/KDE give undecorated - windows no CSD), so mark its edge in CSS instead. */ +/* Linux: on Wayland GTK draws the frameless window's shadow (`decorate_csd` in + windows.rs), but frameless X11 windows have none, so mark the edge in CSS. */ .windowEdge { position: relative; } diff --git a/src/platform/platformInfo.ts b/src/platform/platformInfo.ts index 230a34bdd..95846480c 100644 --- a/src/platform/platformInfo.ts +++ b/src/platform/platformInfo.ts @@ -9,6 +9,8 @@ interface PlatformInfo { overlayTitleBar: boolean; /** Frameless window: the title bar draws the main menu and caption buttons (Tauri, Windows and Linux). */ framelessTitleBar: boolean; + /** Linux on a Wayland display: GTK draws the frameless windows' shadow and resize edges. */ + wayland: boolean; } /** OS from the user agent (before the backend answers, or in a browser). "darwin" must not read as Windows. */ @@ -28,6 +30,7 @@ export const usePlatform = create(() => { version: null, overlayTitleBar: isTauri() && os === "macos", framelessTitleBar: isTauri() && (os === "windows" || os === "linux"), + wayland: false, }; }); @@ -40,6 +43,7 @@ export async function loadPlatformInfo() { version: info.version, overlayTitleBar: info.platform === "macos", framelessTitleBar: info.platform === "windows" || info.platform === "linux", + wayland: info.wayland, }); } catch { /* keep the user-agent guess */ diff --git a/src/workbench/WindowResizeHandles.tsx b/src/workbench/WindowResizeHandles.tsx index 0b5fa9da6..dbaf4e8f3 100644 --- a/src/workbench/WindowResizeHandles.tsx +++ b/src/workbench/WindowResizeHandles.tsx @@ -1,11 +1,12 @@ /** - * Frameless-window resize handles: Windows gives undecorated windows no - * resize borders, so the title bar's frameless look leaves the window - * stuck at its launch size. Eight invisible hit areas along the edges + * Frameless-window resize handles: Windows and Linux on X11 give undecorated + * windows no resize borders, so the title bar's frameless look leaves the + * window stuck at its launch size. Eight invisible hit areas along the edges * start a native resize drag, like any other desktop app. * * macOS renders its own overlay title bar but still provides resize - * handles, so nothing is added there. + * handles, and on Wayland GTK draws resize edges in the client-side shadow + * (`decorate_csd` in windows.rs), so nothing is added there. */ import { useEffect, useState } from "react"; import { usePlatform } from "@/platform/platformInfo"; @@ -33,7 +34,8 @@ async function start(dir: Dir) { export function WindowResizeHandles() { // A frameless window needs them; a decorated one already has native borders. const frameless = usePlatform((s) => s.framelessTitleBar); - const enabled = isTauri() && frameless; + const gtkEdges = usePlatform((s) => s.os === "linux" && s.wayland); + const enabled = isTauri() && frameless && !gtkEdges; // A maximized or full-screen window has nothing to resize; the edges belong to the content. const [sized, setSized] = useState(false); useEffect(() => {