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