Mail · Sorting: a label picker and a snooze menu
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. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
154f7f2b46
commit
5b0a8443a7
16 files changed
+1037
-144
No files matched your search
@@ -0,0 +1,157 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
App, ElementId, Entity, InteractiveElement, ParentElement, SharedString, Styled, Window, div,
|
||||
};
|
||||
use jiff::{
|
||||
Timestamp, ToSpan,
|
||||
civil::{Date, DateTime},
|
||||
tz::TimeZone,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
buttons::{Button, ButtonVariant},
|
||||
forms::{DateTimePicker, FormError},
|
||||
menus::MenuItem,
|
||||
overlays::Dialog,
|
||||
primitives::IconName,
|
||||
typography::format,
|
||||
};
|
||||
|
||||
pub(super) type OnTime = Rc<dyn Fn(Timestamp, &mut Window, &mut App)>;
|
||||
|
||||
/// `hour`:00 on `day` in `zone`.
|
||||
pub(super) fn at_hour(day: Date, hour: i8, zone: &TimeZone) -> Timestamp {
|
||||
day.at(hour, 0, 0, 0)
|
||||
.to_zoned(zone.clone())
|
||||
.expect("the zone holds the hour")
|
||||
.timestamp()
|
||||
}
|
||||
|
||||
/// The first Monday after `day`: a week on when `day` is a Monday.
|
||||
pub(super) fn next_monday(day: Date) -> Date {
|
||||
let ahead = 7 - i64::from(day.weekday().to_monday_zero_offset());
|
||||
day.checked_add(ahead.days()).expect("a Monday lies ahead")
|
||||
}
|
||||
|
||||
/// `name` beside the day and hour of `at` in `zone`, as a menu row shows a time.
|
||||
pub(super) fn named(name: &str, at: Timestamp, zone: &TimeZone) -> (String, Timestamp) {
|
||||
let hour = format::datetime(at, zone, "%a %H:%M").expect("a fixed pattern formats");
|
||||
(format!("{name} · {hour}"), at)
|
||||
}
|
||||
|
||||
/// `times` less each moment an earlier one names, as tomorrow names next Monday on a Sunday.
|
||||
pub(super) fn distinct(
|
||||
times: impl IntoIterator<Item = (String, Timestamp)>,
|
||||
) -> Vec<(String, Timestamp)> {
|
||||
let mut kept: Vec<(String, Timestamp)> = Vec::new();
|
||||
for (name, at) in times {
|
||||
if kept.iter().all(|(_, seen)| *seen != at) {
|
||||
kept.push((name, at));
|
||||
}
|
||||
}
|
||||
kept
|
||||
}
|
||||
|
||||
/// Whether the dialog for a time of one's own shows, and the time picked in it.
|
||||
#[derive(Default)]
|
||||
pub(super) struct Picking {
|
||||
open: bool,
|
||||
chosen: Option<DateTime>,
|
||||
}
|
||||
|
||||
pub(super) fn picking(id: &ElementId, window: &mut Window, cx: &mut App) -> Entity<Picking> {
|
||||
window.use_keyed_state((id.clone(), "picking"), cx, |_, _| Picking::default())
|
||||
}
|
||||
|
||||
/// The menu row that opens the dialog.
|
||||
pub(super) fn pick_item(owner: &'static str, picking: &Entity<Picking>) -> MenuItem {
|
||||
let picking = picking.clone();
|
||||
MenuItem::new("Pick a time…")
|
||||
.icon(IconName::Calendar)
|
||||
.on_click(move |_, cx| {
|
||||
log::info!("{owner}: pick a time");
|
||||
picking.update(cx, |picking, cx| {
|
||||
picking.open = true;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// What the dialog asks, and the action that takes its time.
|
||||
pub(super) struct Ask {
|
||||
pub title: &'static str,
|
||||
pub detail: &'static str,
|
||||
pub action: &'static str,
|
||||
}
|
||||
|
||||
/// While open, a dialog to pick a day and an hour in `zone`. Its action waits for a time ahead of now, then closes and hands the time on.
|
||||
pub(super) fn time_dialog(
|
||||
id: &ElementId,
|
||||
picking: &Entity<Picking>,
|
||||
ask: Ask,
|
||||
zone: &TimeZone,
|
||||
on_time: Option<OnTime>,
|
||||
cx: &App,
|
||||
) -> Option<Dialog> {
|
||||
let now = picking.read(cx);
|
||||
if !now.open {
|
||||
return None;
|
||||
}
|
||||
let chosen = now.chosen;
|
||||
let at = chosen.map(|chosen| {
|
||||
chosen
|
||||
.to_zoned(zone.clone())
|
||||
.expect("the zone holds the time")
|
||||
.timestamp()
|
||||
});
|
||||
let ahead = at.filter(|at| *at > Timestamp::now());
|
||||
let (closing, choosing, id) = (picking.clone(), picking.clone(), id.clone());
|
||||
let late = (at.is_some() && ahead.is_none())
|
||||
.then(|| FormError::new((id.clone(), "late"), "That time has passed."));
|
||||
let dialog = Dialog::new((id.clone(), "dialog"), ask.title, move |_, cx| {
|
||||
closing.update(cx, |picking, cx| {
|
||||
*picking = Picking::default();
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detail(ask.detail)
|
||||
.child(
|
||||
div()
|
||||
.debug_selector(|| "time-dialog".into())
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
DateTimePicker::new((id.clone(), "when"), chosen).on_change(move |at, _, cx| {
|
||||
choosing.update(cx, |picking, cx| {
|
||||
picking.chosen = Some(at);
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
)
|
||||
.children(late),
|
||||
)
|
||||
.action({
|
||||
let id = id.clone();
|
||||
move |close| {
|
||||
Button::new((id, "cancel"), "Cancel")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.on_click(move |_, window, cx| close(window, cx))
|
||||
}
|
||||
})
|
||||
.action(move |close| {
|
||||
Button::new((id.clone(), "take"), SharedString::from(ask.action))
|
||||
.variant(ButtonVariant::Primary)
|
||||
.disabled(ahead.is_none())
|
||||
.on_click(move |_, window, cx| {
|
||||
let at = ahead.expect("the action waits for a time ahead");
|
||||
log::info!("time dialog {id:?}: {at}");
|
||||
close(window, cx);
|
||||
if let Some(on_time) = &on_time {
|
||||
on_time(at, window, cx);
|
||||
}
|
||||
})
|
||||
});
|
||||
Some(dialog)
|
||||
}
|
||||
Reference in new issue
Block a user