1use oxiui_core::geometry::Size;
32use oxiui_core::paint::{DrawCommand, DrawList, GradientStop, RenderBackend};
33use oxiui_core::{Color, UiError};
34use wgpu::util::DeviceExt;
35
36use crate::clip::{ClipRect, ClipStack};
37use crate::gpu::buffer::{
38 push_circle_quad, push_ellipse_quad, push_gradient_quad, push_line_quad, push_rect_quad,
39 push_rounded_rect_per_corner_quad, push_rounded_rect_quad, Globals, GradientUniforms,
40 GradientVertex, LineQuadParams, Vertex, MAX_GRADIENT_STOPS,
41};
42use crate::gpu::device::GpuContext;
43use crate::gpu::pipeline::{GradientPipeline, SolidPipeline};
44use crate::gpu::tessellator::{tessellate_fill, tessellate_stroke};
45
46#[derive(Clone, Copy, Debug)]
49struct DrawSegment {
50 start: u32,
51 end: u32,
52 scissor: Option<[u32; 4]>,
53}
54
55struct GradientDraw {
58 verts: Vec<GradientVertex>,
59 uniforms: GradientUniforms,
60 scissor: Option<[u32; 4]>,
61}
62
63pub struct WgpuBackend {
67 ctx: GpuContext,
68 pipeline: SolidPipeline,
69 gradient_pipeline: GradientPipeline,
70 globals_buffer: wgpu::Buffer,
71 globals_bind_group: wgpu::BindGroup,
72 clear_color: Color,
73}
74
75impl WgpuBackend {
76 pub fn headless(width: u32, height: u32) -> Result<Self, UiError> {
85 let ctx = GpuContext::headless(width, height)?;
86 let pipeline = SolidPipeline::new(&ctx.device);
87 let gradient_pipeline = GradientPipeline::new(&ctx.device);
88
89 let globals = Globals::new(width, height);
90 let globals_buffer = ctx
91 .device
92 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
93 label: Some("oxiui-render-wgpu globals"),
94 contents: bytemuck::bytes_of(&globals),
95 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
96 });
97
98 let globals_bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
99 label: Some("oxiui-render-wgpu globals bind group"),
100 layout: &pipeline.globals_layout,
101 entries: &[wgpu::BindGroupEntry {
102 binding: 0,
103 resource: globals_buffer.as_entire_binding(),
104 }],
105 });
106
107 Ok(Self {
108 ctx,
109 pipeline,
110 gradient_pipeline,
111 globals_buffer,
112 globals_bind_group,
113 clear_color: Color(0, 0, 0, 0),
114 })
115 }
116
117 pub fn set_clear_color(&mut self, color: Color) {
119 self.clear_color = color;
120 }
121
122 pub fn clear_color(&self) -> Color {
124 self.clear_color
125 }
126
127 pub fn width(&self) -> u32 {
129 self.ctx.width
130 }
131
132 pub fn height(&self) -> u32 {
134 self.ctx.height
135 }
136
137 fn scissor_from_stack(&self, stack: &ClipStack) -> Option<[u32; 4]> {
138 let raw = stack.as_scissor()?;
139 Some(self.clamp_scissor(raw))
140 }
141
142 fn clamp_scissor(&self, [x, y, w, h]: [u32; 4]) -> [u32; 4] {
143 let x = x.min(self.ctx.width);
144 let y = y.min(self.ctx.height);
145 let w = w.min(self.ctx.width - x);
146 let h = h.min(self.ctx.height - y);
147 [x, y, w, h]
148 }
149
150 fn build_geometry(
151 &self,
152 list: &DrawList,
153 ) -> (Vec<Vertex>, Vec<DrawSegment>, Vec<GradientDraw>) {
154 let mut verts: Vec<Vertex> = Vec::new();
155 let mut segments: Vec<DrawSegment> = Vec::new();
156 let mut gradient_draws: Vec<GradientDraw> = Vec::new();
157 let mut stack = ClipStack::new();
158
159 let mut current_scissor = self.scissor_from_stack(&stack);
160 let mut segment_start: u32 = 0;
161
162 let flush = |segs: &mut Vec<DrawSegment>, start: u32, end: u32, sc: Option<[u32; 4]>| {
163 if end > start {
164 segs.push(DrawSegment {
165 start,
166 end,
167 scissor: sc,
168 });
169 }
170 };
171
172 for cmd in list.iter() {
173 match cmd {
174 DrawCommand::PushClip { rect } => {
175 flush(
176 &mut segments,
177 segment_start,
178 verts.len() as u32,
179 current_scissor,
180 );
181 stack.push(ClipRect::new(
182 rect.left(),
183 rect.top(),
184 rect.width(),
185 rect.height(),
186 ));
187 current_scissor = self.scissor_from_stack(&stack);
188 segment_start = verts.len() as u32;
189 }
190 DrawCommand::PopClip => {
191 flush(
192 &mut segments,
193 segment_start,
194 verts.len() as u32,
195 current_scissor,
196 );
197 stack.pop();
198 current_scissor = self.scissor_from_stack(&stack);
199 segment_start = verts.len() as u32;
200 }
201 DrawCommand::FillRect { rect, color } => {
202 push_rect_quad(
203 &mut verts,
204 rect.left(),
205 rect.top(),
206 rect.width(),
207 rect.height(),
208 *color,
209 );
210 }
211 DrawCommand::StrokeRect {
212 rect,
213 thickness,
214 color,
215 } => {
216 emit_stroke_rect(
217 &mut verts,
218 rect.left(),
219 rect.top(),
220 rect.width(),
221 rect.height(),
222 *thickness,
223 *color,
224 );
225 }
226 DrawCommand::FillRoundedRect {
227 rect,
228 radius,
229 color,
230 } => {
231 push_rounded_rect_quad(
232 &mut verts,
233 rect.left(),
234 rect.top(),
235 rect.width(),
236 rect.height(),
237 *radius,
238 *color,
239 );
240 }
241 DrawCommand::FillRoundedRectPerCorner { rect, radii, color } => {
242 push_rounded_rect_per_corner_quad(
243 &mut verts,
244 rect.left(),
245 rect.top(),
246 rect.width(),
247 rect.height(),
248 *radii,
249 *color,
250 );
251 }
252 DrawCommand::FillCircle {
253 center,
254 radius,
255 color,
256 } => {
257 push_circle_quad(&mut verts, center.x, center.y, *radius, *color);
258 }
259 DrawCommand::FillEllipse {
260 center,
261 rx,
262 ry,
263 color,
264 } => {
265 push_ellipse_quad(&mut verts, center.x, center.y, *rx, *ry, *color);
266 }
267 DrawCommand::Line { from, to, color } => {
268 push_line_quad(
269 &mut verts,
270 LineQuadParams {
271 from_x: from.x,
272 from_y: from.y,
273 to_x: to.x,
274 to_y: to.y,
275 half_width: 0.5,
276 color: *color,
277 aa_smooth: false,
278 },
279 );
280 }
281 DrawCommand::LineAa { from, to, color } => {
282 push_line_quad(
283 &mut verts,
284 LineQuadParams {
285 from_x: from.x,
286 from_y: from.y,
287 to_x: to.x,
288 to_y: to.y,
289 half_width: 0.5,
290 color: *color,
291 aa_smooth: true,
292 },
293 );
294 }
295 DrawCommand::LineThick {
296 from,
297 to,
298 width,
299 color,
300 } => {
301 push_line_quad(
302 &mut verts,
303 LineQuadParams {
304 from_x: from.x,
305 from_y: from.y,
306 to_x: to.x,
307 to_y: to.y,
308 half_width: width * 0.5,
309 color: *color,
310 aa_smooth: true,
311 },
312 );
313 }
314 DrawCommand::LineDashed {
315 from,
316 to,
317 dash_len,
318 gap_len,
319 color,
320 } => {
321 emit_dashed_line(
322 &mut verts,
323 DashedLineParams {
324 x0: from.x,
325 y0: from.y,
326 x1: to.x,
327 y1: to.y,
328 dash_len: *dash_len,
329 gap_len: *gap_len,
330 color: *color,
331 },
332 );
333 }
334 DrawCommand::FillPath { path, color } => {
335 tessellate_fill(&mut verts, path, *color);
336 }
337 DrawCommand::StrokePath { path, style, color } => {
338 tessellate_stroke(&mut verts, path, style, *color);
339 }
340 DrawCommand::LinearGradient {
341 rect,
342 start,
343 end,
344 stops,
345 } => {
346 if let Some(gd) = build_gradient_draw_linear(LinearGradientParams {
347 x: rect.left(),
348 y: rect.top(),
349 w: rect.width(),
350 h: rect.height(),
351 sx: start.x,
352 sy: start.y,
353 ex: end.x,
354 ey: end.y,
355 stops,
356 scissor: current_scissor,
357 }) {
358 gradient_draws.push(gd);
359 }
360 }
361 DrawCommand::RadialGradient {
362 rect,
363 center,
364 radius,
365 stops,
366 } => {
367 if let Some(gd) = build_gradient_draw_radial(RadialGradientParams {
368 x: rect.left(),
369 y: rect.top(),
370 w: rect.width(),
371 h: rect.height(),
372 cx: center.x,
373 cy: center.y,
374 radius: *radius,
375 stops,
376 scissor: current_scissor,
377 }) {
378 gradient_draws.push(gd);
379 }
380 }
381 _ => {}
384 }
385 }
386
387 flush(
388 &mut segments,
389 segment_start,
390 verts.len() as u32,
391 current_scissor,
392 );
393 (verts, segments, gradient_draws)
394 }
395
396 pub fn readback_rgba(&self) -> Result<Vec<u8>, UiError> {
403 let width = self.ctx.width;
404 let height = self.ctx.height;
405 let unpadded_bytes_per_row = width * 4;
406 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
407 let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
408 let buffer_size = (padded_bytes_per_row * height) as wgpu::BufferAddress;
409
410 let readback = self.ctx.device.create_buffer(&wgpu::BufferDescriptor {
411 label: Some("oxiui-render-wgpu readback"),
412 size: buffer_size,
413 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
414 mapped_at_creation: false,
415 });
416
417 let mut encoder = self
418 .ctx
419 .device
420 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
421 label: Some("oxiui-render-wgpu readback encoder"),
422 });
423
424 encoder.copy_texture_to_buffer(
425 wgpu::TexelCopyTextureInfo {
426 texture: &self.ctx.color_texture,
427 mip_level: 0,
428 origin: wgpu::Origin3d::ZERO,
429 aspect: wgpu::TextureAspect::All,
430 },
431 wgpu::TexelCopyBufferInfo {
432 buffer: &readback,
433 layout: wgpu::TexelCopyBufferLayout {
434 offset: 0,
435 bytes_per_row: Some(padded_bytes_per_row),
436 rows_per_image: Some(height),
437 },
438 },
439 wgpu::Extent3d {
440 width,
441 height,
442 depth_or_array_layers: 1,
443 },
444 );
445
446 self.ctx.queue.submit(Some(encoder.finish()));
447
448 let slice = readback.slice(..);
449 slice.map_async(wgpu::MapMode::Read, |_| {});
450 self.ctx
451 .device
452 .poll(wgpu::PollType::wait_indefinitely())
453 .map_err(|e| UiError::Render(format!("GPU poll failed during readback: {e:?}")))?;
454
455 let data = slice.get_mapped_range();
456
457 let mut out = Vec::with_capacity((unpadded_bytes_per_row * height) as usize);
458 for row in 0..height {
459 let start = (row * padded_bytes_per_row) as usize;
460 let end = start + unpadded_bytes_per_row as usize;
461 out.extend_from_slice(&data[start..end]);
462 }
463
464 drop(data);
465 readback.unmap();
466 Ok(out)
467 }
468
469 pub fn read_pixel(&self, x: u32, y: u32) -> Result<Option<(u8, u8, u8, u8)>, UiError> {
471 if x >= self.ctx.width || y >= self.ctx.height {
472 return Ok(None);
473 }
474 let buf = self.readback_rgba()?;
475 let idx = ((y * self.ctx.width + x) * 4) as usize;
476 Ok(Some((buf[idx], buf[idx + 1], buf[idx + 2], buf[idx + 3])))
477 }
478}
479
480impl RenderBackend for WgpuBackend {
483 fn surface_size(&self) -> Size {
484 Size::new(self.ctx.width as f32, self.ctx.height as f32)
485 }
486
487 fn supports_gradients(&self) -> bool {
488 true
489 }
490
491 fn supports_paths(&self) -> bool {
492 true
493 }
494
495 fn execute(&mut self, list: &DrawList) -> Result<(), UiError> {
496 let globals = Globals::new(self.ctx.width, self.ctx.height);
497 self.ctx
498 .queue
499 .write_buffer(&self.globals_buffer, 0, bytemuck::bytes_of(&globals));
500
501 let (verts, segments, gradient_draws) = self.build_geometry(list);
502
503 let clear = self.clear_color;
504 let clear_value = wgpu::Color {
505 r: clear.0 as f64 / 255.0,
506 g: clear.1 as f64 / 255.0,
507 b: clear.2 as f64 / 255.0,
508 a: clear.3 as f64 / 255.0,
509 };
510
511 let vertex_buffer = if verts.is_empty() {
512 None
513 } else {
514 Some(
515 self.ctx
516 .device
517 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
518 label: Some("oxiui-render-wgpu solid verts"),
519 contents: bytemuck::cast_slice(&verts),
520 usage: wgpu::BufferUsages::VERTEX,
521 }),
522 )
523 };
524
525 let mut encoder = self
526 .ctx
527 .device
528 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
529 label: Some("oxiui-render-wgpu frame encoder"),
530 });
531
532 {
534 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
535 label: Some("oxiui-render-wgpu solid pass"),
536 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
537 view: &self.ctx.color_view,
538 depth_slice: None,
539 resolve_target: None,
540 ops: wgpu::Operations {
541 load: wgpu::LoadOp::Clear(clear_value),
542 store: wgpu::StoreOp::Store,
543 },
544 })],
545 depth_stencil_attachment: None,
546 timestamp_writes: None,
547 occlusion_query_set: None,
548 multiview_mask: None,
549 });
550
551 if let Some(ref vb) = vertex_buffer {
552 pass.set_pipeline(&self.pipeline.pipeline);
553 pass.set_bind_group(0, &self.globals_bind_group, &[]);
554 pass.set_vertex_buffer(0, vb.slice(..));
555
556 for seg in &segments {
557 match seg.scissor {
558 Some([_, _, 0, _]) | Some([_, _, _, 0]) => continue,
559 Some([x, y, w, h]) => pass.set_scissor_rect(x, y, w, h),
560 None => pass.set_scissor_rect(0, 0, self.ctx.width, self.ctx.height),
561 }
562 pass.draw(seg.start..seg.end, 0..1);
563 }
564 }
565 }
566
567 for gd in &gradient_draws {
569 if gd.verts.is_empty() {
570 continue;
571 }
572
573 let grad_vb = self
574 .ctx
575 .device
576 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
577 label: Some("oxiui-render-wgpu gradient verts"),
578 contents: bytemuck::cast_slice(&gd.verts),
579 usage: wgpu::BufferUsages::VERTEX,
580 });
581
582 let grad_ub = self
583 .ctx
584 .device
585 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
586 label: Some("oxiui-render-wgpu gradient uniforms"),
587 contents: bytemuck::bytes_of(&gd.uniforms),
588 usage: wgpu::BufferUsages::UNIFORM,
589 });
590
591 let grad_bg = self
592 .ctx
593 .device
594 .create_bind_group(&wgpu::BindGroupDescriptor {
595 label: Some("oxiui-render-wgpu gradient bg"),
596 layout: &self.gradient_pipeline.bind_group_layout,
597 entries: &[
598 wgpu::BindGroupEntry {
599 binding: 0,
600 resource: self.globals_buffer.as_entire_binding(),
601 },
602 wgpu::BindGroupEntry {
603 binding: 1,
604 resource: grad_ub.as_entire_binding(),
605 },
606 ],
607 });
608
609 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
610 label: Some("oxiui-render-wgpu gradient pass"),
611 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
612 view: &self.ctx.color_view,
613 depth_slice: None,
614 resolve_target: None,
615 ops: wgpu::Operations {
616 load: wgpu::LoadOp::Load,
617 store: wgpu::StoreOp::Store,
618 },
619 })],
620 depth_stencil_attachment: None,
621 timestamp_writes: None,
622 occlusion_query_set: None,
623 multiview_mask: None,
624 });
625
626 pass.set_pipeline(&self.gradient_pipeline.pipeline);
627 pass.set_bind_group(0, &grad_bg, &[]);
628 pass.set_vertex_buffer(0, grad_vb.slice(..));
629
630 match gd.scissor {
631 Some([_, _, 0, _]) | Some([_, _, _, 0]) => continue,
632 Some([x, y, w, h]) => pass.set_scissor_rect(x, y, w, h),
633 None => pass.set_scissor_rect(0, 0, self.ctx.width, self.ctx.height),
634 }
635
636 pass.draw(0..gd.verts.len() as u32, 0..1);
637 }
638
639 self.ctx.queue.submit(Some(encoder.finish()));
640 Ok(())
641 }
642}
643
644fn emit_stroke_rect(out: &mut Vec<Vertex>, x: f32, y: f32, w: f32, h: f32, t: f32, color: Color) {
647 push_rect_quad(out, x, y, w, t, color);
648 push_rect_quad(out, x, y + h - t, w, t, color);
649 push_rect_quad(out, x, y + t, t, h - 2.0 * t, color);
650 push_rect_quad(out, x + w - t, y + t, t, h - 2.0 * t, color);
651}
652
653struct DashedLineParams {
654 x0: f32,
655 y0: f32,
656 x1: f32,
657 y1: f32,
658 dash_len: f32,
659 gap_len: f32,
660 color: Color,
661}
662
663fn emit_dashed_line(out: &mut Vec<Vertex>, p: DashedLineParams) {
664 let DashedLineParams {
665 x0,
666 y0,
667 x1,
668 y1,
669 dash_len,
670 gap_len,
671 color,
672 } = p;
673 let dx = x1 - x0;
674 let dy = y1 - y0;
675 let total = (dx * dx + dy * dy).sqrt();
676 if total < 1e-6 || dash_len <= 0.0 {
677 return;
678 }
679 let ux = dx / total;
680 let uy = dy / total;
681 let period = dash_len + gap_len.max(0.0);
682 if period < 1e-6 {
683 return;
684 }
685 let mut t = 0.0_f32;
686 while t < total {
687 let end = (t + dash_len).min(total);
688 push_line_quad(
689 out,
690 LineQuadParams {
691 from_x: x0 + ux * t,
692 from_y: y0 + uy * t,
693 to_x: x0 + ux * end,
694 to_y: y0 + uy * end,
695 half_width: 0.5,
696 color,
697 aa_smooth: false,
698 },
699 );
700 t += period;
701 }
702}
703
704struct LinearGradientParams<'a> {
705 x: f32,
706 y: f32,
707 w: f32,
708 h: f32,
709 sx: f32,
710 sy: f32,
711 ex: f32,
712 ey: f32,
713 stops: &'a [GradientStop],
714 scissor: Option<[u32; 4]>,
715}
716
717fn build_gradient_draw_linear(p: LinearGradientParams<'_>) -> Option<GradientDraw> {
718 let LinearGradientParams {
719 x,
720 y,
721 w,
722 h,
723 sx,
724 sy,
725 ex,
726 ey,
727 stops,
728 scissor,
729 } = p;
730 let uniforms = build_gradient_uniforms(0, [sx, sy], [ex, ey], 0.0, stops)?;
731 let mut verts = Vec::new();
732 push_gradient_quad(&mut verts, x, y, w, h);
733 Some(GradientDraw {
734 verts,
735 uniforms,
736 scissor,
737 })
738}
739
740struct RadialGradientParams<'a> {
741 x: f32,
742 y: f32,
743 w: f32,
744 h: f32,
745 cx: f32,
746 cy: f32,
747 radius: f32,
748 stops: &'a [GradientStop],
749 scissor: Option<[u32; 4]>,
750}
751
752fn build_gradient_draw_radial(p: RadialGradientParams<'_>) -> Option<GradientDraw> {
753 let RadialGradientParams {
754 x,
755 y,
756 w,
757 h,
758 cx,
759 cy,
760 radius,
761 stops,
762 scissor,
763 } = p;
764 let uniforms = build_gradient_uniforms(1, [cx, cy], [0.0, 0.0], radius, stops)?;
765 let mut verts = Vec::new();
766 push_gradient_quad(&mut verts, x, y, w, h);
767 Some(GradientDraw {
768 verts,
769 uniforms,
770 scissor,
771 })
772}
773
774fn build_gradient_uniforms(
775 gradient_type: u32,
776 p0: [f32; 2],
777 p1: [f32; 2],
778 radius: f32,
779 stops: &[GradientStop],
780) -> Option<GradientUniforms> {
781 if stops.is_empty() {
782 return None;
783 }
784 let count = stops.len().min(MAX_GRADIENT_STOPS);
785 let mut stop_offsets = [[0.0f32; 4]; MAX_GRADIENT_STOPS];
786 let mut stop_colors = [[0.0f32; 4]; MAX_GRADIENT_STOPS];
787 for (i, s) in stops.iter().take(count).enumerate() {
788 stop_offsets[i] = [s.offset, 0.0, 0.0, 0.0];
789 stop_colors[i] = [
790 s.color.0 as f32 / 255.0,
791 s.color.1 as f32 / 255.0,
792 s.color.2 as f32 / 255.0,
793 s.color.3 as f32 / 255.0,
794 ];
795 }
796 Some(GradientUniforms {
797 p0,
798 p1,
799 radius,
800 gradient_type,
801 stop_count: count as u32,
802 _pad: 0,
803 stop_offsets,
804 stop_colors,
805 })
806}
807
808#[cfg(test)]
811mod tests {
812 use super::*;
813 use oxiui_core::geometry::{Point, Rect};
814 use oxiui_core::paint::{DrawList, GradientStop, PathData, StrokeStyle};
815 use oxiui_core::Color;
816
817 fn try_backend(w: u32, h: u32) -> Option<WgpuBackend> {
818 WgpuBackend::headless(w, h).ok()
819 }
820
821 fn assert_visible(b: &WgpuBackend, x: u32, y: u32, label: &str) {
822 let px = b
823 .read_pixel(x, y)
824 .expect("read_pixel ok")
825 .expect("in bounds");
826 assert!(px.3 > 0, "{label}: pixel ({x},{y}) alpha=0, got {px:?}");
827 }
828
829 fn assert_transparent(b: &WgpuBackend, x: u32, y: u32, label: &str) {
830 let px = b
831 .read_pixel(x, y)
832 .expect("read_pixel ok")
833 .expect("in bounds");
834 assert!(
835 px.3 == 0,
836 "{label}: pixel ({x},{y}) expected transparent, got {px:?}"
837 );
838 }
839
840 #[test]
841 fn test_stroke_rect_renders() {
842 let Some(mut b) = try_backend(100, 100) else {
843 return;
844 };
845 let mut dl = DrawList::new();
846 dl.push(DrawCommand::StrokeRect {
847 rect: Rect::new(10.0, 10.0, 80.0, 80.0),
848 thickness: 4.0,
849 color: Color(255, 0, 0, 255),
850 });
851 b.execute(&dl).expect("execute ok");
852 assert_visible(&b, 12, 10, "stroke_rect top border");
853 assert_transparent(&b, 50, 50, "stroke_rect interior");
854 }
855
856 #[test]
857 fn test_fill_rounded_rect_renders() {
858 let Some(mut b) = try_backend(100, 100) else {
859 return;
860 };
861 let mut dl = DrawList::new();
862 dl.push(DrawCommand::FillRoundedRect {
863 rect: Rect::new(10.0, 10.0, 80.0, 80.0),
864 radius: 10.0,
865 color: Color(0, 200, 0, 255),
866 });
867 b.execute(&dl).expect("execute ok");
868 assert_visible(&b, 50, 50, "rrect centre");
869 assert_transparent(&b, 10, 10, "rrect corner tl");
870 }
871
872 #[test]
873 fn test_fill_rounded_rect_per_corner_renders() {
874 let Some(mut b) = try_backend(100, 100) else {
875 return;
876 };
877 let mut dl = DrawList::new();
878 dl.push(DrawCommand::FillRoundedRectPerCorner {
879 rect: Rect::new(10.0, 10.0, 80.0, 80.0),
880 radii: [15.0, 5.0, 15.0, 5.0],
881 color: Color(0, 100, 200, 255),
882 });
883 b.execute(&dl).expect("execute ok");
884 assert_visible(&b, 50, 50, "rrect-pc centre");
885 }
886
887 #[test]
888 fn test_fill_ellipse_renders() {
889 let Some(mut b) = try_backend(100, 100) else {
890 return;
891 };
892 let mut dl = DrawList::new();
893 dl.push(DrawCommand::FillEllipse {
894 center: Point::new(50.0, 50.0),
895 rx: 30.0,
896 ry: 20.0,
897 color: Color(200, 0, 200, 255),
898 });
899 b.execute(&dl).expect("execute ok");
900 assert_visible(&b, 50, 50, "ellipse centre");
901 assert_transparent(&b, 2, 2, "ellipse exterior");
902 }
903
904 #[test]
905 fn test_line_renders() {
906 let Some(mut b) = try_backend(100, 100) else {
907 return;
908 };
909 let mut dl = DrawList::new();
910 dl.push(DrawCommand::Line {
911 from: Point::new(10.0, 50.0),
912 to: Point::new(90.0, 50.0),
913 color: Color(255, 255, 0, 255),
914 });
915 b.execute(&dl).expect("execute ok");
916 assert_visible(&b, 50, 50, "line mid");
917 }
918
919 #[test]
920 fn test_fill_path_renders() {
921 let Some(mut b) = try_backend(100, 100) else {
922 return;
923 };
924 let mut path = PathData::new();
925 path.move_to(Point::new(20.0, 20.0));
926 path.line_to(Point::new(80.0, 20.0));
927 path.line_to(Point::new(50.0, 80.0));
928 path.close();
929 let mut dl = DrawList::new();
930 dl.push(DrawCommand::FillPath {
931 path,
932 color: Color(255, 0, 128, 255),
933 });
934 b.execute(&dl).expect("execute ok");
935 assert_visible(&b, 50, 40, "fill_path interior");
936 assert_transparent(&b, 2, 2, "fill_path exterior");
937 }
938
939 #[test]
940 fn test_stroke_path_renders() {
941 let Some(mut b) = try_backend(100, 100) else {
942 return;
943 };
944 let mut path = PathData::new();
945 path.move_to(Point::new(20.0, 50.0));
946 path.line_to(Point::new(80.0, 50.0));
947 let style = StrokeStyle {
948 width: 4.0,
949 ..Default::default()
950 };
951 let mut dl = DrawList::new();
952 dl.push(DrawCommand::StrokePath {
953 path,
954 style,
955 color: Color(200, 200, 0, 255),
956 });
957 b.execute(&dl).expect("execute ok");
958 assert_visible(&b, 50, 50, "stroke_path mid");
959 }
960
961 #[test]
962 fn test_linear_gradient_renders() {
963 let Some(mut b) = try_backend(100, 100) else {
964 return;
965 };
966 let stops = vec![
967 GradientStop::new(0.0, Color(255, 0, 0, 255)),
968 GradientStop::new(1.0, Color(0, 0, 255, 255)),
969 ];
970 let mut dl = DrawList::new();
971 dl.push(DrawCommand::LinearGradient {
972 rect: Rect::new(0.0, 0.0, 100.0, 100.0),
973 start: Point::new(0.0, 50.0),
974 end: Point::new(100.0, 50.0),
975 stops,
976 });
977 b.execute(&dl).expect("execute ok");
978 let left = b.read_pixel(5, 50).expect("ok").expect("bounds");
979 assert!(left.0 > 128, "left reddish: {left:?}");
980 let right = b.read_pixel(95, 50).expect("ok").expect("bounds");
981 assert!(right.2 > 128, "right bluish: {right:?}");
982 let mid = b.read_pixel(50, 50).expect("ok").expect("bounds");
983 assert!(mid.3 > 0, "mid visible: {mid:?}");
984 }
985
986 #[test]
987 fn test_radial_gradient_renders() {
988 let Some(mut b) = try_backend(100, 100) else {
989 return;
990 };
991 let stops = vec![
992 GradientStop::new(0.0, Color(255, 255, 255, 255)),
993 GradientStop::new(1.0, Color(0, 0, 0, 255)),
994 ];
995 let mut dl = DrawList::new();
996 dl.push(DrawCommand::RadialGradient {
997 rect: Rect::new(0.0, 0.0, 100.0, 100.0),
998 center: Point::new(50.0, 50.0),
999 radius: 40.0,
1000 stops,
1001 });
1002 b.execute(&dl).expect("execute ok");
1003 let centre = b.read_pixel(50, 50).expect("ok").expect("bounds");
1004 assert!(centre.0 > 200, "centre bright: {centre:?}");
1005 let edge = b.read_pixel(90, 50).expect("ok").expect("bounds");
1006 assert!(
1007 edge.0 < centre.0,
1008 "edge darker: edge={edge:?} centre={centre:?}"
1009 );
1010 }
1011
1012 #[test]
1013 fn test_supports_probes() {
1014 let Some(b) = try_backend(64, 64) else {
1015 return;
1016 };
1017 assert!(b.supports_gradients());
1018 assert!(b.supports_paths());
1019 }
1020}