logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
132
crates/gpui_linux/Cargo.toml
Normal file
132
crates/gpui_linux/Cargo.toml
Normal file
@@ -0,0 +1,132 @@
|
||||
[package]
|
||||
name = "gpui_linux"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/gpui_linux.rs"
|
||||
|
||||
[features]
|
||||
default = ["wayland", "x11"]
|
||||
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",
|
||||
"open",
|
||||
"gpui/wayland",
|
||||
]
|
||||
x11 = [
|
||||
"gpui_wgpu",
|
||||
"ashpd",
|
||||
|
||||
"as-raw-xcb-connection",
|
||||
"x11rb",
|
||||
"xkbcommon",
|
||||
"xim",
|
||||
"x11-clipboard",
|
||||
"filedescriptor",
|
||||
"open",
|
||||
"scap?/x11",
|
||||
]
|
||||
screen-capture = [
|
||||
"gpui/screen-capture",
|
||||
"scap",
|
||||
]
|
||||
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
|
||||
anyhow.workspace = true
|
||||
bytemuck = "1"
|
||||
collections.workspace = true
|
||||
image.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
gpui_wgpu = { workspace = true, optional = true, features = ["font-kit"] }
|
||||
http_client.workspace = true
|
||||
itertools.workspace = true
|
||||
libc.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
pathfinder_geometry = "0.5"
|
||||
pollster.workspace = true
|
||||
profiling.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
# Always used
|
||||
oo7 = { version = "0.6", default-features = false, features = [
|
||||
"async-std",
|
||||
"native_crypto",
|
||||
] }
|
||||
calloop = "0.14.3"
|
||||
raw-window-handle = "0.6"
|
||||
|
||||
# Used in both windowing options
|
||||
ashpd = { workspace = true, optional = true }
|
||||
swash = { version = "0.2.6" }
|
||||
bitflags = { workspace = true, optional = true }
|
||||
filedescriptor = { version = "0.8.2", optional = true }
|
||||
open = { version = "5.2.0", optional = true }
|
||||
xkbcommon = { version = "0.8.0", features = ["wayland", "x11"], optional = true }
|
||||
|
||||
# Screen capture
|
||||
scap = { workspace = true, optional = true }
|
||||
|
||||
# Wayland
|
||||
calloop-wayland-source = { version = "0.4.1", optional = true }
|
||||
wayland-backend = { version = "0.3.3", features = [
|
||||
"client_system",
|
||||
"dlopen",
|
||||
], optional = true }
|
||||
wayland-client = { version = "0.31.11", optional = true }
|
||||
wayland-cursor = { version = "0.31.11", optional = true }
|
||||
wayland-protocols = { version = "0.32.9", features = [
|
||||
"client",
|
||||
"staging",
|
||||
"unstable",
|
||||
], optional = true }
|
||||
wayland-protocols-plasma = { version = "0.3.9", features = [
|
||||
"client",
|
||||
], optional = true }
|
||||
wayland-protocols-wlr = { version = "0.3.9", features = [
|
||||
"client",
|
||||
], optional = true }
|
||||
|
||||
# X11
|
||||
as-raw-xcb-connection = { version = "1", optional = true }
|
||||
x11rb = { version = "0.13.1", features = [
|
||||
"allow-unsafe-code",
|
||||
"xkb",
|
||||
"randr",
|
||||
"xinput",
|
||||
"cursor",
|
||||
"resource_manager",
|
||||
"sync",
|
||||
"dri3",
|
||||
], optional = true }
|
||||
# WARNING: If you change this, you must also publish a new version of zed-xim to crates.io
|
||||
xim = { git = "https://github.com/zed-industries/xim-rs.git", rev = "16f35a2c881b815a2b6cdfd6687988e84f8447d8", features = [
|
||||
"x11rb-xcb",
|
||||
"x11rb-client",
|
||||
], package = "zed-xim", version = "0.4.0-zed", optional = true }
|
||||
x11-clipboard = { version = "0.9.3", optional = true }
|
||||
1
crates/gpui_linux/LICENSE-APACHE
Symbolic link
1
crates/gpui_linux/LICENSE-APACHE
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-APACHE
|
||||
4
crates/gpui_linux/src/gpui_linux.rs
Normal file
4
crates/gpui_linux/src/gpui_linux.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
#![cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
mod linux;
|
||||
|
||||
pub use linux::current_platform;
|
||||
57
crates/gpui_linux/src/linux.rs
Normal file
57
crates/gpui_linux/src/linux.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
mod dispatcher;
|
||||
mod headless;
|
||||
mod keyboard;
|
||||
mod platform;
|
||||
#[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<dyn gpui::Platform> {
|
||||
#[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!(),
|
||||
}
|
||||
}
|
||||
417
crates/gpui_linux/src/linux/dispatcher.rs
Normal file
417
crates/gpui_linux/src/linux/dispatcher.rs
Normal file
@@ -0,0 +1,417 @@
|
||||
use calloop::{
|
||||
EventLoop, PostAction,
|
||||
channel::{self, Sender},
|
||||
timer::TimeoutAction,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
use std::{
|
||||
mem::MaybeUninit,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use gpui::{
|
||||
GLOBAL_THREAD_TIMINGS, PlatformDispatcher, Priority, PriorityQueueReceiver,
|
||||
PriorityQueueSender, RunnableVariant, TaskTiming, ThreadTaskTimings, profiler,
|
||||
};
|
||||
|
||||
struct TimerAfter {
|
||||
duration: Duration,
|
||||
runnable: RunnableVariant,
|
||||
}
|
||||
|
||||
pub(crate) struct LinuxDispatcher {
|
||||
main_sender: PriorityQueueCalloopSender<RunnableVariant>,
|
||||
timer_sender: Sender<TimerAfter>,
|
||||
background_sender: PriorityQueueSender<RunnableVariant>,
|
||||
_background_threads: Vec<thread::JoinHandle<()>>,
|
||||
main_thread_id: thread::ThreadId,
|
||||
}
|
||||
|
||||
const MIN_THREADS: usize = 2;
|
||||
|
||||
impl LinuxDispatcher {
|
||||
pub fn new(main_sender: PriorityQueueCalloopSender<RunnableVariant>) -> 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<RunnableVariant> = background_receiver.clone();
|
||||
std::thread::Builder::new()
|
||||
.name(format!("Worker-{i}"))
|
||||
.spawn(move || {
|
||||
for runnable in receiver.iter() {
|
||||
let start = Instant::now();
|
||||
|
||||
let location = runnable.metadata().location;
|
||||
let mut timing = TaskTiming {
|
||||
location,
|
||||
start,
|
||||
end: None,
|
||||
};
|
||||
profiler::add_task_timing(timing);
|
||||
|
||||
runnable.run();
|
||||
|
||||
let end = Instant::now();
|
||||
timing.end = Some(end);
|
||||
profiler::add_task_timing(timing);
|
||||
|
||||
log::trace!(
|
||||
"background thread {}: ran runnable. took: {:?}",
|
||||
i,
|
||||
start.elapsed()
|
||||
);
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (timer_sender, timer_channel) = calloop::channel::channel::<TimerAfter>();
|
||||
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 start = Instant::now();
|
||||
let location = runnable.metadata().location;
|
||||
let mut timing = TaskTiming {
|
||||
location,
|
||||
start,
|
||||
end: None,
|
||||
};
|
||||
profiler::add_task_timing(timing);
|
||||
|
||||
runnable.run();
|
||||
let end = Instant::now();
|
||||
|
||||
timing.end = Some(end);
|
||||
profiler::add_task_timing(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 get_all_timings(&self) -> Vec<gpui::ThreadTaskTimings> {
|
||||
let global_timings = GLOBAL_THREAD_TIMINGS.lock();
|
||||
ThreadTaskTimings::convert(&global_timings)
|
||||
}
|
||||
|
||||
fn get_current_thread_timings(&self) -> gpui::ThreadTaskTimings {
|
||||
gpui::profiler::get_current_thread_task_timings()
|
||||
}
|
||||
|
||||
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) {
|
||||
self.timer_sender
|
||||
.send(TimerAfter { duration, runnable })
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>) {
|
||||
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::<libc::sched_param>::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<T> {
|
||||
sender: PriorityQueueSender<T>,
|
||||
ping: calloop::ping::Ping,
|
||||
}
|
||||
|
||||
impl<T> PriorityQueueCalloopSender<T> {
|
||||
fn new(tx: PriorityQueueSender<T>, ping: calloop::ping::Ping) -> Self {
|
||||
Self { sender: tx, ping }
|
||||
}
|
||||
|
||||
fn send(&self, priority: Priority, item: T) -> Result<(), gpui::queue::SendError<T>> {
|
||||
let res = self.sender.send(priority, item);
|
||||
if res.is_ok() {
|
||||
self.ping.ping();
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for PriorityQueueCalloopSender<T> {
|
||||
fn drop(&mut self) {
|
||||
self.ping.ping();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PriorityQueueCalloopReceiver<T> {
|
||||
receiver: PriorityQueueReceiver<T>,
|
||||
source: calloop::ping::PingSource,
|
||||
ping: calloop::ping::Ping,
|
||||
}
|
||||
|
||||
impl<T> PriorityQueueCalloopReceiver<T> {
|
||||
pub fn new() -> (PriorityQueueCalloopSender<T>, 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<T> calloop::EventSource for PriorityQueueCalloopReceiver<T> {
|
||||
type Event = Event<T>;
|
||||
type Metadata = ();
|
||||
type Ret = ();
|
||||
type Error = ChannelError;
|
||||
|
||||
fn process_events<F>(
|
||||
&mut self,
|
||||
readiness: calloop::Readiness,
|
||||
token: calloop::Token,
|
||||
mut callback: F,
|
||||
) -> Result<calloop::PostAction, Self::Error>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// running 1 test
|
||||
// test linux::dispatcher::tests::tomato ... FAILED
|
||||
|
||||
// failures:
|
||||
|
||||
// ---- linux::dispatcher::tests::tomato stdout ----
|
||||
// [crates/gpui/src/platform/linux/dispatcher.rs:262:9]
|
||||
// returning 1 tasks to process
|
||||
// [crates/gpui/src/platform/linux/dispatcher.rs:480:75] evt = Msg(
|
||||
// (),
|
||||
// )
|
||||
// returning 0 tasks to process
|
||||
|
||||
// thread 'linux::dispatcher::tests::tomato' (478301) panicked at crates/gpui/src/platform/linux/dispatcher.rs:515:9:
|
||||
// assertion failed: data.got_closed
|
||||
// note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||
3
crates/gpui_linux/src/linux/headless.rs
Normal file
3
crates/gpui_linux/src/linux/headless.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod client;
|
||||
|
||||
pub(crate) use client::*;
|
||||
128
crates/gpui_linux/src/linux/headless/client.rs
Normal file
128
crates/gpui_linux/src/linux/headless/client.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use calloop::{EventLoop, LoopHandle};
|
||||
use util::ResultExt;
|
||||
|
||||
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<calloop::EventLoop<'static, HeadlessClient>>,
|
||||
pub(crate) common: LinuxCommon,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct HeadlessClient(Rc<RefCell<HeadlessClientState>>);
|
||||
|
||||
impl HeadlessClient {
|
||||
pub(crate) fn new() -> Self {
|
||||
let event_loop = EventLoop::try_new().unwrap();
|
||||
|
||||
let (common, main_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();
|
||||
|
||||
HeadlessClient(Rc::new(RefCell::new(HeadlessClientState {
|
||||
event_loop: Some(event_loop),
|
||||
_loop_handle: handle,
|
||||
common,
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
impl LinuxClient for HeadlessClient {
|
||||
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
|
||||
f(&mut self.0.borrow_mut().common)
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
||||
Box::new(LinuxKeyboardLayout::new("unknown".into()))
|
||||
}
|
||||
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn display(&self, _id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn gpui::ScreenCaptureSource>>>>
|
||||
{
|
||||
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<AnyWindowHandle> {
|
||||
None
|
||||
}
|
||||
|
||||
fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
_handle: AnyWindowHandle,
|
||||
_params: WindowParams,
|
||||
) -> anyhow::Result<Box<dyn PlatformWindow>> {
|
||||
anyhow::bail!("neither DISPLAY nor WAYLAND_DISPLAY is set. You can run in headless mode");
|
||||
}
|
||||
|
||||
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<gpui::ClipboardItem> {
|
||||
None
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<gpui::ClipboardItem> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
22
crates/gpui_linux/src/linux/keyboard.rs
Normal file
22
crates/gpui_linux/src/linux/keyboard.rs
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
1129
crates/gpui_linux/src/linux/platform.rs
Normal file
1129
crates/gpui_linux/src/linux/platform.rs
Normal file
File diff suppressed because it is too large
Load Diff
1
crates/gpui_linux/src/linux/text_system.rs
Normal file
1
crates/gpui_linux/src/linux/text_system.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub(crate) use gpui_wgpu::CosmicTextSystem;
|
||||
41
crates/gpui_linux/src/linux/wayland.rs
Normal file
41
crates/gpui_linux/src/linux/wayland.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
mod client;
|
||||
mod clipboard;
|
||||
mod cursor;
|
||||
mod display;
|
||||
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,
|
||||
}
|
||||
}
|
||||
2572
crates/gpui_linux/src/linux/wayland/client.rs
Normal file
2572
crates/gpui_linux/src/linux/wayland/client.rs
Normal file
File diff suppressed because it is too large
Load Diff
260
crates/gpui_linux/src/linux/wayland/clipboard.rs
Normal file
260
crates/gpui_linux/src/linux/wayland/clipboard.rs
Normal file
@@ -0,0 +1,260 @@
|
||||
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::read_fd};
|
||||
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<ClipboardItem>,
|
||||
primary_contents: Option<ClipboardItem>,
|
||||
|
||||
// External clipboard
|
||||
cached_read: Option<ClipboardItem>,
|
||||
current_offer: Option<DataOffer<WlDataOffer>>,
|
||||
cached_primary_read: Option<ClipboardItem>,
|
||||
current_primary_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
|
||||
}
|
||||
|
||||
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<T: ReceiveData> {
|
||||
pub inner: T,
|
||||
mime_types: Vec<String>,
|
||||
}
|
||||
|
||||
impl<T: ReceiveData> DataOffer<T> {
|
||||
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<Vec<u8>> {
|
||||
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 unsafe { read_fd(fd) } {
|
||||
Ok(bytes) => Some(bytes),
|
||||
Err(err) => {
|
||||
log::error!("error reading clipboard pipe: {err:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_text(&self, connection: &Connection) -> Option<ClipboardItem> {
|
||||
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<ClipboardItem> {
|
||||
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<DataOffer<WlDataOffer>>) {
|
||||
self.cached_read = None;
|
||||
self.current_offer = data_offer;
|
||||
}
|
||||
|
||||
pub fn set_primary_offer(&mut self, data_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>) {
|
||||
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_internal(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_internal(fd, text.as_bytes().to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&mut self) -> Option<ClipboardItem> {
|
||||
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<ClipboardItem> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn send_internal(&self, fd: OwnedFd, bytes: Vec<u8>) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
152
crates/gpui_linux/src/linux/wayland/cursor.rs
Normal file
152
crates/gpui_linux/src/linux/wayland/cursor.rs
Normal file
@@ -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 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<LoadedTheme>,
|
||||
size: u32,
|
||||
scaled_size: u32,
|
||||
surface: WlSurface,
|
||||
shm: WlShm,
|
||||
connection: Connection,
|
||||
}
|
||||
|
||||
pub(crate) struct LoadedTheme {
|
||||
theme: CursorTheme,
|
||||
name: Option<String>,
|
||||
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<String>) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
42
crates/gpui_linux/src/linux/wayland/display.rs
Normal file
42
crates/gpui_linux/src/linux/wayland/display.rs
Normal file
@@ -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<String>,
|
||||
pub bounds: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
impl Hash for WaylandDisplay {
|
||||
fn hash<H: Hasher>(&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<Uuid> {
|
||||
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<Pixels> {
|
||||
self.bounds
|
||||
}
|
||||
}
|
||||
26
crates/gpui_linux/src/linux/wayland/layer_shell.rs
Normal file
26
crates/gpui_linux/src/linux/wayland/layer_shell.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
67
crates/gpui_linux/src/linux/wayland/serial.rs
Normal file
67
crates/gpui_linux/src/linux/wayland/serial.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use collections::HashMap;
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||
pub(crate) enum SerialKind {
|
||||
DataDevice,
|
||||
InputMethod,
|
||||
MouseEnter,
|
||||
MousePress,
|
||||
KeyPress,
|
||||
/// Serial carried by the most recent `wl_keyboard.enter` event (the
|
||||
/// compositor's keyboard-focus serial). Mutter's
|
||||
/// `meta_wayland_keyboard_can_grab_surface` accepts exactly this serial
|
||||
/// when activating a focused window via xdg-activation-v1, so it must be
|
||||
/// attached to activation tokens — a stale mouse-press serial is rejected.
|
||||
KeyboardEnter,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SerialData {
|
||||
serial: u32,
|
||||
}
|
||||
|
||||
impl SerialData {
|
||||
fn new(value: u32) -> Self {
|
||||
Self { serial: value }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper for tracking of different serial kinds.
|
||||
pub(crate) struct SerialTracker {
|
||||
serials: HashMap<SerialKind, SerialData>,
|
||||
}
|
||||
|
||||
impl SerialTracker {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
serials: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, kind: SerialKind, value: u32) {
|
||||
self.serials.insert(kind, SerialData::new(value));
|
||||
}
|
||||
|
||||
/// Returns the latest tracked serial of the provided [`SerialKind`]
|
||||
///
|
||||
/// Will return 0 if not tracked.
|
||||
pub fn get(&self, kind: SerialKind) -> u32 {
|
||||
self.serials
|
||||
.get(&kind)
|
||||
.map(|serial_data| serial_data.serial)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Returns the latest tracked serial of the provided [`SerialKind`]s
|
||||
///
|
||||
/// Will return 0 if not tracked.
|
||||
pub fn latest_of(&self, kinds: &[SerialKind]) -> u32 {
|
||||
kinds
|
||||
.iter()
|
||||
.filter_map(|kind| self.serials.get(kind))
|
||||
.max_by_key(|serial_data| serial_data.serial)
|
||||
.map(|serial_data| serial_data.serial)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
1664
crates/gpui_linux/src/linux/wayland/window.rs
Normal file
1664
crates/gpui_linux/src/linux/wayland/window.rs
Normal file
File diff suppressed because it is too large
Load Diff
12
crates/gpui_linux/src/linux/x11.rs
Normal file
12
crates/gpui_linux/src/linux/x11.rs
Normal file
@@ -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::*;
|
||||
3116
crates/gpui_linux/src/linux/x11/client.rs
Normal file
3116
crates/gpui_linux/src/linux/x11/client.rs
Normal file
File diff suppressed because it is too large
Load Diff
1255
crates/gpui_linux/src/linux/x11/clipboard.rs
Normal file
1255
crates/gpui_linux/src/linux/x11/clipboard.rs
Normal file
File diff suppressed because it is too large
Load Diff
51
crates/gpui_linux/src/linux/x11/display.rs
Normal file
51
crates/gpui_linux/src/linux/x11/display.rs
Normal file
@@ -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<Pixels>,
|
||||
uuid: Uuid,
|
||||
}
|
||||
|
||||
impl X11Display {
|
||||
pub(crate) fn new(
|
||||
xcb: &XCBConnection,
|
||||
scale_factor: f32,
|
||||
x_screen_index: usize,
|
||||
) -> anyhow::Result<Self> {
|
||||
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<Uuid> {
|
||||
Ok(self.uuid)
|
||||
}
|
||||
|
||||
fn bounds(&self) -> Bounds<Pixels> {
|
||||
self.bounds
|
||||
}
|
||||
}
|
||||
154
crates/gpui_linux/src/linux/x11/event.rs
Normal file
154
crates/gpui_linux/src/linux/x11/event.rs
Normal file
@@ -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<ButtonOrScroll> {
|
||||
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<MouseButton> {
|
||||
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<u32>,
|
||||
valuator_number: u16,
|
||||
) -> Option<usize> {
|
||||
// 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<u32>, 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<u32>, 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));
|
||||
}
|
||||
}
|
||||
1889
crates/gpui_linux/src/linux/x11/window.rs
Normal file
1889
crates/gpui_linux/src/linux/x11/window.rs
Normal file
File diff suppressed because it is too large
Load Diff
133
crates/gpui_linux/src/linux/x11/xim_handler.rs
Normal file
133
crates/gpui_linux/src/linux/x11/xim_handler.rs
Normal file
@@ -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<XimCallbackEvent>,
|
||||
}
|
||||
|
||||
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<C: Client<XEvent = xproto::KeyPressEvent>> ClientHandler<C> 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<AttributeName, Vec<u8>>,
|
||||
) -> 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<xim::Feedback>,
|
||||
) -> 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(())
|
||||
}
|
||||
}
|
||||
191
crates/gpui_linux/src/linux/xdg_desktop_portal.rs
Normal file
191
crates/gpui_linux/src/linux/xdg_desktop_portal.rs
Normal file
@@ -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<Event>,
|
||||
}
|
||||
|
||||
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::<String>("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::<i32>("org.gnome.desktop.interface", "cursor-size")
|
||||
.await
|
||||
{
|
||||
sender.send(Event::CursorSize(initial_size as u32))?;
|
||||
}
|
||||
|
||||
if let Ok(initial_layout) = settings
|
||||
.read::<String>("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::<i32>(
|
||||
"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<F>(
|
||||
&mut self,
|
||||
readiness: Readiness,
|
||||
token: Token,
|
||||
mut callback: F,
|
||||
) -> Result<PostAction, Self::Error>
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user