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

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

109 lines Raw ← Back
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
//! `ActionRow` — wiersz ustawień: tytuł (i podtytuł) z lewej, obszar na kontrolkę z prawej.
//!
//! Widget immediate: rysuje tekst wiersza i zwraca [`Rect`] na kontrolkę (`suffix`), którą
//! umieszcza aplikacja. Padding pochodzi z tokenów motywu ([`ActionRowMetrics`](libluna_core::ActionRowMetrics)).

use libluna_core::style::weight;
use libluna_core::{Rect, Theme};

use crate::ctx::PaintCtx;

/// Wiersz akcji (ustawienie).
#[derive(Debug, Clone, Default)]
pub struct ActionRow {
    title: String,
    subtitle: Option<String>,
}

impl ActionRow {
    pub fn new(title: impl Into<String>) -> Self {
        Self {
            title: title.into(),
            subtitle: None,
        }
    }

    pub fn subtitle(mut self, subtitle: impl Into<String>) -> Self {
        self.subtitle = Some(subtitle.into());
        self
    }

    pub fn title(&self) -> &str {
        &self.title
    }

    pub fn subtitle_text(&self) -> Option<&str> {
        self.subtitle.as_deref()
    }

    /// Obszar na kontrolkę (prawa strona) o zadanej szerokości.
    pub fn suffix_rect(&self, rect: Rect, theme: &Theme, suffix_width: f64) -> Rect {
        let pad = theme.metrics.row.pad_x;
        Rect::from_xywh(
            rect.right() - pad - suffix_width,
            rect.y(),
            suffix_width,
            rect.height(),
        )
    }

    /// Rysuje tytuł (i podtytuł); zwraca obszar na kontrolkę.
    pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect, suffix_width: f64) -> Rect {
        let theme = *ctx.theme;
        let pad = theme.metrics.row.pad_x;
        let x = rect.x() + pad;
        match &self.subtitle {
            Some(subtitle) => {
                let gap = theme.metrics.row.gap;
                ctx.label_left(
                    x,
                    rect.center().y - gap / 2.0 - 2.0,
                    &self.title,
                    theme.typography.body.size,
                    weight::NORMAL,
                    theme.colors.text,
                );
                ctx.label_left(
                    x,
                    rect.center().y + gap / 2.0 + 5.0,
                    subtitle,
                    theme.typography.caption.size,
                    weight::NORMAL,
                    theme.colors.dim_text,
                );
            }
            None => ctx.label_left(
                x,
                rect.center().y,
                &self.title,
                theme.typography.body.size,
                weight::NORMAL,
                theme.colors.text,
            ),
        }
        self.suffix_rect(rect, &theme, suffix_width)
    }
}

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

    #[test]
    fn suffix_is_right_aligned_with_padding() {
        let theme = Theme::light();
        let row = ActionRow::new("Motyw");
        let rect = Rect::from_xywh(0.0, 0.0, 300.0, 56.0);
        let s = row.suffix_rect(rect, &theme, 120.0);
        assert!((s.right() - (rect.right() - theme.metrics.row.pad_x)).abs() < 1e-9);
        assert!((s.width() - 120.0).abs() < 1e-9);
        assert!((s.height() - rect.height()).abs() < 1e-9);
    }

    #[test]
    fn subtitle_builder() {
        let row = ActionRow::new("Sieć").subtitle("Włączone");
        assert_eq!(row.title(), "Sieć");
        assert_eq!(row.subtitle_text(), Some("Włączone"));
    }
}