Files
Ely-GPUI-Components/src/calendar/popover.rs
T
ZacharyZhang-NY 1879f6f08b Onboarding · Flows and highlights: focus that holds as the flow turns, a tip that closes
navigation::Wizard keeps one focus handle for Next and Finish, so a
second press on the step before last finishes; Back that reaches the
first step, and a finished step picked by key, hand focus to Next.
Popover::with_opener hands its content a close that gives focus back:
Got it closes the hotspot's tip before the owner hides it, and the
event details close before Edit and Delete run, so focus never lands
on a panel that left the tree. The wizard's count and the setup
checklist's tally name their selectors after their words, and the bar
takes the count's own value.
2026-09-27 14:48:20 -04:00

187 lines
6.3 KiB
Rust

use std::rc::Rc;
use gpui::{
AnyElement, App, ElementId, FontWeight, InteractiveElement, IntoElement, ParentElement,
RenderOnce, SharedString, Styled, Window, div, prelude::*,
};
use jiff::tz::TimeZone;
use super::{
chip::{EventChip, tint},
event::{Event, When, hours},
people::{Attendee, AttendeeList},
};
use crate::{
buttons::{Button, ButtonVariant},
data_display::color_mark,
forms::{OnValue, Run},
overlays::Popover,
primitives::{Icon, IconName},
theme::{ActiveTheme, ControlSize, IconSize, TextSize},
typography::format,
};
/// When an event happens, in full: the day's name and its hours, or its span of whole days.
pub(crate) fn when(event: &Event, zone: &TimeZone) -> String {
let (first, _) = event.days(zone);
match event.when {
When::AllDay { .. } if event.spans(zone) => hours(event, zone),
_ => format!("{} · {}", first.strftime("%A, %B %-d"), hours(event, zone)),
}
}
/// An event's chip that opens its details: its title in its hue, when and where, who comes, and what can be done with it. A press outside or Escape closes them.
#[derive(IntoElement)]
pub struct EventPopover {
id: ElementId,
event: Event,
zone: Option<TimeZone>,
attendees: Vec<Attendee>,
on_edit: Option<OnValue>,
on_delete: Option<OnValue>,
}
impl EventPopover {
pub fn new(id: impl Into<ElementId>, event: Event) -> Self {
Self {
id: id.into(),
event,
zone: None,
attendees: Vec::new(),
on_edit: None,
on_delete: None,
}
}
/// The zone its hours read in; the system's otherwise.
pub fn zone(mut self, zone: TimeZone) -> Self {
self.zone = Some(zone);
self
}
/// Who is asked, shown under where.
pub fn attendees(mut self, attendees: impl IntoIterator<Item = Attendee>) -> Self {
self.attendees = attendees.into_iter().collect();
self
}
/// Offers Edit; gets the event's key.
pub fn on_edit(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_edit = Some(Rc::new(handler));
self
}
/// Offers Delete; gets the event's key.
pub fn on_delete(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_delete = Some(Rc::new(handler));
self
}
}
/// A line of the details: an icon, then its words, wrapping.
fn line(icon: IconName, words: String, cx: &App) -> AnyElement {
let theme = cx.theme();
div()
.flex()
.items_start()
.gap_2()
.child(
Icon::new(icon)
.size(IconSize::Sm)
.color(theme.colors.fg_subtle),
)
.child(div().flex_1().min_w_0().child(words))
.into_any_element()
}
impl RenderOnce for EventPopover {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let zone = self
.zone
.unwrap_or_else(|| format::system_zone("event popover"));
let (event, chip_zone) = (self.event.clone(), zone.clone());
let (id, chip_id) = (self.id.clone(), (self.id.clone(), "chip"));
let details = move |close: Run, _: &mut Window, cx: &mut App| {
let theme = cx.theme();
let key = self.event.key.clone();
let action = |label: &'static str, variant, handler: Option<OnValue>| {
handler.map(|handler| {
let (key, close) = (key.clone(), close.clone());
Button::new((self.id.clone(), label), label)
.variant(variant)
.size(ControlSize::Sm)
.on_click(move |_, window, cx| {
log::info!("event popover: {label} {key}");
close(window, cx);
handler(&key, window, cx)
})
})
};
let delete = action("Delete", ButtonVariant::Ghost, self.on_delete.clone());
let edit = action("Edit", ButtonVariant::Secondary, self.on_edit.clone());
let key = self.event.key.clone();
div()
.debug_selector(move || format!("event-popover {key}"))
.w(theme.calendar().card)
.flex()
.flex_col()
.gap_3()
.text_size(theme.text_size(TextSize::Sm))
.text_color(theme.colors.fg)
.child(
div()
.flex()
.items_start()
.gap_2()
.child(color_mark(tint(&self.event, cx), cx))
.child(
div()
.flex_1()
.min_w_0()
.text_size(theme.text_size(TextSize::Base))
.font_weight(FontWeight::SEMIBOLD)
.child(self.event.title.clone()),
),
)
.child(line(IconName::Clock, when(&self.event, &zone), cx))
.children(
self.event
.place
.clone()
.map(|place| line(IconName::MapPin, place.to_string(), cx)),
)
.when(!self.attendees.is_empty(), |details| {
details.child(AttendeeList::new(
(self.id.clone(), "guests"),
self.attendees.clone(),
))
})
.when(delete.is_some() || edit.is_some(), |details| {
details.child(
div()
.flex()
.justify_end()
.gap_2()
.children(delete)
.children(edit),
)
})
};
Popover::with_opener(
id,
move |toggle| {
EventChip::new(chip_id, event)
.zone(chip_zone)
.on_click(move |window, cx| toggle(window, cx))
},
details,
)
}
}