//! Phase 6.1 — Core compositor structures. //! //! Pure logique : surfaces, registry, z-order. Aucune dépendance Redox, //! Wayland ou backend display/input. Le composition pipeline (parcourir les //! surfaces dans l'ordre Z et copier les buffers dans un framebuffer) est //! reporté à 6.2 ; le frontend Wayland (mappe `wl_compositor` / //! `wl_shm` / `wl_surface` vers ce core) à 6.4. //! //! ## Modèle d'état //! //! Chaque `Surface` a deux états : `current` (ce qui est affiché) et //! `pending` (modifications en cours). `commit()` copie pending → current. //! C'est la sémantique Wayland : un client peut faire `set_position + //! set_buffer + ...` puis `commit()` pour appliquer atomiquement. //! //! Pour 6.1 l'implémentation est triviale (clone), pas de double-buffer //! atomique. Suffisant tant qu'on n'a qu'un thread compositor. //! //! ## Z-order //! //! Maintenu par le `SurfaceRegistry` sous forme de `Vec` du //! plus bas (index 0) au plus haut (dernier). `raise()` retire et pousse //! en fin. Pas de modal / always-on-top à ce stade — la politique sera //! ajoutée au-dessus quand on aura un cas d'usage. #![forbid(unsafe_code)] use std::collections::HashMap; use std::sync::Arc; /// Identifiant opaque d'une surface. /// /// Newtype pour éviter de mélanger avec d'autres `usize`. Réutilisable /// après destruction (le registry ne garantit pas l'unicité absolue ; /// c'est au caller de ne pas réutiliser un id qu'il a `destroy()`é). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct SurfaceId(u64); impl SurfaceId { pub fn raw(self) -> u64 { self.0 } } /// Buffer ARGB partagé pour une surface. /// /// `Arc<...>` pour permettre à plusieurs sites (registry, composition, /// futur frame callback) de partager sans copier. Le contenu est /// supposé déjà au format ARGB8888 (a en haut, b en bas dans un u32 LE). /// /// Pour 6.1 on utilise un `Vec` simple. En 6.4, le frontend Wayland /// remplacera ça par un wrapper sur `wl_shm_pool` mmap'd, sans changer /// l'API : seul le constructeur diffère. #[derive(Debug, Clone)] pub struct SurfaceBuffer { pub pixels: Arc>, pub width: u32, pub height: u32, } impl SurfaceBuffer { pub fn new_filled(width: u32, height: u32, color: u32) -> Self { let n = (width as usize) * (height as usize); Self { pixels: Arc::new(vec![color; n]), width, height, } } pub fn from_pixels(width: u32, height: u32, pixels: Vec) -> Self { debug_assert_eq!(pixels.len(), (width as usize) * (height as usize)); Self { pixels: Arc::new(pixels), width, height, } } } /// État applicable à une surface : position dans l'écran + buffer + visibilité. /// /// L'état est dupliqué entre `pending` et `current` au sein de la struct /// `Surface`. `commit()` propage pending → current. #[derive(Debug, Clone, Default)] pub struct SurfaceState { /// Position absolue du coin haut-gauche dans le framebuffer cible. pub x: i32, pub y: i32, /// Buffer ARGB. `None` = surface vide (rendue noire ou skip). pub buffer: Option, /// Si `false`, la surface est ignorée pendant la composition. pub visible: bool, } #[derive(Debug)] pub struct Surface { id: SurfaceId, /// État actuellement affiché. current: SurfaceState, /// État en cours de modification ; appliqué par `commit()`. pending: SurfaceState, } impl Surface { fn new(id: SurfaceId) -> Self { Self { id, current: SurfaceState::default(), pending: SurfaceState::default(), } } pub fn id(&self) -> SurfaceId { self.id } pub fn current(&self) -> &SurfaceState { &self.current } pub fn pending(&self) -> &SurfaceState { &self.pending } pub fn pending_mut(&mut self) -> &mut SurfaceState { &mut self.pending } /// Applique `pending` → `current`. Sémantique Wayland : /// les modifications sont visibles seulement après commit. pub fn commit(&mut self) { self.current = self.pending.clone(); } } /// Registre central de toutes les surfaces + leur ordre Z. /// /// L'ordre Z est maintenu par `z_order: Vec` allant du plus /// bas (index 0) au plus haut (dernier index = au premier plan). #[derive(Debug, Default)] pub struct SurfaceRegistry { surfaces: HashMap, z_order: Vec, next_id: u64, } impl SurfaceRegistry { pub fn new() -> Self { Self::default() } /// Crée une nouvelle surface, l'insère au-dessus (top du Z-order). pub fn create(&mut self) -> SurfaceId { let id = SurfaceId(self.next_id); self.next_id += 1; self.surfaces.insert(id, Surface::new(id)); self.z_order.push(id); id } /// Détruit la surface ; le SurfaceId ne doit plus être utilisé. pub fn destroy(&mut self, id: SurfaceId) -> bool { let removed = self.surfaces.remove(&id).is_some(); self.z_order.retain(|&s| s != id); removed } /// Amène la surface au premier plan (top du Z-order). No-op si absent. pub fn raise(&mut self, id: SurfaceId) { if !self.surfaces.contains_key(&id) { return; } self.z_order.retain(|&s| s != id); self.z_order.push(id); } pub fn get(&self, id: SurfaceId) -> Option<&Surface> { self.surfaces.get(&id) } pub fn get_mut(&mut self, id: SurfaceId) -> Option<&mut Surface> { self.surfaces.get_mut(&id) } /// Itérer les surfaces dans l'ordre Z, du fond vers l'avant. /// C'est l'ordre de composition : on peint d'abord le fond, puis /// chaque surface au-dessus. pub fn iter_z_order_back_to_front(&self) -> impl Iterator { self.z_order .iter() .filter_map(|id| self.surfaces.get(id)) } /// Itérer les surfaces dans l'ordre Z, du premier plan vers le fond. /// Utile pour le hit testing : on cherche la première surface /// (la plus haute) qui contient le point. pub fn iter_z_order_front_to_back(&self) -> impl Iterator { self.z_order .iter() .rev() .filter_map(|id| self.surfaces.get(id)) } /// Trouve la surface visible la plus haute qui contient le point `(x, y)` /// dans son rect [state.x..state.x+buffer.width) × [state.y..state.y+buffer.height). /// Retourne `None` si aucune surface ne couvre ce point. /// Surfaces invisibles ou sans buffer sont ignorées. pub fn hit_test(&self, x: i32, y: i32) -> Option { for surface in self.iter_z_order_front_to_back() { let s = surface.current(); if !s.visible { continue; } let Some(buf) = &s.buffer else { continue; }; let x0 = s.x; let y0 = s.y; let x1 = x0.saturating_add(buf.width as i32); let y1 = y0.saturating_add(buf.height as i32); if x >= x0 && x < x1 && y >= y0 && y < y1 { return Some(surface.id()); } } None } pub fn len(&self) -> usize { self.surfaces.len() } pub fn is_empty(&self) -> bool { self.surfaces.is_empty() } /// Convenience : applique pending → current sur une surface donnée. /// Retourne `false` si la surface n'existe pas. pub fn commit(&mut self, id: SurfaceId) -> bool { match self.surfaces.get_mut(&id) { Some(s) => { s.commit(); true } None => false, } } /// Convenience : modifie le pending state via une closure. /// Pratique pour `set_position`, `set_buffer` etc. dans les tests. pub fn modify_pending( &mut self, id: SurfaceId, f: F, ) -> bool { match self.surfaces.get_mut(&id) { Some(s) => { f(&mut s.pending); true } None => false, } } /// Compose les surfaces visibles dans le `Framebuffer` cible. /// /// Itère du fond vers l'avant (z-order), et copie chaque buffer ARGB /// à la position `(state.x, state.y)` du framebuffer cible. Les pixels /// hors limites du framebuffer sont clippés. Les surfaces sans buffer /// ou marquées `visible = false` sont ignorées. /// /// **Pas de blending alpha en 6.2** : overwrite pur. Si tu veux un /// fond, peins-le directement dans le framebuffer avant cet appel. /// L'alpha viendra en 6.4 si nécessaire (Wayland clients peuvent /// produire des surfaces transparentes via `wl_compositor.create_region`). /// /// **Pas de damage tracking** : recompose tout. Suffisant pour 2-3 /// surfaces ; à reconsidérer en 6.4 quand le frontend Wayland /// fournira `wl_surface.damage_buffer`. pub fn compose_into(&self, target: &mut F) { let fb_w = target.width() as i32; let fb_h = target.height() as i32; if fb_w <= 0 || fb_h <= 0 { return; } let fb_w_us = fb_w as usize; for surface in self.iter_z_order_back_to_front() { let state = surface.current(); if !state.visible { continue; } let Some(buf) = &state.buffer else { continue; }; let s_w = buf.width as i32; let s_h = buf.height as i32; if s_w <= 0 || s_h <= 0 { continue; } // Rectangle de la surface dans les coords du framebuffer. let surf_x0 = state.x; let surf_y0 = state.y; let surf_x1 = surf_x0.saturating_add(s_w); let surf_y1 = surf_y0.saturating_add(s_h); // Intersection avec le framebuffer. let dst_x0 = surf_x0.max(0); let dst_y0 = surf_y0.max(0); let dst_x1 = surf_x1.min(fb_w); let dst_y1 = surf_y1.min(fb_h); if dst_x0 >= dst_x1 || dst_y0 >= dst_y1 { continue; // surface entièrement hors écran } // Offset dans la surface (clip négatif sur le coin haut-gauche). let src_x0 = (dst_x0 - surf_x0) as usize; let src_y0 = (dst_y0 - surf_y0) as usize; let copy_w = (dst_x1 - dst_x0) as usize; let copy_h = (dst_y1 - dst_y0) as usize; let s_w_us = s_w as usize; let dst_pixels = target.pixels_mut(); let src_pixels = buf.pixels.as_ref(); for row in 0..copy_h { let src_row_start = (src_y0 + row) * s_w_us + src_x0; let dst_y = (dst_y0 as usize) + row; let dst_row_start = dst_y * fb_w_us + (dst_x0 as usize); let src_slice = &src_pixels[src_row_start..src_row_start + copy_w]; let dst_slice = &mut dst_pixels[dst_row_start..dst_row_start + copy_w]; dst_slice.copy_from_slice(src_slice); } } } } /// Cible de composition. Implémenté par les backends display /// (`redox-wl-display::RedoxOutput` plus tard) et par les mocks de test. /// /// `pixels_mut()` retourne un buffer linéaire ARGB8888 de taille exacte /// `width * height` u32. Le format mémoire est row-major sans padding ; /// si un backend a un stride différent il doit faire son propre /// arrangement avant l'appel. pub trait Framebuffer { fn width(&self) -> u32; fn height(&self) -> u32; fn pixels_mut(&mut self) -> &mut [u32]; } #[cfg(test)] mod tests { use super::*; #[test] fn create_returns_unique_ids() { let mut r = SurfaceRegistry::new(); let a = r.create(); let b = r.create(); let c = r.create(); assert_ne!(a, b); assert_ne!(b, c); assert_ne!(a, c); assert_eq!(r.len(), 3); } #[test] fn destroy_removes_surface_and_zorder_entry() { let mut r = SurfaceRegistry::new(); let a = r.create(); let b = r.create(); assert_eq!(r.len(), 2); assert!(r.destroy(a)); assert_eq!(r.len(), 1); assert!(r.get(a).is_none()); assert!(r.get(b).is_some()); // Re-destroy returns false (no-op) assert!(!r.destroy(a)); } #[test] fn z_order_initial_is_creation_order_back_to_front() { let mut r = SurfaceRegistry::new(); let a = r.create(); let b = r.create(); let c = r.create(); let order: Vec = r .iter_z_order_back_to_front() .map(|s| s.id()) .collect(); assert_eq!(order, vec![a, b, c]); // a au fond, c devant } #[test] fn raise_brings_surface_to_top() { let mut r = SurfaceRegistry::new(); let a = r.create(); let b = r.create(); let c = r.create(); r.raise(a); // a passe au-dessus let order: Vec = r .iter_z_order_back_to_front() .map(|s| s.id()) .collect(); assert_eq!(order, vec![b, c, a]); } #[test] fn raise_is_idempotent_on_top_surface() { let mut r = SurfaceRegistry::new(); let a = r.create(); let b = r.create(); r.raise(b); r.raise(b); // déjà au top let order: Vec = r .iter_z_order_back_to_front() .map(|s| s.id()) .collect(); assert_eq!(order, vec![a, b]); } #[test] fn raise_unknown_id_is_noop() { let mut r = SurfaceRegistry::new(); let a = r.create(); let bogus = SurfaceId(999); r.raise(bogus); // ne doit pas paniquer ni modifier z_order let order: Vec = r .iter_z_order_back_to_front() .map(|s| s.id()) .collect(); assert_eq!(order, vec![a]); } #[test] fn pending_state_is_separate_from_current() { let mut r = SurfaceRegistry::new(); let a = r.create(); r.modify_pending(a, |s| { s.x = 100; s.y = 200; s.visible = true; }); let s = r.get(a).unwrap(); assert_eq!(s.current().x, 0); // pas encore commité assert_eq!(s.current().y, 0); assert!(!s.current().visible); assert_eq!(s.pending().x, 100); assert_eq!(s.pending().y, 200); assert!(s.pending().visible); } #[test] fn commit_propagates_pending_to_current() { let mut r = SurfaceRegistry::new(); let a = r.create(); r.modify_pending(a, |s| { s.x = 42; s.y = 73; s.visible = true; s.buffer = Some(SurfaceBuffer::new_filled(10, 10, 0xFFFF0000)); }); assert!(r.commit(a)); let s = r.get(a).unwrap(); assert_eq!(s.current().x, 42); assert_eq!(s.current().y, 73); assert!(s.current().visible); assert!(s.current().buffer.is_some()); // Le pending state reste — un commit() suivant ré-appliquera la même // chose si on ne modifie pas pending entre-temps. C'est le comportement // Wayland : pending n'est pas vidé après commit. assert_eq!(s.pending().x, 42); } #[test] fn commit_unknown_id_returns_false() { let mut r = SurfaceRegistry::new(); assert!(!r.commit(SurfaceId(0))); } #[test] fn surface_buffer_filled_has_correct_size() { let buf = SurfaceBuffer::new_filled(4, 3, 0xDEADBEEF); assert_eq!(buf.width, 4); assert_eq!(buf.height, 3); assert_eq!(buf.pixels.len(), 12); assert!(buf.pixels.iter().all(|&p| p == 0xDEADBEEF)); } #[test] fn destroyed_surface_is_skipped_during_iteration() { let mut r = SurfaceRegistry::new(); let a = r.create(); let b = r.create(); let c = r.create(); r.destroy(b); let ids: Vec = r .iter_z_order_back_to_front() .map(|s| s.id()) .collect(); assert_eq!(ids, vec![a, c]); } /// Framebuffer mock pour les tests de composition. Stocke les pixels /// dans un Vec aligné row-major (pas de stride). struct MockFb { w: u32, h: u32, pixels: Vec, } impl MockFb { fn new(w: u32, h: u32, fill: u32) -> Self { let n = (w as usize) * (h as usize); Self { w, h, pixels: vec![fill; n], } } fn pixel_at(&self, x: u32, y: u32) -> u32 { self.pixels[(y as usize) * (self.w as usize) + (x as usize)] } } impl Framebuffer for MockFb { fn width(&self) -> u32 { self.w } fn height(&self) -> u32 { self.h } fn pixels_mut(&mut self) -> &mut [u32] { &mut self.pixels } } /// Helper : crée une surface visible avec un buffer plein de `color`, /// commit et retourne son SurfaceId. fn add_surface( r: &mut SurfaceRegistry, x: i32, y: i32, w: u32, h: u32, color: u32, ) -> SurfaceId { let id = r.create(); r.modify_pending(id, |s| { s.x = x; s.y = y; s.visible = true; s.buffer = Some(SurfaceBuffer::new_filled(w, h, color)); }); r.commit(id); id } const BG: u32 = 0xCAFEBABE; #[test] fn compose_empty_registry_keeps_fb_unchanged() { let r = SurfaceRegistry::new(); let mut fb = MockFb::new(10, 10, BG); r.compose_into(&mut fb); assert!(fb.pixels.iter().all(|&p| p == BG)); } #[test] fn compose_one_fullscreen_surface_fills_fb() { let mut r = SurfaceRegistry::new(); add_surface(&mut r, 0, 0, 10, 10, 0xFFFF0000); let mut fb = MockFb::new(10, 10, BG); r.compose_into(&mut fb); assert!(fb.pixels.iter().all(|&p| p == 0xFFFF0000)); } #[test] fn compose_partial_surface_only_modifies_its_rect() { let mut r = SurfaceRegistry::new(); // Surface 4x3 à (2, 1) — couvre les pixels [2..6) × [1..4) add_surface(&mut r, 2, 1, 4, 3, 0xFF00FF00); let mut fb = MockFb::new(10, 10, BG); r.compose_into(&mut fb); // Vérifie pixel par pixel for y in 0..10u32 { for x in 0..10u32 { let expected = if (2..6).contains(&x) && (1..4).contains(&y) { 0xFF00FF00 } else { BG }; let got = fb.pixel_at(x, y); assert_eq!(got, expected, "mismatch at ({x},{y})"); } } } #[test] fn compose_clips_at_right_edge() { let mut r = SurfaceRegistry::new(); // Surface 5x5 à (8, 8) dans un fb 10x10 : déborde à droite de 3 et en bas de 3 add_surface(&mut r, 8, 8, 5, 5, 0xFF0000FF); let mut fb = MockFb::new(10, 10, BG); r.compose_into(&mut fb); // Doit avoir peint [8..10) × [8..10) seulement (4 pixels) for y in 0..10u32 { for x in 0..10u32 { let expected = if (8..10).contains(&x) && (8..10).contains(&y) { 0xFF0000FF } else { BG }; assert_eq!(fb.pixel_at(x, y), expected, "mismatch at ({x},{y})"); } } } #[test] fn compose_clips_at_left_top_with_negative_position() { let mut r = SurfaceRegistry::new(); // Surface 4x4 à (-2, -1) : seul le rect [0..2) × [0..3) doit être peint add_surface(&mut r, -2, -1, 4, 4, 0xFFFFFF00); let mut fb = MockFb::new(10, 10, BG); r.compose_into(&mut fb); for y in 0..10u32 { for x in 0..10u32 { let expected = if (0..2).contains(&x) && (0..3).contains(&y) { 0xFFFFFF00 } else { BG }; assert_eq!(fb.pixel_at(x, y), expected, "mismatch at ({x},{y})"); } } } #[test] fn compose_skips_offscreen_surface() { let mut r = SurfaceRegistry::new(); // Surface 3x3 entièrement à droite du fb 10x10 (à x=15) add_surface(&mut r, 15, 5, 3, 3, 0xFFFF00FF); let mut fb = MockFb::new(10, 10, BG); r.compose_into(&mut fb); assert!(fb.pixels.iter().all(|&p| p == BG)); } #[test] fn compose_skips_invisible_surface() { let mut r = SurfaceRegistry::new(); let id = add_surface(&mut r, 0, 0, 10, 10, 0xFFFF0000); // Override : invisible r.modify_pending(id, |s| s.visible = false); r.commit(id); let mut fb = MockFb::new(10, 10, BG); r.compose_into(&mut fb); assert!(fb.pixels.iter().all(|&p| p == BG)); } #[test] fn compose_skips_surface_without_buffer() { let mut r = SurfaceRegistry::new(); let id = r.create(); r.modify_pending(id, |s| { s.visible = true; s.x = 0; s.y = 0; // pas de buffer }); r.commit(id); let mut fb = MockFb::new(5, 5, BG); r.compose_into(&mut fb); assert!(fb.pixels.iter().all(|&p| p == BG)); } #[test] fn compose_respects_z_order_top_overwrites_bottom() { let mut r = SurfaceRegistry::new(); // bg rouge plein écran add_surface(&mut r, 0, 0, 5, 5, 0xFFFF0000); // overlay bleu 3x3 à (1, 1) — créé après donc au-dessus add_surface(&mut r, 1, 1, 3, 3, 0xFF0000FF); let mut fb = MockFb::new(5, 5, BG); r.compose_into(&mut fb); for y in 0..5u32 { for x in 0..5u32 { let expected = if (1..4).contains(&x) && (1..4).contains(&y) { 0xFF0000FF // bleu au-dessus } else { 0xFFFF0000 // rouge dessous }; assert_eq!(fb.pixel_at(x, y), expected); } } } #[test] fn compose_after_raise_changes_visible_overlap() { let mut r = SurfaceRegistry::new(); let red = add_surface(&mut r, 0, 0, 5, 5, 0xFFFF0000); let blue = add_surface(&mut r, 1, 1, 3, 3, 0xFF0000FF); // Premier rendu : bleu au-dessus let mut fb1 = MockFb::new(5, 5, BG); r.compose_into(&mut fb1); assert_eq!(fb1.pixel_at(2, 2), 0xFF0000FF); // raise red → red passe au-dessus r.raise(red); let mut fb2 = MockFb::new(5, 5, BG); r.compose_into(&mut fb2); // Maintenant le rouge couvre tout, y compris le bleu assert_eq!(fb2.pixel_at(2, 2), 0xFFFF0000); let _ = blue; // tag "fait expressément" } #[test] fn compose_uses_current_state_not_pending() { let mut r = SurfaceRegistry::new(); let id = add_surface(&mut r, 0, 0, 5, 5, 0xFFFF0000); // Modifier pending sans commit r.modify_pending(id, |s| { s.buffer = Some(SurfaceBuffer::new_filled(5, 5, 0xFF00FF00)); }); let mut fb = MockFb::new(5, 5, BG); r.compose_into(&mut fb); // Doit toujours être rouge (current), pas vert (pending) assert!(fb.pixels.iter().all(|&p| p == 0xFFFF0000)); // Après commit, vert r.commit(id); let mut fb2 = MockFb::new(5, 5, BG); r.compose_into(&mut fb2); assert!(fb2.pixels.iter().all(|&p| p == 0xFF00FF00)); } #[test] fn iter_z_order_front_to_back_is_reverse() { let mut r = SurfaceRegistry::new(); let a = r.create(); let b = r.create(); let c = r.create(); let order: Vec = r .iter_z_order_front_to_back() .map(|s| s.id()) .collect(); assert_eq!(order, vec![c, b, a]); } #[test] fn hit_test_returns_topmost_surface_at_point() { let mut r = SurfaceRegistry::new(); // bg : 100x100 à (0, 0) let bg = add_surface(&mut r, 0, 0, 100, 100, 0xFFFF0000); // overlay : 30x30 à (10, 10) let overlay = add_surface(&mut r, 10, 10, 30, 30, 0xFF0000FF); // Point (5, 5) : dans bg uniquement assert_eq!(r.hit_test(5, 5), Some(bg)); // Point (20, 20) : dans les deux, overlay au-dessus → overlay assert_eq!(r.hit_test(20, 20), Some(overlay)); // Point (50, 50) : dans bg seulement assert_eq!(r.hit_test(50, 50), Some(bg)); // Point (200, 200) : hors écran assert_eq!(r.hit_test(200, 200), None); // Point (-1, -1) : hors écran (négatif) assert_eq!(r.hit_test(-1, -1), None); } #[test] fn hit_test_skips_invisible_and_no_buffer() { let mut r = SurfaceRegistry::new(); let a = add_surface(&mut r, 0, 0, 100, 100, 0xFFFF0000); let b = add_surface(&mut r, 0, 0, 100, 100, 0xFF00FF00); // Cache b → hit_test au-dessus de la zone doit retourner a r.modify_pending(b, |s| s.visible = false); r.commit(b); assert_eq!(r.hit_test(50, 50), Some(a)); // c sans buffer let c = r.create(); r.modify_pending(c, |s| { s.x = 0; s.y = 0; s.visible = true; }); r.commit(c); // toujours a (c n'a pas de buffer donc skip) assert_eq!(r.hit_test(50, 50), Some(a)); } #[test] fn hit_test_after_raise_returns_raised_surface() { let mut r = SurfaceRegistry::new(); let a = add_surface(&mut r, 0, 0, 100, 100, 0xFFFF0000); let b = add_surface(&mut r, 0, 0, 100, 100, 0xFF00FF00); // b est au-dessus initialement assert_eq!(r.hit_test(50, 50), Some(b)); r.raise(a); // a passe au-dessus assert_eq!(r.hit_test(50, 50), Some(a)); } #[test] fn typical_compositor_workflow() { // Scénario : 3 fenêtres, on les modifie + commit, on raise une au top let mut r = SurfaceRegistry::new(); let bg = r.create(); let fg1 = r.create(); let fg2 = r.create(); // Configure background r.modify_pending(bg, |s| { s.x = 0; s.y = 0; s.visible = true; s.buffer = Some(SurfaceBuffer::new_filled(800, 600, 0xFF202020)); }); r.commit(bg); // Window 1 (rouge) à (100, 100) 200x150 r.modify_pending(fg1, |s| { s.x = 100; s.y = 100; s.visible = true; s.buffer = Some(SurfaceBuffer::new_filled(200, 150, 0xFFFF0000)); }); r.commit(fg1); // Window 2 (verte) à (250, 200) 200x150 — chevauche partiellement fg1 r.modify_pending(fg2, |s| { s.x = 250; s.y = 200; s.visible = true; s.buffer = Some(SurfaceBuffer::new_filled(200, 150, 0xFF00FF00)); }); r.commit(fg2); // L'ordre attendu pour la composition : bg → fg1 → fg2 (vert au-dessus) let ids: Vec = r .iter_z_order_back_to_front() .map(|s| s.id()) .collect(); assert_eq!(ids, vec![bg, fg1, fg2]); // Click sur fg1 → raise → fg1 passe au-dessus r.raise(fg1); let ids: Vec = r .iter_z_order_back_to_front() .map(|s| s.id()) .collect(); assert_eq!(ids, vec![bg, fg2, fg1]); // Vérifie que le current state est bien défini après les commits for &id in &[bg, fg1, fg2] { let s = r.get(id).unwrap(); assert!(s.current().visible); assert!(s.current().buffer.is_some()); } } }