//! 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 { // 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) }