quick_search/crates/quicksearch-gui/src/activate/raise.rs

230 lines
9.3 KiB
Rust
Raw Normal View History

//! 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
}
}
}