1use crate::config::Config;
54use crate::types::{
55 CursorShaderConfig, CursorShaderMetadata, ResolvedCursorShaderConfig, ResolvedShaderConfig,
56 ShaderBackgroundBlendMode, ShaderConfig, ShaderMetadata,
57};
58use std::collections::BTreeMap;
59use std::path::PathBuf;
60
61pub fn resolve_shader_config(
76 user_override: Option<&ShaderConfig>,
77 metadata: Option<&ShaderMetadata>,
78 config: &Config,
79) -> ResolvedShaderConfig {
80 let meta_defaults = metadata.map(|m| &m.defaults);
82
83 macro_rules! resolve {
85 ($field:ident, $global:expr) => {
86 user_override
87 .and_then(|o| o.$field.clone())
88 .or_else(|| meta_defaults.and_then(|m| m.$field.clone()))
89 .unwrap_or($global)
90 };
91 }
92
93 macro_rules! resolve_path {
96 ($field:ident, $global:expr) => {{
97 if let Some(override_val) = user_override.and_then(|o| o.$field.clone()) {
99 if override_val.is_empty() {
100 None } else {
102 Some(Config::resolve_texture_path(&override_val))
103 }
104 } else {
105 let path_str: Option<String> =
107 meta_defaults.and_then(|m| m.$field.clone()).or($global);
108 path_str
109 .filter(|p| !p.is_empty())
110 .map(|p| Config::resolve_texture_path(&p))
111 }
112 }};
113 }
114
115 let mut custom_uniforms = metadata
116 .map(|m| m.defaults.uniforms.clone())
117 .unwrap_or_default();
118 if let Some(user_override) = user_override {
119 custom_uniforms.extend(user_override.uniforms.clone());
120 }
121
122 let global_brightness = config.shader.custom_shader_brightness;
123 let default_brightness = crate::defaults::custom_shader_brightness();
124 let brightness = user_override
125 .and_then(|override_config| override_config.brightness)
126 .or_else(|| {
127 if (global_brightness - default_brightness).abs() > f32::EPSILON {
128 Some(global_brightness)
129 } else {
130 meta_defaults.and_then(|defaults| defaults.brightness)
131 }
132 })
133 .unwrap_or(global_brightness);
134
135 ResolvedShaderConfig {
136 animation_speed: resolve!(animation_speed, config.shader.custom_shader_animation_speed),
137 brightness,
138 text_opacity: resolve!(text_opacity, config.shader.custom_shader_text_opacity),
139 full_content: resolve!(full_content, config.shader.custom_shader_full_content),
140 channel0: resolve_path!(channel0, config.shader.custom_shader_channel0.clone()),
141 channel1: resolve_path!(channel1, config.shader.custom_shader_channel1.clone()),
142 channel2: resolve_path!(channel2, config.shader.custom_shader_channel2.clone()),
143 channel3: resolve_path!(channel3, config.shader.custom_shader_channel3.clone()),
144 cubemap: resolve_path!(cubemap, config.shader.custom_shader_cubemap.clone()),
145 cubemap_enabled: resolve!(cubemap_enabled, config.shader.custom_shader_cubemap_enabled),
146 use_background_as_channel0: resolve!(
147 use_background_as_channel0,
148 config.shader.custom_shader_use_background_as_channel0
149 ),
150 background_channel0_blend_mode: resolve!(
151 background_channel0_blend_mode,
152 config.shader.custom_shader_background_channel0_blend_mode
153 ),
154 auto_dim_under_text: resolve!(
155 auto_dim_under_text,
156 config.shader.custom_shader_auto_dim_under_text
157 ),
158 auto_dim_strength: resolve!(
159 auto_dim_strength,
160 config.shader.custom_shader_auto_dim_strength
161 )
162 .clamp(0.0, 1.0),
163 custom_uniforms,
164 }
165}
166
167pub fn resolve_cursor_shader_config(
182 user_override: Option<&CursorShaderConfig>,
183 metadata: Option<&CursorShaderMetadata>,
184 config: &Config,
185) -> ResolvedCursorShaderConfig {
186 let meta_defaults = metadata.map(|m| &m.defaults);
188
189 macro_rules! resolve_cursor {
191 ($field:ident, $global:expr) => {
192 user_override
193 .and_then(|o| o.$field)
194 .or_else(|| meta_defaults.and_then(|m| m.$field))
195 .unwrap_or($global)
196 };
197 }
198
199 let animation_speed = user_override
201 .and_then(|o| o.base.animation_speed)
202 .or_else(|| meta_defaults.and_then(|m| m.base.animation_speed))
203 .unwrap_or(config.shader.cursor_shader_animation_speed);
204
205 let base = ResolvedShaderConfig {
208 animation_speed,
209 brightness: 1.0,
210 text_opacity: 1.0,
211 full_content: true, channel0: None,
213 channel1: None,
214 channel2: None,
215 channel3: None,
216 cubemap: None,
217 cubemap_enabled: false,
218 use_background_as_channel0: false,
219 background_channel0_blend_mode: ShaderBackgroundBlendMode::Replace,
220 auto_dim_under_text: false,
221 auto_dim_strength: 0.35,
222 custom_uniforms: BTreeMap::new(),
223 };
224
225 let hides_cursor = resolve_cursor!(hides_cursor, config.shader.cursor_shader_hides_cursor);
227 let disable_in_alt_screen = resolve_cursor!(
228 disable_in_alt_screen,
229 config.shader.cursor_shader_disable_in_alt_screen
230 );
231 let glow_radius = resolve_cursor!(glow_radius, config.shader.cursor_shader_glow_radius);
232 let glow_intensity =
233 resolve_cursor!(glow_intensity, config.shader.cursor_shader_glow_intensity);
234 let trail_duration =
235 resolve_cursor!(trail_duration, config.shader.cursor_shader_trail_duration);
236 let cursor_color = user_override
237 .and_then(|o| o.cursor_color)
238 .or_else(|| meta_defaults.and_then(|m| m.cursor_color))
239 .unwrap_or(config.shader.cursor_shader_color);
240
241 ResolvedCursorShaderConfig {
242 base,
243 hides_cursor,
244 disable_in_alt_screen,
245 glow_radius,
246 glow_intensity,
247 trail_duration,
248 cursor_color,
249 }
250}
251
252impl ResolvedShaderConfig {
253 pub fn for_shader(
263 shader_name: &str,
264 metadata: Option<&ShaderMetadata>,
265 config: &Config,
266 ) -> Self {
267 let user_override = config.get_shader_override(shader_name);
268 resolve_shader_config(user_override, metadata, config)
269 }
270
271 pub fn channel_paths(&self) -> [Option<PathBuf>; 4] {
273 [
274 self.channel0.clone(),
275 self.channel1.clone(),
276 self.channel2.clone(),
277 self.channel3.clone(),
278 ]
279 }
280
281 pub fn cubemap_path(&self) -> Option<&PathBuf> {
283 if self.cubemap_enabled {
284 self.cubemap.as_ref()
285 } else {
286 None
287 }
288 }
289}
290
291impl ResolvedCursorShaderConfig {
292 pub fn for_shader(
299 shader_name: &str,
300 metadata: Option<&CursorShaderMetadata>,
301 config: &Config,
302 ) -> Self {
303 let user_override = config.get_cursor_shader_override(shader_name);
304 resolve_cursor_shader_config(user_override, metadata, config)
305 }
306}
307
308pub mod global_defaults {
312 pub const ANIMATION_SPEED: f32 = 1.0;
313 pub const BRIGHTNESS: f32 = 1.0;
314 pub const TEXT_OPACITY: f32 = 1.0;
315 pub const FULL_CONTENT: bool = false;
316 pub const CUBEMAP_ENABLED: bool = true;
317
318 pub const GLOW_RADIUS: f32 = 80.0;
320 pub const GLOW_INTENSITY: f32 = 0.3;
321 pub const TRAIL_DURATION: f32 = 0.5;
322 pub const CURSOR_COLOR: [u8; 3] = [255, 255, 255];
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use crate::{ShaderBackgroundBlendMode, ShaderConfig, ShaderUniformValue};
329 use std::collections::BTreeMap;
330
331 fn make_test_config() -> Config {
332 Config::default()
333 }
334
335 #[test]
336 fn resolves_background_channel0_blend_mode_from_global_default() {
337 let config = Config::default();
338 let resolved = resolve_shader_config(None, None, &config);
339
340 assert_eq!(
341 resolved.background_channel0_blend_mode,
342 ShaderBackgroundBlendMode::Replace
343 );
344 }
345
346 #[test]
347 fn resolves_background_channel0_blend_mode_override_over_metadata() {
348 let config = Config::default();
349 let metadata = ShaderMetadata {
350 name: Some("Blend Metadata".to_string()),
351 defaults: ShaderConfig {
352 background_channel0_blend_mode: Some(ShaderBackgroundBlendMode::Multiply),
353 ..Default::default()
354 },
355 ..Default::default()
356 };
357 let override_config = ShaderConfig {
358 background_channel0_blend_mode: Some(ShaderBackgroundBlendMode::Screen),
359 ..Default::default()
360 };
361
362 let resolved = resolve_shader_config(Some(&override_config), Some(&metadata), &config);
363
364 assert_eq!(
365 resolved.background_channel0_blend_mode,
366 ShaderBackgroundBlendMode::Screen
367 );
368 }
369
370 #[test]
371 fn preserves_builtin_texture_ids_from_shader_config_sources() {
372 const BUILTIN: &str = "builtin://noise/value-256";
373
374 let mut global_config = Config::default();
375 global_config.shader.custom_shader_channel0 = Some(BUILTIN.to_string());
376 let resolved = resolve_shader_config(None, None, &global_config);
377 assert_eq!(
378 resolved
379 .channel0
380 .expect("global channel0")
381 .display()
382 .to_string(),
383 BUILTIN
384 );
385
386 let metadata = ShaderMetadata {
387 defaults: ShaderConfig {
388 channel0: Some(BUILTIN.to_string()),
389 ..Default::default()
390 },
391 ..Default::default()
392 };
393 let resolved = resolve_shader_config(None, Some(&metadata), &Config::default());
394 assert_eq!(
395 resolved
396 .channel0
397 .expect("metadata channel0")
398 .display()
399 .to_string(),
400 BUILTIN
401 );
402
403 let mut override_config = Config::default();
404 override_config.shader_overrides.shader_configs.insert(
405 "test.glsl".to_string(),
406 ShaderConfig {
407 channel0: Some(BUILTIN.to_string()),
408 ..Default::default()
409 },
410 );
411 let resolved = ResolvedShaderConfig::for_shader("test.glsl", None, &override_config);
412 assert_eq!(
413 resolved
414 .channel0
415 .expect("override channel0")
416 .display()
417 .to_string(),
418 BUILTIN
419 );
420 }
421
422 #[test]
423 fn test_resolve_with_no_overrides() {
424 let config = make_test_config();
425 let resolved = resolve_shader_config(None, None, &config);
426
427 assert_eq!(
428 resolved.animation_speed,
429 config.shader.custom_shader_animation_speed
430 );
431 assert_eq!(resolved.brightness, config.shader.custom_shader_brightness);
432 assert_eq!(
433 resolved.text_opacity,
434 config.shader.custom_shader_text_opacity
435 );
436 assert_eq!(
437 resolved.full_content,
438 config.shader.custom_shader_full_content
439 );
440 }
441
442 #[test]
443 fn test_resolve_with_metadata_defaults() {
444 let config = make_test_config();
445 let shader_defaults = ShaderConfig {
446 animation_speed: Some(0.5),
447 brightness: Some(0.7),
448 ..Default::default()
449 };
450
451 let metadata = ShaderMetadata {
452 name: Some("Test".to_string()),
453 defaults: shader_defaults,
454 ..Default::default()
455 };
456
457 let resolved = resolve_shader_config(None, Some(&metadata), &config);
458
459 assert_eq!(resolved.animation_speed, 0.5);
460 assert_eq!(resolved.brightness, 0.7);
461 assert_eq!(
463 resolved.text_opacity,
464 config.shader.custom_shader_text_opacity
465 );
466 }
467
468 #[test]
469 fn test_resolve_with_user_override() {
470 let config = make_test_config();
471 let user_override = ShaderConfig {
472 animation_speed: Some(2.0),
473 brightness: Some(0.9),
474 ..Default::default()
475 };
476
477 let shader_defaults = ShaderConfig {
478 animation_speed: Some(0.5), text_opacity: Some(0.8), ..Default::default()
481 };
482
483 let metadata = ShaderMetadata {
484 name: Some("Test".to_string()),
485 defaults: shader_defaults,
486 ..Default::default()
487 };
488
489 let resolved = resolve_shader_config(Some(&user_override), Some(&metadata), &config);
490
491 assert_eq!(resolved.animation_speed, 2.0);
493 assert_eq!(resolved.brightness, 0.9);
494 assert_eq!(resolved.text_opacity, 0.8);
496 }
497
498 #[test]
499 fn global_brightness_override_beats_metadata_default() {
500 let mut config = make_test_config();
501 config.shader.custom_shader_brightness = 0.42;
502 let metadata = ShaderMetadata {
503 name: Some("Test".to_string()),
504 defaults: ShaderConfig {
505 brightness: Some(0.7),
506 ..Default::default()
507 },
508 ..Default::default()
509 };
510
511 let resolved = resolve_shader_config(None, Some(&metadata), &config);
512
513 assert_eq!(resolved.brightness, 0.42);
514 }
515
516 #[test]
517 fn resolve_custom_uniforms_user_override_beats_metadata_default() {
518 let config = make_test_config();
519 let user_override = ShaderConfig {
520 uniforms: BTreeMap::from([
521 ("iGlow".to_string(), ShaderUniformValue::Float(0.9)),
522 ("iUserOnly".to_string(), ShaderUniformValue::Bool(true)),
523 ]),
524 ..Default::default()
525 };
526 let metadata = ShaderMetadata {
527 defaults: ShaderConfig {
528 uniforms: BTreeMap::from([
529 ("iGlow".to_string(), ShaderUniformValue::Float(0.4)),
530 ("iMetaOnly".to_string(), ShaderUniformValue::Bool(false)),
531 ]),
532 ..Default::default()
533 },
534 ..Default::default()
535 };
536
537 let resolved = resolve_shader_config(Some(&user_override), Some(&metadata), &config);
538
539 assert_eq!(
540 resolved.custom_uniforms.get("iGlow"),
541 Some(&ShaderUniformValue::Float(0.9))
542 );
543 assert_eq!(
544 resolved.custom_uniforms.get("iMetaOnly"),
545 Some(&ShaderUniformValue::Bool(false))
546 );
547 assert_eq!(
548 resolved.custom_uniforms.get("iUserOnly"),
549 Some(&ShaderUniformValue::Bool(true))
550 );
551 }
552
553 #[test]
554 fn resolve_custom_uniforms_metadata_default_used_when_no_override() {
555 let config = make_test_config();
556 let metadata = ShaderMetadata {
557 defaults: ShaderConfig {
558 uniforms: BTreeMap::from([("iGlow".to_string(), ShaderUniformValue::Float(0.4))]),
559 ..Default::default()
560 },
561 ..Default::default()
562 };
563
564 let resolved = resolve_shader_config(None, Some(&metadata), &config);
565
566 assert_eq!(
567 resolved.custom_uniforms.get("iGlow"),
568 Some(&ShaderUniformValue::Float(0.4))
569 );
570 }
571
572 #[test]
573 fn test_shader_config_uniforms_yaml_roundtrip() {
574 let config = ShaderConfig {
575 uniforms: BTreeMap::from([
576 ("iGlow".to_string(), ShaderUniformValue::Float(0.75)),
577 ("iEnabled".to_string(), ShaderUniformValue::Bool(true)),
578 ]),
579 ..Default::default()
580 };
581
582 let yaml = serde_yaml_ng::to_string(&config).expect("serialize shader config");
583 let roundtrip: ShaderConfig =
584 serde_yaml_ng::from_str(&yaml).expect("deserialize shader config");
585
586 assert_eq!(roundtrip, config);
587 }
588
589 #[test]
590 fn test_shader_config_color_uniforms_serialize_as_hex() {
591 let config = ShaderConfig {
592 uniforms: BTreeMap::from([
593 (
594 "iTint".to_string(),
595 ShaderUniformValue::Color(crate::types::shader::ShaderColorValue([
596 1.0, 0.5, 0.0, 1.0,
597 ])),
598 ),
599 (
600 "iOverlay".to_string(),
601 ShaderUniformValue::Color(crate::types::shader::ShaderColorValue([
602 1.0, 0.5, 0.0, 0.8,
603 ])),
604 ),
605 ]),
606 ..Default::default()
607 };
608
609 let yaml = serde_yaml_ng::to_string(&config).expect("serialize shader config");
610
611 assert!(yaml.contains("iTint: '#ff8000'"));
612 assert!(yaml.contains("iOverlay: '#ff8000cc'"));
613 let roundtrip: ShaderConfig =
614 serde_yaml_ng::from_str(&yaml).expect("deserialize shader config");
615 assert_eq!(
616 roundtrip.uniforms.get("iTint"),
617 Some(&ShaderUniformValue::Color(
618 crate::types::shader::ShaderColorValue([1.0, 128.0 / 255.0, 0.0, 1.0])
619 ))
620 );
621 assert_eq!(
622 roundtrip.uniforms.get("iOverlay"),
623 Some(&ShaderUniformValue::Color(
624 crate::types::shader::ShaderColorValue([1.0, 128.0 / 255.0, 0.0, 204.0 / 255.0])
625 ))
626 );
627 }
628
629 #[test]
630 fn test_channel_paths() {
631 let resolved = ResolvedShaderConfig {
632 channel0: Some(PathBuf::from("/path/to/tex0.png")),
633 channel1: None,
634 channel2: Some(PathBuf::from("/path/to/tex2.png")),
635 channel3: None,
636 ..Default::default()
637 };
638
639 let paths = resolved.channel_paths();
640 assert!(paths[0].is_some());
641 assert!(paths[1].is_none());
642 assert!(paths[2].is_some());
643 assert!(paths[3].is_none());
644 }
645
646 #[test]
647 fn test_cubemap_path_respects_enabled() {
648 let mut resolved = ResolvedShaderConfig {
649 cubemap: Some(PathBuf::from("/path/to/cubemap")),
650 cubemap_enabled: true,
651 ..Default::default()
652 };
653
654 assert!(resolved.cubemap_path().is_some());
655
656 resolved.cubemap_enabled = false;
657 assert!(resolved.cubemap_path().is_none());
658 }
659}