1use std::{
2 hash::{Hash, Hasher},
3 iter, mem,
4 ops::Range,
5};
6
7use smallvec::SmallVec;
8
9use crate::{
10 AbsoluteLength, App, Background, BackgroundTag, BlendMode, BorderStyle, Bounds, ContentMask,
11 Corners, CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement,
12 Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, GridLocation, Hsla, Length, Pixels,
13 Point, PointRefinement, Radians, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun,
14 TransformationMatrix, Window, black, phi, point, quad, rems, size,
15};
16use collections::HashSet;
17use refineable::Refineable;
18use schemars::JsonSchema;
19use serde::{Deserialize, Serialize};
20
21#[cfg(debug_assertions)]
25pub struct DebugBelow;
26
27#[cfg(debug_assertions)]
28impl crate::Global for DebugBelow {}
29
30pub enum ObjectFit {
32 Fill,
34 Contain,
36 Cover,
38 ScaleDown,
40 None,
42}
43
44impl ObjectFit {
45 pub fn get_bounds(
47 &self,
48 bounds: Bounds<Pixels>,
49 image_size: Size<DevicePixels>,
50 ) -> Bounds<Pixels> {
51 let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension)));
52 let image_ratio = image_size.width / image_size.height;
53 let bounds_ratio = bounds.size.width / bounds.size.height;
54
55 match self {
56 ObjectFit::Fill => bounds,
57 ObjectFit::Contain => {
58 let new_size = if bounds_ratio > image_ratio {
59 size(
60 image_size.width * (bounds.size.height / image_size.height),
61 bounds.size.height,
62 )
63 } else {
64 size(
65 bounds.size.width,
66 image_size.height * (bounds.size.width / image_size.width),
67 )
68 };
69
70 Bounds {
71 origin: point(
72 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
73 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
74 ),
75 size: new_size,
76 }
77 }
78 ObjectFit::ScaleDown => {
79 if image_size.width > bounds.size.width || image_size.height > bounds.size.height {
81 let new_size = if bounds_ratio > image_ratio {
83 size(
84 image_size.width * (bounds.size.height / image_size.height),
85 bounds.size.height,
86 )
87 } else {
88 size(
89 bounds.size.width,
90 image_size.height * (bounds.size.width / image_size.width),
91 )
92 };
93
94 Bounds {
95 origin: point(
96 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
97 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
98 ),
99 size: new_size,
100 }
101 } else {
102 let original_size = size(image_size.width, image_size.height);
105 Bounds {
106 origin: point(
107 bounds.origin.x + (bounds.size.width - original_size.width) / 2.0,
108 bounds.origin.y + (bounds.size.height - original_size.height) / 2.0,
109 ),
110 size: original_size,
111 }
112 }
113 }
114 ObjectFit::Cover => {
115 let new_size = if bounds_ratio > image_ratio {
116 size(
117 bounds.size.width,
118 image_size.height * (bounds.size.width / image_size.width),
119 )
120 } else {
121 size(
122 image_size.width * (bounds.size.height / image_size.height),
123 bounds.size.height,
124 )
125 };
126
127 Bounds {
128 origin: point(
129 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
130 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
131 ),
132 size: new_size,
133 }
134 }
135 ObjectFit::None => Bounds {
136 origin: bounds.origin,
137 size: image_size,
138 },
139 }
140 }
141}
142
143#[derive(Clone, Refineable, Debug)]
145#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
146pub struct Style {
147 pub display: Display,
149
150 pub visibility: Visibility,
152
153 #[refineable]
156 pub overflow: Point<Overflow>,
157 pub scrollbar_width: AbsoluteLength,
159 pub allow_concurrent_scroll: bool,
161 pub restrict_scroll_to_axis: bool,
183
184 pub position: Position,
187 #[refineable]
189 pub inset: Edges<Length>,
190
191 #[refineable]
194 pub size: Size<Length>,
195 #[refineable]
197 pub min_size: Size<Length>,
198 #[refineable]
200 pub max_size: Size<Length>,
201 pub aspect_ratio: Option<f32>,
203
204 #[refineable]
207 pub margin: Edges<Length>,
208 #[refineable]
210 pub padding: Edges<DefiniteLength>,
211 #[refineable]
213 pub border_widths: Edges<AbsoluteLength>,
214
215 pub align_items: Option<AlignItems>,
218 pub align_self: Option<AlignSelf>,
220 pub align_content: Option<AlignContent>,
222 pub justify_content: Option<JustifyContent>,
224 #[refineable]
226 pub gap: Size<DefiniteLength>,
227
228 pub flex_direction: FlexDirection,
231 pub flex_wrap: FlexWrap,
233 pub flex_basis: Length,
235 pub flex_grow: f32,
237 pub flex_shrink: f32,
239
240 pub background: Option<Fill>,
242
243 pub border_color: Option<Hsla>,
245
246 pub border_style: BorderStyle,
248
249 #[refineable]
251 pub corner_radii: Corners<AbsoluteLength>,
252
253 pub continuous_corners: bool,
255
256 pub blend_mode: Option<BlendMode>,
258
259 pub box_shadow: SmallVec<[BoxShadow; 1]>,
261
262 pub text: TextStyleRefinement,
264
265 pub mouse_cursor: Option<CursorStyle>,
267
268 pub opacity: Option<f32>,
270
271 pub rotate: Option<f32>,
273
274 pub scale: Option<Point<f32>>,
276
277 pub transform_origin: Option<Point<f32>>,
279
280 pub grid_cols: Option<u16>,
283
284 pub grid_rows: Option<u16>,
287
288 pub grid_location: Option<GridLocation>,
290
291 #[cfg(debug_assertions)]
293 pub debug: bool,
294
295 #[cfg(debug_assertions)]
297 pub debug_below: bool,
298}
299
300impl Styled for StyleRefinement {
301 fn style(&mut self) -> &mut StyleRefinement {
302 self
303 }
304}
305
306impl StyleRefinement {
307 pub fn grid_location_mut(&mut self) -> &mut GridLocation {
309 self.grid_location.get_or_insert_default()
310 }
311}
312
313#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
315pub enum Visibility {
316 #[default]
318 Visible,
319 Hidden,
321}
322
323#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
325pub struct BoxShadow {
326 pub color: Hsla,
328 pub offset: Point<Pixels>,
330 pub blur_radius: Pixels,
332 pub spread_radius: Pixels,
334 pub inset: bool,
336}
337
338#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
340pub enum WhiteSpace {
341 #[default]
343 Normal,
344 Nowrap,
346}
347
348#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
350pub enum TextOverflow {
351 Truncate(SharedString),
354}
355
356#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
358pub enum TextAlign {
359 #[default]
361 Left,
362
363 Center,
365
366 Right,
368}
369
370#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
372pub struct TextShadow {
373 pub color: Hsla,
375 pub offset: Point<Pixels>,
377 pub blur_radius: Pixels,
379}
380
381#[derive(Refineable, Clone, Debug, PartialEq)]
383#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
384pub struct TextStyle {
385 pub color: Hsla,
387
388 pub font_family: SharedString,
390
391 pub font_features: FontFeatures,
393
394 pub font_fallbacks: Option<FontFallbacks>,
396
397 pub font_size: AbsoluteLength,
399
400 pub line_height: DefiniteLength,
402
403 pub font_weight: FontWeight,
405
406 pub font_style: FontStyle,
408
409 pub background_color: Option<Hsla>,
411
412 pub underline: Option<UnderlineStyle>,
414
415 pub strikethrough: Option<StrikethroughStyle>,
417
418 pub white_space: WhiteSpace,
420
421 pub text_overflow: Option<TextOverflow>,
423
424 pub text_align: TextAlign,
426
427 pub letter_spacing: Option<Pixels>,
429
430 pub line_clamp: Option<usize>,
432
433 pub text_shadow: Option<TextShadow>,
435}
436
437impl Default for TextStyle {
438 fn default() -> Self {
439 TextStyle {
440 color: black(),
441 font_family: if cfg!(any(target_os = "linux", target_os = "freebsd")) {
443 "FreeMono".into()
444 } else if cfg!(target_os = "windows") {
445 "Segoe UI".into()
446 } else {
447 "Helvetica".into()
448 },
449 font_features: FontFeatures::default(),
450 font_fallbacks: None,
451 font_size: rems(1.).into(),
452 line_height: phi(),
453 font_weight: FontWeight::default(),
454 font_style: FontStyle::default(),
455 background_color: None,
456 underline: None,
457 strikethrough: None,
458 white_space: WhiteSpace::Normal,
459 text_overflow: None,
460 text_align: TextAlign::default(),
461 letter_spacing: None,
462 line_clamp: None,
463 text_shadow: None,
464 }
465 }
466}
467
468impl TextStyle {
469 pub fn highlight(mut self, style: impl Into<HighlightStyle>) -> Self {
471 let style = style.into();
472 if let Some(weight) = style.font_weight {
473 self.font_weight = weight;
474 }
475 if let Some(style) = style.font_style {
476 self.font_style = style;
477 }
478
479 if let Some(color) = style.color {
480 self.color = self.color.blend(color);
481 }
482
483 if let Some(factor) = style.fade_out {
484 self.color.fade_out(factor);
485 }
486
487 if let Some(background_color) = style.background_color {
488 self.background_color = Some(background_color);
489 }
490
491 if let Some(underline) = style.underline {
492 self.underline = Some(underline);
493 }
494
495 if let Some(strikethrough) = style.strikethrough {
496 self.strikethrough = Some(strikethrough);
497 }
498
499 self
500 }
501
502 pub fn font(&self) -> Font {
504 Font {
505 family: self.font_family.clone(),
506 features: self.font_features.clone(),
507 fallbacks: self.font_fallbacks.clone(),
508 weight: self.font_weight,
509 style: self.font_style,
510 }
511 }
512
513 pub fn line_height_in_pixels(&self, rem_size: Pixels) -> Pixels {
515 self.line_height.to_pixels(self.font_size, rem_size).round()
516 }
517
518 pub fn to_run(&self, len: usize) -> TextRun {
520 TextRun {
521 len,
522 font: Font {
523 family: self.font_family.clone(),
524 features: self.font_features.clone(),
525 fallbacks: self.font_fallbacks.clone(),
526 weight: self.font_weight,
527 style: self.font_style,
528 },
529 color: self.color,
530 background_color: self.background_color,
531 underline: self.underline,
532 strikethrough: self.strikethrough,
533 }
534 }
535}
536
537#[derive(Copy, Clone, Debug, Default, PartialEq)]
540pub struct HighlightStyle {
541 pub color: Option<Hsla>,
543
544 pub font_weight: Option<FontWeight>,
546
547 pub font_style: Option<FontStyle>,
549
550 pub background_color: Option<Hsla>,
552
553 pub underline: Option<UnderlineStyle>,
555
556 pub strikethrough: Option<StrikethroughStyle>,
558
559 pub fade_out: Option<f32>,
561}
562
563impl Eq for HighlightStyle {}
564
565impl Hash for HighlightStyle {
566 fn hash<H: Hasher>(&self, state: &mut H) {
567 self.color.hash(state);
568 self.font_weight.hash(state);
569 self.font_style.hash(state);
570 self.background_color.hash(state);
571 self.underline.hash(state);
572 self.strikethrough.hash(state);
573 state.write_u32(u32::from_be_bytes(
574 self.fade_out.map(|f| f.to_be_bytes()).unwrap_or_default(),
575 ));
576 }
577}
578
579impl Style {
580 pub fn has_opaque_background(&self) -> bool {
582 self.background
583 .as_ref()
584 .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent()))
585 }
586
587 pub fn text_style(&self) -> Option<&TextStyleRefinement> {
589 if self.text.is_some() {
590 Some(&self.text)
591 } else {
592 None
593 }
594 }
595
596 pub fn overflow_mask(
599 &self,
600 bounds: Bounds<Pixels>,
601 rem_size: Pixels,
602 ) -> Option<ContentMask<Pixels>> {
603 match self.overflow {
604 Point {
605 x: Overflow::Visible,
606 y: Overflow::Visible,
607 } => None,
608 _ => {
609 let mut min = bounds.origin;
610 let mut max = bounds.bottom_right();
611
612 if self
613 .border_color
614 .is_some_and(|color| !color.is_transparent())
615 {
616 min.x += self.border_widths.left.to_pixels(rem_size);
617 max.x -= self.border_widths.right.to_pixels(rem_size);
618 min.y += self.border_widths.top.to_pixels(rem_size);
619 max.y -= self.border_widths.bottom.to_pixels(rem_size);
620 }
621
622 let bounds = match (
623 self.overflow.x == Overflow::Visible,
624 self.overflow.y == Overflow::Visible,
625 ) {
626 (true, true) => return None,
628 (true, false) => Bounds::from_corners(
630 point(min.x, bounds.origin.y),
631 point(max.x, bounds.bottom_right().y),
632 ),
633 (false, true) => Bounds::from_corners(
635 point(bounds.origin.x, min.y),
636 point(bounds.bottom_right().x, max.y),
637 ),
638 (false, false) => Bounds::from_corners(min, max),
640 };
641
642 Some(ContentMask { bounds })
643 }
644 }
645 }
646
647 pub fn paint(
649 &self,
650 bounds: Bounds<Pixels>,
651 window: &mut Window,
652 cx: &mut App,
653 continuation: impl FnOnce(&mut Window, &mut App),
654 ) {
655 #[cfg(debug_assertions)]
656 if self.debug_below {
657 cx.set_global(DebugBelow)
658 }
659
660 #[cfg(debug_assertions)]
661 if self.debug || cx.has_global::<DebugBelow>() {
662 window.paint_quad(crate::outline(bounds, crate::red(), BorderStyle::default()));
663 }
664
665 let rem_size = window.rem_size();
666 let corner_radii = self
667 .corner_radii
668 .to_pixels(rem_size)
669 .clamp_radii_for_quad_size(bounds.size);
670
671 let transform = self.compose_transform(bounds);
672
673 window.paint_shadows(bounds, corner_radii, &self.box_shadow);
674
675 let background_color = self.background.as_ref().and_then(Fill::color);
676 if background_color.is_some_and(|color| !color.is_transparent()) {
677 let mut border_color = match background_color {
678 Some(color) => match color.tag {
679 BackgroundTag::Solid => color.solid,
680 BackgroundTag::LinearGradient
681 | BackgroundTag::RadialGradient
682 | BackgroundTag::ConicGradient => color
683 .colors
684 .first()
685 .map(|stop| stop.color)
686 .unwrap_or_default(),
687 BackgroundTag::PatternSlash => color.solid,
688 },
689 None => Hsla::default(),
690 };
691 border_color.a = 0.;
692 let mut bg_quad = quad(
693 bounds,
694 corner_radii,
695 background_color.unwrap_or_default(),
696 Edges::default(),
697 border_color,
698 self.border_style,
699 );
700 bg_quad.continuous_corners = self.continuous_corners;
701 bg_quad.transform = transform;
702 bg_quad.blend_mode = self.blend_mode.unwrap_or_default();
703 window.paint_quad(bg_quad);
704 }
705
706 continuation(window, cx);
707
708 if self.is_border_visible() {
709 let border_widths = self.border_widths.to_pixels(rem_size);
710 let max_border_width = border_widths.max();
711 let max_corner_radius = corner_radii.max();
712
713 let top_bounds = Bounds::from_corners(
714 bounds.origin,
715 bounds.top_right() + point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
716 );
717 let bottom_bounds = Bounds::from_corners(
718 bounds.bottom_left() - point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
719 bounds.bottom_right(),
720 );
721 let left_bounds = Bounds::from_corners(
722 top_bounds.bottom_left(),
723 bottom_bounds.origin + point(max_border_width, Pixels::ZERO),
724 );
725 let right_bounds = Bounds::from_corners(
726 top_bounds.bottom_right() - point(max_border_width, Pixels::ZERO),
727 bottom_bounds.top_right(),
728 );
729
730 let mut background = self.border_color.unwrap_or_default();
731 background.a = 0.;
732 let mut quad = quad(
733 bounds,
734 corner_radii,
735 background,
736 border_widths,
737 self.border_color.unwrap_or_default(),
738 self.border_style,
739 );
740 quad.continuous_corners = self.continuous_corners;
741 quad.transform = transform;
742
743 window.with_content_mask(Some(ContentMask { bounds: top_bounds }), |window| {
744 window.paint_quad(quad.clone());
745 });
746 window.with_content_mask(
747 Some(ContentMask {
748 bounds: right_bounds,
749 }),
750 |window| {
751 window.paint_quad(quad.clone());
752 },
753 );
754 window.with_content_mask(
755 Some(ContentMask {
756 bounds: bottom_bounds,
757 }),
758 |window| {
759 window.paint_quad(quad.clone());
760 },
761 );
762 window.with_content_mask(
763 Some(ContentMask {
764 bounds: left_bounds,
765 }),
766 |window| {
767 window.paint_quad(quad);
768 },
769 );
770 }
771
772 #[cfg(debug_assertions)]
773 if self.debug_below {
774 cx.remove_global::<DebugBelow>();
775 }
776 }
777
778 fn compose_transform(&self, bounds: Bounds<Pixels>) -> TransformationMatrix {
779 let has_transform = self.rotate.is_some() || self.scale.is_some();
780 if !has_transform {
781 return TransformationMatrix::unit();
782 }
783
784 let origin_frac = self.transform_origin.unwrap_or(Point { x: 0.5, y: 0.5 });
785 let cx = bounds.origin.x.0 + bounds.size.width.0 * origin_frac.x;
786 let cy = bounds.origin.y.0 + bounds.size.height.0 * origin_frac.y;
787
788 let mut t = TransformationMatrix::unit();
789
790 if let Some(scale) = self.scale {
791 t = t.scale(crate::Size {
792 width: scale.x,
793 height: scale.y,
794 });
795 }
796 if let Some(rotate) = self.rotate {
797 t = t.rotate(Radians(rotate));
798 }
799
800 let neg_origin = TransformationMatrix {
801 rotation_scale: [[1.0, 0.0], [0.0, 1.0]],
802 translation: [-cx, -cy],
803 };
804 let pos_origin = TransformationMatrix {
805 rotation_scale: [[1.0, 0.0], [0.0, 1.0]],
806 translation: [cx, cy],
807 };
808 pos_origin.compose(t.compose(neg_origin))
809 }
810
811 fn is_border_visible(&self) -> bool {
812 self.border_color
813 .is_some_and(|color| !color.is_transparent())
814 && self.border_widths.any(|length| !length.is_zero())
815 }
816}
817
818impl Default for Style {
819 fn default() -> Self {
820 Style {
821 display: Display::Block,
822 visibility: Visibility::Visible,
823 overflow: Point {
824 x: Overflow::Visible,
825 y: Overflow::Visible,
826 },
827 allow_concurrent_scroll: false,
828 restrict_scroll_to_axis: false,
829 scrollbar_width: AbsoluteLength::default(),
830 position: Position::Relative,
831 inset: Edges::auto(),
832 margin: Edges::<Length>::zero(),
833 padding: Edges::<DefiniteLength>::zero(),
834 border_widths: Edges::<AbsoluteLength>::zero(),
835 size: Size::auto(),
836 min_size: Size::auto(),
837 max_size: Size::auto(),
838 aspect_ratio: None,
839 gap: Size::default(),
840 align_items: None,
842 align_self: None,
843 align_content: None,
844 justify_content: None,
845 flex_direction: FlexDirection::Row,
847 flex_wrap: FlexWrap::NoWrap,
848 flex_grow: 0.0,
849 flex_shrink: 1.0,
850 flex_basis: Length::Auto,
851 background: None,
852 border_color: None,
853 border_style: BorderStyle::default(),
854 corner_radii: Corners::default(),
855 continuous_corners: false,
856 blend_mode: None,
857 box_shadow: Default::default(),
858 text: TextStyleRefinement::default(),
859 mouse_cursor: None,
860 opacity: None,
861 rotate: None,
862 scale: None,
863 transform_origin: None,
864 grid_rows: None,
865 grid_cols: None,
866 grid_location: None,
867
868 #[cfg(debug_assertions)]
869 debug: false,
870 #[cfg(debug_assertions)]
871 debug_below: false,
872 }
873 }
874}
875
876#[derive(
878 Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema,
879)]
880pub struct UnderlineStyle {
881 pub thickness: Pixels,
883
884 pub color: Option<Hsla>,
886
887 pub wavy: bool,
889}
890
891#[derive(
893 Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema,
894)]
895pub struct StrikethroughStyle {
896 pub thickness: Pixels,
898
899 pub color: Option<Hsla>,
901}
902
903#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
905pub enum Fill {
906 Color(Background),
908}
909
910impl Fill {
911 pub fn color(&self) -> Option<Background> {
915 match self {
916 Fill::Color(color) => Some(*color),
917 }
918 }
919}
920
921impl Default for Fill {
922 fn default() -> Self {
923 Self::Color(Background::default())
924 }
925}
926
927impl From<Hsla> for Fill {
928 fn from(color: Hsla) -> Self {
929 Self::Color(color.into())
930 }
931}
932
933impl From<Rgba> for Fill {
934 fn from(color: Rgba) -> Self {
935 Self::Color(color.into())
936 }
937}
938
939impl From<Background> for Fill {
940 fn from(background: Background) -> Self {
941 Self::Color(background)
942 }
943}
944
945impl From<TextStyle> for HighlightStyle {
946 fn from(other: TextStyle) -> Self {
947 Self::from(&other)
948 }
949}
950
951impl From<&TextStyle> for HighlightStyle {
952 fn from(other: &TextStyle) -> Self {
953 Self {
954 color: Some(other.color),
955 font_weight: Some(other.font_weight),
956 font_style: Some(other.font_style),
957 background_color: other.background_color,
958 underline: other.underline,
959 strikethrough: other.strikethrough,
960 fade_out: None,
961 }
962 }
963}
964
965impl HighlightStyle {
966 pub fn color(color: Hsla) -> Self {
968 Self {
969 color: Some(color),
970 ..Default::default()
971 }
972 }
973 #[must_use]
976 pub fn highlight(self, other: HighlightStyle) -> Self {
977 Self {
978 color: other
979 .color
980 .map(|other_color| {
981 if let Some(color) = self.color {
982 color.blend(other_color)
983 } else {
984 other_color
985 }
986 })
987 .or(self.color),
988 font_weight: other.font_weight.or(self.font_weight),
989 font_style: other.font_style.or(self.font_style),
990 background_color: other.background_color.or(self.background_color),
991 underline: other.underline.or(self.underline),
992 strikethrough: other.strikethrough.or(self.strikethrough),
993 fade_out: other
994 .fade_out
995 .map(|source_fade| {
996 self.fade_out
997 .map(|dest_fade| (dest_fade * (1. + source_fade)).clamp(0., 1.))
998 .unwrap_or(source_fade)
999 })
1000 .or(self.fade_out),
1001 }
1002 }
1003}
1004
1005impl From<Hsla> for HighlightStyle {
1006 fn from(color: Hsla) -> Self {
1007 Self {
1008 color: Some(color),
1009 ..Default::default()
1010 }
1011 }
1012}
1013
1014impl From<FontWeight> for HighlightStyle {
1015 fn from(font_weight: FontWeight) -> Self {
1016 Self {
1017 font_weight: Some(font_weight),
1018 ..Default::default()
1019 }
1020 }
1021}
1022
1023impl From<FontStyle> for HighlightStyle {
1024 fn from(font_style: FontStyle) -> Self {
1025 Self {
1026 font_style: Some(font_style),
1027 ..Default::default()
1028 }
1029 }
1030}
1031
1032impl From<Rgba> for HighlightStyle {
1033 fn from(color: Rgba) -> Self {
1034 Self {
1035 color: Some(color.into()),
1036 ..Default::default()
1037 }
1038 }
1039}
1040
1041pub fn combine_highlights(
1043 a: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
1044 b: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
1045) -> impl Iterator<Item = (Range<usize>, HighlightStyle)> {
1046 let mut endpoints = Vec::new();
1047 let mut highlights = Vec::new();
1048 for (range, highlight) in a.into_iter().chain(b) {
1049 if !range.is_empty() {
1050 let highlight_id = highlights.len();
1051 endpoints.push((range.start, highlight_id, true));
1052 endpoints.push((range.end, highlight_id, false));
1053 highlights.push(highlight);
1054 }
1055 }
1056 endpoints.sort_unstable_by_key(|(position, _, _)| *position);
1057 let mut endpoints = endpoints.into_iter().peekable();
1058
1059 let mut active_styles = HashSet::default();
1060 let mut ix = 0;
1061 iter::from_fn(move || {
1062 while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() {
1063 let prev_index = mem::replace(&mut ix, *endpoint_ix);
1064 if ix > prev_index && !active_styles.is_empty() {
1065 let current_style = active_styles
1066 .iter()
1067 .fold(HighlightStyle::default(), |acc, highlight_id| {
1068 acc.highlight(highlights[*highlight_id])
1069 });
1070 return Some((prev_index..ix, current_style));
1071 }
1072
1073 if *is_start {
1074 active_styles.insert(*highlight_id);
1075 } else {
1076 active_styles.remove(highlight_id);
1077 }
1078 endpoints.next();
1079 }
1080 None
1081 })
1082}
1083
1084#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
1090pub enum AlignItems {
1092 Start,
1094 End,
1096 FlexStart,
1101 FlexEnd,
1106 Center,
1108 Baseline,
1110 Stretch,
1112}
1113pub type JustifyItems = AlignItems;
1119pub type AlignSelf = AlignItems;
1126pub type JustifySelf = AlignItems;
1133
1134#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
1140pub enum AlignContent {
1142 Start,
1144 End,
1146 FlexStart,
1151 FlexEnd,
1156 Center,
1158 Stretch,
1160 SpaceBetween,
1163 SpaceEvenly,
1166 SpaceAround,
1169}
1170
1171pub type JustifyContent = AlignContent;
1177
1178#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1182pub enum Display {
1184 Block,
1186 #[default]
1188 Flex,
1189 Grid,
1191 None,
1193}
1194
1195#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1201pub enum FlexWrap {
1203 #[default]
1205 NoWrap,
1206 Wrap,
1208 WrapReverse,
1210}
1211
1212#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1224pub enum FlexDirection {
1226 #[default]
1230 Row,
1231 Column,
1235 RowReverse,
1239 ColumnReverse,
1243}
1244
1245#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1259pub enum Overflow {
1261 #[default]
1264 Visible,
1265 Clip,
1268 Hidden,
1271 Scroll,
1275}
1276
1277#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1287pub enum Position {
1289 #[default]
1292 Relative,
1293 Absolute,
1299}
1300
1301impl From<AlignItems> for taffy::style::AlignItems {
1302 fn from(value: AlignItems) -> Self {
1303 match value {
1304 AlignItems::Start => Self::Start,
1305 AlignItems::End => Self::End,
1306 AlignItems::FlexStart => Self::FlexStart,
1307 AlignItems::FlexEnd => Self::FlexEnd,
1308 AlignItems::Center => Self::Center,
1309 AlignItems::Baseline => Self::Baseline,
1310 AlignItems::Stretch => Self::Stretch,
1311 }
1312 }
1313}
1314
1315impl From<AlignContent> for taffy::style::AlignContent {
1316 fn from(value: AlignContent) -> Self {
1317 match value {
1318 AlignContent::Start => Self::Start,
1319 AlignContent::End => Self::End,
1320 AlignContent::FlexStart => Self::FlexStart,
1321 AlignContent::FlexEnd => Self::FlexEnd,
1322 AlignContent::Center => Self::Center,
1323 AlignContent::Stretch => Self::Stretch,
1324 AlignContent::SpaceBetween => Self::SpaceBetween,
1325 AlignContent::SpaceEvenly => Self::SpaceEvenly,
1326 AlignContent::SpaceAround => Self::SpaceAround,
1327 }
1328 }
1329}
1330
1331impl From<Display> for taffy::style::Display {
1332 fn from(value: Display) -> Self {
1333 match value {
1334 Display::Block => Self::Block,
1335 Display::Flex => Self::Flex,
1336 Display::Grid => Self::Grid,
1337 Display::None => Self::None,
1338 }
1339 }
1340}
1341
1342impl From<FlexWrap> for taffy::style::FlexWrap {
1343 fn from(value: FlexWrap) -> Self {
1344 match value {
1345 FlexWrap::NoWrap => Self::NoWrap,
1346 FlexWrap::Wrap => Self::Wrap,
1347 FlexWrap::WrapReverse => Self::WrapReverse,
1348 }
1349 }
1350}
1351
1352impl From<FlexDirection> for taffy::style::FlexDirection {
1353 fn from(value: FlexDirection) -> Self {
1354 match value {
1355 FlexDirection::Row => Self::Row,
1356 FlexDirection::Column => Self::Column,
1357 FlexDirection::RowReverse => Self::RowReverse,
1358 FlexDirection::ColumnReverse => Self::ColumnReverse,
1359 }
1360 }
1361}
1362
1363impl From<Overflow> for taffy::style::Overflow {
1364 fn from(value: Overflow) -> Self {
1365 match value {
1366 Overflow::Visible => Self::Visible,
1367 Overflow::Clip => Self::Clip,
1368 Overflow::Hidden => Self::Hidden,
1369 Overflow::Scroll => Self::Scroll,
1370 }
1371 }
1372}
1373
1374impl From<Position> for taffy::style::Position {
1375 fn from(value: Position) -> Self {
1376 match value {
1377 Position::Relative => Self::Relative,
1378 Position::Absolute => Self::Absolute,
1379 }
1380 }
1381}
1382
1383#[cfg(test)]
1384mod tests {
1385 use crate::{blue, green, px, red, yellow};
1386
1387 use super::*;
1388
1389 use util_macros::perf;
1390
1391 #[perf]
1392 fn test_basic_highlight_style_combination() {
1393 let style_a = HighlightStyle::default();
1394 let style_b = HighlightStyle::default();
1395 let style_a = style_a.highlight(style_b);
1396 assert_eq!(
1397 style_a,
1398 HighlightStyle::default(),
1399 "Combining empty styles should not produce a non-empty style."
1400 );
1401
1402 let mut style_b = HighlightStyle {
1403 color: Some(red()),
1404 strikethrough: Some(StrikethroughStyle {
1405 thickness: px(2.),
1406 color: Some(blue()),
1407 }),
1408 fade_out: Some(0.),
1409 font_style: Some(FontStyle::Italic),
1410 font_weight: Some(FontWeight(300.)),
1411 background_color: Some(yellow()),
1412 underline: Some(UnderlineStyle {
1413 thickness: px(2.),
1414 color: Some(red()),
1415 wavy: true,
1416 }),
1417 };
1418 let expected_style = style_b;
1419
1420 let style_a = style_a.highlight(style_b);
1421 assert_eq!(
1422 style_a, expected_style,
1423 "Blending an empty style with another style should return the other style"
1424 );
1425
1426 let style_b = style_b.highlight(Default::default());
1427 assert_eq!(
1428 style_b, expected_style,
1429 "Blending a style with an empty style should not change the style."
1430 );
1431
1432 let mut style_c = expected_style;
1433
1434 let style_d = HighlightStyle {
1435 color: Some(blue().alpha(0.7)),
1436 strikethrough: Some(StrikethroughStyle {
1437 thickness: px(4.),
1438 color: Some(crate::red()),
1439 }),
1440 fade_out: Some(0.),
1441 font_style: Some(FontStyle::Oblique),
1442 font_weight: Some(FontWeight(800.)),
1443 background_color: Some(green()),
1444 underline: Some(UnderlineStyle {
1445 thickness: px(4.),
1446 color: None,
1447 wavy: false,
1448 }),
1449 };
1450
1451 let expected_style = HighlightStyle {
1452 color: Some(red().blend(blue().alpha(0.7))),
1453 strikethrough: Some(StrikethroughStyle {
1454 thickness: px(4.),
1455 color: Some(red()),
1456 }),
1457 fade_out: Some(0.),
1459 font_style: Some(FontStyle::Oblique),
1460 font_weight: Some(FontWeight(800.)),
1461 background_color: Some(green()),
1462 underline: Some(UnderlineStyle {
1463 thickness: px(4.),
1464 color: None,
1465 wavy: false,
1466 }),
1467 };
1468
1469 let style_c = style_c.highlight(style_d);
1470 assert_eq!(
1471 style_c, expected_style,
1472 "Blending styles should blend properties where possible and override all others"
1473 );
1474 }
1475
1476 #[perf]
1477 fn test_combine_highlights() {
1478 assert_eq!(
1479 combine_highlights(
1480 [
1481 (0..5, green().into()),
1482 (4..10, FontWeight::BOLD.into()),
1483 (15..20, yellow().into()),
1484 ],
1485 [
1486 (2..6, FontStyle::Italic.into()),
1487 (1..3, blue().into()),
1488 (21..23, red().into()),
1489 ]
1490 )
1491 .collect::<Vec<_>>(),
1492 [
1493 (
1494 0..1,
1495 HighlightStyle {
1496 color: Some(green()),
1497 ..Default::default()
1498 }
1499 ),
1500 (
1501 1..2,
1502 HighlightStyle {
1503 color: Some(blue()),
1504 ..Default::default()
1505 }
1506 ),
1507 (
1508 2..3,
1509 HighlightStyle {
1510 color: Some(blue()),
1511 font_style: Some(FontStyle::Italic),
1512 ..Default::default()
1513 }
1514 ),
1515 (
1516 3..4,
1517 HighlightStyle {
1518 color: Some(green()),
1519 font_style: Some(FontStyle::Italic),
1520 ..Default::default()
1521 }
1522 ),
1523 (
1524 4..5,
1525 HighlightStyle {
1526 color: Some(green()),
1527 font_weight: Some(FontWeight::BOLD),
1528 font_style: Some(FontStyle::Italic),
1529 ..Default::default()
1530 }
1531 ),
1532 (
1533 5..6,
1534 HighlightStyle {
1535 font_weight: Some(FontWeight::BOLD),
1536 font_style: Some(FontStyle::Italic),
1537 ..Default::default()
1538 }
1539 ),
1540 (
1541 6..10,
1542 HighlightStyle {
1543 font_weight: Some(FontWeight::BOLD),
1544 ..Default::default()
1545 }
1546 ),
1547 (
1548 15..20,
1549 HighlightStyle {
1550 color: Some(yellow()),
1551 ..Default::default()
1552 }
1553 ),
1554 (
1555 21..23,
1556 HighlightStyle {
1557 color: Some(red()),
1558 ..Default::default()
1559 }
1560 )
1561 ]
1562 );
1563 }
1564}