Skip to main content

gpui_kit/
scenes.rs

1//! One canonical rendering per component.
2//!
3//! A scene is the single description of a component's states that both the
4//! gallery and the audit tests consume, so a component cannot be reviewed
5//! visually in one arrangement and tested in another.
6
7use 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
33/// One canonical rendering, addressed by name.
34pub 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
48/// Every scene, in a stable order.
49pub 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
455/// The scene registered under `name`, or `None` when nothing is.
456pub fn find(name: &str) -> Option<Scene> {
457    catalog().into_iter().find(|scene| scene.name == name)
458}
459
460/// The reading direction a scene expects.
461///
462/// The direction is a global, like the theme, so a capture run that renders
463/// the whole catalog into one process has to set it per scene the same way it
464/// activates a theme per scene. A scene that changed it while rendering would
465/// leak into whichever scene came next, and the leak would show up as a
466/// changed image somewhere else entirely.
467pub 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
620/// A live processing graph with the visual states and editor gestures shown
621/// together: traffic lanes, a running shockwave, explicit ports, labels,
622/// feedback routing, pan, zoom, node movement, and connection creation.
623fn 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
774/// The complete shell contract: every state remains visibly distinct, and
775/// the narrow panels prove long addresses and page content stay clipped to
776/// the caller-owned viewport.
777fn 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
993/// The dialog the scene shows, kept across frames.
994///
995/// A dialog owns whether it is open and which element had the keyboard before
996/// it opened, so rebuilding it every frame would reopen it every frame.
997struct 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        // Hover help only exists while a pointer rests on the control, so the
1051        // surface itself is also shown outright, where it can be reviewed.
1052        .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
1061/// The menu family the scenes show, kept across frames.
1062///
1063/// Each of these owns whether it is open, where the keyboard is, and which
1064/// submenu stands expanded, so rebuilding them every frame would reopen them
1065/// every frame. Building them once is also what makes the capture static.
1066struct 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        // The trigger keeps its place while the surface is open, because the
1190        // surface is anchored to it rather than laid out beside it.
1191        .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        // Opening a context menu reports the row that was pointed at; what is
1219        // selected stays the host's answer.
1220        .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    // A palette is summoned over whatever is on screen, so it sits centred
1234    // near the top of the surface rather than in a corner of it.
1235    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
1244/// The notification layer the scene shows, kept across frames.
1245///
1246/// The stack, each timer, and each entry animation outlive a frame, so the
1247/// layer is built once and the toasts are pushed once with it.
1248struct 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            // A refusal, so the offer is the thing that could change the
1271            // answer. Retrying an unapproved call only gets refused again.
1272            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        // The failure beside the refusal, so the two tones are on screen
1281        // together and the difference between them is a picture rather than a
1282        // claim.
1283        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        // A failure keeps its report on screen; only the success times out.
1303        .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        // The body belongs to the caller: tabs render the strip only.
1328        .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
1336/// The overflow menu the document-tab scene hangs off the strip.
1337///
1338/// The strip does not own it: a menu is an entity with an open state that
1339/// outlives a frame, so the caller builds it and the strip fills it.
1340struct 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        // An icon-only trigger still has to be nameable, so it carries the
1349        // strip's own word for what moved into it.
1350        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
1402/// The search surfaces the scene shows, kept across frames.
1403///
1404/// Both hold a [`TextInput`], which owns a caret and a selection that outlive
1405/// a frame, so they are built once and driven once.
1406struct 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    // The three counts a field must keep apart. No pointer position produces
1455    // them at once, so each is stated.
1456    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
1487/// One hit count, rendered on its own so the states can be seen side by side.
1488fn 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        // Replace all says how many before it does any, so nobody agrees to a
1514        // number they were never shown.
1515        .child(caption(
1516            &theme,
1517            "replace all names its count before it acts",
1518        ))
1519        .child(replace)
1520        .into_any_element()
1521}
1522
1523/// The notification centre the scene shows, kept across frames.
1524struct 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        // The same three reports the toast scene shows, after their toasts
1567        // have gone.
1568        .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    // A failure, not a refusal. Retrying a refusal would only be refused
1579    // again, so the retry here is offered against something that can succeed
1580    // on a second attempt; refusals are shown by ToolCallCard instead.
1581    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    // Spans are the caller's, in byte offsets into each line. Nothing here
1742    // parses anything.
1743    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
1813/// The cascader owns only the open surface and path, so its scene keeps one
1814/// view alive across capture frames.
1815struct 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
2007/// How many records the list fixture claims to hold.
2008///
2009/// The count exists to make the difference visible: the list publishes all of
2010/// it, and renders only the handful the viewport can show.
2011const FIXTURE_RECORDS: usize = 240;
2012
2013/// One synthetic record. Nothing here stands for a product: the identity is a
2014/// fixture key and the label says so.
2015fn 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
2105/// How many rows the fixture host has handed over, and how many exist behind
2106/// it. The two numbers differ on purpose: that gap is what the select-all box
2107/// and the bulk bar have to be honest about.
2108const FIXTURE_JOBS_LOADED: usize = 240;
2109const FIXTURE_JOBS_TOTAL: usize = 12_000;
2110
2111/// One synthetic job. Nothing here stands for a product: the identity is a
2112/// fixture key and the label says so.
2113fn 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        // Declared second, drawn first: a pinned column holds the left edge
2136        // whatever order the caller puts the columns in.
2137        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
2372/// The same vocabulary an interface uses everywhere, rendered right to left.
2373///
2374/// The point of capturing this is that support for another reading direction
2375/// is a claim about pixels, and a claim about pixels is only checkable in a
2376/// picture. Everything here is a component that already existed; nothing was
2377/// drawn specially for the scene. What should have moved: the trail runs from
2378/// the right, the tree indents from the right and its shut chevrons point
2379/// left, the accordion header reads from the right, and the magnifier's
2380/// handle changed corners. What should not have moved: the checkmark, the
2381/// gear, and the downward chevron, because none of them mean "forward".
2382fn 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                // Directional glyphs on the top row, fixed ones below the
2456                // names, so a reviewer can see which turned around.
2457                .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
2547/// The form scene's controls, kept across frames.
2548///
2549/// Every one of these owns editing state — a caret, a query, an open list —
2550/// so they are built once. Building them once is also what makes the capture
2551/// static.
2552struct 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        // The host holds ninety days while its own limit is sixty. The field
2573        // shows the number that is actually set and says it is out of range,
2574        // rather than quietly drawing a number nobody chose.
2575        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                // The description says what the field is for and the error
2637                // says what went wrong; neither answers for the other.
2638                .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                // The keystroke lives in the hint, so the description does not
2670                // spend a second line repeating it.
2671                .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
2686/// The password in the canonical sign-in composition, kept across frames.
2687struct 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
2769/// The one logical code input in the canonical verification composition.
2770struct 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
2846/// The split button the actions scene shows, kept across frames.
2847struct 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
2995/// The inputs the scene shows, kept across frames.
2996///
2997/// An editable control carries state, so the scene builds its entities once
2998/// rather than on every frame, which would discard whatever was typed.
2999struct 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        // A caret only paints where the keyboard is, so one area takes it:
3066        // otherwise a capture cannot show a caret at all.
3067        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
3118/// A pane of fixture copy, for a scene that needs something on both sides of a
3119/// divider or below a viewport.
3120fn 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
3171/// The same region, already scrolled, so the shadow that says there is content
3172/// above the fold is in a captured image rather than only in a test.
3173fn 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        // Nothing overflows here, so no scrollbar is drawn or published.
3212        .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
3228/// A region scrolled off both ends, so the fade that says there is more in
3229/// either direction is in a captured image, beside one that hides nothing and
3230/// therefore fades at neither edge.
3231fn 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        // Nothing is hidden here, so no edge fades and the caller says so.
3256        .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
3275/// Glass over the page it covers, which is the only way to see that the
3276/// backdrop is blurred rather than merely tinted.
3277fn 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
3368/// The overflow menu of the toolbar scene, kept across frames.
3369struct 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
3538/// The page-size control of the pagination scene, kept across frames.
3539struct 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        // A host that only knows there is another page says exactly that: no
3572        // last-page control, no numbers, and no total.
3573        .child(
3574            Pagination::new("scene.pagination.unknown")
3575                .page(3)
3576                .unknown_total(true)
3577                .on_select(|_, _, _| {}),
3578        )
3579        .into_any_element()
3580}
3581
3582/// The drawer the scene shows, kept across frames and settled so the capture
3583/// photographs the panel where it comes to rest rather than mid-slide.
3584struct 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/// The order the reorder scene is currently showing.
3648///
3649/// A still frame cannot show a slide, so the capture is the settled list and
3650/// the button is what a reviewer presses to watch a row travel.
3651#[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/// Which way every state in the state-transition scene is currently pointing.
3711///
3712/// One flag drives all of them, so a reviewer flips the whole row at once and
3713/// watches a check draw, a knob slide, an indicator travel and a section open
3714/// on the same frame.
3715#[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/// The counts the animated readout scene is currently showing.
3803#[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
3873/// A caption naming the state a drag scene was staged in.
3874fn 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    // A capture cannot photograph a gesture, so the drag is placed by hand.
3884    // Staging fixes the ghost, the indicator, and the open slot, and takes the
3885    // pointer and the spring out of the picture.
3886    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                    // A row slides without its layout slot moving, so the
3919                    // viewport is one row taller than the rows it holds and
3920                    // the open slot has somewhere to be.
3921                    .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    // No single pointer position can produce all three states at once, so each
3989    // zone is pinned to the one it is here to show.
3990    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
4379/// A layout nested three deep, with one leaf collapsed to its rail. The tree
4380/// is the caller's: every divider reports the ratio it was asked for and moves
4381/// nothing here.
4382fn 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            // A collapsed leaf is drawn at its rail, and the divider beside it
4398            // is not offered: a fixed extent has no ratio to trade.
4399            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                        // The rail is narrower than any label, so the collapsed
4418                        // leaf is drawn as the room it still holds.
4419                        .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
4431/// A whole application frame: panels in regions, one region collapsed to a
4432/// rail, one panel the host refuses, and a status bar under all of it.
4433fn 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                    // The refused panel is the one on top, because a refusal
4496                    // nobody can see is a refusal nobody was told about.
4497                    .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                // The host's words, not the recorder's: it has no keymap.
4550                .conflict(Some("Already opens the command palette"))
4551        });
4552        // Recording is a state, not a gesture, so the scene puts one recorder
4553        // into it by hand rather than waiting for a keystroke that a still
4554        // image could not photograph anyway.
4555        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    // A recorder carries its name in the tree rather than drawing one, the way
4567    // every other control here does, so the scene puts it where a keymap page
4568    // would: in a settings row that states what the binding is for.
4569    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    /// The pinned calendar every date scene runs on, so two captures of the
4666    /// same scene are the same picture.
4667    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
4853/// A document with one of every block kind in it, including a tag nobody ran.
4854const 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![The run graph](runs/graph.png "yesterday's run")
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
4906/// The thread the conversation scene shows, one message per delivery state.
4907fn 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            // A viewport shorter than the thread opens at its top, so the
4959            // messages below it have never been on screen and the list says
4960            // how many there are rather than letting them be discovered.
4961            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
4969/// The element a host hands back for an image this crate did not fetch.
4970///
4971/// A flat fill rather than a picture, because the crate ships no photographs
4972/// and a scene has to photograph the same pixels every run.
4973fn 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
5109/// The cube the model scenes read: a glTF document with its own buffer
5110/// inside it, so the reader is exercised on the path a host would take and
5111/// nothing is fetched to draw a scene.
5112const CUBE: &[u8] = include_bytes!("../assets/models/cube.gltf");
5113
5114/// A measured envelope, in the shape a host that read the samples would hand
5115/// over. It is written down rather than sampled, because a scene that drew a
5116/// different waveform every run could not be a visual baseline.
5117fn 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        // A calculator has no files to read, which is not the same as being
5375        // refused them.
5376        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
5467/// Every state a call can be in, side by side.
5468///
5469/// Two columns because a captured image is one screen: stacked, the refusal
5470/// fell below the fold, and a state nobody can see in the snapshot is a state
5471/// the snapshot does not guard.
5472fn 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                // A refusal reads as a decision: it is not the failure above
5520                // it, and not the silent success in the other column.
5521                .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                    // A failure, not a refusal: the skipped step above is what
5557                    // a refusal looks like, and the two must not be worded
5558                    // into each other.
5559                    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        // Reasoning that has finished and reasoning still arriving are the
5582        // same words in the same place, so the difference has to be shown.
5583        .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        // Withheld and absent are two different facts, and neither is the
5601        // collapsed block above.
5602        .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
5613// ------------------------------------------------- structured data and agents
5614
5615/// A document with the three facts a viewer usually confuses: a key holding
5616/// `null`, a key holding an empty object, and a key that is simply not here.
5617fn scene_document() -> JsonValue {
5618    JsonValue::object([
5619        ("id", JsonValue::string("run-4812")),
5620        ("attempts", JsonValue::number("3")),
5621        ("streaming", JsonValue::Bool(true)),
5622        // Nothing has been recorded here yet, which is not the same as the
5623        // key being missing: `resumed_from` is missing, and says nothing.
5624        ("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
5668/// The form kept across frames.
5669///
5670/// Its fields own carets, open menus and a selection, so rebuilding it every
5671/// frame would throw away whatever had been typed into it.
5672struct 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            // An error the host returned rather than one the form derived:
5754            // only the host knows this path is outside the workspace.
5755            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            // Only the one with a catalogue is opened. Expanding the two empty
5770            // ones as well pushed disconnected, failed, and disabled out of
5771            // the captured screen, and a state no image shows is a state no
5772            // image guards.
5773            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
5863/// The ordinary-application views that own state across frames.
5864struct 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                    // A column stretches its children across, and a badge
5930                    // stretched that far stops reading as a label and starts
5931                    // reading as a filled bar.
5932                    .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    // Nobody has pressed this one. It is the state a reader sees almost all
5954    // of the time, and it was the one state of the three no image held.
5955    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    // The confirmation outlives the capture. At its ordinary length it expires
5962    // partway through a run, so which of the two states this scene showed
5963    // depended on how long the run took to reach it.
5964    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}