Skip to content

feat(ui): add surface selector for multi-surface input routing#64

Merged
K1ngst0m merged 4 commits intomainfrom
dev/surface-selector
Jan 24, 2026
Merged

feat(ui): add surface selector for multi-surface input routing#64
K1ngst0m merged 4 commits intomainfrom
dev/surface-selector

Conversation

@K1ngst0m
Copy link
Copy Markdown
Collaborator

@K1ngst0m K1ngst0m commented Jan 23, 2026

User description

  • Add F4 toggle for ImGui surface selector window
  • Display surface list with ID, title, dimensions, and input target indicator
  • Support click-to-select for manual input targeting
  • Add "Reset to Auto" button to revert to automatic selection
  • Query X11 WM_NAME/WM_CLASS for XWayland surface metadata

PR Type

Enhancement


Description

  • Add surface enumeration API with metadata (ID, title, class, dimensions)

  • Implement manual input target selection with auto-fallback behavior

  • Create ImGui surface selector window (F4 toggle) for click-to-select routing

  • Refactor input event handling to support manual override via get_input_target()

  • Track XWayland and XDG surfaces with unique IDs and X11 property queries


Diagram Walkthrough

flowchart LR
  A["User presses F4"] --> B["Toggle surface selector window"]
  B --> C["Display surface list with metadata"]
  C --> D["User clicks surface"]
  D --> E["set_input_target called"]
  E --> F["Manual override active"]
  F --> G["Input routed to selected surface"]
  H["User clicks Reset to Auto"] --> I["clear_input_override called"]
  I --> J["Auto-select first surface"]
  J --> G
  K["Surface disconnects"] --> L["Manual target cleared if matched"]
  L --> J
Loading

File Walkthrough

Relevant files
Enhancement
7 files
application.cpp
Add F4 handler and surface polling integration                     
+17/-1   
compositor_server.hpp
Add SurfaceInfo struct and surface enumeration API             
+19/-0   
compositor_server.cpp
Implement surface tracking, manual override, and input target
resolution
+178/-60
input_forwarder.hpp
Expose surface enumeration and selection methods                 
+9/-0     
input_forwarder.cpp
Forward surface API calls to compositor server                     
+12/-0   
imgui_layer.hpp
Add surface selector state and callback types                       
+23/-0   
imgui_layer.cpp
Implement surface selector window with click-to-select UI
+87/-0   
Documentation
3 files
proposal.md
Document design rationale and implementation approach       
+51/-0   
spec.md
Define surface enumeration and manual selection requirements
+136/-0 
tasks.md
Track implementation tasks and validation checklist           
+68/-0   

Summary by CodeRabbit

  • New Features

    • Surface Selector UI (toggle F4) lists connected surfaces with titles, classes, sizes and focus indicators; click-to-select and "Reset to Auto" restore automatic routing.
    • Manual input routing: select a surface as input target; clear override to resume automatic behavior.
  • Public API

    • Surface enumeration and manual-target controls exposed to query surfaces, set/clear a manual input target, and check override state.
  • Tests

    • Added manual surface-selector test executables for X11 and Wayland.

✏️ Tip: You can customize this high-level summary in your review settings.

- Add F4 toggle for ImGui surface selector window
- Display surface list with ID, title, dimensions, and input target indicator
- Support click-to-select for manual input targeting
- Add "Reset to Auto" button to revert to automatic selection
- Query X11 WM_NAME/WM_CLASS for XWayland surface metadata
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 23, 2026

📝 Walkthrough

Walkthrough

Adds multi-surface input management: track surfaces with SurfaceInfo and unique IDs; CompositorServer enumerates surfaces and supports manual input-target override; InputForwarder exposes these APIs; ImGui adds a toggleable Surface Selector (F4) and Application wires UI callbacks to forwarding APIs.

Changes

Cohort / File(s) Summary
Surface Metadata & Core API
src/input/compositor_server.hpp, src/input/compositor_server.cpp
Add SurfaceInfo, per-surface ID/metadata tracking, mutexed hooks lists, get_surfaces(), is_manual_override_active(), set_input_target(uint32_t), clear_input_override(), and input-target resolution logic used by input event paths.
Input Forwarder API Exposure
src/input/input_forwarder.hpp, src/input/input_forwarder.cpp
Forwarder delegates: get_surfaces(), is_manual_override_active(), set_input_target(uint32_t), clear_input_override() to CompositorServer.
ImGui Surface Selector UI
src/ui/imgui_layer.hpp, src/ui/imgui_layer.cpp
New ImGui APIs/state: toggle visibility, set_surfaces(...), set_manual_override_active(...), set_surface_select_callback(...), set_surface_reset_callback(...), and draw_surface_selector() rendering (list, tooltips, select/reset).
Application Integration
src/app/application.cpp
Register surface-select/reset callbacks with ImGui layer, handle F4 to toggle selector, and poll/update ImGui with surfaces when selector visible and input forwarding active.
Specs, Proposal & Tasks
openspec/changes/add-surface-selector/specs/..., openspec/changes/add-surface-selector/*
Add proposal, spec, and task docs describing SurfaceInfo schema, Wayland/XWayland tracking rules, UI scenarios, manual-override semantics, and implementation steps.
Tests / Manual Test Apps
tests/CMakeLists.txt, tests/input/manual_surface_selector_*.cpp
Add two manual test executables (X11 and Wayland) that create multiple SDL/Vulkan windows and log input events for surface-selector validation.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant ImGui as ImGuiLayer
    participant Forwarder as InputForwarder
    participant Server as CompositorServer

    Note over App,ImGui: F4 toggles selector
    App->>ImGui: toggle_surface_selector()
    ImGui-->>ImGui: m_surface_selector_visible = true

    Note over App,Server: Poll surfaces for UI
    App->>Forwarder: get_surfaces()
    Forwarder->>Server: get_surfaces()
    Server-->>Forwarder: list<SurfaceInfo>
    Forwarder-->>App: list<SurfaceInfo>
    App->>ImGui: set_surfaces(list)

    Note over ImGui,App: User selects surface
    ImGui->>App: on_surface_select(surface_id)
    App->>Forwarder: set_input_target(surface_id)
    Forwarder->>Server: set_input_target(surface_id)
    Server-->>Server: manual_input_target = surface_id

    Note over ImGui,App: User resets to auto
    ImGui->>App: on_surface_reset()
    App->>Forwarder: clear_input_override()
    Forwarder->>Server: clear_input_override()
    Server-->>Server: manual_input_target cleared
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Poem

🐰 I counted windows, tiny and tall,
F4 unveils each frame on the wall.
Click a name and I'll steer your keys,
Or Reset to Auto on gentle breeze.
A hopping rabbit routes input with glee.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main feature: adding a surface selector UI for multi-surface input routing, which aligns directly with the primary objective of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Jan 23, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Concurrency data race

Description: manual_input_target, xdg_hooks, and xwayland_hooks are read/written from public methods
(get_surfaces(), set_input_target(), clear_input_override(), and get_input_target())
without any visible synchronization, which can become a cross-thread data race/UB if these
APIs are called off the compositor thread (e.g., UI thread calling get_surfaces() while
the compositor thread mutates hook vectors), potentially enabling a crash/DoS.
compositor_server.cpp [1095-1196]

Referred Code
auto CompositorServer::Impl::get_input_target() -> InputTarget {
    InputTarget target{};

    if (manual_input_target) {
        for (const auto* hooks : xwayland_hooks) {
            if (hooks->id == *manual_input_target && hooks->xsurface && hooks->xsurface->surface) {
                target.xsurface = hooks->xsurface;
                target.surface = hooks->xsurface->surface;
                return target;
            }
        }
        for (const auto* hooks : xdg_hooks) {
            if (hooks->id == *manual_input_target && hooks->surface) {
                target.surface = hooks->surface;
                return target;
            }
        }
        // Manual target not found (surface may have been destroyed), fall through to auto
    }

    if (focused_xsurface && focused_xsurface->surface) {


 ... (clipped 81 lines)
Use-after-free callback

Description: The lambdas registered via m_imgui_layer->set_surface_select_callback and
set_surface_reset_callback capture a raw pointer from m_input_forwarder.get(), so if the
InputForwarder is destroyed or reset while the ImGui layer still holds/invokes these
callbacks it can trigger a use-after-free crash (potential DoS).
application.cpp [196-203]

Referred Code
app->m_imgui_layer->set_surface_select_callback(
    [forwarder = app->m_input_forwarder.get()](uint32_t surface_id) {
        forwarder->set_input_target(surface_id);
    });
app->m_imgui_layer->set_surface_reset_callback(
    [forwarder = app->m_input_forwarder.get()]() {
        forwarder->clear_input_override();
    });
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status:
Misleading boolean name: The variable has_manual_override is set based on is_input_target which is also true in
auto mode, making the name misleading and the UI mode label incorrect.

Referred Code
// Check if any surface has is_input_target true without manual override
bool has_manual_override = false;
for (const auto& s : m_surfaces) {
    if (s.is_input_target) {
        has_manual_override = true;
        break;
    }
}
// This is a simplification - we check if any is marked as target
// The actual "auto" vs "manual" is tracked in CompositorServer

ImGui::Text("Mode: %s",
            has_manual_override ? "Manual selection" : "Auto (first surface)");
ImGui::Separator();

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Potential stale pointers: get_input_target() and get_surfaces() iterate xwayland_hooks and dereference
hooks->xsurface even though XWayland lifetimes may be unmanaged (no destroy listener),
risking use-after-free unless guaranteed elsewhere.

Referred Code
auto CompositorServer::Impl::get_input_target() -> InputTarget {
    InputTarget target{};

    if (manual_input_target) {
        for (const auto* hooks : xwayland_hooks) {
            if (hooks->id == *manual_input_target && hooks->xsurface && hooks->xsurface->surface) {
                target.xsurface = hooks->xsurface;
                target.surface = hooks->xsurface->surface;
                return target;
            }
        }
        for (const auto* hooks : xdg_hooks) {
            if (hooks->id == *manual_input_target && hooks->surface) {
                target.surface = hooks->surface;
                return target;
            }
        }
        // Manual target not found (surface may have been destroyed), fall through to auto
    }

    if (focused_xsurface && focused_xsurface->surface) {


 ... (clipped 54 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Environment info logged: The new log line prints DISPLAY and WAYLAND_DISPLAY, which may be considered sensitive
environment/context data depending on deployment and log exposure policies.

Referred Code
GOGGLES_LOG_INFO("Input forwarding: DISPLAY={} WAYLAND_DISPLAY={}",
                 app->m_input_forwarder->x11_display(),
                 app->m_input_forwarder->wayland_display());

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Jan 23, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Unify input target selection logic

The logic for determining the automatic input target is inconsistent between
get_surfaces() (for UI) and get_input_target() (for input routing), which can
lead to desynchronization. This should be unified into a single, authoritative
function to ensure the UI always reflects the actual input target.

Examples:

src/input/compositor_server.cpp [1095-1123]
auto CompositorServer::Impl::get_input_target() -> InputTarget {
    InputTarget target{};

    if (manual_input_target) {
        for (const auto* hooks : xwayland_hooks) {
            if (hooks->id == *manual_input_target && hooks->xsurface && hooks->xsurface->surface) {
                target.xsurface = hooks->xsurface;
                target.surface = hooks->xsurface->surface;
                return target;
            }

 ... (clipped 19 lines)
src/input/compositor_server.cpp [1125-1171]
auto CompositorServer::get_surfaces() const -> std::vector<SurfaceInfo> {
    std::vector<SurfaceInfo> result;

    uint32_t target_id = 0;
    if (m_impl->manual_input_target) {
        target_id = *m_impl->manual_input_target;
    } else {
        // Auto-select: first XWayland surface if available, else first XDG surface
        if (!m_impl->xwayland_hooks.empty()) {
            target_id = m_impl->xwayland_hooks.front()->id;

 ... (clipped 37 lines)

Solution Walkthrough:

Before:

// For UI display in get_surfaces()
function get_auto_target_for_ui():
    // Prefers first XWayland surface, then first XDG surface
    if xwayland_surfaces exist:
        return first xwayland_surface.id
    else if xdg_surfaces exist:
        return first xdg_surface.id
    return no_target

// For actual input routing in get_input_target()
function get_auto_target_for_input():
    // Uses whatever surface was last focused
    if focused_xwayland_surface exists:
        return focused_xwayland_surface
    else if focused_xdg_surface exists:
        return focused_xdg_surface
    return no_target

After:

// Unified function to get the current input target
function get_current_input_target():
    if manual_target_id is set and valid:
        return find_surface_by_id(manual_target_id)

    // "Auto" mode logic is now centralized
    if focused_xwayland_surface exists:
        return focused_xwayland_surface
    else if focused_xdg_surface exists:
        return focused_xdg_surface
    
    // Fallback if no focus (e.g., first surface)
    if xwayland_surfaces exist:
        return first xwayland_surface
    ...
    return no_target

// Both UI and input routing use the unified function
get_surfaces() uses get_current_input_target()
get_input_target() uses get_current_input_target()
Suggestion importance[1-10]: 9

__

Why: This suggestion correctly identifies a critical bug where the UI can display a different input target than the one actually receiving input, due to divergent logic in get_surfaces and get_input_target.

High
Possible issue
Fix memory leak on surface destroy

Fix a memory leak in handle_xwayland_surface_destroy by deleting the
XWaylandSurfaceHooks object before removing it from the xwayland_hooks vector.

src/input/compositor_server.cpp [937-950]

 void CompositorServer::Impl::handle_xwayland_surface_destroy(wlr_xwayland_surface* xsurface) {
     auto hook_it =
         std::find_if(xwayland_hooks.begin(), xwayland_hooks.end(),
                      [xsurface](const XWaylandSurfaceHooks* h) { return h->xsurface == xsurface; });
     if (hook_it != xwayland_hooks.end()) {
         if (manual_input_target && *manual_input_target == (*hook_it)->id) {
             manual_input_target.reset();
         }
+        delete *hook_it;
         xwayland_hooks.erase(hook_it);
     }
 
     if (focused_xsurface != xsurface) {
         return;
     }
 ...

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a memory leak where XWaylandSurfaceHooks objects are not deallocated upon surface destruction, which is a critical issue.

High
Prevent crash from invalid surface access

Prevent a potential crash in get_surfaces by iterating to find the first valid
surface for auto-selection, instead of assuming front() is always valid.

src/input/compositor_server.cpp [1125-1138]

 auto CompositorServer::get_surfaces() const -> std::vector<SurfaceInfo> {
     std::vector<SurfaceInfo> result;
 
     uint32_t target_id = 0;
     if (m_impl->manual_input_target) {
         target_id = *m_impl->manual_input_target;
     } else {
-        // Auto-select: first XWayland surface if available, else first XDG surface
-        if (!m_impl->xwayland_hooks.empty()) {
-            target_id = m_impl->xwayland_hooks.front()->id;
-        } else if (!m_impl->xdg_hooks.empty()) {
-            target_id = m_impl->xdg_hooks.front()->id;
+        // Auto-select: first valid XWayland surface if available, else first valid XDG surface
+        auto xwayland_it = std::find_if(m_impl->xwayland_hooks.begin(), m_impl->xwayland_hooks.end(),
+                                        [](const auto* h) { return h->xsurface && h->xsurface->surface; });
+        if (xwayland_it != m_impl->xwayland_hooks.end()) {
+            target_id = (*xwayland_it)->id;
+        } else {
+            auto xdg_it = std::find_if(m_impl->xdg_hooks.begin(), m_impl->xdg_hooks.end(),
+                                       [](const auto* h) { return h->surface; });
+            if (xdg_it != m_impl->xdg_hooks.end()) {
+                target_id = (*xdg_it)->id;
+            }
         }
     }
 ...

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential null pointer dereference that could crash the application when determining the auto-selected input target, which is a significant correctness issue.

Medium
Clear stale manual override

When a manually selected input target is not found in get_input_target, clear
the stale manual_input_target to prevent repeated lookups.

src/input/compositor_server.cpp [1098-1113]

 if (manual_input_target) {
     for (const auto* hooks : xwayland_hooks) {
         if (hooks->id == *manual_input_target && hooks->xsurface && hooks->xsurface->surface) {
             target.xsurface = hooks->xsurface;
             target.surface = hooks->xsurface->surface;
             return target;
         }
     }
     for (const auto* hooks : xdg_hooks) {
         if (hooks->id == *manual_input_target && hooks->surface) {
             target.surface = hooks->surface;
             return target;
         }
     }
-    // Manual target not found (surface may have been destroyed), fall through to auto
+    // Manual target not found, clear stale override and fall back to auto
+    manual_input_target.reset();
 }
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a performance issue where the code repeatedly searches for a destroyed manual target. Clearing the stale ID improves efficiency by avoiding this redundant work.

Low
General
Correctly detect manual override
Suggestion Impact:The commit removes the incorrect "any is_input_target means manual" logic and replaces the UI mode display with an explicit manual-override flag (m_manual_override_active) that can be set externally. While it does not implement the suggested ID-comparison approach, it addresses the same underlying issue (correctly reflecting manual vs auto mode) by using a dedicated state instead.

code diff:

+void ImGuiLayer::set_manual_override_active(bool active) {
+    m_manual_override_active = active;
+}
+
 void ImGuiLayer::set_surface_select_callback(SurfaceSelectCallback callback) {
     m_on_surface_select = std::move(callback);
 }
@@ -544,19 +548,7 @@
         if (m_surfaces.empty()) {
             ImGui::TextDisabled("No surfaces connected");
         } else {
-            // Check if any surface has is_input_target true without manual override
-            bool has_manual_override = false;
-            for (const auto& s : m_surfaces) {
-                if (s.is_input_target) {
-                    has_manual_override = true;
-                    break;
-                }
-            }
-            // This is a simplification - we check if any is marked as target
-            // The actual "auto" vs "manual" is tracked in CompositorServer
-
-            ImGui::Text("Mode: %s",
-                        has_manual_override ? "Manual selection" : "Auto (first surface)");
+            ImGui::Text("Mode: %s", m_manual_override_active ? "Manual" : "Auto");
             ImGui::Separator();

Correctly detect manual override in the UI by comparing the target surface ID
with the first surface's ID.

src/ui/imgui_layer.cpp [547-559]

-// Check if any surface has is_input_target true without manual override
-bool has_manual_override = false;
-for (const auto& s : m_surfaces) {
-    if (s.is_input_target) {
-        has_manual_override = true;
-        break;
+// Determine manual override by comparing target with first surface
+uint32_t target_id = 0;
+if (!m_surfaces.empty()) {
+    for (const auto& s : m_surfaces) {
+        if (s.is_input_target) { target_id = s.id; break; }
     }
 }
-// This is a simplification - we check if any is marked as target
-// The actual "auto" vs "manual" is tracked in CompositorServer
-
+uint32_t first_id = m_surfaces.empty() ? 0u : m_surfaces.front().id;
+bool has_manual_override = target_id != 0 && target_id != first_id;
 ImGui::Text("Mode: %s",
             has_manual_override ? "Manual selection" : "Auto (first surface)");

[Suggestion processed]

Suggestion importance[1-10]: 5

__

Why: The suggestion fixes incorrect logic for displaying the input selection mode, improving the UI's accuracy, although the author noted the original implementation was a simplification.

Low
Warn on invalid surface ID

Add a warning log in set_input_target when the provided surface_id does not
correspond to any known surface to improve debugging.

src/input/compositor_server.cpp [1173-1192]

 void CompositorServer::set_input_target(uint32_t surface_id) {
     bool found = false;
     for (const auto* hooks : m_impl->xwayland_hooks) {
         if (hooks->id == surface_id) {
             found = true;
             break;
         }
     }
     if (!found) {
         for (const auto* hooks : m_impl->xdg_hooks) {
             if (hooks->id == surface_id) {
                 found = true;
                 break;
             }
         }
     }
     if (found) {
         m_impl->manual_input_target = surface_id;
+    } else {
+        GOGGLES_LOG_WARN("set_input_target: surface {} not found", surface_id);
     }
 }
  • Apply / Chat
Suggestion importance[1-10]: 4

__

Why: This suggestion improves debuggability by adding a log for an invalid operation. While helpful, it's a minor enhancement rather than a fix for a bug in existing functionality.

Low
Reserve vector capacity

In get_surfaces, reserve capacity for the result vector to avoid reallocations,
slightly improving performance.

src/input/compositor_server.cpp [1125-1126]

 auto CompositorServer::get_surfaces() const -> std::vector<SurfaceInfo> {
     std::vector<SurfaceInfo> result;
-}
+    result.reserve(m_impl->xwayland_hooks.size() + m_impl->xdg_hooks.size());

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 3

__

Why: This is a valid micro-optimization that can prevent vector reallocations, improving performance slightly in a function that may be called frequently.

Low
  • Update

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/input/compositor_server.cpp (1)

620-639: Manual targeting doesn't update pointer-constraint state.

When set_input_target() changes the input target to a different surface, focus_surface() is not called, leaving active_constraint pointing to the previous surface's constraint. This causes handle_pointer_motion_event() to use stale constraint state: if the previous surface had a locked constraint, the new target won't receive absolute motion updates, and any constraint on the new surface won't be recognized or activated.

Call focus_surface() when manual targeting changes, or explicitly query and activate the constraint for the new target surface using wlr_pointer_constraints_v1_constraint_for_surface().

🤖 Fix all issues with AI agents
In `@openspec/changes/add-surface-selector/proposal.md`:
- Around line 7-15: Update the "What Changes" section to explicitly list spec
deltas using ADDED/MODIFIED notation: mark SurfaceInfo struct as ADDED, the
CompositorServer tracking of connected surfaces and the get_surfaces() API as
MODIFIED/ADDED as appropriate, and mark set_input_target(id) and
clear_input_override() as ADDED (and note the ImGui F4 surface selector UI as
ADDED); include a short line describing each delta (e.g., "ADDED: SurfaceInfo
struct (id, title, class, dimensions, type)"; "ADDED: CompositorServer surface
tracking and get_surfaces() API"; "ADDED: set_input_target(id) /
clear_input_override() APIs"; "ADDED: ImGui F4 surface list UI with
click-to-select and auto/manual indicator") so the proposal conforms to the
required format.

In `@openspec/changes/add-surface-selector/specs/input-forwarding/spec.md`:
- Around line 98-109: The wording is contradictory: it says the system tracks
both xdg_shell and XWayland surfaces but then specifies a "unified list …
(Wayland surfaces only)"; update the spec to clearly state whether there's a
single unified active-surface list including both xdg_shell and XWayland or two
separate trackers (e.g., a unified active_surfaces list for Wayland-native
surfaces and a separate xwayland_windows registry for XWayland metadata), and
clarify behaviors tied to each (e.g., listen for new_toplevel and new_surface,
register destroy listeners only for Wayland surfaces, assign unique IDs on
creation, query WM_NAME/WM_CLASS for XWayland entries, and apply manual
target/auto-focus and mutual exclusion semantics consistently across the two
trackers).

In `@openspec/changes/add-surface-selector/tasks.md`:
- Around line 64-68: The checklist item "9.5 Manual test with Steam: verify
overlay surfaces appear when invoked" is still unchecked — either perform that
Steam overlay manual test and mark item 9.5 done in tasks.md (verifying overlay
surfaces appear and behave correctly), or create a follow-up issue referencing
this test gap (mentioning "9.5 Manual test with Steam" and expected verification
steps) and then update tasks.md to link the new issue and mark the checklist
entry as deferred; ensure the change touches the "9.5 Manual test with Steam"
line so reviewers see the resolution.

In `@src/input/compositor_server.cpp`:
- Around line 1095-1195: The UI-thread/data-race issue: protect access to
xwayland_hooks, xdg_hooks and manual_input_target by adding a std::mutex (e.g.
hooks_mutex) to CompositorServer::Impl and take a std::lock_guard<std::mutex>
(or std::scoped_lock) around all reads/writes in get_surfaces(),
set_input_target(), clear_input_override() and in all mutation points
(handle_new_*, handle_*_destroy and any place that pushes/pops into
xwayland_hooks/xdg_hooks); alternatively marshal these calls to the compositor
thread using the existing compositor task queue—but do not leave those
containers unguarded across threads. Ensure every access uses the same mutex
name so the lock consistently prevents UAFs/races.

In `@src/ui/imgui_layer.cpp`:
- Around line 547-560: The mode label is wrong because m_surfaces’
is_input_target is set for the active surface in both auto and manual cases;
update the UI to rely on an explicit manual flag from the compositor instead of
scanning is_input_target. Add or expose a bool like
SurfaceInfo::is_manual_override (or a CompositorServer getter that returns
whether input selection is manual) and use that flag when computing the mode
label and enabling/disabling the "Reset to Auto" button (replace the
has_manual_override check in the block using m_surfaces and the ImGui::Text call
so the UI reflects the compositor-provided is_manual_override).

Comment on lines +7 to +15
## What Changes

- Add `SurfaceInfo` struct to expose surface metadata (id, title, class, dimensions, type)
- Track all connected surfaces with unique IDs in CompositorServer
- Add `get_surfaces()` API to enumerate connected surfaces
- Add `set_input_target(id)` and `clear_input_override()` for manual selection
- Add new ImGui window (F4) displaying surface list with click-to-select
- Show current input target with "(auto)" or "(manual)" indicator

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add explicit spec deltas under “What Changes.”

The section lists features but doesn’t explicitly enumerate the spec deltas (ADDED/MODIFIED), which the proposal format requires.

✏️ Suggested addition
 ## What Changes
 
+- Spec deltas:
+  - ADDED Requirements: Surface Enumeration; Manual Input Target Selection; Surface Selector UI
+  - MODIFIED Requirements: Surface Tracking
 - Add `SurfaceInfo` struct to expose surface metadata (id, title, class, dimensions, type)
 - Track all connected surfaces with unique IDs in CompositorServer
 - Add `get_surfaces()` API to enumerate connected surfaces
 - Add `set_input_target(id)` and `clear_input_override()` for manual selection
 - Add new ImGui window (F4) displaying surface list with click-to-select
 - Show current input target with "(auto)" or "(manual)" indicator

As per coding guidelines, please list spec deltas in the “What Changes” section.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## What Changes
- Add `SurfaceInfo` struct to expose surface metadata (id, title, class, dimensions, type)
- Track all connected surfaces with unique IDs in CompositorServer
- Add `get_surfaces()` API to enumerate connected surfaces
- Add `set_input_target(id)` and `clear_input_override()` for manual selection
- Add new ImGui window (F4) displaying surface list with click-to-select
- Show current input target with "(auto)" or "(manual)" indicator
## What Changes
- Spec deltas:
- ADDED Requirements: Surface Enumeration; Manual Input Target Selection; Surface Selector UI
- MODIFIED Requirements: Surface Tracking
- Add `SurfaceInfo` struct to expose surface metadata (id, title, class, dimensions, type)
- Track all connected surfaces with unique IDs in CompositorServer
- Add `get_surfaces()` API to enumerate connected surfaces
- Add `set_input_target(id)` and `clear_input_override()` for manual selection
- Add new ImGui window (F4) displaying surface list with click-to-select
- Show current input target with "(auto)" or "(manual)" indicator
🤖 Prompt for AI Agents
In `@openspec/changes/add-surface-selector/proposal.md` around lines 7 - 15,
Update the "What Changes" section to explicitly list spec deltas using
ADDED/MODIFIED notation: mark SurfaceInfo struct as ADDED, the CompositorServer
tracking of connected surfaces and the get_surfaces() API as MODIFIED/ADDED as
appropriate, and mark set_input_target(id) and clear_input_override() as ADDED
(and note the ImGui F4 surface selector UI as ADDED); include a short line
describing each delta (e.g., "ADDED: SurfaceInfo struct (id, title, class,
dimensions, type)"; "ADDED: CompositorServer surface tracking and get_surfaces()
API"; "ADDED: set_input_target(id) / clear_input_override() APIs"; "ADDED: ImGui
F4 surface list UI with click-to-select and auto/manual indicator") so the
proposal conforms to the required format.

Comment on lines +98 to +109
The system SHALL track surfaces from both xdg_shell (Wayland native) and XWayland clients.

The compositor server SHALL:
- Listen for `new_toplevel` signals from xdg_shell
- Listen for `new_surface` signals from XWayland
- Maintain a unified list of active surfaces (Wayland surfaces only)
- Register destroy listeners for Wayland surfaces only
- Assign a unique ID to each surface on creation
- Query X11 window properties (`WM_NAME`, `WM_CLASS`) for XWayland surfaces
- Use manual target if set, otherwise auto-focus the first connected surface
- Use mutual exclusion to manage focus between Wayland and XWayland surfaces

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify the surface list wording to avoid contradiction.

Line 103 says “unified list … (Wayland surfaces only)” which conflicts with tracking both Wayland and XWayland. Consider clarifying the Wayland list vs. XWayland tracking split.

✏️ Suggested wording fix
- - Maintain a unified list of active surfaces (Wayland surfaces only)
+ - Maintain a unified list of active Wayland surfaces (xdg_shell)
+ - Track active XWayland surfaces separately from the Wayland list
🤖 Prompt for AI Agents
In `@openspec/changes/add-surface-selector/specs/input-forwarding/spec.md` around
lines 98 - 109, The wording is contradictory: it says the system tracks both
xdg_shell and XWayland surfaces but then specifies a "unified list … (Wayland
surfaces only)"; update the spec to clearly state whether there's a single
unified active-surface list including both xdg_shell and XWayland or two
separate trackers (e.g., a unified active_surfaces list for Wayland-native
surfaces and a separate xwayland_windows registry for XWayland metadata), and
clarify behaviors tied to each (e.g., listen for new_toplevel and new_surface,
register destroy listeners only for Wayland surfaces, assign unique IDs on
creation, query WM_NAME/WM_CLASS for XWayland entries, and apply manual
target/auto-focus and mutual exclusion semantics consistently across the two
trackers).

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/input/compositor_server.cpp`:
- Around line 1173-1200: set_input_target currently only sets
m_impl->manual_input_target but leaves m_impl->focused_surface /
m_impl->focused_xsurface and m_impl->active_constraint unchanged, so pointer
lock and motion may still target the old surface; update set_input_target to,
after verifying the surface via xwayland_hooks/xdg_hooks, also update the
compositor's focus state and active constraint to point to that surface (e.g.,
set m_impl->focused_surface or focused_xsurface accordingly and update/clear
m_impl->active_constraint as needed) and ensure the update runs on the
compositor thread / under the same guard used when accessing
xwayland_hooks/xdg_hooks; likewise update clear_input_override to reset
manual_input_target and restore or recompute focus/constraint state (e.g., clear
focused_surface/focused_xsurface or recalc focus based on current pointer/seat)
so pointer lock/motion behavior is consistent.
- Around line 1125-1167: get_surfaces() currently auto-selects the "first"
surface as the input target which can drift from what actual routing uses;
update the auto-selection to derive target_id from the real focused/input target
used by the compositor (e.g. call get_input_target() from within get_surfaces()
or read the implementation's focused ids like m_impl->focused_xwayland /
m_impl->focused_xdg) and use that id for info.is_input_target comparisons so the
UI indicator matches actual focus/routing; preserve manual override via
m_impl->manual_input_target when present.
♻️ Duplicate comments (1)
src/input/compositor_server.cpp (1)

183-190: Guard hook containers and manual target across threads.

get_surfaces/set_input_target/clear_input_override can run on the UI thread while the compositor thread mutates xdg_hooks/xwayland_hooks and manual_input_target. That’s a race/UAF risk—please add synchronization or marshal these operations to the compositor thread.

🔒 Minimal locking sketch
struct CompositorServer::Impl {
+    mutable std::mutex surface_mutex;
     ...
};

auto CompositorServer::get_surfaces() const -> std::vector<SurfaceInfo> {
+    std::scoped_lock lock(m_impl->surface_mutex);
     ...
}

void CompositorServer::set_input_target(uint32_t surface_id) {
+    std::scoped_lock lock(m_impl->surface_mutex);
     ...
}

void CompositorServer::clear_input_override() {
+    std::scoped_lock lock(m_impl->surface_mutex);
     m_impl->manual_input_target.reset();
}

(Also apply the same guard around hook mutations in handle_new_* / handle_*_destroy.)

Comment on lines +1173 to +1200
auto CompositorServer::is_manual_override_active() const -> bool {
return m_impl->manual_input_target.has_value();
}

void CompositorServer::set_input_target(uint32_t surface_id) {
bool found = false;
for (const auto* hooks : m_impl->xwayland_hooks) {
if (hooks->id == surface_id) {
found = true;
break;
}
}
if (!found) {
for (const auto* hooks : m_impl->xdg_hooks) {
if (hooks->id == surface_id) {
found = true;
break;
}
}
}
if (found) {
m_impl->manual_input_target = surface_id;
}
}

void CompositorServer::clear_input_override() {
m_impl->manual_input_target.reset();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Manual override doesn’t update focus/constraint state.

set_input_target only stores an ID; focused_surface/focused_xsurface and active_constraint stay tied to the prior focus. Pointer lock/motion behavior can remain bound to the old surface even after manual selection. Consider updating focus/constraints when manual target changes (ideally on the compositor thread / under the same guard as hook access).

🤖 Prompt for AI Agents
In `@src/input/compositor_server.cpp` around lines 1173 - 1200, set_input_target
currently only sets m_impl->manual_input_target but leaves
m_impl->focused_surface / m_impl->focused_xsurface and m_impl->active_constraint
unchanged, so pointer lock and motion may still target the old surface; update
set_input_target to, after verifying the surface via xwayland_hooks/xdg_hooks,
also update the compositor's focus state and active constraint to point to that
surface (e.g., set m_impl->focused_surface or focused_xsurface accordingly and
update/clear m_impl->active_constraint as needed) and ensure the update runs on
the compositor thread / under the same guard used when accessing
xwayland_hooks/xdg_hooks; likewise update clear_input_override to reset
manual_input_target and restore or recompute focus/constraint state (e.g., clear
focused_surface/focused_xsurface or recalc focus based on current pointer/seat)
so pointer lock/motion behavior is consistent.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/input/compositor_server.cpp (1)

639-642: active_constraint check may apply wrong constraint after manual target switch.

The constraint check here uses the global active_constraint, which is tied to focused_surface rather than the manual input target. If the user manually selects a different surface, pointer motion could be incorrectly constrained (or not constrained when it should be).

This relates to the broader issue that set_input_target() doesn't synchronize constraint state with the new target.

♻️ Duplicate comments (2)
src/input/compositor_server.cpp (2)

1199-1219: Manual override only stores ID without updating compositor focus state.

This method validates and stores the surface ID but doesn't update focused_surface, focused_xsurface, or constraint state. As noted in a previous review, this can cause pointer lock/motion behavior to remain bound to the old surface.

Consider either:

  1. Marshaling focus updates to the compositor thread (e.g., via the event queue)
  2. Documenting this as intentional "soft" targeting that only affects input routing via get_input_target() without full focus transfer

1149-1159: Auto target indicator may not match actual input routing.

When no manual override is active, get_surfaces() marks the first surface as the input target (lines 1154-1158), but get_input_target() routes input to focused_xsurface/focused_surface (lines 1135-1140). This can cause the UI to highlight a different surface than the one actually receiving input.

Consider aligning the auto-selection logic:

🛠️ Suggested fix
     uint32_t target_id = 0;
     if (m_impl->manual_input_target) {
         target_id = *m_impl->manual_input_target;
-    } else {
-        // Auto-select: first XWayland surface if available, else first XDG surface
-        if (!m_impl->xwayland_hooks.empty()) {
-            target_id = m_impl->xwayland_hooks.front()->id;
-        } else if (!m_impl->xdg_hooks.empty()) {
-            target_id = m_impl->xdg_hooks.front()->id;
+    } else if (m_impl->focused_xsurface && m_impl->focused_xsurface->surface) {
+        for (const auto* hooks : m_impl->xwayland_hooks) {
+            if (hooks->xsurface == m_impl->focused_xsurface) {
+                target_id = hooks->id;
+                break;
+            }
+        }
+    } else if (m_impl->focused_surface) {
+        for (const auto* hooks : m_impl->xdg_hooks) {
+            if (hooks->surface == m_impl->focused_surface) {
+                target_id = hooks->id;
+                break;
+            }
         }
     }

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tests/CMakeLists.txt`:
- Around line 131-155: These manual SDL/Vulkan test executables (targets
goggles_manual_surface_selector_x11 and goggles_manual_surface_selector_wayland,
sources input/manual_surface_selector_x11.cpp and
input/manual_surface_selector_wayland.cpp) live under tests/ and violate the
Catch2/non-GPU unit-test policy; move their add_executable and
target_link_libraries blocks out of tests/CMakeLists.txt into a new
CMakeLists.txt under a tools/ or examples/manual-tests directory (or
alternatively remove them from tests/ and document an explicit exception for
tests/input/), and update whichever top-level CMakeLists or project registration
that references these targets so they are built as manual tools/examples rather
than unit tests.
♻️ Duplicate comments (2)
tests/input/manual_surface_selector_wayland.cpp (2)

113-174: Same optional improvement as X11: avoid std::exit for cleaner RAII teardown.


189-239: Same optional improvement as X11: guard unknown window IDs in logs.

🧹 Nitpick comments (2)
tests/input/manual_surface_selector_x11.cpp (2)

113-174: Consider propagating init failures instead of std::exit to keep RAII cleanup.
std::exit skips stack unwinding, so partially created windows/renderers won’t be cleaned up. Returning an error (or std::optional) to main lets destructors run and keeps the test harness-friendly.


189-239: Handle unknown window IDs in logs to avoid “window=0”.
If an event arrives without a matching window ID, window_idx stays -1 and logs 0, which is ambiguous. Consider logging “unknown” or skipping the print.

@K1ngst0m K1ngst0m merged commit b4c1c7e into main Jan 24, 2026
4 checks passed
@K1ngst0m K1ngst0m deleted the dev/surface-selector branch January 24, 2026 03:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant