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