⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/graphics/
multi_output.rs

1//! Multi-Output Display Manager
2//!
3//! Manages multiple display outputs for multi-monitor configurations.
4//! Coordinates output enumeration, positioning, per-output page flips,
5//! and hotplug events for DRM connectors.
6
7#![allow(dead_code)]
8
9use core::sync::atomic::{AtomicBool, Ordering};
10
11use spin::Mutex;
12
13use crate::error::KernelError;
14
15// ---------------------------------------------------------------------------
16// Constants
17// ---------------------------------------------------------------------------
18
19/// Maximum number of simultaneous display outputs
20const MAX_OUTPUTS: usize = 8;
21
22/// Default refresh rate in millihertz (60 Hz)
23const DEFAULT_REFRESH_MHZ: u32 = 60000;
24
25// ---------------------------------------------------------------------------
26// Types
27// ---------------------------------------------------------------------------
28
29/// Represents a single display output (physical monitor/connector)
30#[derive(Debug, Clone, Copy)]
31pub struct DisplayOutput {
32    /// Unique output identifier
33    pub id: u32,
34    /// DRM connector ID
35    pub connector_id: u32,
36    /// DRM CRTC ID assigned to this output
37    pub crtc_id: u32,
38    /// Display width in pixels
39    pub width: u32,
40    /// Display height in pixels
41    pub height: u32,
42    /// Refresh rate in millihertz
43    pub refresh_hz: u32,
44    /// X offset in virtual desktop coordinates
45    pub x_offset: i32,
46    /// Y offset in virtual desktop coordinates
47    pub y_offset: i32,
48    /// Whether this output is enabled
49    pub enabled: bool,
50    /// Whether this is the primary output
51    pub primary: bool,
52    /// DRM connector connection status
53    pub connected: bool,
54    /// Physical width in mm (from EDID)
55    pub physical_width_mm: u32,
56    /// Physical height in mm (from EDID)
57    pub physical_height_mm: u32,
58}
59
60impl Default for DisplayOutput {
61    fn default() -> Self {
62        Self {
63            id: 0,
64            connector_id: 0,
65            crtc_id: 0,
66            width: 0,
67            height: 0,
68            refresh_hz: DEFAULT_REFRESH_MHZ,
69            x_offset: 0,
70            y_offset: 0,
71            enabled: false,
72            primary: false,
73            connected: false,
74            physical_width_mm: 0,
75            physical_height_mm: 0,
76        }
77    }
78}
79
80impl DisplayOutput {
81    /// Get the right edge of this output in virtual desktop coordinates
82    pub fn right_edge(&self) -> i32 {
83        self.x_offset.saturating_add(self.width as i32)
84    }
85
86    /// Get the bottom edge of this output in virtual desktop coordinates
87    pub fn bottom_edge(&self) -> i32 {
88        self.y_offset.saturating_add(self.height as i32)
89    }
90
91    /// Check if a point falls within this output
92    pub fn contains_point(&self, x: i32, y: i32) -> bool {
93        if !self.enabled {
94            return false;
95        }
96        x >= self.x_offset && x < self.right_edge() && y >= self.y_offset && y < self.bottom_edge()
97    }
98}
99
100/// Multi-output display manager
101pub struct MultiOutputManager {
102    /// Array of display outputs
103    outputs: [DisplayOutput; MAX_OUTPUTS],
104    /// Number of active outputs
105    num_outputs: usize,
106    /// Next output ID to assign
107    next_id: u32,
108    /// Total virtual desktop width
109    total_width: u32,
110    /// Total virtual desktop height
111    total_height: u32,
112    /// Whether the manager has been initialized
113    initialized: bool,
114}
115
116impl Default for MultiOutputManager {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122impl MultiOutputManager {
123    /// Create a new multi-output manager
124    pub const fn new() -> Self {
125        const DEFAULT: DisplayOutput = DisplayOutput {
126            id: 0,
127            connector_id: 0,
128            crtc_id: 0,
129            width: 0,
130            height: 0,
131            refresh_hz: DEFAULT_REFRESH_MHZ,
132            x_offset: 0,
133            y_offset: 0,
134            enabled: false,
135            primary: false,
136            connected: false,
137            physical_width_mm: 0,
138            physical_height_mm: 0,
139        };
140        Self {
141            outputs: [DEFAULT; MAX_OUTPUTS],
142            num_outputs: 0,
143            next_id: 1,
144            total_width: 0,
145            total_height: 0,
146            initialized: false,
147        }
148    }
149
150    /// Initialize the multi-output manager
151    pub fn init(&mut self) {
152        self.num_outputs = 0;
153        self.next_id = 1;
154        self.total_width = 0;
155        self.total_height = 0;
156        self.initialized = true;
157    }
158
159    /// Add a new display output
160    ///
161    /// The output is placed to the right of all existing outputs by default.
162    pub fn add_output(
163        &mut self,
164        connector_id: u32,
165        crtc_id: u32,
166        width: u32,
167        height: u32,
168        refresh_hz: u32,
169    ) -> Result<u32, KernelError> {
170        if self.num_outputs >= MAX_OUTPUTS {
171            return Err(KernelError::ResourceExhausted {
172                resource: "display outputs",
173            });
174        }
175
176        let id = self.next_id;
177        self.next_id = self.next_id.saturating_add(1);
178
179        // Place output to the right of all existing outputs
180        let x_offset = self.total_width as i32;
181        let is_primary = self.num_outputs == 0;
182
183        self.outputs[self.num_outputs] = DisplayOutput {
184            id,
185            connector_id,
186            crtc_id,
187            width,
188            height,
189            refresh_hz,
190            x_offset,
191            y_offset: 0,
192            enabled: true,
193            primary: is_primary,
194            connected: true,
195            physical_width_mm: 0,
196            physical_height_mm: 0,
197        };
198
199        self.num_outputs += 1;
200        self.recalculate_total_size();
201
202        Ok(id)
203    }
204
205    /// Remove a display output by ID
206    pub fn remove_output(&mut self, output_id: u32) -> Result<(), KernelError> {
207        let idx = self.find_output_index(output_id)?;
208
209        // Shift remaining outputs down
210        for i in idx..self.num_outputs.saturating_sub(1) {
211            self.outputs[i] = self.outputs[i + 1];
212        }
213
214        if self.num_outputs > 0 {
215            self.num_outputs -= 1;
216            self.outputs[self.num_outputs] = DisplayOutput::default();
217        }
218
219        // If we removed the primary, promote the first remaining output
220        if self.num_outputs > 0 {
221            let has_primary = self.outputs[..self.num_outputs].iter().any(|o| o.primary);
222            if !has_primary {
223                self.outputs[0].primary = true;
224            }
225        }
226
227        self.recalculate_total_size();
228        Ok(())
229    }
230
231    /// Set the position of an output in virtual desktop coordinates
232    pub fn set_position(&mut self, output_id: u32, x: i32, y: i32) -> Result<(), KernelError> {
233        let idx = self.find_output_index(output_id)?;
234        self.outputs[idx].x_offset = x;
235        self.outputs[idx].y_offset = y;
236        self.recalculate_total_size();
237        Ok(())
238    }
239
240    /// Set the primary output
241    pub fn set_primary(&mut self, output_id: u32) -> Result<(), KernelError> {
242        let idx = self.find_output_index(output_id)?;
243
244        // Clear primary on all outputs
245        for i in 0..self.num_outputs {
246            self.outputs[i].primary = false;
247        }
248
249        self.outputs[idx].primary = true;
250        Ok(())
251    }
252
253    /// Get list of active outputs
254    pub fn get_outputs(&self) -> &[DisplayOutput] {
255        &self.outputs[..self.num_outputs]
256    }
257
258    /// Get total virtual desktop size
259    pub fn get_total_size(&self) -> (u32, u32) {
260        (self.total_width, self.total_height)
261    }
262
263    /// Map a point in virtual desktop coordinates to a specific output
264    ///
265    /// Returns (output_id, local_x, local_y) or None if the point is
266    /// outside all outputs.
267    pub fn point_to_output(&self, x: i32, y: i32) -> Option<(u32, i32, i32)> {
268        for i in 0..self.num_outputs {
269            let output = &self.outputs[i];
270            if output.enabled && output.contains_point(x, y) {
271                let local_x = x - output.x_offset;
272                let local_y = y - output.y_offset;
273                return Some((output.id, local_x, local_y));
274            }
275        }
276        None
277    }
278
279    /// Get the primary output
280    pub fn primary_output(&self) -> Option<&DisplayOutput> {
281        self.outputs[..self.num_outputs].iter().find(|o| o.primary)
282    }
283
284    /// Get a specific output by ID
285    pub fn get_output(&self, output_id: u32) -> Option<&DisplayOutput> {
286        self.outputs[..self.num_outputs]
287            .iter()
288            .find(|o| o.id == output_id)
289    }
290
291    /// Get a mutable reference to a specific output by ID
292    pub fn get_output_mut(&mut self, output_id: u32) -> Option<&mut DisplayOutput> {
293        self.outputs[..self.num_outputs]
294            .iter_mut()
295            .find(|o| o.id == output_id)
296    }
297
298    /// Schedule a page flip on a specific output
299    ///
300    /// In a real implementation this would issue a DRM page flip ioctl.
301    pub fn flip(&self, output_id: u32, _buffer_id: u32) -> Result<(), KernelError> {
302        let _output = self.get_output(output_id).ok_or(KernelError::NotFound {
303            resource: "output",
304            id: output_id as u64,
305        })?;
306        // Page flip via DRM would happen here
307        Ok(())
308    }
309
310    /// Handle a DRM connector hotplug event
311    ///
312    /// If connected=true and the connector is new, adds an output.
313    /// If connected=false, removes the associated output.
314    pub fn handle_hotplug(
315        &mut self,
316        connector_id: u32,
317        connected: bool,
318        width: u32,
319        height: u32,
320        refresh_hz: u32,
321    ) -> Result<(), KernelError> {
322        if connected {
323            // Check if we already have this connector
324            let existing = self.outputs[..self.num_outputs]
325                .iter()
326                .any(|o| o.connector_id == connector_id);
327
328            if !existing {
329                self.add_output(connector_id, 0, width, height, refresh_hz)?;
330                crate::println!(
331                    "[MULTI-OUTPUT] Connector {} connected ({}x{}@{}Hz)",
332                    connector_id,
333                    width,
334                    height,
335                    refresh_hz / 1000
336                );
337            }
338        } else {
339            // Find and remove the output for this connector
340            if let Some(output_id) = self.outputs[..self.num_outputs]
341                .iter()
342                .find(|o| o.connector_id == connector_id)
343                .map(|o| o.id)
344            {
345                self.remove_output(output_id)?;
346                crate::println!("[MULTI-OUTPUT] Connector {} disconnected", connector_id);
347            }
348        }
349        Ok(())
350    }
351
352    /// Arrange outputs left-to-right based on their current order
353    pub fn auto_layout(&mut self) {
354        let mut x_offset: i32 = 0;
355        for i in 0..self.num_outputs {
356            self.outputs[i].x_offset = x_offset;
357            self.outputs[i].y_offset = 0;
358            x_offset = x_offset.saturating_add(self.outputs[i].width as i32);
359        }
360        self.recalculate_total_size();
361    }
362
363    /// Get the number of active outputs
364    pub fn num_outputs(&self) -> usize {
365        self.num_outputs
366    }
367
368    /// Check if the manager is initialized
369    pub fn is_initialized(&self) -> bool {
370        self.initialized
371    }
372
373    // ----- Private helpers -----
374
375    /// Find the index of an output by ID
376    fn find_output_index(&self, output_id: u32) -> Result<usize, KernelError> {
377        for i in 0..self.num_outputs {
378            if self.outputs[i].id == output_id {
379                return Ok(i);
380            }
381        }
382        Err(KernelError::NotFound {
383            resource: "output",
384            id: output_id as u64,
385        })
386    }
387
388    /// Recalculate the total virtual desktop size
389    fn recalculate_total_size(&mut self) {
390        let mut max_right: i32 = 0;
391        let mut max_bottom: i32 = 0;
392
393        for i in 0..self.num_outputs {
394            let output = &self.outputs[i];
395            if output.enabled {
396                let right = output.right_edge();
397                let bottom = output.bottom_edge();
398                if right > max_right {
399                    max_right = right;
400                }
401                if bottom > max_bottom {
402                    max_bottom = bottom;
403                }
404            }
405        }
406
407        self.total_width = if max_right > 0 { max_right as u32 } else { 0 };
408        self.total_height = if max_bottom > 0 { max_bottom as u32 } else { 0 };
409    }
410}
411
412// ---------------------------------------------------------------------------
413// Global Multi-Output Manager
414// ---------------------------------------------------------------------------
415
416static MULTI_OUTPUT: Mutex<MultiOutputManager> = Mutex::new(MultiOutputManager::new());
417
418static MULTI_OUTPUT_INITIALIZED: AtomicBool = AtomicBool::new(false);
419
420/// Initialize the multi-output display manager
421pub fn multi_output_init() {
422    let mut mgr = MULTI_OUTPUT.lock();
423    mgr.init();
424    MULTI_OUTPUT_INITIALIZED.store(true, Ordering::Release);
425    crate::println!(
426        "[MULTI-OUTPUT] Display manager initialized (max {} outputs)",
427        MAX_OUTPUTS
428    );
429}
430
431/// Add a display output
432pub fn multi_output_add(
433    connector_id: u32,
434    crtc_id: u32,
435    width: u32,
436    height: u32,
437    refresh_hz: u32,
438) -> Result<u32, KernelError> {
439    if !MULTI_OUTPUT_INITIALIZED.load(Ordering::Acquire) {
440        return Err(KernelError::NotInitialized {
441            subsystem: "multi_output",
442        });
443    }
444    let mut mgr = MULTI_OUTPUT.lock();
445    let id = mgr.add_output(connector_id, crtc_id, width, height, refresh_hz)?;
446    // Update CRTC ID on the output
447    if let Some(output) = mgr.get_output_mut(id) {
448        output.crtc_id = crtc_id;
449    }
450    Ok(id)
451}
452
453/// Remove a display output
454pub fn multi_output_remove(output_id: u32) -> Result<(), KernelError> {
455    if !MULTI_OUTPUT_INITIALIZED.load(Ordering::Acquire) {
456        return Err(KernelError::NotInitialized {
457            subsystem: "multi_output",
458        });
459    }
460    MULTI_OUTPUT.lock().remove_output(output_id)
461}
462
463/// Set output position in virtual desktop
464pub fn multi_output_set_position(output_id: u32, x: i32, y: i32) -> Result<(), KernelError> {
465    if !MULTI_OUTPUT_INITIALIZED.load(Ordering::Acquire) {
466        return Err(KernelError::NotInitialized {
467            subsystem: "multi_output",
468        });
469    }
470    MULTI_OUTPUT.lock().set_position(output_id, x, y)
471}
472
473/// Set the primary display output
474pub fn multi_output_set_primary(output_id: u32) -> Result<(), KernelError> {
475    if !MULTI_OUTPUT_INITIALIZED.load(Ordering::Acquire) {
476        return Err(KernelError::NotInitialized {
477            subsystem: "multi_output",
478        });
479    }
480    MULTI_OUTPUT.lock().set_primary(output_id)
481}
482
483/// Get total virtual desktop size
484pub fn multi_output_get_total_size() -> (u32, u32) {
485    if !MULTI_OUTPUT_INITIALIZED.load(Ordering::Acquire) {
486        return (0, 0);
487    }
488    MULTI_OUTPUT.lock().get_total_size()
489}
490
491/// Map a point to a specific output
492pub fn multi_output_point_to_output(x: i32, y: i32) -> Option<(u32, i32, i32)> {
493    if !MULTI_OUTPUT_INITIALIZED.load(Ordering::Acquire) {
494        return None;
495    }
496    MULTI_OUTPUT.lock().point_to_output(x, y)
497}
498
499/// Handle a DRM connector hotplug event
500pub fn multi_output_handle_hotplug(
501    connector_id: u32,
502    connected: bool,
503    width: u32,
504    height: u32,
505    refresh_hz: u32,
506) -> Result<(), KernelError> {
507    if !MULTI_OUTPUT_INITIALIZED.load(Ordering::Acquire) {
508        return Err(KernelError::NotInitialized {
509            subsystem: "multi_output",
510        });
511    }
512    MULTI_OUTPUT
513        .lock()
514        .handle_hotplug(connector_id, connected, width, height, refresh_hz)
515}
516
517/// Schedule a page flip on a specific output
518pub fn multi_output_flip(output_id: u32, buffer_id: u32) -> Result<(), KernelError> {
519    if !MULTI_OUTPUT_INITIALIZED.load(Ordering::Acquire) {
520        return Err(KernelError::NotInitialized {
521            subsystem: "multi_output",
522        });
523    }
524    MULTI_OUTPUT.lock().flip(output_id, buffer_id)
525}
526
527/// Get number of active outputs
528pub fn multi_output_count() -> usize {
529    if !MULTI_OUTPUT_INITIALIZED.load(Ordering::Acquire) {
530        return 0;
531    }
532    MULTI_OUTPUT.lock().num_outputs()
533}
534
535// ---------------------------------------------------------------------------
536// Tests
537// ---------------------------------------------------------------------------
538
539#[cfg(test)]
540mod tests {
541    use super::*;
542
543    #[test]
544    fn test_display_output_default() {
545        let output = DisplayOutput::default();
546        assert!(!output.enabled);
547        assert!(!output.primary);
548        assert_eq!(output.width, 0);
549        assert_eq!(output.height, 0);
550    }
551
552    #[test]
553    fn test_display_output_contains_point() {
554        let mut output = DisplayOutput::default();
555        output.enabled = true;
556        output.width = 1920;
557        output.height = 1080;
558        output.x_offset = 100;
559        output.y_offset = 50;
560
561        assert!(output.contains_point(100, 50));
562        assert!(output.contains_point(500, 500));
563        assert!(output.contains_point(2019, 1129));
564        assert!(!output.contains_point(99, 50));
565        assert!(!output.contains_point(100, 49));
566        assert!(!output.contains_point(2020, 500));
567    }
568
569    #[test]
570    fn test_display_output_disabled_no_contains() {
571        let mut output = DisplayOutput::default();
572        output.width = 1920;
573        output.height = 1080;
574        // Not enabled
575        assert!(!output.contains_point(500, 500));
576    }
577
578    #[test]
579    fn test_display_output_edges() {
580        let mut output = DisplayOutput::default();
581        output.x_offset = 100;
582        output.y_offset = 200;
583        output.width = 1920;
584        output.height = 1080;
585
586        assert_eq!(output.right_edge(), 2020);
587        assert_eq!(output.bottom_edge(), 1280);
588    }
589
590    #[test]
591    fn test_multi_output_manager_new() {
592        let mgr = MultiOutputManager::new();
593        assert!(!mgr.is_initialized());
594        assert_eq!(mgr.num_outputs(), 0);
595    }
596
597    #[test]
598    fn test_multi_output_manager_init() {
599        let mut mgr = MultiOutputManager::new();
600        mgr.init();
601        assert!(mgr.is_initialized());
602        assert_eq!(mgr.num_outputs(), 0);
603        assert_eq!(mgr.get_total_size(), (0, 0));
604    }
605
606    #[test]
607    fn test_add_output() {
608        let mut mgr = MultiOutputManager::new();
609        mgr.init();
610
611        let id = mgr.add_output(1, 10, 1920, 1080, 60000).unwrap();
612        assert_eq!(mgr.num_outputs(), 1);
613        assert_eq!(mgr.get_total_size(), (1920, 1080));
614
615        let output = mgr.get_output(id).unwrap();
616        assert!(output.primary);
617        assert!(output.enabled);
618        assert_eq!(output.x_offset, 0);
619    }
620
621    #[test]
622    fn test_add_two_outputs() {
623        let mut mgr = MultiOutputManager::new();
624        mgr.init();
625
626        let id1 = mgr.add_output(1, 10, 1920, 1080, 60000).unwrap();
627        let id2 = mgr.add_output(2, 20, 2560, 1440, 60000).unwrap();
628
629        assert_eq!(mgr.num_outputs(), 2);
630        assert_eq!(mgr.get_total_size(), (4480, 1440));
631
632        let o1 = mgr.get_output(id1).unwrap();
633        assert_eq!(o1.x_offset, 0);
634        assert!(o1.primary);
635
636        let o2 = mgr.get_output(id2).unwrap();
637        assert_eq!(o2.x_offset, 1920);
638        assert!(!o2.primary);
639    }
640
641    #[test]
642    fn test_remove_output() {
643        let mut mgr = MultiOutputManager::new();
644        mgr.init();
645
646        let id1 = mgr.add_output(1, 10, 1920, 1080, 60000).unwrap();
647        let _id2 = mgr.add_output(2, 20, 2560, 1440, 60000).unwrap();
648
649        mgr.remove_output(id1).unwrap();
650        assert_eq!(mgr.num_outputs(), 1);
651    }
652
653    #[test]
654    fn test_remove_nonexistent() {
655        let mut mgr = MultiOutputManager::new();
656        mgr.init();
657
658        assert!(mgr.remove_output(999).is_err());
659    }
660
661    #[test]
662    fn test_max_outputs() {
663        let mut mgr = MultiOutputManager::new();
664        mgr.init();
665
666        for i in 0..MAX_OUTPUTS {
667            assert!(mgr
668                .add_output(i as u32, i as u32 * 10, 1920, 1080, 60000)
669                .is_ok());
670        }
671        // 9th should fail
672        assert!(mgr.add_output(99, 990, 1920, 1080, 60000).is_err());
673    }
674
675    #[test]
676    fn test_set_position() {
677        let mut mgr = MultiOutputManager::new();
678        mgr.init();
679
680        let id = mgr.add_output(1, 10, 1920, 1080, 60000).unwrap();
681        mgr.set_position(id, 500, 300).unwrap();
682
683        let output = mgr.get_output(id).unwrap();
684        assert_eq!(output.x_offset, 500);
685        assert_eq!(output.y_offset, 300);
686    }
687
688    #[test]
689    fn test_set_primary() {
690        let mut mgr = MultiOutputManager::new();
691        mgr.init();
692
693        let id1 = mgr.add_output(1, 10, 1920, 1080, 60000).unwrap();
694        let id2 = mgr.add_output(2, 20, 2560, 1440, 60000).unwrap();
695
696        mgr.set_primary(id2).unwrap();
697
698        assert!(!mgr.get_output(id1).unwrap().primary);
699        assert!(mgr.get_output(id2).unwrap().primary);
700    }
701
702    #[test]
703    fn test_point_to_output() {
704        let mut mgr = MultiOutputManager::new();
705        mgr.init();
706
707        let id1 = mgr.add_output(1, 10, 1920, 1080, 60000).unwrap();
708        let id2 = mgr.add_output(2, 20, 2560, 1440, 60000).unwrap();
709
710        // Point in first output
711        let result = mgr.point_to_output(500, 500);
712        assert!(result.is_some());
713        let (oid, lx, ly) = result.unwrap();
714        assert_eq!(oid, id1);
715        assert_eq!(lx, 500);
716        assert_eq!(ly, 500);
717
718        // Point in second output
719        let result = mgr.point_to_output(2000, 500);
720        assert!(result.is_some());
721        let (oid, lx, _ly) = result.unwrap();
722        assert_eq!(oid, id2);
723        assert_eq!(lx, 80); // 2000 - 1920
724
725        // Point outside all outputs
726        assert!(mgr.point_to_output(5000, 5000).is_none());
727    }
728
729    #[test]
730    fn test_auto_layout() {
731        let mut mgr = MultiOutputManager::new();
732        mgr.init();
733
734        let id1 = mgr.add_output(1, 10, 1920, 1080, 60000).unwrap();
735        let id2 = mgr.add_output(2, 20, 2560, 1440, 60000).unwrap();
736
737        // Move outputs to weird positions
738        mgr.set_position(id1, 500, 300).unwrap();
739        mgr.set_position(id2, -100, 200).unwrap();
740
741        // Auto-layout should reset to left-to-right
742        mgr.auto_layout();
743
744        let o1 = mgr.get_output(id1).unwrap();
745        assert_eq!(o1.x_offset, 0);
746        assert_eq!(o1.y_offset, 0);
747
748        let o2 = mgr.get_output(id2).unwrap();
749        assert_eq!(o2.x_offset, 1920);
750        assert_eq!(o2.y_offset, 0);
751    }
752
753    #[test]
754    fn test_handle_hotplug_connect() {
755        let mut mgr = MultiOutputManager::new();
756        mgr.init();
757
758        mgr.handle_hotplug(1, true, 1920, 1080, 60000).unwrap();
759        assert_eq!(mgr.num_outputs(), 1);
760    }
761
762    #[test]
763    fn test_handle_hotplug_disconnect() {
764        let mut mgr = MultiOutputManager::new();
765        mgr.init();
766
767        mgr.handle_hotplug(1, true, 1920, 1080, 60000).unwrap();
768        assert_eq!(mgr.num_outputs(), 1);
769
770        mgr.handle_hotplug(1, false, 0, 0, 0).unwrap();
771        assert_eq!(mgr.num_outputs(), 0);
772    }
773
774    #[test]
775    fn test_handle_hotplug_duplicate_connect() {
776        let mut mgr = MultiOutputManager::new();
777        mgr.init();
778
779        mgr.handle_hotplug(1, true, 1920, 1080, 60000).unwrap();
780        mgr.handle_hotplug(1, true, 1920, 1080, 60000).unwrap();
781        assert_eq!(mgr.num_outputs(), 1); // should not duplicate
782    }
783
784    #[test]
785    fn test_flip() {
786        let mut mgr = MultiOutputManager::new();
787        mgr.init();
788
789        let id = mgr.add_output(1, 10, 1920, 1080, 60000).unwrap();
790        assert!(mgr.flip(id, 0).is_ok());
791        assert!(mgr.flip(999, 0).is_err());
792    }
793
794    #[test]
795    fn test_primary_output() {
796        let mut mgr = MultiOutputManager::new();
797        mgr.init();
798
799        assert!(mgr.primary_output().is_none());
800
801        mgr.add_output(1, 10, 1920, 1080, 60000).unwrap();
802        assert!(mgr.primary_output().is_some());
803        assert!(mgr.primary_output().unwrap().primary);
804    }
805
806    #[test]
807    fn test_primary_promotion_on_remove() {
808        let mut mgr = MultiOutputManager::new();
809        mgr.init();
810
811        let id1 = mgr.add_output(1, 10, 1920, 1080, 60000).unwrap();
812        let _id2 = mgr.add_output(2, 20, 2560, 1440, 60000).unwrap();
813
814        // Remove primary
815        mgr.remove_output(id1).unwrap();
816
817        // Second output should be promoted
818        assert!(mgr.primary_output().is_some());
819        assert!(mgr.primary_output().unwrap().primary);
820    }
821}