230 lines
9.3 KiB
Rust
230 lines
9.3 KiB
Rust
|
|
//! Bringing the window to the front when a search shortcut fires — either the
|
||
|
|
//! one QuickSearch registered for itself (`crate::hotkey`) or one a desktop
|
||
|
|
//! binding relayed through `--toggle`. Both land here. Every display server
|
||
|
|
//! stops applications raising themselves, so the request has to carry a
|
||
|
|
//! reason a window manager will accept.
|
||
|
|
//!
|
||
|
|
//! * **X11**: winit asks with `_NET_ACTIVE_WINDOW` source indication 1
|
||
|
|
//! ("application"), which KWin, Mutter and Xfwm all refuse from an
|
||
|
|
//! unfocused window, and its `focus_window` does nothing while minimised.
|
||
|
|
//! Hence [`x11_activate`], which sends source indication 2 (EWMH's "direct
|
||
|
|
//! user action"). Do not replace it with winit's version.
|
||
|
|
//! * **Wayland**: a client cannot raise itself; the compositor only honours
|
||
|
|
//! an xdg-activation token, and winit 0.30 applies one in exactly one
|
||
|
|
//! place — `WindowAttributes::with_activation_token`, at window creation —
|
||
|
|
//! which egui's `ViewportBuilder` has no field for and eframe therefore
|
||
|
|
//! never sets. `focus_window` on Wayland is an empty function body. So a
|
||
|
|
//! `--toggle` that *starts* the app gets whatever focus the compositor
|
||
|
|
//! gives a newly mapped window, and one that finds it already running
|
||
|
|
//! cannot raise it at all: the most this path can do is ask for attention,
|
||
|
|
//! which shows up as a highlighted task entry. Closing that gap means
|
||
|
|
//! patching both winit (to activate a live surface) and eframe (to carry
|
||
|
|
//! the token), the way `vendor/` already patches two other crates.
|
||
|
|
//! * **Windows**: `SetForegroundWindow` is refused to background processes,
|
||
|
|
//! so the same limit applies to a process that did not just receive input.
|
||
|
|
|
||
|
|
/// Whether this is a Wayland session, where an already-open window cannot be
|
||
|
|
/// raised. The Settings tab says so rather than letting the shortcut look
|
||
|
|
/// broken; see the module docs.
|
||
|
|
pub fn is_wayland() -> bool {
|
||
|
|
cfg!(all(unix, not(target_os = "macos"))) && std::env::var_os("WAYLAND_DISPLAY").is_some()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Bring the window to the front, restoring it if it was minimised.
|
||
|
|
pub fn raise(ctx: &egui::Context, frame: &eframe::Frame) {
|
||
|
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||
|
|
{
|
||
|
|
if x11_activate(frame) {
|
||
|
|
// Belt and braces: EWMH says activating an iconified window
|
||
|
|
// de-iconifies it, but not every window manager does, and a
|
||
|
|
// window left minimised cannot take the caret.
|
||
|
|
ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(false));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
// Wayland: nothing below will raise the window, so make the one
|
||
|
|
// request the compositor does honour from a background client.
|
||
|
|
if std::env::var_os("WAYLAND_DISPLAY").is_some() {
|
||
|
|
ctx.send_viewport_cmd(egui::ViewportCommand::RequestUserAttention(
|
||
|
|
egui::UserAttentionType::Informational,
|
||
|
|
));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
#[cfg(not(all(unix, not(target_os = "macos"))))]
|
||
|
|
{
|
||
|
|
let _ = frame;
|
||
|
|
}
|
||
|
|
|
||
|
|
// A window still minimised cannot take focus.
|
||
|
|
ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(false));
|
||
|
|
ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
|
||
|
|
}
|
||
|
|
|
||
|
|
// The X connection used for activation, kept open across presses.
|
||
|
|
// Thread-local because `raise` only ever runs on the UI thread, and held
|
||
|
|
// rather than reconnected because a connect per keypress is both wasteful
|
||
|
|
// and, with the timestamp round trip below, an extra round trip.
|
||
|
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||
|
|
thread_local! {
|
||
|
|
static X11: std::cell::RefCell<Option<X11State>> = const { std::cell::RefCell::new(None) };
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||
|
|
struct X11State {
|
||
|
|
conn: x11rb::rust_connection::RustConnection,
|
||
|
|
root: u32,
|
||
|
|
/// A 1x1 unmapped window of our own, existing only so there is a
|
||
|
|
/// property we may change to ask the server what time it is.
|
||
|
|
clock: u32,
|
||
|
|
net_active: u32,
|
||
|
|
user_time: u32,
|
||
|
|
marker: u32,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||
|
|
impl X11State {
|
||
|
|
fn connect() -> Result<X11State, Box<dyn std::error::Error>> {
|
||
|
|
use x11rb::connection::Connection;
|
||
|
|
use x11rb::protocol::xproto::{ConnectionExt, CreateWindowAux, EventMask, WindowClass};
|
||
|
|
|
||
|
|
let (conn, screen_num) = x11rb::connect(None)?;
|
||
|
|
let root = conn.setup().roots[screen_num].root;
|
||
|
|
let clock = conn.generate_id()?;
|
||
|
|
// InputOnly and never mapped: invisible, and the window manager
|
||
|
|
// ignores it entirely.
|
||
|
|
conn.create_window(
|
||
|
|
0,
|
||
|
|
clock,
|
||
|
|
root,
|
||
|
|
0,
|
||
|
|
0,
|
||
|
|
1,
|
||
|
|
1,
|
||
|
|
0,
|
||
|
|
WindowClass::INPUT_ONLY,
|
||
|
|
0,
|
||
|
|
&CreateWindowAux::new().event_mask(EventMask::PROPERTY_CHANGE),
|
||
|
|
)?;
|
||
|
|
let intern = |name: &[u8]| -> Result<u32, Box<dyn std::error::Error>> {
|
||
|
|
Ok(conn.intern_atom(false, name)?.reply()?.atom)
|
||
|
|
};
|
||
|
|
let state = X11State {
|
||
|
|
root,
|
||
|
|
clock,
|
||
|
|
net_active: intern(b"_NET_ACTIVE_WINDOW")?,
|
||
|
|
user_time: intern(b"_NET_WM_USER_TIME")?,
|
||
|
|
marker: intern(b"_QUICKSEARCH_CLOCK")?,
|
||
|
|
conn,
|
||
|
|
};
|
||
|
|
Ok(state)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The server's current time.
|
||
|
|
///
|
||
|
|
/// **Not `CURRENT_TIME`.** KWin reads a zero timestamp as the window
|
||
|
|
/// saying it does not want focus and refuses the activation, which is
|
||
|
|
/// what made the shortcut raise the window only sometimes. The standard
|
||
|
|
/// way to get a real one is a zero-length property append, which comes
|
||
|
|
/// back as a `PropertyNotify` stamped by the server.
|
||
|
|
fn now(&self) -> Result<u32, Box<dyn std::error::Error>> {
|
||
|
|
use x11rb::connection::Connection;
|
||
|
|
use x11rb::protocol::xproto::{AtomEnum, PropMode};
|
||
|
|
use x11rb::protocol::Event;
|
||
|
|
use x11rb::wrapper::ConnectionExt as _;
|
||
|
|
|
||
|
|
self.conn.change_property8(
|
||
|
|
PropMode::APPEND,
|
||
|
|
self.clock,
|
||
|
|
self.marker,
|
||
|
|
AtomEnum::STRING,
|
||
|
|
&[],
|
||
|
|
)?;
|
||
|
|
self.conn.flush()?;
|
||
|
|
// Bounded: this runs on the UI thread, which may never block on the
|
||
|
|
// X server for longer than a frame or two.
|
||
|
|
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(100);
|
||
|
|
while std::time::Instant::now() < deadline {
|
||
|
|
match self.conn.poll_for_event()? {
|
||
|
|
Some(Event::PropertyNotify(e)) if e.window == self.clock => return Ok(e.time),
|
||
|
|
Some(_) => continue,
|
||
|
|
None => std::thread::sleep(std::time::Duration::from_millis(2)),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Err("the X server did not answer with a timestamp".into())
|
||
|
|
}
|
||
|
|
|
||
|
|
fn activate(&self, window: u32) -> Result<(), Box<dyn std::error::Error>> {
|
||
|
|
use x11rb::connection::Connection;
|
||
|
|
use x11rb::protocol::xproto::{
|
||
|
|
AtomEnum, ClientMessageEvent, ConnectionExt, EventMask, PropMode,
|
||
|
|
};
|
||
|
|
use x11rb::wrapper::ConnectionExt as _;
|
||
|
|
|
||
|
|
let time = self.now()?;
|
||
|
|
// Says the activation traces to real user input, and at this instant.
|
||
|
|
// Without it the window manager weighs our request against whatever
|
||
|
|
// the user touched most recently and can decide we are stale.
|
||
|
|
self.conn.change_property32(
|
||
|
|
PropMode::REPLACE,
|
||
|
|
window,
|
||
|
|
self.user_time,
|
||
|
|
AtomEnum::CARDINAL,
|
||
|
|
&[time],
|
||
|
|
)?;
|
||
|
|
// data: source indication, timestamp, the window losing focus.
|
||
|
|
// Source 2 is EWMH's "direct user action".
|
||
|
|
let event = ClientMessageEvent::new(32, window, self.net_active, [2, time, 0, 0, 0]);
|
||
|
|
self.conn.send_event(
|
||
|
|
false,
|
||
|
|
self.root,
|
||
|
|
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
||
|
|
event,
|
||
|
|
)?;
|
||
|
|
self.conn.flush()?;
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Activate our window, EWMH style. `false` when this is not X11 or the
|
||
|
|
/// server would not take it, so the caller can fall back to winit.
|
||
|
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||
|
|
fn x11_activate(frame: &eframe::Frame) -> bool {
|
||
|
|
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||
|
|
|
||
|
|
// Both of these used to fail silently, which is how a shortcut that never
|
||
|
|
// raised the window looked like a shortcut that never fired.
|
||
|
|
let handle = match frame.window_handle() {
|
||
|
|
Ok(handle) => handle,
|
||
|
|
Err(e) => {
|
||
|
|
quicksearch_core::log_warn!("raising the window: no window handle: {}", e);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
let window = match handle.as_raw() {
|
||
|
|
RawWindowHandle::Xlib(xlib) => xlib.window as u32,
|
||
|
|
other => {
|
||
|
|
quicksearch_core::log_warn!("raising the window: not an X11 window: {:?}", other);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
let sent = X11.with(|slot| -> Result<(), Box<dyn std::error::Error>> {
|
||
|
|
let mut slot = slot.borrow_mut();
|
||
|
|
let state = match slot.as_mut() {
|
||
|
|
Some(state) => state,
|
||
|
|
None => slot.insert(X11State::connect()?),
|
||
|
|
};
|
||
|
|
state.activate(window)
|
||
|
|
});
|
||
|
|
match sent {
|
||
|
|
Ok(()) => true,
|
||
|
|
Err(e) => {
|
||
|
|
quicksearch_core::log_warn!("raising the window: {}", e);
|
||
|
|
// A connection that failed mid-way is not reused: the next press
|
||
|
|
// reconnects rather than inheriting a broken one.
|
||
|
|
X11.with(|slot| slot.borrow_mut().take());
|
||
|
|
false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|