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