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