🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242
//! `PaganWindow` — element przestrzeni (Space) reprezentujący jedno okno.
//!
//! PO CO istnieje ten wrapper, skoro Smithay ma już `Window`?
//!
//! 1. **Dekoracje serwerowe (SSD) w Z-orderze okna.** Pasek tytułu, menu i treść
//! muszą być narysowane razem i trafiać na wierzch stosu jako jedna grupa.
//! Smithay pozwala dołączyć własne elementy renderowania tylko do typu, który
//! sam implementuje `AsRenderElements`. Owinięcie `Window` pozwala nam
//! "wstrzyknąć" elementy paska bezpośrednio przy elementach tego okna.
//! 2. **Miejsce na logikę dekoracji** (hit-testy, menu, stan aktywności).
//!
//! Pasek i menu rysujemy **własnym shaderem GLSL** (`shaders/rounded_corners.frag`)
//! przez `PixelShaderElement`. Dlatego typy renderowania są specjalizowane do
//! `GlesRenderer` (pixel shadery Smithaya działają tylko z konkretnym
//! rendererem) — to nie ogranicza nas w praktyce, bo używamy `GlesRenderer`
//! zarówno w backendzie winit, jak i udev.
//!
//! Uwaga: w specyfikacji pojawiało się `Space<Window>` — używamy
//! `Space<PaganWindow>` z powodów opisanych wyżej (patrz README).
use std::sync::atomic::{AtomicBool, Ordering};
use smithay::{
backend::renderer::{
element::{
memory::MemoryRenderBufferRenderElement, surface::WaylandSurfaceRenderElement,
AsRenderElements, Kind,
},
gles::{element::PixelShaderElement, GlesRenderer},
Color32F,
},
desktop::{space::SpaceElement, Window, WindowSurface},
output::Output,
reexports::{
wayland_protocols::xdg::shell::server::xdg_toplevel,
wayland_server::protocol::wl_surface::WlSurface,
},
render_elements,
utils::{user_data::UserDataMap, IsAlive, Logical, Physical, Point, Rectangle, Scale, Size},
wayland::{
compositor::{with_states, SurfaceData as WlSurfaceData},
seat::WaylandFocus,
shell::xdg::{ToplevelSurface, XdgToplevelSurfaceData},
},
};
use crate::{icons, render, text, theme};
// --- Dekoracje serwerowe (SSD) ------------------------------------------------
//
// Styl: ciemny, płaski, monochromatyczne glify (bez kolorowych \"świateł\").
// Inspiracja nowoczesnymi pulpitami (GNOME), ale z własnym, chłodnym akcentem
// PaganDE — dlatego ikona zamykania jest delikatnie czerwona, a nie szara.
/// Wysokość paska tytułu rysowanego przez kompozytor.
pub const TITLEBAR_HEIGHT: i32 = 40;
/// Promień pola trafienia przycisku (używany w hit-testingu).
const BUTTON_RADIUS: i32 = 10;
/// Odległość środka skrajnego przycisku (Menu/Zamknij) od krawędzi paska.
const BUTTON_MARGIN: i32 = 24;
/// Odstęp między środkami przycisków okna po prawej.
const BUTTON_SPACING: i32 = 30;
/// Grubość cienkiej linii oddzielającej pasek od treści (px).
const BORDER_HEIGHT: i32 = 1;
/// Lewy margines paska (odstęp pierwszej etykiety/burgera od krawędzi).
const BAR_PADDING_X: i32 = 12;
/// Wewnętrzny odstęp poziomy klikalnej etykiety menu (z każdej strony).
const MENU_LABEL_PAD_X: i32 = 10;
/// Odstęp górny/dolny pola etykiety menu od krawędzi paska.
const MENU_LABEL_PAD_Y: i32 = 6;
/// Tło panelu menu wyprowadzamy z koloru paska (patrz `render_elements`).
/// Szerokość panelu menu okna.
const MENU_PANEL_WIDTH: i32 = 176;
/// Wysokość jednego wiersza menu okna.
const MENU_ROW_HEIGHT: i32 = 36;
/// Wewnętrzny odstęp panelu menu (góra/dół).
const MENU_PANEL_PADDING: i32 = 6;
/// Liczba wierszy menu okna (minimalizuj / maksymalizuj / zamknij).
const MENU_ROWS: i32 = 3;
/// Poziomy środek ikony w wierszu menu (liczony od lewej krawędzi panelu).
const MENU_ROW_ICON_X: i32 = 24;
/// Odstęp napisu wiersza menu od jego ikony (px).
const MENU_ROW_LABEL_OFFSET: i32 = 18;
/// Lewy margines napisu w wierszu MENU APLIKACJI (bez ikony, px).
const MENU_LABEL_ROW_PAD_X: i32 = 16;
/// Który przycisk paska tytułu trafiono.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TitlebarButton {
/// „Burger" po lewej — rozwija menu okna.
Menu,
Minimize,
Maximize,
Close,
}
/// Co pokazuje rozwinięcie spod burgera (gdy w pasku nie ma miejsca na etykiety).
///
/// PO CO rozróżniamy: menu APLIKACJI (Plik/Edycja/…, publikowane przez klienta)
/// to coś innego niż menu OKNA (minimalizuj/maksymalizuj/zamknij). Akcje okna
/// mamy już jako przyciski po prawej, więc burger bez własnego menu klienta jest
/// tylko awaryjnym menu okna — a nie duplikatem przycisków.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SsdMenuKind {
/// Menu aplikacji: etykiety górnego poziomu od klienta. Klik odsyłamy
/// klientowi (`clicked`), bo tylko on zna zawartość rozwinięcia.
AppMenu,
/// Menu okna dla klientów bez własnego menu (np. terminale X11).
Window,
}
/// Stan dekoracji okna trzymany w danych użytkownika okna.
///
/// PO CO w danych okna, a nie w `MyCompositor`: dekorację rysujemy razem z oknem
/// w `AsRenderElements`, a ta metoda nie widzi stanu kompozytora. `AtomicBool`
/// wystarcza — potrzebujemy tylko flag „menu otwarte" i „okno aktywne".
#[derive(Debug, Default)]
struct SsdState {
open: AtomicBool,
focused: AtomicBool,
}
// Styl paska i menu opisują stałe poniżej; kolory pochodzą z motywu
// (`theme::Theme`) — patrz `~/.config/pagan/theme.conf`.
/// Marker w danych powierzchni: klient **negocjował** dekoracje
/// (`zxdg_toplevel_decoration_v1`).
///
/// PO CO: tylko wtedy możemy wymusić tryb serwerowy i narysować własny pasek
/// tytułu. Klienci, którzy tego protokołu nie znają (np. GTK3 — `xfce4-terminal`,
/// `thunar`), rysują WŁASNE dekoracje (CSD). Dołożenie drugiego paska
/// nakładałoby je na siebie — to właśnie efekt „przesuniętej dekoracji”.
#[derive(Debug)]
pub struct NegotiatedSsd;
/// Zapisuje fakt negocjacji dekoracji przez klienta (wołane z `new_decoration`).
///
/// PO CO osobna funkcja: `XdgDecorationHandler` żyje w `state.rs`, a decyzję
/// „czy rysować SSD” podejmuje `PaganWindow` w tym module — marker w danych
/// powierzchni łączy oba miejsca bez przeciągania stanu przez cały kompozytor.
pub fn mark_ssd_negotiated(toplevel: &ToplevelSurface) {
with_states(toplevel.wl_surface(), |states| {
states.data_map.insert_if_missing(|| NegotiatedSsd);
});
}
/// Czy klient negocjował dekoracje serwerowe (patrz `PaganWindow::wants_ssd`)?
fn ssd_negotiated(toplevel: &ToplevelSurface) -> bool {
with_states(toplevel.wl_surface(), |states| {
states.data_map.get::<NegotiatedSsd>().is_some()
})
}
/// Reguła decyzji o dekoracjach serwerowych — CZYSTA, żeby dała się przetestować
/// bez żywych powierzchni Waylanda.
///
/// * `x11`: `Some(override_redirect)` dla okna X11, `None` dla okna Waylanda.
/// * `toplevel`: czy to okno najwyższego poziomu (nie popup/subsurface).
/// * `negotiated`: czy klient wynegocjował `zxdg_toplevel_decoration_v1`.
///
/// PO CO taka separacja: X11 nie zna pojęcia CSD, więc zawsze dostaje nasz pasek
/// (poza menu/podpowiedziami `override-redirect`). Okno Waylanda dostaje pasek
/// tylko wtedy, gdy oddało dekoracje kompozytorowi — inaczej (np. GTK3)
/// nakładalibyśmy drugi pasek na jego własny.
fn ssd_decision(x11: Option<bool>, toplevel: bool, negotiated: bool) -> bool {
match x11 {
Some(override_redirect) => !override_redirect,
None => toplevel && negotiated,
}
}
/// Publiczny dostęp do faktu negocjacji — używany w logach diagnostycznych.
pub fn decoration_negotiated(window: &PaganWindow) -> bool {
window.toplevel().is_some_and(ssd_negotiated)
}
#[derive(Debug, Clone, PartialEq)]
pub struct PaganWindow(pub Window);
impl PaganWindow {
/// Tworzy element przestrzeni z powierzchni toplevel (xdg-shell).
pub fn new(toplevel: ToplevelSurface) -> Self {
PaganWindow(Window::new_wayland_window(toplevel))
}
/// Tworzy element przestrzeni z okna X11 (XWayland).
///
/// PO CO osobny konstruktor: aplikacje X11 są zarządzane przez nasz `X11Wm`,
/// a nie przez xdg-shell — ale w przestrzeni i w renderowaniu zachowują się
/// identycznie (XWayland daje każdemu oknu `wl_surface`).
pub fn new_x11(surface: smithay::xwayland::X11Surface) -> Self {
PaganWindow(Window::new_x11_window(surface))
}
/// Powierzchnia X11 tego okna (jeśli to okno X11 — z XWayland).
pub fn x11_surface(&self) -> Option<&smithay::xwayland::X11Surface> {
self.0.x11_surface()
}
/// Powierzchnia Waylanda tego okna.
pub fn wl_surface(&self) -> Option<std::borrow::Cow<'_, WlSurface>> {
self.0.wl_surface()
}
/// Geometria okna w przestrzeni logicznej (bez ramki SSD).
pub fn geometry(&self) -> Rectangle<i32, Logical> {
SpaceElement::geometry(&self.0)
}
/// Bazowy toplevel (configure/move/resize).
pub fn toplevel(&self) -> Option<&ToplevelSurface> {
self.0.toplevel()
}
/// Musi być wołane po commicie powierzchni okna, żeby zaktualizować bbox.
pub fn on_commit(&self) {
self.0.on_commit()
}
/// Uruchamia domknięcie na wszystkich powierzchniach okna.
pub fn with_surfaces<F>(&self, processor: F)
where
F: FnMut(&WlSurface, &WlSurfaceData),
{
self.0.with_surfaces(processor)
}
/// Wysyła frame callback do klienta (VSync).
pub fn send_frame<T, F>(
&self,
output: &Output,
time: T,
throttle: Option<std::time::Duration>,
primary: F,
) where
T: Into<std::time::Duration>,
F: FnMut(&WlSurface, &WlSurfaceData) -> Option<Output> + Copy,
{
self.0.send_frame(output, time, throttle, primary)
}
/// Powierzchnia bazowa (Wayland) — w tym szkieletie zawsze Wayland (CSD).
///
/// PO CO zwracamy referencję: `Window::underlying_surface()` zwraca `&WindowSurface`,
/// a klonowanie powierzchni przy każdym odczycie byłoby marnotrawstwem.
pub fn underlying_surface(&self) -> &WindowSurface {
self.0.underlying_surface()
}
/// Trafienie w okno: zwraca powierzchnię pod punktem (lokalnie w oknie).
pub fn surface_under(
&self,
location: impl Into<Point<f64, Logical>>,
window_type: smithay::desktop::WindowSurfaceType,
) -> Option<(WlSurface, Point<i32, Logical>)> {
self.0.surface_under(location, window_type)
}
pub fn user_data(&self) -> &UserDataMap {
self.0.user_data()
}
/// Czy to okno ma dostać nasz pasek tytułu (dekoracja serwerowa)?
///
/// PO CO wymuszamy to dla WSZYSTKICH okien najwyższego poziomu: chcemy
/// spójnego wyglądu całego środowiska. Aplikacje obce (X11, GTK) nie znają
/// naszego stylu, więc gdyby rysowały własny pasek, każda wyglądałaby inaczej.
/// Negocjacja `xdg-decoration` i tak wymusza tryb serwerowy (patrz `state.rs`),
/// więc klient nie dorysuje drugiego paska.
///
/// WYJĄTEK: okna X11 typu `override-redirect` (menu, podpowiedzi, popupy,
/// ikony przeciągania) to elementy aplikacji, a nie samodzielne okna —
/// pasek nad nimi wyglądałby jak „przesunięta dekoracja” przyklejona do menu.
pub fn wants_ssd(&self) -> bool {
let x11 = self
.x11_surface()
.map(|surface| surface.is_override_redirect());
ssd_decision(
x11,
self.toplevel().is_some(),
self.toplevel().is_some_and(ssd_negotiated),
)
}
/// Dane użytkownika okna z flagami dekoracji (tworzone leniwie).
fn ssd_state(&self) -> &SsdState {
self.0
.user_data()
.get_or_insert_threadsafe(SsdState::default)
}
/// Czy rozwinięte jest menu okna (dropdown z burgera).
pub fn ssd_menu_open(&self) -> bool {
self.ssd_state().open.load(Ordering::Relaxed)
}
/// Ustawia stan menu okna (otwarte/zamknięte).
pub fn set_ssd_menu_open(&self, open: bool) {
self.ssd_state().open.store(open, Ordering::Relaxed);
}
/// Czy okno jest aktywne (ma fokus klawiatury).
///
/// PO CO: firmowy wygląd PaganDE wyróżnia okno aktywne (jaśniejszy, płaski
/// pasek), a pozostałe przygasza. Bez tego stanu wszystkie okna wyglądałyby
/// identycznie.
pub fn is_focused(&self) -> bool {
self.ssd_state().focused.load(Ordering::Relaxed)
}
/// Ustawia stan aktywności okna (wołane z `SeatHandler::focus_changed`).
pub fn set_focused(&self, focused: bool) {
self.ssd_state().focused.store(focused, Ordering::Relaxed);
}
/// Tytuł okna (Wayland: `xdg_toplevel`; X11: `_NET_WM_NAME`).
pub fn window_title(&self) -> Option<String> {
if let Some(toplevel) = self.toplevel() {
let surface = toplevel.wl_surface().clone();
return with_states(&surface, |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.and_then(|data| data.lock().ok())
.and_then(|data| data.title.clone())
})
.filter(|value| !value.is_empty());
}
self.x11_surface()
.map(|x11| x11.title())
.filter(|value| !value.is_empty())
}
/// Czy pasek ma pokazać pełne etykiety menu (zamiast burgera)?
///
/// Warunki: klient opublikował menu (własny protokół `pagan_app_menu_v1`)
/// ORAZ okno jest wystarczająco szerokie. Wąskie okno zwija menu do burgera,
/// żeby nie zabrakło miejsca na tytuł.
pub fn ssd_bar_uses_labels(&self) -> bool {
!crate::app_menu::labels(self).is_empty()
&& self.geometry().size.w >= theme::current().ssd_menu_min_width
}
/// Prostokąty pól etykiet menu aplikacji (układ w pasku SSD).
pub fn ssd_menu_label_rects(
&self,
location: Point<i32, Logical>,
) -> Vec<Rectangle<i32, Logical>> {
let labels = crate::app_menu::labels(self);
if labels.is_empty() {
return Vec::new();
}
let refs: Vec<&str> = labels.iter().map(String::as_str).collect();
let widths = menu_label_widths(&refs, theme::current().ssd_font_px as f64);
menu_label_rects(self.titlebar_rect(location), &widths)
}
/// Prostokąt pola konkretnej etykiety menu (do wyliczenia pozycji rozwinięcia).
pub fn ssd_menu_label_rect(
&self,
location: Point<i32, Logical>,
index: usize,
) -> Option<Rectangle<i32, Logical>> {
self.ssd_menu_label_rects(location).get(index).copied()
}
/// Indeks etykiety menu pod punktem (jeśli któraś trafiona).
pub fn ssd_menu_label_at(
&self,
location: Point<i32, Logical>,
point: Point<f64, Logical>,
) -> Option<usize> {
if !self.wants_ssd() || !self.ssd_bar_uses_labels() {
return None;
}
self.ssd_menu_label_rects(location)
.iter()
.position(|rect| rect.to_f64().contains(point))
}
/// Czy okno jest zmaksymalizowane (Wayland: stan xdg, X11: `_NET_WM_STATE`).
pub fn is_maximized(&self) -> bool {
if let Some(toplevel) = self.toplevel() {
let mut maximized = false;
toplevel.with_pending_state(|state| {
maximized = state.states.contains(xdg_toplevel::State::Maximized);
});
return maximized;
}
if let Some(x11) = self.x11_surface() {
return x11.is_maximized();
}
false
}
/// Prostokąt paska tytułu dla okna postawionego w `location` (układ logiczny).
pub fn titlebar_rect(&self, location: Point<i32, Logical>) -> Rectangle<i32, Logical> {
bar_rect(location, self.geometry().size.w)
}
/// Prostokąt panelu rozwiniętego menu okna (pod burgerem).
///
/// Wysokość zależy od zawartości: menu aplikacji ma tyle wierszy, ile etykiet
/// opublikował klient, a menu okna zawsze trzy (minimalizuj/maksymalizuj/zamknij).
pub fn ssd_menu_rect(&self, location: Point<i32, Logical>) -> Rectangle<i32, Logical> {
menu_panel_rect(self.titlebar_rect(location), self.menu_row_count())
}
/// Co pokazuje rozwinięcie spod burgera (patrz `SsdMenuKind`).
///
/// Skoro burger pojawia się tylko wtedy, gdy w pasku brakuje miejsca na
/// etykiety, rozwinięcie pokazuje menu APLIKACJI dokładnie wtedy, gdy klient
/// je opublikował — a menu okna tylko jako awaryjne dla obcych aplikacji.
pub fn ssd_menu_kind(&self) -> SsdMenuKind {
if crate::app_menu::labels(self).is_empty() {
SsdMenuKind::Window
} else {
SsdMenuKind::AppMenu
}
}
/// Liczba wierszy rozwiniętego menu (używana też do wyliczenia wysokości panelu).
fn menu_row_count(&self) -> i32 {
match self.ssd_menu_kind() {
SsdMenuKind::AppMenu => crate::app_menu::labels(self).len().max(1) as i32,
SsdMenuKind::Window => MENU_ROWS,
}
}
/// Czy punkt leży wewnątrz panelu menu (niezależnie od wiersza).
pub fn ssd_menu_contains(
&self,
location: Point<i32, Logical>,
point: Point<f64, Logical>,
) -> bool {
self.ssd_menu_open() && self.ssd_menu_rect(location).to_f64().contains(point)
}
/// Który wiersz menu OKNA trafiono (minimalizuj / maksymalizuj / zamknij).
pub fn ssd_menu_row_at(
&self,
location: Point<i32, Logical>,
point: Point<f64, Logical>,
) -> Option<TitlebarButton> {
if !self.ssd_menu_open() || self.ssd_menu_kind() != SsdMenuKind::Window {
return None;
}
menu_row_at(self.ssd_menu_rect(location), point)
}
/// Indeks etykiety MENU APLIKACJI trafionej w rozwinięciu (nie w pasku).
///
/// PO CO osobno od `ssd_menu_row_at`: wiersze menu aplikacji odpowiadają
/// etykietom klienta, więc klik przekazujemy klientowi, a nie wykonujemy
/// akcji okna.
pub fn ssd_menu_label_row_at(
&self,
location: Point<i32, Logical>,
point: Point<f64, Logical>,
) -> Option<usize> {
if !self.ssd_menu_open() || self.ssd_menu_kind() != SsdMenuKind::AppMenu {
return None;
}
let panel = self.ssd_menu_rect(location);
if !panel.to_f64().contains(point) {
return None;
}
let row =
((point.y as i32 - panel.loc.y - MENU_PANEL_PADDING) / MENU_ROW_HEIGHT).max(0) as usize;
let count = crate::app_menu::labels(self).len();
(row < count).then_some(row)
}
/// Czy punkt (w układzie przestrzeni) leży na pasku tytułu tego okna.
pub fn titlebar_contains(
&self,
location: Point<i32, Logical>,
point: Point<f64, Logical>,
) -> bool {
self.wants_ssd() && self.titlebar_rect(location).to_f64().contains(point)
}
/// Który przycisk paska trafiono (jeśli którykolwiek).
pub fn titlebar_button_at(
&self,
location: Point<i32, Logical>,
point: Point<f64, Logical>,
) -> Option<TitlebarButton> {
if !self.wants_ssd() {
return None;
}
let bar = self.titlebar_rect(location);
// W trybie etykiet menu NIE ma burgera — trafiają tylko przyciski okna.
// (Etykiety obsługuje osobno `ssd_menu_label_at`.)
if self.ssd_bar_uses_labels() {
window_button_at(bar, point)
} else {
bar_button_at(bar, point)
}
}
}
// --- Czysta geometria paska SSD (bez zależności od Waylanda) -------------------
//
// PO CO osobne funkcje: logika trafień to najłatwiejsze do zepsucia miejsce
// w SSD, a metody wyżej wymagają żywego `ToplevelSurface`. Wydzielenie geometrii
// pozwala przetestować ją jednostkowo, bez kompozytora i bez XTestu.
/// Prostokąt paska tytułu dla treści o pozycji `content_loc` i zadanej szerokości.
fn bar_rect(content_loc: Point<i32, Logical>, content_width: i32) -> Rectangle<i32, Logical> {
Rectangle::new(
Point::from((content_loc.x, content_loc.y - TITLEBAR_HEIGHT)),
Size::from((content_width, TITLEBAR_HEIGHT)),
)
}
/// Środek przycisku „Menu" (burger). Ta sama oś Y co przyciski okna —
/// to wymóg wizualny: menu i przyciski muszą leżeć w jednej linii.
fn menu_center(bar: Rectangle<i32, Logical>) -> Point<i32, Logical> {
Point::from((bar.loc.x + BUTTON_MARGIN, bar.loc.y + TITLEBAR_HEIGHT / 2))
}
/// Środki trzech przycisków paska (od prawej: zamknij, maks., min.).
fn window_buttons(bar: Rectangle<i32, Logical>) -> [(TitlebarButton, Point<i32, Logical>); 3] {
let center_y = bar.loc.y + TITLEBAR_HEIGHT / 2;
let right = bar.loc.x + bar.size.w;
[
(
TitlebarButton::Close,
Point::from((right - BUTTON_MARGIN, center_y)),
),
(
TitlebarButton::Maximize,
Point::from((right - BUTTON_MARGIN - BUTTON_SPACING, center_y)),
),
(
TitlebarButton::Minimize,
Point::from((right - BUTTON_MARGIN - BUTTON_SPACING * 2, center_y)),
),
]
}
/// Panel rozwijanego menu (tuż pod belką paska). `rows` = liczba wierszy.
fn menu_panel_rect(bar: Rectangle<i32, Logical>, rows: i32) -> Rectangle<i32, Logical> {
Rectangle::new(
Point::from((bar.loc.x + MENU_PANEL_PADDING, bar.loc.y + TITLEBAR_HEIGHT)),
Size::from((
MENU_PANEL_WIDTH,
MENU_PANEL_PADDING * 2 + MENU_ROW_HEIGHT * rows.max(1),
)),
)
}
/// Przycisk paska pod punktem (menu po lewej albo przycisk okna po prawej).
///
/// Promień trafienia jest o 3 px większy niż promień rysowania — mały „luz"
/// znacząco poprawia ergonomię klikania niewielkich glifów.
fn bar_button_at(
bar: Rectangle<i32, Logical>,
point: Point<f64, Logical>,
) -> Option<TitlebarButton> {
if within_radius(menu_center(bar), point) {
return Some(TitlebarButton::Menu);
}
window_button_at(bar, point)
}
/// Przycisk OKNA pod punktem (bez burgera) — używane w trybie etykiet menu.
fn window_button_at(
bar: Rectangle<i32, Logical>,
point: Point<f64, Logical>,
) -> Option<TitlebarButton> {
for (button, center) in window_buttons(bar) {
if within_radius(center, point) {
return Some(button);
}
}
None
}
/// Czy punkt trafia w przycisk o zadanym środku (promień z „luzem").
fn within_radius(center: Point<i32, Logical>, point: Point<f64, Logical>) -> bool {
let hit_radius_sq = ((BUTTON_RADIUS + 3) as f64).powi(2);
let dx = point.x - center.x as f64;
let dy = point.y - center.y as f64;
dx * dx + dy * dy <= hit_radius_sq
}
/// Wiersz menu okna pod punktem (prostokąty wierszy są rozłączne i pokrywają
/// cały panel, więc wystarczy podzielić wysokość panelu przez wysokość wiersza).
fn menu_row_at(
panel: Rectangle<i32, Logical>,
point: Point<f64, Logical>,
) -> Option<TitlebarButton> {
let panel = panel.to_f64();
if !panel.contains(point) {
return None;
}
let rel = point.y - panel.loc.y - MENU_PANEL_PADDING as f64;
match (rel / MENU_ROW_HEIGHT as f64).floor() as i32 {
0 => Some(TitlebarButton::Minimize),
1 => Some(TitlebarButton::Maximize),
2 => Some(TitlebarButton::Close),
_ => None,
}
}
impl IsAlive for PaganWindow {
fn alive(&self) -> bool {
self.0.alive()
}
}
impl WaylandFocus for PaganWindow {
fn wl_surface(&self) -> Option<std::borrow::Cow<'_, WlSurface>> {
self.0.wl_surface()
}
}
/// Delegujemy cały kontrakt SpaceElement do wewnętrznego okna.
impl SpaceElement for PaganWindow {
fn geometry(&self) -> Rectangle<i32, Logical> {
SpaceElement::geometry(&self.0)
}
fn bbox(&self) -> Rectangle<i32, Logical> {
let bbox = SpaceElement::bbox(&self.0);
if self.wants_ssd() {
// Rama = pasek + treść; bbox musi go obejmować (rejon uszkodzeń, wyjścia).
Rectangle::new(
Point::from((bbox.loc.x, bbox.loc.y - TITLEBAR_HEIGHT)),
Size::from((bbox.size.w, bbox.size.h + TITLEBAR_HEIGHT)),
)
} else {
bbox
}
}
fn is_in_input_region(&self, point: &Point<f64, Logical>) -> bool {
SpaceElement::is_in_input_region(&self.0, point)
}
fn z_index(&self) -> u8 {
SpaceElement::z_index(&self.0)
}
fn set_activate(&self, activated: bool) {
SpaceElement::set_activate(&self.0, activated)
}
fn output_enter(&self, output: &Output, overlap: Rectangle<i32, Logical>) {
SpaceElement::output_enter(&self.0, output, overlap)
}
fn output_leave(&self, output: &Output) {
SpaceElement::output_leave(&self.0, output)
}
fn refresh(&self) {
SpaceElement::refresh(&self.0)
}
}
// Elementy renderowania okna: pasek SSD (własny pixel shader), tekst i treść.
//
// Wariant `<=GlesRenderer>` mówi makru, że enum jest specjalizowany do
// konkretnego renderera — dzięki temu możemy trzymać w nim `PixelShaderElement`,
// który implementuje `RenderElement` wyłącznie dla `GlesRenderer`.
render_elements! {
pub WindowRenderElement<=GlesRenderer>;
Window=WaylandSurfaceRenderElement<GlesRenderer>,
// Tekst (tytuł, etykiety menu) rasteryzowany Cairo i wysłany jako tekstura.
Text=MemoryRenderBufferRenderElement<GlesRenderer>,
// Element rysowany pixel shaderem: beleczka paska i tło menu SSD.
// (Jedna wariant, bo makro generuje `From` per typ — dwa warianty tego samego
// typu kolidowałyby.)
Pixel=PixelShaderElement,
}
impl std::fmt::Debug for WindowRenderElement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Window(arg0) => f.debug_tuple("Window").field(arg0).finish(),
Self::Text(arg0) => f.debug_tuple("Text").field(arg0).finish(),
Self::Pixel(arg0) => f.debug_tuple("Pixel").field(arg0).finish(),
Self::_GenericCatcher(arg0) => f.debug_tuple("_GenericCatcher").field(arg0).finish(),
}
}
}
impl AsRenderElements<GlesRenderer> for PaganWindow {
type RenderElement = WindowRenderElement;
fn render_elements<C: From<Self::RenderElement>>(
&self,
renderer: &mut GlesRenderer,
location: Point<i32, Physical>,
scale: Scale<f64>,
alpha: f32,
) -> Vec<C> {
let mut elements = Vec::<WindowRenderElement>::new();
// Kolejność ma znaczenie: Smithay oczekuje elementów OD PRZODU DO TYŁU
// (indeks 0 = rysowany na wierzchu; `render_output` rysuje listę od końca).
// Dlatego menu okna i treść idą na początek listy, a belka paska na koniec
// — czyli POD tekstem i glifami.
let logical_loc = location.to_f64().to_logical(scale).to_i32_round::<i32>();
let bar = Rectangle::new(
Point::from((logical_loc.x, logical_loc.y - TITLEBAR_HEIGHT)),
Size::from((self.geometry().size.w, TITLEBAR_HEIGHT)),
);
// Firmowy wygląd PaganDE pochodzi z motywu (`~/.config/pagan/theme.conf`) —
// zmiana kolorów jest widoczna w następnej klatce bez restartu.
let theme = theme::current();
let focused = self.is_focused();
let radius = theme.ssd_radius.max(0.0);
// Tekst i ikony aktywnego okna są jaśniejsze; nieaktywnego — przygaszone.
// Kolor RGB podajemy wprost do rasteryzera (Cairo), stąd trójki `f64`.
let (text_color, close_color) = if focused {
(theme.ssd_text, theme.ssd_icon_close)
} else {
(theme.ssd_text_unfocused, theme.ssd_text_unfocused)
};
let text_rgb = (
text_color.r() as f64,
text_color.g() as f64,
text_color.b() as f64,
);
let close_rgb = (
close_color.r() as f64,
close_color.g() as f64,
close_color.b() as f64,
);
let font_px = theme.ssd_font_px as f64;
let icon_px = theme.ssd_icon_px;
// 0) Menu okna (dropdown z burgera) — musi być NAD treścią okna, dlatego
// trafia na sam przód listy. Rysujemy je tutaj (a nie w `render.rs`),
// bo jest nierozerwalnie związane z konkretnym oknem i jego Z-orderem.
if self.wants_ssd() && self.ssd_menu_open() {
let panel = self.ssd_menu_rect(logical_loc);
// Kolejność: NAJPIERW treść wierszy, POTEM tło panelu — lista jest
// front-to-back, więc tło (wypchnięte później) wyląduje POD napisami.
match self.ssd_menu_kind() {
SsdMenuKind::AppMenu => {
// Menu APLIKACJI: same etykiety górnego poziomu. Rozwinięcie
// rysuje KLIENT — my tylko przekazujemy klik na wiersz.
for (row, label) in crate::app_menu::labels(self).into_iter().enumerate() {
let center_y = panel.loc.y
+ MENU_PANEL_PADDING
+ MENU_ROW_HEIGHT * row as i32
+ MENU_ROW_HEIGHT / 2;
let width = text::measure(&label, font_px)
.map(|size| size.w)
.unwrap_or(0);
push_text(
&mut elements,
renderer,
&label,
font_px,
text_rgb,
Point::from((panel.loc.x + MENU_LABEL_ROW_PAD_X + width / 2, center_y)),
scale,
);
}
}
SsdMenuKind::Window => {
for (row, button) in [
TitlebarButton::Minimize,
TitlebarButton::Maximize,
TitlebarButton::Close,
]
.into_iter()
.enumerate()
{
let center = Point::from((
panel.loc.x + MENU_ROW_ICON_X,
panel.loc.y
+ MENU_PANEL_PADDING
+ MENU_ROW_HEIGHT * row as i32
+ MENU_ROW_HEIGHT / 2,
));
push_icon(
&mut elements,
renderer,
button,
center,
icon_px,
if button == TitlebarButton::Close {
close_rgb
} else {
text_rgb
},
scale,
);
let label = menu_row_label(button);
let text_x = panel.loc.x + MENU_ROW_ICON_X + MENU_ROW_LABEL_OFFSET;
if let Some(texture) = text::texture(label, font_px, text_rgb) {
let loc = Point::<f64, Logical>::from((
text_x as f64,
(center.y - texture.size.h / 2) as f64,
))
.to_physical(scale);
if let Ok(element) = MemoryRenderBufferRenderElement::from_buffer(
renderer,
loc,
&texture.buffer,
None,
None,
None,
Kind::Unspecified,
) {
elements.push(WindowRenderElement::Text(element));
}
}
}
}
}
let panel_bg = Color32F::new(
theme.ssd_bar.r(),
theme.ssd_bar.g(),
theme.ssd_bar.b(),
0.99,
);
if let Some(element) = render::rounded_rect_element(renderer, panel, panel_bg, 10.0) {
elements.push(WindowRenderElement::Pixel(element));
}
}
// 1) Treść okna (klient narysował już swoje wnętrze).
elements.extend(AsRenderElements::<GlesRenderer>::render_elements::<
WindowRenderElement,
>(&self.0, renderer, location, scale, alpha));
// 1b) SSD: PŁASKI pasek tytułu z menu aplikacji w JEDNYM rzędzie
// z przyciskami okna (menu po lewej, przyciski po prawej).
if self.wants_ssd() {
// Lista elementów jest OD PRZODU DO TYŁU, więc tekst i glify muszą być
// wypchnięte PRZED belką — inaczej belka by je zasłoniła.
let menu_labels = crate::app_menu::labels(self);
let labels_mode =
!menu_labels.is_empty() && self.geometry().size.w >= theme.ssd_menu_min_width;
if labels_mode {
let refs: Vec<&str> = menu_labels.iter().map(String::as_str).collect();
let widths = menu_label_widths(&refs, font_px);
for (rect, label) in menu_label_rects(bar, &widths).into_iter().zip(refs) {
push_text(
&mut elements,
renderer,
label,
font_px,
text_rgb,
Point::from((rect.loc.x + rect.size.w / 2, rect.loc.y + rect.size.h / 2)),
scale,
);
}
} else {
push_icon(
&mut elements,
renderer,
TitlebarButton::Menu,
menu_center(bar),
icon_px,
text_rgb,
scale,
);
}
// Przyciski okna po prawej — zawsze, niezależnie od trybu menu.
for (button, center) in window_buttons(bar) {
push_icon(
&mut elements,
renderer,
button,
center,
icon_px,
if button == TitlebarButton::Close {
close_rgb
} else {
text_rgb
},
scale,
);
}
// Tytuł okna — tylko gdy zmieści się między menu a przyciskami.
if let Some(title) = self.window_title() {
let menu_right = if labels_mode {
let refs: Vec<&str> = menu_labels.iter().map(String::as_str).collect();
let widths = menu_label_widths(&refs, font_px);
menu_label_rects(bar, &widths)
.last()
.map(|rect| rect.loc.x + rect.size.w)
.unwrap_or(bar.loc.x + BAR_PADDING_X)
} else {
menu_center(bar).x + 18
};
// Najbardziej na lewo wysunięty przycisk okna to minimalizacja
// (ostatni element listy), stąd indeks 2.
let buttons_left = window_buttons(bar)[2].1.x - 18;
let title_width = text::measure(&title, font_px).map(|s| s.w).unwrap_or(0);
let available = buttons_left - menu_right;
if title_width > 0 && title_width + 24 <= available {
push_text(
&mut elements,
renderer,
&title,
font_px,
text_rgb,
Point::from((menu_right + available / 2, bar.loc.y + bar.size.h / 2)),
scale,
);
}
}
// Cienka, płaska linia oddzielająca pasek od treści okna.
let border = Rectangle::new(
Point::from((bar.loc.x, bar.loc.y + bar.size.h - BORDER_HEIGHT)),
Size::from((bar.size.w, BORDER_HEIGHT)),
);
if let Some(element) =
render::rounded_rect_element(renderer, border, theme.ssd_border, 0.0)
{
elements.push(WindowRenderElement::Pixel(element));
}
// Belka: płaski kolor (bez gradientu — zgodnie z firmowym stylem).
let bar_color = if focused {
theme.ssd_bar
} else {
theme.ssd_unfocused
};
if let Some(element) =
render::top_rounded_rect_element(renderer, bar, bar_color, radius)
{
elements.push(WindowRenderElement::Pixel(element));
}
}
elements.into_iter().map(C::from).collect()
}
}
/// Szerokości klikalnych pól etykiet menu (sam tekst + odstęp po bokach).
fn menu_label_widths(labels: &[&str], font_px: f64) -> Vec<i32> {
labels
.iter()
.map(|label| {
text::measure(label, font_px)
.map(|size| size.w)
.unwrap_or(0)
+ MENU_LABEL_PAD_X * 2
})
.collect()
}
/// Prostokąty pól etykiet menu, układane od lewej krawędzi paska.
///
/// Czysta funkcja (bez Cairo i bez Waylanda) — dzięki temu układ menu można
/// przetestować jednostkowo, a ten sam wynik służy do rysowania i do trafień myszy.
fn menu_label_rects(bar: Rectangle<i32, Logical>, widths: &[i32]) -> Vec<Rectangle<i32, Logical>> {
let height = TITLEBAR_HEIGHT - MENU_LABEL_PAD_Y * 2;
let y = bar.loc.y + MENU_LABEL_PAD_Y;
let mut x = bar.loc.x + BAR_PADDING_X;
widths
.iter()
.map(|width| {
let rect = Rectangle::new(Point::from((x, y)), Size::from((*width, height)));
x += width;
rect
})
.collect()
}
/// Podpis wiersza menu okna (rozwijanego z burgera).
fn menu_row_label(button: TitlebarButton) -> &'static str {
match button {
TitlebarButton::Minimize => "Minimalizuj",
TitlebarButton::Maximize => "Maksymalizuj",
TitlebarButton::Close => "Zamknij",
TitlebarButton::Menu => "Menu",
}
}
/// Rysuje napis wyśrodkowany w zadanym punkcie (tekstura Cayro → element GL).
///
/// PO CO osobno: ta sama operacja dotyczy tytułu okna i etykiet menu.
fn push_text(
elements: &mut Vec<WindowRenderElement>,
renderer: &mut GlesRenderer,
text_value: &str,
font_px: f64,
color: (f64, f64, f64),
center: Point<i32, Logical>,
scale: Scale<f64>,
) {
let Some(texture) = text::texture(text_value, font_px, color) else {
return;
};
let location = Point::<f64, Logical>::from((
(center.x - texture.size.w / 2) as f64,
(center.y - texture.size.h / 2) as f64,
))
.to_physical(scale);
match MemoryRenderBufferRenderElement::from_buffer(
renderer,
location,
&texture.buffer,
None,
None,
None,
Kind::Unspecified,
) {
Ok(element) => elements.push(WindowRenderElement::Text(element)),
Err(err) => tracing::warn!(?err, "nie udało się narysować tekstu paska"),
}
}
/// Dodaje do listy element ikony danego przycisku (spójne w pasku i w menu).
///
/// PO CO osobno: pasek tytułu i wiersze menu rysują dokładnie te same ikony.
/// Jedna funkcja = brak rozjazdu wizualnego między nimi.
#[allow(clippy::too_many_arguments)]
fn push_icon(
elements: &mut Vec<WindowRenderElement>,
renderer: &mut GlesRenderer,
button: TitlebarButton,
center: Point<i32, Logical>,
size_px: i32,
color: (f64, f64, f64),
scale: Scale<f64>,
) {
let Some(texture) = icons::texture(icon_for(button), size_px, color) else {
return;
};
let location = Point::<f64, Logical>::from((
(center.x - texture.size.w / 2) as f64,
(center.y - texture.size.h / 2) as f64,
))
.to_physical(scale);
match MemoryRenderBufferRenderElement::from_buffer(
renderer,
location,
&texture.buffer,
None,
None,
None,
Kind::Unspecified,
) {
Ok(element) => elements.push(WindowRenderElement::Text(element)),
Err(err) => tracing::warn!(?err, "nie udało się narysować ikony paska"),
}
}
/// Ikona odpowiadająca przyciskowi paska.
fn icon_for(button: TitlebarButton) -> icons::Icon {
match button {
TitlebarButton::Menu => icons::Icon::Menu,
TitlebarButton::Minimize => icons::Icon::Minimize,
TitlebarButton::Maximize => icons::Icon::Maximize,
TitlebarButton::Close => icons::Icon::Close,
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Pasek leży DOKŁADNIE nad treścią (treść zaczyna się pod paskiem).
#[test]
fn pasek_jest_nad_trescia() {
let bar = bar_rect(Point::from((20, 40)), 800);
assert_eq!(bar.loc, Point::from((20, 0)));
assert_eq!(bar.size, Size::from((800, TITLEBAR_HEIGHT)));
}
/// Wymóg wizualny: menu (burger) i przyciski okna leżą na TEJ SAMEJ osi Y.
#[test]
fn menu_i_przyciski_na_tej_samej_osi_y() {
let bar = bar_rect(Point::from((20, 40)), 800);
let menu_y = menu_center(bar).y;
for (_, center) in window_buttons(bar) {
assert_eq!(center.y, menu_y, "przycisk rozjechał się z menu");
}
}
/// Klik w środek burgera trafia w „Menu", a nie w pusty pasek.
#[test]
fn trafienie_w_burger() {
let bar = bar_rect(Point::from((20, 40)), 800);
let center = menu_center(bar);
let point = Point::from((center.x as f64, center.y as f64));
assert_eq!(bar_button_at(bar, point), Some(TitlebarButton::Menu));
}
/// Każdy z trzech przycisków okna trafia we właściwą akcję.
#[test]
fn trafienie_w_przyciski_okna() {
let bar = bar_rect(Point::from((0, 40)), 600);
for (button, center) in window_buttons(bar) {
let point = Point::from((center.x as f64, center.y as f64));
assert_eq!(bar_button_at(bar, point), Some(button));
}
}
/// Środek pustej części paska nie należy do żadnego przycisku.
#[test]
fn pusty_pasek_nie_trafia() {
let bar = bar_rect(Point::from((0, 40)), 600);
let point = Point::from((300.0, 20.0));
assert_eq!(bar_button_at(bar, point), None);
}
/// Wiersze menu są w ustalonej kolejności i pokrywają cały panel.
#[test]
fn wiersze_menu_maja_stala_kolejnosc() {
let bar = bar_rect(Point::from((0, 40)), 600);
let panel = menu_panel_rect(bar, MENU_ROWS);
assert_eq!(
panel.loc,
Point::from((MENU_PANEL_PADDING, TITLEBAR_HEIGHT))
);
let row_center = |index: i32| {
Point::from((
(panel.loc.x + MENU_ROW_ICON_X) as f64,
(panel.loc.y + MENU_PANEL_PADDING + MENU_ROW_HEIGHT * index + MENU_ROW_HEIGHT / 2)
as f64,
))
};
assert_eq!(
menu_row_at(panel, row_center(0)),
Some(TitlebarButton::Minimize)
);
assert_eq!(
menu_row_at(panel, row_center(1)),
Some(TitlebarButton::Maximize)
);
assert_eq!(
menu_row_at(panel, row_center(2)),
Some(TitlebarButton::Close)
);
}
/// Punkt poza panelem menu nie trafia w żaden wiersz.
#[test]
fn punkt_poza_panelem_nie_trafia() {
let bar = bar_rect(Point::from((0, 40)), 600);
let panel = menu_panel_rect(bar, MENU_ROWS);
// Po prawej stronie panelu (poza jego szerokością).
let point = Point::from(((panel.loc.x + panel.size.w + 5) as f64, 100.0));
assert_eq!(menu_row_at(panel, point), None);
}
/// Panel menu zaczyna się pod belką paska i jest szerszy niż wysoki.
#[test]
fn panel_menu_jest_pod_belka() {
let bar = bar_rect(Point::from((0, 40)), 800);
let panel = menu_panel_rect(bar, MENU_ROWS);
assert_eq!(panel.loc.y, bar.loc.y + TITLEBAR_HEIGHT);
assert_eq!(panel.size.w, MENU_PANEL_WIDTH);
assert_eq!(
panel.size.h,
MENU_PANEL_PADDING * 2 + MENU_ROW_HEIGHT * MENU_ROWS
);
}
/// Wysokość panelu menu ROŚNIE z liczbą etykiet menu aplikacji.
#[test]
fn panel_menu_aplikacji_ma_tyle_wierszy_ile_etykiet() {
let bar = bar_rect(Point::from((0, 40)), 800);
let cztery = menu_panel_rect(bar, 4);
let dwie = menu_panel_rect(bar, 2);
assert_eq!(cztery.size.h, MENU_PANEL_PADDING * 2 + MENU_ROW_HEIGHT * 4);
assert!(cztery.size.h > dwie.size.h);
}
/// Etykiety menu układają się od lewej, jedna za drugą, na jednej osi Y.
#[test]
fn etykiety_menu_na_tej_samej_osi_y() {
let bar = bar_rect(Point::from((20, 40)), 900);
let widths = [40, 50, 45, 48];
let rects = menu_label_rects(bar, &widths);
assert_eq!(rects.len(), widths.len());
let first_center_y = rects[0].loc.y + rects[0].size.h / 2;
for (index, rect) in rects.iter().enumerate() {
assert_eq!(rect.size.w, widths[index]);
assert_eq!(
rect.loc.y + rect.size.h / 2,
first_center_y,
"etykieta rozjechała się w pionie"
);
if index > 0 {
// Etykiety sąsiadują bez przerw i bez nakładania się.
let previous = rects[index - 1];
assert_eq!(rect.loc.x, previous.loc.x + previous.size.w);
}
}
}
/// Etykiety menu siedzą na tej samej osi Y co przyciski okna.
#[test]
fn etykiety_menu_w_linii_z_przyciskami() {
let bar = bar_rect(Point::from((0, 40)), 1000);
let labels = menu_label_rects(bar, &[40, 40, 40, 40]);
let buttons = window_buttons(bar);
let label_y = labels[0].loc.y + labels[0].size.h / 2;
for (_, center) in buttons {
assert_eq!(center.y, bar.loc.y + TITLEBAR_HEIGHT / 2);
}
// Środki etykiet i przycisków leżą w pionowym środku paska.
assert_eq!(label_y, bar.loc.y + TITLEBAR_HEIGHT / 2);
}
/// Okno X11 zawsze dostaje nasz pasek (X11 nie ma CSD).
#[test]
fn x11_okno_dostaje_ssd() {
assert!(ssd_decision(Some(false), false, false));
}
/// Menu/podpowiedź X11 (`override-redirect`) NIE dostaje paska.
#[test]
fn x11_override_redirect_bez_ssd() {
assert!(!ssd_decision(Some(true), false, false));
}
/// Okno Waylanda bez negocjacji dekoracji (np. GTK3) NIE dostaje paska —
/// inaczej nasz pasek nakładałby się na jego CSD.
#[test]
fn wayland_bez_negocjacji_bez_ssd() {
assert!(!ssd_decision(None, true, false));
}
/// Okno Waylanda, które oddało dekoracje kompozytorowi, dostaje nasz pasek.
#[test]
fn wayland_z_negocjacja_dostaje_ssd() {
assert!(ssd_decision(None, true, true));
}
/// Popup/subsurface nie jest samodzielnym oknem — bez paska.
#[test]
fn wayland_popup_bez_ssd() {
assert!(!ssd_decision(None, false, false));
}
}