feat(apprt-gtk): port the GTK4/libadwaita app runtime
Ports ZIG/src/apprt/gtk into ghostty-apprt-gtk: every GObject class (application, window, tab, split tree, surface + scrolled window, child-exited banner, search/key-state/resize overlays, command palette, global shortcuts, all dialogs, config wrapper) as glib subclasses with the same GType names, properties, signals and template children; GDK key translation; X11 and Wayland window-protocol helpers (blur, CSD, quick terminal via gtk4-layer-shell); single-instance D-Bus IPC; gsettings; cgroup scope creation. build.rs compiles Ghostty's blueprints with blueprint-compiler and bundles UI, CSS and icons into a gresource, as Ghostty's build does. The core is not ported yet, so the crate defines the boundary it needs in src/core.rs (CoreApp, CoreSurface, RtApp, RtSurface, Waker); the spec is in docs/rewrite/notes/ghostty-apprt-gtk.md. ghostty-core will implement these traits (the crate no longer depends on core to avoid a cycle). examples/demo.rs runs the app with a dummy core that clears the GLArea; it launches on Wayland without GTK criticals. Stubs/open items: toggle_visibility, float_window, secure_input and close_all_windows return false; inspector is a no-op; X11/Wayland protocol code is untested against real compositors; gtk4-rs doesn't expose the GLArea framebuffer id, so the renderer must query it. Also registers ghostty-apprt and ghostty-cli in workspace.dependencies. Test Plan: - cargo test -p ghostty-apprt-gtk (36 tests) - DEMO_QUIT_AFTER_MS=3000 cargo run -p ghostty-apprt-gtk --example demo
This commit is contained in:
1 parent
72434a1255
commit
88cc13871c
80 files changed
+17271
-4
No files matched your search
@@ -0,0 +1,208 @@
|
||||
//! Port of apprt/gtk/winproto.zig: abstraction over the windowing protocol
|
||||
//! (X11 / Wayland / none) in use by the GDK display.
|
||||
//!
|
||||
//! * [`App`] is application-wide protocol state (one per application).
|
||||
//! * [`Window`] is per-toplevel protocol state.
|
||||
//!
|
||||
//! Zig's tagged unions over `noop`/`x11`/`wayland` become Rust enums. The Zig
|
||||
//! code held a `*ApprtWindow` pointer; here the window GObject implements
|
||||
//! [`WindowHost`] (a small, weak-referencing adapter), see below.
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gtk::gdk;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use ghostty_config::config::Config;
|
||||
use ghostty_config::types_b::WindowDecoration;
|
||||
use ghostty_input::Mods;
|
||||
|
||||
pub mod noop;
|
||||
pub mod wayland;
|
||||
pub mod x11;
|
||||
|
||||
/// Errors from the winproto backends.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("window surface is not an X11 surface")]
|
||||
NotX11Surface,
|
||||
#[error("window surface is not a Wayland surface")]
|
||||
NotWaylandSurface,
|
||||
#[error("no Wayland display/surface available")]
|
||||
NoWaylandDisplay,
|
||||
#[error("error initializing the XKB extension")]
|
||||
XkbInitialization,
|
||||
#[error("X11 request failed")]
|
||||
RequestFailed,
|
||||
#[error("X11 property not found")]
|
||||
PropertyNotFound,
|
||||
#[error("X11 property type mismatch")]
|
||||
PropertyTypeMismatch,
|
||||
#[error("X11 property format mismatch")]
|
||||
PropertyFormatMismatch,
|
||||
#[error("wayland: {0}")]
|
||||
Wayland(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// What the winproto backends need from the Ghostty window GObject
|
||||
/// (Zig: `ApprtWindow`). Implemented by `class::window` using a weak
|
||||
/// reference to the window (so winproto state, which the window owns, does not
|
||||
/// create a reference cycle).
|
||||
pub trait WindowHost {
|
||||
/// `apprt_window.as(gtk.Window)` (also a `gtk::Native`).
|
||||
fn gtk_window(&self) -> Option<gtk::Window>;
|
||||
/// `apprt_window.isQuickTerminal()`.
|
||||
fn is_quick_terminal(&self) -> bool;
|
||||
/// `apprt_window.getWindowDecoration()`.
|
||||
fn window_decoration(&self) -> WindowDecoration;
|
||||
/// `apprt_window.getConfig()?.get()`.
|
||||
fn config(&self) -> Option<Arc<Config>>;
|
||||
}
|
||||
|
||||
/// Which protocol is active (Zig: `winproto.Protocol`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Protocol {
|
||||
None,
|
||||
Wayland,
|
||||
X11,
|
||||
}
|
||||
|
||||
/// App-state for the underlying windowing protocol.
|
||||
pub enum App {
|
||||
None(noop::App),
|
||||
Wayland(wayland::App),
|
||||
X11(x11::App),
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Probe wayland, then x11, falling back to `None` (noop).
|
||||
pub fn init(gdk_display: &gdk::Display, app_id: &str, config: &Config) -> Result<App> {
|
||||
if let Some(v) = wayland::App::init(gdk_display, app_id, config)? {
|
||||
return Ok(App::Wayland(v));
|
||||
}
|
||||
if let Some(v) = x11::App::init(gdk_display, app_id, config)? {
|
||||
return Ok(App::X11(v));
|
||||
}
|
||||
Ok(App::None(noop::App))
|
||||
}
|
||||
|
||||
pub fn protocol(&self) -> Protocol {
|
||||
match self {
|
||||
App::None(_) => Protocol::None,
|
||||
App::Wayland(_) => Protocol::Wayland,
|
||||
App::X11(_) => Protocol::X11,
|
||||
}
|
||||
}
|
||||
|
||||
/// Modifier state for a key event, falling back to the translation of
|
||||
/// the GDK modifiers (`key.translateMods`).
|
||||
pub fn event_mods(&self, device: Option<&gdk::Device>, gtk_mods: gdk::ModifierType) -> Mods {
|
||||
let v = match self {
|
||||
App::None(v) => v.event_mods(device, gtk_mods),
|
||||
App::Wayland(v) => v.event_mods(device, gtk_mods),
|
||||
App::X11(v) => v.event_mods(device, gtk_mods),
|
||||
};
|
||||
v.unwrap_or_else(|| crate::key::translate_mods(gtk_mods))
|
||||
}
|
||||
|
||||
pub fn supports_quick_terminal(&self) -> bool {
|
||||
match self {
|
||||
App::None(v) => v.supports_quick_terminal(),
|
||||
App::Wayland(v) => v.supports_quick_terminal(),
|
||||
App::X11(v) => v.supports_quick_terminal(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set up necessary support for the quick terminal that must occur
|
||||
/// *before* the window-level winproto object is created (and before the
|
||||
/// window is realized). Only has an effect on Wayland, where
|
||||
/// gtk4-layer-shell is initialized for the window.
|
||||
pub fn init_quick_terminal(&mut self, window: >k::Window) -> Result<()> {
|
||||
match self {
|
||||
App::None(v) => v.init_quick_terminal(window),
|
||||
App::Wayland(v) => v.init_quick_terminal(window),
|
||||
App::X11(v) => v.init_quick_terminal(window),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-Window state for the underlying windowing protocol.
|
||||
pub enum Window {
|
||||
None(noop::Window),
|
||||
Wayland(wayland::Window),
|
||||
X11(x11::Window),
|
||||
}
|
||||
|
||||
impl Window {
|
||||
/// Create the per-window state. The window must be realized (it needs its
|
||||
/// `gdk::Surface`).
|
||||
pub fn init(app: &App, host: Rc<dyn WindowHost>) -> Result<Window> {
|
||||
Ok(match app {
|
||||
App::None(a) => Window::None(noop::Window::init(a, host)?),
|
||||
App::Wayland(a) => Window::Wayland(wayland::Window::init(a, host)?),
|
||||
App::X11(a) => Window::X11(x11::Window::init(a, host)?),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resize_event(&mut self) -> Result<()> {
|
||||
match self {
|
||||
Window::None(v) => v.resize_event(),
|
||||
Window::Wayland(v) => v.resize_event(),
|
||||
Window::X11(v) => v.resize_event(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sync_appearance(&mut self) -> Result<()> {
|
||||
match self {
|
||||
Window::None(v) => v.sync_appearance(),
|
||||
Window::Wayland(v) => v.sync_appearance(),
|
||||
Window::X11(v) => v.sync_appearance(),
|
||||
}
|
||||
}
|
||||
|
||||
/// True if CSD is currently enabled for this window (actual state, not
|
||||
/// desired state).
|
||||
pub fn client_side_decoration_enabled(&self) -> bool {
|
||||
match self {
|
||||
Window::None(v) => v.client_side_decoration_enabled(),
|
||||
Window::Wayland(v) => v.client_side_decoration_enabled(),
|
||||
Window::X11(v) => v.client_side_decoration_enabled(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_subprocess_env(&mut self, env: &mut Vec<(String, String)>) -> Result<()> {
|
||||
match self {
|
||||
Window::None(v) => v.add_subprocess_env(env),
|
||||
Window::Wayland(v) => v.add_subprocess_env(env),
|
||||
Window::X11(v) => v.add_subprocess_env(env),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_urgent(&mut self, urgent: bool) -> Result<()> {
|
||||
match self {
|
||||
Window::None(v) => v.set_urgent(urgent),
|
||||
Window::Wayland(v) => v.set_urgent(urgent),
|
||||
Window::X11(v) => v.set_urgent(urgent),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: widget scale factor etc. used by backends.
|
||||
pub(crate) fn native_surface(w: >k::Window) -> Option<gdk::Surface> {
|
||||
w.upcast_ref::<gtk::Native>().surface()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn noop_defaults() {
|
||||
let a = noop::App;
|
||||
assert!(!a.supports_quick_terminal());
|
||||
assert!(a.event_mods(None, gdk::ModifierType::empty()).is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//! Port of apprt/gtk/winproto/noop.zig: used when neither X11 nor Wayland is
|
||||
//! detected.
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use gtk::gdk;
|
||||
|
||||
use super::{Result, WindowHost};
|
||||
use ghostty_input::Mods;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct App;
|
||||
|
||||
impl App {
|
||||
pub fn event_mods(&self, _: Option<&gdk::Device>, _: gdk::ModifierType) -> Option<Mods> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn supports_quick_terminal(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
pub fn init_quick_terminal(&mut self, _: >k::Window) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Window;
|
||||
|
||||
impl Window {
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub fn init(_: &App, _: Rc<dyn WindowHost>) -> Result<Window> {
|
||||
Ok(Window)
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
pub fn resize_event(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
pub fn sync_appearance(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Always true: this should be the actual present state of the window.
|
||||
#[allow(clippy::unused_self)]
|
||||
pub fn client_side_decoration_enabled(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
pub fn add_subprocess_env(&mut self, _: &mut Vec<(String, String)>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
pub fn set_urgent(&mut self, _: bool) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
//! Port of apprt/gtk/winproto/wayland.zig: Wayland protocol implementation.
|
||||
//!
|
||||
//! The GDK display's `wl_display` is shared through `gdk4-wayland`'s
|
||||
//! `wayland_crate` feature (a `wayland_client::Connection` over GDK's foreign
|
||||
//! display), so no `unsafe` is required here. We use a private event queue on
|
||||
//! that connection for the protocol objects we own (KDE blur / server
|
||||
//! decoration / slide, xdg-activation). GTK's own queue dispatching reads the
|
||||
//! socket; events destined for our queue are picked up by
|
||||
//! [`Shared::dispatch_pending`] (blocking roundtrips at init, a short poll
|
||||
//! while an activation token request is in flight).
|
||||
//!
|
||||
//! # gtk4-layer-shell
|
||||
//! The quick terminal uses the `gtk4-layer-shell` library. Upstream documents
|
||||
//! that libgtk4-layer-shell must be linked/loaded *before* libwayland-client
|
||||
//! (otherwise it cannot interpose the protocol). With Rust the link order is
|
||||
//! not under our control: the final binary should link `gtk4-layer-shell`
|
||||
//! first (or run with `LD_PRELOAD=libgtk4-layer-shell.so`). Without it,
|
||||
//! `layer_shell::is_supported()` is false and the quick terminal is disabled.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use gdk4_wayland::WaylandDisplay;
|
||||
use gdk4_wayland::prelude::*;
|
||||
use gtk::gdk;
|
||||
use gtk::glib;
|
||||
use gtk::prelude::*;
|
||||
use gtk4_layer_shell::{self as layer_shell, LayerShell};
|
||||
use wayland_client::globals::{GlobalListContents, registry_queue_init};
|
||||
use wayland_client::protocol::wl_registry::WlRegistry;
|
||||
use wayland_client::protocol::wl_surface::WlSurface;
|
||||
use wayland_client::{Connection, Dispatch, EventQueue, Proxy, QueueHandle};
|
||||
use wayland_protocols::xdg::activation::v1::client::xdg_activation_token_v1::{
|
||||
self, XdgActivationTokenV1,
|
||||
};
|
||||
use wayland_protocols::xdg::activation::v1::client::xdg_activation_v1::XdgActivationV1;
|
||||
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur;
|
||||
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur_manager::OrgKdeKwinBlurManager;
|
||||
use wayland_protocols_plasma::server_decoration::client::org_kde_kwin_server_decoration::OrgKdeKwinServerDecoration;
|
||||
use wayland_protocols_plasma::server_decoration::client::org_kde_kwin_server_decoration_manager::{
|
||||
self, OrgKdeKwinServerDecorationManager,
|
||||
};
|
||||
use wayland_protocols_plasma::slide::client::org_kde_kwin_slide::OrgKdeKwinSlide;
|
||||
use wayland_protocols_plasma::slide::client::org_kde_kwin_slide_manager::OrgKdeKwinSlideManager;
|
||||
|
||||
use super::{Error, Result, WindowHost};
|
||||
use ghostty_config::config::Config;
|
||||
use ghostty_config::types_b::{
|
||||
QuickTerminalDimensions, QuickTerminalKeyboardInteractivity, QuickTerminalLayer,
|
||||
QuickTerminalPosition, WindowDecoration,
|
||||
};
|
||||
use ghostty_input::Mods;
|
||||
|
||||
/// `org_kde_kwin_server_decoration_manager.mode` values.
|
||||
const DECO_NONE: u32 = 0;
|
||||
const DECO_CLIENT: u32 = 1;
|
||||
const DECO_SERVER: u32 = 2;
|
||||
|
||||
/// `org_kde_kwin_slide.location` values.
|
||||
const SLIDE_TOP: u32 = 1;
|
||||
const SLIDE_LEFT: u32 = 2;
|
||||
const SLIDE_RIGHT: u32 = 3;
|
||||
const SLIDE_BOTTOM: u32 = 4;
|
||||
|
||||
/// Application-wide Wayland globals (Zig: `App.Context`).
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
kde_blur_manager: Option<OrgKdeKwinBlurManager>,
|
||||
// FIXME (upstream): replace with `zxdg_decoration_v1` once GTK merges
|
||||
// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
|
||||
kde_decoration_manager: Option<OrgKdeKwinServerDecorationManager>,
|
||||
kde_slide_manager: Option<OrgKdeKwinSlideManager>,
|
||||
default_deco_mode: Option<u32>,
|
||||
xdg_activation: Option<XdgActivationV1>,
|
||||
/// Whether the xdg_wm_dialog_v1 protocol is present. If so,
|
||||
/// gtk4-layer-shell < 1.0.4 may crash creating a quick terminal.
|
||||
/// See https://github.com/wmww/gtk4-layer-shell/issues/50
|
||||
xdg_wm_dialog_present: bool,
|
||||
/// Activation tokens requested but not yet `done`.
|
||||
pending_tokens: usize,
|
||||
}
|
||||
|
||||
struct Shared {
|
||||
conn: Connection,
|
||||
qh: QueueHandle<State>,
|
||||
queue: RefCell<EventQueue<State>>,
|
||||
state: RefCell<State>,
|
||||
}
|
||||
|
||||
impl Shared {
|
||||
fn dispatch_pending(&self) {
|
||||
let mut queue = self.queue.borrow_mut();
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Err(e) = queue.dispatch_pending(&mut *state) {
|
||||
log::warn!("wayland dispatch error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
if let Err(e) = self.conn.flush() {
|
||||
log::warn!("wayland flush error: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn bind<I: Proxy + 'static>(
|
||||
globals: &wayland_client::globals::GlobalList,
|
||||
qh: &QueueHandle<State>,
|
||||
) -> Option<I>
|
||||
where
|
||||
State: Dispatch<I, ()>,
|
||||
{
|
||||
match globals.bind::<I, _, _>(qh, 1..=1, ()) {
|
||||
Ok(v) => Some(v),
|
||||
Err(wayland_client::globals::BindError::NotPresent) => None,
|
||||
Err(e) => {
|
||||
log::warn!("error binding interface {} error={e}", I::interface().name);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wayland application-wide state.
|
||||
pub struct App {
|
||||
shared: Rc<Shared>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Returns `Ok(None)` if the display isn't Wayland.
|
||||
pub fn init(
|
||||
gdk_display: &gdk::Display,
|
||||
_app_id: &str,
|
||||
_config: &Config,
|
||||
) -> Result<Option<App>> {
|
||||
let Some(wl_display) = gdk_display.downcast_ref::<WaylandDisplay>() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let display = wl_display.wl_display().ok_or(Error::NoWaylandDisplay)?;
|
||||
let backend = display.backend().upgrade().ok_or(Error::NoWaylandDisplay)?;
|
||||
let conn = Connection::from_backend(backend);
|
||||
|
||||
// Get our display registry so we can get all the available interfaces
|
||||
// and bind to what we need.
|
||||
let (globals, mut queue) = registry_queue_init::<State>(&conn)
|
||||
.map_err(|e| Error::Wayland(format!("registry: {e}")))?;
|
||||
let qh = queue.handle();
|
||||
|
||||
let mut state = State::default();
|
||||
globals.contents().with_list(|list| {
|
||||
for g in list {
|
||||
log::debug!("found global {}", g.interface);
|
||||
if g.interface == "xdg_wm_dialog_v1" {
|
||||
state.xdg_wm_dialog_present = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
state.kde_blur_manager = bind(&globals, &qh);
|
||||
state.kde_decoration_manager = bind(&globals, &qh);
|
||||
state.kde_slide_manager = bind(&globals, &qh);
|
||||
state.xdg_activation = bind(&globals, &qh);
|
||||
|
||||
// Do another round-trip to get the default decoration mode
|
||||
queue
|
||||
.roundtrip(&mut state)
|
||||
.map_err(|e| Error::Wayland(format!("roundtrip: {e}")))?;
|
||||
|
||||
Ok(Some(App {
|
||||
shared: Rc::new(Shared {
|
||||
conn,
|
||||
qh,
|
||||
queue: RefCell::new(queue),
|
||||
state: RefCell::new(state),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
pub fn event_mods(&self, _: Option<&gdk::Device>, _: gdk::ModifierType) -> Option<Mods> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn supports_quick_terminal(&self) -> bool {
|
||||
if !layer_shell::is_supported() {
|
||||
log::warn!(
|
||||
"your compositor does not support the wlr-layer-shell protocol; disabling quick terminal"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
let version = (
|
||||
layer_shell::major_version(),
|
||||
layer_shell::minor_version(),
|
||||
layer_shell::micro_version(),
|
||||
);
|
||||
if self.shared.state.borrow().xdg_wm_dialog_present && version < (1, 0, 4) {
|
||||
log::warn!(
|
||||
"the version of gtk4-layer-shell installed on your system is too old (must be 1.0.4 or newer); disabling quick terminal"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Initialize gtk4-layer-shell for the window (must happen before the
|
||||
/// window is realized).
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
pub fn init_quick_terminal(&mut self, window: >k::Window) -> Result<()> {
|
||||
window.init_layer_shell();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Data attached to an xdg activation token.
|
||||
struct ActivationData {
|
||||
surface: WlSurface,
|
||||
/// The live token; cleared when `done` fires.
|
||||
token: Mutex<Option<XdgActivationTokenV1>>,
|
||||
done: AtomicBool,
|
||||
}
|
||||
|
||||
/// Per-window (wl_surface) state for the Wayland protocol.
|
||||
pub struct Window {
|
||||
host: Rc<dyn WindowHost>,
|
||||
shared: Rc<Shared>,
|
||||
surface: WlSurface,
|
||||
/// Present when the window is blurred.
|
||||
blur_token: Option<OrgKdeKwinBlur>,
|
||||
/// Controls the decoration mode (client/server/auto) of the window.
|
||||
decoration: Option<OrgKdeKwinServerDecoration>,
|
||||
/// Slide-in/out animation of the quick terminal (otherwise None).
|
||||
slide: Option<OrgKdeKwinSlide>,
|
||||
/// Present while the window is requesting attention.
|
||||
activation: Option<Arc<ActivationData>>,
|
||||
monitor_handler: Option<(gdk::Surface, glib::SignalHandlerId)>,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn init(app: &App, host: Rc<dyn WindowHost>) -> Result<Window> {
|
||||
let win = host.gtk_window().ok_or(Error::NotWaylandSurface)?;
|
||||
let gdk_surface = super::native_surface(&win).ok_or(Error::NotWaylandSurface)?;
|
||||
let wl_surface = gdk_surface
|
||||
.downcast_ref::<gdk4_wayland::WaylandSurface>()
|
||||
.ok_or(Error::NotWaylandSurface)?
|
||||
.wl_surface()
|
||||
.ok_or(Error::NotWaylandSurface)?;
|
||||
|
||||
let shared = Rc::clone(&app.shared);
|
||||
|
||||
// Get our decoration object so we can control the CSD vs SSD status.
|
||||
let decoration = shared
|
||||
.state
|
||||
.borrow()
|
||||
.kde_decoration_manager
|
||||
.as_ref()
|
||||
.map(|mgr| mgr.create(&wl_surface, &shared.qh, ()));
|
||||
|
||||
let monitor_handler = if host.is_quick_terminal() {
|
||||
let host = Rc::clone(&host);
|
||||
let id = gdk_surface.connect_enter_monitor(move |_, monitor| {
|
||||
entered_monitor(&*host, monitor);
|
||||
});
|
||||
Some((gdk_surface.clone(), id))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Window {
|
||||
host,
|
||||
shared,
|
||||
surface: wl_surface,
|
||||
blur_token: None,
|
||||
decoration,
|
||||
slide: None,
|
||||
activation: None,
|
||||
monitor_handler,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
pub fn resize_event(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub fn sync_appearance(&mut self) -> Result<()> {
|
||||
self.sync_blur();
|
||||
self.sync_decoration();
|
||||
if self.host.is_quick_terminal()
|
||||
&& let Err(e) = self.sync_quick_terminal()
|
||||
{
|
||||
log::warn!("failed to sync quick terminal appearance={e}");
|
||||
}
|
||||
self.shared.flush();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn client_side_decoration_enabled(&self) -> bool {
|
||||
match self.decoration_mode() {
|
||||
DECO_CLIENT => true,
|
||||
// If we support SSDs, then we should *not* enable CSDs if we prefer
|
||||
// SSDs. However, if we do not support SSDs (e.g. GNOME) then we
|
||||
// should enable CSDs even if the user prefers SSDs.
|
||||
DECO_SERVER => self.shared.state.borrow().kde_decoration_manager.is_none(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
pub fn add_subprocess_env(&mut self, _: &mut Vec<(String, String)>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_urgent(&mut self, urgent: bool) -> Result<()> {
|
||||
let Some(activation) = self.shared.state.borrow().xdg_activation.clone() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// If there already is a token, destroy and unset it
|
||||
if let Some(old) = self.activation.take()
|
||||
&& let Some(tok) = old.token.lock().ok().and_then(|mut t| t.take())
|
||||
{
|
||||
tok.destroy();
|
||||
if !old.done.load(Ordering::Relaxed) {
|
||||
let mut st = self.shared.state.borrow_mut();
|
||||
st.pending_tokens = st.pending_tokens.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
if urgent {
|
||||
let data = Arc::new(ActivationData {
|
||||
surface: self.surface.clone(),
|
||||
token: Mutex::new(None),
|
||||
done: AtomicBool::new(false),
|
||||
});
|
||||
let token = activation.get_activation_token(&self.shared.qh, Arc::clone(&data));
|
||||
token.set_surface(&self.surface);
|
||||
token.commit();
|
||||
if let Ok(mut t) = data.token.lock() {
|
||||
*t = Some(token);
|
||||
}
|
||||
self.shared.state.borrow_mut().pending_tokens += 1;
|
||||
self.shared.flush();
|
||||
self.activation = Some(data);
|
||||
|
||||
// Poll our queue until the compositor answers.
|
||||
let shared = Rc::clone(&self.shared);
|
||||
glib::timeout_add_local(std::time::Duration::from_millis(50), move || {
|
||||
shared.dispatch_pending();
|
||||
if shared.state.borrow().pending_tokens == 0 {
|
||||
shared.flush();
|
||||
glib::ControlFlow::Break
|
||||
} else {
|
||||
glib::ControlFlow::Continue
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the blur state of the window.
|
||||
fn sync_blur(&mut self) {
|
||||
let Some(manager) = self.shared.state.borrow().kde_blur_manager.clone() else {
|
||||
return;
|
||||
};
|
||||
let Some(config) = self.host.config() else {
|
||||
return;
|
||||
};
|
||||
let blur = config.background_blur;
|
||||
|
||||
if let Some(tok) = &self.blur_token {
|
||||
// Only release token when transitioning from blurred -> not blurred
|
||||
if !blur.enabled() {
|
||||
manager.unset(&self.surface);
|
||||
tok.release();
|
||||
self.blur_token = None;
|
||||
}
|
||||
} else if blur.enabled() {
|
||||
// Only acquire token when transitioning from not blurred -> blurred
|
||||
let tok = manager.create(&self.surface, &self.shared.qh, ());
|
||||
tok.commit();
|
||||
self.blur_token = Some(tok);
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_decoration(&self) {
|
||||
let Some(deco) = &self.decoration else {
|
||||
return;
|
||||
};
|
||||
deco.request_mode(self.decoration_mode());
|
||||
}
|
||||
|
||||
fn decoration_mode(&self) -> u32 {
|
||||
match self.host.window_decoration() {
|
||||
WindowDecoration::Auto => self
|
||||
.shared
|
||||
.state
|
||||
.borrow()
|
||||
.default_deco_mode
|
||||
.unwrap_or(DECO_CLIENT),
|
||||
WindowDecoration::Client => DECO_CLIENT,
|
||||
WindowDecoration::Server => DECO_SERVER,
|
||||
WindowDecoration::None => DECO_NONE,
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_quick_terminal(&mut self) -> Result<()> {
|
||||
let window = self.host.gtk_window().ok_or(Error::NotWaylandSurface)?;
|
||||
let Some(config) = self.host.config() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
window.set_layer(match config.gtk_quick_terminal_layer {
|
||||
QuickTerminalLayer::Overlay => layer_shell::Layer::Overlay,
|
||||
QuickTerminalLayer::Top => layer_shell::Layer::Top,
|
||||
QuickTerminalLayer::Bottom => layer_shell::Layer::Bottom,
|
||||
QuickTerminalLayer::Background => layer_shell::Layer::Background,
|
||||
});
|
||||
window.set_namespace(Some(config.gtk_quick_terminal_namespace.as_str()));
|
||||
|
||||
window.set_keyboard_mode(match config.quick_terminal_keyboard_interactivity {
|
||||
QuickTerminalKeyboardInteractivity::None => layer_shell::KeyboardMode::None,
|
||||
QuickTerminalKeyboardInteractivity::OnDemand => {
|
||||
if layer_shell::protocol_version() < 4 {
|
||||
log::warn!(
|
||||
"your compositor does not support on-demand keyboard access; falling back to exclusive access"
|
||||
);
|
||||
layer_shell::KeyboardMode::Exclusive
|
||||
} else {
|
||||
layer_shell::KeyboardMode::OnDemand
|
||||
}
|
||||
}
|
||||
QuickTerminalKeyboardInteractivity::Exclusive => {
|
||||
layer_shell::KeyboardMode::Exclusive
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let anchored_edge = match config.quick_terminal_position {
|
||||
QuickTerminalPosition::Left => Some(layer_shell::Edge::Left),
|
||||
QuickTerminalPosition::Right => Some(layer_shell::Edge::Right),
|
||||
QuickTerminalPosition::Top => Some(layer_shell::Edge::Top),
|
||||
QuickTerminalPosition::Bottom => Some(layer_shell::Edge::Bottom),
|
||||
QuickTerminalPosition::Center => None,
|
||||
};
|
||||
|
||||
for edge in [
|
||||
layer_shell::Edge::Left,
|
||||
layer_shell::Edge::Right,
|
||||
layer_shell::Edge::Top,
|
||||
layer_shell::Edge::Bottom,
|
||||
] {
|
||||
if anchored_edge == Some(edge) {
|
||||
window.set_margin(edge, 0);
|
||||
window.set_anchor(edge, true);
|
||||
continue;
|
||||
}
|
||||
// Arbitrary margin - could be made customizable?
|
||||
window.set_margin(edge, 20);
|
||||
window.set_anchor(edge, false);
|
||||
}
|
||||
|
||||
if let Some(slide) = self.slide.take() {
|
||||
slide.release();
|
||||
}
|
||||
|
||||
self.slide = anchored_edge.and_then(|anchored| {
|
||||
let mgr = self.shared.state.borrow().kde_slide_manager.clone()?;
|
||||
let slide = mgr.create(&self.surface, &self.shared.qh, ());
|
||||
slide.set_location(match anchored {
|
||||
layer_shell::Edge::Top => SLIDE_TOP,
|
||||
layer_shell::Edge::Bottom => SLIDE_BOTTOM,
|
||||
layer_shell::Edge::Left => SLIDE_LEFT,
|
||||
_ => SLIDE_RIGHT,
|
||||
});
|
||||
slide.commit();
|
||||
Some(slide)
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Window {
|
||||
fn drop(&mut self) {
|
||||
if let Some((surface, id)) = self.monitor_handler.take() {
|
||||
surface.disconnect(id);
|
||||
}
|
||||
if let Some(blur) = self.blur_token.take() {
|
||||
blur.release();
|
||||
}
|
||||
if let Some(deco) = self.decoration.take() {
|
||||
deco.release();
|
||||
}
|
||||
if let Some(slide) = self.slide.take() {
|
||||
slide.release();
|
||||
}
|
||||
if let Some(act) = self.activation.take()
|
||||
&& let Some(tok) = act.token.lock().ok().and_then(|mut t| t.take())
|
||||
{
|
||||
tok.destroy();
|
||||
}
|
||||
self.shared.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the size of the quick terminal based on monitor dimensions.
|
||||
fn entered_monitor(host: &dyn WindowHost, monitor: &gdk::Monitor) {
|
||||
let Some(window) = host.gtk_window() else {
|
||||
return;
|
||||
};
|
||||
let Some(config) = host.config() else {
|
||||
return;
|
||||
};
|
||||
let size = monitor.geometry();
|
||||
let dims = config.quick_terminal_size.calculate(
|
||||
config.quick_terminal_position,
|
||||
QuickTerminalDimensions {
|
||||
width: size.width() as u32,
|
||||
height: size.height() as u32,
|
||||
},
|
||||
);
|
||||
window.set_default_size(dims.width as i32, dims.height as i32);
|
||||
}
|
||||
|
||||
// ---- wayland-client dispatch plumbing ---------------------------------
|
||||
|
||||
impl Dispatch<WlRegistry, GlobalListContents> for State {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
_: &WlRegistry,
|
||||
_: wayland_client::protocol::wl_registry::Event,
|
||||
_: &GlobalListContents,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
// Globals appearing/disappearing after init are not tracked.
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! no_events {
|
||||
($($t:ty),*) => {$(
|
||||
impl Dispatch<$t, ()> for State {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
_: &$t,
|
||||
_: <$t as Proxy>::Event,
|
||||
(): &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
no_events!(
|
||||
OrgKdeKwinBlurManager,
|
||||
OrgKdeKwinBlur,
|
||||
OrgKdeKwinSlideManager,
|
||||
OrgKdeKwinSlide,
|
||||
XdgActivationV1,
|
||||
OrgKdeKwinServerDecoration
|
||||
);
|
||||
|
||||
impl Dispatch<OrgKdeKwinServerDecorationManager, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_: &OrgKdeKwinServerDecorationManager,
|
||||
event: org_kde_kwin_server_decoration_manager::Event,
|
||||
(): &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
if let org_kde_kwin_server_decoration_manager::Event::DefaultMode { mode } = event {
|
||||
state.default_deco_mode = Some(mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<XdgActivationTokenV1, Arc<ActivationData>> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
token: &XdgActivationTokenV1,
|
||||
event: xdg_activation_token_v1::Event,
|
||||
data: &Arc<ActivationData>,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let xdg_activation_token_v1::Event::Done { token: id } = event else {
|
||||
return;
|
||||
};
|
||||
// Ignore events for tokens we've already dropped.
|
||||
let is_current = data
|
||||
.token
|
||||
.lock()
|
||||
.is_ok_and(|t| t.as_ref().is_some_and(|t| t.id() == token.id()));
|
||||
if !is_current {
|
||||
log::warn!("received event for unknown activation token; ignoring");
|
||||
return;
|
||||
}
|
||||
if let Some(activation) = &state.xdg_activation {
|
||||
activation.activate(id, &data.surface);
|
||||
}
|
||||
token.destroy();
|
||||
if let Ok(mut t) = data.token.lock() {
|
||||
t.take();
|
||||
}
|
||||
data.done.store(true, Ordering::Relaxed);
|
||||
state.pending_tokens = state.pending_tokens.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
//! Port of apprt/gtk/winproto/x11.zig: X11 window protocol implementation.
|
||||
//!
|
||||
//! Uses Xlib (through the `x11` crate, sharing GDK's `Display*`) exactly like
|
||||
//! the Zig code; all raw FFI is confined to small `unsafe` blocks with SAFETY
|
||||
//! comments.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::ffi::{c_int, c_long, c_uchar, c_uint, c_ulong, c_void};
|
||||
use std::rc::Rc;
|
||||
|
||||
use gdk4_x11::x11::xlib;
|
||||
use gdk4_x11::{X11Display, X11Surface};
|
||||
use gtk::gdk;
|
||||
use gtk::glib;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use super::{Error, Result, WindowHost};
|
||||
use ghostty_input::Mods;
|
||||
|
||||
// XKB bits we need that the `x11` crate doesn't expose (XKBlib.h / XKB.h).
|
||||
const XKB_MAJOR_VERSION: c_int = 1;
|
||||
const XKB_MINOR_VERSION: c_int = 0;
|
||||
const XKB_USE_CORE_KBD: c_uint = 0x0100;
|
||||
const XKB_STATE_NOTIFY: c_int = 2;
|
||||
const QUEUED_AFTER_READING: c_int = 1;
|
||||
const XKB_MODIFIER_STATE_MASK: c_ulong = 1 << 0;
|
||||
|
||||
#[link(name = "X11")]
|
||||
unsafe extern "C" {
|
||||
fn XkbQueryExtension(
|
||||
dpy: *mut c_void,
|
||||
opcode_rtrn: *mut c_int,
|
||||
event_rtrn: *mut c_int,
|
||||
error_rtrn: *mut c_int,
|
||||
major_in_out: *mut c_int,
|
||||
minor_in_out: *mut c_int,
|
||||
) -> c_int;
|
||||
|
||||
fn XkbSelectEventDetails(
|
||||
dpy: *mut c_void,
|
||||
device_spec: c_uint,
|
||||
event_type: c_uint,
|
||||
bits_to_change: c_ulong,
|
||||
values_for_bits: c_ulong,
|
||||
) -> c_int;
|
||||
}
|
||||
|
||||
/// Leading part of `XkbStateNotifyEvent` (XKBlib.h) up to `lookup_mods`.
|
||||
#[repr(C)]
|
||||
#[allow(dead_code)]
|
||||
struct XkbStateNotifyEvent {
|
||||
type_: c_int,
|
||||
serial: c_ulong,
|
||||
send_event: c_int,
|
||||
display: *mut xlib::Display,
|
||||
time: c_ulong,
|
||||
xkb_type: c_int,
|
||||
device: c_int,
|
||||
changed: c_uint,
|
||||
group: c_int,
|
||||
base_group: c_int,
|
||||
latched_group: c_int,
|
||||
locked_group: c_int,
|
||||
mods: c_uint,
|
||||
base_mods: c_uint,
|
||||
latched_mods: c_uint,
|
||||
locked_mods: c_uint,
|
||||
compat_state: c_int,
|
||||
grab_mods: c_uchar,
|
||||
compat_grab_mods: c_uchar,
|
||||
lookup_mods: c_uchar,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Atoms {
|
||||
kde_blur: xlib::Atom,
|
||||
motif_wm_hints: xlib::Atom,
|
||||
}
|
||||
|
||||
impl Atoms {
|
||||
fn new(display: &X11Display) -> Atoms {
|
||||
Atoms {
|
||||
kde_blur: gdk4_x11::x11_get_xatom_by_name_for_display(
|
||||
display,
|
||||
"_KDE_NET_WM_BLUR_BEHIND_REGION",
|
||||
),
|
||||
motif_wm_hints: gdk4_x11::x11_get_xatom_by_name_for_display(display, "_MOTIF_WM_HINTS"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// X11 application-wide state.
|
||||
pub struct App {
|
||||
xl: Rc<xlib::Xlib>,
|
||||
display: *mut xlib::Display,
|
||||
base_event_code: c_int,
|
||||
atoms: Atoms,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Returns `Ok(None)` if the display isn't X11.
|
||||
pub fn init(
|
||||
gdk_display: &gdk::Display,
|
||||
app_id: &str,
|
||||
config: &ghostty_config::config::Config,
|
||||
) -> Result<Option<App>> {
|
||||
let Some(x11_display) = gdk_display.downcast_ref::<X11Display>() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let xl = xlib::Xlib::open().map_err(|e| {
|
||||
log::error!("failed to load libX11: {e}");
|
||||
Error::XkbInitialization
|
||||
})?;
|
||||
|
||||
// SAFETY: the display is a live X11 display; the returned pointer is
|
||||
// owned by GDK and valid for the lifetime of the display (the whole
|
||||
// application).
|
||||
let xlib_display = unsafe { x11_display.xdisplay() };
|
||||
|
||||
let program_name = match &config.x11_instance_name {
|
||||
Some(pn) => pn.clone(),
|
||||
None if cfg!(debug_assertions) => "ghostty-debug".to_string(),
|
||||
None => "ghostty".to_string(),
|
||||
};
|
||||
|
||||
// Set the X11 window class property (WM_CLASS); the instance name is
|
||||
// derived from g_set_prgname. Results in e.g.
|
||||
// WM_CLASS(STRING) = "ghostty", "com.mitchellh.ghostty"
|
||||
glib::set_prgname(Some(program_name.as_str()));
|
||||
x11_display.set_program_class(app_id);
|
||||
|
||||
log::debug!("Xkb.init: initializing Xkb");
|
||||
let mut opcode: c_int = 0;
|
||||
let mut base_event_code: c_int = 0;
|
||||
let mut base_error_code: c_int = 0;
|
||||
let mut major = XKB_MAJOR_VERSION;
|
||||
let mut minor = XKB_MINOR_VERSION;
|
||||
// SAFETY: valid display pointer, valid out-pointers to locals.
|
||||
let ok = unsafe {
|
||||
XkbQueryExtension(
|
||||
xlib_display.cast(),
|
||||
&raw mut opcode,
|
||||
&raw mut base_event_code,
|
||||
&raw mut base_error_code,
|
||||
&raw mut major,
|
||||
&raw mut minor,
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
log::error!(
|
||||
"Fatal: error initializing Xkb extension: error executing XkbQueryExtension"
|
||||
);
|
||||
return Err(Error::XkbInitialization);
|
||||
}
|
||||
|
||||
// SAFETY: valid display pointer.
|
||||
let ok = unsafe {
|
||||
XkbSelectEventDetails(
|
||||
xlib_display.cast(),
|
||||
XKB_USE_CORE_KBD,
|
||||
XKB_STATE_NOTIFY as c_uint,
|
||||
XKB_MODIFIER_STATE_MASK,
|
||||
XKB_MODIFIER_STATE_MASK,
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
log::error!(
|
||||
"Fatal: error initializing Xkb extension: error executing XkbSelectEventDetails"
|
||||
);
|
||||
return Err(Error::XkbInitialization);
|
||||
}
|
||||
|
||||
Ok(Some(App {
|
||||
xl: Rc::new(xl),
|
||||
display: xlib_display,
|
||||
base_event_code,
|
||||
atoms: Atoms::new(x11_display),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Checks for an immediate pending XKB state update event and returns the
|
||||
/// keyboard state based on it. Necessary because the standard GTK X11 API
|
||||
/// does not include the currently pressed key in the modifier snapshot
|
||||
/// (paraphrased from Firefox/Gecko widget/gtk/nsGtkKeyUtils.cpp).
|
||||
///
|
||||
/// Returns `None` if there is no such event; callers fall back to the GDK
|
||||
/// modifier state.
|
||||
pub fn event_mods(&self, _: Option<&gdk::Device>, _: gdk::ModifierType) -> Option<Mods> {
|
||||
// SAFETY: `display` is valid for the application's lifetime; XEvent is
|
||||
// plain-old-data, zero-initialised, and filled by XPeekEvent.
|
||||
let lookup_mods = unsafe {
|
||||
if (self.xl.XEventsQueued)(self.display, QUEUED_AFTER_READING) == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut next: xlib::XEvent = std::mem::zeroed();
|
||||
(self.xl.XPeekEvent)(self.display, &raw mut next);
|
||||
if next.type_ != self.base_event_code {
|
||||
return None;
|
||||
}
|
||||
// XkbStateNotifyEvent is a prefix-compatible view of the XEvent
|
||||
// union (which is at least as large and aligned as a long).
|
||||
let ev = &*std::ptr::from_ref(&next).cast::<XkbStateNotifyEvent>();
|
||||
if ev.xkb_type != XKB_STATE_NOTIFY {
|
||||
return None;
|
||||
}
|
||||
ev.lookup_mods
|
||||
};
|
||||
|
||||
let lookup = c_uint::from(lookup_mods);
|
||||
let mut mods = Mods::default();
|
||||
log::debug!("X11: found extra XkbStateNotify event w/lookup_mods: {lookup_mods:b}");
|
||||
if lookup & xlib::ShiftMask != 0 {
|
||||
mods.shift = true;
|
||||
}
|
||||
if lookup & xlib::ControlMask != 0 {
|
||||
mods.ctrl = true;
|
||||
}
|
||||
if lookup & xlib::Mod1Mask != 0 {
|
||||
mods.alt = true;
|
||||
}
|
||||
if lookup & xlib::Mod4Mask != 0 {
|
||||
mods.super_ = true;
|
||||
}
|
||||
if lookup & xlib::LockMask != 0 {
|
||||
mods.caps_lock = true;
|
||||
}
|
||||
Some(mods)
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
pub fn supports_quick_terminal(&self) -> bool {
|
||||
log::warn!("quick terminal is not yet supported on X11");
|
||||
false
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
pub fn init_quick_terminal(&mut self, _: >k::Window) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// `_KDE_NET_WM_BLUR_BEHIND_REGION` payload: x, y, width, height (32-bit
|
||||
/// format properties are arrays of `long`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[repr(C)]
|
||||
struct Region {
|
||||
x: c_long,
|
||||
y: c_long,
|
||||
width: c_long,
|
||||
height: c_long,
|
||||
}
|
||||
|
||||
/// See Xm/MwmUtil.h, packaged with the Motif Window Manager. All fields are
|
||||
/// `long`-sized in the 32-bit format.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[repr(C)]
|
||||
struct MotifWmHints {
|
||||
flags: c_ulong,
|
||||
functions: c_ulong,
|
||||
decorations: c_ulong,
|
||||
input_mode: c_long,
|
||||
status: c_ulong,
|
||||
}
|
||||
|
||||
impl MotifWmHints {
|
||||
const FLAG_DECORATIONS: c_ulong = 1 << 1;
|
||||
const DECORATIONS_ALL: c_ulong = 1 << 0;
|
||||
}
|
||||
|
||||
/// X11 per-window state.
|
||||
pub struct Window {
|
||||
xl: Rc<xlib::Xlib>,
|
||||
display: *mut xlib::Display,
|
||||
atoms: Atoms,
|
||||
host: Rc<dyn WindowHost>,
|
||||
x11_surface: X11Surface,
|
||||
|
||||
blur_region: Region,
|
||||
|
||||
// Cache last applied values to avoid redundant X11 property updates, which
|
||||
// cause visual glitches with some WMs (ghostty-org/ghostty#8075).
|
||||
last_applied_blur_region: Option<Region>,
|
||||
last_applied_decoration_hints: Option<MotifWmHints>,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn init(app: &App, host: Rc<dyn WindowHost>) -> Result<Window> {
|
||||
let win = host.gtk_window().ok_or(Error::NotX11Surface)?;
|
||||
let surface = super::native_surface(&win).ok_or(Error::NotX11Surface)?;
|
||||
let x11_surface = surface
|
||||
.downcast::<X11Surface>()
|
||||
.map_err(|_| Error::NotX11Surface)?;
|
||||
Ok(Window {
|
||||
xl: Rc::clone(&app.xl),
|
||||
display: app.display,
|
||||
atoms: app.atoms,
|
||||
host,
|
||||
x11_surface,
|
||||
blur_region: Region::default(),
|
||||
last_applied_blur_region: None,
|
||||
last_applied_decoration_hints: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resize_event(&mut self) -> Result<()> {
|
||||
// The blur region must update with window resizes
|
||||
self.sync_blur()
|
||||
}
|
||||
|
||||
pub fn sync_appearance(&mut self) -> Result<()> {
|
||||
// The user could have toggled between CSDs and SSDs, therefore we need
|
||||
// to recalculate the blur region offset.
|
||||
if let Some(win) = self.host.gtk_window() {
|
||||
let (mut x, mut y) = win.upcast_ref::<gtk::Native>().surface_transform();
|
||||
// Transform surface coordinates to device coordinates.
|
||||
let scale = f64::from(win.scale_factor());
|
||||
x *= scale;
|
||||
y *= scale;
|
||||
self.blur_region = Region {
|
||||
x: x as c_long,
|
||||
y: y as c_long,
|
||||
..Region::default()
|
||||
};
|
||||
}
|
||||
if let Err(err) = self.sync_blur() {
|
||||
log::error!("failed to synchronize blur={err}");
|
||||
}
|
||||
if let Err(err) = self.sync_decorations() {
|
||||
log::error!("failed to synchronize decorations={err}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn client_side_decoration_enabled(&self) -> bool {
|
||||
use ghostty_config::types_b::WindowDecoration as D;
|
||||
match self.host.window_decoration() {
|
||||
D::Auto | D::Client => true,
|
||||
D::Server | D::None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_blur(&mut self) -> Result<()> {
|
||||
// FIXME (upstream): doesn't factor in rounded corners on Adwaita.
|
||||
let Some(win) = self.host.gtk_window() else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(config) = self.host.config() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// When blur is disabled, remove the property if it was previously set
|
||||
let blur = config.background_blur;
|
||||
if !blur.enabled() {
|
||||
if self.last_applied_blur_region.is_some() {
|
||||
self.delete_property(self.atoms.kde_blur)?;
|
||||
self.last_applied_blur_region = None;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Transform surface coordinates to device coordinates.
|
||||
let scale = win.scale_factor();
|
||||
self.blur_region.width = c_long::from(win.width() * scale);
|
||||
self.blur_region.height = c_long::from(win.height() * scale);
|
||||
|
||||
// Only update X11 properties when the blur region actually changes
|
||||
if self.last_applied_blur_region == Some(self.blur_region) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"set blur={blur:?}, window xid={}, region={:?}",
|
||||
self.x11_surface.xid(),
|
||||
self.blur_region
|
||||
);
|
||||
|
||||
let region = self.blur_region;
|
||||
self.change_property(self.atoms.kde_blur, xlib::XA_CARDINAL, ®ion)?;
|
||||
self.last_applied_blur_region = Some(region);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sync_decorations(&mut self) -> Result<()> {
|
||||
use ghostty_config::types_b::WindowDecoration as D;
|
||||
|
||||
let mut hints = match self.get_window_property::<MotifWmHints>(
|
||||
self.atoms.motif_wm_hints,
|
||||
self.atoms.motif_wm_hints,
|
||||
) {
|
||||
Ok(h) => h,
|
||||
// already default-initialised, so this is fine
|
||||
Err(Error::PropertyNotFound) => MotifWmHints::default(),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
hints.flags |= MotifWmHints::FLAG_DECORATIONS;
|
||||
if matches!(self.host.window_decoration(), D::Server) {
|
||||
hints.decorations |= MotifWmHints::DECORATIONS_ALL;
|
||||
} else {
|
||||
hints.decorations &= !MotifWmHints::DECORATIONS_ALL;
|
||||
}
|
||||
|
||||
// Only update decoration hints when they actually change
|
||||
if self.last_applied_decoration_hints == Some(hints) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.change_property(self.atoms.motif_wm_hints, self.atoms.motif_wm_hints, &hints)?;
|
||||
self.last_applied_decoration_hints = Some(hints);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_subprocess_env(&mut self, env: &mut Vec<(String, String)>) -> Result<()> {
|
||||
env.push(("WINDOWID".to_string(), self.x11_surface.xid().to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub fn set_urgent(&mut self, urgent: bool) -> Result<()> {
|
||||
self.x11_surface.set_urgency_hint(urgent);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `XGetWindowProperty` for a 32-format property that is an array of
|
||||
/// `long`s whose layout matches `T` (`T` is a `repr(C)` struct of longs).
|
||||
fn get_window_property<T: Copy>(&self, name: xlib::Atom, typ: xlib::Atom) -> Result<T> {
|
||||
let mut actual_type: xlib::Atom = 0;
|
||||
let mut actual_format: c_int = 0;
|
||||
let mut nitems: c_ulong = 0;
|
||||
let mut bytes_after: c_ulong = 0;
|
||||
let mut prop: *mut c_uchar = std::ptr::null_mut();
|
||||
|
||||
// SAFETY: valid display and window id; all out-pointers refer to locals.
|
||||
let code = unsafe {
|
||||
(self.xl.XGetWindowProperty)(
|
||||
self.display,
|
||||
self.x11_surface.xid(),
|
||||
name,
|
||||
0,
|
||||
c_long::MAX,
|
||||
xlib::False,
|
||||
typ,
|
||||
&raw mut actual_type,
|
||||
&raw mut actual_format,
|
||||
&raw mut nitems,
|
||||
&raw mut bytes_after,
|
||||
&raw mut prop,
|
||||
)
|
||||
};
|
||||
if code != c_int::from(xlib::Success) {
|
||||
return Err(Error::RequestFailed);
|
||||
}
|
||||
if actual_type == 0 {
|
||||
return Err(Error::PropertyNotFound);
|
||||
}
|
||||
|
||||
let result = if typ != actual_type {
|
||||
Err(Error::PropertyTypeMismatch)
|
||||
} else if actual_format != 32 {
|
||||
Err(Error::PropertyFormatMismatch)
|
||||
} else if (nitems as usize) * std::mem::size_of::<c_long>() < std::mem::size_of::<T>()
|
||||
|| prop.is_null()
|
||||
{
|
||||
Err(Error::RequestFailed)
|
||||
} else {
|
||||
// SAFETY: Xlib returns 32-format data as an array of `long`
|
||||
// (suitably aligned), at least size_of::<T>() bytes of which we
|
||||
// just verified; T is a repr(C) aggregate of longs.
|
||||
Ok(unsafe { prop.cast::<T>().read() })
|
||||
};
|
||||
if !prop.is_null() {
|
||||
// SAFETY: allocated by Xlib.
|
||||
unsafe { (self.xl.XFree)(prop.cast()) };
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn change_property<T: Copy>(&self, name: xlib::Atom, typ: xlib::Atom, value: &T) -> Result<()> {
|
||||
let nelements = std::mem::size_of::<T>() / std::mem::size_of::<c_long>();
|
||||
// SAFETY: valid display/window; `value` points to `nelements` longs
|
||||
// (T is a repr(C) aggregate of longs); data is copied by Xlib.
|
||||
let status = unsafe {
|
||||
(self.xl.XChangeProperty)(
|
||||
self.display,
|
||||
self.x11_surface.xid(),
|
||||
name,
|
||||
typ,
|
||||
32,
|
||||
xlib::PropModeReplace,
|
||||
std::ptr::from_ref(value).cast::<c_uchar>(),
|
||||
nelements as c_int,
|
||||
)
|
||||
};
|
||||
// Xlib alternates between error values (0 = success) and booleans
|
||||
// (1 = success); the Zig code treats 0 as failure here.
|
||||
if status == 0 {
|
||||
return Err(Error::RequestFailed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_property(&self, name: xlib::Atom) -> Result<()> {
|
||||
// SAFETY: valid display and window id.
|
||||
let status =
|
||||
unsafe { (self.xl.XDeleteProperty)(self.display, self.x11_surface.xid(), name) };
|
||||
if status == 0 {
|
||||
return Err(Error::RequestFailed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn property_layouts_are_long_arrays() {
|
||||
let l = std::mem::size_of::<c_long>();
|
||||
assert_eq!(std::mem::size_of::<Region>(), 4 * l);
|
||||
assert_eq!(std::mem::size_of::<MotifWmHints>(), 5 * l);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user