diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e6c1e5b791..b9ceca457d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -35,9 +35,9 @@ use git::{blame::BlameEntry, status::FileStatus, Oid}; use gpui::{ anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background, transparent_black, Action, AnyElement, App, AvailableSpace, - Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, - DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, - GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Keystroke, Length, + Axis, BorderStyle, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, + CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, + FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement, Window, @@ -4549,6 +4549,7 @@ impl EditorElement { flattened_background_color, Edges::default(), transparent_black(), + BorderStyle::default(), )); } else { let flattened_unstaged_background_color = cx @@ -4563,6 +4564,7 @@ impl EditorElement { flattened_unstaged_background_color, Edges::all(Pixels(1.0)), flattened_background_color, + BorderStyle::Solid, )); } } @@ -4927,6 +4929,7 @@ impl EditorElement { left: Pixels::ZERO, }, cx.theme().colors().scrollbar_track_border, + BorderStyle::Solid, )); window.paint_quad(quad( @@ -4940,6 +4943,7 @@ impl EditorElement { left: ScrollbarLayout::BORDER_WIDTH, }, cx.theme().colors().scrollbar_thumb_border, + BorderStyle::Solid, )); }) } @@ -5075,6 +5079,7 @@ impl EditorElement { left: ScrollbarLayout::BORDER_WIDTH, }, cx.theme().colors().scrollbar_track_border, + BorderStyle::Solid, )); let fast_markers = @@ -5100,6 +5105,7 @@ impl EditorElement { left: ScrollbarLayout::BORDER_WIDTH, }, cx.theme().colors().scrollbar_thumb_border, + BorderStyle::Solid, )); }); } @@ -8063,6 +8069,7 @@ impl ScrollbarLayout { pixel_range.color, Edges::default(), Hsla::transparent_black(), + BorderStyle::default(), )); } @@ -8296,7 +8303,7 @@ impl CursorLayout { //Draw background or border quad let cursor = if matches!(self.shape, CursorShape::Hollow) { - outline(bounds, self.color) + outline(bounds, self.color, BorderStyle::Solid) } else { fill(bounds, self.color) }; diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index 7674aac73d..9c2b0bafa9 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -136,6 +136,7 @@ mod macos { "Underline".into(), "UnderlineInputIndex".into(), "Quad".into(), + "BorderStyle".into(), "SpriteInputIndex".into(), "MonochromeSprite".into(), "PolychromeSprite".into(), diff --git a/crates/gpui/examples/hello_world.rs b/crates/gpui/examples/hello_world.rs index e5c4abd339..ca77eae70c 100644 --- a/crates/gpui/examples/hello_world.rs +++ b/crates/gpui/examples/hello_world.rs @@ -27,12 +27,61 @@ impl Render for HelloWorld { div() .flex() .gap_2() - .child(div().size_8().bg(gpui::red())) - .child(div().size_8().bg(gpui::green())) - .child(div().size_8().bg(gpui::blue())) - .child(div().size_8().bg(gpui::yellow())) - .child(div().size_8().bg(gpui::black())) - .child(div().size_8().bg(gpui::white())), + .child( + div() + .size_8() + .bg(gpui::red()) + .border_1() + .border_dashed() + .rounded_md() + .border_color(gpui::white()), + ) + .child( + div() + .size_8() + .bg(gpui::green()) + .border_1() + .border_dashed() + .rounded_md() + .border_color(gpui::white()), + ) + .child( + div() + .size_8() + .bg(gpui::blue()) + .border_1() + .border_dashed() + .rounded_md() + .border_color(gpui::white()), + ) + .child( + div() + .size_8() + .bg(gpui::yellow()) + .border_1() + .border_dashed() + .rounded_md() + .border_color(gpui::white()), + ) + .child( + div() + .size_8() + .bg(gpui::black()) + .border_1() + .border_dashed() + .rounded_md() + .rounded_md() + .border_color(gpui::white()), + ) + .child( + div() + .size_8() + .bg(gpui::white()) + .border_1() + .border_dashed() + .rounded_md() + .border_color(gpui::black()), + ), ) } } @@ -52,5 +101,6 @@ fn main() { }, ) .unwrap(); + cx.activate(true); }); } diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 2f573570f0..e868d3a39b 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -1662,7 +1662,7 @@ impl Interactivity { window: &mut Window, cx: &mut App, ) { - use crate::TextAlign; + use crate::{BorderStyle, TextAlign}; if global_id.is_some() && (style.debug || style.debug_below || cx.has_global::()) @@ -1753,6 +1753,7 @@ impl Interactivity { }, }, crate::red(), + BorderStyle::default(), )) } } diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index 4493bcfedc..324a920d65 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/crates/gpui/src/platform/blade/shaders.wgsl @@ -1,3 +1,33 @@ +/* Functions useful for debugging: + +// A heat map color for debugging (blue -> cyan -> green -> yellow -> red). +fn heat_map_color(value: f32, minValue: f32, maxValue: f32, position: vec2) -> vec4 { + // Normalize value to 0-1 range + let t = clamp((value - minValue) / (maxValue - minValue), 0.0, 1.0); + + // Heat map color calculation + let r = t * t; + let g = 4.0 * t * (1.0 - t); + let b = (1.0 - t) * (1.0 - t); + let heat_color = vec3(r, g, b); + + // Create a checkerboard pattern (black and white) + let sum = floor(position.x / 3) + floor(position.y / 3); + let is_odd = fract(sum * 0.5); // 0.0 for even, 0.5 for odd + let checker_value = is_odd * 2.0; // 0.0 for even, 1.0 for odd + let checker_color = vec3(checker_value); + + // Determine if value is in range (1.0 if in range, 0.0 if out of range) + let in_range = step(minValue, value) * step(value, maxValue); + + // Mix checkerboard and heat map based on whether value is in range + let final_color = mix(checker_color, heat_color, in_range); + + return vec4(final_color, 1.0); +} + +*/ + struct GlobalParams { viewport_size: vec2, premultiplied_alpha: u32, @@ -240,15 +270,16 @@ fn blur_along_x(x: f32, y: f32, sigma: f32, corner: f32, half_size: vec2) - return integral.y - integral.x; } -fn pick_corner_radius(point: vec2, radii: Corners) -> f32 { - if (point.x < 0.0) { - if (point.y < 0.0) { +// Selects corner radius based on quadrant. +fn pick_corner_radius(center_to_point: vec2, radii: Corners) -> f32 { + if (center_to_point.x < 0.0) { + if (center_to_point.y < 0.0) { return radii.top_left; } else { return radii.bottom_left; } } else { - if (point.y < 0.0) { + if (center_to_point.y < 0.0) { return radii.top_right; } else { return radii.bottom_right; @@ -256,15 +287,36 @@ fn pick_corner_radius(point: vec2, radii: Corners) -> f32 { } } +// Signed distance of the point to the quad's border - positive outside the +// border, and negative inside. +// +// See comments on similar code using `quad_sdf_impl` in `fs_quad` for +// explanation. fn quad_sdf(point: vec2, bounds: Bounds, corner_radii: Corners) -> f32 { let half_size = bounds.size / 2.0; let center = bounds.origin + half_size; let center_to_point = point - center; let corner_radius = pick_corner_radius(center_to_point, corner_radii); - let rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius; - return length(max(vec2(0.0), rounded_edge_to_point)) + - min(0.0, max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - - corner_radius; + let corner_to_point = abs(center_to_point) - half_size; + let corner_center_to_point = corner_to_point + corner_radius; + return quad_sdf_impl(corner_center_to_point, corner_radius); +} + +fn quad_sdf_impl(corner_center_to_point: vec2, corner_radius: f32) -> f32 { + if (corner_radius == 0.0) { + // Fast path for unrounded corners. + return max(corner_center_to_point.x, corner_center_to_point.y); + } else { + // Signed distance of the point from a quad that is inset by corner_radius. + // It is negative inside this quad, and positive outside. + let signed_distance_to_inset_quad = + // 0 inside the inset quad, and positive outside. + length(max(vec2(0.0), corner_center_to_point)) + + // 0 outside the inset quad, and negative inside. + min(0.0, max(corner_center_to_point.x, corner_center_to_point.y)); + + return signed_distance_to_inset_quad - corner_radius; + } } // Abstract away the final color transformation based on the @@ -386,7 +438,7 @@ fn gradient_color(background: Background, position: vec2, bounds: Bounds, struct Quad { order: u32, - pad: u32, + border_style: u32, bounds: Bounds, content_mask: Bounds, background: Background, @@ -438,54 +490,342 @@ 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 && + let unrounded = 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) { + quad.corner_radii.bottom_right == 0.0; + + // Fast path when the quad is not rounded and doesn't have any border + if (quad.border_widths.top == 0.0 && + quad.border_widths.left == 0.0 && + quad.border_widths.right == 0.0 && + quad.border_widths.bottom == 0.0 && + unrounded) { return blend_color(background_color, 1.0); } + let size = quad.bounds.size; + let half_size = size / 2.0; + let point = input.position.xy - quad.bounds.origin; + let center_to_point = point - half_size; + + // Signed distance field threshold for inclusion of pixels. Use of 0.5 + // instead of 1.0 causes the width of rounded borders to appear more + // consistent with straight borders. + let antialias_threshold = 0.5; + + // Radius of the nearest corner 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)) + - min(0.0, max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - - corner_radius; - let vertical_border = select(quad.border_widths.left, quad.border_widths.right, center_to_point.x > 0.0); - let horizontal_border = select(quad.border_widths.top, quad.border_widths.bottom, center_to_point.y > 0.0); - let inset_size = half_size - corner_radius - vec2(vertical_border, horizontal_border); - let point_to_inset_corner = abs(center_to_point) - inset_size; + // Width of the nearest borders + let border = vec2( + select( + quad.border_widths.right, + quad.border_widths.left, + center_to_point.x < 0.0), + select( + quad.border_widths.bottom, + quad.border_widths.top, + center_to_point.y < 0.0)); - var border_width = 0.0; - if (point_to_inset_corner.x < 0.0 && point_to_inset_corner.y < 0.0) { - border_width = 0.0; - } else if (point_to_inset_corner.y > point_to_inset_corner.x) { - border_width = horizontal_border; - } else { - border_width = vertical_border; + // Vector from the corner of the quad bounds to the point, after mirroring + // the point into the bottom right quadrant. Both components are <= 0. + let corner_to_point = abs(center_to_point) - half_size; + + // Vector from the point to the center of the rounded corner's circle, also + // mirrored into bottom right quadrant. + let corner_center_to_point = corner_to_point + corner_radius; + + // Whether the nearest point on the border is rounded + let is_near_rounded_corner = + corner_center_to_point.x >= 0 && + corner_center_to_point.y >= 0; + + // Vector from straight border inner corner to point. + let straight_border_inner_corner_to_point = corner_to_point + border; + + // Whether the point is beyond the inner edge of the straight border. + let is_beyond_inner_straight_border = + straight_border_inner_corner_to_point.x > 0 || + straight_border_inner_corner_to_point.y > 0; + + // Whether the point is far enough inside the straight border such that + // pixels are not affected by it. + let is_within_inner_straight_border = + straight_border_inner_corner_to_point.x < -antialias_threshold && + straight_border_inner_corner_to_point.y < -antialias_threshold; + + // Fast path for points that must be part of the background. + // + // This could be optimized further for large rounded corners by including + // points in an inscribed rectangle, or some other quick linear check. + // However, that might negatively impact performance in the case of + // reasonable sizes for rounded corners. + if (is_within_inner_straight_border && !is_near_rounded_corner) { + return blend_color(background_color, 1.0); } + // Signed distance of the point to the outside edge of the quad's border. It + // is positive outside this edge, and negative inside. + let outer_sdf = quad_sdf_impl(corner_center_to_point, corner_radius); + + // Approximate signed distance of the point to the inside edge of the quad's + // border. It is negative outside this edge (within the border), and + // positive inside. + // + // This is not always an accurate signed distance: + // * The rounded portions with varying border width use an approximation of + // nearest-point-on-ellipse. + // * When it is quickly known to be outside the edge, -1.0 is used. + var inner_sdf = 0.0; + if (corner_center_to_point.x <= 0 || corner_center_to_point.y <= 0) { + // Fast paths for straight borders. + inner_sdf = -max(straight_border_inner_corner_to_point.x, + straight_border_inner_corner_to_point.y); + } else if (is_beyond_inner_straight_border) { + // Fast path for points that must be outside the inner edge. + inner_sdf = -1.0; + } else if (border.x == border.y) { + // Fast path for circular inner edge. + inner_sdf = -(outer_sdf + border.x); + } else { + let ellipse_radii = max(vec2(0.0), corner_radius - border); + inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii); + } + + // Negative when inside the border + let border_sdf = max(inner_sdf, outer_sdf); + var color = background_color; - if (border_width > 0.0) { - let inset_distance = distance + border_width; + if (border_sdf < antialias_threshold) { + var border_color = input.border_color; + + // Dashed border logic when border_style == 1 + if (quad.border_style == 1) { + // Position in "dash space", where each dash period has length 1 + var t = 0.0; + + // Total number of dash periods, so that the dash spacing can be + // adjusted to evenly divide it + var max_t = 0.0; + + // Since border width affects the dash size, the density of dashes + // varies, and this is indicated by dash_velocity. It has units + // (dash period / pixel). So a dash velocity of (1 / 10) is 1 dash + // every 10 pixels. + var dash_velocity = 0.0; + + // Dash pattern: (2 * border width) dash, (1 * border width) gap + let dash_length_per_width = 2.0; + let dash_gap_per_width = 1.0; + let dash_period_per_width = dash_length_per_width + dash_gap_per_width; + + // Dividing this by the border width gives the dash velocity + let dv_numerator = 1.0 / dash_period_per_width; + + if (unrounded) { + // When corners aren't rounded, the dashes are separately laid + // out on each straight line, rather than around the whole + // perimeter. This way each line starts and ends with a dash. + let is_horizontal = + corner_center_to_point.x < + corner_center_to_point.y; + let border_width = select(border.y, border.x, is_horizontal); + dash_velocity = dv_numerator / border_width; + t = select(point.y, point.x, is_horizontal) * dash_velocity; + max_t = select(size.y, size.x, is_horizontal) * dash_velocity; + } else { + // When corners are rounded, the dashes are laid out around the + // whole perimeter. + + let r_tr = quad.corner_radii.top_right; + let r_br = quad.corner_radii.bottom_right; + let r_bl = quad.corner_radii.bottom_left; + let r_tl = quad.corner_radii.top_left; + + let w_t = quad.border_widths.top; + let w_r = quad.border_widths.right; + let w_b = quad.border_widths.bottom; + let w_l = quad.border_widths.left; + + // Straight side dash velocities + let dv_t = select(dv_numerator / w_t, 0.0, w_t <= 0.0); + let dv_r = select(dv_numerator / w_r, 0.0, w_r <= 0.0); + let dv_b = select(dv_numerator / w_b, 0.0, w_b <= 0.0); + let dv_l = select(dv_numerator / w_l, 0.0, w_l <= 0.0); + + // Straight side lengths in dash space + let s_t = (size.x - r_tl - r_tr) * dv_t; + let s_r = (size.y - r_tr - r_br) * dv_r; + let s_b = (size.x - r_br - r_bl) * dv_b; + let s_l = (size.y - r_bl - r_tl) * dv_l; + + let corner_dash_velocity_tr = corner_dash_velocity(dv_t, dv_r); + let corner_dash_velocity_br = corner_dash_velocity(dv_b, dv_r); + let corner_dash_velocity_bl = corner_dash_velocity(dv_b, dv_l); + let corner_dash_velocity_tl = corner_dash_velocity(dv_t, dv_l); + + // Corner lengths in dash space + let c_tr = r_tr * (M_PI_F / 2.0) * corner_dash_velocity_tr; + let c_br = r_br * (M_PI_F / 2.0) * corner_dash_velocity_br; + let c_bl = r_bl * (M_PI_F / 2.0) * corner_dash_velocity_bl; + let c_tl = r_tl * (M_PI_F / 2.0) * corner_dash_velocity_tl; + + // Cumulative dash space upto each segment + let upto_tr = s_t; + let upto_r = upto_tr + c_tr; + let upto_br = upto_r + s_r; + let upto_b = upto_br + c_br; + let upto_bl = upto_b + s_b; + let upto_l = upto_bl + c_bl; + let upto_tl = upto_l + s_l; + max_t = upto_tl + c_tl; + + if (is_near_rounded_corner) { + let radians = atan2(corner_center_to_point.y, + corner_center_to_point.x); + let corner_t = radians * corner_radius; + + if (center_to_point.x >= 0.0) { + if (center_to_point.y < 0.0) { + dash_velocity = corner_dash_velocity_tr; + t = upto_r - corner_t * dash_velocity; + } else { + dash_velocity = corner_dash_velocity_br; + t = upto_br + corner_t * dash_velocity; + } + } else { + if (center_to_point.y >= 0.0) { + dash_velocity = corner_dash_velocity_bl; + t = upto_l - corner_t * dash_velocity; + } else { + dash_velocity = corner_dash_velocity_tl; + t = upto_tl + corner_t * dash_velocity; + } + } + } else { + // Straight borders + let is_horizontal = + corner_center_to_point.x < + corner_center_to_point.y; + if (is_horizontal) { + if (center_to_point.y < 0.0) { + dash_velocity = dv_t; + t = (point.x - r_tl) * dash_velocity; + } else { + dash_velocity = dv_b; + t = upto_bl - (point.x - r_bl) * dash_velocity; + } + } else { + if (center_to_point.x < 0.0) { + dash_velocity = dv_l; + t = upto_tl - (point.y - r_tl) * dash_velocity; + } else { + dash_velocity = dv_r; + t = upto_r + (point.y - r_tr) * dash_velocity; + } + } + } + } + + let dash_length = dash_length_per_width / dash_period_per_width; + let desired_dash_gap = dash_gap_per_width / dash_period_per_width; + + // Straight borders should start and end with a dash, so max_t is + // reduced to cause this. + max_t -= select(0.0, dash_length, unrounded); + if (max_t >= 1.0) { + // Adjust dash gap to evenly divide max_t. + let dash_count = floor(max_t); + let dash_period = max_t / dash_count; + border_color.a *= dash_alpha( + t, + dash_period, + dash_length, + dash_velocity, + antialias_threshold); + } else if (unrounded) { + // When there isn't enough space for the full gap between the + // two start / end dashes of a straight border, reduce gap to + // make them fit. + let dash_gap = max_t - dash_length; + if (dash_gap > 0.0) { + let dash_period = dash_length + dash_gap; + border_color.a *= dash_alpha( + t, + dash_period, + dash_length, + dash_velocity, + antialias_threshold); + } + } + } + // 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(background_color, input.border_color); - color = mix(blended_border, background_color, - saturate(0.5 - inset_distance)); + let blended_border = over(background_color, border_color); + color = mix(background_color, blended_border, + saturate(antialias_threshold - inner_sdf)); } - return blend_color(color, saturate(0.5 - distance)); + return blend_color(color, saturate(antialias_threshold - outer_sdf)); +} + +// Returns the dash velocity of a corner given the dash velocity of the two +// sides, by returning the slower velocity (larger dashes). +// +// Since 0 is used for dash velocity when the border width is 0 (instead of +// +inf), this returns the other dash velocity in that case. +// +// An alternative to this might be to appropriately interpolate the dash +// velocity around the corner, but that seems overcomplicated. +fn corner_dash_velocity(dv1: f32, dv2: f32) -> f32 { + if (dv1 == 0.0) { + return dv2; + } else if (dv2 == 0.0) { + return dv1; + } else { + return min(dv1, dv2); + } +} + +// Returns alpha used to render antialiased dashes. +// `t` is within the dash when `fmod(t, period) < length`. +fn dash_alpha(t: f32, period: f32, length: f32, dash_velocity: f32, antialias_threshold: f32) -> f32 { + let half_period = period / 2; + let half_length = length / 2; + // Value in [-half_period, half_period]. + // The dash is in [-half_length, half_length]. + let centered = fmod(t + half_period - half_length, period) - half_period; + // Signed distance for the dash, negative values are inside the dash. + let signed_distance = abs(centered) - half_length; + // Antialiased alpha based on the signed distance. + return saturate(antialias_threshold - signed_distance / dash_velocity); +} + +// This approximates distance to the nearest point to a quarter ellipse in a way +// that is sufficient for anti-aliasing when the ellipse is not very eccentric. +// The components of `point` are expected to be positive. +// +// Negative on the outside and positive on the inside. +fn quarter_ellipse_sdf(point: vec2, radii: vec2) -> f32 { + // Scale the space to treat the ellipse like a unit circle. + let circle_vec = point / radii; + let unit_circle_sdf = length(circle_vec) - 1.0; + // Approximate up-scaling of the length by using the average of the radii. + // + // TODO: A better solution would be to use the gradient of the implicit + // function for an ellipse to approximate a scaling factor. + return unit_circle_sdf * (radii.x + radii.y) * -0.5; +} + +// Modulus that has the same sign as `a`. +fn fmod(a: f32, b: f32) -> f32 { + return a - b * trunc(a / b); } // --- shadows --- // diff --git a/crates/gpui/src/platform/mac/shaders.metal b/crates/gpui/src/platform/mac/shaders.metal index e37d882cc5..62bcf26f06 100644 --- a/crates/gpui/src/platform/mac/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders.metal @@ -18,8 +18,14 @@ float2 to_tile_position(float2 unit_vertex, AtlasTile tile, constant Size_DevicePixels *atlas_size); float4 distance_from_clip_rect(float2 unit_vertex, Bounds_ScaledPixels bounds, Bounds_ScaledPixels clip_bounds); +float corner_dash_velocity(float dv1, float dv2); +float dash_alpha(float t, float period, float length, float dash_velocity, + float antialias_threshold); +float quarter_ellipse_sdf(float2 point, float2 radii); +float pick_corner_radius(float2 center_to_point, Corners_ScaledPixels corner_radii); float quad_sdf(float2 point, Bounds_ScaledPixels bounds, Corners_ScaledPixels corner_radii); +float quad_sdf_impl(float2 center_to_point, float corner_radius); float gaussian(float x, float sigma); float2 erf(float2 x); float blur_along_x(float x, float y, float sigma, float corner, @@ -93,69 +99,314 @@ 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 = fill_color(quad.background, input.position.xy, quad.bounds, + float4 background_color = fill_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. && - quad.corner_radii.top_right == 0. && - 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 color; + bool unrounded = 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; + + // Fast path when the quad is not rounded and doesn't have any border + if (quad.border_widths.top == 0.0 && + quad.border_widths.left == 0.0 && + quad.border_widths.right == 0.0 && + quad.border_widths.bottom == 0.0 && + unrounded) { + return background_color; } - float corner_radius; - if (center_to_point.x < 0.) { - if (center_to_point.y < 0.) { - corner_radius = quad.corner_radii.top_left; - } else { - corner_radius = quad.corner_radii.bottom_left; - } + float2 size = float2(quad.bounds.size.width, quad.bounds.size.height); + float2 half_size = size / 2.0; + float2 point = input.position.xy - float2(quad.bounds.origin.x, quad.bounds.origin.y); + float2 center_to_point = point - half_size; + + // Signed distance field threshold for inclusion of pixels + const float antialias_threshold = 0.5; + + // Radius of the nearest corner + float corner_radius = pick_corner_radius(center_to_point, quad.corner_radii); + + // Width of the nearest borders + float2 border = float2( + center_to_point.x < 0.0 ? quad.border_widths.left : quad.border_widths.right, + center_to_point.y < 0.0 ? quad.border_widths.top : quad.border_widths.bottom + ); + + // Vector from the corner of the quad bounds to the point, after mirroring + // the point into the bottom right quadrant. Both components are <= 0. + float2 corner_to_point = fabs(center_to_point) - half_size; + + // Vector from the point to the center of the rounded corner's circle, also + // mirrored into bottom right quadrant. + float2 corner_center_to_point = corner_to_point + corner_radius; + + // Whether the nearest point on the border is rounded + bool is_near_rounded_corner = + corner_center_to_point.x >= 0.0 && + corner_center_to_point.y >= 0.0; + + // Vector from straight border inner corner to point + float2 straight_border_inner_corner_to_point = corner_to_point + border; + + // Whether the point is beyond the inner edge of the straight border + bool is_beyond_inner_straight_border = + straight_border_inner_corner_to_point.x > 0.0 || + straight_border_inner_corner_to_point.y > 0.0; + + // Whether the point is far enough inside the straight border such that + // pixels are not affected by it + bool is_within_inner_straight_border = + straight_border_inner_corner_to_point.x < -antialias_threshold && + straight_border_inner_corner_to_point.y < -antialias_threshold; + + // Fast path for points that must be part of the background + if (is_within_inner_straight_border && !is_near_rounded_corner) { + return background_color; + } + + // Signed distance of the point to the outside edge of the quad's border + float outer_sdf = quad_sdf_impl(corner_center_to_point, corner_radius); + + // Approximate signed distance of the point to the inside edge of the quad's + // border. It is negative outside this edge (within the border), and + // positive inside. + // + // This is not always an accurate signed distance: + // * The rounded portions with varying border width use an approximation of + // nearest-point-on-ellipse. + // * When it is quickly known to be outside the edge, -1.0 is used. + float inner_sdf = 0.0; + if (corner_center_to_point.x <= 0.0 || corner_center_to_point.y <= 0.0) { + // Fast paths for straight borders + inner_sdf = -max(straight_border_inner_corner_to_point.x, + straight_border_inner_corner_to_point.y); + } else if (is_beyond_inner_straight_border) { + // Fast path for points that must be outside the inner edge + inner_sdf = -1.0; + } else if (border.x == border.y) { + // Fast path for circular inner edge. + inner_sdf = -(outer_sdf + border.x); } else { - if (center_to_point.y < 0.) { - corner_radius = quad.corner_radii.top_right; - } else { - corner_radius = quad.corner_radii.bottom_right; + float2 ellipse_radii = max(float2(0.0), float2(corner_radius) - border); + inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii); + } + + // Negative when inside the border + float border_sdf = max(inner_sdf, outer_sdf); + + float4 color = background_color; + if (border_sdf < antialias_threshold) { + float4 border_color = input.border_color; + + // Dashed border logic when border_style == 1 + if (quad.border_style == 1) { + // Position in "dash space", where each dash period has length 1 + float t = 0.0; + + // Total number of dash periods, so that the dash spacing can be + // adjusted to evenly divide it + float max_t = 0.0; + + // Since border width affects the dash size, the density of dashes + // varies, and this is indicated by dash_velocity. It has units + // (dash period / pixel). So a dash velocity of (1 / 10) is 1 dash + // every 10 pixels. + float dash_velocity = 0.0; + + // Dash pattern: (2 * border width) dash, (1 * border width) gap + const float dash_length_per_width = 2.0; + const float dash_gap_per_width = 1.0; + const float dash_period_per_width = dash_length_per_width + dash_gap_per_width; + + // Dividing this by the border width gives the dash velocity + const float dv_numerator = 1.0 / dash_period_per_width; + + if (unrounded) { + // When corners aren't rounded, the dashes are separately laid + // out on each straight line, rather than around the whole + // perimeter. This way each line starts and ends with a dash. + bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y; + float border_width = is_horizontal ? border.x : border.y; + dash_velocity = dv_numerator / border_width; + t = is_horizontal ? point.x : point.y; + t *= dash_velocity; + max_t = is_horizontal ? size.x : size.y; + max_t *= dash_velocity; + } else { + // When corners are rounded, the dashes are laid out around the + // whole perimeter. + + float r_tr = quad.corner_radii.top_right; + float r_br = quad.corner_radii.bottom_right; + float r_bl = quad.corner_radii.bottom_left; + float r_tl = quad.corner_radii.top_left; + + float w_t = quad.border_widths.top; + float w_r = quad.border_widths.right; + float w_b = quad.border_widths.bottom; + float w_l = quad.border_widths.left; + + // Straight side dash velocities + float dv_t = w_t <= 0.0 ? 0.0 : dv_numerator / w_t; + float dv_r = w_r <= 0.0 ? 0.0 : dv_numerator / w_r; + float dv_b = w_b <= 0.0 ? 0.0 : dv_numerator / w_b; + float dv_l = w_l <= 0.0 ? 0.0 : dv_numerator / w_l; + + // Straight side lengths in dash space + float s_t = (size.x - r_tl - r_tr) * dv_t; + float s_r = (size.y - r_tr - r_br) * dv_r; + float s_b = (size.x - r_br - r_bl) * dv_b; + float s_l = (size.y - r_bl - r_tl) * dv_l; + + float corner_dash_velocity_tr = corner_dash_velocity(dv_t, dv_r); + float corner_dash_velocity_br = corner_dash_velocity(dv_b, dv_r); + float corner_dash_velocity_bl = corner_dash_velocity(dv_b, dv_l); + float corner_dash_velocity_tl = corner_dash_velocity(dv_t, dv_l); + + // Corner lengths in dash space + float c_tr = r_tr * (M_PI_F / 2.0) * corner_dash_velocity_tr; + float c_br = r_br * (M_PI_F / 2.0) * corner_dash_velocity_br; + float c_bl = r_bl * (M_PI_F / 2.0) * corner_dash_velocity_bl; + float c_tl = r_tl * (M_PI_F / 2.0) * corner_dash_velocity_tl; + + // Cumulative dash space upto each segment + float upto_tr = s_t; + float upto_r = upto_tr + c_tr; + float upto_br = upto_r + s_r; + float upto_b = upto_br + c_br; + float upto_bl = upto_b + s_b; + float upto_l = upto_bl + c_bl; + float upto_tl = upto_l + s_l; + max_t = upto_tl + c_tl; + + if (is_near_rounded_corner) { + float radians = atan2(corner_center_to_point.y, corner_center_to_point.x); + float corner_t = radians * corner_radius; + + if (center_to_point.x >= 0.0) { + if (center_to_point.y < 0.0) { + dash_velocity = corner_dash_velocity_tr; + t = upto_r - corner_t * dash_velocity; + } else { + dash_velocity = corner_dash_velocity_br; + t = upto_br + corner_t * dash_velocity; + } + } else { + if (center_to_point.y >= 0.0) { + dash_velocity = corner_dash_velocity_bl; + t = upto_l - corner_t * dash_velocity; + } else { + dash_velocity = corner_dash_velocity_tl; + t = upto_tl + corner_t * dash_velocity; + } + } + } else { + // Straight borders + bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y; + if (is_horizontal) { + if (center_to_point.y < 0.0) { + dash_velocity = dv_t; + t = (point.x - r_tl) * dash_velocity; + } else { + dash_velocity = dv_b; + t = upto_bl - (point.x - r_bl) * dash_velocity; + } + } else { + if (center_to_point.x < 0.0) { + dash_velocity = dv_l; + t = upto_tl - (point.y - r_tl) * dash_velocity; + } else { + dash_velocity = dv_r; + t = upto_r + (point.y - r_tr) * dash_velocity; + } + } + } + } + + float dash_length = dash_length_per_width / dash_period_per_width; + float desired_dash_gap = dash_gap_per_width / dash_period_per_width; + + // Straight borders should start and end with a dash, so max_t is + // reduced to cause this. + max_t -= unrounded ? dash_length : 0.0; + if (max_t >= 1.0) { + // Adjust dash gap to evenly divide max_t + float dash_count = floor(max_t); + float dash_period = max_t / dash_count; + border_color.a *= dash_alpha(t, dash_period, dash_length, dash_velocity, + antialias_threshold); + } else if (unrounded) { + // When there isn't enough space for the full gap between the + // two start / end dashes of a straight border, reduce gap to + // make them fit. + float dash_gap = max_t - dash_length; + if (dash_gap > 0.0) { + float dash_period = dash_length + dash_gap; + border_color.a *= dash_alpha(t, dash_period, dash_length, dash_velocity, + antialias_threshold); + } + } } - } - float2 rounded_edge_to_point = - fabs(center_to_point) - half_size + corner_radius; - float distance = - length(max(0., rounded_edge_to_point)) + - min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - - corner_radius; - - float vertical_border = center_to_point.x <= 0. ? quad.border_widths.left - : quad.border_widths.right; - float horizontal_border = center_to_point.y <= 0. ? quad.border_widths.top - : quad.border_widths.bottom; - float2 inset_size = - half_size - corner_radius - float2(vertical_border, horizontal_border); - float2 point_to_inset_corner = fabs(center_to_point) - inset_size; - float border_width; - if (point_to_inset_corner.x < 0. && point_to_inset_corner.y < 0.) { - border_width = 0.; - } else if (point_to_inset_corner.y > point_to_inset_corner.x) { - border_width = horizontal_border; - } else { - border_width = vertical_border; - } - - 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(color, input.border_color); - color = mix(blended_border, color, - saturate(0.5 - inset_distance)); + float4 blended_border = over(background_color, border_color); + color = mix(background_color, blended_border, + saturate(antialias_threshold - inner_sdf)); } - return color * float4(1., 1., 1., saturate(0.5 - distance)); + return color * float4(1.0, 1.0, 1.0, saturate(antialias_threshold - outer_sdf)); +} + +// Returns the dash velocity of a corner given the dash velocity of the two +// sides, by returning the slower velocity (larger dashes). +// +// Since 0 is used for dash velocity when the border width is 0 (instead of +// +inf), this returns the other dash velocity in that case. +// +// An alternative to this might be to appropriately interpolate the dash +// velocity around the corner, but that seems overcomplicated. +float corner_dash_velocity(float dv1, float dv2) { + if (dv1 == 0.0) { + return dv2; + } else if (dv2 == 0.0) { + return dv1; + } else { + return min(dv1, dv2); + } +} + +// Returns alpha used to render antialiased dashes. +// `t` is within the dash when `fmod(t, period) < length`. +float dash_alpha( + float t, float period, float length, float dash_velocity, + float antialias_threshold) { + float half_period = period / 2.0; + float half_length = length / 2.0; + // Value in [-half_period, half_period] + // The dash is in [-half_length, half_length] + float centered = fmod(t + half_period - half_length, period) - half_period; + // Signed distance for the dash, negative values are inside the dash + float signed_distance = abs(centered) - half_length; + // Antialiased alpha based on the signed distance + return saturate(antialias_threshold - signed_distance / dash_velocity); +} + +// This approximates distance to the nearest point to a quarter ellipse in a way +// that is sufficient for anti-aliasing when the ellipse is not very eccentric. +// The components of `point` are expected to be positive. +// +// Negative on the outside and positive on the inside. +float quarter_ellipse_sdf(float2 point, float2 radii) { + // Scale the space to treat the ellipse like a unit circle + float2 circle_vec = point / radii; + float unit_circle_sdf = length(circle_vec) - 1.0; + // Approximate up-scaling of the length by using the average of the radii. + // + // TODO: A better solution would be to use the gradient of the implicit + // function for an ellipse to approximate a scaling factor. + return unit_circle_sdf * (radii.x + radii.y) * -0.5; } struct ShadowVertexOutput { @@ -720,34 +971,52 @@ float2 to_tile_position(float2 unit_vertex, AtlasTile tile, float2((float)atlas_size->width, (float)atlas_size->height); } -float quad_sdf(float2 point, Bounds_ScaledPixels bounds, - Corners_ScaledPixels corner_radii) { - 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 = point - center; - float corner_radius; +// Selects corner radius based on quadrant. +float pick_corner_radius(float2 center_to_point, Corners_ScaledPixels corner_radii) { if (center_to_point.x < 0.) { if (center_to_point.y < 0.) { - corner_radius = corner_radii.top_left; + return corner_radii.top_left; } else { - corner_radius = corner_radii.bottom_left; + return corner_radii.bottom_left; } } else { if (center_to_point.y < 0.) { - corner_radius = corner_radii.top_right; + return corner_radii.top_right; } else { - corner_radius = corner_radii.bottom_right; + return corner_radii.bottom_right; } } +} - float2 rounded_edge_to_point = - abs(center_to_point) - half_size + corner_radius; - float distance = - length(max(0., rounded_edge_to_point)) + - min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - - corner_radius; +// Signed distance of the point to the quad's border - positive outside the +// border, and negative inside. +float quad_sdf(float2 point, Bounds_ScaledPixels bounds, + Corners_ScaledPixels corner_radii) { + float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.0; + float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size; + float2 center_to_point = point - center; + float corner_radius = pick_corner_radius(center_to_point, corner_radii); + float2 corner_to_point = fabs(center_to_point) - half_size; + float2 corner_center_to_point = corner_to_point + corner_radius; + return quad_sdf_impl(corner_center_to_point, corner_radius); +} - return distance; +// Implementation of quad signed distance field +float quad_sdf_impl(float2 corner_center_to_point, float corner_radius) { + if (corner_radius == 0.0) { + // Fast path for unrounded corners + return max(corner_center_to_point.x, corner_center_to_point.y); + } else { + // Signed distance of the point from a quad that is inset by corner_radius + // It is negative inside this quad, and positive outside + float signed_distance_to_inset_quad = + // 0 inside the inset quad, and positive outside + length(max(float2(0.0), corner_center_to_point)) + + // 0 outside the inset quad, and negative inside + min(0.0, max(corner_center_to_point.x, corner_center_to_point.y)); + + return signed_distance_to_inset_quad - corner_radius; + } } // A standard gaussian function, used for weighting samples diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index b837f2ad91..03862bc149 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -455,7 +455,7 @@ pub(crate) enum PrimitiveBatch<'a> { #[repr(C)] pub(crate) struct Quad { pub order: DrawOrder, - pub pad: u32, // align to 8 bytes + pub border_style: BorderStyle, pub bounds: Bounds, pub content_mask: ContentMask, pub background: Background, @@ -505,6 +505,17 @@ impl From for Primitive { } } +/// The style of a border. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(C)] +pub enum BorderStyle { + /// A solid border. + #[default] + Solid = 0, + /// A dashed border. + Dashed = 1, +} + /// A data type representing a 2 dimensional transformation that can be applied to an element. #[derive(Debug, Clone, Copy, PartialEq)] #[repr(C)] diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index cee1793e70..9aa12ba1f0 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -5,11 +5,11 @@ use std::{ }; use crate::{ - black, phi, point, quad, rems, size, AbsoluteLength, App, 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, - Window, + black, phi, point, quad, rems, size, AbsoluteLength, App, Background, BackgroundTag, + BorderStyle, 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, Window, }; use collections::HashSet; use refineable::Refineable; @@ -244,11 +244,14 @@ pub struct Style { /// The border color of this element pub border_color: Option, + /// The border style of this element + pub border_style: BorderStyle, + /// The radius of the corners of this element #[refineable] pub corner_radii: Corners, - /// Box Shadow of the element + /// Box shadow of the element pub box_shadow: SmallVec<[BoxShadow; 2]>, /// The text style of this element @@ -602,7 +605,7 @@ impl Style { #[cfg(debug_assertions)] if self.debug || cx.has_global::() { - window.paint_quad(crate::outline(bounds, crate::red())); + window.paint_quad(crate::outline(bounds, crate::red(), BorderStyle::default())); } let rem_size = window.rem_size(); @@ -634,6 +637,7 @@ impl Style { background_color.unwrap_or_default(), Edges::default(), border_color, + self.border_style, )); } @@ -670,6 +674,7 @@ impl Style { background, border_widths, self.border_color.unwrap_or_default(), + self.border_style, ); window.with_content_mask(Some(ContentMask { bounds: top_bounds }), |window| { @@ -749,6 +754,7 @@ impl Default for Style { flex_basis: Length::Auto, background: None, border_color: None, + border_style: BorderStyle::default(), corner_radii: Corners::default(), box_shadow: Default::default(), text: TextStyleRefinement::default(), diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index 3fe344fd8a..96e77e8df9 100644 --- a/crates/gpui/src/styled.rs +++ b/crates/gpui/src/styled.rs @@ -1,7 +1,8 @@ use crate::{ - self as gpui, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, DefiniteLength, - Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla, JustifyContent, Length, - SharedString, StrikethroughStyle, StyleRefinement, TextOverflow, UnderlineStyle, WhiteSpace, + self as gpui, px, relative, rems, AbsoluteLength, AlignItems, BorderStyle, CursorStyle, + DefiniteLength, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla, + JustifyContent, Length, SharedString, StrikethroughStyle, StyleRefinement, TextOverflow, + UnderlineStyle, WhiteSpace, }; use crate::{TextAlign, TextStyleRefinement}; pub use gpui_macros::{ @@ -361,6 +362,12 @@ pub trait Styled: Sized { self } + /// Sets the border style of the element. + fn border_dashed(mut self) -> Self { + self.style().border_style = Some(BorderStyle::Dashed); + self + } + /// Returns a mutable reference to the text style that has been configured on this element. fn text_style(&mut self) -> &mut Option { let style: &mut StyleRefinement = self.style(); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 4642db46c6..9f5a82d1f1 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1,18 +1,18 @@ use crate::{ point, prelude::*, px, size, transparent_black, Action, AnyDrag, AnyElement, AnyTooltip, - AnyView, App, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Background, Bounds, - BoxShadow, Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener, - DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, - FileDropEvent, FontId, Global, GlobalElementId, GlyphId, GpuSpecs, Hsla, InputHandler, IsZero, - KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent, LayoutId, - LineLayoutIndex, Modifiers, ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, - MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, - PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, - RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, - ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style, SubscriberSet, - Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement, TransformationMatrix, - Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, WindowBounds, - WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, + AnyView, App, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Background, + BorderStyle, Bounds, BoxShadow, Context, Corners, CursorStyle, Decorations, DevicePixels, + DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, + EntityId, EventEmitter, FileDropEvent, FontId, Global, GlobalElementId, GlyphId, GpuSpecs, + Hsla, InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, + KeystrokeEvent, LayoutId, LineLayoutIndex, Modifiers, ModifiersChangedEvent, MonochromeSprite, + MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, + PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, + PromptLevel, Quad, Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, + Replay, ResizeEdge, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style, + SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement, + TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, + WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Context as _, Result}; @@ -2335,13 +2335,13 @@ impl Window { let opacity = self.element_opacity(); self.next_frame.scene.insert_primitive(Quad { order: 0, - pad: 0, bounds: quad.bounds.scale(scale_factor), content_mask: content_mask.scale(scale_factor), background: quad.background.opacity(opacity), border_color: quad.border_color.opacity(opacity), corner_radii: quad.corner_radii.scale(scale_factor), border_widths: quad.border_widths.scale(scale_factor), + border_style: quad.border_style, }); } @@ -4107,6 +4107,8 @@ pub struct PaintQuad { pub border_widths: Edges, /// The color of the quad's borders. pub border_color: Hsla, + /// The style of the quad's borders. + pub border_style: BorderStyle, } impl PaintQuad { @@ -4150,6 +4152,7 @@ pub fn quad( background: impl Into, border_widths: impl Into>, border_color: impl Into, + border_style: BorderStyle, ) -> PaintQuad { PaintQuad { bounds, @@ -4157,6 +4160,7 @@ pub fn quad( background: background.into(), border_widths: border_widths.into(), border_color: border_color.into(), + border_style, } } @@ -4168,16 +4172,22 @@ pub fn fill(bounds: impl Into>, background: impl Into background: background.into(), border_widths: (0.).into(), border_color: transparent_black(), + border_style: BorderStyle::default(), } } /// Creates a rectangle outline with the given bounds, border color, and a 1px border width -pub fn outline(bounds: impl Into>, border_color: impl Into) -> PaintQuad { +pub fn outline( + bounds: impl Into>, + border_color: impl Into, + border_style: BorderStyle, +) -> PaintQuad { PaintQuad { bounds: bounds.into(), corner_radii: (0.).into(), background: transparent_black().into(), border_widths: (1.).into(), border_color: border_color.into(), + border_style, } } diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 7220dfb919..614e696176 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -9,11 +9,11 @@ use std::sync::Arc; use std::time::Duration; use gpui::{ - actions, point, quad, AnyElement, App, Bounds, ClipboardItem, CursorStyle, DispatchPhase, - Edges, Entity, FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, - KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, Render, - Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun, - TextStyle, TextStyleRefinement, + actions, point, quad, AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, + DispatchPhase, Edges, Entity, FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, + Hitbox, Hsla, KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, + Point, Render, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, + TextRun, TextStyle, TextStyleRefinement, }; use language::{Language, LanguageRegistry, Rope}; use parser::{parse_links_only, parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd}; @@ -353,6 +353,7 @@ impl MarkdownElement { self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), + BorderStyle::default(), )); } else { window.paint_quad(quad( @@ -364,6 +365,7 @@ impl MarkdownElement { self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), + BorderStyle::default(), )); if end_position.y > start_position.y + start_line_height { @@ -376,6 +378,7 @@ impl MarkdownElement { self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), + BorderStyle::default(), )); } @@ -388,6 +391,7 @@ impl MarkdownElement { self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), + BorderStyle::default(), )); } } diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index 155eaed8a9..9ff75263d7 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -2,10 +2,10 @@ use std::{any::Any, cell::Cell, fmt::Debug, ops::Range, rc::Rc, sync::Arc}; use crate::{prelude::*, px, relative, IntoElement}; use gpui::{ - point, quad, Along, App, Axis as ScrollbarAxis, Bounds, ContentMask, Corners, Edges, Element, - ElementId, Entity, EntityId, GlobalElementId, Hitbox, Hsla, LayoutId, MouseDownEvent, - MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollHandle, ScrollWheelEvent, Size, Style, - UniformListScrollHandle, Window, + point, quad, Along, App, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Corners, + Edges, Element, ElementId, Entity, EntityId, GlobalElementId, Hitbox, Hsla, LayoutId, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollHandle, ScrollWheelEvent, + Size, Style, UniformListScrollHandle, Window, }; pub struct Scrollbar { @@ -286,6 +286,7 @@ impl Element for Scrollbar { thumb_background, Edges::default(), Hsla::transparent_black(), + BorderStyle::default(), )); let scroll = self.state.scroll_handle.clone(); diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 7d6f4cc483..34543cc310 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -803,9 +803,9 @@ mod element { use std::{cell::RefCell, iter, rc::Rc, sync::Arc}; use gpui::{ - px, relative, size, Along, AnyElement, App, Axis, Bounds, Element, GlobalElementId, - IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, - Size, Style, WeakEntity, Window, + px, relative, size, Along, AnyElement, App, Axis, BorderStyle, Bounds, Element, + GlobalElementId, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, + Pixels, Point, Size, Style, WeakEntity, Window, }; use gpui::{CursorStyle, Hitbox}; use parking_lot::Mutex; @@ -1165,6 +1165,7 @@ mod element { gpui::transparent_black(), border, cx.theme().colors().border_selected, + BorderStyle::Solid, )); } } diff --git a/crates/zeta/src/completion_diff_element.rs b/crates/zeta/src/completion_diff_element.rs index 46c8f2c06e..9344c43c5e 100644 --- a/crates/zeta/src/completion_diff_element.rs +++ b/crates/zeta/src/completion_diff_element.rs @@ -2,8 +2,8 @@ use std::cmp; use crate::InlineCompletion; use gpui::{ - point, prelude::*, quad, size, AnyElement, App, Bounds, Corners, Edges, HighlightStyle, Hsla, - StyledText, TextLayout, TextStyle, + point, prelude::*, quad, size, AnyElement, App, BorderStyle, Bounds, Corners, Edges, + HighlightStyle, Hsla, StyledText, TextLayout, TextStyle, }; use language::OffsetRangeExt; use settings::Settings; @@ -150,6 +150,7 @@ impl Element for CompletionDiffElement { cx.theme().colors().editor_active_line_background, Edges::default(), Hsla::transparent_black(), + BorderStyle::default(), )); self.element.paint(window, cx); window.paint_quad(quad( @@ -158,6 +159,7 @@ impl Element for CompletionDiffElement { cx.theme().players().local().cursor, Edges::default(), Hsla::transparent_black(), + BorderStyle::default(), )); } }