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                            // Check if this character should be rendered as a monochrome symbol
869                            // (dingbats, etc.) rather than colorful emoji
870                            let force_monochrome =
871                                chars.len() == 1 && super::atlas::should_render_as_symbol(*ch);
872                            let info = if self.glyph_cache.contains_key(&cache_key) {
873                                // Move to front of LRU
874                                self.lru_remove(cache_key);
875                                self.lru_push_front(cache_key);
876                                self.glyph_cache.get(&cache_key).unwrap().clone()
877                            } else if let Some(raster) =
878                                self.rasterize_glyph(font_idx, glyph_id, force_monochrome)
879                            {
880                                let info = self.upload_glyph(cache_key, &raster);
881                                self.glyph_cache.insert(cache_key, info.clone());
882                                self.lru_push_front(cache_key);
883                                info
884                            } else {
885                                x_offset += self.cell_width;
886                                continue;
887                            };
888
889                            let char_w = if is_wide {
890                                self.cell_width * 2.0
891                            } else {
892                                self.cell_width
893                            };
894                            let x0 =
895                                (self.window_padding + self.content_offset_x + x_offset).round();
896                            let x1 =
897                                (self.window_padding + self.content_offset_x + x_offset + char_w)
898                                    .round();
899                            let y0 = (self.window_padding
900                                + self.content_offset_y
901                                + row as f32 * self.cell_height)
902                                .round();
903                            let y1 = (self.window_padding
904                                + self.content_offset_y
905                                + (row + 1) as f32 * self.cell_height)
906                                .round();
907
908                            let cell_w = x1 - x0;
909                            let cell_h = y1 - y0;
910
911                            let scale_x = cell_w / char_w;
912                            let scale_y = cell_h / self.cell_height;
913
914                            // Position glyph relative to snapped cell top-left.
915                            // Round the scaled baseline position once, then subtract
916                            // the integer bearing_y. This ensures all glyphs on a row
917                            // share the same rounded baseline, with bearing offsets
918                            // applied exactly (no scale_y on bearing avoids rounding
919                            // artifacts between glyphs with different bearings).
920                            let baseline_offset = baseline_y_unrounded
921                                - (self.window_padding
922                                    + self.content_offset_y
923                                    + row as f32 * self.cell_height);
924                            let glyph_left = x0 + (info.bearing_x * scale_x).round();
925                            let baseline_in_cell = (baseline_offset * scale_y).round();
926                            let glyph_top = y0 + baseline_in_cell - info.bearing_y;
927
928                            let render_w = info.width as f32 * scale_x;
929                            let render_h = info.height as f32 * scale_y;
930
931                            // For block characters that need font rendering (box drawing, etc.),
932                            // apply snapping to cell boundaries with sub-pixel extension.
933                            // Only apply to single-char graphemes (multi-char are never block chars)
934                            let (final_left, final_top, final_w, final_h) = if chars.len() == 1
935                                && block_chars::should_snap_to_boundaries(char_type)
936                            {
937                                // Snap threshold of 3 pixels, extension of 0.5 pixels
938                                block_chars::snap_glyph_to_cell(
939                                    glyph_left, glyph_top, render_w, render_h, x0, y0, x1, y1, 3.0,
940                                    0.5,
941                                )
942                            } else {
943                                (glyph_left, glyph_top, render_w, render_h)
944                            };
945
946                            row_text.push(TextInstance {
947                                position: [
948                                    final_left / self.config.width as f32 * 2.0 - 1.0,
949                                    1.0 - (final_top / self.config.height as f32 * 2.0),
950                                ],
951                                size: [
952                                    final_w / self.config.width as f32 * 2.0,
953                                    final_h / self.config.height as f32 * 2.0,
954                                ],
955                                tex_offset: [info.x as f32 / 2048.0, info.y as f32 / 2048.0],
956                                tex_size: [info.width as f32 / 2048.0, info.height as f32 / 2048.0],
957                                color: render_fg_color,
958                                is_colored: if info.is_colored { 1 } else { 0 },
959                            });
960                        }
961                    }
962                    x_offset += self.cell_width;
963                    current_col += 1;
964                }
965
966                // Update CPU-side buffers
967                let bg_start = row * self.cols;
968                self.bg_instances[bg_start..bg_start + self.cols].copy_from_slice(&row_bg);
969
970                let text_start = row * self.cols * 2;
971                // Clear row text segment first
972                for i in 0..(self.cols * 2) {
973                    self.text_instances[text_start + i].size = [0.0, 0.0];
974                }
975                // Copy new text instances
976                let text_count = row_text.len().min(self.cols * 2);
977                self.text_instances[text_start..text_start + text_count]
978                    .copy_from_slice(&row_text[..text_count]);
979
980                // Update GPU-side buffers incrementally
981                self.queue.write_buffer(
982                    &self.bg_instance_buffer,
983                    (bg_start * std::mem::size_of::<BackgroundInstance>()) as u64,
984                    bytemuck::cast_slice(&row_bg),
985                );
986                self.queue.write_buffer(
987                    &self.text_instance_buffer,
988                    (text_start * std::mem::size_of::<TextInstance>()) as u64,
989                    bytemuck::cast_slice(
990                        &self.text_instances[text_start..text_start + self.cols * 2],
991                    ),
992                );
993
994                self.row_cache[row] = Some(RowCacheEntry {});
995                self.dirty_rows[row] = false;
996            }
997        }
998
999        // Write cursor-related overlays to extra slots at the end of bg_instances
1000        // Slot layout: [0] cursor overlay (beam/underline), [1] guide, [2] shadow, [3-6] boost glow, [7-10] hollow outline
1001        let base_overlay_index = self.cols * self.rows;
1002        let mut overlay_instances = vec![
1003            BackgroundInstance {
1004                position: [0.0, 0.0],
1005                size: [0.0, 0.0],
1006                color: [0.0, 0.0, 0.0, 0.0],
1007            };
1008            10
1009        ];
1010
1011        // Check if cursor should be visible
1012        let cursor_visible = self.cursor_opacity > 0.0
1013            && !self.cursor_hidden_for_shader
1014            && (self.is_focused
1015                || self.unfocused_cursor_style != par_term_config::UnfocusedCursorStyle::Hidden);
1016
1017        // Calculate cursor pixel positions
1018        let cursor_col = self.cursor_pos.0;
1019        let cursor_row = self.cursor_pos.1;
1020        let cursor_x0 =
1021            self.window_padding + self.content_offset_x + cursor_col as f32 * self.cell_width;
1022        let cursor_x1 = cursor_x0 + self.cell_width;
1023        let cursor_y0 =
1024            self.window_padding + self.content_offset_y + cursor_row as f32 * self.cell_height;
1025        let cursor_y1 = cursor_y0 + self.cell_height;
1026
1027        // Slot 0: Cursor overlay (beam/underline) - handled by existing cursor_overlay
1028        overlay_instances[0] = self.cursor_overlay.unwrap_or(BackgroundInstance {
1029            position: [0.0, 0.0],
1030            size: [0.0, 0.0],
1031            color: [0.0, 0.0, 0.0, 0.0],
1032        });
1033
1034        // Slot 1: Cursor guide (horizontal line spanning full width at cursor row)
1035        if cursor_visible && self.cursor_guide_enabled {
1036            let guide_x0 = self.window_padding + self.content_offset_x;
1037            let guide_x1 =
1038                self.config.width as f32 - self.window_padding - self.content_inset_right;
1039            overlay_instances[1] = BackgroundInstance {
1040                position: [
1041                    guide_x0 / self.config.width as f32 * 2.0 - 1.0,
1042                    1.0 - (cursor_y0 / self.config.height as f32 * 2.0),
1043                ],
1044                size: [
1045                    (guide_x1 - guide_x0) / self.config.width as f32 * 2.0,
1046                    (cursor_y1 - cursor_y0) / self.config.height as f32 * 2.0,
1047                ],
1048                color: self.cursor_guide_color,
1049            };
1050        }
1051
1052        // Slot 2: Cursor shadow (offset rectangle behind cursor)
1053        if cursor_visible && self.cursor_shadow_enabled {
1054            let shadow_x0 = cursor_x0 + self.cursor_shadow_offset[0];
1055            let shadow_y0 = cursor_y0 + self.cursor_shadow_offset[1];
1056            overlay_instances[2] = BackgroundInstance {
1057                position: [
1058                    shadow_x0 / self.config.width as f32 * 2.0 - 1.0,
1059                    1.0 - (shadow_y0 / self.config.height as f32 * 2.0),
1060                ],
1061                size: [
1062                    self.cell_width / self.config.width as f32 * 2.0,
1063                    self.cell_height / self.config.height as f32 * 2.0,
1064                ],
1065                color: self.cursor_shadow_color,
1066            };
1067        }
1068
1069        // Slot 3: Cursor boost glow (larger rectangle around cursor with low opacity)
1070        if cursor_visible && self.cursor_boost > 0.0 {
1071            let glow_expand = 4.0 * self.scale_factor * self.cursor_boost; // Expand by up to 4 logical pixels
1072            let glow_x0 = cursor_x0 - glow_expand;
1073            let glow_y0 = cursor_y0 - glow_expand;
1074            let glow_w = self.cell_width + glow_expand * 2.0;
1075            let glow_h = self.cell_height + glow_expand * 2.0;
1076            overlay_instances[3] = BackgroundInstance {
1077                position: [
1078                    glow_x0 / self.config.width as f32 * 2.0 - 1.0,
1079                    1.0 - (glow_y0 / self.config.height as f32 * 2.0),
1080                ],
1081                size: [
1082                    glow_w / self.config.width as f32 * 2.0,
1083                    glow_h / self.config.height as f32 * 2.0,
1084                ],
1085                color: [
1086                    self.cursor_boost_color[0],
1087                    self.cursor_boost_color[1],
1088                    self.cursor_boost_color[2],
1089                    self.cursor_boost * 0.3 * self.cursor_opacity, // Max 30% alpha
1090                ],
1091            };
1092        }
1093
1094        // Slots 4-7: Hollow cursor outline (4 thin rectangles forming a border)
1095        // Rendered when unfocused with hollow style and block cursor
1096        let render_hollow = cursor_visible
1097            && !self.is_focused
1098            && self.unfocused_cursor_style == par_term_config::UnfocusedCursorStyle::Hollow;
1099
1100        if render_hollow {
1101            use par_term_emu_core_rust::cursor::CursorStyle;
1102            let is_block = matches!(
1103                self.cursor_style,
1104                CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock
1105            );
1106
1107            if is_block {
1108                let border_width = 2.0; // 2 pixel border
1109                let color = [
1110                    self.cursor_color[0],
1111                    self.cursor_color[1],
1112                    self.cursor_color[2],
1113                    self.cursor_opacity,
1114                ];
1115
1116                // Top border
1117                overlay_instances[4] = BackgroundInstance {
1118                    position: [
1119                        cursor_x0 / self.config.width as f32 * 2.0 - 1.0,
1120                        1.0 - (cursor_y0 / self.config.height as f32 * 2.0),
1121                    ],
1122                    size: [
1123                        self.cell_width / self.config.width as f32 * 2.0,
1124                        border_width / self.config.height as f32 * 2.0,
1125                    ],
1126                    color,
1127                };
1128
1129                // Bottom border
1130                overlay_instances[5] = BackgroundInstance {
1131                    position: [
1132                        cursor_x0 / self.config.width as f32 * 2.0 - 1.0,
1133                        1.0 - ((cursor_y1 - border_width) / self.config.height as f32 * 2.0),
1134                    ],
1135                    size: [
1136                        self.cell_width / self.config.width as f32 * 2.0,
1137                        border_width / self.config.height as f32 * 2.0,
1138                    ],
1139                    color,
1140                };
1141
1142                // Left border
1143                overlay_instances[6] = BackgroundInstance {
1144                    position: [
1145                        cursor_x0 / self.config.width as f32 * 2.0 - 1.0,
1146                        1.0 - ((cursor_y0 + border_width) / self.config.height as f32 * 2.0),
1147                    ],
1148                    size: [
1149                        border_width / self.config.width as f32 * 2.0,
1150                        (self.cell_height - border_width * 2.0) / self.config.height as f32 * 2.0,
1151                    ],
1152                    color,
1153                };
1154
1155                // Right border
1156                overlay_instances[7] = BackgroundInstance {
1157                    position: [
1158                        (cursor_x1 - border_width) / self.config.width as f32 * 2.0 - 1.0,
1159                        1.0 - ((cursor_y0 + border_width) / self.config.height as f32 * 2.0),
1160                    ],
1161                    size: [
1162                        border_width / self.config.width as f32 * 2.0,
1163                        (self.cell_height - border_width * 2.0) / self.config.height as f32 * 2.0,
1164                    ],
1165                    color,
1166                };
1167            }
1168        }
1169
1170        // Write all overlay instances to GPU buffer
1171        for (i, instance) in overlay_instances.iter().enumerate() {
1172            self.bg_instances[base_overlay_index + i] = *instance;
1173        }
1174        self.queue.write_buffer(
1175            &self.bg_instance_buffer,
1176            (base_overlay_index * std::mem::size_of::<BackgroundInstance>()) as u64,
1177            bytemuck::cast_slice(&overlay_instances),
1178        );
1179
1180        // Write command separator line instances after cursor overlay slots
1181        let separator_base = self.cols * self.rows + 10;
1182        let mut separator_instances = vec![
1183            BackgroundInstance {
1184                position: [0.0, 0.0],
1185                size: [0.0, 0.0],
1186                color: [0.0, 0.0, 0.0, 0.0],
1187            };
1188            self.rows
1189        ];
1190
1191        if self.command_separator_enabled {
1192            let width_f = self.config.width as f32;
1193            let height_f = self.config.height as f32;
1194            for &(screen_row, exit_code, custom_color) in &self.visible_separator_marks {
1195                if screen_row < self.rows {
1196                    let x0 = self.window_padding + self.content_offset_x;
1197                    let x1 = width_f - self.window_padding - self.content_inset_right;
1198                    let y0 = self.window_padding
1199                        + self.content_offset_y
1200                        + screen_row as f32 * self.cell_height;
1201                    let color = self.separator_color(exit_code, custom_color, 1.0);
1202                    separator_instances[screen_row] = BackgroundInstance {
1203                        position: [x0 / width_f * 2.0 - 1.0, 1.0 - (y0 / height_f * 2.0)],
1204                        size: [
1205                            (x1 - x0) / width_f * 2.0,
1206                            self.command_separator_thickness / height_f * 2.0,
1207                        ],
1208                        color,
1209                    };
1210                }
1211            }
1212        }
1213
1214        for (i, instance) in separator_instances.iter().enumerate() {
1215            if separator_base + i < self.max_bg_instances {
1216                self.bg_instances[separator_base + i] = *instance;
1217            }
1218        }
1219        let separator_byte_offset = separator_base * std::mem::size_of::<BackgroundInstance>();
1220        let separator_byte_count =
1221            separator_instances.len() * std::mem::size_of::<BackgroundInstance>();
1222        if separator_byte_offset + separator_byte_count
1223            <= self.max_bg_instances * std::mem::size_of::<BackgroundInstance>()
1224        {
1225            self.queue.write_buffer(
1226                &self.bg_instance_buffer,
1227                separator_byte_offset as u64,
1228                bytemuck::cast_slice(&separator_instances),
1229            );
1230        }
1231
1232        Ok(())
1233    }
1234
1235    /// Render a single pane's content within a viewport to an existing surface texture
1236    ///
1237    /// This method renders cells to a specific region of the render target,
1238    /// using a GPU scissor rect to clip to the pane bounds.
1239    ///
1240    /// # Arguments
1241    /// * `surface_view` - The texture view to render to
1242    /// * `viewport` - The pane's viewport (position, size, focus state, opacity)
1243    /// * `cells` - The cells to render (should match viewport grid size)
1244    /// * `cols` - Number of columns in the cell grid
1245    /// * `rows` - Number of rows in the cell grid
1246    /// * `cursor_pos` - Cursor position (col, row) within this pane, or None if no cursor
1247    /// * `cursor_opacity` - Cursor opacity (0.0 = hidden, 1.0 = fully visible)
1248    /// * `show_scrollbar` - Whether to render the scrollbar for this pane
1249    /// * `clear_first` - If true, clears the viewport region before rendering
1250    /// * `skip_background_image` - If true, skip rendering the background image. Use this
1251    ///   when the background image has already been rendered full-screen (for split panes).
1252    #[allow(dead_code, clippy::too_many_arguments)]
1253    pub fn render_pane_to_view(
1254        &mut self,
1255        surface_view: &wgpu::TextureView,
1256        viewport: &PaneViewport,
1257        cells: &[Cell],
1258        cols: usize,
1259        rows: usize,
1260        cursor_pos: Option<(usize, usize)>,
1261        cursor_opacity: f32,
1262        show_scrollbar: bool,
1263        clear_first: bool,
1264        skip_background_image: bool,
1265        separator_marks: &[SeparatorMark],
1266        pane_background: Option<&par_term_config::PaneBackground>,
1267    ) -> Result<()> {
1268        // Build instance buffers for this pane's cells
1269        // Skip solid background fill if background (shader/image) was already rendered full-screen
1270        self.build_pane_instance_buffers(
1271            viewport,
1272            cells,
1273            cols,
1274            rows,
1275            cursor_pos,
1276            cursor_opacity,
1277            skip_background_image,
1278            separator_marks,
1279        )?;
1280
1281        // Pre-create per-pane background bind group if needed (must happen before render pass).
1282        // Per-pane backgrounds are explicit user overrides and always created,
1283        // even when a custom shader or global background would normally be skipped.
1284        let pane_bg_resources = if let Some(pane_bg) = pane_background
1285            && let Some(ref path) = pane_bg.image_path
1286        {
1287            self.pane_bg_cache.get(path.as_str()).map(|entry| {
1288                self.create_pane_bg_bind_group(
1289                    entry,
1290                    viewport.x,
1291                    viewport.y,
1292                    viewport.width,
1293                    viewport.height,
1294                    pane_bg.mode,
1295                    pane_bg.opacity,
1296                )
1297            })
1298        } else {
1299            None
1300        };
1301
1302        let mut encoder = self
1303            .device
1304            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1305                label: Some("pane render encoder"),
1306            });
1307
1308        // Determine load operation and clear color
1309        let load_op = if clear_first {
1310            let clear_color = if self.bg_is_solid_color {
1311                wgpu::Color {
1312                    r: self.solid_bg_color[0] as f64
1313                        * self.window_opacity as f64
1314                        * viewport.opacity as f64,
1315                    g: self.solid_bg_color[1] as f64
1316                        * self.window_opacity as f64
1317                        * viewport.opacity as f64,
1318                    b: self.solid_bg_color[2] as f64
1319                        * self.window_opacity as f64
1320                        * viewport.opacity as f64,
1321                    a: self.window_opacity as f64 * viewport.opacity as f64,
1322                }
1323            } else {
1324                wgpu::Color {
1325                    r: self.background_color[0] as f64
1326                        * self.window_opacity as f64
1327                        * viewport.opacity as f64,
1328                    g: self.background_color[1] as f64
1329                        * self.window_opacity as f64
1330                        * viewport.opacity as f64,
1331                    b: self.background_color[2] as f64
1332                        * self.window_opacity as f64
1333                        * viewport.opacity as f64,
1334                    a: self.window_opacity as f64 * viewport.opacity as f64,
1335                }
1336            };
1337            wgpu::LoadOp::Clear(clear_color)
1338        } else {
1339            wgpu::LoadOp::Load
1340        };
1341
1342        {
1343            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1344                label: Some("pane render pass"),
1345                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1346                    view: surface_view,
1347                    resolve_target: None,
1348                    ops: wgpu::Operations {
1349                        load: load_op,
1350                        store: wgpu::StoreOp::Store,
1351                    },
1352                    depth_slice: None,
1353                })],
1354                depth_stencil_attachment: None,
1355                timestamp_writes: None,
1356                occlusion_query_set: None,
1357            });
1358
1359            // Set scissor rect to clip rendering to pane bounds
1360            let (sx, sy, sw, sh) = viewport.to_scissor_rect();
1361            render_pass.set_scissor_rect(sx, sy, sw, sh);
1362
1363            // Render per-pane background image within scissor rect.
1364            // Per-pane backgrounds are explicit user overrides and always render,
1365            // even when a custom shader or global background is active.
1366            if let Some((ref bind_group, ref _buf)) = pane_bg_resources {
1367                render_pass.set_pipeline(&self.bg_image_pipeline);
1368                render_pass.set_bind_group(0, bind_group, &[]);
1369                render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1370                render_pass.draw(0..4, 0..1);
1371            }
1372
1373            // Render cell backgrounds
1374            render_pass.set_pipeline(&self.bg_pipeline);
1375            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1376            render_pass.set_vertex_buffer(1, self.bg_instance_buffer.slice(..));
1377            render_pass.draw(0..4, 0..self.max_bg_instances as u32);
1378
1379            // Render text
1380            render_pass.set_pipeline(&self.text_pipeline);
1381            render_pass.set_bind_group(0, &self.text_bind_group, &[]);
1382            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1383            render_pass.set_vertex_buffer(1, self.text_instance_buffer.slice(..));
1384            render_pass.draw(0..4, 0..self.max_text_instances as u32);
1385
1386            // Render scrollbar if requested (uses its own scissor rect internally)
1387            if show_scrollbar {
1388                // Reset scissor to full surface for scrollbar
1389                render_pass.set_scissor_rect(0, 0, self.config.width, self.config.height);
1390                self.scrollbar.render(&mut render_pass);
1391            }
1392        }
1393
1394        self.queue.submit(std::iter::once(encoder.finish()));
1395        Ok(())
1396    }
1397
1398    /// Build instance buffers for a pane's cells with viewport offset
1399    ///
1400    /// This is similar to `build_instance_buffers` but adjusts all positions
1401    /// to be relative to the viewport origin.
1402    ///
1403    /// # Arguments
1404    /// * `skip_solid_background` - If true, skip adding a solid background fill for the viewport.
1405    ///   Use when a custom shader or background image was already rendered full-screen.
1406    #[allow(clippy::too_many_arguments)]
1407    fn build_pane_instance_buffers(
1408        &mut self,
1409        viewport: &PaneViewport,
1410        cells: &[Cell],
1411        cols: usize,
1412        rows: usize,
1413        cursor_pos: Option<(usize, usize)>,
1414        cursor_opacity: f32,
1415        skip_solid_background: bool,
1416        separator_marks: &[SeparatorMark],
1417    ) -> Result<()> {
1418        let _shaping_options = ShapingOptions {
1419            enable_ligatures: self.enable_ligatures,
1420            enable_kerning: self.enable_kerning,
1421            ..Default::default()
1422        };
1423
1424        // Clear previous instance buffers
1425        for instance in &mut self.bg_instances {
1426            instance.size = [0.0, 0.0];
1427            instance.color = [0.0, 0.0, 0.0, 0.0];
1428        }
1429
1430        // Add a background rectangle covering the entire pane viewport (unless skipped)
1431        // This ensures the pane has a proper background even when cells are skipped.
1432        // Skip when a custom shader or background image was already rendered full-screen.
1433        let bg_start_index = if !skip_solid_background && !self.bg_instances.is_empty() {
1434            let bg_color = self.background_color;
1435            let opacity = self.window_opacity * viewport.opacity;
1436            self.bg_instances[0] = super::types::BackgroundInstance {
1437                position: [viewport.x, viewport.y],
1438                size: [viewport.width, viewport.height],
1439                color: [
1440                    bg_color[0] * opacity,
1441                    bg_color[1] * opacity,
1442                    bg_color[2] * opacity,
1443                    opacity,
1444                ],
1445            };
1446            1 // Start cell backgrounds at index 1
1447        } else {
1448            0 // Start cell backgrounds at index 0 (no viewport fill)
1449        };
1450
1451        for instance in &mut self.text_instances {
1452            instance.size = [0.0, 0.0];
1453        }
1454
1455        // Start at bg_start_index (1 if viewport fill was added, 0 otherwise)
1456        let mut bg_index = bg_start_index;
1457        let mut text_index = 0;
1458
1459        // Content offset - positions are relative to content area (with padding applied)
1460        let (content_x, content_y) = viewport.content_origin();
1461        let opacity_multiplier = viewport.opacity;
1462
1463        for row in 0..rows {
1464            let row_start = row * cols;
1465            let row_end = (row + 1) * cols;
1466            if row_start >= cells.len() {
1467                break;
1468            }
1469            let row_cells = &cells[row_start..row_end.min(cells.len())];
1470
1471            // Background - use RLE to merge consecutive cells with same color
1472            let mut col = 0;
1473            while col < row_cells.len() {
1474                let cell = &row_cells[col];
1475                let is_default_bg = (cell.bg_color[0] as f32 / 255.0 - self.background_color[0])
1476                    .abs()
1477                    < 0.001
1478                    && (cell.bg_color[1] as f32 / 255.0 - self.background_color[1]).abs() < 0.001
1479                    && (cell.bg_color[2] as f32 / 255.0 - self.background_color[2]).abs() < 0.001;
1480
1481                // Check for cursor at this position
1482                let has_cursor = cursor_pos.is_some_and(|(cx, cy)| cx == col && cy == row)
1483                    && cursor_opacity > 0.0
1484                    && !self.cursor_hidden_for_shader;
1485
1486                if is_default_bg && !has_cursor {
1487                    col += 1;
1488                    continue;
1489                }
1490
1491                // Calculate background color with alpha and pane opacity
1492                let bg_alpha =
1493                    if self.transparency_affects_only_default_background && !is_default_bg {
1494                        1.0
1495                    } else {
1496                        self.window_opacity
1497                    };
1498                let pane_alpha = bg_alpha * opacity_multiplier;
1499                let mut bg_color = [
1500                    cell.bg_color[0] as f32 / 255.0,
1501                    cell.bg_color[1] as f32 / 255.0,
1502                    cell.bg_color[2] as f32 / 255.0,
1503                    pane_alpha,
1504                ];
1505
1506                // Handle cursor at this position
1507                if has_cursor {
1508                    use par_term_emu_core_rust::cursor::CursorStyle;
1509                    match self.cursor_style {
1510                        CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock => {
1511                            for (bg, &cursor) in bg_color.iter_mut().take(3).zip(&self.cursor_color)
1512                            {
1513                                *bg = *bg * (1.0 - cursor_opacity) + cursor * cursor_opacity;
1514                            }
1515                            bg_color[3] = bg_color[3].max(cursor_opacity * opacity_multiplier);
1516                        }
1517                        _ => {}
1518                    }
1519
1520                    // Cursor cell can't be merged
1521                    let x0 = content_x + col as f32 * self.cell_width;
1522                    let y0 = content_y + row as f32 * self.cell_height;
1523                    let x1 = x0 + self.cell_width;
1524                    let y1 = y0 + self.cell_height;
1525
1526                    if bg_index < self.max_bg_instances {
1527                        self.bg_instances[bg_index] = BackgroundInstance {
1528                            position: [
1529                                x0 / self.config.width as f32 * 2.0 - 1.0,
1530                                1.0 - (y0 / self.config.height as f32 * 2.0),
1531                            ],
1532                            size: [
1533                                (x1 - x0) / self.config.width as f32 * 2.0,
1534                                (y1 - y0) / self.config.height as f32 * 2.0,
1535                            ],
1536                            color: bg_color,
1537                        };
1538                        bg_index += 1;
1539                    }
1540                    col += 1;
1541                    continue;
1542                }
1543
1544                // RLE: Find run of consecutive cells with same background color
1545                let start_col = col;
1546                let run_color = cell.bg_color;
1547                col += 1;
1548                while col < row_cells.len() {
1549                    let next_cell = &row_cells[col];
1550                    let next_has_cursor = cursor_pos.is_some_and(|(cx, cy)| cx == col && cy == row)
1551                        && cursor_opacity > 0.0;
1552                    if next_cell.bg_color != run_color || next_has_cursor {
1553                        break;
1554                    }
1555                    col += 1;
1556                }
1557                let run_length = col - start_col;
1558
1559                // Create single quad spanning entire run
1560                let x0 = content_x + start_col as f32 * self.cell_width;
1561                let x1 = content_x + (start_col + run_length) as f32 * self.cell_width;
1562                let y0 = content_y + row as f32 * self.cell_height;
1563                let y1 = y0 + self.cell_height;
1564
1565                if bg_index < self.max_bg_instances {
1566                    self.bg_instances[bg_index] = BackgroundInstance {
1567                        position: [
1568                            x0 / self.config.width as f32 * 2.0 - 1.0,
1569                            1.0 - (y0 / self.config.height as f32 * 2.0),
1570                        ],
1571                        size: [
1572                            (x1 - x0) / self.config.width as f32 * 2.0,
1573                            (y1 - y0) / self.config.height as f32 * 2.0,
1574                        ],
1575                        color: bg_color,
1576                    };
1577                    bg_index += 1;
1578                }
1579            }
1580
1581            // Text rendering
1582            let natural_line_height = self.font_ascent + self.font_descent + self.font_leading;
1583            let vertical_padding = (self.cell_height - natural_line_height).max(0.0) / 2.0;
1584            let baseline_y =
1585                content_y + (row as f32 * self.cell_height) + vertical_padding + self.font_ascent;
1586
1587            // Compute text alpha - force opaque if keep_text_opaque is enabled
1588            let text_alpha = if self.keep_text_opaque {
1589                opacity_multiplier // Only apply pane dimming, not window transparency
1590            } else {
1591                self.window_opacity * opacity_multiplier
1592            };
1593
1594            for (col_idx, cell) in row_cells.iter().enumerate() {
1595                if cell.wide_char_spacer || cell.grapheme == " " {
1596                    continue;
1597                }
1598
1599                let chars: Vec<char> = cell.grapheme.chars().collect();
1600                if chars.is_empty() {
1601                    continue;
1602                }
1603
1604                let ch = chars[0];
1605
1606                // Check for block characters that should be rendered geometrically
1607                let char_type = block_chars::classify_char(ch);
1608                if chars.len() == 1 && block_chars::should_render_geometrically(char_type) {
1609                    let char_w = if cell.wide_char {
1610                        self.cell_width * 2.0
1611                    } else {
1612                        self.cell_width
1613                    };
1614                    let x0 = content_x + col_idx as f32 * self.cell_width;
1615                    let y0 = content_y + row as f32 * self.cell_height;
1616
1617                    let fg_color = [
1618                        cell.fg_color[0] as f32 / 255.0,
1619                        cell.fg_color[1] as f32 / 255.0,
1620                        cell.fg_color[2] as f32 / 255.0,
1621                        text_alpha,
1622                    ];
1623
1624                    // Try box drawing geometry first
1625                    let aspect_ratio = self.cell_height / char_w;
1626                    if let Some(box_geo) = block_chars::get_box_drawing_geometry(ch, aspect_ratio) {
1627                        for segment in &box_geo.segments {
1628                            let rect = segment.to_pixel_rect(x0, y0, char_w, self.cell_height);
1629
1630                            // Extension for seamless lines
1631                            let extension = 1.0;
1632                            let ext_x = if segment.x <= 0.01 { extension } else { 0.0 };
1633                            let ext_y = if segment.y <= 0.01 { extension } else { 0.0 };
1634                            let ext_w = if segment.x + segment.width >= 0.99 {
1635                                extension
1636                            } else {
1637                                0.0
1638                            };
1639                            let ext_h = if segment.y + segment.height >= 0.99 {
1640                                extension
1641                            } else {
1642                                0.0
1643                            };
1644
1645                            let final_x = rect.x - ext_x;
1646                            let final_y = rect.y - ext_y;
1647                            let final_w = rect.width + ext_x + ext_w;
1648                            let final_h = rect.height + ext_y + ext_h;
1649
1650                            if text_index < self.max_text_instances {
1651                                self.text_instances[text_index] = TextInstance {
1652                                    position: [
1653                                        final_x / self.config.width as f32 * 2.0 - 1.0,
1654                                        1.0 - (final_y / self.config.height as f32 * 2.0),
1655                                    ],
1656                                    size: [
1657                                        final_w / self.config.width as f32 * 2.0,
1658                                        final_h / self.config.height as f32 * 2.0,
1659                                    ],
1660                                    tex_offset: [
1661                                        self.solid_pixel_offset.0 as f32 / 2048.0,
1662                                        self.solid_pixel_offset.1 as f32 / 2048.0,
1663                                    ],
1664                                    tex_size: [1.0 / 2048.0, 1.0 / 2048.0],
1665                                    color: fg_color,
1666                                    is_colored: 0,
1667                                };
1668                                text_index += 1;
1669                            }
1670                        }
1671                        continue;
1672                    }
1673
1674                    // Try block element geometry
1675                    if let Some(geo_block) = block_chars::get_geometric_block(ch) {
1676                        let rect = geo_block.to_pixel_rect(x0, y0, char_w, self.cell_height);
1677
1678                        // Extension for seamless blocks
1679                        let extension = 1.0;
1680                        let ext_x = if geo_block.x == 0.0 { extension } else { 0.0 };
1681                        let ext_y = if geo_block.y == 0.0 { extension } else { 0.0 };
1682                        let ext_w = if geo_block.x + geo_block.width >= 1.0 {
1683                            extension
1684                        } else {
1685                            0.0
1686                        };
1687                        let ext_h = if geo_block.y + geo_block.height >= 1.0 {
1688                            extension
1689                        } else {
1690                            0.0
1691                        };
1692
1693                        let final_x = rect.x - ext_x;
1694                        let final_y = rect.y - ext_y;
1695                        let final_w = rect.width + ext_x + ext_w;
1696                        let final_h = rect.height + ext_y + ext_h;
1697
1698                        if text_index < self.max_text_instances {
1699                            self.text_instances[text_index] = TextInstance {
1700                                position: [
1701                                    final_x / self.config.width as f32 * 2.0 - 1.0,
1702                                    1.0 - (final_y / self.config.height as f32 * 2.0),
1703                                ],
1704                                size: [
1705                                    final_w / self.config.width as f32 * 2.0,
1706                                    final_h / self.config.height as f32 * 2.0,
1707                                ],
1708                                tex_offset: [
1709                                    self.solid_pixel_offset.0 as f32 / 2048.0,
1710                                    self.solid_pixel_offset.1 as f32 / 2048.0,
1711                                ],
1712                                tex_size: [1.0 / 2048.0, 1.0 / 2048.0],
1713                                color: fg_color,
1714                                is_colored: 0,
1715                            };
1716                            text_index += 1;
1717                        }
1718                        continue;
1719                    }
1720                }
1721
1722                // Regular glyph rendering
1723                let glyph_result = if chars.len() > 1 {
1724                    self.font_manager
1725                        .find_grapheme_glyph(&cell.grapheme, cell.bold, cell.italic)
1726                } else {
1727                    self.font_manager.find_glyph(ch, cell.bold, cell.italic)
1728                };
1729
1730                if let Some((font_idx, glyph_id)) = glyph_result {
1731                    let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
1732                    // Check if this character should be rendered as a monochrome symbol
1733                    // (dingbats, etc.) rather than colorful emoji
1734                    let force_monochrome =
1735                        chars.len() == 1 && super::atlas::should_render_as_symbol(ch);
1736                    let info = if self.glyph_cache.contains_key(&cache_key) {
1737                        self.lru_remove(cache_key);
1738                        self.lru_push_front(cache_key);
1739                        self.glyph_cache.get(&cache_key).unwrap().clone()
1740                    } else if let Some(raster) =
1741                        self.rasterize_glyph(font_idx, glyph_id, force_monochrome)
1742                    {
1743                        let info = self.upload_glyph(cache_key, &raster);
1744                        self.glyph_cache.insert(cache_key, info.clone());
1745                        self.lru_push_front(cache_key);
1746                        info
1747                    } else {
1748                        continue;
1749                    };
1750
1751                    let char_w = if cell.wide_char {
1752                        self.cell_width * 2.0
1753                    } else {
1754                        self.cell_width
1755                    };
1756                    let x0 = content_x + col_idx as f32 * self.cell_width;
1757                    let y0 = content_y + row as f32 * self.cell_height;
1758                    let x1 = x0 + char_w;
1759                    let y1 = y0 + self.cell_height;
1760
1761                    let cell_w = x1 - x0;
1762                    let cell_h = y1 - y0;
1763                    let scale_x = cell_w / char_w;
1764                    let scale_y = cell_h / self.cell_height;
1765
1766                    let baseline_offset = baseline_y - (content_y + row as f32 * self.cell_height);
1767                    let glyph_left = x0 + (info.bearing_x * scale_x).round();
1768                    let baseline_in_cell = (baseline_offset * scale_y).round();
1769                    let glyph_top = y0 + baseline_in_cell - info.bearing_y;
1770
1771                    let render_w = info.width as f32 * scale_x;
1772                    let render_h = info.height as f32 * scale_y;
1773
1774                    let (final_left, final_top, final_w, final_h) =
1775                        if chars.len() == 1 && block_chars::should_snap_to_boundaries(char_type) {
1776                            block_chars::snap_glyph_to_cell(
1777                                glyph_left, glyph_top, render_w, render_h, x0, y0, x1, y1, 3.0, 0.5,
1778                            )
1779                        } else {
1780                            (glyph_left, glyph_top, render_w, render_h)
1781                        };
1782
1783                    let fg_color = [
1784                        cell.fg_color[0] as f32 / 255.0,
1785                        cell.fg_color[1] as f32 / 255.0,
1786                        cell.fg_color[2] as f32 / 255.0,
1787                        text_alpha,
1788                    ];
1789
1790                    if text_index < self.max_text_instances {
1791                        self.text_instances[text_index] = TextInstance {
1792                            position: [
1793                                final_left / self.config.width as f32 * 2.0 - 1.0,
1794                                1.0 - (final_top / self.config.height as f32 * 2.0),
1795                            ],
1796                            size: [
1797                                final_w / self.config.width as f32 * 2.0,
1798                                final_h / self.config.height as f32 * 2.0,
1799                            ],
1800                            tex_offset: [info.x as f32 / 2048.0, info.y as f32 / 2048.0],
1801                            tex_size: [info.width as f32 / 2048.0, info.height as f32 / 2048.0],
1802                            color: fg_color,
1803                            is_colored: if info.is_colored { 1 } else { 0 },
1804                        };
1805                        text_index += 1;
1806                    }
1807                }
1808            }
1809        }
1810
1811        // Inject command separator line instances for split panes
1812        if self.command_separator_enabled && !separator_marks.is_empty() {
1813            let width_f = self.config.width as f32;
1814            let height_f = self.config.height as f32;
1815            let opacity_multiplier = viewport.opacity;
1816            for &(screen_row, exit_code, custom_color) in separator_marks {
1817                if screen_row < rows && bg_index < self.max_bg_instances {
1818                    let x0 = content_x;
1819                    let x1 = content_x + cols as f32 * self.cell_width;
1820                    let y0 = content_y + screen_row as f32 * self.cell_height;
1821                    let color = self.separator_color(exit_code, custom_color, opacity_multiplier);
1822                    self.bg_instances[bg_index] = BackgroundInstance {
1823                        position: [x0 / width_f * 2.0 - 1.0, 1.0 - (y0 / height_f * 2.0)],
1824                        size: [
1825                            (x1 - x0) / width_f * 2.0,
1826                            self.command_separator_thickness / height_f * 2.0,
1827                        ],
1828                        color,
1829                    };
1830                    bg_index += 1;
1831                }
1832            }
1833        }
1834        let _ = bg_index; // suppress unused warning
1835
1836        // Upload instance buffers to GPU
1837        self.queue.write_buffer(
1838            &self.bg_instance_buffer,
1839            0,
1840            bytemuck::cast_slice(&self.bg_instances),
1841        );
1842        self.queue.write_buffer(
1843            &self.text_instance_buffer,
1844            0,
1845            bytemuck::cast_slice(&self.text_instances),
1846        );
1847
1848        Ok(())
1849    }
1850}