1use presentar_core::{
4 widget::{AccessibleRole, LayoutResult, TextStyle},
5 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
6 Point, Rect, Size, TypeId, Widget,
7};
8use serde::{Deserialize, Serialize};
9use std::any::Any;
10use std::time::Duration;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
14pub enum TooltipPlacement {
15 #[default]
17 Top,
18 Bottom,
20 Left,
22 Right,
24 TopLeft,
26 TopRight,
28 BottomLeft,
30 BottomRight,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct Tooltip {
37 content: String,
39 placement: TooltipPlacement,
41 delay_ms: u32,
43 visible: bool,
45 background: Color,
47 text_color: Color,
49 border_color: Color,
51 border_width: f32,
53 corner_radius: f32,
55 padding: f32,
57 arrow_size: f32,
59 show_arrow: bool,
61 max_width: Option<f32>,
63 text_size: f32,
65 accessible_name_value: Option<String>,
67 test_id_value: Option<String>,
69 #[serde(skip)]
71 anchor_bounds: Rect,
72 #[serde(skip)]
74 bounds: Rect,
75}
76
77impl Default for Tooltip {
78 fn default() -> Self {
79 Self {
80 content: String::new(),
81 placement: TooltipPlacement::Top,
82 delay_ms: 200,
83 visible: false,
84 background: Color::new(0.15, 0.15, 0.15, 0.95),
85 text_color: Color::WHITE,
86 border_color: Color::new(0.3, 0.3, 0.3, 1.0),
87 border_width: 0.0,
88 corner_radius: 4.0,
89 padding: 8.0,
90 arrow_size: 6.0,
91 show_arrow: true,
92 max_width: Some(250.0),
93 text_size: 12.0,
94 accessible_name_value: None,
95 test_id_value: None,
96 anchor_bounds: Rect::default(),
97 bounds: Rect::default(),
98 }
99 }
100}
101
102impl Tooltip {
103 #[must_use]
105 pub fn new(content: impl Into<String>) -> Self {
106 Self {
107 content: content.into(),
108 ..Self::default()
109 }
110 }
111
112 #[must_use]
114 pub fn content(mut self, content: impl Into<String>) -> Self {
115 self.content = content.into();
116 self
117 }
118
119 #[must_use]
121 pub const fn placement(mut self, placement: TooltipPlacement) -> Self {
122 self.placement = placement;
123 self
124 }
125
126 #[must_use]
128 pub const fn delay_ms(mut self, ms: u32) -> Self {
129 self.delay_ms = ms;
130 self
131 }
132
133 #[must_use]
135 pub const fn visible(mut self, visible: bool) -> Self {
136 self.visible = visible;
137 self
138 }
139
140 #[must_use]
142 pub const fn background(mut self, color: Color) -> Self {
143 self.background = color;
144 self
145 }
146
147 #[must_use]
149 pub const fn text_color(mut self, color: Color) -> Self {
150 self.text_color = color;
151 self
152 }
153
154 #[must_use]
156 pub const fn border_color(mut self, color: Color) -> Self {
157 self.border_color = color;
158 self
159 }
160
161 #[must_use]
163 pub fn border_width(mut self, width: f32) -> Self {
164 self.border_width = width.max(0.0);
165 self
166 }
167
168 #[must_use]
170 pub fn corner_radius(mut self, radius: f32) -> Self {
171 self.corner_radius = radius.max(0.0);
172 self
173 }
174
175 #[must_use]
177 pub fn padding(mut self, padding: f32) -> Self {
178 self.padding = padding.max(0.0);
179 self
180 }
181
182 #[must_use]
184 pub fn arrow_size(mut self, size: f32) -> Self {
185 self.arrow_size = size.max(0.0);
186 self
187 }
188
189 #[must_use]
191 pub const fn show_arrow(mut self, show: bool) -> Self {
192 self.show_arrow = show;
193 self
194 }
195
196 #[must_use]
198 pub fn max_width(mut self, width: f32) -> Self {
199 self.max_width = Some(width.max(50.0));
200 self
201 }
202
203 #[must_use]
205 pub const fn no_max_width(mut self) -> Self {
206 self.max_width = None;
207 self
208 }
209
210 #[must_use]
212 pub fn text_size(mut self, size: f32) -> Self {
213 self.text_size = size.max(8.0);
214 self
215 }
216
217 #[must_use]
219 pub const fn anchor(mut self, bounds: Rect) -> Self {
220 self.anchor_bounds = bounds;
221 self
222 }
223
224 #[must_use]
226 pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
227 self.accessible_name_value = Some(name.into());
228 self
229 }
230
231 #[must_use]
233 pub fn test_id(mut self, id: impl Into<String>) -> Self {
234 self.test_id_value = Some(id.into());
235 self
236 }
237
238 #[must_use]
240 pub fn get_content(&self) -> &str {
241 &self.content
242 }
243
244 #[must_use]
246 pub const fn get_placement(&self) -> TooltipPlacement {
247 self.placement
248 }
249
250 #[must_use]
252 pub const fn get_delay_ms(&self) -> u32 {
253 self.delay_ms
254 }
255
256 #[must_use]
258 pub const fn is_visible(&self) -> bool {
259 self.visible
260 }
261
262 #[must_use]
264 pub const fn get_anchor(&self) -> Rect {
265 self.anchor_bounds
266 }
267
268 pub fn show(&mut self) {
270 self.visible = true;
271 }
272
273 pub fn hide(&mut self) {
275 self.visible = false;
276 }
277
278 pub fn toggle(&mut self) {
280 self.visible = !self.visible;
281 }
282
283 pub fn set_anchor(&mut self, bounds: Rect) {
285 self.anchor_bounds = bounds;
286 }
287
288 fn estimate_text_width(&self) -> f32 {
290 let char_width = self.text_size * 0.6;
292 self.content.len() as f32 * char_width
293 }
294
295 fn calculate_size(&self) -> Size {
297 let text_width = self.estimate_text_width();
298 let max_text = self.max_width.map(|m| self.padding.mul_add(-2.0, m));
299
300 let content_width = match max_text {
301 Some(max) if text_width > max => max,
302 _ => text_width,
303 };
304
305 let lines = if let Some(max) = max_text {
306 (text_width / max).ceil().max(1.0)
307 } else {
308 1.0
309 };
310
311 let content_height = lines * self.text_size * 1.2;
312
313 Size::new(
314 self.padding.mul_add(2.0, content_width),
315 self.padding.mul_add(2.0, content_height),
316 )
317 }
318
319 fn calculate_position(&self, size: Size) -> Point {
321 let anchor = self.anchor_bounds;
322 let arrow_offset = if self.show_arrow {
323 self.arrow_size
324 } else {
325 0.0
326 };
327
328 match self.placement {
329 TooltipPlacement::Top => Point::new(
330 anchor.x + (anchor.width - size.width) / 2.0,
331 anchor.y - size.height - arrow_offset,
332 ),
333 TooltipPlacement::Bottom => Point::new(
334 anchor.x + (anchor.width - size.width) / 2.0,
335 anchor.y + anchor.height + arrow_offset,
336 ),
337 TooltipPlacement::Left => Point::new(
338 anchor.x - size.width - arrow_offset,
339 anchor.y + (anchor.height - size.height) / 2.0,
340 ),
341 TooltipPlacement::Right => Point::new(
342 anchor.x + anchor.width + arrow_offset,
343 anchor.y + (anchor.height - size.height) / 2.0,
344 ),
345 TooltipPlacement::TopLeft => {
346 Point::new(anchor.x, anchor.y - size.height - arrow_offset)
347 }
348 TooltipPlacement::TopRight => Point::new(
349 anchor.x + anchor.width - size.width,
350 anchor.y - size.height - arrow_offset,
351 ),
352 TooltipPlacement::BottomLeft => {
353 Point::new(anchor.x, anchor.y + anchor.height + arrow_offset)
354 }
355 TooltipPlacement::BottomRight => Point::new(
356 anchor.x + anchor.width - size.width,
357 anchor.y + anchor.height + arrow_offset,
358 ),
359 }
360 }
361}
362
363impl Widget for Tooltip {
364 fn type_id(&self) -> TypeId {
365 TypeId::of::<Self>()
366 }
367
368 fn measure(&self, constraints: Constraints) -> Size {
369 if !self.visible || self.content.is_empty() {
370 return Size::ZERO;
371 }
372
373 let size = self.calculate_size();
374 constraints.constrain(size)
375 }
376
377 fn layout(&mut self, _bounds: Rect) -> LayoutResult {
378 if !self.visible || self.content.is_empty() {
379 self.bounds = Rect::default();
380 return LayoutResult { size: Size::ZERO };
381 }
382
383 let size = self.calculate_size();
384 let position = self.calculate_position(size);
385 self.bounds = Rect::new(position.x, position.y, size.width, size.height);
386
387 LayoutResult { size }
388 }
389
390 fn paint(&self, canvas: &mut dyn Canvas) {
391 if !self.visible || self.content.is_empty() {
392 return;
393 }
394
395 canvas.fill_rect(self.bounds, self.background);
397
398 if self.border_width > 0.0 {
400 canvas.stroke_rect(self.bounds, self.border_color, self.border_width);
401 }
402
403 if self.show_arrow {
405 let arrow_rect = match self.placement {
406 TooltipPlacement::Top | TooltipPlacement::TopLeft | TooltipPlacement::TopRight => {
407 let cx = self.bounds.x + self.bounds.width / 2.0;
408 Rect::new(
409 cx - self.arrow_size,
410 self.bounds.y + self.bounds.height,
411 self.arrow_size * 2.0,
412 self.arrow_size,
413 )
414 }
415 TooltipPlacement::Bottom
416 | TooltipPlacement::BottomLeft
417 | TooltipPlacement::BottomRight => {
418 let cx = self.bounds.x + self.bounds.width / 2.0;
419 Rect::new(
420 cx - self.arrow_size,
421 self.bounds.y - self.arrow_size,
422 self.arrow_size * 2.0,
423 self.arrow_size,
424 )
425 }
426 TooltipPlacement::Left => {
427 let cy = self.bounds.y + self.bounds.height / 2.0;
428 Rect::new(
429 self.bounds.x + self.bounds.width,
430 cy - self.arrow_size,
431 self.arrow_size,
432 self.arrow_size * 2.0,
433 )
434 }
435 TooltipPlacement::Right => {
436 let cy = self.bounds.y + self.bounds.height / 2.0;
437 Rect::new(
438 self.bounds.x - self.arrow_size,
439 cy - self.arrow_size,
440 self.arrow_size,
441 self.arrow_size * 2.0,
442 )
443 }
444 };
445 canvas.fill_rect(arrow_rect, self.background);
446 }
447
448 let text_style = TextStyle {
450 size: self.text_size,
451 color: self.text_color,
452 ..TextStyle::default()
453 };
454
455 canvas.draw_text(
456 &self.content,
457 Point::new(
458 self.bounds.x + self.padding,
459 self.bounds.y + self.padding + self.text_size,
460 ),
461 &text_style,
462 );
463 }
464
465 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
466 if matches!(event, Event::MouseLeave) {
469 self.hide();
470 }
471 None
472 }
473
474 fn children(&self) -> &[Box<dyn Widget>] {
475 &[]
476 }
477
478 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
479 &mut []
480 }
481
482 fn is_interactive(&self) -> bool {
483 false }
485
486 fn is_focusable(&self) -> bool {
487 false
488 }
489
490 fn accessible_name(&self) -> Option<&str> {
491 self.accessible_name_value
492 .as_deref()
493 .or(Some(&self.content))
494 }
495
496 fn accessible_role(&self) -> AccessibleRole {
497 AccessibleRole::Generic }
499
500 fn test_id(&self) -> Option<&str> {
501 self.test_id_value.as_deref()
502 }
503}
504
505impl Brick for Tooltip {
507 fn brick_name(&self) -> &'static str {
508 "Tooltip"
509 }
510
511 fn assertions(&self) -> &[BrickAssertion] {
512 &[
513 BrickAssertion::MaxLatencyMs(16),
514 BrickAssertion::ContrastRatio(4.5), ]
516 }
517
518 fn budget(&self) -> BrickBudget {
519 BrickBudget::uniform(16)
520 }
521
522 fn verify(&self) -> BrickVerification {
523 let mut passed = Vec::new();
524 let mut failed = Vec::new();
525
526 let contrast = self.background.contrast_ratio(&self.text_color);
528 if contrast >= 4.5 {
529 passed.push(BrickAssertion::ContrastRatio(4.5));
530 } else {
531 failed.push((
532 BrickAssertion::ContrastRatio(4.5),
533 format!("Contrast ratio {contrast:.2}:1 < 4.5:1"),
534 ));
535 }
536
537 passed.push(BrickAssertion::MaxLatencyMs(16));
539
540 BrickVerification {
541 passed,
542 failed,
543 verification_time: Duration::from_micros(10),
544 }
545 }
546
547 fn to_html(&self) -> String {
548 let test_id = self.test_id_value.as_deref().unwrap_or("tooltip");
549 let aria_label = self
550 .accessible_name_value
551 .as_deref()
552 .unwrap_or(&self.content);
553 format!(
554 r#"<div class="brick-tooltip" role="tooltip" data-testid="{}" aria-label="{}">{}</div>"#,
555 test_id, aria_label, self.content
556 )
557 }
558
559 fn to_css(&self) -> String {
560 format!(
561 r#".brick-tooltip {{
562 background: {};
563 color: {};
564 padding: {}px;
565 font-size: {}px;
566 border-radius: {}px;
567 max-width: {}px;
568 position: absolute;
569 z-index: 1000;
570 pointer-events: none;
571}}
572.brick-tooltip[data-visible="false"] {{
573 display: none;
574}}"#,
575 self.background.to_hex(),
576 self.text_color.to_hex(),
577 self.padding,
578 self.text_size,
579 self.corner_radius,
580 self.max_width.unwrap_or(250.0),
581 )
582 }
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588
589 #[test]
592 fn test_tooltip_placement_default() {
593 assert_eq!(TooltipPlacement::default(), TooltipPlacement::Top);
594 }
595
596 #[test]
597 fn test_tooltip_placement_variants() {
598 let placements = [
599 TooltipPlacement::Top,
600 TooltipPlacement::Bottom,
601 TooltipPlacement::Left,
602 TooltipPlacement::Right,
603 TooltipPlacement::TopLeft,
604 TooltipPlacement::TopRight,
605 TooltipPlacement::BottomLeft,
606 TooltipPlacement::BottomRight,
607 ];
608 assert_eq!(placements.len(), 8);
609 }
610
611 #[test]
614 fn test_tooltip_new() {
615 let tooltip = Tooltip::new("Help text");
616 assert_eq!(tooltip.get_content(), "Help text");
617 assert!(!tooltip.is_visible());
618 }
619
620 #[test]
621 fn test_tooltip_default() {
622 let tooltip = Tooltip::default();
623 assert!(tooltip.content.is_empty());
624 assert_eq!(tooltip.placement, TooltipPlacement::Top);
625 assert_eq!(tooltip.delay_ms, 200);
626 assert!(!tooltip.visible);
627 }
628
629 #[test]
630 fn test_tooltip_builder() {
631 let tooltip = Tooltip::new("Click to submit")
632 .placement(TooltipPlacement::Bottom)
633 .delay_ms(500)
634 .visible(true)
635 .background(Color::BLACK)
636 .text_color(Color::WHITE)
637 .border_color(Color::RED)
638 .border_width(1.0)
639 .corner_radius(8.0)
640 .padding(12.0)
641 .arrow_size(8.0)
642 .show_arrow(true)
643 .max_width(300.0)
644 .text_size(14.0)
645 .accessible_name("Submit button tooltip")
646 .test_id("submit-tooltip");
647
648 assert_eq!(tooltip.get_content(), "Click to submit");
649 assert_eq!(tooltip.get_placement(), TooltipPlacement::Bottom);
650 assert_eq!(tooltip.get_delay_ms(), 500);
651 assert!(tooltip.is_visible());
652 assert_eq!(
653 Widget::accessible_name(&tooltip),
654 Some("Submit button tooltip")
655 );
656 assert_eq!(Widget::test_id(&tooltip), Some("submit-tooltip"));
657 }
658
659 #[test]
660 fn test_tooltip_content() {
661 let tooltip = Tooltip::new("old").content("new");
662 assert_eq!(tooltip.get_content(), "new");
663 }
664
665 #[test]
668 fn test_tooltip_show() {
669 let mut tooltip = Tooltip::new("Text");
670 assert!(!tooltip.is_visible());
671 tooltip.show();
672 assert!(tooltip.is_visible());
673 }
674
675 #[test]
676 fn test_tooltip_hide() {
677 let mut tooltip = Tooltip::new("Text").visible(true);
678 assert!(tooltip.is_visible());
679 tooltip.hide();
680 assert!(!tooltip.is_visible());
681 }
682
683 #[test]
684 fn test_tooltip_toggle() {
685 let mut tooltip = Tooltip::new("Text");
686 assert!(!tooltip.is_visible());
687 tooltip.toggle();
688 assert!(tooltip.is_visible());
689 tooltip.toggle();
690 assert!(!tooltip.is_visible());
691 }
692
693 #[test]
696 fn test_tooltip_anchor() {
697 let anchor = Rect::new(100.0, 100.0, 80.0, 30.0);
698 let tooltip = Tooltip::new("Help").anchor(anchor);
699 assert_eq!(tooltip.get_anchor(), anchor);
700 }
701
702 #[test]
703 fn test_tooltip_set_anchor() {
704 let mut tooltip = Tooltip::new("Help");
705 let anchor = Rect::new(50.0, 50.0, 100.0, 40.0);
706 tooltip.set_anchor(anchor);
707 assert_eq!(tooltip.get_anchor(), anchor);
708 }
709
710 #[test]
713 fn test_tooltip_border_width_min() {
714 let tooltip = Tooltip::new("Text").border_width(-5.0);
715 assert_eq!(tooltip.border_width, 0.0);
716 }
717
718 #[test]
719 fn test_tooltip_corner_radius_min() {
720 let tooltip = Tooltip::new("Text").corner_radius(-5.0);
721 assert_eq!(tooltip.corner_radius, 0.0);
722 }
723
724 #[test]
725 fn test_tooltip_padding_min() {
726 let tooltip = Tooltip::new("Text").padding(-5.0);
727 assert_eq!(tooltip.padding, 0.0);
728 }
729
730 #[test]
731 fn test_tooltip_arrow_size_min() {
732 let tooltip = Tooltip::new("Text").arrow_size(-5.0);
733 assert_eq!(tooltip.arrow_size, 0.0);
734 }
735
736 #[test]
737 fn test_tooltip_max_width_min() {
738 let tooltip = Tooltip::new("Text").max_width(10.0);
739 assert_eq!(tooltip.max_width, Some(50.0));
740 }
741
742 #[test]
743 fn test_tooltip_no_max_width() {
744 let tooltip = Tooltip::new("Text").max_width(200.0).no_max_width();
745 assert!(tooltip.max_width.is_none());
746 }
747
748 #[test]
749 fn test_tooltip_text_size_min() {
750 let tooltip = Tooltip::new("Text").text_size(2.0);
751 assert_eq!(tooltip.text_size, 8.0);
752 }
753
754 #[test]
757 fn test_tooltip_estimate_text_width() {
758 let tooltip = Tooltip::new("Hello").text_size(12.0);
759 let width = tooltip.estimate_text_width();
760 assert!((width - 36.0).abs() < 0.1);
762 }
763
764 #[test]
765 fn test_tooltip_calculate_size() {
766 let tooltip = Tooltip::new("Test").padding(10.0).text_size(12.0);
767 let size = tooltip.calculate_size();
768 assert!(size.width > 0.0);
769 assert!(size.height > 0.0);
770 }
771
772 #[test]
775 fn test_tooltip_position_top() {
776 let tooltip = Tooltip::new("Text")
777 .placement(TooltipPlacement::Top)
778 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
779 .show_arrow(true)
780 .arrow_size(6.0);
781
782 let size = Size::new(50.0, 24.0);
783 let pos = tooltip.calculate_position(size);
784
785 assert!(pos.y < 100.0);
787 assert!(pos.x > 100.0); }
789
790 #[test]
791 fn test_tooltip_position_bottom() {
792 let tooltip = Tooltip::new("Text")
793 .placement(TooltipPlacement::Bottom)
794 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
795 .show_arrow(true)
796 .arrow_size(6.0);
797
798 let size = Size::new(50.0, 24.0);
799 let pos = tooltip.calculate_position(size);
800
801 assert!(pos.y > 130.0); }
804
805 #[test]
806 fn test_tooltip_position_left() {
807 let tooltip = Tooltip::new("Text")
808 .placement(TooltipPlacement::Left)
809 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
810 .show_arrow(true)
811 .arrow_size(6.0);
812
813 let size = Size::new(50.0, 24.0);
814 let pos = tooltip.calculate_position(size);
815
816 assert!(pos.x < 100.0 - 50.0);
818 }
819
820 #[test]
821 fn test_tooltip_position_right() {
822 let tooltip = Tooltip::new("Text")
823 .placement(TooltipPlacement::Right)
824 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
825 .show_arrow(true)
826 .arrow_size(6.0);
827
828 let size = Size::new(50.0, 24.0);
829 let pos = tooltip.calculate_position(size);
830
831 assert!(pos.x > 180.0); }
834
835 #[test]
838 fn test_tooltip_type_id() {
839 let tooltip = Tooltip::new("Text");
840 assert_eq!(Widget::type_id(&tooltip), TypeId::of::<Tooltip>());
841 }
842
843 #[test]
844 fn test_tooltip_measure_invisible() {
845 let tooltip = Tooltip::new("Text").visible(false);
846 let size = tooltip.measure(Constraints::loose(Size::new(500.0, 500.0)));
847 assert_eq!(size, Size::ZERO);
848 }
849
850 #[test]
851 fn test_tooltip_measure_empty() {
852 let tooltip = Tooltip::default().visible(true);
853 let size = tooltip.measure(Constraints::loose(Size::new(500.0, 500.0)));
854 assert_eq!(size, Size::ZERO);
855 }
856
857 #[test]
858 fn test_tooltip_measure_visible() {
859 let tooltip = Tooltip::new("Some helpful text").visible(true);
860 let size = tooltip.measure(Constraints::loose(Size::new(500.0, 500.0)));
861 assert!(size.width > 0.0);
862 assert!(size.height > 0.0);
863 }
864
865 #[test]
866 fn test_tooltip_layout_invisible() {
867 let mut tooltip = Tooltip::new("Text").visible(false);
868 let result = tooltip.layout(Rect::new(0.0, 0.0, 200.0, 100.0));
869 assert_eq!(result.size, Size::ZERO);
870 }
871
872 #[test]
873 fn test_tooltip_layout_visible() {
874 let mut tooltip = Tooltip::new("Text")
875 .visible(true)
876 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0));
877 let result = tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
878 assert!(result.size.width > 0.0);
879 assert!(result.size.height > 0.0);
880 }
881
882 #[test]
883 fn test_tooltip_children() {
884 let tooltip = Tooltip::new("Text");
885 assert!(tooltip.children().is_empty());
886 }
887
888 #[test]
889 fn test_tooltip_is_interactive() {
890 let tooltip = Tooltip::new("Text");
891 assert!(!tooltip.is_interactive());
892 }
893
894 #[test]
895 fn test_tooltip_is_focusable() {
896 let tooltip = Tooltip::new("Text");
897 assert!(!tooltip.is_focusable());
898 }
899
900 #[test]
901 fn test_tooltip_accessible_role() {
902 let tooltip = Tooltip::new("Text");
903 assert_eq!(tooltip.accessible_role(), AccessibleRole::Generic);
904 }
905
906 #[test]
907 fn test_tooltip_accessible_name_default() {
908 let tooltip = Tooltip::new("Help text");
909 assert_eq!(Widget::accessible_name(&tooltip), Some("Help text"));
911 }
912
913 #[test]
914 fn test_tooltip_accessible_name_explicit() {
915 let tooltip = Tooltip::new("Help text").accessible_name("Explicit name");
916 assert_eq!(Widget::accessible_name(&tooltip), Some("Explicit name"));
917 }
918
919 #[test]
920 fn test_tooltip_test_id() {
921 let tooltip = Tooltip::new("Text").test_id("help-tooltip");
922 assert_eq!(Widget::test_id(&tooltip), Some("help-tooltip"));
923 }
924
925 #[test]
928 fn test_tooltip_mouse_leave_hides() {
929 let mut tooltip = Tooltip::new("Text").visible(true);
930 assert!(tooltip.is_visible());
931
932 tooltip.event(&Event::MouseLeave);
933 assert!(!tooltip.is_visible());
934 }
935
936 #[test]
937 fn test_tooltip_stays_hidden_on_other_events() {
938 let mut tooltip = Tooltip::new("Text").visible(false);
939 tooltip.event(&Event::MouseMove {
941 position: Point::new(0.0, 0.0),
942 });
943 assert!(!tooltip.is_visible());
944 }
945
946 #[test]
949 fn test_tooltip_colors() {
950 let tooltip = Tooltip::new("Text")
951 .background(Color::BLUE)
952 .text_color(Color::RED)
953 .border_color(Color::GREEN);
954
955 assert_eq!(tooltip.background, Color::BLUE);
956 assert_eq!(tooltip.text_color, Color::RED);
957 assert_eq!(tooltip.border_color, Color::GREEN);
958 }
959
960 #[test]
963 fn test_tooltip_position_top_left() {
964 let tooltip = Tooltip::new("Text")
965 .placement(TooltipPlacement::TopLeft)
966 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
967 .show_arrow(true)
968 .arrow_size(6.0);
969
970 let size = Size::new(50.0, 24.0);
971 let pos = tooltip.calculate_position(size);
972
973 assert_eq!(pos.x, 100.0);
975 assert!(pos.y < 100.0); }
977
978 #[test]
979 fn test_tooltip_position_top_right() {
980 let tooltip = Tooltip::new("Text")
981 .placement(TooltipPlacement::TopRight)
982 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
983 .show_arrow(true)
984 .arrow_size(6.0);
985
986 let size = Size::new(50.0, 24.0);
987 let pos = tooltip.calculate_position(size);
988
989 assert_eq!(pos.x, 130.0);
991 assert!(pos.y < 100.0);
992 }
993
994 #[test]
995 fn test_tooltip_position_bottom_left() {
996 let tooltip = Tooltip::new("Text")
997 .placement(TooltipPlacement::BottomLeft)
998 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
999 .show_arrow(true)
1000 .arrow_size(6.0);
1001
1002 let size = Size::new(50.0, 24.0);
1003 let pos = tooltip.calculate_position(size);
1004
1005 assert_eq!(pos.x, 100.0);
1007 assert!(pos.y > 130.0); }
1009
1010 #[test]
1011 fn test_tooltip_position_bottom_right() {
1012 let tooltip = Tooltip::new("Text")
1013 .placement(TooltipPlacement::BottomRight)
1014 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1015 .show_arrow(true)
1016 .arrow_size(6.0);
1017
1018 let size = Size::new(50.0, 24.0);
1019 let pos = tooltip.calculate_position(size);
1020
1021 assert_eq!(pos.x, 130.0);
1023 assert!(pos.y > 130.0);
1024 }
1025
1026 #[test]
1027 fn test_tooltip_position_no_arrow() {
1028 let tooltip = Tooltip::new("Text")
1029 .placement(TooltipPlacement::Top)
1030 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1031 .show_arrow(false);
1032
1033 let size = Size::new(50.0, 24.0);
1034 let pos = tooltip.calculate_position(size);
1035
1036 assert_eq!(pos.y, 100.0 - 24.0); }
1039
1040 #[test]
1043 fn test_calculate_size_no_max_width() {
1044 let tooltip = Tooltip::new("Short text")
1045 .padding(8.0)
1046 .text_size(12.0)
1047 .no_max_width();
1048
1049 let size = tooltip.calculate_size();
1050 assert!(size.width > 0.0);
1051 assert!(size.height > 0.0);
1052 }
1053
1054 #[test]
1055 fn test_calculate_size_wraps_long_text() {
1056 let tooltip = Tooltip::new("This is a very long tooltip text that should wrap")
1057 .padding(8.0)
1058 .text_size(12.0)
1059 .max_width(100.0);
1060
1061 let size = tooltip.calculate_size();
1062 assert!(size.width <= 100.0);
1064 assert!(size.height > 12.0 * 1.2 + 16.0); }
1067
1068 #[test]
1071 fn test_tooltip_paint_invisible() {
1072 use presentar_core::RecordingCanvas;
1073
1074 let tooltip = Tooltip::new("Text").visible(false);
1075 let mut canvas = RecordingCanvas::new();
1076 tooltip.paint(&mut canvas);
1077
1078 assert_eq!(canvas.command_count(), 0);
1080 }
1081
1082 #[test]
1083 fn test_tooltip_paint_empty_content() {
1084 use presentar_core::RecordingCanvas;
1085
1086 let tooltip = Tooltip::default().visible(true);
1087 let mut canvas = RecordingCanvas::new();
1088 tooltip.paint(&mut canvas);
1089
1090 assert_eq!(canvas.command_count(), 0);
1092 }
1093
1094 #[test]
1095 fn test_tooltip_paint_visible() {
1096 use presentar_core::RecordingCanvas;
1097
1098 let mut tooltip = Tooltip::new("Help text")
1099 .visible(true)
1100 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1101 .placement(TooltipPlacement::Top);
1102
1103 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1104
1105 let mut canvas = RecordingCanvas::new();
1106 tooltip.paint(&mut canvas);
1107
1108 assert!(canvas.command_count() >= 2);
1110 }
1111
1112 #[test]
1113 fn test_tooltip_paint_with_border() {
1114 use presentar_core::RecordingCanvas;
1115
1116 let mut tooltip = Tooltip::new("Help text")
1117 .visible(true)
1118 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1119 .border_width(2.0);
1120
1121 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1122
1123 let mut canvas = RecordingCanvas::new();
1124 tooltip.paint(&mut canvas);
1125
1126 assert!(canvas.command_count() >= 3); }
1129
1130 #[test]
1131 fn test_tooltip_paint_without_arrow() {
1132 use presentar_core::RecordingCanvas;
1133
1134 let mut tooltip = Tooltip::new("Help text")
1135 .visible(true)
1136 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1137 .show_arrow(false);
1138
1139 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1140
1141 let mut canvas = RecordingCanvas::new();
1142 tooltip.paint(&mut canvas);
1143
1144 assert!(canvas.command_count() >= 2);
1146 }
1147
1148 #[test]
1149 fn test_tooltip_paint_bottom_arrow() {
1150 use presentar_core::RecordingCanvas;
1151
1152 let mut tooltip = Tooltip::new("Help text")
1153 .visible(true)
1154 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1155 .placement(TooltipPlacement::Bottom);
1156
1157 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1158
1159 let mut canvas = RecordingCanvas::new();
1160 tooltip.paint(&mut canvas);
1161
1162 assert!(canvas.command_count() >= 3);
1163 }
1164
1165 #[test]
1166 fn test_tooltip_paint_left_arrow() {
1167 use presentar_core::RecordingCanvas;
1168
1169 let mut tooltip = Tooltip::new("Help text")
1170 .visible(true)
1171 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1172 .placement(TooltipPlacement::Left);
1173
1174 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1175
1176 let mut canvas = RecordingCanvas::new();
1177 tooltip.paint(&mut canvas);
1178
1179 assert!(canvas.command_count() >= 3);
1180 }
1181
1182 #[test]
1183 fn test_tooltip_paint_right_arrow() {
1184 use presentar_core::RecordingCanvas;
1185
1186 let mut tooltip = Tooltip::new("Help text")
1187 .visible(true)
1188 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1189 .placement(TooltipPlacement::Right);
1190
1191 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1192
1193 let mut canvas = RecordingCanvas::new();
1194 tooltip.paint(&mut canvas);
1195
1196 assert!(canvas.command_count() >= 3);
1197 }
1198
1199 #[test]
1202 fn test_tooltip_brick_name() {
1203 let tooltip = Tooltip::new("Text");
1204 assert_eq!(tooltip.brick_name(), "Tooltip");
1205 }
1206
1207 #[test]
1208 fn test_tooltip_brick_assertions() {
1209 let tooltip = Tooltip::new("Text");
1210 let assertions = tooltip.assertions();
1211 assert_eq!(assertions.len(), 2);
1212 assert!(assertions.contains(&BrickAssertion::MaxLatencyMs(16)));
1213 assert!(assertions.contains(&BrickAssertion::ContrastRatio(4.5)));
1214 }
1215
1216 #[test]
1217 fn test_tooltip_brick_budget() {
1218 let tooltip = Tooltip::new("Text");
1219 let budget = tooltip.budget();
1220 assert!(budget.measure_ms > 0);
1222 assert!(budget.layout_ms > 0);
1223 assert!(budget.paint_ms > 0);
1224 }
1225
1226 #[test]
1227 fn test_tooltip_brick_verify_good_contrast() {
1228 let tooltip = Tooltip::new("Text")
1230 .background(Color::BLACK)
1231 .text_color(Color::WHITE);
1232
1233 let verification = tooltip.verify();
1234 assert!(verification
1235 .passed
1236 .contains(&BrickAssertion::ContrastRatio(4.5)));
1237 assert!(verification.failed.is_empty());
1238 }
1239
1240 #[test]
1241 fn test_tooltip_brick_verify_bad_contrast() {
1242 let tooltip = Tooltip::new("Text")
1244 .background(Color::WHITE)
1245 .text_color(Color::rgb(0.9, 0.9, 0.9)); let verification = tooltip.verify();
1248 assert!(!verification.failed.is_empty());
1249 assert!(verification
1250 .failed
1251 .iter()
1252 .any(|(a, _)| *a == BrickAssertion::ContrastRatio(4.5)));
1253 }
1254
1255 #[test]
1256 fn test_tooltip_to_html() {
1257 let tooltip = Tooltip::new("Help text")
1258 .test_id("help-tooltip")
1259 .accessible_name("Help information");
1260
1261 let html = tooltip.to_html();
1262 assert!(html.contains("role=\"tooltip\""));
1263 assert!(html.contains("data-testid=\"help-tooltip\""));
1264 assert!(html.contains("aria-label=\"Help information\""));
1265 assert!(html.contains("Help text"));
1266 }
1267
1268 #[test]
1269 fn test_tooltip_to_html_default_values() {
1270 let tooltip = Tooltip::new("Text");
1271 let html = tooltip.to_html();
1272
1273 assert!(html.contains("data-testid=\"tooltip\""));
1275 assert!(html.contains("aria-label=\"Text\""));
1276 }
1277
1278 #[test]
1279 fn test_tooltip_to_css() {
1280 let tooltip = Tooltip::new("Text")
1281 .padding(12.0)
1282 .text_size(14.0)
1283 .corner_radius(6.0)
1284 .max_width(300.0);
1285
1286 let css = tooltip.to_css();
1287 assert!(css.contains("padding: 12px"));
1288 assert!(css.contains("font-size: 14px"));
1289 assert!(css.contains("border-radius: 6px"));
1290 assert!(css.contains("max-width: 300px"));
1291 }
1292
1293 #[test]
1296 fn test_tooltip_children_mut() {
1297 let mut tooltip = Tooltip::new("Text");
1298 assert!(tooltip.children_mut().is_empty());
1299 }
1300
1301 #[test]
1304 fn test_tooltip_event_other_events() {
1305 let mut tooltip = Tooltip::new("Text").visible(true);
1306
1307 let result = tooltip.event(&Event::MouseEnter);
1309 assert!(result.is_none());
1310 assert!(tooltip.is_visible());
1311 }
1312
1313 #[test]
1316 fn test_tooltip_paint_topleft_arrow() {
1317 use presentar_core::RecordingCanvas;
1318
1319 let mut tooltip = Tooltip::new("Help text")
1320 .visible(true)
1321 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1322 .placement(TooltipPlacement::TopLeft);
1323
1324 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1325
1326 let mut canvas = RecordingCanvas::new();
1327 tooltip.paint(&mut canvas);
1328
1329 assert!(canvas.command_count() >= 3);
1330 }
1331
1332 #[test]
1333 fn test_tooltip_paint_topright_arrow() {
1334 use presentar_core::RecordingCanvas;
1335
1336 let mut tooltip = Tooltip::new("Help text")
1337 .visible(true)
1338 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1339 .placement(TooltipPlacement::TopRight);
1340
1341 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1342
1343 let mut canvas = RecordingCanvas::new();
1344 tooltip.paint(&mut canvas);
1345
1346 assert!(canvas.command_count() >= 3);
1347 }
1348
1349 #[test]
1350 fn test_tooltip_paint_bottomleft_arrow() {
1351 use presentar_core::RecordingCanvas;
1352
1353 let mut tooltip = Tooltip::new("Help text")
1354 .visible(true)
1355 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1356 .placement(TooltipPlacement::BottomLeft);
1357
1358 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1359
1360 let mut canvas = RecordingCanvas::new();
1361 tooltip.paint(&mut canvas);
1362
1363 assert!(canvas.command_count() >= 3);
1364 }
1365
1366 #[test]
1367 fn test_tooltip_paint_bottomright_arrow() {
1368 use presentar_core::RecordingCanvas;
1369
1370 let mut tooltip = Tooltip::new("Help text")
1371 .visible(true)
1372 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1373 .placement(TooltipPlacement::BottomRight);
1374
1375 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1376
1377 let mut canvas = RecordingCanvas::new();
1378 tooltip.paint(&mut canvas);
1379
1380 assert!(canvas.command_count() >= 3);
1381 }
1382
1383 #[test]
1384 fn test_tooltip_layout_empty() {
1385 let mut tooltip = Tooltip::default().visible(true);
1386 let result = tooltip.layout(Rect::new(0.0, 0.0, 200.0, 100.0));
1387 assert_eq!(result.size, Size::ZERO);
1388 }
1389
1390 #[test]
1391 fn test_tooltip_placement_clone() {
1392 let placement = TooltipPlacement::Bottom;
1393 let cloned = placement;
1394 assert_eq!(cloned, TooltipPlacement::Bottom);
1395 }
1396
1397 #[test]
1398 fn test_tooltip_placement_debug() {
1399 let placement = TooltipPlacement::Right;
1400 let debug = format!("{:?}", placement);
1401 assert!(debug.contains("Right"));
1402 }
1403
1404 #[test]
1405 fn test_tooltip_clone() {
1406 let tooltip = Tooltip::new("Text")
1407 .placement(TooltipPlacement::Left)
1408 .delay_ms(500);
1409 let cloned = tooltip.clone();
1410 assert_eq!(cloned.get_content(), "Text");
1411 assert_eq!(cloned.get_placement(), TooltipPlacement::Left);
1412 assert_eq!(cloned.get_delay_ms(), 500);
1413 }
1414
1415 #[test]
1416 fn test_tooltip_serde() {
1417 let tooltip = Tooltip::new("Help")
1418 .placement(TooltipPlacement::Bottom)
1419 .delay_ms(300);
1420
1421 let json = serde_json::to_string(&tooltip).unwrap();
1422 let deserialized: Tooltip = serde_json::from_str(&json).unwrap();
1423
1424 assert_eq!(deserialized.get_content(), "Help");
1425 assert_eq!(deserialized.get_placement(), TooltipPlacement::Bottom);
1426 assert_eq!(deserialized.get_delay_ms(), 300);
1427 }
1428
1429 #[test]
1430 fn test_tooltip_placement_serde() {
1431 let placement = TooltipPlacement::TopRight;
1432 let json = serde_json::to_string(&placement).unwrap();
1433 let deserialized: TooltipPlacement = serde_json::from_str(&json).unwrap();
1434 assert_eq!(deserialized, TooltipPlacement::TopRight);
1435 }
1436
1437 #[test]
1438 fn test_tooltip_bounds_after_layout() {
1439 let mut tooltip = Tooltip::new("Test tooltip text")
1440 .visible(true)
1441 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1442 .placement(TooltipPlacement::Top);
1443
1444 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1445
1446 assert!(tooltip.bounds.width > 0.0);
1448 assert!(tooltip.bounds.height > 0.0);
1449 }
1450}