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
382 #theme_code
383
384 #message_enum
385
386 pub fn new_model() -> (#model_ident, Task<#message_ident>) {
387 (#model_ident::default(), Task::none())
388 }
389
390 pub fn update_model(model: &mut #model_ident, message: #message_ident) -> Task<#message_ident> {
391 match message {
392 #update_arms
393 #system_theme_arm
394 }
395 }
396
397 pub fn view_model(model: &#model_ident) -> Element<'_, #message_ident> {
398 #view_fn
399 }
400
401 #theme_method
402
403 #subscription_fn
404 };
405
406 Ok(CodegenOutput {
407 code: combined.to_string(),
408 warnings,
409 })
410}
411
412pub fn generate_application_full(
435 document: &DampenDocument,
436 model_name: &str,
437 message_name: &str,
438 handlers: &[HandlerSignature],
439 theme_document: Option<&ThemeDocument>,
440 persistence: Option<&PersistenceConfig>,
441) -> Result<CodegenOutput, CodegenError> {
442 let warnings = Vec::new();
443
444 let sub_config =
446 subscription::SubscriptionConfig::from_theme_document(theme_document, message_name);
447
448 let has_persistence = persistence.is_some();
450
451 let message_enum = generate_message_enum_full(handlers, Some(&sub_config), has_persistence);
453
454 let view_fn = view::generate_view(document, model_name, message_name)?;
455
456 let update_arms = update::generate_arms(handlers, message_name)?;
457
458 let system_theme_arm = subscription::generate_system_theme_update_arm(&sub_config);
460
461 let model_ident = syn::Ident::new(model_name, proc_macro2::Span::call_site());
462 let message_ident = syn::Ident::new(message_name, proc_macro2::Span::call_site());
463
464 let theme_code = if let Some(theme_doc) = theme_document {
466 match theme::generate_theme_code(theme_doc, &document.style_classes, "app") {
467 Ok(generated) => {
468 let code_str = generated.code;
469 syn::parse_str::<TokenStream>(&code_str).unwrap_or_default()
470 }
471 Err(e) => {
472 return Err(CodegenError::ThemeError(e));
473 }
474 }
475 } else {
476 TokenStream::new()
477 };
478
479 let has_theme = theme_document.is_some();
480 let theme_method = if has_theme {
481 quote! {
482 pub fn theme(_model: &AppModel) -> iced::Theme {
483 app_theme()
484 }
485 }
486 } else {
487 quote! {}
488 };
489
490 let (
492 wrapper_struct,
493 new_model_fn,
494 update_model_fn,
495 view_model_fn,
496 subscription_fn,
497 window_settings_fn,
498 ) = if let Some(config) = persistence {
499 let app_name = &config.app_name;
500
501 let wrapper = quote! {
503 pub struct AppModel {
505 pub inner: #model_ident,
507 persisted_window_state: dampen_dev::persistence::WindowState,
509 }
510 };
511
512 let new_model = quote! {
514 pub fn new_model() -> (AppModel, Task<#message_ident>) {
515 let persisted_state = dampen_dev::persistence::load_or_default(#app_name, 800, 600);
516 (
517 AppModel {
518 inner: #model_ident::default(),
519 persisted_window_state: persisted_state,
520 },
521 Task::none(),
522 )
523 }
524 };
525
526 let update_arms_inner = generate_update_arms_for_inner(handlers, message_name)?;
528
529 let update_model = quote! {
531 pub fn update_model(model: &mut AppModel, message: #message_ident) -> Task<#message_ident> {
532 match message {
533 #update_arms_inner
534 #system_theme_arm
535 #message_ident::Window(id, event) => {
536 match event {
537 iced::window::Event::Opened { .. } => {
538 if model.persisted_window_state.maximized {
541 iced::window::maximize(id, true)
542 } else {
543 Task::none()
544 }
545 }
546 iced::window::Event::Resized(size) => {
547 model.persisted_window_state.width = size.width as u32;
549 model.persisted_window_state.height = size.height as u32;
550 Task::none()
551 }
552 iced::window::Event::Moved(position) => {
553 model.persisted_window_state.x = Some(position.x as i32);
554 model.persisted_window_state.y = Some(position.y as i32);
555 Task::none()
556 }
557 iced::window::Event::CloseRequested => {
558 let _ = dampen_dev::persistence::save_window_state(
559 #app_name,
560 &model.persisted_window_state,
561 );
562 iced::window::close(id)
563 }
564 _ => Task::none(),
565 }
566 }
567 }
568 }
569 };
570
571 let view_model = quote! {
574 pub fn view_model(app_model: &AppModel) -> Element<'_, #message_ident> {
575 let model = &app_model.inner;
576 #view_fn
577 }
578 };
579
580 let subscription = if sub_config.system_theme_variant.is_some() {
582 let variant_name = sub_config
583 .system_theme_variant
584 .as_ref()
585 .map(|v| syn::Ident::new(v, proc_macro2::Span::call_site()));
586 quote! {
587 pub fn subscription_model(_model: &AppModel) -> iced::Subscription<#message_ident> {
588 let window_events = iced::window::events()
589 .map(|(id, e)| #message_ident::Window(id, e));
590
591 if app_follows_system() {
592 let system_theme = dampen_iced::watch_system_theme()
593 .map(#message_ident::#variant_name);
594 iced::Subscription::batch(vec![window_events, system_theme])
595 } else {
596 window_events
597 }
598 }
599 }
600 } else {
601 quote! {
602 pub fn subscription_model(_model: &AppModel) -> iced::Subscription<#message_ident> {
603 iced::window::events()
604 .map(|(id, e)| #message_ident::Window(id, e))
605 }
606 }
607 };
608
609 let window_settings = quote! {
611 pub fn window_settings() -> dampen_dev::persistence::WindowSettingsBuilder {
627 dampen_dev::persistence::WindowSettingsBuilder::new(#app_name)
628 }
629 };
630
631 (
632 wrapper,
633 new_model,
634 update_model,
635 view_model,
636 subscription,
637 window_settings,
638 )
639 } else {
640 let wrapper = TokenStream::new();
642
643 let new_model = quote! {
644 pub fn new_model() -> (#model_ident, Task<#message_ident>) {
645 (#model_ident::default(), Task::none())
646 }
647 };
648
649 let update_model = quote! {
650 pub fn update_model(model: &mut #model_ident, message: #message_ident) -> Task<#message_ident> {
651 match message {
652 #update_arms
653 #system_theme_arm
654 }
655 }
656 };
657
658 let view_model = quote! {
659 pub fn view_model(model: &#model_ident) -> Element<'_, #message_ident> {
660 #view_fn
661 }
662 };
663
664 let subscription = subscription::generate_subscription_function(&sub_config);
665
666 let window_settings = TokenStream::new();
667
668 (
669 wrapper,
670 new_model,
671 update_model,
672 view_model,
673 subscription,
674 window_settings,
675 )
676 };
677
678 let combined = quote! {
679 use iced::{Element, Task, Theme};
680 use crate::ui::window::*;
681 use std::collections::HashMap;
682
683 #theme_code
684
685 #message_enum
686
687 #wrapper_struct
688
689 #new_model_fn
690
691 #update_model_fn
692
693 #view_model_fn
694
695 #theme_method
696
697 #subscription_fn
698
699 #window_settings_fn
700 };
701
702 Ok(CodegenOutput {
703 code: combined.to_string(),
704 warnings,
705 })
706}
707
708fn generate_message_enum(handlers: &[HandlerSignature]) -> TokenStream {
710 generate_message_enum_with_subscription(handlers, None)
711}
712
713fn generate_message_enum_with_subscription(
715 handlers: &[HandlerSignature],
716 sub_config: Option<&subscription::SubscriptionConfig>,
717) -> TokenStream {
718 generate_message_enum_full(handlers, sub_config, false)
719}
720
721fn generate_message_enum_full(
723 handlers: &[HandlerSignature],
724 sub_config: Option<&subscription::SubscriptionConfig>,
725 include_window_events: bool,
726) -> TokenStream {
727 let handler_variants: Vec<_> = handlers
728 .iter()
729 .map(|h| {
730 let variant_name = to_upper_camel_case(&h.name);
732 let ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
733
734 if let Some(param_type) = &h.param_type {
735 let type_ident = syn::Ident::new(param_type, proc_macro2::Span::call_site());
736 quote! { #ident(#type_ident) }
737 } else {
738 quote! { #ident }
739 }
740 })
741 .collect();
742
743 let system_theme_variant = sub_config.and_then(subscription::generate_system_theme_variant);
745
746 let window_variant = if include_window_events {
748 Some(quote! {
749 Window(iced::window::Id, iced::window::Event)
751 })
752 } else {
753 None
754 };
755
756 let all_variants: Vec<TokenStream> = handler_variants
757 .into_iter()
758 .chain(system_theme_variant)
759 .chain(window_variant)
760 .collect();
761
762 if all_variants.is_empty() {
763 return quote! {
764 #[derive(Clone, Debug)]
765 pub enum Message {}
766 };
767 }
768
769 quote! {
770 #[derive(Clone, Debug)]
771 pub enum Message {
772 #(#all_variants),*
773 }
774 }
775}
776
777fn to_upper_camel_case(s: &str) -> String {
779 let mut result = String::new();
780 let mut capitalize_next = true;
781 for c in s.chars() {
782 if c == '_' {
783 capitalize_next = true;
784 } else if capitalize_next {
785 result.push(c.to_ascii_uppercase());
786 capitalize_next = false;
787 } else {
788 result.push(c);
789 }
790 }
791 result
792}
793
794fn generate_update_arms_for_inner(
799 handlers: &[HandlerSignature],
800 message_name: &str,
801) -> Result<TokenStream, CodegenError> {
802 use quote::format_ident;
803
804 let message_ident = format_ident!("{}", message_name);
805
806 let match_arms: Vec<TokenStream> = handlers
807 .iter()
808 .map(|handler| {
809 let handler_name = format_ident!("{}", handler.name);
810 let variant_name = to_upper_camel_case(&handler.name);
811 let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
812
813 if let Some(_param_type) = &handler.param_type {
814 quote! {
815 #message_ident::#variant_ident(value) => {
816 #handler_name(&mut model.inner, value);
817 iced::Task::none()
818 }
819 }
820 } else if handler.returns_command {
821 quote! {
822 #message_ident::#variant_ident => {
823 #handler_name(&mut model.inner)
824 }
825 }
826 } else {
827 quote! {
828 #message_ident::#variant_ident => {
829 #handler_name(&mut model.inner);
830 iced::Task::none()
831 }
832 }
833 }
834 })
835 .collect();
836
837 Ok(quote! {
838 #(#match_arms)*
839 })
840}
841
842pub fn constant_folding(code: &str) -> String {
849 let mut result = String::with_capacity(code.len());
850
851 for line in code.lines() {
852 let trimmed = line.trim_end();
853 if !trimmed.is_empty() {
854 result.push_str(trimmed);
855 result.push('\n');
856 }
857 }
858
859 result
860}
861
862pub fn validate_handlers(
864 document: &DampenDocument,
865 available_handlers: &[HandlerSignature],
866) -> Result<(), CodegenError> {
867 let handler_names: Vec<_> = available_handlers.iter().map(|h| h.name.clone()).collect();
868
869 fn collect_handlers(node: &crate::WidgetNode, handlers: &mut Vec<String>) {
871 for event in &node.events {
872 handlers.push(event.handler.clone());
873 }
874 for child in &node.children {
875 collect_handlers(child, handlers);
876 }
877 }
878
879 let mut referenced_handlers = Vec::new();
880 collect_handlers(&document.root, &mut referenced_handlers);
881
882 for handler in referenced_handlers {
884 if !handler_names.contains(&handler) {
885 return Err(CodegenError::MissingHandler(handler));
886 }
887 }
888
889 Ok(())
890}
891
892#[derive(Debug, thiserror::Error)]
894pub enum CodegenError {
895 #[error("Handler '{0}' is referenced but not defined")]
896 MissingHandler(String),
897
898 #[error("Invalid widget kind for code generation: {0}")]
899 InvalidWidget(String),
900
901 #[error("Binding expression error: {0}")]
902 BindingError(String),
903
904 #[error("Theme code generation error: {0}")]
905 ThemeError(String),
906
907 #[error("IO error: {0}")]
908 IoError(#[from] std::io::Error),
909}
910
911#[cfg(test)]
912mod tests {
913 use super::*;
914 use crate::parse;
915
916 #[test]
917 fn test_message_enum_generation() {
918 let handlers = vec![
919 HandlerSignature {
920 name: "increment".to_string(),
921 param_type: None,
922 returns_command: false,
923 },
924 HandlerSignature {
925 name: "update_value".to_string(),
926 param_type: Some("String".to_string()),
927 returns_command: false,
928 },
929 ];
930
931 let tokens = generate_message_enum(&handlers);
932 let code = tokens.to_string();
933
934 assert!(code.contains("Increment"));
935 assert!(code.contains("UpdateValue"));
936 }
937
938 #[test]
939 fn test_handler_validation() {
940 let xml = r#"<column><button on_click="increment" /></column>"#;
941 let doc = parse(xml).unwrap();
942
943 let handlers = vec![HandlerSignature {
944 name: "increment".to_string(),
945 param_type: None,
946 returns_command: false,
947 }];
948
949 assert!(validate_handlers(&doc, &handlers).is_ok());
950
951 let handlers_empty: Vec<HandlerSignature> = vec![];
953 assert!(validate_handlers(&doc, &handlers_empty).is_err());
954 }
955}