Skip to main content

max / quasi

Place a row's parts by role, and give a picture a body Two productions, both from MNW's embeds: `beside <RowPart> <emission>` inside a `row`, and `picture <src> <alt>` with `fit` on its body. `Row::part` had no keyword, so a row could hold controls and settings and nothing placed by role. `beside` is what a run already spells and the container decides which enum the word names, the same way `include` already means `.act()` in a row and `.part()` in a cell. `picture` is the one node member whose body does not set on a `Node`. `Node::Image` holds an `Image` and `fit` is that type's builder, so the body accretes onto the picture and the variant goes on afterwards. `fit` joins the vocabulary table beside `tone`, `width` and `priority`.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-09-04 00:16 UTC
Signed with PGP, not checked
Commit: 092a3c9a47cace09217a12ee8da733abe35c6e8d
Parent: c4a2434
2 files changed, +40 insertions, -2 deletions
@@ -46,6 +46,8 @@
46 46 Cell,
47 47 /// One node member: its body says what it is told afterwards.
48 48 Node,
49 + /// One picture: its body sets on the `Image`, and it emits nothing.
50 + Image,
49 51 }
50 52
51 53 pub fn declaration(declaration: &Declaration) -> Result<TokenStream> {
@@ -541,10 +543,24 @@
541 543 quote!(.act(#held)),
542 544 ))
543 545 }
546 + // A row's parts are placed by role rather than ranked by priority,
547 + // which is the other thing `beside` means and the container is what
548 + // says which. embeds' button strip is the site: a thumbnail and a title
549 + // in `Primary`, the buy control in `Actions`, the price a setting
550 + // between them.
551 + (Container::Row, Emission::Beside { priority, inner }) => Ok((
552 + node(inner)?,
553 + quote!(.part(::quasi_router::layout::RowPart::#priority, #held)),
554 + )),
544 555 (Container::Row, other) => Err(syn::Error::new(
545 556 emission_span(other),
546 - "a row holds controls and says what opening it does: write \
547 - `act <label> to <action>;`, `include <shape>;` or `activate to <action>;`",
557 + "a row holds controls, places its parts and says what opening it does: write \
558 + `act <label> to <action>;`, `include <shape>;`, `beside <part> <emission>` \
559 + or `activate to <action>;`",
560 + )),
561 + (Container::Image, other) => Err(syn::Error::new(
562 + emission_span(other),
563 + "a picture holds nothing: its body says how it sits in its box",
548 564 )),
549 565 (Container::Run, Emission::Beside { priority, inner }) => Ok((
550 566 node(inner)?,
@@ -701,6 +717,21 @@
701 717 /// One emission as a `Node` value.
702 718 fn node(emission: &Emission) -> Result<TokenStream> {
703 719 match emission {
720 + Emission::Simple { member, args, body } if member == "picture" => {
721 + // The one member whose body does not set on a `Node`. `Node::Image`
722 + // holds an `Image`, and `fit`, `lazy`, `caption` and `intrinsic`
723 + // are that type's builders, so the body accretes onto the picture
724 + // and the variant goes on afterwards. embeds' cover is the site: a
725 + // 40-pixel thumbnail whose art is any shape, which is `Fit::Cover`
726 + // and is unsayable without a body.
727 + let args = args.iter().map(self::arg).collect::<Result<Vec<_>>>()?;
728 + let picture = accrete(
729 + body,
730 + Container::Image,
731 + &quote!(::quasi_router::Image::new(#(#args),*)),
732 + )?;
733 + Ok(quote!(::quasi_router::Node::Image(#picture)))
734 + }
704 735 Emission::Simple { member, args, body } => {
705 736 let args = args.iter().map(self::arg).collect::<Result<Vec<_>>>()?;
706 737 accrete(
@@ -54,6 +54,11 @@
54 54 /// The members that are one of `Node`'s own constructors, called by its name.
55 55 /// Growing this list is the whole of adding one.
56 56 ///
57 + /// `picture` is the one exception and it is the last entry for that reason:
58 + /// [`Node::Image`] carries a builder type of its own, so the member's body sets
59 + /// on the `Image` and the emitter wraps the result rather than calling a
60 + /// constructor on `Node`. Everything else here is `Node::<name>`.
61 + ///
57 62 /// **A name that is a setting on any container cannot go here.** An ident inside
58 63 /// a body is a member if it is in one of these two lists and a setting if it is
59 64 /// not, and that is the whole of how the two are told apart, so one name cannot
@@ -74,6 +79,7 @@
74 79 "empty",
75 80 "rich",
76 81 "literal",
82 + "picture",
77 83 ];
78 84
79 85 /// The arrangements a screen may be laid out in.
@@ -95,6 +101,7 @@
95 101 ("tone", "Tone"),
96 102 ("width", "Width"),
97 103 ("priority", "Priority"),
104 + ("fit", "Fit"),
98 105 ];
99 106
100 107 /// The attributes the generated function may carry.