Skip to main content

gpui/
style.rs

1use crate::{
2    AbsoluteLength, App, Background, BorderStyle, Bounds, ColorExt, ContentMask, Corners,
3    CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement, Font,
4    FontFallbacks, FontFeatures, FontStyle, FontWeight, FontWidth, GridLocation, Length, PaintQuad,
5    Pixels, Point, PointRefinement, ScaledPixels, SharedString, Size, SizeRefinement, Styled,
6    TextRun, Window, black, phi, point, px, rems, size,
7};
8use collections::HashSet;
9use palette::{Hsla, IntoColor, rgb::Rgba};
10use refineable::Refineable;
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use std::{iter, mem, ops::Range};
14
15/// Lets a custom element read the `debug_below` setting in debug builds.
16///
17/// When an ancestor enables that setting, GPUI installs this type as a global.
18#[cfg(debug_assertions)]
19pub struct DebugBelow;
20
21#[cfg(debug_assertions)]
22impl crate::Global for DebugBelow {}
23
24/// How an image is sized and positioned inside an element's bounds.
25pub enum ObjectFit {
26    /// Stretches the image to fill both dimensions, ignoring its aspect ratio.
27    Fill,
28    /// Scales the image to fit inside the bounds, preserving its aspect ratio.
29    Contain,
30    /// Scales the image to cover the bounds, preserving its aspect ratio and cropping overflow.
31    Cover,
32    /// Keeps the image's size when it fits; otherwise scales it down like [`ObjectFit::Contain`].
33    ScaleDown,
34    /// Keeps the image's original size and places it at the bounds' origin.
35    None,
36}
37
38impl ObjectFit {
39    /// Returns the image rectangle produced by this fit mode.
40    pub fn get_bounds(
41        &self,
42        bounds: Bounds<Pixels>,
43        image_size: Size<DevicePixels>,
44    ) -> Bounds<Pixels> {
45        let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension)));
46        let image_ratio = image_size.width / image_size.height;
47        let bounds_ratio = bounds.size.width / bounds.size.height;
48
49        match self {
50            ObjectFit::Fill => bounds,
51            ObjectFit::Contain => {
52                let new_size = if bounds_ratio > image_ratio {
53                    size(
54                        image_size.width * (bounds.size.height / image_size.height),
55                        bounds.size.height,
56                    )
57                } else {
58                    size(
59                        bounds.size.width,
60                        image_size.height * (bounds.size.width / image_size.width),
61                    )
62                };
63
64                Bounds {
65                    origin: point(
66                        bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
67                        bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
68                    ),
69                    size: new_size,
70                }
71            }
72            ObjectFit::ScaleDown => {
73                // Check if the image is larger than the bounds in either dimension.
74                if image_size.width > bounds.size.width || image_size.height > bounds.size.height {
75                    // If the image is larger, use the same logic as Contain to scale it down.
76                    let new_size = if bounds_ratio > image_ratio {
77                        size(
78                            image_size.width * (bounds.size.height / image_size.height),
79                            bounds.size.height,
80                        )
81                    } else {
82                        size(
83                            bounds.size.width,
84                            image_size.height * (bounds.size.width / image_size.width),
85                        )
86                    };
87
88                    Bounds {
89                        origin: point(
90                            bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
91                            bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
92                        ),
93                        size: new_size,
94                    }
95                } else {
96                    // If the image is smaller than or equal to the container, display it at its original size,
97                    // centered within the container.
98                    let original_size = size(image_size.width, image_size.height);
99                    Bounds {
100                        origin: point(
101                            bounds.origin.x + (bounds.size.width - original_size.width) / 2.0,
102                            bounds.origin.y + (bounds.size.height - original_size.height) / 2.0,
103                        ),
104                        size: original_size,
105                    }
106                }
107            }
108            ObjectFit::Cover => {
109                let new_size = if bounds_ratio > image_ratio {
110                    size(
111                        bounds.size.width,
112                        image_size.height * (bounds.size.width / image_size.width),
113                    )
114                } else {
115                    size(
116                        image_size.width * (bounds.size.height / image_size.height),
117                        bounds.size.height,
118                    )
119                };
120
121                Bounds {
122                    origin: point(
123                        bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
124                        bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
125                    ),
126                    size: new_size,
127                }
128            }
129            ObjectFit::None => Bounds {
130                origin: bounds.origin,
131                size: image_size,
132            },
133        }
134    }
135}
136
137/// The minimum size of a grid column or row.
138#[derive(
139    Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default, JsonSchema, Serialize, Deserialize,
140)]
141pub enum GridTemplateMinSize {
142    /// The column or row size may be 0
143    #[default]
144    Zero,
145    /// The column or row size can be determined by the min content
146    MinContent,
147    /// The column or row size can be determined by the max content
148    MaxContent,
149}
150
151/// The portion of a grid template that describes track sizes.
152#[derive(
153    Copy,
154    Clone,
155    Refineable,
156    PartialEq,
157    Eq,
158    PartialOrd,
159    Ord,
160    Debug,
161    Default,
162    JsonSchema,
163    Serialize,
164    Deserialize,
165)]
166pub struct GridTemplate {
167    /// How this template directive should be repeated
168    pub repeat: u16,
169    /// The minimum size in the repeat(<>, minmax(_, 1fr)) equation
170    pub min_size: GridTemplateMinSize,
171}
172
173/// A ring's color or gradient.
174#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
175pub enum RingColor {
176    /// Use the element's effective text color, matching CSS `currentColor`.
177    #[default]
178    CurrentColor,
179    /// Use an explicit color or gradient.
180    Color(Background),
181}
182
183impl RingColor {
184    fn resolve(self, current_color: Hsla) -> Background {
185        match self {
186            Self::CurrentColor => current_color.into(),
187            Self::Color(color) => color,
188        }
189    }
190}
191
192/// A ring around or inside an element.
193#[derive(Refineable, Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
194#[refineable(Debug, PartialEq, Serialize, Deserialize)]
195pub struct RingStyle {
196    /// The width of the ring. A zero width disables it.
197    pub width: Pixels,
198    /// The ring color or gradient.
199    pub color: RingColor,
200}
201
202impl RingStyle {
203    fn outer_quad(
204        self,
205        bounds: Bounds<Pixels>,
206        corner_radii: Corners<Pixels>,
207        current_color: Hsla,
208    ) -> Option<PaintQuad> {
209        (self.width > Pixels::ZERO).then(|| {
210            // Quad borders are painted inward, so expanding the quad by the ring width leaves
211            // its transparent interior aligned with the element's original bounds.
212            PaintQuad::new(bounds.dilate(self.width))
213                .corner_radii(corner_radii.map(|radius| *radius + self.width))
214                .border_widths(Edges::all(self.width))
215                .border_color(self.color.resolve(current_color))
216        })
217    }
218
219    fn inset_shadow(self, current_color: Hsla) -> Option<BoxShadow> {
220        (self.width > Pixels::ZERO).then(|| {
221            BoxShadow::new(px(0.), px(0.), self.color.resolve(current_color))
222                .spread_radius(self.width)
223                .inset()
224        })
225    }
226}
227
228/// The resolved styling of an element, which GPUI uses to lay out and paint it.
229///
230/// You rarely build a `Style` yourself. The methods of [`Styled`] write into a
231/// [`StyleRefinement`], and GPUI applies that refinement over `Style::default()` when it
232/// draws the element. A default `Style` uses [`Display::Block`] and is visible.
233///
234/// # Examples
235///
236/// ```
237/// use gpui::{Display, Refineable, Style, StyleRefinement};
238///
239/// let mut style = Style::default();
240/// assert_eq!(style.display, Display::Block);
241///
242/// let mut refinement = StyleRefinement::default();
243/// refinement.display = Some(Display::Flex);
244/// style.refine(&refinement);
245/// assert_eq!(style.display, Display::Flex);
246/// ```
247#[derive(Clone, Refineable, Debug)]
248#[refineable(Debug, PartialEq, Serialize, Deserialize)]
249pub struct Style {
250    /// What layout strategy should be used?
251    pub display: Display,
252
253    /// Should the element be painted on screen?
254    pub visibility: Visibility,
255
256    /// The inline direction of this element and its descendants.
257    pub direction: LayoutDirection,
258
259    /// How this element participates in Unicode bidirectional text formatting.
260    pub unicode_bidi: UnicodeBidi,
261
262    /// Whether `unicode_bidi` was set explicitly rather than left at its initial value.
263    #[doc(hidden)]
264    pub unicode_bidi_explicit: bool,
265
266    // Overflow properties
267    /// How children overflowing their container should affect layout
268    #[refineable]
269    pub overflow: Point<Overflow>,
270    /// Edge fade distances for overflow masking.
271    #[refineable]
272    pub overflow_fade: Edges<AbsoluteLength>,
273    /// How much space (in logical pixels or rems) should be reserved for the scrollbars of `Overflow::Scroll` nodes.
274    pub scrollbar_width: AbsoluteLength,
275    /// Whether both x and y axis should be scrollable at the same time.
276    pub allow_concurrent_scroll: bool,
277    /// Whether scrolling should be restricted to the input gesture's axis.
278    ///
279    /// Pixel-based scroll gestures are locked to their initially dominant axis. The lock may be
280    /// released when the gesture changes direction strongly. Touch phases delimit gestures when
281    /// available, with a timeout fallback for platforms that only emit moved events.
282    ///
283    /// This also prevents input from being remapped to another axis. For example, horizontal input
284    /// will not scroll a container that only has vertical overflow enabled. Mouse wheel platforms
285    /// typically report ordinary wheel input on the Y axis and Shift-modified input on the X axis.
286    ///
287    /// ## Motivation
288    ///
289    /// On the web, a vertical mouse wheel scrolls the Y axis even over a horizontally scrollable
290    /// element.
291    ///
292    /// The only way to scroll horizontally is to hold down `Shift` while scrolling, which then changes the scroll axis
293    /// to the X axis.
294    ///
295    /// GPUI can otherwise choose either axis for mouse-wheel input. In a vertical list with
296    /// horizontally scrollable children, that can send the list's scroll gesture to a child.
297    ///
298    /// Set this property to keep each gesture on its initial axis.
299    pub restrict_scroll_to_axis: bool,
300
301    // Position properties
302    /// What should the `position` value of this struct use as a base offset?
303    pub position: Position,
304    /// How should the position of this element be tweaked relative to the layout defined?
305    #[refineable]
306    pub inset: Edges<Length>,
307
308    // Size properties
309    /// Sets the initial size of the item
310    #[refineable]
311    pub size: Size<Length>,
312    /// Controls the minimum size of the item
313    #[refineable]
314    pub min_size: Size<Length>,
315    /// Controls the maximum size of the item
316    #[refineable]
317    pub max_size: Size<Length>,
318    /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height.
319    pub aspect_ratio: Option<f32>,
320
321    // Spacing Properties
322    /// How large should the margin be on each side?
323    #[refineable]
324    pub margin: Edges<Length>,
325    /// How large should the padding be on each side?
326    #[refineable]
327    pub padding: Edges<DefiniteLength>,
328    /// How large should the border be on each side?
329    #[refineable]
330    pub border_widths: Edges<AbsoluteLength>,
331
332    // Alignment properties
333    /// How this node's children aligned in the cross/block axis?
334    pub align_items: Option<AlignItems>,
335    /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set
336    pub align_self: Option<AlignSelf>,
337    /// How this element aligns vertically when it participates in an inline formatting context.
338    pub vertical_align: VerticalAlign,
339    /// How should content contained within this item be aligned in the cross/block axis
340    pub align_content: Option<AlignContent>,
341    /// How should contained within this item be aligned in the main/inline axis
342    pub justify_content: Option<JustifyContent>,
343    /// How large should the gaps between items in a flex container be?
344    #[refineable]
345    pub gap: Size<DefiniteLength>,
346
347    // Flexbox properties
348    /// Which direction does the main axis flow in?
349    pub flex_direction: FlexDirection,
350    /// Should elements wrap, or stay in a single line?
351    pub flex_wrap: FlexWrap,
352    /// Sets the initial main axis size of the item
353    pub flex_basis: Length,
354    /// 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.
355    pub flex_grow: f32,
356    /// 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.
357    pub flex_shrink: f32,
358
359    /// The fill color of this element
360    pub background: Option<Fill>,
361
362    /// The background painted into the border of this element
363    pub border_color: Option<Background>,
364
365    /// The border style of this element
366    pub border_style: BorderStyle,
367
368    /// The length of each border dash, as a multiple of the border width.
369    pub border_dashed_length: f32,
370
371    /// The gap between border dashes, as a multiple of the border width.
372    pub border_dashed_gap: f32,
373
374    /// The radius of the corners of this element
375    #[refineable]
376    pub corner_radii: Corners<AbsoluteLength>,
377
378    /// Controls the transition from each rounded corner to the straight edges.
379    /// `0.0` produces circular corners. `1.0` applies maximum smoothing.
380    pub corner_smoothing: Option<f32>,
381
382    /// Box shadow of the element
383    pub box_shadow: Vec<BoxShadow>,
384
385    /// A solid ring painted outside the element.
386    #[refineable]
387    pub ring: RingStyle,
388
389    /// A solid ring painted inside the element.
390    #[refineable]
391    pub inset_ring: RingStyle,
392
393    /// Filters applied to this element's own content and children (CSS `filter`).
394    pub filter: Vec<Filter>,
395
396    /// Filters applied to the content rendered behind this element (CSS `backdrop-filter`).
397    pub backdrop_filter: Vec<Filter>,
398
399    /// The text style of this element
400    #[refineable]
401    pub text: TextStyleRefinement,
402
403    /// The mouse cursor style shown when the mouse pointer is over an element.
404    pub mouse_cursor: Option<CursorStyle>,
405
406    /// Opacity applied to this element and its children after compositing them as a group.
407    pub opacity: Option<f32>,
408
409    /// The grid columns of this element
410    /// Roughly equivalent to the Tailwind `grid-cols-<number>`
411    pub grid_cols: Option<GridTemplate>,
412
413    /// The row span of this element
414    /// Equivalent to the Tailwind `grid-rows-<number>`
415    pub grid_rows: Option<GridTemplate>,
416
417    /// The grid location of this element
418    pub grid_location: Option<GridLocation>,
419
420    /// Whether to draw a red debugging outline around this element
421    #[cfg(debug_assertions)]
422    pub debug: bool,
423
424    /// Whether to draw a red debugging outline around this element and all of its conforming children
425    #[cfg(debug_assertions)]
426    pub debug_below: bool,
427}
428
429impl Styled for StyleRefinement {
430    fn style(&mut self) -> &mut StyleRefinement {
431        self
432    }
433}
434
435impl StyleRefinement {
436    /// The grid location of this element
437    pub fn grid_location_mut(&mut self) -> &mut GridLocation {
438        self.grid_location.get_or_insert_default()
439    }
440}
441
442/// Whether an element is painted while it remains in layout.
443#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
444pub enum Visibility {
445    /// The element should be drawn as normal.
446    #[default]
447    Visible,
448    /// The element should not be drawn, but should still take up space in the layout.
449    Hidden,
450}
451
452/// The inline direction established by an element.
453#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize, JsonSchema)]
454pub enum LayoutDirection {
455    /// Use the resolved direction of the logical parent.
456    #[default]
457    Inherit,
458    /// Establish left-to-right directionality.
459    LeftToRight,
460    /// Establish right-to-left directionality.
461    RightToLeft,
462    /// Determine direction from eligible source text.
463    Auto,
464}
465
466/// An element direction after inheritance and automatic detection have been resolved.
467#[doc(hidden)]
468#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
469pub enum ResolvedDirection {
470    /// Left-to-right inline direction.
471    #[default]
472    LeftToRight,
473    /// Right-to-left inline direction.
474    RightToLeft,
475}
476
477impl ResolvedDirection {
478    /// Returns whether this direction is right-to-left.
479    pub fn is_rtl(self) -> bool {
480        self == Self::RightToLeft
481    }
482
483    pub(crate) fn from_first_strong(text: &str) -> Option<Self> {
484        use unicode_bidi::BidiClass;
485
486        text.chars()
487            .find_map(|character| match unicode_bidi::bidi_class(character) {
488                BidiClass::L => Some(Self::LeftToRight),
489                BidiClass::R | BidiClass::AL => Some(Self::RightToLeft),
490                _ => None,
491            })
492    }
493}
494
495/// Controls the Unicode bidirectional scope established by an element.
496#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize, JsonSchema)]
497pub enum UnicodeBidi {
498    /// Apply ordinary bidirectional processing without another scope.
499    #[default]
500    Normal,
501    /// Establish a directional embedding.
502    Embed,
503    /// Isolate the element's inline content from surrounding content.
504    Isolate,
505    /// Override the ordering of inline content with the element direction.
506    BidiOverride,
507    /// Isolate the content and override its ordering.
508    IsolateOverride,
509    /// Determine each paragraph's direction from its own content.
510    Plaintext,
511}
512
513/// The values accepted by an element's box-shadow style.
514#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, derive_setters::Setters)]
515#[setters(generate = false)]
516pub struct BoxShadow {
517    /// The shadow color or gradient.
518    pub color: Background,
519    /// How should it be offset from its element?
520    pub offset: Point<Pixels>,
521    /// How much should the shadow be blurred?
522    #[setters(generate)]
523    pub blur_radius: Pixels,
524    /// How much should the shadow spread?
525    #[setters(generate)]
526    pub spread_radius: Pixels,
527    /// Whether this is an inset shadow (drawn inside the element's bounds).
528    #[setters(generate, bool)]
529    pub inset: bool,
530}
531
532impl BoxShadow {
533    /// Creates a new [`BoxShadow`] with the given offset and color. The blur and spread
534    /// radii are 0 and the shadow is outset. Use the builder methods to change them, or to
535    /// make the shadow inset.
536    pub fn new(offset_x: Pixels, offset_y: Pixels, color: impl Into<Background>) -> Self {
537        Self {
538            color: color.into(),
539            offset: point(offset_x, offset_y),
540            blur_radius: px(0.),
541            spread_radius: px(0.),
542            inset: false,
543        }
544    }
545}
546
547/// A graphical filter that can be applied either to an element's own content
548/// (via [`Styled::filter`], like CSS `filter`) or to the content rendered behind
549/// it (via [`Styled::backdrop_filter`], like CSS `backdrop-filter`).
550#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
551pub enum Filter {
552    /// A gaussian blur with the given radius, in logical pixels. Maps to CSS `blur(<px>)`.
553    Blur(Pixels),
554}
555
556impl Filter {
557    /// Whether this filter has no visible effect, so painting can skip it entirely (and the
558    /// element can avoid the offscreen isolation pass when *all* of its filters are identities).
559    ///
560    /// Each variant defines its own no-op case. A filter that returns `true` is dropped before it
561    /// reaches the renderer.
562    pub fn is_identity(&self) -> bool {
563        match self {
564            Filter::Blur(radius) => *radius <= Pixels::ZERO,
565        }
566    }
567
568    /// Lower this logical-pixel filter into its scene-space ([`ScaledFilter`]) form for the
569    /// renderer, scaling any pixel magnitudes by `factor` (the window scale factor).
570    pub fn scale(&self, factor: f32) -> ScaledFilter {
571        match self {
572            Filter::Blur(radius) => ScaledFilter::Blur(radius.scale(factor)),
573        }
574    }
575}
576
577/// The scene-space (device-pixel) form of a [`Filter`], carried on the scene primitives that the
578/// renderers consume. Produced by [`Filter::scale`]; pixel magnitudes are in [`ScaledPixels`].
579///
580/// The scene stores filters separately so their magnitudes stay in device pixels alongside the
581/// other scene primitives. Matches are exhaustive: adding a filter variant requires handling it
582/// in the scene conversion and renderer.
583#[derive(Clone, Copy, Debug, PartialEq)]
584pub enum ScaledFilter {
585    /// A gaussian blur with the given radius, in scaled (device) pixels.
586    Blur(ScaledPixels),
587}
588
589/// How text handles whitespace and line wrapping.
590#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
591pub enum WhiteSpace {
592    /// Normal line wrapping when text overflows the width of the element
593    #[default]
594    Normal,
595    /// No line wrapping, text will overflow the width of the element
596    Nowrap,
597}
598
599/// How text is truncated when it exceeds the element's width.
600#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
601pub enum TextOverflow {
602    /// Truncate the text at the end when it doesn't fit, and represent this truncation by
603    /// displaying the provided string (e.g., "very long te…").
604    Truncate(SharedString),
605    /// Truncate the text at the start when it doesn't fit, and represent this truncation by
606    /// displaying the provided string at the beginning (e.g., "…ong text here").
607    /// Typically more adequate for file paths where the end is more important than the beginning.
608    TruncateStart(SharedString),
609    /// Truncate the text in the middle when it doesn't fit, preserving both the start and end
610    /// of the string (e.g., "long fi…name.rs"). Useful for filenames where both the prefix
611    /// and the extension are important context.
612    TruncateMiddle(SharedString),
613}
614
615/// How text is aligned within the element.
616#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
617pub enum TextAlign {
618    /// Align text to the start edge for the line's direction.
619    #[default]
620    Start,
621
622    /// Align text to the end edge for the line's direction.
623    End,
624
625    /// Align the text to the physical left edge of the element.
626    Left,
627
628    /// Center the text within the element
629    Center,
630
631    /// Align the text to the right of the element
632    Right,
633}
634
635/// Case mapping applied at layout time while keeping **UTF-8 byte lengths** unchanged.
636///
637/// [`TextTransform::Uppercase`] and [`TextTransform::Lowercase`] use Unicode case mapping via
638/// [`char::to_uppercase`] / [`char::to_lowercase`]. If mapping a code point would change its UTF-8
639/// length, or would replace one scalar value with more than one character, the **original**
640/// character is kept. That keeps indices into the underlying buffer aligned with hit-testing and
641/// editor-style caret positions that use raw byte offsets.
642///
643/// [`TextTransform::Capitalize`] matches CSS / Tailwind [`capitalize`][tw-cap] using Unicode word
644/// boundaries: only the first alphabetic code point in each word is uppercased; other characters
645/// are unchanged.
646///
647/// [tw-cap]: https://tailwindcss.com/docs/text-transform
648#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
649pub enum TextTransform {
650    /// Do not transform text.
651    #[default]
652    None,
653    /// Uppercase text (Unicode, byte-length preserving — see [`TextTransform`]).
654    Uppercase,
655    /// Lowercase text (Unicode, byte-length preserving — see [`TextTransform`]).
656    Lowercase,
657    /// `text-transform: capitalize` semantics (Tailwind class `capitalize`): per Unicode word, only
658    /// the first alphabetic character is mapped to uppercase; the rest of the string is unchanged.
659    Capitalize,
660}
661
662/// The text properties of an element: font, size, color and spacing.
663///
664/// Text properties inherit into child elements, so a value set on a container applies to
665/// its text unless a child sets its own. Set them with the methods of [`Styled`], such as
666/// [`Styled::text_color`] and [`Styled::text_size`], or refine a `TextStyle` directly.
667///
668/// The default text style uses black text, the `.SystemUIFont` family, a font size of
669/// `1rem` (16px at the default rem size), and a line height of the golden ratio times the
670/// font size.
671///
672/// # Examples
673///
674/// ```
675/// use gpui::{AbsoluteLength, Refineable, TextStyle, TextStyleRefinement, hsla, rems};
676///
677/// let mut style = TextStyle::default();
678/// assert_eq!(style.font_size, AbsoluteLength::from(rems(1.)));
679///
680/// let mut refinement = TextStyleRefinement::default();
681/// refinement.color = Some(hsla(0., 1., 0.5, 1.));
682/// refinement.font_size = Some(AbsoluteLength::from(rems(1.5)));
683/// style.refine(&refinement);
684/// assert_eq!(style.color, hsla(0., 1., 0.5, 1.));
685/// ```
686#[derive(Refineable, Clone, Debug, PartialEq)]
687#[refineable(Debug, PartialEq, Serialize, Deserialize)]
688pub struct TextStyle {
689    /// The color of the text
690    pub color: Hsla,
691
692    /// The font family to use
693    pub font_family: SharedString,
694
695    /// The font features to use
696    pub font_features: FontFeatures,
697
698    /// The fallback fonts to use
699    pub font_fallbacks: Option<FontFallbacks>,
700
701    /// The font size to use, in pixels or rems.
702    pub font_size: AbsoluteLength,
703
704    /// The line height to use, in pixels or fractions
705    pub line_height: DefiniteLength,
706
707    /// The font weight, e.g. bold
708    pub font_weight: FontWeight,
709
710    /// The font width as a percentage of normal width.
711    pub font_width: FontWidth,
712
713    /// The font style, e.g. italic
714    pub font_style: FontStyle,
715
716    /// The background color of the text
717    pub background_color: Option<Hsla>,
718
719    /// The underline style of the text
720    pub underline: Option<UnderlineStyle>,
721
722    /// The strikethrough style of the text
723    pub strikethrough: Option<StrikethroughStyle>,
724
725    /// How to handle whitespace in the text
726    pub white_space: WhiteSpace,
727
728    /// The text should be truncated if it overflows the width of the element
729    pub text_overflow: Option<TextOverflow>,
730
731    /// How the text should be aligned within the element
732    pub text_align: TextAlign,
733
734    /// The number of lines to display before truncating the text
735    pub line_clamp: Option<usize>,
736
737    /// Letter spacing added between characters, in pixels (positive widens, negative tightens).
738    ///
739    /// The platform text stack may clamp values outside the range it supports.
740    pub letter_spacing: Option<Pixels>,
741
742    /// Case transformation applied at layout time.
743    pub text_transform: Option<TextTransform>,
744}
745
746impl Default for TextStyle {
747    fn default() -> Self {
748        TextStyle {
749            color: black(),
750            // todo(linux) make this configurable or choose better default
751            font_family: ".SystemUIFont".into(),
752            font_features: FontFeatures::default(),
753            font_fallbacks: None,
754            font_size: rems(1.).into(),
755            line_height: phi().into(),
756            font_weight: FontWeight::default(),
757            font_width: FontWidth::default(),
758            font_style: FontStyle::default(),
759            background_color: None,
760            underline: None,
761            strikethrough: None,
762            white_space: WhiteSpace::Normal,
763            text_overflow: None,
764            text_align: TextAlign::default(),
765            line_clamp: None,
766            letter_spacing: None,
767            text_transform: None,
768        }
769    }
770}
771
772impl TextStyle {
773    /// Create a new text style with the given highlighting applied.
774    pub fn highlight(mut self, style: impl Into<HighlightStyle>) -> Self {
775        let style = style.into();
776        if let Some(weight) = style.font_weight {
777            self.font_weight = weight;
778        }
779
780        if let Some(width) = style.font_width {
781            self.font_width = width;
782        }
783
784        if let Some(style) = style.font_style {
785            self.font_style = style;
786        }
787
788        if let Some(color) = style.color {
789            self.color = self.color.blend(&color);
790        }
791
792        if let Some(factor) = style.fade_out {
793            self.color.fade_out(factor);
794        }
795
796        if let Some(background_color) = style.background_color {
797            self.background_color = Some(background_color);
798        }
799
800        if let Some(underline) = style.underline {
801            self.underline = Some(underline);
802        }
803
804        if let Some(strikethrough) = style.strikethrough {
805            self.strikethrough = Some(strikethrough);
806        }
807
808        self
809    }
810
811    /// Get the font configured for this text style.
812    pub fn font(&self) -> Font {
813        Font {
814            family: self.font_family.clone(),
815            features: self.font_features.clone(),
816            fallbacks: self.font_fallbacks.clone(),
817            weight: self.font_weight,
818            width: self.font_width,
819            style: self.font_style,
820        }
821    }
822
823    /// Returns the rounded line height in pixels.
824    pub fn line_height_in_pixels(&self, rem_size: Pixels) -> Pixels {
825        self.line_height.to_pixels(self.font_size, rem_size).round()
826    }
827
828    /// Convert this text style into a [`TextRun`], for the given length of the text.
829    pub fn to_run(&self, len: usize) -> TextRun {
830        TextRun {
831            len,
832            font: Font {
833                family: self.font_family.clone(),
834                features: self.font_features.clone(),
835                fallbacks: self.font_fallbacks.clone(),
836                weight: self.font_weight,
837                width: self.font_width,
838                style: self.font_style,
839            },
840            color: self.color,
841            background_color: self.background_color,
842            underline: self.underline,
843            strikethrough: self.strikethrough,
844            letter_spacing: self.letter_spacing,
845        }
846    }
847}
848
849/// A highlight style to apply, similar to a `TextStyle` except
850/// for a single font, uniformly sized and spaced text.
851///
852/// Layout extras on the base [`TextStyle`] — such as [`TextStyle::letter_spacing`] and
853/// [`TextStyle::text_transform`] — are not stored here; highlighted segments inherit them
854/// from the surrounding style (see [`TextStyle::highlight`] and
855/// [`crate::StyledText::with_default_highlights`]).
856#[derive(Copy, Clone, Debug, Default, PartialEq)]
857pub struct HighlightStyle {
858    /// The color of the text
859    pub color: Option<Hsla>,
860
861    /// The font weight, e.g. bold
862    pub font_weight: Option<FontWeight>,
863
864    /// The font width as a percentage of normal width.
865    pub font_width: Option<FontWidth>,
866
867    /// The font style, e.g. italic
868    pub font_style: Option<FontStyle>,
869
870    /// The background color of the text
871    pub background_color: Option<Hsla>,
872
873    /// The underline style of the text
874    pub underline: Option<UnderlineStyle>,
875
876    /// The strikethrough style of the text
877    pub strikethrough: Option<StrikethroughStyle>,
878
879    /// Similar to the CSS `opacity` property, this will cause the text to be less vibrant.
880    pub fade_out: Option<f32>,
881}
882
883impl Style {
884    /// Resolves the initial Unicode bidi behavior against this style's direction.
885    #[doc(hidden)]
886    pub fn effective_unicode_bidi(&self) -> UnicodeBidi {
887        if self.unicode_bidi_explicit {
888            self.unicode_bidi
889        } else if self.direction == LayoutDirection::Inherit {
890            UnicodeBidi::Normal
891        } else {
892            UnicodeBidi::Isolate
893        }
894    }
895
896    /// Returns true if the style has a background that is not fully transparent. Visibility is not checked.
897    pub fn has_opaque_background(&self) -> bool {
898        self.background
899            .as_ref()
900            .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent()))
901    }
902
903    /// Get the text style in this element style.
904    pub fn text_style(&self) -> Option<&TextStyleRefinement> {
905        if self.text.is_some() {
906            Some(&self.text)
907        } else {
908            None
909        }
910    }
911
912    /// Get the content mask for this element style, based on the given bounds.
913    /// If the element does not hide its overflow, this will return `None`.
914    pub fn overflow_mask(
915        &self,
916        bounds: Bounds<Pixels>,
917        rem_size: Pixels,
918    ) -> Option<ContentMask<Pixels>> {
919        match self.overflow {
920            Point {
921                x: Overflow::Visible,
922                y: Overflow::Visible,
923            } => None,
924            _ => {
925                let mut min = bounds.origin;
926                let mut max = bounds.bottom_right();
927
928                if self
929                    .border_color
930                    .is_some_and(|background| !background.is_transparent())
931                {
932                    min.x += self.border_widths.left.to_pixels(rem_size);
933                    max.x -= self.border_widths.right.to_pixels(rem_size);
934                    min.y += self.border_widths.top.to_pixels(rem_size);
935                    max.y -= self.border_widths.bottom.to_pixels(rem_size);
936                }
937
938                let bounds = match (
939                    self.overflow.x == Overflow::Visible,
940                    self.overflow.y == Overflow::Visible,
941                ) {
942                    // x and y both visible
943                    (true, true) => return None,
944                    // x visible, y hidden
945                    (true, false) => Bounds::from_corners(
946                        point(min.x, bounds.origin.y),
947                        point(max.x, bounds.bottom_right().y),
948                    ),
949                    // x hidden, y visible
950                    (false, true) => Bounds::from_corners(
951                        point(bounds.origin.x, min.y),
952                        point(bounds.bottom_right().x, max.y),
953                    ),
954                    // both hidden
955                    (false, false) => Bounds::from_corners(min, max),
956                };
957
958                let mut fade_out = self.overflow_fade.to_pixels(rem_size);
959                if self.overflow.x == Overflow::Visible {
960                    fade_out.left = Pixels::ZERO;
961                    fade_out.right = Pixels::ZERO;
962                }
963                if self.overflow.y == Overflow::Visible {
964                    fade_out.top = Pixels::ZERO;
965                    fade_out.bottom = Pixels::ZERO;
966                }
967
968                Some(ContentMask { bounds, fade_out })
969            }
970        }
971    }
972
973    /// Get the content mask for a container that always clips and scrolls
974    /// vertically, like [`List`](crate::List) and
975    /// [`UniformList`](crate::UniformList): the vertical axis always clips,
976    /// the horizontal axis clips according to `overflow.x`, and
977    /// `overflow_fade` distances apply on each clipped axis.
978    pub fn scroll_mask(&self, bounds: Bounds<Pixels>, rem_size: Pixels) -> ContentMask<Pixels> {
979        let scroll_style = Style {
980            overflow: Point {
981                x: self.overflow.x,
982                y: Overflow::Scroll,
983            },
984            border_color: self.border_color,
985            border_widths: self.border_widths,
986            overflow_fade: self.overflow_fade,
987            ..Style::default()
988        };
989        scroll_style
990            .overflow_mask(bounds, rem_size)
991            .expect("vertical scrolling always clips")
992    }
993
994    /// Paints the background of an element styled with this style.
995    pub fn paint(
996        &self,
997        bounds: Bounds<Pixels>,
998        window: &mut Window,
999        cx: &mut App,
1000        continuation: impl FnOnce(&mut Window, &mut App),
1001    ) {
1002        window.with_element_opacity(self.opacity, |window| {
1003            if let Some(fragments) = window.current_inline_fragments.clone() {
1004                for fragment in fragments.iter() {
1005                    self.paint_box(*fragment, window, cx, |_, _| {});
1006                }
1007
1008                continuation(window, cx);
1009
1010                return;
1011            }
1012
1013            self.paint_box(bounds, window, cx, continuation);
1014        });
1015    }
1016
1017    fn paint_box(
1018        &self,
1019        bounds: Bounds<Pixels>,
1020        window: &mut Window,
1021        cx: &mut App,
1022        continuation: impl FnOnce(&mut Window, &mut App),
1023    ) {
1024        #[cfg(debug_assertions)]
1025        if self.debug_below {
1026            cx.set_global(DebugBelow)
1027        }
1028
1029        #[cfg(debug_assertions)]
1030        if self.debug || cx.has_global::<DebugBelow>() {
1031            window.paint_quad(
1032                PaintQuad::new(bounds)
1033                    .border_widths(px(1.))
1034                    .border_color(crate::red()),
1035            );
1036        }
1037
1038        let rem_size = window.rem_size();
1039        let corner_radii = self
1040            .corner_radii
1041            .to_pixels(rem_size)
1042            .clamp_radii_for_quad_size(bounds.size);
1043        let corner_smoothing = self.corner_smoothing.unwrap_or_default();
1044
1045        let current_color = self.text.color.unwrap_or_else(|| window.text_style().color);
1046
1047        window.paint_drop_shadows(bounds, corner_radii, corner_smoothing, &self.box_shadow);
1048
1049        if let Some(ring) = self.ring.outer_quad(bounds, corner_radii, current_color) {
1050            window.paint_quad(ring.corner_smoothing(corner_smoothing));
1051        }
1052
1053        // Blur the content behind this element before its (typically translucent) background
1054        // is painted on top, so the background tints the frosted backdrop (CSS `backdrop-filter`).
1055        if !self.backdrop_filter.is_empty() {
1056            window.paint_backdrop_filter(
1057                bounds,
1058                corner_radii,
1059                corner_smoothing,
1060                &self.backdrop_filter,
1061            );
1062        }
1063
1064        // The element's own box — background, inset shadows, children, and border — painted as a
1065        // unit. A `filter` (CSS `filter`) wraps this whole unit so the renderer blurs the element
1066        // and its children together as one group; without a filter it paints directly.
1067        let paint_box = |window: &mut Window, cx: &mut App| {
1068            let background_color = self.background.as_ref().and_then(Fill::color);
1069            if background_color.is_some_and(|color| !color.is_transparent()) {
1070                let background_color = background_color.unwrap_or_default();
1071                window.paint_quad(
1072                    PaintQuad::new(bounds)
1073                        .corner_radii(corner_radii)
1074                        .background(background_color)
1075                        .border_color(background_color.opacity(0.))
1076                        .border_style(self.border_style)
1077                        .corner_smoothing(corner_smoothing),
1078                );
1079            }
1080
1081            if let Some(ring) = self.inset_ring.inset_shadow(current_color) {
1082                window.paint_inset_shadows(
1083                    bounds,
1084                    corner_radii,
1085                    corner_smoothing,
1086                    std::slice::from_ref(&ring),
1087                );
1088            }
1089            window.paint_inset_shadows(bounds, corner_radii, corner_smoothing, &self.box_shadow);
1090
1091            continuation(window, cx);
1092
1093            if self.is_border_visible() {
1094                let border_widths = self.border_widths.to_pixels(rem_size);
1095                let border_color = self.border_color.unwrap_or_default();
1096                window.paint_quad(
1097                    PaintQuad::new(bounds)
1098                        .corner_radii(corner_radii)
1099                        .background(border_color.opacity(0.))
1100                        .border_widths(border_widths)
1101                        .border_color(border_color)
1102                        .border_style(self.border_style)
1103                        .corner_smoothing(corner_smoothing)
1104                        .border_dashed_length(self.border_dashed_length)
1105                        .border_dashed_gap(self.border_dashed_gap),
1106                );
1107            }
1108        };
1109
1110        if self.filter.is_empty() {
1111            paint_box(window, cx);
1112        } else {
1113            window.with_filter_layer(
1114                bounds,
1115                corner_radii,
1116                corner_smoothing,
1117                &self.filter,
1118                |window| {
1119                    paint_box(window, cx);
1120                },
1121            );
1122        }
1123
1124        #[cfg(debug_assertions)]
1125        if self.debug_below {
1126            cx.remove_global::<DebugBelow>();
1127        }
1128    }
1129
1130    fn is_border_visible(&self) -> bool {
1131        self.border_color
1132            .is_some_and(|background| !background.is_transparent())
1133            && self.border_widths.any(|length| !length.is_zero())
1134    }
1135}
1136
1137impl Default for Style {
1138    fn default() -> Self {
1139        Style {
1140            display: Display::Block,
1141            visibility: Visibility::Visible,
1142            direction: LayoutDirection::Inherit,
1143            unicode_bidi: UnicodeBidi::Normal,
1144            unicode_bidi_explicit: false,
1145            overflow: Point {
1146                x: Overflow::Visible,
1147                y: Overflow::Visible,
1148            },
1149            overflow_fade: Edges::<AbsoluteLength>::zero(),
1150            allow_concurrent_scroll: false,
1151            restrict_scroll_to_axis: false,
1152            scrollbar_width: AbsoluteLength::default(),
1153            position: Position::Relative,
1154            inset: Edges::auto(),
1155            margin: Edges::<Length>::zero(),
1156            padding: Edges::<DefiniteLength>::zero(),
1157            border_widths: Edges::<AbsoluteLength>::zero(),
1158            size: Size::auto(),
1159            min_size: Size::auto(),
1160            max_size: Size::auto(),
1161            aspect_ratio: None,
1162            gap: Size::default(),
1163            // Alignment
1164            align_items: None,
1165            align_self: None,
1166            vertical_align: VerticalAlign::Baseline,
1167            align_content: None,
1168            justify_content: None,
1169            // Flexbox
1170            flex_direction: FlexDirection::Row,
1171            flex_wrap: FlexWrap::NoWrap,
1172            flex_grow: 0.0,
1173            flex_shrink: 1.0,
1174            flex_basis: Length::Auto,
1175            background: None,
1176            border_color: None,
1177            border_style: BorderStyle::default(),
1178            border_dashed_length: crate::scene::DEFAULT_BORDER_DASHED_LENGTH,
1179            border_dashed_gap: crate::scene::DEFAULT_BORDER_DASHED_GAP,
1180            corner_radii: Corners::default(),
1181            corner_smoothing: None,
1182            box_shadow: Default::default(),
1183            ring: Default::default(),
1184            inset_ring: Default::default(),
1185            filter: Default::default(),
1186            backdrop_filter: Default::default(),
1187            text: TextStyleRefinement::default(),
1188            mouse_cursor: None,
1189            opacity: None,
1190            grid_rows: None,
1191            grid_cols: None,
1192            grid_location: None,
1193
1194            #[cfg(debug_assertions)]
1195            debug: false,
1196            #[cfg(debug_assertions)]
1197            debug_below: false,
1198        }
1199    }
1200}
1201
1202/// The properties that can be applied to an underline.
1203#[derive(Refineable, Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
1204pub struct UnderlineStyle {
1205    /// The thickness of the underline.
1206    pub thickness: Pixels,
1207
1208    /// The color of the underline.
1209    pub color: Option<Hsla>,
1210
1211    /// Whether the underline should be wavy, like in a spell checker.
1212    pub wavy: bool,
1213}
1214
1215/// The properties that can be applied to a strikethrough.
1216#[derive(Refineable, Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
1217pub struct StrikethroughStyle {
1218    /// The thickness of the strikethrough.
1219    pub thickness: Pixels,
1220
1221    /// The color of the strikethrough.
1222    pub color: Option<Hsla>,
1223}
1224
1225/// The kinds of fill that can be applied to a shape.
1226#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
1227pub enum Fill {
1228    /// A solid color fill.
1229    Color(Background),
1230}
1231
1232impl Fill {
1233    /// Unwrap this fill into a solid color, if it is one.
1234    ///
1235    /// If the fill is not a solid color, this method returns `None`.
1236    pub fn color(&self) -> Option<Background> {
1237        match self {
1238            Fill::Color(color) => Some(*color),
1239        }
1240    }
1241}
1242
1243impl Default for Fill {
1244    fn default() -> Self {
1245        Self::Color(Background::default())
1246    }
1247}
1248
1249impl<T: Into<Background>> From<T> for Fill {
1250    fn from(color: T) -> Self {
1251        Self::Color(color.into())
1252    }
1253}
1254
1255impl From<TextStyle> for HighlightStyle {
1256    fn from(other: TextStyle) -> Self {
1257        Self::from(&other)
1258    }
1259}
1260
1261impl From<&TextStyle> for HighlightStyle {
1262    fn from(other: &TextStyle) -> Self {
1263        Self {
1264            color: Some(other.color),
1265            font_weight: Some(other.font_weight),
1266            font_width: Some(other.font_width),
1267            font_style: Some(other.font_style),
1268            background_color: other.background_color,
1269            underline: other.underline,
1270            strikethrough: other.strikethrough,
1271            fade_out: None,
1272        }
1273    }
1274}
1275
1276impl HighlightStyle {
1277    /// Creates a highlight style with a foreground color.
1278    pub fn color(color: Hsla) -> Self {
1279        Self {
1280            color: Some(color),
1281            ..Default::default()
1282        }
1283    }
1284    /// Blend this highlight style with another.
1285    /// Non-continuous properties, like font_weight and font_style, are overwritten.
1286    #[must_use]
1287    pub fn highlight(self, other: HighlightStyle) -> Self {
1288        Self {
1289            color: other
1290                .color
1291                .map(|other_color| {
1292                    if let Some(color) = self.color {
1293                        color.blend(&other_color)
1294                    } else {
1295                        other_color
1296                    }
1297                })
1298                .or(self.color),
1299            font_weight: other.font_weight.or(self.font_weight),
1300            font_width: other.font_width.or(self.font_width),
1301            font_style: other.font_style.or(self.font_style),
1302            background_color: other.background_color.or(self.background_color),
1303            underline: other.underline.or(self.underline),
1304            strikethrough: other.strikethrough.or(self.strikethrough),
1305            fade_out: other
1306                .fade_out
1307                .map(|source_fade| {
1308                    self.fade_out
1309                        .map(|dest_fade| (dest_fade * (1. + source_fade)).clamp(0., 1.))
1310                        .unwrap_or(source_fade)
1311                })
1312                .or(self.fade_out),
1313        }
1314    }
1315}
1316
1317impl From<Hsla> for HighlightStyle {
1318    fn from(color: Hsla) -> Self {
1319        Self {
1320            color: Some(color),
1321            ..Default::default()
1322        }
1323    }
1324}
1325
1326impl From<FontWeight> for HighlightStyle {
1327    fn from(font_weight: FontWeight) -> Self {
1328        Self {
1329            font_weight: Some(font_weight),
1330            ..Default::default()
1331        }
1332    }
1333}
1334
1335impl From<FontStyle> for HighlightStyle {
1336    fn from(font_style: FontStyle) -> Self {
1337        Self {
1338            font_style: Some(font_style),
1339            ..Default::default()
1340        }
1341    }
1342}
1343
1344impl From<Rgba> for HighlightStyle {
1345    fn from(color: Rgba) -> Self {
1346        Self {
1347            color: Some(color.into_color()),
1348            ..Default::default()
1349        }
1350    }
1351}
1352
1353/// Combine and merge the highlights and ranges in the two iterators.
1354pub fn combine_highlights(
1355    a: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
1356    b: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
1357) -> impl Iterator<Item = (Range<usize>, HighlightStyle)> {
1358    let mut endpoints = Vec::new();
1359    let mut highlights = Vec::new();
1360    for (range, highlight) in a.into_iter().chain(b) {
1361        if !range.is_empty() {
1362            let highlight_id = highlights.len();
1363            endpoints.push((range.start, highlight_id, true));
1364            endpoints.push((range.end, highlight_id, false));
1365            highlights.push(highlight);
1366        }
1367    }
1368    endpoints.sort_unstable_by_key(|(position, _, _)| *position);
1369    let mut endpoints = endpoints.into_iter().peekable();
1370
1371    let mut active_styles = HashSet::default();
1372    let mut ix = 0;
1373    iter::from_fn(move || {
1374        while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() {
1375            let prev_index = mem::replace(&mut ix, *endpoint_ix);
1376            if ix > prev_index && !active_styles.is_empty() {
1377                let current_style = active_styles
1378                    .iter()
1379                    .fold(HighlightStyle::default(), |acc, highlight_id| {
1380                        acc.highlight(highlights[*highlight_id])
1381                    });
1382                return Some((prev_index..ix, current_style));
1383            }
1384
1385            if *is_start {
1386                active_styles.insert(*highlight_id);
1387            } else {
1388                active_styles.remove(highlight_id);
1389            }
1390            endpoints.next();
1391        }
1392        None
1393    })
1394}
1395
1396/// Aligns children along the cross axis in flexbox and the block axis in grid.
1397///
1398/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items)
1399#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
1400// Copy of taffy::style type of the same name, to derive JsonSchema.
1401pub enum AlignItems {
1402    /// Items are packed toward the start of the axis
1403    Start,
1404    /// Items are packed toward the end of the axis
1405    End,
1406    /// Items are packed towards the flex-relative start of the axis.
1407    ///
1408    /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
1409    /// to End. In all other cases it is equivalent to Start.
1410    FlexStart,
1411    /// Items are packed towards the flex-relative end of the axis.
1412    ///
1413    /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
1414    /// to Start. In all other cases it is equivalent to End.
1415    FlexEnd,
1416    /// Items are packed along the center of the cross axis
1417    Center,
1418    /// Items are aligned such as their baselines align
1419    Baseline,
1420    /// Stretch to fill the container
1421    Stretch,
1422}
1423/// Aligns grid children along the inline axis.
1424///
1425/// Flexbox ignores this property on the container.
1426///
1427/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-items)
1428pub type JustifyItems = AlignItems;
1429/// Aligns one child along the cross axis in flexbox or the block axis in grid.
1430///
1431/// It overrides the parent's `AlignItems` value for that child.
1432///
1433/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-self)
1434pub type AlignSelf = AlignItems;
1435/// Aligns one grid child along the inline axis.
1436///
1437/// It overrides the parent's `JustifyItems` value. Flexbox ignores it.
1438///
1439/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-self)
1440pub type JustifySelf = AlignItems;
1441
1442/// Controls the vertical position of an element box in an inline formatting context.
1443///
1444/// This property has no effect on ordinary block, flex, or grid layout.
1445#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default, Serialize, Deserialize, JsonSchema)]
1446pub enum VerticalAlign {
1447    /// Align the bottom of the box with the text baseline.
1448    #[default]
1449    Baseline,
1450    /// Align the box midpoint with the parent baseline plus half the parent font's x-height.
1451    /// This is CSS text-metric alignment. Use `self_center` or `items_center` for flex or grid
1452    /// container centering.
1453    Middle,
1454    /// Align the top of the box with the top of its line.
1455    Top,
1456    /// Align the bottom of the box with the bottom of its line.
1457    Bottom,
1458}
1459
1460/// Distributes free space between flex lines or grid tracks along the cross axis.
1461///
1462/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-content)
1463#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
1464// Copy of taffy::style type of the same name, to derive JsonSchema.
1465pub enum AlignContent {
1466    /// Items are packed toward the start of the axis
1467    Start,
1468    /// Items are packed toward the end of the axis
1469    End,
1470    /// Items are packed towards the flex-relative start of the axis.
1471    ///
1472    /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
1473    /// to End. In all other cases it is equivalent to Start.
1474    FlexStart,
1475    /// Items are packed towards the flex-relative end of the axis.
1476    ///
1477    /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
1478    /// to Start. In all other cases it is equivalent to End.
1479    FlexEnd,
1480    /// Items are centered around the middle of the axis
1481    Center,
1482    /// Items are stretched to fill the container
1483    Stretch,
1484    /// The first and last items are aligned flush with the edges of the container (no gap)
1485    /// The gap between items is distributed evenly.
1486    SpaceBetween,
1487    /// The gap between the first and last items is exactly THE SAME as the gap between items.
1488    /// The gaps are distributed evenly
1489    SpaceEvenly,
1490    /// The gap between the first and last items is exactly HALF the gap between items.
1491    /// The gaps are distributed evenly in proportion to these ratios.
1492    SpaceAround,
1493}
1494
1495/// Distributes free space between flex items or grid tracks along the main or inline axis.
1496///
1497/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content)
1498pub type JustifyContent = AlignContent;
1499
1500/// Selects the layout algorithm used for this element's children.
1501///
1502/// Defaults to flex layout. Block containers arrange text and inline children in paragraphs.
1503#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1504// Inline participation is handled by GPUI before independent box layout in Taffy.
1505pub enum Display {
1506    /// The children will follow the block layout algorithm
1507    Block,
1508
1509    /// The children will follow the flexbox layout algorithm
1510    #[default]
1511    Flex,
1512
1513    /// The children will follow the CSS Grid layout algorithm
1514    Grid,
1515
1516    /// Contributes its contents to the surrounding paragraph, wrapping across lines.
1517    /// As a root or flex/grid item, lays out its contents independently using block flow.
1518    Inline,
1519
1520    /// An atomic inline box whose children use flex layout.
1521    InlineFlex,
1522
1523    /// The element and its children are hidden and occupy no space.
1524    None,
1525}
1526
1527/// Controls whether flex items stay on one line or wrap onto multiple lines.
1528///
1529/// Defaults to [`FlexWrap::NoWrap`]
1530///
1531/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-wrap-property)
1532#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1533// Copy of taffy::style type of the same name, to derive JsonSchema.
1534pub enum FlexWrap {
1535    /// Items will not wrap and stay on a single line
1536    #[default]
1537    NoWrap,
1538    /// Items will wrap according to this item's [`FlexDirection`]
1539    Wrap,
1540    /// Items will wrap in the opposite direction to this item's [`FlexDirection`]
1541    WrapReverse,
1542}
1543
1544/// The direction of a flex container's main axis.
1545///
1546/// A flex container has a main axis, where children are placed, and a perpendicular cross axis,
1547/// where they are aligned. `justify-*` properties act on the main axis; `align-*` properties act
1548/// on the cross axis.
1549///
1550/// The default behavior is [`FlexDirection::Row`].
1551///
1552/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-direction-property)
1553#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1554// Copy of taffy::style type of the same name, to derive JsonSchema.
1555pub enum FlexDirection {
1556    /// Defines +x as the main axis
1557    ///
1558    /// Items will be added from left to right in a row.
1559    #[default]
1560    Row,
1561    /// Defines +y as the main axis
1562    ///
1563    /// Items will be added from top to bottom in a column.
1564    Column,
1565    /// Defines -x as the main axis
1566    ///
1567    /// Items will be added from right to left in a row.
1568    RowReverse,
1569    /// Defines -y as the main axis
1570    ///
1571    /// Items will be added from bottom to top in a column.
1572    ColumnReverse,
1573}
1574
1575/// How overflow affects layout and the scroll region.
1576///
1577/// GPUI uses this enum for the layout effects of CSS `overflow`; clipping and scrolling are
1578/// applied while the element is painted. Its main layout effects are:
1579///
1580///   - The automatic minimum size Flexbox/CSS Grid items with non-`Visible` overflow is `0` rather than being content based
1581///   - `Overflow::Scroll` nodes have space in the layout reserved for a scrollbar (width controlled by the `scrollbar_width` property)
1582///
1583/// Taffy supplies the layout effects. The amount of space reserved for a scrollbar is controlled
1584/// by `scrollbar_width`; when it is `0`, `Scroll` reserves no space and lays out like `Hidden`.
1585///
1586/// <https://developer.mozilla.org/en-US/docs/Web/CSS/overflow>
1587#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1588// Copy of taffy::style type of the same name, to derive JsonSchema.
1589pub enum Overflow {
1590    /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content.
1591    /// Content that overflows this node *should* contribute to the scroll region of its parent.
1592    #[default]
1593    Visible,
1594    /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content.
1595    /// Content that overflows this node should *not* contribute to the scroll region of its parent.
1596    Clip,
1597    /// The automatic minimum size of this node as a flexbox/grid item should be `0`.
1598    /// Content that overflows this node should *not* contribute to the scroll region of its parent.
1599    Hidden,
1600    /// The automatic minimum size of this node as a flexbox/grid item should be `0`. Additionally, space should be reserved
1601    /// for a scrollbar. The amount of space reserved is controlled by the `scrollbar_width` property.
1602    /// Content that overflows this node should *not* contribute to the scroll region of its parent.
1603    Scroll,
1604}
1605
1606/// The positioning strategy for this item.
1607///
1608/// This controls both how the origin is determined for the [`Style::position`] field,
1609/// and whether or not the item will be controlled by flexbox's layout algorithm.
1610///
1611/// The behavior follows [CSS `position`](https://developer.mozilla.org/en-US/docs/Web/CSS/position),
1612/// including its interaction with layout and offsets.
1613///
1614/// [`Position::Relative`] is GPUI's default; CSS defaults to `static`.
1615#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1616// Copy of taffy::style type of the same name, to derive JsonSchema.
1617pub enum Position {
1618    /// The offset is computed relative to the final position given by the layout algorithm.
1619    /// Offsets do not affect the position of any other items; they are effectively a correction factor applied at the end.
1620    #[default]
1621    Relative,
1622    /// The offset is computed relative to this item's closest positioned ancestor, if any.
1623    /// Otherwise, it is placed relative to the origin.
1624    /// No space is created for the item in the page layout, and its size will not be altered.
1625    ///
1626    /// To remove the item from layout and painting, use [`Display::None`] instead.
1627    Absolute,
1628}
1629
1630impl From<AlignItems> for taffy::style::AlignItems {
1631    fn from(value: AlignItems) -> Self {
1632        match value {
1633            AlignItems::Start => Self::START,
1634            AlignItems::End => Self::END,
1635            AlignItems::FlexStart => Self::FLEX_START,
1636            AlignItems::FlexEnd => Self::FLEX_END,
1637            AlignItems::Center => Self::CENTER,
1638            AlignItems::Baseline => Self::BASELINE,
1639            AlignItems::Stretch => Self::STRETCH,
1640        }
1641    }
1642}
1643
1644impl From<AlignContent> for taffy::style::AlignContent {
1645    fn from(value: AlignContent) -> Self {
1646        match value {
1647            AlignContent::Start => Self::START,
1648            AlignContent::End => Self::END,
1649            AlignContent::FlexStart => Self::FLEX_START,
1650            AlignContent::FlexEnd => Self::FLEX_END,
1651            AlignContent::Center => Self::CENTER,
1652            AlignContent::Stretch => Self::STRETCH,
1653            AlignContent::SpaceBetween => Self::SPACE_BETWEEN,
1654            AlignContent::SpaceEvenly => Self::SPACE_EVENLY,
1655            AlignContent::SpaceAround => Self::SPACE_AROUND,
1656        }
1657    }
1658}
1659
1660impl From<Display> for taffy::style::Display {
1661    fn from(value: Display) -> Self {
1662        match value {
1663            Display::Block | Display::Inline => Self::Block,
1664            Display::Flex | Display::InlineFlex => Self::Flex,
1665            Display::Grid => Self::Grid,
1666            Display::None => Self::None,
1667        }
1668    }
1669}
1670
1671impl From<FlexWrap> for taffy::style::FlexWrap {
1672    fn from(value: FlexWrap) -> Self {
1673        match value {
1674            FlexWrap::NoWrap => Self::NoWrap,
1675            FlexWrap::Wrap => Self::Wrap,
1676            FlexWrap::WrapReverse => Self::WrapReverse,
1677        }
1678    }
1679}
1680
1681impl From<FlexDirection> for taffy::style::FlexDirection {
1682    fn from(value: FlexDirection) -> Self {
1683        match value {
1684            FlexDirection::Row => Self::Row,
1685            FlexDirection::Column => Self::Column,
1686            FlexDirection::RowReverse => Self::RowReverse,
1687            FlexDirection::ColumnReverse => Self::ColumnReverse,
1688        }
1689    }
1690}
1691
1692impl From<Overflow> for taffy::style::Overflow {
1693    fn from(value: Overflow) -> Self {
1694        match value {
1695            Overflow::Visible => Self::Visible,
1696            Overflow::Clip => Self::Clip,
1697            Overflow::Hidden => Self::Hidden,
1698            Overflow::Scroll => Self::Scroll,
1699        }
1700    }
1701}
1702
1703impl From<Position> for taffy::style::Position {
1704    fn from(value: Position) -> Self {
1705        match value {
1706            Position::Relative => Self::Relative,
1707            Position::Absolute => Self::Absolute,
1708        }
1709    }
1710}
1711
1712#[cfg(test)]
1713mod tests {
1714    use crate::{blue, green, hsla, linear_color_stop, linear_gradient, px, red, yellow};
1715    use palette::WithAlpha;
1716
1717    use super::*;
1718
1719    fn ring_gradient() -> Background {
1720        linear_gradient(
1721            90.,
1722            linear_color_stop(red(), 0.),
1723            linear_color_stop(blue(), 1.),
1724        )
1725    }
1726
1727    #[test]
1728    fn border_color_accepts_backgrounds_and_solid_colors() {
1729        let gradient = linear_gradient(
1730            90.0,
1731            linear_color_stop(red(), 0.0),
1732            linear_color_stop(blue(), 1.0),
1733        );
1734        let gradient_style = StyleRefinement::default().border_color(gradient);
1735        assert_eq!(gradient_style.border_color, Some(gradient));
1736
1737        let solid_style = StyleRefinement::default().border_color(red());
1738        assert_eq!(solid_style.border_color, Some(red().into()));
1739    }
1740
1741    #[test]
1742    fn test_basic_highlight_style_combination() {
1743        let style_a = HighlightStyle::default();
1744        let style_b = HighlightStyle::default();
1745        let style_a = style_a.highlight(style_b);
1746        assert_eq!(
1747            style_a,
1748            HighlightStyle::default(),
1749            "Combining empty styles should not produce a non-empty style."
1750        );
1751
1752        let mut style_b = HighlightStyle {
1753            color: Some(red()),
1754            strikethrough: Some(StrikethroughStyle {
1755                thickness: px(2.),
1756                color: Some(blue()),
1757            }),
1758            fade_out: Some(0.),
1759            font_style: Some(FontStyle::Italic),
1760            font_weight: Some(FontWeight(300.)),
1761            font_width: Some(FontWidth::CONDENSED),
1762            background_color: Some(yellow()),
1763            underline: Some(UnderlineStyle {
1764                thickness: px(2.),
1765                color: Some(red()),
1766                wavy: true,
1767            }),
1768        };
1769        let expected_style = style_b;
1770
1771        assert_eq!(
1772            TextStyle::default().highlight(style_b).font_width,
1773            FontWidth::CONDENSED
1774        );
1775
1776        let style_a = style_a.highlight(style_b);
1777        assert_eq!(
1778            style_a, expected_style,
1779            "Blending an empty style with another style should return the other style"
1780        );
1781
1782        let style_b = style_b.highlight(Default::default());
1783        assert_eq!(
1784            style_b, expected_style,
1785            "Blending a style with an empty style should not change the style."
1786        );
1787
1788        let mut style_c = expected_style;
1789
1790        let style_d = HighlightStyle {
1791            color: Some(blue().with_alpha(0.7)),
1792            strikethrough: Some(StrikethroughStyle {
1793                thickness: px(4.),
1794                color: Some(crate::red()),
1795            }),
1796            fade_out: Some(0.),
1797            font_style: Some(FontStyle::Oblique),
1798            font_weight: Some(FontWeight(800.)),
1799            font_width: Some(FontWidth::EXPANDED),
1800            background_color: Some(green()),
1801            underline: Some(UnderlineStyle {
1802                thickness: px(4.),
1803                color: None,
1804                wavy: false,
1805            }),
1806        };
1807
1808        let expected_style = HighlightStyle {
1809            color: Some(red().blend(&blue().with_alpha(0.7))),
1810            strikethrough: Some(StrikethroughStyle {
1811                thickness: px(4.),
1812                color: Some(red()),
1813            }),
1814            // TODO this does not seem right
1815            fade_out: Some(0.),
1816            font_style: Some(FontStyle::Oblique),
1817            font_weight: Some(FontWeight(800.)),
1818            font_width: Some(FontWidth::EXPANDED),
1819            background_color: Some(green()),
1820            underline: Some(UnderlineStyle {
1821                thickness: px(4.),
1822                color: None,
1823                wavy: false,
1824            }),
1825        };
1826
1827        let style_c = style_c.highlight(style_d);
1828        assert_eq!(
1829            style_c, expected_style,
1830            "Blending styles should blend properties where possible and override all others"
1831        );
1832    }
1833
1834    #[test]
1835    fn test_combine_highlights() {
1836        assert_eq!(
1837            combine_highlights(
1838                [
1839                    (0..5, green().into()),
1840                    (4..10, FontWeight::BOLD.into()),
1841                    (15..20, yellow().into()),
1842                ],
1843                [
1844                    (2..6, FontStyle::Italic.into()),
1845                    (1..3, blue().into()),
1846                    (21..23, red().into()),
1847                ]
1848            )
1849            .collect::<Vec<_>>(),
1850            [
1851                (
1852                    0..1,
1853                    HighlightStyle {
1854                        color: Some(green()),
1855                        ..Default::default()
1856                    }
1857                ),
1858                (
1859                    1..2,
1860                    HighlightStyle {
1861                        color: Some(blue()),
1862                        ..Default::default()
1863                    }
1864                ),
1865                (
1866                    2..3,
1867                    HighlightStyle {
1868                        color: Some(blue()),
1869                        font_style: Some(FontStyle::Italic),
1870                        ..Default::default()
1871                    }
1872                ),
1873                (
1874                    3..4,
1875                    HighlightStyle {
1876                        color: Some(green()),
1877                        font_style: Some(FontStyle::Italic),
1878                        ..Default::default()
1879                    }
1880                ),
1881                (
1882                    4..5,
1883                    HighlightStyle {
1884                        color: Some(green()),
1885                        font_weight: Some(FontWeight::BOLD),
1886                        font_style: Some(FontStyle::Italic),
1887                        ..Default::default()
1888                    }
1889                ),
1890                (
1891                    5..6,
1892                    HighlightStyle {
1893                        font_weight: Some(FontWeight::BOLD),
1894                        font_style: Some(FontStyle::Italic),
1895                        ..Default::default()
1896                    }
1897                ),
1898                (
1899                    6..10,
1900                    HighlightStyle {
1901                        font_weight: Some(FontWeight::BOLD),
1902                        ..Default::default()
1903                    }
1904                ),
1905                (
1906                    15..20,
1907                    HighlightStyle {
1908                        color: Some(yellow()),
1909                        ..Default::default()
1910                    }
1911                ),
1912                (
1913                    21..23,
1914                    HighlightStyle {
1915                        color: Some(red()),
1916                        ..Default::default()
1917                    }
1918                )
1919            ]
1920        );
1921    }
1922
1923    #[test]
1924    fn test_text_style_refinement() {
1925        let mut style = Style::default();
1926        style.refine(&StyleRefinement::default().text_size(px(20.0)));
1927        style.refine(&StyleRefinement::default().font_weight(FontWeight::SEMIBOLD));
1928        style.refine(&StyleRefinement::default().font_width(87.5));
1929
1930        assert_eq!(
1931            Some(AbsoluteLength::from(px(20.0))),
1932            style.text_style().unwrap().font_size
1933        );
1934
1935        assert_eq!(
1936            Some(FontWeight::SEMIBOLD),
1937            style.text_style().unwrap().font_weight
1938        );
1939
1940        let mut resolved = TextStyle::default();
1941        resolved.refine(style.text_style().unwrap());
1942        let inherited = resolved.highlight(HighlightStyle::color(red()));
1943
1944        assert_eq!(inherited.font().width, FontWidth::SEMI_CONDENSED);
1945        assert_eq!(inherited.to_run(4).font.width, FontWidth::SEMI_CONDENSED);
1946        assert_eq!(
1947            HighlightStyle::from(&inherited).font_width,
1948            Some(FontWidth::SEMI_CONDENSED)
1949        );
1950
1951        let mut element = crate::div()
1952            .font_width(FontWidth::CONDENSED)
1953            .font(crate::font("IBM Plex Sans"));
1954        assert_eq!(element.text_style().font_width, Some(FontWidth::NORMAL));
1955    }
1956
1957    #[test]
1958    fn dashed_border_pattern_has_legacy_defaults_and_refines_independently() {
1959        let mut style = Style::default();
1960
1961        assert_eq!(
1962            style.border_dashed_length,
1963            crate::scene::DEFAULT_BORDER_DASHED_LENGTH
1964        );
1965        assert_eq!(
1966            style.border_dashed_gap,
1967            crate::scene::DEFAULT_BORDER_DASHED_GAP
1968        );
1969
1970        style.refine(
1971            &StyleRefinement::default()
1972                .border_dashed_length(4.0)
1973                .border_dashed_gap(0.5),
1974        );
1975
1976        assert_eq!(style.border_dashed_length, 4.0);
1977        assert_eq!(style.border_dashed_gap, 0.5);
1978        assert_eq!(style.border_style, BorderStyle::Solid);
1979
1980        style.refine(&StyleRefinement::default().border_dashed());
1981        assert_eq!(style.border_style, BorderStyle::Dashed);
1982    }
1983
1984    #[test]
1985    fn outer_ring_is_hollow_and_composes_with_shadows() {
1986        let drop_shadow = BoxShadow::new(px(0.), px(2.), blue()).blur_radius(px(4.));
1987        let mut style = Style::default();
1988        style.refine(
1989            &StyleRefinement::default()
1990                .ring_2()
1991                .shadow(vec![drop_shadow.clone()]),
1992        );
1993        let current_color = hsla(0.7, 0.5, 0.4, 0.8);
1994        style.refine(&StyleRefinement::default().ring(px(3.5)));
1995        let element_bounds = Bounds {
1996            origin: point(px(10.), px(20.)),
1997            size: size(px(100.), px(40.)),
1998        };
1999        let element_radii = Corners {
2000            top_left: px(0.),
2001            top_right: px(4.),
2002            bottom_right: px(10.),
2003            bottom_left: px(20.),
2004        };
2005        let ring = style
2006            .ring
2007            .outer_quad(element_bounds, element_radii, current_color)
2008            .unwrap();
2009
2010        assert_eq!(ring.bounds.dilate(-px(3.5)), element_bounds);
2011        assert_eq!(ring.border_widths, Edges::all(px(3.5)));
2012        assert_eq!(
2013            ring.corner_radii.map(|radius| *radius - px(3.5)),
2014            element_radii
2015        );
2016        assert!(ring.background.is_transparent());
2017        assert_eq!(ring.border_color, current_color.into());
2018        assert_eq!(ring.border_style, BorderStyle::Solid);
2019        assert_eq!(style.box_shadow, vec![drop_shadow]);
2020
2021        let gradient = ring_gradient();
2022        style.refine(&StyleRefinement::default().ring_color(gradient));
2023        assert_eq!(
2024            style
2025                .ring
2026                .outer_quad(element_bounds, element_radii, current_color)
2027                .unwrap()
2028                .border_color,
2029            gradient
2030        );
2031
2032        style.refine(&StyleRefinement::default().ring_0());
2033        assert!(
2034            style
2035                .ring
2036                .outer_quad(element_bounds, element_radii, current_color)
2037                .is_none()
2038        );
2039        assert_eq!(style.box_shadow.len(), 1);
2040    }
2041
2042    #[test]
2043    fn inset_ring_utility_refines_independently_and_lowers_to_an_inset_shadow() {
2044        let gradient = ring_gradient();
2045        let mut style = Style::default();
2046        style.refine(
2047            &StyleRefinement::default()
2048                .inset_ring(px(1.))
2049                .inset_ring_color(gradient),
2050        );
2051        style.refine(&StyleRefinement::default().inset_ring_4());
2052
2053        let inset = style.inset_ring.inset_shadow(blue()).unwrap();
2054
2055        assert_eq!(inset.offset, point(px(0.), px(0.)));
2056        assert_eq!(inset.blur_radius, px(0.));
2057        assert_eq!(inset.spread_radius, px(4.));
2058        assert_eq!(inset.color, gradient);
2059        assert!(inset.inset);
2060        assert_eq!(style.ring, RingStyle::default());
2061    }
2062}