gpui 0.2.2 asserts on a deferred draw inside another, and every dialog, sheet and popover draws inside one, so opening a date field in the snooze or schedule dialog panicked, as would any list, menu or picker opened in an overlay. primitives::raise replaces each deferred draw: at the top it defers as before; inside something raised it leaves a placeholder in the tree, and its layer lays it out on its own, then draws it last. check.sh refuses gpui's deferred anywhere else. The time dialog's rule is now times::ahead, tested for a moment behind, now itself, a moment ahead and another zone; a gpui test picks yesterday in the snooze dialog, sees that it has passed and that nothing is sent, then picks two days ahead and hears that moment.
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,
|
|
script::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()
|
|
}
|