use egui::{
emath::RectAlign, Color32, CornerRadius, Frame, Margin, Pos2, Rect, Response, Sense, Shape,
Stroke, Ui, Vec2, WidgetText,
};
use crate::theme::Theme;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TooltipSide {
Top,
Bottom,
Left,
Right,
}
impl TooltipSide {
fn to_rect_align(self) -> RectAlign {
match self {
TooltipSide::Top => RectAlign::TOP,
TooltipSide::Bottom => RectAlign::BOTTOM,
TooltipSide::Left => RectAlign::LEFT,
TooltipSide::Right => RectAlign::RIGHT,
}
}
}
#[derive(Clone, Debug)]
#[must_use = "Call `.show(&trigger)` to render the tooltip."]
pub struct Tooltip {
body: WidgetText,
heading: Option<WidgetText>,
shortcut: Option<String>,
shortcut_label: String,
side: TooltipSide,
width: Option<f32>,
arrow: bool,
gap: f32,
}
impl Tooltip {
pub fn new(body: impl Into<WidgetText>) -> Self {
Self {
body: body.into(),
heading: None,
shortcut: None,
shortcut_label: "Shortcut".into(),
side: TooltipSide::Top,
width: None,
arrow: true,
gap: 8.0,
}
}
#[inline]
pub fn heading(mut self, heading: impl Into<WidgetText>) -> Self {
self.heading = Some(heading.into());
self
}
#[inline]
pub fn shortcut(mut self, shortcut: impl Into<String>) -> Self {
self.shortcut = Some(shortcut.into());
self
}
#[inline]
pub fn shortcut_label(mut self, label: impl Into<String>) -> Self {
self.shortcut_label = label.into();
self
}
#[inline]
pub fn side(mut self, side: TooltipSide) -> Self {
self.side = side;
self
}
#[inline]
pub fn width(mut self, w: f32) -> Self {
self.width = Some(w);
self
}
#[inline]
pub fn arrow(mut self, arrow: bool) -> Self {
self.arrow = arrow;
self
}
#[inline]
pub fn gap(mut self, gap: f32) -> Self {
self.gap = gap;
self
}
pub fn show(self, trigger: &Response) -> Option<Response> {
let theme = Theme::current(&trigger.ctx);
let p = &theme.palette;
let frame = Frame::new()
.fill(p.card)
.stroke(Stroke::new(1.0, p.border))
.corner_radius(CornerRadius::same(theme.control_radius as u8))
.inner_margin(Margin::symmetric(10, 8));
let mut tip = egui::Tooltip::for_enabled(trigger);
tip.popup = tip
.popup
.frame(frame)
.align(self.side.to_rect_align())
.align_alternatives(&[])
.gap(self.gap);
tip.popup = tip.popup.width(self.width.unwrap_or(260.0));
let trigger_rect = trigger.rect;
let trigger_ctx = trigger.ctx.clone();
let arrow = self.arrow;
let side = self.side;
let theme_for_paint = theme.clone();
let heading = self.heading;
let body = self.body;
let shortcut = self.shortcut;
let shortcut_label = self.shortcut_label;
let inner = tip.show(move |ui| {
paint_contents(
ui,
&theme_for_paint,
heading.as_ref(),
&body,
shortcut.as_deref(),
&shortcut_label,
);
})?;
if arrow {
let actual_side = detect_side(trigger_rect, inner.response.rect, side);
paint_arrow(
&trigger_ctx,
inner.response.layer_id,
inner.response.rect,
trigger_rect,
actual_side,
theme.palette.card,
theme.palette.border,
);
}
Some(inner.response)
}
}
fn paint_contents(
ui: &mut Ui,
theme: &Theme,
heading: Option<&WidgetText>,
body: &WidgetText,
shortcut: Option<&str>,
shortcut_label: &str,
) {
let p = &theme.palette;
let t = &theme.typography;
if let Some(h) = heading {
ui.add(
egui::Label::new(
egui::RichText::new(h.text())
.color(p.text)
.size(t.body)
.strong(),
)
.wrap_mode(egui::TextWrapMode::Wrap),
);
ui.add_space(2.0);
}
ui.add(
egui::Label::new(
egui::RichText::new(body.text())
.color(p.text_muted)
.size(t.small),
)
.wrap_mode(egui::TextWrapMode::Wrap),
);
if let Some(sc) = shortcut {
ui.add_space(6.0);
let avail = ui.available_width();
let sep_y = ui.cursor().min.y;
ui.painter().line_segment(
[
Pos2::new(ui.cursor().min.x, sep_y),
Pos2::new(ui.cursor().min.x + avail, sep_y),
],
Stroke::new(1.0, p.border),
);
ui.add_space(6.0);
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 6.0;
ui.add(egui::Label::new(
egui::RichText::new(shortcut_label)
.color(p.text_faint)
.size(t.small),
));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.spacing_mut().item_spacing.x = 2.0;
let tokens: Vec<&str> = sc.split_whitespace().collect();
for token in tokens.iter().rev() {
add_kbd(ui, token, theme);
}
});
});
}
}
fn add_kbd(ui: &mut Ui, text: &str, theme: &Theme) -> Response {
let p = &theme.palette;
let font_id = egui::FontId::monospace(11.0);
let galley = ui
.painter()
.layout_no_wrap(text.to_string(), font_id, p.text);
let pad_x = 5.0;
let width = (galley.size().x + pad_x * 2.0).max(16.0);
let height = 18.0;
let (rect, response) = ui.allocate_exact_size(Vec2::new(width, height), Sense::hover());
if ui.is_rect_visible(rect) {
ui.painter().rect(
rect,
CornerRadius::same(3),
p.input_bg,
Stroke::new(1.0, p.border),
egui::StrokeKind::Inside,
);
let pos = Pos2::new(
rect.center().x - galley.size().x * 0.5,
rect.center().y - galley.size().y * 0.5,
);
ui.painter().galley(pos, galley, p.text);
}
response
}
fn detect_side(trigger: Rect, popup: Rect, requested: TooltipSide) -> TooltipSide {
match requested {
TooltipSide::Top | TooltipSide::Bottom => {
if popup.center().y < trigger.center().y {
TooltipSide::Top
} else {
TooltipSide::Bottom
}
}
TooltipSide::Left | TooltipSide::Right => {
if popup.center().x < trigger.center().x {
TooltipSide::Left
} else {
TooltipSide::Right
}
}
}
}
fn paint_arrow(
ctx: &egui::Context,
layer: egui::LayerId,
popup: Rect,
trigger: Rect,
side: TooltipSide,
fill: Color32,
border: Color32,
) {
let painter = ctx.layer_painter(layer);
let half_base = 6.0;
let depth = 6.0;
let inset = 10.0;
let (base_center, perp, base_axis) = match side {
TooltipSide::Bottom => {
let cx = trigger
.center()
.x
.clamp(popup.min.x + inset, popup.max.x - inset);
(
Pos2::new(cx, popup.min.y),
Vec2::new(0.0, -1.0),
Vec2::new(1.0, 0.0),
)
}
TooltipSide::Top => {
let cx = trigger
.center()
.x
.clamp(popup.min.x + inset, popup.max.x - inset);
(
Pos2::new(cx, popup.max.y),
Vec2::new(0.0, 1.0),
Vec2::new(1.0, 0.0),
)
}
TooltipSide::Right => {
let cy = trigger
.center()
.y
.clamp(popup.min.y + inset, popup.max.y - inset);
(
Pos2::new(popup.min.x, cy),
Vec2::new(-1.0, 0.0),
Vec2::new(0.0, 1.0),
)
}
TooltipSide::Left => {
let cy = trigger
.center()
.y
.clamp(popup.min.y + inset, popup.max.y - inset);
(
Pos2::new(popup.max.x, cy),
Vec2::new(1.0, 0.0),
Vec2::new(0.0, 1.0),
)
}
};
let base_a = base_center + base_axis * half_base;
let base_b = base_center - base_axis * half_base;
let tip = base_center + perp * depth;
painter.add(Shape::convex_polygon(
vec![base_a, tip, base_b],
fill,
Stroke::NONE,
));
painter.line_segment([base_a, base_b], Stroke::new(1.5, fill));
let stroke = Stroke::new(1.0, border);
painter.line_segment([base_a, tip], stroke);
painter.line_segment([base_b, tip], stroke);
}