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