Skip to main content

gpui_authoring/
style.rs

1use std::{
2    any::{Any, TypeId},
3    borrow::Cow,
4    fmt,
5    hash::{Hash, Hasher},
6    iter, mem,
7    ops::Range,
8    sync::Arc,
9};
10
11use crate::{
12    AbsoluteLength, AlignContent, AlignItems, AlignSelf, App, Background, BackgroundTag,
13    BorderStyle, Bounds, ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength,
14    DevicePixels, Display, Edges, EdgesRefinement, FlexDirection, FlexWrap, Font, FontFallbacks,
15    FontFeatures, FontStyle, FontWeight, GridLocation, GridTemplate, Hsla, JustifyContent, Length,
16    Overflow, Pixels, Point, PointRefinement, Position, Rgba, SharedString, Size, SizeRefinement,
17    StrikethroughStyle, Styled, TextRun, UnderlineStyle, Window, black, phi, point, px, quad, rems,
18    size,
19};
20use collections::{HashMap, HashSet};
21use refineable::{IsEmpty, Refineable};
22use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
23use serde::{Deserialize, Serialize};
24
25/// Use this struct for interfacing with the 'debug_below' styling from your own elements.
26/// If a parent element has this style set on it, then this struct will be set as a global in
27/// GPUI.
28#[cfg(debug_assertions)]
29pub struct DebugBelow;
30
31#[cfg(debug_assertions)]
32impl crate::Global for DebugBelow {}
33
34/// How to fit the image into the bounds of the element.
35pub enum ObjectFit {
36    /// The image will be stretched to fill the bounds of the element.
37    Fill,
38    /// The image will be scaled to fit within the bounds of the element.
39    Contain,
40    /// The image will be scaled to cover the bounds of the element.
41    Cover,
42    /// The image will be scaled down to fit within the bounds of the element.
43    ScaleDown,
44    /// The image will maintain its original size.
45    None,
46}
47
48impl ObjectFit {
49    /// Get the bounds of the image within the given bounds.
50    pub fn get_bounds(
51        &self,
52        bounds: Bounds<Pixels>,
53        image_size: Size<DevicePixels>,
54    ) -> Bounds<Pixels> {
55        let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension)));
56        let image_ratio = image_size.width / image_size.height;
57        let bounds_ratio = bounds.size.width / bounds.size.height;
58
59        match self {
60            ObjectFit::Fill => bounds,
61            ObjectFit::Contain => {
62                let new_size = if bounds_ratio > image_ratio {
63                    size(
64                        image_size.width * (bounds.size.height / image_size.height),
65                        bounds.size.height,
66                    )
67                } else {
68                    size(
69                        bounds.size.width,
70                        image_size.height * (bounds.size.width / image_size.width),
71                    )
72                };
73
74                Bounds {
75                    origin: point(
76                        bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
77                        bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
78                    ),
79                    size: new_size,
80                }
81            }
82            ObjectFit::ScaleDown => {
83                // Check if the image is larger than the bounds in either dimension.
84                if image_size.width > bounds.size.width || image_size.height > bounds.size.height {
85                    // If the image is larger, use the same logic as Contain to scale it down.
86                    let new_size = if bounds_ratio > image_ratio {
87                        size(
88                            image_size.width * (bounds.size.height / image_size.height),
89                            bounds.size.height,
90                        )
91                    } else {
92                        size(
93                            bounds.size.width,
94                            image_size.height * (bounds.size.width / image_size.width),
95                        )
96                    };
97
98                    Bounds {
99                        origin: point(
100                            bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
101                            bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
102                        ),
103                        size: new_size,
104                    }
105                } else {
106                    // If the image is smaller than or equal to the container, display it at its original size,
107                    // centered within the container.
108                    let original_size = size(image_size.width, image_size.height);
109                    Bounds {
110                        origin: point(
111                            bounds.origin.x + (bounds.size.width - original_size.width) / 2.0,
112                            bounds.origin.y + (bounds.size.height - original_size.height) / 2.0,
113                        ),
114                        size: original_size,
115                    }
116                }
117            }
118            ObjectFit::Cover => {
119                let new_size = if bounds_ratio > image_ratio {
120                    size(
121                        bounds.size.width,
122                        image_size.height * (bounds.size.width / image_size.width),
123                    )
124                } else {
125                    size(
126                        image_size.width * (bounds.size.height / image_size.height),
127                        bounds.size.height,
128                    )
129                };
130
131                Bounds {
132                    origin: point(
133                        bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
134                        bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
135                    ),
136                    size: new_size,
137                }
138            }
139            ObjectFit::None => Bounds {
140                origin: bounds.origin,
141                size: image_size,
142            },
143        }
144    }
145}
146
147/// A style property defined outside of GPUI's core style structs.
148///
149/// Engines and forks carry their own rendering extensions - a backdrop blur
150/// radius, a custom shader's parameters - through the style cascade by defining
151/// a type and setting it with [`Styled::custom_style`], rather than adding a
152/// field to [`Style`] that every consumer of this crate has to know about.
153///
154/// The type itself keys the property, so a reader recovers exactly the type
155/// that was set. Any `Send + Sync + PartialEq + 'static` type is a custom
156/// property as-is; there is nothing to implement. Properties must be
157/// [`PartialEq`] because merging and subtracting style refinements diffs them.
158pub trait CustomStyleProperty: 'static + Send + Sync {
159    /// Upcasts to [`Any`] so [`CustomStyles::get`] can recover the concrete type.
160    fn as_any(&self) -> &dyn Any;
161
162    /// Compares two properties that share a concrete type.
163    fn eq_property(&self, other: &dyn CustomStyleProperty) -> bool;
164}
165
166impl<T: 'static + Send + Sync + PartialEq> CustomStyleProperty for T {
167    fn as_any(&self) -> &dyn Any {
168        self
169    }
170
171    fn eq_property(&self, other: &dyn CustomStyleProperty) -> bool {
172        other.as_any().downcast_ref::<T>() == Some(self)
173    }
174}
175
176/// The custom style properties set on an element or inherited from its
177/// ancestors, keyed by property type.
178///
179/// Every property lives behind one [`Arc`], so cloning a style shares them and
180/// an element that sets none carries only a null pointer. A write copies the
181/// map before mutating it, so clones never observe each other's changes.
182#[derive(Clone, Default)]
183pub struct CustomStyles {
184    entries: Option<Arc<HashMap<TypeId, Arc<dyn CustomStyleProperty>>>>,
185}
186
187/// Custom properties merge key by key, so a refinement has the same shape as
188/// the style it refines and [`Refineable`] can use [`CustomStyles`] directly.
189pub type CustomStylesRefinement = CustomStyles;
190
191impl CustomStyles {
192    /// Sets `property`, replacing any value already set for its type.
193    pub fn insert<T: CustomStyleProperty>(&mut self, property: T) {
194        self.entries_mut()
195            .insert(TypeId::of::<T>(), Arc::new(property));
196    }
197
198    /// Returns the property of type `T`, if one was set.
199    pub fn get<T: CustomStyleProperty>(&self) -> Option<&T> {
200        self.entries
201            .as_ref()?
202            .get(&TypeId::of::<T>())?
203            .as_any()
204            .downcast_ref::<T>()
205    }
206
207    /// Merges `other` into `self`, letting `other` win where both set the same
208    /// type.
209    pub fn refine(&mut self, other: &Self) {
210        let Some(other_entries) = other.entries.as_ref() else {
211            return;
212        };
213        let entries = self.entries_mut();
214        for (type_id, property) in other_entries.iter() {
215            entries.insert(*type_id, property.clone());
216        }
217    }
218
219    fn entries_mut(&mut self) -> &mut HashMap<TypeId, Arc<dyn CustomStyleProperty>> {
220        Arc::make_mut(self.entries.get_or_insert_with(Default::default))
221    }
222
223    /// Whether any property is set. It exists because the `Refineable` derive
224    /// calls `is_some` on every field of the refinement it generates.
225    fn is_some(&self) -> bool {
226        self.entries
227            .as_ref()
228            .is_some_and(|entries| !entries.is_empty())
229    }
230}
231
232impl IsEmpty for CustomStyles {
233    fn is_empty(&self) -> bool {
234        self.entries
235            .as_ref()
236            .is_none_or(|entries| entries.is_empty())
237    }
238}
239
240impl Refineable for CustomStyles {
241    type Refinement = CustomStylesRefinement;
242
243    fn refine(&mut self, refinement: &Self::Refinement) {
244        CustomStyles::refine(self, refinement);
245    }
246
247    fn refined(mut self, refinement: Self::Refinement) -> Self {
248        CustomStyles::refine(&mut self, &refinement);
249        self
250    }
251
252    fn is_superset_of(&self, refinement: &Self::Refinement) -> bool {
253        let Some(refinement_entries) = refinement.entries.as_ref() else {
254            return true;
255        };
256        let Some(entries) = self.entries.as_ref() else {
257            return refinement_entries.is_empty();
258        };
259        refinement_entries.iter().all(|(type_id, property)| {
260            entries
261                .get(type_id)
262                .is_some_and(|value| value.eq_property(&**property))
263        })
264    }
265
266    fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement {
267        let Some(entries) = self.entries.as_ref() else {
268            return CustomStyles::default();
269        };
270        let mut subtracted = CustomStyles::default();
271        for (type_id, property) in entries.iter() {
272            let covered_by_refinement = refinement
273                .entries
274                .as_ref()
275                .and_then(|refinement_entries| refinement_entries.get(type_id))
276                .is_some_and(|refined| property.eq_property(&**refined));
277            if !covered_by_refinement {
278                subtracted.entries_mut().insert(*type_id, property.clone());
279            }
280        }
281        subtracted
282    }
283}
284
285impl PartialEq for CustomStyles {
286    fn eq(&self, other: &Self) -> bool {
287        // An absent map is an empty one, so a style that never set a property
288        // equals one whose properties were all removed.
289        let no_properties = HashMap::default();
290        let entries = self.entries.as_deref().unwrap_or(&no_properties);
291        let other_entries = other.entries.as_deref().unwrap_or(&no_properties);
292        entries.len() == other_entries.len()
293            && entries.iter().all(|(type_id, property)| {
294                other_entries
295                    .get(type_id)
296                    .is_some_and(|other| property.eq_property(&**other))
297            })
298    }
299}
300
301impl fmt::Debug for CustomStyles {
302    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303        // Property values are opaque, so report how many are set rather than
304        // pretending to name them.
305        f.debug_struct("CustomStyles")
306            .field(
307                "len",
308                &self.entries.as_ref().map_or(0, |entries| entries.len()),
309            )
310            .finish()
311    }
312}
313
314impl Serialize for CustomStyles {
315    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
316        // A property's type is what keys it, and no serialized format can name
317        // a Rust type, so custom properties do not round-trip. Serialize an
318        // opaque value instead of claiming values we cannot represent.
319        serializer.serialize_unit()
320    }
321}
322
323impl<'de> Deserialize<'de> for CustomStyles {
324    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
325        // Consume whatever was serialized so the rest of the style still
326        // parses, but recover no properties.
327        serde::de::IgnoredAny::deserialize(deserializer)?;
328        Ok(CustomStyles::default())
329    }
330}
331
332impl JsonSchema for CustomStyles {
333    fn schema_name() -> Cow<'static, str> {
334        "CustomStyles".into()
335    }
336
337    fn json_schema(_: &mut SchemaGenerator) -> Schema {
338        // Custom properties have no serialized form to describe.
339        json_schema!({ "type": "null" })
340    }
341}
342
343/// The CSS styling that can be applied to an element via the `Styled` trait
344#[derive(Clone, Refineable, Debug)]
345#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
346pub struct Style {
347    /// What layout strategy should be used?
348    pub display: Display,
349
350    /// Should the element be painted on screen?
351    pub visibility: Visibility,
352
353    // Overflow properties
354    /// How children overflowing their container should affect layout
355    #[refineable]
356    pub overflow: Point<Overflow>,
357    /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes.
358    pub scrollbar_width: AbsoluteLength,
359    /// Whether both x and y axis should be scrollable at the same time.
360    pub allow_concurrent_scroll: bool,
361    /// Whether scrolling should be restricted to the input gesture's axis.
362    ///
363    /// Pixel-based scroll gestures are locked to their initially dominant axis. The lock may be
364    /// released when the gesture changes direction strongly. Touch phases delimit gestures when
365    /// available, with a timeout fallback for platforms that only emit moved events.
366    ///
367    /// This also prevents input from being remapped to another axis. For example, horizontal input
368    /// will not scroll a container that only has vertical overflow enabled. Mouse wheel platforms
369    /// typically report ordinary wheel input on the Y axis and Shift-modified input on the X axis.
370    ///
371    /// ## Motivation
372    ///
373    /// On the web when scrolling with the mouse wheel, scrolling up and down will always scroll the Y axis, even when
374    /// the mouse is over a horizontally-scrollable element.
375    ///
376    /// The only way to scroll horizontally is to hold down `Shift` while scrolling, which then changes the scroll axis
377    /// to the X axis.
378    ///
379    /// Currently, GPUI operates differently from the web in that it will scroll an element in either the X or Y axis
380    /// when scrolling with just the mouse wheel. This causes problems when scrolling in a vertical list that contains
381    /// horizontally-scrollable elements, as when you get to the horizontally-scrollable elements the scroll will be
382    /// hijacked.
383    ///
384    /// Ideally we would match the web's behavior and not have a need for this, but right now we're adding this opt-in
385    /// style property to limit the potential blast radius.
386    pub restrict_scroll_to_axis: bool,
387
388    // Position properties
389    /// What should the `position` value of this struct use as a base offset?
390    pub position: Position,
391    /// How should the position of this element be tweaked relative to the layout defined?
392    #[refineable]
393    pub inset: Edges<Length>,
394
395    // Size properties
396    /// Sets the initial size of the item
397    #[refineable]
398    pub size: Size<Length>,
399    /// Controls the minimum size of the item
400    #[refineable]
401    pub min_size: Size<Length>,
402    /// Controls the maximum size of the item
403    #[refineable]
404    pub max_size: Size<Length>,
405    /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height.
406    pub aspect_ratio: Option<f32>,
407
408    // Spacing Properties
409    /// How large should the margin be on each side?
410    #[refineable]
411    pub margin: Edges<Length>,
412    /// How large should the padding be on each side?
413    #[refineable]
414    pub padding: Edges<DefiniteLength>,
415    /// How large should the border be on each side?
416    #[refineable]
417    pub border_widths: Edges<AbsoluteLength>,
418
419    // Alignment properties
420    /// How this node's children aligned in the cross/block axis?
421    pub align_items: Option<AlignItems>,
422    /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set
423    pub align_self: Option<AlignSelf>,
424    /// How should content contained within this item be aligned in the cross/block axis
425    pub align_content: Option<AlignContent>,
426    /// How should contained within this item be aligned in the main/inline axis
427    pub justify_content: Option<JustifyContent>,
428    /// How large should the gaps between items in a flex container be?
429    #[refineable]
430    pub gap: Size<DefiniteLength>,
431
432    // Flexbox properties
433    /// Which direction does the main axis flow in?
434    pub flex_direction: FlexDirection,
435    /// Should elements wrap, or stay in a single line?
436    pub flex_wrap: FlexWrap,
437    /// Sets the initial main axis size of the item
438    pub flex_basis: Length,
439    /// 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.
440    pub flex_grow: f32,
441    /// 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.
442    pub flex_shrink: f32,
443
444    /// The fill color of this element
445    pub background: Option<Fill>,
446
447    /// The border color of this element
448    pub border_color: Option<Hsla>,
449
450    /// The border style of this element
451    pub border_style: BorderStyle,
452
453    /// The radius of the corners of this element
454    #[refineable]
455    pub corner_radii: Corners<AbsoluteLength>,
456
457    /// Box shadow of the element
458    pub box_shadow: Vec<BoxShadow>,
459
460    /// The text style of this element
461    #[refineable]
462    pub text: TextStyleRefinement,
463
464    /// The mouse cursor style shown when the mouse pointer is over an element.
465    pub mouse_cursor: Option<CursorStyle>,
466
467    /// The opacity of this element
468    pub opacity: Option<f32>,
469
470    /// The grid columns of this element
471    /// Roughly equivalent to the Tailwind `grid-cols-<number>`
472    pub grid_cols: Option<GridTemplate>,
473
474    /// The row span of this element
475    /// Equivalent to the Tailwind `grid-rows-<number>`
476    pub grid_rows: Option<GridTemplate>,
477
478    /// The grid location of this element
479    pub grid_location: Option<GridLocation>,
480
481    /// Rendering extensions defined outside of GPUI's core style structs.
482    ///
483    /// Engines read these back with [`CustomStyles::get`] while painting.
484    #[refineable]
485    pub custom: CustomStyles,
486
487    /// Whether to draw a red debugging outline around this element
488    #[cfg(debug_assertions)]
489    pub debug: bool,
490
491    /// Whether to draw a red debugging outline around this element and all of its conforming children
492    #[cfg(debug_assertions)]
493    pub debug_below: bool,
494}
495
496impl Styled for StyleRefinement {
497    fn style(&mut self) -> &mut StyleRefinement {
498        self
499    }
500}
501
502impl StyleRefinement {
503    /// The grid location of this element
504    pub fn grid_location_mut(&mut self) -> &mut GridLocation {
505        self.grid_location.get_or_insert_default()
506    }
507}
508
509/// The value of the visibility property, similar to the CSS property `visibility`
510#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
511pub enum Visibility {
512    /// The element should be drawn as normal.
513    #[default]
514    Visible,
515    /// The element should not be drawn, but should still take up space in the layout.
516    Hidden,
517}
518
519/// The possible values of the box-shadow property
520#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
521pub struct BoxShadow {
522    /// What color should the shadow have?
523    pub color: Hsla,
524    /// How should it be offset from its element?
525    pub offset: Point<Pixels>,
526    /// How much should the shadow be blurred?
527    pub blur_radius: Pixels,
528    /// How much should the shadow spread?
529    pub spread_radius: Pixels,
530    /// Whether this is an inset shadow (drawn inside the element's bounds).
531    pub inset: bool,
532}
533
534impl BoxShadow {
535    /// Creates a new [`BoxShadow`] with the given offset and color, matching the order
536    /// of the CSS `box-shadow` property. Use the builder methods to set blur radius,
537    /// spread radius, and inset.
538    pub fn new(offset_x: Pixels, offset_y: Pixels, color: Hsla) -> Self {
539        Self {
540            color,
541            offset: point(offset_x, offset_y),
542            blur_radius: px(0.),
543            spread_radius: px(0.),
544            inset: false,
545        }
546    }
547
548    /// Sets the shadow blur radius.
549    pub fn blur_radius(mut self, blur_radius: Pixels) -> Self {
550        self.blur_radius = blur_radius;
551        self
552    }
553
554    /// Sets the shadow spread radius.
555    pub fn spread_radius(mut self, spread_radius: Pixels) -> Self {
556        self.spread_radius = spread_radius;
557        self
558    }
559
560    /// Marks the shadow as inset (drawn inside the element's bounds).
561    pub fn inset(mut self) -> Self {
562        self.inset = true;
563        self
564    }
565}
566
567/// How to handle whitespace in text
568#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
569pub enum WhiteSpace {
570    /// Normal line wrapping when text overflows the width of the element
571    #[default]
572    Normal,
573    /// No line wrapping, text will overflow the width of the element
574    Nowrap,
575}
576
577/// How to truncate text that overflows the width of the element
578#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
579pub enum TextOverflow {
580    /// Truncate the text at the end when it doesn't fit, and represent this truncation by
581    /// displaying the provided string (e.g., "very long te…").
582    Truncate(SharedString),
583    /// Truncate the text at the start when it doesn't fit, and represent this truncation by
584    /// displaying the provided string at the beginning (e.g., "…ong text here").
585    /// Typically more adequate for file paths where the end is more important than the beginning.
586    TruncateStart(SharedString),
587    /// Truncate the text in the middle when it doesn't fit, preserving both the start and end
588    /// of the string (e.g., "long fi…name.rs"). Useful for filenames where both the prefix
589    /// and the extension are important context.
590    TruncateMiddle(SharedString),
591}
592
593/// How to align text within the element
594#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
595pub enum TextAlign {
596    /// Align the text to the left of the element
597    #[default]
598    Left,
599
600    /// Center the text within the element
601    Center,
602
603    /// Align the text to the right of the element
604    Right,
605}
606
607/// The properties that can be used to style text in GPUI
608#[derive(Refineable, Clone, Debug, PartialEq)]
609#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
610pub struct TextStyle {
611    /// The color of the text
612    pub color: Hsla,
613
614    /// The font family to use
615    pub font_family: SharedString,
616
617    /// The font features to use
618    pub font_features: FontFeatures,
619
620    /// The fallback fonts to use
621    pub font_fallbacks: Option<FontFallbacks>,
622
623    /// The font size to use, in pixels or rems.
624    pub font_size: AbsoluteLength,
625
626    /// The line height to use, in pixels or fractions
627    pub line_height: DefiniteLength,
628
629    /// The font weight, e.g. bold
630    pub font_weight: FontWeight,
631
632    /// The font style, e.g. italic
633    pub font_style: FontStyle,
634
635    /// The background color of the text
636    pub background_color: Option<Hsla>,
637
638    /// The underline style of the text
639    pub underline: Option<UnderlineStyle>,
640
641    /// The strikethrough style of the text
642    pub strikethrough: Option<StrikethroughStyle>,
643
644    /// How to handle whitespace in the text
645    pub white_space: WhiteSpace,
646
647    /// The text should be truncated if it overflows the width of the element
648    pub text_overflow: Option<TextOverflow>,
649
650    /// How the text should be aligned within the element
651    pub text_align: TextAlign,
652
653    /// The number of lines to display before truncating the text
654    pub line_clamp: Option<usize>,
655}
656
657impl Default for TextStyle {
658    fn default() -> Self {
659        TextStyle {
660            color: black(),
661            // todo(linux) make this configurable or choose better default
662            font_family: ".SystemUIFont".into(),
663            font_features: FontFeatures::default(),
664            font_fallbacks: None,
665            font_size: rems(1.).into(),
666            line_height: phi(),
667            font_weight: FontWeight::default(),
668            font_style: FontStyle::default(),
669            background_color: None,
670            underline: None,
671            strikethrough: None,
672            white_space: WhiteSpace::Normal,
673            text_overflow: None,
674            text_align: TextAlign::default(),
675            line_clamp: None,
676        }
677    }
678}
679
680impl TextStyle {
681    /// Create a new text style with the given highlighting applied.
682    pub fn highlight(mut self, style: impl Into<HighlightStyle>) -> Self {
683        let style = style.into();
684        if let Some(weight) = style.font_weight {
685            self.font_weight = weight;
686        }
687        if let Some(style) = style.font_style {
688            self.font_style = style;
689        }
690
691        if let Some(color) = style.color {
692            self.color = self.color.blend(color);
693        }
694
695        if let Some(factor) = style.fade_out {
696            self.color.fade_out(factor);
697        }
698
699        if let Some(background_color) = style.background_color {
700            self.background_color = Some(background_color);
701        }
702
703        if let Some(underline) = style.underline {
704            self.underline = Some(underline);
705        }
706
707        if let Some(strikethrough) = style.strikethrough {
708            self.strikethrough = Some(strikethrough);
709        }
710
711        self
712    }
713
714    /// Get the font configured for this text style.
715    pub fn font(&self) -> Font {
716        Font {
717            family: self.font_family.clone(),
718            features: self.font_features.clone(),
719            fallbacks: self.font_fallbacks.clone(),
720            weight: self.font_weight,
721            style: self.font_style,
722        }
723    }
724
725    /// Returns the rounded line height in pixels.
726    pub fn line_height_in_pixels(&self, rem_size: Pixels) -> Pixels {
727        self.line_height.to_pixels(self.font_size, rem_size).round()
728    }
729
730    /// Convert this text style into a [`TextRun`], for the given length of the text.
731    pub fn to_run(&self, len: usize) -> TextRun {
732        TextRun {
733            len,
734            font: Font {
735                family: self.font_family.clone(),
736                features: self.font_features.clone(),
737                fallbacks: self.font_fallbacks.clone(),
738                weight: self.font_weight,
739                style: self.font_style,
740            },
741            color: self.color,
742            background_color: self.background_color,
743            underline: self.underline,
744            strikethrough: self.strikethrough,
745        }
746    }
747}
748
749/// A highlight style to apply, similar to a `TextStyle` except
750/// for a single font, uniformly sized and spaced text.
751#[derive(Copy, Clone, Debug, Default, PartialEq)]
752pub struct HighlightStyle {
753    /// The color of the text
754    pub color: Option<Hsla>,
755
756    /// The font weight, e.g. bold
757    pub font_weight: Option<FontWeight>,
758
759    /// The font style, e.g. italic
760    pub font_style: Option<FontStyle>,
761
762    /// The background color of the text
763    pub background_color: Option<Hsla>,
764
765    /// The underline style of the text
766    pub underline: Option<UnderlineStyle>,
767
768    /// The underline style of the text
769    pub strikethrough: Option<StrikethroughStyle>,
770
771    /// Similar to the CSS `opacity` property, this will cause the text to be less vibrant.
772    pub fade_out: Option<f32>,
773}
774
775impl Eq for HighlightStyle {}
776
777impl Hash for HighlightStyle {
778    fn hash<H: Hasher>(&self, state: &mut H) {
779        self.color.hash(state);
780        self.font_weight.hash(state);
781        self.font_style.hash(state);
782        self.background_color.hash(state);
783        self.underline.hash(state);
784        self.strikethrough.hash(state);
785        state.write_u32(u32::from_be_bytes(
786            self.fade_out.map(|f| f.to_be_bytes()).unwrap_or_default(),
787        ));
788    }
789}
790
791impl Style {
792    /// Returns true if the style is visible and the background is opaque.
793    pub fn has_opaque_background(&self) -> bool {
794        self.background
795            .as_ref()
796            .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent()))
797    }
798
799    /// Get the text style in this element style.
800    pub fn text_style(&self) -> Option<&TextStyleRefinement> {
801        if self.text.is_some() {
802            Some(&self.text)
803        } else {
804            None
805        }
806    }
807
808    /// Get the content mask for this element style, based on the given bounds.
809    /// If the element does not hide its overflow, this will return `None`.
810    pub fn overflow_mask(
811        &self,
812        bounds: Bounds<Pixels>,
813        rem_size: Pixels,
814    ) -> Option<ContentMask<Pixels>> {
815        match self.overflow {
816            Point {
817                x: Overflow::Visible,
818                y: Overflow::Visible,
819            } => None,
820            _ => {
821                let mut min = bounds.origin;
822                let mut max = bounds.bottom_right();
823
824                if self
825                    .border_color
826                    .is_some_and(|color| !color.is_transparent())
827                {
828                    min.x += self.border_widths.left.to_pixels(rem_size);
829                    max.x -= self.border_widths.right.to_pixels(rem_size);
830                    min.y += self.border_widths.top.to_pixels(rem_size);
831                    max.y -= self.border_widths.bottom.to_pixels(rem_size);
832                }
833
834                let bounds = match (
835                    self.overflow.x == Overflow::Visible,
836                    self.overflow.y == Overflow::Visible,
837                ) {
838                    // x and y both visible
839                    (true, true) => return None,
840                    // x visible, y hidden
841                    (true, false) => Bounds::from_corners(
842                        point(min.x, bounds.origin.y),
843                        point(max.x, bounds.bottom_right().y),
844                    ),
845                    // x hidden, y visible
846                    (false, true) => Bounds::from_corners(
847                        point(bounds.origin.x, min.y),
848                        point(bounds.bottom_right().x, max.y),
849                    ),
850                    // both hidden
851                    (false, false) => Bounds::from_corners(min, max),
852                };
853
854                Some(ContentMask { bounds })
855            }
856        }
857    }
858
859    /// Paints the background of an element styled with this style.
860    pub fn paint(
861        &self,
862        bounds: Bounds<Pixels>,
863        window: &mut Window,
864        cx: &mut App,
865        continuation: impl FnOnce(&mut Window, &mut App),
866    ) {
867        #[cfg(debug_assertions)]
868        if self.debug_below {
869            cx.set_global(DebugBelow)
870        }
871
872        #[cfg(debug_assertions)]
873        if self.debug || cx.has_global::<DebugBelow>() {
874            window.paint_quad(crate::outline(bounds, crate::red(), BorderStyle::default()));
875        }
876
877        let rem_size = window.rem_size();
878        let corner_radii = self
879            .corner_radii
880            .to_pixels(rem_size)
881            .clamp_radii_for_quad_size(bounds.size);
882
883        window.paint_drop_shadows(bounds, corner_radii, &self.box_shadow);
884
885        let background_color = self.background.as_ref().and_then(Fill::color);
886        if background_color.is_some_and(|color| !color.is_transparent()) {
887            let mut border_color = match background_color {
888                Some(color) => match color.tag {
889                    BackgroundTag::Solid
890                    | BackgroundTag::PatternSlash
891                    | BackgroundTag::Checkerboard => color.solid,
892
893                    BackgroundTag::LinearGradient => color
894                        .colors
895                        .first()
896                        .map(|stop| stop.color)
897                        .unwrap_or_default(),
898                },
899                None => Hsla::default(),
900            };
901            border_color.a = 0.;
902            window.paint_quad(quad(
903                bounds,
904                corner_radii,
905                background_color.unwrap_or_default(),
906                Edges::default(),
907                border_color,
908                self.border_style,
909            ));
910        }
911
912        window.paint_inset_shadows(bounds, corner_radii, &self.box_shadow);
913
914        continuation(window, cx);
915
916        if self.is_border_visible() {
917            let border_widths = self.border_widths.to_pixels(rem_size);
918            let mut background = self.border_color.unwrap_or_default();
919            background.a = 0.;
920            window.paint_quad(quad(
921                bounds,
922                corner_radii,
923                background,
924                border_widths,
925                self.border_color.unwrap_or_default(),
926                self.border_style,
927            ));
928        }
929
930        #[cfg(debug_assertions)]
931        if self.debug_below {
932            cx.remove_global::<DebugBelow>();
933        }
934    }
935
936    fn is_border_visible(&self) -> bool {
937        self.border_color
938            .is_some_and(|color| !color.is_transparent())
939            && self.border_widths.any(|length| !length.is_zero())
940    }
941}
942
943impl Default for Style {
944    fn default() -> Self {
945        Style {
946            display: Display::Block,
947            visibility: Visibility::Visible,
948            overflow: Point {
949                x: Overflow::Visible,
950                y: Overflow::Visible,
951            },
952            allow_concurrent_scroll: false,
953            restrict_scroll_to_axis: false,
954            scrollbar_width: AbsoluteLength::default(),
955            position: Position::Relative,
956            inset: Edges::auto(),
957            margin: Edges::<Length>::zero(),
958            padding: Edges::<DefiniteLength>::zero(),
959            border_widths: Edges::<AbsoluteLength>::zero(),
960            size: Size::auto(),
961            min_size: Size::auto(),
962            max_size: Size::auto(),
963            aspect_ratio: None,
964            gap: Size::default(),
965            // Alignment
966            align_items: None,
967            align_self: None,
968            align_content: None,
969            justify_content: None,
970            // Flexbox
971            flex_direction: FlexDirection::Row,
972            flex_wrap: FlexWrap::NoWrap,
973            flex_grow: 0.0,
974            flex_shrink: 1.0,
975            flex_basis: Length::Auto,
976            background: None,
977            border_color: None,
978            border_style: BorderStyle::default(),
979            corner_radii: Corners::default(),
980            box_shadow: Default::default(),
981            text: TextStyleRefinement::default(),
982            mouse_cursor: None,
983            opacity: None,
984            grid_rows: None,
985            grid_cols: None,
986            grid_location: None,
987            custom: CustomStyles::default(),
988
989            #[cfg(debug_assertions)]
990            debug: false,
991            #[cfg(debug_assertions)]
992            debug_below: false,
993        }
994    }
995}
996
997/// The kinds of fill that can be applied to a shape.
998#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
999pub enum Fill {
1000    /// A solid color fill.
1001    Color(Background),
1002}
1003
1004impl Fill {
1005    /// Unwrap this fill into a solid color, if it is one.
1006    ///
1007    /// If the fill is not a solid color, this method returns `None`.
1008    pub fn color(&self) -> Option<Background> {
1009        match self {
1010            Fill::Color(color) => Some(*color),
1011        }
1012    }
1013}
1014
1015impl Default for Fill {
1016    fn default() -> Self {
1017        Self::Color(Background::default())
1018    }
1019}
1020
1021impl From<Hsla> for Fill {
1022    fn from(color: Hsla) -> Self {
1023        Self::Color(color.into())
1024    }
1025}
1026
1027impl From<Rgba> for Fill {
1028    fn from(color: Rgba) -> Self {
1029        Self::Color(color.into())
1030    }
1031}
1032
1033impl From<Background> for Fill {
1034    fn from(background: Background) -> Self {
1035        Self::Color(background)
1036    }
1037}
1038
1039impl From<TextStyle> for HighlightStyle {
1040    fn from(other: TextStyle) -> Self {
1041        Self::from(&other)
1042    }
1043}
1044
1045impl From<&TextStyle> for HighlightStyle {
1046    fn from(other: &TextStyle) -> Self {
1047        Self {
1048            color: Some(other.color),
1049            font_weight: Some(other.font_weight),
1050            font_style: Some(other.font_style),
1051            background_color: other.background_color,
1052            underline: other.underline,
1053            strikethrough: other.strikethrough,
1054            fade_out: None,
1055        }
1056    }
1057}
1058
1059impl HighlightStyle {
1060    /// Create a highlight style with just a color
1061    pub fn color(color: Hsla) -> Self {
1062        Self {
1063            color: Some(color),
1064            ..Default::default()
1065        }
1066    }
1067    /// Blend this highlight style with another.
1068    /// Non-continuous properties, like font_weight and font_style, are overwritten.
1069    #[must_use]
1070    pub fn highlight(self, other: HighlightStyle) -> Self {
1071        Self {
1072            color: other
1073                .color
1074                .map(|other_color| {
1075                    if let Some(color) = self.color {
1076                        color.blend(other_color)
1077                    } else {
1078                        other_color
1079                    }
1080                })
1081                .or(self.color),
1082            font_weight: other.font_weight.or(self.font_weight),
1083            font_style: other.font_style.or(self.font_style),
1084            background_color: other.background_color.or(self.background_color),
1085            underline: other.underline.or(self.underline),
1086            strikethrough: other.strikethrough.or(self.strikethrough),
1087            fade_out: other
1088                .fade_out
1089                .map(|source_fade| {
1090                    self.fade_out
1091                        .map(|dest_fade| (dest_fade * (1. + source_fade)).clamp(0., 1.))
1092                        .unwrap_or(source_fade)
1093                })
1094                .or(self.fade_out),
1095        }
1096    }
1097}
1098
1099impl From<Hsla> for HighlightStyle {
1100    fn from(color: Hsla) -> Self {
1101        Self {
1102            color: Some(color),
1103            ..Default::default()
1104        }
1105    }
1106}
1107
1108impl From<FontWeight> for HighlightStyle {
1109    fn from(font_weight: FontWeight) -> Self {
1110        Self {
1111            font_weight: Some(font_weight),
1112            ..Default::default()
1113        }
1114    }
1115}
1116
1117impl From<FontStyle> for HighlightStyle {
1118    fn from(font_style: FontStyle) -> Self {
1119        Self {
1120            font_style: Some(font_style),
1121            ..Default::default()
1122        }
1123    }
1124}
1125
1126impl From<Rgba> for HighlightStyle {
1127    fn from(color: Rgba) -> Self {
1128        Self {
1129            color: Some(color.into()),
1130            ..Default::default()
1131        }
1132    }
1133}
1134
1135/// Combine and merge the highlights and ranges in the two iterators.
1136pub fn combine_highlights(
1137    a: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
1138    b: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
1139) -> impl Iterator<Item = (Range<usize>, HighlightStyle)> {
1140    let mut endpoints = Vec::new();
1141    let mut highlights = Vec::new();
1142    for (range, highlight) in a.into_iter().chain(b) {
1143        if !range.is_empty() {
1144            let highlight_id = highlights.len();
1145            endpoints.push((range.start, highlight_id, true));
1146            endpoints.push((range.end, highlight_id, false));
1147            highlights.push(highlight);
1148        }
1149    }
1150    endpoints.sort_unstable_by_key(|(position, _, _)| *position);
1151    let mut endpoints = endpoints.into_iter().peekable();
1152
1153    let mut active_styles = HashSet::default();
1154    let mut ix = 0;
1155    iter::from_fn(move || {
1156        while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() {
1157            let prev_index = mem::replace(&mut ix, *endpoint_ix);
1158            if ix > prev_index && !active_styles.is_empty() {
1159                let current_style = active_styles
1160                    .iter()
1161                    .fold(HighlightStyle::default(), |acc, highlight_id| {
1162                        acc.highlight(highlights[*highlight_id])
1163                    });
1164                return Some((prev_index..ix, current_style));
1165            }
1166
1167            if *is_start {
1168                active_styles.insert(*highlight_id);
1169            } else {
1170                active_styles.remove(highlight_id);
1171            }
1172            endpoints.next();
1173        }
1174        None
1175    })
1176}
1177
1178#[cfg(test)]
1179mod tests {
1180    use crate::{blue, green, px, red, yellow};
1181
1182    use super::*;
1183
1184    use util_macros::perf;
1185
1186    #[perf]
1187    fn test_basic_highlight_style_combination() {
1188        let style_a = HighlightStyle::default();
1189        let style_b = HighlightStyle::default();
1190        let style_a = style_a.highlight(style_b);
1191        assert_eq!(
1192            style_a,
1193            HighlightStyle::default(),
1194            "Combining empty styles should not produce a non-empty style."
1195        );
1196
1197        let mut style_b = HighlightStyle {
1198            color: Some(red()),
1199            strikethrough: Some(StrikethroughStyle {
1200                thickness: px(2.),
1201                color: Some(blue()),
1202            }),
1203            fade_out: Some(0.),
1204            font_style: Some(FontStyle::Italic),
1205            font_weight: Some(FontWeight(300.)),
1206            background_color: Some(yellow()),
1207            underline: Some(UnderlineStyle {
1208                thickness: px(2.),
1209                color: Some(red()),
1210                wavy: true,
1211            }),
1212        };
1213        let expected_style = style_b;
1214
1215        let style_a = style_a.highlight(style_b);
1216        assert_eq!(
1217            style_a, expected_style,
1218            "Blending an empty style with another style should return the other style"
1219        );
1220
1221        let style_b = style_b.highlight(Default::default());
1222        assert_eq!(
1223            style_b, expected_style,
1224            "Blending a style with an empty style should not change the style."
1225        );
1226
1227        let mut style_c = expected_style;
1228
1229        let style_d = HighlightStyle {
1230            color: Some(blue().alpha(0.7)),
1231            strikethrough: Some(StrikethroughStyle {
1232                thickness: px(4.),
1233                color: Some(crate::red()),
1234            }),
1235            fade_out: Some(0.),
1236            font_style: Some(FontStyle::Oblique),
1237            font_weight: Some(FontWeight(800.)),
1238            background_color: Some(green()),
1239            underline: Some(UnderlineStyle {
1240                thickness: px(4.),
1241                color: None,
1242                wavy: false,
1243            }),
1244        };
1245
1246        let expected_style = HighlightStyle {
1247            color: Some(red().blend(blue().alpha(0.7))),
1248            strikethrough: Some(StrikethroughStyle {
1249                thickness: px(4.),
1250                color: Some(red()),
1251            }),
1252            // TODO this does not seem right
1253            fade_out: Some(0.),
1254            font_style: Some(FontStyle::Oblique),
1255            font_weight: Some(FontWeight(800.)),
1256            background_color: Some(green()),
1257            underline: Some(UnderlineStyle {
1258                thickness: px(4.),
1259                color: None,
1260                wavy: false,
1261            }),
1262        };
1263
1264        let style_c = style_c.highlight(style_d);
1265        assert_eq!(
1266            style_c, expected_style,
1267            "Blending styles should blend properties where possible and override all others"
1268        );
1269    }
1270
1271    #[perf]
1272    fn test_combine_highlights() {
1273        assert_eq!(
1274            combine_highlights(
1275                [
1276                    (0..5, green().into()),
1277                    (4..10, FontWeight::BOLD.into()),
1278                    (15..20, yellow().into()),
1279                ],
1280                [
1281                    (2..6, FontStyle::Italic.into()),
1282                    (1..3, blue().into()),
1283                    (21..23, red().into()),
1284                ]
1285            )
1286            .collect::<Vec<_>>(),
1287            [
1288                (
1289                    0..1,
1290                    HighlightStyle {
1291                        color: Some(green()),
1292                        ..Default::default()
1293                    }
1294                ),
1295                (
1296                    1..2,
1297                    HighlightStyle {
1298                        color: Some(blue()),
1299                        ..Default::default()
1300                    }
1301                ),
1302                (
1303                    2..3,
1304                    HighlightStyle {
1305                        color: Some(blue()),
1306                        font_style: Some(FontStyle::Italic),
1307                        ..Default::default()
1308                    }
1309                ),
1310                (
1311                    3..4,
1312                    HighlightStyle {
1313                        color: Some(green()),
1314                        font_style: Some(FontStyle::Italic),
1315                        ..Default::default()
1316                    }
1317                ),
1318                (
1319                    4..5,
1320                    HighlightStyle {
1321                        color: Some(green()),
1322                        font_weight: Some(FontWeight::BOLD),
1323                        font_style: Some(FontStyle::Italic),
1324                        ..Default::default()
1325                    }
1326                ),
1327                (
1328                    5..6,
1329                    HighlightStyle {
1330                        font_weight: Some(FontWeight::BOLD),
1331                        font_style: Some(FontStyle::Italic),
1332                        ..Default::default()
1333                    }
1334                ),
1335                (
1336                    6..10,
1337                    HighlightStyle {
1338                        font_weight: Some(FontWeight::BOLD),
1339                        ..Default::default()
1340                    }
1341                ),
1342                (
1343                    15..20,
1344                    HighlightStyle {
1345                        color: Some(yellow()),
1346                        ..Default::default()
1347                    }
1348                ),
1349                (
1350                    21..23,
1351                    HighlightStyle {
1352                        color: Some(red()),
1353                        ..Default::default()
1354                    }
1355                )
1356            ]
1357        );
1358    }
1359
1360    #[perf]
1361    fn test_text_style_refinement() {
1362        let mut style = Style::default();
1363        style.refine(&StyleRefinement::default().text_size(px(20.0)));
1364        style.refine(&StyleRefinement::default().font_weight(FontWeight::SEMIBOLD));
1365
1366        assert_eq!(
1367            Some(AbsoluteLength::from(px(20.0))),
1368            style.text_style().unwrap().font_size
1369        );
1370
1371        assert_eq!(
1372            Some(FontWeight::SEMIBOLD),
1373            style.text_style().unwrap().font_weight
1374        );
1375    }
1376
1377    #[derive(Clone, Debug, PartialEq)]
1378    struct BackdropBlur(f32);
1379
1380    #[derive(Clone, Debug, PartialEq)]
1381    struct CustomShader(u32);
1382
1383    #[derive(Default)]
1384    struct Element {
1385        refinement: StyleRefinement,
1386    }
1387
1388    impl Styled for Element {
1389        fn style(&mut self) -> &mut StyleRefinement {
1390            &mut self.refinement
1391        }
1392    }
1393
1394    #[test]
1395    fn custom_style_properties_reach_the_resolved_style() {
1396        let element = Element::default()
1397            .custom_style(BackdropBlur(8.0))
1398            .custom_style(CustomShader(7));
1399
1400        let mut style = Style::default();
1401        style.refine(&element.refinement);
1402
1403        assert_eq!(style.custom.get::<BackdropBlur>(), Some(&BackdropBlur(8.0)));
1404        assert_eq!(style.custom.get::<CustomShader>(), Some(&CustomShader(7)));
1405    }
1406
1407    #[test]
1408    fn refining_custom_style_properties_overrides_matching_keys_and_keeps_disjoint_ones() {
1409        let mut inherited = StyleRefinement::default();
1410        inherited.custom.insert(BackdropBlur(8.0));
1411        inherited.custom.insert(CustomShader(7));
1412
1413        let mut overridden = StyleRefinement::default();
1414        overridden.custom.insert(BackdropBlur(16.0));
1415
1416        let mut style = Style::default();
1417        style.refine(&inherited);
1418        style.refine(&overridden);
1419
1420        assert_eq!(
1421            style.custom.get::<BackdropBlur>(),
1422            Some(&BackdropBlur(16.0)),
1423            "the later refinement should win"
1424        );
1425        assert_eq!(
1426            style.custom.get::<CustomShader>(),
1427            Some(&CustomShader(7)),
1428            "a property the later refinement does not set should survive"
1429        );
1430    }
1431
1432    #[test]
1433    fn custom_style_properties_are_shared_until_written() {
1434        let mut style = Style::default();
1435        style.custom.insert(BackdropBlur(8.0));
1436
1437        let mut clone = style.clone();
1438        clone.custom.insert(BackdropBlur(16.0));
1439
1440        assert_eq!(
1441            style.custom.get::<BackdropBlur>(),
1442            Some(&BackdropBlur(8.0)),
1443            "writing to a clone should not disturb the original"
1444        );
1445    }
1446
1447    #[test]
1448    fn custom_style_properties_are_diffed_by_value() {
1449        let mut style = Style::default();
1450        style.custom.insert(BackdropBlur(8.0));
1451
1452        let mut matching = StyleRefinement::default();
1453        matching.custom.insert(BackdropBlur(8.0));
1454        assert!(style.is_superset_of(&matching));
1455        assert!(style.subtract(&matching).custom.is_empty());
1456
1457        let mut differing = StyleRefinement::default();
1458        differing.custom.insert(BackdropBlur(16.0));
1459        assert!(!style.is_superset_of(&differing));
1460        assert_eq!(
1461            style.subtract(&differing).custom.get::<BackdropBlur>(),
1462            Some(&BackdropBlur(8.0))
1463        );
1464    }
1465
1466    #[test]
1467    fn styles_without_custom_style_properties_are_indistinguishable() {
1468        assert_eq!(Style::default().custom, CustomStyles::default());
1469        assert!(Style::default().custom.is_empty());
1470        assert_eq!(
1471            std::mem::size_of::<CustomStyles>(),
1472            std::mem::size_of::<Option<Arc<()>>>(),
1473            "custom properties should cost a style no more than a null pointer"
1474        );
1475    }
1476
1477    #[test]
1478    fn custom_style_properties_do_not_round_trip_through_json() {
1479        let mut refinement = StyleRefinement::default();
1480        refinement.custom.insert(BackdropBlur(8.0));
1481
1482        let json = serde_json::to_string(&refinement).expect("the refinement should serialize");
1483        let deserialized: StyleRefinement =
1484            serde_json::from_str(&json).expect("the refinement should deserialize");
1485
1486        assert!(
1487            deserialized.custom.is_empty(),
1488            "a property type cannot be named in JSON, so nothing should come back"
1489        );
1490    }
1491}