Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions backends/wayland/include/display_server_ports.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@ class DisplayServerInputPort final : public InputPort {
void ungrab_all_keys() override;
void grab_button(WindowId window, uint8_t button, uint16_t mods) override;
void ungrab_all_buttons(WindowId window) override;
void grab_button_any(WindowId window) override;
void grab_pointer() override;
void ungrab_pointer() override;
void allow_events(bool replay) override;
void warp_pointer(WindowId window, Vec2i16 pos) override;
void warp_pointer_abs(Vec2i16 pos) override;
void flush() override;
Expand Down
3 changes: 0 additions & 3 deletions backends/wayland/src/ports/display_server_input_port.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ void DisplayServerInputPort::send_intercepts() {

void DisplayServerInputPort::grab_button(WindowId, uint8_t, uint16_t) {}
void DisplayServerInputPort::ungrab_all_buttons(WindowId) {}
void DisplayServerInputPort::grab_button_any(WindowId) {}

void DisplayServerInputPort::grab_pointer() {
backend_.grab_pointer();
Expand All @@ -34,8 +33,6 @@ void DisplayServerInputPort::ungrab_pointer() {
backend_.ungrab_pointer();
}

void DisplayServerInputPort::allow_events(bool) {}

void DisplayServerInputPort::warp_pointer(WindowId, Vec2i16 pos) {
backend_.warp_pointer(pos.x(), pos.y());
}
Expand Down
12 changes: 0 additions & 12 deletions backends/x11/include/x11_backend.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,6 @@ class X11Backend final : public Backend {
void set_wm_state_normal(WindowId win);
void reload_border_colors();

// Focus arbiter: one pending focus request per tick, highest priority wins.
// Consumed at end of pump_events() after all X events and backend effects.
// kPointer — EnterNotify / button click (always wins)
// kEWMH — _NET_ACTIVE_WINDOW client message, new window at MapRequest
// kWorkspace — workspace switch, restore_visible_focus, reload
// kNone — no request this tick
enum FocusPriority { kFocusNone = 0, kFocusWorkspace = 1, kFocusEWMH = 2, kFocusPointer = 3 };
WindowId pending_focus_win_ = NO_WINDOW;
FocusPriority pending_focus_priority_ = kFocusNone;
void request_focus(WindowId win, FocusPriority priority);

// Timestamp from the most recent user-input X event (button/key/motion/enter).
// Used for xcb_set_input_focus to satisfy clients that reject timestamp=0.
xcb_timestamp_t last_event_time_ = XCB_CURRENT_TIME;
Expand Down Expand Up @@ -127,7 +116,6 @@ class X11Backend final : public Backend {
void handle_enter_notify(xcb_enter_notify_event_t* ev);
void apply_core_backend_effects();
void apply_xresources(Core& core);
void restore_visible_focus();
void update_focus(event::FocusChanged ev); // X11 state only, no emit
// ewmh_on_* — synchronous EWMH/ICCCM reactions called from X event
// handlers BEFORE core.dispatch runs, because WindowUnmapped needs the
Expand Down
58 changes: 39 additions & 19 deletions backends/x11/src/backend/adopt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ struct RestartState {
struct WindowMetadata {
std::string wm_instance;
std::string wm_class;
std::string title;
WindowType type = WindowType::Normal;
bool wm_fixed_size = false;
bool wm_no_decorations = false;
Expand Down Expand Up @@ -73,20 +74,19 @@ RestartState load_restart_state() {
return out;
}

std::string read_window_title(XConnection& xconn, xcb_window_t win,
xcb_atom_t net_wm_name, xcb_atom_t utf8_string) {
auto title = xconn.get_text_property(win, net_wm_name, utf8_string);
if (title.empty())
title = xconn.get_text_property(win, XCB_ATOM_WM_NAME, XCB_ATOM_STRING);
return title;
}

WindowMetadata read_window_metadata(XConnection& xconn, WindowId window) {
WindowMetadata out;
auto [instance, cls] = xconn.get_wm_class(window);
out.wm_instance = std::move(instance);
out.wm_class = std::move(cls);

static const auto named = xconn.intern_atoms({ "_NET_WM_NAME", "UTF8_STRING" });
static xcb_atom_t net_wm_name = named.at("_NET_WM_NAME");
static xcb_atom_t utf8_string = named.at("UTF8_STRING");
out.title = xconn.get_text_property(window, net_wm_name, utf8_string);
if (out.title.empty())
out.title = xconn.get_text_property(window, XCB_ATOM_WM_NAME, XCB_ATOM_STRING);

const auto& atoms = window_type_atoms(xconn);
auto types = xconn.get_atom_list_property(window, atoms.net_wm_window_type);
if (has_atom(types, atoms.modal)) out.type = WindowType::Modal;
Expand All @@ -105,7 +105,8 @@ StartupSnapshot X11Backend::scan_existing_windows() {
constexpr uint32_t kManagedEventMask =
XCB_EVENT_MASK_STRUCTURE_NOTIFY |
XCB_EVENT_MASK_ENTER_WINDOW |
XCB_EVENT_MASK_FOCUS_CHANGE;
XCB_EVENT_MASK_FOCUS_CHANGE |
XCB_EVENT_MASK_PROPERTY_CHANGE;

StartupSnapshot result;
auto& out = result.windows;
Expand All @@ -119,8 +120,6 @@ StartupSnapshot X11Backend::scan_existing_windows() {
"_NET_WM_WINDOW_TYPE_NOTIFICATION",
"_NET_WM_WINDOW_TYPE_TOOLTIP",
"_NET_WM_WINDOW_TYPE_DND",
"_NET_WM_NAME",
"UTF8_STRING",
"WM_STATE",
});
xcb_atom_t NET_WM_WINDOW_TYPE = atoms["_NET_WM_WINDOW_TYPE"];
Expand All @@ -129,8 +128,6 @@ StartupSnapshot X11Backend::scan_existing_windows() {
xcb_atom_t NET_WM_WINDOW_TYPE_NOTIFICATION = atoms["_NET_WM_WINDOW_TYPE_NOTIFICATION"];
xcb_atom_t NET_WM_WINDOW_TYPE_TOOLTIP = atoms["_NET_WM_WINDOW_TYPE_TOOLTIP"];
xcb_atom_t NET_WM_WINDOW_TYPE_DND = atoms["_NET_WM_WINDOW_TYPE_DND"];
xcb_atom_t NET_WM_NAME = atoms["_NET_WM_NAME"];
xcb_atom_t UTF8_STRING = atoms["UTF8_STRING"];
xcb_atom_t WM_STATE = atoms["WM_STATE"];

auto children = xconn.query_tree_children(root_window);
Expand Down Expand Up @@ -167,9 +164,8 @@ StartupSnapshot X11Backend::scan_existing_windows() {
continue;
}

auto [instance, cls] = xconn.get_wm_class(win);
auto title = read_window_title(xconn, win, NET_WM_NAME, UTF8_STRING);
bool identifiable = !instance.empty() || !cls.empty() || !title.empty();
auto meta = read_window_metadata(xconn, win);
bool identifiable = !meta.wm_instance.empty() || !meta.wm_class.empty() || !meta.title.empty();

int wm_state = xconn.get_wm_state_value(win, WM_STATE);
bool iconic = (wm_state == ICCCM_ICONIC_STATE);
Expand All @@ -187,14 +183,14 @@ StartupSnapshot X11Backend::scan_existing_windows() {
!unsupported_type &&
identifiable;

auto meta = read_window_metadata(xconn, win);
ExistingWindowSnapshot snap{};
snap.window = win;
snap.currently_viewable = (attrs.map_state == XCB_MAP_STATE_VIEWABLE);
snap.default_manage = default_manage;
snap.from_restart = from_restart;
snap.wm_instance = std::move(meta.wm_instance);
snap.wm_class = std::move(meta.wm_class);
snap.title = std::move(meta.title);
snap.type = meta.type;
snap.hints.fixed_size = meta.wm_fixed_size;
snap.hints.no_decorations = meta.wm_no_decorations;
Expand All @@ -217,11 +213,35 @@ StartupSnapshot X11Backend::scan_existing_windows() {

LOG_DEBUG(
"scan_existing_windows: candidate %u map_state=%u class='%s' instance='%s' source=%s default_manage=%d",
win, attrs.map_state, cls.c_str(), instance.c_str(),
win, attrs.map_state, snap.wm_class.c_str(), snap.wm_instance.c_str(),
from_restart ? "snapshot" : "scan", default_manage ? 1 : 0);
out.push_back(std::move(snap));
}

LOG_INFO("scan_existing_windows: discovered %d candidate window(s)", (int)out.size());
// Resolve native X input focus up to a top-level (direct child of root).
// Electron/VSCode give input focus to an unmanaged sub-surface; walk the
// parent chain until we land on a window that's a direct child of root.
xcb_window_t focus = xconn.get_input_focus();
if (focus != XCB_WINDOW_NONE && focus != root_window) {
for (int depth = 0; depth < 8; depth++) {
bool is_toplevel = false;
for (auto c : children) {
if (c == focus) {
is_toplevel = true; break;
}
}
if (is_toplevel) {
result.focused_window = focus;
break;
}
auto parent = xconn.query_parent(focus);
if (!parent)
break;
focus = *parent;
}
}

LOG_INFO("scan_existing_windows: discovered %d candidate window(s), focus=%u",
(int)out.size(), (unsigned)result.focused_window);
return result;
}
69 changes: 18 additions & 51 deletions backends/x11/src/backend/events.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -387,9 +387,8 @@ void X11Backend::handle_map_request(xcb_map_request_event_t* ev) {
mapped_window &&
mapped_window->is_visible()) {
(void)core.dispatch(command::atom::FocusWindow{ ev->window });
request_focus(ev->window, kFocusEWMH);
} else {
restore_visible_focus();
core.focus(NO_WINDOW);
}

LOG_DEBUG("MapRequest(%d): parent %d", ev->window, ev->parent);
Expand Down Expand Up @@ -491,10 +490,8 @@ void X11Backend::handle_unmap_notify(xcb_unmap_notify_event_t* ev) {
(void)core.dispatch(command::atom::RemoveWindowFromAllWorkspaces{ ev->window });
ewmh_on_window_unmapped(event::WindowUnmapped{ ev->window, /*withdrawn=*/ true });
runtime.post_event(event::WindowUnmapped{ ev->window, /*withdrawn=*/ true });
if (ws_visible) {
if (ws_visible)
(void)core.dispatch(command::atom::ReconcileNow{});
restore_visible_focus();
}
LOG_DEBUG("UnmapNotify(%d): borderless client withdrawal, unmanaging", ev->window);
return;
}
Expand All @@ -510,10 +507,8 @@ void X11Backend::handle_unmap_notify(xcb_unmap_notify_event_t* ev) {
(void)core.dispatch(command::atom::RemoveWindowFromAllWorkspaces{ ev->window });
ewmh_on_window_unmapped(event::WindowUnmapped{ ev->window, /*withdrawn=*/ true });
runtime.post_event(event::WindowUnmapped{ ev->window, /*withdrawn=*/ true });
if (ws_visible) {
if (ws_visible)
(void)core.dispatch(command::atom::ReconcileNow{});
restore_visible_focus();
}

LOG_DEBUG("UnmapNotify(%d): client withdrawal, unmanaging", ev->window);
}
Expand Down Expand Up @@ -544,10 +539,8 @@ void X11Backend::handle_destroy_notify(xcb_destroy_notify_event_t* ev) {
runtime.post_event(event::WindowUnmapped{ ev->window, /*withdrawn=*/ true });
ewmh_update_client_list();

if (ws_visible) {
if (ws_visible)
(void)core.dispatch(command::atom::ReconcileNow{});
restore_visible_focus();
}

LOG_DEBUG("DestroyNotify(%d)", ev->window);
}
Expand Down Expand Up @@ -903,40 +896,15 @@ void X11Backend::handle_focus_event(xcb_focus_in_event_t* ev) {
return;
}

// Pure theft-protection (mirrors dwm's focusin(), dwm.c:814). Core owns
// the "who is focused" decision — this path ONLY re-asserts X focus when
// the X server disagrees with core. Never writes core state from FocusIn:
// that inversion is what the focus refactor deleted.
if (ev->event == root_window)
return;

// dwm-style focusin: if a window stole focus from our selection via an
// indirect/synthetic route (NotifyWhileGrabbed, NotifyPointerRoot, etc.),
// reassert focus. Only act on NotifyNormal/NotifyWhileGrabbed and only
// when the event is not from a pointer crossing (detail != NotifyInferior).
// Do NOT reassert for NotifyPointer/NotifyVirtual — those are legitimate
// focus changes initiated by us or the user.
auto sel = core.focused_window_state();
if (sel && ev->event != sel->id &&
(ev->detail != XCB_NOTIFY_DETAIL_POINTER &&
ev->detail != XCB_NOTIFY_DETAIL_POINTER_ROOT &&
ev->detail != XCB_NOTIFY_DETAIL_NONE) &&
ev->mode == XCB_NOTIFY_MODE_WHILE_GRABBED) {
if (sel && ev->event != sel->id)
xconn.focus_window(sel->id);
return;
}

auto window = core.window_state_any(ev->event);
if (!window || !window->is_visible())
return;

// Sync internal focus state only — do NOT call xconn.focus_window() here.
// Calling xcb_set_input_focus in response to a FocusIn event creates a
// ping-pong loop: our set_input_focus → X sends FocusOut(A)+FocusIn(B) →
// we call set_input_focus again → FocusOut(B)+FocusIn(A) → ... This
// causes thousands of FocusIn/FocusOut events per second and makes the
// focused application (e.g. VSCode with multiple managed child windows)
// freeze: it keeps receiving FocusIn/FocusOut and cannot process input.
// The actual X focus has already been set by whichever path triggered this
// FocusIn (EnterNotify, button press, EWMH, keybinding). Here we only need
// to keep the WM's internal focused-window pointer in sync.
(void)core.dispatch(command::atom::FocusWindow{ ev->event });
}

void X11Backend::handle_button_event(xcb_button_press_event_t* ev) {
Expand All @@ -952,6 +920,12 @@ void X11Backend::handle_button_event(xcb_button_press_event_t* ev) {

void X11Backend::handle_motion_notify(xcb_motion_notify_event_t* ev) {
last_pointer_ = { ev->root_x, ev->root_y };
// Follow pointer across monitors even when it's over empty root area
// (between windows, on bar strips, between monitors). EnterNotify alone
// isn't enough — it only fires on window boundaries, so crossing via
// root would leave focused_monitor stale until the pointer hits a window.
if (ev->event == root_window)
core.focus_monitor_at_point(ev->root_x, ev->root_y);
runtime.post_event(event::MotionEv{ ev->event, { ev->root_x, ev->root_y }, ev->state });
}

Expand Down Expand Up @@ -1007,21 +981,14 @@ void X11Backend::handle_enter_notify(xcb_enter_notify_event_t* ev) {
last_event_time_ = ev->time;
last_pointer_ = { ev->root_x, ev->root_y };

// Use window_state_any so that windows on the second monitor's active
// workspace are found even when focused_monitor hasn't been updated yet
// (focused_monitor is only updated on button press / motion, not on enter).
auto window = core.window_state_any(ev->event);
if (!window || !window->is_visible())
return;

// Keep focused_monitor in sync so subsequent workspace/layout ops target
// the correct monitor without requiring a click first.
core.focus_monitor_at_point(ev->root_x, ev->root_y);

// Route pointer-enter focus through the single source of truth.
// Focusing a window on another monitor updates focused_monitor_ as
// part of the focus intent, so no separate focus_monitor_at_point call.
(void)core.dispatch(command::atom::FocusWindow{ ev->event });
// Request focus at kPointer priority — applied after apply_core_backend_effects()
// so pointer always wins over stale workspace-switch FocusWindow effects.
request_focus(ev->event, kFocusPointer);
}

// TODO: temporary adapter. The cleaner end state is for X11Backend itself to
Expand Down
11 changes: 0 additions & 11 deletions backends/x11/src/backend/ewmh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,6 @@ void X11Backend::set_border_color(WindowId win, uint32_t pixel) {
xw->set_border_color(pixel);
}

void X11Backend::restore_visible_focus() {
if (auto focused = core.focused_window_state(); focused && focused->is_visible()) {
request_focus(focused->id, kFocusWorkspace);
} else {
// No visible focused window — fall back to root immediately (no arbiter needed).
xconn.focus_window(root_window);
core.emit_focus_changed(NO_WINDOW);
}
}

void X11Backend::ewmh_intern_atoms() {
ewmh_atoms_.resolve(xconn.raw());
}
Expand Down Expand Up @@ -448,7 +438,6 @@ bool X11Backend::handle(event::ClientMessageEv ev) {
return true;

(void)core.dispatch(command::atom::FocusWindow{ ev.window });
request_focus(ev.window, kFocusEWMH);
return true;
}

Expand Down
Loading
Loading