par_term/renderer/
mod.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
9pub mod graphics;
10pub mod shaders;
11
12/// Renderer for the terminal using custom wgpu cell renderer
13pub struct Renderer {
14    // Cell renderer (owns the scrollbar)
15    pub(crate) cell_renderer: CellRenderer,
16
17    // Graphics renderer for sixel images
18    pub(crate) graphics_renderer: GraphicsRenderer,
19
20    // Current sixel graphics to render: (id, row, col, width_cells, height_cells, alpha, scroll_offset_rows)
21    // Note: row is isize to allow negative values for graphics scrolled off top
22    pub(crate) sixel_graphics: Vec<(u64, isize, usize, usize, usize, f32, usize)>,
23
24    // egui renderer for settings UI
25    pub(crate) egui_renderer: egui_wgpu::Renderer,
26
27    // Custom shader renderer for post-processing effects (background shader)
28    pub(crate) custom_shader_renderer: Option<CustomShaderRenderer>,
29    // Track current shader path to detect changes
30    pub(crate) custom_shader_path: Option<String>,
31
32    // Cursor shader renderer for cursor-specific effects (separate from background shader)
33    pub(crate) cursor_shader_renderer: Option<CustomShaderRenderer>,
34    // Track current cursor shader path to detect changes
35    pub(crate) cursor_shader_path: Option<String>,
36
37    // Cached for convenience
38    pub(crate) size: PhysicalSize<u32>,
39
40    // Dirty flag for optimization - only render when content has changed
41    pub(crate) dirty: bool,
42
43    // Debug overlay text
44    #[allow(dead_code)]
45    #[allow(dead_code)]
46    pub(crate) debug_text: Option<String>,
47}
48
49impl Renderer {
50    /// Create a new renderer
51    #[allow(clippy::too_many_arguments)]
52    pub async fn new(
53        window: Arc<Window>,
54        font_family: Option<&str>,
55        font_family_bold: Option<&str>,
56        font_family_italic: Option<&str>,
57        font_family_bold_italic: Option<&str>,
58        font_ranges: &[crate::config::FontRange],
59        font_size: f32,
60        window_padding: f32,
61        line_spacing: f32,
62        char_spacing: f32,
63        scrollbar_position: &str,
64        scrollbar_width: f32,
65        scrollbar_thumb_color: [f32; 4],
66        scrollbar_track_color: [f32; 4],
67        enable_text_shaping: bool,
68        enable_ligatures: bool,
69        enable_kerning: bool,
70        vsync_mode: crate::config::VsyncMode,
71        window_opacity: f32,
72        background_color: [u8; 3],
73        background_image_path: Option<&str>,
74        background_image_enabled: bool,
75        background_image_mode: crate::config::BackgroundImageMode,
76        background_image_opacity: f32,
77        custom_shader_path: Option<&str>,
78        custom_shader_enabled: bool,
79        custom_shader_animation: bool,
80        custom_shader_animation_speed: f32,
81        custom_shader_text_opacity: f32,
82        custom_shader_full_content: bool,
83        custom_shader_brightness: f32,
84        // Custom shader channel textures (iChannel1-4)
85        custom_shader_channel_paths: &[Option<std::path::PathBuf>; 4],
86        // Cursor shader settings (separate from background shader)
87        cursor_shader_path: Option<&str>,
88        cursor_shader_enabled: bool,
89        cursor_shader_animation: bool,
90        cursor_shader_animation_speed: f32,
91    ) -> Result<Self> {
92        let size = window.inner_size();
93        let scale_factor = window.scale_factor();
94
95        // Standard DPI for the platform
96        // macOS typically uses 72 DPI for points, Windows and most Linux use 96 DPI
97        let platform_dpi = if cfg!(target_os = "macos") {
98            72.0
99        } else {
100            96.0
101        };
102
103        // Convert font size from points to pixels for cell size calculation, honoring DPI and scale
104        let base_font_pixels = font_size * platform_dpi / 72.0;
105        let font_size_pixels = (base_font_pixels * scale_factor as f32).max(1.0);
106
107        // Preliminary font lookup to get metrics for accurate cell height
108        let font_manager = crate::font_manager::FontManager::new(
109            font_family,
110            font_family_bold,
111            font_family_italic,
112            font_family_bold_italic,
113            font_ranges,
114        )?;
115
116        let (font_ascent, font_descent, font_leading, char_advance) = {
117            let primary_font = font_manager.get_font(0).unwrap();
118            let metrics = primary_font.metrics(&[]);
119            let scale = font_size_pixels / metrics.units_per_em as f32;
120
121            // Get advance width of a standard character ('m' is common for monospace width)
122            let glyph_id = primary_font.charmap().map('m');
123            let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
124
125            (
126                metrics.ascent * scale,
127                metrics.descent * scale,
128                metrics.leading * scale,
129                advance,
130            )
131        };
132
133        // Use font metrics for cell height if line_spacing is 1.0
134        // Natural line height = ascent + descent + leading
135        let natural_line_height = font_ascent + font_descent + font_leading;
136        let char_height = (natural_line_height * line_spacing).max(1.0);
137
138        // Calculate available space after padding (padding on all sides)
139        let available_width = (size.width as f32 - window_padding * 2.0).max(0.0);
140        let available_height = (size.height as f32 - window_padding * 2.0).max(0.0);
141
142        // Calculate terminal dimensions based on font size in pixels and spacing
143        let char_width = (char_advance * char_spacing).max(1.0); // Configurable character width
144        let cols = (available_width / char_width).max(1.0) as usize;
145        let rows = (available_height / char_height).max(1.0) as usize;
146
147        // Create cell renderer with font fallback support (owns scrollbar)
148        let cell_renderer = CellRenderer::new(
149            window.clone(),
150            font_family,
151            font_family_bold,
152            font_family_italic,
153            font_family_bold_italic,
154            font_ranges,
155            font_size,
156            cols,
157            rows,
158            window_padding,
159            line_spacing,
160            char_spacing,
161            scrollbar_position,
162            scrollbar_width,
163            scrollbar_thumb_color,
164            scrollbar_track_color,
165            enable_text_shaping,
166            enable_ligatures,
167            enable_kerning,
168            vsync_mode,
169            window_opacity,
170            background_color,
171            if background_image_enabled {
172                background_image_path
173            } else {
174                None
175            },
176            background_image_mode,
177            background_image_opacity,
178        )
179        .await?;
180
181        // Create egui renderer for settings UI
182        let egui_renderer = egui_wgpu::Renderer::new(
183            cell_renderer.device(),
184            cell_renderer.surface_format(),
185            egui_wgpu::RendererOptions {
186                msaa_samples: 1,
187                depth_stencil_format: None,
188                dithering: false,
189                predictable_texture_filtering: false,
190            },
191        );
192
193        // Create graphics renderer for sixel images
194        let graphics_renderer = GraphicsRenderer::new(
195            cell_renderer.device(),
196            cell_renderer.surface_format(),
197            cell_renderer.cell_width(),
198            cell_renderer.cell_height(),
199            cell_renderer.window_padding(),
200        )?;
201
202        // Create custom shader renderer if configured
203        let (custom_shader_renderer, initial_shader_path) = if custom_shader_enabled {
204            if let Some(shader_path) = custom_shader_path {
205                let path = crate::config::Config::shader_path(shader_path);
206                match CustomShaderRenderer::new(
207                    cell_renderer.device(),
208                    cell_renderer.queue(),
209                    cell_renderer.surface_format(),
210                    &path,
211                    size.width,
212                    size.height,
213                    custom_shader_animation,
214                    custom_shader_animation_speed,
215                    window_opacity,
216                    custom_shader_text_opacity,
217                    custom_shader_full_content,
218                    custom_shader_channel_paths,
219                ) {
220                    Ok(mut renderer) => {
221                        // Sync cell dimensions for cursor position calculation
222                        renderer.update_cell_dimensions(
223                            cell_renderer.cell_width(),
224                            cell_renderer.cell_height(),
225                            window_padding,
226                        );
227                        // Apply brightness setting
228                        renderer.set_brightness(custom_shader_brightness);
229                        log::info!(
230                            "Custom shader renderer initialized from: {}",
231                            path.display()
232                        );
233                        (Some(renderer), Some(shader_path.to_string()))
234                    }
235                    Err(e) => {
236                        log::error!("Failed to load custom shader '{}': {}", path.display(), e);
237                        (None, None)
238                    }
239                }
240            } else {
241                (None, None)
242            }
243        } else {
244            (None, None)
245        };
246
247        // Create cursor shader renderer if configured (separate from background shader)
248        let (cursor_shader_renderer, initial_cursor_shader_path) = if cursor_shader_enabled {
249            if let Some(shader_path) = cursor_shader_path {
250                let path = crate::config::Config::shader_path(shader_path);
251                // Cursor shader doesn't use channel textures
252                let empty_channels: [Option<std::path::PathBuf>; 4] = [None, None, None, None];
253                match CustomShaderRenderer::new(
254                    cell_renderer.device(),
255                    cell_renderer.queue(),
256                    cell_renderer.surface_format(),
257                    &path,
258                    size.width,
259                    size.height,
260                    cursor_shader_animation,
261                    cursor_shader_animation_speed,
262                    window_opacity,
263                    1.0,  // Text opacity (cursor shader always uses 1.0)
264                    true, // Full content mode (cursor shader always uses full content)
265                    &empty_channels,
266                ) {
267                    Ok(mut renderer) => {
268                        // Sync cell dimensions for cursor position calculation
269                        let cell_w = cell_renderer.cell_width();
270                        let cell_h = cell_renderer.cell_height();
271                        renderer.update_cell_dimensions(cell_w, cell_h, window_padding);
272                        log::info!(
273                            "Cursor shader renderer initialized from: {} (cell={}x{}, padding={})",
274                            path.display(),
275                            cell_w,
276                            cell_h,
277                            window_padding
278                        );
279                        (Some(renderer), Some(shader_path.to_string()))
280                    }
281                    Err(e) => {
282                        log::error!("Failed to load cursor shader '{}': {}", path.display(), e);
283                        (None, None)
284                    }
285                }
286            } else {
287                (None, None)
288            }
289        } else {
290            (None, None)
291        };
292
293        Ok(Self {
294            cell_renderer,
295            graphics_renderer,
296            sixel_graphics: Vec::new(),
297            egui_renderer,
298            custom_shader_renderer,
299            custom_shader_path: initial_shader_path,
300            cursor_shader_renderer,
301            cursor_shader_path: initial_cursor_shader_path,
302            size,
303            dirty: true, // Start dirty to ensure initial render
304            debug_text: None,
305        })
306    }
307
308    /// Resize the renderer and recalculate grid dimensions based on padding/font metrics
309    pub fn resize(&mut self, new_size: PhysicalSize<u32>) -> (usize, usize) {
310        if new_size.width > 0 && new_size.height > 0 {
311            self.size = new_size;
312            self.dirty = true; // Mark dirty on resize
313            let result = self.cell_renderer.resize(new_size.width, new_size.height);
314
315            // Update graphics renderer cell dimensions
316            self.graphics_renderer.update_cell_dimensions(
317                self.cell_renderer.cell_width(),
318                self.cell_renderer.cell_height(),
319                self.cell_renderer.window_padding(),
320            );
321
322            // Update custom shader renderer dimensions
323            if let Some(ref mut custom_shader) = self.custom_shader_renderer {
324                custom_shader.resize(self.cell_renderer.device(), new_size.width, new_size.height);
325                // Sync cell dimensions for cursor position calculation
326                custom_shader.update_cell_dimensions(
327                    self.cell_renderer.cell_width(),
328                    self.cell_renderer.cell_height(),
329                    self.cell_renderer.window_padding(),
330                );
331            }
332
333            // Update cursor shader renderer dimensions
334            if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
335                cursor_shader.resize(self.cell_renderer.device(), new_size.width, new_size.height);
336                // Sync cell dimensions for cursor position calculation
337                cursor_shader.update_cell_dimensions(
338                    self.cell_renderer.cell_width(),
339                    self.cell_renderer.cell_height(),
340                    self.cell_renderer.window_padding(),
341                );
342            }
343
344            return result;
345        }
346
347        self.cell_renderer.grid_size()
348    }
349
350    /// Update scale factor and resize so the PTY grid matches the new DPI.
351    pub fn handle_scale_factor_change(
352        &mut self,
353        scale_factor: f64,
354        new_size: PhysicalSize<u32>,
355    ) -> (usize, usize) {
356        self.cell_renderer.update_scale_factor(scale_factor);
357        self.resize(new_size)
358    }
359
360    /// Update the terminal cells
361    pub fn update_cells(&mut self, cells: &[Cell]) {
362        self.cell_renderer.update_cells(cells);
363        self.dirty = true; // Mark dirty when cells change
364    }
365
366    /// Clear all cells in the renderer.
367    /// Call this when switching tabs to ensure a clean slate.
368    pub fn clear_all_cells(&mut self) {
369        self.cell_renderer.clear_all_cells();
370        self.dirty = true;
371    }
372
373    /// Update cursor position and style for geometric rendering
374    pub fn update_cursor(
375        &mut self,
376        position: (usize, usize),
377        opacity: f32,
378        style: par_term_emu_core_rust::cursor::CursorStyle,
379    ) {
380        self.cell_renderer.update_cursor(position, opacity, style);
381        self.dirty = true;
382    }
383
384    /// Clear cursor (hide it)
385    pub fn clear_cursor(&mut self) {
386        self.cell_renderer.clear_cursor();
387        self.dirty = true;
388    }
389
390    /// Update scrollbar state
391    ///
392    /// # Arguments
393    /// * `scroll_offset` - Current scroll offset (0 = at bottom)
394    /// * `visible_lines` - Number of lines visible on screen
395    /// * `total_lines` - Total number of lines including scrollback
396    pub fn update_scrollbar(
397        &mut self,
398        scroll_offset: usize,
399        visible_lines: usize,
400        total_lines: usize,
401    ) {
402        self.cell_renderer
403            .update_scrollbar(scroll_offset, visible_lines, total_lines);
404        self.dirty = true; // Mark dirty when scrollbar changes
405    }
406
407    /// Set the visual bell flash intensity
408    ///
409    /// # Arguments
410    /// * `intensity` - Flash intensity from 0.0 (no flash) to 1.0 (full white flash)
411    pub fn set_visual_bell_intensity(&mut self, intensity: f32) {
412        self.cell_renderer.set_visual_bell_intensity(intensity);
413        if intensity > 0.0 {
414            self.dirty = true; // Mark dirty when flash is active
415        }
416    }
417
418    /// Update window opacity in real-time
419    pub fn update_opacity(&mut self, opacity: f32) {
420        self.cell_renderer.update_opacity(opacity);
421
422        // Propagate to custom shader renderer if present
423        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
424            custom_shader.set_opacity(opacity);
425        }
426
427        self.dirty = true;
428    }
429
430    /// Update cursor color for cell rendering
431    pub fn update_cursor_color(&mut self, color: [u8; 3]) {
432        self.cell_renderer.update_cursor_color(color);
433        self.dirty = true;
434    }
435
436    /// Set whether cursor should be hidden when cursor shader is active
437    pub fn set_cursor_hidden_for_shader(&mut self, hidden: bool) {
438        self.cell_renderer.set_cursor_hidden_for_shader(hidden);
439        self.dirty = true;
440    }
441
442    /// Update window padding in real-time without full renderer rebuild
443    /// Returns Some((cols, rows)) if grid size changed and terminal needs resize
444    pub fn update_window_padding(&mut self, padding: f32) -> Option<(usize, usize)> {
445        let result = self.cell_renderer.update_window_padding(padding);
446        self.dirty = true;
447        result
448    }
449
450    /// Enable/disable background image and reload if needed
451    pub fn set_background_image_enabled(
452        &mut self,
453        enabled: bool,
454        path: Option<&str>,
455        mode: crate::config::BackgroundImageMode,
456        opacity: f32,
457    ) {
458        let path = if enabled { path } else { None };
459        self.cell_renderer.set_background_image(path, mode, opacity);
460        self.dirty = true;
461    }
462
463    /// Update scrollbar appearance in real-time
464    pub fn update_scrollbar_appearance(
465        &mut self,
466        width: f32,
467        thumb_color: [f32; 4],
468        track_color: [f32; 4],
469    ) {
470        self.cell_renderer
471            .update_scrollbar_appearance(width, thumb_color, track_color);
472        self.dirty = true;
473    }
474
475    /// Update scrollbar position (left/right) in real-time
476    #[allow(dead_code)]
477    pub fn update_scrollbar_position(&mut self, position: &str) {
478        self.cell_renderer.update_scrollbar_position(position);
479        self.dirty = true;
480    }
481
482    /// Update background image opacity in real-time
483    pub fn update_background_image_opacity(&mut self, opacity: f32) {
484        self.cell_renderer.update_background_image_opacity(opacity);
485        self.dirty = true;
486    }
487
488    /// Check if animation requires continuous rendering
489    ///
490    /// Returns true if shader animation is enabled or a cursor trail animation
491    /// might still be in progress.
492    pub fn needs_continuous_render(&self) -> bool {
493        let custom_needs = self
494            .custom_shader_renderer
495            .as_ref()
496            .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
497        let cursor_needs = self
498            .cursor_shader_renderer
499            .as_ref()
500            .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
501        custom_needs || cursor_needs
502    }
503
504    /// Render a frame with optional egui overlay
505    /// Returns true if rendering was performed, false if skipped
506    pub fn render(
507        &mut self,
508        egui_data: Option<(egui::FullOutput, &egui::Context)>,
509        force_egui_opaque: bool,
510        show_scrollbar: bool,
511    ) -> Result<bool> {
512        // Custom shader animation forces continuous rendering
513        let force_render = self.needs_continuous_render();
514
515        if !self.dirty && egui_data.is_none() && !force_render {
516            // Skip rendering if nothing has changed
517            return Ok(false);
518        }
519
520        // Check if shaders are enabled
521        let has_custom_shader = self.custom_shader_renderer.is_some();
522        let has_cursor_shader = self.cursor_shader_renderer.is_some();
523
524        // Cell renderer renders terminal content
525        let t1 = std::time::Instant::now();
526        let surface_texture = if has_custom_shader {
527            // Render terminal to intermediate texture for background shader
528            self.cell_renderer.render_to_texture(
529                self.custom_shader_renderer
530                    .as_ref()
531                    .unwrap()
532                    .intermediate_texture_view(),
533            )?
534        } else if has_cursor_shader {
535            // Render terminal to intermediate texture for cursor shader
536            self.cell_renderer.render_to_texture(
537                self.cursor_shader_renderer
538                    .as_ref()
539                    .unwrap()
540                    .intermediate_texture_view(),
541            )?
542        } else {
543            // Render directly to surface
544            self.cell_renderer.render(show_scrollbar)?
545        };
546        let cell_render_time = t1.elapsed();
547
548        // Apply background custom shader if enabled
549        let t_custom = std::time::Instant::now();
550        let custom_shader_time = if let Some(ref mut custom_shader) = self.custom_shader_renderer {
551            if has_cursor_shader {
552                // Background shader renders to cursor shader's intermediate texture
553                custom_shader.render(
554                    self.cell_renderer.device(),
555                    self.cell_renderer.queue(),
556                    self.cursor_shader_renderer
557                        .as_ref()
558                        .unwrap()
559                        .intermediate_texture_view(),
560                )?;
561            } else {
562                // Background shader renders directly to surface
563                let surface_view = surface_texture
564                    .texture
565                    .create_view(&wgpu::TextureViewDescriptor::default());
566                custom_shader.render(
567                    self.cell_renderer.device(),
568                    self.cell_renderer.queue(),
569                    &surface_view,
570                )?;
571
572                // Render overlays (scrollbar, visual bell) on top after shader
573                self.cell_renderer
574                    .render_overlays(&surface_texture, show_scrollbar)?;
575            }
576            t_custom.elapsed()
577        } else {
578            std::time::Duration::ZERO
579        };
580
581        // Apply cursor shader if enabled
582        let t_cursor = std::time::Instant::now();
583        let cursor_shader_time = if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
584            log::trace!("Rendering cursor shader");
585            let surface_view = surface_texture
586                .texture
587                .create_view(&wgpu::TextureViewDescriptor::default());
588            cursor_shader.render(
589                self.cell_renderer.device(),
590                self.cell_renderer.queue(),
591                &surface_view,
592            )?;
593
594            // Render overlays (scrollbar, visual bell) on top after cursor shader
595            self.cell_renderer
596                .render_overlays(&surface_texture, show_scrollbar)?;
597            t_cursor.elapsed()
598        } else {
599            std::time::Duration::ZERO
600        };
601
602        // Render sixel graphics on top of cells
603        let t2 = std::time::Instant::now();
604        if !self.sixel_graphics.is_empty() {
605            self.render_sixel_graphics(&surface_texture)?;
606        }
607        let sixel_render_time = t2.elapsed();
608
609        // Render egui overlay if provided
610        let t3 = std::time::Instant::now();
611        if let Some((egui_output, egui_ctx)) = egui_data {
612            self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
613        }
614        let egui_render_time = t3.elapsed();
615
616        // Present the surface texture - THIS IS WHERE VSYNC WAIT HAPPENS
617        let t4 = std::time::Instant::now();
618        surface_texture.present();
619        let present_time = t4.elapsed();
620
621        // Log timing breakdown
622        let total = cell_render_time
623            + custom_shader_time
624            + cursor_shader_time
625            + sixel_render_time
626            + egui_render_time
627            + present_time;
628        if present_time.as_millis() > 10 || total.as_millis() > 10 {
629            log::info!(
630                "RENDER_BREAKDOWN: CellRender={:.2}ms BgShader={:.2}ms CursorShader={:.2}ms Sixel={:.2}ms Egui={:.2}ms PRESENT={:.2}ms Total={:.2}ms",
631                cell_render_time.as_secs_f64() * 1000.0,
632                custom_shader_time.as_secs_f64() * 1000.0,
633                cursor_shader_time.as_secs_f64() * 1000.0,
634                sixel_render_time.as_secs_f64() * 1000.0,
635                egui_render_time.as_secs_f64() * 1000.0,
636                present_time.as_secs_f64() * 1000.0,
637                total.as_secs_f64() * 1000.0
638            );
639        }
640
641        // Clear dirty flag after successful render
642        self.dirty = false;
643
644        Ok(true)
645    }
646
647    /// Render egui overlay on top of the terminal
648    fn render_egui(
649        &mut self,
650        surface_texture: &wgpu::SurfaceTexture,
651        egui_output: egui::FullOutput,
652        egui_ctx: &egui::Context,
653        force_opaque: bool,
654    ) -> Result<()> {
655        use wgpu::TextureViewDescriptor;
656
657        // Create view of the surface texture
658        let view = surface_texture
659            .texture
660            .create_view(&TextureViewDescriptor::default());
661
662        // Create command encoder for egui
663        let mut encoder =
664            self.cell_renderer
665                .device()
666                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
667                    label: Some("egui encoder"),
668                });
669
670        // Convert egui output to screen descriptor
671        let screen_descriptor = egui_wgpu::ScreenDescriptor {
672            size_in_pixels: [self.size.width, self.size.height],
673            pixels_per_point: egui_output.pixels_per_point,
674        };
675
676        // Update egui textures
677        for (id, image_delta) in &egui_output.textures_delta.set {
678            self.egui_renderer.update_texture(
679                self.cell_renderer.device(),
680                self.cell_renderer.queue(),
681                *id,
682                image_delta,
683            );
684        }
685
686        // Tessellate egui shapes into paint jobs
687        let mut paint_jobs = egui_ctx.tessellate(egui_output.shapes, egui_output.pixels_per_point);
688
689        // If requested, force all egui vertices to full opacity so UI stays solid
690        if force_opaque {
691            for job in paint_jobs.iter_mut() {
692                match &mut job.primitive {
693                    egui::epaint::Primitive::Mesh(mesh) => {
694                        for v in mesh.vertices.iter_mut() {
695                            v.color[3] = 255;
696                        }
697                    }
698                    egui::epaint::Primitive::Callback(_) => {}
699                }
700            }
701        }
702
703        // Update egui buffers
704        self.egui_renderer.update_buffers(
705            self.cell_renderer.device(),
706            self.cell_renderer.queue(),
707            &mut encoder,
708            &paint_jobs,
709            &screen_descriptor,
710        );
711
712        // Render egui on top of the terminal content
713        {
714            let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
715                label: Some("egui render pass"),
716                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
717                    view: &view,
718                    resolve_target: None,
719                    ops: wgpu::Operations {
720                        load: wgpu::LoadOp::Load, // Don't clear - render on top of terminal
721                        store: wgpu::StoreOp::Store,
722                    },
723                    depth_slice: None,
724                })],
725                depth_stencil_attachment: None,
726                timestamp_writes: None,
727                occlusion_query_set: None,
728            });
729
730            // Convert to 'static lifetime as required by egui_renderer.render()
731            let mut render_pass = render_pass.forget_lifetime();
732
733            self.egui_renderer
734                .render(&mut render_pass, &paint_jobs, &screen_descriptor);
735        } // render_pass dropped here
736
737        // Submit egui commands
738        self.cell_renderer
739            .queue()
740            .submit(std::iter::once(encoder.finish()));
741
742        // Free egui textures
743        for id in &egui_output.textures_delta.free {
744            self.egui_renderer.free_texture(id);
745        }
746
747        Ok(())
748    }
749
750    /// Get the current size
751    pub fn size(&self) -> PhysicalSize<u32> {
752        self.size
753    }
754
755    /// Get the current grid dimensions (columns, rows)
756    pub fn grid_size(&self) -> (usize, usize) {
757        self.cell_renderer.grid_size()
758    }
759
760    /// Get cell width in pixels
761    pub fn cell_width(&self) -> f32 {
762        self.cell_renderer.cell_width()
763    }
764
765    /// Get cell height in pixels
766    pub fn cell_height(&self) -> f32 {
767        self.cell_renderer.cell_height()
768    }
769
770    /// Get window padding in pixels
771    pub fn window_padding(&self) -> f32 {
772        self.cell_renderer.window_padding()
773    }
774
775    /// Check if a point (in pixel coordinates) is within the scrollbar bounds
776    ///
777    /// # Arguments
778    /// * `x` - X coordinate in pixels (from left edge)
779    /// * `y` - Y coordinate in pixels (from top edge)
780    pub fn scrollbar_contains_point(&self, x: f32, y: f32) -> bool {
781        self.cell_renderer.scrollbar_contains_point(x, y)
782    }
783
784    /// Get the scrollbar thumb bounds (top Y, height) in pixels
785    pub fn scrollbar_thumb_bounds(&self) -> Option<(f32, f32)> {
786        self.cell_renderer.scrollbar_thumb_bounds()
787    }
788
789    /// Check if an X coordinate is within the scrollbar track
790    pub fn scrollbar_track_contains_x(&self, x: f32) -> bool {
791        self.cell_renderer.scrollbar_track_contains_x(x)
792    }
793
794    /// Convert a mouse Y position to a scroll offset
795    ///
796    /// # Arguments
797    /// * `mouse_y` - Mouse Y coordinate in pixels (from top edge)
798    ///
799    /// # Returns
800    /// The scroll offset corresponding to the mouse position, or None if scrollbar is not visible
801    pub fn scrollbar_mouse_y_to_scroll_offset(&self, mouse_y: f32) -> Option<usize> {
802        self.cell_renderer
803            .scrollbar_mouse_y_to_scroll_offset(mouse_y)
804    }
805
806    /// Check if the renderer needs to be redrawn
807    #[allow(dead_code)]
808    pub fn is_dirty(&self) -> bool {
809        self.dirty
810    }
811
812    /// Mark the renderer as dirty, forcing a redraw on next render call
813    #[allow(dead_code)]
814    pub fn mark_dirty(&mut self) {
815        self.dirty = true;
816    }
817
818    /// Set debug overlay text to be rendered
819    #[allow(dead_code)]
820    #[allow(dead_code)]
821    pub fn render_debug_overlay(&mut self, text: &str) {
822        self.debug_text = Some(text.to_string());
823        self.dirty = true; // Mark dirty to ensure debug overlay renders
824    }
825
826    /// Reconfigure the surface (call when surface becomes outdated or lost)
827    /// This typically happens when dragging the window between displays
828    pub fn reconfigure_surface(&mut self) {
829        self.cell_renderer.reconfigure_surface();
830        self.dirty = true;
831    }
832
833    /// Clear the glyph cache to force re-rasterization
834    /// Useful after display changes where font rendering may differ
835    pub fn clear_glyph_cache(&mut self) {
836        self.cell_renderer.clear_glyph_cache();
837        self.dirty = true;
838    }
839}