1use gpui::{
8 AnyElement, App, Entity, Focusable, Global, IntoElement, SharedString, Window, div, prelude::*,
9 px,
10};
11use gpui_kit_assets::Icon;
12use gpui_kit_semantics::{NodeSpec, Role, Semantic};
13use gpui_kit_theme::{Radius, Space, TextTone, Theme, TypeScale};
14
15use crate::controls::combobox::Combobox;
16use crate::controls::input::TextInput;
17use crate::controls::keybinding_recorder::KeybindingRecorder;
18use crate::controls::number_input::NumberInput;
19use crate::controls::select::{Select, SelectOption};
20use crate::controls::split_button::SplitButton;
21use crate::controls::tag_input::TagInput;
22use crate::controls::textarea::TextArea;
23use crate::display::badge::Tone;
24use crate::display::icon::{Icon as IconView, IconTone};
25use crate::foundation::ActiveTheme;
26use crate::foundation::direction::{ActiveDirection, DirectionalExt, LayoutDirection};
27use crate::interaction::dnd;
28use crate::overlay::toast::push as toast_push;
29use crate::overlay::{Edge, Kbd, Overlay, Placement, Tooltip, Tooltipped};
30use crate::prelude::*;
31use crate::strings::{ActiveStrings, StringKey};
32
33pub struct Scene {
35 pub name: &'static str,
36 pub build: fn(&mut Window, &mut App) -> AnyElement,
37}
38
39impl std::fmt::Debug for Scene {
40 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 formatter
42 .debug_struct("Scene")
43 .field("name", &self.name)
44 .finish()
45 }
46}
47
48pub fn catalog() -> Vec<Scene> {
50 #[allow(unused_mut)]
51 let mut scenes = vec![
52 Scene {
53 name: "button",
54 build: button,
55 },
56 Scene {
57 name: "badge",
58 build: badge,
59 },
60 Scene {
61 name: "card",
62 build: card,
63 },
64 Scene {
65 name: "status",
66 build: status,
67 },
68 Scene {
69 name: "loading",
70 build: loading,
71 },
72 Scene {
73 name: "choice",
74 build: choice,
75 },
76 Scene {
77 name: "input",
78 build: input,
79 },
80 Scene {
81 name: "textarea",
82 build: textarea,
83 },
84 Scene {
85 name: "form",
86 build: form,
87 },
88 Scene {
89 name: "auth-sign-in",
90 build: auth_sign_in,
91 },
92 Scene {
93 name: "auth-verification",
94 build: auth_verification,
95 },
96 Scene {
97 name: "actions",
98 build: actions,
99 },
100 Scene {
101 name: "content",
102 build: content,
103 },
104 Scene {
105 name: "kbd",
106 build: kbd,
107 },
108 Scene {
109 name: "overlay",
110 build: overlay,
111 },
112 Scene {
113 name: "dialog",
114 build: dialog,
115 },
116 Scene {
117 name: "tooltip",
118 build: tooltip,
119 },
120 Scene {
121 name: "menu",
122 build: menu,
123 },
124 Scene {
125 name: "context-menu",
126 build: context_menu,
127 },
128 Scene {
129 name: "popover",
130 build: popover,
131 },
132 Scene {
133 name: "command-palette",
134 build: command_palette,
135 },
136 Scene {
137 name: "toast",
138 build: toast,
139 },
140 Scene {
141 name: "tabs",
142 build: tabs,
143 },
144 Scene {
145 name: "accordion",
146 build: accordion,
147 },
148 Scene {
149 name: "breadcrumb",
150 build: breadcrumb,
151 },
152 Scene {
153 name: "list",
154 build: list,
155 },
156 Scene {
157 name: "table",
158 build: table,
159 },
160 Scene {
161 name: "data-grid",
162 build: data_grid,
163 },
164 Scene {
165 name: "data-grid-editing",
166 build: data_grid_editing,
167 },
168 Scene {
169 name: "tree-grid",
170 build: tree_grid,
171 },
172 Scene {
173 name: "tree",
174 build: tree,
175 },
176 Scene {
177 name: "split-pane",
178 build: split_pane,
179 },
180 Scene {
181 name: "scroll-area",
182 build: scroll_area,
183 },
184 Scene {
185 name: "scroll-shadow",
186 build: scroll_shadow,
187 },
188 Scene {
189 name: "scroll-fade",
190 build: scroll_fade,
191 },
192 Scene {
193 name: "frost",
194 build: frost,
195 },
196 Scene {
197 name: "toolbar",
198 build: toolbar,
199 },
200 Scene {
201 name: "sidebar",
202 build: sidebar,
203 },
204 Scene {
205 name: "pagination",
206 build: pagination,
207 },
208 Scene {
209 name: "drawer",
210 build: drawer,
211 },
212 Scene {
213 name: "motion-flip",
214 build: motion_flip,
215 },
216 Scene {
217 name: "motion-state",
218 build: motion_state,
219 },
220 Scene {
221 name: "animated-number",
222 build: animated_number,
223 },
224 Scene {
225 name: "drag-list",
226 build: drag_list,
227 },
228 Scene {
229 name: "drag-tree",
230 build: drag_tree,
231 },
232 Scene {
233 name: "dropzone",
234 build: dropzone,
235 },
236 Scene {
237 name: "wizard",
238 build: wizard,
239 },
240 Scene {
241 name: "undo-history",
242 build: undo_history,
243 },
244 Scene {
245 name: "settings",
246 build: settings,
247 },
248 Scene {
249 name: "detail",
250 build: detail,
251 },
252 Scene {
253 name: "filter-bar",
254 build: filter_bar,
255 },
256 Scene {
257 name: "inline-edit",
258 build: inline_edit,
259 },
260 Scene {
261 name: "progress-circle",
262 build: progress_circle,
263 },
264 Scene {
265 name: "split-tree",
266 build: split_tree,
267 },
268 Scene {
269 name: "ide-shell",
270 build: ide_shell,
271 },
272 Scene {
273 name: "keybinding",
274 build: keybinding,
275 },
276 Scene {
277 name: "keymap-editor",
278 build: keymap_editor,
279 },
280 Scene {
281 name: "markdown",
282 build: markdown,
283 },
284 Scene {
285 name: "conversation",
286 build: conversation,
287 },
288 Scene {
289 name: "image-viewer",
290 build: image_viewer,
291 },
292 Scene {
293 name: "transport",
294 build: transport,
295 },
296 Scene {
297 name: "audio-player",
298 build: audio_player,
299 },
300 Scene {
301 name: "video-player",
302 build: video_player,
303 },
304 Scene {
305 name: "model-viewer",
306 build: model_viewer,
307 },
308 Scene {
309 name: "approval",
310 build: approval,
311 },
312 Scene {
313 name: "permission-matrix",
314 build: permission_matrix,
315 },
316 Scene {
317 name: "cost-meter",
318 build: cost_meter,
319 },
320 Scene {
321 name: "tool-call",
322 build: tool_call,
323 },
324 Scene {
325 name: "step-list",
326 build: step_list,
327 },
328 Scene {
329 name: "node-graph",
330 build: node_graph,
331 },
332 Scene {
333 name: "browser-panel",
334 build: browser_panel,
335 },
336 Scene {
337 name: "thinking",
338 build: thinking,
339 },
340 Scene {
341 name: "json-view",
342 build: json_view,
343 },
344 Scene {
345 name: "schema-form",
346 build: schema_form,
347 },
348 Scene {
349 name: "server-list",
350 build: server_list,
351 },
352 Scene {
353 name: "offering-catalog",
354 build: offering_catalog,
355 },
356 Scene {
357 name: "reading-direction",
358 build: reading_direction,
359 },
360 Scene {
361 name: "toggle",
362 build: toggle,
363 },
364 Scene {
365 name: "collapsible",
366 build: collapsible,
367 },
368 Scene {
369 name: "hover-card",
370 build: hover_card,
371 },
372 Scene {
373 name: "menubar",
374 build: menubar,
375 },
376 Scene {
377 name: "copy-button",
378 build: copy_button,
379 },
380 Scene {
381 name: "aspect-ratio",
382 build: aspect_ratio,
383 },
384 Scene {
385 name: "document-tabs",
386 build: document_tabs,
387 },
388 Scene {
389 name: "search-field",
390 build: search_field,
391 },
392 Scene {
393 name: "find-replace",
394 build: find_replace,
395 },
396 Scene {
397 name: "notification-center",
398 build: notification_center,
399 },
400 Scene {
401 name: "failure-panel",
402 build: failure_panel,
403 },
404 Scene {
405 name: "log-stream",
406 build: log_stream,
407 },
408 Scene {
409 name: "diff-view",
410 build: diff_view,
411 },
412 Scene {
413 name: "sparkline",
414 build: sparkline,
415 },
416 Scene {
417 name: "code-view",
418 build: code_view,
419 },
420 Scene {
421 name: "upload-list",
422 build: upload_list,
423 },
424 Scene {
425 name: "cascader",
426 build: cascader,
427 },
428 Scene {
429 name: "anchor-list",
430 build: anchor_navigation,
431 },
432 Scene {
433 name: "diagnostics-list",
434 build: diagnostics_surface,
435 },
436 ];
437 #[cfg(feature = "fixtures")]
438 scenes.extend([
439 Scene {
440 name: "calendar",
441 build: calendar,
442 },
443 Scene {
444 name: "date-range",
445 build: date_range,
446 },
447 Scene {
448 name: "date-time",
449 build: date_time,
450 },
451 ]);
452 scenes
453}
454
455pub fn find(name: &str) -> Option<Scene> {
457 catalog().into_iter().find(|scene| scene.name == name)
458}
459
460pub fn direction(name: &str) -> LayoutDirection {
468 match name {
469 "reading-direction" => LayoutDirection::RightToLeft,
470 _ => LayoutDirection::LeftToRight,
471 }
472}
473
474fn stack(theme: &Theme) -> gpui::Div {
475 div()
476 .column()
477 .gap(px(theme.spacing.md))
478 .p(px(theme.spacing.lg))
479 .bg(theme.colors.canvas)
480 .text_color(theme.colors.text)
481 .font_family(theme.typography.sans.clone())
482}
483
484fn row(theme: &Theme) -> gpui::Div {
485 div()
486 .row()
487 .flex_wrap()
488 .gap(px(theme.spacing.sm))
489 .items_center()
490}
491
492fn button(_window: &mut Window, cx: &mut App) -> AnyElement {
493 let theme = cx.theme().clone();
494 stack(&theme)
495 .child(
496 row(&theme)
497 .child(
498 Button::new("scene.button.primary")
499 .label("Primary")
500 .primary()
501 .on_click(|_, _| {}),
502 )
503 .child(
504 Button::new("scene.button.secondary")
505 .label("Secondary")
506 .secondary()
507 .on_click(|_, _| {}),
508 )
509 .child(
510 Button::new("scene.button.ghost")
511 .label("Ghost")
512 .ghost()
513 .on_click(|_, _| {}),
514 )
515 .child(
516 Button::new("scene.button.danger")
517 .label("Delete")
518 .danger()
519 .on_click(|_, _| {}),
520 )
521 .child(
522 Button::new("scene.button.link")
523 .label("Learn more")
524 .link()
525 .on_click(|_, _| {}),
526 ),
527 )
528 .child(
529 row(&theme)
530 .child(
531 Button::new("scene.button.disabled")
532 .label("Unavailable")
533 .primary()
534 .disabled(true)
535 .on_click(|_, _| {}),
536 )
537 .child(
538 Button::new("scene.button.loading")
539 .label("Saving")
540 .primary()
541 .loading(true)
542 .on_click(|_, _| {}),
543 )
544 .child(
545 Button::new("scene.button.selected")
546 .label("Selected")
547 .secondary()
548 .selected(true)
549 .on_click(|_, _| {}),
550 ),
551 )
552 .child(
553 row(&theme)
554 .child(Button::new("scene.button.xs").label("Extra small").xs())
555 .child(Button::new("scene.button.sm").label("Small").small())
556 .child(Button::new("scene.button.md").label("Medium").medium())
557 .child(Button::new("scene.button.lg").label("Large").large()),
558 )
559 .into_any_element()
560}
561
562fn badge(_window: &mut Window, cx: &mut App) -> AnyElement {
563 let theme = cx.theme().clone();
564 stack(&theme)
565 .child(
566 row(&theme)
567 .child(Badge::new("Neutral").neutral().id("scene.badge.neutral"))
568 .child(Badge::new("Accent").accent().id("scene.badge.accent"))
569 .child(Badge::new("Success").success().id("scene.badge.success"))
570 .child(Badge::new("Warning").warning().id("scene.badge.warning"))
571 .child(Badge::new("Danger").danger().id("scene.badge.danger"))
572 .child(Badge::new("Info").info().id("scene.badge.info")),
573 )
574 .into_any_element()
575}
576
577fn card(_window: &mut Window, cx: &mut App) -> AnyElement {
578 let theme = cx.theme().clone();
579 stack(&theme)
580 .child(
581 Card::new()
582 .id("scene.card")
583 .child(
584 ListRow::new()
585 .id("scene.card.runtime")
586 .child(div().flex_1().child(crate::foundation::text(
587 &theme,
588 TypeScale::Label,
589 "Native runtime",
590 )))
591 .child(Badge::new("Ready").success()),
592 )
593 .child(
594 ListRow::new()
595 .id("scene.card.catalog")
596 .child(div().flex_1().child(crate::foundation::text(
597 &theme,
598 TypeScale::Label,
599 "Model catalog",
600 )))
601 .child(Badge::new("Stale").warning()),
602 ),
603 )
604 .into_any_element()
605}
606
607#[derive(Debug)]
608struct SceneGraph {
609 viewport: GraphViewport,
610 ingest: gpui::Point<f32>,
611 validate: gpui::Point<f32>,
612 persist: gpui::Point<f32>,
613 observe: gpui::Point<f32>,
614 publish: gpui::Point<f32>,
615 edges: Vec<GraphEdge>,
616}
617
618impl Global for SceneGraph {}
619
620fn node_graph(_window: &mut Window, cx: &mut App) -> AnyElement {
624 if !cx.has_global::<SceneGraph>() {
625 cx.set_global(SceneGraph {
626 viewport: GraphViewport::default(),
627 ingest: gpui::point(24.0, 200.0),
628 validate: gpui::point(244.0, 56.0),
629 persist: gpui::point(500.0, 56.0),
630 observe: gpui::point(464.0, 330.0),
631 publish: gpui::point(684.0, 200.0),
632 edges: vec![
633 GraphEdge::new("scene.graph.ingest", "scene.graph.validate")
634 .id("scene.graph.edge.rows")
635 .ports("rows", "records")
636 .label("12.4k rows")
637 .active(true),
638 GraphEdge::new("scene.graph.validate", "scene.graph.persist")
639 .id("scene.graph.edge.valid")
640 .ports("valid", "records")
641 .active(true),
642 GraphEdge::new("scene.graph.validate", "scene.graph.observe")
643 .id("scene.graph.edge.telemetry")
644 .ports("telemetry", "events")
645 .label("telemetry")
646 .lane(-1),
647 GraphEdge::new("scene.graph.persist", "scene.graph.publish")
648 .id("scene.graph.edge.commit")
649 .ports("commit", "artifact")
650 .label("commit 8f72")
651 .active(true),
652 GraphEdge::new("scene.graph.observe", "scene.graph.validate")
653 .id("scene.graph.edge.retry")
654 .ports("retry", "retry")
655 .label("retry policy")
656 .lane(1)
657 .feedback(),
658 ],
659 });
660 }
661 let scene = cx.global::<SceneGraph>();
662 let viewport = scene.viewport;
663 let ingest = scene.ingest;
664 let validate = scene.validate;
665 let persist = scene.persist;
666 let observe = scene.observe;
667 let publish = scene.publish;
668 let edges = scene.edges.clone();
669 let theme = cx.theme().clone();
670 stack(&theme)
671 .child(
672 div().w(px(860.0)).h(px(500.0)).child(
673 NodeGraph::new("scene.graph")
674 .viewport(viewport)
675 .zoom_range(0.55, 1.8)
676 .node(
677 GraphNode::new("scene.graph.ingest", "Stream ingest")
678 .width(176.0)
679 .state(NodeState::Succeeded)
680 .action("orders.v2 · partition 18")
681 .metric("rate", "3.2k/s")
682 .port(GraphPort::input("source", "Source").side(PortSide::Top))
683 .port(GraphPort::output("rows", "Rows"))
684 .port(GraphPort::output("errors", "Errors")),
685 ingest.x,
686 ingest.y,
687 )
688 .node(
689 GraphNode::new("scene.graph.validate", "Validate & enrich")
690 .width(176.0)
691 .state(NodeState::Running)
692 .action("schema + fraud signals")
693 .metric("p95", "18 ms")
694 .port(GraphPort::input("records", "Records"))
695 .port(GraphPort::input("retry", "Retry").side(PortSide::Bottom))
696 .port(GraphPort::output("valid", "Valid"))
697 .port(GraphPort::output("telemetry", "Events").side(PortSide::Bottom))
698 .selected(true),
699 validate.x,
700 validate.y,
701 )
702 .node(
703 GraphNode::new("scene.graph.persist", "Persist batch")
704 .width(176.0)
705 .state(NodeState::Succeeded)
706 .action("warehouse / orders")
707 .metric("written", "12.3k")
708 .port(GraphPort::input("records", "Records"))
709 .port(GraphPort::output("commit", "Commit")),
710 persist.x,
711 persist.y,
712 )
713 .node(
714 GraphNode::new("scene.graph.observe", "Observe quality")
715 .width(176.0)
716 .state(NodeState::Failed)
717 .action("drift threshold exceeded")
718 .metric("rejected", "94")
719 .port(GraphPort::input("events", "Events").side(PortSide::Top))
720 .port(GraphPort::output("retry", "Retry").side(PortSide::Top)),
721 observe.x,
722 observe.y,
723 )
724 .node(
725 GraphNode::new("scene.graph.publish", "Publish artifact")
726 .width(176.0)
727 .state(NodeState::Pending)
728 .action("waiting for commit")
729 .port(GraphPort::input("artifact", "Artifact").side(PortSide::Top))
730 .port(GraphPort::output("release", "Release").side(PortSide::Bottom)),
731 publish.x,
732 publish.y,
733 )
734 .edges(edges)
735 .on_event(|event, _, cx| {
736 cx.update_global::<SceneGraph, ()>(|scene, _| match event {
737 NodeGraphEvent::ViewportChanged(viewport) => {
738 scene.viewport = *viewport;
739 }
740 NodeGraphEvent::NodeMoved { id, position } => match id.as_ref() {
741 "scene.graph.ingest" => scene.ingest = *position,
742 "scene.graph.validate" => scene.validate = *position,
743 "scene.graph.persist" => scene.persist = *position,
744 "scene.graph.observe" => scene.observe = *position,
745 "scene.graph.publish" => scene.publish = *position,
746 _ => {}
747 },
748 NodeGraphEvent::ConnectionRequested { from, to } => {
749 let id = format!(
750 "scene.graph.edge.user.{}.{}.{}.{}",
751 from.node, from.port, to.node, to.port
752 );
753 if !scene
754 .edges
755 .iter()
756 .any(|edge| edge.from() == &from.node && edge.to() == &to.node)
757 {
758 scene.edges.push(
759 GraphEdge::new(from.node.clone(), to.node.clone())
760 .id(id)
761 .ports(from.port.clone(), to.port.clone())
762 .label("new connection"),
763 );
764 }
765 }
766 });
767 cx.refresh_windows();
768 }),
769 ),
770 )
771 .into_any_element()
772}
773
774fn browser_panel(_window: &mut Window, cx: &mut App) -> AnyElement {
778 let theme = cx.theme().clone();
779 stack(&theme)
780 .child(
781 row(&theme)
782 .items_start()
783 .child(
784 div().w(px(232.0)).h(px(190.0)).child(
785 BrowserPanel::new("scene.browser.loading")
786 .url("https://docs.example.com/a/long/path/that-must-stay-inside-the-address-well")
787 .state(ViewportState::Loading)
788 .on_reload(|_, _| {}),
789 ),
790 )
791 .child(
792 div().w(px(232.0)).h(px(190.0)).child(
793 BrowserPanel::new("scene.browser.empty")
794 .url("https://docs.example.com/empty")
795 .state(ViewportState::Empty)
796 .on_back(|_, _| {})
797 .on_reload(|_, _| {}),
798 ),
799 )
800 .child(
801 div().w(px(232.0)).h(px(190.0)).child(
802 BrowserPanel::new("scene.browser.unavailable")
803 .url("https://internal.example.com/admin")
804 .state(ViewportState::Unavailable(
805 "The workspace policy does not allow this host.".into(),
806 ))
807 .on_reload(|_, _| {}),
808 ),
809 ),
810 )
811 .child(
812 row(&theme)
813 .items_start()
814 .child(
815 div().w(px(352.0)).h(px(210.0)).child(
816 BrowserPanel::new("scene.browser.error")
817 .url("https://status.example.com/incidents/current")
818 .state(ViewportState::Error(
819 "The host could not resolve this address.".into(),
820 ))
821 .on_back(|_, _| {})
822 .on_reload(|_, _| {}),
823 ),
824 )
825 .child(
826 div().w(px(352.0)).h(px(210.0)).child(
827 BrowserPanel::new("scene.browser.ready")
828 .url("https://docs.example.com/ready")
829 .state(ViewportState::Ready)
830 .viewport(
831 div()
832 .size_full()
833 .p_token(&theme, Space::Md)
834 .child(crate::foundation::text(
835 &theme,
836 TypeScale::Subtitle,
837 "Host-owned page surface",
838 ))
839 .child(
840 div()
841 .mt_token(&theme, Space::Sm)
842 .child(crate::foundation::text(&theme, TypeScale::Body, "Long page content remains clipped by the BrowserPanel viewport even when it cannot wrap naturally.").text_tone(&theme, TextTone::Muted)),
843 ),
844 )
845 .on_back(|_, _| {})
846 .on_forward(|_, _| {})
847 .on_reload(|_, _| {}),
848 ),
849 ),
850 )
851 .into_any_element()
852}
853
854fn status(_window: &mut Window, cx: &mut App) -> AnyElement {
855 let theme = cx.theme().clone();
856 stack(&theme)
857 .child(
858 row(&theme)
859 .child(StatusDot::new(Tone::Success))
860 .child(StatusDot::new(Tone::Warning))
861 .child(StatusDot::new(Tone::Danger))
862 .child(StatusDot::new(Tone::Neutral)),
863 )
864 .child(
865 StatusLine::new("Connected", Tone::Success).id("scene.status.line"),
866 )
867 .child(
868 Callout::new(
869 "The host refused this action. The refusal is shown, not converted to an empty state.",
870 Tone::Danger,
871 )
872 .id("scene.status.refusal"),
873 )
874 .child(
875 Callout::new("Refreshing failed. The last verified value remains visible.", Tone::Warning)
876 .id("scene.status.stale"),
877 )
878 .into_any_element()
879}
880
881fn loading(_window: &mut Window, cx: &mut App) -> AnyElement {
882 let theme = cx.theme().clone();
883 let indicator = |title: &'static str, loader: AnyElement| {
884 div()
885 .column()
886 .items_center()
887 .justify_center()
888 .gap_token(&theme, Space::Md)
889 .w(px(220.0))
890 .h(px(112.0))
891 .p_token(&theme, Space::Md)
892 .radius(&theme, Radius::Card)
893 .surface(&theme, gpui_kit_theme::Surface::Panel)
894 .child(crate::foundation::text(&theme, TypeScale::Label, title))
895 .child(loader)
896 };
897 stack(&theme)
898 .w_full()
899 .max_w(px(520.0))
900 .child(
901 row(&theme)
902 .gap_token(&theme, Space::Md)
903 .child(indicator(
904 "Loading providers",
905 PulseLoader::new("scene.loading.pulse")
906 .label("Loading providers")
907 .into_any_element(),
908 ))
909 .child(indicator(
910 "Contacting host",
911 GradientSpinner::new("scene.loading.spinner")
912 .label("Contacting host")
913 .into_any_element(),
914 )),
915 )
916 .child(
917 div()
918 .column()
919 .gap_token(&theme, Space::Sm)
920 .w_full()
921 .child(crate::foundation::text(
922 &theme,
923 TypeScale::Label,
924 "Loading list",
925 ))
926 .child(
927 Skeleton::new("scene.loading.skeleton")
928 .rows(3)
929 .label("Loading list"),
930 ),
931 )
932 .into_any_element()
933}
934
935fn kbd(_window: &mut Window, cx: &mut App) -> AnyElement {
936 let theme = cx.theme().clone();
937 stack(&theme)
938 .child(
939 row(&theme)
940 .child(Kbd::new("cmd-shift-p").id("scene.kbd.palette"))
941 .child(Kbd::new("ctrl-c").id("scene.kbd.copy"))
942 .child(Kbd::new("enter").id("scene.kbd.confirm"))
943 .child(Kbd::new("escape").id("scene.kbd.dismiss")),
944 )
945 .into_any_element()
946}
947
948fn overlay(_window: &mut Window, cx: &mut App) -> AnyElement {
949 let theme = cx.theme().clone();
950 stack(&theme)
951 .w(px(520.0))
952 .h(px(320.0))
953 .child(crate::foundation::text(
954 &theme,
955 TypeScale::Body,
956 "Content behind the dialog",
957 ))
958 .child(
959 Overlay::modal("scene.overlay.dialog")
960 .placement(Placement::Center)
961 .child(
962 crate::overlay::surface(&theme, gpui_kit_theme::Elevation::Modal)
963 .w(px(320.0))
964 .p(px(theme.spacing.lg))
965 .gap(px(theme.spacing.sm))
966 .child(crate::foundation::text(
967 &theme,
968 TypeScale::Subtitle,
969 "Delete this workspace?",
970 ))
971 .child(
972 div()
973 .row()
974 .gap(px(theme.spacing.sm))
975 .child(
976 Button::new("scene.overlay.cancel")
977 .label("Cancel")
978 .secondary()
979 .on_click(|_, _| {}),
980 )
981 .child(
982 Button::new("scene.overlay.confirm")
983 .label("Delete")
984 .danger()
985 .on_click(|_, _| {}),
986 ),
987 ),
988 ),
989 )
990 .into_any_element()
991}
992
993struct SceneDialog {
998 replace: Entity<Dialog>,
999}
1000
1001impl Global for SceneDialog {}
1002
1003fn dialog(window: &mut Window, cx: &mut App) -> AnyElement {
1004 if !cx.has_global::<SceneDialog>() {
1005 let replace = cx.new(|cx| {
1006 Dialog::new("scene.dialog.replace", window, cx)
1007 .title("Replace the existing theme?")
1008 .description(
1009 "The application owns this decision. The dialog presents it and reports what \
1010 was chosen.",
1011 )
1012 .cancel_label("Cancel")
1013 .confirm_label("Replace")
1014 });
1015 replace.update(cx, |dialog, cx| dialog.open(window, cx));
1016 cx.set_global(SceneDialog { replace });
1017 }
1018 let replace = cx.global::<SceneDialog>().replace.clone();
1019 let theme = cx.theme().clone();
1020
1021 stack(&theme)
1022 .w(px(560.0))
1023 .h(px(360.0))
1024 .child(crate::foundation::text(
1025 &theme,
1026 TypeScale::Body,
1027 "Content behind the dialog",
1028 ))
1029 .child(replace)
1030 .into_any_element()
1031}
1032
1033fn tooltip(_window: &mut Window, cx: &mut App) -> AnyElement {
1034 let theme = cx.theme().clone();
1035 stack(&theme)
1036 .child(
1037 row(&theme).child(
1038 div()
1039 .id("scene.tooltip.host")
1040 .tip("scene.tooltip.export", "Writes the theme to a file on disk")
1041 .child(
1042 Button::new("scene.tooltip.export")
1043 .label("Export theme")
1044 .accessible_description("Writes the theme to a file on disk")
1045 .secondary()
1046 .on_click(|_, _| {}),
1047 ),
1048 ),
1049 )
1050 .child(
1053 row(&theme).child(
1054 Tooltip::new("scene.tooltip.help", "Writes the theme to a file on disk")
1055 .describes("scene.tooltip.export"),
1056 ),
1057 )
1058 .into_any_element()
1059}
1060
1061struct SceneMenus {
1067 menu: Entity<Menu>,
1068 context: Entity<ContextMenu>,
1069 palette: Entity<CommandPalette>,
1070 popover: Entity<Popover>,
1071}
1072
1073impl Global for SceneMenus {}
1074
1075fn menu_items() -> Vec<MenuItem> {
1076 vec![
1077 MenuItem::section("group", "This run"),
1078 MenuItem::command("copy", "Copy run id")
1079 .icon(Icon::Copy)
1080 .shortcut("cmd-c"),
1081 MenuItem::check("follow", "Follow output", true),
1082 MenuItem::separator("rule"),
1083 MenuItem::command("publish", "Publish").disabled(true),
1084 MenuItem::submenu(
1085 "share",
1086 "Share",
1087 [
1088 MenuItem::command("share.link", "Copy link").shortcut("cmd-shift-c"),
1089 MenuItem::command("share.export", "Export as file"),
1090 ],
1091 ),
1092 ]
1093}
1094
1095fn scene_commands() -> Vec<Command> {
1096 vec![
1097 Command::new("workspace.open", "Open workspace")
1098 .section("Workspace")
1099 .shortcut("cmd-o"),
1100 Command::new("workspace.close", "Close workspace").section("Workspace"),
1101 Command::new("workspace.publish", "Publish workspace")
1102 .section("Workspace")
1103 .unavailable("Approval is required"),
1104 Command::new("editor.wrap", "Toggle word wrap").section("Editor"),
1105 ]
1106}
1107
1108fn ensure_menus(window: &mut Window, cx: &mut App) {
1109 if cx.has_global::<SceneMenus>() {
1110 return;
1111 }
1112 let menu = cx.new(|cx| {
1113 Menu::new("scene.menu.run", window, cx)
1114 .trigger("Run actions")
1115 .items(menu_items())
1116 });
1117 menu.update(cx, |menu, cx| {
1118 menu.open_submenu("share", window, cx);
1119 });
1120
1121 let context = cx.new(|cx| {
1122 ContextMenu::new("scene.context.run", window, cx)
1123 .name("Run actions")
1124 .target("run-a04")
1125 .menu(menu_items())
1126 .content(|_, cx| {
1127 let theme = cx.theme().clone();
1128 div()
1129 .w(px(320.0))
1130 .p(px(theme.spacing.md))
1131 .hairline(&theme)
1132 .radius(&theme, Radius::Card)
1133 .child(crate::foundation::text(
1134 &theme,
1135 TypeScale::Body,
1136 "Right-click this fixture row",
1137 ))
1138 .into_any_element()
1139 })
1140 });
1141 context.update(cx, |context, cx| {
1142 context.open_at(gpui::point(px(180.0), px(150.0)), window, cx);
1143 });
1144
1145 let palette = cx.new(|cx| {
1146 CommandPalette::new("scene.palette.commands", window, cx).commands(scene_commands())
1147 });
1148 palette.update(cx, |palette, cx| palette.set_query("work", cx));
1149
1150 let popover = cx.new(|cx| {
1151 Popover::new("scene.popover.filters", window, cx)
1152 .trigger("Filters")
1153 .content(|_, cx| {
1154 let theme = cx.theme().clone();
1155 div()
1156 .column()
1157 .w(px(260.0))
1158 .gap(px(theme.spacing.sm))
1159 .child(crate::foundation::text(
1160 &theme,
1161 TypeScale::Body,
1162 "Anything can live in a popover.",
1163 ))
1164 .child(
1165 Checkbox::new("scene.popover.failing")
1166 .label("Failing runs only")
1167 .on_change(|_, _, _| {}),
1168 )
1169 .into_any_element()
1170 })
1171 });
1172 popover.update(cx, |popover, cx| popover.open(window, cx));
1173
1174 cx.set_global(SceneMenus {
1175 menu,
1176 context,
1177 palette,
1178 popover,
1179 });
1180}
1181
1182fn popover(window: &mut Window, cx: &mut App) -> AnyElement {
1183 ensure_menus(window, cx);
1184 let popover = cx.global::<SceneMenus>().popover.clone();
1185 let theme = cx.theme().clone();
1186 stack(&theme)
1187 .w(px(520.0))
1188 .h(px(320.0))
1189 .child(crate::foundation::text(
1192 &theme,
1193 TypeScale::Body,
1194 "The trigger owns whether the surface is open.",
1195 ))
1196 .child(popover)
1197 .into_any_element()
1198}
1199
1200fn menu(window: &mut Window, cx: &mut App) -> AnyElement {
1201 ensure_menus(window, cx);
1202 let menu = cx.global::<SceneMenus>().menu.clone();
1203 let theme = cx.theme().clone();
1204 stack(&theme)
1205 .w(px(560.0))
1206 .h(px(360.0))
1207 .child(menu)
1208 .into_any_element()
1209}
1210
1211fn context_menu(window: &mut Window, cx: &mut App) -> AnyElement {
1212 ensure_menus(window, cx);
1213 let context = cx.global::<SceneMenus>().context.clone();
1214 let theme = cx.theme().clone();
1215 stack(&theme)
1216 .w(px(560.0))
1217 .h(px(400.0))
1218 .child(crate::foundation::text(
1221 &theme,
1222 TypeScale::Body,
1223 "The right-click reports the row. Nothing is selected by it.",
1224 ))
1225 .child(context)
1226 .into_any_element()
1227}
1228
1229fn command_palette(window: &mut Window, cx: &mut App) -> AnyElement {
1230 ensure_menus(window, cx);
1231 let palette = cx.global::<SceneMenus>().palette.clone();
1232 let theme = cx.theme().clone();
1233 stack(&theme)
1236 .w_full()
1237 .h(px(420.0))
1238 .items_center()
1239 .pt(px(theme.spacing.xxl))
1240 .child(palette)
1241 .into_any_element()
1242}
1243
1244struct SceneToasts {
1249 layer: Entity<ToastLayer>,
1250}
1251
1252impl Global for SceneToasts {}
1253
1254fn toast(_window: &mut Window, cx: &mut App) -> AnyElement {
1255 if !cx.has_global::<SceneToasts>() {
1256 let layer = cx.new(|cx| ToastLayer::new(cx).capacity(4));
1257 cx.set_global(SceneToasts { layer });
1258 toast_push(
1259 cx,
1260 Toast::new("scene.toast.saved", "Theme exported to disk").tone(Tone::Success),
1261 );
1262 toast_push(
1263 cx,
1264 Toast::new("scene.toast.stale", "Refreshing the model catalog failed")
1265 .tone(Tone::Warning)
1266 .detail("The last verified catalog is still shown."),
1267 );
1268 toast_push(
1269 cx,
1270 Toast::new(
1273 "scene.toast.refused",
1274 "The host refused to publish this run",
1275 )
1276 .tone(Tone::Warning)
1277 .detail("Approval is required for this workspace.")
1278 .action("Request approval", |_, _| {}),
1279 );
1280 toast_push(
1284 cx,
1285 Toast::new("scene.toast.failed", "Publishing this run failed")
1286 .tone(Tone::Danger)
1287 .detail("The publish service did not respond.")
1288 .action("Try again", |_, _| {}),
1289 );
1290 }
1291 let layer = cx.global::<SceneToasts>().layer.clone();
1292 let theme = cx.theme().clone();
1293
1294 stack(&theme)
1295 .w(px(560.0))
1296 .h(px(360.0))
1297 .child(crate::foundation::text(
1298 &theme,
1299 TypeScale::Body,
1300 "Content behind the notifications",
1301 ))
1302 .child(crate::foundation::text(
1304 &theme,
1305 TypeScale::Body,
1306 "A danger or warning toast stays until it is dismissed.",
1307 ))
1308 .child(layer)
1309 .into_any_element()
1310}
1311
1312fn tabs(_window: &mut Window, cx: &mut App) -> AnyElement {
1313 let theme = cx.theme().clone();
1314 stack(&theme)
1315 .w(px(520.0))
1316 .child(
1317 Tabs::new("scene.tabs.workspace")
1318 .tabs([
1319 TabItem::new("overview", "Overview").icon(Icon::Widget),
1320 TabItem::new("runs", "Runs").badge("12"),
1321 TabItem::new("logs", "Logs"),
1322 TabItem::new("billing", "Billing").disabled(true),
1323 ])
1324 .selected("runs")
1325 .on_select(|_, _, _| {}),
1326 )
1327 .child(crate::foundation::text(
1329 &theme,
1330 TypeScale::Body,
1331 "Runs are rendered by the caller, not by the strip.",
1332 ))
1333 .into_any_element()
1334}
1335
1336struct SceneDocumentTabs {
1341 overflow: Entity<Menu>,
1342}
1343
1344impl Global for SceneDocumentTabs {}
1345
1346fn document_tabs(window: &mut Window, cx: &mut App) -> AnyElement {
1347 if !cx.has_global::<SceneDocumentTabs>() {
1348 let name = cx.strings().text(StringKey::TabMoreTabs);
1351 let overflow = cx.new(|cx| {
1352 Menu::new("scene.document-tabs.overflow", window, cx)
1353 .trigger_icon(Icon::AltArrowDown)
1354 .trigger_name(name)
1355 });
1356 cx.set_global(SceneDocumentTabs { overflow });
1357 }
1358 let overflow = cx.global::<SceneDocumentTabs>().overflow.clone();
1359 let theme = cx.theme().clone();
1360 stack(&theme)
1361 .w(px(620.0))
1362 .child(caption(
1363 &theme,
1364 "clean, unsaved, saving, save failed — three marks, and silence for the fourth",
1365 ))
1366 .child(
1367 Tabs::new("scene.document-tabs.editor")
1368 .tabs([
1369 TabItem::new("readme", "README.md").closable(true),
1370 TabItem::new("main", "main.rs").dirty().closable(true),
1371 TabItem::new("theme", "theme.json").saving().closable(true),
1372 TabItem::new("notes", "notes.md")
1373 .save_failed("The workspace is read-only.")
1374 .closable(true),
1375 ])
1376 .selected("main")
1377 .on_select(|_, _, _| {})
1378 .on_close(|_, _, _| {}),
1379 )
1380 .child(caption(
1381 &theme,
1382 "past the declared limit the rest go to a menu, which stays reachable from the keyboard",
1383 ))
1384 .child(
1385 Tabs::new("scene.document-tabs.overflowing")
1386 .tabs([
1387 TabItem::new("one", "adapter.rs").closable(true),
1388 TabItem::new("two", "catalog.rs").dirty().closable(true),
1389 TabItem::new("three", "harness.rs").closable(true),
1390 TabItem::new("four", "registry.rs").closable(true),
1391 TabItem::new("five", "transport.rs").dirty().closable(true),
1392 ])
1393 .selected("two")
1394 .overflow_after(3)
1395 .overflow_menu(overflow)
1396 .on_select(|_, _, _| {})
1397 .on_close(|_, _, _| {}),
1398 )
1399 .into_any_element()
1400}
1401
1402struct SceneSearch {
1407 field: Entity<SearchField>,
1408 replace: Entity<FindReplace>,
1409}
1410
1411impl Global for SceneSearch {}
1412
1413fn ensure_search(window: &mut Window, cx: &mut App) {
1414 if cx.has_global::<SceneSearch>() {
1415 return;
1416 }
1417 let field = cx.new(|cx| SearchField::new("scene.search.field", window, cx));
1418 field.update(cx, |field, cx| {
1419 field.set_query("transport", cx);
1420 field.set_count(
1421 HitCount::Known {
1422 total: 12,
1423 current: Some(2),
1424 },
1425 cx,
1426 );
1427 });
1428
1429 let replace = cx.new(|cx| FindReplace::new("scene.search.replace", window, cx));
1430 replace.update(cx, |replace, cx| {
1431 replace.search_field().update(cx, |field, cx| {
1432 field.set_query("transport", cx);
1433 });
1434 replace.replacement_input().update(cx, |input, cx| {
1435 input.set_value("delivery", cx);
1436 });
1437 replace.set_count(
1438 HitCount::Known {
1439 total: 12,
1440 current: Some(2),
1441 },
1442 cx,
1443 );
1444 });
1445
1446 cx.set_global(SceneSearch { field, replace });
1447}
1448
1449fn search_field(window: &mut Window, cx: &mut App) -> AnyElement {
1450 ensure_search(window, cx);
1451 let field = cx.global::<SceneSearch>().field.clone();
1452 let theme = cx.theme().clone();
1453
1454 stack(&theme)
1457 .w(px(620.0))
1458 .child(field)
1459 .child(caption(
1460 &theme,
1461 "counting is not none, and too many is not a total",
1462 ))
1463 .child(
1464 row(&theme)
1465 .child(hit_count_sample(&theme, "counting", HitCount::Counting))
1466 .child(hit_count_sample(&theme, "none", HitCount::None))
1467 .child(hit_count_sample(
1468 &theme,
1469 "too many",
1470 HitCount::TooMany { counted: 500 },
1471 )),
1472 )
1473 .child(caption(&theme, "the current hit is not the other hits"))
1474 .child(
1475 div().child(
1476 HighlightedText::new(
1477 "The transport reports what it did; the transport never decides.",
1478 )
1479 .id("scene.search.line")
1480 .hits([4..13, 39..48])
1481 .current(1),
1482 ),
1483 )
1484 .into_any_element()
1485}
1486
1487fn hit_count_sample(theme: &Theme, label: &'static str, count: HitCount) -> gpui::Div {
1489 div()
1490 .column()
1491 .gap(px(theme.spacing.xs))
1492 .child(caption(theme, label))
1493 .child(
1494 div()
1495 .px(px(theme.spacing.sm))
1496 .py(px(theme.spacing.xs))
1497 .hairline(theme)
1498 .radius(theme, Radius::Control)
1499 .child(crate::foundation::text(
1500 theme,
1501 TypeScale::Label,
1502 count.name(),
1503 )),
1504 )
1505}
1506
1507fn find_replace(window: &mut Window, cx: &mut App) -> AnyElement {
1508 ensure_search(window, cx);
1509 let replace = cx.global::<SceneSearch>().replace.clone();
1510 let theme = cx.theme().clone();
1511 stack(&theme)
1512 .w(px(620.0))
1513 .child(caption(
1516 &theme,
1517 "replace all names its count before it acts",
1518 ))
1519 .child(replace)
1520 .into_any_element()
1521}
1522
1523struct SceneNotifications {
1525 centre: Entity<NotificationCenter>,
1526}
1527
1528impl Global for SceneNotifications {}
1529
1530fn notification_center(_window: &mut Window, cx: &mut App) -> AnyElement {
1531 if !cx.has_global::<SceneNotifications>() {
1532 let centre = cx.new(|cx| NotificationCenter::new("scene.notifications", cx));
1533 centre.update(cx, |centre, cx| {
1534 centre.record(
1535 Notification::new("scene.notify.exported", "Theme exported to disk")
1536 .tone(Tone::Success)
1537 .at("9:41")
1538 .read(true),
1539 cx,
1540 );
1541 centre.record(
1542 Notification::new("scene.notify.stale", "Refreshing the model catalog failed")
1543 .tone(Tone::Warning)
1544 .detail("The last verified catalog is still shown.")
1545 .at("9:44"),
1546 cx,
1547 );
1548 centre.record(
1549 Notification::new(
1550 "scene.notify.refused",
1551 "The host refused to publish this run",
1552 )
1553 .tone(Tone::Warning)
1554 .detail("Approval is required for this workspace.")
1555 .at("9:46")
1556 .action("Request approval", |_, _| {}),
1557 cx,
1558 );
1559 });
1560 cx.set_global(SceneNotifications { centre });
1561 }
1562 let centre = cx.global::<SceneNotifications>().centre.clone();
1563 let theme = cx.theme().clone();
1564 stack(&theme)
1565 .w(px(520.0))
1566 .child(caption(
1569 &theme,
1570 "what the toasts said, still here once they timed out",
1571 ))
1572 .child(centre)
1573 .into_any_element()
1574}
1575
1576fn failure_panel(_window: &mut Window, cx: &mut App) -> AnyElement {
1577 let theme = cx.theme().clone();
1578 let failed: Result<(), &str> = Err("The runs service did not respond.");
1582 stack(&theme)
1583 .w(px(560.0))
1584 .child(caption(
1585 &theme,
1586 "the host's own words, kept on screen; the retry belongs to the host",
1587 ))
1588 .children(
1589 FailurePanel::from_result("scene.failure.query", &failed).map(|panel| {
1590 panel
1591 .title("Runs")
1592 .detail("The connection timed out after 30 seconds.")
1593 .attempts(3)
1594 .on_retry(|_, _| {})
1595 }),
1596 )
1597 .into_any_element()
1598}
1599
1600fn log_stream(_window: &mut Window, cx: &mut App) -> AnyElement {
1601 let theme = cx.theme().clone();
1602 let entries = [
1603 LogEntry::new("boot", "Fixture worker entered the queue")
1604 .timestamp("09:41:02")
1605 .level("INFO", Tone::Info)
1606 .source("scheduler"),
1607 LogEntry::new("claim", "Fixture worker claimed run demo-42")
1608 .timestamp("09:41:03")
1609 .level("INFO", Tone::Info)
1610 .source("worker")
1611 .search_hits(std::iter::once(23..26))
1612 .current_hit(0),
1613 LogEntry::new("cache", "Fixture cache was not warm")
1614 .timestamp("09:41:04")
1615 .level("WARN", Tone::Warning)
1616 .source("cache"),
1617 LogEntry::new("retry", "Fixture request completed after one retry")
1618 .timestamp("09:41:05")
1619 .level("INFO", Tone::Info)
1620 .source("worker"),
1621 LogEntry::new("finish", "Fixture run demo-42 completed")
1622 .timestamp("09:41:06")
1623 .level("DONE", Tone::Success)
1624 .source("scheduler")
1625 .search_hits(std::iter::once(12..19)),
1626 ];
1627 stack(&theme)
1628 .w(px(780.0))
1629 .child(caption(
1630 &theme,
1631 "fixed virtual rows; metadata and search ranges are caller inputs",
1632 ))
1633 .child(
1634 LogStream::new("scene.log", entries)
1635 .state(LogStreamState::Stale(
1636 "The latest refresh did not complete; verified entries remain.".into(),
1637 ))
1638 .visible_rows(5)
1639 .selected("retry")
1640 .on_select(|_, _, _| {})
1641 .on_copy(|_, _, _| {}),
1642 )
1643 .into_any_element()
1644}
1645
1646fn scene_diff() -> Vec<DiffFile> {
1647 vec![DiffFile::new(
1648 "report",
1649 "src/report.rs",
1650 [DiffHunk::new(
1651 "summary",
1652 "@@ -40,3 +40,4 @@ fn report",
1653 [
1654 DiffLine::new("signature", "fn report() -> Outcome {")
1655 .old_number(40)
1656 .new_number(40)
1657 .spans([CodeSpan {
1658 range: 0..2,
1659 tone: Tone::Accent,
1660 }]),
1661 DiffLine::paired("cache", " old_cache.read()", " verified_cache.read()")
1662 .old_number(41)
1663 .new_number(41),
1664 DiffLine::added("audit", " audit.record()").new_number(42),
1665 DiffLine::new("close", "}").old_number(42).new_number(43),
1666 ],
1667 )],
1668 )]
1669}
1670
1671fn diff_view(_window: &mut Window, cx: &mut App) -> AnyElement {
1672 let theme = cx.theme().clone();
1673 stack(&theme)
1674 .w(px(840.0))
1675 .child(caption(
1676 &theme,
1677 "the same caller-owned rows arranged as unified and split",
1678 ))
1679 .child(
1680 div()
1681 .column()
1682 .items_start()
1683 .gap_token(&theme, Space::Md)
1684 .child(
1685 div().w(px(840.0)).child(
1686 DiffView::new("scene.diff.unified", scene_diff())
1687 .visible_rows(7)
1688 .on_event(|_, _, _| {}),
1689 ),
1690 )
1691 .child(
1692 div().w(px(840.0)).child(
1693 DiffView::new("scene.diff.split", scene_diff())
1694 .presentation(DiffPresentation::Split)
1695 .visible_rows(7)
1696 .on_event(|_, _, _| {}),
1697 ),
1698 ),
1699 )
1700 .into_any_element()
1701}
1702
1703fn sparkline(_window: &mut Window, cx: &mut App) -> AnyElement {
1704 let theme = cx.theme().clone();
1705 let points = [
1706 SparklinePoint::new(0.0, 0.20),
1707 SparklinePoint::new(0.14, 0.34),
1708 SparklinePoint::new(0.28, 0.29),
1709 SparklinePoint::new(0.42, 0.56),
1710 SparklinePoint::new(0.57, 0.48),
1711 SparklinePoint::new(0.71, 0.74),
1712 SparklinePoint::new(0.85, 0.66),
1713 SparklinePoint::new(1.0, 0.82),
1714 ];
1715 stack(&theme)
1716 .w(px(520.0))
1717 .child(caption(
1718 &theme,
1719 "normalized geometry with caller-formatted current, minimum and maximum",
1720 ))
1721 .child(Sparkline::new(
1722 "scene.sparkline.rate",
1723 "Fixture throughput",
1724 SparklineState::Ready(SparklineReading::new(
1725 points, "82 req/s", "20 req/s", "82 req/s",
1726 )),
1727 ))
1728 .child(Sparkline::new(
1729 "scene.sparkline.stale",
1730 "Fixture queue depth",
1731 SparklineState::Stale {
1732 reading: SparklineReading::new(points, "34 jobs", "8 jobs", "41 jobs"),
1733 reason: "The latest sample is unavailable; the verified reading remains.".into(),
1734 },
1735 ))
1736 .into_any_element()
1737}
1738
1739fn code_view(_window: &mut Window, cx: &mut App) -> AnyElement {
1740 let theme = cx.theme().clone();
1741 let lines = [
1744 CodeLine::new(40, "fn report(&self) -> Outcome {").spans([
1745 CodeSpan {
1746 range: 0..2,
1747 tone: Tone::Accent,
1748 },
1749 CodeSpan {
1750 range: 3..9,
1751 tone: Tone::Success,
1752 },
1753 ]),
1754 CodeLine::new(41, " let verified = self.check();")
1755 .spans([CodeSpan {
1756 range: 4..7,
1757 tone: Tone::Accent,
1758 }])
1759 .mark(LineMark::Added),
1760 CodeLine::new(42, " let stale = self.cached();").mark(LineMark::Removed),
1761 CodeLine::new(43, " Outcome::from(verified)").mark(LineMark::Changed),
1762 CodeLine::new(
1763 44,
1764 " // this line runs off the edge rather than wrapping, because a column carries meaning in code",
1765 ),
1766 CodeLine::new(45, "}").mark(LineMark::Error),
1767 ];
1768 stack(&theme)
1769 .w(px(620.0))
1770 .child(caption(
1771 &theme,
1772 "line numbers are the file's, marks are the host's, colour is the caller's",
1773 ))
1774 .child(CodeView::new("scene.code.report", lines).language("rust"))
1775 .into_any_element()
1776}
1777
1778fn upload_list(_window: &mut Window, cx: &mut App) -> AnyElement {
1779 let theme = cx.theme().clone();
1780 stack(&theme)
1781 .w(px(560.0))
1782 .child(caption(
1783 &theme,
1784 "a refusal is not a failure, and only a failure is offered a retry",
1785 ))
1786 .child(
1787 UploadList::new("scene.uploads")
1788 .dropzone(
1789 Dropzone::new("scene.uploads.zone", "Drop files to attach")
1790 .hint("PDF, PNG, or plain text")
1791 .on_files(|_, _, _| {}),
1792 )
1793 .uploads([
1794 Upload::new("brief", "brief.pdf").size("1.2 MB").done(),
1795 Upload::new("capture", "capture.png")
1796 .size("4.8 MB")
1797 .uploading(0.4),
1798 Upload::new("notes", "notes.txt").size("12 KB"),
1799 Upload::new("archive", "archive.zip")
1800 .size("240 MB")
1801 .failed("The connection dropped."),
1802 Upload::new("installer", "installer.exe")
1803 .size("64 MB")
1804 .refused("This zone does not take programs."),
1805 ])
1806 .on_retry(|_, _, _| {})
1807 .on_cancel(|_, _, _| {})
1808 .on_remove(|_, _, _| {}),
1809 )
1810 .into_any_element()
1811}
1812
1813struct SceneCascader {
1816 cascader: Entity<Cascader>,
1817}
1818
1819impl Global for SceneCascader {}
1820
1821fn cascader(window: &mut Window, cx: &mut App) -> AnyElement {
1822 if !cx.has_global::<SceneCascader>() {
1823 let cascader = cx.new(|cx| {
1824 Cascader::new("scene.cascader", window, cx)
1825 .name("Fixture destination")
1826 .selected("release-notes")
1827 .options([
1828 CascaderOption::new("guides", "Guides").children([
1829 CascaderOption::new("getting-started", "Getting started"),
1830 CascaderOption::new("configuration", "Configuration"),
1831 ]),
1832 CascaderOption::new("reference", "Reference").loading_children(),
1833 CascaderOption::new("archive", "Archive").unavailable_children(
1834 "The fixture host does not provide archived sections.",
1835 ),
1836 CascaderOption::new("release-notes", "Release notes"),
1837 CascaderOption::new("managed", "Managed section").disabled(true),
1838 ])
1839 });
1840 cascader.update(cx, |cascader, cx| cascader.open(window, cx));
1841 cx.set_global(SceneCascader { cascader });
1842 }
1843 let theme = cx.theme().clone();
1844 let cascader = cx.global::<SceneCascader>().cascader.clone();
1845 stack(&theme)
1846 .w(px(680.0))
1847 .child(caption(
1848 &theme,
1849 "caller-owned hierarchy and value; the open path belongs only to the view",
1850 ))
1851 .child(cascader)
1852 .into_any_element()
1853}
1854
1855struct SceneAnchorList {
1856 overflow: Entity<Menu>,
1857}
1858
1859impl Global for SceneAnchorList {}
1860
1861fn anchor_navigation(window: &mut Window, cx: &mut App) -> AnyElement {
1862 if !cx.has_global::<SceneAnchorList>() {
1863 let label = cx.strings().text(StringKey::AnchorMoreSections);
1864 let overflow = cx.new(|cx| Menu::new("scene.anchor-list.menu", window, cx).trigger(label));
1865 cx.set_global(SceneAnchorList { overflow });
1866 }
1867 let theme = cx.theme().clone();
1868 let overflow = cx.global::<SceneAnchorList>().overflow.clone();
1869 stack(&theme)
1870 .w(px(700.0))
1871 .child(caption(
1872 &theme,
1873 "section intents only; the declared overflow moves anchors into a menu",
1874 ))
1875 .child(
1876 AnchorList::new("scene.anchor-list")
1877 .anchors([
1878 Anchor::new("summary", "Summary"),
1879 Anchor::new("inputs", "Inputs"),
1880 Anchor::new("constraints", "Constraints"),
1881 Anchor::new("verification", "Verification"),
1882 Anchor::new("history", "History").disabled(true),
1883 ])
1884 .active("inputs")
1885 .overflow_after(3)
1886 .overflow_menu(overflow)
1887 .on_navigate(|_, _, _| {}),
1888 )
1889 .into_any_element()
1890}
1891
1892fn diagnostics_surface(_window: &mut Window, cx: &mut App) -> AnyElement {
1893 let theme = cx.theme().clone();
1894 let diagnostics = [
1895 Diagnostic::new(
1896 "fixture-error",
1897 DiagnosticSeverity::Error,
1898 DiagnosticLocation::new("fixture.rs:18"),
1899 "Fixture diagnostic: incompatible value",
1900 )
1901 .action(DiagnosticAction::new("inspect", "Inspect fixture")),
1902 Diagnostic::new(
1903 "fixture-warning",
1904 DiagnosticSeverity::Warning,
1905 DiagnosticLocation::new("fixture.rs:31"),
1906 "Fixture diagnostic: unused declaration",
1907 ),
1908 Diagnostic::new(
1909 "fixture-information",
1910 DiagnosticSeverity::Information,
1911 DiagnosticLocation::new("fixture.rs:47"),
1912 "Fixture diagnostic: a simpler form is available",
1913 ),
1914 Diagnostic::new(
1915 "fixture-hint",
1916 DiagnosticSeverity::Hint,
1917 DiagnosticLocation::new("fixture.rs:64"),
1918 "Fixture diagnostic: consider a descriptive name",
1919 ),
1920 ];
1921 stack(&theme)
1922 .w(px(760.0))
1923 .child(caption(
1924 &theme,
1925 "synthetic diagnostics; filters, selection, and actions are caller-owned intents",
1926 ))
1927 .child(
1928 DiagnosticsList::new("scene.diagnostics", Loadable::Ready(diagnostics.to_vec()))
1929 .selected("fixture-warning")
1930 .visible_rows(5)
1931 .on_filter(|_, _, _| {})
1932 .on_select(|_, _, _| {})
1933 .on_action(|_, _, _, _| {}),
1934 )
1935 .into_any_element()
1936}
1937
1938fn accordion(_window: &mut Window, cx: &mut App) -> AnyElement {
1939 let theme = cx.theme().clone();
1940 stack(&theme)
1941 .w(px(520.0))
1942 .child(
1943 Accordion::new("scene.accordion.settings")
1944 .expanded_ids(&["network"])
1945 .on_toggle(|_, _, _, _| {})
1946 .section(
1947 AccordionSection::new("network", "Network")
1948 .description("How this machine reaches a host")
1949 .body(crate::foundation::text(
1950 &theme,
1951 TypeScale::Body,
1952 "Requests go out over the system proxy.",
1953 )),
1954 )
1955 .section(
1956 AccordionSection::new("storage", "Storage")
1957 .description("Where verified results are kept")
1958 .body(crate::foundation::text(
1959 &theme,
1960 TypeScale::Body,
1961 "Nothing is written outside the workspace.",
1962 )),
1963 )
1964 .section(
1965 AccordionSection::new("policy", "Managed by policy")
1966 .description("This machine cannot change these")
1967 .disabled(true)
1968 .body(crate::foundation::text(
1969 &theme,
1970 TypeScale::Body,
1971 "Set by the administrator.",
1972 )),
1973 ),
1974 )
1975 .into_any_element()
1976}
1977
1978fn breadcrumb(_window: &mut Window, cx: &mut App) -> AnyElement {
1979 let theme = cx.theme().clone();
1980 stack(&theme)
1981 .w(px(520.0))
1982 .child(
1983 Breadcrumb::new("scene.breadcrumb.short")
1984 .crumbs([
1985 Crumb::new("workspace", "Workspace"),
1986 Crumb::new("runs", "Runs"),
1987 Crumb::new("run-4821", "Indexing"),
1988 ])
1989 .on_select(|_, _, _| {}),
1990 )
1991 .child(
1992 Breadcrumb::new("scene.breadcrumb.long")
1993 .crumbs([
1994 Crumb::new("workspace", "Workspace"),
1995 Crumb::new("projects", "Projects"),
1996 Crumb::new("gpui-kit", "gpui-kit"),
1997 Crumb::new("runs", "Runs"),
1998 Crumb::new("run-4821", "Indexing"),
1999 ])
2000 .max_visible(3)
2001 .on_select(|_, _, _| {})
2002 .on_reveal(|_, _, _| {}),
2003 )
2004 .into_any_element()
2005}
2006
2007const FIXTURE_RECORDS: usize = 240;
2012
2013fn fixture_record(index: usize) -> (SharedString, SharedString) {
2016 (
2017 SharedString::from(format!("record-{index:04}")),
2018 SharedString::from(format!("Fixture record {index:04}")),
2019 )
2020}
2021
2022fn list(_window: &mut Window, cx: &mut App) -> AnyElement {
2023 let theme = cx.theme().clone();
2024 stack(&theme)
2025 .w(px(420.0))
2026 .child(
2027 crate::foundation::text(
2028 &theme,
2029 TypeScale::Caption,
2030 SharedString::from(format!(
2031 "{FIXTURE_RECORDS} fixture records; only the rendered ones publish"
2032 )),
2033 )
2034 .text_tone(&theme, TextTone::Muted),
2035 )
2036 .child(
2037 div()
2038 .hairline(&theme)
2039 .radius(&theme, Radius::Card)
2040 .overflow_hidden()
2041 .child(
2042 List::new("scene.list.records", FIXTURE_RECORDS, |index, _, _| {
2043 let (id, label) = fixture_record(index);
2044 ListItem::new(id, label.clone()).text(label)
2045 })
2046 .selected(fixture_record(2).0)
2047 .visible_rows(8)
2048 .on_select(|_, _, _| {}),
2049 ),
2050 )
2051 .into_any_element()
2052}
2053
2054fn table(_window: &mut Window, cx: &mut App) -> AnyElement {
2055 let theme = cx.theme().clone();
2056 let state = |label: &'static str, tone: Tone| {
2057 Cell::new(Badge::new(label).tone(tone))
2058 .text(label)
2059 .published(true)
2060 };
2061 stack(&theme)
2062 .w(px(600.0))
2063 .child(
2064 Table::new("scene.table.runs")
2065 .columns([
2066 Column::new("name", "Run").flex(2.0).sortable(true),
2067 Column::new("state", "State").fixed(110.0),
2068 Column::new("duration", "Duration")
2069 .fixed(96.0)
2070 .align(Align::End)
2071 .sortable(true),
2072 ])
2073 .sorted_by("duration", SortDirection::Descending)
2074 .selected("run-b12")
2075 .rows([
2076 Row::new("run-a04")
2077 .text("Indexing")
2078 .cell("name", "Indexing")
2079 .cell("state", state("Ready", Tone::Success))
2080 .cell("duration", "4m 12s"),
2081 Row::new("run-b12")
2082 .text("Verifying")
2083 .cell("name", "Verifying")
2084 .cell("state", state("Stale", Tone::Warning))
2085 .cell("duration", "2m 08s"),
2086 Row::new("run-c31")
2087 .text("Publishing")
2088 .cell("name", "Publishing")
2089 .cell("state", state("Refused", Tone::Danger))
2090 .cell("duration", "1m 44s"),
2091 Row::new("run-d02")
2092 .text("Archiving")
2093 .disabled(true)
2094 .cell("name", "Archiving")
2095 .cell("state", state("Managed", Tone::Neutral))
2096 .cell("duration", "0m 51s"),
2097 ])
2098 .visible_rows(6)
2099 .on_sort(|_, _, _, _| {})
2100 .on_select(|_, _, _| {}),
2101 )
2102 .into_any_element()
2103}
2104
2105const FIXTURE_JOBS_LOADED: usize = 240;
2109const FIXTURE_JOBS_TOTAL: usize = 12_000;
2110
2111fn fixture_job(index: usize) -> (SharedString, SharedString, SharedString, SharedString) {
2114 const PHASES: [&str; 4] = ["Indexing", "Verifying", "Publishing", "Archiving"];
2115 const OWNERS: [&str; 3] = ["fixture-a", "fixture-b", "fixture-c"];
2116 (
2117 SharedString::from(format!("job-{index:04}")),
2118 SharedString::from(format!("{} {index:04}", PHASES[index % PHASES.len()])),
2119 SharedString::from(OWNERS[index % OWNERS.len()]),
2120 SharedString::from(format!("{}m {:02}s", index % 9 + 1, index * 7 % 60)),
2121 )
2122}
2123
2124fn fixture_job_tone(index: usize) -> (&'static str, Tone) {
2125 match index % 4 {
2126 0 => ("Ready", Tone::Success),
2127 1 => ("Stale", Tone::Warning),
2128 2 => ("Refused", Tone::Danger),
2129 _ => ("Managed", Tone::Neutral),
2130 }
2131}
2132
2133fn grid_columns() -> [GridColumn; 4] {
2134 [
2135 GridColumn::new("owner", "Owner")
2138 .fixed(120.0)
2139 .reorderable(true)
2140 .editable(true),
2141 GridColumn::new("name", "Job")
2142 .flex(2.0)
2143 .min_width(140.0)
2144 .pinned(true)
2145 .sortable(true)
2146 .resizable(true),
2147 GridColumn::new("state", "State")
2148 .fixed(110.0)
2149 .reorderable(true),
2150 GridColumn::new("duration", "Duration")
2151 .fixed(104.0)
2152 .align(Align::End)
2153 .sortable(true)
2154 .resizable(true)
2155 .reorderable(true),
2156 ]
2157}
2158
2159fn grid_row(index: usize) -> GridRow {
2160 let (id, name, owner, duration) = fixture_job(index);
2161 let (label, tone) = fixture_job_tone(index);
2162 GridRow::new(id)
2163 .text(name.clone())
2164 .cell("name", Cell::new(name.clone()).text(name).published(true))
2165 .cell("owner", Cell::new(owner.clone()).text(owner))
2166 .cell(
2167 "state",
2168 Cell::new(Badge::new(label).tone(tone))
2169 .text(label)
2170 .published(true),
2171 )
2172 .cell("duration", duration)
2173}
2174
2175fn grid_detail(theme: &Theme, id: SharedString) -> AnyElement {
2176 div()
2177 .column()
2178 .gap(px(theme.spacing.xs))
2179 .child(
2180 crate::foundation::text(
2181 theme,
2182 TypeScale::Caption,
2183 SharedString::from(format!("Fixture detail for {id}")),
2184 )
2185 .text_tone(theme, TextTone::Muted),
2186 )
2187 .child(
2188 crate::foundation::text(
2189 theme,
2190 TypeScale::Body,
2191 SharedString::from(
2192 "Only an opened row builds this region; the rest never ask for it.",
2193 ),
2194 )
2195 .text_tone(theme, TextTone::Muted),
2196 )
2197 .into_any_element()
2198}
2199
2200fn data_grid(_window: &mut Window, cx: &mut App) -> AnyElement {
2201 let theme = cx.theme().clone();
2202 let detail_theme = theme.clone();
2203 stack(&theme)
2204 .w(px(760.0))
2205 .child(
2206 BulkBar::new("scene.data-grid.bulk", 2)
2207 .total(FIXTURE_JOBS_TOTAL)
2208 .action(
2209 Button::new("scene.data-grid.bulk.retry")
2210 .label("Retry")
2211 .secondary()
2212 .small()
2213 .on_click(|_, _| {}),
2214 )
2215 .action(
2216 Button::new("scene.data-grid.bulk.archive")
2217 .label("Archive")
2218 .secondary()
2219 .small()
2220 .on_click(|_, _| {}),
2221 )
2222 .on_select_all(|_, _| {})
2223 .on_dismiss(|_, _| {}),
2224 )
2225 .child(
2226 DataGrid::new(
2227 "scene.data-grid.jobs",
2228 FIXTURE_JOBS_LOADED,
2229 |index, _, _| grid_row(index),
2230 )
2231 .total(FIXTURE_JOBS_TOTAL)
2232 .columns(grid_columns())
2233 .sorted_by("duration", SortDirection::Descending)
2234 .selection_mode(SelectionMode::Multiple)
2235 .selected(["job-0001", "job-0003"])
2236 .expanded([Expanded::new("job-0002", 2)])
2237 .detail_rows(2)
2238 .detail(move |id, _, _| grid_detail(&detail_theme, id))
2239 .visible_rows(9)
2240 .on_sort(|_, _, _, _| {})
2241 .on_select(|_, _, _| {})
2242 .on_resize(|_, _, _, _| {})
2243 .on_fit(|_, _, _| {})
2244 .on_reorder(|_, _, _| {})
2245 .on_expand(|_, _, _, _| {})
2246 .on_edit_request(|_, _, _, _| {})
2247 .on_edit(|_, _, _| {}),
2248 )
2249 .child(
2250 crate::foundation::text(
2251 &theme,
2252 TypeScale::Caption,
2253 SharedString::from(format!(
2254 "{FIXTURE_JOBS_LOADED} rows loaded of {FIXTURE_JOBS_TOTAL}; only the drawn \
2255 ones publish"
2256 )),
2257 )
2258 .text_tone(&theme, TextTone::Muted),
2259 )
2260 .into_any_element()
2261}
2262
2263fn data_grid_editing(_window: &mut Window, cx: &mut App) -> AnyElement {
2264 let theme = cx.theme().clone();
2265 stack(&theme)
2266 .w(px(760.0))
2267 .child(
2268 DataGrid::new("scene.data-grid-editing.jobs", 6, |index, _, _| {
2269 grid_row(index)
2270 })
2271 .columns(grid_columns())
2272 .sorted_by("duration", SortDirection::Descending)
2273 .selection_mode(SelectionMode::Single)
2274 .selected(["job-0001"])
2275 .editing(Some(EditingCell::new("job-0001", "owner", "fixture-b")))
2276 .visible_rows(6)
2277 .on_sort(|_, _, _, _| {})
2278 .on_select(|_, _, _| {})
2279 .on_resize(|_, _, _, _| {})
2280 .on_edit_request(|_, _, _, _| {})
2281 .on_edit(|_, _, _| {}),
2282 )
2283 .child(
2284 crate::foundation::text(
2285 &theme,
2286 TypeScale::Caption,
2287 SharedString::from(
2288 "Escape reverts, enter commits, tab commits and moves on. The grid never \
2289 writes the value.",
2290 ),
2291 )
2292 .text_tone(&theme, TextTone::Muted),
2293 )
2294 .into_any_element()
2295}
2296
2297fn tree_grid(_window: &mut Window, cx: &mut App) -> AnyElement {
2298 let theme = cx.theme().clone();
2299 let rows = [
2300 ("workspace", "Workspace", 1, true, true, None),
2301 ("src", "src", 2, true, true, Some("workspace")),
2302 ("components", "components", 3, false, false, Some("src")),
2303 ("lib", "lib.rs", 3, false, false, Some("src")),
2304 ("docs", "docs", 2, true, false, Some("workspace")),
2305 ];
2306 stack(&theme)
2307 .w(px(720.0))
2308 .child(
2309 TreeGrid::new("scene.tree-grid.files", rows.len(), move |index, _, _| {
2310 let (id, name, level, branch, expanded, parent) = rows[index];
2311 let mut row = TreeGridRow::new(id, level)
2312 .text(name)
2313 .cell("name", Cell::new(name).text(name).published(true))
2314 .cell("kind", if branch { "Folder" } else { "File" })
2315 .cell("state", if expanded { "Expanded" } else { "Ready" });
2316 if branch {
2317 row = row.branch(expanded);
2318 }
2319 if let Some(parent) = parent {
2320 row = row.parent(parent);
2321 }
2322 row
2323 })
2324 .columns([
2325 GridColumn::new("name", "Name").flex(2.0),
2326 GridColumn::new("kind", "Kind").fixed(120.0),
2327 GridColumn::new("state", "State").flex(1.0),
2328 ])
2329 .selected("components")
2330 .visible_rows(5)
2331 .lines(GridLines::Rows)
2332 .on_select(|_, _, _| {})
2333 .on_expand(|_, _, _, _| {}),
2334 )
2335 .into_any_element()
2336}
2337
2338fn tree(_window: &mut Window, cx: &mut App) -> AnyElement {
2339 let theme = cx.theme().clone();
2340 stack(&theme)
2341 .w(px(360.0))
2342 .child(
2343 Tree::new("scene.tree.workspace")
2344 .expanded_ids(&["workspace", "crates"])
2345 .selected("tokens")
2346 .nodes([
2347 TreeNode::new("workspace", "workspace")
2348 .icon(Icon::Folder)
2349 .children([
2350 TreeNode::new("crates", "crates")
2351 .icon(Icon::Folder)
2352 .children([
2353 TreeNode::new("kit", "gpui-kit").icon(Icon::Document),
2354 TreeNode::new("tokens", "gpui-kit-tokens").icon(Icon::Document),
2355 ]),
2356 TreeNode::new("docs", "docs")
2357 .icon(Icon::Folder)
2358 .children([TreeNode::new("components", "components.md")
2359 .icon(Icon::Document)]),
2360 ]),
2361 TreeNode::new("target", "target")
2362 .icon(Icon::Archive)
2363 .disabled(true)
2364 .children([TreeNode::new("debug", "debug").icon(Icon::Folder)]),
2365 ])
2366 .on_toggle(|_, _, _, _| {})
2367 .on_select(|_, _, _| {}),
2368 )
2369 .into_any_element()
2370}
2371
2372fn reading_direction(_window: &mut Window, cx: &mut App) -> AnyElement {
2383 let theme = cx.theme().clone();
2384 let direction = cx.layout_direction();
2385 stack(&theme)
2386 .w(px(560.0))
2387 .child(
2388 Breadcrumb::new("scene.rtl.trail")
2389 .crumbs([
2390 Crumb::new("workspace", "Workspace"),
2391 Crumb::new("runs", "Runs"),
2392 Crumb::new("run-4821", "Indexing"),
2393 ])
2394 .on_select(|_, _, _| {}),
2395 )
2396 .child(
2397 Tabs::new("scene.rtl.tabs")
2398 .tabs([
2399 TabItem::new("overview", "Overview").icon(Icon::Widget),
2400 TabItem::new("runs", "Runs").badge("12"),
2401 TabItem::new("logs", "Logs"),
2402 ])
2403 .selected("runs")
2404 .on_select(|_, _, _| {}),
2405 )
2406 .child(
2407 div()
2408 .row_reading(direction)
2409 .gap(px(theme.space(Space::Md)))
2410 .items_start()
2411 .child(
2412 div().w(px(240.0)).child(
2413 Tree::new("scene.rtl.tree")
2414 .expanded_ids(&["workspace"])
2415 .selected("tokens")
2416 .nodes([TreeNode::new("workspace", "workspace")
2417 .icon(Icon::Folder)
2418 .children([
2419 TreeNode::new("crates", "crates")
2420 .icon(Icon::Folder)
2421 .children([
2422 TreeNode::new("kit", "gpui-kit").icon(Icon::Document)
2423 ]),
2424 TreeNode::new("tokens", "tokens").icon(Icon::Document),
2425 ])])
2426 .on_toggle(|_, _, _, _| {})
2427 .on_select(|_, _, _| {}),
2428 ),
2429 )
2430 .child(
2431 div().flex_1().child(
2432 Accordion::new("scene.rtl.sections")
2433 .expanded_ids(&["network"])
2434 .on_toggle(|_, _, _, _| {})
2435 .section(
2436 AccordionSection::new("network", "Network")
2437 .description("How this machine reaches a host")
2438 .body(crate::foundation::text(
2439 &theme,
2440 TypeScale::Body,
2441 "Requests go out over the system proxy.",
2442 )),
2443 )
2444 .section(
2445 AccordionSection::new("storage", "Storage")
2446 .description("Where verified results are kept"),
2447 ),
2448 ),
2449 ),
2450 )
2451 .child(
2452 div()
2453 .row_reading(direction)
2454 .gap(px(theme.space(Space::Md)))
2455 .child(IconView::new(Icon::Magnifier).large().muted())
2458 .child(IconView::new(Icon::AltArrowRight).large().muted())
2459 .child(IconView::new(Icon::Return).large().muted())
2460 .child(IconView::new(Icon::Check).large().tone(IconTone::Success))
2461 .child(IconView::new(Icon::Settings).large().muted())
2462 .child(IconView::new(Icon::AltArrowDown).large().muted())
2463 .child(
2464 IconView::named("scene.rtl.alone", Icon::Danger, "Run failed")
2465 .large()
2466 .tone(IconTone::Danger),
2467 ),
2468 )
2469 .child(
2470 div()
2471 .row_reading(direction)
2472 .gap(px(theme.space(Space::Sm)))
2473 .child(
2474 Button::new("scene.rtl.save")
2475 .label("Save")
2476 .primary()
2477 .icon(Icon::Check)
2478 .on_click(|_, _| {}),
2479 )
2480 .child(
2481 Button::new("scene.rtl.cancel")
2482 .label("Cancel")
2483 .secondary()
2484 .on_click(|_, _| {}),
2485 ),
2486 )
2487 .into_any_element()
2488}
2489
2490fn choice(_window: &mut Window, cx: &mut App) -> AnyElement {
2491 let theme = cx.theme().clone();
2492 div()
2493 .flex()
2494 .flex_col()
2495 .gap(px(theme.space(Space::Md)))
2496 .p(px(theme.space(Space::Lg)))
2497 .w(px(360.0))
2498 .child(
2499 Checkbox::new("scene.choice.telemetry")
2500 .label("Send anonymous usage data")
2501 .description("Counts only, never file contents")
2502 .checked(true)
2503 .on_change(|_, _, _| {}),
2504 )
2505 .child(
2506 Checkbox::new("scene.choice.partial")
2507 .label("Some providers enabled")
2508 .mixed()
2509 .on_change(|_, _, _| {}),
2510 )
2511 .child(
2512 Checkbox::new("scene.choice.locked")
2513 .label("Managed by policy")
2514 .checked(true)
2515 .disabled(true),
2516 )
2517 .child(
2518 Radio::new("scene.choice.ask")
2519 .label("Ask before every action")
2520 .selected(true)
2521 .on_select(|_, _| {}),
2522 )
2523 .child(
2524 Radio::new("scene.choice.auto")
2525 .label("Run without asking")
2526 .description("Consequential actions still require approval")
2527 .on_select(|_, _| {}),
2528 )
2529 .child(
2530 Switch::new("scene.choice.preview")
2531 .label("Preview releases")
2532 .on(true)
2533 .on_change(|_, _, _| {}),
2534 )
2535 .child(
2536 Slider::new("scene.choice.temperature")
2537 .label("Temperature")
2538 .range(0.0, 2.0)
2539 .step(0.1)
2540 .value(0.7)
2541 .display("0.7")
2542 .on_change(|_, _, _| {}),
2543 )
2544 .into_any_element()
2545}
2546
2547struct SceneForm {
2553 name: Entity<TextInput>,
2554 retention: Entity<NumberInput>,
2555 region: Entity<Combobox>,
2556 labels: Entity<TagInput>,
2557}
2558
2559impl Global for SceneForm {}
2560
2561fn ensure_form(window: &mut Window, cx: &mut App) {
2562 if cx.has_global::<SceneForm>() {
2563 return;
2564 }
2565 let name = cx.new(|cx| {
2566 TextInput::new("scene.form.name", window, cx)
2567 .text("Runs 2024")
2568 .required(true)
2569 .invalid(true)
2570 });
2571 let retention = cx.new(|cx| {
2572 NumberInput::new("scene.form.retention", window, cx)
2576 .value(90.0)
2577 .range(1.0, 60.0)
2578 .step(5.0)
2579 .unit("days")
2580 .required(true)
2581 });
2582 let region = cx.new(|cx| {
2583 let mut options = (0..14)
2584 .map(|index| {
2585 SelectOption::new(
2586 format!("model-{index:02}"),
2587 format!("Agent model {index:02}"),
2588 )
2589 })
2590 .collect::<Vec<_>>();
2591 options.push(SelectOption::new("unknown", "Unknown model").description(
2592 "This model may not support chat in direct mode.\nChoose a chat-capable model.",
2593 ));
2594 options.push(SelectOption::new("managed", "Managed model").disabled(true));
2595 Combobox::new("scene.form.region", window, cx)
2596 .name("All Agent models")
2597 .options(options)
2598 .selected("unknown")
2599 .placeholder("Choose an Agent model")
2600 });
2601 let labels = cx.new(|cx| {
2602 TagInput::new("scene.form.labels", window, cx)
2603 .tags(["indexing", "nightly", "verified"])
2604 .placeholder("Add a label")
2605 .max(5)
2606 });
2607 region.update(cx, |combobox, cx| {
2608 combobox.set_query("Unknown model", cx);
2609 });
2610 cx.set_global(SceneForm {
2611 name,
2612 retention,
2613 region,
2614 labels,
2615 });
2616}
2617
2618fn form(window: &mut Window, cx: &mut App) -> AnyElement {
2619 ensure_form(window, cx);
2620 let form = cx.global::<SceneForm>();
2621 let (name, retention, region, labels) = (
2622 form.name.clone(),
2623 form.retention.clone(),
2624 form.region.clone(),
2625 form.labels.clone(),
2626 );
2627 let theme = cx.theme().clone();
2628
2629 stack(&theme)
2630 .w(px(420.0))
2631 .h(px(920.0))
2632 .child(
2633 FormField::new("scene.form.name.form-field", "Workspace name")
2634 .control("scene.form.name")
2635 .required(true)
2636 .description("Shown wherever this workspace appears.")
2639 .error("A workspace with this name already exists.")
2640 .child(name),
2641 )
2642 .child(
2643 FormField::new("scene.form.retention.form-field", "Retention")
2644 .control("scene.form.retention")
2645 .required(true)
2646 .description("How long a finished run is kept.")
2647 .error("This workspace allows at most 60 days.")
2648 .child(retention),
2649 )
2650 .child(
2651 FormField::new("scene.form.visibility.form-field", "Visibility")
2652 .control("scene.form.visibility")
2653 .description("Who can open the runs in this workspace.")
2654 .child(
2655 SegmentedControl::new("scene.form.visibility")
2656 .label("Visibility")
2657 .segments([
2658 Segment::new("private", "Private"),
2659 Segment::new("team", "Team"),
2660 Segment::new("public", "Public").disabled(true),
2661 ])
2662 .selected("team")
2663 .on_select(|_, _, _| {}),
2664 ),
2665 )
2666 .child(
2667 FormField::new("scene.form.labels.form-field", "Labels")
2668 .control("scene.form.labels")
2669 .description("At most five, and each one only once.")
2672 .hint("enter")
2673 .child(labels),
2674 )
2675 .child(
2676 div().mt_auto().child(
2677 FormField::new("scene.form.region.form-field", "All Agent models")
2678 .control("scene.form.region")
2679 .description("Choose a chat-capable model for direct runs.")
2680 .child(region),
2681 ),
2682 )
2683 .into_any_element()
2684}
2685
2686struct SceneAuthSignIn {
2688 password: Entity<PasswordInput>,
2689}
2690
2691impl Global for SceneAuthSignIn {}
2692
2693fn ensure_auth_sign_in(window: &mut Window, cx: &mut App) {
2694 if cx.has_global::<SceneAuthSignIn>() {
2695 return;
2696 }
2697 let password = cx.new(|cx| {
2698 PasswordInput::new("scene.auth.sign-in.password", window, cx)
2699 .name("Password")
2700 .placeholder("Enter password")
2701 .required(true)
2702 });
2703 cx.set_global(SceneAuthSignIn { password });
2704}
2705
2706fn auth_sign_in(window: &mut Window, cx: &mut App) -> AnyElement {
2707 ensure_auth_sign_in(window, cx);
2708 let password = cx.global::<SceneAuthSignIn>().password.clone();
2709 let theme = cx.theme().clone();
2710 let card_id = "scene.auth.sign-in.card";
2711 let title = crate::foundation::text(&theme, TypeScale::Title, "Sign in").semantic_in(
2712 cx,
2713 NodeSpec::new("scene.auth.sign-in.title", Role::Text)
2714 .parent(card_id)
2715 .text("Sign in"),
2716 );
2717
2718 stack(&theme)
2719 .w(px(440.0))
2720 .child(
2721 Card::new().id(card_id).padded(true).child(
2722 div()
2723 .column()
2724 .gap_token(&theme, Space::Md)
2725 .child(title)
2726 .child(
2727 Callout::new("Credentials are verified by the caller.", Tone::Neutral)
2728 .id("scene.auth.sign-in.boundary"),
2729 )
2730 .child(
2731 FormField::new("scene.auth.sign-in.password.field", "Password")
2732 .control("scene.auth.sign-in.password")
2733 .required(true)
2734 .child(password),
2735 )
2736 .child(
2737 Button::new("scene.auth.sign-in.submit")
2738 .label("Sign in")
2739 .full_width(true)
2740 .on_click(|_, _| {}),
2741 )
2742 .child(
2743 Button::new("scene.auth.sign-in.passkey")
2744 .label("Continue with passkey")
2745 .icon(Icon::Key)
2746 .secondary()
2747 .full_width(true)
2748 .on_click(|_, _| {}),
2749 )
2750 .child(
2751 Button::new("scene.auth.sign-in.organization")
2752 .label("Continue with organization sign-on")
2753 .icon(Icon::Global)
2754 .secondary()
2755 .full_width(true)
2756 .on_click(|_, _| {}),
2757 )
2758 .child(
2759 Button::new("scene.auth.sign-in.recovery")
2760 .label("Use a recovery option")
2761 .link()
2762 .on_click(|_, _| {}),
2763 ),
2764 ),
2765 )
2766 .into_any_element()
2767}
2768
2769struct SceneAuthVerification {
2771 code: Entity<OneTimeCodeInput>,
2772}
2773
2774impl Global for SceneAuthVerification {}
2775
2776fn ensure_auth_verification(window: &mut Window, cx: &mut App) {
2777 if cx.has_global::<SceneAuthVerification>() {
2778 return;
2779 }
2780 let code = cx.new(|cx| {
2781 OneTimeCodeInput::new("scene.auth.verification.code", window, cx)
2782 .name("Verification code")
2783 .slots(6)
2784 .required(true)
2785 });
2786 cx.set_global(SceneAuthVerification { code });
2787}
2788
2789fn auth_verification(window: &mut Window, cx: &mut App) -> AnyElement {
2790 ensure_auth_verification(window, cx);
2791 let code = cx.global::<SceneAuthVerification>().code.clone();
2792 let theme = cx.theme().clone();
2793 let card_id = "scene.auth.verification.card";
2794 let title = crate::foundation::text(&theme, TypeScale::Title, "Verify sign-in").semantic_in(
2795 cx,
2796 NodeSpec::new("scene.auth.verification.title", Role::Text)
2797 .parent(card_id)
2798 .text("Verify sign-in"),
2799 );
2800
2801 stack(&theme)
2802 .w(px(440.0))
2803 .child(
2804 Card::new().id(card_id).padded(true).child(
2805 div()
2806 .column()
2807 .gap_token(&theme, Space::Md)
2808 .child(title)
2809 .child(
2810 Callout::new(
2811 "Enter the code from your authenticator or recovery method.",
2812 Tone::Neutral,
2813 )
2814 .id("scene.auth.verification.guidance"),
2815 )
2816 .child(
2817 FormField::new("scene.auth.verification.code.field", "Verification code")
2818 .control("scene.auth.verification.code")
2819 .required(true)
2820 .child(code),
2821 )
2822 .child(
2823 Button::new("scene.auth.verification.submit")
2824 .label("Verify")
2825 .full_width(true)
2826 .on_click(|_, _| {}),
2827 )
2828 .child(
2829 Button::new("scene.auth.verification.alternative")
2830 .label("Use another method")
2831 .secondary()
2832 .full_width(true)
2833 .on_click(|_, _| {}),
2834 )
2835 .child(
2836 Button::new("scene.auth.verification.recovery")
2837 .label("Use a recovery option")
2838 .link()
2839 .on_click(|_, _| {}),
2840 ),
2841 ),
2842 )
2843 .into_any_element()
2844}
2845
2846struct SceneActions {
2848 split: Entity<SplitButton>,
2849}
2850
2851impl Global for SceneActions {}
2852
2853fn ensure_actions(window: &mut Window, cx: &mut App) {
2854 if cx.has_global::<SceneActions>() {
2855 return;
2856 }
2857 let split = cx.new(|cx| {
2858 SplitButton::new("scene.actions.publish", window, cx)
2859 .label("Publish")
2860 .primary()
2861 .on_click(|_, _| {})
2862 .items(
2863 [
2864 MenuItem::command("publish.draft", "Save as draft"),
2865 MenuItem::command("publish.schedule", "Schedule…").shortcut("cmd-shift-s"),
2866 MenuItem::separator("publish.rule"),
2867 MenuItem::command("publish.export", "Export without publishing"),
2868 ],
2869 cx,
2870 )
2871 });
2872 split.update(cx, |split, cx| split.open_menu(window, cx));
2873 cx.set_global(SceneActions { split });
2874}
2875
2876fn actions(window: &mut Window, cx: &mut App) -> AnyElement {
2877 ensure_actions(window, cx);
2878 let split = cx.global::<SceneActions>().split.clone();
2879 let theme = cx.theme().clone();
2880
2881 stack(&theme)
2882 .w(px(560.0))
2883 .h(px(360.0))
2884 .child(
2885 row(&theme)
2886 .child(
2887 IconButton::new("scene.actions.copy", Icon::Copy, "Copy run id")
2888 .on_click(|_, _| {}),
2889 )
2890 .child(
2891 IconButton::new("scene.actions.rename", Icon::Pen, "Rename run")
2892 .secondary()
2893 .on_click(|_, _| {}),
2894 )
2895 .child(
2896 IconButton::new("scene.actions.refresh", Icon::Refresh, "Refresh")
2897 .secondary()
2898 .loading(true)
2899 .on_click(|_, _| {}),
2900 )
2901 .child(
2902 IconButton::new("scene.actions.delete", Icon::Trash, "Delete run")
2903 .danger()
2904 .on_click(|_, _| {}),
2905 )
2906 .child(
2907 IconButton::new("scene.actions.archive", Icon::Archive, "Archive run")
2908 .secondary()
2909 .disabled(true)
2910 .on_click(|_, _| {}),
2911 ),
2912 )
2913 .child(
2914 row(&theme).child(
2915 ButtonGroup::new("scene.actions.range")
2916 .children([
2917 Button::new("scene.actions.range.day")
2918 .label("Day")
2919 .secondary()
2920 .on_click(|_, _| {}),
2921 Button::new("scene.actions.range.week")
2922 .label("Week")
2923 .secondary()
2924 .selected(true)
2925 .on_click(|_, _| {}),
2926 Button::new("scene.actions.range.month")
2927 .label("Month")
2928 .secondary()
2929 .on_click(|_, _| {}),
2930 ])
2931 .small(),
2932 ),
2933 )
2934 .child(row(&theme).child(split))
2935 .into_any_element()
2936}
2937
2938fn content(_window: &mut Window, cx: &mut App) -> AnyElement {
2939 let theme = cx.theme().clone();
2940 div()
2941 .flex()
2942 .flex_col()
2943 .gap(px(theme.space(Space::Lg)))
2944 .p(px(theme.space(Space::Lg)))
2945 .w(px(420.0))
2946 .child(
2947 ProgressBar::new("scene.content.upload")
2948 .label("Indexing workspace")
2949 .count(3, 12),
2950 )
2951 .child(ProgressBar::new("scene.content.unknown").label("Contacting host"))
2952 .child(Divider::new().id("scene.content.rule").label("Filters"))
2953 .child(
2954 div()
2955 .flex()
2956 .flex_row()
2957 .flex_wrap()
2958 .gap(px(theme.space(Space::Xs)))
2959 .child(Tag::new("scene.content.tag.rust", "rust").on_remove(|_, _| {}))
2960 .child(
2961 Tag::new("scene.content.tag.failing", "failing")
2962 .tone(Tone::Danger)
2963 .on_remove(|_, _| {}),
2964 )
2965 .child(Tag::new("scene.content.tag.pinned", "pinned").disabled(true)),
2966 )
2967 .child(
2968 div()
2969 .flex()
2970 .flex_row()
2971 .items_center()
2972 .gap(px(theme.space(Space::Sm)))
2973 .child(Avatar::new("Ada Lovelace").id("scene.content.avatar"))
2974 .child(Avatar::new("").size(24.0)),
2975 )
2976 .child(
2977 EmptyState::new("scene.content.empty", "No runs yet")
2978 .kind(EmptyKind::Unstarted)
2979 .detail("A run appears here once one has been started."),
2980 )
2981 .child(
2982 EmptyState::new("scene.content.refused", "The host refused the request")
2983 .kind(EmptyKind::Unavailable)
2984 .detail("Approval is required for this workspace.")
2985 .action(
2986 Button::new("scene.content.retry")
2987 .label("Try again")
2988 .secondary()
2989 .on_click(|_, _| {}),
2990 ),
2991 )
2992 .into_any_element()
2993}
2994
2995struct SceneInputs {
3000 token: Entity<TextInput>,
3001 disabled: Entity<TextInput>,
3002 invalid: Entity<TextInput>,
3003 provider: Entity<Select>,
3004 notes: Entity<TextArea>,
3005 review: Entity<TextArea>,
3006 frozen: Entity<TextArea>,
3007}
3008
3009impl Global for SceneInputs {}
3010
3011fn ensure_inputs(window: &mut Window, cx: &mut App) {
3012 if !cx.has_global::<SceneInputs>() {
3013 let inputs = SceneInputs {
3014 token: cx.new(|cx| {
3015 TextInput::new("scene.input.token", window, cx)
3016 .name("API token")
3017 .placeholder("sk-...")
3018 .secret(true)
3019 }),
3020 disabled: cx.new(|cx| {
3021 TextInput::new("scene.input.disabled", window, cx)
3022 .name("Disabled")
3023 .text("read only")
3024 .disabled(true)
3025 }),
3026 invalid: cx.new(|cx| {
3027 TextInput::new("scene.input.invalid", window, cx)
3028 .name("Email")
3029 .text("not an email")
3030 .invalid(true)
3031 .required(true)
3032 }),
3033 provider: cx.new(|cx| {
3034 Select::new("scene.input.provider", window, cx)
3035 .name("Provider")
3036 .options([
3037 SelectOption::new("anthropic", "Anthropic"),
3038 SelectOption::new("openai", "OpenAI").description("Requires a key"),
3039 SelectOption::new("local", "Local runtime").disabled(true),
3040 ])
3041 .selected("anthropic")
3042 .placeholder("Choose a provider")
3043 }),
3044 notes: cx.new(|cx| {
3045 TextArea::new("scene.textarea.notes", window, cx)
3046 .text(
3047 "The refusal is shown exactly as the host worded it, and the last \
3048 verified value stays on screen.",
3049 )
3050 .rows(3)
3051 .max_rows(6)
3052 }),
3053 review: cx.new(|cx| {
3054 TextArea::new("scene.textarea.review", window, cx)
3055 .placeholder("What changed, and why")
3056 .rows(3)
3057 }),
3058 frozen: cx.new(|cx| {
3059 TextArea::new("scene.textarea.frozen", window, cx)
3060 .text("Set by the administrator.\nThis machine cannot change it.")
3061 .rows(2)
3062 .disabled(true)
3063 }),
3064 };
3065 window.focus(&inputs.review.read(cx).focus_handle(cx), cx);
3068 cx.set_global(inputs);
3069 }
3070}
3071
3072fn input(window: &mut Window, cx: &mut App) -> AnyElement {
3073 ensure_inputs(window, cx);
3074 let inputs = cx.global::<SceneInputs>();
3075 let (token, disabled, invalid, provider) = (
3076 inputs.token.clone(),
3077 inputs.disabled.clone(),
3078 inputs.invalid.clone(),
3079 inputs.provider.clone(),
3080 );
3081 let theme = cx.theme().clone();
3082
3083 div()
3084 .flex()
3085 .flex_col()
3086 .gap(px(theme.space(Space::Md)))
3087 .p(px(theme.space(Space::Lg)))
3088 .w(px(360.0))
3089 .child(token)
3090 .child(disabled)
3091 .child(invalid)
3092 .child(provider)
3093 .into_any_element()
3094}
3095
3096fn textarea(window: &mut Window, cx: &mut App) -> AnyElement {
3097 ensure_inputs(window, cx);
3098 let inputs = cx.global::<SceneInputs>();
3099 let (notes, review, frozen) = (
3100 inputs.notes.clone(),
3101 inputs.review.clone(),
3102 inputs.frozen.clone(),
3103 );
3104 let theme = cx.theme().clone();
3105
3106 div()
3107 .flex()
3108 .flex_col()
3109 .gap(px(theme.space(Space::Md)))
3110 .p(px(theme.space(Space::Lg)))
3111 .w(px(360.0))
3112 .child(notes)
3113 .child(review)
3114 .child(frozen)
3115 .into_any_element()
3116}
3117
3118fn filler(theme: &Theme, title: &'static str, lines: usize) -> gpui::Div {
3121 let mut pane = div()
3122 .flex()
3123 .flex_col()
3124 .gap(px(theme.space(Space::Xs)))
3125 .p(px(theme.space(Space::Md)))
3126 .child(crate::foundation::text(
3127 theme,
3128 TypeScale::Label,
3129 SharedString::from(title),
3130 ));
3131 for line in 1..=lines {
3132 pane = pane.child(
3133 crate::foundation::text(
3134 theme,
3135 TypeScale::Code,
3136 SharedString::from(format!("Fixture line {line:02}")),
3137 )
3138 .text_tone(theme, TextTone::Muted),
3139 );
3140 }
3141 pane
3142}
3143
3144fn split_pane(_window: &mut Window, cx: &mut App) -> AnyElement {
3145 let theme = cx.theme().clone();
3146 stack(&theme)
3147 .w(px(620.0))
3148 .h(px(380.0))
3149 .child(
3150 div()
3151 .h(px(320.0))
3152 .hairline(&theme)
3153 .radius(&theme, Radius::Card)
3154 .overflow_hidden()
3155 .child(
3156 SplitPane::new("scene.split.workspace")
3157 .horizontal()
3158 .ratio(0.34)
3159 .min_sizes(120.0, 200.0)
3160 .collapsible(true)
3161 .handle_label("Resize the file tree")
3162 .start(filler(&theme, "Files", 6))
3163 .end(filler(&theme, "Editor", 8))
3164 .on_resize(|_, _, _| {})
3165 .on_collapse(|_, _, _| {}),
3166 ),
3167 )
3168 .into_any_element()
3169}
3170
3171fn scroll_shadow(_window: &mut Window, cx: &mut App) -> AnyElement {
3174 let theme = cx.theme().clone();
3175 crate::layout::scroll_to("scene.scroll.scrolled", gpui::point(px(0.0), px(120.0)), cx);
3176 stack(&theme)
3177 .w(px(480.0))
3178 .child(
3179 div()
3180 .hairline(&theme)
3181 .radius(&theme, Radius::Card)
3182 .overflow_hidden()
3183 .child(
3184 ScrollArea::new("scene.scroll.scrolled")
3185 .label("Run output")
3186 .vertical()
3187 .height(200.0)
3188 .child(filler(&theme, "Output", 20)),
3189 ),
3190 )
3191 .into_any_element()
3192}
3193
3194fn scroll_area(_window: &mut Window, cx: &mut App) -> AnyElement {
3195 let theme = cx.theme().clone();
3196 stack(&theme)
3197 .w(px(480.0))
3198 .child(
3199 div()
3200 .hairline(&theme)
3201 .radius(&theme, Radius::Card)
3202 .overflow_hidden()
3203 .child(
3204 ScrollArea::new("scene.scroll.output")
3205 .label("Run output")
3206 .vertical()
3207 .height(200.0)
3208 .child(filler(&theme, "Output", 20)),
3209 ),
3210 )
3211 .child(
3213 div()
3214 .hairline(&theme)
3215 .radius(&theme, Radius::Card)
3216 .overflow_hidden()
3217 .child(
3218 ScrollArea::new("scene.scroll.summary")
3219 .label("Summary")
3220 .vertical()
3221 .height(120.0)
3222 .child(filler(&theme, "Summary", 2)),
3223 ),
3224 )
3225 .into_any_element()
3226}
3227
3228fn scroll_fade(_window: &mut Window, cx: &mut App) -> AnyElement {
3232 let theme = cx.theme().clone();
3233 crate::layout::scroll_to("scene.fade.output", gpui::point(px(0.0), px(120.0)), cx);
3234 stack(&theme)
3235 .w(px(480.0))
3236 .child(caption(&theme, "Scrolled: content runs past both ends"))
3237 .child(
3238 div()
3239 .hairline(&theme)
3240 .radius(&theme, Radius::Card)
3241 .overflow_hidden()
3242 .child(
3243 ScrollFade::new("scene.fade.scrolled")
3244 .edges(FadeEdges::vertical())
3245 .fit_height()
3246 .child(
3247 ScrollArea::new("scene.fade.output")
3248 .label("Run output")
3249 .vertical()
3250 .height(200.0)
3251 .child(filler(&theme, "Output", 20)),
3252 ),
3253 ),
3254 )
3255 .child(caption(&theme, "Nothing hidden: no edge fades"))
3257 .child(
3258 div()
3259 .hairline(&theme)
3260 .radius(&theme, Radius::Card)
3261 .overflow_hidden()
3262 .child(
3263 ScrollFade::new("scene.fade.settled").fit_height().child(
3264 ScrollArea::new("scene.fade.summary")
3265 .label("Summary")
3266 .vertical()
3267 .height(120.0)
3268 .child(filler(&theme, "Summary", 2)),
3269 ),
3270 ),
3271 )
3272 .into_any_element()
3273}
3274
3275fn frost(_window: &mut Window, cx: &mut App) -> AnyElement {
3278 let theme = cx.theme().clone();
3279 let card = |title: &'static str, body: &'static str| {
3280 div()
3281 .column()
3282 .gap(px(theme.space(Space::Xs)))
3283 .p(px(theme.space(Space::Md)))
3284 .child(crate::foundation::text(
3285 &theme,
3286 TypeScale::Label,
3287 SharedString::from(title),
3288 ))
3289 .child(caption(&theme, body))
3290 };
3291 let stripes = || {
3292 div()
3293 .absolute()
3294 .top(px(56.0))
3295 .left(px(100.0))
3296 .w(px(330.0))
3297 .h(px(64.0))
3298 .flex()
3299 .overflow_hidden()
3300 .children((0..22).map(|index| {
3301 div()
3302 .flex_none()
3303 .w(px(15.0))
3304 .h(px(64.0))
3305 .bg(if index % 2 == 0 {
3306 theme.colors.accent
3307 } else {
3308 theme.colors.canvas
3309 })
3310 }))
3311 };
3312 stack(&theme)
3313 .w(px(480.0))
3314 .child(caption(&theme, "A floating surface on glass"))
3315 .child(
3316 div()
3317 .relative()
3318 .h(px(200.0))
3319 .hairline(&theme)
3320 .radius(&theme, Radius::Card)
3321 .overflow_hidden()
3322 .child(stripes())
3323 .child(filler(&theme, "Document", 8))
3324 .child(
3325 div()
3326 .absolute()
3327 .top(px(48.0))
3328 .left(px(120.0))
3329 .w(px(240.0))
3330 .child(
3331 Frost::new("scene.frost.popover")
3332 .radius(Radius::Card)
3333 .child(card(
3334 "Rename",
3335 "The page behind stays visible, out of focus",
3336 )),
3337 ),
3338 ),
3339 )
3340 .child(caption(&theme, "The same glass over a panel surface"))
3341 .child(
3342 div()
3343 .relative()
3344 .h(px(160.0))
3345 .hairline(&theme)
3346 .radius(&theme, Radius::Card)
3347 .overflow_hidden()
3348 .child(stripes())
3349 .child(filler(&theme, "Files", 6))
3350 .child(
3351 div()
3352 .absolute()
3353 .top(px(32.0))
3354 .left(px(120.0))
3355 .w(px(280.0))
3356 .child(
3357 Frost::new("scene.frost.rail")
3358 .surface(gpui_kit_theme::Surface::Panel)
3359 .radius(Radius::Dialog)
3360 .blur(32.0)
3361 .child(card("Rail", "The striped backdrop stays out of focus")),
3362 ),
3363 ),
3364 )
3365 .into_any_element()
3366}
3367
3368struct SceneToolbar {
3370 overflow: Entity<Menu>,
3371}
3372
3373impl Global for SceneToolbar {}
3374
3375fn toolbar(window: &mut Window, cx: &mut App) -> AnyElement {
3376 if !cx.has_global::<SceneToolbar>() {
3377 let overflow = cx.new(|cx| {
3378 Menu::new("scene.toolbar.overflow", window, cx)
3379 .trigger_icon(Icon::List)
3380 .trigger_name("More actions")
3381 });
3382 cx.set_global(SceneToolbar { overflow });
3383 }
3384 let overflow = cx.global::<SceneToolbar>().overflow.clone();
3385 let theme = cx.theme().clone();
3386
3387 stack(&theme)
3388 .w(px(620.0))
3389 .child(
3390 Toolbar::new("scene.toolbar.editor")
3391 .label("Editor actions")
3392 .group(
3393 "history",
3394 [
3395 ToolbarItem::new(
3396 "editor.undo",
3397 "Undo",
3398 IconButton::new("scene.toolbar.undo", Icon::ArrowLeft, "Undo")
3399 .ghost()
3400 .small()
3401 .on_click(|_, _| {}),
3402 )
3403 .icon(Icon::ArrowLeft)
3404 .shortcut("cmd-z"),
3405 ToolbarItem::new(
3406 "editor.redo",
3407 "Redo",
3408 IconButton::new("scene.toolbar.redo", Icon::ArrowRight, "Redo")
3409 .ghost()
3410 .small()
3411 .on_click(|_, _| {}),
3412 )
3413 .icon(Icon::ArrowRight),
3414 ],
3415 )
3416 .group(
3417 "view",
3418 [ToolbarItem::new(
3419 "editor.view",
3420 "View",
3421 SegmentedControl::new("scene.toolbar.view")
3422 .label("View")
3423 .segments([
3424 Segment::new("code", "Code"),
3425 Segment::new("split", "Split"),
3426 Segment::new("preview", "Preview"),
3427 ])
3428 .selected("split")
3429 .small()
3430 .on_select(|_, _, _| {}),
3431 )],
3432 )
3433 .spacer()
3434 .group(
3435 "publish",
3436 [
3437 ToolbarItem::new(
3438 "editor.share",
3439 "Share",
3440 Button::new("scene.toolbar.share")
3441 .label("Share")
3442 .secondary()
3443 .small()
3444 .on_click(|_, _| {}),
3445 )
3446 .icon(Icon::Copy),
3447 ToolbarItem::new(
3448 "editor.publish",
3449 "Publish",
3450 Button::new("scene.toolbar.publish")
3451 .label("Publish")
3452 .primary()
3453 .small()
3454 .on_click(|_, _| {}),
3455 )
3456 .icon(Icon::ArchiveUp),
3457 ToolbarItem::new(
3458 "editor.archive",
3459 "Archive",
3460 Button::new("scene.toolbar.archive")
3461 .label("Archive")
3462 .secondary()
3463 .small()
3464 .on_click(|_, _| {}),
3465 )
3466 .icon(Icon::Archive)
3467 .disabled(true),
3468 ],
3469 )
3470 .overflow_after(4)
3471 .overflow_menu(overflow),
3472 )
3473 .child(crate::foundation::text(
3474 &theme,
3475 TypeScale::Body,
3476 "The last two actions moved into the overflow menu.",
3477 ))
3478 .into_any_element()
3479}
3480
3481fn navigation_sections() -> Vec<SidebarSection> {
3482 vec![
3483 SidebarSection::new("work").title("Work").items([
3484 SidebarItem::new("runs", "Runs")
3485 .icon(Icon::List)
3486 .badge("12")
3487 .children([
3488 SidebarItem::new("runs.active", "Active").icon(Icon::Refresh),
3489 SidebarItem::new("runs.archived", "Archived").icon(Icon::Archive),
3490 ]),
3491 SidebarItem::new("files", "Files").icon(Icon::Folder),
3492 ]),
3493 SidebarSection::new("admin").title("Administration").items([
3494 SidebarItem::new("settings", "Settings").icon(Icon::Settings),
3495 SidebarItem::new("policy", "Managed by policy")
3496 .icon(Icon::Key)
3497 .disabled(true),
3498 ]),
3499 ]
3500}
3501
3502fn sidebar(_window: &mut Window, cx: &mut App) -> AnyElement {
3503 let theme = cx.theme().clone();
3504 let rail = |ident: &'static str, collapsed: bool| {
3505 Sidebar::new(ident)
3506 .sections(navigation_sections())
3507 .active("runs.active")
3508 .collapsed(collapsed)
3509 .footer(
3510 crate::foundation::text(
3511 &theme,
3512 TypeScale::Caption,
3513 if collapsed {
3514 SharedString::new_static("v0")
3515 } else {
3516 SharedString::new_static("Fixture workspace")
3517 },
3518 )
3519 .text_tone(&theme, TextTone::Faint),
3520 )
3521 .on_select(|_, _, _| {})
3522 };
3523
3524 stack(&theme)
3525 .h(px(420.0))
3526 .child(
3527 div()
3528 .flex()
3529 .flex_row()
3530 .h(px(360.0))
3531 .gap(px(theme.space(Space::Lg)))
3532 .child(rail("scene.sidebar.expanded", false))
3533 .child(rail("scene.sidebar.collapsed", true)),
3534 )
3535 .into_any_element()
3536}
3537
3538struct ScenePagination {
3540 page_size: Entity<Select>,
3541}
3542
3543impl Global for ScenePagination {}
3544
3545fn pagination(window: &mut Window, cx: &mut App) -> AnyElement {
3546 if !cx.has_global::<ScenePagination>() {
3547 let page_size = cx.new(|cx| {
3548 Select::new("scene.pagination.size", window, cx)
3549 .name("Rows per page")
3550 .options([
3551 SelectOption::new("25", "25 per page"),
3552 SelectOption::new("50", "50 per page"),
3553 SelectOption::new("100", "100 per page"),
3554 ])
3555 .selected("50")
3556 });
3557 cx.set_global(ScenePagination { page_size });
3558 }
3559 let page_size = cx.global::<ScenePagination>().page_size.clone();
3560 let theme = cx.theme().clone();
3561
3562 stack(&theme)
3563 .w(px(620.0))
3564 .child(
3565 Pagination::new("scene.pagination.known")
3566 .page(9)
3567 .total_pages(20)
3568 .page_size(page_size)
3569 .on_select(|_, _, _| {}),
3570 )
3571 .child(
3574 Pagination::new("scene.pagination.unknown")
3575 .page(3)
3576 .unknown_total(true)
3577 .on_select(|_, _, _| {}),
3578 )
3579 .into_any_element()
3580}
3581
3582struct SceneDrawer {
3585 filters: Entity<Drawer>,
3586}
3587
3588impl Global for SceneDrawer {}
3589
3590fn drawer(window: &mut Window, cx: &mut App) -> AnyElement {
3591 if !cx.has_global::<SceneDrawer>() {
3592 let filters = cx.new(|cx| {
3593 Drawer::new("scene.drawer.filters", window, cx)
3594 .edge(Edge::Right)
3595 .size(320.0)
3596 .title("Filter runs")
3597 .description("The drawer reports what was chosen. The host applies it.")
3598 .content(|_, cx| {
3599 let theme = cx.theme().clone();
3600 div()
3601 .flex()
3602 .flex_col()
3603 .gap(px(theme.space(Space::Sm)))
3604 .child(
3605 Checkbox::new("scene.drawer.failed")
3606 .label("Failed runs only")
3607 .checked(true)
3608 .on_change(|_, _, _| {}),
3609 )
3610 .child(
3611 Checkbox::new("scene.drawer.mine")
3612 .label("Started by me")
3613 .on_change(|_, _, _| {}),
3614 )
3615 .into_any_element()
3616 })
3617 .footer(|_, _| {
3618 Button::new("scene.drawer.apply")
3619 .label("Apply")
3620 .primary()
3621 .full_width(true)
3622 .on_click(|_, _| {})
3623 .into_any_element()
3624 })
3625 });
3626 filters.update(cx, |drawer, cx| {
3627 drawer.open(window, cx);
3628 drawer.settle(cx);
3629 });
3630 cx.set_global(SceneDrawer { filters });
3631 }
3632 let filters = cx.global::<SceneDrawer>().filters.clone();
3633 let theme = cx.theme().clone();
3634
3635 stack(&theme)
3636 .w(px(620.0))
3637 .h(px(400.0))
3638 .child(crate::foundation::text(
3639 &theme,
3640 TypeScale::Body,
3641 "Content behind the drawer",
3642 ))
3643 .child(filters)
3644 .into_any_element()
3645}
3646
3647#[derive(Debug)]
3652struct SceneQueue {
3653 steps: Vec<(&'static str, &'static str)>,
3654}
3655
3656impl Global for SceneQueue {}
3657
3658fn motion_flip(window: &mut Window, cx: &mut App) -> AnyElement {
3659 if !cx.has_global::<SceneQueue>() {
3660 cx.set_global(SceneQueue {
3661 steps: vec![
3662 ("render", "Render frames"),
3663 ("upload", "Upload artifacts"),
3664 ("verify", "Verify checksums"),
3665 ("publish", "Publish release"),
3666 ],
3667 });
3668 }
3669 let steps = cx.global::<SceneQueue>().steps.clone();
3670 let theme = cx.theme().clone();
3671
3672 let mut queue = Card::new().id("scene.motion.queue");
3673 for (index, (id, label)) in steps.iter().enumerate() {
3674 let ident = format!("scene.motion.{id}");
3675 let handle = flip(ident.clone(), cx);
3676 queue = queue.child(
3677 ListRow::new()
3678 .id(ident)
3679 .child(div().flex_1().child(*label))
3680 .child(Badge::new(format!("{}", index + 1)).neutral())
3681 .flip(&handle, window, cx),
3682 );
3683 }
3684
3685 stack(&theme)
3686 .w(px(420.0))
3687 .child(
3688 row(&theme).child(
3689 Button::new("scene.motion.reorder")
3690 .label("Move the last step first")
3691 .secondary()
3692 .on_click(|_, cx| {
3693 cx.update_global::<SceneQueue, ()>(|queue, _| queue.steps.rotate_right(1));
3694 cx.refresh_windows();
3695 }),
3696 ),
3697 )
3698 .child(queue)
3699 .child(
3700 crate::foundation::text(
3701 &theme,
3702 TypeScale::Body,
3703 "Rows land in their new slot at once and slide into it.",
3704 )
3705 .text_tone(&theme, TextTone::Muted),
3706 )
3707 .into_any_element()
3708}
3709
3710#[derive(Debug)]
3716struct SceneStates {
3717 forward: bool,
3718}
3719
3720impl Global for SceneStates {}
3721
3722fn motion_state(_window: &mut Window, cx: &mut App) -> AnyElement {
3723 if !cx.has_global::<SceneStates>() {
3724 cx.set_global(SceneStates { forward: false });
3725 }
3726 let forward = cx.global::<SceneStates>().forward;
3727 let theme = cx.theme().clone();
3728
3729 stack(&theme)
3730 .w(px(460.0))
3731 .child(
3732 row(&theme).child(
3733 Button::new("scene.state.flip")
3734 .label("Flip every state")
3735 .secondary()
3736 .on_click(|_, cx| {
3737 cx.update_global::<SceneStates, ()>(|state, _| {
3738 state.forward = !state.forward
3739 });
3740 cx.refresh_windows();
3741 }),
3742 ),
3743 )
3744 .child({
3745 let terms = Checkbox::new("scene.state.terms").label("Accept the terms");
3746 if forward {
3747 terms.checked(true)
3748 } else {
3749 terms.mixed()
3750 }
3751 .on_change(|_, _, _| {})
3752 })
3753 .child(
3754 Radio::new("scene.state.plan")
3755 .label("Bill monthly")
3756 .selected(forward)
3757 .on_select(|_, _| {}),
3758 )
3759 .child(
3760 Switch::new("scene.state.notify")
3761 .label("Send run notifications")
3762 .on(forward)
3763 .on_change(|_, _, _| {}),
3764 )
3765 .child(
3766 SegmentedControl::new("scene.state.view")
3767 .segments(vec![
3768 Segment::new("list", "List"),
3769 Segment::new("grid", "Grid"),
3770 ])
3771 .selected(if forward { "grid" } else { "list" })
3772 .on_select(|_, _, _| {}),
3773 )
3774 .child(
3775 Tabs::new("scene.state.tabs")
3776 .tabs(vec![
3777 TabItem::new("overview", "Overview"),
3778 TabItem::new("runs", "Runs"),
3779 ])
3780 .selected(if forward { "runs" } else { "overview" })
3781 .on_select(|_, _, _| {}),
3782 )
3783 .child(
3784 ProgressBar::new("scene.state.progress")
3785 .label("Uploading artifacts")
3786 .fraction(if forward { 0.86 } else { 0.12 }),
3787 )
3788 .child(
3789 Accordion::new("scene.state.sections")
3790 .expanded_ids(if forward { &["retention"][..] } else { &[][..] })
3791 .on_toggle(|_, _, _, _| {})
3792 .section(
3793 AccordionSection::new("retention", "Retention")
3794 .description("How long verified results are kept")
3795 .body(crate::foundation::text(&theme, TypeScale::Body, "Results are kept in the workspace for 30 days.")),
3796 ),
3797 )
3798 .child(crate::foundation::text(&theme, TypeScale::Body, "Every state settles within a fifth of a second; the values are published the moment they change.").text_tone(&theme, TextTone::Muted))
3799 .into_any_element()
3800}
3801
3802#[derive(Debug)]
3804struct SceneCounts {
3805 runs: f64,
3806 seconds: f64,
3807}
3808
3809impl Global for SceneCounts {}
3810
3811fn animated_number(_window: &mut Window, cx: &mut App) -> AnyElement {
3812 if !cx.has_global::<SceneCounts>() {
3813 cx.set_global(SceneCounts {
3814 runs: 1204.0,
3815 seconds: 18.4,
3816 });
3817 }
3818 let counts = cx.global::<SceneCounts>();
3819 let (runs, seconds) = (counts.runs, counts.seconds);
3820 let theme = cx.theme().clone();
3821
3822 let readout = |label: &'static str, number: AnimatedNumber| {
3823 div()
3824 .column()
3825 .gap(px(theme.spacing.xs))
3826 .child(
3827 crate::foundation::text(&theme, TypeScale::Caption, label)
3828 .text_tone(&theme, TextTone::Muted),
3829 )
3830 .child(number)
3831 };
3832
3833 stack(&theme)
3834 .child(
3835 row(&theme)
3836 .gap(px(theme.spacing.xl))
3837 .items_start()
3838 .child(readout(
3839 "Runs this week",
3840 AnimatedNumber::new("scene.number.runs", runs).format(grouped),
3841 ))
3842 .child(readout(
3843 "Median duration",
3844 AnimatedNumber::new("scene.number.seconds", seconds)
3845 .format(|value| format!("{value:.1}s")),
3846 )),
3847 )
3848 .child(
3849 row(&theme).child(
3850 Button::new("scene.number.recount")
3851 .label("Recount")
3852 .secondary()
3853 .on_click(|_, cx| {
3854 cx.update_global::<SceneCounts, ()>(|counts, _| {
3855 counts.runs += 318.0;
3856 counts.seconds += 4.7;
3857 });
3858 cx.refresh_windows();
3859 }),
3860 ),
3861 )
3862 .child(
3863 crate::foundation::text(
3864 &theme,
3865 TypeScale::Body,
3866 "The published value is the target, from the frame it changes.",
3867 )
3868 .text_tone(&theme, TextTone::Muted),
3869 )
3870 .into_any_element()
3871}
3872
3873fn caption(theme: &Theme, text: impl Into<SharedString>) -> gpui::Div {
3875 crate::foundation::text(theme, TypeScale::Caption, text.into())
3876 .text_tone(theme, TextTone::Muted)
3877}
3878
3879fn drag_list(_window: &mut Window, cx: &mut App) -> AnyElement {
3880 let theme = cx.theme().clone();
3881 let carried = fixture_record(4);
3882 let anchor = fixture_record(1);
3883 dnd::stage(
3887 StagedDrag::new(DragItem::new(
3888 "scene.drag.records",
3889 carried.0.clone(),
3890 carried.1.clone(),
3891 ))
3892 .landing(
3893 "scene.drag.records",
3894 DropPosition::Before(anchor.0.clone()),
3895 Some(1),
3896 true,
3897 ),
3898 cx,
3899 );
3900
3901 stack(&theme)
3902 .w(px(420.0))
3903 .child(caption(
3904 &theme,
3905 SharedString::from(format!("{} moving before {}", carried.1, anchor.1)),
3906 ))
3907 .child(
3908 div()
3909 .relative()
3910 .hairline(&theme)
3911 .radius(&theme, Radius::Card)
3912 .overflow_hidden()
3913 .child(
3914 List::new("scene.drag.records", 6, |index, _, _| {
3915 let (id, label) = fixture_record(index);
3916 ListItem::new(id, label.clone()).text(label)
3917 })
3918 .visible_rows(7)
3922 .reorderable(true)
3923 .on_select(|_, _, _| {})
3924 .on_reorder(|_, _, _| {}),
3925 )
3926 .children(
3927 dnd::staged_ghost(cx)
3928 .map(|ghost| div().absolute().left(px(96.0)).top(px(18.0)).child(ghost)),
3929 ),
3930 )
3931 .into_any_element()
3932}
3933
3934fn drag_tree(_window: &mut Window, cx: &mut App) -> AnyElement {
3935 let theme = cx.theme().clone();
3936 dnd::stage(
3937 StagedDrag::new(
3938 DragItem::new("scene.drag.workspace", "kit", "gpui-kit").icon(Icon::Document),
3939 )
3940 .landing(
3941 "scene.drag.workspace",
3942 DropPosition::Into(SharedString::new_static("docs")),
3943 None,
3944 true,
3945 ),
3946 cx,
3947 );
3948
3949 stack(&theme)
3950 .w(px(360.0))
3951 .child(caption(&theme, "gpui-kit moving into docs"))
3952 .child(
3953 div()
3954 .relative()
3955 .child(
3956 Tree::new("scene.drag.workspace")
3957 .expanded_ids(&["workspace", "crates", "docs"])
3958 .nodes([TreeNode::new("workspace", "workspace")
3959 .icon(Icon::Folder)
3960 .children([
3961 TreeNode::new("crates", "crates")
3962 .icon(Icon::Folder)
3963 .children([
3964 TreeNode::new("kit", "gpui-kit").icon(Icon::Document),
3965 TreeNode::new("tokens", "gpui-kit-tokens")
3966 .icon(Icon::Document),
3967 ]),
3968 TreeNode::new("docs", "docs")
3969 .icon(Icon::Folder)
3970 .children([TreeNode::new("components", "components.md")
3971 .icon(Icon::Document)]),
3972 ])])
3973 .reorderable(true)
3974 .on_toggle(|_, _, _, _| {})
3975 .on_select(|_, _, _| {})
3976 .on_move(|_, _, _| {}),
3977 )
3978 .children(
3979 dnd::staged_ghost(cx)
3980 .map(|ghost| div().absolute().left(px(212.0)).top(px(116.0)).child(ghost)),
3981 ),
3982 )
3983 .into_any_element()
3984}
3985
3986fn dropzone(_window: &mut Window, cx: &mut App) -> AnyElement {
3987 let theme = cx.theme().clone();
3988 stack(&theme)
3991 .w(px(560.0))
3992 .child(caption(&theme, "idle, accepting, refusing"))
3993 .child(
3994 row(&theme)
3995 .items_stretch()
3996 .child(
3997 div().flex_1().child(
3998 Dropzone::new("scene.dropzone.idle", "Drop files to attach")
3999 .hint("PDF, PNG, or plain text")
4000 .state(DropzoneState::Idle)
4001 .on_files(|_, _, _| {}),
4002 ),
4003 )
4004 .child(
4005 div().flex_1().child(
4006 Dropzone::new("scene.dropzone.accepting", "Drop files to attach")
4007 .hint("PDF, PNG, or plain text")
4008 .state(DropzoneState::Accepting)
4009 .on_files(|_, _, _| {}),
4010 ),
4011 )
4012 .child(
4013 div().flex_1().child(
4014 Dropzone::new("scene.dropzone.refusing", "Drop files to attach")
4015 .refusal("A folder cannot be attached.")
4016 .state(DropzoneState::Refusing)
4017 .on_files(|_, _, _| {}),
4018 ),
4019 ),
4020 )
4021 .into_any_element()
4022}
4023
4024fn wizard(_window: &mut Window, cx: &mut App) -> AnyElement {
4025 let theme = cx.theme().clone();
4026 stack(&theme)
4027 .w(px(720.0))
4028 .child(caption(
4029 &theme,
4030 "horizontal, with a blocked step and a failed one",
4031 ))
4032 .child(
4033 Wizard::new("scene.wizard.release")
4034 .steps([
4035 WizardStep::new("prepare", "Prepare")
4036 .description("Check the workspace is clean")
4037 .complete(),
4038 WizardStep::new("build", "Build")
4039 .description("Compile every target")
4040 .failed("The build failed on the test target."),
4041 WizardStep::new("sign", "Sign").current(),
4042 WizardStep::new("publish", "Publish")
4043 .blocked("Approval is required for this workspace."),
4044 ])
4045 .body(
4046 div()
4047 .p(px(theme.spacing.md))
4048 .radius(&theme, Radius::Card)
4049 .hairline(&theme)
4050 .child(crate::foundation::text(
4051 &theme,
4052 TypeScale::Body,
4053 SharedString::new_static(
4054 "The body of the current step belongs to the caller.",
4055 ),
4056 )),
4057 )
4058 .back_to("build")
4059 .on_navigate(|_, _, _| {}),
4060 )
4061 .child(caption(&theme, "vertical, finishing"))
4062 .child(
4063 Wizard::new("scene.wizard.setup")
4064 .vertical()
4065 .steps([
4066 WizardStep::new("account", "Account").complete(),
4067 WizardStep::new("workspace", "Workspace").complete(),
4068 WizardStep::new("review", "Review").current(),
4069 ])
4070 .back_to("workspace")
4071 .finish(true)
4072 .on_navigate(|_, _, _| {}),
4073 )
4074 .into_any_element()
4075}
4076
4077fn undo_history(_window: &mut Window, cx: &mut App) -> AnyElement {
4078 let theme = cx.theme().clone();
4079 stack(&theme)
4080 .w(px(520.0))
4081 .child(caption(
4082 &theme,
4083 "the caller owns every revision and whether it can be restored",
4084 ))
4085 .child(
4086 UndoHistory::new("scene.undo-history", "Document undo history")
4087 .entries([
4088 HistoryEntry::new("opened", "Opened the fixture")
4089 .description("The initial verified document")
4090 .source("Fixture host")
4091 .time("10:14"),
4092 HistoryEntry::new("renamed", "Renamed the title")
4093 .source("Alex")
4094 .time("10:16"),
4095 HistoryEntry::new("imported", "Imported archived blocks")
4096 .description("The archive is no longer available")
4097 .source("Archive")
4098 .time("10:18")
4099 .unavailable("This revision cannot be restored without the archive."),
4100 HistoryEntry::new("current", "Reordered the summary")
4101 .description("Current document")
4102 .source("Alex")
4103 .time("10:21"),
4104 HistoryEntry::new("draft", "Drafted a new conclusion")
4105 .source("Fixture host")
4106 .time("10:23"),
4107 ])
4108 .current("current")
4109 .on_jump(|_, _, _| {}),
4110 )
4111 .into_any_element()
4112}
4113
4114fn settings(_window: &mut Window, cx: &mut App) -> AnyElement {
4115 let theme = cx.theme().clone();
4116 stack(&theme)
4117 .w(px(620.0))
4118 .child(
4119 SettingsSection::new("scene.settings.general", "General")
4120 .description("How this workspace behaves")
4121 .row(
4122 SettingsRow::new("scene.settings.general.autosave", "Save automatically")
4123 .description("Write changes as they happen")
4124 .control(
4125 Switch::new("scene.settings.general.autosave.switch")
4126 .named("Save automatically")
4127 .on(true)
4128 .on_change(|_, _, _| {}),
4129 ),
4130 )
4131 .row(
4132 SettingsRow::new("scene.settings.general.runtime", "Native runtime")
4133 .description("Runs work on this machine instead of a host")
4134 .badge("Requires restart")
4135 .control(
4136 Switch::new("scene.settings.general.runtime.switch")
4137 .named("Native runtime")
4138 .on(false)
4139 .on_change(|_, _, _| {}),
4140 ),
4141 )
4142 .row(
4143 SettingsRow::new("scene.settings.general.telemetry", "Usage reporting")
4144 .description("Nobody on this machine can change this")
4145 .value("Off")
4146 .managed("your administrator"),
4147 ),
4148 )
4149 .child(
4150 SettingsSection::new("scene.settings.sync", "Synchronisation")
4151 .description("What travels between machines")
4152 .dimmed_by("This workspace is local, so nothing synchronises.")
4153 .row(
4154 SettingsRow::new("scene.settings.sync.settings", "Sync settings")
4155 .value("Off")
4156 .control(
4157 Switch::new("scene.settings.sync.settings.switch")
4158 .named("Sync settings")
4159 .on(false)
4160 .on_change(|_, _, _| {}),
4161 ),
4162 )
4163 .row(
4164 SettingsRow::new("scene.settings.sync.history", "Sync history")
4165 .value("Off")
4166 .control(
4167 Switch::new("scene.settings.sync.history.switch")
4168 .named("Sync history")
4169 .on(false)
4170 .on_change(|_, _, _| {}),
4171 ),
4172 ),
4173 )
4174 .into_any_element()
4175}
4176
4177fn detail(_window: &mut Window, cx: &mut App) -> AnyElement {
4178 let theme = cx.theme().clone();
4179 stack(&theme)
4180 .w(px(640.0))
4181 .child(caption(
4182 &theme,
4183 "unknown, not applicable, and redacted are three facts",
4184 ))
4185 .child(
4186 DescriptionList::new("scene.detail.facts")
4187 .columns(2)
4188 .items([
4189 DescriptionItem::new("id", "Run", "run-4821"),
4190 DescriptionItem::new("owner", "Owner", "fixture-owner"),
4191 DescriptionItem::new("finished", "Finished", DescriptionValue::Unknown),
4192 DescriptionItem::new("artifact", "Artifact", DescriptionValue::NotApplicable),
4193 DescriptionItem::new(
4194 "token",
4195 "Access token",
4196 DescriptionValue::redacted("51 characters"),
4197 )
4198 .copyable(true),
4199 ])
4200 .on_copy(|_, _, _| {}),
4201 )
4202 .child(caption(
4203 &theme,
4204 "what happened, in the words the host chose",
4205 ))
4206 .child(
4207 Timeline::new("scene.detail.activity")
4208 .group(
4209 TimelineGroup::new("today", "Today")
4210 .entry(
4211 TimelineEntry::new("queued", "Run queued")
4212 .time("09:12")
4213 .actor("fixture-owner")
4214 .tone(Tone::Neutral),
4215 )
4216 .entry(
4217 TimelineEntry::new("started", "Indexing started")
4218 .time("09:13")
4219 .actor("scheduler")
4220 .tone(Tone::Info),
4221 )
4222 .entry(
4223 TimelineEntry::new("failed", "Indexing failed")
4224 .time("09:41")
4225 .actor("scheduler")
4226 .tone(Tone::Danger)
4227 .detail(crate::foundation::text(
4228 &theme,
4229 TypeScale::Body,
4230 SharedString::new_static(
4231 "The host refused the request. The refusal is shown as it \
4232 arrived.",
4233 ),
4234 ).text_tone(&theme, TextTone::Muted)),
4235 ),
4236 )
4237 .group(
4238 TimelineGroup::new("earlier", "Earlier").entry(
4239 TimelineEntry::new("imported", "Workspace imported")
4240 .time_unknown()
4241 .actor("fixture-owner")
4242 .tone(Tone::Neutral),
4243 ),
4244 ),
4245 )
4246 .into_any_element()
4247}
4248
4249fn filter_bar(_window: &mut Window, cx: &mut App) -> AnyElement {
4250 let theme = cx.theme().clone();
4251 stack(&theme)
4252 .w(px(720.0))
4253 .child(
4254 FilterBar::new("scene.filter-bar.runs")
4255 .conditions([
4256 FilterCondition::new("status", "Status", "is", "failed"),
4257 FilterCondition::new("owner", "Owner", "is", "fixture-owner"),
4258 FilterCondition::new("started", "Started", "after", "09:00"),
4259 ])
4260 .count(ResultCount::Known(14))
4261 .noun("runs")
4262 .on_add(|_, _| {})
4263 .on_remove(|_, _, _| {})
4264 .on_clear(|_, _| {}),
4265 )
4266 .child(caption(&theme, "counting is not zero"))
4267 .child(
4268 FilterBar::new("scene.filter-bar.counting")
4269 .conditions([FilterCondition::new("status", "Status", "is", "queued")])
4270 .count(ResultCount::Counting)
4271 .on_add(|_, _| {})
4272 .on_remove(|_, _, _| {})
4273 .on_clear(|_, _| {}),
4274 )
4275 .into_any_element()
4276}
4277
4278fn inline_edit(_window: &mut Window, cx: &mut App) -> AnyElement {
4279 let theme = cx.theme().clone();
4280 stack(&theme)
4281 .w(px(420.0))
4282 .child(caption(
4283 &theme,
4284 "reading, editing, and a save that did not take",
4285 ))
4286 .child(
4287 InlineEdit::new("scene.inline-edit.title", "Indexing the workspace")
4288 .on_edit(|_, _| {})
4289 .on_commit(|_, _, _| {})
4290 .on_cancel(|_, _| {}),
4291 )
4292 .child(
4293 InlineEdit::new("scene.inline-edit.owner", "fixture-owner")
4294 .editing(true)
4295 .on_edit(|_, _| {})
4296 .on_commit(|_, _, _| {})
4297 .on_cancel(|_, _| {}),
4298 )
4299 .child(
4300 InlineEdit::new(
4301 "scene.inline-edit.note",
4302 "Retry after the host is reachable",
4303 )
4304 .editing(true)
4305 .failure("The host refused this change. What you typed is still here.")
4306 .on_edit(|_, _| {})
4307 .on_commit(|_, _, _| {})
4308 .on_cancel(|_, _| {}),
4309 )
4310 .child(
4311 InlineEdit::new("scene.inline-edit.policy", "Set by the administrator")
4312 .disabled(true)
4313 .on_edit(|_, _| {}),
4314 )
4315 .into_any_element()
4316}
4317
4318fn progress_circle(_window: &mut Window, cx: &mut App) -> AnyElement {
4319 let theme = cx.theme().clone();
4320 stack(&theme)
4321 .w(px(520.0))
4322 .child(caption(
4323 &theme,
4324 "a position exists only when the extent is known",
4325 ))
4326 .child(
4327 row(&theme)
4328 .gap(px(theme.spacing.lg))
4329 .child(
4330 ProgressCircle::new("scene.progress-circle.upload")
4331 .count(3, 12)
4332 .label("Uploading artifacts")
4333 .centre("25%"),
4334 )
4335 .child(
4336 ProgressCircle::new("scene.progress-circle.verify")
4337 .fraction(0.72)
4338 .label("Verifying checksums")
4339 .display("72%")
4340 .centre("72%"),
4341 )
4342 .child(
4343 ProgressCircle::new("scene.progress-circle.contact")
4344 .label("Contacting the host"),
4345 ),
4346 )
4347 .child(caption(&theme, "the size ramp"))
4348 .child(
4349 row(&theme)
4350 .gap(px(theme.spacing.lg))
4351 .child(
4352 ProgressCircle::new("scene.progress-circle.xs")
4353 .fraction(0.4)
4354 .label("Extra small")
4355 .xs(),
4356 )
4357 .child(
4358 ProgressCircle::new("scene.progress-circle.sm")
4359 .fraction(0.4)
4360 .label("Small")
4361 .small(),
4362 )
4363 .child(
4364 ProgressCircle::new("scene.progress-circle.md")
4365 .fraction(0.4)
4366 .label("Medium")
4367 .medium(),
4368 )
4369 .child(
4370 ProgressCircle::new("scene.progress-circle.lg")
4371 .fraction(0.4)
4372 .label("Large")
4373 .large(),
4374 ),
4375 )
4376 .into_any_element()
4377}
4378
4379fn split_tree(_window: &mut Window, cx: &mut App) -> AnyElement {
4383 let theme = cx.theme().clone();
4384 let layout = SplitLayout::horizontal(
4385 "workspace",
4386 0.26,
4387 SplitLayout::leaf(SplitPaneSpec::new("files").min_width(140.0)),
4388 SplitLayout::horizontal(
4389 "body",
4390 0.74,
4391 SplitLayout::vertical(
4392 "editing",
4393 0.6,
4394 SplitLayout::leaf(SplitPaneSpec::new("editor").min_height(90.0)),
4395 SplitLayout::leaf(SplitPaneSpec::new("terminal").min_height(70.0)),
4396 ),
4397 SplitLayout::leaf(SplitPaneSpec::new("outline").rail(40.0).collapsed(true)),
4400 ),
4401 );
4402
4403 stack(&theme)
4404 .w(px(680.0))
4405 .child(
4406 div()
4407 .h(px(340.0))
4408 .hairline(&theme)
4409 .radius(&theme, Radius::Card)
4410 .overflow_hidden()
4411 .child(
4412 SplitTree::new("scene.tree.workspace")
4413 .layout(layout)
4414 .pane("files", filler(&theme, "Files", 6))
4415 .pane("editor", filler(&theme, "main.rs", 6))
4416 .pane("terminal", filler(&theme, "Terminal", 2))
4417 .pane("outline", div())
4420 .on_change(|_, _, _| {}),
4421 ),
4422 )
4423 .child(caption(
4424 &theme,
4425 "A divider high in the tree stops where a leaf far below it would \
4426 run out of room.",
4427 ))
4428 .into_any_element()
4429}
4430
4431fn ide_shell(_window: &mut Window, cx: &mut App) -> AnyElement {
4434 let theme = cx.theme().clone();
4435 let mut branch = AsyncValue::<SharedString, String>::ready("main@a1b2c3".into());
4436 branch.refresh();
4437 branch.fail_refresh("the host is unreachable".into());
4438
4439 div()
4440 .column()
4441 .w(px(900.0))
4442 .h(px(560.0))
4443 .bg(theme.colors.canvas)
4444 .text_color(theme.colors.text)
4445 .font_family(theme.typography.sans.clone())
4446 .child(
4447 div().flex_1().min_h(px(0.0)).child(
4448 Dock::new("scene.shell")
4449 .share(DockRegion::Left, 0.24)
4450 .share(DockRegion::Bottom, 0.3)
4451 .panel(
4452 DockRegion::Left,
4453 DockPanel::new("files", "Files")
4454 .icon(Icon::Folder)
4455 .content(filler(&theme, "Workspace", 8)),
4456 )
4457 .panel(
4458 DockRegion::Left,
4459 DockPanel::new("search", "Search")
4460 .icon(Icon::Magnifier)
4461 .badge("12"),
4462 )
4463 .active(DockRegion::Left, "files")
4464 .panel(
4465 DockRegion::Centre,
4466 DockPanel::new("editor", "main.rs")
4467 .icon(Icon::Document)
4468 .content(filler(&theme, "fn main()", 12)),
4469 )
4470 .panel(
4471 DockRegion::Right,
4472 DockPanel::new("outline", "Outline").icon(Icon::List),
4473 )
4474 .panel(
4475 DockRegion::Right,
4476 DockPanel::new("history", "History").icon(Icon::GitBranch),
4477 )
4478 .collapsed(DockRegion::Right, true)
4479 .panel(
4480 DockRegion::Bottom,
4481 DockPanel::new("terminal", "Terminal")
4482 .icon(Icon::Terminal)
4483 .content(filler(&theme, "$ cargo test", 4)),
4484 )
4485 .panel(
4486 DockRegion::Bottom,
4487 DockPanel::new("problems", "Problems")
4488 .icon(Icon::Danger)
4489 .badge("3")
4490 .unavailable(
4491 "The language server is not running, so problems cannot be \
4492 listed. Nothing here is out of date; there is nothing here.",
4493 ),
4494 )
4495 .active(DockRegion::Bottom, "problems")
4498 .on_event(|_, _, _| {}),
4499 ),
4500 )
4501 .child(
4502 StatusBar::new("scene.shell.status")
4503 .label("Workspace status")
4504 .start([
4505 StatusItem::text("branch", "main")
4506 .icon(Icon::GitBranch)
4507 .tracking(&branch),
4508 StatusItem::state("build", "Build passing", Tone::Success),
4509 ])
4510 .centre([StatusItem::progress("index", "Indexing the workspace")
4511 .count(7, 12)
4512 .state_name("loading")])
4513 .end([
4514 StatusItem::text("position", "Ln 42, Col 7"),
4515 StatusItem::action("encoding", "UTF-8").on_click(|_, _| {}),
4516 ]),
4517 )
4518 .into_any_element()
4519}
4520
4521#[derive(Clone)]
4522struct SceneRecorders {
4523 idle: Entity<KeybindingRecorder>,
4524 recording: Entity<KeybindingRecorder>,
4525 captured: Entity<KeybindingRecorder>,
4526 conflicting: Entity<KeybindingRecorder>,
4527}
4528
4529impl Global for SceneRecorders {}
4530
4531fn keybinding(window: &mut Window, cx: &mut App) -> AnyElement {
4532 if !cx.has_global::<SceneRecorders>() {
4533 let idle = cx.new(|cx| {
4534 KeybindingRecorder::new("scene.keybinding.idle", window, cx).label("Open workspace")
4535 });
4536 let recording = cx.new(|cx| {
4537 KeybindingRecorder::new("scene.keybinding.recording", window, cx)
4538 .label("Command palette")
4539 });
4540 let captured = cx.new(|cx| {
4541 KeybindingRecorder::new("scene.keybinding.captured", window, cx)
4542 .label("Toggle terminal")
4543 .binding("ctrl-`")
4544 });
4545 let conflicting = cx.new(|cx| {
4546 KeybindingRecorder::new("scene.keybinding.conflicting", window, cx)
4547 .label("Split editor")
4548 .binding("cmd-shift-p")
4549 .conflict(Some("Already opens the command palette"))
4551 });
4552 recording.update(cx, |recorder, cx| recorder.start(window, cx));
4556 cx.set_global(SceneRecorders {
4557 idle,
4558 recording,
4559 captured,
4560 conflicting,
4561 });
4562 }
4563 let recorders = cx.global::<SceneRecorders>().clone();
4564 let theme = cx.theme().clone();
4565
4566 stack(&theme)
4570 .w(px(680.0))
4571 .child(
4572 SettingsSection::new("scene.keybinding.keymap", "Keyboard shortcuts")
4573 .description("Recording captures the next keystroke instead of acting on it.")
4574 .row(
4575 SettingsRow::new("scene.keybinding.row.open", "Open workspace")
4576 .description("Nothing is bound yet")
4577 .control(recorders.idle),
4578 )
4579 .row(
4580 SettingsRow::new("scene.keybinding.row.palette", "Command palette")
4581 .description("Listening for a keystroke")
4582 .control(recorders.recording),
4583 )
4584 .row(
4585 SettingsRow::new("scene.keybinding.row.terminal", "Toggle terminal")
4586 .control(recorders.captured),
4587 )
4588 .row(
4589 SettingsRow::new("scene.keybinding.row.split", "Split editor")
4590 .description("The host judged this one, and said so")
4591 .control(recorders.conflicting),
4592 ),
4593 )
4594 .child(caption(
4595 &theme,
4596 "Escape ends recording without capturing, so escape cannot be bound \
4597 unless the caller turns allow_escape on.",
4598 ))
4599 .into_any_element()
4600}
4601
4602#[derive(Clone)]
4603struct SceneKeymapEditor(Entity<KeymapEditor>);
4604
4605impl Global for SceneKeymapEditor {}
4606
4607fn keymap_editor(window: &mut Window, cx: &mut App) -> AnyElement {
4608 if !cx.has_global::<SceneKeymapEditor>() {
4609 let editor = cx.new(|cx| {
4610 KeymapEditor::new("scene.keymap-editor", window, cx).commands([
4611 KeymapCommand::new("workspace.open", "Open workspace")
4612 .context("Workspace")
4613 .defaults(["cmd-o"])
4614 .bindings([
4615 KeymapBinding::new("user", "cmd-shift-o")
4616 .conflict("Already opens recent workspaces")
4617 .provenance("User keymap"),
4618 KeymapBinding::new("workspace", "ctrl-o").provenance("Workspace keymap"),
4619 ])
4620 .searchable("Open a folder or project", ["folder", "project"]),
4621 KeymapCommand::new("terminal.toggle", "Toggle terminal")
4622 .context("Terminal")
4623 .defaults(["ctrl-`"])
4624 .bindings([KeymapBinding::new("default", "ctrl-`")])
4625 .searchable("Show the integrated terminal", ["panel", "console"]),
4626 KeymapCommand::new("policy.locked", "Managed shortcut")
4627 .context("Workspace")
4628 .defaults(["cmd-l"])
4629 .bindings([KeymapBinding::new("managed", "cmd-l").provenance("Host policy")])
4630 .refused("This binding is managed by the host."),
4631 ])
4632 });
4633 cx.set_global(SceneKeymapEditor(editor));
4634 }
4635 let editor = cx.global::<SceneKeymapEditor>().0.clone();
4636 let theme = cx.theme().clone();
4637
4638 stack(&theme)
4639 .w(px(760.0))
4640 .child(editor)
4641 .child(caption(
4642 &theme,
4643 "Bindings remain caller-owned; add, remove, and reset are intents.",
4644 ))
4645 .into_any_element()
4646}
4647
4648#[cfg(feature = "fixtures")]
4649mod dates {
4650 use std::rc::Rc;
4651
4652 use gpui::{AnyElement, App, Entity, Global, IntoElement, Window, div, prelude::*, px};
4653 use gpui_kit_theme::{Space, TextTone, TypeScale};
4654
4655 use crate::datetime::fixture::FixtureDateAdapter;
4656 use crate::datetime::{
4657 Calendar, DateInput, DayMark, DayRange, RangePicker, SharedDateAdapter, TimeInput,
4658 TimeOfDay,
4659 };
4660 use crate::display::badge::Tone;
4661 use crate::foundation::{ActiveTheme, StyledExt};
4662
4663 use super::{row, stack};
4664
4665 fn adapter() -> SharedDateAdapter {
4668 Rc::new(
4669 FixtureDateAdapter::pinned(2024, 3, 14)
4670 .blocking(2024, 3, 8, "The workspace is frozen for the release.")
4671 .blocking(2024, 3, 20, "Nobody is on call that day."),
4672 )
4673 }
4674
4675 fn marks(day: crate::datetime::Day) -> Option<DayMark> {
4676 match day.0.rem_euclid(7) {
4677 0 => Some(DayMark::new("Two runs finished here").tone(Tone::Success)),
4678 3 => Some(DayMark::new("One run failed here").tone(Tone::Danger)),
4679 _ => None,
4680 }
4681 }
4682
4683 struct SceneDates {
4684 month: Entity<Calendar>,
4685 unknown: Entity<Calendar>,
4686 incomplete: Entity<RangePicker>,
4687 preview: Entity<RangePicker>,
4688 blocked: Entity<RangePicker>,
4689 field: Entity<DateInput>,
4690 refused: Entity<DateInput>,
4691 clock: Entity<TimeInput>,
4692 twelve: Entity<TimeInput>,
4693 }
4694
4695 impl Global for SceneDates {}
4696
4697 fn ensure(window: &mut Window, cx: &mut App) {
4698 if cx.has_global::<SceneDates>() {
4699 return;
4700 }
4701 let pinned = adapter();
4702 let unknown_adapter: SharedDateAdapter = Rc::new(FixtureDateAdapter::without_today());
4703 let march = FixtureDateAdapter::pinned(2024, 3, 14);
4704
4705 let month = cx.new(|cx| {
4706 Calendar::new("scene.calendar", pinned.clone(), window, cx)
4707 .selected([march.day(2024, 3, 14)])
4708 .overlay(marks)
4709 });
4710 let unknown = cx.new(|cx| {
4711 Calendar::new(
4712 "scene.calendar.unknown",
4713 unknown_adapter.clone(),
4714 window,
4715 cx,
4716 )
4717 });
4718 let incomplete = cx.new(|cx| {
4719 RangePicker::new("scene.range.incomplete", pinned.clone(), window, cx)
4720 .range(DayRange::starting(march.day(2024, 3, 11)))
4721 });
4722 let preview = cx.new(|cx| {
4723 RangePicker::new("scene.range.preview", pinned.clone(), window, cx)
4724 .range(DayRange::starting(march.day(2024, 3, 11)))
4725 });
4726 let blocked = cx.new(|cx| {
4727 RangePicker::new("scene.range.blocked", pinned.clone(), window, cx)
4728 .range(DayRange::new(march.day(2024, 3, 6), march.day(2024, 3, 9)))
4729 });
4730 let field = cx.new(|cx| {
4731 DateInput::new("scene.date.field", pinned.clone(), window, cx)
4732 .value(march.day(2024, 3, 14))
4733 });
4734 let refused = cx.new(|cx| DateInput::new("scene.date.refused", pinned.clone(), window, cx));
4735 let clock = cx.new(|cx| {
4736 TimeInput::new("scene.time.clock", pinned.clone(), window, cx)
4737 .value(TimeOfDay::new(9, 30).with_second(0))
4738 .seconds(true)
4739 });
4740 let twelve_adapter: SharedDateAdapter =
4741 Rc::new(FixtureDateAdapter::pinned(2024, 3, 14).twelve_hour(true));
4742 let twelve = cx.new(|cx| {
4743 TimeInput::new("scene.time.twelve", twelve_adapter, window, cx)
4744 .value(TimeOfDay::new(9, 30).with_meridiem(1))
4745 });
4746
4747 let hovered = march.day(2024, 3, 15);
4748 preview.update(cx, |picker, cx| {
4749 picker.calendar().update(cx, |calendar, cx| {
4750 calendar.set_hovered_day(Some(hovered), cx);
4751 });
4752 });
4753 let refused_field = refused.read(cx).field().clone();
4754 refused_field.update(cx, |input, cx| input.set_value("the fifth", cx));
4755
4756 cx.set_global(SceneDates {
4757 month,
4758 unknown,
4759 incomplete,
4760 preview,
4761 blocked,
4762 field,
4763 refused,
4764 clock,
4765 twelve,
4766 });
4767 }
4768
4769 pub(super) fn calendar(window: &mut Window, cx: &mut App) -> AnyElement {
4770 ensure(window, cx);
4771 let theme = cx.theme().clone();
4772 let dates = cx.global::<SceneDates>();
4773 let (month, unknown) = (dates.month.clone(), dates.unknown.clone());
4774 stack(&theme)
4775 .child(
4776 row(&theme)
4777 .items_start()
4778 .gap(px(theme.space(Space::Lg)))
4779 .child(month)
4780 .child(unknown),
4781 )
4782 .child(
4783 crate::foundation::text(
4784 &theme,
4785 TypeScale::Body,
4786 "Every weekday name, month name, and blocked reason above came from the \
4787 host. The calendar on the right has no today, so it draws no ring and \
4788 guesses no month.",
4789 )
4790 .max_w(px(560.0))
4791 .text_tone(&theme, TextTone::Muted),
4792 )
4793 .into_any_element()
4794 }
4795
4796 pub(super) fn date_range(window: &mut Window, cx: &mut App) -> AnyElement {
4797 ensure(window, cx);
4798 let theme = cx.theme().clone();
4799 let dates = cx.global::<SceneDates>();
4800 let (incomplete, preview, blocked) = (
4801 dates.incomplete.clone(),
4802 dates.preview.clone(),
4803 dates.blocked.clone(),
4804 );
4805 stack(&theme)
4806 .child(
4807 row(&theme)
4808 .items_start()
4809 .gap(px(theme.space(Space::Lg)))
4810 .child(incomplete)
4811 .child(preview)
4812 .child(blocked),
4813 )
4814 .into_any_element()
4815 }
4816
4817 pub(super) fn date_time(window: &mut Window, cx: &mut App) -> AnyElement {
4818 ensure(window, cx);
4819 let theme = cx.theme().clone();
4820 let dates = cx.global::<SceneDates>();
4821 let (field, refused, clock, twelve) = (
4822 dates.field.clone(),
4823 dates.refused.clone(),
4824 dates.clock.clone(),
4825 dates.twelve.clone(),
4826 );
4827 stack(&theme)
4828 .child(div().w(px(280.0)).child(field))
4829 .child(div().w(px(280.0)).child(refused))
4830 .child(
4831 row(&theme)
4832 .gap(px(theme.space(Space::Lg)))
4833 .child(clock)
4834 .child(twelve),
4835 )
4836 .child(
4837 crate::foundation::text(
4838 &theme,
4839 TypeScale::Body,
4840 "What the field could not read is still in it, and the refusal is the \
4841 adapter's own sentence.",
4842 )
4843 .max_w(px(560.0))
4844 .text_tone(&theme, TextTone::Muted),
4845 )
4846 .into_any_element()
4847 }
4848}
4849
4850#[cfg(feature = "fixtures")]
4851use dates::{calendar, date_range, date_time};
4852
4853const SCENE_DOCUMENT: &str = r#"# Release notes
4855
4856The build is **green** again, with *one* caveat and a ~~withdrawn~~ fix.
4857Details are in [the run log](https://example.test/runs/4821 "the failing run").
4858
4859## What changed
4860
4861- Retries are bounded
4862 - and the bound is stated
4863- Refusals keep their reason
4864
48651. Verify the workspace
48662. Publish the artifacts
4867
4868- [x] Bounded retries
4869- [ ] Bounded backoff
4870
4871> A refused request is displayed as a refusal.
4872
4873```rust
4874fn main() {
4875 println!("still green");
4876}
4877```
4878
4879| Stage | Result |
4880|:------|-------:|
4881| Build | passed |
4882| Test | passed |
4883
4884<div onclick="steal()">This was written as HTML.</div>
4885
4886
4887
4888---
4889
4890Everything below this line is what truncation leaves out."#;
4891
4892fn markdown(_window: &mut Window, cx: &mut App) -> AnyElement {
4893 let theme = cx.theme().clone();
4894 stack(&theme)
4895 .w(px(640.0))
4896 .child(Markdown::new("scene.markdown.document", SCENE_DOCUMENT).on_event(|_, _, _| {}))
4897 .child(Divider::new().id("scene.markdown.rule").label("Truncated"))
4898 .child(
4899 Markdown::new("scene.markdown.short", SCENE_DOCUMENT)
4900 .max_lines(4)
4901 .on_event(|_, _, _| {}),
4902 )
4903 .into_any_element()
4904}
4905
4906fn scene_thread() -> Vec<Message> {
4908 vec![
4909 Message::new("msg-open", "Is the release still blocked?")
4910 .author("Ada")
4911 .time("09:14")
4912 .delivery(DeliveryState::Read),
4913 Message::markdown(
4914 "msg-answer",
4915 "It is not. The **retry bound** landed, so the last failure is gone.",
4916 )
4917 .author("Grace")
4918 .time("09:15")
4919 .delivery(DeliveryState::Delivered)
4920 .reaction(Reaction::new("thumbs", "👍", 2)),
4921 Message::new("msg-log", "Attaching the run log.")
4922 .author("Grace")
4923 .time("09:15")
4924 .delivery(DeliveryState::Sent)
4925 .attachment(Attachment::new("run-4821", "run-4821.log").detail("12 KB")),
4926 Message::new("msg-queued", "Then I will publish the artifacts.")
4927 .author("Ada")
4928 .delivery(DeliveryState::Sending),
4929 Message::new("msg-refused", "Publishing the artifacts now.")
4930 .author("Ada")
4931 .time("09:16")
4932 .failed("The workspace is frozen for the release."),
4933 Message::markdown("msg-stream", "Checking the freeze window")
4934 .author("Assistant")
4935 .time("09:16")
4936 .streaming(true)
4937 .delivery(DeliveryState::Sending),
4938 ]
4939}
4940
4941fn conversation(_window: &mut Window, cx: &mut App) -> AnyElement {
4942 let theme = cx.theme().clone();
4943 stack(&theme)
4944 .w(px(560.0))
4945 .child(
4946 MessageList::new("scene.conversation.thread", scene_thread())
4947 .group_consecutive(true)
4948 .body_lines(2)
4949 .on_retry(|_, _, _| {})
4950 .on_markdown(|_, _, _, _| {}),
4951 )
4952 .child(
4953 Divider::new()
4954 .id("scene.conversation.rule")
4955 .label("Scrolled away from the newest message"),
4956 )
4957 .child(
4958 MessageList::new("scene.conversation.behind", scene_thread())
4962 .visible_rows(2)
4963 .body_lines(2)
4964 .on_retry(|_, _, _| {}),
4965 )
4966 .into_any_element()
4967}
4968
4969fn scene_picture(label: &'static str, cx: &App) -> AnyElement {
4974 let theme = cx.theme().clone();
4975 div()
4976 .size_full()
4977 .flex()
4978 .items_center()
4979 .justify_center()
4980 .bg(theme.colors.accent.opacity(0.35))
4981 .border(px(theme.borders.thick))
4982 .border_color(theme.colors.accent)
4983 .child(crate::foundation::text(
4984 &theme,
4985 TypeScale::Label,
4986 SharedString::new_static(label),
4987 ))
4988 .into_any_element()
4989}
4990
4991fn image_viewer(_window: &mut Window, cx: &mut App) -> AnyElement {
4992 let theme = cx.theme().clone();
4993 stack(&theme)
4994 .w(px(860.0))
4995 .child(
4996 div()
4997 .row()
4998 .items_start()
4999 .gap(px(theme.spacing.lg))
5000 .child(
5001 div().w(px(396.0)).child(
5002 ImageViewer::new(
5003 "scene.image.ready",
5004 [
5005 ImageFrame::new("graph", "The run graph")
5006 .source("runs/graph.png")
5007 .natural(1600, 900),
5008 ImageFrame::new("trace", "The failing trace")
5009 .source("runs/trace.png")
5010 .natural(1200, 1200),
5011 ],
5012 )
5013 .showing("graph")
5014 .fit(FitMode::Contain)
5015 .height(200.0)
5016 .image(|_, _, cx| Some(scene_picture("Supplied by the host", cx)))
5017 .on_event(|_, _, _| {}),
5018 ),
5019 )
5020 .child(
5021 div().w(px(396.0)).child(
5022 ImageViewer::new(
5023 "scene.image.refused",
5024 [ImageFrame::new("scan", "Page 4 of the scan")
5025 .source("scans/page-4.tiff")
5026 .natural(2480, 3508)
5027 .unavailable("The workspace is frozen for the release.")],
5028 )
5029 .height(200.0)
5030 .image(|_, _, cx| Some(scene_picture("Supplied by the host", cx)))
5031 .on_event(|_, _, _| {}),
5032 ),
5033 ),
5034 )
5035 .child(
5036 Divider::new()
5037 .id("scene.image.rule.unmeasured")
5038 .label("A size the host never stated"),
5039 )
5040 .child(
5041 div().w(px(396.0)).child(
5042 ImageViewer::new(
5043 "scene.image.unmeasured",
5044 [ImageFrame::new("sketch", "A pasted sketch").source("clipboard")],
5045 )
5046 .height(200.0)
5047 .image(|_, _, cx| Some(scene_picture("Size never stated", cx)))
5048 .on_event(|_, _, _| {}),
5049 ),
5050 )
5051 .into_any_element()
5052}
5053
5054fn transport(_window: &mut Window, cx: &mut App) -> AnyElement {
5055 let theme = cx.theme().clone();
5056 stack(&theme)
5057 .w(px(640.0))
5058 .child(
5059 TransportBar::new("scene.transport.playing")
5060 .label("Release walkthrough")
5061 .state(TransportState::Playing)
5062 .position(72.0)
5063 .duration(240.0)
5064 .elapsed("01:12")
5065 .remaining("-02:48")
5066 .buffered([BufferedRange::new(0.0, 156.0)])
5067 .volume(0.7)
5068 .speeds([1.0, 1.5, 2.0], 1.0)
5069 .step_seconds(10.0)
5070 .has_next(true)
5071 .on_event(|_, _, _| {}),
5072 )
5073 .child(
5074 Divider::new()
5075 .id("scene.transport.rule.live")
5076 .label("A stream nobody measured"),
5077 )
5078 .child(
5079 TransportBar::new("scene.transport.live")
5080 .label("Incident bridge")
5081 .state(TransportState::Playing)
5082 .position(1543.0)
5083 .unknown_duration()
5084 .elapsed("25:43")
5085 .volume(0.4)
5086 .on_event(|_, _, _| {}),
5087 )
5088 .child(
5089 Divider::new()
5090 .id("scene.transport.rule.stalled")
5091 .label("Playing and waiting"),
5092 )
5093 .child(
5094 TransportBar::new("scene.transport.stalled")
5095 .label("Release walkthrough")
5096 .state(TransportState::Buffering)
5097 .position(158.0)
5098 .duration(240.0)
5099 .elapsed("02:38")
5100 .remaining("-01:22")
5101 .buffered([BufferedRange::new(0.0, 160.0)])
5102 .volume(0.7)
5103 .muted(true)
5104 .on_event(|_, _, _| {}),
5105 )
5106 .into_any_element()
5107}
5108
5109const CUBE: &[u8] = include_bytes!("../assets/models/cube.gltf");
5113
5114fn scene_peaks(count: usize) -> Vec<f32> {
5118 (0..count)
5119 .map(|index| {
5120 let phase = index as f32 / count as f32;
5121 let swell = (phase * 11.0).sin() * 0.32 + (phase * 3.0).cos() * 0.28;
5122 (0.42 + swell).clamp(0.05, 1.0)
5123 })
5124 .collect()
5125}
5126
5127fn audio_player(_window: &mut Window, cx: &mut App) -> AnyElement {
5128 let theme = cx.theme().clone();
5129 stack(&theme)
5130 .w(px(640.0))
5131 .child(
5132 AudioPlayer::new("scene.audio.ready")
5133 .title("Release walkthrough")
5134 .subtitle("Recorded 12 March")
5135 .transport(
5136 FixtureTransport::ready(240.0)
5137 .position(72.0)
5138 .state(TransportState::Playing)
5139 .volume(0.7)
5140 .buffered([BufferedRange::new(0.0, 156.0)])
5141 .shared(),
5142 )
5143 .elapsed("01:12")
5144 .remaining("-02:48")
5145 .peaks(scene_peaks(96))
5146 .speeds([1.0, 1.5, 2.0])
5147 .step_seconds(10.0)
5148 .on_event(|_, _, _| {}),
5149 )
5150 .child(
5151 Divider::new()
5152 .id("scene.audio.rule.backend")
5153 .label("Nothing here can decode it"),
5154 )
5155 .child(
5156 AudioPlayer::new("scene.audio.absent")
5157 .title("interview.opus")
5158 .transport(
5159 FixtureTransport::new()
5160 .no_backend("There is no Opus decoder on this machine.")
5161 .shared(),
5162 )
5163 .on_event(|_, _, _| {}),
5164 )
5165 .child(
5166 Divider::new()
5167 .id("scene.audio.rule.none")
5168 .label("No player connected at all"),
5169 )
5170 .child(
5171 AudioPlayer::new("scene.audio.none")
5172 .title("Standup recording")
5173 .on_event(|_, _, _| {}),
5174 )
5175 .into_any_element()
5176}
5177
5178fn video_player(_window: &mut Window, cx: &mut App) -> AnyElement {
5179 let theme = cx.theme().clone();
5180 stack(&theme)
5181 .w(px(440.0))
5182 .child(
5183 VideoPlayer::new("scene.video.frame")
5184 .title("Screen capture")
5185 .transport(
5186 FixtureTransport::ready(96.0)
5187 .position(24.0)
5188 .state(TransportState::Playing)
5189 .volume(0.6)
5190 .shared(),
5191 )
5192 .frame(|_, cx| Some(scene_picture("Supplied frame", cx)))
5193 .elapsed("00:24")
5194 .remaining("-01:12")
5195 .on_event(|_, _, _| {}),
5196 )
5197 .child(
5198 Divider::new()
5199 .id("scene.video.rule.poster")
5200 .label("Ready, and no frames supplied"),
5201 )
5202 .child(
5203 VideoPlayer::new("scene.video.poster")
5204 .title("Release walkthrough")
5205 .transport(FixtureTransport::ready(240.0).shared())
5206 .poster(|_, cx| Some(scene_picture("Poster", cx)))
5207 .elapsed("00:00")
5208 .remaining("-04:00")
5209 .on_event(|_, _, _| {}),
5210 )
5211 .into_any_element()
5212}
5213
5214fn model_viewer(_window: &mut Window, cx: &mut App) -> AnyElement {
5215 let theme = cx.theme().clone();
5216 let read = || ModelState::read(CUBE, ModelBounds::default());
5217 stack(&theme)
5218 .w(px(720.0))
5219 .child(
5220 div()
5221 .row()
5222 .items_start()
5223 .gap(px(theme.spacing.lg))
5224 .child(
5225 div().flex_1().min_w_0().child(
5226 ModelViewer::new("scene.model.flat")
5227 .title("cube.gltf")
5228 .state(read())
5229 .orbit(0.62, 0.42)
5230 .height(240.0)
5231 .on_event(|_, _, _| {}),
5232 ),
5233 )
5234 .child(
5235 div().flex_1().min_w_0().child(
5236 ModelViewer::new("scene.model.wireframe")
5237 .title("cube.gltf")
5238 .state(read())
5239 .shading(ModelShading::Wireframe)
5240 .orbit(0.62, 0.42)
5241 .height(240.0)
5242 .on_event(|_, _, _| {}),
5243 ),
5244 ),
5245 )
5246 .child(
5247 Divider::new()
5248 .id("scene.model.rule.refused")
5249 .label("A document past a bound"),
5250 )
5251 .child(
5252 ModelViewer::new("scene.model.refused")
5253 .title("city-block.glb")
5254 .rejected(ModelError::TooLarge {
5255 limit: ModelLimit::Triangles,
5256 found: 412_930,
5257 allowed: 24_576,
5258 })
5259 .height(160.0)
5260 .on_event(|_, _, _| {}),
5261 )
5262 .into_any_element()
5263}
5264
5265struct SceneApprovals {
5266 pending: Entity<ApprovalPrompt>,
5267 declined: Entity<ApprovalPrompt>,
5268 expired: Entity<ApprovalPrompt>,
5269 superseded: Entity<ApprovalPrompt>,
5270}
5271
5272impl Global for SceneApprovals {}
5273
5274fn approval(window: &mut Window, cx: &mut App) -> AnyElement {
5275 if !cx.has_global::<SceneApprovals>() {
5276 let pending = cx.new(|cx| {
5277 ApprovalPrompt::new(
5278 "scene.approval.pending",
5279 "Write to /work/report/summary.md",
5280 window,
5281 cx,
5282 )
5283 .details([
5284 DescriptionItem::new("tool", "Tool", "write-file"),
5285 DescriptionItem::new("path", "Path", "/work/report/summary.md"),
5286 DescriptionItem::new("bytes", "Size", "4 KB"),
5287 ])
5288 .always(AlwaysScope::Session)
5289 .always(AlwaysScope::path("/work/report"))
5290 .always(AlwaysScope::tool("write-file"))
5291 });
5292 let declined = cx.new(|cx| {
5293 ApprovalPrompt::new(
5294 "scene.approval.declined",
5295 "Delete /work/report/draft.md",
5296 window,
5297 cx,
5298 )
5299 .status(ApprovalStatus::Declined)
5300 });
5301 let expired = cx.new(|cx| {
5302 ApprovalPrompt::new(
5303 "scene.approval.expired",
5304 "Open a connection to build.internal:8443",
5305 window,
5306 cx,
5307 )
5308 .status(ApprovalStatus::Expired)
5309 });
5310 let superseded = cx.new(|cx| {
5311 ApprovalPrompt::new(
5312 "scene.approval.superseded",
5313 "Run the test suite in /work",
5314 window,
5315 cx,
5316 )
5317 .status(ApprovalStatus::Superseded {
5318 by: "a later request covering the whole workspace".into(),
5319 })
5320 });
5321 cx.set_global(SceneApprovals {
5322 pending,
5323 declined,
5324 expired,
5325 superseded,
5326 });
5327 }
5328 let prompts = cx.global::<SceneApprovals>();
5329 let pending = prompts.pending.clone();
5330 let declined = prompts.declined.clone();
5331 let expired = prompts.expired.clone();
5332 let superseded = prompts.superseded.clone();
5333 let theme = cx.theme().clone();
5334
5335 stack(&theme)
5336 .w(px(560.0))
5337 .child(pending)
5338 .child(
5339 Divider::new()
5340 .id("scene.approval.rule.resolved")
5341 .label("Answered, expired, and replaced are three different things"),
5342 )
5343 .child(declined)
5344 .child(expired)
5345 .child(superseded)
5346 .into_any_element()
5347}
5348
5349fn permission_matrix(_window: &mut Window, cx: &mut App) -> AnyElement {
5350 let theme = cx.theme().clone();
5351 let actions = [
5352 PermissionAction::new("read", "Read files"),
5353 PermissionAction::new("write", "Write files"),
5354 PermissionAction::new("network", "Reach the network"),
5355 ];
5356 let subjects = [
5357 PermissionSubject::new("workspace", "This workspace")
5358 .cell("read", PermissionEntry::new(PermissionState::Allowed))
5359 .cell("write", PermissionEntry::new(PermissionState::Ask))
5360 .cell(
5361 "network",
5362 PermissionEntry::inherited(PermissionState::Denied, "the organisation policy"),
5363 ),
5364 PermissionSubject::new("scratch", "The scratch directory")
5365 .cell(
5366 "read",
5367 PermissionEntry::inherited(PermissionState::Allowed, "this workspace"),
5368 )
5369 .cell("write", PermissionEntry::new(PermissionState::Allowed))
5370 .cell(
5371 "network",
5372 PermissionEntry::inherited(PermissionState::Denied, "the organisation policy"),
5373 ),
5374 PermissionSubject::new("calculator", "The calculator tool")
5377 .cell("network", PermissionEntry::new(PermissionState::Denied)),
5378 ];
5379
5380 stack(&theme)
5381 .w(px(720.0))
5382 .child(
5383 PermissionMatrix::new("scene.permission.editable")
5384 .actions(actions.clone())
5385 .subjects(subjects.clone())
5386 .on_change(|_change, _window, _cx| {}),
5387 )
5388 .child(
5389 Divider::new()
5390 .id("scene.permission.rule.read-only")
5391 .label("The same permissions, shown rather than offered"),
5392 )
5393 .child(
5394 PermissionMatrix::new("scene.permission.read-only")
5395 .actions(actions)
5396 .subjects(subjects),
5397 )
5398 .into_any_element()
5399}
5400
5401fn cost_meter(_window: &mut Window, cx: &mut App) -> AnyElement {
5402 let theme = cx.theme().clone();
5403 stack(&theme)
5404 .w(px(520.0))
5405 .child(
5406 CostMeter::new("scene.cost.meter")
5407 .label("This run")
5408 .line(CostLine::new(
5409 "spend",
5410 "Spend",
5411 Reading::measured(1.24, "1.24 credits"),
5412 ))
5413 .line(CostLine::new(
5414 "projected",
5415 "Projected at this rate",
5416 Reading::estimated(4.0, "4.00 credits"),
5417 ))
5418 .line(
5419 CostLine::new(
5420 "account",
5421 "Account balance",
5422 Reading::measured(112.0, "112.00 credits"),
5423 )
5424 .stale(LastVerified::at("09:41 today")),
5425 )
5426 .line(CostLine::new(
5427 "storage",
5428 "Storage",
5429 Reading::unavailable_because("The billing host refused the request."),
5430 )),
5431 )
5432 .child(
5433 Divider::new()
5434 .id("scene.cost.rule.gauge")
5435 .label("A limit that is known, and one that is not"),
5436 )
5437 .child(
5438 ContextGauge::new(
5439 "scene.cost.context.known",
5440 Reading::measured(48_000.0, "48,000 tokens"),
5441 )
5442 .label("Context used")
5443 .limit(Limit::measured(128_000.0, "128,000 tokens")),
5444 )
5445 .child(
5446 ContextGauge::new(
5447 "scene.cost.context.unknown",
5448 Reading::estimated(48_000.0, "48,000 tokens"),
5449 )
5450 .label("Context used"),
5451 )
5452 .child(
5453 ContextGauge::new("scene.cost.context.unavailable", Reading::unavailable())
5454 .label("Context used")
5455 .limit(Limit::measured(128_000.0, "128,000 tokens")),
5456 )
5457 .into_any_element()
5458}
5459
5460fn scene_arguments() -> ToolBody {
5461 ToolBody::new(
5462 "{\n \"path\": \"docs/coverage.md\",\n \"pattern\": \"unknown\",\n \"limit\": 20\n}",
5463 )
5464 .max_lines(2)
5465}
5466
5467fn tool_call(_window: &mut Window, cx: &mut App) -> AnyElement {
5473 let theme = cx.theme().clone();
5474 div()
5475 .flex()
5476 .gap(px(theme.spacing.lg))
5477 .child(
5478 stack(&theme).w(px(460.0)).child(
5479 ToolCallCard::new("scene.tool.pending", "workspace.search")
5480 .arguments(scene_arguments())
5481 .state(ToolCallState::PendingApproval),
5482 )
5483 .child(
5484 ToolCallCard::new("scene.tool.running", "workspace.index")
5485 .arguments("{ \"root\": \"crates\" }")
5486 .state(ToolCallState::Running)
5487 .elapsed("4.2 s"),
5488 )
5489 .child(
5490 ToolCallCard::new("scene.tool.succeeded", "workspace.read")
5491 .arguments("{ \"path\": \"README.md\" }")
5492 .state(ToolCallState::succeeded(
5493 ToolBody::new(
5494 "# GPUI Box\n\nProduct-neutral components.\n\nEvery word is replaceable.",
5495 )
5496 .max_lines(2),
5497 ))
5498 .elapsed("0.3 s"),
5499 )
5500 .child(
5501 ToolCallCard::new("scene.tool.silent", "workspace.touch")
5502 .arguments("{ \"path\": \"notes.md\" }")
5503 .state(ToolCallState::succeeded_silently())
5504 .elapsed("0.1 s"),
5505 ),
5506 )
5507 .child(
5508 stack(&theme)
5509 .w(px(460.0))
5510 .child(
5511 ToolCallCard::new("scene.tool.failed", "workspace.write")
5512 .arguments("{ \"path\": \"/read-only/notes.md\" }")
5513 .state(ToolCallState::failed(
5514 "The file system reported that the path is read only.",
5515 ))
5516 .elapsed("0.2 s")
5517 .on_retry(|_, _| {}),
5518 )
5519 .child(
5522 ToolCallCard::new("scene.tool.refused", "shell.run")
5523 .arguments("{ \"command\": \"rm -rf build\" }")
5524 .state(ToolCallState::refused(
5525 "This workspace does not allow shell commands.",
5526 )),
5527 ),
5528 )
5529 .into_any_element()
5530}
5531
5532fn step_list(_window: &mut Window, cx: &mut App) -> AnyElement {
5533 let theme = cx.theme().clone();
5534 stack(&theme)
5535 .child(caption(&theme, "A run somebody counted"))
5536 .child(
5537 StepList::new("scene.steps.counted")
5538 .step(Step::new("read", "Read the brief").state(StepState::Done))
5539 .step(
5540 Step::new("search", "Search the workspace")
5541 .state(StepState::Running)
5542 .body(
5543 ToolCallCard::new("scene.steps.search.call", "workspace.search")
5544 .arguments(scene_arguments())
5545 .state(ToolCallState::Running)
5546 .elapsed("1.1 s"),
5547 ),
5548 )
5549 .step(Step::new("summarise", "Summarise what was found"))
5550 .step(
5551 Step::new("publish", "Publish the summary").state(StepState::Skipped(
5552 "Publishing is turned off for this workspace.".into(),
5553 )),
5554 )
5555 .step(
5556 Step::new("notify", "Notify the reviewers").state(StepState::Failed(
5560 "The notification service did not respond.".into(),
5561 )),
5562 ),
5563 )
5564 .child(caption(&theme, "A run still being decided"))
5565 .child(
5566 StepList::new("scene.steps.open")
5567 .length(RunLength::Unknown)
5568 .step(Step::new("read", "Read the brief").state(StepState::Done))
5569 .step(Step::new("plan", "Decide what to do next").state(StepState::Running)),
5570 )
5571 .into_any_element()
5572}
5573
5574fn thinking(_window: &mut Window, cx: &mut App) -> AnyElement {
5575 let theme = cx.theme().clone();
5576 stack(&theme)
5577 .child(ThinkingBlock::new(
5578 "scene.thinking.collapsed",
5579 Reasoning::present("The brief asks for two files, so read both before answering."),
5580 ))
5581 .child(
5584 ThinkingBlock::new(
5585 "scene.thinking.working",
5586 Reasoning::present("Reading the second file before answering."),
5587 )
5588 .thinking(true),
5589 )
5590 .child(
5591 ThinkingBlock::new(
5592 "scene.thinking.open",
5593 Reasoning::present(
5594 "The brief asks for two files.\nRead both before answering.\nThen summarise.",
5595 ),
5596 )
5597 .expanded(true)
5598 .on_toggle(|_, _, _| {}),
5599 )
5600 .child(ThinkingBlock::new(
5603 "scene.thinking.withheld",
5604 Reasoning::withheld("This connection does not hand over reasoning."),
5605 ))
5606 .child(ThinkingBlock::new(
5607 "scene.thinking.absent",
5608 Reasoning::Absent,
5609 ))
5610 .into_any_element()
5611}
5612
5613fn scene_document() -> JsonValue {
5618 JsonValue::object([
5619 ("id", JsonValue::string("run-4812")),
5620 ("attempts", JsonValue::number("3")),
5621 ("streaming", JsonValue::Bool(true)),
5622 ("cursor", JsonValue::Null),
5625 ("labels", JsonValue::object(Vec::<(&str, JsonValue)>::new())),
5626 (
5627 "credentials",
5628 JsonValue::object([("token", JsonValue::redacted("51 characters"))]),
5629 ),
5630 (
5631 "request",
5632 JsonValue::object([
5633 ("method", JsonValue::string("POST")),
5634 (
5635 "headers",
5636 JsonValue::object([
5637 ("content-type", JsonValue::string("application/json")),
5638 ("authorization", JsonValue::redacted("a value")),
5639 ]),
5640 ),
5641 ]),
5642 ),
5643 (
5644 "steps",
5645 JsonValue::array([
5646 JsonValue::string("plan"),
5647 JsonValue::string("apply"),
5648 JsonValue::object([("retries", JsonValue::number("0"))]),
5649 ]),
5650 ),
5651 ])
5652}
5653
5654fn json_view(_window: &mut Window, cx: &mut App) -> AnyElement {
5655 let theme = cx.theme().clone();
5656 stack(&theme)
5657 .w(px(520.0))
5658 .child(
5659 JsonView::new("scene.json", scene_document())
5660 .expanded_paths(&["credentials", "request", "request/headers", "steps"])
5661 .selected("request/method")
5662 .on_toggle(|_, _, _, _| {})
5663 .on_select(|_, _, _| {}),
5664 )
5665 .into_any_element()
5666}
5667
5668struct SceneSchemaForm {
5673 form: Entity<SchemaForm>,
5674}
5675
5676impl Global for SceneSchemaForm {}
5677
5678fn scene_schema() -> Schema {
5679 Schema::new()
5680 .field(
5681 SchemaField::new(
5682 "path",
5683 SchemaKind::Text {
5684 placeholder: Some("relative to the workspace".into()),
5685 secret: false,
5686 },
5687 )
5688 .label("File")
5689 .description("Which file the call reads")
5690 .required(true),
5691 )
5692 .field(
5693 SchemaField::new(
5694 "max_bytes",
5695 SchemaKind::Integer(NumberBounds::new().min(1.0).max(65_536.0).step(1024.0)),
5696 )
5697 .label("Maximum bytes"),
5698 )
5699 .field(
5700 SchemaField::new("follow_symlinks", SchemaKind::Boolean).label("Follow symbolic links"),
5701 )
5702 .field(
5703 SchemaField::new(
5704 "encoding",
5705 SchemaKind::Enum(vec![
5706 SchemaChoice::new("utf-8", "UTF-8"),
5707 SchemaChoice::new("latin-1", "Latin-1"),
5708 ]),
5709 )
5710 .label("Encoding")
5711 .required(true),
5712 )
5713 .field(
5714 SchemaField::new(
5715 "profile",
5716 SchemaKind::OpenEnum(vec![
5717 SchemaChoice::new("fast", "Fast"),
5718 SchemaChoice::new("thorough", "Thorough"),
5719 ]),
5720 )
5721 .label("Profile")
5722 .description("One of these, or whatever you type"),
5723 )
5724 .field(SchemaField::new("tags", SchemaKind::TextList { max: Some(4) }).label("Tags"))
5725 .field(
5726 SchemaField::new(
5727 "limits",
5728 SchemaKind::Object(vec![
5729 SchemaField::new("timeout_ms", SchemaKind::Integer(NumberBounds::new()))
5730 .label("Timeout in milliseconds"),
5731 ]),
5732 )
5733 .label("Limits"),
5734 )
5735 .field(
5736 SchemaField::new(
5737 "matcher",
5738 SchemaKind::Unrenderable(
5739 "This argument is one of three shapes at once, and no single control \
5740 stands for that."
5741 .into(),
5742 ),
5743 )
5744 .label("Matcher")
5745 .required(true),
5746 )
5747}
5748
5749fn schema_form(window: &mut Window, cx: &mut App) -> AnyElement {
5750 if !cx.has_global::<SceneSchemaForm>() {
5751 let form = cx.new(|cx| SchemaForm::new("scene.schema", scene_schema(), window, cx));
5752 form.update(cx, |form, cx| {
5753 form.set_error("path", "That path is outside the workspace.", cx);
5756 });
5757 cx.set_global(SceneSchemaForm { form });
5758 }
5759 let form = cx.global::<SceneSchemaForm>().form.clone();
5760 let theme = cx.theme().clone();
5761 stack(&theme).w(px(520.0)).child(form).into_any_element()
5762}
5763
5764fn server_list(_window: &mut Window, cx: &mut App) -> AnyElement {
5765 let theme = cx.theme().clone();
5766 stack(&theme)
5767 .w(px(560.0))
5768 .child(
5769 ServerList::new("scene.servers")
5774 .expanded_ids(&["workspace"])
5775 .selected("workspace")
5776 .servers([
5777 ServerEntry::new("workspace", "Workspace tools")
5778 .detail("Running beside this window")
5779 .state(ServerState::Connected)
5780 .offers([
5781 Offering::tool("read", "Read a file")
5782 .summary("Returns the contents of one file")
5783 .qualifier("path, max_bytes"),
5784 Offering::tool("write", "Write a file")
5785 .summary("Replaces the contents of one file"),
5786 Offering::skill("review", "Review a change")
5787 .summary("Reads a diff and reports what it finds"),
5788 Offering::resource("changelog", "Changelog")
5789 .qualifier("workspace:/CHANGELOG.md"),
5790 ]),
5791 ServerEntry::new("index", "Search index")
5792 .detail("Answered, and the answer was empty")
5793 .state(ServerState::Connected)
5794 .offers([]),
5795 ServerEntry::new("archive", "Archive")
5796 .detail("Nobody has asked it anything yet")
5797 .state(ServerState::Connected),
5798 ServerEntry::new("build", "Build runner")
5799 .state(ServerState::Connecting)
5800 .catalog(Catalog::Asking),
5801 ServerEntry::new("notes", "Notes").state(ServerState::Disconnected),
5802 ServerEntry::new("deploy", "Deployment").state(ServerState::Failed {
5803 reason: "The connection was refused after three attempts.".into(),
5804 }),
5805 ServerEntry::new("telemetry", "Telemetry").state(ServerState::Disabled {
5806 reason: Some("You turned this one off.".into()),
5807 }),
5808 ])
5809 .on_select(|_, _, _| {})
5810 .on_retry(|_, _, _| {})
5811 .on_toggle(|_, _, _, _| {}),
5812 )
5813 .into_any_element()
5814}
5815
5816fn offering_catalog(_window: &mut Window, cx: &mut App) -> AnyElement {
5817 let theme = cx.theme().clone();
5818 let workspace = OfferingSource::new(
5819 "workspace",
5820 "Workspace tools",
5821 OfferingSourceState::Ready(vec![
5822 SearchableOffering::new(
5823 Offering::tool("read", "Read a file")
5824 .summary("Returns the contents of one file")
5825 .qualifier("path, max_bytes"),
5826 "read file contents path workspace",
5827 ),
5828 SearchableOffering::new(
5829 Offering::skill("review", "Review a change")
5830 .summary("Reads a diff and reports what it finds"),
5831 "review change diff findings",
5832 ),
5833 ]),
5834 );
5835 let archive = OfferingSource::new(
5836 "archive",
5837 "Archive tools",
5838 OfferingSourceState::Stale {
5839 offerings: vec![
5840 SearchableOffering::new(
5841 Offering::tool("read", "Read a file").summary("Reads one archived file"),
5842 "read archived file contents",
5843 ),
5844 SearchableOffering::new(
5845 Offering::resource("changelog", "Changelog").qualifier("archive:/CHANGELOG.md"),
5846 "changelog changes release history",
5847 ),
5848 ],
5849 reason: "Archive refresh failed; showing the last verified results.".into(),
5850 },
5851 );
5852 stack(&theme)
5853 .w(px(560.0))
5854 .child(
5855 OfferingCatalog::new("scene.offering-catalog")
5856 .sources([workspace, archive])
5857 .selected(OfferingIdentity::new("workspace", "read"))
5858 .on_activate(|_, _, _| {}),
5859 )
5860 .into_any_element()
5861}
5862
5863struct SceneOrdinary {
5865 menubar: Entity<Menubar>,
5866 hover_card: Entity<HoverCard>,
5867 copy_idle: Entity<CopyButton>,
5868 copy: Entity<CopyButton>,
5869 copy_refused: Entity<CopyButton>,
5870}
5871
5872impl Global for SceneOrdinary {}
5873
5874fn ensure_ordinary(window: &mut Window, cx: &mut App) {
5875 if cx.has_global::<SceneOrdinary>() {
5876 return;
5877 }
5878 let menubar = cx.new(|cx| {
5879 Menubar::new(
5880 "scene.menubar",
5881 [
5882 MenubarMenu::new(
5883 "file",
5884 "File",
5885 [
5886 MenuItem::command("file.new", "New run").shortcut("cmd-n"),
5887 MenuItem::command("file.open", "Open workspace").shortcut("cmd-o"),
5888 MenuItem::separator("file.rule"),
5889 MenuItem::submenu(
5890 "file.export",
5891 "Export",
5892 [
5893 MenuItem::command("file.export.json", "As JSON"),
5894 MenuItem::command("file.export.text", "As plain text"),
5895 ],
5896 ),
5897 ],
5898 ),
5899 MenubarMenu::new(
5900 "edit",
5901 "Edit",
5902 [
5903 MenuItem::command("edit.undo", "Undo").shortcut("cmd-z"),
5904 MenuItem::check("edit.wrap", "Wrap lines", true),
5905 ],
5906 ),
5907 MenubarMenu::new("view", "View", [MenuItem::command("view.zoom", "Zoom in")]),
5908 MenubarMenu::new("policy", "Policy", []).disabled(true),
5909 ],
5910 window,
5911 cx,
5912 )
5913 });
5914 menubar.update(cx, |bar, cx| bar.open("file", window, cx));
5915
5916 let hover_card = cx.new(|cx| {
5917 HoverCard::new("scene.hover-card", window, cx)
5918 .name("Run 4821")
5919 .trigger(|_, cx| {
5920 let theme = cx.theme().clone();
5921 crate::foundation::text(&theme, TypeScale::Caption, "run 4821")
5922 .text_color(theme.colors.accent)
5923 .into_any_element()
5924 })
5925 .content(|_, cx| {
5926 let theme = cx.theme().clone();
5927 div()
5928 .column()
5929 .items_start()
5933 .gap(px(theme.spacing.xs))
5934 .child(crate::foundation::text(
5935 &theme,
5936 TypeScale::Strong,
5937 "Nightly regression sweep",
5938 ))
5939 .child(
5940 crate::foundation::text(
5941 &theme,
5942 TypeScale::Caption,
5943 "Finished in 4 minutes, 12 checks, none failed.",
5944 )
5945 .text_tone(&theme, TextTone::Muted),
5946 )
5947 .child(Badge::new("Ready").success())
5948 .into_any_element()
5949 })
5950 });
5951 hover_card.update(cx, |card, cx| card.open(cx));
5952
5953 let copy_idle = cx.new(|cx| {
5956 CopyButton::new("scene.copy-idle", window, cx)
5957 .text("run-4821-9f3a")
5958 .copier(|_, _| Ok(()))
5959 });
5960
5961 let copy = cx.new(|cx| {
5965 CopyButton::new("scene.copy", window, cx)
5966 .text("run-4821-9f3a")
5967 .confirmation(std::time::Duration::from_secs(60 * 60))
5968 .copier(|_, _| Ok(()))
5969 });
5970 copy.update(cx, |button, cx| button.copy(cx));
5971
5972 let copy_refused = cx.new(|cx| {
5973 CopyButton::new("scene.copy-refused", window, cx)
5974 .text("run-4821-9f3a")
5975 .copier(|_, _| Err("The clipboard did not take it.".into()))
5976 });
5977 copy_refused.update(cx, |button, cx| button.copy(cx));
5978
5979 cx.set_global(SceneOrdinary {
5980 menubar,
5981 hover_card,
5982 copy_idle,
5983 copy,
5984 copy_refused,
5985 });
5986}
5987
5988fn toggle(_window: &mut Window, cx: &mut App) -> AnyElement {
5989 let theme = cx.theme().clone();
5990 stack(&theme)
5991 .child(caption(&theme, "A button that stays in"))
5992 .child(
5993 row(&theme)
5994 .child(
5995 Toggle::new("scene.toggle.bold")
5996 .label("Bold")
5997 .pressed(true)
5998 .on_press(|_, _, _| {}),
5999 )
6000 .child(
6001 Toggle::new("scene.toggle.italic")
6002 .label("Italic")
6003 .on_press(|_, _, _| {}),
6004 )
6005 .child(
6006 Toggle::new("scene.toggle.review")
6007 .label("Review mode")
6008 .secondary()
6009 .pressed(true)
6010 .on_press(|_, _, _| {}),
6011 )
6012 .child(
6013 Toggle::new("scene.toggle.locked")
6014 .label("Locked")
6015 .disabled(true),
6016 ),
6017 )
6018 .child(caption(&theme, "Any number in at once"))
6019 .child(
6020 ToggleGroup::new("scene.toggle-group.format")
6021 .label("Formatting")
6022 .selection(ToggleSelection::Any)
6023 .items([
6024 ToggleItem::new("bold", "Bold"),
6025 ToggleItem::new("italic", "Italic"),
6026 ToggleItem::new("underline", "Underline").disabled(true),
6027 ])
6028 .pressed_ids(&["bold", "italic"])
6029 .on_change(|_, _, _, _| {}),
6030 )
6031 .child(caption(
6032 &theme,
6033 "One or none, which a segmented strip cannot say",
6034 ))
6035 .child(
6036 ToggleGroup::new("scene.toggle-group.density")
6037 .label("Density")
6038 .selection(ToggleSelection::AtMostOne)
6039 .items([
6040 ToggleItem::new("compact", "Compact"),
6041 ToggleItem::new("cosy", "Cosy"),
6042 ToggleItem::new("roomy", "Roomy"),
6043 ])
6044 .pressed_ids(&["cosy"])
6045 .on_change(|_, _, _, _| {}),
6046 )
6047 .into_any_element()
6048}
6049
6050fn collapsible(_window: &mut Window, cx: &mut App) -> AnyElement {
6051 let theme = cx.theme().clone();
6052 stack(&theme)
6053 .w(px(520.0))
6054 .child(
6055 Collapsible::new("scene.collapsible.open", "Advanced")
6056 .description("Settings most runs never touch")
6057 .open(true)
6058 .body(crate::foundation::text(
6059 &theme,
6060 TypeScale::Body,
6061 "Requests go out over the system proxy.",
6062 ))
6063 .on_toggle(|_, _, _| {}),
6064 )
6065 .child(
6066 Collapsible::new("scene.collapsible.shut", "Diagnostics")
6067 .description("Nothing is collected until this is opened")
6068 .body(crate::foundation::text(
6069 &theme,
6070 TypeScale::Body,
6071 "This body is absent from the tree while it is shut.",
6072 ))
6073 .on_toggle(|_, _, _| {}),
6074 )
6075 .child(
6076 Collapsible::new("scene.collapsible.refused", "Managed by policy")
6077 .description("This machine cannot change these")
6078 .disabled(true)
6079 .body(crate::foundation::text(
6080 &theme,
6081 TypeScale::Body,
6082 "Set by the administrator.",
6083 )),
6084 )
6085 .into_any_element()
6086}
6087
6088fn hover_card(window: &mut Window, cx: &mut App) -> AnyElement {
6089 ensure_ordinary(window, cx);
6090 let card = cx.global::<SceneOrdinary>().hover_card.clone();
6091 let theme = cx.theme().clone();
6092 stack(&theme)
6093 .w(px(460.0))
6094 .h(px(300.0))
6095 .child(caption(&theme, "A preview the pointer can travel into"))
6096 .child(
6097 row(&theme)
6098 .child(crate::foundation::text(
6099 &theme,
6100 TypeScale::Label,
6101 "Reported by",
6102 ))
6103 .child(card),
6104 )
6105 .into_any_element()
6106}
6107
6108fn menubar(window: &mut Window, cx: &mut App) -> AnyElement {
6109 ensure_ordinary(window, cx);
6110 let bar = cx.global::<SceneOrdinary>().menubar.clone();
6111 let theme = cx.theme().clone();
6112 stack(&theme)
6113 .w(px(560.0))
6114 .h(px(360.0))
6115 .child(bar)
6116 .into_any_element()
6117}
6118
6119fn copy_button(window: &mut Window, cx: &mut App) -> AnyElement {
6120 ensure_ordinary(window, cx);
6121 let scene = cx.global::<SceneOrdinary>();
6122 let idle = scene.copy_idle.clone();
6123 let copied = scene.copy.clone();
6124 let refused = scene.copy_refused.clone();
6125 let theme = cx.theme().clone();
6126 stack(&theme)
6127 .child(caption(&theme, "Nobody has pressed it yet"))
6128 .child(idle)
6129 .child(caption(&theme, "The clipboard took it"))
6130 .child(copied)
6131 .child(caption(&theme, "It did not go through, and says so"))
6132 .child(refused)
6133 .into_any_element()
6134}
6135
6136fn aspect_ratio(_window: &mut Window, cx: &mut App) -> AnyElement {
6137 let theme = cx.theme().clone();
6138 let filled = |label: &'static str| {
6139 div()
6140 .size_full()
6141 .flex()
6142 .items_center()
6143 .justify_center()
6144 .bg(theme.colors.hover)
6145 .child(
6146 crate::foundation::text(&theme, TypeScale::Label, label)
6147 .text_tone(&theme, TextTone::Muted),
6148 )
6149 };
6150 stack(&theme)
6151 .child(caption(&theme, "Width given, height from the ratio"))
6152 .child(
6153 div().w(px(320.0)).child(
6154 AspectRatio::of("scene.aspect.wide", 16.0, 9.0)
6155 .width_driven()
6156 .child(filled("16 by 9")),
6157 ),
6158 )
6159 .child(caption(&theme, "Height given, width from the ratio"))
6160 .child(
6161 div().h(px(120.0)).child(
6162 AspectRatio::new("scene.aspect.square", 1.0)
6163 .height_driven()
6164 .child(filled("square")),
6165 ),
6166 )
6167 .into_any_element()
6168}
6169
6170#[cfg(test)]
6171mod tests {
6172 use super::*;
6173
6174 #[test]
6175 fn scene_names_are_unique_and_addressable() {
6176 let mut names: Vec<&str> = catalog().iter().map(|scene| scene.name).collect();
6177 let count = names.len();
6178 names.sort_unstable();
6179 names.dedup();
6180 assert_eq!(names.len(), count);
6181 assert!(find("button").is_some());
6182 assert!(find("nothing").is_none());
6183 }
6184}