1use std::sync::Arc;
7
8use wgpu::util::DeviceExt;
9use wgpu::{
10 BindGroup, BindGroupLayout, Buffer, BufferUsages, Device, Queue, RenderPipeline, ShaderModule,
11};
12
13use super::backdrop_pass::{BackdropRegion, BackdropRenderer, CoverageMask};
14use super::clip::ClipChains;
15use super::clip_records::ClipRecords;
16use super::commands::{CornerRadii, DrawCommand};
17use super::constants::TEXT_SUPERSAMPLE;
18use super::flatten::{CommandLayer, FlatFrame, FlattenedCommand};
19use super::gpu::{QUAD_INDICES, QUAD_VERTICES, QuadVertex, ShaderUniforms, ShapeInstance};
20use super::gpu_context::RenderTarget;
21use super::image_quad::{ImageQuadRenderer, PreparedImageQuad};
22use super::text::TextRenderState;
23use super::text_mask::{MaskSpec, TextMaskRenderer};
24use super::text_quad::{PreparedTextQuad, TextQuadRenderer};
25use super::types::TextEntry;
26use crate::render_stats::{self, Pipeline};
27use crate::shape::PlacedShape;
28use crate::widgets::{Color, Rect};
29
30pub struct Renderer {
35 device: Arc<Device>,
36 queue: Arc<Queue>,
37 pipeline: RenderPipeline,
38 #[allow(dead_code)] bind_group_layout: BindGroupLayout,
40
41 vertex_buffer: Buffer,
43 index_buffer: Buffer,
44
45 uniform_buffer: Buffer,
47 uniform_bind_group: BindGroup,
48
49 instance_buffer: Buffer,
51 instance_buffer_capacity: usize,
52
53 clip_records: ClipRecords,
56
57 text_state: TextRenderState,
59
60 text_quad_renderer: TextQuadRenderer,
62
63 text_mask: TextMaskRenderer,
65
66 image_quad_renderer: ImageQuadRenderer,
68
69 shape_instance_buf: Vec<ShapeInstance>,
72 text_entry_buf: Vec<TextEntry>,
73 image_quads: Vec<PreparedImageQuad>,
74 text_quads: Vec<PreparedTextQuad>,
75 prepared_layers: Vec<PreparedLayer>,
77 backdrop: BackdropRenderer,
78
79 screen_width: f32,
81 screen_height: f32,
82 scale_factor: f32,
83}
84
85impl Renderer {
86 pub fn new(device: Arc<Device>, queue: Arc<Queue>, format: wgpu::TextureFormat) -> Self {
88 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
90 label: Some("Renderer Shader"),
91 source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
92 });
93
94 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
96 label: Some("Renderer Bind Group Layout"),
97 entries: &[
98 wgpu::BindGroupLayoutEntry {
99 binding: 0,
100 visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
101 ty: wgpu::BindingType::Buffer {
102 ty: wgpu::BufferBindingType::Uniform,
103 has_dynamic_offset: false,
104 min_binding_size: None,
105 },
106 count: None,
107 },
108 wgpu::BindGroupLayoutEntry {
112 binding: 1,
113 visibility: wgpu::ShaderStages::VERTEX,
114 ty: wgpu::BindingType::Buffer {
115 ty: wgpu::BufferBindingType::Storage { read_only: true },
116 has_dynamic_offset: false,
117 min_binding_size: None,
118 },
119 count: None,
120 },
121 ],
122 });
123
124 let clip_records = ClipRecords::new(&device);
125
126 let pipeline = Self::create_pipeline(
128 &device,
129 &shader,
130 &bind_group_layout,
131 clip_records.layout(),
132 format,
133 );
134
135 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
137 label: Some("Renderer Vertex Buffer"),
138 contents: bytemuck::cast_slice(QUAD_VERTICES),
139 usage: BufferUsages::VERTEX,
140 });
141
142 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
144 label: Some("Renderer Index Buffer"),
145 contents: bytemuck::cast_slice(QUAD_INDICES),
146 usage: BufferUsages::INDEX,
147 });
148
149 let uniforms = ShaderUniforms::new(800.0, 600.0, 1.0);
151 let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
152 label: Some("Renderer Uniform Buffer"),
153 contents: bytemuck::cast_slice(&[uniforms]),
154 usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
155 });
156
157 let initial_capacity = 256;
159 let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
160 label: Some("Renderer Instance Buffer"),
161 size: (initial_capacity * std::mem::size_of::<ShapeInstance>()) as u64,
162 usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
163 mapped_at_creation: false,
164 });
165
166 let uniform_bind_group = Self::make_bind_group(
167 &device,
168 &bind_group_layout,
169 &uniform_buffer,
170 &instance_buffer,
171 );
172
173 let text_state = TextRenderState::new(&device, &queue, format);
175
176 let text_quad_renderer = TextQuadRenderer::new(&device, format, clip_records.layout());
178 let text_mask = TextMaskRenderer::new(format);
179
180 let image_quad_renderer = ImageQuadRenderer::new(&device, format, clip_records.layout());
182
183 let backdrop = BackdropRenderer::new(&device, format, clip_records.layout());
184
185 Self {
186 device,
187 queue,
188 pipeline,
189 bind_group_layout,
190 vertex_buffer,
191 index_buffer,
192 uniform_buffer,
193 uniform_bind_group,
194 instance_buffer,
195 instance_buffer_capacity: initial_capacity,
196 clip_records,
197 text_state,
198 text_quad_renderer,
199 text_mask,
200 image_quad_renderer,
201 shape_instance_buf: Vec::new(),
202 text_entry_buf: Vec::new(),
203 image_quads: Vec::new(),
204 text_quads: Vec::new(),
205 prepared_layers: Vec::new(),
206 backdrop,
207 screen_width: 800.0,
208 screen_height: 600.0,
209 scale_factor: 1.0,
210 }
211 }
212
213 fn create_pipeline(
215 device: &Device,
216 shader: &ShaderModule,
217 bind_group_layout: &BindGroupLayout,
218 clip_records: &BindGroupLayout,
219 format: wgpu::TextureFormat,
220 ) -> RenderPipeline {
221 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
222 label: Some("Renderer Pipeline Layout"),
223 bind_group_layouts: &[Some(bind_group_layout), Some(clip_records)],
224 immediate_size: 0,
225 });
226
227 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
228 label: Some("Renderer Pipeline"),
229 layout: Some(&pipeline_layout),
230 vertex: wgpu::VertexState {
231 module: shader,
232 entry_point: Some("vs_main"),
233 buffers: &[Some(QuadVertex::desc())],
234 compilation_options: wgpu::PipelineCompilationOptions::default(),
235 },
236 fragment: Some(wgpu::FragmentState {
237 module: shader,
238 entry_point: Some("fs_main"),
239 targets: &[Some(wgpu::ColorTargetState {
240 format,
241 blend: Some(wgpu::BlendState {
242 color: wgpu::BlendComponent {
243 src_factor: wgpu::BlendFactor::SrcAlpha,
244 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
245 operation: wgpu::BlendOperation::Add,
246 },
247 alpha: wgpu::BlendComponent {
248 src_factor: wgpu::BlendFactor::One,
249 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
250 operation: wgpu::BlendOperation::Add,
251 },
252 }),
253 write_mask: wgpu::ColorWrites::ALL,
254 })],
255 compilation_options: wgpu::PipelineCompilationOptions::default(),
256 }),
257 primitive: wgpu::PrimitiveState {
258 topology: wgpu::PrimitiveTopology::TriangleList,
259 strip_index_format: None,
260 front_face: wgpu::FrontFace::Ccw,
261 cull_mode: None, polygon_mode: wgpu::PolygonMode::Fill,
263 unclipped_depth: false,
264 conservative: false,
265 },
266 depth_stencil: None,
267 multisample: wgpu::MultisampleState::default(),
268 multiview_mask: None,
269 cache: None,
270 })
271 }
272
273 pub fn set_screen_size(&mut self, width: f32, height: f32) {
275 self.screen_width = width;
276 self.screen_height = height;
277 }
278
279 pub fn set_scale_factor(&mut self, scale: f32) {
281 self.scale_factor = scale;
282 }
283
284 fn make_bind_group(
290 device: &wgpu::Device,
291 layout: &BindGroupLayout,
292 uniforms: &Buffer,
293 instances: &Buffer,
294 ) -> wgpu::BindGroup {
295 device.create_bind_group(&wgpu::BindGroupDescriptor {
296 label: Some("Renderer Bind Group"),
297 layout,
298 entries: &[
299 wgpu::BindGroupEntry {
300 binding: 0,
301 resource: uniforms.as_entire_binding(),
302 },
303 wgpu::BindGroupEntry {
304 binding: 1,
305 resource: instances.as_entire_binding(),
306 },
307 ],
308 })
309 }
310
311 fn ensure_instance_capacity(&mut self, count: usize) {
313 if count > self.instance_buffer_capacity {
314 let new_capacity = (self.instance_buffer_capacity * 2).max(count);
316 self.instance_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
317 label: Some("Renderer Instance Buffer"),
318 size: (new_capacity * std::mem::size_of::<ShapeInstance>()) as u64,
319 usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
320 mapped_at_creation: false,
321 });
322 self.instance_buffer_capacity = new_capacity;
323 self.uniform_bind_group = Self::make_bind_group(
324 &self.device,
325 &self.bind_group_layout,
326 &self.uniform_buffer,
327 &self.instance_buffer,
328 );
329 }
330 }
331
332 #[cfg(feature = "testing")]
334 pub(crate) fn image_textures(&self) -> usize {
335 self.image_quad_renderer.texture_count()
336 }
337
338 #[cfg(feature = "testing")]
340 pub(crate) fn image_atlas_pages(&self) -> usize {
341 self.image_quad_renderer.atlas_pages()
342 }
343
344 #[cfg(feature = "testing")]
346 pub(crate) fn forget_image_textures(&mut self) {
347 self.image_quad_renderer.forget_textures();
348 }
349
350 pub fn render(
356 &mut self,
357 target: &mut RenderTarget,
358 frame: &FlatFrame<'_>,
359 clear_color: Color,
360 ) -> bool {
361 #[cfg(any(test, feature = "testing"))]
367 let surface = match target {
368 RenderTarget::Offscreen(offscreen) => {
369 let view = offscreen
370 .texture
371 .create_view(&wgpu::TextureViewDescriptor::default());
372 let width = offscreen.texture.width();
373 let height = offscreen.texture.height();
374 self.render_to_view(&view, width, height, frame, clear_color);
375 return true;
376 }
377 RenderTarget::Swapchain(surface) => surface,
378 };
379 #[cfg(not(any(test, feature = "testing")))]
380 let RenderTarget::Swapchain(surface) = target;
381 let output = match surface.surface.get_current_texture() {
382 wgpu::CurrentSurfaceTexture::Success(output)
383 | wgpu::CurrentSurfaceTexture::Suboptimal(output) => output,
384 wgpu::CurrentSurfaceTexture::Lost => {
385 surface.resize(surface.width(), surface.height());
386 return false;
387 }
388 other => {
389 log::error!("Surface error: {:?}", other);
390 return false;
391 }
392 };
393
394 let view = output
395 .texture
396 .create_view(&wgpu::TextureViewDescriptor::default());
397
398 self.render_to_view(&view, surface.width(), surface.height(), frame, clear_color);
399
400 self.queue.present(output);
401 true
402 }
403
404 pub fn render_to_view(
414 &mut self,
415 view: &wgpu::TextureView,
416 width: u32,
417 height: u32,
418 frame: &FlatFrame<'_>,
419 clear_color: Color,
420 ) {
421 let FlatFrame {
422 commands,
423 layers,
424 clips,
425 } = *frame;
426 let uniforms =
428 ShaderUniforms::new(self.screen_width, self.screen_height, self.scale_factor);
429 self.queue
430 .write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
431
432 self.prepare_layers(commands, layers, clips);
433
434 let uses_backdrop = layers.iter().any(|layer| !layer.backdrop.is_empty());
439 if uses_backdrop {
440 self.backdrop
441 .ensure_targets(&self.device, width.max(1), height.max(1));
442 } else {
443 self.backdrop.note_unused();
444 }
445
446 self.clip_records
447 .upload(&self.device, &self.queue, clips, self.scale_factor);
448
449 self.ensure_instance_capacity(self.shape_instance_buf.len());
452 if !self.shape_instance_buf.is_empty() {
453 self.queue.write_buffer(
454 &self.instance_buffer,
455 0,
456 bytemuck::cast_slice(&self.shape_instance_buf),
457 );
458 }
459
460 let mut encoder = self
461 .device
462 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
463 label: Some("Renderer Encoder"),
464 });
465
466 let scene_view = uses_backdrop.then(|| self.backdrop.scene_view()).flatten();
467 let target = scene_view.unwrap_or(view);
468
469 let scale = self.scale_factor;
470 {
471 let clear = wgpu::LoadOp::Clear(wgpu::Color {
472 r: clear_color.r as f64,
473 g: clear_color.g as f64,
474 b: clear_color.b as f64,
475 a: clear_color.a as f64,
476 });
477 let mut load = clear;
478 let mut render_pass = begin_pass(&mut encoder, target, load);
479 load = wgpu::LoadOp::Load;
480
481 for (index, layer) in self.prepared_layers.iter().enumerate() {
485 if !layers[index].backdrop.is_empty() {
486 drop(render_pass);
489 let clip_records = self.clip_records.bind_group();
490 for command in &commands[layers[index].backdrop.clone()] {
491 if let Some(region) = command_to_backdrop_region(command, scale) {
492 self.backdrop
493 .apply(&self.device, &mut encoder, ®ion, clip_records);
494 } else if let Some(frost) = command_to_text_backdrop(command, scale) {
495 if let Some(view) =
499 self.text_mask.mask(&self.device, &self.queue, &frost.spec)
500 {
501 let mask = CoverageMask {
502 view: &view,
503 size: frost.spec.size,
504 };
505 self.backdrop.apply_masked(
506 &self.device,
507 &mut encoder,
508 &frost.region,
509 clip_records,
510 mask,
511 );
512 if let Some(outline) = frost.outline {
515 self.backdrop.apply_outline(
516 &self.device,
517 &mut encoder,
518 &frost.region,
519 clip_records,
520 mask,
521 outline,
522 );
523 }
524 }
525 }
526 }
527 render_pass = begin_pass(&mut encoder, target, load);
528 }
529
530 if !layer.shapes.is_empty() {
531 self.bind_shape_pipeline(&mut render_pass);
532 render_pass.draw_indexed(0..6, 0, layer.shapes.clone());
533 render_stats::record_draw_calls(Pipeline::Shapes, 1);
534 }
535
536 if !layer.images.is_empty() {
537 self.image_quad_renderer.render(
538 &mut render_pass,
539 &self.image_quads[layer.images.clone()],
540 self.clip_records.bind_group(),
541 );
542 }
543
544 if let Some(slot) = layer.text_slot {
545 self.text_state.render_slot(slot, &mut render_pass);
546 }
547
548 if !layer.text_quads.is_empty() {
549 self.text_quad_renderer.render(
550 &mut render_pass,
551 &self.text_quads[layer.text_quads.clone()],
552 self.clip_records.bind_group(),
553 );
554 }
555
556 if !layer.overlay.is_empty() {
557 self.bind_shape_pipeline(&mut render_pass);
558 render_pass.draw_indexed(0..6, 0, layer.overlay.clone());
559 render_stats::record_draw_calls(Pipeline::Shapes, 1);
560 }
561 }
562 }
563
564 if uses_backdrop {
565 self.backdrop.present(&self.device, &mut encoder, view);
566 }
567
568 self.queue.submit(std::iter::once(encoder.finish()));
569 }
570
571 fn bind_shape_pipeline(&self, pass: &mut wgpu::RenderPass<'_>) {
572 pass.set_pipeline(&self.pipeline);
573 pass.set_bind_group(0, &self.uniform_bind_group, &[]);
574 pass.set_bind_group(1, self.clip_records.bind_group(), &[]);
575 pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
576 pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
577 }
578
579 fn prepare_layers(
585 &mut self,
586 commands: &[FlattenedCommand],
587 layers: &[CommandLayer],
588 clips: &ClipChains,
589 ) {
590 let scale = self.scale_factor;
591
592 self.shape_instance_buf.clear();
593 self.image_quads.clear();
594 self.text_quads.clear();
595 self.prepared_layers.clear();
596 self.image_quad_renderer.begin_frame();
597 self.text_quad_renderer.begin_frame();
598 self.text_mask.begin_frame();
599 self.text_state.begin_frame(
600 &self.queue,
601 (self.screen_width as u32, self.screen_height as u32),
602 );
603
604 let mut next_text_slot = 0;
607
608 for layer in layers {
609 let shapes_start = self.shape_instance_buf.len() as u32;
610 self.shape_instance_buf.extend(
611 commands[layer.shapes.clone()]
612 .iter()
613 .filter_map(|c| faded_instance(c, scale)),
614 );
615 let shapes = shapes_start..self.shape_instance_buf.len() as u32;
616
617 let images_start = self.image_quads.len();
618 if !layer.images.is_empty() {
619 self.image_quad_renderer
620 .set_screen_size(self.screen_width, self.screen_height);
621 self.image_quad_renderer.prepare(
622 &self.device,
623 &self.queue,
624 &commands[layer.images.clone()],
625 scale,
626 &mut self.image_quads,
627 );
628 }
629 let images = images_start..self.image_quads.len();
630
631 self.text_entry_buf.clear();
632 self.text_entry_buf.extend(
633 commands[layer.text.clone()]
634 .iter()
635 .filter_map(command_to_text_entry),
636 );
637 let text_quads_start = self.text_quads.len();
638 let mut text_slot = None;
639 if !self.text_entry_buf.is_empty() {
640 let slot = next_text_slot;
641 next_text_slot += 1;
642 let transformed = self.text_state.prepare_layer(
643 slot,
644 &self.device,
645 &self.queue,
646 &self.text_entry_buf,
647 clips,
648 (self.screen_width as u32, self.screen_height as u32),
649 scale,
650 );
651 if transformed.len() < self.text_entry_buf.len() {
654 text_slot = Some(slot);
655 }
656 if !transformed.is_empty() {
657 self.text_quad_renderer
658 .set_screen_size(self.screen_width, self.screen_height);
659 self.text_quad_renderer.prepare(
660 &self.device,
661 &self.queue,
662 &self.text_entry_buf,
663 transformed,
664 scale,
665 &mut self.text_quads,
666 );
667 }
668 }
669 let text_quads = text_quads_start..self.text_quads.len();
670
671 let overlay_start = self.shape_instance_buf.len() as u32;
672 self.shape_instance_buf.extend(
673 commands[layer.overlay.clone()]
674 .iter()
675 .filter_map(|c| faded_instance(c, scale)),
676 );
677 let overlay = overlay_start..self.shape_instance_buf.len() as u32;
678
679 self.prepared_layers.push(PreparedLayer {
680 shapes,
681 images,
682 text_slot,
683 text_quads,
684 overlay,
685 });
686 }
687
688 self.image_quad_renderer.upload(&self.device, &self.queue);
689 self.text_quad_renderer.upload(&self.device, &self.queue);
690 self.image_quad_renderer.trim(crate::image_cache_budget());
691 self.text_state.end_frame();
692 }
693}
694
695struct PreparedLayer {
698 shapes: std::ops::Range<u32>,
699 images: std::ops::Range<usize>,
700 text_slot: Option<usize>,
702 text_quads: std::ops::Range<usize>,
703 overlay: std::ops::Range<u32>,
704}
705
706fn begin_pass<'a>(
708 encoder: &'a mut wgpu::CommandEncoder,
709 target: &'a wgpu::TextureView,
710 load: wgpu::LoadOp<wgpu::Color>,
711) -> wgpu::RenderPass<'a> {
712 encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
713 label: Some("Renderer Render Pass"),
714 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
715 view: target,
716 resolve_target: None,
717 ops: wgpu::Operations {
718 load,
719 store: wgpu::StoreOp::Store,
720 },
721 depth_slice: None,
722 })],
723 depth_stencil_attachment: None,
724 timestamp_writes: None,
725 occlusion_query_set: None,
726 multiview_mask: None,
727 })
728}
729
730fn backdrop_region(
738 shape: PlacedShape,
739 radius: f32,
740 cmd: &FlattenedCommand,
741 scale: f32,
742) -> BackdropRegion {
743 BackdropRegion {
744 shape,
745 scale,
746 radius,
747 clip: cmd.clip(),
751 opacity: cmd.opacity,
752 }
753}
754
755fn command_to_backdrop_region(cmd: &FlattenedCommand, scale: f32) -> Option<BackdropRegion> {
757 let DrawCommand::BackdropBlur {
758 rect,
759 sources,
760 radius,
761 corner_radii,
762 curvature,
763 } = &*cmd.command
764 else {
765 return None;
766 };
767 if !sources.contains(crate::backdrop::BackdropSources::SURFACE) {
770 return None;
771 }
772
773 Some(backdrop_region(
776 PlacedShape::placed(*rect, *corner_radii, *curvature, cmd.world_transform),
777 *radius,
778 cmd,
779 scale,
780 ))
781}
782
783struct TextBackdrop<'a> {
785 region: BackdropRegion,
786 spec: MaskSpec<'a>,
787 outline: Option<(Color, f32)>,
790}
791
792fn command_to_text_backdrop(cmd: &FlattenedCommand, scale: f32) -> Option<TextBackdrop<'_>> {
803 let DrawCommand::TextBackdropBlur {
804 text,
805 stroke,
806 rect,
807 radius,
808 font_size,
809 font_family,
810 font_weight,
811 line_height,
812 letter_spacing,
813 fit,
814 align,
815 } = &*cmd.command
816 else {
817 return None;
818 };
819
820 let slack = font_size * 0.5 + stroke.map(|s| s.width).unwrap_or(0.0);
825 let box_of_ink = rect.outset(slack);
826
827 let density = if cmd.world_transform.is_translation_only() {
849 scale
850 } else {
851 scale * TEXT_SUPERSAMPLE
852 };
853 let width = (box_of_ink.width * density).ceil().max(1.0);
854 let height = (box_of_ink.height * density).ceil().max(1.0);
855
856 let frame = Rect::new(
863 box_of_ink.x,
864 box_of_ink.y,
865 width / density,
866 height / density,
867 );
868
869 let region = backdrop_region(
872 PlacedShape::placed(frame, CornerRadii::uniform(0.0), 1.0, cmd.world_transform),
873 *radius,
874 cmd,
875 scale,
876 );
877
878 Some(TextBackdrop {
879 region,
880 spec: MaskSpec {
881 text,
882 font_size: *font_size,
883 font_family: *font_family,
884 font_weight: *font_weight,
885 line_height: *line_height,
886 letter_spacing: *letter_spacing,
887 fit: *fit,
888 align: *align,
889 buffer_height: if cmd.world_transform.is_translation_only() {
893 super::text::buffer_height(*rect, density)
894 } else {
895 super::text_quad::buffer_height(*rect, density)
896 },
897 box_width: rect.width * density,
898 size: (width as u32, height as u32),
899 offset: (slack * density, slack * density),
900 density,
901 },
902 outline: stroke.map(|s| (s.color, s.width * density)),
903 })
904}
905
906fn faded_instance(cmd: &FlattenedCommand, scale: f32) -> Option<ShapeInstance> {
909 command_to_instance(cmd, scale).map(|instance| instance.faded(cmd.opacity))
910}
911
912fn command_to_instance(cmd: &FlattenedCommand, scale: f32) -> Option<ShapeInstance> {
914 match &*cmd.command {
915 DrawCommand::RoundedRect {
916 rect,
917 color,
918 radius,
919 curvature,
920 border,
921 shadow,
922 gradient,
923 } => {
924 let mut instance = ShapeInstance::from_rect(
925 [
926 rect.x * scale,
927 rect.y * scale,
928 rect.width * scale,
929 rect.height * scale,
930 ],
931 [color.r, color.g, color.b, color.a],
932 radius.scaled(scale).to_array(),
933 *curvature,
934 )
935 .with_transform(&cmd.world_transform, scale);
936
937 if let Some(b) = border {
938 instance = instance.with_border(b, scale);
939 }
940 if let Some(s) = shadow {
941 instance = instance.with_shadow(s, scale);
942 }
943 if let Some(g) = gradient {
944 instance = instance.with_gradient(g);
945 }
946 if let Some(clip) = cmd.clip() {
947 instance = instance.with_clip(&clip, scale);
948 }
949
950 Some(instance)
951 }
952 DrawCommand::Circle {
953 center,
954 radius,
955 color,
956 } => {
957 let rect_x = (center.0 - radius) * scale;
959 let rect_y = (center.1 - radius) * scale;
960 let size = radius * 2.0 * scale;
961
962 let mut instance = ShapeInstance::from_rect(
963 [rect_x, rect_y, size, size],
964 [color.r, color.g, color.b, color.a],
965 [radius * scale; 4], 1.0, )
968 .with_transform(&cmd.world_transform, scale);
969
970 if let Some(clip) = cmd.clip() {
971 instance = instance.with_clip(&clip, scale);
972 }
973
974 Some(instance)
975 }
976 DrawCommand::Text { .. } => None,
978 DrawCommand::BackdropBlur { .. } | DrawCommand::TextBackdropBlur { .. } => None,
981 DrawCommand::InputRegion { .. } => None,
983 DrawCommand::Image { .. } => None,
985 }
986}
987
988fn command_to_text_entry(cmd: &FlattenedCommand) -> Option<TextEntry> {
990 match &*cmd.command {
991 DrawCommand::Text {
992 text,
993 rect,
994 color,
995 font_size,
996 font_family,
997 font_weight,
998 line_height,
999 letter_spacing,
1000 fit,
1001 align,
1002 } => Some(TextEntry {
1003 text: text.clone(),
1004 rect: *rect,
1005 color: *color,
1006 font_size: *font_size,
1007 font_family: *font_family,
1008 font_weight: *font_weight,
1009 line_height: *line_height,
1010 letter_spacing: *letter_spacing,
1011 fit: *fit,
1012 align: *align,
1013 opacity: cmd.opacity,
1014 clip: cmd.clip(),
1015 transform: cmd.world_transform,
1016 }),
1017 _ => None,
1018 }
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023 use super::*;
1024 use std::rc::Rc;
1025
1026 use crate::renderer::flatten::RenderLayer;
1027 use crate::transform::Transform;
1028 use crate::widgets::FontFamily;
1029
1030 fn frosted(rect: Rect, transform: Transform) -> FlattenedCommand {
1031 FlattenedCommand {
1032 command: Rc::new(DrawCommand::TextBackdropBlur {
1033 text: "09:41".to_owned(),
1034 stroke: None,
1035 rect,
1036 radius: 10.0,
1037 font_size: 20.0,
1038 font_family: FontFamily::default(),
1039 font_weight: Default::default(),
1040 line_height: Default::default(),
1041 letter_spacing: 0.0,
1042 fit: None,
1043 align: Default::default(),
1044 }),
1045 world_transform: transform,
1046 layer: RenderLayer::Backdrop,
1047 opacity: 1.0,
1048 clip: None,
1049 }
1050 }
1051
1052 #[test]
1057 fn the_glyph_origin_in_the_mask_is_where_the_text_is() {
1058 for transform in [
1059 Transform::IDENTITY,
1060 Transform::translate(40.0, 5.0),
1061 Transform::scale(2.5),
1062 Transform::scale_xy(0.5, 3.0),
1063 Transform::rotate(0.4),
1064 ] {
1065 let cmd = frosted(Rect::new(10.3, 20.6, 100.0, 30.0), transform);
1066 let frost = command_to_text_backdrop(&cmd, 2.0).expect("a frost");
1067 let frame = frost.region.shape.rect;
1068 let origin = (
1069 frame.x + frost.spec.offset.0 / frost.spec.density,
1070 frame.y + frost.spec.offset.1 / frost.spec.density,
1071 );
1072 assert!(
1073 (origin.0 - 10.3).abs() < 1e-3 && (origin.1 - 20.6).abs() < 1e-3,
1074 "{origin:?} is not the text's own origin under {transform:?}"
1075 );
1076 assert_eq!(
1080 (
1081 frame.width * frost.spec.density,
1082 frame.height * frost.spec.density
1083 ),
1084 (frost.spec.size.0 as f32, frost.spec.size.1 as f32),
1085 "the frame and the texture are the same extent under {transform:?}"
1086 );
1087 }
1088 }
1089
1090 #[test]
1093 fn the_region_covers_more_than_the_layout_box() {
1094 let cmd = frosted(Rect::new(10.0, 20.0, 100.0, 30.0), Transform::IDENTITY);
1095 let frost = command_to_text_backdrop(&cmd, 1.0).expect("a frost");
1096 assert!(
1097 frost.region.viewport().width >= 110.0,
1098 "{:?}",
1099 frost.region.viewport()
1100 );
1101 assert!(
1102 frost.region.viewport().height >= 40.0,
1103 "{:?}",
1104 frost.region.viewport()
1105 );
1106 }
1107
1108 #[test]
1116 fn the_viewport_is_the_box_around_the_shape() {
1117 for transform in [
1118 Transform::IDENTITY,
1119 Transform::translate(40.0, 5.0),
1120 Transform::scale(2.5),
1121 Transform::rotate_degrees(30.0),
1122 ] {
1123 let cmd = frosted(Rect::new(10.0, 20.0, 100.0, 30.0), transform);
1124 for scale in [1.0_f32, 2.0] {
1125 let frost = command_to_text_backdrop(&cmd, scale).expect("a frost");
1126 let world = frost.region.shape.world_aabb();
1127 let viewport = frost.region.viewport();
1128 assert!(
1129 (viewport.x - world.x * scale).abs() < 1e-3
1130 && (viewport.y - world.y * scale).abs() < 1e-3
1131 && (viewport.width - world.width * scale).abs() < 1e-3
1132 && (viewport.height - world.height * scale).abs() < 1e-3,
1133 "{transform:?} at scale {scale}: {viewport:?} is not \
1134 {world:?} in physical pixels"
1135 );
1136 }
1137 }
1138 }
1139
1140 #[test]
1145 fn the_scale_factor_reaches_the_viewport_and_the_density_and_nothing_twice() {
1146 let cmd = frosted(Rect::new(10.0, 20.0, 100.0, 30.0), Transform::IDENTITY);
1147 let one = command_to_text_backdrop(&cmd, 1.0).expect("a frost");
1148 let two = command_to_text_backdrop(&cmd, 2.0).expect("a frost");
1149
1150 assert_eq!(two.region.scale, 2.0);
1151 assert_eq!(
1152 two.region.radius, one.region.radius,
1153 "the radius is logical, and the scale is carried rather than baked in"
1154 );
1155 assert_eq!(
1156 two.region.shape.rect, one.region.shape.rect,
1157 "and so is the shape"
1158 );
1159 assert!(two.region.viewport().width >= one.region.viewport().width * 2.0 - 1.0);
1160 assert_eq!(two.spec.density, one.spec.density * 2.0);
1161 }
1162
1163 #[test]
1166 fn a_translated_text_is_frosted_where_it_ends_up() {
1167 let rect = Rect::new(10.0, 20.0, 100.0, 30.0);
1168 let at_rest = frosted(rect, Transform::IDENTITY);
1169 let at_rest = command_to_text_backdrop(&at_rest, 1.0).expect("a frost");
1170 let moved = frosted(rect, Transform::translate(40.0, 5.0));
1171 let moved = command_to_text_backdrop(&moved, 1.0).expect("a frost");
1172
1173 assert_eq!(moved.spec.size, at_rest.spec.size);
1174 assert_eq!(moved.spec.offset, at_rest.spec.offset);
1175 assert!((moved.region.viewport().x - at_rest.region.viewport().x - 40.0).abs() < 1e-3);
1176 assert!((moved.region.viewport().y - at_rest.region.viewport().y - 5.0).abs() < 1e-3);
1177 }
1178
1179 #[test]
1186 fn the_clip_reaches_the_region_as_a_shape() {
1187 let clip = crate::shape::PlacedShape {
1188 rect: Rect::new(0.0, 0.0, 200.0, 200.0),
1189 radii: CornerRadii::uniform(16.0),
1190 curvature: 1.0,
1191 placement: Transform::rotate_degrees(20.0),
1192 };
1193 let mut cmd = frosted(Rect::new(10.0, 20.0, 100.0, 30.0), Transform::IDENTITY);
1194 cmd.clip = Some(crate::renderer::clip::ClipRef::placed(clip));
1195
1196 let frost = command_to_text_backdrop(&cmd, 2.0).expect("a frost");
1197 let arrived = frost.region.clip.expect("a clip").first;
1198 assert_eq!(arrived.rect, clip.rect, "logical, like the shape beside it");
1199 assert_eq!(
1200 arrived.radii.top_left, 16.0,
1201 "with its corners, which a box would have squared off"
1202 );
1203 assert_eq!(
1204 arrived.placement, clip.placement,
1205 "and its placement, which a box would have flattened"
1206 );
1207 }
1208
1209 #[test]
1214 fn a_turned_text_is_frosted_along_its_letters() {
1215 let cmd = frosted(Rect::new(10.0, 20.0, 100.0, 30.0), Transform::rotate(0.4));
1216 let frost = command_to_text_backdrop(&cmd, 1.0).expect("a frost");
1217
1218 let frame = frost.region.shape.rect;
1219 let turned = cmd.world_transform.map_rect(frame);
1220 assert!(
1221 (frost.region.viewport().x - turned.x).abs() < 1e-3
1222 && (frost.region.viewport().width - turned.width).abs() < 1e-3,
1223 "the viewport is the box around the turned frame: {:?} vs {turned:?}",
1224 frost.region.viewport()
1225 );
1226 assert!(
1227 turned.width > frame.width,
1228 "a turn widens the box it needs, or this proves nothing"
1229 );
1230
1231 let to_shape = frost
1235 .region
1236 .shape
1237 .to_local(frost.region.scale)
1238 .expect("invertible");
1239 let (x, y) = cmd.world_transform.transform_point(frame.x, frame.y);
1240 let (back_x, back_y) = to_shape.transform_point(x, y);
1241 assert!((back_x - frame.x).abs() < 1e-2 && (back_y - frame.y).abs() < 1e-2);
1242 }
1243
1244 #[test]
1250 fn a_scale_grows_the_viewport_and_leaves_the_mask_alone() {
1251 let rect = Rect::new(10.0, 20.0, 100.0, 30.0);
1252 let small = frosted(rect, Transform::scale(1.6));
1253 let small = command_to_text_backdrop(&small, 1.0).expect("a frost");
1254 let large = frosted(rect, Transform::scale(3.0));
1255 let large = command_to_text_backdrop(&large, 1.0).expect("a frost");
1256
1257 assert_eq!(large.spec.size, small.spec.size);
1258 assert_eq!(large.spec.density, small.spec.density);
1259 assert_eq!(large.region.shape.rect, small.region.shape.rect);
1260 assert!(
1261 (large.region.viewport().width - small.region.viewport().width / 1.6 * 3.0).abs()
1262 < 1e-2,
1263 "{:?} did not grow with the scale against {:?}",
1264 large.region.viewport(),
1265 small.region.viewport()
1266 );
1267 }
1268
1269 #[test]
1275 fn only_a_stretched_text_pays_for_the_supersample() {
1276 let rect = Rect::new(10.0, 20.0, 100.0, 30.0);
1277 let moved = frosted(rect, Transform::translate(40.0, 5.0));
1278 let moved = command_to_text_backdrop(&moved, 2.0).expect("a frost");
1279 let turned = frosted(rect, Transform::rotate(0.4));
1280 let turned = command_to_text_backdrop(&turned, 2.0).expect("a frost");
1281
1282 assert_eq!(moved.spec.density, 2.0, "the surface scale, and no more");
1283 assert_eq!(turned.spec.density, 2.0 * TEXT_SUPERSAMPLE);
1284 }
1285
1286 #[test]
1302 fn the_frame_s_groups_are_prepared_into_a_buffer_the_renderer_keeps() {
1303 const MIN_CAP: usize = 4;
1305 assert!(std::mem::size_of::<PreparedLayer>() <= 1024);
1306
1307 let Some(gpu) = crate::or_skip(crate::renderer::GpuContext::try_new()) else {
1308 return;
1309 };
1310 let mut renderer = Renderer::new(
1311 gpu.device.clone(),
1312 gpu.queue.clone(),
1313 wgpu::TextureFormat::Rgba8Unorm,
1314 );
1315 let mut target = RenderTarget::offscreen(&gpu, 32, 32);
1316
1317 let box_at = |x: f32| FlattenedCommand {
1318 command: Rc::new(DrawCommand::RoundedRect {
1319 rect: Rect::new(x, 0.0, 10.0, 10.0),
1320 color: crate::widgets::Color::WHITE,
1321 radius: CornerRadii::uniform(0.0),
1322 curvature: 1.0,
1323 border: None,
1324 shadow: None,
1325 gradient: None,
1326 }),
1327 world_transform: Transform::IDENTITY,
1328 layer: RenderLayer::Shapes,
1329 opacity: 1.0,
1330 clip: None,
1331 };
1332 let commands: [FlattenedCommand; 5] = std::array::from_fn(|i| box_at(i as f32 * 12.0));
1333 let group = |shapes: std::ops::Range<usize>| CommandLayer {
1334 backdrop: 0..0,
1335 shapes,
1336 images: 0..0,
1337 text: 0..0,
1338 overlay: 0..0,
1339 };
1340 let layers: [CommandLayer; 5] = std::array::from_fn(|i| group(i..i + 1));
1341
1342 let mut frame = |renderer: &mut Renderer, layers: &[CommandLayer]| {
1343 renderer.render(
1344 &mut target,
1345 &FlatFrame {
1346 commands: &commands,
1347 layers,
1348 clips: &Default::default(),
1349 },
1350 crate::widgets::Color::TRANSPARENT,
1351 );
1352 };
1353
1354 frame(&mut renderer, &layers);
1355 let first = renderer.prepared_layers.capacity();
1356 assert!(
1357 first > MIN_CAP,
1358 "five groups should have grown past a fresh `Vec`'s floor: {first}"
1359 );
1360
1361 frame(&mut renderer, &layers[..1]);
1364 assert_eq!(
1365 renderer.prepared_layers.capacity(),
1366 first,
1367 "the second frame reused the first frame's buffer"
1368 );
1369 }
1370}