🔒 Repository is read-only – file editing is disabled.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523
//! Pojedyncze okno Wayland (`xdg-shell`) z buforem `wl_shm`, wejściem i pętlą zdarzeń.
//!
//! Kontrakt: [`run`] otwiera okno i woła domknięcie rysujące. Domknięcie otrzymuje
//! [`Frame`], który daje dostęp do sceny, motywu, silnika tekstu oraz **zdarzeń wejścia**
//! od ostatniej klatki. Motywem steruje się z poziomu domknięcia przez
//! [`Frame::set_preference`] i [`Frame::set_accent`] (zmiana jest natychmiast odzwierciedlona
//! i zapisana).
//!
//! Dekoracje są po stronie klienta (CSD) — rysujemy własny HeaderBar.
use std::cell::RefCell;
use std::error::Error;
use std::time::{Duration, Instant};
use libluna_core::{
Accent, ButtonState, Color, Event, KeyEvent, Modifiers, Point, PointerButton, PointerEvent,
Rect, ScrollEvent, Size, Theme, ThemePreference, ThemeSettings, WindowEvent,
};
use libluna_render::{Scene, SoftwareRenderer, TextEngine};
use smithay_client_toolkit::reexports::calloop::{EventLoop, LoopHandle};
use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource;
use smithay_client_toolkit::reexports::csd_frame::WindowState;
use smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel;
use smithay_client_toolkit::{
compositor::{CompositorHandler, CompositorState},
delegate_compositor, delegate_keyboard, delegate_layer, delegate_output, delegate_pointer,
delegate_registry, delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window,
output::{OutputHandler, OutputState},
registry::{ProvidesRegistryState, RegistryState},
registry_handlers,
seat::{
keyboard::{KeyEvent as SctkKeyEvent, KeyboardHandler, Keysym, Modifiers as KbModifiers},
pointer::{PointerEvent as SctkPointerEvent, PointerEventKind, PointerHandler},
Capability, SeatHandler, SeatState,
},
shell::{
wlr_layer::{
Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
LayerSurfaceConfigure,
},
xdg::{
window::{Window, WindowConfigure, WindowDecorations, WindowHandler},
XdgShell,
},
WaylandSurface,
},
shm::{
slot::{Buffer, SlotPool},
Shm, ShmHandler,
},
};
use wayland_client::{
globals::registry_queue_init,
protocol::{wl_keyboard, wl_output, wl_pointer, wl_region, wl_seat, wl_shm, wl_surface},
Connection, Dispatch, QueueHandle,
};
/// Minimalny odstęp między klatkami wywołanymi **wyłącznie** ruchem kursora.
/// Bez tego każdy ruch myszy (setki zdarzeń/s) wysyłałby cały bufor 1920×680.
const MOTION_FRAME: Duration = Duration::from_millis(12);
/// Maksymalne tempo prezentacji powłoki (~60 Hz). Bez tego pętla potrafiła wysłać
/// kilkaset buforów na sekundę: każde wybudzenie przez `wl_buffer.release` kończyło się
/// kolejnym `commit`, a kompozytor dopiero co zwolnił bufor. Powłoka paliła CPU, a obraz
/// i tak nie był szybszy niż odświeżanie monitora.
const FRAME_INTERVAL: Duration = Duration::from_millis(16);
/// Domknięcie rysujące jedną klatkę (łącznie z obsługą zdarzeń).
type RenderFn = dyn for<'a> FnMut(&mut Frame<'a>);
/// Uchwyt do wybudzania pętli zdarzeń z innego wątku.
///
/// Używany np. przez powłokę po odebraniu powiadomienia D-Bus: [`WakeHandle::wake`]
/// sprawia, że okno zostanie przerysowane, mimo że nie było żadnego zdarzenia wejścia.
#[derive(Clone)]
pub struct WakeHandle {
ping: calloop::ping::Ping,
}
impl WakeHandle {
/// Budzi pętlę zdarzeń (bezpieczne z dowolnego wątku).
pub fn wake(&self) {
self.ping.ping();
}
}
/// Konfiguracja okna.
#[derive(Debug, Clone)]
pub struct AppConfig {
pub title: String,
pub app_id: String,
pub initial_size: (u32, u32),
pub settings: ThemeSettings,
/// Czy system zgłasza tryb ciemny (dla preferencji `System`).
pub system_dark: bool,
/// Gdy ustawione — zamiast okna `xdg-shell` tworzymy powierzchnię `wlr-layer-shell`
/// (panel/dock/tło). To tryb używany przez powłokę Luna.
pub layer: Option<LayerConfig>,
/// Przezroczyste tło powierzchni (dla paneli/OSD warstwowych).
pub transparent: bool,
}
/// Region wejścia powierzchni.
///
/// `Auto` pozwala kompozytorowi wyliczyć region z kanału alfa (panele) — kliknięcia
/// w przezroczyste części przechodzą do okien pod spodem. `Rects` ustawia jawny region;
/// powłoka używa go, by przy otwartym popupie przechwycić **wszystkie** kliknięcia
/// i zamknąć popup także po kliknięciu poza jego obrysem.
#[derive(Debug, Clone, PartialEq, Default)]
pub enum InputRegion {
#[default]
Auto,
Rects(Vec<Rect>),
}
/// Konfiguracja powierzchni warstwowej (`wlr-layer-shell`).
#[derive(Debug, Clone)]
pub struct LayerConfig {
/// Przestrzeń nazw (np. „luna-panel”).
pub namespace: String,
/// Warstwa rysowania.
pub layer: Layer,
/// Kotwice krawędzi (np. `TOP|LEFT|RIGHT` = pasek na całą szerokość).
pub anchor: Anchor,
/// Rozmiar (0 = wybierze kompozytor/klient).
pub size: (u32, u32),
/// Marginesy: góra, prawa, dół, lewa.
pub margin: (i32, i32, i32, i32),
/// Strefa wyłączna (zarezerwowane miejsce; 0 = brak).
pub exclusive_zone: i32,
/// Czy panel przyjmuje fokus klawiatury na żądanie.
pub keyboard: bool,
}
impl LayerConfig {
/// Pasek na górnej krawędzi, na całą szerokość, o zadanej grubości.
pub fn top_panel(namespace: impl Into<String>, thickness: u32, exclusive: bool) -> Self {
Self {
namespace: namespace.into(),
layer: Layer::Top,
anchor: Anchor::TOP | Anchor::LEFT | Anchor::RIGHT,
size: (0, thickness),
margin: (0, 0, 0, 0),
exclusive_zone: if exclusive { thickness as i32 } else { 0 },
keyboard: true,
}
}
/// Panel nakładkowy (OSD/notyfikacje) na środku, bez strefy wyłącznej.
pub fn overlay(namespace: impl Into<String>, width: u32, height: u32) -> Self {
Self {
namespace: namespace.into(),
layer: Layer::Overlay,
anchor: Anchor::TOP,
size: (width, height),
margin: (24, 0, 0, 0),
exclusive_zone: 0,
keyboard: false,
}
}
/// Powierzchnia „chrome”: pasek o grubości `bar` plus przezroczysty obszar pod nim
/// (na OSD i powiadomienia). Strefa wyłączna obejmuje tylko pasek.
/// Gdy `total == 0`, powierzchnia rozciąga się na cały monitor (`Anchor::BOTTOM`).
pub fn top_chrome(namespace: impl Into<String>, bar: u32, total: u32) -> Self {
let anchor = if total == 0 {
Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT
} else {
Anchor::TOP | Anchor::LEFT | Anchor::RIGHT
};
Self {
namespace: namespace.into(),
layer: Layer::Top,
anchor,
size: (0, total),
margin: (0, 0, 0, 0),
exclusive_zone: bar as i32,
keyboard: true,
}
}
}
impl AppConfig {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
app_id: "eu.paganlinux.Luna.Demo".to_owned(),
initial_size: (900, 560),
settings: ThemeSettings::default(),
system_dark: false,
layer: None,
transparent: false,
}
}
/// Tworzy powierzchnię warstwową zamiast zwykłego okna.
pub fn layer(mut self, layer: LayerConfig) -> Self {
self.layer = Some(layer);
self
}
/// Przezroczyste tło (panel/OSD warstwowy).
pub fn transparent(mut self, transparent: bool) -> Self {
self.transparent = transparent;
self
}
pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
self.app_id = app_id.into();
self
}
pub fn initial_size(mut self, width: u32, height: u32) -> Self {
self.initial_size = (width, height);
self
}
pub fn settings(mut self, settings: ThemeSettings) -> Self {
self.settings = settings;
self
}
pub fn system_dark(mut self, dark: bool) -> Self {
self.system_dark = dark;
self
}
}
/// Akcje okna zgłaszane przez aplikację (przez [`Frame::close`] itd.).
#[derive(Debug, Clone, Copy, Default)]
pub struct WindowActions {
pub close: bool,
pub minimize: bool,
pub toggle_maximize: bool,
/// Żądanie rozpoczęcia przeciągania okna (klient przesuwa je przez kompozytor).
pub move_requested: bool,
/// Żądanie zmiany rozmiaru; bitmaska: 1 = góra, 2 = dół, 4 = lewo, 8 = prawo.
pub resize_requested: Option<u8>,
}
/// Kontekst rysowania i obsługi zdarzeń przekazywany do domknięcia aplikacji.
///
/// Scena jest w pikselach **logicznych**; [`Frame::scale`] mówi, ile pikseli
/// fizycznych przypada na jednostkę. Zdarzenia z [`Frame::events`] dotyczą okresu
/// od poprzedniej klatki.
pub struct Frame<'a> {
/// Scena w pikselach **logicznych**. Platforma czyści ją przed każdym wywołaniem
/// domknięcia (aplikacja rysuje całą klatkę od zera — nie musi wołać `clear`).
pub scene: &'a mut Scene,
/// Rozwiązany motyw (jasny/ciemny + akcent) dla bieżącej klatki.
pub theme: Theme,
pub text: &'a TextEngine,
pub size: Size,
pub scale: f64,
/// Zdarzenia wejścia od poprzedniej klatki.
pub events: &'a [Event],
/// Czas od poprzedniej klatki (sekundy) — dla animacji (`advance(dt, …)`).
pub dt: f64,
settings: &'a mut ThemeSettings,
actions: &'a mut WindowActions,
frame_requested: &'a mut bool,
/// Termin kolejnej klatki (np. zegar), niezależny od animacji.
frame_deadline: &'a mut Option<Instant>,
motion_redraw: &'a mut bool,
input_region: &'a RefCell<InputRegion>,
system_dark: bool,
/// Uchwyt do wybudzania pętli z innego wątku (np. po powiadomieniu D-Bus).
waker: WakeHandle,
}
impl Frame<'_> {
/// Prosi o zamknięcie okna.
pub fn close(&mut self) {
self.actions.close = true;
}
/// Prosi o zminimalizowanie okna.
pub fn minimize(&mut self) {
self.actions.minimize = true;
}
/// Przełącza maksymalizację okna.
pub fn toggle_maximize(&mut self) {
self.actions.toggle_maximize = true;
}
/// Rozpoczyna przeciąganie okna. Wywołaj przy wciśnięciu lewego przycisku na
/// własnym pasku tytułu (CSD) — dalszy ruch wykonuje już kompozytor, więc okno
/// nie „ucieka” względem kursora i działa też poza jego powierzchnią.
///
/// Ma sens tylko dla zwykłych okien (`xdg_toplevel`), nie dla powierzchni
/// warstwowych.
pub fn begin_move(&mut self) {
self.actions.move_requested = true;
}
/// Rozpoczyna zmianę rozmiaru okna z podanej krawędzi/rogu.
///
/// `edges` to bitmaska: 1 = góra, 2 = dół, 4 = lewo, 8 = prawo (jak w xdg-shell).
pub fn begin_resize(&mut self, edges: u8) {
if edges & 0xf != 0 {
self.actions.resize_requested = Some(edges & 0xf);
}
}
/// Czas od poprzedniej klatki w sekundach (do kroku animacji).
pub fn delta_time(&self) -> f64 {
self.dt
}
/// Prosi o kolejną klatkę — używane, gdy trwa animacja (utrzymuje płynność).
/// Dopóki aplikacja nie zawoła tej metody, okno odświeża się tylko na zdarzenia.
pub fn request_frame(&mut self) {
*self.frame_requested = true;
}
/// Prosi o klatkę po `delay` — np. zegar w panelu albo okresowe sprawdzanie stanu.
/// Jeśli termin jest już ustawiony wcześniej, zostaje zachowany.
pub fn request_frame_after(&mut self, delay: Duration) {
let deadline = Instant::now() + delay;
match *self.frame_deadline {
Some(current) if current <= deadline => {}
_ => *self.frame_deadline = Some(deadline),
}
}
/// Włącza/wyłącza odświeżanie na **sam ruch wskaźnika** (np. podświetlenie pod
/// kursorem w launcherze). Domyślnie wyłączone dla powierzchni warstwowych, bo
/// ruch myszy potrafi wysyłać setki zdarzeń na sekundę i wywoływać lawinę
/// commitów buforów. Powłoka włącza to tylko, gdy otwarte jest menu/launcher.
pub fn set_motion_redraw(&mut self, on: bool) {
*self.motion_redraw = on;
}
/// Czy włączone jest odświeżanie na ruch wskaźnika.
pub fn motion_redraw(&self) -> bool {
*self.motion_redraw
}
/// Ustawia region wejścia powierzchni (patrz [`InputRegion`]). Zmiana jest
/// zastosowana przy najbliższym `commit` (czyli w tej samej klatce).
///
/// Metoda bierze `&self`, bo powłoka wywołuje ją, trzymając już wypożyczone
/// `frame.text`/`frame.events`.
pub fn set_input_region(&self, region: InputRegion) {
*self.input_region.borrow_mut() = region;
}
/// Bieżące ustawienia wyglądu (kopia).
pub fn settings(&self) -> ThemeSettings {
self.settings.clone()
}
/// Mierzy tekst w pikselach logicznych (szerokość i wysokość).
pub fn measure_text(&self, text: &str, size: f64, weight: u16) -> Size {
self.text.measure(text, size, weight)
}
/// Bieżąca preferencja trybu.
pub fn preference(&self) -> ThemePreference {
self.settings.preference()
}
/// Ustawia preferencję trybu (auto/jasny/ciemny); zmiana jest zapisywana.
pub fn set_preference(&mut self, preference: ThemePreference) {
self.settings.set_preference(preference);
let _ = self.settings.save();
}
/// Bieżący kolor akcentu.
pub fn accent(&self) -> Accent {
self.settings.accent()
}
/// Ustawia kolor akcentu; zmiana jest zapisywana.
pub fn set_accent(&mut self, accent: Accent) {
self.settings.set_accent(accent);
let _ = self.settings.save();
}
/// Nazwa wybranego stylu (instalowanego z pliku), jeśli ustawiona.
pub fn style(&self) -> Option<&str> {
self.settings.style()
}
/// Ustawia styl (instalowany z pliku); zapisuje i przerysowuje.
pub fn set_style(&mut self, style: Option<String>) {
self.settings.set_style(style);
let _ = self.settings.save();
}
/// Czy system zgłasza tryb ciemny.
pub const fn system_dark(&self) -> bool {
self.system_dark
}
/// Uchwyt do wybudzania pętli zdarzeń z innego wątku.
pub fn waker(&self) -> WakeHandle {
self.waker.clone()
}
}
/// Otwiera okno i uruchamia pętlę zdarzeń, wołając `render` przy każdej potrzebie
/// odświeżenia (start, zmiana rozmiaru/skali, zdarzenia wejścia, zmiana motywu).
///
/// Funkcja wraca, gdy okno zostanie zamknięte (przez użytkownika lub `Escape`).
/// Ustawia `WAYLAND_DISPLAY`, gdy go brak (terminal uruchomiony w sesji X11).
///
/// Kolejność: zmienna → marker Selene (`luna-wayland`) → `wayland-0`/`wayland-1`.
/// Dzięki temu `luna-files`, `luna-demo` i inne aplikacje libLuny działają także
/// z terminala odziedziczonego po X11, gdzie `WAYLAND_DISPLAY` nie jest ustawione.
fn ensure_wayland_display() -> Result<(), Box<dyn Error>> {
if std::env::var_os("WAYLAND_DISPLAY").is_some_and(|v| !v.is_empty()) {
return Ok(());
}
let rt = std::env::var_os("XDG_RUNTIME_DIR")
.map(std::path::PathBuf::from)
.ok_or("brak XDG_RUNTIME_DIR i WAYLAND_DISPLAY — uruchom aplikację w sesji Luny")?;
let mut names: Vec<String> = Vec::new();
if let Ok(name) = std::fs::read_to_string(rt.join("luna-wayland")) {
let name = name.trim().to_owned();
if !name.is_empty() {
names.push(name);
}
}
names.push("wayland-0".to_owned());
names.push("wayland-1".to_owned());
for name in names {
let path = if name.starts_with('/') {
std::path::PathBuf::from(&name)
} else {
rt.join(&name)
};
if path.exists() {
std::env::set_var("WAYLAND_DISPLAY", &name);
return Ok(());
}
}
Err(format!(
"Nie znaleziono kompozytora Wayland (brak WAYLAND_DISPLAY i gniazda w {}). Uruchom aplikację w sesji Luny.",
rt.display()
)
.into())
}
pub fn run<F>(config: AppConfig, render: F) -> Result<(), Box<dyn Error>>
where
F: for<'a> FnMut(&mut Frame<'a>) + 'static,
{
// Klient potrzebuje `XDG_RUNTIME_DIR`, żeby znaleźć gniazdo kompozytora. Menedżer
// sesji zwykle go ustawia; gdy brak (np. zepsuty pam_systemd), wybieramy wspólny
// katalog — ten sam co kompozytor — żeby uniknąć `NoCompositor`.
if let Ok(Some(dir)) = libluna_core::runtime::ensure_runtime_dir() {
eprintln!("libLuna: XDG_RUNTIME_DIR not set, using {}", dir.display());
}
// `wayland-client` **nie** zgaduje nazwy gniazda: bez `WAYLAND_DISPLAY` kończy
// się `NoCompositor`. Terminale uruchamiane przez powłokę dostają czyste
// środowisko X11, więc odnajdujemy gniazdo Selene po markerze / `wayland-0`.
ensure_wayland_display()?;
let conn = Connection::connect_to_env()?;
let (globals, event_queue) = registry_queue_init(&conn)?;
let qh = event_queue.handle();
let mut event_loop: EventLoop<App> = EventLoop::try_new()?;
let loop_handle = event_loop.handle();
WaylandSource::new(conn.clone(), event_queue).insert(loop_handle)?;
// Wybudzanie pętli z innego wątku (np. powiadomienia z magistrali D‑Bus).
let (ping, ping_source) = calloop::ping::make_ping()?;
event_loop
.handle()
.insert_source(ping_source, |_, _, app: &mut App| app.needs_redraw = true)?;
let wake = WakeHandle { ping };
let compositor = CompositorState::bind(&globals, &qh)
.map_err(|e| format!("wl_compositor unavailable: {e}"))?;
let shm = Shm::bind(&globals, &qh).map_err(|e| format!("wl_shm unavailable: {e}"))?;
// Stan wyjść tworzymy **przed** powierzchnią warstwową — pozwala to wybrać monitor,
// do którego kotwiczymy panel (`LUNA_OUTPUT=<indeks>`). Dzięki temu każdy monitor ma
// własną instancję powłoki, a nie kopię tego samego bufora.
let output_state = OutputState::new(&globals, &qh);
let chosen_output: Option<wl_output::WlOutput> = std::env::var("LUNA_OUTPUT")
.ok()
.and_then(|s| s.trim().parse::<usize>().ok())
.and_then(|idx| output_state.outputs().nth(idx));
let surface = compositor.create_surface(&qh);
// Dwa tryby: zwykłe okno `xdg-shell` (CSD) albo powierzchnia warstwowa (panel/dock).
let target = match &config.layer {
Some(layer_cfg) => {
let layer_shell = LayerShell::bind(&globals, &qh)
.map_err(|e| format!("wlr-layer-shell unavailable: {e}"))?;
let ls = layer_shell.create_layer_surface(
&qh,
surface,
layer_cfg.layer,
Some(layer_cfg.namespace.clone()),
chosen_output.as_ref(),
);
ls.set_anchor(layer_cfg.anchor);
ls.set_size(layer_cfg.size.0, layer_cfg.size.1);
ls.set_margin(
layer_cfg.margin.0,
layer_cfg.margin.1,
layer_cfg.margin.2,
layer_cfg.margin.3,
);
ls.set_exclusive_zone(layer_cfg.exclusive_zone);
ls.set_keyboard_interactivity(if layer_cfg.keyboard {
KeyboardInteractivity::OnDemand
} else {
KeyboardInteractivity::None
});
// Pierwszy commit bez bufora — kompozytor odpowie konfiguracją (docelowy rozmiar).
ls.commit();
Target::Layer(ls)
}
None => {
let xdg_shell =
XdgShell::bind(&globals, &qh).map_err(|e| format!("xdg-shell unavailable: {e}"))?;
// CSD: sami rysujemy HeaderBar (brak dekoracji serwera).
let window = xdg_shell.create_window(surface, WindowDecorations::ClientOnly, &qh);
window.set_title(&config.title);
window.set_app_id(&config.app_id);
window.set_min_size(Some((380, 260)));
// Pierwszy commit bez bufora — kompozytor odpowie konfiguracją.
window.commit();
Target::Window(window)
}
};
let (iw, ih) = match &config.layer {
Some(l) => (l.size.0.max(1), l.size.1.max(1)),
None => config.initial_size,
};
let pool = SlotPool::new((iw as usize * ih as usize * 4).max(1024), &shm)?;
let mut app = App {
registry_state: RegistryState::new(&globals),
seat_state: SeatState::new(&globals, &qh),
output_state,
shm,
loop_handle: event_loop.handle(),
exit: false,
pool,
width: iw,
height: ih,
scale: 1.0,
system_dark: config.system_dark,
system_dark_config: config.system_dark,
theme_stamp: None,
transparent: config.transparent,
buffer: None,
buffer_format: None,
target,
configured: config.layer.is_none(),
compositor: compositor.clone(),
qh: qh.clone(),
keyboard: None,
pointer: None,
seat: None,
last_serial: 0,
modifiers: Modifiers::NONE,
renderer: SoftwareRenderer::new(iw, ih),
settings: config.settings,
scene: Scene::new(),
render: Box::new(render),
pending_events: Vec::new(),
needs_redraw: true,
actions: WindowActions::default(),
frame_requested: false,
frame_deadline: None,
motion_redraw: false,
input_region: RefCell::new(InputRegion::Auto),
// Domyślnie regionu **nie dotykamy** (okna mają pełny obszar, panele liczą go
// z alfy). Dopiero jawna zmiana tworzy `wl_region`.
applied_input_region: Some(InputRegion::Auto),
stats: std::env::var("LUNA_STATS").is_ok(),
stat_frames: 0,
stat_ms_draw: 0.0,
stat_ms_present: 0.0,
stat_report: Instant::now(),
last_tick: Instant::now(),
last_present: Instant::now() - Duration::from_millis(100),
maximized: false,
wake,
};
while !app.exit {
// Gdy trwa animacja (aplikacja prosi o klatki), odświeżamy płynnie (~60 Hz).
let mut timeout = if app.needs_redraw {
Duration::from_millis(16)
} else {
Duration::from_millis(100)
};
// Ruch kursora nie może wysyłać bufora częściej niż ~80 Hz — inaczej setki
// zdarzeń na sekundę zamieniają powłokę w młynkiem commitów (1–5 FPS).
if app.needs_redraw && app.motion_only_pending() {
let since = app.last_present.elapsed();
if since < MOTION_FRAME {
timeout = timeout.min(MOTION_FRAME - since);
}
}
// Ogólny limit ~60 Hz: dotyczy też animacji i odpowiedzi na zdarzenia.
if app.needs_redraw {
let since = app.last_present.elapsed();
if since < FRAME_INTERVAL {
timeout = timeout.min(FRAME_INTERVAL - since);
}
}
// Zaplanowana klatka (np. zegar co kilka sekund) — budzimy pętlę na czas.
if let Some(deadline) = app.frame_deadline {
let now = Instant::now();
if now >= deadline {
app.frame_deadline = None;
app.needs_redraw = true;
} else {
timeout = timeout.min(deadline - now);
}
}
event_loop.dispatch(timeout, &mut app)?;
// Motyw/system „auto” mogą zmienić się w innym procesie — sprawdzaj przy każdym
// przebudzeniu pętli (stat pliku jest tani), żeby zmiana działała też bez zdarzeń.
app.refresh_appearance();
if app.needs_redraw && app.configured {
if app.motion_only_pending() && app.last_present.elapsed() < MOTION_FRAME {
// Za wcześnie — kolejna iteracja pętli dokończy klatkę.
continue;
}
if app.last_present.elapsed() < FRAME_INTERVAL {
// Kompozytor nie pokaże szybciej niż monitor i tak; nie zalewajmy go.
continue;
}
app.redraw();
}
}
Ok(())
}
struct App {
registry_state: RegistryState,
seat_state: SeatState,
output_state: OutputState,
shm: Shm,
#[allow(dead_code)]
loop_handle: LoopHandle<'static, App>,
exit: bool,
pool: SlotPool,
width: u32,
height: u32,
scale: f64,
system_dark: bool,
/// Wartość z `AppConfig` (jawny override). Reszta pochodzi z systemu (`auto`).
system_dark_config: bool,
/// Znacznik czasu `theme.conf` — wykrywa zmiany motywu w innych procesach.
theme_stamp: Option<std::time::SystemTime>,
/// Czy tło powierzchni ma być przezroczyste (panele/OSD).
transparent: bool,
buffer: Option<Buffer>,
/// Format wybrany dla buforów shm (`Abgr/Xbgr` = bez swizzle; `Argb` = fallback).
buffer_format: Option<wl_shm::Format>,
target: Target,
/// Powierzchnia warstwowa ma sens dopiero po pierwszej konfiguracji.
configured: bool,
compositor: CompositorState,
/// Kolejka zdarzeń — potrzebna do tworzenia regionów wejścia w trakcie działania.
qh: QueueHandle<App>,
keyboard: Option<wl_keyboard::WlKeyboard>,
pointer: Option<wl_pointer::WlPointer>,
/// Ostatni `wl_seat` — potrzebny do `xdg_toplevel.move`/`resize`.
seat: Option<wl_seat::WlSeat>,
/// Serial ostatniego zdarzenia wskaźnika (wymagany przez `move`/`resize`).
last_serial: u32,
modifiers: Modifiers,
renderer: SoftwareRenderer,
settings: ThemeSettings,
scene: Scene,
render: Box<RenderFn>,
pending_events: Vec<Event>,
needs_redraw: bool,
actions: WindowActions,
/// Aplikacja poprosiła o kolejną klatkę (animacja w toku).
frame_requested: bool,
/// Termin kolejnej klatki zaplanowanej przez aplikację (np. zegar).
frame_deadline: Option<Instant>,
/// Czy sam ruch kursora ma wywoływać przerysowanie (patrz [`Frame::set_motion_redraw`]).
motion_redraw: bool,
/// Żądany region wejścia powierzchni.
input_region: RefCell<InputRegion>,
/// Region faktycznie ustawiony (żeby nie tworzyć nowego `wl_region` co klatkę).
applied_input_region: Option<InputRegion>,
/// Diagnostyka (`LUNA_STATS=1`): koszt rysowania i wysyłki bufora.
stats: bool,
stat_frames: u64,
stat_ms_draw: f64,
stat_ms_present: f64,
stat_report: Instant,
/// Znacznik czasu poprzedniej klatki (do liczenia `dt`).
last_tick: Instant,
/// Kiedy ostatnio wysłaliśmy bufor (limit klatek na ruch kursora).
last_present: Instant,
maximized: bool,
/// Uchwyt wybudzania pętli (np. z wątku D‑Bus).
wake: WakeHandle,
}
/// Powierzchnia docelowa: zwykłe okno albo powierzchnia warstwowa (panel/dock).
enum Target {
Window(Window),
Layer(LayerSurface),
}
impl Target {
fn surface(&self) -> &wl_surface::WlSurface {
match self {
Target::Window(w) => w.wl_surface(),
Target::Layer(l) => l.wl_surface(),
}
}
fn commit(&self) {
match self {
Target::Window(w) => w.commit(),
Target::Layer(l) => l.commit(),
}
}
}
impl App {
fn queue(&mut self, event: Event) {
let is_motion = matches!(event, Event::PointerMotion { .. } | Event::PointerLeave);
let is_layer = matches!(&self.target, Target::Layer(_));
self.pending_events.push(event);
// Dla powierzchni warstwowych (np. powłoka luna-shell) sam ruch kursora nie
// wymusza przerysowania — chyba że aplikacja jawnie o to poprosi
// ([`Frame::set_motion_redraw`], np. gdy otwarty jest launcher i chcemy
// podświetlać pozycję pod kursorem). Zapobiega to lawinie commitów buforów
// 1920x1080 i spadkowi wydajności do 1-5 FPS.
if !(is_layer && is_motion) || self.motion_redraw {
self.needs_redraw = true;
}
}
/// Wykrywa zmiany motywu zapisane przez inny proces (np. Quick Panel w powłoce)
/// i aktualizuje stan „system ciemny” dla trybu Auto.
fn refresh_appearance(&mut self) {
let stamp = ThemeSettings::config_path()
.and_then(|p| std::fs::metadata(p).ok())
.and_then(|m| m.modified().ok());
if stamp.is_some() && stamp != self.theme_stamp {
self.theme_stamp = stamp;
self.settings = ThemeSettings::load();
self.needs_redraw = true;
if self.stats {
eprintln!("luna-platform: przeładowano motyw z pliku");
}
}
let effective = self.system_dark_config || libluna_core::appearance::system_dark();
if effective != self.system_dark {
self.system_dark = effective;
self.needs_redraw = true;
if self.stats {
eprintln!("luna-platform: system dark = {effective}");
}
}
}
fn redraw(&mut self) {
let now = Instant::now();
let dt = (now - self.last_tick).as_secs_f64().clamp(0.0, 0.1);
self.last_tick = now;
let events = std::mem::take(&mut self.pending_events);
self.needs_redraw = false;
self.actions = WindowActions::default();
self.frame_requested = false;
self.settings.clear_dirty();
let mut theme = self.settings.resolve(self.system_dark);
let t_draw = Instant::now();
self.render_frame(theme, &events, dt);
// Jeśli domknięcie zmieniło motyw w trakcie, przerysuj bez zdarzeń.
let mut guard = 0;
while self.settings.is_dirty() && guard < 4 {
self.settings.clear_dirty();
theme = self.settings.resolve(self.system_dark);
self.render_frame(theme, &[], dt);
guard += 1;
}
if self.settings.is_dirty() {
self.settings.clear_dirty();
self.needs_redraw = true;
}
let is_layer = matches!(&self.target, Target::Layer(_));
let motion_only = !events.is_empty()
&& events
.iter()
.all(|e| matches!(e, Event::PointerMotion { .. } | Event::PointerLeave));
// Region wejścia ustawiamy **przed** ewentualnym wczesnym powrotem — zmiana
// regionu (otwarcie/zamknięcie popupu) musi zostać wysłana do kompozytora.
let input_changed = self.apply_input_region();
if is_layer && !self.frame_requested && !self.motion_redraw && motion_only {
// Brak żądania nowej klatki i tylko ruchy myszy — nie wysyłaj bufora do
// kompozytora. Gdy powłoka prosi o odświeżanie na ruch (`motion_redraw`,
// otwarty launcher/popup), klatkę jednak wysyłamy — inaczej podświetlenie
// pod kursorem nie nadążało za myszą.
if input_changed {
self.target.commit();
}
return;
}
let t_present = Instant::now();
self.present();
let ms_present = t_present.elapsed().as_secs_f64() * 1000.0;
self.apply_actions();
// Diagnostyka (`LUNA_STATS=1`): ile kosztuje rysowanie sceny, a ile wysyłka
// bufora do kompozytora. To rozstrzyga, czy wąskim gardłem jest powłoka, czy
// samo `present` (konwersja + `commit`).
if self.stats {
self.stat_ms_draw += t_draw.elapsed().as_secs_f64() * 1000.0 - ms_present;
self.stat_ms_present += ms_present;
self.stat_frames += 1;
let secs = self.stat_report.elapsed().as_secs_f64();
if secs >= 2.0 {
let n = self.stat_frames.max(1) as f64;
eprintln!(
"luna-platform: {:.1} prezentacji/s | rysowanie {:.2} ms, wysyłka {:.2} ms",
self.stat_frames as f64 / secs,
self.stat_ms_draw / n,
self.stat_ms_present / n
);
self.stat_frames = 0;
self.stat_ms_draw = 0.0;
self.stat_ms_present = 0.0;
self.stat_report = Instant::now();
}
}
// Aplikacja poprosiła o kolejną klatkę (animacja) — odśwież ponownie.
if self.frame_requested {
self.needs_redraw = true;
}
}
/// Wykonuje akcje okna zgłoszone w trakcie klatki.
fn apply_actions(&mut self) {
if self.actions.close {
if std::env::var("LUNA_DEBUG_INPUT").is_ok() {
eprintln!("platform: actions.close → exit");
}
self.exit = true;
}
let (want_move, want_resize) = (self.actions.move_requested, self.actions.resize_requested);
if std::env::var("LUNA_DEBUG_INPUT").is_ok() && (want_move || want_resize.is_some()) {
eprintln!(
"platform: actions move={want_move} resize={want_resize:?} seat={} target_window={}",
self.seat.is_some(),
matches!(&self.target, Target::Window(_))
);
}
if let Target::Window(window) = &self.target {
if self.actions.minimize {
window.set_minimized();
}
if self.actions.toggle_maximize {
self.maximized = !self.maximized;
if self.maximized {
window.set_maximized();
} else {
window.unset_maximized();
}
}
if want_move || want_resize.is_some() {
if let Some(seat) = self.seat.clone() {
let serial = self.last_serial;
if std::env::var("LUNA_DEBUG_INPUT").is_ok() {
eprintln!(
"platform: move={want_move} resize={want_resize:?} serial={serial}"
);
}
if want_move {
window.move_(&seat, serial);
} else if let Some(edges) = want_resize {
window.resize(&seat, serial, resize_edge(edges));
}
}
}
}
}
fn render_frame(&mut self, theme: Theme, events: &[Event], dt: f64) {
let (width, height, scale, system_dark) =
(self.width, self.height, self.scale, self.system_dark);
let phys_w = (width as f64 * scale).round().max(1.0) as u32;
let phys_h = (height as f64 * scale).round().max(1.0) as u32;
self.renderer.resize(phys_w, phys_h);
// Scena jest wielokrotnego użytku, więc **musi** zostać wyczyszczona przed każdym
// wywołaniem domknięcia. Bez tego komendy rysowania sumowały się z klatki na
// klatkę i powłoka zwalniała z każdą sekundą (panel schodził do 1–5 FPS).
// Kanarek: gdyby ktoś usunął czyszczenie, build z `debug_assertions` to zgłosi.
debug_assert!(
self.scene.is_empty(),
"scena nie została wyczyszczona po poprzedniej klatce"
);
self.scene.clear();
{
let App {
render,
scene,
renderer,
settings,
actions,
frame_requested,
frame_deadline,
motion_redraw,
input_region,
wake,
..
} = self;
let mut frame = Frame {
scene,
theme,
text: renderer.text_engine(),
size: Size::new(width as f64, height as f64),
scale,
events,
dt,
settings,
actions,
frame_requested,
frame_deadline,
motion_redraw,
input_region: &*input_region,
system_dark,
waker: wake.clone(),
};
render(&mut frame);
}
// Panele/OSD warstwowe mają przezroczyste tło (widać pulpit pod spodem).
let bg = if self.transparent {
Color::rgba(0.0, 0.0, 0.0, 0.0)
} else {
theme.colors.window_bg
};
// Klatka zawierająca wyłącznie ruch kursora, pusta scena i brak żądania klatki:
// nie ma czego rysować ani wysyłać. Wypełnianie bufora (1920x680 ≈ 5 MB)
// dla każdego zdarzenia myszy zapychało procesor bez żadnego efektu.
let motion_only = events
.iter()
.all(|e| matches!(e, Event::PointerMotion { .. } | Event::PointerLeave));
let is_layer = matches!(&self.target, Target::Layer(_));
if self.transparent
&& is_layer
&& self.scene.is_empty()
&& motion_only
&& !self.frame_requested
{
return;
}
self.renderer.draw(&self.scene, scale, bg);
}
fn present(&mut self) {
let (width, height, scale) = (self.width, self.height, self.scale);
let phys_w = (width as f64 * scale).round().max(1.0) as u32;
let phys_h = (height as f64 * scale).round().max(1.0) as u32;
let stride = phys_w as i32 * 4;
// Preferujemy format natywny dla renderera (`RGBA` w pamięci): `Abgr8888`
// (z alfą, panele) albo `Xbgr8888` (okna nieprzezroczyste). Dzięki temu
// `present` to zwykły `memcpy`, a nie podmiana R↔B na 5 MB każdą klatkę.
// Gdy kompozytor ich nie ogłasza — zostaje `Argb8888` + swizzle.
let format = match self.buffer_format {
Some(f) => f,
None => {
let want = if self.transparent {
wl_shm::Format::Abgr8888
} else {
wl_shm::Format::Xbgr8888
};
let f = if self.shm.formats().contains(&want) {
want
} else {
wl_shm::Format::Argb8888
};
self.buffer_format = Some(f);
if self.stats {
eprintln!("luna-platform: format shm {:?}", f);
}
f
}
};
let buffer = self.buffer.get_or_insert_with(|| {
self.pool
.create_buffer(phys_w as i32, phys_h as i32, stride, format)
.expect("shm buffer allocation")
.0
});
let canvas = match self.pool.canvas(buffer) {
Some(canvas) => canvas,
None => {
// Kompozytor wciąż trzyma poprzedni bufor — potrzebny drugi (double buffering).
let (second, canvas) = self
.pool
.create_buffer(phys_w as i32, phys_h as i32, stride, format)
.expect("second shm buffer allocation");
*buffer = second;
canvas
}
};
let pixels = self.renderer.data();
let count = (phys_w as usize * phys_h as usize)
.min(canvas.len() / 4)
.min(pixels.len() / 4);
let direct = matches!(format, wl_shm::Format::Abgr8888 | wl_shm::Format::Xbgr8888);
if direct {
// Pamięć już w kolejności RGBA — kopiujemy całość jednym `memcpy`.
canvas[..count * 4].copy_from_slice(&pixels[..count * 4]);
} else {
#[cfg(target_endian = "little")]
{
// Po 4 bajty naraz (u32): podmiana R↔B to kilka operacji bitowych.
// Bajt-po-bajcie na 1920×680 zajmowało ~2.5 ms **na każdą klatkę**.
let src32 =
unsafe { std::slice::from_raw_parts(pixels.as_ptr() as *const u32, count) };
let dst32 = unsafe {
std::slice::from_raw_parts_mut(canvas.as_mut_ptr() as *mut u32, count)
};
for (d, &s) in dst32.iter_mut().zip(src32.iter()) {
*d = (s & 0xFF00_FF00) | ((s & 0x00FF_0000) >> 16) | ((s & 0x0000_00FF) << 16);
}
}
#[cfg(not(target_endian = "little"))]
{
for (dst, src) in canvas
.chunks_exact_mut(4)
.zip(pixels.chunks_exact(4))
.take(count)
{
dst[0] = src[2];
dst[1] = src[1];
dst[2] = src[0];
dst[3] = src[3];
}
}
}
let surface = self.target.surface();
surface.damage_buffer(0, 0, phys_w as i32, phys_h as i32);
buffer.attach_to(surface).expect("buffer attach");
self.target.commit();
self.last_present = Instant::now();
}
/// Ustawia region wejścia, gdy zmienił się od poprzedniej klatki.
///
/// `InputRegion::Auto` wysyła **pusty** region — kompozytor traktuje go jako
/// „wróć do regionu z kanału alfa”. `Rects` wysyła jawne prostokąty.
fn apply_input_region(&mut self) -> bool {
let desired = self.input_region.borrow().clone();
if self.applied_input_region.as_ref() == Some(&desired) {
return false;
}
let surface = self.target.surface().clone();
let region = self.compositor.wl_compositor().create_region(&self.qh, ());
if let InputRegion::Rects(rects) = &desired {
for r in rects {
region.add(
r.x().round() as i32,
r.y().round() as i32,
r.width().round().max(0.0) as i32,
r.height().round().max(0.0) as i32,
);
}
}
surface.set_input_region(Some(®ion));
region.destroy();
self.applied_input_region = Some(desired);
true
}
/// Czy oczekujące zdarzenia to wyłącznie ruch kursora (bez kliknięć/klawiszy).
fn motion_only_pending(&self) -> bool {
!self.pending_events.is_empty()
&& self
.pending_events
.iter()
.all(|e| matches!(e, Event::PointerMotion { .. } | Event::PointerLeave))
}
}
/// Zamienia kod przycisku evdev na typ libLuny.
fn map_button(code: u32) -> PointerButton {
match code {
0x110 => PointerButton::Left,
0x111 => PointerButton::Right,
0x112 => PointerButton::Middle,
// Myszy raportują „wstecz/dalej” na dwa sposoby: BTN_SIDE/BTN_EXTRA
// (0x113/0x114 — tak robi większość myszy i X11 jako 8/9) oraz
// BTN_BACK/BTN_FORWARD (0x116/0x115). Obsługujemy oba warianty.
0x113 | 0x116 => PointerButton::Back,
0x114 | 0x115 => PointerButton::Forward,
other => PointerButton::Other(other),
}
}
/// Bitmaska krawędzi (1=góra, 2=dół, 4=lewo, 8=prawo) → `xdg_toplevel::ResizeEdge`.
fn resize_edge(bits: u8) -> xdg_toplevel::ResizeEdge {
match bits & 0xf {
1 => xdg_toplevel::ResizeEdge::Top,
2 => xdg_toplevel::ResizeEdge::Bottom,
4 => xdg_toplevel::ResizeEdge::Left,
8 => xdg_toplevel::ResizeEdge::Right,
5 => xdg_toplevel::ResizeEdge::TopLeft,
9 => xdg_toplevel::ResizeEdge::TopRight,
6 => xdg_toplevel::ResizeEdge::BottomLeft,
10 => xdg_toplevel::ResizeEdge::BottomRight,
_ => xdg_toplevel::ResizeEdge::None,
}
}
impl CompositorHandler for App {
fn scale_factor_changed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
new_factor: i32,
) {
self.scale = new_factor as f64;
self.queue(Event::Window(WindowEvent::ScaleChanged(self.scale)));
}
fn transform_changed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_transform: wl_output::Transform,
) {
}
fn frame(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_time: u32,
) {
self.needs_redraw = true;
}
fn surface_enter(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_output: &wl_output::WlOutput,
) {
}
fn surface_leave(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_output: &wl_output::WlOutput,
) {
}
}
impl OutputHandler for App {
fn output_state(&mut self) -> &mut OutputState {
&mut self.output_state
}
fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
}
impl WindowHandler for App {
fn request_close(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &Window) {
self.exit = true;
}
fn configure(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_window: &Window,
configure: WindowConfigure,
_serial: u32,
) {
if std::env::var("LUNA_DEBUG_INPUT").is_ok() {
eprintln!(
"platform: configure size={:?} state={:?}",
configure.new_size, configure.state
);
}
self.buffer = None;
self.width = configure.new_size.0.map(|v| v.get()).unwrap_or(self.width);
self.height = configure.new_size.1.map(|v| v.get()).unwrap_or(self.height);
// Synchronizuj stan z kompozytorem — okno może zostać zmaksymalizowane
// także z panelu/przełącznika, nie tylko przyciskiem w HeaderBarze.
self.maximized = configure.state.contains(WindowState::MAXIMIZED)
|| configure.state.contains(WindowState::FULLSCREEN);
self.queue(Event::Window(WindowEvent::Resize(Size::new(
self.width as f64,
self.height as f64,
))));
}
}
impl LayerShellHandler for App {
fn closed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &LayerSurface) {
self.exit = true;
}
fn configure(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_layer: &LayerSurface,
configure: LayerSurfaceConfigure,
_serial: u32,
) {
self.buffer = None;
self.width = configure.new_size.0.max(1);
self.height = configure.new_size.1.max(1);
self.configured = true;
self.queue(Event::Window(WindowEvent::Resize(Size::new(
self.width as f64,
self.height as f64,
))));
}
}
impl SeatHandler for App {
fn seat_state(&mut self) -> &mut SeatState {
&mut self.seat_state
}
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
self.seat = Some(seat);
}
fn new_capability(
&mut self,
_conn: &Connection,
qh: &QueueHandle<Self>,
seat: wl_seat::WlSeat,
capability: Capability,
) {
// SCTK nie zawsze woła `new_seat` przy podłączaniu do istniejącej sesji —
// jedynym pewnym miejscem, w którym mamy uchwyt do globalnego `wl_seat`,
// jest `new_capability`. Bez tego `self.seat` zostawało `None` i żądania
// `xdg_toplevel.move`/`resize` (przeciąganie okna za pasek) były po cichu
// pomijane.
if self.seat.is_none() {
self.seat = Some(seat.clone());
}
if capability == Capability::Keyboard && self.keyboard.is_none() {
if let Ok(keyboard) = self.seat_state.get_keyboard(qh, &seat, None) {
self.keyboard = Some(keyboard);
}
}
if capability == Capability::Pointer && self.pointer.is_none() {
if let Ok(pointer) = self.seat_state.get_pointer(qh, &seat) {
self.pointer = Some(pointer);
}
}
}
fn remove_capability(
&mut self,
_conn: &Connection,
_: &QueueHandle<Self>,
_seat: wl_seat::WlSeat,
capability: Capability,
) {
if capability == Capability::Keyboard {
if let Some(kbd) = self.keyboard.take() {
kbd.release();
}
}
if capability == Capability::Pointer {
if let Some(ptr) = self.pointer.take() {
ptr.release();
}
}
}
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
}
impl KeyboardHandler for App {
fn enter(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_surface: &wl_surface::WlSurface,
_serial: u32,
_raw: &[u32],
_keysyms: &[Keysym],
) {
}
fn leave(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_surface: &wl_surface::WlSurface,
_serial: u32,
) {
}
fn press_key(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_serial: u32,
event: SctkKeyEvent,
) {
if event.keysym == Keysym::Escape {
self.exit = true;
}
self.queue(Event::Key(KeyEvent {
keysym: u32::from(event.keysym),
keycode: event.raw_code,
text: event.utf8.clone(),
state: ButtonState::Pressed,
modifiers: self.modifiers,
repeat: false,
}));
}
fn release_key(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_serial: u32,
event: SctkKeyEvent,
) {
self.queue(Event::Key(KeyEvent {
keysym: u32::from(event.keysym),
keycode: event.raw_code,
text: None,
state: ButtonState::Released,
modifiers: self.modifiers,
repeat: false,
}));
}
fn update_modifiers(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_serial: u32,
modifiers: KbModifiers,
_layout: u32,
) {
self.modifiers = Modifiers {
ctrl: modifiers.ctrl,
alt: modifiers.alt,
shift: modifiers.shift,
logo: modifiers.logo,
// SCTK nie raportuje osobno poziomu 3; stan i tak ustawia kompozytor.
level3: false,
};
}
}
impl PointerHandler for App {
fn pointer_frame(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_pointer::WlPointer,
events: &[SctkPointerEvent],
) {
for event in events {
if &event.surface != self.target.surface() {
continue;
}
let position = Point::new(event.position.0, event.position.1);
match event.kind {
PointerEventKind::Enter { .. } => {
self.queue(Event::PointerMotion {
position,
modifiers: self.modifiers,
});
}
PointerEventKind::Leave { .. } => {
self.queue(Event::PointerLeave);
}
PointerEventKind::Motion { .. } => {
self.queue(Event::PointerMotion {
position,
modifiers: self.modifiers,
});
}
PointerEventKind::Press { button, serial, .. } => {
// Serial jest wymagany przez `xdg_toplevel.move`/`resize`.
self.last_serial = serial;
self.queue(Event::PointerButton(PointerEvent {
position,
button: map_button(button),
state: ButtonState::Pressed,
modifiers: self.modifiers,
}));
}
PointerEventKind::Release { button, .. } => {
self.queue(Event::PointerButton(PointerEvent {
position,
button: map_button(button),
state: ButtonState::Released,
modifiers: self.modifiers,
}));
}
PointerEventKind::Axis {
horizontal,
vertical,
..
} => {
self.queue(Event::Scroll(ScrollEvent {
position,
dx: horizontal.absolute,
dy: vertical.absolute,
}));
}
}
}
}
}
impl ShmHandler for App {
fn shm_state(&mut self) -> &mut Shm {
&mut self.shm
}
}
delegate_compositor!(App);
delegate_output!(App);
delegate_shm!(App);
delegate_seat!(App);
delegate_keyboard!(App);
delegate_pointer!(App);
delegate_xdg_shell!(App);
delegate_xdg_window!(App);
delegate_layer!(App);
delegate_registry!(App);
impl ProvidesRegistryState for App {
fn registry(&mut self) -> &mut RegistryState {
&mut self.registry_state
}
registry_handlers![OutputState, SeatState];
}
// `wl_region` nie ma zdarzeń — potrzebny tylko do utworzenia regionu wejścia.
impl Dispatch<wl_region::WlRegion, ()> for App {
fn event(
_state: &mut Self,
_proxy: &wl_region::WlRegion,
_event: <wl_region::WlRegion as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resize_edges_map_to_xdg_edges() {
assert_eq!(resize_edge(1), xdg_toplevel::ResizeEdge::Top);
assert_eq!(resize_edge(2), xdg_toplevel::ResizeEdge::Bottom);
assert_eq!(resize_edge(4), xdg_toplevel::ResizeEdge::Left);
assert_eq!(resize_edge(8), xdg_toplevel::ResizeEdge::Right);
// Kombinacje to sumy bitów, nie „albo” — to był realny błąd w pierwszej wersji.
assert_eq!(resize_edge(1 | 4), xdg_toplevel::ResizeEdge::TopLeft);
assert_eq!(resize_edge(1 | 8), xdg_toplevel::ResizeEdge::TopRight);
assert_eq!(resize_edge(2 | 4), xdg_toplevel::ResizeEdge::BottomLeft);
assert_eq!(resize_edge(2 | 8), xdg_toplevel::ResizeEdge::BottomRight);
assert_eq!(resize_edge(0), xdg_toplevel::ResizeEdge::None);
}
#[test]
fn mouse_side_buttons_map_to_back_and_forward() {
// Większość myszy: BTN_SIDE/BTN_EXTRA (X11: 8/9).
assert_eq!(map_button(0x113), PointerButton::Back);
assert_eq!(map_button(0x114), PointerButton::Forward);
// Niektóre urządzenia raportują BTN_BACK/BTN_FORWARD.
assert_eq!(map_button(0x116), PointerButton::Back);
assert_eq!(map_button(0x115), PointerButton::Forward);
assert_eq!(map_button(0x112), PointerButton::Middle);
assert_eq!(map_button(0x999), PointerButton::Other(0x999));
}
}