Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tooltip

Group: Overlays

Code

const ANCHOR_TEXT: &str = "hover target";
const TOOLTIP_TEXT: &str = "Tooltip content";
const TOOLTIP_TITLE: &str = "Info";

pub struct TooltipDemo {
    show_title: bool,
}

impl TooltipDemo {
    pub fn new() -> Self {
        Self { show_title: true }
    }

    fn border(variant: usize) -> TooltipBorder {
        match variant {
            0 => TooltipBorder::Sides,
            1 => TooltipBorder::Full,
            _ => TooltipBorder::None,
        }
    }

    fn border_label(variant: usize) -> &'static str {
        match variant {
            0 => "Sides",
            1 => "Full",
            _ => "None",
        }
    }

    fn tooltip() -> Tooltip {
        let mut tip = Tooltip::new(WidgetId::new("gallery:tooltip:tip"), TOOLTIP_TEXT);
        tip.placement = TooltipPlacement::Bottom;
        tip
    }

    /// `Full` needs a top and bottom border row on top of the one content
    /// row; `Sides`/`None` reserve no border rows at all (see the
    /// contract note on `TooltipMeasure`).
    fn rows(variant: usize) -> f32 {
        if variant == 1 {
            3.0
        } else {
            1.0
        }
    }

    fn status_bar(&self, variant: usize) -> StatusBar {
        StatusBar {
            id: WidgetId::new("gallery:tooltip:status"),
            left_segments: vec![StatusBarSegment {
                text: format!(
                    " border: {} | title: {} ",
                    Self::border_label(variant),
                    self.show_title
                ),
                fg: Color::rgb(255, 255, 255),
                bg: Color::rgb(40, 80, 120),
                bold: false,
                action_id: None,
            }],
            right_segments: vec![StatusBarSegment {
                text: " t=toggle title (Full only) ".into(),
                fg: Color::rgb(220, 220, 220),
                bg: Color::rgb(40, 80, 120),
                bold: false,
                action_id: None,
            }],
        }
    }

    fn anchor_bar() -> StatusBar {
        StatusBar {
            id: WidgetId::new("gallery:tooltip:anchor"),
            left_segments: vec![StatusBarSegment {
                text: format!(" {ANCHOR_TEXT} "),
                fg: Color::rgb(220, 220, 220),
                bg: Color::rgb(37, 37, 38),
                bold: false,
                action_id: None,
            }],
            right_segments: vec![],
        }
    }
}

impl Default for TooltipDemo {
    fn default() -> Self {
        Self::new()
    }
}

impl Demo for TooltipDemo {
    fn name(&self) -> &'static str {
        "Tooltip"
    }

    fn group(&self) -> &'static str {
        "Overlays"
    }

    fn variants(&self) -> &'static [&'static str] {
        &["Sides", "Full", "None"]
    }

    fn render(&self, variant: usize, backend: &mut dyn Backend, area: Rect) {
        let lh = backend.line_height();
        let cw = backend.char_width();

        // Anchor bar at the top — the element the tooltip "describes".
        let anchor = Rect::new(area.x, area.y, area.width, lh);
        let _ = backend.draw_status_bar_interactive(
            anchor,
            &Self::anchor_bar(),
            &InteractionState::new(),
        );

        // Status bar at the bottom shows the current border/title choice
        // plus the key hint.
        let status_rect = Rect::new(area.x, area.y + area.height - lh, area.width, lh);
        let _ = backend.draw_status_bar_interactive(
            status_rect,
            &self.status_bar(variant),
            &InteractionState::new(),
        );

        // Tooltip renders between the two bars, below the anchor.
        let clamp = Rect::new(
            area.x,
            area.y + lh,
            area.width,
            (area.height - 2.0 * lh).max(0.0),
        );
        let tooltip = Self::tooltip();
        let measure = TooltipMeasure::new(
            cw * (TOOLTIP_TEXT.chars().count() as f32 + 4.0),
            lh * Self::rows(variant),
        );
        let layout = tooltip.layout(anchor, clamp, measure, lh);
        let mut chrome = TooltipChrome::new(Self::border(variant));
        if self.show_title && variant == 1 {
            chrome.title = Some(TOOLTIP_TITLE.to_string());
        }
        backend.draw_tooltip_with_chrome(&tooltip, &layout, &chrome);
    }

    fn handle(
        &mut self,
        _variant: usize,
        event: &UiEvent,
        _backend: &mut dyn Backend,
        _area: Rect,
    ) -> Reaction {
        match event {
            UiEvent::KeyPressed {
                key: Key::Char('t'),
                ..
            } => {
                self.show_title = !self.show_title;
                Reaction::Redraw
            }
            _ => Reaction::Continue,
        }
    }

    fn source(&self) -> &'static str {
        extract_region(SOURCE)
    }

    fn data(&self, variant: usize) -> serde_json::Value {
        serde_json::json!({
            "border": Self::border_label(variant),
            "title_shown": self.show_title && variant == 1,
        })
    }

    fn caps_note(&self, _variant: usize, _caps: &BackendCaps) -> Option<String> {
        // `draw_tooltip_with_chrome` is a required `Backend` trait method
        // with no no-op default — every backend paints the same chrome
        // vocabulary, so there's no capability gap to report here.
        None
    }
}

Backends

VariantTUIGTKmacOSWindows
Sides
Full
None