use crate::LayoutAnimationConfig;
use std::collections::HashMap;
use web_sys::DomRect;
#[derive(Debug, Clone)]
pub struct LayoutPreset {
pub name: String,
pub description: String,
pub preset_type: PresetType,
pub default_config: LayoutAnimationConfig,
pub required_parameters: Vec<PresetParameter>,
pub optional_parameters: Vec<PresetParameter>,
}
#[derive(Debug, Clone)]
pub struct PresetParameter {
pub name: String,
pub description: String,
pub parameter_type: ParameterType,
pub default_value: Option<String>,
pub required: bool,
}
#[derive(Debug, Clone)]
pub enum PresetType {
ListReorder,
GridResize,
ResponsiveBreakpoint,
ModalExpand,
CardFlip,
Custom(String),
}
#[derive(Debug, Clone)]
pub enum ParameterType {
Number,
String,
Boolean,
Duration,
Easing,
}
pub struct LayoutPresetManager {
built_in_presets: HashMap<String, LayoutPreset>,
custom_presets: HashMap<String, LayoutPreset>,
}
impl LayoutPresetManager {
pub fn new() -> Self {
let mut manager = Self {
built_in_presets: HashMap::new(),
custom_presets: HashMap::new(),
};
manager.initialize_built_in_presets();
manager
}
fn initialize_built_in_presets(&mut self) {
let list_reorder = LayoutPreset {
name: "list-reorder".to_string(),
description: "Smooth animation for reordering list items".to_string(),
preset_type: PresetType::ListReorder,
default_config: LayoutAnimationConfig {
duration: 300.0,
easing: "ease-out".to_string(),
delay: 0.0,
stagger: 50.0,
..Default::default()
},
required_parameters: vec![PresetParameter {
name: "direction".to_string(),
description: "Reorder direction (horizontal/vertical)".to_string(),
parameter_type: ParameterType::String,
default_value: Some("vertical".to_string()),
required: false,
}],
optional_parameters: vec![PresetParameter {
name: "stagger".to_string(),
description: "Delay between item animations".to_string(),
parameter_type: ParameterType::Duration,
default_value: Some("50".to_string()),
required: false,
}],
};
let grid_resize = LayoutPreset {
name: "grid-resize".to_string(),
description: "Animated grid column/row changes".to_string(),
preset_type: PresetType::GridResize,
default_config: LayoutAnimationConfig {
duration: 400.0,
easing: "ease-in-out".to_string(),
delay: 0.0,
stagger: 0.0,
..Default::default()
},
required_parameters: vec![PresetParameter {
name: "columns".to_string(),
description: "Target number of columns".to_string(),
parameter_type: ParameterType::Number,
default_value: None,
required: true,
}],
optional_parameters: vec![PresetParameter {
name: "maintain_aspect".to_string(),
description: "Maintain item aspect ratios".to_string(),
parameter_type: ParameterType::Boolean,
default_value: Some("true".to_string()),
required: false,
}],
};
let responsive_breakpoint = LayoutPreset {
name: "responsive-breakpoint".to_string(),
description: "Smooth transition between responsive layouts".to_string(),
preset_type: PresetType::ResponsiveBreakpoint,
default_config: LayoutAnimationConfig {
duration: 500.0,
easing: "ease-in-out".to_string(),
delay: 0.0,
stagger: 0.0,
..Default::default()
},
required_parameters: vec![PresetParameter {
name: "breakpoint".to_string(),
description: "Target breakpoint (sm/md/lg/xl)".to_string(),
parameter_type: ParameterType::String,
default_value: None,
required: true,
}],
optional_parameters: vec![PresetParameter {
name: "preserve_content".to_string(),
description: "Preserve content during transition".to_string(),
parameter_type: ParameterType::Boolean,
default_value: Some("true".to_string()),
required: false,
}],
};
let modal_expand = LayoutPreset {
name: "modal-expand".to_string(),
description: "Expand/collapse modal with backdrop".to_string(),
preset_type: PresetType::ModalExpand,
default_config: LayoutAnimationConfig {
duration: 250.0,
easing: "ease-out".to_string(),
delay: 0.0,
stagger: 0.0,
..Default::default()
},
required_parameters: vec![PresetParameter {
name: "action".to_string(),
description: "Action to perform (expand/collapse)".to_string(),
parameter_type: ParameterType::String,
default_value: None,
required: true,
}],
optional_parameters: vec![PresetParameter {
name: "backdrop".to_string(),
description: "Animate backdrop separately".to_string(),
parameter_type: ParameterType::Boolean,
default_value: Some("true".to_string()),
required: false,
}],
};
let card_flip = LayoutPreset {
name: "card-flip".to_string(),
description: "3D card flip animation".to_string(),
preset_type: PresetType::CardFlip,
default_config: LayoutAnimationConfig {
duration: 600.0,
easing: "ease-in-out".to_string(),
delay: 0.0,
stagger: 0.0,
..Default::default()
},
required_parameters: vec![PresetParameter {
name: "axis".to_string(),
description: "Flip axis (x/y)".to_string(),
parameter_type: ParameterType::String,
default_value: Some("y".to_string()),
required: false,
}],
optional_parameters: vec![PresetParameter {
name: "perspective".to_string(),
description: "3D perspective distance".to_string(),
parameter_type: ParameterType::Number,
default_value: Some("1000".to_string()),
required: false,
}],
};
self.built_in_presets
.insert("list-reorder".to_string(), list_reorder);
self.built_in_presets
.insert("grid-resize".to_string(), grid_resize);
self.built_in_presets
.insert("responsive-breakpoint".to_string(), responsive_breakpoint);
self.built_in_presets
.insert("modal-expand".to_string(), modal_expand);
self.built_in_presets
.insert("card-flip".to_string(), card_flip);
}
pub fn get_built_in_preset(&self, name: &str) -> Option<&LayoutPreset> {
self.built_in_presets.get(name)
}
pub fn get_built_in_preset_names(&self) -> Vec<&String> {
self.built_in_presets.keys().collect()
}
pub fn add_custom_preset(&mut self, preset: LayoutPreset) -> Result<(), String> {
self.validate_preset(&preset)?;
if self.built_in_presets.contains_key(&preset.name) {
return Err("Cannot override built-in preset".to_string());
}
if self.custom_presets.contains_key(&preset.name) {
return Err("Custom preset with this name already exists".to_string());
}
self.custom_presets.insert(preset.name.clone(), preset);
Ok(())
}
pub fn get_custom_preset(&self, name: &str) -> Option<&LayoutPreset> {
self.custom_presets.get(name)
}
pub fn remove_custom_preset(&mut self, name: &str) -> Result<(), String> {
if self.custom_presets.remove(name).is_some() {
Ok(())
} else {
Err("Custom preset not found".to_string())
}
}
pub fn get_custom_preset_names(&self) -> Vec<&String> {
self.custom_presets.keys().collect()
}
pub fn calculate_target_layout(
&self,
preset: &LayoutPreset,
current_layout: &DomRect,
parameters: &HashMap<String, String>,
) -> Result<DomRect, String> {
match &preset.preset_type {
PresetType::ListReorder => {
Ok(current_layout.clone())
}
PresetType::GridResize => {
let column_count = parameters
.get("columns")
.ok_or("Missing required parameter: columns")?
.parse::<f64>()
.map_err(|_| "Invalid column count")?;
let _new_width = current_layout.width() * column_count;
Ok(current_layout.clone())
}
PresetType::ResponsiveBreakpoint => {
let breakpoint = parameters
.get("breakpoint")
.ok_or("Missing required parameter: breakpoint")?;
log::info!("Calculating layout for breakpoint: {}", breakpoint);
Ok(current_layout.clone())
}
PresetType::ModalExpand => {
let action = parameters
.get("action")
.ok_or("Missing required parameter: action")?;
log::info!("Calculating layout for modal action: {}", action);
Ok(current_layout.clone())
}
PresetType::CardFlip => {
Ok(current_layout.clone())
}
PresetType::Custom(name) => {
log::info!("Calculating layout for custom preset: {}", name);
Ok(current_layout.clone())
}
}
}
fn validate_preset(&self, preset: &LayoutPreset) -> Result<(), String> {
for param in &preset.required_parameters {
if !param.required {
return Err(format!(
"Required parameter '{}' is marked as not required",
param.name
));
}
}
let mut param_names = std::collections::HashSet::new();
for param in &preset.required_parameters {
if !param_names.insert(¶m.name) {
return Err(format!("Duplicate parameter name: {}", param.name));
}
}
for param in &preset.optional_parameters {
if !param_names.insert(¶m.name) {
return Err(format!("Duplicate parameter name: {}", param.name));
}
}
for param in &preset.required_parameters {
for opt_param in &preset.optional_parameters {
if param.name == opt_param.name {
return Err(format!(
"Parameter '{}' appears in both required and optional lists",
param.name
));
}
}
}
Ok(())
}
pub fn get_preset(&self, name: &str) -> Option<&LayoutPreset> {
self.built_in_presets
.get(name)
.or_else(|| self.custom_presets.get(name))
}
pub fn get_all_preset_names(&self) -> Vec<&String> {
let mut names: Vec<&String> = self.built_in_presets.keys().collect();
names.extend(self.custom_presets.keys());
names
}
}
impl Default for LayoutPresetManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn test_preset_manager_creation() {
let manager = LayoutPresetManager::new();
assert!(!manager.get_built_in_preset_names().is_empty());
assert!(manager.get_custom_preset_names().is_empty());
}
#[wasm_bindgen_test]
fn test_built_in_presets() {
let manager = LayoutPresetManager::new();
assert!(manager.get_built_in_preset("list-reorder").is_some());
assert!(manager.get_built_in_preset("grid-resize").is_some());
assert!(
manager
.get_built_in_preset("responsive-breakpoint")
.is_some()
);
assert!(manager.get_built_in_preset("modal-expand").is_some());
assert!(manager.get_built_in_preset("card-flip").is_some());
}
#[wasm_bindgen_test]
fn test_custom_preset_management() {
let mut manager = LayoutPresetManager::new();
let custom_preset = LayoutPreset {
name: "custom-test".to_string(),
description: "Test custom preset".to_string(),
preset_type: PresetType::Custom("test".to_string()),
default_config: LayoutAnimationConfig::default(),
required_parameters: vec![],
optional_parameters: vec![],
};
assert!(manager.add_custom_preset(custom_preset).is_ok());
assert!(manager.get_custom_preset("custom-test").is_some());
assert!(manager.remove_custom_preset("custom-test").is_ok());
assert!(manager.get_custom_preset("custom-test").is_none());
}
#[wasm_bindgen_test]
fn test_target_layout_calculation() {
let manager = LayoutPresetManager::new();
let preset = manager.get_built_in_preset("list-reorder").unwrap();
let current_layout = DomRect::new().unwrap();
let parameters = HashMap::new();
let result = manager.calculate_target_layout(preset, ¤t_layout, ¶meters);
assert!(result.is_ok());
}
}