use crate::compat::{String, Vec};
use crate::core::{ObjectId, Point, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::impl_draw_bridge;
use crate::impl_widget_property_hooks;
use crate::property_names_of;
use crate::render::RenderContext;
use crate::signal::{EventSignalRef, Signal1};
use crate::widget::capability::coercion::{expect_bool, expect_u32};
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::metrics::effective_font;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use super::helpers::{clamp_axis, surface_ink};
pub struct TooltipManager {
base: BaseWidget,
registrations: Vec<(ObjectId, String)>,
hovered: Option<ObjectId>,
hover_elapsed_ms: u64,
current: Option<ObjectId>,
visible: bool,
shown_at_ms: u64,
now_ms: u64,
pointer: Point,
delay_ms: u32,
duration_ms: u32,
enabled: bool,
follow_pointer: bool,
max_width: u32,
pub tooltip_shown: Signal1<ObjectId>,
pub tooltip_hidden: Signal1<ObjectId>,
}
impl TooltipManager {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::TooltipManager, geometry, "TooltipManager"),
registrations: Vec::new(),
hovered: None,
hover_elapsed_ms: 0,
current: None,
visible: false,
shown_at_ms: 0,
now_ms: 0,
pointer: Point::new(0, 0),
delay_ms: 500,
duration_ms: 3000,
enabled: true,
follow_pointer: true,
max_width: 260,
tooltip_shown: Signal1::new(),
tooltip_hidden: Signal1::new(),
}
}
pub fn register(&mut self, widget_id: ObjectId, text: impl Into<String>) {
let text = text.into();
if let Some(entry) = self.registrations.iter_mut().find(|entry| entry.0 == widget_id) {
entry.1 = text;
} else {
self.registrations.push((widget_id, text));
}
}
pub fn unregister(&mut self, widget_id: ObjectId) {
self.registrations.retain(|entry| entry.0 != widget_id);
if self.current == Some(widget_id) {
self.hide();
}
if self.hovered == Some(widget_id) {
self.hovered = None;
self.hover_elapsed_ms = 0;
}
}
pub fn show_now(&mut self, widget_id: ObjectId) -> bool {
if !self.enabled {
return false;
}
if self.text_for(widget_id).is_none() {
return false;
}
if self.visible && self.current == Some(widget_id) {
return true;
}
self.hide();
self.current = Some(widget_id);
self.visible = true;
self.shown_at_ms = self.now_ms;
self.base.request_redraw();
self.tooltip_shown.emit(widget_id);
true
}
pub fn hide(&mut self) {
let Some(widget_id) = self.current.take() else {
return;
};
self.visible = false;
self.base.request_redraw();
self.tooltip_hidden.emit(widget_id);
}
pub fn tick(&mut self, delta_ms: u64, now_ms: u64) -> bool {
self.now_ms = self.now_ms.max(now_ms);
let before = self.visible;
if !self.enabled {
self.hide();
return before != self.visible;
}
if self.visible
&& self.duration_ms > 0
&& self.now_ms.saturating_sub(self.shown_at_ms) >= u64::from(self.duration_ms)
{
self.hide();
self.hover_elapsed_ms = 0;
}
if !self.visible {
match self.hovered {
Some(widget_id) => {
self.hover_elapsed_ms = self.hover_elapsed_ms.saturating_add(delta_ms);
if self.hover_elapsed_ms >= u64::from(self.delay_ms) {
self.show_now(widget_id);
}
}
None => self.hover_elapsed_ms = 0,
}
}
before != self.visible
}
pub fn note_hover(&mut self, widget_id: ObjectId) {
if self.hovered == Some(widget_id) {
return;
}
self.hovered = Some(widget_id);
self.hover_elapsed_ms = 0;
if self.visible && self.current != Some(widget_id) {
self.hide();
}
}
pub fn note_leave(&mut self) {
self.hovered = None;
self.hover_elapsed_ms = 0;
self.hide();
}
pub fn note_pointer(&mut self, pointer: Point) {
self.pointer = pointer;
if self.visible && self.follow_pointer {
self.base.request_redraw();
}
}
pub fn tooltip(&self) -> Option<&str> {
if !self.visible {
return None;
}
self.current.and_then(|widget_id| self.text_for(widget_id))
}
pub fn active_widget(&self) -> Option<ObjectId> {
self.current
}
pub fn text_for(&self, widget_id: ObjectId) -> Option<&str> {
self.registrations.iter().find(|entry| entry.0 == widget_id).map(|entry| entry.1.as_str())
}
pub fn set_delay_ms(&mut self, delay_ms: u32) {
self.delay_ms = delay_ms;
}
pub fn delay_ms(&self) -> u32 {
self.delay_ms
}
pub fn set_duration_ms(&mut self, duration_ms: u32) {
self.duration_ms = duration_ms;
}
pub fn duration_ms(&self) -> u32 {
self.duration_ms
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
if !enabled {
self.hide();
}
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn set_follow_pointer(&mut self, follow_pointer: bool) {
self.follow_pointer = follow_pointer;
}
pub fn follow_pointer(&self) -> bool {
self.follow_pointer
}
pub fn set_max_width(&mut self, max_width: u32) {
self.max_width = max_width.max(24);
self.base.request_redraw();
}
pub fn max_width(&self) -> u32 {
self.max_width
}
}
impl EventHandler for TooltipManager {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
match event {
Event::MouseMove { pos } | Event::PointerMove { pos, .. } => self.note_pointer(*pos),
Event::MouseLeave { .. } => self.note_leave(),
Event::KeyPress { key: 27, .. } => self.hide(),
_ => {}
}
}
}
impl Draw for TooltipManager {
fn draw(&mut self, context: &mut RenderContext) {
let Some(text) = self.tooltip() else {
let rect = self.geometry();
let style = self.style();
let (surface, ink) = surface_ink(style);
context.fill_rect(rect, surface.with_alpha(32));
context.draw_rect(rect, surface.blend(&ink, 0.18));
return;
};
let style = self.style();
let (surface, ink) = surface_ink(style);
let font = effective_font(style);
let width = context.measure_text(text, font).width.min(self.max_width).max(24);
let height = context.measure_text(text, font).height.max(16) + 8;
let rect = self.geometry();
let origin = if self.follow_pointer {
self.pointer + (12, 20)
} else {
Point::new(rect.x + 8, rect.y + 8)
};
let max_x = rect.x + rect.width as i32 - width as i32;
let max_y = rect.y + rect.height as i32 - height as i32;
let bubble = Rect::new(
clamp_axis(origin.x, rect.x, max_x),
clamp_axis(origin.y, rect.y, max_y),
width,
height,
);
let fill = ink.blend(&surface, 0.10);
context.fill_rect(bubble, fill);
context.draw_rect(bubble, fill.contrast_color().with_alpha(60));
context.draw_text_fitted(
bubble,
text,
font,
fill.contrast_color(),
crate::core::HorizontalAlignment::Center,
);
}
}
impl Widget for TooltipManager {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
Size::new(240, 40)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
fn event_signal_dyn(&self, name: &str) -> Option<EventSignalRef> {
match name {
"tooltip_shown" => {
Some(EventSignalRef::mapped("tooltip_shown", &self.tooltip_shown, |value| {
CapabilityValue::UInt(*value)
}))
}
"tooltip_hidden" => {
Some(EventSignalRef::mapped("tooltip_hidden", &self.tooltip_hidden, |value| {
CapabilityValue::UInt(*value)
}))
}
_ => None,
}
}
}
impl WidgetProperties for TooltipManager {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"delay_ms" => Ok(CapabilityValue::UInt(u64::from(self.delay_ms))),
"duration_ms" => Ok(CapabilityValue::UInt(u64::from(self.duration_ms))),
"enabled" => Ok(CapabilityValue::Bool(self.enabled)),
"follow_pointer" => Ok(CapabilityValue::Bool(self.follow_pointer)),
"max_width" => Ok(CapabilityValue::UInt(u64::from(self.max_width))),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"delay_ms" => {
self.set_delay_ms(expect_u32(value)?);
Ok(())
}
"duration_ms" => {
self.set_duration_ms(expect_u32(value)?);
Ok(())
}
"enabled" => {
self.set_enabled(expect_bool(value)?);
Ok(())
}
"follow_pointer" => {
self.set_follow_pointer(expect_bool(value)?);
Ok(())
}
"max_width" => {
self.set_max_width(expect_u32(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"delay_ms",
"duration_ms",
"enabled",
"follow_pointer",
"max_width",
"visible",
"tooltip",
"geometry"
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_a_registered_widget_can_show_a_tooltip() {
let mut manager = TooltipManager::new(Rect::new(0, 0, 320, 200));
manager.register(11, "Save");
assert_eq!(manager.text_for(11), Some("Save"));
assert!(manager.show_now(11));
assert_eq!(manager.tooltip(), Some("Save"));
manager.hide();
assert!(manager.tooltip().is_none());
assert!(!manager.show_now(12));
}
}