Skip to main content

par_term_render/renderer/shaders/
cursor.rs

1//! Cursor shader initialisation and runtime management.
2//!
3//! Covers the cursor `CustomShaderRenderer` lifecycle: creation at startup via
4//! [`init_cursor_shader`] and runtime enable/disable/reload operations
5//! exposed as `impl Renderer` methods.
6
7use super::super::Renderer;
8use super::CursorShaderInitParams;
9use crate::cell_renderer::CellRenderer;
10use crate::custom_shader_renderer::CustomShaderRenderer;
11
12/// Initialize the cursor shader renderer if configured.
13///
14/// Returns `(renderer, shader_path)` where both are `Some` if initialization succeeded.
15pub(super) fn init_cursor_shader(
16    cell_renderer: &CellRenderer,
17    params: CursorShaderInitParams<'_>,
18) -> (Option<CustomShaderRenderer>, Option<String>) {
19    let CursorShaderInitParams {
20        size_width,
21        size_height,
22        window_padding,
23        path: cursor_shader_path,
24        enabled: cursor_shader_enabled,
25        animation: cursor_shader_animation,
26        animation_speed: cursor_shader_animation_speed,
27        window_opacity,
28    } = params;
29    log::debug!(
30        "[cursor-shader] Init: enabled={}, path={:?}, animation={}, speed={}",
31        cursor_shader_enabled,
32        cursor_shader_path,
33        cursor_shader_animation,
34        cursor_shader_animation_speed
35    );
36
37    if !cursor_shader_enabled {
38        log::info!("[cursor-shader] Disabled by config");
39        return (None, None);
40    }
41
42    let Some(shader_path) = cursor_shader_path else {
43        log::info!("[cursor-shader] Enabled but no path provided");
44        return (None, None);
45    };
46
47    let path = par_term_config::Config::shader_path(shader_path);
48    let empty_channels: [Option<std::path::PathBuf>; 4] = [None, None, None, None];
49    let empty_custom_uniforms = std::collections::BTreeMap::new();
50
51    match CustomShaderRenderer::new(
52        cell_renderer.device(),
53        cell_renderer.queue(),
54        crate::custom_shader_renderer::CustomShaderRendererConfig {
55            surface_format: cell_renderer.surface_format(),
56            shader_path: &path,
57            width: size_width,
58            height: size_height,
59            animation_enabled: cursor_shader_animation,
60            animation_speed: cursor_shader_animation_speed,
61            window_opacity,
62            full_content_mode: true, // Cursor shader always uses full content
63            channel_paths: &empty_channels,
64            cubemap_path: None, // Cursor shaders don't use cubemaps
65            custom_uniforms: &empty_custom_uniforms,
66            background_channel0_blend_mode: par_term_config::ShaderBackgroundBlendMode::Replace,
67        },
68    ) {
69        Ok(mut renderer) => {
70            let cell_w = cell_renderer.cell_width();
71            let cell_h = cell_renderer.cell_height();
72            renderer.update_cell_dimensions(cell_w, cell_h, window_padding);
73            renderer.set_scale_factor(cell_renderer.scale_factor);
74            log::info!(
75                "[SHADER] Cursor shader renderer initialized from: {} (cell={}x{}, padding={})",
76                path.display(),
77                cell_w,
78                cell_h,
79                window_padding
80            );
81            (Some(renderer), Some(shader_path.to_string()))
82        }
83        Err(e) => {
84            log::info!(
85                "[SHADER] ERROR: Failed to load cursor shader '{}': {}",
86                path.display(),
87                e
88            );
89            (None, None)
90        }
91    }
92}
93
94// ============================================================================
95// Cursor shader impl Renderer methods
96// ============================================================================
97
98impl Renderer {
99    /// Get the current cursor shader path
100    pub fn cursor_shader_path(&self) -> Option<&str> {
101        self.cursor_shader_path.as_deref()
102    }
103
104    /// Reload the cursor shader from source code
105    pub fn reload_cursor_shader_from_source(
106        &mut self,
107        source: &str,
108    ) -> Result<(), crate::error::RenderError> {
109        if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
110            cursor_shader
111                .reload_from_source(self.cell_renderer.device(), source, "cursor_editor")
112                .map_err(|e| crate::error::RenderError::NoActiveShader(format!("{:#}", e)))?;
113            self.dirty = true;
114            Ok(())
115        } else {
116            Err(crate::error::RenderError::NoActiveShader(
117                "No cursor shader renderer active".to_string(),
118            ))
119        }
120    }
121
122    /// Enable or disable the cursor shader at runtime.
123    ///
124    /// # Returns
125    /// `Ok(())` if successful, `Err` with error message if compilation fails
126    pub fn set_cursor_shader_enabled(
127        &mut self,
128        enabled: bool,
129        path: Option<&str>,
130        window_opacity: f32,
131        animation_enabled: bool,
132        animation_speed: f32,
133    ) -> Result<(), String> {
134        log::debug!(
135            "[cursor-shader] Toggle: enabled={}, path={:?}, animation={}, speed={}, opacity={}",
136            enabled,
137            path,
138            animation_enabled,
139            animation_speed,
140            window_opacity
141        );
142        match (enabled, path) {
143            (true, Some(path)) => {
144                let path_changed = self.cursor_shader_path.as_ref().is_none_or(|p| p != path);
145
146                // If we already have a shader renderer and path hasn't changed, just update flags
147                if let Some(renderer) = &mut self.cursor_shader_renderer
148                    && !path_changed
149                {
150                    renderer.set_animation_enabled(animation_enabled);
151                    renderer.set_animation_speed(animation_speed);
152                    renderer.set_opacity(window_opacity);
153                    self.dirty = true;
154                    log::info!("[cursor-shader] Already loaded; updated animation/opacities");
155                    return Ok(());
156                }
157
158                let shader_path_full = par_term_config::Config::shader_path(path);
159                // Cursor shader doesn't use channel textures or cubemaps
160                let empty_channels: [Option<std::path::PathBuf>; 4] = [None, None, None, None];
161                let empty_custom_uniforms = std::collections::BTreeMap::new();
162                match CustomShaderRenderer::new(
163                    self.cell_renderer.device(),
164                    self.cell_renderer.queue(),
165                    crate::custom_shader_renderer::CustomShaderRendererConfig {
166                        surface_format: self.cell_renderer.surface_format(),
167                        shader_path: &shader_path_full,
168                        width: self.size.width,
169                        height: self.size.height,
170                        animation_enabled,
171                        animation_speed,
172                        window_opacity,
173                        full_content_mode: true, // Cursor shader always uses full content
174                        channel_paths: &empty_channels,
175                        cubemap_path: None, // Cursor shaders don't use cubemaps
176                        custom_uniforms: &empty_custom_uniforms,
177                        background_channel0_blend_mode:
178                            par_term_config::ShaderBackgroundBlendMode::Replace,
179                    },
180                ) {
181                    Ok(mut renderer) => {
182                        // Sync cell dimensions for cursor position calculation
183                        renderer.update_cell_dimensions(
184                            self.cell_renderer.cell_width(),
185                            self.cell_renderer.cell_height(),
186                            self.cell_renderer.window_padding(),
187                        );
188                        // Sync DPI scale factor for cursor sizing
189                        renderer.set_scale_factor(self.cell_renderer.scale_factor);
190                        // Sync keep_text_opaque from cell renderer
191                        renderer.set_keep_text_opaque(self.cell_renderer.keep_text_opaque());
192                        // When background shader is enabled and chained into cursor shader,
193                        // don't give cursor shader its own background - background shader handles it
194                        let has_background_shader = self.custom_shader_renderer.is_some();
195
196                        if has_background_shader {
197                            // Background shader handles the background, cursor shader just passes through
198                            renderer.set_background_color([0.0, 0.0, 0.0], false);
199                            renderer.set_background_texture(self.cell_renderer.device(), None);
200                            renderer.update_use_background_as_channel0(
201                                self.cell_renderer.device(),
202                                false,
203                            );
204                        } else {
205                            // Sync background color for solid color mode
206                            renderer.set_background_color(
207                                self.cell_renderer.solid_background_color(),
208                                self.cell_renderer.is_solid_color_background(),
209                            );
210                            // Sync background image for image mode
211                            let is_image_mode = self.cell_renderer.has_background_image()
212                                && !self.cell_renderer.is_solid_color_background();
213                            if is_image_mode {
214                                let bg_texture =
215                                    self.cell_renderer.get_background_as_channel_texture();
216                                renderer.set_background_texture(
217                                    self.cell_renderer.device(),
218                                    bg_texture,
219                                );
220                                renderer.update_use_background_as_channel0(
221                                    self.cell_renderer.device(),
222                                    true,
223                                );
224                            }
225                        }
226                        log::info!(
227                            "[cursor-shader] Enabled at runtime: {}",
228                            shader_path_full.display()
229                        );
230                        self.cursor_shader_renderer = Some(renderer);
231                        self.cursor_shader_path = Some(path.to_string());
232                        self.dirty = true;
233                        Ok(())
234                    }
235                    Err(e) => {
236                        let error_msg = format!(
237                            "Failed to load cursor shader '{}': {}",
238                            shader_path_full.display(),
239                            e
240                        );
241                        log::error!("[cursor-shader] {}", error_msg);
242                        Err(error_msg)
243                    }
244                }
245            }
246            _ => {
247                if self.cursor_shader_renderer.is_some() {
248                    log::info!("[cursor-shader] Disabled at runtime");
249                } else {
250                    log::debug!("[cursor-shader] Already disabled");
251                }
252                self.cursor_shader_renderer = None;
253                self.cursor_shader_path = None;
254                self.dirty = true;
255                Ok(())
256            }
257        }
258    }
259
260    /// Sync the cursor shader's background state based on whether the background shader is enabled.
261    ///
262    /// When background shader is enabled, cursor shader should NOT have its own background
263    /// (the background shader handles it). When background shader is disabled, cursor shader
264    /// should have its own background.
265    pub(super) fn sync_cursor_shader_background_state(&mut self) {
266        let Some(ref mut cursor_shader) = self.cursor_shader_renderer else {
267            return;
268        };
269
270        let has_background_shader = self.custom_shader_renderer.is_some();
271
272        if has_background_shader {
273            cursor_shader.set_background_color([0.0, 0.0, 0.0], false);
274            cursor_shader.set_background_texture(self.cell_renderer.device(), None);
275            cursor_shader.update_use_background_as_channel0(self.cell_renderer.device(), false);
276        } else {
277            cursor_shader.set_background_color(
278                self.cell_renderer.solid_background_color(),
279                self.cell_renderer.is_solid_color_background(),
280            );
281
282            let is_image_mode = self.cell_renderer.has_background_image()
283                && !self.cell_renderer.is_solid_color_background();
284            if is_image_mode {
285                let bg_texture = self.cell_renderer.get_background_as_channel_texture();
286                cursor_shader.set_background_texture(self.cell_renderer.device(), bg_texture);
287                cursor_shader.update_use_background_as_channel0(self.cell_renderer.device(), true);
288            } else {
289                cursor_shader.set_background_texture(self.cell_renderer.device(), None);
290                cursor_shader.update_use_background_as_channel0(self.cell_renderer.device(), false);
291            }
292        }
293    }
294}