mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
Merge pull request #2 from pcastro93/wayland-csd-shadow
fix(linux): draw GTK's CSD shadow on frameless windows under Wayland
This commit is contained in:
Generated
+1
@@ -6152,6 +6152,7 @@ dependencies = [
|
||||
"block2 0.6.2",
|
||||
"dirs 6.0.0",
|
||||
"dunce",
|
||||
"gtk",
|
||||
"ide-agent",
|
||||
"ide-analysis",
|
||||
"ide-build",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<bool> = 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<tauri::PhysicalSize<u32>> {
|
||||
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::<u32>(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<Frame> {
|
||||
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::Wry> {
|
||||
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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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::<f64>(scale);
|
||||
let own_scale = window.scale_factor()?;
|
||||
let own = window.outer_size()?.to_logical::<f64>(own_scale);
|
||||
let inner = window.inner_size()?.to_logical::<f64>(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::<f64>(own_scale);
|
||||
let frame = (own.width - inner.width, own.height - inner.height);
|
||||
let bounds = fit_bounds(
|
||||
(pos.x, pos.y, size.width, size.height),
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<PlatformInfo>(() => {
|
||||
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 */
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user