The Date field opens a month calendar with past days disabled, as in T3 Code, and clicking in it no longer closes the dialog. Taking back a settle or snooze leaves the thread where it was in the list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Showing 1 changed file with 77 additions and 29 deletions.
| @@ -11,6 +11,7 @@ use gpui_kit::component::{ | ||
| 11 | 11 | input::{Input, InputState}, |
| 12 | 12 | menu::{DropdownMenu as _, PopupMenu, PopupMenuItem}, |
| 13 | 13 | notification::Notification, |
| 14 | + calendar::{Calendar, CalendarEvent, CalendarState, Matcher}, | |
| 14 | 15 | v_flex, |
| 15 | 16 | }; |
| 16 | 17 | use gpui_kit::prelude::FluentBuilder as _; |
| @@ -233,6 +234,8 @@ impl Workspace { | ||
| 233 | 234 | let keep = self.undo.iter().rposition(|entry| entry.kind != latest).map_or(0, |ix| ix + 1); |
| 234 | 235 | let group = self.undo.split_off(keep); |
| 235 | 236 | for entry in group { |
| 237 | + // Taking an action back keeps the thread where it was in the list. | |
| 238 | + let updated = entry.session.read(cx).updated; | |
| 236 | 239 | match entry.kind { |
| 237 | 240 | UndoKind::Settled => entry.session.update(cx, |session, cx| session.unsettle(cx)), |
| 238 | 241 | UndoKind::Snoozed => entry.session.update(cx, |session, cx| session.snooze(None, cx)), |
| @@ -240,10 +243,11 @@ impl Workspace { | ||
| 240 | 243 | UndoKind::Archived => entry.session.update(cx, |session, cx| session.set_archived(false, cx)), |
| 241 | 244 | UndoKind::Discarded => { |
| 242 | 245 | if let Some((key, chat)) = entry.draft { |
| 243 | - self.reattach_thread(&key, entry.session, chat, window, cx); | |
| 246 | + self.reattach_thread(&key, entry.session.clone(), chat, window, cx); | |
| 244 | 247 | } |
| 245 | 248 | } |
| 246 | 249 | } |
| 250 | + entry.session.update(cx, |session, _| session.updated = updated); | |
| 247 | 251 | } |
| 248 | 252 | cx.notify(); |
| 249 | 253 | true |
| @@ -884,6 +888,8 @@ impl Workspace { | ||
| 884 | 888 | let submit = submit.clone(); |
| 885 | 889 | dialog |
| 886 | 890 | .w(px(384.)) |
| 891 | + // The calendar pops out past the dialog; a click there isn't a click away. | |
| 892 | + .overlay_closable(false) | |
| 887 | 893 | .title( |
| 888 | 894 | v_flex() |
| 889 | 895 | .gap(px(4.)) |
| @@ -967,6 +973,10 @@ impl SnoozeUnit { | ||
| 967 | 973 | struct CustomSnooze { |
| 968 | 974 | mode: SnoozeMode, |
| 969 | 975 | date: NaiveDate, |
| 976 | + /// The date's calendar, past days disabled, and whether it's open. | |
| 977 | + calendar: Entity<CalendarState>, | |
| 978 | + calendar_open: bool, | |
| 979 | + _calendar_events: Subscription, | |
| 970 | 980 | time: Entity<InputState>, |
| 971 | 981 | amount: Entity<InputState>, |
| 972 | 982 | unit: SnoozeUnit, |
| @@ -978,7 +988,32 @@ impl CustomSnooze { | ||
| 978 | 988 | let initial: chrono::DateTime<Local> = (SystemTime::now() + HOUR).into(); |
| 979 | 989 | let time = cx.new(|cx| InputState::new(window, cx).placeholder("HH:MM").default_value(initial.format("%H:%M").to_string())); |
| 980 | 990 | let amount = cx.new(|cx| InputState::new(window, cx).default_value("2")); |
| 981 | - Self { mode: SnoozeMode::Date, date: initial.date_naive(), time, amount, unit: SnoozeUnit::Hours, error: None } | |
| 991 | + let today = Local::now().date_naive(); | |
| 992 | + let calendar = cx.new(|cx| { | |
| 993 | + let mut calendar = CalendarState::new(window, cx).disabled_matcher(Matcher::interval(Some(today), None)); | |
| 994 | + calendar.set_date(initial.date_naive(), window, cx); | |
| 995 | + calendar | |
| 996 | + }); | |
| 997 | + let calendar_events = cx.subscribe(&calendar, |this, _, event: &CalendarEvent, cx| { | |
| 998 | + let CalendarEvent::Selected(date) = event; | |
| 999 | + if let Some(date) = date.start() { | |
| 1000 | + this.date = date; | |
| 1001 | + this.calendar_open = false; | |
| 1002 | + this.error = None; | |
| 1003 | + cx.notify(); | |
| 1004 | + } | |
| 1005 | + }); | |
| 1006 | + Self { | |
| 1007 | + mode: SnoozeMode::Date, | |
| 1008 | + date: initial.date_naive(), | |
| 1009 | + calendar, | |
| 1010 | + calendar_open: false, | |
| 1011 | + _calendar_events: calendar_events, | |
| 1012 | + time, | |
| 1013 | + amount, | |
| 1014 | + unit: SnoozeUnit::Hours, | |
| 1015 | + error: None, | |
| 1016 | + } | |
| 982 | 1017 | } |
| 983 | 1018 | |
| 984 | 1019 | /// The wake time the form describes, or `None` (showing why) when it |
| @@ -1047,36 +1082,48 @@ impl Render for CustomSnooze { | ||
| 1047 | 1082 | }; |
| 1048 | 1083 | let fields = match self.mode { |
| 1049 | 1084 | SnoozeMode::Date => { |
| 1050 | - let today = Local::now().date_naive(); | |
| 1051 | 1085 | let date = self.date; |
| 1052 | - let entity = entity.clone(); | |
| 1053 | - let date_button = MetalButton::new("snooze-date") | |
| 1054 | - .compact() | |
| 1055 | - .w_full() | |
| 1056 | - .justify_between() | |
| 1057 | - .label(date.format("%b %-d, %Y").to_string()) | |
| 1058 | - .stretch() | |
| 1059 | - .icon(IconName::Calendar) | |
| 1060 | - .dropdown_menu(move |mut menu, _, _| { | |
| 1061 | - // The next five weeks, past days left out. | |
| 1062 | - for offset in 0..35 { | |
| 1063 | - let day = today + chrono::Duration::days(offset); | |
| 1064 | - let entity = entity.clone(); | |
| 1065 | - menu = menu.item( | |
| 1066 | - PopupMenuItem::new(day.format("%a, %b %-d, %Y").to_string()).checked(day == date).on_click( | |
| 1067 | - move |_, _, cx| { | |
| 1068 | - let _ = entity.update(cx, |this, cx| { | |
| 1069 | - this.date = day; | |
| 1070 | - this.error = None; | |
| 1071 | - cx.notify(); | |
| 1072 | - }); | |
| 1073 | - }, | |
| 1074 | - ), | |
| 1075 | - ); | |
| 1076 | - } | |
| 1077 | - menu.max_h(px(320.)).scrollable(true) | |
| 1086 | + let toggle = entity.clone(); | |
| 1087 | + let close = entity.clone(); | |
| 1088 | + let date_button = div() | |
| 1089 | + .relative() | |
| 1090 | + .child( | |
| 1091 | + MetalButton::new("snooze-date") | |
| 1092 | + .compact() | |
| 1093 | + .w_full() | |
| 1094 | + .label(date.format("%b %-d, %Y").to_string()) | |
| 1095 | + .stretch() | |
| 1096 | + .icon(IconName::Calendar) | |
| 1097 | + .on_click(move |_, _, cx| { | |
| 1098 | + let _ = toggle.update(cx, |this, cx| { | |
| 1099 | + this.calendar_open = !this.calendar_open; | |
| 1100 | + cx.notify(); | |
| 1101 | + }); | |
| 1102 | + }), | |
| 1103 | + ) | |
| 1104 | + .when(self.calendar_open, |this| { | |
| 1105 | + this.child( | |
| 1106 | + div().absolute().top_full().left_0().child( | |
| 1107 | + deferred( | |
| 1108 | + anchored().snap_to_window_with_margin(px(8.)).child( | |
| 1109 | + crate::ui::pickers::panel(p) | |
| 1110 | + .mt(px(4.)) | |
| 1111 | + .p(px(8.)) | |
| 1112 | + .on_mouse_down_out(move |_, _, cx| { | |
| 1113 | + let _ = close.update(cx, |this, cx| { | |
| 1114 | + this.calendar_open = false; | |
| 1115 | + cx.notify(); | |
| 1116 | + }); | |
| 1117 | + }) | |
| 1118 | + .child(Calendar::new(&self.calendar)), | |
| 1119 | + ), | |
| 1120 | + ) | |
| 1121 | + .with_priority(100), | |
| 1122 | + ), | |
| 1123 | + ) | |
| 1078 | 1124 | }); |
| 1079 | 1125 | h_flex() |
| 1126 | + .items_start() | |
| 1080 | 1127 | .gap(px(12.)) |
| 1081 | 1128 | .child(v_flex().flex_1().min_w_0().gap(px(6.)).child(label("Date")).child(date_button)) |
| 1082 | 1129 | .child( |
| @@ -1095,6 +1142,7 @@ impl Render for CustomSnooze { | ||
| 1095 | 1142 | let dec = entity.clone(); |
| 1096 | 1143 | let inc = entity.clone(); |
| 1097 | 1144 | h_flex() |
| 1145 | + .items_start() | |
| 1098 | 1146 | .gap(px(12.)) |
| 1099 | 1147 | .child( |
| 1100 | 1148 | v_flex().flex_1().min_w_0().gap(px(6.)).child(label("Snooze for")).child( |