🔒 Repository is read-only – file editing is disabled.

PaganDE/pagan-toolkit/src/draw.rs main

306 linii Raw ← Powrót
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)
}