1#![deny(
19 clippy::unwrap_used,
20 clippy::expect_used,
21 clippy::indexing_slicing,
22 clippy::panic,
23 clippy::unreachable
24)]
25
26#[derive(Clone, Copy, Debug, PartialEq)]
29pub struct Panel {
30 pub x: f32,
32 pub y: f32,
34 pub w: f32,
36 pub h: f32,
38 pub alpha: f32,
41 pub scanlines: bool,
43 pub kind: PanelKind,
45}
46
47#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
49pub enum PanelKind {
50 #[default]
53 Backdrop,
54 Highlight,
57}
58
59pub const HIGHLIGHT_BAR_W: f32 = 4.0;
61
62impl Panel {
63 pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
65 Self {
66 x,
67 y,
68 w,
69 h,
70 alpha: 1.0,
71 scanlines: true,
72 kind: PanelKind::Backdrop,
73 }
74 }
75
76 pub fn highlight(x: f32, y: f32, w: f32, h: f32) -> Self {
78 Self {
79 scanlines: false,
80 kind: PanelKind::Highlight,
81 ..Self::new(x, y, w, h)
82 }
83 }
84
85 pub fn around(x0: f32, y0: f32, x1: f32, y1: f32, pad: f32) -> Self {
88 Self::new(
89 x0 - pad,
90 y0 - pad,
91 (x1 - x0) + 2.0 * pad,
92 (y1 - y0) + 2.0 * pad,
93 )
94 }
95
96 pub fn scaled(self, s: f32) -> Self {
99 Self {
100 x: self.x * s,
101 y: self.y * s,
102 w: self.w * s,
103 h: self.h * s,
104 ..self
105 }
106 }
107}
108
109#[cfg(feature = "text")]
110pub(crate) use pass::PanelPass;
111
112#[cfg(feature = "text")]
113mod pass {
114 use super::{HIGHLIGHT_BAR_W, Panel, PanelKind};
115 use crate::render::theme::THEME;
116
117 const MAX_PANELS: usize = 32;
119
120 #[repr(C)]
122 #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
123 struct Instance {
124 rect: [f32; 4],
126 fill: [f32; 4],
128 edge: [f32; 4],
130 bar: [f32; 4],
133 params: [f32; 4],
135 extra: [f32; 4],
137 }
138
139 const SHADER: &str = r#"
144struct VsOut {
145 @builtin(position) pos: vec4<f32>,
146 @location(0) rect: vec4<f32>,
147 @location(1) fill: vec4<f32>,
148 @location(2) edge: vec4<f32>,
149 @location(3) bar: vec4<f32>,
150 @location(4) params: vec4<f32>,
151 @location(5) extra: vec4<f32>,
152};
153
154@vertex
155fn vs_main(
156 @builtin(vertex_index) vi: u32,
157 @location(0) rect: vec4<f32>,
158 @location(1) fill: vec4<f32>,
159 @location(2) edge: vec4<f32>,
160 @location(3) bar: vec4<f32>,
161 @location(4) params: vec4<f32>,
162 @location(5) extra: vec4<f32>,
163) -> VsOut {
164 var corners = array<vec2<f32>, 6>(
165 vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 0.0), vec2<f32>(0.0, 1.0),
166 vec2<f32>(0.0, 1.0), vec2<f32>(1.0, 0.0), vec2<f32>(1.0, 1.0),
167 );
168 let px = rect.xy + corners[vi] * rect.zw;
169 let size = vec2<f32>(extra.x, params.w);
170 var out: VsOut;
171 out.pos = vec4<f32>(px.x / size.x * 2.0 - 1.0, 1.0 - px.y / size.y * 2.0, 0.0, 1.0);
172 out.rect = rect;
173 out.fill = fill;
174 out.edge = edge;
175 out.bar = bar;
176 out.params = params;
177 out.extra = extra;
178 return out;
179}
180
181@fragment
182fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
183 let p = in.pos.xy;
184 let half = in.rect.zw * 0.5;
185 let r = min(in.params.x, min(half.x, half.y));
186 let q = abs(p - (in.rect.xy + half)) - half + vec2<f32>(r, r);
187 // Signed distance to the rounded box: negative inside.
188 let d = length(max(q, vec2<f32>(0.0, 0.0))) + min(max(q.x, q.y), 0.0) - r;
189 let coverage = clamp(0.5 - d, 0.0, 1.0);
190 if (coverage <= 0.0) {
191 discard;
192 }
193
194 var fill = in.fill;
195 // Every pitch-th row from the panel's top carries the scanline.
196 let pitch = max(in.params.z, 1.0);
197 let row = floor(p.y - in.rect.y);
198 if (in.params.y > 0.0 && row - pitch * floor(row / pitch) < 0.5) {
199 fill = vec4<f32>(fill.rgb * (1.0 - in.params.y), fill.a + in.params.y * (1.0 - fill.a));
200 }
201
202 // The 1 px lit edge: full for `d > -0.5`, gone by `d = -1.5`, so a one-pixel
203 // band inside the outline, antialiased on its inner side (its outer side is
204 // `coverage`'s).
205 var edge_amt = clamp(d + 1.5, 0.0, 1.0) * in.edge.a;
206 var edge_rgb = in.edge.rgb;
207 // The accent bar replaces the edge along the left `extra.y` pixels.
208 if (p.x - in.rect.x < in.extra.y) {
209 edge_amt = in.bar.a;
210 edge_rgb = in.bar.rgb;
211 }
212 let rgb = edge_rgb * edge_amt + fill.rgb * fill.a * (1.0 - edge_amt);
213 let a = edge_amt + fill.a * (1.0 - edge_amt);
214 return vec4<f32>(rgb, a) * coverage;
215}
216"#;
217
218 pub(crate) struct PanelPass {
221 pipeline: wgpu::RenderPipeline,
222 instances: wgpu::Buffer,
223 scratch: Vec<Instance>,
224 count: u32,
226 }
227
228 impl PanelPass {
229 pub(crate) fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
230 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
231 label: Some("rlx-panel-shader"),
232 source: wgpu::ShaderSource::Wgsl(SHADER.into()),
233 });
234 let instances = device.create_buffer(&wgpu::BufferDescriptor {
235 label: Some("rlx-panel-instances"),
236 size: (MAX_PANELS * std::mem::size_of::<Instance>()) as u64,
237 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
238 mapped_at_creation: false,
239 });
240 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
241 label: Some("rlx-panel-pipeline-layout"),
242 bind_group_layouts: &[],
243 immediate_size: 0,
244 });
245 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
246 label: Some("rlx-panel-pipeline"),
247 layout: Some(&layout),
248 vertex: wgpu::VertexState {
249 module: &shader,
250 entry_point: Some("vs_main"),
251 compilation_options: Default::default(),
252 buffers: &[Some(wgpu::VertexBufferLayout {
253 array_stride: std::mem::size_of::<Instance>() as u64,
254 step_mode: wgpu::VertexStepMode::Instance,
255 attributes: &wgpu::vertex_attr_array![
256 0 => Float32x4,
257 1 => Float32x4,
258 2 => Float32x4,
259 3 => Float32x4,
260 4 => Float32x4,
261 5 => Float32x4,
262 ],
263 })],
264 },
265 fragment: Some(wgpu::FragmentState {
266 module: &shader,
267 entry_point: Some("fs_main"),
268 compilation_options: Default::default(),
269 targets: &[Some(wgpu::ColorTargetState {
270 format,
271 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
273 write_mask: wgpu::ColorWrites::ALL,
274 })],
275 }),
276 primitive: wgpu::PrimitiveState::default(),
277 depth_stencil: None,
278 multisample: wgpu::MultisampleState::default(),
279 multiview_mask: None,
280 cache: None,
281 });
282 Self {
283 pipeline,
284 instances,
285 scratch: Vec::with_capacity(MAX_PANELS),
286 count: 0,
287 }
288 }
289
290 pub(crate) fn prepare(
294 &mut self,
295 queue: &wgpu::Queue,
296 panels: &[Panel],
297 width: u32,
298 height: u32,
299 ) -> bool {
300 self.scratch.clear();
301 let backdrop = (THEME.panel.linear(), THEME.panel_edge.linear());
303 let highlight = THEME.highlight.linear();
304 let accent = THEME.accent.linear();
305 let scan = THEME.scanline.a;
306 let with_alpha = |[r, g, b, a]: [f32; 4], k: f32| [r, g, b, a * k];
307 for panel in panels.iter().take(MAX_PANELS) {
308 let finite = [panel.x, panel.y, panel.w, panel.h, panel.alpha]
309 .iter()
310 .all(|v| v.is_finite());
311 if !finite || panel.w <= 0.0 || panel.h <= 0.0 || panel.alpha <= 0.0 {
312 continue;
313 }
314 let alpha = panel.alpha.min(1.0);
315 let (fill, edge, bar, radius, bar_w) = match panel.kind {
316 PanelKind::Backdrop => (backdrop.0, backdrop.1, [0.0; 4], THEME.radius, 0.0),
317 PanelKind::Highlight => (highlight, [0.0; 4], accent, 0.0, HIGHLIGHT_BAR_W),
318 };
319 let scanlines = panel.scanlines && panel.kind == PanelKind::Backdrop;
320 self.scratch.push(Instance {
321 rect: [panel.x, panel.y, panel.w, panel.h],
322 fill: with_alpha(fill, alpha),
323 edge: with_alpha(edge, alpha),
324 bar: with_alpha(bar, alpha),
325 params: [
326 radius,
327 if scanlines { scan * alpha } else { 0.0 },
328 THEME.scanline_pitch as f32,
329 height.max(1) as f32,
330 ],
331 extra: [width.max(1) as f32, bar_w, 0.0, 0.0],
332 });
333 }
334 self.count = self.scratch.len() as u32;
335 if self.count == 0 {
336 return false;
337 }
338 queue.write_buffer(&self.instances, 0, bytemuck::cast_slice(&self.scratch));
339 true
340 }
341
342 pub(crate) fn render(&self, pass: &mut wgpu::RenderPass<'_>) {
344 if self.count == 0 {
345 return;
346 }
347 pass.set_pipeline(&self.pipeline);
348 pass.set_vertex_buffer(0, self.instances.slice(..));
349 pass.draw(0..6, 0..self.count);
350 }
351
352 pub(crate) fn end_frame(&mut self) {
354 self.count = 0;
355 }
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn a_panel_around_a_block_pads_every_side() {
365 let p = Panel::around(10.0, 20.0, 110.0, 70.0, 8.0);
366 assert_eq!((p.x, p.y, p.w, p.h), (2.0, 12.0, 116.0, 66.0));
367 assert_eq!(p.alpha, 1.0);
368 }
369
370 #[test]
371 fn scaling_a_panel_scales_its_rectangle_and_nothing_else() {
372 let p = Panel {
373 alpha: 0.5,
374 scanlines: false,
375 ..Panel::new(10.0, 20.0, 30.0, 40.0)
376 }
377 .scaled(0.5);
378 assert_eq!((p.x, p.y, p.w, p.h), (5.0, 10.0, 15.0, 20.0));
379 assert_eq!(p.alpha, 0.5);
380 assert!(!p.scanlines);
381 }
382}