Skip to main content

par_term_render/cell_renderer/
render.rs

1use super::block_chars;
2use super::{BackgroundInstance, Cell, CellRenderer, PaneViewport, RowCacheEntry, TextInstance};
3use anyhow::Result;
4use par_term_config::SeparatorMark;
5use par_term_fonts::text_shaper::ShapingOptions;
6
7impl CellRenderer {
8    pub fn render(
9        &mut self,
10        _show_scrollbar: bool,
11        pane_background: Option<&par_term_config::PaneBackground>,
12    ) -> Result<wgpu::SurfaceTexture> {
13        let output = self.surface.get_current_texture()?;
14        let view = output
15            .texture
16            .create_view(&wgpu::TextureViewDescriptor::default());
17        self.build_instance_buffers()?;
18
19        // Pre-create per-pane background bind group if needed (must happen before render pass)
20        // This supports pane 0 background in single-pane (no splits) mode.
21        let pane_bg_resources = if !self.bg_is_solid_color {
22            if let Some(pane_bg) = pane_background {
23                if let Some(ref path) = pane_bg.image_path {
24                    self.pane_bg_cache.get(path.as_str()).map(|entry| {
25                        self.create_pane_bg_bind_group(
26                            entry,
27                            0.0, // pane_x: full window starts at 0
28                            0.0, // pane_y: full window starts at 0
29                            self.config.width as f32,
30                            self.config.height as f32,
31                            pane_bg.mode,
32                            pane_bg.opacity,
33                        )
34                    })
35                } else {
36                    None
37                }
38            } else {
39                None
40            }
41        } else {
42            None
43        };
44
45        let mut encoder = self
46            .device
47            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
48                label: Some("render encoder"),
49            });
50
51        // Determine clear color and whether to use bg_image pipeline:
52        // - Solid color mode: use clear color directly (same as Default mode for proper transparency)
53        // - Image mode: use TRANSPARENT clear, let bg_image_pipeline handle background
54        // - Default mode: use theme background with window_opacity
55        // - Per-pane bg: use TRANSPARENT clear, render pane bg before global bg
56        let has_pane_bg = pane_bg_resources.is_some();
57        let (clear_color, use_bg_image_pipeline) = if has_pane_bg {
58            // Per-pane background: use transparent clear, pane bg will be rendered first
59            (wgpu::Color::TRANSPARENT, false)
60        } else if self.bg_is_solid_color {
61            // Solid color mode: use clear color directly for proper window transparency
62            // This works the same as Default mode - LoadOp::Clear sets alpha correctly
63            log::info!(
64                "[BACKGROUND] Solid color mode: RGB({:.3}, {:.3}, {:.3}) * opacity {:.3}",
65                self.solid_bg_color[0],
66                self.solid_bg_color[1],
67                self.solid_bg_color[2],
68                self.window_opacity
69            );
70            (
71                wgpu::Color {
72                    r: self.solid_bg_color[0] as f64 * self.window_opacity as f64,
73                    g: self.solid_bg_color[1] as f64 * self.window_opacity as f64,
74                    b: self.solid_bg_color[2] as f64 * self.window_opacity as f64,
75                    a: self.window_opacity as f64,
76                },
77                false,
78            )
79        } else if self.bg_image_bind_group.is_some() {
80            // Image mode: use TRANSPARENT, let bg_image_pipeline handle background
81            (wgpu::Color::TRANSPARENT, true)
82        } else {
83            // Default mode: use theme background with window_opacity
84            (
85                wgpu::Color {
86                    r: self.background_color[0] as f64 * self.window_opacity as f64,
87                    g: self.background_color[1] as f64 * self.window_opacity as f64,
88                    b: self.background_color[2] as f64 * self.window_opacity as f64,
89                    a: self.window_opacity as f64,
90                },
91                false,
92            )
93        };
94
95        {
96            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
97                label: Some("render pass"),
98                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
99                    view: &view,
100                    resolve_target: None,
101                    ops: wgpu::Operations {
102                        load: wgpu::LoadOp::Clear(clear_color),
103                        store: wgpu::StoreOp::Store,
104                    },
105                    depth_slice: None,
106                })],
107                depth_stencil_attachment: None,
108                timestamp_writes: None,
109                occlusion_query_set: None,
110            });
111
112            // Render per-pane background for single-pane mode (pane 0)
113            if let Some((ref bind_group, _)) = pane_bg_resources {
114                render_pass.set_pipeline(&self.bg_image_pipeline);
115                render_pass.set_bind_group(0, bind_group, &[]);
116                render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
117                render_pass.draw(0..4, 0..1);
118            }
119
120            // Render global background image if present (not used for solid color or pane bg mode)
121            if use_bg_image_pipeline && let Some(ref bg_bind_group) = self.bg_image_bind_group {
122                render_pass.set_pipeline(&self.bg_image_pipeline);
123                render_pass.set_bind_group(0, bg_bind_group, &[]);
124                render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
125                render_pass.draw(0..4, 0..1);
126            }
127
128            render_pass.set_pipeline(&self.bg_pipeline);
129            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
130            render_pass.set_vertex_buffer(1, self.bg_instance_buffer.slice(..));
131            render_pass.draw(0..4, 0..self.max_bg_instances as u32);
132
133            render_pass.set_pipeline(&self.text_pipeline);
134            render_pass.set_bind_group(0, &self.text_bind_group, &[]);
135            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
136            render_pass.set_vertex_buffer(1, self.text_instance_buffer.slice(..));
137            render_pass.draw(0..4, 0..self.max_text_instances as u32);
138        }
139
140        self.queue.submit(std::iter::once(encoder.finish()));
141        Ok(output)
142    }
143
144    /// Render terminal content to an intermediate texture for shader processing.
145    ///
146    /// # Arguments
147    /// * `target_view` - The texture view to render to
148    /// * `skip_background_image` - If true, skip rendering the background image. Use this when
149    ///   a custom shader will handle the background image via iChannel0 instead.
150    ///
151    /// Note: Solid color backgrounds are NOT rendered here. For cursor shaders, the solid color
152    /// is passed to the shader's render function as the clear color instead.
153    pub fn render_to_texture(
154        &mut self,
155        target_view: &wgpu::TextureView,
156        skip_background_image: bool,
157    ) -> Result<wgpu::SurfaceTexture> {
158        let output = self.surface.get_current_texture()?;
159        self.build_instance_buffers()?;
160
161        // Only render background IMAGE to intermediate texture (not solid color).
162        // Solid colors are handled by the shader's clear color for proper compositing.
163        let render_background_image =
164            !skip_background_image && !self.bg_is_solid_color && self.bg_image_bind_group.is_some();
165        let saved_window_opacity = self.window_opacity;
166
167        if render_background_image {
168            // Temporarily set window_opacity to 1.0 for the background render
169            // The shader wrapper will apply window_opacity at the end
170            self.window_opacity = 1.0;
171            self.update_bg_image_uniforms();
172        }
173
174        let mut encoder = self
175            .device
176            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
177                label: Some("render to texture encoder"),
178            });
179
180        // Always clear with TRANSPARENT for intermediate textures
181        let clear_color = wgpu::Color::TRANSPARENT;
182
183        {
184            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
185                label: Some("render pass"),
186                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
187                    view: target_view,
188                    resolve_target: None,
189                    ops: wgpu::Operations {
190                        load: wgpu::LoadOp::Clear(clear_color),
191                        store: wgpu::StoreOp::Store,
192                    },
193                    depth_slice: None,
194                })],
195                depth_stencil_attachment: None,
196                timestamp_writes: None,
197                occlusion_query_set: None,
198            });
199
200            // Render background IMAGE (not solid color) via bg_image_pipeline at full opacity
201            if render_background_image && let Some(ref bg_bind_group) = self.bg_image_bind_group {
202                log::info!(
203                    "[BACKGROUND] render_to_texture: bg_image_pipeline (image, window_opacity={:.3} applied by shader)",
204                    saved_window_opacity
205                );
206                render_pass.set_pipeline(&self.bg_image_pipeline);
207                render_pass.set_bind_group(0, bg_bind_group, &[]);
208                render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
209                render_pass.draw(0..4, 0..1);
210            }
211
212            render_pass.set_pipeline(&self.bg_pipeline);
213            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
214            render_pass.set_vertex_buffer(1, self.bg_instance_buffer.slice(..));
215            render_pass.draw(0..4, 0..self.max_bg_instances as u32);
216
217            render_pass.set_pipeline(&self.text_pipeline);
218            render_pass.set_bind_group(0, &self.text_bind_group, &[]);
219            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
220            render_pass.set_vertex_buffer(1, self.text_instance_buffer.slice(..));
221            render_pass.draw(0..4, 0..self.max_text_instances as u32);
222        }
223
224        self.queue.submit(std::iter::once(encoder.finish()));
225
226        // Restore window_opacity and update uniforms
227        if render_background_image {
228            self.window_opacity = saved_window_opacity;
229            self.update_bg_image_uniforms();
230        }
231
232        Ok(output)
233    }
234
235    /// Render only the background (image or solid color) to a view.
236    ///
237    /// This is useful for split pane rendering where the background should be
238    /// rendered once full-screen before rendering each pane's cells on top.
239    ///
240    /// # Arguments
241    /// * `target_view` - The texture view to render to
242    /// * `clear_first` - If true, clear the surface before rendering
243    ///
244    /// # Returns
245    /// `true` if a background image was rendered, `false` if only clear color was used
246    pub fn render_background_only(
247        &self,
248        target_view: &wgpu::TextureView,
249        clear_first: bool,
250    ) -> Result<bool> {
251        let mut encoder = self
252            .device
253            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
254                label: Some("background only encoder"),
255            });
256
257        // Determine clear color and whether to use bg_image pipeline
258        let (clear_color, use_bg_image_pipeline) = if self.bg_is_solid_color {
259            (
260                wgpu::Color {
261                    r: self.solid_bg_color[0] as f64 * self.window_opacity as f64,
262                    g: self.solid_bg_color[1] as f64 * self.window_opacity as f64,
263                    b: self.solid_bg_color[2] as f64 * self.window_opacity as f64,
264                    a: self.window_opacity as f64,
265                },
266                false,
267            )
268        } else if self.bg_image_bind_group.is_some() {
269            (wgpu::Color::TRANSPARENT, true)
270        } else {
271            (
272                wgpu::Color {
273                    r: self.background_color[0] as f64 * self.window_opacity as f64,
274                    g: self.background_color[1] as f64 * self.window_opacity as f64,
275                    b: self.background_color[2] as f64 * self.window_opacity as f64,
276                    a: self.window_opacity as f64,
277                },
278                false,
279            )
280        };
281
282        let load_op = if clear_first {
283            wgpu::LoadOp::Clear(clear_color)
284        } else {
285            wgpu::LoadOp::Load
286        };
287
288        {
289            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
290                label: Some("background only render pass"),
291                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
292                    view: target_view,
293                    resolve_target: None,
294                    ops: wgpu::Operations {
295                        load: load_op,
296                        store: wgpu::StoreOp::Store,
297                    },
298                    depth_slice: None,
299                })],
300                depth_stencil_attachment: None,
301                timestamp_writes: None,
302                occlusion_query_set: None,
303            });
304
305            // Render background image if present
306            if use_bg_image_pipeline && let Some(ref bg_bind_group) = self.bg_image_bind_group {
307                render_pass.set_pipeline(&self.bg_image_pipeline);
308                render_pass.set_bind_group(0, bg_bind_group, &[]);
309                render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
310                render_pass.draw(0..4, 0..1);
311            }
312        }
313
314        self.queue.submit(std::iter::once(encoder.finish()));
315        Ok(use_bg_image_pipeline)
316    }
317
318    /// Render terminal content to a view for screenshots.
319    /// This renders without requiring the surface texture.
320    pub fn render_to_view(&self, target_view: &wgpu::TextureView) -> Result<()> {
321        // Note: We don't rebuild instance buffers here since this is typically called
322        // right after a normal render, and the buffers should already be up to date.
323
324        let mut encoder = self
325            .device
326            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
327                label: Some("screenshot render encoder"),
328            });
329
330        // Determine clear color and whether to use bg_image pipeline
331        let (clear_color, use_bg_image_pipeline) = if self.bg_is_solid_color {
332            (
333                wgpu::Color {
334                    r: self.solid_bg_color[0] as f64 * self.window_opacity as f64,
335                    g: self.solid_bg_color[1] as f64 * self.window_opacity as f64,
336                    b: self.solid_bg_color[2] as f64 * self.window_opacity as f64,
337                    a: self.window_opacity as f64,
338                },
339                false,
340            )
341        } else if self.bg_image_bind_group.is_some() {
342            (wgpu::Color::TRANSPARENT, true)
343        } else {
344            (
345                wgpu::Color {
346                    r: self.background_color[0] as f64 * self.window_opacity as f64,
347                    g: self.background_color[1] as f64 * self.window_opacity as f64,
348                    b: self.background_color[2] as f64 * self.window_opacity as f64,
349                    a: self.window_opacity as f64,
350                },
351                false,
352            )
353        };
354
355        {
356            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
357                label: Some("screenshot render pass"),
358                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
359                    view: target_view,
360                    resolve_target: None,
361                    ops: wgpu::Operations {
362                        load: wgpu::LoadOp::Clear(clear_color),
363                        store: wgpu::StoreOp::Store,
364                    },
365                    depth_slice: None,
366                })],
367                depth_stencil_attachment: None,
368                timestamp_writes: None,
369                occlusion_query_set: None,
370            });
371
372            // Render background image if present
373            if use_bg_image_pipeline && let Some(ref bg_bind_group) = self.bg_image_bind_group {
374                render_pass.set_pipeline(&self.bg_image_pipeline);
375                render_pass.set_bind_group(0, bg_bind_group, &[]);
376                render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
377                render_pass.draw(0..4, 0..1);
378            }
379
380            render_pass.set_pipeline(&self.bg_pipeline);
381            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
382            render_pass.set_vertex_buffer(1, self.bg_instance_buffer.slice(..));
383            render_pass.draw(0..4, 0..self.max_bg_instances as u32);
384
385            render_pass.set_pipeline(&self.text_pipeline);
386            render_pass.set_bind_group(0, &self.text_bind_group, &[]);
387            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
388            render_pass.set_vertex_buffer(1, self.text_instance_buffer.slice(..));
389            render_pass.draw(0..4, 0..self.max_text_instances as u32);
390
391            // Render scrollbar
392            self.scrollbar.render(&mut render_pass);
393        }
394
395        self.queue.submit(std::iter::once(encoder.finish()));
396        Ok(())
397    }
398
399    pub fn render_overlays(
400        &mut self,
401        surface_texture: &wgpu::SurfaceTexture,
402        show_scrollbar: bool,
403    ) -> Result<()> {
404        let view = surface_texture
405            .texture
406            .create_view(&wgpu::TextureViewDescriptor::default());
407        let mut encoder = self
408            .device
409            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
410                label: Some("overlay encoder"),
411            });
412
413        {
414            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
415                label: Some("overlay pass"),
416                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
417                    view: &view,
418                    resolve_target: None,
419                    ops: wgpu::Operations {
420                        load: wgpu::LoadOp::Load,
421                        store: wgpu::StoreOp::Store,
422                    },
423                    depth_slice: None,
424                })],
425                depth_stencil_attachment: None,
426                timestamp_writes: None,
427                occlusion_query_set: None,
428            });
429
430            if show_scrollbar {
431                self.scrollbar.render(&mut render_pass);
432            }
433
434            if self.visual_bell_intensity > 0.0 {
435                // Visual bell logic
436            }
437        }
438
439        self.queue.submit(std::iter::once(encoder.finish()));
440        Ok(())
441    }
442
443    pub(crate) fn build_instance_buffers(&mut self) -> Result<()> {
444        let _shaping_options = ShapingOptions {
445            enable_ligatures: self.enable_ligatures,
446            enable_kerning: self.enable_kerning,
447            ..Default::default()
448        };
449
450        for row in 0..self.rows {
451            if self.dirty_rows[row] || self.row_cache[row].is_none() {
452                let start = row * self.cols;
453                let end = (row + 1) * self.cols;
454                let row_cells = &self.cells[start..end];
455
456                let mut row_bg = Vec::with_capacity(self.cols);
457                let mut row_text = Vec::with_capacity(self.cols);
458
459                // Background - use RLE to merge consecutive cells with same color (like iTerm2)
460                // This eliminates seams between adjacent same-colored cells
461                let mut col = 0;
462                while col < row_cells.len() {
463                    let cell = &row_cells[col];
464                    let is_default_bg =
465                        (cell.bg_color[0] as f32 / 255.0 - self.background_color[0]).abs() < 0.001
466                            && (cell.bg_color[1] as f32 / 255.0 - self.background_color[1]).abs()
467                                < 0.001
468                            && (cell.bg_color[2] as f32 / 255.0 - self.background_color[2]).abs()
469                                < 0.001;
470
471                    // Check for cursor at this position, accounting for unfocused state
472                    let cursor_visible = self.cursor_opacity > 0.0
473                        && !self.cursor_hidden_for_shader
474                        && self.cursor_pos.1 == row
475                        && self.cursor_pos.0 == col;
476
477                    // Handle unfocused cursor visibility
478                    let has_cursor = if cursor_visible && !self.is_focused {
479                        match self.unfocused_cursor_style {
480                            par_term_config::UnfocusedCursorStyle::Hidden => false,
481                            par_term_config::UnfocusedCursorStyle::Hollow
482                            | par_term_config::UnfocusedCursorStyle::Same => true,
483                        }
484                    } else {
485                        cursor_visible
486                    };
487
488                    if is_default_bg && !has_cursor {
489                        col += 1;
490                        continue;
491                    }
492
493                    // Calculate background color with alpha
494                    let bg_alpha =
495                        if self.transparency_affects_only_default_background && !is_default_bg {
496                            1.0
497                        } else {
498                            self.window_opacity
499                        };
500                    let mut bg_color = [
501                        cell.bg_color[0] as f32 / 255.0,
502                        cell.bg_color[1] as f32 / 255.0,
503                        cell.bg_color[2] as f32 / 255.0,
504                        bg_alpha,
505                    ];
506
507                    // Handle cursor at this position
508                    if has_cursor && self.cursor_opacity > 0.0 {
509                        use par_term_emu_core_rust::cursor::CursorStyle;
510
511                        // Check if we should render hollow cursor (unfocused hollow style)
512                        let render_hollow = !self.is_focused
513                            && self.unfocused_cursor_style
514                                == par_term_config::UnfocusedCursorStyle::Hollow;
515
516                        match self.cursor_style {
517                            CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock => {
518                                if render_hollow {
519                                    // Hollow cursor: don't fill the cell, outline will be added later
520                                    // Keep original background color
521                                } else {
522                                    // Solid block cursor
523                                    for (bg, &cursor) in
524                                        bg_color.iter_mut().take(3).zip(&self.cursor_color)
525                                    {
526                                        *bg = *bg * (1.0 - self.cursor_opacity)
527                                            + cursor * self.cursor_opacity;
528                                    }
529                                    bg_color[3] = bg_color[3].max(self.cursor_opacity);
530                                }
531                            }
532                            _ => {}
533                        }
534                        // Cursor cell can't be merged, render it alone
535                        let x0 = self.window_padding
536                            + self.content_offset_x
537                            + col as f32 * self.cell_width;
538                        let x1 = self.window_padding
539                            + self.content_offset_x
540                            + (col + 1) as f32 * self.cell_width;
541                        let y0 = self.window_padding
542                            + self.content_offset_y
543                            + row as f32 * self.cell_height;
544                        let y1 = y0 + self.cell_height;
545                        row_bg.push(BackgroundInstance {
546                            position: [
547                                x0 / self.config.width as f32 * 2.0 - 1.0,
548                                1.0 - (y0 / self.config.height as f32 * 2.0),
549                            ],
550                            size: [
551                                (x1 - x0) / self.config.width as f32 * 2.0,
552                                (y1 - y0) / self.config.height as f32 * 2.0,
553                            ],
554                            color: bg_color,
555                        });
556                        col += 1;
557                        continue;
558                    }
559
560                    // RLE: Find run of consecutive cells with same background color
561                    let start_col = col;
562                    let run_color = cell.bg_color;
563                    col += 1;
564                    while col < row_cells.len() {
565                        let next_cell = &row_cells[col];
566                        let next_has_cursor = self.cursor_opacity > 0.0
567                            && !self.cursor_hidden_for_shader
568                            && self.cursor_pos.1 == row
569                            && self.cursor_pos.0 == col;
570                        // Stop run if color differs or cursor is here
571                        if next_cell.bg_color != run_color || next_has_cursor {
572                            break;
573                        }
574                        col += 1;
575                    }
576                    let run_length = col - start_col;
577
578                    // Create single quad spanning entire run (no per-cell rounding)
579                    let x0 = self.window_padding
580                        + self.content_offset_x
581                        + start_col as f32 * self.cell_width;
582                    let x1 = self.window_padding
583                        + self.content_offset_x
584                        + (start_col + run_length) as f32 * self.cell_width;
585                    let y0 =
586                        self.window_padding + self.content_offset_y + row as f32 * self.cell_height;
587                    let y1 = y0 + self.cell_height;
588
589                    row_bg.push(BackgroundInstance {
590                        position: [
591                            x0 / self.config.width as f32 * 2.0 - 1.0,
592                            1.0 - (y0 / self.config.height as f32 * 2.0),
593                        ],
594                        size: [
595                            (x1 - x0) / self.config.width as f32 * 2.0,
596                            (y1 - y0) / self.config.height as f32 * 2.0,
597                        ],
598                        color: bg_color,
599                    });
600                }
601
602                // Pad row_bg to expected size with empty instances
603                // (RLE creates fewer instances than cells, but buffer expects cols entries)
604                while row_bg.len() < self.cols {
605                    row_bg.push(BackgroundInstance {
606                        position: [0.0, 0.0],
607                        size: [0.0, 0.0],
608                        color: [0.0, 0.0, 0.0, 0.0],
609                    });
610                }
611
612                // Text
613                let mut x_offset = 0.0;
614                #[allow(clippy::type_complexity)]
615                let cell_data: Vec<(
616                    String,
617                    bool,
618                    bool,
619                    [u8; 4],
620                    [u8; 4],
621                    bool,
622                    bool,
623                )> = row_cells
624                    .iter()
625                    .map(|c| {
626                        (
627                            c.grapheme.clone(),
628                            c.bold,
629                            c.italic,
630                            c.fg_color,
631                            c.bg_color,
632                            c.wide_char_spacer,
633                            c.wide_char,
634                        )
635                    })
636                    .collect();
637
638                // Dynamic baseline calculation based on font metrics
639                let natural_line_height = self.font_ascent + self.font_descent + self.font_leading;
640                let vertical_padding = (self.cell_height - natural_line_height).max(0.0) / 2.0;
641                let baseline_y_unrounded = self.window_padding
642                    + self.content_offset_y
643                    + (row as f32 * self.cell_height)
644                    + vertical_padding
645                    + self.font_ascent;
646
647                // Check if this row has the cursor and it's a visible block cursor
648                // (for cursor text color override)
649                let cursor_is_block_on_this_row = {
650                    use par_term_emu_core_rust::cursor::CursorStyle;
651                    self.cursor_pos.1 == row
652                        && self.cursor_opacity > 0.0
653                        && !self.cursor_hidden_for_shader
654                        && matches!(
655                            self.cursor_style,
656                            CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock
657                        )
658                        && (self.is_focused
659                            || self.unfocused_cursor_style
660                                == par_term_config::UnfocusedCursorStyle::Same)
661                };
662
663                let mut current_col = 0usize;
664                for (grapheme, bold, italic, fg_color, bg_color, is_spacer, is_wide) in cell_data {
665                    if is_spacer || grapheme == " " {
666                        x_offset += self.cell_width;
667                        current_col += 1;
668                        continue;
669                    }
670
671                    // Compute text alpha - force opaque if keep_text_opaque is enabled,
672                    // otherwise use window opacity so text becomes transparent with the window
673                    let text_alpha = if self.keep_text_opaque {
674                        1.0
675                    } else {
676                        self.window_opacity
677                    };
678
679                    // Determine text color - use cursor_text_color if this is the cursor position
680                    // with a block cursor, otherwise use the cell's foreground color
681                    let render_fg_color: [f32; 4] =
682                        if cursor_is_block_on_this_row && current_col == self.cursor_pos.0 {
683                            if let Some(cursor_text) = self.cursor_text_color {
684                                [cursor_text[0], cursor_text[1], cursor_text[2], text_alpha]
685                            } else {
686                                // Auto-contrast: use cursor color as a starting point
687                                // Simple inversion: if cursor is bright, use dark text; if dark, use bright
688                                let cursor_brightness = (self.cursor_color[0]
689                                    + self.cursor_color[1]
690                                    + self.cursor_color[2])
691                                    / 3.0;
692                                if cursor_brightness > 0.5 {
693                                    [0.0, 0.0, 0.0, text_alpha] // Dark text on bright cursor
694                                } else {
695                                    [1.0, 1.0, 1.0, text_alpha] // Bright text on dark cursor
696                                }
697                            }
698                        } else {
699                            // Determine the effective background color for contrast calculation
700                            // If the cell has a non-default bg, use that; otherwise use terminal background
701                            let effective_bg = if bg_color[3] > 0 {
702                                // Cell has explicit background
703                                [
704                                    bg_color[0] as f32 / 255.0,
705                                    bg_color[1] as f32 / 255.0,
706                                    bg_color[2] as f32 / 255.0,
707                                    1.0,
708                                ]
709                            } else {
710                                // Use terminal default background
711                                [
712                                    self.background_color[0],
713                                    self.background_color[1],
714                                    self.background_color[2],
715                                    1.0,
716                                ]
717                            };
718
719                            let base_fg = [
720                                fg_color[0] as f32 / 255.0,
721                                fg_color[1] as f32 / 255.0,
722                                fg_color[2] as f32 / 255.0,
723                                text_alpha,
724                            ];
725
726                            // Apply minimum contrast adjustment if enabled
727                            self.ensure_minimum_contrast(base_fg, effective_bg)
728                        };
729
730                    let chars: Vec<char> = grapheme.chars().collect();
731                    #[allow(clippy::collapsible_if)]
732                    if let Some(ch) = chars.first() {
733                        // Classify the character for rendering optimization
734                        // Only classify based on first char for block drawing detection
735                        let char_type = block_chars::classify_char(*ch);
736
737                        // Check if we should render this character geometrically
738                        // (only for single-char graphemes that are block drawing chars)
739                        if chars.len() == 1 && block_chars::should_render_geometrically(char_type) {
740                            let char_w = if is_wide {
741                                self.cell_width * 2.0
742                            } else {
743                                self.cell_width
744                            };
745                            let x0 =
746                                (self.window_padding + self.content_offset_x + x_offset).round();
747                            let y0 = (self.window_padding
748                                + self.content_offset_y
749                                + row as f32 * self.cell_height)
750                                .round();
751
752                            // Try box drawing geometry first (for lines, corners, junctions)
753                            // Pass aspect ratio so vertical lines have same visual thickness as horizontal
754                            let aspect_ratio = self.cell_height / char_w;
755                            if let Some(box_geo) =
756                                block_chars::get_box_drawing_geometry(*ch, aspect_ratio)
757                            {
758                                for segment in &box_geo.segments {
759                                    let rect =
760                                        segment.to_pixel_rect(x0, y0, char_w, self.cell_height);
761
762                                    // Extend segments that touch cell edges
763                                    let extension = 1.0;
764                                    let ext_x = if segment.x <= 0.01 { extension } else { 0.0 };
765                                    let ext_y = if segment.y <= 0.01 { extension } else { 0.0 };
766                                    let ext_w = if segment.x + segment.width >= 0.99 {
767                                        extension
768                                    } else {
769                                        0.0
770                                    };
771                                    let ext_h = if segment.y + segment.height >= 0.99 {
772                                        extension
773                                    } else {
774                                        0.0
775                                    };
776
777                                    let final_x = rect.x - ext_x;
778                                    let final_y = rect.y - ext_y;
779                                    let final_w = rect.width + ext_x + ext_w;
780                                    let final_h = rect.height + ext_y + ext_h;
781
782                                    row_text.push(TextInstance {
783                                        position: [
784                                            final_x / self.config.width as f32 * 2.0 - 1.0,
785                                            1.0 - (final_y / self.config.height as f32 * 2.0),
786                                        ],
787                                        size: [
788                                            final_w / self.config.width as f32 * 2.0,
789                                            final_h / self.config.height as f32 * 2.0,
790                                        ],
791                                        tex_offset: [
792                                            self.solid_pixel_offset.0 as f32 / 2048.0,
793                                            self.solid_pixel_offset.1 as f32 / 2048.0,
794                                        ],
795                                        tex_size: [1.0 / 2048.0, 1.0 / 2048.0],
796                                        color: render_fg_color,
797                                        is_colored: 0,
798                                    });
799                                }
800                                x_offset += self.cell_width;
801                                current_col += 1;
802                                continue;
803                            }
804
805                            // Try block element geometry (for solid blocks, half blocks, etc.)
806                            if let Some(geo_block) = block_chars::get_geometric_block(*ch) {
807                                let rect =
808                                    geo_block.to_pixel_rect(x0, y0, char_w, self.cell_height);
809
810                                // Add small extension to prevent gaps (1 pixel overlap)
811                                let extension = 1.0;
812                                let ext_x = if geo_block.x == 0.0 { extension } else { 0.0 };
813                                let ext_y = if geo_block.y == 0.0 { extension } else { 0.0 };
814                                let ext_w = if geo_block.x + geo_block.width >= 1.0 {
815                                    extension
816                                } else {
817                                    0.0
818                                };
819                                let ext_h = if geo_block.y + geo_block.height >= 1.0 {
820                                    extension
821                                } else {
822                                    0.0
823                                };
824
825                                let final_x = rect.x - ext_x;
826                                let final_y = rect.y - ext_y;
827                                let final_w = rect.width + ext_x + ext_w;
828                                let final_h = rect.height + ext_y + ext_h;
829
830                                // Render as a colored rectangle using the solid white pixel in atlas
831                                // This goes through the text pipeline with foreground color
832                                row_text.push(TextInstance {
833                                    position: [
834                                        final_x / self.config.width as f32 * 2.0 - 1.0,
835                                        1.0 - (final_y / self.config.height as f32 * 2.0),
836                                    ],
837                                    size: [
838                                        final_w / self.config.width as f32 * 2.0,
839                                        final_h / self.config.height as f32 * 2.0,
840                                    ],
841                                    // Use solid white pixel from atlas
842                                    tex_offset: [
843                                        self.solid_pixel_offset.0 as f32 / 2048.0,
844                                        self.solid_pixel_offset.1 as f32 / 2048.0,
845                                    ],
846                                    tex_size: [1.0 / 2048.0, 1.0 / 2048.0],
847                                    color: render_fg_color,
848                                    is_colored: 0,
849                                });
850
851                                x_offset += self.cell_width;
852                                current_col += 1;
853                                continue;
854                            }
855                        }
856
857                        // Use grapheme-aware glyph lookup for multi-character sequences
858                        // (flags, emoji with skin tones, ZWJ sequences, combining chars)
859                        let glyph_result = if chars.len() > 1 {
860                            self.font_manager
861                                .find_grapheme_glyph(&grapheme, bold, italic)
862                        } else {
863                            self.font_manager.find_glyph(*ch, bold, italic)
864                        };
865
866                        if let Some((font_idx, glyph_id)) = glyph_result {
867                            let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
868                            let info = if self.glyph_cache.contains_key(&cache_key) {
869                                // Move to front of LRU
870                                self.lru_remove(cache_key);
871                                self.lru_push_front(cache_key);
872                                self.glyph_cache.get(&cache_key).unwrap().clone()
873                            } else if let Some(raster) = self.rasterize_glyph(font_idx, glyph_id) {
874                                let info = self.upload_glyph(cache_key, &raster);
875                                self.glyph_cache.insert(cache_key, info.clone());
876                                self.lru_push_front(cache_key);
877                                info
878                            } else {
879                                x_offset += self.cell_width;
880                                continue;
881                            };
882
883                            let char_w = if is_wide {
884                                self.cell_width * 2.0
885                            } else {
886                                self.cell_width
887                            };
888                            let x0 =
889                                (self.window_padding + self.content_offset_x + x_offset).round();
890                            let x1 =
891                                (self.window_padding + self.content_offset_x + x_offset + char_w)
892                                    .round();
893                            let y0 = (self.window_padding
894                                + self.content_offset_y
895                                + row as f32 * self.cell_height)
896                                .round();
897                            let y1 = (self.window_padding
898                                + self.content_offset_y
899                                + (row + 1) as f32 * self.cell_height)
900                                .round();
901
902                            let cell_w = x1 - x0;
903                            let cell_h = y1 - y0;
904
905                            let scale_x = cell_w / char_w;
906                            let scale_y = cell_h / self.cell_height;
907
908                            // Position glyph relative to snapped cell top-left.
909                            // Round the scaled baseline position once, then subtract
910                            // the integer bearing_y. This ensures all glyphs on a row
911                            // share the same rounded baseline, with bearing offsets
912                            // applied exactly (no scale_y on bearing avoids rounding
913                            // artifacts between glyphs with different bearings).
914                            let baseline_offset = baseline_y_unrounded
915                                - (self.window_padding
916                                    + self.content_offset_y
917                                    + row as f32 * self.cell_height);
918                            let glyph_left = x0 + (info.bearing_x * scale_x).round();
919                            let baseline_in_cell = (baseline_offset * scale_y).round();
920                            let glyph_top = y0 + baseline_in_cell - info.bearing_y;
921
922                            let render_w = info.width as f32 * scale_x;
923                            let render_h = info.height as f32 * scale_y;
924
925                            // For block characters that need font rendering (box drawing, etc.),
926                            // apply snapping to cell boundaries with sub-pixel extension.
927                            // Only apply to single-char graphemes (multi-char are never block chars)
928                            let (final_left, final_top, final_w, final_h) = if chars.len() == 1
929                                && block_chars::should_snap_to_boundaries(char_type)
930                            {
931                                // Snap threshold of 3 pixels, extension of 0.5 pixels
932                                block_chars::snap_glyph_to_cell(
933                                    glyph_left, glyph_top, render_w, render_h, x0, y0, x1, y1, 3.0,
934                                    0.5,
935                                )
936                            } else {
937                                (glyph_left, glyph_top, render_w, render_h)
938                            };
939
940                            row_text.push(TextInstance {
941                                position: [
942                                    final_left / self.config.width as f32 * 2.0 - 1.0,
943                                    1.0 - (final_top / self.config.height as f32 * 2.0),
944                                ],
945                                size: [
946                                    final_w / self.config.width as f32 * 2.0,
947                                    final_h / self.config.height as f32 * 2.0,
948                                ],
949                                tex_offset: [info.x as f32 / 2048.0, info.y as f32 / 2048.0],
950                                tex_size: [info.width as f32 / 2048.0, info.height as f32 / 2048.0],
951                                color: render_fg_color,
952                                is_colored: if info.is_colored { 1 } else { 0 },
953                            });
954                        }
955                    }
956                    x_offset += self.cell_width;
957                    current_col += 1;
958                }
959
960                // Update CPU-side buffers
961                let bg_start = row * self.cols;
962                self.bg_instances[bg_start..bg_start + self.cols].copy_from_slice(&row_bg);
963
964                let text_start = row * self.cols * 2;
965                // Clear row text segment first
966                for i in 0..(self.cols * 2) {
967                    self.text_instances[text_start + i].size = [0.0, 0.0];
968                }
969                // Copy new text instances
970                let text_count = row_text.len().min(self.cols * 2);
971                self.text_instances[text_start..text_start + text_count]
972                    .copy_from_slice(&row_text[..text_count]);
973
974                // Update GPU-side buffers incrementally
975                self.queue.write_buffer(
976                    &self.bg_instance_buffer,
977                    (bg_start * std::mem::size_of::<BackgroundInstance>()) as u64,
978                    bytemuck::cast_slice(&row_bg),
979                );
980                self.queue.write_buffer(
981                    &self.text_instance_buffer,
982                    (text_start * std::mem::size_of::<TextInstance>()) as u64,
983                    bytemuck::cast_slice(
984                        &self.text_instances[text_start..text_start + self.cols * 2],
985                    ),
986                );
987
988                self.row_cache[row] = Some(RowCacheEntry {});
989                self.dirty_rows[row] = false;
990            }
991        }
992
993        // Write cursor-related overlays to extra slots at the end of bg_instances
994        // Slot layout: [0] cursor overlay (beam/underline), [1] guide, [2] shadow, [3-6] boost glow, [7-10] hollow outline
995        let base_overlay_index = self.cols * self.rows;
996        let mut overlay_instances = vec![
997            BackgroundInstance {
998                position: [0.0, 0.0],
999                size: [0.0, 0.0],
1000                color: [0.0, 0.0, 0.0, 0.0],
1001            };
1002            10
1003        ];
1004
1005        // Check if cursor should be visible
1006        let cursor_visible = self.cursor_opacity > 0.0
1007            && !self.cursor_hidden_for_shader
1008            && (self.is_focused
1009                || self.unfocused_cursor_style != par_term_config::UnfocusedCursorStyle::Hidden);
1010
1011        // Calculate cursor pixel positions
1012        let cursor_col = self.cursor_pos.0;
1013        let cursor_row = self.cursor_pos.1;
1014        let cursor_x0 =
1015            self.window_padding + self.content_offset_x + cursor_col as f32 * self.cell_width;
1016        let cursor_x1 = cursor_x0 + self.cell_width;
1017        let cursor_y0 =
1018            self.window_padding + self.content_offset_y + cursor_row as f32 * self.cell_height;
1019        let cursor_y1 = cursor_y0 + self.cell_height;
1020
1021        // Slot 0: Cursor overlay (beam/underline) - handled by existing cursor_overlay
1022        overlay_instances[0] = self.cursor_overlay.unwrap_or(BackgroundInstance {
1023            position: [0.0, 0.0],
1024            size: [0.0, 0.0],
1025            color: [0.0, 0.0, 0.0, 0.0],
1026        });
1027
1028        // Slot 1: Cursor guide (horizontal line spanning full width at cursor row)
1029        if cursor_visible && self.cursor_guide_enabled {
1030            let guide_x0 = self.window_padding + self.content_offset_x;
1031            let guide_x1 =
1032                self.config.width as f32 - self.window_padding - self.content_inset_right;
1033            overlay_instances[1] = BackgroundInstance {
1034                position: [
1035                    guide_x0 / self.config.width as f32 * 2.0 - 1.0,
1036                    1.0 - (cursor_y0 / self.config.height as f32 * 2.0),
1037                ],
1038                size: [
1039                    (guide_x1 - guide_x0) / self.config.width as f32 * 2.0,
1040                    (cursor_y1 - cursor_y0) / self.config.height as f32 * 2.0,
1041                ],
1042                color: self.cursor_guide_color,
1043            };
1044        }
1045
1046        // Slot 2: Cursor shadow (offset rectangle behind cursor)
1047        if cursor_visible && self.cursor_shadow_enabled {
1048            let shadow_x0 = cursor_x0 + self.cursor_shadow_offset[0];
1049            let shadow_y0 = cursor_y0 + self.cursor_shadow_offset[1];
1050            overlay_instances[2] = BackgroundInstance {
1051                position: [
1052                    shadow_x0 / self.config.width as f32 * 2.0 - 1.0,
1053                    1.0 - (shadow_y0 / self.config.height as f32 * 2.0),
1054                ],
1055                size: [
1056                    self.cell_width / self.config.width as f32 * 2.0,
1057                    self.cell_height / self.config.height as f32 * 2.0,
1058                ],
1059                color: self.cursor_shadow_color,
1060            };
1061        }
1062
1063        // Slot 3: Cursor boost glow (larger rectangle around cursor with low opacity)
1064        if cursor_visible && self.cursor_boost > 0.0 {
1065            let glow_expand = 4.0 * self.scale_factor * self.cursor_boost; // Expand by up to 4 logical pixels
1066            let glow_x0 = cursor_x0 - glow_expand;
1067            let glow_y0 = cursor_y0 - glow_expand;
1068            let glow_w = self.cell_width + glow_expand * 2.0;
1069            let glow_h = self.cell_height + glow_expand * 2.0;
1070            overlay_instances[3] = BackgroundInstance {
1071                position: [
1072                    glow_x0 / self.config.width as f32 * 2.0 - 1.0,
1073                    1.0 - (glow_y0 / self.config.height as f32 * 2.0),
1074                ],
1075                size: [
1076                    glow_w / self.config.width as f32 * 2.0,
1077                    glow_h / self.config.height as f32 * 2.0,
1078                ],
1079                color: [
1080                    self.cursor_boost_color[0],
1081                    self.cursor_boost_color[1],
1082                    self.cursor_boost_color[2],
1083                    self.cursor_boost * 0.3 * self.cursor_opacity, // Max 30% alpha
1084                ],
1085            };
1086        }
1087
1088        // Slots 4-7: Hollow cursor outline (4 thin rectangles forming a border)
1089        // Rendered when unfocused with hollow style and block cursor
1090        let render_hollow = cursor_visible
1091            && !self.is_focused
1092            && self.unfocused_cursor_style == par_term_config::UnfocusedCursorStyle::Hollow;
1093
1094        if render_hollow {
1095            use par_term_emu_core_rust::cursor::CursorStyle;
1096            let is_block = matches!(
1097                self.cursor_style,
1098                CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock
1099            );
1100
1101            if is_block {
1102                let border_width = 2.0; // 2 pixel border
1103                let color = [
1104                    self.cursor_color[0],
1105                    self.cursor_color[1],
1106                    self.cursor_color[2],
1107                    self.cursor_opacity,
1108                ];
1109
1110                // Top border
1111                overlay_instances[4] = BackgroundInstance {
1112                    position: [
1113                        cursor_x0 / self.config.width as f32 * 2.0 - 1.0,
1114                        1.0 - (cursor_y0 / self.config.height as f32 * 2.0),
1115                    ],
1116                    size: [
1117                        self.cell_width / self.config.width as f32 * 2.0,
1118                        border_width / self.config.height as f32 * 2.0,
1119                    ],
1120                    color,
1121                };
1122
1123                // Bottom border
1124                overlay_instances[5] = BackgroundInstance {
1125                    position: [
1126                        cursor_x0 / self.config.width as f32 * 2.0 - 1.0,
1127                        1.0 - ((cursor_y1 - border_width) / self.config.height as f32 * 2.0),
1128                    ],
1129                    size: [
1130                        self.cell_width / self.config.width as f32 * 2.0,
1131                        border_width / self.config.height as f32 * 2.0,
1132                    ],
1133                    color,
1134                };
1135
1136                // Left border
1137                overlay_instances[6] = BackgroundInstance {
1138                    position: [
1139                        cursor_x0 / self.config.width as f32 * 2.0 - 1.0,
1140                        1.0 - ((cursor_y0 + border_width) / self.config.height as f32 * 2.0),
1141                    ],
1142                    size: [
1143                        border_width / self.config.width as f32 * 2.0,
1144                        (self.cell_height - border_width * 2.0) / self.config.height as f32 * 2.0,
1145                    ],
1146                    color,
1147                };
1148
1149                // Right border
1150                overlay_instances[7] = BackgroundInstance {
1151                    position: [
1152                        (cursor_x1 - border_width) / self.config.width as f32 * 2.0 - 1.0,
1153                        1.0 - ((cursor_y0 + border_width) / self.config.height as f32 * 2.0),
1154                    ],
1155                    size: [
1156                        border_width / self.config.width as f32 * 2.0,
1157                        (self.cell_height - border_width * 2.0) / self.config.height as f32 * 2.0,
1158                    ],
1159                    color,
1160                };
1161            }
1162        }
1163
1164        // Write all overlay instances to GPU buffer
1165        for (i, instance) in overlay_instances.iter().enumerate() {
1166            self.bg_instances[base_overlay_index + i] = *instance;
1167        }
1168        self.queue.write_buffer(
1169            &self.bg_instance_buffer,
1170            (base_overlay_index * std::mem::size_of::<BackgroundInstance>()) as u64,
1171            bytemuck::cast_slice(&overlay_instances),
1172        );
1173
1174        // Write command separator line instances after cursor overlay slots
1175        let separator_base = self.cols * self.rows + 10;
1176        let mut separator_instances = vec![
1177            BackgroundInstance {
1178                position: [0.0, 0.0],
1179                size: [0.0, 0.0],
1180                color: [0.0, 0.0, 0.0, 0.0],
1181            };
1182            self.rows
1183        ];
1184
1185        if self.command_separator_enabled {
1186            let width_f = self.config.width as f32;
1187            let height_f = self.config.height as f32;
1188            for &(screen_row, exit_code, custom_color) in &self.visible_separator_marks {
1189                if screen_row < self.rows {
1190                    let x0 = self.window_padding + self.content_offset_x;
1191                    let x1 = width_f - self.window_padding - self.content_inset_right;
1192                    let y0 = self.window_padding
1193                        + self.content_offset_y
1194                        + screen_row as f32 * self.cell_height;
1195                    let color = self.separator_color(exit_code, custom_color, 1.0);
1196                    separator_instances[screen_row] = BackgroundInstance {
1197                        position: [x0 / width_f * 2.0 - 1.0, 1.0 - (y0 / height_f * 2.0)],
1198                        size: [
1199                            (x1 - x0) / width_f * 2.0,
1200                            self.command_separator_thickness / height_f * 2.0,
1201                        ],
1202                        color,
1203                    };
1204                }
1205            }
1206        }
1207
1208        for (i, instance) in separator_instances.iter().enumerate() {
1209            if separator_base + i < self.max_bg_instances {
1210                self.bg_instances[separator_base + i] = *instance;
1211            }
1212        }
1213        let separator_byte_offset = separator_base * std::mem::size_of::<BackgroundInstance>();
1214        let separator_byte_count =
1215            separator_instances.len() * std::mem::size_of::<BackgroundInstance>();
1216        if separator_byte_offset + separator_byte_count
1217            <= self.max_bg_instances * std::mem::size_of::<BackgroundInstance>()
1218        {
1219            self.queue.write_buffer(
1220                &self.bg_instance_buffer,
1221                separator_byte_offset as u64,
1222                bytemuck::cast_slice(&separator_instances),
1223            );
1224        }
1225
1226        Ok(())
1227    }
1228
1229    /// Render a single pane's content within a viewport to an existing surface texture
1230    ///
1231    /// This method renders cells to a specific region of the render target,
1232    /// using a GPU scissor rect to clip to the pane bounds.
1233    ///
1234    /// # Arguments
1235    /// * `surface_view` - The texture view to render to
1236    /// * `viewport` - The pane's viewport (position, size, focus state, opacity)
1237    /// * `cells` - The cells to render (should match viewport grid size)
1238    /// * `cols` - Number of columns in the cell grid
1239    /// * `rows` - Number of rows in the cell grid
1240    /// * `cursor_pos` - Cursor position (col, row) within this pane, or None if no cursor
1241    /// * `cursor_opacity` - Cursor opacity (0.0 = hidden, 1.0 = fully visible)
1242    /// * `show_scrollbar` - Whether to render the scrollbar for this pane
1243    /// * `clear_first` - If true, clears the viewport region before rendering
1244    /// * `skip_background_image` - If true, skip rendering the background image. Use this
1245    ///   when the background image has already been rendered full-screen (for split panes).
1246    #[allow(dead_code, clippy::too_many_arguments)]
1247    pub fn render_pane_to_view(
1248        &mut self,
1249        surface_view: &wgpu::TextureView,
1250        viewport: &PaneViewport,
1251        cells: &[Cell],
1252        cols: usize,
1253        rows: usize,
1254        cursor_pos: Option<(usize, usize)>,
1255        cursor_opacity: f32,
1256        show_scrollbar: bool,
1257        clear_first: bool,
1258        skip_background_image: bool,
1259        separator_marks: &[SeparatorMark],
1260        pane_background: Option<&par_term_config::PaneBackground>,
1261    ) -> Result<()> {
1262        // Build instance buffers for this pane's cells
1263        // Skip solid background fill if background (shader/image) was already rendered full-screen
1264        self.build_pane_instance_buffers(
1265            viewport,
1266            cells,
1267            cols,
1268            rows,
1269            cursor_pos,
1270            cursor_opacity,
1271            skip_background_image,
1272            separator_marks,
1273        )?;
1274
1275        // Pre-create per-pane background bind group if needed (must happen before render pass).
1276        // Per-pane backgrounds are explicit user overrides and always created,
1277        // even when a custom shader or global background would normally be skipped.
1278        let pane_bg_resources = if let Some(pane_bg) = pane_background
1279            && let Some(ref path) = pane_bg.image_path
1280        {
1281            self.pane_bg_cache.get(path.as_str()).map(|entry| {
1282                self.create_pane_bg_bind_group(
1283                    entry,
1284                    viewport.x,
1285                    viewport.y,
1286                    viewport.width,
1287                    viewport.height,
1288                    pane_bg.mode,
1289                    pane_bg.opacity,
1290                )
1291            })
1292        } else {
1293            None
1294        };
1295
1296        let mut encoder = self
1297            .device
1298            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1299                label: Some("pane render encoder"),
1300            });
1301
1302        // Determine load operation and clear color
1303        let load_op = if clear_first {
1304            let clear_color = if self.bg_is_solid_color {
1305                wgpu::Color {
1306                    r: self.solid_bg_color[0] as f64
1307                        * self.window_opacity as f64
1308                        * viewport.opacity as f64,
1309                    g: self.solid_bg_color[1] as f64
1310                        * self.window_opacity as f64
1311                        * viewport.opacity as f64,
1312                    b: self.solid_bg_color[2] as f64
1313                        * self.window_opacity as f64
1314                        * viewport.opacity as f64,
1315                    a: self.window_opacity as f64 * viewport.opacity as f64,
1316                }
1317            } else {
1318                wgpu::Color {
1319                    r: self.background_color[0] as f64
1320                        * self.window_opacity as f64
1321                        * viewport.opacity as f64,
1322                    g: self.background_color[1] as f64
1323                        * self.window_opacity as f64
1324                        * viewport.opacity as f64,
1325                    b: self.background_color[2] as f64
1326                        * self.window_opacity as f64
1327                        * viewport.opacity as f64,
1328                    a: self.window_opacity as f64 * viewport.opacity as f64,
1329                }
1330            };
1331            wgpu::LoadOp::Clear(clear_color)
1332        } else {
1333            wgpu::LoadOp::Load
1334        };
1335
1336        {
1337            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1338                label: Some("pane render pass"),
1339                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1340                    view: surface_view,
1341                    resolve_target: None,
1342                    ops: wgpu::Operations {
1343                        load: load_op,
1344                        store: wgpu::StoreOp::Store,
1345                    },
1346                    depth_slice: None,
1347                })],
1348                depth_stencil_attachment: None,
1349                timestamp_writes: None,
1350                occlusion_query_set: None,
1351            });
1352
1353            // Set scissor rect to clip rendering to pane bounds
1354            let (sx, sy, sw, sh) = viewport.to_scissor_rect();
1355            render_pass.set_scissor_rect(sx, sy, sw, sh);
1356
1357            // Render per-pane background image within scissor rect.
1358            // Per-pane backgrounds are explicit user overrides and always render,
1359            // even when a custom shader or global background is active.
1360            if let Some((ref bind_group, ref _buf)) = pane_bg_resources {
1361                render_pass.set_pipeline(&self.bg_image_pipeline);
1362                render_pass.set_bind_group(0, bind_group, &[]);
1363                render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1364                render_pass.draw(0..4, 0..1);
1365            }
1366
1367            // Render cell backgrounds
1368            render_pass.set_pipeline(&self.bg_pipeline);
1369            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1370            render_pass.set_vertex_buffer(1, self.bg_instance_buffer.slice(..));
1371            render_pass.draw(0..4, 0..self.max_bg_instances as u32);
1372
1373            // Render text
1374            render_pass.set_pipeline(&self.text_pipeline);
1375            render_pass.set_bind_group(0, &self.text_bind_group, &[]);
1376            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1377            render_pass.set_vertex_buffer(1, self.text_instance_buffer.slice(..));
1378            render_pass.draw(0..4, 0..self.max_text_instances as u32);
1379
1380            // Render scrollbar if requested (uses its own scissor rect internally)
1381            if show_scrollbar {
1382                // Reset scissor to full surface for scrollbar
1383                render_pass.set_scissor_rect(0, 0, self.config.width, self.config.height);
1384                self.scrollbar.render(&mut render_pass);
1385            }
1386        }
1387
1388        self.queue.submit(std::iter::once(encoder.finish()));
1389        Ok(())
1390    }
1391
1392    /// Build instance buffers for a pane's cells with viewport offset
1393    ///
1394    /// This is similar to `build_instance_buffers` but adjusts all positions
1395    /// to be relative to the viewport origin.
1396    ///
1397    /// # Arguments
1398    /// * `skip_solid_background` - If true, skip adding a solid background fill for the viewport.
1399    ///   Use when a custom shader or background image was already rendered full-screen.
1400    #[allow(clippy::too_many_arguments)]
1401    fn build_pane_instance_buffers(
1402        &mut self,
1403        viewport: &PaneViewport,
1404        cells: &[Cell],
1405        cols: usize,
1406        rows: usize,
1407        cursor_pos: Option<(usize, usize)>,
1408        cursor_opacity: f32,
1409        skip_solid_background: bool,
1410        separator_marks: &[SeparatorMark],
1411    ) -> Result<()> {
1412        let _shaping_options = ShapingOptions {
1413            enable_ligatures: self.enable_ligatures,
1414            enable_kerning: self.enable_kerning,
1415            ..Default::default()
1416        };
1417
1418        // Clear previous instance buffers
1419        for instance in &mut self.bg_instances {
1420            instance.size = [0.0, 0.0];
1421            instance.color = [0.0, 0.0, 0.0, 0.0];
1422        }
1423
1424        // Add a background rectangle covering the entire pane viewport (unless skipped)
1425        // This ensures the pane has a proper background even when cells are skipped.
1426        // Skip when a custom shader or background image was already rendered full-screen.
1427        let bg_start_index = if !skip_solid_background && !self.bg_instances.is_empty() {
1428            let bg_color = self.background_color;
1429            let opacity = self.window_opacity * viewport.opacity;
1430            self.bg_instances[0] = super::types::BackgroundInstance {
1431                position: [viewport.x, viewport.y],
1432                size: [viewport.width, viewport.height],
1433                color: [
1434                    bg_color[0] * opacity,
1435                    bg_color[1] * opacity,
1436                    bg_color[2] * opacity,
1437                    opacity,
1438                ],
1439            };
1440            1 // Start cell backgrounds at index 1
1441        } else {
1442            0 // Start cell backgrounds at index 0 (no viewport fill)
1443        };
1444
1445        for instance in &mut self.text_instances {
1446            instance.size = [0.0, 0.0];
1447        }
1448
1449        // Start at bg_start_index (1 if viewport fill was added, 0 otherwise)
1450        let mut bg_index = bg_start_index;
1451        let mut text_index = 0;
1452
1453        // Content offset - positions are relative to content area (with padding applied)
1454        let (content_x, content_y) = viewport.content_origin();
1455        let opacity_multiplier = viewport.opacity;
1456
1457        for row in 0..rows {
1458            let row_start = row * cols;
1459            let row_end = (row + 1) * cols;
1460            if row_start >= cells.len() {
1461                break;
1462            }
1463            let row_cells = &cells[row_start..row_end.min(cells.len())];
1464
1465            // Background - use RLE to merge consecutive cells with same color
1466            let mut col = 0;
1467            while col < row_cells.len() {
1468                let cell = &row_cells[col];
1469                let is_default_bg = (cell.bg_color[0] as f32 / 255.0 - self.background_color[0])
1470                    .abs()
1471                    < 0.001
1472                    && (cell.bg_color[1] as f32 / 255.0 - self.background_color[1]).abs() < 0.001
1473                    && (cell.bg_color[2] as f32 / 255.0 - self.background_color[2]).abs() < 0.001;
1474
1475                // Check for cursor at this position
1476                let has_cursor = cursor_pos.is_some_and(|(cx, cy)| cx == col && cy == row)
1477                    && cursor_opacity > 0.0
1478                    && !self.cursor_hidden_for_shader;
1479
1480                if is_default_bg && !has_cursor {
1481                    col += 1;
1482                    continue;
1483                }
1484
1485                // Calculate background color with alpha and pane opacity
1486                let bg_alpha =
1487                    if self.transparency_affects_only_default_background && !is_default_bg {
1488                        1.0
1489                    } else {
1490                        self.window_opacity
1491                    };
1492                let pane_alpha = bg_alpha * opacity_multiplier;
1493                let mut bg_color = [
1494                    cell.bg_color[0] as f32 / 255.0,
1495                    cell.bg_color[1] as f32 / 255.0,
1496                    cell.bg_color[2] as f32 / 255.0,
1497                    pane_alpha,
1498                ];
1499
1500                // Handle cursor at this position
1501                if has_cursor {
1502                    use par_term_emu_core_rust::cursor::CursorStyle;
1503                    match self.cursor_style {
1504                        CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock => {
1505                            for (bg, &cursor) in bg_color.iter_mut().take(3).zip(&self.cursor_color)
1506                            {
1507                                *bg = *bg * (1.0 - cursor_opacity) + cursor * cursor_opacity;
1508                            }
1509                            bg_color[3] = bg_color[3].max(cursor_opacity * opacity_multiplier);
1510                        }
1511                        _ => {}
1512                    }
1513
1514                    // Cursor cell can't be merged
1515                    let x0 = content_x + col as f32 * self.cell_width;
1516                    let y0 = content_y + row as f32 * self.cell_height;
1517                    let x1 = x0 + self.cell_width;
1518                    let y1 = y0 + self.cell_height;
1519
1520                    if bg_index < self.max_bg_instances {
1521                        self.bg_instances[bg_index] = BackgroundInstance {
1522                            position: [
1523                                x0 / self.config.width as f32 * 2.0 - 1.0,
1524                                1.0 - (y0 / self.config.height as f32 * 2.0),
1525                            ],
1526                            size: [
1527                                (x1 - x0) / self.config.width as f32 * 2.0,
1528                                (y1 - y0) / self.config.height as f32 * 2.0,
1529                            ],
1530                            color: bg_color,
1531                        };
1532                        bg_index += 1;
1533                    }
1534                    col += 1;
1535                    continue;
1536                }
1537
1538                // RLE: Find run of consecutive cells with same background color
1539                let start_col = col;
1540                let run_color = cell.bg_color;
1541                col += 1;
1542                while col < row_cells.len() {
1543                    let next_cell = &row_cells[col];
1544                    let next_has_cursor = cursor_pos.is_some_and(|(cx, cy)| cx == col && cy == row)
1545                        && cursor_opacity > 0.0;
1546                    if next_cell.bg_color != run_color || next_has_cursor {
1547                        break;
1548                    }
1549                    col += 1;
1550                }
1551                let run_length = col - start_col;
1552
1553                // Create single quad spanning entire run
1554                let x0 = content_x + start_col as f32 * self.cell_width;
1555                let x1 = content_x + (start_col + run_length) as f32 * self.cell_width;
1556                let y0 = content_y + row as f32 * self.cell_height;
1557                let y1 = y0 + self.cell_height;
1558
1559                if bg_index < self.max_bg_instances {
1560                    self.bg_instances[bg_index] = BackgroundInstance {
1561                        position: [
1562                            x0 / self.config.width as f32 * 2.0 - 1.0,
1563                            1.0 - (y0 / self.config.height as f32 * 2.0),
1564                        ],
1565                        size: [
1566                            (x1 - x0) / self.config.width as f32 * 2.0,
1567                            (y1 - y0) / self.config.height as f32 * 2.0,
1568                        ],
1569                        color: bg_color,
1570                    };
1571                    bg_index += 1;
1572                }
1573            }
1574
1575            // Text rendering
1576            let natural_line_height = self.font_ascent + self.font_descent + self.font_leading;
1577            let vertical_padding = (self.cell_height - natural_line_height).max(0.0) / 2.0;
1578            let baseline_y =
1579                content_y + (row as f32 * self.cell_height) + vertical_padding + self.font_ascent;
1580
1581            // Compute text alpha - force opaque if keep_text_opaque is enabled
1582            let text_alpha = if self.keep_text_opaque {
1583                opacity_multiplier // Only apply pane dimming, not window transparency
1584            } else {
1585                self.window_opacity * opacity_multiplier
1586            };
1587
1588            for (col_idx, cell) in row_cells.iter().enumerate() {
1589                if cell.wide_char_spacer || cell.grapheme == " " {
1590                    continue;
1591                }
1592
1593                let chars: Vec<char> = cell.grapheme.chars().collect();
1594                if chars.is_empty() {
1595                    continue;
1596                }
1597
1598                let ch = chars[0];
1599
1600                // Check for block characters that should be rendered geometrically
1601                let char_type = block_chars::classify_char(ch);
1602                if chars.len() == 1 && block_chars::should_render_geometrically(char_type) {
1603                    let char_w = if cell.wide_char {
1604                        self.cell_width * 2.0
1605                    } else {
1606                        self.cell_width
1607                    };
1608                    let x0 = content_x + col_idx as f32 * self.cell_width;
1609                    let y0 = content_y + row as f32 * self.cell_height;
1610
1611                    let fg_color = [
1612                        cell.fg_color[0] as f32 / 255.0,
1613                        cell.fg_color[1] as f32 / 255.0,
1614                        cell.fg_color[2] as f32 / 255.0,
1615                        text_alpha,
1616                    ];
1617
1618                    // Try box drawing geometry first
1619                    let aspect_ratio = self.cell_height / char_w;
1620                    if let Some(box_geo) = block_chars::get_box_drawing_geometry(ch, aspect_ratio) {
1621                        for segment in &box_geo.segments {
1622                            let rect = segment.to_pixel_rect(x0, y0, char_w, self.cell_height);
1623
1624                            // Extension for seamless lines
1625                            let extension = 1.0;
1626                            let ext_x = if segment.x <= 0.01 { extension } else { 0.0 };
1627                            let ext_y = if segment.y <= 0.01 { extension } else { 0.0 };
1628                            let ext_w = if segment.x + segment.width >= 0.99 {
1629                                extension
1630                            } else {
1631                                0.0
1632                            };
1633                            let ext_h = if segment.y + segment.height >= 0.99 {
1634                                extension
1635                            } else {
1636                                0.0
1637                            };
1638
1639                            let final_x = rect.x - ext_x;
1640                            let final_y = rect.y - ext_y;
1641                            let final_w = rect.width + ext_x + ext_w;
1642                            let final_h = rect.height + ext_y + ext_h;
1643
1644                            if text_index < self.max_text_instances {
1645                                self.text_instances[text_index] = TextInstance {
1646                                    position: [
1647                                        final_x / self.config.width as f32 * 2.0 - 1.0,
1648                                        1.0 - (final_y / self.config.height as f32 * 2.0),
1649                                    ],
1650                                    size: [
1651                                        final_w / self.config.width as f32 * 2.0,
1652                                        final_h / self.config.height as f32 * 2.0,
1653                                    ],
1654                                    tex_offset: [
1655                                        self.solid_pixel_offset.0 as f32 / 2048.0,
1656                                        self.solid_pixel_offset.1 as f32 / 2048.0,
1657                                    ],
1658                                    tex_size: [1.0 / 2048.0, 1.0 / 2048.0],
1659                                    color: fg_color,
1660                                    is_colored: 0,
1661                                };
1662                                text_index += 1;
1663                            }
1664                        }
1665                        continue;
1666                    }
1667
1668                    // Try block element geometry
1669                    if let Some(geo_block) = block_chars::get_geometric_block(ch) {
1670                        let rect = geo_block.to_pixel_rect(x0, y0, char_w, self.cell_height);
1671
1672                        // Extension for seamless blocks
1673                        let extension = 1.0;
1674                        let ext_x = if geo_block.x == 0.0 { extension } else { 0.0 };
1675                        let ext_y = if geo_block.y == 0.0 { extension } else { 0.0 };
1676                        let ext_w = if geo_block.x + geo_block.width >= 1.0 {
1677                            extension
1678                        } else {
1679                            0.0
1680                        };
1681                        let ext_h = if geo_block.y + geo_block.height >= 1.0 {
1682                            extension
1683                        } else {
1684                            0.0
1685                        };
1686
1687                        let final_x = rect.x - ext_x;
1688                        let final_y = rect.y - ext_y;
1689                        let final_w = rect.width + ext_x + ext_w;
1690                        let final_h = rect.height + ext_y + ext_h;
1691
1692                        if text_index < self.max_text_instances {
1693                            self.text_instances[text_index] = TextInstance {
1694                                position: [
1695                                    final_x / self.config.width as f32 * 2.0 - 1.0,
1696                                    1.0 - (final_y / self.config.height as f32 * 2.0),
1697                                ],
1698                                size: [
1699                                    final_w / self.config.width as f32 * 2.0,
1700                                    final_h / self.config.height as f32 * 2.0,
1701                                ],
1702                                tex_offset: [
1703                                    self.solid_pixel_offset.0 as f32 / 2048.0,
1704                                    self.solid_pixel_offset.1 as f32 / 2048.0,
1705                                ],
1706                                tex_size: [1.0 / 2048.0, 1.0 / 2048.0],
1707                                color: fg_color,
1708                                is_colored: 0,
1709                            };
1710                            text_index += 1;
1711                        }
1712                        continue;
1713                    }
1714                }
1715
1716                // Regular glyph rendering
1717                let glyph_result = if chars.len() > 1 {
1718                    self.font_manager
1719                        .find_grapheme_glyph(&cell.grapheme, cell.bold, cell.italic)
1720                } else {
1721                    self.font_manager.find_glyph(ch, cell.bold, cell.italic)
1722                };
1723
1724                if let Some((font_idx, glyph_id)) = glyph_result {
1725                    let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
1726                    let info = if self.glyph_cache.contains_key(&cache_key) {
1727                        self.lru_remove(cache_key);
1728                        self.lru_push_front(cache_key);
1729                        self.glyph_cache.get(&cache_key).unwrap().clone()
1730                    } else if let Some(raster) = self.rasterize_glyph(font_idx, glyph_id) {
1731                        let info = self.upload_glyph(cache_key, &raster);
1732                        self.glyph_cache.insert(cache_key, info.clone());
1733                        self.lru_push_front(cache_key);
1734                        info
1735                    } else {
1736                        continue;
1737                    };
1738
1739                    let char_w = if cell.wide_char {
1740                        self.cell_width * 2.0
1741                    } else {
1742                        self.cell_width
1743                    };
1744                    let x0 = content_x + col_idx as f32 * self.cell_width;
1745                    let y0 = content_y + row as f32 * self.cell_height;
1746                    let x1 = x0 + char_w;
1747                    let y1 = y0 + self.cell_height;
1748
1749                    let cell_w = x1 - x0;
1750                    let cell_h = y1 - y0;
1751                    let scale_x = cell_w / char_w;
1752                    let scale_y = cell_h / self.cell_height;
1753
1754                    let baseline_offset = baseline_y - (content_y + row as f32 * self.cell_height);
1755                    let glyph_left = x0 + (info.bearing_x * scale_x).round();
1756                    let baseline_in_cell = (baseline_offset * scale_y).round();
1757                    let glyph_top = y0 + baseline_in_cell - info.bearing_y;
1758
1759                    let render_w = info.width as f32 * scale_x;
1760                    let render_h = info.height as f32 * scale_y;
1761
1762                    let (final_left, final_top, final_w, final_h) =
1763                        if chars.len() == 1 && block_chars::should_snap_to_boundaries(char_type) {
1764                            block_chars::snap_glyph_to_cell(
1765                                glyph_left, glyph_top, render_w, render_h, x0, y0, x1, y1, 3.0, 0.5,
1766                            )
1767                        } else {
1768                            (glyph_left, glyph_top, render_w, render_h)
1769                        };
1770
1771                    let fg_color = [
1772                        cell.fg_color[0] as f32 / 255.0,
1773                        cell.fg_color[1] as f32 / 255.0,
1774                        cell.fg_color[2] as f32 / 255.0,
1775                        text_alpha,
1776                    ];
1777
1778                    if text_index < self.max_text_instances {
1779                        self.text_instances[text_index] = TextInstance {
1780                            position: [
1781                                final_left / self.config.width as f32 * 2.0 - 1.0,
1782                                1.0 - (final_top / self.config.height as f32 * 2.0),
1783                            ],
1784                            size: [
1785                                final_w / self.config.width as f32 * 2.0,
1786                                final_h / self.config.height as f32 * 2.0,
1787                            ],
1788                            tex_offset: [info.x as f32 / 2048.0, info.y as f32 / 2048.0],
1789                            tex_size: [info.width as f32 / 2048.0, info.height as f32 / 2048.0],
1790                            color: fg_color,
1791                            is_colored: if info.is_colored { 1 } else { 0 },
1792                        };
1793                        text_index += 1;
1794                    }
1795                }
1796            }
1797        }
1798
1799        // Inject command separator line instances for split panes
1800        if self.command_separator_enabled && !separator_marks.is_empty() {
1801            let width_f = self.config.width as f32;
1802            let height_f = self.config.height as f32;
1803            let opacity_multiplier = viewport.opacity;
1804            for &(screen_row, exit_code, custom_color) in separator_marks {
1805                if screen_row < rows && bg_index < self.max_bg_instances {
1806                    let x0 = content_x;
1807                    let x1 = content_x + cols as f32 * self.cell_width;
1808                    let y0 = content_y + screen_row as f32 * self.cell_height;
1809                    let color = self.separator_color(exit_code, custom_color, opacity_multiplier);
1810                    self.bg_instances[bg_index] = BackgroundInstance {
1811                        position: [x0 / width_f * 2.0 - 1.0, 1.0 - (y0 / height_f * 2.0)],
1812                        size: [
1813                            (x1 - x0) / width_f * 2.0,
1814                            self.command_separator_thickness / height_f * 2.0,
1815                        ],
1816                        color,
1817                    };
1818                    bg_index += 1;
1819                }
1820            }
1821        }
1822        let _ = bg_index; // suppress unused warning
1823
1824        // Upload instance buffers to GPU
1825        self.queue.write_buffer(
1826            &self.bg_instance_buffer,
1827            0,
1828            bytemuck::cast_slice(&self.bg_instances),
1829        );
1830        self.queue.write_buffer(
1831            &self.text_instance_buffer,
1832            0,
1833            bytemuck::cast_slice(&self.text_instances),
1834        );
1835
1836        Ok(())
1837    }
1838}