use crate::compat::{String, Vec};
use crate::core::{Color, 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, GenericSignal};
use crate::widget::capability::coercion::{expect_bool, expect_string_list};
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, expect_point, point_value, surface_ink};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TooltipLine {
pub text: String,
pub color: Option<Color>,
}
impl TooltipLine {
pub fn new(text: impl Into<String>) -> Self {
Self { text: text.into(), color: None }
}
pub fn colored(text: impl Into<String>, color: Color) -> Self {
Self { text: text.into(), color: Some(color) }
}
}
pub struct ChartTooltip {
base: BaseWidget,
anchor: Point,
offset: Point,
follow_pointer: bool,
content: Vec<TooltipLine>,
visible: bool,
pub shown: GenericSignal,
pub hidden: GenericSignal,
}
impl ChartTooltip {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::ChartTooltip, geometry, "ChartTooltip"),
anchor: Point::new(0, 0),
offset: Point::new(12, 18),
follow_pointer: true,
content: Vec::new(),
visible: false,
shown: GenericSignal::new(),
hidden: GenericSignal::new(),
}
}
pub fn show_at(&mut self, anchor: Point, content: Vec<TooltipLine>) {
self.anchor = anchor;
self.content = content;
let was_visible = self.visible;
self.visible = true;
self.base.request_redraw();
if !was_visible {
self.shown.emit();
}
}
pub fn hide(&mut self) {
if !self.visible {
return;
}
self.visible = false;
self.base.request_redraw();
self.hidden.emit();
}
pub fn is_visible(&self) -> bool {
self.visible
}
pub fn anchor(&self) -> Point {
self.anchor
}
pub fn set_anchor(&mut self, anchor: Point) {
if self.anchor != anchor {
self.anchor = anchor;
self.base.request_redraw();
}
}
pub fn offset(&self) -> Point {
self.offset
}
pub fn set_offset(&mut self, offset: Point) {
if self.offset != offset {
self.offset = offset;
self.base.request_redraw();
}
}
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_content(&mut self, content: Vec<TooltipLine>) {
self.content = content;
self.base.request_redraw();
}
pub fn content(&self) -> &[TooltipLine] {
&self.content
}
fn bubble_rect(&self, context: &RenderContext, font: &crate::core::Font) -> Rect {
let mut width = 40;
let mut height = 8;
for line in &self.content {
let metrics = context.measure_text(&line.text, font);
width = width.max(metrics.width + 16);
height += metrics.height.max(1) + 2;
}
let rect = self.geometry();
let origin = self.anchor + (self.offset.x, self.offset.y);
let max_x = rect.x + rect.width as i32 - width as i32;
let max_y = rect.y + rect.height as i32 - height as i32;
Rect::new(
clamp_axis(origin.x, rect.x, max_x),
clamp_axis(origin.y, rect.y, max_y),
width,
height,
)
}
}
impl EventHandler for ChartTooltip {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
match event {
Event::MouseMove { pos } | Event::PointerMove { pos, .. } => {
if self.visible && self.follow_pointer {
self.set_anchor(*pos);
}
}
Event::MouseLeave { .. } => {
if self.follow_pointer {
self.hide();
}
}
Event::KeyPress { key: 27, .. } => self.hide(),
_ => {}
}
}
}
impl Draw for ChartTooltip {
fn draw(&mut self, context: &mut RenderContext) {
if !self.visible || self.content.is_empty() {
let rect = self.geometry();
let style = self.style();
let (surface, ink) = surface_ink(style);
context.fill_rect(rect, surface.with_alpha(40));
context.draw_rect(rect, surface.blend(&ink, 0.20));
return;
}
let style = self.style();
let (surface, ink) = surface_ink(style);
let font = effective_font(style);
let bubble = self.bubble_rect(context, font);
let fill = ink.blend(&surface, 0.10);
let shadow = Rect::new(bubble.x + 2, bubble.y + 2, bubble.width, bubble.height);
context.fill_rect(shadow, Color::BLACK.with_alpha(40));
context.fill_rect(bubble, fill);
context.draw_rect(bubble, fill.contrast_color().with_alpha(50));
let line_height = bubble.height.div_ceil(self.content.len() as u32);
for (index, line) in self.content.iter().enumerate() {
let row = Rect::new(
bubble.x + 8,
bubble.y + 4 + index as u32 as i32 * line_height as i32,
bubble.width.saturating_sub(16),
line_height,
);
let color = line.color.unwrap_or_else(|| fill.contrast_color());
context.draw_text_fitted(
row,
&line.text,
font,
color,
crate::core::HorizontalAlignment::Left,
);
}
}
}
impl Widget for ChartTooltip {
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(220, 64)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
fn event_signal_dyn(&self, name: &str) -> Option<EventSignalRef> {
match name {
"shown" => Some(EventSignalRef::unit("shown", &self.shown)),
"hidden" => Some(EventSignalRef::unit("hidden", &self.hidden)),
_ => None,
}
}
}
impl WidgetProperties for ChartTooltip {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"anchor" => Ok(point_value(self.anchor)),
"offset" => Ok(point_value(self.offset)),
"follow_pointer" => Ok(CapabilityValue::Bool(self.follow_pointer)),
"content" => Ok(CapabilityValue::Tuple(
self.content
.iter()
.map(|line| CapabilityValue::String(line.text.clone()))
.collect(),
)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"anchor" => {
self.set_anchor(expect_point(value)?);
Ok(())
}
"offset" => {
self.set_offset(expect_point(value)?);
Ok(())
}
"follow_pointer" => {
self.set_follow_pointer(expect_bool(value)?);
Ok(())
}
"content" => {
let texts = expect_string_list(value)?;
self.set_content(texts.into_iter().map(TooltipLine::new).collect());
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of!["anchor", "offset", "follow_pointer", "content", BASE_PROPERTY_NAMES]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn showing_a_bubble_publishes_it_and_hiding_clears_it() {
let mut tooltip = ChartTooltip::new(Rect::new(0, 0, 240, 160));
assert!(!tooltip.is_visible());
tooltip.show_at(
Point::new(20, 30),
crate::compat::Vec::from([TooltipLine::new("AAPL 182.10")]),
);
assert!(tooltip.is_visible());
assert_eq!(tooltip.anchor(), Point::new(20, 30));
assert_eq!(tooltip.content().len(), 1);
assert_eq!(tooltip.content()[0].text, "AAPL 182.10");
tooltip.hide();
assert!(!tooltip.is_visible());
}
}