🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
//! Logika okna: model, hit-testing, hover oraz renderowanie Cairo do bufora SHM.
use cairo::{Context, Format, ImageSurface};
use memmap2::MmapMut;
use wayland_protocols::xdg::shell::client::xdg_toplevel::ResizeEdge;
use crate::draw;
use crate::i18n::Locale;
/// Wspólna oś Y środka WSZYSTKICH kontrolek paska tytułu (Menu i przycisków okna).
///
/// To jest jedyne miejsce, w którym definiujemy tę wartość. Dzięki temu „Menu" po
/// lewej i Zamknij/Minimalizuj/Maksymalizuj po prawej leżą idealnie na tej samej
/// wysokości — wystarczy zmienić jedną stałą.
pub const TITLEBAR_CONTROL_CENTER_Y: f64 = 20.0;
/// Domyślna wysokość paska tytułu w pikselach.
pub const TITLEBAR_HEIGHT: i32 = 40;
/// Domyślny promień zaokrąglenia rogów okna w pikselach.
pub const CORNER_RADIUS: f64 = 12.0;
/// Promień okręgu przycisku.
pub const CONTROL_RADIUS: f64 = 8.0;
/// Odstęp między środkami sąsiednich przycisków po prawej stronie.
pub const CONTROL_SPACING: f64 = 32.0;
/// Poziomy środek przycisku „Menu" (liczony od lewej krawędzi).
pub const MENU_CENTER_X: f64 = 20.0;
/// Odstęp prawej krawędzi okna od środka ostatniego przycisku (Zamknij).
pub const WINDOW_CONTROLS_RIGHT_MARGIN: f64 = 20.0;
// --- Rozwinięte menu aplikacji (rysowane przez klienta) -----------------------
/// Rozmiar czcionki pozycji menu (px).
pub const MENU_FONT_SIZE: f64 = 13.0;
/// Wysokość jednej pozycji menu (px).
pub const MENU_ITEM_HEIGHT: i32 = 26;
/// Wewnętrzny odstęp poziomy pozycji menu (z każdej strony).
pub const MENU_ITEM_PAD_X: i32 = 14;
/// Wewnętrzny odstęp górny/dolny panelu menu (px).
pub const MENU_PANEL_PAD_Y: i32 = 6;
/// Minimalna szerokość panelu menu (px).
pub const MENU_PANEL_MIN_WIDTH: i32 = 170;
/// Rozwinięte menu aplikacji — panel wewnątrz powierzchni klienta.
///
/// PO CO wewnątrz powierzchni, a nie jako `xdg_popup`: wynik widać od razu,
/// bez zależności od tego, czy kompozytor rysuje popupy. Panel pojawia się
/// dokładnie pod etykietą klikniętą w pasku SSD (jej pozycję przysyła kompozytor).
#[derive(Debug, Clone, PartialEq)]
pub struct OpenMenu {
/// Indeks tytułu górnego poziomu (np. 0 = „Plik”).
pub title_index: usize,
/// Lewa krawędź panelu w układzie powierzchni (px).
pub x: i32,
/// Szerokość panelu (wyliczona z najdłuższej pozycji).
pub width: i32,
/// Pozycje menu.
pub items: Vec<String>,
/// Pozycja podświetlona kursorem.
pub hover: Option<usize>,
}
impl OpenMenu {
/// Prostokąt panelu `(x, y, szerokość, wysokość)` w układzie powierzchni.
pub fn panel_rect(&self) -> (i32, i32, i32, i32) {
let height = self.items.len() as i32 * MENU_ITEM_HEIGHT + MENU_PANEL_PAD_Y * 2;
(self.x, 0, self.width, height)
}
/// Prostokąt pozycji o danym indeksie.
pub fn item_rect(&self, index: usize) -> (i32, i32, i32, i32) {
let (x, y, width, _) = self.panel_rect();
let top = y + MENU_PANEL_PAD_Y + index as i32 * MENU_ITEM_HEIGHT;
(x, top, width, MENU_ITEM_HEIGHT)
}
/// Indeks pozycji pod punktem (jeśli trafiona).
pub fn item_at(&self, px: i32, py: i32) -> Option<usize> {
for index in 0..self.items.len() {
let (x, y, w, h) = self.item_rect(index);
if px >= x && px < x + w && py >= y && py < y + h {
return Some(index);
}
}
None
}
/// Czy punkt leży w obrębie panelu (także w odstępie między pozycjami).
pub fn contains(&self, px: i32, py: i32) -> bool {
let (x, y, w, h) = self.panel_rect();
px >= x && px < x + w && py >= y && py < y + h
}
}
/// Szerokość strefy przy krawędziach, w której klik rozpoczyna resize.
pub const RESIZE_BORDER: i32 = 8;
/// Identyfikator klikalnej kontrolki okna.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Control {
/// Przycisk menu (lewa strona paska tytułu).
Menu,
/// Minimalizacja.
Minimize,
/// Maksymalizacja / przywrócenie.
Maximize,
/// Zamknięcie.
Close,
}
/// Akcja wynikająca z kliknięcia w powierzchnię klienta.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WindowAction {
/// Nic się nie stało.
None,
/// Użytkownik kliknął „Zamknij".
Close,
/// Użytkownik kliknął „Minimalizuj".
Minimize,
/// Użytkownik kliknął „Maksymalizuj".
Maximize,
/// Użytkownik chwycił pasek tytułu → okno należy przeciągnąć.
StartMove,
/// Użytkownik kliknął „Menu".
ShowMenu,
/// Użytkownik wybrał pozycję rozwiniętego menu aplikacji (indeks w rozwinięciu).
MenuItem(usize),
}
/// Zbiór krawędzi okna, przy których trwa interaktywny resize.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Edges {
/// Górna krawędź.
pub top: bool,
/// Dolna krawędź.
pub bottom: bool,
/// Lewa krawędź.
pub left: bool,
/// Prawa krawędź.
pub right: bool,
}
impl Edges {
/// Czy zaznaczona jest chociaż jedna krawędź.
pub fn any(&self) -> bool {
self.top || self.bottom || self.left || self.right
}
/// Tłumaczy kombinację krawędzi na wartość protokołu xdg-shell.
///
/// `ResizeEdge` jest zwykłym `enum` (nie bitflagami), więc kombinacje
/// budujemy jawnie. Kolejność `match` pokrywa wszystkie 8 sensownych przypadków;
/// każdy inny (np. sprzeczny top+bottom) degraduje się do [`ResizeEdge::None`].
pub fn to_xdg_resize_edge(&self) -> ResizeEdge {
use ResizeEdge::*;
match (self.top, self.bottom, self.left, self.right) {
(true, false, true, false) => TopLeft,
(true, false, false, false) => Top,
(true, false, false, true) => TopRight,
(false, false, true, false) => Left,
(false, false, false, true) => Right,
(false, true, true, false) => BottomLeft,
(false, true, false, false) => Bottom,
(false, true, false, true) => BottomRight,
_ => None,
}
}
}
/// Nasze okno: rozmiar, wygląd oraz bieżący stan hover.
///
/// Struktura jest czysto „modelowa" — nie ma w niej żadnych obiektów Waylanda ani
/// Cairo. Dzięki temu można ją testować jednostkowo i rysować do dowolnego bufora.
pub struct MyToolkitWindow {
/// Szerokość powierzchni w pikselach.
pub width: i32,
/// Wysokość powierzchni w pikselach.
pub height: i32,
/// Wysokość paska tytułu w pikselach.
pub titlebar_height: i32,
/// Promień zaokrąglenia rogów okna.
pub corner_radius: f64,
/// Tytuł okna (używany też w `xdg_toplevel.set_title`).
pub title: String,
/// Język interfejsu.
pub locale: Locale,
/// Aktualnie podświetlona kontrolka (stan hover) — prywatne, bo wyliczane z pozycji.
hover: Option<Control>,
/// Czy kompozytor rysuje dekoracje serwerowe (SSD).
///
/// Gdy `true`, klient NIE rysuje własnego paska tytułu ani przycisków okna —
/// robi to kompozytor. Zapobiega to podwójnemu paskowi.
pub ssd: bool,
/// Rozwinięte menu aplikacji (jeśli otwarte).
menu: Option<OpenMenu>,
}
impl MyToolkitWindow {
/// Tworzy okno o zadanym rozmiarze i tytule; język bierze z `PAGAN_LANG`.
pub fn new(width: i32, height: i32, title: impl Into<String>) -> Self {
Self::with_locale(width, height, title, Locale::from_env())
}
/// Tworzy okno z jawnie podanym językiem (przydatne w testach i osadzaniu).
pub fn with_locale(width: i32, height: i32, title: impl Into<String>, locale: Locale) -> Self {
Self {
width,
height,
titlebar_height: TITLEBAR_HEIGHT,
corner_radius: CORNER_RADIUS,
title: title.into(),
locale,
hover: None,
ssd: false,
menu: None,
}
}
/// Otwiera rozwinięte menu aplikacji pod etykietą o pozycji `x`.
///
/// PO CO liczymy szerokość tutaj: panel ma objąć najdłuższą pozycję, a ten
/// sam prostokąt służy potem do rysowania I do trafień myszy.
pub fn open_menu(&mut self, title_index: usize, x: i32, items: Vec<String>) {
if items.is_empty() {
self.menu = None;
return;
}
let text_width = items
.iter()
.map(|item| draw::measure_text(item, MENU_FONT_SIZE).unwrap_or(0.0))
.fold(0.0_f64, f64::max);
let width = (text_width.ceil() as i32 + MENU_ITEM_PAD_X * 2).max(MENU_PANEL_MIN_WIDTH);
// Panel nie może wyjść za prawą krawędź okna.
let x = x.min((self.width - width - 4).max(0));
self.menu = Some(OpenMenu {
title_index,
x,
width,
items,
hover: None,
});
}
/// Zamyka rozwinięte menu. Zwraca `true`, jeśli coś było otwarte.
pub fn close_menu(&mut self) -> bool {
self.menu.take().is_some()
}
/// Czy rozwinięte menu jest otwarte.
pub fn menu_is_open(&self) -> bool {
self.menu.is_some()
}
/// Włącza/wyłącza tryb dekoracji serwerowych (SSD).
///
/// Wołane po zdarzeniu `zxdg_toplevel_decoration_v1.configure`: gdy kompozytor
/// wymusza SSD, klient musi zrezygnować z własnego paska tytułu.
pub fn set_ssd(&mut self, ssd: bool) {
if self.ssd != ssd {
self.ssd = ssd;
self.hover = None;
self.menu = None;
}
}
/// Ustawia nowy rozmiar powierzchni (np. po `xdg_toplevel.configure`).
///
/// Zerowanie hover jest istotne: po zmianie rozmiaru przyciski przesuwają się
/// poziomo, więc zapamiętana kontrolka mogłaby wskazywać na złe miejsce.
pub fn resize(&mut self, width: i32, height: i32) {
self.width = width;
self.height = height;
self.hover = None;
// Rozwinięte menu zależało od szerokości okna — po zmianie rozmiaru
// jego pozycja byłaby nieaktualna.
self.menu = None;
}
/// Zwraca środek podanej kontrolki w układzie lokalnym powierzchni.
fn control_center(&self, control: Control) -> (f64, f64) {
// Środek prawego skraju zestawu przycisków = przycisk „Zamknij".
let close_x = self.width as f64 - WINDOW_CONTROLS_RIGHT_MARGIN;
let maximize_x = close_x - CONTROL_SPACING;
let minimize_x = close_x - CONTROL_SPACING * 2.0;
let y = TITLEBAR_CONTROL_CENTER_Y;
match control {
Control::Menu => (MENU_CENTER_X, y),
Control::Minimize => (minimize_x, y),
Control::Maximize => (maximize_x, y),
Control::Close => (close_x, y),
}
}
/// Zwraca przycisk, na którym znajduje się punkt (jeśli którykolwiek).
///
/// Promień trafienia jest o 2 px większy niż promień rysowania — drobny
/// „luz" znacząco poprawia ergonomię klikania małych okrągłych przycisków.
fn hit_control(&self, x: i32, y: i32) -> Option<Control> {
let hit_radius = CONTROL_RADIUS + 2.0;
let hit_radius_sq = hit_radius * hit_radius;
let (xf, yf) = (x as f64, y as f64);
for control in [
Control::Menu,
Control::Minimize,
Control::Maximize,
Control::Close,
] {
let (cx, cy) = self.control_center(control);
let dx = xf - cx;
let dy = yf - cy;
if dx * dx + dy * dy <= hit_radius_sq {
return Some(control);
}
}
None
}
/// Czy punkt leży wewnątrz powierzchni.
fn is_inside(&self, x: i32, y: i32) -> bool {
x >= 0 && y >= 0 && x < self.width && y < self.height
}
/// Rysuje CAŁE okno do bufora pamięci współdzielonej.
///
/// `mmap` musi mieć co najmniej `width * height * 4` bajtów (ARGB32).
///
/// API-NOTE: powierzchnia Cairo tworzona przez `create_for_data_unsafe` NIE może
/// przeżyć danych, na których została utworzona. Dlatego trzymamy ją lokalnie,
/// kończymy rysowanie i wołamy `surface.flush()` zanim `mmap` wróci do wołającego.
pub fn render_to_buffer(&self, mmap: &mut MmapMut) {
if self.width <= 0 || self.height <= 0 {
tracing::error!(
width = self.width,
height = self.height,
"pomijam render: niepoprawny rozmiar okna"
);
return;
}
// ARGB32 = dokładnie 4 bajty na piksel (premultiplied).
let stride = match self.width.checked_mul(4) {
Some(s) if s > 0 => s,
_ => {
tracing::error!(width = self.width, "pomijam render: przepełnienie stride");
return;
}
};
let required = match (stride as i64).checked_mul(self.height as i64) {
Some(n) if n > 0 => n as usize,
_ => {
tracing::error!("pomijam render: przepełnienie rozmiaru bufora");
return;
}
};
if mmap.len() < required {
tracing::error!(
available = mmap.len(),
required,
"pomijam render: bufor SHM jest za mały"
);
return;
}
// SAFETY: `mmap` żyje co najmniej do końca tej funkcji, ma co najmniej
// `stride * height` bajtów i jest wyrównany do strony (mapowanie mmap).
let surface = unsafe {
ImageSurface::create_for_data_unsafe(
mmap.as_mut_ptr(),
Format::ARgb32,
self.width,
self.height,
stride,
)
};
let surface = match surface {
Ok(surface) => surface,
Err(err) => {
tracing::error!(error = %err, "nie udało się utworzyć powierzchni Cairo na buforze SHM");
return;
}
};
let ctx = match Context::new(&surface) {
Ok(ctx) => ctx,
Err(err) => {
tracing::error!(error = %err, "nie udało się utworzyć kontekstu Cairo");
return;
}
};
if let Err(err) = self.draw(&ctx).and_then(|()| self.draw_menu(&ctx)) {
tracing::error!(error = %err, "błąd rysowania okna");
}
// Wypycha wszystkie operacje rysowania do pamięci bufora przed powrotem.
// `ImageSurface::flush()` nic nie zwraca (to `cairo_surface_flush`).
surface.flush();
}
/// Właściwa kolejność warstw rysowania okna.
fn draw(&self, ctx: &Context) -> Result<(), cairo::Error> {
// W trybie SSD pasek tytułu i przyciski rysuje kompozytor — klient
// rysuje tylko korpus oraz własny pasek menu.
if self.ssd {
return self.draw_ssd(ctx);
}
let width = self.width as f64;
let height = self.height as f64;
// 1. Wyzeruj bufor → przezroczystość poza zaokrągleniami.
draw::clear_transparent(ctx)?;
// 2. Pełne zaokrąglone tło (wszystkie cztery narożniki, promień 12 px).
draw::rounded_rectangle(ctx, 0.0, 0.0, width, height, self.corner_radius);
draw::set_color(ctx, draw::BODY_COLOR);
ctx.fill()?;
// 3. Pasek tytułu (ciemny, zaokrąglone górne rogi).
draw::draw_titlebar(ctx, width, self.titlebar_height as f64, self.corner_radius)?;
// 4. Kontrolki. Używamy DOKŁADNIE tej samej współrzędnej Y dla menu i przycisków —
// to wymóg wizualny, dlatego obie wartości pochodzą z jednej stałej.
let menu_y = TITLEBAR_CONTROL_CENTER_Y;
let buttons_y = TITLEBAR_CONTROL_CENTER_Y; // Ta sama wartość!
let (menu_x, _) = self.control_center(Control::Menu);
let menu_color = if self.hover == Some(Control::Menu) {
draw::highlight_color(draw::COLOR_MENU)
} else {
draw::COLOR_MENU
};
draw::draw_menu_button(ctx, menu_x, menu_y, CONTROL_RADIUS, menu_color)?;
// Trzy osobne okręgi po prawej: minimalizuj, maksymalizuj, zamknij.
let (right_start, _) = self.control_center(Control::Minimize);
draw::draw_window_controls(
ctx,
right_start,
buttons_y,
CONTROL_RADIUS,
CONTROL_SPACING,
self.hover == Some(Control::Close),
self.hover == Some(Control::Minimize),
self.hover == Some(Control::Maximize),
)?;
// 5. Tytuł okna, wyrównany do tej samej osi co kontrolki.
let text_x = menu_x + CONTROL_RADIUS + 12.0;
draw::draw_title_text(
ctx,
&self.title,
text_x,
TITLEBAR_CONTROL_CENTER_Y,
draw::TITLE_TEXT_COLOR,
)?;
Ok(())
}
/// Rysuje okno w trybie dekoracji serwerowych (SSD).
///
/// PO CO osobna ścieżka: w trybie SSD CAŁĄ dekorację (pasek tytułu, menu
/// aplikacji, przyciski okna) rysuje kompozytor. Klient nie może dołożyć
/// własnego paska — powstałyby dwa. Zostaje mu tylko korpus okna
/// (z zaokrąglonymi dolnymi rogami, bo górę zasłania ramka kompozytora).
fn draw_ssd(&self, ctx: &Context) -> Result<(), cairo::Error> {
let width = self.width as f64;
let height = self.height as f64;
draw::clear_transparent(ctx)?;
// Korpus: zaokrąglone tylko DOLNE rogi (górę zasłania pasek SSD
// o ostrych dolnych krawędziach — zaokrąglenie góry dałoby prześwit).
draw::rounded_bottom_rectangle(ctx, 0.0, 0.0, width, height, self.corner_radius);
draw::set_color(ctx, draw::BODY_COLOR);
ctx.fill()?;
Ok(())
}
/// Rysuje rozwinięte menu aplikacji (jeśli otwarte).
///
/// PO CO osobno od `draw`/`draw_ssd`: menu jest nakładką nad treścią okna
/// i wygląda tak samo niezależnie od tego, kto rysuje pasek tytułu (klient
/// czy kompozytor).
fn draw_menu(&self, ctx: &Context) -> Result<(), cairo::Error> {
let Some(menu) = &self.menu else {
return Ok(());
};
let (px, py, pw, ph) = menu.panel_rect();
let (x, y, w, h) = (px as f64, py as f64, pw as f64, ph as f64);
// Cień pod panelem — bez niego menu zlewa się z treścią okna.
draw::rounded_rectangle(ctx, x + 1.0, y + 3.0, w, h, 10.0);
ctx.set_source_rgba(0.0, 0.0, 0.0, 0.35);
ctx.fill()?;
// Tło panelu.
draw::rounded_rectangle(ctx, x, y, w, h, 10.0);
draw::set_color(ctx, draw::MENU_PANEL_COLOR);
ctx.fill()?;
// Delikatna obwódka w kolorze akcentu PaganDE.
draw::rounded_rectangle(ctx, x + 0.5, y + 0.5, w - 1.0, h - 1.0, 10.0);
ctx.set_source_rgba(0.31, 0.63, 1.0, 0.35);
ctx.set_line_width(1.0);
ctx.stroke()?;
// Pozycje menu.
for (index, item) in menu.items.iter().enumerate() {
let (ix, iy, iw, ih) = menu.item_rect(index);
if menu.hover == Some(index) {
draw::rounded_rectangle(
ctx,
ix as f64 + 4.0,
iy as f64 + 1.0,
(iw - 8) as f64,
(ih - 2) as f64,
6.0,
);
draw::set_color(ctx, draw::MENU_ITEM_HOVER_COLOR);
ctx.fill()?;
}
draw::draw_title_text(
ctx,
item,
ix as f64 + MENU_ITEM_PAD_X as f64,
iy as f64 + f64::from(ih) / 2.0,
draw::TITLE_TEXT_COLOR,
)?;
}
Ok(())
}
/// Hit-testing: zwraca akcję dla kliknięcia w punkcie `(x, y)`.
///
/// Kolejność ma znaczenie: najpierw przyciski (małe, precyzyjne cele), dopiero
/// potem „pusty" pasek tytułu, który rozpoczyna przeciąganie.
pub fn handle_mouse_click(&mut self, x: i32, y: i32) -> WindowAction {
// Rozwinięte menu jest nad wszystkim — obsługujemy je pierwsze.
// Kliknięcie poza pozycją (także poza panelem) po prostu zamyka menu.
if self.menu.is_some() {
let hit = self.menu.as_ref().and_then(|menu| menu.item_at(x, y));
self.menu = None;
return hit
.map(WindowAction::MenuItem)
.unwrap_or(WindowAction::None);
}
if !self.is_inside(x, y) {
return WindowAction::None;
}
// W trybie SSD całą dekorację rysuje kompozytor — klient nie ma tu
// żadnych własnych kontrolek (menu aplikacji jest w pasku kompozytora).
if self.ssd {
return WindowAction::None;
}
if let Some(control) = self.hit_control(x, y) {
return match control {
Control::Menu => WindowAction::ShowMenu,
Control::Close => WindowAction::Close,
Control::Minimize => WindowAction::Minimize,
Control::Maximize => WindowAction::Maximize,
};
}
// Klik w pasek tytułu poza przyciskami → przeciąganie okna.
if y < self.titlebar_height {
return WindowAction::StartMove;
}
WindowAction::None
}
/// Aktualizuje stan hover na podstawie ruchu kursora.
///
/// Zwraca `true`, gdy stan się zmienił — tylko wtedy trzeba przerysować okno,
/// co oszczędza pełne renderowanie Cairo przy każdym drobnym ruchu myszy.
pub fn handle_mouse_motion(&mut self, x: i32, y: i32) -> bool {
// Nad otwartym menu podświetlamy pozycję pod kursorem.
if let Some(menu) = self.menu.as_mut() {
let hover = menu.item_at(x, y);
if hover != menu.hover {
menu.hover = hover;
return true;
}
return false;
}
if self.ssd {
// W trybie SSD klient nie rysuje własnych kontrolek — nic nie podświetlamy.
if self.hover.take().is_some() {
return true;
}
return false;
}
let new_hover = if self.is_inside(x, y) {
self.hit_control(x, y)
} else {
None
};
if new_hover != self.hover {
self.hover = new_hover;
true
} else {
false
}
}
/// Zwraca krawędzie do resize, jeśli punkt leży na krawędzi okna; inaczej `None`.
///
/// Strefa łapania ma [`RESIZE_BORDER`] pikseli. Punkty poza powierzchnią są
/// ignorowane — resize wywołuje kompozytor dopiero po kliknięciu w naszym oknie.
pub fn resize_edge_at(&self, x: i32, y: i32) -> Option<Edges> {
if !self.is_inside(x, y) {
return None;
}
let mut edges = Edges::default();
if y < RESIZE_BORDER {
edges.top = true;
}
if y >= self.height - RESIZE_BORDER {
edges.bottom = true;
}
if x < RESIZE_BORDER {
edges.left = true;
}
if x >= self.width - RESIZE_BORDER {
edges.right = true;
}
if edges.any() {
Some(edges)
} else {
None
}
}
/// Czy kontrolka jest aktualnie podświetlona (do tooltipów/logów w aplikacji).
pub fn hovered_control(&self) -> Option<Control> {
self.hover
}
}