Improve ColorPicker to support input hex color. (#201)
https://github.com/user-attachments/assets/be5fc290-0464-4356-9f97-c35720e3e11d
This commit is contained in:
parent
a049f606c0
commit
f5cae46e42
7 changed files with 300 additions and 162 deletions
|
|
@ -11,12 +11,13 @@ use workspace::TitleBar;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use ui::{
|
use ui::{
|
||||||
button::Button,
|
button::Button,
|
||||||
|
color_picker::{ColorPicker, ColorPickerEvent},
|
||||||
dock::{DockArea, StackPanel, TabPanel},
|
dock::{DockArea, StackPanel, TabPanel},
|
||||||
drawer::Drawer,
|
drawer::Drawer,
|
||||||
h_flex,
|
h_flex,
|
||||||
modal::Modal,
|
modal::Modal,
|
||||||
popup_menu::PopupMenuExt,
|
popup_menu::PopupMenuExt,
|
||||||
theme::{ActiveTheme, Theme},
|
theme::{ActiveTheme, Colorize as _, Theme},
|
||||||
ContextModal, IconName, Root, Sizable,
|
ContextModal, IconName, Root, Sizable,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -38,8 +39,9 @@ pub fn init(_app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StoryWorkspace {
|
pub struct StoryWorkspace {
|
||||||
locale_selector: View<LocaleSelector>,
|
|
||||||
dock_area: View<DockArea>,
|
dock_area: View<DockArea>,
|
||||||
|
locale_selector: View<LocaleSelector>,
|
||||||
|
theme_color_picker: View<ColorPicker>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StoryWorkspace {
|
impl StoryWorkspace {
|
||||||
|
|
@ -228,9 +230,34 @@ impl StoryWorkspace {
|
||||||
|
|
||||||
let locale_selector = cx.new_view(LocaleSelector::new);
|
let locale_selector = cx.new_view(LocaleSelector::new);
|
||||||
|
|
||||||
|
let theme_color_picker = cx.new_view(|cx| {
|
||||||
|
let mut picker = ColorPicker::new("theme-color-picker", cx)
|
||||||
|
.xsmall()
|
||||||
|
.anchor(AnchorCorner::TopRight)
|
||||||
|
.label("Primary Color");
|
||||||
|
picker.set_value(cx.theme().primary, cx);
|
||||||
|
picker
|
||||||
|
});
|
||||||
|
cx.subscribe(
|
||||||
|
&theme_color_picker,
|
||||||
|
|_, _, ev: &ColorPickerEvent, cx| match ev {
|
||||||
|
ColorPickerEvent::Change(color) => {
|
||||||
|
if let Some(color) = color {
|
||||||
|
let theme = cx.global_mut::<Theme>();
|
||||||
|
theme.primary = *color;
|
||||||
|
theme.primary_hover = color.lighten(0.1);
|
||||||
|
theme.primary_active = color.darken(0.1);
|
||||||
|
cx.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
dock_area,
|
dock_area,
|
||||||
locale_selector,
|
locale_selector,
|
||||||
|
theme_color_picker,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -326,7 +353,7 @@ impl Render for StoryWorkspace {
|
||||||
.justify_end()
|
.justify_end()
|
||||||
.px_2()
|
.px_2()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(self.locale_selector.clone())
|
.child(self.theme_color_picker.clone())
|
||||||
.child(
|
.child(
|
||||||
Button::new("theme-mode", cx)
|
Button::new("theme-mode", cx)
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
|
|
@ -347,6 +374,7 @@ impl Render for StoryWorkspace {
|
||||||
Theme::change(mode, cx);
|
Theme::change(mode, cx);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.child(self.locale_selector.clone())
|
||||||
.child(
|
.child(
|
||||||
Button::new("github", cx)
|
Button::new("github", cx)
|
||||||
.icon(IconName::GitHub)
|
.icon(IconName::GitHub)
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,9 @@ use gpui::{
|
||||||
use ui::{
|
use ui::{
|
||||||
button::Button,
|
button::Button,
|
||||||
checkbox::Checkbox,
|
checkbox::Checkbox,
|
||||||
color_picker::{ColorPicker, ColorPickerEvent},
|
|
||||||
h_flex,
|
h_flex,
|
||||||
input::{InputEvent, OtpInput, TextInput},
|
input::{InputEvent, OtpInput, TextInput},
|
||||||
prelude::FluentBuilder as _,
|
prelude::FluentBuilder as _,
|
||||||
theme::{Colorize, Theme},
|
|
||||||
v_flex, FocusableCycle, IconName, Sizable,
|
v_flex, FocusableCycle, IconName, Sizable,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -44,7 +42,6 @@ pub struct InputStory {
|
||||||
otp_input_small: View<OtpInput>,
|
otp_input_small: View<OtpInput>,
|
||||||
otp_input_large: View<OtpInput>,
|
otp_input_large: View<OtpInput>,
|
||||||
opt_input_sized: View<OtpInput>,
|
opt_input_sized: View<OtpInput>,
|
||||||
color_picker: View<ColorPicker>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputStory {
|
impl InputStory {
|
||||||
|
|
@ -106,23 +103,6 @@ impl InputStory {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let color_picker = cx.new_view(|cx| {
|
|
||||||
let picker = ColorPicker::new("picker1", cx);
|
|
||||||
picker
|
|
||||||
});
|
|
||||||
cx.subscribe(&color_picker, |_, _, ev: &ColorPickerEvent, cx| match ev {
|
|
||||||
ColorPickerEvent::Change(color) => {
|
|
||||||
if let Some(color) = color {
|
|
||||||
let theme = cx.global_mut::<Theme>();
|
|
||||||
theme.primary = *color;
|
|
||||||
theme.primary_hover = color.lighten(0.1);
|
|
||||||
theme.primary_active = color.darken(0.1);
|
|
||||||
cx.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
input1,
|
input1,
|
||||||
input2,
|
input2,
|
||||||
|
|
@ -167,7 +147,6 @@ impl InputStory {
|
||||||
.default_value("654321")
|
.default_value("654321")
|
||||||
.with_size(px(55.))
|
.with_size(px(55.))
|
||||||
}),
|
}),
|
||||||
color_picker,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -218,7 +197,6 @@ impl FocusableCycle for InputStory {
|
||||||
self.suffix_input1.focus_handle(cx),
|
self.suffix_input1.focus_handle(cx),
|
||||||
self.large_input.focus_handle(cx),
|
self.large_input.focus_handle(cx),
|
||||||
self.small_input.focus_handle(cx),
|
self.small_input.focus_handle(cx),
|
||||||
self.color_picker.focus_handle(cx),
|
|
||||||
self.otp_input.focus_handle(cx),
|
self.otp_input.focus_handle(cx),
|
||||||
]
|
]
|
||||||
.to_vec()
|
.to_vec()
|
||||||
|
|
@ -266,7 +244,6 @@ impl Render for InputStory {
|
||||||
.child(self.small_input.clone()),
|
.child(self.small_input.clone()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(section("Color Picker", cx).child(self.color_picker.clone()))
|
|
||||||
.child(
|
.child(
|
||||||
section(
|
section(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,25 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
anchored, deferred, div, prelude::FluentBuilder as _, px, AppContext, ElementId, EventEmitter,
|
anchored, canvas, deferred, div, prelude::FluentBuilder as _, px, relative, AnchorCorner,
|
||||||
FocusHandle, FocusableView, Hsla, InteractiveElement as _, IntoElement, KeyBinding, Length,
|
AppContext, Bounds, ElementId, EventEmitter, FocusHandle, FocusableView, Hsla,
|
||||||
MouseButton, ParentElement, Render, SharedString, StatefulInteractiveElement as _, Styled,
|
InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
|
||||||
ViewContext,
|
Render, SharedString, StatefulInteractiveElement as _, Styled, View, ViewContext,
|
||||||
|
VisualContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
colors::DEFAULT_COLOR,
|
|
||||||
divider::Divider,
|
divider::Divider,
|
||||||
h_flex,
|
h_flex,
|
||||||
input::ClearButton,
|
input::{InputEvent, TextInput},
|
||||||
popover::Escape,
|
popover::Escape,
|
||||||
theme::{ActiveTheme as _, Colorize},
|
theme::{ActiveTheme as _, Colorize},
|
||||||
v_flex, ColorExt as _, Icon, IconName, Size, StyleSized as _, StyledExt as _,
|
tooltip::Tooltip,
|
||||||
|
v_flex, ColorExt as _, Sizable, Size, StyleSized,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const KEY_CONTEXT: &'static str = "ColorPicker";
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
let context = Some("ColorPicker");
|
cx.bind_keys([KeyBinding::new("escape", Escape, Some(KEY_CONTEXT))])
|
||||||
cx.bind_keys([KeyBinding::new("escape", Escape, context)])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -26,6 +28,7 @@ pub enum ColorPickerEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn color_palettes() -> Vec<Vec<Hsla>> {
|
fn color_palettes() -> Vec<Vec<Hsla>> {
|
||||||
|
use crate::colors::DEFAULT_COLOR;
|
||||||
use itertools::Itertools as _;
|
use itertools::Itertools as _;
|
||||||
|
|
||||||
macro_rules! c {
|
macro_rules! c {
|
||||||
|
|
@ -55,22 +58,47 @@ fn color_palettes() -> Vec<Vec<Hsla>> {
|
||||||
pub struct ColorPicker {
|
pub struct ColorPicker {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
featured_colors: Vec<Hsla>,
|
|
||||||
value: Option<Hsla>,
|
value: Option<Hsla>,
|
||||||
cleanable: bool,
|
featured_colors: Vec<Hsla>,
|
||||||
open: bool,
|
|
||||||
size: Size,
|
|
||||||
width: Length,
|
|
||||||
hovered_color: Option<Hsla>,
|
hovered_color: Option<Hsla>,
|
||||||
|
label: Option<SharedString>,
|
||||||
|
size: Size,
|
||||||
|
anchor: AnchorCorner,
|
||||||
|
color_input: View<TextInput>,
|
||||||
|
|
||||||
|
open: bool,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ColorPicker {
|
impl ColorPicker {
|
||||||
pub fn new(id: impl Into<ElementId>, cx: &mut ViewContext<Self>) -> Self {
|
pub fn new(id: impl Into<ElementId>, cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
let color_input = cx.new_view(|cx| TextInput::new(cx).xsmall());
|
||||||
|
|
||||||
|
cx.subscribe(&color_input, |this, _, ev: &InputEvent, cx| match ev {
|
||||||
|
InputEvent::Change(value) => {
|
||||||
|
if let Ok(color) = Hsla::parse_hex_string(value) {
|
||||||
|
this.value = Some(color);
|
||||||
|
this.hovered_color = Some(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputEvent::PressEnter => {
|
||||||
|
let val = this.color_input.read(cx).text();
|
||||||
|
if let Ok(color) = Hsla::parse_hex_string(&val) {
|
||||||
|
this.open = false;
|
||||||
|
this.update_value(Some(color), true, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
featured_colors: vec![
|
featured_colors: vec![
|
||||||
crate::black(),
|
crate::black(),
|
||||||
|
crate::gray_600(),
|
||||||
|
crate::gray_400(),
|
||||||
crate::white(),
|
crate::white(),
|
||||||
crate::red_600(),
|
crate::red_600(),
|
||||||
crate::orange_600(),
|
crate::orange_600(),
|
||||||
|
|
@ -81,43 +109,55 @@ impl ColorPicker {
|
||||||
crate::purple_600(),
|
crate::purple_600(),
|
||||||
],
|
],
|
||||||
value: None,
|
value: None,
|
||||||
cleanable: false,
|
|
||||||
open: false,
|
|
||||||
size: Size::default(),
|
|
||||||
width: Length::Auto,
|
|
||||||
hovered_color: None,
|
hovered_color: None,
|
||||||
|
size: Size::Medium,
|
||||||
|
label: None,
|
||||||
|
anchor: AnchorCorner::TopLeft,
|
||||||
|
color_input,
|
||||||
|
open: false,
|
||||||
|
bounds: Bounds::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set true to show the clear button when the input field is not empty.
|
/// Set the featured colors to be displayed in the color picker.
|
||||||
pub fn cleanable(mut self) -> Self {
|
///
|
||||||
self.cleanable = true;
|
/// This is used to display a set of colors that the user can quickly select from,
|
||||||
self
|
/// for example provided user's last used colors.
|
||||||
}
|
|
||||||
|
|
||||||
/// Set width of the date picker input field, default is `Length::Auto`.
|
|
||||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
|
||||||
self.width = width.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn featured_colors(mut self, colors: Vec<Hsla>) -> Self {
|
pub fn featured_colors(mut self, colors: Vec<Hsla>) -> Self {
|
||||||
self.featured_colors = colors;
|
self.featured_colors = colors;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn value(mut self, value: Hsla) -> Self {
|
/// Set current color value.
|
||||||
self.value = Some(value);
|
pub fn set_value(&mut self, value: Hsla, cx: &mut ViewContext<Self>) {
|
||||||
|
self.update_value(Some(value), false, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the size of the color picker, default is `Size::Medium`.
|
||||||
|
pub fn size(mut self, size: Size) -> Self {
|
||||||
|
self.size = size;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
|
/// Set the label to be displayed above the color picker.
|
||||||
self.open = false;
|
///
|
||||||
cx.notify();
|
/// Default is `None`.
|
||||||
|
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
||||||
|
self.label = Some(label.into());
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clean(&mut self, _: &gpui::ClickEvent, cx: &mut ViewContext<Self>) {
|
/// Set the anchor corner of the color picker.
|
||||||
self.update_value(None, cx)
|
///
|
||||||
|
/// Default is `AnchorCorner::TopLeft`.
|
||||||
|
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
|
||||||
|
self.anchor = anchor;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
|
||||||
|
self.open = false;
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_picker(&mut self, _: &gpui::ClickEvent, cx: &mut ViewContext<Self>) {
|
fn toggle_picker(&mut self, _: &gpui::ClickEvent, cx: &mut ViewContext<Self>) {
|
||||||
|
|
@ -125,9 +165,19 @@ impl ColorPicker {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_value(&mut self, value: Option<Hsla>, cx: &mut ViewContext<Self>) {
|
fn update_value(&mut self, value: Option<Hsla>, emit: bool, cx: &mut ViewContext<Self>) {
|
||||||
self.value = value;
|
self.value = value;
|
||||||
cx.emit(ColorPickerEvent::Change(value));
|
self.hovered_color = value;
|
||||||
|
self.color_input.update(cx, |view, cx| {
|
||||||
|
if let Some(value) = value {
|
||||||
|
view.set_text(value.to_hex_string(), cx);
|
||||||
|
} else {
|
||||||
|
view.set_text("", cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if emit {
|
||||||
|
cx.emit(ColorPickerEvent::Change(value));
|
||||||
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,18 +195,22 @@ impl ColorPicker {
|
||||||
.h_5()
|
.h_5()
|
||||||
.w_5()
|
.w_5()
|
||||||
.bg(color)
|
.bg(color)
|
||||||
.rounded_sm()
|
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(color.darken(0.1))
|
.border_color(color.darken(0.1))
|
||||||
.when(clickable, |this| {
|
.when(clickable, |this| {
|
||||||
this.cursor_pointer()
|
this.cursor_pointer()
|
||||||
.hover(|this| this.border_color(color.darken(0.3)))
|
.hover(|this| {
|
||||||
|
this.border_color(color.darken(0.3))
|
||||||
|
.bg(color.lighten(0.1))
|
||||||
|
.shadow_sm()
|
||||||
|
})
|
||||||
|
.active(|this| this.border_color(color.darken(0.5)).bg(color.darken(0.2)))
|
||||||
.on_mouse_move(cx.listener(move |view, _, cx| {
|
.on_mouse_move(cx.listener(move |view, _, cx| {
|
||||||
view.hovered_color = Some(color);
|
view.hovered_color = Some(color);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}))
|
}))
|
||||||
.on_click(cx.listener(move |view, _, cx| {
|
.on_click(cx.listener(move |view, _, cx| {
|
||||||
view.update_value(Some(color), cx);
|
view.update_value(Some(color), true, cx);
|
||||||
view.open = false;
|
view.open = false;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}))
|
}))
|
||||||
|
|
@ -165,7 +219,7 @@ impl ColorPicker {
|
||||||
|
|
||||||
fn render_colors(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_colors(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_3()
|
||||||
.child(
|
.child(
|
||||||
h_flex().gap_1().children(
|
h_flex().gap_1().children(
|
||||||
self.featured_colors
|
self.featured_colors
|
||||||
|
|
@ -173,6 +227,7 @@ impl ColorPicker {
|
||||||
.map(|color| self.render_item(*color, true, cx)),
|
.map(|color| self.render_item(*color, true, cx)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.child(Divider::horizontal())
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
|
|
@ -185,120 +240,135 @@ impl ColorPicker {
|
||||||
)
|
)
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.when_some(self.hovered_color.clone(), |this, hovered_color| {
|
.when_some(self.hovered_color, |this, hovered_color| {
|
||||||
this.child(Divider::horizontal()).child(
|
this.child(Divider::horizontal()).child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_2()
|
||||||
.items_center()
|
.items_center()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.bg(hovered_color)
|
.bg(hovered_color)
|
||||||
|
.border_1()
|
||||||
|
.border_color(hovered_color.darken(0.2))
|
||||||
.size_5()
|
.size_5()
|
||||||
.rounded(px(cx.theme().radius)),
|
.rounded(px(cx.theme().radius)),
|
||||||
)
|
)
|
||||||
.child(hovered_color.to_hex_string()),
|
.child(self.color_input.clone()),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolved_corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
|
||||||
|
match self.anchor {
|
||||||
|
AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
|
||||||
|
AnchorCorner::TopRight => AnchorCorner::BottomRight,
|
||||||
|
AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
|
||||||
|
AnchorCorner::BottomRight => AnchorCorner::TopRight,
|
||||||
|
}
|
||||||
|
.corner(bounds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Sizable for ColorPicker {
|
||||||
|
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||||
|
self.size = size.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
impl EventEmitter<ColorPickerEvent> for ColorPicker {}
|
impl EventEmitter<ColorPickerEvent> for ColorPicker {}
|
||||||
impl FocusableView for ColorPicker {
|
impl FocusableView for ColorPicker {
|
||||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
|
||||||
self.focus_handle.clone()
|
self.focus_handle.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for ColorPicker {
|
impl Render for ColorPicker {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let is_focused = self.focus_handle.is_focused(cx);
|
let display_title: SharedString = if let Some(value) = self.value {
|
||||||
let show_clean = self.cleanable && self.value.is_some();
|
value.to_hex_string()
|
||||||
|
|
||||||
let display_title = if let Some(value) = self.value {
|
|
||||||
format!("{}", value.to_hex_string())
|
|
||||||
} else {
|
} else {
|
||||||
"Select a color".to_string()
|
"".to_string()
|
||||||
};
|
}
|
||||||
|
.into();
|
||||||
|
|
||||||
let value = self.value.unwrap_or_else(|| cx.theme().foreground);
|
let view = cx.view().clone();
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
.key_context("ColorPicker")
|
.key_context(KEY_CONTEXT)
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.on_action(cx.listener(Self::escape))
|
.on_action(cx.listener(Self::on_escape))
|
||||||
.w_full()
|
|
||||||
.relative()
|
|
||||||
.map(|this| match self.width {
|
|
||||||
Length::Definite(l) => this.flex_none().w(l),
|
|
||||||
Length::Auto => this.w_full(),
|
|
||||||
})
|
|
||||||
.input_text_size(self.size)
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
h_flex()
|
||||||
.id("color-picker-input")
|
.id("color-picker-input")
|
||||||
.relative()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_between()
|
|
||||||
.bg(cx.theme().background)
|
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().input)
|
|
||||||
.rounded(px(cx.theme().radius))
|
|
||||||
.shadow_sm()
|
|
||||||
.cursor_pointer()
|
.cursor_pointer()
|
||||||
.overflow_hidden()
|
.gap_2()
|
||||||
|
.items_center()
|
||||||
.input_text_size(self.size)
|
.input_text_size(self.size)
|
||||||
.when(is_focused, |this| this.outline(cx))
|
.line_height(relative(1.))
|
||||||
.input_size(self.size)
|
|
||||||
.when(!self.open, |this| {
|
|
||||||
this.on_click(cx.listener(Self::toggle_picker))
|
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
div()
|
||||||
.w_full()
|
.id("color-picker-square")
|
||||||
.items_center()
|
.bg(cx.theme().background)
|
||||||
.justify_between()
|
.border_1()
|
||||||
.gap_1()
|
.border_color(cx.theme().input)
|
||||||
.child(self.render_item(value, false, cx))
|
.rounded(px(cx.theme().radius))
|
||||||
.child(div().flex_1().overflow_hidden().child(display_title))
|
.bg(cx.theme().background)
|
||||||
.when(show_clean, |this| {
|
.shadow_sm()
|
||||||
this.child(ClearButton::new(cx).on_click(cx.listener(Self::clean)))
|
.overflow_hidden()
|
||||||
|
.size_with(self.size)
|
||||||
|
.when_some(self.value, |this, value| {
|
||||||
|
this.bg(value).border_color(value.darken(0.3))
|
||||||
})
|
})
|
||||||
.when(!show_clean, |this| {
|
.tooltip(move |cx| Tooltip::new(display_title.clone(), cx)),
|
||||||
this.child(
|
)
|
||||||
Icon::new(IconName::Palette)
|
.when_some(self.label.clone(), |this, label| this.child(label))
|
||||||
.text_color(cx.theme().muted_foreground),
|
.on_click(cx.listener(Self::toggle_picker))
|
||||||
)
|
.child(
|
||||||
}),
|
canvas(
|
||||||
|
move |bounds, cx| view.update(cx, |r, _| r.bounds = bounds),
|
||||||
|
|_, _, _| {},
|
||||||
|
)
|
||||||
|
.absolute()
|
||||||
|
.size_full(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.when(self.open, |this| {
|
.when(self.open, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
deferred(
|
deferred(
|
||||||
anchored().snap_to_window().child(
|
anchored()
|
||||||
div()
|
.anchor(self.anchor)
|
||||||
.track_focus(&self.focus_handle)
|
.snap_to_window()
|
||||||
.occlude()
|
.position(self.resolved_corner(self.bounds))
|
||||||
.absolute()
|
.child(
|
||||||
.mt_1p5()
|
div()
|
||||||
.w_72()
|
.track_focus(&self.focus_handle)
|
||||||
.overflow_hidden()
|
.occlude()
|
||||||
.rounded_lg()
|
.map(|this| match self.anchor {
|
||||||
.p_3()
|
AnchorCorner::TopLeft | AnchorCorner::TopRight => {
|
||||||
.border_1()
|
this.mt_1p5()
|
||||||
.border_color(cx.theme().border)
|
}
|
||||||
.shadow_lg()
|
AnchorCorner::BottomLeft | AnchorCorner::BottomRight => {
|
||||||
.rounded_lg()
|
this.mb_1p5()
|
||||||
.bg(cx.theme().background)
|
}
|
||||||
.on_mouse_up_out(
|
})
|
||||||
MouseButton::Left,
|
.w_72()
|
||||||
cx.listener(|view, _, cx| view.escape(&Escape, cx)),
|
.overflow_hidden()
|
||||||
)
|
.rounded_lg()
|
||||||
.child(self.render_colors(cx)),
|
.p_3()
|
||||||
),
|
.border_1()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.shadow_lg()
|
||||||
|
.rounded_lg()
|
||||||
|
.bg(cx.theme().background)
|
||||||
|
.on_mouse_up_out(
|
||||||
|
MouseButton::Left,
|
||||||
|
cx.listener(|view, _, cx| view.on_escape(&Escape, cx)),
|
||||||
|
)
|
||||||
|
.child(self.render_colors(cx)),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.with_priority(2),
|
.with_priority(1),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@ use serde::{de::Error, Deserialize, Deserializer};
|
||||||
use crate::theme::hsl;
|
use crate::theme::hsl;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
pub trait ColorExt {
|
pub(crate) trait ColorExt {
|
||||||
fn to_hex_string(&self) -> String;
|
fn to_hex_string(&self) -> String;
|
||||||
|
fn parse_hex_string(hex: &str) -> Result<Hsla>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ColorExt for Hsla {
|
impl ColorExt for Hsla {
|
||||||
|
|
@ -17,20 +18,41 @@ impl ColorExt for Hsla {
|
||||||
if rgb.a < 1. {
|
if rgb.a < 1. {
|
||||||
return format!(
|
return format!(
|
||||||
"#{:02X}{:02X}{:02X}{:02X}",
|
"#{:02X}{:02X}{:02X}{:02X}",
|
||||||
u32::from((rgb.r * 255.) as u32),
|
((rgb.r * 255.) as u32),
|
||||||
u32::from((rgb.g * 255.) as u32),
|
((rgb.g * 255.) as u32),
|
||||||
u32::from((rgb.b * 255.) as u32),
|
((rgb.b * 255.) as u32),
|
||||||
u32::from((self.a * 255.) as u32)
|
((self.a * 255.) as u32)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"#{:02X}{:02X}{:02X}",
|
"#{:02X}{:02X}{:02X}",
|
||||||
u32::from((rgb.r * 255.) as u32),
|
((rgb.r * 255.) as u32),
|
||||||
u32::from((rgb.g * 255.) as u32),
|
((rgb.g * 255.) as u32),
|
||||||
u32::from((rgb.b * 255.) as u32)
|
((rgb.b * 255.) as u32)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_hex_string(hex: &str) -> Result<Hsla> {
|
||||||
|
let hex = hex.trim_start_matches('#');
|
||||||
|
let len = hex.len();
|
||||||
|
if len != 6 && len != 8 {
|
||||||
|
return Err(anyhow::anyhow!("invalid hex color"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let r = u8::from_str_radix(&hex[0..2], 16)? as f32 / 255.;
|
||||||
|
let g = u8::from_str_radix(&hex[2..4], 16)? as f32 / 255.;
|
||||||
|
let b = u8::from_str_radix(&hex[4..6], 16)? as f32 / 255.;
|
||||||
|
let a = if len == 8 {
|
||||||
|
u8::from_str_radix(&hex[6..8], 16)? as f32 / 255.
|
||||||
|
} else {
|
||||||
|
1.
|
||||||
|
};
|
||||||
|
|
||||||
|
let v = gpui::Rgba { r, g, b, a };
|
||||||
|
let color: Hsla = v.into();
|
||||||
|
Ok(color)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) static DEFAULT_COLOR: once_cell::sync::Lazy<ShacnColors> =
|
pub(crate) static DEFAULT_COLOR: once_cell::sync::Lazy<ShacnColors> =
|
||||||
|
|
@ -211,6 +233,8 @@ color_methods!(rose);
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use gpui::{rgb, rgba};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -229,4 +253,28 @@ mod tests {
|
||||||
assert_eq!(blue_400(), hsl(213.1, 93.9, 67.8));
|
assert_eq!(blue_400(), hsl(213.1, 93.9, 67.8));
|
||||||
assert_eq!(indigo_500(), hsl(238.7, 83.5, 66.7));
|
assert_eq!(indigo_500(), hsl(238.7, 83.5, 66.7));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_to_hex_string() {
|
||||||
|
let color: Hsla = rgb(0xf8fafc).into();
|
||||||
|
assert_eq!(color.to_hex_string(), "#F8FAFC");
|
||||||
|
|
||||||
|
let color: Hsla = rgb(0xfef2f2).into();
|
||||||
|
assert_eq!(color.to_hex_string(), "#FEF2F2");
|
||||||
|
|
||||||
|
let color: Hsla = rgba(0x0413fcaa).into();
|
||||||
|
assert_eq!(color.to_hex_string(), "#0413FCAA");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_hex_string() {
|
||||||
|
let color: Hsla = Hsla::parse_hex_string("#F8FAFC").unwrap();
|
||||||
|
assert_eq!(color, rgb(0xf8fafc).into());
|
||||||
|
|
||||||
|
let color: Hsla = Hsla::parse_hex_string("#FEF2F2").unwrap();
|
||||||
|
assert_eq!(color, rgb(0xfef2f2).into());
|
||||||
|
|
||||||
|
let color: Hsla = Hsla::parse_hex_string("#0413FCAA").unwrap();
|
||||||
|
assert_eq!(color, rgba(0x0413fcaa).into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -662,6 +662,7 @@ where
|
||||||
deferred(
|
deferred(
|
||||||
anchored().snap_to_window().child(
|
anchored().snap_to_window().child(
|
||||||
div()
|
div()
|
||||||
|
.occlude()
|
||||||
.map(|this| match self.menu_width {
|
.map(|this| match self.menu_width {
|
||||||
Length::Auto => this.w(bounds.size.width),
|
Length::Auto => this.w(bounds.size.width),
|
||||||
Length::Definite(w) => this.w(w),
|
Length::Definite(w) => this.w(w),
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
theme::{ActiveTheme, Colorize},
|
theme::{ActiveTheme, Colorize},
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, rems, Axis, Div, Element, EntityId, Fill, FocusHandle, Pixels, Styled, WindowContext,
|
div, px, Axis, Div, Element, EntityId, Fill, FocusHandle, Pixels, Styled, WindowContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Returns a `Div` as horizontal flex layout.
|
/// Returns a `Div` as horizontal flex layout.
|
||||||
|
|
@ -238,15 +238,17 @@ pub trait StyleSized<T: Styled> {
|
||||||
fn list_size(self, size: Size) -> Self;
|
fn list_size(self, size: Size) -> Self;
|
||||||
fn list_px(self, size: Size) -> Self;
|
fn list_px(self, size: Size) -> Self;
|
||||||
fn list_py(self, size: Size) -> Self;
|
fn list_py(self, size: Size) -> Self;
|
||||||
|
/// Apply size with the given `Size`.
|
||||||
|
fn size_with(self, size: Size) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Styled> StyleSized<T> for T {
|
impl<T: Styled> StyleSized<T> for T {
|
||||||
fn input_text_size(self, size: Size) -> Self {
|
fn input_text_size(self, size: Size) -> Self {
|
||||||
match size {
|
match size {
|
||||||
Size::XSmall => self.text_size(rems(0.75)),
|
Size::XSmall => self.text_xs(),
|
||||||
Size::Small => self.text_size(rems(0.8)),
|
Size::Small => self.text_sm(),
|
||||||
Size::Medium => self.text_size(rems(0.875)),
|
Size::Medium => self.text_base(),
|
||||||
Size::Large => self.text_size(rems(1.)),
|
Size::Large => self.text_lg(),
|
||||||
Size::Size(size) => self.text_size(size),
|
Size::Size(size) => self.text_size(size),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -315,6 +317,16 @@ impl<T: Styled> StyleSized<T> for T {
|
||||||
_ => self.py_1(),
|
_ => self.py_1(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn size_with(self, size: Size) -> Self {
|
||||||
|
match size {
|
||||||
|
Size::Large => self.size_11(),
|
||||||
|
Size::Medium => self.size_8(),
|
||||||
|
Size::Small => self.size_5(),
|
||||||
|
Size::XSmall => self.size_4(),
|
||||||
|
Size::Size(size) => self.size(size),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait AxisExt {
|
pub trait AxisExt {
|
||||||
|
|
|
||||||
|
|
@ -76,9 +76,9 @@ impl Colorize for Hsla {
|
||||||
/// Returns a new color with the given opacity.
|
/// Returns a new color with the given opacity.
|
||||||
///
|
///
|
||||||
/// The opacity is a value between 0.0 and 1.0, where 0.0 is fully transparent and 1.0 is fully opaque.
|
/// The opacity is a value between 0.0 and 1.0, where 0.0 is fully transparent and 1.0 is fully opaque.
|
||||||
fn opacity(&self, opacity: f32) -> Hsla {
|
fn opacity(&self, factor: f32) -> Hsla {
|
||||||
Hsla {
|
Hsla {
|
||||||
a: self.a * opacity,
|
a: self.a * factor.clamp(0.0, 1.0),
|
||||||
..*self
|
..*self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,14 +111,16 @@ impl Colorize for Hsla {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lighten(&self, amount: f32) -> Hsla {
|
/// Return a new color with the lightness increased by the given factor.
|
||||||
let l = (self.l * (1.0 + amount)).min(1.0);
|
fn lighten(&self, factor: f32) -> Hsla {
|
||||||
|
let l = (self.l * 1.0 - factor.clamp(0.0, 1.0)).min(1.0);
|
||||||
|
|
||||||
Hsla { l, ..*self }
|
Hsla { l, ..*self }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn darken(&self, amount: f32) -> Hsla {
|
/// Return a new color with the darkness increased by the given factor.
|
||||||
let l = (self.l * (1.0 - amount)).max(0.0);
|
fn darken(&self, factor: f32) -> Hsla {
|
||||||
|
let l = (self.l * 1.0 - factor.clamp(0.0, 1.0)).max(0.0);
|
||||||
|
|
||||||
Hsla { l, ..*self }
|
Hsla { l, ..*self }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue