pub mod application;
pub mod bindings;
pub mod config;
pub mod handlers;
pub mod inventory;
pub mod status_mapping;
pub mod subscription;
pub mod theme;
pub mod update;
pub mod view;
use crate::DampenDocument;
use crate::HandlerSignature;
use proc_macro2::TokenStream;
use quote::quote;
use std::path::PathBuf;
use std::time::SystemTime;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HandlerSignatureType {
Simple,
WithValue,
WithCommand,
}
#[derive(Debug, Clone)]
pub struct HandlerInfo {
pub name: &'static str,
pub signature_type: HandlerSignatureType,
pub param_types: &'static [&'static str],
pub return_type: &'static str,
pub source_file: &'static str,
pub source_line: u32,
}
impl HandlerInfo {
pub fn to_signature(&self) -> HandlerSignature {
let param_type = match self.signature_type {
HandlerSignatureType::Simple => None,
HandlerSignatureType::WithValue => {
if self.param_types.len() > 1 {
Some(self.param_types[1].to_string())
} else {
None
}
}
HandlerSignatureType::WithCommand => None,
};
let returns_command = matches!(self.signature_type, HandlerSignatureType::WithCommand);
HandlerSignature {
name: self.name.to_string(),
param_type,
returns_command,
}
}
}
#[derive(Debug)]
pub struct CodegenOutput {
pub code: String,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct GeneratedApplication {
pub code: String,
pub handlers: Vec<String>,
pub widgets: Vec<String>,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct GeneratedCode {
pub code: String,
pub module_name: String,
pub source_file: PathBuf,
pub timestamp: SystemTime,
pub validated: bool,
}
impl GeneratedCode {
pub fn new(code: String, module_name: String, source_file: PathBuf) -> Self {
Self {
code,
module_name,
source_file,
timestamp: SystemTime::now(),
validated: false,
}
}
pub fn validate(&mut self) -> Result<(), String> {
match syn::parse_file(&self.code) {
Ok(_) => {
self.validated = true;
Ok(())
}
Err(e) => Err(format!("Syntax validation failed: {}", e)),
}
}
pub fn format(&mut self) -> Result<(), String> {
match syn::parse_file(&self.code) {
Ok(syntax_tree) => {
self.code = prettyplease::unparse(&syntax_tree);
Ok(())
}
Err(e) => Err(format!("Failed to parse code for formatting: {}", e)),
}
}
pub fn write_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
std::fs::write(path, &self.code)
}
}
pub fn generate_application(
document: &DampenDocument,
model_name: &str,
message_name: &str,
handlers: &[HandlerSignature],
) -> Result<CodegenOutput, CodegenError> {
let warnings = Vec::new();
let message_enum = generate_message_enum(handlers)?;
let view_fn = view::generate_view(document, model_name, message_name)?;
let update_arms = update::generate_arms(handlers, message_name)?;
let model_ident = syn::Ident::new(model_name, proc_macro2::Span::call_site());
let message_ident = syn::Ident::new(message_name, proc_macro2::Span::call_site());
let combined = quote! {
use iced::{Element, Task};
use crate::ui::window::*;
#message_enum
pub fn new_model() -> (#model_ident, Task<#message_ident>) {
(#model_ident::default(), Task::none())
}
pub fn update_model(model: &mut #model_ident, message: #message_ident) -> Task<#message_ident> {
match message {
#update_arms
}
}
pub fn view_model(model: &#model_ident) -> Element<'_, #message_ident> {
#view_fn
}
};
Ok(CodegenOutput {
code: combined.to_string(),
warnings,
})
}
use crate::ir::theme::ThemeDocument;
pub use config::PersistenceConfig;
pub fn generate_application_with_theme_and_subscriptions(
document: &DampenDocument,
model_name: &str,
message_name: &str,
handlers: &[HandlerSignature],
theme_document: Option<&ThemeDocument>,
) -> Result<CodegenOutput, CodegenError> {
let warnings = Vec::new();
let sub_config =
subscription::SubscriptionConfig::from_theme_document(theme_document, message_name);
let message_enum = generate_message_enum_with_subscription(handlers, Some(&sub_config))?;
let view_fn = view::generate_view(document, model_name, message_name)?;
let update_arms = update::generate_arms(handlers, message_name)?;
let system_theme_arm = subscription::generate_system_theme_update_arm(&sub_config);
let model_ident = syn::Ident::new(model_name, proc_macro2::Span::call_site());
let message_ident = syn::Ident::new(message_name, proc_macro2::Span::call_site());
let theme_code = if let Some(theme_doc) = theme_document {
match theme::generate_theme_code(theme_doc, &document.style_classes, "app") {
Ok(generated) => {
let code_str = generated.code;
syn::parse_str::<TokenStream>(&code_str).unwrap_or_default()
}
Err(e) => {
return Err(CodegenError::ThemeError(e));
}
}
} else {
TokenStream::new()
};
let subscription_fn = subscription::generate_subscription_function(&sub_config);
let has_theme = theme_document.is_some();
let theme_method = if has_theme {
quote! {
pub fn theme(_model: &#model_ident) -> iced::Theme {
app_theme()
}
}
} else {
quote! {}
};
let combined = quote! {
use iced::{Element, Task, Theme};
use crate::ui::window::*;
use std::collections::HashMap;
use dampen_core::handler::CanvasEvent;
#theme_code
#message_enum
pub fn new_model() -> (#model_ident, Task<#message_ident>) {
(#model_ident::default(), Task::none())
}
pub fn update_model(model: &mut #model_ident, message: #message_ident) -> Task<#message_ident> {
match message {
#update_arms
#system_theme_arm
}
}
pub fn view_model(model: &#model_ident) -> Element<'_, #message_ident> {
#view_fn
}
#theme_method
#subscription_fn
};
Ok(CodegenOutput {
code: combined.to_string(),
warnings,
})
}
pub fn generate_application_full(
document: &DampenDocument,
model_name: &str,
message_name: &str,
handlers: &[HandlerSignature],
theme_document: Option<&ThemeDocument>,
persistence: Option<&PersistenceConfig>,
) -> Result<CodegenOutput, CodegenError> {
let warnings = Vec::new();
let sub_config =
subscription::SubscriptionConfig::from_theme_document(theme_document, message_name);
let has_persistence = persistence.is_some();
let message_enum = generate_message_enum_full(handlers, Some(&sub_config), has_persistence)?;
let view_fn = view::generate_view(document, model_name, message_name)?;
let update_arms = update::generate_arms(handlers, message_name)?;
let system_theme_arm = subscription::generate_system_theme_update_arm(&sub_config);
let model_ident = syn::Ident::new(model_name, proc_macro2::Span::call_site());
let message_ident = syn::Ident::new(message_name, proc_macro2::Span::call_site());
let theme_code = if let Some(theme_doc) = theme_document {
match theme::generate_theme_code(theme_doc, &document.style_classes, "app") {
Ok(generated) => {
let code_str = generated.code;
syn::parse_str::<TokenStream>(&code_str).unwrap_or_default()
}
Err(e) => {
return Err(CodegenError::ThemeError(e));
}
}
} else {
TokenStream::new()
};
let has_theme = theme_document.is_some();
let theme_method = if has_theme {
quote! {
pub fn theme(_model: &AppModel) -> iced::Theme {
app_theme()
}
}
} else {
quote! {}
};
let (
wrapper_struct,
new_model_fn,
update_model_fn,
view_model_fn,
subscription_fn,
window_settings_fn,
) = if let Some(config) = persistence {
let app_name = &config.app_name;
let wrapper = quote! {
pub struct AppModel {
pub inner: #model_ident,
persisted_window_state: dampen_dev::persistence::WindowState,
}
};
let new_model = quote! {
pub fn new_model() -> (AppModel, Task<#message_ident>) {
let persisted_state = dampen_dev::persistence::load_or_default(#app_name, 800, 600);
(
AppModel {
inner: #model_ident::default(),
persisted_window_state: persisted_state,
},
Task::none(),
)
}
};
let update_arms_inner = generate_update_arms_for_inner(handlers, message_name)?;
let update_model = quote! {
pub fn update_model(model: &mut AppModel, message: #message_ident) -> Task<#message_ident> {
match message {
#update_arms_inner
#system_theme_arm
#message_ident::Window(id, event) => {
match event {
iced::window::Event::Opened { .. } => {
if model.persisted_window_state.maximized {
iced::window::maximize(id, true)
} else {
Task::none()
}
}
iced::window::Event::Resized(size) => {
model.persisted_window_state.width = size.width as u32;
model.persisted_window_state.height = size.height as u32;
Task::none()
}
iced::window::Event::Moved(position) => {
model.persisted_window_state.x = Some(position.x as i32);
model.persisted_window_state.y = Some(position.y as i32);
Task::none()
}
iced::window::Event::CloseRequested => {
let _ = dampen_dev::persistence::save_window_state(
#app_name,
&model.persisted_window_state,
);
iced::window::close(id)
}
_ => Task::none(),
}
}
}
}
};
let view_model = quote! {
pub fn view_model(app_model: &AppModel) -> Element<'_, #message_ident> {
let model = &app_model.inner;
#view_fn
}
};
let subscription = if sub_config.system_theme_variant.is_some() {
let variant_name = sub_config
.system_theme_variant
.as_ref()
.map(|v| syn::Ident::new(v, proc_macro2::Span::call_site()));
quote! {
pub fn subscription_model(_model: &AppModel) -> iced::Subscription<#message_ident> {
let window_events = iced::window::events()
.map(|(id, e)| #message_ident::Window(id, e));
if app_follows_system() {
let system_theme = dampen_iced::watch_system_theme()
.map(#message_ident::#variant_name);
iced::Subscription::batch(vec![window_events, system_theme])
} else {
window_events
}
}
}
} else {
quote! {
pub fn subscription_model(_model: &AppModel) -> iced::Subscription<#message_ident> {
iced::window::events()
.map(|(id, e)| #message_ident::Window(id, e))
}
}
};
let window_settings = quote! {
pub fn window_settings() -> dampen_dev::persistence::WindowSettingsBuilder {
dampen_dev::persistence::WindowSettingsBuilder::new(#app_name)
}
};
(
wrapper,
new_model,
update_model,
view_model,
subscription,
window_settings,
)
} else {
let wrapper = TokenStream::new();
let new_model = quote! {
pub fn new_model() -> (#model_ident, Task<#message_ident>) {
(#model_ident::default(), Task::none())
}
};
let update_model = quote! {
pub fn update_model(model: &mut #model_ident, message: #message_ident) -> Task<#message_ident> {
match message {
#update_arms
#system_theme_arm
}
}
};
let view_model = quote! {
pub fn view_model(model: &#model_ident) -> Element<'_, #message_ident> {
#view_fn
}
};
let subscription = subscription::generate_subscription_function(&sub_config);
let window_settings = TokenStream::new();
(
wrapper,
new_model,
update_model,
view_model,
subscription,
window_settings,
)
};
let combined = quote! {
use iced::{Element, Task, Theme};
use crate::ui::window::*;
use std::collections::HashMap;
use dampen_core::handler::CanvasEvent;
#theme_code
#message_enum
#wrapper_struct
#new_model_fn
#update_model_fn
#view_model_fn
#theme_method
#subscription_fn
#window_settings_fn
};
Ok(CodegenOutput {
code: combined.to_string(),
warnings,
})
}
fn generate_message_enum(handlers: &[HandlerSignature]) -> Result<TokenStream, syn::Error> {
generate_message_enum_with_subscription(handlers, None)
}
fn generate_message_enum_with_subscription(
handlers: &[HandlerSignature],
sub_config: Option<&subscription::SubscriptionConfig>,
) -> Result<TokenStream, syn::Error> {
generate_message_enum_full(handlers, sub_config, false)
}
fn generate_message_enum_full(
handlers: &[HandlerSignature],
sub_config: Option<&subscription::SubscriptionConfig>,
include_window_events: bool,
) -> Result<TokenStream, syn::Error> {
let handler_variants: Vec<_> = handlers
.iter()
.map(|h| {
let variant_name = to_upper_camel_case(&h.name);
let ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
if let Some(param_type) = &h.param_type {
let effective_type = if param_type == "&str" {
"String"
} else {
param_type
};
let type_path: syn::Type = syn::parse_str(effective_type).map_err(|e| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"Failed to parse parameter type '{}' for handler '{}': {}",
effective_type, h.name, e
),
)
})?;
Ok::<_, syn::Error>(quote! { #ident(#type_path) })
} else {
Ok::<_, syn::Error>(quote! { #ident })
}
})
.collect::<Result<Vec<_>, _>>()?;
let system_theme_variant = sub_config.and_then(subscription::generate_system_theme_variant);
let window_variant = if include_window_events {
Some(quote! {
Window(iced::window::Id, iced::window::Event)
})
} else {
None
};
let all_variants: Vec<TokenStream> = handler_variants
.into_iter()
.chain(system_theme_variant)
.chain(window_variant)
.collect();
Ok(quote! {
#[derive(Debug, Clone)]
pub enum Message {
#(#all_variants),*
}
})
}
fn to_upper_camel_case(s: &str) -> String {
let mut result = String::new();
let mut capitalize_next = true;
for c in s.chars() {
if c == '_' {
capitalize_next = true;
} else if capitalize_next {
result.push(c.to_ascii_uppercase());
capitalize_next = false;
} else {
result.push(c);
}
}
result
}
fn generate_update_arms_for_inner(
handlers: &[HandlerSignature],
message_name: &str,
) -> Result<TokenStream, CodegenError> {
use quote::format_ident;
let message_ident = format_ident!("{}", message_name);
let match_arms: Vec<TokenStream> = handlers
.iter()
.map(|handler| {
let handler_name = format_ident!("{}", handler.name);
let variant_name = to_upper_camel_case(&handler.name);
let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
if let Some(_param_type) = &handler.param_type {
quote! {
#message_ident::#variant_ident(value) => {
#handler_name(&mut model.inner, value);
iced::Task::none()
}
}
} else if handler.returns_command {
quote! {
#message_ident::#variant_ident => {
#handler_name(&mut model.inner)
}
}
} else {
quote! {
#message_ident::#variant_ident => {
#handler_name(&mut model.inner);
iced::Task::none()
}
}
}
})
.collect();
Ok(quote! {
#(#match_arms)*
})
}
pub fn constant_folding(code: &str) -> String {
let mut result = String::with_capacity(code.len());
for line in code.lines() {
let trimmed = line.trim_end();
if !trimmed.is_empty() {
result.push_str(trimmed);
result.push('\n');
}
}
result
}
pub fn validate_handlers(
document: &DampenDocument,
available_handlers: &[HandlerSignature],
) -> Result<(), CodegenError> {
let handler_names: Vec<_> = available_handlers.iter().map(|h| h.name.clone()).collect();
fn collect_handlers(node: &crate::WidgetNode, handlers: &mut Vec<String>) {
for event in &node.events {
handlers.push(event.handler.clone());
}
for child in &node.children {
collect_handlers(child, handlers);
}
}
let mut referenced_handlers = Vec::new();
collect_handlers(&document.root, &mut referenced_handlers);
for handler in referenced_handlers {
if !handler_names.contains(&handler) {
return Err(CodegenError::MissingHandler(handler));
}
}
Ok(())
}
#[derive(Debug, thiserror::Error)]
pub enum CodegenError {
#[error("Handler '{0}' is referenced but not defined")]
MissingHandler(String),
#[error("Invalid widget kind for code generation: {0}")]
InvalidWidget(String),
#[error("Binding expression error: {0}")]
BindingError(String),
#[error("Theme code generation error: {0}")]
ThemeError(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Syntax error: {0}")]
SyntaxError(#[from] syn::Error),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse;
#[test]
fn test_message_enum_generation() {
let handlers = vec![
HandlerSignature {
name: "increment".to_string(),
param_type: None,
returns_command: false,
},
HandlerSignature {
name: "update_value".to_string(),
param_type: Some("String".to_string()),
returns_command: false,
},
];
let tokens = generate_message_enum(&handlers).unwrap();
let code = tokens.to_string();
assert!(code.contains("Increment"));
assert!(code.contains("UpdateValue"));
}
#[test]
fn test_handler_validation() {
let xml = r#"<column><button on_click="increment" /></column>"#;
let doc = parse(xml).unwrap();
let handlers = vec![HandlerSignature {
name: "increment".to_string(),
param_type: None,
returns_command: false,
}];
assert!(validate_handlers(&doc, &handlers).is_ok());
let handlers_empty: Vec<HandlerSignature> = vec![];
assert!(validate_handlers(&doc, &handlers_empty).is_err());
}
}