The gallery's entry runs natively or, on wasm32, from a wasm_bindgen start through gpui_web, single-threaded with WebGL2 fallback. ?page, ?story and ?theme choose what shows; an unknown one fails in the page and the log. set_theme switches the mode later. Pictures load from ELY_GALLERY_ASSETS. scripts/stories.py writes examples/gallery/stories.json from the pages.
339 lines
12 KiB
Rust
339 lines
12 KiB
Rust
use std::time::Duration;
|
|
|
|
use anyhow::anyhow;
|
|
use ely_gpui_component::{
|
|
buttons::{Button, ButtonVariant},
|
|
feedback::{
|
|
ConfirmationCard, ConnectionStatus, Countdown, ErrorBoundary, ErrorView, InlineMessage,
|
|
ResultView, SaveState, SavingIndicator, SyncState, SyncStatus, Timer,
|
|
},
|
|
primitives::Severity,
|
|
shell::Connectivity,
|
|
theme::{ActiveTheme, Radius, TextSize},
|
|
typography::Caption,
|
|
};
|
|
use gpui::{App, Div, IntoElement, ParentElement, Styled, Window, div, prelude::FluentBuilder};
|
|
use web_time::Instant;
|
|
|
|
use crate::{
|
|
probe::probe,
|
|
ui::{keep, row, section, set, specimen, specimens},
|
|
};
|
|
|
|
fn frame(cx: &App) -> Div {
|
|
let theme = cx.theme();
|
|
div()
|
|
.flex_1()
|
|
.min_w_0()
|
|
.h_80()
|
|
.flex()
|
|
.flex_col()
|
|
.justify_center()
|
|
.rounded(theme.radius(Radius::Lg))
|
|
.border_1()
|
|
.border_color(theme.colors.border)
|
|
}
|
|
|
|
pub fn errors(cx: &mut App) -> impl IntoElement + use<> {
|
|
section(
|
|
"ErrorView / NotFound / NoPermission / Maintenance / Offline",
|
|
"A view that could not show what was asked says what happened and offers a way on.",
|
|
cx,
|
|
)
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.gap_3()
|
|
.child(frame(cx).child(
|
|
ErrorView::not_found("error-404").action(Button::new("error-home", "Go home")),
|
|
))
|
|
.child(
|
|
frame(cx).child(
|
|
ErrorView::no_permission("error-403")
|
|
.action(Button::new("error-ask", "Request access").primary()),
|
|
),
|
|
),
|
|
)
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.gap_3()
|
|
.child(frame(cx).child(ErrorView::maintenance("error-maintenance")))
|
|
.child(frame(cx).child(
|
|
ErrorView::offline("error-offline").action(Button::new("error-retry", "Try again")),
|
|
)),
|
|
)
|
|
}
|
|
|
|
pub fn boundary(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|
let broken = keep("boundary-broken", || false, window, cx);
|
|
let (now, toggle, retry) = (*broken.read(cx), broken.clone(), broken.clone());
|
|
let theme = cx.theme();
|
|
let content = if now {
|
|
Err(anyhow!("the export service answered 503").context("Revenue, third quarter"))
|
|
} else {
|
|
Ok(div()
|
|
.size_full()
|
|
.flex()
|
|
.items_center()
|
|
.justify_center()
|
|
.text_color(theme.colors.fg_muted)
|
|
.child("Revenue, third quarter: 1.2 M"))
|
|
};
|
|
section(
|
|
"ErrorBoundary",
|
|
"Shows its content, or the error it holds, logged once. It renders a Result; it never catches a panic.",
|
|
cx,
|
|
)
|
|
.child(
|
|
div().flex().child(
|
|
frame(cx).child(
|
|
ErrorBoundary::new("boundary", content)
|
|
.on_retry(move |_, cx| set(&retry, false, cx)),
|
|
),
|
|
),
|
|
)
|
|
.child(
|
|
row().child(probe(
|
|
"boundary-break",
|
|
Button::new("boundary-toggle", if now { "Fix it" } else { "Break it" })
|
|
.on_click(move |_, _, cx| set(&toggle, !now, cx)),
|
|
)),
|
|
)
|
|
}
|
|
|
|
pub fn results(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|
let turn = keep("result-turn", || 0usize, window, cx);
|
|
let (now, replay) = (*turn.read(cx), turn.clone());
|
|
section(
|
|
"Result",
|
|
"Where a task ended. The ring and its mark draw themselves once.",
|
|
cx,
|
|
)
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.gap_3()
|
|
.child(
|
|
frame(cx).child(
|
|
ResultView::success(("result-ok", now), "Payment sent")
|
|
.body("€240.00 to Studio Ottanta. A receipt is on its way.")
|
|
.action(Button::new("result-done", "Done").primary())
|
|
.action(Button::new("result-receipt", "View receipt")),
|
|
),
|
|
)
|
|
.child(
|
|
frame(cx).child(
|
|
ResultView::failure(("result-no", now), "Payment declined")
|
|
.body("Your bank declined the charge. No money moved.")
|
|
.action(Button::new("result-again", "Try again")),
|
|
),
|
|
),
|
|
)
|
|
.child(
|
|
row().child(probe(
|
|
"result-replay",
|
|
Button::new("result-replay", "Replay")
|
|
.variant(ButtonVariant::Ghost)
|
|
.on_click(move |_, _, cx| set(&replay, now + 1, cx)),
|
|
)),
|
|
)
|
|
}
|
|
|
|
pub fn confirmations(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|
let said = keep("confirm-said", || None::<&'static str>, window, cx);
|
|
let theme = cx.theme();
|
|
let answer = |what: &'static str| {
|
|
let said = said.clone();
|
|
move |_: &mut Window, cx: &mut App| set(&said, Some(what), cx)
|
|
};
|
|
let command = div()
|
|
.px_3()
|
|
.py_2()
|
|
.rounded(theme.radius(Radius::Md))
|
|
.bg(theme.colors.sunken)
|
|
.font_family(theme.mono_family.clone())
|
|
.text_size(theme.text_size(TextSize::Sm))
|
|
.child("cargo clean && cargo build --release");
|
|
section(
|
|
"ConfirmationCard",
|
|
"Asks in place before an action runs, with what it will do.",
|
|
cx,
|
|
)
|
|
.child(
|
|
div()
|
|
.w_128()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_3()
|
|
.child(
|
|
ConfirmationCard::new("confirm-run", "Run this command?")
|
|
.body("The agent wants to rebuild the project from scratch.")
|
|
.confirm("Run")
|
|
.on_confirm(answer("Ran the command"))
|
|
.on_cancel(answer("Kept the build"))
|
|
.child(command),
|
|
)
|
|
.child(
|
|
ConfirmationCard::new("confirm-delete", "Delete 3 branches?")
|
|
.body("feature/toasts, feature/menus and old/draft go for good.")
|
|
.confirm("Delete")
|
|
.destructive()
|
|
.on_confirm(answer("Deleted 3 branches"))
|
|
.on_cancel(answer("Kept the branches")),
|
|
)
|
|
.when_some(*said.read(cx), |cards, what| {
|
|
cards.child(InlineMessage::new(Severity::Info, what))
|
|
}),
|
|
)
|
|
}
|
|
|
|
pub fn statuses(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|
const SAVES: [SaveState; 3] = [SaveState::Saving, SaveState::Saved, SaveState::Failed];
|
|
const SYNCS: [SyncState; 4] = [
|
|
SyncState::Syncing(12),
|
|
SyncState::Synced,
|
|
SyncState::Paused,
|
|
SyncState::Failed,
|
|
];
|
|
const LINKS: [Connectivity; 3] = [
|
|
Connectivity::Online,
|
|
Connectivity::Reconnecting,
|
|
Connectivity::Offline,
|
|
];
|
|
let at = keep("status-states", || [0usize; 3], window, cx);
|
|
let now = *at.read(cx);
|
|
let next = |slot: usize, count: usize| {
|
|
let at = at.clone();
|
|
move |_: &gpui::ClickEvent, _: &mut Window, cx: &mut App| {
|
|
let mut then = now;
|
|
then[slot] = (now[slot] + 1) % count;
|
|
set(&at, then, cx)
|
|
}
|
|
};
|
|
let retry = at.clone();
|
|
section(
|
|
"SavingIndicator / SyncStatus / ConnectionStatus",
|
|
"Quiet lines for work in the background. Each rises in when its state changes; sync arrows turn while they work.",
|
|
cx,
|
|
)
|
|
.child(
|
|
specimens()
|
|
.child(specimen(
|
|
"saving",
|
|
row()
|
|
.child(probe(
|
|
"saving-next",
|
|
Button::new("saving-next", "Next").on_click(next(0, SAVES.len())),
|
|
))
|
|
.child(
|
|
SavingIndicator::new("saving", SAVES[now[0]]).on_retry(move |_, cx| {
|
|
let mut then = now;
|
|
then[0] = 0;
|
|
set(&retry, then, cx)
|
|
}),
|
|
),
|
|
cx,
|
|
))
|
|
.child(specimen(
|
|
"sync",
|
|
row()
|
|
.child(probe(
|
|
"sync-next",
|
|
Button::new("sync-next", "Next").on_click(next(1, SYNCS.len())),
|
|
))
|
|
.child(SyncStatus::new("sync", SYNCS[now[1]])),
|
|
cx,
|
|
))
|
|
.child(specimen(
|
|
"connection",
|
|
row()
|
|
.child(probe(
|
|
"connection-next",
|
|
Button::new("connection-next", "Next").on_click(next(2, LINKS.len())),
|
|
))
|
|
.child(ConnectionStatus::new("connection", LINKS[now[2]])),
|
|
cx,
|
|
)),
|
|
)
|
|
}
|
|
|
|
pub fn clocks(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|
let opened = keep("clock-opened", Instant::now, window, cx);
|
|
let short = keep(
|
|
"clock-short",
|
|
|| Instant::now() + Duration::from_secs(10),
|
|
window,
|
|
cx,
|
|
);
|
|
let stopped = keep("clock-stopped", || None::<Instant>, window, cx);
|
|
let over = keep("clock-over", || false, window, cx);
|
|
let (since, until, halt) = (*opened.read(cx), *short.read(cx), *stopped.read(cx));
|
|
let (restart, toggle, done, reset) =
|
|
(short.clone(), stopped.clone(), over.clone(), opened.clone());
|
|
let reopen = over.clone();
|
|
section(
|
|
"Countdown / Timer",
|
|
"Time left and time spent. Digits roll as they change.",
|
|
cx,
|
|
)
|
|
.child(
|
|
specimens()
|
|
.child(specimen(
|
|
"launch in",
|
|
Countdown::new(
|
|
"countdown-launch",
|
|
since + Duration::from_secs(2 * 3_600 + 14 * 60 + 7),
|
|
),
|
|
cx,
|
|
))
|
|
.child(specimen(
|
|
"ten seconds",
|
|
div()
|
|
.flex()
|
|
.items_center()
|
|
.gap_3()
|
|
.child(
|
|
Countdown::new("countdown-short", until)
|
|
.on_done(move |_, cx| set(&done, true, cx)),
|
|
)
|
|
.child(probe(
|
|
"countdown-restart",
|
|
Button::new("countdown-restart", "Restart")
|
|
.variant(ButtonVariant::Ghost)
|
|
.on_click(move |_, _, cx| {
|
|
set(&restart, Instant::now() + Duration::from_secs(10), cx);
|
|
set(&reopen, false, cx);
|
|
}),
|
|
)),
|
|
cx,
|
|
))
|
|
.child(specimen(
|
|
"on this page",
|
|
div()
|
|
.flex()
|
|
.items_center()
|
|
.gap_3()
|
|
.child(Timer::new("timer", since).stopped(halt))
|
|
.child(
|
|
Button::new(
|
|
"timer-toggle",
|
|
if halt.is_some() { "Reset" } else { "Stop" },
|
|
)
|
|
.variant(ButtonVariant::Ghost)
|
|
.on_click(move |_, _, cx| match halt {
|
|
Some(_) => {
|
|
set(&reset, Instant::now(), cx);
|
|
set(&toggle, None, cx);
|
|
}
|
|
None => set(&toggle, Some(Instant::now()), cx),
|
|
}),
|
|
),
|
|
cx,
|
|
)),
|
|
)
|
|
.when(*over.read(cx), |section| {
|
|
section.child(Caption::new("The ten seconds ran out; on_done ran once."))
|
|
})
|
|
}
|