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.
270 lines
9.0 KiB
Rust
270 lines
9.0 KiB
Rust
use ely_gpui_component::{
|
|
buttons::Button,
|
|
feedback::EmptyState,
|
|
mail::{Mail, MailList, Mailbox, MailboxList},
|
|
primitives::IconName,
|
|
theme::{ActiveTheme, Radius, TextSize},
|
|
typography::format,
|
|
};
|
|
use gpui::{AnyElement, App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
|
use jiff::{SignedDuration, Timestamp, tz::TimeZone};
|
|
|
|
mod read;
|
|
mod sort;
|
|
mod write;
|
|
|
|
use super::Page;
|
|
use crate::{
|
|
probe::probe,
|
|
step::Step,
|
|
ui::{change, keep, section},
|
|
};
|
|
|
|
pub const PAGE: Page = Page {
|
|
number: 29,
|
|
slug: "mail",
|
|
title: "Mail",
|
|
summary: "Mailboxes and the messages in them: reading, writing, sorting and putting off.",
|
|
render,
|
|
script: SCRIPT,
|
|
};
|
|
|
|
const SCRIPT: &[Step] = &[
|
|
Step::DownAt("mail-boxes", 100.0, 139.0),
|
|
Step::UpAt("mail-boxes", 100.0, 139.0),
|
|
Step::Wait(200),
|
|
Step::Shot("box-opened"),
|
|
Step::DownAt("mail-list", 417.0, 112.0),
|
|
Step::UpAt("mail-list", 417.0, 112.0),
|
|
Step::Wait(200),
|
|
Step::Shot("starred"),
|
|
Step::DownAt("mail-reader", 12.0, 227.0),
|
|
Step::UpAt("mail-reader", 12.0, 227.0),
|
|
Step::Wait(300),
|
|
Step::Shot("quoted-open"),
|
|
Step::DownAt("mail-thread", 200.0, 120.0),
|
|
Step::UpAt("mail-thread", 200.0, 120.0),
|
|
Step::Wait(300),
|
|
Step::Shot("thread-opened"),
|
|
Step::DownAt("mail-composer", 270.0, 31.0),
|
|
Step::UpAt("mail-composer", 270.0, 31.0),
|
|
Step::Type("an"),
|
|
Step::Wait(200),
|
|
Step::Shot("recipient-suggested"),
|
|
Step::Key("enter"),
|
|
Step::Wait(200),
|
|
Step::Shot("recipient-added"),
|
|
Step::DownAt("mail-labels", 150.0, 25.0),
|
|
Step::UpAt("mail-labels", 150.0, 25.0),
|
|
Step::Type("Suppliers"),
|
|
Step::Wait(200),
|
|
Step::Shot("label-new"),
|
|
Step::Key("enter"),
|
|
Step::Wait(200),
|
|
Step::Shot("label-made"),
|
|
Step::DownAt("mail-snooze", 50.0, 16.0),
|
|
Step::UpAt("mail-snooze", 50.0, 16.0),
|
|
Step::Wait(300),
|
|
Step::Shot("snooze-open"),
|
|
Step::Key("up"),
|
|
Step::Key("enter"),
|
|
Step::Wait(300),
|
|
Step::Shot("snooze-dialog"),
|
|
Step::Key("tab"),
|
|
Step::Key("enter"),
|
|
Step::Wait(300),
|
|
Step::Shot("snooze-when"),
|
|
Step::Key("escape"),
|
|
Step::Key("escape"),
|
|
];
|
|
|
|
/// What a demo says it did, under it.
|
|
fn quiet(text: SharedString, cx: &App) -> impl IntoElement + use<> {
|
|
let theme = cx.theme();
|
|
div()
|
|
.text_size(theme.text_size(TextSize::Sm))
|
|
.text_color(theme.colors.fg_muted)
|
|
.child(text)
|
|
}
|
|
|
|
/// A moment as the demos say it: its weekday and hour here.
|
|
fn day_and_hour(at: Timestamp) -> String {
|
|
let zone = TimeZone::try_system().expect("the gallery reads the system time zone");
|
|
format::datetime(at, &zone, "%a %H:%M").expect("a fixed pattern formats")
|
|
}
|
|
|
|
fn boxes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|
let open = keep("mail-open-box", || SharedString::from("inbox"), window, cx);
|
|
let shown = open.read(cx).clone();
|
|
section(
|
|
"MailboxList / FolderList",
|
|
"Mailboxes, folders and labels in sections: a press, an arrow or Enter opens one, and the one open is lit. A box with mail unread stands out with its count; a label wears its hue.",
|
|
cx,
|
|
)
|
|
.child(probe(
|
|
"mail-boxes",
|
|
div().w(px(260.)).child(
|
|
MailboxList::new("mail-boxes")
|
|
.section(
|
|
"Mailboxes",
|
|
[
|
|
Mailbox::new("inbox", "Inbox", IconName::Inbox).unread(12),
|
|
Mailbox::new("starred", "Starred", IconName::Star),
|
|
Mailbox::new("snoozed", "Snoozed", IconName::Clock),
|
|
Mailbox::new("sent", "Sent", IconName::Send),
|
|
Mailbox::new("drafts", "Drafts", IconName::FileText).unread(2),
|
|
Mailbox::new("archive", "Archive", IconName::Archive),
|
|
Mailbox::new("spam", "Spam", IconName::OctagonAlert).unread(4),
|
|
Mailbox::new("trash", "Trash", IconName::Trash2),
|
|
],
|
|
)
|
|
.section(
|
|
"Folders",
|
|
[
|
|
Mailbox::new("atrium", "Atrium", IconName::Folder).unread(3),
|
|
Mailbox::new("invoices", "Invoices", IconName::Folder),
|
|
],
|
|
)
|
|
.section(
|
|
"Labels",
|
|
[
|
|
Mailbox::label("clients", "Clients", 0),
|
|
Mailbox::label("studio", "Studio", 5),
|
|
Mailbox::label("press", "Press", 3),
|
|
],
|
|
)
|
|
.open(shown)
|
|
.on_open(move |key, _, cx| change(&open, cx, |open| *open = key.clone())),
|
|
),
|
|
))
|
|
}
|
|
|
|
/// The demo's inbox: dated back from now, so the list reads the time, the weekday, the date and the year.
|
|
fn inbox() -> Vec<Mail> {
|
|
let now = Timestamp::now();
|
|
let ago = |minutes: i64| now - SignedDuration::from_mins(minutes);
|
|
vec![
|
|
Mail::new(
|
|
"review",
|
|
"Ana Lima",
|
|
"Friday's review moves to 10:00",
|
|
ago(12),
|
|
)
|
|
.snippet("We'll meet in the model room. Bring the plaster samples if they've dried.")
|
|
.unread(true),
|
|
Mail::new("samples", "Ben Ito", "Plaster samples", ago(95))
|
|
.snippet("Three finishes are in the stair hall: lime, tadelakt and a sanded gypsum.")
|
|
.attachments(3)
|
|
.unread(true),
|
|
Mail::new("brief", "Chloé Martin", "The brief, final", ago(60 * 26))
|
|
.snippet("Light from above, one stair, olive trees by the windows.")
|
|
.starred(true),
|
|
Mail::new(
|
|
"invoice",
|
|
"Studio Office",
|
|
"Invoice 2026-114",
|
|
ago(60 * 24 * 3),
|
|
)
|
|
.snippet("Please find the invoice for the September model work attached.")
|
|
.attachments(1),
|
|
Mail::new(
|
|
"press",
|
|
"Dev Rao",
|
|
"A visit from the press",
|
|
ago(60 * 24 * 9),
|
|
)
|
|
.snippet("They'd like to see the atrium before the opening. Would the 14th work?"),
|
|
Mail::new(
|
|
"welcome",
|
|
"Eli Park",
|
|
"Welcome to the studio",
|
|
ago(60 * 24 * 400),
|
|
)
|
|
.snippet("A few notes to start: where the models live, and who to ask.")
|
|
.starred(true),
|
|
]
|
|
}
|
|
|
|
fn list(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|
let selected = keep(
|
|
"mail-selected",
|
|
|| vec![SharedString::from("review")],
|
|
window,
|
|
cx,
|
|
);
|
|
let stars = keep(
|
|
"mail-stars",
|
|
|| vec![SharedString::from("brief"), SharedString::from("welcome")],
|
|
window,
|
|
cx,
|
|
);
|
|
let starred = stars.read(cx).clone();
|
|
let mails = inbox()
|
|
.into_iter()
|
|
.map(|mail| {
|
|
let on = starred.contains(&mail.key);
|
|
mail.starred(on)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let chosen = selected.read(cx).clone();
|
|
section(
|
|
"MailList / MailItem",
|
|
"Messages with the sender strong while unread, the subject and first words under it, when each came, a paperclip for files, and a star to press. A press or an arrow selects; Command or Shift takes several.",
|
|
cx,
|
|
)
|
|
.child(probe(
|
|
"mail-list",
|
|
div().w(px(440.)).child(
|
|
MailList::new("mail-list", mails)
|
|
.selected(chosen)
|
|
.on_select(move |keys, _, cx| change(&selected, cx, |selected| *selected = keys.to_vec()))
|
|
.on_star(move |key, on, _, cx| {
|
|
change(&stars, cx, |stars| {
|
|
stars.retain(|each| each != key);
|
|
if on {
|
|
stars.push(key.clone());
|
|
}
|
|
})
|
|
}),
|
|
),
|
|
))
|
|
}
|
|
|
|
fn zero(cx: &mut App) -> impl IntoElement + use<> {
|
|
let theme = cx.theme();
|
|
section(
|
|
"InboxZeroState → EmptyState",
|
|
"An inbox with nothing left in it says so, calm and brief.",
|
|
cx,
|
|
)
|
|
.child(probe(
|
|
"mail-zero",
|
|
div()
|
|
.w(px(440.))
|
|
.py_6()
|
|
.border_1()
|
|
.border_color(theme.colors.border)
|
|
.rounded(theme.radius(Radius::Lg))
|
|
.child(
|
|
EmptyState::new("mail-zero", IconName::Inbox, "All caught up")
|
|
.body("Nothing waits in your inbox. New mail lands here, newest first.")
|
|
.action(Button::new("mail-zero-archive", "Open the archive")),
|
|
),
|
|
))
|
|
}
|
|
|
|
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
|
div()
|
|
.child(boxes(window, cx))
|
|
.child(list(window, cx))
|
|
.child(zero(cx))
|
|
.child(read::reader(cx))
|
|
.child(read::thread(cx))
|
|
.child(write::composer(window, cx))
|
|
.child(write::signature(window, cx))
|
|
.child(sort::labels(window, cx))
|
|
.child(sort::snooze(window, cx))
|
|
.child(write::schedule(window, cx))
|
|
.into_any_element()
|
|
}
|