use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use crate::color::Color;
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult};
use crate::geometry::{Point, Rect, Size};
use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
use crate::text::Font;
use crate::widget::{current_mouse_world, Widget};
const HOVER_DELAY_FRAMES: u32 = 18;
const TOOLTIP_FONT_SIZE: f64 = 12.0;
const TOOLTIP_PAD_X: f64 = 8.0;
const TOOLTIP_PAD_Y: f64 = 6.0;
const TOOLTIP_GAP: f64 = 4.0;
const SCREEN_MARGIN: f64 = 4.0;
#[derive(Clone)]
enum TooltipLineKind {
Text,
Code,
Link,
}
#[derive(Clone)]
struct TooltipLine {
text: String,
kind: TooltipLineKind,
}
struct TooltipRequest {
font: Arc<Font>,
lines: Vec<TooltipLine>,
anchor: Point,
at_pointer: bool,
}
thread_local! {
static TOOLTIP_QUEUE: RefCell<Vec<TooltipRequest>> = const { RefCell::new(Vec::new()) };
}
pub struct Tooltip {
bounds: Rect,
children: Vec<Box<dyn Widget>>,
base: WidgetBase,
hover_frames: u32,
hovered: bool,
cursor: Point,
font: Arc<Font>,
lines: Vec<TooltipLine>,
disabled_lines: Vec<TooltipLine>,
disabled_when: Option<Rc<dyn Fn() -> bool>>,
at_pointer: bool,
}
impl Tooltip {
pub fn new(child: Box<dyn Widget>, text: impl Into<String>, font: Arc<Font>) -> Self {
Self {
bounds: Rect::default(),
children: vec![child],
base: WidgetBase::new(),
hover_frames: 0,
hovered: false,
cursor: Point::ORIGIN,
font,
lines: text_to_lines(text),
disabled_lines: Vec::new(),
disabled_when: None,
at_pointer: false,
}
}
pub fn with_text(mut self, text: impl Into<String>) -> Self {
self.lines.extend(text_to_lines(text));
self
}
pub fn with_code_line(mut self, text: impl Into<String>) -> Self {
self.lines.push(TooltipLine {
text: text.into(),
kind: TooltipLineKind::Code,
});
self
}
pub fn with_link_line(mut self, text: impl Into<String>) -> Self {
self.lines.push(TooltipLine {
text: text.into(),
kind: TooltipLineKind::Link,
});
self
}
pub fn at_pointer(mut self) -> Self {
self.at_pointer = true;
self
}
pub fn with_disabled_text(
mut self,
text: impl Into<String>,
disabled_when: impl Fn() -> bool + 'static,
) -> Self {
self.disabled_lines = text_to_lines(text);
self.disabled_when = Some(Rc::new(disabled_when));
self
}
pub fn with_margin(mut self, m: Insets) -> Self {
self.base.margin = m;
self
}
pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
self.base.h_anchor = h;
self
}
pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
self.base.v_anchor = v;
self
}
fn show_tip(&self) -> bool {
self.hovered && self.hover_frames >= HOVER_DELAY_FRAMES
}
fn active_lines(&self) -> Vec<TooltipLine> {
if self.disabled_when.as_ref().map(|f| f()).unwrap_or(false)
&& !self.disabled_lines.is_empty()
{
self.disabled_lines.clone()
} else {
self.lines.clone()
}
}
}
impl Widget for Tooltip {
fn type_name(&self) -> &'static str {
"Tooltip"
}
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
fn margin(&self) -> Insets {
self.base.margin
}
fn h_anchor(&self) -> HAnchor {
self.base.h_anchor
}
fn v_anchor(&self) -> VAnchor {
self.base.v_anchor
}
fn is_focusable(&self) -> bool {
self.children
.first()
.map(|c| c.is_focusable())
.unwrap_or(false)
}
fn layout(&mut self, available: Size) -> Size {
let s = if let Some(child) = self.children.first_mut() {
let cs = child.layout(available);
child.set_bounds(Rect::new(0.0, 0.0, cs.width, cs.height));
cs
} else {
available
};
self.bounds = Rect::new(0.0, 0.0, s.width, s.height);
s
}
fn paint(&mut self, _: &mut dyn DrawCtx) {}
fn paint_overlay(&mut self, ctx: &mut dyn DrawCtx) {
if self.hovered {
self.hover_frames = self.hover_frames.saturating_add(1);
if !self.show_tip() {
crate::animation::request_draw();
}
}
if !self.show_tip() {
return;
}
let mut anchor = if self.at_pointer {
current_mouse_world().unwrap_or(self.cursor)
} else {
let mut x = self.bounds.width * 0.5;
let mut y = self.bounds.height;
ctx.root_transform().transform(&mut x, &mut y);
Point::new(x, y)
};
if self.at_pointer {
anchor.x += 14.0;
anchor.y += 14.0;
}
submit_tooltip(TooltipRequest {
font: Arc::clone(&self.font),
lines: self.active_lines(),
anchor,
at_pointer: self.at_pointer,
});
}
fn on_event(&mut self, event: &Event) -> EventResult {
match event {
Event::MouseMove { pos } => {
let was = self.hovered;
self.hovered = self.hit_test(*pos);
self.cursor = *pos;
if !self.hovered {
self.hover_frames = 0;
}
if self.hovered != was {
crate::animation::request_draw();
}
EventResult::Ignored
}
Event::MouseWheel { .. } => {
self.hovered = false;
self.hover_frames = 0;
EventResult::Ignored
}
_ => EventResult::Ignored,
}
}
fn hit_test(&self, local_pos: Point) -> bool {
local_pos.x >= 0.0
&& local_pos.x <= self.bounds.width
&& local_pos.y >= 0.0
&& local_pos.y <= self.bounds.height
}
}
fn text_to_lines(text: impl Into<String>) -> Vec<TooltipLine> {
text.into()
.lines()
.map(|line| TooltipLine {
text: line.to_owned(),
kind: TooltipLineKind::Text,
})
.collect()
}
fn submit_tooltip(request: TooltipRequest) {
TOOLTIP_QUEUE.with(|q| q.borrow_mut().push(request));
}
pub(crate) fn begin_tooltip_frame() {
TOOLTIP_QUEUE.with(|q| q.borrow_mut().clear());
}
pub(crate) fn paint_global_tooltips(ctx: &mut dyn DrawCtx, viewport: Size) {
let requests = TOOLTIP_QUEUE.with(|q| q.borrow_mut().drain(..).collect::<Vec<_>>());
for request in requests {
paint_request(ctx, viewport, request);
}
}
fn paint_request(ctx: &mut dyn DrawCtx, viewport: Size, request: TooltipRequest) {
if request.lines.is_empty() {
return;
}
let v = ctx.visuals();
ctx.set_font(Arc::clone(&request.font));
ctx.set_font_size(TOOLTIP_FONT_SIZE);
let line_h = TOOLTIP_FONT_SIZE * 1.45;
let mut max_w = 0.0_f64;
for line in &request.lines {
if let Some(m) = ctx.measure_text(&line.text) {
max_w = max_w.max(m.width);
}
}
let panel_w = (max_w + TOOLTIP_PAD_X * 2.0).max(64.0);
let panel_h = request.lines.len() as f64 * line_h + TOOLTIP_PAD_Y * 2.0;
let mut panel_x = if request.at_pointer {
request.anchor.x
} else {
request.anchor.x - panel_w * 0.5
};
let mut panel_y = request.anchor.y + TOOLTIP_GAP;
if panel_x + panel_w > viewport.width - SCREEN_MARGIN {
panel_x = viewport.width - panel_w - SCREEN_MARGIN;
}
if panel_y + panel_h > viewport.height - SCREEN_MARGIN {
panel_y = request.anchor.y - panel_h - TOOLTIP_GAP * 3.0;
}
panel_x = panel_x.clamp(
SCREEN_MARGIN,
(viewport.width - panel_w - SCREEN_MARGIN).max(SCREEN_MARGIN),
);
panel_y = panel_y.clamp(
SCREEN_MARGIN,
(viewport.height - panel_h - SCREEN_MARGIN).max(SCREEN_MARGIN),
);
ctx.set_fill_color(Color::rgba(0.0, 0.0, 0.0, 0.20));
ctx.begin_path();
ctx.rounded_rect(panel_x + 1.0, panel_y - 1.0, panel_w, panel_h, 5.0);
ctx.fill();
ctx.set_fill_color(v.window_fill);
ctx.begin_path();
ctx.rounded_rect(panel_x, panel_y, panel_w, panel_h, 5.0);
ctx.fill();
ctx.set_stroke_color(v.widget_stroke);
ctx.set_line_width(1.0);
ctx.begin_path();
ctx.rounded_rect(panel_x, panel_y, panel_w, panel_h, 5.0);
ctx.stroke();
for (i, line) in request.lines.iter().enumerate() {
let y = panel_y + panel_h - TOOLTIP_PAD_Y - (i as f64 + 1.0) * line_h + 2.0;
match line.kind {
TooltipLineKind::Text => {
ctx.set_fill_color(v.text_color);
ctx.fill_text(&line.text, panel_x + TOOLTIP_PAD_X, y);
}
TooltipLineKind::Code => {
if let Some(m) = ctx.measure_text(&line.text) {
ctx.set_fill_color(v.track_bg);
ctx.begin_path();
ctx.rounded_rect(
panel_x + TOOLTIP_PAD_X - 3.0,
y - 3.0,
m.width + 6.0,
line_h,
3.0,
);
ctx.fill();
}
ctx.set_fill_color(v.text_color);
ctx.fill_text(&line.text, panel_x + TOOLTIP_PAD_X, y);
}
TooltipLineKind::Link => {
ctx.set_fill_color(v.text_link);
ctx.fill_text(&line.text, panel_x + TOOLTIP_PAD_X, y);
if let Some(m) = ctx.measure_text(&line.text) {
ctx.set_stroke_color(v.text_link);
ctx.set_line_width(1.0);
ctx.begin_path();
ctx.move_to(panel_x + TOOLTIP_PAD_X, y - 2.0);
ctx.line_to(panel_x + TOOLTIP_PAD_X + m.width, y - 2.0);
ctx.stroke();
}
}
}
}
}