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

luna/crates/libluna-widgets/src/search_bar.rs main

113 lines Raw ← Back
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
//! `SearchBar` — pasek wyszukiwania (GTK4): odsłaniana szuflada z polem wyszukiwania.
//!
//! W modelu immediate aplikacja rysuje [`SearchEntry`](crate::SearchEntry) w prostokącie
//! [`SearchBar::entry_rect`], a widget odpowiada za tryb (widoczny/ukryty), geometrię
//! i tło. Przełączanie trybu: [`SearchBar::toggle`] / [`SearchBar::set_search_mode`].

use libluna_core::{Point, Rect, Theme};

use crate::ctx::PaintCtx;

/// Pasek wyszukiwania.
#[derive(Debug, Clone, Copy, Default)]
pub struct SearchBar {
    mode: bool,
}

impl SearchBar {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn with_mode(mode: bool) -> Self {
        Self { mode }
    }

    pub fn is_search_mode(&self) -> bool {
        self.mode
    }

    pub fn set_search_mode(&mut self, mode: bool) {
        self.mode = mode;
    }

    /// Przełącza tryb; zwraca nowy stan.
    pub fn toggle(&mut self) -> bool {
        self.mode = !self.mode;
        self.mode
    }

    /// Wysokość paska (gdy widoczny).
    pub fn height(&self, theme: &Theme) -> f64 {
        theme.metrics.entry.height + theme.spacing.sm * 2.0
    }

    /// Obszar paska w `rect` (pas u góry kontenera). Gdy ukryty — o wysokości zerowej.
    pub fn reveal_rect(&self, rect: Rect, theme: &Theme) -> Rect {
        let h = if self.mode { self.height(theme) } else { 0.0 };
        Rect::from_xywh(rect.x(), rect.y(), rect.width(), h)
    }

    /// Prostokąt pola wyszukiwania (z wewnętrznym marginesem); zerowy, gdy ukryty.
    pub fn entry_rect(&self, rect: Rect, theme: &Theme) -> Rect {
        let bar = self.reveal_rect(rect, theme);
        if bar.height() <= 0.0 {
            return Rect::from_xywh(bar.x(), bar.y(), bar.width(), 0.0);
        }
        let pad = theme.spacing.sm;
        Rect::from_xywh(
            bar.x() + pad,
            bar.y() + pad,
            (bar.width() - 2.0 * pad).max(0.0),
            (bar.height() - 2.0 * pad).max(0.0),
        )
    }

    /// Czy punkt leży w obszarze paska (np. by nie zaczynać przeciągania okna).
    pub fn hit(&self, rect: Rect, theme: &Theme, p: Point) -> bool {
        self.reveal_rect(rect, theme).contains(p)
    }

    pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect) {
        if !self.mode {
            return;
        }
        let theme = *ctx.theme;
        let bar = self.reveal_rect(rect, &theme);
        ctx.scene.fill_rect(bar, theme.colors.window_bg);
        let line = Rect::from_xywh(bar.x(), bar.bottom() - 1.0, bar.width(), 1.0);
        ctx.scene.fill_rect(line, theme.colors.border);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn hidden_bar_has_zero_height() {
        let theme = Theme::light();
        let rect = Rect::from_xywh(0.0, 0.0, 400.0, 300.0);
        let b = SearchBar::new();
        assert_eq!(b.reveal_rect(rect, &theme).height(), 0.0);
        assert_eq!(b.entry_rect(rect, &theme).height(), 0.0);
        assert!(!b.hit(rect, &theme, Point::new(10.0, 10.0)));
    }

    #[test]
    fn visible_bar_reveals_entry_with_padding() {
        let theme = Theme::light();
        let rect = Rect::from_xywh(0.0, 0.0, 400.0, 300.0);
        let mut b = SearchBar::new();
        assert!(b.toggle());
        let bar = b.reveal_rect(rect, &theme);
        assert!(
            (bar.height() - (theme.metrics.entry.height + theme.spacing.sm * 2.0)).abs() < 1e-9
        );
        let e = b.entry_rect(rect, &theme);
        assert_eq!(e.x(), theme.spacing.sm);
        assert_eq!(e.width(), 400.0 - 2.0 * theme.spacing.sm);
        assert!(b.hit(rect, &theme, e.center()));
        assert!(!b.hit(rect, &theme, Point::new(10.0, 250.0)));
    }
}