Skip to main content

gpui/
style.rs

1use crate::{
2    AbsoluteLength, App, Background, BackgroundTag, BorderStyle, Bounds, ColorExt, ContentMask,
3    Corners, CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement,
4    Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, GridLocation, Length, Pixels, Point,
5    PointRefinement, ScaledPixels, SharedString, Size, SizeRefinement, Styled, TextRun, Window,
6    black, phi, point, px, quad, 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/// Use this struct for interfacing with the 'debug_below' styling from your own elements.
16/// If a parent element has this style set on it, then this struct will be set as a global in
17/// GPUI.
18#[cfg(debug_assertions)]
19pub struct DebugBelow;
20
21#[cfg(debug_assertions)]
22impl crate::Global for DebugBelow {}
23
24/// How to fit the image into the bounds of the element.
25pub enum ObjectFit {
26    /// The image will be stretched to fill the bounds of the element.
27    Fill,
28    /// The image will be scaled to fit within the bounds of the element.
29    Contain,
30    /// The image will be scaled to cover the bounds of the element.
31    Cover,
32    /// The image will be scaled down to fit within the bounds of the element.
33    ScaleDown,
34    /// The image will maintain its original size.
35    None,
36}
37
38impl ObjectFit {
39    /// Get the bounds of the image within the given bounds.
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 column or row in a grid layout
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/// A simplified representation of the grid-template-* value
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/// The CSS styling that can be applied to an element via the `Styled` trait
174#[derive(Clone, Refineable, Debug)]
175#[refineable(Debug, PartialEq, Serialize, Deserialize)]
176pub struct Style {
177    /// What layout strategy should be used?
178    pub display: Display,
179
180    /// Should the element be painted on screen?
181    pub visibility: Visibility,
182
183    // Overflow properties
184    /// How children overflowing their container should affect layout
185    #[refineable]
186    pub overflow: Point<Overflow>,
187    /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes.
188    pub scrollbar_width: AbsoluteLength,
189    /// Whether both x and y axis should be scrollable at the same time.
190    pub allow_concurrent_scroll: bool,
191    /// Whether scrolling should be restricted to the input gesture's axis.
192    ///
193    /// Pixel-based scroll gestures are locked to their initially dominant axis. The lock may be
194    /// released when the gesture changes direction strongly. Touch phases delimit gestures when
195    /// available, with a timeout fallback for platforms that only emit moved events.
196    ///
197    /// This also prevents input from being remapped to another axis. For example, horizontal input
198    /// will not scroll a container that only has vertical overflow enabled. Mouse wheel platforms
199    /// typically report ordinary wheel input on the Y axis and Shift-modified input on the X axis.
200    ///
201    /// ## Motivation
202    ///
203    /// On the web when scrolling with the mouse wheel, scrolling up and down will always scroll the Y axis, even when
204    /// the mouse is over a horizontally-scrollable element.
205    ///
206    /// The only way to scroll horizontally is to hold down `Shift` while scrolling, which then changes the scroll axis
207    /// to the X axis.
208    ///
209    /// Currently, GPUI operates differently from the web in that it will scroll an element in either the X or Y axis
210    /// when scrolling with just the mouse wheel. This causes problems when scrolling in a vertical list that contains
211    /// horizontally-scrollable elements, as when you get to the horizontally-scrollable elements the scroll will be
212    /// hijacked.
213    ///
214    /// Ideally we would match the web's behavior and not have a need for this, but right now we're adding this opt-in
215    /// style property to limit the potential blast radius.
216    pub restrict_scroll_to_axis: bool,
217
218    // Position properties
219    /// What should the `position` value of this struct use as a base offset?
220    pub position: Position,
221    /// How should the position of this element be tweaked relative to the layout defined?
222    #[refineable]
223    pub inset: Edges<Length>,
224
225    // Size properties
226    /// Sets the initial size of the item
227    #[refineable]
228    pub size: Size<Length>,
229    /// Controls the minimum size of the item
230    #[refineable]
231    pub min_size: Size<Length>,
232    /// Controls the maximum size of the item
233    #[refineable]
234    pub max_size: Size<Length>,
235    /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height.
236    pub aspect_ratio: Option<f32>,
237
238    // Spacing Properties
239    /// How large should the margin be on each side?
240    #[refineable]
241    pub margin: Edges<Length>,
242    /// How large should the padding be on each side?
243    #[refineable]
244    pub padding: Edges<DefiniteLength>,
245    /// How large should the border be on each side?
246    #[refineable]
247    pub border_widths: Edges<AbsoluteLength>,
248
249    // Alignment properties
250    /// How this node's children aligned in the cross/block axis?
251    pub align_items: Option<AlignItems>,
252    /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set
253    pub align_self: Option<AlignSelf>,
254    /// How should content contained within this item be aligned in the cross/block axis
255    pub align_content: Option<AlignContent>,
256    /// How should contained within this item be aligned in the main/inline axis
257    pub justify_content: Option<JustifyContent>,
258    /// How large should the gaps between items in a flex container be?
259    #[refineable]
260    pub gap: Size<DefiniteLength>,
261
262    // Flexbox properties
263    /// Which direction does the main axis flow in?
264    pub flex_direction: FlexDirection,
265    /// Should elements wrap, or stay in a single line?
266    pub flex_wrap: FlexWrap,
267    /// Sets the initial main axis size of the item
268    pub flex_basis: Length,
269    /// 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.
270    pub flex_grow: f32,
271    /// 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.
272    pub flex_shrink: f32,
273
274    /// The fill color of this element
275    pub background: Option<Fill>,
276
277    /// The border color of this element
278    pub border_color: Option<Hsla>,
279
280    /// The border style of this element
281    pub border_style: BorderStyle,
282
283    /// The radius of the corners of this element
284    #[refineable]
285    pub corner_radii: Corners<AbsoluteLength>,
286
287    /// Box shadow of the element
288    pub box_shadow: Vec<BoxShadow>,
289
290    /// Filters applied to this element's own content and children (CSS `filter`).
291    pub filter: Vec<Filter>,
292
293    /// Filters applied to the content rendered behind this element (CSS `backdrop-filter`).
294    pub backdrop_filter: Vec<Filter>,
295
296    /// The text style of this element
297    #[refineable]
298    pub text: TextStyleRefinement,
299
300    /// The mouse cursor style shown when the mouse pointer is over an element.
301    pub mouse_cursor: Option<CursorStyle>,
302
303    /// The opacity of this element
304    pub opacity: Option<f32>,
305
306    /// The grid columns of this element
307    /// Roughly equivalent to the Tailwind `grid-cols-<number>`
308    pub grid_cols: Option<GridTemplate>,
309
310    /// The row span of this element
311    /// Equivalent to the Tailwind `grid-rows-<number>`
312    pub grid_rows: Option<GridTemplate>,
313
314    /// The grid location of this element
315    pub grid_location: Option<GridLocation>,
316
317    /// Whether to draw a red debugging outline around this element
318    #[cfg(debug_assertions)]
319    pub debug: bool,
320
321    /// Whether to draw a red debugging outline around this element and all of its conforming children
322    #[cfg(debug_assertions)]
323    pub debug_below: bool,
324}
325
326impl Styled for StyleRefinement {
327    fn style(&mut self) -> &mut StyleRefinement {
328        self
329    }
330}
331
332impl StyleRefinement {
333    /// The grid location of this element
334    pub fn grid_location_mut(&mut self) -> &mut GridLocation {
335        self.grid_location.get_or_insert_default()
336    }
337}
338
339/// The value of the visibility property, similar to the CSS property `visibility`
340#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
341pub enum Visibility {
342    /// The element should be drawn as normal.
343    #[default]
344    Visible,
345    /// The element should not be drawn, but should still take up space in the layout.
346    Hidden,
347}
348
349/// The possible values of the box-shadow property
350#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
351pub struct BoxShadow {
352    /// What color should the shadow have?
353    pub color: Hsla,
354    /// How should it be offset from its element?
355    pub offset: Point<Pixels>,
356    /// How much should the shadow be blurred?
357    pub blur_radius: Pixels,
358    /// How much should the shadow spread?
359    pub spread_radius: Pixels,
360    /// Whether this is an inset shadow (drawn inside the element's bounds).
361    pub inset: bool,
362}
363
364impl BoxShadow {
365    /// Creates a new [`BoxShadow`] with the given offset and color, matching the order
366    /// of the CSS `box-shadow` property. Use the builder methods to set blur radius,
367    /// spread radius, and inset.
368    pub fn new(offset_x: Pixels, offset_y: Pixels, color: Hsla) -> Self {
369        Self {
370            color,
371            offset: point(offset_x, offset_y),
372            blur_radius: px(0.),
373            spread_radius: px(0.),
374            inset: false,
375        }
376    }
377
378    /// Sets the shadow blur radius.
379    pub fn blur_radius(mut self, blur_radius: Pixels) -> Self {
380        self.blur_radius = blur_radius;
381        self
382    }
383
384    /// Sets the shadow spread radius.
385    pub fn spread_radius(mut self, spread_radius: Pixels) -> Self {
386        self.spread_radius = spread_radius;
387        self
388    }
389
390    /// Marks the shadow as inset (drawn inside the element's bounds).
391    pub fn inset(mut self) -> Self {
392        self.inset = true;
393        self
394    }
395}
396
397/// A graphical filter that can be applied either to an element's own content
398/// (via [`Styled::filter`], like CSS `filter`) or to the content rendered behind
399/// it (via [`Styled::backdrop_filter`], like CSS `backdrop-filter`).
400#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
401pub enum Filter {
402    /// A gaussian blur with the given radius, in logical pixels. Maps to CSS `blur(<px>)`.
403    Blur(Pixels),
404}
405
406impl Filter {
407    /// Whether this filter has no visible effect, so painting can skip it entirely (and the
408    /// element can avoid the offscreen isolation pass when *all* of its filters are identities).
409    ///
410    /// Each variant declares its own no-op case here rather than the pipeline special-casing
411    /// blur — adding a filter that this returns `true` for is silently dropped before it ever
412    /// reaches the renderer.
413    pub fn is_identity(&self) -> bool {
414        match self {
415            Filter::Blur(radius) => *radius <= Pixels::ZERO,
416        }
417    }
418
419    /// Lower this logical-pixel filter into its scene-space ([`ScaledFilter`]) form for the
420    /// renderer, scaling any pixel magnitudes by `factor` (the window scale factor).
421    pub fn scale(&self, factor: f32) -> ScaledFilter {
422        match self {
423            Filter::Blur(radius) => ScaledFilter::Blur(radius.scale(factor)),
424        }
425    }
426}
427
428/// The scene-space (device-pixel) form of a [`Filter`], carried on the scene primitives that the
429/// renderers consume. Produced by [`Filter::scale`]; pixel magnitudes are in [`ScaledPixels`].
430///
431/// This is intentionally a separate enum from [`Filter`] (rather than reusing it) so the scene
432/// stays in device space like every other primitive, and so the renderers `match` on it
433/// exhaustively — adding a filter variant breaks each backend's match, forcing a deliberate
434/// implement-or-decline decision per backend instead of silently rendering nothing.
435#[derive(Clone, Copy, Debug, PartialEq)]
436pub enum ScaledFilter {
437    /// A gaussian blur with the given radius, in scaled (device) pixels.
438    Blur(ScaledPixels),
439}
440
441/// How to handle whitespace in text
442#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
443pub enum WhiteSpace {
444    /// Normal line wrapping when text overflows the width of the element
445    #[default]
446    Normal,
447    /// No line wrapping, text will overflow the width of the element
448    Nowrap,
449}
450
451/// How to truncate text that overflows the width of the element
452#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
453pub enum TextOverflow {
454    /// Truncate the text at the end when it doesn't fit, and represent this truncation by
455    /// displaying the provided string (e.g., "very long te…").
456    Truncate(SharedString),
457    /// Truncate the text at the start when it doesn't fit, and represent this truncation by
458    /// displaying the provided string at the beginning (e.g., "…ong text here").
459    /// Typically more adequate for file paths where the end is more important than the beginning.
460    TruncateStart(SharedString),
461    /// Truncate the text in the middle when it doesn't fit, preserving both the start and end
462    /// of the string (e.g., "long fi…name.rs"). Useful for filenames where both the prefix
463    /// and the extension are important context.
464    TruncateMiddle(SharedString),
465}
466
467/// How to align text within the element
468#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
469pub enum TextAlign {
470    /// Align the text to the left of the element
471    #[default]
472    Left,
473
474    /// Center the text within the element
475    Center,
476
477    /// Align the text to the right of the element
478    Right,
479}
480
481/// Case mapping applied at layout time while keeping **UTF-8 byte lengths** unchanged.
482///
483/// [`TextTransform::Uppercase`] and [`TextTransform::Lowercase`] use Unicode full case folding via
484/// [`char::to_uppercase`] / [`char::to_lowercase`]. If mapping a code point would change its UTF-8
485/// length, or would replace one scalar value with more than one character, the **original**
486/// character is kept. That keeps indices into the underlying buffer aligned with hit-testing and
487/// editor-style caret positions that use raw byte offsets.
488///
489/// [`TextTransform::Capitalize`] matches CSS / Tailwind [`capitalize`][tw-cap] using Unicode word
490/// boundaries: only the first alphabetic code point in each word is uppercased; other characters
491/// are unchanged.
492///
493/// [tw-cap]: https://tailwindcss.com/docs/text-transform
494#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
495pub enum TextTransform {
496    /// Do not transform text.
497    #[default]
498    None,
499    /// Uppercase text (Unicode, byte-length preserving — see [`TextTransform`]).
500    Uppercase,
501    /// Lowercase text (Unicode, byte-length preserving — see [`TextTransform`]).
502    Lowercase,
503    /// `text-transform: capitalize` semantics (Tailwind class `capitalize`): per Unicode word, only
504    /// the first alphabetic character is mapped to uppercase; the rest of the string is unchanged.
505    Capitalize,
506}
507
508/// The properties that can be used to style text in GPUI
509#[derive(Refineable, Clone, Debug, PartialEq)]
510#[refineable(Debug, PartialEq, Serialize, Deserialize)]
511pub struct TextStyle {
512    /// The color of the text
513    pub color: Hsla,
514
515    /// The font family to use
516    pub font_family: SharedString,
517
518    /// The font features to use
519    pub font_features: FontFeatures,
520
521    /// The fallback fonts to use
522    pub font_fallbacks: Option<FontFallbacks>,
523
524    /// The font size to use, in pixels or rems.
525    pub font_size: AbsoluteLength,
526
527    /// The line height to use, in pixels or fractions
528    pub line_height: DefiniteLength,
529
530    /// The font weight, e.g. bold
531    pub font_weight: FontWeight,
532
533    /// The font style, e.g. italic
534    pub font_style: FontStyle,
535
536    /// The background color of the text
537    pub background_color: Option<Hsla>,
538
539    /// The underline style of the text
540    pub underline: Option<UnderlineStyle>,
541
542    /// The strikethrough style of the text
543    pub strikethrough: Option<StrikethroughStyle>,
544
545    /// How to handle whitespace in the text
546    pub white_space: WhiteSpace,
547
548    /// The text should be truncated if it overflows the width of the element
549    pub text_overflow: Option<TextOverflow>,
550
551    /// How the text should be aligned within the element
552    pub text_align: TextAlign,
553
554    /// The number of lines to display before truncating the text
555    pub line_clamp: Option<usize>,
556
557    /// Letter spacing added between characters, in pixels (positive widens, negative tightens).
558    ///
559    /// The platform text stack may clamp values outside the range it supports.
560    pub letter_spacing: Option<Pixels>,
561
562    /// Case transformation applied at layout time.
563    pub text_transform: Option<TextTransform>,
564}
565
566impl Default for TextStyle {
567    fn default() -> Self {
568        TextStyle {
569            color: black(),
570            // todo(linux) make this configurable or choose better default
571            font_family: ".SystemUIFont".into(),
572            font_features: FontFeatures::default(),
573            font_fallbacks: None,
574            font_size: rems(1.).into(),
575            line_height: phi(),
576            font_weight: FontWeight::default(),
577            font_style: FontStyle::default(),
578            background_color: None,
579            underline: None,
580            strikethrough: None,
581            white_space: WhiteSpace::Normal,
582            text_overflow: None,
583            text_align: TextAlign::default(),
584            line_clamp: None,
585            letter_spacing: None,
586            text_transform: None,
587        }
588    }
589}
590
591impl TextStyle {
592    /// Create a new text style with the given highlighting applied.
593    pub fn highlight(mut self, style: impl Into<HighlightStyle>) -> Self {
594        let style = style.into();
595        if let Some(weight) = style.font_weight {
596            self.font_weight = weight;
597        }
598        if let Some(style) = style.font_style {
599            self.font_style = style;
600        }
601
602        if let Some(color) = style.color {
603            self.color = self.color.blend(&color);
604        }
605
606        if let Some(factor) = style.fade_out {
607            self.color.fade_out(factor);
608        }
609
610        if let Some(background_color) = style.background_color {
611            self.background_color = Some(background_color);
612        }
613
614        if let Some(underline) = style.underline {
615            self.underline = Some(underline);
616        }
617
618        if let Some(strikethrough) = style.strikethrough {
619            self.strikethrough = Some(strikethrough);
620        }
621
622        self
623    }
624
625    /// Get the font configured for this text style.
626    pub fn font(&self) -> Font {
627        Font {
628            family: self.font_family.clone(),
629            features: self.font_features.clone(),
630            fallbacks: self.font_fallbacks.clone(),
631            weight: self.font_weight,
632            style: self.font_style,
633        }
634    }
635
636    /// Returns the rounded line height in pixels.
637    pub fn line_height_in_pixels(&self, rem_size: Pixels) -> Pixels {
638        self.line_height.to_pixels(self.font_size, rem_size).round()
639    }
640
641    /// Convert this text style into a [`TextRun`], for the given length of the text.
642    pub fn to_run(&self, len: usize) -> TextRun {
643        TextRun {
644            len,
645            font: Font {
646                family: self.font_family.clone(),
647                features: self.font_features.clone(),
648                fallbacks: self.font_fallbacks.clone(),
649                weight: self.font_weight,
650                style: self.font_style,
651            },
652            color: self.color,
653            background_color: self.background_color,
654            underline: self.underline,
655            strikethrough: self.strikethrough,
656            letter_spacing: self.letter_spacing,
657        }
658    }
659}
660
661/// A highlight style to apply, similar to a `TextStyle` except
662/// for a single font, uniformly sized and spaced text.
663///
664/// Layout extras on the base [`TextStyle`] — such as [`TextStyle::letter_spacing`] and
665/// [`TextStyle::text_transform`] — are not stored here; highlighted segments inherit them
666/// from the surrounding style (see [`TextStyle::highlight`] and
667/// [`crate::StyledText::with_default_highlights`]).
668#[derive(Copy, Clone, Debug, Default, PartialEq)]
669pub struct HighlightStyle {
670    /// The color of the text
671    pub color: Option<Hsla>,
672
673    /// The font weight, e.g. bold
674    pub font_weight: Option<FontWeight>,
675
676    /// The font style, e.g. italic
677    pub font_style: Option<FontStyle>,
678
679    /// The background color of the text
680    pub background_color: Option<Hsla>,
681
682    /// The underline style of the text
683    pub underline: Option<UnderlineStyle>,
684
685    /// The underline style of the text
686    pub strikethrough: Option<StrikethroughStyle>,
687
688    /// Similar to the CSS `opacity` property, this will cause the text to be less vibrant.
689    pub fade_out: Option<f32>,
690}
691
692impl Style {
693    /// Returns true if the style is visible and the background is opaque.
694    pub fn has_opaque_background(&self) -> bool {
695        self.background
696            .as_ref()
697            .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent()))
698    }
699
700    /// Get the text style in this element style.
701    pub fn text_style(&self) -> Option<&TextStyleRefinement> {
702        if self.text.is_some() {
703            Some(&self.text)
704        } else {
705            None
706        }
707    }
708
709    /// Get the content mask for this element style, based on the given bounds.
710    /// If the element does not hide its overflow, this will return `None`.
711    pub fn overflow_mask(
712        &self,
713        bounds: Bounds<Pixels>,
714        rem_size: Pixels,
715    ) -> Option<ContentMask<Pixels>> {
716        match self.overflow {
717            Point {
718                x: Overflow::Visible,
719                y: Overflow::Visible,
720            } => None,
721            _ => {
722                let mut min = bounds.origin;
723                let mut max = bounds.bottom_right();
724
725                if self.border_color.is_some_and(|color| color.alpha > 0.) {
726                    min.x += self.border_widths.left.to_pixels(rem_size);
727                    max.x -= self.border_widths.right.to_pixels(rem_size);
728                    min.y += self.border_widths.top.to_pixels(rem_size);
729                    max.y -= self.border_widths.bottom.to_pixels(rem_size);
730                }
731
732                let bounds = match (
733                    self.overflow.x == Overflow::Visible,
734                    self.overflow.y == Overflow::Visible,
735                ) {
736                    // x and y both visible
737                    (true, true) => return None,
738                    // x visible, y hidden
739                    (true, false) => Bounds::from_corners(
740                        point(min.x, bounds.origin.y),
741                        point(max.x, bounds.bottom_right().y),
742                    ),
743                    // x hidden, y visible
744                    (false, true) => Bounds::from_corners(
745                        point(bounds.origin.x, min.y),
746                        point(bounds.bottom_right().x, max.y),
747                    ),
748                    // both hidden
749                    (false, false) => Bounds::from_corners(min, max),
750                };
751
752                Some(ContentMask { bounds })
753            }
754        }
755    }
756
757    /// Paints the background of an element styled with this style.
758    pub fn paint(
759        &self,
760        bounds: Bounds<Pixels>,
761        window: &mut Window,
762        cx: &mut App,
763        continuation: impl FnOnce(&mut Window, &mut App),
764    ) {
765        #[cfg(debug_assertions)]
766        if self.debug_below {
767            cx.set_global(DebugBelow)
768        }
769
770        #[cfg(debug_assertions)]
771        if self.debug || cx.has_global::<DebugBelow>() {
772            window.paint_quad(crate::outline(bounds, crate::red(), BorderStyle::default()));
773        }
774
775        let rem_size = window.rem_size();
776        let corner_radii = self
777            .corner_radii
778            .to_pixels(rem_size)
779            .clamp_radii_for_quad_size(bounds.size);
780
781        window.paint_drop_shadows(bounds, corner_radii, &self.box_shadow);
782
783        // Blur the content behind this element before its (typically translucent) background
784        // is painted on top, so the background tints the frosted backdrop (CSS `backdrop-filter`).
785        if !self.backdrop_filter.is_empty() {
786            window.paint_backdrop_filter(bounds, corner_radii, &self.backdrop_filter);
787        }
788
789        // The element's own box — background, inset shadows, children, and border — painted as a
790        // unit. A `filter` (CSS `filter`) wraps this whole unit so the renderer blurs the element
791        // and its children together as one group; without a filter it paints directly.
792        let paint_box = |window: &mut Window, cx: &mut App| {
793            let background_color = self.background.as_ref().and_then(Fill::color);
794            if background_color.is_some_and(|color| !color.is_transparent()) {
795                let mut border_color = match background_color {
796                    Some(color) => match color.tag {
797                        BackgroundTag::Solid
798                        | BackgroundTag::PatternSlash
799                        | BackgroundTag::Checkerboard => color.solid.into(),
800
801                        BackgroundTag::LinearGradient => color
802                            .colors
803                            .first()
804                            .map(|stop| stop.color.into())
805                            .unwrap_or_default(),
806                    },
807                    None => Hsla::default(),
808                };
809                border_color.alpha = 0.;
810                window.paint_quad(quad(
811                    bounds,
812                    corner_radii,
813                    background_color.unwrap_or_default(),
814                    Edges::default(),
815                    border_color,
816                    self.border_style,
817                ));
818            }
819
820            window.paint_inset_shadows(bounds, corner_radii, &self.box_shadow);
821
822            continuation(window, cx);
823
824            if self.is_border_visible() {
825                let border_widths = self.border_widths.to_pixels(rem_size);
826                let mut background = self.border_color.unwrap_or_default();
827                background.alpha = 0.;
828                window.paint_quad(quad(
829                    bounds,
830                    corner_radii,
831                    background,
832                    border_widths,
833                    self.border_color.unwrap_or_default(),
834                    self.border_style,
835                ));
836            }
837        };
838
839        if self.filter.is_empty() {
840            paint_box(window, cx);
841        } else {
842            window.with_filter_layer(bounds, corner_radii, &self.filter, |window| {
843                paint_box(window, cx);
844            });
845        }
846
847        #[cfg(debug_assertions)]
848        if self.debug_below {
849            cx.remove_global::<DebugBelow>();
850        }
851    }
852
853    fn is_border_visible(&self) -> bool {
854        self.border_color.is_some_and(|color| color.alpha > 0.)
855            && self.border_widths.any(|length| !length.is_zero())
856    }
857}
858
859impl Default for Style {
860    fn default() -> Self {
861        Style {
862            display: Display::Block,
863            visibility: Visibility::Visible,
864            overflow: Point {
865                x: Overflow::Visible,
866                y: Overflow::Visible,
867            },
868            allow_concurrent_scroll: false,
869            restrict_scroll_to_axis: false,
870            scrollbar_width: AbsoluteLength::default(),
871            position: Position::Relative,
872            inset: Edges::auto(),
873            margin: Edges::<Length>::zero(),
874            padding: Edges::<DefiniteLength>::zero(),
875            border_widths: Edges::<AbsoluteLength>::zero(),
876            size: Size::auto(),
877            min_size: Size::auto(),
878            max_size: Size::auto(),
879            aspect_ratio: None,
880            gap: Size::default(),
881            // Alignment
882            align_items: None,
883            align_self: None,
884            align_content: None,
885            justify_content: None,
886            // Flexbox
887            flex_direction: FlexDirection::Row,
888            flex_wrap: FlexWrap::NoWrap,
889            flex_grow: 0.0,
890            flex_shrink: 1.0,
891            flex_basis: Length::Auto,
892            background: None,
893            border_color: None,
894            border_style: BorderStyle::default(),
895            corner_radii: Corners::default(),
896            box_shadow: Default::default(),
897            filter: Default::default(),
898            backdrop_filter: Default::default(),
899            text: TextStyleRefinement::default(),
900            mouse_cursor: None,
901            opacity: None,
902            grid_rows: None,
903            grid_cols: None,
904            grid_location: None,
905
906            #[cfg(debug_assertions)]
907            debug: false,
908            #[cfg(debug_assertions)]
909            debug_below: false,
910        }
911    }
912}
913
914/// The properties that can be applied to an underline.
915#[derive(Refineable, Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
916pub struct UnderlineStyle {
917    /// The thickness of the underline.
918    pub thickness: Pixels,
919
920    /// The color of the underline.
921    pub color: Option<Hsla>,
922
923    /// Whether the underline should be wavy, like in a spell checker.
924    pub wavy: bool,
925}
926
927/// The properties that can be applied to a strikethrough.
928#[derive(Refineable, Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
929pub struct StrikethroughStyle {
930    /// The thickness of the strikethrough.
931    pub thickness: Pixels,
932
933    /// The color of the strikethrough.
934    pub color: Option<Hsla>,
935}
936
937/// The kinds of fill that can be applied to a shape.
938#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
939pub enum Fill {
940    /// A solid color fill.
941    Color(Background),
942}
943
944impl Fill {
945    /// Unwrap this fill into a solid color, if it is one.
946    ///
947    /// If the fill is not a solid color, this method returns `None`.
948    pub fn color(&self) -> Option<Background> {
949        match self {
950            Fill::Color(color) => Some(*color),
951        }
952    }
953}
954
955impl Default for Fill {
956    fn default() -> Self {
957        Self::Color(Background::default())
958    }
959}
960
961impl<T: Into<Background>> From<T> for Fill {
962    fn from(color: T) -> Self {
963        Self::Color(color.into())
964    }
965}
966
967impl From<TextStyle> for HighlightStyle {
968    fn from(other: TextStyle) -> Self {
969        Self::from(&other)
970    }
971}
972
973impl From<&TextStyle> for HighlightStyle {
974    fn from(other: &TextStyle) -> Self {
975        Self {
976            color: Some(other.color),
977            font_weight: Some(other.font_weight),
978            font_style: Some(other.font_style),
979            background_color: other.background_color,
980            underline: other.underline,
981            strikethrough: other.strikethrough,
982            fade_out: None,
983        }
984    }
985}
986
987impl HighlightStyle {
988    /// Create a highlight style with just a color
989    pub fn color(color: Hsla) -> Self {
990        Self {
991            color: Some(color),
992            ..Default::default()
993        }
994    }
995    /// Blend this highlight style with another.
996    /// Non-continuous properties, like font_weight and font_style, are overwritten.
997    #[must_use]
998    pub fn highlight(self, other: HighlightStyle) -> Self {
999        Self {
1000            color: other
1001                .color
1002                .map(|other_color| {
1003                    if let Some(color) = self.color {
1004                        color.blend(&other_color)
1005                    } else {
1006                        other_color
1007                    }
1008                })
1009                .or(self.color),
1010            font_weight: other.font_weight.or(self.font_weight),
1011            font_style: other.font_style.or(self.font_style),
1012            background_color: other.background_color.or(self.background_color),
1013            underline: other.underline.or(self.underline),
1014            strikethrough: other.strikethrough.or(self.strikethrough),
1015            fade_out: other
1016                .fade_out
1017                .map(|source_fade| {
1018                    self.fade_out
1019                        .map(|dest_fade| (dest_fade * (1. + source_fade)).clamp(0., 1.))
1020                        .unwrap_or(source_fade)
1021                })
1022                .or(self.fade_out),
1023        }
1024    }
1025}
1026
1027impl From<Hsla> for HighlightStyle {
1028    fn from(color: Hsla) -> Self {
1029        Self {
1030            color: Some(color),
1031            ..Default::default()
1032        }
1033    }
1034}
1035
1036impl From<FontWeight> for HighlightStyle {
1037    fn from(font_weight: FontWeight) -> Self {
1038        Self {
1039            font_weight: Some(font_weight),
1040            ..Default::default()
1041        }
1042    }
1043}
1044
1045impl From<FontStyle> for HighlightStyle {
1046    fn from(font_style: FontStyle) -> Self {
1047        Self {
1048            font_style: Some(font_style),
1049            ..Default::default()
1050        }
1051    }
1052}
1053
1054impl From<Rgba> for HighlightStyle {
1055    fn from(color: Rgba) -> Self {
1056        Self {
1057            color: Some(color.into_color()),
1058            ..Default::default()
1059        }
1060    }
1061}
1062
1063/// Combine and merge the highlights and ranges in the two iterators.
1064pub fn combine_highlights(
1065    a: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
1066    b: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
1067) -> impl Iterator<Item = (Range<usize>, HighlightStyle)> {
1068    let mut endpoints = Vec::new();
1069    let mut highlights = Vec::new();
1070    for (range, highlight) in a.into_iter().chain(b) {
1071        if !range.is_empty() {
1072            let highlight_id = highlights.len();
1073            endpoints.push((range.start, highlight_id, true));
1074            endpoints.push((range.end, highlight_id, false));
1075            highlights.push(highlight);
1076        }
1077    }
1078    endpoints.sort_unstable_by_key(|(position, _, _)| *position);
1079    let mut endpoints = endpoints.into_iter().peekable();
1080
1081    let mut active_styles = HashSet::default();
1082    let mut ix = 0;
1083    iter::from_fn(move || {
1084        while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() {
1085            let prev_index = mem::replace(&mut ix, *endpoint_ix);
1086            if ix > prev_index && !active_styles.is_empty() {
1087                let current_style = active_styles
1088                    .iter()
1089                    .fold(HighlightStyle::default(), |acc, highlight_id| {
1090                        acc.highlight(highlights[*highlight_id])
1091                    });
1092                return Some((prev_index..ix, current_style));
1093            }
1094
1095            if *is_start {
1096                active_styles.insert(*highlight_id);
1097            } else {
1098                active_styles.remove(highlight_id);
1099            }
1100            endpoints.next();
1101        }
1102        None
1103    })
1104}
1105
1106/// Used to control how child nodes are aligned.
1107/// For Flexbox it controls alignment in the cross axis
1108/// For Grid it controls alignment in the block axis
1109///
1110/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items)
1111#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
1112// Copy of taffy::style type of the same name, to derive JsonSchema.
1113pub enum AlignItems {
1114    /// Items are packed toward the start of the axis
1115    Start,
1116    /// Items are packed toward the end of the axis
1117    End,
1118    /// Items are packed towards the flex-relative start of the axis.
1119    ///
1120    /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
1121    /// to End. In all other cases it is equivalent to Start.
1122    FlexStart,
1123    /// Items are packed towards the flex-relative end of the axis.
1124    ///
1125    /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
1126    /// to Start. In all other cases it is equivalent to End.
1127    FlexEnd,
1128    /// Items are packed along the center of the cross axis
1129    Center,
1130    /// Items are aligned such as their baselines align
1131    Baseline,
1132    /// Stretch to fill the container
1133    Stretch,
1134}
1135/// Used to control how child nodes are aligned.
1136/// Does not apply to Flexbox, and will be ignored if specified on a flex container
1137/// For Grid it controls alignment in the inline axis
1138///
1139/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-items)
1140pub type JustifyItems = AlignItems;
1141/// Used to control how the specified nodes is aligned.
1142/// Overrides the parent Node's `AlignItems` property.
1143/// For Flexbox it controls alignment in the cross axis
1144/// For Grid it controls alignment in the block axis
1145///
1146/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-self)
1147pub type AlignSelf = AlignItems;
1148/// Used to control how the specified nodes is aligned.
1149/// Overrides the parent Node's `JustifyItems` property.
1150/// Does not apply to Flexbox, and will be ignored if specified on a flex child
1151/// For Grid it controls alignment in the inline axis
1152///
1153/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-self)
1154pub type JustifySelf = AlignItems;
1155
1156/// Sets the distribution of space between and around content items
1157/// For Flexbox it controls alignment in the cross axis
1158/// For Grid it controls alignment in the block axis
1159///
1160/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-content)
1161#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
1162// Copy of taffy::style type of the same name, to derive JsonSchema.
1163pub enum AlignContent {
1164    /// Items are packed toward the start of the axis
1165    Start,
1166    /// Items are packed toward the end of the axis
1167    End,
1168    /// Items are packed towards the flex-relative start of the axis.
1169    ///
1170    /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
1171    /// to End. In all other cases it is equivalent to Start.
1172    FlexStart,
1173    /// Items are packed towards the flex-relative end of the axis.
1174    ///
1175    /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
1176    /// to Start. In all other cases it is equivalent to End.
1177    FlexEnd,
1178    /// Items are centered around the middle of the axis
1179    Center,
1180    /// Items are stretched to fill the container
1181    Stretch,
1182    /// The first and last items are aligned flush with the edges of the container (no gap)
1183    /// The gap between items is distributed evenly.
1184    SpaceBetween,
1185    /// The gap between the first and last items is exactly THE SAME as the gap between items.
1186    /// The gaps are distributed evenly
1187    SpaceEvenly,
1188    /// The gap between the first and last items is exactly HALF the gap between items.
1189    /// The gaps are distributed evenly in proportion to these ratios.
1190    SpaceAround,
1191}
1192
1193/// Sets the distribution of space between and around content items
1194/// For Flexbox it controls alignment in the main axis
1195/// For Grid it controls alignment in the inline axis
1196///
1197/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content)
1198pub type JustifyContent = AlignContent;
1199
1200/// Sets the layout used for the children of this node
1201///
1202/// The default values depends on on which feature flags are enabled. The order of precedence is: Flex, Grid, Block, None.
1203#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1204// Copy of taffy::style type of the same name, to derive JsonSchema.
1205pub enum Display {
1206    /// The children will follow the block layout algorithm
1207    Block,
1208    /// The children will follow the flexbox layout algorithm
1209    #[default]
1210    Flex,
1211    /// The children will follow the CSS Grid layout algorithm
1212    Grid,
1213    /// The children will not be laid out, and will follow absolute positioning
1214    None,
1215}
1216
1217/// Controls whether flex items are forced onto one line or can wrap onto multiple lines.
1218///
1219/// Defaults to [`FlexWrap::NoWrap`]
1220///
1221/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-wrap-property)
1222#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1223// Copy of taffy::style type of the same name, to derive JsonSchema.
1224pub enum FlexWrap {
1225    /// Items will not wrap and stay on a single line
1226    #[default]
1227    NoWrap,
1228    /// Items will wrap according to this item's [`FlexDirection`]
1229    Wrap,
1230    /// Items will wrap in the opposite direction to this item's [`FlexDirection`]
1231    WrapReverse,
1232}
1233
1234/// The direction of the flexbox layout main axis.
1235///
1236/// There are always two perpendicular layout axes: main (or primary) and cross (or secondary).
1237/// Adding items will cause them to be positioned adjacent to each other along the main axis.
1238/// By varying this value throughout your tree, you can create complex axis-aligned layouts.
1239///
1240/// Items are always aligned relative to the cross axis, and justified relative to the main axis.
1241///
1242/// The default behavior is [`FlexDirection::Row`].
1243///
1244/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-direction-property)
1245#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1246// Copy of taffy::style type of the same name, to derive JsonSchema.
1247pub enum FlexDirection {
1248    /// Defines +x as the main axis
1249    ///
1250    /// Items will be added from left to right in a row.
1251    #[default]
1252    Row,
1253    /// Defines +y as the main axis
1254    ///
1255    /// Items will be added from top to bottom in a column.
1256    Column,
1257    /// Defines -x as the main axis
1258    ///
1259    /// Items will be added from right to left in a row.
1260    RowReverse,
1261    /// Defines -y as the main axis
1262    ///
1263    /// Items will be added from bottom to top in a column.
1264    ColumnReverse,
1265}
1266
1267/// How children overflowing their container should affect layout
1268///
1269/// In CSS the primary effect of this property is to control whether contents of a parent container that overflow that container should
1270/// be displayed anyway, be clipped, or trigger the container to become a scroll container. However it also has secondary effects on layout,
1271/// the main ones being:
1272///
1273///   - The automatic minimum size Flexbox/CSS Grid items with non-`Visible` overflow is `0` rather than being content based
1274///   - `Overflow::Scroll` nodes have space in the layout reserved for a scrollbar (width controlled by the `scrollbar_width` property)
1275///
1276/// In Taffy, we only implement the layout related secondary effects as we are not concerned with drawing/painting. The amount of space reserved for
1277/// a scrollbar is controlled by the `scrollbar_width` property. If this is `0` then `Scroll` behaves identically to `Hidden`.
1278///
1279/// <https://developer.mozilla.org/en-US/docs/Web/CSS/overflow>
1280#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1281// Copy of taffy::style type of the same name, to derive JsonSchema.
1282pub enum Overflow {
1283    /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content.
1284    /// Content that overflows this node *should* contribute to the scroll region of its parent.
1285    #[default]
1286    Visible,
1287    /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content.
1288    /// Content that overflows this node should *not* contribute to the scroll region of its parent.
1289    Clip,
1290    /// The automatic minimum size of this node as a flexbox/grid item should be `0`.
1291    /// Content that overflows this node should *not* contribute to the scroll region of its parent.
1292    Hidden,
1293    /// The automatic minimum size of this node as a flexbox/grid item should be `0`. Additionally, space should be reserved
1294    /// for a scrollbar. The amount of space reserved is controlled by the `scrollbar_width` property.
1295    /// Content that overflows this node should *not* contribute to the scroll region of its parent.
1296    Scroll,
1297}
1298
1299/// The positioning strategy for this item.
1300///
1301/// This controls both how the origin is determined for the [`Style::position`] field,
1302/// and whether or not the item will be controlled by flexbox's layout algorithm.
1303///
1304/// WARNING: this enum follows the behavior of [CSS's `position` property](https://developer.mozilla.org/en-US/docs/Web/CSS/position),
1305/// which can be unintuitive.
1306///
1307/// [`Position::Relative`] is the default value, in contrast to the default behavior in CSS.
1308#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1309// Copy of taffy::style type of the same name, to derive JsonSchema.
1310pub enum Position {
1311    /// The offset is computed relative to the final position given by the layout algorithm.
1312    /// Offsets do not affect the position of any other items; they are effectively a correction factor applied at the end.
1313    #[default]
1314    Relative,
1315    /// The offset is computed relative to this item's closest positioned ancestor, if any.
1316    /// Otherwise, it is placed relative to the origin.
1317    /// No space is created for the item in the page layout, and its size will not be altered.
1318    ///
1319    /// WARNING: to opt-out of layouting entirely, you must use [`Display::None`] instead on your [`Style`] object.
1320    Absolute,
1321}
1322
1323impl From<AlignItems> for taffy::style::AlignItems {
1324    fn from(value: AlignItems) -> Self {
1325        match value {
1326            AlignItems::Start => Self::START,
1327            AlignItems::End => Self::END,
1328            AlignItems::FlexStart => Self::FLEX_START,
1329            AlignItems::FlexEnd => Self::FLEX_END,
1330            AlignItems::Center => Self::CENTER,
1331            AlignItems::Baseline => Self::BASELINE,
1332            AlignItems::Stretch => Self::STRETCH,
1333        }
1334    }
1335}
1336
1337impl From<AlignContent> for taffy::style::AlignContent {
1338    fn from(value: AlignContent) -> Self {
1339        match value {
1340            AlignContent::Start => Self::START,
1341            AlignContent::End => Self::END,
1342            AlignContent::FlexStart => Self::FLEX_START,
1343            AlignContent::FlexEnd => Self::FLEX_END,
1344            AlignContent::Center => Self::CENTER,
1345            AlignContent::Stretch => Self::STRETCH,
1346            AlignContent::SpaceBetween => Self::SPACE_BETWEEN,
1347            AlignContent::SpaceEvenly => Self::SPACE_EVENLY,
1348            AlignContent::SpaceAround => Self::SPACE_AROUND,
1349        }
1350    }
1351}
1352
1353impl From<Display> for taffy::style::Display {
1354    fn from(value: Display) -> Self {
1355        match value {
1356            Display::Block => Self::Block,
1357            Display::Flex => Self::Flex,
1358            Display::Grid => Self::Grid,
1359            Display::None => Self::None,
1360        }
1361    }
1362}
1363
1364impl From<FlexWrap> for taffy::style::FlexWrap {
1365    fn from(value: FlexWrap) -> Self {
1366        match value {
1367            FlexWrap::NoWrap => Self::NoWrap,
1368            FlexWrap::Wrap => Self::Wrap,
1369            FlexWrap::WrapReverse => Self::WrapReverse,
1370        }
1371    }
1372}
1373
1374impl From<FlexDirection> for taffy::style::FlexDirection {
1375    fn from(value: FlexDirection) -> Self {
1376        match value {
1377            FlexDirection::Row => Self::Row,
1378            FlexDirection::Column => Self::Column,
1379            FlexDirection::RowReverse => Self::RowReverse,
1380            FlexDirection::ColumnReverse => Self::ColumnReverse,
1381        }
1382    }
1383}
1384
1385impl From<Overflow> for taffy::style::Overflow {
1386    fn from(value: Overflow) -> Self {
1387        match value {
1388            Overflow::Visible => Self::Visible,
1389            Overflow::Clip => Self::Clip,
1390            Overflow::Hidden => Self::Hidden,
1391            Overflow::Scroll => Self::Scroll,
1392        }
1393    }
1394}
1395
1396impl From<Position> for taffy::style::Position {
1397    fn from(value: Position) -> Self {
1398        match value {
1399            Position::Relative => Self::Relative,
1400            Position::Absolute => Self::Absolute,
1401        }
1402    }
1403}
1404
1405#[cfg(test)]
1406mod tests {
1407    use crate::{blue, green, px, red, yellow};
1408    use palette::WithAlpha;
1409
1410    use super::*;
1411
1412    #[test]
1413    fn test_basic_highlight_style_combination() {
1414        let style_a = HighlightStyle::default();
1415        let style_b = HighlightStyle::default();
1416        let style_a = style_a.highlight(style_b);
1417        assert_eq!(
1418            style_a,
1419            HighlightStyle::default(),
1420            "Combining empty styles should not produce a non-empty style."
1421        );
1422
1423        let mut style_b = HighlightStyle {
1424            color: Some(red()),
1425            strikethrough: Some(StrikethroughStyle {
1426                thickness: px(2.),
1427                color: Some(blue()),
1428            }),
1429            fade_out: Some(0.),
1430            font_style: Some(FontStyle::Italic),
1431            font_weight: Some(FontWeight(300.)),
1432            background_color: Some(yellow()),
1433            underline: Some(UnderlineStyle {
1434                thickness: px(2.),
1435                color: Some(red()),
1436                wavy: true,
1437            }),
1438        };
1439        let expected_style = style_b;
1440
1441        let style_a = style_a.highlight(style_b);
1442        assert_eq!(
1443            style_a, expected_style,
1444            "Blending an empty style with another style should return the other style"
1445        );
1446
1447        let style_b = style_b.highlight(Default::default());
1448        assert_eq!(
1449            style_b, expected_style,
1450            "Blending a style with an empty style should not change the style."
1451        );
1452
1453        let mut style_c = expected_style;
1454
1455        let style_d = HighlightStyle {
1456            color: Some(blue().with_alpha(0.7)),
1457            strikethrough: Some(StrikethroughStyle {
1458                thickness: px(4.),
1459                color: Some(crate::red()),
1460            }),
1461            fade_out: Some(0.),
1462            font_style: Some(FontStyle::Oblique),
1463            font_weight: Some(FontWeight(800.)),
1464            background_color: Some(green()),
1465            underline: Some(UnderlineStyle {
1466                thickness: px(4.),
1467                color: None,
1468                wavy: false,
1469            }),
1470        };
1471
1472        let expected_style = HighlightStyle {
1473            color: Some(red().blend(&blue().with_alpha(0.7))),
1474            strikethrough: Some(StrikethroughStyle {
1475                thickness: px(4.),
1476                color: Some(red()),
1477            }),
1478            // TODO this does not seem right
1479            fade_out: Some(0.),
1480            font_style: Some(FontStyle::Oblique),
1481            font_weight: Some(FontWeight(800.)),
1482            background_color: Some(green()),
1483            underline: Some(UnderlineStyle {
1484                thickness: px(4.),
1485                color: None,
1486                wavy: false,
1487            }),
1488        };
1489
1490        let style_c = style_c.highlight(style_d);
1491        assert_eq!(
1492            style_c, expected_style,
1493            "Blending styles should blend properties where possible and override all others"
1494        );
1495    }
1496
1497    #[test]
1498    fn test_combine_highlights() {
1499        assert_eq!(
1500            combine_highlights(
1501                [
1502                    (0..5, green().into()),
1503                    (4..10, FontWeight::BOLD.into()),
1504                    (15..20, yellow().into()),
1505                ],
1506                [
1507                    (2..6, FontStyle::Italic.into()),
1508                    (1..3, blue().into()),
1509                    (21..23, red().into()),
1510                ]
1511            )
1512            .collect::<Vec<_>>(),
1513            [
1514                (
1515                    0..1,
1516                    HighlightStyle {
1517                        color: Some(green()),
1518                        ..Default::default()
1519                    }
1520                ),
1521                (
1522                    1..2,
1523                    HighlightStyle {
1524                        color: Some(blue()),
1525                        ..Default::default()
1526                    }
1527                ),
1528                (
1529                    2..3,
1530                    HighlightStyle {
1531                        color: Some(blue()),
1532                        font_style: Some(FontStyle::Italic),
1533                        ..Default::default()
1534                    }
1535                ),
1536                (
1537                    3..4,
1538                    HighlightStyle {
1539                        color: Some(green()),
1540                        font_style: Some(FontStyle::Italic),
1541                        ..Default::default()
1542                    }
1543                ),
1544                (
1545                    4..5,
1546                    HighlightStyle {
1547                        color: Some(green()),
1548                        font_weight: Some(FontWeight::BOLD),
1549                        font_style: Some(FontStyle::Italic),
1550                        ..Default::default()
1551                    }
1552                ),
1553                (
1554                    5..6,
1555                    HighlightStyle {
1556                        font_weight: Some(FontWeight::BOLD),
1557                        font_style: Some(FontStyle::Italic),
1558                        ..Default::default()
1559                    }
1560                ),
1561                (
1562                    6..10,
1563                    HighlightStyle {
1564                        font_weight: Some(FontWeight::BOLD),
1565                        ..Default::default()
1566                    }
1567                ),
1568                (
1569                    15..20,
1570                    HighlightStyle {
1571                        color: Some(yellow()),
1572                        ..Default::default()
1573                    }
1574                ),
1575                (
1576                    21..23,
1577                    HighlightStyle {
1578                        color: Some(red()),
1579                        ..Default::default()
1580                    }
1581                )
1582            ]
1583        );
1584    }
1585
1586    #[test]
1587    fn test_text_style_refinement() {
1588        let mut style = Style::default();
1589        style.refine(&StyleRefinement::default().text_size(px(20.0)));
1590        style.refine(&StyleRefinement::default().font_weight(FontWeight::SEMIBOLD));
1591
1592        assert_eq!(
1593            Some(AbsoluteLength::from(px(20.0))),
1594            style.text_style().unwrap().font_size
1595        );
1596
1597        assert_eq!(
1598            Some(FontWeight::SEMIBOLD),
1599            style.text_style().unwrap().font_weight
1600        );
1601    }
1602}