Skip to main content

par_term_render/renderer/
mod.rs

1use crate::cell_renderer::{Cell, CellRenderer, PaneViewport};
2use crate::custom_shader_renderer::CustomShaderRenderer;
3use crate::graphics_renderer::GraphicsRenderer;
4use anyhow::Result;
5use std::sync::Arc;
6use winit::dpi::PhysicalSize;
7use winit::window::Window;
8
9pub mod graphics;
10pub mod shaders;
11
12// Re-export SeparatorMark from par-term-config
13pub use par_term_config::SeparatorMark;
14
15/// Compute which separator marks are visible in the current viewport.
16///
17/// Maps absolute scrollback line numbers to screen rows for the current view.
18/// Deduplicates marks that are close together (e.g., multi-line prompts generate
19/// both a PromptStart and CommandStart mark within a few lines). When marks are
20/// within `MERGE_THRESHOLD` lines of each other, they are merged — keeping the
21/// earliest screen row (from PromptStart) while inheriting exit code and color
22/// from whichever mark carries them.
23pub fn compute_visible_separator_marks(
24    marks: &[par_term_config::ScrollbackMark],
25    scrollback_len: usize,
26    scroll_offset: usize,
27    visible_lines: usize,
28) -> Vec<SeparatorMark> {
29    let viewport_start = scrollback_len.saturating_sub(scroll_offset);
30    let viewport_end = viewport_start + visible_lines;
31
32    marks
33        .iter()
34        .filter_map(|mark| {
35            if mark.line >= viewport_start && mark.line < viewport_end {
36                let screen_row = mark.line - viewport_start;
37                Some((screen_row, mark.exit_code, mark.color))
38            } else {
39                None
40            }
41        })
42        .collect()
43}
44
45/// Information needed to render a single pane
46pub struct PaneRenderInfo<'a> {
47    /// Viewport bounds and state for this pane
48    pub viewport: PaneViewport,
49    /// Cells to render (should match viewport grid size)
50    pub cells: &'a [Cell],
51    /// Grid dimensions (cols, rows)
52    pub grid_size: (usize, usize),
53    /// Cursor position within this pane (col, row), or None if no cursor visible
54    pub cursor_pos: Option<(usize, usize)>,
55    /// Cursor opacity (0.0 = hidden, 1.0 = fully visible)
56    pub cursor_opacity: f32,
57    /// Whether this pane has a scrollbar visible
58    pub show_scrollbar: bool,
59    /// Scrollback marks for this pane
60    pub marks: Vec<par_term_config::ScrollbackMark>,
61    /// Scrollback length for this pane (needed for separator mark mapping)
62    pub scrollback_len: usize,
63    /// Current scroll offset for this pane (needed for separator mark mapping)
64    pub scroll_offset: usize,
65    /// Per-pane background image override (None = use global background)
66    pub background: Option<par_term_config::PaneBackground>,
67}
68
69/// Information needed to render a pane divider
70#[derive(Clone, Copy, Debug)]
71pub struct DividerRenderInfo {
72    /// X position in pixels
73    pub x: f32,
74    /// Y position in pixels
75    pub y: f32,
76    /// Width in pixels
77    pub width: f32,
78    /// Height in pixels
79    pub height: f32,
80    /// Whether this divider is currently being hovered
81    pub hovered: bool,
82}
83
84impl DividerRenderInfo {
85    /// Create from a DividerRect
86    pub fn from_rect(rect: &par_term_config::DividerRect, hovered: bool) -> Self {
87        Self {
88            x: rect.x,
89            y: rect.y,
90            width: rect.width,
91            height: rect.height,
92            hovered,
93        }
94    }
95}
96
97/// Information needed to render a pane title bar
98#[derive(Clone, Debug)]
99pub struct PaneTitleInfo {
100    /// X position of the title bar in pixels
101    pub x: f32,
102    /// Y position of the title bar in pixels
103    pub y: f32,
104    /// Width of the title bar in pixels
105    pub width: f32,
106    /// Height of the title bar in pixels
107    pub height: f32,
108    /// Title text to display
109    pub title: String,
110    /// Whether this pane is focused
111    pub focused: bool,
112    /// Text color [R, G, B] as floats (0.0-1.0)
113    pub text_color: [f32; 3],
114    /// Background color [R, G, B] as floats (0.0-1.0)
115    pub bg_color: [f32; 3],
116}
117
118/// Settings for rendering pane dividers and focus indicators
119#[derive(Clone, Copy, Debug)]
120pub struct PaneDividerSettings {
121    /// Color for dividers [R, G, B] as floats (0.0-1.0)
122    pub divider_color: [f32; 3],
123    /// Color when hovering over dividers [R, G, B] as floats (0.0-1.0)
124    pub hover_color: [f32; 3],
125    /// Whether to show focus indicator around focused pane
126    pub show_focus_indicator: bool,
127    /// Color for focus indicator [R, G, B] as floats (0.0-1.0)
128    pub focus_color: [f32; 3],
129    /// Width of focus indicator border in pixels
130    pub focus_width: f32,
131    /// Style of dividers (solid, double, dashed, shadow)
132    pub divider_style: par_term_config::DividerStyle,
133}
134
135impl Default for PaneDividerSettings {
136    fn default() -> Self {
137        Self {
138            divider_color: [0.3, 0.3, 0.3],
139            hover_color: [0.5, 0.6, 0.8],
140            show_focus_indicator: true,
141            focus_color: [0.4, 0.6, 1.0],
142            focus_width: 2.0,
143            divider_style: par_term_config::DividerStyle::default(),
144        }
145    }
146}
147
148/// Renderer for the terminal using custom wgpu cell renderer
149pub struct Renderer {
150    // Cell renderer (owns the scrollbar)
151    pub(crate) cell_renderer: CellRenderer,
152
153    // Graphics renderer for sixel images
154    pub(crate) graphics_renderer: GraphicsRenderer,
155
156    // Current sixel graphics to render: (id, row, col, width_cells, height_cells, alpha, scroll_offset_rows)
157    // Note: row is isize to allow negative values for graphics scrolled off top
158    pub(crate) sixel_graphics: Vec<(u64, isize, usize, usize, usize, f32, usize)>,
159
160    // egui renderer for settings UI
161    pub(crate) egui_renderer: egui_wgpu::Renderer,
162
163    // Custom shader renderer for post-processing effects (background shader)
164    pub(crate) custom_shader_renderer: Option<CustomShaderRenderer>,
165    // Track current shader path to detect changes
166    pub(crate) custom_shader_path: Option<String>,
167
168    // Cursor shader renderer for cursor-specific effects (separate from background shader)
169    pub(crate) cursor_shader_renderer: Option<CustomShaderRenderer>,
170    // Track current cursor shader path to detect changes
171    pub(crate) cursor_shader_path: Option<String>,
172
173    // Cached for convenience
174    pub(crate) size: PhysicalSize<u32>,
175
176    // Dirty flag for optimization - only render when content has changed
177    pub(crate) dirty: bool,
178
179    // Skip cursor shader when alt screen is active (TUI apps like vim, htop)
180    pub(crate) cursor_shader_disabled_for_alt_screen: bool,
181
182    // Debug overlay text
183    #[allow(dead_code)]
184    #[allow(dead_code)]
185    pub(crate) debug_text: Option<String>,
186}
187
188impl Renderer {
189    /// Create a new renderer
190    #[allow(clippy::too_many_arguments)]
191    pub async fn new(
192        window: Arc<Window>,
193        font_family: Option<&str>,
194        font_family_bold: Option<&str>,
195        font_family_italic: Option<&str>,
196        font_family_bold_italic: Option<&str>,
197        font_ranges: &[par_term_config::FontRange],
198        font_size: f32,
199        window_padding: f32,
200        line_spacing: f32,
201        char_spacing: f32,
202        scrollbar_position: &str,
203        scrollbar_width: f32,
204        scrollbar_thumb_color: [f32; 4],
205        scrollbar_track_color: [f32; 4],
206        enable_text_shaping: bool,
207        enable_ligatures: bool,
208        enable_kerning: bool,
209        font_antialias: bool,
210        font_hinting: bool,
211        font_thin_strokes: par_term_config::ThinStrokesMode,
212        minimum_contrast: f32,
213        vsync_mode: par_term_config::VsyncMode,
214        power_preference: par_term_config::PowerPreference,
215        window_opacity: f32,
216        background_color: [u8; 3],
217        background_image_path: Option<&str>,
218        background_image_enabled: bool,
219        background_image_mode: par_term_config::BackgroundImageMode,
220        background_image_opacity: f32,
221        custom_shader_path: Option<&str>,
222        custom_shader_enabled: bool,
223        custom_shader_animation: bool,
224        custom_shader_animation_speed: f32,
225        custom_shader_full_content: bool,
226        custom_shader_brightness: f32,
227        // Custom shader channel textures (iChannel0-3)
228        custom_shader_channel_paths: &[Option<std::path::PathBuf>; 4],
229        // Cubemap texture path prefix for environment mapping (iCubemap)
230        custom_shader_cubemap_path: Option<&std::path::Path>,
231        // Use background image as iChannel0 for custom shaders
232        use_background_as_channel0: bool,
233        // Inline image scaling mode (nearest vs linear)
234        image_scaling_mode: par_term_config::ImageScalingMode,
235        // Preserve aspect ratio when scaling inline images
236        image_preserve_aspect_ratio: bool,
237        // Cursor shader settings (separate from background shader)
238        cursor_shader_path: Option<&str>,
239        cursor_shader_enabled: bool,
240        cursor_shader_animation: bool,
241        cursor_shader_animation_speed: f32,
242    ) -> Result<Self> {
243        let size = window.inner_size();
244        let scale_factor = window.scale_factor();
245
246        // Standard DPI for the platform
247        // macOS typically uses 72 DPI for points, Windows and most Linux use 96 DPI
248        let platform_dpi = if cfg!(target_os = "macos") {
249            72.0
250        } else {
251            96.0
252        };
253
254        // Convert font size from points to pixels for cell size calculation, honoring DPI and scale
255        let base_font_pixels = font_size * platform_dpi / 72.0;
256        let font_size_pixels = (base_font_pixels * scale_factor as f32).max(1.0);
257
258        // Preliminary font lookup to get metrics for accurate cell height
259        let font_manager = par_term_fonts::font_manager::FontManager::new(
260            font_family,
261            font_family_bold,
262            font_family_italic,
263            font_family_bold_italic,
264            font_ranges,
265        )?;
266
267        let (font_ascent, font_descent, font_leading, char_advance) = {
268            let primary_font = font_manager.get_font(0).unwrap();
269            let metrics = primary_font.metrics(&[]);
270            let scale = font_size_pixels / metrics.units_per_em as f32;
271
272            // Get advance width of a standard character ('m' is common for monospace width)
273            let glyph_id = primary_font.charmap().map('m');
274            let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
275
276            (
277                metrics.ascent * scale,
278                metrics.descent * scale,
279                metrics.leading * scale,
280                advance,
281            )
282        };
283
284        // Use font metrics for cell height if line_spacing is 1.0
285        // Natural line height = ascent + descent + leading
286        let natural_line_height = font_ascent + font_descent + font_leading;
287        let char_height = (natural_line_height * line_spacing).max(1.0);
288
289        // Scale logical pixel values (config) to physical pixels (wgpu surface)
290        let scale = scale_factor as f32;
291        let window_padding = window_padding * scale;
292        let scrollbar_width = scrollbar_width * scale;
293
294        // Calculate available space after padding (padding on all sides)
295        let available_width = (size.width as f32 - window_padding * 2.0).max(0.0);
296        let available_height = (size.height as f32 - window_padding * 2.0).max(0.0);
297
298        // Calculate terminal dimensions based on font size in pixels and spacing
299        let char_width = (char_advance * char_spacing).max(1.0); // Configurable character width
300        let cols = (available_width / char_width).max(1.0) as usize;
301        let rows = (available_height / char_height).max(1.0) as usize;
302
303        // Create cell renderer with font fallback support (owns scrollbar)
304        let cell_renderer = CellRenderer::new(
305            window.clone(),
306            font_family,
307            font_family_bold,
308            font_family_italic,
309            font_family_bold_italic,
310            font_ranges,
311            font_size,
312            cols,
313            rows,
314            window_padding,
315            line_spacing,
316            char_spacing,
317            scrollbar_position,
318            scrollbar_width,
319            scrollbar_thumb_color,
320            scrollbar_track_color,
321            enable_text_shaping,
322            enable_ligatures,
323            enable_kerning,
324            font_antialias,
325            font_hinting,
326            font_thin_strokes,
327            minimum_contrast,
328            vsync_mode,
329            power_preference,
330            window_opacity,
331            background_color,
332            {
333                let bg_path = if background_image_enabled {
334                    background_image_path
335                } else {
336                    None
337                };
338                log::info!(
339                    "Renderer::new: background_image_enabled={}, path={:?}",
340                    background_image_enabled,
341                    bg_path
342                );
343                bg_path
344            },
345            background_image_mode,
346            background_image_opacity,
347        )
348        .await?;
349
350        // Create egui renderer for settings UI
351        let egui_renderer = egui_wgpu::Renderer::new(
352            cell_renderer.device(),
353            cell_renderer.surface_format(),
354            egui_wgpu::RendererOptions {
355                msaa_samples: 1,
356                depth_stencil_format: None,
357                dithering: false,
358                predictable_texture_filtering: false,
359            },
360        );
361
362        // Create graphics renderer for sixel images
363        let graphics_renderer = GraphicsRenderer::new(
364            cell_renderer.device(),
365            cell_renderer.surface_format(),
366            cell_renderer.cell_width(),
367            cell_renderer.cell_height(),
368            cell_renderer.window_padding(),
369            image_scaling_mode,
370            image_preserve_aspect_ratio,
371        )?;
372
373        // Create custom shader renderer if configured
374        let (mut custom_shader_renderer, initial_shader_path) = shaders::init_custom_shader(
375            &cell_renderer,
376            size.width,
377            size.height,
378            window_padding,
379            custom_shader_path,
380            custom_shader_enabled,
381            custom_shader_animation,
382            custom_shader_animation_speed,
383            window_opacity,
384            custom_shader_full_content,
385            custom_shader_brightness,
386            custom_shader_channel_paths,
387            custom_shader_cubemap_path,
388            use_background_as_channel0,
389        );
390
391        // Create cursor shader renderer if configured (separate from background shader)
392        let (mut cursor_shader_renderer, initial_cursor_shader_path) = shaders::init_cursor_shader(
393            &cell_renderer,
394            size.width,
395            size.height,
396            window_padding,
397            cursor_shader_path,
398            cursor_shader_enabled,
399            cursor_shader_animation,
400            cursor_shader_animation_speed,
401            window_opacity,
402        );
403
404        // Sync DPI scale factor to shader renderers for cursor sizing
405        if let Some(ref mut cs) = custom_shader_renderer {
406            cs.set_scale_factor(scale);
407        }
408        if let Some(ref mut cs) = cursor_shader_renderer {
409            cs.set_scale_factor(scale);
410        }
411
412        log::info!(
413            "[renderer] Renderer created: custom_shader_loaded={}, cursor_shader_loaded={}",
414            initial_shader_path.is_some(),
415            initial_cursor_shader_path.is_some()
416        );
417
418        Ok(Self {
419            cell_renderer,
420            graphics_renderer,
421            sixel_graphics: Vec::new(),
422            egui_renderer,
423            custom_shader_renderer,
424            custom_shader_path: initial_shader_path,
425            cursor_shader_renderer,
426            cursor_shader_path: initial_cursor_shader_path,
427            size,
428            dirty: true, // Start dirty to ensure initial render
429            cursor_shader_disabled_for_alt_screen: false,
430            debug_text: None,
431        })
432    }
433
434    /// Resize the renderer and recalculate grid dimensions based on padding/font metrics
435    pub fn resize(&mut self, new_size: PhysicalSize<u32>) -> (usize, usize) {
436        if new_size.width > 0 && new_size.height > 0 {
437            self.size = new_size;
438            self.dirty = true; // Mark dirty on resize
439            let result = self.cell_renderer.resize(new_size.width, new_size.height);
440
441            // Update graphics renderer cell dimensions
442            self.graphics_renderer.update_cell_dimensions(
443                self.cell_renderer.cell_width(),
444                self.cell_renderer.cell_height(),
445                self.cell_renderer.window_padding(),
446            );
447
448            // Update custom shader renderer dimensions
449            if let Some(ref mut custom_shader) = self.custom_shader_renderer {
450                custom_shader.resize(self.cell_renderer.device(), new_size.width, new_size.height);
451                // Sync cell dimensions for cursor position calculation
452                custom_shader.update_cell_dimensions(
453                    self.cell_renderer.cell_width(),
454                    self.cell_renderer.cell_height(),
455                    self.cell_renderer.window_padding(),
456                );
457            }
458
459            // Update cursor shader renderer dimensions
460            if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
461                cursor_shader.resize(self.cell_renderer.device(), new_size.width, new_size.height);
462                // Sync cell dimensions for cursor position calculation
463                cursor_shader.update_cell_dimensions(
464                    self.cell_renderer.cell_width(),
465                    self.cell_renderer.cell_height(),
466                    self.cell_renderer.window_padding(),
467                );
468            }
469
470            return result;
471        }
472
473        self.cell_renderer.grid_size()
474    }
475
476    /// Update scale factor and resize so the PTY grid matches the new DPI.
477    pub fn handle_scale_factor_change(
478        &mut self,
479        scale_factor: f64,
480        new_size: PhysicalSize<u32>,
481    ) -> (usize, usize) {
482        let old_scale = self.cell_renderer.scale_factor;
483        self.cell_renderer.update_scale_factor(scale_factor);
484        let new_scale = self.cell_renderer.scale_factor;
485
486        // Rescale physical pixel values when DPI changes
487        if old_scale > 0.0 && (old_scale - new_scale).abs() > f32::EPSILON {
488            // Rescale content_offset_y
489            let logical_offset_y = self.cell_renderer.content_offset_y() / old_scale;
490            let new_physical_offset_y = logical_offset_y * new_scale;
491            self.cell_renderer
492                .set_content_offset_y(new_physical_offset_y);
493            self.graphics_renderer
494                .set_content_offset_y(new_physical_offset_y);
495            if let Some(ref mut cs) = self.custom_shader_renderer {
496                cs.set_content_offset_y(new_physical_offset_y);
497            }
498            if let Some(ref mut cs) = self.cursor_shader_renderer {
499                cs.set_content_offset_y(new_physical_offset_y);
500            }
501
502            // Rescale content_offset_x
503            let logical_offset_x = self.cell_renderer.content_offset_x() / old_scale;
504            let new_physical_offset_x = logical_offset_x * new_scale;
505            self.cell_renderer
506                .set_content_offset_x(new_physical_offset_x);
507            self.graphics_renderer
508                .set_content_offset_x(new_physical_offset_x);
509            if let Some(ref mut cs) = self.custom_shader_renderer {
510                cs.set_content_offset_x(new_physical_offset_x);
511            }
512            if let Some(ref mut cs) = self.cursor_shader_renderer {
513                cs.set_content_offset_x(new_physical_offset_x);
514            }
515
516            // Rescale content_inset_bottom
517            let logical_inset_bottom = self.cell_renderer.content_inset_bottom() / old_scale;
518            let new_physical_inset_bottom = logical_inset_bottom * new_scale;
519            self.cell_renderer
520                .set_content_inset_bottom(new_physical_inset_bottom);
521
522            // Rescale egui_bottom_inset (status bar)
523            if self.cell_renderer.egui_bottom_inset > 0.0 {
524                let logical_egui_bottom = self.cell_renderer.egui_bottom_inset / old_scale;
525                self.cell_renderer.egui_bottom_inset = logical_egui_bottom * new_scale;
526            }
527
528            // Rescale content_inset_right (AI Inspector panel)
529            if self.cell_renderer.content_inset_right > 0.0 {
530                let logical_inset_right = self.cell_renderer.content_inset_right / old_scale;
531                self.cell_renderer.content_inset_right = logical_inset_right * new_scale;
532            }
533
534            // Rescale egui_right_inset
535            if self.cell_renderer.egui_right_inset > 0.0 {
536                let logical_egui_right = self.cell_renderer.egui_right_inset / old_scale;
537                self.cell_renderer.egui_right_inset = logical_egui_right * new_scale;
538            }
539
540            // Rescale window_padding
541            let logical_padding = self.cell_renderer.window_padding() / old_scale;
542            let new_physical_padding = logical_padding * new_scale;
543            self.cell_renderer
544                .update_window_padding(new_physical_padding);
545
546            // Sync new scale factor to shader renderers for cursor sizing
547            if let Some(ref mut cs) = self.custom_shader_renderer {
548                cs.set_scale_factor(new_scale);
549            }
550            if let Some(ref mut cs) = self.cursor_shader_renderer {
551                cs.set_scale_factor(new_scale);
552            }
553        }
554
555        self.resize(new_size)
556    }
557
558    /// Update the terminal cells
559    pub fn update_cells(&mut self, cells: &[Cell]) {
560        self.cell_renderer.update_cells(cells);
561        self.dirty = true; // Mark dirty when cells change
562    }
563
564    /// Clear all cells in the renderer.
565    /// Call this when switching tabs to ensure a clean slate.
566    pub fn clear_all_cells(&mut self) {
567        self.cell_renderer.clear_all_cells();
568        self.dirty = true;
569    }
570
571    /// Update cursor position and style for geometric rendering
572    pub fn update_cursor(
573        &mut self,
574        position: (usize, usize),
575        opacity: f32,
576        style: par_term_emu_core_rust::cursor::CursorStyle,
577    ) {
578        self.cell_renderer.update_cursor(position, opacity, style);
579        self.dirty = true;
580    }
581
582    /// Clear cursor (hide it)
583    pub fn clear_cursor(&mut self) {
584        self.cell_renderer.clear_cursor();
585        self.dirty = true;
586    }
587
588    /// Update scrollbar state
589    ///
590    /// # Arguments
591    /// * `scroll_offset` - Current scroll offset (0 = at bottom)
592    /// * `visible_lines` - Number of lines visible on screen
593    /// * `total_lines` - Total number of lines including scrollback
594    /// * `marks` - Scrollback marks for visualization on the scrollbar
595    pub fn update_scrollbar(
596        &mut self,
597        scroll_offset: usize,
598        visible_lines: usize,
599        total_lines: usize,
600        marks: &[par_term_config::ScrollbackMark],
601    ) {
602        self.cell_renderer
603            .update_scrollbar(scroll_offset, visible_lines, total_lines, marks);
604        self.dirty = true; // Mark dirty when scrollbar changes
605    }
606
607    /// Set the visual bell flash intensity
608    ///
609    /// # Arguments
610    /// * `intensity` - Flash intensity from 0.0 (no flash) to 1.0 (full white flash)
611    pub fn set_visual_bell_intensity(&mut self, intensity: f32) {
612        self.cell_renderer.set_visual_bell_intensity(intensity);
613        if intensity > 0.0 {
614            self.dirty = true; // Mark dirty when flash is active
615        }
616    }
617
618    /// Update window opacity in real-time
619    pub fn update_opacity(&mut self, opacity: f32) {
620        self.cell_renderer.update_opacity(opacity);
621
622        // Propagate to custom shader renderer if present
623        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
624            custom_shader.set_opacity(opacity);
625        }
626
627        // Propagate to cursor shader renderer if present
628        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
629            cursor_shader.set_opacity(opacity);
630        }
631
632        self.dirty = true;
633    }
634
635    /// Update cursor color for cell rendering
636    pub fn update_cursor_color(&mut self, color: [u8; 3]) {
637        self.cell_renderer.update_cursor_color(color);
638        self.dirty = true;
639    }
640
641    /// Update cursor text color (color of text under block cursor)
642    pub fn update_cursor_text_color(&mut self, color: Option<[u8; 3]>) {
643        self.cell_renderer.update_cursor_text_color(color);
644        self.dirty = true;
645    }
646
647    /// Set whether cursor should be hidden when cursor shader is active
648    pub fn set_cursor_hidden_for_shader(&mut self, hidden: bool) {
649        self.cell_renderer.set_cursor_hidden_for_shader(hidden);
650        self.dirty = true;
651    }
652
653    /// Set window focus state (affects unfocused cursor rendering)
654    pub fn set_focused(&mut self, focused: bool) {
655        self.cell_renderer.set_focused(focused);
656        self.dirty = true;
657    }
658
659    /// Update cursor guide settings
660    pub fn update_cursor_guide(&mut self, enabled: bool, color: [u8; 4]) {
661        self.cell_renderer.update_cursor_guide(enabled, color);
662        self.dirty = true;
663    }
664
665    /// Update cursor shadow settings.
666    /// Offset and blur are in logical pixels and will be scaled to physical pixels internally.
667    pub fn update_cursor_shadow(
668        &mut self,
669        enabled: bool,
670        color: [u8; 4],
671        offset: [f32; 2],
672        blur: f32,
673    ) {
674        let scale = self.cell_renderer.scale_factor;
675        let physical_offset = [offset[0] * scale, offset[1] * scale];
676        let physical_blur = blur * scale;
677        self.cell_renderer
678            .update_cursor_shadow(enabled, color, physical_offset, physical_blur);
679        self.dirty = true;
680    }
681
682    /// Update cursor boost settings
683    pub fn update_cursor_boost(&mut self, intensity: f32, color: [u8; 3]) {
684        self.cell_renderer.update_cursor_boost(intensity, color);
685        self.dirty = true;
686    }
687
688    /// Update unfocused cursor style
689    pub fn update_unfocused_cursor_style(&mut self, style: par_term_config::UnfocusedCursorStyle) {
690        self.cell_renderer.update_unfocused_cursor_style(style);
691        self.dirty = true;
692    }
693
694    /// Update command separator settings from config.
695    /// Thickness is in logical pixels and will be scaled to physical pixels internally.
696    pub fn update_command_separator(
697        &mut self,
698        enabled: bool,
699        logical_thickness: f32,
700        opacity: f32,
701        exit_color: bool,
702        color: [u8; 3],
703    ) {
704        let physical_thickness = logical_thickness * self.cell_renderer.scale_factor;
705        self.cell_renderer.update_command_separator(
706            enabled,
707            physical_thickness,
708            opacity,
709            exit_color,
710            color,
711        );
712        self.dirty = true;
713    }
714
715    /// Set the visible separator marks for the current frame (single-pane path)
716    pub fn set_separator_marks(&mut self, marks: Vec<SeparatorMark>) {
717        self.cell_renderer.set_separator_marks(marks);
718        self.dirty = true;
719    }
720
721    /// Set whether transparency affects only default background cells.
722    /// When true, non-default (colored) backgrounds remain opaque for readability.
723    pub fn set_transparency_affects_only_default_background(&mut self, value: bool) {
724        self.cell_renderer
725            .set_transparency_affects_only_default_background(value);
726        self.dirty = true;
727    }
728
729    /// Set whether text should always be rendered at full opacity.
730    /// When true, text remains opaque regardless of window transparency settings.
731    pub fn set_keep_text_opaque(&mut self, value: bool) {
732        self.cell_renderer.set_keep_text_opaque(value);
733
734        // Also propagate to custom shader renderer if present
735        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
736            custom_shader.set_keep_text_opaque(value);
737        }
738
739        // And to cursor shader renderer if present
740        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
741            cursor_shader.set_keep_text_opaque(value);
742        }
743
744        self.dirty = true;
745    }
746
747    pub fn set_link_underline_style(&mut self, style: par_term_config::LinkUnderlineStyle) {
748        self.cell_renderer.set_link_underline_style(style);
749        self.dirty = true;
750    }
751
752    /// Set whether cursor shader should be disabled due to alt screen being active
753    ///
754    /// When alt screen is active (e.g., vim, htop, less), cursor shader effects
755    /// are disabled since TUI applications typically have their own cursor handling.
756    pub fn set_cursor_shader_disabled_for_alt_screen(&mut self, disabled: bool) {
757        if self.cursor_shader_disabled_for_alt_screen != disabled {
758            log::debug!("[cursor-shader] Alt-screen disable set to {}", disabled);
759            self.cursor_shader_disabled_for_alt_screen = disabled;
760        } else {
761            self.cursor_shader_disabled_for_alt_screen = disabled;
762        }
763    }
764
765    /// Update window padding in real-time without full renderer rebuild.
766    /// Accepts logical pixels (from config); scales to physical pixels internally.
767    /// Returns Some((cols, rows)) if grid size changed and terminal needs resize.
768    #[allow(dead_code)]
769    pub fn update_window_padding(&mut self, logical_padding: f32) -> Option<(usize, usize)> {
770        let physical_padding = logical_padding * self.cell_renderer.scale_factor;
771        let result = self.cell_renderer.update_window_padding(physical_padding);
772        // Update graphics renderer padding
773        self.graphics_renderer.update_cell_dimensions(
774            self.cell_renderer.cell_width(),
775            self.cell_renderer.cell_height(),
776            physical_padding,
777        );
778        // Update custom shader renderer padding
779        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
780            custom_shader.update_cell_dimensions(
781                self.cell_renderer.cell_width(),
782                self.cell_renderer.cell_height(),
783                physical_padding,
784            );
785        }
786        // Update cursor shader renderer padding
787        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
788            cursor_shader.update_cell_dimensions(
789                self.cell_renderer.cell_width(),
790                self.cell_renderer.cell_height(),
791                physical_padding,
792            );
793        }
794        self.dirty = true;
795        result
796    }
797
798    /// Enable/disable background image and reload if needed
799    #[allow(dead_code)]
800    pub fn set_background_image_enabled(
801        &mut self,
802        enabled: bool,
803        path: Option<&str>,
804        mode: par_term_config::BackgroundImageMode,
805        opacity: f32,
806    ) {
807        let path = if enabled { path } else { None };
808        self.cell_renderer.set_background_image(path, mode, opacity);
809
810        // Sync background texture to custom shader if it's using background as channel0
811        self.sync_background_texture_to_shader();
812
813        self.dirty = true;
814    }
815
816    /// Set background based on mode (Default, Color, or Image).
817    ///
818    /// This unified method handles all background types and syncs with shaders.
819    pub fn set_background(
820        &mut self,
821        mode: par_term_config::BackgroundMode,
822        color: [u8; 3],
823        image_path: Option<&str>,
824        image_mode: par_term_config::BackgroundImageMode,
825        image_opacity: f32,
826        image_enabled: bool,
827    ) {
828        self.cell_renderer.set_background(
829            mode,
830            color,
831            image_path,
832            image_mode,
833            image_opacity,
834            image_enabled,
835        );
836
837        // Sync background texture to custom shader if it's using background as channel0
838        self.sync_background_texture_to_shader();
839
840        // Sync background to shaders for proper compositing
841        let is_solid_color = matches!(mode, par_term_config::BackgroundMode::Color);
842        let is_image_mode = matches!(mode, par_term_config::BackgroundMode::Image);
843        let normalized_color = [
844            color[0] as f32 / 255.0,
845            color[1] as f32 / 255.0,
846            color[2] as f32 / 255.0,
847        ];
848
849        // Sync to cursor shader
850        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
851            // When background shader is enabled and chained into cursor shader,
852            // don't give cursor shader its own background - background shader handles it
853            let has_background_shader = self.custom_shader_renderer.is_some();
854
855            if has_background_shader {
856                // Background shader handles the background, cursor shader just passes through
857                cursor_shader.set_background_color([0.0, 0.0, 0.0], false);
858                cursor_shader.set_background_texture(self.cell_renderer.device(), None);
859                cursor_shader.update_use_background_as_channel0(self.cell_renderer.device(), false);
860            } else {
861                cursor_shader.set_background_color(normalized_color, is_solid_color);
862
863                // For image mode, pass background image as iChannel0
864                if is_image_mode && image_enabled {
865                    let bg_texture = self.cell_renderer.get_background_as_channel_texture();
866                    cursor_shader.set_background_texture(self.cell_renderer.device(), bg_texture);
867                    cursor_shader
868                        .update_use_background_as_channel0(self.cell_renderer.device(), true);
869                } else {
870                    // Clear background texture when not in image mode
871                    cursor_shader.set_background_texture(self.cell_renderer.device(), None);
872                    cursor_shader
873                        .update_use_background_as_channel0(self.cell_renderer.device(), false);
874                }
875            }
876        }
877
878        // Sync to custom shader
879        // Note: We don't pass is_solid_color=true to custom shaders because
880        // that would replace the shader output with a solid color, making the
881        // shader invisible. Custom shaders handle their own background.
882        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
883            custom_shader.set_background_color(normalized_color, false);
884        }
885
886        self.dirty = true;
887    }
888
889    /// Update scrollbar appearance in real-time.
890    /// Width is in logical pixels and will be scaled to physical pixels internally.
891    pub fn update_scrollbar_appearance(
892        &mut self,
893        logical_width: f32,
894        thumb_color: [f32; 4],
895        track_color: [f32; 4],
896    ) {
897        let physical_width = logical_width * self.cell_renderer.scale_factor;
898        self.cell_renderer
899            .update_scrollbar_appearance(physical_width, thumb_color, track_color);
900        self.dirty = true;
901    }
902
903    /// Update scrollbar position (left/right) in real-time
904    #[allow(dead_code)]
905    pub fn update_scrollbar_position(&mut self, position: &str) {
906        self.cell_renderer.update_scrollbar_position(position);
907        self.dirty = true;
908    }
909
910    /// Update background image opacity in real-time
911    #[allow(dead_code)]
912    pub fn update_background_image_opacity(&mut self, opacity: f32) {
913        self.cell_renderer.update_background_image_opacity(opacity);
914        self.dirty = true;
915    }
916
917    /// Load a per-pane background image into the texture cache.
918    /// Delegates to CellRenderer::load_pane_background.
919    pub fn load_pane_background(&mut self, path: &str) -> anyhow::Result<bool> {
920        self.cell_renderer.load_pane_background(path)
921    }
922
923    /// Update inline image scaling mode (nearest vs linear filtering).
924    ///
925    /// Recreates the GPU sampler and clears the texture cache so images
926    /// are re-rendered with the new filter mode.
927    pub fn update_image_scaling_mode(&mut self, scaling_mode: par_term_config::ImageScalingMode) {
928        self.graphics_renderer
929            .update_scaling_mode(self.cell_renderer.device(), scaling_mode);
930        self.dirty = true;
931    }
932
933    /// Update whether inline images preserve their aspect ratio.
934    pub fn update_image_preserve_aspect_ratio(&mut self, preserve: bool) {
935        self.graphics_renderer.set_preserve_aspect_ratio(preserve);
936        self.dirty = true;
937    }
938
939    /// Check if animation requires continuous rendering
940    ///
941    /// Returns true if shader animation is enabled or a cursor trail animation
942    /// might still be in progress.
943    pub fn needs_continuous_render(&self) -> bool {
944        let custom_needs = self
945            .custom_shader_renderer
946            .as_ref()
947            .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
948        let cursor_needs = self
949            .cursor_shader_renderer
950            .as_ref()
951            .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
952        custom_needs || cursor_needs
953    }
954
955    /// Render a frame with optional egui overlay
956    /// Returns true if rendering was performed, false if skipped
957    pub fn render(
958        &mut self,
959        egui_data: Option<(egui::FullOutput, &egui::Context)>,
960        force_egui_opaque: bool,
961        show_scrollbar: bool,
962        pane_background: Option<&par_term_config::PaneBackground>,
963    ) -> Result<bool> {
964        // Custom shader animation forces continuous rendering
965        let force_render = self.needs_continuous_render();
966
967        if !self.dirty && egui_data.is_none() && !force_render {
968            // Skip rendering if nothing has changed
969            return Ok(false);
970        }
971
972        // Check if shaders are enabled
973        let has_custom_shader = self.custom_shader_renderer.is_some();
974        // Only use cursor shader if it's enabled and not disabled for alt screen
975        let use_cursor_shader =
976            self.cursor_shader_renderer.is_some() && !self.cursor_shader_disabled_for_alt_screen;
977
978        // Cell renderer renders terminal content
979        let t1 = std::time::Instant::now();
980        let surface_texture = if has_custom_shader {
981            // When custom shader is enabled, always skip rendering background image
982            // to the intermediate texture. The shader controls the background:
983            // - If user wants background image in shader, enable use_background_as_channel0
984            // - Otherwise, the shader's own effects provide the background
985            // This prevents the background image from being treated as "terminal content"
986            // and passed through unchanged by the shader.
987
988            // Render terminal to intermediate texture for background shader
989            self.cell_renderer.render_to_texture(
990                self.custom_shader_renderer
991                    .as_ref()
992                    .unwrap()
993                    .intermediate_texture_view(),
994                true, // Always skip background image - shader handles background
995            )?
996        } else if use_cursor_shader {
997            // Render terminal to intermediate texture for cursor shader
998            // Skip background image - it will be handled via iBackgroundColor uniform
999            // or passed as iChannel0. This ensures proper opacity handling.
1000            self.cell_renderer.render_to_texture(
1001                self.cursor_shader_renderer
1002                    .as_ref()
1003                    .unwrap()
1004                    .intermediate_texture_view(),
1005                true, // Skip background image - shader handles it
1006            )?
1007        } else {
1008            // Render directly to surface (no shaders, or cursor shader disabled for alt screen)
1009            // Note: scrollbar is rendered separately after egui so it appears on top
1010            self.cell_renderer.render(show_scrollbar, pane_background)?
1011        };
1012        let cell_render_time = t1.elapsed();
1013
1014        // Apply background custom shader if enabled
1015        let t_custom = std::time::Instant::now();
1016        let custom_shader_time = if let Some(ref mut custom_shader) = self.custom_shader_renderer {
1017            if use_cursor_shader {
1018                // Background shader renders to cursor shader's intermediate texture
1019                // Don't apply opacity here - cursor shader will apply it when rendering to surface
1020                custom_shader.render(
1021                    self.cell_renderer.device(),
1022                    self.cell_renderer.queue(),
1023                    self.cursor_shader_renderer
1024                        .as_ref()
1025                        .unwrap()
1026                        .intermediate_texture_view(),
1027                    false, // Don't apply opacity - cursor shader will do it
1028                )?;
1029            } else {
1030                // Background shader renders directly to surface
1031                // (cursor shader disabled for alt screen or not configured)
1032                let surface_view = surface_texture
1033                    .texture
1034                    .create_view(&wgpu::TextureViewDescriptor::default());
1035                custom_shader.render(
1036                    self.cell_renderer.device(),
1037                    self.cell_renderer.queue(),
1038                    &surface_view,
1039                    true, // Apply opacity - this is the final render
1040                )?;
1041            }
1042            t_custom.elapsed()
1043        } else {
1044            std::time::Duration::ZERO
1045        };
1046
1047        // Apply cursor shader if enabled (skip when alt screen is active for TUI apps)
1048        let t_cursor = std::time::Instant::now();
1049        let cursor_shader_time = if use_cursor_shader {
1050            log::trace!("Rendering cursor shader");
1051            let cursor_shader = self.cursor_shader_renderer.as_mut().unwrap();
1052            let surface_view = surface_texture
1053                .texture
1054                .create_view(&wgpu::TextureViewDescriptor::default());
1055
1056            cursor_shader.render(
1057                self.cell_renderer.device(),
1058                self.cell_renderer.queue(),
1059                &surface_view,
1060                true, // Apply opacity - this is the final render to surface
1061            )?;
1062            t_cursor.elapsed()
1063        } else {
1064            if self.cursor_shader_disabled_for_alt_screen {
1065                log::trace!("Skipping cursor shader - alt screen active");
1066            }
1067            std::time::Duration::ZERO
1068        };
1069
1070        // Render sixel graphics on top of cells
1071        let t2 = std::time::Instant::now();
1072        if !self.sixel_graphics.is_empty() {
1073            self.render_sixel_graphics(&surface_texture)?;
1074        }
1075        let sixel_render_time = t2.elapsed();
1076
1077        // Render overlays (scrollbar, visual bell) BEFORE egui so that modal
1078        // dialogs (egui) render on top of the scrollbar. The scrollbar track
1079        // already accounts for status bar inset via content_inset_bottom.
1080        self.cell_renderer
1081            .render_overlays(&surface_texture, show_scrollbar)?;
1082
1083        // Render egui overlay if provided
1084        let t3 = std::time::Instant::now();
1085        if let Some((egui_output, egui_ctx)) = egui_data {
1086            self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
1087        }
1088        let egui_render_time = t3.elapsed();
1089
1090        // Present the surface texture - THIS IS WHERE VSYNC WAIT HAPPENS
1091        let t4 = std::time::Instant::now();
1092        surface_texture.present();
1093        let present_time = t4.elapsed();
1094
1095        // Log timing breakdown
1096        let total = cell_render_time
1097            + custom_shader_time
1098            + cursor_shader_time
1099            + sixel_render_time
1100            + egui_render_time
1101            + present_time;
1102        if present_time.as_millis() > 10 || total.as_millis() > 10 {
1103            log::info!(
1104                "[RENDER] RENDER_BREAKDOWN: CellRender={:.2}ms BgShader={:.2}ms CursorShader={:.2}ms Sixel={:.2}ms Egui={:.2}ms PRESENT={:.2}ms Total={:.2}ms",
1105                cell_render_time.as_secs_f64() * 1000.0,
1106                custom_shader_time.as_secs_f64() * 1000.0,
1107                cursor_shader_time.as_secs_f64() * 1000.0,
1108                sixel_render_time.as_secs_f64() * 1000.0,
1109                egui_render_time.as_secs_f64() * 1000.0,
1110                present_time.as_secs_f64() * 1000.0,
1111                total.as_secs_f64() * 1000.0
1112            );
1113        }
1114
1115        // Clear dirty flag after successful render
1116        self.dirty = false;
1117
1118        Ok(true)
1119    }
1120
1121    /// Render multiple panes to the surface
1122    ///
1123    /// This method renders each pane's content to its viewport region,
1124    /// handling focus indicators and inactive pane dimming.
1125    ///
1126    /// # Arguments
1127    /// * `panes` - List of panes to render with their viewport info
1128    /// * `egui_data` - Optional egui overlay data
1129    /// * `force_egui_opaque` - Force egui to render at full opacity
1130    ///
1131    /// # Returns
1132    /// `true` if rendering was performed, `false` if skipped
1133    #[allow(dead_code)]
1134    pub fn render_panes(
1135        &mut self,
1136        panes: &[PaneRenderInfo<'_>],
1137        egui_data: Option<(egui::FullOutput, &egui::Context)>,
1138        force_egui_opaque: bool,
1139    ) -> Result<bool> {
1140        // Check if we need to render
1141        let force_render = self.needs_continuous_render();
1142        if !self.dirty && egui_data.is_none() && !force_render {
1143            return Ok(false);
1144        }
1145
1146        // Get the surface texture
1147        let surface_texture = self.cell_renderer.surface.get_current_texture()?;
1148        let surface_view = surface_texture
1149            .texture
1150            .create_view(&wgpu::TextureViewDescriptor::default());
1151
1152        // Clear the surface first with the background color (respecting solid color mode)
1153        {
1154            let mut encoder = self.cell_renderer.device().create_command_encoder(
1155                &wgpu::CommandEncoderDescriptor {
1156                    label: Some("pane clear encoder"),
1157                },
1158            );
1159
1160            let opacity = self.cell_renderer.window_opacity as f64;
1161            let clear_color = if self.cell_renderer.bg_is_solid_color {
1162                wgpu::Color {
1163                    r: self.cell_renderer.solid_bg_color[0] as f64 * opacity,
1164                    g: self.cell_renderer.solid_bg_color[1] as f64 * opacity,
1165                    b: self.cell_renderer.solid_bg_color[2] as f64 * opacity,
1166                    a: opacity,
1167                }
1168            } else {
1169                wgpu::Color {
1170                    r: self.cell_renderer.background_color[0] as f64 * opacity,
1171                    g: self.cell_renderer.background_color[1] as f64 * opacity,
1172                    b: self.cell_renderer.background_color[2] as f64 * opacity,
1173                    a: opacity,
1174                }
1175            };
1176
1177            {
1178                let _clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1179                    label: Some("surface clear pass"),
1180                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1181                        view: &surface_view,
1182                        resolve_target: None,
1183                        ops: wgpu::Operations {
1184                            load: wgpu::LoadOp::Clear(clear_color),
1185                            store: wgpu::StoreOp::Store,
1186                        },
1187                        depth_slice: None,
1188                    })],
1189                    depth_stencil_attachment: None,
1190                    timestamp_writes: None,
1191                    occlusion_query_set: None,
1192                });
1193            }
1194
1195            self.cell_renderer
1196                .queue()
1197                .submit(std::iter::once(encoder.finish()));
1198        }
1199
1200        // Render background image first (full-screen, before panes)
1201        let has_background_image = self
1202            .cell_renderer
1203            .render_background_only(&surface_view, false)?;
1204
1205        // Render each pane (skip background image since we rendered it full-screen)
1206        for pane in panes {
1207            let separator_marks = compute_visible_separator_marks(
1208                &pane.marks,
1209                pane.scrollback_len,
1210                pane.scroll_offset,
1211                pane.grid_size.1,
1212            );
1213            self.cell_renderer.render_pane_to_view(
1214                &surface_view,
1215                &pane.viewport,
1216                pane.cells,
1217                pane.grid_size.0,
1218                pane.grid_size.1,
1219                pane.cursor_pos,
1220                pane.cursor_opacity,
1221                pane.show_scrollbar,
1222                false,                // Don't clear - we already cleared the surface
1223                has_background_image, // Skip background image if already rendered full-screen
1224                &separator_marks,
1225                pane.background.as_ref(),
1226            )?;
1227        }
1228
1229        // Render egui overlay if provided
1230        if let Some((egui_output, egui_ctx)) = egui_data {
1231            self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
1232        }
1233
1234        // Present the surface
1235        surface_texture.present();
1236
1237        self.dirty = false;
1238        Ok(true)
1239    }
1240
1241    /// Render split panes with dividers and focus indicator
1242    ///
1243    /// This is the main entry point for rendering a split pane layout.
1244    /// It handles:
1245    /// 1. Clearing the surface
1246    /// 2. Rendering each pane's content
1247    /// 3. Rendering dividers between panes
1248    /// 4. Rendering focus indicator around the focused pane
1249    /// 5. Rendering egui overlay if provided
1250    /// 6. Presenting the surface
1251    ///
1252    /// # Arguments
1253    /// * `panes` - List of panes to render with their viewport info
1254    /// * `dividers` - List of dividers between panes with hover state
1255    /// * `focused_viewport` - Viewport of the focused pane (for focus indicator)
1256    /// * `divider_settings` - Settings for divider and focus indicator appearance
1257    /// * `egui_data` - Optional egui overlay data
1258    /// * `force_egui_opaque` - Force egui to render at full opacity
1259    ///
1260    /// # Returns
1261    /// `true` if rendering was performed, `false` if skipped
1262    #[allow(dead_code, clippy::too_many_arguments)]
1263    pub fn render_split_panes(
1264        &mut self,
1265        panes: &[PaneRenderInfo<'_>],
1266        dividers: &[DividerRenderInfo],
1267        pane_titles: &[PaneTitleInfo],
1268        focused_viewport: Option<&PaneViewport>,
1269        divider_settings: &PaneDividerSettings,
1270        egui_data: Option<(egui::FullOutput, &egui::Context)>,
1271        force_egui_opaque: bool,
1272    ) -> Result<bool> {
1273        // Check if we need to render
1274        let force_render = self.needs_continuous_render();
1275        if !self.dirty && egui_data.is_none() && !force_render {
1276            return Ok(false);
1277        }
1278
1279        let has_custom_shader = self.custom_shader_renderer.is_some();
1280
1281        // Pre-load any per-pane background textures that aren't cached yet
1282        for pane in panes.iter() {
1283            if let Some(ref bg) = pane.background
1284                && let Some(ref path) = bg.image_path
1285                && let Err(e) = self.cell_renderer.load_pane_background(path)
1286            {
1287                log::error!("Failed to load pane background '{}': {}", path, e);
1288            }
1289        }
1290
1291        // Get the surface texture
1292        let surface_texture = self.cell_renderer.surface.get_current_texture()?;
1293        let surface_view = surface_texture
1294            .texture
1295            .create_view(&wgpu::TextureViewDescriptor::default());
1296
1297        // Clear the surface with background color (respecting solid color mode)
1298        let opacity = self.cell_renderer.window_opacity as f64;
1299        let clear_color = if self.cell_renderer.bg_is_solid_color {
1300            wgpu::Color {
1301                r: self.cell_renderer.solid_bg_color[0] as f64 * opacity,
1302                g: self.cell_renderer.solid_bg_color[1] as f64 * opacity,
1303                b: self.cell_renderer.solid_bg_color[2] as f64 * opacity,
1304                a: opacity,
1305            }
1306        } else {
1307            wgpu::Color {
1308                r: self.cell_renderer.background_color[0] as f64 * opacity,
1309                g: self.cell_renderer.background_color[1] as f64 * opacity,
1310                b: self.cell_renderer.background_color[2] as f64 * opacity,
1311                a: opacity,
1312            }
1313        };
1314
1315        // If custom shader is enabled, render it with the background clear color
1316        // (the shader's render pass will handle clearing the surface)
1317        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
1318            // Clear the intermediate texture to remove any old single-pane content
1319            // This prevents the shader from displaying stale terminal content
1320            custom_shader.clear_intermediate_texture(
1321                self.cell_renderer.device(),
1322                self.cell_renderer.queue(),
1323            );
1324
1325            // Render shader effect to surface with background color as clear
1326            // Don't apply opacity here - pane cells will blend on top
1327            custom_shader.render_with_clear_color(
1328                self.cell_renderer.device(),
1329                self.cell_renderer.queue(),
1330                &surface_view,
1331                false, // Don't apply opacity - let pane rendering handle it
1332                clear_color,
1333            )?;
1334        } else {
1335            // No custom shader - just clear the surface with background color
1336            let mut encoder = self.cell_renderer.device().create_command_encoder(
1337                &wgpu::CommandEncoderDescriptor {
1338                    label: Some("split pane clear encoder"),
1339                },
1340            );
1341
1342            {
1343                let _clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1344                    label: Some("surface clear pass"),
1345                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1346                        view: &surface_view,
1347                        resolve_target: None,
1348                        ops: wgpu::Operations {
1349                            load: wgpu::LoadOp::Clear(clear_color),
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
1360            self.cell_renderer
1361                .queue()
1362                .submit(std::iter::once(encoder.finish()));
1363        }
1364
1365        // Render background image (full-screen, after shader but before panes)
1366        // Skip if custom shader is handling the background.
1367        // Also skip if any pane has a per-pane background configured -
1368        // per-pane backgrounds are rendered individually in render_pane_to_view.
1369        let any_pane_has_background = panes.iter().any(|p| p.background.is_some());
1370        let has_background_image = if !has_custom_shader && !any_pane_has_background {
1371            self.cell_renderer
1372                .render_background_only(&surface_view, false)?
1373        } else {
1374            false
1375        };
1376
1377        // Render each pane's content (skip background image since we rendered it full-screen)
1378        for pane in panes {
1379            let separator_marks = compute_visible_separator_marks(
1380                &pane.marks,
1381                pane.scrollback_len,
1382                pane.scroll_offset,
1383                pane.grid_size.1,
1384            );
1385            self.cell_renderer.render_pane_to_view(
1386                &surface_view,
1387                &pane.viewport,
1388                pane.cells,
1389                pane.grid_size.0,
1390                pane.grid_size.1,
1391                pane.cursor_pos,
1392                pane.cursor_opacity,
1393                pane.show_scrollbar,
1394                false, // Don't clear - we already cleared the surface
1395                has_background_image || has_custom_shader, // Skip background if already rendered
1396                &separator_marks,
1397                pane.background.as_ref(),
1398            )?;
1399        }
1400
1401        // Render dividers between panes
1402        if !dividers.is_empty() {
1403            self.render_dividers(&surface_view, dividers, divider_settings)?;
1404        }
1405
1406        // Render pane title bars (background + text)
1407        if !pane_titles.is_empty() {
1408            self.render_pane_titles(&surface_view, pane_titles)?;
1409        }
1410
1411        // Render focus indicator around focused pane (only if multiple panes)
1412        if panes.len() > 1
1413            && let Some(viewport) = focused_viewport
1414        {
1415            self.render_focus_indicator(&surface_view, viewport, divider_settings)?;
1416        }
1417
1418        // Render egui overlay if provided
1419        if let Some((egui_output, egui_ctx)) = egui_data {
1420            self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
1421        }
1422
1423        // Present the surface
1424        surface_texture.present();
1425
1426        self.dirty = false;
1427        Ok(true)
1428    }
1429
1430    /// Render pane dividers on top of pane content
1431    ///
1432    /// This should be called after rendering pane content but before egui.
1433    ///
1434    /// # Arguments
1435    /// * `surface_view` - The texture view to render to
1436    /// * `dividers` - List of dividers to render with hover state
1437    /// * `settings` - Divider appearance settings
1438    #[allow(dead_code)]
1439    pub fn render_dividers(
1440        &mut self,
1441        surface_view: &wgpu::TextureView,
1442        dividers: &[DividerRenderInfo],
1443        settings: &PaneDividerSettings,
1444    ) -> Result<()> {
1445        if dividers.is_empty() {
1446            return Ok(());
1447        }
1448
1449        // Build divider instances using the cell renderer's background pipeline
1450        // We reuse the bg_instances buffer for dividers
1451        let mut instances = Vec::with_capacity(dividers.len() * 3); // Extra capacity for multi-rect styles
1452
1453        let w = self.size.width as f32;
1454        let h = self.size.height as f32;
1455
1456        for divider in dividers {
1457            let color = if divider.hovered {
1458                settings.hover_color
1459            } else {
1460                settings.divider_color
1461            };
1462
1463            use par_term_config::DividerStyle;
1464            match settings.divider_style {
1465                DividerStyle::Solid => {
1466                    let x_ndc = divider.x / w * 2.0 - 1.0;
1467                    let y_ndc = 1.0 - (divider.y / h * 2.0);
1468                    let w_ndc = divider.width / w * 2.0;
1469                    let h_ndc = divider.height / h * 2.0;
1470
1471                    instances.push(crate::cell_renderer::types::BackgroundInstance {
1472                        position: [x_ndc, y_ndc],
1473                        size: [w_ndc, h_ndc],
1474                        color: [color[0], color[1], color[2], 1.0],
1475                    });
1476                }
1477                DividerStyle::Double => {
1478                    // Two parallel lines with a visible gap between them
1479                    let is_horizontal = divider.width > divider.height;
1480                    let thickness = if is_horizontal {
1481                        divider.height
1482                    } else {
1483                        divider.width
1484                    };
1485
1486                    if thickness >= 4.0 {
1487                        // Enough space for two 1px lines with visible gap
1488                        if is_horizontal {
1489                            // Top line
1490                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1491                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1492                                size: [divider.width / w * 2.0, 1.0 / h * 2.0],
1493                                color: [color[0], color[1], color[2], 1.0],
1494                            });
1495                            // Bottom line (gap in between shows background)
1496                            let bottom_y = divider.y + divider.height - 1.0;
1497                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1498                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (bottom_y / h * 2.0)],
1499                                size: [divider.width / w * 2.0, 1.0 / h * 2.0],
1500                                color: [color[0], color[1], color[2], 1.0],
1501                            });
1502                        } else {
1503                            // Left line
1504                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1505                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1506                                size: [1.0 / w * 2.0, divider.height / h * 2.0],
1507                                color: [color[0], color[1], color[2], 1.0],
1508                            });
1509                            // Right line
1510                            let right_x = divider.x + divider.width - 1.0;
1511                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1512                                position: [right_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1513                                size: [1.0 / w * 2.0, divider.height / h * 2.0],
1514                                color: [color[0], color[1], color[2], 1.0],
1515                            });
1516                        }
1517                    } else {
1518                        // Divider too thin for double lines — render centered 1px line
1519                        // (visibly thinner than Solid to differentiate)
1520                        if is_horizontal {
1521                            let center_y = divider.y + (divider.height - 1.0) / 2.0;
1522                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1523                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (center_y / h * 2.0)],
1524                                size: [divider.width / w * 2.0, 1.0 / h * 2.0],
1525                                color: [color[0], color[1], color[2], 1.0],
1526                            });
1527                        } else {
1528                            let center_x = divider.x + (divider.width - 1.0) / 2.0;
1529                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1530                                position: [center_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1531                                size: [1.0 / w * 2.0, divider.height / h * 2.0],
1532                                color: [color[0], color[1], color[2], 1.0],
1533                            });
1534                        }
1535                    }
1536                }
1537                DividerStyle::Dashed => {
1538                    // Dashed line effect using segments
1539                    let is_horizontal = divider.width > divider.height;
1540                    let dash_len: f32 = 6.0;
1541                    let gap_len: f32 = 4.0;
1542
1543                    if is_horizontal {
1544                        let mut x = divider.x;
1545                        while x < divider.x + divider.width {
1546                            let seg_w = dash_len.min(divider.x + divider.width - x);
1547                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1548                                position: [x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1549                                size: [seg_w / w * 2.0, divider.height / h * 2.0],
1550                                color: [color[0], color[1], color[2], 1.0],
1551                            });
1552                            x += dash_len + gap_len;
1553                        }
1554                    } else {
1555                        let mut y = divider.y;
1556                        while y < divider.y + divider.height {
1557                            let seg_h = dash_len.min(divider.y + divider.height - y);
1558                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1559                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (y / h * 2.0)],
1560                                size: [divider.width / w * 2.0, seg_h / h * 2.0],
1561                                color: [color[0], color[1], color[2], 1.0],
1562                            });
1563                            y += dash_len + gap_len;
1564                        }
1565                    }
1566                }
1567                DividerStyle::Shadow => {
1568                    // Beveled/embossed effect — all rendering stays within divider bounds
1569                    // Highlight on top/left edge, shadow on bottom/right edge
1570                    let is_horizontal = divider.width > divider.height;
1571                    let thickness = if is_horizontal {
1572                        divider.height
1573                    } else {
1574                        divider.width
1575                    };
1576
1577                    // Brighter highlight color
1578                    let highlight = [
1579                        (color[0] + 0.3).min(1.0),
1580                        (color[1] + 0.3).min(1.0),
1581                        (color[2] + 0.3).min(1.0),
1582                        1.0,
1583                    ];
1584                    // Darker shadow color
1585                    let shadow = [(color[0] * 0.3), (color[1] * 0.3), (color[2] * 0.3), 1.0];
1586
1587                    if thickness >= 3.0 {
1588                        // 3+ px: highlight line / main body / shadow line
1589                        let edge = 1.0_f32;
1590                        if is_horizontal {
1591                            // Top highlight
1592                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1593                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1594                                size: [divider.width / w * 2.0, edge / h * 2.0],
1595                                color: highlight,
1596                            });
1597                            // Main body (middle portion)
1598                            let body_y = divider.y + edge;
1599                            let body_h = divider.height - edge * 2.0;
1600                            if body_h > 0.0 {
1601                                instances.push(crate::cell_renderer::types::BackgroundInstance {
1602                                    position: [divider.x / w * 2.0 - 1.0, 1.0 - (body_y / h * 2.0)],
1603                                    size: [divider.width / w * 2.0, body_h / h * 2.0],
1604                                    color: [color[0], color[1], color[2], 1.0],
1605                                });
1606                            }
1607                            // Bottom shadow
1608                            let shadow_y = divider.y + divider.height - edge;
1609                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1610                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (shadow_y / h * 2.0)],
1611                                size: [divider.width / w * 2.0, edge / h * 2.0],
1612                                color: shadow,
1613                            });
1614                        } else {
1615                            // Left highlight
1616                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1617                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1618                                size: [edge / w * 2.0, divider.height / h * 2.0],
1619                                color: highlight,
1620                            });
1621                            // Main body
1622                            let body_x = divider.x + edge;
1623                            let body_w = divider.width - edge * 2.0;
1624                            if body_w > 0.0 {
1625                                instances.push(crate::cell_renderer::types::BackgroundInstance {
1626                                    position: [body_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1627                                    size: [body_w / w * 2.0, divider.height / h * 2.0],
1628                                    color: [color[0], color[1], color[2], 1.0],
1629                                });
1630                            }
1631                            // Right shadow
1632                            let shadow_x = divider.x + divider.width - edge;
1633                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1634                                position: [shadow_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1635                                size: [edge / w * 2.0, divider.height / h * 2.0],
1636                                color: shadow,
1637                            });
1638                        }
1639                    } else {
1640                        // 2px or less: top/left half highlight, bottom/right half shadow
1641                        if is_horizontal {
1642                            let half = (divider.height / 2.0).max(1.0);
1643                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1644                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1645                                size: [divider.width / w * 2.0, half / h * 2.0],
1646                                color: highlight,
1647                            });
1648                            let bottom_y = divider.y + half;
1649                            let bottom_h = divider.height - half;
1650                            if bottom_h > 0.0 {
1651                                instances.push(crate::cell_renderer::types::BackgroundInstance {
1652                                    position: [
1653                                        divider.x / w * 2.0 - 1.0,
1654                                        1.0 - (bottom_y / h * 2.0),
1655                                    ],
1656                                    size: [divider.width / w * 2.0, bottom_h / h * 2.0],
1657                                    color: shadow,
1658                                });
1659                            }
1660                        } else {
1661                            let half = (divider.width / 2.0).max(1.0);
1662                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1663                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1664                                size: [half / w * 2.0, divider.height / h * 2.0],
1665                                color: highlight,
1666                            });
1667                            let right_x = divider.x + half;
1668                            let right_w = divider.width - half;
1669                            if right_w > 0.0 {
1670                                instances.push(crate::cell_renderer::types::BackgroundInstance {
1671                                    position: [
1672                                        right_x / w * 2.0 - 1.0,
1673                                        1.0 - (divider.y / h * 2.0),
1674                                    ],
1675                                    size: [right_w / w * 2.0, divider.height / h * 2.0],
1676                                    color: shadow,
1677                                });
1678                            }
1679                        }
1680                    }
1681                }
1682            }
1683        }
1684
1685        // Write instances to GPU buffer
1686        self.cell_renderer.queue().write_buffer(
1687            &self.cell_renderer.bg_instance_buffer,
1688            0,
1689            bytemuck::cast_slice(&instances),
1690        );
1691
1692        // Render dividers
1693        let mut encoder =
1694            self.cell_renderer
1695                .device()
1696                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1697                    label: Some("divider render encoder"),
1698                });
1699
1700        {
1701            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1702                label: Some("divider render pass"),
1703                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1704                    view: surface_view,
1705                    resolve_target: None,
1706                    ops: wgpu::Operations {
1707                        load: wgpu::LoadOp::Load, // Don't clear - render on top
1708                        store: wgpu::StoreOp::Store,
1709                    },
1710                    depth_slice: None,
1711                })],
1712                depth_stencil_attachment: None,
1713                timestamp_writes: None,
1714                occlusion_query_set: None,
1715            });
1716
1717            render_pass.set_pipeline(&self.cell_renderer.bg_pipeline);
1718            render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
1719            render_pass.set_vertex_buffer(1, self.cell_renderer.bg_instance_buffer.slice(..));
1720            render_pass.draw(0..4, 0..instances.len() as u32);
1721        }
1722
1723        self.cell_renderer
1724            .queue()
1725            .submit(std::iter::once(encoder.finish()));
1726        Ok(())
1727    }
1728
1729    /// Render focus indicator around a pane
1730    ///
1731    /// This draws a colored border around the focused pane to highlight it.
1732    ///
1733    /// # Arguments
1734    /// * `surface_view` - The texture view to render to
1735    /// * `viewport` - The focused pane's viewport
1736    /// * `settings` - Divider/focus settings
1737    #[allow(dead_code)]
1738    pub fn render_focus_indicator(
1739        &mut self,
1740        surface_view: &wgpu::TextureView,
1741        viewport: &PaneViewport,
1742        settings: &PaneDividerSettings,
1743    ) -> Result<()> {
1744        if !settings.show_focus_indicator {
1745            return Ok(());
1746        }
1747
1748        let border_w = settings.focus_width;
1749        let color = [
1750            settings.focus_color[0],
1751            settings.focus_color[1],
1752            settings.focus_color[2],
1753            1.0,
1754        ];
1755
1756        // Create 4 border rectangles (top, bottom, left, right)
1757        let instances = vec![
1758            // Top border
1759            crate::cell_renderer::types::BackgroundInstance {
1760                position: [
1761                    viewport.x / self.size.width as f32 * 2.0 - 1.0,
1762                    1.0 - (viewport.y / self.size.height as f32 * 2.0),
1763                ],
1764                size: [
1765                    viewport.width / self.size.width as f32 * 2.0,
1766                    border_w / self.size.height as f32 * 2.0,
1767                ],
1768                color,
1769            },
1770            // Bottom border
1771            crate::cell_renderer::types::BackgroundInstance {
1772                position: [
1773                    viewport.x / self.size.width as f32 * 2.0 - 1.0,
1774                    1.0 - ((viewport.y + viewport.height - border_w) / self.size.height as f32
1775                        * 2.0),
1776                ],
1777                size: [
1778                    viewport.width / self.size.width as f32 * 2.0,
1779                    border_w / self.size.height as f32 * 2.0,
1780                ],
1781                color,
1782            },
1783            // Left border (between top and bottom)
1784            crate::cell_renderer::types::BackgroundInstance {
1785                position: [
1786                    viewport.x / self.size.width as f32 * 2.0 - 1.0,
1787                    1.0 - ((viewport.y + border_w) / self.size.height as f32 * 2.0),
1788                ],
1789                size: [
1790                    border_w / self.size.width as f32 * 2.0,
1791                    (viewport.height - border_w * 2.0) / self.size.height as f32 * 2.0,
1792                ],
1793                color,
1794            },
1795            // Right border (between top and bottom)
1796            crate::cell_renderer::types::BackgroundInstance {
1797                position: [
1798                    (viewport.x + viewport.width - border_w) / self.size.width as f32 * 2.0 - 1.0,
1799                    1.0 - ((viewport.y + border_w) / self.size.height as f32 * 2.0),
1800                ],
1801                size: [
1802                    border_w / self.size.width as f32 * 2.0,
1803                    (viewport.height - border_w * 2.0) / self.size.height as f32 * 2.0,
1804                ],
1805                color,
1806            },
1807        ];
1808
1809        // Write instances to GPU buffer
1810        self.cell_renderer.queue().write_buffer(
1811            &self.cell_renderer.bg_instance_buffer,
1812            0,
1813            bytemuck::cast_slice(&instances),
1814        );
1815
1816        // Render focus indicator
1817        let mut encoder =
1818            self.cell_renderer
1819                .device()
1820                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1821                    label: Some("focus indicator encoder"),
1822                });
1823
1824        {
1825            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1826                label: Some("focus indicator pass"),
1827                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1828                    view: surface_view,
1829                    resolve_target: None,
1830                    ops: wgpu::Operations {
1831                        load: wgpu::LoadOp::Load, // Don't clear - render on top
1832                        store: wgpu::StoreOp::Store,
1833                    },
1834                    depth_slice: None,
1835                })],
1836                depth_stencil_attachment: None,
1837                timestamp_writes: None,
1838                occlusion_query_set: None,
1839            });
1840
1841            render_pass.set_pipeline(&self.cell_renderer.bg_pipeline);
1842            render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
1843            render_pass.set_vertex_buffer(1, self.cell_renderer.bg_instance_buffer.slice(..));
1844            render_pass.draw(0..4, 0..instances.len() as u32);
1845        }
1846
1847        self.cell_renderer
1848            .queue()
1849            .submit(std::iter::once(encoder.finish()));
1850        Ok(())
1851    }
1852
1853    /// Render pane title bars (background rectangles + text)
1854    ///
1855    /// Title bars are rendered on top of pane content and dividers.
1856    /// Each title bar consists of a colored background rectangle and centered text.
1857    #[allow(dead_code)]
1858    pub fn render_pane_titles(
1859        &mut self,
1860        surface_view: &wgpu::TextureView,
1861        titles: &[PaneTitleInfo],
1862    ) -> Result<()> {
1863        if titles.is_empty() {
1864            return Ok(());
1865        }
1866
1867        let width = self.size.width as f32;
1868        let height = self.size.height as f32;
1869
1870        // Phase 1: Render title bar backgrounds
1871        let mut bg_instances = Vec::with_capacity(titles.len());
1872        for title in titles {
1873            let x_ndc = title.x / width * 2.0 - 1.0;
1874            let y_ndc = 1.0 - (title.y / height * 2.0);
1875            let w_ndc = title.width / width * 2.0;
1876            let h_ndc = title.height / height * 2.0;
1877
1878            // Title bar must be fully opaque (alpha=1.0) to cover the background.
1879            // Differentiate focused/unfocused by lightening/darkening the color.
1880            let brightness = if title.focused { 1.0 } else { 0.7 };
1881
1882            bg_instances.push(crate::cell_renderer::types::BackgroundInstance {
1883                position: [x_ndc, y_ndc],
1884                size: [w_ndc, h_ndc],
1885                color: [
1886                    title.bg_color[0] * brightness,
1887                    title.bg_color[1] * brightness,
1888                    title.bg_color[2] * brightness,
1889                    1.0, // Always fully opaque
1890                ],
1891            });
1892        }
1893
1894        // Write background instances to GPU buffer
1895        self.cell_renderer.queue().write_buffer(
1896            &self.cell_renderer.bg_instance_buffer,
1897            0,
1898            bytemuck::cast_slice(&bg_instances),
1899        );
1900
1901        // Render title backgrounds
1902        let mut encoder =
1903            self.cell_renderer
1904                .device()
1905                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1906                    label: Some("pane title bg encoder"),
1907                });
1908
1909        {
1910            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1911                label: Some("pane title bg pass"),
1912                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1913                    view: surface_view,
1914                    resolve_target: None,
1915                    ops: wgpu::Operations {
1916                        load: wgpu::LoadOp::Load,
1917                        store: wgpu::StoreOp::Store,
1918                    },
1919                    depth_slice: None,
1920                })],
1921                depth_stencil_attachment: None,
1922                timestamp_writes: None,
1923                occlusion_query_set: None,
1924            });
1925
1926            render_pass.set_pipeline(&self.cell_renderer.bg_pipeline);
1927            render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
1928            render_pass.set_vertex_buffer(1, self.cell_renderer.bg_instance_buffer.slice(..));
1929            render_pass.draw(0..4, 0..bg_instances.len() as u32);
1930        }
1931
1932        self.cell_renderer
1933            .queue()
1934            .submit(std::iter::once(encoder.finish()));
1935
1936        // Phase 2: Render title text using glyph atlas
1937        let mut text_instances = Vec::new();
1938        let baseline_y = self.cell_renderer.font_ascent;
1939
1940        for title in titles {
1941            let title_text = &title.title;
1942            if title_text.is_empty() {
1943                continue;
1944            }
1945
1946            // Calculate starting X position (centered in title bar with left padding)
1947            let padding_x = 8.0;
1948            let mut x_pos = title.x + padding_x;
1949            let y_base = title.y + (title.height - self.cell_renderer.cell_height) / 2.0;
1950
1951            let text_color = [
1952                title.text_color[0],
1953                title.text_color[1],
1954                title.text_color[2],
1955                if title.focused { 1.0 } else { 0.8 },
1956            ];
1957
1958            // Truncate title if it would overflow the title bar
1959            let max_chars =
1960                ((title.width - padding_x * 2.0) / self.cell_renderer.cell_width) as usize;
1961            let display_text: String = if title_text.len() > max_chars && max_chars > 3 {
1962                let truncated: String = title_text.chars().take(max_chars - 1).collect();
1963                format!("{}\u{2026}", truncated) // ellipsis
1964            } else {
1965                title_text.clone()
1966            };
1967
1968            for ch in display_text.chars() {
1969                if x_pos >= title.x + title.width - padding_x {
1970                    break;
1971                }
1972
1973                if let Some((font_idx, glyph_id)) =
1974                    self.cell_renderer.font_manager.find_glyph(ch, false, false)
1975                {
1976                    let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
1977                    // Check if this character should be rendered as a monochrome symbol
1978                    let force_monochrome = crate::cell_renderer::atlas::should_render_as_symbol(ch);
1979                    let info = if self.cell_renderer.glyph_cache.contains_key(&cache_key) {
1980                        self.cell_renderer.lru_remove(cache_key);
1981                        self.cell_renderer.lru_push_front(cache_key);
1982                        self.cell_renderer
1983                            .glyph_cache
1984                            .get(&cache_key)
1985                            .unwrap()
1986                            .clone()
1987                    } else if let Some(raster) =
1988                        self.cell_renderer
1989                            .rasterize_glyph(font_idx, glyph_id, force_monochrome)
1990                    {
1991                        let info = self.cell_renderer.upload_glyph(cache_key, &raster);
1992                        self.cell_renderer
1993                            .glyph_cache
1994                            .insert(cache_key, info.clone());
1995                        self.cell_renderer.lru_push_front(cache_key);
1996                        info
1997                    } else {
1998                        x_pos += self.cell_renderer.cell_width;
1999                        continue;
2000                    };
2001
2002                    let glyph_left = x_pos + info.bearing_x;
2003                    let glyph_top = y_base + (baseline_y - info.bearing_y);
2004
2005                    text_instances.push(crate::cell_renderer::types::TextInstance {
2006                        position: [
2007                            glyph_left / width * 2.0 - 1.0,
2008                            1.0 - (glyph_top / height * 2.0),
2009                        ],
2010                        size: [
2011                            info.width as f32 / width * 2.0,
2012                            info.height as f32 / height * 2.0,
2013                        ],
2014                        tex_offset: [info.x as f32 / 2048.0, info.y as f32 / 2048.0],
2015                        tex_size: [info.width as f32 / 2048.0, info.height as f32 / 2048.0],
2016                        color: text_color,
2017                        is_colored: if info.is_colored { 1 } else { 0 },
2018                    });
2019                }
2020
2021                x_pos += self.cell_renderer.cell_width;
2022            }
2023        }
2024
2025        if text_instances.is_empty() {
2026            return Ok(());
2027        }
2028
2029        // Write text instances to GPU buffer
2030        self.cell_renderer.queue().write_buffer(
2031            &self.cell_renderer.text_instance_buffer,
2032            0,
2033            bytemuck::cast_slice(&text_instances),
2034        );
2035
2036        // Render title text
2037        let mut encoder =
2038            self.cell_renderer
2039                .device()
2040                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2041                    label: Some("pane title text encoder"),
2042                });
2043
2044        {
2045            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2046                label: Some("pane title text pass"),
2047                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2048                    view: surface_view,
2049                    resolve_target: None,
2050                    ops: wgpu::Operations {
2051                        load: wgpu::LoadOp::Load,
2052                        store: wgpu::StoreOp::Store,
2053                    },
2054                    depth_slice: None,
2055                })],
2056                depth_stencil_attachment: None,
2057                timestamp_writes: None,
2058                occlusion_query_set: None,
2059            });
2060
2061            render_pass.set_pipeline(&self.cell_renderer.text_pipeline);
2062            render_pass.set_bind_group(0, &self.cell_renderer.text_bind_group, &[]);
2063            render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
2064            render_pass.set_vertex_buffer(1, self.cell_renderer.text_instance_buffer.slice(..));
2065            render_pass.draw(0..4, 0..text_instances.len() as u32);
2066        }
2067
2068        self.cell_renderer
2069            .queue()
2070            .submit(std::iter::once(encoder.finish()));
2071
2072        Ok(())
2073    }
2074
2075    /// Render egui overlay on top of the terminal
2076    fn render_egui(
2077        &mut self,
2078        surface_texture: &wgpu::SurfaceTexture,
2079        egui_output: egui::FullOutput,
2080        egui_ctx: &egui::Context,
2081        force_opaque: bool,
2082    ) -> Result<()> {
2083        use wgpu::TextureViewDescriptor;
2084
2085        // Create view of the surface texture
2086        let view = surface_texture
2087            .texture
2088            .create_view(&TextureViewDescriptor::default());
2089
2090        // Create command encoder for egui
2091        let mut encoder =
2092            self.cell_renderer
2093                .device()
2094                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2095                    label: Some("egui encoder"),
2096                });
2097
2098        // Convert egui output to screen descriptor
2099        let screen_descriptor = egui_wgpu::ScreenDescriptor {
2100            size_in_pixels: [self.size.width, self.size.height],
2101            pixels_per_point: egui_output.pixels_per_point,
2102        };
2103
2104        // Update egui textures
2105        for (id, image_delta) in &egui_output.textures_delta.set {
2106            self.egui_renderer.update_texture(
2107                self.cell_renderer.device(),
2108                self.cell_renderer.queue(),
2109                *id,
2110                image_delta,
2111            );
2112        }
2113
2114        // Tessellate egui shapes into paint jobs
2115        let mut paint_jobs = egui_ctx.tessellate(egui_output.shapes, egui_output.pixels_per_point);
2116
2117        // If requested, force all egui vertices to full opacity so UI stays solid
2118        if force_opaque {
2119            for job in paint_jobs.iter_mut() {
2120                match &mut job.primitive {
2121                    egui::epaint::Primitive::Mesh(mesh) => {
2122                        for v in mesh.vertices.iter_mut() {
2123                            v.color[3] = 255;
2124                        }
2125                    }
2126                    egui::epaint::Primitive::Callback(_) => {}
2127                }
2128            }
2129        }
2130
2131        // Update egui buffers
2132        self.egui_renderer.update_buffers(
2133            self.cell_renderer.device(),
2134            self.cell_renderer.queue(),
2135            &mut encoder,
2136            &paint_jobs,
2137            &screen_descriptor,
2138        );
2139
2140        // Render egui on top of the terminal content
2141        {
2142            let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2143                label: Some("egui render pass"),
2144                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2145                    view: &view,
2146                    resolve_target: None,
2147                    ops: wgpu::Operations {
2148                        load: wgpu::LoadOp::Load, // Don't clear - render on top of terminal
2149                        store: wgpu::StoreOp::Store,
2150                    },
2151                    depth_slice: None,
2152                })],
2153                depth_stencil_attachment: None,
2154                timestamp_writes: None,
2155                occlusion_query_set: None,
2156            });
2157
2158            // Convert to 'static lifetime as required by egui_renderer.render()
2159            let mut render_pass = render_pass.forget_lifetime();
2160
2161            self.egui_renderer
2162                .render(&mut render_pass, &paint_jobs, &screen_descriptor);
2163        } // render_pass dropped here
2164
2165        // Submit egui commands
2166        self.cell_renderer
2167            .queue()
2168            .submit(std::iter::once(encoder.finish()));
2169
2170        // Free egui textures
2171        for id in &egui_output.textures_delta.free {
2172            self.egui_renderer.free_texture(id);
2173        }
2174
2175        Ok(())
2176    }
2177
2178    /// Get the current size
2179    pub fn size(&self) -> PhysicalSize<u32> {
2180        self.size
2181    }
2182
2183    /// Get the current grid dimensions (columns, rows)
2184    pub fn grid_size(&self) -> (usize, usize) {
2185        self.cell_renderer.grid_size()
2186    }
2187
2188    /// Get cell width in pixels
2189    pub fn cell_width(&self) -> f32 {
2190        self.cell_renderer.cell_width()
2191    }
2192
2193    /// Get cell height in pixels
2194    pub fn cell_height(&self) -> f32 {
2195        self.cell_renderer.cell_height()
2196    }
2197
2198    /// Get window padding in physical pixels (scaled by DPI)
2199    pub fn window_padding(&self) -> f32 {
2200        self.cell_renderer.window_padding()
2201    }
2202
2203    /// Get the vertical content offset in physical pixels (e.g., tab bar height scaled by DPI)
2204    pub fn content_offset_y(&self) -> f32 {
2205        self.cell_renderer.content_offset_y()
2206    }
2207
2208    /// Get the display scale factor (e.g., 2.0 on Retina displays)
2209    pub fn scale_factor(&self) -> f32 {
2210        self.cell_renderer.scale_factor
2211    }
2212
2213    /// Set the vertical content offset (e.g., tab bar height) in logical pixels.
2214    /// The offset is scaled by the display scale factor to physical pixels internally,
2215    /// since the cell renderer works in physical pixel coordinates while egui (tab bar)
2216    /// uses logical pixels.
2217    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
2218    pub fn set_content_offset_y(&mut self, logical_offset: f32) -> Option<(usize, usize)> {
2219        // Scale from logical pixels (egui/config) to physical pixels (wgpu surface)
2220        let physical_offset = logical_offset * self.cell_renderer.scale_factor;
2221        let result = self.cell_renderer.set_content_offset_y(physical_offset);
2222        // Always update graphics renderer offset, even if grid size didn't change
2223        self.graphics_renderer.set_content_offset_y(physical_offset);
2224        // Update custom shader renderer content offset
2225        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2226            custom_shader.set_content_offset_y(physical_offset);
2227        }
2228        // Update cursor shader renderer content offset
2229        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2230            cursor_shader.set_content_offset_y(physical_offset);
2231        }
2232        if result.is_some() {
2233            self.dirty = true;
2234        }
2235        result
2236    }
2237
2238    /// Get the horizontal content offset in physical pixels
2239    pub fn content_offset_x(&self) -> f32 {
2240        self.cell_renderer.content_offset_x()
2241    }
2242
2243    /// Set the horizontal content offset (e.g., tab bar on left) in logical pixels.
2244    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
2245    pub fn set_content_offset_x(&mut self, logical_offset: f32) -> Option<(usize, usize)> {
2246        let physical_offset = logical_offset * self.cell_renderer.scale_factor;
2247        let result = self.cell_renderer.set_content_offset_x(physical_offset);
2248        self.graphics_renderer.set_content_offset_x(physical_offset);
2249        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2250            custom_shader.set_content_offset_x(physical_offset);
2251        }
2252        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2253            cursor_shader.set_content_offset_x(physical_offset);
2254        }
2255        if result.is_some() {
2256            self.dirty = true;
2257        }
2258        result
2259    }
2260
2261    /// Get the bottom content inset in physical pixels
2262    pub fn content_inset_bottom(&self) -> f32 {
2263        self.cell_renderer.content_inset_bottom()
2264    }
2265
2266    /// Get the right content inset in physical pixels
2267    pub fn content_inset_right(&self) -> f32 {
2268        self.cell_renderer.content_inset_right()
2269    }
2270
2271    /// Set the bottom content inset (e.g., tab bar at bottom) in logical pixels.
2272    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
2273    pub fn set_content_inset_bottom(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
2274        let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2275        let result = self.cell_renderer.set_content_inset_bottom(physical_inset);
2276        if result.is_some() {
2277            self.dirty = true;
2278        }
2279        result
2280    }
2281
2282    /// Set the right content inset (e.g., AI Inspector panel) in logical pixels.
2283    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
2284    pub fn set_content_inset_right(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
2285        let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2286        let result = self.cell_renderer.set_content_inset_right(physical_inset);
2287
2288        // Also update custom shader renderer to exclude panel area from effects
2289        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2290            custom_shader.set_content_inset_right(physical_inset);
2291        }
2292        // Also update cursor shader renderer
2293        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2294            cursor_shader.set_content_inset_right(physical_inset);
2295        }
2296
2297        if result.is_some() {
2298            self.dirty = true;
2299        }
2300        result
2301    }
2302
2303    /// Set the additional bottom inset from egui panels (status bar, tmux bar).
2304    ///
2305    /// This inset reduces the terminal grid height so content does not render
2306    /// behind the status bar. Also affects scrollbar bounds.
2307    /// Returns `Some((cols, rows))` if the grid was resized.
2308    pub fn set_egui_bottom_inset(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
2309        let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2310        if (self.cell_renderer.egui_bottom_inset - physical_inset).abs() > f32::EPSILON {
2311            self.cell_renderer.egui_bottom_inset = physical_inset;
2312            let (w, h) = (
2313                self.cell_renderer.config.width,
2314                self.cell_renderer.config.height,
2315            );
2316            return Some(self.cell_renderer.resize(w, h));
2317        }
2318        None
2319    }
2320
2321    /// Set the additional right inset from egui panels (AI Inspector).
2322    ///
2323    /// This inset is added to `content_inset_right` for scrollbar bounds only.
2324    /// egui panels already claim space before wgpu rendering, so this doesn't
2325    /// affect the terminal grid sizing.
2326    pub fn set_egui_right_inset(&mut self, logical_inset: f32) {
2327        let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2328        self.cell_renderer.egui_right_inset = physical_inset;
2329    }
2330
2331    /// Check if a point (in pixel coordinates) is within the scrollbar bounds
2332    ///
2333    /// # Arguments
2334    /// * `x` - X coordinate in pixels (from left edge)
2335    /// * `y` - Y coordinate in pixels (from top edge)
2336    pub fn scrollbar_contains_point(&self, x: f32, y: f32) -> bool {
2337        self.cell_renderer.scrollbar_contains_point(x, y)
2338    }
2339
2340    /// Get the scrollbar thumb bounds (top Y, height) in pixels
2341    pub fn scrollbar_thumb_bounds(&self) -> Option<(f32, f32)> {
2342        self.cell_renderer.scrollbar_thumb_bounds()
2343    }
2344
2345    /// Check if an X coordinate is within the scrollbar track
2346    pub fn scrollbar_track_contains_x(&self, x: f32) -> bool {
2347        self.cell_renderer.scrollbar_track_contains_x(x)
2348    }
2349
2350    /// Convert a mouse Y position to a scroll offset
2351    ///
2352    /// # Arguments
2353    /// * `mouse_y` - Mouse Y coordinate in pixels (from top edge)
2354    ///
2355    /// # Returns
2356    /// The scroll offset corresponding to the mouse position, or None if scrollbar is not visible
2357    pub fn scrollbar_mouse_y_to_scroll_offset(&self, mouse_y: f32) -> Option<usize> {
2358        self.cell_renderer
2359            .scrollbar_mouse_y_to_scroll_offset(mouse_y)
2360    }
2361
2362    /// Find a scrollbar mark at the given mouse position for tooltip display.
2363    ///
2364    /// # Arguments
2365    /// * `mouse_x` - Mouse X coordinate in pixels
2366    /// * `mouse_y` - Mouse Y coordinate in pixels
2367    /// * `tolerance` - Maximum distance in pixels to match a mark
2368    ///
2369    /// # Returns
2370    /// The mark at that position, or None if no mark is within tolerance
2371    pub fn scrollbar_mark_at_position(
2372        &self,
2373        mouse_x: f32,
2374        mouse_y: f32,
2375        tolerance: f32,
2376    ) -> Option<&par_term_config::ScrollbackMark> {
2377        self.cell_renderer
2378            .scrollbar_mark_at_position(mouse_x, mouse_y, tolerance)
2379    }
2380
2381    /// Check if the renderer needs to be redrawn
2382    #[allow(dead_code)]
2383    pub fn is_dirty(&self) -> bool {
2384        self.dirty
2385    }
2386
2387    /// Mark the renderer as dirty, forcing a redraw on next render call
2388    #[allow(dead_code)]
2389    pub fn mark_dirty(&mut self) {
2390        self.dirty = true;
2391    }
2392
2393    /// Set debug overlay text to be rendered
2394    #[allow(dead_code)]
2395    #[allow(dead_code)]
2396    pub fn render_debug_overlay(&mut self, text: &str) {
2397        self.debug_text = Some(text.to_string());
2398        self.dirty = true; // Mark dirty to ensure debug overlay renders
2399    }
2400
2401    /// Reconfigure the surface (call when surface becomes outdated or lost)
2402    /// This typically happens when dragging the window between displays
2403    pub fn reconfigure_surface(&mut self) {
2404        self.cell_renderer.reconfigure_surface();
2405        self.dirty = true;
2406    }
2407
2408    /// Check if a vsync mode is supported
2409    pub fn is_vsync_mode_supported(&self, mode: par_term_config::VsyncMode) -> bool {
2410        self.cell_renderer.is_vsync_mode_supported(mode)
2411    }
2412
2413    /// Update the vsync mode. Returns the actual mode applied (may differ if requested mode unsupported).
2414    /// Also returns whether the mode was changed.
2415    pub fn update_vsync_mode(
2416        &mut self,
2417        mode: par_term_config::VsyncMode,
2418    ) -> (par_term_config::VsyncMode, bool) {
2419        let result = self.cell_renderer.update_vsync_mode(mode);
2420        if result.1 {
2421            self.dirty = true;
2422        }
2423        result
2424    }
2425
2426    /// Get the current vsync mode
2427    #[allow(dead_code)]
2428    pub fn current_vsync_mode(&self) -> par_term_config::VsyncMode {
2429        self.cell_renderer.current_vsync_mode()
2430    }
2431
2432    /// Clear the glyph cache to force re-rasterization
2433    /// Useful after display changes where font rendering may differ
2434    pub fn clear_glyph_cache(&mut self) {
2435        self.cell_renderer.clear_glyph_cache();
2436        self.dirty = true;
2437    }
2438
2439    /// Update font anti-aliasing setting
2440    /// Returns true if the setting changed (requiring glyph cache clear)
2441    pub fn update_font_antialias(&mut self, enabled: bool) -> bool {
2442        let changed = self.cell_renderer.update_font_antialias(enabled);
2443        if changed {
2444            self.dirty = true;
2445        }
2446        changed
2447    }
2448
2449    /// Update font hinting setting
2450    /// Returns true if the setting changed (requiring glyph cache clear)
2451    pub fn update_font_hinting(&mut self, enabled: bool) -> bool {
2452        let changed = self.cell_renderer.update_font_hinting(enabled);
2453        if changed {
2454            self.dirty = true;
2455        }
2456        changed
2457    }
2458
2459    /// Update thin strokes mode
2460    /// Returns true if the setting changed (requiring glyph cache clear)
2461    pub fn update_font_thin_strokes(&mut self, mode: par_term_config::ThinStrokesMode) -> bool {
2462        let changed = self.cell_renderer.update_font_thin_strokes(mode);
2463        if changed {
2464            self.dirty = true;
2465        }
2466        changed
2467    }
2468
2469    /// Update minimum contrast ratio
2470    /// Returns true if the setting changed (requiring redraw)
2471    pub fn update_minimum_contrast(&mut self, ratio: f32) -> bool {
2472        let changed = self.cell_renderer.update_minimum_contrast(ratio);
2473        if changed {
2474            self.dirty = true;
2475        }
2476        changed
2477    }
2478
2479    /// Pause shader animations (e.g., when window loses focus)
2480    /// This reduces GPU usage when the terminal is not actively being viewed
2481    pub fn pause_shader_animations(&mut self) {
2482        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2483            custom_shader.set_animation_enabled(false);
2484        }
2485        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2486            cursor_shader.set_animation_enabled(false);
2487        }
2488        log::info!("[SHADER] Shader animations paused");
2489    }
2490
2491    /// Resume shader animations (e.g., when window regains focus)
2492    /// Only resumes if the user's config has animation enabled
2493    pub fn resume_shader_animations(
2494        &mut self,
2495        custom_shader_animation: bool,
2496        cursor_shader_animation: bool,
2497    ) {
2498        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2499            custom_shader.set_animation_enabled(custom_shader_animation);
2500        }
2501        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2502            cursor_shader.set_animation_enabled(cursor_shader_animation);
2503        }
2504        self.dirty = true;
2505        log::info!(
2506            "[SHADER] Shader animations resumed (custom: {}, cursor: {})",
2507            custom_shader_animation,
2508            cursor_shader_animation
2509        );
2510    }
2511
2512    /// Take a screenshot of the current terminal content
2513    /// Returns an RGBA image that can be saved to disk
2514    ///
2515    /// This captures the fully composited output including shader effects.
2516    pub fn take_screenshot(&mut self) -> Result<image::RgbaImage> {
2517        log::info!(
2518            "take_screenshot: Starting screenshot capture ({}x{})",
2519            self.size.width,
2520            self.size.height
2521        );
2522
2523        let width = self.size.width;
2524        let height = self.size.height;
2525        // Use the same format as the surface to match pipeline expectations
2526        let format = self.cell_renderer.surface_format();
2527        log::info!("take_screenshot: Using texture format {:?}", format);
2528
2529        // Create a texture to render the final composited output to (with COPY_SRC for reading back)
2530        let screenshot_texture =
2531            self.cell_renderer
2532                .device()
2533                .create_texture(&wgpu::TextureDescriptor {
2534                    label: Some("screenshot texture"),
2535                    size: wgpu::Extent3d {
2536                        width,
2537                        height,
2538                        depth_or_array_layers: 1,
2539                    },
2540                    mip_level_count: 1,
2541                    sample_count: 1,
2542                    dimension: wgpu::TextureDimension::D2,
2543                    format,
2544                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2545                    view_formats: &[],
2546                });
2547
2548        let screenshot_view =
2549            screenshot_texture.create_view(&wgpu::TextureViewDescriptor::default());
2550
2551        // Render the full composited frame (cells + shaders + overlays)
2552        log::info!("take_screenshot: Rendering composited frame...");
2553
2554        // Check if shaders are enabled
2555        let has_custom_shader = self.custom_shader_renderer.is_some();
2556        let use_cursor_shader =
2557            self.cursor_shader_renderer.is_some() && !self.cursor_shader_disabled_for_alt_screen;
2558
2559        if has_custom_shader {
2560            // Render cells to the custom shader's intermediate texture
2561            let intermediate_view = self
2562                .custom_shader_renderer
2563                .as_ref()
2564                .unwrap()
2565                .intermediate_texture_view()
2566                .clone();
2567            self.cell_renderer
2568                .render_to_texture(&intermediate_view, true)?;
2569
2570            if use_cursor_shader {
2571                // Background shader renders to cursor shader's intermediate texture
2572                let cursor_intermediate = self
2573                    .cursor_shader_renderer
2574                    .as_ref()
2575                    .unwrap()
2576                    .intermediate_texture_view()
2577                    .clone();
2578                self.custom_shader_renderer.as_mut().unwrap().render(
2579                    self.cell_renderer.device(),
2580                    self.cell_renderer.queue(),
2581                    &cursor_intermediate,
2582                    false,
2583                )?;
2584                // Cursor shader renders to screenshot texture
2585                self.cursor_shader_renderer.as_mut().unwrap().render(
2586                    self.cell_renderer.device(),
2587                    self.cell_renderer.queue(),
2588                    &screenshot_view,
2589                    true,
2590                )?;
2591            } else {
2592                // Background shader renders directly to screenshot texture
2593                self.custom_shader_renderer.as_mut().unwrap().render(
2594                    self.cell_renderer.device(),
2595                    self.cell_renderer.queue(),
2596                    &screenshot_view,
2597                    true,
2598                )?;
2599            }
2600        } else if use_cursor_shader {
2601            // Render cells to cursor shader's intermediate texture
2602            let cursor_intermediate = self
2603                .cursor_shader_renderer
2604                .as_ref()
2605                .unwrap()
2606                .intermediate_texture_view()
2607                .clone();
2608            self.cell_renderer
2609                .render_to_texture(&cursor_intermediate, true)?;
2610            // Cursor shader renders to screenshot texture
2611            self.cursor_shader_renderer.as_mut().unwrap().render(
2612                self.cell_renderer.device(),
2613                self.cell_renderer.queue(),
2614                &screenshot_view,
2615                true,
2616            )?;
2617        } else {
2618            // No shaders - render directly to screenshot texture
2619            self.cell_renderer.render_to_view(&screenshot_view)?;
2620        }
2621
2622        log::info!("take_screenshot: Render complete");
2623
2624        // Get device and queue references for buffer operations
2625        let device = self.cell_renderer.device();
2626        let queue = self.cell_renderer.queue();
2627
2628        // Create buffer for reading back the texture
2629        let bytes_per_pixel = 4u32;
2630        let unpadded_bytes_per_row = width * bytes_per_pixel;
2631        // wgpu requires rows to be aligned to 256 bytes
2632        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
2633        let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
2634        let buffer_size = (padded_bytes_per_row * height) as u64;
2635
2636        let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2637            label: Some("screenshot buffer"),
2638            size: buffer_size,
2639            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2640            mapped_at_creation: false,
2641        });
2642
2643        // Copy texture to buffer
2644        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2645            label: Some("screenshot encoder"),
2646        });
2647
2648        encoder.copy_texture_to_buffer(
2649            wgpu::TexelCopyTextureInfo {
2650                texture: &screenshot_texture,
2651                mip_level: 0,
2652                origin: wgpu::Origin3d::ZERO,
2653                aspect: wgpu::TextureAspect::All,
2654            },
2655            wgpu::TexelCopyBufferInfo {
2656                buffer: &output_buffer,
2657                layout: wgpu::TexelCopyBufferLayout {
2658                    offset: 0,
2659                    bytes_per_row: Some(padded_bytes_per_row),
2660                    rows_per_image: Some(height),
2661                },
2662            },
2663            wgpu::Extent3d {
2664                width,
2665                height,
2666                depth_or_array_layers: 1,
2667            },
2668        );
2669
2670        queue.submit(std::iter::once(encoder.finish()));
2671        log::info!("take_screenshot: Texture copy submitted");
2672
2673        // Map the buffer and read the data
2674        let buffer_slice = output_buffer.slice(..);
2675        let (tx, rx) = std::sync::mpsc::channel();
2676        buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
2677            let _ = tx.send(result);
2678        });
2679
2680        // Wait for GPU to finish
2681        log::info!("take_screenshot: Waiting for GPU...");
2682        let _ = device.poll(wgpu::PollType::wait_indefinitely());
2683        log::info!("take_screenshot: GPU poll complete, waiting for buffer map...");
2684        rx.recv()
2685            .map_err(|e| anyhow::anyhow!("Failed to receive map result: {}", e))?
2686            .map_err(|e| anyhow::anyhow!("Failed to map buffer: {:?}", e))?;
2687        log::info!("take_screenshot: Buffer mapped successfully");
2688
2689        // Read the data
2690        let data = buffer_slice.get_mapped_range();
2691        let mut pixels = Vec::with_capacity((width * height * 4) as usize);
2692
2693        // Check if format is BGRA (needs swizzle) or RGBA (direct copy)
2694        let is_bgra = matches!(
2695            format,
2696            wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
2697        );
2698
2699        // Copy data row by row (to handle padding)
2700        for y in 0..height {
2701            let row_start = (y * padded_bytes_per_row) as usize;
2702            let row_end = row_start + (width * bytes_per_pixel) as usize;
2703            let row = &data[row_start..row_end];
2704
2705            if is_bgra {
2706                // Convert BGRA to RGBA
2707                for chunk in row.chunks(4) {
2708                    pixels.push(chunk[2]); // R (was B)
2709                    pixels.push(chunk[1]); // G
2710                    pixels.push(chunk[0]); // B (was R)
2711                    pixels.push(chunk[3]); // A
2712                }
2713            } else {
2714                // Already RGBA, direct copy
2715                pixels.extend_from_slice(row);
2716            }
2717        }
2718
2719        drop(data);
2720        output_buffer.unmap();
2721
2722        // Create image
2723        image::RgbaImage::from_raw(width, height, pixels)
2724            .ok_or_else(|| anyhow::anyhow!("Failed to create image from pixel data"))
2725    }
2726}