Skip to main content

gpui_core/
style.rs

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/// Use this struct for interfacing with the 'debug_below' styling from your own elements.
22/// If a parent element has this style set on it, then this struct will be set as a global in
23/// GPUI.
24#[cfg(debug_assertions)]
25pub struct DebugBelow;
26
27#[cfg(debug_assertions)]
28impl crate::Global for DebugBelow {}
29
30/// How to fit the image into the bounds of the element.
31pub enum ObjectFit {
32    /// The image will be stretched to fill the bounds of the element.
33    Fill,
34    /// The image will be scaled to fit within the bounds of the element.
35    Contain,
36    /// The image will be scaled to cover the bounds of the element.
37    Cover,
38    /// The image will be scaled down to fit within the bounds of the element.
39    ScaleDown,
40    /// The image will maintain its original size.
41    None,
42}
43
44impl ObjectFit {
45    /// Get the bounds of the image within the given bounds.
46    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                // Check if the image is larger than the bounds in either dimension.
80                if image_size.width > bounds.size.width || image_size.height > bounds.size.height {
81                    // If the image is larger, use the same logic as Contain to scale it down.
82                    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                    // If the image is smaller than or equal to the container, display it at its original size,
103                    // centered within the container.
104                    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/// The minimum size of a repeated grid column or row.
144#[derive(
145    Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default, JsonSchema, Serialize, Deserialize,
146)]
147pub enum TemplateColumnMinSize {
148    /// The track minimum is zero.
149    #[default]
150    Zero,
151    /// The track minimum is min-content.
152    MinContent,
153    /// The track maximum is max-content.
154    MaxContent,
155}
156
157/// A simplified representation of a repeated `grid-template-*` value.
158#[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    /// Number of repeated tracks.
174    pub repeat: u16,
175    /// Track sizing mode.
176    pub min_size: TemplateColumnMinSize,
177}
178
179/// The CSS styling that can be applied to an element via the `Styled` trait
180#[derive(Clone, Refineable, Debug)]
181#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
182pub struct Style {
183    /// What layout strategy should be used?
184    pub display: Display,
185
186    /// Should the element be painted on screen?
187    pub visibility: Visibility,
188
189    // Overflow properties
190    /// How children overflowing their container should affect layout
191    #[refineable]
192    pub overflow: Point<Overflow>,
193    /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes.
194    pub scrollbar_width: AbsoluteLength,
195    /// Whether both x and y axis should be scrollable at the same time.
196    pub allow_concurrent_scroll: bool,
197    /// Whether scrolling should be restricted to the input gesture's axis.
198    ///
199    /// Pixel-based scroll gestures are locked to their initially dominant axis. The lock may be
200    /// released when the gesture changes direction strongly. Touch phases delimit gestures when
201    /// available, with a timeout fallback for platforms that only emit moved events.
202    ///
203    /// This also prevents input from being remapped to another axis. For example, horizontal input
204    /// will not scroll a container that only has vertical overflow enabled. Mouse wheel platforms
205    /// typically report ordinary wheel input on the Y axis and Shift-modified input on the X axis.
206    ///
207    /// ## Motivation
208    ///
209    /// On the web when scrolling with the mouse wheel, scrolling up and down will always scroll the Y axis, even when
210    /// the mouse is over a horizontally-scrollable element.
211    ///
212    /// The only way to scroll horizontally is to hold down `Shift` while scrolling, which then changes the scroll axis
213    /// to the X axis.
214    ///
215    /// Currently, GPUI operates differently from the web in that it will scroll an element in either the X or Y axis
216    /// when scrolling with just the mouse wheel. This causes problems when scrolling in a vertical list that contains
217    /// horizontally-scrollable elements, as when you get to the horizontally-scrollable elements the scroll will be
218    /// hijacked.
219    ///
220    /// Ideally we would match the web's behavior and not have a need for this, but right now we're adding this opt-in
221    /// style property to limit the potential blast radius.
222    pub restrict_scroll_to_axis: bool,
223
224    // Position properties
225    /// What should the `position` value of this struct use as a base offset?
226    pub position: Position,
227    /// How should the position of this element be tweaked relative to the layout defined?
228    #[refineable]
229    pub inset: Edges<Length>,
230
231    // Size properties
232    /// Sets the initial size of the item
233    #[refineable]
234    pub size: Size<Length>,
235    /// Controls the minimum size of the item
236    #[refineable]
237    pub min_size: Size<Length>,
238    /// Controls the maximum size of the item
239    #[refineable]
240    pub max_size: Size<Length>,
241    /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height.
242    pub aspect_ratio: Option<f32>,
243
244    // Spacing Properties
245    /// How large should the margin be on each side?
246    #[refineable]
247    pub margin: Edges<Length>,
248    /// How large should the padding be on each side?
249    #[refineable]
250    pub padding: Edges<DefiniteLength>,
251    /// How large should the border be on each side?
252    #[refineable]
253    pub border_widths: Edges<AbsoluteLength>,
254
255    // Alignment properties
256    /// How this node's children aligned in the cross/block axis?
257    pub align_items: Option<AlignItems>,
258    /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set
259    pub align_self: Option<AlignSelf>,
260    /// How should content contained within this item be aligned in the cross/block axis
261    pub align_content: Option<AlignContent>,
262    /// How should contained within this item be aligned in the main/inline axis
263    pub justify_content: Option<JustifyContent>,
264    /// How large should the gaps between items in a flex container be?
265    #[refineable]
266    pub gap: Size<DefiniteLength>,
267
268    // Flexbox properties
269    /// Which direction does the main axis flow in?
270    pub flex_direction: FlexDirection,
271    /// Should elements wrap, or stay in a single line?
272    pub flex_wrap: FlexWrap,
273    /// Sets the initial main axis size of the item
274    pub flex_basis: Length,
275    /// The relative rate at which this item grows when it is expanding to fill space, 0.0 is the default value, and this value must be positive.
276    pub flex_grow: f32,
277    /// The relative rate at which this item shrinks when it is contracting to fit into space, 1.0 is the default value, and this value must be positive.
278    pub flex_shrink: f32,
279
280    /// The fill color of this element
281    pub background: Option<Fill>,
282
283    /// The border color of this element
284    pub border_color: Option<Hsla>,
285
286    /// The border style of this element
287    pub border_style: BorderStyle,
288
289    /// The radius of the corners of this element
290    #[refineable]
291    pub corner_radii: Corners<AbsoluteLength>,
292
293    /// Whether to use continuous (squircle) corner rounding instead of circular.
294    pub continuous_corners: bool,
295
296    /// The blend mode to apply when rendering this element's background.
297    pub blend_mode: Option<BlendMode>,
298
299    /// Box shadow of the element
300    pub box_shadow: SmallVec<[BoxShadow; 1]>,
301
302    /// The text style of this element
303    pub text: TextStyleRefinement,
304
305    /// The mouse cursor style shown when the mouse pointer is over an element.
306    pub mouse_cursor: Option<CursorStyle>,
307
308    /// The opacity of this element
309    pub opacity: Option<f32>,
310
311    /// Rotation in radians (clockwise).
312    pub rotate: Option<f32>,
313
314    /// Scale factor (x, y). Defaults to (1.0, 1.0) when not set.
315    pub scale: Option<Point<f32>>,
316
317    /// Transform origin as fraction of element size (0.0-1.0). Defaults to center (0.5, 0.5).
318    pub transform_origin: Option<Point<f32>>,
319
320    /// The grid columns of this element
321    /// Equivalent to the Tailwind `grid-cols-<number>`
322    pub grid_cols: Option<GridTemplate>,
323
324    /// The row span of this element
325    /// Equivalent to the Tailwind `grid-rows-<number>`
326    pub grid_rows: Option<GridTemplate>,
327
328    /// The grid location of this element
329    pub grid_location: Option<GridLocation>,
330
331    /// Whether to draw a red debugging outline around this element
332    #[cfg(debug_assertions)]
333    pub debug: bool,
334
335    /// Whether to draw a red debugging outline around this element and all of its conforming children
336    #[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    /// The grid location of this element
348    pub fn grid_location_mut(&mut self) -> &mut GridLocation {
349        self.grid_location.get_or_insert_default()
350    }
351}
352
353/// The value of the visibility property, similar to the CSS property `visibility`
354#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
355pub enum Visibility {
356    /// The element should be drawn as normal.
357    #[default]
358    Visible,
359    /// The element should not be drawn, but should still take up space in the layout.
360    Hidden,
361}
362
363/// The possible values of the box-shadow property
364#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
365pub struct BoxShadow {
366    /// What color should the shadow have?
367    pub color: Hsla,
368    /// How should it be offset from its element?
369    pub offset: Point<Pixels>,
370    /// How much should the shadow be blurred?
371    pub blur_radius: Pixels,
372    /// How much should the shadow spread?
373    pub spread_radius: Pixels,
374    /// Should the shadow render inside the element bounds?
375    pub inset: bool,
376}
377
378impl BoxShadow {
379    /// Creates a new [`BoxShadow`] with the given offset and color, matching the order
380    /// of the CSS `box-shadow` property. Use the builder methods to set blur radius,
381    /// spread radius, and inset.
382    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    /// Sets the shadow blur radius.
393    pub fn blur_radius(mut self, blur_radius: Pixels) -> Self {
394        self.blur_radius = blur_radius;
395        self
396    }
397
398    /// Sets the shadow spread radius.
399    pub fn spread_radius(mut self, spread_radius: Pixels) -> Self {
400        self.spread_radius = spread_radius;
401        self
402    }
403
404    /// Marks the shadow as inset (drawn inside the element's bounds).
405    pub fn inset(mut self) -> Self {
406        self.inset = true;
407        self
408    }
409}
410
411/// How to handle whitespace in text
412#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
413pub enum WhiteSpace {
414    /// Normal line wrapping when text overflows the width of the element
415    #[default]
416    Normal,
417    /// No line wrapping, text will overflow the width of the element
418    Nowrap,
419}
420
421/// How to truncate text that overflows the width of the element
422#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
423pub enum TextOverflow {
424    /// Truncate the text when it doesn't fit, and represent this truncation by displaying the
425    /// provided string.
426    Truncate(SharedString),
427}
428
429/// How to align text within the element
430#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
431pub enum TextAlign {
432    /// Align the text to the left of the element
433    #[default]
434    Left,
435
436    /// Center the text within the element
437    Center,
438
439    /// Align the text to the right of the element
440    Right,
441}
442
443/// A shadow effect applied to text, rendered by painting glyphs twice.
444#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
445pub struct TextShadow {
446    /// The color of the text shadow
447    pub color: Hsla,
448    /// How far the shadow is offset from the text
449    pub offset: Point<Pixels>,
450    /// The blur radius of the shadow (approximated via opacity reduction)
451    pub blur_radius: Pixels,
452}
453
454/// The properties that can be used to style text in GPUI
455#[derive(Refineable, Clone, Debug, PartialEq)]
456#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
457pub struct TextStyle {
458    /// The color of the text
459    pub color: Hsla,
460
461    /// The font family to use
462    pub font_family: SharedString,
463
464    /// The font features to use
465    pub font_features: FontFeatures,
466
467    /// The fallback fonts to use
468    pub font_fallbacks: Option<FontFallbacks>,
469
470    /// The font size to use, in pixels or rems.
471    pub font_size: AbsoluteLength,
472
473    /// The line height to use, in pixels or fractions
474    pub line_height: DefiniteLength,
475
476    /// The font weight, e.g. bold
477    pub font_weight: FontWeight,
478
479    /// The font style, e.g. italic
480    pub font_style: FontStyle,
481
482    /// The background color of the text
483    pub background_color: Option<Hsla>,
484
485    /// The underline style of the text
486    pub underline: Option<UnderlineStyle>,
487
488    /// The strikethrough style of the text
489    pub strikethrough: Option<StrikethroughStyle>,
490
491    /// How to handle whitespace in the text
492    pub white_space: WhiteSpace,
493
494    /// The text should be truncated if it overflows the width of the element
495    pub text_overflow: Option<TextOverflow>,
496
497    /// How the text should be aligned within the element
498    pub text_align: TextAlign,
499
500    /// Additional spacing between characters (letter-spacing/tracking)
501    pub letter_spacing: Option<Pixels>,
502
503    /// The number of lines to display before truncating the text
504    pub line_clamp: Option<usize>,
505
506    /// A shadow effect rendered behind the text
507    pub text_shadow: Option<TextShadow>,
508}
509
510impl Default for TextStyle {
511    fn default() -> Self {
512        TextStyle {
513            color: black(),
514            // todo(linux) make this configurable or choose better default
515            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    /// Create a new text style with the given highlighting applied.
543    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    /// Get the font configured for this text style.
576    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    /// Returns the rounded line height in pixels.
587    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    /// Convert this text style into a [`TextRun`], for the given length of the text.
592    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/// A highlight style to apply, similar to a `TextStyle` except
611/// for a single font, uniformly sized and spaced text.
612#[derive(Copy, Clone, Debug, Default, PartialEq)]
613pub struct HighlightStyle {
614    /// The color of the text
615    pub color: Option<Hsla>,
616
617    /// The font weight, e.g. bold
618    pub font_weight: Option<FontWeight>,
619
620    /// The font style, e.g. italic
621    pub font_style: Option<FontStyle>,
622
623    /// The background color of the text
624    pub background_color: Option<Hsla>,
625
626    /// The underline style of the text
627    pub underline: Option<UnderlineStyle>,
628
629    /// The underline style of the text
630    pub strikethrough: Option<StrikethroughStyle>,
631
632    /// Similar to the CSS `opacity` property, this will cause the text to be less vibrant.
633    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    /// Returns true if the style is visible and the background is opaque.
654    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    /// Get the text style in this element style.
661    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    /// Get the content mask for this element style, based on the given bounds.
670    /// If the element does not hide its overflow, this will return `None`.
671    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                    // x and y both visible
700                    (true, true) => return None,
701                    // x visible, y hidden
702                    (true, false) => Bounds::from_corners(
703                        point(min.x, bounds.origin.y),
704                        point(max.x, bounds.bottom_right().y),
705                    ),
706                    // x hidden, y visible
707                    (false, true) => Bounds::from_corners(
708                        point(bounds.origin.x, min.y),
709                        point(bounds.bottom_right().x, max.y),
710                    ),
711                    // both hidden
712                    (false, false) => Bounds::from_corners(min, max),
713                };
714
715                Some(ContentMask { bounds })
716            }
717        }
718    }
719
720    /// Paints the background of an element styled with this style.
721    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            // Alignment
868            align_items: None,
869            align_self: None,
870            align_content: None,
871            justify_content: None,
872            // Flexbox
873            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/// The properties that can be applied to an underline.
904#[derive(
905    Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema,
906)]
907pub struct UnderlineStyle {
908    /// The thickness of the underline.
909    pub thickness: Pixels,
910
911    /// The color of the underline.
912    pub color: Option<Hsla>,
913
914    /// Whether the underline should be wavy, like in a spell checker.
915    pub wavy: bool,
916}
917
918/// The properties that can be applied to a strikethrough.
919#[derive(
920    Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema,
921)]
922pub struct StrikethroughStyle {
923    /// The thickness of the strikethrough.
924    pub thickness: Pixels,
925
926    /// The color of the strikethrough.
927    pub color: Option<Hsla>,
928}
929
930/// The kinds of fill that can be applied to a shape.
931#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
932pub enum Fill {
933    /// A solid color fill.
934    Color(Background),
935}
936
937impl Fill {
938    /// Unwrap this fill into a solid color, if it is one.
939    ///
940    /// If the fill is not a solid color, this method returns `None`.
941    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    /// Create a highlight style with just a color
994    pub fn color(color: Hsla) -> Self {
995        Self {
996            color: Some(color),
997            ..Default::default()
998        }
999    }
1000    /// Blend this highlight style with another.
1001    /// Non-continuous properties, like font_weight and font_style, are overwritten.
1002    #[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
1068/// Combine and merge the highlights and ranges in the two iterators.
1069pub 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/// Used to control how child nodes are aligned.
1112/// For Flexbox it controls alignment in the cross axis
1113/// For Grid it controls alignment in the block axis
1114///
1115/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items)
1116#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
1117// Copy of taffy::style type of the same name, to derive JsonSchema.
1118pub enum AlignItems {
1119    /// Items are packed toward the start of the axis
1120    Start,
1121    /// Items are packed toward the end of the axis
1122    End,
1123    /// Items are packed towards the flex-relative start of the axis.
1124    ///
1125    /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
1126    /// to End. In all other cases it is equivalent to Start.
1127    FlexStart,
1128    /// Items are packed towards the flex-relative end of the axis.
1129    ///
1130    /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
1131    /// to Start. In all other cases it is equivalent to End.
1132    FlexEnd,
1133    /// Items are packed along the center of the cross axis
1134    Center,
1135    /// Items are aligned such as their baselines align
1136    Baseline,
1137    /// Stretch to fill the container
1138    Stretch,
1139}
1140/// Used to control how child nodes are aligned.
1141/// Does not apply to Flexbox, and will be ignored if specified on a flex container
1142/// For Grid it controls alignment in the inline axis
1143///
1144/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-items)
1145pub type JustifyItems = AlignItems;
1146/// Used to control how the specified nodes is aligned.
1147/// Overrides the parent Node's `AlignItems` property.
1148/// For Flexbox it controls alignment in the cross axis
1149/// For Grid it controls alignment in the block axis
1150///
1151/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-self)
1152pub type AlignSelf = AlignItems;
1153/// Used to control how the specified nodes is aligned.
1154/// Overrides the parent Node's `JustifyItems` property.
1155/// Does not apply to Flexbox, and will be ignored if specified on a flex child
1156/// For Grid it controls alignment in the inline axis
1157///
1158/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-self)
1159pub type JustifySelf = AlignItems;
1160
1161/// Sets the distribution of space between and around content items
1162/// For Flexbox it controls alignment in the cross axis
1163/// For Grid it controls alignment in the block axis
1164///
1165/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-content)
1166#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
1167// Copy of taffy::style type of the same name, to derive JsonSchema.
1168pub enum AlignContent {
1169    /// Items are packed toward the start of the axis
1170    Start,
1171    /// Items are packed toward the end of the axis
1172    End,
1173    /// Items are packed towards the flex-relative start of the axis.
1174    ///
1175    /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
1176    /// to End. In all other cases it is equivalent to Start.
1177    FlexStart,
1178    /// Items are packed towards the flex-relative end of the axis.
1179    ///
1180    /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
1181    /// to Start. In all other cases it is equivalent to End.
1182    FlexEnd,
1183    /// Items are centered around the middle of the axis
1184    Center,
1185    /// Items are stretched to fill the container
1186    Stretch,
1187    /// The first and last items are aligned flush with the edges of the container (no gap)
1188    /// The gap between items is distributed evenly.
1189    SpaceBetween,
1190    /// The gap between the first and last items is exactly THE SAME as the gap between items.
1191    /// The gaps are distributed evenly
1192    SpaceEvenly,
1193    /// The gap between the first and last items is exactly HALF the gap between items.
1194    /// The gaps are distributed evenly in proportion to these ratios.
1195    SpaceAround,
1196}
1197
1198/// Sets the distribution of space between and around content items
1199/// For Flexbox it controls alignment in the main axis
1200/// For Grid it controls alignment in the inline axis
1201///
1202/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content)
1203pub type JustifyContent = AlignContent;
1204
1205/// Sets the layout used for the children of this node
1206///
1207/// The default values depends on on which feature flags are enabled. The order of precedence is: Flex, Grid, Block, None.
1208#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1209// Copy of taffy::style type of the same name, to derive JsonSchema.
1210pub enum Display {
1211    /// The children will follow the block layout algorithm
1212    Block,
1213    /// The children will follow the flexbox layout algorithm
1214    #[default]
1215    Flex,
1216    /// The children will follow the CSS Grid layout algorithm
1217    Grid,
1218    /// The children will not be laid out, and will follow absolute positioning
1219    None,
1220}
1221
1222/// Controls whether flex items are forced onto one line or can wrap onto multiple lines.
1223///
1224/// Defaults to [`FlexWrap::NoWrap`]
1225///
1226/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-wrap-property)
1227#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1228// Copy of taffy::style type of the same name, to derive JsonSchema.
1229pub enum FlexWrap {
1230    /// Items will not wrap and stay on a single line
1231    #[default]
1232    NoWrap,
1233    /// Items will wrap according to this item's [`FlexDirection`]
1234    Wrap,
1235    /// Items will wrap in the opposite direction to this item's [`FlexDirection`]
1236    WrapReverse,
1237}
1238
1239/// The direction of the flexbox layout main axis.
1240///
1241/// There are always two perpendicular layout axes: main (or primary) and cross (or secondary).
1242/// Adding items will cause them to be positioned adjacent to each other along the main axis.
1243/// By varying this value throughout your tree, you can create complex axis-aligned layouts.
1244///
1245/// Items are always aligned relative to the cross axis, and justified relative to the main axis.
1246///
1247/// The default behavior is [`FlexDirection::Row`].
1248///
1249/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-direction-property)
1250#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1251// Copy of taffy::style type of the same name, to derive JsonSchema.
1252pub enum FlexDirection {
1253    /// Defines +x as the main axis
1254    ///
1255    /// Items will be added from left to right in a row.
1256    #[default]
1257    Row,
1258    /// Defines +y as the main axis
1259    ///
1260    /// Items will be added from top to bottom in a column.
1261    Column,
1262    /// Defines -x as the main axis
1263    ///
1264    /// Items will be added from right to left in a row.
1265    RowReverse,
1266    /// Defines -y as the main axis
1267    ///
1268    /// Items will be added from bottom to top in a column.
1269    ColumnReverse,
1270}
1271
1272/// How children overflowing their container should affect layout
1273///
1274/// In CSS the primary effect of this property is to control whether contents of a parent container that overflow that container should
1275/// be displayed anyway, be clipped, or trigger the container to become a scroll container. However it also has secondary effects on layout,
1276/// the main ones being:
1277///
1278///   - The automatic minimum size Flexbox/CSS Grid items with non-`Visible` overflow is `0` rather than being content based
1279///   - `Overflow::Scroll` nodes have space in the layout reserved for a scrollbar (width controlled by the `scrollbar_width` property)
1280///
1281/// In Taffy, we only implement the layout related secondary effects as we are not concerned with drawing/painting. The amount of space reserved for
1282/// a scrollbar is controlled by the `scrollbar_width` property. If this is `0` then `Scroll` behaves identically to `Hidden`.
1283///
1284/// <https://developer.mozilla.org/en-US/docs/Web/CSS/overflow>
1285#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1286// Copy of taffy::style type of the same name, to derive JsonSchema.
1287pub enum Overflow {
1288    /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content.
1289    /// Content that overflows this node *should* contribute to the scroll region of its parent.
1290    #[default]
1291    Visible,
1292    /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content.
1293    /// Content that overflows this node should *not* contribute to the scroll region of its parent.
1294    Clip,
1295    /// The automatic minimum size of this node as a flexbox/grid item should be `0`.
1296    /// Content that overflows this node should *not* contribute to the scroll region of its parent.
1297    Hidden,
1298    /// The automatic minimum size of this node as a flexbox/grid item should be `0`. Additionally, space should be reserved
1299    /// for a scrollbar. The amount of space reserved is controlled by the `scrollbar_width` property.
1300    /// Content that overflows this node should *not* contribute to the scroll region of its parent.
1301    Scroll,
1302}
1303
1304/// The positioning strategy for this item.
1305///
1306/// This controls both how the origin is determined for the [`Style::position`] field,
1307/// and whether or not the item will be controlled by flexbox's layout algorithm.
1308///
1309/// WARNING: this enum follows the behavior of [CSS's `position` property](https://developer.mozilla.org/en-US/docs/Web/CSS/position),
1310/// which can be unintuitive.
1311///
1312/// [`Position::Relative`] is the default value, in contrast to the default behavior in CSS.
1313#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1314// Copy of taffy::style type of the same name, to derive JsonSchema.
1315pub enum Position {
1316    /// The offset is computed relative to the final position given by the layout algorithm.
1317    /// Offsets do not affect the position of any other items; they are effectively a correction factor applied at the end.
1318    #[default]
1319    Relative,
1320    /// The offset is computed relative to this item's closest positioned ancestor, if any.
1321    /// Otherwise, it is placed relative to the origin.
1322    /// No space is created for the item in the page layout, and its size will not be altered.
1323    ///
1324    /// WARNING: to opt-out of layouting entirely, you must use [`Display::None`] instead on your [`Style`] object.
1325    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            // TODO this does not seem right
1500            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}