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