par-term-render 0.7.1

GPU-accelerated rendering engine for par-term terminal emulator
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
use crate::cell_renderer::Cell;
use anyhow::Result;
use par_term_config::SeparatorMark;
use par_term_config::color_u8_to_f32;

use super::Renderer;

// Dirty flag, debug overlay, surface configuration, vsync, font quality, and
// scrollbar hit-test accessors. Co-located here with the cell/cursor/scrollbar
// update methods since they all deal with renderer operational state.
impl Renderer {
    /// Check if the renderer needs to be redrawn
    pub fn is_dirty(&self) -> bool {
        self.dirty
    }

    /// Mark the renderer as dirty, forcing a redraw on next render call
    pub fn mark_dirty(&mut self) {
        self.dirty = true;
    }

    /// Set debug overlay text to be rendered
    pub fn render_debug_overlay(&mut self, text: &str) {
        self.debug_text = Some(text.to_string());
        self.dirty = true; // Mark dirty to ensure debug overlay renders
    }

    /// Reconfigure the surface (call when surface becomes outdated or lost)
    /// This typically happens when dragging the window between displays
    pub fn reconfigure_surface(&mut self) {
        self.cell_renderer.reconfigure_surface();
        self.dirty = true;
    }

    /// Check if a vsync mode is supported
    pub fn is_vsync_mode_supported(&self, mode: par_term_config::VsyncMode) -> bool {
        self.cell_renderer.is_vsync_mode_supported(mode)
    }

    /// Update the vsync mode. Returns the actual mode applied (may differ if requested mode unsupported).
    /// Also returns whether the mode was changed.
    pub fn update_vsync_mode(
        &mut self,
        mode: par_term_config::VsyncMode,
    ) -> (par_term_config::VsyncMode, bool) {
        let result = self.cell_renderer.update_vsync_mode(mode);
        if result.1 {
            self.dirty = true;
        }
        result
    }

    /// Get the current vsync mode
    pub fn current_vsync_mode(&self) -> par_term_config::VsyncMode {
        self.cell_renderer.current_vsync_mode()
    }

    /// Clear the glyph cache to force re-rasterization
    /// Useful after display changes where font rendering may differ
    pub fn clear_glyph_cache(&mut self) {
        self.cell_renderer.clear_glyph_cache();
        self.dirty = true;
    }

    /// Update font anti-aliasing setting
    /// Returns true if the setting changed (requiring glyph cache clear)
    pub fn update_font_antialias(&mut self, enabled: bool) -> bool {
        let changed = self.cell_renderer.update_font_antialias(enabled);
        if changed {
            self.dirty = true;
        }
        changed
    }

    /// Update font hinting setting
    /// Returns true if the setting changed (requiring glyph cache clear)
    pub fn update_font_hinting(&mut self, enabled: bool) -> bool {
        let changed = self.cell_renderer.update_font_hinting(enabled);
        if changed {
            self.dirty = true;
        }
        changed
    }

    /// Update thin strokes mode
    /// Returns true if the setting changed (requiring glyph cache clear)
    pub fn update_font_thin_strokes(&mut self, mode: par_term_config::ThinStrokesMode) -> bool {
        let changed = self.cell_renderer.update_font_thin_strokes(mode);
        if changed {
            self.dirty = true;
        }
        changed
    }

    /// Update minimum contrast value
    /// Returns true if the setting changed (requiring redraw)
    pub fn update_minimum_contrast(&mut self, value: f32) -> bool {
        let changed = self.cell_renderer.update_minimum_contrast(value);
        if changed {
            self.dirty = true;
        }
        changed
    }

    /// Check if a point (in pixel coordinates) is within the scrollbar bounds
    ///
    /// # Arguments
    /// * `x` - X coordinate in pixels (from left edge)
    /// * `y` - Y coordinate in pixels (from top edge)
    pub fn scrollbar_contains_point(&self, x: f32, y: f32) -> bool {
        self.cell_renderer.scrollbar_contains_point(x, y)
    }

    /// Get the scrollbar thumb bounds (top Y, height) in pixels
    pub fn scrollbar_thumb_bounds(&self) -> Option<(f32, f32)> {
        self.cell_renderer.scrollbar_thumb_bounds()
    }

    /// Check if an X coordinate is within the scrollbar track
    pub fn scrollbar_track_contains_x(&self, x: f32) -> bool {
        self.cell_renderer.scrollbar_track_contains_x(x)
    }

    /// Convert a mouse Y position to a scroll offset
    ///
    /// # Arguments
    /// * `mouse_y` - Mouse Y coordinate in pixels (from top edge)
    ///
    /// # Returns
    /// The scroll offset corresponding to the mouse position, or None if scrollbar is not visible
    pub fn scrollbar_mouse_y_to_scroll_offset(&self, mouse_y: f32) -> Option<usize> {
        self.cell_renderer
            .scrollbar_mouse_y_to_scroll_offset(mouse_y)
    }

    /// Find a scrollbar mark at the given mouse position for tooltip display.
    ///
    /// # Arguments
    /// * `mouse_x` - Mouse X coordinate in pixels
    /// * `mouse_y` - Mouse Y coordinate in pixels
    /// * `tolerance` - Maximum distance in pixels to match a mark
    ///
    /// # Returns
    /// The mark at that position, or None if no mark is within tolerance
    pub fn scrollbar_mark_at_position(
        &self,
        mouse_x: f32,
        mouse_y: f32,
        tolerance: f32,
    ) -> Option<&par_term_config::ScrollbackMark> {
        self.cell_renderer
            .scrollbar_mark_at_position(mouse_x, mouse_y, tolerance)
    }
}

impl Renderer {
    pub fn update_cells(&mut self, cells: &[Cell]) {
        if self.cell_renderer.update_cells(cells) {
            self.dirty = true;
        }
    }

    /// Clear all cells in the renderer.
    /// Call this when switching tabs to ensure a clean slate.
    pub fn clear_all_cells(&mut self) {
        self.cell_renderer.clear_all_cells();
        self.dirty = true;
    }

    /// Update cursor position and style for geometric rendering
    pub fn update_cursor(
        &mut self,
        position: (usize, usize),
        opacity: f32,
        style: par_term_emu_core_rust::cursor::CursorStyle,
    ) {
        if self.cell_renderer.update_cursor(position, opacity, style) {
            self.dirty = true;
        }
    }

    /// Clear cursor (hide it)
    pub fn clear_cursor(&mut self) {
        if self.cell_renderer.clear_cursor() {
            self.dirty = true;
        }
    }

    /// Update scrollbar state.
    pub fn update_scrollbar(
        &mut self,
        scroll_offset: usize,
        visible_lines: usize,
        total_lines: usize,
        marks: &[par_term_config::ScrollbackMark],
    ) {
        let new_state = (
            scroll_offset,
            visible_lines,
            total_lines,
            marks.len(),
            self.cell_renderer.config.width,
            self.cell_renderer.config.height,
            // No pane viewport in single-pane path — use zeros
            0,
            0,
            0,
            0,
        );
        if new_state == self.last_scrollbar_state {
            return;
        }
        self.last_scrollbar_state = new_state;
        self.cell_renderer
            .update_scrollbar(scroll_offset, visible_lines, total_lines, marks);
        self.dirty = true;
    }

    /// Set the visual bell flash intensity
    ///
    /// # Arguments
    /// * `intensity` - Flash intensity from 0.0 (no flash) to 1.0 (full white flash)
    pub fn set_visual_bell_intensity(&mut self, intensity: f32) {
        self.cell_renderer.set_visual_bell_intensity(intensity);
        if intensity > 0.0 {
            self.dirty = true; // Mark dirty when flash is active
        }
    }

    /// Set the visual bell flash color (RGB, 0.0-1.0 per channel).
    pub fn set_visual_bell_color(&mut self, color: [f32; 3]) {
        self.cell_renderer.set_visual_bell_color(color);
    }

    /// Update window opacity in real-time
    pub fn update_opacity(&mut self, opacity: f32) {
        self.cell_renderer.update_opacity(opacity);

        // Propagate to custom shader renderer if present
        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
            custom_shader.set_opacity(opacity);
        }

        // Propagate to cursor shader renderer if present
        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
            cursor_shader.set_opacity(opacity);
        }

        self.dirty = true;
    }

    /// Update cursor color for cell rendering
    pub fn update_cursor_color(&mut self, color: [u8; 3]) {
        self.cell_renderer.update_cursor_color(color);
        self.dirty = true;
    }

    /// Update cursor text color (color of text under block cursor)
    pub fn update_cursor_text_color(&mut self, color: Option<[u8; 3]>) {
        self.cell_renderer.update_cursor_text_color(color);
        self.dirty = true;
    }

    /// Set whether cursor should be hidden when cursor shader is active
    pub fn set_cursor_hidden_for_shader(&mut self, hidden: bool) {
        if self.cell_renderer.set_cursor_hidden_for_shader(hidden) {
            self.dirty = true;
        }
    }

    /// Set window focus state (affects unfocused cursor rendering)
    pub fn set_focused(&mut self, focused: bool) {
        if self.cell_renderer.set_focused(focused) {
            self.dirty = true;
        }
    }

    /// Update cursor guide settings
    pub fn update_cursor_guide(&mut self, enabled: bool, color: [u8; 4]) {
        self.cell_renderer.update_cursor_guide(enabled, color);
        self.dirty = true;
    }

    /// Update cursor shadow settings.
    /// Offset and blur are in logical pixels and will be scaled to physical pixels internally.
    pub fn update_cursor_shadow(
        &mut self,
        enabled: bool,
        color: [u8; 4],
        offset: [f32; 2],
        blur: f32,
    ) {
        let scale = self.cell_renderer.scale_factor;
        let physical_offset = [offset[0] * scale, offset[1] * scale];
        let physical_blur = blur * scale;
        self.cell_renderer
            .update_cursor_shadow(enabled, color, physical_offset, physical_blur);
        self.dirty = true;
    }

    /// Update cursor boost settings
    pub fn update_cursor_boost(&mut self, intensity: f32, color: [u8; 3]) {
        self.cell_renderer.update_cursor_boost(intensity, color);
        self.dirty = true;
    }

    /// Update unfocused cursor style
    pub fn update_unfocused_cursor_style(&mut self, style: par_term_config::UnfocusedCursorStyle) {
        self.cell_renderer.update_unfocused_cursor_style(style);
        self.dirty = true;
    }

    /// Update command separator settings from config.
    /// Thickness is in logical pixels and will be scaled to physical pixels internally.
    pub fn update_command_separator(
        &mut self,
        enabled: bool,
        logical_thickness: f32,
        opacity: f32,
        exit_color: bool,
        color: [u8; 3],
    ) {
        let physical_thickness = logical_thickness * self.cell_renderer.scale_factor;
        self.cell_renderer.update_command_separator(
            enabled,
            physical_thickness,
            opacity,
            exit_color,
            color,
        );
        self.dirty = true;
    }

    /// Set the visible separator marks for the current frame (single-pane path)
    pub fn set_separator_marks(&mut self, marks: Vec<SeparatorMark>) {
        if self.cell_renderer.set_separator_marks(marks) {
            self.dirty = true;
        }
    }

    /// Set gutter indicator data for the current frame (single-pane path).
    pub fn set_gutter_indicators(&mut self, indicators: Vec<(usize, [f32; 4])>) {
        self.cell_renderer.set_gutter_indicators(indicators);
        self.dirty = true;
    }

    /// Set whether transparency affects only default background cells.
    /// When true, non-default (colored) backgrounds remain opaque for readability.
    pub fn set_transparency_affects_only_default_background(&mut self, value: bool) {
        self.cell_renderer
            .set_transparency_affects_only_default_background(value);
        self.dirty = true;
    }

    /// Set whether text should always be rendered at full opacity.
    /// When true, text remains opaque regardless of window transparency settings.
    pub fn set_keep_text_opaque(&mut self, value: bool) {
        self.cell_renderer.set_keep_text_opaque(value);

        // Also propagate to custom shader renderer if present
        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
            custom_shader.set_keep_text_opaque(value);
        }

        // And to cursor shader renderer if present
        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
            cursor_shader.set_keep_text_opaque(value);
        }

        self.dirty = true;
    }

    pub fn set_link_underline_style(&mut self, style: par_term_config::LinkUnderlineStyle) {
        self.cell_renderer.set_link_underline_style(style);
        self.dirty = true;
    }

    /// Set whether cursor shader should be disabled due to alt screen being active
    ///
    /// When alt screen is active (e.g., vim, htop, less), cursor shader effects
    /// are disabled since TUI applications typically have their own cursor handling.
    pub fn set_cursor_shader_disabled_for_alt_screen(&mut self, disabled: bool) {
        if self.cursor_shader_disabled_for_alt_screen != disabled {
            log::debug!("[cursor-shader] Alt-screen disable set to {}", disabled);
            self.cursor_shader_disabled_for_alt_screen = disabled;
        } else {
            self.cursor_shader_disabled_for_alt_screen = disabled;
        }
    }

    /// Update window padding in real-time without full renderer rebuild.
    /// Accepts logical pixels (from config); scales to physical pixels internally.
    /// Returns Some((cols, rows)) if grid size changed and terminal needs resize.
    pub fn update_window_padding(&mut self, logical_padding: f32) -> Option<(usize, usize)> {
        let physical_padding = logical_padding * self.cell_renderer.scale_factor;
        let result = self.cell_renderer.update_window_padding(physical_padding);
        // Update graphics renderer padding
        self.graphics_renderer.update_cell_dimensions(
            self.cell_renderer.cell_width(),
            self.cell_renderer.cell_height(),
            physical_padding,
        );
        // Update custom shader renderer padding
        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
            custom_shader.update_cell_dimensions(
                self.cell_renderer.cell_width(),
                self.cell_renderer.cell_height(),
                physical_padding,
            );
        }
        // Update cursor shader renderer padding
        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
            cursor_shader.update_cell_dimensions(
                self.cell_renderer.cell_width(),
                self.cell_renderer.cell_height(),
                physical_padding,
            );
        }
        self.dirty = true;
        result
    }

    /// Enable/disable background image and reload if needed
    pub fn set_background_image_enabled(
        &mut self,
        enabled: bool,
        path: Option<&str>,
        mode: par_term_config::BackgroundImageMode,
        opacity: f32,
    ) {
        let path = if enabled { path } else { None };
        self.cell_renderer.set_background_image(path, mode, opacity);

        // Sync background texture to custom shader if it's using background as channel0
        self.sync_background_texture_to_shader();

        self.dirty = true;
    }

    /// Set background based on mode (Default, Color, or Image).
    ///
    /// This unified method handles all background types and syncs with shaders.
    pub fn set_background(
        &mut self,
        mode: par_term_config::BackgroundMode,
        color: [u8; 3],
        image_path: Option<&str>,
        image_mode: par_term_config::BackgroundImageMode,
        image_opacity: f32,
        image_enabled: bool,
    ) {
        self.cell_renderer.set_background(
            mode,
            color,
            image_path,
            image_mode,
            image_opacity,
            image_enabled,
        );

        // Sync background texture to custom shader if it's using background as channel0
        self.sync_background_texture_to_shader();

        // Sync background to shaders for proper compositing
        let is_solid_color = matches!(mode, par_term_config::BackgroundMode::Color);
        let is_image_mode = matches!(mode, par_term_config::BackgroundMode::Image);
        let normalized_color = color_u8_to_f32(color);

        // Sync to cursor shader
        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
            // When background shader is enabled and chained into cursor shader,
            // don't give cursor shader its own background - background shader handles it
            let has_background_shader = self.custom_shader_renderer.is_some();

            if has_background_shader {
                // Background shader handles the background, cursor shader just passes through
                cursor_shader.set_background_color([0.0, 0.0, 0.0], false);
                cursor_shader.set_background_texture(self.cell_renderer.device(), None);
                cursor_shader.update_use_background_as_channel0(self.cell_renderer.device(), false);
            } else {
                cursor_shader.set_background_color(normalized_color, is_solid_color);

                // For image mode, pass background image as iChannel0
                if is_image_mode && image_enabled {
                    let bg_texture = self.cell_renderer.get_background_as_channel_texture();
                    cursor_shader.set_background_texture(self.cell_renderer.device(), bg_texture);
                    cursor_shader
                        .update_use_background_as_channel0(self.cell_renderer.device(), true);
                } else {
                    // Clear background texture when not in image mode
                    cursor_shader.set_background_texture(self.cell_renderer.device(), None);
                    cursor_shader
                        .update_use_background_as_channel0(self.cell_renderer.device(), false);
                }
            }
        }

        // Sync to custom shader
        // Note: We don't pass is_solid_color=true to custom shaders because
        // that would replace the shader output with a solid color, making the
        // shader invisible. Custom shaders handle their own background.
        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
            custom_shader.set_background_color(normalized_color, false);
        }

        self.dirty = true;
    }

    /// Update scrollbar appearance in real-time.
    /// Width is in logical pixels and will be scaled to physical pixels internally.
    pub fn update_scrollbar_appearance(
        &mut self,
        logical_width: f32,
        thumb_color: [f32; 4],
        track_color: [f32; 4],
    ) {
        let physical_width = logical_width * self.cell_renderer.scale_factor;
        self.cell_renderer
            .update_scrollbar_appearance(physical_width, thumb_color, track_color);
        // Force the next update_scrollbar() call to re-upload GPU uniforms with new colors,
        // since uniform upload is normally skipped when scroll state hasn't changed.
        self.last_scrollbar_state = (usize::MAX, 0, 0, 0, 0, 0, 0, 0, 0, 0);
        self.dirty = true;
    }

    /// Update scrollbar position (left/right) in real-time
    pub fn update_scrollbar_position(&mut self, position: &str) {
        self.cell_renderer.update_scrollbar_position(position);
        self.dirty = true;
    }

    /// Update background image opacity in real-time
    pub fn update_background_image_opacity(&mut self, opacity: f32) {
        self.cell_renderer.update_background_image_opacity(opacity);
        self.dirty = true;
    }

    /// Load a per-pane background image into the texture cache.
    /// Delegates to CellRenderer::load_pane_background.
    pub fn load_pane_background(&mut self, path: &str) -> Result<bool, crate::error::RenderError> {
        self.cell_renderer.load_pane_background(path)
    }

    /// Update inline image scaling mode (nearest vs linear filtering).
    ///
    /// Recreates the GPU sampler and clears the texture cache so images
    /// are re-rendered with the new filter mode.
    pub fn update_image_scaling_mode(&mut self, scaling_mode: par_term_config::ImageScalingMode) {
        self.graphics_renderer
            .update_scaling_mode(self.cell_renderer.device(), scaling_mode);
        self.dirty = true;
    }

    /// Update whether inline images preserve their aspect ratio.
    pub fn update_image_preserve_aspect_ratio(&mut self, preserve: bool) {
        self.graphics_renderer.set_preserve_aspect_ratio(preserve);
        self.dirty = true;
    }

    /// Check if animation requires continuous rendering
    ///
    /// Returns true if shader animation is enabled or a cursor trail animation
    /// might still be in progress.
    pub fn needs_continuous_render(&self) -> bool {
        let custom_needs = self
            .custom_shader_renderer
            .as_ref()
            .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
        let cursor_needs = self
            .cursor_shader_renderer
            .as_ref()
            .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
        custom_needs || cursor_needs
    }
}