Skip to main content

par_term_render/renderer/
shaders.rs

1use super::Renderer;
2use crate::cell_renderer::CellRenderer;
3use crate::custom_shader_renderer::CustomShaderRenderer;
4use anyhow::Result;
5
6/// Initialize the custom shader renderer if configured.
7///
8/// Returns (renderer, shader_path) tuple where both are Some if initialization succeeded.
9#[allow(clippy::too_many_arguments)]
10pub(super) fn init_custom_shader(
11    cell_renderer: &CellRenderer,
12    size_width: u32,
13    size_height: u32,
14    window_padding: f32,
15    custom_shader_path: Option<&str>,
16    custom_shader_enabled: bool,
17    custom_shader_animation: bool,
18    custom_shader_animation_speed: f32,
19    window_opacity: f32,
20    custom_shader_full_content: bool,
21    custom_shader_brightness: f32,
22    custom_shader_channel_paths: &[Option<std::path::PathBuf>; 4],
23    custom_shader_cubemap_path: Option<&std::path::Path>,
24    use_background_as_channel0: bool,
25) -> (Option<CustomShaderRenderer>, Option<String>) {
26    if !custom_shader_enabled {
27        return (None, None);
28    }
29
30    let Some(shader_path) = custom_shader_path else {
31        return (None, None);
32    };
33
34    let path = par_term_config::Config::shader_path(shader_path);
35    match CustomShaderRenderer::new(
36        cell_renderer.device(),
37        cell_renderer.queue(),
38        cell_renderer.surface_format(),
39        &path,
40        size_width,
41        size_height,
42        custom_shader_animation,
43        custom_shader_animation_speed,
44        window_opacity,
45        custom_shader_full_content,
46        custom_shader_channel_paths,
47        custom_shader_cubemap_path,
48    ) {
49        Ok(mut renderer) => {
50            renderer.update_cell_dimensions(
51                cell_renderer.cell_width(),
52                cell_renderer.cell_height(),
53                window_padding,
54            );
55            renderer.set_scale_factor(cell_renderer.scale_factor);
56            renderer.set_brightness(custom_shader_brightness);
57
58            // Apply use_background_as_channel0 setting
59            if use_background_as_channel0 {
60                // Sync background texture and set flag
61                let bg_texture = cell_renderer.get_background_as_channel_texture();
62                renderer.set_background_texture(cell_renderer.device(), bg_texture);
63                renderer.update_use_background_as_channel0(
64                    cell_renderer.device(),
65                    use_background_as_channel0,
66                );
67            }
68
69            log::info!(
70                "[SHADER] Custom shader renderer initialized from: {} (use_bg_as_ch0={})",
71                path.display(),
72                use_background_as_channel0
73            );
74            (Some(renderer), Some(shader_path.to_string()))
75        }
76        Err(e) => {
77            log::info!(
78                "[SHADER] ERROR: Failed to load custom shader '{}': {}",
79                path.display(),
80                e
81            );
82            (None, None)
83        }
84    }
85}
86
87/// Initialize the cursor shader renderer if configured.
88///
89/// Returns (renderer, shader_path) tuple where both are Some if initialization succeeded.
90#[allow(clippy::too_many_arguments)]
91pub(super) fn init_cursor_shader(
92    cell_renderer: &CellRenderer,
93    size_width: u32,
94    size_height: u32,
95    window_padding: f32,
96    cursor_shader_path: Option<&str>,
97    cursor_shader_enabled: bool,
98    cursor_shader_animation: bool,
99    cursor_shader_animation_speed: f32,
100    window_opacity: f32,
101) -> (Option<CustomShaderRenderer>, Option<String>) {
102    log::debug!(
103        "[cursor-shader] Init: enabled={}, path={:?}, animation={}, speed={}",
104        cursor_shader_enabled,
105        cursor_shader_path,
106        cursor_shader_animation,
107        cursor_shader_animation_speed
108    );
109
110    if !cursor_shader_enabled {
111        log::info!("[cursor-shader] Disabled by config");
112        return (None, None);
113    }
114
115    let Some(shader_path) = cursor_shader_path else {
116        log::info!("[cursor-shader] Enabled but no path provided");
117        return (None, None);
118    };
119
120    let path = par_term_config::Config::shader_path(shader_path);
121    let empty_channels: [Option<std::path::PathBuf>; 4] = [None, None, None, None];
122
123    match CustomShaderRenderer::new(
124        cell_renderer.device(),
125        cell_renderer.queue(),
126        cell_renderer.surface_format(),
127        &path,
128        size_width,
129        size_height,
130        cursor_shader_animation,
131        cursor_shader_animation_speed,
132        window_opacity,
133        true, // Full content mode (cursor shader always uses full content)
134        &empty_channels,
135        None, // Cursor shaders don't use cubemaps
136    ) {
137        Ok(mut renderer) => {
138            let cell_w = cell_renderer.cell_width();
139            let cell_h = cell_renderer.cell_height();
140            renderer.update_cell_dimensions(cell_w, cell_h, window_padding);
141            renderer.set_scale_factor(cell_renderer.scale_factor);
142            log::info!(
143                "[SHADER] Cursor shader renderer initialized from: {} (cell={}x{}, padding={})",
144                path.display(),
145                cell_w,
146                cell_h,
147                window_padding
148            );
149            (Some(renderer), Some(shader_path.to_string()))
150        }
151        Err(e) => {
152            log::info!(
153                "[SHADER] ERROR: Failed to load cursor shader '{}': {}",
154                path.display(),
155                e
156            );
157            (None, None)
158        }
159    }
160}
161
162impl Renderer {
163    /// Enable or disable animation for the custom shader at runtime
164    #[allow(dead_code)]
165    pub fn set_custom_shader_animation(&mut self, enabled: bool) {
166        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
167            custom_shader.set_animation_enabled(enabled);
168            self.dirty = true;
169        }
170    }
171
172    /// Update mouse position for custom shader (iMouse uniform)
173    ///
174    /// # Arguments
175    /// * `x` - Mouse X position in pixels (0 = left edge)
176    /// * `y` - Mouse Y position in pixels (0 = top edge)
177    pub fn set_shader_mouse_position(&mut self, x: f32, y: f32) {
178        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
179            custom_shader.set_mouse_position(x, y);
180        }
181    }
182
183    /// Update mouse button state for custom shader (iMouse uniform)
184    ///
185    /// # Arguments
186    /// * `pressed` - True if left mouse button is pressed
187    /// * `x` - Mouse X position at time of click/release
188    /// * `y` - Mouse Y position at time of click/release
189    pub fn set_shader_mouse_button(&mut self, pressed: bool, x: f32, y: f32) {
190        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
191            custom_shader.set_mouse_button(pressed, x, y);
192        }
193    }
194
195    /// Update key press time for custom shaders (iTimeKeyPress uniform)
196    ///
197    /// Call this when a key is pressed to enable key-press-based shader effects
198    /// like screen pulses, typing animations, or keystroke visualizations.
199    pub fn update_key_press_time(&mut self) {
200        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
201            custom_shader.update_key_press();
202        }
203        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
204            cursor_shader.update_key_press();
205        }
206    }
207
208    /// Update cursor state for custom shader (Ghostty-compatible cursor uniforms)
209    ///
210    /// This enables cursor trail effects and other cursor-based animations in custom shaders.
211    ///
212    /// # Arguments
213    /// * `col` - Cursor column position (0-based)
214    /// * `row` - Cursor row position (0-based)
215    /// * `opacity` - Cursor opacity (0.0 = invisible, 1.0 = fully visible)
216    /// * `color` - Cursor RGBA color
217    /// * `style` - Cursor style (Block, Beam, Underline)
218    pub fn update_shader_cursor(
219        &mut self,
220        col: usize,
221        row: usize,
222        opacity: f32,
223        color: [f32; 4],
224        style: par_term_emu_core_rust::cursor::CursorStyle,
225    ) {
226        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
227            custom_shader.update_cursor(col, row, opacity, color, style);
228        }
229        // Also update cursor shader renderer
230        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
231            cursor_shader.update_cursor(col, row, opacity, color, style);
232        }
233    }
234
235    /// Update progress bar state for custom shaders (iProgress uniform)
236    ///
237    /// # Arguments
238    /// * `state` - Progress state (0=hidden, 1=normal, 2=error, 3=indeterminate, 4=warning)
239    /// * `percent` - Progress percentage as 0.0-1.0
240    /// * `is_active` - 1.0 if any progress bar is active, 0.0 otherwise
241    /// * `active_count` - Total count of active bars (simple + named)
242    pub fn update_shader_progress(
243        &mut self,
244        state: f32,
245        percent: f32,
246        is_active: f32,
247        active_count: f32,
248    ) {
249        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
250            custom_shader.update_progress(state, percent, is_active, active_count);
251        }
252        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
253            cursor_shader.update_progress(state, percent, is_active, active_count);
254        }
255    }
256
257    /// Update cursor shader configuration from config values.
258    /// Glow radius is in logical pixels and will be scaled to physical pixels internally.
259    ///
260    /// # Arguments
261    /// * `color` - Cursor color for shader effects [R, G, B] (0-255)
262    /// * `trail_duration` - Duration of cursor trail effect in seconds
263    /// * `glow_radius` - Radius of cursor glow effect in logical pixels
264    /// * `glow_intensity` - Intensity of cursor glow effect (0.0-1.0)
265    pub fn update_cursor_shader_config(
266        &mut self,
267        color: [u8; 3],
268        trail_duration: f32,
269        glow_radius: f32,
270        glow_intensity: f32,
271    ) {
272        let physical_glow_radius = glow_radius * self.cell_renderer.scale_factor;
273        // Update both shaders with cursor config
274        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
275            custom_shader.update_cursor_shader_config(
276                color,
277                trail_duration,
278                physical_glow_radius,
279                glow_intensity,
280            );
281        }
282        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
283            cursor_shader.update_cursor_shader_config(
284                color,
285                trail_duration,
286                physical_glow_radius,
287                glow_intensity,
288            );
289        }
290    }
291
292    /// Enable or disable the cursor shader at runtime
293    ///
294    /// # Arguments
295    /// * `enabled` - Whether to enable the cursor shader
296    /// * `path` - Optional shader path (relative to shaders folder or absolute)
297    /// * `window_opacity` - Current window opacity
298    /// * `animation_enabled` - Whether animation is enabled
299    /// * `animation_speed` - Animation speed multiplier
300    ///
301    /// # Returns
302    /// Ok(()) if successful, Err with error message if compilation fails
303    #[allow(clippy::too_many_arguments)]
304    pub fn set_cursor_shader_enabled(
305        &mut self,
306        enabled: bool,
307        path: Option<&str>,
308        window_opacity: f32,
309        animation_enabled: bool,
310        animation_speed: f32,
311    ) -> Result<(), String> {
312        log::debug!(
313            "[cursor-shader] Toggle: enabled={}, path={:?}, animation={}, speed={}, opacity={}",
314            enabled,
315            path,
316            animation_enabled,
317            animation_speed,
318            window_opacity
319        );
320        match (enabled, path) {
321            (true, Some(path)) => {
322                let path_changed = self.cursor_shader_path.as_ref().is_none_or(|p| p != path);
323
324                // If we already have a shader renderer and path hasn't changed, just update flags
325                if let Some(renderer) = &mut self.cursor_shader_renderer
326                    && !path_changed
327                {
328                    renderer.set_animation_enabled(animation_enabled);
329                    renderer.set_animation_speed(animation_speed);
330                    renderer.set_opacity(window_opacity);
331                    self.dirty = true;
332                    log::info!("[cursor-shader] Already loaded; updated animation/opacities");
333                    return Ok(());
334                }
335
336                let shader_path_full = par_term_config::Config::shader_path(path);
337                // Cursor shader doesn't use channel textures or cubemaps
338                let empty_channels: [Option<std::path::PathBuf>; 4] = [None, None, None, None];
339                match CustomShaderRenderer::new(
340                    self.cell_renderer.device(),
341                    self.cell_renderer.queue(),
342                    self.cell_renderer.surface_format(),
343                    &shader_path_full,
344                    self.size.width,
345                    self.size.height,
346                    animation_enabled,
347                    animation_speed,
348                    window_opacity,
349                    true, // Full content mode (cursor shader always uses full content)
350                    &empty_channels,
351                    None, // Cursor shaders don't use cubemaps
352                ) {
353                    Ok(mut renderer) => {
354                        // Sync cell dimensions for cursor position calculation
355                        renderer.update_cell_dimensions(
356                            self.cell_renderer.cell_width(),
357                            self.cell_renderer.cell_height(),
358                            self.cell_renderer.window_padding(),
359                        );
360                        // Sync DPI scale factor for cursor sizing
361                        renderer.set_scale_factor(self.cell_renderer.scale_factor);
362                        // Sync keep_text_opaque from cell renderer
363                        renderer.set_keep_text_opaque(self.cell_renderer.keep_text_opaque());
364                        // When background shader is enabled and chained into cursor shader,
365                        // don't give cursor shader its own background - background shader handles it
366                        let has_background_shader = self.custom_shader_renderer.is_some();
367
368                        if has_background_shader {
369                            // Background shader handles the background, cursor shader just passes through
370                            renderer.set_background_color([0.0, 0.0, 0.0], false);
371                            renderer.set_background_texture(self.cell_renderer.device(), None);
372                            renderer.update_use_background_as_channel0(
373                                self.cell_renderer.device(),
374                                false,
375                            );
376                        } else {
377                            // Sync background color for solid color mode
378                            renderer.set_background_color(
379                                self.cell_renderer.solid_background_color(),
380                                self.cell_renderer.is_solid_color_background(),
381                            );
382                            // Sync background image for image mode
383                            let is_image_mode = self.cell_renderer.has_background_image()
384                                && !self.cell_renderer.is_solid_color_background();
385                            if is_image_mode {
386                                let bg_texture =
387                                    self.cell_renderer.get_background_as_channel_texture();
388                                renderer.set_background_texture(
389                                    self.cell_renderer.device(),
390                                    bg_texture,
391                                );
392                                renderer.update_use_background_as_channel0(
393                                    self.cell_renderer.device(),
394                                    true,
395                                );
396                            }
397                        }
398                        log::info!(
399                            "[cursor-shader] Enabled at runtime: {}",
400                            shader_path_full.display()
401                        );
402                        self.cursor_shader_renderer = Some(renderer);
403                        self.cursor_shader_path = Some(path.to_string());
404                        self.dirty = true;
405                        Ok(())
406                    }
407                    Err(e) => {
408                        let error_msg = format!(
409                            "Failed to load cursor shader '{}': {}",
410                            shader_path_full.display(),
411                            e
412                        );
413                        log::error!("[cursor-shader] {}", error_msg);
414                        Err(error_msg)
415                    }
416                }
417            }
418            _ => {
419                if self.cursor_shader_renderer.is_some() {
420                    log::info!("[cursor-shader] Disabled at runtime");
421                } else {
422                    log::debug!("[cursor-shader] Already disabled");
423                }
424                self.cursor_shader_renderer = None;
425                self.cursor_shader_path = None;
426                self.dirty = true;
427                Ok(())
428            }
429        }
430    }
431
432    /// Get the current cursor shader path
433    #[allow(dead_code)]
434    pub fn cursor_shader_path(&self) -> Option<&str> {
435        self.cursor_shader_path.as_deref()
436    }
437
438    /// Reload the cursor shader from source code
439    pub fn reload_cursor_shader_from_source(&mut self, source: &str) -> Result<()> {
440        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
441            cursor_shader.reload_from_source(
442                self.cell_renderer.device(),
443                source,
444                "cursor_editor",
445            )?;
446            self.dirty = true;
447            Ok(())
448        } else {
449            Err(anyhow::anyhow!("No cursor shader renderer active"))
450        }
451    }
452
453    /// Reload the custom shader from source code
454    ///
455    /// This method compiles the new shader source and replaces the current pipeline.
456    /// If compilation fails, returns an error and the old shader remains active.
457    ///
458    /// # Arguments
459    /// * `source` - The GLSL shader source code
460    ///
461    /// # Returns
462    /// Ok(()) if successful, Err with error message if compilation fails
463    pub fn reload_shader_from_source(&mut self, source: &str) -> Result<()> {
464        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
465            custom_shader.reload_from_source(self.cell_renderer.device(), source, "editor")?;
466            self.dirty = true;
467            Ok(())
468        } else {
469            Err(anyhow::anyhow!(
470                "No custom shader is currently loaded. Enable a custom shader first."
471            ))
472        }
473    }
474
475    /// Enable/disable custom shader at runtime. When enabling, tries to
476    /// (re)load the shader from the given path; when disabling, drops the
477    /// renderer instance.
478    ///
479    /// Returns Ok(()) on success, or Err with error message on failure.
480    #[allow(clippy::too_many_arguments)]
481    pub fn set_custom_shader_enabled(
482        &mut self,
483        enabled: bool,
484        shader_path: Option<&str>,
485        window_opacity: f32,
486        animation_enabled: bool,
487        animation_speed: f32,
488        full_content: bool,
489        brightness: f32,
490        channel_paths: &[Option<std::path::PathBuf>; 4],
491        cubemap_path: Option<&std::path::Path>,
492    ) -> Result<(), String> {
493        match (enabled, shader_path) {
494            (true, Some(path)) => {
495                // Check if the shader path has changed
496                let path_changed = self.custom_shader_path.as_deref() != Some(path);
497
498                // If we already have a shader renderer and path hasn't changed, just update flags and textures
499                if let Some(renderer) = &mut self.custom_shader_renderer
500                    && !path_changed
501                {
502                    renderer.set_animation_enabled(animation_enabled);
503                    renderer.set_animation_speed(animation_speed);
504                    renderer.set_opacity(window_opacity);
505                    renderer.set_full_content_mode(full_content);
506                    renderer.set_brightness(brightness);
507
508                    // Update channel textures (they may have changed even if shader path didn't)
509                    for (i, path) in channel_paths.iter().enumerate() {
510                        if let Err(e) = renderer.update_channel_texture(
511                            self.cell_renderer.device(),
512                            self.cell_renderer.queue(),
513                            (i + 1) as u8, // channel indices are 1-4
514                            path.as_deref(),
515                        ) {
516                            log::warn!("Failed to update channel {} texture: {}", i, e);
517                        }
518                    }
519
520                    // Update cubemap if provided
521                    if let Some(cubemap) = cubemap_path
522                        && let Err(e) = renderer.update_cubemap(
523                            self.cell_renderer.device(),
524                            self.cell_renderer.queue(),
525                            Some(cubemap),
526                        )
527                    {
528                        log::warn!("Failed to update cubemap: {}", e);
529                    }
530
531                    return Ok(());
532                }
533
534                let shader_path_full = par_term_config::Config::shader_path(path);
535                match CustomShaderRenderer::new(
536                    self.cell_renderer.device(),
537                    self.cell_renderer.queue(),
538                    self.cell_renderer.surface_format(),
539                    &shader_path_full,
540                    self.size.width,
541                    self.size.height,
542                    animation_enabled,
543                    animation_speed,
544                    window_opacity,
545                    full_content,
546                    channel_paths,
547                    cubemap_path,
548                ) {
549                    Ok(mut renderer) => {
550                        // Sync cell dimensions for cursor position calculation
551                        renderer.update_cell_dimensions(
552                            self.cell_renderer.cell_width(),
553                            self.cell_renderer.cell_height(),
554                            self.cell_renderer.window_padding(),
555                        );
556                        // Sync DPI scale factor for cursor sizing
557                        renderer.set_scale_factor(self.cell_renderer.scale_factor);
558                        // Apply brightness setting
559                        renderer.set_brightness(brightness);
560                        // Sync keep_text_opaque from cell renderer
561                        renderer.set_keep_text_opaque(self.cell_renderer.keep_text_opaque());
562                        // Pass background color but don't activate solid color mode
563                        // Custom shaders handle their own background
564                        renderer.set_background_color(
565                            self.cell_renderer.solid_background_color(),
566                            false,
567                        );
568                        log::info!(
569                            "[SHADER] Custom shader enabled at runtime: {}",
570                            shader_path_full.display()
571                        );
572                        self.custom_shader_renderer = Some(renderer);
573                        self.custom_shader_path = Some(path.to_string());
574
575                        // When background shader is enabled, cursor shader should not have its own background
576                        self.sync_cursor_shader_background_state();
577
578                        self.dirty = true;
579                        Ok(())
580                    }
581                    Err(e) => {
582                        let error_msg = format!(
583                            "Failed to load shader '{}': {}",
584                            shader_path_full.display(),
585                            e
586                        );
587                        log::info!("[SHADER] ERROR: {}", error_msg);
588                        Err(error_msg)
589                    }
590                }
591            }
592            _ => {
593                if self.custom_shader_renderer.is_some() {
594                    log::info!("[SHADER] Custom shader disabled at runtime");
595                }
596                self.custom_shader_renderer = None;
597                self.custom_shader_path = None;
598
599                // When background shader is disabled, cursor shader should get its own background back
600                self.sync_cursor_shader_background_state();
601
602                self.dirty = true;
603                Ok(())
604            }
605        }
606    }
607
608    /// Sync the cursor shader's background state based on whether the background shader is enabled.
609    ///
610    /// When background shader is enabled, cursor shader should NOT have its own background
611    /// (the background shader handles it). When background shader is disabled, cursor shader
612    /// should have its own background.
613    fn sync_cursor_shader_background_state(&mut self) {
614        let Some(ref mut cursor_shader) = self.cursor_shader_renderer else {
615            return;
616        };
617
618        let has_background_shader = self.custom_shader_renderer.is_some();
619
620        if has_background_shader {
621            // Background shader handles the background, cursor shader just passes through
622            cursor_shader.set_background_color([0.0, 0.0, 0.0], false);
623            cursor_shader.set_background_texture(self.cell_renderer.device(), None);
624            cursor_shader.update_use_background_as_channel0(self.cell_renderer.device(), false);
625        } else {
626            // Cursor shader needs its own background
627            cursor_shader.set_background_color(
628                self.cell_renderer.solid_background_color(),
629                self.cell_renderer.is_solid_color_background(),
630            );
631
632            let is_image_mode = self.cell_renderer.has_background_image()
633                && !self.cell_renderer.is_solid_color_background();
634            if is_image_mode {
635                let bg_texture = self.cell_renderer.get_background_as_channel_texture();
636                cursor_shader.set_background_texture(self.cell_renderer.device(), bg_texture);
637                cursor_shader.update_use_background_as_channel0(self.cell_renderer.device(), true);
638            } else {
639                cursor_shader.set_background_texture(self.cell_renderer.device(), None);
640                cursor_shader.update_use_background_as_channel0(self.cell_renderer.device(), false);
641            }
642        }
643    }
644
645    /// Set whether to use the background image as iChannel0 for the custom shader.
646    ///
647    /// When enabled, the app's configured background image is bound as iChannel0
648    /// instead of the custom_shader_channel0 texture file.
649    #[allow(dead_code)]
650    pub fn set_use_background_as_channel0(&mut self, use_background: bool) {
651        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
652            custom_shader
653                .update_use_background_as_channel0(self.cell_renderer.device(), use_background);
654            self.dirty = true;
655        }
656    }
657
658    /// Update the background texture for use as iChannel0 in shaders.
659    ///
660    /// Call this whenever the background image changes to sync the shader's
661    /// channel0 texture. This only has an effect if use_background_as_channel0
662    /// is enabled.
663    pub fn sync_background_texture_to_shader(&mut self) {
664        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
665            let bg_texture = self.cell_renderer.get_background_as_channel_texture();
666            custom_shader.set_background_texture(self.cell_renderer.device(), bg_texture);
667            self.dirty = true;
668        }
669    }
670
671    /// Update both the use_background_as_channel0 flag and sync the texture.
672    ///
673    /// This method should be called when:
674    /// - The use_background_as_channel0 setting changes
675    /// - The background image or solid color changes (to sync the new texture)
676    /// - Per-shader config changes
677    ///
678    /// The background texture is always synced to ensure changes are reflected.
679    #[allow(dead_code)]
680    pub fn update_background_as_channel0(&mut self, use_background: bool) {
681        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
682            // Always sync the background texture first - it may have changed
683            let bg_texture = self.cell_renderer.get_background_as_channel_texture();
684            custom_shader.set_background_texture(self.cell_renderer.device(), bg_texture);
685
686            // Then update the flag - this will recreate bind group if flag actually changed
687            custom_shader
688                .update_use_background_as_channel0(self.cell_renderer.device(), use_background);
689
690            self.dirty = true;
691        }
692    }
693
694    /// Update background as channel0 with solid color support.
695    ///
696    /// This method handles the case where background_mode is Color and we need to
697    /// create a solid color texture to pass as iChannel0 instead of an image.
698    ///
699    /// # Arguments
700    /// * `use_background` - Whether to use background as iChannel0
701    /// * `background_mode` - The current background mode (Default, Color, or Image)
702    /// * `color` - The solid background color (used if mode is Color)
703    pub fn update_background_as_channel0_with_mode(
704        &mut self,
705        use_background: bool,
706        background_mode: par_term_config::BackgroundMode,
707        color: [u8; 3],
708    ) {
709        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
710            // Get the appropriate texture based on background mode
711            let bg_texture = match background_mode {
712                par_term_config::BackgroundMode::Default => {
713                    log::info!("update_background_as_channel0_with_mode: Default mode, no texture");
714                    None
715                }
716                par_term_config::BackgroundMode::Color => {
717                    // Create a solid color texture for the shader
718                    log::info!(
719                        "update_background_as_channel0_with_mode: Color mode, creating solid color texture RGB({},{},{})",
720                        color[0],
721                        color[1],
722                        color[2]
723                    );
724                    Some(self.cell_renderer.get_solid_color_as_channel_texture(color))
725                }
726                par_term_config::BackgroundMode::Image => {
727                    // Use the existing background image texture
728                    let tex = self.cell_renderer.get_background_as_channel_texture();
729                    log::info!(
730                        "update_background_as_channel0_with_mode: Image mode, texture={}",
731                        if tex.is_some() { "Some" } else { "None" }
732                    );
733                    tex
734                }
735            };
736
737            let has_texture = bg_texture.is_some();
738            custom_shader.set_background_texture(self.cell_renderer.device(), bg_texture);
739            custom_shader
740                .update_use_background_as_channel0(self.cell_renderer.device(), use_background);
741
742            log::info!(
743                "update_background_as_channel0_with_mode: use_background={}, has_texture={}",
744                use_background,
745                has_texture
746            );
747
748            self.dirty = true;
749        }
750    }
751}