🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
//! Doklejanie okien do krawędzi i rogów ekranu („Aero Snap”).
//!
//! PO CO osobny moduł: to CZYSTA funkcja geometrii — `punkt kursora + obszar
//! wyjścia → strefa doklejania → docelowy prostokąt okna`. Nie zależy od Smithaya
//! ani od stanu kompozytora, więc można ją przetestować jednostkowo bez GPU
//! (patrz `#[cfg(test)]` na dole). Logika *kiedy* i *jak* przesłać to klientowi
//! siedzi w `grabs.rs` — tutaj jest tylko matematyka.
//!
//! Semantyka (jak w Windows Aero Snap / GNOME / KDE):
//!
//! ```text
//! ┌─────────┬─────────┐ krawędź lewa/prawa → połowa ekranu
//! │ TL │ TR │ krawędź górna → pełny ekran (maksymalizacja)
//! ├─────────┼─────────┤ róg (2 krawędzie) → ćwiartka ekranu
//! │ │ │ krawędź dolna → bez doklejania
//! │ BL │ BR │ (albo dolna połowa, gdy PAGAN_SNAP_BOTTOM=1)
//! └─────────┴─────────┘
//! ```
//!
//! „Stackowanie”: gdy okna doklejane są do przeciwległych połówek lub do rogów,
//! same układają się w kafelki (2 kolumny / siatka 2×2) — nie potrzebujemy do tego
//! żadnego menedżera kafelkowego, wystarczy, że każdy snap zajmuje inną strefę.
//!
//! Konfiguracja (zmienne środowiskowe, czytane raz przy starcie):
//! * `PAGAN_SNAP_THRESHOLD` — odległość od krawędzi włączająca doklejanie (px),
//! * `PAGAN_SNAP_BOTTOM=1` — włącza doklejanie do dolnej krawędzi (dolna połowa).
use smithay::utils::{Logical, Point, Rectangle};
/// Domyślna odległość kursora od krawędzi wyjścia, przy której włączamy
/// doklejanie (w pikselach logicznych). Wartość kompromisowa: na tyle duża, by
/// łatwo „trafić” krawędź myszką, i na tyle mała, by nie snapowało przy zwykłym
/// przesuwaniu okna po środku ekranu.
pub const SNAP_THRESHOLD: f64 = 12.0;
/// Konfiguracja doklejania (czytana ze środowiska przy starcie kompozytora).
#[derive(Debug, Clone, Copy)]
pub struct SnapConfig {
/// Odległość od krawędzi włączająca doklejanie.
pub threshold: f64,
/// Czy dolna krawędź ma doklejać (dolna połowa). Domyślnie `false`, jak w Windows.
pub bottom: bool,
}
impl Default for SnapConfig {
fn default() -> Self {
Self {
threshold: SNAP_THRESHOLD,
bottom: false,
}
}
}
impl SnapConfig {
/// Buduje konfigurację z `PAGAN_SNAP_THRESHOLD` i `PAGAN_SNAP_BOTTOM`.
///
/// PO CO z env, a nie na sztywno: ergonomia myszy jest subiektywna, a to
/// pozwala dostroić próg bez rekompilacji (spójne z `PAGAN_SCALE` itd.).
pub fn from_env() -> Self {
let threshold = std::env::var("PAGAN_SNAP_THRESHOLD")
.ok()
.and_then(|value| value.trim().parse::<f64>().ok())
.filter(|value| *value > 0.0)
.unwrap_or(SNAP_THRESHOLD);
let bottom = matches!(
std::env::var("PAGAN_SNAP_BOTTOM").as_deref(),
Ok("1") | Ok("true") | Ok("yes") | Ok("on")
);
Self { threshold, bottom }
}
}
/// Strefa doklejania wyznaczona z pozycji kursora.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SnapZone {
/// Lewa krawędź → lewa połowa.
Left,
/// Prawa krawędź → prawa połowa.
Right,
/// Górna krawędź (bez rogów) → pełny ekran.
Top,
/// Dolna krawędź (tylko gdy włączona w konfiguracji) → dolna połowa.
Bottom,
/// Lewy górny róg → lewa górna ćwiartka.
TopLeft,
/// Prawy górny róg → prawa górna ćwiartka.
TopRight,
/// Lewy dolny róg → lewa dolna ćwiartka.
BottomLeft,
/// Prawy dolny róg → prawa dolna ćwiartka.
BottomRight,
}
/// Wyznacza strefę doklejania dla kursora w `pointer`.
///
/// Zwraca `None`, gdy kursor jest poza obszarem wyjścia albo w jego wnętrzu
/// (dalej niż próg od każdej istotnej krawędzi). Dolna krawędź celowo **nie**
/// snapuje domyślnie (jak w Aero Snap) — doklejanie do dołu bywa mylące.
pub fn snap_zone(
pointer: Point<f64, Logical>,
output: Rectangle<i32, Logical>,
config: SnapConfig,
) -> Option<SnapZone> {
// Kursor poza wyjściem → brak doklejania (nie „przyciągamy” z drugiego monitora).
if !output.to_f64().contains(pointer) {
return None;
}
let x0 = output.loc.x as f64;
let y0 = output.loc.y as f64;
let x1 = x0 + output.size.w as f64;
let y1 = y0 + output.size.h as f64;
let t = config.threshold;
let at_left = pointer.x - x0 <= t;
let at_right = x1 - pointer.x <= t;
let at_top = pointer.y - y0 <= t;
let at_bottom = y1 - pointer.y <= t;
match (at_left, at_right, at_top, at_bottom) {
// Rogi mają priorytet nad pojedynczą krawędzią.
(true, _, true, _) => Some(SnapZone::TopLeft),
(true, _, _, true) => Some(SnapZone::BottomLeft),
(_, true, true, _) => Some(SnapZone::TopRight),
(_, true, _, true) => Some(SnapZone::BottomRight),
(true, _, _, _) => Some(SnapZone::Left),
(_, true, _, _) => Some(SnapZone::Right),
(_, _, true, _) => Some(SnapZone::Top),
// Dolna krawędź: tylko gdy użytkownik ją włączył.
(_, _, _, true) if config.bottom => Some(SnapZone::Bottom),
// Dolna krawędź bez wsparcia i środek: brak doklejania.
_ => None,
}
}
/// Docelowa geometria okna dla danej strefy, w współrzędnych przestrzeni
/// logicznej (loc może być różny od zera dla drugiego monitora w układzie).
///
/// Dzielenie `size / 2` używamy konsekwentnie dla obu połówek, a drugą połowę
/// wyrównujemy do prawej/dolnej krawędzi (`w - half`), żeby przy nieparzystej
/// szerokości nie powstała jednopikselowa szczelina między kafelkami.
pub fn snap_geometry(zone: SnapZone, output: Rectangle<i32, Logical>) -> Rectangle<i32, Logical> {
let x = output.loc.x;
let y = output.loc.y;
let w = output.size.w;
let h = output.size.h;
let half_w = w / 2;
let half_h = h / 2;
match zone {
SnapZone::Left => Rectangle::new((x, y).into(), (half_w, h).into()),
SnapZone::Right => Rectangle::new((x + half_w, y).into(), (w - half_w, h).into()),
// „Górna krawędź” = zmaksymalizowane okno na całym wyjściu.
SnapZone::Top => Rectangle::new((x, y).into(), (w, h).into()),
// Dolna krawędź (gdy włączona) = dolna połowa.
SnapZone::Bottom => Rectangle::new((x, y + half_h).into(), (w, h - half_h).into()),
SnapZone::TopLeft => Rectangle::new((x, y).into(), (half_w, half_h).into()),
SnapZone::TopRight => Rectangle::new((x + half_w, y).into(), (w - half_w, half_h).into()),
SnapZone::BottomLeft => Rectangle::new((x, y + half_h).into(), (half_w, h - half_h).into()),
SnapZone::BottomRight => Rectangle::new(
(x + half_w, y + half_h).into(),
(w - half_w, h - half_h).into(),
),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn output() -> Rectangle<i32, Logical> {
// Typowe wyjście Full HD w punkcie (0,0).
Rectangle::new((0, 0).into(), (1920, 1080).into())
}
fn p(x: f64, y: f64) -> Point<f64, Logical> {
Point::from((x, y))
}
fn cfg() -> SnapConfig {
SnapConfig::default()
}
#[test]
fn srodek_ekranu_nie_snapuje() {
assert_eq!(snap_zone(p(960.0, 540.0), output(), cfg()), None);
}
#[test]
fn dolna_krawedz_nie_snapuje_domyslnie() {
// Środek dolnej krawędzi — domyślnie bez doklejania (jak w Windows).
assert_eq!(snap_zone(p(960.0, 1079.0), output(), cfg()), None);
}
#[test]
fn dolna_krawedz_snapuje_gdy_wlaczona() {
let config = SnapConfig {
threshold: SNAP_THRESHOLD,
bottom: true,
};
assert_eq!(
snap_zone(p(960.0, 1079.0), output(), config),
Some(SnapZone::Bottom)
);
// Geometria: dolna połowa 540..1080.
assert_eq!(
snap_geometry(SnapZone::Bottom, output()),
Rectangle::new((0, 540).into(), (1920, 540).into())
);
}
#[test]
fn prog_jest_konfigurowalny() {
// Przy bardzo małym progu punkt 3 px od krawędzi już nie snapuje.
let strict = SnapConfig {
threshold: 1.0,
bottom: false,
};
assert_eq!(snap_zone(p(3.0, 540.0), output(), strict), None);
// A przy dużym progu snapuje nawet 20 px od krawędzi.
let loose = SnapConfig {
threshold: 25.0,
bottom: false,
};
assert_eq!(
snap_zone(p(20.0, 540.0), output(), loose),
Some(SnapZone::Left)
);
}
#[test]
fn krawedzie_daja_polowki_i_pelny_ekran() {
assert_eq!(
snap_zone(p(3.0, 540.0), output(), cfg()),
Some(SnapZone::Left)
);
assert_eq!(
snap_zone(p(1917.0, 540.0), output(), cfg()),
Some(SnapZone::Right)
);
assert_eq!(
snap_zone(p(960.0, 3.0), output(), cfg()),
Some(SnapZone::Top)
);
}
#[test]
fn rogi_maja_priorytet() {
assert_eq!(
snap_zone(p(3.0, 3.0), output(), cfg()),
Some(SnapZone::TopLeft)
);
assert_eq!(
snap_zone(p(1917.0, 3.0), output(), cfg()),
Some(SnapZone::TopRight)
);
assert_eq!(
snap_zone(p(3.0, 1077.0), output(), cfg()),
Some(SnapZone::BottomLeft)
);
assert_eq!(
snap_zone(p(1917.0, 1077.0), output(), cfg()),
Some(SnapZone::BottomRight)
);
}
#[test]
fn kursor_poza_wyjściem_nie_snapuje() {
assert_eq!(snap_zone(p(1925.0, 540.0), output(), cfg()), None);
assert_eq!(snap_zone(p(-5.0, 540.0), output(), cfg()), None);
}
#[test]
fn geometria_polowek_i_cwiartek() {
let out = output();
// Lewa połowa: 0..960.
assert_eq!(
snap_geometry(SnapZone::Left, out),
Rectangle::new((0, 0).into(), (960, 1080).into())
);
// Prawa połowa: 960..1920 (szerokość = 1920-960 = 960).
assert_eq!(
snap_geometry(SnapZone::Right, out),
Rectangle::new((960, 0).into(), (960, 1080).into())
);
// Górna krawędź = pełny ekran.
assert_eq!(
snap_geometry(SnapZone::Top, out),
Rectangle::new((0, 0).into(), (1920, 1080).into())
);
// Ćwiartki.
assert_eq!(
snap_geometry(SnapZone::TopLeft, out),
Rectangle::new((0, 0).into(), (960, 540).into())
);
assert_eq!(
snap_geometry(SnapZone::BottomRight, out),
Rectangle::new((960, 540).into(), (960, 540).into())
);
}
#[test]
fn nieparzysty_rozmiar_bez_szczeliny() {
// Szerokość 1921: połowy 960 i 961, suma = 1921 (brak szczeliny).
let out = Rectangle::new((0, 0).into(), (1921, 1080).into());
let left = snap_geometry(SnapZone::Left, out);
let right = snap_geometry(SnapZone::Right, out);
assert_eq!(left.size.w + right.size.w, 1921);
assert_eq!(left.loc.x + left.size.w, right.loc.x);
}
#[test]
fn dziala_na_drugim_monitorze() {
// Wyjście przesunięte w prawo — geometrie muszą respektować offset.
let out = Rectangle::new((1920, 0).into(), (1920, 1080).into());
assert_eq!(
snap_zone(p(1923.0, 540.0), out, cfg()),
Some(SnapZone::Left)
);
assert_eq!(
snap_geometry(SnapZone::Left, out),
Rectangle::new((1920, 0).into(), (960, 1080).into())
);
assert_eq!(
snap_geometry(SnapZone::TopRight, out),
Rectangle::new((2880, 0).into(), (960, 540).into())
);
}
}