Skip to main content

par_term_render/renderer/
state.rs

1use crate::cell_renderer::Cell;
2use anyhow::Result;
3use par_term_config::SeparatorMark;
4use par_term_config::color_u8_to_f32;
5
6use super::Renderer;
7
8// Dirty flag, debug overlay, surface configuration, vsync, font quality, and
9// scrollbar hit-test accessors. Co-located here with the cell/cursor/scrollbar
10// update methods since they all deal with renderer operational state.
11impl Renderer {
12    /// Check if the renderer needs to be redrawn
13    pub fn is_dirty(&self) -> bool {
14        self.dirty
15    }
16
17    /// Mark the renderer as dirty, forcing a redraw on next render call
18    pub fn mark_dirty(&mut self) {
19        self.dirty = true;
20    }
21
22    /// Set debug overlay text to be rendered
23    pub fn render_debug_overlay(&mut self, text: &str) {
24        self.debug_text = Some(text.to_string());
25        self.dirty = true; // Mark dirty to ensure debug overlay renders
26    }
27
28    /// Reconfigure the surface (call when surface becomes outdated or lost)
29    /// This typically happens when dragging the window between displays
30    pub fn reconfigure_surface(&mut self) {
31        self.cell_renderer.reconfigure_surface();
32        self.dirty = true;
33    }
34
35    /// Reconfigure the surface after a display-configuration change (monitor
36    /// attach/detach/move): re-derives the config from fresh capabilities and
37    /// cycles the present mode, healing the strobe state a plain reconfigure
38    /// leaves in place. `width`/`height` are the window's current physical
39    /// extent.
40    pub fn reconfigure_after_display_change(&mut self, width: u32, height: u32) {
41        self.cell_renderer
42            .reconfigure_after_display_change(width, height);
43        self.dirty = true;
44    }
45
46    /// Check if a vsync mode is supported
47    pub fn is_vsync_mode_supported(&self, mode: par_term_config::VsyncMode) -> bool {
48        self.cell_renderer.is_vsync_mode_supported(mode)
49    }
50
51    /// Update the vsync mode. Returns the actual mode applied (may differ if requested mode unsupported).
52    /// Also returns whether the mode was changed.
53    pub fn update_vsync_mode(
54        &mut self,
55        mode: par_term_config::VsyncMode,
56    ) -> (par_term_config::VsyncMode, bool) {
57        let result = self.cell_renderer.update_vsync_mode(mode);
58        if result.1 {
59            self.dirty = true;
60        }
61        result
62    }
63
64    /// Get the current vsync mode
65    pub fn current_vsync_mode(&self) -> par_term_config::VsyncMode {
66        self.cell_renderer.current_vsync_mode()
67    }
68
69    /// Clear the glyph cache to force re-rasterization
70    /// Useful after display changes where font rendering may differ
71    pub fn clear_glyph_cache(&mut self) {
72        self.cell_renderer.clear_glyph_cache();
73        self.dirty = true;
74    }
75
76    /// Update font anti-aliasing setting
77    /// Returns true if the setting changed (requiring glyph cache clear)
78    pub fn update_font_antialias(&mut self, enabled: bool) -> bool {
79        let changed = self.cell_renderer.update_font_antialias(enabled);
80        if changed {
81            self.dirty = true;
82        }
83        changed
84    }
85
86    /// Update font hinting setting
87    /// Returns true if the setting changed (requiring glyph cache clear)
88    pub fn update_font_hinting(&mut self, enabled: bool) -> bool {
89        let changed = self.cell_renderer.update_font_hinting(enabled);
90        if changed {
91            self.dirty = true;
92        }
93        changed
94    }
95
96    /// Update thin strokes mode
97    /// Returns true if the setting changed (requiring glyph cache clear)
98    pub fn update_font_thin_strokes(&mut self, mode: par_term_config::ThinStrokesMode) -> bool {
99        let changed = self.cell_renderer.update_font_thin_strokes(mode);
100        if changed {
101            self.dirty = true;
102        }
103        changed
104    }
105
106    /// Update minimum contrast value
107    /// Returns true if the setting changed (requiring redraw)
108    pub fn update_minimum_contrast(&mut self, value: f32) -> bool {
109        let changed = self.cell_renderer.update_minimum_contrast(value);
110        if changed {
111            self.dirty = true;
112        }
113        changed
114    }
115
116    /// Check if a point (in pixel coordinates) is within the scrollbar bounds
117    ///
118    /// # Arguments
119    /// * `x` - X coordinate in pixels (from left edge)
120    /// * `y` - Y coordinate in pixels (from top edge)
121    pub fn scrollbar_contains_point(&self, x: f32, y: f32) -> bool {
122        self.cell_renderer.scrollbar_contains_point(x, y)
123    }
124
125    /// Get the scrollbar thumb bounds (top Y, height) in pixels
126    pub fn scrollbar_thumb_bounds(&self) -> Option<(f32, f32)> {
127        self.cell_renderer.scrollbar_thumb_bounds()
128    }
129
130    /// Check if an X coordinate is within the scrollbar track
131    pub fn scrollbar_track_contains_x(&self, x: f32) -> bool {
132        self.cell_renderer.scrollbar_track_contains_x(x)
133    }
134
135    /// Convert a mouse Y position to a scroll offset
136    ///
137    /// # Arguments
138    /// * `mouse_y` - Mouse Y coordinate in pixels (from top edge)
139    ///
140    /// # Returns
141    /// The scroll offset corresponding to the mouse position, or None if scrollbar is not visible
142    pub fn scrollbar_mouse_y_to_scroll_offset(&self, mouse_y: f32) -> Option<usize> {
143        self.cell_renderer
144            .scrollbar_mouse_y_to_scroll_offset(mouse_y)
145    }
146
147    /// Find a scrollbar mark at the given mouse position for tooltip display.
148    ///
149    /// # Arguments
150    /// * `mouse_x` - Mouse X coordinate in pixels
151    /// * `mouse_y` - Mouse Y coordinate in pixels
152    /// * `tolerance` - Maximum distance in pixels to match a mark
153    ///
154    /// # Returns
155    /// The mark at that position, or None if no mark is within tolerance
156    pub fn scrollbar_mark_at_position(
157        &self,
158        mouse_x: f32,
159        mouse_y: f32,
160        tolerance: f32,
161    ) -> Option<&par_term_config::ScrollbackMark> {
162        self.cell_renderer
163            .scrollbar_mark_at_position(mouse_x, mouse_y, tolerance)
164    }
165}
166
167impl Renderer {
168    pub fn update_cells(&mut self, cells: &[Cell]) {
169        if self.cell_renderer.update_cells(cells) {
170            self.dirty = true;
171        }
172    }
173
174    /// Clear all cells in the renderer.
175    /// Call this when switching tabs to ensure a clean slate.
176    pub fn clear_all_cells(&mut self) {
177        self.cell_renderer.clear_all_cells();
178        self.dirty = true;
179    }
180
181    /// Update cursor position and style for geometric rendering
182    pub fn update_cursor(
183        &mut self,
184        position: (usize, usize),
185        opacity: f32,
186        style: par_term_emu_core_rust::cursor::CursorStyle,
187    ) {
188        if self.cell_renderer.update_cursor(position, opacity, style) {
189            self.dirty = true;
190        }
191    }
192
193    /// Clear cursor (hide it)
194    pub fn clear_cursor(&mut self) {
195        if self.cell_renderer.clear_cursor() {
196            self.dirty = true;
197        }
198    }
199
200    /// Update scrollbar state.
201    pub fn update_scrollbar(
202        &mut self,
203        scroll_offset: usize,
204        visible_lines: usize,
205        total_lines: usize,
206        marks: &[par_term_config::ScrollbackMark],
207    ) {
208        let new_state = (
209            scroll_offset,
210            visible_lines,
211            total_lines,
212            marks.len(),
213            self.cell_renderer.config.width,
214            self.cell_renderer.config.height,
215            // No pane viewport in single-pane path — use zeros
216            0,
217            0,
218            0,
219            0,
220        );
221        if new_state == self.last_scrollbar_state {
222            return;
223        }
224        self.last_scrollbar_state = new_state;
225        self.cell_renderer
226            .update_scrollbar(scroll_offset, visible_lines, total_lines, marks);
227        self.dirty = true;
228    }
229
230    /// Set the visual bell flash intensity
231    ///
232    /// # Arguments
233    /// * `intensity` - Flash intensity from 0.0 (no flash) to 1.0 (full white flash)
234    pub fn set_visual_bell_intensity(&mut self, intensity: f32) {
235        self.cell_renderer.set_visual_bell_intensity(intensity);
236        if intensity > 0.0 {
237            self.dirty = true; // Mark dirty when flash is active
238        }
239    }
240
241    /// Set the visual bell flash color (RGB, 0.0-1.0 per channel).
242    pub fn set_visual_bell_color(&mut self, color: [f32; 3]) {
243        self.cell_renderer.set_visual_bell_color(color);
244    }
245
246    /// Update window opacity in real-time
247    pub fn update_opacity(&mut self, opacity: f32) {
248        self.cell_renderer.update_opacity(opacity);
249
250        // Propagate to custom shader renderer if present
251        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
252            custom_shader.set_opacity(opacity);
253        }
254
255        // Propagate to cursor shader renderer if present
256        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
257            cursor_shader.set_opacity(opacity);
258        }
259
260        self.dirty = true;
261    }
262
263    /// Update cursor color for cell rendering
264    pub fn update_cursor_color(&mut self, color: [u8; 3]) {
265        self.cell_renderer.update_cursor_color(color);
266        self.dirty = true;
267    }
268
269    /// Update cursor text color (color of text under block cursor)
270    pub fn update_cursor_text_color(&mut self, color: Option<[u8; 3]>) {
271        self.cell_renderer.update_cursor_text_color(color);
272        self.dirty = true;
273    }
274
275    /// Set whether cursor should be hidden when cursor shader is active
276    pub fn set_cursor_hidden_for_shader(&mut self, hidden: bool) {
277        if self.cell_renderer.set_cursor_hidden_for_shader(hidden) {
278            self.dirty = true;
279        }
280    }
281
282    /// Set window focus state (affects unfocused cursor rendering)
283    pub fn set_focused(&mut self, focused: bool) {
284        if self.cell_renderer.set_focused(focused) {
285            self.dirty = true;
286        }
287    }
288
289    /// Update cursor guide settings
290    pub fn update_cursor_guide(&mut self, enabled: bool, color: [u8; 4]) {
291        self.cell_renderer.update_cursor_guide(enabled, color);
292        self.dirty = true;
293    }
294
295    /// Update cursor shadow settings.
296    /// Offset and blur are in logical pixels and will be scaled to physical pixels internally.
297    pub fn update_cursor_shadow(
298        &mut self,
299        enabled: bool,
300        color: [u8; 4],
301        offset: [f32; 2],
302        blur: f32,
303    ) {
304        let scale = self.cell_renderer.scale_factor;
305        let physical_offset = [offset[0] * scale, offset[1] * scale];
306        let physical_blur = blur * scale;
307        self.cell_renderer
308            .update_cursor_shadow(enabled, color, physical_offset, physical_blur);
309        self.dirty = true;
310    }
311
312    /// Update cursor boost settings
313    pub fn update_cursor_boost(&mut self, intensity: f32, color: [u8; 3]) {
314        self.cell_renderer.update_cursor_boost(intensity, color);
315        self.dirty = true;
316    }
317
318    /// Update unfocused cursor style
319    pub fn update_unfocused_cursor_style(&mut self, style: par_term_config::UnfocusedCursorStyle) {
320        self.cell_renderer.update_unfocused_cursor_style(style);
321        self.dirty = true;
322    }
323
324    /// Update command separator settings from config.
325    /// Thickness is in logical pixels and will be scaled to physical pixels internally.
326    pub fn update_command_separator(
327        &mut self,
328        enabled: bool,
329        logical_thickness: f32,
330        opacity: f32,
331        exit_color: bool,
332        color: [u8; 3],
333    ) {
334        let physical_thickness = logical_thickness * self.cell_renderer.scale_factor;
335        self.cell_renderer.update_command_separator(
336            enabled,
337            physical_thickness,
338            opacity,
339            exit_color,
340            color,
341        );
342        self.dirty = true;
343    }
344
345    /// Set the visible separator marks for the current frame (single-pane path)
346    pub fn set_separator_marks(&mut self, marks: Vec<SeparatorMark>) {
347        if self.cell_renderer.set_separator_marks(marks) {
348            self.dirty = true;
349        }
350    }
351
352    /// Set gutter indicator data for the current frame (single-pane path).
353    pub fn set_gutter_indicators(&mut self, indicators: Vec<(usize, [f32; 4])>) {
354        self.cell_renderer.set_gutter_indicators(indicators);
355        self.dirty = true;
356    }
357
358    /// Set whether transparency affects only default background cells.
359    /// When true, non-default (colored) backgrounds remain opaque for readability.
360    pub fn set_transparency_affects_only_default_background(&mut self, value: bool) {
361        self.cell_renderer
362            .set_transparency_affects_only_default_background(value);
363        self.dirty = true;
364    }
365
366    /// Set whether text should always be rendered at full opacity.
367    /// When true, text remains opaque regardless of window transparency settings.
368    pub fn set_keep_text_opaque(&mut self, value: bool) {
369        self.cell_renderer.set_keep_text_opaque(value);
370
371        // Also propagate to custom shader renderer if present
372        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
373            custom_shader.set_keep_text_opaque(value);
374        }
375
376        // And to cursor shader renderer if present
377        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
378            cursor_shader.set_keep_text_opaque(value);
379        }
380
381        self.dirty = true;
382    }
383
384    pub fn set_link_underline_style(&mut self, style: par_term_config::LinkUnderlineStyle) {
385        self.cell_renderer.set_link_underline_style(style);
386        self.dirty = true;
387    }
388
389    /// Set whether cursor shader should be disabled due to alt screen being active
390    ///
391    /// When alt screen is active (e.g., vim, htop, less), cursor shader effects
392    /// are disabled since TUI applications typically have their own cursor handling.
393    pub fn set_cursor_shader_disabled_for_alt_screen(&mut self, disabled: bool) {
394        if self.cursor_shader_disabled_for_alt_screen != disabled {
395            log::debug!("[cursor-shader] Alt-screen disable set to {}", disabled);
396            self.cursor_shader_disabled_for_alt_screen = disabled;
397        } else {
398            self.cursor_shader_disabled_for_alt_screen = disabled;
399        }
400    }
401
402    /// Update window padding in real-time without full renderer rebuild.
403    /// Accepts logical pixels (from config); scales to physical pixels internally.
404    /// Returns Some((cols, rows)) if grid size changed and terminal needs resize.
405    pub fn update_window_padding(&mut self, logical_padding: f32) -> Option<(usize, usize)> {
406        let physical_padding = logical_padding * self.cell_renderer.scale_factor;
407        let result = self.cell_renderer.update_window_padding(physical_padding);
408        // Update graphics renderer padding
409        self.graphics_renderer.update_cell_dimensions(
410            self.cell_renderer.cell_width(),
411            self.cell_renderer.cell_height(),
412            physical_padding,
413        );
414        // Update custom shader renderer padding
415        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
416            custom_shader.update_cell_dimensions(
417                self.cell_renderer.cell_width(),
418                self.cell_renderer.cell_height(),
419                physical_padding,
420            );
421        }
422        // Update cursor shader renderer padding
423        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
424            cursor_shader.update_cell_dimensions(
425                self.cell_renderer.cell_width(),
426                self.cell_renderer.cell_height(),
427                physical_padding,
428            );
429        }
430        self.dirty = true;
431        result
432    }
433
434    /// Enable/disable background image and reload if needed
435    pub fn set_background_image_enabled(
436        &mut self,
437        enabled: bool,
438        path: Option<&str>,
439        mode: par_term_config::BackgroundImageMode,
440        opacity: f32,
441    ) {
442        let path = if enabled { path } else { None };
443        self.cell_renderer.set_background_image(path, mode, opacity);
444
445        // Sync background texture to custom shader if it's using background as channel0
446        self.sync_background_texture_to_shader();
447
448        self.dirty = true;
449    }
450
451    /// Set background based on mode (Default, Color, or Image).
452    ///
453    /// This unified method handles all background types and syncs with shaders.
454    pub fn set_background(
455        &mut self,
456        mode: par_term_config::BackgroundMode,
457        color: [u8; 3],
458        image_path: Option<&str>,
459        image_mode: par_term_config::BackgroundImageMode,
460        image_opacity: f32,
461        image_enabled: bool,
462    ) {
463        self.cell_renderer.set_background(
464            mode,
465            color,
466            image_path,
467            image_mode,
468            image_opacity,
469            image_enabled,
470        );
471
472        // Sync background texture to custom shader if it's using background as channel0
473        self.sync_background_texture_to_shader();
474
475        // Sync background to shaders for proper compositing
476        let is_solid_color = matches!(mode, par_term_config::BackgroundMode::Color);
477        let is_image_mode = matches!(mode, par_term_config::BackgroundMode::Image);
478        let normalized_color = color_u8_to_f32(color);
479
480        // Sync to cursor shader
481        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
482            // When background shader is enabled and chained into cursor shader,
483            // don't give cursor shader its own background - background shader handles it
484            let has_background_shader = self.custom_shader_renderer.is_some();
485
486            if has_background_shader {
487                // Background shader handles the background, cursor shader just passes through
488                cursor_shader.set_background_color([0.0, 0.0, 0.0], false);
489                cursor_shader.set_background_texture(self.cell_renderer.device(), None);
490                cursor_shader.update_use_background_as_channel0(self.cell_renderer.device(), false);
491            } else {
492                cursor_shader.set_background_color(normalized_color, is_solid_color);
493
494                // For image mode, pass background image as iChannel0
495                if is_image_mode && image_enabled {
496                    let bg_texture = self.cell_renderer.get_background_as_channel_texture();
497                    cursor_shader.set_background_texture(self.cell_renderer.device(), bg_texture);
498                    cursor_shader
499                        .update_use_background_as_channel0(self.cell_renderer.device(), true);
500                } else {
501                    // Clear background texture when not in image mode
502                    cursor_shader.set_background_texture(self.cell_renderer.device(), None);
503                    cursor_shader
504                        .update_use_background_as_channel0(self.cell_renderer.device(), false);
505                }
506            }
507        }
508
509        // Sync to custom shader
510        // Note: We don't pass is_solid_color=true to custom shaders because
511        // that would replace the shader output with a solid color, making the
512        // shader invisible. Custom shaders handle their own background.
513        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
514            custom_shader.set_background_color(normalized_color, false);
515        }
516
517        self.dirty = true;
518    }
519
520    /// Update scrollbar appearance in real-time.
521    /// Width is in logical pixels and will be scaled to physical pixels internally.
522    pub fn update_scrollbar_appearance(
523        &mut self,
524        logical_width: f32,
525        thumb_color: [f32; 4],
526        track_color: [f32; 4],
527    ) {
528        let physical_width = logical_width * self.cell_renderer.scale_factor;
529        self.cell_renderer
530            .update_scrollbar_appearance(physical_width, thumb_color, track_color);
531        // Force the next update_scrollbar() call to re-upload GPU uniforms with new colors,
532        // since uniform upload is normally skipped when scroll state hasn't changed.
533        self.last_scrollbar_state = (usize::MAX, 0, 0, 0, 0, 0, 0, 0, 0, 0);
534        self.dirty = true;
535    }
536
537    /// Update scrollbar position (left/right) in real-time
538    pub fn update_scrollbar_position(&mut self, position: &str) {
539        self.cell_renderer.update_scrollbar_position(position);
540        self.dirty = true;
541    }
542
543    /// Update background image opacity in real-time
544    pub fn update_background_image_opacity(&mut self, opacity: f32) {
545        self.cell_renderer.update_background_image_opacity(opacity);
546        self.dirty = true;
547    }
548
549    /// Load a per-pane background image into the texture cache.
550    /// Delegates to CellRenderer::load_pane_background.
551    pub fn load_pane_background(&mut self, path: &str) -> Result<bool, crate::error::RenderError> {
552        self.cell_renderer.load_pane_background(path)
553    }
554
555    /// Update inline image scaling mode (nearest vs linear filtering).
556    ///
557    /// Recreates the GPU sampler and clears the texture cache so images
558    /// are re-rendered with the new filter mode.
559    pub fn update_image_scaling_mode(&mut self, scaling_mode: par_term_config::ImageScalingMode) {
560        self.graphics_renderer
561            .update_scaling_mode(self.cell_renderer.device(), scaling_mode);
562        self.dirty = true;
563    }
564
565    /// Update whether inline images preserve their aspect ratio.
566    pub fn update_image_preserve_aspect_ratio(&mut self, preserve: bool) {
567        self.graphics_renderer.set_preserve_aspect_ratio(preserve);
568        self.dirty = true;
569    }
570
571    /// Check if animation requires continuous rendering
572    ///
573    /// Returns true if shader animation is enabled or a cursor trail animation
574    /// might still be in progress.
575    pub fn needs_continuous_render(&self) -> bool {
576        let custom_needs = self
577            .custom_shader_renderer
578            .as_ref()
579            .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
580        let cursor_needs = self
581            .cursor_shader_renderer
582            .as_ref()
583            .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
584        custom_needs || cursor_needs
585    }
586}