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    /// Set whether cursor shader should be disabled due to alt screen being active
748    ///
749    /// When alt screen is active (e.g., vim, htop, less), cursor shader effects
750    /// are disabled since TUI applications typically have their own cursor handling.
751    pub fn set_cursor_shader_disabled_for_alt_screen(&mut self, disabled: bool) {
752        if self.cursor_shader_disabled_for_alt_screen != disabled {
753            log::debug!("[cursor-shader] Alt-screen disable set to {}", disabled);
754            self.cursor_shader_disabled_for_alt_screen = disabled;
755        } else {
756            self.cursor_shader_disabled_for_alt_screen = disabled;
757        }
758    }
759
760    /// Update window padding in real-time without full renderer rebuild.
761    /// Accepts logical pixels (from config); scales to physical pixels internally.
762    /// Returns Some((cols, rows)) if grid size changed and terminal needs resize.
763    #[allow(dead_code)]
764    pub fn update_window_padding(&mut self, logical_padding: f32) -> Option<(usize, usize)> {
765        let physical_padding = logical_padding * self.cell_renderer.scale_factor;
766        let result = self.cell_renderer.update_window_padding(physical_padding);
767        // Update graphics renderer padding
768        self.graphics_renderer.update_cell_dimensions(
769            self.cell_renderer.cell_width(),
770            self.cell_renderer.cell_height(),
771            physical_padding,
772        );
773        // Update custom shader renderer padding
774        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
775            custom_shader.update_cell_dimensions(
776                self.cell_renderer.cell_width(),
777                self.cell_renderer.cell_height(),
778                physical_padding,
779            );
780        }
781        // Update cursor shader renderer padding
782        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
783            cursor_shader.update_cell_dimensions(
784                self.cell_renderer.cell_width(),
785                self.cell_renderer.cell_height(),
786                physical_padding,
787            );
788        }
789        self.dirty = true;
790        result
791    }
792
793    /// Enable/disable background image and reload if needed
794    #[allow(dead_code)]
795    pub fn set_background_image_enabled(
796        &mut self,
797        enabled: bool,
798        path: Option<&str>,
799        mode: par_term_config::BackgroundImageMode,
800        opacity: f32,
801    ) {
802        let path = if enabled { path } else { None };
803        self.cell_renderer.set_background_image(path, mode, opacity);
804
805        // Sync background texture to custom shader if it's using background as channel0
806        self.sync_background_texture_to_shader();
807
808        self.dirty = true;
809    }
810
811    /// Set background based on mode (Default, Color, or Image).
812    ///
813    /// This unified method handles all background types and syncs with shaders.
814    pub fn set_background(
815        &mut self,
816        mode: par_term_config::BackgroundMode,
817        color: [u8; 3],
818        image_path: Option<&str>,
819        image_mode: par_term_config::BackgroundImageMode,
820        image_opacity: f32,
821        image_enabled: bool,
822    ) {
823        self.cell_renderer.set_background(
824            mode,
825            color,
826            image_path,
827            image_mode,
828            image_opacity,
829            image_enabled,
830        );
831
832        // Sync background texture to custom shader if it's using background as channel0
833        self.sync_background_texture_to_shader();
834
835        // Sync background to shaders for proper compositing
836        let is_solid_color = matches!(mode, par_term_config::BackgroundMode::Color);
837        let is_image_mode = matches!(mode, par_term_config::BackgroundMode::Image);
838        let normalized_color = [
839            color[0] as f32 / 255.0,
840            color[1] as f32 / 255.0,
841            color[2] as f32 / 255.0,
842        ];
843
844        // Sync to cursor shader
845        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
846            // When background shader is enabled and chained into cursor shader,
847            // don't give cursor shader its own background - background shader handles it
848            let has_background_shader = self.custom_shader_renderer.is_some();
849
850            if has_background_shader {
851                // Background shader handles the background, cursor shader just passes through
852                cursor_shader.set_background_color([0.0, 0.0, 0.0], false);
853                cursor_shader.set_background_texture(self.cell_renderer.device(), None);
854                cursor_shader.update_use_background_as_channel0(self.cell_renderer.device(), false);
855            } else {
856                cursor_shader.set_background_color(normalized_color, is_solid_color);
857
858                // For image mode, pass background image as iChannel0
859                if is_image_mode && image_enabled {
860                    let bg_texture = self.cell_renderer.get_background_as_channel_texture();
861                    cursor_shader.set_background_texture(self.cell_renderer.device(), bg_texture);
862                    cursor_shader
863                        .update_use_background_as_channel0(self.cell_renderer.device(), true);
864                } else {
865                    // Clear background texture when not in image mode
866                    cursor_shader.set_background_texture(self.cell_renderer.device(), None);
867                    cursor_shader
868                        .update_use_background_as_channel0(self.cell_renderer.device(), false);
869                }
870            }
871        }
872
873        // Sync to custom shader
874        // Note: We don't pass is_solid_color=true to custom shaders because
875        // that would replace the shader output with a solid color, making the
876        // shader invisible. Custom shaders handle their own background.
877        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
878            custom_shader.set_background_color(normalized_color, false);
879        }
880
881        self.dirty = true;
882    }
883
884    /// Update scrollbar appearance in real-time.
885    /// Width is in logical pixels and will be scaled to physical pixels internally.
886    pub fn update_scrollbar_appearance(
887        &mut self,
888        logical_width: f32,
889        thumb_color: [f32; 4],
890        track_color: [f32; 4],
891    ) {
892        let physical_width = logical_width * self.cell_renderer.scale_factor;
893        self.cell_renderer
894            .update_scrollbar_appearance(physical_width, thumb_color, track_color);
895        self.dirty = true;
896    }
897
898    /// Update scrollbar position (left/right) in real-time
899    #[allow(dead_code)]
900    pub fn update_scrollbar_position(&mut self, position: &str) {
901        self.cell_renderer.update_scrollbar_position(position);
902        self.dirty = true;
903    }
904
905    /// Update background image opacity in real-time
906    #[allow(dead_code)]
907    pub fn update_background_image_opacity(&mut self, opacity: f32) {
908        self.cell_renderer.update_background_image_opacity(opacity);
909        self.dirty = true;
910    }
911
912    /// Load a per-pane background image into the texture cache.
913    /// Delegates to CellRenderer::load_pane_background.
914    pub fn load_pane_background(&mut self, path: &str) -> anyhow::Result<bool> {
915        self.cell_renderer.load_pane_background(path)
916    }
917
918    /// Update inline image scaling mode (nearest vs linear filtering).
919    ///
920    /// Recreates the GPU sampler and clears the texture cache so images
921    /// are re-rendered with the new filter mode.
922    pub fn update_image_scaling_mode(&mut self, scaling_mode: par_term_config::ImageScalingMode) {
923        self.graphics_renderer
924            .update_scaling_mode(self.cell_renderer.device(), scaling_mode);
925        self.dirty = true;
926    }
927
928    /// Update whether inline images preserve their aspect ratio.
929    pub fn update_image_preserve_aspect_ratio(&mut self, preserve: bool) {
930        self.graphics_renderer.set_preserve_aspect_ratio(preserve);
931        self.dirty = true;
932    }
933
934    /// Check if animation requires continuous rendering
935    ///
936    /// Returns true if shader animation is enabled or a cursor trail animation
937    /// might still be in progress.
938    pub fn needs_continuous_render(&self) -> bool {
939        let custom_needs = self
940            .custom_shader_renderer
941            .as_ref()
942            .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
943        let cursor_needs = self
944            .cursor_shader_renderer
945            .as_ref()
946            .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
947        custom_needs || cursor_needs
948    }
949
950    /// Render a frame with optional egui overlay
951    /// Returns true if rendering was performed, false if skipped
952    pub fn render(
953        &mut self,
954        egui_data: Option<(egui::FullOutput, &egui::Context)>,
955        force_egui_opaque: bool,
956        show_scrollbar: bool,
957        pane_background: Option<&par_term_config::PaneBackground>,
958    ) -> Result<bool> {
959        // Custom shader animation forces continuous rendering
960        let force_render = self.needs_continuous_render();
961
962        if !self.dirty && egui_data.is_none() && !force_render {
963            // Skip rendering if nothing has changed
964            return Ok(false);
965        }
966
967        // Check if shaders are enabled
968        let has_custom_shader = self.custom_shader_renderer.is_some();
969        // Only use cursor shader if it's enabled and not disabled for alt screen
970        let use_cursor_shader =
971            self.cursor_shader_renderer.is_some() && !self.cursor_shader_disabled_for_alt_screen;
972
973        // Cell renderer renders terminal content
974        let t1 = std::time::Instant::now();
975        let surface_texture = if has_custom_shader {
976            // When custom shader is enabled, always skip rendering background image
977            // to the intermediate texture. The shader controls the background:
978            // - If user wants background image in shader, enable use_background_as_channel0
979            // - Otherwise, the shader's own effects provide the background
980            // This prevents the background image from being treated as "terminal content"
981            // and passed through unchanged by the shader.
982
983            // Render terminal to intermediate texture for background shader
984            self.cell_renderer.render_to_texture(
985                self.custom_shader_renderer
986                    .as_ref()
987                    .unwrap()
988                    .intermediate_texture_view(),
989                true, // Always skip background image - shader handles background
990            )?
991        } else if use_cursor_shader {
992            // Render terminal to intermediate texture for cursor shader
993            // Skip background image - it will be handled via iBackgroundColor uniform
994            // or passed as iChannel0. This ensures proper opacity handling.
995            self.cell_renderer.render_to_texture(
996                self.cursor_shader_renderer
997                    .as_ref()
998                    .unwrap()
999                    .intermediate_texture_view(),
1000                true, // Skip background image - shader handles it
1001            )?
1002        } else {
1003            // Render directly to surface (no shaders, or cursor shader disabled for alt screen)
1004            // Note: scrollbar is rendered separately after egui so it appears on top
1005            self.cell_renderer.render(show_scrollbar, pane_background)?
1006        };
1007        let cell_render_time = t1.elapsed();
1008
1009        // Apply background custom shader if enabled
1010        let t_custom = std::time::Instant::now();
1011        let custom_shader_time = if let Some(ref mut custom_shader) = self.custom_shader_renderer {
1012            if use_cursor_shader {
1013                // Background shader renders to cursor shader's intermediate texture
1014                // Don't apply opacity here - cursor shader will apply it when rendering to surface
1015                custom_shader.render(
1016                    self.cell_renderer.device(),
1017                    self.cell_renderer.queue(),
1018                    self.cursor_shader_renderer
1019                        .as_ref()
1020                        .unwrap()
1021                        .intermediate_texture_view(),
1022                    false, // Don't apply opacity - cursor shader will do it
1023                )?;
1024            } else {
1025                // Background shader renders directly to surface
1026                // (cursor shader disabled for alt screen or not configured)
1027                let surface_view = surface_texture
1028                    .texture
1029                    .create_view(&wgpu::TextureViewDescriptor::default());
1030                custom_shader.render(
1031                    self.cell_renderer.device(),
1032                    self.cell_renderer.queue(),
1033                    &surface_view,
1034                    true, // Apply opacity - this is the final render
1035                )?;
1036            }
1037            t_custom.elapsed()
1038        } else {
1039            std::time::Duration::ZERO
1040        };
1041
1042        // Apply cursor shader if enabled (skip when alt screen is active for TUI apps)
1043        let t_cursor = std::time::Instant::now();
1044        let cursor_shader_time = if use_cursor_shader {
1045            log::trace!("Rendering cursor shader");
1046            let cursor_shader = self.cursor_shader_renderer.as_mut().unwrap();
1047            let surface_view = surface_texture
1048                .texture
1049                .create_view(&wgpu::TextureViewDescriptor::default());
1050
1051            cursor_shader.render(
1052                self.cell_renderer.device(),
1053                self.cell_renderer.queue(),
1054                &surface_view,
1055                true, // Apply opacity - this is the final render to surface
1056            )?;
1057            t_cursor.elapsed()
1058        } else {
1059            if self.cursor_shader_disabled_for_alt_screen {
1060                log::trace!("Skipping cursor shader - alt screen active");
1061            }
1062            std::time::Duration::ZERO
1063        };
1064
1065        // Render sixel graphics on top of cells
1066        let t2 = std::time::Instant::now();
1067        if !self.sixel_graphics.is_empty() {
1068            self.render_sixel_graphics(&surface_texture)?;
1069        }
1070        let sixel_render_time = t2.elapsed();
1071
1072        // Render overlays (scrollbar, visual bell) BEFORE egui so that modal
1073        // dialogs (egui) render on top of the scrollbar. The scrollbar track
1074        // already accounts for status bar inset via content_inset_bottom.
1075        self.cell_renderer
1076            .render_overlays(&surface_texture, show_scrollbar)?;
1077
1078        // Render egui overlay if provided
1079        let t3 = std::time::Instant::now();
1080        if let Some((egui_output, egui_ctx)) = egui_data {
1081            self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
1082        }
1083        let egui_render_time = t3.elapsed();
1084
1085        // Present the surface texture - THIS IS WHERE VSYNC WAIT HAPPENS
1086        let t4 = std::time::Instant::now();
1087        surface_texture.present();
1088        let present_time = t4.elapsed();
1089
1090        // Log timing breakdown
1091        let total = cell_render_time
1092            + custom_shader_time
1093            + cursor_shader_time
1094            + sixel_render_time
1095            + egui_render_time
1096            + present_time;
1097        if present_time.as_millis() > 10 || total.as_millis() > 10 {
1098            log::info!(
1099                "[RENDER] RENDER_BREAKDOWN: CellRender={:.2}ms BgShader={:.2}ms CursorShader={:.2}ms Sixel={:.2}ms Egui={:.2}ms PRESENT={:.2}ms Total={:.2}ms",
1100                cell_render_time.as_secs_f64() * 1000.0,
1101                custom_shader_time.as_secs_f64() * 1000.0,
1102                cursor_shader_time.as_secs_f64() * 1000.0,
1103                sixel_render_time.as_secs_f64() * 1000.0,
1104                egui_render_time.as_secs_f64() * 1000.0,
1105                present_time.as_secs_f64() * 1000.0,
1106                total.as_secs_f64() * 1000.0
1107            );
1108        }
1109
1110        // Clear dirty flag after successful render
1111        self.dirty = false;
1112
1113        Ok(true)
1114    }
1115
1116    /// Render multiple panes to the surface
1117    ///
1118    /// This method renders each pane's content to its viewport region,
1119    /// handling focus indicators and inactive pane dimming.
1120    ///
1121    /// # Arguments
1122    /// * `panes` - List of panes to render with their viewport info
1123    /// * `egui_data` - Optional egui overlay data
1124    /// * `force_egui_opaque` - Force egui to render at full opacity
1125    ///
1126    /// # Returns
1127    /// `true` if rendering was performed, `false` if skipped
1128    #[allow(dead_code)]
1129    pub fn render_panes(
1130        &mut self,
1131        panes: &[PaneRenderInfo<'_>],
1132        egui_data: Option<(egui::FullOutput, &egui::Context)>,
1133        force_egui_opaque: bool,
1134    ) -> Result<bool> {
1135        // Check if we need to render
1136        let force_render = self.needs_continuous_render();
1137        if !self.dirty && egui_data.is_none() && !force_render {
1138            return Ok(false);
1139        }
1140
1141        // Get the surface texture
1142        let surface_texture = self.cell_renderer.surface.get_current_texture()?;
1143        let surface_view = surface_texture
1144            .texture
1145            .create_view(&wgpu::TextureViewDescriptor::default());
1146
1147        // Clear the surface first with the background color (respecting solid color mode)
1148        {
1149            let mut encoder = self.cell_renderer.device().create_command_encoder(
1150                &wgpu::CommandEncoderDescriptor {
1151                    label: Some("pane clear encoder"),
1152                },
1153            );
1154
1155            let opacity = self.cell_renderer.window_opacity as f64;
1156            let clear_color = if self.cell_renderer.bg_is_solid_color {
1157                wgpu::Color {
1158                    r: self.cell_renderer.solid_bg_color[0] as f64 * opacity,
1159                    g: self.cell_renderer.solid_bg_color[1] as f64 * opacity,
1160                    b: self.cell_renderer.solid_bg_color[2] as f64 * opacity,
1161                    a: opacity,
1162                }
1163            } else {
1164                wgpu::Color {
1165                    r: self.cell_renderer.background_color[0] as f64 * opacity,
1166                    g: self.cell_renderer.background_color[1] as f64 * opacity,
1167                    b: self.cell_renderer.background_color[2] as f64 * opacity,
1168                    a: opacity,
1169                }
1170            };
1171
1172            {
1173                let _clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1174                    label: Some("surface clear pass"),
1175                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1176                        view: &surface_view,
1177                        resolve_target: None,
1178                        ops: wgpu::Operations {
1179                            load: wgpu::LoadOp::Clear(clear_color),
1180                            store: wgpu::StoreOp::Store,
1181                        },
1182                        depth_slice: None,
1183                    })],
1184                    depth_stencil_attachment: None,
1185                    timestamp_writes: None,
1186                    occlusion_query_set: None,
1187                });
1188            }
1189
1190            self.cell_renderer
1191                .queue()
1192                .submit(std::iter::once(encoder.finish()));
1193        }
1194
1195        // Render background image first (full-screen, before panes)
1196        let has_background_image = self
1197            .cell_renderer
1198            .render_background_only(&surface_view, false)?;
1199
1200        // Render each pane (skip background image since we rendered it full-screen)
1201        for pane in panes {
1202            let separator_marks = compute_visible_separator_marks(
1203                &pane.marks,
1204                pane.scrollback_len,
1205                pane.scroll_offset,
1206                pane.grid_size.1,
1207            );
1208            self.cell_renderer.render_pane_to_view(
1209                &surface_view,
1210                &pane.viewport,
1211                pane.cells,
1212                pane.grid_size.0,
1213                pane.grid_size.1,
1214                pane.cursor_pos,
1215                pane.cursor_opacity,
1216                pane.show_scrollbar,
1217                false,                // Don't clear - we already cleared the surface
1218                has_background_image, // Skip background image if already rendered full-screen
1219                &separator_marks,
1220                pane.background.as_ref(),
1221            )?;
1222        }
1223
1224        // Render egui overlay if provided
1225        if let Some((egui_output, egui_ctx)) = egui_data {
1226            self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
1227        }
1228
1229        // Present the surface
1230        surface_texture.present();
1231
1232        self.dirty = false;
1233        Ok(true)
1234    }
1235
1236    /// Render split panes with dividers and focus indicator
1237    ///
1238    /// This is the main entry point for rendering a split pane layout.
1239    /// It handles:
1240    /// 1. Clearing the surface
1241    /// 2. Rendering each pane's content
1242    /// 3. Rendering dividers between panes
1243    /// 4. Rendering focus indicator around the focused pane
1244    /// 5. Rendering egui overlay if provided
1245    /// 6. Presenting the surface
1246    ///
1247    /// # Arguments
1248    /// * `panes` - List of panes to render with their viewport info
1249    /// * `dividers` - List of dividers between panes with hover state
1250    /// * `focused_viewport` - Viewport of the focused pane (for focus indicator)
1251    /// * `divider_settings` - Settings for divider and focus indicator appearance
1252    /// * `egui_data` - Optional egui overlay data
1253    /// * `force_egui_opaque` - Force egui to render at full opacity
1254    ///
1255    /// # Returns
1256    /// `true` if rendering was performed, `false` if skipped
1257    #[allow(dead_code, clippy::too_many_arguments)]
1258    pub fn render_split_panes(
1259        &mut self,
1260        panes: &[PaneRenderInfo<'_>],
1261        dividers: &[DividerRenderInfo],
1262        pane_titles: &[PaneTitleInfo],
1263        focused_viewport: Option<&PaneViewport>,
1264        divider_settings: &PaneDividerSettings,
1265        egui_data: Option<(egui::FullOutput, &egui::Context)>,
1266        force_egui_opaque: bool,
1267    ) -> Result<bool> {
1268        // Check if we need to render
1269        let force_render = self.needs_continuous_render();
1270        if !self.dirty && egui_data.is_none() && !force_render {
1271            return Ok(false);
1272        }
1273
1274        let has_custom_shader = self.custom_shader_renderer.is_some();
1275
1276        // Pre-load any per-pane background textures that aren't cached yet
1277        for pane in panes.iter() {
1278            if let Some(ref bg) = pane.background
1279                && let Some(ref path) = bg.image_path
1280                && let Err(e) = self.cell_renderer.load_pane_background(path)
1281            {
1282                log::error!("Failed to load pane background '{}': {}", path, e);
1283            }
1284        }
1285
1286        // Get the surface texture
1287        let surface_texture = self.cell_renderer.surface.get_current_texture()?;
1288        let surface_view = surface_texture
1289            .texture
1290            .create_view(&wgpu::TextureViewDescriptor::default());
1291
1292        // Clear the surface with background color (respecting solid color mode)
1293        let opacity = self.cell_renderer.window_opacity as f64;
1294        let clear_color = if self.cell_renderer.bg_is_solid_color {
1295            wgpu::Color {
1296                r: self.cell_renderer.solid_bg_color[0] as f64 * opacity,
1297                g: self.cell_renderer.solid_bg_color[1] as f64 * opacity,
1298                b: self.cell_renderer.solid_bg_color[2] as f64 * opacity,
1299                a: opacity,
1300            }
1301        } else {
1302            wgpu::Color {
1303                r: self.cell_renderer.background_color[0] as f64 * opacity,
1304                g: self.cell_renderer.background_color[1] as f64 * opacity,
1305                b: self.cell_renderer.background_color[2] as f64 * opacity,
1306                a: opacity,
1307            }
1308        };
1309
1310        // If custom shader is enabled, render it with the background clear color
1311        // (the shader's render pass will handle clearing the surface)
1312        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
1313            // Clear the intermediate texture to remove any old single-pane content
1314            // This prevents the shader from displaying stale terminal content
1315            custom_shader.clear_intermediate_texture(
1316                self.cell_renderer.device(),
1317                self.cell_renderer.queue(),
1318            );
1319
1320            // Render shader effect to surface with background color as clear
1321            // Don't apply opacity here - pane cells will blend on top
1322            custom_shader.render_with_clear_color(
1323                self.cell_renderer.device(),
1324                self.cell_renderer.queue(),
1325                &surface_view,
1326                false, // Don't apply opacity - let pane rendering handle it
1327                clear_color,
1328            )?;
1329        } else {
1330            // No custom shader - just clear the surface with background color
1331            let mut encoder = self.cell_renderer.device().create_command_encoder(
1332                &wgpu::CommandEncoderDescriptor {
1333                    label: Some("split pane clear encoder"),
1334                },
1335            );
1336
1337            {
1338                let _clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1339                    label: Some("surface clear pass"),
1340                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1341                        view: &surface_view,
1342                        resolve_target: None,
1343                        ops: wgpu::Operations {
1344                            load: wgpu::LoadOp::Clear(clear_color),
1345                            store: wgpu::StoreOp::Store,
1346                        },
1347                        depth_slice: None,
1348                    })],
1349                    depth_stencil_attachment: None,
1350                    timestamp_writes: None,
1351                    occlusion_query_set: None,
1352                });
1353            }
1354
1355            self.cell_renderer
1356                .queue()
1357                .submit(std::iter::once(encoder.finish()));
1358        }
1359
1360        // Render background image (full-screen, after shader but before panes)
1361        // Skip if custom shader is handling the background.
1362        // Also skip if any pane has a per-pane background configured -
1363        // per-pane backgrounds are rendered individually in render_pane_to_view.
1364        let any_pane_has_background = panes.iter().any(|p| p.background.is_some());
1365        let has_background_image = if !has_custom_shader && !any_pane_has_background {
1366            self.cell_renderer
1367                .render_background_only(&surface_view, false)?
1368        } else {
1369            false
1370        };
1371
1372        // Render each pane's content (skip background image since we rendered it full-screen)
1373        for pane in panes {
1374            let separator_marks = compute_visible_separator_marks(
1375                &pane.marks,
1376                pane.scrollback_len,
1377                pane.scroll_offset,
1378                pane.grid_size.1,
1379            );
1380            self.cell_renderer.render_pane_to_view(
1381                &surface_view,
1382                &pane.viewport,
1383                pane.cells,
1384                pane.grid_size.0,
1385                pane.grid_size.1,
1386                pane.cursor_pos,
1387                pane.cursor_opacity,
1388                pane.show_scrollbar,
1389                false, // Don't clear - we already cleared the surface
1390                has_background_image || has_custom_shader, // Skip background if already rendered
1391                &separator_marks,
1392                pane.background.as_ref(),
1393            )?;
1394        }
1395
1396        // Render dividers between panes
1397        if !dividers.is_empty() {
1398            self.render_dividers(&surface_view, dividers, divider_settings)?;
1399        }
1400
1401        // Render pane title bars (background + text)
1402        if !pane_titles.is_empty() {
1403            self.render_pane_titles(&surface_view, pane_titles)?;
1404        }
1405
1406        // Render focus indicator around focused pane (only if multiple panes)
1407        if panes.len() > 1
1408            && let Some(viewport) = focused_viewport
1409        {
1410            self.render_focus_indicator(&surface_view, viewport, divider_settings)?;
1411        }
1412
1413        // Render egui overlay if provided
1414        if let Some((egui_output, egui_ctx)) = egui_data {
1415            self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
1416        }
1417
1418        // Present the surface
1419        surface_texture.present();
1420
1421        self.dirty = false;
1422        Ok(true)
1423    }
1424
1425    /// Render pane dividers on top of pane content
1426    ///
1427    /// This should be called after rendering pane content but before egui.
1428    ///
1429    /// # Arguments
1430    /// * `surface_view` - The texture view to render to
1431    /// * `dividers` - List of dividers to render with hover state
1432    /// * `settings` - Divider appearance settings
1433    #[allow(dead_code)]
1434    pub fn render_dividers(
1435        &mut self,
1436        surface_view: &wgpu::TextureView,
1437        dividers: &[DividerRenderInfo],
1438        settings: &PaneDividerSettings,
1439    ) -> Result<()> {
1440        if dividers.is_empty() {
1441            return Ok(());
1442        }
1443
1444        // Build divider instances using the cell renderer's background pipeline
1445        // We reuse the bg_instances buffer for dividers
1446        let mut instances = Vec::with_capacity(dividers.len() * 3); // Extra capacity for multi-rect styles
1447
1448        let w = self.size.width as f32;
1449        let h = self.size.height as f32;
1450
1451        for divider in dividers {
1452            let color = if divider.hovered {
1453                settings.hover_color
1454            } else {
1455                settings.divider_color
1456            };
1457
1458            use par_term_config::DividerStyle;
1459            match settings.divider_style {
1460                DividerStyle::Solid => {
1461                    let x_ndc = divider.x / w * 2.0 - 1.0;
1462                    let y_ndc = 1.0 - (divider.y / h * 2.0);
1463                    let w_ndc = divider.width / w * 2.0;
1464                    let h_ndc = divider.height / h * 2.0;
1465
1466                    instances.push(crate::cell_renderer::types::BackgroundInstance {
1467                        position: [x_ndc, y_ndc],
1468                        size: [w_ndc, h_ndc],
1469                        color: [color[0], color[1], color[2], 1.0],
1470                    });
1471                }
1472                DividerStyle::Double => {
1473                    // Two parallel lines with a visible gap between them
1474                    let is_horizontal = divider.width > divider.height;
1475                    let thickness = if is_horizontal {
1476                        divider.height
1477                    } else {
1478                        divider.width
1479                    };
1480
1481                    if thickness >= 4.0 {
1482                        // Enough space for two 1px lines with visible gap
1483                        if is_horizontal {
1484                            // Top line
1485                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1486                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1487                                size: [divider.width / w * 2.0, 1.0 / h * 2.0],
1488                                color: [color[0], color[1], color[2], 1.0],
1489                            });
1490                            // Bottom line (gap in between shows background)
1491                            let bottom_y = divider.y + divider.height - 1.0;
1492                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1493                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (bottom_y / h * 2.0)],
1494                                size: [divider.width / w * 2.0, 1.0 / h * 2.0],
1495                                color: [color[0], color[1], color[2], 1.0],
1496                            });
1497                        } else {
1498                            // Left line
1499                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1500                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1501                                size: [1.0 / w * 2.0, divider.height / h * 2.0],
1502                                color: [color[0], color[1], color[2], 1.0],
1503                            });
1504                            // Right line
1505                            let right_x = divider.x + divider.width - 1.0;
1506                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1507                                position: [right_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1508                                size: [1.0 / w * 2.0, divider.height / h * 2.0],
1509                                color: [color[0], color[1], color[2], 1.0],
1510                            });
1511                        }
1512                    } else {
1513                        // Divider too thin for double lines — render centered 1px line
1514                        // (visibly thinner than Solid to differentiate)
1515                        if is_horizontal {
1516                            let center_y = divider.y + (divider.height - 1.0) / 2.0;
1517                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1518                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (center_y / h * 2.0)],
1519                                size: [divider.width / w * 2.0, 1.0 / h * 2.0],
1520                                color: [color[0], color[1], color[2], 1.0],
1521                            });
1522                        } else {
1523                            let center_x = divider.x + (divider.width - 1.0) / 2.0;
1524                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1525                                position: [center_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1526                                size: [1.0 / w * 2.0, divider.height / h * 2.0],
1527                                color: [color[0], color[1], color[2], 1.0],
1528                            });
1529                        }
1530                    }
1531                }
1532                DividerStyle::Dashed => {
1533                    // Dashed line effect using segments
1534                    let is_horizontal = divider.width > divider.height;
1535                    let dash_len: f32 = 6.0;
1536                    let gap_len: f32 = 4.0;
1537
1538                    if is_horizontal {
1539                        let mut x = divider.x;
1540                        while x < divider.x + divider.width {
1541                            let seg_w = dash_len.min(divider.x + divider.width - x);
1542                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1543                                position: [x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1544                                size: [seg_w / w * 2.0, divider.height / h * 2.0],
1545                                color: [color[0], color[1], color[2], 1.0],
1546                            });
1547                            x += dash_len + gap_len;
1548                        }
1549                    } else {
1550                        let mut y = divider.y;
1551                        while y < divider.y + divider.height {
1552                            let seg_h = dash_len.min(divider.y + divider.height - y);
1553                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1554                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (y / h * 2.0)],
1555                                size: [divider.width / w * 2.0, seg_h / h * 2.0],
1556                                color: [color[0], color[1], color[2], 1.0],
1557                            });
1558                            y += dash_len + gap_len;
1559                        }
1560                    }
1561                }
1562                DividerStyle::Shadow => {
1563                    // Beveled/embossed effect — all rendering stays within divider bounds
1564                    // Highlight on top/left edge, shadow on bottom/right edge
1565                    let is_horizontal = divider.width > divider.height;
1566                    let thickness = if is_horizontal {
1567                        divider.height
1568                    } else {
1569                        divider.width
1570                    };
1571
1572                    // Brighter highlight color
1573                    let highlight = [
1574                        (color[0] + 0.3).min(1.0),
1575                        (color[1] + 0.3).min(1.0),
1576                        (color[2] + 0.3).min(1.0),
1577                        1.0,
1578                    ];
1579                    // Darker shadow color
1580                    let shadow = [(color[0] * 0.3), (color[1] * 0.3), (color[2] * 0.3), 1.0];
1581
1582                    if thickness >= 3.0 {
1583                        // 3+ px: highlight line / main body / shadow line
1584                        let edge = 1.0_f32;
1585                        if is_horizontal {
1586                            // Top highlight
1587                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1588                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1589                                size: [divider.width / w * 2.0, edge / h * 2.0],
1590                                color: highlight,
1591                            });
1592                            // Main body (middle portion)
1593                            let body_y = divider.y + edge;
1594                            let body_h = divider.height - edge * 2.0;
1595                            if body_h > 0.0 {
1596                                instances.push(crate::cell_renderer::types::BackgroundInstance {
1597                                    position: [divider.x / w * 2.0 - 1.0, 1.0 - (body_y / h * 2.0)],
1598                                    size: [divider.width / w * 2.0, body_h / h * 2.0],
1599                                    color: [color[0], color[1], color[2], 1.0],
1600                                });
1601                            }
1602                            // Bottom shadow
1603                            let shadow_y = divider.y + divider.height - edge;
1604                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1605                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (shadow_y / h * 2.0)],
1606                                size: [divider.width / w * 2.0, edge / h * 2.0],
1607                                color: shadow,
1608                            });
1609                        } else {
1610                            // Left highlight
1611                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1612                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1613                                size: [edge / w * 2.0, divider.height / h * 2.0],
1614                                color: highlight,
1615                            });
1616                            // Main body
1617                            let body_x = divider.x + edge;
1618                            let body_w = divider.width - edge * 2.0;
1619                            if body_w > 0.0 {
1620                                instances.push(crate::cell_renderer::types::BackgroundInstance {
1621                                    position: [body_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1622                                    size: [body_w / w * 2.0, divider.height / h * 2.0],
1623                                    color: [color[0], color[1], color[2], 1.0],
1624                                });
1625                            }
1626                            // Right shadow
1627                            let shadow_x = divider.x + divider.width - edge;
1628                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1629                                position: [shadow_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1630                                size: [edge / w * 2.0, divider.height / h * 2.0],
1631                                color: shadow,
1632                            });
1633                        }
1634                    } else {
1635                        // 2px or less: top/left half highlight, bottom/right half shadow
1636                        if is_horizontal {
1637                            let half = (divider.height / 2.0).max(1.0);
1638                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1639                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1640                                size: [divider.width / w * 2.0, half / h * 2.0],
1641                                color: highlight,
1642                            });
1643                            let bottom_y = divider.y + half;
1644                            let bottom_h = divider.height - half;
1645                            if bottom_h > 0.0 {
1646                                instances.push(crate::cell_renderer::types::BackgroundInstance {
1647                                    position: [
1648                                        divider.x / w * 2.0 - 1.0,
1649                                        1.0 - (bottom_y / h * 2.0),
1650                                    ],
1651                                    size: [divider.width / w * 2.0, bottom_h / h * 2.0],
1652                                    color: shadow,
1653                                });
1654                            }
1655                        } else {
1656                            let half = (divider.width / 2.0).max(1.0);
1657                            instances.push(crate::cell_renderer::types::BackgroundInstance {
1658                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1659                                size: [half / w * 2.0, divider.height / h * 2.0],
1660                                color: highlight,
1661                            });
1662                            let right_x = divider.x + half;
1663                            let right_w = divider.width - half;
1664                            if right_w > 0.0 {
1665                                instances.push(crate::cell_renderer::types::BackgroundInstance {
1666                                    position: [
1667                                        right_x / w * 2.0 - 1.0,
1668                                        1.0 - (divider.y / h * 2.0),
1669                                    ],
1670                                    size: [right_w / w * 2.0, divider.height / h * 2.0],
1671                                    color: shadow,
1672                                });
1673                            }
1674                        }
1675                    }
1676                }
1677            }
1678        }
1679
1680        // Write instances to GPU buffer
1681        self.cell_renderer.queue().write_buffer(
1682            &self.cell_renderer.bg_instance_buffer,
1683            0,
1684            bytemuck::cast_slice(&instances),
1685        );
1686
1687        // Render dividers
1688        let mut encoder =
1689            self.cell_renderer
1690                .device()
1691                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1692                    label: Some("divider render encoder"),
1693                });
1694
1695        {
1696            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1697                label: Some("divider render pass"),
1698                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1699                    view: surface_view,
1700                    resolve_target: None,
1701                    ops: wgpu::Operations {
1702                        load: wgpu::LoadOp::Load, // Don't clear - render on top
1703                        store: wgpu::StoreOp::Store,
1704                    },
1705                    depth_slice: None,
1706                })],
1707                depth_stencil_attachment: None,
1708                timestamp_writes: None,
1709                occlusion_query_set: None,
1710            });
1711
1712            render_pass.set_pipeline(&self.cell_renderer.bg_pipeline);
1713            render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
1714            render_pass.set_vertex_buffer(1, self.cell_renderer.bg_instance_buffer.slice(..));
1715            render_pass.draw(0..4, 0..instances.len() as u32);
1716        }
1717
1718        self.cell_renderer
1719            .queue()
1720            .submit(std::iter::once(encoder.finish()));
1721        Ok(())
1722    }
1723
1724    /// Render focus indicator around a pane
1725    ///
1726    /// This draws a colored border around the focused pane to highlight it.
1727    ///
1728    /// # Arguments
1729    /// * `surface_view` - The texture view to render to
1730    /// * `viewport` - The focused pane's viewport
1731    /// * `settings` - Divider/focus settings
1732    #[allow(dead_code)]
1733    pub fn render_focus_indicator(
1734        &mut self,
1735        surface_view: &wgpu::TextureView,
1736        viewport: &PaneViewport,
1737        settings: &PaneDividerSettings,
1738    ) -> Result<()> {
1739        if !settings.show_focus_indicator {
1740            return Ok(());
1741        }
1742
1743        let border_w = settings.focus_width;
1744        let color = [
1745            settings.focus_color[0],
1746            settings.focus_color[1],
1747            settings.focus_color[2],
1748            1.0,
1749        ];
1750
1751        // Create 4 border rectangles (top, bottom, left, right)
1752        let instances = vec![
1753            // Top border
1754            crate::cell_renderer::types::BackgroundInstance {
1755                position: [
1756                    viewport.x / self.size.width as f32 * 2.0 - 1.0,
1757                    1.0 - (viewport.y / self.size.height as f32 * 2.0),
1758                ],
1759                size: [
1760                    viewport.width / self.size.width as f32 * 2.0,
1761                    border_w / self.size.height as f32 * 2.0,
1762                ],
1763                color,
1764            },
1765            // Bottom border
1766            crate::cell_renderer::types::BackgroundInstance {
1767                position: [
1768                    viewport.x / self.size.width as f32 * 2.0 - 1.0,
1769                    1.0 - ((viewport.y + viewport.height - border_w) / self.size.height as f32
1770                        * 2.0),
1771                ],
1772                size: [
1773                    viewport.width / self.size.width as f32 * 2.0,
1774                    border_w / self.size.height as f32 * 2.0,
1775                ],
1776                color,
1777            },
1778            // Left border (between top and bottom)
1779            crate::cell_renderer::types::BackgroundInstance {
1780                position: [
1781                    viewport.x / self.size.width as f32 * 2.0 - 1.0,
1782                    1.0 - ((viewport.y + border_w) / self.size.height as f32 * 2.0),
1783                ],
1784                size: [
1785                    border_w / self.size.width as f32 * 2.0,
1786                    (viewport.height - border_w * 2.0) / self.size.height as f32 * 2.0,
1787                ],
1788                color,
1789            },
1790            // Right border (between top and bottom)
1791            crate::cell_renderer::types::BackgroundInstance {
1792                position: [
1793                    (viewport.x + viewport.width - border_w) / self.size.width as f32 * 2.0 - 1.0,
1794                    1.0 - ((viewport.y + border_w) / self.size.height as f32 * 2.0),
1795                ],
1796                size: [
1797                    border_w / self.size.width as f32 * 2.0,
1798                    (viewport.height - border_w * 2.0) / self.size.height as f32 * 2.0,
1799                ],
1800                color,
1801            },
1802        ];
1803
1804        // Write instances to GPU buffer
1805        self.cell_renderer.queue().write_buffer(
1806            &self.cell_renderer.bg_instance_buffer,
1807            0,
1808            bytemuck::cast_slice(&instances),
1809        );
1810
1811        // Render focus indicator
1812        let mut encoder =
1813            self.cell_renderer
1814                .device()
1815                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1816                    label: Some("focus indicator encoder"),
1817                });
1818
1819        {
1820            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1821                label: Some("focus indicator pass"),
1822                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1823                    view: surface_view,
1824                    resolve_target: None,
1825                    ops: wgpu::Operations {
1826                        load: wgpu::LoadOp::Load, // Don't clear - render on top
1827                        store: wgpu::StoreOp::Store,
1828                    },
1829                    depth_slice: None,
1830                })],
1831                depth_stencil_attachment: None,
1832                timestamp_writes: None,
1833                occlusion_query_set: None,
1834            });
1835
1836            render_pass.set_pipeline(&self.cell_renderer.bg_pipeline);
1837            render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
1838            render_pass.set_vertex_buffer(1, self.cell_renderer.bg_instance_buffer.slice(..));
1839            render_pass.draw(0..4, 0..instances.len() as u32);
1840        }
1841
1842        self.cell_renderer
1843            .queue()
1844            .submit(std::iter::once(encoder.finish()));
1845        Ok(())
1846    }
1847
1848    /// Render pane title bars (background rectangles + text)
1849    ///
1850    /// Title bars are rendered on top of pane content and dividers.
1851    /// Each title bar consists of a colored background rectangle and centered text.
1852    #[allow(dead_code)]
1853    pub fn render_pane_titles(
1854        &mut self,
1855        surface_view: &wgpu::TextureView,
1856        titles: &[PaneTitleInfo],
1857    ) -> Result<()> {
1858        if titles.is_empty() {
1859            return Ok(());
1860        }
1861
1862        let width = self.size.width as f32;
1863        let height = self.size.height as f32;
1864
1865        // Phase 1: Render title bar backgrounds
1866        let mut bg_instances = Vec::with_capacity(titles.len());
1867        for title in titles {
1868            let x_ndc = title.x / width * 2.0 - 1.0;
1869            let y_ndc = 1.0 - (title.y / height * 2.0);
1870            let w_ndc = title.width / width * 2.0;
1871            let h_ndc = title.height / height * 2.0;
1872
1873            // Title bar must be fully opaque (alpha=1.0) to cover the background.
1874            // Differentiate focused/unfocused by lightening/darkening the color.
1875            let brightness = if title.focused { 1.0 } else { 0.7 };
1876
1877            bg_instances.push(crate::cell_renderer::types::BackgroundInstance {
1878                position: [x_ndc, y_ndc],
1879                size: [w_ndc, h_ndc],
1880                color: [
1881                    title.bg_color[0] * brightness,
1882                    title.bg_color[1] * brightness,
1883                    title.bg_color[2] * brightness,
1884                    1.0, // Always fully opaque
1885                ],
1886            });
1887        }
1888
1889        // Write background instances to GPU buffer
1890        self.cell_renderer.queue().write_buffer(
1891            &self.cell_renderer.bg_instance_buffer,
1892            0,
1893            bytemuck::cast_slice(&bg_instances),
1894        );
1895
1896        // Render title backgrounds
1897        let mut encoder =
1898            self.cell_renderer
1899                .device()
1900                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1901                    label: Some("pane title bg encoder"),
1902                });
1903
1904        {
1905            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1906                label: Some("pane title bg pass"),
1907                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1908                    view: surface_view,
1909                    resolve_target: None,
1910                    ops: wgpu::Operations {
1911                        load: wgpu::LoadOp::Load,
1912                        store: wgpu::StoreOp::Store,
1913                    },
1914                    depth_slice: None,
1915                })],
1916                depth_stencil_attachment: None,
1917                timestamp_writes: None,
1918                occlusion_query_set: None,
1919            });
1920
1921            render_pass.set_pipeline(&self.cell_renderer.bg_pipeline);
1922            render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
1923            render_pass.set_vertex_buffer(1, self.cell_renderer.bg_instance_buffer.slice(..));
1924            render_pass.draw(0..4, 0..bg_instances.len() as u32);
1925        }
1926
1927        self.cell_renderer
1928            .queue()
1929            .submit(std::iter::once(encoder.finish()));
1930
1931        // Phase 2: Render title text using glyph atlas
1932        let mut text_instances = Vec::new();
1933        let baseline_y = self.cell_renderer.font_ascent;
1934
1935        for title in titles {
1936            let title_text = &title.title;
1937            if title_text.is_empty() {
1938                continue;
1939            }
1940
1941            // Calculate starting X position (centered in title bar with left padding)
1942            let padding_x = 8.0;
1943            let mut x_pos = title.x + padding_x;
1944            let y_base = title.y + (title.height - self.cell_renderer.cell_height) / 2.0;
1945
1946            let text_color = [
1947                title.text_color[0],
1948                title.text_color[1],
1949                title.text_color[2],
1950                if title.focused { 1.0 } else { 0.8 },
1951            ];
1952
1953            // Truncate title if it would overflow the title bar
1954            let max_chars =
1955                ((title.width - padding_x * 2.0) / self.cell_renderer.cell_width) as usize;
1956            let display_text: String = if title_text.len() > max_chars && max_chars > 3 {
1957                let truncated: String = title_text.chars().take(max_chars - 1).collect();
1958                format!("{}\u{2026}", truncated) // ellipsis
1959            } else {
1960                title_text.clone()
1961            };
1962
1963            for ch in display_text.chars() {
1964                if x_pos >= title.x + title.width - padding_x {
1965                    break;
1966                }
1967
1968                if let Some((font_idx, glyph_id)) =
1969                    self.cell_renderer.font_manager.find_glyph(ch, false, false)
1970                {
1971                    let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
1972                    let info = if self.cell_renderer.glyph_cache.contains_key(&cache_key) {
1973                        self.cell_renderer.lru_remove(cache_key);
1974                        self.cell_renderer.lru_push_front(cache_key);
1975                        self.cell_renderer
1976                            .glyph_cache
1977                            .get(&cache_key)
1978                            .unwrap()
1979                            .clone()
1980                    } else if let Some(raster) =
1981                        self.cell_renderer.rasterize_glyph(font_idx, glyph_id)
1982                    {
1983                        let info = self.cell_renderer.upload_glyph(cache_key, &raster);
1984                        self.cell_renderer
1985                            .glyph_cache
1986                            .insert(cache_key, info.clone());
1987                        self.cell_renderer.lru_push_front(cache_key);
1988                        info
1989                    } else {
1990                        x_pos += self.cell_renderer.cell_width;
1991                        continue;
1992                    };
1993
1994                    let glyph_left = x_pos + info.bearing_x;
1995                    let glyph_top = y_base + (baseline_y - info.bearing_y);
1996
1997                    text_instances.push(crate::cell_renderer::types::TextInstance {
1998                        position: [
1999                            glyph_left / width * 2.0 - 1.0,
2000                            1.0 - (glyph_top / height * 2.0),
2001                        ],
2002                        size: [
2003                            info.width as f32 / width * 2.0,
2004                            info.height as f32 / height * 2.0,
2005                        ],
2006                        tex_offset: [info.x as f32 / 2048.0, info.y as f32 / 2048.0],
2007                        tex_size: [info.width as f32 / 2048.0, info.height as f32 / 2048.0],
2008                        color: text_color,
2009                        is_colored: if info.is_colored { 1 } else { 0 },
2010                    });
2011                }
2012
2013                x_pos += self.cell_renderer.cell_width;
2014            }
2015        }
2016
2017        if text_instances.is_empty() {
2018            return Ok(());
2019        }
2020
2021        // Write text instances to GPU buffer
2022        self.cell_renderer.queue().write_buffer(
2023            &self.cell_renderer.text_instance_buffer,
2024            0,
2025            bytemuck::cast_slice(&text_instances),
2026        );
2027
2028        // Render title text
2029        let mut encoder =
2030            self.cell_renderer
2031                .device()
2032                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2033                    label: Some("pane title text encoder"),
2034                });
2035
2036        {
2037            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2038                label: Some("pane title text pass"),
2039                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2040                    view: surface_view,
2041                    resolve_target: None,
2042                    ops: wgpu::Operations {
2043                        load: wgpu::LoadOp::Load,
2044                        store: wgpu::StoreOp::Store,
2045                    },
2046                    depth_slice: None,
2047                })],
2048                depth_stencil_attachment: None,
2049                timestamp_writes: None,
2050                occlusion_query_set: None,
2051            });
2052
2053            render_pass.set_pipeline(&self.cell_renderer.text_pipeline);
2054            render_pass.set_bind_group(0, &self.cell_renderer.text_bind_group, &[]);
2055            render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
2056            render_pass.set_vertex_buffer(1, self.cell_renderer.text_instance_buffer.slice(..));
2057            render_pass.draw(0..4, 0..text_instances.len() as u32);
2058        }
2059
2060        self.cell_renderer
2061            .queue()
2062            .submit(std::iter::once(encoder.finish()));
2063
2064        Ok(())
2065    }
2066
2067    /// Render egui overlay on top of the terminal
2068    fn render_egui(
2069        &mut self,
2070        surface_texture: &wgpu::SurfaceTexture,
2071        egui_output: egui::FullOutput,
2072        egui_ctx: &egui::Context,
2073        force_opaque: bool,
2074    ) -> Result<()> {
2075        use wgpu::TextureViewDescriptor;
2076
2077        // Create view of the surface texture
2078        let view = surface_texture
2079            .texture
2080            .create_view(&TextureViewDescriptor::default());
2081
2082        // Create command encoder for egui
2083        let mut encoder =
2084            self.cell_renderer
2085                .device()
2086                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2087                    label: Some("egui encoder"),
2088                });
2089
2090        // Convert egui output to screen descriptor
2091        let screen_descriptor = egui_wgpu::ScreenDescriptor {
2092            size_in_pixels: [self.size.width, self.size.height],
2093            pixels_per_point: egui_output.pixels_per_point,
2094        };
2095
2096        // Update egui textures
2097        for (id, image_delta) in &egui_output.textures_delta.set {
2098            self.egui_renderer.update_texture(
2099                self.cell_renderer.device(),
2100                self.cell_renderer.queue(),
2101                *id,
2102                image_delta,
2103            );
2104        }
2105
2106        // Tessellate egui shapes into paint jobs
2107        let mut paint_jobs = egui_ctx.tessellate(egui_output.shapes, egui_output.pixels_per_point);
2108
2109        // If requested, force all egui vertices to full opacity so UI stays solid
2110        if force_opaque {
2111            for job in paint_jobs.iter_mut() {
2112                match &mut job.primitive {
2113                    egui::epaint::Primitive::Mesh(mesh) => {
2114                        for v in mesh.vertices.iter_mut() {
2115                            v.color[3] = 255;
2116                        }
2117                    }
2118                    egui::epaint::Primitive::Callback(_) => {}
2119                }
2120            }
2121        }
2122
2123        // Update egui buffers
2124        self.egui_renderer.update_buffers(
2125            self.cell_renderer.device(),
2126            self.cell_renderer.queue(),
2127            &mut encoder,
2128            &paint_jobs,
2129            &screen_descriptor,
2130        );
2131
2132        // Render egui on top of the terminal content
2133        {
2134            let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2135                label: Some("egui render pass"),
2136                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2137                    view: &view,
2138                    resolve_target: None,
2139                    ops: wgpu::Operations {
2140                        load: wgpu::LoadOp::Load, // Don't clear - render on top of terminal
2141                        store: wgpu::StoreOp::Store,
2142                    },
2143                    depth_slice: None,
2144                })],
2145                depth_stencil_attachment: None,
2146                timestamp_writes: None,
2147                occlusion_query_set: None,
2148            });
2149
2150            // Convert to 'static lifetime as required by egui_renderer.render()
2151            let mut render_pass = render_pass.forget_lifetime();
2152
2153            self.egui_renderer
2154                .render(&mut render_pass, &paint_jobs, &screen_descriptor);
2155        } // render_pass dropped here
2156
2157        // Submit egui commands
2158        self.cell_renderer
2159            .queue()
2160            .submit(std::iter::once(encoder.finish()));
2161
2162        // Free egui textures
2163        for id in &egui_output.textures_delta.free {
2164            self.egui_renderer.free_texture(id);
2165        }
2166
2167        Ok(())
2168    }
2169
2170    /// Get the current size
2171    pub fn size(&self) -> PhysicalSize<u32> {
2172        self.size
2173    }
2174
2175    /// Get the current grid dimensions (columns, rows)
2176    pub fn grid_size(&self) -> (usize, usize) {
2177        self.cell_renderer.grid_size()
2178    }
2179
2180    /// Get cell width in pixels
2181    pub fn cell_width(&self) -> f32 {
2182        self.cell_renderer.cell_width()
2183    }
2184
2185    /// Get cell height in pixels
2186    pub fn cell_height(&self) -> f32 {
2187        self.cell_renderer.cell_height()
2188    }
2189
2190    /// Get window padding in physical pixels (scaled by DPI)
2191    pub fn window_padding(&self) -> f32 {
2192        self.cell_renderer.window_padding()
2193    }
2194
2195    /// Get the vertical content offset in physical pixels (e.g., tab bar height scaled by DPI)
2196    pub fn content_offset_y(&self) -> f32 {
2197        self.cell_renderer.content_offset_y()
2198    }
2199
2200    /// Get the display scale factor (e.g., 2.0 on Retina displays)
2201    pub fn scale_factor(&self) -> f32 {
2202        self.cell_renderer.scale_factor
2203    }
2204
2205    /// Set the vertical content offset (e.g., tab bar height) in logical pixels.
2206    /// The offset is scaled by the display scale factor to physical pixels internally,
2207    /// since the cell renderer works in physical pixel coordinates while egui (tab bar)
2208    /// uses logical pixels.
2209    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
2210    pub fn set_content_offset_y(&mut self, logical_offset: f32) -> Option<(usize, usize)> {
2211        // Scale from logical pixels (egui/config) to physical pixels (wgpu surface)
2212        let physical_offset = logical_offset * self.cell_renderer.scale_factor;
2213        let result = self.cell_renderer.set_content_offset_y(physical_offset);
2214        // Always update graphics renderer offset, even if grid size didn't change
2215        self.graphics_renderer.set_content_offset_y(physical_offset);
2216        // Update custom shader renderer content offset
2217        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2218            custom_shader.set_content_offset_y(physical_offset);
2219        }
2220        // Update cursor shader renderer content offset
2221        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2222            cursor_shader.set_content_offset_y(physical_offset);
2223        }
2224        if result.is_some() {
2225            self.dirty = true;
2226        }
2227        result
2228    }
2229
2230    /// Get the horizontal content offset in physical pixels
2231    pub fn content_offset_x(&self) -> f32 {
2232        self.cell_renderer.content_offset_x()
2233    }
2234
2235    /// Set the horizontal content offset (e.g., tab bar on left) in logical pixels.
2236    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
2237    pub fn set_content_offset_x(&mut self, logical_offset: f32) -> Option<(usize, usize)> {
2238        let physical_offset = logical_offset * self.cell_renderer.scale_factor;
2239        let result = self.cell_renderer.set_content_offset_x(physical_offset);
2240        self.graphics_renderer.set_content_offset_x(physical_offset);
2241        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2242            custom_shader.set_content_offset_x(physical_offset);
2243        }
2244        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2245            cursor_shader.set_content_offset_x(physical_offset);
2246        }
2247        if result.is_some() {
2248            self.dirty = true;
2249        }
2250        result
2251    }
2252
2253    /// Get the bottom content inset in physical pixels
2254    pub fn content_inset_bottom(&self) -> f32 {
2255        self.cell_renderer.content_inset_bottom()
2256    }
2257
2258    /// Get the right content inset in physical pixels
2259    pub fn content_inset_right(&self) -> f32 {
2260        self.cell_renderer.content_inset_right()
2261    }
2262
2263    /// Set the bottom content inset (e.g., tab bar at bottom) in logical pixels.
2264    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
2265    pub fn set_content_inset_bottom(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
2266        let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2267        let result = self.cell_renderer.set_content_inset_bottom(physical_inset);
2268        if result.is_some() {
2269            self.dirty = true;
2270        }
2271        result
2272    }
2273
2274    /// Set the right content inset (e.g., AI Inspector panel) in logical pixels.
2275    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
2276    pub fn set_content_inset_right(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
2277        let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2278        let result = self.cell_renderer.set_content_inset_right(physical_inset);
2279
2280        // Also update custom shader renderer to exclude panel area from effects
2281        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2282            custom_shader.set_content_inset_right(physical_inset);
2283        }
2284        // Also update cursor shader renderer
2285        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2286            cursor_shader.set_content_inset_right(physical_inset);
2287        }
2288
2289        if result.is_some() {
2290            self.dirty = true;
2291        }
2292        result
2293    }
2294
2295    /// Set the additional bottom inset from egui panels (status bar, tmux bar).
2296    ///
2297    /// This inset reduces the terminal grid height so content does not render
2298    /// behind the status bar. Also affects scrollbar bounds.
2299    /// Returns `Some((cols, rows))` if the grid was resized.
2300    pub fn set_egui_bottom_inset(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
2301        let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2302        if (self.cell_renderer.egui_bottom_inset - physical_inset).abs() > f32::EPSILON {
2303            self.cell_renderer.egui_bottom_inset = physical_inset;
2304            let (w, h) = (
2305                self.cell_renderer.config.width,
2306                self.cell_renderer.config.height,
2307            );
2308            return Some(self.cell_renderer.resize(w, h));
2309        }
2310        None
2311    }
2312
2313    /// Set the additional right inset from egui panels (AI Inspector).
2314    ///
2315    /// This inset is added to `content_inset_right` for scrollbar bounds only.
2316    /// egui panels already claim space before wgpu rendering, so this doesn't
2317    /// affect the terminal grid sizing.
2318    pub fn set_egui_right_inset(&mut self, logical_inset: f32) {
2319        let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2320        self.cell_renderer.egui_right_inset = physical_inset;
2321    }
2322
2323    /// Check if a point (in pixel coordinates) is within the scrollbar bounds
2324    ///
2325    /// # Arguments
2326    /// * `x` - X coordinate in pixels (from left edge)
2327    /// * `y` - Y coordinate in pixels (from top edge)
2328    pub fn scrollbar_contains_point(&self, x: f32, y: f32) -> bool {
2329        self.cell_renderer.scrollbar_contains_point(x, y)
2330    }
2331
2332    /// Get the scrollbar thumb bounds (top Y, height) in pixels
2333    pub fn scrollbar_thumb_bounds(&self) -> Option<(f32, f32)> {
2334        self.cell_renderer.scrollbar_thumb_bounds()
2335    }
2336
2337    /// Check if an X coordinate is within the scrollbar track
2338    pub fn scrollbar_track_contains_x(&self, x: f32) -> bool {
2339        self.cell_renderer.scrollbar_track_contains_x(x)
2340    }
2341
2342    /// Convert a mouse Y position to a scroll offset
2343    ///
2344    /// # Arguments
2345    /// * `mouse_y` - Mouse Y coordinate in pixels (from top edge)
2346    ///
2347    /// # Returns
2348    /// The scroll offset corresponding to the mouse position, or None if scrollbar is not visible
2349    pub fn scrollbar_mouse_y_to_scroll_offset(&self, mouse_y: f32) -> Option<usize> {
2350        self.cell_renderer
2351            .scrollbar_mouse_y_to_scroll_offset(mouse_y)
2352    }
2353
2354    /// Find a scrollbar mark at the given mouse position for tooltip display.
2355    ///
2356    /// # Arguments
2357    /// * `mouse_x` - Mouse X coordinate in pixels
2358    /// * `mouse_y` - Mouse Y coordinate in pixels
2359    /// * `tolerance` - Maximum distance in pixels to match a mark
2360    ///
2361    /// # Returns
2362    /// The mark at that position, or None if no mark is within tolerance
2363    pub fn scrollbar_mark_at_position(
2364        &self,
2365        mouse_x: f32,
2366        mouse_y: f32,
2367        tolerance: f32,
2368    ) -> Option<&par_term_config::ScrollbackMark> {
2369        self.cell_renderer
2370            .scrollbar_mark_at_position(mouse_x, mouse_y, tolerance)
2371    }
2372
2373    /// Check if the renderer needs to be redrawn
2374    #[allow(dead_code)]
2375    pub fn is_dirty(&self) -> bool {
2376        self.dirty
2377    }
2378
2379    /// Mark the renderer as dirty, forcing a redraw on next render call
2380    #[allow(dead_code)]
2381    pub fn mark_dirty(&mut self) {
2382        self.dirty = true;
2383    }
2384
2385    /// Set debug overlay text to be rendered
2386    #[allow(dead_code)]
2387    #[allow(dead_code)]
2388    pub fn render_debug_overlay(&mut self, text: &str) {
2389        self.debug_text = Some(text.to_string());
2390        self.dirty = true; // Mark dirty to ensure debug overlay renders
2391    }
2392
2393    /// Reconfigure the surface (call when surface becomes outdated or lost)
2394    /// This typically happens when dragging the window between displays
2395    pub fn reconfigure_surface(&mut self) {
2396        self.cell_renderer.reconfigure_surface();
2397        self.dirty = true;
2398    }
2399
2400    /// Check if a vsync mode is supported
2401    pub fn is_vsync_mode_supported(&self, mode: par_term_config::VsyncMode) -> bool {
2402        self.cell_renderer.is_vsync_mode_supported(mode)
2403    }
2404
2405    /// Update the vsync mode. Returns the actual mode applied (may differ if requested mode unsupported).
2406    /// Also returns whether the mode was changed.
2407    pub fn update_vsync_mode(
2408        &mut self,
2409        mode: par_term_config::VsyncMode,
2410    ) -> (par_term_config::VsyncMode, bool) {
2411        let result = self.cell_renderer.update_vsync_mode(mode);
2412        if result.1 {
2413            self.dirty = true;
2414        }
2415        result
2416    }
2417
2418    /// Get the current vsync mode
2419    #[allow(dead_code)]
2420    pub fn current_vsync_mode(&self) -> par_term_config::VsyncMode {
2421        self.cell_renderer.current_vsync_mode()
2422    }
2423
2424    /// Clear the glyph cache to force re-rasterization
2425    /// Useful after display changes where font rendering may differ
2426    pub fn clear_glyph_cache(&mut self) {
2427        self.cell_renderer.clear_glyph_cache();
2428        self.dirty = true;
2429    }
2430
2431    /// Update font anti-aliasing setting
2432    /// Returns true if the setting changed (requiring glyph cache clear)
2433    pub fn update_font_antialias(&mut self, enabled: bool) -> bool {
2434        let changed = self.cell_renderer.update_font_antialias(enabled);
2435        if changed {
2436            self.dirty = true;
2437        }
2438        changed
2439    }
2440
2441    /// Update font hinting setting
2442    /// Returns true if the setting changed (requiring glyph cache clear)
2443    pub fn update_font_hinting(&mut self, enabled: bool) -> bool {
2444        let changed = self.cell_renderer.update_font_hinting(enabled);
2445        if changed {
2446            self.dirty = true;
2447        }
2448        changed
2449    }
2450
2451    /// Update thin strokes mode
2452    /// Returns true if the setting changed (requiring glyph cache clear)
2453    pub fn update_font_thin_strokes(&mut self, mode: par_term_config::ThinStrokesMode) -> bool {
2454        let changed = self.cell_renderer.update_font_thin_strokes(mode);
2455        if changed {
2456            self.dirty = true;
2457        }
2458        changed
2459    }
2460
2461    /// Update minimum contrast ratio
2462    /// Returns true if the setting changed (requiring redraw)
2463    pub fn update_minimum_contrast(&mut self, ratio: f32) -> bool {
2464        let changed = self.cell_renderer.update_minimum_contrast(ratio);
2465        if changed {
2466            self.dirty = true;
2467        }
2468        changed
2469    }
2470
2471    /// Pause shader animations (e.g., when window loses focus)
2472    /// This reduces GPU usage when the terminal is not actively being viewed
2473    pub fn pause_shader_animations(&mut self) {
2474        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2475            custom_shader.set_animation_enabled(false);
2476        }
2477        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2478            cursor_shader.set_animation_enabled(false);
2479        }
2480        log::info!("[SHADER] Shader animations paused");
2481    }
2482
2483    /// Resume shader animations (e.g., when window regains focus)
2484    /// Only resumes if the user's config has animation enabled
2485    pub fn resume_shader_animations(
2486        &mut self,
2487        custom_shader_animation: bool,
2488        cursor_shader_animation: bool,
2489    ) {
2490        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2491            custom_shader.set_animation_enabled(custom_shader_animation);
2492        }
2493        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2494            cursor_shader.set_animation_enabled(cursor_shader_animation);
2495        }
2496        self.dirty = true;
2497        log::info!(
2498            "[SHADER] Shader animations resumed (custom: {}, cursor: {})",
2499            custom_shader_animation,
2500            cursor_shader_animation
2501        );
2502    }
2503
2504    /// Take a screenshot of the current terminal content
2505    /// Returns an RGBA image that can be saved to disk
2506    ///
2507    /// This captures the fully composited output including shader effects.
2508    pub fn take_screenshot(&mut self) -> Result<image::RgbaImage> {
2509        log::info!(
2510            "take_screenshot: Starting screenshot capture ({}x{})",
2511            self.size.width,
2512            self.size.height
2513        );
2514
2515        let width = self.size.width;
2516        let height = self.size.height;
2517        // Use the same format as the surface to match pipeline expectations
2518        let format = self.cell_renderer.surface_format();
2519        log::info!("take_screenshot: Using texture format {:?}", format);
2520
2521        // Create a texture to render the final composited output to (with COPY_SRC for reading back)
2522        let screenshot_texture =
2523            self.cell_renderer
2524                .device()
2525                .create_texture(&wgpu::TextureDescriptor {
2526                    label: Some("screenshot texture"),
2527                    size: wgpu::Extent3d {
2528                        width,
2529                        height,
2530                        depth_or_array_layers: 1,
2531                    },
2532                    mip_level_count: 1,
2533                    sample_count: 1,
2534                    dimension: wgpu::TextureDimension::D2,
2535                    format,
2536                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2537                    view_formats: &[],
2538                });
2539
2540        let screenshot_view =
2541            screenshot_texture.create_view(&wgpu::TextureViewDescriptor::default());
2542
2543        // Render the full composited frame (cells + shaders + overlays)
2544        log::info!("take_screenshot: Rendering composited frame...");
2545
2546        // Check if shaders are enabled
2547        let has_custom_shader = self.custom_shader_renderer.is_some();
2548        let use_cursor_shader =
2549            self.cursor_shader_renderer.is_some() && !self.cursor_shader_disabled_for_alt_screen;
2550
2551        if has_custom_shader {
2552            // Render cells to the custom shader's intermediate texture
2553            let intermediate_view = self
2554                .custom_shader_renderer
2555                .as_ref()
2556                .unwrap()
2557                .intermediate_texture_view()
2558                .clone();
2559            self.cell_renderer
2560                .render_to_texture(&intermediate_view, true)?;
2561
2562            if use_cursor_shader {
2563                // Background shader renders to cursor shader's intermediate texture
2564                let cursor_intermediate = self
2565                    .cursor_shader_renderer
2566                    .as_ref()
2567                    .unwrap()
2568                    .intermediate_texture_view()
2569                    .clone();
2570                self.custom_shader_renderer.as_mut().unwrap().render(
2571                    self.cell_renderer.device(),
2572                    self.cell_renderer.queue(),
2573                    &cursor_intermediate,
2574                    false,
2575                )?;
2576                // Cursor shader renders to screenshot texture
2577                self.cursor_shader_renderer.as_mut().unwrap().render(
2578                    self.cell_renderer.device(),
2579                    self.cell_renderer.queue(),
2580                    &screenshot_view,
2581                    true,
2582                )?;
2583            } else {
2584                // Background shader renders directly to screenshot texture
2585                self.custom_shader_renderer.as_mut().unwrap().render(
2586                    self.cell_renderer.device(),
2587                    self.cell_renderer.queue(),
2588                    &screenshot_view,
2589                    true,
2590                )?;
2591            }
2592        } else if use_cursor_shader {
2593            // Render cells to cursor shader's intermediate texture
2594            let cursor_intermediate = self
2595                .cursor_shader_renderer
2596                .as_ref()
2597                .unwrap()
2598                .intermediate_texture_view()
2599                .clone();
2600            self.cell_renderer
2601                .render_to_texture(&cursor_intermediate, true)?;
2602            // Cursor shader renders to screenshot texture
2603            self.cursor_shader_renderer.as_mut().unwrap().render(
2604                self.cell_renderer.device(),
2605                self.cell_renderer.queue(),
2606                &screenshot_view,
2607                true,
2608            )?;
2609        } else {
2610            // No shaders - render directly to screenshot texture
2611            self.cell_renderer.render_to_view(&screenshot_view)?;
2612        }
2613
2614        log::info!("take_screenshot: Render complete");
2615
2616        // Get device and queue references for buffer operations
2617        let device = self.cell_renderer.device();
2618        let queue = self.cell_renderer.queue();
2619
2620        // Create buffer for reading back the texture
2621        let bytes_per_pixel = 4u32;
2622        let unpadded_bytes_per_row = width * bytes_per_pixel;
2623        // wgpu requires rows to be aligned to 256 bytes
2624        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
2625        let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
2626        let buffer_size = (padded_bytes_per_row * height) as u64;
2627
2628        let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2629            label: Some("screenshot buffer"),
2630            size: buffer_size,
2631            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2632            mapped_at_creation: false,
2633        });
2634
2635        // Copy texture to buffer
2636        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2637            label: Some("screenshot encoder"),
2638        });
2639
2640        encoder.copy_texture_to_buffer(
2641            wgpu::TexelCopyTextureInfo {
2642                texture: &screenshot_texture,
2643                mip_level: 0,
2644                origin: wgpu::Origin3d::ZERO,
2645                aspect: wgpu::TextureAspect::All,
2646            },
2647            wgpu::TexelCopyBufferInfo {
2648                buffer: &output_buffer,
2649                layout: wgpu::TexelCopyBufferLayout {
2650                    offset: 0,
2651                    bytes_per_row: Some(padded_bytes_per_row),
2652                    rows_per_image: Some(height),
2653                },
2654            },
2655            wgpu::Extent3d {
2656                width,
2657                height,
2658                depth_or_array_layers: 1,
2659            },
2660        );
2661
2662        queue.submit(std::iter::once(encoder.finish()));
2663        log::info!("take_screenshot: Texture copy submitted");
2664
2665        // Map the buffer and read the data
2666        let buffer_slice = output_buffer.slice(..);
2667        let (tx, rx) = std::sync::mpsc::channel();
2668        buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
2669            let _ = tx.send(result);
2670        });
2671
2672        // Wait for GPU to finish
2673        log::info!("take_screenshot: Waiting for GPU...");
2674        let _ = device.poll(wgpu::PollType::wait_indefinitely());
2675        log::info!("take_screenshot: GPU poll complete, waiting for buffer map...");
2676        rx.recv()
2677            .map_err(|e| anyhow::anyhow!("Failed to receive map result: {}", e))?
2678            .map_err(|e| anyhow::anyhow!("Failed to map buffer: {:?}", e))?;
2679        log::info!("take_screenshot: Buffer mapped successfully");
2680
2681        // Read the data
2682        let data = buffer_slice.get_mapped_range();
2683        let mut pixels = Vec::with_capacity((width * height * 4) as usize);
2684
2685        // Check if format is BGRA (needs swizzle) or RGBA (direct copy)
2686        let is_bgra = matches!(
2687            format,
2688            wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
2689        );
2690
2691        // Copy data row by row (to handle padding)
2692        for y in 0..height {
2693            let row_start = (y * padded_bytes_per_row) as usize;
2694            let row_end = row_start + (width * bytes_per_pixel) as usize;
2695            let row = &data[row_start..row_end];
2696
2697            if is_bgra {
2698                // Convert BGRA to RGBA
2699                for chunk in row.chunks(4) {
2700                    pixels.push(chunk[2]); // R (was B)
2701                    pixels.push(chunk[1]); // G
2702                    pixels.push(chunk[0]); // B (was R)
2703                    pixels.push(chunk[3]); // A
2704                }
2705            } else {
2706                // Already RGBA, direct copy
2707                pixels.extend_from_slice(row);
2708            }
2709        }
2710
2711        drop(data);
2712        output_buffer.unmap();
2713
2714        // Create image
2715        image::RgbaImage::from_raw(width, height, pixels)
2716            .ok_or_else(|| anyhow::anyhow!("Failed to create image from pixel data"))
2717    }
2718}