par_term/
renderer.rs

1use super::cell_renderer::{Cell, CellRenderer};
2use super::graphics_renderer::GraphicsRenderer;
3use crate::custom_shader_renderer::CustomShaderRenderer;
4use anyhow::Result;
5use std::sync::Arc;
6use winit::dpi::PhysicalSize;
7use winit::window::Window;
8
9/// Renderer for the terminal using custom wgpu cell renderer
10pub struct Renderer {
11    // Cell renderer (owns the scrollbar)
12    cell_renderer: CellRenderer,
13
14    // Graphics renderer for sixel images
15    graphics_renderer: GraphicsRenderer,
16
17    // Current sixel graphics to render: (id, row, col, width_cells, height_cells, alpha, scroll_offset_rows)
18    // Note: row is isize to allow negative values for graphics scrolled off top
19    sixel_graphics: Vec<(u64, isize, usize, usize, usize, f32, usize)>,
20
21    // egui renderer for settings UI
22    egui_renderer: egui_wgpu::Renderer,
23
24    // Custom shader renderer for post-processing effects (background shader)
25    custom_shader_renderer: Option<CustomShaderRenderer>,
26    // Track current shader path to detect changes
27    custom_shader_path: Option<String>,
28
29    // Cursor shader renderer for cursor-specific effects (separate from background shader)
30    cursor_shader_renderer: Option<CustomShaderRenderer>,
31    // Track current cursor shader path to detect changes
32    cursor_shader_path: Option<String>,
33
34    // Cached for convenience
35    size: PhysicalSize<u32>,
36
37    // Dirty flag for optimization - only render when content has changed
38    dirty: bool,
39
40    // Debug overlay text
41    #[allow(dead_code)]
42    #[allow(dead_code)]
43    debug_text: Option<String>,
44}
45
46impl Renderer {
47    /// Create a new renderer
48    #[allow(clippy::too_many_arguments)]
49    pub async fn new(
50        window: Arc<Window>,
51        font_family: Option<&str>,
52        font_family_bold: Option<&str>,
53        font_family_italic: Option<&str>,
54        font_family_bold_italic: Option<&str>,
55        font_ranges: &[crate::config::FontRange],
56        font_size: f32,
57        window_padding: f32,
58        line_spacing: f32,
59        char_spacing: f32,
60        scrollbar_position: &str,
61        scrollbar_width: f32,
62        scrollbar_thumb_color: [f32; 4],
63        scrollbar_track_color: [f32; 4],
64        enable_text_shaping: bool,
65        enable_ligatures: bool,
66        enable_kerning: bool,
67        vsync_mode: crate::config::VsyncMode,
68        window_opacity: f32,
69        background_color: [u8; 3],
70        background_image_path: Option<&str>,
71        background_image_enabled: bool,
72        background_image_mode: crate::config::BackgroundImageMode,
73        background_image_opacity: f32,
74        custom_shader_path: Option<&str>,
75        custom_shader_enabled: bool,
76        custom_shader_animation: bool,
77        custom_shader_animation_speed: f32,
78        custom_shader_text_opacity: f32,
79        custom_shader_full_content: bool,
80        // Cursor shader settings (separate from background shader)
81        cursor_shader_path: Option<&str>,
82        cursor_shader_enabled: bool,
83        cursor_shader_animation: bool,
84        cursor_shader_animation_speed: f32,
85    ) -> Result<Self> {
86        let size = window.inner_size();
87        let scale_factor = window.scale_factor();
88
89        // Standard DPI for the platform
90        // macOS typically uses 72 DPI for points, Windows and most Linux use 96 DPI
91        let platform_dpi = if cfg!(target_os = "macos") {
92            72.0
93        } else {
94            96.0
95        };
96
97        // Convert font size from points to pixels for cell size calculation, honoring DPI and scale
98        let base_font_pixels = font_size * platform_dpi / 72.0;
99        let font_size_pixels = (base_font_pixels * scale_factor as f32).max(1.0);
100
101        // Preliminary font lookup to get metrics for accurate cell height
102        let font_manager = crate::font_manager::FontManager::new(
103            font_family,
104            font_family_bold,
105            font_family_italic,
106            font_family_bold_italic,
107            font_ranges,
108        )?;
109
110        let (font_ascent, font_descent, font_leading, char_advance) = {
111            let primary_font = font_manager.get_font(0).unwrap();
112            let metrics = primary_font.metrics(&[]);
113            let scale = font_size_pixels / metrics.units_per_em as f32;
114
115            // Get advance width of a standard character ('m' is common for monospace width)
116            let glyph_id = primary_font.charmap().map('m');
117            let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
118
119            (
120                metrics.ascent * scale,
121                metrics.descent * scale,
122                metrics.leading * scale,
123                advance,
124            )
125        };
126
127        // Use font metrics for cell height if line_spacing is 1.0
128        // Natural line height = ascent + descent + leading
129        let natural_line_height = font_ascent + font_descent + font_leading;
130        let char_height = (natural_line_height * line_spacing).max(1.0);
131
132        // Calculate available space after padding (padding on all sides)
133        let available_width = (size.width as f32 - window_padding * 2.0).max(0.0);
134        let available_height = (size.height as f32 - window_padding * 2.0).max(0.0);
135
136        // Calculate terminal dimensions based on font size in pixels and spacing
137        let char_width = (char_advance * char_spacing).max(1.0); // Configurable character width
138        let cols = (available_width / char_width).max(1.0) as usize;
139        let rows = (available_height / char_height).max(1.0) as usize;
140
141        // Create cell renderer with font fallback support (owns scrollbar)
142        let cell_renderer = CellRenderer::new(
143            window.clone(),
144            font_family,
145            font_family_bold,
146            font_family_italic,
147            font_family_bold_italic,
148            font_ranges,
149            font_size,
150            cols,
151            rows,
152            window_padding,
153            line_spacing,
154            char_spacing,
155            scrollbar_position,
156            scrollbar_width,
157            scrollbar_thumb_color,
158            scrollbar_track_color,
159            enable_text_shaping,
160            enable_ligatures,
161            enable_kerning,
162            vsync_mode,
163            window_opacity,
164            background_color,
165            if background_image_enabled {
166                background_image_path
167            } else {
168                None
169            },
170            background_image_mode,
171            background_image_opacity,
172        )
173        .await?;
174
175        // Create egui renderer for settings UI
176        let egui_renderer = egui_wgpu::Renderer::new(
177            cell_renderer.device(),
178            cell_renderer.surface_format(),
179            egui_wgpu::RendererOptions {
180                msaa_samples: 1,
181                depth_stencil_format: None,
182                dithering: false,
183                predictable_texture_filtering: false,
184            },
185        );
186
187        // Create graphics renderer for sixel images
188        let graphics_renderer = GraphicsRenderer::new(
189            cell_renderer.device(),
190            cell_renderer.surface_format(),
191            cell_renderer.cell_width(),
192            cell_renderer.cell_height(),
193            cell_renderer.window_padding(),
194        )?;
195
196        // Create custom shader renderer if configured
197        let (custom_shader_renderer, initial_shader_path) = if custom_shader_enabled {
198            if let Some(shader_path) = custom_shader_path {
199                let path = crate::config::Config::shader_path(shader_path);
200                match CustomShaderRenderer::new(
201                    cell_renderer.device(),
202                    cell_renderer.queue(),
203                    cell_renderer.surface_format(),
204                    &path,
205                    size.width,
206                    size.height,
207                    custom_shader_animation,
208                    custom_shader_animation_speed,
209                    window_opacity,
210                    custom_shader_text_opacity,
211                    custom_shader_full_content,
212                ) {
213                    Ok(mut renderer) => {
214                        // Sync cell dimensions for cursor position calculation
215                        renderer.update_cell_dimensions(
216                            cell_renderer.cell_width(),
217                            cell_renderer.cell_height(),
218                            window_padding,
219                        );
220                        log::info!(
221                            "Custom shader renderer initialized from: {}",
222                            path.display()
223                        );
224                        (Some(renderer), Some(shader_path.to_string()))
225                    }
226                    Err(e) => {
227                        log::error!("Failed to load custom shader '{}': {}", path.display(), e);
228                        (None, None)
229                    }
230                }
231            } else {
232                (None, None)
233            }
234        } else {
235            (None, None)
236        };
237
238        // Create cursor shader renderer if configured (separate from background shader)
239        let (cursor_shader_renderer, initial_cursor_shader_path) = if cursor_shader_enabled {
240            if let Some(shader_path) = cursor_shader_path {
241                let path = crate::config::Config::shader_path(shader_path);
242                match CustomShaderRenderer::new(
243                    cell_renderer.device(),
244                    cell_renderer.queue(),
245                    cell_renderer.surface_format(),
246                    &path,
247                    size.width,
248                    size.height,
249                    cursor_shader_animation,
250                    cursor_shader_animation_speed,
251                    window_opacity,
252                    1.0,  // Text opacity (cursor shader always uses 1.0)
253                    true, // Full content mode (cursor shader always uses full content)
254                ) {
255                    Ok(mut renderer) => {
256                        // Sync cell dimensions for cursor position calculation
257                        let cell_w = cell_renderer.cell_width();
258                        let cell_h = cell_renderer.cell_height();
259                        renderer.update_cell_dimensions(cell_w, cell_h, window_padding);
260                        log::info!(
261                            "Cursor shader renderer initialized from: {} (cell={}x{}, padding={})",
262                            path.display(),
263                            cell_w,
264                            cell_h,
265                            window_padding
266                        );
267                        (Some(renderer), Some(shader_path.to_string()))
268                    }
269                    Err(e) => {
270                        log::error!("Failed to load cursor shader '{}': {}", path.display(), e);
271                        (None, None)
272                    }
273                }
274            } else {
275                (None, None)
276            }
277        } else {
278            (None, None)
279        };
280
281        Ok(Self {
282            cell_renderer,
283            graphics_renderer,
284            sixel_graphics: Vec::new(),
285            egui_renderer,
286            custom_shader_renderer,
287            custom_shader_path: initial_shader_path,
288            cursor_shader_renderer,
289            cursor_shader_path: initial_cursor_shader_path,
290            size,
291            dirty: true, // Start dirty to ensure initial render
292            debug_text: None,
293        })
294    }
295
296    /// Resize the renderer and recalculate grid dimensions based on padding/font metrics
297    pub fn resize(&mut self, new_size: PhysicalSize<u32>) -> (usize, usize) {
298        if new_size.width > 0 && new_size.height > 0 {
299            self.size = new_size;
300            self.dirty = true; // Mark dirty on resize
301            let result = self.cell_renderer.resize(new_size.width, new_size.height);
302
303            // Update graphics renderer cell dimensions
304            self.graphics_renderer.update_cell_dimensions(
305                self.cell_renderer.cell_width(),
306                self.cell_renderer.cell_height(),
307                self.cell_renderer.window_padding(),
308            );
309
310            // Update custom shader renderer dimensions
311            if let Some(ref mut custom_shader) = self.custom_shader_renderer {
312                custom_shader.resize(self.cell_renderer.device(), new_size.width, new_size.height);
313                // Sync cell dimensions for cursor position calculation
314                custom_shader.update_cell_dimensions(
315                    self.cell_renderer.cell_width(),
316                    self.cell_renderer.cell_height(),
317                    self.cell_renderer.window_padding(),
318                );
319            }
320
321            // Update cursor shader renderer dimensions
322            if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
323                cursor_shader.resize(self.cell_renderer.device(), new_size.width, new_size.height);
324                // Sync cell dimensions for cursor position calculation
325                cursor_shader.update_cell_dimensions(
326                    self.cell_renderer.cell_width(),
327                    self.cell_renderer.cell_height(),
328                    self.cell_renderer.window_padding(),
329                );
330            }
331
332            return result;
333        }
334
335        self.cell_renderer.grid_size()
336    }
337
338    /// Update scale factor and resize so the PTY grid matches the new DPI.
339    pub fn handle_scale_factor_change(
340        &mut self,
341        scale_factor: f64,
342        new_size: PhysicalSize<u32>,
343    ) -> (usize, usize) {
344        self.cell_renderer.update_scale_factor(scale_factor);
345        self.resize(new_size)
346    }
347
348    /// Update the terminal cells
349    pub fn update_cells(&mut self, cells: &[Cell]) {
350        self.cell_renderer.update_cells(cells);
351        self.dirty = true; // Mark dirty when cells change
352    }
353
354    /// Update cursor position and style for geometric rendering
355    pub fn update_cursor(
356        &mut self,
357        position: (usize, usize),
358        opacity: f32,
359        style: par_term_emu_core_rust::cursor::CursorStyle,
360    ) {
361        self.cell_renderer.update_cursor(position, opacity, style);
362        self.dirty = true;
363    }
364
365    /// Clear cursor (hide it)
366    pub fn clear_cursor(&mut self) {
367        self.cell_renderer.clear_cursor();
368        self.dirty = true;
369    }
370
371    /// Update scrollbar state
372    ///
373    /// # Arguments
374    /// * `scroll_offset` - Current scroll offset (0 = at bottom)
375    /// * `visible_lines` - Number of lines visible on screen
376    /// * `total_lines` - Total number of lines including scrollback
377    pub fn update_scrollbar(
378        &mut self,
379        scroll_offset: usize,
380        visible_lines: usize,
381        total_lines: usize,
382    ) {
383        self.cell_renderer
384            .update_scrollbar(scroll_offset, visible_lines, total_lines);
385        self.dirty = true; // Mark dirty when scrollbar changes
386    }
387
388    /// Set the visual bell flash intensity
389    ///
390    /// # Arguments
391    /// * `intensity` - Flash intensity from 0.0 (no flash) to 1.0 (full white flash)
392    pub fn set_visual_bell_intensity(&mut self, intensity: f32) {
393        self.cell_renderer.set_visual_bell_intensity(intensity);
394        if intensity > 0.0 {
395            self.dirty = true; // Mark dirty when flash is active
396        }
397    }
398
399    /// Update window opacity in real-time
400    pub fn update_opacity(&mut self, opacity: f32) {
401        self.cell_renderer.update_opacity(opacity);
402
403        // Propagate to custom shader renderer if present
404        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
405            custom_shader.set_opacity(opacity);
406        }
407
408        self.dirty = true;
409    }
410
411    /// Update cursor color for cell rendering
412    pub fn update_cursor_color(&mut self, color: [u8; 3]) {
413        self.cell_renderer.update_cursor_color(color);
414        self.dirty = true;
415    }
416
417    /// Update window padding in real-time without full renderer rebuild
418    /// Returns Some((cols, rows)) if grid size changed and terminal needs resize
419    pub fn update_window_padding(&mut self, padding: f32) -> Option<(usize, usize)> {
420        let result = self.cell_renderer.update_window_padding(padding);
421        self.dirty = true;
422        result
423    }
424
425    /// Enable/disable background image and reload if needed
426    pub fn set_background_image_enabled(
427        &mut self,
428        enabled: bool,
429        path: Option<&str>,
430        mode: crate::config::BackgroundImageMode,
431        opacity: f32,
432    ) {
433        let path = if enabled { path } else { None };
434        self.cell_renderer.set_background_image(path, mode, opacity);
435        self.dirty = true;
436    }
437
438    /// Enable or disable animation for the custom shader at runtime
439    #[allow(dead_code)]
440    pub fn set_custom_shader_animation(&mut self, enabled: bool) {
441        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
442            custom_shader.set_animation_enabled(enabled);
443            self.dirty = true;
444        }
445    }
446
447    /// Update mouse position for custom shader (iMouse uniform)
448    ///
449    /// # Arguments
450    /// * `x` - Mouse X position in pixels (0 = left edge)
451    /// * `y` - Mouse Y position in pixels (0 = top edge)
452    pub fn set_shader_mouse_position(&mut self, x: f32, y: f32) {
453        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
454            custom_shader.set_mouse_position(x, y);
455        }
456    }
457
458    /// Update mouse button state for custom shader (iMouse uniform)
459    ///
460    /// # Arguments
461    /// * `pressed` - True if left mouse button is pressed
462    /// * `x` - Mouse X position at time of click/release
463    /// * `y` - Mouse Y position at time of click/release
464    pub fn set_shader_mouse_button(&mut self, pressed: bool, x: f32, y: f32) {
465        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
466            custom_shader.set_mouse_button(pressed, x, y);
467        }
468    }
469
470    /// Update cursor state for custom shader (Ghostty-compatible cursor uniforms)
471    ///
472    /// This enables cursor trail effects and other cursor-based animations in custom shaders.
473    ///
474    /// # Arguments
475    /// * `col` - Cursor column position (0-based)
476    /// * `row` - Cursor row position (0-based)
477    /// * `opacity` - Cursor opacity (0.0 = invisible, 1.0 = fully visible)
478    /// * `color` - Cursor RGBA color
479    /// * `style` - Cursor style (Block, Beam, Underline)
480    pub fn update_shader_cursor(
481        &mut self,
482        col: usize,
483        row: usize,
484        opacity: f32,
485        color: [f32; 4],
486        style: par_term_emu_core_rust::cursor::CursorStyle,
487    ) {
488        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
489            custom_shader.update_cursor(col, row, opacity, color, style);
490        }
491        // Also update cursor shader renderer
492        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
493            cursor_shader.update_cursor(col, row, opacity, color, style);
494        }
495    }
496
497    /// Update cursor shader configuration from config values
498    ///
499    /// # Arguments
500    /// * `color` - Cursor color for shader effects [R, G, B] (0-255)
501    /// * `trail_duration` - Duration of cursor trail effect in seconds
502    /// * `glow_radius` - Radius of cursor glow effect in pixels
503    /// * `glow_intensity` - Intensity of cursor glow effect (0.0-1.0)
504    pub fn update_cursor_shader_config(
505        &mut self,
506        color: [u8; 3],
507        trail_duration: f32,
508        glow_radius: f32,
509        glow_intensity: f32,
510    ) {
511        // Update both shaders with cursor config
512        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
513            custom_shader.update_cursor_shader_config(
514                color,
515                trail_duration,
516                glow_radius,
517                glow_intensity,
518            );
519        }
520        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
521            cursor_shader.update_cursor_shader_config(
522                color,
523                trail_duration,
524                glow_radius,
525                glow_intensity,
526            );
527        }
528    }
529
530    /// Enable or disable the cursor shader at runtime
531    ///
532    /// # Arguments
533    /// * `enabled` - Whether to enable the cursor shader
534    /// * `path` - Optional shader path (relative to shaders folder or absolute)
535    /// * `window_opacity` - Current window opacity
536    /// * `animation_enabled` - Whether animation is enabled
537    /// * `animation_speed` - Animation speed multiplier
538    ///
539    /// # Returns
540    /// Ok(()) if successful, Err with error message if compilation fails
541    #[allow(clippy::too_many_arguments)]
542    pub fn set_cursor_shader_enabled(
543        &mut self,
544        enabled: bool,
545        path: Option<&str>,
546        window_opacity: f32,
547        animation_enabled: bool,
548        animation_speed: f32,
549    ) -> Result<(), String> {
550        match (enabled, path) {
551            (true, Some(path)) => {
552                let path_changed = self.cursor_shader_path.as_ref().is_none_or(|p| p != path);
553
554                // If we already have a shader renderer and path hasn't changed, just update flags
555                if let Some(renderer) = &mut self.cursor_shader_renderer
556                    && !path_changed
557                {
558                    renderer.set_animation_enabled(animation_enabled);
559                    renderer.set_animation_speed(animation_speed);
560                    renderer.set_opacity(window_opacity);
561                    self.dirty = true;
562                    return Ok(());
563                }
564
565                let shader_path_full = crate::config::Config::shader_path(path);
566                match CustomShaderRenderer::new(
567                    self.cell_renderer.device(),
568                    self.cell_renderer.queue(),
569                    self.cell_renderer.surface_format(),
570                    &shader_path_full,
571                    self.size.width,
572                    self.size.height,
573                    animation_enabled,
574                    animation_speed,
575                    window_opacity,
576                    1.0,  // Text opacity (cursor shader always uses 1.0)
577                    true, // Full content mode (cursor shader always uses full content)
578                ) {
579                    Ok(mut renderer) => {
580                        // Sync cell dimensions for cursor position calculation
581                        renderer.update_cell_dimensions(
582                            self.cell_renderer.cell_width(),
583                            self.cell_renderer.cell_height(),
584                            self.cell_renderer.window_padding(),
585                        );
586                        log::info!(
587                            "Cursor shader enabled at runtime: {}",
588                            shader_path_full.display()
589                        );
590                        self.cursor_shader_renderer = Some(renderer);
591                        self.cursor_shader_path = Some(path.to_string());
592                        self.dirty = true;
593                        Ok(())
594                    }
595                    Err(e) => {
596                        let error_msg = format!(
597                            "Failed to load cursor shader '{}': {}",
598                            shader_path_full.display(),
599                            e
600                        );
601                        log::error!("{}", error_msg);
602                        Err(error_msg)
603                    }
604                }
605            }
606            _ => {
607                if self.cursor_shader_renderer.is_some() {
608                    log::info!("Cursor shader disabled at runtime");
609                }
610                self.cursor_shader_renderer = None;
611                self.cursor_shader_path = None;
612                self.dirty = true;
613                Ok(())
614            }
615        }
616    }
617
618    /// Get the current cursor shader path
619    #[allow(dead_code)]
620    pub fn cursor_shader_path(&self) -> Option<&str> {
621        self.cursor_shader_path.as_deref()
622    }
623
624    /// Reload the cursor shader from source code
625    pub fn reload_cursor_shader_from_source(&mut self, source: &str) -> Result<()> {
626        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
627            cursor_shader.reload_from_source(
628                self.cell_renderer.device(),
629                source,
630                "cursor_editor",
631            )?;
632            self.dirty = true;
633            Ok(())
634        } else {
635            Err(anyhow::anyhow!("No cursor shader renderer active"))
636        }
637    }
638
639    /// Update scrollbar appearance in real-time
640    pub fn update_scrollbar_appearance(
641        &mut self,
642        width: f32,
643        thumb_color: [f32; 4],
644        track_color: [f32; 4],
645    ) {
646        self.cell_renderer
647            .update_scrollbar_appearance(width, thumb_color, track_color);
648        self.dirty = true;
649    }
650
651    /// Update scrollbar position (left/right) in real-time
652    #[allow(dead_code)]
653    pub fn update_scrollbar_position(&mut self, position: &str) {
654        self.cell_renderer.update_scrollbar_position(position);
655        self.dirty = true;
656    }
657
658    /// Reload the custom shader from source code
659    ///
660    /// This method compiles the new shader source and replaces the current pipeline.
661    /// If compilation fails, returns an error and the old shader remains active.
662    ///
663    /// # Arguments
664    /// * `source` - The GLSL shader source code
665    ///
666    /// # Returns
667    /// Ok(()) if successful, Err with error message if compilation fails
668    pub fn reload_shader_from_source(&mut self, source: &str) -> Result<()> {
669        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
670            custom_shader.reload_from_source(self.cell_renderer.device(), source, "editor")?;
671            self.dirty = true;
672            Ok(())
673        } else {
674            Err(anyhow::anyhow!(
675                "No custom shader is currently loaded. Enable a custom shader first."
676            ))
677        }
678    }
679
680    /// Enable/disable custom shader at runtime. When enabling, tries to
681    /// (re)load the shader from the given path; when disabling, drops the
682    /// renderer instance.
683    ///
684    /// Returns Ok(()) on success, or Err with error message on failure.
685    #[allow(clippy::too_many_arguments)]
686    pub fn set_custom_shader_enabled(
687        &mut self,
688        enabled: bool,
689        shader_path: Option<&str>,
690        window_opacity: f32,
691        text_opacity: f32,
692        animation_enabled: bool,
693        animation_speed: f32,
694        full_content: bool,
695    ) -> Result<(), String> {
696        match (enabled, shader_path) {
697            (true, Some(path)) => {
698                // Check if the shader path has changed
699                let path_changed = self.custom_shader_path.as_deref() != Some(path);
700
701                // If we already have a shader renderer and path hasn't changed, just update flags
702                if let Some(renderer) = &mut self.custom_shader_renderer {
703                    if !path_changed {
704                        renderer.set_animation_enabled(animation_enabled);
705                        renderer.set_animation_speed(animation_speed);
706                        renderer.set_opacity(window_opacity);
707                        renderer.set_full_content_mode(full_content);
708                        return Ok(());
709                    }
710                    // Path changed - we need to reload, so drop the old renderer
711                    log::info!("Shader path changed, reloading shader");
712                }
713
714                let shader_path_full = crate::config::Config::shader_path(path);
715                match CustomShaderRenderer::new(
716                    self.cell_renderer.device(),
717                    self.cell_renderer.queue(),
718                    self.cell_renderer.surface_format(),
719                    &shader_path_full,
720                    self.size.width,
721                    self.size.height,
722                    animation_enabled,
723                    animation_speed,
724                    window_opacity,
725                    text_opacity,
726                    full_content,
727                ) {
728                    Ok(mut renderer) => {
729                        // Sync cell dimensions for cursor position calculation
730                        renderer.update_cell_dimensions(
731                            self.cell_renderer.cell_width(),
732                            self.cell_renderer.cell_height(),
733                            self.cell_renderer.window_padding(),
734                        );
735                        log::info!(
736                            "Custom shader enabled at runtime: {}",
737                            shader_path_full.display()
738                        );
739                        self.custom_shader_renderer = Some(renderer);
740                        self.custom_shader_path = Some(path.to_string());
741                        self.dirty = true;
742                        Ok(())
743                    }
744                    Err(e) => {
745                        let error_msg = format!(
746                            "Failed to load shader '{}': {}",
747                            shader_path_full.display(),
748                            e
749                        );
750                        log::error!("{}", error_msg);
751                        Err(error_msg)
752                    }
753                }
754            }
755            _ => {
756                if self.custom_shader_renderer.is_some() {
757                    log::info!("Custom shader disabled at runtime");
758                }
759                self.custom_shader_renderer = None;
760                self.custom_shader_path = None;
761                self.dirty = true;
762                Ok(())
763            }
764        }
765    }
766
767    /// Update background image opacity in real-time
768    pub fn update_background_image_opacity(&mut self, opacity: f32) {
769        self.cell_renderer.update_background_image_opacity(opacity);
770        self.dirty = true;
771    }
772
773    /// Update graphics textures (Sixel, iTerm2, Kitty)
774    ///
775    /// # Arguments
776    /// * `graphics` - Graphics from the terminal with RGBA data
777    /// * `view_scroll_offset` - Current view scroll offset (0 = viewing current content)
778    /// * `scrollback_len` - Total lines in scrollback buffer
779    /// * `visible_rows` - Number of visible rows in terminal
780    #[allow(dead_code)]
781    pub fn update_graphics(
782        &mut self,
783        graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
784        view_scroll_offset: usize,
785        scrollback_len: usize,
786        visible_rows: usize,
787    ) -> Result<()> {
788        // Clear old graphics list
789        self.sixel_graphics.clear();
790
791        // Calculate the view window in absolute terms
792        // total_lines = scrollback_len + visible_rows
793        // When scroll_offset = 0, we view lines [scrollback_len, scrollback_len + visible_rows)
794        // When scroll_offset > 0, we view earlier lines
795        let total_lines = scrollback_len + visible_rows;
796        let view_end = total_lines.saturating_sub(view_scroll_offset);
797        let view_start = view_end.saturating_sub(visible_rows);
798
799        // Process each graphic
800        for graphic in graphics {
801            // Use the unique ID from the graphic (stable across position changes)
802            let id = graphic.id;
803            let (col, row) = graphic.position;
804
805            // Calculate screen row based on whether this is a scrollback graphic or current
806            let screen_row: isize = if let Some(sb_row) = graphic.scrollback_row {
807                // Scrollback graphic: sb_row is absolute index in scrollback
808                // Screen row = sb_row - view_start
809                sb_row as isize - view_start as isize
810            } else {
811                // Current graphic: position is relative to visible area
812                // Absolute position = scrollback_len + row - scroll_offset_rows
813                // This keeps the graphic at its original absolute position as scrollback grows
814                let absolute_row = scrollback_len.saturating_sub(graphic.scroll_offset_rows) + row;
815
816                debug_trace!(
817                    "RENDERER",
818                    "CALC: scrollback_len={}, row={}, scroll_offset_rows={}, absolute_row={}, view_start={}, screen_row={}",
819                    scrollback_len,
820                    row,
821                    graphic.scroll_offset_rows,
822                    absolute_row,
823                    view_start,
824                    absolute_row as isize - view_start as isize
825                );
826
827                absolute_row as isize - view_start as isize
828            };
829
830            debug_log!(
831                "RENDERER",
832                "Graphics update: id={}, protocol={:?}, pos=({},{}), screen_row={}, scrollback_row={:?}, scroll_offset_rows={}, size={}x{}, view=[{},{})",
833                id,
834                graphic.protocol,
835                col,
836                row,
837                screen_row,
838                graphic.scrollback_row,
839                graphic.scroll_offset_rows,
840                graphic.width,
841                graphic.height,
842                view_start,
843                view_end
844            );
845
846            // Create or update texture in cache
847            self.graphics_renderer.get_or_create_texture(
848                self.cell_renderer.device(),
849                self.cell_renderer.queue(),
850                id,
851                &graphic.pixels, // RGBA pixel data (Arc<Vec<u8>>)
852                graphic.width as u32,
853                graphic.height as u32,
854            )?;
855
856            // Add to render list with position and dimensions
857            // Calculate size in cells (rounding up to cover all affected cells)
858            let width_cells =
859                ((graphic.width as f32 / self.cell_renderer.cell_width()).ceil() as usize).max(1);
860            let height_cells =
861                ((graphic.height as f32 / self.cell_renderer.cell_height()).ceil() as usize).max(1);
862
863            // Calculate effective clip rows based on screen position
864            // If screen_row < 0, we need to clip that many rows from the top
865            // If screen_row >= 0, no clipping needed (we can see the full graphic)
866            let effective_clip_rows = if screen_row < 0 {
867                (-screen_row) as usize
868            } else {
869                0
870            };
871
872            self.sixel_graphics.push((
873                id,
874                screen_row, // row position (can be negative if scrolled off top)
875                col,        // col position
876                width_cells,
877                height_cells,
878                1.0,                 // Full opacity by default
879                effective_clip_rows, // Rows to clip from top for partial rendering
880            ));
881        }
882
883        if !graphics.is_empty() {
884            self.dirty = true; // Mark dirty when graphics change
885        }
886
887        Ok(())
888    }
889
890    /// Check if animation requires continuous rendering
891    ///
892    /// Returns true if shader animation is enabled or a cursor trail animation
893    /// might still be in progress.
894    pub fn needs_continuous_render(&self) -> bool {
895        let custom_needs = self
896            .custom_shader_renderer
897            .as_ref()
898            .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
899        let cursor_needs = self
900            .cursor_shader_renderer
901            .as_ref()
902            .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
903        custom_needs || cursor_needs
904    }
905
906    /// Render a frame with optional egui overlay
907    /// Returns true if rendering was performed, false if skipped
908    pub fn render(
909        &mut self,
910        egui_data: Option<(egui::FullOutput, &egui::Context)>,
911        force_egui_opaque: bool,
912        show_scrollbar: bool,
913    ) -> Result<bool> {
914        // Custom shader animation forces continuous rendering
915        let force_render = self.needs_continuous_render();
916
917        if !self.dirty && egui_data.is_none() && !force_render {
918            // Skip rendering if nothing has changed
919            return Ok(false);
920        }
921
922        // Check if shaders are enabled
923        let has_custom_shader = self.custom_shader_renderer.is_some();
924        let has_cursor_shader = self.cursor_shader_renderer.is_some();
925
926        // Cell renderer renders terminal content
927        let t1 = std::time::Instant::now();
928        let surface_texture = if has_custom_shader {
929            // Render terminal to intermediate texture for background shader
930            self.cell_renderer.render_to_texture(
931                self.custom_shader_renderer
932                    .as_ref()
933                    .unwrap()
934                    .intermediate_texture_view(),
935            )?
936        } else if has_cursor_shader {
937            // Render terminal to intermediate texture for cursor shader
938            self.cell_renderer.render_to_texture(
939                self.cursor_shader_renderer
940                    .as_ref()
941                    .unwrap()
942                    .intermediate_texture_view(),
943            )?
944        } else {
945            // Render directly to surface
946            self.cell_renderer.render(show_scrollbar)?
947        };
948        let cell_render_time = t1.elapsed();
949
950        // Apply background custom shader if enabled
951        let t_custom = std::time::Instant::now();
952        let custom_shader_time = if let Some(ref mut custom_shader) = self.custom_shader_renderer {
953            if has_cursor_shader {
954                // Background shader renders to cursor shader's intermediate texture
955                custom_shader.render(
956                    self.cell_renderer.device(),
957                    self.cell_renderer.queue(),
958                    self.cursor_shader_renderer
959                        .as_ref()
960                        .unwrap()
961                        .intermediate_texture_view(),
962                )?;
963            } else {
964                // Background shader renders directly to surface
965                let surface_view = surface_texture
966                    .texture
967                    .create_view(&wgpu::TextureViewDescriptor::default());
968                custom_shader.render(
969                    self.cell_renderer.device(),
970                    self.cell_renderer.queue(),
971                    &surface_view,
972                )?;
973
974                // Render overlays (scrollbar, visual bell) on top after shader
975                self.cell_renderer
976                    .render_overlays(&surface_texture, show_scrollbar)?;
977            }
978            t_custom.elapsed()
979        } else {
980            std::time::Duration::ZERO
981        };
982
983        // Apply cursor shader if enabled
984        let t_cursor = std::time::Instant::now();
985        let cursor_shader_time = if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
986            log::trace!("Rendering cursor shader");
987            let surface_view = surface_texture
988                .texture
989                .create_view(&wgpu::TextureViewDescriptor::default());
990            cursor_shader.render(
991                self.cell_renderer.device(),
992                self.cell_renderer.queue(),
993                &surface_view,
994            )?;
995
996            // Render overlays (scrollbar, visual bell) on top after cursor shader
997            self.cell_renderer
998                .render_overlays(&surface_texture, show_scrollbar)?;
999            t_cursor.elapsed()
1000        } else {
1001            std::time::Duration::ZERO
1002        };
1003
1004        // Render sixel graphics on top of cells
1005        let t2 = std::time::Instant::now();
1006        if !self.sixel_graphics.is_empty() {
1007            self.render_sixel_graphics(&surface_texture)?;
1008        }
1009        let sixel_render_time = t2.elapsed();
1010
1011        // Render egui overlay if provided
1012        let t3 = std::time::Instant::now();
1013        if let Some((egui_output, egui_ctx)) = egui_data {
1014            self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
1015        }
1016        let egui_render_time = t3.elapsed();
1017
1018        // Present the surface texture - THIS IS WHERE VSYNC WAIT HAPPENS
1019        let t4 = std::time::Instant::now();
1020        surface_texture.present();
1021        let present_time = t4.elapsed();
1022
1023        // Log timing breakdown
1024        let total = cell_render_time
1025            + custom_shader_time
1026            + cursor_shader_time
1027            + sixel_render_time
1028            + egui_render_time
1029            + present_time;
1030        if present_time.as_millis() > 10 || total.as_millis() > 10 {
1031            log::info!(
1032                "RENDER_BREAKDOWN: CellRender={:.2}ms BgShader={:.2}ms CursorShader={:.2}ms Sixel={:.2}ms Egui={:.2}ms PRESENT={:.2}ms Total={:.2}ms",
1033                cell_render_time.as_secs_f64() * 1000.0,
1034                custom_shader_time.as_secs_f64() * 1000.0,
1035                cursor_shader_time.as_secs_f64() * 1000.0,
1036                sixel_render_time.as_secs_f64() * 1000.0,
1037                egui_render_time.as_secs_f64() * 1000.0,
1038                present_time.as_secs_f64() * 1000.0,
1039                total.as_secs_f64() * 1000.0
1040            );
1041        }
1042
1043        // Clear dirty flag after successful render
1044        self.dirty = false;
1045
1046        Ok(true)
1047    }
1048
1049    /// Render sixel graphics on top of terminal cells
1050    fn render_sixel_graphics(&mut self, surface_texture: &wgpu::SurfaceTexture) -> Result<()> {
1051        use wgpu::TextureViewDescriptor;
1052
1053        // Create view of the surface texture
1054        let view = surface_texture
1055            .texture
1056            .create_view(&TextureViewDescriptor::default());
1057
1058        // Create command encoder for sixel rendering
1059        let mut encoder =
1060            self.cell_renderer
1061                .device()
1062                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1063                    label: Some("sixel encoder"),
1064                });
1065
1066        // Create render pass
1067        {
1068            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1069                label: Some("sixel render pass"),
1070                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1071                    view: &view,
1072                    resolve_target: None,
1073                    ops: wgpu::Operations {
1074                        load: wgpu::LoadOp::Load, // Don't clear - render on top of terminal
1075                        store: wgpu::StoreOp::Store,
1076                    },
1077                    depth_slice: None,
1078                })],
1079                depth_stencil_attachment: None,
1080                timestamp_writes: None,
1081                occlusion_query_set: None,
1082            });
1083
1084            // Render all sixel graphics
1085            self.graphics_renderer.render(
1086                self.cell_renderer.device(),
1087                self.cell_renderer.queue(),
1088                &mut render_pass,
1089                &self.sixel_graphics,
1090                self.size.width as f32,
1091                self.size.height as f32,
1092            )?;
1093        } // render_pass dropped here
1094
1095        // Submit sixel commands
1096        self.cell_renderer
1097            .queue()
1098            .submit(std::iter::once(encoder.finish()));
1099
1100        Ok(())
1101    }
1102
1103    /// Render egui overlay on top of the terminal
1104    fn render_egui(
1105        &mut self,
1106        surface_texture: &wgpu::SurfaceTexture,
1107        egui_output: egui::FullOutput,
1108        egui_ctx: &egui::Context,
1109        force_opaque: bool,
1110    ) -> Result<()> {
1111        use wgpu::TextureViewDescriptor;
1112
1113        // Create view of the surface texture
1114        let view = surface_texture
1115            .texture
1116            .create_view(&TextureViewDescriptor::default());
1117
1118        // Create command encoder for egui
1119        let mut encoder =
1120            self.cell_renderer
1121                .device()
1122                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1123                    label: Some("egui encoder"),
1124                });
1125
1126        // Convert egui output to screen descriptor
1127        let screen_descriptor = egui_wgpu::ScreenDescriptor {
1128            size_in_pixels: [self.size.width, self.size.height],
1129            pixels_per_point: egui_output.pixels_per_point,
1130        };
1131
1132        // Update egui textures
1133        for (id, image_delta) in &egui_output.textures_delta.set {
1134            self.egui_renderer.update_texture(
1135                self.cell_renderer.device(),
1136                self.cell_renderer.queue(),
1137                *id,
1138                image_delta,
1139            );
1140        }
1141
1142        // Tessellate egui shapes into paint jobs
1143        let mut paint_jobs = egui_ctx.tessellate(egui_output.shapes, egui_output.pixels_per_point);
1144
1145        // If requested, force all egui vertices to full opacity so UI stays solid
1146        if force_opaque {
1147            for job in paint_jobs.iter_mut() {
1148                match &mut job.primitive {
1149                    egui::epaint::Primitive::Mesh(mesh) => {
1150                        for v in mesh.vertices.iter_mut() {
1151                            v.color[3] = 255;
1152                        }
1153                    }
1154                    egui::epaint::Primitive::Callback(_) => {}
1155                }
1156            }
1157        }
1158
1159        // Update egui buffers
1160        self.egui_renderer.update_buffers(
1161            self.cell_renderer.device(),
1162            self.cell_renderer.queue(),
1163            &mut encoder,
1164            &paint_jobs,
1165            &screen_descriptor,
1166        );
1167
1168        // Render egui on top of the terminal content
1169        {
1170            let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1171                label: Some("egui render pass"),
1172                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1173                    view: &view,
1174                    resolve_target: None,
1175                    ops: wgpu::Operations {
1176                        load: wgpu::LoadOp::Load, // Don't clear - render on top of terminal
1177                        store: wgpu::StoreOp::Store,
1178                    },
1179                    depth_slice: None,
1180                })],
1181                depth_stencil_attachment: None,
1182                timestamp_writes: None,
1183                occlusion_query_set: None,
1184            });
1185
1186            // Convert to 'static lifetime as required by egui_renderer.render()
1187            let mut render_pass = render_pass.forget_lifetime();
1188
1189            self.egui_renderer
1190                .render(&mut render_pass, &paint_jobs, &screen_descriptor);
1191        } // render_pass dropped here
1192
1193        // Submit egui commands
1194        self.cell_renderer
1195            .queue()
1196            .submit(std::iter::once(encoder.finish()));
1197
1198        // Free egui textures
1199        for id in &egui_output.textures_delta.free {
1200            self.egui_renderer.free_texture(id);
1201        }
1202
1203        Ok(())
1204    }
1205
1206    /// Get the current size
1207    pub fn size(&self) -> PhysicalSize<u32> {
1208        self.size
1209    }
1210
1211    /// Get the current grid dimensions (columns, rows)
1212    pub fn grid_size(&self) -> (usize, usize) {
1213        self.cell_renderer.grid_size()
1214    }
1215
1216    /// Get cell width in pixels
1217    pub fn cell_width(&self) -> f32 {
1218        self.cell_renderer.cell_width()
1219    }
1220
1221    /// Get cell height in pixels
1222    pub fn cell_height(&self) -> f32 {
1223        self.cell_renderer.cell_height()
1224    }
1225
1226    /// Get window padding in pixels
1227    pub fn window_padding(&self) -> f32 {
1228        self.cell_renderer.window_padding()
1229    }
1230
1231    /// Check if a point (in pixel coordinates) is within the scrollbar bounds
1232    ///
1233    /// # Arguments
1234    /// * `x` - X coordinate in pixels (from left edge)
1235    /// * `y` - Y coordinate in pixels (from top edge)
1236    pub fn scrollbar_contains_point(&self, x: f32, y: f32) -> bool {
1237        self.cell_renderer.scrollbar_contains_point(x, y)
1238    }
1239
1240    /// Get the scrollbar thumb bounds (top Y, height) in pixels
1241    pub fn scrollbar_thumb_bounds(&self) -> Option<(f32, f32)> {
1242        self.cell_renderer.scrollbar_thumb_bounds()
1243    }
1244
1245    /// Check if an X coordinate is within the scrollbar track
1246    pub fn scrollbar_track_contains_x(&self, x: f32) -> bool {
1247        self.cell_renderer.scrollbar_track_contains_x(x)
1248    }
1249
1250    /// Convert a mouse Y position to a scroll offset
1251    ///
1252    /// # Arguments
1253    /// * `mouse_y` - Mouse Y coordinate in pixels (from top edge)
1254    ///
1255    /// # Returns
1256    /// The scroll offset corresponding to the mouse position, or None if scrollbar is not visible
1257    pub fn scrollbar_mouse_y_to_scroll_offset(&self, mouse_y: f32) -> Option<usize> {
1258        self.cell_renderer
1259            .scrollbar_mouse_y_to_scroll_offset(mouse_y)
1260    }
1261
1262    /// Check if the renderer needs to be redrawn
1263    #[allow(dead_code)]
1264    pub fn is_dirty(&self) -> bool {
1265        self.dirty
1266    }
1267
1268    /// Mark the renderer as dirty, forcing a redraw on next render call
1269    #[allow(dead_code)]
1270    pub fn mark_dirty(&mut self) {
1271        self.dirty = true;
1272    }
1273
1274    /// Clear all cached sixel textures
1275    #[allow(dead_code)]
1276    pub fn clear_sixel_cache(&mut self) {
1277        self.graphics_renderer.clear_cache();
1278        self.sixel_graphics.clear();
1279        self.dirty = true;
1280    }
1281
1282    /// Get the number of cached sixel textures
1283    #[allow(dead_code)]
1284    pub fn sixel_cache_size(&self) -> usize {
1285        self.graphics_renderer.cache_size()
1286    }
1287
1288    /// Remove a specific sixel texture from cache
1289    #[allow(dead_code)]
1290    pub fn remove_sixel_texture(&mut self, id: u64) {
1291        self.graphics_renderer.remove_texture(id);
1292        self.sixel_graphics
1293            .retain(|(gid, _, _, _, _, _, _)| *gid != id);
1294        self.dirty = true;
1295    }
1296
1297    /// Set debug overlay text to be rendered
1298    #[allow(dead_code)]
1299    #[allow(dead_code)]
1300    pub fn render_debug_overlay(&mut self, text: &str) {
1301        self.debug_text = Some(text.to_string());
1302        self.dirty = true; // Mark dirty to ensure debug overlay renders
1303    }
1304
1305    /// Reconfigure the surface (call when surface becomes outdated or lost)
1306    /// This typically happens when dragging the window between displays
1307    pub fn reconfigure_surface(&mut self) {
1308        self.cell_renderer.reconfigure_surface();
1309        self.dirty = true;
1310    }
1311
1312    /// Clear the glyph cache to force re-rasterization
1313    /// Useful after display changes where font rendering may differ
1314    pub fn clear_glyph_cache(&mut self) {
1315        self.cell_renderer.clear_glyph_cache();
1316        self.dirty = true;
1317    }
1318}