use crate::config::Config;
use crate::types::{
CursorShaderConfig, CursorShaderMetadata, ResolvedCursorShaderConfig, ResolvedShaderConfig,
ShaderBackgroundBlendMode, ShaderConfig, ShaderMetadata,
};
use std::collections::BTreeMap;
use std::path::PathBuf;
pub fn resolve_shader_config(
user_override: Option<&ShaderConfig>,
metadata: Option<&ShaderMetadata>,
config: &Config,
) -> ResolvedShaderConfig {
let meta_defaults = metadata.map(|m| &m.defaults);
macro_rules! resolve {
($field:ident, $global:expr) => {
user_override
.and_then(|o| o.$field.clone())
.or_else(|| meta_defaults.and_then(|m| m.$field.clone()))
.unwrap_or($global)
};
}
macro_rules! resolve_path {
($field:ident, $global:expr) => {{
if let Some(override_val) = user_override.and_then(|o| o.$field.clone()) {
if override_val.is_empty() {
None } else {
Some(Config::resolve_texture_path(&override_val))
}
} else {
let path_str: Option<String> =
meta_defaults.and_then(|m| m.$field.clone()).or($global);
path_str
.filter(|p| !p.is_empty())
.map(|p| Config::resolve_texture_path(&p))
}
}};
}
let mut custom_uniforms = metadata
.map(|m| m.defaults.uniforms.clone())
.unwrap_or_default();
if let Some(user_override) = user_override {
custom_uniforms.extend(user_override.uniforms.clone());
}
let global_brightness = config.shader.custom_shader_brightness;
let default_brightness = crate::defaults::custom_shader_brightness();
let brightness = user_override
.and_then(|override_config| override_config.brightness)
.or_else(|| {
if (global_brightness - default_brightness).abs() > f32::EPSILON {
Some(global_brightness)
} else {
meta_defaults.and_then(|defaults| defaults.brightness)
}
})
.unwrap_or(global_brightness);
ResolvedShaderConfig {
animation_speed: resolve!(animation_speed, config.shader.custom_shader_animation_speed),
brightness,
text_opacity: resolve!(text_opacity, config.shader.custom_shader_text_opacity),
full_content: resolve!(full_content, config.shader.custom_shader_full_content),
channel0: resolve_path!(channel0, config.shader.custom_shader_channel0.clone()),
channel1: resolve_path!(channel1, config.shader.custom_shader_channel1.clone()),
channel2: resolve_path!(channel2, config.shader.custom_shader_channel2.clone()),
channel3: resolve_path!(channel3, config.shader.custom_shader_channel3.clone()),
cubemap: resolve_path!(cubemap, config.shader.custom_shader_cubemap.clone()),
cubemap_enabled: resolve!(cubemap_enabled, config.shader.custom_shader_cubemap_enabled),
use_background_as_channel0: resolve!(
use_background_as_channel0,
config.shader.custom_shader_use_background_as_channel0
),
background_channel0_blend_mode: resolve!(
background_channel0_blend_mode,
config.shader.custom_shader_background_channel0_blend_mode
),
auto_dim_under_text: resolve!(
auto_dim_under_text,
config.shader.custom_shader_auto_dim_under_text
),
auto_dim_strength: resolve!(
auto_dim_strength,
config.shader.custom_shader_auto_dim_strength
)
.clamp(0.0, 1.0),
custom_uniforms,
}
}
pub fn resolve_cursor_shader_config(
user_override: Option<&CursorShaderConfig>,
metadata: Option<&CursorShaderMetadata>,
config: &Config,
) -> ResolvedCursorShaderConfig {
let meta_defaults = metadata.map(|m| &m.defaults);
macro_rules! resolve_cursor {
($field:ident, $global:expr) => {
user_override
.and_then(|o| o.$field)
.or_else(|| meta_defaults.and_then(|m| m.$field))
.unwrap_or($global)
};
}
let animation_speed = user_override
.and_then(|o| o.base.animation_speed)
.or_else(|| meta_defaults.and_then(|m| m.base.animation_speed))
.unwrap_or(config.shader.cursor_shader_animation_speed);
let base = ResolvedShaderConfig {
animation_speed,
brightness: 1.0,
text_opacity: 1.0,
full_content: true, channel0: None,
channel1: None,
channel2: None,
channel3: None,
cubemap: None,
cubemap_enabled: false,
use_background_as_channel0: false,
background_channel0_blend_mode: ShaderBackgroundBlendMode::Replace,
auto_dim_under_text: false,
auto_dim_strength: 0.35,
custom_uniforms: BTreeMap::new(),
};
let hides_cursor = resolve_cursor!(hides_cursor, config.shader.cursor_shader_hides_cursor);
let disable_in_alt_screen = resolve_cursor!(
disable_in_alt_screen,
config.shader.cursor_shader_disable_in_alt_screen
);
let glow_radius = resolve_cursor!(glow_radius, config.shader.cursor_shader_glow_radius);
let glow_intensity =
resolve_cursor!(glow_intensity, config.shader.cursor_shader_glow_intensity);
let trail_duration =
resolve_cursor!(trail_duration, config.shader.cursor_shader_trail_duration);
let cursor_color = user_override
.and_then(|o| o.cursor_color)
.or_else(|| meta_defaults.and_then(|m| m.cursor_color))
.unwrap_or(config.shader.cursor_shader_color);
ResolvedCursorShaderConfig {
base,
hides_cursor,
disable_in_alt_screen,
glow_radius,
glow_intensity,
trail_duration,
cursor_color,
}
}
impl ResolvedShaderConfig {
pub fn for_shader(
shader_name: &str,
metadata: Option<&ShaderMetadata>,
config: &Config,
) -> Self {
let user_override = config.get_shader_override(shader_name);
resolve_shader_config(user_override, metadata, config)
}
pub fn channel_paths(&self) -> [Option<PathBuf>; 4] {
[
self.channel0.clone(),
self.channel1.clone(),
self.channel2.clone(),
self.channel3.clone(),
]
}
pub fn cubemap_path(&self) -> Option<&PathBuf> {
if self.cubemap_enabled {
self.cubemap.as_ref()
} else {
None
}
}
}
impl ResolvedCursorShaderConfig {
pub fn for_shader(
shader_name: &str,
metadata: Option<&CursorShaderMetadata>,
config: &Config,
) -> Self {
let user_override = config.get_cursor_shader_override(shader_name);
resolve_cursor_shader_config(user_override, metadata, config)
}
}
pub mod global_defaults {
pub const ANIMATION_SPEED: f32 = 1.0;
pub const BRIGHTNESS: f32 = 1.0;
pub const TEXT_OPACITY: f32 = 1.0;
pub const FULL_CONTENT: bool = false;
pub const CUBEMAP_ENABLED: bool = true;
pub const GLOW_RADIUS: f32 = 80.0;
pub const GLOW_INTENSITY: f32 = 0.3;
pub const TRAIL_DURATION: f32 = 0.5;
pub const CURSOR_COLOR: [u8; 3] = [255, 255, 255];
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ShaderBackgroundBlendMode, ShaderConfig, ShaderUniformValue};
use std::collections::BTreeMap;
fn make_test_config() -> Config {
Config::default()
}
#[test]
fn resolves_background_channel0_blend_mode_from_global_default() {
let config = Config::default();
let resolved = resolve_shader_config(None, None, &config);
assert_eq!(
resolved.background_channel0_blend_mode,
ShaderBackgroundBlendMode::Replace
);
}
#[test]
fn resolves_background_channel0_blend_mode_override_over_metadata() {
let config = Config::default();
let metadata = ShaderMetadata {
name: Some("Blend Metadata".to_string()),
defaults: ShaderConfig {
background_channel0_blend_mode: Some(ShaderBackgroundBlendMode::Multiply),
..Default::default()
},
..Default::default()
};
let override_config = ShaderConfig {
background_channel0_blend_mode: Some(ShaderBackgroundBlendMode::Screen),
..Default::default()
};
let resolved = resolve_shader_config(Some(&override_config), Some(&metadata), &config);
assert_eq!(
resolved.background_channel0_blend_mode,
ShaderBackgroundBlendMode::Screen
);
}
#[test]
fn preserves_builtin_texture_ids_from_shader_config_sources() {
const BUILTIN: &str = "builtin://noise/value-256";
let mut global_config = Config::default();
global_config.shader.custom_shader_channel0 = Some(BUILTIN.to_string());
let resolved = resolve_shader_config(None, None, &global_config);
assert_eq!(
resolved
.channel0
.expect("global channel0")
.display()
.to_string(),
BUILTIN
);
let metadata = ShaderMetadata {
defaults: ShaderConfig {
channel0: Some(BUILTIN.to_string()),
..Default::default()
},
..Default::default()
};
let resolved = resolve_shader_config(None, Some(&metadata), &Config::default());
assert_eq!(
resolved
.channel0
.expect("metadata channel0")
.display()
.to_string(),
BUILTIN
);
let mut override_config = Config::default();
override_config.shader_configs.insert(
"test.glsl".to_string(),
ShaderConfig {
channel0: Some(BUILTIN.to_string()),
..Default::default()
},
);
let resolved = ResolvedShaderConfig::for_shader("test.glsl", None, &override_config);
assert_eq!(
resolved
.channel0
.expect("override channel0")
.display()
.to_string(),
BUILTIN
);
}
#[test]
fn test_resolve_with_no_overrides() {
let config = make_test_config();
let resolved = resolve_shader_config(None, None, &config);
assert_eq!(
resolved.animation_speed,
config.shader.custom_shader_animation_speed
);
assert_eq!(resolved.brightness, config.shader.custom_shader_brightness);
assert_eq!(
resolved.text_opacity,
config.shader.custom_shader_text_opacity
);
assert_eq!(
resolved.full_content,
config.shader.custom_shader_full_content
);
}
#[test]
fn test_resolve_with_metadata_defaults() {
let config = make_test_config();
let shader_defaults = ShaderConfig {
animation_speed: Some(0.5),
brightness: Some(0.7),
..Default::default()
};
let metadata = ShaderMetadata {
name: Some("Test".to_string()),
defaults: shader_defaults,
..Default::default()
};
let resolved = resolve_shader_config(None, Some(&metadata), &config);
assert_eq!(resolved.animation_speed, 0.5);
assert_eq!(resolved.brightness, 0.7);
assert_eq!(
resolved.text_opacity,
config.shader.custom_shader_text_opacity
);
}
#[test]
fn test_resolve_with_user_override() {
let config = make_test_config();
let user_override = ShaderConfig {
animation_speed: Some(2.0),
brightness: Some(0.9),
..Default::default()
};
let shader_defaults = ShaderConfig {
animation_speed: Some(0.5), text_opacity: Some(0.8), ..Default::default()
};
let metadata = ShaderMetadata {
name: Some("Test".to_string()),
defaults: shader_defaults,
..Default::default()
};
let resolved = resolve_shader_config(Some(&user_override), Some(&metadata), &config);
assert_eq!(resolved.animation_speed, 2.0);
assert_eq!(resolved.brightness, 0.9);
assert_eq!(resolved.text_opacity, 0.8);
}
#[test]
fn global_brightness_override_beats_metadata_default() {
let mut config = make_test_config();
config.shader.custom_shader_brightness = 0.42;
let metadata = ShaderMetadata {
name: Some("Test".to_string()),
defaults: ShaderConfig {
brightness: Some(0.7),
..Default::default()
},
..Default::default()
};
let resolved = resolve_shader_config(None, Some(&metadata), &config);
assert_eq!(resolved.brightness, 0.42);
}
#[test]
fn resolve_custom_uniforms_user_override_beats_metadata_default() {
let config = make_test_config();
let user_override = ShaderConfig {
uniforms: BTreeMap::from([
("iGlow".to_string(), ShaderUniformValue::Float(0.9)),
("iUserOnly".to_string(), ShaderUniformValue::Bool(true)),
]),
..Default::default()
};
let metadata = ShaderMetadata {
defaults: ShaderConfig {
uniforms: BTreeMap::from([
("iGlow".to_string(), ShaderUniformValue::Float(0.4)),
("iMetaOnly".to_string(), ShaderUniformValue::Bool(false)),
]),
..Default::default()
},
..Default::default()
};
let resolved = resolve_shader_config(Some(&user_override), Some(&metadata), &config);
assert_eq!(
resolved.custom_uniforms.get("iGlow"),
Some(&ShaderUniformValue::Float(0.9))
);
assert_eq!(
resolved.custom_uniforms.get("iMetaOnly"),
Some(&ShaderUniformValue::Bool(false))
);
assert_eq!(
resolved.custom_uniforms.get("iUserOnly"),
Some(&ShaderUniformValue::Bool(true))
);
}
#[test]
fn resolve_custom_uniforms_metadata_default_used_when_no_override() {
let config = make_test_config();
let metadata = ShaderMetadata {
defaults: ShaderConfig {
uniforms: BTreeMap::from([("iGlow".to_string(), ShaderUniformValue::Float(0.4))]),
..Default::default()
},
..Default::default()
};
let resolved = resolve_shader_config(None, Some(&metadata), &config);
assert_eq!(
resolved.custom_uniforms.get("iGlow"),
Some(&ShaderUniformValue::Float(0.4))
);
}
#[test]
fn test_shader_config_uniforms_yaml_roundtrip() {
let config = ShaderConfig {
uniforms: BTreeMap::from([
("iGlow".to_string(), ShaderUniformValue::Float(0.75)),
("iEnabled".to_string(), ShaderUniformValue::Bool(true)),
]),
..Default::default()
};
let yaml = serde_yaml_ng::to_string(&config).expect("serialize shader config");
let roundtrip: ShaderConfig =
serde_yaml_ng::from_str(&yaml).expect("deserialize shader config");
assert_eq!(roundtrip, config);
}
#[test]
fn test_shader_config_color_uniforms_serialize_as_hex() {
let config = ShaderConfig {
uniforms: BTreeMap::from([
(
"iTint".to_string(),
ShaderUniformValue::Color(crate::types::shader::ShaderColorValue([
1.0, 0.5, 0.0, 1.0,
])),
),
(
"iOverlay".to_string(),
ShaderUniformValue::Color(crate::types::shader::ShaderColorValue([
1.0, 0.5, 0.0, 0.8,
])),
),
]),
..Default::default()
};
let yaml = serde_yaml_ng::to_string(&config).expect("serialize shader config");
assert!(yaml.contains("iTint: '#ff8000'"));
assert!(yaml.contains("iOverlay: '#ff8000cc'"));
let roundtrip: ShaderConfig =
serde_yaml_ng::from_str(&yaml).expect("deserialize shader config");
assert_eq!(
roundtrip.uniforms.get("iTint"),
Some(&ShaderUniformValue::Color(
crate::types::shader::ShaderColorValue([1.0, 128.0 / 255.0, 0.0, 1.0])
))
);
assert_eq!(
roundtrip.uniforms.get("iOverlay"),
Some(&ShaderUniformValue::Color(
crate::types::shader::ShaderColorValue([1.0, 128.0 / 255.0, 0.0, 204.0 / 255.0])
))
);
}
#[test]
fn test_channel_paths() {
let resolved = ResolvedShaderConfig {
channel0: Some(PathBuf::from("/path/to/tex0.png")),
channel1: None,
channel2: Some(PathBuf::from("/path/to/tex2.png")),
channel3: None,
..Default::default()
};
let paths = resolved.channel_paths();
assert!(paths[0].is_some());
assert!(paths[1].is_none());
assert!(paths[2].is_some());
assert!(paths[3].is_none());
}
#[test]
fn test_cubemap_path_respects_enabled() {
let mut resolved = ResolvedShaderConfig {
cubemap: Some(PathBuf::from("/path/to/cubemap")),
cubemap_enabled: true,
..Default::default()
};
assert!(resolved.cubemap_path().is_some());
resolved.cubemap_enabled = false;
assert!(resolved.cubemap_path().is_none());
}
}