Sprint 0 — industrialisation : CI, tracing, tests xdg-shell, GPLv3

5 livrables d'industrialisation posés avant la phase 13 (client réel).

- .gitlab-ci.yml : pipeline minimal (fmt-check + tests core +
  tests frontend nightly). Pas de cross-compile Redox dans le MVP.
- rustfmt.toml + fmt.sh : config formatter racine + boucle sur les 23
  crates ; fmt sweep appliqué (d'où les diffs cosmétiques).
- tracing dans wayland-frontend : 22 println/eprintln remplacés par
  debug/info/warn/error selon sévérité. Préfixe "[frontend]"
  supprimé (le target tracing le fournit).
- tracing-subscriber dans le compositor : TeeWriter écrit sur
  stdout + /scheme/debug, filtre via RUST_LOG (défaut info).
  DebugSink/dlog supprimés.
- 15 tests unitaires xdg-shell après extraction de 2 helpers libres
  (clamp_to_min_max + should_throttle_configure). Couvre
  compute_resize_geom, contraintes min/max et throttling configure.
- LICENSE (GPLv3 texte officiel FSF) + .editorconfig.

Total tests : 27 → 42 automatisés (compositor-core + frontend).

Leyoda 2026 – GPLv3
This commit is contained in:
Votre Nom 2026-05-14 20:46:07 +02:00
parent 2150f31d82
commit 8795f39f08
30 changed files with 1373 additions and 298 deletions

View file

@ -187,9 +187,7 @@ impl SurfaceRegistry {
/// 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))
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.
@ -248,11 +246,7 @@ impl SurfaceRegistry {
/// 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 {
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);
@ -385,10 +379,7 @@ mod tests {
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();
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
}
@ -399,10 +390,7 @@ mod tests {
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();
let order: Vec<SurfaceId> = r.iter_z_order_back_to_front().map(|s| s.id()).collect();
assert_eq!(order, vec![b, c, a]);
}
@ -413,10 +401,7 @@ mod tests {
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();
let order: Vec<SurfaceId> = r.iter_z_order_back_to_front().map(|s| s.id()).collect();
assert_eq!(order, vec![a, b]);
}
@ -426,10 +411,7 @@ mod tests {
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();
let order: Vec<SurfaceId> = r.iter_z_order_back_to_front().map(|s| s.id()).collect();
assert_eq!(order, vec![a]);
}
@ -495,10 +477,7 @@ mod tests {
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();
let ids: Vec<SurfaceId> = r.iter_z_order_back_to_front().map(|s| s.id()).collect();
assert_eq!(ids, vec![a, c]);
}
@ -748,10 +727,7 @@ mod tests {
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();
let order: Vec<SurfaceId> = r.iter_z_order_front_to_back().map(|s| s.id()).collect();
assert_eq!(order, vec![c, b, a]);
}
@ -845,18 +821,12 @@ mod tests {
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();
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();
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

View file

@ -9,3 +9,5 @@ redox-wl-display = { path = "../redox-wl-display" }
redox-wl-input = { path = "../redox-wl-input" }
redox-wl-compositor-core = { path = "../redox-wl-compositor-core" }
redox-wl-wayland-frontend = { path = "../redox-wl-wayland-frontend" }
tracing = { version = "0.1", default-features = false, features = ["std", "attributes"] }
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "env-filter"] }

View file

@ -14,16 +14,27 @@
//! - sleep ~16ms
//!
//! Tourne 60 secondes max, exit propre.
//!
//! Logs : `tracing` + `tracing-subscriber`. Configurable via `RUST_LOG`
//! (cf. `EnvFilter::from_default_env`). Par défaut : `info`. Sortie
//! tee'd sur stdout ET sur `/scheme/debug` (= serial sous Redox/QEMU).
//! Exemples :
//! RUST_LOG=info → events lifecycle (focus, drag, garbage_collect)
//! RUST_LOG=debug → idem + détails protocole (set_min_size, hit_test…)
//! RUST_LOG=warn → uniquement anomalies (refused, rejected, mismatch)
use std::env;
use std::fs::OpenOptions;
use std::io::Write;
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::path::PathBuf;
use std::process::{Command, ExitCode};
use std::sync::{Mutex, OnceLock};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use tracing::{error, info};
use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
use redox_wl_compositor_core::Framebuffer;
use redox_wl_display::RedoxOutput;
use redox_wl_input::{InputBackend, InputEvent};
@ -32,36 +43,77 @@ use redox_wl_wayland_frontend::WaylandFrontend;
const SOCKET_PATH: &str = "/tmp/redox-wl-comp.sock";
const BG_COLOR: u32 = 0xFF101820;
struct DebugSink(Mutex<Option<std::fs::File>>);
impl DebugSink {
/// MakeWriter qui tee chaque ligne de log vers stdout ET, si disponible,
/// vers `/scheme/debug` (serial console côté host quand on tourne sous
/// `make qemu`).
#[derive(Clone)]
struct TeeWriter {
debug_scheme: Arc<Mutex<Option<File>>>,
}
impl TeeWriter {
fn new() -> Self {
Self(Mutex::new(
OpenOptions::new().write(true).open("/scheme/debug").ok(),
))
}
fn writeln(&self, s: &str) {
println!("{s}");
if let Ok(mut g) = self.0.lock() {
if let Some(f) = g.as_mut() {
let _ = writeln!(f, "{s}");
}
let f = OpenOptions::new().write(true).open("/scheme/debug").ok();
Self {
debug_scheme: Arc::new(Mutex::new(f)),
}
}
}
fn dlog(s: &str) {
static SINK: OnceLock<DebugSink> = OnceLock::new();
SINK.get_or_init(DebugSink::new).writeln(s);
struct TeeWriterGuard {
debug_scheme: Arc<Mutex<Option<File>>>,
}
impl Write for TeeWriterGuard {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let _ = io::stdout().write_all(buf);
if let Ok(mut g) = self.debug_scheme.lock() {
if let Some(f) = g.as_mut() {
let _ = f.write_all(buf);
}
}
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
let _ = io::stdout().flush();
if let Ok(mut g) = self.debug_scheme.lock() {
if let Some(f) = g.as_mut() {
let _ = f.flush();
}
}
Ok(())
}
}
impl<'a> MakeWriter<'a> for TeeWriter {
type Writer = TeeWriterGuard;
fn make_writer(&'a self) -> Self::Writer {
TeeWriterGuard {
debug_scheme: Arc::clone(&self.debug_scheme),
}
}
}
fn init_tracing() {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(TeeWriter::new())
.with_target(true)
.with_ansi(false)
.without_time()
.init();
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
dlog("[comp] Phase 6.4 — compositor Wayland démarrage");
info!("Phase 6.4 — compositor Wayland démarrage");
// Display
let mut output = RedoxOutput::open()?;
let our_vt = output.vt();
let fb_w = output.width();
let fb_h = output.height();
dlog(&format!("[comp] display {fb_w}x{fb_h}, VT={our_vt}"));
info!("display {fb_w}x{fb_h}, VT={our_vt}");
let _ = Command::new("inputd")
.arg("-A")
@ -69,7 +121,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.status();
thread::sleep(Duration::from_millis(300));
output.take_crtc()?;
dlog("[comp] CRTC pris");
info!("CRTC pris");
// Clear initial → fond bleu nuit pour signaler "compositor up"
{
@ -88,7 +140,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut frontend = WaylandFrontend::bind_absolute(&socket_path)?;
// Phase 7.3 : curseur visible dès le démarrage, placé au centre du fb.
frontend.set_cursor_initial_position((fb_w as i32) / 2, (fb_h as i32) / 2);
dlog(&format!("[comp] Wayland socket : {SOCKET_PATH}"));
info!("Wayland socket : {SOCKET_PATH}");
// Exporter WAYLAND_DISPLAY pour les clients lancés par l'OS qui regarderaient
// l'env. (Notre client de test va connecter explicitement au path.)
@ -108,12 +160,12 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
// 1. Accepter nouveaux clients Wayland
if let Err(e) = frontend.accept_pending_clients() {
dlog(&format!("[comp] accept err: {e}"));
error!("accept err: {e}");
}
// 2. Dispatch des requêtes Wayland en attente
if let Err(e) = frontend.dispatch_clients() {
dlog(&format!("[comp] dispatch err: {e}"));
error!("dispatch err: {e}");
}
// 2.5. Phase 7.6 : nettoyer les surfaces des clients déconnectés.
@ -124,7 +176,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
// 3. Input
if let Ok(events) = input.poll() {
if !events.is_empty() {
dlog(&format!("[comp] {} input events from inputd", events.len()));
tracing::debug!("{} input events from inputd", events.len());
}
for ev in events {
match &ev {
@ -132,13 +184,13 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
scancode, pressed, ..
} if *pressed && *scancode == 0x01 => {
// Esc → exit
dlog("[comp] Esc → exit");
info!("Esc → exit");
let _ = frontend.flush_clients();
let _ = std::fs::remove_file(SOCKET_PATH);
return Ok(());
}
InputEvent::Quit => {
dlog("[comp] Quit reçu");
info!("Quit reçu");
let _ = frontend.flush_clients();
let _ = std::fs::remove_file(SOCKET_PATH);
return Ok(());
@ -164,7 +216,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
// Phase 7.3 : curseur software par-dessus la composition.
frontend.draw_cursor(&mut output);
if let Err(e) = output.present_with_takeover() {
dlog(&format!("[comp] present err: {e}"));
error!("present err: {e}");
}
// 5. Frame callbacks done après le present
@ -174,33 +226,34 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
// 6. Flush vers les clients
if let Err(e) = frontend.flush_clients() {
dlog(&format!("[comp] flush err: {e}"));
error!("flush err: {e}");
}
// Log occasionnel
if tick % 30 == 0 {
dlog(&format!(
"[comp] tick={tick} surfaces={nb} elapsed={:.1}s",
info!(
"tick={tick} surfaces={nb} elapsed={:.1}s",
start.elapsed().as_secs_f32()
));
);
}
thread::sleep(frame_period);
}
dlog("[comp] timeout atteint, exit");
info!("timeout atteint, exit");
let _ = std::fs::remove_file(SOCKET_PATH);
Ok(())
}
fn main() -> ExitCode {
init_tracing();
match run() {
Ok(()) => {
dlog("[comp] PASS");
info!("PASS");
ExitCode::SUCCESS
}
Err(e) => {
dlog(&format!("[comp] FAIL: {e}"));
error!("FAIL: {e}");
ExitCode::FAILURE
}
}

View file

@ -20,12 +20,9 @@ use std::os::fd::AsRawFd;
use std::slice;
use std::sync::Arc;
use drm::Device as _;
use drm::buffer::{Buffer as _, DrmFourcc};
use drm::control::{
ClipRect, Device as _, Mode, connector,
crtc as drm_crtc, framebuffer,
};
use drm::control::{connector, crtc as drm_crtc, framebuffer, ClipRect, Device as _, Mode};
use drm::Device as _;
use graphics_ipc::{CpuBackedBuffer, V2GraphicsHandle};
use inputd::ConsumerHandle;
@ -82,8 +79,7 @@ impl RedoxOutput {
break;
}
}
let (connector, info) =
chosen.ok_or_else(|| io::Error::other("no connected display"))?;
let (connector, info) = chosen.ok_or_else(|| io::Error::other("no connected display"))?;
let mode = info
.modes()
@ -180,9 +176,7 @@ impl RedoxOutput {
let bytes = buffer.shadow_buf();
let (w, h) = (self.width as usize, self.height as usize);
// SAFETY: alignment 4 garantie par DumbBuffer ARGB8888 + len % 4 == 0
let pixels = unsafe {
slice::from_raw_parts_mut(bytes.as_mut_ptr() as *mut u32, w * h)
};
let pixels = unsafe { slice::from_raw_parts_mut(bytes.as_mut_ptr() as *mut u32, w * h) };
Ok(pixels)
}

View file

@ -112,7 +112,9 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
thread::sleep(frame_period);
}
dlog(&format!("[paint] {total_frames} frames terminées, exit propre"));
dlog(&format!(
"[paint] {total_frames} frames terminées, exit propre"
));
drop(output); // libère framebuffer + buffer explicitement
thread::sleep(Duration::from_millis(500)); // laisser le serial flush
Ok(())

View file

@ -114,10 +114,7 @@ fn translate(ev: &Event) -> InputEvent {
character: t.character,
},
EventOption::Mouse(m) => InputEvent::PointerMotion { x: m.x, y: m.y },
EventOption::MouseRelative(m) => InputEvent::PointerMotionRelative {
dx: m.dx,
dy: m.dy,
},
EventOption::MouseRelative(m) => InputEvent::PointerMotionRelative { dx: m.dx, dy: m.dy },
EventOption::Button(b) => InputEvent::PointerButton {
left: b.left,
middle: b.middle,

View file

@ -157,7 +157,10 @@ unsafe fn child_main(child_sock: OwnedFd) -> ! {
};
eprintln!(
"[poc CLIENT] shm '{}' size={} mapped at {:p} fd={}",
SHM_NAME, SHM_BYTES, map, shm_fd.as_raw_fd()
SHM_NAME,
SHM_BYTES,
map,
shm_fd.as_raw_fd()
);
// Dessine le pattern ARGB déterministe
@ -188,7 +191,11 @@ unsafe fn child_main(child_sock: OwnedFd) -> ! {
}
unsafe fn parent_main(parent_sock: OwnedFd, child_pid: libc::pid_t) -> Result<(), String> {
eprintln!("[poc SERVER pid={}] waiting for fd from child {}", libc::getpid(), child_pid);
eprintln!(
"[poc SERVER pid={}] waiting for fd from child {}",
libc::getpid(),
child_pid
);
let received = recv_fd_from(parent_sock.as_raw_fd())?;
let recv_raw = received.as_raw_fd();
@ -213,16 +220,15 @@ unsafe fn parent_main(parent_sock: OwnedFd, child_pid: libc::pid_t) -> Result<()
}
if !mismatches.is_empty() {
for (x, y, got, want) in &mismatches {
eprintln!(
"[poc SERVER] MISMATCH at ({x},{y}): got {got:#010x} want {want:#010x}"
);
eprintln!("[poc SERVER] MISMATCH at ({x},{y}): got {got:#010x} want {want:#010x}");
}
return Err(format!("{} pixel mismatches over {}", mismatches.len(), W * H));
return Err(format!(
"{} pixel mismatches over {}",
mismatches.len(),
W * H
));
}
eprintln!(
"[poc SERVER] all {} pixels match expected pattern",
W * H
);
eprintln!("[poc SERVER] all {} pixels match expected pattern", W * H);
// Bonus : dump en PPM pour visualisation (P6 binaire RGB)
match write_ppm(PPM_OUTPUT, pixels, W, H) {
@ -240,9 +246,7 @@ unsafe fn parent_main(parent_sock: OwnedFd, child_pid: libc::pid_t) -> Result<()
return Err(format!("waitpid: {}", errno_str()));
}
if !libc::WIFEXITED(status) || libc::WEXITSTATUS(status) != 0 {
return Err(format!(
"child exited abnormally: status={status}"
));
return Err(format!("child exited abnormally: status={status}"));
}
eprintln!("[poc SERVER] child reaped cleanly");

View file

@ -20,7 +20,6 @@ use std::thread;
use std::time::Duration;
use wayland_client::{
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
backend::Backend,
protocol::{
wl_buffer::WlBuffer,
@ -33,6 +32,7 @@ use wayland_client::{
wl_shm_pool::WlShmPool,
wl_surface::WlSurface,
},
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
};
use wayland_protocols::xdg::shell::client::{
xdg_surface::{self, XdgSurface},
@ -86,7 +86,12 @@ impl Dispatch<wl_registry::WlRegistry, ()> for ClientState {
_conn: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_registry::Event::Global { name, interface, version } = event {
if let wl_registry::Event::Global {
name,
interface,
version,
} = event
{
match interface.as_str() {
"wl_compositor" => {
state.compositor = Some(registry.bind(name, version.min(5), qh, ()));
@ -258,7 +263,12 @@ impl Dispatch<WlPointer, ()> for ClientState {
_qh: &QueueHandle<Self>,
) {
match event {
wl_pointer::Event::Enter { serial, surface_x, surface_y, .. } => {
wl_pointer::Event::Enter {
serial,
surface_x,
surface_y,
..
} => {
dlog(&format!(
"[client-input] wl_pointer.enter serial={serial} ({surface_x},{surface_y})"
));

View file

@ -19,12 +19,12 @@ use std::thread;
use std::time::Duration;
use wayland_client::{
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
backend::Backend,
protocol::{
wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_registry, wl_seat::WlSeat,
wl_shm::WlShm, wl_shm_pool::WlShmPool, wl_surface::WlSurface,
},
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
};
use wayland_protocols::xdg::shell::client::{
xdg_surface::{self, XdgSurface},
@ -82,7 +82,12 @@ impl Dispatch<wl_registry::WlRegistry, ()> for ClientState {
_conn: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_registry::Event::Global { name, interface, version } = event {
if let wl_registry::Event::Global {
name,
interface,
version,
} = event
{
match interface.as_str() {
"wl_compositor" => {
state.compositor = Some(registry.bind(name, version.min(5), qh, ()));
@ -176,12 +181,7 @@ impl Dispatch<XdgToplevel, ()> for ClientState {
}
/// Crée un shm fd + buffer 32-bit ARGB rempli en `color` + bordure noire 2px.
unsafe fn alloc_buffer(
name: &str,
w: i32,
h: i32,
color: u32,
) -> Result<(OwnedFd, i32), String> {
unsafe fn alloc_buffer(name: &str, w: i32, h: i32, color: u32) -> Result<(OwnedFd, i32), String> {
let stride = w * 4;
let size = stride * h;
let cname = CString::new(name).unwrap();

View file

@ -26,12 +26,12 @@ use std::thread;
use std::time::Duration;
use wayland_client::{
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
backend::Backend,
protocol::{
wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_registry, wl_seat::WlSeat,
wl_shm::WlShm, wl_shm_pool::WlShmPool, wl_surface::WlSurface,
},
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
};
use wayland_protocols::xdg::shell::client::{
xdg_surface::{self, XdgSurface},
@ -86,7 +86,12 @@ impl Dispatch<wl_registry::WlRegistry, ()> for ClientState {
_conn: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_registry::Event::Global { name, interface, version } = event {
if let wl_registry::Event::Global {
name,
interface,
version,
} = event
{
match interface.as_str() {
"wl_compositor" => {
state.compositor = Some(registry.bind(name, version.min(5), qh, ()));
@ -252,7 +257,12 @@ unsafe fn create_shm_pattern(name: &str, base: u32, letter: char) -> Result<Owne
Ok(OwnedFd::from_raw_fd(fd))
}
fn run_one(label: &str, base_color: u32, letter: char, shm_name: &str) -> Result<(), Box<dyn std::error::Error>> {
fn run_one(
label: &str,
base_color: u32,
letter: char,
shm_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
dlog(&format!("[{label}] connect to compositor"));
for i in 0..50 {

View file

@ -19,12 +19,12 @@ use std::thread;
use std::time::Duration;
use wayland_client::{
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
backend::Backend,
protocol::{
wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_registry, wl_shm::WlShm,
wl_shm_pool::WlShmPool, wl_surface::WlSurface,
},
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
};
use wayland_protocols::xdg::shell::client::{
xdg_surface::{self, XdgSurface},
@ -78,7 +78,12 @@ impl Dispatch<wl_registry::WlRegistry, ()> for ClientState {
_conn: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_registry::Event::Global { name, interface, version } = event {
if let wl_registry::Event::Global {
name,
interface,
version,
} = event
{
match interface.as_str() {
"wl_compositor" => {
state.compositor = Some(registry.bind(name, version.min(5), qh, ()));

View file

@ -62,9 +62,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.status();
thread::sleep(Duration::from_millis(300));
output
.take_crtc()
.map_err(|e| format!("take_crtc: {e}"))?;
output.take_crtc().map_err(|e| format!("take_crtc: {e}"))?;
dlog("[compose] CRTC pris");
// Input setup -----------------------------------------------------------

View file

@ -27,8 +27,8 @@ use std::io::{self, Write};
use std::process::{Command, ExitCode};
use std::sync::OnceLock;
use drm::control::{connector::State, Device as _};
use drm::Device as _;
use drm::control::{Device as _, connector::State};
use graphics_ipc::V2GraphicsHandle;
use inputd::ConsumerHandle;
@ -149,8 +149,12 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
dlog(&format!(
"[disp] driver caps: dumb_buffer={:?} cursor={}x{}",
dumb,
cursor_w.map(|v| v.to_string()).unwrap_or_else(|_| "?".into()),
cursor_h.map(|v| v.to_string()).unwrap_or_else(|_| "?".into()),
cursor_w
.map(|v| v.to_string())
.unwrap_or_else(|_| "?".into()),
cursor_h
.map(|v| v.to_string())
.unwrap_or_else(|_| "?".into()),
));
// Step 7: allocate a small CpuBackedBuffer, paint a pattern, destroy.

View file

@ -118,7 +118,11 @@ unsafe fn recv_fd_from(socket: RawFd) -> Result<OwnedFd, String> {
unsafe fn child_main(child_sock: OwnedFd) -> ! {
let raw_sock = child_sock.as_raw_fd();
eprintln!("[test-02b CHILD pid={}] recvmsg on sock {}", libc::getpid(), raw_sock);
eprintln!(
"[test-02b CHILD pid={}] recvmsg on sock {}",
libc::getpid(),
raw_sock
);
let received = match recv_fd_from(raw_sock) {
Ok(fd) => fd,
Err(e) => {
@ -156,8 +160,12 @@ fn run() -> Result<(), String> {
unsafe {
let (sock_parent, sock_child) = make_socketpair()?;
let tmp_fd = open_tmp_with_marker()?;
println!("[test-02b PARENT] tmp fd={}, sockets {}/{}",
tmp_fd.as_raw_fd(), sock_parent.as_raw_fd(), sock_child.as_raw_fd());
println!(
"[test-02b PARENT] tmp fd={}, sockets {}/{}",
tmp_fd.as_raw_fd(),
sock_parent.as_raw_fd(),
sock_child.as_raw_fd()
);
let pid = libc::fork();
if pid < 0 {

View file

@ -111,7 +111,10 @@ unsafe fn recv_fd_from(socket: RawFd) -> Result<OwnedFd, String> {
if n < 0 {
return Err(format!("recvmsg: {}", errno_str()));
}
println!("[test-02] recvmsg got {n} bytes + control of {} bytes", msg.msg_controllen);
println!(
"[test-02] recvmsg got {n} bytes + control of {} bytes",
msg.msg_controllen
);
let cmsg = libc::CMSG_FIRSTHDR(&msg);
if cmsg.is_null() {
@ -147,7 +150,10 @@ fn run() -> Result<(), String> {
if dup_fd < 0 {
return Err(format!("dup: {}", errno_str()));
}
println!("[test-02] dup'd tmp_fd ({}) -> {dup_fd}", tmp_fd.as_raw_fd());
println!(
"[test-02] dup'd tmp_fd ({}) -> {dup_fd}",
tmp_fd.as_raw_fd()
);
send_fd_over(sock_a.as_raw_fd(), dup_fd)?;
let received = recv_fd_from(sock_b.as_raw_fd())?;
@ -165,11 +171,16 @@ fn run() -> Result<(), String> {
let mut readback = vec![0u8; MARKER.len()];
let n = libc::read(recv_raw, readback.as_mut_ptr() as *mut _, readback.len());
if n != MARKER.len() as isize {
return Err(format!("read from received fd: only {n} bytes (errno {})",
io::Error::last_os_error().raw_os_error().unwrap_or(0)));
return Err(format!(
"read from received fd: only {n} bytes (errno {})",
io::Error::last_os_error().raw_os_error().unwrap_or(0)
));
}
if readback != MARKER {
return Err(format!("marker mismatch: got {:?}", String::from_utf8_lossy(&readback)));
return Err(format!(
"marker mismatch: got {:?}",
String::from_utf8_lossy(&readback)
));
}
println!("[test-02] read {} bytes via received fd: marker matches", n);

View file

@ -37,12 +37,12 @@ use std::thread;
use std::time::Duration;
use wayland_client::{
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
backend::Backend,
protocol::{
wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_registry, wl_shm::WlShm,
wl_shm_pool::WlShmPool, wl_surface::WlSurface,
},
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
};
use wayland_protocols::xdg::shell::client::{
xdg_surface::{self, XdgSurface},
@ -91,7 +91,12 @@ impl Dispatch<wl_registry::WlRegistry, ()> for ClientState {
_conn: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_registry::Event::Global { name, interface, version } = event {
if let wl_registry::Event::Global {
name,
interface,
version,
} = event
{
match interface.as_str() {
"wl_compositor" => {
state.compositor = Some(registry.bind(name, version.min(5), qh, ()));
@ -171,7 +176,8 @@ fn wait_socket() -> Result<(), String> {
Err("compositor socket missing".into())
}
fn connect() -> Result<(Connection, EventQueue<ClientState>, ClientState), Box<dyn std::error::Error>> {
fn connect(
) -> Result<(Connection, EventQueue<ClientState>, ClientState), Box<dyn std::error::Error>> {
wait_socket()?;
let stream = UnixStream::connect(SOCKET_PATH)?;
let backend = Backend::connect(stream)?;

View file

@ -17,14 +17,13 @@ use std::process::ExitCode;
use std::sync::Arc;
use wayland_client::{
Connection as ClientConnection, Dispatch as ClientDispatch, EventQueue,
backend::Backend as ClientBackend,
protocol::wl_registry,
backend::Backend as ClientBackend, protocol::wl_registry, Connection as ClientConnection,
Dispatch as ClientDispatch, EventQueue,
};
use wayland_server::{
Display as ServerDisplay, GlobalDispatch, New,
backend::ClientData,
protocol::{wl_compositor, wl_shm},
Display as ServerDisplay, GlobalDispatch, New,
};
// ---- Server side ----
@ -102,7 +101,12 @@ impl ClientDispatch<wl_registry::WlRegistry, ()> for ClientState {
_conn: &ClientConnection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
if let wl_registry::Event::Global { name, interface, version } = event {
if let wl_registry::Event::Global {
name,
interface,
version,
} = event
{
println!("[test-05 CLIENT] global #{name}: {interface} v{version}");
state.globals.push((name, interface, version));
}
@ -168,7 +172,11 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.into());
}
let names: Vec<&str> = client_state.globals.iter().map(|(_, n, _)| n.as_str()).collect();
let names: Vec<&str> = client_state
.globals
.iter()
.map(|(_, n, _)| n.as_str())
.collect();
let has_compositor = names.contains(&"wl_compositor");
let has_shm = names.contains(&"wl_shm");
@ -197,4 +205,3 @@ fn main() -> ExitCode {
}
}
}

View file

@ -63,9 +63,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
thread::sleep(Duration::from_millis(300));
// Prend le CRTC, peint un fond bleu marine pour signaler "on est là"
output
.take_crtc()
.map_err(|e| format!("take_crtc: {e}"))?;
output.take_crtc().map_err(|e| format!("take_crtc: {e}"))?;
{
let pixels = output.pixels_mut()?;
for p in pixels.iter_mut() {
@ -111,10 +109,14 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
));
}
InputEvent::TextInput { character } => {
dlog(&format!("[input] #{event_count:04} TextInput '{character}'"));
dlog(&format!(
"[input] #{event_count:04} TextInput '{character}'"
));
}
InputEvent::PointerMotion { x, y } => {
dlog(&format!("[input] #{event_count:04} PointerMotion abs=({x},{y})"));
dlog(&format!(
"[input] #{event_count:04} PointerMotion abs=({x},{y})"
));
}
InputEvent::PointerMotionRelative { dx, dy } => {
dlog(&format!(
@ -131,7 +133,9 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
));
}
InputEvent::PointerScroll { dx, dy } => {
dlog(&format!("[input] #{event_count:04} PointerScroll d=({dx},{dy})"));
dlog(&format!(
"[input] #{event_count:04} PointerScroll d=({dx},{dy})"
));
}
InputEvent::Quit => {
dlog(&format!("[input] #{event_count:04} Quit demandé"));

View file

@ -18,9 +18,7 @@ use std::thread;
use std::time::Duration;
use wayland_client::{
Connection, Dispatch, EventQueue, QueueHandle,
backend::Backend,
protocol::wl_registry,
backend::Backend, protocol::wl_registry, Connection, Dispatch, EventQueue, QueueHandle,
};
const SOCKET_PATH: &str = "/tmp/redox-wl-comp.sock";
@ -82,7 +80,11 @@ const EXPECTED: &[(&str, &str, &str)] = &[
("wl_output", "core", "monitors info (geometry, scale, mode)"),
("wl_data_device_manager", "core", "clipboard + DnD"),
// --- xdg-shell ---
("xdg_wm_base", "stable", "window management (toplevel + popup)"),
(
"xdg_wm_base",
"stable",
"window management (toplevel + popup)",
),
// --- protocoles externes très demandés ---
(
"zxdg_decoration_manager_v1",

View file

@ -45,9 +45,21 @@ fn run() -> Result<(), String> {
// ---- Subtest A: timeout-only, no fd ready ----
let mut pfds = [
libc::pollfd { fd: ra, events: libc::POLLIN, revents: 0 },
libc::pollfd { fd: rb, events: libc::POLLIN, revents: 0 },
libc::pollfd { fd: rc, events: libc::POLLIN, revents: 0 },
libc::pollfd {
fd: ra,
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: rb,
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: rc,
events: libc::POLLIN,
revents: 0,
},
];
let n = poll_revents(&mut pfds, 50)?;
if n != 0 {
@ -62,12 +74,16 @@ fn run() -> Result<(), String> {
return Err(format!("write to wb: {}", errno_str()));
}
for p in pfds.iter_mut() { p.revents = 0; }
for p in pfds.iter_mut() {
p.revents = 0;
}
let n = poll_revents(&mut pfds, 1000)?;
if n != 1 {
return Err(format!("expected 1 ready fd, got {n}"));
}
let ready: Vec<_> = pfds.iter().enumerate()
let ready: Vec<_> = pfds
.iter()
.enumerate()
.filter(|(_, p)| p.revents & libc::POLLIN != 0)
.map(|(i, _)| i)
.collect();
@ -85,22 +101,33 @@ fn run() -> Result<(), String> {
// ---- Subtest C: close write end of A, expect HUP on read end ----
libc::close(wa);
for p in pfds.iter_mut() { p.revents = 0; }
for p in pfds.iter_mut() {
p.revents = 0;
}
let n = poll_revents(&mut pfds, 100)?;
if n < 1 {
return Err(format!("expected at least 1 ready (POLLHUP on ra), got {n}"));
return Err(format!(
"expected at least 1 ready (POLLHUP on ra), got {n}"
));
}
let hup_a = pfds[0].revents & (libc::POLLHUP | libc::POLLIN) != 0;
if !hup_a {
return Err(format!("expected HUP/IN on ra, revents={:#x}", pfds[0].revents));
return Err(format!(
"expected HUP/IN on ra, revents={:#x}",
pfds[0].revents
));
}
println!("[test-04] C: closing wa caused POLLHUP/POLLIN on ra (revents={:#x})",
pfds[0].revents);
println!(
"[test-04] C: closing wa caused POLLHUP/POLLIN on ra (revents={:#x})",
pfds[0].revents
);
// Cleanup
libc::close(ra);
libc::close(rb); libc::close(wb);
libc::close(rc); libc::close(wc);
libc::close(rb);
libc::close(wb);
libc::close(rc);
libc::close(wc);
}
Ok(())

View file

@ -30,11 +30,7 @@ fn errno_str() -> String {
unsafe fn shm_open_rw_create(name: &str, mode: libc::mode_t) -> Result<RawFd, String> {
let cname = CString::new(name).unwrap();
let fd = libc::shm_open(
cname.as_ptr(),
libc::O_RDWR | libc::O_CREAT,
mode as _,
);
let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR | libc::O_CREAT, mode as _);
if fd < 0 {
return Err(format!("shm_open({name}): {}", errno_str()));
}
@ -107,7 +103,9 @@ fn run() -> Result<(), String> {
libc::close(fd2);
let cname = CString::new(SHM_NAME).unwrap();
libc::shm_unlink(cname.as_ptr());
return Err(format!("pixel {i} mismatch: got {p:#x}, expected {expected:#x}"));
return Err(format!(
"pixel {i} mismatch: got {p:#x}, expected {expected:#x}"
));
}
}
println!("[test-03] phase2: pattern verified across close/reopen");

View file

@ -23,21 +23,19 @@ use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use wayland_client::{
Connection as ClientConnection, Dispatch as ClientDispatch, EventQueue, Proxy as _,
QueueHandle as ClientQueueHandle,
backend::Backend as ClientBackend,
protocol::{
wl_buffer::WlBuffer as ClientBuffer, wl_compositor::WlCompositor as ClientCompositor,
wl_registry, wl_shm::WlShm as ClientShm, wl_shm_pool::WlShmPool as ClientShmPool,
wl_surface::WlSurface as ClientSurface,
},
Connection as ClientConnection, Dispatch as ClientDispatch, EventQueue, Proxy as _,
QueueHandle as ClientQueueHandle,
};
use wayland_server::{
Client, DataInit, Display as ServerDisplay, DisplayHandle, GlobalDispatch,
backend::ClientData,
protocol::{
wl_buffer, wl_callback, wl_compositor, wl_shm, wl_shm_pool, wl_surface,
},
protocol::{wl_buffer, wl_callback, wl_compositor, wl_shm, wl_shm_pool, wl_surface},
Client, DataInit, Display as ServerDisplay, DisplayHandle, GlobalDispatch,
};
const W: i32 = 100;
@ -215,8 +213,7 @@ impl wayland_server::Dispatch<wl_surface::WlSurface, ()> for ServerState {
state.obs.lock().unwrap().surface_attached = true;
eprintln!("[server] wl_surface.attach");
}
wl_surface::Request::Damage { .. }
| wl_surface::Request::DamageBuffer { .. } => {
wl_surface::Request::Damage { .. } | wl_surface::Request::DamageBuffer { .. } => {
eprintln!("[server] wl_surface.damage");
}
wl_surface::Request::Commit => {
@ -260,7 +257,12 @@ impl ClientDispatch<wl_registry::WlRegistry, ()> for ClientState {
_conn: &ClientConnection,
qh: &ClientQueueHandle<Self>,
) {
if let wl_registry::Event::Global { name, interface, version } = event {
if let wl_registry::Event::Global {
name,
interface,
version,
} = event
{
eprintln!("[client] global #{name}: {interface} v{version}");
match interface.as_str() {
"wl_compositor" => {
@ -332,7 +334,10 @@ unsafe fn create_shm_buffer(name: &str, size: usize) -> Result<(OwnedFd, *mut u8
use std::os::fd::FromRawFd;
unsafe fn child_main(server_path: &str) -> ! {
eprintln!("[client pid={}] connecting to {server_path}", libc::getpid());
eprintln!(
"[client pid={}] connecting to {server_path}",
libc::getpid()
);
// Connect to the server socket
let stream = match UnixStream::connect(server_path) {
@ -393,9 +398,13 @@ unsafe fn child_main(server_path: &str) -> ! {
// Create wl_buffer
let buffer = pool.create_buffer(
0, W, H, STRIDE,
0,
W,
H,
STRIDE,
wayland_client::protocol::wl_shm::Format::Argb8888,
&qh, (),
&qh,
(),
);
// Create wl_surface
@ -439,8 +448,8 @@ fn run() -> Result<(), String> {
let obs = Arc::new(Mutex::new(ServerObservation::default()));
let mut display: ServerDisplay<ServerState> = ServerDisplay::new()
.map_err(|e| format!("Display::new: {e:?}"))?;
let mut display: ServerDisplay<ServerState> =
ServerDisplay::new().map_err(|e| format!("Display::new: {e:?}"))?;
let mut dh = display.handle();
dh.create_global::<ServerState, wl_compositor::WlCompositor, _>(5, ());
dh.create_global::<ServerState, wl_shm::WlShm, _>(1, ());
@ -549,7 +558,11 @@ fn run() -> Result<(), String> {
for (x, y, got, want) in &mismatches {
eprintln!("[server] mismatch at ({x},{y}): got {got:#010x} want {want:#010x}");
}
return Err(format!("{} pixel mismatches in {} total", mismatches.len(), W * H));
return Err(format!(
"{} pixel mismatches in {} total",
mismatches.len(),
W * H
));
}
println!(

View file

@ -44,8 +44,10 @@ fn main() -> ExitCode {
eprintln!("[test-01] FAIL: write_all: {e}");
return ExitCode::FAILURE;
}
println!("[test-01] sent {} bytes (header 8 + payload {} aligned to {})",
total_size, payload_len, aligned_len);
println!(
"[test-01] sent {} bytes (header 8 + payload {} aligned to {})",
total_size, payload_len, aligned_len
);
let mut header = [0u8; 8];
if let Err(e) = b.read_exact(&mut header) {
@ -58,7 +60,9 @@ fn main() -> ExitCode {
let recv_op = (recv_so & 0xFFFF) as u16;
if recv_oid != OBJECT_ID || recv_op != OPCODE || recv_size != total_size {
eprintln!("[test-01] FAIL: header mismatch oid={recv_oid:#x} op={recv_op:#x} size={recv_size}");
eprintln!(
"[test-01] FAIL: header mismatch oid={recv_oid:#x} op={recv_op:#x} size={recv_size}"
);
return ExitCode::FAILURE;
}
@ -72,7 +76,9 @@ fn main() -> ExitCode {
return ExitCode::FAILURE;
}
println!("[test-01] PASS: roundtrip OK, {} bytes recv, oid={:#x} op={:#x}",
total_size, recv_oid, recv_op);
println!(
"[test-01] PASS: roundtrip OK, {} bytes recv, oid={:#x} op={:#x}",
total_size, recv_oid, recv_op
);
ExitCode::SUCCESS
}

View file

@ -11,3 +11,4 @@ wayland-server = { path = "../../../wayland-rs/wayland-server", default-features
wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false }
wayland-protocols = { path = "../../../wayland-rs/wayland-protocols", default-features = false, features = ["server"] }
libc = "0.2"
tracing = { version = "0.1", default-features = false, features = ["std", "attributes"] }

View file

@ -27,17 +27,19 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tracing::{debug, error, info, warn};
use redox_wl_compositor_core::{Framebuffer, SurfaceBuffer, SurfaceId, SurfaceRegistry};
use wayland_protocols::xdg::shell::server::{
xdg_popup, xdg_positioner, xdg_surface, xdg_toplevel, xdg_wm_base,
};
use wayland_server::{
Client, DataInit, Display as WlDisplay, DisplayHandle, GlobalDispatch, Resource,
backend::{ClientData, ClientId, DisconnectReason},
protocol::{
wl_buffer, wl_callback, wl_compositor, wl_keyboard, wl_pointer, wl_region, wl_seat,
wl_shm, wl_shm_pool, wl_surface,
wl_buffer, wl_callback, wl_compositor, wl_keyboard, wl_pointer, wl_region, wl_seat, wl_shm,
wl_shm_pool, wl_surface,
},
Client, DataInit, Display as WlDisplay, DisplayHandle, GlobalDispatch, Resource,
};
use redox_wl_input::InputEvent as RedoxInputEvent;
@ -259,6 +261,40 @@ fn compute_resize_geom(
(new_x, new_y, new_w, new_h)
}
/// Phase 7.9 : applique les contraintes `set_min_size` / `set_max_size`.
/// Convention spec wayland-protocols : `(0, 0)` = pas de contrainte
/// active (axe par axe). Une dimension `min > 0` impose `w >= min`,
/// une dimension `max > 0` impose `w <= max`.
fn clamp_to_min_max(w: u32, h: u32, min: (i32, i32), max: (i32, i32)) -> (u32, u32) {
let mut w = w;
let mut h = h;
if min.0 > 0 {
w = w.max(min.0 as u32);
}
if min.1 > 0 {
h = h.max(min.1 as u32);
}
if max.0 > 0 {
w = w.min(max.0 as u32);
}
if max.1 > 0 {
h = h.min(max.1 as u32);
}
(w, h)
}
/// Phase 7.9 : décide si un `xdg_toplevel.configure` doit être envoyé
/// pendant un drag de resize. Skip si la taille n'a pas bougé OU si
/// le dernier configure date de moins de `min_interval` (~60 fps).
fn should_throttle_configure(
new_size: (u32, u32),
last_size: (u32, u32),
elapsed: Duration,
min_interval: Duration,
) -> bool {
new_size == last_size || elapsed < min_interval
}
/// Phase 7.7 : état d'un drag interactif en cours. Posé par le handler
/// `xdg_toplevel.Move`/`Resize`, lu par `forward_input` pour appliquer
/// les deltas, retiré au release du bouton gauche.
@ -432,10 +468,7 @@ impl WaylandFrontend {
let cd = DumbClientData {
dead_clients: Arc::clone(&self.dead_clients),
};
let _ = self
.display
.handle()
.insert_client(stream, Arc::new(cd));
let _ = self.display.handle().insert_client(stream, Arc::new(cd));
}
Ok(None) => break, // pas de client en attente
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
@ -497,8 +530,8 @@ impl WaylandFrontend {
// déjà déconnecté.
self.pointers.retain(|p| p.client().is_some());
self.keyboards.retain(|k| k.client().is_some());
println!(
"[frontend] garbage_collect: client {cid:?} → destroyed {} surfaces",
info!(
"garbage_collect: client {cid:?} → destroyed {} surfaces",
to_destroy.len()
);
}
@ -571,19 +604,12 @@ impl WaylandFrontend {
.as_ref()
.and_then(|s| s.data::<Arc<SurfaceData>>())
.and_then(|d| *d.id.lock().unwrap());
println!(
"[frontend] focus change: {old_sid:?} → {new_sid:?}"
);
info!("focus change: {old_sid:?} → {new_sid:?}");
// Phase 7.9 : envoyer configure(w, h, [Activated]) au nouveau
// focused toplevel et configure(w, h, []) à l'ancien, pour que
// les toolkits puissent styler le focus (titlebar active, etc.).
let empty_state: Vec<u8> = vec![];
let activated_state: Vec<u8> = vec![
(xdg_toplevel::State::Activated as u32) as u8,
0,
0,
0,
];
let activated_state: Vec<u8> = vec![(xdg_toplevel::State::Activated as u32) as u8, 0, 0, 0];
if let Some(sid) = old_sid {
if let Some(tl) = self.toplevels_by_id.get(&sid).cloned() {
self.send_focus_configure(&tl, sid, empty_state);
@ -616,12 +642,7 @@ impl WaylandFrontend {
// surface_x, surface_y : on prend la position relative
// simple basée sur cursor - surface origin
let (sx, sy) = self.surface_local_cursor(new);
ptr.enter(
serial_enter,
new,
fixed_from_int(sx),
fixed_from_int(sy),
);
ptr.enter(serial_enter, new, fixed_from_int(sx), fixed_from_int(sy));
if SEAT_VERSION >= 5 {
ptr.frame();
}
@ -658,7 +679,12 @@ impl WaylandFrontend {
let (w, h) = self
.registry
.get(sid)
.and_then(|s| s.current().buffer.as_ref().map(|b| (b.width as i32, b.height as i32)))
.and_then(|s| {
s.current()
.buffer
.as_ref()
.map(|b| (b.width as i32, b.height as i32))
})
.unwrap_or(DEFAULT_TOPLEVEL_SIZE);
toplevel.configure(w, h, states);
if let Some(td) = toplevel.data::<Arc<XdgToplevelData>>() {
@ -709,22 +735,12 @@ impl WaylandFrontend {
);
// Phase 7.9 : clamp aux contraintes min/max envoyées
// par le client via xdg_toplevel.set_min/max_size.
// (0, 0) = pas de contrainte.
if let Some(td) = drag.xdg_toplevel_res.data::<Arc<XdgToplevelData>>() {
let (min_w, min_h) = *td.min_size.lock().unwrap();
let (max_w, max_h) = *td.max_size.lock().unwrap();
if min_w > 0 {
new_w = new_w.max(min_w as u32);
}
if min_h > 0 {
new_h = new_h.max(min_h as u32);
}
if max_w > 0 {
new_w = new_w.min(max_w as u32);
}
if max_h > 0 {
new_h = new_h.min(max_h as u32);
}
let min = *td.min_size.lock().unwrap();
let max = *td.max_size.lock().unwrap();
let clamped = clamp_to_min_max(new_w, new_h, min, max);
new_w = clamped.0;
new_h = clamped.1;
}
// Mettre à jour la position côté compositor immédiatement
// (la taille effective dépendra du prochain commit du client).
@ -738,22 +754,20 @@ impl WaylandFrontend {
// (re)allocations côté client : skip si la taille n'a
// pas changé OU si moins de 16ms depuis le dernier
// configure (~60fps max).
let same_size = (new_w, new_h) == drag.last_configure_size;
let too_soon =
drag.last_configure_at.elapsed() < Duration::from_millis(16);
if same_size || too_soon {
if should_throttle_configure(
(new_w, new_h),
drag.last_configure_size,
drag.last_configure_at.elapsed(),
Duration::from_millis(16),
) {
return true;
}
// Annoncer la nouvelle taille au client. Le state
// `Resizing` (3) indique au toolkit qu'il est en train
// d'être redimensionné.
let resizing_state: Vec<u8> = vec![
(xdg_toplevel::State::Resizing as u32) as u8,
0,
0,
0,
];
let resizing_state: Vec<u8> =
vec![(xdg_toplevel::State::Resizing as u32) as u8, 0, 0, 0];
drag.xdg_toplevel_res
.configure(new_w as i32, new_h as i32, resizing_state);
let serial = self.next_xdg_serial;
@ -876,14 +890,14 @@ impl WaylandFrontend {
// Phase 7.7 : si drag actif et le bouton gauche est relâché,
// sortir du mode drag avant l'envoi du button au client.
if !*left && self.interactive_drag.is_some() {
println!("[frontend] left-release → exit interactive drag");
info!("left-release → exit interactive drag");
self.interactive_drag = None;
}
if *left {
let hit = self.registry.hit_test(self.cursor_x, self.cursor_y);
println!(
"[frontend] left-click @ ({}, {}) → hit_test = {:?}",
debug!(
"left-click @ ({}, {}) → hit_test = {:?}",
self.cursor_x, self.cursor_y, hit
);
if let Some(sid) = hit {
@ -959,11 +973,7 @@ impl WaylandFrontend {
continue;
}
if *dy != 0 {
ptr.axis(
time,
wl_pointer::Axis::VerticalScroll,
fixed_from_int(*dy),
);
ptr.axis(time, wl_pointer::Axis::VerticalScroll, fixed_from_int(*dy));
}
if *dx != 0 {
ptr.axis(
@ -1271,7 +1281,7 @@ impl wayland_server::Dispatch<wl_shm::WlShm, ()> for WaylandFrontend {
data_init.init(id, Arc::new(Mutex::new(p)));
}
Err(e) => {
eprintln!("[frontend] shm CreatePool mmap failed: {e}");
error!("shm CreatePool mmap failed: {e}");
// En cas d'échec on ne peut pas init le pool. Le client va
// probablement avoir un fd resource leaked, mais c'est
// moins grave qu'un crash. Implementation simple pour 6.4.
@ -1321,7 +1331,8 @@ impl wayland_server::Dispatch<wl_shm_pool::WlShmPool, Arc<Mutex<ShmPool>>> for W
if valid {
let pool_size = pool.lock().unwrap().size as i64;
let needed = (offset as i64).saturating_add(
(stride as i64).saturating_mul((height as i64).saturating_sub(1))
(stride as i64)
.saturating_mul((height as i64).saturating_sub(1))
.saturating_add((width as i64).saturating_mul(4)),
);
if needed > pool_size {
@ -1329,8 +1340,8 @@ impl wayland_server::Dispatch<wl_shm_pool::WlShmPool, Arc<Mutex<ShmPool>>> for W
}
}
if !valid {
println!(
"[frontend] wl_shm_pool.create_buffer rejected: \
warn!(
"wl_shm_pool.create_buffer rejected: \
offset={offset} width={width} height={height} stride={stride}"
);
}
@ -1431,17 +1442,11 @@ impl wayland_server::Dispatch<wl_surface::WlSurface, Arc<SurfaceData>> for Wayla
} else {
let pool = bd.pool.lock().unwrap();
let pixels_opt = unsafe {
pool.read_argb(
bd.offset as usize,
bd.width,
bd.height,
bd.stride,
)
pool.read_argb(bd.offset as usize, bd.width, bd.height, bd.stride)
};
drop(pool);
if let Some(pixels) = pixels_opt {
let sb =
SurfaceBuffer::from_pixels(bd.width, bd.height, pixels);
let sb = SurfaceBuffer::from_pixels(bd.width, bd.height, pixels);
// Pour une surface curseur, on stocke le buffer
// mais visible=false (cf draw_cursor).
state.registry.modify_pending(id, |s| {
@ -1449,9 +1454,7 @@ impl wayland_server::Dispatch<wl_surface::WlSurface, Arc<SurfaceData>> for Wayla
s.visible = !is_cursor;
});
} else {
println!(
"[frontend] commit: read_argb refused buffer (overrun guard)"
);
warn!("commit: read_argb refused buffer (overrun guard)");
}
}
}
@ -1683,8 +1686,8 @@ impl wayland_server::Dispatch<xdg_surface::XdgSurface, Arc<XdgSurfaceData>> for
// abus délibérés.
let last = *data.last_serial.lock().unwrap();
if serial > last {
println!(
"[frontend] xdg_surface.ack_configure: serial {serial} > last_sent {last}, ignoring"
warn!(
"xdg_surface.ack_configure: serial {serial} > last_sent {last}, ignoring"
);
return;
}
@ -1901,9 +1904,7 @@ fn send_keymap_no_keymap(kb: &wl_keyboard::WlKeyboard) {
use std::os::fd::AsFd;
// ---- xdg_toplevel ----
impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
for WaylandFrontend
{
impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>> for WaylandFrontend {
fn request(
state: &mut Self,
_client: &Client,
@ -1922,19 +1923,16 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
}
xdg_toplevel::Request::SetMinSize { width, height } => {
*data.min_size.lock().unwrap() = (width, height);
println!("[frontend] xdg_toplevel.set_min_size({width}, {height})");
debug!("xdg_toplevel.set_min_size({width}, {height})");
}
xdg_toplevel::Request::SetMaxSize { width, height } => {
*data.max_size.lock().unwrap() = (width, height);
println!("[frontend] xdg_toplevel.set_max_size({width}, {height})");
debug!("xdg_toplevel.set_max_size({width}, {height})");
}
xdg_toplevel::Request::Move {
seat: _,
serial,
} => {
xdg_toplevel::Request::Move { seat: _, serial } => {
// Phase 7.7/7.8 : entrer en mode drag-move.
if serial == 0 {
println!("[frontend] xdg_toplevel.move: serial=0 refused");
warn!("xdg_toplevel.move: serial=0 refused");
return;
}
// Phase 7.8 : validation soft du serial — log warning si
@ -1942,17 +1940,17 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
// tous les clients utiliseront un vrai serial capté
// depuis pointer.button).
if state.last_button_serial != 0 && serial != state.last_button_serial {
println!(
"[frontend] xdg_toplevel.move: serial {serial} ≠ last_button {} (accepting)",
debug!(
"xdg_toplevel.move: serial {serial} ≠ last_button {} (accepting)",
state.last_button_serial
);
}
if state.interactive_drag.is_some() {
println!("[frontend] xdg_toplevel.move: drag already in progress");
warn!("xdg_toplevel.move: drag already in progress");
return;
}
let Some(xdg_surf) = data.xdg_surface.lock().unwrap().clone() else {
println!("[frontend] xdg_toplevel.move: no parent xdg_surface");
warn!("xdg_toplevel.move: no parent xdg_surface");
return;
};
let Some(xdg_data) = xdg_surf.data::<Arc<XdgSurfaceData>>() else {
@ -1966,8 +1964,8 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
let caller_cid = resource.client().map(|c| c.id());
let owner_cid = surf_data.client_id.lock().unwrap().clone();
if caller_cid != owner_cid {
println!(
"[frontend] xdg_toplevel.move: caller={caller_cid:?} ≠ owner={owner_cid:?} → REJECTED"
warn!(
"xdg_toplevel.move: caller={caller_cid:?} ≠ owner={owner_cid:?} → REJECTED"
);
return;
}
@ -1997,8 +1995,8 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
last_configure_size: (start_w, start_h),
last_configure_at: Instant::now(),
};
println!(
"[frontend] xdg_toplevel.move: enter drag sid={:?} start=({},{}) cursor=({},{})",
info!(
"xdg_toplevel.move: enter drag sid={:?} start=({},{}) cursor=({},{})",
sid, st.x, st.y, state.cursor_x, state.cursor_y
);
state.interactive_drag = Some(drag);
@ -2014,21 +2012,21 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
Err(_) => 0,
};
if serial == 0 {
println!("[frontend] xdg_toplevel.resize: serial=0 refused");
warn!("xdg_toplevel.resize: serial=0 refused");
return;
}
if edges_raw == 0 {
println!("[frontend] xdg_toplevel.resize: edges=None, ignoring");
warn!("xdg_toplevel.resize: edges=None, ignoring");
return;
}
if state.last_button_serial != 0 && serial != state.last_button_serial {
println!(
"[frontend] xdg_toplevel.resize: serial {serial} ≠ last_button {} (accepting, durcir 7.9)",
debug!(
"xdg_toplevel.resize: serial {serial} ≠ last_button {} (accepting, durcir 7.9)",
state.last_button_serial
);
}
if state.interactive_drag.is_some() {
println!("[frontend] xdg_toplevel.resize: drag already in progress");
warn!("xdg_toplevel.resize: drag already in progress");
return;
}
let Some(xdg_surf) = data.xdg_surface.lock().unwrap().clone() else {
@ -2045,8 +2043,8 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
let caller_cid = resource.client().map(|c| c.id());
let owner_cid = surf_data.client_id.lock().unwrap().clone();
if caller_cid != owner_cid {
println!(
"[frontend] xdg_toplevel.resize: caller={caller_cid:?} ≠ owner={owner_cid:?} → REJECTED"
warn!(
"xdg_toplevel.resize: caller={caller_cid:?} ≠ owner={owner_cid:?} → REJECTED"
);
return;
}
@ -2057,11 +2055,11 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
return;
};
let st = s.current();
let (start_w, start_h) = st
.buffer
.as_ref()
.map(|b| (b.width, b.height))
.unwrap_or((DEFAULT_TOPLEVEL_SIZE.0 as u32, DEFAULT_TOPLEVEL_SIZE.1 as u32));
let (start_w, start_h) =
st.buffer.as_ref().map(|b| (b.width, b.height)).unwrap_or((
DEFAULT_TOPLEVEL_SIZE.0 as u32,
DEFAULT_TOPLEVEL_SIZE.1 as u32,
));
let drag = InteractiveDrag {
surface_id: sid,
xdg_toplevel_res: resource.clone(),
@ -2076,8 +2074,8 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
last_configure_size: (start_w, start_h),
last_configure_at: Instant::now(),
};
println!(
"[frontend] xdg_toplevel.resize: enter drag sid={sid:?} edges={edges_raw} start_geom=({},{},{},{}) cursor=({},{})",
info!(
"xdg_toplevel.resize: enter drag sid={sid:?} edges={edges_raw} start_geom=({},{},{},{}) cursor=({},{})",
st.x, st.y, start_w, start_h, state.cursor_x, state.cursor_y
);
state.interactive_drag = Some(drag);
@ -2098,3 +2096,151 @@ impl wayland_server::Dispatch<xdg_toplevel::XdgToplevel, Arc<XdgToplevelData>>
}
}
}
// ---------------------------------------------------------------------------
// Tests unitaires xdg-shell (sprint 0 point 4).
//
// Couverture : la logique pure extraite des handlers Dispatch — c.-à-d.
// `compute_resize_geom`, `clamp_to_min_max`, `should_throttle_configure`.
// Pas de Wayland-server runtime ici : ces fns sont libres et déterministes,
// donc isolables. Les tests "protocole" (ack_configure serial, set_focus
// configure) nécessitent un vrai client + Display et sont reportés à une
// future phase d'intégration.
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
// ---- compute_resize_geom : bord Right (8) ----
#[test]
fn resize_right_grows_width_keeps_origin() {
let (x, y, w, h) = compute_resize_geom(8, 100, 200, 300, 150, 50, 0);
assert_eq!((x, y), (100, 200));
assert_eq!((w, h), (350, 150));
}
// ---- compute_resize_geom : bord Left (4) ----
#[test]
fn resize_left_moves_origin_and_shrinks_on_positive_dx() {
let (x, y, w, h) = compute_resize_geom(4, 100, 200, 300, 150, 40, 0);
assert_eq!((x, y), (140, 200));
assert_eq!((w, h), (260, 150));
}
// ---- compute_resize_geom : bord Bottom (2) ----
#[test]
fn resize_bottom_grows_height_keeps_origin() {
let (x, y, w, h) = compute_resize_geom(2, 100, 200, 300, 150, 0, 70);
assert_eq!((x, y), (100, 200));
assert_eq!((w, h), (300, 220));
}
// ---- compute_resize_geom : bord Top (1) ----
#[test]
fn resize_top_moves_origin_and_shrinks_on_positive_dy() {
let (x, y, w, h) = compute_resize_geom(1, 100, 200, 300, 150, 0, 60);
assert_eq!((x, y), (100, 260));
assert_eq!((w, h), (300, 90));
}
// ---- compute_resize_geom : coin BottomRight (Bottom|Right = 10) ----
#[test]
fn resize_bottom_right_corner_grows_both_axes() {
let (x, y, w, h) = compute_resize_geom(10, 100, 200, 300, 150, 25, 35);
assert_eq!((x, y), (100, 200));
assert_eq!((w, h), (325, 185));
}
// ---- compute_resize_geom : delta trop négatif clamp à MIN_RESIZE_DIM ----
#[test]
fn resize_clamps_to_min_resize_dim_on_extreme_shrink() {
let (_x, _y, w, h) = compute_resize_geom(10, 0, 0, 300, 150, -10_000, -10_000);
assert_eq!((w, h), (MIN_RESIZE_DIM, MIN_RESIZE_DIM));
}
// ---- compute_resize_geom : edges=0 (aucun bord) → géom inchangée ----
#[test]
fn resize_with_no_edges_returns_starting_geometry() {
let (x, y, w, h) = compute_resize_geom(0, 50, 60, 200, 100, 999, 999);
assert_eq!((x, y, w, h), (50, 60, 200, 100));
}
// ---- clamp_to_min_max : (0, 0) = pas de contrainte ----
#[test]
fn clamp_with_zero_constraints_is_identity() {
let (w, h) = clamp_to_min_max(320, 200, (0, 0), (0, 0));
assert_eq!((w, h), (320, 200));
}
// ---- clamp_to_min_max : min seul force le plancher ----
#[test]
fn clamp_applies_min_floor() {
let (w, h) = clamp_to_min_max(50, 30, (150, 80), (0, 0));
assert_eq!((w, h), (150, 80));
}
// ---- clamp_to_min_max : max seul force le plafond ----
#[test]
fn clamp_applies_max_ceiling() {
let (w, h) = clamp_to_min_max(900, 500, (0, 0), (600, 400));
assert_eq!((w, h), (600, 400));
}
// ---- clamp_to_min_max : min + max coexistent ----
#[test]
fn clamp_applies_both_min_and_max() {
// size dans la fenêtre → identique
let (w, h) = clamp_to_min_max(320, 200, (150, 80), (600, 400));
assert_eq!((w, h), (320, 200));
// size sous le plancher → remontée
let (w, h) = clamp_to_min_max(10, 10, (150, 80), (600, 400));
assert_eq!((w, h), (150, 80));
// size au-dessus du plafond → descente
let (w, h) = clamp_to_min_max(9999, 9999, (150, 80), (600, 400));
assert_eq!((w, h), (600, 400));
}
// ---- clamp_to_min_max : axes indépendants ----
#[test]
fn clamp_constraints_are_independent_per_axis() {
// min sur width seulement
let (w, h) = clamp_to_min_max(10, 200, (150, 0), (0, 0));
assert_eq!((w, h), (150, 200));
// max sur height seulement
let (w, h) = clamp_to_min_max(300, 9999, (0, 0), (0, 400));
assert_eq!((w, h), (300, 400));
}
// ---- should_throttle_configure : même taille → skip ----
#[test]
fn throttle_skips_when_size_unchanged() {
assert!(should_throttle_configure(
(320, 200),
(320, 200),
Duration::from_secs(10),
Duration::from_millis(16),
));
}
// ---- should_throttle_configure : trop tôt → skip ----
#[test]
fn throttle_skips_when_elapsed_below_min_interval() {
assert!(should_throttle_configure(
(321, 200),
(320, 200),
Duration::from_millis(8),
Duration::from_millis(16),
));
}
// ---- should_throttle_configure : taille différente + délai OK → envoie ----
#[test]
fn throttle_allows_configure_when_size_changed_and_interval_elapsed() {
assert!(!should_throttle_configure(
(321, 200),
(320, 200),
Duration::from_millis(20),
Duration::from_millis(16),
));
}
}