1mod data;
28pub mod html;
29mod icon;
30pub(crate) mod node;
31mod types;
32
33use std::time::Instant;
34
35pub use data::*;
36pub use icon::Icon;
37pub use node::{Node, View};
38pub use types::*;
39
40use self::node::{
41 actions_methods, base_methods, children_methods, event_methods, kind_type, max_method, props,
42 title_method,
43};
44
45pub type CardStatus = Status;
47
48#[macro_export]
58macro_rules! spans {
59 ($($s:expr),* $(,)?) => {
60 vec![$($crate::ui::Span::from($s)),*]
61 };
62}
63
64#[macro_export]
74macro_rules! nodes {
75 ($($n:expr),* $(,)?) => {
76 vec![$($crate::ui::Node::from($n)),*]
77 };
78}
79
80pub fn span(t: impl Into<String>) -> Span {
82 Span::new(t)
83}
84
85pub fn node<M>(kind: impl Into<String>) -> Node<M> {
87 Node::new(kind)
88}
89
90macro_rules! common_kind {
92 ($(#[$m:meta])* $name:ident) => {
93 kind_type!($(#[$m])* $name);
94
95 impl<M> $name<M> {
96 base_methods!();
97 title_method!();
98 max_method!();
99 actions_methods!();
100 event_methods!();
101 }
102 };
103}
104
105macro_rules! container_kind {
107 ($(#[$m:meta])* $name:ident) => {
108 common_kind!($(#[$m])* $name);
109
110 impl<M> $name<M> {
111 children_methods!();
112 }
113 };
114}
115
116fn new<M, T: From<Node<M>>>(kind: &str) -> T {
118 T::from(Node::new(kind))
119}
120
121macro_rules! from_node {
123 ($($name:ident),*) => {
124 $(impl<M> From<Node<M>> for $name<M> {
125 fn from(node: Node<M>) -> Self {
126 Self(node)
127 }
128 })*
129 };
130}
131
132from_node!(
133 Col, Row, Card, Section, Rule, Spacer, TextNode, Md, Code, Diff, Ansi, Rows, Math, Kv, Table,
134 Tree, Badge, Kbd, IconNode, Image, List, Item, Tabs, Picker, Spinner, Shimmer, Elapsed, Rate,
135 Progress, Meter, Chart, Effort, Editor, Input, StatusBar, Seg, Toast, Overlay, Tool, Agent,
136 Checklist, Prefs
137);
138
139container_kind!(
144 Col
146);
147
148impl<M> Col<M> {
149 props! {
150 gap(Gap) = "gap";
152 align(Align) = "align";
154 justify(Justify) = "justify";
156 wrap(bool) = "wrap";
158 }
159}
160
161pub fn col<M>() -> Col<M> {
163 new("col")
164}
165
166container_kind!(
167 Row
169);
170
171impl<M> Row<M> {
172 props! {
173 gap(Gap) = "gap";
175 align(Align) = "align";
177 justify(Justify) = "justify";
179 wrap(bool) = "wrap";
181 }
182}
183
184pub fn row<M>() -> Row<M> {
186 new("row")
187}
188
189container_kind!(
190 Card
192);
193
194impl<M> Card<M> {
195 props! {
196 head(Text) = "head";
198 status(Status) = "status";
200 collapsible(bool) = "collapsible";
202 collapsed(bool) = "collapsed";
204 preview(Preview) = "preview";
206 selected(bool) = "selected";
208 inset(bool) = "inset";
210 variant(CardVariant) = "variant";
212 }
213}
214
215pub fn card<M>() -> Card<M> {
217 new("card")
218}
219
220container_kind!(
221 Section
223);
224
225impl<M> Section<M> {
226 props! {
227 head(Text) = "head";
229 collapsible(bool) = "collapsible";
231 collapsed(bool) = "collapsed";
233 }
234}
235
236pub fn section<M>() -> Section<M> {
238 new("section")
239}
240
241common_kind!(
242 Rule
244);
245
246impl<M> Rule<M> {
247 props! {
248 label(Text) = "label";
250 }
251}
252
253pub fn rule<M>() -> Rule<M> {
255 new("rule")
256}
257
258common_kind!(
259 Spacer
261);
262
263impl<M> Spacer<M> {
264 props! {
265 size(Gap) = "size";
267 }
268}
269
270pub fn spacer<M>() -> Spacer<M> {
272 new("spacer")
273}
274
275common_kind!(
280 TextNode
282);
283
284impl<M> TextNode<M> {
285 props! {
286 wrap(Wrap) = "wrap";
288 truncate(Truncate) = "truncate";
290 lines(u32) = "lines";
292 measure(Measure) = "measure";
294 }
295
296 pub fn text(mut self, text: impl Into<Text>) -> Self {
298 self.0.set_text(text.into());
299 self
300 }
301}
302
303pub fn text<M>(text: impl Into<Text>) -> TextNode<M> {
305 let mut node = Node::new("text");
306 node.set_text(text.into());
307 TextNode(node)
308}
309
310common_kind!(
311 Md
313);
314
315impl<M> Md<M> {
316 props! {
317 text(String) = "text";
319 stream(bool) = "stream";
321 marks(Vec<Span>) = "marks";
323 }
324}
325
326pub fn md<M>(source: impl Into<String>) -> Md<M> {
328 new::<M, Md<M>>("md").text(source)
329}
330
331common_kind!(
332 Code
334);
335
336impl<M> Code<M> {
337 props! {
338 text(String) = "text";
340 lang(String) = "lang";
342 path(String) = "path";
344 numbers(bool) = "numbers";
346 start(i64) = "start";
348 marks(Vec<CodeMark>) = "marks";
350 wrap(bool) = "wrap";
352 }
353}
354
355pub fn code<M>(source: impl Into<String>) -> Code<M> {
357 new::<M, Code<M>>("code").text(source)
358}
359
360common_kind!(
361 Diff
363);
364
365impl<M> Diff<M> {
366 props! {
367 text(String) = "text";
369 hunks(Vec<Hunk>) = "hunks";
371 path(String) = "path";
373 lang(String) = "lang";
375 mode(DiffMode) = "mode";
377 }
378}
379
380pub fn diff<M>() -> Diff<M> {
382 new("diff")
383}
384
385common_kind!(
386 Ansi
388);
389
390impl<M> Ansi<M> {
391 props! {
392 text(String) = "text";
394 cols(u32) = "cols";
396 follow(bool) = "follow";
398 }
399
400 pub fn preview(mut self, lines: u32) -> Self {
402 self
403 .0
404 .set("preview", &serde_json::json!({ "lines": lines }));
405 self
406 }
407}
408
409pub fn ansi<M>(output: impl Into<String>) -> Ansi<M> {
411 new::<M, Ansi<M>>("ansi").text(output)
412}
413
414common_kind!(
415 Rows
417);
418
419impl<M> Rows<M> {
420 props! {
421 lines(Vec<String>) = "lines";
423 cols(u32) = "cols";
425 }
426}
427
428pub fn rows<M>(lines: impl Into<Vec<String>>) -> Rows<M> {
430 new::<M, Rows<M>>("rows").lines(lines)
431}
432
433common_kind!(
434 Math
436);
437
438impl<M> Math<M> {
439 props! {
440 text(String) = "text";
442 display(bool) = "display";
444 }
445}
446
447pub fn math<M>(tex: impl Into<String>) -> Math<M> {
449 new::<M, Math<M>>("math").text(tex)
450}
451
452common_kind!(
457 Kv
459);
460
461impl<M> Kv<M> {
462 props! {
463 items(Vec<KvItem>) = "items";
465 layout(KvLayout) = "layout";
467 }
468
469 pub fn item(mut self, k: impl Into<Text>, v: impl Into<Text>) -> Self {
471 push(&mut self.0, "items", &KvItem { k: k.into(), v: v.into() });
472 self
473 }
474}
475
476pub fn kv<M>() -> Kv<M> {
478 new("kv")
479}
480
481common_kind!(
482 Table
484);
485
486impl<M> Table<M> {
487 props! {
488 cols(Vec<Column>) = "cols";
490 rows(Vec<TableRow>) = "rows";
492 }
493
494 pub fn col(mut self, col: Column) -> Self {
496 push(&mut self.0, "cols", &col);
497 self
498 }
499
500 pub fn row(mut self, row: TableRow) -> Self {
502 push(&mut self.0, "rows", &row);
503 self
504 }
505}
506
507pub fn table<M>() -> Table<M> {
509 new("table")
510}
511
512common_kind!(
513 Tree
515);
516
517impl<M> Tree<M> {
518 props! {
519 nodes(Vec<TreeItem>) = "nodes";
521 }
522
523 pub fn item(mut self, item: TreeItem) -> Self {
525 push(&mut self.0, "nodes", &item);
526 self
527 }
528}
529
530pub fn tree<M>() -> Tree<M> {
532 new("tree")
533}
534
535common_kind!(
536 Badge
538);
539
540impl<M> Badge<M> {
541 props! {
542 text(String) = "text";
544 }
545}
546
547pub fn badge<M>(text: impl Into<String>) -> Badge<M> {
549 new::<M, Badge<M>>("badge").text(text)
550}
551
552common_kind!(
553 Kbd
555);
556
557impl<M> Kbd<M> {
558 props! {
559 keys(Vec<String>) = "keys";
561 }
562}
563
564pub fn kbd<M, K: Into<String>>(keys: impl IntoIterator<Item = K>) -> Kbd<M> {
566 new::<M, Kbd<M>>("kbd").keys(keys.into_iter().map(Into::into).collect::<Vec<_>>())
567}
568
569common_kind!(
570 IconNode
572);
573
574impl<M> IconNode<M> {
575 props! {
576 name(Icon) = "name";
578 }
579}
580
581pub fn icon<M>(name: impl Into<Icon>) -> IconNode<M> {
583 new::<M, IconNode<M>>("icon").name(name)
584}
585
586common_kind!(
587 Image
589);
590
591impl<M> Image<M> {
592 props! {
593 blob(String) = "blob";
595 builtin(String) = "builtin";
597 alt(String) = "alt";
599 w(f64) = "w";
601 h(f64) = "h";
603 path(String) = "path";
605 }
606}
607
608pub fn image<M>() -> Image<M> {
610 new("image")
611}
612
613kind_type!(
618 List
620);
621
622impl<M> List<M> {
623 base_methods!();
624
625 title_method!();
626
627 actions_methods!();
628
629 event_methods!();
630
631 children_methods!();
632
633 props! {
634 selected(String) = "selected";
636 filter(String) = "filter";
638 empty(Text) = "empty";
640 virtualize(bool) = "virtual";
642 }
643
644 pub fn max_lines(mut self, lines: u32) -> Self {
646 self.0.set("max", &serde_json::json!({ "lines": lines }));
647 self
648 }
649
650 pub fn max_fraction(mut self, fraction: f64) -> Self {
652 self.0.set("max", &fraction);
653 self
654 }
655}
656
657pub fn list<M>() -> List<M> {
659 new("list")
660}
661
662common_kind!(
663 Item
665);
666
667impl<M> Item<M> {
668 props! {
669 label(Text) = "label";
671 detail(Text) = "detail";
673 value(Text) = "value";
675 icon(Icon) = "icon";
677 hint(Vec<String>) = "hint";
679 disabled(bool) = "disabled";
681 }
682}
683
684pub fn item<M>(label: impl Into<Text>) -> Item<M> {
686 new::<M, Item<M>>("item").label(label)
687}
688
689common_kind!(
690 Tabs
692);
693
694impl<M> Tabs<M> {
695 props! {
696 items(Vec<Tab>) = "items";
698 active(String) = "active";
700 }
701
702 pub fn tab(mut self, id: impl Into<String>, label: impl Into<Text>) -> Self {
704 push(&mut self.0, "items", &Tab { id: id.into(), label: label.into() });
705 self
706 }
707}
708
709pub fn tabs<M>() -> Tabs<M> {
711 new("tabs")
712}
713
714kind_type!(
715 Picker
717);
718
719impl<M> Picker<M> {
720 base_methods!();
721
722 max_method!();
723
724 event_methods!();
725
726 children_methods!();
727
728 props! {
729 size(PickerSize) = "size";
731 layout(PickerLayout) = "layout";
733 preview(PickerPreview) = "preview";
735 title(Text) = "title";
737 subtitle(Text) = "subtitle";
739 icon(Icon) = "icon";
741 query(String) = "query";
743 cursor(usize) = "cursor";
745 placeholder(String) = "placeholder";
747 noun(String) = "noun";
749 focus(PickerFocus) = "focus";
751 state(PickerState) = "state";
753 message(Text) = "message";
755 empty(Text) = "empty";
757 total(u64) = "total";
759 items(Vec<PickerItem>) = "items";
761 items_add(Vec<PickerItem>) = "itemsAdd";
763 items_del(Vec<String>) = "itemsDel";
765 order(Vec<OrderEntry>) = "order";
767 selected(String) = "selected";
769 current(Vec<String>) = "current";
771 hits(std::collections::BTreeMap<String, Vec<[usize; 2]>>) = "hits";
773 columns(Vec<PickerColumn>) = "columns";
775 confirm(Confirm) = "confirm";
777 scopes(Vec<Scope>) = "scopes";
779 scope(String) = "scope";
781 tabs(Vec<PickerTab>) = "tabs";
783 tab(String) = "tab";
785 strip(Strip) = "strip";
787 actions(Vec<PickerAction>) = "actions";
789 }
790}
791
792pub fn picker<M>() -> Picker<M> {
794 new("picker")
795}
796
797common_kind!(
802 Spinner
804);
805
806impl<M> Spinner<M> {
807 props! {
808 style(SpinnerStyle) = "style";
810 label(Text) = "label";
812 }
813}
814
815pub fn spinner<M>() -> Spinner<M> {
817 new("spinner")
818}
819
820common_kind!(
821 Shimmer
823);
824
825impl<M> Shimmer<M> {
826 props! {
827 mode(ShimmerMode) = "mode";
829 palette(ShimmerPalette) = "palette";
831 }
832
833 pub fn text(mut self, text: impl Into<Text>) -> Self {
835 self.0.set_text(text.into());
836 self
837 }
838}
839
840pub fn shimmer<M>(text: impl Into<Text>) -> Shimmer<M> {
842 new::<M, Shimmer<M>>("shimmer").text(text)
843}
844
845common_kind!(
846 Elapsed
848);
849
850impl<M> Elapsed<M> {
851 props! {
852 age(i64) = "age";
854 stopped(u64) = "stopped";
856 format(ElapsedFormat) = "format";
858 }
859
860 pub fn since(self, start: Instant) -> Self {
862 let age = i64::try_from(start.elapsed().as_millis()).unwrap_or(i64::MAX);
863 self.age(age)
864 }
865}
866
867pub fn elapsed<M>() -> Elapsed<M> {
869 new("elapsed")
870}
871
872common_kind!(
873 Rate
875);
876
877impl<M> Rate<M> {
878 props! {
879 value(f64) = "value";
881 unit(String) = "unit";
883 }
884}
885
886pub fn rate<M>(value: f64) -> Rate<M> {
888 new::<M, Rate<M>>("rate").value(value)
889}
890
891common_kind!(
892 Progress
894);
895
896impl<M> Progress<M> {
897 props! {
898 value(f64) = "value";
900 label(Text) = "label";
902 }
903}
904
905pub fn progress<M>() -> Progress<M> {
907 new("progress")
908}
909
910common_kind!(
911 Meter
913);
914
915impl<M> Meter<M> {
916 props! {
917 value(f64) = "value";
919 parts(Vec<Part>) = "parts";
921 thresholds(Thresholds) = "thresholds";
923 style(MeterStyle) = "style";
925 size(Size) = "size";
927 steps(u32) = "steps";
929 marks(Vec<MeterMark>) = "marks";
931 label(Text) = "label";
933 total(Text) = "total";
935 }
936}
937
938pub fn meter<M>() -> Meter<M> {
940 new("meter")
941}
942
943common_kind!(
944 Chart
946);
947
948impl<M> Chart<M> {
949 props! {
950 kind(ChartKind) = "kind";
952 size(Size) = "size";
954 token(String) = "token";
956 summary(Text) = "summary";
958 series(Vec<Point>) = "series";
960 cells(Vec<Vec<Option<f64>>>) = "cells";
962 tips(Vec<Vec<Option<String>>>) = "tips";
964 rows(Vec<String>) = "rows";
966 cols(Vec<ChartCol>) = "cols";
968 }
969}
970
971pub fn chart<M>(kind: impl Into<ChartKind>) -> Chart<M> {
973 new::<M, Chart<M>>("chart").kind(kind)
974}
975
976common_kind!(
977 Effort
979);
980
981impl<M> Effort<M> {
982 props! {
983 level(EffortLevel) = "level";
985 }
986}
987
988pub fn effort<M>(level: impl Into<EffortLevel>) -> Effort<M> {
990 new::<M, Effort<M>>("effort").level(level)
991}
992
993common_kind!(
998 Editor
1000);
1001
1002impl<M> Editor<M> {
1003 props! {
1004 text(String) = "text";
1006 cursor(usize) = "cursor";
1008 anchor(usize) = "anchor";
1010 decor(Vec<Decor>) = "decor";
1012 ghost(String) = "ghost";
1014 placeholder(String) = "placeholder";
1016 prompt(Text) = "prompt";
1018 mode(String) = "mode";
1020 lang(String) = "lang";
1022 readonly(bool) = "readonly";
1024 sendable(bool) = "sendable";
1026 max_lines(u32) = "maxLines";
1028 }
1029}
1030
1031pub fn editor<M>() -> Editor<M> {
1033 new("editor")
1034}
1035
1036common_kind!(
1037 Input
1039);
1040
1041impl<M> Input<M> {
1042 props! {
1043 text(String) = "text";
1045 cursor(usize) = "cursor";
1047 anchor(usize) = "anchor";
1049 decor(Vec<Decor>) = "decor";
1051 ghost(String) = "ghost";
1053 placeholder(String) = "placeholder";
1055 prompt(Text) = "prompt";
1057 mode(String) = "mode";
1059 lang(String) = "lang";
1061 readonly(bool) = "readonly";
1063 sendable(bool) = "sendable";
1065 }
1066}
1067
1068pub fn input<M>() -> Input<M> {
1070 new("input")
1071}
1072
1073container_kind!(
1078 StatusBar
1080);
1081
1082impl<M> StatusBar<M> {
1083 props! {
1084 transparent(bool) = "transparent";
1086 }
1087}
1088
1089pub fn status<M>() -> StatusBar<M> {
1091 new("status")
1092}
1093
1094container_kind!(
1095 Seg
1097);
1098
1099impl<M> Seg<M> {
1100 props! {
1101 icon(Icon) = "icon";
1103 side(Side) = "side";
1105 priority(f64) = "priority";
1107 }
1108
1109 pub fn text(mut self, text: impl Into<Text>) -> Self {
1111 self.0.set_text(text.into());
1112 self
1113 }
1114}
1115
1116pub fn seg<M>(text: impl Into<Text>) -> Seg<M> {
1118 new::<M, Seg<M>>("seg").text(text)
1119}
1120
1121common_kind!(
1122 Toast
1124);
1125
1126impl<M> Toast<M> {
1127 props! {
1128 text(String) = "text";
1130 #[allow(clippy::should_implement_trait, reason = "the prop is named `sub`")]
1132 sub(String) = "sub";
1133 ttl(u64) = "ttl";
1135 }
1136}
1137
1138pub fn toast<M>(text: impl Into<String>) -> Toast<M> {
1140 new::<M, Toast<M>>("toast").text(text)
1141}
1142
1143container_kind!(
1144 Overlay
1146);
1147
1148impl<M> Overlay<M> {
1149 props! {
1150 anchor(Anchor) = "anchor";
1152 size(OverlaySize) = "size";
1154 modal(bool) = "modal";
1156 head(Text) = "head";
1158 }
1159}
1160
1161pub fn overlay<M>() -> Overlay<M> {
1163 new("overlay")
1164}
1165
1166kind_type!(
1171 Tool
1173);
1174
1175impl<M> Tool<M> {
1176 base_methods!();
1177
1178 max_method!();
1179
1180 actions_methods!();
1181
1182 event_methods!();
1183
1184 children_methods!();
1185
1186 props! {
1187 name(String) = "name";
1189 title(Text) = "title";
1191 target(Text) = "target";
1193 target_kind(TargetKind) = "targetKind";
1195 meta(Vec<Text>) = "meta";
1197 badges(Vec<BadgeSpec>) = "badges";
1199 note(Text) = "note";
1201 exit(i64) = "exit";
1203 status(Status) = "status";
1205 age(i64) = "age";
1207 took(u64) = "took";
1209 intent(String) = "intent";
1211 frame(ToolFrame) = "frame";
1213 collapsible(bool) = "collapsible";
1215 collapsed(bool) = "collapsed";
1217 preview(ToolPreview) = "preview";
1219 tools(Vec<ToolButton>) = "tools";
1221 }
1222}
1223
1224pub fn tool<M>(name: impl Into<String>) -> Tool<M> {
1226 new::<M, Tool<M>>("tool").name(name)
1227}
1228
1229container_kind!(
1230 Agent
1232);
1233
1234impl<M> Agent<M> {
1235 props! {
1236 name(String) = "name";
1238 agent(String) = "agent";
1240 badges(Vec<BadgeSpec>) = "badges";
1242 task(Text) = "task";
1244 status(AgentStatus) = "status";
1246 model(String) = "model";
1248 thinking(String) = "thinking";
1250 stats(AgentStats) = "stats";
1252 tool(AgentTool) = "tool";
1254 retry(Retry) = "retry";
1256 depth(u32) = "depth";
1258 collapsible(bool) = "collapsible";
1260 collapsed(bool) = "collapsed";
1262 }
1263}
1264
1265pub fn agent<M>(name: impl Into<String>) -> Agent<M> {
1267 new::<M, Agent<M>>("agent").name(name)
1268}
1269
1270common_kind!(
1271 Checklist
1273);
1274
1275impl<M> Checklist<M> {
1276 props! {
1277 phases(Vec<Phase>) = "phases";
1279 mode(ChecklistMode) = "mode";
1281 note(Text) = "note";
1283 }
1284}
1285
1286pub fn checklist<M>() -> Checklist<M> {
1288 new("checklist")
1289}
1290
1291kind_type!(
1292 Prefs
1294);
1295
1296impl<M> Prefs<M> {
1297 base_methods!();
1298
1299 max_method!();
1300
1301 actions_methods!();
1302
1303 event_methods!();
1304
1305 children_methods!();
1306
1307 props! {
1308 title(String) = "title";
1310 pages(Vec<PrefsPage>) = "pages";
1312 page(String) = "page";
1314 lead(String) = "lead";
1316 sections(Vec<PrefsSection>) = "sections";
1318 query(String) = "query";
1320 cursor(usize) = "cursor";
1322 focus(String) = "focus";
1324 editing(Editing) = "editing";
1326 }
1327}
1328
1329pub fn prefs<M>() -> Prefs<M> {
1331 new("prefs")
1332}
1333
1334fn push<M>(node: &mut Node<M>, key: &str, value: &impl serde::Serialize) {
1336 let Ok(value) = serde_json::to_value(value) else {
1337 return;
1338 };
1339 match node.props.get_mut(key) {
1340 Some(serde_json::Value::Array(list)) => list.push(value),
1341 _ => {
1342 node
1343 .props
1344 .insert(key.to_owned(), serde_json::Value::Array(vec![value]));
1345 },
1346 }
1347}
1348
1349#[cfg(test)]
1350mod tests {
1351 use serde_json::json;
1352
1353 use super::*;
1354
1355 fn wire<M>(node: impl Into<Node<M>>) -> serde_json::Value {
1356 let node = node.into();
1357 json!({ "k": node.kind, "p": node.props })
1358 }
1359
1360 #[test]
1361 fn text_takes_a_string_or_spans() {
1362 assert_eq!(wire::<()>(text("hi")), json!({"k": "text", "p": {"text": "hi"}}));
1363 let styled = text::<()>(spans!["a", span("b").style(Token::Muted).style("strong")]);
1364 assert_eq!(
1365 wire(styled.clone()),
1366 json!({"k": "text", "p": {"spans": [{"t": "a"}, {"t": "b", "s": "muted strong"}]}})
1367 );
1368 assert_eq!(wire(styled.text("c")), json!({"k": "text", "p": {"text": "c"}}));
1370 }
1371
1372 #[test]
1373 fn typed_values_and_escape_hatches_serialize() {
1374 let c = card::<()>()
1375 .head("Deploy")
1376 .status(Status::Running)
1377 .tone("brand-new")
1378 .min(Bound::w(Extent::Ch(10.0)))
1379 .basis(Basis::Content)
1380 .key(7)
1381 .prop("future", json!([1]))
1382 .prop("head", serde_json::Value::Null);
1383 assert_eq!(
1384 wire(c),
1385 json!({"k": "card", "p": {
1386 "status": "running", "tone": "brand-new", "min": {"w": "10ch"},
1387 "basis": "content", "key": 7, "future": [1]
1388 }})
1389 );
1390 }
1391
1392 #[test]
1393 fn kind_specific_props_replace_common_meanings() {
1394 let title = spans![span("Heading").style("strong")];
1395 assert_eq!(
1396 wire(tool::<()>("read").title(title.clone()))["p"]["title"],
1397 json!([{"t": "Heading", "s": "strong"}])
1398 );
1399 assert_eq!(
1400 wire(picker::<()>().title(title).actions(vec![PickerAction {
1401 id: "open".into(),
1402 primary: Some(true),
1403 ..PickerAction::default()
1404 }]))["p"],
1405 json!({
1406 "title": [{"t": "Heading", "s": "strong"}],
1407 "actions": [{"id": "open", "primary": true}]
1408 })
1409 );
1410 assert_eq!(wire(prefs::<()>().title("Settings"))["p"]["title"], "Settings");
1411 assert_eq!(
1412 wire(list::<()>().max_fraction(0.5).max_lines(4))["p"]["max"],
1413 json!({"lines": 4})
1414 );
1415 assert_eq!(wire(list::<()>().max_lines(4).max_fraction(0.5))["p"]["max"], 0.5);
1416 }
1417
1418 #[test]
1419 fn agent_statuses_have_their_own_vocabulary() {
1420 for (status, expected) in [
1421 (AgentStatus::Pending, "pending"),
1422 (AgentStatus::Running, "running"),
1423 (AgentStatus::Done, "done"),
1424 (AgentStatus::Failed, "failed"),
1425 (AgentStatus::Aborted, "aborted"),
1426 (AgentStatus::Idle, "idle"),
1427 (AgentStatus::Parked, "parked"),
1428 (AgentStatus::Other("future".into()), "future"),
1429 ] {
1430 assert_eq!(wire(agent::<()>("worker").status(status))["p"]["status"], expected);
1431 }
1432 }
1433
1434 #[test]
1435 fn table_meter_cells_keep_the_wire_wrapper() {
1436 let node = wire(table::<()>().row(TableRow::new("a").cell("usage", MeterCell {
1437 value: Some(0.5),
1438 title: Some("Half".into()),
1439 tone: Some(Tone::Success),
1440 ..MeterCell::default()
1441 })));
1442 assert_eq!(
1443 node["p"]["rows"][0]["cells"]["usage"],
1444 json!({
1445 "meter": {"value": 0.5, "title": "Half", "tone": "success"}
1446 })
1447 );
1448 assert_eq!(
1449 serde_json::to_value(Cell::Meter(MeterCell::default())).unwrap(),
1450 json!({"meter": {}})
1451 );
1452 }
1453
1454 #[test]
1455 fn list_props_append() {
1456 let k = kv::<()>().item("a", "1").item("b", span("2").style("num"));
1457 assert_eq!(
1458 wire(k),
1459 json!({"k": "kv", "p": {"items": [
1460 {"k": "a", "v": "1"}, {"k": "b", "v": [{"t": "2", "s": "num"}]}
1461 ]}})
1462 );
1463 }
1464}