Add support for dashed borders to GPUI (#27139)

Features:

* Scales dash spacing with border width.
* Laying out dashes around rounded corners.
* Varying border widths with rounded corners - now uses an ellipse for the inner edge of the border.
* When there are no rounded corners, each straight border is laid out separately, so that the dashes to meet at the corners.
* All sides of each dash are antialiased.

![image](https://github.com/user-attachments/assets/b3789a98-a5be-4f97-9736-c4e59615afe6)

![image](https://github.com/user-attachments/assets/739bdc57-4580-42c8-bfc3-6e287411a408)

Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <michael@zed.dev>
Co-authored-by: Ben <ben@zed.dev>
This commit is contained in:
Nathan Sobo 2025-03-25 11:11:04 -06:00 committed by GitHub
parent 2fe2028e20
commit cd1e56d6c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 869 additions and 159 deletions

View file

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

View file

@ -136,6 +136,7 @@ mod macos {
"Underline".into(),
"UnderlineInputIndex".into(),
"Quad".into(),
"BorderStyle".into(),
"SpriteInputIndex".into(),
"MonochromeSprite".into(),
"PolychromeSprite".into(),

View file

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

View file

@ -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::<crate::DebugBelow>())
@ -1753,6 +1753,7 @@ impl Interactivity {
},
},
crate::red(),
BorderStyle::default(),
))
}
}

View file

@ -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<f32>) -> vec4<f32> {
// 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<f32>(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<f32>(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<f32>(final_color, 1.0);
}
*/
struct GlobalParams {
viewport_size: vec2<f32>,
premultiplied_alpha: u32,
@ -240,15 +270,16 @@ fn blur_along_x(x: f32, y: f32, sigma: f32, corner: f32, half_size: vec2<f32>) -
return integral.y - integral.x;
}
fn pick_corner_radius(point: vec2<f32>, 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<f32>, 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<f32>, 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<f32>, 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<f32>(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<f32>, 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<f32>(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<f32>, 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<f32> {
}
let quad = b_quads[input.quad_id];
let half_size = quad.bounds.size / 2.0;
let center = quad.bounds.origin + half_size;
let center_to_point = input.position.xy - center;
let background_color = gradient_color(quad.background, input.position.xy, quad.bounds,
input.background_solid, input.background_color0, input.background_color1);
// Fast path when the quad is not rounded and doesn't have any border.
if (quad.corner_radii.top_left == 0.0 && quad.corner_radii.bottom_left == 0.0 &&
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<f32>(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<f32>(vertical_border, horizontal_border);
let point_to_inset_corner = abs(center_to_point) - inset_size;
// Width of the nearest borders
let border = vec2<f32>(
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<f32>(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<f32>, radii: vec2<f32>) -> 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 --- //

View file

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

View file

@ -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<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub background: Background,
@ -505,6 +505,17 @@ impl From<Shadow> 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)]

View file

@ -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<Hsla>,
/// The border style of this element
pub border_style: BorderStyle,
/// The radius of the corners of this element
#[refineable]
pub corner_radii: Corners<AbsoluteLength>,
/// 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::<DebugBelow>() {
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(),

View file

@ -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<TextStyleRefinement> {
let style: &mut StyleRefinement = self.style();

View file

@ -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<Pixels>,
/// 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<Background>,
border_widths: impl Into<Edges<Pixels>>,
border_color: impl Into<Hsla>,
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<Bounds<Pixels>>, background: impl Into<Background>
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<Bounds<Pixels>>, border_color: impl Into<Hsla>) -> PaintQuad {
pub fn outline(
bounds: impl Into<Bounds<Pixels>>,
border_color: impl Into<Hsla>,
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,
}
}

View file

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

View file

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

View file

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

View file

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