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

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

139 lines Raw ← Back
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
//! `DropDown` — kontrolka wyboru jednej opcji. Zamknięty stan to pudełko
//! z etykietą i chevronem; rozwinięcie realizuje [`Menu`] (patrz [`DropDown::menu`]).

use libluna_core::style::weight;
use libluna_core::{Point, Rect, Theme};
use libluna_render::TextEngine;

use crate::ctx::PaintCtx;
use crate::icon;
use crate::menu::{Menu, MenuEntry};

/// Lista rozwijana.
#[derive(Debug, Clone)]
pub struct DropDown {
    options: Vec<String>,
    selected: usize,
}

impl DropDown {
    pub fn new<I, S>(options: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        Self {
            options: options.into_iter().map(Into::into).collect(),
            selected: 0,
        }
    }

    pub fn options(&self) -> &[String] {
        &self.options
    }

    pub fn selected(&self) -> usize {
        self.selected
    }

    pub fn selected_label(&self) -> Option<&str> {
        self.options.get(self.selected).map(String::as_str)
    }

    pub fn set_selected(&mut self, index: usize) {
        if !self.options.is_empty() {
            self.selected = index.min(self.options.len() - 1);
        }
    }

    /// Szerokość dopasowana do najdłuższej opcji.
    pub fn width(&self, text: &TextEngine, theme: &Theme) -> f64 {
        let m = theme.metrics.dropdown;
        let body = theme.typography.body.size;
        let mut w = m.min_width;
        for option in &self.options {
            w = w.max(
                text.measure(option, body, weight::NORMAL).width
                    + 2.0 * m.pad_x
                    + m.chevron_gap
                    + m.chevron,
            );
        }
        w
    }

    pub fn height(&self, theme: &Theme) -> f64 {
        theme.metrics.dropdown.height
    }

    pub fn hit(&self, rect: Rect, p: Point) -> bool {
        rect.contains(p)
    }

    /// Zwraca `true`, jeśli klik trafił w kontrolkę (rozwinięcie robi aplikacja).
    pub fn click_at(&self, rect: Rect, p: Point) -> bool {
        rect.contains(p)
    }

    /// Pozycje do popoveru (menu rozwijane).
    pub fn menu(&self) -> Menu {
        Menu::new(
            self.options
                .iter()
                .cloned()
                .map(MenuEntry::action)
                .collect(),
        )
    }

    /// Rysuje zamkniętą kontrolkę.
    pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect) {
        let theme = *ctx.theme;
        let colors = theme.colors;
        ctx.scene
            .fill_rounded_rect(rect, theme.radius.small, colors.card_bg);
        ctx.scene
            .stroke_rounded_rect(rect, theme.radius.small, 1.0, colors.border);

        let body = theme.typography.body.size;
        let pad_x = theme.metrics.dropdown.pad_x;
        let label = self.selected_label().unwrap_or("");
        ctx.label_left(
            rect.x() + pad_x,
            rect.center().y,
            label,
            body,
            weight::NORMAL,
            colors.text,
        );
        icon::chevron_down(
            ctx,
            colors.dim_text,
            Point::new(rect.right() - pad_x, rect.center().y),
            4.0,
            2.0,
        );
    }
}

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

    #[test]
    fn selection_and_menu_len() {
        let mut dd = DropDown::new(["A", "B", "C"]);
        assert_eq!(dd.selected_label(), Some("A"));
        dd.set_selected(2);
        assert_eq!(dd.selected_label(), Some("C"));
        assert_eq!(dd.menu().entries().len(), 3);
    }

    #[test]
    fn hit_test() {
        let dd = DropDown::new(["A"]);
        let rect = Rect::from_xywh(0.0, 0.0, 160.0, 34.0);
        assert!(dd.click_at(rect, Point::new(10.0, 10.0)));
        assert!(!dd.click_at(rect, Point::new(300.0, 10.0)));
    }
}