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