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