use std::time::Duration;
use crate::color::Color;
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult, Key};
use crate::geometry::{Point, Rect, Size};
use crate::widget::{dispatch_event_dyn, hit_test_subtree, paint_subtree, Widget};
use super::render::current_tooltip_viewport;
use super::timings::{tooltip_now, tooltip_timings};
use super::{Tooltip, SCREEN_MARGIN, TOOLTIP_GAP, TOOLTIP_PAD_X, TOOLTIP_PAD_Y};
pub(super) const TOOLTIP_CLOSE_GRACE: Duration = Duration::from_millis(250);
const INTERACTIVE_MAX_WIDTH: f64 = 320.0;
impl Tooltip {
pub(super) fn update_interactive_state(&mut self) {
if !self.tip_open {
if self.hovered {
if let Some(started) = self.hover_started_at {
let initial = tooltip_timings().initial_delay;
let elapsed = tooltip_now().saturating_duration_since(started);
if elapsed >= initial {
self.tip_open = true;
self.close_requested_at = None;
crate::animation::request_draw_tagged("tooltip.interactive.open");
} else {
crate::animation::request_draw_after_tagged(
initial - elapsed,
"tooltip.interactive.open_arm",
);
}
}
}
return;
}
if self.hovered || self.tip_hovered {
self.close_requested_at = None;
return;
}
match self.close_requested_at {
Some(since) => {
let elapsed = tooltip_now().saturating_duration_since(since);
if elapsed >= TOOLTIP_CLOSE_GRACE {
self.close_tip();
crate::animation::request_draw_tagged("tooltip.interactive.close");
} else {
crate::animation::request_draw_after_tagged(
TOOLTIP_CLOSE_GRACE - elapsed,
"tooltip.interactive.close_grace_rearm",
);
}
}
None => {
self.close_requested_at = Some(tooltip_now());
crate::animation::request_draw_after_tagged(
TOOLTIP_CLOSE_GRACE,
"tooltip.interactive.close_grace_arm",
);
}
}
}
pub(super) fn close_tip(&mut self) {
self.tip_open = false;
self.tip_hovered = false;
self.close_requested_at = None;
self.tip_panel_local = None;
self.hover_started_at = None;
self.clear_content_hover();
}
pub(super) fn layout_content(&mut self) -> Size {
let avail = Size::new(INTERACTIVE_MAX_WIDTH, 4096.0);
if let Some(content) = self.content.as_mut() {
let s = content.layout(avail);
content.set_bounds(Rect::new(0.0, 0.0, s.width, s.height));
self.content_size = s;
s
} else {
Size::new(0.0, 0.0)
}
}
pub(super) fn paint_interactive_tip(&mut self, ctx: &mut dyn DrawCtx) {
self.update_interactive_state();
if !self.tip_open || self.content.is_none() {
return;
}
let inner = self.layout_content();
let panel_w = inner.width + TOOLTIP_PAD_X * 2.0;
let panel_h = inner.height + TOOLTIP_PAD_Y * 2.0;
let mut panel_x = 0.0_f64;
let mut panel_bottom = -TOOLTIP_GAP - panel_h;
let scale = crate::device_scale::device_scale().max(1e-6);
let mut ox = 0.0;
let mut oy = 0.0;
ctx.root_transform().transform(&mut ox, &mut oy);
let ox = ox / scale;
let oy = oy / scale;
let viewport = current_tooltip_viewport();
if viewport.width > 0.0 && viewport.height > 0.0 {
if oy + panel_bottom < SCREEN_MARGIN {
panel_bottom = self.bounds.height + TOOLTIP_GAP;
}
let overflow = (ox + panel_x + panel_w) - (viewport.width - SCREEN_MARGIN);
if overflow > 0.0 {
panel_x -= overflow;
}
let underflow = SCREEN_MARGIN - (ox + panel_x);
if underflow > 0.0 {
panel_x += underflow;
}
}
self.tip_panel_local = Some(Rect::new(panel_x, panel_bottom, panel_w, panel_h));
self.content_origin_local =
Point::new(panel_x + TOOLTIP_PAD_X, panel_bottom + TOOLTIP_PAD_Y);
let v = ctx.visuals();
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_bottom - 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_bottom, 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_bottom, panel_w, panel_h, 5.0);
ctx.stroke();
let origin = self.content_origin_local;
if let Some(content) = self.content.as_mut() {
ctx.save();
ctx.translate(origin.x, origin.y);
paint_subtree(content.as_mut(), ctx);
ctx.restore();
}
}
pub(super) fn interactive_hit(&self, local_pos: Point) -> bool {
self.tip_open
&& self
.tip_panel_local
.map(|r| r.contains(local_pos))
.unwrap_or(false)
}
pub(super) fn interactive_unconsumed_key(&mut self, key: &Key) -> EventResult {
if self.tip_open && matches!(key, Key::Escape) {
self.close_tip();
crate::animation::request_draw();
EventResult::Consumed
} else {
EventResult::Ignored
}
}
pub(super) fn on_interactive_event(&mut self, event: &Event) -> EventResult {
match event {
Event::MouseMove { pos } => {
let was = self.hovered;
self.hovered = self.hit_test(*pos);
self.tip_hovered = self.interactive_hit(*pos);
self.cursor = *pos;
if self.hovered && !was {
self.hover_started_at = Some(tooltip_now());
crate::animation::request_draw_after_tagged(
tooltip_timings().initial_delay,
"tooltip.interactive.enter_arm",
);
}
if self.hovered || self.tip_hovered {
self.close_requested_at = None;
} else if self.tip_open && self.close_requested_at.is_none() {
self.close_requested_at = Some(tooltip_now());
crate::animation::request_draw_after_tagged(
TOOLTIP_CLOSE_GRACE,
"tooltip.interactive.leave_close_arm",
);
}
crate::animation::request_draw();
if self.tip_open && self.tip_hovered {
let cpos = Point::new(
pos.x - self.content_origin_local.x,
pos.y - self.content_origin_local.y,
);
self.forward_move_to_content(cpos);
} else {
self.clear_content_hover();
}
self.children
.first_mut()
.map(|child| child.on_event(event))
.unwrap_or(EventResult::Ignored)
}
Event::MouseDown { .. } | Event::MouseUp { .. } => {
if self.tip_open && self.interactive_hit(pos_of(event)) {
let p = pos_of(event);
let cpos = Point::new(
p.x - self.content_origin_local.x,
p.y - self.content_origin_local.y,
);
return self.forward_click_to_content(event, cpos);
}
self.children
.first_mut()
.map(|child| child.on_event(event))
.unwrap_or(EventResult::Ignored)
}
Event::MouseWheel { .. } => {
self.hovered = false;
self.close_tip();
crate::animation::request_draw();
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 forward_move_to_content(&mut self, cpos: Point) {
let Some(mut content) = self.content.take() else {
return;
};
let new_path = hit_test_subtree(content.as_ref(), cpos);
if new_path != self.last_content_path {
if let Some(old) = self.last_content_path.take() {
let clear = Event::MouseMove {
pos: Point::new(-1.0, -1.0),
};
dispatch_event_dyn(content.as_mut(), &old, &clear, Point::new(-1.0, -1.0));
}
self.last_content_path = new_path.clone();
}
if let Some(path) = new_path {
let ev = Event::MouseMove { pos: cpos };
dispatch_event_dyn(content.as_mut(), &path, &ev, cpos);
}
self.content = Some(content);
}
fn forward_click_to_content(&mut self, event: &Event, cpos: Point) -> EventResult {
let Some(mut content) = self.content.take() else {
return EventResult::Ignored;
};
let result = match hit_test_subtree(content.as_ref(), cpos) {
Some(path) => dispatch_event_dyn(content.as_mut(), &path, event, cpos),
None => EventResult::Ignored,
};
self.content = Some(content);
result
}
fn clear_content_hover(&mut self) {
let Some(old) = self.last_content_path.take() else {
return;
};
if let Some(mut content) = self.content.take() {
let clear = Event::MouseMove {
pos: Point::new(-1.0, -1.0),
};
dispatch_event_dyn(content.as_mut(), &old, &clear, Point::new(-1.0, -1.0));
self.content = Some(content);
}
}
}
fn pos_of(event: &Event) -> Point {
match event {
Event::MouseDown { pos, .. } | Event::MouseUp { pos, .. } => *pos,
_ => Point::ORIGIN,
}
}