From ccbc0970e581236f24e928a147506fe280ede992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 27 Sep 2026 01:16:24 -0400 Subject: [PATCH] =?UTF-8?q?Mail=20=C2=B7=20Sorting:=20a=20label=20picker?= =?UTF-8?q?=20and=20a=20snooze=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LabelPicker puts labels on a message: a field finds them by name, arrows move and a press or Enter turns one on or off, each row with its check and its hue. A name no label has can be made into one, and the field starts over. Its rows are the option rows every list shares; forms::marked_row leads one with a mark. The cursor shows only while the field has focus. SnoozePicker offers later today, tomorrow, this weekend and next week, each with its day and hour, or a time picked in a dialog. It shares mail::times with ScheduleSend: the names, the dialog, which now waits for a time ahead of now, and a rule that a moment comes once, so on a Sunday tomorrow morning no longer repeats as Monday morning. --- AGENTS.md | 2 + TASKS.md | 4 +- examples/gallery/pages/mail/mod.rs | 40 +++- examples/gallery/pages/mail/sort.rs | 112 +++++++++++ examples/gallery/pages/mail/write.rs | 17 +- src/forms/mod.rs | 4 +- src/forms/options.rs | 13 ++ src/mail/boxes.rs | 15 +- src/mail/labels.rs | 290 +++++++++++++++++++++++++++ src/mail/mod.rs | 5 + src/mail/send.rs | 163 +++++---------- src/mail/snooze.rs | 186 +++++++++++++++++ src/mail/tests/mod.rs | 1 + src/mail/tests/sort.rs | 168 ++++++++++++++++ src/mail/times.rs | 157 +++++++++++++++ tasks/ch21-30.md | 4 +- 16 files changed, 1037 insertions(+), 144 deletions(-) create mode 100644 examples/gallery/pages/mail/sort.rs create mode 100644 src/mail/labels.rs create mode 100644 src/mail/snooze.rs create mode 100644 src/mail/tests/sort.rs create mode 100644 src/mail/times.rs diff --git a/AGENTS.md b/AGENTS.md index 1bf916a..ca94289 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -162,6 +162,8 @@ Ely GPUI Component. A component library for GPUI, in light and dark. - The focus color belongs to the focus ring. A chosen or shown item takes accent, as `forms::picks` and `lists::ListItem` do. - A box that shows a host's picture takes the picture's shape from the host (`ratio`), so nothing is cropped and a point given as a share lands on the picture. A focusable one keeps its focus border on an outer box, so the picture keeps that shape exactly (`media::ImageCropper`). - A default only the component can work out, such as a crop fitted to a picture it decodes, reaches the owner once through `window.defer` after render (`media::ImageUpload`). +- A menu of times goes through `mail::times`: each row names its day and hour, a moment comes once (tomorrow is next Monday on a Sunday), and "Pick a time…" opens one dialog that waits for a time ahead of now (`ScheduleSend`, `SnoozePicker`). +- A list whose cursor a field drives marks the cursor only while the field has focus; at rest its first row would look hovered (`mail::LabelPicker`). Its rows are option rows, and `forms::marked_row` leads one with a mark such as a label's hue. ## Decisions diff --git a/TASKS.md b/TASKS.md index bedc99e..279598a 100644 --- a/TASKS.md +++ b/TASKS.md @@ -105,7 +105,7 @@ Per-component lines live in `tasks/`. Tags there: - [x] T28e Messaging · Stickers and GIFs - [x] T29a Mail · Boxes and lists - [x] T29b Mail · Reading -- [ ] T29c Mail · Writing +- [x] T29c Mail · Writing - [ ] T29d Mail · Sorting - [ ] T30 Calendar - [ ] T31 Project — `tasks/ch31-43.md` @@ -346,3 +346,5 @@ Per-component lines live in `tasks/`. Tags there: | T29a | 2 | PASS | | | T29b | 1 | FAIL | a folded message lost focus as it opened; a long name beside a long date ran past a 280px fold | | T29b | 2 | PASS | | +| T29c | 1 | FAIL | a long chip ran past its tag field at 280px | +| T29c | 2 | PASS | | diff --git a/examples/gallery/pages/mail/mod.rs b/examples/gallery/pages/mail/mod.rs index 04bb8a2..53d0bb2 100644 --- a/examples/gallery/pages/mail/mod.rs +++ b/examples/gallery/pages/mail/mod.rs @@ -3,12 +3,14 @@ use ely_gpui_component::{ feedback::EmptyState, mail::{Mail, MailList, Mailbox, MailboxList}, primitives::IconName, - theme::{ActiveTheme, Radius}, + theme::{ActiveTheme, Radius, TextSize}, + typography::format, }; use gpui::{AnyElement, App, IntoElement, ParentElement, SharedString, Styled, Window, div, px}; -use jiff::{SignedDuration, Timestamp}; +use jiff::{SignedDuration, Timestamp, tz::TimeZone}; mod read; +mod sort; mod write; use super::Page; @@ -52,8 +54,40 @@ const SCRIPT: &[Step] = &[ 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("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(); @@ -223,6 +257,8 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement { .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() } diff --git a/examples/gallery/pages/mail/sort.rs b/examples/gallery/pages/mail/sort.rs new file mode 100644 index 0000000..0ee568e --- /dev/null +++ b/examples/gallery/pages/mail/sort.rs @@ -0,0 +1,112 @@ +use ely_gpui_component::{ + mail::{Label, LabelPicker, SnoozePicker}, + theme::{ActiveTheme, Radius}, +}; +use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px}; + +use super::{day_and_hour, quiet}; +use crate::{ + probe::probe, + ui::{change, keep, section, set}, +}; + +/// The demo's labels and the keys on. +#[derive(Clone)] +struct Sorting { + labels: Vec