Detailed changes
@@ -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);
+ });
+}
@@ -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);
+ }
}
@@ -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,
}
@@ -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 --- //
@@ -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,
}
@@ -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;
+}
@@ -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,
@@ -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)
@@ -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(),
}