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 #[must_use]
67 pub const fn union(self, other: Self) -> Self {
68 Self(self.0 | other.0)
69 }
70}
71
72impl core::ops::BitOr for StyleFlags {
73 type Output = Self;
74
75 #[inline]
76 fn bitor(self, rhs: Self) -> Self::Output {
77 Self(self.0 | rhs.0)
78 }
79}
80
81impl core::ops::BitOrAssign for StyleFlags {
82 #[inline]
83 fn bitor_assign(&mut self, rhs: Self) {
84 self.0 |= rhs.0;
85 }
86}
87
88#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
108pub struct Style {
109 pub fg: Option<PackedRgba>,
111 pub bg: Option<PackedRgba>,
113 pub attrs: Option<StyleFlags>,
115 pub underline_color: Option<PackedRgba>,
117}
118
119impl Style {
120 #[inline]
122 pub const fn new() -> Self {
123 Self {
124 fg: None,
125 bg: None,
126 attrs: None,
127 underline_color: None,
128 }
129 }
130
131 #[inline]
133 #[must_use]
134 pub fn fg<C: Into<PackedRgba>>(mut self, color: C) -> Self {
135 self.fg = Some(color.into());
136 self
137 }
138
139 #[inline]
141 #[must_use]
142 pub fn bg<C: Into<PackedRgba>>(mut self, color: C) -> Self {
143 self.bg = Some(color.into());
144 self
145 }
146
147 #[inline]
149 #[must_use]
150 pub fn bold(self) -> Self {
151 self.add_attr(StyleFlags::BOLD)
152 }
153
154 #[inline]
156 #[must_use]
157 pub fn italic(self) -> Self {
158 self.add_attr(StyleFlags::ITALIC)
159 }
160
161 #[inline]
163 #[must_use]
164 pub fn underline(self) -> Self {
165 self.add_attr(StyleFlags::UNDERLINE)
166 }
167
168 #[inline]
170 #[must_use]
171 pub fn dim(self) -> Self {
172 self.add_attr(StyleFlags::DIM)
173 }
174
175 #[inline]
177 #[must_use]
178 pub fn reverse(self) -> Self {
179 self.add_attr(StyleFlags::REVERSE)
180 }
181
182 #[inline]
184 #[must_use]
185 pub fn strikethrough(self) -> Self {
186 self.add_attr(StyleFlags::STRIKETHROUGH)
187 }
188
189 #[inline]
191 #[must_use]
192 pub fn blink(self) -> Self {
193 self.add_attr(StyleFlags::BLINK)
194 }
195
196 #[inline]
198 #[must_use]
199 pub fn hidden(self) -> Self {
200 self.add_attr(StyleFlags::HIDDEN)
201 }
202
203 #[inline]
205 #[must_use]
206 pub fn double_underline(self) -> Self {
207 self.add_attr(StyleFlags::DOUBLE_UNDERLINE)
208 }
209
210 #[inline]
212 #[must_use]
213 pub fn curly_underline(self) -> Self {
214 self.add_attr(StyleFlags::CURLY_UNDERLINE)
215 }
216
217 #[inline]
219 fn add_attr(mut self, flag: StyleFlags) -> Self {
220 match &mut self.attrs {
221 Some(attrs) => attrs.insert(flag),
222 None => self.attrs = Some(flag),
223 }
224 self
225 }
226
227 #[inline]
229 #[must_use]
230 pub const fn underline_color(mut self, color: PackedRgba) -> Self {
231 self.underline_color = Some(color);
232 self
233 }
234
235 #[inline]
237 #[must_use]
238 pub const fn attrs(mut self, attrs: StyleFlags) -> Self {
239 self.attrs = Some(attrs);
240 self
241 }
242
243 #[instrument(skip(self, parent), level = "trace")]
262 pub fn merge(&self, parent: &Style) -> Style {
263 trace!("Merging child style into parent");
264 Style {
265 fg: self.fg.or(parent.fg),
266 bg: self.bg.or(parent.bg),
267 attrs: match (self.attrs, parent.attrs) {
268 (Some(c), Some(p)) => Some(c.union(p)),
269 (Some(c), None) => Some(c),
270 (None, Some(p)) => Some(p),
271 (None, None) => None,
272 },
273 underline_color: self.underline_color.or(parent.underline_color),
274 }
275 }
276
277 #[inline]
284 pub fn patch(&self, child: &Style) -> Style {
285 child.merge(self)
286 }
287
288 #[inline]
290 pub const fn is_empty(&self) -> bool {
291 self.fg.is_none()
292 && self.bg.is_none()
293 && self.attrs.is_none()
294 && self.underline_color.is_none()
295 }
296
297 #[inline]
299 pub fn has_attr(&self, flag: StyleFlags) -> bool {
300 self.attrs.is_some_and(|a| a.contains(flag))
301 }
302}
303
304impl From<ftui_render::cell::StyleFlags> for StyleFlags {
306 fn from(flags: ftui_render::cell::StyleFlags) -> Self {
307 let mut result = StyleFlags::NONE;
308 if flags.contains(ftui_render::cell::StyleFlags::BOLD) {
309 result.insert(StyleFlags::BOLD);
310 }
311 if flags.contains(ftui_render::cell::StyleFlags::DIM) {
312 result.insert(StyleFlags::DIM);
313 }
314 if flags.contains(ftui_render::cell::StyleFlags::ITALIC) {
315 result.insert(StyleFlags::ITALIC);
316 }
317 if flags.contains(ftui_render::cell::StyleFlags::UNDERLINE) {
318 result.insert(StyleFlags::UNDERLINE);
319 }
320 if flags.contains(ftui_render::cell::StyleFlags::BLINK) {
321 result.insert(StyleFlags::BLINK);
322 }
323 if flags.contains(ftui_render::cell::StyleFlags::REVERSE) {
324 result.insert(StyleFlags::REVERSE);
325 }
326 if flags.contains(ftui_render::cell::StyleFlags::STRIKETHROUGH) {
327 result.insert(StyleFlags::STRIKETHROUGH);
328 }
329 if flags.contains(ftui_render::cell::StyleFlags::HIDDEN) {
330 result.insert(StyleFlags::HIDDEN);
331 }
332 result
333 }
334}
335
336impl From<StyleFlags> for ftui_render::cell::StyleFlags {
341 fn from(flags: StyleFlags) -> Self {
342 use ftui_render::cell::StyleFlags as CellFlags;
343 let mut result = CellFlags::empty();
344 if flags.contains(StyleFlags::BOLD) {
345 result |= CellFlags::BOLD;
346 }
347 if flags.contains(StyleFlags::DIM) {
348 result |= CellFlags::DIM;
349 }
350 if flags.contains(StyleFlags::ITALIC) {
351 result |= CellFlags::ITALIC;
352 }
353 if flags.contains(StyleFlags::UNDERLINE)
355 || flags.contains(StyleFlags::DOUBLE_UNDERLINE)
356 || flags.contains(StyleFlags::CURLY_UNDERLINE)
357 {
358 result |= CellFlags::UNDERLINE;
359 }
360 if flags.contains(StyleFlags::BLINK) {
361 result |= CellFlags::BLINK;
362 }
363 if flags.contains(StyleFlags::REVERSE) {
364 result |= CellFlags::REVERSE;
365 }
366 if flags.contains(StyleFlags::STRIKETHROUGH) {
367 result |= CellFlags::STRIKETHROUGH;
368 }
369 if flags.contains(StyleFlags::HIDDEN) {
370 result |= CellFlags::HIDDEN;
371 }
372 result
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn test_default_is_empty() {
382 let s = Style::default();
383 assert!(s.is_empty());
384 assert_eq!(s.fg, None);
385 assert_eq!(s.bg, None);
386 assert_eq!(s.attrs, None);
387 assert_eq!(s.underline_color, None);
388 }
389
390 #[test]
391 fn test_new_is_empty() {
392 let s = Style::new();
393 assert!(s.is_empty());
394 }
395
396 #[test]
397 fn test_builder_pattern_colors() {
398 let red = PackedRgba::rgb(255, 0, 0);
399 let black = PackedRgba::rgb(0, 0, 0);
400
401 let s = Style::new().fg(red).bg(black);
402
403 assert_eq!(s.fg, Some(red));
404 assert_eq!(s.bg, Some(black));
405 assert!(!s.is_empty());
406 }
407
408 #[test]
409 fn test_builder_pattern_attrs() {
410 let s = Style::new().bold().underline().italic();
411
412 assert!(s.has_attr(StyleFlags::BOLD));
413 assert!(s.has_attr(StyleFlags::UNDERLINE));
414 assert!(s.has_attr(StyleFlags::ITALIC));
415 assert!(!s.has_attr(StyleFlags::DIM));
416 }
417
418 #[test]
419 fn test_all_attribute_builders() {
420 let s = Style::new()
421 .bold()
422 .dim()
423 .italic()
424 .underline()
425 .blink()
426 .reverse()
427 .hidden()
428 .strikethrough()
429 .double_underline()
430 .curly_underline();
431
432 assert!(s.has_attr(StyleFlags::BOLD));
433 assert!(s.has_attr(StyleFlags::DIM));
434 assert!(s.has_attr(StyleFlags::ITALIC));
435 assert!(s.has_attr(StyleFlags::UNDERLINE));
436 assert!(s.has_attr(StyleFlags::BLINK));
437 assert!(s.has_attr(StyleFlags::REVERSE));
438 assert!(s.has_attr(StyleFlags::HIDDEN));
439 assert!(s.has_attr(StyleFlags::STRIKETHROUGH));
440 assert!(s.has_attr(StyleFlags::DOUBLE_UNDERLINE));
441 assert!(s.has_attr(StyleFlags::CURLY_UNDERLINE));
442 }
443
444 #[test]
445 fn test_merge_child_wins_on_conflict() {
446 let red = PackedRgba::rgb(255, 0, 0);
447 let blue = PackedRgba::rgb(0, 0, 255);
448
449 let parent = Style::new().fg(red);
450 let child = Style::new().fg(blue);
451 let merged = child.merge(&parent);
452
453 assert_eq!(merged.fg, Some(blue)); }
455
456 #[test]
457 fn test_merge_parent_fills_gaps() {
458 let red = PackedRgba::rgb(255, 0, 0);
459 let blue = PackedRgba::rgb(0, 0, 255);
460 let white = PackedRgba::rgb(255, 255, 255);
461
462 let parent = Style::new().fg(red).bg(white);
463 let child = Style::new().fg(blue); let merged = child.merge(&parent);
465
466 assert_eq!(merged.fg, Some(blue)); assert_eq!(merged.bg, Some(white)); }
469
470 #[test]
471 fn test_merge_attrs_combine() {
472 let parent = Style::new().bold();
473 let child = Style::new().italic();
474 let merged = child.merge(&parent);
475
476 assert!(merged.has_attr(StyleFlags::BOLD)); assert!(merged.has_attr(StyleFlags::ITALIC)); }
479
480 #[test]
481 fn test_merge_with_empty_returns_self() {
482 let red = PackedRgba::rgb(255, 0, 0);
483 let style = Style::new().fg(red).bold();
484 let empty = Style::default();
485
486 let merged = style.merge(&empty);
487 assert_eq!(merged, style);
488 }
489
490 #[test]
491 fn test_empty_merge_with_parent() {
492 let red = PackedRgba::rgb(255, 0, 0);
493 let parent = Style::new().fg(red).bold();
494 let child = Style::default();
495
496 let merged = child.merge(&parent);
497 assert_eq!(merged, parent);
498 }
499
500 #[test]
501 fn test_patch_is_symmetric_with_merge() {
502 let red = PackedRgba::rgb(255, 0, 0);
503 let blue = PackedRgba::rgb(0, 0, 255);
504
505 let parent = Style::new().fg(red);
506 let child = Style::new().bg(blue);
507
508 let merged1 = child.merge(&parent);
509 let merged2 = parent.patch(&child);
510
511 assert_eq!(merged1, merged2);
512 }
513
514 #[test]
515 fn test_underline_color() {
516 let red = PackedRgba::rgb(255, 0, 0);
517 let s = Style::new().underline().underline_color(red);
518
519 assert!(s.has_attr(StyleFlags::UNDERLINE));
520 assert_eq!(s.underline_color, Some(red));
521 }
522
523 #[test]
524 fn test_style_flags_operations() {
525 let mut flags = StyleFlags::NONE;
526 assert!(flags.is_empty());
527
528 flags.insert(StyleFlags::BOLD);
529 flags.insert(StyleFlags::ITALIC);
530
531 assert!(flags.contains(StyleFlags::BOLD));
532 assert!(flags.contains(StyleFlags::ITALIC));
533 assert!(!flags.contains(StyleFlags::UNDERLINE));
534 assert!(!flags.is_empty());
535
536 flags.remove(StyleFlags::BOLD);
537 assert!(!flags.contains(StyleFlags::BOLD));
538 assert!(flags.contains(StyleFlags::ITALIC));
539 }
540
541 #[test]
542 fn test_style_flags_bitor() {
543 let flags = StyleFlags::BOLD | StyleFlags::ITALIC;
544 assert!(flags.contains(StyleFlags::BOLD));
545 assert!(flags.contains(StyleFlags::ITALIC));
546 }
547
548 #[test]
549 fn test_style_flags_bitor_assign() {
550 let mut flags = StyleFlags::BOLD;
551 flags |= StyleFlags::ITALIC;
552 assert!(flags.contains(StyleFlags::BOLD));
553 assert!(flags.contains(StyleFlags::ITALIC));
554 }
555
556 #[test]
557 fn test_style_flags_union() {
558 let a = StyleFlags::BOLD;
559 let b = StyleFlags::ITALIC;
560 let c = a.union(b);
561 assert!(c.contains(StyleFlags::BOLD));
562 assert!(c.contains(StyleFlags::ITALIC));
563 }
564
565 #[test]
566 fn test_style_size() {
567 assert!(
572 core::mem::size_of::<Style>() <= 40,
573 "Style is {} bytes, expected <= 40",
574 core::mem::size_of::<Style>()
575 );
576 }
577
578 #[test]
579 fn test_style_flags_size() {
580 assert_eq!(core::mem::size_of::<StyleFlags>(), 2);
581 }
582
583 #[test]
584 fn test_convert_to_cell_flags() {
585 let flags = StyleFlags::BOLD | StyleFlags::ITALIC | StyleFlags::UNDERLINE;
586 let cell_flags: ftui_render::cell::StyleFlags = flags.into();
587
588 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::BOLD));
589 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::ITALIC));
590 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::UNDERLINE));
591 }
592
593 #[test]
594 fn test_convert_to_cell_flags_all_basic() {
595 let flags = StyleFlags::BOLD
596 | StyleFlags::DIM
597 | StyleFlags::ITALIC
598 | StyleFlags::UNDERLINE
599 | StyleFlags::BLINK
600 | StyleFlags::REVERSE
601 | StyleFlags::STRIKETHROUGH
602 | StyleFlags::HIDDEN;
603 let cell_flags: ftui_render::cell::StyleFlags = flags.into();
604
605 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::BOLD));
606 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::DIM));
607 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::ITALIC));
608 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::UNDERLINE));
609 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::BLINK));
610 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::REVERSE));
611 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::STRIKETHROUGH));
612 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::HIDDEN));
613 }
614
615 #[test]
616 fn test_convert_from_cell_flags() {
617 use ftui_render::cell::StyleFlags as CellFlags;
618 let cell_flags = CellFlags::BOLD | CellFlags::ITALIC;
619 let style_flags: StyleFlags = cell_flags.into();
620
621 assert!(style_flags.contains(StyleFlags::BOLD));
622 assert!(style_flags.contains(StyleFlags::ITALIC));
623 }
624
625 #[test]
626 fn test_cell_flags_round_trip_preserves_basic_flags() {
627 use ftui_render::cell::StyleFlags as CellFlags;
628 let original = StyleFlags::BOLD
629 | StyleFlags::DIM
630 | StyleFlags::ITALIC
631 | StyleFlags::UNDERLINE
632 | StyleFlags::BLINK
633 | StyleFlags::REVERSE
634 | StyleFlags::STRIKETHROUGH
635 | StyleFlags::HIDDEN;
636 let cell_flags: CellFlags = original.into();
637 let round_trip: StyleFlags = cell_flags.into();
638
639 assert!(round_trip.contains(StyleFlags::BOLD));
640 assert!(round_trip.contains(StyleFlags::DIM));
641 assert!(round_trip.contains(StyleFlags::ITALIC));
642 assert!(round_trip.contains(StyleFlags::UNDERLINE));
643 assert!(round_trip.contains(StyleFlags::BLINK));
644 assert!(round_trip.contains(StyleFlags::REVERSE));
645 assert!(round_trip.contains(StyleFlags::STRIKETHROUGH));
646 assert!(round_trip.contains(StyleFlags::HIDDEN));
647 }
648
649 #[test]
650 fn test_extended_underline_maps_to_basic() {
651 let flags = StyleFlags::DOUBLE_UNDERLINE | StyleFlags::CURLY_UNDERLINE;
652 let cell_flags: ftui_render::cell::StyleFlags = flags.into();
653
654 assert!(cell_flags.contains(ftui_render::cell::StyleFlags::UNDERLINE));
656 }
657}
658
659#[cfg(test)]
660mod property_tests {
661 use super::*;
662 use proptest::prelude::*;
663
664 fn arb_packed_rgba() -> impl Strategy<Value = PackedRgba> {
665 any::<u32>().prop_map(PackedRgba)
666 }
667
668 fn arb_style_flags() -> impl Strategy<Value = StyleFlags> {
669 any::<u16>().prop_map(StyleFlags)
670 }
671
672 fn arb_style() -> impl Strategy<Value = Style> {
673 (
674 proptest::option::of(arb_packed_rgba()),
675 proptest::option::of(arb_packed_rgba()),
676 proptest::option::of(arb_style_flags()),
677 proptest::option::of(arb_packed_rgba()),
678 )
679 .prop_map(|(fg, bg, attrs, underline_color)| Style {
680 fg,
681 bg,
682 attrs,
683 underline_color,
684 })
685 }
686
687 proptest! {
688 #[test]
689 fn merge_with_empty_is_identity(s in arb_style()) {
690 let empty = Style::default();
691 prop_assert_eq!(s.merge(&empty), s);
692 }
693
694 #[test]
695 fn empty_merge_with_any_equals_any(parent in arb_style()) {
696 let empty = Style::default();
697 prop_assert_eq!(empty.merge(&parent), parent);
698 }
699
700 #[test]
701 fn merge_is_deterministic(a in arb_style(), b in arb_style()) {
702 let merged1 = a.merge(&b);
703 let merged2 = a.merge(&b);
704 prop_assert_eq!(merged1, merged2);
705 }
706
707 #[test]
708 fn patch_equals_reverse_merge(parent in arb_style(), child in arb_style()) {
709 let via_merge = child.merge(&parent);
710 let via_patch = parent.patch(&child);
711 prop_assert_eq!(via_merge, via_patch);
712 }
713
714 #[test]
715 fn style_flags_union_is_commutative(a in arb_style_flags(), b in arb_style_flags()) {
716 prop_assert_eq!(a.union(b), b.union(a));
717 }
718
719 #[test]
720 fn style_flags_union_is_associative(
721 a in arb_style_flags(),
722 b in arb_style_flags(),
723 c in arb_style_flags()
724 ) {
725 prop_assert_eq!(a.union(b).union(c), a.union(b.union(c)));
726 }
727 }
728}
729
730#[cfg(test)]
731mod merge_semantic_tests {
732 use super::*;
738
739 #[test]
740 fn merge_chain_three_styles() {
741 let red = PackedRgba::rgb(255, 0, 0);
743 let green = PackedRgba::rgb(0, 255, 0);
744 let blue = PackedRgba::rgb(0, 0, 255);
745 let white = PackedRgba::rgb(255, 255, 255);
746
747 let grandparent = Style::new().fg(red).bg(white).bold();
748 let parent = Style::new().fg(green).italic();
749 let child = Style::new().fg(blue);
750
751 let parent_merged = parent.merge(&grandparent);
753 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);
760 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)); }
765
766 #[test]
767 fn merge_chain_attrs_accumulate() {
768 let s1 = Style::new().bold();
770 let s2 = Style::new().italic();
771 let s3 = Style::new().underline();
772
773 let merged = s3.merge(&s2.merge(&s1));
774
775 assert!(merged.has_attr(StyleFlags::BOLD));
776 assert!(merged.has_attr(StyleFlags::ITALIC));
777 assert!(merged.has_attr(StyleFlags::UNDERLINE));
778 }
779
780 #[test]
781 fn has_attr_returns_false_for_none() {
782 let style = Style::new(); assert!(!style.has_attr(StyleFlags::BOLD));
784 assert!(!style.has_attr(StyleFlags::ITALIC));
785 assert!(!style.has_attr(StyleFlags::NONE));
786 }
787
788 #[test]
789 fn has_attr_returns_true_for_set_flags() {
790 let style = Style::new().bold().italic();
791 assert!(style.has_attr(StyleFlags::BOLD));
792 assert!(style.has_attr(StyleFlags::ITALIC));
793 assert!(!style.has_attr(StyleFlags::UNDERLINE));
794 }
795
796 #[test]
797 fn attrs_method_sets_directly() {
798 let flags = StyleFlags::BOLD | StyleFlags::DIM | StyleFlags::ITALIC;
799 let style = Style::new().attrs(flags);
800
801 assert_eq!(style.attrs, Some(flags));
802 assert!(style.has_attr(StyleFlags::BOLD));
803 assert!(style.has_attr(StyleFlags::DIM));
804 assert!(style.has_attr(StyleFlags::ITALIC));
805 }
806
807 #[test]
808 fn attrs_method_overwrites_previous() {
809 let style = Style::new().bold().italic().attrs(StyleFlags::UNDERLINE); assert!(style.has_attr(StyleFlags::UNDERLINE));
812 assert!(!style.has_attr(StyleFlags::BOLD));
814 assert!(!style.has_attr(StyleFlags::ITALIC));
815 }
816
817 #[test]
818 fn merge_preserves_explicit_transparent_color() {
819 let transparent = PackedRgba::TRANSPARENT;
821 let red = PackedRgba::rgb(255, 0, 0);
822
823 let parent = Style::new().fg(red);
824 let child = Style::new().fg(transparent);
825
826 let merged = child.merge(&parent);
827 assert_eq!(merged.fg, Some(transparent));
829 }
830
831 #[test]
832 fn merge_all_fields_independently() {
833 let parent = Style::new()
834 .fg(PackedRgba::rgb(1, 1, 1))
835 .bg(PackedRgba::rgb(2, 2, 2))
836 .underline_color(PackedRgba::rgb(3, 3, 3))
837 .bold();
838
839 let child = Style::new()
840 .fg(PackedRgba::rgb(10, 10, 10))
841 .underline_color(PackedRgba::rgb(30, 30, 30))
843 .italic();
844
845 let merged = child.merge(&parent);
846
847 assert_eq!(merged.fg, Some(PackedRgba::rgb(10, 10, 10)));
849 assert_eq!(merged.bg, Some(PackedRgba::rgb(2, 2, 2)));
851 assert_eq!(merged.underline_color, Some(PackedRgba::rgb(30, 30, 30)));
853 assert!(merged.has_attr(StyleFlags::BOLD));
855 assert!(merged.has_attr(StyleFlags::ITALIC));
856 }
857
858 #[test]
859 fn style_is_copy() {
860 let style = Style::new().fg(PackedRgba::rgb(255, 0, 0)).bold();
861 let copy = style; assert_eq!(style, copy);
863 }
864
865 #[test]
866 fn style_is_eq() {
867 let a = Style::new().fg(PackedRgba::rgb(255, 0, 0)).bold();
868 let b = Style::new().fg(PackedRgba::rgb(255, 0, 0)).bold();
869 let c = Style::new().fg(PackedRgba::rgb(0, 255, 0)).bold();
870
871 assert_eq!(a, b);
872 assert_ne!(a, c);
873 }
874
875 #[test]
876 fn style_is_hashable() {
877 use std::collections::HashSet;
878 let mut set = HashSet::new();
879
880 let a = Style::new().fg(PackedRgba::rgb(255, 0, 0)).bold();
881 let b = Style::new().fg(PackedRgba::rgb(0, 255, 0)).italic();
882
883 set.insert(a);
884 set.insert(b);
885 set.insert(a); assert_eq!(set.len(), 2);
888 }
889
890 #[test]
891 fn style_flags_contains_combined() {
892 let combined = StyleFlags::BOLD | StyleFlags::ITALIC | StyleFlags::UNDERLINE;
893
894 assert!(combined.contains(StyleFlags::BOLD));
896 assert!(combined.contains(StyleFlags::ITALIC));
897 assert!(combined.contains(StyleFlags::UNDERLINE));
898
899 assert!(combined.contains(StyleFlags::BOLD | StyleFlags::ITALIC));
901
902 assert!(!combined.contains(StyleFlags::DIM));
904 assert!(!combined.contains(StyleFlags::BOLD | StyleFlags::DIM));
905 }
906
907 #[test]
908 fn style_flags_none_is_identity_for_union() {
909 let flags = StyleFlags::BOLD | StyleFlags::ITALIC;
910 assert_eq!(flags.union(StyleFlags::NONE), flags);
911 assert_eq!(StyleFlags::NONE.union(flags), flags);
912 }
913
914 #[test]
915 fn style_flags_remove_nonexistent_is_noop() {
916 let mut flags = StyleFlags::BOLD;
917 flags.remove(StyleFlags::ITALIC); assert!(flags.contains(StyleFlags::BOLD));
919 assert!(!flags.contains(StyleFlags::ITALIC));
920 }
921}
922
923#[cfg(test)]
924mod performance_tests {
925 use super::*;
926
927 #[test]
928 fn test_style_merge_performance() {
929 let red = PackedRgba::rgb(255, 0, 0);
930 let blue = PackedRgba::rgb(0, 0, 255);
931
932 let parent = Style::new().fg(red).bold();
933 let child = Style::new().bg(blue).italic();
934
935 let start = std::time::Instant::now();
936 for _ in 0..1_000_000 {
937 let _ = std::hint::black_box(child.merge(&parent));
938 }
939 let elapsed = start.elapsed();
940
941 assert!(
944 elapsed.as_millis() < 100,
945 "Merge too slow: {:?} for 1M iterations",
946 elapsed
947 );
948 }
949}