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
| Variant | TUI | GTK | macOS | Windows |
|---|---|---|---|---|
| Sides | ||||
| Full | ||||
| None |