1struct Bounds {
2 origin: vec2<f32>,
3 size: vec2<f32>,
4}
5struct Corners {
6 top_left: f32,
7 top_right: f32,
8 bottom_right: f32,
9 bottom_left: f32,
10}
11struct Edges {
12 top: f32,
13 right: f32,
14 bottom: f32,
15 left: f32,
16}
17struct Hsla {
18 h: f32,
19 s: f32,
20 l: f32,
21 a: f32,
22}
23
24struct Quad {
25 view_id: vec2<u32>,
26 layer_id: u32,
27 order: u32,
28 bounds: Bounds,
29 content_mask: Bounds,
30 background: Hsla,
31 border_color: Hsla,
32 corner_radii: Corners,
33 border_widths: Edges,
34}
35
36struct Globals {
37 viewport_size: vec2<f32>,
38 pad: vec2<u32>,
39}
40
41var<uniform> globals: Globals;
42var<storage, read> quads: array<Quad>;
43
44struct QuadsVarying {
45 @builtin(position) position: vec4<f32>,
46 @location(0) @interpolate(flat) background_color: vec4<f32>,
47 @location(1) @interpolate(flat) border_color: vec4<f32>,
48 @location(2) @interpolate(flat) quad_id: u32,
49 //TODO: use `clip_distance` once Naga supports it
50 @location(3) clip_distances: vec4<f32>,
51}
52
53fn to_device_position(unit_vertex: vec2<f32>, bounds: Bounds) -> vec4<f32> {
54 let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
55 let device_position = position / globals.viewport_size * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0);
56 return vec4<f32>(device_position, 0.0, 1.0);
57}
58
59fn distance_from_clip_rect(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds: Bounds) -> vec4<f32> {
60 let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
61 let tl = position - clip_bounds.origin;
62 let br = clip_bounds.origin + clip_bounds.size - position;
63 return vec4<f32>(tl.x, br.x, tl.y, br.y);
64}
65
66fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
67 let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
68 let s = hsla.s;
69 let l = hsla.l;
70 let a = hsla.a;
71
72 let c = (1.0 - abs(2.0 * l - 1.0)) * s;
73 let x = c * (1.0 - abs(h % 2.0 - 1.0));
74 let m = l - c / 2.0;
75
76 var color = vec4<f32>(m, m, m, a);
77
78 if (h >= 0.0 && h < 1.0) {
79 color.r += c;
80 color.g += x;
81 } else if (h >= 1.0 && h < 2.0) {
82 color.r += x;
83 color.g += c;
84 } else if (h >= 2.0 && h < 3.0) {
85 color.g += c;
86 color.b += x;
87 } else if (h >= 3.0 && h < 4.0) {
88 color.g += x;
89 color.b += c;
90 } else if (h >= 4.0 && h < 5.0) {
91 color.r += x;
92 color.b += c;
93 } else {
94 color.r += c;
95 color.b += x;
96 }
97
98 return color;
99}
100
101fn over(below: vec4<f32>, above: vec4<f32>) -> vec4<f32> {
102 let alpha = above.a + below.a * (1.0 - above.a);
103 let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha;
104 return vec4<f32>(color, alpha);
105}
106
107@vertex
108fn vs_quads(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> QuadsVarying {
109 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
110 let quad = quads[instance_id];
111
112 var out = QuadsVarying();
113 out.position = to_device_position(unit_vertex, quad.bounds);
114 out.background_color = hsla_to_rgba(quad.background);
115 out.border_color = hsla_to_rgba(quad.border_color);
116 out.quad_id = instance_id;
117 out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
118 return out;
119}
120
121@fragment
122fn fs_quads(input: QuadsVarying) -> @location(0) vec4<f32> {
123 // Alpha clip first, since we don't have `clip_distance`.
124 let min_distance = min(
125 min(input.clip_distances.x, input.clip_distances.y),
126 min(input.clip_distances.z, input.clip_distances.w)
127 );
128 if min_distance <= 0.0 {
129 return vec4<f32>(0.0);
130 }
131
132 let quad = quads[input.quad_id];
133 let half_size = quad.bounds.size / 2.0;
134 let center = quad.bounds.origin + half_size;
135 let center_to_point = input.position.xy - center;
136
137 var corner_radius = 0.0;
138 if (center_to_point.x < 0.0) {
139 if (center_to_point.y < 0.0) {
140 corner_radius = quad.corner_radii.top_left;
141 } else {
142 corner_radius = quad.corner_radii.bottom_left;
143 }
144 } else {
145 if (center_to_point.y < 0.) {
146 corner_radius = quad.corner_radii.top_right;
147 } else {
148 corner_radius = quad.corner_radii.bottom_right;
149 }
150 }
151
152 let rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
153 let distance =
154 length(max(vec2<f32>(0.0), rounded_edge_to_point)) +
155 min(0.0, max(rounded_edge_to_point.x, rounded_edge_to_point.y)) -
156 corner_radius;
157
158 let vertical_border = select(quad.border_widths.left, quad.border_widths.right, center_to_point.x > 0.0);
159 let horizontal_border = select(quad.border_widths.top, quad.border_widths.bottom, center_to_point.y > 0.0);
160 let inset_size = half_size - corner_radius - vec2<f32>(vertical_border, horizontal_border);
161 let point_to_inset_corner = abs(center_to_point) - inset_size;
162
163 var border_width = 0.0;
164 if (point_to_inset_corner.x < 0.0 && point_to_inset_corner.y < 0.0) {
165 border_width = 0.0;
166 } else if (point_to_inset_corner.y > point_to_inset_corner.x) {
167 border_width = horizontal_border;
168 } else {
169 border_width = vertical_border;
170 }
171
172 var color = input.background_color;
173 if (border_width > 0.0) {
174 let inset_distance = distance + border_width;
175 // Blend the border on top of the background and then linearly interpolate
176 // between the two as we slide inside the background.
177 let blended_border = over(input.background_color, input.border_color);
178 color = mix(blended_border, input.background_color,
179 saturate(0.5 - inset_distance));
180 }
181
182 return color * vec4<f32>(1.0, 1.0, 1.0, saturate(0.5 - distance));
183}