1#![forbid(unsafe_code)]
2
3use ftui_render::cell::PackedRgba;
6use tracing::{instrument, trace};
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
13#[repr(transparent)]
14pub struct StyleFlags(pub u16);
15
16impl StyleFlags {
17 pub const NONE: Self = Self(0);
19 pub const BOLD: Self = Self(1 << 0);
21 pub const DIM: Self = Self(1 << 1);
23 pub const ITALIC: Self = Self(1 << 2);
25 pub const UNDERLINE: Self = Self(1 << 3);
27 pub const BLINK: Self = Self(1 << 4);
29 pub const REVERSE: Self = Self(1 << 5);
31 pub const HIDDEN: Self = Self(1 << 6);
33 pub const STRIKETHROUGH: Self = Self(1 << 7);
35 pub const DOUBLE_UNDERLINE: Self = Self(1 << 8);
37 pub const CURLY_UNDERLINE: Self = Self(1 << 9);
39
40 #[inline]
42 pub const fn contains(self, other: Self) -> bool {
43 (self.0 & other.0) == other.0
44 }
45
46 #[inline]
48 pub fn insert(&mut self, other: Self) {
49 self.0 |= other.0;
50 }
51
52 #[inline]
54 pub fn remove(&mut self, other: Self) {
55 self.0 &= !other.0;
56 }
57
58 #[inline]
60 pub const fn is_empty(self) -> bool {
61 self.0 == 0
62 }
63
64 #[inline]
66 pub const fn union(self, other: Self) -> Self {
67 Self(self.0 | other.0)
68 }
69}
70
71impl core::ops::BitOr for StyleFlags {
72 type Output = Self;
73
74 #[inline]
75 fn bitor(self, rhs: Self) -> Self::Output {
76 Self(self.0 | rhs.0)
77 }
78}
79
80impl core::ops::BitOrAssign for StyleFlags {
81 #[inline]
82 fn bitor_assign(&mut self, rhs: Self) {
83 self.0 |= rhs.0;
84 }
85}
86
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
107pub struct Style {
108 pub fg: Option<PackedRgba>,
110 pub bg: Option<PackedRgba>,
112 pub attrs: Option<StyleFlags>,
114 pub underline_color: Option<PackedRgba>,
116}
117
118impl Style {
119 #[inline]
121 pub const fn new() -> Self {
122 Self {
123 fg: None,
124 bg: None,
125 attrs: None,
126 underline_color: None,
127 }
128 }
129
130 #[inline]
132 pub fn fg<C: Into<PackedRgba>>(mut self, color: C) -> Self {
133 self.fg = Some(color.into());
134 self
135 }
136
137 #[inline]
139 pub fn bg<C: Into<PackedRgba>>(mut self, color: C) -> Self {
140 self.bg = Some(color.into());
141 self
142 }
143
144 #[inline]
146 pub fn bold(self) -> Self {
147 self.add_attr(StyleFlags::BOLD)
148 }
149
150 #[inline]
152 pub fn italic(self) -> Self {
153 self.add_attr(StyleFlags::ITALIC)
154 }
155
156 #[inline]
158 pub fn underline(self) -> Self {
159 self.add_attr(StyleFlags::UNDERLINE)
160 }
161
162 #[inline]
164 pub fn dim(self) -> Self {
165 self.add_attr(StyleFlags::DIM)
166 }
167
168 #[inline]
170 pub fn reverse(self) -> Self {
171 self.add_attr(StyleFlags::REVERSE)
172 }
173
174 #[inline]
176 pub fn strikethrough(self) -> Self {
177 self.add_attr(StyleFlags::STRIKETHROUGH)
178 }
179
180 #[inline]
182 pub fn blink(self) -> Self {
183 self.add_attr(StyleFlags::BLINK)
184 }
185
186 #[inline]
188 pub fn hidden(self) -> Self {
189 self.add_attr(StyleFlags::HIDDEN)
190 }
191
192 #[inline]
194 pub fn double_underline(self) -> Self {
195 self.add_attr(StyleFlags::DOUBLE_UNDERLINE)
196 }
197
198 #[inline]
200 pub fn curly_underline(self) -> Self {
201 self.add_attr(StyleFlags::CURLY_UNDERLINE)
202 }
203
204 #[inline]
206 fn add_attr(mut self, flag: StyleFlags) -> Self {
207 match &mut self.attrs {
208 Some(attrs) => attrs.insert(flag),
209 None => self.attrs = Some(flag),
210 }
211 self
212 }
213
214 #[inline]
216 pub const fn underline_color(mut self, color: PackedRgba) -> Self {
217 self.underline_color = Some(color);
218 self
219 }
220
221 #[inline]
223 pub const fn attrs(mut self, attrs: StyleFlags) -> Self {
224 self.attrs = Some(attrs);
225 self
226 }
227
228 #[instrument(skip(self, parent), level = "trace")]
247 pub fn merge(&self, parent: &Style) -> Style {
248 trace!("Merging child style into parent");
249 Style {
250 fg: self.fg.or(parent.fg),
251 bg: self.bg.or(parent.bg),
252 attrs: match (self.attrs, parent.attrs) {
253 (Some(c), Some(p)) => Some(c.union(p)),
254 (Some(c), None) => Some(c),
255 (None, Some(p)) => Some(p),
256 (None, None) => None,
257 },
258 underline_color: self.underline_color.or(parent.underline_color),
259 }
260 }
261
262 #[inline]
269 pub fn patch(&self, child: &Style) -> Style {
270 child.merge(self)
271 }
272
273 #[inline]
275 pub const fn is_empty(&self) -> bool {
276 self.fg.is_none()
277 && self.bg.is_none()
278 && self.attrs.is_none()
279 && self.underline_color.is_none()
280 }
281
282 #[inline]
284 pub fn has_attr(&self, flag: StyleFlags) -> bool {
285 self.attrs.is_some_and(|a| a.contains(flag))
286 }
287}
288
289impl From<ftui_render::cell::StyleFlags> for StyleFlags {
291 fn from(flags: ftui_render::cell::StyleFlags) -> Self {
292 let mut result = StyleFlags::NONE;
293 if flags.contains(ftui_render::cell::StyleFlags::BOLD) {
294 result.insert(StyleFlags::BOLD);
295 }
296 if flags.contains(ftui_render::cell::StyleFlags::DIM) {
297 result.insert(StyleFlags::DIM);
298 }
299 if flags.contains(ftui_render::cell::StyleFlags::ITALIC) {
300 result.insert(StyleFlags::ITALIC);
301 }
302 if flags.contains(ftui_render::cell::StyleFlags::UNDERLINE) {
303 result.insert(StyleFlags::UNDERLINE);
304 }
305 if flags.contains(ftui_render::cell::StyleFlags::BLINK) {
306 result.insert(StyleFlags::BLINK);
307 }
308 if flags.contains(ftui_render::cell::StyleFlags::REVERSE) {
309 result.insert(StyleFlags::REVERSE);
310 }
311 if flags.contains(ftui_render::cell::StyleFlags::STRIKETHROUGH) {
312 result.insert(StyleFlags::STRIKETHROUGH);
313 }
314 if flags.contains(ftui_render::cell::StyleFlags::HIDDEN) {
315 result.insert(StyleFlags::HIDDEN);
316 }
317 result
318 }
319}
320
321impl From<StyleFlags> for ftui_render::cell::StyleFlags {
326 fn from(flags: StyleFlags) -> Self {
327 use ftui_render::cell::StyleFlags as CellFlags;
328 let mut result = CellFlags::empty();
329 if flags.contains(StyleFlags::BOLD) {
330 result |= CellFlags::BOLD;
331 }
332 if flags.contains(StyleFlags::DIM) {
333 result |= CellFlags::DIM;
334 }
335 if flags.contains(StyleFlags::ITALIC) {
336 result |= CellFlags::ITALIC;
337 }
338 if flags.contains(StyleFlags::UNDERLINE)
340 || flags.contains(StyleFlags::DOUBLE_UNDERLINE)
341 || flags.contains(StyleFlags::CURLY_UNDERLINE)
342 {
343 result |= CellFlags::UNDERLINE;
344 }
345 if flags.contains(StyleFlags::BLINK) {
346 result |= CellFlags::BLINK;
347 }
348 if flags.contains(StyleFlags::REVERSE) {
349 result |= CellFlags::REVERSE;
350 }
351 if flags.contains(StyleFlags::STRIKETHROUGH) {
352 result |= CellFlags::STRIKETHROUGH;
353 }
354 if flags.contains(StyleFlags::HIDDEN) {
355 result |= CellFlags::HIDDEN;
356 }
357 result
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
366 fn test_default_is_empty() {
367 let s = Style::default();
368 assert!(s.is_empty());
369 assert_eq!(s.fg, None);
370 assert_eq!(s.bg, None);
371 assert_eq!(s.attrs, None);
372 assert_eq!(s.underline_color, None);
373 }
374
375 #[test]
376 fn test_new_is_empty() {
377 let s = Style::new();
378 assert!(s.is_empty());
379 }
380
381 #[test]
382 fn test_builder_pattern_colors() {
383 let red = PackedRgba::rgb(255, 0, 0);
384 let black = PackedRgba::rgb(0, 0, 0);
385
386 let s = Style::new().fg(red).bg(black);
387
388 assert_eq!(s.fg, Some(red));
389 assert_eq!(s.bg, Some(black));
390 assert!(!s.is_empty());
391 }
392
393 #[test]
394 fn test_builder_pattern_attrs() {
395 let s = Style::new().bold().underline().italic();
396
397 assert!(s.has_attr(StyleFlags::BOLD));
398 assert!(s.has_attr(StyleFlags::UNDERLINE));
399 assert!(s.has_attr(StyleFlags::ITALIC));
400 assert!(!s.has_attr(StyleFlags::DIM));
401 }
402
403 #[test]
404 fn test_all_attribute_builders() {
405 let s = Style::new()
406 .bold()
407 .dim()
408 .italic()
409 .underline()
410 .blink()
411 .reverse()
412 .hidden()
413 .strikethrough()
414 .double_underline()
415 .curly_underline();
416
417 assert!(s.has_attr(StyleFlags::BOLD));
418 assert!(s.has_attr(StyleFlags::DIM));
419 assert!(s.has_attr(StyleFlags::ITALIC));
420 assert!(s.has_attr(StyleFlags::UNDERLINE));
421 assert!(s.has_attr(StyleFlags::BLINK));
422 assert!(s.has_attr(StyleFlags::REVERSE));
423 assert!(s.has_attr(StyleFlags::HIDDEN));
424 assert!(s.has_attr(StyleFlags::STRIKETHROUGH));
425 assert!(s.has_attr(StyleFlags::DOUBLE_UNDERLINE));
426 assert!(s.has_attr(StyleFlags::CURLY_UNDERLINE));
427 }
428
429 #[test]
430 fn test_merge_child_wins_on_conflict() {
431 let red = PackedRgba::rgb(255, 0, 0);
432 let blue = PackedRgba::rgb(0, 0, 255);
433
434 let parent = Style::new().fg(red);
435 let child = Style::new().fg(blue);
436 let merged = child.merge(&parent);
437
438 assert_eq!(merged.fg, Some(blue)); }
440
441 #[test]
442 fn test_merge_parent_fills_gaps() {
443 let red = PackedRgba::rgb(255, 0, 0);
444 let blue = PackedRgba::rgb(0, 0, 255);
445 let white = PackedRgba::rgb(255, 255, 255);
446
447 let parent = Style::new().fg(red).bg(white);
448 let child = Style::new().fg(blue); let merged = child.merge(&parent);
450
451 assert_eq!(merged.fg, Some(blue)); assert_eq!(merged.bg, Some(white)); }
454
455 #[test]
456 fn test_merge_attrs_combine() {
457 let parent = Style::new().bold();
458 let child = Style::new().italic();
459 let merged = child.merge(&parent);
460
461 assert!(merged.has_attr(StyleFlags::BOLD)); assert!(merged.has_attr(StyleFlags::ITALIC)); }
464
465 #[test]
466 fn test_merge_with_empty_returns_self() {
467 let red = PackedRgba::rgb(255, 0, 0);
468 let style = Style::new().fg(red).bold();
469 let empty = Style::default();
470
471 let merged = style.merge(&empty);
472 assert_eq!(merged, style);
473 }
474
475 #[test]
476 fn test_empty_merge_with_parent() {
477 let red = PackedRgba::rgb(255, 0, 0);
478 let parent = Style::new().fg(red).bold();
479 let child = Style::default();
480
481 let merged = child.merge(&parent);
482 assert_eq!(merged, parent);
483 }
484
485 #[test]
486 fn test_patch_is_symmetric_with_merge() {
487 let red = PackedRgba::rgb(255, 0, 0);
488 let blue = PackedRgba::rgb(0, 0, 255);
489
490 let parent = Style::new().fg(red);
491 let child = Style::new().bg(blue);
492
493 let merged1 = child.merge(&parent);
494 let merged2 = parent.patch(&child);
495
496 assert_eq!(merged1, merged2);
497 }
498
499 #[test]
500 fn test_underline_color() {
501 let red = PackedRgba::rgb(255, 0, 0);
502 let s = Style::new().underline().underline_color(red);
503
504 assert!(s.has_attr(StyleFlags::UNDERLINE));
505 assert_eq!(s.underline_color, Some(red));
506 }
507
508 #[test]
509 fn test_style_flags_operations() {
510 let mut flags = StyleFlags::NONE;
511 assert!(flags.is_empty());
512
513 flags.insert(StyleFlags::BOLD);
514 flags.insert(StyleFlags::ITALIC);
515
516 assert!(flags.contains(StyleFlags::BOLD));
517 assert!(flags.contains(StyleFlags::ITALIC));
518 assert!(!flags.contains(StyleFlags::UNDERLINE));
519 assert!(!flags.is_empty());
520
521 flags.remove(StyleFlags::BOLD);
522 assert!(!flags.contains(StyleFlags::BOLD));
523 assert!(flags.contains(StyleFlags::ITALIC));
524 }
525
526 #[test]
527 fn test_style_flags_bitor() {
528 let flags = StyleFlags::BOLD | StyleFlags::ITALIC;
529 assert!(flags.contains(StyleFlags::BOLD));
530 assert!(flags.contains(StyleFlags::ITALIC));
531 }
532
533 #[test]
534 fn test_style_flags_bitor_assign() {
535 let mut flags = StyleFlags::BOLD;
536 flags |= StyleFlags::ITALIC;
537 assert!(flags.contains(StyleFlags::BOLD));
538 assert!(flags.contains(StyleFlags::ITALIC));
539 }
540
541 #[test]
542 fn test_style_flags_union() {
543 let a = StyleFlags::BOLD;
544 let b = StyleFlags::ITALIC;
545 let c = a.union(b);
546 assert!(c.contains(StyleFlags::BOLD));
547 assert!(c.contains(StyleFlags::ITALIC));
548 }
549
550 #[test]
551 fn test_style_size() {
552 assert!(
557 core::mem::size_of::<Style>() <= 40,
558 "Style is {} bytes, expected <= 40",
559 core::mem::size_of::<Style>()
560 );
561 }
562
563 #[test]
564 fn test_style_flags_size() {
565 assert_eq!(core::mem::size_of::<StyleFlags>(), 2);
566 }
567
568 #[test]
569 fn test_convert_to_cell_flags() {
570 let flags = StyleFlags::BOLD | StyleFlags::ITALIC | StyleFlags::UNDERLINE;
571 let cell_flags: ftui_render::cell::StyleFlags = flags.into();
572
573 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::BOLD));
574 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::ITALIC));
575 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::UNDERLINE));
576 }
577
578 #[test]
579 fn test_convert_to_cell_flags_all_basic() {
580 let flags = StyleFlags::BOLD
581 | StyleFlags::DIM
582 | StyleFlags::ITALIC
583 | StyleFlags::UNDERLINE
584 | StyleFlags::BLINK
585 | StyleFlags::REVERSE
586 | StyleFlags::STRIKETHROUGH
587 | StyleFlags::HIDDEN;
588 let cell_flags: ftui_render::cell::StyleFlags = flags.into();
589
590 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::BOLD));
591 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::DIM));
592 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::ITALIC));
593 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::UNDERLINE));
594 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::BLINK));
595 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::REVERSE));
596 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::STRIKETHROUGH));
597 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::HIDDEN));
598 }
599
600 #[test]
601 fn test_convert_from_cell_flags() {
602 use ftui_render::cell::StyleFlags as CellFlags;
603 let cell_flags = CellFlags::BOLD | CellFlags::ITALIC;
604 let style_flags: StyleFlags = cell_flags.into();
605
606 assert!(style_flags.contains(StyleFlags::BOLD));
607 assert!(style_flags.contains(StyleFlags::ITALIC));
608 }
609
610 #[test]
611 fn test_cell_flags_round_trip_preserves_basic_flags() {
612 use ftui_render::cell::StyleFlags as CellFlags;
613 let original = StyleFlags::BOLD
614 | StyleFlags::DIM
615 | StyleFlags::ITALIC
616 | StyleFlags::UNDERLINE
617 | StyleFlags::BLINK
618 | StyleFlags::REVERSE
619 | StyleFlags::STRIKETHROUGH
620 | StyleFlags::HIDDEN;
621 let cell_flags: CellFlags = original.into();
622 let round_trip: StyleFlags = cell_flags.into();
623
624 assert!(round_trip.contains(StyleFlags::BOLD));
625 assert!(round_trip.contains(StyleFlags::DIM));
626 assert!(round_trip.contains(StyleFlags::ITALIC));
627 assert!(round_trip.contains(StyleFlags::UNDERLINE));
628 assert!(round_trip.contains(StyleFlags::BLINK));
629 assert!(round_trip.contains(StyleFlags::REVERSE));
630 assert!(round_trip.contains(StyleFlags::STRIKETHROUGH));
631 assert!(round_trip.contains(StyleFlags::HIDDEN));
632 }
633
634 #[test]
635 fn test_extended_underline_maps_to_basic() {
636 let flags = StyleFlags::DOUBLE_UNDERLINE | StyleFlags::CURLY_UNDERLINE;
637 let cell_flags: ftui_render::cell::StyleFlags = flags.into();
638
639 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::UNDERLINE));
641 }
642}
643
644#[cfg(test)]
645mod property_tests {
646 use super::*;
647 use proptest::prelude::*;
648
649 fn arb_packed_rgba() -> impl Strategy<Value = PackedRgba> {
650 any::<u32>().prop_map(PackedRgba)
651 }
652
653 fn arb_style_flags() -> impl Strategy<Value = StyleFlags> {
654 any::<u16>().prop_map(StyleFlags)
655 }
656
657 fn arb_style() -> impl Strategy<Value = Style> {
658 (
659 proptest::option::of(arb_packed_rgba()),
660 proptest::option::of(arb_packed_rgba()),
661 proptest::option::of(arb_style_flags()),
662 proptest::option::of(arb_packed_rgba()),
663 )
664 .prop_map(|(fg, bg, attrs, underline_color)| Style {
665 fg,
666 bg,
667 attrs,
668 underline_color,
669 })
670 }
671
672 proptest! {
673 #[test]
674 fn merge_with_empty_is_identity(s in arb_style()) {
675 let empty = Style::default();
676 prop_assert_eq!(s.merge(&empty), s);
677 }
678
679 #[test]
680 fn empty_merge_with_any_equals_any(parent in arb_style()) {
681 let empty = Style::default();
682 prop_assert_eq!(empty.merge(&parent), parent);
683 }
684
685 #[test]
686 fn merge_is_deterministic(a in arb_style(), b in arb_style()) {
687 let merged1 = a.merge(&b);
688 let merged2 = a.merge(&b);
689 prop_assert_eq!(merged1, merged2);
690 }
691
692 #[test]
693 fn patch_equals_reverse_merge(parent in arb_style(), child in arb_style()) {
694 let via_merge = child.merge(&parent);
695 let via_patch = parent.patch(&child);
696 prop_assert_eq!(via_merge, via_patch);
697 }
698
699 #[test]
700 fn style_flags_union_is_commutative(a in arb_style_flags(), b in arb_style_flags()) {
701 prop_assert_eq!(a.union(b), b.union(a));
702 }
703
704 #[test]
705 fn style_flags_union_is_associative(
706 a in arb_style_flags(),
707 b in arb_style_flags(),
708 c in arb_style_flags()
709 ) {
710 prop_assert_eq!(a.union(b).union(c), a.union(b.union(c)));
711 }
712 }
713}
714
715#[cfg(test)]
716mod merge_semantic_tests {
717 use super::*;
723
724 #[test]
725 fn merge_chain_three_styles() {
726 let red = PackedRgba::rgb(255, 0, 0);
728 let green = PackedRgba::rgb(0, 255, 0);
729 let blue = PackedRgba::rgb(0, 0, 255);
730 let white = PackedRgba::rgb(255, 255, 255);
731
732 let grandparent = Style::new().fg(red).bg(white).bold();
733 let parent = Style::new().fg(green).italic();
734 let child = Style::new().fg(blue);
735
736 let parent_merged = parent.merge(&grandparent);
738 assert_eq!(parent_merged.fg, Some(green)); assert_eq!(parent_merged.bg, Some(white)); assert!(parent_merged.has_attr(StyleFlags::BOLD)); assert!(parent_merged.has_attr(StyleFlags::ITALIC)); let child_merged = child.merge(&parent_merged);
745 assert_eq!(child_merged.fg, Some(blue)); assert_eq!(child_merged.bg, Some(white)); assert!(child_merged.has_attr(StyleFlags::BOLD)); assert!(child_merged.has_attr(StyleFlags::ITALIC)); }
750
751 #[test]
752 fn merge_chain_attrs_accumulate() {
753 let s1 = Style::new().bold();
755 let s2 = Style::new().italic();
756 let s3 = Style::new().underline();
757
758 let merged = s3.merge(&s2.merge(&s1));
759
760 assert!(merged.has_attr(StyleFlags::BOLD));
761 assert!(merged.has_attr(StyleFlags::ITALIC));
762 assert!(merged.has_attr(StyleFlags::UNDERLINE));
763 }
764
765 #[test]
766 fn has_attr_returns_false_for_none() {
767 let style = Style::new(); assert!(!style.has_attr(StyleFlags::BOLD));
769 assert!(!style.has_attr(StyleFlags::ITALIC));
770 assert!(!style.has_attr(StyleFlags::NONE));
771 }
772
773 #[test]
774 fn has_attr_returns_true_for_set_flags() {
775 let style = Style::new().bold().italic();
776 assert!(style.has_attr(StyleFlags::BOLD));
777 assert!(style.has_attr(StyleFlags::ITALIC));
778 assert!(!style.has_attr(StyleFlags::UNDERLINE));
779 }
780
781 #[test]
782 fn attrs_method_sets_directly() {
783 let flags = StyleFlags::BOLD | StyleFlags::DIM | StyleFlags::ITALIC;
784 let style = Style::new().attrs(flags);
785
786 assert_eq!(style.attrs, Some(flags));
787 assert!(style.has_attr(StyleFlags::BOLD));
788 assert!(style.has_attr(StyleFlags::DIM));
789 assert!(style.has_attr(StyleFlags::ITALIC));
790 }
791
792 #[test]
793 fn attrs_method_overwrites_previous() {
794 let style = Style::new().bold().italic().attrs(StyleFlags::UNDERLINE); assert!(style.has_attr(StyleFlags::UNDERLINE));
797 assert!(!style.has_attr(StyleFlags::BOLD));
799 assert!(!style.has_attr(StyleFlags::ITALIC));
800 }
801
802 #[test]
803 fn merge_preserves_explicit_transparent_color() {
804 let transparent = PackedRgba::TRANSPARENT;
806 let red = PackedRgba::rgb(255, 0, 0);
807
808 let parent = Style::new().fg(red);
809 let child = Style::new().fg(transparent);
810
811 let merged = child.merge(&parent);
812 assert_eq!(merged.fg, Some(transparent));
814 }
815
816 #[test]
817 fn merge_all_fields_independently() {
818 let parent = Style::new()
819 .fg(PackedRgba::rgb(1, 1, 1))
820 .bg(PackedRgba::rgb(2, 2, 2))
821 .underline_color(PackedRgba::rgb(3, 3, 3))
822 .bold();
823
824 let child = Style::new()
825 .fg(PackedRgba::rgb(10, 10, 10))
826 .underline_color(PackedRgba::rgb(30, 30, 30))
828 .italic();
829
830 let merged = child.merge(&parent);
831
832 assert_eq!(merged.fg, Some(PackedRgba::rgb(10, 10, 10)));
834 assert_eq!(merged.bg, Some(PackedRgba::rgb(2, 2, 2)));
836 assert_eq!(merged.underline_color, Some(PackedRgba::rgb(30, 30, 30)));
838 assert!(merged.has_attr(StyleFlags::BOLD));
840 assert!(merged.has_attr(StyleFlags::ITALIC));
841 }
842
843 #[test]
844 fn style_is_copy() {
845 let style = Style::new().fg(PackedRgba::rgb(255, 0, 0)).bold();
846 let copy = style; assert_eq!(style, copy);
848 }
849
850 #[test]
851 fn style_is_eq() {
852 let a = Style::new().fg(PackedRgba::rgb(255, 0, 0)).bold();
853 let b = Style::new().fg(PackedRgba::rgb(255, 0, 0)).bold();
854 let c = Style::new().fg(PackedRgba::rgb(0, 255, 0)).bold();
855
856 assert_eq!(a, b);
857 assert_ne!(a, c);
858 }
859
860 #[test]
861 fn style_is_hashable() {
862 use std::collections::HashSet;
863 let mut set = HashSet::new();
864
865 let a = Style::new().fg(PackedRgba::rgb(255, 0, 0)).bold();
866 let b = Style::new().fg(PackedRgba::rgb(0, 255, 0)).italic();
867
868 set.insert(a);
869 set.insert(b);
870 set.insert(a); assert_eq!(set.len(), 2);
873 }
874
875 #[test]
876 fn style_flags_contains_combined() {
877 let combined = StyleFlags::BOLD | StyleFlags::ITALIC | StyleFlags::UNDERLINE;
878
879 assert!(combined.contains(StyleFlags::BOLD));
881 assert!(combined.contains(StyleFlags::ITALIC));
882 assert!(combined.contains(StyleFlags::UNDERLINE));
883
884 assert!(combined.contains(StyleFlags::BOLD | StyleFlags::ITALIC));
886
887 assert!(!combined.contains(StyleFlags::DIM));
889 assert!(!combined.contains(StyleFlags::BOLD | StyleFlags::DIM));
890 }
891
892 #[test]
893 fn style_flags_none_is_identity_for_union() {
894 let flags = StyleFlags::BOLD | StyleFlags::ITALIC;
895 assert_eq!(flags.union(StyleFlags::NONE), flags);
896 assert_eq!(StyleFlags::NONE.union(flags), flags);
897 }
898
899 #[test]
900 fn style_flags_remove_nonexistent_is_noop() {
901 let mut flags = StyleFlags::BOLD;
902 flags.remove(StyleFlags::ITALIC); assert!(flags.contains(StyleFlags::BOLD));
904 assert!(!flags.contains(StyleFlags::ITALIC));
905 }
906}
907
908#[cfg(test)]
909mod performance_tests {
910 use super::*;
911
912 #[test]
913 fn test_style_merge_performance() {
914 let red = PackedRgba::rgb(255, 0, 0);
915 let blue = PackedRgba::rgb(0, 0, 255);
916
917 let parent = Style::new().fg(red).bold();
918 let child = Style::new().bg(blue).italic();
919
920 let start = std::time::Instant::now();
921 for _ in 0..1_000_000 {
922 let _ = std::hint::black_box(child.merge(&parent));
923 }
924 let elapsed = start.elapsed();
925
926 assert!(
929 elapsed.as_millis() < 100,
930 "Merge too slow: {:?} for 1M iterations",
931 elapsed
932 );
933 }
934}