1use gpui::prelude::*;
18use gpui::{
19 deferred, div, px, App, Context, Entity, EventEmitter, FocusHandle, Hsla, IntoElement,
20 KeyDownEvent, MouseButton, SharedString, Window,
21};
22
23use super::{apply_key, control_metrics, edit::TextEdit, Field, KeyOutcome};
24use crate::reactive::Signal;
25use crate::theme::{css, theme, Color, ColorName, Size};
26
27#[derive(Debug, Clone, Copy)]
29pub struct ColorInputEvent(pub Hsla);
30
31pub struct ColorInput {
34 edit: TextEdit,
35 value: Hsla,
36 open: bool,
37 focus: FocusHandle,
38 label: Option<SharedString>,
39 description: Option<SharedString>,
40 error: Option<SharedString>,
41 size: Size,
42 disabled: bool,
43}
44
45impl EventEmitter<ColorInputEvent> for ColorInput {}
46
47fn to_hex(color: Hsla) -> String {
49 let c = Color::from_hsla(color);
50 format!("#{:02x}{:02x}{:02x}", c.r, c.g, c.b)
51}
52
53fn opaque(color: Hsla) -> Hsla {
57 Hsla { a: 1.0, ..color }
58}
59
60impl ColorInput {
61 pub fn new(cx: &mut Context<Self>) -> Self {
62 let value = gpui::black();
63 ColorInput {
64 edit: TextEdit::new(&to_hex(value)),
65 value,
66 open: false,
67 focus: cx.focus_handle(),
68 label: None,
69 description: None,
70 error: None,
71 size: Size::Sm,
72 disabled: false,
73 }
74 }
75
76 pub fn value(mut self, color: impl Into<Hsla>) -> Self {
79 let color = opaque(color.into());
80 self.value = color;
81 self.edit = TextEdit::new(&to_hex(color));
82 self
83 }
84
85 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
86 self.label = Some(label.into());
87 self
88 }
89
90 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
91 self.description = Some(description.into());
92 self
93 }
94
95 pub fn error(mut self, error: impl Into<SharedString>) -> Self {
96 self.error = Some(error.into());
97 self
98 }
99
100 pub fn size(mut self, size: Size) -> Self {
101 self.size = size;
102 self
103 }
104
105 pub fn disabled(mut self, disabled: bool) -> Self {
106 self.disabled = disabled;
107 self
108 }
109
110 pub fn color_value(&self) -> Hsla {
112 self.value
113 }
114
115 pub fn bind(entity: &Entity<ColorInput>, signal: &Signal<Hsla>, cx: &mut App) {
121 let initial = signal.get(cx);
122 entity.update(cx, |this, cx| this.sync_value(initial, cx));
123 let sink = signal.clone();
124 cx.subscribe(entity, move |_input, event: &ColorInputEvent, cx| {
125 sink.set_if_changed(cx, event.0);
126 })
127 .detach();
128 let input = entity.downgrade();
129 cx.observe(signal.entity(), move |observed, cx| {
130 let value = *observed.read(cx);
131 input.update(cx, |this, cx| this.sync_value(value, cx)).ok();
132 })
133 .detach();
134 }
135
136 fn sync_value(&mut self, color: Hsla, cx: &mut Context<Self>) {
138 let color = opaque(color);
139 if self.value != color {
140 self.value = color;
141 self.edit = TextEdit::new(&to_hex(color));
142 cx.notify();
143 }
144 }
145
146 fn choose(&mut self, color: Hsla, cx: &mut Context<Self>) {
148 self.open = false;
149 self.edit = TextEdit::new(&to_hex(color));
150 if self.value != color {
151 self.value = color;
152 cx.emit(ColorInputEvent(color));
153 }
154 cx.notify();
155 }
156
157 fn adopt_buffer(&mut self, cx: &mut Context<Self>) {
159 if let Ok(color) = css(&self.edit.text()).map(opaque) {
160 if self.value != color {
161 self.value = color;
162 cx.emit(ColorInputEvent(color));
163 }
164 }
165 }
166
167 fn on_key(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
168 if self.disabled {
169 return;
170 }
171 match apply_key(&mut self.edit, &event.keystroke) {
172 KeyOutcome::Submit => {
173 self.adopt_buffer(cx);
174 self.edit = TextEdit::new(&to_hex(self.value));
176 self.open = false;
177 cx.notify();
178 cx.stop_propagation();
179 }
180 KeyOutcome::Edited => {
181 self.adopt_buffer(cx);
182 cx.notify();
183 cx.stop_propagation();
184 }
185 KeyOutcome::Cancel => {
186 if self.open {
188 self.open = false;
189 cx.notify();
190 cx.stop_propagation();
191 }
192 }
193 KeyOutcome::Pass => {}
194 }
195 }
196}
197
198impl Render for ColorInput {
199 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
200 let t = theme(cx);
201 let (height, pad_x, font) = control_metrics(self.size);
202 let radius = t.radius(t.default_radius);
203 let focused = self.focus.is_focused(window) && !self.disabled;
204
205 let border = if self.error.is_some() {
206 t.color(ColorName::Red, 6)
207 } else if focused {
208 t.primary()
209 } else {
210 t.border()
211 }
212 .hsla();
213 let plain_border = t.border().hsla();
214 let text_color = t.text().hsla();
215 let surface = t.surface().hsla();
216 let caret = t.primary().hsla();
217 let swatch_px = height - 16.0;
218
219 let swatch = div()
220 .id("guise-colorinput-swatch")
221 .flex_none()
222 .w(px(swatch_px))
223 .h(px(swatch_px))
224 .rounded(px(4.0))
225 .border_1()
226 .border_color(plain_border)
227 .bg(self.value)
228 .cursor_pointer()
229 .on_click(cx.listener(|this, _ev, window, cx| {
230 if !this.disabled {
231 this.open = !this.open;
232 window.focus(&this.focus);
233 cx.notify();
234 }
235 }));
236
237 let interior = if focused {
238 let (before, after) = self.edit.split();
239 div()
240 .flex()
241 .items_center()
242 .text_color(text_color)
243 .child(SharedString::from(before))
244 .child(div().w(px(1.0)).h(px(font * 1.15)).bg(caret))
245 .child(SharedString::from(after))
246 } else {
247 div()
248 .text_color(text_color)
249 .child(SharedString::from(self.edit.text()))
250 };
251
252 let field = div()
253 .id("guise-colorinput")
254 .track_focus(&self.focus)
255 .on_key_down(cx.listener(Self::on_key))
256 .on_mouse_down(
257 MouseButton::Left,
258 cx.listener(|this, _ev, window, cx| {
259 window.focus(&this.focus);
260 cx.notify();
261 }),
262 )
263 .flex()
264 .items_center()
265 .gap(px(8.0))
266 .h(px(height))
267 .px(px(pad_x))
268 .rounded(px(radius))
269 .border_1()
270 .border_color(border)
271 .bg(surface)
272 .text_size(px(font))
273 .child(swatch)
274 .child(interior);
275
276 let mut wrap = div().relative().child(field);
277
278 if self.open && !self.disabled {
279 let current = Color::from_hsla(self.value);
280 let mut grid = div()
281 .occlude()
282 .absolute()
283 .top(px(height + 6.0))
284 .left(px(0.0))
285 .flex()
286 .flex_col()
287 .gap(px(2.0))
288 .p(px(6.0))
289 .rounded(px(radius))
290 .border_1()
291 .border_color(plain_border)
292 .bg(surface)
293 .shadow_md();
294
295 for (row, name) in ColorName::ALL.into_iter().enumerate() {
296 let mut cells = div().flex().flex_row().gap(px(2.0));
297 for shade in 0..10 {
298 let cell_color = t.color(name, shade);
299 let cell_hsla = cell_color.hsla();
300 let mut cell = div()
301 .id(("guise-colorinput-cell", row * 10 + shade))
302 .w(px(14.0))
303 .h(px(14.0))
304 .rounded(px(3.0))
305 .bg(cell_hsla)
306 .cursor_pointer()
307 .on_click(cx.listener(move |this, _ev, _window, cx| {
308 this.choose(cell_hsla, cx);
309 }));
310 if cell_color == current {
311 cell = cell
312 .border_2()
313 .border_color(cell_color.contrasting().hsla());
314 }
315 cells = cells.child(cell);
316 }
317 grid = grid.child(cells);
318 }
319
320 wrap = wrap.child(deferred(grid));
321 }
322
323 let mut chrome = Field::new().child(if self.disabled {
324 wrap.opacity(0.6)
325 } else {
326 wrap
327 });
328 if let Some(label) = self.label.clone() {
329 chrome = chrome.label(label);
330 }
331 if let Some(error) = self.error.clone() {
332 chrome = chrome.error(error);
333 } else if let Some(description) = self.description.clone() {
334 chrome = chrome.description(description);
335 }
336 chrome
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::to_hex;
343 use crate::theme::css;
344
345 #[test]
346 fn hex_round_trips_through_css_parsing() {
347 for hex in [
348 "#ff0000", "#00ff00", "#0000ff", "#ffffff", "#000000", "#808080",
349 ] {
350 assert_eq!(to_hex(css(hex).unwrap()), hex);
351 }
352 }
353
354 #[test]
355 fn alpha_is_dropped() {
356 let translucent = css("rgba(255, 0, 0, 0.5)").unwrap();
357 assert_eq!(to_hex(translucent), "#ff0000");
358 }
359
360 #[test]
361 fn adopted_colors_are_opaque() {
362 let adopted = super::opaque(css("rgba(255, 0, 0, 0.5)").unwrap());
365 assert_eq!(adopted, css("rgb(255, 0, 0)").unwrap());
366 assert_eq!(adopted.a, 1.0);
367 }
368}