Skip to main content

par_term_render/renderer/
mod.rs

1// ARC-009 TODO: This file is 743 lines (limit: 800 — approaching threshold). When it
2// exceeds 800 lines, extract into sibling sub-modules under renderer/:
3//
4//   frame_timing.rs   — Frame throttle logic and vsync bookkeeping
5//   resize_ops.rs     — Window/surface resize and snap-to-grid
6//
7// The renderer/ directory already has render_passes.rs, rendering.rs, egui_render.rs,
8// graphics.rs, shaders/, params.rs, state.rs — follow that existing split pattern.
9//
10// Tracking: Issue ARC-009 in AUDIT.md.
11
12use crate::cell_renderer::{Cell, CellRenderer, CellRendererConfig, PaneViewport};
13use crate::custom_shader_renderer::CustomShaderRenderer;
14use crate::graphics_renderer::GraphicsRenderer;
15use anyhow::Result;
16use winit::dpi::PhysicalSize;
17
18mod egui_render;
19pub mod graphics;
20pub mod params;
21
22mod render_passes;
23mod rendering;
24pub mod shaders;
25mod state;
26
27// Re-export SeparatorMark from par-term-config
28pub use par_term_config::SeparatorMark;
29pub use params::RendererParams;
30pub use rendering::SplitPanesRenderParams;
31
32/// Compute which separator marks are visible in the current viewport.
33///
34/// Maps absolute scrollback line numbers to screen rows for the current view.
35/// Returns only the marks whose absolute line index falls within the visible
36/// window `[viewport_start, viewport_start + visible_lines)`, converting each
37/// to a zero-based screen row. No deduplication or merging is performed; marks
38/// are returned in the same order they appear in `marks`.
39pub fn compute_visible_separator_marks(
40    marks: &[par_term_config::ScrollbackMark],
41    scrollback_len: usize,
42    scroll_offset: usize,
43    visible_lines: usize,
44) -> Vec<SeparatorMark> {
45    let mut out = Vec::new();
46    fill_visible_separator_marks(
47        &mut out,
48        marks,
49        scrollback_len,
50        scroll_offset,
51        visible_lines,
52    );
53    out
54}
55
56/// Fill `out` with the separator marks visible in the current viewport, reusing
57/// the provided allocation to avoid a per-call heap allocation on the render hot path.
58///
59/// The buffer is cleared on entry. Semantics are identical to
60/// [`compute_visible_separator_marks`].
61pub(crate) fn fill_visible_separator_marks(
62    out: &mut Vec<SeparatorMark>,
63    marks: &[par_term_config::ScrollbackMark],
64    scrollback_len: usize,
65    scroll_offset: usize,
66    visible_lines: usize,
67) {
68    out.clear();
69    let viewport_start = scrollback_len.saturating_sub(scroll_offset);
70    let viewport_end = viewport_start + visible_lines;
71    for mark in marks {
72        if mark.line >= viewport_start && mark.line < viewport_end {
73            let screen_row = mark.line - viewport_start;
74            out.push((screen_row, mark.exit_code, mark.color));
75        }
76    }
77}
78
79/// Information needed to render a single pane
80pub struct PaneRenderInfo<'a> {
81    /// Viewport bounds and state for this pane
82    pub viewport: PaneViewport,
83    /// Cells to render (should match viewport grid size)
84    pub cells: &'a [Cell],
85    /// Grid dimensions (cols, rows)
86    pub grid_size: (usize, usize),
87    /// Cursor position within this pane (col, row), or None if no cursor visible
88    pub cursor_pos: Option<(usize, usize)>,
89    /// Cursor opacity (0.0 = hidden, 1.0 = fully visible)
90    pub cursor_opacity: f32,
91    /// Whether this pane has a scrollbar visible
92    pub show_scrollbar: bool,
93    /// Scrollback marks for this pane
94    pub marks: Vec<par_term_config::ScrollbackMark>,
95    /// Scrollback length for this pane (needed for separator mark mapping)
96    pub scrollback_len: usize,
97    /// Current scroll offset for this pane (needed for separator mark mapping)
98    pub scroll_offset: usize,
99    /// Per-pane background image override (None = use global background)
100    pub background: Option<par_term_config::PaneBackground>,
101    /// Inline graphics (Sixel/iTerm2/Kitty) to render for this pane
102    pub graphics: Vec<par_term_emu_core_rust::graphics::TerminalGraphic>,
103    /// Kitty virtual placements (U=1) used as prototypes for Unicode placeholder
104    /// rendering. The actual on-screen position is taken from the placeholder
105    /// cells in `cells`, not from each graphic's `position` field.
106    pub virtual_placements: Vec<par_term_emu_core_rust::graphics::TerminalGraphic>,
107}
108
109/// Information needed to render a pane divider
110#[derive(Clone, Copy, Debug)]
111pub struct DividerRenderInfo {
112    /// X position in pixels
113    pub x: f32,
114    /// Y position in pixels
115    pub y: f32,
116    /// Width in pixels
117    pub width: f32,
118    /// Height in pixels
119    pub height: f32,
120    /// Whether this divider is currently being hovered
121    pub hovered: bool,
122}
123
124impl DividerRenderInfo {
125    /// Create from a DividerRect
126    pub fn from_rect(rect: &par_term_config::DividerRect, hovered: bool) -> Self {
127        Self {
128            x: rect.x,
129            y: rect.y,
130            width: rect.width,
131            height: rect.height,
132            hovered,
133        }
134    }
135}
136
137/// Information needed to render a pane title bar
138#[derive(Clone, Debug)]
139pub struct PaneTitleInfo {
140    /// X position of the title bar in pixels
141    pub x: f32,
142    /// Y position of the title bar in pixels
143    pub y: f32,
144    /// Width of the title bar in pixels
145    pub width: f32,
146    /// Height of the title bar in pixels
147    pub height: f32,
148    /// Title text to display
149    pub title: String,
150    /// Whether this pane is focused
151    pub focused: bool,
152    /// Text color [R, G, B] as floats (0.0-1.0)
153    pub text_color: [f32; 3],
154    /// Background color [R, G, B] as floats (0.0-1.0)
155    pub bg_color: [f32; 3],
156}
157
158/// Settings for rendering pane dividers and focus indicators
159#[derive(Clone, Copy, Debug)]
160pub struct PaneDividerSettings {
161    /// Color for dividers [R, G, B] as floats (0.0-1.0)
162    pub divider_color: [f32; 3],
163    /// Color when hovering over dividers [R, G, B] as floats (0.0-1.0)
164    pub hover_color: [f32; 3],
165    /// Whether to show focus indicator around focused pane
166    pub show_focus_indicator: bool,
167    /// Color for focus indicator [R, G, B] as floats (0.0-1.0)
168    pub focus_color: [f32; 3],
169    /// Width of focus indicator border in pixels
170    pub focus_width: f32,
171    /// Style of dividers (solid, double, dashed, shadow)
172    pub divider_style: par_term_config::DividerStyle,
173}
174
175impl Default for PaneDividerSettings {
176    fn default() -> Self {
177        Self {
178            divider_color: [0.3, 0.3, 0.3],
179            hover_color: [0.5, 0.6, 0.8],
180            show_focus_indicator: true,
181            focus_color: [0.4, 0.6, 1.0],
182            focus_width: 1.0,
183            divider_style: par_term_config::DividerStyle::default(),
184        }
185    }
186}
187
188/// Renderer for the terminal using custom wgpu cell renderer
189pub struct Renderer {
190    // Cell renderer (owns the scrollbar)
191    pub(crate) cell_renderer: CellRenderer,
192
193    // Graphics renderer for sixel images
194    pub(crate) graphics_renderer: GraphicsRenderer,
195
196    // Current sixel graphics to render.
197    // Note: screen_row is isize to allow negative values for graphics scrolled off top
198    pub(crate) sixel_graphics: Vec<crate::graphics_renderer::GraphicRenderInfo>,
199
200    // egui renderer for settings UI
201    pub(crate) egui_renderer: egui_wgpu::Renderer,
202
203    // Custom shader renderer for post-processing effects (background shader)
204    pub(crate) custom_shader_renderer: Option<CustomShaderRenderer>,
205    // Track current shader path to detect changes
206    pub(crate) custom_shader_path: Option<String>,
207
208    // Cursor shader renderer for cursor-specific effects (separate from background shader)
209    pub(crate) cursor_shader_renderer: Option<CustomShaderRenderer>,
210    // Track current cursor shader path to detect changes
211    pub(crate) cursor_shader_path: Option<String>,
212
213    // Cached for convenience
214    pub(crate) size: PhysicalSize<u32>,
215
216    // Dirty flag for optimization - only render when content has changed
217    pub(crate) dirty: bool,
218
219    // Cached scrollbar state to avoid redundant GPU uploads.
220    // Includes scroll position, line counts, marks, window size, AND pane viewport
221    // bounds so that pane splits/resizes correctly trigger a scrollbar geometry update.
222    pub(crate) last_scrollbar_state: (usize, usize, usize, usize, u32, u32, u32, u32, u32, u32),
223
224    // Skip cursor shader when alt screen is active (TUI apps like vim, htop)
225    pub(crate) cursor_shader_disabled_for_alt_screen: bool,
226
227    // Debug overlay text
228    pub(crate) debug_text: Option<String>,
229
230    // Scratch buffer for divider instances, reused each frame to avoid
231    // per-call heap allocations in `render_dividers`.
232    pub(crate) scratch_divider_instances: Vec<crate::cell_renderer::BackgroundInstance>,
233}
234
235impl Renderer {
236    /// Create a new renderer
237    pub async fn new(params: RendererParams<'_>) -> Result<Self> {
238        let window = params.window;
239        let font_family = params.font_family;
240        let font_family_bold = params.font_family_bold;
241        let font_family_italic = params.font_family_italic;
242        let font_family_bold_italic = params.font_family_bold_italic;
243        let font_ranges = params.font_ranges;
244        let font_size = params.font_size;
245        let line_spacing = params.line_spacing;
246        let char_spacing = params.char_spacing;
247        let scrollbar_position = params.scrollbar_position;
248        let scrollbar_thumb_color = params.scrollbar_thumb_color;
249        let scrollbar_track_color = params.scrollbar_track_color;
250        let enable_text_shaping = params.enable_text_shaping;
251        let enable_ligatures = params.enable_ligatures;
252        let enable_kerning = params.enable_kerning;
253        let font_antialias = params.font_antialias;
254        let font_hinting = params.font_hinting;
255        let font_thin_strokes = params.font_thin_strokes;
256        let minimum_contrast = params.minimum_contrast;
257        let vsync_mode = params.vsync_mode;
258        let power_preference = params.power_preference;
259        let window_opacity = params.window_opacity;
260        let background_color = params.background_color;
261        let background_image_path = params.background_image_path;
262        let background_image_enabled = params.background_image_enabled;
263        let background_image_mode = params.background_image_mode;
264        let background_image_opacity = params.background_image_opacity;
265        let custom_shader_path = params.custom_shader_path;
266        let custom_shader_enabled = params.custom_shader_enabled;
267        let custom_shader_animation = params.custom_shader_animation;
268        let custom_shader_animation_speed = params.custom_shader_animation_speed;
269        let custom_shader_full_content = params.custom_shader_full_content;
270        let custom_shader_brightness = params.custom_shader_brightness;
271        let custom_shader_channel_paths = params.custom_shader_channel_paths;
272        let custom_shader_cubemap_path = params.custom_shader_cubemap_path;
273        let custom_shader_custom_uniforms = params.custom_shader_custom_uniforms;
274        let use_background_as_channel0 = params.use_background_as_channel0;
275        let background_channel0_blend_mode = params.background_channel0_blend_mode;
276        let custom_shader_auto_dim_under_text = params.custom_shader_auto_dim_under_text;
277        let custom_shader_auto_dim_strength = params.custom_shader_auto_dim_strength;
278        let image_scaling_mode = params.image_scaling_mode;
279        let image_preserve_aspect_ratio = params.image_preserve_aspect_ratio;
280        let cursor_shader_path = params.cursor_shader_path;
281        let cursor_shader_enabled = params.cursor_shader_enabled;
282        let cursor_shader_animation = params.cursor_shader_animation;
283        let cursor_shader_animation_speed = params.cursor_shader_animation_speed;
284
285        let size = window.inner_size();
286        let scale_factor = window.scale_factor();
287
288        // Standard DPI for the platform
289        // macOS typically uses 72 DPI for points, Windows and most Linux use 96 DPI
290        let platform_dpi = if cfg!(target_os = "macos") {
291            72.0
292        } else {
293            96.0
294        };
295
296        // Convert font size from points to pixels for cell size calculation, honoring DPI and scale
297        let base_font_pixels = font_size * platform_dpi / 72.0;
298        let font_size_pixels = (base_font_pixels * scale_factor as f32).max(1.0);
299
300        // Preliminary font lookup to get metrics for accurate cell height
301        let font_manager = par_term_fonts::font_manager::FontManager::new(
302            font_family,
303            font_family_bold,
304            font_family_italic,
305            font_family_bold_italic,
306            font_ranges,
307        )?;
308
309        let (font_ascent, font_descent, font_leading, char_advance) = {
310            let primary_font = font_manager
311                .get_font(0)
312                .expect("Primary font at index 0 must exist after FontManager initialization");
313            let metrics = primary_font.metrics(&[]);
314            let scale = font_size_pixels / metrics.units_per_em as f32;
315
316            // Get advance width of a standard character ('m' is common for monospace width)
317            let glyph_id = primary_font.charmap().map('m');
318            let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
319
320            (
321                metrics.ascent * scale,
322                metrics.descent * scale,
323                metrics.leading * scale,
324                advance,
325            )
326        };
327
328        // Use font metrics for cell height if line_spacing is 1.0
329        // Natural line height = ascent + descent + leading
330        let natural_line_height = font_ascent + font_descent + font_leading;
331        let char_height = (natural_line_height * line_spacing).max(1.0).round();
332
333        // Scale logical pixel values (config) to physical pixels (wgpu surface)
334        let scale = scale_factor as f32;
335        let window_padding = params.window_padding * scale;
336        let scrollbar_width = params.scrollbar_width * scale;
337
338        // Calculate available space after padding and scrollbar
339        let available_width = (size.width as f32 - window_padding * 2.0 - scrollbar_width).max(0.0);
340        let available_height = (size.height as f32 - window_padding * 2.0).max(0.0);
341
342        // Calculate terminal dimensions based on font size in pixels and spacing
343        let char_width = (char_advance * char_spacing).max(1.0).round(); // Configurable character width (rounded to integer pixels)
344        let cols = (available_width / char_width).max(1.0) as usize;
345        let rows = (available_height / char_height).max(1.0) as usize;
346
347        // Create cell renderer with font fallback support (owns scrollbar)
348        let bg_path = if background_image_enabled {
349            background_image_path
350        } else {
351            None
352        };
353        log::info!(
354            "Renderer::new: background_image_enabled={}, path={:?}",
355            background_image_enabled,
356            bg_path
357        );
358        let cell_renderer = CellRenderer::new(
359            window.clone(),
360            CellRendererConfig {
361                font_family,
362                font_family_bold,
363                font_family_italic,
364                font_family_bold_italic,
365                font_ranges,
366                font_size,
367                cols,
368                rows,
369                window_padding,
370                line_spacing,
371                char_spacing,
372                scrollbar_position,
373                scrollbar_width,
374                scrollbar_thumb_color,
375                scrollbar_track_color,
376                enable_text_shaping,
377                enable_ligatures,
378                enable_kerning,
379                font_antialias,
380                font_hinting,
381                font_thin_strokes,
382                minimum_contrast,
383                vsync_mode,
384                power_preference,
385                window_opacity,
386                background_color,
387                background_image_path: bg_path,
388                background_image_mode,
389                background_image_opacity,
390            },
391        )
392        .await?;
393
394        // Create egui renderer for settings UI
395        let egui_renderer = egui_wgpu::Renderer::new(
396            cell_renderer.device(),
397            cell_renderer.surface_format(),
398            egui_wgpu::RendererOptions {
399                msaa_samples: 1,
400                depth_stencil_format: None,
401                dithering: false,
402                predictable_texture_filtering: false,
403            },
404        );
405
406        // Create graphics renderer for sixel images
407        let graphics_renderer = GraphicsRenderer::new(
408            cell_renderer.device(),
409            cell_renderer.surface_format(),
410            cell_renderer.cell_width(),
411            cell_renderer.cell_height(),
412            cell_renderer.window_padding(),
413            image_scaling_mode,
414            image_preserve_aspect_ratio,
415        )?;
416
417        // Create custom shader renderer if configured
418        let (mut custom_shader_renderer, initial_shader_path) = shaders::init_custom_shader(
419            &cell_renderer,
420            shaders::CustomShaderInitParams {
421                size_width: size.width,
422                size_height: size.height,
423                window_padding,
424                path: custom_shader_path,
425                enabled: custom_shader_enabled,
426                animation: custom_shader_animation,
427                animation_speed: custom_shader_animation_speed,
428                window_opacity,
429                full_content: custom_shader_full_content,
430                brightness: custom_shader_brightness,
431                channel_paths: custom_shader_channel_paths,
432                cubemap_path: custom_shader_cubemap_path,
433                custom_uniforms: custom_shader_custom_uniforms,
434                use_background_as_channel0,
435                background_channel0_blend_mode,
436                auto_dim_under_text: custom_shader_auto_dim_under_text,
437                auto_dim_strength: custom_shader_auto_dim_strength,
438            },
439        );
440
441        // Create cursor shader renderer if configured (separate from background shader)
442        let (mut cursor_shader_renderer, initial_cursor_shader_path) = shaders::init_cursor_shader(
443            &cell_renderer,
444            shaders::CursorShaderInitParams {
445                size_width: size.width,
446                size_height: size.height,
447                window_padding,
448                path: cursor_shader_path,
449                enabled: cursor_shader_enabled,
450                animation: cursor_shader_animation,
451                animation_speed: cursor_shader_animation_speed,
452                window_opacity,
453            },
454        );
455
456        // Sync DPI scale factor to shader renderers for cursor sizing
457        if let Some(ref mut cs) = custom_shader_renderer {
458            cs.set_scale_factor(scale);
459        }
460        if let Some(ref mut cs) = cursor_shader_renderer {
461            cs.set_scale_factor(scale);
462        }
463
464        log::info!(
465            "[renderer] Renderer created: custom_shader_loaded={}, cursor_shader_loaded={}",
466            initial_shader_path.is_some(),
467            initial_cursor_shader_path.is_some()
468        );
469
470        Ok(Self {
471            cell_renderer,
472            graphics_renderer,
473            sixel_graphics: Vec::new(),
474            egui_renderer,
475            custom_shader_renderer,
476            custom_shader_path: initial_shader_path,
477            cursor_shader_renderer,
478            cursor_shader_path: initial_cursor_shader_path,
479            size,
480            dirty: true, // Start dirty to ensure initial render
481            last_scrollbar_state: (usize::MAX, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Force first update
482            cursor_shader_disabled_for_alt_screen: false,
483            debug_text: None,
484            scratch_divider_instances: Vec::new(),
485        })
486    }
487
488    /// Resize the renderer and recalculate grid dimensions based on padding/font metrics
489    pub fn resize(&mut self, new_size: PhysicalSize<u32>) -> (usize, usize) {
490        if new_size.width > 0 && new_size.height > 0 {
491            self.size = new_size;
492            self.dirty = true; // Mark dirty on resize
493            let result = self.cell_renderer.resize(new_size.width, new_size.height);
494
495            // Update graphics renderer cell dimensions
496            self.graphics_renderer.update_cell_dimensions(
497                self.cell_renderer.cell_width(),
498                self.cell_renderer.cell_height(),
499                self.cell_renderer.window_padding(),
500            );
501
502            // Update custom shader renderer dimensions
503            if let Some(ref mut custom_shader) = self.custom_shader_renderer {
504                custom_shader.resize(self.cell_renderer.device(), new_size.width, new_size.height);
505                // Sync cell dimensions for cursor position calculation
506                custom_shader.update_cell_dimensions(
507                    self.cell_renderer.cell_width(),
508                    self.cell_renderer.cell_height(),
509                    self.cell_renderer.window_padding(),
510                );
511            }
512
513            // Update cursor shader renderer dimensions
514            if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
515                cursor_shader.resize(self.cell_renderer.device(), new_size.width, new_size.height);
516                // Sync cell dimensions for cursor position calculation
517                cursor_shader.update_cell_dimensions(
518                    self.cell_renderer.cell_width(),
519                    self.cell_renderer.cell_height(),
520                    self.cell_renderer.window_padding(),
521                );
522            }
523
524            return result;
525        }
526
527        self.cell_renderer.grid_size()
528    }
529
530    /// Update scale factor and resize so the PTY grid matches the new DPI.
531    pub fn handle_scale_factor_change(
532        &mut self,
533        scale_factor: f64,
534        new_size: PhysicalSize<u32>,
535    ) -> (usize, usize) {
536        let old_scale = self.cell_renderer.scale_factor;
537        self.cell_renderer.update_scale_factor(scale_factor);
538        let new_scale = self.cell_renderer.scale_factor;
539
540        // Rescale physical pixel values when DPI changes
541        if old_scale > 0.0 && (old_scale - new_scale).abs() > f32::EPSILON {
542            // Rescale content_offset_y
543            let logical_offset_y = self.cell_renderer.content_offset_y() / old_scale;
544            let new_physical_offset_y = logical_offset_y * new_scale;
545            self.cell_renderer
546                .set_content_offset_y(new_physical_offset_y);
547            self.graphics_renderer
548                .set_content_offset_y(new_physical_offset_y);
549            if let Some(ref mut cs) = self.custom_shader_renderer {
550                cs.set_content_offset_y(new_physical_offset_y);
551            }
552            if let Some(ref mut cs) = self.cursor_shader_renderer {
553                cs.set_content_offset_y(new_physical_offset_y);
554            }
555
556            // Rescale content_offset_x
557            let logical_offset_x = self.cell_renderer.content_offset_x() / old_scale;
558            let new_physical_offset_x = logical_offset_x * new_scale;
559            self.cell_renderer
560                .set_content_offset_x(new_physical_offset_x);
561            self.graphics_renderer
562                .set_content_offset_x(new_physical_offset_x);
563            if let Some(ref mut cs) = self.custom_shader_renderer {
564                cs.set_content_offset_x(new_physical_offset_x);
565            }
566            if let Some(ref mut cs) = self.cursor_shader_renderer {
567                cs.set_content_offset_x(new_physical_offset_x);
568            }
569
570            // Rescale content_inset_bottom
571            let logical_inset_bottom = self.cell_renderer.content_inset_bottom() / old_scale;
572            let new_physical_inset_bottom = logical_inset_bottom * new_scale;
573            self.cell_renderer
574                .set_content_inset_bottom(new_physical_inset_bottom);
575
576            // Rescale egui_bottom_inset (status bar)
577            if self.cell_renderer.grid.egui_bottom_inset > 0.0 {
578                let logical_egui_bottom = self.cell_renderer.grid.egui_bottom_inset / old_scale;
579                self.cell_renderer.grid.egui_bottom_inset = logical_egui_bottom * new_scale;
580            }
581
582            // Rescale content_inset_right (AI Inspector panel)
583            if self.cell_renderer.grid.content_inset_right > 0.0 {
584                let logical_inset_right = self.cell_renderer.grid.content_inset_right / old_scale;
585                self.cell_renderer.grid.content_inset_right = logical_inset_right * new_scale;
586            }
587
588            // Rescale egui_right_inset
589            if self.cell_renderer.grid.egui_right_inset > 0.0 {
590                let logical_egui_right = self.cell_renderer.grid.egui_right_inset / old_scale;
591                self.cell_renderer.grid.egui_right_inset = logical_egui_right * new_scale;
592            }
593
594            // Rescale window_padding
595            let logical_padding = self.cell_renderer.window_padding() / old_scale;
596            let new_physical_padding = logical_padding * new_scale;
597            self.cell_renderer
598                .update_window_padding(new_physical_padding);
599
600            // Rescale scrollbar width
601            let logical_scrollbar = self.cell_renderer.scrollbar.width() / old_scale;
602            let new_physical_scrollbar = logical_scrollbar * new_scale;
603            self.cell_renderer.scrollbar.update_appearance(
604                new_physical_scrollbar,
605                self.cell_renderer.scrollbar.thumb_color(),
606                self.cell_renderer.scrollbar.track_color(),
607            );
608
609            // Sync new scale factor to shader renderers for cursor sizing
610            if let Some(ref mut cs) = self.custom_shader_renderer {
611                cs.set_scale_factor(new_scale);
612            }
613            if let Some(ref mut cs) = self.cursor_shader_renderer {
614                cs.set_scale_factor(new_scale);
615            }
616        }
617
618        self.resize(new_size)
619    }
620}
621
622// Layout and sizing accessors — simple getters/setters for grid geometry, padding,
623// and content offsets. Co-located here with resize/handle_scale_factor_change since
624// all of these deal with the spatial layout of the renderer.
625impl Renderer {
626    /// Get the current size
627    pub fn size(&self) -> PhysicalSize<u32> {
628        self.size
629    }
630
631    /// Get the current grid dimensions (columns, rows)
632    pub fn grid_size(&self) -> (usize, usize) {
633        self.cell_renderer.grid_size()
634    }
635
636    /// Get cell width in pixels
637    pub fn cell_width(&self) -> f32 {
638        self.cell_renderer.cell_width()
639    }
640
641    /// Get cell height in pixels
642    pub fn cell_height(&self) -> f32 {
643        self.cell_renderer.cell_height()
644    }
645
646    /// Get window padding in physical pixels (scaled by DPI)
647    pub fn window_padding(&self) -> f32 {
648        self.cell_renderer.window_padding()
649    }
650
651    /// Get the scrollbar width in physical pixels
652    pub fn scrollbar_width(&self) -> f32 {
653        self.cell_renderer.scrollbar.width()
654    }
655
656    /// Returns total non-terminal pixel overhead as (horizontal_px, vertical_px).
657    /// See `CellRenderer::chrome_overhead` for details.
658    pub fn chrome_overhead(&self) -> (f32, f32) {
659        self.cell_renderer.chrome_overhead()
660    }
661
662    /// Get the vertical content offset in physical pixels (e.g., tab bar height scaled by DPI)
663    pub fn content_offset_y(&self) -> f32 {
664        self.cell_renderer.content_offset_y()
665    }
666
667    /// Get the display scale factor (e.g., 2.0 on Retina displays)
668    pub fn scale_factor(&self) -> f32 {
669        self.cell_renderer.scale_factor
670    }
671
672    /// Set the vertical content offset (e.g., tab bar height) in logical pixels.
673    /// The offset is scaled by the display scale factor to physical pixels internally,
674    /// since the cell renderer works in physical pixel coordinates while egui (tab bar)
675    /// uses logical pixels.
676    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
677    pub fn set_content_offset_y(&mut self, logical_offset: f32) -> Option<(usize, usize)> {
678        // Scale from logical pixels (egui/config) to physical pixels (wgpu surface)
679        let physical_offset = logical_offset * self.cell_renderer.scale_factor;
680        let result = self.cell_renderer.set_content_offset_y(physical_offset);
681        // Always update graphics renderer offset, even if grid size didn't change
682        self.graphics_renderer.set_content_offset_y(physical_offset);
683        // Update custom shader renderer content offset
684        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
685            custom_shader.set_content_offset_y(physical_offset);
686        }
687        // Update cursor shader renderer content offset
688        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
689            cursor_shader.set_content_offset_y(physical_offset);
690        }
691        if result.is_some() {
692            self.dirty = true;
693        }
694        result
695    }
696
697    /// Get the horizontal content offset in physical pixels
698    pub fn content_offset_x(&self) -> f32 {
699        self.cell_renderer.content_offset_x()
700    }
701
702    /// Set the horizontal content offset (e.g., tab bar on left) in logical pixels.
703    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
704    pub fn set_content_offset_x(&mut self, logical_offset: f32) -> Option<(usize, usize)> {
705        let physical_offset = logical_offset * self.cell_renderer.scale_factor;
706        let result = self.cell_renderer.set_content_offset_x(physical_offset);
707        self.graphics_renderer.set_content_offset_x(physical_offset);
708        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
709            custom_shader.set_content_offset_x(physical_offset);
710        }
711        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
712            cursor_shader.set_content_offset_x(physical_offset);
713        }
714        if result.is_some() {
715            self.dirty = true;
716        }
717        result
718    }
719
720    /// Get the bottom content inset in physical pixels
721    pub fn content_inset_bottom(&self) -> f32 {
722        self.cell_renderer.content_inset_bottom()
723    }
724
725    /// Get the right content inset in physical pixels
726    pub fn content_inset_right(&self) -> f32 {
727        self.cell_renderer.content_inset_right()
728    }
729
730    /// Set the bottom content inset (e.g., tab bar at bottom) in logical pixels.
731    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
732    pub fn set_content_inset_bottom(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
733        let physical_inset = logical_inset * self.cell_renderer.scale_factor;
734        let result = self.cell_renderer.set_content_inset_bottom(physical_inset);
735        if result.is_some() {
736            self.dirty = true;
737            // Invalidate the scrollbar cache — the track height depends on
738            // the bottom inset, so the scrollbar must be repositioned.
739            self.last_scrollbar_state = (usize::MAX, 0, 0, 0, 0, 0, 0, 0, 0, 0);
740        }
741        result
742    }
743
744    /// Set the right content inset (e.g., AI Inspector panel) in logical pixels.
745    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
746    pub fn set_content_inset_right(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
747        let physical_inset = logical_inset * self.cell_renderer.scale_factor;
748        let result = self.cell_renderer.set_content_inset_right(physical_inset);
749
750        // Also update custom shader renderer to exclude panel area from effects
751        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
752            custom_shader.set_content_inset_right(physical_inset);
753        }
754        // Also update cursor shader renderer
755        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
756            cursor_shader.set_content_inset_right(physical_inset);
757        }
758
759        if result.is_some() {
760            self.dirty = true;
761            // Invalidate the scrollbar cache so the next update_scrollbar()
762            // repositions the scrollbar at the new right inset. Without this,
763            // the cache guard sees the same (scroll_offset, visible_lines,
764            // total_lines) tuple and skips the GPU upload, leaving the
765            // scrollbar stuck at the old position.
766            self.last_scrollbar_state = (usize::MAX, 0, 0, 0, 0, 0, 0, 0, 0, 0);
767        }
768        result
769    }
770
771    /// Set the additional bottom inset from egui panels (status bar, tmux bar).
772    ///
773    /// This inset reduces the terminal grid height so content does not render
774    /// behind the status bar. Also affects scrollbar bounds.
775    /// Returns `Some((cols, rows))` if the grid was resized.
776    pub fn set_egui_bottom_inset(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
777        let physical_inset = logical_inset * self.cell_renderer.scale_factor;
778        if (self.cell_renderer.grid.egui_bottom_inset - physical_inset).abs() > f32::EPSILON {
779            self.cell_renderer.grid.egui_bottom_inset = physical_inset;
780            let (w, h) = (
781                self.cell_renderer.config.width,
782                self.cell_renderer.config.height,
783            );
784            return Some(self.cell_renderer.resize(w, h));
785        }
786        None
787    }
788
789    /// Set the additional right inset from egui panels (AI Inspector).
790    ///
791    /// This inset is added to `content_inset_right` for scrollbar bounds only.
792    /// egui panels already claim space before wgpu rendering, so this doesn't
793    /// affect the terminal grid sizing.
794    pub fn set_egui_right_inset(&mut self, logical_inset: f32) {
795        let physical_inset = logical_inset * self.cell_renderer.scale_factor;
796        self.cell_renderer.grid.egui_right_inset = physical_inset;
797    }
798}