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