1#![allow(clippy::needless_doctest_main)]
59
60use std::collections::BTreeMap;
61
62use denise_ui::widgets::{Payload, PropertyKind, all};
63
64use crate::error::{At, Error, Reason};
65use crate::form::Form;
66
67const KEYWORDS: &[&str] = &[
73 "as", "async", "await", "break", "const", "continue", "dyn", "else", "enum", "extern", "false",
74 "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
75 "return", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where",
76 "while", "abstract", "become", "box", "do", "final", "macro", "override", "priv", "try",
77 "typeof", "unsized", "virtual", "yield", "gen",
78];
79
80const NEVER_RAW: &[&str] = &["crate", "self", "Self"];
82
83#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct Generated {
86 pub source: String,
88 pub kind: String,
90 pub message: String,
92}
93
94pub fn generate(source: &str, name: &str) -> Result<Generated, Error> {
125 let form = Form::parse(source)?;
126 let kind = type_name(name).ok_or_else(|| {
127 Error::new(
128 At::START,
129 Reason::NotAnIdentifier {
130 found: name.to_string(),
131 because: "a form's name has to start with a letter",
132 },
133 )
134 })?;
135 let message = format!("{kind}Message");
136
137 let mut fields: Vec<(String, String)> = Vec::new();
140 let mut taken: BTreeMap<String, String> = BTreeMap::new();
141 let mut messages: Vec<Message> = Vec::new();
142 let mut seen: BTreeMap<String, (Payload, String)> = BTreeMap::new();
143
144 for node in form.written() {
145 if node.path.is_empty() {
146 continue;
147 }
148 if let Some(name) = &node.name {
149 let field = field_name(name)?;
150 if let Some(first) = taken.get(&field) {
151 return Err(Error::new(
152 At::START,
153 Reason::Collides {
154 found: name.clone(),
155 with: first.clone(),
156 spelled: field,
157 },
158 ));
159 }
160 taken.insert(field.clone(), name.clone());
161 fields.push((field, name.clone()));
162 }
163
164 let Some(info) = all().iter().find(|widget| widget.kind == node.kind) else {
165 continue;
166 };
167 for property in info.properties {
168 let PropertyKind::Message(payload) = property.kind else {
169 continue;
170 };
171 let Some(used) = form.property(&node.path, property.name) else {
172 continue;
173 };
174 match seen.get(&used) {
175 Some((first, _)) if *first != payload => {
176 return Err(Error::new(
177 At::START,
178 Reason::PayloadClash {
179 found: used,
180 first: shape(*first),
181 then: shape(payload),
182 },
183 ));
184 }
185 Some(_) => continue,
186 None => {}
187 }
188 let variant = variant_name(&used)?;
189 let method = field_name(&used)?;
190 seen.insert(used.clone(), (payload, variant.clone()));
191 messages.push(Message {
192 variant,
193 method,
194 name: used,
195 payload,
196 });
197 }
198 }
199
200 Ok(Generated {
201 source: write(source, &kind, &message, &fields, &messages),
202 kind,
203 message,
204 })
205}
206
207pub fn to_out_dir(path: impl AsRef<std::path::Path>) -> Result<std::path::PathBuf, String> {
224 let path = path.as_ref();
225 println!("cargo:rerun-if-changed={}", path.display());
228
229 let stem = path.file_stem().and_then(|s| s.to_str()).ok_or_else(|| {
230 format!(
231 "{}: no file name to take a struct name from",
232 path.display()
233 )
234 })?;
235 let source = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
236 let generated = generate(&source, stem).map_err(|e| format!("{}:{e}", path.display()))?;
237
238 let out = std::path::PathBuf::from(
239 std::env::var("OUT_DIR")
240 .map_err(|_| String::from("OUT_DIR is not set; this is for a build script"))?,
241 )
242 .join(format!("{stem}.rs"));
243 std::fs::write(&out, &generated.source).map_err(|e| format!("{}: {e}", out.display()))?;
244 Ok(out)
245}
246
247const fn shape(payload: Payload) -> &'static str {
249 match payload {
250 Payload::None => "the message itself",
251 Payload::Bool => "a `fn(bool)`",
252 Payload::Index => "a `fn(usize)`",
253 Payload::Number => "a `fn(f32)`",
254 }
255}
256
257struct Message {
259 variant: String,
261 method: String,
263 name: String,
265 payload: Payload,
267}
268
269const fn parameter(payload: Payload) -> &'static str {
272 match payload {
273 Payload::None => "",
274 Payload::Bool => ", on: bool",
275 Payload::Index => ", index: usize",
276 Payload::Number => ", value: f32",
277 }
278}
279
280const fn carried(payload: Payload) -> &'static str {
282 match payload {
283 Payload::None => "",
284 Payload::Bool => "(bool)",
285 Payload::Index => "(usize)",
286 Payload::Number => "(f32)",
287 }
288}
289
290fn write(
292 source: &str,
293 kind: &str,
294 message: &str,
295 fields: &[(String, String)],
296 messages: &[Message],
297) -> String {
298 let mut out = String::new();
299 out.push_str(
300 "// Generated from a `.dform` file by `denise_forms::codegen`. Do not edit:\n\
301 // the form file is the source, and this is rewritten on every build.\n\n",
302 );
303
304 out.push_str(&format!(
306 "/// The form this was generated from, as it stood at build time.\n\
307 pub const {}_SOURCE: &str = r####\"{source}\"####;\n\n",
308 upper(kind),
309 ));
310
311 out.push_str(&format!(
313 "/// Every node [`{kind}`] names, as a field.\n\
314 ///\n\
315 /// Rename one in the form and this stops compiling where it was used,\n\
316 /// which is the whole reason this file is generated.\n\
317 #[derive(Clone, Copy, Debug, PartialEq, Eq)]\n\
318 pub struct {kind} {{\n"
319 ));
320 for (field, name) in fields {
321 out.push_str(&format!(
322 " /// The node the form calls `{name}`.\n pub {field}: ::denise_ui::NodeId,\n"
323 ));
324 }
325 if fields.is_empty() {
326 out.push_str(" /// The form names no nodes.\n _private: (),\n");
327 }
328 out.push_str("}\n\n");
329
330 out.push_str(&format!(
332 "/// Every message [`{kind}`] can emit.\n\
333 ///\n\
334 /// Add one to the form and every `match` on this stops compiling,\n\
335 /// because it is no longer exhaustive.\n\
336 #[derive(Clone, Copy, PartialEq, Debug)]\n\
337 pub enum {message} {{\n"
338 ));
339 for Message {
340 variant,
341 name,
342 payload,
343 ..
344 } in messages
345 {
346 out.push_str(&format!(
347 " /// What the form calls `{name}`.\n {variant}{},\n",
348 carried(*payload)
349 ));
350 }
351 if messages.is_empty() {
352 out.push_str(" /// The form emits nothing, and this cannot be constructed.\n #[doc(hidden)]\n Never,\n");
353 }
354 out.push_str("}\n\n");
355
356 out.push_str(&format!(
361 "/// What [`{kind}`]'s events reach: one method per message, named for it.\n\
362 ///\n\
363 /// Implement this for the type that handles the form, and hand it every\n\
364 /// message through [`{message}::dispatch`]. Add an event to the form and\n\
365 /// this `impl` stops compiling, naming the method it is missing — which\n\
366 /// is the method the designer writes when asked to open that event.\n\
367 pub trait {kind}Handlers {{\n"
368 ));
369 for Message {
370 method,
371 name,
372 payload,
373 ..
374 } in messages
375 {
376 out.push_str(&format!(
377 " /// What the form calls `{name}`.\n fn {method}(&mut self{});\n",
378 parameter(*payload)
379 ));
380 }
381 out.push_str("}\n\n");
382 out.push_str(&format!(
383 "impl {message} {{\n\
384 \x20 /// Hands this message to the method named for it.\n\
385 \x20 pub fn dispatch(self, handlers: &mut impl {kind}Handlers) {{\n\
386 \x20 match self {{\n"
387 ));
388 for Message {
389 variant,
390 method,
391 payload,
392 ..
393 } in messages
394 {
395 out.push_str(&match payload {
396 Payload::None => format!(" Self::{variant} => handlers.{method}(),\n"),
397 _ => format!(" Self::{variant}(value) => handlers.{method}(value),\n"),
398 });
399 }
400 if messages.is_empty() {
401 out.push_str(" Self::Never => {}\n");
402 }
403 out.push_str(" }\n }\n}\n\n");
404
405 out.push_str(&format!(
407 "impl {kind} {{\n\
408 \x20 /// Builds the form under `parent`, with no pictures.\n\
409 \x20 ///\n\
410 \x20 /// # Errors\n\
411 \x20 ///\n\
412 \x20 /// Whatever [`denise_forms::Form::build`] says, with a line and a column.\n\
413 \x20 pub fn build(\n\
414 \x20 ui: &mut ::denise_ui::Ui<{message}>,\n\
415 \x20 parent: ::denise_ui::NodeId,\n\
416 \x20 ) -> ::core::result::Result<Self, ::denise_forms::Error> {{\n\
417 \x20 Self::build_with(ui, parent, &mut |_: &str| None)\n\
418 \x20 }}\n\n\
419 \x20 /// Builds the form, loading pictures through `assets`.\n\
420 \x20 ///\n\
421 \x20 /// # Errors\n\
422 \x20 ///\n\
423 \x20 /// Whatever [`denise_forms::Form::build`] says, with a line and a column.\n\
424 \x20 pub fn build_with(\n\
425 \x20 ui: &mut ::denise_ui::Ui<{message}>,\n\
426 \x20 parent: ::denise_ui::NodeId,\n\
427 \x20 assets: &mut dyn FnMut(&str) -> Option<::denise_forms::Picture>,\n\
428 \x20 ) -> ::core::result::Result<Self, ::denise_forms::Error> {{\n\
429 \x20 let form = ::denise_forms::Form::parse({0}_SOURCE)?;\n\
430 \x20 let fit = ::denise_forms::Placement {{\n\
431 \x20 x: 1.0,\n\
432 \x20 y: 1.0,\n\
433 \x20 rect: ::denise::Rect::from_size(form.size()),\n\
434 \x20 }};\n\
435 \x20 Self::place(ui, parent, fit, assets)\n\
436 \x20 }}\n\n\
437 \x20 /// Builds the form at a [`Placement`](denise_forms::Placement), which is\n\
438 \x20 /// what the file's own `scaling=` works out to.\n\
439 \x20 ///\n\
440 \x20 /// The typed door onto [`Form::build_fitted`](denise_forms::Form::build_fitted),\n\
441 \x20 /// so a generated form scales exactly as a loaded one does.\n\
442 \x20 ///\n\
443 \x20 /// # Errors\n\
444 \x20 ///\n\
445 \x20 /// Whatever [`denise_forms::Form::build_fitted`] says, with a line and a column.\n\
446 \x20 pub fn place(\n\
447 \x20 ui: &mut ::denise_ui::Ui<{message}>,\n\
448 \x20 parent: ::denise_ui::NodeId,\n\
449 \x20 fit: ::denise_forms::Placement,\n\
450 \x20 assets: &mut dyn FnMut(&str) -> Option<::denise_forms::Picture>,\n\
451 \x20 ) -> ::core::result::Result<Self, ::denise_forms::Error> {{\n\
452 \x20 let form = ::denise_forms::Form::parse({0}_SOURCE)?;\n\
453 \x20 let built = form.build_fitted(ui, parent, fit, &mut {kind}Wiring {{ assets }})?;\n\
454 \x20 Ok(Self {{\n",
455 upper(kind),
456 ));
457 for (field, name) in fields {
458 out.push_str(&format!(
459 " {field}: built.node({name:?}).expect(\"the form names it, and this file was generated from that form\"),\n"
460 ));
461 }
462 if fields.is_empty() {
463 out.push_str(" _private: (),\n");
464 }
465 out.push_str(" })\n }\n\n");
466
467 out.push_str(&format!(
469 " /// What the form was designed at, and what it is called.\n\
470 \x20 ///\n\
471 \x20 /// # Panics\n\
472 \x20 ///\n\
473 \x20 /// Never: the source was parsed at build time to generate this.\n\
474 \x20 pub fn form() -> ::denise_forms::Form {{\n\
475 \x20 ::denise_forms::Form::parse({}_SOURCE).expect(\"generated from this very text\")\n\
476 \x20 }}\n}}\n\n",
477 upper(kind),
478 ));
479
480 out.push_str(&format!(
482 "/// Turns the form's message names into [`{message}`].\n\
483 ///\n\
484 /// One arm per name, generated — so a name the form uses and this does\n\
485 /// not answer is impossible rather than an error at load.\n\
486 struct {kind}Wiring<'a> {{\n\
487 \x20 assets: &'a mut dyn FnMut(&str) -> Option<::denise_forms::Picture>,\n\
488 }}\n\n\
489 impl ::denise_forms::Wiring<{message}> for {kind}Wiring<'_> {{\n\
490 \x20 fn message(\n\
491 \x20 &mut self,\n\
492 \x20 name: &str,\n\
493 \x20 payload: ::denise_forms::Payload,\n\
494 \x20 ) -> Option<::denise_forms::Handler<{message}>> {{\n\
495 \x20 Some(match (name, payload) {{\n"
496 ));
497 for Message {
498 variant,
499 name,
500 payload,
501 ..
502 } in messages
503 {
504 let arm = match payload {
505 Payload::None => format!("::denise_forms::Handler::Plain({message}::{variant})"),
506 Payload::Bool => format!("::denise_forms::Handler::Bool({message}::{variant})"),
507 Payload::Index => format!("::denise_forms::Handler::Index({message}::{variant})"),
508 Payload::Number => format!("::denise_forms::Handler::Number({message}::{variant})"),
509 };
510 out.push_str(&format!(
511 " ({name:?}, ::denise_forms::Payload::{:?}) => {arm},\n",
512 payload
513 ));
514 }
515 out.push_str(
516 " _ => return None,\n })\n }\n\n\
517 \x20 fn asset(&mut self, path: &str) -> Option<::denise_forms::Picture> {\n\
518 \x20 (self.assets)(path)\n }\n}\n",
519 );
520 out
521}
522
523fn upper(kind: &str) -> String {
525 let mut out = String::new();
526 for (index, character) in kind.chars().enumerate() {
527 if character.is_uppercase() && index > 0 {
528 out.push('_');
529 }
530 out.extend(character.to_uppercase());
531 }
532 out
533}
534
535fn type_name(name: &str) -> Option<String> {
537 let mut out = String::new();
538 let mut upper = true;
539 for character in name.chars() {
540 if character == '-' || character == '_' || character == ' ' {
541 upper = true;
542 continue;
543 }
544 if !character.is_ascii_alphanumeric() {
545 return None;
546 }
547 if upper {
548 out.extend(character.to_uppercase());
549 upper = false;
550 } else {
551 out.push(character);
552 }
553 }
554 (!out.is_empty() && out.starts_with(|c: char| c.is_ascii_alphabetic())).then_some(out)
555}
556
557fn variant_name(name: &str) -> Result<String, Error> {
559 type_name(name).ok_or_else(|| {
560 Error::new(
561 At::START,
562 Reason::NotAnIdentifier {
563 found: name.to_string(),
564 because: "a message name becomes an enum variant, so it must be letters, \
565 digits and dashes, starting with a letter",
566 },
567 )
568 })
569}
570
571fn field_name(name: &str) -> Result<String, Error> {
573 let refuse = |because| {
574 Err(Error::new(
575 At::START,
576 Reason::NotAnIdentifier {
577 found: name.to_string(),
578 because,
579 },
580 ))
581 };
582 if name.is_empty() {
583 return refuse("a name has to be something");
584 }
585 if NEVER_RAW.contains(&name) {
586 return refuse("Rust will not accept this word as an identifier, even raw");
587 }
588 let mut out = String::new();
589 for character in name.chars() {
590 match character {
591 '-' | ' ' => out.push('_'),
592 c if c.is_ascii_alphanumeric() || c == '_' => out.push(c),
593 _ => return refuse("a field name is letters, digits, dashes and underscores"),
594 }
595 }
596 if !out.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') {
597 return refuse("a field name has to start with a letter or an underscore");
598 }
599 if KEYWORDS.contains(&out.as_str()) {
600 out.insert_str(0, "r#");
601 }
602 Ok(out)
603}