par_term/
cell_renderer.rs

1use anyhow::{Context, Result};
2use std::collections::HashMap;
3use std::sync::Arc;
4use wgpu::util::DeviceExt;
5use winit::window::Window;
6
7use crate::font_manager::FontManager;
8use crate::scrollbar::Scrollbar;
9use crate::text_shaper::ShapingOptions;
10
11/// Vertex for cell rendering
12#[repr(C)]
13#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
14struct Vertex {
15    position: [f32; 2],
16    tex_coords: [f32; 2],
17}
18
19/// Instance data for background rendering
20#[repr(C)]
21#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
22struct BackgroundInstance {
23    position: [f32; 2],
24    size: [f32; 2],
25    color: [f32; 4],
26}
27
28/// Instance data for text rendering
29#[repr(C)]
30#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
31struct TextInstance {
32    position: [f32; 2],
33    size: [f32; 2],
34    tex_offset: [f32; 2],
35    tex_size: [f32; 2],
36    color: [f32; 4],
37    is_colored: u32, // 1 for emoji/colored glyphs, 0 for regular text
38}
39
40/// A single terminal cell
41#[derive(Clone, Debug, PartialEq)]
42pub struct Cell {
43    pub grapheme: String,
44    pub fg_color: [u8; 4],
45    pub bg_color: [u8; 4],
46    pub bold: bool,
47    pub italic: bool,
48    pub underline: bool,
49    pub strikethrough: bool,
50    pub hyperlink_id: Option<u32>,
51    pub wide_char: bool,
52    pub wide_char_spacer: bool,
53}
54
55impl Default for Cell {
56    fn default() -> Self {
57        Self {
58            grapheme: " ".to_string(),
59            fg_color: [255, 255, 255, 255],
60            bg_color: [0, 0, 0, 0],
61            bold: false,
62            italic: false,
63            underline: false,
64            strikethrough: false,
65            hyperlink_id: None,
66            wide_char: false,
67            wide_char_spacer: false,
68        }
69    }
70}
71
72/// Glyph info for atlas
73#[derive(Clone, Debug)]
74struct GlyphInfo {
75    #[allow(dead_code)]
76    key: u64,
77    x: u32,
78    y: u32,
79    width: u32,
80    height: u32,
81    #[allow(dead_code)]
82    bearing_x: f32,
83    #[allow(dead_code)]
84    bearing_y: f32,
85    is_colored: bool,
86    prev: Option<u64>,
87    next: Option<u64>,
88}
89
90/// Row cache entry
91struct RowCacheEntry {}
92
93pub struct CellRenderer {
94    device: Arc<wgpu::Device>,
95    queue: Arc<wgpu::Queue>,
96    surface: wgpu::Surface<'static>,
97    config: wgpu::SurfaceConfiguration,
98
99    // Pipelines
100    bg_pipeline: wgpu::RenderPipeline,
101    text_pipeline: wgpu::RenderPipeline,
102    bg_image_pipeline: wgpu::RenderPipeline,
103    #[allow(dead_code)]
104    visual_bell_pipeline: wgpu::RenderPipeline,
105
106    // Buffers
107    vertex_buffer: wgpu::Buffer,
108    bg_instance_buffer: wgpu::Buffer,
109    text_instance_buffer: wgpu::Buffer,
110    bg_image_uniform_buffer: wgpu::Buffer,
111    #[allow(dead_code)]
112    visual_bell_uniform_buffer: wgpu::Buffer,
113
114    // Bind groups
115    text_bind_group: wgpu::BindGroup,
116    #[allow(dead_code)]
117    text_bind_group_layout: wgpu::BindGroupLayout,
118    bg_image_bind_group: Option<wgpu::BindGroup>,
119    bg_image_bind_group_layout: wgpu::BindGroupLayout,
120    #[allow(dead_code)]
121    visual_bell_bind_group: wgpu::BindGroup,
122
123    // Glyph atlas
124    atlas_texture: wgpu::Texture,
125    #[allow(dead_code)]
126    atlas_view: wgpu::TextureView,
127    glyph_cache: HashMap<u64, GlyphInfo>,
128    lru_head: Option<u64>,
129    lru_tail: Option<u64>,
130    atlas_next_x: u32,
131    atlas_next_y: u32,
132    atlas_row_height: u32,
133
134    // Grid state
135    cols: usize,
136    rows: usize,
137    cell_width: f32,
138    cell_height: f32,
139    window_padding: f32,
140    #[allow(dead_code)]
141    scale_factor: f32,
142
143    // Components
144    font_manager: FontManager,
145    scrollbar: Scrollbar,
146
147    // Dynamic state
148    cells: Vec<Cell>,
149    dirty_rows: Vec<bool>,
150    row_cache: Vec<Option<RowCacheEntry>>,
151    cursor_pos: (usize, usize),
152    cursor_opacity: f32,
153    cursor_style: par_term_emu_core_rust::cursor::CursorStyle,
154    /// Separate cursor instance for beam/underline styles (rendered as overlay)
155    cursor_overlay: Option<BackgroundInstance>,
156    /// Cursor color [R, G, B] as floats (0.0-1.0)
157    cursor_color: [f32; 3],
158    visual_bell_intensity: f32,
159    window_opacity: f32,
160    background_color: [f32; 4],
161
162    // Metrics
163    font_ascent: f32,
164    font_descent: f32,
165    font_leading: f32,
166    font_size_pixels: f32,
167
168    // Background image
169    bg_image_texture: Option<wgpu::Texture>,
170    bg_image_mode: crate::config::BackgroundImageMode,
171    bg_image_opacity: f32,
172
173    // Metrics
174    max_bg_instances: usize,
175    max_text_instances: usize,
176
177    // CPU-side instance buffers for incremental updates
178    bg_instances: Vec<BackgroundInstance>,
179    text_instances: Vec<TextInstance>,
180
181    // Shaping options
182    #[allow(dead_code)]
183    enable_text_shaping: bool,
184    enable_ligatures: bool,
185    enable_kerning: bool,
186}
187
188impl CellRenderer {
189    #[allow(clippy::too_many_arguments)]
190    pub async fn new(
191        window: Arc<Window>,
192        font_family: Option<&str>,
193        font_family_bold: Option<&str>,
194        font_family_italic: Option<&str>,
195        font_family_bold_italic: Option<&str>,
196        font_ranges: &[crate::config::FontRange],
197        font_size: f32,
198        cols: usize,
199        rows: usize,
200        window_padding: f32,
201        line_spacing: f32,
202        char_spacing: f32,
203        scrollbar_position: &str,
204        scrollbar_width: f32,
205        scrollbar_thumb_color: [f32; 4],
206        scrollbar_track_color: [f32; 4],
207        enable_text_shaping: bool,
208        enable_ligatures: bool,
209        enable_kerning: bool,
210        vsync_mode: crate::config::VsyncMode,
211        window_opacity: f32,
212        background_color: [u8; 3],
213        background_image_path: Option<&str>,
214        background_image_mode: crate::config::BackgroundImageMode,
215        background_image_opacity: f32,
216    ) -> Result<Self> {
217        let instance = wgpu::Instance::default();
218        let surface = instance.create_surface(window.clone())?;
219        let adapter = instance
220            .request_adapter(&wgpu::RequestAdapterOptions {
221                power_preference: wgpu::PowerPreference::HighPerformance,
222                compatible_surface: Some(&surface),
223                force_fallback_adapter: false,
224            })
225            .await
226            .context("Failed to find wgpu adapter")?;
227
228        let (device, queue) = adapter
229            .request_device(&wgpu::DeviceDescriptor {
230                label: Some("device"),
231                required_features: wgpu::Features::empty(),
232                required_limits: wgpu::Limits::default(),
233                memory_hints: wgpu::MemoryHints::default(),
234                ..Default::default()
235            })
236            .await?;
237
238        let device = Arc::new(device);
239        let queue = Arc::new(queue);
240
241        let size = window.inner_size();
242        let surface_caps = surface.get_capabilities(&adapter);
243        let surface_format = surface_caps
244            .formats
245            .iter()
246            .copied()
247            .find(|f| !f.is_srgb())
248            .unwrap_or(surface_caps.formats[0]);
249
250        let config = wgpu::SurfaceConfiguration {
251            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
252            format: surface_format,
253            width: size.width.max(1),
254            height: size.height.max(1),
255            present_mode: vsync_mode.to_present_mode(),
256            alpha_mode: surface_caps.alpha_modes[0],
257            view_formats: vec![],
258            desired_maximum_frame_latency: 2,
259        };
260        surface.configure(&device, &config);
261
262        let scale_factor = window.scale_factor() as f32;
263
264        // Standard DPI for the platform
265        // macOS typically uses 72 DPI for points, Windows and most Linux use 96 DPI
266        let platform_dpi = if cfg!(target_os = "macos") {
267            72.0
268        } else {
269            96.0
270        };
271
272        let base_font_pixels = font_size * platform_dpi / 72.0;
273        let font_size_pixels = (base_font_pixels * scale_factor).max(1.0);
274
275        let font_manager = FontManager::new(
276            font_family,
277            font_family_bold,
278            font_family_italic,
279            font_family_bold_italic,
280            font_ranges,
281        )?;
282
283        // Extract font metrics for better baseline alignment
284        let (font_ascent, font_descent, font_leading, char_advance) = {
285            let primary_font = font_manager.get_font(0).unwrap();
286            let metrics = primary_font.metrics(&[]);
287            let scale = font_size_pixels / metrics.units_per_em as f32;
288
289            // Get advance width of a standard character ('m' is common for monospace width)
290            let glyph_id = primary_font.charmap().map('m');
291            let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
292
293            (
294                metrics.ascent * scale,
295                metrics.descent * scale,
296                metrics.leading * scale,
297                advance,
298            )
299        };
300
301        // Use font metrics for cell height if line_spacing is 1.0
302        // Natural line height = ascent + descent + leading
303        let natural_line_height = font_ascent + font_descent + font_leading;
304        let cell_height = (natural_line_height * line_spacing).max(1.0);
305        let cell_width = (char_advance * char_spacing).max(1.0);
306
307        let scrollbar = Scrollbar::new(
308            &device,
309            surface_format,
310            scrollbar_width,
311            scrollbar_position,
312            scrollbar_thumb_color,
313            scrollbar_track_color,
314        );
315
316        // Shaders
317        let bg_shader = device.create_shader_module(wgpu::include_wgsl!("shaders/cell_bg.wgsl"));
318        let text_shader =
319            device.create_shader_module(wgpu::include_wgsl!("shaders/cell_text.wgsl"));
320        let bg_image_shader =
321            device.create_shader_module(wgpu::include_wgsl!("shaders/background_image.wgsl"));
322
323        // Vertex buffer
324        let vertices = [
325            Vertex {
326                position: [0.0, 0.0],
327                tex_coords: [0.0, 0.0],
328            },
329            Vertex {
330                position: [1.0, 0.0],
331                tex_coords: [1.0, 0.0],
332            },
333            Vertex {
334                position: [0.0, 1.0],
335                tex_coords: [0.0, 1.0],
336            },
337            Vertex {
338                position: [1.0, 1.0],
339                tex_coords: [1.0, 1.0],
340            },
341        ];
342        let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
343            label: Some("vertex buffer"),
344            contents: bytemuck::cast_slice(&vertices),
345            usage: wgpu::BufferUsages::VERTEX,
346        });
347
348        // Background pipeline
349        let bg_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
350            label: Some("bg pipeline layout"),
351            bind_group_layouts: &[],
352            push_constant_ranges: &[],
353        });
354
355        let bg_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
356            label: Some("bg pipeline"),
357            layout: Some(&bg_pipeline_layout),
358            vertex: wgpu::VertexState {
359                module: &bg_shader,
360                entry_point: Some("vs_main"),
361                compilation_options: Default::default(),
362                buffers: &[
363                    wgpu::VertexBufferLayout {
364                        array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
365                        step_mode: wgpu::VertexStepMode::Vertex,
366                        attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2],
367                    },
368                    wgpu::VertexBufferLayout {
369                        array_stride: std::mem::size_of::<BackgroundInstance>() as wgpu::BufferAddress,
370                        step_mode: wgpu::VertexStepMode::Instance,
371                        attributes: &wgpu::vertex_attr_array![2 => Float32x2, 3 => Float32x2, 4 => Float32x4],
372                    },
373                ],
374            },
375            fragment: Some(wgpu::FragmentState {
376                module: &bg_shader,
377                entry_point: Some("fs_main"),
378                compilation_options: Default::default(),
379                targets: &[Some(wgpu::ColorTargetState {
380                    format: surface_format,
381                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
382                    write_mask: wgpu::ColorWrites::ALL,
383                })],
384            }),
385            primitive: wgpu::PrimitiveState {
386                topology: wgpu::PrimitiveTopology::TriangleStrip,
387                ..Default::default()
388            },
389            depth_stencil: None,
390            multisample: wgpu::MultisampleState::default(),
391            multiview: None,
392            cache: None,
393        });
394
395        // Atlas
396        let atlas_size = 2048;
397        let atlas_texture = device.create_texture(&wgpu::TextureDescriptor {
398            label: Some("atlas texture"),
399            size: wgpu::Extent3d {
400                width: atlas_size,
401                height: atlas_size,
402                depth_or_array_layers: 1,
403            },
404            mip_level_count: 1,
405            sample_count: 1,
406            dimension: wgpu::TextureDimension::D2,
407            format: wgpu::TextureFormat::Rgba8Unorm,
408            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
409            view_formats: &[],
410        });
411        let atlas_view = atlas_texture.create_view(&wgpu::TextureViewDescriptor::default());
412        let atlas_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
413            address_mode_u: wgpu::AddressMode::ClampToEdge,
414            address_mode_v: wgpu::AddressMode::ClampToEdge,
415            mag_filter: wgpu::FilterMode::Linear,
416            min_filter: wgpu::FilterMode::Linear,
417            ..Default::default()
418        });
419
420        let text_bind_group_layout =
421            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
422                label: Some("text bind group layout"),
423                entries: &[
424                    wgpu::BindGroupLayoutEntry {
425                        binding: 0,
426                        visibility: wgpu::ShaderStages::FRAGMENT,
427                        ty: wgpu::BindingType::Texture {
428                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
429                            view_dimension: wgpu::TextureViewDimension::D2,
430                            multisampled: false,
431                        },
432                        count: None,
433                    },
434                    wgpu::BindGroupLayoutEntry {
435                        binding: 1,
436                        visibility: wgpu::ShaderStages::FRAGMENT,
437                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
438                        count: None,
439                    },
440                ],
441            });
442
443        let text_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
444            label: Some("text bind group"),
445            layout: &text_bind_group_layout,
446            entries: &[
447                wgpu::BindGroupEntry {
448                    binding: 0,
449                    resource: wgpu::BindingResource::TextureView(&atlas_view),
450                },
451                wgpu::BindGroupEntry {
452                    binding: 1,
453                    resource: wgpu::BindingResource::Sampler(&atlas_sampler),
454                },
455            ],
456        });
457
458        let text_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
459            label: Some("text pipeline layout"),
460            bind_group_layouts: &[&text_bind_group_layout],
461            push_constant_ranges: &[],
462        });
463
464        let text_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
465            label: Some("text pipeline"),
466            layout: Some(&text_pipeline_layout),
467            vertex: wgpu::VertexState {
468                module: &text_shader,
469                entry_point: Some("vs_main"),
470                compilation_options: Default::default(),
471                buffers: &[
472                    wgpu::VertexBufferLayout {
473                        array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
474                        step_mode: wgpu::VertexStepMode::Vertex,
475                        attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2],
476                    },
477                    wgpu::VertexBufferLayout {
478                        array_stride: std::mem::size_of::<TextInstance>() as wgpu::BufferAddress,
479                        step_mode: wgpu::VertexStepMode::Instance,
480                        attributes: &wgpu::vertex_attr_array![
481                            2 => Float32x2,
482                            3 => Float32x2,
483                            4 => Float32x2,
484                            5 => Float32x2,
485                            6 => Float32x4,
486                            7 => Uint32
487                        ],
488                    },
489                ],
490            },
491            fragment: Some(wgpu::FragmentState {
492                module: &text_shader,
493                entry_point: Some("fs_main"),
494                compilation_options: Default::default(),
495                targets: &[Some(wgpu::ColorTargetState {
496                    format: surface_format,
497                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
498                    write_mask: wgpu::ColorWrites::ALL,
499                })],
500            }),
501            primitive: wgpu::PrimitiveState {
502                topology: wgpu::PrimitiveTopology::TriangleStrip,
503                ..Default::default()
504            },
505            depth_stencil: None,
506            multisample: wgpu::MultisampleState::default(),
507            multiview: None,
508            cache: None,
509        });
510
511        // Background image
512        let bg_image_bind_group_layout =
513            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
514                label: Some("bg image bind group layout"),
515                entries: &[
516                    wgpu::BindGroupLayoutEntry {
517                        binding: 0,
518                        visibility: wgpu::ShaderStages::FRAGMENT,
519                        ty: wgpu::BindingType::Texture {
520                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
521                            view_dimension: wgpu::TextureViewDimension::D2,
522                            multisampled: false,
523                        },
524                        count: None,
525                    },
526                    wgpu::BindGroupLayoutEntry {
527                        binding: 1,
528                        visibility: wgpu::ShaderStages::FRAGMENT,
529                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
530                        count: None,
531                    },
532                    wgpu::BindGroupLayoutEntry {
533                        binding: 2,
534                        visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
535                        ty: wgpu::BindingType::Buffer {
536                            ty: wgpu::BufferBindingType::Uniform,
537                            has_dynamic_offset: false,
538                            min_binding_size: None,
539                        },
540                        count: None,
541                    },
542                ],
543            });
544
545        let bg_image_pipeline_layout =
546            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
547                label: Some("bg image pipeline layout"),
548                bind_group_layouts: &[&bg_image_bind_group_layout],
549                push_constant_ranges: &[],
550            });
551
552        let bg_image_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
553            label: Some("bg image pipeline"),
554            layout: Some(&bg_image_pipeline_layout),
555            vertex: wgpu::VertexState {
556                module: &bg_image_shader,
557                entry_point: Some("vs_main"),
558                compilation_options: Default::default(),
559                buffers: &[wgpu::VertexBufferLayout {
560                    array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
561                    step_mode: wgpu::VertexStepMode::Vertex,
562                    attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2],
563                }],
564            },
565            fragment: Some(wgpu::FragmentState {
566                module: &bg_image_shader,
567                entry_point: Some("fs_main"),
568                compilation_options: Default::default(),
569                targets: &[Some(wgpu::ColorTargetState {
570                    format: surface_format,
571                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
572                    write_mask: wgpu::ColorWrites::ALL,
573                })],
574            }),
575            primitive: wgpu::PrimitiveState {
576                topology: wgpu::PrimitiveTopology::TriangleStrip,
577                ..Default::default()
578            },
579            depth_stencil: None,
580            multisample: wgpu::MultisampleState::default(),
581            multiview: None,
582            cache: None,
583        });
584
585        let bg_image_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
586            label: Some("bg image uniform buffer"),
587            size: 64, // Sufficient for basic uniforms
588            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
589            mapped_at_creation: false,
590        });
591
592        // Visual bell
593        let visual_bell_shader =
594            device.create_shader_module(wgpu::include_wgsl!("shaders/cell_bg.wgsl"));
595        let visual_bell_bind_group_layout =
596            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
597                label: Some("visual bell bind group layout"),
598                entries: &[wgpu::BindGroupLayoutEntry {
599                    binding: 0,
600                    visibility: wgpu::ShaderStages::FRAGMENT,
601                    ty: wgpu::BindingType::Buffer {
602                        ty: wgpu::BufferBindingType::Uniform,
603                        has_dynamic_offset: false,
604                        min_binding_size: None,
605                    },
606                    count: None,
607                }],
608            });
609
610        let visual_bell_pipeline_layout =
611            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
612                label: Some("visual bell pipeline layout"),
613                bind_group_layouts: &[&visual_bell_bind_group_layout],
614                push_constant_ranges: &[],
615            });
616
617        let visual_bell_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
618            label: Some("visual bell pipeline"),
619            layout: Some(&visual_bell_pipeline_layout),
620            vertex: wgpu::VertexState {
621                module: &visual_bell_shader,
622                entry_point: Some("vs_main"),
623                compilation_options: Default::default(),
624                buffers: &[
625                    wgpu::VertexBufferLayout {
626                        array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
627                        step_mode: wgpu::VertexStepMode::Vertex,
628                        attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2],
629                    },
630                    wgpu::VertexBufferLayout {
631                        array_stride: std::mem::size_of::<BackgroundInstance>() as wgpu::BufferAddress,
632                        step_mode: wgpu::VertexStepMode::Instance,
633                        attributes: &wgpu::vertex_attr_array![2 => Float32x2, 3 => Float32x2, 4 => Float32x4],
634                    },
635                ],
636            },
637            fragment: Some(wgpu::FragmentState {
638                module: &visual_bell_shader,
639                entry_point: Some("fs_main"),
640                compilation_options: Default::default(),
641                targets: &[Some(wgpu::ColorTargetState {
642                    format: surface_format,
643                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
644                    write_mask: wgpu::ColorWrites::ALL,
645                })],
646            }),
647            primitive: wgpu::PrimitiveState {
648                topology: wgpu::PrimitiveTopology::TriangleStrip,
649                ..Default::default()
650            },
651            depth_stencil: None,
652            multisample: wgpu::MultisampleState::default(),
653            multiview: None,
654            cache: None,
655        });
656
657        let visual_bell_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
658            label: Some("visual bell uniform buffer"),
659            size: 64,
660            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
661            mapped_at_creation: false,
662        });
663
664        let visual_bell_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
665            label: Some("visual bell bind group"),
666            layout: &visual_bell_bind_group_layout,
667            entries: &[wgpu::BindGroupEntry {
668                binding: 0,
669                resource: visual_bell_uniform_buffer.as_entire_binding(),
670            }],
671        });
672
673        // Initialize instance buffers (+1 for cursor overlay)
674        let max_bg_instances = cols * rows + 1;
675        let max_text_instances = cols * rows * 2; // Extra for shaped text
676        let bg_instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
677            label: Some("bg instance buffer"),
678            size: (max_bg_instances * std::mem::size_of::<BackgroundInstance>()) as u64,
679            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
680            mapped_at_creation: false,
681        });
682        let text_instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
683            label: Some("text instance buffer"),
684            size: (max_text_instances * std::mem::size_of::<TextInstance>()) as u64,
685            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
686            mapped_at_creation: false,
687        });
688
689        let mut renderer = Self {
690            device,
691            queue,
692            surface,
693            config,
694            bg_pipeline,
695            text_pipeline,
696            bg_image_pipeline,
697            visual_bell_pipeline,
698            vertex_buffer,
699            bg_instance_buffer,
700            text_instance_buffer,
701            bg_image_uniform_buffer,
702            visual_bell_uniform_buffer,
703            text_bind_group,
704            text_bind_group_layout,
705            bg_image_bind_group: None,
706            bg_image_bind_group_layout,
707            visual_bell_bind_group,
708            atlas_texture,
709            atlas_view,
710            glyph_cache: HashMap::new(),
711            lru_head: None,
712            lru_tail: None,
713            atlas_next_x: 0,
714            atlas_next_y: 0,
715            atlas_row_height: 0,
716            cols,
717            rows,
718            cell_width,
719            cell_height,
720            window_padding,
721            scale_factor,
722            font_manager,
723            scrollbar,
724            cells: vec![Cell::default(); cols * rows],
725            dirty_rows: vec![true; rows],
726            row_cache: (0..rows).map(|_| None).collect(),
727            cursor_pos: (0, 0),
728            cursor_opacity: 0.0,
729            cursor_style: par_term_emu_core_rust::cursor::CursorStyle::SteadyBlock,
730            cursor_overlay: None,
731            cursor_color: [1.0, 1.0, 1.0], // Default white
732            visual_bell_intensity: 0.0,
733            window_opacity,
734            background_color: [
735                background_color[0] as f32 / 255.0,
736                background_color[1] as f32 / 255.0,
737                background_color[2] as f32 / 255.0,
738                1.0,
739            ],
740            font_ascent,
741            font_descent,
742            font_leading,
743            font_size_pixels,
744            bg_image_texture: None,
745            bg_image_mode: background_image_mode,
746            bg_image_opacity: background_image_opacity,
747            max_bg_instances,
748            max_text_instances,
749            bg_instances: vec![
750                BackgroundInstance {
751                    position: [0.0, 0.0],
752                    size: [0.0, 0.0],
753                    color: [0.0, 0.0, 0.0, 0.0],
754                };
755                max_bg_instances
756            ],
757            text_instances: vec![
758                TextInstance {
759                    position: [0.0, 0.0],
760                    size: [0.0, 0.0],
761                    tex_offset: [0.0, 0.0],
762                    tex_size: [0.0, 0.0],
763                    color: [0.0, 0.0, 0.0, 0.0],
764                    is_colored: 0,
765                };
766                max_text_instances
767            ],
768            enable_text_shaping,
769            enable_ligatures,
770            enable_kerning,
771        };
772
773        if let Some(path) = background_image_path {
774            renderer.load_background_image(path)?;
775        }
776
777        Ok(renderer)
778    }
779
780    pub fn device(&self) -> &wgpu::Device {
781        &self.device
782    }
783    pub fn queue(&self) -> &wgpu::Queue {
784        &self.queue
785    }
786    pub fn surface_format(&self) -> wgpu::TextureFormat {
787        self.config.format
788    }
789    pub fn cell_width(&self) -> f32 {
790        self.cell_width
791    }
792    pub fn cell_height(&self) -> f32 {
793        self.cell_height
794    }
795    pub fn window_padding(&self) -> f32 {
796        self.window_padding
797    }
798    pub fn grid_size(&self) -> (usize, usize) {
799        (self.cols, self.rows)
800    }
801
802    pub fn resize(&mut self, width: u32, height: u32) -> (usize, usize) {
803        if width == 0 || height == 0 {
804            return (self.cols, self.rows);
805        }
806        self.config.width = width;
807        self.config.height = height;
808        self.surface.configure(&self.device, &self.config);
809
810        let available_width = (width as f32 - self.window_padding * 2.0).max(0.0);
811        let available_height = (height as f32 - self.window_padding * 2.0).max(0.0);
812        let new_cols = (available_width / self.cell_width).max(1.0) as usize;
813        let new_rows = (available_height / self.cell_height).max(1.0) as usize;
814
815        if new_cols != self.cols || new_rows != self.rows {
816            self.cols = new_cols;
817            self.rows = new_rows;
818            self.cells = vec![Cell::default(); self.cols * self.rows];
819            self.dirty_rows = vec![true; self.rows];
820            self.row_cache = (0..self.rows).map(|_| None).collect();
821            self.recreate_instance_buffers();
822        }
823
824        self.update_bg_image_uniforms();
825        (self.cols, self.rows)
826    }
827
828    fn recreate_instance_buffers(&mut self) {
829        self.max_bg_instances = self.cols * self.rows + 1; // +1 for cursor overlay
830        self.max_text_instances = self.cols * self.rows * 2;
831        self.bg_instance_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
832            label: Some("bg instance buffer"),
833            size: (self.max_bg_instances * std::mem::size_of::<BackgroundInstance>()) as u64,
834            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
835            mapped_at_creation: false,
836        });
837        self.text_instance_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
838            label: Some("text instance buffer"),
839            size: (self.max_text_instances * std::mem::size_of::<TextInstance>()) as u64,
840            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
841            mapped_at_creation: false,
842        });
843
844        self.bg_instances = vec![
845            BackgroundInstance {
846                position: [0.0, 0.0],
847                size: [0.0, 0.0],
848                color: [0.0, 0.0, 0.0, 0.0],
849            };
850            self.max_bg_instances
851        ];
852        self.text_instances = vec![
853            TextInstance {
854                position: [0.0, 0.0],
855                size: [0.0, 0.0],
856                tex_offset: [0.0, 0.0],
857                tex_size: [0.0, 0.0],
858                color: [0.0, 0.0, 0.0, 0.0],
859                is_colored: 0,
860            };
861            self.max_text_instances
862        ];
863    }
864
865    pub fn update_cells(&mut self, new_cells: &[Cell]) {
866        for row in 0..self.rows {
867            let start = row * self.cols;
868            let end = (row + 1) * self.cols;
869            if start < new_cells.len() && end <= new_cells.len() {
870                let row_slice = &new_cells[start..end];
871                if row_slice != &self.cells[start..end] {
872                    self.cells[start..end].clone_from_slice(row_slice);
873                    self.dirty_rows[row] = true;
874                }
875            }
876        }
877    }
878
879    pub fn update_cursor(
880        &mut self,
881        pos: (usize, usize),
882        opacity: f32,
883        style: par_term_emu_core_rust::cursor::CursorStyle,
884    ) {
885        if self.cursor_pos != pos || self.cursor_opacity != opacity || self.cursor_style != style {
886            self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
887            self.cursor_pos = pos;
888            self.cursor_opacity = opacity;
889            self.cursor_style = style;
890            self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
891
892            // Compute cursor overlay for beam/underline styles
893            use par_term_emu_core_rust::cursor::CursorStyle;
894            self.cursor_overlay = if opacity > 0.0 {
895                let col = pos.0;
896                let row = pos.1;
897                let x0 = (self.window_padding + col as f32 * self.cell_width).round();
898                let x1 = (self.window_padding + (col + 1) as f32 * self.cell_width).round();
899                let y0 = (self.window_padding + row as f32 * self.cell_height).round();
900                let y1 = (self.window_padding + (row + 1) as f32 * self.cell_height).round();
901
902                match style {
903                    // Block cursor: handled in cell background, no overlay needed
904                    CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock => None,
905                    // Beam/Bar cursor: thin vertical line on the left (2 pixels wide)
906                    CursorStyle::SteadyBar | CursorStyle::BlinkingBar => {
907                        Some(BackgroundInstance {
908                            position: [
909                                x0 / self.config.width as f32 * 2.0 - 1.0,
910                                1.0 - (y0 / self.config.height as f32 * 2.0),
911                            ],
912                            size: [
913                                2.0 / self.config.width as f32 * 2.0,
914                                (y1 - y0) / self.config.height as f32 * 2.0,
915                            ],
916                            color: [self.cursor_color[0], self.cursor_color[1], self.cursor_color[2], opacity],
917                        })
918                    }
919                    // Underline cursor: thin horizontal line at the bottom (2 pixels tall)
920                    CursorStyle::SteadyUnderline | CursorStyle::BlinkingUnderline => {
921                        Some(BackgroundInstance {
922                            position: [
923                                x0 / self.config.width as f32 * 2.0 - 1.0,
924                                1.0 - ((y1 - 2.0) / self.config.height as f32 * 2.0),
925                            ],
926                            size: [
927                                (x1 - x0) / self.config.width as f32 * 2.0,
928                                2.0 / self.config.height as f32 * 2.0,
929                            ],
930                            color: [self.cursor_color[0], self.cursor_color[1], self.cursor_color[2], opacity],
931                        })
932                    }
933                }
934            } else {
935                None
936            };
937        }
938    }
939
940    pub fn clear_cursor(&mut self) {
941        self.update_cursor(self.cursor_pos, 0.0, self.cursor_style);
942    }
943
944    /// Update cursor color
945    pub fn update_cursor_color(&mut self, color: [u8; 3]) {
946        self.cursor_color = [
947            color[0] as f32 / 255.0,
948            color[1] as f32 / 255.0,
949            color[2] as f32 / 255.0,
950        ];
951        // Mark cursor row as dirty to redraw with new color
952        self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
953    }
954
955    pub fn update_scrollbar(
956        &mut self,
957        scroll_offset: usize,
958        visible_lines: usize,
959        total_lines: usize,
960    ) {
961        self.scrollbar.update(
962            &self.queue,
963            scroll_offset,
964            visible_lines,
965            total_lines,
966            self.config.width,
967            self.config.height,
968        );
969    }
970
971    pub fn set_visual_bell_intensity(&mut self, intensity: f32) {
972        self.visual_bell_intensity = intensity;
973    }
974
975    pub fn update_opacity(&mut self, opacity: f32) {
976        self.window_opacity = opacity;
977        self.update_bg_image_uniforms();
978    }
979
980    pub fn update_scale_factor(&mut self, scale_factor: f64) {
981        self.scale_factor = scale_factor as f32;
982    }
983
984    pub fn update_window_padding(&mut self, padding: f32) -> Option<(usize, usize)> {
985        if (self.window_padding - padding).abs() > f32::EPSILON {
986            self.window_padding = padding;
987            let size = (self.config.width, self.config.height);
988            return Some(self.resize(size.0, size.1));
989        }
990        None
991    }
992
993    pub fn set_background_image(
994        &mut self,
995        path: Option<&str>,
996        mode: crate::config::BackgroundImageMode,
997        opacity: f32,
998    ) {
999        self.bg_image_mode = mode;
1000        self.bg_image_opacity = opacity;
1001        if let Some(p) = path {
1002            let _ = self.load_background_image(p);
1003        } else {
1004            self.bg_image_texture = None;
1005            self.bg_image_bind_group = None;
1006        }
1007        self.update_bg_image_uniforms();
1008    }
1009
1010    pub fn update_background_image_opacity(&mut self, opacity: f32) {
1011        self.bg_image_opacity = opacity;
1012        self.update_bg_image_uniforms();
1013    }
1014
1015    pub fn update_scrollbar_appearance(
1016        &mut self,
1017        width: f32,
1018        thumb_color: [f32; 4],
1019        track_color: [f32; 4],
1020    ) {
1021        self.scrollbar
1022            .update_appearance(width, thumb_color, track_color);
1023    }
1024
1025    pub fn update_scrollbar_position(&mut self, position: &str) {
1026        self.scrollbar.update_position(position);
1027    }
1028
1029    pub fn scrollbar_contains_point(&self, x: f32, y: f32) -> bool {
1030        self.scrollbar.contains_point(x, y)
1031    }
1032
1033    pub fn scrollbar_thumb_bounds(&self) -> Option<(f32, f32)> {
1034        self.scrollbar.thumb_bounds()
1035    }
1036
1037    pub fn scrollbar_track_contains_x(&self, x: f32) -> bool {
1038        self.scrollbar.track_contains_x(x)
1039    }
1040
1041    pub fn scrollbar_mouse_y_to_scroll_offset(&self, mouse_y: f32) -> Option<usize> {
1042        self.scrollbar.mouse_y_to_scroll_offset(mouse_y)
1043    }
1044
1045    pub fn reconfigure_surface(&mut self) {
1046        self.surface.configure(&self.device, &self.config);
1047    }
1048
1049    pub fn clear_glyph_cache(&mut self) {
1050        self.glyph_cache.clear();
1051        self.lru_head = None;
1052        self.lru_tail = None;
1053        self.atlas_next_x = 0;
1054        self.atlas_next_y = 0;
1055        self.atlas_row_height = 0;
1056        self.dirty_rows.fill(true);
1057    }
1058
1059    fn lru_remove(&mut self, key: u64) {
1060        let info = self.glyph_cache.get(&key).unwrap();
1061        let prev = info.prev;
1062        let next = info.next;
1063
1064        if let Some(p) = prev {
1065            self.glyph_cache.get_mut(&p).unwrap().next = next;
1066        } else {
1067            self.lru_head = next;
1068        }
1069
1070        if let Some(n) = next {
1071            self.glyph_cache.get_mut(&n).unwrap().prev = prev;
1072        } else {
1073            self.lru_tail = prev;
1074        }
1075    }
1076
1077    fn lru_push_front(&mut self, key: u64) {
1078        let next = self.lru_head;
1079        if let Some(n) = next {
1080            self.glyph_cache.get_mut(&n).unwrap().prev = Some(key);
1081        } else {
1082            self.lru_tail = Some(key);
1083        }
1084
1085        let info = self.glyph_cache.get_mut(&key).unwrap();
1086        info.prev = None;
1087        info.next = next;
1088        self.lru_head = Some(key);
1089    }
1090
1091    fn load_background_image(&mut self, path: &str) -> Result<()> {
1092        let img = image::open(path)?.to_rgba8();
1093        let (width, height) = img.dimensions();
1094        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
1095            label: Some("bg image"),
1096            size: wgpu::Extent3d {
1097                width,
1098                height,
1099                depth_or_array_layers: 1,
1100            },
1101            mip_level_count: 1,
1102            sample_count: 1,
1103            dimension: wgpu::TextureDimension::D2,
1104            format: wgpu::TextureFormat::Rgba8UnormSrgb,
1105            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1106            view_formats: &[],
1107        });
1108        self.queue.write_texture(
1109            wgpu::TexelCopyTextureInfo {
1110                texture: &texture,
1111                mip_level: 0,
1112                origin: wgpu::Origin3d::ZERO,
1113                aspect: wgpu::TextureAspect::All,
1114            },
1115            &img,
1116            wgpu::TexelCopyBufferLayout {
1117                offset: 0,
1118                bytes_per_row: Some(4 * width),
1119                rows_per_image: Some(height),
1120            },
1121            wgpu::Extent3d {
1122                width,
1123                height,
1124                depth_or_array_layers: 1,
1125            },
1126        );
1127
1128        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1129        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
1130            mag_filter: wgpu::FilterMode::Linear,
1131            min_filter: wgpu::FilterMode::Linear,
1132            ..Default::default()
1133        });
1134
1135        self.bg_image_bind_group =
1136            Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1137                label: Some("bg image bind group"),
1138                layout: &self.bg_image_bind_group_layout,
1139                entries: &[
1140                    wgpu::BindGroupEntry {
1141                        binding: 0,
1142                        resource: wgpu::BindingResource::TextureView(&view),
1143                    },
1144                    wgpu::BindGroupEntry {
1145                        binding: 1,
1146                        resource: wgpu::BindingResource::Sampler(&sampler),
1147                    },
1148                    wgpu::BindGroupEntry {
1149                        binding: 2,
1150                        resource: self.bg_image_uniform_buffer.as_entire_binding(),
1151                    },
1152                ],
1153            }));
1154        self.bg_image_texture = Some(texture);
1155        self.update_bg_image_uniforms();
1156        Ok(())
1157    }
1158
1159    fn update_bg_image_uniforms(&mut self) {
1160        let mut data = [0.0f32; 16];
1161        data[0] = self.bg_image_opacity;
1162        data[1] = self.window_opacity;
1163        data[2] = self.bg_image_mode as u32 as f32;
1164        data[3] = self.config.width as f32;
1165        data[4] = self.config.height as f32;
1166        self.queue.write_buffer(
1167            &self.bg_image_uniform_buffer,
1168            0,
1169            bytemuck::cast_slice(&data),
1170        );
1171    }
1172
1173    pub fn render(&mut self, show_scrollbar: bool) -> Result<wgpu::SurfaceTexture> {
1174        let output = self.surface.get_current_texture()?;
1175        let view = output
1176            .texture
1177            .create_view(&wgpu::TextureViewDescriptor::default());
1178        self.build_instance_buffers()?;
1179
1180        let mut encoder = self
1181            .device
1182            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1183                label: Some("render encoder"),
1184            });
1185
1186        {
1187            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1188                label: Some("render pass"),
1189                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1190                    view: &view,
1191                    resolve_target: None,
1192                    ops: wgpu::Operations {
1193                        load: wgpu::LoadOp::Clear(wgpu::Color {
1194                            r: self.background_color[0] as f64,
1195                            g: self.background_color[1] as f64,
1196                            b: self.background_color[2] as f64,
1197                            a: self.window_opacity as f64,
1198                        }),
1199                        store: wgpu::StoreOp::Store,
1200                    },
1201                    depth_slice: None,
1202                })],
1203                depth_stencil_attachment: None,
1204                timestamp_writes: None,
1205                occlusion_query_set: None,
1206            });
1207
1208            if let Some(ref bg_bind_group) = self.bg_image_bind_group {
1209                render_pass.set_pipeline(&self.bg_image_pipeline);
1210                render_pass.set_bind_group(0, bg_bind_group, &[]);
1211                render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1212                render_pass.draw(0..4, 0..1);
1213            }
1214
1215            render_pass.set_pipeline(&self.bg_pipeline);
1216            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1217            render_pass.set_vertex_buffer(1, self.bg_instance_buffer.slice(..));
1218            render_pass.draw(0..4, 0..self.max_bg_instances as u32);
1219
1220            render_pass.set_pipeline(&self.text_pipeline);
1221            render_pass.set_bind_group(0, &self.text_bind_group, &[]);
1222            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1223            render_pass.set_vertex_buffer(1, self.text_instance_buffer.slice(..));
1224            render_pass.draw(0..4, 0..self.max_text_instances as u32);
1225
1226            if show_scrollbar {
1227                self.scrollbar.render(&mut render_pass);
1228            }
1229        }
1230
1231        self.queue.submit(std::iter::once(encoder.finish()));
1232        Ok(output)
1233    }
1234
1235    pub fn render_to_texture(
1236        &mut self,
1237        target_view: &wgpu::TextureView,
1238    ) -> Result<wgpu::SurfaceTexture> {
1239        let output = self.surface.get_current_texture()?;
1240        self.build_instance_buffers()?;
1241
1242        let mut encoder = self
1243            .device
1244            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1245                label: Some("render to texture encoder"),
1246            });
1247
1248        {
1249            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1250                label: Some("render pass"),
1251                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1252                    view: target_view,
1253                    resolve_target: None,
1254                    ops: wgpu::Operations {
1255                        load: wgpu::LoadOp::Clear(wgpu::Color {
1256                            r: 0.0,
1257                            g: 0.0,
1258                            b: 0.0,
1259                            a: 0.0,
1260                        }),
1261                        store: wgpu::StoreOp::Store,
1262                    },
1263                    depth_slice: None,
1264                })],
1265                depth_stencil_attachment: None,
1266                timestamp_writes: None,
1267                occlusion_query_set: None,
1268            });
1269
1270            if let Some(ref bg_bind_group) = self.bg_image_bind_group {
1271                render_pass.set_pipeline(&self.bg_image_pipeline);
1272                render_pass.set_bind_group(0, bg_bind_group, &[]);
1273                render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1274                render_pass.draw(0..4, 0..1);
1275            }
1276
1277            render_pass.set_pipeline(&self.bg_pipeline);
1278            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1279            render_pass.set_vertex_buffer(1, self.bg_instance_buffer.slice(..));
1280            render_pass.draw(0..4, 0..self.max_bg_instances as u32);
1281
1282            render_pass.set_pipeline(&self.text_pipeline);
1283            render_pass.set_bind_group(0, &self.text_bind_group, &[]);
1284            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1285            render_pass.set_vertex_buffer(1, self.text_instance_buffer.slice(..));
1286            render_pass.draw(0..4, 0..self.max_text_instances as u32);
1287        }
1288
1289        self.queue.submit(std::iter::once(encoder.finish()));
1290        Ok(output)
1291    }
1292
1293    pub fn render_overlays(
1294        &mut self,
1295        surface_texture: &wgpu::SurfaceTexture,
1296        show_scrollbar: bool,
1297    ) -> Result<()> {
1298        let view = surface_texture
1299            .texture
1300            .create_view(&wgpu::TextureViewDescriptor::default());
1301        let mut encoder = self
1302            .device
1303            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1304                label: Some("overlay encoder"),
1305            });
1306
1307        {
1308            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1309                label: Some("overlay pass"),
1310                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1311                    view: &view,
1312                    resolve_target: None,
1313                    ops: wgpu::Operations {
1314                        load: wgpu::LoadOp::Load,
1315                        store: wgpu::StoreOp::Store,
1316                    },
1317                    depth_slice: None,
1318                })],
1319                depth_stencil_attachment: None,
1320                timestamp_writes: None,
1321                occlusion_query_set: None,
1322            });
1323
1324            if show_scrollbar {
1325                self.scrollbar.render(&mut render_pass);
1326            }
1327
1328            if self.visual_bell_intensity > 0.0 {
1329                // Visual bell logic
1330            }
1331        }
1332
1333        self.queue.submit(std::iter::once(encoder.finish()));
1334        Ok(())
1335    }
1336
1337    fn build_instance_buffers(&mut self) -> Result<()> {
1338        let _shaping_options = ShapingOptions {
1339            enable_ligatures: self.enable_ligatures,
1340            enable_kerning: self.enable_kerning,
1341            ..Default::default()
1342        };
1343
1344        for row in 0..self.rows {
1345            if self.dirty_rows[row] || self.row_cache[row].is_none() {
1346                let start = row * self.cols;
1347                let end = (row + 1) * self.cols;
1348                let row_cells = &self.cells[start..end];
1349
1350                let mut row_bg = Vec::with_capacity(self.cols);
1351                let mut row_text = Vec::with_capacity(self.cols);
1352
1353                // Background
1354                for (col, cell) in row_cells.iter().enumerate() {
1355                    let is_default_bg =
1356                        (cell.bg_color[0] as f32 / 255.0 - self.background_color[0]).abs() < 0.001
1357                            && (cell.bg_color[1] as f32 / 255.0 - self.background_color[1]).abs()
1358                                < 0.001
1359                            && (cell.bg_color[2] as f32 / 255.0 - self.background_color[2]).abs()
1360                                < 0.001;
1361
1362                    let has_cursor = self.cursor_opacity > 0.0
1363                        && self.cursor_pos.1 == row
1364                        && self.cursor_pos.0 == col;
1365
1366                    if is_default_bg && !has_cursor {
1367                        row_bg.push(BackgroundInstance {
1368                            position: [0.0, 0.0],
1369                            size: [0.0, 0.0],
1370                            color: [0.0, 0.0, 0.0, 0.0],
1371                        });
1372                        continue;
1373                    }
1374
1375                    let bg_color = [
1376                        cell.bg_color[0] as f32 / 255.0,
1377                        cell.bg_color[1] as f32 / 255.0,
1378                        cell.bg_color[2] as f32 / 255.0,
1379                        cell.bg_color[3] as f32 / 255.0,
1380                    ];
1381
1382                    let x0 = (self.window_padding + col as f32 * self.cell_width).round();
1383                    let x1 = (self.window_padding + (col + 1) as f32 * self.cell_width).round();
1384                    let y0 = (self.window_padding + row as f32 * self.cell_height).round();
1385                    let y1 = (self.window_padding + (row + 1) as f32 * self.cell_height).round();
1386
1387                    // Geometric cursor rendering based on cursor style
1388                    // For block cursor, blend into cell background; for others, add overlay later
1389                    let mut final_bg_color = bg_color;
1390                    if has_cursor && self.cursor_opacity > 0.0 {
1391                        use par_term_emu_core_rust::cursor::CursorStyle;
1392                        match self.cursor_style {
1393                            // Block cursor: blend cursor color into background
1394                            CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock => {
1395                                for (bg, &cursor) in
1396                                    final_bg_color.iter_mut().take(3).zip(&self.cursor_color)
1397                                {
1398                                    *bg = *bg * (1.0 - self.cursor_opacity)
1399                                        + cursor * self.cursor_opacity;
1400                                }
1401                                final_bg_color[3] = final_bg_color[3].max(self.cursor_opacity);
1402                            }
1403                            // Beam/Bar and Underline: handled separately in cursor_instance
1404                            _ => {}
1405                        }
1406                    }
1407
1408                    // Add cell background
1409                    row_bg.push(BackgroundInstance {
1410                        position: [
1411                            x0 / self.config.width as f32 * 2.0 - 1.0,
1412                            1.0 - (y0 / self.config.height as f32 * 2.0),
1413                        ],
1414                        size: [
1415                            (x1 - x0) / self.config.width as f32 * 2.0,
1416                            (y1 - y0) / self.config.height as f32 * 2.0,
1417                        ],
1418                        color: final_bg_color,
1419                    });
1420                }
1421
1422                // Text
1423                let mut x_offset = 0.0;
1424                let cell_data: Vec<(String, bool, bool, [u8; 4], bool, bool)> = row_cells
1425                    .iter()
1426                    .map(|c| {
1427                        (
1428                            c.grapheme.clone(),
1429                            c.bold,
1430                            c.italic,
1431                            c.fg_color,
1432                            c.wide_char_spacer,
1433                            c.wide_char,
1434                        )
1435                    })
1436                    .collect();
1437
1438                // Dynamic baseline calculation based on font metrics
1439                let natural_line_height = self.font_ascent + self.font_descent + self.font_leading;
1440                let vertical_padding = (self.cell_height - natural_line_height).max(0.0) / 2.0;
1441                let baseline_y_unrounded = self.window_padding
1442                    + (row as f32 * self.cell_height)
1443                    + vertical_padding
1444                    + self.font_ascent;
1445
1446                for (grapheme, bold, italic, fg_color, is_spacer, is_wide) in cell_data {
1447                    if is_spacer || grapheme == " " {
1448                        x_offset += self.cell_width;
1449                        continue;
1450                    }
1451
1452                    let chars: Vec<char> = grapheme.chars().collect();
1453                    #[allow(clippy::collapsible_if)]
1454                    if let Some(ch) = chars.first() {
1455                        if let Some((font_idx, glyph_id)) =
1456                            self.font_manager.find_glyph(*ch, bold, italic)
1457                        {
1458                            let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
1459                            let info = if self.glyph_cache.contains_key(&cache_key) {
1460                                // Move to front of LRU
1461                                self.lru_remove(cache_key);
1462                                self.lru_push_front(cache_key);
1463                                self.glyph_cache.get(&cache_key).unwrap().clone()
1464                            } else if let Some(raster) = self.rasterize_glyph(font_idx, glyph_id) {
1465                                let info = self.upload_glyph(cache_key, &raster);
1466                                self.glyph_cache.insert(cache_key, info.clone());
1467                                self.lru_push_front(cache_key);
1468                                info
1469                            } else {
1470                                x_offset += self.cell_width;
1471                                continue;
1472                            };
1473
1474                            let char_w = if is_wide {
1475                                self.cell_width * 2.0
1476                            } else {
1477                                self.cell_width
1478                            };
1479                            let x0 = (self.window_padding + x_offset).round();
1480                            let x1 = (self.window_padding + x_offset + char_w).round();
1481                            let y0 = (self.window_padding + row as f32 * self.cell_height).round();
1482                            let y1 =
1483                                (self.window_padding + (row + 1) as f32 * self.cell_height).round();
1484
1485                            let cell_w = x1 - x0;
1486                            let cell_h = y1 - y0;
1487
1488                            let scale_x = cell_w / char_w;
1489                            let scale_y = cell_h / self.cell_height;
1490
1491                            // Position glyph relative to snapped cell top-left
1492                            let baseline_offset = baseline_y_unrounded
1493                                - (self.window_padding + row as f32 * self.cell_height);
1494                            let mut glyph_left = x0 + (info.bearing_x * scale_x).round();
1495                            let mut glyph_top =
1496                                y0 + ((baseline_offset - info.bearing_y) * scale_y).round();
1497
1498                            let mut render_w = info.width as f32 * scale_x;
1499                            let mut render_h = info.height as f32 * scale_y;
1500
1501                            // Special case: for box drawing and block elements, ensure they fill the cell
1502                            // if they are close to the edges to avoid 1px gaps.
1503                            let char_code = *ch as u32;
1504                            let is_block_char = (0x2500..=0x259F).contains(&char_code)
1505                                || (0xE0A0..=0xE0D4).contains(&char_code)
1506                                || (0x25A0..=0x25FF).contains(&char_code); // Geometric shapes
1507
1508                            if is_block_char {
1509                                // Snap to left/right cell boundaries
1510                                if (glyph_left - x0).abs() < 3.0 {
1511                                    let right = glyph_left + render_w;
1512                                    glyph_left = x0;
1513                                    render_w = (right - x0).max(render_w);
1514                                }
1515                                if (x1 - (glyph_left + render_w)).abs() < 3.0 {
1516                                    render_w = x1 - glyph_left;
1517                                }
1518
1519                                // Snap to top/bottom cell boundaries
1520                                if (glyph_top - y0).abs() < 3.0 {
1521                                    let bottom = glyph_top + render_h;
1522                                    glyph_top = y0;
1523                                    render_h = (bottom - y0).max(render_h);
1524                                }
1525                                if (y1 - (glyph_top + render_h)).abs() < 3.0 {
1526                                    render_h = y1 - glyph_top;
1527                                }
1528
1529                                // For half-blocks and quadrants, also snap to middle boundaries
1530                                let cx = (x0 + x1) / 2.0;
1531                                let cy = (y0 + y1) / 2.0;
1532
1533                                // Vertical middle snap
1534                                if (glyph_top + render_h - cy).abs() < 2.0 {
1535                                    render_h = cy - glyph_top;
1536                                } else if (glyph_top - cy).abs() < 2.0 {
1537                                    let bottom = glyph_top + render_h;
1538                                    glyph_top = cy;
1539                                    render_h = bottom - cy;
1540                                }
1541
1542                                // Horizontal middle snap
1543                                if (glyph_left + render_w - cx).abs() < 2.0 {
1544                                    render_w = cx - glyph_left;
1545                                } else if (glyph_left - cx).abs() < 2.0 {
1546                                    let right = glyph_left + render_w;
1547                                    glyph_left = cx;
1548                                    render_w = right - cx;
1549                                }
1550                            }
1551
1552                            row_text.push(TextInstance {
1553                                position: [
1554                                    glyph_left / self.config.width as f32 * 2.0 - 1.0,
1555                                    1.0 - (glyph_top / self.config.height as f32 * 2.0),
1556                                ],
1557                                size: [
1558                                    render_w / self.config.width as f32 * 2.0,
1559                                    render_h / self.config.height as f32 * 2.0,
1560                                ],
1561                                tex_offset: [info.x as f32 / 2048.0, info.y as f32 / 2048.0],
1562                                tex_size: [info.width as f32 / 2048.0, info.height as f32 / 2048.0],
1563                                color: [
1564                                    fg_color[0] as f32 / 255.0,
1565                                    fg_color[1] as f32 / 255.0,
1566                                    fg_color[2] as f32 / 255.0,
1567                                    fg_color[3] as f32 / 255.0,
1568                                ],
1569                                is_colored: if info.is_colored { 1 } else { 0 },
1570                            });
1571                        }
1572                    }
1573                    x_offset += self.cell_width;
1574                }
1575
1576                // Update CPU-side buffers
1577                let bg_start = row * self.cols;
1578                self.bg_instances[bg_start..bg_start + self.cols].copy_from_slice(&row_bg);
1579
1580                let text_start = row * self.cols * 2;
1581                // Clear row text segment first
1582                for i in 0..(self.cols * 2) {
1583                    self.text_instances[text_start + i].size = [0.0, 0.0];
1584                }
1585                // Copy new text instances
1586                let text_count = row_text.len().min(self.cols * 2);
1587                self.text_instances[text_start..text_start + text_count]
1588                    .copy_from_slice(&row_text[..text_count]);
1589
1590                // Update GPU-side buffers incrementally
1591                self.queue.write_buffer(
1592                    &self.bg_instance_buffer,
1593                    (bg_start * std::mem::size_of::<BackgroundInstance>()) as u64,
1594                    bytemuck::cast_slice(&row_bg),
1595                );
1596                self.queue.write_buffer(
1597                    &self.text_instance_buffer,
1598                    (text_start * std::mem::size_of::<TextInstance>()) as u64,
1599                    bytemuck::cast_slice(
1600                        &self.text_instances[text_start..text_start + self.cols * 2],
1601                    ),
1602                );
1603
1604                self.row_cache[row] = Some(RowCacheEntry {});
1605                self.dirty_rows[row] = false;
1606            }
1607        }
1608
1609        // Write cursor overlay to the last slot of bg_instances (for beam/underline cursors)
1610        let cursor_overlay_index = self.cols * self.rows;
1611        let cursor_overlay_instance = self.cursor_overlay.unwrap_or(BackgroundInstance {
1612            position: [0.0, 0.0],
1613            size: [0.0, 0.0],
1614            color: [0.0, 0.0, 0.0, 0.0],
1615        });
1616        self.bg_instances[cursor_overlay_index] = cursor_overlay_instance;
1617        self.queue.write_buffer(
1618            &self.bg_instance_buffer,
1619            (cursor_overlay_index * std::mem::size_of::<BackgroundInstance>()) as u64,
1620            bytemuck::cast_slice(&[cursor_overlay_instance]),
1621        );
1622
1623        Ok(())
1624    }
1625
1626    fn rasterize_glyph(&self, font_idx: usize, glyph_id: u16) -> Option<RasterizedGlyph> {
1627        let font = self.font_manager.get_font(font_idx)?;
1628        // Use swash to rasterize
1629        use swash::scale::image::Content;
1630        use swash::scale::{Render, ScaleContext};
1631        let mut context = ScaleContext::new();
1632        let mut scaler = context
1633            .builder(*font)
1634            .size(self.font_size_pixels)
1635            .hint(true)
1636            .build();
1637        let image = Render::new(&[
1638            swash::scale::Source::ColorOutline(0),
1639            swash::scale::Source::ColorBitmap(swash::scale::StrikeWith::BestFit),
1640            swash::scale::Source::Outline,
1641        ])
1642        .render(&mut scaler, glyph_id)?;
1643
1644        let mut pixels = Vec::with_capacity(image.data.len() * 4);
1645        let is_colored = match image.content {
1646            Content::Color => {
1647                pixels.extend_from_slice(&image.data);
1648                true
1649            }
1650            Content::Mask => {
1651                for &mask in &image.data {
1652                    pixels.push(255);
1653                    pixels.push(255);
1654                    pixels.push(255);
1655                    pixels.push(mask);
1656                }
1657                false
1658            }
1659            _ => return None,
1660        };
1661
1662        Some(RasterizedGlyph {
1663            width: image.placement.width,
1664            height: image.placement.height,
1665            bearing_x: image.placement.left as f32,
1666            bearing_y: image.placement.top as f32,
1667            pixels,
1668            is_colored,
1669        })
1670    }
1671
1672    fn upload_glyph(&mut self, _key: u64, raster: &RasterizedGlyph) -> GlyphInfo {
1673        let padding = 2;
1674        if self.atlas_next_x + raster.width + padding > 2048 {
1675            self.atlas_next_x = 0;
1676            self.atlas_next_y += self.atlas_row_height + padding;
1677            self.atlas_row_height = 0;
1678        }
1679
1680        if self.atlas_next_y + raster.height + padding > 2048 {
1681            self.clear_glyph_cache();
1682        }
1683
1684        let info = GlyphInfo {
1685            key: _key,
1686            x: self.atlas_next_x,
1687            y: self.atlas_next_y,
1688            width: raster.width,
1689            height: raster.height,
1690            bearing_x: raster.bearing_x,
1691            bearing_y: raster.bearing_y,
1692            is_colored: raster.is_colored,
1693            prev: None,
1694            next: None,
1695        };
1696
1697        self.queue.write_texture(
1698            wgpu::TexelCopyTextureInfo {
1699                texture: &self.atlas_texture,
1700                mip_level: 0,
1701                origin: wgpu::Origin3d {
1702                    x: info.x,
1703                    y: info.y,
1704                    z: 0,
1705                },
1706                aspect: wgpu::TextureAspect::All,
1707            },
1708            &raster.pixels,
1709            wgpu::TexelCopyBufferLayout {
1710                offset: 0,
1711                bytes_per_row: Some(4 * raster.width),
1712                rows_per_image: Some(raster.height),
1713            },
1714            wgpu::Extent3d {
1715                width: raster.width,
1716                height: raster.height,
1717                depth_or_array_layers: 1,
1718            },
1719        );
1720
1721        self.atlas_next_x += raster.width + padding;
1722        self.atlas_row_height = self.atlas_row_height.max(raster.height);
1723
1724        info
1725    }
1726
1727    #[allow(dead_code)]
1728    pub fn update_graphics(
1729        &mut self,
1730        _graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
1731        _scroll_offset: usize,
1732        _scrollback_len: usize,
1733        _visible_lines: usize,
1734    ) -> Result<()> {
1735        Ok(())
1736    }
1737
1738    #[allow(dead_code)]
1739    pub fn update_background_image_opacity_only(&mut self, opacity: f32) {
1740        self.bg_image_opacity = opacity;
1741        self.update_bg_image_uniforms();
1742    }
1743}
1744
1745struct RasterizedGlyph {
1746    width: u32,
1747    height: u32,
1748    #[allow(dead_code)]
1749    bearing_x: f32,
1750    #[allow(dead_code)]
1751    bearing_y: f32,
1752    pixels: Vec<u8>,
1753    is_colored: bool,
1754}