🔒 Repository is read-only – file editing is disabled.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689
//! `wl_seat` — wejście: wskaźnik (`wl_pointer`), klawiatura (`wl_keyboard`) i trasowanie.
//!
//! Zdarzenia wejścia pochodzą z magistrali (`crate::input`) — przekazuje je wątek wyjścia,
//! który odbiera je od nadrzędnego kompozytora. Tutaj tłumaczymy je na zdarzenia protokołu
//! wysyłane do sfokusowanych powierzchni klientów oraz na operacje okien (przeciąganie,
//! przeskalowanie, zamknięcie) wykonywane przez wbudowaną dekorację (SSD).
use std::io::{Seek, Write};
use std::os::fd::{AsFd, OwnedFd};
use std::time::{Duration, Instant};
use libluna_core::{ButtonState, Event, Modifiers, Point, PointerButton, Rect};
use wayland_protocols::xdg::shell::server::{xdg_popup, xdg_surface, xdg_toplevel};
use wayland_server::protocol::{wl_keyboard, wl_pointer, wl_seat, wl_surface, wl_touch};
use wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum,
};
use crate::compositor::SurfaceData;
use crate::data;
use crate::shell::{XdgSurfaceData, XdgToplevelData};
use crate::state::{DragState, ResizeState, SavedGeom, State, TITLEBAR};
use crate::sync::LockExt;
/// Ile pikseli powierzchni odpowiada jednemu „ząbkowi” kółka myszy (`REL_WHEEL`).
///
/// `wl_pointer.axis` przenosi wartości w jednostkach powierzchni, a evdev daje ±1 na
/// obrót — dlatego mnożymy. Wartość dobrana tak, by jeden obrót przewijał ~1 wiersz
/// listy (~60 px) po uwzględnieniu mnożnika w powłoce.
const WHEEL_STEP_PX: f64 = 20.0;
/// Buduje tekst mapy klawiszy XKB dla danego układu (i opcjonalnego wariantu).
///
/// Nazwy są sanityzowane, aby nie można było wstrzyknąć treści spoza zestawu
/// znaków używanego przez XKB.
fn keymap_text_for(layout: &str, variant: Option<&str>) -> String {
let layout = sanitize_xkb(layout);
let layout = if layout.is_empty() {
"us".to_owned()
} else {
layout
};
let variant = variant.map(sanitize_xkb).filter(|v| !v.is_empty());
// `level3(ralt_switch)` sprawia, że prawy Alt jest AltGr (poziom 3) — bez tego
// na układzie polskim nie da się wpisać ą, ć, ę, ł, ń, ó, ś, ź, ż.
let symbols = match variant {
Some(v) => format!("pc+{layout}({v})+inet(evdev)+level3(ralt_switch)"),
None => format!("pc+{layout}+inet(evdev)+level3(ralt_switch)"),
};
format!(
"xkb_keymap {{\n xkb_keycodes {{ include \"evdev+aliases(qwerty)\" }};\n xkb_types {{ include \"complete\" }};\n xkb_compat {{ include \"complete\" }};\n xkb_symbols {{ include \"{symbols}\" }};\n xkb_geometry {{ include \"pc(pc105)\" }};\n}};\n"
)
}
/// Bieżący układ z otoczenia (`LUNA_KEYBOARD_LAYOUT` / `LUNA_KEYBOARD_VARIANT`).
///
/// Najpierw próbujemy **skompilować** mapę do postaci bez `include`. Klienty z własnym
/// `libxkbcommon` bez ścieżek wyszukiwania (np. Zed) inaczej nie potrafią jej sparsować
/// („Couldn't find file keycodes/evdev”, „There are no include paths to search”).
pub(crate) fn keymap_text() -> String {
let (layout, variant) = keymap_layout();
if let Some(km) = compiled_keymap(&layout, variant.as_deref()) {
return km;
}
keymap_text_for(&layout, variant.as_deref())
}
/// Kompiluje RMLVO do pełnej mapy XKB (bez dyrektyw `include`).
fn compiled_keymap(layout: &str, variant: Option<&str>) -> Option<String> {
use xkbcommon::xkb;
let layout = sanitize_xkb(layout);
let layout = if layout.is_empty() {
"us".to_owned()
} else {
layout
};
let variant = variant.map(sanitize_xkb).unwrap_or_default();
let ctx = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
let keymap = xkb::Keymap::new_from_names(
&ctx,
"",
"",
layout.as_str(),
variant.as_str(),
// W RMLVO opcje podaje się jako `nazwa:wartość`, a nie jako dyrektywę
// `include` — `level3(ralt_switch)` było ignorowane z błędem XKB-595
// (i AltGr nie wpisywał ą/ć/ę…).
Some("lv3:ralt_switch".to_owned()),
xkb::KEYMAP_COMPILE_NO_FLAGS,
)?;
Some(keymap.get_as_string(xkb::KEYMAP_FORMAT_TEXT_V1))
}
/// Ustala układ klawiatury: jawny override → standard Wayland → locale → `us`.
///
/// Wcześniej był tu na sztywno `us`, więc w launcherze (i w każdym innym polu
/// tekstowym) wpisywało się co innego, niż użytkownik ma na klawiaturze.
fn keymap_layout() -> (String, Option<String>) {
let layout = std::env::var("LUNA_KEYBOARD_LAYOUT")
.ok()
.filter(|s| !s.is_empty())
.or_else(|| {
std::env::var("XKB_DEFAULT_LAYOUT")
.ok()
.filter(|s| !s.is_empty())
})
.or_else(layout_from_locale)
.unwrap_or_else(|| "us".to_owned());
let variant = std::env::var("LUNA_KEYBOARD_VARIANT")
.ok()
.filter(|s| !s.is_empty())
.or_else(|| {
std::env::var("XKB_DEFAULT_VARIANT")
.ok()
.filter(|s| !s.is_empty())
});
(layout, variant)
}
/// Zgaduje układ XKB z locale (`pl_PL.UTF-8` → `pl`).
fn layout_from_locale() -> Option<String> {
let raw = ["LC_ALL", "LC_CTYPE", "LANG"]
.iter()
.find_map(|k| std::env::var(k).ok().filter(|s| !s.is_empty()))?;
locale_to_layout(&raw)
}
/// Zamienia locale (`pl_PL.UTF-8`, `en_GB`, `C`) na nazwę układu XKB.
fn locale_to_layout(raw: &str) -> Option<String> {
let lang = raw.split(['.', '@']).next().unwrap_or("");
let mut parts = lang.split('_');
let language = parts.next().unwrap_or("").to_ascii_lowercase();
let country = parts.next().unwrap_or("").to_ascii_uppercase();
if language.is_empty() || language == "c" || language == "posix" {
return None;
}
// Brytyjski angielski to osobny układ w XKB (`gb`), nie `en`.
if language == "en" && country == "GB" {
return Some("gb".to_owned());
}
Some(language)
}
/// Dopuszcza tylko znaki legalne w nazwie układu XKB.
fn sanitize_xkb(s: &str) -> String {
s.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '-')
.collect()
}
/// Maski krawędzi zmiany rozmiaru.
const EDGE_TOP: u8 = 1;
const EDGE_BOTTOM: u8 = 2;
const EDGE_LEFT: u8 = 4;
const EDGE_RIGHT: u8 = 8;
/// Minimalny rozmiar treści okna przy przeskalowaniu.
const MIN_W: f64 = 120.0;
const MIN_H: f64 = 80.0;
/// Ile pikseli okna musi zostać widoczne, żeby dało się je złapać z powrotem.
/// Tyle samo trzymamy w pionie: pasek tytułu nigdy nie chowa się pod panelem.
const MIN_VISIBLE: f64 = 96.0;
/// Odległość od krawędzi monitora, przy której upuszczenie okna przyciąga je do strefy.
const SNAP_EDGE: f64 = 12.0;
impl GlobalDispatch<wl_seat::WlSeat, ()> for State {
fn bind(
_state: &mut Self,
_handle: &DisplayHandle,
_client: &Client,
resource: New<wl_seat::WlSeat>,
_data: &(),
data_init: &mut DataInit<'_, Self>,
) {
let seat = data_init.init(resource, ());
let caps = wl_seat::Capability::Pointer | wl_seat::Capability::Keyboard;
let _ = seat.send_event(wl_seat::Event::Capabilities {
capabilities: WEnum::Value(caps),
});
}
}
impl Dispatch<wl_seat::WlSeat, ()> for State {
fn request(
state: &mut Self,
_client: &Client,
_resource: &wl_seat::WlSeat,
request: wl_seat::Request,
_data: &(),
_handle: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
match request {
wl_seat::Request::GetPointer { id } => {
state.pointers.retain(|p| p.is_alive());
state.pointers.push(data_init.init(id, ()));
}
wl_seat::Request::GetKeyboard { id } => {
let kb = data_init.init(id, ());
if let Some((fd, size)) = make_keymap() {
let _ = kb.send_event(wl_keyboard::Event::Keymap {
format: WEnum::Value(wl_keyboard::KeymapFormat::XkbV1),
fd: fd.as_fd(),
size,
});
}
// Powtarzanie klawiszy: 25/s po 600 ms. `repeat_info` istnieje dopiero
// od wersji 4 `wl_keyboard` — wysłanie go do klienta, który związał
// starszą wersję, kończy klienta twardym przerwaniem (brak funkcji
// obsługi zdarzenia).
if kb.version() >= 4 {
let _ = kb.send_event(wl_keyboard::Event::RepeatInfo {
rate: 25,
delay: 600,
});
}
state.keyboards.retain(|k| k.is_alive());
state.keyboards.push(kb);
}
wl_seat::Request::GetTouch { id } => {
data_init.init(id, ());
}
_ => {}
}
}
}
macro_rules! empty_dispatch {
($iface:ty) => {
impl Dispatch<$iface, ()> for State {
fn request(
_state: &mut Self,
_client: &Client,
_resource: &$iface,
_request: <$iface as wayland_server::Resource>::Request,
_data: &(),
_handle: &DisplayHandle,
_data_init: &mut DataInit<'_, Self>,
) {
}
}
};
}
empty_dispatch!(wl_pointer::WlPointer);
empty_dispatch!(wl_keyboard::WlKeyboard);
empty_dispatch!(wl_touch::WlTouch);
/// Buduje deskryptor z mapą klawiszy (memfd + tekst XKB, zakończony NUL).
fn make_keymap() -> Option<(OwnedFd, u32)> {
let fd = rustix::fs::memfd_create("selene-keymap", rustix::fs::MemfdFlags::CLOEXEC).ok()?;
let mut file = std::fs::File::from(fd);
let mut bytes = keymap_text().into_bytes();
bytes.push(0);
file.write_all(&bytes).ok()?;
file.rewind().ok()?;
Some((OwnedFd::from(file), bytes.len() as u32))
}
// --- Trasowanie wejścia ---
/// Część okna, w którą trafił wskaźnik.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Part {
Close,
Maximize,
Minimize,
Title,
Content,
/// Zmiana rozmiaru za krawędź/narożnik (bitmaska: 1=góra,2=dół,4=lewo,8=prawo).
Resize(u8),
}
/// Trasuje jedno zdarzenie wejścia do klientów/okien.
pub fn handle_input(state: &mut State, handle: &DisplayHandle, event: &Event) {
match event {
Event::PointerMotion { position, .. } => pointer_motion(state, handle, *position),
Event::PointerButton(pe) => pointer_button(state, handle, pe.position, pe.button, pe.state),
Event::PointerLeave => pointer_leave(state),
Event::Scroll(se) => pointer_axis(state, se.dx, se.dy),
Event::Key(ke) => keyboard_key(state, ke.keysym, ke.keycode, ke.state),
_ => {}
}
}
/// Trafienie w okno (najwyższe wg `z`) i w którą jego część.
fn hit(state: &State, pos: Point) -> Option<(usize, Part)> {
let windows = state.windows.lock_or_recover();
let cur_ws = *state.ws_current.lock_or_recover();
let mut best: Option<(usize, i32)> = None;
for (i, w) in windows.iter().enumerate() {
let inside = match input_region_hit(w, pos) {
Some(h) => h,
None => w.outer().contains(pos),
};
if w.mapped && !w.minimized && w.in_workspace(cur_ws) && inside {
match best {
Some((_, z)) if z >= w.z => {}
_ => best = Some((i, w.z)),
}
}
}
let (i, _) = best?;
let w = &windows[i];
let part = if w.close_rect().contains(pos) {
Part::Close
} else if w.maximize_rect().contains(pos) {
Part::Maximize
} else if w.minimize_rect().contains(pos) {
Part::Minimize
} else {
let edges = w.resize_edges(pos);
if edges != 0 {
Part::Resize(edges)
} else if w.titlebar().contains(pos) {
Part::Title
} else {
Part::Content
}
};
Some((i, part))
}
/// Najwyższe okno, którego **treść** zawiera punkt (do zdarzeń klienta).
fn content_window(state: &State, pos: Point) -> Option<usize> {
let windows = state.windows.lock_or_recover();
let cur_ws = *state.ws_current.lock_or_recover();
let mut best: Option<(usize, i32)> = None;
for (i, w) in windows.iter().enumerate() {
let inside = match input_region_hit(w, pos) {
Some(h) => h,
None => w.content().contains(pos),
};
if w.mapped && !w.minimized && w.in_workspace(cur_ws) && inside {
match best {
Some((_, z)) if z >= w.z => {}
_ => best = Some((i, w.z)),
}
}
}
best.map(|(i, _)| i)
}
/// Zamyka popupy, poza którymi kliknięto (wysyła `xdg_popup.popup_done`, zdejmuje z widoku).
fn dismiss_popups(state: &mut State, pos: Point) {
loop {
let top = {
let windows = state.windows.lock_or_recover();
let mut best: Option<(usize, i32)> = None;
for (i, w) in windows.iter().enumerate() {
if w.popup && w.mapped {
match best {
Some((_, z)) if z >= w.z => {}
_ => best = Some((i, w.z)),
}
}
}
best.map(|(i, _)| i)
};
let Some(slot) = top else { break };
let inside = state
.windows
.lock_or_recover()
.get(slot)
.map(|w| w.outer().contains(pos))
.unwrap_or(false);
if inside {
break;
}
if let Some(surface) = state.slots.get(slot).cloned() {
if let Some(sd) = surface.data::<SurfaceData>() {
if let Some(xs) = sd.xdg_surface.lock_or_recover().clone() {
if let Some(d) = xs.data::<XdgSurfaceData>() {
if let Some(popup) = d.popup.lock_or_recover().as_ref() {
let _ = popup.send_event(xdg_popup::Event::PopupDone);
}
}
}
}
}
if let Some(w) = state.windows.lock_or_recover().get_mut(slot) {
w.mapped = false;
}
}
}
/// Okno docelowe dla punktu (publiczne dla drag & drop).
pub(crate) fn window_at(state: &State, pos: (f64, f64)) -> Option<usize> {
content_window(state, Point::new(pos.0, pos.1))
}
/// Trafienie w region wejścia powierzchni warstwowej; `None` = brak regionu (cała).
fn input_region_hit(w: &crate::state::Window, pos: Point) -> Option<bool> {
if !w.layer {
return None;
}
w.input
.as_ref()
.map(|rects| rects.iter().any(|r| r.offset(w.x, w.y).contains(pos)))
}
/// Slot powierzchni warstwowej przechwytującej kliknięcia, gdy punkt leży **poza**
/// jej widoczną treścią. Używane, gdy powłoka ma otwarty popup/launcher: klik gdzieś
/// indziej (pulpit, drugi monitor, dolna część ekranu) ma zamknąć popup, a nie
/// aktywować okno pod spodem.
fn capturing_layer_outside(state: &State, pos: Point) -> Option<usize> {
let windows = state.windows.lock_or_recover();
windows
.iter()
.position(|w| w.layer && w.mapped && w.input_capture && !w.outer().contains(pos))
}
fn window_geometry(state: &State, slot: usize) -> Option<(f64, f64, f64, f64)> {
let windows = state.windows.lock_or_recover();
windows
.get(slot)
.map(|w| (w.x, w.y, w.width as f64, w.height as f64))
}
/// Bitmaska krawędzi z żądania `xdg_toplevel.resize` (bity Waylanda pokrywają się
/// z naszymi `EDGE_*`: 1=góra, 2=dół, 4=lewo, 8=prawo).
fn resize_edge_bits(edge: WEnum<xdg_toplevel::ResizeEdge>) -> u8 {
match edge {
WEnum::Unknown(v) => (v & 0xf) as u8,
WEnum::Value(e) => match e {
xdg_toplevel::ResizeEdge::Top => EDGE_TOP,
xdg_toplevel::ResizeEdge::Bottom => EDGE_BOTTOM,
xdg_toplevel::ResizeEdge::Left => EDGE_LEFT,
xdg_toplevel::ResizeEdge::Right => EDGE_RIGHT,
xdg_toplevel::ResizeEdge::TopLeft => EDGE_TOP | EDGE_LEFT,
xdg_toplevel::ResizeEdge::TopRight => EDGE_TOP | EDGE_RIGHT,
xdg_toplevel::ResizeEdge::BottomLeft => EDGE_BOTTOM | EDGE_LEFT,
xdg_toplevel::ResizeEdge::BottomRight => EDGE_BOTTOM | EDGE_RIGHT,
_ => 0,
},
}
}
/// Zaczyna przesuwanie okna na żądanie `xdg_toplevel.move` (okna CSD proszą kompozytor
/// o przesuwanie w trakcie przeciągania własnego paska tytułu).
pub fn interactive_move_from_toplevel(state: &mut State, data: &XdgToplevelData) {
// Żądanie `move` może dotrzeć już po zwolnieniu przycisku (szybki klik).
// Bez tej straży zostawał aktywny `drag`, który przy następnym ruchu myszy
// przesuwał okno mimo puszczonego przycisku.
if !state.left_down {
return;
}
let Some(slot) = toplevel_slot(data) else {
if std::env::var("SELENE_STATS").is_ok() {
eprintln!("selene/win: move bez slotu");
}
return;
};
if std::env::var("SELENE_STATS").is_ok() {
eprintln!("selene/win: move slot={slot} pos={:?}", state.pointer_pos);
}
// Pozycja wskaźnika aktualizowana przy każdym ruchu (`pointer_motion`) — działa też
// w trybie nested, gdzie nie ma pętli evdev.
let pos = Point::new(state.pointer_pos.0, state.pointer_pos.1);
if window_geometry(state, slot).is_some() {
raise_and_focus(state, slot);
begin_drag(state, slot, pos);
}
}
/// Zaczyna zmianę rozmiaru na żądanie `xdg_toplevel.resize`.
pub fn interactive_resize_from_toplevel(
state: &mut State,
data: &XdgToplevelData,
edges: WEnum<xdg_toplevel::ResizeEdge>,
) {
if !state.left_down {
return;
}
let Some(slot) = toplevel_slot(data) else {
return;
};
let pos = Point::new(state.pointer_pos.0, state.pointer_pos.1);
let bits = resize_edge_bits(edges);
if bits == 0 {
return;
}
if let Some((x, y, w, h)) = window_geometry(state, slot) {
state.resize = Some(ResizeState {
slot,
edges: bits,
start_x: x,
start_y: y,
start_w: w,
start_h: h,
start_px: pos.x,
start_py: pos.y,
});
}
}
/// Rozpoczyna przeciągnięcie okna z pozycji globalnej (X11 `_NET_WM_MOVERESIZE`).
pub fn interactive_move_at(state: &mut State, slot: usize, pos: Point) {
if window_geometry(state, slot).is_none() {
return;
}
raise_and_focus(state, slot);
begin_drag(state, slot, pos);
}
/// Rozpoczyna zmianę rozmiaru z pozycji globalnej (X11 `_NET_WM_MOVERESIZE`).
pub fn interactive_resize_at(state: &mut State, slot: usize, edges: u8, pos: Point) {
if edges == 0 {
return;
}
let Some((x, y, w, h)) = window_geometry(state, slot) else {
return;
};
raise_and_focus(state, slot);
state.resize = Some(ResizeState {
slot,
edges,
start_x: x,
start_y: y,
start_w: w,
start_h: h,
start_px: pos.x,
start_py: pos.y,
});
}
/// Slot okna dla danych `xdg_toplevel` (przez powiązaną `wl_surface`).
pub(crate) fn toplevel_slot(data: &XdgToplevelData) -> Option<usize> {
let sd = data.surface.data::<SurfaceData>()?;
*sd.slot.lock_or_recover()
}
fn content_local(state: &State, slot: usize, pos: Point) -> (f64, f64) {
// Używamy `content()` — dla paneli/popupów pasek tytułu ma wysokość 0, więc
// współrzędne nie są przesunięte (wcześniej odejmowano `TITLEBAR` zawsze).
// Bufor okien CSD jest większy od widocznego okna o cienie — klient liczy
// współrzędne od początku **bufora**, więc dodajemy `image_offset`.
let windows = state.windows.lock_or_recover();
match windows.get(slot) {
Some(w) => {
let c = w.content();
(
pos.x - c.x() + w.image_offset.0,
pos.y - c.y() + w.image_offset.1,
)
}
None => (pos.x, pos.y),
}
}
/// Klawiatura klienta, do którego należy `surface`.
///
/// **Nie wolno** wysyłać zdarzeń klawiatury z powierzchnią innego klienta — protokół
/// (i `wayland-backend`) reaguje na to paniką. Dlatego każde zdarzenie szuka zasobu
/// należącego do tego samego klienta, co okno docelowe.
fn keyboard_for(state: &State, surface: &wl_surface::WlSurface) -> Option<wl_keyboard::WlKeyboard> {
let client = surface.client()?;
state
.keyboards
.iter()
.find(|kb| kb.is_alive() && kb.client().as_ref() == Some(&client))
.cloned()
}
/// Wskaźnik klienta, do którego należy `surface` (patrz [`keyboard_for`]).
fn pointer_for(state: &State, surface: &wl_surface::WlSurface) -> Option<wl_pointer::WlPointer> {
let client = surface.client()?;
state
.pointers
.iter()
.find(|p| p.is_alive() && p.client().as_ref() == Some(&client))
.cloned()
}
fn pointer_motion(state: &mut State, handle: &DisplayHandle, pos: Point) {
state.pointer_pos = (pos.x, pos.y);
if state.overview_on() {
// W podglądzie ruch myszy tylko aktualizuje „ducha” przeciąganego okna.
state.move_overview_drag((pos.x, pos.y));
return;
}
if state.resize.is_some() {
do_resize(state, pos);
return;
}
if let Some(drag) = state.drag {
let width = state
.windows
.lock_or_recover()
.get(drag.slot)
.map(|w| w.width as f64)
.unwrap_or(MIN_W);
let area = output_rect_at(state, pos);
let (x, y) = clamp_drag_pos(
area,
inset_at(state, pos),
width,
pos.x - drag.off_x,
pos.y - drag.off_y,
);
{
let mut windows = state.windows.lock_or_recover();
if let Some(w) = windows.get_mut(drag.slot) {
w.x = x;
w.y = y;
}
}
// Okno X11 musi znać nową pozycję — inaczej jego menu i podpowiedzi
// pojawiają się względem starego miejsca.
sync_x11_geometry(state, drag.slot);
}
// Przeciąganie danych (drag & drop) aktualizuje cel.
if state.dnd.is_some() {
data::dnd_motion(state, handle, (pos.x, pos.y));
}
update_pointer_focus(state, pos);
if let Some((slot, surface)) = state.pointer_focus.clone() {
let (lx, ly) = content_local(state, slot, pos);
let time = state.time_ms();
if let Some(ptr) = pointer_for(state, &surface) {
let _ = ptr.send_event(wl_pointer::Event::Motion {
time,
surface_x: lx,
surface_y: ly,
});
pointer_frame(&ptr);
}
}
}
fn pointer_button(
state: &mut State,
handle: &DisplayHandle,
pos: Point,
button: PointerButton,
st: ButtonState,
) {
if button == PointerButton::Left {
state.left_down = st == ButtonState::Pressed;
}
// W trybie podglądu: przeciąganie kafelka, wybór przestrzeni albo zamknięcie.
if state.overview_on() {
if button == PointerButton::Left {
match st {
ButtonState::Pressed => {
if let Some(slot) = overview_tile_at(state, pos) {
state.set_overview_drag(Some((slot, (pos.x, pos.y))));
} else if let Some(i) = overview_chip_at(state, pos) {
state.set_overview(false);
switch_workspace(state, i);
} else {
state.set_overview(false);
}
}
ButtonState::Released => {
let drag = state.overview.lock_or_recover().drag;
if let Some((slot, _)) = drag {
if let Some(i) = overview_chip_at(state, pos) {
// Upuszczenie na przestrzeni — przenieś okno.
move_window_to_workspace(state, slot, i);
} else {
// Zwykły klik — aktywuj okno.
raise_and_focus(state, slot);
}
state.set_overview(false);
}
}
}
}
return;
}
// Otwarty popup powłoki przechwytuje kliknięcia: klik poza jej widoczną treścią
// (pulpit, drugi monitor, dolna część ekranu) ma ją zamknąć, a nie aktywować
// okno pod spodem. Wysyłamy powłoce `enter` + `button` we współrzędnych lokalnych.
if st == ButtonState::Pressed {
if let Some(slot) = capturing_layer_outside(state, pos) {
if let Some(surface) = state.slots.get(slot).cloned() {
pointer_leave(state);
state.pointer_focus = Some((slot, surface.clone()));
if let Some(ptr) = pointer_for(state, &surface) {
let (lx, ly) = content_local(state, slot, pos);
let enter_serial = state.next_serial();
let _ = ptr.send_event(wl_pointer::Event::Enter {
serial: enter_serial,
surface: surface.clone(),
surface_x: lx,
surface_y: ly,
});
let serial = state.next_serial();
let time = state.time_ms();
let _ = ptr.send_event(wl_pointer::Event::Button {
serial,
time,
button: button_code(button),
state: WEnum::Value(wl_pointer::ButtonState::Pressed),
});
pointer_frame(&ptr);
}
}
return;
}
}
if st == ButtonState::Released {
if let Some(drag) = state.drag.take() {
// Upuszczenie przy krawędzi/narożniku: połowa ekranu, ćwiartka albo
// maksymalizacja („Aero Snap” / kafelkowanie). Zwykły klik bez ruchu
// (np. na pasku tytułu przy krawędzi) nie może niczego przyciągać.
let moved = (pos.x - drag.start_px).abs() + (pos.y - drag.start_py).abs() > 6.0;
if moved {
apply_drag_snap(state, drag.slot, pos, drag.origin);
}
}
state.resize = None;
// Zakończ przeciąganie danych, jeśli trwało.
if state.dnd.is_some() {
data::dnd_drop(state, handle);
}
} else {
// Klik poza popupem zamyka popup (przed trafieniem w okno).
dismiss_popups(state, pos);
}
// Obsłuż dekorację/okno pod kursorem.
if let Some((slot, part)) = hit(state, pos) {
if st == ButtonState::Pressed && std::env::var("SELENE_STATS").is_ok() {
let (lx, ly) = content_local(state, slot, pos);
eprintln!("selene/input: klik slot={slot} part={part:?} lokalne=({lx:.0},{ly:.0})");
}
if st == ButtonState::Pressed {
raise_and_focus(state, slot);
// Alt+Drag lub Super+Drag: przesuwanie (LPM) i skalowanie (PPM) okna z dowolnego punktu
let is_normal_win = state
.windows
.lock_or_recover()
.get(slot)
.map(|w| !w.layer && !w.popup)
.unwrap_or(false);
if is_normal_win && (state.sent_mods.alt || state.sent_mods.logo) {
pointer_leave(state);
if button == PointerButton::Left {
begin_drag(state, slot, pos);
return;
} else if button == PointerButton::Right {
if let Some((x, y, w, h)) = window_geometry(state, slot) {
state.resize = Some(ResizeState {
slot,
edges: EDGE_RIGHT | EDGE_BOTTOM,
start_x: x,
start_y: y,
start_w: w,
start_h: h,
start_px: pos.x,
start_py: pos.y,
});
}
return;
}
}
match part {
Part::Close => {
pointer_leave(state);
debug_part(state, slot, "close");
send_close(state, slot);
return;
}
Part::Minimize => {
pointer_leave(state);
debug_part(state, slot, "minimize");
toggle_minimize(state, slot);
return;
}
Part::Maximize => {
pointer_leave(state);
debug_part(state, slot, "maximize");
toggle_maximize(state, slot);
return;
}
Part::Title => {
pointer_leave(state);
// Podwójny klik na pasku tytułu: maksymalizacja / powrót do domyślnego
// rozmiaru (standardowe zachowanie pulpitów).
if button == PointerButton::Left {
let now = Instant::now();
let double = state
.last_title_click
.map(|(s, t)| {
s == slot && now.duration_since(t) < Duration::from_millis(400)
})
.unwrap_or(false);
if double {
state.last_title_click = None;
toggle_maximize(state, slot);
return;
}
state.last_title_click = Some((slot, now));
}
begin_drag(state, slot, pos);
return;
}
Part::Resize(edges) => {
pointer_leave(state);
// Zmaxymalizowane/pełnoekranowe okno najpierw wraca do rozmiaru
// domyślnego — inaczej „ciągnięcie za krawędź” zaczynałoby od
// geometrii całego ekranu.
let special = state
.windows
.lock_or_recover()
.get(slot)
.map(|w| w.maximized || w.fullscreen)
.unwrap_or(false);
if special {
restore_window(state, slot);
}
if let Some((x, y, w, h)) = window_geometry(state, slot) {
state.resize = Some(ResizeState {
slot,
edges,
start_x: x,
start_y: y,
start_w: w,
start_h: h,
start_px: pos.x,
start_py: pos.y,
});
}
return;
}
Part::Content => update_pointer_focus(state, pos),
}
} else if part == Part::Content {
// Zwolnienie nad treścią → przekaż je klientowi poniżej.
update_pointer_focus(state, pos);
} else {
return;
}
} else {
update_pointer_focus(state, pos);
}
// Zdarzenie przycisku do klienta — tylko gdy kursor nad treścią okna.
if let Some((_, surface)) = state.pointer_focus.clone() {
if let Some(ptr) = pointer_for(state, &surface) {
let serial = state.next_serial();
let time = state.time_ms();
let wl_state = match st {
ButtonState::Pressed => WEnum::Value(wl_pointer::ButtonState::Pressed),
ButtonState::Released => WEnum::Value(wl_pointer::ButtonState::Released),
};
let _ = ptr.send_event(wl_pointer::Event::Button {
serial,
time,
button: button_code(button),
state: wl_state,
});
pointer_frame(&ptr);
}
}
}
/// Diagnostyka (`SELENE_STATS=1`): który element dekoracji został kliknięty.
fn debug_part(state: &State, slot: usize, what: &str) {
if std::env::var("SELENE_STATS").is_err() {
return;
}
let geometry = state
.windows
.lock_or_recover()
.get(slot)
.map(|w| (w.x, w.y, w.width, w.height, w.ssd, w.maximized, w.minimized));
eprintln!("selene/win: klik {what} slot={slot} {geometry:?}");
}
fn pointer_leave(state: &mut State) {
if let Some((_, surface)) = state.pointer_focus.clone() {
if let Some(ptr) = pointer_for(state, &surface) {
let serial = state.next_serial();
let _ = ptr.send_event(wl_pointer::Event::Leave { serial, surface });
pointer_frame(&ptr);
}
}
state.pointer_focus = None;
}
/// Zamyka grupę zdarzeń wskaźnika (`wl_pointer.frame`, protokół ≥ 5).
///
/// Klienty takie jak GPUI (Zed) czekają z przetworzeniem ruchu/scrolla na `frame`;
/// bez tego zdarzenia są gromadzone i nigdy nie trafiają do aplikacji.
fn pointer_frame(ptr: &wl_pointer::WlPointer) {
if ptr.version() >= 5 {
let _ = ptr.send_event(wl_pointer::Event::Frame);
}
}
fn pointer_axis(state: &mut State, dx: f64, dy: f64) {
let Some((_, surface)) = state.pointer_focus.clone() else {
return;
};
let Some(ptr) = pointer_for(state, &surface) else {
return;
};
let time = state.time_ms();
// `wl_pointer.axis` oczekuje wartości w **jednostkach powierzchni** (pikselach),
// a evdev daje „ząbki” (±1). Skalujemy więc ząbek do [`WHEEL_STEP_PX`] px —
// inaczej klienci (a nasza powłoka) przewijaliby 1 px na obrót kółka.
//
// Od wersji 5 protokołu wysyłamy też `axis_source` i `axis_discrete`: GPUI
// (Zed) ignoruje `axis` przy źródle „wheel” i przewija na podstawie `discrete`.
if ptr.version() >= 5 {
let _ = ptr.send_event(wl_pointer::Event::AxisSource {
axis_source: WEnum::Value(wl_pointer::AxisSource::Wheel),
});
}
if dy != 0.0 {
let _ = ptr.send_event(wl_pointer::Event::Axis {
time,
axis: WEnum::Value(wl_pointer::Axis::VerticalScroll),
value: dy * WHEEL_STEP_PX,
});
if ptr.version() >= 5 {
let _ = ptr.send_event(wl_pointer::Event::AxisDiscrete {
axis: WEnum::Value(wl_pointer::Axis::VerticalScroll),
discrete: dy.round() as i32,
});
}
}
if dx != 0.0 {
let _ = ptr.send_event(wl_pointer::Event::Axis {
time,
axis: WEnum::Value(wl_pointer::Axis::HorizontalScroll),
value: dx * WHEEL_STEP_PX,
});
if ptr.version() >= 5 {
let _ = ptr.send_event(wl_pointer::Event::AxisDiscrete {
axis: WEnum::Value(wl_pointer::Axis::HorizontalScroll),
discrete: dx.round() as i32,
});
}
}
pointer_frame(&ptr);
}
fn update_pointer_focus(state: &mut State, pos: Point) {
let target = content_window(state, pos);
let same = matches!((state.pointer_focus.as_ref(), target), (Some((s, _)), Some(t)) if *s == t);
if same {
return;
}
if let Some((_, surface)) = state.pointer_focus.clone() {
if let Some(ptr) = pointer_for(state, &surface) {
let serial = state.next_serial();
let _ = ptr.send_event(wl_pointer::Event::Leave { serial, surface });
pointer_frame(&ptr);
}
}
state.pointer_focus = None;
if let Some(slot) = target {
if let Some(surface) = state.slots.get(slot).cloned() {
let serial = state.next_serial();
let (lx, ly) = content_local(state, slot, pos);
if let Some(ptr) = pointer_for(state, &surface) {
let _ = ptr.send_event(wl_pointer::Event::Enter {
serial,
surface: surface.clone(),
surface_x: lx,
surface_y: ly,
});
pointer_frame(&ptr);
}
state.pointer_focus = Some((slot, surface));
}
}
}
/// Przeskalowanie okna w trakcie przeciągania krawędzi/narożnika; wysyła `xdg_toplevel.configure`.
///
/// Okno nie może wyjść za monitor ani schować paska tytułu pod górnym panelem —
/// przyciągamy krawędź do obszaru roboczego i przeliczamy rozmiar tak, by przeciwna
/// krawędź została w miejscu (zachowanie „gumki”).
fn do_resize(state: &mut State, pos: Point) {
let Some(rs) = state.resize else {
return;
};
let (ox, oy, ow, oh) = window_output_rect(state, rs.slot);
let top = window_inset(state, rs.slot);
let bar = state
.windows
.lock_or_recover()
.get(rs.slot)
.map(|w| w.bar())
.unwrap_or(0.0);
let right = ox + ow;
let work_top = oy + top;
let bottom = oy + oh;
let dx = pos.x - rs.start_px;
let dy = pos.y - rs.start_py;
let mut x = rs.start_x;
let mut y = rs.start_y;
let mut w = rs.start_w;
let mut h = rs.start_h;
if rs.edges & EDGE_LEFT != 0 {
w = (rs.start_w - dx).max(MIN_W);
x = rs.start_x + (rs.start_w - w);
if x < ox {
x = ox;
w = (rs.start_x + rs.start_w - x).max(MIN_W);
}
} else if rs.edges & EDGE_RIGHT != 0 {
w = (rs.start_w + dx).max(MIN_W).min((right - x).max(MIN_W));
}
if rs.edges & EDGE_TOP != 0 {
h = (rs.start_h - dy).max(MIN_H);
y = rs.start_y + (rs.start_h - h);
// Pasek tytułu nigdy pod panelem — dolna krawędź zostaje w miejscu.
if y < work_top {
y = work_top;
h = (rs.start_y + rs.start_h - y).max(MIN_H);
}
} else if rs.edges & EDGE_BOTTOM != 0 {
h = (rs.start_h + dy).max(MIN_H);
h = h.min((bottom - y - bar).max(MIN_H));
}
{
let mut windows = state.windows.lock_or_recover();
if let Some(w2) = windows.get_mut(rs.slot) {
w2.x = x;
w2.y = y;
w2.width = w as i32;
w2.height = h as i32;
}
}
send_configure(state, rs.slot, w, h);
// Okno X11: bez tego klient nie wie o nowym rozmiarze (i nie przerysuje treści).
sync_x11_geometry(state, rs.slot);
}
/// Wysyła konfigurację rozmiaru i stanów (`xdg_surface.configure` + `xdg_toplevel.configure`).
pub(crate) fn send_configure(state: &mut State, slot: usize, w: f64, h: f64) {
let Some(surface) = state.slots.get(slot).cloned() else {
return;
};
let Some(sd) = surface.data::<SurfaceData>() else {
return;
};
let Some(xs) = sd.xdg_surface.lock_or_recover().clone() else {
return;
};
let states = window_states(state, slot);
if std::env::var("SELENE_STATS").is_ok() {
eprintln!("selene/win: send_configure slot={slot} {w}x{h}");
}
let serial = state.next_serial();
// Kolejność ma znaczenie: `xdg_toplevel.configure` **przed** `xdg_surface.configure`.
// `xdg_surface.configure` kończy sekwencję konfiguracji — klient zatwierdza wtedy
// ostatni stan toplevela. Odwrotna kolejność (surface → toplevel) sprawiała, że
// klient aplikował **poprzedni** rozmiar, a nowy zostawał w kolejce na zawsze
// (maksymalizacja „nie działała”, okno zostawało 1200×1028).
if let Some(xd) = xs.data::<XdgSurfaceData>() {
if let Some(tl) = xd.toplevel.lock_or_recover().as_ref() {
let _ = tl.send_event(xdg_toplevel::Event::Configure {
width: w as i32,
height: h as i32,
states,
});
}
}
let _ = xs.send_event(xdg_surface::Event::Configure { serial });
}
/// Stany `xdg_toplevel` dla okna (ACTIVATED + ewentualnie MAXIMIZED/FULLSCREEN).
///
/// Protokół przesyła je jako tablicę **u32** (`array` uint), a binding Waylanda
/// wystawia ją jako `Vec<u8>` — trzeba więc zapisać każdy stan jako 4 bajty.
/// Wcześniej wysyłaliśmy pojedyncze bajty i klient (SCTK/GTK) widział pustą listę:
/// okno nigdy nie było „zmaksymalizowane”, więc jego przycisk max/min/close
/// zachowywał się losowo.
fn window_states(state: &State, slot: usize) -> Vec<u8> {
let mut list = vec![4u32]; // ACTIVATED
if let Some(w) = state.windows.lock_or_recover().get(slot) {
if w.maximized {
list.push(1); // MAXIMIZED
}
if w.fullscreen {
list.push(2); // FULLSCREEN
}
}
encode_states(&list)
}
/// Koduje stany `xdg_toplevel` w tablicę bajtów oczekiwaną przez protokół.
fn encode_states(list: &[u32]) -> Vec<u8> {
list.iter().flat_map(|s| s.to_ne_bytes()).collect()
}
/// Prostokąt monitora pod punktem (x, y, szerokość, wysokość).
fn output_rect_at(state: &State, pos: Point) -> (f64, f64, f64, f64) {
let outs = state.outputs.lock_or_recover();
let idx = crate::monitors::output_at(&outs, pos.x, pos.y);
match outs.get(idx).or_else(|| outs.first()) {
Some(o) => (o.x as f64, o.y as f64, o.width as f64, o.height as f64),
None => (0.0, 0.0, 1280.0, 800.0),
}
}
/// Prostokąt monitora, na którym leży okno (x, y, szerokość, wysokość).
fn window_output_rect(state: &State, slot: usize) -> (f64, f64, f64, f64) {
let center = state
.windows
.lock_or_recover()
.get(slot)
.map(|w| w.content().center())
.unwrap_or(Point::new(0.0, 0.0));
output_rect_at(state, center)
}
/// Indeks monitora, na którym leży okno.
fn window_output_index(state: &State, slot: usize) -> usize {
let center = state
.windows
.lock_or_recover()
.get(slot)
.map(|w| w.content().center())
.unwrap_or(Point::new(0.0, 0.0));
let outs = state.outputs.lock_or_recover();
crate::monitors::output_at(&outs, center.x, center.y)
}
/// Górna strefa wyłączna panelu na danym monitorze (0 = brak panelu).
fn inset_for_output(state: &State, idx: usize) -> f64 {
state.top_insets.get(&idx).copied().unwrap_or(0.0)
}
/// Górna strefa wyłączna panelu na monitorze, na którym leży okno.
fn window_inset(state: &State, slot: usize) -> f64 {
inset_for_output(state, window_output_index(state, slot))
}
/// Górna strefa wyłączna panelu na monitorze pod punktem.
fn inset_at(state: &State, pos: Point) -> f64 {
let idx = {
let outs = state.outputs.lock_or_recover();
crate::monitors::output_at(&outs, pos.x, pos.y)
};
inset_for_output(state, idx)
}
/// Ogranicza pozycję przeciąganego okna do obszaru roboczego.
///
/// Pionowo: pasek tytułu nigdy nie chowa się pod górnym panelem (`top_inset`) ani nie
/// wyjeżdża tak daleko, by nie dało się go złapać. Poziomo: co najmniej
/// [`MIN_VISIBLE`] pikseli okna zostaje na monitorze.
fn clamp_drag_pos(
area: (f64, f64, f64, f64),
top_inset: f64,
width: f64,
x: f64,
y: f64,
) -> (f64, f64) {
let (ox, oy, ow, oh) = area;
let min_y = oy + top_inset;
let max_y = (oy + oh - MIN_VISIBLE).max(min_y);
let y = y.clamp(min_y, max_y);
let min_x = ox - (width - MIN_VISIBLE).max(0.0);
let max_x = (ox + ow - MIN_VISIBLE).max(min_x);
let x = x.clamp(min_x, max_x);
(x, y)
}
/// Zaczyna przeciąganie okna.
///
/// Zmaxymalizowane/pełnoekranowe okno najpierw wraca do rozmiaru domyślnego — kursor
/// zostaje w tym samym miejscu względem szerokości (typowe zachowanie pulpitów).
fn begin_drag(state: &mut State, slot: usize, pos: Point) {
let info = {
let windows = state.windows.lock_or_recover();
let Some(w) = windows.get(slot) else {
return;
};
(
w.maximized,
w.fullscreen,
w.x,
w.y,
w.width as f64,
w.height as f64,
)
};
if info.0 || info.1 {
let fx = if info.4 > 0.0 {
((pos.x - info.2) / info.4).clamp(0.0, 1.0)
} else {
0.5
};
restore_window(state, slot);
if let Some((nx, ny, nw, nh)) = window_geometry(state, slot) {
state.drag = Some(DragState {
slot,
off_x: fx * nw,
// Chwytamy pasek tytułu, nawet jeśli kursor był wyżej (np. na kliencie CSD).
off_y: (pos.y - ny).clamp(0.0, (TITLEBAR - 1.0).max(0.0)),
start_px: pos.x,
start_py: pos.y,
origin: SavedGeom {
x: nx,
y: ny,
width: nw as i32,
height: nh as i32,
},
});
}
return;
}
state.drag = Some(DragState {
slot,
off_x: pos.x - info.2,
off_y: pos.y - info.3,
start_px: pos.x,
start_py: pos.y,
origin: SavedGeom {
x: info.2,
y: info.3,
width: info.4 as i32,
height: info.5 as i32,
},
});
}
/// Ustawia minimalizację okna (ukrycie / przywrócenie).
pub fn set_minimized(state: &mut State, slot: usize, minimized: bool) {
let changed = {
let mut windows = state.windows.lock_or_recover();
match windows.get_mut(slot) {
Some(w) if !w.popup && !w.layer && w.minimized != minimized => {
w.minimized = minimized;
true
}
_ => false,
}
};
if changed {
if minimized {
clear_focus_if(state, slot);
} else {
raise_and_focus(state, slot);
}
crate::foreign_toplevel::broadcast_states(state);
}
}
/// Przełącza minimalizację (ukrycie przez kompozytor).
fn toggle_minimize(state: &mut State, slot: usize) {
let cur = state
.windows
.lock_or_recover()
.get(slot)
.map(|w| w.minimized)
.unwrap_or(false);
set_minimized(state, slot, !cur);
}
/// Ustawia stan maksymalizacji okna.
pub fn set_maximized(state: &mut State, slot: usize, maximized: bool) {
let cur = state
.windows
.lock_or_recover()
.get(slot)
.map(|w| w.maximized);
if std::env::var("SELENE_STATS").is_ok() {
eprintln!("selene/win: set_maximized slot={slot} cur={cur:?} want={maximized}");
}
if cur != Some(maximized) {
toggle_maximize(state, slot);
}
}
/// Ustawia stan pełnego ekranu okna.
pub fn set_fullscreen(state: &mut State, slot: usize, fullscreen: bool) {
let cur = state
.windows
.lock_or_recover()
.get(slot)
.map(|w| w.fullscreen);
if cur != Some(fullscreen) {
toggle_fullscreen(state, slot);
}
}
/// Przełącza maksymalizację (zapamiętuje/odtwarza geometrię).
fn toggle_maximize(state: &mut State, slot: usize) {
let (ox, oy, ow, oh) = window_output_rect(state, slot);
// Górny panel rezerwuje miejsce **tylko na swoim monitorze**.
let top = window_inset(state, slot);
{
let mut windows = state.windows.lock_or_recover();
let Some(w) = windows.get_mut(slot) else {
return;
};
if w.popup || w.layer {
return;
}
if w.maximized {
if let Some(sv) = w.saved.take() {
w.x = sv.x;
w.y = sv.y;
w.width = sv.width;
w.height = sv.height;
}
w.maximized = false;
} else {
// Zapamiętujemy **tylko** pierwszy rozmiar „normalny”. Po przyciągnięciu do
// połowy/ćwiartki `saved` już jest ustawione i maksymalizacja nie może go
// nadpisać — inaczej powrót z pełnego ekranu gubił rozmiar domyślny.
if !w.fullscreen && w.saved.is_none() {
w.saved = Some(SavedGeom {
x: w.x,
y: w.y,
width: w.width,
height: w.height,
});
}
w.maximized = true;
w.fullscreen = false;
w.minimized = false;
// Okna CSD (GTK/libadwaita, X11) nie mają naszego paska tytułu — nie
// rezerwujemy dla niego wysokości paska, bo zostawałaby pusta szrama u dołu.
let bar = if w.ssd { TITLEBAR } else { 0.0 };
w.x = ox;
w.y = oy + top;
w.width = ow as i32;
w.height = (oh - top - bar).max(MIN_H) as i32;
}
}
let (nw, nh) = window_geometry(state, slot)
.map(|(_, _, w, h)| (w, h))
.unwrap_or((ow, oh));
send_configure(state, slot, nw, nh);
// Okno X11: powiadom klienta o stanie maksymalizacji (EWMH `_NET_WM_STATE`)
// i przekaż mu nową geometrię — X11 nie zna `xdg_toplevel.configure`.
let maximized = state
.windows
.lock_or_recover()
.get(slot)
.map(|w| w.maximized)
.unwrap_or(false);
if let Some(xwm) = crate::xwm::global() {
if let Some(win) = x11_window(state, slot) {
xwm.set_maximized(win, maximized);
}
}
sync_x11_geometry(state, slot);
crate::foreign_toplevel::broadcast_states(state);
}
/// Przełącza pełny ekran (ukrywa dekorację).
fn toggle_fullscreen(state: &mut State, slot: usize) {
let (ox, oy, ow, oh) = window_output_rect(state, slot);
// Panel górny jest zawsze widoczny na swoim monitorze — pełny ekran zajmuje
// **obszar roboczy tylko tam**. Na monitorze bez panelu okno dostaje całą wysokość.
let top = window_inset(state, slot);
// Nowa kolejność dla wejścia i wyjścia — okno wraca nad pozostałe.
let raise_z = state.next_z();
{
let mut windows = state.windows.lock_or_recover();
let Some(w) = windows.get_mut(slot) else {
return;
};
if w.popup || w.layer {
return;
}
if w.fullscreen {
if let Some(sv) = w.saved.take() {
w.x = sv.x;
w.y = sv.y;
w.width = sv.width;
w.height = sv.height;
}
w.fullscreen = false;
w.z = raise_z;
} else {
if !w.maximized && w.saved.is_none() {
w.saved = Some(SavedGeom {
x: w.x,
y: w.y,
width: w.width,
height: w.height,
});
}
w.fullscreen = true;
w.maximized = false;
w.minimized = false;
w.x = ox;
w.y = oy + top;
w.width = ow as i32;
w.height = (oh - top).max(MIN_H) as i32;
w.z = raise_z;
}
}
let (nw, nh) = window_geometry(state, slot)
.map(|(_, _, w, h)| (w, h))
.unwrap_or((ow, oh));
send_configure(state, slot, nw, nh);
// Okno X11: powiadom klienta o stanie (EWMH `_NET_WM_STATE`) i geometrii pełnego ekranu.
let (fullscreen, maximized) = state
.windows
.lock_or_recover()
.get(slot)
.map(|w| (w.fullscreen, w.maximized))
.unwrap_or((false, false));
if let Some(xwm) = crate::xwm::global() {
if let Some(win) = x11_window(state, slot) {
xwm.set_maximized(win, maximized);
xwm.set_fullscreen(win, fullscreen);
}
}
sync_x11_geometry(state, slot);
crate::foreign_toplevel::broadcast_states(state);
}
/// Przekazuje oknu X11 bieżącą geometrię treści (pozycję i rozmiar).
///
/// X11 nie dostaje `xdg_toplevel.configure`, więc każda zmiana położenia
/// (przeciąganie, maksymalizacja, pełny ekran) musi iść osobnym `ConfigureWindow`.
fn sync_x11_geometry(state: &State, slot: usize) {
let Some((x, y, w, h)) = window_geometry(state, slot) else {
return;
};
// `Window.x/y` to lewy-górny róg **całego** okna (z naszym paskiem). Treść X11
// zaczyna się `bar` pikseli niżej — bez tego klient rysowałby pod dekoracją.
let bar = state
.windows
.lock_or_recover()
.get(slot)
.map(|w| w.bar())
.unwrap_or(0.0);
let Some(xwm) = crate::xwm::global() else {
return;
};
if let Some(win) = x11_window(state, slot) {
xwm.set_geometry(win, x as i32, (y + bar) as i32, w as u32, h as u32);
}
}
/// Okno X11 odpowiadające slotowi (jeśli to nie okno X11 — `None`).
fn x11_window(state: &State, slot: usize) -> Option<u32> {
let surface = state.slots.get(slot)?;
let sd = surface.data::<SurfaceData>()?;
if !*sd.xwayland.lock_or_recover() {
return None;
}
let serial = *sd.x11_serial.lock_or_recover();
crate::xwm::global()?.window_for_serial(serial)
}
/// Zdejmuje fokus klawiatury/wskaźnika z okna (np. po minimalizacji).
pub(crate) fn clear_focus_if(state: &mut State, slot: usize) {
if state.keyboard_focus.as_ref().map(|(s, _)| *s) == Some(slot) {
if let Some((_, surface)) = state.keyboard_focus.clone() {
if let Some(kb) = keyboard_for(state, &surface) {
let serial = state.next_serial();
let _ = kb.send_event(wl_keyboard::Event::Leave { serial, surface });
}
}
state.keyboard_focus = None;
}
if state.pointer_focus.as_ref().map(|(s, _)| *s) == Some(slot) {
pointer_leave(state);
}
crate::foreign_toplevel::broadcast_states(state);
}
/// Prosi klienta o zamknięcie okna (`xdg_toplevel.close`).
fn send_close(state: &mut State, slot: usize) {
let Some(surface) = state.slots.get(slot).cloned() else {
return;
};
let Some(sd) = surface.data::<SurfaceData>() else {
return;
};
// Okno X11 nie ma `xdg_toplevel` — zamykamy je przez XWM (`WM_DELETE_WINDOW`).
if *sd.xwayland.lock_or_recover() {
let serial = *sd.x11_serial.lock_or_recover();
if let Some(xwm) = crate::xwm::global() {
if let Some(win) = xwm.window_for_serial(serial) {
xwm.close(win);
}
}
return;
}
let Some(xs) = sd.xdg_surface.lock_or_recover().clone() else {
return;
};
if let Some(xd) = xs.data::<XdgSurfaceData>() {
if let Some(tl) = xd.toplevel.lock_or_recover().as_ref() {
let _ = tl.send_event(xdg_toplevel::Event::Close);
}
}
}
fn raise_and_focus(state: &mut State, slot: usize) {
let z = state.next_z();
{
let mut windows = state.windows.lock_or_recover();
if let Some(w) = windows.get_mut(slot) {
// Powierzchnie warstwowe (panele/docki) trzymają stałą kolejność.
if !w.layer {
w.z = z;
}
}
}
set_keyboard_focus(state, slot);
crate::foreign_toplevel::broadcast_states(state);
}
/// Aktywuje okno o podanym slocie (z `zwlr_foreign_toplevel_handle_v1.activate`).
/// Zminimalizowane okno zostaje przywrócone — inaczej klik w pasek nie pokazywał okna.
pub fn activate_slot(state: &mut State, slot: usize) {
let minimized = state
.windows
.lock_or_recover()
.get(slot)
.map(|w| w.minimized)
.unwrap_or(false);
if minimized {
set_minimized(state, slot, false);
}
raise_and_focus(state, slot);
}
/// Zamyka okno reprezentowane przez `surface` (z `...handle_v1.close`).
pub fn close_surface(state: &mut State, surface: &wayland_server::protocol::wl_surface::WlSurface) {
if let Some(sd) = surface.data::<SurfaceData>() {
if let Some(slot) = *sd.slot.lock_or_recover() {
send_close(state, slot);
}
}
}
fn set_keyboard_focus(state: &mut State, slot: usize) {
if state.keyboard_focus.as_ref().map(|(s, _)| *s) == Some(slot) {
return;
}
if let Some((_, surface)) = state.keyboard_focus.clone() {
if let Some(kb) = keyboard_for(state, &surface) {
let serial = state.next_serial();
let _ = kb.send_event(wl_keyboard::Event::Leave { serial, surface });
}
}
state.keyboard_focus = None;
if let Some(surface) = state.slots.get(slot).cloned() {
if let Some(kb) = keyboard_for(state, &surface) {
let serial = state.next_serial();
let _ = kb.send_event(wl_keyboard::Event::Enter {
serial,
surface: surface.clone(),
keys: vec![],
});
}
state.keyboard_focus = Some((slot, surface));
// X11: powiadom XWM, które okno jest aktywne (`_NET_ACTIVE_WINDOW`).
notify_xwm_focus(state, slot);
}
}
/// Informuje XWM o aktywnym oknie (jeśli to okno X11).
fn notify_xwm_focus(state: &State, slot: usize) {
let Some(xwm) = crate::xwm::global() else {
return;
};
let Some(surface) = state.slots.get(slot) else {
return;
};
let Some(sd) = surface.data::<SurfaceData>() else {
return;
};
if *sd.xwayland.lock_or_recover() {
let serial = *sd.x11_serial.lock_or_recover();
xwm.set_active(xwm.window_for_serial(serial));
}
}
/// Zgłasza żądanie zrzutu ekranu (realizuje je wątek wyjścia).
fn request_screenshot(state: &mut State) {
state
.screenshot
.store(true, std::sync::atomic::Ordering::Relaxed);
if std::env::var("SELENE_STATS").is_ok() {
eprintln!("selene: żądanie zrzutu ekranu");
}
}
fn keyboard_key(state: &mut State, keysym: u32, keycode: u32, st: ButtonState) {
sync_modifiers(state, keysym, st);
if is_modifier_keysym(keysym) || is_lock_keysym(keysym) {
return;
}
// Escape zamyka podgląd (Super+Tab) i nie trafia do klienta.
if keysym == 0xff1b && state.overview_on() {
state.set_overview(false);
return;
}
// `Print` — zrzut ekranu bez modyfikatora (alternatywa dla Super+F12).
if keysym == 0xff61 {
if st == ButtonState::Pressed {
request_screenshot(state);
}
return;
}
// Alt+Tab — przełącz okna (klasyczny skrót pulpitu)
if state.sent_mods.alt && keysym == 0xff09 && st == ButtonState::Pressed {
cycle_windows(state);
return;
}
// Alt+F4 — zamknij aktywne okno
if state.sent_mods.alt && keysym == 0xffbe && st == ButtonState::Pressed {
if let Some((s, _)) = state.keyboard_focus {
send_close(state, s);
}
return;
}
// Klawisze multimedialne (dźwięk/głośność)
match keysym {
0x1008ff13 => {
if st == ButtonState::Pressed {
let _ = std::process::Command::new("wpctl")
.args(["set-volume", "@DEFAULT_AUDIO_SINK@", "5%+"])
.spawn();
}
return;
}
0x1008ff11 => {
if st == ButtonState::Pressed {
let _ = std::process::Command::new("wpctl")
.args(["set-volume", "@DEFAULT_AUDIO_SINK@", "5%-"])
.spawn();
}
return;
}
0x1008ff12 => {
if st == ButtonState::Pressed {
let _ = std::process::Command::new("wpctl")
.args(["set-mute", "@DEFAULT_AUDIO_SINK@", "toggle"])
.spawn();
}
return;
}
_ => {}
}
// Skróty menedżera okien (Super + …) przechwytujemy przed klientem.
if state.sent_mods.logo && window_shortcut(state, keysym, st) {
return;
}
// Klawiatura klienta okna z fokusem — nie wolno pisać do zasobu innego klienta.
let Some((_, surface)) = state.keyboard_focus.clone() else {
return;
};
let Some(kb) = keyboard_for(state, &surface) else {
return;
};
// Do klienta trafia **fizyczny kod klawisza** (evdev), a nie kod X11 ani keysym —
// klient liczy z niego znak według mapy klawiszy, którą mu wysłaliśmy.
if keycode != 0 {
let serial = state.next_serial();
let time = state.time_ms();
let wl_state = match st {
ButtonState::Pressed => WEnum::Value(wl_keyboard::KeyState::Pressed),
ButtonState::Released => WEnum::Value(wl_keyboard::KeyState::Released),
};
let _ = kb.send_event(wl_keyboard::Event::Key {
serial,
time,
key: keycode,
state: wl_state,
});
}
}
/// Skróty menedżera okien z modyfikatorem Super. Zwraca `true`, gdy obsłużono.
fn window_shortcut(state: &mut State, keysym: u32, st: ButtonState) -> bool {
if st != ButtonState::Pressed {
// Zjedz także zwolnienia naszych klawiszy, by nie trafiły do klienta.
return matches!(
keysym,
0x006d | 0x0066 | 0x006c | 0x004c | 0xff52 | 0xff54 | 0xff09 | 0x0060 | 0x0031
..=0x0038 | 0xff51 | 0xff53 | 0x0071 | 0xffc9
);
}
let focused = state.keyboard_focus.as_ref().map(|(s, _)| *s);
match keysym {
0xffc9 => {
// Super+F12 — zrzut ekranu. Realizuje go wątek wyjścia (ma gotowe klatki).
request_screenshot(state);
true
}
0x0071 => {
// Super+Q — zamknij okno.
if let Some(s) = focused {
send_close(state, s);
}
true
}
0xff51 => {
// Super+Left — kafelkuj do lewej połowy ekranu (snap).
if let Some(s) = focused {
snap_window(state, s, true);
}
true
}
0xff53 => {
// Super+Right — kafelkuj do prawej połowy ekranu (snap).
if let Some(s) = focused {
snap_window(state, s, false);
}
true
}
0x006d => {
// Super+M — minimalizuj/przywróć.
if let Some(s) = focused {
toggle_minimize(state, s);
}
true
}
0x0066 => {
// Super+F — pełny ekran.
if let Some(s) = focused {
toggle_fullscreen(state, s);
}
true
}
0x006c | 0x004c => {
// Super+L — wyloguj. Kończymy kompozytor (sesję), więc menedżer logowania
// wraca do greetera; jądro zwalnia mastera DRM przy zamknięciu deskryptorów.
println!("{}", libluna_i18n::tr("logging out"));
std::process::exit(0);
}
0xff52 => {
// Super+Up — maksymalizuj/przywróć.
if let Some(s) = focused {
toggle_maximize(state, s);
}
true
}
0xff54 => {
// Super+Down — przywróć z maksymalizacji/fullscreenu.
if let Some(s) = focused {
restore_window(state, s);
}
true
}
0xff09 => {
// Super+Tab — podgląd okien i przestrzeni.
let on = state.overview_on();
state.set_overview(!on);
true
}
0x0060 => {
// Super+` — przełącz okna (przywraca zminimalizowane).
cycle_windows(state);
true
}
// Super+1..8 — przełącz przestrzeń; Super+Shift+1..8 — przenieś okno.
0x0031..=0x0038 => {
let ws = (keysym - 0x0031) as usize;
if state.sent_mods.shift {
if let Some(s) = focused {
move_window_to_workspace(state, s, ws);
}
} else {
switch_workspace(state, ws);
}
true
}
_ => false,
}
}
/// Strefa przyciągania okna po upuszczeniu przy krawędzi/narożniku monitora.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SnapZone {
Left,
Right,
TopLeft,
TopRight,
BottomLeft,
BottomRight,
Maximize,
}
/// Wykrywa strefę na podstawie pozycji kursora w obszarze roboczym.
fn snap_zone(pos: Point, area: Rect) -> Option<SnapZone> {
let left = pos.x <= area.x() + SNAP_EDGE;
let right = pos.x >= area.right() - SNAP_EDGE;
let top = pos.y <= area.y() + SNAP_EDGE;
let bottom = pos.y >= area.bottom() - SNAP_EDGE;
match (left, top, bottom, right) {
// Narożniki mają pierwszeństwo — dają ćwiartki.
(true, true, _, _) => Some(SnapZone::TopLeft),
(true, _, true, _) => Some(SnapZone::BottomLeft),
(_, true, _, true) => Some(SnapZone::TopRight),
(_, _, true, true) => Some(SnapZone::BottomRight),
// Krawędzie — połowy; góra (środek) — maksymalizacja.
(true, _, _, _) => Some(SnapZone::Left),
(_, _, _, true) => Some(SnapZone::Right),
(_, true, _, _) => Some(SnapZone::Maximize),
_ => None,
}
}
/// Geometria **treści** okna dla strefy: `(x, y, szerokość, wysokość)`.
///
/// `bar` to wysokość naszego paska tytułu (0 dla CSD) — dzięki temu zewnętrzna
/// krawędź okna dokładnie pokrywa się z krawędzią połowy/ćwiartki monitora.
fn snap_rect(zone: SnapZone, area: Rect, bar: f64) -> (f64, f64, f64, f64) {
let half_w = (area.width() / 2.0).round().max(MIN_W);
let right_w = (area.width() - half_w).max(MIN_W);
let half_outer_h = (area.height() / 2.0).round();
let quarter_h = (half_outer_h - bar).max(MIN_H);
let full_h = (area.height() - bar).max(MIN_H);
let bottom_y = area.y() + half_outer_h;
match zone {
SnapZone::Maximize => (area.x(), area.y(), area.width(), full_h),
SnapZone::Left => (area.x(), area.y(), half_w, full_h),
SnapZone::Right => (area.x() + half_w, area.y(), right_w, full_h),
SnapZone::TopLeft => (area.x(), area.y(), half_w, quarter_h),
SnapZone::TopRight => (area.x() + half_w, area.y(), right_w, quarter_h),
SnapZone::BottomLeft => (area.x(), bottom_y, half_w, quarter_h),
SnapZone::BottomRight => (area.x() + half_w, bottom_y, right_w, quarter_h),
}
}
/// Obszar roboczy monitora pod kursorem (bez górnego panelu — jeśli tam jest).
fn workarea_at(state: &State, pos: Point) -> Rect {
let (ox, oy, ow, oh) = output_rect_at(state, pos);
let top = inset_at(state, pos).min(oh - MIN_H);
Rect::from_xywh(ox, oy + top, ow, (oh - top).max(MIN_H))
}
/// Po upuszczeniu przeciąganego okna: przyciągnij je, jeśli kursor jest przy krawędzi.
fn apply_drag_snap(state: &mut State, slot: usize, pos: Point, origin: SavedGeom) {
if let Some(zone) = snap_zone(pos, workarea_at(state, pos)) {
snap_to_zone(state, slot, zone, Some(origin));
}
}
/// Ustawia geometrię treści okna (bez ruszania zapamiętanego rozmiaru).
fn place_window(state: &mut State, slot: usize, x: f64, y: f64, w: f64, h: f64) {
{
let mut windows = state.windows.lock_or_recover();
let Some(win) = windows.get_mut(slot) else {
return;
};
if win.popup || win.layer {
return;
}
win.x = x;
win.y = y;
win.width = w as i32;
win.height = h as i32;
}
send_configure(state, slot, w, h);
sync_x11_geometry(state, slot);
crate::foreign_toplevel::broadcast_states(state);
}
/// Przyciąga okno do strefy: połowa, ćwiartka albo maksymalizacja.
///
/// `remember` to geometria sprzed przeciągania — wraca jako rozmiar domyślny
/// (Super+Down / podwójny klik po wyjściu ze stanu).
fn snap_to_zone(state: &mut State, slot: usize, zone: SnapZone, remember: Option<SavedGeom>) {
if zone == SnapZone::Maximize {
set_maximized(state, slot, true);
return;
}
let (ox, oy, ow, oh) = window_output_rect(state, slot);
let top = window_inset(state, slot).min(oh - MIN_H);
let work = Rect::from_xywh(ox, oy + top, ow, (oh - top).max(MIN_H));
let bar = {
let windows = state.windows.lock_or_recover();
match windows.get(slot) {
Some(w) if !w.popup && !w.layer => {
if w.ssd {
TITLEBAR
} else {
0.0
}
}
_ => return,
}
};
let (x, y, w, h) = snap_rect(zone, work, bar);
{
let mut windows = state.windows.lock_or_recover();
let Some(win) = windows.get_mut(slot) else {
return;
};
// Pierwszy „normalny” rozmiar jest punktem powrotu dla maksymalizacji i
// pełnego ekranu; kolejne przyciągnięcia go nie nadpisują.
if win.saved.is_none() {
win.saved = remember.or(Some(SavedGeom {
x: win.x,
y: win.y,
width: win.width,
height: win.height,
}));
}
win.maximized = false;
win.fullscreen = false;
win.minimized = false;
}
place_window(state, slot, x, y, w, h);
}
/// Kafelkuje okno do lewej lub prawej połowy ekranu (skrót klawiszowy).
fn snap_window(state: &mut State, slot: usize, left: bool) {
snap_to_zone(
state,
slot,
if left {
SnapZone::Left
} else {
SnapZone::Right
},
None,
);
}
/// Przywraca okno z minimalizacji/maksymalizacji/fullscreenu/przyciągnięcia.
fn restore_window(state: &mut State, slot: usize) {
let (min, max, full, saved) = {
let windows = state.windows.lock_or_recover();
match windows.get(slot) {
Some(w) => (w.minimized, w.maximized, w.fullscreen, w.saved),
None => return,
}
};
if min {
toggle_minimize(state, slot);
} else if full {
toggle_fullscreen(state, slot);
} else if max {
toggle_maximize(state, slot);
} else if let Some(sv) = saved {
// Okno przyciągnięte do połowy/ćwiartki — wróć do rozmiaru domyślnego.
if let Some(w) = state.windows.lock_or_recover().get_mut(slot) {
w.saved = None;
}
place_window(state, slot, sv.x, sv.y, sv.width as f64, sv.height as f64);
}
}
/// Przełącza fokus na kolejne okno (przywracając je, jeśli było zminimalizowane).
fn cycle_windows(state: &mut State) {
let slots: Vec<usize> = {
let windows = state.windows.lock_or_recover();
windows
.iter()
.enumerate()
.filter(|(_, w)| w.mapped && !w.popup && !w.layer)
.map(|(i, _)| i)
.collect()
};
if slots.is_empty() {
return;
}
let cur = state.keyboard_focus.as_ref().map(|(s, _)| *s);
let next = match cur.and_then(|c| slots.iter().position(|&s| s == c)) {
Some(p) => slots[(p + 1) % slots.len()],
None => slots[0],
};
if let Some(w) = state.windows.lock_or_recover().get_mut(next) {
w.minimized = false;
}
raise_and_focus(state, next);
}
/// Najwyższe zwykłe okno w danej przestrzeni roboczej.
fn top_window_in_ws(state: &State, ws: usize) -> Option<usize> {
let windows = state.windows.lock_or_recover();
windows
.iter()
.enumerate()
.filter(|(_, w)| w.mapped && !w.minimized && !w.popup && !w.layer && w.workspace == ws)
.max_by_key(|(_, w)| w.z)
.map(|(i, _)| i)
}
/// Przełącza bieżącą przestrzeń roboczą, przenosząc fokus na jej wierzchołek.
pub fn switch_workspace(state: &mut State, ws: usize) {
let ws = ws.min(state.ws_count.saturating_sub(1));
{
let mut cur = state.ws_current.lock_or_recover();
if *cur == ws {
return;
}
*cur = ws;
}
// Zdejmij fokus z okna, którego nie ma już na ekranie.
let focused = state.keyboard_focus.as_ref().map(|(s, _)| *s);
let keep = focused
.and_then(|s| {
state
.windows
.lock_or_recover()
.get(s)
.map(|w| w.in_workspace(ws))
})
.unwrap_or(false);
if !keep {
set_keyboard_focus(state, usize::MAX);
if let Some(top) = top_window_in_ws(state, ws) {
raise_and_focus(state, top);
}
}
crate::workspace::broadcast_workspace_states(state);
}
/// Przenosi okno o podanym tytule na przestrzeń `ws` (polecenie z powłoki, PPM w taskbarze).
///
/// Preferujemy okno z fokusem (jeśli tytuł pasuje), potem pierwsze o dokładnie takim
/// tytule. Tytuły są tym, co widzi użytkownik w taskbarze — nie potrzebujemy wewnętrznych
/// identyfikatorów kompozytora.
pub fn move_window_by_title(state: &mut State, title: &str, ws: usize) {
if title.is_empty() || ws >= state.ws_count {
return;
}
let slot = {
let windows = state.windows.lock_or_recover();
let matches = |s: usize| {
windows
.get(s)
.map(|w| !w.layer && !w.popup && w.title == title)
.unwrap_or(false)
};
state
.keyboard_focus
.as_ref()
.map(|(s, _)| *s)
.filter(|s| matches(*s))
.or_else(|| (0..windows.len()).find(|s| matches(*s)))
};
match slot {
Some(slot) => {
move_window_to_workspace(state, slot, ws);
if std::env::var("SELENE_STATS").is_ok() {
eprintln!("selene/control: przeniesiono {title:?} → przestrzeń {ws}");
}
}
None => {
if std::env::var("SELENE_STATS").is_ok() {
eprintln!("selene/control: nie znaleziono okna {title:?}");
}
}
}
}
/// Przenosi okno do zadanej przestrzeni roboczej (skrót Super+Shift+cyfra).
fn move_window_to_workspace(state: &mut State, slot: usize, ws: usize) {
let ws = ws.min(state.ws_count.saturating_sub(1));
{
let mut windows = state.windows.lock_or_recover();
match windows.get_mut(slot) {
Some(w) if !w.layer && !w.popup => w.workspace = ws,
_ => return,
}
}
let cur = *state.ws_current.lock_or_recover();
if ws != cur && state.keyboard_focus.as_ref().map(|(s, _)| *s) == Some(slot) {
set_keyboard_focus(state, usize::MAX);
if let Some(top) = top_window_in_ws(state, cur) {
raise_and_focus(state, top);
}
}
crate::workspace::broadcast_workspace_states(state);
}
/// Monitor pod punktem: `(origin_x, origin_y, (szerokość, wysokość))`.
fn overview_output(state: &State, pos: Point) -> (f64, f64, (f64, f64)) {
let outs = state.outputs.lock_or_recover();
let idx = crate::monitors::output_at(&outs, pos.x, pos.y);
match outs.get(idx) {
Some(o) => (o.x as f64, o.y as f64, (o.width as f64, o.height as f64)),
None => (0.0, 0.0, (1280.0, 800.0)),
}
}
/// Slot okna pod punktem w podglądzie (na monitorze pod kursorem).
fn overview_tile_at(state: &State, pos: Point) -> Option<usize> {
let (ox, oy, size) = overview_output(state, pos);
let local = Point::new(pos.x - ox, pos.y - oy);
let cur_ws = *state.ws_current.lock_or_recover();
let slots = {
let w = state.windows.lock_or_recover();
crate::state::overview_slots_on(&w, cur_ws, ox, size.0)
};
let tiles = crate::state::overview_tiles(slots.len(), size);
tiles
.iter()
.position(|r| r.contains(local))
.and_then(|k| slots.get(k).copied())
}
/// Indeks „chipa” przestrzeni pod punktem.
fn overview_chip_at(state: &State, pos: Point) -> Option<usize> {
let (ox, oy, size) = overview_output(state, pos);
let local = Point::new(pos.x - ox, pos.y - oy);
crate::state::overview_ws_chips(state.ws_count, size)
.iter()
.position(|r| r.contains(local))
}
/// Wysyła wciśnięcia/zwolnienia klawiszy modyfikujących wynikające z bieżącego stanu.
fn sync_modifiers(state: &mut State, keysym: u32, st: ButtonState) {
let mut mods = state.sent_mods;
if let Some(field) = modifier_for_keysym(keysym) {
match st {
ButtonState::Pressed => *field(&mut mods) = true,
ButtonState::Released => *field(&mut mods) = false,
}
}
// CapsLock/NumLock to **blokady**, nie modyfikatory wciśnięte. Klient nie dostaje
// ich jako zwykłego klawisza (inaczej przełączyłby stan drugi raz); dostaje
// wyłącznie `wl_keyboard.modifiers` z `mods_locked`.
let mut locked_changed = false;
if st == ButtonState::Pressed {
match keysym {
0xffe5 => {
state.caps_lock = !state.caps_lock;
locked_changed = true;
}
0xff7f => {
state.num_lock = !state.num_lock;
locked_changed = true;
}
_ => {}
}
}
// Stan modyfikatorów jest globalny, więc rozgłaszamy go do klawiatur wszystkich
// klientów (każdy ma własny zasób `wl_keyboard`).
let keyboards: Vec<wl_keyboard::WlKeyboard> = state
.keyboards
.iter()
.filter(|k| k.is_alive())
.cloned()
.collect();
for kb in &keyboards {
for (before, now, code) in [
// Kody **evdev** klawiszy modyfikujących (protokół Wayland używa evdev, nie X11).
(state.sent_mods.shift, mods.shift, 42u32), // KEY_LEFTSHIFT
(state.sent_mods.ctrl, mods.ctrl, 29), // KEY_LEFTCTRL
(state.sent_mods.alt, mods.alt, 56), // KEY_LEFTALT
(state.sent_mods.logo, mods.logo, 125), // KEY_LEFTMETA
(state.sent_mods.level3, mods.level3, 100), // KEY_RIGHTALT (AltGr)
] {
if before != now {
let serial = state.next_serial();
let time = state.time_ms();
let ks = if now {
WEnum::Value(wl_keyboard::KeyState::Pressed)
} else {
WEnum::Value(wl_keyboard::KeyState::Released)
};
let _ = kb.send_event(wl_keyboard::Event::Key {
serial,
time,
key: code,
state: ks,
});
}
}
}
if mods != state.sent_mods || locked_changed {
state.sent_mods = mods;
// Lock = bit 1, Mod2 (NumLock) = bit 4 — tak samo jak w masce XKB.
let locked = (u32::from(state.caps_lock) << 1) | (u32::from(state.num_lock) << 4);
for kb in &keyboards {
let serial = state.next_serial();
let _ = kb.send_event(wl_keyboard::Event::Modifiers {
serial,
mods_depressed: modifier_mask(mods),
mods_latched: 0,
mods_locked: locked,
group: 0,
});
}
}
}
fn modifier_for_keysym(keysym: u32) -> Option<fn(&mut Modifiers) -> &mut bool> {
match keysym {
0xffe1 | 0xffe2 => Some(|m| &mut m.shift), // Shift_L / Shift_R
0xffe3 | 0xffe4 => Some(|m| &mut m.ctrl), // Control_L / Control_R
0xffe9 | 0xffea => Some(|m| &mut m.alt), // Alt_L / Alt_R
0xffeb | 0xffec => Some(|m| &mut m.logo), // Super_L / Super_R
0xfe03 => Some(|m| &mut m.level3), // ISO_Level3_Shift (AltGr)
_ => None,
}
}
fn is_modifier_keysym(keysym: u32) -> bool {
modifier_for_keysym(keysym).is_some()
}
/// CapsLock/NumLock — blokady obsługiwane przez `mods_locked` (nie przez zwykły klawisz).
fn is_lock_keysym(keysym: u32) -> bool {
matches!(keysym, 0xffe5 | 0xff7f)
}
fn modifier_mask(m: Modifiers) -> u32 {
(m.shift as u32)
| ((m.ctrl as u32) << 2)
| ((m.alt as u32) << 3)
| ((m.logo as u32) << 6)
// Poziom 3 (AltGr) w XKB to zwykle Mod5 → bit 7.
| ((m.level3 as u32) << 7)
}
fn button_code(button: PointerButton) -> u32 {
match button {
PointerButton::Left => 0x110,
PointerButton::Right => 0x111,
PointerButton::Middle => 0x112,
PointerButton::Back => 0x116,
PointerButton::Forward => 0x115,
PointerButton::Other(c) => c,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::{Window, BUTTON_W};
use libluna_render::ImageData;
use std::sync::Arc;
#[test]
fn modifier_mask_matches_xkb() {
let m = Modifiers {
shift: true,
ctrl: true,
alt: false,
logo: true,
level3: false,
};
assert_eq!(modifier_mask(m), 1 | 4 | 64);
}
#[test]
fn recognizes_modifier_keysyms() {
assert!(is_modifier_keysym(0xffe1)); // Shift_L
assert!(!is_modifier_keysym(0x0061));
}
#[test]
fn lock_keys_use_locked_state() {
// CapsLock/NumLock nie są „wciśniętymi” modyfikatorami — idą przez `mods_locked`.
assert!(is_lock_keysym(0xffe5)); // Caps_Lock
assert!(is_lock_keysym(0xff7f)); // Num_Lock
assert!(!is_modifier_keysym(0xffe5));
}
#[test]
fn locale_maps_to_xkb_layout() {
assert_eq!(locale_to_layout("pl_PL.UTF-8"), Some("pl".to_owned()));
assert_eq!(locale_to_layout("pl"), Some("pl".to_owned()));
assert_eq!(locale_to_layout("de_DE@euro"), Some("de".to_owned()));
assert_eq!(locale_to_layout("en_GB.UTF-8"), Some("gb".to_owned()));
assert_eq!(locale_to_layout("en_US.UTF-8"), Some("en".to_owned()));
// Bez języka nie ma czego zgadywać — zostaje domyślny `us`.
assert_eq!(locale_to_layout("C"), None);
assert_eq!(locale_to_layout("POSIX"), None);
assert_eq!(locale_to_layout(""), None);
}
#[test]
fn keymap_includes_locale_layout() {
let text = keymap_text_for("pl", None);
assert!(text.contains("pc+pl+inet(evdev)+level3(ralt_switch)"));
let with_variant = keymap_text_for("pl", Some("legacy"));
assert!(with_variant.contains("pc+pl(legacy)+inet(evdev)+level3(ralt_switch)"));
}
#[test]
fn compiled_keymap_keeps_altgr() {
// Kompilujemy RMLVO — opcja musi być w formacie `lv3:ralt_switch`.
// Wcześniej `level3(ralt_switch)` było odrzucane przez xkbcommon (XKB-595)
// i AltGr nie dawał poziomu 3 (ą, ć, ę, ł, ń, ó, ś, ź, ż).
if let Some(km) = compiled_keymap("pl", None) {
assert!(
km.contains("level3(ralt_switch)"),
"brak opcji level3 w skompilowanej mapie"
);
assert!(
km.contains("0xfe03"),
"AltGr (ISO_Level3_Shift) nieobecny w skompilowanej mapie"
);
}
}
#[test]
fn altgr_is_level3_and_super_is_logo() {
assert!(matches!(modifier_for_keysym(0xfe03), Some(_)));
let mut m = Modifiers::NONE;
if let Some(f) = modifier_for_keysym(0xfe03) {
*f(&mut m) = true;
}
assert!(m.level3);
assert_eq!(modifier_mask(m), 1 << 7);
}
fn sample_window() -> Window {
Window {
x: 100.0,
y: 50.0,
width: 200,
height: 120,
image_offset: (0.0, 0.0),
z: 1,
title: "Test".to_owned(),
popup: false,
layer: false,
mapped: true,
workspace: 0,
input: None,
input_capture: false,
minimized: false,
maximized: false,
fullscreen: false,
ssd: true,
saved: None,
data: Arc::new(ImageData::empty(200, 120)),
}
}
#[test]
fn content_local_does_not_offset_layer_surfaces() {
// Panel (powierzchnia warstwowa) nie ma paska tytułu — współrzędne bez przesunięcia.
let s = State::default();
let mut w = sample_window();
w.layer = true;
w.x = 0.0;
w.y = 0.0;
s.windows.lock_or_recover().push(w);
assert_eq!(content_local(&s, 0, Point::new(50.0, 20.0)), (50.0, 20.0));
}
#[test]
fn content_local_offsets_titlebar_for_windows() {
let s = State::default();
s.windows.lock_or_recover().push(sample_window()); // x=100, y=50
assert_eq!(
content_local(&s, 0, Point::new(120.0, 80.0)),
(20.0, 80.0 - (50.0 + TITLEBAR))
);
}
#[test]
fn content_local_adds_csd_shadow_offset() {
// Okno CSD: punkt ekranu (240,150) leży 40x50 od widocznego rogu, ale bufor
// zaczyna się 14x12 wcześniej — klient musi dostać (54,62), inaczej jego
// własne przyciski ✕/□ są „obok” kursora i klik nic nie robi.
let s = State::default();
let mut w = sample_window();
w.ssd = false;
w.image_offset = (14.0, 12.0);
s.windows.lock_or_recover().push(w); // x=100, y=50
assert_eq!(content_local(&s, 0, Point::new(140.0, 100.0)), (54.0, 62.0));
}
#[test]
fn capturing_layer_only_outside_its_content() {
// Powłoka z otwartym popupem ma `input_capture`; klik poza jej widoczną
// treścią (np. dolna część ekranu) ma do niej trafić i zamknąć popup.
let s = State::default();
let mut w = sample_window();
w.layer = true;
w.input_capture = true;
w.x = 0.0;
w.y = 0.0;
w.width = 1920;
w.height = 680;
s.windows.lock_or_recover().push(w);
assert_eq!(capturing_layer_outside(&s, Point::new(900.0, 300.0)), None);
assert_eq!(
capturing_layer_outside(&s, Point::new(900.0, 900.0)),
Some(0)
);
s.windows.lock_or_recover()[0].input_capture = false;
assert_eq!(capturing_layer_outside(&s, Point::new(900.0, 900.0)), None);
}
#[test]
fn workspace_predicate_switch_and_move() {
let mut s = State::default();
let mut a = sample_window();
a.workspace = 0;
let mut b = sample_window();
b.workspace = 1;
{
let mut w = s.windows.lock_or_recover();
w.push(a);
w.push(b);
}
assert!(s.windows.lock_or_recover()[0].in_workspace(0));
assert!(!s.windows.lock_or_recover()[0].in_workspace(1));
// Powierzchnia warstwowa (panel) jest widoczna w każdej przestrzeni.
let mut panel = sample_window();
panel.layer = true;
panel.workspace = 3;
assert!(panel.in_workspace(0));
switch_workspace(&mut s, 1);
assert_eq!(*s.ws_current.lock_or_recover(), 1);
move_window_to_workspace(&mut s, 0, 2);
assert_eq!(s.windows.lock_or_recover()[0].workspace, 2);
// Numer przestrzeni jest klamrowany do zakresu.
switch_workspace(&mut s, 99);
assert_eq!(*s.ws_current.lock_or_recover(), s.ws_count - 1);
}
#[test]
fn window_controls_are_laid_out() {
let w = sample_window();
// Pasek tytułu na górze, treść pod nim.
assert_eq!(w.titlebar().bottom(), w.y + TITLEBAR);
assert_eq!(w.content().y(), w.y + TITLEBAR);
// Zamknij po prawej, wyśrodkowane w pionie, 6 px od krawędzi (układ HeaderBar).
let c = w.close_rect();
assert_eq!((c.width(), c.height()), (BUTTON_W, BUTTON_W));
assert!((c.center().y - (w.y + TITLEBAR / 2.0)).abs() < 0.01);
assert!((w.x + 200.0 - c.right() - 6.0).abs() < 0.01);
assert!(c.contains(c.center()));
// Maksymalizuj po lewej od zamknij, minimalizuj po lewej od maksymalizuj.
let m = w.maximize_rect();
let n = w.minimize_rect();
assert!(m.right() <= c.x() + 0.01);
assert!(n.right() <= m.x() + 0.01);
}
#[test]
fn resize_edges_detect_corners() {
let w = sample_window();
let o = w.outer();
// Lewy-górny narożnik → góra + lewo.
assert_eq!(
w.resize_edges(Point::new(o.x() + 1.0, o.y() + 1.0)),
EDGE_TOP | EDGE_LEFT
);
// Prawy-dolny narożnik → dół + prawo.
assert_eq!(
w.resize_edges(Point::new(o.right() - 1.0, o.bottom() - 1.0)),
EDGE_BOTTOM | EDGE_RIGHT
);
// Środek → brak krawędzi.
assert_eq!(w.resize_edges(o.center()), 0);
}
#[test]
fn fullscreen_hides_titlebar() {
let mut w = sample_window();
w.fullscreen = true;
assert_eq!(w.titlebar().bottom(), w.y);
assert_eq!(w.content().y(), w.y);
}
#[test]
fn minimize_and_restore_via_activation() {
// Żądanie `xdg_toplevel.set_minimized` musi naprawdę ukryć okno, a aktywacja
// (pasek/przełącznik) — przywrócić je. Wcześniej oba żądania były ignorowane.
let mut s = State::default();
s.windows.lock_or_recover().push(sample_window());
set_minimized(&mut s, 0, true);
assert!(s.windows.lock_or_recover()[0].minimized);
activate_slot(&mut s, 0);
assert!(!s.windows.lock_or_recover()[0].minimized);
}
#[test]
fn maximize_and_restore_geometry() {
let mut s = State::default();
s.windows.lock_or_recover().push(sample_window());
let before = {
let windows = s.windows.lock_or_recover();
let w = &windows[0];
(w.x, w.y, w.width, w.height)
};
set_maximized(&mut s, 0, true);
{
let windows = s.windows.lock_or_recover();
let w = &windows[0];
assert!(w.maximized);
assert!(w.saved.is_some());
}
set_maximized(&mut s, 0, false);
{
let windows = s.windows.lock_or_recover();
let w = &windows[0];
assert!(!w.maximized);
assert_eq!((w.x, w.y, w.width, w.height), before);
}
}
/// Dwa monitory obok siebie; panel rezerwuje 40 px **tylko na lewym**.
fn two_output_state() -> State {
let mut s = State::default();
{
let mut outs = s.outputs.lock_or_recover();
*outs = vec![
crate::monitors::OutputInfo {
name: "left".to_owned(),
width: 1920,
height: 1080,
refresh_hz: 60,
scale: 1,
x: 0,
y: 0,
},
crate::monitors::OutputInfo {
name: "right".to_owned(),
width: 1600,
height: 900,
refresh_hz: 60,
scale: 1,
x: 1920,
y: 0,
},
];
}
s.top_insets.insert(0, 40.0);
s
}
#[test]
fn maximize_below_panel_only_on_panel_output() {
let mut s = two_output_state();
s.windows.lock_or_recover().push(sample_window());
set_maximized(&mut s, 0, true);
let (y, h) = {
let ws = s.windows.lock_or_recover();
(ws[0].y, ws[0].height)
};
assert_eq!(y, 40.0);
assert_eq!(h, 1080 - 40 - TITLEBAR as i32);
}
#[test]
fn maximize_uses_full_height_on_output_without_panel() {
let mut s = two_output_state();
let mut w = sample_window();
w.x = 2000.0;
w.y = 100.0;
s.windows.lock_or_recover().push(w);
set_maximized(&mut s, 0, true);
let (x, y, width, height) = {
let ws = s.windows.lock_or_recover();
(ws[0].x, ws[0].y, ws[0].width, ws[0].height)
};
assert_eq!((x, y), (1920.0, 0.0));
assert_eq!(width, 1600);
assert_eq!(height, 900 - TITLEBAR as i32);
}
#[test]
fn fullscreen_drops_panel_gap_only_on_output_without_panel() {
let mut s = two_output_state();
let mut w = sample_window();
w.x = 2000.0;
w.y = 100.0;
s.windows.lock_or_recover().push(w);
set_fullscreen(&mut s, 0, true);
let (x, y, width, height) = {
let ws = s.windows.lock_or_recover();
(ws[0].x, ws[0].y, ws[0].width, ws[0].height)
};
assert_eq!((x, y, width, height), (1920.0, 0.0, 1600, 900));
}
#[test]
fn fullscreen_keeps_panel_gap_on_panel_output() {
let mut s = two_output_state();
s.windows.lock_or_recover().push(sample_window());
set_fullscreen(&mut s, 0, true);
let (y, height) = {
let ws = s.windows.lock_or_recover();
(ws[0].y, ws[0].height)
};
assert_eq!(y, 40.0);
assert_eq!(height, 1080 - 40);
}
#[test]
fn xdg_toplevel_states_are_encoded_as_u32_array() {
let bytes = encode_states(&[4, 1]);
assert_eq!(bytes.len(), 8, "każdy stan to 4 bajty (uint), nie 1");
let decoded: Vec<u32> = bytes
.chunks_exact(4)
.map(|c| u32::from_ne_bytes(c.try_into().unwrap()))
.collect();
assert_eq!(decoded, vec![4, 1]);
}
#[test]
fn keymap_defaults_to_us_layout() {
let km = keymap_text_for("us", None);
assert!(km.contains("xkb_keymap"));
assert!(km.contains("pc+us+inet(evdev)"));
}
#[test]
fn keymap_supports_variants_and_sanitizes_input() {
assert!(keymap_text_for("pl", Some("legacy")).contains("pc+pl(legacy)+inet(evdev)"));
// Znaki spoza zestawu XKB są odfiltrowane (bezpieczeństwo).
assert!(keymap_text_for("u; rm -rf", None).contains("pc+urm-rf+inet(evdev)"));
}
#[test]
fn empty_layout_falls_back_to_us() {
assert!(keymap_text_for("!!!", None).contains("pc+us+inet(evdev)"));
}
#[test]
fn snap_zones_cover_edges_and_corners() {
let area = Rect::from_xywh(0.0, 48.0, 1920.0, 1032.0);
assert_eq!(
snap_zone(Point::new(2.0, 500.0), area),
Some(SnapZone::Left)
);
assert_eq!(
snap_zone(Point::new(1918.0, 500.0), area),
Some(SnapZone::Right)
);
assert_eq!(
snap_zone(Point::new(960.0, 50.0), area),
Some(SnapZone::Maximize)
);
assert_eq!(
snap_zone(Point::new(2.0, 50.0), area),
Some(SnapZone::TopLeft)
);
assert_eq!(
snap_zone(Point::new(1918.0, 50.0), area),
Some(SnapZone::TopRight)
);
assert_eq!(
snap_zone(Point::new(2.0, 1078.0), area),
Some(SnapZone::BottomLeft)
);
assert_eq!(
snap_zone(Point::new(1918.0, 1078.0), area),
Some(SnapZone::BottomRight)
);
// Środek ekranu nie przyciąga.
assert_eq!(snap_zone(Point::new(960.0, 500.0), area), None);
}
#[test]
fn snap_rect_fills_halves_and_quarters() {
let area = Rect::from_xywh(0.0, 48.0, 1920.0, 1032.0);
let bar = TITLEBAR;
let (x, y, w, h) = snap_rect(SnapZone::Left, area, bar);
assert_eq!((x, y, w), (0.0, 48.0, 960.0));
assert_eq!(y + bar + h, area.bottom());
let (x, y, w, h) = snap_rect(SnapZone::TopRight, area, bar);
assert_eq!((x, y, w), (960.0, 48.0, 960.0));
assert_eq!(h, 1032.0 / 2.0 - TITLEBAR);
let (x, y, w, h) = snap_rect(SnapZone::BottomLeft, area, bar);
assert_eq!((x, y, w), (0.0, 48.0 + 516.0, 960.0));
assert_eq!(y + bar + h, area.bottom());
// Prawa połowa dokładnie dopełnia lewą.
let (rx, _, rw, _) = snap_rect(SnapZone::Right, area, bar);
assert_eq!(rx, 960.0);
assert_eq!(rw, 960.0);
}
#[test]
fn drag_clamp_keeps_titlebar_below_panel() {
let area = (0.0, 0.0, 1920.0, 1080.0);
let (x, y) = clamp_drag_pos(area, 48.0, 800.0, 100.0, -200.0);
assert_eq!((x, y), (100.0, 48.0));
// Nie da się wywieźć okna całkowicie za prawą krawędź.
let (x2, _) = clamp_drag_pos(area, 48.0, 800.0, 5000.0, 200.0);
assert_eq!(x2, 1920.0 - MIN_VISIBLE);
}
}