Skip to main content

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";