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