Skip to main content

par_term_render/renderer/
mod.rs

1// ARC-009: `Renderer` is split across sibling modules under `renderer/`. This
2// file holds the struct definition, its re-exports, and `Renderer::new`.
3//
4//   types.rs      — Pane/divider/title render-input types and separator-mark mapping
5//   resize.rs     — Surface resize and DPI scale-factor changes
6//   layout.rs     — Grid geometry, padding, and content offset/inset accessors
7//   screenshot.rs — Offscreen capture: `take_screenshot` and its shader chain
8//
9// The remaining behaviour lives in rendering.rs, render_passes.rs, egui_render.rs,
10// graphics.rs, shaders/, params.rs, and state.rs.
11//
12// Tracking: Issue ARC-009 in AUDIT.md.
13
14use crate::cell_renderer::{CellRenderer, CellRendererConfig};
15use crate::custom_shader_renderer::CustomShaderRenderer;
16use crate::graphics_renderer::GraphicsRenderer;
17use anyhow::Result;
18use winit::dpi::PhysicalSize;
19
20mod egui_render;
21pub mod graphics;
22mod layout;
23pub mod params;
24
25mod render_passes;
26mod rendering;
27mod resize;
28mod screenshot;
29pub mod shaders;
30mod state;
31mod types;
32
33// Re-export SeparatorMark from par-term-config
34pub use par_term_config::SeparatorMark;
35pub use params::RendererParams;
36pub use rendering::{PaneCaptureParams, SplitPanesRenderParams};
37pub use types::{
38    DividerRenderInfo, PaneDividerSettings, PaneRenderInfo, PaneTitleInfo,
39    compute_visible_separator_marks,
40};
41// Intentionally narrower than the `pub use` above: this helper is a crate-internal
42// hot-path variant and must not become part of the published API surface.
43pub(crate) use types::fill_visible_separator_marks;
44
45/// Renderer for the terminal using custom wgpu cell renderer
46pub struct Renderer {
47    // Cell renderer (owns the scrollbar)
48    pub(crate) cell_renderer: CellRenderer,
49
50    // Graphics renderer for sixel images
51    pub(crate) graphics_renderer: GraphicsRenderer,
52
53    // Current sixel graphics to render.
54    // Note: screen_row is isize to allow negative values for graphics scrolled off top
55    pub(crate) sixel_graphics: Vec<crate::graphics_renderer::GraphicRenderInfo>,
56
57    // egui renderer for settings UI
58    pub(crate) egui_renderer: egui_wgpu::Renderer,
59
60    // Custom shader renderer for post-processing effects (background shader)
61    pub(crate) custom_shader_renderer: Option<CustomShaderRenderer>,
62    // Track current shader path to detect changes
63    pub(crate) custom_shader_path: Option<String>,
64    // Background shader failure from construction, awaiting collection by the
65    // frontend's shader-error sink. Read once via `take_startup_shader_errors`.
66    pub(crate) startup_shader_error: Option<String>,
67
68    // Cursor shader renderer for cursor-specific effects (separate from background shader)
69    pub(crate) cursor_shader_renderer: Option<CustomShaderRenderer>,
70    // Track current cursor shader path to detect changes
71    pub(crate) cursor_shader_path: Option<String>,
72    // Cursor shader failure from construction; see `startup_shader_error`.
73    pub(crate) startup_cursor_shader_error: Option<String>,
74
75    // Cached for convenience
76    pub(crate) size: PhysicalSize<u32>,
77
78    // Dirty flag for optimization - only render when content has changed
79    pub(crate) dirty: bool,
80
81    // Cached scrollbar state to avoid redundant GPU uploads.
82    // Includes scroll position, line counts, marks, window size, AND pane viewport
83    // bounds so that pane splits/resizes correctly trigger a scrollbar geometry update.
84    pub(crate) last_scrollbar_state: (usize, usize, usize, usize, u32, u32, u32, u32, u32, u32),
85
86    // Skip cursor shader when alt screen is active (TUI apps like vim, htop)
87    pub(crate) cursor_shader_disabled_for_alt_screen: bool,
88
89    // Debug overlay text
90    pub(crate) debug_text: Option<String>,
91
92    // Scratch buffer for divider instances, reused each frame to avoid
93    // per-call heap allocations in `render_dividers`.
94    pub(crate) scratch_divider_instances: Vec<crate::cell_renderer::BackgroundInstance>,
95}
96
97impl Renderer {
98    /// Create a new renderer
99    pub async fn new(params: RendererParams<'_>) -> Result<Self> {
100        let window = params.window;
101        let font_family = params.font_family;
102        let font_family_bold = params.font_family_bold;
103        let font_family_italic = params.font_family_italic;
104        let font_family_bold_italic = params.font_family_bold_italic;
105        let font_ranges = params.font_ranges;
106        let font_size = params.font_size;
107        let line_spacing = params.line_spacing;
108        let char_spacing = params.char_spacing;
109        let scrollbar_position = params.scrollbar_position;
110        let scrollbar_thumb_color = params.scrollbar_thumb_color;
111        let scrollbar_track_color = params.scrollbar_track_color;
112        let enable_text_shaping = params.enable_text_shaping;
113        let enable_ligatures = params.enable_ligatures;
114        let enable_kerning = params.enable_kerning;
115        let font_antialias = params.font_antialias;
116        let font_hinting = params.font_hinting;
117        let font_thin_strokes = params.font_thin_strokes;
118        let minimum_contrast = params.minimum_contrast;
119        let vsync_mode = params.vsync_mode;
120        let power_preference = params.power_preference;
121        let window_opacity = params.window_opacity;
122        let background_color = params.background_color;
123        let background_image_path = params.background_image_path;
124        let background_image_enabled = params.background_image_enabled;
125        let background_image_mode = params.background_image_mode;
126        let background_image_opacity = params.background_image_opacity;
127        let custom_shader_path = params.custom_shader_path;
128        let custom_shader_enabled = params.custom_shader_enabled;
129        let custom_shader_animation = params.custom_shader_animation;
130        let custom_shader_animation_speed = params.custom_shader_animation_speed;
131        let custom_shader_full_content = params.custom_shader_full_content;
132        let custom_shader_brightness = params.custom_shader_brightness;
133        let custom_shader_channel_paths = params.custom_shader_channel_paths;
134        let custom_shader_cubemap_path = params.custom_shader_cubemap_path;
135        let custom_shader_custom_uniforms = params.custom_shader_custom_uniforms;
136        let use_background_as_channel0 = params.use_background_as_channel0;
137        let background_channel0_blend_mode = params.background_channel0_blend_mode;
138        let custom_shader_auto_dim_under_text = params.custom_shader_auto_dim_under_text;
139        let custom_shader_auto_dim_strength = params.custom_shader_auto_dim_strength;
140        let image_scaling_mode = params.image_scaling_mode;
141        let image_preserve_aspect_ratio = params.image_preserve_aspect_ratio;
142        let cursor_shader_path = params.cursor_shader_path;
143        let cursor_shader_enabled = params.cursor_shader_enabled;
144        let cursor_shader_animation = params.cursor_shader_animation;
145        let cursor_shader_animation_speed = params.cursor_shader_animation_speed;
146
147        let size = window.inner_size();
148        let scale_factor = window.scale_factor();
149
150        // Standard DPI for the platform
151        // macOS typically uses 72 DPI for points, Windows and most Linux use 96 DPI
152        let platform_dpi = if cfg!(target_os = "macos") {
153            72.0
154        } else {
155            96.0
156        };
157
158        // Convert font size from points to pixels for cell size calculation, honoring DPI and scale
159        let base_font_pixels = font_size * platform_dpi / 72.0;
160        let font_size_pixels = (base_font_pixels * scale_factor as f32).max(1.0);
161
162        // Font lookup for the metrics that determine `cols`/`rows` below.  The
163        // manager is then handed to `CellRenderer::new`; building a second one
164        // there would re-enumerate every system font on each renderer rebuild.
165        let font_manager = par_term_fonts::font_manager::FontManager::new(
166            font_family,
167            font_family_bold,
168            font_family_italic,
169            font_family_bold_italic,
170            font_ranges,
171        )?;
172
173        let (font_ascent, font_descent, font_leading, char_advance) = {
174            let primary_font = font_manager
175                .get_font(0)
176                .expect("Primary font at index 0 must exist after FontManager initialization");
177            let metrics = primary_font.metrics(&[]);
178            let scale = font_size_pixels / metrics.units_per_em as f32;
179
180            // Get advance width of a standard character ('m' is common for monospace width)
181            let glyph_id = primary_font.charmap().map('m');
182            let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
183
184            (
185                metrics.ascent * scale,
186                metrics.descent * scale,
187                metrics.leading * scale,
188                advance,
189            )
190        };
191
192        // Use font metrics for cell height if line_spacing is 1.0
193        // Natural line height = ascent + descent + leading
194        let natural_line_height = font_ascent + font_descent + font_leading;
195        let char_height = (natural_line_height * line_spacing).max(1.0).round();
196
197        // Scale logical pixel values (config) to physical pixels (wgpu surface)
198        let scale = scale_factor as f32;
199        let window_padding = params.window_padding * scale;
200        let scrollbar_width = params.scrollbar_width * scale;
201
202        // Calculate available space after padding and scrollbar
203        let available_width = (size.width as f32 - window_padding * 2.0 - scrollbar_width).max(0.0);
204        let available_height = (size.height as f32 - window_padding * 2.0).max(0.0);
205
206        // Calculate terminal dimensions based on font size in pixels and spacing
207        let char_width = (char_advance * char_spacing).max(1.0).round(); // Configurable character width (rounded to integer pixels)
208        let cols = (available_width / char_width).max(1.0) as usize;
209        let rows = (available_height / char_height).max(1.0) as usize;
210
211        // Create cell renderer with font fallback support (owns scrollbar)
212        let bg_path = if background_image_enabled {
213            background_image_path
214        } else {
215            None
216        };
217        log::info!(
218            "Renderer::new: background_image_enabled={}, path={:?}",
219            background_image_enabled,
220            bg_path
221        );
222        let cell_renderer = CellRenderer::new(
223            window.clone(),
224            CellRendererConfig {
225                font_manager,
226                font_size,
227                cols,
228                rows,
229                window_padding,
230                line_spacing,
231                char_spacing,
232                scrollbar_position,
233                scrollbar_width,
234                scrollbar_thumb_color,
235                scrollbar_track_color,
236                enable_text_shaping,
237                enable_ligatures,
238                enable_kerning,
239                font_antialias,
240                font_hinting,
241                font_thin_strokes,
242                minimum_contrast,
243                vsync_mode,
244                power_preference,
245                window_opacity,
246                background_color,
247                background_image_path: bg_path,
248                background_image_mode,
249                background_image_opacity,
250            },
251        )
252        .await?;
253
254        // Create egui renderer for settings UI
255        let egui_renderer = egui_wgpu::Renderer::new(
256            cell_renderer.device(),
257            cell_renderer.surface_format(),
258            egui_wgpu::RendererOptions {
259                msaa_samples: 1,
260                depth_stencil_format: None,
261                dithering: false,
262                predictable_texture_filtering: false,
263            },
264        );
265
266        // Create graphics renderer for sixel images
267        let graphics_renderer = GraphicsRenderer::new(
268            cell_renderer.device(),
269            cell_renderer.surface_format(),
270            cell_renderer.cell_width(),
271            cell_renderer.cell_height(),
272            cell_renderer.window_padding(),
273            image_scaling_mode,
274            image_preserve_aspect_ratio,
275        )?;
276
277        // Create custom shader renderer if configured
278        let (mut custom_shader_renderer, initial_shader_path, startup_shader_error) =
279            shaders::init_custom_shader(
280                &cell_renderer,
281                shaders::CustomShaderInitParams {
282                    size_width: size.width,
283                    size_height: size.height,
284                    window_padding,
285                    path: custom_shader_path,
286                    enabled: custom_shader_enabled,
287                    animation: custom_shader_animation,
288                    animation_speed: custom_shader_animation_speed,
289                    window_opacity,
290                    full_content: custom_shader_full_content,
291                    brightness: custom_shader_brightness,
292                    channel_paths: custom_shader_channel_paths,
293                    cubemap_path: custom_shader_cubemap_path,
294                    custom_uniforms: custom_shader_custom_uniforms,
295                    use_background_as_channel0,
296                    background_channel0_blend_mode,
297                    auto_dim_under_text: custom_shader_auto_dim_under_text,
298                    auto_dim_strength: custom_shader_auto_dim_strength,
299                },
300            );
301
302        // Create cursor shader renderer if configured (separate from background shader)
303        let (mut cursor_shader_renderer, initial_cursor_shader_path, startup_cursor_shader_error) =
304            shaders::init_cursor_shader(
305                &cell_renderer,
306                shaders::CursorShaderInitParams {
307                    size_width: size.width,
308                    size_height: size.height,
309                    window_padding,
310                    path: cursor_shader_path,
311                    enabled: cursor_shader_enabled,
312                    animation: cursor_shader_animation,
313                    animation_speed: cursor_shader_animation_speed,
314                    window_opacity,
315                },
316            );
317
318        // Sync DPI scale factor to shader renderers for cursor sizing
319        if let Some(ref mut cs) = custom_shader_renderer {
320            cs.set_scale_factor(scale);
321        }
322        if let Some(ref mut cs) = cursor_shader_renderer {
323            cs.set_scale_factor(scale);
324        }
325
326        log::info!(
327            "[renderer] Renderer created: custom_shader_loaded={}, cursor_shader_loaded={}",
328            initial_shader_path.is_some(),
329            initial_cursor_shader_path.is_some()
330        );
331
332        Ok(Self {
333            cell_renderer,
334            graphics_renderer,
335            sixel_graphics: Vec::new(),
336            egui_renderer,
337            custom_shader_renderer,
338            custom_shader_path: initial_shader_path,
339            startup_shader_error,
340            cursor_shader_renderer,
341            cursor_shader_path: initial_cursor_shader_path,
342            startup_cursor_shader_error,
343            size,
344            dirty: true, // Start dirty to ensure initial render
345            last_scrollbar_state: (usize::MAX, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Force first update
346            cursor_shader_disabled_for_alt_screen: false,
347            debug_text: None,
348            scratch_divider_instances: Vec::new(),
349        })
350    }
351}