🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
//! Pomocnicze funkcje rysujące Cairo.
//!
//! Moduł celowo nie zna pojęcia „okna" ani „zdarzeń" — operuje wyłącznie na
//! [`cairo::Context`]. Dzięki temu te same prymitywy (okrąg, zaokrąglony prostokąt,
//! pasek tytułu) można wykorzystać w innych widgetach PaganDE.
//!
//! Ważne własności Cairo, na których opiera się ten kod:
//!
//! * Cairo włącza antyaliasing domyślnie — nic nie trzeba robić.
//! * `Operator::Clear` zeruje kanał alfa bufora, co daje prawdziwą przezroczystość
//! poza zaokrągleniami okna (a nie „czarne" piksele).
//! * Format `ARgb32` jest premultiplied, dlatego rysujemy po prostu kolorami
//! niepremultipliplowanymi przez standardowy operator `Over`.
use std::f64::consts::{FRAC_PI_2, PI};
use cairo::{Context, FontSlant, FontWeight, Format, ImageSurface, Operator};
/// Ciemne tło paska tytułu.
pub const TITLEBAR_COLOR: [f64; 3] = [0.13, 0.14, 0.18];
/// Ciemne tło „ciała" okna (poniżej paska tytułu).
pub const BODY_COLOR: [f64; 3] = [0.09, 0.10, 0.12];
/// Tło rozwiniętego menu aplikacji.
pub const MENU_PANEL_COLOR: [f64; 3] = [0.13, 0.14, 0.18];
/// Tło pozycji menu pod kursorem.
pub const MENU_ITEM_HOVER_COLOR: [f64; 3] = [0.22, 0.26, 0.34];
/// Tło paska menu rysowanego przez klienta w trybie SSD (pod paskiem kompozytora).
pub const MENU_STRIP_COLOR: [f64; 3] = [0.11, 0.12, 0.15];
/// Kolor monochromatycznych ikon w trybie SSD (spójny z glifami kompozytora).
pub const ICON_COLOR: [f64; 3] = [0.82, 0.84, 0.88];
/// Kolor napisu tytułu.
pub const TITLE_TEXT_COLOR: [f64; 3] = [0.82, 0.84, 0.88];
/// Bazowy kolor przycisku „Menu".
pub const COLOR_MENU: [f64; 3] = [0.20, 0.60, 0.40];
/// Bazowy kolor przycisku minimalizacji.
pub const COLOR_MINIMIZE: [f64; 3] = [0.90, 0.72, 0.28];
/// Bazowy kolor przycisku maksymalizacji.
pub const COLOR_MAXIMIZE: [f64; 3] = [0.32, 0.72, 0.40];
/// Bazowy kolor przycisku zamknięcia.
pub const COLOR_CLOSE: [f64; 3] = [0.82, 0.30, 0.30];
/// Mierzy szerokość napisu w pikselach (bez rysowania).
///
/// PO CO: panel rozwiniętego menu musi mieć szerokość dopasowaną do najdłuższej
/// pozycji — a decydujemy o niej, zanim cokolwiek narysujemy (żeby trafienia myszy
/// i rysowanie zgadzały się co do piksela).
pub fn measure_text(text: &str, font_px: f64) -> Result<f64, cairo::Error> {
// Pomiar wymaga kontekstu, więc tworzymy maleńką powierzchnię pomocniczą.
let surface = ImageSurface::create(Format::ARgb32, 1, 1)?;
let ctx = Context::new(&surface)?;
ctx.select_font_face("sans-serif", FontSlant::Normal, FontWeight::Normal);
ctx.set_font_size(font_px);
Ok(ctx.text_extents(text)?.width())
}
/// Ustawia bieżący kolor źródła w Cairo.
///
/// Wydzielone, bo w całym module powtarzamy ten sam wzorzec i chcemy mieć jedno
/// miejsce, które tłumaczy tablicę `[f64; 3]` na wywołanie Cairo.
pub fn set_color(ctx: &Context, color: [f64; 3]) {
ctx.set_source_rgb(color[0], color[1], color[2]);
}
/// Rozjaśnia kolor na potrzeby stanu „hover".
///
/// Mieszamy kolor z bielą w stałej proporcji — proste, tanie i wystarczająco
/// czytelne wizualnie, bez potrzeby trzymania osobnej palety aktywnych kolorów.
pub fn highlight_color(color: [f64; 3]) -> [f64; 3] {
let k = 0.45;
[
(color[0] + (1.0 - color[0]) * k).min(1.0),
(color[1] + (1.0 - color[1]) * k).min(1.0),
(color[2] + (1.0 - color[2]) * k).min(1.0),
]
}
/// Czyści cały bufor do pełnej przezroczystości (`alpha = 0`).
///
/// Używamy `Operator::Clear`, a nie malowania „czarnym z alpha=0", bo tylko Clear
/// zeruje jednocześnie RGB i alfę. Dzięki temu obszar poza zaokrąglonymi rogami
/// jest w 100% przezroczysty i kompozytor pokaże pod nim pulpit.
pub fn clear_transparent(ctx: &Context) -> Result<(), cairo::Error> {
ctx.save()?;
ctx.set_operator(Operator::Clear);
ctx.paint()?;
ctx.restore()?;
// Przywracamy domyślny operator kompozycji dla kolejnych warstw.
ctx.set_operator(Operator::Over);
Ok(())
}
/// Buduje ścieżkę zaokrąglonego prostokąta (bez wypełniania).
///
/// Cairo nie ma takiego helpera, więc składamy ścieżkę z czterech łuków połączonych
/// odcinkami. `new_sub_path()` jest kluczowe: bez niego `arc()` dorysowałby linię
/// od bieżącego punktu do początku pierwszego łuku.
pub fn rounded_rectangle(ctx: &Context, x: f64, y: f64, w: f64, h: f64, r: f64) {
// Promień nie może przekroczyć połowy krótszego boku, inaczej łuki „zazębiają się".
let r = r.max(0.0).min(w.min(h) / 2.0);
let right = x + w;
let bottom = y + h;
ctx.new_sub_path();
// Górna krawędź: od (x+r, y) do (right-r, y) przez łuk w prawym górnym rogu.
ctx.arc(right - r, y + r, r, -FRAC_PI_2, 0.0);
ctx.arc(right - r, bottom - r, r, 0.0, FRAC_PI_2);
ctx.arc(x + r, bottom - r, r, FRAC_PI_2, PI);
ctx.arc(x + r, y + r, r, PI, 3.0 * FRAC_PI_2);
ctx.close_path();
}
/// Buduje ścieżkę prostokąta z zaokrąglonymi **tylko dolnymi** rogami.
///
/// PO CO: w trybie SSD pasek tytułu rysuje kompozytor NAD treścią okna. Gdybyśmy
/// zaokrąglili też górne rogi korpusu, w narożnikach prześwitywałby pulpit
/// (pasek kompozytora ma ostre dolne krawędzie).
pub fn rounded_bottom_rectangle(ctx: &Context, x: f64, y: f64, w: f64, h: f64, r: f64) {
let r = r.max(0.0).min(w.min(h) / 2.0);
let right = x + w;
let bottom = y + h;
ctx.new_sub_path();
ctx.move_to(right, y);
ctx.line_to(right, bottom - r);
ctx.arc(right - r, bottom - r, r, 0.0, FRAC_PI_2);
ctx.arc(x + r, bottom - r, r, FRAC_PI_2, PI);
ctx.line_to(x, y);
ctx.close_path();
}
/// Rysuje tekst wyśrodkowany względem punktu (`center_x`, `center_y`).
///
/// Mierzymy szerokość napisu (`text_extents`) i przesuwamy punkt startowy o jej
/// połowę — bez tego tytuł okna byłby wyrównany do lewej i wyglądałby krzywo.
pub fn draw_centered_text(
ctx: &Context,
text: &str,
center_x: f64,
center_y: f64,
color: [f64; 3],
) -> Result<(), cairo::Error> {
const FONT_SIZE: f64 = 15.0;
ctx.select_font_face("sans-serif", FontSlant::Normal, FontWeight::Normal);
ctx.set_font_size(FONT_SIZE);
let extents = ctx.text_extents(text)?;
set_color(ctx, color);
ctx.move_to(
center_x - extents.width() / 2.0,
center_y + FONT_SIZE * 0.35,
);
ctx.show_text(text)
}
/// Rysuje pasek tytułu z zaokrąglonymi **tylko górnymi** rogami.
///
/// Dolne rogi są ostre, ponieważ pasek styka się bezpośrednio z treścią okna —
/// zaokrąglenie tam wyglądałoby jak szczelina.
pub fn draw_titlebar(
ctx: &Context,
width: f64,
height: f64,
corner_radius: f64,
) -> Result<(), cairo::Error> {
let r = corner_radius.max(0.0).min(width.min(height) / 2.0);
ctx.new_sub_path();
// Start w lewym górnym łuku, koniec w prawym górnym, potem prosto w dół.
ctx.arc(r, r, r, PI, 3.0 * FRAC_PI_2);
ctx.line_to(width - r, 0.0);
ctx.arc(width - r, r, r, -FRAC_PI_2, 0.0);
ctx.line_to(width, height);
ctx.line_to(0.0, height);
ctx.close_path();
set_color(ctx, TITLEBAR_COLOR);
ctx.fill()
}
/// Rysuje pojedynczy przycisk-okrąg.
///
/// `color` to kolor bazowy; przy hover przekazujemy wersję rozjaśnioną przez
/// [`highlight_color`]. Funkcja nie zna pojęcia „hover" — to celowe, dzięki temu
/// jest uniwersalna.
pub fn draw_circle_button(
ctx: &Context,
center_x: f64,
center_y: f64,
radius: f64,
color: [f64; 3],
) -> Result<(), cairo::Error> {
ctx.new_sub_path();
ctx.arc(center_x, center_y, radius, 0.0, 2.0 * PI);
ctx.close_path();
set_color(ctx, color);
ctx.fill()
}
/// Rysuje przycisk „Menu": okrąg + trzy poziome kreski („hamburger").
///
/// Środek okręgu wyznacza jednocześnie środek pionowy kresek, dzięki czemu ikona
/// jest idealnie wyrównana z okręgiem niezależnie od promienia.
pub fn draw_menu_button(
ctx: &Context,
center_x: f64,
center_y: f64,
radius: f64,
color: [f64; 3],
) -> Result<(), cairo::Error> {
draw_circle_button(ctx, center_x, center_y, radius, color)?;
ctx.set_source_rgba(1.0, 1.0, 1.0, 0.85);
ctx.set_line_width(1.6);
let half_width = radius * 0.55;
let gap = radius * 0.42;
for line in -1..=1 {
let y = center_y + (line as f64) * gap;
ctx.move_to(center_x - half_width, y);
ctx.line_to(center_x + half_width, y);
}
ctx.stroke()
}
/// Rysuje samą ikonę „burger" (trzy kreski), bez koła tła.
///
/// PO CO osobno: w trybie SSD pasek tytułu rysuje kompozytor w płaskim, monochromatycznym
/// stylu. Klient dokłada w swoim pasku menu ikonę w tym samym stylu — kolorowe koło
/// wyglądałoby tam niespójnie.
pub fn draw_burger(
ctx: &Context,
center_x: f64,
center_y: f64,
half_width: f64,
color: [f64; 3],
) -> Result<(), cairo::Error> {
set_color(ctx, color);
ctx.set_line_width(2.0);
// Zaokrąglone końce kresek — spójne z glifami rysowanymi przez kompozytor.
ctx.set_line_cap(cairo::LineCap::Round);
let gap = 4.0;
for line in -1..=1 {
let y = center_y + (line as f64) * gap;
ctx.move_to(center_x - half_width, y);
ctx.line_to(center_x + half_width, y);
}
ctx.stroke()
}
/// Rysuje trzy przyciski okna (minimalizuj / maksymalizuj / zamknij).
///
/// Wszystkie trzy leżą na tej samej osi Y (`center_y`), którą wołający ustawia na
/// `TITLEBAR_CONTROL_CENTER_Y`. Wymuszamy to parametrem, a nie liczeniem wysokości
/// paska w tym miejscu, żeby zmiana wysokości paska nigdy nie rozjechała ikon.
#[allow(clippy::too_many_arguments)]
pub fn draw_window_controls(
ctx: &Context,
right_start: f64,
center_y: f64,
radius: f64,
spacing: f64,
hover_close: bool,
hover_minimize: bool,
hover_maximize: bool,
) -> Result<(), cairo::Error> {
let minimize = if hover_minimize {
highlight_color(COLOR_MINIMIZE)
} else {
COLOR_MINIMIZE
};
let maximize = if hover_maximize {
highlight_color(COLOR_MAXIMIZE)
} else {
COLOR_MAXIMIZE
};
let close = if hover_close {
highlight_color(COLOR_CLOSE)
} else {
COLOR_CLOSE
};
// Kolejność od lewej do prawej: minimalizuj, maksymalizuj, zamknij.
draw_circle_button(ctx, right_start, center_y, radius, minimize)?;
draw_circle_button(ctx, right_start + spacing, center_y, radius, maximize)?;
draw_circle_button(ctx, right_start + spacing * 2.0, center_y, radius, close)
}
/// Rysuje tekst tytułu przy użyciu „toy" API Cairo (bez Pango/HarfBuzz).
///
/// `center_y` to oś pionowa, na której ma być wyśrodkowany tekst; przeliczamy ją
/// na linię bazową, bo Cairo pozycjonuje tekst względem baseline'u.
pub fn draw_title_text(
ctx: &Context,
text: &str,
x: f64,
center_y: f64,
color: [f64; 3],
) -> Result<(), cairo::Error> {
const FONT_SIZE: f64 = 13.0;
// API-NOTE: toy font API (`select_font_face`/`show_text`) jest dostępne w cairo-rs
// bez dodatkowych feature'ów, ale warto zweryfikować sygnatury przy zmianie wersji.
ctx.select_font_face("sans-serif", FontSlant::Normal, FontWeight::Normal);
ctx.set_font_size(FONT_SIZE);
set_color(ctx, color);
ctx.move_to(x, center_y + FONT_SIZE * 0.35);
ctx.show_text(text)
}