🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
//! `wl_subcompositor` — powierzchnie potomne (`wl_subsurface`).
//!
//! Bez tego globalu klienty, które rysują część UI w powierzchniach potomnych
//! (GTK4/GtkGLArea, Qt, Electron, część gier) nie mają gdzie ich umieścić: treść
//! po prostu nie pojawia się na ekranie, a kliknięcia trafiają w pustkę.
//!
//! Model: dziecko ma własny bufor, pozycję względem rodzica i tryb `sync`.
//! W trybie `sync` (domyślnym) zmiany wchodzą dopiero przy commicie rodzica;
//! w `desync` — natychmiast. Rodzic trzyma listę dzieci w kolejności stosu,
//! a wynik składamy w jeden obraz (`ImageData`) przy każdym commicie.
use std::sync::Arc;
use libluna_render::ImageData;
use wayland_server::{
protocol::{wl_subcompositor, wl_subsurface, wl_surface::WlSurface},
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use crate::compositor::SurfaceData;
use crate::shm::BufferData;
use crate::state::State;
use crate::sync::LockExt;
/// Stan powierzchni potomnej (trzymany w [`SurfaceData::subsurface`]).
#[derive(Debug, Clone)]
pub struct SubsurfaceState {
pub parent: WlSurface,
/// Pozycja względem lewego-górnego rogu bufora rodzica.
pub position: (i32, i32),
/// Pozycja z commita czekająca na commit rodzica (tryb `sync`).
pub pending_position: Option<(i32, i32)>,
/// Czy zmiany czekają na commit rodzica (domyślnie tak).
pub sync: bool,
/// Obraz aktualnie widoczny.
pub image: Option<Arc<ImageData>>,
/// Obraz z commita czekający na commit rodzica (tryb `sync`).
pub pending: Option<Arc<ImageData>>,
}
impl SubsurfaceState {
pub fn new(parent: WlSurface) -> Self {
Self {
parent,
position: (0, 0),
pending_position: None,
sync: true,
image: None,
pending: None,
}
}
}
/// Dane obiektu `wl_subsurface` (do sprzątania przy zniszczeniu).
pub struct SubsurfaceData {
pub surface: WlSurface,
pub parent: WlSurface,
}
impl GlobalDispatch<wl_subcompositor::WlSubcompositor, ()> for State {
fn bind(
_state: &mut Self,
_handle: &DisplayHandle,
_client: &Client,
resource: New<wl_subcompositor::WlSubcompositor>,
_data: &(),
data_init: &mut DataInit<'_, Self>,
) {
data_init.init(resource, ());
}
}
impl Dispatch<wl_subcompositor::WlSubcompositor, ()> for State {
fn request(
_state: &mut Self,
_client: &Client,
_resource: &wl_subcompositor::WlSubcompositor,
request: wl_subcompositor::Request,
_data: &(),
_handle: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
if let wl_subcompositor::Request::GetSubsurface {
id,
surface,
parent,
} = request
{
// Powierzchnia może mieć tylko jedną rolę; nie może być własnym rodzicem.
if surface == parent {
return;
}
let Some(sd) = surface.data::<SurfaceData>() else {
return;
};
if sd.subsurface.lock_or_recover().is_some() {
return;
}
data_init.init(
id,
SubsurfaceData {
surface: surface.clone(),
parent: parent.clone(),
},
);
*sd.subsurface.lock_or_recover() = Some(SubsurfaceState::new(parent.clone()));
if let Some(pd) = parent.data::<SurfaceData>() {
let mut list = pd.children.lock_or_recover();
list.push(surface.clone());
if std::env::var("SELENE_STATS").is_ok() {
eprintln!(
"selene/subsurface: nowa powierzchnia potomna (dzieci={})",
list.len()
);
}
}
}
}
}
impl Dispatch<wl_subsurface::WlSubsurface, SubsurfaceData> for State {
fn request(
_state: &mut Self,
_client: &wayland_server::Client,
_resource: &wl_subsurface::WlSubsurface,
request: wl_subsurface::Request,
data: &SubsurfaceData,
_handle: &DisplayHandle,
_data_init: &mut wayland_server::DataInit<'_, Self>,
) {
match request {
wl_subsurface::Request::SetPosition { x, y } => {
let Some(sd) = data.surface.data::<SurfaceData>() else {
return;
};
let mut guard = sd.subsurface.lock_or_recover();
if let Some(sub) = guard.as_mut() {
if sub.sync {
sub.pending_position = Some((x, y));
} else {
sub.position = (x, y);
}
}
}
wl_subsurface::Request::PlaceAbove { sibling } => {
restack(&data.parent, &data.surface, Some(&sibling), true);
}
wl_subsurface::Request::PlaceBelow { sibling } => {
restack(&data.parent, &data.surface, Some(&sibling), false);
}
wl_subsurface::Request::SetSync => set_sync(&data.surface, true),
wl_subsurface::Request::SetDesync => set_sync(&data.surface, false),
wl_subsurface::Request::Destroy => cleanup(&data.surface, &data.parent),
_ => {}
}
}
fn destroyed(
_state: &mut Self,
_client: wayland_server::backend::ClientId,
_resource: &wl_subsurface::WlSubsurface,
data: &SubsurfaceData,
) {
cleanup(&data.surface, &data.parent);
}
}
fn set_sync(surface: &WlSurface, sync: bool) {
let Some(sd) = surface.data::<SurfaceData>() else {
return;
};
let mut guard = sd.subsurface.lock_or_recover();
if let Some(sub) = guard.as_mut() {
// Przejście `sync` → `desync` natychmiast aplikuje oczekujący stan.
if !sync {
if let Some(p) = sub.pending_position.take() {
sub.position = p;
}
if let Some(img) = sub.pending.take() {
sub.image = Some(img);
}
}
sub.sync = sync;
}
}
/// Przenosi dziecko na wierzch/spód stosu albo tuż nad/pod `sibling`.
fn restack(parent: &WlSurface, child: &WlSurface, sibling: Option<&WlSurface>, above: bool) {
let Some(pd) = parent.data::<SurfaceData>() else {
return;
};
let mut list = pd.children.lock_or_recover();
let Some(from) = list.iter().position(|s| s == child) else {
return;
};
list.remove(from);
let idx = match sibling {
// `place_above(parent)` = nad wszystkimi braćmi, `place_below(parent)` = pod nimi.
Some(sib) if sib == parent => {
if above {
list.len()
} else {
0
}
}
Some(sib) => list
.iter()
.position(|s| s == sib)
.map(|i| if above { i + 1 } else { i })
.unwrap_or(if above { list.len() } else { 0 }),
None => {
if above {
list.len()
} else {
0
}
}
};
let len = list.len();
list.insert(idx.min(len), child.clone());
}
fn cleanup(surface: &WlSurface, parent: &WlSurface) {
if let Some(sd) = surface.data::<SurfaceData>() {
*sd.subsurface.lock_or_recover() = None;
}
if let Some(pd) = parent.data::<SurfaceData>() {
pd.children.lock_or_recover().retain(|s| s != surface);
}
}
/// Odczytuje bufor powierzchni do obrazu RGBA (premultiplied).
fn decode(data: &SurfaceData) -> Option<Arc<ImageData>> {
let buffer = data.buffer.lock_or_recover().clone()?;
let bd = buffer.data::<BufferData>()?;
let rgba = bd.to_rgba()?;
Some(Arc::new(ImageData::new(
bd.width as u32,
bd.height as u32,
rgba,
)))
}
/// Commit powierzchni potomnej: bufor trafia do obrazu od razu (`desync`)
/// albo czeka na commit rodzica (`sync`).
pub fn commit(state: &mut State, data: &SurfaceData) {
let image = decode(data);
// Bufor oddajemy klientowi raz na `attach` — tak samo jak dla okien.
if data.take_release() {
if let Some(buffer) = data.buffer.lock_or_recover().clone() {
let _ = buffer.send_event(wayland_server::protocol::wl_buffer::Event::Release);
}
}
let (parent, repaint) = {
let mut guard = data.subsurface.lock_or_recover();
let Some(sub) = guard.as_mut() else {
return;
};
let was_empty = sub.image.is_none();
match image {
Some(img) if sub.sync => sub.pending = Some(img),
Some(img) => {
if was_empty && std::env::var("SELENE_STATS").is_ok() {
eprintln!(
"selene/subsurface: pierwszy obraz desync {}x{}",
img.width, img.height
);
}
sub.image = Some(img);
}
None => {}
}
// Zmiana pozycji/obrazu w trybie `desync` działa natychmiast.
let repaint = !sub.sync;
(sub.parent.clone(), repaint)
};
if repaint {
repaint_parent(state, &parent);
}
}
/// Aplikuje oczekujące zmiany dzieci — wołane przy commicie rodzica.
pub fn apply_pending(parent: &WlSurface) {
let Some(pd) = parent.data::<SurfaceData>() else {
return;
};
// Listę kopiujemy, żeby nie trzymać blokady rodzica przy blokadach dzieci.
let children = pd.children.lock_or_recover().clone();
for child in children {
let Some(sd) = child.data::<SurfaceData>() else {
continue;
};
let mut guard = sd.subsurface.lock_or_recover();
if let Some(sub) = guard.as_mut() {
if let Some(p) = sub.pending_position.take() {
sub.position = p;
}
if let Some(img) = sub.pending.take() {
sub.image = Some(img);
}
}
}
}
/// Składa dzieci na obraz rodzica. Gdy dzieci nie mają jeszcze obrazu,
/// zwraca `base` bez kopiowania.
pub fn overlay_children(parent: &WlSurface, base: Arc<ImageData>) -> Arc<ImageData> {
let Some(pd) = parent.data::<SurfaceData>() else {
return base;
};
let children = pd.children.lock_or_recover().clone();
if children.is_empty() {
return base;
}
let mut out: Option<Vec<u8>> = None;
for child in children {
let Some(sd) = child.data::<SurfaceData>() else {
continue;
};
let (image, pos) = {
let guard = sd.subsurface.lock_or_recover();
match guard.as_ref() {
Some(sub) => (sub.image.clone(), sub.position),
None => continue,
}
};
let Some(img) = image else {
continue;
};
let dst = out.get_or_insert_with(|| base.rgba.clone());
blend(dst, base.width, base.height, &img, pos);
}
match out {
Some(rgba) => Arc::new(ImageData::new(base.width, base.height, rgba)),
None => base,
}
}
/// Przemalowuje okno rodzica po natychmiastowej zmianie dziecka (`desync`).
pub fn repaint_parent(state: &mut State, parent: &WlSurface) {
let Some(pd) = parent.data::<SurfaceData>() else {
return;
};
let Some(base) = pd.base_image.lock_or_recover().clone() else {
return;
};
let image = overlay_children(parent, base);
if let Some(slot) = *pd.slot.lock_or_recover() {
if let Some(w) = state.windows.lock_or_recover().get_mut(slot) {
w.data = image;
}
}
}
/// Nakłada obraz dziecka na bufor rodzica (premultiplied source-over) z przycięciem.
fn blend(dst: &mut [u8], dw: u32, dh: u32, src: &ImageData, pos: (i32, i32)) {
let sw = src.width as i32;
let sh = src.height as i32;
for sy in 0..sh {
let dy = pos.1 + sy;
if dy < 0 || dy >= dh as i32 {
continue;
}
for sx in 0..sw {
let dx = pos.0 + sx;
if dx < 0 || dx >= dw as i32 {
continue;
}
let si = ((sy as usize * sw as usize) + sx as usize) * 4;
let a = u32::from(src.rgba[si + 3]);
if a == 0 {
continue;
}
let di = (dy as usize * dw as usize + dx as usize) * 4;
if a == 255 {
dst[di..di + 4].copy_from_slice(&src.rgba[si..si + 4]);
} else {
let inv = 255 - a;
for c in 0..3 {
dst[di + c] =
(u32::from(src.rgba[si + c]) + u32::from(dst[di + c]) * inv / 255) as u8;
}
dst[di + 3] = (a + u32::from(dst[di + 3]) * inv / 255).min(255) as u8;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn base(rgba: Vec<u8>, w: u32, h: u32) -> Arc<ImageData> {
Arc::new(ImageData::new(w, h, rgba))
}
#[test]
fn opaque_child_replaces_pixels() {
let mut dst = vec![9, 9, 9, 255, 9, 9, 9, 255, 9, 9, 9, 255, 9, 9, 9, 255];
let src = ImageData::new(2, 1, vec![255, 0, 0, 255, 0, 255, 0, 255]);
blend(&mut dst, 2, 2, &src, (0, 0));
assert_eq!(&dst[0..4], &[255, 0, 0, 255]);
assert_eq!(&dst[4..8], &[0, 255, 0, 255]);
// Drugi wiersz nietknięty.
assert_eq!(&dst[8..12], &[9, 9, 9, 255]);
}
#[test]
fn half_transparent_child_is_blended_premultiplied() {
let mut dst = vec![0, 0, 0, 255];
// Premultiplied: pełne czerwone światło przy alpha 128 → R=128, A=128.
let src = ImageData::new(1, 1, vec![128, 0, 0, 128]);
blend(&mut dst, 1, 1, &src, (0, 0));
assert_eq!(dst[0], 128);
assert_eq!(dst[3], 255);
}
#[test]
fn child_is_clipped_to_parent_bounds() {
let mut dst = vec![0, 0, 0, 255];
let src = ImageData::new(2, 1, vec![255, 0, 0, 255, 0, 0, 255, 255]);
// Pozycja (-1,0): pierwszy piksel poza ekranem, drugi (zielony) trafia w x=0.
blend(&mut dst, 1, 1, &src, (-1, 0));
assert_eq!(&dst[0..4], &[0, 0, 255, 255]);
}
#[test]
fn fully_transparent_pixels_are_skipped() {
let mut dst = vec![7, 7, 7, 255];
let src = ImageData::new(1, 1, vec![255, 0, 0, 0]);
blend(&mut dst, 1, 1, &src, (0, 0));
assert_eq!(&dst[0..4], &[7, 7, 7, 255]);
}
}