use super::prim;
use crate::error::Error;
use crate::layout::{Bbox, PlacedNode};
use crate::resolve::{AttrMap, ResolvedValue};
use crate::span::Span;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Tooltip {
None,
Hover,
Auto,
Always,
}
impl Tooltip {
pub fn inline(self) -> bool {
matches!(self, Tooltip::Auto | Tooltip::Always)
}
pub fn forced(self) -> bool {
matches!(self, Tooltip::Always)
}
}
const SIZE: f64 = 11.0;
const PAD: f64 = 5.0;
const GAP: f64 = 7.0;
pub fn read(attrs: &AttrMap) -> Result<Tooltip, Error> {
read_or(attrs, Tooltip::Auto)
}
pub fn read_or(attrs: &AttrMap, default: Tooltip) -> Result<Tooltip, Error> {
match attrs.get("tooltip") {
None => Ok(default),
Some(ResolvedValue::Ident(s)) => match s.as_str() {
"none" => Ok(Tooltip::None),
"hover" => Ok(Tooltip::Hover),
"auto" => Ok(Tooltip::Auto),
"always" => Ok(Tooltip::Always),
_ => Err(Error::at(
Span::empty(),
"'tooltip' is none, hover, auto, or always",
)),
},
_ => Err(Error::at(
Span::empty(),
"'tooltip' is none, hover, auto, or always",
)),
}
}
pub fn apply(kids: Vec<PlacedNode>, mode: Tooltip, w: f64, h: f64) -> Vec<PlacedNode> {
let mut kids = kids;
if mode == Tooltip::None {
for n in &mut kids {
n.attrs.remove("title");
}
return kids;
}
let mut cards = Vec::new();
for node in kids.iter_mut() {
let Some(text) = title_string(node) else {
continue;
};
let i = cards.len();
let (ax, ay) = anchor(node);
cards.push(make_card(&text, ax, ay, i, w, h));
node.type_chain.push(format!("hit-{i}"));
}
kids.extend(cards);
kids
}
fn title_string(node: &PlacedNode) -> Option<String> {
match node.attrs.get("title") {
Some(ResolvedValue::String(s)) => Some(s.clone()),
_ => None,
}
}
fn anchor(node: &PlacedNode) -> (f64, f64) {
(
node.cx + (node.bbox.min_x + node.bbox.max_x) / 2.0,
node.cy + (node.bbox.min_y + node.bbox.max_y) / 2.0,
)
}
fn make_card(text: &str, ax: f64, ay: f64, index: usize, w: f64, h: f64) -> PlacedNode {
let cw = prim::text_width(text, SIZE) + PAD * 2.0;
let ch = SIZE + PAD * 2.0;
let cx = (ax + GAP + cw / 2.0).clamp(-w / 2.0 + cw / 2.0, w / 2.0 - cw / 2.0);
let cy = (ay - GAP - ch / 2.0).clamp(-h / 2.0 + ch / 2.0, h / 2.0 - ch / 2.0);
let mut bg = prim::rect(cx, cy, cw, ch, live("tip-bg"), 1.0);
prim::round(&mut bg, 3.0);
let txt = prim::text(text, cx, cy, SIZE, Some(live("tip-fg")), false);
let bbox = Bbox {
min_x: cx - cw / 2.0,
min_y: cy - ch / 2.0,
max_x: cx + cw / 2.0,
max_y: cy + ch / 2.0,
};
let classes = vec!["chart-tip".to_string(), format!("tip-{index}")];
prim::group(vec![bg, txt], classes, bbox)
}
fn live(name: &str) -> ResolvedValue {
ResolvedValue::LiveVar {
name: name.into(),
raw: false,
}
}