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

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.
This commit is contained in:
Jason Lee 2024-12-12 03:52:52 +08:00 committed by GitHub
parent c594ccb0af
commit de89f8cf83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 902 additions and 73 deletions

View file

@ -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);
});
}

View file

@ -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);
}
}

View file

@ -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,
}

View file

@ -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 --- //

View file

@ -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,
}

View file

@ -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 {
float4 position [[position]];
float4 background_color [[flat]];
float4 border_color [[flat]];
uint quad_id [[flat]];
float4 position [[position]];
float4 border_color [[flat]];
float4 background_solid [[flat]];
float4 background_color0 [[flat]];
float4 background_color1 [[flat]];
float clip_distance [[clip_distance]][4];
};
struct QuadFragmentInput {
float4 position [[position]];
float4 background_color [[flat]];
float4 border_color [[flat]];
uint quad_id [[flat]];
float4 position [[position]];
float4 border_color [[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{
device_position,
background_color,
border_color,
quad_id,
device_position,
border_color,
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;
}

View file

@ -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,

View file

@ -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)

View file

@ -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(),
}