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, px, 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(
145 Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default, JsonSchema, Serialize, Deserialize,
146)]
147pub enum TemplateColumnMinSize {
148 #[default]
150 Zero,
151 MinContent,
153 MaxContent,
155}
156
157#[derive(
159 Copy,
160 Clone,
161 Refineable,
162 PartialEq,
163 Eq,
164 PartialOrd,
165 Ord,
166 Debug,
167 Default,
168 JsonSchema,
169 Serialize,
170 Deserialize,
171)]
172pub struct GridTemplate {
173 pub repeat: u16,
175 pub min_size: TemplateColumnMinSize,
177}
178
179#[derive(Clone, Refineable, Debug)]
181#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
182pub struct Style {
183 pub display: Display,
185
186 pub visibility: Visibility,
188
189 #[refineable]
192 pub overflow: Point<Overflow>,
193 pub scrollbar_width: AbsoluteLength,
195 pub allow_concurrent_scroll: bool,
197 pub restrict_scroll_to_axis: bool,
223
224 pub position: Position,
227 #[refineable]
229 pub inset: Edges<Length>,
230
231 #[refineable]
234 pub size: Size<Length>,
235 #[refineable]
237 pub min_size: Size<Length>,
238 #[refineable]
240 pub max_size: Size<Length>,
241 pub aspect_ratio: Option<f32>,
243
244 #[refineable]
247 pub margin: Edges<Length>,
248 #[refineable]
250 pub padding: Edges<DefiniteLength>,
251 #[refineable]
253 pub border_widths: Edges<AbsoluteLength>,
254
255 pub align_items: Option<AlignItems>,
258 pub align_self: Option<AlignSelf>,
260 pub align_content: Option<AlignContent>,
262 pub justify_content: Option<JustifyContent>,
264 #[refineable]
266 pub gap: Size<DefiniteLength>,
267
268 pub flex_direction: FlexDirection,
271 pub flex_wrap: FlexWrap,
273 pub flex_basis: Length,
275 pub flex_grow: f32,
277 pub flex_shrink: f32,
279
280 pub background: Option<Fill>,
282
283 pub border_color: Option<Hsla>,
285
286 pub border_style: BorderStyle,
288
289 #[refineable]
291 pub corner_radii: Corners<AbsoluteLength>,
292
293 pub continuous_corners: bool,
295
296 pub blend_mode: Option<BlendMode>,
298
299 pub box_shadow: SmallVec<[BoxShadow; 1]>,
301
302 pub text: TextStyleRefinement,
304
305 pub mouse_cursor: Option<CursorStyle>,
307
308 pub opacity: Option<f32>,
310
311 pub rotate: Option<f32>,
313
314 pub scale: Option<Point<f32>>,
316
317 pub transform_origin: Option<Point<f32>>,
319
320 pub grid_cols: Option<GridTemplate>,
323
324 pub grid_rows: Option<GridTemplate>,
327
328 pub grid_location: Option<GridLocation>,
330
331 #[cfg(debug_assertions)]
333 pub debug: bool,
334
335 #[cfg(debug_assertions)]
337 pub debug_below: bool,
338}
339
340impl Styled for StyleRefinement {
341 fn style(&mut self) -> &mut StyleRefinement {
342 self
343 }
344}
345
346impl StyleRefinement {
347 pub fn grid_location_mut(&mut self) -> &mut GridLocation {
349 self.grid_location.get_or_insert_default()
350 }
351}
352
353#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
355pub enum Visibility {
356 #[default]
358 Visible,
359 Hidden,
361}
362
363#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
365pub struct BoxShadow {
366 pub color: Hsla,
368 pub offset: Point<Pixels>,
370 pub blur_radius: Pixels,
372 pub spread_radius: Pixels,
374 pub inset: bool,
376}
377
378impl BoxShadow {
379 pub fn new(offset_x: Pixels, offset_y: Pixels, color: Hsla) -> Self {
383 Self {
384 color,
385 offset: point(offset_x, offset_y),
386 blur_radius: px(0.),
387 spread_radius: px(0.),
388 inset: false,
389 }
390 }
391
392 pub fn blur_radius(mut self, blur_radius: Pixels) -> Self {
394 self.blur_radius = blur_radius;
395 self
396 }
397
398 pub fn spread_radius(mut self, spread_radius: Pixels) -> Self {
400 self.spread_radius = spread_radius;
401 self
402 }
403
404 pub fn inset(mut self) -> Self {
406 self.inset = true;
407 self
408 }
409}
410
411#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
413pub enum WhiteSpace {
414 #[default]
416 Normal,
417 Nowrap,
419}
420
421#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
423pub enum TextOverflow {
424 Truncate(SharedString),
427}
428
429#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
431pub enum TextAlign {
432 #[default]
434 Left,
435
436 Center,
438
439 Right,
441}
442
443#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
445pub struct TextShadow {
446 pub color: Hsla,
448 pub offset: Point<Pixels>,
450 pub blur_radius: Pixels,
452}
453
454#[derive(Refineable, Clone, Debug, PartialEq)]
456#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
457pub struct TextStyle {
458 pub color: Hsla,
460
461 pub font_family: SharedString,
463
464 pub font_features: FontFeatures,
466
467 pub font_fallbacks: Option<FontFallbacks>,
469
470 pub font_size: AbsoluteLength,
472
473 pub line_height: DefiniteLength,
475
476 pub font_weight: FontWeight,
478
479 pub font_style: FontStyle,
481
482 pub background_color: Option<Hsla>,
484
485 pub underline: Option<UnderlineStyle>,
487
488 pub strikethrough: Option<StrikethroughStyle>,
490
491 pub white_space: WhiteSpace,
493
494 pub text_overflow: Option<TextOverflow>,
496
497 pub text_align: TextAlign,
499
500 pub letter_spacing: Option<Pixels>,
502
503 pub line_clamp: Option<usize>,
505
506 pub text_shadow: Option<TextShadow>,
508}
509
510impl Default for TextStyle {
511 fn default() -> Self {
512 TextStyle {
513 color: black(),
514 font_family: if cfg!(any(target_os = "linux", target_os = "freebsd")) {
516 "FreeMono".into()
517 } else if cfg!(target_os = "windows") {
518 "Segoe UI".into()
519 } else {
520 "Helvetica".into()
521 },
522 font_features: FontFeatures::default(),
523 font_fallbacks: None,
524 font_size: rems(1.).into(),
525 line_height: phi(),
526 font_weight: FontWeight::default(),
527 font_style: FontStyle::default(),
528 background_color: None,
529 underline: None,
530 strikethrough: None,
531 white_space: WhiteSpace::Normal,
532 text_overflow: None,
533 text_align: TextAlign::default(),
534 letter_spacing: None,
535 line_clamp: None,
536 text_shadow: None,
537 }
538 }
539}
540
541impl TextStyle {
542 pub fn highlight(mut self, style: impl Into<HighlightStyle>) -> Self {
544 let style = style.into();
545 if let Some(weight) = style.font_weight {
546 self.font_weight = weight;
547 }
548 if let Some(style) = style.font_style {
549 self.font_style = style;
550 }
551
552 if let Some(color) = style.color {
553 self.color = self.color.blend(color);
554 }
555
556 if let Some(factor) = style.fade_out {
557 self.color.fade_out(factor);
558 }
559
560 if let Some(background_color) = style.background_color {
561 self.background_color = Some(background_color);
562 }
563
564 if let Some(underline) = style.underline {
565 self.underline = Some(underline);
566 }
567
568 if let Some(strikethrough) = style.strikethrough {
569 self.strikethrough = Some(strikethrough);
570 }
571
572 self
573 }
574
575 pub fn font(&self) -> Font {
577 Font {
578 family: self.font_family.clone(),
579 features: self.font_features.clone(),
580 fallbacks: self.font_fallbacks.clone(),
581 weight: self.font_weight,
582 style: self.font_style,
583 }
584 }
585
586 pub fn line_height_in_pixels(&self, rem_size: Pixels) -> Pixels {
588 self.line_height.to_pixels(self.font_size, rem_size).round()
589 }
590
591 pub fn to_run(&self, len: usize) -> TextRun {
593 TextRun {
594 len,
595 font: Font {
596 family: self.font_family.clone(),
597 features: self.font_features.clone(),
598 fallbacks: self.font_fallbacks.clone(),
599 weight: self.font_weight,
600 style: self.font_style,
601 },
602 color: self.color,
603 background_color: self.background_color,
604 underline: self.underline,
605 strikethrough: self.strikethrough,
606 }
607 }
608}
609
610#[derive(Copy, Clone, Debug, Default, PartialEq)]
613pub struct HighlightStyle {
614 pub color: Option<Hsla>,
616
617 pub font_weight: Option<FontWeight>,
619
620 pub font_style: Option<FontStyle>,
622
623 pub background_color: Option<Hsla>,
625
626 pub underline: Option<UnderlineStyle>,
628
629 pub strikethrough: Option<StrikethroughStyle>,
631
632 pub fade_out: Option<f32>,
634}
635
636impl Eq for HighlightStyle {}
637
638impl Hash for HighlightStyle {
639 fn hash<H: Hasher>(&self, state: &mut H) {
640 self.color.hash(state);
641 self.font_weight.hash(state);
642 self.font_style.hash(state);
643 self.background_color.hash(state);
644 self.underline.hash(state);
645 self.strikethrough.hash(state);
646 state.write_u32(u32::from_be_bytes(
647 self.fade_out.map(|f| f.to_be_bytes()).unwrap_or_default(),
648 ));
649 }
650}
651
652impl Style {
653 pub fn has_opaque_background(&self) -> bool {
655 self.background
656 .as_ref()
657 .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent()))
658 }
659
660 pub fn text_style(&self) -> Option<&TextStyleRefinement> {
662 if self.text.is_some() {
663 Some(&self.text)
664 } else {
665 None
666 }
667 }
668
669 pub fn overflow_mask(
672 &self,
673 bounds: Bounds<Pixels>,
674 rem_size: Pixels,
675 ) -> Option<ContentMask<Pixels>> {
676 match self.overflow {
677 Point {
678 x: Overflow::Visible,
679 y: Overflow::Visible,
680 } => None,
681 _ => {
682 let mut min = bounds.origin;
683 let mut max = bounds.bottom_right();
684
685 if self
686 .border_color
687 .is_some_and(|color| !color.is_transparent())
688 {
689 min.x += self.border_widths.left.to_pixels(rem_size);
690 max.x -= self.border_widths.right.to_pixels(rem_size);
691 min.y += self.border_widths.top.to_pixels(rem_size);
692 max.y -= self.border_widths.bottom.to_pixels(rem_size);
693 }
694
695 let bounds = match (
696 self.overflow.x == Overflow::Visible,
697 self.overflow.y == Overflow::Visible,
698 ) {
699 (true, true) => return None,
701 (true, false) => Bounds::from_corners(
703 point(min.x, bounds.origin.y),
704 point(max.x, bounds.bottom_right().y),
705 ),
706 (false, true) => Bounds::from_corners(
708 point(bounds.origin.x, min.y),
709 point(bounds.bottom_right().x, max.y),
710 ),
711 (false, false) => Bounds::from_corners(min, max),
713 };
714
715 Some(ContentMask { bounds })
716 }
717 }
718 }
719
720 pub fn paint(
722 &self,
723 bounds: Bounds<Pixels>,
724 window: &mut Window,
725 cx: &mut App,
726 continuation: impl FnOnce(&mut Window, &mut App),
727 ) {
728 #[cfg(debug_assertions)]
729 if self.debug_below {
730 cx.set_global(DebugBelow)
731 }
732
733 #[cfg(debug_assertions)]
734 if self.debug || cx.has_global::<DebugBelow>() {
735 window.paint_quad(crate::outline(bounds, crate::red(), BorderStyle::default()));
736 }
737
738 let rem_size = window.rem_size();
739 let corner_radii = self
740 .corner_radii
741 .to_pixels(rem_size)
742 .clamp_radii_for_quad_size(bounds.size);
743
744 let transform = self.compose_transform(bounds);
745
746 window.paint_shadows(bounds, corner_radii, &self.box_shadow);
747
748 let background_color = self.background.as_ref().and_then(Fill::color);
749 if background_color.is_some_and(|color| !color.is_transparent()) {
750 let mut border_color = match background_color {
751 Some(color) => match color.tag {
752 BackgroundTag::Solid => color.solid,
753 BackgroundTag::LinearGradient
754 | BackgroundTag::RadialGradient
755 | BackgroundTag::ConicGradient => color
756 .colors
757 .first()
758 .map(|stop| stop.color)
759 .unwrap_or_default(),
760 BackgroundTag::PatternSlash => color.solid,
761 },
762 None => Hsla::default(),
763 };
764 border_color.a = 0.;
765 let mut bg_quad = quad(
766 bounds,
767 corner_radii,
768 background_color.unwrap_or_default(),
769 Edges::default(),
770 border_color,
771 self.border_style,
772 );
773 bg_quad.continuous_corners = self.continuous_corners;
774 bg_quad.transform = transform;
775 bg_quad.blend_mode = self.blend_mode.unwrap_or_default();
776 window.paint_quad(bg_quad);
777 }
778
779 continuation(window, cx);
780
781 if self.is_border_visible() {
782 let border_widths = self.border_widths.to_pixels(rem_size);
783 let mut background = self.border_color.unwrap_or_default();
784 background.a = 0.;
785 let mut quad = quad(
786 bounds,
787 corner_radii,
788 background,
789 border_widths,
790 self.border_color.unwrap_or_default(),
791 self.border_style,
792 );
793 quad.continuous_corners = self.continuous_corners;
794 quad.transform = transform;
795 quad.blend_mode = self.blend_mode.unwrap_or_default();
796 window.paint_quad(quad);
797 }
798
799 #[cfg(debug_assertions)]
800 if self.debug_below {
801 cx.remove_global::<DebugBelow>();
802 }
803 }
804
805 fn compose_transform(&self, bounds: Bounds<Pixels>) -> TransformationMatrix {
806 let has_transform = self.rotate.is_some() || self.scale.is_some();
807 if !has_transform {
808 return TransformationMatrix::unit();
809 }
810
811 let origin_frac = self.transform_origin.unwrap_or(Point { x: 0.5, y: 0.5 });
812 let cx = bounds.origin.x.0 + bounds.size.width.0 * origin_frac.x;
813 let cy = bounds.origin.y.0 + bounds.size.height.0 * origin_frac.y;
814
815 let mut t = TransformationMatrix::unit();
816
817 if let Some(scale) = self.scale {
818 t = t.scale(crate::Size {
819 width: scale.x,
820 height: scale.y,
821 });
822 }
823 if let Some(rotate) = self.rotate {
824 t = t.rotate(Radians(rotate));
825 }
826
827 let neg_origin = TransformationMatrix {
828 rotation_scale: [[1.0, 0.0], [0.0, 1.0]],
829 translation: [-cx, -cy],
830 };
831 let pos_origin = TransformationMatrix {
832 rotation_scale: [[1.0, 0.0], [0.0, 1.0]],
833 translation: [cx, cy],
834 };
835 pos_origin.compose(t.compose(neg_origin))
836 }
837
838 fn is_border_visible(&self) -> bool {
839 self.border_color
840 .is_some_and(|color| !color.is_transparent())
841 && self.border_widths.any(|length| !length.is_zero())
842 }
843}
844
845impl Default for Style {
846 fn default() -> Self {
847 Style {
848 display: Display::Block,
849 visibility: Visibility::Visible,
850 overflow: Point {
851 x: Overflow::Visible,
852 y: Overflow::Visible,
853 },
854 allow_concurrent_scroll: false,
855 restrict_scroll_to_axis: false,
856 scrollbar_width: AbsoluteLength::default(),
857 position: Position::Relative,
858 inset: Edges::auto(),
859 margin: Edges::<Length>::zero(),
860 padding: Edges::<DefiniteLength>::zero(),
861 border_widths: Edges::<AbsoluteLength>::zero(),
862 size: Size::auto(),
863 min_size: Size::auto(),
864 max_size: Size::auto(),
865 aspect_ratio: None,
866 gap: Size::default(),
867 align_items: None,
869 align_self: None,
870 align_content: None,
871 justify_content: None,
872 flex_direction: FlexDirection::Row,
874 flex_wrap: FlexWrap::NoWrap,
875 flex_grow: 0.0,
876 flex_shrink: 1.0,
877 flex_basis: Length::Auto,
878 background: None,
879 border_color: None,
880 border_style: BorderStyle::default(),
881 corner_radii: Corners::default(),
882 continuous_corners: false,
883 blend_mode: None,
884 box_shadow: Default::default(),
885 text: TextStyleRefinement::default(),
886 mouse_cursor: None,
887 opacity: None,
888 rotate: None,
889 scale: None,
890 transform_origin: None,
891 grid_rows: None,
892 grid_cols: None,
893 grid_location: None,
894
895 #[cfg(debug_assertions)]
896 debug: false,
897 #[cfg(debug_assertions)]
898 debug_below: false,
899 }
900 }
901}
902
903#[derive(
905 Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema,
906)]
907pub struct UnderlineStyle {
908 pub thickness: Pixels,
910
911 pub color: Option<Hsla>,
913
914 pub wavy: bool,
916}
917
918#[derive(
920 Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema,
921)]
922pub struct StrikethroughStyle {
923 pub thickness: Pixels,
925
926 pub color: Option<Hsla>,
928}
929
930#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
932pub enum Fill {
933 Color(Background),
935}
936
937impl Fill {
938 pub fn color(&self) -> Option<Background> {
942 match self {
943 Fill::Color(color) => Some(*color),
944 }
945 }
946}
947
948impl Default for Fill {
949 fn default() -> Self {
950 Self::Color(Background::default())
951 }
952}
953
954impl From<Hsla> for Fill {
955 fn from(color: Hsla) -> Self {
956 Self::Color(color.into())
957 }
958}
959
960impl From<Rgba> for Fill {
961 fn from(color: Rgba) -> Self {
962 Self::Color(color.into())
963 }
964}
965
966impl From<Background> for Fill {
967 fn from(background: Background) -> Self {
968 Self::Color(background)
969 }
970}
971
972impl From<TextStyle> for HighlightStyle {
973 fn from(other: TextStyle) -> Self {
974 Self::from(&other)
975 }
976}
977
978impl From<&TextStyle> for HighlightStyle {
979 fn from(other: &TextStyle) -> Self {
980 Self {
981 color: Some(other.color),
982 font_weight: Some(other.font_weight),
983 font_style: Some(other.font_style),
984 background_color: other.background_color,
985 underline: other.underline,
986 strikethrough: other.strikethrough,
987 fade_out: None,
988 }
989 }
990}
991
992impl HighlightStyle {
993 pub fn color(color: Hsla) -> Self {
995 Self {
996 color: Some(color),
997 ..Default::default()
998 }
999 }
1000 #[must_use]
1003 pub fn highlight(self, other: HighlightStyle) -> Self {
1004 Self {
1005 color: other
1006 .color
1007 .map(|other_color| {
1008 if let Some(color) = self.color {
1009 color.blend(other_color)
1010 } else {
1011 other_color
1012 }
1013 })
1014 .or(self.color),
1015 font_weight: other.font_weight.or(self.font_weight),
1016 font_style: other.font_style.or(self.font_style),
1017 background_color: other.background_color.or(self.background_color),
1018 underline: other.underline.or(self.underline),
1019 strikethrough: other.strikethrough.or(self.strikethrough),
1020 fade_out: other
1021 .fade_out
1022 .map(|source_fade| {
1023 self.fade_out
1024 .map(|dest_fade| (dest_fade * (1. + source_fade)).clamp(0., 1.))
1025 .unwrap_or(source_fade)
1026 })
1027 .or(self.fade_out),
1028 }
1029 }
1030}
1031
1032impl From<Hsla> for HighlightStyle {
1033 fn from(color: Hsla) -> Self {
1034 Self {
1035 color: Some(color),
1036 ..Default::default()
1037 }
1038 }
1039}
1040
1041impl From<FontWeight> for HighlightStyle {
1042 fn from(font_weight: FontWeight) -> Self {
1043 Self {
1044 font_weight: Some(font_weight),
1045 ..Default::default()
1046 }
1047 }
1048}
1049
1050impl From<FontStyle> for HighlightStyle {
1051 fn from(font_style: FontStyle) -> Self {
1052 Self {
1053 font_style: Some(font_style),
1054 ..Default::default()
1055 }
1056 }
1057}
1058
1059impl From<Rgba> for HighlightStyle {
1060 fn from(color: Rgba) -> Self {
1061 Self {
1062 color: Some(color.into()),
1063 ..Default::default()
1064 }
1065 }
1066}
1067
1068pub fn combine_highlights(
1070 a: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
1071 b: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
1072) -> impl Iterator<Item = (Range<usize>, HighlightStyle)> {
1073 let mut endpoints = Vec::new();
1074 let mut highlights = Vec::new();
1075 for (range, highlight) in a.into_iter().chain(b) {
1076 if !range.is_empty() {
1077 let highlight_id = highlights.len();
1078 endpoints.push((range.start, highlight_id, true));
1079 endpoints.push((range.end, highlight_id, false));
1080 highlights.push(highlight);
1081 }
1082 }
1083 endpoints.sort_unstable_by_key(|(position, _, _)| *position);
1084 let mut endpoints = endpoints.into_iter().peekable();
1085
1086 let mut active_styles = HashSet::default();
1087 let mut ix = 0;
1088 iter::from_fn(move || {
1089 while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() {
1090 let prev_index = mem::replace(&mut ix, *endpoint_ix);
1091 if ix > prev_index && !active_styles.is_empty() {
1092 let current_style = active_styles
1093 .iter()
1094 .fold(HighlightStyle::default(), |acc, highlight_id| {
1095 acc.highlight(highlights[*highlight_id])
1096 });
1097 return Some((prev_index..ix, current_style));
1098 }
1099
1100 if *is_start {
1101 active_styles.insert(*highlight_id);
1102 } else {
1103 active_styles.remove(highlight_id);
1104 }
1105 endpoints.next();
1106 }
1107 None
1108 })
1109}
1110
1111#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
1117pub enum AlignItems {
1119 Start,
1121 End,
1123 FlexStart,
1128 FlexEnd,
1133 Center,
1135 Baseline,
1137 Stretch,
1139}
1140pub type JustifyItems = AlignItems;
1146pub type AlignSelf = AlignItems;
1153pub type JustifySelf = AlignItems;
1160
1161#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
1167pub enum AlignContent {
1169 Start,
1171 End,
1173 FlexStart,
1178 FlexEnd,
1183 Center,
1185 Stretch,
1187 SpaceBetween,
1190 SpaceEvenly,
1193 SpaceAround,
1196}
1197
1198pub type JustifyContent = AlignContent;
1204
1205#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1209pub enum Display {
1211 Block,
1213 #[default]
1215 Flex,
1216 Grid,
1218 None,
1220}
1221
1222#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1228pub enum FlexWrap {
1230 #[default]
1232 NoWrap,
1233 Wrap,
1235 WrapReverse,
1237}
1238
1239#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1251pub enum FlexDirection {
1253 #[default]
1257 Row,
1258 Column,
1262 RowReverse,
1266 ColumnReverse,
1270}
1271
1272#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1286pub enum Overflow {
1288 #[default]
1291 Visible,
1292 Clip,
1295 Hidden,
1298 Scroll,
1302}
1303
1304#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1314pub enum Position {
1316 #[default]
1319 Relative,
1320 Absolute,
1326}
1327
1328impl From<AlignItems> for taffy::style::AlignItems {
1329 fn from(value: AlignItems) -> Self {
1330 match value {
1331 AlignItems::Start => Self::START,
1332 AlignItems::End => Self::END,
1333 AlignItems::FlexStart => Self::FLEX_START,
1334 AlignItems::FlexEnd => Self::FLEX_END,
1335 AlignItems::Center => Self::CENTER,
1336 AlignItems::Baseline => Self::BASELINE,
1337 AlignItems::Stretch => Self::STRETCH,
1338 }
1339 }
1340}
1341
1342impl From<AlignContent> for taffy::style::AlignContent {
1343 fn from(value: AlignContent) -> Self {
1344 match value {
1345 AlignContent::Start => Self::START,
1346 AlignContent::End => Self::END,
1347 AlignContent::FlexStart => Self::FLEX_START,
1348 AlignContent::FlexEnd => Self::FLEX_END,
1349 AlignContent::Center => Self::CENTER,
1350 AlignContent::Stretch => Self::STRETCH,
1351 AlignContent::SpaceBetween => Self::SPACE_BETWEEN,
1352 AlignContent::SpaceEvenly => Self::SPACE_EVENLY,
1353 AlignContent::SpaceAround => Self::SPACE_AROUND,
1354 }
1355 }
1356}
1357
1358impl From<Display> for taffy::style::Display {
1359 fn from(value: Display) -> Self {
1360 match value {
1361 Display::Block => Self::Block,
1362 Display::Flex => Self::Flex,
1363 Display::Grid => Self::Grid,
1364 Display::None => Self::None,
1365 }
1366 }
1367}
1368
1369impl From<FlexWrap> for taffy::style::FlexWrap {
1370 fn from(value: FlexWrap) -> Self {
1371 match value {
1372 FlexWrap::NoWrap => Self::NoWrap,
1373 FlexWrap::Wrap => Self::Wrap,
1374 FlexWrap::WrapReverse => Self::WrapReverse,
1375 }
1376 }
1377}
1378
1379impl From<FlexDirection> for taffy::style::FlexDirection {
1380 fn from(value: FlexDirection) -> Self {
1381 match value {
1382 FlexDirection::Row => Self::Row,
1383 FlexDirection::Column => Self::Column,
1384 FlexDirection::RowReverse => Self::RowReverse,
1385 FlexDirection::ColumnReverse => Self::ColumnReverse,
1386 }
1387 }
1388}
1389
1390impl From<Overflow> for taffy::style::Overflow {
1391 fn from(value: Overflow) -> Self {
1392 match value {
1393 Overflow::Visible => Self::Visible,
1394 Overflow::Clip => Self::Clip,
1395 Overflow::Hidden => Self::Hidden,
1396 Overflow::Scroll => Self::Scroll,
1397 }
1398 }
1399}
1400
1401impl From<Position> for taffy::style::Position {
1402 fn from(value: Position) -> Self {
1403 match value {
1404 Position::Relative => Self::Relative,
1405 Position::Absolute => Self::Absolute,
1406 }
1407 }
1408}
1409
1410#[cfg(test)]
1411mod tests {
1412 use crate::{blue, green, hsla, point, px, red, yellow};
1413
1414 use super::*;
1415
1416 use util_macros::perf;
1417
1418 #[test]
1419 fn test_box_shadow_builder_sets_expected_fields() {
1420 let color = hsla(210., 0.5, 0.4, 0.75);
1421 let shadow = BoxShadow::new(px(1.), px(2.), color)
1422 .blur_radius(px(3.))
1423 .spread_radius(px(4.))
1424 .inset();
1425
1426 assert_eq!(shadow.color, color);
1427 assert_eq!(shadow.offset, point(px(1.), px(2.)));
1428 assert_eq!(shadow.blur_radius, px(3.));
1429 assert_eq!(shadow.spread_radius, px(4.));
1430 assert!(shadow.inset);
1431 }
1432
1433 #[perf]
1434 fn test_basic_highlight_style_combination() {
1435 let style_a = HighlightStyle::default();
1436 let style_b = HighlightStyle::default();
1437 let style_a = style_a.highlight(style_b);
1438 assert_eq!(
1439 style_a,
1440 HighlightStyle::default(),
1441 "Combining empty styles should not produce a non-empty style."
1442 );
1443
1444 let mut style_b = HighlightStyle {
1445 color: Some(red()),
1446 strikethrough: Some(StrikethroughStyle {
1447 thickness: px(2.),
1448 color: Some(blue()),
1449 }),
1450 fade_out: Some(0.),
1451 font_style: Some(FontStyle::Italic),
1452 font_weight: Some(FontWeight(300.)),
1453 background_color: Some(yellow()),
1454 underline: Some(UnderlineStyle {
1455 thickness: px(2.),
1456 color: Some(red()),
1457 wavy: true,
1458 }),
1459 };
1460 let expected_style = style_b;
1461
1462 let style_a = style_a.highlight(style_b);
1463 assert_eq!(
1464 style_a, expected_style,
1465 "Blending an empty style with another style should return the other style"
1466 );
1467
1468 let style_b = style_b.highlight(Default::default());
1469 assert_eq!(
1470 style_b, expected_style,
1471 "Blending a style with an empty style should not change the style."
1472 );
1473
1474 let mut style_c = expected_style;
1475
1476 let style_d = HighlightStyle {
1477 color: Some(blue().alpha(0.7)),
1478 strikethrough: Some(StrikethroughStyle {
1479 thickness: px(4.),
1480 color: Some(crate::red()),
1481 }),
1482 fade_out: Some(0.),
1483 font_style: Some(FontStyle::Oblique),
1484 font_weight: Some(FontWeight(800.)),
1485 background_color: Some(green()),
1486 underline: Some(UnderlineStyle {
1487 thickness: px(4.),
1488 color: None,
1489 wavy: false,
1490 }),
1491 };
1492
1493 let expected_style = HighlightStyle {
1494 color: Some(red().blend(blue().alpha(0.7))),
1495 strikethrough: Some(StrikethroughStyle {
1496 thickness: px(4.),
1497 color: Some(red()),
1498 }),
1499 fade_out: Some(0.),
1501 font_style: Some(FontStyle::Oblique),
1502 font_weight: Some(FontWeight(800.)),
1503 background_color: Some(green()),
1504 underline: Some(UnderlineStyle {
1505 thickness: px(4.),
1506 color: None,
1507 wavy: false,
1508 }),
1509 };
1510
1511 let style_c = style_c.highlight(style_d);
1512 assert_eq!(
1513 style_c, expected_style,
1514 "Blending styles should blend properties where possible and override all others"
1515 );
1516 }
1517
1518 #[perf]
1519 fn test_combine_highlights() {
1520 assert_eq!(
1521 combine_highlights(
1522 [
1523 (0..5, green().into()),
1524 (4..10, FontWeight::BOLD.into()),
1525 (15..20, yellow().into()),
1526 ],
1527 [
1528 (2..6, FontStyle::Italic.into()),
1529 (1..3, blue().into()),
1530 (21..23, red().into()),
1531 ]
1532 )
1533 .collect::<Vec<_>>(),
1534 [
1535 (
1536 0..1,
1537 HighlightStyle {
1538 color: Some(green()),
1539 ..Default::default()
1540 }
1541 ),
1542 (
1543 1..2,
1544 HighlightStyle {
1545 color: Some(blue()),
1546 ..Default::default()
1547 }
1548 ),
1549 (
1550 2..3,
1551 HighlightStyle {
1552 color: Some(blue()),
1553 font_style: Some(FontStyle::Italic),
1554 ..Default::default()
1555 }
1556 ),
1557 (
1558 3..4,
1559 HighlightStyle {
1560 color: Some(green()),
1561 font_style: Some(FontStyle::Italic),
1562 ..Default::default()
1563 }
1564 ),
1565 (
1566 4..5,
1567 HighlightStyle {
1568 color: Some(green()),
1569 font_weight: Some(FontWeight::BOLD),
1570 font_style: Some(FontStyle::Italic),
1571 ..Default::default()
1572 }
1573 ),
1574 (
1575 5..6,
1576 HighlightStyle {
1577 font_weight: Some(FontWeight::BOLD),
1578 font_style: Some(FontStyle::Italic),
1579 ..Default::default()
1580 }
1581 ),
1582 (
1583 6..10,
1584 HighlightStyle {
1585 font_weight: Some(FontWeight::BOLD),
1586 ..Default::default()
1587 }
1588 ),
1589 (
1590 15..20,
1591 HighlightStyle {
1592 color: Some(yellow()),
1593 ..Default::default()
1594 }
1595 ),
1596 (
1597 21..23,
1598 HighlightStyle {
1599 color: Some(red()),
1600 ..Default::default()
1601 }
1602 )
1603 ]
1604 );
1605 }
1606}