1pub mod application;
34pub mod bindings;
35pub mod config;
36pub mod handlers;
37pub mod inventory;
38pub mod status_mapping;
39pub mod subscription;
40pub mod theme;
41pub mod update;
42pub mod view;
43
44use crate::DampenDocument;
45use crate::HandlerSignature;
46use proc_macro2::TokenStream;
47use quote::quote;
48use std::path::PathBuf;
49use std::time::SystemTime;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum HandlerSignatureType {
54 Simple,
57
58 WithValue,
61
62 WithCommand,
65}
66
67#[derive(Debug, Clone)]
69pub struct HandlerInfo {
70 pub name: &'static str,
72
73 pub signature_type: HandlerSignatureType,
75
76 pub param_types: &'static [&'static str],
78
79 pub return_type: &'static str,
81
82 pub source_file: &'static str,
84
85 pub source_line: u32,
87}
88
89impl HandlerInfo {
90 pub fn to_signature(&self) -> HandlerSignature {
99 let param_type = match self.signature_type {
101 HandlerSignatureType::Simple => None,
102 HandlerSignatureType::WithValue => {
103 if self.param_types.len() > 1 {
105 Some(self.param_types[1].to_string())
106 } else {
107 None
108 }
109 }
110 HandlerSignatureType::WithCommand => None,
111 };
112
113 let returns_command = matches!(self.signature_type, HandlerSignatureType::WithCommand);
114
115 HandlerSignature {
116 name: self.name.to_string(),
117 param_type,
118 returns_command,
119 }
120 }
121}
122
123#[derive(Debug)]
127pub struct CodegenOutput {
128 pub code: String,
130 pub warnings: Vec<String>,
132}
133
134#[derive(Debug, Clone)]
136pub struct GeneratedApplication {
137 pub code: String,
139
140 pub handlers: Vec<String>,
142
143 pub widgets: Vec<String>,
145
146 pub warnings: Vec<String>,
148}
149
150#[derive(Debug, Clone)]
156pub struct GeneratedCode {
157 pub code: String,
159
160 pub module_name: String,
162
163 pub source_file: PathBuf,
165
166 pub timestamp: SystemTime,
168
169 pub validated: bool,
171}
172
173impl GeneratedCode {
174 pub fn new(code: String, module_name: String, source_file: PathBuf) -> Self {
184 Self {
185 code,
186 module_name,
187 source_file,
188 timestamp: SystemTime::now(),
189 validated: false,
190 }
191 }
192
193 pub fn validate(&mut self) -> Result<(), String> {
198 match syn::parse_file(&self.code) {
199 Ok(_) => {
200 self.validated = true;
201 Ok(())
202 }
203 Err(e) => Err(format!("Syntax validation failed: {}", e)),
204 }
205 }
206
207 pub fn format(&mut self) -> Result<(), String> {
212 match syn::parse_file(&self.code) {
214 Ok(syntax_tree) => {
215 self.code = prettyplease::unparse(&syntax_tree);
217 Ok(())
218 }
219 Err(e) => Err(format!("Failed to parse code for formatting: {}", e)),
220 }
221 }
222
223 pub fn write_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
231 std::fs::write(path, &self.code)
232 }
233}
234
235pub fn generate_application(
257 document: &DampenDocument,
258 model_name: &str,
259 message_name: &str,
260 handlers: &[HandlerSignature],
261) -> Result<CodegenOutput, CodegenError> {
262 let warnings = Vec::new();
263
264 let message_enum = generate_message_enum(handlers)?;
265
266 let view_fn = view::generate_view(document, model_name, message_name)?;
267
268 let update_arms = update::generate_arms(handlers, message_name)?;
269
270 let model_ident = syn::Ident::new(model_name, proc_macro2::Span::call_site());
271 let message_ident = syn::Ident::new(message_name, proc_macro2::Span::call_site());
272
273 let combined = quote! {
274 use iced::{Element, Task};
275 use crate::ui::window::*;
276
277 #message_enum
278
279 pub fn new_model() -> (#model_ident, Task<#message_ident>) {
280 (#model_ident::default(), Task::none())
281 }
282
283 pub fn update_model(model: &mut #model_ident, message: #message_ident) -> Task<#message_ident> {
284 match message {
285 #update_arms
286 }
287 }
288
289 pub fn view_model(model: &#model_ident) -> Element<'_, #message_ident> {
290 #view_fn
291 }
292 };
293
294 Ok(CodegenOutput {
295 code: combined.to_string(),
296 warnings,
297 })
298}
299
300use crate::ir::theme::ThemeDocument;
301pub use config::PersistenceConfig;
302
303pub fn generate_application_with_theme_and_subscriptions(
323 document: &DampenDocument,
324 model_name: &str,
325 message_name: &str,
326 handlers: &[HandlerSignature],
327 theme_document: Option<&ThemeDocument>,
328) -> Result<CodegenOutput, CodegenError> {
329 let warnings = Vec::new();
330
331 let sub_config =
333 subscription::SubscriptionConfig::from_theme_document(theme_document, message_name);
334
335 let message_enum = generate_message_enum_with_subscription(handlers, Some(&sub_config))?;
337
338 let view_fn = view::generate_view(document, model_name, message_name)?;
339
340 let update_arms = update::generate_arms(handlers, message_name)?;
341
342 let system_theme_arm = subscription::generate_system_theme_update_arm(&sub_config);
344
345 let model_ident = syn::Ident::new(model_name, proc_macro2::Span::call_site());
346 let message_ident = syn::Ident::new(message_name, proc_macro2::Span::call_site());
347
348 let theme_code = if let Some(theme_doc) = theme_document {
350 match theme::generate_theme_code(theme_doc, &document.style_classes, "app") {
351 Ok(generated) => {
352 let code_str = generated.code;
353 syn::parse_str::<TokenStream>(&code_str).unwrap_or_default()
354 }
355 Err(e) => {
356 return Err(CodegenError::ThemeError(e));
357 }
358 }
359 } else {
360 TokenStream::new()
361 };
362
363 let subscription_fn = subscription::generate_subscription_function(&sub_config);
365
366 let has_theme = theme_document.is_some();
367 let theme_method = if has_theme {
368 quote! {
369 pub fn theme(_model: &#model_ident) -> iced::Theme {
370 app_theme()
371 }
372 }
373 } else {
374 quote! {}
375 };
376
377 let combined = quote! {
378 use iced::{Element, Task, Theme};
379 use crate::ui::window::*;
380 use std::collections::HashMap;
381 use dampen_core::handler::CanvasEvent;
382
383 #theme_code
384
385
386 #message_enum
387
388 pub fn new_model() -> (#model_ident, Task<#message_ident>) {
389 (#model_ident::default(), Task::none())
390 }
391
392 pub fn update_model(model: &mut #model_ident, message: #message_ident) -> Task<#message_ident> {
393 match message {
394 #update_arms
395 #system_theme_arm
396 }
397 }
398
399 pub fn view_model(model: &#model_ident) -> Element<'_, #message_ident> {
400 #view_fn
401 }
402
403 #theme_method
404
405 #subscription_fn
406 };
407
408 Ok(CodegenOutput {
409 code: combined.to_string(),
410 warnings,
411 })
412}
413
414pub fn generate_application_full(
437 document: &DampenDocument,
438 model_name: &str,
439 message_name: &str,
440 handlers: &[HandlerSignature],
441 theme_document: Option<&ThemeDocument>,
442 persistence: Option<&PersistenceConfig>,
443) -> Result<CodegenOutput, CodegenError> {
444 let warnings = Vec::new();
445
446 let sub_config =
448 subscription::SubscriptionConfig::from_theme_document(theme_document, message_name);
449
450 let has_persistence = persistence.is_some();
452
453 let message_enum = generate_message_enum_full(handlers, Some(&sub_config), has_persistence)?;
455
456 let view_fn = view::generate_view(document, model_name, message_name)?;
457
458 let update_arms = update::generate_arms(handlers, message_name)?;
459
460 let system_theme_arm = subscription::generate_system_theme_update_arm(&sub_config);
462
463 let model_ident = syn::Ident::new(model_name, proc_macro2::Span::call_site());
464 let message_ident = syn::Ident::new(message_name, proc_macro2::Span::call_site());
465
466 let theme_code = if let Some(theme_doc) = theme_document {
468 match theme::generate_theme_code(theme_doc, &document.style_classes, "app") {
469 Ok(generated) => {
470 let code_str = generated.code;
471 syn::parse_str::<TokenStream>(&code_str).unwrap_or_default()
472 }
473 Err(e) => {
474 return Err(CodegenError::ThemeError(e));
475 }
476 }
477 } else {
478 TokenStream::new()
479 };
480
481 let has_theme = theme_document.is_some();
482 let theme_method = if has_theme {
483 quote! {
484 pub fn theme(_model: &AppModel) -> iced::Theme {
485 app_theme()
486 }
487 }
488 } else {
489 quote! {}
490 };
491
492 let (
494 wrapper_struct,
495 new_model_fn,
496 update_model_fn,
497 view_model_fn,
498 subscription_fn,
499 window_settings_fn,
500 ) = if let Some(config) = persistence {
501 let app_name = &config.app_name;
502
503 let wrapper = quote! {
505 pub struct AppModel {
507 pub inner: #model_ident,
509 persisted_window_state: dampen_dev::persistence::WindowState,
511 }
512 };
513
514 let new_model = quote! {
516 pub fn new_model() -> (AppModel, Task<#message_ident>) {
517 let persisted_state = dampen_dev::persistence::load_or_default(#app_name, 800, 600);
518 (
519 AppModel {
520 inner: #model_ident::default(),
521 persisted_window_state: persisted_state,
522 },
523 Task::none(),
524 )
525 }
526 };
527
528 let update_arms_inner = generate_update_arms_for_inner(handlers, message_name)?;
530
531 let update_model = quote! {
533 pub fn update_model(model: &mut AppModel, message: #message_ident) -> Task<#message_ident> {
534 match message {
535 #update_arms_inner
536 #system_theme_arm
537 #message_ident::Window(id, event) => {
538 match event {
539 iced::window::Event::Opened { .. } => {
540 if model.persisted_window_state.maximized {
543 iced::window::maximize(id, true)
544 } else {
545 Task::none()
546 }
547 }
548 iced::window::Event::Resized(size) => {
549 model.persisted_window_state.width = size.width as u32;
551 model.persisted_window_state.height = size.height as u32;
552 Task::none()
553 }
554 iced::window::Event::Moved(position) => {
555 model.persisted_window_state.x = Some(position.x as i32);
556 model.persisted_window_state.y = Some(position.y as i32);
557 Task::none()
558 }
559 iced::window::Event::CloseRequested => {
560 let _ = dampen_dev::persistence::save_window_state(
561 #app_name,
562 &model.persisted_window_state,
563 );
564 iced::window::close(id)
565 }
566 _ => Task::none(),
567 }
568 }
569 }
570 }
571 };
572
573 let view_model = quote! {
576 pub fn view_model(app_model: &AppModel) -> Element<'_, #message_ident> {
577 let model = &app_model.inner;
578 #view_fn
579 }
580 };
581
582 let subscription = if sub_config.system_theme_variant.is_some() {
584 let variant_name = sub_config
585 .system_theme_variant
586 .as_ref()
587 .map(|v| syn::Ident::new(v, proc_macro2::Span::call_site()));
588 quote! {
589 pub fn subscription_model(_model: &AppModel) -> iced::Subscription<#message_ident> {
590 let window_events = iced::window::events()
591 .map(|(id, e)| #message_ident::Window(id, e));
592
593 if app_follows_system() {
594 let system_theme = dampen_iced::watch_system_theme()
595 .map(#message_ident::#variant_name);
596 iced::Subscription::batch(vec![window_events, system_theme])
597 } else {
598 window_events
599 }
600 }
601 }
602 } else {
603 quote! {
604 pub fn subscription_model(_model: &AppModel) -> iced::Subscription<#message_ident> {
605 iced::window::events()
606 .map(|(id, e)| #message_ident::Window(id, e))
607 }
608 }
609 };
610
611 let window_settings = quote! {
613 pub fn window_settings() -> dampen_dev::persistence::WindowSettingsBuilder {
629 dampen_dev::persistence::WindowSettingsBuilder::new(#app_name)
630 }
631 };
632
633 (
634 wrapper,
635 new_model,
636 update_model,
637 view_model,
638 subscription,
639 window_settings,
640 )
641 } else {
642 let wrapper = TokenStream::new();
644
645 let new_model = quote! {
646 pub fn new_model() -> (#model_ident, Task<#message_ident>) {
647 (#model_ident::default(), Task::none())
648 }
649 };
650
651 let update_model = quote! {
652 pub fn update_model(model: &mut #model_ident, message: #message_ident) -> Task<#message_ident> {
653 match message {
654 #update_arms
655 #system_theme_arm
656 }
657 }
658 };
659
660 let view_model = quote! {
661 pub fn view_model(model: &#model_ident) -> Element<'_, #message_ident> {
662 #view_fn
663 }
664 };
665
666 let subscription = subscription::generate_subscription_function(&sub_config);
667
668 let window_settings = TokenStream::new();
669
670 (
671 wrapper,
672 new_model,
673 update_model,
674 view_model,
675 subscription,
676 window_settings,
677 )
678 };
679
680 let combined = quote! {
681 use iced::{Element, Task, Theme};
682 use crate::ui::window::*;
683 use std::collections::HashMap;
684 use dampen_core::handler::CanvasEvent;
685
686 #theme_code
687
688
689 #message_enum
690
691 #wrapper_struct
692
693 #new_model_fn
694
695 #update_model_fn
696
697 #view_model_fn
698
699 #theme_method
700
701 #subscription_fn
702
703 #window_settings_fn
704 };
705
706 Ok(CodegenOutput {
707 code: combined.to_string(),
708 warnings,
709 })
710}
711
712fn generate_message_enum(handlers: &[HandlerSignature]) -> Result<TokenStream, syn::Error> {
714 generate_message_enum_with_subscription(handlers, None)
715}
716
717fn generate_message_enum_with_subscription(
719 handlers: &[HandlerSignature],
720 sub_config: Option<&subscription::SubscriptionConfig>,
721) -> Result<TokenStream, syn::Error> {
722 generate_message_enum_full(handlers, sub_config, false)
723}
724
725fn generate_message_enum_full(
727 handlers: &[HandlerSignature],
728 sub_config: Option<&subscription::SubscriptionConfig>,
729 include_window_events: bool,
730) -> Result<TokenStream, syn::Error> {
731 let handler_variants: Vec<_> = handlers
732 .iter()
733 .map(|h| {
734 let variant_name = to_upper_camel_case(&h.name);
736 let ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
737
738 if let Some(param_type) = &h.param_type {
739 let effective_type = if param_type == "&str" {
742 "String"
743 } else {
744 param_type
745 };
746 let type_path: syn::Type = syn::parse_str(effective_type).map_err(|e| {
747 syn::Error::new(
748 proc_macro2::Span::call_site(),
749 format!(
750 "Failed to parse parameter type '{}' for handler '{}': {}",
751 effective_type, h.name, e
752 ),
753 )
754 })?;
755 Ok::<_, syn::Error>(quote! { #ident(#type_path) })
756 } else {
757 Ok::<_, syn::Error>(quote! { #ident })
758 }
759 })
760 .collect::<Result<Vec<_>, _>>()?;
761
762 let system_theme_variant = sub_config.and_then(subscription::generate_system_theme_variant);
764
765 let window_variant = if include_window_events {
767 Some(quote! {
768 Window(iced::window::Id, iced::window::Event)
770 })
771 } else {
772 None
773 };
774
775 let all_variants: Vec<TokenStream> = handler_variants
776 .into_iter()
777 .chain(system_theme_variant)
778 .chain(window_variant)
779 .collect();
780
781 Ok(quote! {
782 #[derive(Debug, Clone)]
783 pub enum Message {
784 #(#all_variants),*
785 }
786 })
787}
788
789fn to_upper_camel_case(s: &str) -> String {
791 let mut result = String::new();
792 let mut capitalize_next = true;
793 for c in s.chars() {
794 if c == '_' {
795 capitalize_next = true;
796 } else if capitalize_next {
797 result.push(c.to_ascii_uppercase());
798 capitalize_next = false;
799 } else {
800 result.push(c);
801 }
802 }
803 result
804}
805
806fn generate_update_arms_for_inner(
811 handlers: &[HandlerSignature],
812 message_name: &str,
813) -> Result<TokenStream, CodegenError> {
814 use quote::format_ident;
815
816 let message_ident = format_ident!("{}", message_name);
817
818 let match_arms: Vec<TokenStream> = handlers
819 .iter()
820 .map(|handler| {
821 let handler_name = format_ident!("{}", handler.name);
822 let variant_name = to_upper_camel_case(&handler.name);
823 let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
824
825 if let Some(_param_type) = &handler.param_type {
826 quote! {
827 #message_ident::#variant_ident(value) => {
828 #handler_name(&mut model.inner, value);
829 iced::Task::none()
830 }
831 }
832 } else if handler.returns_command {
833 quote! {
834 #message_ident::#variant_ident => {
835 #handler_name(&mut model.inner)
836 }
837 }
838 } else {
839 quote! {
840 #message_ident::#variant_ident => {
841 #handler_name(&mut model.inner);
842 iced::Task::none()
843 }
844 }
845 }
846 })
847 .collect();
848
849 Ok(quote! {
850 #(#match_arms)*
851 })
852}
853
854pub fn constant_folding(code: &str) -> String {
861 let mut result = String::with_capacity(code.len());
862
863 for line in code.lines() {
864 let trimmed = line.trim_end();
865 if !trimmed.is_empty() {
866 result.push_str(trimmed);
867 result.push('\n');
868 }
869 }
870
871 result
872}
873
874pub fn validate_handlers(
876 document: &DampenDocument,
877 available_handlers: &[HandlerSignature],
878) -> Result<(), CodegenError> {
879 let handler_names: Vec<_> = available_handlers.iter().map(|h| h.name.clone()).collect();
880
881 fn collect_handlers(node: &crate::WidgetNode, handlers: &mut Vec<String>) {
883 for event in &node.events {
884 handlers.push(event.handler.clone());
885 }
886 for child in &node.children {
887 collect_handlers(child, handlers);
888 }
889 }
890
891 let mut referenced_handlers = Vec::new();
892 collect_handlers(&document.root, &mut referenced_handlers);
893
894 for handler in referenced_handlers {
896 if !handler_names.contains(&handler) {
897 return Err(CodegenError::MissingHandler(handler));
898 }
899 }
900
901 Ok(())
902}
903
904#[derive(Debug, thiserror::Error)]
906pub enum CodegenError {
907 #[error("Handler '{0}' is referenced but not defined")]
908 MissingHandler(String),
909
910 #[error("Invalid widget kind for code generation: {0}")]
911 InvalidWidget(String),
912
913 #[error("Binding expression error: {0}")]
914 BindingError(String),
915
916 #[error("Theme code generation error: {0}")]
917 ThemeError(String),
918
919 #[error("IO error: {0}")]
920 IoError(#[from] std::io::Error),
921
922 #[error("Syntax error: {0}")]
923 SyntaxError(#[from] syn::Error),
924}
925
926#[cfg(test)]
927mod tests {
928 use super::*;
929 use crate::parse;
930
931 #[test]
932 fn test_message_enum_generation() {
933 let handlers = vec![
934 HandlerSignature {
935 name: "increment".to_string(),
936 param_type: None,
937 returns_command: false,
938 },
939 HandlerSignature {
940 name: "update_value".to_string(),
941 param_type: Some("String".to_string()),
942 returns_command: false,
943 },
944 ];
945
946 let tokens = generate_message_enum(&handlers).unwrap();
947 let code = tokens.to_string();
948
949 assert!(code.contains("Increment"));
950 assert!(code.contains("UpdateValue"));
951 }
952
953 #[test]
954 fn test_handler_validation() {
955 let xml = r#"<column><button on_click="increment" /></column>"#;
956 let doc = parse(xml).unwrap();
957
958 let handlers = vec![HandlerSignature {
959 name: "increment".to_string(),
960 param_type: None,
961 returns_command: false,
962 }];
963
964 assert!(validate_handlers(&doc, &handlers).is_ok());
965
966 let handlers_empty: Vec<HandlerSignature> = vec![];
968 assert!(validate_handlers(&doc, &handlers_empty).is_err());
969 }
970}