Skip to main content

par_term_config/
shader_config.rs

1//! Shader configuration resolution.
2//!
3//! Handles merging of per-shader configurations from multiple sources:
4//! 1. User overrides (from config.yaml shader_configs)
5//! 2. Shader metadata defaults (from embedded YAML in shader files)
6//! 3. Global defaults (from defaults.rs / Config struct)
7
8use crate::config::Config;
9use crate::types::{
10    CursorShaderConfig, CursorShaderMetadata, ResolvedCursorShaderConfig, ResolvedShaderConfig,
11    ShaderConfig, ShaderMetadata,
12};
13use std::path::PathBuf;
14
15/// Resolve a shader configuration by merging sources in priority order.
16///
17/// Priority (highest to lowest):
18/// 1. User overrides from config.yaml
19/// 2. Defaults embedded in shader metadata
20/// 3. Global defaults from Config
21///
22/// # Arguments
23/// * `user_override` - Optional user overrides from config.yaml
24/// * `metadata` - Optional shader metadata with embedded defaults
25/// * `config` - Global config for fallback values
26///
27/// # Returns
28/// A fully resolved configuration with all values filled in
29#[allow(dead_code)]
30pub fn resolve_shader_config(
31    user_override: Option<&ShaderConfig>,
32    metadata: Option<&ShaderMetadata>,
33    config: &Config,
34) -> ResolvedShaderConfig {
35    // Extract metadata defaults if available
36    let meta_defaults = metadata.map(|m| &m.defaults);
37
38    // Helper to resolve a single value through the priority chain
39    macro_rules! resolve {
40        ($field:ident, $global:expr) => {
41            user_override
42                .and_then(|o| o.$field.clone())
43                .or_else(|| meta_defaults.and_then(|m| m.$field.clone()))
44                .unwrap_or($global)
45        };
46    }
47
48    // Helper for Option<String> -> Option<PathBuf> with path resolution
49    // An explicit empty string means "no texture" (don't fall back to defaults)
50    macro_rules! resolve_path {
51        ($field:ident, $global:expr) => {{
52            // Check for user override first
53            if let Some(override_val) = user_override.and_then(|o| o.$field.clone()) {
54                if override_val.is_empty() {
55                    None // User explicitly cleared this channel
56                } else {
57                    Some(Config::resolve_texture_path(&override_val))
58                }
59            } else {
60                // No user override, fall back to metadata then global
61                let path_str: Option<String> =
62                    meta_defaults.and_then(|m| m.$field.clone()).or($global);
63                path_str
64                    .filter(|p| !p.is_empty())
65                    .map(|p| Config::resolve_texture_path(&p))
66            }
67        }};
68    }
69
70    ResolvedShaderConfig {
71        animation_speed: resolve!(animation_speed, config.custom_shader_animation_speed),
72        brightness: resolve!(brightness, config.custom_shader_brightness),
73        text_opacity: resolve!(text_opacity, config.custom_shader_text_opacity),
74        full_content: resolve!(full_content, config.custom_shader_full_content),
75        channel0: resolve_path!(channel0, config.custom_shader_channel0.clone()),
76        channel1: resolve_path!(channel1, config.custom_shader_channel1.clone()),
77        channel2: resolve_path!(channel2, config.custom_shader_channel2.clone()),
78        channel3: resolve_path!(channel3, config.custom_shader_channel3.clone()),
79        cubemap: resolve_path!(cubemap, config.custom_shader_cubemap.clone()),
80        cubemap_enabled: resolve!(cubemap_enabled, config.custom_shader_cubemap_enabled),
81        use_background_as_channel0: resolve!(
82            use_background_as_channel0,
83            config.custom_shader_use_background_as_channel0
84        ),
85    }
86}
87
88/// Resolve a cursor shader configuration by merging sources in priority order.
89///
90/// Priority (highest to lowest):
91/// 1. User overrides from config.yaml cursor_shader_configs
92/// 2. Defaults embedded in cursor shader metadata
93/// 3. Global defaults from Config
94///
95/// # Arguments
96/// * `user_override` - Optional user overrides from config.yaml
97/// * `metadata` - Optional cursor shader metadata with embedded defaults
98/// * `config` - Global config for fallback values
99///
100/// # Returns
101/// A fully resolved cursor shader configuration with all values filled in
102pub fn resolve_cursor_shader_config(
103    user_override: Option<&CursorShaderConfig>,
104    metadata: Option<&CursorShaderMetadata>,
105    config: &Config,
106) -> ResolvedCursorShaderConfig {
107    // Extract metadata defaults if available
108    let meta_defaults = metadata.map(|m| &m.defaults);
109
110    // Helper to resolve a cursor-specific value through the priority chain
111    macro_rules! resolve_cursor {
112        ($field:ident, $global:expr) => {
113            user_override
114                .and_then(|o| o.$field)
115                .or_else(|| meta_defaults.and_then(|m| m.$field))
116                .unwrap_or($global)
117        };
118    }
119
120    // Resolve base shader settings (animation_speed comes from base)
121    let animation_speed = user_override
122        .and_then(|o| o.base.animation_speed)
123        .or_else(|| meta_defaults.and_then(|m| m.base.animation_speed))
124        .unwrap_or(config.cursor_shader_animation_speed);
125
126    // Build a minimal resolved base config for cursor shader
127    // (cursor shaders don't use most of the base shader features)
128    let base = ResolvedShaderConfig {
129        animation_speed,
130        brightness: 1.0,
131        text_opacity: 1.0,
132        full_content: true, // Cursor shaders always use full content
133        channel0: None,
134        channel1: None,
135        channel2: None,
136        channel3: None,
137        cubemap: None,
138        cubemap_enabled: false,
139        use_background_as_channel0: false,
140    };
141
142    // Resolve cursor-specific values
143    let hides_cursor = resolve_cursor!(hides_cursor, config.cursor_shader_hides_cursor);
144    let disable_in_alt_screen = resolve_cursor!(
145        disable_in_alt_screen,
146        config.cursor_shader_disable_in_alt_screen
147    );
148    let glow_radius = resolve_cursor!(glow_radius, config.cursor_shader_glow_radius);
149    let glow_intensity = resolve_cursor!(glow_intensity, config.cursor_shader_glow_intensity);
150    let trail_duration = resolve_cursor!(trail_duration, config.cursor_shader_trail_duration);
151    let cursor_color = user_override
152        .and_then(|o| o.cursor_color)
153        .or_else(|| meta_defaults.and_then(|m| m.cursor_color))
154        .unwrap_or(config.cursor_shader_color);
155
156    ResolvedCursorShaderConfig {
157        base,
158        hides_cursor,
159        disable_in_alt_screen,
160        glow_radius,
161        glow_intensity,
162        trail_duration,
163        cursor_color,
164    }
165}
166
167#[allow(dead_code)]
168impl ResolvedShaderConfig {
169    /// Resolve a shader config for a specific shader.
170    ///
171    /// This is a convenience method that looks up the user override and
172    /// combines it with metadata and global config.
173    ///
174    /// # Arguments
175    /// * `shader_name` - Name of the shader file (e.g., "crt.glsl")
176    /// * `metadata` - Optional shader metadata
177    /// * `config` - Global config
178    pub fn for_shader(
179        shader_name: &str,
180        metadata: Option<&ShaderMetadata>,
181        config: &Config,
182    ) -> Self {
183        let user_override = config.get_shader_override(shader_name);
184        resolve_shader_config(user_override, metadata, config)
185    }
186
187    /// Get channel paths as an array suitable for passing to the renderer.
188    pub fn channel_paths(&self) -> [Option<PathBuf>; 4] {
189        [
190            self.channel0.clone(),
191            self.channel1.clone(),
192            self.channel2.clone(),
193            self.channel3.clone(),
194        ]
195    }
196
197    /// Get the cubemap path if configured.
198    pub fn cubemap_path(&self) -> Option<&PathBuf> {
199        if self.cubemap_enabled {
200            self.cubemap.as_ref()
201        } else {
202            None
203        }
204    }
205}
206
207#[allow(dead_code)]
208impl ResolvedCursorShaderConfig {
209    /// Resolve a cursor shader config for a specific shader.
210    ///
211    /// # Arguments
212    /// * `shader_name` - Name of the cursor shader file
213    /// * `metadata` - Optional cursor shader metadata
214    /// * `config` - Global config
215    pub fn for_shader(
216        shader_name: &str,
217        metadata: Option<&CursorShaderMetadata>,
218        config: &Config,
219    ) -> Self {
220        let user_override = config.get_cursor_shader_override(shader_name);
221        resolve_cursor_shader_config(user_override, metadata, config)
222    }
223}
224
225/// Default global values for shader configuration.
226///
227/// These are used when neither user override nor metadata provide a value.
228#[allow(dead_code)]
229pub mod global_defaults {
230    pub const ANIMATION_SPEED: f32 = 1.0;
231    pub const BRIGHTNESS: f32 = 1.0;
232    pub const TEXT_OPACITY: f32 = 1.0;
233    pub const FULL_CONTENT: bool = false;
234    pub const CUBEMAP_ENABLED: bool = true;
235
236    // Cursor shader defaults
237    pub const GLOW_RADIUS: f32 = 80.0;
238    pub const GLOW_INTENSITY: f32 = 0.3;
239    pub const TRAIL_DURATION: f32 = 0.5;
240    pub const CURSOR_COLOR: [u8; 3] = [255, 255, 255];
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use crate::ShaderConfig;
247
248    fn make_test_config() -> Config {
249        Config::default()
250    }
251
252    #[test]
253    fn test_resolve_with_no_overrides() {
254        let config = make_test_config();
255        let resolved = resolve_shader_config(None, None, &config);
256
257        assert_eq!(
258            resolved.animation_speed,
259            config.custom_shader_animation_speed
260        );
261        assert_eq!(resolved.brightness, config.custom_shader_brightness);
262        assert_eq!(resolved.text_opacity, config.custom_shader_text_opacity);
263        assert_eq!(resolved.full_content, config.custom_shader_full_content);
264    }
265
266    #[test]
267    fn test_resolve_with_metadata_defaults() {
268        let config = make_test_config();
269        let shader_defaults = ShaderConfig {
270            animation_speed: Some(0.5),
271            brightness: Some(0.7),
272            ..Default::default()
273        };
274
275        let metadata = ShaderMetadata {
276            name: Some("Test".to_string()),
277            defaults: shader_defaults,
278            ..Default::default()
279        };
280
281        let resolved = resolve_shader_config(None, Some(&metadata), &config);
282
283        assert_eq!(resolved.animation_speed, 0.5);
284        assert_eq!(resolved.brightness, 0.7);
285        // Others should use global defaults
286        assert_eq!(resolved.text_opacity, config.custom_shader_text_opacity);
287    }
288
289    #[test]
290    fn test_resolve_with_user_override() {
291        let config = make_test_config();
292        let user_override = ShaderConfig {
293            animation_speed: Some(2.0),
294            brightness: Some(0.9),
295            ..Default::default()
296        };
297
298        let shader_defaults = ShaderConfig {
299            animation_speed: Some(0.5), // Should be overridden
300            text_opacity: Some(0.8),    // Should be used (no user override)
301            ..Default::default()
302        };
303
304        let metadata = ShaderMetadata {
305            name: Some("Test".to_string()),
306            defaults: shader_defaults,
307            ..Default::default()
308        };
309
310        let resolved = resolve_shader_config(Some(&user_override), Some(&metadata), &config);
311
312        // User override takes priority
313        assert_eq!(resolved.animation_speed, 2.0);
314        assert_eq!(resolved.brightness, 0.9);
315        // Metadata default used when no user override
316        assert_eq!(resolved.text_opacity, 0.8);
317    }
318
319    #[test]
320    fn test_channel_paths() {
321        let resolved = ResolvedShaderConfig {
322            channel0: Some(PathBuf::from("/path/to/tex0.png")),
323            channel1: None,
324            channel2: Some(PathBuf::from("/path/to/tex2.png")),
325            channel3: None,
326            ..Default::default()
327        };
328
329        let paths = resolved.channel_paths();
330        assert!(paths[0].is_some());
331        assert!(paths[1].is_none());
332        assert!(paths[2].is_some());
333        assert!(paths[3].is_none());
334    }
335
336    #[test]
337    fn test_cubemap_path_respects_enabled() {
338        let mut resolved = ResolvedShaderConfig {
339            cubemap: Some(PathBuf::from("/path/to/cubemap")),
340            cubemap_enabled: true,
341            ..Default::default()
342        };
343
344        assert!(resolved.cubemap_path().is_some());
345
346        resolved.cubemap_enabled = false;
347        assert!(resolved.cubemap_path().is_none());
348    }
349}