gpui_component/time/
time_field.rs1use std::sync::Arc;
2
3use gpui::{
4 App, ElementId, Entity, Focusable as _, FontFeatures, IntoElement, ParentElement as _,
5 RenderOnce, StyleRefinement, Styled, Window, div, prelude::FluentBuilder as _,
6};
7
8use crate::{
9 ActiveTheme as _, Disableable, Sizable, Size, StyleSized as _, StyledExt as _,
10 ThemeStyled as _, input::input_style,
11};
12
13use gpui_base::TimeField as BaseTimeField;
14pub use gpui_base::{HourCycle, TimeFieldEvent, TimeFieldState, TimePrecision, TimeSegment};
15
16pub(crate) fn tabular_figures() -> FontFeatures {
19 FontFeatures(Arc::new(vec![("tnum".into(), 1)]))
20}
21
22fn period_label(pm: bool) -> impl IntoElement {
25 let label = |text: &'static str, active: bool| {
26 div()
27 .col_start(1)
28 .row_start(1)
29 .when(!active, |this| this.text_color(gpui::transparent_black()))
30 .child(text)
31 };
32 div()
33 .grid()
34 .grid_cols(1)
35 .child(label("AM", !pm))
36 .child(label("PM", pm))
37}
38
39#[derive(IntoElement)]
43pub struct TimeField {
44 id: ElementId,
45 state: Entity<TimeFieldState>,
46 size: Size,
47 style: StyleRefinement,
48 disabled: bool,
49 invalid: bool,
50}
51
52impl TimeField {
53 pub fn new(state: &Entity<TimeFieldState>) -> Self {
54 Self {
55 id: ("time-field", state.entity_id()).into(),
56 state: state.clone(),
57 size: Size::default(),
58 style: StyleRefinement::default(),
59 disabled: false,
60 invalid: false,
61 }
62 }
63
64 pub(crate) fn with_id(mut self, id: impl Into<ElementId>) -> Self {
65 self.id = id.into();
66 self
67 }
68
69 pub fn invalid(mut self, invalid: bool) -> Self {
71 self.invalid = invalid;
72 self
73 }
74}
75
76impl Sizable for TimeField {
77 fn with_size(mut self, size: impl Into<Size>) -> Self {
78 self.size = size.into();
79 self
80 }
81}
82
83impl Disableable for TimeField {
84 fn disabled(mut self, disabled: bool) -> Self {
85 self.disabled = disabled;
86 self
87 }
88}
89
90impl Styled for TimeField {
91 fn style(&mut self) -> &mut StyleRefinement {
92 &mut self.style
93 }
94}
95
96impl RenderOnce for TimeField {
97 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
98 let focused = self.state.read(cx).focus_handle(cx).is_focused(window);
99 let (bg, fg) = input_style(self.disabled, cx);
100 let segment_radius = cx.theme().radius / 2.;
101
102 div()
103 .flex()
104 .items_center()
105 .flex_none()
106 .font_features(tabular_figures())
107 .bg(bg)
108 .text_color(fg)
109 .border_1()
110 .border_color(cx.theme().input)
111 .rounded(cx.theme().radius)
112 .input_text_size(self.size)
113 .input_h(self.size)
114 .px_1()
115 .when(self.disabled, |this| this.opacity(0.5))
116 .when(focused && !self.disabled, |this| {
117 this.focus_ring_style(window, cx)
118 })
119 .when(self.invalid, |this| this.border_color(cx.theme().danger))
120 .child(
121 BaseTimeField::new(self.id, &self.state)
122 .disabled(self.disabled)
123 .flex()
124 .h_full()
125 .items_center()
126 .render_segment(move |segment, state, _, cx| {
127 segment
128 .px_0p5()
129 .when(state.segment() == TimeSegment::Period, |this| {
130 this.ml_1()
131 .clear_children()
132 .child(period_label(state.value() == 1))
133 })
134 .rounded(segment_radius)
135 .when(state.is_selected(), |this| this.bg(cx.theme().selection))
136 .into_any_element()
137 }),
138 )
139 .refine_style(&self.style)
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use gpui::{AppContext as _, px};
146
147 use super::*;
148
149 #[gpui::test]
150 fn test_time_field_builder(cx: &mut gpui::TestAppContext) {
151 let window = cx.add_empty_window();
152 let state = window.update(|window, cx| {
153 cx.new(|cx| {
154 TimeFieldState::new(window, cx)
155 .precision(TimePrecision::Second)
156 .hour_cycle(HourCycle::H12)
157 })
158 });
159 let field = TimeField::new(&state)
160 .with_id("start")
161 .large()
162 .disabled(true)
163 .invalid(true)
164 .w(px(120.));
165
166 assert_eq!(field.id, "start".into());
167 assert_eq!(field.size, Size::Large);
168 assert!(field.disabled);
169 assert!(field.invalid);
170 }
171}