use ratatui_core::{
layout::{Layout, Rect},
style::Style,
};
use crate::{
dsl::{environment::DslEnv, expressions::FnCallInfo, Arguments, DslError, EffectDsl},
fx::IntoTemporaryEffect,
pattern::{
AnyPattern, CheckerboardPattern, CoalescePattern, CombinedPattern, DiagonalPattern,
DiamondPattern, DissolvePattern, InvertedPattern, RadialPattern, SpiralPattern,
SweepPattern, WavePattern,
},
wave::{Modulator, Oscillator, WaveLayer},
CellFilter, Effect,
};
pub(super) trait ChainableMethods
where
Self: Sized,
{
fn fold_fns<'dsl>(
self,
self_fns: Vec<FnCallInfo>,
context: &'dsl EffectDsl,
vars: &'dsl DslEnv,
) -> Result<Self, DslError> {
self_fns.into_iter().try_fold(self, |this, f| {
let name = f.name;
let mut args = Arguments::new(f.args.into(), context, vars, f.span);
let result = Self::apply_fn(this, name.as_str(), &mut args);
if args.remaining_arg_count() > 0 {
Err(DslError::TooManyArguments {
name,
count: args.remaining_arg_count(),
location: args.span(),
})
} else {
result
}
})
}
fn apply_fn(object: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError>;
}
impl ChainableMethods for CellFilter {
fn apply_fn(filter: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => filter,
"negated" => filter.negated(),
"into_static" => filter.into_static(),
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for Effect {
fn apply_fn(effect: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => effect,
"reversed" => effect.reversed(),
"with_area" => effect.with_area(args.rect()?),
"with_color_space" => effect.with_color_space(args.color_space()?),
"with_duration" => effect.with_duration(args.duration()?),
"with_filter" | "filter" => effect.with_filter(args.cell_filter()?),
"with_pattern" => effect.with_pattern(args.pattern()?),
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for Layout {
fn apply_fn(layout: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => layout,
"direction" => layout.direction(args.direction()?),
"flex" => layout.flex(args.flex()?),
"constraints" => layout.constraints(args.array(Arguments::constraint)?),
"margin" => layout.margin(args.read_u16()?),
"horizontal_margin" => layout.horizontal_margin(args.read_u16()?),
"vertical_margin" => layout.vertical_margin(args.read_u16()?),
"spacing" => layout.spacing(args.read_u16()?),
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for Style {
fn apply_fn(style: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => style,
"fg" => style.fg(args.color()?),
"bg" => style.bg(args.color()?),
"add_modifier" => style.add_modifier(args.modifier()?),
"remove_modifier" => style.remove_modifier(args.modifier()?),
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for Rect {
fn apply_fn(rect: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => rect,
"clamp" => rect.clamp(args.rect()?),
"inner" => rect.inner(args.margin()?),
"intersection" => rect.intersection(args.rect()?),
"union" => rect.union(args.rect()?),
"offset" => rect.offset(args.offset()?),
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for CheckerboardPattern {
fn apply_fn(pattern: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => pattern,
"with_transition_width" => pattern.with_transition_width(args.read_f32()?),
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for DiagonalPattern {
fn apply_fn(pattern: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => pattern,
"with_transition_width" => pattern.with_transition_width(args.read_f32()?),
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for RadialPattern {
fn apply_fn(pattern: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => pattern,
"with_transition_width" => pattern.with_transition_width(args.read_f32()?),
"with_center" => {
let center_x = args.read_f32()?;
let center_y = args.read_f32()?;
pattern.with_center((center_x, center_y))
},
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for DiamondPattern {
fn apply_fn(pattern: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => pattern,
"with_transition_width" => pattern.with_transition_width(args.read_f32()?),
"with_center" => {
let center_x = args.read_f32()?;
let center_y = args.read_f32()?;
pattern.with_center((center_x, center_y))
},
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for SpiralPattern {
fn apply_fn(pattern: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => pattern,
"with_transition_width" => pattern.with_transition_width(args.read_f32()?),
"with_center" => {
let center_x = args.read_f32()?;
let center_y = args.read_f32()?;
pattern.with_center((center_x, center_y))
},
"with_arms" => pattern.with_arms(args.read_u16()?),
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for SweepPattern {
fn apply_fn(pattern: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => pattern,
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for CoalescePattern {
fn apply_fn(pattern: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => pattern,
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for DissolvePattern {
fn apply_fn(pattern: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => pattern,
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for CombinedPattern {
fn apply_fn(pattern: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => pattern,
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for InvertedPattern {
fn apply_fn(pattern: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => pattern,
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for WavePattern {
fn apply_fn(pattern: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => pattern,
"with_layer" => pattern.with_layer(args.wave_layer()?),
"with_contrast" => pattern.with_contrast(args.read_i32()?),
"with_transition_width" => pattern.with_transition_width(args.read_f32()?),
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for Modulator {
fn apply_fn(modulator: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => modulator,
"phase" => modulator.phase(args.read_f32()?),
"intensity" => modulator.intensity(args.read_f32()?),
"on_phase" => modulator.on_phase(),
"on_amplitude" => modulator.on_amplitude(),
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for Oscillator {
fn apply_fn(osc: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => osc,
"phase" => osc.phase(args.read_f32()?),
"modulated_by" => osc.modulated_by(args.modulator()?),
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for WaveLayer {
fn apply_fn(layer: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match name {
"clone" => layer,
"multiply" => layer.multiply(args.oscillator()?),
"average" => layer.average(args.oscillator()?),
"max" => layer.max(args.oscillator()?),
"amplitude" => layer.amplitude(args.read_f32()?),
"power" => layer.power(args.read_i32()?),
"abs" => layer.abs(),
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
})
}
}
impl ChainableMethods for AnyPattern {
fn apply_fn(pattern: Self, name: &str, args: &mut Arguments<'_>) -> Result<Self, DslError> {
Ok(match pattern {
AnyPattern::Identity => match name {
"clone" => pattern,
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
},
AnyPattern::Radial(inner) => {
AnyPattern::Radial(RadialPattern::apply_fn(inner, name, args)?)
},
AnyPattern::Diamond(inner) => {
AnyPattern::Diamond(DiamondPattern::apply_fn(inner, name, args)?)
},
AnyPattern::Diagonal(inner) => {
AnyPattern::Diagonal(DiagonalPattern::apply_fn(inner, name, args)?)
},
AnyPattern::Checkerboard(inner) => {
AnyPattern::Checkerboard(CheckerboardPattern::apply_fn(inner, name, args)?)
},
AnyPattern::Sweep(inner) => {
AnyPattern::Sweep(SweepPattern::apply_fn(inner, name, args)?)
},
AnyPattern::Coalesce(inner) => {
AnyPattern::Coalesce(CoalescePattern::apply_fn(inner, name, args)?)
},
AnyPattern::Dissolve(inner) => {
AnyPattern::Dissolve(DissolvePattern::apply_fn(inner, name, args)?)
},
AnyPattern::Wave(inner) => AnyPattern::Wave(WavePattern::apply_fn(inner, name, args)?),
AnyPattern::Spiral(inner) => {
AnyPattern::Spiral(SpiralPattern::apply_fn(inner, name, args)?)
},
AnyPattern::Combined(inner) => {
AnyPattern::Combined(CombinedPattern::apply_fn(inner, name, args)?)
},
AnyPattern::Inverted(inner) => {
AnyPattern::Inverted(InvertedPattern::apply_fn(inner, name, args)?)
},
AnyPattern::Blend(_) => match name {
"clone" => pattern,
_ => Err(DslError::UnknownFunction { name: name.into(), location: args.span() })?,
},
})
}
}
#[cfg(test)]
mod tests {
use alloc::collections::VecDeque;
use super::*;
use crate::{
dsl::{
environment::DslEnv,
expressions::{Expr, ExprSpan, Value},
DslFormat, EffectDsl,
},
Duration,
};
fn create_test_args<'dsl>(
values: Vec<Value>,
context: &'dsl EffectDsl,
vars: &'dsl DslEnv,
) -> Arguments<'dsl> {
let exprs: VecDeque<Expr> = values
.into_iter()
.map(|v| Expr::Literal(v, ExprSpan { start: 0, end: 1 }))
.collect();
Arguments::new(exprs, context, vars, ExprSpan { start: 0, end: 1 })
}
#[test]
fn test_radial_pattern_method_chaining() {
let context = EffectDsl::new();
let vars = DslEnv::new();
let pattern = RadialPattern::center();
let mut args = create_test_args(vec![Value::F32(3.5)], &context, &vars);
let result = RadialPattern::apply_fn(pattern, "with_transition_width", &mut args).unwrap();
assert!(result.dsl_format().contains("3.5"));
let pattern = RadialPattern::center();
let mut args = create_test_args(vec![Value::F32(0.3), Value::F32(0.7)], &context, &vars);
let result = RadialPattern::apply_fn(pattern, "with_center", &mut args).unwrap();
let formatted = result.dsl_format();
assert!(formatted.contains("0.3") && formatted.contains("0.7"));
let pattern = RadialPattern::center();
let mut args = create_test_args(vec![], &context, &vars);
let result = RadialPattern::apply_fn(pattern, "clone", &mut args).unwrap();
assert_eq!(result.dsl_format(), "RadialPattern::center()");
let pattern = RadialPattern::center();
let mut args = create_test_args(vec![], &context, &vars);
let result = RadialPattern::apply_fn(pattern, "unknown_method", &mut args);
assert!(result.is_err());
}
#[test]
fn test_any_pattern_method_chaining() {
let context = EffectDsl::new();
let vars = DslEnv::new();
let pattern = AnyPattern::Radial(RadialPattern::center());
let mut args = create_test_args(vec![Value::F32(4.0)], &context, &vars);
let result = AnyPattern::apply_fn(pattern, "with_transition_width", &mut args).unwrap();
if let AnyPattern::Radial(radial) = result {
assert!(radial.dsl_format().contains("4"));
} else {
panic!("Expected AnyPattern::Radial");
}
let pattern = AnyPattern::Identity;
let mut args = create_test_args(vec![], &context, &vars);
let result = AnyPattern::apply_fn(pattern, "clone", &mut args).unwrap();
assert!(matches!(result, AnyPattern::Identity));
let pattern = AnyPattern::Identity;
let mut args = create_test_args(vec![], &context, &vars);
let result = AnyPattern::apply_fn(pattern, "unknown_method", &mut args);
assert!(result.is_err());
}
#[test]
fn test_effect_with_pattern_method_chaining() {
use crate::fx;
let context = EffectDsl::new();
let vars = DslEnv::new();
let effect = fx::dissolve(Duration::from_millis(1000));
let _radial_pattern =
AnyPattern::Radial(RadialPattern::center().with_transition_width(3.0));
let pattern_expr = Expr::FnCall {
call: crate::dsl::expressions::FnCallInfo {
name: "RadialPattern::center".into(),
args: Vec::new(),
span: ExprSpan { start: 0, end: 1 },
},
self_fns: vec![],
};
let mut args = create_test_args(vec![], &context, &vars);
let exprs: VecDeque<Expr> = vec![pattern_expr].into();
let _pattern_args = Arguments::new(exprs, &context, &vars, ExprSpan { start: 0, end: 1 });
let result = Effect::apply_fn(effect, "with_pattern", &mut args);
assert!(result.is_err());
let effect2 = fx::dissolve(Duration::from_millis(500));
let mut args2 = create_test_args(vec![], &context, &vars);
let result2 = Effect::apply_fn(effect2, "unknown_method", &mut args2);
assert!(result2.is_err());
}
}