diff --git a/Cargo.lock b/Cargo.lock index bbdcaf6ef..36545d642 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3782,8 +3782,6 @@ dependencies = [ [[package]] name = "gpui-pre-linux" version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aab655cda1d6dcd549ac869e8678babc84cf573ab6794a1fe1a39c6c70c60b48" dependencies = [ "accesskit", "accesskit_unix", diff --git a/Cargo.toml b/Cargo.toml index a0d501734..5a9ce9b4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,7 @@ [workspace] members = ["crates/embed-resource", "crates/herdr-client", "crates/herdr-gpui", "crates/herdr-gpui-launcher", "crates/herdr-protocol"] +# Vendored third-party crates keep their upstream layout and lint baseline. +exclude = ["vendor/gpui-pre-linux"] resolver = "2" [workspace.package] @@ -138,3 +140,6 @@ zeroize = "=1.9.1" # no-op stands in for it. See crates/embed-resource. [patch.crates-io] embed-resource = { path = "crates/embed-resource" } +# Unmodified gpui-pre-linux 0.3.6 (zed@bcf6582), vendored so Linux platform +# fixes can ship before a gpui-pre release carries them. See vendor/README.md. +gpui-pre-linux = { path = "vendor/gpui-pre-linux" } diff --git a/scripts/check-file-size.sh b/scripts/check-file-size.sh index 8f80578fe..eafc699d3 100755 --- a/scripts/check-file-size.sh +++ b/scripts/check-file-size.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # Fail when a tracked source file (Rust, Python, shell, Swift) exceeds MAX_LINES. # There is no allowlist: split an oversized file by responsibility instead. +# Third-party crates under vendor/ keep their upstream layout and are skipped. set -euo pipefail @@ -17,7 +18,7 @@ while IFS= read -r -d '' file; do echo "FAIL: $file has $lines lines (limit $MAX_LINES)" failures=$((failures + 1)) fi -done < <(git ls-files -z -- '*.rs' '*.py' '*.sh' '*.swift') +done < <(git ls-files -z -- '*.rs' '*.py' '*.sh' '*.swift' ':(exclude)vendor/**') if ((failures > 0)); then echo diff --git a/vendor/README.md b/vendor/README.md new file mode 100644 index 000000000..ab8e441d8 --- /dev/null +++ b/vendor/README.md @@ -0,0 +1,13 @@ +# Vendored crates + +## gpui-pre-linux + +`gpui-pre-linux` 0.3.6 from crates.io, Zed's `gpui_linux` crate at +zed-industries/zed@bcf6582ce3500df93a8a39366640173e6786cea6. It replaces the +registry crate through `[patch.crates-io]` in the workspace `Cargo.toml` and +is excluded from the workspace, so it keeps its upstream layout. + +The first commit adding it is a byte-for-byte copy of the published crate +(without the registry's `Cargo.toml.orig` and `.cargo-ok`). Later commits carry +only fixes that are also proposed upstream to Zed; drop the vendored copy once +a `gpui-pre` release includes them. diff --git a/vendor/gpui-pre-linux/Cargo.toml b/vendor/gpui-pre-linux/Cargo.toml new file mode 100644 index 000000000..368bacf84 --- /dev/null +++ b/vendor/gpui-pre-linux/Cargo.toml @@ -0,0 +1,307 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +name = "gpui-pre-linux" +version = "0.3.6" +build = false +publish = true +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Zed's `gpui_linux` crate (gpui-pre snapshot of zed@bcf6582)" +readme = false +license = "Apache-2.0" +repository = "https://github.com/zed-industries/zed" +resolver = "2" + +[package.metadata.cargo-shear] +ignored = [ + "bitflags", + "scap", + "x11-clipboard", +] + +[package.metadata.gpui-pre] +zed-crate = "gpui_linux" +zed-version = "0.1.0" +zed-rev = "bcf6582ce3500df93a8a39366640173e6786cea6" + +[features] +default = [ + "wayland", + "x11", +] +screen-capture = [ + "gpui/screen-capture", + "scap", +] +test-support = ["gpui/test-support"] +wayland = [ + "bitflags", + "gpui_wgpu", + "ashpd/wayland", + "calloop-wayland-source", + "wayland-backend", + "wayland-client", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-plasma", + "wayland-protocols-wlr", + "filedescriptor", + "xkbcommon/wayland", + "open", + "gpui/wayland", +] +x11 = [ + "gpui_wgpu", + "ashpd", + "as-raw-xcb-connection", + "x11rb", + "xkbcommon/x11", + "xim", + "x11-clipboard", + "filedescriptor", + "open", + "scap?/x11", +] + +[lib] +name = "gpui_linux" +path = "src/gpui_linux.rs" +doctest = false + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.accesskit] +version = "0.24.0" +features = ["enumn"] + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.accesskit_unix] +version = "0.22" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.anyhow] +version = "1.0.86" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.as-raw-xcb-connection] +version = "1" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.ashpd] +version = "0.13" +features = [ + "async-io", + "inhibit", + "notification", + "open_uri", + "file_chooser", + "settings", + "trash", +] +optional = true +default-features = false + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.bitflags] +version = "2.6.0" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.bytemuck] +version = "1" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.calloop] +version = "0.14.3" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.calloop-wayland-source] +version = "0.4.1" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.collections] +version = "=0.3.6" +package = "gpui-pre-collections" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.filedescriptor] +version = "0.8.2" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.futures] +version = "0.3.34" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.gpui] +version = "=0.3.6" +default-features = false +package = "gpui-pre" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.gpui_util] +version = "=0.3.6" +package = "gpui-pre-util" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.gpui_wgpu] +version = "=0.3.6" +features = ["font-kit"] +optional = true +package = "gpui-pre-wgpu" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.http_client] +version = "=0.3.6" +package = "gpui-pre-http-client" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.libc] +version = "0.2" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.log] +version = "0.4.16" +features = [ + "kv_unstable_serde", + "serde", +] + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.notify-rust] +version = "4" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.oo7] +version = "0.6" +features = [ + "async-std", + "native_crypto", +] +default-features = false + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.open] +version = "5.2.0" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.parking_lot] +version = "0.12.1" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.raw-window-handle] +version = "0.6" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.scap] +version = "0.0.8-zed" +optional = true +default-features = false +package = "zed-scap" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.smallvec] +version = "1.6" +features = [ + "union", + "const_new", +] + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.smol] +version = "2.0" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.strum] +version = "0.28" +features = ["derive"] + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.url] +version = "2.2" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.uuid] +version = "1.1.2" +features = [ + "v4", + "v5", + "v7", + "serde", +] + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-backend] +version = "0.3.15" +features = [ + "client_system", + "dlopen", + "log", +] +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-client] +version = "0.31.11" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-cursor] +version = "0.31.11" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-protocols] +version = "0.32.9" +features = [ + "client", + "staging", + "unstable", +] +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-protocols-plasma] +version = "0.3.9" +features = ["client"] +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-protocols-wlr] +version = "0.3.9" +features = ["client"] +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.x11-clipboard] +version = "0.9.3" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.x11rb] +version = "0.13.1" +features = [ + "allow-unsafe-code", + "xkb", + "randr", + "xinput", + "cursor", + "resource_manager", + "sync", + "dri3", +] +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.xim] +version = "0.4.0-zed" +features = [ + "x11rb-xcb", + "x11rb-client", +] +optional = true +package = "zed-xim" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.xkbcommon] +version = "0.8.0" +optional = true +default-features = false + +[lints.clippy] +dbg_macro = "deny" +declare_interior_mutable_const = "deny" +disallowed_methods = "deny" +large_enum_variant = "allow" +let_underscore_future = "allow" +nonminimal_bool = "allow" +redundant_clone = "deny" +single_range_in_vec_init = "allow" +todo = "deny" +too_many_arguments = "allow" +type_complexity = "allow" + +[lints.clippy.style] +level = "allow" +priority = -1 + +[lints.rust.unexpected_cfgs] +level = "allow" +priority = 0 diff --git a/vendor/gpui-pre-linux/LICENSE-APACHE b/vendor/gpui-pre-linux/LICENSE-APACHE new file mode 100644 index 000000000..461a0fe5b --- /dev/null +++ b/vendor/gpui-pre-linux/LICENSE-APACHE @@ -0,0 +1,222 @@ +Copyright 2022 - 2025 Zed Industries, Inc. + + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + + http://www.apache.org/licenses/LICENSE-2.0 + + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + + 1. Definitions. + + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + + END OF TERMS AND CONDITIONS diff --git a/vendor/gpui-pre-linux/src/gpui_linux.rs b/vendor/gpui-pre-linux/src/gpui_linux.rs new file mode 100644 index 000000000..62d972dc4 --- /dev/null +++ b/vendor/gpui-pre-linux/src/gpui_linux.rs @@ -0,0 +1,4 @@ +#![cfg(any(target_os = "linux", target_os = "freebsd"))] +mod linux; + +pub use linux::current_platform; diff --git a/vendor/gpui-pre-linux/src/linux.rs b/vendor/gpui-pre-linux/src/linux.rs new file mode 100644 index 000000000..33393f7e9 --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux.rs @@ -0,0 +1,60 @@ +mod dispatcher; +mod headless; +mod keyboard; +mod platform; +mod system_notifications; +#[cfg(any(feature = "wayland", feature = "x11"))] +mod text_system; +#[cfg(feature = "wayland")] +mod wayland; +#[cfg(feature = "x11")] +mod x11; + +#[cfg(any(feature = "wayland", feature = "x11"))] +mod xdg_desktop_portal; + +pub use dispatcher::*; +pub(crate) use headless::*; +pub(crate) use keyboard::*; +pub(crate) use platform::*; +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(crate) use text_system::*; +#[cfg(feature = "wayland")] +pub(crate) use wayland::*; +#[cfg(feature = "x11")] +pub(crate) use x11::*; + +use std::rc::Rc; + +/// Returns the default platform implementation for the current OS. +pub fn current_platform(headless: bool) -> Rc { + #[cfg(feature = "x11")] + use anyhow::Context as _; + + if headless { + return Rc::new(LinuxPlatform { + inner: HeadlessClient::new(), + }); + } + + match gpui::guess_compositor() { + #[cfg(feature = "wayland")] + "Wayland" => Rc::new(LinuxPlatform { + inner: WaylandClient::new(), + }), + + #[cfg(feature = "x11")] + "X11" => Rc::new(LinuxPlatform { + inner: X11Client::new() + .context("Failed to initialize X11 client.") + .unwrap(), + }), + + "Headless" => Rc::new(LinuxPlatform { + inner: HeadlessClient::new(), + }), + _ => unreachable!( + r#"At least one of the "wayland" or "x11" features must be enabled on gpui_linux or gpui_platform."# + ), + } +} diff --git a/vendor/gpui-pre-linux/src/linux/dispatcher.rs b/vendor/gpui-pre-linux/src/linux/dispatcher.rs new file mode 100644 index 000000000..fd9caf89f --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/dispatcher.rs @@ -0,0 +1,365 @@ +use calloop::{ + EventLoop, PostAction, + channel::{self, Sender}, + timer::TimeoutAction, +}; +use gpui_util::ResultExt; + +use std::{mem::MaybeUninit, thread, time::Duration}; + +use gpui::{ + PlatformDispatcher, Priority, PriorityQueueReceiver, PriorityQueueSender, RunnableVariant, + profiler, +}; + +struct TimerAfter { + duration: Duration, + runnable: RunnableVariant, +} + +pub(crate) struct LinuxDispatcher { + main_sender: PriorityQueueCalloopSender, + timer_sender: Sender, + background_sender: PriorityQueueSender, + _background_threads: Vec>, + main_thread_id: thread::ThreadId, +} + +const MIN_THREADS: usize = 2; + +impl LinuxDispatcher { + pub fn new(main_sender: PriorityQueueCalloopSender) -> Self { + let (background_sender, background_receiver) = PriorityQueueReceiver::new(); + let thread_count = + std::thread::available_parallelism().map_or(MIN_THREADS, |i| i.get().max(MIN_THREADS)); + + let mut background_threads = (0..thread_count) + .map(|i| { + let receiver: PriorityQueueReceiver = background_receiver.clone(); + std::thread::Builder::new() + .name(format!("Worker-{i}")) + .spawn(move || { + for runnable in receiver.iter() { + let location = runnable.metadata().location; + let spawned = runnable.metadata().spawned; + profiler::update_running_task(spawned, location); + runnable.run(); + profiler::save_task_timing(); + } + }) + .unwrap() + }) + .collect::>(); + + let (timer_sender, timer_channel) = calloop::channel::channel::(); + let timer_thread = std::thread::Builder::new() + .name("Timer".to_owned()) + .spawn(move || { + let mut event_loop: EventLoop<()> = + EventLoop::try_new().expect("Failed to initialize timer loop!"); + + let handle = event_loop.handle(); + let timer_handle = event_loop.handle(); + handle + .insert_source(timer_channel, move |e, _, _| { + if let channel::Event::Msg(timer) = e { + let mut runnable = Some(timer.runnable); + timer_handle + .insert_source( + calloop::timer::Timer::from_duration(timer.duration), + move |_, _, _| { + if let Some(runnable) = runnable.take() { + let location = runnable.metadata().location; + let spawned = runnable.metadata().spawned; + profiler::update_running_task(spawned, location); + runnable.run(); + profiler::save_task_timing(); + } + TimeoutAction::Drop + }, + ) + .expect("Failed to start timer"); + } + }) + .expect("Failed to start timer thread"); + + event_loop.run(None, &mut (), |_| {}).log_err(); + }) + .unwrap(); + + background_threads.push(timer_thread); + + Self { + main_sender, + timer_sender, + background_sender, + _background_threads: background_threads, + main_thread_id: thread::current().id(), + } + } +} + +impl PlatformDispatcher for LinuxDispatcher { + fn is_main_thread(&self) -> bool { + thread::current().id() == self.main_thread_id + } + + fn dispatch(&self, runnable: RunnableVariant, priority: Priority) { + self.background_sender + .send(priority, runnable) + .unwrap_or_else(|_| panic!("blocking sender returned without value")); + } + + fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) { + self.main_sender + .send(priority, runnable) + .unwrap_or_else(|runnable| { + // NOTE: Runnable may wrap a Future that is !Send. + // + // This is usually safe because we only poll it on the main thread. + // However if the send fails, we know that: + // 1. main_receiver has been dropped (which implies the app is shutting down) + // 2. we are on a background thread. + // It is not safe to drop something !Send on the wrong thread, and + // the app will exit soon anyway, so we must forget the runnable. + std::mem::forget(runnable); + }); + } + + fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) { + if let Err(err) = self.timer_sender.send(TimerAfter { duration, runnable }) { + // The timer thread has shut down. Dropping a scheduled runnable cancels its task + // and makes the next poll of any awaiter panic. Leaking leaves the task pending, + // which is acceptable during shutdown. + std::mem::forget(err); + } + } + + fn spawn_realtime(&self, f: Box) { + std::thread::spawn(move || { + // SAFETY: always safe to call + let thread_id = unsafe { libc::pthread_self() }; + + let policy = libc::SCHED_FIFO; + let sched_priority = 65; + + // SAFETY: all sched_param members are valid when initialized to zero. + let mut sched_param = + unsafe { MaybeUninit::::zeroed().assume_init() }; + sched_param.sched_priority = sched_priority; + // SAFETY: sched_param is a valid initialized structure + let result = unsafe { libc::pthread_setschedparam(thread_id, policy, &sched_param) }; + if result != 0 { + log::warn!("failed to set realtime thread priority"); + } + + f(); + }); + } +} + +pub struct PriorityQueueCalloopSender { + sender: PriorityQueueSender, + ping: calloop::ping::Ping, +} + +impl PriorityQueueCalloopSender { + fn new(tx: PriorityQueueSender, ping: calloop::ping::Ping) -> Self { + Self { sender: tx, ping } + } + + fn send(&self, priority: Priority, item: T) -> Result<(), gpui::queue::SendError> { + let res = self.sender.send(priority, item); + if res.is_ok() { + self.ping.ping(); + } + res + } +} + +impl Drop for PriorityQueueCalloopSender { + fn drop(&mut self) { + self.ping.ping(); + } +} + +pub struct PriorityQueueCalloopReceiver { + receiver: PriorityQueueReceiver, + source: calloop::ping::PingSource, + ping: calloop::ping::Ping, +} + +impl PriorityQueueCalloopReceiver { + pub fn new() -> (PriorityQueueCalloopSender, Self) { + let (ping, source) = calloop::ping::make_ping().expect("Failed to create a Ping."); + + let (tx, rx) = PriorityQueueReceiver::new(); + + ( + PriorityQueueCalloopSender::new(tx, ping.clone()), + Self { + receiver: rx, + source, + ping, + }, + ) + } +} + +use calloop::channel::Event; + +#[derive(Debug)] +pub struct ChannelError(calloop::ping::PingError); + +impl std::fmt::Display for ChannelError { + #[cfg_attr(feature = "nightly_coverage", coverage(off))] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } +} + +impl std::error::Error for ChannelError { + #[cfg_attr(feature = "nightly_coverage", coverage(off))] + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.0) + } +} + +impl calloop::EventSource for PriorityQueueCalloopReceiver { + type Event = Event; + type Metadata = (); + type Ret = (); + type Error = ChannelError; + + fn process_events( + &mut self, + readiness: calloop::Readiness, + token: calloop::Token, + mut callback: F, + ) -> Result + where + F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, + { + let mut clear_readiness = false; + let mut disconnected = false; + + let action = self + .source + .process_events(readiness, token, |(), &mut ()| { + let mut is_empty = true; + + let receiver = self.receiver.clone(); + for runnable in receiver.try_iter() { + match runnable { + Ok(r) => { + callback(Event::Msg(r), &mut ()); + is_empty = false; + } + Err(_) => { + disconnected = true; + } + } + } + + if disconnected { + callback(Event::Closed, &mut ()); + } + + if is_empty { + clear_readiness = true; + } + }) + .map_err(ChannelError)?; + + if disconnected { + Ok(PostAction::Remove) + } else if clear_readiness { + Ok(action) + } else { + // Re-notify the ping source so we can try again. + self.ping.ping(); + Ok(PostAction::Continue) + } + } + + fn register( + &mut self, + poll: &mut calloop::Poll, + token_factory: &mut calloop::TokenFactory, + ) -> calloop::Result<()> { + self.source.register(poll, token_factory) + } + + fn reregister( + &mut self, + poll: &mut calloop::Poll, + token_factory: &mut calloop::TokenFactory, + ) -> calloop::Result<()> { + self.source.reregister(poll, token_factory) + } + + fn unregister(&mut self, poll: &mut calloop::Poll) -> calloop::Result<()> { + self.source.unregister(poll) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn calloop_works() { + let mut event_loop = calloop::EventLoop::try_new().unwrap(); + let handle = event_loop.handle(); + + let (tx, rx) = PriorityQueueCalloopReceiver::new(); + + struct Data { + got_msg: bool, + got_closed: bool, + } + + let mut data = Data { + got_msg: false, + got_closed: false, + }; + + let _channel_token = handle + .insert_source(rx, move |evt, &mut (), data: &mut Data| match evt { + Event::Msg(()) => { + data.got_msg = true; + } + + Event::Closed => { + data.got_closed = true; + } + }) + .unwrap(); + + // nothing is sent, nothing is received + event_loop + .dispatch(Some(::std::time::Duration::ZERO), &mut data) + .unwrap(); + + assert!(!data.got_msg); + assert!(!data.got_closed); + // a message is send + + tx.send(Priority::Medium, ()).unwrap(); + event_loop + .dispatch(Some(::std::time::Duration::ZERO), &mut data) + .unwrap(); + + assert!(data.got_msg); + assert!(!data.got_closed); + + // the sender is dropped + drop(tx); + event_loop + .dispatch(Some(::std::time::Duration::ZERO), &mut data) + .unwrap(); + + assert!(data.got_msg); + assert!(data.got_closed); + } +} diff --git a/vendor/gpui-pre-linux/src/linux/headless.rs b/vendor/gpui-pre-linux/src/linux/headless.rs new file mode 100644 index 000000000..6667a3b5a --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/headless.rs @@ -0,0 +1,4 @@ +mod client; +mod window; + +pub(crate) use client::*; diff --git a/vendor/gpui-pre-linux/src/linux/headless/client.rs b/vendor/gpui-pre-linux/src/linux/headless/client.rs new file mode 100644 index 000000000..0e94a0600 --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/headless/client.rs @@ -0,0 +1,143 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use calloop::{EventLoop, LoopHandle}; +use gpui_util::ResultExt; + +use crate::linux::headless::window::{HeadlessDisplay, HeadlessWindow}; +use crate::linux::{LinuxClient, LinuxCommon, LinuxKeyboardLayout}; +use gpui::{ + AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, PlatformKeyboardLayout, + PlatformWindow, WindowParams, +}; + +pub struct HeadlessClientState { + pub(crate) _loop_handle: LoopHandle<'static, HeadlessClient>, + pub(crate) event_loop: Option>, + pub(crate) common: LinuxCommon, + pub(crate) display: Rc, +} + +#[derive(Clone)] +pub(crate) struct HeadlessClient(Rc>); + +impl HeadlessClient { + pub(crate) fn new() -> Self { + let event_loop = EventLoop::try_new().unwrap(); + + let (common, main_receiver, power_receiver) = LinuxCommon::new(event_loop.get_signal()); + + let handle = event_loop.handle(); + + handle + .insert_source(main_receiver, |event, _, _: &mut HeadlessClient| { + if let calloop::channel::Event::Msg(runnable) = event { + runnable.run(); + } + }) + .ok(); + + handle + .insert_source(power_receiver, |event, _, client: &mut HeadlessClient| { + if let calloop::channel::Event::Msg(event) = event { + client.with_common(|common| common.handle_system_power_event(event)); + } + }) + .ok(); + + HeadlessClient(Rc::new(RefCell::new(HeadlessClientState { + event_loop: Some(event_loop), + _loop_handle: handle, + common, + display: Rc::new(HeadlessDisplay::new()), + }))) + } +} + +impl LinuxClient for HeadlessClient { + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { + f(&mut self.0.borrow_mut().common) + } + + fn keyboard_layout(&self) -> Box { + Box::new(LinuxKeyboardLayout::new("unknown".into())) + } + + fn displays(&self) -> Vec> { + vec![self.0.borrow().display.clone()] + } + + fn primary_display(&self) -> Option> { + Some(self.0.borrow().display.clone()) + } + + fn display(&self, id: DisplayId) -> Option> { + let display = self.0.borrow().display.clone(); + (display.id() == id).then_some(display) + } + + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> futures::channel::oneshot::Receiver>>> + { + let (tx, rx) = futures::channel::oneshot::channel(); + tx.send(Err(anyhow::anyhow!( + "Headless mode does not support screen capture." + ))) + .ok(); + rx + } + + fn active_window(&self) -> Option { + None + } + + fn window_stack(&self) -> Option> { + None + } + + fn open_window( + &self, + _handle: AnyWindowHandle, + params: WindowParams, + ) -> anyhow::Result> { + Ok(Box::new(HeadlessWindow::new( + params, + self.0.borrow().display.clone(), + ))) + } + + fn compositor_name(&self) -> &'static str { + "headless" + } + + fn set_cursor_style(&self, _style: CursorStyle) {} + + fn open_uri(&self, _uri: &str) {} + + fn reveal_path(&self, _path: std::path::PathBuf) {} + + fn write_to_primary(&self, _item: gpui::ClipboardItem) {} + + fn write_to_clipboard(&self, _item: gpui::ClipboardItem) {} + + fn read_from_primary(&self) -> Option { + None + } + + fn read_from_clipboard(&self) -> Option { + None + } + + fn run(&self) { + let mut event_loop = self + .0 + .borrow_mut() + .event_loop + .take() + .expect("App is already running"); + + event_loop.run(None, &mut self.clone(), |_| {}).log_err(); + } +} diff --git a/vendor/gpui-pre-linux/src/linux/headless/window.rs b/vendor/gpui-pre-linux/src/linux/headless/window.rs new file mode 100644 index 000000000..35096d922 --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/headless/window.rs @@ -0,0 +1,236 @@ +//! Windows for the headless platform client. +//! +//! A headless window has no compositor surface and no GPU: layout, text +//! shaping, and entity plumbing run normally, `draw` discards the scene, and +//! the sprite atlas hands out tiles without uploading pixels (the same +//! `HeadlessAtlas` GPUI's `TestWindow` uses). This lets command-line tools +//! drive real `Window`-based code paths without a display server. + +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::Arc; + +use uuid::Uuid; + +use gpui::{ + Bounds, Capslock, DispatchEventResult, DisplayId, GpuSpecs, HeadlessAtlas, Modifiers, Pixels, + PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, + PromptButton, PromptLevel, RequestFrameOptions, Scene, Size, WindowAppearance, + WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowParams, WindowVisibility, + px, +}; + +#[derive(Debug)] +pub(crate) struct HeadlessDisplay { + bounds: Bounds, +} + +impl HeadlessDisplay { + pub(crate) fn new() -> Self { + Self { + bounds: Bounds::from_corners(Point::default(), Point::new(px(1920.), px(1080.))), + } + } +} + +impl PlatformDisplay for HeadlessDisplay { + fn id(&self) -> DisplayId { + DisplayId::new(0) + } + + fn uuid(&self) -> anyhow::Result { + // Stable identity: there is exactly one headless display. + Ok(Uuid::nil()) + } + + fn bounds(&self) -> Bounds { + self.bounds + } +} + +struct HeadlessWindowState { + bounds: Bounds, + display: Rc, + input_handler: Option, + title: Option, + is_fullscreen: bool, +} + +pub(crate) struct HeadlessWindow(Rc>); + +impl raw_window_handle::HasWindowHandle for HeadlessWindow { + fn window_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + // Headless windows are not backed by a native window. + Err(raw_window_handle::HandleError::NotSupported) + } +} + +impl raw_window_handle::HasDisplayHandle for HeadlessWindow { + fn display_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + Err(raw_window_handle::HandleError::NotSupported) + } +} + +impl HeadlessWindow { + pub(crate) fn new(params: WindowParams, display: Rc) -> Self { + Self(Rc::new(RefCell::new(HeadlessWindowState { + bounds: params.bounds, + display, + input_handler: None, + title: None, + is_fullscreen: false, + }))) + } +} + +impl PlatformWindow for HeadlessWindow { + fn bounds(&self) -> Bounds { + self.0.borrow().bounds + } + + fn is_maximized(&self) -> bool { + false + } + + fn window_bounds(&self) -> WindowBounds { + WindowBounds::Windowed(self.bounds()) + } + + fn content_size(&self) -> Size { + self.bounds().size + } + + fn resize(&mut self, size: Size) { + self.0.borrow_mut().bounds.size = size; + } + + fn scale_factor(&self) -> f32 { + 1.0 + } + + fn appearance(&self) -> WindowAppearance { + WindowAppearance::Dark + } + + fn display(&self) -> Option> { + Some(self.0.borrow().display.clone()) + } + + fn mouse_position(&self) -> Point { + Point::default() + } + + fn modifiers(&self) -> Modifiers { + Modifiers::default() + } + + fn capslock(&self) -> Capslock { + Capslock::default() + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.0.borrow_mut().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.0.borrow_mut().input_handler.take() + } + + fn prompt( + &self, + _level: PromptLevel, + _msg: &str, + _detail: Option<&str>, + _answers: &[PromptButton], + ) -> Option> { + // Fall back to GPUI's rendered prompts. + None + } + + fn activate(&self) {} + + fn is_active(&self) -> bool { + false + } + + fn visibility(&self) -> WindowVisibility { + // There is no display server: `draw` discards every scene. + WindowVisibility::Hidden + } + + fn is_hovered(&self) -> bool { + false + } + + fn background_appearance(&self) -> WindowBackgroundAppearance { + WindowBackgroundAppearance::Opaque + } + + fn set_title(&mut self, title: &str) { + self.0.borrow_mut().title = Some(title.to_owned()); + } + + fn get_title(&self) -> String { + self.0.borrow().title.clone().unwrap_or_default() + } + + fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {} + + fn minimize(&self) {} + + fn zoom(&self) {} + + fn toggle_fullscreen(&self) { + let mut state = self.0.borrow_mut(); + state.is_fullscreen = !state.is_fullscreen; + } + + fn is_fullscreen(&self) -> bool { + self.0.borrow().is_fullscreen + } + + // No compositor drives a frame loop, so frame and status callbacks are + // dropped: anything that awaits a frame will never resolve headlessly. + fn on_request_frame(&self, _callback: Box) {} + + fn on_input(&self, _callback: Box DispatchEventResult>) {} + + fn on_active_status_change(&self, _callback: Box) {} + + fn on_visibility_change(&self, _callback: Box) {} + + fn on_hover_status_change(&self, _callback: Box) {} + + fn on_resize(&self, _callback: Box, f32)>) {} + + fn on_moved(&self, _callback: Box) {} + + fn on_should_close(&self, _callback: Box bool>) {} + + fn on_close(&self, _callback: Box) {} + + fn on_hit_test_window_control(&self, _callback: Box Option>) { + } + + fn on_appearance_changed(&self, _callback: Box) {} + + fn draw(&self, _scene: &Scene) {} + + fn sprite_atlas(&self) -> Arc { + Arc::new(HeadlessAtlas::default()) + } + + fn is_subpixel_rendering_supported(&self) -> bool { + false + } + + fn update_ime_position(&self, _bounds: Bounds) {} + + fn gpu_specs(&self) -> Option { + None + } +} diff --git a/vendor/gpui-pre-linux/src/linux/keyboard.rs b/vendor/gpui-pre-linux/src/linux/keyboard.rs new file mode 100644 index 000000000..d810a2f4f --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/keyboard.rs @@ -0,0 +1,22 @@ +use gpui::{PlatformKeyboardLayout, SharedString}; + +#[derive(Clone)] +pub(crate) struct LinuxKeyboardLayout { + name: SharedString, +} + +impl PlatformKeyboardLayout for LinuxKeyboardLayout { + fn id(&self) -> &str { + &self.name + } + + fn name(&self) -> &str { + &self.name + } +} + +impl LinuxKeyboardLayout { + pub(crate) fn new(name: SharedString) -> Self { + Self { name } + } +} diff --git a/vendor/gpui-pre-linux/src/linux/platform.rs b/vendor/gpui-pre-linux/src/linux/platform.rs new file mode 100644 index 000000000..7bf77a1bc --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/platform.rs @@ -0,0 +1,1604 @@ +use std::{ + env, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, +}; +#[cfg(any(feature = "wayland", feature = "x11"))] +use std::{ + ffi::OsString, + fs::File, + io::Read as _, + os::fd::{AsFd, AsRawFd}, + time::Duration, +}; + +#[cfg(any(feature = "wayland", feature = "x11"))] +use anyhow::ensure; +use anyhow::{Context as _, anyhow}; +#[cfg(any(feature = "wayland", feature = "x11"))] +use ashpd::{ + desktop::{ + Request, + inhibit::{InhibitFlags, InhibitOptions, InhibitProxy}, + }, + enumflags2::BitFlags, +}; +use calloop::{LoopSignal, channel::Sender}; +use futures::channel::oneshot; +use gpui_util::{ResultExt as _, new_std_command}; +#[cfg(any(feature = "wayland", feature = "x11"))] +use xkbcommon::xkb::{self, Keycode, Keysym, State}; + +use crate::linux::{LinuxDispatcher, PriorityQueueCalloopReceiver}; +use gpui::{ + Action, ActivityGuard, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, + DisplayId, ForegroundExecutor, Keymap, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, + PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, + PlatformWindow, Result, RunnableVariant, Task, ThermalState, WindowAppearance, + WindowButtonLayout, WindowParams, +}; +#[cfg(any(feature = "wayland", feature = "x11"))] +use gpui::{Pixels, Point, px}; + +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(crate) const SCROLL_LINES: f32 = 3.0; + +// Values match the defaults on GTK. +// Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320 +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400); +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0); +pub(crate) const KEYRING_LABEL: &str = "zed-github-account"; + +#[cfg(any(feature = "wayland", feature = "x11"))] +const FILE_PICKER_PORTAL_MISSING: &str = + "Couldn't open file picker due to missing xdg-desktop-portal implementation."; + +pub(crate) trait LinuxClient { + fn compositor_name(&self) -> &'static str; + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R; + fn keyboard_layout(&self) -> Box; + fn displays(&self) -> Vec>; + #[allow(unused)] + fn display(&self, id: DisplayId) -> Option>; + fn primary_display(&self) -> Option>; + + #[cfg(feature = "screen-capture")] + fn is_screen_capture_supported(&self) -> bool { + true + } + + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + let (sources_tx, sources_rx) = oneshot::channel(); + sources_tx + .send(Err(anyhow::anyhow!( + "gpui_linux was compiled without the screen-capture feature" + ))) + .ok(); + sources_rx + } + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> anyhow::Result>; + fn set_cursor_style(&self, style: CursorStyle); + fn hide_cursor_until_mouse_moves(&self) {} + fn is_cursor_visible(&self) -> bool { + true + } + fn open_uri(&self, uri: &str); + fn reveal_path(&self, path: PathBuf); + fn write_to_primary(&self, item: ClipboardItem); + fn write_to_clipboard(&self, item: ClipboardItem); + fn read_from_primary(&self) -> Option; + fn read_from_clipboard(&self) -> Option; + fn active_window(&self) -> Option; + fn window_stack(&self) -> Option>; + fn run(&self); + + #[cfg(any(feature = "wayland", feature = "x11"))] + fn window_identifier( + &self, + ) -> impl Future> + Send + 'static { + std::future::ready::>(None) + } +} + +#[derive(Default)] +pub(crate) struct PlatformHandlers { + pub(crate) open_urls: Option)>>, + pub(crate) quit: Option bool>>, + pub(crate) reopen: Option>, + pub(crate) app_menu_action: Option>, + pub(crate) will_open_app_menu: Option>, + pub(crate) validate_app_menu_command: Option bool>>, + pub(crate) keyboard_layout_change: Option>, + pub(crate) system_sleep: Option>, + pub(crate) system_wake: Option>, +} + +/// A logind `PrepareForSleep` signal, forwarded from the D-Bus listener to +/// the client's event loop. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + not(all(target_os = "linux", any(feature = "wayland", feature = "x11"))), + allow(dead_code) +)] +pub(crate) enum SystemPowerEvent { + Sleep, + Wake, +} + +pub(crate) struct LinuxCommon { + pub(crate) background_executor: BackgroundExecutor, + pub(crate) foreground_executor: ForegroundExecutor, + pub(crate) text_system: Arc, + pub(crate) appearance: WindowAppearance, + pub(crate) auto_hide_scrollbars: bool, + pub(crate) button_layout: WindowButtonLayout, + pub(crate) callbacks: PlatformHandlers, + pub(crate) signal: LoopSignal, + pub(crate) menus: Vec, + app_name: Option, + system_notifications: crate::linux::system_notifications::SystemNotificationState, + #[cfg_attr( + not(all(target_os = "linux", any(feature = "wayland", feature = "x11"))), + allow(dead_code) + )] + power_sender: Sender, + power_listener_started: bool, +} + +impl LinuxCommon { + pub fn new( + signal: LoopSignal, + ) -> ( + Self, + PriorityQueueCalloopReceiver, + calloop::channel::Channel, + ) { + let (main_sender, main_receiver) = PriorityQueueCalloopReceiver::new(); + let (power_sender, power_receiver) = calloop::channel::channel(); + + #[cfg(any(feature = "wayland", feature = "x11"))] + let text_system = Arc::new(crate::linux::CosmicTextSystem::new("IBM Plex Sans")); + #[cfg(not(any(feature = "wayland", feature = "x11")))] + let text_system = Arc::new(gpui::NoopTextSystem::new()); + + let callbacks = PlatformHandlers::default(); + + let dispatcher = Arc::new(LinuxDispatcher::new(main_sender)); + + let background_executor = BackgroundExecutor::new(dispatcher.clone()); + + let common = LinuxCommon { + background_executor, + foreground_executor: ForegroundExecutor::new(dispatcher), + text_system, + appearance: WindowAppearance::Light, + auto_hide_scrollbars: false, + button_layout: WindowButtonLayout::linux_default(), + callbacks, + signal, + menus: Vec::new(), + app_name: None, + system_notifications: crate::linux::system_notifications::SystemNotificationState::new( + ), + power_sender, + power_listener_started: false, + }; + + (common, main_receiver, power_receiver) + } + + pub(crate) fn start_power_listener(&mut self) { + if !self.power_listener_started { + #[cfg(all(target_os = "linux", any(feature = "wayland", feature = "x11")))] + smol::spawn({ + let power_sender = self.power_sender.clone(); + async move { + if let Err(error) = listen_for_system_power_events(power_sender).await { + log::debug!("failed to listen for system sleep/wake events: {error:?}"); + } + } + }) + .detach(); + + self.power_listener_started = true; + } + } + + pub(crate) fn handle_system_power_event(&mut self, event: SystemPowerEvent) { + let callback = match event { + SystemPowerEvent::Sleep => &mut self.callbacks.system_sleep, + SystemPowerEvent::Wake => &mut self.callbacks.system_wake, + }; + if let Some(callback) = callback.as_mut() { + callback(); + } + } +} + +#[cfg(all(target_os = "linux", any(feature = "wayland", feature = "x11")))] +async fn listen_for_system_power_events( + power_sender: Sender, +) -> anyhow::Result<()> { + use futures::StreamExt as _; + + let connection = ashpd::zbus::Connection::system().await?; + let proxy = ashpd::zbus::Proxy::new( + &connection, + "org.freedesktop.login1", + "/org/freedesktop/login1", + "org.freedesktop.login1.Manager", + ) + .await?; + let mut sleep_events = proxy.receive_signal("PrepareForSleep").await?; + + while let Some(message) = sleep_events.next().await { + let event = if message.body().deserialize::()? { + SystemPowerEvent::Sleep + } else { + SystemPowerEvent::Wake + }; + power_sender.send(event).ok(); + } + + Ok(()) +} + +pub(crate) struct LinuxPlatform

{ + pub(crate) inner: P, +} + +impl Platform for LinuxPlatform

{ + fn background_executor(&self) -> BackgroundExecutor { + self.inner + .with_common(|common| common.background_executor.clone()) + } + + fn foreground_executor(&self) -> ForegroundExecutor { + self.inner + .with_common(|common| common.foreground_executor.clone()) + } + + fn text_system(&self) -> Arc { + self.inner.with_common(|common| common.text_system.clone()) + } + + fn keyboard_layout(&self) -> Box { + self.inner.keyboard_layout() + } + + fn keyboard_mapper(&self) -> Rc { + Rc::new(gpui::DummyKeyboardMapper) + } + + fn on_keyboard_layout_change(&self, callback: Box) { + self.inner + .with_common(|common| common.callbacks.keyboard_layout_change = Some(callback)); + } + + fn on_thermal_state_change(&self, _callback: Box) {} + + fn thermal_state(&self) -> ThermalState { + ThermalState::Nominal + } + + #[cfg(not(any(feature = "wayland", feature = "x11")))] + fn prevent_idle_sleep(&self, reason: &str) -> Task> { + Task::ready(Err(anyhow!( + "Idle sleep prevention for {reason:?} requires a Linux windowing backend" + ))) + } + + #[cfg(any(feature = "wayland", feature = "x11"))] + fn prevent_idle_sleep(&self, reason: &str) -> Task> { + let executor = self.background_executor(); + let (guard_tx, guard_rx) = oneshot::channel(); + executor + .spawn({ + let executor = executor.clone(); + let reason = reason.to_owned(); + async move { + guard_tx + .send(inhibit_idle_sleep(reason, executor).await) + .ok(); + } + }) + .detach(); + executor + .clone() + .spawn(async move { await_idle_sleep_prevention(guard_rx, &executor).await }) + } + + fn run(&self, on_finish_launching: Box) { + on_finish_launching(); + + LinuxClient::run(&self.inner); + + let quit = self + .inner + .with_common(|common| common.callbacks.quit.take()); + if let Some(mut fun) = quit { + fun(); + } + } + + fn quit(&self) { + self.inner.with_common(|common| common.signal.stop()); + } + + fn compositor_name(&self) -> &'static str { + self.inner.compositor_name() + } + + fn restart(&self, binary_path: Option, arguments: Vec) { + use std::os::unix::process::CommandExt as _; + + // get the process id of the current process + let app_pid = std::process::id().to_string(); + // get the path to the executable + let app_path = if let Some(path) = binary_path { + path + } else { + match self.app_path() { + Ok(path) => path, + Err(err) => { + log::error!("Failed to get app path: {:?}", err); + return; + } + } + }; + + log::info!("Restarting process, using app path: {:?}", app_path); + + // Script to wait for the current process to exit and then restart the app. + // Pass dynamic values as positional parameters to avoid shell interpolation issues. + let script = r#" + while kill -0 "$0" 2>/dev/null; do + sleep 0.1 + done + + app_path="$1" + shift + "$app_path" "$@" + "#; + + #[allow( + clippy::disallowed_methods, + reason = "We are restarting ourselves, using std command thus is fine" + )] + let restart_process = new_std_command("/usr/bin/env") + .arg("bash") + .arg("-c") + .arg(script) + .arg(&app_pid) + .arg(&app_path) + .args(arguments) + .process_group(0) + .spawn(); + + match restart_process { + Ok(_) => self.quit(), + Err(e) => log::error!("failed to spawn restart script: {:?}", e), + } + } + + fn activate(&self, _ignoring_other_apps: bool) { + log::info!("activate is not implemented on Linux, ignoring the call") + } + + fn hide(&self) { + log::info!("hide is not implemented on Linux, ignoring the call") + } + + fn hide_other_apps(&self) { + log::info!("hide_other_apps is not implemented on Linux, ignoring the call") + } + + fn unhide_other_apps(&self) { + log::info!("unhide_other_apps is not implemented on Linux, ignoring the call") + } + + fn primary_display(&self) -> Option> { + self.inner.primary_display() + } + + fn displays(&self) -> Vec> { + self.inner.displays() + } + + #[cfg(feature = "screen-capture")] + fn is_screen_capture_supported(&self) -> bool { + self.inner.is_screen_capture_supported() + } + + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + self.inner.screen_capture_sources() + } + + fn active_window(&self) -> Option { + self.inner.active_window() + } + + fn window_stack(&self) -> Option> { + self.inner.window_stack() + } + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> anyhow::Result> { + self.inner.open_window(handle, options) + } + + fn open_url(&self, url: &str) { + self.inner.open_uri(url); + } + + fn on_open_urls(&self, callback: Box)>) { + self.inner + .with_common(|common| common.callbacks.open_urls = Some(callback)); + } + + fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>>> { + let (done_tx, done_rx) = oneshot::channel(); + + #[cfg(not(any(feature = "wayland", feature = "x11")))] + let _ = (done_tx.send(Ok(None)), options); + + #[cfg(any(feature = "wayland", feature = "x11"))] + let identifier = self.inner.window_identifier(); + + #[cfg(any(feature = "wayland", feature = "x11"))] + self.foreground_executor() + .spawn(async move { + let title = if options.directories { + "Open Folder" + } else { + "Open File" + }; + + let request = match ashpd::desktop::file_chooser::OpenFileRequest::default() + .identifier(identifier.await) + .modal(true) + .title(title) + .accept_label(options.prompt.as_ref().map(gpui::SharedString::as_str)) + .multiple(options.multiple) + .directory(options.directories) + .send() + .await + { + Ok(request) => request, + Err(err) => { + let result = match err { + ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING), + err => err.into(), + }; + let _ = done_tx.send(Err(result)); + return; + } + }; + + let result = match request.response() { + Ok(response) => Ok(Some( + response + .uris() + .iter() + .filter_map(|uri: &ashpd::Uri| url::Url::parse(uri.as_str()).ok()) + .filter_map(|uri: url::Url| uri.to_file_path().ok()) + .collect::>(), + )), + Err(ashpd::Error::Response(_)) => Ok(None), + Err(e) => Err(e.into()), + }; + let _ = done_tx.send(result); + }) + .detach(); + done_rx + } + + fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> oneshot::Receiver>> { + let (done_tx, done_rx) = oneshot::channel(); + + #[cfg(not(any(feature = "wayland", feature = "x11")))] + let _ = (done_tx.send(Ok(None)), directory, suggested_name); + + #[cfg(any(feature = "wayland", feature = "x11"))] + let identifier = self.inner.window_identifier(); + + #[cfg(any(feature = "wayland", feature = "x11"))] + self.foreground_executor() + .spawn({ + let directory = directory.to_owned(); + let suggested_name = suggested_name.map(|s| s.to_owned()); + + async move { + let mut request_builder = + ashpd::desktop::file_chooser::SaveFileRequest::default() + .identifier(identifier.await) + .modal(true) + .title("Save File") + .current_folder(directory) + .expect("pathbuf should not be nul terminated"); + + if let Some(suggested_name) = suggested_name { + request_builder = request_builder.current_name(suggested_name.as_str()); + } + + let request = match request_builder.send().await { + Ok(request) => request, + Err(err) => { + let result = match err { + ashpd::Error::PortalNotFound(_) => { + anyhow!(FILE_PICKER_PORTAL_MISSING) + } + err => err.into(), + }; + let _ = done_tx.send(Err(result)); + return; + } + }; + + let result = match request.response() { + Ok(response) => Ok(response + .uris() + .first() + .and_then(|uri: &ashpd::Uri| url::Url::parse(uri.as_str()).ok()) + .and_then(|uri: url::Url| uri.to_file_path().ok())), + Err(ashpd::Error::Response(_)) => Ok(None), + Err(e) => Err(e.into()), + }; + let _ = done_tx.send(result); + } + }) + .detach(); + + done_rx + } + + fn can_select_mixed_files_and_dirs(&self) -> bool { + // org.freedesktop.portal.FileChooser only supports "pick files" and "pick directories". + false + } + + fn reveal_path(&self, path: &Path) { + self.inner.reveal_path(path.to_owned()); + } + + fn open_with_system(&self, path: &Path) { + let path = path.to_owned(); + self.background_executor() + .spawn(async move { + #[allow( + clippy::disallowed_methods, + reason = "running on a background thread, so blocking is fine" + )] + new_std_command("xdg-open") + .arg(path) + .status() + .context("invoking xdg-open") + .log_err(); + }) + .detach(); + } + + fn on_quit(&self, callback: Box bool>) { + self.inner.with_common(|common| { + common.callbacks.quit = Some(callback); + }); + } + + fn on_reopen(&self, callback: Box) { + self.inner.with_common(|common| { + common.callbacks.reopen = Some(callback); + }); + } + + fn on_system_sleep(&self, callback: Box) { + self.inner.with_common(|common| { + common.callbacks.system_sleep = Some(callback); + common.start_power_listener(); + }); + } + + fn on_system_wake(&self, callback: Box) { + self.inner.with_common(|common| { + common.callbacks.system_wake = Some(callback); + common.start_power_listener(); + }); + } + + fn set_app_identity(&self, _identifier: &str, name: &str) { + self.inner + .with_common(|common| common.app_name = Some(name.to_string())); + } + + fn show_system_notification(&self, notification: gpui::SystemNotification) { + self.inner.with_common(|common| { + common + .system_notifications + .show(common.app_name.as_deref(), notification) + }); + } + + fn dismiss_system_notification(&self, tag: &str) { + self.inner + .with_common(|common| common.system_notifications.dismiss(tag)); + } + + fn on_system_notification_response( + &self, + callback: Box, + ) { + self.inner.with_common(|common| { + let executor = common.foreground_executor.clone(); + common.system_notifications.on_response(&executor, callback) + }); + } + + fn on_app_menu_action(&self, callback: Box) { + self.inner.with_common(|common| { + common.callbacks.app_menu_action = Some(callback); + }); + } + + fn on_will_open_app_menu(&self, callback: Box) { + self.inner.with_common(|common| { + common.callbacks.will_open_app_menu = Some(callback); + }); + } + + fn on_validate_app_menu_command(&self, callback: Box bool>) { + self.inner.with_common(|common| { + common.callbacks.validate_app_menu_command = Some(callback); + }); + } + + fn app_path(&self) -> Result { + // get the path of the executable of the current process + let app_path = env::current_exe()?; + Ok(app_path) + } + + fn set_menus(&self, menus: Vec

, _keymap: &Keymap) { + self.inner.with_common(|common| { + common.menus = menus.into_iter().map(|menu| menu.owned()).collect(); + }) + } + + fn get_menus(&self) -> Option> { + self.inner.with_common(|common| Some(common.menus.clone())) + } + + fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) { + // todo(linux) + } + + fn path_for_auxiliary_executable(&self, _name: &str) -> Result { + Err(anyhow::Error::msg( + "Platform::path_for_auxiliary_executable is not implemented yet", + )) + } + + fn set_cursor_style(&self, style: CursorStyle) { + self.inner.set_cursor_style(style) + } + + fn hide_cursor_until_mouse_moves(&self) { + self.inner.hide_cursor_until_mouse_moves() + } + + fn is_cursor_visible(&self) -> bool { + self.inner.is_cursor_visible() + } + + fn should_auto_hide_scrollbars(&self) -> bool { + self.inner.with_common(|common| common.auto_hide_scrollbars) + } + + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { + let url = url.to_string(); + let username = username.to_string(); + let password = password.to_vec(); + self.background_executor().spawn(async move { + let keyring = oo7::Keyring::new().await?; + keyring.unlock().await?; + keyring + .create_item( + KEYRING_LABEL, + &vec![("url", &url), ("username", &username)], + password, + true, + ) + .await?; + Ok(()) + }) + } + + fn read_credentials(&self, url: &str) -> Task)>>> { + let url = url.to_string(); + self.background_executor().spawn(async move { + let keyring = oo7::Keyring::new().await?; + keyring.unlock().await?; + + let items = keyring.search_items(&vec![("url", &url)]).await?; + + for item in items.into_iter() { + if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) { + let attributes = item.attributes().await?; + let username = attributes + .get("username") + .context("Cannot find username in stored credentials")?; + item.unlock().await?; + let secret = item.secret().await?; + + // we lose the zeroizing capabilities at this boundary, + // a current limitation GPUI's credentials api + return Ok(Some((username.to_string(), secret.to_vec()))); + } else { + continue; + } + } + Ok(None) + }) + } + + fn delete_credentials(&self, url: &str) -> Task> { + let url = url.to_string(); + self.background_executor().spawn(async move { + let keyring = oo7::Keyring::new().await?; + keyring.unlock().await?; + + let items = keyring.search_items(&vec![("url", &url)]).await?; + + for item in items.into_iter() { + if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) { + item.delete().await?; + return Ok(()); + } + } + + Ok(()) + }) + } + + fn window_appearance(&self) -> WindowAppearance { + self.inner.with_common(|common| common.appearance) + } + + fn button_layout(&self) -> Option { + Some(self.inner.with_common(|common| common.button_layout)) + } + + fn register_url_scheme(&self, _: &str) -> Task> { + Task::ready(Err(anyhow!("register_url_scheme unimplemented"))) + } + + fn write_to_primary(&self, item: ClipboardItem) { + self.inner.write_to_primary(item) + } + + fn write_to_clipboard(&self, item: ClipboardItem) { + self.inner.write_to_clipboard(item) + } + + fn read_from_primary(&self) -> Option { + self.inner.read_from_primary() + } + + fn read_from_clipboard(&self) -> Option { + self.inner.read_from_clipboard() + } + + fn add_recent_document(&self, _path: &Path) {} +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) fn open_uri_internal( + executor: BackgroundExecutor, + uri: &str, + activation_token: Option, +) { + if let Some(uri) = ashpd::Uri::parse(uri).log_err() { + executor + .spawn(async move { + let mut xdg_open_failed = false; + for mut command in open::commands(uri.to_string()) { + if let Some(token) = activation_token.as_ref() { + command.env("XDG_ACTIVATION_TOKEN", token); + } + let program = format!("{:?}", command.get_program()); + match smol::process::Command::from(command).spawn() { + Ok(mut cmd) => match cmd.status().await { + Ok(status) if status.success() => return, + Ok(status) => { + log::error!("Command {} exited with status: {}", program, status); + xdg_open_failed = true; + } + Err(e) => { + log::error!("Failed to get status from {}: {}", program, e); + xdg_open_failed = true; + } + }, + Err(e) => { + log::error!("Failed to open with {}: {}", program, e); + xdg_open_failed = true; + } + } + } + + if xdg_open_failed { + match ashpd::desktop::open_uri::OpenFileRequest::default() + .activation_token(activation_token.map(ashpd::ActivationToken::from)) + .send_uri(&uri) + .await + .and_then(|e| e.response()) + { + Ok(()) => {} + Err(ashpd::Error::Response(ashpd::desktop::ResponseError::Cancelled)) => {} + Err(e) => { + log::error!("Failed to open with dbus: {}", e); + } + } + } + }) + .detach(); + } +} + +#[cfg(any(feature = "x11", feature = "wayland"))] +pub(super) fn reveal_path_internal( + executor: BackgroundExecutor, + path: PathBuf, + activation_token: Option, +) { + executor + .spawn(async move { + if let Some(dir) = File::open(path.clone()).log_err() { + match ashpd::desktop::open_uri::OpenDirectoryRequest::default() + .activation_token(activation_token.map(ashpd::ActivationToken::from)) + .send(&dir.as_fd()) + .await + { + Ok(_) => return, + Err(e) => log::error!("Failed to open with dbus: {}", e), + } + if path.is_dir() { + open::that_detached(path).log_err(); + } else { + open::that_detached(path.parent().unwrap_or(Path::new(""))).log_err(); + } + } + }) + .detach(); +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) fn is_within_click_distance(a: Point, b: Point) -> bool { + let diff = a - b; + diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE +} + +/// Creates an XKB context for keymaps supplied by Wayland or X11. +/// +/// Server keymaps are already resolved and need no local keyboard definitions. +/// Loading default include paths can fail on systems without those files. +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) fn new_xkb_context() -> anyhow::Result { + validate_xkb_context(xkb::Context::new(xkb::CONTEXT_NO_DEFAULT_INCLUDES)) +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +fn validate_xkb_context(context: xkb::Context) -> anyhow::Result { + ensure!( + !context.get_raw_ptr().is_null(), + "libxkbcommon failed to create an XKB context" + ); + Ok(context) +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option { + let mut locales = Vec::default(); + if let Some(locale) = env::var_os("LC_CTYPE") { + locales.push(locale); + } + locales.push(OsString::from("C")); + let mut state: Option = None; + for locale in locales { + if let Ok(table) = + xkb::compose::Table::new_from_locale(cx, &locale, xkb::compose::COMPILE_NO_FLAGS) + { + state = Some(xkb::compose::State::new( + &table, + xkb::compose::STATE_NO_FLAGS, + )); + break; + } + } + state +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) const PIPE_READ_TIMEOUT: Duration = Duration::from_secs(4); + +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) fn read_fd_with_timeout( + mut fd: filedescriptor::FileDescriptor, + timeout: Duration, +) -> Result> { + fd.set_non_blocking(true)?; + let mut buffer = Vec::new(); + let mut chunk = [0u8; 8192]; + loop { + let mut poll_fds = [filedescriptor::pollfd { + fd: fd.as_raw_fd(), + events: filedescriptor::POLLIN, + revents: 0, + }]; + let ready = match filedescriptor::poll(&mut poll_fds, Some(timeout)) { + Ok(ready) => ready, + Err(filedescriptor::Error::Poll(err)) + if err.kind() == std::io::ErrorKind::Interrupted => + { + continue; + } + Err(err) => return Err(err.into()), + }; + if ready == 0 { + anyhow::bail!("timed out waiting for data on pipe after {timeout:?}"); + } + match fd.read(&mut chunk) { + Ok(0) => return Ok(buffer), + Ok(len) => buffer.extend_from_slice(&chunk[..len]), + Err(err) + if err.kind() == std::io::ErrorKind::WouldBlock + || err.kind() == std::io::ErrorKind::Interrupted => {} + Err(err) => return Err(err.into()), + } + } +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) const DEFAULT_CURSOR_ICON_NAME: &str = "left_ptr"; + +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) fn cursor_style_to_icon_names(style: CursorStyle) -> &'static [&'static str] { + // Based on cursor names from chromium: + // https://github.com/chromium/chromium/blob/d3069cf9c973dc3627fa75f64085c6a86c8f41bf/ui/base/cursor/cursor_factory.cc#L113 + match style { + CursorStyle::Arrow => &[DEFAULT_CURSOR_ICON_NAME], + CursorStyle::IBeam => &["text", "xterm"], + CursorStyle::Crosshair => &["crosshair", "cross"], + CursorStyle::ClosedHand => &["closedhand", "grabbing", "hand2"], + CursorStyle::OpenHand => &["openhand", "grab", "hand1"], + CursorStyle::PointingHand => &["pointer", "hand", "hand2"], + CursorStyle::ResizeLeft => &["w-resize", "left_side"], + CursorStyle::ResizeRight => &["e-resize", "right_side"], + CursorStyle::ResizeLeftRight => &["ew-resize", "sb_h_double_arrow"], + CursorStyle::ResizeUp => &["n-resize", "top_side"], + CursorStyle::ResizeDown => &["s-resize", "bottom_side"], + CursorStyle::ResizeUpDown => &["sb_v_double_arrow", "ns-resize"], + CursorStyle::ResizeUpLeftDownRight => &["size_fdiag", "bd_double_arrow", "nwse-resize"], + CursorStyle::ResizeUpRightDownLeft => &["size_bdiag", "nesw-resize", "fd_double_arrow"], + CursorStyle::ResizeColumn => &["col-resize", "sb_h_double_arrow"], + CursorStyle::ResizeRow => &["row-resize", "sb_v_double_arrow"], + CursorStyle::IBeamCursorForVerticalLayout => &["vertical-text"], + CursorStyle::OperationNotAllowed => &["not-allowed", "crossed_circle"], + CursorStyle::DragLink => &["alias"], + CursorStyle::DragCopy => &["copy"], + CursorStyle::ContextualMenu => &["context-menu"], + } +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) fn log_cursor_icon_warning(message: impl std::fmt::Display) { + if let Ok(xcursor_path) = env::var("XCURSOR_PATH") { + log::warn!( + "{:#}\ncursor icon loading may be failing if XCURSOR_PATH environment variable is invalid. \ + XCURSOR_PATH overrides the default icon search. Its current value is '{}'", + message, + xcursor_path + ); + } else { + log::warn!("{:#}", message); + } +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +fn guess_ascii(keycode: Keycode, shift: bool) -> Option { + let c = match (keycode.raw(), shift) { + (24, _) => 'q', + (25, _) => 'w', + (26, _) => 'e', + (27, _) => 'r', + (28, _) => 't', + (29, _) => 'y', + (30, _) => 'u', + (31, _) => 'i', + (32, _) => 'o', + (33, _) => 'p', + (34, false) => '[', + (34, true) => '{', + (35, false) => ']', + (35, true) => '}', + (38, _) => 'a', + (39, _) => 's', + (40, _) => 'd', + (41, _) => 'f', + (42, _) => 'g', + (43, _) => 'h', + (44, _) => 'j', + (45, _) => 'k', + (46, _) => 'l', + (47, false) => ';', + (47, true) => ':', + (48, false) => '\'', + (48, true) => '"', + (49, false) => '`', + (49, true) => '~', + (51, false) => '\\', + (51, true) => '|', + (52, _) => 'z', + (53, _) => 'x', + (54, _) => 'c', + (55, _) => 'v', + (56, _) => 'b', + (57, _) => 'n', + (58, _) => 'm', + (59, false) => ',', + (59, true) => '>', + (60, false) => '.', + (60, true) => '<', + (61, false) => '/', + (61, true) => '?', + + _ => return None, + }; + + Some(c) +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) fn keystroke_from_xkb( + state: &State, + mut modifiers: gpui::Modifiers, + keycode: Keycode, +) -> gpui::Keystroke { + let key_utf32 = state.key_get_utf32(keycode); + let key_utf8 = state.key_get_utf8(keycode); + let key_sym = state.key_get_one_sym(keycode); + + let key = match key_sym { + Keysym::Return => "enter".to_owned(), + Keysym::Prior => "pageup".to_owned(), + Keysym::Next => "pagedown".to_owned(), + Keysym::ISO_Left_Tab => "tab".to_owned(), + Keysym::KP_Prior => "pageup".to_owned(), + Keysym::KP_Next => "pagedown".to_owned(), + Keysym::XF86_Back => "back".to_owned(), + Keysym::XF86_Forward => "forward".to_owned(), + Keysym::XF86_Cut => "cut".to_owned(), + Keysym::XF86_Copy => "copy".to_owned(), + Keysym::XF86_Paste => "paste".to_owned(), + Keysym::XF86_New => "new".to_owned(), + Keysym::XF86_Open => "open".to_owned(), + Keysym::XF86_Save => "save".to_owned(), + + Keysym::comma => ",".to_owned(), + Keysym::period => ".".to_owned(), + Keysym::less => "<".to_owned(), + Keysym::greater => ">".to_owned(), + Keysym::slash => "/".to_owned(), + Keysym::question => "?".to_owned(), + + Keysym::semicolon => ";".to_owned(), + Keysym::colon => ":".to_owned(), + Keysym::apostrophe => "'".to_owned(), + Keysym::quotedbl => "\"".to_owned(), + + Keysym::bracketleft => "[".to_owned(), + Keysym::braceleft => "{".to_owned(), + Keysym::bracketright => "]".to_owned(), + Keysym::braceright => "}".to_owned(), + Keysym::backslash => "\\".to_owned(), + Keysym::bar => "|".to_owned(), + + Keysym::grave => "`".to_owned(), + Keysym::asciitilde => "~".to_owned(), + Keysym::exclam => "!".to_owned(), + Keysym::at => "@".to_owned(), + Keysym::numbersign => "#".to_owned(), + Keysym::dollar => "$".to_owned(), + Keysym::percent => "%".to_owned(), + Keysym::asciicircum => "^".to_owned(), + Keysym::ampersand => "&".to_owned(), + Keysym::asterisk => "*".to_owned(), + Keysym::parenleft => "(".to_owned(), + Keysym::parenright => ")".to_owned(), + Keysym::minus => "-".to_owned(), + Keysym::underscore => "_".to_owned(), + Keysym::equal => "=".to_owned(), + Keysym::plus => "+".to_owned(), + Keysym::space => "space".to_owned(), + Keysym::BackSpace => "backspace".to_owned(), + Keysym::Tab => "tab".to_owned(), + Keysym::Delete => "delete".to_owned(), + Keysym::Escape => "escape".to_owned(), + + Keysym::Left => "left".to_owned(), + Keysym::Right => "right".to_owned(), + Keysym::Up => "up".to_owned(), + Keysym::Down => "down".to_owned(), + Keysym::Home => "home".to_owned(), + Keysym::End => "end".to_owned(), + Keysym::Insert => "insert".to_owned(), + + _ => { + let name = xkb::keysym_get_name(key_sym).to_lowercase(); + if key_sym.is_keypad_key() { + name.replace("kp_", "") + } else if let Some(key) = key_utf8.chars().next() + && key_utf8.len() == 1 + && key.is_ascii() + { + if key.is_ascii_graphic() { + key_utf8.to_lowercase() + // map ctrl-a to `a` + // ctrl-0..9 may emit control codes like ctrl-[, but + // we don't want to map them to `[` + } else if key_utf32 <= 0x1f + && !name.chars().next().is_some_and(|c| c.is_ascii_digit()) + { + ((key_utf32 as u8 + 0x40) as char) + .to_ascii_lowercase() + .to_string() + } else { + name + } + } else if let Some(key_en) = guess_ascii(keycode, modifiers.shift) { + String::from(key_en) + } else { + name + } + } + }; + + if modifiers.shift { + // we only include the shift for upper-case letters by convention, + // so don't include for numbers and symbols, but do include for + // tab/enter, etc. + if key.chars().count() == 1 && key.to_lowercase() == key.to_uppercase() { + modifiers.shift = false; + } + } + + // Ignore control characters (and DEL) for the purposes of key_char + let key_char = + (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8); + + gpui::Keystroke { + modifiers, + key, + key_char, + } +} + +/** + * Returns which symbol the dead key represents + * + */ +#[cfg(any(feature = "wayland", feature = "x11"))] +pub fn keystroke_underlying_dead_key(keysym: Keysym) -> Option { + match keysym { + Keysym::dead_grave => Some("`".to_owned()), + Keysym::dead_acute => Some("´".to_owned()), + Keysym::dead_circumflex => Some("^".to_owned()), + Keysym::dead_tilde => Some("~".to_owned()), + Keysym::dead_macron => Some("¯".to_owned()), + Keysym::dead_breve => Some("˘".to_owned()), + Keysym::dead_abovedot => Some("˙".to_owned()), + Keysym::dead_diaeresis => Some("¨".to_owned()), + Keysym::dead_abovering => Some("˚".to_owned()), + Keysym::dead_doubleacute => Some("˝".to_owned()), + Keysym::dead_caron => Some("ˇ".to_owned()), + Keysym::dead_cedilla => Some("¸".to_owned()), + Keysym::dead_ogonek => Some("˛".to_owned()), + Keysym::dead_iota => Some("ͅ".to_owned()), + Keysym::dead_voiced_sound => Some("゙".to_owned()), + Keysym::dead_semivoiced_sound => Some("゚".to_owned()), + Keysym::dead_belowdot => Some("̣̣".to_owned()), + Keysym::dead_hook => Some("̡".to_owned()), + Keysym::dead_horn => Some("̛".to_owned()), + Keysym::dead_stroke => Some("̶̶".to_owned()), + Keysym::dead_abovecomma => Some("̓̓".to_owned()), + Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()), + Keysym::dead_doublegrave => Some("̏".to_owned()), + Keysym::dead_belowring => Some("˳".to_owned()), + Keysym::dead_belowmacron => Some("̱".to_owned()), + Keysym::dead_belowcircumflex => Some("ꞈ".to_owned()), + Keysym::dead_belowtilde => Some("̰".to_owned()), + Keysym::dead_belowbreve => Some("̮".to_owned()), + Keysym::dead_belowdiaeresis => Some("̤".to_owned()), + Keysym::dead_invertedbreve => Some("̯".to_owned()), + Keysym::dead_belowcomma => Some("̦".to_owned()), + Keysym::dead_currency => None, + Keysym::dead_lowline => None, + Keysym::dead_aboveverticalline => None, + Keysym::dead_belowverticalline => None, + Keysym::dead_longsolidusoverlay => None, + Keysym::dead_a => None, + Keysym::dead_A => None, + Keysym::dead_e => None, + Keysym::dead_E => None, + Keysym::dead_i => None, + Keysym::dead_I => None, + Keysym::dead_o => None, + Keysym::dead_O => None, + Keysym::dead_u => None, + Keysym::dead_U => None, + Keysym::dead_small_schwa => Some("ə".to_owned()), + Keysym::dead_capital_schwa => Some("Ə".to_owned()), + Keysym::dead_greek => None, + _ => None, + } +} +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) fn modifiers_from_xkb(keymap_state: &State) -> gpui::Modifiers { + let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE); + let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE); + let control = keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE); + let platform = keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE); + gpui::Modifiers { + shift, + alt, + control, + platform, + function: false, + } +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) fn capslock_from_xkb(keymap_state: &State) -> gpui::Capslock { + let on = keymap_state.mod_name_is_active(xkb::MOD_NAME_CAPS, xkb::STATE_MODS_EFFECTIVE); + gpui::Capslock { on } +} + +/// Resolve a Linux `dev_t` to PCI vendor/device IDs via sysfs, returning a +/// [`CompositorGpuHint`] that the GPU adapter selection code can use to +/// prioritize the compositor's rendering device. +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) fn compositor_gpu_hint_from_dev_t(dev: u64) -> Option { + fn dev_major(dev: u64) -> u32 { + ((dev >> 8) & 0xfff) as u32 | (((dev >> 32) & !0xfff) as u32) + } + + fn dev_minor(dev: u64) -> u32 { + (dev & 0xff) as u32 | (((dev >> 12) & !0xff) as u32) + } + + fn read_sysfs_hex_id(path: &str) -> Option { + let content = std::fs::read_to_string(path).ok()?; + let trimmed = content.trim().strip_prefix("0x").unwrap_or(content.trim()); + u32::from_str_radix(trimmed, 16).ok() + } + + let major = dev_major(dev); + let minor = dev_minor(dev); + + let vendor_path = format!("/sys/dev/char/{major}:{minor}/device/vendor"); + let device_path = format!("/sys/dev/char/{major}:{minor}/device/device"); + + let vendor_id = read_sysfs_hex_id(&vendor_path)?; + let device_id = read_sysfs_hex_id(&device_path)?; + + log::info!( + "Compositor GPU hint: vendor={:#06x}, device={:#06x} (from dev {major}:{minor})", + vendor_id, + device_id, + ); + + Some(gpui_wgpu::CompositorGpuHint { + vendor_id, + device_id, + }) +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +async fn inhibit_idle_sleep(reason: String, executor: BackgroundExecutor) -> Result { + let proxy = InhibitProxy::new() + .await + .context("Idle sleep prevention portal is unavailable")?; + let request = proxy + .inhibit( + None, + BitFlags::from(InhibitFlags::Suspend), + InhibitOptions::default().set_reason(reason.as_str()), + ) + .await + .context("Failed to request idle sleep prevention")?; + request + .response() + .context("Idle sleep prevention request was rejected")?; + Ok(release_on_drop(request, executor)) +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +fn release_on_drop(request: Request<()>, executor: BackgroundExecutor) -> ActivityGuard { + ActivityGuard::new(move || { + executor + .spawn(async move { + request + .close() + .await + .context("Failed to release idle sleep prevention") + .log_err(); + }) + .detach(); + }) +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +async fn await_idle_sleep_prevention( + guard_rx: oneshot::Receiver>, + executor: &BackgroundExecutor, +) -> Result { + match futures::future::select(guard_rx, executor.timer(Duration::from_secs(10))).await { + futures::future::Either::Left((Ok(result), _)) => result, + futures::future::Either::Left((Err(_), _)) => { + Err(anyhow!("Idle sleep prevention request was abandoned")) + } + futures::future::Either::Right(_) => Err(anyhow!( + "Idle sleep prevention acquisition timed out after 10 seconds" + )), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::{Point, px}; + + #[cfg(any(feature = "wayland", feature = "x11"))] + #[test] + fn rejects_null_xkb_context() { + let context = unsafe { + // libxkbcommon permits unref on null, matching the value returned by Context::new on failure. + xkb::Context::from_raw_ptr(std::ptr::null_mut()) + }; + let error = validate_xkb_context(context) + .err() + .expect("null XKB context should be rejected"); + + assert_eq!( + error.to_string(), + "libxkbcommon failed to create an XKB context" + ); + } + + #[test] + fn test_is_within_click_distance() { + let zero = Point::new(px(0.0), px(0.0)); + assert!(is_within_click_distance(zero, Point::new(px(5.0), px(5.0)))); + assert!(is_within_click_distance( + zero, + Point::new(px(-4.9), px(5.0)) + )); + assert!(is_within_click_distance( + Point::new(px(3.0), px(2.0)), + Point::new(px(-2.0), px(-2.0)) + )); + assert!(!is_within_click_distance( + zero, + Point::new(px(5.0), px(5.1)) + ),); + } + + #[cfg(any(feature = "wayland", feature = "x11"))] + mod idle_sleep_prevention { + use super::super::await_idle_sleep_prevention; + use anyhow::{Result, anyhow}; + use futures::channel::oneshot; + use gpui::{ActivityGuard, TestAppContext}; + use std::{ + sync::{ + Arc, + atomic::{AtomicUsize, Ordering::SeqCst}, + }, + time::Duration, + }; + + #[gpui::test] + async fn guard_passes_through_and_releases_on_drop(cx: &mut TestAppContext) { + let released = Arc::new(AtomicUsize::new(0)); + let (guard_tx, guard_rx) = oneshot::channel(); + assert!(guard_tx.send(Ok(release_guard(released.clone()))).is_ok()); + let guard = await_idle_sleep_prevention(guard_rx, &cx.background_executor) + .await + .expect("acquisition should succeed"); + + assert_eq!(released.load(SeqCst), 0); + drop(guard); + assert_eq!(released.load(SeqCst), 1); + } + + #[gpui::test] + async fn acquisition_error_passes_through(cx: &mut TestAppContext) { + let (guard_tx, guard_rx) = oneshot::channel::>(); + assert!(guard_tx.send(Err(anyhow!("inhibition rejected"))).is_ok()); + assert_eq!( + await_idle_sleep_prevention(guard_rx, &cx.background_executor) + .await + .err() + .expect("acquisition should fail") + .to_string(), + "inhibition rejected" + ); + } + + #[gpui::test] + async fn abandoned_acquisition_is_an_error(cx: &mut TestAppContext) { + let (guard_tx, guard_rx) = oneshot::channel::>(); + drop(guard_tx); + assert_eq!( + await_idle_sleep_prevention(guard_rx, &cx.background_executor) + .await + .err() + .expect("acquisition should fail") + .to_string(), + "Idle sleep prevention request was abandoned" + ); + } + + #[gpui::test] + async fn late_guard_after_timeout_is_released(cx: &mut TestAppContext) { + let released = Arc::new(AtomicUsize::new(0)); + let (guard_tx, guard_rx) = oneshot::channel(); + let task = cx.background_executor.spawn({ + let executor = cx.background_executor.clone(); + async move { await_idle_sleep_prevention(guard_rx, &executor).await } + }); + cx.run_until_parked(); + cx.executor().advance_clock(Duration::from_secs(9)); + cx.run_until_parked(); + assert!(!guard_tx.is_canceled()); + cx.executor().advance_clock(Duration::from_secs(1)); + assert_eq!( + task.await + .err() + .expect("acquisition should time out") + .to_string(), + "Idle sleep prevention acquisition timed out after 10 seconds" + ); + + assert!(guard_tx.is_canceled()); + if let Err(Ok(guard)) = guard_tx.send(Ok(release_guard(released.clone()))) { + assert_eq!(released.load(SeqCst), 0); + drop(guard); + } + assert_eq!(released.load(SeqCst), 1); + } + + fn release_guard(released: Arc) -> ActivityGuard { + ActivityGuard::new(move || { + released.fetch_add(1, SeqCst); + }) + } + } + + #[cfg(any(feature = "wayland", feature = "x11"))] + mod read_fd_with_timeout { + use super::super::{PIPE_READ_TIMEOUT, read_fd_with_timeout}; + use std::io::Write as _; + use std::time::{Duration, Instant}; + + #[test] + fn reads_data_written_before_close() { + let mut pipe = filedescriptor::Pipe::new().unwrap(); + pipe.write.write_all(b"hello clipboard").unwrap(); + drop(pipe.write); + + let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap(); + assert_eq!(bytes, b"hello clipboard"); + } + + #[test] + fn returns_empty_when_writer_closes_without_writing() { + let pipe = filedescriptor::Pipe::new().unwrap(); + drop(pipe.write); + + let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap(); + assert!(bytes.is_empty()); + } + + #[test] + fn times_out_when_writer_never_writes() { + let pipe = filedescriptor::Pipe::new().unwrap(); + let _open_writer = pipe.write; + + let timeout = Duration::from_millis(50); + let started = Instant::now(); + let result = read_fd_with_timeout(pipe.read, timeout); + let elapsed = started.elapsed(); + + let err = result.unwrap_err(); + assert!( + err.to_string().contains("timed out"), + "unexpected error: {err}" + ); + assert!(elapsed >= timeout, "returned before the timeout elapsed"); + } + + #[test] + fn times_out_when_writer_stalls_after_partial_write() { + let mut pipe = filedescriptor::Pipe::new().unwrap(); + pipe.write.write_all(b"partial").unwrap(); + let _open_writer = pipe.write; + + let err = read_fd_with_timeout(pipe.read, Duration::from_millis(50)).unwrap_err(); + assert!( + err.to_string().contains("timed out"), + "unexpected error: {err}" + ); + } + + #[test] + fn slow_writer_resets_deadline_between_chunks() { + let pipe = filedescriptor::Pipe::new().unwrap(); + let chunks = 12; + let gap = Duration::from_millis(40); + let timeout = Duration::from_millis(400); + + let writer = std::thread::spawn({ + let mut write = pipe.write; + move || { + for _ in 0..chunks { + std::thread::sleep(gap); + write.write_all(&[b'x'; 1000]).unwrap(); + } + } + }); + // The total transfer (~480ms) exceeds the timeout; this only + // passes because the timeout is re-armed per chunk. + let bytes = read_fd_with_timeout(pipe.read, timeout).unwrap(); + writer.join().unwrap(); + assert_eq!(bytes, vec![b'x'; 1000 * chunks]); + } + + #[test] + fn reads_payload_larger_than_pipe_capacity() { + let pipe = filedescriptor::Pipe::new().unwrap(); + // Exceeds the 64 KiB pipe capacity, forcing the writer to block. + let payload = vec![b'z'; 1024 * 1024]; + + let writer = std::thread::spawn({ + let mut write = pipe.write; + let payload = payload.clone(); + move || write.write_all(&payload).unwrap() + }); + let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap(); + writer.join().unwrap(); + assert_eq!(bytes, payload); + } + } +} diff --git a/vendor/gpui-pre-linux/src/linux/system_notifications.rs b/vendor/gpui-pre-linux/src/linux/system_notifications.rs new file mode 100644 index 000000000..3530c6f34 --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/system_notifications.rs @@ -0,0 +1,154 @@ +//! System notifications over the XDG notifications D-Bus interface, via +//! `notify-rust`. + +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; + +use futures::StreamExt as _; +use futures::channel::mpsc; +use gpui::{ + ForegroundExecutor, SharedString, SystemNotification, SystemNotificationResponse, Task, +}; + +/// The XDG action key invoked when the user activates the notification body +/// rather than a specific action button. +const DEFAULT_ACTION: &str = "default"; + +type ResponseCallback = Rc>>>; + +pub(crate) struct SystemNotificationState { + response_sender: mpsc::UnboundedSender, + response_receiver: Option>, + callback: ResponseCallback, + _response_task: Option>, +} + +impl SystemNotificationState { + pub(crate) fn new() -> Self { + let (response_sender, response_receiver) = mpsc::unbounded(); + Self { + response_sender, + response_receiver: Some(response_receiver), + callback: Rc::new(RefCell::new(None)), + _response_task: None, + } + } + + pub(crate) fn show(&self, app_name: Option<&str>, notification: SystemNotification) { + let mut builder = notify_rust::Notification::new(); + if let Some(app_name) = app_name { + builder.appname(app_name); + } + builder + .summary(¬ification.title) + .body(¬ification.body) + .action(DEFAULT_ACTION, DEFAULT_ACTION); + let mut action_ids = HashMap::new(); + for (index, action) in notification.actions.iter().enumerate() { + let transport_id = format!("gpui-action-{index}"); + builder.action(&transport_id, &action.label); + action_ids.insert(transport_id, action.id.clone()); + } + let built = builder.finalize(); + + let sender = self.response_sender.clone(); + let tag = notification.tag.clone(); + // `show` connects to the session bus and `wait_for_action` blocks + // until the notification closes, so both run off the UI thread. + let spawn_result = std::thread::Builder::new() + .name("system-notification".to_string()) + .spawn(move || match built.show() { + Ok(handle) => handle.wait_for_action(|action| { + let action_id = match action { + // Sent by `notify-rust` when the notification closes + // without being activated. + "__closed" => return, + transport_id => { + let Some(action_id) = response_action_id(transport_id, &action_ids) + else { + log::warn!( + "system notification returned unknown action {transport_id:?}" + ); + return; + }; + action_id + } + }; + sender + .unbounded_send(SystemNotificationResponse { tag, action_id }) + .ok(); + }), + Err(error) => log::warn!("failed to show system notification: {error}"), + }); + if let Err(error) = spawn_result { + log::warn!("failed to spawn system notification thread: {error}"); + } + } + + pub(crate) fn dismiss(&self, _tag: &str) { + // The XDG notifications protocol only allows closing via the + // server-assigned id on a live handle, which `wait_for_action` + // consumes; stale notifications simply age out of the shade. + } + + pub(crate) fn on_response( + &mut self, + executor: &ForegroundExecutor, + callback: Box, + ) { + *self.callback.borrow_mut() = Some(callback); + + // Responses arrive from per-notification threads; this task hands + // them to the registered callback on the main thread. + if let Some(mut receiver) = self.response_receiver.take() { + let callback = self.callback.clone(); + self._response_task = Some(executor.spawn(async move { + while let Some(response) = receiver.next().await { + // Take the callback out for the call: it may re-enter the + // platform or replace itself. + let taken = callback.borrow_mut().take(); + if let Some(mut taken) = taken { + taken(response); + callback.borrow_mut().get_or_insert(taken); + } + } + })); + } + } +} + +fn response_action_id( + transport_id: &str, + action_ids: &HashMap, +) -> Option> { + if transport_id == DEFAULT_ACTION { + Some(None) + } else { + action_ids.get(transport_id).cloned().map(Some) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn caller_action_ids_do_not_collide_with_transport_action_ids() { + let action_ids = HashMap::from([ + ("gpui-action-0".to_string(), SharedString::from("default")), + ("gpui-action-1".to_string(), SharedString::from("__closed")), + ]); + + assert_eq!(response_action_id(DEFAULT_ACTION, &action_ids), Some(None)); + assert_eq!( + response_action_id("gpui-action-0", &action_ids), + Some(Some("default".into())) + ); + assert_eq!( + response_action_id("gpui-action-1", &action_ids), + Some(Some("__closed".into())) + ); + assert_eq!(response_action_id("unknown", &action_ids), None); + } +} diff --git a/vendor/gpui-pre-linux/src/linux/text_system.rs b/vendor/gpui-pre-linux/src/linux/text_system.rs new file mode 100644 index 000000000..d6571021e --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/text_system.rs @@ -0,0 +1 @@ +pub(crate) use gpui_wgpu::CosmicTextSystem; diff --git a/vendor/gpui-pre-linux/src/linux/wayland.rs b/vendor/gpui-pre-linux/src/linux/wayland.rs new file mode 100644 index 000000000..cbc962bcf --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/wayland.rs @@ -0,0 +1,42 @@ +mod client; +mod clipboard; +mod cursor; +mod display; +mod popup; +mod serial; +mod window; + +/// Contains Types for configuring layer_shell surfaces. +pub mod layer_shell; + +pub(crate) use client::*; + +use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape; + +use gpui::CursorStyle; + +pub(super) fn to_shape(style: CursorStyle) -> Shape { + match style { + CursorStyle::Arrow => Shape::Default, + CursorStyle::IBeam => Shape::Text, + CursorStyle::Crosshair => Shape::Crosshair, + CursorStyle::ClosedHand => Shape::Grabbing, + CursorStyle::OpenHand => Shape::Grab, + CursorStyle::PointingHand => Shape::Pointer, + CursorStyle::ResizeLeft => Shape::WResize, + CursorStyle::ResizeRight => Shape::EResize, + CursorStyle::ResizeLeftRight => Shape::EwResize, + CursorStyle::ResizeUp => Shape::NResize, + CursorStyle::ResizeDown => Shape::SResize, + CursorStyle::ResizeUpDown => Shape::NsResize, + CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize, + CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize, + CursorStyle::ResizeColumn => Shape::ColResize, + CursorStyle::ResizeRow => Shape::RowResize, + CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText, + CursorStyle::OperationNotAllowed => Shape::NotAllowed, + CursorStyle::DragLink => Shape::Alias, + CursorStyle::DragCopy => Shape::Copy, + CursorStyle::ContextualMenu => Shape::ContextMenu, + } +} diff --git a/vendor/gpui-pre-linux/src/linux/wayland/client.rs b/vendor/gpui-pre-linux/src/linux/wayland/client.rs new file mode 100644 index 000000000..37b3a65a2 --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/wayland/client.rs @@ -0,0 +1,3231 @@ +use std::{ + cell::{RefCell, RefMut}, + hash::Hash, + os::fd::{AsRawFd, BorrowedFd}, + path::PathBuf, + rc::{Rc, Weak}, + time::{Duration, Instant}, +}; + +use ashpd::WindowIdentifier; +use calloop::{ + EventLoop, LoopHandle, + ping::Ping, + timer::{TimeoutAction, Timer}, +}; +use calloop_wayland_source::WaylandSource; +use collections::HashMap; +use filedescriptor::Pipe; +use gpui_util::ResultExt as _; +use http_client::Url; +use smallvec::SmallVec; +use wayland_backend::client::ObjectId; +use wayland_backend::protocol::WEnum; +use wayland_client::event_created_child; +use wayland_client::globals::{GlobalList, GlobalListContents, registry_queue_init}; +use wayland_client::protocol::wl_callback::{self, WlCallback}; +use wayland_client::protocol::wl_data_device_manager::DndAction; +use wayland_client::protocol::wl_data_offer::WlDataOffer; +use wayland_client::protocol::wl_pointer::AxisSource; +use wayland_client::protocol::{ + wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, wl_region, +}; +use wayland_client::{ + Connection, Dispatch, Proxy, QueueHandle, delegate_noop, + protocol::{ + wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_registry, wl_seat, wl_shm, + wl_shm_pool, wl_surface, + }, +}; +use wayland_protocols::wp::pointer_gestures::zv1::client::{ + zwp_pointer_gesture_pinch_v1, zwp_pointer_gestures_v1, +}; +use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{ + self, ZwpPrimarySelectionOfferV1, +}; +use wayland_protocols::wp::primary_selection::zv1::client::{ + zwp_primary_selection_device_manager_v1, zwp_primary_selection_device_v1, + zwp_primary_selection_source_v1, +}; +use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3::{ + ContentHint, ContentPurpose, +}; +use wayland_protocols::wp::text_input::zv3::client::{ + zwp_text_input_manager_v3, zwp_text_input_v3, +}; +use wayland_protocols::wp::viewporter::client::{wp_viewport, wp_viewporter}; +use wayland_protocols::xdg::activation::v1::client::{xdg_activation_token_v1, xdg_activation_v1}; +use wayland_protocols::xdg::decoration::zv1::client::{ + zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1, +}; +use wayland_protocols::xdg::shell::client::{ + xdg_popup, xdg_positioner, xdg_surface, xdg_toplevel, xdg_wm_base, +}; +use wayland_protocols::xdg::system_bell::v1::client::xdg_system_bell_v1; +use wayland_protocols::{ + wp::cursor_shape::v1::client::{wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1}, + xdg::dialog::v1::client::xdg_wm_dialog_v1::{self, XdgWmDialogV1}, +}; +use wayland_protocols::{ + wp::fractional_scale::v1::client::{wp_fractional_scale_manager_v1, wp_fractional_scale_v1}, + xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1, +}; +use wayland_protocols::ext::background_effect::v1::client::{ + ext_background_effect_manager_v1, ext_background_effect_surface_v1, +}; +use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager}; +use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1}; +use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1; +use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, Keycode}; + +use super::{ + display::WaylandDisplay, + window::{ImeInput, WaylandWindowStatePtr}, +}; + +use crate::linux::{ + DOUBLE_CLICK_INTERVAL, LinuxClient, LinuxCommon, LinuxKeyboardLayout, PIPE_READ_TIMEOUT, + SCROLL_LINES, capslock_from_xkb, cursor_style_to_icon_names, get_xkb_compose_state, + is_within_click_distance, keystroke_from_xkb, keystroke_underlying_dead_key, + modifiers_from_xkb, new_xkb_context, open_uri_internal, read_fd_with_timeout, + reveal_path_internal, + wayland::{ + clipboard::{Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPES}, + cursor::Cursor, + serial::{Serial, SerialKind, SerialTracker}, + to_shape, + window::WaylandWindow, + }, + xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}, +}; +use gpui::{ + AnyWindowHandle, Bounds, Capslock, CursorStyle, DevicePixels, DisplayId, ExternalDragPayload, + FileDragPaths, FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, + Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, + MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, PlatformInput, + PlatformKeyboardLayout, PlatformWindow, Point, ScrollDelta, ScrollWheelEvent, SharedString, + Size, TouchPhase, WindowButtonLayout, WindowKind, WindowParams, point, profiler, px, size, +}; +use gpui_wgpu::{CompositorGpuHint, GpuContext}; +use wayland_protocols::wp::linux_dmabuf::zv1::client::{ + zwp_linux_dmabuf_feedback_v1, zwp_linux_dmabuf_v1, +}; + +/// Used to convert evdev scancode to xkb scancode +const MIN_KEYCODE: u32 = 8; + +const UNKNOWN_KEYBOARD_LAYOUT_NAME: SharedString = SharedString::new_static("unknown"); +const XDG_ACTIVATION_TOKEN_ENV_VAR: &str = "XDG_ACTIVATION_TOKEN"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct ImeCursorRectangle { + x: i32, + y: i32, + width: i32, + height: i32, +} + +impl From> for ImeCursorRectangle { + fn from(bounds: Bounds) -> Self { + Self { + x: bounds.origin.x.as_f32() as i32, + y: bounds.origin.y.as_f32() as i32, + width: bounds.size.width.as_f32() as i32, + height: bounds.size.height.as_f32() as i32, + } + } +} + +trait ImeCursorRectangleSink { + fn set_ime_cursor_rectangle(&self, x: i32, y: i32, width: i32, height: i32); + fn commit_ime_state(&self); +} + +impl ImeCursorRectangleSink for zwp_text_input_v3::ZwpTextInputV3 { + fn set_ime_cursor_rectangle(&self, x: i32, y: i32, width: i32, height: i32) { + self.set_cursor_rectangle(x, y, width, height); + } + + fn commit_ime_state(&self) { + self.commit(); + } +} + +fn set_ime_cursor_rectangle( + text_input: &impl ImeCursorRectangleSink, + cursor_rectangle: ImeCursorRectangle, +) { + text_input.set_ime_cursor_rectangle( + cursor_rectangle.x, + cursor_rectangle.y, + cursor_rectangle.width, + cursor_rectangle.height, + ); +} + +fn update_ime_cursor_rectangle( + text_input: &impl ImeCursorRectangleSink, + last_ime_cursor_rectangle: &mut Option, + bounds: Bounds, +) { + let cursor_rectangle = ImeCursorRectangle::from(bounds); + if *last_ime_cursor_rectangle == Some(cursor_rectangle) { + return; + } + + *last_ime_cursor_rectangle = Some(cursor_rectangle); + set_ime_cursor_rectangle(text_input, cursor_rectangle); + text_input.commit_ime_state(); +} + +fn set_ime_cursor_rectangle_after_done( + text_input: &impl ImeCursorRectangleSink, + last_ime_cursor_rectangle: &mut Option, + bounds: Bounds, + should_commit: bool, +) { + if should_commit { + update_ime_cursor_rectangle(text_input, last_ime_cursor_rectangle, bounds); + } else { + set_ime_cursor_rectangle(text_input, ImeCursorRectangle::from(bounds)); + } +} + +/// Pacing for retry ticks: a fixed 60Hz interval. Retries only occur for throttled or +/// failed-present frames, so matching the output's actual refresh rate wouldn't be observable. +const FRAME_RETRY_INTERVAL: Duration = Duration::from_micros(16_667); + +fn take_startup_activation_token_from_environment() -> Option { + let startup_activation_token = std::env::var(XDG_ACTIVATION_TOKEN_ENV_VAR) + .ok() + .filter(|token| !token.is_empty()); + // The token must be removed from the environment so it isn't inherited by child + // processes we spawn, per the xdg-activation spec: https://wayland.app/protocols/xdg-activation-v1 + // SAFETY: This runs during Wayland platform initialization before GPUI starts + // concurrent environment access or spawning child processes. + unsafe { std::env::remove_var(XDG_ACTIVATION_TOKEN_ENV_VAR) }; + startup_activation_token +} + +#[derive(Clone)] +pub struct Globals { + pub qh: QueueHandle, + pub activation: Option, + pub compositor: wl_compositor::WlCompositor, + pub cursor_shape_manager: Option, + pub data_device_manager: Option, + pub primary_selection_manager: + Option, + pub wm_base: xdg_wm_base::XdgWmBase, + pub shm: wl_shm::WlShm, + pub seat: wl_seat::WlSeat, + pub viewporter: Option, + pub fractional_scale_manager: + Option, + pub decoration_manager: Option, + pub layer_shell: Option, + pub blur_manager: Option, + pub background_effect_manager: + Option, + pub text_input_manager: Option, + pub gesture_manager: Option, + pub dialog: Option, + pub system_bell: Option, + pub executor: ForegroundExecutor, + pub frame_ping: Ping, +} + +impl Globals { + fn new( + globals: GlobalList, + executor: ForegroundExecutor, + qh: QueueHandle, + seat: wl_seat::WlSeat, + frame_ping: Ping, + ) -> Self { + let dialog_v = XdgWmDialogV1::interface().version; + Globals { + activation: globals.bind(&qh, 1..=1, ()).ok(), + compositor: globals + .bind( + &qh, + wl_surface::REQ_SET_BUFFER_SCALE_SINCE + ..=wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE, + (), + ) + .unwrap(), + cursor_shape_manager: globals.bind(&qh, 1..=1, ()).ok(), + data_device_manager: globals + .bind( + &qh, + WL_DATA_DEVICE_MANAGER_VERSION..=WL_DATA_DEVICE_MANAGER_VERSION, + (), + ) + .ok(), + primary_selection_manager: globals.bind(&qh, 1..=1, ()).ok(), + shm: globals.bind(&qh, 1..=1, ()).unwrap(), + seat, + // Accept any xdg_wm_base version up to 6, which added the `suspended` + // toplevel state; older compositors bind at their own version. + wm_base: globals.bind(&qh, 1..=6, ()).unwrap(), + viewporter: globals.bind(&qh, 1..=1, ()).ok(), + fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(), + decoration_manager: globals.bind(&qh, 1..=1, ()).ok(), + layer_shell: globals.bind(&qh, 1..=5, ()).ok(), + blur_manager: globals.bind(&qh, 1..=1, ()).ok(), + background_effect_manager: globals.bind(&qh, 1..=1, ()).ok(), + text_input_manager: globals.bind(&qh, 1..=1, ()).ok(), + gesture_manager: globals.bind(&qh, 1..=3, ()).ok(), + dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(), + system_bell: globals.bind(&qh, 1..=1, ()).ok(), + executor, + qh, + frame_ping, + } + } +} + +#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] +pub struct InProgressOutput { + name: Option, + scale: Option, + position: Option>, + size: Option>, + subpixel: Option, +} + +impl InProgressOutput { + fn complete(&self) -> Option { + if let Some((position, size)) = self.position.zip(self.size) { + let scale = self.scale.unwrap_or(1); + Some(Output { + name: self.name.clone(), + scale, + bounds: Bounds::new(position, size), + subpixel: self.subpixel, + }) + } else { + None + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Output { + pub name: Option, + pub scale: i32, + pub bounds: Bounds, + pub subpixel: Option, +} + +pub(crate) struct WaylandClientState { + serial_tracker: SerialTracker, + globals: Globals, + pub gpu_context: GpuContext, + pub compositor_gpu: Option, + wl_seat: wl_seat::WlSeat, // TODO: Multi seat support + wl_pointer: Option, + pinch_gesture: Option, + pinch_scale: f32, + wl_keyboard: Option, + cursor_shape_device: Option, + data_device: Option, + primary_selection: Option, + text_input: Option, + pre_edit_text: Option, + ime_pre_edit: Option, + composing: bool, + last_ime_cursor_rectangle: Option, + // Surface to Window mapping + windows: HashMap, + // Output to scale mapping + outputs: HashMap, + in_progress_outputs: HashMap, + wl_outputs: HashMap, + keyboard_layout: LinuxKeyboardLayout, + keymap_state: Option, + compose_state: Option, + drag: DragState, + external_drag: Option, + click: ClickState, + repeat: KeyRepeat, + pub modifiers: Modifiers, + pub capslock: Capslock, + axis_source: AxisSource, + pub mouse_location: Option>, + continuous_scroll_delta: Option>, + discrete_scroll_delta: Option>, + vertical_modifier: f32, + horizontal_modifier: f32, + scroll_event_received: bool, + enter_token: Option<()>, + button_pressed: Option, + mouse_focused_window: Option, + keyboard_focused_window: Option, + loop_handle: LoopHandle<'static, WaylandClientStatePtr>, + cursor_style: Option, + cursor_hidden_window: Option, + clipboard: Clipboard, + data_offers: Vec>, + primary_data_offer: Option>, + cursor: Cursor, + pending_activation: Option, + startup_activation_token: Option, + event_loop: Option>, + pub common: LinuxCommon, + ime_enabled: Option, +} + +struct DragState { + data_offer: Option, + window: Option, + position: Point, + uri_read_generation: u64, +} + +impl DragState { + fn begin_uri_read(&mut self) -> u64 { + self.invalidate_uri_read(); + self.uri_read_generation + } + + fn invalidate_uri_read(&mut self) { + self.uri_read_generation = self.uri_read_generation.wrapping_add(1); + } + + fn is_uri_read_current(&self, generation: u64) -> bool { + self.uri_read_generation == generation + } +} + +trait FileDragDataOffer { + fn finish(&self); + fn destroy(&self); +} + +impl FileDragDataOffer for wl_data_offer::WlDataOffer { + fn finish(&self) { + wl_data_offer::WlDataOffer::finish(self); + } + + fn destroy(&self) { + wl_data_offer::WlDataOffer::destroy(self); + } +} + +impl DragState +where + DataOffer: FileDragDataOffer + Clone, + Window: Clone, +{ + fn handle_leave(&mut self) -> Option<(Window, PlatformInput)> { + self.invalidate_uri_read(); + let window = self.window.clone()?; + let data_offer = self.data_offer.clone()?; + data_offer.destroy(); + self.data_offer = None; + self.window = None; + Some((window, PlatformInput::FileDrop(FileDropEvent::Exited {}))) + } + + fn handle_drop(&mut self) -> Option<(Window, PlatformInput)> { + self.invalidate_uri_read(); + let window = self.window.clone()?; + let data_offer = self.data_offer.clone()?; + data_offer.finish(); + data_offer.destroy(); + self.data_offer = None; + self.window = None; + Some(( + window, + PlatformInput::FileDrop(FileDropEvent::Submit { + position: self.position, + }), + )) + } + + fn complete_uri_read( + &mut self, + generation: u64, + data_offer: DataOffer, + window: Window, + position: Point, + paths: gpui::ExternalPaths, + ) -> Option<(Window, PlatformInput)> { + if !self.is_uri_read_current(generation) { + data_offer.destroy(); + return None; + } + + self.data_offer = Some(data_offer); + self.window = Some(window.clone()); + self.position = position; + Some(( + window, + PlatformInput::FileDrop(FileDropEvent::Entered { position, paths }), + )) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum DataSourceKind { + Clipboard, + Drag, +} + +pub(crate) struct ExternalDrag { + source: wl_data_source::WlDataSource, + bytes: Vec, + window: WaylandWindowStatePtr, +} + +fn file_uri_list(paths: &FileDragPaths) -> String { + paths + .entries() + .iter() + .filter_map(|(path, _)| Url::from_file_path(path).ok()) + .fold(String::new(), |mut list, url| { + list.push_str(url.as_str()); + list.push_str("\r\n"); + list + }) +} + +pub struct ClickState { + last_mouse_button: Option, + last_click: Instant, + last_location: Point, + current_count: usize, +} + +pub(crate) struct KeyRepeat { + characters_per_second: u32, + delay: Duration, + current_id: u64, + current_keycode: Option, +} + +pub(crate) enum PendingActivation { + /// URI to open in the web browser. + Uri(String), + /// Path to open in the file explorer. + Path(PathBuf), + /// A window from ourselves to raise. + Window(ObjectId), +} + +impl WaylandClientState { + fn consume_startup_activation_token(&mut self, surface: &wl_surface::WlSurface) { + let Some(startup_activation_token) = self.startup_activation_token.take() else { + return; + }; + let Some(activation) = self.globals.activation.as_ref() else { + return; + }; + activation.activate(startup_activation_token, surface); + } +} + +/// This struct is required to conform to Rust's orphan rules, so we can dispatch on the state but hand the +/// window to GPUI. +#[derive(Clone)] +pub struct WaylandClientStatePtr(Weak>); + +impl WaylandClientStatePtr { + pub fn get_client(&self) -> Rc> { + self.0 + .upgrade() + .expect("The pointer should always be valid when dispatching in wayland") + } + + pub fn dispatch_scheduled_frames(&self) { + let Some(client) = self.0.upgrade() else { + return; + }; + // Release the client borrow before ticking: the tick re-enters GPUI, which can + // borrow the client again (e.g. IME updates). + let windows = client + .borrow() + .windows + .values() + .cloned() + .collect::>(); + for window in windows { + window.scheduled_frame_fired(); + } + } + + /// Queue a retry tick for `surface_id` one refresh interval from now. An immediate + /// retry would spin against the frame-rate throttle that deferred the draw in the + /// first place. + pub fn schedule_frame_retry(&self, surface_id: &ObjectId) { + let client = self.get_client(); + let state = client.borrow(); + let surface_id = surface_id.clone(); + if let Err(err) = state.loop_handle.insert_source( + Timer::from_duration(FRAME_RETRY_INTERVAL), + move |_, _, this| { + let client = this.get_client(); + let window = get_window(&mut client.borrow_mut(), &surface_id); + if let Some(window) = window { + window.retry_timer_fired(); + } + TimeoutAction::Drop + }, + ) { + log::error!("Failed to schedule frame retry: {err}"); + } + } + + pub fn get_serial(&self, kind: SerialKind) -> Serial { + self.0.upgrade().unwrap().borrow().serial_tracker.get(kind) + } + + pub fn start_external_drag( + &self, + surface: &wl_surface::WlSurface, + payload: &ExternalDragPayload, + ) -> bool { + let client = self.get_client(); + let mut state = client.borrow_mut(); + + let Some(window) = get_window(&mut state, &surface.id()) else { + return false; + }; + + let (Some(data_device_manager), Some(data_device)) = ( + state.globals.data_device_manager.as_ref(), + state.data_device.as_ref(), + ) else { + return false; + }; + + let ExternalDragPayload::Files(paths) = payload; + let uri_list = file_uri_list(paths); + if uri_list.is_empty() { + return false; + } + + let serial = state.serial_tracker.get(SerialKind::MousePress); + let source = + data_device_manager.create_data_source(&state.globals.qh, DataSourceKind::Drag); + source.offer(FILE_LIST_MIME_TYPE.to_string()); + source.set_actions(DndAction::Copy | DndAction::Move); + data_device.start_drag(Some(&source), surface, None, serial.as_raw()); + + state.external_drag = Some(ExternalDrag { + source, + bytes: uri_list.into_bytes(), + window, + }); + true + } + + pub fn set_pending_activation(&self, window: ObjectId) { + self.0.upgrade().unwrap().borrow_mut().pending_activation = + Some(PendingActivation::Window(window)); + } + + pub fn enable_ime(&self) { + let client = self.get_client(); + let mut state = client.borrow_mut(); + state.ime_enabled = Some(true); + state.last_ime_cursor_rectangle = None; + let Some(text_input) = state.text_input.take() else { + return; + }; + + text_input.enable(); + text_input.set_content_type(ContentHint::None, ContentPurpose::Normal); + let mut cursor_rectangle = None; + if let Some(window) = state.keyboard_focused_window.clone() { + drop(state); + if let Some(area) = window.get_ime_area() { + let area = ImeCursorRectangle::from(area); + set_ime_cursor_rectangle(&text_input, area); + cursor_rectangle = Some(area); + } + state = client.borrow_mut(); + } + text_input.commit(); + state.last_ime_cursor_rectangle = cursor_rectangle; + state.text_input = Some(text_input); + } + + pub fn disable_ime(&self) { + let client = self.get_client(); + let mut state = client.borrow_mut(); + state.ime_enabled = Some(false); + state.composing = false; + if let Some(text_input) = &state.text_input { + text_input.disable(); + text_input.commit(); + } + } + + pub fn ime_enabled(&self) -> Option { + let client = self.get_client(); + client.borrow().ime_enabled + } + + pub fn update_ime_position(&self, bounds: Bounds) { + let client = self.get_client(); + let mut state = client.borrow_mut(); + if state.pre_edit_text.is_some() { + return; + } + let Some(text_input) = state.text_input.clone() else { + return; + }; + update_ime_cursor_rectangle(&text_input, &mut state.last_ime_cursor_rectangle, bounds); + } + + pub fn handle_keyboard_layout_change(&self) { + let client = self.get_client(); + let mut state = client.borrow_mut(); + let changed = if let Some(keymap_state) = &state.keymap_state { + let layout_idx = keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE); + let keymap = keymap_state.get_keymap(); + let layout_name = keymap.layout_get_name(layout_idx); + let changed = layout_name != state.keyboard_layout.name(); + if changed { + state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into()); + } + changed + } else { + let changed = &UNKNOWN_KEYBOARD_LAYOUT_NAME != state.keyboard_layout.name(); + if changed { + state.keyboard_layout = LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME); + } + changed + }; + + if changed && let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() + { + drop(state); + callback(); + state = client.borrow_mut(); + state.common.callbacks.keyboard_layout_change = Some(callback); + } + } + + pub fn drop_window(&self, surface_id: &ObjectId) { + let client = self.get_client(); + let mut state = client.borrow_mut(); + let closed_window = state.windows.remove(surface_id).unwrap(); + if let Some(window) = state.mouse_focused_window.take() + && !window.ptr_eq(&closed_window) + { + state.mouse_focused_window = Some(window); + } + if let Some(window) = state.keyboard_focused_window.take() + && !window.ptr_eq(&closed_window) + { + state.keyboard_focused_window = Some(window); + } + if let Some(window) = state.cursor_hidden_window.take() + && !window.ptr_eq(&closed_window) + { + state.cursor_hidden_window = Some(window); + } + } +} + +impl WaylandClientState { + fn hide_cursor_until_mouse_moves(&mut self) { + if self.cursor_hidden_window.is_some() { + return; + } + let Some(focused_window) = self.mouse_focused_window.clone() else { + // No surface to apply the hidden cursor to. + return; + }; + let Some(wl_pointer) = self.wl_pointer.clone() else { + // Seat lost its pointer capability; nothing to hide. + return; + }; + let serial = self.serial_tracker.get(SerialKind::MouseEnter); + wl_pointer.set_cursor(serial.as_raw(), None, 0, 0); + self.cursor_hidden_window = Some(focused_window); + } + + fn restore_cursor_after_hide(&mut self) { + if self.cursor_hidden_window.take().is_none() { + return; + } + let Some(style) = self.cursor_style else { + return; + }; + let serial = self.serial_tracker.get(SerialKind::MouseEnter); + if let Some(cursor_shape_device) = &self.cursor_shape_device { + cursor_shape_device.set_shape(serial.as_raw(), to_shape(style)); + return; + } + let Some(focused_window) = self.mouse_focused_window.clone() else { + log::warn!( + "wayland: no focused surface to restore cursor style {:?} after hide; cursor may stay invisible", + style + ); + return; + }; + let Some(wl_pointer) = self.wl_pointer.clone() else { + log::warn!( + "wayland: no wl_pointer to restore cursor style {:?} after hide; cursor may stay invisible", + style + ); + return; + }; + let scale = focused_window.primary_output_scale(); + self.cursor.set_icon( + &wl_pointer, + serial.as_raw(), + cursor_style_to_icon_names(style), + scale, + ); + } +} + +#[derive(Clone)] +pub struct WaylandClient(Rc>); + +impl Drop for WaylandClient { + fn drop(&mut self) { + let mut state = self.0.borrow_mut(); + state.windows.clear(); + + if let Some(wl_pointer) = &state.wl_pointer { + wl_pointer.release(); + } + if let Some(cursor_shape_device) = &state.cursor_shape_device { + cursor_shape_device.destroy(); + } + if let Some(data_device) = &state.data_device { + data_device.release(); + } + if let Some(text_input) = &state.text_input { + text_input.destroy(); + } + } +} + +const WL_DATA_DEVICE_MANAGER_VERSION: u32 = 3; + +fn wl_seat_version(version: u32) -> u32 { + // We rely on the wl_pointer.frame event + const WL_SEAT_MIN_VERSION: u32 = 5; + const WL_SEAT_MAX_VERSION: u32 = 9; + + if version < WL_SEAT_MIN_VERSION { + panic!( + "wl_seat below required version: {} < {}", + version, WL_SEAT_MIN_VERSION + ); + } + + version.clamp(WL_SEAT_MIN_VERSION, WL_SEAT_MAX_VERSION) +} + +fn wl_output_version(version: u32) -> u32 { + const WL_OUTPUT_MIN_VERSION: u32 = 2; + const WL_OUTPUT_MAX_VERSION: u32 = 4; + + if version < WL_OUTPUT_MIN_VERSION { + panic!( + "wl_output below required version: {} < {}", + version, WL_OUTPUT_MIN_VERSION + ); + } + + version.clamp(WL_OUTPUT_MIN_VERSION, WL_OUTPUT_MAX_VERSION) +} + +impl WaylandClient { + pub(crate) fn new() -> Self { + let startup_activation_token = take_startup_activation_token_from_environment(); + let conn = Connection::connect_to_env().unwrap(); + + let (globals, event_queue) = registry_queue_init::(&conn).unwrap(); + let qh = event_queue.handle(); + + let mut seat: Option = None; + #[allow(clippy::mutable_key_type)] + let mut in_progress_outputs = HashMap::default(); + #[allow(clippy::mutable_key_type)] + let mut wl_outputs: HashMap = HashMap::default(); + globals.contents().with_list(|list| { + for global in list { + match &global.interface[..] { + "wl_seat" => { + seat = Some(globals.registry().bind::( + global.name, + wl_seat_version(global.version), + &qh, + (), + )); + } + "wl_output" => { + let output = globals.registry().bind::( + global.name, + wl_output_version(global.version), + &qh, + (), + ); + in_progress_outputs.insert(output.id(), InProgressOutput::default()); + wl_outputs.insert(output.id(), output); + } + _ => {} + } + } + }); + + let event_loop = EventLoop::::try_new().unwrap(); + + let (common, main_receiver, power_receiver) = LinuxCommon::new(event_loop.get_signal()); + + let handle = event_loop.handle(); + handle + .insert_source(main_receiver, { + let handle = handle.clone(); + move |event, _, _: &mut WaylandClientStatePtr| { + if let calloop::channel::Event::Msg(runnable) = event { + handle.insert_idle(|_| { + let location = runnable.metadata().location; + let spawned = runnable.metadata().spawned; + profiler::update_running_task(spawned, location); + runnable.run(); + profiler::save_task_timing(); + }); + } + } + }) + .unwrap(); + + handle + .insert_source( + power_receiver, + |event, _, client: &mut WaylandClientStatePtr| { + if let calloop::channel::Event::Msg(event) = event { + client + .get_client() + .borrow_mut() + .common + .handle_system_power_event(event); + } + }, + ) + .unwrap(); + + let compositor_gpu = detect_compositor_gpu(); + let gpu_context = Rc::new(RefCell::new(None)); + + let (frame_ping, frame_ping_source) = + calloop::ping::make_ping().expect("Failed to create the frame ping"); + handle + .insert_source(frame_ping_source, |_, _, client| { + client.dispatch_scheduled_frames(); + }) + .unwrap(); + + let seat = seat.unwrap(); + let globals = Globals::new( + globals, + common.foreground_executor.clone(), + qh.clone(), + seat.clone(), + frame_ping, + ); + + let data_device = globals + .data_device_manager + .as_ref() + .map(|data_device_manager| data_device_manager.get_data_device(&seat, &qh, ())); + + let primary_selection = globals + .primary_selection_manager + .as_ref() + .map(|primary_selection_manager| primary_selection_manager.get_device(&seat, &qh, ())); + + let cursor = Cursor::new(&conn, &globals, 24); + + handle + .insert_source(XDPEventSource::new(&common.background_executor), { + move |event, _, client| match event { + XDPEvent::WindowAppearance(appearance) => { + if let Some(client) = client.0.upgrade() { + let mut client = client.borrow_mut(); + + client.common.appearance = appearance; + + for window in client.windows.values_mut() { + window.set_appearance(appearance); + } + } + } + XDPEvent::ButtonLayout(layout_str) => { + if let Some(client) = client.0.upgrade() { + let layout = WindowButtonLayout::parse(&layout_str) + .log_err() + .unwrap_or_else(WindowButtonLayout::linux_default); + let mut client = client.borrow_mut(); + client.common.button_layout = layout; + + for window in client.windows.values_mut() { + window.set_button_layout(); + } + } + } + XDPEvent::CursorTheme(theme) => { + if let Some(client) = client.0.upgrade() { + let mut client = client.borrow_mut(); + client.cursor.set_theme(theme); + } + } + XDPEvent::CursorSize(size) => { + if let Some(client) = client.0.upgrade() { + let mut client = client.borrow_mut(); + client.cursor.set_size(size); + } + } + } + }) + .unwrap(); + + let state = Rc::new(RefCell::new(WaylandClientState { + serial_tracker: SerialTracker::new(), + globals, + gpu_context, + compositor_gpu, + wl_seat: seat, + wl_pointer: None, + wl_keyboard: None, + pinch_gesture: None, + pinch_scale: 1.0, + cursor_shape_device: None, + data_device, + primary_selection, + text_input: None, + pre_edit_text: None, + ime_pre_edit: None, + composing: false, + last_ime_cursor_rectangle: None, + outputs: HashMap::default(), + in_progress_outputs, + wl_outputs, + windows: HashMap::default(), + common, + keyboard_layout: LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME), + keymap_state: None, + compose_state: None, + drag: DragState { + data_offer: None, + window: None, + position: Point::default(), + uri_read_generation: 0, + }, + external_drag: None, + click: ClickState { + last_click: Instant::now(), + last_mouse_button: None, + last_location: Point::default(), + current_count: 0, + }, + repeat: KeyRepeat { + characters_per_second: 16, + delay: Duration::from_millis(500), + current_id: 0, + current_keycode: None, + }, + modifiers: Modifiers { + shift: false, + control: false, + alt: false, + function: false, + platform: false, + }, + capslock: Capslock { on: false }, + scroll_event_received: false, + axis_source: AxisSource::Wheel, + mouse_location: None, + continuous_scroll_delta: None, + discrete_scroll_delta: None, + vertical_modifier: -1.0, + horizontal_modifier: -1.0, + button_pressed: None, + mouse_focused_window: None, + keyboard_focused_window: None, + loop_handle: handle.clone(), + enter_token: None, + cursor_style: None, + cursor_hidden_window: None, + clipboard: Clipboard::new(conn.clone(), handle.clone()), + data_offers: Vec::new(), + primary_data_offer: None, + cursor, + pending_activation: None, + startup_activation_token, + event_loop: Some(event_loop), + ime_enabled: None, + })); + + WaylandSource::new(conn, event_queue) + .insert(handle) + .unwrap(); + + Self(state) + } +} + +impl LinuxClient for WaylandClient { + fn keyboard_layout(&self) -> Box { + Box::new(self.0.borrow().keyboard_layout.clone()) + } + + fn displays(&self) -> Vec> { + self.0 + .borrow() + .outputs + .iter() + .map(|(id, output)| { + Rc::new(WaylandDisplay { + id: id.clone(), + name: output.name.clone(), + bounds: output.bounds.to_pixels(output.scale as f32), + }) as Rc + }) + .collect() + } + + fn display(&self, id: DisplayId) -> Option> { + self.0 + .borrow() + .outputs + .iter() + .find_map(|(object_id, output)| { + (object_id.protocol_id() as u64 == u64::from(id)).then(|| { + Rc::new(WaylandDisplay { + id: object_id.clone(), + name: output.name.clone(), + bounds: output.bounds.to_pixels(output.scale as f32), + }) as Rc + }) + }) + } + + fn primary_display(&self) -> Option> { + None + } + + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> futures::channel::oneshot::Receiver>>> + { + // TODO: Get screen capture working on wayland. Be sure to try window resizing as that may + // be tricky. + // + // start_scap_default_target_source() + let (sources_tx, sources_rx) = futures::channel::oneshot::channel(); + sources_tx + .send(Err(anyhow::anyhow!( + "Wayland screen capture not yet implemented." + ))) + .ok(); + sources_rx + } + + fn open_window( + &self, + handle: AnyWindowHandle, + params: WindowParams, + ) -> anyhow::Result> { + let mut state = self.0.borrow_mut(); + + // Popups name their parent explicitly. Other kinds are parented to the focused window. + let (parent, popup_grab) = match ¶ms.kind { + WindowKind::AnchoredPopup(options) => { + let parent = state + .windows + .values() + .find(|window| window.handle() == options.parent) + .cloned() + .ok_or_else(|| anyhow::anyhow!("popup parent window not found"))?; + // A popup grab must reference a press event or the compositor declines it and + // immediately dismisses the popup, so use the most recent press serial, or no + // grab before any press. + let popup_grab = options.grab.then(|| { + let serial = state + .serial_tracker + .get(SerialKind::MousePress) + .as_raw() + .max(state.serial_tracker.get(SerialKind::KeyPress).as_raw()); + (serial != 0).then(|| (serial, state.wl_seat.clone())) + }); + (Some(parent), popup_grab.flatten()) + } + _ => (state.keyboard_focused_window.clone(), None), + }; + + let target_output = params.display_id.and_then(|display_id| { + let target_protocol_id: u64 = display_id.into(); + state + .wl_outputs + .iter() + .find(|(id, _)| id.protocol_id() as u64 == target_protocol_id) + .map(|(_, output)| output.clone()) + }); + + let appearance = state.common.appearance; + let compositor_gpu = state.compositor_gpu.take(); + + let (window, surface_id) = WaylandWindow::new( + handle, + state.globals.clone(), + state.gpu_context.clone(), + compositor_gpu, + WaylandClientStatePtr(Rc::downgrade(&self.0)), + params, + appearance, + parent, + popup_grab, + target_output, + )?; + + if window.0.toplevel().is_some() { + state.consume_startup_activation_token(&window.0.surface()); + } + state.windows.insert(surface_id, window.0.clone()); + + Ok(Box::new(window)) + } + + fn set_cursor_style(&self, style: CursorStyle) { + let mut state = self.0.borrow_mut(); + + let need_update = state.cursor_style != Some(style) + && (state.mouse_focused_window.is_none() + || state + .mouse_focused_window + .as_ref() + .is_some_and(|w| !w.is_blocked())); + + if !need_update { + return; + } + + state.cursor_style = Some(style); + + // Don't clobber the invisible cursor; restore reads back from `cursor_style`. + if state.cursor_hidden_window.is_some() { + return; + } + + let serial = state.serial_tracker.get(SerialKind::MouseEnter); + if let Some(cursor_shape_device) = &state.cursor_shape_device { + cursor_shape_device.set_shape(serial.as_raw(), to_shape(style)); + } else if let Some(focused_window) = &state.mouse_focused_window { + // cursor-shape-v1 isn't supported, set the cursor using a surface. + let wl_pointer = state + .wl_pointer + .clone() + .expect("window is focused by pointer"); + let scale = focused_window.primary_output_scale(); + state.cursor.set_icon( + &wl_pointer, + serial.as_raw(), + cursor_style_to_icon_names(style), + scale, + ); + } + } + + fn hide_cursor_until_mouse_moves(&self) { + self.0.borrow_mut().hide_cursor_until_mouse_moves(); + } + + fn is_cursor_visible(&self) -> bool { + self.0.borrow().cursor_hidden_window.is_none() + } + + fn open_uri(&self, uri: &str) { + let mut state = self.0.borrow_mut(); + if let (Some(activation), Some(window)) = ( + state.globals.activation.clone(), + state.mouse_focused_window.clone(), + ) { + state.pending_activation = Some(PendingActivation::Uri(uri.to_string())); + let token = activation.get_activation_token(&state.globals.qh, ()); + let serial = state.serial_tracker.get(SerialKind::MousePress); + token.set_serial(serial.as_raw(), &state.wl_seat); + token.set_surface(&window.surface()); + token.commit(); + } else { + let executor = state.common.background_executor.clone(); + open_uri_internal(executor, uri, None); + } + } + + fn reveal_path(&self, path: PathBuf) { + let mut state = self.0.borrow_mut(); + if let (Some(activation), Some(window)) = ( + state.globals.activation.clone(), + state.mouse_focused_window.clone(), + ) { + state.pending_activation = Some(PendingActivation::Path(path)); + let token = activation.get_activation_token(&state.globals.qh, ()); + let serial = state.serial_tracker.get(SerialKind::MousePress); + token.set_serial(serial.as_raw(), &state.wl_seat); + token.set_surface(&window.surface()); + token.commit(); + } else { + let executor = state.common.background_executor.clone(); + reveal_path_internal(executor, path, None); + } + } + + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { + f(&mut self.0.borrow_mut().common) + } + + fn run(&self) { + let mut event_loop = self + .0 + .borrow_mut() + .event_loop + .take() + .expect("App is already running"); + + event_loop + .run( + None, + &mut WaylandClientStatePtr(Rc::downgrade(&self.0)), + |_| {}, + ) + .log_err(); + } + + fn write_to_primary(&self, item: gpui::ClipboardItem) { + let mut state = self.0.borrow_mut(); + let (Some(primary_selection_manager), Some(primary_selection)) = ( + state.globals.primary_selection_manager.clone(), + state.primary_selection.clone(), + ) else { + return; + }; + if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() { + state.clipboard.set_primary(item); + let Some(serial) = state.serial_tracker.selection_serial() else { + log::warn!( + "Skipping Wayland primary selection ownership request because no keyboard or pointer press serial has been received" + ); + return; + }; + let data_source = primary_selection_manager.create_source(&state.globals.qh, ()); + for mime_type in TEXT_MIME_TYPES { + data_source.offer(mime_type.to_string()); + } + data_source.offer(state.clipboard.self_mime()); + primary_selection.set_selection(Some(&data_source), serial.as_raw()); + } + } + + fn write_to_clipboard(&self, item: gpui::ClipboardItem) { + let mut state = self.0.borrow_mut(); + let (Some(data_device_manager), Some(data_device)) = ( + state.globals.data_device_manager.clone(), + state.data_device.clone(), + ) else { + return; + }; + if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() { + state.clipboard.set(item); + let Some(serial) = state.serial_tracker.selection_serial() else { + log::warn!( + "Skipping Wayland clipboard ownership request because no keyboard or pointer press serial has been received" + ); + return; + }; + let data_source = data_device_manager + .create_data_source(&state.globals.qh, DataSourceKind::Clipboard); + for mime_type in TEXT_MIME_TYPES { + data_source.offer(mime_type.to_string()); + } + data_source.offer(state.clipboard.self_mime()); + data_device.set_selection(Some(&data_source), serial.as_raw()); + } + } + + fn read_from_primary(&self) -> Option { + self.0.borrow_mut().clipboard.read_primary() + } + + fn read_from_clipboard(&self) -> Option { + self.0.borrow_mut().clipboard.read() + } + + fn active_window(&self) -> Option { + self.0 + .borrow_mut() + .keyboard_focused_window + .as_ref() + .map(|window| window.handle()) + } + + fn window_stack(&self) -> Option> { + None + } + + fn compositor_name(&self) -> &'static str { + "Wayland" + } + + fn window_identifier(&self) -> impl Future> + Send + 'static { + async fn inner(surface: Option) -> Option { + if let Some(surface) = surface { + ashpd::WindowIdentifier::from_wayland(&surface).await + } else { + None + } + } + + let client_state = self.0.borrow(); + let active_window = client_state.keyboard_focused_window.as_ref(); + inner(active_window.map(|aw| aw.surface())) + } +} + +struct DmabufProbeState { + device: Option, +} + +impl Dispatch for DmabufProbeState { + fn event( + _: &mut Self, + _: &wl_registry::WlRegistry, + _: wl_registry::Event, + _: &GlobalListContents, + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for DmabufProbeState { + fn event( + _: &mut Self, + _: &zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, + _: zwp_linux_dmabuf_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for DmabufProbeState { + fn event( + state: &mut Self, + _: &zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1, + event: zwp_linux_dmabuf_feedback_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let zwp_linux_dmabuf_feedback_v1::Event::MainDevice { device } = event { + if let Ok(bytes) = <[u8; 8]>::try_from(device.as_slice()) { + state.device = Some(u64::from_ne_bytes(bytes)); + } + } + } +} + +fn detect_compositor_gpu() -> Option { + let connection = Connection::connect_to_env().ok()?; + let (globals, mut event_queue) = registry_queue_init::(&connection).ok()?; + let queue_handle = event_queue.handle(); + + let dmabuf: zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1 = + globals.bind(&queue_handle, 4..=4, ()).ok()?; + let feedback = dmabuf.get_default_feedback(&queue_handle, ()); + + let mut state = DmabufProbeState { device: None }; + + event_queue.roundtrip(&mut state).ok()?; + + feedback.destroy(); + dmabuf.destroy(); + + crate::linux::compositor_gpu_hint_from_dev_t(state.device?) +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _: &GlobalListContents, + _: &Connection, + qh: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + wl_registry::Event::Global { + name, + interface, + version, + } => match &interface[..] { + "wl_seat" => { + if let Some(wl_pointer) = state.wl_pointer.take() { + wl_pointer.release(); + } + if let Some(wl_keyboard) = state.wl_keyboard.take() { + wl_keyboard.release(); + } + state.wl_seat.release(); + state.wl_seat = registry.bind::( + name, + wl_seat_version(version), + qh, + (), + ); + } + "wl_output" => { + let output = registry.bind::( + name, + wl_output_version(version), + qh, + (), + ); + + state + .in_progress_outputs + .insert(output.id(), InProgressOutput::default()); + state.wl_outputs.insert(output.id(), output); + } + _ => {} + }, + wl_registry::Event::GlobalRemove { name: _ } => { + // TODO: handle global removal + } + _ => {} + } + } +} + +delegate_noop!(WaylandClientStatePtr: ignore xdg_activation_v1::XdgActivationV1); +delegate_noop!(WaylandClientStatePtr: ignore xdg_system_bell_v1::XdgSystemBellV1); +delegate_noop!(WaylandClientStatePtr: ignore wl_compositor::WlCompositor); +delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_device_v1::WpCursorShapeDeviceV1); +delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_manager_v1::WpCursorShapeManagerV1); +delegate_noop!(WaylandClientStatePtr: ignore wl_data_device_manager::WlDataDeviceManager); +delegate_noop!(WaylandClientStatePtr: ignore zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1); +delegate_noop!(WaylandClientStatePtr: ignore wl_shm::WlShm); +delegate_noop!(WaylandClientStatePtr: ignore wl_shm_pool::WlShmPool); +delegate_noop!(WaylandClientStatePtr: ignore wl_buffer::WlBuffer); +delegate_noop!(WaylandClientStatePtr: ignore wl_region::WlRegion); +delegate_noop!(WaylandClientStatePtr: ignore wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1); +delegate_noop!(WaylandClientStatePtr: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1); +delegate_noop!(WaylandClientStatePtr: ignore zwlr_layer_shell_v1::ZwlrLayerShellV1); +delegate_noop!(WaylandClientStatePtr: ignore xdg_positioner::XdgPositioner); +delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur_manager::OrgKdeKwinBlurManager); +delegate_noop!(WaylandClientStatePtr: ignore zwp_text_input_manager_v3::ZwpTextInputManagerV3); +delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur::OrgKdeKwinBlur); +delegate_noop!(WaylandClientStatePtr: ignore ext_background_effect_manager_v1::ExtBackgroundEffectManagerV1); +delegate_noop!(WaylandClientStatePtr: ignore ext_background_effect_surface_v1::ExtBackgroundEffectSurfaceV1); +delegate_noop!(WaylandClientStatePtr: ignore wp_viewporter::WpViewporter); +delegate_noop!(WaylandClientStatePtr: ignore wp_viewport::WpViewport); + +impl Dispatch for WaylandClientStatePtr { + fn event( + state: &mut WaylandClientStatePtr, + _: &wl_callback::WlCallback, + event: wl_callback::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = state.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + drop(state); + + if let wl_callback::Event::Done { .. } = event { + window.frame_callback_fired(); + } + } +} + +pub(crate) fn get_window( + state: &mut RefMut, + surface_id: &ObjectId, +) -> Option { + state.windows.get(surface_id).cloned() +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + surface: &wl_surface::WlSurface, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + let Some(window) = get_window(&mut state, &surface.id()) else { + return; + }; + #[allow(clippy::mutable_key_type)] + let outputs = state.outputs.clone(); + drop(state); + + window.handle_surface_event(event, outputs); + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + output: &wl_output::WlOutput, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + let Some(in_progress_output) = state.in_progress_outputs.get_mut(&output.id()) else { + return; + }; + + match event { + wl_output::Event::Name { name } => { + in_progress_output.name = Some(name); + } + wl_output::Event::Scale { factor } => { + in_progress_output.scale = Some(factor); + } + wl_output::Event::Geometry { x, y, subpixel, .. } => { + in_progress_output.position = Some(point(DevicePixels(x), DevicePixels(y))); + if let WEnum::Value(subpixel) = subpixel { + in_progress_output.subpixel = Some(subpixel); + } + } + wl_output::Event::Mode { width, height, .. } => { + in_progress_output.size = Some(size(DevicePixels(width), DevicePixels(height))) + } + wl_output::Event::Done => { + if let Some(complete) = in_progress_output.complete() { + state.outputs.insert(output.id(), complete); + } + state.in_progress_outputs.remove(&output.id()); + } + _ => {} + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + state: &mut Self, + _: &xdg_surface::XdgSurface, + event: xdg_surface::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = state.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + drop(state); + window.handle_xdg_surface_event(event); + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + _: &xdg_toplevel::XdgToplevel, + event: ::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + + drop(state); + let should_close = window.handle_toplevel_event(event); + + if should_close { + // The close logic will be handled in drop_window() + window.close(); + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + _: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, + event: ::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + + drop(state); + let should_close = window.handle_layersurface_event(event); + + if should_close { + // Close logic will be handled in drop_window() + window.close(); + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + _: &xdg_popup::XdgPopup, + event: ::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + + drop(state); + let should_close = window.handle_popup_event(event); + + if should_close { + // The close logic will be handled in drop_window() + window.close(); + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + _: &mut Self, + wm_base: &xdg_wm_base::XdgWmBase, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_wm_base::Event::Ping { serial } = event { + wm_base.pong(serial); + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + token: &xdg_activation_token_v1::XdgActivationTokenV1, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + if let xdg_activation_token_v1::Event::Done { token } = event { + let executor = state.common.background_executor.clone(); + match state.pending_activation.take() { + Some(PendingActivation::Uri(uri)) => open_uri_internal(executor, &uri, Some(token)), + Some(PendingActivation::Path(path)) => { + reveal_path_internal(executor, path, Some(token)) + } + Some(PendingActivation::Window(window)) => { + let Some(window) = get_window(&mut state, &window) else { + return; + }; + let activation = state.globals.activation.as_ref().unwrap(); + activation.activate(token, &window.surface()); + } + None => log::error!("activation token received with no pending activation"), + } + } + + token.destroy(); + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + state: &mut Self, + seat: &wl_seat::WlSeat, + event: wl_seat::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + if let wl_seat::Event::Capabilities { + capabilities: WEnum::Value(capabilities), + } = event + { + let client = state.get_client(); + let mut state = client.borrow_mut(); + if capabilities.contains(wl_seat::Capability::Keyboard) { + let keyboard = seat.get_keyboard(qh, ()); + + if let Some(text_input) = state.text_input.take() { + text_input.destroy(); + state.ime_pre_edit = None; + state.composing = false; + } + + state.text_input = state + .globals + .text_input_manager + .as_ref() + .map(|text_input_manager| text_input_manager.get_text_input(seat, qh, ())); + + if let Some(wl_keyboard) = &state.wl_keyboard { + wl_keyboard.release(); + } + + state.wl_keyboard = Some(keyboard); + } + if capabilities.contains(wl_seat::Capability::Pointer) { + let pointer = seat.get_pointer(qh, ()); + + if let Some(cursor_shape_device) = state.cursor_shape_device.take() { + cursor_shape_device.destroy(); + } + + state.cursor_shape_device = state + .globals + .cursor_shape_manager + .as_ref() + .map(|cursor_shape_manager| cursor_shape_manager.get_pointer(&pointer, qh, ())); + + state.pinch_gesture = state.globals.gesture_manager.as_ref().map( + |gesture_manager: &zwp_pointer_gestures_v1::ZwpPointerGesturesV1| { + gesture_manager.get_pinch_gesture(&pointer, qh, ()) + }, + ); + + if let Some(wl_pointer) = &state.wl_pointer { + wl_pointer.release(); + } + + state.wl_pointer = Some(pointer); + } + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + _: &wl_keyboard::WlKeyboard, + event: wl_keyboard::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + match event { + wl_keyboard::Event::RepeatInfo { rate, delay } => { + state.repeat.characters_per_second = rate as u32; + state.repeat.delay = Duration::from_millis(delay as u64); + } + wl_keyboard::Event::Keymap { + format: WEnum::Value(format), + fd, + size, + .. + } => { + if format != wl_keyboard::KeymapFormat::XkbV1 { + log::error!("Received keymap format {:?}, expected XkbV1", format); + return; + } + let xkb_context = match new_xkb_context() { + Ok(context) => context, + Err(error) => { + log::error!("Failed to process Wayland keymap: {error:#}"); + return; + } + }; + let keymap = unsafe { + xkb::Keymap::new_from_fd( + &xkb_context, + fd, + size as usize, + XKB_KEYMAP_FORMAT_TEXT_V1, + KEYMAP_COMPILE_NO_FLAGS, + ) + .log_err() + .flatten() + .expect("Failed to create keymap") + }; + state.keymap_state = Some(xkb::State::new(&keymap)); + state.compose_state = get_xkb_compose_state(&xkb_context); + drop(state); + + this.handle_keyboard_layout_change(); + } + wl_keyboard::Event::Enter { surface, .. } => { + state.keyboard_focused_window = get_window(&mut state, &surface.id()); + state.enter_token = Some(()); + + if let Some(window) = state.keyboard_focused_window.clone() { + drop(state); + window.set_focused(true); + } + } + wl_keyboard::Event::Leave { surface, .. } => { + let keyboard_focused_window = get_window(&mut state, &surface.id()); + state.keyboard_focused_window = None; + state.enter_token.take(); + // Prevent keyboard events from repeating after opening e.g. a file chooser and closing it quickly + state.repeat.current_id += 1; + state.restore_cursor_after_hide(); + + if let Some(window) = keyboard_focused_window { + if let Some(ref mut compose) = state.compose_state { + compose.reset(); + } + state.pre_edit_text.take(); + drop(state); + window.handle_ime(ImeInput::DeleteText); + window.set_focused(false); + } + } + wl_keyboard::Event::Modifiers { + mods_depressed, + mods_latched, + mods_locked, + group, + .. + } => { + let focused_window = state.keyboard_focused_window.clone(); + + let keymap_state = state.keymap_state.as_mut().unwrap(); + let old_layout = + keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE); + keymap_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group); + state.modifiers = modifiers_from_xkb(keymap_state); + let keymap_state = state.keymap_state.as_mut().unwrap(); + state.capslock = capslock_from_xkb(keymap_state); + + let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent { + modifiers: state.modifiers, + capslock: state.capslock, + }); + drop(state); + + if let Some(focused_window) = focused_window { + focused_window.handle_input(input); + } + + if group != old_layout { + this.handle_keyboard_layout_change(); + } + } + wl_keyboard::Event::Key { + serial, + key, + state: WEnum::Value(key_state), + .. + } => { + if key_state == wl_keyboard::KeyState::Pressed { + state.serial_tracker.update(SerialKind::KeyPress, serial); + } + + let focused_window = state.keyboard_focused_window.clone(); + let Some(focused_window) = focused_window else { + return; + }; + + let keymap_state = state.keymap_state.as_ref().unwrap(); + let keycode = Keycode::from(key + MIN_KEYCODE); + let keysym = keymap_state.key_get_one_sym(keycode); + + match key_state { + wl_keyboard::KeyState::Pressed if !keysym.is_modifier_key() => { + let mut keystroke = + keystroke_from_xkb(keymap_state, state.modifiers, keycode); + if let Some(mut compose) = state.compose_state.take() { + compose.feed(keysym); + match compose.status() { + xkb::Status::Composing => { + keystroke.key_char = None; + state.pre_edit_text = + compose.utf8().or(keystroke_underlying_dead_key(keysym)); + let pre_edit = + state.pre_edit_text.clone().unwrap_or(String::default()); + drop(state); + focused_window.handle_ime(ImeInput::SetMarkedText(pre_edit)); + state = client.borrow_mut(); + } + + xkb::Status::Composed => { + state.pre_edit_text.take(); + keystroke.key_char = compose.utf8(); + if let Some(keysym) = compose.keysym() { + keystroke.key = xkb::keysym_get_name(keysym); + } + } + xkb::Status::Cancelled => { + let pre_edit = state.pre_edit_text.take(); + let new_pre_edit = keystroke_underlying_dead_key(keysym); + state.pre_edit_text = new_pre_edit.clone(); + drop(state); + if let Some(pre_edit) = pre_edit { + focused_window.handle_ime(ImeInput::InsertText(pre_edit)); + } + if let Some(current_key) = new_pre_edit { + focused_window + .handle_ime(ImeInput::SetMarkedText(current_key)); + } + compose.feed(keysym); + state = client.borrow_mut(); + } + _ => {} + } + state.compose_state = Some(compose); + } + let input = PlatformInput::KeyDown(KeyDownEvent { + keystroke: keystroke.clone(), + is_held: false, + prefer_character_input: false, + }); + + state.repeat.current_id += 1; + state.repeat.current_keycode = Some(keycode); + + let rate = state.repeat.characters_per_second; + let repeat_interval = Duration::from_secs(1) / rate.max(1); + let id = state.repeat.current_id; + state + .loop_handle + .insert_source(Timer::from_duration(state.repeat.delay), { + let input = PlatformInput::KeyDown(KeyDownEvent { + keystroke, + is_held: true, + prefer_character_input: false, + }); + move |event_timestamp, _metadata, this| { + let client = this.get_client(); + let state = client.borrow(); + let is_repeating = id == state.repeat.current_id + && state.repeat.current_keycode.is_some() + && state.keyboard_focused_window.is_some(); + + if !is_repeating || rate == 0 { + return TimeoutAction::Drop; + } + + let focused_window = + state.keyboard_focused_window.as_ref().unwrap().clone(); + + drop(state); + focused_window.handle_input(input.clone()); + + // If the new scheduled time is in the past the event will repeat as soon as possible + TimeoutAction::ToInstant(event_timestamp + repeat_interval) + } + }) + .unwrap(); + + drop(state); + focused_window.handle_input(input); + } + wl_keyboard::KeyState::Released if !keysym.is_modifier_key() => { + let input = PlatformInput::KeyUp(KeyUpEvent { + keystroke: keystroke_from_xkb(keymap_state, state.modifiers, keycode), + }); + + if state.repeat.current_keycode == Some(keycode) { + state.repeat.current_keycode = None; + } + + drop(state); + focused_window.handle_input(input); + } + _ => {} + } + } + _ => {} + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + text_input: &zwp_text_input_v3::ZwpTextInputV3, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + match event { + zwp_text_input_v3::Event::Enter { .. } => { + drop(state); + this.enable_ime(); + } + zwp_text_input_v3::Event::Leave { .. } => { + drop(state); + this.disable_ime(); + } + zwp_text_input_v3::Event::CommitString { text } => { + state.composing = false; + let Some(window) = state.keyboard_focused_window.clone() else { + return; + }; + + if let Some(commit_text) = text { + drop(state); + // IBus Intercepts keys like `a`, `b`, but those keys are needed for vim mode. + // We should only send ASCII characters to Zed, otherwise a user could remap a letter like `か` or `相`. + if commit_text.len() == 1 { + window.handle_input(PlatformInput::KeyDown(KeyDownEvent { + keystroke: Keystroke { + modifiers: Modifiers::default(), + key: commit_text.clone(), + key_char: Some(commit_text), + }, + is_held: false, + prefer_character_input: false, + })); + } else { + window.handle_ime(ImeInput::InsertText(commit_text)); + } + } + } + zwp_text_input_v3::Event::PreeditString { text, .. } => { + state.composing = true; + state.ime_pre_edit = text; + } + zwp_text_input_v3::Event::Done { serial } => { + let last_serial = state.serial_tracker.get(SerialKind::InputMethod); + state.serial_tracker.update(SerialKind::InputMethod, serial); + let Some(window) = state.keyboard_focused_window.clone() else { + return; + }; + + if let Some(text) = state.ime_pre_edit.take() { + drop(state); + window.handle_ime(ImeInput::SetMarkedText(text)); + if let Some(area) = window.get_ime_area() { + let mut state = client.borrow_mut(); + set_ime_cursor_rectangle_after_done( + text_input, + &mut state.last_ime_cursor_rectangle, + area, + last_serial.as_raw() == serial, + ); + } + } else { + state.composing = false; + drop(state); + window.handle_ime(ImeInput::DeleteText); + } + } + _ => {} + } + } +} + +fn linux_button_to_gpui(button: u32) -> Option { + // These values are coming from . + const BTN_LEFT: u32 = 0x110; + const BTN_RIGHT: u32 = 0x111; + const BTN_MIDDLE: u32 = 0x112; + const BTN_SIDE: u32 = 0x113; + const BTN_EXTRA: u32 = 0x114; + const BTN_FORWARD: u32 = 0x115; + const BTN_BACK: u32 = 0x116; + + Some(match button { + BTN_LEFT => MouseButton::Left, + BTN_RIGHT => MouseButton::Right, + BTN_MIDDLE => MouseButton::Middle, + BTN_BACK | BTN_SIDE => MouseButton::Navigate(NavigationDirection::Back), + BTN_FORWARD | BTN_EXTRA => MouseButton::Navigate(NavigationDirection::Forward), + _ => return None, + }) +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + wl_pointer: &wl_pointer::WlPointer, + event: wl_pointer::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + wl_pointer::Event::Enter { + serial, + surface, + surface_x, + surface_y, + .. + } => { + let position = point(px(surface_x as f32), px(surface_y as f32)); + state.serial_tracker.update(SerialKind::MouseEnter, serial); + state.mouse_location = Some(position); + state.button_pressed = None; + + if let Some(window) = get_window(&mut state, &surface.id()) { + state.mouse_focused_window = Some(window.clone()); + + if state.enter_token.is_some() { + state.enter_token = None; + } + state.restore_cursor_after_hide(); + if let Some(style) = state.cursor_style { + if let Some(cursor_shape_device) = &state.cursor_shape_device { + cursor_shape_device.set_shape(serial, to_shape(style)); + } else { + let scale = window.primary_output_scale(); + state.cursor.set_icon( + wl_pointer, + serial, + cursor_style_to_icon_names(style), + scale, + ); + } + } + let modifiers = state.modifiers; + drop(state); + window.set_hovered(true); + // No Motion follows Enter unless the pointer keeps moving, so synthesize + // a MouseMove to establish hover at the entry position. + window.handle_input(PlatformInput::MouseMove(MouseMoveEvent { + position, + pressed_button: None, + modifiers, + })); + } + } + wl_pointer::Event::Leave { .. } => { + if let Some(focused_window) = state.mouse_focused_window.clone() { + let input = PlatformInput::MouseExited(MouseExitEvent { + position: state.mouse_location.unwrap(), + pressed_button: state.button_pressed, + modifiers: state.modifiers, + }); + state.mouse_focused_window = None; + state.mouse_location = None; + state.button_pressed = None; + state.cursor_hidden_window = None; + + drop(state); + focused_window.handle_input(input); + focused_window.set_hovered(false); + } + } + wl_pointer::Event::Motion { + surface_x, + surface_y, + .. + } => { + if state.mouse_focused_window.is_none() { + return; + } + state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32))); + state.restore_cursor_after_hide(); + + if let Some(window) = state.mouse_focused_window.clone() { + if window.is_blocked() { + let default_style = CursorStyle::Arrow; + if state.cursor_style != Some(default_style) { + let serial = state.serial_tracker.get(SerialKind::MouseEnter); + state.cursor_style = Some(default_style); + + if let Some(cursor_shape_device) = &state.cursor_shape_device { + cursor_shape_device + .set_shape(serial.as_raw(), to_shape(default_style)); + } else { + // cursor-shape-v1 isn't supported, set the cursor using a surface. + let wl_pointer = state + .wl_pointer + .clone() + .expect("window is focused by pointer"); + let scale = window.primary_output_scale(); + state.cursor.set_icon( + &wl_pointer, + serial.as_raw(), + cursor_style_to_icon_names(default_style), + scale, + ); + } + } + } + if state + .keyboard_focused_window + .as_ref() + .is_some_and(|keyboard_window| window.ptr_eq(keyboard_window)) + { + state.enter_token = None; + } + let input = PlatformInput::MouseMove(MouseMoveEvent { + position: state.mouse_location.unwrap(), + pressed_button: state.button_pressed, + modifiers: state.modifiers, + }); + drop(state); + window.handle_input(input); + } + } + wl_pointer::Event::Button { + serial, + button, + state: WEnum::Value(button_state), + .. + } => { + // Record presses only. Requests referencing this serial (popup grabs, + // interactive moves) are declined when given a release serial. + if button_state == wl_pointer::ButtonState::Pressed { + state.serial_tracker.update(SerialKind::MousePress, serial); + } + let button = linux_button_to_gpui(button); + let Some(button) = button else { return }; + if state.mouse_focused_window.is_none() { + return; + } + match button_state { + wl_pointer::ButtonState::Pressed => { + if let Some(window) = state.keyboard_focused_window.clone() { + if state.composing && state.text_input.is_some() { + drop(state); + // text_input_v3 don't have something like a reset function + this.disable_ime(); + this.enable_ime(); + window.handle_ime(ImeInput::UnmarkText); + state = client.borrow_mut(); + } else if let (Some(text), Some(compose)) = + (state.pre_edit_text.take(), state.compose_state.as_mut()) + { + compose.reset(); + drop(state); + window.handle_ime(ImeInput::InsertText(text)); + state = client.borrow_mut(); + } + } + let click_elapsed = state.click.last_click.elapsed(); + + if click_elapsed < DOUBLE_CLICK_INTERVAL + && state + .click + .last_mouse_button + .is_some_and(|prev_button| prev_button == button) + && is_within_click_distance( + state.click.last_location, + state.mouse_location.unwrap(), + ) + { + state.click.current_count += 1; + } else { + state.click.current_count = 1; + } + + state.click.last_click = Instant::now(); + state.click.last_mouse_button = Some(button); + state.click.last_location = state.mouse_location.unwrap(); + + state.button_pressed = Some(button); + + if let Some(window) = state.mouse_focused_window.clone() { + let input = PlatformInput::MouseDown(MouseDownEvent { + button, + position: state.mouse_location.unwrap(), + modifiers: state.modifiers, + click_count: state.click.current_count, + first_mouse: state.enter_token.take().is_some(), + }); + drop(state); + window.handle_input(input); + } + } + wl_pointer::ButtonState::Released => { + state.button_pressed = None; + + if let Some(window) = state.mouse_focused_window.clone() { + let input = PlatformInput::MouseUp(MouseUpEvent { + button, + position: state.mouse_location.unwrap(), + modifiers: state.modifiers, + click_count: state.click.current_count, + }); + drop(state); + window.handle_input(input); + } + } + _ => {} + } + } + + // Axis Events + wl_pointer::Event::AxisSource { + axis_source: WEnum::Value(axis_source), + } => { + state.axis_source = axis_source; + } + wl_pointer::Event::Axis { + axis: WEnum::Value(axis), + value, + .. + } => { + if state.axis_source == AxisSource::Wheel { + return; + } + let axis = if state.modifiers.shift { + wl_pointer::Axis::HorizontalScroll + } else { + axis + }; + let axis_modifier = match axis { + wl_pointer::Axis::VerticalScroll => state.vertical_modifier, + wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, + _ => 1.0, + }; + state.scroll_event_received = true; + let scroll_delta = state + .continuous_scroll_delta + .get_or_insert(point(px(0.0), px(0.0))); + let modifier = 3.0; + match axis { + wl_pointer::Axis::VerticalScroll => { + scroll_delta.y += px(value as f32 * modifier * axis_modifier); + } + wl_pointer::Axis::HorizontalScroll => { + scroll_delta.x += px(value as f32 * modifier * axis_modifier); + } + _ => unreachable!(), + } + } + wl_pointer::Event::AxisDiscrete { + axis: WEnum::Value(axis), + discrete, + } => { + state.scroll_event_received = true; + let axis = if state.modifiers.shift { + wl_pointer::Axis::HorizontalScroll + } else { + axis + }; + let axis_modifier = match axis { + wl_pointer::Axis::VerticalScroll => state.vertical_modifier, + wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, + _ => 1.0, + }; + + let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0)); + match axis { + wl_pointer::Axis::VerticalScroll => { + scroll_delta.y += discrete as f32 * axis_modifier * SCROLL_LINES; + } + wl_pointer::Axis::HorizontalScroll => { + scroll_delta.x += discrete as f32 * axis_modifier * SCROLL_LINES; + } + _ => unreachable!(), + } + } + wl_pointer::Event::AxisValue120 { + axis: WEnum::Value(axis), + value120, + } => { + state.scroll_event_received = true; + let axis = if state.modifiers.shift { + wl_pointer::Axis::HorizontalScroll + } else { + axis + }; + let axis_modifier = match axis { + wl_pointer::Axis::VerticalScroll => state.vertical_modifier, + wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, + _ => unreachable!(), + }; + + let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0)); + let wheel_percent = value120 as f32 / 120.0; + match axis { + wl_pointer::Axis::VerticalScroll => { + scroll_delta.y += wheel_percent * axis_modifier * SCROLL_LINES; + } + wl_pointer::Axis::HorizontalScroll => { + scroll_delta.x += wheel_percent * axis_modifier * SCROLL_LINES; + } + _ => unreachable!(), + } + } + wl_pointer::Event::Frame => { + if state.scroll_event_received { + state.scroll_event_received = false; + let continuous = state.continuous_scroll_delta.take(); + let discrete = state.discrete_scroll_delta.take(); + if let Some(continuous) = continuous { + if let Some(window) = state.mouse_focused_window.clone() { + let input = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: state.mouse_location.unwrap(), + delta: ScrollDelta::Pixels(continuous), + modifiers: state.modifiers, + touch_phase: TouchPhase::Moved, + }); + drop(state); + window.handle_input(input); + } + } else if let Some(discrete) = discrete + && let Some(window) = state.mouse_focused_window.clone() + { + let input = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: state.mouse_location.unwrap(), + delta: ScrollDelta::Lines(discrete), + modifiers: state.modifiers, + touch_phase: TouchPhase::Moved, + }); + drop(state); + window.handle_input(input); + } + } + } + _ => {} + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + _this: &mut Self, + _: &zwp_pointer_gestures_v1::ZwpPointerGesturesV1, + _: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + // The gesture manager doesn't generate events + } +} + +impl Dispatch + for WaylandClientStatePtr +{ + fn event( + this: &mut Self, + _: &zwp_pointer_gesture_pinch_v1::ZwpPointerGesturePinchV1, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + use gpui::PinchEvent; + + let client = this.get_client(); + let mut state = client.borrow_mut(); + + let Some(window) = state.mouse_focused_window.clone() else { + return; + }; + + match event { + zwp_pointer_gesture_pinch_v1::Event::Begin { + serial: _, + time: _, + surface: _, + fingers: _, + } => { + state.pinch_scale = 1.0; + let input = PlatformInput::Pinch(PinchEvent { + position: state.mouse_location.unwrap_or(point(px(0.0), px(0.0))), + delta: 0.0, + modifiers: state.modifiers, + phase: TouchPhase::Started, + }); + drop(state); + window.handle_input(input); + } + zwp_pointer_gesture_pinch_v1::Event::Update { time: _, scale, .. } => { + let new_absolute_scale = scale as f32; + let previous_scale = state.pinch_scale; + let zoom_delta = new_absolute_scale - previous_scale; + state.pinch_scale = new_absolute_scale; + + let input = PlatformInput::Pinch(PinchEvent { + position: state.mouse_location.unwrap_or(point(px(0.0), px(0.0))), + delta: zoom_delta, + modifiers: state.modifiers, + phase: TouchPhase::Moved, + }); + drop(state); + window.handle_input(input); + } + zwp_pointer_gesture_pinch_v1::Event::End { + serial: _, + time: _, + cancelled: _, + } => { + state.pinch_scale = 1.0; + let input = PlatformInput::Pinch(PinchEvent { + position: state.mouse_location.unwrap_or(point(px(0.0), px(0.0))), + delta: 0.0, + modifiers: state.modifiers, + phase: TouchPhase::Ended, + }); + drop(state); + window.handle_input(input); + } + _ => {} + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + _: &wp_fractional_scale_v1::WpFractionalScaleV1, + event: ::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + + drop(state); + window.handle_fractional_scale_event(event); + } +} + +impl Dispatch + for WaylandClientStatePtr +{ + fn event( + this: &mut Self, + _: &zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, + event: zxdg_toplevel_decoration_v1::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + + drop(state); + window.handle_toplevel_decoration_event(event); + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + _: &wl_data_device::WlDataDevice, + event: wl_data_device::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + // Clipboard + wl_data_device::Event::DataOffer { id: data_offer } => { + state.data_offers.push(DataOffer::new(data_offer)); + if state.data_offers.len() > 2 { + // At most we store a clipboard offer and a drag and drop offer. + state.data_offers.remove(0).inner.destroy(); + } + } + wl_data_device::Event::Selection { id: data_offer } => { + if let Some(offer) = data_offer { + let offer = state + .data_offers + .iter() + .find(|wrapper| wrapper.inner.id() == offer.id()); + let offer = offer.cloned(); + state.clipboard.set_offer(offer); + } else { + state.clipboard.set_offer(None); + } + } + + // Drag and drop + wl_data_device::Event::Enter { + serial, + surface, + x, + y, + id: data_offer, + } => { + state.serial_tracker.update(SerialKind::DataDevice, serial); + if let Some(data_offer) = data_offer { + let Some(drag_window) = get_window(&mut state, &surface.id()) else { + return; + }; + let uri_read_generation = state.drag.begin_uri_read(); + + const ACTIONS: DndAction = DndAction::Copy; + data_offer.set_actions(ACTIONS, ACTIONS); + + let pipe = Pipe::new().unwrap(); + data_offer.receive(FILE_LIST_MIME_TYPE.to_string(), unsafe { + BorrowedFd::borrow_raw(pipe.write.as_raw_fd()) + }); + let fd = pipe.read; + drop(pipe.write); + + let read_task = state.common.background_executor.spawn(async { + let buffer = read_fd_with_timeout(fd, PIPE_READ_TIMEOUT)?; + let text = String::from_utf8(buffer)?; + anyhow::Ok(text) + }); + + let this = this.clone(); + state + .common + .foreground_executor + .spawn(async move { + let file_list = match read_task.await { + Ok(list) => list, + Err(err) => { + log::error!("error reading drag and drop pipe: {err:?}"); + return; + } + }; + + let paths: SmallVec<[_; 2]> = file_list + .lines() + .filter_map(|path| Url::parse(path).log_err()) + .filter_map(|url| match url.to_file_path() { + Ok(url) => Some(url), + Err(()) => { + log::error!("Failed turn {url:?} into a file path"); + None + } + }) + .collect(); + let position = Point::new(x.into(), y.into()); + + // Prevent dropping text from other programs. + if paths.is_empty() { + data_offer.destroy(); + return; + } + let client = this.get_client(); + let mut state = client.borrow_mut(); + let input = state.drag.complete_uri_read( + uri_read_generation, + data_offer, + drag_window, + position, + gpui::ExternalPaths(paths), + ); + + drop(state); + if let Some((drag_window, input)) = input { + drag_window.handle_input(input); + } + }) + .detach(); + } + } + wl_data_device::Event::Motion { x, y, .. } => { + let Some(drag_window) = state.drag.window.clone() else { + return; + }; + let position = Point::new(x.into(), y.into()); + state.drag.position = position; + + let input = PlatformInput::FileDrop(FileDropEvent::Pending { position }); + drop(state); + drag_window.handle_input(input); + } + wl_data_device::Event::Leave => { + let input = state.drag.handle_leave(); + drop(state); + if let Some((drag_window, input)) = input { + drag_window.handle_input(input); + } + } + wl_data_device::Event::Drop => { + let input = state.drag.handle_drop(); + drop(state); + if let Some((drag_window, input)) = input { + drag_window.handle_input(input); + } + } + _ => {} + } + } + + event_created_child!(WaylandClientStatePtr, wl_data_device::WlDataDevice, [ + wl_data_device::EVT_DATA_OFFER_OPCODE => (wl_data_offer::WlDataOffer, ()), + ]); +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + data_offer: &wl_data_offer::WlDataOffer, + event: wl_data_offer::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + if let wl_data_offer::Event::Offer { mime_type } = event { + // Drag and drop + if mime_type == FILE_LIST_MIME_TYPE { + let serial = state.serial_tracker.get(SerialKind::DataDevice); + let mime_type = mime_type.clone(); + data_offer.accept(serial.as_raw(), Some(mime_type)); + } + + // Clipboard + if let Some(offer) = state + .data_offers + .iter_mut() + .find(|wrapper| wrapper.inner.id() == data_offer.id()) + { + offer.add_mime_type(mime_type); + } + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + data_source: &wl_data_source::WlDataSource, + event: wl_data_source::Event, + kind: &DataSourceKind, + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + match (kind, event) { + (DataSourceKind::Clipboard, wl_data_source::Event::Send { mime_type, fd }) => { + state.clipboard.send(mime_type, fd); + } + (DataSourceKind::Clipboard, wl_data_source::Event::Cancelled) => { + data_source.destroy(); + } + (DataSourceKind::Drag, wl_data_source::Event::Send { fd, .. }) => { + let Some(external_drag) = state.external_drag.as_ref() else { + return; + }; + let bytes = external_drag.bytes.clone(); + state.clipboard.send_bytes(fd, bytes); + } + ( + DataSourceKind::Drag, + wl_data_source::Event::DndFinished | wl_data_source::Event::Cancelled, + ) => { + let Some(external_drag) = state.external_drag.take() else { + return; + }; + external_drag.source.destroy(); + + let input = PlatformInput::FileDrop(FileDropEvent::Ended); + drop(state); + external_drag.window.handle_input(input); + } + _ => {} + } + } +} + +impl Dispatch + for WaylandClientStatePtr +{ + fn event( + this: &mut Self, + _: &zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, + event: zwp_primary_selection_device_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + zwp_primary_selection_device_v1::Event::DataOffer { offer } => { + let old_offer = state.primary_data_offer.replace(DataOffer::new(offer)); + if let Some(old_offer) = old_offer { + old_offer.inner.destroy(); + } + } + zwp_primary_selection_device_v1::Event::Selection { id: data_offer } => { + if data_offer.is_some() { + let offer = state.primary_data_offer.clone(); + state.clipboard.set_primary_offer(offer); + } else { + state.clipboard.set_primary_offer(None); + } + } + _ => {} + } + } + + event_created_child!(WaylandClientStatePtr, zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, [ + zwp_primary_selection_device_v1::EVT_DATA_OFFER_OPCODE => (zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, ()), + ]); +} + +impl Dispatch + for WaylandClientStatePtr +{ + fn event( + this: &mut Self, + _data_offer: &zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, + event: zwp_primary_selection_offer_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + if let zwp_primary_selection_offer_v1::Event::Offer { mime_type } = event + && let Some(offer) = state.primary_data_offer.as_mut() + { + offer.add_mime_type(mime_type); + } + } +} + +impl Dispatch + for WaylandClientStatePtr +{ + fn event( + this: &mut Self, + selection_source: &zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, + event: zwp_primary_selection_source_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let state = client.borrow_mut(); + + match event { + zwp_primary_selection_source_v1::Event::Send { mime_type, fd } => { + state.clipboard.send_primary(mime_type, fd); + } + zwp_primary_selection_source_v1::Event::Cancelled => { + selection_source.destroy(); + } + _ => {} + } + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + _: &mut Self, + _: &XdgWmDialogV1, + _: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + _state: &mut Self, + _proxy: &XdgDialogV1, + _event: ::Event, + _data: &(), + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +#[cfg(test)] +mod tests { + use std::cell::Cell; + + use super::*; + + #[derive(Clone)] + struct FakeDataOffer { + name: &'static str, + finished: Rc>, + destroyed: Rc>, + } + + impl FakeDataOffer { + fn new(name: &'static str) -> Self { + Self { + name, + finished: Rc::new(Cell::new(false)), + destroyed: Rc::new(Cell::new(false)), + } + } + } + + impl FileDragDataOffer for FakeDataOffer { + fn finish(&self) { + self.finished.set(true); + } + + fn destroy(&self) { + self.destroyed.set(true); + } + } + + fn drag_state() -> DragState { + DragState { + data_offer: None, + window: None, + position: Point::default(), + uri_read_generation: 0, + } + } + + fn complete_drag_uri_read( + drag: &mut DragState, + generation: u64, + data_offer: FakeDataOffer, + window: &'static str, + ) -> Option<(&'static str, PlatformInput)> { + drag.complete_uri_read( + generation, + data_offer, + window, + Point::default(), + gpui::ExternalPaths(SmallVec::from_vec(vec![PathBuf::from("/tmp/file")])), + ) + } + + fn record_entered( + input: Option<(&'static str, PlatformInput)>, + entered_windows: &mut Vec<&'static str>, + ) { + if let Some((window, PlatformInput::FileDrop(FileDropEvent::Entered { .. }))) = input { + entered_windows.push(window); + } + } + + #[test] + fn leave_discards_pending_drag_uri_read() { + let mut drag = drag_state(); + let data_offer = FakeDataOffer::new("A"); + let generation = drag.begin_uri_read(); + let mut entered_windows = Vec::new(); + + record_entered(drag.handle_leave(), &mut entered_windows); + record_entered( + complete_drag_uri_read(&mut drag, generation, data_offer.clone(), "window A"), + &mut entered_windows, + ); + + assert!(entered_windows.is_empty()); + assert!(data_offer.destroyed.get()); + assert!(drag.data_offer.is_none()); + assert!(drag.window.is_none()); + } + + #[test] + fn drop_then_leave_discards_pending_drag_uri_read() { + let mut drag = drag_state(); + let data_offer = FakeDataOffer::new("A"); + let generation = drag.begin_uri_read(); + let mut entered_windows = Vec::new(); + + record_entered(drag.handle_drop(), &mut entered_windows); + assert_ne!(drag.uri_read_generation, generation); + record_entered(drag.handle_leave(), &mut entered_windows); + record_entered( + complete_drag_uri_read(&mut drag, generation, data_offer.clone(), "window A"), + &mut entered_windows, + ); + + assert!(entered_windows.is_empty()); + assert!(!data_offer.finished.get()); + assert!(data_offer.destroyed.get()); + assert!(drag.data_offer.is_none()); + assert!(drag.window.is_none()); + } + + #[test] + fn stale_completion_preserves_newer_drag_destination() { + let mut drag = drag_state(); + let data_offer_a = FakeDataOffer::new("A"); + let generation_a = drag.begin_uri_read(); + let mut entered_windows = Vec::new(); + record_entered(drag.handle_leave(), &mut entered_windows); + + let data_offer_b = FakeDataOffer::new("B"); + let generation_b = drag.begin_uri_read(); + record_entered( + complete_drag_uri_read(&mut drag, generation_b, data_offer_b.clone(), "window B"), + &mut entered_windows, + ); + record_entered( + complete_drag_uri_read(&mut drag, generation_a, data_offer_a.clone(), "window A"), + &mut entered_windows, + ); + + assert_eq!(entered_windows, ["window B"]); + assert!(data_offer_a.destroyed.get()); + assert!(!data_offer_b.destroyed.get()); + assert_eq!(drag.data_offer.as_ref().map(|offer| offer.name), Some("B")); + assert_eq!(drag.window, Some("window B")); + } + + #[derive(Default)] + struct FakeImeCursorRectangleSink { + cursor_rectangles: RefCell>, + commit_count: Cell, + } + + impl ImeCursorRectangleSink for FakeImeCursorRectangleSink { + fn set_ime_cursor_rectangle(&self, x: i32, y: i32, width: i32, height: i32) { + self.cursor_rectangles + .borrow_mut() + .push((x, y, width, height)); + } + + fn commit_ime_state(&self) { + self.commit_count.set(self.commit_count.get() + 1); + } + } + + #[test] + fn builds_terminated_uri_list_for_each_dragged_path() { + let paths = FileDragPaths::new([ + (PathBuf::from("/tmp/first"), false), + (PathBuf::from("/tmp/nested directory"), true), + ]); + + assert_eq!( + file_uri_list(&paths), + "file:///tmp/first\r\nfile:///tmp/nested%20directory\r\n" + ); + } + + #[test] + fn skips_paths_that_have_no_file_url() { + let paths = FileDragPaths::new([ + (PathBuf::from("relative/path"), false), + (PathBuf::from("/tmp/absolute"), false), + ]); + + assert_eq!(file_uri_list(&paths), "file:///tmp/absolute\r\n"); + } + + #[test] + fn builds_empty_uri_list_without_convertible_paths() { + assert!(file_uri_list(&FileDragPaths::default()).is_empty()); + assert!( + file_uri_list(&FileDragPaths::new([(PathBuf::from("relative"), false)])).is_empty() + ); + } + + fn ime_cursor_bounds(x: f32) -> Bounds { + Bounds::new(point(px(x), px(20.25)), size(px(1.0), px(18.75))) + } + + #[test] + fn caches_cursor_rectangle_committed_after_done() { + let text_input = FakeImeCursorRectangleSink::default(); + let mut last_ime_cursor_rectangle = None; + let initial_bounds = ime_cursor_bounds(10.0); + let updated_bounds = ime_cursor_bounds(20.0); + + update_ime_cursor_rectangle(&text_input, &mut last_ime_cursor_rectangle, initial_bounds); + set_ime_cursor_rectangle_after_done( + &text_input, + &mut last_ime_cursor_rectangle, + updated_bounds, + true, + ); + update_ime_cursor_rectangle(&text_input, &mut last_ime_cursor_rectangle, updated_bounds); + + assert_eq!(text_input.commit_count.get(), 2); + assert_eq!(text_input.cursor_rectangles.borrow().len(), 2); + } + + #[test] + fn skips_unchanged_cursor_rectangle_after_done() { + let text_input = FakeImeCursorRectangleSink::default(); + let mut last_ime_cursor_rectangle = None; + let bounds = ime_cursor_bounds(10.0); + + update_ime_cursor_rectangle(&text_input, &mut last_ime_cursor_rectangle, bounds); + set_ime_cursor_rectangle_after_done( + &text_input, + &mut last_ime_cursor_rectangle, + bounds, + true, + ); + + assert_eq!(text_input.commit_count.get(), 1); + assert_eq!(text_input.cursor_rectangles.borrow().len(), 1); + } + + #[test] + fn skips_cursor_rectangles_with_unchanged_protocol_coordinates() { + let text_input = FakeImeCursorRectangleSink::default(); + let mut last_ime_cursor_rectangle = None; + + update_ime_cursor_rectangle( + &text_input, + &mut last_ime_cursor_rectangle, + ime_cursor_bounds(10.25), + ); + update_ime_cursor_rectangle( + &text_input, + &mut last_ime_cursor_rectangle, + ime_cursor_bounds(10.75), + ); + + assert_eq!(text_input.commit_count.get(), 1); + assert_eq!( + text_input.cursor_rectangles.borrow().as_slice(), + &[(10, 20, 1, 18)] + ); + } +} diff --git a/vendor/gpui-pre-linux/src/linux/wayland/clipboard.rs b/vendor/gpui-pre-linux/src/linux/wayland/clipboard.rs new file mode 100644 index 000000000..57ecd9345 --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/wayland/clipboard.rs @@ -0,0 +1,263 @@ +use std::{ + fs::File, + io::{ErrorKind, Write}, + os::fd::{AsRawFd, BorrowedFd, OwnedFd}, +}; + +use calloop::{LoopHandle, PostAction}; +use filedescriptor::Pipe; +use strum::IntoEnumIterator; +use wayland_client::{Connection, protocol::wl_data_offer::WlDataOffer}; +use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1; + +use crate::linux::{ + WaylandClientStatePtr, + platform::{PIPE_READ_TIMEOUT, read_fd_with_timeout}, +}; +use gpui::{ClipboardEntry, ClipboardItem, Image, ImageFormat, hash}; + +/// Text mime types that we'll offer to other programs. +pub(crate) const TEXT_MIME_TYPES: [&str; 3] = + ["text/plain;charset=utf-8", "UTF8_STRING", "text/plain"]; +pub(crate) const FILE_LIST_MIME_TYPE: &str = "text/uri-list"; + +/// Text mime types that we'll accept from other programs. +pub(crate) const ALLOWED_TEXT_MIME_TYPES: [&str; 2] = ["text/plain;charset=utf-8", "UTF8_STRING"]; + +pub(crate) struct Clipboard { + connection: Connection, + loop_handle: LoopHandle<'static, WaylandClientStatePtr>, + self_mime: String, + + // Internal clipboard + contents: Option, + primary_contents: Option, + + // External clipboard + cached_read: Option, + current_offer: Option>, + cached_primary_read: Option, + current_primary_offer: Option>, +} + +pub(crate) trait ReceiveData { + fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>); +} + +impl ReceiveData for WlDataOffer { + fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) { + self.receive(mime_type, fd); + } +} + +impl ReceiveData for ZwpPrimarySelectionOfferV1 { + fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) { + self.receive(mime_type, fd); + } +} + +#[derive(Clone, Debug)] +/// Wrapper for `WlDataOffer` and `ZwpPrimarySelectionOfferV1`, used to help track mime types. +pub(crate) struct DataOffer { + pub inner: T, + mime_types: Vec, +} + +impl DataOffer { + pub fn new(offer: T) -> Self { + Self { + inner: offer, + mime_types: Vec::new(), + } + } + + pub fn add_mime_type(&mut self, mime_type: String) { + self.mime_types.push(mime_type) + } + + fn has_mime_type(&self, mime_type: &str) -> bool { + self.mime_types.iter().any(|t| t == mime_type) + } + + fn read_bytes(&self, connection: &Connection, mime_type: &str) -> Option> { + let pipe = Pipe::new().unwrap(); + self.inner.receive_data(mime_type.to_string(), unsafe { + BorrowedFd::borrow_raw(pipe.write.as_raw_fd()) + }); + let fd = pipe.read; + drop(pipe.write); + + connection.flush().unwrap(); + + match read_fd_with_timeout(fd, PIPE_READ_TIMEOUT) { + Ok(bytes) => Some(bytes), + Err(err) => { + log::error!("error reading clipboard pipe: {err:?}"); + None + } + } + } + + fn read_text(&self, connection: &Connection) -> Option { + let mime_type = self.mime_types.iter().find(|&mime_type| { + ALLOWED_TEXT_MIME_TYPES + .iter() + .any(|&allowed| allowed == mime_type) + })?; + let bytes = self.read_bytes(connection, mime_type)?; + let text_content = match String::from_utf8(bytes) { + Ok(content) => content, + Err(e) => { + log::error!("Failed to convert clipboard content to UTF-8: {}", e); + return None; + } + }; + + // Normalize the text to unix line endings, otherwise + // copying from eg: firefox inserts a lot of blank + // lines, and that is super annoying. + let result = text_content.replace("\r\n", "\n"); + Some(ClipboardItem::new_string(result)) + } + + fn read_image(&self, connection: &Connection) -> Option { + for format in ImageFormat::iter() { + let mime_type = format.mime_type(); + if !self.has_mime_type(mime_type) { + continue; + } + + if let Some(bytes) = self.read_bytes(connection, mime_type) { + let id = hash(&bytes); + return Some(ClipboardItem { + entries: vec![ClipboardEntry::Image(Image { format, bytes, id })], + }); + } + } + None + } +} + +impl Clipboard { + pub fn new( + connection: Connection, + loop_handle: LoopHandle<'static, WaylandClientStatePtr>, + ) -> Self { + Self { + connection, + loop_handle, + self_mime: format!("pid/{}", std::process::id()), + + contents: None, + primary_contents: None, + + cached_read: None, + current_offer: None, + cached_primary_read: None, + current_primary_offer: None, + } + } + + pub fn set(&mut self, item: ClipboardItem) { + self.contents = Some(item); + } + + pub fn set_primary(&mut self, item: ClipboardItem) { + self.primary_contents = Some(item); + } + + pub fn set_offer(&mut self, data_offer: Option>) { + self.cached_read = None; + self.current_offer = data_offer; + } + + pub fn set_primary_offer(&mut self, data_offer: Option>) { + self.cached_primary_read = None; + self.current_primary_offer = data_offer; + } + + pub fn self_mime(&self) -> String { + self.self_mime.clone() + } + + pub fn send(&self, _mime_type: String, fd: OwnedFd) { + if let Some(text) = self.contents.as_ref().and_then(|contents| contents.text()) { + self.send_bytes(fd, text.as_bytes().to_owned()); + } + } + + pub fn send_primary(&self, _mime_type: String, fd: OwnedFd) { + if let Some(text) = self + .primary_contents + .as_ref() + .and_then(|contents| contents.text()) + { + self.send_bytes(fd, text.as_bytes().to_owned()); + } + } + + pub fn read(&mut self) -> Option { + let offer = self.current_offer.as_ref()?; + if let Some(cached) = self.cached_read.clone() { + return Some(cached); + } + + if offer.has_mime_type(&self.self_mime) { + return self.contents.clone(); + } + + let item = offer + .read_text(&self.connection) + .or_else(|| offer.read_image(&self.connection))?; + + self.cached_read = Some(item.clone()); + Some(item) + } + + pub fn read_primary(&mut self) -> Option { + let offer = self.current_primary_offer.as_ref()?; + if let Some(cached) = self.cached_primary_read.clone() { + return Some(cached); + } + + if offer.has_mime_type(&self.self_mime) { + return self.primary_contents.clone(); + } + + let item = offer + .read_text(&self.connection) + .or_else(|| offer.read_image(&self.connection))?; + + self.cached_primary_read = Some(item.clone()); + Some(item) + } + + pub fn send_bytes(&self, fd: OwnedFd, bytes: Vec) { + let mut written = 0; + self.loop_handle + .insert_source( + calloop::generic::Generic::new( + File::from(fd), + calloop::Interest::WRITE, + calloop::Mode::Level, + ), + move |_, file, _| { + let file = unsafe { file.get_mut() }; + loop { + match file.write(&bytes[written..]) { + Ok(n) if written + n == bytes.len() => { + written += n; + break Ok(PostAction::Remove); + } + Ok(n) => written += n, + Err(err) if err.kind() == ErrorKind::WouldBlock => { + break Ok(PostAction::Continue); + } + Err(_) => break Ok(PostAction::Remove), + } + } + }, + ) + .unwrap(); + } +} diff --git a/vendor/gpui-pre-linux/src/linux/wayland/cursor.rs b/vendor/gpui-pre-linux/src/linux/wayland/cursor.rs new file mode 100644 index 000000000..2d66fed95 --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/wayland/cursor.rs @@ -0,0 +1,152 @@ +use crate::linux::Globals; +use crate::linux::{DEFAULT_CURSOR_ICON_NAME, log_cursor_icon_warning}; +use anyhow::{Context as _, anyhow}; +use gpui_util::ResultExt; + +use wayland_client::Connection; +use wayland_client::protocol::wl_surface::WlSurface; +use wayland_client::protocol::{wl_pointer::WlPointer, wl_shm::WlShm}; +use wayland_cursor::{CursorImageBuffer, CursorTheme}; + +pub(crate) struct Cursor { + loaded_theme: Option, + size: u32, + scaled_size: u32, + surface: WlSurface, + shm: WlShm, + connection: Connection, +} + +pub(crate) struct LoadedTheme { + theme: CursorTheme, + name: Option, + scaled_size: u32, +} + +impl Drop for Cursor { + fn drop(&mut self) { + self.loaded_theme.take(); + self.surface.destroy(); + } +} + +impl Cursor { + pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self { + let mut this = Self { + loaded_theme: None, + size, + scaled_size: size, + surface: globals.compositor.create_surface(&globals.qh, ()), + shm: globals.shm.clone(), + connection: connection.clone(), + }; + this.set_theme_internal(None); + this + } + + fn set_theme_internal(&mut self, theme_name: Option) { + if let Some(loaded_theme) = self.loaded_theme.as_ref() + && loaded_theme.name == theme_name + && loaded_theme.scaled_size == self.scaled_size + { + return; + } + let result = if let Some(theme_name) = theme_name.as_ref() { + CursorTheme::load_from_name( + &self.connection, + self.shm.clone(), + theme_name, + self.scaled_size, + ) + } else { + CursorTheme::load(&self.connection, self.shm.clone(), self.scaled_size) + }; + if let Some(theme) = result + .context("Wayland: Failed to load cursor theme") + .log_err() + { + self.loaded_theme = Some(LoadedTheme { + theme, + name: theme_name, + scaled_size: self.scaled_size, + }); + } + } + + pub fn set_theme(&mut self, theme_name: String) { + self.set_theme_internal(Some(theme_name)); + } + + fn set_scaled_size(&mut self, scaled_size: u32) { + self.scaled_size = scaled_size; + let theme_name = self + .loaded_theme + .as_ref() + .and_then(|loaded_theme| loaded_theme.name.clone()); + self.set_theme_internal(theme_name); + } + + pub fn set_size(&mut self, size: u32) { + self.size = size; + self.set_scaled_size(size); + } + + pub fn set_icon( + &mut self, + wl_pointer: &WlPointer, + serial_id: u32, + cursor_icon_names: &[&str], + scale: i32, + ) { + self.set_scaled_size(self.size * scale as u32); + + let Some(loaded_theme) = &mut self.loaded_theme else { + log::warn!("Wayland: Unable to load cursor themes"); + return; + }; + let theme = &mut loaded_theme.theme; + + let buffer: &CursorImageBuffer; + 'outer: { + for cursor_icon_name in cursor_icon_names { + if let Some(cursor) = theme.get_cursor(cursor_icon_name) { + buffer = &cursor[0]; + break 'outer; + } + } + + if let Some(cursor) = theme.get_cursor(DEFAULT_CURSOR_ICON_NAME) { + buffer = &cursor[0]; + log_cursor_icon_warning(anyhow!( + "wayland: Unable to get cursor icon {:?}. \ + Using default cursor icon: '{}'", + cursor_icon_names, + DEFAULT_CURSOR_ICON_NAME + )); + } else { + log_cursor_icon_warning(anyhow!( + "wayland: Unable to fallback on default cursor icon '{}' for theme '{}'", + DEFAULT_CURSOR_ICON_NAME, + loaded_theme.name.as_deref().unwrap_or("default") + )); + return; + } + } + + let (width, height) = buffer.dimensions(); + let (hot_x, hot_y) = buffer.hotspot(); + + self.surface.set_buffer_scale(scale); + + wl_pointer.set_cursor( + serial_id, + Some(&self.surface), + hot_x as i32 / scale, + hot_y as i32 / scale, + ); + + self.surface.attach(Some(buffer), 0, 0); + self.surface.damage(0, 0, width as i32, height as i32); + self.surface.commit(); + } +} diff --git a/vendor/gpui-pre-linux/src/linux/wayland/display.rs b/vendor/gpui-pre-linux/src/linux/wayland/display.rs new file mode 100644 index 000000000..8fa9122d6 --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/wayland/display.rs @@ -0,0 +1,42 @@ +use std::{ + fmt::Debug, + hash::{Hash, Hasher}, +}; + +use anyhow::Context as _; +use uuid::Uuid; +use wayland_backend::client::ObjectId; + +use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay}; + +#[derive(Debug, Clone)] +pub(crate) struct WaylandDisplay { + /// The ID of the wl_output object + pub id: ObjectId, + pub name: Option, + pub bounds: Bounds, +} + +impl Hash for WaylandDisplay { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl PlatformDisplay for WaylandDisplay { + fn id(&self) -> DisplayId { + DisplayId::new(self.id.protocol_id() as u64) + } + + fn uuid(&self) -> anyhow::Result { + let name = self + .name + .as_ref() + .context("Wayland display does not have a name")?; + Ok(Uuid::new_v5(&Uuid::NAMESPACE_DNS, name.as_bytes())) + } + + fn bounds(&self) -> Bounds { + self.bounds + } +} diff --git a/vendor/gpui-pre-linux/src/linux/wayland/layer_shell.rs b/vendor/gpui-pre-linux/src/linux/wayland/layer_shell.rs new file mode 100644 index 000000000..a40055206 --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/wayland/layer_shell.rs @@ -0,0 +1,26 @@ +pub use gpui::layer_shell::*; + +use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1}; + +pub(crate) fn wayland_layer(layer: Layer) -> zwlr_layer_shell_v1::Layer { + match layer { + Layer::Background => zwlr_layer_shell_v1::Layer::Background, + Layer::Bottom => zwlr_layer_shell_v1::Layer::Bottom, + Layer::Top => zwlr_layer_shell_v1::Layer::Top, + Layer::Overlay => zwlr_layer_shell_v1::Layer::Overlay, + } +} + +pub(crate) fn wayland_anchor(anchor: Anchor) -> zwlr_layer_surface_v1::Anchor { + zwlr_layer_surface_v1::Anchor::from_bits_truncate(anchor.bits()) +} + +pub(crate) fn wayland_keyboard_interactivity( + value: KeyboardInteractivity, +) -> zwlr_layer_surface_v1::KeyboardInteractivity { + match value { + KeyboardInteractivity::None => zwlr_layer_surface_v1::KeyboardInteractivity::None, + KeyboardInteractivity::Exclusive => zwlr_layer_surface_v1::KeyboardInteractivity::Exclusive, + KeyboardInteractivity::OnDemand => zwlr_layer_surface_v1::KeyboardInteractivity::OnDemand, + } +} diff --git a/vendor/gpui-pre-linux/src/linux/wayland/popup.rs b/vendor/gpui-pre-linux/src/linux/wayland/popup.rs new file mode 100644 index 000000000..4a15e7821 --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/wayland/popup.rs @@ -0,0 +1,38 @@ +pub use gpui::popup::*; + +use wayland_protocols::xdg::shell::client::xdg_positioner; + +pub(crate) fn wayland_anchor(anchor: PopupAnchor) -> xdg_positioner::Anchor { + match anchor { + PopupAnchor::Center => xdg_positioner::Anchor::None, + PopupAnchor::Top => xdg_positioner::Anchor::Top, + PopupAnchor::Bottom => xdg_positioner::Anchor::Bottom, + PopupAnchor::Left => xdg_positioner::Anchor::Left, + PopupAnchor::Right => xdg_positioner::Anchor::Right, + PopupAnchor::TopLeft => xdg_positioner::Anchor::TopLeft, + PopupAnchor::BottomLeft => xdg_positioner::Anchor::BottomLeft, + PopupAnchor::TopRight => xdg_positioner::Anchor::TopRight, + PopupAnchor::BottomRight => xdg_positioner::Anchor::BottomRight, + } +} + +pub(crate) fn wayland_gravity(gravity: PopupGravity) -> xdg_positioner::Gravity { + match gravity { + PopupGravity::Center => xdg_positioner::Gravity::None, + PopupGravity::Top => xdg_positioner::Gravity::Top, + PopupGravity::Bottom => xdg_positioner::Gravity::Bottom, + PopupGravity::Left => xdg_positioner::Gravity::Left, + PopupGravity::Right => xdg_positioner::Gravity::Right, + PopupGravity::TopLeft => xdg_positioner::Gravity::TopLeft, + PopupGravity::BottomLeft => xdg_positioner::Gravity::BottomLeft, + PopupGravity::TopRight => xdg_positioner::Gravity::TopRight, + PopupGravity::BottomRight => xdg_positioner::Gravity::BottomRight, + } +} + +pub(crate) fn wayland_constraint_adjustment( + adjustment: PopupConstraintAdjustment, +) -> xdg_positioner::ConstraintAdjustment { + // The flag values match the protocol bitfield, so the bits map across directly. + xdg_positioner::ConstraintAdjustment::from_bits_truncate(adjustment.bits()) +} diff --git a/vendor/gpui-pre-linux/src/linux/wayland/serial.rs b/vendor/gpui-pre-linux/src/linux/wayland/serial.rs new file mode 100644 index 000000000..9f5372a8a --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/wayland/serial.rs @@ -0,0 +1,126 @@ +use collections::HashMap; + +#[derive(Debug, Hash, PartialEq, Eq)] +pub(crate) enum SerialKind { + DataDevice, + InputMethod, + MouseEnter, + MousePress, + KeyPress, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct Serial(u32); + +impl Serial { + pub(super) fn as_raw(self) -> u32 { + self.0 + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct SelectionSerial(Serial); + +impl SelectionSerial { + pub(super) fn as_raw(self) -> u32 { + self.0.as_raw() + } +} + +#[derive(Debug)] +/// Helper for tracking of different serial kinds. +pub(crate) struct SerialTracker { + serials: HashMap, + selection_serial: Option, +} + +impl SerialTracker { + pub fn new() -> Self { + Self { + serials: HashMap::default(), + selection_serial: None, + } + } + + pub fn update(&mut self, kind: SerialKind, value: u32) { + let serial = Serial(value); + + if matches!(&kind, SerialKind::KeyPress | SerialKind::MousePress) { + self.selection_serial = Some(SelectionSerial(serial)); + } + + self.serials.insert(kind, serial); + } + + /// Returns the latest tracked serial of the provided [`SerialKind`]. + /// + /// Returns a serial with a raw value of 0 if the kind has not been tracked. + pub fn get(&self, kind: SerialKind) -> Serial { + self.serials.get(&kind).copied().unwrap_or(Serial(0)) + } + + pub fn selection_serial(&self) -> Option { + self.selection_serial + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn raw_selection_serial(serial_tracker: &SerialTracker) -> Option { + serial_tracker + .selection_serial() + .map(SelectionSerial::as_raw) + } + + #[test] + fn test_selection_serial_ignores_unrelated_serial_kinds() { + let mut serial_tracker = SerialTracker::new(); + serial_tracker.update(SerialKind::MousePress, 3783); + serial_tracker.update(SerialKind::InputMethod, 5011); + serial_tracker.update(SerialKind::KeyPress, 3787); + serial_tracker.update(SerialKind::MouseEnter, 6000); + serial_tracker.update(SerialKind::DataDevice, 7000); + + assert_eq!(raw_selection_serial(&serial_tracker), Some(3787)); + } + + #[test] + fn test_selection_serial_uses_mouse_press_without_key_press() { + let mut serial_tracker = SerialTracker::new(); + serial_tracker.update(SerialKind::MousePress, 3783); + + assert_eq!(raw_selection_serial(&serial_tracker), Some(3783)); + } + + #[test] + fn test_selection_serial_uses_event_arrival_order_across_rollover() { + let mut serial_tracker = SerialTracker::new(); + serial_tracker.update(SerialKind::KeyPress, 0xffff_fff0); + serial_tracker.update(SerialKind::MousePress, 0x0000_0010); + + assert_eq!(raw_selection_serial(&serial_tracker), Some(0x0000_0010)); + } + + #[test] + fn test_selection_serial_is_unavailable_without_eligible_input() { + let mut serial_tracker = SerialTracker::new(); + + assert_eq!(raw_selection_serial(&serial_tracker), None); + + serial_tracker.update(SerialKind::InputMethod, 5011); + serial_tracker.update(SerialKind::MouseEnter, 6000); + serial_tracker.update(SerialKind::DataDevice, 7000); + + assert_eq!(raw_selection_serial(&serial_tracker), None); + } + + #[test] + fn test_zero_is_a_valid_selection_serial() { + let mut serial_tracker = SerialTracker::new(); + serial_tracker.update(SerialKind::KeyPress, 0); + + assert_eq!(raw_selection_serial(&serial_tracker), Some(0)); + } +} diff --git a/vendor/gpui-pre-linux/src/linux/wayland/window.rs b/vendor/gpui-pre-linux/src/linux/wayland/window.rs new file mode 100644 index 000000000..9ff93a967 --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/wayland/window.rs @@ -0,0 +1,2357 @@ +use std::{ + cell::{Cell, Ref, RefCell, RefMut}, + ffi::c_void, + ptr::NonNull, + rc::Rc, + sync::Arc, +}; + +use calloop::ping::Ping; +use collections::{FxHashMap, HashMap}; +use futures::channel::oneshot::Receiver; + +use raw_window_handle as rwh; +use wayland_backend::client::ObjectId; +use wayland_client::WEnum; +use wayland_client::{ + Proxy, + protocol::{wl_callback, wl_output, wl_seat, wl_surface}, +}; +use wayland_protocols::wp::viewporter::client::wp_viewport; +use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1; +use wayland_protocols::xdg::shell::client::xdg_popup; +use wayland_protocols::xdg::shell::client::xdg_positioner; +use wayland_protocols::xdg::shell::client::xdg_surface; +use wayland_protocols::xdg::shell::client::xdg_toplevel::{self}; +use wayland_protocols::{ + wp::fractional_scale::v1::client::wp_fractional_scale_v1, + xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1, +}; +use wayland_protocols::ext::background_effect::v1::client::ext_background_effect_surface_v1; +use wayland_protocols_plasma::blur::client::org_kde_kwin_blur; +use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1; + +use crate::linux::wayland::{display::WaylandDisplay, serial::SerialKind}; +use crate::linux::{Globals, Output, WaylandClientStatePtr, get_window}; +use gpui::{ + AnyWindowHandle, Bounds, Capslock, Decorations, DevicePixels, ExternalDragPayload, GpuSpecs, + Modifiers, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, + PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, ResizeEdge, Scene, Size, + Tiling, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, + WindowControls, WindowDecorations, WindowKind, WindowParams, WindowVisibility, + layer_shell::{Anchor, LayerShellNotSupportedError}, + popup::PopupOptions, + px, size, +}; +use gpui_wgpu::{CompositorGpuHint, WgpuRenderer, WgpuSurfaceConfig, wgpu}; + +#[derive(Default)] +pub(crate) struct Callbacks { + request_frame: Option>, + input: Option gpui::DispatchEventResult>>, + active_status_change: Option>, + visibility_change: Option>, + hover_status_change: Option>, + resize: Option, f32)>>, + moved: Option>, + should_close: Option bool>>, + close: Option>, + appearance_changed: Option>, + button_layout_changed: Option>, +} + +#[derive(Debug, Clone, Copy)] +struct RawWindow { + window: *mut c_void, + display: *mut c_void, +} + +// Safety: The raw pointers in RawWindow point to Wayland surface/display +// which are valid for the window's lifetime. These are used only for +// passing to wgpu which needs Send+Sync for surface creation. +unsafe impl Send for RawWindow {} +unsafe impl Sync for RawWindow {} + +impl rwh::HasWindowHandle for RawWindow { + fn window_handle(&self) -> Result, rwh::HandleError> { + let window = NonNull::new(self.window).unwrap(); + let handle = rwh::WaylandWindowHandle::new(window); + Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) + } +} +impl rwh::HasDisplayHandle for RawWindow { + fn display_handle(&self) -> Result, rwh::HandleError> { + let display = NonNull::new(self.display).unwrap(); + let handle = rwh::WaylandDisplayHandle::new(display); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) + } +} + +#[derive(Debug)] +struct InProgressConfigure { + size: Option>, + fullscreen: bool, + maximized: bool, + resizing: bool, + visibility: WindowVisibility, + tiling: Tiling, +} + +pub struct WaylandWindowState { + surface_state: WaylandSurfaceState, + parent: Option, + /// Child surfaces mapped to whether they block this window's input (dialogs + /// block, popups don't). Children are closed before this window closes. + children: FxHashMap, + pub surface: wl_surface::WlSurface, + app_id: Option, + appearance: WindowAppearance, + blur: Option, + background_effect: Option, + viewport: Option, + outputs: HashMap, + display: Option<(ObjectId, Output)>, + globals: Globals, + renderer: WgpuRenderer, + bounds: Bounds, + scale: f32, + input_handler: Option, + decorations: WindowDecorations, + background_appearance: WindowBackgroundAppearance, + fullscreen: bool, + maximized: bool, + /// `Hidden` while the `xdg_toplevel` `suspended` state (xdg-shell v6) is set. + visibility: WindowVisibility, + tiling: Tiling, + window_bounds: Bounds, + client: WaylandClientStatePtr, + handle: AnyWindowHandle, + active: bool, + hovered: bool, + redraw_requested: bool, + presentation: PresentationState, + pending_frame_callback: Option, + in_progress_configure: Option, + resize_throttle: bool, + in_progress_window_controls: Option, + window_controls: WindowControls, + client_inset: Option, + accesskit_adapter: Option, +} + +pub enum WaylandSurfaceState { + Xdg(WaylandXdgSurfaceState), + LayerShell(WaylandLayerSurfaceState), + Popup(WaylandPopupSurfaceState), +} + +impl WaylandSurfaceState { + fn new( + surface: &wl_surface::WlSurface, + globals: &Globals, + params: &WindowParams, + parent: Option, + popup_grab: Option<(u32, wl_seat::WlSeat)>, + target_output: Option, + ) -> anyhow::Result { + // For layer_shell windows, create a layer surface instead of an xdg surface + if let WindowKind::LayerShell(options) = ¶ms.kind { + let Some(layer_shell) = globals.layer_shell.as_ref() else { + return Err(LayerShellNotSupportedError.into()); + }; + + let layer_surface = layer_shell.get_layer_surface( + &surface, + target_output.as_ref(), + super::layer_shell::wayland_layer(options.layer), + options.namespace.clone(), + &globals.qh, + surface.id(), + ); + + let width = f32::from(params.bounds.size.width); + let height = f32::from(params.bounds.size.height); + layer_surface.set_size(width as u32, height as u32); + + layer_surface.set_anchor(super::layer_shell::wayland_anchor(options.anchor)); + layer_surface.set_keyboard_interactivity( + super::layer_shell::wayland_keyboard_interactivity(options.keyboard_interactivity), + ); + + if let Some(margin) = options.margin { + layer_surface.set_margin( + f32::from(margin.0) as i32, + f32::from(margin.1) as i32, + f32::from(margin.2) as i32, + f32::from(margin.3) as i32, + ) + } + + if let Some(exclusive_zone) = options.exclusive_zone { + layer_surface.set_exclusive_zone(f32::from(exclusive_zone) as i32); + } + + if let Some(exclusive_edge) = options.exclusive_edge { + Self::apply_exclusive_edge(&layer_surface, options.anchor, exclusive_edge); + } + + return Ok(WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { + layer_surface, + anchor: options.anchor, + })); + } + + if let WindowKind::AnchoredPopup(options) = ¶ms.kind { + let Some(parent) = parent.as_ref() else { + return Err(anyhow::anyhow!("popup parent window not found")); + }; + + let positioner = build_popup_positioner( + globals, + options, + params.bounds.size, + parent.window_geometry(), + ); + + let xdg_surface = globals + .wm_base + .get_xdg_surface(&surface, &globals.qh, surface.id()); + + // A layer-shell parent takes a null xdg parent and is attached via the layer + // surface. Every other surface kind has an xdg_surface to parent to directly. + let xdg_popup = if let Some(parent_layer_surface) = parent.layer_surface() { + let xdg_popup = xdg_surface.get_popup(None, &positioner, &globals.qh, surface.id()); + parent_layer_surface.get_popup(&xdg_popup); + xdg_popup + } else { + xdg_surface.get_popup( + parent.xdg_surface().as_ref(), + &positioner, + &globals.qh, + surface.id(), + ) + }; + positioner.destroy(); + + if let Some((serial, seat)) = popup_grab { + xdg_popup.grab(&seat, serial); + } + + // Non-blocking: the parent keeps its input so it can dismiss the popup on + // clicks in its own window. + parent.add_child(surface.id(), false); + + return Ok(WaylandSurfaceState::Popup(WaylandPopupSurfaceState { + xdg_surface, + xdg_popup, + options: options.clone(), + next_reposition_token: Cell::new(0), + })); + } + + // All other WindowKinds result in a regular xdg surface + let xdg_surface = globals + .wm_base + .get_xdg_surface(&surface, &globals.qh, surface.id()); + + let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id()); + let xdg_parent = parent.as_ref().and_then(|w| w.toplevel()); + + if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog { + toplevel.set_parent(xdg_parent.as_ref()); + } + + let dialog = if params.kind == WindowKind::Dialog { + let dialog = globals.dialog.as_ref().map(|dialog| { + let xdg_dialog = dialog.get_xdg_dialog(&toplevel, &globals.qh, ()); + xdg_dialog.set_modal(); + xdg_dialog + }); + + if let Some(parent) = parent.as_ref() { + parent.add_child(surface.id(), true); + } + + dialog + } else { + None + }; + + if let Some(size) = params.window_min_size { + toplevel.set_min_size(f32::from(size.width) as i32, f32::from(size.height) as i32); + } + + // Attempt to set up window decorations based on the requested configuration + let decoration = globals + .decoration_manager + .as_ref() + .map(|decoration_manager| { + decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id()) + }); + + Ok(WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { + xdg_surface, + toplevel, + decoration, + dialog, + })) + } +} + +pub struct WaylandXdgSurfaceState { + xdg_surface: xdg_surface::XdgSurface, + toplevel: xdg_toplevel::XdgToplevel, + decoration: Option, + dialog: Option, +} + +pub struct WaylandLayerSurfaceState { + layer_surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, + anchor: Anchor, +} + +pub struct WaylandPopupSurfaceState { + xdg_surface: xdg_surface::XdgSurface, + xdg_popup: xdg_popup::XdgPopup, + // Kept so the popup can be re-anchored via `xdg_popup.reposition` when resized. + options: PopupOptions, + next_reposition_token: Cell, +} + +fn build_popup_positioner( + globals: &Globals, + options: &PopupOptions, + size: Size, + parent_geometry: Bounds, +) -> xdg_positioner::XdgPositioner { + let positioner = globals.wm_base.create_positioner(&globals.qh, ()); + // A zero or negative size is a protocol error. + positioner.set_size( + f32::from(size.width).max(1.0) as i32, + f32::from(size.height).max(1.0) as i32, + ); + + // The protocol wants the anchor rect relative to the parent's window geometry, while + // `options.anchor_rect` is in gpui window coordinates (surface-local). A rect extending + // outside the geometry or with a zero size is a protocol error, so translate, then clamp + // to at least one pixel inside the geometry, pulling the origin inward at the edges. + let anchor_rect = Bounds { + origin: options.anchor_rect.origin - parent_geometry.origin, + size: options.anchor_rect.size, + }; + let one = Point::new(px(1.0), px(1.0)); + let geometry_bottom_right: Point = parent_geometry.size.into(); + let top_left = anchor_rect + .origin + .min(&(geometry_bottom_right - one)) + .max(&Point::default()); + let bottom_right = anchor_rect + .bottom_right() + .min(&geometry_bottom_right) + .max(&(top_left + one)); + let anchor_rect = Bounds::from_corners(top_left, bottom_right); + positioner.set_anchor_rect( + f32::from(anchor_rect.origin.x) as i32, + f32::from(anchor_rect.origin.y) as i32, + f32::from(anchor_rect.size.width) as i32, + f32::from(anchor_rect.size.height) as i32, + ); + + positioner.set_anchor(super::popup::wayland_anchor(options.anchor)); + positioner.set_gravity(super::popup::wayland_gravity(options.gravity)); + positioner.set_constraint_adjustment(super::popup::wayland_constraint_adjustment( + options.constraint_adjustment, + )); + positioner.set_offset( + f32::from(options.offset.x) as i32, + f32::from(options.offset.y) as i32, + ); + positioner +} + +impl WaylandSurfaceState { + fn ack_configure(&self, serial: u32) { + match self { + WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { xdg_surface, .. }) => { + xdg_surface.ack_configure(serial); + } + WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) => { + layer_surface.ack_configure(serial); + } + WaylandSurfaceState::Popup(WaylandPopupSurfaceState { xdg_surface, .. }) => { + xdg_surface.ack_configure(serial); + } + } + } + + fn decoration(&self) -> Option<&zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1> { + if let WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { decoration, .. }) = self { + decoration.as_ref() + } else { + None + } + } + + fn toplevel(&self) -> Option<&xdg_toplevel::XdgToplevel> { + if let WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { toplevel, .. }) = self { + Some(toplevel) + } else { + None + } + } + + fn xdg_surface(&self) -> Option<&xdg_surface::XdgSurface> { + match self { + WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { xdg_surface, .. }) => { + Some(xdg_surface) + } + WaylandSurfaceState::Popup(WaylandPopupSurfaceState { xdg_surface, .. }) => { + Some(xdg_surface) + } + WaylandSurfaceState::LayerShell(_) => None, + } + } + + fn layer_surface(&self) -> Option<&zwlr_layer_surface_v1::ZwlrLayerSurfaceV1> { + if let WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) = + self + { + Some(layer_surface) + } else { + None + } + } + + fn set_geometry(&self, x: i32, y: i32, width: i32, height: i32) { + match self { + WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { xdg_surface, .. }) => { + xdg_surface.set_window_geometry(x, y, width, height); + } + WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) => { + // cannot set window position of a layer surface + layer_surface.set_size(width as u32, height as u32); + } + WaylandSurfaceState::Popup(WaylandPopupSurfaceState { xdg_surface, .. }) => { + xdg_surface.set_window_geometry(x, y, width, height); + } + } + } + + // Re-anchors a mapped popup at a new size via `xdg_popup.reposition`. Repositioning an + // unmapped popup (before the first configure) is a protocol error. + fn reposition_popup( + &self, + globals: &Globals, + size: Size, + parent_geometry: Bounds, + ) { + if let WaylandSurfaceState::Popup(WaylandPopupSurfaceState { + xdg_popup, + options, + next_reposition_token, + .. + }) = self + && xdg_popup.version() >= xdg_popup::REQ_REPOSITION_SINCE + { + let token = next_reposition_token.get(); + next_reposition_token.set(token.wrapping_add(1)); + + let positioner = build_popup_positioner(globals, options, size, parent_geometry); + xdg_popup.reposition(&positioner, token); + positioner.destroy(); + } + } + + fn set_exclusive_zone(&self, zone: i32) -> bool { + if let WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) = + self + { + layer_surface.set_exclusive_zone(zone); + true + } else { + false + } + } + + /// An exclusive edge must be a single edge that the surface is anchored to, + /// otherwise the compositor raises a fatal `invalid_exclusive_edge` protocol + /// error. An invalid edge is logged and ignored. Returns whether it applied. + fn apply_exclusive_edge( + layer_surface: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, + anchor: Anchor, + edge: Anchor, + ) -> bool { + if edge.bits().count_ones() == 1 && anchor.contains(edge) { + layer_surface.set_exclusive_edge(super::layer_shell::wayland_anchor(edge)); + true + } else { + log::warn!( + "ignoring exclusive edge {edge:?}: must be a single edge of the surface anchor {anchor:?}" + ); + false + } + } + + fn set_exclusive_edge(&self, edge: Anchor) -> bool { + if let WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { + layer_surface, + anchor, + .. + }) = self + { + Self::apply_exclusive_edge(layer_surface, *anchor, edge) + } else { + false + } + } + + fn destroy(&mut self) { + match self { + WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { + xdg_surface, + toplevel, + decoration: _decoration, + dialog, + }) => { + // drop the dialog before toplevel so compositor can explicitly unapply it's effects + if let Some(dialog) = dialog { + dialog.destroy(); + } + + // The role object (toplevel) must always be destroyed before the xdg_surface. + // See https://wayland.app/protocols/xdg-shell#xdg_surface:request:destroy + toplevel.destroy(); + xdg_surface.destroy(); + } + WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) => { + layer_surface.destroy(); + } + WaylandSurfaceState::Popup(WaylandPopupSurfaceState { + xdg_surface, + xdg_popup, + .. + }) => { + // Role object before its xdg_surface, as with the toplevel above. + xdg_popup.destroy(); + xdg_surface.destroy(); + } + } + } +} + +#[derive(Clone)] +pub struct WaylandWindowStatePtr { + state: Rc>, + callbacks: Rc>, + frame_loop: Rc>, + frame_ping: Ping, +} + +impl WaylandWindowState { + pub(crate) fn new( + handle: AnyWindowHandle, + surface: wl_surface::WlSurface, + surface_state: WaylandSurfaceState, + appearance: WindowAppearance, + viewport: Option, + client: WaylandClientStatePtr, + globals: Globals, + gpu_context: gpui_wgpu::GpuContext, + compositor_gpu: Option, + options: WindowParams, + parent: Option, + ) -> anyhow::Result { + let renderer = { + let raw_window = RawWindow { + window: surface.id().as_ptr().cast::(), + display: surface + .backend() + .upgrade() + .unwrap() + .display_ptr() + .cast::(), + }; + let config = WgpuSurfaceConfig { + size: Size { + width: DevicePixels(f32::from(options.bounds.size.width) as i32), + height: DevicePixels(f32::from(options.bounds.size.height) as i32), + }, + transparent: true, + // Prefer Mailbox to avoid blocking. Falls back to FIFO if Mailbox is unsupported. + preferred_present_mode: Some(wgpu::PresentMode::Mailbox), + }; + WgpuRenderer::new(gpu_context, &raw_window, config, compositor_gpu)? + }; + + if let WaylandSurfaceState::Xdg(ref xdg_state) = surface_state { + if let Some(title) = options.titlebar.and_then(|titlebar| titlebar.title) { + xdg_state.toplevel.set_title(title.to_string()); + } + + if let Some(app_id) = options.app_id.as_ref() { + xdg_state.toplevel.set_app_id(app_id.clone()); + } + + // Set max window size based on the GPU's maximum texture dimension. + // This prevents the window from being resized larger than what the GPU can render. + let max_texture_size = renderer.max_texture_size() as i32; + xdg_state + .toplevel + .set_max_size(max_texture_size, max_texture_size); + } + + Ok(Self { + surface_state, + parent, + children: FxHashMap::default(), + surface, + app_id: options.app_id, + blur: None, + background_effect: None, + viewport, + globals, + outputs: HashMap::default(), + display: None, + renderer, + bounds: options.bounds, + scale: 1.0, + input_handler: None, + decorations: WindowDecorations::Client, + background_appearance: WindowBackgroundAppearance::Opaque, + fullscreen: false, + maximized: false, + visibility: WindowVisibility::Visible, + tiling: Tiling::default(), + window_bounds: options.bounds, + in_progress_configure: None, + resize_throttle: false, + client, + appearance, + handle, + active: false, + hovered: false, + redraw_requested: false, + presentation: PresentationState::Unpresented, + pending_frame_callback: None, + in_progress_window_controls: None, + window_controls: WindowControls::default(), + client_inset: None, + accesskit_adapter: None, + }) + } + + pub fn is_transparent(&self) -> bool { + self.decorations == WindowDecorations::Client + || self.background_appearance != WindowBackgroundAppearance::Opaque + } + + fn update_subpixel_layout(&mut self) { + use wayland_client::protocol::wl_output::Subpixel; + let is_bgr = self + .display + .as_ref() + .and_then(|(_, output)| output.subpixel) + .is_some_and(|s| s == Subpixel::HorizontalBgr); + self.renderer.set_subpixel_layout(is_bgr); + } + + pub fn primary_output_scale(&mut self) -> i32 { + let mut scale = 1; + let mut current_output = self.display.take(); + for (id, output) in self.outputs.iter() { + if let Some((_, output_data)) = ¤t_output { + if output.scale > output_data.scale { + current_output = Some((id.clone(), output.clone())); + } + } else { + current_output = Some((id.clone(), output.clone())); + } + scale = scale.max(output.scale); + } + self.display = current_output; + scale + } + + pub fn inset(&self) -> Pixels { + match self.decorations { + WindowDecorations::Server => px(0.0), + WindowDecorations::Client => self.client_inset.unwrap_or(px(0.0)), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PresentationState { + Unpresented, + Presented, + RetryBeforeFirstPresent, + RetryAfterPresent, +} + +impl PresentationState { + fn requires_presentation(self) -> bool { + matches!( + self, + Self::RetryBeforeFirstPresent | Self::RetryAfterPresent + ) + } + + fn failed(self) -> Self { + match self { + Self::Unpresented | Self::RetryBeforeFirstPresent => Self::RetryBeforeFirstPresent, + Self::Presented | Self::RetryAfterPresent => Self::RetryAfterPresent, + } + } +} + +#[cfg(test)] +mod presentation_state_tests { + use super::PresentationState; + + #[test] + fn failure_tracks_whether_the_surface_has_presented() { + assert_eq!( + PresentationState::Unpresented.failed(), + PresentationState::RetryBeforeFirstPresent + ); + assert_eq!( + PresentationState::RetryBeforeFirstPresent.failed(), + PresentationState::RetryBeforeFirstPresent + ); + assert_eq!( + PresentationState::Presented.failed(), + PresentationState::RetryAfterPresent + ); + assert_eq!( + PresentationState::RetryAfterPresent.failed(), + PresentationState::RetryAfterPresent + ); + } + + #[test] + fn only_retry_states_require_presentation() { + assert!(!PresentationState::Unpresented.requires_presentation()); + assert!(!PresentationState::Presented.requires_presentation()); + assert!(PresentationState::RetryBeforeFirstPresent.requires_presentation()); + assert!(PresentationState::RetryAfterPresent.requires_presentation()); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FrameLoop { + Unconfigured, + Ticking, + RescheduleRequested, + PresentationFailed, + AwaitingCallback, + Scheduled, + RetryScheduled, + Parked, +} + +pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr); +pub enum ImeInput { + InsertText(String), + SetMarkedText(String), + UnmarkText, + DeleteText, +} + +impl Drop for WaylandWindow { + fn drop(&mut self) { + self.0.frame_loop.set(FrameLoop::Parked); + + let mut state = self.0.state.borrow_mut(); + let surface_id = state.surface.id(); + + if let Some(parent) = state.parent.as_ref() { + parent.state.borrow_mut().children.remove(&surface_id); + } + + let client = state.client.clone(); + + state.renderer.destroy(); + + // Destroy blur first, this has no dependencies. + if let Some(blur) = &state.blur { + blur.release(); + } + if let Some(effect) = &state.background_effect { + effect.destroy(); + } + + // Decorations must be destroyed before the xdg state. + // See https://wayland.app/protocols/xdg-decoration-unstable-v1#zxdg_toplevel_decoration_v1 + if let Some(decoration) = &state.surface_state.decoration() { + decoration.destroy(); + } + + // Surface state might contain xdg_toplevel/xdg_surface which can be destroyed now that + // decorations are gone. layer_surface has no dependencies. + state.surface_state.destroy(); + + // Viewport must be destroyed before the wl_surface. + // See https://wayland.app/protocols/viewporter#wp_viewport + if let Some(viewport) = &state.viewport { + viewport.destroy(); + } + + // The wl_surface itself should always be destroyed last. + state.surface.destroy(); + + let state_ptr = self.0.clone(); + state + .globals + .executor + .spawn(async move { + state_ptr.close(); + client.drop_window(&surface_id) + }) + .detach(); + drop(state); + } +} + +impl WaylandWindow { + fn borrow(&self) -> Ref<'_, WaylandWindowState> { + self.0.state.borrow() + } + + fn borrow_mut(&self) -> RefMut<'_, WaylandWindowState> { + self.0.state.borrow_mut() + } + + pub fn new( + handle: AnyWindowHandle, + globals: Globals, + gpu_context: gpui_wgpu::GpuContext, + compositor_gpu: Option, + client: WaylandClientStatePtr, + params: WindowParams, + appearance: WindowAppearance, + parent: Option, + popup_grab: Option<(u32, wl_seat::WlSeat)>, + target_output: Option, + ) -> anyhow::Result<(Self, ObjectId)> { + let surface = globals.compositor.create_surface(&globals.qh, ()); + let surface_state = WaylandSurfaceState::new( + &surface, + &globals, + ¶ms, + parent.clone(), + popup_grab, + target_output, + )?; + + if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() { + fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id()); + } + + let viewport = globals + .viewporter + .as_ref() + .map(|viewporter| viewporter.get_viewport(&surface, &globals.qh, ())); + + let frame_ping = globals.frame_ping.clone(); + let this = Self(WaylandWindowStatePtr { + state: Rc::new(RefCell::new(WaylandWindowState::new( + handle, + surface.clone(), + surface_state, + appearance, + viewport, + client, + globals, + gpu_context, + compositor_gpu, + params, + parent, + )?)), + callbacks: Rc::new(RefCell::new(Callbacks::default())), + frame_loop: Rc::new(Cell::new(FrameLoop::Unconfigured)), + frame_ping, + }); + + // Kick things off + surface.commit(); + + Ok((this, surface.id())) + } +} + +impl WaylandWindowStatePtr { + pub fn handle(&self) -> AnyWindowHandle { + self.state.borrow().handle + } + + pub fn surface(&self) -> wl_surface::WlSurface { + self.state.borrow().surface.clone() + } + + pub fn toplevel(&self) -> Option { + self.state.borrow().surface_state.toplevel().cloned() + } + + /// The `xdg_surface` backing this window, if it has one. Used to anchor child popups. + pub fn xdg_surface(&self) -> Option { + self.state.borrow().surface_state.xdg_surface().cloned() + } + + /// The layer-shell surface backing this window, if it is one. Used to anchor child popups. + pub fn layer_surface(&self) -> Option { + self.state.borrow().surface_state.layer_surface().cloned() + } + + /// This window's xdg window geometry in surface-local coordinates. Child popup anchor + /// rectangles are relative to it, while gpui coordinates are surface-local. + pub fn window_geometry(&self) -> Bounds { + let state = self.state.borrow(); + inset_by_tiling( + state.bounds.map_origin(|_| px(0.0)), + state.inset(), + state.tiling, + ) + } + + pub fn ptr_eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.state, &other.state) + } + + pub fn add_child(&self, child: ObjectId, blocking: bool) { + let mut state = self.state.borrow_mut(); + state.children.insert(child, blocking); + } + + pub fn is_blocked(&self) -> bool { + let state = self.state.borrow(); + state.children.values().any(|&blocking| blocking) + } + + pub fn frame(&self) { + self.frame_loop.set(FrameLoop::Ticking); + let mut state = self.state.borrow_mut(); + state.resize_throttle = false; + // GPUI may throttle this tick without calling draw, so leave the request + // latched until a draw actually reaches the renderer. + let force_render = state.redraw_requested; + let require_presentation = state.presentation.requires_presentation(); + drop(state); + + let mut callbacks = self.callbacks.borrow_mut(); + let Some(request_frame_callback) = callbacks.request_frame.as_mut() else { + self.frame_loop.set(FrameLoop::Parked); + return; + }; + request_frame_callback(RequestFrameOptions { + force_render, + require_presentation, + }); + self.update_ime_enabled(); + drop(callbacks); + + self.complete_frame(); + } + + fn complete_frame(&self) { + let mut state = self.state.borrow_mut(); + + let frame_loop = self.frame_loop.get(); + if frame_loop == FrameLoop::AwaitingCallback { + return; + } + + if state.presentation.requires_presentation() { + // Before the first present, or when throttling skipped draw, a + // callback may never arrive. Otherwise let the compositor pace + // retries so an occluded window does not keep polling. + if frame_loop == FrameLoop::PresentationFailed + && state.presentation == PresentationState::RetryAfterPresent + { + if state.pending_frame_callback.is_none() { + let callback = state.surface.frame(&state.globals.qh, state.surface.id()); + state.pending_frame_callback = Some(callback); + } + state.surface.commit(); + self.frame_loop.set(FrameLoop::AwaitingCallback); + return; + } + + self.frame_loop.set(FrameLoop::RetryScheduled); + let surface_id = state.surface.id(); + let client = state.client.clone(); + drop(state); + client.schedule_frame_retry(&surface_id); + return; + } + + if frame_loop == FrameLoop::RescheduleRequested || state.redraw_requested { + self.frame_loop.set(FrameLoop::RetryScheduled); + let surface_id = state.surface.id(); + let client = state.client.clone(); + drop(state); + client.schedule_frame_retry(&surface_id); + return; + } + + self.frame_loop.set(FrameLoop::Parked); + } + + pub fn frame_callback_fired(&self) { + // Another wl_surface commit may have carried this callback while a retry + // timer owned the render-loop wakeup. + self.state.borrow_mut().pending_frame_callback = None; + if self.frame_loop.get() == FrameLoop::AwaitingCallback { + self.frame(); + } + } + + pub fn scheduled_frame_fired(&self) { + if self.frame_loop.get() == FrameLoop::Scheduled { + self.frame(); + } + } + + pub fn retry_timer_fired(&self) { + if self.frame_loop.get() == FrameLoop::RetryScheduled { + self.frame(); + } + } + + pub fn is_configured(&self) -> bool { + self.frame_loop.get() != FrameLoop::Unconfigured + } + + pub fn schedule_frame(&self) { + match self.frame_loop.get() { + FrameLoop::Parked => { + self.frame_loop.set(FrameLoop::Scheduled); + self.frame_ping.ping(); + } + FrameLoop::Ticking => { + self.frame_loop.set(FrameLoop::RescheduleRequested); + } + // A wake is already armed: a ping or retry timer is in flight, or a + // presented buffer guarantees a compositor frame callback. + _ => {} + } + } + + fn request_redraw(&self) { + self.state.borrow_mut().redraw_requested = true; + self.schedule_frame(); + } + + fn update_ime_enabled(&self) { + let mut state = self.state.borrow_mut(); + if !state.active { + return; + } + let client = state.client.clone(); + let ime_enabled = if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + let accepts_text_input = input_handler.query_accepts_text_input(); + self.state.borrow_mut().input_handler = Some(input_handler); + accepts_text_input + } else { + drop(state); + false + }; + if Some(ime_enabled) == client.ime_enabled() { + return; + } + + if ime_enabled { + client.enable_ime(); + } else { + client.disable_ime(); + } + } + + pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) { + if let xdg_surface::Event::Configure { serial } = event { + { + let mut state = self.state.borrow_mut(); + if let Some(window_controls) = state.in_progress_window_controls.take() { + state.window_controls = window_controls; + + drop(state); + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() { + appearance_changed(); + } + } + } + { + let mut state = self.state.borrow_mut(); + + if let Some(mut configure) = state.in_progress_configure.take() { + let got_unmaximized = state.maximized && !configure.maximized; + state.fullscreen = configure.fullscreen; + state.maximized = configure.maximized; + state.tiling = configure.tiling; + let visibility_changed = state.visibility != configure.visibility; + state.visibility = configure.visibility; + // Limit interactive resizes to once per vblank + let throttled = configure.resizing && state.resize_throttle; + if throttled { + state.surface_state.ack_configure(serial); + } else { + if configure.resizing { + state.resize_throttle = true; + } + if !configure.fullscreen && !configure.maximized { + configure.size = if got_unmaximized { + Some(state.window_bounds.size) + } else { + compute_outer_size(state.inset(), configure.size, state.tiling) + }; + if let Some(size) = configure.size { + state.window_bounds = Bounds { + origin: Point::default(), + size, + }; + } + } + } + drop(state); + if visibility_changed { + self.report_visibility(configure.visibility); + } + if throttled { + return; + } + if let Some(size) = configure.size { + self.resize(size); + } + } + } + let state = self.state.borrow_mut(); + state.surface_state.ack_configure(serial); + + let window_geometry = inset_by_tiling( + state.bounds.map_origin(|_| px(0.0)), + state.inset(), + state.tiling, + ) + .map(|v| f32::from(v) as i32) + .map_size(|v| if v <= 0 { 1 } else { v }); + + state.surface_state.set_geometry( + window_geometry.origin.x, + window_geometry.origin.y, + window_geometry.size.width, + window_geometry.size.height, + ); + + let initial_configure = self.frame_loop.get() == FrameLoop::Unconfigured; + drop(state); + if initial_configure { + self.frame(); + } else { + self.request_redraw(); + } + } + } + + pub fn handle_toplevel_decoration_event(&self, event: zxdg_toplevel_decoration_v1::Event) { + if let zxdg_toplevel_decoration_v1::Event::Configure { mode } = event { + match mode { + WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => { + self.state.borrow_mut().decorations = WindowDecorations::Server; + let callback = self.callbacks.borrow_mut().appearance_changed.take(); + if let Some(mut fun) = callback { + fun(); + self.callbacks.borrow_mut().appearance_changed = Some(fun); + } + } + WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => { + self.state.borrow_mut().decorations = WindowDecorations::Client; + // Update background to be transparent + let callback = self.callbacks.borrow_mut().appearance_changed.take(); + if let Some(mut fun) = callback { + fun(); + self.callbacks.borrow_mut().appearance_changed = Some(fun); + } + } + WEnum::Value(_) => { + log::warn!("Unknown decoration mode"); + return; + } + WEnum::Unknown(v) => { + log::warn!("Unknown decoration mode: {}", v); + return; + } + } + update_window(self.state.borrow_mut()); + self.request_redraw(); + } + } + + pub fn handle_fractional_scale_event(&self, event: wp_fractional_scale_v1::Event) { + if let wp_fractional_scale_v1::Event::PreferredScale { scale } = event { + self.rescale(scale as f32 / 120.0); + self.request_redraw(); + } + } + + pub fn handle_toplevel_event(&self, event: xdg_toplevel::Event) -> bool { + match event { + xdg_toplevel::Event::Configure { + width, + height, + states, + } => { + let size = if width == 0 || height == 0 { + None + } else { + Some(size(px(width as f32), px(height as f32))) + }; + + let states = extract_states::(&states); + + let mut tiling = Tiling::default(); + let mut fullscreen = false; + let mut maximized = false; + let mut resizing = false; + let mut visibility = WindowVisibility::Visible; + + for state in states { + match state { + xdg_toplevel::State::Maximized => { + maximized = true; + } + xdg_toplevel::State::Fullscreen => { + fullscreen = true; + } + xdg_toplevel::State::Resizing => resizing = true, + xdg_toplevel::State::Suspended => visibility = WindowVisibility::Hidden, + xdg_toplevel::State::TiledTop => { + tiling.top = true; + } + xdg_toplevel::State::TiledLeft => { + tiling.left = true; + } + xdg_toplevel::State::TiledRight => { + tiling.right = true; + } + xdg_toplevel::State::TiledBottom => { + tiling.bottom = true; + } + _ => { + // noop + } + } + } + + if fullscreen || maximized { + tiling = Tiling::tiled(); + } + + let mut state = self.state.borrow_mut(); + state.in_progress_configure = Some(InProgressConfigure { + size, + fullscreen, + maximized, + resizing, + visibility, + tiling, + }); + + false + } + xdg_toplevel::Event::Close => { + let mut cb = self.callbacks.borrow_mut(); + if let Some(mut should_close) = cb.should_close.take() { + let result = (should_close)(); + cb.should_close = Some(should_close); + if result { + drop(cb); + self.close(); + } + result + } else { + true + } + } + xdg_toplevel::Event::WmCapabilities { capabilities } => { + let mut window_controls = WindowControls { + maximize: false, + minimize: false, + fullscreen: false, + window_menu: false, + }; + + let states = extract_states::(&capabilities); + + for state in states { + match state { + xdg_toplevel::WmCapabilities::Maximize => { + window_controls.maximize = true; + } + xdg_toplevel::WmCapabilities::Minimize => { + window_controls.minimize = true; + } + xdg_toplevel::WmCapabilities::Fullscreen => { + window_controls.fullscreen = true; + } + xdg_toplevel::WmCapabilities::WindowMenu => { + window_controls.window_menu = true; + } + _ => {} + } + } + + let mut state = self.state.borrow_mut(); + state.in_progress_window_controls = Some(window_controls); + false + } + _ => false, + } + } + + pub fn handle_layersurface_event(&self, event: zwlr_layer_surface_v1::Event) -> bool { + match event { + zwlr_layer_surface_v1::Event::Configure { + width, + height, + serial, + } => { + let size = if width == 0 || height == 0 { + None + } else { + Some(size(px(width as f32), px(height as f32))) + }; + + let mut state = self.state.borrow_mut(); + state.in_progress_configure = Some(InProgressConfigure { + size, + fullscreen: false, + maximized: false, + resizing: false, + visibility: WindowVisibility::Visible, + tiling: Tiling::default(), + }); + drop(state); + + // just do the same thing we'd do as an xdg_surface + self.handle_xdg_surface_event(xdg_surface::Event::Configure { serial }); + + false + } + zwlr_layer_surface_v1::Event::Closed => { + // unlike xdg, we don't have a choice here: the surface is closing. + true + } + _ => false, + } + } + + // Returns `true` if the popup should be closed. + pub fn handle_popup_event(&self, event: xdg_popup::Event) -> bool { + match event { + // Only the size is needed, the position is the compositor's. The following + // xdg_surface.configure applies the change. + xdg_popup::Event::Configure { width, height, .. } => { + let size = if width <= 0 || height <= 0 { + None + } else { + Some(size(px(width as f32), px(height as f32))) + }; + + self.state.borrow_mut().in_progress_configure = Some(InProgressConfigure { + size, + fullscreen: false, + maximized: false, + resizing: false, + visibility: WindowVisibility::Visible, + tiling: Tiling::default(), + }); + + false + } + xdg_popup::Event::PopupDone => true, + // Precedes the reposition's Configure, which does the work. The token is not needed. + xdg_popup::Event::Repositioned { .. } => false, + _ => false, + } + } + + #[allow(clippy::mutable_key_type)] + pub fn handle_surface_event( + &self, + event: wl_surface::Event, + outputs: HashMap, + ) { + let mut state = self.state.borrow_mut(); + + match event { + wl_surface::Event::Enter { output } => { + let id = output.id(); + + let Some(output) = outputs.get(&id) else { + return; + }; + + state.outputs.insert(id, output.clone()); + + let scale = state.primary_output_scale(); + state.update_subpixel_layout(); + + // We use `PreferredBufferScale` instead to set the scale if it's available + if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE { + state.surface.set_buffer_scale(scale); + drop(state); + self.rescale(scale as f32); + } else { + drop(state); + } + self.request_redraw(); + } + wl_surface::Event::Leave { output } => { + state.outputs.remove(&output.id()); + + let scale = state.primary_output_scale(); + state.update_subpixel_layout(); + + // We use `PreferredBufferScale` instead to set the scale if it's available + if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE { + state.surface.set_buffer_scale(scale); + drop(state); + self.rescale(scale as f32); + } else { + drop(state); + } + self.request_redraw(); + } + wl_surface::Event::PreferredBufferScale { factor } => { + // We use `WpFractionalScale` instead to set the scale if it's available + if state.globals.fractional_scale_manager.is_none() { + state.surface.set_buffer_scale(factor); + drop(state); + self.rescale(factor as f32); + self.request_redraw(); + } + } + _ => {} + } + } + + pub fn handle_ime(&self, ime: ImeInput) { + if self.is_blocked() { + return; + } + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + match ime { + ImeInput::InsertText(text) => { + input_handler.replace_text_in_range(None, &text); + } + ImeInput::SetMarkedText(text) => { + input_handler.replace_and_mark_text_in_range(None, &text, None); + } + ImeInput::UnmarkText => { + input_handler.unmark_text(); + } + ImeInput::DeleteText => { + if let Some(marked) = input_handler.marked_text_range() { + input_handler.replace_text_in_range(Some(marked), ""); + } + } + } + self.state.borrow_mut().input_handler = Some(input_handler); + } + } + + pub fn get_ime_area(&self) -> Option> { + let mut state = self.state.borrow_mut(); + let mut bounds: Option> = None; + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + bounds = input_handler.ime_candidate_bounds(); + self.state.borrow_mut().input_handler = Some(input_handler); + } + bounds + } + + pub fn set_size_and_scale(&self, size: Option>, scale: Option) { + let (size, scale) = { + let mut state = self.state.borrow_mut(); + if size.is_none_or(|size| size == state.bounds.size) + && scale.is_none_or(|scale| scale == state.scale) + { + return; + } + if let Some(size) = size { + state.bounds.size = size; + } + if let Some(scale) = scale { + state.scale = scale; + } + let device_bounds = state.bounds.to_device_pixels(state.scale); + state.renderer.update_drawable_size(device_bounds.size); + (state.bounds.size, state.scale) + }; + + let callback = self.callbacks.borrow_mut().resize.take(); + if let Some(mut fun) = callback { + fun(size, scale); + self.callbacks.borrow_mut().resize = Some(fun); + } + + { + let state = self.state.borrow(); + if let Some(viewport) = &state.viewport { + viewport + .set_destination(f32::from(size.width) as i32, f32::from(size.height) as i32); + } + } + } + + pub fn resize(&self, size: Size) { + self.set_size_and_scale(Some(size), None); + } + + pub fn rescale(&self, scale: f32) { + self.set_size_and_scale(None, Some(scale)); + } + + pub fn close(&self) { + let state = self.state.borrow(); + let client = state.client.get_client(); + let children = state.children.keys().cloned().collect::>(); + drop(state); + + for child in children { + let mut client_state = client.borrow_mut(); + let window = get_window(&mut client_state, &child); + drop(client_state); + + if let Some(child) = window { + child.close(); + } + } + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(fun) = callbacks.close.take() { + fun() + } + } + + pub fn handle_input(&self, input: PlatformInput) { + if self.is_blocked() { + return; + } + let callback = self.callbacks.borrow_mut().input.take(); + if let Some(mut fun) = callback { + let result = fun(input.clone()); + self.callbacks.borrow_mut().input = Some(fun); + if !result.propagate { + return; + } + } + if let PlatformInput::KeyDown(event) = input + && event.keystroke.modifiers.is_subset_of(&Modifiers::shift()) + && let Some(key_char) = &event.keystroke.key_char + { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.replace_text_in_range(None, key_char); + self.state.borrow_mut().input_handler = Some(input_handler); + } + } + } + + pub fn set_focused(&self, focus: bool) { + self.state.borrow_mut().active = focus; + let callback = self.callbacks.borrow_mut().active_status_change.take(); + if let Some(mut fun) = callback { + fun(focus); + self.callbacks.borrow_mut().active_status_change = Some(fun); + } + if let Some(adapter) = self.state.borrow_mut().accesskit_adapter.as_mut() { + adapter.update_window_focus_state(focus); + } + } + + pub fn set_hovered(&self, focus: bool) { + let callback = self.callbacks.borrow_mut().hover_status_change.take(); + if let Some(mut fun) = callback { + fun(focus); + self.callbacks.borrow_mut().hover_status_change = Some(fun); + } + } + + fn report_visibility(&self, visibility: WindowVisibility) { + let callback = self.callbacks.borrow_mut().visibility_change.take(); + if let Some(mut callback) = callback { + callback(visibility); + self.callbacks.borrow_mut().visibility_change = Some(callback); + } + } + + pub fn set_appearance(&mut self, appearance: WindowAppearance) { + self.state.borrow_mut().appearance = appearance; + + let callback = self.callbacks.borrow_mut().appearance_changed.take(); + if let Some(mut fun) = callback { + fun(); + self.callbacks.borrow_mut().appearance_changed = Some(fun); + } + } + + pub fn set_button_layout(&self) { + let callback = self.callbacks.borrow_mut().button_layout_changed.take(); + if let Some(mut fun) = callback { + fun(); + self.callbacks.borrow_mut().button_layout_changed = Some(fun); + } + } + + pub fn primary_output_scale(&self) -> i32 { + self.state.borrow_mut().primary_output_scale() + } +} + +fn extract_states<'a, S: TryFrom + 'a>(states: &'a [u8]) -> impl Iterator + 'a +where + >::Error: 'a, +{ + states + .chunks_exact(4) + .flat_map(TryInto::<[u8; 4]>::try_into) + .map(u32::from_ne_bytes) + .flat_map(S::try_from) +} + +impl rwh::HasWindowHandle for WaylandWindow { + fn window_handle(&self) -> Result, rwh::HandleError> { + let surface = self.0.surface().id().as_ptr() as *mut libc::c_void; + let c_ptr = NonNull::new(surface).ok_or(rwh::HandleError::Unavailable)?; + let handle = rwh::WaylandWindowHandle::new(c_ptr); + let raw_handle = rwh::RawWindowHandle::Wayland(handle); + Ok(unsafe { rwh::WindowHandle::borrow_raw(raw_handle) }) + } +} + +impl rwh::HasDisplayHandle for WaylandWindow { + fn display_handle(&self) -> Result, rwh::HandleError> { + let display = self + .0 + .surface() + .backend() + .upgrade() + .ok_or(rwh::HandleError::Unavailable)? + .display_ptr() as *mut libc::c_void; + + let c_ptr = NonNull::new(display).ok_or(rwh::HandleError::Unavailable)?; + let handle = rwh::WaylandDisplayHandle::new(c_ptr); + let raw_handle = rwh::RawDisplayHandle::Wayland(handle); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(raw_handle) }) + } +} + +impl PlatformWindow for WaylandWindow { + fn bounds(&self) -> Bounds { + self.borrow().bounds + } + + fn is_maximized(&self) -> bool { + self.borrow().maximized + } + + fn window_bounds(&self) -> WindowBounds { + let state = self.borrow(); + if state.fullscreen { + WindowBounds::Fullscreen(state.window_bounds) + } else if state.maximized { + WindowBounds::Maximized(state.window_bounds) + } else { + drop(state); + WindowBounds::Windowed(self.bounds()) + } + } + + fn inner_window_bounds(&self) -> WindowBounds { + let state = self.borrow(); + if state.fullscreen { + WindowBounds::Fullscreen(state.window_bounds) + } else if state.maximized { + WindowBounds::Maximized(state.window_bounds) + } else { + let inset = state.inset(); + drop(state); + WindowBounds::Windowed(self.bounds().inset(inset)) + } + } + + fn content_size(&self) -> Size { + self.borrow().bounds.size + } + + fn resize(&mut self, size: Size) { + let state = self.borrow(); + let state_ptr = self.0.clone(); + + // A popup's placement is the compositor's, so a resize re-runs the positioner and the + // configure reply drives the buffer resize. Before the first configure the popup is + // unmapped and cannot reposition, but the initial positioner already carries the size. + if matches!(state.surface_state, WaylandSurfaceState::Popup(_)) { + if self.0.is_configured() { + let parent_geometry = state + .parent + .as_ref() + .map(|parent| parent.window_geometry()) + .unwrap_or_default(); + state + .surface_state + .reposition_popup(&state.globals, size, parent_geometry); + } + return; + } + + // Keep window geometry consistent with configure handling. On Wayland, window geometry is + // surface-local: resizing should not attempt to translate the window; the compositor + // controls placement. We also account for client-side decoration insets and tiling. + let window_geometry = inset_by_tiling( + Bounds { + origin: Point::default(), + size, + }, + state.inset(), + state.tiling, + ) + .map(|v| f32::from(v) as i32) + .map_size(|v| if v <= 0 { 1 } else { v }); + + state.surface_state.set_geometry( + window_geometry.origin.x, + window_geometry.origin.y, + window_geometry.size.width, + window_geometry.size.height, + ); + + state + .globals + .executor + .spawn(async move { state_ptr.resize(size) }) + .detach(); + } + + fn scale_factor(&self) -> f32 { + self.borrow().scale + } + + fn appearance(&self) -> WindowAppearance { + self.borrow().appearance + } + + fn display(&self) -> Option> { + let state = self.borrow(); + state.display.as_ref().map(|(id, display)| { + Rc::new(WaylandDisplay { + id: id.clone(), + name: display.name.clone(), + bounds: display.bounds.to_pixels(state.scale), + }) as Rc + }) + } + + fn mouse_position(&self) -> Point { + self.borrow() + .client + .get_client() + .borrow() + .mouse_location + .unwrap_or_default() + } + + fn modifiers(&self) -> Modifiers { + self.borrow().client.get_client().borrow().modifiers + } + + fn capslock(&self) -> Capslock { + self.borrow().client.get_client().borrow().capslock + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.borrow_mut().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.borrow_mut().input_handler.take() + } + + fn prompt( + &self, + _level: PromptLevel, + _msg: &str, + _detail: Option<&str>, + _answers: &[PromptButton], + ) -> Option> { + None + } + + fn activate(&self) { + // Try to request an activation token. Even though the activation is likely going to be rejected, + // KWin and Mutter can use the app_id to visually indicate we're requesting attention. + let state = self.borrow(); + if let (Some(activation), Some(app_id)) = (&state.globals.activation, state.app_id.clone()) + { + state.client.set_pending_activation(state.surface.id()); + let token = activation.get_activation_token(&state.globals.qh, ()); + // The serial isn't exactly important here, since the activation is probably going to be rejected anyway. + let serial = state.client.get_serial(SerialKind::MousePress); + token.set_app_id(app_id); + token.set_serial(serial.as_raw(), &state.globals.seat); + token.set_surface(&state.surface); + token.commit(); + } + } + + fn request_attention(&self) {} + + fn is_active(&self) -> bool { + self.borrow().active + } + + fn visibility(&self) -> WindowVisibility { + self.borrow().visibility + } + + fn is_hovered(&self) -> bool { + self.borrow().hovered + } + + fn set_title(&mut self, title: &str) { + if let Some(toplevel) = self.borrow().surface_state.toplevel() { + toplevel.set_title(title.to_string()); + } + } + + fn set_app_id(&mut self, app_id: &str) { + let mut state = self.borrow_mut(); + if let Some(toplevel) = state.surface_state.toplevel() { + toplevel.set_app_id(app_id.to_owned()); + } + state.app_id = Some(app_id.to_owned()); + } + + fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { + let mut state = self.borrow_mut(); + if state.background_appearance == background_appearance { + return; + } + state.background_appearance = background_appearance; + update_window(state); + self.0.request_redraw(); + } + + fn background_appearance(&self) -> WindowBackgroundAppearance { + self.borrow().background_appearance + } + + fn is_subpixel_rendering_supported(&self) -> bool { + let client = self.borrow().client.get_client(); + let state = client.borrow(); + state + .gpu_context + .borrow() + .as_ref() + .is_some_and(|ctx| ctx.supports_dual_source_blending()) + } + + fn minimize(&self) { + if let Some(toplevel) = self.borrow().surface_state.toplevel() { + toplevel.set_minimized(); + } + } + + fn zoom(&self) { + let state = self.borrow(); + if let Some(toplevel) = state.surface_state.toplevel() { + if !state.maximized { + toplevel.set_maximized(); + } else { + toplevel.unset_maximized(); + } + } + } + + fn toggle_fullscreen(&self) { + let state = self.borrow(); + if let Some(toplevel) = state.surface_state.toplevel() { + if !state.fullscreen { + toplevel.set_fullscreen(None); + } else { + toplevel.unset_fullscreen(); + } + } + } + + fn is_fullscreen(&self) -> bool { + self.borrow().fullscreen + } + + fn on_request_frame(&self, callback: Box) { + self.0.callbacks.borrow_mut().request_frame = Some(callback); + } + + fn on_input(&self, callback: Box gpui::DispatchEventResult>) { + self.0.callbacks.borrow_mut().input = Some(callback); + } + + fn on_active_status_change(&self, callback: Box) { + self.0.callbacks.borrow_mut().active_status_change = Some(callback); + } + + fn on_visibility_change(&self, callback: Box) { + self.0.callbacks.borrow_mut().visibility_change = Some(callback); + } + + fn on_hover_status_change(&self, callback: Box) { + self.0.callbacks.borrow_mut().hover_status_change = Some(callback); + } + + fn on_resize(&self, callback: Box, f32)>) { + self.0.callbacks.borrow_mut().resize = Some(callback); + } + + fn on_moved(&self, callback: Box) { + self.0.callbacks.borrow_mut().moved = Some(callback); + } + + fn on_should_close(&self, callback: Box bool>) { + self.0.callbacks.borrow_mut().should_close = Some(callback); + } + + fn on_close(&self, callback: Box) { + self.0.callbacks.borrow_mut().close = Some(callback); + } + + fn on_hit_test_window_control(&self, _callback: Box Option>) { + } + + fn on_appearance_changed(&self, callback: Box) { + self.0.callbacks.borrow_mut().appearance_changed = Some(callback); + } + + fn on_button_layout_changed(&self, callback: Box) { + self.0.callbacks.borrow_mut().button_layout_changed = Some(callback); + } + + fn draw(&self, scene: &Scene) { + let mut state = self.borrow_mut(); + + if state.renderer.device_lost() { + let raw_window = RawWindow { + window: state.surface.id().as_ptr().cast::(), + display: state + .surface + .backend() + .upgrade() + .unwrap() + .display_ptr() + .cast::(), + }; + match state.renderer.recover(&raw_window) { + Ok(()) => {} + Err(err) => { + log::warn!("GPU recovery failed, will retry on next frame: {err}"); + } + } + + state.redraw_requested = true; + return; + } + + // Surface state changed during this GPUI tick is included in this presentation. + state.redraw_requested = false; + if state.pending_frame_callback.is_none() { + let callback = state.surface.frame(&state.globals.qh, state.surface.id()); + state.pending_frame_callback = Some(callback); + } + if state.renderer.draw(scene) { + state.presentation = PresentationState::Presented; + self.0.frame_loop.set(FrameLoop::AwaitingCallback); + } else { + state.presentation = state.presentation.failed(); + self.0.frame_loop.set(FrameLoop::PresentationFailed); + } + + if state.renderer.needs_redraw() { + state.redraw_requested = true; + } + } + + fn schedule_frame(&self) { + self.0.schedule_frame(); + } + + fn sprite_atlas(&self) -> Arc { + let state = self.borrow(); + state.renderer.sprite_atlas().clone() + } + + fn show_window_menu(&self, position: Point) { + let state = self.borrow(); + let serial = state.client.get_serial(SerialKind::MousePress); + if let Some(toplevel) = state.surface_state.toplevel() { + toplevel.show_window_menu( + &state.globals.seat, + serial.as_raw(), + f32::from(position.x) as i32, + f32::from(position.y) as i32, + ); + } + } + + fn start_window_move(&self) { + let state = self.borrow(); + let serial = state.client.get_serial(SerialKind::MousePress); + if let Some(toplevel) = state.surface_state.toplevel() { + toplevel._move(&state.globals.seat, serial.as_raw()); + } + } + + fn can_start_external_drag(&self) -> bool { + true + } + + fn start_external_drag(&self, payload: &ExternalDragPayload) -> bool { + let state = self.borrow(); + state.client.start_external_drag(&state.surface, payload) + } + + fn start_window_resize(&self, edge: gpui::ResizeEdge) { + let state = self.borrow(); + if let Some(toplevel) = state.surface_state.toplevel() { + toplevel.resize( + &state.globals.seat, + state.client.get_serial(SerialKind::MousePress).as_raw(), + edge.to_xdg(), + ) + } + } + + fn set_exclusive_zone(&self, zone: Pixels) { + let state = self.borrow(); + if state + .surface_state + .set_exclusive_zone(f32::from(zone) as i32) + { + // Commit to apply it immediately, otherwise it only takes effect + // on the next frame. + state.surface.commit(); + } + } + + fn set_exclusive_edge(&self, edge: Anchor) { + let state = self.borrow(); + if state.surface_state.set_exclusive_edge(edge) { + // Commit to apply it immediately, otherwise it only takes effect + // on the next frame. + state.surface.commit(); + } + } + + fn set_input_region(&self, region: Option<&[Bounds]>) { + let state = self.borrow(); + match region { + // No region means the whole surface receives input. + None => state.surface.set_input_region(None), + // A region restricts input to its rectangles. An empty region + // receives no input at all. + Some(rects) => { + let wl_region = state + .globals + .compositor + .create_region(&state.globals.qh, ()); + for rect in rects { + let rect = rect.map(|pixels| f32::from(pixels) as i32); + wl_region.add( + rect.origin.x, + rect.origin.y, + rect.size.width, + rect.size.height, + ); + } + state.surface.set_input_region(Some(&wl_region)); + wl_region.destroy(); + } + } + + // Commit so the new input region applies immediately. Otherwise it + // waits for the next frame, which could be the very click we want to + // allow passing through. + state.surface.commit(); + } + + fn window_decorations(&self) -> Decorations { + let state = self.borrow(); + match state.decorations { + WindowDecorations::Server => Decorations::Server, + WindowDecorations::Client => Decorations::Client { + tiling: state.tiling, + }, + } + } + + fn request_decorations(&self, decorations: WindowDecorations) { + let mut state = self.borrow_mut(); + match state.surface_state.decoration().as_ref() { + Some(decoration) => { + decoration.set_mode(decorations.to_xdg()); + state.decorations = decorations; + update_window(state); + } + None => { + if matches!(decorations, WindowDecorations::Server) { + log::info!( + "Server-side decorations requested, but the Wayland server does not support them. Falling back to client-side decorations." + ); + } + state.decorations = WindowDecorations::Client; + update_window(state); + } + } + self.0.request_redraw(); + } + + fn window_controls(&self) -> WindowControls { + self.borrow().window_controls + } + + fn set_client_inset(&self, inset: Pixels) { + let mut state = self.borrow_mut(); + if Some(inset) != state.client_inset { + state.client_inset = Some(inset); + update_window(state); + self.0.request_redraw(); + } + } + + fn update_ime_position(&self, bounds: Bounds) { + let state = self.borrow(); + if !state.active { + return; + } + state.client.update_ime_position(bounds); + } + + fn gpu_specs(&self) -> Option { + self.borrow().renderer.gpu_specs().into() + } + + fn play_system_bell(&self) { + let state = self.borrow(); + let surface = if state.surface_state.toplevel().is_some() { + Some(&state.surface) + } else { + None + }; + if let Some(bell) = state.globals.system_bell.as_ref() { + bell.ring(surface); + } + } + + fn a11y_init(&self, callbacks: gpui::A11yCallbacks) { + let activation_handler = TrivialActivationHandler { + callback: callbacks.activation, + }; + let action_handler = TrivialActionHandler(callbacks.action); + let deactivation_handler = TrivialDeactivationHandler { + callback: callbacks.deactivation, + }; + + let adapter = + accesskit_unix::Adapter::new(activation_handler, action_handler, deactivation_handler); + + self.borrow_mut().accesskit_adapter = Some(adapter); + } + + fn a11y_tree_update(&self, tree_update: accesskit::TreeUpdate) { + let mut state = self.borrow_mut(); + if let Some(adapter) = state.accesskit_adapter.as_mut() { + adapter.update_if_active(|| tree_update); + } + } + + fn a11y_update_window_bounds(&self) { + // Wayland doesn't expose window position, so this is a no-op + } +} + +struct TrivialActivationHandler { + callback: Box Option + Send + 'static>, +} + +impl accesskit::ActivationHandler for TrivialActivationHandler { + fn request_initial_tree(&mut self) -> Option { + (self.callback)() + } +} + +struct TrivialActionHandler(Box); + +impl accesskit::ActionHandler for TrivialActionHandler { + fn do_action(&mut self, request: accesskit::ActionRequest) { + (self.0)(request); + } +} + +struct TrivialDeactivationHandler { + callback: Box, +} + +impl accesskit::DeactivationHandler for TrivialDeactivationHandler { + fn deactivate_accessibility(&mut self) { + (self.callback)(); + } +} + +fn update_window(mut state: RefMut) { + let opaque = !state.is_transparent(); + + state.renderer.update_transparency(!opaque); + let opaque_area = state.window_bounds.map(|v| f32::from(v) as i32); + opaque_area.inset(f32::from(state.inset()) as i32); + + let region = state + .globals + .compositor + .create_region(&state.globals.qh, ()); + region.add( + opaque_area.origin.x, + opaque_area.origin.y, + opaque_area.size.width, + opaque_area.size.height, + ); + + // Note that rounded corners make this rectangle API hard to work with. + // As this is common when using CSD, let's just disable this API. + if state.background_appearance == WindowBackgroundAppearance::Opaque + && state.decorations == WindowDecorations::Server + { + // Promise the compositor that this region of the window surface + // contains no transparent pixels. This allows the compositor to skip + // updating whatever is behind the surface for better performance. + state.surface.set_opaque_region(Some(®ion)); + } else { + state.surface.set_opaque_region(None); + } + + // Prefer the standard ext-background-effect protocol; KWin 6.8 and later + // no longer advertise org_kde_kwin_blur_manager. + if let Some(effect_manager) = state.globals.background_effect_manager.clone() { + if state.background_appearance == WindowBackgroundAppearance::Blurred { + if state.background_effect.is_none() { + let effect = + effect_manager.get_background_effect(&state.surface, &state.globals.qh, ()); + state.background_effect = Some(effect); + } + // Unlike org_kde_kwin_blur, whose default covers the whole surface, + // the blur region starts empty, so the window area is set explicitly. + // It is double-buffered and applies with the next surface commit. + if let Some(effect) = &state.background_effect { + effect.set_blur_region(Some(®ion)); + } + } else if let Some(effect) = state.background_effect.take() { + // Destroying the object removes its blur region on the next commit. + effect.destroy(); + } + } else if let Some(ref blur_manager) = state.globals.blur_manager { + if state.background_appearance == WindowBackgroundAppearance::Blurred { + if state.blur.is_none() { + let blur = blur_manager.create(&state.surface, &state.globals.qh, ()); + state.blur = Some(blur); + } + state.blur.as_ref().unwrap().commit(); + } else { + // It probably doesn't hurt to clear the blur for opaque windows + blur_manager.unset(&state.surface); + if let Some(b) = state.blur.take() { + b.release() + } + } + } + + region.destroy(); +} + +pub(crate) trait WindowDecorationsExt { + fn to_xdg(self) -> zxdg_toplevel_decoration_v1::Mode; +} + +impl WindowDecorationsExt for WindowDecorations { + fn to_xdg(self) -> zxdg_toplevel_decoration_v1::Mode { + match self { + WindowDecorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide, + WindowDecorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide, + } + } +} + +pub(crate) trait ResizeEdgeWaylandExt { + fn to_xdg(self) -> xdg_toplevel::ResizeEdge; +} + +impl ResizeEdgeWaylandExt for ResizeEdge { + fn to_xdg(self) -> xdg_toplevel::ResizeEdge { + match self { + ResizeEdge::Top => xdg_toplevel::ResizeEdge::Top, + ResizeEdge::TopRight => xdg_toplevel::ResizeEdge::TopRight, + ResizeEdge::Right => xdg_toplevel::ResizeEdge::Right, + ResizeEdge::BottomRight => xdg_toplevel::ResizeEdge::BottomRight, + ResizeEdge::Bottom => xdg_toplevel::ResizeEdge::Bottom, + ResizeEdge::BottomLeft => xdg_toplevel::ResizeEdge::BottomLeft, + ResizeEdge::Left => xdg_toplevel::ResizeEdge::Left, + ResizeEdge::TopLeft => xdg_toplevel::ResizeEdge::TopLeft, + } + } +} + +/// The configuration event is in terms of the window geometry, which we are constantly +/// updating to account for the client decorations. But that's not the area we want to render +/// to, due to our intrusize CSD. So, here we calculate the 'actual' size, by adding back in the insets +fn compute_outer_size( + inset: Pixels, + new_size: Option>, + tiling: Tiling, +) -> Option> { + new_size.map(|mut new_size| { + if !tiling.top { + new_size.height += inset; + } + if !tiling.bottom { + new_size.height += inset; + } + if !tiling.left { + new_size.width += inset; + } + if !tiling.right { + new_size.width += inset; + } + + new_size + }) +} + +fn inset_by_tiling(mut bounds: Bounds, inset: Pixels, tiling: Tiling) -> Bounds { + if !tiling.top { + bounds.origin.y += inset; + bounds.size.height -= inset; + } + if !tiling.bottom { + bounds.size.height -= inset; + } + if !tiling.left { + bounds.origin.x += inset; + bounds.size.width -= inset; + } + if !tiling.right { + bounds.size.width -= inset; + } + + bounds +} diff --git a/vendor/gpui-pre-linux/src/linux/x11.rs b/vendor/gpui-pre-linux/src/linux/x11.rs new file mode 100644 index 000000000..5c7a0c2ac --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/x11.rs @@ -0,0 +1,12 @@ +mod client; +mod clipboard; +mod display; +mod event; +mod window; +mod xim_handler; + +pub(crate) use client::*; +pub(crate) use display::*; +pub(crate) use event::*; +pub(crate) use window::*; +pub(crate) use xim_handler::*; diff --git a/vendor/gpui-pre-linux/src/linux/x11/client.rs b/vendor/gpui-pre-linux/src/linux/x11/client.rs new file mode 100644 index 000000000..3ab9a6e6e --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/x11/client.rs @@ -0,0 +1,3159 @@ +use anyhow::{Context as _, anyhow}; +use ashpd::WindowIdentifier; +use calloop::{ + EventLoop, LoopHandle, RegistrationToken, + generic::{FdWrapper, Generic}, +}; +use collections::HashMap; +use core::str; +use gpui::{Capslock, profiler}; +use gpui_util::ResultExt as _; +use http_client::Url; +use log::Level; +use smallvec::SmallVec; +use std::{ + cell::{RefCell, RefMut}, + collections::{BTreeMap, HashSet}, + ops::Deref, + path::PathBuf, + rc::{Rc, Weak}, + time::{Duration, Instant}, +}; + +use x11rb::{ + connection::{Connection, RequestConnection}, + cursor, + errors::ConnectionError, + protocol::randr::ConnectionExt as _, + protocol::xinput::ConnectionExt, + protocol::xkb::ConnectionExt as _, + protocol::xproto::{ + AtomEnum, ChangeWindowAttributesAux, ClientMessageData, ClientMessageEvent, + ConnectionExt as _, EventMask, Visibility, + }, + protocol::{Event, dri3, randr, render, xinput, xkb, xproto}, + resource_manager::Database, + wrapper::ConnectionExt as _, + xcb_ffi::XCBConnection, +}; +use xim::{AttributeName, Client, InputStyle, x11rb::X11rbClient}; +use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION}; +use xkbcommon::xkb::{self as xkbc, STATE_LAYOUT_EFFECTIVE}; + +use super::{ + ButtonOrScroll, ScrollDirection, X11Display, X11WindowStatePtr, XcbAtoms, XimCallbackEvent, + XimHandler, button_or_scroll_from_event_detail, check_reply, + clipboard::{self, Clipboard}, + get_reply, get_valuator_axis_index, handle_connection_error, modifiers_from_state, + pressed_button_from_mask, xcb_flush, +}; + +use crate::linux::{ + DEFAULT_CURSOR_ICON_NAME, LinuxClient, capslock_from_xkb, cursor_style_to_icon_names, + get_xkb_compose_state, is_within_click_distance, keystroke_from_xkb, + keystroke_underlying_dead_key, log_cursor_icon_warning, modifiers_from_xkb, new_xkb_context, + open_uri_internal, + platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES}, + reveal_path_internal, + xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}, +}; +use crate::linux::{LinuxCommon, LinuxKeyboardLayout, X11Window, modifiers_from_xinput_info}; + +use gpui::{ + AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke, + Modifiers, ModifiersChangedEvent, MouseButton, Pixels, PlatformDisplay, PlatformInput, + PlatformKeyboardLayout, PlatformWindow, Point, RequestFrameOptions, ScrollDelta, Size, + TouchPhase, WindowButtonLayout, WindowParams, WindowVisibility, point, px, +}; +use gpui_wgpu::{CompositorGpuHint, GpuContext}; + +/// Value for DeviceId parameters which selects all devices. +pub(crate) const XINPUT_ALL_DEVICES: xinput::DeviceId = 0; + +/// Value for DeviceId parameters which selects all device groups. Events that +/// occur within the group are emitted by the group itself. +/// +/// In XInput 2's interface, these are referred to as "master devices", but that +/// terminology is both archaic and unclear. +pub(crate) const XINPUT_ALL_DEVICE_GROUPS: xinput::DeviceId = 1; + +const GPUI_X11_SCALE_FACTOR_ENV: &str = "GPUI_X11_SCALE_FACTOR"; + +pub(crate) struct WindowRef { + window: X11WindowStatePtr, + refresh_state: Option, + last_visibility: Visibility, + is_mapped: bool, +} + +impl WindowRef { + pub fn handle(&self) -> AnyWindowHandle { + self.window.state.borrow().handle + } + + /// Whether the X server is presenting this window. Compositing window + /// managers rarely report `FULLY_OBSCURED`, so under them this is the + /// mapped state alone. + fn visibility(&self) -> WindowVisibility { + if self.is_mapped && !matches!(self.last_visibility, Visibility::FULLY_OBSCURED) { + WindowVisibility::Visible + } else { + WindowVisibility::Hidden + } + } +} + +impl Deref for WindowRef { + type Target = X11WindowStatePtr; + + fn deref(&self) -> &Self::Target { + &self.window + } +} + +enum RefreshState { + Hidden { + refresh_rate: Duration, + }, + PeriodicRefresh { + refresh_rate: Duration, + event_loop_token: RegistrationToken, + }, +} + +#[derive(Debug)] +#[non_exhaustive] +pub enum EventHandlerError { + XCBConnectionError(ConnectionError), + XIMClientError(xim::ClientError), +} + +impl std::error::Error for EventHandlerError {} + +impl std::fmt::Display for EventHandlerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EventHandlerError::XCBConnectionError(err) => err.fmt(f), + EventHandlerError::XIMClientError(err) => err.fmt(f), + } + } +} + +impl From for EventHandlerError { + fn from(err: ConnectionError) -> Self { + EventHandlerError::XCBConnectionError(err) + } +} + +impl From for EventHandlerError { + fn from(err: xim::ClientError) -> Self { + EventHandlerError::XIMClientError(err) + } +} + +#[derive(Debug, Default)] +pub struct Xdnd { + other_window: xproto::Window, + drag_type: u32, + retrieved: bool, + position: Point, +} + +#[derive(Debug)] +struct PointerDeviceState { + horizontal: ScrollAxisState, + vertical: ScrollAxisState, +} + +#[derive(Debug, Default)] +struct ScrollAxisState { + /// Valuator number for looking up this axis's scroll value. + valuator_number: Option, + /// Conversion factor from scroll units to lines. + multiplier: f32, + /// Last scroll value for calculating scroll delta. + /// + /// This gets set to `None` whenever it might be invalid - when devices change or when window focus changes. + /// The logic errs on the side of invalidating this, since the consequence is just skipping the delta of one scroll event. + /// The consequence of not invalidating it can be large invalid deltas, which are much more user visible. + scroll_value: Option, +} + +pub struct X11ClientState { + pub(crate) loop_handle: LoopHandle<'static, X11Client>, + pub(crate) event_loop: Option>, + + pub(crate) last_click: Instant, + pub(crate) last_mouse_button: Option, + pub(crate) last_location: Point, + pub(crate) current_count: usize, + pub(crate) pinch_scale: f32, + + pub(crate) gpu_context: GpuContext, + pub(crate) compositor_gpu: Option, + + pub(crate) scale_factor: f32, + + xkb_context: xkbc::Context, + pub(crate) xcb_connection: Rc, + xkb_device_id: i32, + client_side_decorations_supported: bool, + pub(crate) x_root_index: usize, + pub(crate) resource_database: Database, + pub(crate) atoms: XcbAtoms, + pub(crate) windows: HashMap, + pub(crate) mouse_focused_window: Option, + pub(crate) keyboard_focused_window: Option, + pub(crate) xkb: xkbc::State, + keyboard_layout: LinuxKeyboardLayout, + pub(crate) ximc: Option>>, + pub(crate) xim_handler: Option, + pub modifiers: Modifiers, + pub capslock: Capslock, + // TODO: Can the other updates to `modifiers` be removed so that this is unnecessary? + // capslock logic was done analog to modifiers + pub last_modifiers_changed_event: Modifiers, + pub last_capslock_changed_event: Capslock, + + pub(crate) compose_state: Option, + pub(crate) pre_edit_text: Option, + pub(crate) composing: bool, + pub(crate) pre_key_char_down: Option, + pub(crate) cursor_handle: cursor::Handle, + pub(crate) cursor_styles: HashMap, + pub(crate) cursor_cache: HashMap>, + pub(crate) invisible_cursor_cache: Option, + pub(crate) cursor_hidden_window: Option, + + pointer_device_states: BTreeMap, + + pub(crate) supports_xinput_gestures: bool, + + pub(crate) common: LinuxCommon, + pub(crate) clipboard: Clipboard, + pub(crate) clipboard_item: Option, + pub(crate) xdnd_state: Xdnd, +} + +#[derive(Clone)] +pub struct X11ClientStatePtr(pub Weak>); + +impl X11ClientStatePtr { + pub fn get_client(&self) -> Option { + self.0.upgrade().map(X11Client) + } + + pub fn drop_window(&self, x_window: u32) { + let Some(client) = self.get_client() else { + return; + }; + let mut state = client.0.borrow_mut(); + + if let Some(window_ref) = state.windows.remove(&x_window) + && let Some(RefreshState::PeriodicRefresh { + event_loop_token, .. + }) = window_ref.refresh_state + { + state.loop_handle.remove(event_loop_token); + } + if state.mouse_focused_window == Some(x_window) { + state.mouse_focused_window = None; + } + if state.keyboard_focused_window == Some(x_window) { + state.keyboard_focused_window = None; + } + if state.cursor_hidden_window == Some(x_window) { + state.cursor_hidden_window = None; + } + state.cursor_styles.remove(&x_window); + } + + pub fn update_ime_position(&self, bounds: Bounds) { + let Some(client) = self.get_client() else { + return; + }; + let mut state = client.0.borrow_mut(); + if state.composing || state.ximc.is_none() { + return; + } + + let Some(mut ximc) = state.ximc.take() else { + log::error!("bug: xim connection not set"); + return; + }; + let Some(xim_handler) = state.xim_handler.take() else { + log::error!("bug: xim handler not set"); + state.ximc = Some(ximc); + return; + }; + let scaled_bounds = bounds.scale(state.scale_factor); + let ic_attributes = ximc + .build_ic_attributes() + .push( + xim::AttributeName::InputStyle, + xim::InputStyle::PREEDIT_CALLBACKS, + ) + .push(xim::AttributeName::ClientWindow, xim_handler.window) + .push(xim::AttributeName::FocusWindow, xim_handler.window) + .nested_list(xim::AttributeName::PreeditAttributes, |b| { + b.push( + xim::AttributeName::SpotLocation, + xim::Point { + x: u32::from(scaled_bounds.origin.x + scaled_bounds.size.width) as i16, + y: u32::from(scaled_bounds.origin.y + scaled_bounds.size.height) as i16, + }, + ); + }) + .build(); + let _ = ximc + .set_ic_values(xim_handler.im_id, xim_handler.ic_id, ic_attributes) + .log_err(); + state.ximc = Some(ximc); + state.xim_handler = Some(xim_handler); + } +} + +#[derive(Clone)] +pub(crate) struct X11Client(pub(crate) Rc>); + +impl X11Client { + pub(crate) fn new() -> anyhow::Result { + let event_loop = EventLoop::try_new()?; + + let (common, main_receiver, power_receiver) = LinuxCommon::new(event_loop.get_signal()); + + let handle = event_loop.handle(); + + handle + .insert_source(main_receiver, { + let handle = handle.clone(); + move |event, _, _: &mut X11Client| { + if let calloop::channel::Event::Msg(runnable) = event { + // Insert the runnables as idle callbacks, so we make sure that user-input and X11 + // events have higher priority and runnables are only worked off after the event + // callbacks. + handle.insert_idle(|client| { + let location = runnable.metadata().location; + let spawned = runnable.metadata().spawned; + profiler::update_running_task(spawned, location); + runnable.run(); + profiler::save_task_timing(); + + let xcb_connection = client.0.borrow().xcb_connection.clone(); + client.process_x11_events(&xcb_connection).log_err(); + }); + } + } + }) + .map_err(|err| { + anyhow!("Failed to initialize event loop handling of foreground tasks: {err:?}") + })?; + + handle + .insert_source(power_receiver, |event, _, client: &mut X11Client| { + if let calloop::channel::Event::Msg(event) = event { + client + .0 + .borrow_mut() + .common + .handle_system_power_event(event); + } + }) + .map_err(|err| { + anyhow!("Failed to initialize event loop handling of sleep/wake events: {err:?}") + })?; + + let (xcb_connection, x_root_index) = XCBConnection::connect(None)?; + xcb_connection.prefetch_extension_information(xkb::X11_EXTENSION_NAME)?; + xcb_connection.prefetch_extension_information(randr::X11_EXTENSION_NAME)?; + xcb_connection.prefetch_extension_information(render::X11_EXTENSION_NAME)?; + xcb_connection.prefetch_extension_information(xinput::X11_EXTENSION_NAME)?; + + // Announce to X server that XInput up to 2.4 is supported. + // Version 2.4 is needed for gesture events (GesturePinchBegin/Update/End). + // The server responds with the highest version it supports; if < 2.4, + // we must not request gesture event masks in XISelectEvents. + let xinput_version = get_reply( + || "XInput XiQueryVersion failed", + xcb_connection.xinput_xi_query_version(2, 4), + )?; + assert!( + xinput_version.major_version >= 2, + "XInput version >= 2 required." + ); + let supports_xinput_gestures = xinput_version.major_version > 2 + || (xinput_version.major_version == 2 && xinput_version.minor_version >= 4); + log::info!( + "XInput version: {}.{}, gesture support: {}", + xinput_version.major_version, + xinput_version.minor_version, + supports_xinput_gestures, + ); + + let pointer_device_states = + current_pointer_device_states(&xcb_connection, &BTreeMap::new()).unwrap_or_default(); + + let atoms = XcbAtoms::new(&xcb_connection) + .context("Failed to get XCB atoms")? + .reply() + .context("Failed to get XCB atoms")?; + + let root = xcb_connection.setup().roots[0].root; + let compositor_present = check_compositor_present(&xcb_connection, root); + let gtk_frame_extents_supported = + check_gtk_frame_extents_supported(&xcb_connection, &atoms, root); + let client_side_decorations_supported = compositor_present && gtk_frame_extents_supported; + log::info!( + "x11: compositor present: {}, gtk_frame_extents_supported: {}", + compositor_present, + gtk_frame_extents_supported + ); + + let xkb = get_reply( + || "Failed to initialize XKB extension", + xcb_connection + .xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION), + )?; + assert!(xkb.supported); + + let events = xkb::EventType::STATE_NOTIFY + | xkb::EventType::MAP_NOTIFY + | xkb::EventType::NEW_KEYBOARD_NOTIFY; + let map_notify_parts = xkb::MapPart::KEY_TYPES + | xkb::MapPart::KEY_SYMS + | xkb::MapPart::MODIFIER_MAP + | xkb::MapPart::EXPLICIT_COMPONENTS + | xkb::MapPart::KEY_ACTIONS + | xkb::MapPart::KEY_BEHAVIORS + | xkb::MapPart::VIRTUAL_MODS + | xkb::MapPart::VIRTUAL_MOD_MAP; + check_reply( + || "Failed to select XKB events", + xcb_connection.xkb_select_events( + xkb::ID::USE_CORE_KBD.into(), + 0u8.into(), + events, + map_notify_parts, + map_notify_parts, + &xkb::SelectEventsAux::new(), + ), + )?; + + let xkb_context = new_xkb_context()?; + let xkb_device_id = xkbc::x11::get_core_keyboard_device_id(&xcb_connection); + let xkb_state = { + let xkb_keymap = xkbc::x11::keymap_new_from_device( + &xkb_context, + &xcb_connection, + xkb_device_id, + xkbc::KEYMAP_COMPILE_NO_FLAGS, + ); + xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id) + }; + let compose_state = get_xkb_compose_state(&xkb_context); + let layout_idx = xkb_state.serialize_layout(STATE_LAYOUT_EFFECTIVE); + let layout_name = xkb_state + .get_keymap() + .layout_get_name(layout_idx) + .to_string(); + let keyboard_layout = LinuxKeyboardLayout::new(layout_name.into()); + + let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection) + .context("Failed to create resource database")?; + let scale_factor = get_scale_factor(&xcb_connection, &resource_database, x_root_index); + let cursor_handle = cursor::Handle::new(&xcb_connection, x_root_index, &resource_database) + .context("Failed to initialize cursor theme handler")? + .reply() + .context("Failed to initialize cursor theme handler")?; + + let clipboard = Clipboard::new().context("Failed to initialize clipboard")?; + + let screen = &xcb_connection.setup().roots[x_root_index]; + let compositor_gpu = detect_compositor_gpu(&xcb_connection, screen); + + let xcb_connection = Rc::new(xcb_connection); + + let ximc = X11rbClient::init(Rc::clone(&xcb_connection), x_root_index, None).ok(); + let xim_handler = if ximc.is_some() { + Some(XimHandler::new()) + } else { + None + }; + + // Safety: Safe if xcb::Connection always returns a valid fd + let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) }; + + handle + .insert_source( + Generic::new_with_error::( + fd, + calloop::Interest::READ, + calloop::Mode::Level, + ), + { + let xcb_connection = xcb_connection.clone(); + move |_readiness, _, client| { + client.process_x11_events(&xcb_connection)?; + Ok(calloop::PostAction::Continue) + } + }, + ) + .map_err(|err| anyhow!("Failed to initialize X11 event source: {err:?}"))?; + + handle + .insert_source(XDPEventSource::new(&common.background_executor), { + move |event, _, client| match event { + XDPEvent::WindowAppearance(appearance) => { + client.with_common(|common| common.appearance = appearance); + for window in client.0.borrow_mut().windows.values_mut() { + window.window.set_appearance(appearance); + } + } + XDPEvent::ButtonLayout(layout_str) => { + let layout = WindowButtonLayout::parse(&layout_str) + .log_err() + .unwrap_or_else(WindowButtonLayout::linux_default); + client.with_common(|common| common.button_layout = layout); + for window in client.0.borrow_mut().windows.values_mut() { + window.window.set_button_layout(); + } + } + XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => { + // noop, X11 manages this for us. + } + } + }) + .map_err(|err| anyhow!("Failed to initialize XDP event source: {err:?}"))?; + + xcb_flush(&xcb_connection); + + Ok(X11Client(Rc::new(RefCell::new(X11ClientState { + modifiers: Modifiers::default(), + capslock: Capslock::default(), + last_modifiers_changed_event: Modifiers::default(), + last_capslock_changed_event: Capslock::default(), + event_loop: Some(event_loop), + loop_handle: handle, + common, + last_click: Instant::now(), + last_mouse_button: None, + last_location: Point::new(px(0.0), px(0.0)), + current_count: 0, + pinch_scale: 1.0, + gpu_context: Rc::new(RefCell::new(None)), + compositor_gpu, + scale_factor, + + xkb_context, + xcb_connection, + xkb_device_id, + client_side_decorations_supported, + x_root_index, + resource_database, + atoms, + windows: HashMap::default(), + mouse_focused_window: None, + keyboard_focused_window: None, + xkb: xkb_state, + keyboard_layout, + ximc, + xim_handler, + + compose_state, + pre_edit_text: None, + pre_key_char_down: None, + composing: false, + + cursor_handle, + cursor_styles: HashMap::default(), + cursor_cache: HashMap::default(), + cursor_hidden_window: None, + invisible_cursor_cache: None, + + pointer_device_states, + + supports_xinput_gestures, + + clipboard, + clipboard_item: None, + xdnd_state: Xdnd::default(), + })))) + } + + pub fn process_x11_events( + &self, + xcb_connection: &XCBConnection, + ) -> Result<(), EventHandlerError> { + loop { + let mut events = Vec::new(); + let mut windows_to_refresh = HashSet::new(); + + let mut last_key_release = None; + + // event handlers for new keyboard / remapping refresh the state without using event + // details, this deduplicates them. + let mut last_keymap_change_event: Option = None; + + loop { + match xcb_connection.poll_for_event() { + Ok(Some(event)) => { + match event { + Event::Expose(expose_event) => { + windows_to_refresh.insert(expose_event.window); + } + Event::KeyRelease(_) => { + if let Some(last_keymap_change_event) = + last_keymap_change_event.take() + { + if let Some(last_key_release) = last_key_release.take() { + events.push(last_key_release); + } + events.push(last_keymap_change_event); + } + + last_key_release = Some(event); + } + Event::KeyPress(key_press) => { + if let Some(last_keymap_change_event) = + last_keymap_change_event.take() + { + if let Some(last_key_release) = last_key_release.take() { + events.push(last_key_release); + } + events.push(last_keymap_change_event); + } + + if let Some(Event::KeyRelease(key_release)) = + last_key_release.take() + { + // We ignore that last KeyRelease if it's too close to this KeyPress, + // suggesting that it's auto-generated by X11 as a key-repeat event. + if key_release.detail != key_press.detail + || key_press.time.saturating_sub(key_release.time) > 20 + { + events.push(Event::KeyRelease(key_release)); + } + } + events.push(Event::KeyPress(key_press)); + } + Event::XkbNewKeyboardNotify(_) | Event::XkbMapNotify(_) => { + if let Some(release_event) = last_key_release.take() { + events.push(release_event); + } + last_keymap_change_event = Some(event); + } + _ => { + if let Some(release_event) = last_key_release.take() { + events.push(release_event); + } + events.push(event); + } + } + } + Ok(None) => { + break; + } + Err(err @ ConnectionError::IoError(..)) => { + return Err(EventHandlerError::from(err)); + } + Err(err) => { + let err = handle_connection_error(err); + log::warn!("error while polling for X11 events: {err:?}"); + break; + } + } + } + + if let Some(release_event) = last_key_release.take() { + events.push(release_event); + } + if let Some(keymap_change_event) = last_keymap_change_event.take() { + events.push(keymap_change_event); + } + + if events.is_empty() && windows_to_refresh.is_empty() { + break; + } + + for event in events.into_iter() { + let mut state = self.0.borrow_mut(); + if !state.has_xim() { + drop(state); + self.handle_event(event); + continue; + } + + let Some((mut ximc, mut xim_handler)) = state.take_xim() else { + continue; + }; + let xim_connected = xim_handler.connected; + drop(state); + + let xim_filtered = ximc.filter_event(&event, &mut xim_handler); + let xim_callback_event = xim_handler.last_callback_event.take(); + + let mut state = self.0.borrow_mut(); + state.restore_xim(ximc, xim_handler); + drop(state); + + if let Some(event) = xim_callback_event { + self.handle_xim_callback_event(event); + } + + match xim_filtered { + Ok(handled) => { + if handled { + continue; + } + if xim_connected { + self.xim_handle_event(event); + } else { + self.handle_event(event); + } + } + Err(err) => { + // this might happen when xim server crashes on one of the events + // we do lose 1-2 keys when crash happens since there is no reliable way to get that info + // luckily, x11 sends us window not found error when xim server crashes upon further key press + // hence we fall back to handle_event + log::error!("XIMClientError: {}", err); + let mut state = self.0.borrow_mut(); + state.take_xim(); + drop(state); + self.handle_event(event); + } + } + } + + for x_window in windows_to_refresh { + let window = self + .0 + .borrow() + .windows + .get(&x_window) + .and_then(|window| window.is_mapped.then(|| window.window.clone())); + if let Some(window) = window { + window.refresh(RequestFrameOptions { + require_presentation: true, + force_render: false, + }); + } + } + } + Ok(()) + } + + pub fn enable_ime(&self) { + let mut state = self.0.borrow_mut(); + if !state.has_xim() { + return; + } + + let Some((mut ximc, xim_handler)) = state.take_xim() else { + return; + }; + let mut ic_attributes = ximc + .build_ic_attributes() + .push(AttributeName::InputStyle, InputStyle::PREEDIT_CALLBACKS) + .push(AttributeName::ClientWindow, xim_handler.window) + .push(AttributeName::FocusWindow, xim_handler.window); + + let window_id = state.keyboard_focused_window; + drop(state); + if let Some(window_id) = window_id { + let Some(window) = self.get_window(window_id) else { + log::error!("Failed to get window for IME positioning"); + let mut state = self.0.borrow_mut(); + state.ximc = Some(ximc); + state.xim_handler = Some(xim_handler); + return; + }; + if let Some(scaled_area) = window.get_ime_area() { + ic_attributes = + ic_attributes.nested_list(xim::AttributeName::PreeditAttributes, |b| { + b.push( + xim::AttributeName::SpotLocation, + xim::Point { + x: u32::from(scaled_area.origin.x + scaled_area.size.width) as i16, + y: u32::from(scaled_area.origin.y + scaled_area.size.height) as i16, + }, + ); + }); + } + } + ximc.create_ic(xim_handler.im_id, ic_attributes.build()) + .ok(); + let mut state = self.0.borrow_mut(); + state.restore_xim(ximc, xim_handler); + } + + pub fn reset_ime(&self) { + let mut state = self.0.borrow_mut(); + state.composing = false; + if let Some(mut ximc) = state.ximc.take() { + if let Some(xim_handler) = state.xim_handler.as_ref() { + ximc.reset_ic(xim_handler.im_id, xim_handler.ic_id).ok(); + } else { + log::error!("bug: xim handler not set in reset_ime"); + } + state.ximc = Some(ximc); + } + } + + pub(crate) fn get_window(&self, win: xproto::Window) -> Option { + let state = self.0.borrow(); + state + .windows + .get(&win) + .filter(|window_reference| !window_reference.window.state.borrow().destroyed) + .map(|window_reference| window_reference.window.clone()) + } + + fn handle_event(&self, event: Event) -> Option<()> { + match event { + Event::UnmapNotify(event) => { + let mut state = self.0.borrow_mut(); + if let Some(window_ref) = state.windows.get_mut(&event.window) { + window_ref.is_mapped = false; + } + handle_visibility_changed(state, event.window); + } + Event::MapNotify(event) => { + let mut state = self.0.borrow_mut(); + if let Some(window_ref) = state.windows.get_mut(&event.window) { + window_ref.is_mapped = true; + } + handle_visibility_changed(state, event.window); + } + Event::VisibilityNotify(event) => { + let mut state = self.0.borrow_mut(); + if let Some(window_ref) = state.windows.get_mut(&event.window) { + window_ref.last_visibility = event.state; + } + handle_visibility_changed(state, event.window); + } + Event::ClientMessage(event) => { + let window = self.get_window(event.window)?; + let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32(); + let delete_window_atom = self.0.borrow().atoms.WM_DELETE_WINDOW; + + if atom == delete_window_atom { + if window.should_close() { + // window "x" button clicked by user + // Rest of the close logic is handled in drop_window() + window.close(); + } + return Some(()); + } + + let mut state = self.0.borrow_mut(); + if atom == state.atoms._NET_WM_SYNC_REQUEST { + window.state.borrow_mut().last_sync_counter = + Some(x11rb::protocol::sync::Int64 { + lo: arg2, + hi: arg3 as i32, + }) + } + + if event.type_ == state.atoms.XdndEnter { + state.xdnd_state.other_window = atom; + if (arg1 & 0x1) == 0x1 { + state.xdnd_state.drag_type = xdnd_get_supported_atom( + &state.xcb_connection, + &state.atoms, + state.xdnd_state.other_window, + ); + } else { + if let Some(atom) = [arg2, arg3, arg4] + .into_iter() + .find(|atom| xdnd_is_atom_supported(*atom, &state.atoms)) + { + state.xdnd_state.drag_type = atom; + } + } + } else if event.type_ == state.atoms.XdndLeave { + let position = state.xdnd_state.position; + drop(state); + window + .handle_input(PlatformInput::FileDrop(FileDropEvent::Pending { position })); + window.handle_input(PlatformInput::FileDrop(FileDropEvent::Exited {})); + self.0.borrow_mut().xdnd_state = Xdnd::default(); + } else if event.type_ == state.atoms.XdndPosition { + if let Ok(pos) = get_reply( + || "Failed to query pointer position", + state.xcb_connection.query_pointer(event.window), + ) { + state.xdnd_state.position = + Point::new(px(pos.win_x as f32), px(pos.win_y as f32)); + } + if !state.xdnd_state.retrieved { + check_reply( + || "Failed to convert selection for drag and drop", + state.xcb_connection.convert_selection( + event.window, + state.atoms.XdndSelection, + state.xdnd_state.drag_type, + state.atoms.XDND_DATA, + arg3, + ), + ) + .log_err(); + } + xdnd_send_status( + &state.xcb_connection, + &state.atoms, + event.window, + state.xdnd_state.other_window, + arg4, + ); + let position = state.xdnd_state.position; + drop(state); + window + .handle_input(PlatformInput::FileDrop(FileDropEvent::Pending { position })); + } else if event.type_ == state.atoms.XdndDrop { + xdnd_send_finished( + &state.xcb_connection, + &state.atoms, + event.window, + state.xdnd_state.other_window, + ); + let position = state.xdnd_state.position; + drop(state); + window + .handle_input(PlatformInput::FileDrop(FileDropEvent::Submit { position })); + self.0.borrow_mut().xdnd_state = Xdnd::default(); + } + } + Event::SelectionNotify(event) => { + let window = self.get_window(event.requestor)?; + let state = self.0.borrow_mut(); + let reply = get_reply( + || "Failed to get XDND_DATA", + state.xcb_connection.get_property( + false, + event.requestor, + state.atoms.XDND_DATA, + AtomEnum::ANY, + 0, + 1024, + ), + ) + .log_err(); + let Some(reply) = reply else { + return Some(()); + }; + if let Ok(file_list) = str::from_utf8(&reply.value) { + let paths: SmallVec<[_; 2]> = file_list + .lines() + .filter_map(|path| Url::parse(path).log_err()) + .filter_map(|url| match url.to_file_path() { + Ok(url) => Some(url), + Err(()) => { + log::error!("Failed turn {url:?} into a file path"); + None + } + }) + .collect(); + let input = PlatformInput::FileDrop(FileDropEvent::Entered { + position: state.xdnd_state.position, + paths: gpui::ExternalPaths(paths), + }); + drop(state); + window.handle_input(input); + self.0.borrow_mut().xdnd_state.retrieved = true; + } + } + Event::ConfigureNotify(event) => { + let bounds = Bounds { + origin: Point { + x: event.x.into(), + y: event.y.into(), + }, + size: Size { + width: event.width.into(), + height: event.height.into(), + }, + }; + let window = self.get_window(event.window)?; + window + .set_bounds(bounds) + .context("X11: Failed to set window bounds") + .log_err(); + } + Event::PropertyNotify(event) => { + let window = self.get_window(event.window)?; + window + .property_notify(event) + .context("X11: Failed to handle property notify") + .log_err(); + } + Event::FocusIn(event) => { + let window = self.get_window(event.event)?; + window.set_active(true); + let mut state = self.0.borrow_mut(); + state.keyboard_focused_window = Some(event.event); + if let Some(handler) = state.xim_handler.as_mut() { + handler.window = event.event; + } + drop(state); + self.enable_ime(); + } + Event::FocusOut(event) => { + let window = self.get_window(event.event)?; + window.set_active(false); + let mut state = self.0.borrow_mut(); + // Set last scroll values to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global) + reset_all_pointer_device_scroll_positions(&mut state.pointer_device_states); + state.keyboard_focused_window = None; + if let Some(compose_state) = state.compose_state.as_mut() { + compose_state.reset(); + } + state.pre_edit_text.take(); + state.restore_cursor_after_hide(); + drop(state); + self.reset_ime(); + window.handle_ime_delete(); + } + Event::XkbNewKeyboardNotify(_) | Event::XkbMapNotify(_) => { + let mut state = self.0.borrow_mut(); + let xkb_state = { + let xkb_keymap = xkbc::x11::keymap_new_from_device( + &state.xkb_context, + &state.xcb_connection, + state.xkb_device_id, + xkbc::KEYMAP_COMPILE_NO_FLAGS, + ); + xkbc::x11::state_new_from_device( + &xkb_keymap, + &state.xcb_connection, + state.xkb_device_id, + ) + }; + state.xkb = xkb_state; + drop(state); + self.handle_keyboard_layout_change(); + } + Event::XkbStateNotify(event) => { + let mut state = self.0.borrow_mut(); + let old_layout = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE); + let new_layout = u32::from(event.group); + state.xkb.update_mask( + event.base_mods.into(), + event.latched_mods.into(), + event.locked_mods.into(), + event.base_group as u32, + event.latched_group as u32, + event.locked_group.into(), + ); + let modifiers = modifiers_from_xkb(&state.xkb); + let capslock = capslock_from_xkb(&state.xkb); + if state.last_modifiers_changed_event == modifiers + && state.last_capslock_changed_event == capslock + { + drop(state); + } else { + let focused_window_id = state.keyboard_focused_window?; + state.modifiers = modifiers; + state.last_modifiers_changed_event = modifiers; + state.capslock = capslock; + state.last_capslock_changed_event = capslock; + drop(state); + + let focused_window = self.get_window(focused_window_id)?; + focused_window.handle_input(PlatformInput::ModifiersChanged( + ModifiersChangedEvent { + modifiers, + capslock, + }, + )); + } + + if new_layout != old_layout { + self.handle_keyboard_layout_change(); + } + } + Event::KeyPress(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + + let modifiers = modifiers_from_state(event.state); + state.modifiers = modifiers; + state.pre_key_char_down.take(); + let key_event_state = xkb_state_for_key_event(&state.xkb, event.state); + + let keystroke = { + let code = event.detail.into(); + let mut keystroke = keystroke_from_xkb(&key_event_state, modifiers, code); + let keysym = key_event_state.key_get_one_sym(code); + + if keysym.is_modifier_key() { + return Some(()); + } + + if let Some(mut compose_state) = state.compose_state.take() { + compose_state.feed(keysym); + match compose_state.status() { + xkbc::Status::Composed => { + state.pre_edit_text.take(); + keystroke.key_char = compose_state.utf8(); + if let Some(keysym) = compose_state.keysym() { + keystroke.key = xkbc::keysym_get_name(keysym); + } + } + xkbc::Status::Composing => { + keystroke.key_char = None; + state.pre_edit_text = compose_state + .utf8() + .or(keystroke_underlying_dead_key(keysym)); + let pre_edit = + state.pre_edit_text.clone().unwrap_or(String::default()); + drop(state); + window.handle_ime_preedit(pre_edit); + state = self.0.borrow_mut(); + } + xkbc::Status::Cancelled => { + let pre_edit = state.pre_edit_text.take(); + drop(state); + if let Some(pre_edit) = pre_edit { + window.handle_ime_commit(pre_edit); + } + if let Some(current_key) = keystroke_underlying_dead_key(keysym) { + window.handle_ime_preedit(current_key); + } + state = self.0.borrow_mut(); + compose_state.feed(keysym); + } + _ => {} + } + state.compose_state = Some(compose_state); + } + keystroke + }; + drop(state); + window.handle_input(PlatformInput::KeyDown(gpui::KeyDownEvent { + keystroke, + is_held: false, + prefer_character_input: false, + })); + } + Event::KeyRelease(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + + let modifiers = modifiers_from_state(event.state); + state.modifiers = modifiers; + let key_event_state = xkb_state_for_key_event(&state.xkb, event.state); + + let keystroke = { + let code = event.detail.into(); + let keystroke = keystroke_from_xkb(&key_event_state, modifiers, code); + let keysym = key_event_state.key_get_one_sym(code); + + if keysym.is_modifier_key() { + return Some(()); + } + + keystroke + }; + drop(state); + window.handle_input(PlatformInput::KeyUp(gpui::KeyUpEvent { keystroke })); + } + Event::XinputButtonPress(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + + if state.composing && state.ximc.is_some() { + drop(state); + self.reset_ime(); + window.handle_ime_unmark(); + state = self.0.borrow_mut(); + } else if let Some(text) = state.pre_edit_text.take() { + if let Some(compose_state) = state.compose_state.as_mut() { + compose_state.reset(); + } + drop(state); + window.handle_ime_commit(text); + state = self.0.borrow_mut(); + } + match button_or_scroll_from_event_detail(event.detail) { + Some(ButtonOrScroll::Button(button)) => { + let click_elapsed = state.last_click.elapsed(); + if click_elapsed < DOUBLE_CLICK_INTERVAL + && state + .last_mouse_button + .is_some_and(|prev_button| prev_button == button) + && is_within_click_distance(state.last_location, position) + { + state.current_count += 1; + } else { + state.current_count = 1; + } + + state.last_click = Instant::now(); + state.last_mouse_button = Some(button); + state.last_location = position; + let current_count = state.current_count; + + drop(state); + window.handle_input(PlatformInput::MouseDown(gpui::MouseDownEvent { + button, + position, + modifiers, + click_count: current_count, + first_mouse: false, + })); + } + Some(ButtonOrScroll::Scroll(direction)) => { + drop(state); + // Emulated scroll button presses are sent simultaneously with smooth scrolling XinputMotion events. + // Since handling those events does the scrolling, they are skipped here. + if !event + .flags + .contains(xinput::PointerEventFlags::POINTER_EMULATED) + { + let scroll_delta = match direction { + ScrollDirection::Up => Point::new(0.0, SCROLL_LINES), + ScrollDirection::Down => Point::new(0.0, -SCROLL_LINES), + ScrollDirection::Left => Point::new(SCROLL_LINES, 0.0), + ScrollDirection::Right => Point::new(-SCROLL_LINES, 0.0), + }; + window.handle_input(PlatformInput::ScrollWheel( + make_scroll_wheel_event(position, scroll_delta, modifiers), + )); + } + } + None => { + log::error!("Unknown x11 button: {}", event.detail); + } + } + } + Event::XinputButtonRelease(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + match button_or_scroll_from_event_detail(event.detail) { + Some(ButtonOrScroll::Button(button)) => { + let click_count = state.current_count; + drop(state); + window.handle_input(PlatformInput::MouseUp(gpui::MouseUpEvent { + button, + position, + modifiers, + click_count, + })); + } + Some(ButtonOrScroll::Scroll(_)) => {} + None => {} + } + } + Event::XinputMotion(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + state.restore_cursor_after_hide(); + if window.is_blocked() { + // We want to set the cursor to the default arrow + // when the window is blocked + let style = CursorStyle::Arrow; + + let current_style = state + .cursor_styles + .get(&window.x_window) + .unwrap_or(&CursorStyle::Arrow); + if *current_style != style + && let Some(cursor) = state.get_cursor_icon(style) + { + state.cursor_styles.insert(window.x_window, style); + check_reply( + || "Failed to set cursor style", + state.xcb_connection.change_window_attributes( + window.x_window, + &ChangeWindowAttributesAux { + cursor: Some(cursor), + ..Default::default() + }, + ), + ) + .log_err(); + state.xcb_connection.flush().log_err(); + }; + } + let pressed_button = pressed_button_from_mask(event.button_mask[0]); + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + drop(state); + + if event.valuator_mask[0] & 3 != 0 { + window.handle_input(PlatformInput::MouseMove(gpui::MouseMoveEvent { + position, + pressed_button, + modifiers, + })); + } + + state = self.0.borrow_mut(); + if let Some(pointer) = state.pointer_device_states.get_mut(&event.sourceid) { + let scroll_delta = get_scroll_delta_and_update_state(pointer, &event); + drop(state); + if let Some(scroll_delta) = scroll_delta { + window.handle_input(PlatformInput::ScrollWheel(make_scroll_wheel_event( + position, + scroll_delta, + modifiers, + ))); + } + } + } + Event::XinputEnter(event) if event.mode == xinput::NotifyMode::NORMAL => { + let window = self.get_window(event.event)?; + window.set_hovered(true); + let mut state = self.0.borrow_mut(); + state.mouse_focused_window = Some(event.event); + state.restore_cursor_after_hide(); + } + Event::XinputLeave(event) if event.mode == xinput::NotifyMode::NORMAL => { + let mut state = self.0.borrow_mut(); + + // Set last scroll values to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global) + reset_all_pointer_device_scroll_positions(&mut state.pointer_device_states); + state.mouse_focused_window = None; + let pressed_button = pressed_button_from_mask(event.buttons[0]); + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + drop(state); + + let window = self.get_window(event.event)?; + window.handle_input(PlatformInput::MouseExited(gpui::MouseExitEvent { + pressed_button, + position, + modifiers, + })); + window.set_hovered(false); + } + Event::XinputHierarchy(event) => { + let mut state = self.0.borrow_mut(); + // Temporarily use `state.pointer_device_states` to only store pointers that still have valid scroll values. + // Any change to a device invalidates its scroll values. + for info in event.infos { + if is_pointer_device(info.type_) { + state.pointer_device_states.remove(&info.deviceid); + } + } + if let Some(pointer_device_states) = current_pointer_device_states( + &state.xcb_connection, + &state.pointer_device_states, + ) { + state.pointer_device_states = pointer_device_states; + } + } + Event::XinputDeviceChanged(event) => { + let mut state = self.0.borrow_mut(); + if let Some(pointer) = state.pointer_device_states.get_mut(&event.sourceid) { + reset_pointer_device_scroll_positions(pointer); + } + } + Event::XinputGesturePinchBegin(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + state.pinch_scale = 1.0; + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + drop(state); + window.handle_input(PlatformInput::Pinch(gpui::PinchEvent { + position, + delta: 0.0, + modifiers, + phase: gpui::TouchPhase::Started, + })); + } + Event::XinputGesturePinchUpdate(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + // scale is in FP16.16 format: divide by 65536 to get the float value + let new_absolute_scale = event.scale as f32 / 65536.0; + let previous_scale = state.pinch_scale; + let zoom_delta = new_absolute_scale - previous_scale; + state.pinch_scale = new_absolute_scale; + drop(state); + window.handle_input(PlatformInput::Pinch(gpui::PinchEvent { + position, + delta: zoom_delta, + modifiers, + phase: gpui::TouchPhase::Moved, + })); + } + Event::XinputGesturePinchEnd(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + state.pinch_scale = 1.0; + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + drop(state); + window.handle_input(PlatformInput::Pinch(gpui::PinchEvent { + position, + delta: 0.0, + modifiers, + phase: gpui::TouchPhase::Ended, + })); + } + _ => {} + }; + + Some(()) + } + + fn handle_xim_callback_event(&self, event: XimCallbackEvent) { + match event { + XimCallbackEvent::XimXEvent(event) => { + self.handle_event(event); + } + XimCallbackEvent::XimCommitEvent(window, text) => { + self.xim_handle_commit(window, text); + } + XimCallbackEvent::XimPreeditEvent(window, text) => { + self.xim_handle_preedit(window, text); + } + }; + } + + fn xim_handle_event(&self, event: Event) -> Option<()> { + match event { + Event::KeyPress(event) | Event::KeyRelease(event) => { + let mut state = self.0.borrow_mut(); + state.pre_key_char_down = Some(keystroke_from_xkb( + &state.xkb, + state.modifiers, + event.detail.into(), + )); + let (mut ximc, mut xim_handler) = state.take_xim()?; + drop(state); + xim_handler.window = event.event; + ximc.forward_event( + xim_handler.im_id, + xim_handler.ic_id, + xim::ForwardEventFlag::empty(), + &event, + ) + .context("X11: Failed to forward XIM event") + .log_err(); + let mut state = self.0.borrow_mut(); + state.restore_xim(ximc, xim_handler); + drop(state); + } + event => { + self.handle_event(event); + } + } + Some(()) + } + + fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> { + let Some(window) = self.get_window(window) else { + log::error!("bug: Failed to get window for XIM commit"); + return None; + }; + let mut state = self.0.borrow_mut(); + state.composing = false; + drop(state); + window.handle_ime_commit(text); + Some(()) + } + + fn xim_handle_preedit(&self, window: xproto::Window, text: String) -> Option<()> { + let Some(window) = self.get_window(window) else { + log::error!("bug: Failed to get window for XIM preedit"); + return None; + }; + + let mut state = self.0.borrow_mut(); + let (mut ximc, xim_handler) = state.take_xim()?; + state.composing = !text.is_empty(); + drop(state); + window.handle_ime_preedit(text); + + if let Some(scaled_area) = window.get_ime_area() { + let ic_attributes = ximc + .build_ic_attributes() + .push( + xim::AttributeName::InputStyle, + xim::InputStyle::PREEDIT_CALLBACKS, + ) + .push(xim::AttributeName::ClientWindow, xim_handler.window) + .push(xim::AttributeName::FocusWindow, xim_handler.window) + .nested_list(xim::AttributeName::PreeditAttributes, |b| { + b.push( + xim::AttributeName::SpotLocation, + xim::Point { + x: u32::from(scaled_area.origin.x + scaled_area.size.width) as i16, + y: u32::from(scaled_area.origin.y + scaled_area.size.height) as i16, + }, + ); + }) + .build(); + ximc.set_ic_values(xim_handler.im_id, xim_handler.ic_id, ic_attributes) + .ok(); + } + let mut state = self.0.borrow_mut(); + state.restore_xim(ximc, xim_handler); + drop(state); + Some(()) + } + + fn handle_keyboard_layout_change(&self) { + let mut state = self.0.borrow_mut(); + let layout_idx = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE); + let keymap = state.xkb.get_keymap(); + let layout_name = keymap.layout_get_name(layout_idx); + if layout_name != state.keyboard_layout.name() { + state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into()); + if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() { + drop(state); + callback(); + state = self.0.borrow_mut(); + state.common.callbacks.keyboard_layout_change = Some(callback); + } + } + } +} + +impl LinuxClient for X11Client { + fn compositor_name(&self) -> &'static str { + "X11" + } + + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { + f(&mut self.0.borrow_mut().common) + } + + fn keyboard_layout(&self) -> Box { + let state = self.0.borrow(); + Box::new(state.keyboard_layout.clone()) + } + + fn displays(&self) -> Vec> { + let state = self.0.borrow(); + let setup = state.xcb_connection.setup(); + setup + .roots + .iter() + .enumerate() + .filter_map(|(root_id, _)| { + Some(Rc::new( + X11Display::new(&state.xcb_connection, state.scale_factor, root_id).ok()?, + ) as Rc) + }) + .collect() + } + + fn primary_display(&self) -> Option> { + let state = self.0.borrow(); + X11Display::new( + &state.xcb_connection, + state.scale_factor, + state.x_root_index, + ) + .log_err() + .map(|display| Rc::new(display) as Rc) + } + + fn display(&self, id: DisplayId) -> Option> { + let state = self.0.borrow(); + + Some(Rc::new( + X11Display::new( + &state.xcb_connection, + state.scale_factor, + u64::from(id) as usize, + ) + .ok()?, + )) + } + + #[cfg(feature = "screen-capture")] + fn is_screen_capture_supported(&self) -> bool { + true + } + + #[cfg(feature = "screen-capture")] + fn screen_capture_sources( + &self, + ) -> futures::channel::oneshot::Receiver>>> + { + gpui::scap_screen_capture::scap_screen_sources(&self.0.borrow().common.foreground_executor) + } + + fn open_window( + &self, + handle: AnyWindowHandle, + params: WindowParams, + ) -> anyhow::Result> { + let mut state = self.0.borrow_mut(); + let parent_window = state + .keyboard_focused_window + .and_then(|focused_window| state.windows.get(&focused_window)) + .map(|w| w.window.clone()); + let x_window = state + .xcb_connection + .generate_id() + .context("X11: Failed to generate window ID")?; + + let xcb_connection = state.xcb_connection.clone(); + let client_side_decorations_supported = state.client_side_decorations_supported; + let x_root_index = state.x_root_index; + let atoms = state.atoms; + let scale_factor = state.scale_factor; + let appearance = state.common.appearance; + let compositor_gpu = state.compositor_gpu.take(); + let supports_xinput_gestures = state.supports_xinput_gestures; + let is_bgr = state + .resource_database + .get_string("Xft.rgba", "Xft.Rgba") + .is_some_and(|v| v.eq_ignore_ascii_case("bgr")); + let window = X11Window::new( + handle, + X11ClientStatePtr(Rc::downgrade(&self.0)), + state.common.foreground_executor.clone(), + state.gpu_context.clone(), + compositor_gpu, + params, + &xcb_connection, + client_side_decorations_supported, + x_root_index, + x_window, + &atoms, + scale_factor, + appearance, + parent_window, + supports_xinput_gestures, + is_bgr, + )?; + check_reply( + || "Failed to set XdndAware property", + state.xcb_connection.change_property32( + xproto::PropMode::REPLACE, + x_window, + state.atoms.XdndAware, + state.atoms.XA_ATOM, + &[5], + ), + ) + .log_err(); + xcb_flush(&state.xcb_connection); + + let window_ref = WindowRef { + window: window.0.clone(), + refresh_state: None, + last_visibility: Visibility::UNOBSCURED, + is_mapped: false, + }; + + state.windows.insert(x_window, window_ref); + Ok(Box::new(window)) + } + + fn set_cursor_style(&self, style: CursorStyle) { + let mut state = self.0.borrow_mut(); + let Some(focused_window) = state.mouse_focused_window else { + return; + }; + let current_style = state + .cursor_styles + .get(&focused_window) + .unwrap_or(&CursorStyle::Arrow); + + let window = state + .mouse_focused_window + .and_then(|w| state.windows.get(&w)); + + let should_change = *current_style != style + && (window.is_none() || window.is_some_and(|w| !w.is_blocked())); + + if !should_change { + return; + } + + state.cursor_styles.insert(focused_window, style); + + // Don't clobber the invisible cursor; restore reads back from `cursor_styles`. + if state.cursor_hidden_window == Some(focused_window) { + return; + } + + let Some(cursor) = state.get_cursor_icon(style) else { + return; + }; + + check_reply( + || "Failed to set cursor style", + state.xcb_connection.change_window_attributes( + focused_window, + &ChangeWindowAttributesAux { + cursor: Some(cursor), + ..Default::default() + }, + ), + ) + .log_err(); + state.xcb_connection.flush().log_err(); + } + + fn hide_cursor_until_mouse_moves(&self) { + self.0.borrow_mut().hide_cursor_until_mouse_moves(); + } + + fn is_cursor_visible(&self) -> bool { + self.0.borrow().cursor_hidden_window.is_none() + } + + fn open_uri(&self, uri: &str) { + #[cfg(any(feature = "wayland", feature = "x11"))] + open_uri_internal( + self.with_common(|c| c.background_executor.clone()), + uri, + None, + ); + } + + fn reveal_path(&self, path: PathBuf) { + #[cfg(any(feature = "x11", feature = "wayland"))] + reveal_path_internal( + self.with_common(|c| c.background_executor.clone()), + path, + None, + ); + } + + fn write_to_primary(&self, item: gpui::ClipboardItem) { + let state = self.0.borrow_mut(); + state + .clipboard + .set_text( + std::borrow::Cow::Owned(item.text().unwrap_or_default()), + clipboard::ClipboardKind::Primary, + clipboard::WaitConfig::None, + ) + .context("X11 Failed to write to clipboard (primary)") + .log_with_level(log::Level::Debug); + } + + fn write_to_clipboard(&self, item: gpui::ClipboardItem) { + let mut state = self.0.borrow_mut(); + state + .clipboard + .set_text( + std::borrow::Cow::Owned(item.text().unwrap_or_default()), + clipboard::ClipboardKind::Clipboard, + clipboard::WaitConfig::None, + ) + .context("X11: Failed to write to clipboard (clipboard)") + .log_with_level(log::Level::Debug); + state.clipboard_item.replace(item); + } + + fn read_from_primary(&self) -> Option { + let state = self.0.borrow_mut(); + state + .clipboard + .get_any(clipboard::ClipboardKind::Primary) + .context("X11: Failed to read from clipboard (primary)") + .log_with_level(log::Level::Debug) + } + + fn read_from_clipboard(&self) -> Option { + let state = self.0.borrow_mut(); + // if the last copy was from this app, return our cached item + // which has metadata attached. + if state + .clipboard + .is_owner(clipboard::ClipboardKind::Clipboard) + { + return state.clipboard_item.clone(); + } + state + .clipboard + .get_any(clipboard::ClipboardKind::Clipboard) + .context("X11: Failed to read from clipboard (clipboard)") + .log_with_level(log::Level::Debug) + } + + fn run(&self) { + let Some(mut event_loop) = self + .0 + .borrow_mut() + .event_loop + .take() + .context("X11Client::run called but it's already running") + .log_err() + else { + return; + }; + + event_loop.run(None, &mut self.clone(), |_| {}).log_err(); + } + + fn active_window(&self) -> Option { + let state = self.0.borrow(); + state.keyboard_focused_window.and_then(|focused_window| { + state + .windows + .get(&focused_window) + .map(|window| window.handle()) + }) + } + + fn window_stack(&self) -> Option> { + let state = self.0.borrow(); + let root = state.xcb_connection.setup().roots[state.x_root_index].root; + + let reply = state + .xcb_connection + .get_property( + false, + root, + state.atoms._NET_CLIENT_LIST_STACKING, + xproto::AtomEnum::WINDOW, + 0, + u32::MAX, + ) + .ok()? + .reply() + .ok()?; + + let window_ids = reply + .value + .chunks_exact(4) + .filter_map(|chunk| chunk.try_into().ok().map(u32::from_ne_bytes)) + .collect::>(); + + let mut handles = Vec::new(); + + // We need to reverse, since _NET_CLIENT_LIST_STACKING has + // a back-to-front order. + // See: https://specifications.freedesktop.org/wm-spec/1.3/ar01s03.html + for window_ref in window_ids + .iter() + .rev() + .filter_map(|&win| state.windows.get(&win)) + { + if !window_ref.window.state.borrow().destroyed { + handles.push(window_ref.handle()); + } + } + + Some(handles) + } + + fn window_identifier(&self) -> impl Future> + Send + 'static { + let state = self.0.borrow(); + state + .keyboard_focused_window + .and_then(|focused_window| state.windows.get(&focused_window)) + .map(|window| window.window.x_window as u64) + .map(|x_window| std::future::ready(Some(WindowIdentifier::from_xid(x_window)))) + .unwrap_or(std::future::ready(None)) + } +} + +impl X11ClientState { + fn has_xim(&self) -> bool { + self.ximc.is_some() && self.xim_handler.is_some() + } + + fn take_xim(&mut self) -> Option<(X11rbClient>, XimHandler)> { + let ximc = self + .ximc + .take() + .ok_or(anyhow!("bug: XIM connection not set")) + .log_err()?; + if let Some(xim_handler) = self.xim_handler.take() { + Some((ximc, xim_handler)) + } else { + self.ximc = Some(ximc); + log::error!("bug: XIM handler not set"); + None + } + } + + fn restore_xim(&mut self, ximc: X11rbClient>, xim_handler: XimHandler) { + self.ximc = Some(ximc); + self.xim_handler = Some(xim_handler); + } + + fn update_refresh_loop(&mut self, x_window: xproto::Window) { + let Some(window_ref) = self.windows.get_mut(&x_window) else { + return; + }; + let is_visible = window_ref.visibility().is_visible(); + match (is_visible, window_ref.refresh_state.take()) { + (false, refresh_state @ Some(RefreshState::Hidden { .. })) + | (false, refresh_state @ None) + | (true, refresh_state @ Some(RefreshState::PeriodicRefresh { .. })) => { + window_ref.refresh_state = refresh_state; + } + ( + false, + Some(RefreshState::PeriodicRefresh { + refresh_rate, + event_loop_token, + }), + ) => { + self.loop_handle.remove(event_loop_token); + window_ref.refresh_state = Some(RefreshState::Hidden { refresh_rate }); + } + (true, Some(RefreshState::Hidden { refresh_rate })) => { + let event_loop_token = self.start_refresh_loop(x_window, refresh_rate); + let Some(window_ref) = self.windows.get_mut(&x_window) else { + return; + }; + window_ref.refresh_state = Some(RefreshState::PeriodicRefresh { + refresh_rate, + event_loop_token, + }); + } + (true, None) => { + let Some(screen_resources) = get_reply( + || "Failed to get screen resources", + self.xcb_connection + .randr_get_screen_resources_current(x_window), + ) + .log_err() else { + return; + }; + + // Ideally this would be re-queried when the window changes screens, but there + // doesn't seem to be an efficient / straightforward way to do this. Should also be + // updated when screen configurations change. + let mode_info = screen_resources.crtcs.iter().find_map(|crtc| { + let crtc_info = self + .xcb_connection + .randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME) + .ok()? + .reply() + .ok()?; + + screen_resources + .modes + .iter() + .find(|m| m.id == crtc_info.mode) + }); + let refresh_rate = match mode_info { + Some(mode_info) => mode_refresh_rate(mode_info), + None => { + log::error!( + "Failed to get screen mode info from xrandr, \ + defaulting to 60hz refresh rate." + ); + Duration::from_micros(1_000_000 / 60) + } + }; + + let event_loop_token = self.start_refresh_loop(x_window, refresh_rate); + let Some(window_ref) = self.windows.get_mut(&x_window) else { + return; + }; + window_ref.refresh_state = Some(RefreshState::PeriodicRefresh { + refresh_rate, + event_loop_token, + }); + } + } + } + + #[must_use] + fn start_refresh_loop( + &self, + x_window: xproto::Window, + refresh_rate: Duration, + ) -> RegistrationToken { + self.loop_handle + .insert_source(calloop::timer::Timer::immediate(), { + move |mut instant, (), client| { + let xcb_connection = { + let mut state = client.0.borrow_mut(); + let xcb_connection = state.xcb_connection.clone(); + if let Some(window) = state.windows.get_mut(&x_window) { + let force_render = std::mem::take( + &mut window.window.state.borrow_mut().force_render_after_recovery, + ); + let window = window.window.clone(); + drop(state); + window.refresh(RequestFrameOptions { + require_presentation: false, + force_render, + }); + } + xcb_connection + }; + client.process_x11_events(&xcb_connection).log_err(); + + // Take into account that some frames have been skipped + let now = Instant::now(); + while instant < now { + instant += refresh_rate; + } + calloop::timer::TimeoutAction::ToInstant(instant) + } + }) + .expect("Failed to initialize window refresh timer") + } + + fn get_cursor_icon(&mut self, style: CursorStyle) -> Option { + if let Some(cursor) = self.cursor_cache.get(&style) { + return *cursor; + } + + let result = 'outer: { + let mut errors = String::new(); + let cursor_icon_names = cursor_style_to_icon_names(style); + for cursor_icon_name in cursor_icon_names { + match self + .cursor_handle + .load_cursor(&self.xcb_connection, cursor_icon_name) + { + Ok(loaded_cursor) => { + if loaded_cursor != x11rb::NONE { + break 'outer Ok(loaded_cursor); + } + } + Err(err) => { + errors.push_str(&err.to_string()); + errors.push('\n'); + } + } + } + if errors.is_empty() { + Err(anyhow!( + "errors while loading cursor icons {:?}:\n{}", + cursor_icon_names, + errors + )) + } else { + Err(anyhow!("did not find cursor icons {:?}", cursor_icon_names)) + } + }; + + let cursor = match result { + Ok(cursor) => Some(cursor), + Err(err) => { + match self + .cursor_handle + .load_cursor(&self.xcb_connection, DEFAULT_CURSOR_ICON_NAME) + { + Ok(default) => { + log_cursor_icon_warning(err.context(format!( + "X11: error loading cursor icon, falling back on default icon '{}'", + DEFAULT_CURSOR_ICON_NAME + ))); + Some(default) + } + Err(default_err) => { + log_cursor_icon_warning(err.context(default_err).context(format!( + "X11: error loading default cursor fallback '{}'", + DEFAULT_CURSOR_ICON_NAME + ))); + None + } + } + } + }; + + self.cursor_cache.insert(style, cursor); + cursor + } + + fn get_or_create_invisible_cursor(&mut self) -> Option { + if let Some(cursor) = self.invisible_cursor_cache { + return Some(cursor); + } + let cursor = create_invisible_cursor(&self.xcb_connection) + .context("X11: error while creating invisible cursor") + .log_err()?; + self.invisible_cursor_cache = Some(cursor); + Some(cursor) + } + + fn hide_cursor_until_mouse_moves(&mut self) { + if self.cursor_hidden_window.is_some() { + return; + } + let Some(focused_window) = self.mouse_focused_window else { + // No window to apply the per-window invisible cursor to. + return; + }; + let Some(invisible_cursor) = self.get_or_create_invisible_cursor() else { + return; + }; + check_reply( + || "Failed to hide cursor", + self.xcb_connection.change_window_attributes( + focused_window, + &ChangeWindowAttributesAux { + cursor: Some(invisible_cursor), + ..Default::default() + }, + ), + ) + .log_err(); + self.xcb_connection.flush().log_err(); + self.cursor_hidden_window = Some(focused_window); + } + + fn restore_cursor_after_hide(&mut self) { + let Some(hidden_window) = self.cursor_hidden_window.take() else { + return; + }; + let style = self + .cursor_styles + .get(&hidden_window) + .copied() + .unwrap_or(CursorStyle::Arrow); + let Some(cursor) = self.get_cursor_icon(style) else { + log::warn!( + "X11: no cursor icon available to restore {:?} after hide; cursor may stay invisible", + style + ); + return; + }; + check_reply( + || "Failed to restore cursor style after hide", + self.xcb_connection.change_window_attributes( + hidden_window, + &ChangeWindowAttributesAux { + cursor: Some(cursor), + ..Default::default() + }, + ), + ) + .log_err(); + self.xcb_connection.flush().log_err(); + } +} + +// Adapted from: +// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111 +pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration { + if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 { + return Duration::from_millis(16); + } + + let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64); + let micros = 1_000_000_000 / millihertz; + log::info!("Refreshing every {}ms", micros / 1_000); + Duration::from_micros(micros) +} + +/// Applies a mapped/obscured change to the refresh loop and reports the +/// resulting visibility to the window. Consumes the client borrow because the +/// visibility callback re-enters GPUI. +fn handle_visibility_changed(mut state: RefMut<'_, X11ClientState>, x_window: xproto::Window) { + state.update_refresh_loop(x_window); + let Some(window_ref) = state.windows.get(&x_window) else { + return; + }; + let visibility = window_ref.visibility(); + let window = window_ref.window.clone(); + drop(state); + window.set_visibility(visibility); +} + +fn fp3232_to_f32(value: xinput::Fp3232) -> f32 { + value.integral as f32 + value.frac as f32 / u32::MAX as f32 +} + +fn detect_compositor_gpu( + xcb_connection: &XCBConnection, + screen: &xproto::Screen, +) -> Option { + use std::os::fd::AsRawFd; + use std::os::unix::fs::MetadataExt; + + xcb_connection + .extension_information(dri3::X11_EXTENSION_NAME) + .ok()??; + + let reply = dri3::open(xcb_connection, screen.root, 0) + .ok()? + .reply() + .ok()?; + let fd = reply.device_fd; + + let path = format!("/proc/self/fd/{}", fd.as_raw_fd()); + let metadata = std::fs::metadata(&path).ok()?; + + crate::linux::compositor_gpu_hint_from_dev_t(metadata.rdev()) +} + +fn check_compositor_present(xcb_connection: &XCBConnection, root: xproto::Window) -> bool { + // Method 1: Check for _NET_WM_CM_S{root} + let atom_name = format!("_NET_WM_CM_S{}", root); + let atom1 = get_reply( + || format!("Failed to intern {atom_name}"), + xcb_connection.intern_atom(false, atom_name.as_bytes()), + ); + let method1 = match atom1.log_with_level(Level::Debug) { + Some(reply) if reply.atom != x11rb::NONE => { + let atom = reply.atom; + get_reply( + || format!("Failed to get {atom_name} owner"), + xcb_connection.get_selection_owner(atom), + ) + .map(|reply| reply.owner != 0) + .log_with_level(Level::Debug) + .unwrap_or(false) + } + _ => false, + }; + + // Method 2: Check for _NET_WM_CM_OWNER + let atom_name = "_NET_WM_CM_OWNER"; + let atom2 = get_reply( + || format!("Failed to intern {atom_name}"), + xcb_connection.intern_atom(false, atom_name.as_bytes()), + ); + let method2 = match atom2.log_with_level(Level::Debug) { + Some(reply) if reply.atom != x11rb::NONE => { + let atom = reply.atom; + get_reply( + || format!("Failed to get {atom_name}"), + xcb_connection.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1), + ) + .map(|reply| reply.value_len > 0) + .unwrap_or(false) + } + _ => return false, + }; + + // Method 3: Check for _NET_SUPPORTING_WM_CHECK + let atom_name = "_NET_SUPPORTING_WM_CHECK"; + let atom3 = get_reply( + || format!("Failed to intern {atom_name}"), + xcb_connection.intern_atom(false, atom_name.as_bytes()), + ); + let method3 = match atom3.log_with_level(Level::Debug) { + Some(reply) if reply.atom != x11rb::NONE => { + let atom = reply.atom; + get_reply( + || format!("Failed to get {atom_name}"), + xcb_connection.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1), + ) + .map(|reply| reply.value_len > 0) + .unwrap_or(false) + } + _ => return false, + }; + + log::debug!( + "Compositor detection: _NET_WM_CM_S?={}, _NET_WM_CM_OWNER={}, _NET_SUPPORTING_WM_CHECK={}", + method1, + method2, + method3 + ); + + method1 || method2 || method3 +} + +fn check_gtk_frame_extents_supported( + xcb_connection: &XCBConnection, + atoms: &XcbAtoms, + root: xproto::Window, +) -> bool { + let Some(supported_atoms) = get_reply( + || "Failed to get _NET_SUPPORTED", + xcb_connection.get_property( + false, + root, + atoms._NET_SUPPORTED, + xproto::AtomEnum::ATOM, + 0, + 1024, + ), + ) + .log_with_level(Level::Debug) else { + return false; + }; + + let supported_atom_ids: Vec = supported_atoms + .value + .chunks_exact(4) + .filter_map(|chunk| chunk.try_into().ok().map(u32::from_ne_bytes)) + .collect(); + + supported_atom_ids.contains(&atoms._GTK_FRAME_EXTENTS) +} + +fn xdnd_is_atom_supported(atom: u32, atoms: &XcbAtoms) -> bool { + atom == atoms.TEXT + || atom == atoms.STRING + || atom == atoms.UTF8_STRING + || atom == atoms.TEXT_PLAIN + || atom == atoms.TEXT_PLAIN_UTF8 + || atom == atoms.TextUriList +} + +fn xdnd_get_supported_atom( + xcb_connection: &XCBConnection, + supported_atoms: &XcbAtoms, + target: xproto::Window, +) -> u32 { + if let Some(reply) = get_reply( + || "Failed to get XDnD supported atoms", + xcb_connection.get_property( + false, + target, + supported_atoms.XdndTypeList, + AtomEnum::ANY, + 0, + 1024, + ), + ) + .log_with_level(Level::Warn) + && let Some(atoms) = reply.value32() + { + for atom in atoms { + if xdnd_is_atom_supported(atom, supported_atoms) { + return atom; + } + } + } + 0 +} + +fn xdnd_send_finished( + xcb_connection: &XCBConnection, + atoms: &XcbAtoms, + source: xproto::Window, + target: xproto::Window, +) { + let message = ClientMessageEvent { + format: 32, + window: target, + type_: atoms.XdndFinished, + data: ClientMessageData::from([source, 1, atoms.XdndActionCopy, 0, 0]), + sequence: 0, + response_type: xproto::CLIENT_MESSAGE_EVENT, + }; + check_reply( + || "Failed to send XDnD finished event", + xcb_connection.send_event(false, target, EventMask::default(), message), + ) + .log_err(); + xcb_connection.flush().log_err(); +} + +fn xdnd_send_status( + xcb_connection: &XCBConnection, + atoms: &XcbAtoms, + source: xproto::Window, + target: xproto::Window, + action: u32, +) { + let message = ClientMessageEvent { + format: 32, + window: target, + type_: atoms.XdndStatus, + data: ClientMessageData::from([source, 1, 0, 0, action]), + sequence: 0, + response_type: xproto::CLIENT_MESSAGE_EVENT, + }; + check_reply( + || "Failed to send XDnD status event", + xcb_connection.send_event(false, target, EventMask::default(), message), + ) + .log_err(); + xcb_connection.flush().log_err(); +} + +/// Recomputes `pointer_device_states` by querying all pointer devices. +/// When a device is present in `scroll_values_to_preserve`, its value for `ScrollAxisState.scroll_value` is used. +fn current_pointer_device_states( + xcb_connection: &XCBConnection, + scroll_values_to_preserve: &BTreeMap, +) -> Option> { + let devices_query_result = get_reply( + || "Failed to query XInput devices", + xcb_connection.xinput_xi_query_device(XINPUT_ALL_DEVICES), + ) + .log_err()?; + + let mut pointer_device_states = BTreeMap::new(); + pointer_device_states.extend( + devices_query_result + .infos + .iter() + .filter(|info| is_pointer_device(info.type_)) + .filter_map(|info| { + let scroll_data = info + .classes + .iter() + .filter_map(|class| class.data.as_scroll()) + .copied() + .rev() + .collect::>(); + let old_state = scroll_values_to_preserve.get(&info.deviceid); + let old_horizontal = old_state.map(|state| &state.horizontal); + let old_vertical = old_state.map(|state| &state.vertical); + let horizontal = scroll_data + .iter() + .find(|data| data.scroll_type == xinput::ScrollType::HORIZONTAL) + .map(|data| scroll_data_to_axis_state(data, old_horizontal)); + let vertical = scroll_data + .iter() + .find(|data| data.scroll_type == xinput::ScrollType::VERTICAL) + .map(|data| scroll_data_to_axis_state(data, old_vertical)); + if horizontal.is_none() && vertical.is_none() { + None + } else { + Some(( + info.deviceid, + PointerDeviceState { + horizontal: horizontal.unwrap_or_else(Default::default), + vertical: vertical.unwrap_or_else(Default::default), + }, + )) + } + }), + ); + if pointer_device_states.is_empty() { + log::error!("Found no xinput mouse pointers."); + } + Some(pointer_device_states) +} + +/// Returns true if the device is a pointer device. Does not include pointer device groups. +fn is_pointer_device(type_: xinput::DeviceType) -> bool { + type_ == xinput::DeviceType::SLAVE_POINTER +} + +fn scroll_data_to_axis_state( + data: &xinput::DeviceClassDataScroll, + old_axis_state_with_valid_scroll_value: Option<&ScrollAxisState>, +) -> ScrollAxisState { + ScrollAxisState { + valuator_number: Some(data.number), + multiplier: SCROLL_LINES / fp3232_to_f32(data.increment), + scroll_value: old_axis_state_with_valid_scroll_value.and_then(|state| state.scroll_value), + } +} + +fn reset_all_pointer_device_scroll_positions( + pointer_device_states: &mut BTreeMap, +) { + pointer_device_states + .iter_mut() + .for_each(|(_, device_state)| reset_pointer_device_scroll_positions(device_state)); +} + +fn reset_pointer_device_scroll_positions(pointer: &mut PointerDeviceState) { + pointer.horizontal.scroll_value = None; + pointer.vertical.scroll_value = None; +} + +/// Returns the scroll delta for a smooth scrolling motion event, or `None` if no scroll data is present. +fn get_scroll_delta_and_update_state( + pointer: &mut PointerDeviceState, + event: &xinput::MotionEvent, +) -> Option> { + let delta_x = get_axis_scroll_delta_and_update_state(event, &mut pointer.horizontal); + let delta_y = get_axis_scroll_delta_and_update_state(event, &mut pointer.vertical); + if delta_x.is_some() || delta_y.is_some() { + Some(Point::new(delta_x.unwrap_or(0.0), delta_y.unwrap_or(0.0))) + } else { + None + } +} + +fn get_axis_scroll_delta_and_update_state( + event: &xinput::MotionEvent, + axis: &mut ScrollAxisState, +) -> Option { + let axis_index = get_valuator_axis_index(&event.valuator_mask, axis.valuator_number?)?; + if let Some(axis_value) = event.axisvalues.get(axis_index) { + let new_scroll = fp3232_to_f32(*axis_value); + let delta_scroll = axis + .scroll_value + .map(|old_scroll| (old_scroll - new_scroll) * axis.multiplier); + axis.scroll_value = Some(new_scroll); + delta_scroll + } else { + log::error!("Encountered invalid XInput valuator_mask, scrolling may not work properly."); + None + } +} + +fn make_scroll_wheel_event( + position: Point, + scroll_delta: Point, + modifiers: Modifiers, +) -> gpui::ScrollWheelEvent { + // When shift is held down, vertical scrolling turns into horizontal scrolling. + let delta = if modifiers.shift { + Point { + x: scroll_delta.y, + y: 0.0, + } + } else { + scroll_delta + }; + gpui::ScrollWheelEvent { + position, + delta: ScrollDelta::Lines(delta), + modifiers, + touch_phase: TouchPhase::default(), + } +} + +fn create_invisible_cursor( + connection: &XCBConnection, +) -> anyhow::Result { + let empty_pixmap = connection.generate_id()?; + let root = connection.setup().roots[0].root; + connection.create_pixmap(1, empty_pixmap, root, 1, 1)?; + + let cursor = connection.generate_id()?; + connection.create_cursor(cursor, empty_pixmap, empty_pixmap, 0, 0, 0, 0, 0, 0, 0, 0)?; + + connection.free_pixmap(empty_pixmap)?; + + xcb_flush(connection); + Ok(cursor) +} + +enum DpiMode { + Randr, + Scale(f32), + NotSet, +} + +fn get_scale_factor( + connection: &XCBConnection, + resource_database: &Database, + screen_index: usize, +) -> f32 { + let env_dpi = std::env::var(GPUI_X11_SCALE_FACTOR_ENV) + .ok() + .map(|var| { + if var.to_lowercase() == "randr" { + DpiMode::Randr + } else if let Ok(scale) = var.parse::() { + if valid_scale_factor(scale) { + DpiMode::Scale(scale) + } else { + panic!( + "`{}` must be a positive normal number or `randr`. Got `{}`", + GPUI_X11_SCALE_FACTOR_ENV, var + ); + } + } else if var.is_empty() { + DpiMode::NotSet + } else { + panic!( + "`{}` must be a positive number or `randr`. Got `{}`", + GPUI_X11_SCALE_FACTOR_ENV, var + ); + } + }) + .unwrap_or(DpiMode::NotSet); + + match env_dpi { + DpiMode::Scale(scale) => { + log::info!( + "Using scale factor from {}: {}", + GPUI_X11_SCALE_FACTOR_ENV, + scale + ); + return scale; + } + DpiMode::Randr => { + if let Some(scale) = get_randr_scale_factor(connection, screen_index) { + log::info!( + "Using RandR scale factor from {}=randr: {}", + GPUI_X11_SCALE_FACTOR_ENV, + scale + ); + return scale; + } + log::warn!("Failed to calculate RandR scale factor, falling back to default"); + return 1.0; + } + DpiMode::NotSet => {} + } + + // TODO: Use scale factor from XSettings here + + if let Some(dpi) = resource_database + .get_value::("Xft.dpi", "Xft.dpi") + .ok() + .flatten() + { + let scale = dpi / 96.0; // base dpi + log::info!("Using scale factor from Xft.dpi: {}", scale); + return scale; + } + + if let Some(scale) = get_randr_scale_factor(connection, screen_index) { + log::info!("Using RandR scale factor: {}", scale); + return scale; + } + + log::info!("Using default scale factor: 1.0"); + 1.0 +} + +fn get_randr_scale_factor(connection: &XCBConnection, screen_index: usize) -> Option { + let root = connection.setup().roots.get(screen_index)?.root; + + let version_cookie = connection.randr_query_version(1, 6).ok()?; + let version_reply = version_cookie.reply().ok()?; + if version_reply.major_version < 1 + || (version_reply.major_version == 1 && version_reply.minor_version < 5) + { + return legacy_get_randr_scale_factor(connection, root); // for randr <1.5 + } + + let monitors_cookie = connection.randr_get_monitors(root, true).ok()?; // true for active only + let monitors_reply = monitors_cookie.reply().ok()?; + + let mut fallback_scale: Option = None; + for monitor in monitors_reply.monitors { + if monitor.width_in_millimeters == 0 || monitor.height_in_millimeters == 0 { + continue; + } + let scale_factor = get_dpi_factor( + (monitor.width as u32, monitor.height as u32), + ( + monitor.width_in_millimeters as u64, + monitor.height_in_millimeters as u64, + ), + ); + if monitor.primary { + return Some(scale_factor); + } else if fallback_scale.is_none() { + fallback_scale = Some(scale_factor); + } + } + + fallback_scale +} + +fn legacy_get_randr_scale_factor(connection: &XCBConnection, root: u32) -> Option { + let primary_cookie = connection.randr_get_output_primary(root).ok()?; + let primary_reply = primary_cookie.reply().ok()?; + let primary_output = primary_reply.output; + + let primary_output_cookie = connection + .randr_get_output_info(primary_output, x11rb::CURRENT_TIME) + .ok()?; + let primary_output_info = primary_output_cookie.reply().ok()?; + + // try primary + if primary_output_info.connection == randr::Connection::CONNECTED + && primary_output_info.mm_width > 0 + && primary_output_info.mm_height > 0 + && primary_output_info.crtc != 0 + { + let crtc_cookie = connection + .randr_get_crtc_info(primary_output_info.crtc, x11rb::CURRENT_TIME) + .ok()?; + let crtc_info = crtc_cookie.reply().ok()?; + + if crtc_info.width > 0 && crtc_info.height > 0 { + let scale_factor = get_dpi_factor( + (crtc_info.width as u32, crtc_info.height as u32), + ( + primary_output_info.mm_width as u64, + primary_output_info.mm_height as u64, + ), + ); + return Some(scale_factor); + } + } + + // fallback: full scan + let resources_cookie = connection.randr_get_screen_resources_current(root).ok()?; + let screen_resources = resources_cookie.reply().ok()?; + + let mut crtc_cookies = Vec::with_capacity(screen_resources.crtcs.len()); + for &crtc in &screen_resources.crtcs { + if let Ok(cookie) = connection.randr_get_crtc_info(crtc, x11rb::CURRENT_TIME) { + crtc_cookies.push((crtc, cookie)); + } + } + + let mut crtc_infos: HashMap = HashMap::default(); + let mut valid_outputs: HashSet = HashSet::new(); + for (crtc, cookie) in crtc_cookies { + if let Ok(reply) = cookie.reply() + && reply.width > 0 + && reply.height > 0 + && !reply.outputs.is_empty() + { + crtc_infos.insert(crtc, reply.clone()); + valid_outputs.extend(&reply.outputs); + } + } + + if valid_outputs.is_empty() { + return None; + } + + let mut output_cookies = Vec::with_capacity(valid_outputs.len()); + for &output in &valid_outputs { + if let Ok(cookie) = connection.randr_get_output_info(output, x11rb::CURRENT_TIME) { + output_cookies.push((output, cookie)); + } + } + let mut output_infos: HashMap = HashMap::default(); + for (output, cookie) in output_cookies { + if let Ok(reply) = cookie.reply() { + output_infos.insert(output, reply); + } + } + + let mut fallback_scale: Option = None; + for crtc_info in crtc_infos.values() { + for &output in &crtc_info.outputs { + if let Some(output_info) = output_infos.get(&output) { + if output_info.connection != randr::Connection::CONNECTED { + continue; + } + + if output_info.mm_width == 0 || output_info.mm_height == 0 { + continue; + } + + let scale_factor = get_dpi_factor( + (crtc_info.width as u32, crtc_info.height as u32), + (output_info.mm_width as u64, output_info.mm_height as u64), + ); + + if output != primary_output && fallback_scale.is_none() { + fallback_scale = Some(scale_factor); + } + } + } + } + + fallback_scale +} + +fn get_dpi_factor((width_px, height_px): (u32, u32), (width_mm, height_mm): (u64, u64)) -> f32 { + let ppmm = ((width_px as f64 * height_px as f64) / (width_mm as f64 * height_mm as f64)).sqrt(); // pixels per mm + + const MM_PER_INCH: f64 = 25.4; + const BASE_DPI: f64 = 96.0; + const QUANTIZE_STEP: f64 = 12.0; // e.g. 1.25 = 15/12, 1.5 = 18/12, 1.75 = 21/12, 2.0 = 24/12 + const MIN_SCALE: f64 = 1.0; + const MAX_SCALE: f64 = 20.0; + + let dpi_factor = + ((ppmm * (QUANTIZE_STEP * MM_PER_INCH / BASE_DPI)).round() / QUANTIZE_STEP).max(MIN_SCALE); + + let validated_factor = if dpi_factor <= MAX_SCALE { + dpi_factor + } else { + MIN_SCALE + }; + + if valid_scale_factor(validated_factor as f32) { + validated_factor as f32 + } else { + log::warn!( + "Calculated DPI factor {} is invalid, using 1.0", + validated_factor + ); + 1.0 + } +} + +#[inline] +fn valid_scale_factor(scale_factor: f32) -> bool { + scale_factor.is_sign_positive() && scale_factor.is_normal() +} + +#[inline] +fn xkb_state_for_key_event(xkb: &xkbc::State, event_state: xproto::KeyButMask) -> xkbc::State { + let keymap = xkb.get_keymap(); + let mut key_event_state = xkbc::State::new(&keymap); + + let latched_modifiers = xkb.serialize_mods(xkbc::STATE_MODS_LATCHED); + let locked_modifiers = xkb.serialize_mods(xkbc::STATE_MODS_LOCKED); + let active_modifier_mask: xkbc::ModMask = u16::from( + event_state + & (xproto::KeyButMask::SHIFT + | xproto::KeyButMask::LOCK + | xproto::KeyButMask::CONTROL + | xproto::KeyButMask::MOD1 + | xproto::KeyButMask::MOD2 + | xproto::KeyButMask::MOD3 + | xproto::KeyButMask::MOD4 + | xproto::KeyButMask::MOD5), + ) + .into(); + let depressed_modifiers = active_modifier_mask & !(latched_modifiers | locked_modifiers); + + key_event_state.update_mask( + depressed_modifiers, + latched_modifiers, + locked_modifiers, + xkb.serialize_layout(xkbc::STATE_LAYOUT_DEPRESSED), + xkb.serialize_layout(xkbc::STATE_LAYOUT_LATCHED), + xkb.serialize_layout(xkbc::STATE_LAYOUT_LOCKED), + ); + + key_event_state +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_keymap(layouts: &str) -> xkbc::Keymap { + test_keymap_with_variant(layouts, "") + } + + fn test_keymap_with_variant(layouts: &str, variant: &str) -> xkbc::Keymap { + // These fixtures compile layout names from local files, unlike server keymaps. + let context = xkbc::Context::new(xkbc::CONTEXT_NO_FLAGS); + assert!(!context.get_raw_ptr().is_null()); + xkbc::Keymap::new_from_names( + &context, + "", + "pc105", + layouts, + variant, + None, + xkbc::COMPILE_NO_FLAGS, + ) + .expect("test keymap should compile") + } + + // Returns a state where the second layout is active via a temporary + // mechanism (holding a key down or one-shot), not a permanent toggle. + fn state_with_non_locked_layout(keymap: &xkbc::Keymap) -> xkbc::State { + let mut depressed_layout_state = xkbc::State::new(keymap); + depressed_layout_state.update_mask(0, 0, 0, 1, 0, 0); + if depressed_layout_state.serialize_layout(STATE_LAYOUT_EFFECTIVE) == 1 { + return depressed_layout_state; + } + + let mut latched_layout_state = xkbc::State::new(keymap); + latched_layout_state.update_mask(0, 0, 0, 0, 1, 0); + if latched_layout_state.serialize_layout(STATE_LAYOUT_EFFECTIVE) == 1 { + return latched_layout_state; + } + + panic!("test keymap should support a non-locked secondary layout"); + } + + #[test] + fn key_event_state_uses_event_modifiers_without_mutating_server_state() { + let keymap = test_keymap("us"); + let server_state = xkbc::State::new(&keymap); + // The "9" key on a US keyboard. + let keycode = keymap + .key_by_name("AE09") + .expect("test key should exist in the keymap"); + + // Simulate pressing Shift+9 (which should produce "("). + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::SHIFT); + let keystroke = keystroke_from_xkb( + &key_event_state, + modifiers_from_state(xproto::KeyButMask::SHIFT), + keycode, + ); + + // Assert Shift+9 produces "(" on US layout. + assert_eq!(keystroke.key, "("); + assert_eq!(keystroke.key_char.as_deref(), Some("(")); + // Assert the long-lived server state was not mutated by the key event. + assert_eq!(server_state.key_get_utf8(keycode), "9"); + } + + #[test] + fn key_event_state_ignores_pointer_button_bits() { + let keymap = test_keymap("us"); + let server_state = xkbc::State::new(&keymap); + // The "9" key on a US keyboard. + let keycode = keymap + .key_by_name("AE09") + .expect("test key should exist in the keymap"); + + // Simulate Shift held down. + let shifted_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::SHIFT); + // Simulate Shift held down while also clicking the left mouse button. + let shifted_with_button_state = xkb_state_for_key_event( + &server_state, + xproto::KeyButMask::SHIFT | xproto::KeyButMask::BUTTON1, + ); + + // Assert the mouse button has no effect on modifier state. + assert_eq!( + shifted_with_button_state.serialize_mods(xkbc::STATE_MODS_EFFECTIVE), + shifted_state.serialize_mods(xkbc::STATE_MODS_EFFECTIVE) + ); + // Assert both cases produce the same character. + assert_eq!( + shifted_with_button_state.key_get_utf8(keycode), + shifted_state.key_get_utf8(keycode) + ); + } + + #[test] + fn key_event_state_preserves_non_locked_layout_components() { + // US + Russian dual-layout keyboard. + let keymap = test_keymap("us,ru"); + // Simulate the Russian layout being active via a temporary layout + // switch (holding a key), not a permanent toggle. + let server_state = state_with_non_locked_layout(&keymap); + // The "Q" key position, which produces a Cyrillic character in Russian layout. + let keycode = keymap + .key_by_name("AD01") + .expect("test key should exist in the keymap"); + + let expected_text = server_state.key_get_utf8(keycode); + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::default()); + + // Assert the temporary layout switch is preserved. + assert_eq!( + key_event_state.serialize_layout(STATE_LAYOUT_EFFECTIVE), + server_state.serialize_layout(STATE_LAYOUT_EFFECTIVE) + ); + // Assert the key produces the same character as expected from the + // Russian layout. + assert_eq!(key_event_state.key_get_utf8(keycode), expected_text); + } + + // https://github.com/zed-industries/zed/issues/14282 + #[test] + fn capslock_toggle_produces_uppercase() { + let keymap = test_keymap("us"); + let mut server_state = xkbc::State::new(&keymap); + // The "A" key position on a US keyboard. + let keycode = keymap + .key_by_name("AC01") + .expect("'a' key should exist in the keymap"); + + // Simulate the user having toggled CapsLock on (it's now permanently + // active until pressed again). + let lock_mod = u16::from(xproto::KeyButMask::LOCK) as xkbc::ModMask; + server_state.update_mask(0, 0, lock_mod, 0, 0, 0); + + // Simulate pressing the "a" key while CapsLock is on. + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::LOCK); + + // Assert CapsLock is treated as a toggle (locked), not as a held key + // (depressed). This distinction matters because XKB only applies + // capitalization when CapsLock is in the "locked" state. + assert_eq!( + key_event_state.serialize_mods(xkbc::STATE_MODS_LOCKED) & lock_mod, + lock_mod, + ); + // Assert typing "a" with CapsLock on produces "A". + assert_eq!(key_event_state.key_get_utf8(keycode), "A"); + } + + // https://github.com/zed-industries/zed/issues/14282 + #[test] + fn neo2_level3_via_capslock_produces_ellipsis() { + // Neo 2 is a German keyboard layout that repurposes CapsLock as a + // "level 3" modifier key for accessing additional characters. + let keymap = test_keymap_with_variant("de", "neo"); + let server_state = xkbc::State::new(&keymap); + // The key in the "Q" position, which produces "x" on Neo 2 base layer. + let keycode = keymap + .key_by_name("AD01") + .expect("test key should exist in the keymap"); + + // Simulate holding CapsLock, which in Neo 2 activates the "level 3" + // layer (mapped to the Mod5 modifier internally). + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::MOD5); + + // Assert holding CapsLock + pressing the "x" key produces "..." + // (ellipsis), which is the level 3 character on that key in Neo 2. + assert_eq!(key_event_state.key_get_utf8(keycode), "\u{2026}"); + } + + // https://github.com/zed-industries/zed/issues/14282 + #[test] + fn neo2_latched_mod5_preserved() { + // Neo 2 also supports "latching" the level 3 modifier (via Caps+Tab), + // which activates it for only the next keypress and then deactivates. + let keymap = test_keymap_with_variant("de", "neo"); + let mut server_state = xkbc::State::new(&keymap); + let keycode = keymap + .key_by_name("AD01") + .expect("test key should exist in the keymap"); + + // Simulate the level 3 modifier being latched (one-shot active). + let mod5 = u16::from(xproto::KeyButMask::MOD5) as xkbc::ModMask; + server_state.update_mask(0, mod5, 0, 0, 0, 0); + + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::MOD5); + + // Assert the modifier stays classified as "latched" (one-shot) rather + // than being reclassified as "depressed" (held down). This matters + // because latched modifiers auto-deactivate after one keypress. + assert_eq!( + key_event_state.serialize_mods(xkbc::STATE_MODS_LATCHED) & mod5, + mod5, + ); + // Assert the latched level 3 still produces the ellipsis character. + assert_eq!(key_event_state.key_get_utf8(keycode), "\u{2026}"); + } + + // https://github.com/zed-industries/zed/pull/31193 + #[test] + fn german_layout_correct_key_resolution() { + // Standard German keyboard layout. + let keymap = test_keymap("de"); + let server_state = xkbc::State::new(&keymap); + // The "7" key on the number row. + let keycode = keymap + .key_by_name("AE07") + .expect("'7' key should exist in the keymap"); + + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::default()); + + // Assert pressing the "7" key on a German layout produces "7". + assert_eq!(key_event_state.key_get_utf8(keycode), "7"); + } + + // https://github.com/zed-industries/zed/issues/26468 + // https://github.com/zed-industries/zed/issues/16667 + #[test] + fn space_works_with_cyrillic_layout_active() { + // US + Russian dual-layout keyboard. + let keymap = test_keymap("us,ru"); + let mut server_state = xkbc::State::new(&keymap); + let space = keymap + .key_by_name("SPCE") + .expect("space key should exist in the keymap"); + + // Simulate the user having switched to the Russian layout + // (e.g. via a keyboard shortcut like Super+Space). + server_state.update_mask(0, 0, 0, 0, 0, 1); + + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::default()); + + // Assert the Russian layout is still active after constructing the + // key event state (not accidentally reset to US). + assert_eq!(key_event_state.serialize_layout(STATE_LAYOUT_EFFECTIVE), 1); + // Assert pressing space while on the Russian layout still types a space. + assert_eq!(key_event_state.key_get_utf8(space), " "); + } + + // https://github.com/zed-industries/zed/issues/40678 + #[test] + fn macro_shift_bracket_produces_brace() { + let keymap = test_keymap("us"); + let server_state = xkbc::State::new(&keymap); + // The "]" key on a US keyboard. + let bracket = keymap + .key_by_name("AD12") + .expect("']' key should exist in the keymap"); + + // Simulate a keyboard macro (e.g. from a ZMK/QMK firmware keyboard) + // that sends Shift + "]" very rapidly. The modifier state notification + // for Shift hasn't reached us yet, so the server state has no + // modifiers. But the key event itself carries the correct Shift state. + assert_eq!(server_state.serialize_mods(xkbc::STATE_MODS_EFFECTIVE), 0); + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::SHIFT); + + // Assert Shift+"]" produces "}" even when the Shift notification + // arrived late. + assert_eq!(key_event_state.key_get_utf8(bracket), "}"); + } + + // https://github.com/zed-industries/zed/issues/49329 + #[test] + fn sequential_key_events_do_not_corrupt_state() { + let keymap = test_keymap("us"); + let server_state = xkbc::State::new(&keymap); + + // Simulate typing "a s d" with spaces in between, all without any + // modifier keys held. + let keys: &[(&str, &str)] = &[ + ("AC01", "a"), + ("SPCE", " "), + ("AC02", "s"), + ("SPCE", " "), + ("AC03", "d"), + ]; + + for &(key_name, expected_utf8) in keys { + let keycode = keymap + .key_by_name(key_name) + .expect("test key should exist in the keymap"); + + let key_event_state = + xkb_state_for_key_event(&server_state, xproto::KeyButMask::default()); + + // Assert each key in the sequence produces the expected character + // (no dropped or garbled input from state corruption). + assert_eq!( + key_event_state.key_get_utf8(keycode), + expected_utf8, + "key {key_name} should produce {expected_utf8:?}", + ); + } + + // Assert the server state is completely untouched after processing + // all key events. + assert_eq!(server_state.serialize_mods(xkbc::STATE_MODS_EFFECTIVE), 0); + assert_eq!(server_state.serialize_layout(STATE_LAYOUT_EFFECTIVE), 0); + } + + // https://github.com/zed-industries/zed/issues/26468 + #[test] + fn space_works_with_czech_layout_active() { + // US + Czech dual-layout keyboard. + let keymap = test_keymap("us,cz"); + let mut server_state = xkbc::State::new(&keymap); + let space = keymap + .key_by_name("SPCE") + .expect("space key should exist in the keymap"); + + // Simulate the user having switched to the Czech layout. + server_state.update_mask(0, 0, 0, 0, 0, 1); + + let key_event_state = xkb_state_for_key_event(&server_state, xproto::KeyButMask::default()); + + // Assert pressing space while on the Czech layout still types a space. + assert_eq!(key_event_state.key_get_utf8(space), " "); + } +} diff --git a/vendor/gpui-pre-linux/src/linux/x11/clipboard.rs b/vendor/gpui-pre-linux/src/linux/x11/clipboard.rs new file mode 100644 index 000000000..706e7b096 --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/x11/clipboard.rs @@ -0,0 +1,1255 @@ +/* + * Copyright 2022 - 2025 Zed Industries, Inc. + * License: Apache-2.0 + * See LICENSE-APACHE for complete license terms + * + * Adapted from the x11 submodule of the arboard project https://github.com/1Password/arboard + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + * Copyright 2022 The Arboard contributors + * + * The project to which this file belongs is licensed under either of + * the Apache 2.0 or the MIT license at the licensee's choice. The terms + * and conditions of the chosen license apply to this file. +*/ + +// More info about using the clipboard on X11: +// https://tronche.com/gui/x/icccm/sec-2.html#s-2.6 +// https://freedesktop.org/wiki/ClipboardManager/ + +use std::{ + borrow::Cow, + cell::RefCell, + collections::{HashMap, hash_map::Entry}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + thread::JoinHandle, + thread_local, + time::{Duration, Instant}, +}; + +use parking_lot::{Condvar, Mutex, MutexGuard, RwLock}; +use x11rb::{ + COPY_DEPTH_FROM_PARENT, COPY_FROM_PARENT, NONE, + connection::Connection, + protocol::{ + Event, + xproto::{ + Atom, AtomEnum, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, Property, + PropertyNotifyEvent, SELECTION_NOTIFY_EVENT, SelectionNotifyEvent, + SelectionRequestEvent, Time, WindowClass, + }, + }, + rust_connection::RustConnection, + wrapper::ConnectionExt as _, +}; + +use gpui::{ClipboardItem, Image, ImageFormat, hash}; +use strum::IntoEnumIterator; + +type Result = std::result::Result; + +static CLIPBOARD: Mutex> = parking_lot::const_mutex(None); + +x11rb::atom_manager! { + pub Atoms: AtomCookies { + CLIPBOARD, + PRIMARY, + SECONDARY, + + CLIPBOARD_MANAGER, + SAVE_TARGETS, + TARGETS, + ATOM, + INCR, + + UTF8_STRING, + UTF8_MIME_0: b"text/plain;charset=utf-8", + UTF8_MIME_1: b"text/plain;charset=UTF-8", + // Text in ISO Latin-1 encoding + // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 + STRING, + // Text in unknown encoding + // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 + TEXT, + TEXT_MIME_UNKNOWN: b"text/plain", + + // HTML: b"text/html", + // URI_LIST: b"text/uri-list", + + PNG__MIME: ImageFormat::mime_type(ImageFormat::Png ).as_bytes(), + JPEG_MIME: ImageFormat::mime_type(ImageFormat::Jpeg).as_bytes(), + WEBP_MIME: ImageFormat::mime_type(ImageFormat::Webp).as_bytes(), + GIF__MIME: ImageFormat::mime_type(ImageFormat::Gif ).as_bytes(), + SVG__MIME: ImageFormat::mime_type(ImageFormat::Svg ).as_bytes(), + BMP__MIME: ImageFormat::mime_type(ImageFormat::Bmp ).as_bytes(), + TIFF_MIME: ImageFormat::mime_type(ImageFormat::Tiff).as_bytes(), + ICO__MIME: ImageFormat::mime_type(ImageFormat::Ico ).as_bytes(), + PNM__MIME: ImageFormat::mime_type(ImageFormat::Pnm ).as_bytes(), + // This is just some random name for the property on our window, into which + // the clipboard owner writes the data we requested. + ARBOARD_CLIPBOARD, + } +} + +thread_local! { + static ATOM_NAME_CACHE: RefCell> = Default::default(); +} + +// Some clipboard items, like images, may take a very long time to produce a +// `SelectionNotify`. Multiple seconds long. +const LONG_TIMEOUT_DUR: Duration = Duration::from_millis(4000); +const SHORT_TIMEOUT_DUR: Duration = Duration::from_millis(10); + +#[derive(Debug, PartialEq, Eq)] +enum ManagerHandoverState { + Idle, + InProgress, + Finished, +} + +struct GlobalClipboard { + inner: Arc, + + /// Join handle to the thread which serves selection requests. + server_handle: JoinHandle<()>, +} + +struct XContext { + conn: RustConnection, + win_id: u32, +} + +struct Inner { + /// The context for the thread which serves clipboard read + /// requests coming to us. + server: XContext, + atoms: Atoms, + + clipboard: Selection, + primary: Selection, + secondary: Selection, + + handover_state: Mutex, + handover_cv: Condvar, + + serve_stopped: AtomicBool, +} + +impl XContext { + fn new() -> Result { + // create a new connection to an X11 server + let (conn, screen_num): (RustConnection, _) = + RustConnection::connect(None).map_err(|_| { + Error::unknown("X11 server connection timed out because it was unreachable") + })?; + let screen = conn + .setup() + .roots + .get(screen_num) + .ok_or(Error::unknown("no screen found"))?; + let win_id = conn.generate_id().map_err(into_unknown)?; + + let event_mask = + // Just in case that some program reports SelectionNotify events + // with XCB_EVENT_MASK_PROPERTY_CHANGE mask. + EventMask::PROPERTY_CHANGE | + // To receive DestroyNotify event and stop the message loop. + EventMask::STRUCTURE_NOTIFY; + // create the window + conn.create_window( + // copy as much as possible from the parent, because no other specific input is needed + COPY_DEPTH_FROM_PARENT, + win_id, + screen.root, + 0, + 0, + 1, + 1, + 0, + WindowClass::COPY_FROM_PARENT, + COPY_FROM_PARENT, + // don't subscribe to any special events because we are requesting everything we need ourselves + &CreateWindowAux::new().event_mask(event_mask), + ) + .map_err(into_unknown)?; + conn.flush().map_err(into_unknown)?; + + Ok(Self { conn, win_id }) + } +} + +#[derive(Default)] +struct Selection { + data: RwLock>>, + /// Mutex around nothing to use with the below condvar. + mutex: Mutex<()>, + /// A condvar that is notified when the contents of this clipboard are changed. + /// + /// This is associated with `Self::mutex`. + data_changed: Condvar, +} + +#[derive(Debug, Clone)] +struct ClipboardData { + bytes: Vec, + + /// The atom representing the format in which the data is encoded. + format: Atom, +} + +enum ReadSelNotifyResult { + GotData(ClipboardData), + IncrStarted, + EventNotRecognized, +} + +impl Inner { + fn new() -> Result { + let server = XContext::new()?; + let atoms = Atoms::new(&server.conn) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)?; + + Ok(Self { + server, + atoms, + clipboard: Selection::default(), + primary: Selection::default(), + secondary: Selection::default(), + handover_state: Mutex::new(ManagerHandoverState::Idle), + handover_cv: Condvar::new(), + serve_stopped: AtomicBool::new(false), + }) + } + + fn write( + &self, + data: Vec, + selection: ClipboardKind, + wait: WaitConfig, + ) -> Result<()> { + if self.serve_stopped.load(Ordering::Relaxed) { + return Err(Error::unknown( + "The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)", + )); + } + + let server_win = self.server.win_id; + + // ICCCM version 2, section 2.6.1.3 states that we should re-assert ownership whenever data + // changes. + self.server + .conn + .set_selection_owner(server_win, self.atom_of(selection), Time::CURRENT_TIME) + .map_err(|_| Error::ClipboardOccupied)?; + + self.server.conn.flush().map_err(into_unknown)?; + + // Just setting the data, and the `serve_requests` will take care of the rest. + let selection = self.selection_of(selection); + let mut data_guard = selection.data.write(); + *data_guard = Some(data); + + // Lock the mutex to both ensure that no wakers of `data_changed` can wake us between + // dropping the `data_guard` and calling `wait[_for]` and that we don't we wake other + // threads in that position. + let mut guard = selection.mutex.lock(); + + // Notify any existing waiting threads that we have changed the data in the selection. + // It is important that the mutex is locked to prevent this notification getting lost. + selection.data_changed.notify_all(); + + match wait { + WaitConfig::None => {} + WaitConfig::Forever => { + drop(data_guard); + selection.data_changed.wait(&mut guard); + } + WaitConfig::Until(deadline) => { + drop(data_guard); + selection.data_changed.wait_until(&mut guard, deadline); + } + } + + Ok(()) + } + + /// `formats` must be a slice of atoms, where each atom represents a target format. + /// The first format from `formats`, which the clipboard owner supports will be the + /// format of the return value. + fn read(&self, formats: &[Atom], selection: ClipboardKind) -> Result { + // if we are the current owner, we can get the current clipboard ourselves + if self.is_owner(selection)? { + let data = self.selection_of(selection).data.read(); + if let Some(data_list) = &*data { + for data in data_list { + for format in formats { + if *format == data.format { + return Ok(data.clone()); + } + } + } + } + return Err(Error::ContentNotAvailable); + } + let reader = XContext::new()?; + + let highest_precedence_format = + match self.read_single(&reader, selection, self.atoms.TARGETS) { + Err(err) => { + log::trace!("Clipboard TARGETS query failed with {err:?}"); + None + } + Ok(ClipboardData { bytes, format }) => { + if format == self.atoms.ATOM { + let available_formats = Self::parse_formats(&bytes); + formats + .iter() + .find(|format| available_formats.contains(format)) + } else { + log::trace!( + "Unexpected clipboard TARGETS format {}", + self.atom_name(format) + ); + None + } + } + }; + + if let Some(&format) = highest_precedence_format { + let data = self.read_single(&reader, selection, format)?; + if !formats.contains(&data.format) { + // This shouldn't happen since the format is from the TARGETS list. + log::trace!( + "Conversion to {} responded with {} which is not supported", + self.atom_name(format), + self.atom_name(data.format), + ); + return Err(Error::ConversionFailure); + } + return Ok(data); + } + + log::trace!("Falling back on attempting to convert clipboard to each format."); + for format in formats { + match self.read_single(&reader, selection, *format) { + Ok(data) => { + if formats.contains(&data.format) { + return Ok(data); + } else { + log::trace!( + "Conversion to {} responded with {} which is not supported", + self.atom_name(*format), + self.atom_name(data.format), + ); + continue; + } + } + Err(Error::ContentNotAvailable) => { + continue; + } + Err(e) => { + log::trace!("Conversion to {} failed: {}", self.atom_name(*format), e); + return Err(e); + } + } + } + log::trace!("All conversions to supported formats failed."); + Err(Error::ContentNotAvailable) + } + + fn parse_formats(bytes: &[u8]) -> Vec { + bytes + .chunks_exact(4) + .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) + .collect() + } + + fn read_single( + &self, + reader: &XContext, + selection: ClipboardKind, + target_format: Atom, + ) -> Result { + // Delete the property so that we can detect (using property notify) + // when the selection owner receives our request. + reader + .conn + .delete_property(reader.win_id, self.atoms.ARBOARD_CLIPBOARD) + .map_err(into_unknown)?; + + // request to convert the clipboard selection to our data type(s) + reader + .conn + .convert_selection( + reader.win_id, + self.atom_of(selection), + target_format, + self.atoms.ARBOARD_CLIPBOARD, + Time::CURRENT_TIME, + ) + .map_err(into_unknown)?; + reader.conn.sync().map_err(into_unknown)?; + + log::trace!("Finished `convert_selection`"); + + let mut incr_data: Vec = Vec::new(); + let mut using_incr = false; + + let mut timeout_end = Instant::now() + LONG_TIMEOUT_DUR; + + while Instant::now() < timeout_end { + let event = reader.conn.poll_for_event().map_err(into_unknown)?; + let event = match event { + Some(e) => e, + None => { + std::thread::sleep(Duration::from_millis(1)); + continue; + } + }; + match event { + // The first response after requesting a selection. + Event::SelectionNotify(event) => { + log::trace!("Read SelectionNotify"); + let result = self.handle_read_selection_notify( + reader, + target_format, + &mut using_incr, + &mut incr_data, + event, + )?; + match result { + ReadSelNotifyResult::GotData(data) => return Ok(data), + ReadSelNotifyResult::IncrStarted => { + // This means we received an indication that an the + // data is going to be sent INCRementally. Let's + // reset our timeout. + timeout_end += SHORT_TIMEOUT_DUR; + } + ReadSelNotifyResult::EventNotRecognized => (), + } + } + // If the previous SelectionNotify event specified that the data + // will be sent in INCR segments, each segment is transferred in + // a PropertyNotify event. + Event::PropertyNotify(event) => { + let result = self.handle_read_property_notify( + reader, + target_format, + using_incr, + &mut incr_data, + &mut timeout_end, + event, + )?; + if result { + return Ok(ClipboardData { + bytes: incr_data, + format: target_format, + }); + } + } + _ => log::trace!( + "An unexpected event arrived while reading the clipboard: {:?}", + event + ), + } + } + log::info!("Time-out hit while reading the clipboard."); + Err(Error::ContentNotAvailable) + } + + fn atom_of(&self, selection: ClipboardKind) -> Atom { + match selection { + ClipboardKind::Clipboard => self.atoms.CLIPBOARD, + ClipboardKind::Primary => self.atoms.PRIMARY, + ClipboardKind::Secondary => self.atoms.SECONDARY, + } + } + + fn selection_of(&self, selection: ClipboardKind) -> &Selection { + match selection { + ClipboardKind::Clipboard => &self.clipboard, + ClipboardKind::Primary => &self.primary, + ClipboardKind::Secondary => &self.secondary, + } + } + + fn kind_of(&self, atom: Atom) -> Option { + match atom { + a if a == self.atoms.CLIPBOARD => Some(ClipboardKind::Clipboard), + a if a == self.atoms.PRIMARY => Some(ClipboardKind::Primary), + a if a == self.atoms.SECONDARY => Some(ClipboardKind::Secondary), + _ => None, + } + } + + fn is_owner(&self, selection: ClipboardKind) -> Result { + let current = self + .server + .conn + .get_selection_owner(self.atom_of(selection)) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)? + .owner; + + Ok(current == self.server.win_id) + } + + fn query_atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> Result { + String::from_utf8( + self.server + .conn + .get_atom_name(atom) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)? + .name, + ) + .map_err(into_unknown) + } + + fn atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> &'static str { + ATOM_NAME_CACHE.with(|cache| { + let mut cache = cache.borrow_mut(); + match cache.entry(atom) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let s = self + .query_atom_name(atom) + .map(|s| Box::leak(s.into_boxed_str()) as &str) + .unwrap_or("FAILED-TO-GET-THE-ATOM-NAME"); + entry.insert(s); + s + } + } + }) + } + + fn handle_read_selection_notify( + &self, + reader: &XContext, + target_format: u32, + using_incr: &mut bool, + incr_data: &mut Vec, + event: SelectionNotifyEvent, + ) -> Result { + // The property being set to NONE means that the `convert_selection` + // failed. + + // According to: https://tronche.com/gui/x/icccm/sec-2.html#s-2.4 + // the target must be set to the same as what we requested. + if event.property == NONE || event.target != target_format { + return Err(Error::ContentNotAvailable); + } + if self.kind_of(event.selection).is_none() { + log::info!( + "Received a SelectionNotify for a selection other than CLIPBOARD, PRIMARY or SECONDARY. This is unexpected." + ); + return Ok(ReadSelNotifyResult::EventNotRecognized); + } + if *using_incr { + log::warn!("Received a SelectionNotify while already expecting INCR segments."); + return Ok(ReadSelNotifyResult::EventNotRecognized); + } + // Accept any property type. The property type will typically match the format type except + // when it is `TARGETS` in which case it is `ATOM`. `ANY` is provided to handle the case + // where the clipboard is not convertible to the requested format. In this case + // `reply.type_` will have format information, but `bytes` will only be non-empty if `ANY` + // is provided. + let property_type = AtomEnum::ANY; + // request the selection + let mut reply = reader + .conn + .get_property( + true, + event.requestor, + event.property, + property_type, + 0, + u32::MAX / 4, + ) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)?; + + // we found something + if reply.type_ == self.atoms.INCR { + // Note that we call the get_property again because we are + // indicating that we are ready to receive the data by deleting the + // property, however deleting only works if the type matches the + // property type. But the type didn't match in the previous call. + reply = reader + .conn + .get_property( + true, + event.requestor, + event.property, + self.atoms.INCR, + 0, + u32::MAX / 4, + ) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)?; + log::trace!("Receiving INCR segments"); + *using_incr = true; + if reply.value_len == 4 { + let min_data_len = reply + .value32() + .and_then(|mut vals| vals.next()) + .unwrap_or(0); + incr_data.reserve(min_data_len as usize); + } + Ok(ReadSelNotifyResult::IncrStarted) + } else { + Ok(ReadSelNotifyResult::GotData(ClipboardData { + bytes: reply.value, + format: reply.type_, + })) + } + } + + /// Returns Ok(true) when the incr_data is ready + fn handle_read_property_notify( + &self, + reader: &XContext, + target_format: u32, + using_incr: bool, + incr_data: &mut Vec, + timeout_end: &mut Instant, + event: PropertyNotifyEvent, + ) -> Result { + if event.atom != self.atoms.ARBOARD_CLIPBOARD || event.state != Property::NEW_VALUE { + return Ok(false); + } + if !using_incr { + // This must mean the selection owner received our request, and is + // now preparing the data + return Ok(false); + } + let reply = reader + .conn + .get_property( + true, + event.window, + event.atom, + if target_format == self.atoms.TARGETS { + self.atoms.ATOM + } else { + target_format + }, + 0, + u32::MAX / 4, + ) + .map_err(into_unknown)? + .reply() + .map_err(into_unknown)?; + + // log::trace!("Received segment. value_len {}", reply.value_len,); + if reply.value_len == 0 { + // This indicates that all the data has been sent. + return Ok(true); + } + incr_data.extend(reply.value); + + // Let's reset our timeout, since we received a valid chunk. + *timeout_end = Instant::now() + SHORT_TIMEOUT_DUR; + + // Not yet complete + Ok(false) + } + + fn handle_selection_request(&self, event: SelectionRequestEvent) -> Result<()> { + let selection = match self.kind_of(event.selection) { + Some(kind) => kind, + None => { + log::warn!( + "Received a selection request to a selection other than the CLIPBOARD, PRIMARY or SECONDARY. This is unexpected." + ); + return Ok(()); + } + }; + + let success; + // we are asked for a list of supported conversion targets + if event.target == self.atoms.TARGETS { + log::trace!( + "Handling TARGETS, dst property is {}", + self.atom_name(event.property) + ); + let mut targets = Vec::with_capacity(10); + targets.push(self.atoms.TARGETS); + targets.push(self.atoms.SAVE_TARGETS); + let data = self.selection_of(selection).data.read(); + if let Some(data_list) = &*data { + for data in data_list { + targets.push(data.format); + if data.format == self.atoms.UTF8_STRING { + // When we are storing a UTF8 string, + // add all equivalent formats to the supported targets + targets.push(self.atoms.UTF8_MIME_0); + targets.push(self.atoms.UTF8_MIME_1); + } + } + } + self.server + .conn + .change_property32( + PropMode::REPLACE, + event.requestor, + event.property, + // TODO: change to `AtomEnum::ATOM` + self.atoms.ATOM, + &targets, + ) + .map_err(into_unknown)?; + self.server.conn.flush().map_err(into_unknown)?; + success = true; + } else { + log::trace!("Handling request for (probably) the clipboard contents."); + let data = self.selection_of(selection).data.read(); + if let Some(data_list) = &*data { + success = match data_list.iter().find(|d| d.format == event.target) { + Some(data) => { + self.server + .conn + .change_property8( + PropMode::REPLACE, + event.requestor, + event.property, + event.target, + &data.bytes, + ) + .map_err(into_unknown)?; + self.server.conn.flush().map_err(into_unknown)?; + true + } + None => false, + }; + } else { + // This must mean that we lost ownership of the data + // since the other side requested the selection. + // Let's respond with the property set to none. + success = false; + } + } + // on failure we notify the requester of it + let property = if success { + event.property + } else { + AtomEnum::NONE.into() + }; + // tell the requestor that we finished sending data + self.server + .conn + .send_event( + false, + event.requestor, + EventMask::NO_EVENT, + SelectionNotifyEvent { + response_type: SELECTION_NOTIFY_EVENT, + sequence: event.sequence, + time: event.time, + requestor: event.requestor, + selection: event.selection, + target: event.target, + property, + }, + ) + .map_err(into_unknown)?; + + self.server.conn.flush().map_err(into_unknown) + } + + fn ask_clipboard_manager_to_request_our_data(&self) -> Result<()> { + if self.server.win_id == 0 { + // This shouldn't really ever happen but let's just check. + log::error!("The server's window id was 0. This is unexpected"); + return Ok(()); + } + + if !self.is_owner(ClipboardKind::Clipboard)? { + // We are not owning the clipboard, nothing to do. + return Ok(()); + } + if self + .selection_of(ClipboardKind::Clipboard) + .data + .read() + .is_none() + { + // If we don't have any data, there's nothing to do. + return Ok(()); + } + + // It's important that we lock the state before sending the request + // because we don't want the request server thread to lock the state + // after the request but before we can lock it here. + let mut handover_state = self.handover_state.lock(); + + log::trace!("Sending the data to the clipboard manager"); + self.server + .conn + .convert_selection( + self.server.win_id, + self.atoms.CLIPBOARD_MANAGER, + self.atoms.SAVE_TARGETS, + self.atoms.ARBOARD_CLIPBOARD, + Time::CURRENT_TIME, + ) + .map_err(into_unknown)?; + self.server.conn.flush().map_err(into_unknown)?; + + *handover_state = ManagerHandoverState::InProgress; + let max_handover_duration = Duration::from_millis(100); + + // Note that we are using a parking_lot condvar here, which doesn't wake up + // spuriously + let result = self + .handover_cv + .wait_for(&mut handover_state, max_handover_duration); + + if *handover_state == ManagerHandoverState::Finished { + return Ok(()); + } + if result.timed_out() { + log::warn!( + "Could not hand the clipboard contents over to the clipboard manager. The request timed out." + ); + return Ok(()); + } + + Err(Error::unknown( + "The handover was not finished and the condvar didn't time out, yet the condvar wait ended. This should be unreachable.", + )) + } +} + +fn serve_requests(context: Arc) -> Result<(), Box> { + fn handover_finished(clip: &Arc, mut handover_state: MutexGuard) { + log::trace!("Finishing clipboard manager handover."); + *handover_state = ManagerHandoverState::Finished; + + // Not sure if unlocking the mutex is necessary here but better safe than sorry. + drop(handover_state); + + clip.handover_cv.notify_all(); + } + + log::trace!("Started serve requests thread."); + + let _guard = gpui_util::defer(|| { + context.serve_stopped.store(true, Ordering::Relaxed); + }); + + let mut written = false; + let mut notified = false; + + loop { + match context.server.conn.wait_for_event().map_err(into_unknown)? { + Event::DestroyNotify(_) => { + // This window is being destroyed. + log::trace!("Clipboard server window is being destroyed x_x"); + return Ok(()); + } + Event::SelectionClear(event) => { + // TODO: check if this works + // Someone else has new content in the clipboard, so it is + // notifying us that we should delete our data now. + log::trace!("Somebody else owns the clipboard now"); + + if let Some(selection) = context.kind_of(event.selection) { + let selection = context.selection_of(selection); + let mut data_guard = selection.data.write(); + *data_guard = None; + + // It is important that this mutex is locked at the time of calling + // `notify_all` to prevent notifications getting lost in case the sleeping + // thread has unlocked its `data_guard` and is just about to sleep. + // It is also important that the RwLock is kept write-locked for the same + // reason. + let _guard = selection.mutex.lock(); + selection.data_changed.notify_all(); + } + } + Event::SelectionRequest(event) => { + log::trace!( + "SelectionRequest - selection is: {}, target is {}", + context.atom_name(event.selection), + context.atom_name(event.target), + ); + // Someone is requesting the clipboard content from us. + context + .handle_selection_request(event) + .map_err(into_unknown)?; + + // if we are in the progress of saving to the clipboard manager + // make sure we save that we have finished writing + let handover_state = context.handover_state.lock(); + if *handover_state == ManagerHandoverState::InProgress { + // Only set written, when the actual contents were written, + // not just a response to what TARGETS we have. + if event.target != context.atoms.TARGETS { + log::trace!("The contents were written to the clipboard manager."); + written = true; + // if we have written and notified, make sure to notify that we are done + if notified { + handover_finished(&context, handover_state); + } + } + } + } + Event::SelectionNotify(event) => { + // We've requested the clipboard content and this is the answer. + // Considering that this thread is not responsible for reading + // clipboard contents, this must come from the clipboard manager + // signaling that the data was handed over successfully. + if event.selection != context.atoms.CLIPBOARD_MANAGER { + log::error!( + "Received a `SelectionNotify` from a selection other than the CLIPBOARD_MANAGER. This is unexpected in this thread." + ); + continue; + } + let handover_state = context.handover_state.lock(); + if *handover_state == ManagerHandoverState::InProgress { + // Note that some clipboard managers send a selection notify + // before even sending a request for the actual contents. + // (That's why we use the "notified" & "written" flags) + log::trace!( + "The clipboard manager indicated that it's done requesting the contents from us." + ); + notified = true; + + // One would think that we could also finish if the property + // here is set 0, because that indicates failure. However + // this is not the case; for example on KDE plasma 5.18, we + // immediately get a SelectionNotify with property set to 0, + // but following that, we also get a valid SelectionRequest + // from the clipboard manager. + if written { + handover_finished(&context, handover_state); + } + } + } + _event => { + // May be useful for debugging but nothing else really. + //log::trace!("Received unwanted event: {:?}", event); + } + } + } +} + +pub(crate) struct Clipboard { + inner: Arc, +} + +impl Clipboard { + pub(crate) fn new() -> Result { + let mut global_cb = CLIPBOARD.lock(); + if let Some(global_cb) = &*global_cb { + return Ok(Self { + inner: Arc::clone(&global_cb.inner), + }); + } + // At this point we know that the clipboard does not exist. + let ctx = Arc::new(Inner::new()?); + let join_handle = std::thread::Builder::new() + .name("Clipboard".to_owned()) + .spawn({ + let ctx = Arc::clone(&ctx); + move || { + if let Err(error) = serve_requests(ctx) { + log::error!("Worker thread errored with: {}", error); + } + } + }) + .unwrap(); + *global_cb = Some(GlobalClipboard { + inner: Arc::clone(&ctx), + server_handle: join_handle, + }); + Ok(Self { inner: ctx }) + } + + pub(crate) fn set_text( + &self, + message: Cow<'_, str>, + selection: ClipboardKind, + wait: WaitConfig, + ) -> Result<()> { + let data = vec![ClipboardData { + bytes: message.into_owned().into_bytes(), + format: self.inner.atoms.UTF8_STRING, + }]; + self.inner.write(data, selection, wait) + } + + fn image_format_atom(&self, format: ImageFormat) -> Atom { + match format { + ImageFormat::Png => self.inner.atoms.PNG__MIME, + ImageFormat::Jpeg => self.inner.atoms.JPEG_MIME, + ImageFormat::Webp => self.inner.atoms.WEBP_MIME, + ImageFormat::Gif => self.inner.atoms.GIF__MIME, + ImageFormat::Svg => self.inner.atoms.SVG__MIME, + ImageFormat::Bmp => self.inner.atoms.BMP__MIME, + ImageFormat::Tiff => self.inner.atoms.TIFF_MIME, + ImageFormat::Ico => self.inner.atoms.ICO__MIME, + ImageFormat::Pnm => self.inner.atoms.PNM__MIME, + } + } + + #[allow(unused)] + pub(crate) fn set_image( + &self, + image: Image, + selection: ClipboardKind, + wait: WaitConfig, + ) -> Result<()> { + let format = self.image_format_atom(image.format); + let data = vec![ClipboardData { + bytes: image.bytes, + format: self.inner.atoms.PNG__MIME, + }]; + self.inner.write(data, selection, wait) + } + + pub(crate) fn get_any(&self, selection: ClipboardKind) -> Result { + let image_entries = ImageFormat::iter() + .map(|format| (self.image_format_atom(format), format)) + .collect::>(); + + let text_format_atoms: &[Atom] = &[ + self.inner.atoms.UTF8_STRING, + self.inner.atoms.UTF8_MIME_0, + self.inner.atoms.UTF8_MIME_1, + self.inner.atoms.STRING, + self.inner.atoms.TEXT, + self.inner.atoms.TEXT_MIME_UNKNOWN, + ]; + + // image formats first, as they are more specific, and read will return the first + // format that the contents can be converted to + let mut format_atoms = Vec::with_capacity(image_entries.len() + text_format_atoms.len()); + format_atoms.extend(image_entries.iter().map(|(atom, _)| *atom)); + format_atoms.extend_from_slice(text_format_atoms); + + let result = self.inner.read(&format_atoms, selection)?; + + log::trace!( + "read clipboard as format {:?}", + self.inner.atom_name(result.format) + ); + + for (format_atom, image_format) in image_entries { + if result.format == format_atom { + let bytes = result.bytes; + let id = hash(&bytes); + return Ok(ClipboardItem::new_image(&Image { + id, + format: image_format, + bytes, + })); + } + } + + let text = if result.format == self.inner.atoms.STRING { + // ISO Latin-1 + // See: https://stackoverflow.com/questions/28169745/what-are-the-options-to-convert-iso-8859-1-latin-1-to-a-string-utf-8 + result.bytes.into_iter().map(|c| c as char).collect() + } else { + String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)? + }; + Ok(ClipboardItem::new_string(text)) + } + + pub fn is_owner(&self, selection: ClipboardKind) -> bool { + self.inner.is_owner(selection).unwrap_or(false) + } +} + +impl Drop for Clipboard { + fn drop(&mut self) { + // There are always at least 3 owners: + // the global, the server thread, and one `Clipboard::inner` + const MIN_OWNERS: usize = 3; + + // We start with locking the global guard to prevent race + // conditions below. + let mut global_cb = CLIPBOARD.lock(); + if Arc::strong_count(&self.inner) == MIN_OWNERS { + // If the are the only owners of the clipboard are ourselves and + // the global object, then we should destroy the global object, + // and send the data to the clipboard manager + + if let Err(e) = self.inner.ask_clipboard_manager_to_request_our_data() { + log::error!( + "Could not hand the clipboard data over to the clipboard manager: {}", + e + ); + } + let global_cb = global_cb.take(); + if let Err(e) = self + .inner + .server + .conn + .destroy_window(self.inner.server.win_id) + { + log::error!("Failed to destroy the clipboard window. Error: {}", e); + return; + } + if let Err(e) = self.inner.server.conn.flush() { + log::error!("Failed to flush the clipboard window. Error: {}", e); + return; + } + if let Some(global_cb) = global_cb + && let Err(e) = global_cb.server_handle.join() + { + // Let's try extracting the error message + let message; + if let Some(msg) = e.downcast_ref::<&'static str>() { + message = Some((*msg).to_string()); + } else if let Some(msg) = e.downcast_ref::() { + message = Some(msg.clone()); + } else { + message = None; + } + if let Some(message) = message { + log::error!( + "The clipboard server thread panicked. Panic message: '{}'", + message, + ); + } else { + log::error!("The clipboard server thread panicked."); + } + } + } + } +} + +fn into_unknown(error: E) -> Error { + Error::Unknown { + description: error.to_string(), + } +} + +/// Clipboard selection +/// +/// Linux has a concept of clipboard "selections" which tend to be used in different contexts. This +/// enum provides a way to get/set to a specific clipboard +/// +/// See for a better +/// description of the different clipboards. +#[derive(Copy, Clone, Debug)] +pub enum ClipboardKind { + /// Typically used selection for explicit cut/copy/paste actions (ie. windows/macos like + /// clipboard behavior) + Clipboard, + + /// Typically used for mouse selections and/or currently selected text. Accessible via middle + /// mouse click. + Primary, + + /// The secondary clipboard is rarely used but theoretically available on X11. + Secondary, +} + +/// Configuration on how long to wait for a new X11 copy event is emitted. +#[derive(Default)] +pub(crate) enum WaitConfig { + /// Waits until the given [`Instant`] has reached. + #[allow( + unused, + reason = "Right now we don't wait for clipboard contents to sync on app close, but we may in the future" + )] + Until(Instant), + + /// Waits forever until a new event is reached. + #[allow(unused)] + #[allow( + unused, + reason = "Right now we don't wait for clipboard contents to sync on app close, but we may in the future" + )] + Forever, + + /// It shouldn't wait. + #[default] + None, +} + +#[non_exhaustive] +pub enum Error { + /// The clipboard contents were not available in the requested format. + /// This could either be due to the clipboard being empty or the clipboard contents having + /// an incompatible format to the requested one (eg when calling `get_image` on text) + ContentNotAvailable, + + /// The native clipboard is not accessible due to being held by an other party. + /// + /// This "other party" could be a different process or it could be within + /// the same program. So for example you may get this error when trying + /// to interact with the clipboard from multiple threads at once. + /// + /// Note that it's OK to have multiple `Clipboard` instances. The underlying + /// implementation will make sure that the native clipboard is only + /// opened for transferring data and then closed as soon as possible. + ClipboardOccupied, + + /// The image or the text that was about the be transferred to/from the clipboard could not be + /// converted to the appropriate format. + ConversionFailure, + + /// Any error that doesn't fit the other error types. + /// + /// The `description` field is only meant to help the developer and should not be relied on as a + /// means to identify an error case during runtime. + Unknown { description: String }, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ContentNotAvailable => f.write_str("The clipboard contents were not available in the requested format or the clipboard is empty."), + Error::ClipboardOccupied => f.write_str("The native clipboard is not accessible due to being held by an other party."), + Error::ConversionFailure => f.write_str("The image or the text that was about the be transferred to/from the clipboard could not be converted to the appropriate format."), + Error::Unknown { description } => f.write_fmt(format_args!("Unknown error while interacting with the clipboard: {description}")), + } + } +} + +impl std::error::Error for Error {} + +impl std::fmt::Debug for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use Error::*; + macro_rules! kind_to_str { + ($( $e: pat ),*) => { + match self { + $( + $e => stringify!($e), + )* + } + } + } + let name = kind_to_str!( + ContentNotAvailable, + ClipboardOccupied, + ConversionFailure, + Unknown { .. } + ); + f.write_fmt(format_args!("{name} - \"{self}\"")) + } +} + +impl Error { + pub(crate) fn unknown>(message: M) -> Self { + Error::Unknown { + description: message.into(), + } + } +} diff --git a/vendor/gpui-pre-linux/src/linux/x11/display.rs b/vendor/gpui-pre-linux/src/linux/x11/display.rs new file mode 100644 index 000000000..582d76f7f --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/x11/display.rs @@ -0,0 +1,51 @@ +use anyhow::Context as _; +use uuid::Uuid; +use x11rb::{connection::Connection as _, xcb_ffi::XCBConnection}; + +use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay, Size, px}; + +#[derive(Debug)] +pub(crate) struct X11Display { + x_screen_index: usize, + bounds: Bounds, + uuid: Uuid, +} + +impl X11Display { + pub(crate) fn new( + xcb: &XCBConnection, + scale_factor: f32, + x_screen_index: usize, + ) -> anyhow::Result { + let screen = xcb + .setup() + .roots + .get(x_screen_index) + .with_context(|| format!("No screen found with index {x_screen_index}"))?; + Ok(Self { + x_screen_index, + bounds: Bounds { + origin: Default::default(), + size: Size { + width: px(screen.width_in_pixels as f32 / scale_factor), + height: px(screen.height_in_pixels as f32 / scale_factor), + }, + }, + uuid: Uuid::from_bytes([0; 16]), + }) + } +} + +impl PlatformDisplay for X11Display { + fn id(&self) -> DisplayId { + DisplayId::new(self.x_screen_index as u64) + } + + fn uuid(&self) -> anyhow::Result { + Ok(self.uuid) + } + + fn bounds(&self) -> Bounds { + self.bounds + } +} diff --git a/vendor/gpui-pre-linux/src/linux/x11/event.rs b/vendor/gpui-pre-linux/src/linux/x11/event.rs new file mode 100644 index 000000000..3fb916425 --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/x11/event.rs @@ -0,0 +1,154 @@ +use x11rb::protocol::{ + xinput, + xproto::{self, ModMask}, +}; + +use gpui::{Modifiers, MouseButton, NavigationDirection}; + +pub(crate) enum ButtonOrScroll { + Button(MouseButton), + Scroll(ScrollDirection), +} + +pub(crate) enum ScrollDirection { + Up, + Down, + Left, + Right, +} + +pub(crate) fn button_or_scroll_from_event_detail(detail: u32) -> Option { + Some(match detail { + 1 => ButtonOrScroll::Button(MouseButton::Left), + 2 => ButtonOrScroll::Button(MouseButton::Middle), + 3 => ButtonOrScroll::Button(MouseButton::Right), + 4 => ButtonOrScroll::Scroll(ScrollDirection::Up), + 5 => ButtonOrScroll::Scroll(ScrollDirection::Down), + 6 => ButtonOrScroll::Scroll(ScrollDirection::Left), + 7 => ButtonOrScroll::Scroll(ScrollDirection::Right), + 8 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Back)), + 9 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Forward)), + _ => return None, + }) +} + +pub(crate) fn modifiers_from_state(state: xproto::KeyButMask) -> Modifiers { + Modifiers { + control: state.contains(xproto::KeyButMask::CONTROL), + alt: state.contains(xproto::KeyButMask::MOD1), + shift: state.contains(xproto::KeyButMask::SHIFT), + platform: state.contains(xproto::KeyButMask::MOD4), + function: false, + } +} + +pub(crate) fn modifiers_from_xinput_info(modifier_info: xinput::ModifierInfo) -> Modifiers { + Modifiers { + control: modifier_info.effective as u16 & ModMask::CONTROL.bits() + == ModMask::CONTROL.bits(), + alt: modifier_info.effective as u16 & ModMask::M1.bits() == ModMask::M1.bits(), + shift: modifier_info.effective as u16 & ModMask::SHIFT.bits() == ModMask::SHIFT.bits(), + platform: modifier_info.effective as u16 & ModMask::M4.bits() == ModMask::M4.bits(), + function: false, + } +} + +pub(crate) fn pressed_button_from_mask(button_mask: u32) -> Option { + Some(if button_mask & 2 == 2 { + MouseButton::Left + } else if button_mask & 4 == 4 { + MouseButton::Middle + } else if button_mask & 8 == 8 { + MouseButton::Right + } else { + return None; + }) +} + +pub(crate) fn get_valuator_axis_index( + valuator_mask: &Vec, + valuator_number: u16, +) -> Option { + // XInput valuator masks have a 1 at the bit indexes corresponding to each + // valuator present in this event's axisvalues. Axisvalues is ordered from + // lowest valuator number to highest, so counting bits before the 1 bit for + // this valuator yields the index in axisvalues. + if bit_is_set_in_vec(valuator_mask, valuator_number) { + Some(popcount_upto_bit_index(valuator_mask, valuator_number) as usize) + } else { + None + } +} + +/// Returns the number of 1 bits in `bit_vec` for all bits where `i < bit_index`. +fn popcount_upto_bit_index(bit_vec: &Vec, bit_index: u16) -> u32 { + let array_index = bit_index as usize / 32; + let popcount: u32 = bit_vec + .get(array_index) + .map_or(0, |bits| keep_bits_upto(*bits, bit_index % 32).count_ones()); + if array_index == 0 { + popcount + } else { + // Valuator numbers over 32 probably never occur for scroll position, but may as well + // support it. + let leading_popcount: u32 = bit_vec + .iter() + .take(array_index) + .map(|bits| bits.count_ones()) + .sum(); + popcount + leading_popcount + } +} + +fn bit_is_set_in_vec(bit_vec: &Vec, bit_index: u16) -> bool { + let array_index = bit_index as usize / 32; + bit_vec + .get(array_index) + .is_some_and(|bits| bit_is_set(*bits, bit_index % 32)) +} + +fn bit_is_set(bits: u32, bit_index: u16) -> bool { + bits & (1 << bit_index) != 0 +} + +/// Sets every bit with `i >= bit_index` to 0. +fn keep_bits_upto(bits: u32, bit_index: u16) -> u32 { + if bit_index == 0 { + 0 + } else if bit_index >= 32 { + u32::MAX + } else { + bits & ((1 << bit_index) - 1) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_valuator_axis_index() { + assert!(get_valuator_axis_index(&vec![0b11], 0) == Some(0)); + assert!(get_valuator_axis_index(&vec![0b11], 1) == Some(1)); + assert!(get_valuator_axis_index(&vec![0b11], 2) == None); + + assert!(get_valuator_axis_index(&vec![0b100], 0) == None); + assert!(get_valuator_axis_index(&vec![0b100], 1) == None); + assert!(get_valuator_axis_index(&vec![0b100], 2) == Some(0)); + assert!(get_valuator_axis_index(&vec![0b100], 3) == None); + + assert!(get_valuator_axis_index(&vec![0b1010, 0], 0) == None); + assert!(get_valuator_axis_index(&vec![0b1010, 0], 1) == Some(0)); + assert!(get_valuator_axis_index(&vec![0b1010, 0], 2) == None); + assert!(get_valuator_axis_index(&vec![0b1010, 0], 3) == Some(1)); + + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 0) == None); + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 1) == Some(0)); + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 2) == None); + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 3) == Some(1)); + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 32) == Some(2)); + assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 33) == None); + + assert!(get_valuator_axis_index(&vec![0b1010, 0b101], 34) == Some(3)); + } +} diff --git a/vendor/gpui-pre-linux/src/linux/x11/window.rs b/vendor/gpui-pre-linux/src/linux/x11/window.rs new file mode 100644 index 000000000..56b3d8a90 --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/x11/window.rs @@ -0,0 +1,2051 @@ +use anyhow::{Context as _, anyhow}; +use x11rb::connection::RequestConnection; + +use crate::linux::X11ClientStatePtr; +use gpui::{ + AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, GpuSpecs, Modifiers, + Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, + Point, PromptButton, PromptLevel, RequestFrameOptions, ResizeEdge, ScaledPixels, Scene, Size, + Tiling, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, + WindowDecorations, WindowKind, WindowParams, WindowVisibility, popup::PopupNotSupportedError, + px, +}; +use gpui_wgpu::{CompositorGpuHint, WgpuRenderer, WgpuSurfaceConfig}; + +use collections::FxHashSet; +use gpui_util::{ResultExt, maybe}; +use raw_window_handle as rwh; +use x11rb::{ + connection::Connection, + cookie::{Cookie, VoidCookie}, + errors::ConnectionError, + properties::{WmHints, WmSizeHints}, + protocol::{ + sync, + xinput::{self, ConnectionExt as _}, + xproto::{self, ClientMessageEvent, ConnectionExt, TranslateCoordinatesReply}, + }, + wrapper::ConnectionExt as _, + xcb_ffi::XCBConnection, +}; + +use std::{ + cell::RefCell, ffi::c_void, fmt::Display, num::NonZeroU32, ptr::NonNull, rc::Rc, sync::Arc, +}; + +use super::{X11Display, XINPUT_ALL_DEVICE_GROUPS, XINPUT_ALL_DEVICES}; + +x11rb::atom_manager! { + pub XcbAtoms: AtomsCookie { + XA_ATOM, + XdndAware, + XdndStatus, + XdndEnter, + XdndLeave, + XdndPosition, + XdndSelection, + XdndDrop, + XdndFinished, + XdndTypeList, + XdndActionCopy, + TextUriList: b"text/uri-list", + UTF8_STRING, + TEXT, + STRING, + TEXT_PLAIN_UTF8: b"text/plain;charset=utf-8", + TEXT_PLAIN: b"text/plain", + XDND_DATA, + WM_PROTOCOLS, + WM_DELETE_WINDOW, + WM_CHANGE_STATE, + WM_TRANSIENT_FOR, + _NET_WM_PID, + _NET_WM_NAME, + _NET_WM_ICON, + _NET_WM_STATE, + _NET_WM_STATE_MAXIMIZED_VERT, + _NET_WM_STATE_MAXIMIZED_HORZ, + _NET_WM_STATE_FULLSCREEN, + _NET_WM_STATE_HIDDEN, + _NET_WM_STATE_FOCUSED, + _NET_ACTIVE_WINDOW, + _NET_WM_SYNC_REQUEST, + _NET_WM_SYNC_REQUEST_COUNTER, + _NET_WM_BYPASS_COMPOSITOR, + _NET_WM_MOVERESIZE, + _NET_WM_WINDOW_TYPE, + _NET_WM_WINDOW_TYPE_NOTIFICATION, + _NET_WM_WINDOW_TYPE_DIALOG, + _NET_WM_STATE_MODAL, + _NET_WM_SYNC, + _NET_SUPPORTED, + _MOTIF_WM_HINTS, + _GTK_SHOW_WINDOW_MENU, + _GTK_FRAME_EXTENTS, + _GTK_EDGE_CONSTRAINTS, + _NET_CLIENT_LIST_STACKING, + } +} + +fn query_render_extent( + xcb: &Rc, + x_window: xproto::Window, +) -> anyhow::Result> { + let reply = get_reply(|| "X11 GetGeometry failed.", xcb.get_geometry(x_window))?; + Ok(Size { + width: DevicePixels(reply.width as i32), + height: DevicePixels(reply.height as i32), + }) +} + +fn resize_edge_to_moveresize(edge: ResizeEdge) -> u32 { + match edge { + ResizeEdge::TopLeft => 0, + ResizeEdge::Top => 1, + ResizeEdge::TopRight => 2, + ResizeEdge::Right => 3, + ResizeEdge::BottomRight => 4, + ResizeEdge::Bottom => 5, + ResizeEdge::BottomLeft => 6, + ResizeEdge::Left => 7, + } +} + +#[derive(Debug)] +struct EdgeConstraints { + top_tiled: bool, + #[allow(dead_code)] + top_resizable: bool, + + right_tiled: bool, + #[allow(dead_code)] + right_resizable: bool, + + bottom_tiled: bool, + #[allow(dead_code)] + bottom_resizable: bool, + + left_tiled: bool, + #[allow(dead_code)] + left_resizable: bool, +} + +impl EdgeConstraints { + fn from_atom(atom: u32) -> Self { + EdgeConstraints { + top_tiled: (atom & (1 << 0)) != 0, + top_resizable: (atom & (1 << 1)) != 0, + right_tiled: (atom & (1 << 2)) != 0, + right_resizable: (atom & (1 << 3)) != 0, + bottom_tiled: (atom & (1 << 4)) != 0, + bottom_resizable: (atom & (1 << 5)) != 0, + left_tiled: (atom & (1 << 6)) != 0, + left_resizable: (atom & (1 << 7)) != 0, + } + } + + fn to_tiling(&self) -> Tiling { + Tiling { + top: self.top_tiled, + right: self.right_tiled, + bottom: self.bottom_tiled, + left: self.left_tiled, + } + } +} + +#[derive(Copy, Clone, Debug)] +struct Visual { + id: xproto::Visualid, + colormap: u32, + depth: u8, +} + +struct VisualSet { + inherit: Visual, + opaque: Option, + transparent: Option, + root: u32, + black_pixel: u32, +} + +fn find_visuals(xcb: &XCBConnection, screen_index: usize) -> VisualSet { + let screen = &xcb.setup().roots[screen_index]; + let mut set = VisualSet { + inherit: Visual { + id: screen.root_visual, + colormap: screen.default_colormap, + depth: screen.root_depth, + }, + opaque: None, + transparent: None, + root: screen.root, + black_pixel: screen.black_pixel, + }; + + for depth_info in screen.allowed_depths.iter() { + for visual_type in depth_info.visuals.iter() { + let visual = Visual { + id: visual_type.visual_id, + colormap: 0, + depth: depth_info.depth, + }; + log::debug!( + "Visual id: {}, class: {:?}, depth: {}, bits_per_value: {}, masks: 0x{:x} 0x{:x} 0x{:x}", + visual_type.visual_id, + visual_type.class, + depth_info.depth, + visual_type.bits_per_rgb_value, + visual_type.red_mask, + visual_type.green_mask, + visual_type.blue_mask, + ); + + if ( + visual_type.red_mask, + visual_type.green_mask, + visual_type.blue_mask, + ) != (0xFF0000, 0xFF00, 0xFF) + { + continue; + } + let color_mask = visual_type.red_mask | visual_type.green_mask | visual_type.blue_mask; + let alpha_mask = color_mask as usize ^ ((1usize << depth_info.depth) - 1); + + if alpha_mask == 0 { + if set.opaque.is_none() { + set.opaque = Some(visual); + } + } else { + if set.transparent.is_none() { + set.transparent = Some(visual); + } + } + } + } + + set +} + +#[derive(Debug, Clone, Copy)] +struct RawWindow { + connection: *mut c_void, + screen_id: usize, + window_id: u32, + visual_id: u32, +} + +// Safety: The raw pointers in RawWindow point to X11 connection +// which is valid for the window's lifetime. These are used only for +// passing to wgpu which needs Send+Sync for surface creation. +unsafe impl Send for RawWindow {} +unsafe impl Sync for RawWindow {} + +#[derive(Default)] +pub struct Callbacks { + request_frame: Option>, + input: Option gpui::DispatchEventResult>>, + active_status_change: Option>, + visibility_change: Option>, + hovered_status_change: Option>, + resize: Option, f32)>>, + moved: Option>, + should_close: Option bool>>, + close: Option>, + appearance_changed: Option>, + button_layout_changed: Option>, +} + +pub struct X11WindowState { + pub destroyed: bool, + parent: Option, + children: FxHashSet, + client: X11ClientStatePtr, + executor: ForegroundExecutor, + atoms: XcbAtoms, + x_root_window: xproto::Window, + x_screen_index: usize, + visual_id: u32, + pub(crate) counter_id: sync::Counter, + pub(crate) last_sync_counter: Option, + bounds: Bounds, + scale_factor: f32, + renderer: WgpuRenderer, + display: Rc, + input_handler: Option, + appearance: WindowAppearance, + background_appearance: WindowBackgroundAppearance, + maximized_vertical: bool, + maximized_horizontal: bool, + hidden: bool, + active: bool, + /// Owned by the client's `WindowRef`, which combines the mapped state with + /// `VisibilityNotify`; this is the last value it reported. + visibility: WindowVisibility, + hovered: bool, + pub(crate) force_render_after_recovery: bool, + fullscreen: bool, + client_side_decorations_supported: bool, + decorations: WindowDecorations, + edge_constraints: Option, + pub handle: AnyWindowHandle, + last_insets: [u32; 4], + accesskit_adapter: Option, +} + +impl X11WindowState { + fn is_transparent(&self) -> bool { + self.background_appearance != WindowBackgroundAppearance::Opaque + } +} + +#[derive(Clone)] +pub(crate) struct X11WindowStatePtr { + pub state: Rc>, + pub(crate) callbacks: Rc>, + xcb: Rc, + pub(crate) x_window: xproto::Window, +} + +impl rwh::HasWindowHandle for RawWindow { + fn window_handle(&self) -> Result, rwh::HandleError> { + let Some(non_zero) = NonZeroU32::new(self.window_id) else { + log::error!("RawWindow.window_id zero when getting window handle."); + return Err(rwh::HandleError::Unavailable); + }; + let mut handle = rwh::XcbWindowHandle::new(non_zero); + handle.visual_id = NonZeroU32::new(self.visual_id); + Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) + } +} +impl rwh::HasDisplayHandle for RawWindow { + fn display_handle(&self) -> Result, rwh::HandleError> { + let Some(non_zero) = NonNull::new(self.connection) else { + log::error!("Null RawWindow.connection when getting display handle."); + return Err(rwh::HandleError::Unavailable); + }; + let handle = rwh::XcbDisplayHandle::new(Some(non_zero), self.screen_id as i32); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) + } +} + +impl rwh::HasWindowHandle for X11Window { + fn window_handle(&self) -> Result, rwh::HandleError> { + let Some(non_zero) = NonZeroU32::new(self.0.x_window) else { + return Err(rwh::HandleError::Unavailable); + }; + let handle = rwh::XcbWindowHandle::new(non_zero); + Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) + } +} + +impl rwh::HasDisplayHandle for X11Window { + fn display_handle(&self) -> Result, rwh::HandleError> { + let connection = + as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection(&*self.0.xcb) + as *mut _; + let Some(non_zero) = NonNull::new(connection) else { + return Err(rwh::HandleError::Unavailable); + }; + let screen_id = { + let state = self.0.state.borrow(); + u64::from(state.display.id()) as i32 + }; + let handle = rwh::XcbDisplayHandle::new(Some(non_zero), screen_id); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) + } +} + +pub(crate) fn xcb_flush(xcb: &XCBConnection) { + xcb.flush() + .map_err(handle_connection_error) + .context("X11 flush failed") + .log_err(); +} + +pub(crate) fn check_reply( + failure_context: F, + result: Result, ConnectionError>, +) -> anyhow::Result<()> +where + E: Display + Send + Sync + 'static, + F: FnOnce() -> E, + C: RequestConnection, +{ + result + .map_err(handle_connection_error) + .and_then(|response| response.check().map_err(|reply_error| anyhow!(reply_error))) + .with_context(failure_context) +} + +pub(crate) fn get_reply( + failure_context: F, + result: Result, ConnectionError>, +) -> anyhow::Result +where + E: Display + Send + Sync + 'static, + F: FnOnce() -> E, + C: RequestConnection, + O: x11rb::x11_utils::TryParse, +{ + result + .map_err(handle_connection_error) + .and_then(|response| response.reply().map_err(|reply_error| anyhow!(reply_error))) + .with_context(failure_context) +} + +/// Sets or clears the ICCCM WM_HINTS urgency flag, preserving the other hints. +/// +/// Clearing when the flag isn't set is skipped: writing it back would create a +/// WM_HINTS property on windows that never requested attention, and would add an +/// X round trip to every window state change. +fn set_wm_hints_urgency(xcb: &XCBConnection, x_window: xproto::Window, urgent: bool) { + let mut hints = WmHints::new(); + match WmHints::get(xcb, x_window) { + Ok(cookie) => match cookie.reply() { + Ok(Some(existing_hints)) => hints = existing_hints, + Ok(None) => {} + Err(error) => { + log::debug!("failed to read X11 WM_HINTS before setting urgency: {error}") + } + }, + Err(error) => { + log::debug!("failed to request X11 WM_HINTS before setting urgency: {error}") + } + } + + if !urgent && !hints.urgent { + return; + } + + hints.urgent = urgent; + check_reply( + || "X11 ChangeProperty for WM_HINTS urgency failed.", + hints.set(xcb, x_window), + ) + .log_err(); + xcb_flush(xcb); +} + +/// Convert X11 connection errors to `anyhow::Error` and panic for unrecoverable errors. +pub(crate) fn handle_connection_error(err: ConnectionError) -> anyhow::Error { + match err { + ConnectionError::UnknownError => anyhow!("X11 connection: Unknown error"), + ConnectionError::UnsupportedExtension => anyhow!("X11 connection: Unsupported extension"), + ConnectionError::MaximumRequestLengthExceeded => { + anyhow!("X11 connection: Maximum request length exceeded") + } + ConnectionError::FdPassingFailed => { + panic!("X11 connection: File descriptor passing failed") + } + ConnectionError::ParseError(parse_error) => { + anyhow!(parse_error).context("Parse error in X11 response") + } + ConnectionError::InsufficientMemory => panic!("X11 connection: Insufficient memory"), + ConnectionError::IoError(err) => anyhow!(err).context("X11 connection: IOError"), + _ => anyhow!(err), + } +} + +impl X11WindowState { + pub fn new( + handle: AnyWindowHandle, + client: X11ClientStatePtr, + executor: ForegroundExecutor, + gpu_context: gpui_wgpu::GpuContext, + compositor_gpu: Option, + params: WindowParams, + xcb: &Rc, + client_side_decorations_supported: bool, + x_main_screen_index: usize, + x_window: xproto::Window, + atoms: &XcbAtoms, + scale_factor: f32, + appearance: WindowAppearance, + parent_window: Option, + supports_xinput_gestures: bool, + is_bgr: bool, + ) -> anyhow::Result { + // Native popups are not implemented on X11 yet. Rejecting lets callers fall back to + // gpui's in-window popovers. + if let WindowKind::AnchoredPopup(_) = params.kind { + return Err(PopupNotSupportedError.into()); + } + + let x_screen_index = params + .display_id + .map_or(x_main_screen_index, |did| u64::from(did) as usize); + + let visual_set = find_visuals(xcb, x_screen_index); + + let visual = match visual_set.transparent { + Some(visual) => visual, + None => { + log::warn!("Unable to find a transparent visual",); + visual_set.inherit + } + }; + log::info!("Using {:?}", visual); + + let colormap = if visual.colormap != 0 { + visual.colormap + } else { + let id = xcb.generate_id()?; + log::info!("Creating colormap {}", id); + check_reply( + || format!("X11 CreateColormap failed. id: {}", id), + xcb.create_colormap(xproto::ColormapAlloc::NONE, id, visual_set.root, visual.id), + )?; + id + }; + + let win_aux = xproto::CreateWindowAux::new() + // https://stackoverflow.com/questions/43218127/x11-xlib-xcb-creating-a-window-requires-border-pixel-if-specifying-colormap-wh + .border_pixel(visual_set.black_pixel) + .colormap(colormap) + .override_redirect((params.kind == WindowKind::PopUp) as u32) + .event_mask( + xproto::EventMask::EXPOSURE + | xproto::EventMask::STRUCTURE_NOTIFY + | xproto::EventMask::FOCUS_CHANGE + | xproto::EventMask::KEY_PRESS + | xproto::EventMask::KEY_RELEASE + | xproto::EventMask::PROPERTY_CHANGE + | xproto::EventMask::VISIBILITY_CHANGE, + ); + + let mut bounds = params.bounds.to_device_pixels(scale_factor); + if bounds.size.width.0 == 0 || bounds.size.height.0 == 0 { + log::warn!( + "Window bounds contain a zero value. height={}, width={}. Falling back to defaults.", + bounds.size.height.0, + bounds.size.width.0 + ); + bounds.size.width = 800.into(); + bounds.size.height = 600.into(); + } + + check_reply( + || { + format!( + "X11 CreateWindow failed. depth: {}, x_window: {}, visual_set.root: {}, bounds.origin.x.0: {}, bounds.origin.y.0: {}, bounds.size.width.0: {}, bounds.size.height.0: {}", + visual.depth, + x_window, + visual_set.root, + bounds.origin.x.0 + 2, + bounds.origin.y.0, + bounds.size.width.0, + bounds.size.height.0 + ) + }, + xcb.create_window( + visual.depth, + x_window, + visual_set.root, + (bounds.origin.x.0 + 2) as i16, + bounds.origin.y.0 as i16, + bounds.size.width.0 as u16, + bounds.size.height.0 as u16, + 0, + xproto::WindowClass::INPUT_OUTPUT, + visual.id, + &win_aux, + ), + )?; + + // Collect errors during setup, so that window can be destroyed on failure. + let setup_result = maybe!({ + let pid = std::process::id(); + check_reply( + || "X11 ChangeProperty for _NET_WM_PID failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_PID, + xproto::AtomEnum::CARDINAL, + &[pid], + ), + )?; + + let reply = get_reply(|| "X11 GetGeometry failed.", xcb.get_geometry(x_window))?; + if reply.x == 0 && reply.y == 0 { + bounds.origin.x.0 += 2; + // Work around a bug where our rendered content appears + // outside the window bounds when opened at the default position + // (14px, 49px on X + Gnome + Ubuntu 22). + let x = bounds.origin.x.0; + let y = bounds.origin.y.0; + check_reply( + || format!("X11 ConfigureWindow failed. x: {}, y: {}", x, y), + xcb.configure_window(x_window, &xproto::ConfigureWindowAux::new().x(x).y(y)), + )?; + } + if let Some(titlebar) = params.titlebar + && let Some(title) = titlebar.title + { + check_reply( + || "X11 ChangeProperty8 on WM_NAME failed.", + xcb.change_property8( + xproto::PropMode::REPLACE, + x_window, + xproto::AtomEnum::WM_NAME, + xproto::AtomEnum::STRING, + title.as_bytes(), + ), + )?; + check_reply( + || "X11 ChangeProperty8 on _NET_WM_NAME failed.", + xcb.change_property8( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_NAME, + atoms.UTF8_STRING, + title.as_bytes(), + ), + )?; + } + + if params.kind == WindowKind::PopUp { + check_reply( + || "X11 ChangeProperty32 setting window type for pop-up failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_WINDOW_TYPE, + xproto::AtomEnum::ATOM, + &[atoms._NET_WM_WINDOW_TYPE_NOTIFICATION], + ), + )?; + } + + if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog { + if let Some(parent_window) = parent_window.as_ref().map(|w| w.x_window) { + // WM_TRANSIENT_FOR hint indicating the main application window. For floating windows, we set + // a parent window (WM_TRANSIENT_FOR) such that the window manager knows where to + // place the floating window in relation to the main window. + // https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html + check_reply( + || "X11 ChangeProperty32 setting WM_TRANSIENT_FOR for floating window failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms.WM_TRANSIENT_FOR, + xproto::AtomEnum::WINDOW, + &[parent_window], + ), + )?; + } + } + + let parent = if params.kind == WindowKind::Dialog + && let Some(parent) = parent_window + { + parent.add_child(x_window); + + Some(parent) + } else { + None + }; + + if params.kind == WindowKind::Dialog { + // _NET_WM_WINDOW_TYPE_DIALOG indicates that this is a dialog (floating) window + // https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html + check_reply( + || "X11 ChangeProperty32 setting window type for dialog window failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_WINDOW_TYPE, + xproto::AtomEnum::ATOM, + &[atoms._NET_WM_WINDOW_TYPE_DIALOG], + ), + )?; + + // We set the modal state for dialog windows, so that the window manager + // can handle it appropriately (e.g., prevent interaction with the parent window + // while the dialog is open). + check_reply( + || "X11 ChangeProperty32 setting modal state for dialog window failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_STATE, + xproto::AtomEnum::ATOM, + &[atoms._NET_WM_STATE_MODAL], + ), + )?; + } + + check_reply( + || "X11 ChangeProperty32 setting protocols failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms.WM_PROTOCOLS, + xproto::AtomEnum::ATOM, + &[atoms.WM_DELETE_WINDOW, atoms._NET_WM_SYNC_REQUEST], + ), + )?; + + get_reply( + || "X11 sync protocol initialize failed.", + sync::initialize(xcb, 3, 1), + )?; + let sync_request_counter = xcb.generate_id()?; + check_reply( + || "X11 sync CreateCounter failed.", + sync::create_counter(xcb, sync_request_counter, sync::Int64 { lo: 0, hi: 0 }), + )?; + + check_reply( + || "X11 ChangeProperty32 setting sync request counter failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_SYNC_REQUEST_COUNTER, + xproto::AtomEnum::CARDINAL, + &[sync_request_counter], + ), + )?; + + let mut xi_event_mask = xinput::XIEventMask::MOTION + | xinput::XIEventMask::BUTTON_PRESS + | xinput::XIEventMask::BUTTON_RELEASE + | xinput::XIEventMask::ENTER + | xinput::XIEventMask::LEAVE; + if supports_xinput_gestures { + // x11rb 0.13 doesn't define XIEventMask constants for gesture + // events, so we construct them from the event opcodes (each + // XInput event type N maps to mask bit N). + xi_event_mask |= + xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_BEGIN_EVENT) + | xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_UPDATE_EVENT) + | xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_END_EVENT); + } + check_reply( + || "X11 XiSelectEvents failed.", + xcb.xinput_xi_select_events( + x_window, + &[xinput::EventMask { + deviceid: XINPUT_ALL_DEVICE_GROUPS, + mask: vec![xi_event_mask], + }], + ), + )?; + + check_reply( + || "X11 XiSelectEvents for device changes failed.", + xcb.xinput_xi_select_events( + x_window, + &[xinput::EventMask { + deviceid: XINPUT_ALL_DEVICES, + mask: vec![ + xinput::XIEventMask::HIERARCHY | xinput::XIEventMask::DEVICE_CHANGED, + ], + }], + ), + )?; + + xcb_flush(xcb); + + let mut renderer = { + let raw_window = RawWindow { + connection: as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection( + xcb, + ) as *mut _, + screen_id: x_screen_index, + window_id: x_window, + visual_id: visual.id, + }; + let config = WgpuSurfaceConfig { + // Note: this has to be done after the GPU init, or otherwise + // the sizes are immediately invalidated. + size: query_render_extent(xcb, x_window)?, + // We set it to transparent by default, even if we have client-side + // decorations, since those seem to work on X11 even without `true` here. + // If the window appearance changes, then the renderer will get updated + // too + transparent: false, + preferred_present_mode: None, + }; + WgpuRenderer::new(gpu_context, &raw_window, config, compositor_gpu)? + }; + + renderer.set_subpixel_layout(is_bgr); + + // Set max window size hints based on the GPU's maximum texture dimension. + // This prevents the window from being resized larger than what the GPU can render. + let max_texture_size = renderer.max_texture_size(); + let mut size_hints = WmSizeHints::new(); + if let Some(size) = params.window_min_size { + size_hints.min_size = + Some((f32::from(size.width) as i32, f32::from(size.height) as i32)); + } + size_hints.max_size = Some((max_texture_size as i32, max_texture_size as i32)); + check_reply( + || { + format!( + "X11 change of WM_SIZE_HINTS failed. max_size: {:?}", + max_texture_size + ) + }, + size_hints.set_normal_hints(xcb, x_window), + )?; + + if let Some(image) = params.icon { + // https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html#id-1.6.13 + let property_size = 2 + (image.width() * image.height()) as usize; + let mut property_data: Vec = Vec::with_capacity(property_size); + property_data.push(image.width()); + property_data.push(image.height()); + property_data.extend(image.pixels().map(|px| { + let [r, g, b, a]: [u8; 4] = px.0; + u32::from_le_bytes([b, g, r, a]) + })); + + check_reply( + || "X11 ChangeProperty32 for _NET_ICON_NAME failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_ICON, + xproto::AtomEnum::CARDINAL, + &property_data, + ), + )?; + } + + let display = Rc::new(X11Display::new(xcb, scale_factor, x_screen_index)?); + + Ok(Self { + parent, + children: FxHashSet::default(), + client, + executor, + display, + x_root_window: visual_set.root, + x_screen_index, + visual_id: visual.id, + bounds: bounds.to_pixels(scale_factor), + scale_factor, + renderer, + atoms: *atoms, + input_handler: None, + active: false, + // The window is not mapped until the client sees `MapNotify`. + visibility: WindowVisibility::Hidden, + hovered: false, + force_render_after_recovery: false, + fullscreen: false, + maximized_vertical: false, + maximized_horizontal: false, + hidden: false, + appearance, + handle, + background_appearance: WindowBackgroundAppearance::Opaque, + destroyed: false, + client_side_decorations_supported, + decorations: WindowDecorations::Server, + last_insets: [0, 0, 0, 0], + edge_constraints: None, + accesskit_adapter: None, + counter_id: sync_request_counter, + last_sync_counter: None, + }) + }); + + if setup_result.is_err() { + check_reply( + || "X11 DestroyWindow failed while cleaning it up after setup failure.", + xcb.destroy_window(x_window), + )?; + xcb_flush(xcb); + } + + setup_result + } + + fn content_size(&self) -> Size { + self.bounds.size + } +} + +pub(crate) struct X11Window(pub X11WindowStatePtr); + +impl Drop for X11Window { + fn drop(&mut self) { + let mut state = self.0.state.borrow_mut(); + + if let Some(parent) = state.parent.as_ref() { + parent.state.borrow_mut().children.remove(&self.0.x_window); + } + + state.renderer.destroy(); + + let destroy_x_window = maybe!({ + check_reply( + || "X11 DestroyWindow failure.", + self.0.xcb.destroy_window(self.0.x_window), + )?; + xcb_flush(&self.0.xcb); + + anyhow::Ok(()) + }) + .log_err(); + + if destroy_x_window.is_some() { + state.destroyed = true; + + let this_ptr = self.0.clone(); + let client_ptr = state.client.clone(); + state + .executor + .spawn(async move { + this_ptr.close(); + client_ptr.drop_window(this_ptr.x_window); + }) + .detach(); + } + + drop(state); + } +} + +enum WmHintPropertyState { + // Remove = 0, + // Add = 1, + Toggle = 2, +} + +impl X11Window { + pub fn new( + handle: AnyWindowHandle, + client: X11ClientStatePtr, + executor: ForegroundExecutor, + gpu_context: gpui_wgpu::GpuContext, + compositor_gpu: Option, + params: WindowParams, + xcb: &Rc, + client_side_decorations_supported: bool, + x_main_screen_index: usize, + x_window: xproto::Window, + atoms: &XcbAtoms, + scale_factor: f32, + appearance: WindowAppearance, + parent_window: Option, + supports_xinput_gestures: bool, + is_bgr: bool, + ) -> anyhow::Result { + let ptr = X11WindowStatePtr { + state: Rc::new(RefCell::new(X11WindowState::new( + handle, + client, + executor, + gpu_context, + compositor_gpu, + params, + xcb, + client_side_decorations_supported, + x_main_screen_index, + x_window, + atoms, + scale_factor, + appearance, + parent_window, + supports_xinput_gestures, + is_bgr, + )?)), + callbacks: Rc::new(RefCell::new(Callbacks::default())), + xcb: xcb.clone(), + x_window, + }; + + let state = ptr.state.borrow_mut(); + ptr.set_wm_properties(state)?; + + Ok(Self(ptr)) + } + + fn set_wm_hints C>( + &self, + failure_context: F, + wm_hint_property_state: WmHintPropertyState, + prop1: u32, + prop2: u32, + ) -> anyhow::Result<()> { + let state = self.0.state.borrow(); + let message = ClientMessageEvent::new( + 32, + self.0.x_window, + state.atoms._NET_WM_STATE, + [wm_hint_property_state as u32, prop1, prop2, 1, 0], + ); + check_reply( + failure_context, + self.0.xcb.send_event( + false, + state.x_root_window, + xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + message, + ), + )?; + xcb_flush(&self.0.xcb); + Ok(()) + } + + fn get_root_position( + &self, + position: Point, + ) -> anyhow::Result { + let state = self.0.state.borrow(); + get_reply( + || "X11 TranslateCoordinates failed.", + self.0.xcb.translate_coordinates( + self.0.x_window, + state.x_root_window, + (f32::from(position.x) * state.scale_factor) as i16, + (f32::from(position.y) * state.scale_factor) as i16, + ), + ) + } + + fn send_moveresize(&self, flag: u32) -> anyhow::Result<()> { + let state = self.0.state.borrow(); + + check_reply( + || "X11 UngrabPointer before move/resize of window failed.", + self.0.xcb.ungrab_pointer(x11rb::CURRENT_TIME), + )?; + + let pointer = get_reply( + || "X11 QueryPointer before move/resize of window failed.", + self.0.xcb.query_pointer(self.0.x_window), + )?; + let message = ClientMessageEvent::new( + 32, + self.0.x_window, + state.atoms._NET_WM_MOVERESIZE, + [ + pointer.root_x as u32, + pointer.root_y as u32, + flag, + 0, // Left mouse button + 0, + ], + ); + check_reply( + || "X11 SendEvent to move/resize window failed.", + self.0.xcb.send_event( + false, + state.x_root_window, + xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + message, + ), + )?; + + xcb_flush(&self.0.xcb); + Ok(()) + } +} + +impl X11WindowStatePtr { + pub fn should_close(&self) -> bool { + let mut cb = self.callbacks.borrow_mut(); + if let Some(mut should_close) = cb.should_close.take() { + let result = (should_close)(); + cb.should_close = Some(should_close); + result + } else { + true + } + } + + pub fn property_notify(&self, event: xproto::PropertyNotifyEvent) -> anyhow::Result<()> { + let state = self.state.borrow_mut(); + if event.atom == state.atoms._NET_WM_STATE { + self.set_wm_properties(state)?; + } else if event.atom == state.atoms._GTK_EDGE_CONSTRAINTS { + self.set_edge_constraints(state)?; + } + Ok(()) + } + + fn set_edge_constraints( + &self, + mut state: std::cell::RefMut, + ) -> anyhow::Result<()> { + let reply = get_reply( + || "X11 GetProperty for _GTK_EDGE_CONSTRAINTS failed.", + self.xcb.get_property( + false, + self.x_window, + state.atoms._GTK_EDGE_CONSTRAINTS, + xproto::AtomEnum::CARDINAL, + 0, + 4, + ), + )?; + + if reply.value_len != 0 { + if let Ok(bytes) = reply.value[0..4].try_into() { + let atom = u32::from_ne_bytes(bytes); + let edge_constraints = EdgeConstraints::from_atom(atom); + state.edge_constraints.replace(edge_constraints); + } else { + log::error!("Failed to parse GTK_EDGE_CONSTRAINTS"); + } + } + + Ok(()) + } + + fn set_wm_properties( + &self, + mut state: std::cell::RefMut, + ) -> anyhow::Result<()> { + let reply = get_reply( + || "X11 GetProperty for _NET_WM_STATE failed.", + self.xcb.get_property( + false, + self.x_window, + state.atoms._NET_WM_STATE, + xproto::AtomEnum::ATOM, + 0, + u32::MAX, + ), + )?; + + let atoms = reply + .value + .chunks_exact(4) + .map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])); + + let was_active = state.active; + state.active = false; + state.fullscreen = false; + state.maximized_vertical = false; + state.maximized_horizontal = false; + state.hidden = false; + + for atom in atoms { + if atom == state.atoms._NET_WM_STATE_FOCUSED { + state.active = true; + } else if atom == state.atoms._NET_WM_STATE_FULLSCREEN { + state.fullscreen = true; + } else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_VERT { + state.maximized_vertical = true; + } else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_HORZ { + state.maximized_horizontal = true; + } else if atom == state.atoms._NET_WM_STATE_HIDDEN { + state.hidden = true; + } + } + + // The urgency hint has no withdrawal signal of its own; ICCCM leaves that to + // the client, and focus is the conventional means for the user to zero it. + if state.active && !was_active { + set_wm_hints_urgency(&self.xcb, self.x_window, false); + } + + Ok(()) + } + + pub fn add_child(&self, child: xproto::Window) { + let mut state = self.state.borrow_mut(); + state.children.insert(child); + } + + pub fn is_blocked(&self) -> bool { + let state = self.state.borrow(); + !state.children.is_empty() + } + + pub fn close(&self) { + let state = self.state.borrow(); + let client = state.client.clone(); + #[allow(clippy::mutable_key_type)] + let children = state.children.clone(); + drop(state); + + if let Some(client) = client.get_client() { + for child in children { + if let Some(child_window) = client.get_window(child) { + child_window.close(); + } + } + } + + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(fun) = callbacks.close.take() { + fun() + } + } + + pub fn refresh(&self, request_frame_options: RequestFrameOptions) { + let callback = self.callbacks.borrow_mut().request_frame.take(); + if let Some(mut fun) = callback { + fun(request_frame_options); + self.callbacks.borrow_mut().request_frame = Some(fun); + } + } + + pub fn handle_input(&self, input: PlatformInput) { + if self.is_blocked() { + return; + } + let callback = self.callbacks.borrow_mut().input.take(); + if let Some(mut fun) = callback { + let result = fun(input.clone()); + self.callbacks.borrow_mut().input = Some(fun); + if !result.propagate { + return; + } + } + if let PlatformInput::KeyDown(event) = input { + // only allow shift modifier when inserting text + if event.keystroke.modifiers.is_subset_of(&Modifiers::shift()) { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + if let Some(key_char) = &event.keystroke.key_char { + drop(state); + input_handler.replace_text_in_range(None, key_char); + state = self.state.borrow_mut(); + } + state.input_handler = Some(input_handler); + } + } + } + } + + pub fn handle_ime_commit(&self, text: String) { + if self.is_blocked() { + return; + } + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.replace_text_in_range(None, &text); + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + } + } + + pub fn handle_ime_preedit(&self, text: String) { + if self.is_blocked() { + return; + } + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.replace_and_mark_text_in_range(None, &text, None); + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + } + } + + pub fn handle_ime_unmark(&self) { + if self.is_blocked() { + return; + } + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.unmark_text(); + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + } + } + + pub fn handle_ime_delete(&self) { + if self.is_blocked() { + return; + } + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + if let Some(marked) = input_handler.marked_text_range() { + input_handler.replace_text_in_range(Some(marked), ""); + } + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + } + } + + pub fn get_ime_area(&self) -> Option> { + let mut state = self.state.borrow_mut(); + let scale_factor = state.scale_factor; + let mut bounds: Option> = None; + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + if let Some(selection) = input_handler.selected_text_range(true) { + bounds = input_handler.bounds_for_range(selection.range); + } + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + }; + bounds.map(|b| b.scale(scale_factor)) + } + + pub fn set_bounds(&self, bounds: Bounds) -> anyhow::Result<()> { + let (is_resize, content_size, scale_factor) = { + let mut state = self.state.borrow_mut(); + let bounds = bounds.map(|f| px(f as f32 / state.scale_factor)); + + let is_resize = bounds.size.width != state.bounds.size.width + || bounds.size.height != state.bounds.size.height; + + // If it's a resize event (only width/height changed), we ignore `bounds.origin` + // because it contains wrong values. + if is_resize { + state.bounds.size = bounds.size; + } else { + state.bounds = bounds; + } + + let gpu_size = query_render_extent(&self.xcb, self.x_window)?; + state.renderer.update_drawable_size(gpu_size); + let result = (is_resize, state.content_size(), state.scale_factor); + if let Some(value) = state.last_sync_counter.take() { + check_reply( + || "X11 sync SetCounter failed.", + sync::set_counter(&self.xcb, state.counter_id, value), + )?; + } + result + }; + + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(ref mut fun) = callbacks.resize { + fun(content_size, scale_factor) + } + + if !is_resize && let Some(ref mut fun) = callbacks.moved { + fun(); + } + + Ok(()) + } + + pub fn set_active(&self, focus: bool) { + let callback = self.callbacks.borrow_mut().active_status_change.take(); + if let Some(mut fun) = callback { + fun(focus); + self.callbacks.borrow_mut().active_status_change = Some(fun); + } + if let Some(adapter) = self.state.borrow_mut().accesskit_adapter.as_mut() { + adapter.update_window_focus_state(focus); + } + } + + pub fn set_hovered(&self, focus: bool) { + let callback = self.callbacks.borrow_mut().hovered_status_change.take(); + if let Some(mut fun) = callback { + fun(focus); + self.callbacks.borrow_mut().hovered_status_change = Some(fun); + } + } + + pub fn set_visibility(&self, visibility: WindowVisibility) { + if std::mem::replace(&mut self.state.borrow_mut().visibility, visibility) == visibility { + return; + } + let callback = self.callbacks.borrow_mut().visibility_change.take(); + if let Some(mut fun) = callback { + fun(visibility); + self.callbacks.borrow_mut().visibility_change = Some(fun); + } + } + + pub fn set_appearance(&mut self, appearance: WindowAppearance) { + let mut state = self.state.borrow_mut(); + state.appearance = appearance; + let is_transparent = state.is_transparent(); + state.renderer.update_transparency(is_transparent); + state.appearance = appearance; + drop(state); + let callback = self.callbacks.borrow_mut().appearance_changed.take(); + if let Some(mut fun) = callback { + fun(); + self.callbacks.borrow_mut().appearance_changed = Some(fun); + } + } + + pub fn set_button_layout(&self) { + let callback = self.callbacks.borrow_mut().button_layout_changed.take(); + if let Some(mut fun) = callback { + fun(); + self.callbacks.borrow_mut().button_layout_changed = Some(fun); + } + } +} + +impl PlatformWindow for X11Window { + fn bounds(&self) -> Bounds { + self.0.state.borrow().bounds + } + + fn is_maximized(&self) -> bool { + let state = self.0.state.borrow(); + + // A maximized window that gets minimized will still retain its maximized state. + !state.hidden && state.maximized_vertical && state.maximized_horizontal + } + + fn window_bounds(&self) -> WindowBounds { + let state = self.0.state.borrow(); + if self.is_maximized() { + WindowBounds::Maximized(state.bounds) + } else { + WindowBounds::Windowed(state.bounds) + } + } + + fn inner_window_bounds(&self) -> WindowBounds { + let state = self.0.state.borrow(); + if self.is_maximized() { + WindowBounds::Maximized(state.bounds) + } else { + let mut bounds = state.bounds; + let [left, right, top, bottom] = state.last_insets; + + let [left, right, top, bottom] = [ + px((left as f32) / state.scale_factor), + px((right as f32) / state.scale_factor), + px((top as f32) / state.scale_factor), + px((bottom as f32) / state.scale_factor), + ]; + + bounds.origin.x += left; + bounds.origin.y += top; + bounds.size.width -= left + right; + bounds.size.height -= top + bottom; + + WindowBounds::Windowed(bounds) + } + } + + fn content_size(&self) -> Size { + // After the wgpu migration, X11WindowState::content_size() returns logical pixels + // (bounds.size is already divided by scale_factor in set_bounds), so no further + // division is needed here. This matches the Wayland implementation. + self.0.state.borrow().content_size() + } + + fn resize(&mut self, size: Size) { + let state = self.0.state.borrow(); + let size = size.to_device_pixels(state.scale_factor); + let width = size.width.0 as u32; + let height = size.height.0 as u32; + + check_reply( + || { + format!( + "X11 ConfigureWindow failed. width: {}, height: {}", + width, height + ) + }, + self.0.xcb.configure_window( + self.0.x_window, + &xproto::ConfigureWindowAux::new() + .width(width) + .height(height), + ), + ) + .log_err(); + xcb_flush(&self.0.xcb); + } + + fn scale_factor(&self) -> f32 { + self.0.state.borrow().scale_factor + } + + fn appearance(&self) -> WindowAppearance { + self.0.state.borrow().appearance + } + + fn display(&self) -> Option> { + Some(self.0.state.borrow().display.clone()) + } + + fn mouse_position(&self) -> Point { + get_reply( + || "X11 QueryPointer failed.", + self.0.xcb.query_pointer(self.0.x_window), + ) + .log_err() + .map_or(Point::new(Pixels::ZERO, Pixels::ZERO), |reply| { + let scale_factor = self.0.state.borrow().scale_factor; + Point::new( + px(reply.win_x as f32 / scale_factor), + px(reply.win_y as f32 / scale_factor), + ) + }) + } + + fn modifiers(&self) -> Modifiers { + self.0 + .state + .borrow() + .client + .0 + .upgrade() + .map(|ref_cell| ref_cell.borrow().modifiers) + .unwrap_or_default() + } + + fn capslock(&self) -> gpui::Capslock { + self.0 + .state + .borrow() + .client + .0 + .upgrade() + .map(|ref_cell| ref_cell.borrow().capslock) + .unwrap_or_default() + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.0.state.borrow_mut().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.0.state.borrow_mut().input_handler.take() + } + + fn prompt( + &self, + _level: PromptLevel, + _msg: &str, + _detail: Option<&str>, + _answers: &[PromptButton], + ) -> Option> { + None + } + + fn activate(&self) { + let data = [1, xproto::Time::CURRENT_TIME.into(), 0, 0, 0]; + let message = xproto::ClientMessageEvent::new( + 32, + self.0.x_window, + self.0.state.borrow().atoms._NET_ACTIVE_WINDOW, + data, + ); + self.0 + .xcb + .send_event( + false, + self.0.state.borrow().x_root_window, + xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + message, + ) + .log_err(); + xcb_flush(&self.0.xcb); + } + + fn request_attention(&self) { + if self.is_active() { + return; + } + + set_wm_hints_urgency(&self.0.xcb, self.0.x_window, true); + } + + fn is_active(&self) -> bool { + self.0.state.borrow().active + } + + fn visibility(&self) -> WindowVisibility { + self.0.state.borrow().visibility + } + + fn is_hovered(&self) -> bool { + self.0.state.borrow().hovered + } + + fn set_title(&mut self, title: &str) { + check_reply( + || "X11 ChangeProperty8 on WM_NAME failed.", + self.0.xcb.change_property8( + xproto::PropMode::REPLACE, + self.0.x_window, + xproto::AtomEnum::WM_NAME, + xproto::AtomEnum::STRING, + title.as_bytes(), + ), + ) + .log_err(); + + check_reply( + || "X11 ChangeProperty8 on _NET_WM_NAME failed.", + self.0.xcb.change_property8( + xproto::PropMode::REPLACE, + self.0.x_window, + self.0.state.borrow().atoms._NET_WM_NAME, + self.0.state.borrow().atoms.UTF8_STRING, + title.as_bytes(), + ), + ) + .log_err(); + xcb_flush(&self.0.xcb); + } + + fn set_app_id(&mut self, app_id: &str) { + let mut data = Vec::with_capacity(app_id.len() * 2 + 2); + data.extend(app_id.bytes()); // instance https://unix.stackexchange.com/a/494170 + data.push(b'\0'); + data.extend(app_id.bytes()); // class + data.push(b'\0'); + + check_reply( + || "X11 ChangeProperty8 for WM_CLASS failed.", + self.0.xcb.change_property8( + xproto::PropMode::REPLACE, + self.0.x_window, + xproto::AtomEnum::WM_CLASS, + xproto::AtomEnum::STRING, + &data, + ), + ) + .log_err(); + } + + fn map_window(&mut self) -> anyhow::Result<()> { + check_reply( + || "X11 MapWindow failed.", + self.0.xcb.map_window(self.0.x_window), + )?; + Ok(()) + } + + fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { + let mut state = self.0.state.borrow_mut(); + state.background_appearance = background_appearance; + let transparent = state.is_transparent(); + state.renderer.update_transparency(transparent); + } + + fn background_appearance(&self) -> WindowBackgroundAppearance { + self.0.state.borrow().background_appearance + } + + fn is_subpixel_rendering_supported(&self) -> bool { + self.0 + .state + .borrow() + .client + .0 + .upgrade() + .map(|ref_cell| { + let state = ref_cell.borrow(); + state + .gpu_context + .borrow() + .as_ref() + .is_some_and(|ctx| ctx.supports_dual_source_blending()) + }) + .unwrap_or_default() + } + + fn minimize(&self) { + let state = self.0.state.borrow(); + const WINDOW_ICONIC_STATE: u32 = 3; + let message = ClientMessageEvent::new( + 32, + self.0.x_window, + state.atoms.WM_CHANGE_STATE, + [WINDOW_ICONIC_STATE, 0, 0, 0, 0], + ); + check_reply( + || "X11 SendEvent to minimize window failed.", + self.0.xcb.send_event( + false, + state.x_root_window, + xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + message, + ), + ) + .log_err(); + } + + fn zoom(&self) { + let state = self.0.state.borrow(); + self.set_wm_hints( + || "X11 SendEvent to maximize a window failed.", + WmHintPropertyState::Toggle, + state.atoms._NET_WM_STATE_MAXIMIZED_VERT, + state.atoms._NET_WM_STATE_MAXIMIZED_HORZ, + ) + .log_err(); + } + + fn toggle_fullscreen(&self) { + let state = self.0.state.borrow(); + self.set_wm_hints( + || "X11 SendEvent to fullscreen a window failed.", + WmHintPropertyState::Toggle, + state.atoms._NET_WM_STATE_FULLSCREEN, + xproto::AtomEnum::NONE.into(), + ) + .log_err(); + } + + fn is_fullscreen(&self) -> bool { + self.0.state.borrow().fullscreen + } + + fn on_request_frame(&self, callback: Box) { + self.0.callbacks.borrow_mut().request_frame = Some(callback); + } + + fn on_input(&self, callback: Box gpui::DispatchEventResult>) { + self.0.callbacks.borrow_mut().input = Some(callback); + } + + fn on_active_status_change(&self, callback: Box) { + self.0.callbacks.borrow_mut().active_status_change = Some(callback); + } + + fn on_visibility_change(&self, callback: Box) { + self.0.callbacks.borrow_mut().visibility_change = Some(callback); + } + + fn on_hover_status_change(&self, callback: Box) { + self.0.callbacks.borrow_mut().hovered_status_change = Some(callback); + } + + fn on_resize(&self, callback: Box, f32)>) { + self.0.callbacks.borrow_mut().resize = Some(callback); + } + + fn on_moved(&self, callback: Box) { + self.0.callbacks.borrow_mut().moved = Some(callback); + } + + fn on_should_close(&self, callback: Box bool>) { + self.0.callbacks.borrow_mut().should_close = Some(callback); + } + + fn on_close(&self, callback: Box) { + self.0.callbacks.borrow_mut().close = Some(callback); + } + + fn on_hit_test_window_control(&self, _callback: Box Option>) { + } + + fn on_appearance_changed(&self, callback: Box) { + self.0.callbacks.borrow_mut().appearance_changed = Some(callback); + } + + fn on_button_layout_changed(&self, callback: Box) { + self.0.callbacks.borrow_mut().button_layout_changed = Some(callback); + } + + fn draw(&self, scene: &Scene) { + let mut inner = self.0.state.borrow_mut(); + + if inner.renderer.device_lost() { + let raw_window = RawWindow { + connection: as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection( + &*self.0.xcb, + ) as *mut _, + screen_id: inner.x_screen_index, + window_id: self.0.x_window, + visual_id: inner.visual_id, + }; + match inner.renderer.recover(&raw_window) { + Ok(()) => {} + Err(err) => { + log::warn!("GPU recovery failed, will retry on next frame: {err}"); + } + } + + inner.force_render_after_recovery = true; + return; + } + + inner.renderer.draw(scene); + + if inner.renderer.needs_redraw() { + inner.force_render_after_recovery = true; + } + } + + fn sprite_atlas(&self) -> Arc { + let inner = self.0.state.borrow(); + inner.renderer.sprite_atlas().clone() + } + + fn show_window_menu(&self, position: Point) { + let state = self.0.state.borrow(); + + check_reply( + || "X11 UngrabPointer failed.", + self.0.xcb.ungrab_pointer(x11rb::CURRENT_TIME), + ) + .log_err(); + + let Some(coords) = self.get_root_position(position).log_err() else { + return; + }; + let message = ClientMessageEvent::new( + 32, + self.0.x_window, + state.atoms._GTK_SHOW_WINDOW_MENU, + [ + XINPUT_ALL_DEVICE_GROUPS as u32, + coords.dst_x as u32, + coords.dst_y as u32, + 0, + 0, + ], + ); + check_reply( + || "X11 SendEvent to show window menu failed.", + self.0.xcb.send_event( + false, + state.x_root_window, + xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + message, + ), + ) + .log_err(); + } + + fn start_window_move(&self) { + const MOVERESIZE_MOVE: u32 = 8; + self.send_moveresize(MOVERESIZE_MOVE).log_err(); + } + + fn start_window_resize(&self, edge: ResizeEdge) { + self.send_moveresize(resize_edge_to_moveresize(edge)) + .log_err(); + } + + fn window_decorations(&self) -> gpui::Decorations { + let state = self.0.state.borrow(); + + // Client window decorations require compositor support + if !state.client_side_decorations_supported { + return Decorations::Server; + } + + match state.decorations { + WindowDecorations::Server => Decorations::Server, + WindowDecorations::Client => { + let tiling = if state.fullscreen { + Tiling::tiled() + } else if let Some(edge_constraints) = &state.edge_constraints { + edge_constraints.to_tiling() + } else { + // https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=2519;drc=1f14cc876cc5bf899d13284a12c451498219bb2d + Tiling { + top: state.maximized_vertical, + bottom: state.maximized_vertical, + left: state.maximized_horizontal, + right: state.maximized_horizontal, + } + }; + Decorations::Client { tiling } + } + } + } + + fn set_client_inset(&self, inset: Pixels) { + let mut state = self.0.state.borrow_mut(); + + let dp = (f32::from(inset) * state.scale_factor) as u32; + + let insets = if state.fullscreen { + [0, 0, 0, 0] + } else if let Some(edge_constraints) = &state.edge_constraints { + let left = if edge_constraints.left_tiled { 0 } else { dp }; + let top = if edge_constraints.top_tiled { 0 } else { dp }; + let right = if edge_constraints.right_tiled { 0 } else { dp }; + let bottom = if edge_constraints.bottom_tiled { 0 } else { dp }; + + [left, right, top, bottom] + } else { + let (left, right) = if state.maximized_horizontal { + (0, 0) + } else { + (dp, dp) + }; + let (top, bottom) = if state.maximized_vertical { + (0, 0) + } else { + (dp, dp) + }; + [left, right, top, bottom] + }; + + if state.last_insets != insets { + state.last_insets = insets; + + check_reply( + || "X11 ChangeProperty for _GTK_FRAME_EXTENTS failed.", + self.0.xcb.change_property( + xproto::PropMode::REPLACE, + self.0.x_window, + state.atoms._GTK_FRAME_EXTENTS, + xproto::AtomEnum::CARDINAL, + size_of::() as u8 * 8, + 4, + bytemuck::cast_slice::(&insets), + ), + ) + .log_err(); + } + } + + fn request_decorations(&self, mut decorations: gpui::WindowDecorations) { + let mut state = self.0.state.borrow_mut(); + + if matches!(decorations, gpui::WindowDecorations::Client) + && !state.client_side_decorations_supported + { + log::info!( + "x11: no compositor present, falling back to server-side window decorations" + ); + decorations = gpui::WindowDecorations::Server; + } + + // https://github.com/rust-windowing/winit/blob/master/src/platform_impl/linux/x11/util/hint.rs#L53-L87 + let hints_data: [u32; 5] = match decorations { + WindowDecorations::Server => [1 << 1, 0, 1, 0, 0], + WindowDecorations::Client => [1 << 1, 0, 0, 0, 0], + }; + + let success = check_reply( + || "X11 ChangeProperty for _MOTIF_WM_HINTS failed.", + self.0.xcb.change_property( + xproto::PropMode::REPLACE, + self.0.x_window, + state.atoms._MOTIF_WM_HINTS, + state.atoms._MOTIF_WM_HINTS, + size_of::() as u8 * 8, + 5, + bytemuck::cast_slice::(&hints_data), + ), + ) + .log_err(); + + let Some(()) = success else { + return; + }; + + match decorations { + WindowDecorations::Server => { + state.decorations = WindowDecorations::Server; + let is_transparent = state.is_transparent(); + state.renderer.update_transparency(is_transparent); + } + WindowDecorations::Client => { + state.decorations = WindowDecorations::Client; + let is_transparent = state.is_transparent(); + state.renderer.update_transparency(is_transparent); + } + } + + drop(state); + let mut callbacks = self.0.callbacks.borrow_mut(); + if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() { + appearance_changed(); + } + } + + fn update_ime_position(&self, bounds: Bounds) { + let state = self.0.state.borrow(); + let client = state.client.clone(); + drop(state); + client.update_ime_position(bounds); + } + + fn gpu_specs(&self) -> Option { + self.0.state.borrow().renderer.gpu_specs().into() + } + + fn play_system_bell(&self) { + // Volume 0% means don't increase or decrease from system volume + let _ = self.0.xcb.bell(0); + } + + fn a11y_init(&self, callbacks: gpui::A11yCallbacks) { + let activation_handler = TrivialActivationHandler { + callback: callbacks.activation, + }; + let action_handler = TrivialActionHandler(callbacks.action); + let deactivation_handler = TrivialDeactivationHandler { + callback: callbacks.deactivation, + }; + + let adapter = + accesskit_unix::Adapter::new(activation_handler, action_handler, deactivation_handler); + + self.0.state.borrow_mut().accesskit_adapter = Some(adapter); + } + + fn a11y_tree_update(&self, tree_update: accesskit::TreeUpdate) { + let mut state = self.0.state.borrow_mut(); + if let Some(adapter) = state.accesskit_adapter.as_mut() { + adapter.update_if_active(|| tree_update); + } + } + + fn a11y_update_window_bounds(&self) { + let mut state = self.0.state.borrow_mut(); + let scale = state.scale_factor; + let bounds = state.bounds; + let [left, right, top, bottom] = state.last_insets; + + let x = f32::from(bounds.origin.x); + let y = f32::from(bounds.origin.y); + let width = f32::from(bounds.size.width); + let height = f32::from(bounds.size.height); + + let outer = accesskit::Rect { + x0: (x * scale) as f64, + y0: (y * scale) as f64, + x1: ((x + width) * scale) as f64, + y1: ((y + height) * scale) as f64, + }; + + let inner = accesskit::Rect { + x0: (x * scale) as f64 + left as f64, + y0: (y * scale) as f64 + top as f64, + x1: ((x + width) * scale) as f64 - right as f64, + y1: ((y + height) * scale) as f64 - bottom as f64, + }; + + if let Some(adapter) = state.accesskit_adapter.as_mut() { + adapter.set_root_window_bounds(outer, inner); + } + } +} + +struct TrivialActivationHandler { + callback: Box Option + Send + 'static>, +} + +impl accesskit::ActivationHandler for TrivialActivationHandler { + fn request_initial_tree(&mut self) -> Option { + (self.callback)() + } +} + +struct TrivialActionHandler(Box); + +impl accesskit::ActionHandler for TrivialActionHandler { + fn do_action(&mut self, request: accesskit::ActionRequest) { + (self.0)(request); + } +} + +struct TrivialDeactivationHandler { + callback: Box, +} + +impl accesskit::DeactivationHandler for TrivialDeactivationHandler { + fn deactivate_accessibility(&mut self) { + (self.callback)(); + } +} diff --git a/vendor/gpui-pre-linux/src/linux/x11/xim_handler.rs b/vendor/gpui-pre-linux/src/linux/x11/xim_handler.rs new file mode 100644 index 000000000..82b7c9631 --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/x11/xim_handler.rs @@ -0,0 +1,133 @@ +use std::default::Default; + +use x11rb::protocol::{Event, xproto}; +use xim::{AHashMap, AttributeName, Client, ClientError, ClientHandler, InputStyle}; + +pub enum XimCallbackEvent { + XimXEvent(x11rb::protocol::Event), + XimPreeditEvent(xproto::Window, String), + XimCommitEvent(xproto::Window, String), +} + +pub struct XimHandler { + pub im_id: u16, + pub ic_id: u16, + pub connected: bool, + pub window: xproto::Window, + pub last_callback_event: Option, +} + +impl XimHandler { + pub fn new() -> Self { + Self { + im_id: Default::default(), + ic_id: Default::default(), + connected: false, + window: Default::default(), + last_callback_event: None, + } + } +} + +impl> ClientHandler for XimHandler { + fn handle_connect(&mut self, client: &mut C) -> Result<(), ClientError> { + client.open("C") + } + + fn handle_open(&mut self, client: &mut C, input_method_id: u16) -> Result<(), ClientError> { + self.im_id = input_method_id; + + client.get_im_values(input_method_id, &[AttributeName::QueryInputStyle]) + } + + fn handle_get_im_values( + &mut self, + client: &mut C, + input_method_id: u16, + _attributes: AHashMap>, + ) -> Result<(), ClientError> { + let ic_attributes = client + .build_ic_attributes() + .push(AttributeName::InputStyle, InputStyle::PREEDIT_CALLBACKS) + .push(AttributeName::ClientWindow, self.window) + .push(AttributeName::FocusWindow, self.window) + .build(); + client.create_ic(input_method_id, ic_attributes) + } + + fn handle_create_ic( + &mut self, + _client: &mut C, + _input_method_id: u16, + input_context_id: u16, + ) -> Result<(), ClientError> { + self.connected = true; + self.ic_id = input_context_id; + Ok(()) + } + + fn handle_commit( + &mut self, + _client: &mut C, + _input_method_id: u16, + _input_context_id: u16, + text: &str, + ) -> Result<(), ClientError> { + self.last_callback_event = Some(XimCallbackEvent::XimCommitEvent( + self.window, + String::from(text), + )); + Ok(()) + } + + fn handle_forward_event( + &mut self, + _client: &mut C, + _input_method_id: u16, + _input_context_id: u16, + _flag: xim::ForwardEventFlag, + xev: C::XEvent, + ) -> Result<(), ClientError> { + match xev.response_type { + x11rb::protocol::xproto::KEY_PRESS_EVENT => { + self.last_callback_event = Some(XimCallbackEvent::XimXEvent(Event::KeyPress(xev))); + } + x11rb::protocol::xproto::KEY_RELEASE_EVENT => { + self.last_callback_event = + Some(XimCallbackEvent::XimXEvent(Event::KeyRelease(xev))); + } + _ => {} + } + Ok(()) + } + + fn handle_close(&mut self, client: &mut C, _input_method_id: u16) -> Result<(), ClientError> { + client.disconnect() + } + + fn handle_preedit_draw( + &mut self, + _client: &mut C, + _input_method_id: u16, + _input_context_id: u16, + _caret: i32, + _chg_first: i32, + _chg_len: i32, + _status: xim::PreeditDrawStatus, + preedit_string: &str, + _feedbacks: Vec, + ) -> Result<(), ClientError> { + // XIMReverse: 1, XIMPrimary: 8, XIMTertiary: 32: selected text + // XIMUnderline: 2, XIMSecondary: 16: underlined text + // XIMHighlight: 4: normal text + // XIMVisibleToForward: 64, XIMVisibleToBackward: 128, XIMVisibleCenter: 256: text align position + // XIMPrimary, XIMHighlight, XIMSecondary, XIMTertiary are not specified, + // but interchangeable as above + // Currently there's no way to support these. + self.last_callback_event = Some(XimCallbackEvent::XimPreeditEvent( + self.window, + String::from(preedit_string), + )); + Ok(()) + } +} diff --git a/vendor/gpui-pre-linux/src/linux/xdg_desktop_portal.rs b/vendor/gpui-pre-linux/src/linux/xdg_desktop_portal.rs new file mode 100644 index 000000000..9b5d72476 --- /dev/null +++ b/vendor/gpui-pre-linux/src/linux/xdg_desktop_portal.rs @@ -0,0 +1,191 @@ +//! Provides a [calloop] event source from [XDG Desktop Portal] events +//! +//! This module uses the [ashpd] crate + +use ashpd::desktop::settings::{ColorScheme, Settings}; +use calloop::channel::Channel; +use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory}; +use smol::stream::StreamExt; + +use gpui::{BackgroundExecutor, WindowAppearance}; + +pub enum Event { + WindowAppearance(WindowAppearance), + #[cfg_attr(feature = "x11", allow(dead_code))] + CursorTheme(String), + #[cfg_attr(feature = "x11", allow(dead_code))] + CursorSize(u32), + ButtonLayout(String), +} + +pub struct XDPEventSource { + channel: Channel, +} + +impl XDPEventSource { + pub fn new(executor: &BackgroundExecutor) -> Self { + let (sender, channel) = calloop::channel::channel(); + + let background = executor.clone(); + + executor + .spawn(async move { + let settings = Settings::new().await?; + + if let Ok(initial_appearance) = settings.color_scheme().await { + sender.send(Event::WindowAppearance( + window_appearance_from_color_scheme(initial_appearance), + ))?; + } + if let Ok(initial_theme) = settings + .read::("org.gnome.desktop.interface", "cursor-theme") + .await + { + sender.send(Event::CursorTheme(initial_theme))?; + } + + // If u32 is used here, it throws invalid type error + if let Ok(initial_size) = settings + .read::("org.gnome.desktop.interface", "cursor-size") + .await + { + sender.send(Event::CursorSize(initial_size as u32))?; + } + + if let Ok(initial_layout) = settings + .read::("org.gnome.desktop.wm.preferences", "button-layout") + .await + { + sender.send(Event::ButtonLayout(initial_layout))?; + } + + if let Ok(mut cursor_theme_changed) = settings + .receive_setting_changed_with_args( + "org.gnome.desktop.interface", + "cursor-theme", + ) + .await + { + let sender = sender.clone(); + background + .spawn(async move { + while let Some(theme) = cursor_theme_changed.next().await { + let theme = theme?; + sender.send(Event::CursorTheme(theme))?; + } + anyhow::Ok(()) + }) + .detach(); + } + + if let Ok(mut cursor_size_changed) = settings + .receive_setting_changed_with_args::( + "org.gnome.desktop.interface", + "cursor-size", + ) + .await + { + let sender = sender.clone(); + background + .spawn(async move { + while let Some(size) = cursor_size_changed.next().await { + let size = size?; + sender.send(Event::CursorSize(size as u32))?; + } + anyhow::Ok(()) + }) + .detach(); + } + + if let Ok(mut button_layout_changed) = settings + .receive_setting_changed_with_args( + "org.gnome.desktop.wm.preferences", + "button-layout", + ) + .await + { + let sender = sender.clone(); + background + .spawn(async move { + while let Some(layout) = button_layout_changed.next().await { + let layout = layout?; + sender.send(Event::ButtonLayout(layout))?; + } + anyhow::Ok(()) + }) + .detach(); + } + + let mut appearance_changed = settings.receive_color_scheme_changed().await?; + while let Some(scheme) = appearance_changed.next().await { + sender.send(Event::WindowAppearance( + window_appearance_from_color_scheme(scheme), + ))?; + } + + anyhow::Ok(()) + }) + .detach(); + + Self { channel } + } +} + +impl EventSource for XDPEventSource { + type Event = Event; + type Metadata = (); + type Ret = (); + type Error = anyhow::Error; + + fn process_events( + &mut self, + readiness: Readiness, + token: Token, + mut callback: F, + ) -> Result + where + F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, + { + self.channel.process_events(readiness, token, |evt, _| { + if let calloop::channel::Event::Msg(msg) = evt { + (callback)(msg, &mut ()) + } + })?; + + Ok(PostAction::Continue) + } + + fn register( + &mut self, + poll: &mut Poll, + token_factory: &mut TokenFactory, + ) -> calloop::Result<()> { + self.channel.register(poll, token_factory)?; + + Ok(()) + } + + fn reregister( + &mut self, + poll: &mut Poll, + token_factory: &mut TokenFactory, + ) -> calloop::Result<()> { + self.channel.reregister(poll, token_factory)?; + + Ok(()) + } + + fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> { + self.channel.unregister(poll)?; + + Ok(()) + } +} + +fn window_appearance_from_color_scheme(cs: ColorScheme) -> WindowAppearance { + match cs { + ColorScheme::PreferDark => WindowAppearance::Dark, + ColorScheme::PreferLight => WindowAppearance::Light, + ColorScheme::NoPreference => WindowAppearance::Light, + } +}