Skip to content

Conversation

@iparaskev
Copy link
Contributor

@iparaskev iparaskev commented Jan 28, 2026

Fixes an issue where the window was placed in the wrong display in a system with 3 displays.

Summary by CodeRabbit

  • New Features

    • Per-monitor overlay management: overlays created and shown per display for more reliable multi-monitor behavior.
    • Improved overlay show/hide lifecycle for smoother window presentation.
  • Refactoring

    • Centralized window management for consistent fullscreen/positioning across platforms.
    • Graphics initialization updated to use shared window handling for more consistent sizing and surface setup.
  • Tests

    • Added an end-to-end screenshare test that iterates sharing across every monitor.

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

Fixes an issue where the window was placed in the wrong display in a
system with 3 displays.
@netlify
Copy link

netlify bot commented Jan 28, 2026

Deploy Preview for hoppdocs ready!

Name Link
🔨 Latest commit d230b21
🔍 Latest deploy log https://app.netlify.com/projects/hoppdocs/deploys/697aa9d11d4e8f0008faeda7
😎 Deploy Preview https://deploy-preview-241--hoppdocs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

Adds a new cross-platform WindowManager that creates and manages per-monitor overlay windows, refactors Application to delegate window lifecycle and events to it, changes GraphicsContext::new to accept an Arc, and adds tests to iterate screenshare across every monitor.

Changes

Cohort / File(s) Summary
Window Manager Architecture
core/src/window_manager.rs
New WindowManager module with new, show_window, hide_active_window, update methods; manages WindowEntry (Arc<Window> + position); platform-specific fullscreen/positioning helpers; WindowManagerError and From<WindowManagerError> for ServerError.
Application Integration
core/src/lib.rs
Application gains window_manager: Option<window_manager::WindowManager>; resumed lazily initializes it; overlay lifecycle delegated to WindowManager; signatures changed for get_available_content, screenshare, create_overlay_window; window events forward ScaleFactorChanged/Resized/Moved to window_manager.update.
Graphics Context Update
core/src/graphics/graphics_context.rs
GraphicsContext::new signature changed to accept window_arc: Arc<Window> (was Window); internal uses updated to window_arc.inner_size() and consistent Arc usage.
Test Dispatch
core/tests/src/main.rs
Adds ScreenshareTest::EveryMonitor variant and dispatch path to run the new test.
Test Suite
core/tests/src/screenshare_client.rs
Adds test_every_monitor() that enumerates display content, starts a 1920×1080 screenshare per monitor for 10s, stops, and proceeds to next monitor with short delays.

Sequence Diagram(s)

sequenceDiagram
    participant EventLoop as Event Loop
    participant App as Application
    participant WM as WindowManager
    participant Mon as Monitor
    participant Win as Window

    rect rgba(100, 200, 150, 0.5)
    EventLoop->>App: resumed(event_loop)
    App->>WM: WindowManager::new(event_loop)
    WM->>Mon: enumerate monitors
    loop per monitor
      WM->>Win: create overlay window (Arc\<Window\>)
      Win-->>WM: Arc\<Window\>
      WM->>WM: store WindowEntry(window, position)
    end
    WM-->>App: initialized
    end

    rect rgba(150, 150, 200, 0.5)
    App->>WM: show_window(monitor_handle)
    WM->>Win: set_fullscreen & set_visible(true)
    Win-->>WM: visible Arc\<Window\>
    WM-->>App: Arc\<Window\> returned
    App->>App: start screenshare using Arc\<Window\>
    end

    rect rgba(200, 100, 100, 0.5)
    EventLoop->>App: ScaleFactorChanged / Resized / Moved
    App->>WM: update(event_loop)
    WM->>Mon: re-enumerate monitors
    WM->>WM: add/remove windows to match monitors
    WM-->>App: update complete
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • feat: initial drawing support #217 — Modifies core/src/graphics/graphics_context.rs constructor to use Arc<Window>; directly related to the GraphicsContext signature change.

Suggested reviewers

  • konsalex

Poem

🐰 I hop from monitor to monitor with glee,
Arc-wrapped windows shining for all to see,
Event loops hum, the manager keeps pace,
Screenshare per screen — each window finds its place,
A rabbit cheers the code's new grace.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'fix: wrong display position' is vague and generic. While it mentions fixing a display issue, it doesn't convey specific information about the substantial architectural changes made (WindowManager abstraction, GraphicsContext refactoring, per-monitor overlay window management). Consider a more descriptive title that reflects the main architectural change, such as 'refactor: introduce WindowManager for per-monitor overlay window handling' or 'feat: add WindowManager abstraction for display position fixes'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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.

Copy link

@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

Caution

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

⚠️ Outside diff range comments (1)
core/src/lib.rs (1)

222-233: Don’t swallow WindowManager::update errors.

Silent failures can mask monitor topology changes and lead to hard-to-debug window placement issues. Logging (or propagating) the error would help.

🛠️ Proposed fix
-        if let Some(wm) = self.window_manager.as_mut() {
-            let _ = wm.update(event_loop);
-        }
+        if let Some(wm) = self.window_manager.as_mut() {
+            if let Err(e) = wm.update(event_loop) {
+                log::error!("get_available_content: WindowManager update failed: {e:?}");
+            }
+        }
@@
-                if let Some(wm) = self.window_manager.as_mut() {
-                    let _ = wm.update(event_loop);
-                }
+                if let Some(wm) = self.window_manager.as_mut() {
+                    if let Err(e) = wm.update(event_loop) {
+                        log::error!("window_event: WindowManager update failed: {e:?}");
+                    }
+                }

Also applies to: 999-1004

🤖 Fix all issues with AI agents
In `@core/src/window_manager.rs`:
- Around line 121-218: Before removing stale entries in update(), capture the
current active window's monitor_position (if any) from
self.windows[self.active_window_index.unwrap()].monitor_position; then run the
retain as-is; after retain, re-find that monitor_position in the updated
self.windows (using iter().position(|entry| entry.monitor_position ==
saved_pos)) and set self.active_window_index = position_option (or None if not
found). This ensures hide_active_window(), update(), and other logic using
active_window_index remain valid; reference functions/fields: update(),
hide_active_window(), active_window_index, self.windows, and
entry.monitor_position.

In `@core/tests/src/screenshare_client.rs`:
- Around line 170-190: The test test_every_monitor should fail fast when no
display monitors are reported: after computing monitors from
get_available_content (filtering by ContentType::Display), check
monitors.is_empty() and return an io::Error (e.g., io::Error::new or
io::Error::other) with a clear message like "No display monitors available"
instead of allowing the test to continue and print "Success"; update the logic
around the monitors variable in test_every_monitor (and any downstream
success/print paths) so an empty monitors list yields an error rather than a
passing test.
🧹 Nitpick comments (1)
core/src/window_manager.rs (1)

221-281: Consider moving platform-specific fullscreen logic into OS modules.

This file now contains OS-specific fullscreen handling; splitting that into src/**/{linux,macos,windows}.rs would align with the project’s platform-module organization standard. As per coding guidelines, Organize platform-specific modules in src/**/{linux,macos,windows}.rs subdirectories.

Comment on lines +121 to +218
pub fn show_window(
&mut self,
monitor: &MonitorHandle,
) -> Result<Arc<Window>, WindowManagerError> {
let monitor_position = monitor.position();
log::info!(
"WindowManager::show_window: looking for window at {:?}",
monitor_position
);

for entry in &self.windows {
log::info!(
"WindowManager::show_window: display {:?} window {:?}",
entry.monitor_position,
entry.window.outer_position()
);
}
let index = self
.windows
.iter()
.position(|entry| entry.monitor_position == monitor_position)
.ok_or(WindowManagerError::MonitorNotFound)?;

let window = &self.windows[index].window;

log::info!("window fullscreen {:?}", window.fullscreen());
if let Err(e) = set_fullscreen(window, monitor.clone()) {
log::error!(
"WindowManager::show_window: error setting fullscreen: {:?}",
e
);
return Err(WindowManagerError::FullscreenError(e.to_string()));
}

window.set_visible(true);
self.active_window_index = Some(index);

Ok(window.clone())
}

pub fn hide_active_window(&mut self) {
if let Some(index) = self.active_window_index.take() {
log::info!(
"WindowManager::hide_active_window: hiding window at index {}",
index
);
self.windows[index].window.set_visible(false);
}
}

pub fn update(&mut self, event_loop: &ActiveEventLoop) -> Result<(), WindowManagerError> {
let monitors: Vec<MonitorHandle> = event_loop.available_monitors().collect();
let mut monitor_positions: Vec<PhysicalPosition<i32>> =
monitors.iter().map(|m| m.position()).collect();

log::info!(
"WindowManager::update: checking {} monitors",
monitors.len()
);

self.windows.retain(|entry| {
if let Some(pos_index) = monitor_positions
.iter()
.position(|&pos| pos == entry.monitor_position)
{
monitor_positions.remove(pos_index);
true
} else {
log::info!(
"WindowManager::update: removing window at outdated position {:?}",
entry.monitor_position
);
false
}
});

for position in monitor_positions {
log::info!(
"WindowManager::update: adding new window for position {:?}",
position
);

let monitor = monitors
.iter()
.find(|m| m.position() == position)
.ok_or(WindowManagerError::MonitorNotFound)?;

let window_entry = Self::create_window_entry(event_loop, monitor)?;
self.windows.push(window_entry);
}

log::info!(
"WindowManager::update: windows list length: {:?}",
self.windows.len()
);

Ok(())
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent stale active_window_index after monitor changes.

update() can remove entries, but active_window_index isn’t revalidated. That can lead to an out-of-bounds access in hide_active_window() after a monitor topology change.

🛠️ Proposed fix
     pub fn hide_active_window(&mut self) {
         if let Some(index) = self.active_window_index.take() {
             log::info!(
                 "WindowManager::hide_active_window: hiding window at index {}",
                 index
             );
-            self.windows[index].window.set_visible(false);
+            if let Some(entry) = self.windows.get(index) {
+                entry.window.set_visible(false);
+            }
         }
     }
 
     pub fn update(&mut self, event_loop: &ActiveEventLoop) -> Result<(), WindowManagerError> {
+        let active_position = self
+            .active_window_index
+            .and_then(|i| self.windows.get(i).map(|e| e.monitor_position));
+
         let monitors: Vec<MonitorHandle> = event_loop.available_monitors().collect();
         let mut monitor_positions: Vec<PhysicalPosition<i32>> =
             monitors.iter().map(|m| m.position()).collect();
@@
         for position in monitor_positions {
             log::info!(
                 "WindowManager::update: adding new window for position {:?}",
                 position
             );
@@
             let window_entry = Self::create_window_entry(event_loop, monitor)?;
             self.windows.push(window_entry);
         }
+
+        self.active_window_index = active_position
+            .and_then(|pos| self.windows.iter().position(|e| e.monitor_position == pos));
🤖 Prompt for AI Agents
In `@core/src/window_manager.rs` around lines 121 - 218, Before removing stale
entries in update(), capture the current active window's monitor_position (if
any) from self.windows[self.active_window_index.unwrap()].monitor_position; then
run the retain as-is; after retain, re-find that monitor_position in the updated
self.windows (using iter().position(|entry| entry.monitor_position ==
saved_pos)) and set self.active_window_index = position_option (or None if not
found). This ensures hide_active_window(), update(), and other logic using
active_window_index remain valid; reference functions/fields: update(),
hide_active_window(), active_window_index, self.windows, and
entry.monitor_position.

Comment on lines +170 to +190
pub fn test_every_monitor() -> io::Result<()> {
let mut socket = connect_socket()?;
println!("Connected to socket.");

let livekit_server_url =
env::var("LIVEKIT_URL").expect("LIVEKIT_URL environment variable not set");
socket.send_message(Message::LivekitServerUrl(livekit_server_url))?;

let available_content = match get_available_content(&mut socket)? {
Message::AvailableContent(available_content) => available_content,
_ => return Err(io::Error::other("Failed to get available content")),
};

let monitors: Vec<_> = available_content
.content
.into_iter()
.filter(|c| matches!(c.content.content_type, ContentType::Display))
.collect();

println!("Found {} monitors to test.", monitors.len());

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fail fast when no display monitors are available.

Right now the test prints “Success” even if zero displays are reported, which can mask regressions. Consider returning an error when monitors is empty.

🛠️ Proposed fix
     println!("Found {} monitors to test.", monitors.len());
+    if monitors.is_empty() {
+        return Err(io::Error::other("No display monitors available to test"));
+    }
📝 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
pub fn test_every_monitor() -> io::Result<()> {
let mut socket = connect_socket()?;
println!("Connected to socket.");
let livekit_server_url =
env::var("LIVEKIT_URL").expect("LIVEKIT_URL environment variable not set");
socket.send_message(Message::LivekitServerUrl(livekit_server_url))?;
let available_content = match get_available_content(&mut socket)? {
Message::AvailableContent(available_content) => available_content,
_ => return Err(io::Error::other("Failed to get available content")),
};
let monitors: Vec<_> = available_content
.content
.into_iter()
.filter(|c| matches!(c.content.content_type, ContentType::Display))
.collect();
println!("Found {} monitors to test.", monitors.len());
pub fn test_every_monitor() -> io::Result<()> {
let mut socket = connect_socket()?;
println!("Connected to socket.");
let livekit_server_url =
env::var("LIVEKIT_URL").expect("LIVEKIT_URL environment variable not set");
socket.send_message(Message::LivekitServerUrl(livekit_server_url))?;
let available_content = match get_available_content(&mut socket)? {
Message::AvailableContent(available_content) => available_content,
_ => return Err(io::Error::other("Failed to get available content")),
};
let monitors: Vec<_> = available_content
.content
.into_iter()
.filter(|c| matches!(c.content.content_type, ContentType::Display))
.collect();
println!("Found {} monitors to test.", monitors.len());
if monitors.is_empty() {
return Err(io::Error::other("No display monitors available to test"));
}
🤖 Prompt for AI Agents
In `@core/tests/src/screenshare_client.rs` around lines 170 - 190, The test
test_every_monitor should fail fast when no display monitors are reported: after
computing monitors from get_available_content (filtering by
ContentType::Display), check monitors.is_empty() and return an io::Error (e.g.,
io::Error::new or io::Error::other) with a clear message like "No display
monitors available" instead of allowing the test to continue and print
"Success"; update the logic around the monitors variable in test_every_monitor
(and any downstream success/print paths) so an empty monitors list yields an
error rather than a passing test.

Copy link

@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

Caution

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

⚠️ Outside diff range comments (1)
core/src/lib.rs (1)

219-230: Don’t silently ignore WindowManager::update failures.

If window creation fails during update, swallowing the error makes later failures harder to diagnose and leaves stale state. At least log the failure here.

✅ Suggested tweak
         if let Some(wm) = self.window_manager.as_mut() {
-            let _ = wm.update(event_loop);
+            if let Err(e) = wm.update(event_loop) {
+                log::error!("get_available_content: WindowManager update failed: {e:?}");
+            }
         }
🤖 Fix all issues with AI agents
In `@core/src/lib.rs`:
- Around line 961-971: resumed() currently only creates a WindowManager when
self.window_manager is None, which leaves a stale manager after monitor topology
changes; change the logic in Application::resumed to always attempt to (re)build
or refresh the window manager: call
window_manager::WindowManager::new(event_loop) on every resume and on Ok replace
self.window_manager with the new instance (dropping the old one), and on Err log
the error (using the same log::error path) so the app uses an up-to-date
WindowManager after resume.
🧹 Nitpick comments (1)
core/src/window_manager.rs (1)

90-108: Split OS-specific behavior into dedicated platform modules.

This file mixes macOS/Windows/Linux code paths inline. Please extract platform-specific window handling into per-OS modules under core/src/**/{linux,macos,windows}.rs and call them from the shared manager.

As per coding guidelines, core/src/**/*.rs: Organize platform-specific modules in src/**/{linux,macos,windows}.rs subdirectories.

Also applies to: 240-285

Comment on lines +961 to +971
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.window_manager.is_none() {
log::info!("Application::resumed: initializing WindowManager");
match window_manager::WindowManager::new(event_loop) {
Ok(wm) => self.window_manager = Some(wm),
Err(e) => log::error!(
"Application::resumed: failed to initialize WindowManager: {:?}",
e
),
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Refresh WindowManager on resume to handle monitor topology changes.

resumed() only initializes when None. If displays change while the app is suspended, the existing WindowManager can become stale and target the wrong display or fail to find a monitor. Update (or rebuild) it on every resume.

🛠️ Suggested fix
     fn resumed(&mut self, event_loop: &ActiveEventLoop) {
-        if self.window_manager.is_none() {
-            log::info!("Application::resumed: initializing WindowManager");
-            match window_manager::WindowManager::new(event_loop) {
-                Ok(wm) => self.window_manager = Some(wm),
-                Err(e) => log::error!(
-                    "Application::resumed: failed to initialize WindowManager: {:?}",
-                    e
-                ),
-            }
-        }
+        if let Some(wm) = self.window_manager.as_mut() {
+            if let Err(e) = wm.update(event_loop) {
+                log::error!(
+                    "Application::resumed: failed to update WindowManager: {:?}",
+                    e
+                );
+            }
+            return;
+        }
+
+        log::info!("Application::resumed: initializing WindowManager");
+        match window_manager::WindowManager::new(event_loop) {
+            Ok(wm) => self.window_manager = Some(wm),
+            Err(e) => log::error!(
+                "Application::resumed: failed to initialize WindowManager: {:?}",
+                e
+            ),
+        }
     }
🤖 Prompt for AI Agents
In `@core/src/lib.rs` around lines 961 - 971, resumed() currently only creates a
WindowManager when self.window_manager is None, which leaves a stale manager
after monitor topology changes; change the logic in Application::resumed to
always attempt to (re)build or refresh the window manager: call
window_manager::WindowManager::new(event_loop) on every resume and on Ok replace
self.window_manager with the new instance (dropping the old one), and on Err log
the error (using the same log::error path) so the app uses an up-to-date
WindowManager after resume.

@iparaskev iparaskev merged commit 4692730 into main Jan 29, 2026
19 checks passed
@iparaskev iparaskev deleted the fix_display_issue branch January 29, 2026 00:40
@coderabbitai coderabbitai bot mentioned this pull request Feb 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants