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)]
586#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
587mod tests {
588 use super::*;
589
590 #[test]
593 fn test_tooltip_placement_default() {
594 assert_eq!(TooltipPlacement::default(), TooltipPlacement::Top);
595 }
596
597 #[test]
598 fn test_tooltip_placement_variants() {
599 let placements = [
600 TooltipPlacement::Top,
601 TooltipPlacement::Bottom,
602 TooltipPlacement::Left,
603 TooltipPlacement::Right,
604 TooltipPlacement::TopLeft,
605 TooltipPlacement::TopRight,
606 TooltipPlacement::BottomLeft,
607 TooltipPlacement::BottomRight,
608 ];
609 assert_eq!(placements.len(), 8);
610 }
611
612 #[test]
615 fn test_tooltip_new() {
616 let tooltip = Tooltip::new("Help text");
617 assert_eq!(tooltip.get_content(), "Help text");
618 assert!(!tooltip.is_visible());
619 }
620
621 #[test]
622 fn test_tooltip_default() {
623 let tooltip = Tooltip::default();
624 assert!(tooltip.content.is_empty());
625 assert_eq!(tooltip.placement, TooltipPlacement::Top);
626 assert_eq!(tooltip.delay_ms, 200);
627 assert!(!tooltip.visible);
628 }
629
630 #[test]
631 fn test_tooltip_builder() {
632 let tooltip = Tooltip::new("Click to submit")
633 .placement(TooltipPlacement::Bottom)
634 .delay_ms(500)
635 .visible(true)
636 .background(Color::BLACK)
637 .text_color(Color::WHITE)
638 .border_color(Color::RED)
639 .border_width(1.0)
640 .corner_radius(8.0)
641 .padding(12.0)
642 .arrow_size(8.0)
643 .show_arrow(true)
644 .max_width(300.0)
645 .text_size(14.0)
646 .accessible_name("Submit button tooltip")
647 .test_id("submit-tooltip");
648
649 assert_eq!(tooltip.get_content(), "Click to submit");
650 assert_eq!(tooltip.get_placement(), TooltipPlacement::Bottom);
651 assert_eq!(tooltip.get_delay_ms(), 500);
652 assert!(tooltip.is_visible());
653 assert_eq!(
654 Widget::accessible_name(&tooltip),
655 Some("Submit button tooltip")
656 );
657 assert_eq!(Widget::test_id(&tooltip), Some("submit-tooltip"));
658 }
659
660 #[test]
661 fn test_tooltip_content() {
662 let tooltip = Tooltip::new("old").content("new");
663 assert_eq!(tooltip.get_content(), "new");
664 }
665
666 #[test]
669 fn test_tooltip_show() {
670 let mut tooltip = Tooltip::new("Text");
671 assert!(!tooltip.is_visible());
672 tooltip.show();
673 assert!(tooltip.is_visible());
674 }
675
676 #[test]
677 fn test_tooltip_hide() {
678 let mut tooltip = Tooltip::new("Text").visible(true);
679 assert!(tooltip.is_visible());
680 tooltip.hide();
681 assert!(!tooltip.is_visible());
682 }
683
684 #[test]
685 fn test_tooltip_toggle() {
686 let mut tooltip = Tooltip::new("Text");
687 assert!(!tooltip.is_visible());
688 tooltip.toggle();
689 assert!(tooltip.is_visible());
690 tooltip.toggle();
691 assert!(!tooltip.is_visible());
692 }
693
694 #[test]
697 fn test_tooltip_anchor() {
698 let anchor = Rect::new(100.0, 100.0, 80.0, 30.0);
699 let tooltip = Tooltip::new("Help").anchor(anchor);
700 assert_eq!(tooltip.get_anchor(), anchor);
701 }
702
703 #[test]
704 fn test_tooltip_set_anchor() {
705 let mut tooltip = Tooltip::new("Help");
706 let anchor = Rect::new(50.0, 50.0, 100.0, 40.0);
707 tooltip.set_anchor(anchor);
708 assert_eq!(tooltip.get_anchor(), anchor);
709 }
710
711 #[test]
714 fn test_tooltip_border_width_min() {
715 let tooltip = Tooltip::new("Text").border_width(-5.0);
716 assert_eq!(tooltip.border_width, 0.0);
717 }
718
719 #[test]
720 fn test_tooltip_corner_radius_min() {
721 let tooltip = Tooltip::new("Text").corner_radius(-5.0);
722 assert_eq!(tooltip.corner_radius, 0.0);
723 }
724
725 #[test]
726 fn test_tooltip_padding_min() {
727 let tooltip = Tooltip::new("Text").padding(-5.0);
728 assert_eq!(tooltip.padding, 0.0);
729 }
730
731 #[test]
732 fn test_tooltip_arrow_size_min() {
733 let tooltip = Tooltip::new("Text").arrow_size(-5.0);
734 assert_eq!(tooltip.arrow_size, 0.0);
735 }
736
737 #[test]
738 fn test_tooltip_max_width_min() {
739 let tooltip = Tooltip::new("Text").max_width(10.0);
740 assert_eq!(tooltip.max_width, Some(50.0));
741 }
742
743 #[test]
744 fn test_tooltip_no_max_width() {
745 let tooltip = Tooltip::new("Text").max_width(200.0).no_max_width();
746 assert!(tooltip.max_width.is_none());
747 }
748
749 #[test]
750 fn test_tooltip_text_size_min() {
751 let tooltip = Tooltip::new("Text").text_size(2.0);
752 assert_eq!(tooltip.text_size, 8.0);
753 }
754
755 #[test]
758 fn test_tooltip_estimate_text_width() {
759 let tooltip = Tooltip::new("Hello").text_size(12.0);
760 let width = tooltip.estimate_text_width();
761 assert!((width - 36.0).abs() < 0.1);
763 }
764
765 #[test]
766 fn test_tooltip_calculate_size() {
767 let tooltip = Tooltip::new("Test").padding(10.0).text_size(12.0);
768 let size = tooltip.calculate_size();
769 assert!(size.width > 0.0);
770 assert!(size.height > 0.0);
771 }
772
773 #[test]
776 fn test_tooltip_position_top() {
777 let tooltip = Tooltip::new("Text")
778 .placement(TooltipPlacement::Top)
779 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
780 .show_arrow(true)
781 .arrow_size(6.0);
782
783 let size = Size::new(50.0, 24.0);
784 let pos = tooltip.calculate_position(size);
785
786 assert!(pos.y < 100.0);
788 assert!(pos.x > 100.0); }
790
791 #[test]
792 fn test_tooltip_position_bottom() {
793 let tooltip = Tooltip::new("Text")
794 .placement(TooltipPlacement::Bottom)
795 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
796 .show_arrow(true)
797 .arrow_size(6.0);
798
799 let size = Size::new(50.0, 24.0);
800 let pos = tooltip.calculate_position(size);
801
802 assert!(pos.y > 130.0); }
805
806 #[test]
807 fn test_tooltip_position_left() {
808 let tooltip = Tooltip::new("Text")
809 .placement(TooltipPlacement::Left)
810 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
811 .show_arrow(true)
812 .arrow_size(6.0);
813
814 let size = Size::new(50.0, 24.0);
815 let pos = tooltip.calculate_position(size);
816
817 assert!(pos.x < 100.0 - 50.0);
819 }
820
821 #[test]
822 fn test_tooltip_position_right() {
823 let tooltip = Tooltip::new("Text")
824 .placement(TooltipPlacement::Right)
825 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
826 .show_arrow(true)
827 .arrow_size(6.0);
828
829 let size = Size::new(50.0, 24.0);
830 let pos = tooltip.calculate_position(size);
831
832 assert!(pos.x > 180.0); }
835
836 #[test]
839 fn test_tooltip_type_id() {
840 let tooltip = Tooltip::new("Text");
841 assert_eq!(Widget::type_id(&tooltip), TypeId::of::<Tooltip>());
842 }
843
844 #[test]
845 fn test_tooltip_measure_invisible() {
846 let tooltip = Tooltip::new("Text").visible(false);
847 let size = tooltip.measure(Constraints::loose(Size::new(500.0, 500.0)));
848 assert_eq!(size, Size::ZERO);
849 }
850
851 #[test]
852 fn test_tooltip_measure_empty() {
853 let tooltip = Tooltip::default().visible(true);
854 let size = tooltip.measure(Constraints::loose(Size::new(500.0, 500.0)));
855 assert_eq!(size, Size::ZERO);
856 }
857
858 #[test]
859 fn test_tooltip_measure_visible() {
860 let tooltip = Tooltip::new("Some helpful text").visible(true);
861 let size = tooltip.measure(Constraints::loose(Size::new(500.0, 500.0)));
862 assert!(size.width > 0.0);
863 assert!(size.height > 0.0);
864 }
865
866 #[test]
867 fn test_tooltip_layout_invisible() {
868 let mut tooltip = Tooltip::new("Text").visible(false);
869 let result = tooltip.layout(Rect::new(0.0, 0.0, 200.0, 100.0));
870 assert_eq!(result.size, Size::ZERO);
871 }
872
873 #[test]
874 fn test_tooltip_layout_visible() {
875 let mut tooltip = Tooltip::new("Text")
876 .visible(true)
877 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0));
878 let result = tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
879 assert!(result.size.width > 0.0);
880 assert!(result.size.height > 0.0);
881 }
882
883 #[test]
884 fn test_tooltip_children() {
885 let tooltip = Tooltip::new("Text");
886 assert!(tooltip.children().is_empty());
887 }
888
889 #[test]
890 fn test_tooltip_is_interactive() {
891 let tooltip = Tooltip::new("Text");
892 assert!(!tooltip.is_interactive());
893 }
894
895 #[test]
896 fn test_tooltip_is_focusable() {
897 let tooltip = Tooltip::new("Text");
898 assert!(!tooltip.is_focusable());
899 }
900
901 #[test]
902 fn test_tooltip_accessible_role() {
903 let tooltip = Tooltip::new("Text");
904 assert_eq!(tooltip.accessible_role(), AccessibleRole::Generic);
905 }
906
907 #[test]
908 fn test_tooltip_accessible_name_default() {
909 let tooltip = Tooltip::new("Help text");
910 assert_eq!(Widget::accessible_name(&tooltip), Some("Help text"));
912 }
913
914 #[test]
915 fn test_tooltip_accessible_name_explicit() {
916 let tooltip = Tooltip::new("Help text").accessible_name("Explicit name");
917 assert_eq!(Widget::accessible_name(&tooltip), Some("Explicit name"));
918 }
919
920 #[test]
921 fn test_tooltip_test_id() {
922 let tooltip = Tooltip::new("Text").test_id("help-tooltip");
923 assert_eq!(Widget::test_id(&tooltip), Some("help-tooltip"));
924 }
925
926 #[test]
929 fn test_tooltip_mouse_leave_hides() {
930 let mut tooltip = Tooltip::new("Text").visible(true);
931 assert!(tooltip.is_visible());
932
933 tooltip.event(&Event::MouseLeave);
934 assert!(!tooltip.is_visible());
935 }
936
937 #[test]
938 fn test_tooltip_stays_hidden_on_other_events() {
939 let mut tooltip = Tooltip::new("Text").visible(false);
940 tooltip.event(&Event::MouseMove {
942 position: Point::new(0.0, 0.0),
943 });
944 assert!(!tooltip.is_visible());
945 }
946
947 #[test]
950 fn test_tooltip_colors() {
951 let tooltip = Tooltip::new("Text")
952 .background(Color::BLUE)
953 .text_color(Color::RED)
954 .border_color(Color::GREEN);
955
956 assert_eq!(tooltip.background, Color::BLUE);
957 assert_eq!(tooltip.text_color, Color::RED);
958 assert_eq!(tooltip.border_color, Color::GREEN);
959 }
960
961 #[test]
964 fn test_tooltip_position_top_left() {
965 let tooltip = Tooltip::new("Text")
966 .placement(TooltipPlacement::TopLeft)
967 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
968 .show_arrow(true)
969 .arrow_size(6.0);
970
971 let size = Size::new(50.0, 24.0);
972 let pos = tooltip.calculate_position(size);
973
974 assert_eq!(pos.x, 100.0);
976 assert!(pos.y < 100.0); }
978
979 #[test]
980 fn test_tooltip_position_top_right() {
981 let tooltip = Tooltip::new("Text")
982 .placement(TooltipPlacement::TopRight)
983 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
984 .show_arrow(true)
985 .arrow_size(6.0);
986
987 let size = Size::new(50.0, 24.0);
988 let pos = tooltip.calculate_position(size);
989
990 assert_eq!(pos.x, 130.0);
992 assert!(pos.y < 100.0);
993 }
994
995 #[test]
996 fn test_tooltip_position_bottom_left() {
997 let tooltip = Tooltip::new("Text")
998 .placement(TooltipPlacement::BottomLeft)
999 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1000 .show_arrow(true)
1001 .arrow_size(6.0);
1002
1003 let size = Size::new(50.0, 24.0);
1004 let pos = tooltip.calculate_position(size);
1005
1006 assert_eq!(pos.x, 100.0);
1008 assert!(pos.y > 130.0); }
1010
1011 #[test]
1012 fn test_tooltip_position_bottom_right() {
1013 let tooltip = Tooltip::new("Text")
1014 .placement(TooltipPlacement::BottomRight)
1015 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1016 .show_arrow(true)
1017 .arrow_size(6.0);
1018
1019 let size = Size::new(50.0, 24.0);
1020 let pos = tooltip.calculate_position(size);
1021
1022 assert_eq!(pos.x, 130.0);
1024 assert!(pos.y > 130.0);
1025 }
1026
1027 #[test]
1028 fn test_tooltip_position_no_arrow() {
1029 let tooltip = Tooltip::new("Text")
1030 .placement(TooltipPlacement::Top)
1031 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1032 .show_arrow(false);
1033
1034 let size = Size::new(50.0, 24.0);
1035 let pos = tooltip.calculate_position(size);
1036
1037 assert_eq!(pos.y, 100.0 - 24.0); }
1040
1041 #[test]
1044 fn test_calculate_size_no_max_width() {
1045 let tooltip = Tooltip::new("Short text")
1046 .padding(8.0)
1047 .text_size(12.0)
1048 .no_max_width();
1049
1050 let size = tooltip.calculate_size();
1051 assert!(size.width > 0.0);
1052 assert!(size.height > 0.0);
1053 }
1054
1055 #[test]
1056 fn test_calculate_size_wraps_long_text() {
1057 let tooltip = Tooltip::new("This is a very long tooltip text that should wrap")
1058 .padding(8.0)
1059 .text_size(12.0)
1060 .max_width(100.0);
1061
1062 let size = tooltip.calculate_size();
1063 assert!(size.width <= 100.0);
1065 assert!(size.height > 12.0f32.mul_add(1.2, 16.0)); }
1068
1069 #[test]
1072 fn test_tooltip_paint_invisible() {
1073 use presentar_core::RecordingCanvas;
1074
1075 let tooltip = Tooltip::new("Text").visible(false);
1076 let mut canvas = RecordingCanvas::new();
1077 tooltip.paint(&mut canvas);
1078
1079 assert_eq!(canvas.command_count(), 0);
1081 }
1082
1083 #[test]
1084 fn test_tooltip_paint_empty_content() {
1085 use presentar_core::RecordingCanvas;
1086
1087 let tooltip = Tooltip::default().visible(true);
1088 let mut canvas = RecordingCanvas::new();
1089 tooltip.paint(&mut canvas);
1090
1091 assert_eq!(canvas.command_count(), 0);
1093 }
1094
1095 #[test]
1096 fn test_tooltip_paint_visible() {
1097 use presentar_core::RecordingCanvas;
1098
1099 let mut tooltip = Tooltip::new("Help text")
1100 .visible(true)
1101 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1102 .placement(TooltipPlacement::Top);
1103
1104 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1105
1106 let mut canvas = RecordingCanvas::new();
1107 tooltip.paint(&mut canvas);
1108
1109 assert!(canvas.command_count() >= 2);
1111 }
1112
1113 #[test]
1114 fn test_tooltip_paint_with_border() {
1115 use presentar_core::RecordingCanvas;
1116
1117 let mut tooltip = Tooltip::new("Help text")
1118 .visible(true)
1119 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1120 .border_width(2.0);
1121
1122 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1123
1124 let mut canvas = RecordingCanvas::new();
1125 tooltip.paint(&mut canvas);
1126
1127 assert!(canvas.command_count() >= 3); }
1130
1131 #[test]
1132 fn test_tooltip_paint_without_arrow() {
1133 use presentar_core::RecordingCanvas;
1134
1135 let mut tooltip = Tooltip::new("Help text")
1136 .visible(true)
1137 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1138 .show_arrow(false);
1139
1140 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1141
1142 let mut canvas = RecordingCanvas::new();
1143 tooltip.paint(&mut canvas);
1144
1145 assert!(canvas.command_count() >= 2);
1147 }
1148
1149 #[test]
1150 fn test_tooltip_paint_bottom_arrow() {
1151 use presentar_core::RecordingCanvas;
1152
1153 let mut tooltip = Tooltip::new("Help text")
1154 .visible(true)
1155 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1156 .placement(TooltipPlacement::Bottom);
1157
1158 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1159
1160 let mut canvas = RecordingCanvas::new();
1161 tooltip.paint(&mut canvas);
1162
1163 assert!(canvas.command_count() >= 3);
1164 }
1165
1166 #[test]
1167 fn test_tooltip_paint_left_arrow() {
1168 use presentar_core::RecordingCanvas;
1169
1170 let mut tooltip = Tooltip::new("Help text")
1171 .visible(true)
1172 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1173 .placement(TooltipPlacement::Left);
1174
1175 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1176
1177 let mut canvas = RecordingCanvas::new();
1178 tooltip.paint(&mut canvas);
1179
1180 assert!(canvas.command_count() >= 3);
1181 }
1182
1183 #[test]
1184 fn test_tooltip_paint_right_arrow() {
1185 use presentar_core::RecordingCanvas;
1186
1187 let mut tooltip = Tooltip::new("Help text")
1188 .visible(true)
1189 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1190 .placement(TooltipPlacement::Right);
1191
1192 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1193
1194 let mut canvas = RecordingCanvas::new();
1195 tooltip.paint(&mut canvas);
1196
1197 assert!(canvas.command_count() >= 3);
1198 }
1199
1200 #[test]
1203 fn test_tooltip_brick_name() {
1204 let tooltip = Tooltip::new("Text");
1205 assert_eq!(tooltip.brick_name(), "Tooltip");
1206 }
1207
1208 #[test]
1209 fn test_tooltip_brick_assertions() {
1210 let tooltip = Tooltip::new("Text");
1211 let assertions = tooltip.assertions();
1212 assert_eq!(assertions.len(), 2);
1213 assert!(assertions.contains(&BrickAssertion::MaxLatencyMs(16)));
1214 assert!(assertions.contains(&BrickAssertion::ContrastRatio(4.5)));
1215 }
1216
1217 #[test]
1218 fn test_tooltip_brick_budget() {
1219 let tooltip = Tooltip::new("Text");
1220 let budget = tooltip.budget();
1221 assert!(budget.measure_ms > 0);
1223 assert!(budget.layout_ms > 0);
1224 assert!(budget.paint_ms > 0);
1225 }
1226
1227 #[test]
1228 fn test_tooltip_brick_verify_good_contrast() {
1229 let tooltip = Tooltip::new("Text")
1231 .background(Color::BLACK)
1232 .text_color(Color::WHITE);
1233
1234 let verification = tooltip.verify();
1235 assert!(verification
1236 .passed
1237 .contains(&BrickAssertion::ContrastRatio(4.5)));
1238 assert!(verification.failed.is_empty());
1239 }
1240
1241 #[test]
1242 fn test_tooltip_brick_verify_bad_contrast() {
1243 let tooltip = Tooltip::new("Text")
1245 .background(Color::WHITE)
1246 .text_color(Color::rgb(0.9, 0.9, 0.9)); let verification = tooltip.verify();
1249 assert!(!verification.failed.is_empty());
1250 assert!(verification
1251 .failed
1252 .iter()
1253 .any(|(a, _)| *a == BrickAssertion::ContrastRatio(4.5)));
1254 }
1255
1256 #[test]
1257 fn test_tooltip_to_html() {
1258 let tooltip = Tooltip::new("Help text")
1259 .test_id("help-tooltip")
1260 .accessible_name("Help information");
1261
1262 let html = tooltip.to_html();
1263 assert!(html.contains("role=\"tooltip\""));
1264 assert!(html.contains("data-testid=\"help-tooltip\""));
1265 assert!(html.contains("aria-label=\"Help information\""));
1266 assert!(html.contains("Help text"));
1267 }
1268
1269 #[test]
1270 fn test_tooltip_to_html_default_values() {
1271 let tooltip = Tooltip::new("Text");
1272 let html = tooltip.to_html();
1273
1274 assert!(html.contains("data-testid=\"tooltip\""));
1276 assert!(html.contains("aria-label=\"Text\""));
1277 }
1278
1279 #[test]
1280 fn test_tooltip_to_css() {
1281 let tooltip = Tooltip::new("Text")
1282 .padding(12.0)
1283 .text_size(14.0)
1284 .corner_radius(6.0)
1285 .max_width(300.0);
1286
1287 let css = tooltip.to_css();
1288 assert!(css.contains("padding: 12px"));
1289 assert!(css.contains("font-size: 14px"));
1290 assert!(css.contains("border-radius: 6px"));
1291 assert!(css.contains("max-width: 300px"));
1292 }
1293
1294 #[test]
1297 fn test_tooltip_children_mut() {
1298 let mut tooltip = Tooltip::new("Text");
1299 assert!(tooltip.children_mut().is_empty());
1300 }
1301
1302 #[test]
1305 fn test_tooltip_event_other_events() {
1306 let mut tooltip = Tooltip::new("Text").visible(true);
1307
1308 let result = tooltip.event(&Event::MouseEnter);
1310 assert!(result.is_none());
1311 assert!(tooltip.is_visible());
1312 }
1313
1314 #[test]
1317 fn test_tooltip_paint_topleft_arrow() {
1318 use presentar_core::RecordingCanvas;
1319
1320 let mut tooltip = Tooltip::new("Help text")
1321 .visible(true)
1322 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1323 .placement(TooltipPlacement::TopLeft);
1324
1325 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1326
1327 let mut canvas = RecordingCanvas::new();
1328 tooltip.paint(&mut canvas);
1329
1330 assert!(canvas.command_count() >= 3);
1331 }
1332
1333 #[test]
1334 fn test_tooltip_paint_topright_arrow() {
1335 use presentar_core::RecordingCanvas;
1336
1337 let mut tooltip = Tooltip::new("Help text")
1338 .visible(true)
1339 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1340 .placement(TooltipPlacement::TopRight);
1341
1342 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1343
1344 let mut canvas = RecordingCanvas::new();
1345 tooltip.paint(&mut canvas);
1346
1347 assert!(canvas.command_count() >= 3);
1348 }
1349
1350 #[test]
1351 fn test_tooltip_paint_bottomleft_arrow() {
1352 use presentar_core::RecordingCanvas;
1353
1354 let mut tooltip = Tooltip::new("Help text")
1355 .visible(true)
1356 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1357 .placement(TooltipPlacement::BottomLeft);
1358
1359 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1360
1361 let mut canvas = RecordingCanvas::new();
1362 tooltip.paint(&mut canvas);
1363
1364 assert!(canvas.command_count() >= 3);
1365 }
1366
1367 #[test]
1368 fn test_tooltip_paint_bottomright_arrow() {
1369 use presentar_core::RecordingCanvas;
1370
1371 let mut tooltip = Tooltip::new("Help text")
1372 .visible(true)
1373 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1374 .placement(TooltipPlacement::BottomRight);
1375
1376 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1377
1378 let mut canvas = RecordingCanvas::new();
1379 tooltip.paint(&mut canvas);
1380
1381 assert!(canvas.command_count() >= 3);
1382 }
1383
1384 #[test]
1385 fn test_tooltip_layout_empty() {
1386 let mut tooltip = Tooltip::default().visible(true);
1387 let result = tooltip.layout(Rect::new(0.0, 0.0, 200.0, 100.0));
1388 assert_eq!(result.size, Size::ZERO);
1389 }
1390
1391 #[test]
1392 fn test_tooltip_placement_clone() {
1393 let placement = TooltipPlacement::Bottom;
1394 let cloned = placement;
1395 assert_eq!(cloned, TooltipPlacement::Bottom);
1396 }
1397
1398 #[test]
1399 fn test_tooltip_placement_debug() {
1400 let placement = TooltipPlacement::Right;
1401 let debug = format!("{placement:?}");
1402 assert!(debug.contains("Right"));
1403 }
1404
1405 #[test]
1406 fn test_tooltip_clone() {
1407 let tooltip = Tooltip::new("Text")
1408 .placement(TooltipPlacement::Left)
1409 .delay_ms(500);
1410 let cloned = tooltip;
1411 assert_eq!(cloned.get_content(), "Text");
1412 assert_eq!(cloned.get_placement(), TooltipPlacement::Left);
1413 assert_eq!(cloned.get_delay_ms(), 500);
1414 }
1415
1416 #[test]
1417 fn test_tooltip_serde() {
1418 let tooltip = Tooltip::new("Help")
1419 .placement(TooltipPlacement::Bottom)
1420 .delay_ms(300);
1421
1422 let json = serde_json::to_string(&tooltip).unwrap();
1423 let deserialized: Tooltip = serde_json::from_str(&json).unwrap();
1424
1425 assert_eq!(deserialized.get_content(), "Help");
1426 assert_eq!(deserialized.get_placement(), TooltipPlacement::Bottom);
1427 assert_eq!(deserialized.get_delay_ms(), 300);
1428 }
1429
1430 #[test]
1431 fn test_tooltip_placement_serde() {
1432 let placement = TooltipPlacement::TopRight;
1433 let json = serde_json::to_string(&placement).unwrap();
1434 let deserialized: TooltipPlacement = serde_json::from_str(&json).unwrap();
1435 assert_eq!(deserialized, TooltipPlacement::TopRight);
1436 }
1437
1438 #[test]
1439 fn test_tooltip_bounds_after_layout() {
1440 let mut tooltip = Tooltip::new("Test tooltip text")
1441 .visible(true)
1442 .anchor(Rect::new(100.0, 100.0, 80.0, 30.0))
1443 .placement(TooltipPlacement::Top);
1444
1445 tooltip.layout(Rect::new(0.0, 0.0, 500.0, 500.0));
1446
1447 assert!(tooltip.bounds.width > 0.0);
1449 assert!(tooltip.bounds.height > 0.0);
1450 }
1451}