presentar_widgets/
tooltip.rs

1//! Tooltip widget for contextual hover information.
2
3use presentar_core::{
4    widget::{AccessibleRole, LayoutResult, TextStyle},
5    Canvas, Color, Constraints, Event, Point, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9
10/// Tooltip placement relative to the anchor element.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
12pub enum TooltipPlacement {
13    /// Above the anchor
14    #[default]
15    Top,
16    /// Below the anchor
17    Bottom,
18    /// Left of the anchor
19    Left,
20    /// Right of the anchor
21    Right,
22    /// Top left corner
23    TopLeft,
24    /// Top right corner
25    TopRight,
26    /// Bottom left corner
27    BottomLeft,
28    /// Bottom right corner
29    BottomRight,
30}
31
32/// Tooltip widget for showing contextual information on hover.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Tooltip {
35    /// Tooltip text content
36    content: String,
37    /// Placement preference
38    placement: TooltipPlacement,
39    /// Show delay in milliseconds
40    delay_ms: u32,
41    /// Whether tooltip is currently visible
42    visible: bool,
43    /// Background color
44    background: Color,
45    /// Text color
46    text_color: Color,
47    /// Border color
48    border_color: Color,
49    /// Border width
50    border_width: f32,
51    /// Corner radius
52    corner_radius: f32,
53    /// Padding
54    padding: f32,
55    /// Arrow size
56    arrow_size: f32,
57    /// Show arrow
58    show_arrow: bool,
59    /// Maximum width
60    max_width: Option<f32>,
61    /// Text size
62    text_size: f32,
63    /// Accessible name
64    accessible_name_value: Option<String>,
65    /// Test ID
66    test_id_value: Option<String>,
67    /// Anchor bounds (for positioning)
68    #[serde(skip)]
69    anchor_bounds: Rect,
70    /// Cached bounds
71    #[serde(skip)]
72    bounds: Rect,
73}
74
75impl Default for Tooltip {
76    fn default() -> Self {
77        Self {
78            content: String::new(),
79            placement: TooltipPlacement::Top,
80            delay_ms: 200,
81            visible: false,
82            background: Color::new(0.15, 0.15, 0.15, 0.95),
83            text_color: Color::WHITE,
84            border_color: Color::new(0.3, 0.3, 0.3, 1.0),
85            border_width: 0.0,
86            corner_radius: 4.0,
87            padding: 8.0,
88            arrow_size: 6.0,
89            show_arrow: true,
90            max_width: Some(250.0),
91            text_size: 12.0,
92            accessible_name_value: None,
93            test_id_value: None,
94            anchor_bounds: Rect::default(),
95            bounds: Rect::default(),
96        }
97    }
98}
99
100impl Tooltip {
101    /// Create a new tooltip.
102    #[must_use]
103    pub fn new(content: impl Into<String>) -> Self {
104        Self {
105            content: content.into(),
106            ..Self::default()
107        }
108    }
109
110    /// Set the content.
111    #[must_use]
112    pub fn content(mut self, content: impl Into<String>) -> Self {
113        self.content = content.into();
114        self
115    }
116
117    /// Set the placement.
118    #[must_use]
119    pub const fn placement(mut self, placement: TooltipPlacement) -> Self {
120        self.placement = placement;
121        self
122    }
123
124    /// Set the show delay in milliseconds.
125    #[must_use]
126    pub const fn delay_ms(mut self, ms: u32) -> Self {
127        self.delay_ms = ms;
128        self
129    }
130
131    /// Set visibility.
132    #[must_use]
133    pub const fn visible(mut self, visible: bool) -> Self {
134        self.visible = visible;
135        self
136    }
137
138    /// Set background color.
139    #[must_use]
140    pub const fn background(mut self, color: Color) -> Self {
141        self.background = color;
142        self
143    }
144
145    /// Set text color.
146    #[must_use]
147    pub const fn text_color(mut self, color: Color) -> Self {
148        self.text_color = color;
149        self
150    }
151
152    /// Set border color.
153    #[must_use]
154    pub const fn border_color(mut self, color: Color) -> Self {
155        self.border_color = color;
156        self
157    }
158
159    /// Set border width.
160    #[must_use]
161    pub fn border_width(mut self, width: f32) -> Self {
162        self.border_width = width.max(0.0);
163        self
164    }
165
166    /// Set corner radius.
167    #[must_use]
168    pub fn corner_radius(mut self, radius: f32) -> Self {
169        self.corner_radius = radius.max(0.0);
170        self
171    }
172
173    /// Set padding.
174    #[must_use]
175    pub fn padding(mut self, padding: f32) -> Self {
176        self.padding = padding.max(0.0);
177        self
178    }
179
180    /// Set arrow size.
181    #[must_use]
182    pub fn arrow_size(mut self, size: f32) -> Self {
183        self.arrow_size = size.max(0.0);
184        self
185    }
186
187    /// Set whether to show arrow.
188    #[must_use]
189    pub const fn show_arrow(mut self, show: bool) -> Self {
190        self.show_arrow = show;
191        self
192    }
193
194    /// Set maximum width.
195    #[must_use]
196    pub fn max_width(mut self, width: f32) -> Self {
197        self.max_width = Some(width.max(50.0));
198        self
199    }
200
201    /// Remove maximum width constraint.
202    #[must_use]
203    pub const fn no_max_width(mut self) -> Self {
204        self.max_width = None;
205        self
206    }
207
208    /// Set text size.
209    #[must_use]
210    pub fn text_size(mut self, size: f32) -> Self {
211        self.text_size = size.max(8.0);
212        self
213    }
214
215    /// Set anchor bounds for positioning.
216    #[must_use]
217    pub const fn anchor(mut self, bounds: Rect) -> Self {
218        self.anchor_bounds = bounds;
219        self
220    }
221
222    /// Set accessible name.
223    #[must_use]
224    pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
225        self.accessible_name_value = Some(name.into());
226        self
227    }
228
229    /// Set test ID.
230    #[must_use]
231    pub fn test_id(mut self, id: impl Into<String>) -> Self {
232        self.test_id_value = Some(id.into());
233        self
234    }
235
236    /// Get the content.
237    #[must_use]
238    pub fn get_content(&self) -> &str {
239        &self.content
240    }
241
242    /// Get the placement.
243    #[must_use]
244    pub const fn get_placement(&self) -> TooltipPlacement {
245        self.placement
246    }
247
248    /// Get the delay in milliseconds.
249    #[must_use]
250    pub const fn get_delay_ms(&self) -> u32 {
251        self.delay_ms
252    }
253
254    /// Check if visible.
255    #[must_use]
256    pub const fn is_visible(&self) -> bool {
257        self.visible
258    }
259
260    /// Get the anchor bounds.
261    #[must_use]
262    pub const fn get_anchor(&self) -> Rect {
263        self.anchor_bounds
264    }
265
266    /// Show the tooltip.
267    pub fn show(&mut self) {
268        self.visible = true;
269    }
270
271    /// Hide the tooltip.
272    pub fn hide(&mut self) {
273        self.visible = false;
274    }
275
276    /// Toggle visibility.
277    pub fn toggle(&mut self) {
278        self.visible = !self.visible;
279    }
280
281    /// Set anchor bounds (mutable).
282    pub fn set_anchor(&mut self, bounds: Rect) {
283        self.anchor_bounds = bounds;
284    }
285
286    /// Estimate text width.
287    fn estimate_text_width(&self) -> f32 {
288        // Approximate: chars * text_size * 0.6
289        let char_width = self.text_size * 0.6;
290        self.content.len() as f32 * char_width
291    }
292
293    /// Calculate tooltip size.
294    fn calculate_size(&self) -> Size {
295        let text_width = self.estimate_text_width();
296        let max_text = self.max_width.map(|m| self.padding.mul_add(-2.0, m));
297
298        let content_width = match max_text {
299            Some(max) if text_width > max => max,
300            _ => text_width,
301        };
302
303        let lines = if let Some(max) = max_text {
304            (text_width / max).ceil().max(1.0)
305        } else {
306            1.0
307        };
308
309        let content_height = lines * self.text_size * 1.2;
310
311        Size::new(
312            self.padding.mul_add(2.0, content_width),
313            self.padding.mul_add(2.0, content_height),
314        )
315    }
316
317    /// Calculate tooltip position based on placement and anchor.
318    fn calculate_position(&self, size: Size) -> Point {
319        let anchor = self.anchor_bounds;
320        let arrow_offset = if self.show_arrow {
321            self.arrow_size
322        } else {
323            0.0
324        };
325
326        match self.placement {
327            TooltipPlacement::Top => Point::new(
328                anchor.x + (anchor.width - size.width) / 2.0,
329                anchor.y - size.height - arrow_offset,
330            ),
331            TooltipPlacement::Bottom => Point::new(
332                anchor.x + (anchor.width - size.width) / 2.0,
333                anchor.y + anchor.height + arrow_offset,
334            ),
335            TooltipPlacement::Left => Point::new(
336                anchor.x - size.width - arrow_offset,
337                anchor.y + (anchor.height - size.height) / 2.0,
338            ),
339            TooltipPlacement::Right => Point::new(
340                anchor.x + anchor.width + arrow_offset,
341                anchor.y + (anchor.height - size.height) / 2.0,
342            ),
343            TooltipPlacement::TopLeft => {
344                Point::new(anchor.x, anchor.y - size.height - arrow_offset)
345            }
346            TooltipPlacement::TopRight => Point::new(
347                anchor.x + anchor.width - size.width,
348                anchor.y - size.height - arrow_offset,
349            ),
350            TooltipPlacement::BottomLeft => {
351                Point::new(anchor.x, anchor.y + anchor.height + arrow_offset)
352            }
353            TooltipPlacement::BottomRight => Point::new(
354                anchor.x + anchor.width - size.width,
355                anchor.y + anchor.height + arrow_offset,
356            ),
357        }
358    }
359}
360
361impl Widget for Tooltip {
362    fn type_id(&self) -> TypeId {
363        TypeId::of::<Self>()
364    }
365
366    fn measure(&self, constraints: Constraints) -> Size {
367        if !self.visible || self.content.is_empty() {
368            return Size::ZERO;
369        }
370
371        let size = self.calculate_size();
372        constraints.constrain(size)
373    }
374
375    fn layout(&mut self, _bounds: Rect) -> LayoutResult {
376        if !self.visible || self.content.is_empty() {
377            self.bounds = Rect::default();
378            return LayoutResult { size: Size::ZERO };
379        }
380
381        let size = self.calculate_size();
382        let position = self.calculate_position(size);
383        self.bounds = Rect::new(position.x, position.y, size.width, size.height);
384
385        LayoutResult { size }
386    }
387
388    fn paint(&self, canvas: &mut dyn Canvas) {
389        if !self.visible || self.content.is_empty() {
390            return;
391        }
392
393        // Draw background
394        canvas.fill_rect(self.bounds, self.background);
395
396        // Draw border if needed
397        if self.border_width > 0.0 {
398            canvas.stroke_rect(self.bounds, self.border_color, self.border_width);
399        }
400
401        // Draw arrow
402        if self.show_arrow {
403            let arrow_rect = match self.placement {
404                TooltipPlacement::Top | TooltipPlacement::TopLeft | TooltipPlacement::TopRight => {
405                    let cx = self.bounds.x + self.bounds.width / 2.0;
406                    Rect::new(
407                        cx - self.arrow_size,
408                        self.bounds.y + self.bounds.height,
409                        self.arrow_size * 2.0,
410                        self.arrow_size,
411                    )
412                }
413                TooltipPlacement::Bottom
414                | TooltipPlacement::BottomLeft
415                | TooltipPlacement::BottomRight => {
416                    let cx = self.bounds.x + self.bounds.width / 2.0;
417                    Rect::new(
418                        cx - self.arrow_size,
419                        self.bounds.y - self.arrow_size,
420                        self.arrow_size * 2.0,
421                        self.arrow_size,
422                    )
423                }
424                TooltipPlacement::Left => {
425                    let cy = self.bounds.y + self.bounds.height / 2.0;
426                    Rect::new(
427                        self.bounds.x + self.bounds.width,
428                        cy - self.arrow_size,
429                        self.arrow_size,
430                        self.arrow_size * 2.0,
431                    )
432                }
433                TooltipPlacement::Right => {
434                    let cy = self.bounds.y + self.bounds.height / 2.0;
435                    Rect::new(
436                        self.bounds.x - self.arrow_size,
437                        cy - self.arrow_size,
438                        self.arrow_size,
439                        self.arrow_size * 2.0,
440                    )
441                }
442            };
443            canvas.fill_rect(arrow_rect, self.background);
444        }
445
446        // Draw text
447        let text_style = TextStyle {
448            size: self.text_size,
449            color: self.text_color,
450            ..TextStyle::default()
451        };
452
453        canvas.draw_text(
454            &self.content,
455            Point::new(
456                self.bounds.x + self.padding,
457                self.bounds.y + self.padding + self.text_size,
458            ),
459            &text_style,
460        );
461    }
462
463    fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
464        // Tooltip doesn't handle events directly
465        // Visibility is controlled by the parent/anchor widget
466        if matches!(event, Event::MouseLeave) {
467            self.hide();
468        }
469        None
470    }
471
472    fn children(&self) -> &[Box<dyn Widget>] {
473        &[]
474    }
475
476    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
477        &mut []
478    }
479
480    fn is_interactive(&self) -> bool {
481        false // Tooltip itself is not interactive
482    }
483
484    fn is_focusable(&self) -> bool {
485        false
486    }
487
488    fn accessible_name(&self) -> Option<&str> {
489        self.accessible_name_value
490            .as_deref()
491            .or(Some(&self.content))
492    }
493
494    fn accessible_role(&self) -> AccessibleRole {
495        AccessibleRole::Generic // Tooltip role
496    }
497
498    fn test_id(&self) -> Option<&str> {
499        self.test_id_value.as_deref()
500    }
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506
507    // ===== TooltipPlacement Tests =====
508
509    #[test]
510    fn test_tooltip_placement_default() {
511        assert_eq!(TooltipPlacement::default(), TooltipPlacement::Top);
512    }
513
514    #[test]
515    fn test_tooltip_placement_variants() {
516        let placements = [
517            TooltipPlacement::Top,
518            TooltipPlacement::Bottom,
519            TooltipPlacement::Left,
520            TooltipPlacement::Right,
521            TooltipPlacement::TopLeft,
522            TooltipPlacement::TopRight,
523            TooltipPlacement::BottomLeft,
524            TooltipPlacement::BottomRight,
525        ];
526        assert_eq!(placements.len(), 8);
527    }
528
529    // ===== Tooltip Construction Tests =====
530
531    #[test]
532    fn test_tooltip_new() {
533        let tooltip = Tooltip::new("Help text");
534        assert_eq!(tooltip.get_content(), "Help text");
535        assert!(!tooltip.is_visible());
536    }
537
538    #[test]
539    fn test_tooltip_default() {
540        let tooltip = Tooltip::default();
541        assert!(tooltip.content.is_empty());
542        assert_eq!(tooltip.placement, TooltipPlacement::Top);
543        assert_eq!(tooltip.delay_ms, 200);
544        assert!(!tooltip.visible);
545    }
546
547    #[test]
548    fn test_tooltip_builder() {
549        let tooltip = Tooltip::new("Click to submit")
550            .placement(TooltipPlacement::Bottom)
551            .delay_ms(500)
552            .visible(true)
553            .background(Color::BLACK)
554            .text_color(Color::WHITE)
555            .border_color(Color::RED)
556            .border_width(1.0)
557            .corner_radius(8.0)
558            .padding(12.0)
559            .arrow_size(8.0)
560            .show_arrow(true)
561            .max_width(300.0)
562            .text_size(14.0)
563            .accessible_name("Submit button tooltip")
564            .test_id("submit-tooltip");
565
566        assert_eq!(tooltip.get_content(), "Click to submit");
567        assert_eq!(tooltip.get_placement(), TooltipPlacement::Bottom);
568        assert_eq!(tooltip.get_delay_ms(), 500);
569        assert!(tooltip.is_visible());
570        assert_eq!(
571            Widget::accessible_name(&tooltip),
572            Some("Submit button tooltip")
573        );
574        assert_eq!(Widget::test_id(&tooltip), Some("submit-tooltip"));
575    }
576
577    #[test]
578    fn test_tooltip_content() {
579        let tooltip = Tooltip::new("old").content("new");
580        assert_eq!(tooltip.get_content(), "new");
581    }
582
583    // ===== Visibility Tests =====
584
585    #[test]
586    fn test_tooltip_show() {
587        let mut tooltip = Tooltip::new("Text");
588        assert!(!tooltip.is_visible());
589        tooltip.show();
590        assert!(tooltip.is_visible());
591    }
592
593    #[test]
594    fn test_tooltip_hide() {
595        let mut tooltip = Tooltip::new("Text").visible(true);
596        assert!(tooltip.is_visible());
597        tooltip.hide();
598        assert!(!tooltip.is_visible());
599    }
600
601    #[test]
602    fn test_tooltip_toggle() {
603        let mut tooltip = Tooltip::new("Text");
604        assert!(!tooltip.is_visible());
605        tooltip.toggle();
606        assert!(tooltip.is_visible());
607        tooltip.toggle();
608        assert!(!tooltip.is_visible());
609    }
610
611    // ===== Anchor Tests =====
612
613    #[test]
614    fn test_tooltip_anchor() {
615        let anchor = Rect::new(100.0, 100.0, 80.0, 30.0);
616        let tooltip = Tooltip::new("Help").anchor(anchor);
617        assert_eq!(tooltip.get_anchor(), anchor);
618    }
619
620    #[test]
621    fn test_tooltip_set_anchor() {
622        let mut tooltip = Tooltip::new("Help");
623        let anchor = Rect::new(50.0, 50.0, 100.0, 40.0);
624        tooltip.set_anchor(anchor);
625        assert_eq!(tooltip.get_anchor(), anchor);
626    }
627
628    // ===== Dimension Constraints Tests =====
629
630    #[test]
631    fn test_tooltip_border_width_min() {
632        let tooltip = Tooltip::new("Text").border_width(-5.0);
633        assert_eq!(tooltip.border_width, 0.0);
634    }
635
636    #[test]
637    fn test_tooltip_corner_radius_min() {
638        let tooltip = Tooltip::new("Text").corner_radius(-5.0);
639        assert_eq!(tooltip.corner_radius, 0.0);
640    }
641
642    #[test]
643    fn test_tooltip_padding_min() {
644        let tooltip = Tooltip::new("Text").padding(-5.0);
645        assert_eq!(tooltip.padding, 0.0);
646    }
647
648    #[test]
649    fn test_tooltip_arrow_size_min() {
650        let tooltip = Tooltip::new("Text").arrow_size(-5.0);
651        assert_eq!(tooltip.arrow_size, 0.0);
652    }
653
654    #[test]
655    fn test_tooltip_max_width_min() {
656        let tooltip = Tooltip::new("Text").max_width(10.0);
657        assert_eq!(tooltip.max_width, Some(50.0));
658    }
659
660    #[test]
661    fn test_tooltip_no_max_width() {
662        let tooltip = Tooltip::new("Text").max_width(200.0).no_max_width();
663        assert!(tooltip.max_width.is_none());
664    }
665
666    #[test]
667    fn test_tooltip_text_size_min() {
668        let tooltip = Tooltip::new("Text").text_size(2.0);
669        assert_eq!(tooltip.text_size, 8.0);
670    }
671
672    // ===== Size Calculation Tests =====
673
674    #[test]
675    fn test_tooltip_estimate_text_width() {
676        let tooltip = Tooltip::new("Hello").text_size(12.0);
677        let width = tooltip.estimate_text_width();
678        // 5 chars * 12 * 0.6 = 36
679        assert!((width - 36.0).abs() < 0.1);
680    }
681
682    #[test]
683    fn test_tooltip_calculate_size() {
684        let tooltip = Tooltip::new("Test").padding(10.0).text_size(12.0);
685        let size = tooltip.calculate_size();
686        assert!(size.width > 0.0);
687        assert!(size.height > 0.0);
688    }
689
690    // ===== Position Calculation Tests =====
691
692    #[test]
693    fn test_tooltip_position_top() {
694        let tooltip = Tooltip::new("Text")
695            .placement(TooltipPlacement::Top)
696            .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
697            .show_arrow(true)
698            .arrow_size(6.0);
699
700        let size = Size::new(50.0, 24.0);
701        let pos = tooltip.calculate_position(size);
702
703        // Should be above anchor, centered
704        assert!(pos.y < 100.0);
705        assert!(pos.x > 100.0); // Offset for centering
706    }
707
708    #[test]
709    fn test_tooltip_position_bottom() {
710        let tooltip = Tooltip::new("Text")
711            .placement(TooltipPlacement::Bottom)
712            .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
713            .show_arrow(true)
714            .arrow_size(6.0);
715
716        let size = Size::new(50.0, 24.0);
717        let pos = tooltip.calculate_position(size);
718
719        // Should be below anchor
720        assert!(pos.y > 130.0); // 100 + 30 height + arrow
721    }
722
723    #[test]
724    fn test_tooltip_position_left() {
725        let tooltip = Tooltip::new("Text")
726            .placement(TooltipPlacement::Left)
727            .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
728            .show_arrow(true)
729            .arrow_size(6.0);
730
731        let size = Size::new(50.0, 24.0);
732        let pos = tooltip.calculate_position(size);
733
734        // Should be to the left
735        assert!(pos.x < 100.0 - 50.0);
736    }
737
738    #[test]
739    fn test_tooltip_position_right() {
740        let tooltip = Tooltip::new("Text")
741            .placement(TooltipPlacement::Right)
742            .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
743            .show_arrow(true)
744            .arrow_size(6.0);
745
746        let size = Size::new(50.0, 24.0);
747        let pos = tooltip.calculate_position(size);
748
749        // Should be to the right
750        assert!(pos.x > 180.0); // 100 + 80 width + arrow
751    }
752
753    // ===== Widget Trait Tests =====
754
755    #[test]
756    fn test_tooltip_type_id() {
757        let tooltip = Tooltip::new("Text");
758        assert_eq!(Widget::type_id(&tooltip), TypeId::of::<Tooltip>());
759    }
760
761    #[test]
762    fn test_tooltip_measure_invisible() {
763        let tooltip = Tooltip::new("Text").visible(false);
764        let size = tooltip.measure(Constraints::loose(Size::new(500.0, 500.0)));
765        assert_eq!(size, Size::ZERO);
766    }
767
768    #[test]
769    fn test_tooltip_measure_empty() {
770        let tooltip = Tooltip::default().visible(true);
771        let size = tooltip.measure(Constraints::loose(Size::new(500.0, 500.0)));
772        assert_eq!(size, Size::ZERO);
773    }
774
775    #[test]
776    fn test_tooltip_measure_visible() {
777        let tooltip = Tooltip::new("Some helpful text").visible(true);
778        let size = tooltip.measure(Constraints::loose(Size::new(500.0, 500.0)));
779        assert!(size.width > 0.0);
780        assert!(size.height > 0.0);
781    }
782
783    #[test]
784    fn test_tooltip_layout_invisible() {
785        let mut tooltip = Tooltip::new("Text").visible(false);
786        let result = tooltip.layout(Rect::new(0.0, 0.0, 200.0, 100.0));
787        assert_eq!(result.size, Size::ZERO);
788    }
789
790    #[test]
791    fn test_tooltip_layout_visible() {
792        let mut tooltip = Tooltip::new("Text")
793            .visible(true)
794            .anchor(Rect::new(100.0, 100.0, 80.0, 30.0));
795        let result = tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
796        assert!(result.size.width > 0.0);
797        assert!(result.size.height > 0.0);
798    }
799
800    #[test]
801    fn test_tooltip_children() {
802        let tooltip = Tooltip::new("Text");
803        assert!(tooltip.children().is_empty());
804    }
805
806    #[test]
807    fn test_tooltip_is_interactive() {
808        let tooltip = Tooltip::new("Text");
809        assert!(!tooltip.is_interactive());
810    }
811
812    #[test]
813    fn test_tooltip_is_focusable() {
814        let tooltip = Tooltip::new("Text");
815        assert!(!tooltip.is_focusable());
816    }
817
818    #[test]
819    fn test_tooltip_accessible_role() {
820        let tooltip = Tooltip::new("Text");
821        assert_eq!(tooltip.accessible_role(), AccessibleRole::Generic);
822    }
823
824    #[test]
825    fn test_tooltip_accessible_name_default() {
826        let tooltip = Tooltip::new("Help text");
827        // Falls back to content if no explicit name
828        assert_eq!(Widget::accessible_name(&tooltip), Some("Help text"));
829    }
830
831    #[test]
832    fn test_tooltip_accessible_name_explicit() {
833        let tooltip = Tooltip::new("Help text").accessible_name("Explicit name");
834        assert_eq!(Widget::accessible_name(&tooltip), Some("Explicit name"));
835    }
836
837    #[test]
838    fn test_tooltip_test_id() {
839        let tooltip = Tooltip::new("Text").test_id("help-tooltip");
840        assert_eq!(Widget::test_id(&tooltip), Some("help-tooltip"));
841    }
842
843    // ===== Event Tests =====
844
845    #[test]
846    fn test_tooltip_mouse_leave_hides() {
847        let mut tooltip = Tooltip::new("Text").visible(true);
848        assert!(tooltip.is_visible());
849
850        tooltip.event(&Event::MouseLeave);
851        assert!(!tooltip.is_visible());
852    }
853
854    #[test]
855    fn test_tooltip_stays_hidden_on_other_events() {
856        let mut tooltip = Tooltip::new("Text").visible(false);
857        // Other events don't affect visibility - delay handled externally
858        tooltip.event(&Event::MouseMove {
859            position: Point::new(0.0, 0.0),
860        });
861        assert!(!tooltip.is_visible());
862    }
863
864    // ===== Color Tests =====
865
866    #[test]
867    fn test_tooltip_colors() {
868        let tooltip = Tooltip::new("Text")
869            .background(Color::BLUE)
870            .text_color(Color::RED)
871            .border_color(Color::GREEN);
872
873        assert_eq!(tooltip.background, Color::BLUE);
874        assert_eq!(tooltip.text_color, Color::RED);
875        assert_eq!(tooltip.border_color, Color::GREEN);
876    }
877}