From de89f8cf83089893ba81c713e24a36a69100e39e Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 12 Dec 2024 03:52:52 +0800 Subject: [PATCH] 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): image In GPUI (Oklab): image In CSS (sRGB): https://codepen.io/huacnlee/pen/rNXgxBY image In CSS (Oklab): https://codepen.io/huacnlee/pen/wBwBKOp image --- 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. --- crates/gpui/examples/gradient.rs | 254 ++++++++++++++++++ crates/gpui/src/color.rs | 186 +++++++++++++ .../gpui/src/platform/blade/blade_renderer.rs | 4 +- crates/gpui/src/platform/blade/shaders.wgsl | 235 ++++++++++++++-- .../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(-) create mode 100644 crates/gpui/examples/gradient.rs diff --git a/crates/gpui/examples/gradient.rs b/crates/gpui/examples/gradient.rs new file mode 100644 index 0000000000000..b0e1af5acb1c7 --- /dev/null +++ b/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) -> 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); + }); +} diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 04a35e6886456..19182b088b357 100644 --- a/crates/gpui/src/color.rs +++ b/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, + to: impl Into, +) -> 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, 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 for Background { + fn from(value: Hsla) -> Self { + Background { + tag: BackgroundTag::Solid, + solid: value, + ..Default::default() + } + } +} +impl From 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); + } } diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/crates/gpui/src/platform/blade/blade_renderer.rs index 860a86f2b6254..6781d46ed4224 100644 --- a/crates/gpui/src/platform/blade/blade_renderer.rs +++ b/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, - color: Hsla, + color: Background, tile: AtlasTile, } diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index 6099cbd93ad42..d497c40d7aa42 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/crates/gpui/src/platform/blade/shaders.wgsl @@ -15,18 +15,21 @@ struct Bounds { origin: vec2, size: vec2, } + 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, + pad: u32, +} + struct AtlasTextureId { index: u32, kind: u32, @@ -43,6 +64,7 @@ struct AtlasBounds { origin: vec2, size: vec2, } + struct AtlasTile { texture_id: AtlasTextureId, tile_id: u32, @@ -96,6 +118,24 @@ fn srgb_to_linear(srgb: vec3) -> vec3 { return select(higher, lower, cutoff); } +fn linear_to_srgb(linear: vec3) -> vec3 { + let cutoff = linear < vec3(0.0031308); + let higher = vec3(1.055) * pow(linear, vec3(1.0 / 2.4)) - vec3(0.055); + let lower = linear * vec3(12.92); + return select(higher, lower, cutoff); +} + +/// Convert a linear color to sRGBA space. +fn linear_to_srgba(color: vec4) -> vec4 { + return vec4(linear_to_srgb(color.rgb), color.a); +} + +/// Convert a sRGBA color to linear space. +fn srgba_to_linear(color: vec4) -> vec4 { + return vec4(srgb_to_linear(color.rgb), color.a); +} + +/// Hsla to linear RGBA conversion. fn hsla_to_rgba(hsla: Hsla) -> vec4 { 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 { return vec4(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) -> vec4 { + 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( + 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) -> vec4 { + 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( + 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, above: vec4) -> vec4 { 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, alpha_factor: f32) -> vec4 { return vec4(color.rgb * multiplier, alpha); } + +struct GradientColor { + solid: vec4, + color0: vec4, + color1: vec4, +} + +fn prepare_gradient_color(tag: u32, color_space: u32, + solid: Hsla, colors: array) -> 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, bounds: Bounds, + sold_color: vec4, color0: vec4, color1: vec4) -> vec4 { + var background_color = vec4(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(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 b_quads: array; struct QuadVarying { @builtin(position) position: vec4, - @location(0) @interpolate(flat) background_color: vec4, - @location(1) @interpolate(flat) border_color: vec4, - @location(2) @interpolate(flat) quad_id: u32, - //TODO: use `clip_distance` once Naga supports it - @location(3) clip_distances: vec4, + @location(0) @interpolate(flat) border_color: vec4, + @location(1) @interpolate(flat) quad_id: u32, + // TODO: use `clip_distance` once Naga supports it + @location(2) clip_distances: vec4, + @location(3) @interpolate(flat) background_solid: vec4, + @location(4) @interpolate(flat) background_color0: vec4, + @location(5) @interpolate(flat) background_color1: vec4, } @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 { } 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(0.0), rounded_edge_to_point)) + @@ -277,13 +455,13 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4 { 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 b_path_sprites: array; @@ -416,7 +594,10 @@ var b_path_sprites: array; struct PathVarying { @builtin(position) position: vec4, @location(0) tile_position: vec2, - @location(1) color: vec4, + @location(1) @interpolate(flat) instance_id: u32, + @location(2) @interpolate(flat) color_solid: vec4, + @location(3) @interpolate(flat) color0: vec4, + @location(4) @interpolate(flat) color1: vec4, } @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 { 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 --- // diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index f42a2e2df7b94..c290d12f7e752 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/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, - pub color: Hsla, + pub color: Background, pub tile: AtlasTile, } diff --git a/crates/gpui/src/platform/mac/shaders.metal b/crates/gpui/src/platform/mac/shaders.metal index 464e4b5903977..d8ad197a91d4f 100644 --- a/crates/gpui/src/platform/mac/shaders.metal +++ b/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; +} diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 418be6af22b96..778a5d1f27341 100644 --- a/crates/gpui/src/scene.rs +++ b/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, pub content_mask: ContentMask, - pub background: Hsla, + pub background: Background, pub border_color: Hsla, pub corner_radii: Corners, pub border_widths: Edges, @@ -671,7 +671,7 @@ pub struct Path { pub(crate) bounds: Bounds

, pub(crate) content_mask: ContentMask

, pub(crate) vertices: Vec>, - pub(crate) color: Hsla, + pub(crate) color: Background, start: Point

, current: Point

, contour_count: usize, diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index cfe1034891744..c35f4926e0856 100644 --- a/crates/gpui/src/style.rs +++ b/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 { + /// + /// If the fill is not a solid color, this method returns `None`. + pub fn color(&self) -> Option { 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 for Fill { fn from(color: Hsla) -> Self { - Self::Color(color) + Self::Color(color.into()) } } @@ -767,6 +780,12 @@ impl From for Fill { } } +impl From for Fill { + fn from(background: Background) -> Self { + Self::Color(background) + } +} + impl From for HighlightStyle { fn from(other: TextStyle) -> Self { Self::from(&other) diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 06298a81adb77..5b88870e7c7a0 100644 --- a/crates/gpui/src/window.rs +++ b/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, color: impl Into) { + pub fn paint_path(&mut self, mut path: Path, color: impl Into) { 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, /// The background color of the quad. - pub background: Hsla, + pub background: Background, /// The widths of the quad's borders. pub border_widths: Edges, /// 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) -> Self { + pub fn background(self, background: impl Into) -> Self { PaintQuad { background: background.into(), ..self @@ -5025,7 +5026,7 @@ impl PaintQuad { pub fn quad( bounds: Bounds, corner_radii: impl Into>, - background: impl Into, + background: impl Into, border_widths: impl Into>, border_color: impl Into, ) -> 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>, background: impl Into) -> PaintQuad { +pub fn fill(bounds: impl Into>, background: impl Into) -> PaintQuad { PaintQuad { bounds: bounds.into(), corner_radii: (0.).into(), @@ -5054,7 +5055,7 @@ pub fn outline(bounds: impl Into>, border_color: impl Into) PaintQuad { bounds: bounds.into(), corner_radii: (0.).into(), - background: transparent_black(), + background: transparent_black().into(), border_widths: (1.).into(), border_color: border_color.into(), }