🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
//! Globalny stan kompozytora Selene.
use std::sync::{Arc, Mutex};
use std::time::Instant;
use libluna_core::{Modifiers, Point, Rect};
use libluna_render::ImageData;
use wayland_protocols::ext::workspace::v1::server::{
ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1,
ext_workspace_handle_v1::ExtWorkspaceHandleV1, ext_workspace_manager_v1::ExtWorkspaceManagerV1,
};
use wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
use wayland_server::protocol::wl_data_device::WlDataDevice;
use wayland_server::protocol::wl_data_source::WlDataSource;
use wayland_server::protocol::wl_keyboard::WlKeyboard;
use wayland_server::protocol::wl_output::WlOutput;
use wayland_server::protocol::wl_pointer::WlPointer;
use wayland_server::protocol::wl_surface::WlSurface;
use crate::data::DndState;
use crate::monitors::{OutputInfo, Outputs};
use crate::sync::LockExt;
/// Wysokość paska tytułu dekoracji (SSD), w pikselach logicznych.
/// Wysokość naszego paska tytułu (SSD). Musi być równa
/// `theme.metrics.headerbar.height` — kompozytor rysuje pasek widgetem libLuny.
pub const TITLEBAR: f64 = 38.0;
/// Szerokość przycisku sterowania oknem (minimalizuj/maksymalizuj/zamknij).
pub const BUTTON_W: f64 = 30.0;
/// Odstęp kontrolek od prawej krawędzi paska (token `spacing.sm`).
const CONTROL_PAD: f64 = 6.0;
/// Odstęp między kontrolkami (token `headerbar.control_gap`).
const CONTROL_GAP: f64 = 2.0;
/// Grubość krawędzi wyłapywanej do zmiany rozmiaru.
pub const BORDER: f64 = 6.0;
/// Zapamiętana geometria przed maksymalizacją/fullscreenem (do przywrócenia).
#[derive(Debug, Clone, Copy)]
pub struct SavedGeom {
pub x: f64,
pub y: f64,
pub width: i32,
pub height: i32,
}
/// Okno (toplevel/popup) w modelu kompozycji: pozycja, rozmiar, kolejność i piksele.
pub struct Window {
/// Lewy-górny róg **widocznego** okna (`xdg_surface.set_window_geometry`),
/// nie bufora — bufor okien CSD zawiera dodatkowo cienie.
pub x: f64,
pub y: f64,
/// Rozmiar **widocznego** okna (bez cieni CSD). To on idzie w `configure`
/// i wyznacza trafienia; bufor klienta może być większy o `image_offset`.
pub width: i32,
pub height: i32,
/// Przesunięcie bufora względem widocznego okna (`xdg_surface.set_window_geometry`).
/// Cienie CSD są rysowane poza obrębem okna i nie powinny przesuwać trafień.
pub image_offset: (f64, f64),
/// Kolejność (wyższa = wyżej); do renderu i trafień.
pub z: i32,
/// Tytuł okna (z `xdg_toplevel.set_title`).
pub title: String,
/// Czy to `xdg_popup` (bez dekoracji, zamykany kliknięciem poza).
pub popup: bool,
/// Czy to powierzchnia `wlr-layer-shell` (panel/dock; bez dekoracji, kotwiczona).
pub layer: bool,
/// Czy powierzchnia jest widoczna (popup znika po `popup_done`).
pub mapped: bool,
/// Przestrzeń robocza, do której należy okno (panele/popupy są globalne).
pub workspace: usize,
/// Region wejścia powierzchni (`wl_surface.set_input_region`); `None` = cała.
pub input: Option<Vec<Rect>>,
/// Czy powierzchnia jawnie przechwytuje **wszystkie** kliknięcia (otwarty popup
/// powłoki). Wtedy klik poza jej widoczną treścią/poza buforem jest przekazywany
/// właśnie jej — dzięki temu popup zamyka się także po kliknięciu poza obrysem.
pub input_capture: bool,
/// Zminimalizowane (ukryte przez kompozytor; przywracane skrótem).
pub minimized: bool,
/// Zmaksymalizowane do obszaru monitora.
pub maximized: bool,
/// Pełny ekran.
pub fullscreen: bool,
/// Czy rysujemy **nasze** dekoracje (SSD). `true` tylko dla okien, które
/// negocjowały tryb `server_side`; klienty z własnymi dekoracjami (GTK, X11) — `false`.
pub ssd: bool,
/// Geometria zapamiętana przed maksymalizacją/fullscreenem.
pub saved: Option<SavedGeom>,
/// Piksele (premultiplied RGBA8) ostatnio zacommitowanego bufora.
pub data: Arc<ImageData>,
}
impl Window {
/// Czy okno pokazuje się w danej przestrzeni roboczej.
///
/// Powierzchnie warstwowe (pasek, tło, OSD) są globalne — należą do każdej
/// przestrzeni; zwykłe okna i popupy widoczne są tylko w swojej.
pub fn in_workspace(&self, ws: usize) -> bool {
self.layer || self.workspace == ws
}
/// Wysokość dekoracji (zero dla popupów, paneli i pełnego ekranu).
pub fn bar(&self) -> f64 {
// Pasek rysujemy **tylko** gdy okno o to poprosiło (`ssd`). Klienty GTK/
// libadwaita zawsze rysują własne dekoracje (CSD) — dokładanie naszych dawało
// podwójny pasek tytułu, a okna X11 (XWayland) swoje dekoracje przynoszą ze sobą.
if self.popup || self.layer || self.fullscreen || !self.ssd {
0.0
} else {
TITLEBAR
}
}
/// Prostokąt całego okna (dekoracja + treść).
pub fn outer(&self) -> Rect {
Rect::from_xywh(
self.x,
self.y,
self.width as f64,
self.bar() + self.height as f64,
)
}
/// Prostokąt, w którym rysujemy bufor klienta.
///
/// Dla okien CSD bufor jest większy od widocznego okna (cienie) — rysujemy go
/// przesuniętego o `image_offset`, a nie rozciągniętego do `content()`.
pub fn buffer_rect(&self) -> Rect {
let c = self.content();
Rect::from_xywh(
c.x() - self.image_offset.0,
c.y() - self.image_offset.1,
self.data.width as f64,
self.data.height as f64,
)
}
/// Pasek tytułu (zerowy dla popupów).
pub fn titlebar(&self) -> Rect {
Rect::from_xywh(self.x, self.y, self.width as f64, self.bar())
}
/// Obszar treści (klienta).
pub fn content(&self) -> Rect {
Rect::from_xywh(
self.x,
self.y + self.bar(),
self.width as f64,
self.height as f64,
)
}
/// Przycisk sterowania oknem licząc od prawej (0 = zamknij, 1 = maksymalizuj, 2 = minimalizuj).
fn control_rect(&self, from_right: usize) -> Rect {
// Kontrolki rysujemy **tylko** oknom z naszym paskiem (SSD). Wcześniej
// istniały też dla okien CSD — niewidzialne strefy close/max/min leżały
// wtedy na treści aplikacji i „czasem” zamykały/maksymalizowały okno
// zamiast przekazać klik do klienta.
if self.popup || self.layer || !self.ssd {
return Rect::ZERO;
}
// Układ jak w `libluna_widgets::HeaderBar`: kwadratowe kontrolki 30 px,
// 2 px odstępu, 6 px od prawej krawędzi, wyśrodkowane w pionie.
let side = (TITLEBAR - BUTTON_W) / 2.0;
Rect::from_xywh(
self.x + self.width as f64
- CONTROL_PAD
- BUTTON_W * (from_right as f64 + 1.0)
- CONTROL_GAP * from_right as f64,
self.y + side,
BUTTON_W,
BUTTON_W,
)
}
/// Kwadratowy przycisk zamknięcia (tylko dla topleveli).
pub fn close_rect(&self) -> Rect {
self.control_rect(0)
}
/// Przycisk maksymalizacji.
pub fn maximize_rect(&self) -> Rect {
self.control_rect(1)
}
/// Przycisk minimalizacji.
pub fn minimize_rect(&self) -> Rect {
self.control_rect(2)
}
/// Bitmaska krawędzi wyłapywanej do zmiany rozmiaru dla punktu (0 gdy brak).
/// 1 = góra, 2 = dół, 4 = lewo, 8 = prawo.
pub fn resize_edges(&self, p: Point) -> u8 {
if self.popup || self.layer || self.fullscreen || self.maximized {
return 0;
}
let o = self.outer();
if !o.contains(p) {
return 0;
}
let mut e = 0u8;
if p.x <= o.x() + BORDER {
e |= 4;
}
if p.x >= o.right() - BORDER {
e |= 8;
}
if p.y <= o.y() + BORDER {
e |= 1;
}
if p.y >= o.bottom() - BORDER {
e |= 2;
}
e
}
}
/// Współdzielony model okien (pisany przez wątek protokołu, czytany przez wyjście).
pub type WindowModel = Arc<Mutex<Vec<Window>>>;
/// Żądanie zrzutu ekranu (`Super+F12`).
///
/// Kompozytor ma w pamięci gotową klatkę każdego monitora, więc zrzut to zapis tych
/// buforów do PNG — bez odpytywania DRM. Flagę ustawia wątek protokołu (skrót),
/// a realizuje wątek wyjścia.
pub type ScreenshotModel = Arc<std::sync::atomic::AtomicBool>;
/// Siła night lightu (0 = wyłączony, 100 = najmocniej ciepło).
pub type NightLightModel = Arc<std::sync::atomic::AtomicU8>;
/// Tryb oszczędzania energii (kompozytor ogranicza wtedy tempo prezentacji).
pub type PowerSaverModel = Arc<std::sync::atomic::AtomicBool>;
/// Współdzielona pozycja kursora (tryb DRM/evdev); `None` = nie rysujemy kursora.
pub type CursorModel = Arc<Mutex<Option<Point>>>;
/// Współdzielony numer bieżącej przestrzeni roboczej (kompozytor ↔ wątek wyjścia).
pub type WorkspaceModel = Arc<Mutex<usize>>;
/// Stan podglądu okien/przestrzeni (współdzielony z wątkiem wyjścia).
#[derive(Debug, Clone, Copy, Default)]
pub struct Overview {
/// Czy podgląd jest włączony.
pub on: bool,
/// Przeciągane okno: `(slot, pozycja kursora)`.
pub drag: Option<(usize, (f64, f64))>,
}
/// Współdzielony tryb podglądu okien/przestrzeni (kompozytor ↔ wątek wyjścia).
pub type OverviewModel = Arc<Mutex<Overview>>;
/// Margines podglądu (od krawędzi ekranu).
pub const OVERVIEW_PAD: f64 = 56.0;
/// Odstęp między kafelkami podglądu.
pub const OVERVIEW_GAP: f64 = 16.0;
/// Wysokość pasa „chipsów” przestrzeni u dołu podglądu.
pub const OVERVIEW_STRIP_H: f64 = 56.0;
/// Wysokość paska tytułu kafelka.
pub const OVERVIEW_TITLE: f64 = 26.0;
/// Sloty okien bieżącej przestrzeni (bez paneli/popupów), w kolejności z-order.
#[allow(dead_code)]
pub fn overview_slots(windows: &[Window], current_ws: usize) -> Vec<usize> {
let mut v: Vec<(usize, i32)> = windows
.iter()
.enumerate()
.filter(|(_, w)| {
w.mapped && !w.minimized && !w.popup && !w.layer && w.workspace == current_ws
})
.map(|(i, w)| (i, w.z))
.collect();
v.sort_by_key(|&(_, z)| z);
v.into_iter().map(|(i, _)| i).collect()
}
/// Sloty okien bieżącej przestrzeni, których **środek** leży na wyjściu `[ox, ox+w)`.
pub fn overview_slots_on(windows: &[Window], current_ws: usize, ox: f64, w: f64) -> Vec<usize> {
let mut v: Vec<(usize, i32)> = windows
.iter()
.enumerate()
.filter(|(_, win)| {
win.mapped && !win.minimized && !win.popup && !win.layer && win.workspace == current_ws
})
.filter(|(_, win)| {
let cx = win.x + win.width as f64 / 2.0;
cx >= ox && cx < ox + w
})
.map(|(i, w)| (i, w.z))
.collect();
v.sort_by_key(|&(_, z)| z);
v.into_iter().map(|(i, _)| i).collect()
}
/// Prostokąty kafelków w siatce na obszarze `size` (zostawiając pas u dołu).
pub fn overview_tiles(count: usize, size: (f64, f64)) -> Vec<Rect> {
if count == 0 {
return Vec::new();
}
let cols = (count as f64).sqrt().ceil() as usize;
let cols = cols.max(1);
let rows = count.div_ceil(cols);
let avail_w = (size.0 - 2.0 * OVERVIEW_PAD).max(1.0);
let avail_h = (size.1 - 2.0 * OVERVIEW_PAD - OVERVIEW_STRIP_H).max(1.0);
let tw = ((avail_w - (cols - 1) as f64 * OVERVIEW_GAP) / cols as f64).max(1.0);
let th = ((avail_h - (rows - 1) as f64 * OVERVIEW_GAP) / rows as f64).max(1.0);
let grid_w = cols as f64 * tw + (cols - 1) as f64 * OVERVIEW_GAP;
let grid_h = rows as f64 * th + (rows - 1) as f64 * OVERVIEW_GAP;
let x0 = (size.0 - grid_w) / 2.0;
let y0 = OVERVIEW_PAD + ((avail_h - grid_h) / 2.0).max(0.0);
(0..count)
.map(|k| {
let c = (k % cols) as f64;
let r = (k / cols) as f64;
Rect::from_xywh(
x0 + c * (tw + OVERVIEW_GAP),
y0 + r * (th + OVERVIEW_GAP),
tw,
th,
)
})
.collect()
}
/// Prostokąty „chipsów” przestrzeni na dole podglądu.
pub fn overview_ws_chips(count: usize, size: (f64, f64)) -> Vec<Rect> {
if count == 0 {
return Vec::new();
}
let (w, h, gap) = (128.0, 36.0, 12.0);
let total = count as f64 * w + (count - 1) as f64 * gap;
let x0 = (size.0 - total) / 2.0;
let y = size.1 - OVERVIEW_PAD - h;
(0..count)
.map(|i| Rect::from_xywh(x0 + i as f64 * (w + gap), y, w, h))
.collect()
}
/// Trwające przeciąganie okna.
#[derive(Debug, Clone, Copy)]
pub struct DragState {
pub slot: usize,
/// Przesunięcie kursora względem lewego-górnego narożnika okna w chwili wciśnięcia.
pub off_x: f64,
pub off_y: f64,
/// Pozycja kursora w chwili rozpoczęcia przeciągania — pozwala odróżnić
/// zwykły klik na pasku tytułu od realnego przeciągnięcia do krawędzi.
pub start_px: f64,
pub start_py: f64,
/// Geometria okna w chwili rozpoczęcia przeciągania. To ona wraca jako rozmiar
/// domyślny po przyciągnięciu do krawędzi (a nie pozycja upuszczenia).
pub origin: SavedGeom,
}
/// Trwające przeskalowanie okna (za dowolną krawędź/narożnik).
#[derive(Debug, Clone, Copy)]
pub struct ResizeState {
pub slot: usize,
/// Bitmaska krawędzi (1=góra, 2=dół, 4=lewo, 8=prawo).
pub edges: u8,
pub start_x: f64,
pub start_y: f64,
pub start_w: f64,
pub start_h: f64,
pub start_px: f64,
pub start_py: f64,
}
/// Stan współdzielony przez wszystkie handlery protokołu.
pub struct State {
/// Start pracy kompozytora — do znaczników czasu (frame callbacks).
pub start: Instant,
/// Licznik seriali (dla `xdg_surface.configure` itp.).
serial: u32,
/// Liczniki diagnostyczne.
pub clients: u32,
pub surfaces: u32,
pub toplevels: u32,
/// Model okien współdzielony z wątkiem wyjścia.
pub windows: WindowModel,
/// Pozycja kursora do narysowania (tryb DRM/evdev), współdzielona z wątkiem wyjścia.
pub cursor: CursorModel,
/// Ostatnie wciśnięcie lewego przycisku na pasku tytułu: `(slot, czas)`.
/// Pozwala rozpoznać podwójny klik (maksymalizacja/przywrcenie domyślnego rozmiaru).
pub last_title_click: Option<(usize, Instant)>,
/// Powierzchnie okien (indeks = slot w [`State::windows`]).
pub slots: Vec<WlSurface>,
/// Licznik kolejności (z-order).
z_counter: i32,
/// Monitory (współdzielone z wątkiem wyjścia).
pub outputs: Outputs,
/// Podłączeni menedżerowie `zwlr_foreign_toplevel` (paski/taskbary).
pub toplevel_managers: Vec<ZwlrForeignToplevelManagerV1>,
// — Przestrzenie robocze (`ext_workspace_v1`) —
/// Liczba przestrzeni roboczych.
pub ws_count: usize,
/// Bieżąca przestrzeń robocza (współdzielona z wątkiem wyjścia).
pub ws_current: WorkspaceModel,
/// Tryb podglądu okien/przestrzeni (`Super+Tab`), współdzielony z wątkiem wyjścia.
pub overview: OverviewModel,
/// Podłączeni menedżerowie `ext_workspace` oraz ich grupy i uchwyty przestrzeni.
pub ws_managers: Vec<ExtWorkspaceManagerV1>,
pub ws_groups: Vec<ExtWorkspaceGroupHandleV1>,
pub ws_handles: Vec<ExtWorkspaceHandleV1>,
// — Wejście —
/// Wskaźniki/klawiatury **per klient**. Jeden globalny zasób prowadził do wysyłania
/// zdarzeń z powierzchnią obcego klienta (panic „objects from wrong client”) oraz do
/// braku wejścia w aplikacjach uruchomionych z launchera.
pub pointers: Vec<WlPointer>,
pub keyboards: Vec<WlKeyboard>,
pub pointer_focus: Option<(usize, WlSurface)>,
pub keyboard_focus: Option<(usize, WlSurface)>,
/// Modyfikatory, o których już powiadomiliśmy klienta klawiatury.
pub sent_mods: Modifiers,
/// Blokady (`wl_keyboard.modifiers.mods_locked`): CapsLock i NumLock.
/// Bez tego klienty mogły ignorować stan blokad, a numeryczna klawiatura
/// dawała kody nawigacji zamiast cyfr/przecinków.
pub caps_lock: bool,
pub num_lock: bool,
/// Bieżąca selekcja (schowek): źródło danych.
pub selection: Option<WlDataSource>,
/// Urządzenia schowka zarejestrowane przez klientów (do rozsyłania selekcji).
pub data_devices: Vec<WlDataDevice>,
/// Zasoby `wl_output` związane przez klientów. Trzymamy je, żeby po faktycznym
/// modesecie DRM wysłać poprawione `geometry`/`mode`/`scale`.
pub bound_outputs: Vec<(usize, WlOutput)>,
/// Sygnatura listy monitorów ostatnio rozesłanej klientom.
pub outputs_sig: u64,
/// Ile globali `wl_output` już utworzono (może się zwiększyć po faktycznym
/// wykryciu większej liczby monitorów przez DRM).
pub output_globals: usize,
/// Stan trwającego przeciągania (drag & drop).
pub dnd: Option<DndState>,
/// Ostatnia pozycja wskaźnika (współrzędne globalne).
pub pointer_pos: (f64, f64),
/// Czy lewy przycisk wskaźnika jest wciśnięty. Potrzebne, bo `xdg_toplevel.move`
/// przychodzi od klienta **asynchronicznie** — przy bardzo szybkim kliknięciu
/// docierał już po zwolnieniu przycisku i zostawiał „przyklejone” przeciąganie.
pub left_down: bool,
/// Strefa wyłączna górnych paneli, **per indeks wyjścia**. Panel jest tylko na
/// jednym monitorze — drugi nie może dostawać u góry pustej szczeliny.
pub top_insets: std::collections::HashMap<usize, f64>,
/// Trwające przeciąganie okna.
pub drag: Option<DragState>,
/// Trwające przeskalowanie okna.
pub resize: Option<ResizeState>,
/// Żądanie zrzutu ekranu (Super+F12) — przełącza je wątek wyjścia.
pub screenshot: ScreenshotModel,
/// Siła night lightu (gamma LUT) — ustawiana z powłoki przez gniazdo sterowania.
pub night_light: NightLightModel,
/// Tryb oszczędzania (limit tempa prezentacji w wątku wyjścia).
pub power_saver: PowerSaverModel,
}
impl Default for State {
fn default() -> Self {
Self {
start: Instant::now(),
serial: 0,
clients: 0,
surfaces: 0,
toplevels: 0,
windows: Arc::new(Mutex::new(Vec::new())),
cursor: Arc::new(Mutex::new(None)),
slots: Vec::new(),
z_counter: 1,
outputs: Arc::new(Mutex::new(vec![OutputInfo::default()])),
toplevel_managers: Vec::new(),
ws_count: 4,
ws_current: Arc::new(Mutex::new(0)),
overview: Arc::new(Mutex::new(Overview::default())),
ws_managers: Vec::new(),
ws_groups: Vec::new(),
ws_handles: Vec::new(),
pointers: Vec::new(),
keyboards: Vec::new(),
pointer_focus: None,
keyboard_focus: None,
sent_mods: Modifiers::NONE,
caps_lock: false,
num_lock: false,
selection: None,
data_devices: Vec::new(),
bound_outputs: Vec::new(),
outputs_sig: 0,
output_globals: 0,
dnd: None,
pointer_pos: (0.0, 0.0),
left_down: false,
top_insets: std::collections::HashMap::new(),
last_title_click: None,
drag: None,
resize: None,
screenshot: Arc::new(std::sync::atomic::AtomicBool::new(false)),
night_light: Arc::new(std::sync::atomic::AtomicU8::new(0)),
power_saver: Arc::new(std::sync::atomic::AtomicBool::new(false)),
}
}
}
impl State {
/// Kolejny serial protokołu.
pub fn next_serial(&mut self) -> u32 {
self.serial = self.serial.wrapping_add(1).max(1);
self.serial
}
/// Czas od startu w milisekundach (dla `wl_callback.done`).
pub fn time_ms(&self) -> u32 {
self.start.elapsed().as_millis() as u32
}
/// Kolejna wartość z-order (podnosi okno na wierzch).
pub fn next_z(&mut self) -> i32 {
self.z_counter += 1;
self.z_counter
}
/// Czy trwa podgląd okien/przestrzeni.
pub fn overview_on(&self) -> bool {
self.overview.lock_or_recover().on
}
/// Włącza/wyłącza podgląd okien/przestrzeni (wyłączenie kończy przeciąganie).
pub fn set_overview(&self, on: bool) {
let mut o = self.overview.lock_or_recover();
o.on = on;
if !on {
o.drag = None;
}
}
/// Ustawia przeciągane okno podglądu: `(slot, pozycja kursora)`.
pub fn set_overview_drag(&self, drag: Option<(usize, (f64, f64))>) {
self.overview.lock_or_recover().drag = drag;
}
/// Aktualizuje tylko pozycję przeciąganego okna (zachowując slot).
pub fn move_overview_drag(&self, pos: (f64, f64)) {
let mut o = self.overview.lock_or_recover();
if let Some((slot, _)) = o.drag {
o.drag = Some((slot, pos));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serials_are_non_zero_and_increasing() {
let mut s = State::default();
let a = s.next_serial();
let b = s.next_serial();
assert!(a >= 1);
assert!(b > a);
}
#[test]
fn z_order_increases() {
let mut s = State::default();
let a = s.next_z();
let b = s.next_z();
assert!(b > a);
}
fn window_at_ws(ws: usize, z: i32) -> Window {
Window {
x: 0.0,
y: 0.0,
width: 10,
height: 10,
image_offset: (0.0, 0.0),
z,
title: String::new(),
popup: false,
layer: false,
mapped: true,
workspace: ws,
input: None,
input_capture: false,
minimized: false,
maximized: false,
fullscreen: false,
ssd: true,
saved: None,
data: std::sync::Arc::new(libluna_render::ImageData::empty(1, 1)),
}
}
#[test]
fn overview_slots_filter_and_order() {
let mut panel = window_at_ws(0, 100);
panel.layer = true;
let ws = vec![
window_at_ws(0, 5),
window_at_ws(1, 9),
window_at_ws(0, 1),
panel,
];
// Tylko okna bieżącej przestrzeni, bez panelu, w kolejności z.
assert_eq!(overview_slots(&ws, 0), vec![2, 0]);
assert_eq!(overview_slots(&ws, 1), vec![1]);
}
#[test]
fn overview_slots_split_by_output() {
let mut a = window_at_ws(0, 1);
a.x = 100.0;
a.width = 200; // środek 200 → monitor 0
let mut b = window_at_ws(0, 2);
b.x = 2000.0;
b.width = 100; // środek 2050 → monitor 1
let ws = vec![a, b];
assert_eq!(overview_slots_on(&ws, 0, 0.0, 1920.0), vec![0]);
assert_eq!(overview_slots_on(&ws, 0, 1920.0, 1280.0), vec![1]);
assert!(overview_slots_on(&ws, 1, 0.0, 1920.0).is_empty());
}
#[test]
fn overview_tiles_and_chips_layout() {
let size = (1920.0, 1080.0);
assert!(overview_tiles(0, size).is_empty());
let four = overview_tiles(4, size);
assert_eq!(four.len(), 4);
// Drugi kafelek na prawo od pierwszego i bez nachodzenia.
assert!(four[1].x() > four[0].x());
assert!(four[0].right() <= four[1].x());
for r in &four {
assert!(r.x() >= 0.0 && r.right() <= size.0 + 0.5);
assert!(r.bottom() <= size.1 + 0.5);
}
let chips = overview_ws_chips(4, size);
assert_eq!(chips.len(), 4);
assert!(chips[0].x() < chips[3].x());
}
#[test]
fn csd_buffer_is_drawn_shifted_by_window_geometry() {
// Okno CSD: widoczny prostokąt to (100,50)+200x120, a bufor 228x144
// (cienie 14/12 po każdej stronie). Bufor rysujemy od (86,38), nie od (100,50).
let mut w = window_at_ws(0, 1);
w.x = 100.0;
w.y = 50.0;
w.width = 200;
w.height = 120;
w.ssd = false;
w.image_offset = (14.0, 12.0);
w.data = std::sync::Arc::new(libluna_render::ImageData::empty(228, 144));
let b = w.buffer_rect();
assert_eq!(
(b.x(), b.y(), b.width(), b.height()),
(86.0, 38.0, 228.0, 144.0)
);
// Trafienia liczymy od widocznego okna, nie od cienia.
assert!(w.content().contains(Point::new(100.0, 50.0)));
assert!(!w.content().contains(Point::new(95.0, 45.0)));
}
}