Skip to main content

orinium_browser/platform/renderer/
gpu.rs

1//! wgpuを使用してGPUで描画するためのコンテキストと処理を提供するモジュール
2
3use 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
15/// GPU描画コンテキスト
16pub struct GpuRenderer {
17    /// GPUの描画対象
18    surface: wgpu::Surface<'static>,
19    /// GPUの論理デバイス
20    device: wgpu::Device,
21    /// コマンド送信用キュー
22    queue: wgpu::Queue,
23    /// サーフェス設定、解像度・フォーマットなどのフレームバッファ設定
24    config: wgpu::SurfaceConfiguration,
25    /// WindowSize
26    size: winit::dpi::PhysicalSize<u32>,
27    /// ディスプレイ倍率
28    scale_factor: f64,
29    /// RenderPipelin(頂点 to ピクセル)
30    render_pipeline: wgpu::RenderPipeline,
31    /// 頂点バッファ
32    vertex_buffer: Option<wgpu::Buffer>,
33    /// 頂点
34    vertices: Vec<Vertex>,
35    /// 頂点数
36    num_vertices: u32,
37
38    /// テキスト描画用ラッパー
39    text_renderer: Option<TextRenderer>,
40
41    /// テキストカリングを有効にする
42    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    /// 新しいGPUレンダラーを作成
75    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        // GPUドライバとの通信インスタンス
80        // wgpuインスタンスの作成
81        //
82        // [`InstanceDescriptor::new_with_out_display_hundler`] の実装を参考に
83        // backends 選択は [`select_wgpu_backends`] を使った実装。
84        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        // OSウィンドウとGPUの描画対象(サーフェス)を関連付ける
93        // サーフェスの作成
94        let surface = instance.create_surface(Arc::clone(&window))?;
95
96        // 利用可能なGPU(物理デバイス)アダプターの取得
97        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                // This is currently only used for the browser's rendering backend, but we
103                // enable limit bucketing preemptively in case WebGPU is exposed to web content
104                // in the future.
105                apply_limit_buckets: true,
106            })
107            .await?;
108
109        // デバイスとキューの作成
110        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        // サーフェス設定
122        // フレームバッファ設定(解像度・フォーマットなど)
123        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            // Use automatic color space selection until browser-level color
135            // management and CSS color spaces are implemented.
136            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        // シェーダーの読み込み
147        // シェーダーモジュールの作成
148        // vertex/fragment for main pipeline
149        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        // --- レンダーパイプライン(頂点→ピクセル変換のルール)の作成 ---
155        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, // 三角扇がカリングで消えちゃう...
187                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        // --- レンダーパイプライン作成終了 ---
200
201        // テキスト描画用ラッパーの初期化。引数で渡されたフォントパスがあればそれを優先して読み込む。
202        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        // Enable text culling by default, allow override by env var
229        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    /// ウィンドウサイズが変更された時の処理
248    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    /// 描画命令を解析して頂点バッファやテキストキューに登録
274    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        // --- 頂点データ ---
279        let mut vertices = Vec::new();
280        // --- Text ---
281        let mut sections: Vec<TextSection> = Vec::new();
282        // --- scale_factor ---
283        let sf = self.scale_factor as f32;
284        // --- transform stack ---
285        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        // --- clip stack ---
296        #[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                // Transform (Push / Pop)
314                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                // Clip (Push / Pop)
324                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                    // 現在の clip との AND を取る
339                    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                // Rectangle
361                DrawCommand::DrawRect {
362                    x,
363                    y,
364                    width: w,
365                    height: h,
366                    color,
367                } => {
368                    // transform
369                    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                    // clip 取得
376                    let clip = current_clip(&clip_stack);
377
378                    // 完全に外なら skip
379                    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                    // 部分クリップ
388                    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                    // NDC
394                    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                // Gradient Rectangle
416                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),         // TL
446                        ((x + tdx) * sf, (y + tdy + h) * sf),     // BL
447                        ((x + tdx + w) * sf, (y + tdy) * sf),     // TR
448                        ((x + tdx + w) * sf, (y + tdy + h) * sf), // BR
449                    ];
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                // Text
499                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                    // Text culling: if enabled and the text's bounding box is fully outside current clip, skip creating buffer
510                    let mut skip_text = false;
511                    if self.enable_text_culling {
512                        // compute screen-space bbox
513                        let sx1 = (x + tdx) * sf;
514                        let sy1 = (y + tdy) * sf;
515                        // if width/height are zero or NaN, estimate from font size and line count
516                        let est_w = if !tw.is_finite() || tw <= 0.0 {
517                            // fall back: estimate width as font_size * 10.0 * approximate_chars
518                            (*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                            // estimate height as font_size * 1.2 * lines
524                            (*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                    // Use TextRenderer helper to create a Buffer with correct FontSystem handling
546                    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                        // No text renderer available; skip
559                        continue;
560                    };
561                    sections.push(section);
562                }
563
564                // Polygon
565                DrawCommand::DrawPolygon { points, color } => {
566                    // transform
567                    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                    // clip 取得
574                    let clip = current_clip(&clip_stack);
575                    // clip in scaled (screen) coords
576                    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                    // Quick reject by bounding box
582                    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                        // fully outside
594                        continue;
595                    }
596
597                    // Helper: Sutherland–Hodgman polygon clipping against an axis-aligned edge
598                    let clip_against_edge = |poly: &Vec<(f32, f32)>, edge: u8| -> Vec<(f32, f32)> {
599                        // edge: 0=left,1=right,2=top,3=bottom
600                        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                            // inside test
609                            let inside = |x: f32, y: f32| -> bool {
610                                match edge {
611                                    0 => x >= clip_l, // left
612                                    1 => x <= clip_r, // right
613                                    2 => y >= clip_t, // top
614                                    3 => y <= clip_b, // bottom
615                                    _ => 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                                // both inside
623                                out.push((ex, ey));
624                            } else if s_in && !e_in {
625                                // going out: add intersection
626                                // compute intersection between segment and clipping line
627                                let (ix, iy) = match edge {
628                                    0 | 1 => {
629                                        // vertical line x = clip_l or clip_r
630                                        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                                        // horizontal line y = clip_t or clip_b
641                                        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                                // entering: add intersection then end point
655                                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                                // both outside: do nothing
682                            }
683                        }
684                        out
685                    };
686
687                    // Triangulate polygon into fan triangles from vertex 0, clip each triangle, and push resulting triangles
688                    if transformed_points.len() < 3 {
689                        continue;
690                    }
691
692                    // NDC helper
693                    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                        // clip triangle against rect using Sutherland–Hodgman (4 edges)
701                        let mut poly = tri;
702                        poly = clip_against_edge(&poly, 0); // left
703                        if poly.is_empty() {
704                            continue;
705                        }
706                        poly = clip_against_edge(&poly, 1); // right
707                        if poly.is_empty() {
708                            continue;
709                        }
710                        poly = clip_against_edge(&poly, 2); // top
711                        if poly.is_empty() {
712                            continue;
713                        }
714                        poly = clip_against_edge(&poly, 3); // bottom
715                        if poly.is_empty() {
716                            continue;
717                        }
718
719                        // triangulate resulting polygon as fan
720                        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                // Ellipse
749                #[allow(unused)]
750                DrawCommand::DrawEllipse {
751                    center,
752                    radius_x,
753                    radius_y,
754                    color,
755                } => {
756                    // transform
757                    let (tdx, tdy) = current_transform(&transform_stack);
758                    let cx = center.0 + tdx;
759                    let cy = center.1 + tdy;
760
761                    // clip 取得
762                    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        // テキストセクションをキューに追加
772        if let Some(tr) = &mut self.text_renderer {
773            tr.queue(&self.device, &self.queue, &sections).unwrap();
774        }
775    }
776
777    /// フレームを描画
778    ///
779    /// TODO:
780    /// [`wgpu::CurrentSurfaceTexture`] をよりよく処理する必要があります。
781    /// 現在は、 Success 時以外の結果を無視し、Errorにまとめて返す挙動をします。
782    pub fn render(&mut self) -> Result<()> {
783        // 描画するフレームバッファを取得
784        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        // アニメーション中はテキストブラシが更新位置を反映できるようにセクションを再キューする必要がある
799        // 補足: 呼び出し元(UI層)も各フレームで描画コマンドを再キューしているため、ここではアニメーション状態を返り値で通知するだけ
800
801        // GPUコマンドのエンコーダーの作成
802        let mut encoder = self
803            .device
804            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
805                label: Some("Render Encoder"),
806            });
807
808        // 描画パスの開始
809        {
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                        // 背景色をクリア
817                        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            // 使用するシェーダー・設定をセット
834            render_pass.set_pipeline(&self.render_pipeline);
835            // 頂点バッファをセットして描画
836            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        // テキストをレンダリング
843        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        // コマンドをGPUに送信
864        self.queue.submit(std::iter::once(encoder.finish()));
865
866        // フレームを画面に表示
867        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            // old NDC -> logical
886            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            // logical -> new NDC
890            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        // 頂点バッファを登録
898        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
916/// Compute the 4 corner colors for a linear gradient rectangle.
917///
918/// `extent_corners` define the full gradient extent (min/max projection).
919/// `sample_corners` are the actual corners to compute colors for (usually the clipped rect).
920/// corners layout: [TL, BL, TR, BR] in physical (pre-NDC) screen coordinates.
921fn 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    // Compute gradient extent from the full (unclipped) rectangle
934    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    // Sample the gradient at each of the visible (clipped) corners
946    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
959/// Returns the required ellipse radius `rx` (in the x direction) such that
960/// an ellipse centered with aspect ratio `w/h` passes through a point at
961/// offset `(dx, dy)` from its center.
962///
963/// The derivation:
964///   (dx/rx)² + (dy/ry)² = 1   and   ry = rx * h/w
965///   ⇒  rx² = dx² + dy² * w²/h²
966fn 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
1135/// Subdivide a radial gradient rectangle into an NxN grid for proper rendering.
1136fn 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
1189/// Sample a gradient's color stops at normalized position `t` (0.0–1.0).
1190fn 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    // Interpolate in linear RGB space for correct color mixing
1229    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        // WSLg + Wayland can be unstable with Vulkan; prefer GL by default.
1256        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    // ── ellipse_rx_for_corner ──────────────────────────────────────────
1270
1271    #[test]
1272    fn test_ellipse_rx_for_corner_centered() {
1273        // Box 200x100, center (100,50), corner (200,100)
1274        // dx=100, dy=50, w=200, h=100
1275        // rx = sqrt(100^2 + 50^2 * (200/100)^2) = sqrt(10000 + 2500*4) = sqrt(20000) ≈ 141.42
1276        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        // Box 100x100, center (0,0), corner (100,100)
1289        // dx=100, dy=100, w=100, h=100
1290        // rx = sqrt(100^2 + 100^2 * (100/100)^2) = sqrt(20000) ≈ 141.42
1291        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        // Degenerate box: h=0
1304        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    // ── color_at_point ─────────────────────────────────────────────────
1309
1310    #[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        // Point (50, 25) on ellipse rx=100, ry=50
1331        // distance = sqrt((50/100)^2 + (25/50)^2) = sqrt(0.25+0.25) = sqrt(0.5) ≈ 0.707
1332        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    // ── compute_radial_params ──────────────────────────────────────────
1337
1338    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        // 200x100 box, center (100,50)
1353        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        // closest side = min(100, 100, 50, 50) = 50
1357        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        // 200x100 box, center (100,50). Distance to any corner = sqrt(100^2+50^2) ≈ 111.80
1364        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        // All corners equidistant from center: rx = sqrt(100^2 + 50^2 * (200/100)^2) = sqrt(20000) ≈ 141.42
1410        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        // Same as closest-corner for centered position (all corners equidistant)
1436        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        // Box 200x100, position (0.2, 0.3) → center at (40, 30)
1462        // Closest corner = TL (0,0): dx=40, dy=30
1463        // rx = sqrt(40^2 + 30^2 * (200/100)^2) = sqrt(1600 + 900*4) = sqrt(5200) ≈ 72.11
1464        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        // Box 200x100, position (0.2, 0.3) → center at (40, 30)
1490        // Farthest corner = BR (200,100): dx=160, dy=70
1491        // rx = sqrt(160^2 + 70^2 * (200/100)^2) = sqrt(25600 + 4900*4) = sqrt(45200) ≈ 212.60
1492        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    // ── sample_gradient_stops / lerp_color ─────────────────────────────
1516
1517    #[test]
1518    fn test_lerp_color_srgb_vs_linear() {
1519        // Interpolation between red and blue at t=0.5 should differ
1520        // between sRGB and linear space.
1521        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        // In sRGB space: (128, 0, 128)
1525        // In linear space: ~ (188, 0, 188)  (perceptually mid-way)
1526        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        // Middle should be a linear-space blend of red and blue
1565        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    // ── compute_gradient_corner_colors_extent ──────────────────────────
1571
1572    #[test]
1573    fn test_linear_gradient_extent_clipped() {
1574        let gradient = Gradient {
1575            kind: GradientKind::Linear { angle: 90.0 }, // left→right
1576            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        // Full rect: x=0, y=0, w=200, h=100
1588        let full = [(0.0, 0.0), (0.0, 100.0), (200.0, 0.0), (200.0, 100.0)];
1589        // Visible rect: right portion x=120..200 (t=0.6..1.0), all corners should be more blue than red
1590        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        // TL/BL should be at t=0.6 (more red than TR/BR at t=1.0)
1603        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        // TL/BL should be red, TR/BR should be blue
1624        assert_eq!(colors[0], Color(255, 0, 0, 255)); // TL
1625        assert_eq!(colors[1], Color(255, 0, 0, 255)); // BL
1626        assert_eq!(colors[2], Color(0, 0, 255, 255)); // TR
1627        assert_eq!(colors[3], Color(0, 0, 255, 255)); // BR
1628    }
1629}