use syn::{Error, Expr, Field, Lit, Result as SynResult, Type, TypePath};
#[derive(Debug, Clone)]
pub enum ControlType {
Range { min: f32, max: f32 },
IntRange { min: i32, max: i32 },
Toggle,
VectorRange { components: Vec<(u8, f32, f32)> }, LinearRgba, Srgba, Hsla, Hsva, }
#[derive(Debug, Clone)]
pub enum RangeSpec {
Float(f32, f32),
Int(i32, i32),
}
#[derive(Debug, Clone)]
pub struct MidiMapping {
pub cc: u8,
pub range: Option<RangeSpec>,
}
pub fn parse_midi_attributes(field: &Field) -> SynResult<Vec<MidiMapping>> {
let mut mappings = Vec::new();
for attr in &field.attrs {
if attr.path().is_ident("midi") {
let mapping = attr.parse_args_with(|input: syn::parse::ParseStream| {
let cc: syn::LitInt = input.parse()?;
let cc_value = cc.base10_parse::<u8>()?;
let range = if input.peek(syn::Token![,]) {
input.parse::<syn::Token![,]>()?;
let expr: Expr = input.parse()?;
Some(parse_range_expr(&expr)?)
} else {
None
};
Ok(MidiMapping {
cc: cc_value,
range,
})
})?;
mappings.push(mapping);
}
}
Ok(mappings)
}
pub fn determine_control_type(ty: &Type, mappings: &[MidiMapping]) -> SynResult<ControlType> {
let type_name = get_type_name(ty);
if let Some(ref name) = type_name {
match name.as_str() {
"LinearRgba" => {
if mappings.len() != 4 {
return Err(Error::new_spanned(
ty,
"LinearRgba requires exactly 4 #[midi] attributes (r, g, b, a)",
));
}
return Ok(ControlType::LinearRgba);
}
"Srgba" => {
if mappings.len() != 4 {
return Err(Error::new_spanned(
ty,
"Srgba requires exactly 4 #[midi] attributes (r, g, b, a)",
));
}
return Ok(ControlType::Srgba);
}
"Hsla" => {
if mappings.len() != 4 {
return Err(Error::new_spanned(
ty,
"Hsla requires exactly 4 #[midi] attributes (h, s, l, a)",
));
}
return Ok(ControlType::Hsla);
}
"Hsva" => {
if mappings.len() != 4 {
return Err(Error::new_spanned(
ty,
"Hsva requires exactly 4 #[midi] attributes (h, s, v, a)",
));
}
return Ok(ControlType::Hsva);
}
_ => {}
}
}
if let Some(vec_dim) = get_vector_dimension(ty) {
if mappings.len() != vec_dim {
return Err(Error::new_spanned(
ty,
format!(
"Vector type {} requires exactly {} #[midi] attributes",
type_name.unwrap_or_else(|| "Vec".to_string()),
vec_dim
),
));
}
let mut components = Vec::new();
for mapping in mappings {
let (min, max) = if let Some(ref range) = mapping.range {
match range {
RangeSpec::Float(min, max) => (*min, *max),
RangeSpec::Int(min, max) => (*min as f32, *max as f32),
}
} else {
(0.0, 1.0) };
components.push((mapping.cc, min, max));
}
return Ok(ControlType::VectorRange { components });
}
if mappings.len() != 1 {
return Err(Error::new_spanned(
ty,
"Scalar types require exactly one #[midi] attribute",
));
}
let mapping = &mappings[0];
if is_bool_type(ty) {
return Ok(ControlType::Toggle);
}
if let Some(numeric_type) = get_numeric_type(ty) {
match numeric_type {
NumericType::Float => {
let (min, max) = if let Some(ref range) = mapping.range {
match range {
RangeSpec::Float(min, max) => (*min, *max),
RangeSpec::Int(min, max) => (*min as f32, *max as f32),
}
} else {
(0.0, 1.0) };
Ok(ControlType::Range { min, max })
}
NumericType::SignedInt => {
let (min, max) = if let Some(ref range) = mapping.range {
match range {
RangeSpec::Int(min, max) => (*min, *max),
RangeSpec::Float(min, max) => (*min as i32, *max as i32),
}
} else {
(0, 127) };
Ok(ControlType::IntRange { min, max })
}
}
} else {
Err(Error::new_spanned(
ty,
"MIDI parameters only support bool, f32, f64, i32, vector types (Vec2, Vec3, Vec4), and Bevy color types (LinearRgba, Srgba, Hsla, Hsva)",
))
}
}
fn parse_range_expr(expr: &Expr) -> SynResult<RangeSpec> {
if let Expr::Range(range) = expr {
let start = range
.start
.as_ref()
.ok_or_else(|| Error::new_spanned(expr, "Range must have a start value"))?;
let end = range
.end
.as_ref()
.ok_or_else(|| Error::new_spanned(expr, "Range must have an end value"))?;
if let (Some(min), Some(max)) = (parse_int_expr(start), parse_int_expr(end)) {
return Ok(RangeSpec::Int(min, max));
}
let min = parse_float_expr(start)
.ok_or_else(|| Error::new_spanned(start, "Invalid range start value"))?;
let max = parse_float_expr(end)
.ok_or_else(|| Error::new_spanned(end, "Invalid range end value"))?;
Ok(RangeSpec::Float(min, max))
} else {
Err(Error::new_spanned(
expr,
"Expected range expression (e.g., 0.0..1.0 or 0..127)",
))
}
}
fn parse_float_expr(expr: &Expr) -> Option<f32> {
match expr {
Expr::Lit(lit) => {
if let Lit::Float(f) = &lit.lit {
f.base10_parse().ok()
} else if let Lit::Int(i) = &lit.lit {
i.base10_parse::<i32>().ok().map(|v| v as f32)
} else {
None
}
}
Expr::Unary(unary) => {
if let syn::UnOp::Neg(_) = unary.op {
parse_float_expr(&unary.expr).map(|v| -v)
} else {
None
}
}
_ => None,
}
}
fn parse_int_expr(expr: &Expr) -> Option<i32> {
match expr {
Expr::Lit(lit) => {
if let Lit::Int(i) = &lit.lit {
i.base10_parse().ok()
} else {
None
}
}
Expr::Unary(unary) => {
if let syn::UnOp::Neg(_) = unary.op {
parse_int_expr(&unary.expr).map(|v| -v)
} else {
None
}
}
_ => None,
}
}
fn is_bool_type(ty: &Type) -> bool {
if let Type::Path(TypePath { path, .. }) = ty {
if let Some(segment) = path.segments.last() {
return segment.ident == "bool";
}
}
false
}
fn get_type_name(ty: &Type) -> Option<String> {
if let Type::Path(TypePath { path, .. }) = ty {
if let Some(segment) = path.segments.last() {
return Some(segment.ident.to_string());
}
}
None
}
fn get_vector_dimension(ty: &Type) -> Option<usize> {
get_type_name(ty).and_then(|name| match name.as_str() {
"Vec2" => Some(2),
"Vec3" => Some(3),
"Vec4" => Some(4),
_ => None,
})
}
#[derive(Debug, Clone, Copy)]
enum NumericType {
Float,
SignedInt,
}
fn get_numeric_type(ty: &Type) -> Option<NumericType> {
if let Type::Path(TypePath { path, .. }) = ty {
if let Some(segment) = path.segments.last() {
let ident_str = segment.ident.to_string();
match ident_str.as_str() {
"f32" | "f64" => Some(NumericType::Float),
"i32" => Some(NumericType::SignedInt),
_ => None,
}
} else {
None
}
} else {
None
}
}