1use std::{
2 any::{Any, TypeId},
3 borrow::Cow,
4 fmt,
5 hash::{Hash, Hasher},
6 iter, mem,
7 ops::Range,
8 sync::Arc,
9};
10
11use crate::{
12 AbsoluteLength, AlignContent, AlignItems, AlignSelf, App, Background, BackgroundTag,
13 BorderStyle, Bounds, ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength,
14 DevicePixels, Display, Edges, EdgesRefinement, FlexDirection, FlexWrap, Font, FontFallbacks,
15 FontFeatures, FontStyle, FontWeight, GridLocation, GridTemplate, Hsla, JustifyContent, Length,
16 Overflow, Pixels, Point, PointRefinement, Position, Rgba, SharedString, Size, SizeRefinement,
17 StrikethroughStyle, Styled, TextRun, UnderlineStyle, Window, black, phi, point, px, quad, rems,
18 size,
19};
20use collections::{HashMap, HashSet};
21use refineable::{IsEmpty, Refineable};
22use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
23use serde::{Deserialize, Serialize};
24
25#[cfg(debug_assertions)]
29pub struct DebugBelow;
30
31#[cfg(debug_assertions)]
32impl crate::Global for DebugBelow {}
33
34pub enum ObjectFit {
36 Fill,
38 Contain,
40 Cover,
42 ScaleDown,
44 None,
46}
47
48impl ObjectFit {
49 pub fn get_bounds(
51 &self,
52 bounds: Bounds<Pixels>,
53 image_size: Size<DevicePixels>,
54 ) -> Bounds<Pixels> {
55 let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension)));
56 let image_ratio = image_size.width / image_size.height;
57 let bounds_ratio = bounds.size.width / bounds.size.height;
58
59 match self {
60 ObjectFit::Fill => bounds,
61 ObjectFit::Contain => {
62 let new_size = if bounds_ratio > image_ratio {
63 size(
64 image_size.width * (bounds.size.height / image_size.height),
65 bounds.size.height,
66 )
67 } else {
68 size(
69 bounds.size.width,
70 image_size.height * (bounds.size.width / image_size.width),
71 )
72 };
73
74 Bounds {
75 origin: point(
76 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
77 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
78 ),
79 size: new_size,
80 }
81 }
82 ObjectFit::ScaleDown => {
83 if image_size.width > bounds.size.width || image_size.height > bounds.size.height {
85 let new_size = if bounds_ratio > image_ratio {
87 size(
88 image_size.width * (bounds.size.height / image_size.height),
89 bounds.size.height,
90 )
91 } else {
92 size(
93 bounds.size.width,
94 image_size.height * (bounds.size.width / image_size.width),
95 )
96 };
97
98 Bounds {
99 origin: point(
100 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
101 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
102 ),
103 size: new_size,
104 }
105 } else {
106 let original_size = size(image_size.width, image_size.height);
109 Bounds {
110 origin: point(
111 bounds.origin.x + (bounds.size.width - original_size.width) / 2.0,
112 bounds.origin.y + (bounds.size.height - original_size.height) / 2.0,
113 ),
114 size: original_size,
115 }
116 }
117 }
118 ObjectFit::Cover => {
119 let new_size = if bounds_ratio > image_ratio {
120 size(
121 bounds.size.width,
122 image_size.height * (bounds.size.width / image_size.width),
123 )
124 } else {
125 size(
126 image_size.width * (bounds.size.height / image_size.height),
127 bounds.size.height,
128 )
129 };
130
131 Bounds {
132 origin: point(
133 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
134 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
135 ),
136 size: new_size,
137 }
138 }
139 ObjectFit::None => Bounds {
140 origin: bounds.origin,
141 size: image_size,
142 },
143 }
144 }
145}
146
147pub trait CustomStyleProperty: 'static + Send + Sync {
159 fn as_any(&self) -> &dyn Any;
161
162 fn eq_property(&self, other: &dyn CustomStyleProperty) -> bool;
164}
165
166impl<T: 'static + Send + Sync + PartialEq> CustomStyleProperty for T {
167 fn as_any(&self) -> &dyn Any {
168 self
169 }
170
171 fn eq_property(&self, other: &dyn CustomStyleProperty) -> bool {
172 other.as_any().downcast_ref::<T>() == Some(self)
173 }
174}
175
176#[derive(Clone, Default)]
183pub struct CustomStyles {
184 entries: Option<Arc<HashMap<TypeId, Arc<dyn CustomStyleProperty>>>>,
185}
186
187pub type CustomStylesRefinement = CustomStyles;
190
191impl CustomStyles {
192 pub fn insert<T: CustomStyleProperty>(&mut self, property: T) {
194 self.entries_mut()
195 .insert(TypeId::of::<T>(), Arc::new(property));
196 }
197
198 pub fn get<T: CustomStyleProperty>(&self) -> Option<&T> {
200 self.entries
201 .as_ref()?
202 .get(&TypeId::of::<T>())?
203 .as_any()
204 .downcast_ref::<T>()
205 }
206
207 pub fn refine(&mut self, other: &Self) {
210 let Some(other_entries) = other.entries.as_ref() else {
211 return;
212 };
213 let entries = self.entries_mut();
214 for (type_id, property) in other_entries.iter() {
215 entries.insert(*type_id, property.clone());
216 }
217 }
218
219 fn entries_mut(&mut self) -> &mut HashMap<TypeId, Arc<dyn CustomStyleProperty>> {
220 Arc::make_mut(self.entries.get_or_insert_with(Default::default))
221 }
222
223 fn is_some(&self) -> bool {
226 self.entries
227 .as_ref()
228 .is_some_and(|entries| !entries.is_empty())
229 }
230}
231
232impl IsEmpty for CustomStyles {
233 fn is_empty(&self) -> bool {
234 self.entries
235 .as_ref()
236 .is_none_or(|entries| entries.is_empty())
237 }
238}
239
240impl Refineable for CustomStyles {
241 type Refinement = CustomStylesRefinement;
242
243 fn refine(&mut self, refinement: &Self::Refinement) {
244 CustomStyles::refine(self, refinement);
245 }
246
247 fn refined(mut self, refinement: Self::Refinement) -> Self {
248 CustomStyles::refine(&mut self, &refinement);
249 self
250 }
251
252 fn is_superset_of(&self, refinement: &Self::Refinement) -> bool {
253 let Some(refinement_entries) = refinement.entries.as_ref() else {
254 return true;
255 };
256 let Some(entries) = self.entries.as_ref() else {
257 return refinement_entries.is_empty();
258 };
259 refinement_entries.iter().all(|(type_id, property)| {
260 entries
261 .get(type_id)
262 .is_some_and(|value| value.eq_property(&**property))
263 })
264 }
265
266 fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement {
267 let Some(entries) = self.entries.as_ref() else {
268 return CustomStyles::default();
269 };
270 let mut subtracted = CustomStyles::default();
271 for (type_id, property) in entries.iter() {
272 let covered_by_refinement = refinement
273 .entries
274 .as_ref()
275 .and_then(|refinement_entries| refinement_entries.get(type_id))
276 .is_some_and(|refined| property.eq_property(&**refined));
277 if !covered_by_refinement {
278 subtracted.entries_mut().insert(*type_id, property.clone());
279 }
280 }
281 subtracted
282 }
283}
284
285impl PartialEq for CustomStyles {
286 fn eq(&self, other: &Self) -> bool {
287 let no_properties = HashMap::default();
290 let entries = self.entries.as_deref().unwrap_or(&no_properties);
291 let other_entries = other.entries.as_deref().unwrap_or(&no_properties);
292 entries.len() == other_entries.len()
293 && entries.iter().all(|(type_id, property)| {
294 other_entries
295 .get(type_id)
296 .is_some_and(|other| property.eq_property(&**other))
297 })
298 }
299}
300
301impl fmt::Debug for CustomStyles {
302 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303 f.debug_struct("CustomStyles")
306 .field(
307 "len",
308 &self.entries.as_ref().map_or(0, |entries| entries.len()),
309 )
310 .finish()
311 }
312}
313
314impl Serialize for CustomStyles {
315 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
316 serializer.serialize_unit()
320 }
321}
322
323impl<'de> Deserialize<'de> for CustomStyles {
324 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
325 serde::de::IgnoredAny::deserialize(deserializer)?;
328 Ok(CustomStyles::default())
329 }
330}
331
332impl JsonSchema for CustomStyles {
333 fn schema_name() -> Cow<'static, str> {
334 "CustomStyles".into()
335 }
336
337 fn json_schema(_: &mut SchemaGenerator) -> Schema {
338 json_schema!({ "type": "null" })
340 }
341}
342
343#[derive(Clone, Refineable, Debug)]
345#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
346pub struct Style {
347 pub display: Display,
349
350 pub visibility: Visibility,
352
353 #[refineable]
356 pub overflow: Point<Overflow>,
357 pub scrollbar_width: AbsoluteLength,
359 pub allow_concurrent_scroll: bool,
361 pub restrict_scroll_to_axis: bool,
387
388 pub position: Position,
391 #[refineable]
393 pub inset: Edges<Length>,
394
395 #[refineable]
398 pub size: Size<Length>,
399 #[refineable]
401 pub min_size: Size<Length>,
402 #[refineable]
404 pub max_size: Size<Length>,
405 pub aspect_ratio: Option<f32>,
407
408 #[refineable]
411 pub margin: Edges<Length>,
412 #[refineable]
414 pub padding: Edges<DefiniteLength>,
415 #[refineable]
417 pub border_widths: Edges<AbsoluteLength>,
418
419 pub align_items: Option<AlignItems>,
422 pub align_self: Option<AlignSelf>,
424 pub align_content: Option<AlignContent>,
426 pub justify_content: Option<JustifyContent>,
428 #[refineable]
430 pub gap: Size<DefiniteLength>,
431
432 pub flex_direction: FlexDirection,
435 pub flex_wrap: FlexWrap,
437 pub flex_basis: Length,
439 pub flex_grow: f32,
441 pub flex_shrink: f32,
443
444 pub background: Option<Fill>,
446
447 pub border_color: Option<Hsla>,
449
450 pub border_style: BorderStyle,
452
453 #[refineable]
455 pub corner_radii: Corners<AbsoluteLength>,
456
457 pub box_shadow: Vec<BoxShadow>,
459
460 #[refineable]
462 pub text: TextStyleRefinement,
463
464 pub mouse_cursor: Option<CursorStyle>,
466
467 pub opacity: Option<f32>,
469
470 pub grid_cols: Option<GridTemplate>,
473
474 pub grid_rows: Option<GridTemplate>,
477
478 pub grid_location: Option<GridLocation>,
480
481 #[refineable]
485 pub custom: CustomStyles,
486
487 #[cfg(debug_assertions)]
489 pub debug: bool,
490
491 #[cfg(debug_assertions)]
493 pub debug_below: bool,
494}
495
496impl Styled for StyleRefinement {
497 fn style(&mut self) -> &mut StyleRefinement {
498 self
499 }
500}
501
502impl StyleRefinement {
503 pub fn grid_location_mut(&mut self) -> &mut GridLocation {
505 self.grid_location.get_or_insert_default()
506 }
507}
508
509#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
511pub enum Visibility {
512 #[default]
514 Visible,
515 Hidden,
517}
518
519#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
521pub struct BoxShadow {
522 pub color: Hsla,
524 pub offset: Point<Pixels>,
526 pub blur_radius: Pixels,
528 pub spread_radius: Pixels,
530 pub inset: bool,
532}
533
534impl BoxShadow {
535 pub fn new(offset_x: Pixels, offset_y: Pixels, color: Hsla) -> Self {
539 Self {
540 color,
541 offset: point(offset_x, offset_y),
542 blur_radius: px(0.),
543 spread_radius: px(0.),
544 inset: false,
545 }
546 }
547
548 pub fn blur_radius(mut self, blur_radius: Pixels) -> Self {
550 self.blur_radius = blur_radius;
551 self
552 }
553
554 pub fn spread_radius(mut self, spread_radius: Pixels) -> Self {
556 self.spread_radius = spread_radius;
557 self
558 }
559
560 pub fn inset(mut self) -> Self {
562 self.inset = true;
563 self
564 }
565}
566
567#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
569pub enum WhiteSpace {
570 #[default]
572 Normal,
573 Nowrap,
575}
576
577#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
579pub enum TextOverflow {
580 Truncate(SharedString),
583 TruncateStart(SharedString),
587 TruncateMiddle(SharedString),
591}
592
593#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
595pub enum TextAlign {
596 #[default]
598 Left,
599
600 Center,
602
603 Right,
605}
606
607#[derive(Refineable, Clone, Debug, PartialEq)]
609#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
610pub struct TextStyle {
611 pub color: Hsla,
613
614 pub font_family: SharedString,
616
617 pub font_features: FontFeatures,
619
620 pub font_fallbacks: Option<FontFallbacks>,
622
623 pub font_size: AbsoluteLength,
625
626 pub line_height: DefiniteLength,
628
629 pub font_weight: FontWeight,
631
632 pub font_style: FontStyle,
634
635 pub background_color: Option<Hsla>,
637
638 pub underline: Option<UnderlineStyle>,
640
641 pub strikethrough: Option<StrikethroughStyle>,
643
644 pub white_space: WhiteSpace,
646
647 pub text_overflow: Option<TextOverflow>,
649
650 pub text_align: TextAlign,
652
653 pub line_clamp: Option<usize>,
655}
656
657impl Default for TextStyle {
658 fn default() -> Self {
659 TextStyle {
660 color: black(),
661 font_family: ".SystemUIFont".into(),
663 font_features: FontFeatures::default(),
664 font_fallbacks: None,
665 font_size: rems(1.).into(),
666 line_height: phi(),
667 font_weight: FontWeight::default(),
668 font_style: FontStyle::default(),
669 background_color: None,
670 underline: None,
671 strikethrough: None,
672 white_space: WhiteSpace::Normal,
673 text_overflow: None,
674 text_align: TextAlign::default(),
675 line_clamp: None,
676 }
677 }
678}
679
680impl TextStyle {
681 pub fn highlight(mut self, style: impl Into<HighlightStyle>) -> Self {
683 let style = style.into();
684 if let Some(weight) = style.font_weight {
685 self.font_weight = weight;
686 }
687 if let Some(style) = style.font_style {
688 self.font_style = style;
689 }
690
691 if let Some(color) = style.color {
692 self.color = self.color.blend(color);
693 }
694
695 if let Some(factor) = style.fade_out {
696 self.color.fade_out(factor);
697 }
698
699 if let Some(background_color) = style.background_color {
700 self.background_color = Some(background_color);
701 }
702
703 if let Some(underline) = style.underline {
704 self.underline = Some(underline);
705 }
706
707 if let Some(strikethrough) = style.strikethrough {
708 self.strikethrough = Some(strikethrough);
709 }
710
711 self
712 }
713
714 pub fn font(&self) -> Font {
716 Font {
717 family: self.font_family.clone(),
718 features: self.font_features.clone(),
719 fallbacks: self.font_fallbacks.clone(),
720 weight: self.font_weight,
721 style: self.font_style,
722 }
723 }
724
725 pub fn line_height_in_pixels(&self, rem_size: Pixels) -> Pixels {
727 self.line_height.to_pixels(self.font_size, rem_size).round()
728 }
729
730 pub fn to_run(&self, len: usize) -> TextRun {
732 TextRun {
733 len,
734 font: Font {
735 family: self.font_family.clone(),
736 features: self.font_features.clone(),
737 fallbacks: self.font_fallbacks.clone(),
738 weight: self.font_weight,
739 style: self.font_style,
740 },
741 color: self.color,
742 background_color: self.background_color,
743 underline: self.underline,
744 strikethrough: self.strikethrough,
745 }
746 }
747}
748
749#[derive(Copy, Clone, Debug, Default, PartialEq)]
752pub struct HighlightStyle {
753 pub color: Option<Hsla>,
755
756 pub font_weight: Option<FontWeight>,
758
759 pub font_style: Option<FontStyle>,
761
762 pub background_color: Option<Hsla>,
764
765 pub underline: Option<UnderlineStyle>,
767
768 pub strikethrough: Option<StrikethroughStyle>,
770
771 pub fade_out: Option<f32>,
773}
774
775impl Eq for HighlightStyle {}
776
777impl Hash for HighlightStyle {
778 fn hash<H: Hasher>(&self, state: &mut H) {
779 self.color.hash(state);
780 self.font_weight.hash(state);
781 self.font_style.hash(state);
782 self.background_color.hash(state);
783 self.underline.hash(state);
784 self.strikethrough.hash(state);
785 state.write_u32(u32::from_be_bytes(
786 self.fade_out.map(|f| f.to_be_bytes()).unwrap_or_default(),
787 ));
788 }
789}
790
791impl Style {
792 pub fn has_opaque_background(&self) -> bool {
794 self.background
795 .as_ref()
796 .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent()))
797 }
798
799 pub fn text_style(&self) -> Option<&TextStyleRefinement> {
801 if self.text.is_some() {
802 Some(&self.text)
803 } else {
804 None
805 }
806 }
807
808 pub fn overflow_mask(
811 &self,
812 bounds: Bounds<Pixels>,
813 rem_size: Pixels,
814 ) -> Option<ContentMask<Pixels>> {
815 match self.overflow {
816 Point {
817 x: Overflow::Visible,
818 y: Overflow::Visible,
819 } => None,
820 _ => {
821 let mut min = bounds.origin;
822 let mut max = bounds.bottom_right();
823
824 if self
825 .border_color
826 .is_some_and(|color| !color.is_transparent())
827 {
828 min.x += self.border_widths.left.to_pixels(rem_size);
829 max.x -= self.border_widths.right.to_pixels(rem_size);
830 min.y += self.border_widths.top.to_pixels(rem_size);
831 max.y -= self.border_widths.bottom.to_pixels(rem_size);
832 }
833
834 let bounds = match (
835 self.overflow.x == Overflow::Visible,
836 self.overflow.y == Overflow::Visible,
837 ) {
838 (true, true) => return None,
840 (true, false) => Bounds::from_corners(
842 point(min.x, bounds.origin.y),
843 point(max.x, bounds.bottom_right().y),
844 ),
845 (false, true) => Bounds::from_corners(
847 point(bounds.origin.x, min.y),
848 point(bounds.bottom_right().x, max.y),
849 ),
850 (false, false) => Bounds::from_corners(min, max),
852 };
853
854 Some(ContentMask { bounds })
855 }
856 }
857 }
858
859 pub fn paint(
861 &self,
862 bounds: Bounds<Pixels>,
863 window: &mut Window,
864 cx: &mut App,
865 continuation: impl FnOnce(&mut Window, &mut App),
866 ) {
867 #[cfg(debug_assertions)]
868 if self.debug_below {
869 cx.set_global(DebugBelow)
870 }
871
872 #[cfg(debug_assertions)]
873 if self.debug || cx.has_global::<DebugBelow>() {
874 window.paint_quad(crate::outline(bounds, crate::red(), BorderStyle::default()));
875 }
876
877 let rem_size = window.rem_size();
878 let corner_radii = self
879 .corner_radii
880 .to_pixels(rem_size)
881 .clamp_radii_for_quad_size(bounds.size);
882
883 window.paint_drop_shadows(bounds, corner_radii, &self.box_shadow);
884
885 let background_color = self.background.as_ref().and_then(Fill::color);
886 if background_color.is_some_and(|color| !color.is_transparent()) {
887 let mut border_color = match background_color {
888 Some(color) => match color.tag {
889 BackgroundTag::Solid
890 | BackgroundTag::PatternSlash
891 | BackgroundTag::Checkerboard => color.solid,
892
893 BackgroundTag::LinearGradient => color
894 .colors
895 .first()
896 .map(|stop| stop.color)
897 .unwrap_or_default(),
898 },
899 None => Hsla::default(),
900 };
901 border_color.a = 0.;
902 window.paint_quad(quad(
903 bounds,
904 corner_radii,
905 background_color.unwrap_or_default(),
906 Edges::default(),
907 border_color,
908 self.border_style,
909 ));
910 }
911
912 window.paint_inset_shadows(bounds, corner_radii, &self.box_shadow);
913
914 continuation(window, cx);
915
916 if self.is_border_visible() {
917 let border_widths = self.border_widths.to_pixels(rem_size);
918 let mut background = self.border_color.unwrap_or_default();
919 background.a = 0.;
920 window.paint_quad(quad(
921 bounds,
922 corner_radii,
923 background,
924 border_widths,
925 self.border_color.unwrap_or_default(),
926 self.border_style,
927 ));
928 }
929
930 #[cfg(debug_assertions)]
931 if self.debug_below {
932 cx.remove_global::<DebugBelow>();
933 }
934 }
935
936 fn is_border_visible(&self) -> bool {
937 self.border_color
938 .is_some_and(|color| !color.is_transparent())
939 && self.border_widths.any(|length| !length.is_zero())
940 }
941}
942
943impl Default for Style {
944 fn default() -> Self {
945 Style {
946 display: Display::Block,
947 visibility: Visibility::Visible,
948 overflow: Point {
949 x: Overflow::Visible,
950 y: Overflow::Visible,
951 },
952 allow_concurrent_scroll: false,
953 restrict_scroll_to_axis: false,
954 scrollbar_width: AbsoluteLength::default(),
955 position: Position::Relative,
956 inset: Edges::auto(),
957 margin: Edges::<Length>::zero(),
958 padding: Edges::<DefiniteLength>::zero(),
959 border_widths: Edges::<AbsoluteLength>::zero(),
960 size: Size::auto(),
961 min_size: Size::auto(),
962 max_size: Size::auto(),
963 aspect_ratio: None,
964 gap: Size::default(),
965 align_items: None,
967 align_self: None,
968 align_content: None,
969 justify_content: None,
970 flex_direction: FlexDirection::Row,
972 flex_wrap: FlexWrap::NoWrap,
973 flex_grow: 0.0,
974 flex_shrink: 1.0,
975 flex_basis: Length::Auto,
976 background: None,
977 border_color: None,
978 border_style: BorderStyle::default(),
979 corner_radii: Corners::default(),
980 box_shadow: Default::default(),
981 text: TextStyleRefinement::default(),
982 mouse_cursor: None,
983 opacity: None,
984 grid_rows: None,
985 grid_cols: None,
986 grid_location: None,
987 custom: CustomStyles::default(),
988
989 #[cfg(debug_assertions)]
990 debug: false,
991 #[cfg(debug_assertions)]
992 debug_below: false,
993 }
994 }
995}
996
997#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
999pub enum Fill {
1000 Color(Background),
1002}
1003
1004impl Fill {
1005 pub fn color(&self) -> Option<Background> {
1009 match self {
1010 Fill::Color(color) => Some(*color),
1011 }
1012 }
1013}
1014
1015impl Default for Fill {
1016 fn default() -> Self {
1017 Self::Color(Background::default())
1018 }
1019}
1020
1021impl From<Hsla> for Fill {
1022 fn from(color: Hsla) -> Self {
1023 Self::Color(color.into())
1024 }
1025}
1026
1027impl From<Rgba> for Fill {
1028 fn from(color: Rgba) -> Self {
1029 Self::Color(color.into())
1030 }
1031}
1032
1033impl From<Background> for Fill {
1034 fn from(background: Background) -> Self {
1035 Self::Color(background)
1036 }
1037}
1038
1039impl From<TextStyle> for HighlightStyle {
1040 fn from(other: TextStyle) -> Self {
1041 Self::from(&other)
1042 }
1043}
1044
1045impl From<&TextStyle> for HighlightStyle {
1046 fn from(other: &TextStyle) -> Self {
1047 Self {
1048 color: Some(other.color),
1049 font_weight: Some(other.font_weight),
1050 font_style: Some(other.font_style),
1051 background_color: other.background_color,
1052 underline: other.underline,
1053 strikethrough: other.strikethrough,
1054 fade_out: None,
1055 }
1056 }
1057}
1058
1059impl HighlightStyle {
1060 pub fn color(color: Hsla) -> Self {
1062 Self {
1063 color: Some(color),
1064 ..Default::default()
1065 }
1066 }
1067 #[must_use]
1070 pub fn highlight(self, other: HighlightStyle) -> Self {
1071 Self {
1072 color: other
1073 .color
1074 .map(|other_color| {
1075 if let Some(color) = self.color {
1076 color.blend(other_color)
1077 } else {
1078 other_color
1079 }
1080 })
1081 .or(self.color),
1082 font_weight: other.font_weight.or(self.font_weight),
1083 font_style: other.font_style.or(self.font_style),
1084 background_color: other.background_color.or(self.background_color),
1085 underline: other.underline.or(self.underline),
1086 strikethrough: other.strikethrough.or(self.strikethrough),
1087 fade_out: other
1088 .fade_out
1089 .map(|source_fade| {
1090 self.fade_out
1091 .map(|dest_fade| (dest_fade * (1. + source_fade)).clamp(0., 1.))
1092 .unwrap_or(source_fade)
1093 })
1094 .or(self.fade_out),
1095 }
1096 }
1097}
1098
1099impl From<Hsla> for HighlightStyle {
1100 fn from(color: Hsla) -> Self {
1101 Self {
1102 color: Some(color),
1103 ..Default::default()
1104 }
1105 }
1106}
1107
1108impl From<FontWeight> for HighlightStyle {
1109 fn from(font_weight: FontWeight) -> Self {
1110 Self {
1111 font_weight: Some(font_weight),
1112 ..Default::default()
1113 }
1114 }
1115}
1116
1117impl From<FontStyle> for HighlightStyle {
1118 fn from(font_style: FontStyle) -> Self {
1119 Self {
1120 font_style: Some(font_style),
1121 ..Default::default()
1122 }
1123 }
1124}
1125
1126impl From<Rgba> for HighlightStyle {
1127 fn from(color: Rgba) -> Self {
1128 Self {
1129 color: Some(color.into()),
1130 ..Default::default()
1131 }
1132 }
1133}
1134
1135pub fn combine_highlights(
1137 a: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
1138 b: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
1139) -> impl Iterator<Item = (Range<usize>, HighlightStyle)> {
1140 let mut endpoints = Vec::new();
1141 let mut highlights = Vec::new();
1142 for (range, highlight) in a.into_iter().chain(b) {
1143 if !range.is_empty() {
1144 let highlight_id = highlights.len();
1145 endpoints.push((range.start, highlight_id, true));
1146 endpoints.push((range.end, highlight_id, false));
1147 highlights.push(highlight);
1148 }
1149 }
1150 endpoints.sort_unstable_by_key(|(position, _, _)| *position);
1151 let mut endpoints = endpoints.into_iter().peekable();
1152
1153 let mut active_styles = HashSet::default();
1154 let mut ix = 0;
1155 iter::from_fn(move || {
1156 while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() {
1157 let prev_index = mem::replace(&mut ix, *endpoint_ix);
1158 if ix > prev_index && !active_styles.is_empty() {
1159 let current_style = active_styles
1160 .iter()
1161 .fold(HighlightStyle::default(), |acc, highlight_id| {
1162 acc.highlight(highlights[*highlight_id])
1163 });
1164 return Some((prev_index..ix, current_style));
1165 }
1166
1167 if *is_start {
1168 active_styles.insert(*highlight_id);
1169 } else {
1170 active_styles.remove(highlight_id);
1171 }
1172 endpoints.next();
1173 }
1174 None
1175 })
1176}
1177
1178#[cfg(test)]
1179mod tests {
1180 use crate::{blue, green, px, red, yellow};
1181
1182 use super::*;
1183
1184 use util_macros::perf;
1185
1186 #[perf]
1187 fn test_basic_highlight_style_combination() {
1188 let style_a = HighlightStyle::default();
1189 let style_b = HighlightStyle::default();
1190 let style_a = style_a.highlight(style_b);
1191 assert_eq!(
1192 style_a,
1193 HighlightStyle::default(),
1194 "Combining empty styles should not produce a non-empty style."
1195 );
1196
1197 let mut style_b = HighlightStyle {
1198 color: Some(red()),
1199 strikethrough: Some(StrikethroughStyle {
1200 thickness: px(2.),
1201 color: Some(blue()),
1202 }),
1203 fade_out: Some(0.),
1204 font_style: Some(FontStyle::Italic),
1205 font_weight: Some(FontWeight(300.)),
1206 background_color: Some(yellow()),
1207 underline: Some(UnderlineStyle {
1208 thickness: px(2.),
1209 color: Some(red()),
1210 wavy: true,
1211 }),
1212 };
1213 let expected_style = style_b;
1214
1215 let style_a = style_a.highlight(style_b);
1216 assert_eq!(
1217 style_a, expected_style,
1218 "Blending an empty style with another style should return the other style"
1219 );
1220
1221 let style_b = style_b.highlight(Default::default());
1222 assert_eq!(
1223 style_b, expected_style,
1224 "Blending a style with an empty style should not change the style."
1225 );
1226
1227 let mut style_c = expected_style;
1228
1229 let style_d = HighlightStyle {
1230 color: Some(blue().alpha(0.7)),
1231 strikethrough: Some(StrikethroughStyle {
1232 thickness: px(4.),
1233 color: Some(crate::red()),
1234 }),
1235 fade_out: Some(0.),
1236 font_style: Some(FontStyle::Oblique),
1237 font_weight: Some(FontWeight(800.)),
1238 background_color: Some(green()),
1239 underline: Some(UnderlineStyle {
1240 thickness: px(4.),
1241 color: None,
1242 wavy: false,
1243 }),
1244 };
1245
1246 let expected_style = HighlightStyle {
1247 color: Some(red().blend(blue().alpha(0.7))),
1248 strikethrough: Some(StrikethroughStyle {
1249 thickness: px(4.),
1250 color: Some(red()),
1251 }),
1252 fade_out: Some(0.),
1254 font_style: Some(FontStyle::Oblique),
1255 font_weight: Some(FontWeight(800.)),
1256 background_color: Some(green()),
1257 underline: Some(UnderlineStyle {
1258 thickness: px(4.),
1259 color: None,
1260 wavy: false,
1261 }),
1262 };
1263
1264 let style_c = style_c.highlight(style_d);
1265 assert_eq!(
1266 style_c, expected_style,
1267 "Blending styles should blend properties where possible and override all others"
1268 );
1269 }
1270
1271 #[perf]
1272 fn test_combine_highlights() {
1273 assert_eq!(
1274 combine_highlights(
1275 [
1276 (0..5, green().into()),
1277 (4..10, FontWeight::BOLD.into()),
1278 (15..20, yellow().into()),
1279 ],
1280 [
1281 (2..6, FontStyle::Italic.into()),
1282 (1..3, blue().into()),
1283 (21..23, red().into()),
1284 ]
1285 )
1286 .collect::<Vec<_>>(),
1287 [
1288 (
1289 0..1,
1290 HighlightStyle {
1291 color: Some(green()),
1292 ..Default::default()
1293 }
1294 ),
1295 (
1296 1..2,
1297 HighlightStyle {
1298 color: Some(blue()),
1299 ..Default::default()
1300 }
1301 ),
1302 (
1303 2..3,
1304 HighlightStyle {
1305 color: Some(blue()),
1306 font_style: Some(FontStyle::Italic),
1307 ..Default::default()
1308 }
1309 ),
1310 (
1311 3..4,
1312 HighlightStyle {
1313 color: Some(green()),
1314 font_style: Some(FontStyle::Italic),
1315 ..Default::default()
1316 }
1317 ),
1318 (
1319 4..5,
1320 HighlightStyle {
1321 color: Some(green()),
1322 font_weight: Some(FontWeight::BOLD),
1323 font_style: Some(FontStyle::Italic),
1324 ..Default::default()
1325 }
1326 ),
1327 (
1328 5..6,
1329 HighlightStyle {
1330 font_weight: Some(FontWeight::BOLD),
1331 font_style: Some(FontStyle::Italic),
1332 ..Default::default()
1333 }
1334 ),
1335 (
1336 6..10,
1337 HighlightStyle {
1338 font_weight: Some(FontWeight::BOLD),
1339 ..Default::default()
1340 }
1341 ),
1342 (
1343 15..20,
1344 HighlightStyle {
1345 color: Some(yellow()),
1346 ..Default::default()
1347 }
1348 ),
1349 (
1350 21..23,
1351 HighlightStyle {
1352 color: Some(red()),
1353 ..Default::default()
1354 }
1355 )
1356 ]
1357 );
1358 }
1359
1360 #[perf]
1361 fn test_text_style_refinement() {
1362 let mut style = Style::default();
1363 style.refine(&StyleRefinement::default().text_size(px(20.0)));
1364 style.refine(&StyleRefinement::default().font_weight(FontWeight::SEMIBOLD));
1365
1366 assert_eq!(
1367 Some(AbsoluteLength::from(px(20.0))),
1368 style.text_style().unwrap().font_size
1369 );
1370
1371 assert_eq!(
1372 Some(FontWeight::SEMIBOLD),
1373 style.text_style().unwrap().font_weight
1374 );
1375 }
1376
1377 #[derive(Clone, Debug, PartialEq)]
1378 struct BackdropBlur(f32);
1379
1380 #[derive(Clone, Debug, PartialEq)]
1381 struct CustomShader(u32);
1382
1383 #[derive(Default)]
1384 struct Element {
1385 refinement: StyleRefinement,
1386 }
1387
1388 impl Styled for Element {
1389 fn style(&mut self) -> &mut StyleRefinement {
1390 &mut self.refinement
1391 }
1392 }
1393
1394 #[test]
1395 fn custom_style_properties_reach_the_resolved_style() {
1396 let element = Element::default()
1397 .custom_style(BackdropBlur(8.0))
1398 .custom_style(CustomShader(7));
1399
1400 let mut style = Style::default();
1401 style.refine(&element.refinement);
1402
1403 assert_eq!(style.custom.get::<BackdropBlur>(), Some(&BackdropBlur(8.0)));
1404 assert_eq!(style.custom.get::<CustomShader>(), Some(&CustomShader(7)));
1405 }
1406
1407 #[test]
1408 fn refining_custom_style_properties_overrides_matching_keys_and_keeps_disjoint_ones() {
1409 let mut inherited = StyleRefinement::default();
1410 inherited.custom.insert(BackdropBlur(8.0));
1411 inherited.custom.insert(CustomShader(7));
1412
1413 let mut overridden = StyleRefinement::default();
1414 overridden.custom.insert(BackdropBlur(16.0));
1415
1416 let mut style = Style::default();
1417 style.refine(&inherited);
1418 style.refine(&overridden);
1419
1420 assert_eq!(
1421 style.custom.get::<BackdropBlur>(),
1422 Some(&BackdropBlur(16.0)),
1423 "the later refinement should win"
1424 );
1425 assert_eq!(
1426 style.custom.get::<CustomShader>(),
1427 Some(&CustomShader(7)),
1428 "a property the later refinement does not set should survive"
1429 );
1430 }
1431
1432 #[test]
1433 fn custom_style_properties_are_shared_until_written() {
1434 let mut style = Style::default();
1435 style.custom.insert(BackdropBlur(8.0));
1436
1437 let mut clone = style.clone();
1438 clone.custom.insert(BackdropBlur(16.0));
1439
1440 assert_eq!(
1441 style.custom.get::<BackdropBlur>(),
1442 Some(&BackdropBlur(8.0)),
1443 "writing to a clone should not disturb the original"
1444 );
1445 }
1446
1447 #[test]
1448 fn custom_style_properties_are_diffed_by_value() {
1449 let mut style = Style::default();
1450 style.custom.insert(BackdropBlur(8.0));
1451
1452 let mut matching = StyleRefinement::default();
1453 matching.custom.insert(BackdropBlur(8.0));
1454 assert!(style.is_superset_of(&matching));
1455 assert!(style.subtract(&matching).custom.is_empty());
1456
1457 let mut differing = StyleRefinement::default();
1458 differing.custom.insert(BackdropBlur(16.0));
1459 assert!(!style.is_superset_of(&differing));
1460 assert_eq!(
1461 style.subtract(&differing).custom.get::<BackdropBlur>(),
1462 Some(&BackdropBlur(8.0))
1463 );
1464 }
1465
1466 #[test]
1467 fn styles_without_custom_style_properties_are_indistinguishable() {
1468 assert_eq!(Style::default().custom, CustomStyles::default());
1469 assert!(Style::default().custom.is_empty());
1470 assert_eq!(
1471 std::mem::size_of::<CustomStyles>(),
1472 std::mem::size_of::<Option<Arc<()>>>(),
1473 "custom properties should cost a style no more than a null pointer"
1474 );
1475 }
1476
1477 #[test]
1478 fn custom_style_properties_do_not_round_trip_through_json() {
1479 let mut refinement = StyleRefinement::default();
1480 refinement.custom.insert(BackdropBlur(8.0));
1481
1482 let json = serde_json::to_string(&refinement).expect("the refinement should serialize");
1483 let deserialized: StyleRefinement =
1484 serde_json::from_str(&json).expect("the refinement should deserialize");
1485
1486 assert!(
1487 deserialized.custom.is_empty(),
1488 "a property type cannot be named in JSON, so nothing should come back"
1489 );
1490 }
1491}