compose_rust/schema.rs
1//! Closed Rust-side schema and metadata used for Kotlin code generation.
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4pub struct EnumVariantSchema {
5 pub name: &'static str,
6 pub tag: u16,
7}
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum FieldType {
11 Float,
12 U32,
13 U64,
14 /// A `Paint`, either a `ColorRole` or a literal ARGB `Color`.
15 Paint,
16 /// A role enum, named so codegen can emit the matching Kotlin type.
17 Role(&'static str),
18}
19
20/// Which half of which `(first, second)` word a modifier field occupies.
21///
22/// Two `f32` share one `u64`: the low 32 bits hold the first value.
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum FieldSlot {
25 FirstLow,
26 FirstHigh,
27 First,
28 SecondLow,
29 SecondHigh,
30 Second,
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub struct FieldSchema {
35 pub name: &'static str,
36 pub ty: FieldType,
37 pub slot: FieldSlot,
38}
39
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub struct VariantSchema {
42 pub name: &'static str,
43 pub tag: u16,
44 pub fields: &'static [FieldSchema],
45}
46
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48pub enum EventPayloadType {
49 None,
50 Text,
51 ProtocolError,
52 KeyDown,
53 Range,
54 /// One `f64`. Every value-carrying widget shares it, so the same concept has one
55 /// name on the wire whether the value counts days or slides between two ends.
56 Double,
57 WindowSize,
58 /// One `u16`: the tag of the design system the Renderer resolved the theme to.
59 DesignSystem,
60 /// A `u32` saying what was pressed (0 for the body, 1 or 2 for an action button), then
61 /// the notification's key as a string reference.
62 NotificationActivation,
63 /// One `u32`: the notification permission the Renderer last saw.
64 NotificationPermission,
65}
66
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub struct EventSchema {
69 pub name: &'static str,
70 pub tag: u16,
71 pub payload: EventPayloadType,
72}
73
74/// Canonical schema text. Variant order is wire-significant and must only be appended to.
75pub const SCHEMA_DESCRIPTOR: &str = concat!(
76 "compose-rust/v1;",
77 "widgets=Column,Row,Box,Text,TextField,Button,Spacer,LazyColumn,ScrollColumn,Image,Icon,Checkbox,RadioButton,Switch,Slider,ProgressIndicator,Divider,Card,Surface,Dialog,Menu,Tabs,TopAppBar,LazyRow,Tooltip,Canvas,DatePicker,TimePicker,Dropdown,Navigation,NavigationItem,Sheet,Scaffold,ScaffoldSlot,LazyGrid,FileDropTarget,ScrollRow,Chip,FloatingAction,Badge,SelectionContainer,SplitPane;",
78 "properties=text,placeholder,enabled,multiline,on_click,on_value_change,on_submit,on_focus_lost,on_key_down,item_count,item_key,on_range_requested,type_role,font_size,font_weight,line_height,letter_spacing,color,text_align,max_lines,overflow,arrangement,spacing,space_role,alignment,variant,asset,checked,steps,determinate,circular,vertical,open,on_dismiss,selected_index,commands,value,min,max,icon,slot,columns,min_column_width,spans,on_files_entered,on_files_dropped,section,count,collapsible;",
79 "modifiers=Empty,Padding,FillMaxWidth,FillMaxHeight,Width,Height,Size,Background,Clickable,PaddingRole,PaddingEach,Weight,Shape,ShapeRole,Border,Elevation,ObserveSize,Motion,Material;",
80 "keys=Enter;",
81 "events=Clicked,TextChanged,TextSubmitted,FocusLost,ProtocolError,KeyDown,RangeRequested,ValueChanged,WindowSizeChanged,DesignSystemResolved,FilesEntered,FilesDropped,NotificationActivated,NotificationPermissionChanged;",
82 "windowsizeclasses=Compact,Medium,Expanded;",
83 "windowheightclasses=Compact,Medium,Expanded;",
84 "commands=Create,SetProp,SetModifier,Insert,Move,Remove,SetText,AppendText,SetTheme,SetWindow,RegisterAsset,ReleaseAsset,ShowMessage,PostNotification,WithdrawNotification,RequestNotificationPermission"
85);
86
87const fn hash_bytes(mut hash: u64, bytes: &[u8]) -> u64 {
88 let mut index = 0;
89 while index < bytes.len() {
90 hash ^= bytes[index] as u64;
91 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
92 index += 1;
93 }
94 hash
95}
96
97const fn hash_enum_schema(mut hash: u64, schema: &[EnumVariantSchema]) -> u64 {
98 let mut index = 0;
99 while index < schema.len() {
100 hash = hash_bytes(hash, schema[index].name.as_bytes());
101 hash = hash_bytes(hash, &schema[index].tag.to_le_bytes());
102 index += 1;
103 }
104 hash
105}
106
107const fn schema_hash() -> u64 {
108 let mut hash = hash_bytes(0xcbf2_9ce4_8422_2325_u64, SCHEMA_DESCRIPTOR.as_bytes());
109 hash = hash_bytes(hash, crate::extensions::SCHEMA_DESCRIPTOR.as_bytes());
110 hash = hash_enum_schema(hash, WIDGET_SCHEMA);
111 hash = hash_enum_schema(hash, PROPERTY_SCHEMA);
112 hash = hash_enum_schema(hash, KEY_SCHEMA);
113 hash = hash_enum_schema(hash, WINDOW_SIZE_CLASS_SCHEMA);
114 hash = hash_enum_schema(hash, WINDOW_HEIGHT_CLASS_SCHEMA);
115 let mut role_index = 0;
116 while role_index < ROLE_ENUM_SCHEMA.len() {
117 hash = hash_bytes(hash, ROLE_ENUM_SCHEMA[role_index].name.as_bytes());
118 hash = hash_enum_schema(hash, ROLE_ENUM_SCHEMA[role_index].variants);
119 role_index += 1;
120 }
121 let mut index = 0;
122 while index < MODIFIER_SCHEMA.len() {
123 let modifier = MODIFIER_SCHEMA[index];
124 hash = hash_bytes(hash, modifier.name.as_bytes());
125 hash = hash_bytes(hash, &modifier.tag.to_le_bytes());
126 let mut field_index = 0;
127 while field_index < modifier.fields.len() {
128 let field = modifier.fields[field_index];
129 hash = hash_bytes(hash, field.name.as_bytes());
130 hash = hash_bytes(
131 hash,
132 &[match field.ty {
133 FieldType::Float => 1,
134 FieldType::U32 => 2,
135 FieldType::U64 => 3,
136 FieldType::Paint => 4,
137 FieldType::Role(_) => 5,
138 }],
139 );
140 if let FieldType::Role(role) = field.ty {
141 hash = hash_bytes(hash, role.as_bytes());
142 }
143 hash = hash_bytes(
144 hash,
145 &[match field.slot {
146 FieldSlot::FirstLow => 0,
147 FieldSlot::FirstHigh => 1,
148 FieldSlot::First => 2,
149 FieldSlot::SecondLow => 3,
150 FieldSlot::SecondHigh => 4,
151 FieldSlot::Second => 5,
152 }],
153 );
154 field_index += 1;
155 }
156 index += 1;
157 }
158 index = 0;
159 while index < crate::drawing::DRAW_COMMAND_SCHEMA.len() {
160 let command = crate::drawing::DRAW_COMMAND_SCHEMA[index];
161 hash = hash_bytes(hash, command.name.as_bytes());
162 hash = hash_bytes(hash, &command.tag.to_le_bytes());
163 let mut field_index = 0;
164 while field_index < command.fields.len() {
165 let field = command.fields[field_index];
166 hash = hash_bytes(hash, field.name.as_bytes());
167 hash = hash_bytes(hash, &[field.word]);
168 hash = hash_bytes(
169 hash,
170 &[match field.ty {
171 crate::drawing::DrawFieldType::Float => 1,
172 crate::drawing::DrawFieldType::U32 => 2,
173 crate::drawing::DrawFieldType::Role(_) => 3,
174 }],
175 );
176 if let crate::drawing::DrawFieldType::Role(role) = field.ty {
177 hash = hash_bytes(hash, role.as_bytes());
178 }
179 field_index += 1;
180 }
181 index += 1;
182 }
183 index = 0;
184 while index < EVENT_SCHEMA.len() {
185 let event = EVENT_SCHEMA[index];
186 hash = hash_bytes(hash, event.name.as_bytes());
187 hash = hash_bytes(hash, &event.tag.to_le_bytes());
188 hash = hash_bytes(
189 hash,
190 &[match event.payload {
191 EventPayloadType::None => 0,
192 EventPayloadType::Text => 1,
193 EventPayloadType::ProtocolError => 2,
194 EventPayloadType::KeyDown => 3,
195 EventPayloadType::Range => 4,
196 EventPayloadType::Double => 5,
197 EventPayloadType::WindowSize => 6,
198 EventPayloadType::DesignSystem => 7,
199 EventPayloadType::NotificationActivation => 8,
200 EventPayloadType::NotificationPermission => 9,
201 }],
202 );
203 index += 1;
204 }
205 hash
206}
207
208/// Stable handshake hash of the complete ordered schema metadata.
209pub const SCHEMA_HASH: u64 = schema_hash();
210pub const PROTOCOL_VERSION: u16 = 1;
211
212fn wire_name_eq(input: &str, schema_name: &str) -> bool {
213 input
214 .bytes()
215 .filter(|byte| *byte != b'_')
216 .map(|byte| byte.to_ascii_lowercase())
217 .eq(schema_name.bytes().map(|byte| byte.to_ascii_lowercase()))
218}
219
220macro_rules! define_wire_enum {
221 ($schema:ident, $name:ident { $($variant:ident = $tag:literal),+ $(,)? }) => {
222 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
223 #[repr(u16)]
224 pub enum $name {
225 $($variant = $tag),+
226 }
227
228 pub const $schema: &[EnumVariantSchema] = &[
229 $(EnumVariantSchema { name: stringify!($variant), tag: $tag }),+
230 ];
231
232 impl From<$name> for u16 {
233 fn from(value: $name) -> Self {
234 value as Self
235 }
236 }
237
238 impl TryFrom<u16> for $name {
239 type Error = ();
240
241 // Spelled `()` rather than `Self::Error`: a `ColorRole::Error` variant would
242 // otherwise make the associated item ambiguous.
243 fn try_from(value: u16) -> Result<Self, ()> {
244 match value {
245 $($tag => Ok(Self::$variant),)+
246 _ => Err(()),
247 }
248 }
249 }
250
251 impl $name {
252 #[allow(dead_code)]
253 #[doc(hidden)]
254 #[allow(clippy::result_unit_err)]
255 pub fn from_name(value: &str) -> Result<Self, ()> {
256 $(if wire_name_eq(value, stringify!($variant)) {
257 return Ok(Self::$variant);
258 })+
259 Err(())
260 }
261 }
262 };
263}
264
265crate::extensions::define_widget_schema_with_extensions!(define_wire_enum; WIDGET_SCHEMA, WidgetKind {
266 Column = 1,
267 Row = 2,
268 Box = 3,
269 Text = 4,
270 TextField = 5,
271 Button = 6,
272 Spacer = 7,
273 LazyColumn = 8,
274 ScrollColumn = 9,
275 Image = 10,
276 Icon = 11,
277 // The selection controls and the indicators. Each one emits a state and a role and
278 // nothing about how it is drawn: the tick, the track, the thumb and the sweep of an
279 // indeterminate bar are the design system's.
280 Checkbox = 12,
281 RadioButton = 13,
282 Switch = 14,
283 Slider = 15,
284 ProgressIndicator = 16,
285 Divider = 17,
286 Card = 18,
287 Surface = 19,
288 Dialog = 20,
289 Menu = 21,
290 Tabs = 22,
291 TopAppBar = 23,
292 LazyRow = 24,
293 Tooltip = 25,
294 // Drawing commands instead of child nodes. The command list is its only property.
295 Canvas = 26,
296 DatePicker = 27,
297 TimePicker = 28,
298 Dropdown = 29,
299 // One declaration, three presentations. The Renderer picks a bottom bar, a rail or a
300 // permanent drawer from the width it has already measured, so the same tree looks
301 // native on a phone and on a desktop without the Host branching on the size class.
302 Navigation = 30,
303 // One destination. Its label and its icon are properties rather than children,
304 // because a child tree would fix the arrangement the presentations need to differ in.
305 NavigationItem = 31,
306 // A temporary surface that slides in from an edge of the screen. Which edge is the
307 // Renderer's decision, for the same reason the navigation presentation is.
308 Sheet = 32,
309 // The screen's frame. The application fills slots and the Renderer decides what each
310 // one becomes on this platform and at this width, which is the same decision
311 // `Navigation` already makes for itself, widened to the whole frame.
312 Scaffold = 33,
313 // One filled slot. A wrapper rather than a property on the slot's own root, because
314 // an application may put any tree in a slot and the Renderer has to know which slot
315 // it is looking at without reading into it.
316 ScaffoldSlot = 34,
317 // A lazy grid. The windowing protocol is the list's, unchanged: the Renderer asks for a
318 // range of items and the Host materialises exactly that range. What the grid adds is
319 // that the Renderer rounds the range it asks for to whole rows, because a row is what
320 // it lays out, and how many items a row holds is its own decision when the columns
321 // were given as a minimum width rather than a count.
322 LazyGrid = 35,
323 // A place files may be dropped. Being this widget is the willingness: a node that is
324 // not one is never offered as a target, so the platform shows no drop cursor over it
325 // and nothing is reported.
326 FileDropTarget = 36,
327 // The whole content with a horizontal scroll, which is a ScrollColumn turned on its
328 // side. Every child is materialised and the scroll position is the Renderer's, so
329 // nothing about scrolling crosses the boundary; a long horizontal list is a LazyRow.
330 ScrollRow = 37,
331 // A small token that is chosen or filters. It carries its label and whether it is
332 // chosen, and the chosen state is the Host's: it changes only because the Host's own
333 // click handler changed it. The shape is the design system's.
334 Chip = 38,
335 // The one action a screen is about. It carries an icon, a label and a click. Where it
336 // goes and what it is drawn as is the design system's: a disc floating over the page
337 // is one system's answer, a plus at the trailing end of the bar is another's, and an
338 // accent button at the head of the command bar is a third.
339 FloatingAction = 39,
340 // A small mark on something else that says how many, that there is something new, or
341 // one short word. With one child it is attached to that child; with none it stands on
342 // its own, at the end of a row. Where it sits on the child, its shape, and how a large
343 // count is shortened are the design system's: nothing here can ask for a corner or a
344 // ceiling.
345 Badge = 40,
346 // A region whose text can be dragged over and copied, as one selection across every
347 // Text inside it. Text outside one cannot be selected. The selection and the copy are
348 // the Renderer's entirely, so nothing about either crosses the boundary.
349 SelectionContainer = 41,
350 // Two panes side by side, a side pane and a body, with a divider the user drags to
351 // change the side pane's width. Exactly two children, the side pane first.
352 //
353 // The drag is the Renderer's, like a scroll position: nothing crosses the boundary
354 // while it moves, and the width it ends on is reported once through the value change
355 // event. How the divider looks and how far its grip reaches, the side pane's default
356 // width, the body's minimum, and whether a narrow place shows the two panes side by
357 // side, with the side pane laid over the body, or one at a time, are the design
358 // system's answers.
359 SplitPane = 42,
360});
361
362#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
363#[repr(u8)]
364pub enum LoopMode {
365 #[default]
366 Renderer = 0,
367 Platform = 1,
368}
369
370#[derive(Clone, Copy, Debug, Eq, PartialEq)]
371pub struct Selection {
372 pub start: u32,
373 pub end: u32,
374}
375
376// Compose key identities supported by the M0 schema.
377define_wire_enum!(KEY_SCHEMA, Key {
378 Enter = 1,
379});
380
381define_wire_enum!(WINDOW_SIZE_CLASS_SCHEMA, WindowSizeClass {
382 Compact = 0,
383 Medium = 1,
384 Expanded = 2,
385});
386
387impl WindowSizeClass {
388 /// The width in dp at which `Medium` begins, and the width at which `Expanded` does.
389 ///
390 /// These are the Material 3 window size class boundaries. Using different numbers
391 /// would make the Compose components adapt at one width and the Rust layout branch at
392 /// another, and that mismatch is visible on screen.
393 pub const MEDIUM_MIN_WIDTH_DP: f32 = 600.0;
394 pub const EXPANDED_MIN_WIDTH_DP: f32 = 840.0;
395
396 /// The class a window of this width belongs to. Height does not take part.
397 pub fn from_width_dp(width_dp: f32) -> Self {
398 if width_dp >= Self::EXPANDED_MIN_WIDTH_DP {
399 Self::Expanded
400 } else if width_dp >= Self::MEDIUM_MIN_WIDTH_DP {
401 Self::Medium
402 } else {
403 Self::Compact
404 }
405 }
406}
407
408define_wire_enum!(WINDOW_HEIGHT_CLASS_SCHEMA, WindowHeightClass {
409 Compact = 0,
410 Medium = 1,
411 Expanded = 2,
412});
413
414impl WindowHeightClass {
415 /// The height in dp at which `Medium` begins, and the height at which `Expanded` does.
416 ///
417 /// The same published table the width boundaries came from, so a layout that folds
418 /// when it runs out of vertical room folds at the height Compose's own components
419 /// treat as short.
420 pub const MEDIUM_MIN_HEIGHT_DP: f32 = 480.0;
421 pub const EXPANDED_MIN_HEIGHT_DP: f32 = 900.0;
422
423 /// The class a window of this height belongs to. Width does not take part.
424 pub fn from_height_dp(height_dp: f32) -> Self {
425 if height_dp >= Self::EXPANDED_MIN_HEIGHT_DP {
426 Self::Expanded
427 } else if height_dp >= Self::MEDIUM_MIN_HEIGHT_DP {
428 Self::Medium
429 } else {
430 Self::Compact
431 }
432 }
433}
434
435/// A role enum that codegen mirrors into Kotlin.
436#[derive(Clone, Copy, Debug, Eq, PartialEq)]
437pub struct RoleEnumSchema {
438 pub name: &'static str,
439 pub variants: &'static [EnumVariantSchema],
440}
441
442// An opaque ARGB colour. Gradients and image brushes are deliberately absent: they do not
443// fit the two words a modifier variant has, and they would need resources to cross the
444// boundary, which the fixed-layout records cannot carry.
445#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
446#[repr(transparent)]
447pub struct Color(pub u32);
448
449impl Color {
450 /// An opaque colour from a 24-bit `0xRRGGBB` value.
451 pub const fn rgb(value: u32) -> Self {
452 Self(0xff00_0000 | (value & 0x00ff_ffff))
453 }
454
455 /// A colour from a full 32-bit `0xAARRGGBB` value.
456 pub const fn argb(value: u32) -> Self {
457 Self(value)
458 }
459
460 pub const fn to_argb(self) -> u32 {
461 self.0
462 }
463}
464
465// Semantic colour slots. The design system resolves them, inside the Renderer.
466define_wire_enum!(COLOR_ROLE_SCHEMA, ColorRole {
467 Primary = 1,
468 OnPrimary = 2,
469 Secondary = 3,
470 OnSecondary = 4,
471 Surface = 5,
472 OnSurface = 6,
473 SurfaceVariant = 7,
474 OnSurfaceVariant = 8,
475 Background = 9,
476 OnBackground = 10,
477 Outline = 11,
478 OutlineVariant = 12,
479 Error = 13,
480 OnError = 14,
481 // The layer a panel is made of: a thing raised off the page, holding the page's own
482 // reading ink. It exists because `Surface` cannot do that job everywhere. Material 3
483 // gives `Surface` and `Background` one value on purpose and expresses depth through
484 // tonal containers instead, so a panel filled with `Surface` on a page of `Background`
485 // is drawn, in the right colour, and is invisible. `SurfaceVariant` is not the answer
486 // either: its ink pair is the muted secondary ink, so a panel holding primary content
487 // would have to set that content half faded. This is the one role every design system
488 // promises to make visible against the page, and `OnSurface` is what reads on it.
489 SurfaceContainer = 15,
490 // The third accent, and a quiet fill for each of the three.
491 //
492 // Two accents and a page say a button, a bar and a heading. They cannot say a grid of
493 // subject tiles where the subject is the colour, a mood picker, or a panel of costs
494 // beside a panel of totals. Those screens need fills that read as relatives of one
495 // another, are not reading surfaces, and are quiet enough that body text sits on them.
496 //
497 // A container is a colour in the table rather than its accent at a lower opacity.
498 // Opacity only means something once you know what is behind it, and a role has to
499 // answer before anyone knows that. So each container is its own value and carries its
500 // own ink, held to the body-text bound rather than the label bound, because the reason
501 // it exists is that paragraphs land on it.
502 //
503 // The three containers are not required to be told apart from each other. Material 3's
504 // baseline primary and secondary containers are neighbouring tones of one palette, and
505 // that is its published scheme, not a mistake. A caller who needs three fills that
506 // separate at a glance reaches for the tertiary pair.
507 Tertiary = 16,
508 OnTertiary = 17,
509 PrimaryContainer = 18,
510 OnPrimaryContainer = 19,
511 SecondaryContainer = 20,
512 OnSecondaryContainer = 21,
513 TertiaryContainer = 22,
514 OnTertiaryContainer = 23,
515 // What a highlighter paints code with. A conversation shows code blocks and diffs, and
516 // with only literals to paint them the same grey comment would be stamped into seven
517 // systems and two schemes, which is how a light comment grey disappears on a dark page.
518 // So a token kind is a role like any other, and each system answers it from the code
519 // editor of its own platform.
520 //
521 // The split follows what tree-sitter's highlight names, VS Code's semantic token kinds
522 // and TextMate scopes all agree on. Each of those divides more finely, and the finer
523 // names fold into one of these (`keyword.return` is a keyword). A token that fits none
524 // is left in the body ink rather than given a colour of its own.
525 //
526 // The fifteen syntax inks hold the body-text bound on the code block's panel, comments
527 // included: a comment can be quieter in hue and lightness, but one nobody can read is
528 // a comment that is not there.
529 SyntaxKeyword = 24,
530 SyntaxString = 25,
531 SyntaxComment = 26,
532 SyntaxNumber = 27,
533 SyntaxConstant = 28,
534 SyntaxType = 29,
535 SyntaxFunction = 30,
536 SyntaxVariable = 31,
537 SyntaxProperty = 32,
538 SyntaxOperator = 33,
539 SyntaxPunctuation = 34,
540 SyntaxTag = 35,
541 SyntaxAttribute = 36,
542 SyntaxEscape = 37,
543 SyntaxMacro = 38,
544 // A diff. The ink marks an added or removed line and its text; the container is the
545 // whole line's background, drawn as the line node's own background; the emphasis is
546 // the background behind the words that actually changed, drawn by the span. Added and
547 // removed are never told apart by colour alone: the `+` and `-` at the head of a line
548 // stay, because the two backgrounds can look the same to a red-green colour blind
549 // reader.
550 DiffAdded = 39,
551 DiffRemoved = 40,
552 DiffModified = 41,
553 DiffAddedContainer = 42,
554 DiffRemovedContainer = 43,
555 DiffAddedEmphasis = 44,
556 DiffRemovedEmphasis = 45,
557});
558
559/// How many colour roles there are, which is how many rows every token table has and how
560/// many slots a palette carries per scheme.
561pub const COLOR_ROLE_COUNT: usize = 45;
562
563// The nine-rung type ladder every supported design system maps onto.
564define_wire_enum!(TYPE_ROLE_SCHEMA, TypeRole {
565 Display = 1,
566 Headline = 2,
567 Title = 3,
568 Subtitle = 4,
569 Body = 5,
570 BodyStrong = 6,
571 Label = 7,
572 Caption = 8,
573 Mono = 9,
574});
575
576// Corner roles. The radius is the design system's decision.
577define_wire_enum!(SHAPE_ROLE_SCHEMA, ShapeRole {
578 None = 1,
579 ExtraSmall = 2,
580 Small = 3,
581 Medium = 4,
582 Large = 5,
583 Full = 6,
584});
585
586// How long a change takes and along which curve. A role rather than a duration, because
587// Material's emphasized curve and Cupertino's spring are different answers to the same
588// question, and a milliseconds figure from the application would settle it in the wrong
589// place. A system asked to reduce motion answers every one of these instantly.
590define_wire_enum!(MOTION_ROLE_SCHEMA, MotionRole {
591 Instant = 1,
592 Quick = 2,
593 Standard = 3,
594 Slow = 4,
595 Emphasized = 5,
596});
597
598// What a surface is made of. Four roles rather than a blur radius, because Cupertino and
599// Liquid Glass answer with blur, Material 3 with elevation and a tone laid over the
600// surface, and the GNOME and KDE systems with an opaque fill. A radius from the
601// application would be a blur instruction to systems that do not blur.
602define_wire_enum!(MATERIAL_ROLE_SCHEMA, MaterialRole {
603 Thin = 1,
604 Regular = 2,
605 Thick = 3,
606 Chrome = 4,
607});
608
609// Density roles, because dp density differs per design system.
610define_wire_enum!(SPACE_ROLE_SCHEMA, SpaceRole {
611 None = 1,
612 Xs = 2,
613 Sm = 3,
614 Md = 4,
615 Lg = 5,
616 Xl = 6,
617 Xxl = 7,
618});
619
620define_wire_enum!(TEXT_ALIGN_SCHEMA, TextAlign {
621 Start = 1,
622 Center = 2,
623 End = 3,
624 Justify = 4,
625});
626
627define_wire_enum!(TEXT_OVERFLOW_SCHEMA, TextOverflow {
628 Clip = 1,
629 Ellipsis = 2,
630 Visible = 3,
631});
632
633define_wire_enum!(ARRANGEMENT_SCHEMA, Arrangement {
634 Start = 1,
635 Center = 2,
636 End = 3,
637 SpaceBetween = 4,
638 SpaceAround = 5,
639 SpaceEvenly = 6,
640});
641
642define_wire_enum!(ALIGNMENT_SCHEMA, Alignment {
643 TopStart = 1,
644 TopCenter = 2,
645 TopEnd = 3,
646 CenterStart = 4,
647 Center = 5,
648 CenterEnd = 6,
649 BottomStart = 7,
650 BottomCenter = 8,
651 BottomEnd = 9,
652});
653
654// The neutral component variant names the design system styles.
655define_wire_enum!(BUTTON_VARIANT_SCHEMA, ButtonVariant {
656 Filled = 1,
657 Tonal = 2,
658 Outlined = 3,
659 Text = 4,
660 Operator = 5,
661});
662
663// What the bytes behind an asset id are. A kind the Renderer cannot read is a reported
664// protocol error, not a guess.
665define_wire_enum!(ASSET_KIND_SCHEMA, AssetKind {
666 Png = 1,
667 Jpeg = 2,
668 Svg = 3,
669 VectorIcon = 4,
670 // A font file, registered so that a type role can resolve to it. The bytes are the
671 // font, which is why this is an asset and not a name: a name would put the check that
672 // the font exists at run time on the reader's machine, and a missing one there is a
673 // screen in the wrong typeface with nobody to tell.
674 Font = 5,
675 // A gradient or an image fill, registered so that any Paint may name it. The bytes
676 // are the brush's own fixed layout rather than a file: nothing on any platform stores
677 // a list of stops as a document, and a format would be a parser to write and keep.
678 Brush = 6,
679});
680
681// How a brush carries on past the area it was given.
682define_wire_enum!(TILE_MODE_SCHEMA, TileMode {
683 // Stops at the edge and drags the last colour outwards, which is what a gradient
684 // filling a surface wants.
685 Clamp = 1,
686 Repeat = 2,
687 // Repeats, flipping every other copy, so the seams do not show.
688 Mirror = 3,
689});
690
691// The closed set of icon meanings. An icon is addressed by what it is for, never by a
692// system icon name: a name would defer the check that the icon exists to run time, and it
693// would pin one platform's artwork into the protocol. The role is what lets the same
694// declaration come out as SF Symbols under Cupertino and Material Symbols under Material 3.
695define_wire_enum!(ICON_ROLE_SCHEMA, IconRole {
696 Back = 1,
697 Forward = 2,
698 Close = 3,
699 Search = 4,
700 Add = 5,
701 Check = 6,
702 Settings = 7,
703 More = 8,
704 // The meanings a set of destinations needs before any other: where the application
705 // starts, the list it is mostly about, and what has arrived.
706 Home = 9,
707 List = 10,
708 Inbox = 11,
709 // What a window's own bar needs: the button that opens everything this screen does
710 // not have room for, and the one that goes back over what has already happened. Both
711 // are in the reference bar of more than one sample, and neither is a destination, so
712 // neither could be said with the three above.
713 Menu = 12,
714 History = 13,
715 // What putting the samples beside the applications they are drawn from turned up: eight
716 // meanings the set could not say. Each is a meaning rather than a picture, so a system
717 // that draws sending as a paper aeroplane is still answering `Send`.
718 //
719 // None of them is an existing role used loosely. A composer's send was drawn with
720 // `Forward` for a while, and an arrow pointing sideways says "on to the next one"
721 // rather than "send this".
722 Send = 14,
723 Mic = 15,
724 Image = 16,
725 Video = 17,
726 Library = 18,
727 Sidebar = 19,
728 Compose = 20,
729 Collapse = 21,
730});
731
732// How long a transient message stays on screen. Closed, and deliberately short: a message
733// that has to be acknowledged is a `Dialog`, so there is no indefinite duration. What the
734// two names mean in milliseconds is the design system's decision.
735define_wire_enum!(MESSAGE_DURATION_SCHEMA, MessageDuration {
736 Short = 1,
737 Long = 2,
738});
739
740// How much a notification matters. Closed for the same reason a message's duration is: what
741// it is worth in sound, in an Android channel's importance or in getting through Do Not
742// Disturb is the platform's answer, so the Host has no way to send a sound file or a
743// priority number.
744define_wire_enum!(NOTIFICATION_IMPORTANCE_SCHEMA, NotificationImportance {
745 Normal = 1,
746 Urgent = 2,
747});
748
749// Whether a notification is shown while the application's window is the active one.
750// `WhenInactive` is dropped by the Renderer when the window has focus: whether it has is
751// the Renderer's state, and a Host that wanted to judge it would need every focus change
752// sent across.
753define_wire_enum!(NOTIFICATION_PRESENTATION_SCHEMA, NotificationPresentation {
754 Always = 1,
755 WhenInactive = 2,
756});
757
758// Whether this run may show notifications. `Unsupported` is a platform or a run that has
759// no way to show one (no bundle identifier, no AppUserModelID, no notification daemon),
760// which is an answer and not an error.
761define_wire_enum!(NOTIFICATION_PERMISSION_SCHEMA, NotificationPermission {
762 NotDetermined = 1,
763 Granted = 2,
764 Denied = 3,
765 Unsupported = 4,
766});
767
768// The design systems of phase one. Later ones append variants here and nowhere else: a new
769// design system is one variant plus one token table and rule implementation in the Renderer.
770//
771// `LiquidGlass` is the language macOS 26 and iOS 26 draw, and it sits beside `Cupertino`
772// rather than replacing it: one draws grouped inset lists on a grey page, the other draws
773// translucent floating capsules, and an application that wants one does not want the
774// other. Tags are append only, so it lands after the Linux three even though it belongs
775// next to `Cupertino` in any list a reader would write.
776define_wire_enum!(DESIGN_SYSTEM_SCHEMA, DesignSystem {
777 Material3 = 1,
778 Cupertino = 2,
779 Fluent = 3,
780 Gnome = 4,
781 Breeze = 5,
782 Deepin = 6,
783 LiquidGlass = 7,
784});
785
786// Light and dark selection. `FollowSystem` leaves the choice to the Renderer, which learns
787// of a system change first and applies it without involving the Host.
788define_wire_enum!(COLOR_SCHEME_SCHEMA, ColorScheme {
789 Light = 1,
790 Dark = 2,
791 FollowSystem = 3,
792});
793
794// Which part of the screen's frame a `Scaffold` slot fills.
795//
796// What each one becomes is the Renderer's: a top bar may be the window's caption, an
797// ordinary bar under the system title bar, or a large title that shrinks as the page
798// scrolls; a bottom bar may be a bar, a rail or a permanent drawer; a floating action
799// may float, sit in the toolbar, or fold into a menu. The application says which slot
800// it filled and nothing about the answer.
801define_wire_enum!(SLOT_ROLE_SCHEMA, SlotRole {
802 TopBar = 1,
803 BottomBar = 2,
804 FloatingAction = 3,
805 Content = 4,
806});
807
808/// Every role enum codegen mirrors, in wire order. Appending is the only allowed edit.
809pub const ROLE_ENUM_SCHEMA: &[RoleEnumSchema] = &[
810 RoleEnumSchema {
811 name: "ColorRole",
812 variants: COLOR_ROLE_SCHEMA,
813 },
814 RoleEnumSchema {
815 name: "TypeRole",
816 variants: TYPE_ROLE_SCHEMA,
817 },
818 RoleEnumSchema {
819 name: "ShapeRole",
820 variants: SHAPE_ROLE_SCHEMA,
821 },
822 RoleEnumSchema {
823 name: "MotionRole",
824 variants: MOTION_ROLE_SCHEMA,
825 },
826 RoleEnumSchema {
827 name: "TileMode",
828 variants: TILE_MODE_SCHEMA,
829 },
830 RoleEnumSchema {
831 name: "MaterialRole",
832 variants: MATERIAL_ROLE_SCHEMA,
833 },
834 RoleEnumSchema {
835 name: "SpaceRole",
836 variants: SPACE_ROLE_SCHEMA,
837 },
838 RoleEnumSchema {
839 name: "TextAlign",
840 variants: TEXT_ALIGN_SCHEMA,
841 },
842 RoleEnumSchema {
843 name: "TextOverflow",
844 variants: TEXT_OVERFLOW_SCHEMA,
845 },
846 RoleEnumSchema {
847 name: "Arrangement",
848 variants: ARRANGEMENT_SCHEMA,
849 },
850 RoleEnumSchema {
851 name: "Alignment",
852 variants: ALIGNMENT_SCHEMA,
853 },
854 RoleEnumSchema {
855 name: "ButtonVariant",
856 variants: BUTTON_VARIANT_SCHEMA,
857 },
858 RoleEnumSchema {
859 name: "DesignSystem",
860 variants: DESIGN_SYSTEM_SCHEMA,
861 },
862 RoleEnumSchema {
863 name: "ColorScheme",
864 variants: COLOR_SCHEME_SCHEMA,
865 },
866 RoleEnumSchema {
867 name: "AssetKind",
868 variants: ASSET_KIND_SCHEMA,
869 },
870 RoleEnumSchema {
871 name: "IconRole",
872 variants: ICON_ROLE_SCHEMA,
873 },
874 RoleEnumSchema {
875 name: "MessageDuration",
876 variants: MESSAGE_DURATION_SCHEMA,
877 },
878 RoleEnumSchema {
879 name: "Chrome",
880 variants: CHROME_SCHEMA,
881 },
882 RoleEnumSchema {
883 name: "TitleBar",
884 variants: TITLE_BAR_SCHEMA,
885 },
886 RoleEnumSchema {
887 name: "SlotRole",
888 variants: SLOT_ROLE_SCHEMA,
889 },
890 RoleEnumSchema {
891 name: "NotificationImportance",
892 variants: NOTIFICATION_IMPORTANCE_SCHEMA,
893 },
894 RoleEnumSchema {
895 name: "NotificationPresentation",
896 variants: NOTIFICATION_PRESENTATION_SCHEMA,
897 },
898 RoleEnumSchema {
899 name: "NotificationPermission",
900 variants: NOTIFICATION_PERMISSION_SCHEMA,
901 },
902];
903
904/// Every place that takes a colour takes a `Paint`, so colour is expressed once.
905///
906/// Encoded as one `u64`: the high 32 bits are the kind, the low 32 bits are the value.
907#[derive(Clone, Copy, Debug, Eq, PartialEq)]
908pub enum Paint {
909 Role(ColorRole),
910 Literal(Color),
911 /// A registered brush: a gradient, or a picture laid out as a fill.
912 ///
913 /// An id rather than the thing itself, because a list of stops does not fit the two
914 /// words a modifier has and because a brush outlives the frame that draws it, which
915 /// is what the asset path already exists for.
916 Asset(u32),
917}
918
919const PAINT_KIND_ROLE: u64 = 1;
920const PAINT_KIND_LITERAL: u64 = 2;
921const PAINT_KIND_ASSET: u64 = 3;
922
923impl Paint {
924 pub const fn to_bits(self) -> u64 {
925 match self {
926 Self::Role(role) => (PAINT_KIND_ROLE << 32) | role as u64,
927 Self::Literal(color) => (PAINT_KIND_LITERAL << 32) | color.0 as u64,
928 Self::Asset(id) => (PAINT_KIND_ASSET << 32) | id as u64,
929 }
930 }
931
932 pub fn from_bits(bits: u64) -> Option<Self> {
933 let value = bits as u32;
934 match bits >> 32 {
935 PAINT_KIND_ROLE => Some(Self::Role(
936 ColorRole::try_from(u16::try_from(value).ok()?).ok()?,
937 )),
938 PAINT_KIND_LITERAL => Some(Self::Literal(Color(value))),
939 PAINT_KIND_ASSET => Some(Self::Asset(value)),
940 _ => None,
941 }
942 }
943}
944
945// How the window wears its title bar.
946//
947// `Modern` runs content into the title bar area and is the default, because every desktop
948// platform now expects it and a window with a separate system bar looks a decade old.
949//
950// `System` is the platform's ordinary bar, and it is kept deliberately. Some tool-shaped
951// applications want one, and more importantly it is where an application goes when
952// `Modern` turns out to be wrong on some machine: a default with no way out of it is a
953// default that strands people.
954define_wire_enum!(CHROME_SCHEMA, Chrome {
955 Modern = 1,
956 System = 2,
957});
958
959// Which of two title bars this window has. `Chrome` chooses who draws the caption; this
960// chooses, inside that, what kind of window the caption belongs to.
961//
962// What each one means is not the same on every platform, and the numbers are never the
963// Host's. On macOS the buttons sit further in and the window is rounded more; on Windows
964// and Linux the caption is a different height and the buttons stay where that platform
965// puts them.
966define_wire_enum!(TITLE_BAR_SCHEMA, TitleBar {
967 // What an application on that platform actually looks like now, which is why it is the
968 // default.
969 Normal = 1,
970 // An ordinary window. For tool applications, for the places the other reads oddly, and
971 // as the way back.
972 Simple = 2,
973});
974
975/// What an application may decide about its own window.
976///
977/// The window belongs to the Renderer, so this is short and stays short. What is here is
978/// what an application knows and the Renderer cannot guess: what the window is called and
979/// how large it should open. Nothing here names a colour or a corner, because those are
980/// the design system's, and nothing here is read more than once.
981#[derive(Clone, Copy, Debug, Eq, PartialEq)]
982pub struct Window {
983 pub chrome: Chrome,
984 /// Which of the two title bars this window has.
985 pub title_bar: TitleBar,
986 /// What the window calls itself.
987 ///
988 /// Empty means the application said nothing, and the renderer uses its own name for
989 /// it. It is not decoration: a desktop lists windows by this, and a window with no
990 /// title of its own is listed under whatever the renderer happened to be called.
991 pub title: &'static str,
992 /// The picture the window wears, as an asset id, or zero for none.
993 ///
994 /// An id rather than a path or a name, because a path is a fact about the machine the
995 /// application was built on and a name asks the toolkit to find something it may not
996 /// have. Registering the bytes and referring to them is what every other picture in
997 /// this protocol does.
998 pub icon: u32,
999 /// Zero means the Renderer chooses, which is what an application that said nothing
1000 /// gets. A size is in the same density independent pixels gestures are measured in.
1001 pub width: u16,
1002 pub height: u16,
1003 pub min_width: u16,
1004 pub min_height: u16,
1005 pub resizable: bool,
1006}
1007
1008impl Window {
1009 pub const fn new() -> Self {
1010 Self {
1011 chrome: Chrome::Modern,
1012 title_bar: TitleBar::Normal,
1013 title: "",
1014 icon: 0,
1015 width: 0,
1016 height: 0,
1017 min_width: 0,
1018 min_height: 0,
1019 resizable: true,
1020 }
1021 }
1022
1023 /// Which of the two title bars this window has.
1024 ///
1025 /// `Normal` unless it is said otherwise, because that is the window an application on
1026 /// this platform has now. What the mode is worth in pixels is the design system's, so
1027 /// nothing here is a measurement.
1028 pub const fn with_title_bar(mut self, title_bar: TitleBar) -> Self {
1029 self.title_bar = title_bar;
1030 self
1031 }
1032
1033 /// Names the window. A literal, because this is read once before the window is stood
1034 /// up and never again, so there is nothing for a computed title to change.
1035 pub const fn with_title(mut self, title: &'static str) -> Self {
1036 self.title = title;
1037 self
1038 }
1039
1040 /// Dresses the window in an asset that was registered with [`AssetKind::Png`].
1041 pub const fn with_icon(mut self, asset_id: u32) -> Self {
1042 self.icon = asset_id;
1043 self
1044 }
1045
1046 pub const fn with_chrome(mut self, chrome: Chrome) -> Self {
1047 self.chrome = chrome;
1048 self
1049 }
1050
1051 pub const fn with_size(mut self, width: u16, height: u16) -> Self {
1052 self.width = width;
1053 self.height = height;
1054 self
1055 }
1056
1057 pub const fn with_min_size(mut self, width: u16, height: u16) -> Self {
1058 self.min_width = width;
1059 self.min_height = height;
1060 self
1061 }
1062
1063 pub const fn resizable(mut self, resizable: bool) -> Self {
1064 self.resizable = resizable;
1065 self
1066 }
1067}
1068
1069impl Default for Window {
1070 fn default() -> Self {
1071 Self::new()
1072 }
1073}
1074
1075/// A design system choice plus how it reacts to the host platform.
1076#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1077pub struct Theme {
1078 pub design_system: DesignSystem,
1079 pub fallback: DesignSystem,
1080 pub color_scheme: ColorScheme,
1081 pub adaptive: bool,
1082 /// The font asset each type role resolves to, or zero for the system font.
1083 ///
1084 /// Per theme and not per node. A node that could name its own font would be a node
1085 /// deciding typography, and the whole point of roles is that it does not: an
1086 /// application changes what `Display` is made of, and every title changes with it.
1087 ///
1088 /// Indexed by the role's wire tag minus one, so the array and the enum cannot drift
1089 /// apart without the compiler saying so.
1090 pub fonts: [u32; TYPE_ROLE_COUNT],
1091 /// The application's own colours, laid over whichever design system is chosen.
1092 ///
1093 /// A reference to something that lives for the whole program, so that a theme stays
1094 /// `Copy` and can still be built in a `const`. The same palette goes onto every system
1095 /// an adaptive theme may pick, because a brand colour does not change with the
1096 /// platform. A palette built while the application runs is handed to `use_theme`,
1097 /// which owns it, so application code never has to give it that lifetime itself.
1098 pub palette: Option<&'static crate::palette::Palette>,
1099}
1100
1101/// How many type roles there are, which is how many font slots a theme carries.
1102pub const TYPE_ROLE_COUNT: usize = 9;
1103
1104impl Theme {
1105 /// The same design system on every platform.
1106 pub const fn unified(design_system: DesignSystem) -> Self {
1107 Self {
1108 design_system,
1109 fallback: design_system,
1110 color_scheme: ColorScheme::FollowSystem,
1111 adaptive: false,
1112 fonts: [0; TYPE_ROLE_COUNT],
1113 palette: None,
1114 }
1115 }
1116
1117 /// Follows the host platform. The fallback is required, so no platform is left undecided.
1118 pub const fn adaptive(fallback: DesignSystem) -> Self {
1119 Self {
1120 design_system: fallback,
1121 fallback,
1122 color_scheme: ColorScheme::FollowSystem,
1123 adaptive: true,
1124 fonts: [0; TYPE_ROLE_COUNT],
1125 palette: None,
1126 }
1127 }
1128
1129 pub const fn with_color_scheme(mut self, color_scheme: ColorScheme) -> Self {
1130 self.color_scheme = color_scheme;
1131 self
1132 }
1133
1134 /// Resolves one type role to a registered font, leaving every other role alone.
1135 ///
1136 /// The asset has to be registered before the theme that names it reaches the
1137 /// Renderer, the same way a picture does. An id that names nothing is reported and
1138 /// the role falls back to the system font, because a screen in the wrong typeface is
1139 /// better than no screen.
1140 pub const fn with_font(mut self, role: TypeRole, asset: u32) -> Self {
1141 self.fonts[role as usize - 1] = asset;
1142 self
1143 }
1144
1145 /// Lays the application's own colours over the design system.
1146 ///
1147 /// Only colour roles change. Shape, type, spacing, elevation, motion and how each
1148 /// component is drawn stay the design system's, so the same brand colour reads as
1149 /// that system's button rather than as a copy of another's. Both schemes go to the
1150 /// Renderer at once, so a switch between light and dark never asks the Host.
1151 pub const fn with_palette(mut self, palette: &'static crate::palette::Palette) -> Self {
1152 self.palette = Some(palette);
1153 self
1154 }
1155
1156 /// The font this role resolves to, or `None` for the system font.
1157 pub const fn font(&self, role: TypeRole) -> Option<u32> {
1158 match self.fonts[role as usize - 1] {
1159 0 => None,
1160 asset => Some(asset),
1161 }
1162 }
1163}
1164
1165/// Saying nothing follows the host platform, with Material 3 where the platform has no look
1166/// of its own. The default used to be `unified(Material3)`, on the theory that a default
1167/// looking the same everywhere is more predictable. In practice that meant macOS showed a
1168/// Material screen with no configuration, which is the wrong first impression for a toolkit
1169/// that claims native desktop UI, and it meant nothing on the default path ever exercised
1170/// platform adaptation. A default that can be broken without anyone noticing is not
1171/// predictability. An application that wants one design system everywhere says
1172/// `Theme::unified(..)`, which is a clearer statement of that intent than silence was.
1173impl Default for Theme {
1174 fn default() -> Self {
1175 Self::adaptive(DesignSystem::Material3)
1176 }
1177}
1178
1179#[derive(Clone, Debug, PartialEq)]
1180pub enum Modifier {
1181 Empty,
1182 Padding(f32),
1183 FillMaxWidth,
1184 FillMaxHeight,
1185 Width(f32),
1186 Height(f32),
1187 Size {
1188 width: f32,
1189 height: f32,
1190 },
1191 /// A `Paint`, not a raw ARGB. Colour has exactly one wire representation.
1192 Background(Paint),
1193 Clickable {
1194 handler_id: u64,
1195 },
1196 PaddingRole(SpaceRole),
1197 PaddingEach {
1198 start: f32,
1199 top: f32,
1200 end: f32,
1201 bottom: f32,
1202 },
1203 Weight(f32),
1204 Shape {
1205 top_start: f32,
1206 top_end: f32,
1207 bottom_end: f32,
1208 bottom_start: f32,
1209 },
1210 ShapeRole(ShapeRole),
1211 Border {
1212 width: f32,
1213 paint: Paint,
1214 },
1215 /// One dp value. How the shadow is drawn is the design system's rule: tonal lift plus a
1216 /// shadow, a wide soft shadow, or layered shadow plus a hairline stroke.
1217 Elevation(f32),
1218 /// Asks the Renderer to report this node's measured size, the way it reports the
1219 /// window's.
1220 ///
1221 /// The token is the application's own name for the node. A node id belongs to the
1222 /// Renderer and is never something the Host chose, so a screen that wants to know how
1223 /// wide one of its own containers ended up needs a name it gave itself. The Renderer
1224 /// does not read it.
1225 ObserveSize {
1226 token: u32,
1227 },
1228 /// Which curve and length this node's changes run along.
1229 ///
1230 /// The node says how important the change is, not how long it takes. Appearing,
1231 /// disappearing, a selection moving and a size changing all read this.
1232 Motion(MotionRole),
1233 /// What this node's surface is made of.
1234 ///
1235 /// A role, so that a system which blurs blurs and a system which does not lifts its
1236 /// surface instead. Nothing here claims a particular effect was achieved.
1237 Material(MaterialRole),
1238}
1239
1240const NO_FIELDS: &[FieldSchema] = &[];
1241const TOKEN_U32_FIELD: &[FieldSchema] = &[FieldSchema {
1242 name: "token",
1243 ty: FieldType::U32,
1244 slot: FieldSlot::FirstLow,
1245}];
1246const VALUE_FLOAT_FIELD: &[FieldSchema] = &[FieldSchema {
1247 name: "value",
1248 ty: FieldType::Float,
1249 slot: FieldSlot::FirstLow,
1250}];
1251const SIZE_FIELDS: &[FieldSchema] = &[
1252 FieldSchema {
1253 name: "width",
1254 ty: FieldType::Float,
1255 slot: FieldSlot::FirstLow,
1256 },
1257 FieldSchema {
1258 name: "height",
1259 ty: FieldType::Float,
1260 slot: FieldSlot::SecondLow,
1261 },
1262];
1263const PAINT_FIELD: &[FieldSchema] = &[FieldSchema {
1264 name: "paint",
1265 ty: FieldType::Paint,
1266 slot: FieldSlot::First,
1267}];
1268const HANDLER_ID_FIELD: &[FieldSchema] = &[FieldSchema {
1269 name: "handlerId",
1270 ty: FieldType::U64,
1271 slot: FieldSlot::First,
1272}];
1273const SPACE_ROLE_FIELD: &[FieldSchema] = &[FieldSchema {
1274 name: "role",
1275 ty: FieldType::Role("SpaceRole"),
1276 slot: FieldSlot::FirstLow,
1277}];
1278const SHAPE_ROLE_FIELD: &[FieldSchema] = &[FieldSchema {
1279 name: "role",
1280 ty: FieldType::Role("ShapeRole"),
1281 slot: FieldSlot::FirstLow,
1282}];
1283const MOTION_ROLE_FIELD: &[FieldSchema] = &[FieldSchema {
1284 name: "role",
1285 ty: FieldType::Role("MotionRole"),
1286 slot: FieldSlot::FirstLow,
1287}];
1288const MATERIAL_ROLE_FIELD: &[FieldSchema] = &[FieldSchema {
1289 name: "role",
1290 ty: FieldType::Role("MaterialRole"),
1291 slot: FieldSlot::FirstLow,
1292}];
1293const PADDING_EACH_FIELDS: &[FieldSchema] = &[
1294 FieldSchema {
1295 name: "start",
1296 ty: FieldType::Float,
1297 slot: FieldSlot::FirstLow,
1298 },
1299 FieldSchema {
1300 name: "top",
1301 ty: FieldType::Float,
1302 slot: FieldSlot::FirstHigh,
1303 },
1304 FieldSchema {
1305 name: "end",
1306 ty: FieldType::Float,
1307 slot: FieldSlot::SecondLow,
1308 },
1309 FieldSchema {
1310 name: "bottom",
1311 ty: FieldType::Float,
1312 slot: FieldSlot::SecondHigh,
1313 },
1314];
1315const SHAPE_FIELDS: &[FieldSchema] = &[
1316 FieldSchema {
1317 name: "topStart",
1318 ty: FieldType::Float,
1319 slot: FieldSlot::FirstLow,
1320 },
1321 FieldSchema {
1322 name: "topEnd",
1323 ty: FieldType::Float,
1324 slot: FieldSlot::FirstHigh,
1325 },
1326 FieldSchema {
1327 name: "bottomEnd",
1328 ty: FieldType::Float,
1329 slot: FieldSlot::SecondLow,
1330 },
1331 FieldSchema {
1332 name: "bottomStart",
1333 ty: FieldType::Float,
1334 slot: FieldSlot::SecondHigh,
1335 },
1336];
1337const BORDER_FIELDS: &[FieldSchema] = &[
1338 FieldSchema {
1339 name: "width",
1340 ty: FieldType::Float,
1341 slot: FieldSlot::FirstLow,
1342 },
1343 FieldSchema {
1344 name: "paint",
1345 ty: FieldType::Paint,
1346 slot: FieldSlot::Second,
1347 },
1348];
1349
1350pub const MODIFIER_SCHEMA: &[VariantSchema] = &[
1351 VariantSchema {
1352 name: "Empty",
1353 tag: 0,
1354 fields: NO_FIELDS,
1355 },
1356 VariantSchema {
1357 name: "Padding",
1358 tag: 1,
1359 fields: VALUE_FLOAT_FIELD,
1360 },
1361 VariantSchema {
1362 name: "FillMaxWidth",
1363 tag: 2,
1364 fields: NO_FIELDS,
1365 },
1366 VariantSchema {
1367 name: "FillMaxHeight",
1368 tag: 3,
1369 fields: NO_FIELDS,
1370 },
1371 VariantSchema {
1372 name: "Width",
1373 tag: 4,
1374 fields: VALUE_FLOAT_FIELD,
1375 },
1376 VariantSchema {
1377 name: "Height",
1378 tag: 5,
1379 fields: VALUE_FLOAT_FIELD,
1380 },
1381 VariantSchema {
1382 name: "Size",
1383 tag: 6,
1384 fields: SIZE_FIELDS,
1385 },
1386 VariantSchema {
1387 name: "Background",
1388 tag: 7,
1389 fields: PAINT_FIELD,
1390 },
1391 VariantSchema {
1392 name: "Clickable",
1393 tag: 8,
1394 fields: HANDLER_ID_FIELD,
1395 },
1396 VariantSchema {
1397 name: "PaddingRole",
1398 tag: 9,
1399 fields: SPACE_ROLE_FIELD,
1400 },
1401 VariantSchema {
1402 name: "PaddingEach",
1403 tag: 10,
1404 fields: PADDING_EACH_FIELDS,
1405 },
1406 VariantSchema {
1407 name: "Weight",
1408 tag: 11,
1409 fields: VALUE_FLOAT_FIELD,
1410 },
1411 VariantSchema {
1412 name: "Shape",
1413 tag: 12,
1414 fields: SHAPE_FIELDS,
1415 },
1416 VariantSchema {
1417 name: "ShapeRole",
1418 tag: 13,
1419 fields: SHAPE_ROLE_FIELD,
1420 },
1421 VariantSchema {
1422 name: "Border",
1423 tag: 14,
1424 fields: BORDER_FIELDS,
1425 },
1426 VariantSchema {
1427 name: "Elevation",
1428 tag: 15,
1429 fields: VALUE_FLOAT_FIELD,
1430 },
1431 VariantSchema {
1432 name: "ObserveSize",
1433 tag: 16,
1434 fields: TOKEN_U32_FIELD,
1435 },
1436 VariantSchema {
1437 name: "Motion",
1438 tag: 17,
1439 fields: MOTION_ROLE_FIELD,
1440 },
1441 VariantSchema {
1442 name: "Material",
1443 tag: 18,
1444 fields: MATERIAL_ROLE_FIELD,
1445 },
1446];
1447
1448#[derive(Clone, Debug, PartialEq)]
1449pub struct ColumnProps {
1450 pub enabled: bool,
1451}
1452
1453#[derive(Clone, Debug, PartialEq)]
1454pub struct RowProps {
1455 pub enabled: bool,
1456}
1457
1458#[derive(Clone, Debug, PartialEq)]
1459pub struct BoxProps {
1460 pub enabled: bool,
1461}
1462
1463#[derive(Clone, Debug, PartialEq)]
1464pub struct TextProps<'a> {
1465 pub text: &'a str,
1466}
1467
1468#[derive(Clone, Debug, PartialEq)]
1469pub struct TextFieldProps<'a> {
1470 pub placeholder: &'a str,
1471 pub enabled: bool,
1472 pub multiline: bool,
1473}
1474
1475#[derive(Clone, Debug, PartialEq)]
1476pub struct ButtonProps<'a> {
1477 pub text: &'a str,
1478 pub enabled: bool,
1479}
1480
1481#[derive(Clone, Debug, PartialEq)]
1482pub struct SpacerProps {
1483 pub width: f32,
1484 pub height: f32,
1485}
1486
1487crate::extensions::define_property_schema_with_extensions!(define_wire_enum; PROPERTY_SCHEMA, PropertyKind {
1488 Text = 1,
1489 Placeholder = 2,
1490 Enabled = 3,
1491 Multiline = 4,
1492 OnClick = 5,
1493 OnValueChange = 6,
1494 OnSubmit = 7,
1495 OnFocusLost = 8,
1496 OnKeyDown = 9,
1497 ItemCount = 10,
1498 ItemKey = 11,
1499 OnRangeRequested = 12,
1500 TypeRole = 13,
1501 FontSize = 14,
1502 FontWeight = 15,
1503 LineHeight = 16,
1504 LetterSpacing = 17,
1505 Color = 18,
1506 TextAlign = 19,
1507 MaxLines = 20,
1508 Overflow = 21,
1509 Arrangement = 22,
1510 Spacing = 23,
1511 SpaceRole = 24,
1512 Alignment = 25,
1513 Variant = 26,
1514 // The id of an asset the Host registered. Image and Icon carry nothing else: the
1515 // bytes were copied into the Renderer's cache once, at registration.
1516 Asset = 28,
1517 // Whether a toggle is on. One boolean for all three of them: a selected radio button
1518 // and a switch that is on are the same fact, and the schema names a concept once.
1519 Checked = 32,
1520 // Discrete stops between a slider's two ends. Zero leaves it continuous.
1521 Steps = 33,
1522 // Whether a progress indicator knows how far along it is. When it does not, `Value`
1523 // is not read at all.
1524 Determinate = 34,
1525 // Which of the two forms a progress indicator takes. It picks a shape, not a size.
1526 Circular = 35,
1527 // A divider's axis, which is the only thing a divider carries.
1528 Vertical = 36,
1529 // Whether an overlay is showing. The Renderer owns the state; this seeds it and
1530 // carries changes that came from outside the Renderer.
1531 Open = 40,
1532 OnDismiss = 41,
1533 SelectedIndex = 42,
1534 // The Canvas drawing command list, a byte blob in the batch arena.
1535 Commands = 50,
1536 // The control's current value, in the widget's own unit: an epoch day count for a
1537 // date, minutes since midnight for a time, the chosen position for a Dropdown, a
1538 // position between the ends for a Slider, a fraction for a ProgressIndicator. One tag,
1539 // because "this control's value" is one concept whichever widget holds it.
1540 Value = 51,
1541 // The ends of the selectable range, in the same unit as `Value`.
1542 Min = 52,
1543 Max = 53,
1544 // The meaning of the icon a destination carries, as an `IconRole` tag. Tag 0 is "not
1545 // sent", so a destination without an icon is label only.
1546 Icon = 60,
1547 // Which slot of a Scaffold this subtree fills.
1548 Slot = 61,
1549 // How many columns a grid has, where the screen said a number.
1550 Columns = 62,
1551 // How narrow a column may get before the grid drops one, where the screen said a
1552 // width instead of a number. The Renderer divides its own width by this, which is the
1553 // same kind of judgement as a size class and for the same reason is not the Host's.
1554 MinColumnWidth = 63,
1555 // Runs of different treatment inside one string, as a blob of fixed-length records.
1556 // A Text that says nothing about runs carries none of this and travels as it always
1557 // did.
1558 Spans = 64,
1559 // A node that is willing to have files dropped on it. The handler is the willingness:
1560 // a node without one is never told that files are over it, which is what keeps a
1561 // screen from lighting up every container it has.
1562 OnFilesEntered = 65,
1563 OnFilesDropped = 66,
1564 // Which named group of a navigation's strip this destination belongs to. Neighbouring
1565 // destinations carrying the same string are one group and the string is its heading.
1566 //
1567 // 76 rather than 67, because 67 through 74 are reserved for the gestures and 75 for a
1568 // button's kind. A reservation is not a free tag.
1569 Section = 76,
1570 // How many a badge counts. Absent rather than zero when the badge is a dot or a word,
1571 // because "no count" and "a count of nothing" are different things to show.
1572 //
1573 // 80 opens a fresh block of ten for widget tags 40 onwards, after the last tag in use
1574 // anywhere, so two pieces of work landing in either order cannot collide.
1575 Count = 80,
1576 // Whether a split pane's side pane may be folded away by dragging past its minimum, or
1577 // by the keyboard, or by the platform's own show and hide sidebar command. Whether a
1578 // side pane can be hidden is a fact about the screen rather than about the design
1579 // system: a list of sessions can, a settings screen's list of sections cannot.
1580 //
1581 // 90 opens the split pane's block of ten, after the badge's.
1582 Collapsible = 90,
1583});
1584
1585#[derive(Clone, Debug, PartialEq)]
1586pub enum EventPayload<'a> {
1587 Clicked,
1588 TextChanged(&'a str),
1589 TextSubmitted(&'a str),
1590 FocusLost,
1591 ProtocolError {
1592 code: u32,
1593 message: &'a str,
1594 },
1595 KeyDown {
1596 key: Key,
1597 shift_key: bool,
1598 ctrl_key: bool,
1599 alt_key: bool,
1600 meta_key: bool,
1601 },
1602 /// The Renderer asks the Host to materialise the visible item range.
1603 RangeRequested {
1604 start: u32,
1605 count: u32,
1606 },
1607 /// A control's new value, in the widget's own unit: an epoch count for a picker, a
1608 /// position for a slider, 0.0 or 1.0 for a toggle. The Renderer decided how the user
1609 /// reached it, so nothing about calendars, wheels, tracks or thumbs crosses here.
1610 ///
1611 /// One `f64` holds all of them: integers up to 2^53 survive it exactly, so splitting
1612 /// the event in two would only give the same concept two names.
1613 ValueChanged(f64),
1614 /// The window, or one observed node, moved into a different size class. The Renderer
1615 /// measures it and sends this only when a class changes, never on every layout pass.
1616 /// A `node_id` of 0 is the window; anything else is the node that asked to be
1617 /// measured.
1618 WindowSizeChanged {
1619 width_dp: f32,
1620 height_dp: f32,
1621 class: WindowSizeClass,
1622 height_class: WindowHeightClass,
1623 },
1624 /// Files are over a node that said it would take them. Nothing about what they are:
1625 /// the platforms disagree about what is knowable before a drop, and a screen that
1626 /// only needs to light up does not need to know.
1627 FilesEntered,
1628 /// Files were let go over a node. The paths arrive together, separated by a byte no
1629 /// path on any of the three desktops may contain.
1630 FilesDropped(&'a str),
1631 /// The Renderer resolved the theme and this is what it chose. Sent once and then
1632 /// only when the answer changes, the same way a size class is.
1633 DesignSystemResolved(DesignSystem),
1634 /// The Renderer lost its node table and asks for the whole tree again. A recreated
1635 /// Activity is the case that produces it.
1636 Resync,
1637 /// The platform brought the UI back. Timers and animations resume.
1638 LifecycleStart,
1639 /// The platform stopped the UI. Timers and animations are suppressed, so a process
1640 /// that is not on screen is not asked to draw.
1641 LifecycleStop,
1642 /// The user pressed a notification the application posted. `action` is 0 for the body
1643 /// and 1 or 2 for an action button; `key` is the name the application gave it.
1644 ///
1645 /// A name rather than a handler, because a notification outlives the component that
1646 /// posted it by hours and a callback dies with its scope.
1647 NotificationActivated {
1648 action: u32,
1649 key: &'a str,
1650 },
1651 /// Whether notifications may be shown, sent once after start and then only when the
1652 /// answer changes.
1653 NotificationPermissionChanged(NotificationPermission),
1654}
1655
1656pub const EVENT_SCHEMA: &[EventSchema] = &[
1657 EventSchema {
1658 name: "Clicked",
1659 tag: 1,
1660 payload: EventPayloadType::None,
1661 },
1662 EventSchema {
1663 name: "TextChanged",
1664 tag: 2,
1665 payload: EventPayloadType::Text,
1666 },
1667 EventSchema {
1668 name: "TextSubmitted",
1669 tag: 3,
1670 payload: EventPayloadType::Text,
1671 },
1672 EventSchema {
1673 name: "FocusLost",
1674 tag: 4,
1675 payload: EventPayloadType::None,
1676 },
1677 EventSchema {
1678 name: "ProtocolError",
1679 tag: 5,
1680 payload: EventPayloadType::ProtocolError,
1681 },
1682 EventSchema {
1683 name: "KeyDown",
1684 tag: 6,
1685 payload: EventPayloadType::KeyDown,
1686 },
1687 EventSchema {
1688 name: "RangeRequested",
1689 tag: 7,
1690 payload: EventPayloadType::Range,
1691 },
1692 EventSchema {
1693 name: "ValueChanged",
1694 // Tags 8 to 15 are reserved for the pointer gesture events.
1695 tag: 16,
1696 payload: EventPayloadType::Double,
1697 },
1698 EventSchema {
1699 name: "WindowSizeChanged",
1700 tag: 17,
1701 payload: EventPayloadType::WindowSize,
1702 },
1703 // The three below address the Host itself rather than a node, so they carry no handler
1704 // and the Host answers them before it looks one up.
1705 EventSchema {
1706 name: "Resync",
1707 tag: 18,
1708 payload: EventPayloadType::None,
1709 },
1710 EventSchema {
1711 name: "LifecycleStart",
1712 tag: 19,
1713 payload: EventPayloadType::None,
1714 },
1715 EventSchema {
1716 name: "LifecycleStop",
1717 tag: 20,
1718 payload: EventPayloadType::None,
1719 },
1720 // Addressed to the Host rather than to a node, like the three above: it is a fact
1721 // about the window, not something that happened inside it.
1722 EventSchema {
1723 name: "DesignSystemResolved",
1724 tag: 21,
1725 payload: EventPayloadType::DesignSystem,
1726 },
1727 EventSchema {
1728 name: "FilesEntered",
1729 tag: 22,
1730 payload: EventPayloadType::None,
1731 },
1732 // One string for however many paths, which is the string convention unchanged. A
1733 // second way of carrying a list would be a second thing to keep in step.
1734 EventSchema {
1735 name: "FilesDropped",
1736 tag: 23,
1737 payload: EventPayloadType::Text,
1738 },
1739 // Both address the Host itself, like the resolved design system: a notification is not
1740 // in the tree, so there is no node and no handler to name.
1741 EventSchema {
1742 name: "NotificationActivated",
1743 tag: 24,
1744 payload: EventPayloadType::NotificationActivation,
1745 },
1746 EventSchema {
1747 name: "NotificationPermissionChanged",
1748 tag: 25,
1749 payload: EventPayloadType::NotificationPermission,
1750 },
1751];
1752
1753/// One argument of a boundary operation.
1754///
1755/// The set is deliberately tiny, because only primitives, pointers and lengths cross the
1756/// boundary. Every platform binding can then be generated from the same list.
1757#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1758pub enum BoundaryParam {
1759 /// A readable byte range. It arrives as a direct byte buffer and a length on Android,
1760 /// as a pointer and a length on desktop.
1761 Bytes { name: &'static str },
1762 /// A 64-bit platform frame timestamp.
1763 Nanos { name: &'static str },
1764}
1765
1766impl BoundaryParam {
1767 pub const fn name(self) -> &'static str {
1768 match self {
1769 Self::Bytes { name } | Self::Nanos { name } => name,
1770 }
1771 }
1772}
1773
1774/// One logical boundary operation, stated without saying which side calls it.
1775///
1776/// `symbol` is the Host's C export. Every platform binding, the GraalVM function
1777/// declarations, the Android shims and the browser forwarders, is a rendering of this
1778/// table, which is why none of them is written by hand.
1779#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1780pub struct BoundaryOp {
1781 /// Direction-neutral name, in PascalCase.
1782 pub name: &'static str,
1783 pub symbol: &'static str,
1784 pub params: &'static [BoundaryParam],
1785 /// Whether the operation answers with a mutation batch.
1786 pub returns_batch: bool,
1787 /// Whether the call may skip the runtime's thread state transition on Android. That
1788 /// is only sound for a call that cannot run long, because the thread stays runnable
1789 /// and the garbage collector cannot suspend it while the call is in flight. A call
1790 /// that runs the VirtualDom can run long.
1791 pub fast: bool,
1792}
1793
1794pub const BOUNDARY_SCHEMA: &[BoundaryOp] = &[
1795 BoundaryOp {
1796 name: "Init",
1797 symbol: "compose_rust_host_init",
1798 params: &[BoundaryParam::Bytes { name: "handshake" }],
1799 returns_batch: true,
1800 fast: false,
1801 },
1802 BoundaryOp {
1803 name: "DispatchEvent",
1804 symbol: "compose_rust_host_dispatch_event",
1805 params: &[BoundaryParam::Bytes { name: "event" }],
1806 returns_batch: true,
1807 fast: false,
1808 },
1809 BoundaryOp {
1810 name: "RenderFrame",
1811 symbol: "compose_rust_host_render_frame",
1812 params: &[BoundaryParam::Nanos {
1813 name: "frameTimeNanos",
1814 }],
1815 returns_batch: true,
1816 fast: false,
1817 },
1818 BoundaryOp {
1819 name: "ReleaseBatch",
1820 symbol: "compose_rust_host_release_batch",
1821 params: &[],
1822 returns_batch: false,
1823 fast: true,
1824 },
1825 BoundaryOp {
1826 name: "Shutdown",
1827 symbol: "compose_rust_host_shutdown",
1828 params: &[],
1829 returns_batch: false,
1830 fast: false,
1831 },
1832];
1833
1834/// The JVM class the Android shims bind to. The Kotlin file is generated under the same
1835/// name, so the two sides cannot drift.
1836pub const ANDROID_BRIDGE_CLASS: &str = "dev/darkpyonix/composerust/ui/platform/HostBridge";
1837
1838// ---------------------------------------------------------------------------------------
1839// The browser's shared linear memory.
1840//
1841// One `WebAssembly.Memory` holds both modules' data. The Kotlin module defines and exports
1842// it, because a Kotlin/Wasm module has no way to import one, and the Rust module is linked
1843// with `--import-memory` so that the arena the Host writes is the arena the Renderer reads.
1844// Nothing below crosses the boundary as data: these are the addresses and offsets both
1845// generated halves are written against.
1846// ---------------------------------------------------------------------------------------
1847
1848/// The first address that belongs to the Rust module.
1849///
1850/// Below it is the Kotlin module's `kotlin.wasm.unsafe` allocator; at and above it are the
1851/// Rust module's data, stack and heap, which the linker is told with `--global-base`. Two
1852/// allocators handing out addresses in one memory is the hazard: an overlap does not crash,
1853/// it draws the wrong screen, so the Renderer checks which side of this line its own
1854/// addresses fall on before it makes the first boundary call.
1855pub const WEB_RUST_REGION_BASE: u32 = 4 * 1024 * 1024;
1856
1857/// One field of the `MutationBatch` record the Host writes into the shared memory.
1858#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1859pub struct WebBatchField {
1860 /// PascalCase, so the generated Kotlin constant reads as a name rather than an offset.
1861 pub name: &'static str,
1862 pub offset: u32,
1863 pub width: u32,
1864 pub description: &'static str,
1865}
1866
1867/// `MutationBatch` as a wasm32 target lays it out.
1868///
1869/// The offsets are written down rather than read from `offset_of!` because codegen runs on
1870/// the machine that generates, not the machine that runs: a 64-bit host makes the same
1871/// `#[repr(C)]` struct 24 bytes with an 8 byte pointer. The generated wasm glue asserts
1872/// this table against the real layout, so a compiler that disagrees stops that build
1873/// instead of producing a Renderer that reads the wrong four bytes.
1874pub const WEB_BATCH_FIELDS: &[WebBatchField] = &[
1875 WebBatchField {
1876 name: "Address",
1877 offset: 0,
1878 width: 4,
1879 description: "where the batch starts, as an address in the shared memory",
1880 },
1881 WebBatchField {
1882 name: "Length",
1883 offset: 4,
1884 width: 4,
1885 description: "the batch's length in bytes",
1886 },
1887 WebBatchField {
1888 name: "Result",
1889 offset: 8,
1890 width: 8,
1891 description: "the handler's synchronous result",
1892 },
1893];
1894
1895/// How long that record is, including the padding `#[repr(C)]` adds before the result.
1896pub const WEB_BATCH_BYTES: u32 = 16;
1897
1898/// How much room the Host lends the Renderer to encode one event into.
1899///
1900/// The same figure the Android connection allocates for the same job. An event that would
1901/// not fit is refused by the encoder rather than truncated.
1902pub const WEB_EVENT_BUFFER_BYTES: u32 = 4096;
1903
1904/// Where the event buffer starts inside the block `compose_rust_host_web_start` reports.
1905///
1906/// The out record comes first, at offset zero, and the event buffer follows it.
1907pub const WEB_EVENT_BUFFER_OFFSET: u32 = WEB_BATCH_BYTES;
1908
1909/// The extra Host export a browser needs, because a page has no library loader.
1910///
1911/// It does what `JNI_OnLoad` does on Android, registering the root component and installing
1912/// the renderer API, and it answers with the address of the block above.
1913pub const WEB_START_SYMBOL: &str = "compose_rust_host_web_start";
1914
1915/// The wasm import module the Host's frame request comes from.
1916///
1917/// This direction has no JavaScript on it: the page hands Kotlin's exported function object
1918/// straight to the Rust instantiation, which the engine binds as a wasm-to-wasm call. It can,
1919/// because Rust is instantiated second and Kotlin's exports already exist by then.
1920pub const WEB_RENDERER_IMPORT_MODULE: &str = "compose_rust_renderer";
1921
1922/// The global the generated forwarders read the Host's exports off.
1923///
1924/// A `@JsFun` body is inlined into the import object module Kotlin generates, so the only
1925/// name it can reach the loader through is a property of the global object.
1926pub const WEB_HOST_GLOBAL: &str = "__composeRustHost";
1927
1928/// The global the loader module publishes the compiled Host module under.
1929///
1930/// Compiling is all the page does, and the reason the page has to do it at all is that
1931/// compiling is asynchronous while the boundary is not: the fetch has to have finished
1932/// before the Renderer's `main` runs. Everything after it, the memory, the imports and the
1933/// entry point, is in the generated Kotlin, because only the Renderer's own module can name
1934/// them.
1935pub const WEB_MODULE_GLOBAL: &str = "__composeRustHostModule";