Rust · 26848 bytes Raw Blame History
1 //! Mouse edge capture via layer-shell
2 //!
3 //! Creates invisible 1-pixel barriers at screen edges to detect
4 //! when the cursor attempts to leave the screen.
5
6 use std::sync::mpsc;
7 use std::time::Instant;
8
9 use hyprkvm_common::Direction;
10 use smithay_client_toolkit::{
11 compositor::{CompositorHandler, CompositorState},
12 delegate_compositor, delegate_layer, delegate_output, delegate_pointer, delegate_registry,
13 delegate_seat, delegate_shm,
14 output::{OutputHandler, OutputState},
15 registry::{ProvidesRegistryState, RegistryState},
16 registry_handlers,
17 seat::{
18 pointer::{PointerEvent, PointerEventKind, PointerHandler},
19 Capability, SeatHandler, SeatState,
20 },
21 shell::{
22 wlr_layer::{
23 Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
24 LayerSurfaceConfigure,
25 },
26 WaylandSurface,
27 },
28 shm::{slot::SlotPool, Shm, ShmHandler},
29 };
30 use wayland_client::{
31 globals::registry_queue_init,
32 protocol::{wl_output, wl_pointer, wl_seat, wl_shm, wl_surface},
33 Connection, QueueHandle,
34 };
35
36 /// Event emitted when cursor hits a screen edge
37 #[derive(Debug, Clone)]
38 pub struct EdgeEvent {
39 pub direction: Direction,
40 pub position: (i32, i32),
41 pub timestamp: Instant,
42 }
43
44 /// Monitor info from Hyprland
45 #[derive(Debug, Clone)]
46 pub struct MonitorInfo {
47 pub name: String,
48 pub x: i32,
49 pub y: i32,
50 pub width: u32,
51 pub height: u32,
52 pub scale: f32,
53 }
54
55 /// Configuration for edge barriers
56 #[derive(Debug, Clone)]
57 pub struct EdgeCaptureConfig {
58 /// Barrier thickness in pixels
59 pub barrier_size: u32,
60 /// Which edges to create barriers on
61 pub enabled_edges: Vec<Direction>,
62 /// Monitor positions from Hyprland (used instead of Wayland output positions)
63 pub monitors: Vec<MonitorInfo>,
64 }
65
66 impl Default for EdgeCaptureConfig {
67 fn default() -> Self {
68 Self {
69 barrier_size: 5, // 5 pixels to ensure we catch events
70 enabled_edges: vec![
71 Direction::Left,
72 Direction::Right,
73 Direction::Up,
74 Direction::Down,
75 ],
76 monitors: Vec::new(),
77 }
78 }
79 }
80
81 /// An edge barrier surface
82 struct EdgeBarrier {
83 direction: Direction,
84 surface: LayerSurface,
85 width: u32,
86 height: u32,
87 configured: bool,
88 }
89
90 /// Edge capture system state
91 struct EdgeCaptureState {
92 registry_state: RegistryState,
93 compositor_state: CompositorState,
94 output_state: OutputState,
95 seat_state: SeatState,
96 shm_state: Shm,
97 layer_shell: LayerShell,
98
99 barriers: Vec<EdgeBarrier>,
100 pool: Option<SlotPool>,
101 event_tx: mpsc::Sender<EdgeEvent>,
102 config: EdgeCaptureConfig,
103
104 // Track outputs for barrier sizing
105 outputs: Vec<OutputInfo>,
106
107 // Track which barrier surface the pointer is currently on
108 active_barrier_idx: Option<usize>,
109 // Last position on barrier (for detecting edge-pushing motion)
110 last_barrier_pos: (f64, f64),
111 // Debounce: last time we sent an event for each direction
112 last_trigger_time: std::collections::HashMap<Direction, Instant>,
113 }
114
115 #[derive(Debug, Clone)]
116 struct OutputInfo {
117 output: wl_output::WlOutput,
118 width: u32,
119 height: u32,
120 x: i32,
121 y: i32,
122 }
123
124 impl EdgeCaptureState {
125 /// Update output positions from Hyprland monitor info
126 fn update_output_positions(&mut self) {
127 // Hyprland reports physical resolution, Wayland reports logical (after scaling)
128 // We need to match by finding monitors whose logical size matches the wayland output
129 // Logical size = physical size / scale
130
131 let mut used_monitors: Vec<bool> = vec![false; self.config.monitors.len()];
132 let mut assigned_outputs: Vec<bool> = vec![false; self.outputs.len()];
133
134 // Helper to check if a monitor's logical size matches an output
135 let sizes_match = |mon: &MonitorInfo, out_w: u32, out_h: u32| -> bool {
136 // Use the monitor's actual scale from Hyprland
137 let scale = mon.scale as f64;
138 let logical_w = (mon.width as f64 / scale).round() as u32;
139 let logical_h = (mon.height as f64 / scale).round() as u32;
140
141 // Allow 1 pixel tolerance for rounding differences
142 let w_match = (logical_w as i32 - out_w as i32).abs() <= 1;
143 let h_match = (logical_h as i32 - out_h as i32).abs() <= 1;
144 w_match && h_match
145 };
146
147 // First pass: try to find unique matches
148 for (out_idx, out) in self.outputs.iter_mut().enumerate() {
149 let mut candidates: Vec<usize> = Vec::new();
150 for (i, mon) in self.config.monitors.iter().enumerate() {
151 if used_monitors[i] {
152 continue;
153 }
154 if sizes_match(mon, out.width, out.height) {
155 candidates.push(i);
156 }
157 }
158
159 // If exactly one candidate, use it
160 if candidates.len() == 1 {
161 let i = candidates[0];
162 let mon = &self.config.monitors[i];
163 tracing::info!("Matched output {}x{} to monitor {} at ({}, {}) scale={}",
164 out.width, out.height, mon.name, mon.x, mon.y, mon.scale);
165 out.x = mon.x;
166 out.y = mon.y;
167 used_monitors[i] = true;
168 assigned_outputs[out_idx] = true;
169 } else if candidates.len() > 1 {
170 tracing::debug!("Multiple candidates for output {}x{}: {:?}",
171 out.width, out.height,
172 candidates.iter().map(|&i| &self.config.monitors[i].name).collect::<Vec<_>>());
173 }
174 }
175
176 // Second pass: assign remaining outputs to remaining monitors by order
177 for (out_idx, out) in self.outputs.iter_mut().enumerate() {
178 if assigned_outputs[out_idx] {
179 continue; // Already assigned in first pass
180 }
181 // Find first unused monitor that matches
182 for (i, mon) in self.config.monitors.iter().enumerate() {
183 if used_monitors[i] {
184 continue;
185 }
186 if sizes_match(mon, out.width, out.height) {
187 tracing::info!("Assigned remaining output {}x{} to monitor {} at ({}, {}) scale={}",
188 out.width, out.height, mon.name, mon.x, mon.y, mon.scale);
189 out.x = mon.x;
190 out.y = mon.y;
191 used_monitors[i] = true;
192 assigned_outputs[out_idx] = true;
193 break;
194 }
195 }
196 }
197 }
198
199 fn create_barriers(&mut self, qh: &QueueHandle<Self>) {
200 // Update positions from Hyprland before creating barriers
201 self.update_output_positions();
202
203 // Log current outputs for debugging
204 tracing::info!("Creating barriers with {} outputs:", self.outputs.len());
205 for out in &self.outputs {
206 tracing::info!(" Output at ({}, {}) size {}x{}", out.x, out.y, out.width, out.height);
207 }
208
209 // Get total screen bounds
210 let (min_x, min_y, max_x, max_y) = self.screen_bounds();
211 tracing::info!("Screen bounds: ({}, {}) to ({}, {})", min_x, min_y, max_x, max_y);
212
213 for direction in &self.config.enabled_edges.clone() {
214 // Find the correct output for this edge direction
215 let target_output = self.find_edge_output(*direction);
216 tracing::info!(" {:?} edge -> output at {:?}",
217 direction,
218 target_output.as_ref().map(|o| (o.x, o.y, o.width, o.height)));
219
220 let (width, height, anchor) = match direction {
221 Direction::Left => (
222 self.config.barrier_size,
223 target_output.as_ref().map(|o| o.height).unwrap_or((max_y - min_y) as u32),
224 Anchor::LEFT | Anchor::TOP | Anchor::BOTTOM,
225 ),
226 Direction::Right => (
227 self.config.barrier_size,
228 target_output.as_ref().map(|o| o.height).unwrap_or((max_y - min_y) as u32),
229 Anchor::RIGHT | Anchor::TOP | Anchor::BOTTOM,
230 ),
231 Direction::Up => (
232 target_output.as_ref().map(|o| o.width).unwrap_or((max_x - min_x) as u32),
233 self.config.barrier_size,
234 Anchor::TOP | Anchor::LEFT | Anchor::RIGHT,
235 ),
236 Direction::Down => (
237 target_output.as_ref().map(|o| o.width).unwrap_or((max_x - min_x) as u32),
238 self.config.barrier_size,
239 Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT,
240 ),
241 };
242
243 // Create layer surface on the specific edge output
244 let surface = self.compositor_state.create_surface(qh);
245 let output_ref = target_output.as_ref().map(|o| &o.output);
246 let layer_surface = self.layer_shell.create_layer_surface(
247 qh,
248 surface,
249 Layer::Top, // Top layer to catch pointer
250 Some(format!("hyprkvm-edge-{}", direction)),
251 output_ref,
252 );
253
254 tracing::info!(
255 "Creating {:?} barrier on output {:?}, size {}x{}",
256 direction,
257 target_output.as_ref().map(|o| format!("at ({}, {})", o.x, o.y)),
258 width,
259 height
260 );
261
262 // Configure the layer surface
263 layer_surface.set_anchor(anchor);
264 layer_surface.set_size(width, height);
265 layer_surface.set_exclusive_zone(-1); // Don't reserve space
266 layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
267
268 // Commit to apply configuration
269 layer_surface.commit();
270
271 self.barriers.push(EdgeBarrier {
272 direction: *direction,
273 surface: layer_surface,
274 width,
275 height,
276 configured: false,
277 });
278 }
279 }
280
281 /// Find the output at the edge of the screen for a given direction
282 fn find_edge_output(&self, direction: Direction) -> Option<OutputInfo> {
283 if self.outputs.is_empty() {
284 return None;
285 }
286
287 match direction {
288 Direction::Left => {
289 // Find output with minimum x (leftmost)
290 self.outputs.iter().min_by_key(|o| o.x).cloned()
291 }
292 Direction::Right => {
293 // Find output with maximum x + width (rightmost)
294 self.outputs.iter().max_by_key(|o| o.x + o.width as i32).cloned()
295 }
296 Direction::Up => {
297 // Find output with minimum y (topmost)
298 self.outputs.iter().min_by_key(|o| o.y).cloned()
299 }
300 Direction::Down => {
301 // Find output with maximum y + height (bottommost)
302 self.outputs.iter().max_by_key(|o| o.y + o.height as i32).cloned()
303 }
304 }
305 }
306
307 fn screen_bounds(&self) -> (i32, i32, i32, i32) {
308 if self.outputs.is_empty() {
309 return (0, 0, 1920, 1080); // Fallback
310 }
311
312 let mut min_x = i32::MAX;
313 let mut min_y = i32::MAX;
314 let mut max_x = i32::MIN;
315 let mut max_y = i32::MIN;
316
317 for output in &self.outputs {
318 min_x = min_x.min(output.x);
319 min_y = min_y.min(output.y);
320 max_x = max_x.max(output.x + output.width as i32);
321 max_y = max_y.max(output.y + output.height as i32);
322 }
323
324 (min_x, min_y, max_x, max_y)
325 }
326
327 fn draw_barrier(&mut self, barrier_idx: usize, _qh: &QueueHandle<Self>) {
328 let barrier = &self.barriers[barrier_idx];
329 if !barrier.configured {
330 return;
331 }
332
333 let pool = match &mut self.pool {
334 Some(pool) => pool,
335 None => return,
336 };
337
338 let width = barrier.width;
339 let height = barrier.height;
340 let stride = width * 4;
341
342 let (buffer, canvas) = pool
343 .create_buffer(
344 width as i32,
345 height as i32,
346 stride as i32,
347 wl_shm::Format::Argb8888,
348 )
349 .expect("Failed to create buffer");
350
351 // Fill with transparent pixels
352 canvas.fill(0);
353
354 // Attach and commit
355 barrier.surface.wl_surface().attach(Some(buffer.wl_buffer()), 0, 0);
356 barrier.surface.wl_surface().damage_buffer(0, 0, width as i32, height as i32);
357 barrier.surface.commit();
358 }
359
360 fn find_barrier_by_surface(&self, surface: &wl_surface::WlSurface) -> Option<usize> {
361 self.barriers
362 .iter()
363 .position(|b| b.surface.wl_surface() == surface)
364 }
365 }
366
367 // Implement all the required handlers
368
369 impl CompositorHandler for EdgeCaptureState {
370 fn scale_factor_changed(
371 &mut self,
372 _conn: &Connection,
373 _qh: &QueueHandle<Self>,
374 _surface: &wl_surface::WlSurface,
375 _new_factor: i32,
376 ) {
377 }
378
379 fn transform_changed(
380 &mut self,
381 _conn: &Connection,
382 _qh: &QueueHandle<Self>,
383 _surface: &wl_surface::WlSurface,
384 _new_transform: wl_output::Transform,
385 ) {
386 }
387
388 fn frame(
389 &mut self,
390 _conn: &Connection,
391 _qh: &QueueHandle<Self>,
392 _surface: &wl_surface::WlSurface,
393 _time: u32,
394 ) {
395 }
396
397 fn surface_enter(
398 &mut self,
399 _conn: &Connection,
400 _qh: &QueueHandle<Self>,
401 _surface: &wl_surface::WlSurface,
402 _output: &wl_output::WlOutput,
403 ) {
404 }
405
406 fn surface_leave(
407 &mut self,
408 _conn: &Connection,
409 _qh: &QueueHandle<Self>,
410 _surface: &wl_surface::WlSurface,
411 _output: &wl_output::WlOutput,
412 ) {
413 }
414 }
415
416 impl OutputHandler for EdgeCaptureState {
417 fn output_state(&mut self) -> &mut OutputState {
418 &mut self.output_state
419 }
420
421 fn new_output(
422 &mut self,
423 _conn: &Connection,
424 _qh: &QueueHandle<Self>,
425 output: wl_output::WlOutput,
426 ) {
427 // We'll get geometry info from the output state
428 if let Some(info) = self.output_state.info(&output) {
429 if let Some(size) = info.logical_size {
430 let (x, y) = info.location;
431 self.outputs.push(OutputInfo {
432 output,
433 width: size.0 as u32,
434 height: size.1 as u32,
435 x,
436 y,
437 });
438 }
439 }
440 }
441
442 fn update_output(
443 &mut self,
444 _conn: &Connection,
445 _qh: &QueueHandle<Self>,
446 output: wl_output::WlOutput,
447 ) {
448 // Update output info
449 if let Some(info) = self.output_state.info(&output) {
450 if let Some(size) = info.logical_size {
451 let (x, y) = info.location;
452 if let Some(out) = self.outputs.iter_mut().find(|o| o.output == output) {
453 out.width = size.0 as u32;
454 out.height = size.1 as u32;
455 out.x = x;
456 out.y = y;
457 }
458 }
459 }
460 }
461
462 fn output_destroyed(
463 &mut self,
464 _conn: &Connection,
465 _qh: &QueueHandle<Self>,
466 output: wl_output::WlOutput,
467 ) {
468 self.outputs.retain(|o| o.output != output);
469 }
470 }
471
472 impl LayerShellHandler for EdgeCaptureState {
473 fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {
474 // Barrier closed, could recreate if needed
475 }
476
477 fn configure(
478 &mut self,
479 _conn: &Connection,
480 qh: &QueueHandle<Self>,
481 layer: &LayerSurface,
482 configure: LayerSurfaceConfigure,
483 _serial: u32,
484 ) {
485 // Find which barrier this is
486 if let Some(idx) = self.barriers.iter().position(|b| &b.surface == layer) {
487 let barrier = &mut self.barriers[idx];
488 barrier.configured = true;
489
490 // Update size if compositor changed it
491 if configure.new_size.0 > 0 {
492 barrier.width = configure.new_size.0;
493 }
494 if configure.new_size.1 > 0 {
495 barrier.height = configure.new_size.1;
496 }
497
498 // Draw the (transparent) barrier
499 self.draw_barrier(idx, qh);
500 }
501 }
502 }
503
504 impl SeatHandler for EdgeCaptureState {
505 fn seat_state(&mut self) -> &mut SeatState {
506 &mut self.seat_state
507 }
508
509 fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _seat: wl_seat::WlSeat) {}
510
511 fn new_capability(
512 &mut self,
513 _conn: &Connection,
514 qh: &QueueHandle<Self>,
515 seat: wl_seat::WlSeat,
516 capability: Capability,
517 ) {
518 if capability == Capability::Pointer {
519 self.seat_state.get_pointer(qh, &seat).ok();
520 }
521 }
522
523 fn remove_capability(
524 &mut self,
525 _conn: &Connection,
526 _qh: &QueueHandle<Self>,
527 _seat: wl_seat::WlSeat,
528 _capability: Capability,
529 ) {
530 }
531
532 fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _seat: wl_seat::WlSeat) {
533 }
534 }
535
536 impl PointerHandler for EdgeCaptureState {
537 fn pointer_frame(
538 &mut self,
539 _conn: &Connection,
540 _qh: &QueueHandle<Self>,
541 _pointer: &wl_pointer::WlPointer,
542 events: &[PointerEvent],
543 ) {
544 const DEBOUNCE_MS: u128 = 300; // Minimum time between triggers
545 const EDGE_THRESHOLD: f64 = 2.0; // Position threshold for "at edge"
546
547 for event in events {
548 match &event.kind {
549 PointerEventKind::Enter { .. } => {
550 // Pointer entered one of our barrier surfaces
551 if let Some(idx) = self.find_barrier_by_surface(&event.surface) {
552 self.active_barrier_idx = Some(idx);
553 self.last_barrier_pos = event.position;
554 tracing::debug!(
555 "Pointer entered {:?} barrier at ({}, {})",
556 self.barriers[idx].direction,
557 event.position.0,
558 event.position.1
559 );
560 }
561 }
562 PointerEventKind::Leave { .. } => {
563 if self.find_barrier_by_surface(&event.surface).is_some() {
564 self.active_barrier_idx = None;
565 tracing::debug!("Pointer left barrier");
566 }
567 }
568 PointerEventKind::Motion { .. } => {
569 // Check if we're on a barrier surface and detect edge-pushing motion
570 if let Some(idx) = self.active_barrier_idx {
571 let barrier = &self.barriers[idx];
572 let (x, y) = event.position;
573 let (last_x, last_y) = self.last_barrier_pos;
574
575 tracing::trace!(
576 "Motion on {:?} barrier: pos=({:.1}, {:.1}) last=({:.1}, {:.1}) size={}x{}",
577 barrier.direction,
578 x, y, last_x, last_y, barrier.width, barrier.height
579 );
580
581 // Calculate motion delta
582 let dx = x - last_x;
583 let dy = y - last_y;
584
585 // Check if motion is toward the edge while at the edge
586 let is_edge_push = match barrier.direction {
587 Direction::Left => x <= EDGE_THRESHOLD && dx < -0.1,
588 Direction::Right => x >= (barrier.width as f64 - EDGE_THRESHOLD) && dx > 0.1,
589 Direction::Up => y <= EDGE_THRESHOLD && dy < -0.1,
590 Direction::Down => y >= (barrier.height as f64 - EDGE_THRESHOLD) && dy > 0.1,
591 };
592
593 // Also trigger if cursor is stuck at edge position (x=0 or similar)
594 let is_at_edge = match barrier.direction {
595 Direction::Left => x < 1.0,
596 Direction::Right => x > (barrier.width as f64 - 1.0),
597 Direction::Up => y < 1.0,
598 Direction::Down => y > (barrier.height as f64 - 1.0),
599 };
600
601 if is_edge_push || is_at_edge {
602 // Check debounce
603 let now = Instant::now();
604 let should_trigger = self
605 .last_trigger_time
606 .get(&barrier.direction)
607 .map(|t| now.duration_since(*t).as_millis() >= DEBOUNCE_MS)
608 .unwrap_or(true);
609
610 if should_trigger {
611 self.last_trigger_time.insert(barrier.direction, now);
612
613 // Send edge event with direction only
614 // Main daemon will query Hyprland for actual cursor position
615 // and verify we're at screen boundary before triggering transfer
616 let edge_event = EdgeEvent {
617 direction: barrier.direction,
618 position: (0, 0), // Placeholder - main daemon uses Hyprland cursor pos
619 timestamp: now,
620 };
621
622 tracing::info!(
623 "Edge event: {:?} barrier triggered",
624 barrier.direction
625 );
626
627 let _ = self.event_tx.send(edge_event);
628 }
629 }
630
631 self.last_barrier_pos = event.position;
632 }
633 }
634 _ => {}
635 }
636 }
637 }
638 }
639
640 impl ShmHandler for EdgeCaptureState {
641 fn shm_state(&mut self) -> &mut Shm {
642 &mut self.shm_state
643 }
644 }
645
646 impl ProvidesRegistryState for EdgeCaptureState {
647 fn registry(&mut self) -> &mut RegistryState {
648 &mut self.registry_state
649 }
650
651 registry_handlers!(OutputState, SeatState);
652 }
653
654 // Delegate macros
655 delegate_compositor!(EdgeCaptureState);
656 delegate_output!(EdgeCaptureState);
657 delegate_layer!(EdgeCaptureState);
658 delegate_seat!(EdgeCaptureState);
659 delegate_pointer!(EdgeCaptureState);
660 delegate_shm!(EdgeCaptureState);
661 delegate_registry!(EdgeCaptureState);
662
663 /// Handle for controlling edge capture
664 pub struct EdgeCapture {
665 event_rx: mpsc::Receiver<EdgeEvent>,
666 // The event loop runs in a separate thread
667 _thread: std::thread::JoinHandle<()>,
668 }
669
670 impl EdgeCapture {
671 /// Create and start edge capture
672 pub fn new(config: EdgeCaptureConfig) -> Result<Self, EdgeCaptureError> {
673 let (event_tx, event_rx) = mpsc::channel();
674
675 let thread = std::thread::spawn(move || {
676 if let Err(e) = run_capture_loop(config, event_tx) {
677 tracing::error!("Edge capture error: {}", e);
678 }
679 });
680
681 Ok(Self {
682 event_rx,
683 _thread: thread,
684 })
685 }
686
687 /// Try to receive an edge event (non-blocking)
688 pub fn try_recv(&self) -> Option<EdgeEvent> {
689 self.event_rx.try_recv().ok()
690 }
691
692 /// Receive an edge event (blocking)
693 pub fn recv(&self) -> Option<EdgeEvent> {
694 self.event_rx.recv().ok()
695 }
696
697 /// Get the receiver for async integration
698 pub fn receiver(&self) -> &mpsc::Receiver<EdgeEvent> {
699 &self.event_rx
700 }
701 }
702
703 fn run_capture_loop(
704 config: EdgeCaptureConfig,
705 event_tx: mpsc::Sender<EdgeEvent>,
706 ) -> Result<(), EdgeCaptureError> {
707 let conn = Connection::connect_to_env().map_err(|e| EdgeCaptureError::Connection(e.to_string()))?;
708
709 let (globals, mut event_queue) =
710 registry_queue_init(&conn).map_err(|e| EdgeCaptureError::Registry(e.to_string()))?;
711 let qh = event_queue.handle();
712
713 let compositor_state = CompositorState::bind(&globals, &qh)
714 .map_err(|e| EdgeCaptureError::Protocol(format!("compositor: {}", e)))?;
715 let layer_shell = LayerShell::bind(&globals, &qh)
716 .map_err(|e| EdgeCaptureError::Protocol(format!("layer_shell: {}", e)))?;
717 let shm_state =
718 Shm::bind(&globals, &qh).map_err(|e| EdgeCaptureError::Protocol(format!("shm: {}", e)))?;
719
720 let mut state = EdgeCaptureState {
721 registry_state: RegistryState::new(&globals),
722 compositor_state,
723 output_state: OutputState::new(&globals, &qh),
724 seat_state: SeatState::new(&globals, &qh),
725 shm_state,
726 layer_shell,
727 barriers: Vec::new(),
728 pool: None,
729 event_tx,
730 config,
731 outputs: Vec::new(),
732 active_barrier_idx: None,
733 last_barrier_pos: (0.0, 0.0),
734 last_trigger_time: std::collections::HashMap::new(),
735 };
736
737 // Multiple roundtrips to ensure all output info is received
738 // Output geometry comes in separate events that may need multiple roundtrips
739 event_queue.roundtrip(&mut state).map_err(|e| EdgeCaptureError::Dispatch(e.to_string()))?;
740 event_queue.roundtrip(&mut state).map_err(|e| EdgeCaptureError::Dispatch(e.to_string()))?;
741 event_queue.roundtrip(&mut state).map_err(|e| EdgeCaptureError::Dispatch(e.to_string()))?;
742
743 tracing::info!("After roundtrips: {} outputs detected", state.outputs.len());
744
745 // Create shm pool for buffers
746 state.pool = Some(
747 SlotPool::new(256 * 256 * 4, &state.shm_state)
748 .map_err(|e| EdgeCaptureError::Shm(e.to_string()))?,
749 );
750
751 // Create barriers after we have output info
752 state.create_barriers(&qh);
753
754 // Another roundtrip to configure barriers
755 event_queue.roundtrip(&mut state).map_err(|e| EdgeCaptureError::Dispatch(e.to_string()))?;
756
757 tracing::info!("Edge capture running with {} barriers", state.barriers.len());
758
759 // Main event loop
760 loop {
761 event_queue.blocking_dispatch(&mut state).map_err(|e| EdgeCaptureError::Dispatch(e.to_string()))?;
762 }
763 }
764
765 #[derive(Debug, thiserror::Error)]
766 pub enum EdgeCaptureError {
767 #[error("Failed to connect to Wayland: {0}")]
768 Connection(String),
769
770 #[error("Registry error: {0}")]
771 Registry(String),
772
773 #[error("Protocol not available: {0}")]
774 Protocol(String),
775
776 #[error("Dispatch error: {0}")]
777 Dispatch(String),
778
779 #[error("SHM error: {0}")]
780 Shm(String),
781 }
782