gpui: Add linear gradient support to fill background (#20812)

Jason Lee created

Release Notes:

- gpui: Add linear gradient support to fill background

Run example:

```
cargo run -p gpui --example gradient
cargo run -p gpui --example gradient --features macos-blade
```

## Demo

In GPUI (sRGB):

<img width="761" alt="image"
src="https://github.com/user-attachments/assets/568c02e8-3065-43c2-b5c2-5618d553dd6e">

In GPUI (Oklab):

<img width="761" alt="image"
src="https://github.com/user-attachments/assets/b008b0de-2705-4f99-831d-998ce48eed42">

In CSS (sRGB): 

https://codepen.io/huacnlee/pen/rNXgxBY

<img width="505" alt="image"
src="https://github.com/user-attachments/assets/239f4b65-24b3-4797-9491-a13eea420158">

In CSS (Oklab):

https://codepen.io/huacnlee/pen/wBwBKOp

<img width="658" alt="image"
src="https://github.com/user-attachments/assets/56fdd55f-d219-45de-922f-7227f535b210">


---

Currently only support 2 color stops with linear-gradient. I think this
is we first introduce the gradient feature in GPUI, and the
linear-gradient is most popular for use. So we can just add this first
and then to add more other supports.

Change summary

crates/gpui/examples/gradient.rs                 | 254 ++++++++++++++++++
crates/gpui/src/color.rs                         | 186 +++++++++++++
crates/gpui/src/platform/blade/blade_renderer.rs |   4 
crates/gpui/src/platform/blade/shaders.wgsl      | 235 +++++++++++++++-
crates/gpui/src/platform/mac/metal_renderer.rs   |   6 
crates/gpui/src/platform/mac/shaders.metal       | 220 +++++++++++++-
crates/gpui/src/scene.rs                         |   8 
crates/gpui/src/style.rs                         |  37 +
crates/gpui/src/window.rs                        |  19 
9 files changed, 899 insertions(+), 70 deletions(-)

Detailed changes

crates/gpui/examples/gradient.rs 🔗

@@ -0,0 +1,254 @@
+use gpui::{
+    canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, size, App, AppContext,
+    Bounds, ColorSpace, Half, Render, ViewContext, WindowOptions,
+};
+
+struct GradientViewer {
+    color_space: ColorSpace,
+}
+
+impl GradientViewer {
+    fn new() -> Self {
+        Self {
+            color_space: ColorSpace::default(),
+        }
+    }
+}
+
+impl Render for GradientViewer {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let color_space = self.color_space;
+
+        div()
+            .font_family(".SystemUIFont")
+            .bg(gpui::white())
+            .size_full()
+            .p_4()
+            .flex()
+            .flex_col()
+            .gap_3()
+            .child(
+                div()
+                    .flex()
+                    .gap_2()
+                    .justify_between()
+                    .items_center()
+                    .child("Gradient Examples")
+                    .child(
+                        div().flex().gap_2().items_center().child(
+                            div()
+                                .id("method")
+                                .flex()
+                                .px_3()
+                                .py_1()
+                                .text_sm()
+                                .bg(gpui::black())
+                                .text_color(gpui::white())
+                                .child(format!("{}", color_space))
+                                .active(|this| this.opacity(0.8))
+                                .on_click(cx.listener(move |this, _, cx| {
+                                    this.color_space = match this.color_space {
+                                        ColorSpace::Oklab => ColorSpace::Srgb,
+                                        ColorSpace::Srgb => ColorSpace::Oklab,
+                                    };
+                                    cx.notify();
+                                })),
+                        ),
+                    ),
+            )
+            .child(
+                div()
+                    .flex()
+                    .flex_1()
+                    .gap_3()
+                    .child(
+                        div()
+                            .size_full()
+                            .rounded_xl()
+                            .flex()
+                            .items_center()
+                            .justify_center()
+                            .bg(gpui::red())
+                            .text_color(gpui::white())
+                            .child("Solid Color"),
+                    )
+                    .child(
+                        div()
+                            .size_full()
+                            .rounded_xl()
+                            .flex()
+                            .items_center()
+                            .justify_center()
+                            .bg(gpui::blue())
+                            .text_color(gpui::white())
+                            .child("Solid Color"),
+                    ),
+            )
+            .child(
+                div()
+                    .flex()
+                    .flex_1()
+                    .gap_3()
+                    .h_24()
+                    .text_color(gpui::white())
+                    .child(
+                        div().flex_1().rounded_xl().bg(linear_gradient(
+                            45.,
+                            linear_color_stop(gpui::red(), 0.),
+                            linear_color_stop(gpui::blue(), 1.),
+                        )
+                        .color_space(color_space)),
+                    )
+                    .child(
+                        div().flex_1().rounded_xl().bg(linear_gradient(
+                            135.,
+                            linear_color_stop(gpui::red(), 0.),
+                            linear_color_stop(gpui::green(), 1.),
+                        )
+                        .color_space(color_space)),
+                    )
+                    .child(
+                        div().flex_1().rounded_xl().bg(linear_gradient(
+                            225.,
+                            linear_color_stop(gpui::green(), 0.),
+                            linear_color_stop(gpui::blue(), 1.),
+                        )
+                        .color_space(color_space)),
+                    )
+                    .child(
+                        div().flex_1().rounded_xl().bg(linear_gradient(
+                            315.,
+                            linear_color_stop(gpui::green(), 0.),
+                            linear_color_stop(gpui::yellow(), 1.),
+                        )
+                        .color_space(color_space)),
+                    ),
+            )
+            .child(
+                div()
+                    .flex()
+                    .flex_1()
+                    .gap_3()
+                    .h_24()
+                    .text_color(gpui::white())
+                    .child(
+                        div().flex_1().rounded_xl().bg(linear_gradient(
+                            0.,
+                            linear_color_stop(gpui::red(), 0.),
+                            linear_color_stop(gpui::white(), 1.),
+                        )
+                        .color_space(color_space)),
+                    )
+                    .child(
+                        div().flex_1().rounded_xl().bg(linear_gradient(
+                            90.,
+                            linear_color_stop(gpui::blue(), 0.),
+                            linear_color_stop(gpui::white(), 1.),
+                        )
+                        .color_space(color_space)),
+                    )
+                    .child(
+                        div().flex_1().rounded_xl().bg(linear_gradient(
+                            180.,
+                            linear_color_stop(gpui::green(), 0.),
+                            linear_color_stop(gpui::white(), 1.),
+                        )
+                        .color_space(color_space)),
+                    )
+                    .child(
+                        div().flex_1().rounded_xl().bg(linear_gradient(
+                            360.,
+                            linear_color_stop(gpui::yellow(), 0.),
+                            linear_color_stop(gpui::white(), 1.),
+                        )
+                        .color_space(color_space)),
+                    ),
+            )
+            .child(
+                div().flex_1().rounded_xl().bg(linear_gradient(
+                    0.,
+                    linear_color_stop(gpui::green(), 0.05),
+                    linear_color_stop(gpui::yellow(), 0.95),
+                )
+                .color_space(color_space)),
+            )
+            .child(
+                div().flex_1().rounded_xl().bg(linear_gradient(
+                    90.,
+                    linear_color_stop(gpui::blue(), 0.05),
+                    linear_color_stop(gpui::red(), 0.95),
+                )
+                .color_space(color_space)),
+            )
+            .child(
+                div()
+                    .flex()
+                    .flex_1()
+                    .gap_3()
+                    .child(
+                        div().flex().flex_1().gap_3().child(
+                            div().flex_1().rounded_xl().bg(linear_gradient(
+                                90.,
+                                linear_color_stop(gpui::blue(), 0.5),
+                                linear_color_stop(gpui::red(), 0.5),
+                            )
+                            .color_space(color_space)),
+                        ),
+                    )
+                    .child(
+                        div().flex_1().rounded_xl().bg(linear_gradient(
+                            180.,
+                            linear_color_stop(gpui::green(), 0.),
+                            linear_color_stop(gpui::blue(), 0.5),
+                        )
+                        .color_space(color_space)),
+                    ),
+            )
+            .child(div().h_24().child(canvas(
+                move |_, _| {},
+                move |bounds, _, cx| {
+                    let size = size(bounds.size.width * 0.8, px(80.));
+                    let square_bounds = Bounds {
+                        origin: point(
+                            bounds.size.width.half() - size.width.half(),
+                            bounds.origin.y,
+                        ),
+                        size,
+                    };
+                    let height = square_bounds.size.height;
+                    let horizontal_offset = height;
+                    let vertical_offset = px(30.);
+                    let mut path = gpui::Path::new(square_bounds.lower_left());
+                    path.line_to(square_bounds.origin + point(horizontal_offset, vertical_offset));
+                    path.line_to(
+                        square_bounds.upper_right() + point(-horizontal_offset, vertical_offset),
+                    );
+                    path.line_to(square_bounds.lower_right());
+                    path.line_to(square_bounds.lower_left());
+                    cx.paint_path(
+                        path,
+                        linear_gradient(
+                            180.,
+                            linear_color_stop(gpui::red(), 0.),
+                            linear_color_stop(gpui::blue(), 1.),
+                        )
+                        .color_space(color_space),
+                    );
+                },
+            )))
+    }
+}
+
+fn main() {
+    App::new().run(|cx: &mut AppContext| {
+        cx.open_window(
+            WindowOptions {
+                focus: true,
+                ..Default::default()
+            },
+            |cx| cx.new_view(|_| GradientViewer::new()),
+        )
+        .unwrap();
+        cx.activate(true);
+    });
+}

crates/gpui/src/color.rs 🔗

@@ -548,6 +548,164 @@ impl<'de> Deserialize<'de> for Hsla {
     }
 }
 
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[repr(C)]
+pub(crate) enum BackgroundTag {
+    Solid = 0,
+    LinearGradient = 1,
+}
+
+/// A color space for color interpolation.
+///
+/// References:
+/// - https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method
+/// - https://www.w3.org/TR/css-color-4/#typedef-color-space
+#[derive(Debug, Clone, Copy, PartialEq, Default)]
+#[repr(C)]
+pub enum ColorSpace {
+    #[default]
+    /// The sRGB color space.
+    Srgb = 0,
+    /// The Oklab color space.
+    Oklab = 1,
+}
+
+impl Display for ColorSpace {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        match self {
+            ColorSpace::Srgb => write!(f, "sRGB"),
+            ColorSpace::Oklab => write!(f, "Oklab"),
+        }
+    }
+}
+
+/// A background color, which can be either a solid color or a linear gradient.
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[repr(C)]
+pub struct Background {
+    pub(crate) tag: BackgroundTag,
+    pub(crate) color_space: ColorSpace,
+    pub(crate) solid: Hsla,
+    pub(crate) angle: f32,
+    pub(crate) colors: [LinearColorStop; 2],
+    /// Padding for alignment for repr(C) layout.
+    pad: u32,
+}
+
+impl Eq for Background {}
+impl Default for Background {
+    fn default() -> Self {
+        Self {
+            tag: BackgroundTag::Solid,
+            solid: Hsla::default(),
+            color_space: ColorSpace::default(),
+            angle: 0.0,
+            colors: [LinearColorStop::default(), LinearColorStop::default()],
+            pad: 0,
+        }
+    }
+}
+
+/// Creates a LinearGradient background color.
+///
+/// The gradient line's angle of direction. A value of `0.` is equivalent to to top; increasing values rotate clockwise from there.
+///
+/// The `angle` is in degrees value in the range 0.0 to 360.0.
+///
+/// https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient
+pub fn linear_gradient(
+    angle: f32,
+    from: impl Into<LinearColorStop>,
+    to: impl Into<LinearColorStop>,
+) -> Background {
+    Background {
+        tag: BackgroundTag::LinearGradient,
+        angle,
+        colors: [from.into(), to.into()],
+        ..Default::default()
+    }
+}
+
+/// A color stop in a linear gradient.
+///
+/// https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient#linear-color-stop
+#[derive(Debug, Clone, Copy, Default, PartialEq)]
+#[repr(C)]
+pub struct LinearColorStop {
+    /// The color of the color stop.
+    pub color: Hsla,
+    /// The percentage of the gradient, in the range 0.0 to 1.0.
+    pub percentage: f32,
+}
+
+/// Creates a new linear color stop.
+///
+/// The percentage of the gradient, in the range 0.0 to 1.0.
+pub fn linear_color_stop(color: impl Into<Hsla>, percentage: f32) -> LinearColorStop {
+    LinearColorStop {
+        color: color.into(),
+        percentage,
+    }
+}
+
+impl LinearColorStop {
+    /// Returns a new color stop with the same color, but with a modified alpha value.
+    pub fn opacity(&self, factor: f32) -> Self {
+        Self {
+            percentage: self.percentage,
+            color: self.color.opacity(factor),
+        }
+    }
+}
+
+impl Background {
+    /// Use specified color space for color interpolation.
+    ///
+    /// https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method
+    pub fn color_space(mut self, color_space: ColorSpace) -> Self {
+        self.color_space = color_space;
+        self
+    }
+
+    /// Returns a new background color with the same hue, saturation, and lightness, but with a modified alpha value.
+    pub fn opacity(&self, factor: f32) -> Self {
+        let mut background = *self;
+        background.solid = background.solid.opacity(factor);
+        background.colors = [
+            self.colors[0].opacity(factor),
+            self.colors[1].opacity(factor),
+        ];
+        background
+    }
+
+    /// Returns whether the background color is transparent.
+    pub fn is_transparent(&self) -> bool {
+        match self.tag {
+            BackgroundTag::Solid => self.solid.is_transparent(),
+            BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.is_transparent()),
+        }
+    }
+}
+
+impl From<Hsla> for Background {
+    fn from(value: Hsla) -> Self {
+        Background {
+            tag: BackgroundTag::Solid,
+            solid: value,
+            ..Default::default()
+        }
+    }
+}
+impl From<Rgba> for Background {
+    fn from(value: Rgba) -> Self {
+        Background {
+            tag: BackgroundTag::Solid,
+            solid: Hsla::from(value),
+            ..Default::default()
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use serde_json::json;
@@ -595,4 +753,32 @@ mod tests {
 
         assert_eq!(actual, rgba(0xdeadbeef))
     }
+
+    #[test]
+    fn test_background_solid() {
+        let color = Hsla::from(rgba(0xff0099ff));
+        let mut background = Background::from(color);
+        assert_eq!(background.tag, BackgroundTag::Solid);
+        assert_eq!(background.solid, color);
+
+        assert_eq!(background.opacity(0.5).solid, color.opacity(0.5));
+        assert_eq!(background.is_transparent(), false);
+        background.solid = hsla(0.0, 0.0, 0.0, 0.0);
+        assert_eq!(background.is_transparent(), true);
+    }
+
+    #[test]
+    fn test_background_linear_gradient() {
+        let from = linear_color_stop(rgba(0xff0099ff), 0.0);
+        let to = linear_color_stop(rgba(0x00ff99ff), 1.0);
+        let background = linear_gradient(90.0, from, to);
+        assert_eq!(background.tag, BackgroundTag::LinearGradient);
+        assert_eq!(background.colors[0], from);
+        assert_eq!(background.colors[1], to);
+
+        assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5));
+        assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5));
+        assert_eq!(background.is_transparent(), false);
+        assert_eq!(background.opacity(0.0).is_transparent(), true);
+    }
 }

crates/gpui/src/platform/blade/blade_renderer.rs 🔗

@@ -3,7 +3,7 @@
 
 use super::{BladeAtlas, PATH_TEXTURE_FORMAT};
 use crate::{
-    AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels, GPUSpecs, Hsla,
+    AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask, DevicePixels, GPUSpecs,
     MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad,
     ScaledPixels, Scene, Shadow, Size, Underline,
 };
@@ -174,7 +174,7 @@ struct ShaderSurfacesData {
 #[repr(C)]
 struct PathSprite {
     bounds: Bounds<ScaledPixels>,
-    color: Hsla,
+    color: Background,
     tile: AtlasTile,
 }
 

crates/gpui/src/platform/blade/shaders.wgsl 🔗

@@ -15,18 +15,21 @@ struct Bounds {
     origin: vec2<f32>,
     size: vec2<f32>,
 }
+
 struct Corners {
     top_left: f32,
     top_right: f32,
     bottom_right: f32,
     bottom_left: f32,
 }
+
 struct Edges {
     top: f32,
     right: f32,
     bottom: f32,
     left: f32,
 }
+
 struct Hsla {
     h: f32,
     s: f32,
@@ -34,6 +37,24 @@ struct Hsla {
     a: f32,
 }
 
+struct LinearColorStop {
+    color: Hsla,
+    percentage: f32,
+}
+
+struct Background {
+    // 0u is Solid
+    // 1u is LinearGradient
+    tag: u32,
+    // 0u is sRGB linear color
+    // 1u is Oklab color
+    color_space: u32,
+    solid: Hsla,
+    angle: f32,
+    colors: array<LinearColorStop, 2>,
+    pad: u32,
+}
+
 struct AtlasTextureId {
     index: u32,
     kind: u32,
@@ -43,6 +64,7 @@ struct AtlasBounds {
     origin: vec2<i32>,
     size: vec2<i32>,
 }
+
 struct AtlasTile {
     texture_id: AtlasTextureId,
     tile_id: u32,
@@ -96,6 +118,24 @@ fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
     return select(higher, lower, cutoff);
 }
 
+fn linear_to_srgb(linear: vec3<f32>) -> vec3<f32> {
+    let cutoff = linear < vec3<f32>(0.0031308);
+    let higher = vec3<f32>(1.055) * pow(linear, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055);
+    let lower = linear * vec3<f32>(12.92);
+    return select(higher, lower, cutoff);
+}
+
+/// Convert a linear color to sRGBA space.
+fn linear_to_srgba(color: vec4<f32>) -> vec4<f32> {
+    return vec4<f32>(linear_to_srgb(color.rgb), color.a);
+}
+
+/// Convert a sRGBA color to linear space.
+fn srgba_to_linear(color: vec4<f32>) -> vec4<f32> {
+    return vec4<f32>(srgb_to_linear(color.rgb), color.a);
+}
+
+/// Hsla to linear RGBA conversion.
 fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
     let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
     let s = hsla.s;
@@ -135,6 +175,43 @@ fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
     return vec4<f32>(linear, a);
 }
 
+/// Convert a linear sRGB to Oklab space.
+/// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
+fn linear_srgb_to_oklab(color: vec4<f32>) -> vec4<f32> {
+	let l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b;
+	let m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b;
+	let s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b;
+
+	let l_ = pow(l, 1.0 / 3.0);
+	let m_ = pow(m, 1.0 / 3.0);
+	let s_ = pow(s, 1.0 / 3.0);
+
+	return vec4<f32>(
+		0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
+		1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
+		0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
+		color.a
+	);
+}
+
+/// Convert an Oklab color to linear sRGB space.
+fn oklab_to_linear_srgb(color: vec4<f32>) -> vec4<f32> {
+	let l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b;
+	let m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b;
+	let s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b;
+
+	let l = l_ * l_ * l_;
+	let m = m_ * m_ * m_;
+	let s = s_ * s_ * s_;
+
+	return vec4<f32>(
+		4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
+		-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
+		-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
+		color.a
+	);
+}
+
 fn over(below: vec4<f32>, above: vec4<f32>) -> vec4<f32> {
     let alpha = above.a + below.a * (1.0 - above.a);
     let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha;
@@ -197,6 +274,94 @@ fn blend_color(color: vec4<f32>, alpha_factor: f32) -> vec4<f32> {
     return vec4<f32>(color.rgb * multiplier, alpha);
 }
 
+
+struct GradientColor {
+    solid: vec4<f32>,
+    color0: vec4<f32>,
+    color1: vec4<f32>,
+}
+
+fn prepare_gradient_color(tag: u32, color_space: u32,
+    solid: Hsla, colors: array<LinearColorStop, 2>) -> GradientColor {
+    var result = GradientColor();
+
+    if (tag == 0u) {
+        result.solid = hsla_to_rgba(solid);
+    } else if (tag == 1u) {
+        // The hsla_to_rgba is returns a linear sRGB color
+        result.color0 = hsla_to_rgba(colors[0].color);
+        result.color1 = hsla_to_rgba(colors[1].color);
+
+        // Prepare color space in vertex for avoid conversion
+        // in fragment shader for performance reasons
+        if (color_space == 0u) {
+            // sRGB
+            result.color0 = linear_to_srgba(result.color0);
+            result.color1 = linear_to_srgba(result.color1);
+        } else if (color_space == 1u) {
+            // Oklab
+            result.color0 = linear_srgb_to_oklab(result.color0);
+            result.color1 = linear_srgb_to_oklab(result.color1);
+        }
+    }
+
+    return result;
+}
+
+fn gradient_color(background: Background, position: vec2<f32>, bounds: Bounds,
+    sold_color: vec4<f32>, color0: vec4<f32>, color1: vec4<f32>) -> vec4<f32> {
+    var background_color = vec4<f32>(0.0);
+
+    switch (background.tag) {
+        default: {
+            return sold_color;
+        }
+        case 1u: {
+            // Linear gradient background.
+            // -90 degrees to match the CSS gradient angle.
+            let radians = (background.angle % 360.0 - 90.0) * M_PI_F / 180.0;
+            var direction = vec2<f32>(cos(radians), sin(radians));
+            let stop0_percentage = background.colors[0].percentage;
+            let stop1_percentage = background.colors[1].percentage;
+
+            // Expand the short side to be the same as the long side
+            if (bounds.size.x > bounds.size.y) {
+                direction.y *= bounds.size.y / bounds.size.x;
+            } else {
+                direction.x *= bounds.size.x / bounds.size.y;
+            }
+
+            // Get the t value for the linear gradient with the color stop percentages.
+            let half_size = bounds.size / 2.0;
+            let center = bounds.origin + half_size;
+            let center_to_point = position - center;
+            var t = dot(center_to_point, direction) / length(direction);
+            // Check the direct to determine the use x or y
+            if (abs(direction.x) > abs(direction.y)) {
+                t = (t + half_size.x) / bounds.size.x;
+            } else {
+                t = (t + half_size.y) / bounds.size.y;
+            }
+
+            // Adjust t based on the stop percentages
+            t = (t - stop0_percentage) / (stop1_percentage - stop0_percentage);
+            t = clamp(t, 0.0, 1.0);
+
+            switch (background.color_space) {
+                default: {
+                    background_color = srgba_to_linear(mix(color0, color1, t));
+                }
+                case 1u: {
+                    let oklab_color = mix(color0, color1, t);
+                    background_color = oklab_to_linear_srgb(oklab_color);
+                }
+            }
+        }
+    }
+
+    return background_color;
+}
+
 // --- quads --- //
 
 struct Quad {
@@ -204,7 +369,7 @@ struct Quad {
     pad: u32,
     bounds: Bounds,
     content_mask: Bounds,
-    background: Hsla,
+    background: Background,
     border_color: Hsla,
     corner_radii: Corners,
     border_widths: Edges,
@@ -213,11 +378,13 @@ var<storage, read> b_quads: array<Quad>;
 
 struct QuadVarying {
     @builtin(position) position: vec4<f32>,
-    @location(0) @interpolate(flat) background_color: vec4<f32>,
-    @location(1) @interpolate(flat) border_color: vec4<f32>,
-    @location(2) @interpolate(flat) quad_id: u32,
-    //TODO: use `clip_distance` once Naga supports it
-    @location(3) clip_distances: vec4<f32>,
+    @location(0) @interpolate(flat) border_color: vec4<f32>,
+    @location(1) @interpolate(flat) quad_id: u32,
+    // TODO: use `clip_distance` once Naga supports it
+    @location(2) clip_distances: vec4<f32>,
+    @location(3) @interpolate(flat) background_solid: vec4<f32>,
+    @location(4) @interpolate(flat) background_color0: vec4<f32>,
+    @location(5) @interpolate(flat) background_color1: vec4<f32>,
 }
 
 @vertex
@@ -227,7 +394,16 @@ fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
 
     var out = QuadVarying();
     out.position = to_device_position(unit_vertex, quad.bounds);
-    out.background_color = hsla_to_rgba(quad.background);
+
+    let gradient = prepare_gradient_color(
+        quad.background.tag,
+        quad.background.color_space,
+        quad.background.solid,
+        quad.background.colors
+    );
+    out.background_solid = gradient.solid;
+    out.background_color0 = gradient.color0;
+    out.background_color1 = gradient.color1;
     out.border_color = hsla_to_rgba(quad.border_color);
     out.quad_id = instance_id;
     out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
@@ -242,21 +418,23 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
     }
 
     let quad = b_quads[input.quad_id];
+    let half_size = quad.bounds.size / 2.0;
+    let center = quad.bounds.origin + half_size;
+    let center_to_point = input.position.xy - center;
+
+    let background_color = gradient_color(quad.background, input.position.xy, quad.bounds,
+        input.background_solid, input.background_color0, input.background_color1);
+
     // Fast path when the quad is not rounded and doesn't have any border.
     if (quad.corner_radii.top_left == 0.0 && quad.corner_radii.bottom_left == 0.0 &&
         quad.corner_radii.top_right == 0.0 &&
         quad.corner_radii.bottom_right == 0.0 && quad.border_widths.top == 0.0 &&
         quad.border_widths.left == 0.0 && quad.border_widths.right == 0.0 &&
         quad.border_widths.bottom == 0.0) {
-        return blend_color(input.background_color, 1.0);
+        return blend_color(background_color, 1.0);
     }
 
-    let half_size = quad.bounds.size / 2.0;
-    let center = quad.bounds.origin + half_size;
-    let center_to_point = input.position.xy - center;
-
     let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
-
     let rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
     let distance =
       length(max(vec2<f32>(0.0), rounded_edge_to_point)) +
@@ -277,13 +455,13 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
         border_width = vertical_border;
     }
 
-    var color = input.background_color;
+    var color = background_color;
     if (border_width > 0.0) {
         let inset_distance = distance + border_width;
         // Blend the border on top of the background and then linearly interpolate
         // between the two as we slide inside the background.
-        let blended_border = over(input.background_color, input.border_color);
-        color = mix(blended_border, input.background_color,
+        let blended_border = over(background_color, input.border_color);
+        color = mix(blended_border, background_color,
                     saturate(0.5 - inset_distance));
     }
 
@@ -408,7 +586,7 @@ fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) f32 {
 
 struct PathSprite {
     bounds: Bounds,
-    color: Hsla,
+    color: Background,
     tile: AtlasTile,
 }
 var<storage, read> b_path_sprites: array<PathSprite>;
@@ -416,7 +594,10 @@ var<storage, read> b_path_sprites: array<PathSprite>;
 struct PathVarying {
     @builtin(position) position: vec4<f32>,
     @location(0) tile_position: vec2<f32>,
-    @location(1) color: vec4<f32>,
+    @location(1) @interpolate(flat) instance_id: u32,
+    @location(2) @interpolate(flat) color_solid: vec4<f32>,
+    @location(3) @interpolate(flat) color0: vec4<f32>,
+    @location(4) @interpolate(flat) color1: vec4<f32>,
 }
 
 @vertex
@@ -428,7 +609,17 @@ fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
     var out = PathVarying();
     out.position = to_device_position(unit_vertex, sprite.bounds);
     out.tile_position = to_tile_position(unit_vertex, sprite.tile);
-    out.color = hsla_to_rgba(sprite.color);
+    out.instance_id = instance_id;
+
+    let gradient = prepare_gradient_color(
+        sprite.color.tag,
+        sprite.color.color_space,
+        sprite.color.solid,
+        sprite.color.colors
+    );
+    out.color_solid = gradient.solid;
+    out.color0 = gradient.color0;
+    out.color1 = gradient.color1;
     return out;
 }
 
@@ -436,7 +627,11 @@ fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
 fn fs_path(input: PathVarying) -> @location(0) vec4<f32> {
     let sample = textureSample(t_sprite, s_sprite, input.tile_position).r;
     let mask = 1.0 - abs(1.0 - sample % 2.0);
-    return blend_color(input.color, mask);
+    let sprite = b_path_sprites[input.instance_id];
+    let background = sprite.color;
+    let color = gradient_color(background, input.position.xy, sprite.bounds,
+        input.color_solid, input.color0, input.color1);
+    return blend_color(color, mask);
 }
 
 // --- underlines --- //

crates/gpui/src/platform/mac/metal_renderer.rs 🔗

@@ -1,7 +1,7 @@
 use super::metal_atlas::MetalAtlas;
 use crate::{
-    point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels,
-    Hsla, MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite,
+    point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask,
+    DevicePixels, MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite,
     PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline,
 };
 use anyhow::{anyhow, Result};
@@ -1242,7 +1242,7 @@ enum PathRasterizationInputIndex {
 #[repr(C)]
 pub struct PathSprite {
     pub bounds: Bounds<ScaledPixels>,
-    pub color: Hsla,
+    pub color: Background,
     pub tile: AtlasTile,
 }
 

crates/gpui/src/platform/mac/shaders.metal 🔗

@@ -4,6 +4,10 @@
 using namespace metal;
 
 float4 hsla_to_rgba(Hsla hsla);
+float3 srgb_to_linear(float3 color);
+float3 linear_to_srgb(float3 color);
+float4 srgb_to_oklab(float4 color);
+float4 oklab_to_srgb(float4 color);
 float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds,
                           constant Size_DevicePixels *viewport_size);
 float4 to_device_position_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds,
@@ -21,20 +25,34 @@ float2 erf(float2 x);
 float blur_along_x(float x, float y, float sigma, float corner,
                    float2 half_size);
 float4 over(float4 below, float4 above);
+float radians(float degrees);
+float4 gradient_color(Background background, float2 position, Bounds_ScaledPixels bounds,
+  float4 solid_color, float4 color0, float4 color1);
+
+struct GradientColor {
+  float4 solid;
+  float4 color0;
+  float4 color1;
+};
+GradientColor prepare_gradient_color(uint tag, uint color_space, Hsla solid, Hsla color0, Hsla color1);
 
 struct QuadVertexOutput {
+  uint quad_id [[flat]];
   float4 position [[position]];
-  float4 background_color [[flat]];
   float4 border_color [[flat]];
-  uint quad_id [[flat]];
+  float4 background_solid [[flat]];
+  float4 background_color0 [[flat]];
+  float4 background_color1 [[flat]];
   float clip_distance [[clip_distance]][4];
 };
 
 struct QuadFragmentInput {
+  uint quad_id [[flat]];
   float4 position [[position]];
-  float4 background_color [[flat]];
   float4 border_color [[flat]];
-  uint quad_id [[flat]];
+  float4 background_solid [[flat]];
+  float4 background_color0 [[flat]];
+  float4 background_color1 [[flat]];
 };
 
 vertex QuadVertexOutput quad_vertex(uint unit_vertex_id [[vertex_id]],
@@ -51,13 +69,23 @@ vertex QuadVertexOutput quad_vertex(uint unit_vertex_id [[vertex_id]],
       to_device_position(unit_vertex, quad.bounds, viewport_size);
   float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds,
                                                  quad.content_mask.bounds);
-  float4 background_color = hsla_to_rgba(quad.background);
   float4 border_color = hsla_to_rgba(quad.border_color);
+
+  GradientColor gradient = prepare_gradient_color(
+    quad.background.tag,
+    quad.background.color_space,
+    quad.background.solid,
+    quad.background.colors[0].color,
+    quad.background.colors[1].color
+  );
+
   return QuadVertexOutput{
+      quad_id,
       device_position,
-      background_color,
       border_color,
-      quad_id,
+      gradient.solid,
+      gradient.color0,
+      gradient.color1,
       {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}};
 }
 
@@ -65,6 +93,11 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
                               constant Quad *quads
                               [[buffer(QuadInputIndex_Quads)]]) {
   Quad quad = quads[input.quad_id];
+  float2 half_size = float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
+  float2 center = float2(quad.bounds.origin.x, quad.bounds.origin.y) + half_size;
+  float2 center_to_point = input.position.xy - center;
+  float4 color = gradient_color(quad.background, input.position.xy, quad.bounds,
+    input.background_solid, input.background_color0, input.background_color1);
 
   // Fast path when the quad is not rounded and doesn't have any border.
   if (quad.corner_radii.top_left == 0. && quad.corner_radii.bottom_left == 0. &&
@@ -72,14 +105,9 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
       quad.corner_radii.bottom_right == 0. && quad.border_widths.top == 0. &&
       quad.border_widths.left == 0. && quad.border_widths.right == 0. &&
       quad.border_widths.bottom == 0.) {
-    return input.background_color;
+    return color;
   }
 
-  float2 half_size =
-      float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
-  float2 center =
-      float2(quad.bounds.origin.x, quad.bounds.origin.y) + half_size;
-  float2 center_to_point = input.position.xy - center;
   float corner_radius;
   if (center_to_point.x < 0.) {
     if (center_to_point.y < 0.) {
@@ -118,15 +146,12 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
     border_width = vertical_border;
   }
 
-  float4 color;
-  if (border_width == 0.) {
-    color = input.background_color;
-  } else {
+  if (border_width != 0.) {
     float inset_distance = distance + border_width;
     // Blend the border on top of the background and then linearly interpolate
     // between the two as we slide inside the background.
-    float4 blended_border = over(input.background_color, input.border_color);
-    color = mix(blended_border, input.background_color,
+    float4 blended_border = over(color, input.border_color);
+    color = mix(blended_border, color,
                 saturate(0.5 - inset_distance));
   }
 
@@ -437,7 +462,10 @@ fragment float4 path_rasterization_fragment(PathRasterizationFragmentInput input
 struct PathSpriteVertexOutput {
   float4 position [[position]];
   float2 tile_position;
-  float4 color [[flat]];
+  uint sprite_id [[flat]];
+  float4 solid_color [[flat]];
+  float4 color0 [[flat]];
+  float4 color1 [[flat]];
 };
 
 vertex PathSpriteVertexOutput path_sprite_vertex(
@@ -456,8 +484,23 @@ vertex PathSpriteVertexOutput path_sprite_vertex(
   float4 device_position =
       to_device_position(unit_vertex, sprite.bounds, viewport_size);
   float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
-  float4 color = hsla_to_rgba(sprite.color);
-  return PathSpriteVertexOutput{device_position, tile_position, color};
+
+  GradientColor gradient = prepare_gradient_color(
+    sprite.color.tag,
+    sprite.color.color_space,
+    sprite.color.solid,
+    sprite.color.colors[0].color,
+    sprite.color.colors[1].color
+  );
+
+  return PathSpriteVertexOutput{
+    device_position,
+    tile_position,
+    sprite_id,
+    gradient.solid,
+    gradient.color0,
+    gradient.color1
+  };
 }
 
 fragment float4 path_sprite_fragment(
@@ -469,7 +512,10 @@ fragment float4 path_sprite_fragment(
   float4 sample =
       atlas_texture.sample(atlas_texture_sampler, input.tile_position);
   float mask = 1. - abs(1. - fmod(sample.r, 2.));
-  float4 color = input.color;
+  PathSprite sprite = sprites[input.sprite_id];
+  Background background = sprite.color;
+  float4 color = gradient_color(background, input.position.xy, sprite.bounds,
+    input.solid_color, input.color0, input.color1);
   color.a *= mask;
   return color;
 }
@@ -574,6 +620,56 @@ float4 hsla_to_rgba(Hsla hsla) {
   return rgba;
 }
 
+float3 srgb_to_linear(float3 color) {
+  return pow(color, float3(2.2));
+}
+
+float3 linear_to_srgb(float3 color) {
+  return pow(color, float3(1.0 / 2.2));
+}
+
+// Converts a sRGB color to the Oklab color space.
+// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
+float4 srgb_to_oklab(float4 color) {
+  // Convert non-linear sRGB to linear sRGB
+  color = float4(srgb_to_linear(color.rgb), color.a);
+
+  float l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b;
+  float m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b;
+  float s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b;
+
+  float l_ = pow(l, 1.0/3.0);
+  float m_ = pow(m, 1.0/3.0);
+  float s_ = pow(s, 1.0/3.0);
+
+  return float4(
+   	0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
+   	1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
+   	0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
+   	color.a
+  );
+}
+
+// Converts an Oklab color to the sRGB color space.
+float4 oklab_to_srgb(float4 color) {
+  float l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b;
+  float m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b;
+  float s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b;
+
+  float l = l_ * l_ * l_;
+  float m = m_ * m_ * m_;
+  float s = s_ * s_ * s_;
+
+  float3 linear_rgb = float3(
+   	4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
+   	-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
+   	-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
+  );
+
+  // Convert linear sRGB to non-linear sRGB
+  return float4(linear_to_srgb(linear_rgb), color.a);
+}
+
 float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds,
                           constant Size_DevicePixels *input_viewport_size) {
   float2 position =
@@ -691,3 +787,81 @@ float4 over(float4 below, float4 above) {
   result.a = alpha;
   return result;
 }
+
+GradientColor prepare_gradient_color(uint tag, uint color_space, Hsla solid,
+                                     Hsla color0, Hsla color1) {
+  GradientColor out;
+  if (tag == 0) {
+    out.solid = hsla_to_rgba(solid);
+  } else if (tag == 1) {
+    out.color0 = hsla_to_rgba(color0);
+    out.color1 = hsla_to_rgba(color1);
+
+    // Prepare color space in vertex for avoid conversion
+    // in fragment shader for performance reasons
+    if (color_space == 1) {
+      // Oklab
+      out.color0 = srgb_to_oklab(out.color0);
+      out.color1 = srgb_to_oklab(out.color1);
+    }
+  }
+
+  return out;
+}
+
+float4 gradient_color(Background background,
+                      float2 position,
+                      Bounds_ScaledPixels bounds,
+                      float4 solid_color, float4 color0, float4 color1) {
+  float4 color;
+
+  switch (background.tag) {
+    case 0:
+      color = solid_color;
+      break;
+    case 1: {
+      // -90 degrees to match the CSS gradient angle.
+      float radians = (fmod(background.angle, 360.0) - 90.0) * (M_PI_F / 180.0);
+      float2 direction = float2(cos(radians), sin(radians));
+
+      // Expand the short side to be the same as the long side
+      if (bounds.size.width > bounds.size.height) {
+          direction.y *= bounds.size.height / bounds.size.width;
+      } else {
+          direction.x *=  bounds.size.width / bounds.size.height;
+      }
+
+      // Get the t value for the linear gradient with the color stop percentages.
+      float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.;
+      float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size;
+      float2 center_to_point = position - center;
+      float t = dot(center_to_point, direction) / length(direction);
+      // Check the direct to determine the use x or y
+      if (abs(direction.x) > abs(direction.y)) {
+          t = (t + half_size.x) / bounds.size.width;
+      } else {
+          t = (t + half_size.y) / bounds.size.height;
+      }
+
+      // Adjust t based on the stop percentages
+      t = (t - background.colors[0].percentage)
+        / (background.colors[1].percentage
+        - background.colors[0].percentage);
+      t = clamp(t, 0.0, 1.0);
+
+      switch (background.color_space) {
+        case 0:
+          color = mix(color0, color1, t);
+          break;
+        case 1: {
+          float4 oklab_color = mix(color0, color1, t);
+          color = oklab_to_srgb(oklab_color);
+          break;
+        }
+      }
+      break;
+    }
+  }
+
+  return color;
+}

crates/gpui/src/scene.rs 🔗

@@ -2,8 +2,8 @@
 #![cfg_attr(windows, allow(dead_code))]
 
 use crate::{
-    bounds_tree::BoundsTree, point, AtlasTextureId, AtlasTile, Bounds, ContentMask, Corners, Edges,
-    Hsla, Pixels, Point, Radians, ScaledPixels, Size,
+    bounds_tree::BoundsTree, point, AtlasTextureId, AtlasTile, Background, Bounds, ContentMask,
+    Corners, Edges, Hsla, Pixels, Point, Radians, ScaledPixels, Size,
 };
 use std::{fmt::Debug, iter::Peekable, ops::Range, slice};
 
@@ -458,7 +458,7 @@ pub(crate) struct Quad {
     pub pad: u32, // align to 8 bytes
     pub bounds: Bounds<ScaledPixels>,
     pub content_mask: ContentMask<ScaledPixels>,
-    pub background: Hsla,
+    pub background: Background,
     pub border_color: Hsla,
     pub corner_radii: Corners<ScaledPixels>,
     pub border_widths: Edges<ScaledPixels>,
@@ -671,7 +671,7 @@ pub struct Path<P: Clone + Default + Debug> {
     pub(crate) bounds: Bounds<P>,
     pub(crate) content_mask: ContentMask<P>,
     pub(crate) vertices: Vec<PathVertex<P>>,
-    pub(crate) color: Hsla,
+    pub(crate) color: Background,
     start: Point<P>,
     current: Point<P>,
     contour_count: usize,

crates/gpui/src/style.rs 🔗

@@ -5,10 +5,11 @@ use std::{
 };
 
 use crate::{
-    black, phi, point, quad, rems, size, AbsoluteLength, Bounds, ContentMask, Corners,
-    CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement, Font,
-    FontFallbacks, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point,
-    PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
+    black, phi, point, quad, rems, size, AbsoluteLength, Background, BackgroundTag, Bounds,
+    ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges,
+    EdgesRefinement, Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, Hsla, Length,
+    Pixels, Point, PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun,
+    WindowContext,
 };
 use collections::HashSet;
 use refineable::Refineable;
@@ -572,7 +573,17 @@ impl Style {
 
         let background_color = self.background.as_ref().and_then(Fill::color);
         if background_color.map_or(false, |color| !color.is_transparent()) {
-            let mut border_color = background_color.unwrap_or_default();
+            let mut border_color = match background_color {
+                Some(color) => match color.tag {
+                    BackgroundTag::Solid => color.solid,
+                    BackgroundTag::LinearGradient => color
+                        .colors
+                        .first()
+                        .map(|stop| stop.color)
+                        .unwrap_or_default(),
+                },
+                None => Hsla::default(),
+            };
             border_color.a = 0.;
             cx.paint_quad(quad(
                 bounds,
@@ -737,12 +748,14 @@ pub struct StrikethroughStyle {
 #[derive(Clone, Debug)]
 pub enum Fill {
     /// A solid color fill.
-    Color(Hsla),
+    Color(Background),
 }
 
 impl Fill {
     /// Unwrap this fill into a solid color, if it is one.
-    pub fn color(&self) -> Option<Hsla> {
+    ///
+    /// If the fill is not a solid color, this method returns `None`.
+    pub fn color(&self) -> Option<Background> {
         match self {
             Fill::Color(color) => Some(*color),
         }
@@ -751,13 +764,13 @@ impl Fill {
 
 impl Default for Fill {
     fn default() -> Self {
-        Self::Color(Hsla::default())
+        Self::Color(Background::default())
     }
 }
 
 impl From<Hsla> for Fill {
     fn from(color: Hsla) -> Self {
-        Self::Color(color)
+        Self::Color(color.into())
     }
 }
 
@@ -767,6 +780,12 @@ impl From<Rgba> for Fill {
     }
 }
 
+impl From<Background> for Fill {
+    fn from(background: Background) -> Self {
+        Self::Color(background)
+    }
+}
+
 impl From<TextStyle> for HighlightStyle {
     fn from(other: TextStyle) -> Self {
         Self::from(&other)

crates/gpui/src/window.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     point, prelude::*, px, size, transparent_black, Action, AnyDrag, AnyElement, AnyTooltip,
-    AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow,
-    Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
+    AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Background, Bounds,
+    BoxShadow, Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
     DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
     FileDropEvent, Flatten, FontId, GPUSpecs, Global, GlobalElementId, GlyphId, Hsla, InputHandler,
     IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent,
@@ -2325,7 +2325,7 @@ impl<'a> WindowContext<'a> {
     /// Paint the given `Path` into the scene for the next frame at the current z-index.
     ///
     /// This method should only be called as part of the paint phase of element drawing.
-    pub fn paint_path(&mut self, mut path: Path<Pixels>, color: impl Into<Hsla>) {
+    pub fn paint_path(&mut self, mut path: Path<Pixels>, color: impl Into<Background>) {
         debug_assert_eq!(
             self.window.draw_phase,
             DrawPhase::Paint,
@@ -2336,7 +2336,8 @@ impl<'a> WindowContext<'a> {
         let content_mask = self.content_mask();
         let opacity = self.element_opacity();
         path.content_mask = content_mask;
-        path.color = color.into().opacity(opacity);
+        let color: Background = color.into();
+        path.color = color.opacity(opacity);
         self.window
             .next_frame
             .scene
@@ -4980,7 +4981,7 @@ pub struct PaintQuad {
     /// The radii of the quad's corners.
     pub corner_radii: Corners<Pixels>,
     /// The background color of the quad.
-    pub background: Hsla,
+    pub background: Background,
     /// The widths of the quad's borders.
     pub border_widths: Edges<Pixels>,
     /// The color of the quad's borders.
@@ -5013,7 +5014,7 @@ impl PaintQuad {
     }
 
     /// Sets the background color of the quad.
-    pub fn background(self, background: impl Into<Hsla>) -> Self {
+    pub fn background(self, background: impl Into<Background>) -> Self {
         PaintQuad {
             background: background.into(),
             ..self
@@ -5025,7 +5026,7 @@ impl PaintQuad {
 pub fn quad(
     bounds: Bounds<Pixels>,
     corner_radii: impl Into<Corners<Pixels>>,
-    background: impl Into<Hsla>,
+    background: impl Into<Background>,
     border_widths: impl Into<Edges<Pixels>>,
     border_color: impl Into<Hsla>,
 ) -> PaintQuad {
@@ -5039,7 +5040,7 @@ pub fn quad(
 }
 
 /// Creates a filled quad with the given bounds and background color.
-pub fn fill(bounds: impl Into<Bounds<Pixels>>, background: impl Into<Hsla>) -> PaintQuad {
+pub fn fill(bounds: impl Into<Bounds<Pixels>>, background: impl Into<Background>) -> PaintQuad {
     PaintQuad {
         bounds: bounds.into(),
         corner_radii: (0.).into(),
@@ -5054,7 +5055,7 @@ pub fn outline(bounds: impl Into<Bounds<Pixels>>, border_color: impl Into<Hsla>)
     PaintQuad {
         bounds: bounds.into(),
         corner_radii: (0.).into(),
-        background: transparent_black(),
+        background: transparent_black().into(),
         border_widths: (1.).into(),
         border_color: border_color.into(),
     }