1use crate::engine::layouter::types::{
4 Color, ColorStop, Gradient, GradientKind, RadialShape, RadialSizeKind,
5};
6use crate::engine::renderer_model::DrawCommand;
7use anyhow::Result;
8use std::sync::Arc;
9use std::{env, fmt::Debug};
10use wgpu::util::DeviceExt;
11use winit::window::Window;
12
13use super::text::text::{TextRenderer, TextSection};
14
15pub struct GpuRenderer {
17 surface: wgpu::Surface<'static>,
19 device: wgpu::Device,
21 queue: wgpu::Queue,
23 config: wgpu::SurfaceConfiguration,
25 size: winit::dpi::PhysicalSize<u32>,
27 scale_factor: f64,
29 render_pipeline: wgpu::RenderPipeline,
31 vertex_buffer: Option<wgpu::Buffer>,
33 vertices: Vec<Vertex>,
35 num_vertices: u32,
37
38 text_renderer: Option<TextRenderer>,
40
41 enable_text_culling: bool,
43}
44
45#[repr(C)]
46#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
47struct Vertex {
48 position: [f32; 3],
49 color: [f32; 4],
50}
51
52impl Vertex {
53 fn desc() -> wgpu::VertexBufferLayout<'static> {
54 wgpu::VertexBufferLayout {
55 array_stride: size_of::<Vertex>() as wgpu::BufferAddress,
56 step_mode: wgpu::VertexStepMode::Vertex,
57 attributes: &[
58 wgpu::VertexAttribute {
59 offset: 0,
60 shader_location: 0,
61 format: wgpu::VertexFormat::Float32x3,
62 },
63 wgpu::VertexAttribute {
64 offset: size_of::<[f32; 3]>() as wgpu::BufferAddress,
65 shader_location: 1,
66 format: wgpu::VertexFormat::Float32x4,
67 },
68 ],
69 }
70 }
71}
72
73impl GpuRenderer {
74 pub async fn new(window: Arc<Window>, font_path: Option<&str>) -> Result<Self> {
76 let size = window.inner_size();
77 let scale_factor = window.scale_factor();
78
79 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
85 backends: select_wgpu_backends(),
86 flags: Default::default(),
87 memory_budget_thresholds: Default::default(),
88 backend_options: Default::default(),
89 display: None,
90 });
91
92 let surface = instance.create_surface(Arc::clone(&window))?;
95
96 let adapter = instance
98 .request_adapter(&wgpu::RequestAdapterOptions {
99 power_preference: wgpu::PowerPreference::default(),
100 compatible_surface: Some(&surface),
101 force_fallback_adapter: false,
102 apply_limit_buckets: true,
106 })
107 .await?;
108
109 let (device, queue) = adapter
111 .request_device(&wgpu::DeviceDescriptor {
112 label: None,
113 required_features: wgpu::Features::empty(),
114 required_limits: wgpu::Limits::default(),
115 experimental_features: Default::default(),
116 memory_hints: wgpu::MemoryHints::default(),
117 trace: Default::default(),
118 })
119 .await?;
120
121 let surface_caps = surface.get_capabilities(&adapter);
124 let surface_format = surface_caps
125 .formats
126 .iter()
127 .copied()
128 .find(|f| f.is_srgb())
129 .unwrap_or(surface_caps.formats[0]);
130
131 let config = wgpu::SurfaceConfiguration {
132 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
133 format: surface_format,
134 color_space: wgpu::SurfaceColorSpace::Auto,
137 width: size.width,
138 height: size.height,
139 present_mode: surface_caps.present_modes[0],
140 alpha_mode: surface_caps.alpha_modes[0],
141 view_formats: vec![],
142 desired_maximum_frame_latency: 2,
143 };
144 surface.configure(&device, &config);
145
146 let main_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
150 label: Some("Main Shader"),
151 source: wgpu::ShaderSource::Wgsl(include_str!("shader/main.wgsl").into()),
152 });
153
154 let render_pipeline_layout =
156 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
157 label: Some("Render Pipeline Layout"),
158 bind_group_layouts: &[],
159 immediate_size: 0,
160 });
161
162 let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
163 label: Some("Render Pipeline"),
164 layout: Some(&render_pipeline_layout),
165 cache: None,
166 vertex: wgpu::VertexState {
167 module: &main_shader,
168 entry_point: Some("vs_main"),
169 buffers: &[Some(Vertex::desc())],
170 compilation_options: wgpu::PipelineCompilationOptions::default(),
171 },
172 fragment: Some(wgpu::FragmentState {
173 module: &main_shader,
174 entry_point: Some("fs_main"),
175 targets: &[Some(wgpu::ColorTargetState {
176 format: config.format,
177 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
178 write_mask: wgpu::ColorWrites::ALL,
179 })],
180 compilation_options: wgpu::PipelineCompilationOptions::default(),
181 }),
182 primitive: wgpu::PrimitiveState {
183 topology: wgpu::PrimitiveTopology::TriangleList,
184 strip_index_format: None,
185 front_face: wgpu::FrontFace::Ccw,
186 cull_mode: None, polygon_mode: wgpu::PolygonMode::Fill,
188 unclipped_depth: false,
189 conservative: false,
190 },
191 depth_stencil: None,
192 multisample: wgpu::MultisampleState {
193 count: 1,
194 mask: !0,
195 alpha_to_coverage_enabled: false,
196 },
197 multiview_mask: None,
198 });
199 let text_renderer = if let Some(p) = font_path {
203 match std::fs::read(p) {
204 Ok(bytes) => {
205 match TextRenderer::new_from_bytes(&device, &queue, config.format, bytes) {
206 Ok(t) => Some(t),
207 Err(e) => {
208 log::warn!(target:"PRender::gpu::font" ,"failed to init text renderer from provided font: {}", e);
209 None
210 }
211 }
212 }
213 Err(e) => {
214 log::warn!(target:"PRender::gpu::font" ,"failed to read font path '{}': {}", p, e);
215 None
216 }
217 }
218 } else {
219 match TextRenderer::new_from_device(&device, &queue, config.format) {
220 Ok(t) => Some(t),
221 Err(e) => {
222 log::warn!(target:"PRender::gpu::font" ,"no system font found for text renderer: {}", e);
223 None
224 }
225 }
226 };
227
228 let enable_text_culling = std::env::var("ORINIUM_TEXT_CULL").map_or(true, |v| v != "0");
230
231 Ok(Self {
232 surface,
233 device,
234 queue,
235 config,
236 size,
237 scale_factor,
238 render_pipeline,
239 vertex_buffer: None,
240 vertices: vec![],
241 num_vertices: 0,
242 text_renderer,
243 enable_text_culling,
244 })
245 }
246
247 pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
249 if new_size.width > 0 && new_size.height > 0 {
250 log::info!(target:"PRender::gpu::resized", "Resized: {}x{}", new_size.width, new_size.height);
251
252 let old_size = self.size;
253
254 self.size = new_size;
255
256 self.config.width = new_size.width;
257 self.config.height = new_size.height;
258
259 self.surface.configure(&self.device, &self.config);
260
261 self.update_vertices(old_size, new_size);
262
263 if let Some(tr) = &mut self.text_renderer {
264 tr.resize_view(
265 self.config.width as f32,
266 self.config.height as f32,
267 &self.queue,
268 );
269 }
270 }
271 }
272
273 pub fn parse_draw_commands(&mut self, commands: &[DrawCommand]) {
275 let screen_width = self.size.width as f32;
276 let screen_height = self.size.height as f32;
277
278 let mut vertices = Vec::new();
280 let mut sections: Vec<TextSection> = Vec::new();
282 let sf = self.scale_factor as f32;
284 let mut transform_stack: Vec<(f32, f32)> = vec![(0.0, 0.0)];
286 let current_transform = |stack: &Vec<(f32, f32)>| -> (f32, f32) {
287 let mut dx = 0.0;
288 let mut dy = 0.0;
289 for (x, y) in stack.iter() {
290 dx += x;
291 dy += y;
292 }
293 (dx, dy)
294 };
295 #[derive(Clone, Copy)]
297 struct ClipRect {
298 x: f32,
299 y: f32,
300 w: f32,
301 h: f32,
302 }
303 let mut clip_stack: Vec<ClipRect> = vec![ClipRect {
304 x: 0.0,
305 y: 0.0,
306 w: screen_width,
307 h: screen_height,
308 }];
309 let current_clip = |stack: &Vec<ClipRect>| -> ClipRect { *stack.last().unwrap() };
310
311 for command in commands {
312 match command {
313 DrawCommand::PushTransform { dx, dy } => {
315 transform_stack.push((*dx, *dy));
316 }
317 DrawCommand::PopTransform => {
318 if transform_stack.len() > 1 {
319 transform_stack.pop();
320 }
321 }
322
323 DrawCommand::PushClip {
325 x,
326 y,
327 width: w,
328 height: h,
329 } => {
330 let (tdx, tdy) = current_transform(&transform_stack);
331 let new_clip = ClipRect {
332 x: x + tdx,
333 y: y + tdy,
334 w: *w,
335 h: *h,
336 };
337
338 let parent = current_clip(&clip_stack);
340
341 let x1 = new_clip.x.max(parent.x);
342 let y1 = new_clip.y.max(parent.y);
343 let x2 = (new_clip.x + new_clip.w).min(parent.x + parent.w);
344 let y2 = (new_clip.y + new_clip.h).min(parent.y + parent.h);
345
346 clip_stack.push(ClipRect {
347 x: x1,
348 y: y1,
349 w: (x2 - x1).max(0.0),
350 h: (y2 - y1).max(0.0),
351 });
352 }
353
354 DrawCommand::PopClip => {
355 if clip_stack.len() > 1 {
356 clip_stack.pop();
357 }
358 }
359
360 DrawCommand::DrawRect {
362 x,
363 y,
364 width: w,
365 height: h,
366 color,
367 } => {
368 let (tdx, tdy) = current_transform(&transform_stack);
370 let mut x1 = (x + tdx) * sf;
371 let mut y1 = (y + tdy) * sf;
372 let mut x2 = x1 + w * sf;
373 let mut y2 = y1 + h * sf;
374
375 let clip = current_clip(&clip_stack);
377
378 if x2 <= clip.x * sf
380 || x1 >= (clip.x + clip.w) * sf
381 || y2 <= clip.y * sf
382 || y1 >= (clip.y + clip.h) * sf
383 {
384 continue;
385 }
386
387 x1 = x1.max(clip.x * sf);
389 y1 = y1.max(clip.y * sf);
390 x2 = x2.min((clip.x + clip.w) * sf);
391 y2 = y2.min((clip.y + clip.h) * sf);
392
393 let ndc = |v, max| (v / max) * 2.0 - 1.0;
395
396 let px1 = ndc(x1, screen_width);
397 let py1 = -ndc(y1, screen_height);
398 let px2 = ndc(x2, screen_width);
399 let py2 = -ndc(y2, screen_height);
400
401 let color = color.to_linear_f32_array();
402
403 #[rustfmt::skip]
404 vertices.extend_from_slice(&[
405 Vertex { position: [px1, py1, 0.0], color },
406 Vertex { position: [px1, py2, 0.0], color },
407 Vertex { position: [px2, py1, 0.0], color },
408
409 Vertex { position: [px2, py1, 0.0], color },
410 Vertex { position: [px1, py2, 0.0], color },
411 Vertex { position: [px2, py2, 0.0], color },
412 ]);
413 }
414
415 DrawCommand::DrawGradientRect {
417 x,
418 y,
419 width: w,
420 height: h,
421 gradient,
422 } => {
423 let (tdx, tdy) = current_transform(&transform_stack);
424 let mut x1 = (x + tdx) * sf;
425 let mut y1 = (y + tdy) * sf;
426 let mut x2 = x1 + w * sf;
427 let mut y2 = y1 + h * sf;
428
429 let clip = current_clip(&clip_stack);
430
431 if x2 <= clip.x * sf
432 || x1 >= (clip.x + clip.w) * sf
433 || y2 <= clip.y * sf
434 || y1 >= (clip.y + clip.h) * sf
435 {
436 continue;
437 }
438
439 x1 = x1.max(clip.x * sf);
440 y1 = y1.max(clip.y * sf);
441 x2 = x2.min((clip.x + clip.w) * sf);
442 y2 = y2.min((clip.y + clip.h) * sf);
443
444 let logical_corners = [
445 ((x + tdx) * sf, (y + tdy) * sf), ((x + tdx) * sf, (y + tdy + h) * sf), ((x + tdx + w) * sf, (y + tdy) * sf), ((x + tdx + w) * sf, (y + tdy + h) * sf), ];
450
451 match &gradient.kind {
452 GradientKind::Linear { .. } => {
453 let visible_corners = [(x1, y1), (x1, y2), (x2, y1), (x2, y2)];
454
455 let corner_colors = compute_gradient_corner_colors_extent(
456 gradient,
457 &logical_corners,
458 &visible_corners,
459 );
460 let colors_lin = [
461 corner_colors[0].to_linear_f32_array(),
462 corner_colors[1].to_linear_f32_array(),
463 corner_colors[2].to_linear_f32_array(),
464 corner_colors[3].to_linear_f32_array(),
465 ];
466 emit_rect_vertices(
467 &mut vertices,
468 x1,
469 y1,
470 x2,
471 y2,
472 screen_width,
473 screen_height,
474 colors_lin,
475 );
476 }
477 GradientKind::Radial { .. } => {
478 let (cx, cy, rx, ry) =
479 compute_radial_params(&gradient.kind, &logical_corners);
480 emit_radial_gradient_vertices(
481 &mut vertices,
482 x1,
483 y1,
484 x2,
485 y2,
486 screen_width,
487 screen_height,
488 cx,
489 cy,
490 rx,
491 ry,
492 &gradient.stops,
493 );
494 }
495 }
496 }
497
498 DrawCommand::DrawText { x, y, text, style } => {
500 let (tdx, tdy) = current_transform(&transform_stack);
501
502 let clip = current_clip(&clip_stack);
503
504 let tw = clip.w;
505 let th = clip.h;
506
507 let font_size = &style.font_size;
508
509 let mut skip_text = false;
511 if self.enable_text_culling {
512 let sx1 = (x + tdx) * sf;
514 let sy1 = (y + tdy) * sf;
515 let est_w = if !tw.is_finite() || tw <= 0.0 {
517 (*font_size * sf) * (text.len().max(1) as f32) * 0.5
519 } else {
520 tw * sf
521 };
522 let est_h = if !th.is_finite() || th <= 0.0 {
523 (*font_size * sf) * 1.2 * (text.lines().count() as f32).max(1.0)
525 } else {
526 th * sf
527 };
528 let sx2 = sx1 + est_w;
529 let sy2 = sy1 + est_h;
530
531 let clip_l = clip.x * sf;
532 let clip_t = clip.y * sf;
533 let clip_r = (clip.x + clip.w) * sf;
534 let clip_b = (clip.y + clip.h) * sf;
535
536 if sx2 <= clip_l || sx1 >= clip_r || sy2 <= clip_t || sy1 >= clip_b {
537 skip_text = true;
538 }
539 }
540
541 if skip_text {
542 continue;
543 }
544
545 let section = if let Some(tr) = &mut self.text_renderer {
547 let mut render_text_style = *style;
548 render_text_style.font_size = *font_size * sf;
549 let layout = tr.create_buffer_for_text(text, render_text_style);
550
551 TextSection {
552 screen_position: ((*x + tdx) * sf, (*y + tdy) * sf),
553 clip_origin: (clip.x * sf, clip.y * sf),
554 bounds: (tw * sf, th * sf),
555 layout,
556 }
557 } else {
558 continue;
560 };
561 sections.push(section);
562 }
563
564 DrawCommand::DrawPolygon { points, color } => {
566 let (tdx, tdy) = current_transform(&transform_stack);
568 let transformed_points: Vec<(f32, f32)> = points
569 .iter()
570 .map(|(px, py)| ((px + tdx) * sf, (py + tdy) * sf))
571 .collect();
572
573 let clip = current_clip(&clip_stack);
575 let clip_l = clip.x * sf;
577 let clip_t = clip.y * sf;
578 let clip_r = (clip.x + clip.w) * sf;
579 let clip_b = (clip.y + clip.h) * sf;
580
581 let mut min_x = f32::INFINITY;
583 let mut min_y = f32::INFINITY;
584 let mut max_x = f32::NEG_INFINITY;
585 let mut max_y = f32::NEG_INFINITY;
586 for (x, y) in transformed_points.iter() {
587 min_x = min_x.min(*x);
588 min_y = min_y.min(*y);
589 max_x = max_x.max(*x);
590 max_y = max_y.max(*y);
591 }
592 if max_x <= clip_l || min_x >= clip_r || max_y <= clip_t || min_y >= clip_b {
593 continue;
595 }
596
597 let clip_against_edge = |poly: &Vec<(f32, f32)>, edge: u8| -> Vec<(f32, f32)> {
599 let mut out: Vec<(f32, f32)> = Vec::new();
601 if poly.is_empty() {
602 return out;
603 }
604 let len = poly.len();
605 for i in 0..len {
606 let (sx, sy) = poly[i];
607 let (ex, ey) = poly[(i + 1) % len];
608 let inside = |x: f32, y: f32| -> bool {
610 match edge {
611 0 => x >= clip_l, 1 => x <= clip_r, 2 => y >= clip_t, 3 => y <= clip_b, _ => true,
616 }
617 };
618 let s_in = inside(sx, sy);
619 let e_in = inside(ex, ey);
620
621 if s_in && e_in {
622 out.push((ex, ey));
624 } else if s_in && !e_in {
625 let (ix, iy) = match edge {
628 0 | 1 => {
629 let x_edge = if edge == 0 { clip_l } else { clip_r };
631 let dx = ex - sx;
632 if dx.abs() < f32::EPSILON {
633 (x_edge, sy)
634 } else {
635 let t = (x_edge - sx) / dx;
636 (x_edge, sy + t * (ey - sy))
637 }
638 }
639 2 | 3 => {
640 let y_edge = if edge == 2 { clip_t } else { clip_b };
642 let dy = ey - sy;
643 if dy.abs() < f32::EPSILON {
644 (sx, y_edge)
645 } else {
646 let t = (y_edge - sy) / dy;
647 (sx + t * (ex - sx), y_edge)
648 }
649 }
650 _ => (ex, ey),
651 };
652 out.push((ix, iy));
653 } else if !s_in && e_in {
654 let (ix, iy) = match edge {
656 0 | 1 => {
657 let x_edge = if edge == 0 { clip_l } else { clip_r };
658 let dx = ex - sx;
659 if dx.abs() < f32::EPSILON {
660 (x_edge, sy)
661 } else {
662 let t = (x_edge - sx) / dx;
663 (x_edge, sy + t * (ey - sy))
664 }
665 }
666 2 | 3 => {
667 let y_edge = if edge == 2 { clip_t } else { clip_b };
668 let dy = ey - sy;
669 if dy.abs() < f32::EPSILON {
670 (sx, y_edge)
671 } else {
672 let t = (y_edge - sy) / dy;
673 (sx + t * (ex - sx), y_edge)
674 }
675 }
676 _ => (ex, ey),
677 };
678 out.push((ix, iy));
679 out.push((ex, ey));
680 } else {
681 }
683 }
684 out
685 };
686
687 if transformed_points.len() < 3 {
689 continue;
690 }
691
692 let ndc = |v: f32, max: f32| (v / max) * 2.0 - 1.0;
694
695 let color_arr = color.to_linear_f32_array();
696
697 let v0 = transformed_points[0];
698 for i in 1..(transformed_points.len() - 1) {
699 let tri = vec![v0, transformed_points[i], transformed_points[i + 1]];
700 let mut poly = tri;
702 poly = clip_against_edge(&poly, 0); if poly.is_empty() {
704 continue;
705 }
706 poly = clip_against_edge(&poly, 1); if poly.is_empty() {
708 continue;
709 }
710 poly = clip_against_edge(&poly, 2); if poly.is_empty() {
712 continue;
713 }
714 poly = clip_against_edge(&poly, 3); if poly.is_empty() {
716 continue;
717 }
718
719 for j in 1..(poly.len() - 1) {
721 let p1 = poly[0];
722 let p2 = poly[j];
723 let p3 = poly[j + 1];
724
725 let px1 = ndc(p1.0, screen_width);
726 let py1 = -ndc(p1.1, screen_height);
727 let px2 = ndc(p2.0, screen_width);
728 let py2 = -ndc(p2.1, screen_height);
729 let px3 = ndc(p3.0, screen_width);
730 let py3 = -ndc(p3.1, screen_height);
731
732 vertices.push(Vertex {
733 position: [px1, py1, 0.0],
734 color: color_arr,
735 });
736 vertices.push(Vertex {
737 position: [px2, py2, 0.0],
738 color: color_arr,
739 });
740 vertices.push(Vertex {
741 position: [px3, py3, 0.0],
742 color: color_arr,
743 });
744 }
745 }
746 }
747
748 #[allow(unused)]
750 DrawCommand::DrawEllipse {
751 center,
752 radius_x,
753 radius_y,
754 color,
755 } => {
756 let (tdx, tdy) = current_transform(&transform_stack);
758 let cx = center.0 + tdx;
759 let cy = center.1 + tdy;
760
761 let clip = current_clip(&clip_stack);
763
764 todo!("Ellipse drawing with clipping is not implemented yet");
765 }
766 }
767 }
768
769 self.set_vertex_buffer(vertices);
770
771 if let Some(tr) = &mut self.text_renderer {
773 tr.queue(&self.device, &self.queue, §ions).unwrap();
774 }
775 }
776
777 pub fn render(&mut self) -> Result<()> {
783 let current_surface_texture = self.surface.get_current_texture();
785
786 let output = if let wgpu::CurrentSurfaceTexture::Success(frame) = current_surface_texture {
787 frame
788 } else {
789 anyhow::bail!(
790 "`surface.get_current_texture` hasn't succeeded: {:?}.",
791 current_surface_texture
792 );
793 };
794 let view = output
795 .texture
796 .create_view(&wgpu::TextureViewDescriptor::default());
797
798 let mut encoder = self
803 .device
804 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
805 label: Some("Render Encoder"),
806 });
807
808 {
810 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
811 label: Some("Render Pass"),
812 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
813 view: &view,
814 resolve_target: None,
815 ops: wgpu::Operations {
816 load: wgpu::LoadOp::Clear(wgpu::Color {
818 r: 1.0,
819 g: 1.0,
820 b: 1.0,
821 a: 1.0,
822 }),
823 store: wgpu::StoreOp::Store,
824 },
825 depth_slice: None,
826 })],
827 depth_stencil_attachment: None,
828 occlusion_query_set: None,
829 timestamp_writes: None,
830 multiview_mask: None,
831 });
832
833 render_pass.set_pipeline(&self.render_pipeline);
835 if let Some(ref vertex_buffer) = self.vertex_buffer {
837 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
838 render_pass.draw(0..self.num_vertices, 0..1);
839 }
840 }
841
842 if let Some(tr) = &mut self.text_renderer {
844 let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
845 label: Some("Text Render Pass"),
846 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
847 view: &view,
848 resolve_target: None,
849 ops: wgpu::Operations {
850 load: wgpu::LoadOp::Load,
851 store: wgpu::StoreOp::Store,
852 },
853 depth_slice: None,
854 })],
855 depth_stencil_attachment: None,
856 occlusion_query_set: None,
857 timestamp_writes: None,
858 multiview_mask: None,
859 });
860 tr.draw(&mut rpass);
861 }
862
863 self.queue.submit(std::iter::once(encoder.finish()));
865
866 self.queue.present(output);
868
869 Ok(())
870 }
871
872 fn update_vertices(
873 &mut self,
874 old_size: winit::dpi::PhysicalSize<u32>,
875 new_size: winit::dpi::PhysicalSize<u32>,
876 ) {
877 let old_w = old_size.width as f32;
878 let old_h = old_size.height as f32;
879 let new_w = new_size.width as f32;
880 let new_h = new_size.height as f32;
881
882 let mut new_vertices = self.vertices.clone();
883
884 for vertex in new_vertices.iter_mut() {
885 let logical_x = (vertex.position[0] + 1.0) / 2.0 * old_w;
887 let logical_y = -(vertex.position[1] - 1.0) / 2.0 * old_h;
888
889 vertex.position[0] = (logical_x / new_w) * 2.0 - 1.0;
891 vertex.position[1] = -((logical_y / new_h) * 2.0 - 1.0);
892 }
893 self.set_vertex_buffer(new_vertices);
894 }
895
896 fn set_vertex_buffer(&mut self, vertices: Vec<Vertex>) {
897 if !vertices.is_empty() {
899 self.vertex_buffer = Some(self.device.create_buffer_init(
900 &wgpu::util::BufferInitDescriptor {
901 label: Some("Vertex Buffer"),
902 contents: bytemuck::cast_slice(&vertices),
903 usage: wgpu::BufferUsages::VERTEX,
904 },
905 ));
906 self.num_vertices = vertices.len() as u32;
907 }
908 self.vertices = vertices;
909 }
910
911 pub fn set_scale_factor(&mut self, scale_factor: f64) {
912 self.scale_factor = scale_factor;
913 }
914}
915
916fn compute_gradient_corner_colors_extent(
922 gradient: &Gradient,
923 extent_corners: &[(f32, f32); 4],
924 sample_corners: &[(f32, f32); 4],
925) -> [Color; 4] {
926 let GradientKind::Linear { angle } = &gradient.kind else {
927 return [Color(0, 0, 0, 0); 4];
928 };
929 let rad = angle.to_radians();
930 let dir_x = rad.sin();
931 let dir_y = -rad.cos();
932
933 let extent_projs: Vec<f32> = extent_corners
935 .iter()
936 .map(|(cx, cy)| cx * dir_x + cy * dir_y)
937 .collect();
938 let min_p = extent_projs.iter().cloned().fold(f32::INFINITY, f32::min);
939 let max_p = extent_projs
940 .iter()
941 .cloned()
942 .fold(f32::NEG_INFINITY, f32::max);
943 let range = max_p - min_p;
944
945 let mut colors = [Color(0, 0, 0, 0); 4];
947 for (i, (cx, cy)) in sample_corners.iter().enumerate() {
948 let p = cx * dir_x + cy * dir_y;
949 let t = if range > 0.0 {
950 (p - min_p) / range
951 } else {
952 0.0
953 };
954 colors[i] = sample_gradient_stops(&gradient.stops, t);
955 }
956 colors
957}
958
959fn ellipse_rx_for_corner(dx: f32, dy: f32, w: f32, h: f32) -> f32 {
967 if h <= 0.0 || w <= 0.0 {
968 return dx;
969 }
970 (dx * dx + dy * dy * (w / h) * (w / h)).sqrt()
971}
972
973fn compute_radial_params(kind: &GradientKind, corners: &[(f32, f32); 4]) -> (f32, f32, f32, f32) {
974 let GradientKind::Radial {
975 shape,
976 size,
977 position,
978 } = kind
979 else {
980 return (0.0, 0.0, 1.0, 1.0);
981 };
982
983 let min_x = corners.iter().map(|c| c.0).fold(f32::INFINITY, f32::min);
984 let max_x = corners
985 .iter()
986 .map(|c| c.0)
987 .fold(f32::NEG_INFINITY, f32::max);
988 let min_y = corners.iter().map(|c| c.1).fold(f32::INFINITY, f32::min);
989 let max_y = corners
990 .iter()
991 .map(|c| c.1)
992 .fold(f32::NEG_INFINITY, f32::max);
993 let w = max_x - min_x;
994 let h = max_y - min_y;
995 let cx = min_x + w * position.0;
996 let cy = min_y + h * position.1;
997
998 log::trace!(
999 "compute_radial_params: corners=[({:.1},{:.1}),({:.1},{:.1}),({:.1},{:.1}),({:.1},{:.1})] \
1000 box=({:.1},{:.1}) center=({:.1},{:.1}) shape={:?} size={:?}",
1001 corners[0].0,
1002 corners[0].1,
1003 corners[1].0,
1004 corners[1].1,
1005 corners[2].0,
1006 corners[2].1,
1007 corners[3].0,
1008 corners[3].1,
1009 w,
1010 h,
1011 cx,
1012 cy,
1013 shape,
1014 size,
1015 );
1016
1017 let (rx, ry) = match (shape, size) {
1018 (RadialShape::Circle, RadialSizeKind::ClosestSide) => {
1019 let d = (cx - min_x)
1020 .min(max_x - cx)
1021 .min((cy - min_y).min(max_y - cy));
1022 (d, d)
1023 }
1024 (RadialShape::Circle, RadialSizeKind::FarthestSide) => {
1025 let d = (cx - min_x)
1026 .max(max_x - cx)
1027 .max((cy - min_y).max(max_y - cy));
1028 (d, d)
1029 }
1030 (RadialShape::Circle, RadialSizeKind::ClosestCorner) => {
1031 let d = corners
1032 .iter()
1033 .map(|(px, py)| ((px - cx).powi(2) + (py - cy).powi(2)).sqrt())
1034 .fold(f32::INFINITY, f32::min);
1035 (d, d)
1036 }
1037 (RadialShape::Circle, RadialSizeKind::FarthestCorner) => {
1038 let d = corners
1039 .iter()
1040 .map(|(px, py)| ((px - cx).powi(2) + (py - cy).powi(2)).sqrt())
1041 .fold(0.0f32, f32::max);
1042 (d, d)
1043 }
1044 (RadialShape::Ellipse, RadialSizeKind::ClosestSide) => {
1045 let rx = (cx - min_x).min(max_x - cx);
1046 let ry = (cy - min_y).min(max_y - cy);
1047 (rx, ry)
1048 }
1049 (RadialShape::Ellipse, RadialSizeKind::FarthestSide) => {
1050 let rx = (cx - min_x).max(max_x - cx);
1051 let ry = (cy - min_y).max(max_y - cy);
1052 (rx, ry)
1053 }
1054 (RadialShape::Ellipse, RadialSizeKind::ClosestCorner) => {
1055 let mut best_rx = f32::INFINITY;
1056 let mut best_ry = f32::INFINITY;
1057 for (px, py) in corners.iter() {
1058 let dx = (px - cx).abs();
1059 let dy = (py - cy).abs();
1060 let rx = ellipse_rx_for_corner(dx, dy, w, h);
1061 let ry = rx * h / w;
1062 if rx < best_rx {
1063 best_rx = rx;
1064 best_ry = ry;
1065 }
1066 }
1067 (best_rx, best_ry)
1068 }
1069 (RadialShape::Ellipse, RadialSizeKind::FarthestCorner) => {
1070 let mut best_rx = 0.0f32;
1071 let mut best_ry = 0.0f32;
1072 for (px, py) in corners.iter() {
1073 let dx = (px - cx).abs();
1074 let dy = (py - cy).abs();
1075 let rx = ellipse_rx_for_corner(dx, dy, w, h);
1076 let ry = rx * h / w;
1077 if rx > best_rx {
1078 best_rx = rx;
1079 best_ry = ry;
1080 }
1081 }
1082 (best_rx, best_ry)
1083 }
1084 };
1085
1086 let rx = rx.max(0.001);
1087 let ry = ry.max(0.001);
1088
1089 log::trace!(
1090 "compute_radial_params: result cx={:.1} cy={:.1} rx={:.1} ry={:.1}",
1091 cx,
1092 cy,
1093 rx,
1094 ry,
1095 );
1096
1097 (cx, cy, rx, ry)
1098}
1099
1100fn color_at_point(cx: f32, cy: f32, rx: f32, ry: f32, px: f32, py: f32) -> f32 {
1101 let dx = px - cx;
1102 let dy = py - cy;
1103 ((dx / rx).powi(2) + (dy / ry).powi(2))
1104 .sqrt()
1105 .clamp(0.0, 1.0)
1106}
1107
1108fn emit_rect_vertices(
1109 vertices: &mut Vec<Vertex>,
1110 x1: f32,
1111 y1: f32,
1112 x2: f32,
1113 y2: f32,
1114 screen_width: f32,
1115 screen_height: f32,
1116 colors: [[f32; 4]; 4],
1117) {
1118 let ndc = |v: f32, max: f32| (v / max) * 2.0 - 1.0;
1119 let px1 = ndc(x1, screen_width);
1120 let py1 = -ndc(y1, screen_height);
1121 let px2 = ndc(x2, screen_width);
1122 let py2 = -ndc(y2, screen_height);
1123
1124 #[rustfmt::skip]
1125 vertices.extend_from_slice(&[
1126 Vertex { position: [px1, py1, 0.0], color: colors[0] },
1127 Vertex { position: [px1, py2, 0.0], color: colors[1] },
1128 Vertex { position: [px2, py1, 0.0], color: colors[2] },
1129 Vertex { position: [px2, py1, 0.0], color: colors[2] },
1130 Vertex { position: [px1, py2, 0.0], color: colors[1] },
1131 Vertex { position: [px2, py2, 0.0], color: colors[3] },
1132 ]);
1133}
1134
1135fn emit_radial_gradient_vertices(
1137 vertices: &mut Vec<Vertex>,
1138 x1: f32,
1139 y1: f32,
1140 x2: f32,
1141 y2: f32,
1142 screen_width: f32,
1143 screen_height: f32,
1144 cx: f32,
1145 cy: f32,
1146 rx: f32,
1147 ry: f32,
1148 stops: &[ColorStop],
1149) {
1150 const SUBDIV: u32 = 32;
1151 let rect_w = x2 - x1;
1152 let rect_h = y2 - y1;
1153 let step_x = rect_w / SUBDIV as f32;
1154 let step_y = rect_h / SUBDIV as f32;
1155
1156 for gy in 0..SUBDIV {
1157 for gx in 0..SUBDIV {
1158 let sx1 = x1 + gx as f32 * step_x;
1159 let sy1 = y1 + gy as f32 * step_y;
1160 let sx2 = sx1 + step_x;
1161 let sy2 = sy1 + step_y;
1162
1163 let t_tl = color_at_point(cx, cy, rx, ry, sx1, sy1);
1164 let t_bl = color_at_point(cx, cy, rx, ry, sx1, sy2);
1165 let t_tr = color_at_point(cx, cy, rx, ry, sx2, sy1);
1166 let t_br = color_at_point(cx, cy, rx, ry, sx2, sy2);
1167
1168 let colors = [
1169 sample_gradient_stops(stops, t_tl).to_linear_f32_array(),
1170 sample_gradient_stops(stops, t_bl).to_linear_f32_array(),
1171 sample_gradient_stops(stops, t_tr).to_linear_f32_array(),
1172 sample_gradient_stops(stops, t_br).to_linear_f32_array(),
1173 ];
1174
1175 emit_rect_vertices(
1176 vertices,
1177 sx1,
1178 sy1,
1179 sx2,
1180 sy2,
1181 screen_width,
1182 screen_height,
1183 colors,
1184 );
1185 }
1186 }
1187}
1188
1189fn sample_gradient_stops(stops: &[ColorStop], t: f32) -> Color {
1191 if stops.is_empty() {
1192 return Color(0, 0, 0, 0);
1193 }
1194 if stops.len() == 1 {
1195 return stops[0].color;
1196 }
1197
1198 let t = t.clamp(0.0, 1.0);
1199 let last = stops.len() - 1;
1200
1201 for i in 0..last {
1202 let pos_i = stops[i]
1203 .position
1204 .unwrap_or(if i == 0 { 0.0 } else { i as f32 / last as f32 });
1205 let pos_j = stops[i + 1].position.unwrap_or(if i + 1 == last {
1206 1.0
1207 } else {
1208 (i + 1) as f32 / last as f32
1209 });
1210
1211 if t >= pos_i && t <= pos_j {
1212 let local = if pos_j > pos_i {
1213 (t - pos_i) / (pos_j - pos_i)
1214 } else {
1215 0.0
1216 };
1217 return lerp_color(stops[i].color, stops[i + 1].color, local);
1218 }
1219 }
1220
1221 if t <= stops[0].position.unwrap_or(0.0) {
1222 return stops[0].color;
1223 }
1224 stops[last].color
1225}
1226
1227fn lerp_color(a: Color, b: Color, t: f32) -> Color {
1228 let al = a.to_linear_f32_array();
1230 let bl = b.to_linear_f32_array();
1231 Color::from_linear_f32_array([
1232 al[0] + (bl[0] - al[0]) * t,
1233 al[1] + (bl[1] - al[1]) * t,
1234 al[2] + (bl[2] - al[2]) * t,
1235 al[3] + (bl[3] - al[3]) * t,
1236 ])
1237}
1238
1239fn select_wgpu_backends() -> wgpu::Backends {
1240 if let Ok(value) = env::var("ORINIUM_WGPU_BACKEND") {
1241 match value.to_lowercase().as_str() {
1242 "gl" | "opengl" => return wgpu::Backends::GL,
1243 "vulkan" | "vk" => return wgpu::Backends::VULKAN,
1244 "metal" => return wgpu::Backends::METAL,
1245 "dx12" | "d3d12" => return wgpu::Backends::DX12,
1246 "primary" => return wgpu::Backends::PRIMARY,
1247 _ => {}
1248 }
1249 }
1250
1251 let is_wsl = env::var_os("WSL_DISTRO_NAME").is_some() || env::var_os("WSL_INTEROP").is_some();
1252 let is_wayland = env::var_os("WAYLAND_DISPLAY").is_some();
1253
1254 if is_wsl && is_wayland {
1255 return wgpu::Backends::GL;
1257 }
1258
1259 wgpu::Backends::PRIMARY
1260}
1261
1262#[cfg(test)]
1263mod tests {
1264 use super::*;
1265 use crate::engine::layouter::types::{
1266 ColorStop, Gradient, GradientKind, RadialShape, RadialSizeKind,
1267 };
1268
1269 #[test]
1272 fn test_ellipse_rx_for_corner_centered() {
1273 let rx = ellipse_rx_for_corner(100.0, 50.0, 200.0, 100.0);
1277 let expected = (20000.0f32).sqrt();
1278 assert!(
1279 (rx - expected).abs() < 0.01,
1280 "rx={} expected={}",
1281 rx,
1282 expected
1283 );
1284 }
1285
1286 #[test]
1287 fn test_ellipse_rx_for_corner_square() {
1288 let rx = ellipse_rx_for_corner(100.0, 100.0, 100.0, 100.0);
1292 let expected = (20000.0f32).sqrt();
1293 assert!(
1294 (rx - expected).abs() < 0.01,
1295 "rx={} expected={}",
1296 rx,
1297 expected
1298 );
1299 }
1300
1301 #[test]
1302 fn test_ellipse_rx_for_corner_zero_box() {
1303 assert_eq!(ellipse_rx_for_corner(50.0, 30.0, 100.0, 0.0), 50.0);
1305 assert_eq!(ellipse_rx_for_corner(50.0, 30.0, 0.0, 100.0), 50.0);
1306 }
1307
1308 #[test]
1311 fn test_color_at_point_center() {
1312 let d = color_at_point(100.0, 100.0, 50.0, 50.0, 100.0, 100.0);
1313 assert!((d - 0.0).abs() < 1e-6, "d={}", d);
1314 }
1315
1316 #[test]
1317 fn test_color_at_point_on_edge() {
1318 let d = color_at_point(100.0, 100.0, 50.0, 50.0, 150.0, 100.0);
1319 assert!((d - 1.0).abs() < 1e-6, "d={}", d);
1320 }
1321
1322 #[test]
1323 fn test_color_at_point_outside() {
1324 let d = color_at_point(100.0, 100.0, 50.0, 50.0, 200.0, 200.0);
1325 assert!((d - 1.0).abs() < 1e-6, "d={}", d);
1326 }
1327
1328 #[test]
1329 fn test_color_at_point_ellipse() {
1330 let d = color_at_point(0.0, 0.0, 100.0, 50.0, 50.0, 25.0);
1333 assert!((d - (0.5f32).sqrt()).abs() < 1e-6, "d={}", d);
1334 }
1335
1336 fn make_radial(shape: RadialShape, size: RadialSizeKind, position: (f32, f32)) -> GradientKind {
1339 GradientKind::Radial {
1340 shape,
1341 size,
1342 position,
1343 }
1344 }
1345
1346 fn corners_from_rect(x: f32, y: f32, w: f32, h: f32) -> [(f32, f32); 4] {
1347 [(x, y), (x, y + h), (x + w, y), (x + w, y + h)]
1348 }
1349
1350 #[test]
1351 fn test_radial_circle_closest_side_centered() {
1352 let kind = make_radial(RadialShape::Circle, RadialSizeKind::ClosestSide, (0.5, 0.5));
1354 let (_cx, _cy, rx, ry) =
1355 compute_radial_params(&kind, &corners_from_rect(0.0, 0.0, 200.0, 100.0));
1356 assert!((rx - 50.0).abs() < 0.01, "rx={}", rx);
1358 assert!((ry - 50.0).abs() < 0.01, "ry={}", ry);
1359 }
1360
1361 #[test]
1362 fn test_radial_circle_farthest_corner_centered() {
1363 let kind = make_radial(
1365 RadialShape::Circle,
1366 RadialSizeKind::FarthestCorner,
1367 (0.5, 0.5),
1368 );
1369 let (_cx, _cy, rx, ry) =
1370 compute_radial_params(&kind, &corners_from_rect(0.0, 0.0, 200.0, 100.0));
1371 let expected = ((100.0f32 * 100.0 + 50.0 * 50.0) as f32).sqrt();
1372 assert!(
1373 (rx - expected).abs() < 0.01,
1374 "rx={} expected={}",
1375 rx,
1376 expected
1377 );
1378 assert!((ry - expected).abs() < 0.01, "ry={}", ry);
1379 }
1380
1381 #[test]
1382 fn test_radial_ellipse_closest_side_centered() {
1383 let kind = make_radial(
1384 RadialShape::Ellipse,
1385 RadialSizeKind::ClosestSide,
1386 (0.5, 0.5),
1387 );
1388 let (_cx, _cy, rx, ry) =
1389 compute_radial_params(&kind, &corners_from_rect(0.0, 0.0, 200.0, 100.0));
1390 assert!((rx - 100.0).abs() < 0.01, "rx={}", rx);
1391 assert!((ry - 50.0).abs() < 0.01, "ry={}", ry);
1392 }
1393
1394 #[test]
1395 fn test_radial_ellipse_farthest_side_centered() {
1396 let kind = make_radial(
1397 RadialShape::Ellipse,
1398 RadialSizeKind::FarthestSide,
1399 (0.5, 0.5),
1400 );
1401 let (_cx, _cy, rx, ry) =
1402 compute_radial_params(&kind, &corners_from_rect(0.0, 0.0, 200.0, 100.0));
1403 assert!((rx - 100.0).abs() < 0.01, "rx={}", rx);
1404 assert!((ry - 50.0).abs() < 0.01, "ry={}", ry);
1405 }
1406
1407 #[test]
1408 fn test_radial_ellipse_closest_corner_centered() {
1409 let kind = make_radial(
1411 RadialShape::Ellipse,
1412 RadialSizeKind::ClosestCorner,
1413 (0.5, 0.5),
1414 );
1415 let (_cx, _cy, rx, ry) =
1416 compute_radial_params(&kind, &corners_from_rect(0.0, 0.0, 200.0, 100.0));
1417 let expected_rx = (20000.0f32).sqrt();
1418 let expected_ry = expected_rx * 100.0 / 200.0;
1419 assert!(
1420 (rx - expected_rx).abs() < 0.01,
1421 "rx={} expected={}",
1422 rx,
1423 expected_rx
1424 );
1425 assert!(
1426 (ry - expected_ry).abs() < 0.01,
1427 "ry={} expected={}",
1428 ry,
1429 expected_ry
1430 );
1431 }
1432
1433 #[test]
1434 fn test_radial_ellipse_farthest_corner_centered() {
1435 let kind = make_radial(
1437 RadialShape::Ellipse,
1438 RadialSizeKind::FarthestCorner,
1439 (0.5, 0.5),
1440 );
1441 let (_cx, _cy, rx, ry) =
1442 compute_radial_params(&kind, &corners_from_rect(0.0, 0.0, 200.0, 100.0));
1443 let expected_rx = (20000.0f32).sqrt();
1444 let expected_ry = expected_rx * 100.0 / 200.0;
1445 assert!(
1446 (rx - expected_rx).abs() < 0.01,
1447 "rx={} expected={}",
1448 rx,
1449 expected_rx
1450 );
1451 assert!(
1452 (ry - expected_ry).abs() < 0.01,
1453 "ry={} expected={}",
1454 ry,
1455 expected_ry
1456 );
1457 }
1458
1459 #[test]
1460 fn test_radial_ellipse_closest_corner_offset() {
1461 let kind = make_radial(
1465 RadialShape::Ellipse,
1466 RadialSizeKind::ClosestCorner,
1467 (0.2, 0.3),
1468 );
1469 let (_cx, _cy, rx, ry) =
1470 compute_radial_params(&kind, &corners_from_rect(0.0, 0.0, 200.0, 100.0));
1471 let expected_rx = (5200.0f32).sqrt();
1472 let expected_ry = expected_rx * 100.0 / 200.0;
1473 assert!(
1474 (rx - expected_rx).abs() < 0.1,
1475 "rx={} expected={}",
1476 rx,
1477 expected_rx
1478 );
1479 assert!(
1480 (ry - expected_ry).abs() < 0.1,
1481 "ry={} expected={}",
1482 ry,
1483 expected_ry
1484 );
1485 }
1486
1487 #[test]
1488 fn test_radial_ellipse_farthest_corner_offset() {
1489 let kind = make_radial(
1493 RadialShape::Ellipse,
1494 RadialSizeKind::FarthestCorner,
1495 (0.2, 0.3),
1496 );
1497 let (_cx, _cy, rx, ry) =
1498 compute_radial_params(&kind, &corners_from_rect(0.0, 0.0, 200.0, 100.0));
1499 let expected_rx = (45200.0f32).sqrt();
1500 let expected_ry = expected_rx * 100.0 / 200.0;
1501 assert!(
1502 (rx - expected_rx).abs() < 0.1,
1503 "rx={} expected={}",
1504 rx,
1505 expected_rx
1506 );
1507 assert!(
1508 (ry - expected_ry).abs() < 0.1,
1509 "ry={} expected={}",
1510 ry,
1511 expected_ry
1512 );
1513 }
1514
1515 #[test]
1518 fn test_lerp_color_srgb_vs_linear() {
1519 let red = Color(255, 0, 0, 255);
1522 let blue = Color(0, 0, 255, 255);
1523 let mixed = lerp_color(red, blue, 0.5);
1524 assert!(
1527 mixed.0 > 128,
1528 "R should be >128 in linear lerp, got {}",
1529 mixed.0
1530 );
1531 assert!(
1532 mixed.2 > 128,
1533 "B should be >128 in linear lerp, got {}",
1534 mixed.2
1535 );
1536 }
1537
1538 #[test]
1539 fn test_sample_gradient_single_stop() {
1540 let stops = vec![ColorStop {
1541 color: Color(255, 0, 0, 255),
1542 position: None,
1543 }];
1544 let c = sample_gradient_stops(&stops, 0.5);
1545 assert_eq!(c, Color(255, 0, 0, 255));
1546 }
1547
1548 #[test]
1549 fn test_sample_gradient_two_stops() {
1550 let stops = vec![
1551 ColorStop {
1552 color: Color(255, 0, 0, 255),
1553 position: None,
1554 },
1555 ColorStop {
1556 color: Color(0, 0, 255, 255),
1557 position: None,
1558 },
1559 ];
1560 let c0 = sample_gradient_stops(&stops, 0.0);
1561 assert_eq!(c0, Color(255, 0, 0, 255));
1562 let c1 = sample_gradient_stops(&stops, 1.0);
1563 assert_eq!(c1, Color(0, 0, 255, 255));
1564 let mid = sample_gradient_stops(&stops, 0.5);
1566 assert!(mid.0 > 0 && mid.0 < 255);
1567 assert!(mid.2 > 0 && mid.2 < 255);
1568 }
1569
1570 #[test]
1573 fn test_linear_gradient_extent_clipped() {
1574 let gradient = Gradient {
1575 kind: GradientKind::Linear { angle: 90.0 }, stops: vec![
1577 ColorStop {
1578 color: Color(255, 0, 0, 255),
1579 position: None,
1580 },
1581 ColorStop {
1582 color: Color(0, 0, 255, 255),
1583 position: None,
1584 },
1585 ],
1586 };
1587 let full = [(0.0, 0.0), (0.0, 100.0), (200.0, 0.0), (200.0, 100.0)];
1589 let visible = [(120.0, 0.0), (120.0, 100.0), (200.0, 0.0), (200.0, 100.0)];
1591
1592 let colors = compute_gradient_corner_colors_extent(&gradient, &full, &visible);
1593 for (i, c) in colors.iter().enumerate() {
1594 assert!(
1595 c.2 > c.0,
1596 "Corner {} should be more blue (got r={}, b={})",
1597 i,
1598 c.0,
1599 c.2
1600 );
1601 }
1602 assert!(colors[0].0 > colors[2].0, "TL should have more red than TR");
1604 }
1605
1606 #[test]
1607 fn test_linear_gradient_extent_unclipped() {
1608 let gradient = Gradient {
1609 kind: GradientKind::Linear { angle: 90.0 },
1610 stops: vec![
1611 ColorStop {
1612 color: Color(255, 0, 0, 255),
1613 position: None,
1614 },
1615 ColorStop {
1616 color: Color(0, 0, 255, 255),
1617 position: None,
1618 },
1619 ],
1620 };
1621 let corners = [(0.0, 0.0), (0.0, 100.0), (200.0, 0.0), (200.0, 100.0)];
1622 let colors = compute_gradient_corner_colors_extent(&gradient, &corners, &corners);
1623 assert_eq!(colors[0], Color(255, 0, 0, 255)); assert_eq!(colors[1], Color(255, 0, 0, 255)); assert_eq!(colors[2], Color(0, 0, 255, 255)); assert_eq!(colors[3], Color(0, 0, 255, 255)); }
1629}