🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
//! Pozycjonowanie `xdg_popup` zgodnie z `xdg_positioner`.
//!
//! Wcześniej liczyliśmy tylko „anchor + offset”, ignorując krawędź zakotwiczenia,
//! kierunek rozwoju (`gravity`) i `constraint_adjustment`. Efekt: menu i palety poleceń
//! (Zed, VS Code, GTK) potrafiły wylądować obok miejsca, w które użytkownik klikał —
//! wyglądało to jak „nieklikalna treść”. Tutaj jest pełny algorytm z xdg-shell:
//! punkt zakotwiczenia → grawitacja → przesunięcie → flip/slide w obszarze roboczym.
use libluna_core::Rect;
use crate::shell::PopupGeom;
/// `xdg_positioner.constraint_adjustment`: przesuń w bok, żeby zmieścić popup.
pub const ADJUST_SLIDE_X: u32 = 1;
/// Przesuń w pionie.
pub const ADJUST_SLIDE_Y: u32 = 2;
/// Odbij w poziomie na drugą stronę kotwicy.
pub const ADJUST_FLIP_X: u32 = 4;
/// Odbij w pionie.
pub const ADJUST_FLIP_Y: u32 = 8;
// Wartości enuma `xdg_positioner.anchor` / `.gravity` (0–8, nie bitmaski).
#[cfg(test)]
const NONE: u8 = 0;
const TOP: u8 = 1;
const BOTTOM: u8 = 2;
const LEFT: u8 = 3;
const RIGHT: u8 = 4;
const TOP_LEFT: u8 = 5;
const BOTTOM_LEFT: u8 = 6;
const TOP_RIGHT: u8 = 7;
const BOTTOM_RIGHT: u8 = 8;
/// Oblicza lewy-górny róg **widocznego** popupu w układzie bezwzględnym.
///
/// * `geom` — stan `xdg_positioner` (kotwica względem geometrii okna rodzica),
/// * `parent` — prostokąt treści rodzica (bezwzględny),
/// * `work` — obszar roboczy monitora (bez panelu),
/// * `fallback` — rozmiar popupu, gdy positioner nie podał go w `set_size`.
pub fn place(geom: &PopupGeom, parent: Rect, work: Rect, fallback: (f64, f64)) -> (f64, f64) {
let size = (
if geom.width > 0 {
geom.width as f64
} else {
fallback.0
},
if geom.height > 0 {
geom.height as f64
} else {
fallback.1
},
);
let anchor_rect = Rect::from_xywh(
parent.x() + geom.anchor_x,
parent.y() + geom.anchor_y,
geom.anchor_w,
geom.anchor_h,
);
let offset = (geom.offset_x, geom.offset_y);
let (mut x, mut y) = raw_position(anchor_rect, geom.anchor, geom.gravity, offset, size);
// Kolejność jak w specyfikacji: flip_x, flip_y, slide_x, slide_y.
let outside_x = |x: f64| x < work.x() - 0.5 || x + size.0 > work.right() + 0.5;
let outside_y = |y: f64| y < work.y() - 0.5 || y + size.1 > work.bottom() + 0.5;
if outside_x(x) && geom.constraint & ADJUST_FLIP_X != 0 {
let (fx, _) = raw_position(
anchor_rect,
flip_x(geom.anchor),
flip_x(geom.gravity),
offset,
size,
);
x = fx;
}
if outside_y(y) && geom.constraint & ADJUST_FLIP_Y != 0 {
let (_, fy) = raw_position(
anchor_rect,
flip_y(geom.anchor),
flip_y(geom.gravity),
offset,
size,
);
y = fy;
}
if geom.constraint & ADJUST_SLIDE_X != 0 {
x = clamp_axis(x, size.0, work.x(), work.right());
}
if geom.constraint & ADJUST_SLIDE_Y != 0 {
y = clamp_axis(y, size.1, work.y(), work.bottom());
}
(x, y)
}
/// Pozycja bez uwzględnienia ograniczeń monitora.
fn raw_position(
anchor_rect: Rect,
anchor: u8,
gravity: u8,
offset: (f64, f64),
size: (f64, f64),
) -> (f64, f64) {
let (ax, ay) = anchor_point(anchor_rect, anchor);
let (gx, gy) = gravity_offset(gravity, size);
(ax + offset.0 + gx, ay + offset.1 + gy)
}
/// Punkt na krawędzi/narożniku prostokąta kotwicy (`none` = środek).
fn anchor_point(r: Rect, anchor: u8) -> (f64, f64) {
let x = match anchor {
LEFT | TOP_LEFT | BOTTOM_LEFT => r.x(),
RIGHT | TOP_RIGHT | BOTTOM_RIGHT => r.right(),
_ => r.center().x,
};
let y = match anchor {
TOP | TOP_LEFT | TOP_RIGHT => r.y(),
BOTTOM | BOTTOM_LEFT | BOTTOM_RIGHT => r.bottom(),
_ => r.center().y,
};
(x, y)
}
/// Przesunięcie popupu względem punktu kotwicy (kierunek „rozwoju”).
fn gravity_offset(gravity: u8, size: (f64, f64)) -> (f64, f64) {
let x = match gravity {
LEFT | TOP_LEFT | BOTTOM_LEFT => -size.0,
RIGHT | TOP_RIGHT | BOTTOM_RIGHT => 0.0,
_ => -size.0 / 2.0,
};
let y = match gravity {
TOP | TOP_LEFT | TOP_RIGHT => -size.1,
BOTTOM | BOTTOM_LEFT | BOTTOM_RIGHT => 0.0,
_ => -size.1 / 2.0,
};
(x, y)
}
fn flip_x(bits: u8) -> u8 {
match bits {
LEFT => RIGHT,
RIGHT => LEFT,
TOP_LEFT => TOP_RIGHT,
TOP_RIGHT => TOP_LEFT,
BOTTOM_LEFT => BOTTOM_RIGHT,
BOTTOM_RIGHT => BOTTOM_LEFT,
other => other,
}
}
fn flip_y(bits: u8) -> u8 {
match bits {
TOP => BOTTOM,
BOTTOM => TOP,
TOP_LEFT => BOTTOM_LEFT,
BOTTOM_LEFT => TOP_LEFT,
TOP_RIGHT => BOTTOM_RIGHT,
BOTTOM_RIGHT => TOP_RIGHT,
other => other,
}
}
/// Wsuwa popup w obszar; gdy jest większy niż obszar, wyrównuje do początku.
fn clamp_axis(pos: f64, size: f64, start: f64, end: f64) -> f64 {
let max = (end - size).max(start);
pos.clamp(start, max)
}
#[cfg(test)]
mod tests {
use super::*;
fn geom(anchor: u8, gravity: u8) -> PopupGeom {
PopupGeom {
width: 200,
height: 100,
anchor_x: 100.0,
anchor_y: 200.0,
anchor_w: 50.0,
anchor_h: 30.0,
offset_x: 0.0,
offset_y: 0.0,
anchor,
gravity,
constraint: 0,
}
}
fn parent() -> Rect {
Rect::from_xywh(0.0, 0.0, 800.0, 600.0)
}
fn work() -> Rect {
Rect::from_xywh(0.0, 0.0, 1920.0, 1080.0)
}
#[test]
fn center_anchor_and_gravity_center_on_anchor() {
let g = geom(NONE, NONE);
// Kotwica: (100,200)+(50x30) → środek (125,215); popup 200x100 → lewy-górny (25,165).
assert_eq!(place(&g, parent(), work(), (200.0, 100.0)), (25.0, 165.0));
}
#[test]
fn bottom_anchor_with_bottom_gravity_opens_below() {
// GTK/Qt domyślnie: kotwica na dolnej krawędzi, popup rośnie w dół.
let g = geom(BOTTOM, BOTTOM);
// Punkt kotwicy: (środek x = 125, dolna krawędź = 230); popup 200 px → x = -75? nie:
// grawitacja „dół” nie przesuwa w poziomie, ale brak kotwicy poziomej = środek.
assert_eq!(
place(&g, parent(), work(), (200.0, 100.0)),
(125.0 - 100.0, 230.0)
);
}
#[test]
fn offset_is_applied_after_gravity() {
let mut g = geom(BOTTOM, BOTTOM);
g.offset_x = 7.0;
g.offset_y = -3.0;
assert_eq!(place(&g, parent(), work(), (200.0, 100.0)), (32.0, 227.0));
}
#[test]
fn flip_y_puts_popup_above_when_below_does_not_fit() {
// Rodzic nisko: dolna krawędź kotwicy na 1030, popup 100 px nie mieści się.
let mut g = geom(BOTTOM, BOTTOM);
g.anchor_y = 1000.0;
g.constraint = ADJUST_FLIP_Y;
let parent = Rect::from_xywh(0.0, 0.0, 800.0, 1080.0);
let (_, y) = place(&g, parent, work(), (200.0, 100.0));
// Po odbiciu kotwica jest na górnej krawędzi (1000), popup rośnie w górę.
assert_eq!(y, 900.0);
}
#[test]
fn slide_x_keeps_popup_inside_output() {
let mut g = geom(NONE, NONE);
g.anchor_x = 1800.0;
g.constraint = ADJUST_SLIDE_X;
let (x, _) = place(&g, parent(), work(), (200.0, 100.0));
assert_eq!(x, 1920.0 - 200.0);
}
#[test]
fn oversized_popup_is_aligned_to_work_area_start() {
let mut g = geom(NONE, NONE);
g.width = 4000;
g.height = 4000;
g.constraint = ADJUST_SLIDE_X | ADJUST_SLIDE_Y;
let (x, y) = place(&g, parent(), work(), (4000.0, 2000.0));
assert_eq!((x, y), (0.0, 0.0));
}
#[test]
fn corner_anchor_matches_bottom_right() {
let mut g = geom(BOTTOM_RIGHT, TOP_LEFT);
g.anchor_x = 300.0;
// Kotwica (350,230); grawitacja w lewo/górę — popup kończy się w tym punkcie.
assert_eq!(place(&g, parent(), work(), (200.0, 100.0)), (150.0, 130.0));
}
}