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

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

90 lines Raw ← Back
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
//! `Tooltip` — dymek podpowiedzi umieszczany nad elementem (nakładka).
//!
//! Widget immediate: [`Tooltip::rect`] liczy prostokąt nad kotwicą, [`Tooltip::paint`] rysuje
//! powierzchnię o wysokim kontraście z napisem.

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

use crate::ctx::PaintCtx;

/// Dymek podpowiedzi.
#[derive(Debug, Clone, Default)]
pub struct Tooltip {
    text: String,
}

impl Tooltip {
    pub fn new(text: impl Into<String>) -> Self {
        Self { text: text.into() }
    }

    pub fn set_text(&mut self, text: impl Into<String>) {
        self.text = text.into();
    }

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

    /// Prostokąt dymka nad kotwicą `anchor` (wyśrodkowany, odsunięty o `gap`).
    pub fn rect(&self, anchor: Rect, theme: &Theme, text: &TextEngine) -> Rect {
        let m = theme.metrics.tooltip;
        let caption = theme.typography.caption;
        let width = m.pad_x * 2.0 + text.measure(&self.text, caption.size, weight::NORMAL).width;
        let height = m.pad_y * 2.0 + caption.line_height;
        Rect::from_xywh(
            anchor.center().x - width / 2.0,
            anchor.y() - m.gap - height,
            width,
            height,
        )
    }

    /// Rysuje dymek (nakładka — na wierzchu sceny).
    pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect) {
        let theme = *ctx.theme;
        let m = theme.metrics.tooltip;
        let (bg, fg) = match theme.appearance {
            Appearance::Light => (theme.colors.text, theme.colors.view_bg),
            Appearance::Dark => (theme.colors.card_bg, theme.colors.text),
        };
        ctx.scene.fill_rounded_rect(rect, m.radius, bg);
        ctx.label_center(
            rect.center().x,
            rect.center().y,
            &self.text,
            theme.typography.caption.size,
            weight::NORMAL,
            fg,
        );
    }
}

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

    #[test]
    fn rect_sits_above_anchor_and_is_centered() {
        let theme = Theme::light();
        let text = TextEngine::system();
        let tooltip = Tooltip::new("Zapisz zmiany");
        let anchor = Rect::from_xywh(100.0, 200.0, 40.0, 20.0);
        let r = tooltip.rect(anchor, &theme, &text);
        assert!((r.center().x - anchor.center().x).abs() < 1e-9);
        assert!(r.bottom() <= anchor.y());
        assert!((anchor.y() - r.bottom() - theme.metrics.tooltip.gap).abs() < 1e-9);
    }

    #[test]
    fn width_grows_with_text() {
        let theme = Theme::light();
        let text = TextEngine::system();
        let anchor = Rect::from_xywh(0.0, 100.0, 10.0, 10.0);
        let short = Tooltip::new("a").rect(anchor, &theme, &text);
        let long = Tooltip::new("znacznie dłuższy napis").rect(anchor, &theme, &text);
        assert!(long.width() > short.width());
    }
}