use gpui::prelude::*;
use gpui::{
deferred, div, px, App, Context, Entity, EventEmitter, FocusHandle, Hsla, IntoElement,
KeyDownEvent, MouseButton, SharedString, Window,
};
use super::{apply_key, control_metrics, edit::TextEdit, Field, KeyOutcome};
use crate::reactive::Signal;
use crate::theme::{css, theme, Color, ColorName, Size};
#[derive(Debug, Clone, Copy)]
pub struct ColorInputEvent(pub Hsla);
pub struct ColorInput {
edit: TextEdit,
value: Hsla,
open: bool,
focus: FocusHandle,
label: Option<SharedString>,
description: Option<SharedString>,
error: Option<SharedString>,
size: Size,
disabled: bool,
}
impl EventEmitter<ColorInputEvent> for ColorInput {}
fn to_hex(color: Hsla) -> String {
let c = Color::from_hsla(color);
format!("#{:02x}{:02x}{:02x}", c.r, c.g, c.b)
}
fn opaque(color: Hsla) -> Hsla {
Hsla { a: 1.0, ..color }
}
impl ColorInput {
pub fn new(cx: &mut Context<Self>) -> Self {
let value = gpui::black();
ColorInput {
edit: TextEdit::new(&to_hex(value)),
value,
open: false,
focus: cx.focus_handle(),
label: None,
description: None,
error: None,
size: Size::Sm,
disabled: false,
}
}
pub fn value(mut self, color: impl Into<Hsla>) -> Self {
let color = opaque(color.into());
self.value = color;
self.edit = TextEdit::new(&to_hex(color));
self
}
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
self.description = Some(description.into());
self
}
pub fn error(mut self, error: impl Into<SharedString>) -> Self {
self.error = Some(error.into());
self
}
pub fn size(mut self, size: Size) -> Self {
self.size = size;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn color_value(&self) -> Hsla {
self.value
}
pub fn bind(entity: &Entity<ColorInput>, signal: &Signal<Hsla>, cx: &mut App) {
let initial = signal.get(cx);
entity.update(cx, |this, cx| this.sync_value(initial, cx));
let sink = signal.clone();
cx.subscribe(entity, move |_input, event: &ColorInputEvent, cx| {
sink.set_if_changed(cx, event.0);
})
.detach();
let input = entity.downgrade();
cx.observe(signal.entity(), move |observed, cx| {
let value = *observed.read(cx);
input.update(cx, |this, cx| this.sync_value(value, cx)).ok();
})
.detach();
}
fn sync_value(&mut self, color: Hsla, cx: &mut Context<Self>) {
let color = opaque(color);
if self.value != color {
self.value = color;
self.edit = TextEdit::new(&to_hex(color));
cx.notify();
}
}
fn choose(&mut self, color: Hsla, cx: &mut Context<Self>) {
self.open = false;
self.edit = TextEdit::new(&to_hex(color));
if self.value != color {
self.value = color;
cx.emit(ColorInputEvent(color));
}
cx.notify();
}
fn adopt_buffer(&mut self, cx: &mut Context<Self>) {
if let Ok(color) = css(&self.edit.text()).map(opaque) {
if self.value != color {
self.value = color;
cx.emit(ColorInputEvent(color));
}
}
}
fn on_key(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
if self.disabled {
return;
}
match apply_key(&mut self.edit, &event.keystroke) {
KeyOutcome::Submit => {
self.adopt_buffer(cx);
self.edit = TextEdit::new(&to_hex(self.value));
self.open = false;
cx.notify();
cx.stop_propagation();
}
KeyOutcome::Edited => {
self.adopt_buffer(cx);
cx.notify();
cx.stop_propagation();
}
KeyOutcome::Cancel => {
if self.open {
self.open = false;
cx.notify();
cx.stop_propagation();
}
}
KeyOutcome::Pass => {}
}
}
}
impl Render for ColorInput {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let t = theme(cx);
let (height, pad_x, font) = control_metrics(self.size);
let radius = t.radius(t.default_radius);
let focused = self.focus.is_focused(window) && !self.disabled;
let border = if self.error.is_some() {
t.color(ColorName::Red, 6)
} else if focused {
t.primary()
} else {
t.border()
}
.hsla();
let plain_border = t.border().hsla();
let text_color = t.text().hsla();
let surface = t.surface().hsla();
let caret = t.primary().hsla();
let swatch_px = height - 16.0;
let swatch = div()
.id("guise-colorinput-swatch")
.flex_none()
.w(px(swatch_px))
.h(px(swatch_px))
.rounded(px(4.0))
.border_1()
.border_color(plain_border)
.bg(self.value)
.cursor_pointer()
.on_click(cx.listener(|this, _ev, window, cx| {
if !this.disabled {
this.open = !this.open;
window.focus(&this.focus);
cx.notify();
}
}));
let interior = if focused {
let (before, after) = self.edit.split();
div()
.flex()
.items_center()
.text_color(text_color)
.child(SharedString::from(before))
.child(div().w(px(1.0)).h(px(font * 1.15)).bg(caret))
.child(SharedString::from(after))
} else {
div()
.text_color(text_color)
.child(SharedString::from(self.edit.text()))
};
let field = div()
.id("guise-colorinput")
.track_focus(&self.focus)
.on_key_down(cx.listener(Self::on_key))
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _ev, window, cx| {
window.focus(&this.focus);
cx.notify();
}),
)
.flex()
.items_center()
.gap(px(8.0))
.h(px(height))
.px(px(pad_x))
.rounded(px(radius))
.border_1()
.border_color(border)
.bg(surface)
.text_size(px(font))
.child(swatch)
.child(interior);
let mut wrap = div().relative().child(field);
if self.open && !self.disabled {
let current = Color::from_hsla(self.value);
let mut grid = div()
.occlude()
.absolute()
.top(px(height + 6.0))
.left(px(0.0))
.flex()
.flex_col()
.gap(px(2.0))
.p(px(6.0))
.rounded(px(radius))
.border_1()
.border_color(plain_border)
.bg(surface)
.shadow_md();
for (row, name) in ColorName::ALL.into_iter().enumerate() {
let mut cells = div().flex().flex_row().gap(px(2.0));
for shade in 0..10 {
let cell_color = t.color(name, shade);
let cell_hsla = cell_color.hsla();
let mut cell = div()
.id(("guise-colorinput-cell", row * 10 + shade))
.w(px(14.0))
.h(px(14.0))
.rounded(px(3.0))
.bg(cell_hsla)
.cursor_pointer()
.on_click(cx.listener(move |this, _ev, _window, cx| {
this.choose(cell_hsla, cx);
}));
if cell_color == current {
cell = cell
.border_2()
.border_color(cell_color.contrasting().hsla());
}
cells = cells.child(cell);
}
grid = grid.child(cells);
}
wrap = wrap.child(deferred(grid));
}
let mut chrome = Field::new().child(if self.disabled {
wrap.opacity(0.6)
} else {
wrap
});
if let Some(label) = self.label.clone() {
chrome = chrome.label(label);
}
if let Some(error) = self.error.clone() {
chrome = chrome.error(error);
} else if let Some(description) = self.description.clone() {
chrome = chrome.description(description);
}
chrome
}
}
#[cfg(test)]
mod tests {
use super::to_hex;
use crate::theme::css;
#[test]
fn hex_round_trips_through_css_parsing() {
for hex in [
"#ff0000", "#00ff00", "#0000ff", "#ffffff", "#000000", "#808080",
] {
assert_eq!(to_hex(css(hex).unwrap()), hex);
}
}
#[test]
fn alpha_is_dropped() {
let translucent = css("rgba(255, 0, 0, 0.5)").unwrap();
assert_eq!(to_hex(translucent), "#ff0000");
}
#[test]
fn adopted_colors_are_opaque() {
let adopted = super::opaque(css("rgba(255, 0, 0, 0.5)").unwrap());
assert_eq!(adopted, css("rgb(255, 0, 0)").unwrap());
assert_eq!(adopted.a, 1.0);
}
}