redox-wayland-compositor/crates/redox-wl-compositor-core/src/lib.rs
Votre Nom 509aae7769 🎉 Phase 6.3 — display + input + compositor-core intégrés runtime
Captures preuves dans docs/phase6-3-*.png : 4 frames qui prouvent
visuellement que raise change l'ordre Z et que compose_into propage le
résultat à l'écran QEMU :
- default-z.png : 3 surfaces overlap, blue top (créé en dernier)
- red-top.png : sendkey 1 → raise(red) → red couvre vert et bleu
- green-top.png : sendkey 2 → raise(green) → green couvre tout dans sa zone
- blue-top.png : sendkey 3 → raise(blue) → retour visuel à initial

Modifications :

compositor-core (commit dbf3bff → maintenant) :
- + iter_z_order_front_to_back() : utile pour hit testing
- + hit_test(x, y) -> Option<SurfaceId> : trouve la surface visible la
  plus haute qui contient le point
- + 4 tests unitaires : 27 total / 27 pass natif (0.00s)

redox-wl-display :
- + dep redox-wl-compositor-core
- + impl Framebuffer for RedoxOutput (délègue à pixels_mut + width/height)

bin redox-wl-test-compose-static (190 lignes) :
- ouvre RedoxOutput + take_crtc
- crée InputBackend partagé
- 3 surfaces ARGB unies (rouge/vert/bleu) avec overlap centré
- boucle event : '1'/'2'/'3' raise resp. red/green/blue
- clic souris → hit_test puis raise (motion non testé sans usb-tablet)
- ré-render seulement si raise → économie CPU
- present_with_takeover() à chaque iter pour tenir le CRTC

Validation QEMU automatisée : sendkey 1/2/3 + screendump entre chaque.
Les 4 PNG montrent l'ordre Z évoluer correctement.

Image Redox restaurée à boot Orbital normal.

docs/phase6-compositor-core.md : compte-rendu 6.1-6.3 complet,
architecture, dépendances, API, limitations, plan 6.4.

Phase 6.3 close. Reste 6.4 (frontend Wayland : wl_compositor + wl_shm
+ xdg-shell mappés vers compositor-core, damage tracking, frame
callbacks). Estimé 2-3 sessions.

Leyoda 2026 – GPLv3
2026-05-09 12:20:04 +02:00

869 lines
28 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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<SurfaceId>` 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<u32>` 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<Vec<u32>>,
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<u32>) -> 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<SurfaceBuffer>,
/// 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<SurfaceId>` allant du plus
/// bas (index 0) au plus haut (dernier index = au premier plan).
#[derive(Debug, Default)]
pub struct SurfaceRegistry {
surfaces: HashMap<SurfaceId, Surface>,
z_order: Vec<SurfaceId>,
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<Item = &Surface> {
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<Item = &Surface> {
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<SurfaceId> {
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<F: FnOnce(&mut SurfaceState)>(
&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<F: Framebuffer + ?Sized>(&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<SurfaceId> = 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<SurfaceId> = 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<SurfaceId> = 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<SurfaceId> = 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<SurfaceId> = 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<u32> aligné row-major (pas de stride).
struct MockFb {
w: u32,
h: u32,
pixels: Vec<u32>,
}
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<SurfaceId> = 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<SurfaceId> = 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<SurfaceId> = 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());
}
}
}