pub mod controller;
mod interactive;
mod render;
mod timings;
pub(crate) use render::{begin_tooltip_frame, paint_global_tooltips};
pub use timings::{set_tooltip_timings, tooltip_timings, TooltipTimings};
#[doc(hidden)]
pub use timings::{
advance_tooltip_test_clock, reset_tooltip_test_state, set_tooltip_test_clock,
};
use timings::{last_tooltip_visible_at, note_tooltip_visible, tooltip_now};
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
use web_time::Instant;
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};
use render::{submit_tooltip, TooltipRequest};
const TOOLTIP_INITIAL_DELAY: Duration = timings::DEFAULT_INITIAL_DELAY;
pub(super) const TOOLTIP_FONT_SIZE: f64 = 12.0;
pub(super) const TOOLTIP_PAD_X: f64 = 8.0;
pub(super) const TOOLTIP_PAD_Y: f64 = 6.0;
pub(super) const TOOLTIP_GAP: f64 = 4.0;
pub(super) const POINTER_TOOLTIP_EXTRA_DROP: f64 = 10.0;
pub(super) const SCREEN_MARGIN: f64 = 4.0;
#[derive(Clone)]
pub(super) enum TooltipLineKind {
Text,
Code,
Link,
}
#[derive(Clone)]
pub(super) struct TooltipLine {
pub text: String,
pub kind: TooltipLineKind,
}
pub struct Tooltip {
bounds: Rect,
children: Vec<Box<dyn Widget>>,
base: WidgetBase,
hover_started_at: Option<Instant>,
hovered: bool,
tooltip_visible: bool,
pending_delay: Duration,
tip_shown_at: Option<Instant>,
suppressed: bool,
pointer_down: bool,
cursor: Point,
font: Arc<Font>,
lines: Vec<TooltipLine>,
disabled_lines: Vec<TooltipLine>,
disabled_when: Option<Rc<dyn Fn() -> bool>>,
at_pointer: bool,
interactive: bool,
content: Option<Box<dyn Widget>>,
content_size: Size,
tip_open: bool,
tip_hovered: bool,
tip_panel_local: Option<Rect>,
content_origin_local: Point,
close_requested_at: Option<Instant>,
last_content_path: Option<Vec<usize>>,
}
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_started_at: None,
hovered: false,
tooltip_visible: false,
pending_delay: TOOLTIP_INITIAL_DELAY,
tip_shown_at: None,
suppressed: false,
pointer_down: false,
cursor: Point::ORIGIN,
font,
lines: text_to_lines(text),
disabled_lines: Vec::new(),
disabled_when: None,
at_pointer: true,
interactive: false,
content: None,
content_size: Size::new(0.0, 0.0),
tip_open: false,
tip_hovered: false,
tip_panel_local: None,
content_origin_local: Point::ORIGIN,
close_requested_at: None,
last_content_path: None,
}
}
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 with_interactive_content(mut self, content: Box<dyn Widget>) -> Self {
self.interactive = true;
self.content = Some(content);
self.at_pointer = false;
self
}
pub fn at_pointer(mut self) -> Self {
self.at_pointer = true;
self
}
pub fn at_widget(mut self) -> Self {
self.at_pointer = false;
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 effective_delay(&self) -> Duration {
let t = tooltip_timings();
match last_tooltip_visible_at() {
Some(at) if tooltip_now().saturating_duration_since(at) <= t.reshow_window() => {
t.reshow_delay
}
_ => t.initial_delay,
}
}
fn should_show_tip(&self) -> bool {
if self.suppressed || self.pointer_down || !self.hovered {
return false;
}
match self.hover_started_at {
Some(started) => {
tooltip_now().saturating_duration_since(started) >= self.pending_delay
}
None => false,
}
}
fn update_visibility(&mut self) -> bool {
if self.tooltip_visible {
if let Some(shown) = self.tip_shown_at {
let autopop = tooltip_timings().autopop;
let elapsed = tooltip_now().saturating_duration_since(shown);
if elapsed >= autopop {
self.hide_tip();
self.suppressed = true;
return false;
}
crate::animation::request_draw_after_tagged(
autopop - elapsed,
"tooltip.wrapper.autopop_rearm",
);
}
}
if self.should_show_tip() {
if !self.tooltip_visible {
self.tooltip_visible = true;
self.tip_shown_at = Some(tooltip_now());
crate::animation::request_draw_tagged("tooltip.wrapper.show");
crate::animation::request_draw_after_tagged(
tooltip_timings().autopop,
"tooltip.wrapper.autopop_arm",
);
}
note_tooltip_visible();
return true;
}
if self.tooltip_visible {
self.hide_tip();
} else if let Some(remaining) = self.remaining_delay() {
if remaining.is_zero() {
crate::animation::request_draw_tagged("tooltip.wrapper.remaining_zero");
} else {
crate::animation::request_draw_after_tagged(
remaining,
"tooltip.wrapper.remaining",
);
}
}
false
}
fn remaining_delay(&self) -> Option<Duration> {
if !self.hovered || self.suppressed || self.pointer_down {
return None;
}
let elapsed = tooltip_now().saturating_duration_since(self.hover_started_at?);
Some(self.pending_delay.saturating_sub(elapsed))
}
fn hide_tip(&mut self) {
if self.tooltip_visible {
crate::animation::request_draw();
}
self.tooltip_visible = false;
self.tip_shown_at = None;
}
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 widget_base(&self) -> Option<&WidgetBase> {
Some(&self.base)
}
fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
Some(&mut self.base)
}
fn h_anchor(&self) -> HAnchor {
self.base.h_anchor
}
fn v_anchor(&self) -> VAnchor {
self.base.v_anchor
}
fn min_size(&self) -> Size {
self.children
.first()
.map(|c| c.min_size())
.unwrap_or(Size::ZERO)
}
fn max_size(&self) -> Size {
self.children
.first()
.map(|c| c.max_size())
.unwrap_or(Size::MAX)
}
fn properties(&self) -> Vec<(&'static str, String)> {
vec![(
"tooltip",
self.lines
.iter()
.map(|l| l.text.as_str())
.collect::<Vec<_>>()
.join("\n"),
)]
}
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.interactive {
return;
}
if !self.update_visibility() {
return;
}
let anchor = if self.at_pointer {
current_mouse_world().unwrap_or(self.cursor)
} else {
let mut x = self.bounds.width * 0.5;
let mut y = 0.0;
ctx.root_transform().transform(&mut x, &mut y);
Point::new(x, y)
};
submit_tooltip(TooltipRequest {
font: Arc::clone(&self.font),
lines: self.active_lines(),
anchor,
at_pointer: self.at_pointer,
});
}
fn paint_global_overlay(&mut self, ctx: &mut dyn DrawCtx) {
if self.interactive {
self.paint_interactive_tip(ctx);
}
}
fn hit_test_global_overlay(&self, local_pos: Point) -> bool {
self.interactive && self.interactive_hit(local_pos)
}
fn on_unconsumed_key(
&mut self,
key: &crate::event::Key,
_modifiers: crate::event::Modifiers,
) -> EventResult {
if self.interactive {
self.interactive_unconsumed_key(key)
} else {
EventResult::Ignored
}
}
fn on_event(&mut self, event: &Event) -> EventResult {
if self.interactive {
return self.on_interactive_event(event);
}
match event {
Event::MouseMove { pos } => {
let was = self.hovered;
self.hovered = self.hit_test(*pos);
self.cursor = *pos;
if self.hovered && !was {
self.hover_started_at = Some(tooltip_now());
self.pending_delay = self.effective_delay();
self.suppressed = false;
crate::animation::request_draw_after_tagged(
self.pending_delay,
"tooltip.wrapper.enter_arm",
);
} else if !self.hovered && was {
self.hover_started_at = None;
self.suppressed = false;
self.hide_tip();
}
if self.hovered != was {
crate::animation::request_draw();
}
self.children
.first_mut()
.map(|child| child.on_event(event))
.unwrap_or(EventResult::Ignored)
}
Event::MouseDown { .. } => {
self.pointer_down = true;
self.suppressed = true;
self.hide_tip();
self.children
.first_mut()
.map(|child| child.on_event(event))
.unwrap_or(EventResult::Ignored)
}
Event::MouseUp { .. } => {
self.pointer_down = false;
self.children
.first_mut()
.map(|child| child.on_event(event))
.unwrap_or(EventResult::Ignored)
}
Event::MouseWheel { .. } => {
self.hovered = false;
self.hover_started_at = None;
self.suppressed = false;
self.hide_tip();
self.children
.first_mut()
.map(|child| child.on_event(event))
.unwrap_or(EventResult::Ignored)
}
_ => self
.children
.first_mut()
.map(|child| child.on_event(event))
.unwrap_or(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()
}
#[cfg(test)]
mod tests;