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 ui::{
|
||||
button::Button,
|
||||
color_picker::{ColorPicker, ColorPickerEvent},
|
||||
dock::{DockArea, StackPanel, TabPanel},
|
||||
drawer::Drawer,
|
||||
h_flex,
|
||||
modal::Modal,
|
||||
popup_menu::PopupMenuExt,
|
||||
theme::{ActiveTheme, Theme},
|
||||
theme::{ActiveTheme, Colorize as _, Theme},
|
||||
ContextModal, IconName, Root, Sizable,
|
||||
};
|
||||
|
||||
|
|
@ -38,8 +39,9 @@ pub fn init(_app_state: Arc<AppState>, cx: &mut AppContext) {
|
|||
}
|
||||
|
||||
pub struct StoryWorkspace {
|
||||
locale_selector: View<LocaleSelector>,
|
||||
dock_area: View<DockArea>,
|
||||
locale_selector: View<LocaleSelector>,
|
||||
theme_color_picker: View<ColorPicker>,
|
||||
}
|
||||
|
||||
impl StoryWorkspace {
|
||||
|
|
@ -228,9 +230,34 @@ impl StoryWorkspace {
|
|||
|
||||
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 {
|
||||
dock_area,
|
||||
locale_selector,
|
||||
theme_color_picker,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -326,7 +353,7 @@ impl Render for StoryWorkspace {
|
|||
.justify_end()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.child(self.locale_selector.clone())
|
||||
.child(self.theme_color_picker.clone())
|
||||
.child(
|
||||
Button::new("theme-mode", cx)
|
||||
.map(|this| {
|
||||
|
|
@ -347,6 +374,7 @@ impl Render for StoryWorkspace {
|
|||
Theme::change(mode, cx);
|
||||
}),
|
||||
)
|
||||
.child(self.locale_selector.clone())
|
||||
.child(
|
||||
Button::new("github", cx)
|
||||
.icon(IconName::GitHub)
|
||||
|
|
|
|||
|
|
@ -7,11 +7,9 @@ use gpui::{
|
|||
use ui::{
|
||||
button::Button,
|
||||
checkbox::Checkbox,
|
||||
color_picker::{ColorPicker, ColorPickerEvent},
|
||||
h_flex,
|
||||
input::{InputEvent, OtpInput, TextInput},
|
||||
prelude::FluentBuilder as _,
|
||||
theme::{Colorize, Theme},
|
||||
v_flex, FocusableCycle, IconName, Sizable,
|
||||
};
|
||||
|
||||
|
|
@ -44,7 +42,6 @@ pub struct InputStory {
|
|||
otp_input_small: View<OtpInput>,
|
||||
otp_input_large: View<OtpInput>,
|
||||
opt_input_sized: View<OtpInput>,
|
||||
color_picker: View<ColorPicker>,
|
||||
}
|
||||
|
||||
impl InputStory {
|
||||
|
|
@ -106,23 +103,6 @@ impl InputStory {
|
|||
})
|
||||
.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 {
|
||||
input1,
|
||||
input2,
|
||||
|
|
@ -167,7 +147,6 @@ impl InputStory {
|
|||
.default_value("654321")
|
||||
.with_size(px(55.))
|
||||
}),
|
||||
color_picker,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -218,7 +197,6 @@ impl FocusableCycle for InputStory {
|
|||
self.suffix_input1.focus_handle(cx),
|
||||
self.large_input.focus_handle(cx),
|
||||
self.small_input.focus_handle(cx),
|
||||
self.color_picker.focus_handle(cx),
|
||||
self.otp_input.focus_handle(cx),
|
||||
]
|
||||
.to_vec()
|
||||
|
|
@ -266,7 +244,6 @@ impl Render for InputStory {
|
|||
.child(self.small_input.clone()),
|
||||
),
|
||||
)
|
||||
.child(section("Color Picker", cx).child(self.color_picker.clone()))
|
||||
.child(
|
||||
section(
|
||||
h_flex()
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
use gpui::{
|
||||
anchored, deferred, div, prelude::FluentBuilder as _, px, AppContext, ElementId, EventEmitter,
|
||||
FocusHandle, FocusableView, Hsla, InteractiveElement as _, IntoElement, KeyBinding, Length,
|
||||
MouseButton, ParentElement, Render, SharedString, StatefulInteractiveElement as _, Styled,
|
||||
ViewContext,
|
||||
anchored, canvas, deferred, div, prelude::FluentBuilder as _, px, relative, AnchorCorner,
|
||||
AppContext, Bounds, ElementId, EventEmitter, FocusHandle, FocusableView, Hsla,
|
||||
InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
|
||||
Render, SharedString, StatefulInteractiveElement as _, Styled, View, ViewContext,
|
||||
VisualContext,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
colors::DEFAULT_COLOR,
|
||||
divider::Divider,
|
||||
h_flex,
|
||||
input::ClearButton,
|
||||
input::{InputEvent, TextInput},
|
||||
popover::Escape,
|
||||
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) {
|
||||
let context = Some("ColorPicker");
|
||||
cx.bind_keys([KeyBinding::new("escape", Escape, context)])
|
||||
cx.bind_keys([KeyBinding::new("escape", Escape, Some(KEY_CONTEXT))])
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -26,6 +28,7 @@ pub enum ColorPickerEvent {
|
|||
}
|
||||
|
||||
fn color_palettes() -> Vec<Vec<Hsla>> {
|
||||
use crate::colors::DEFAULT_COLOR;
|
||||
use itertools::Itertools as _;
|
||||
|
||||
macro_rules! c {
|
||||
|
|
@ -55,22 +58,47 @@ fn color_palettes() -> Vec<Vec<Hsla>> {
|
|||
pub struct ColorPicker {
|
||||
id: ElementId,
|
||||
focus_handle: FocusHandle,
|
||||
featured_colors: Vec<Hsla>,
|
||||
value: Option<Hsla>,
|
||||
cleanable: bool,
|
||||
open: bool,
|
||||
size: Size,
|
||||
width: Length,
|
||||
featured_colors: Vec<Hsla>,
|
||||
hovered_color: Option<Hsla>,
|
||||
label: Option<SharedString>,
|
||||
size: Size,
|
||||
anchor: AnchorCorner,
|
||||
color_input: View<TextInput>,
|
||||
|
||||
open: bool,
|
||||
bounds: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
impl ColorPicker {
|
||||
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 {
|
||||
id: id.into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
featured_colors: vec![
|
||||
crate::black(),
|
||||
crate::gray_600(),
|
||||
crate::gray_400(),
|
||||
crate::white(),
|
||||
crate::red_600(),
|
||||
crate::orange_600(),
|
||||
|
|
@ -81,43 +109,55 @@ impl ColorPicker {
|
|||
crate::purple_600(),
|
||||
],
|
||||
value: None,
|
||||
cleanable: false,
|
||||
open: false,
|
||||
size: Size::default(),
|
||||
width: Length::Auto,
|
||||
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.
|
||||
pub fn cleanable(mut self) -> Self {
|
||||
self.cleanable = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Set the featured colors to be displayed in the color picker.
|
||||
///
|
||||
/// This is used to display a set of colors that the user can quickly select from,
|
||||
/// for example provided user's last used colors.
|
||||
pub fn featured_colors(mut self, colors: Vec<Hsla>) -> Self {
|
||||
self.featured_colors = colors;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn value(mut self, value: Hsla) -> Self {
|
||||
self.value = Some(value);
|
||||
/// Set current color 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
|
||||
}
|
||||
|
||||
fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
|
||||
self.open = false;
|
||||
cx.notify();
|
||||
/// Set the label to be displayed above the color picker.
|
||||
///
|
||||
/// 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>) {
|
||||
self.update_value(None, cx)
|
||||
/// Set the anchor corner of the color picker.
|
||||
///
|
||||
/// 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>) {
|
||||
|
|
@ -125,9 +165,19 @@ impl ColorPicker {
|
|||
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;
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -145,18 +195,22 @@ impl ColorPicker {
|
|||
.h_5()
|
||||
.w_5()
|
||||
.bg(color)
|
||||
.rounded_sm()
|
||||
.border_1()
|
||||
.border_color(color.darken(0.1))
|
||||
.when(clickable, |this| {
|
||||
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| {
|
||||
view.hovered_color = Some(color);
|
||||
cx.notify();
|
||||
}))
|
||||
.on_click(cx.listener(move |view, _, cx| {
|
||||
view.update_value(Some(color), cx);
|
||||
view.update_value(Some(color), true, cx);
|
||||
view.open = false;
|
||||
cx.notify();
|
||||
}))
|
||||
|
|
@ -165,7 +219,7 @@ impl ColorPicker {
|
|||
|
||||
fn render_colors(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.gap_3()
|
||||
.child(
|
||||
h_flex().gap_1().children(
|
||||
self.featured_colors
|
||||
|
|
@ -173,6 +227,7 @@ impl ColorPicker {
|
|||
.map(|color| self.render_item(*color, true, cx)),
|
||||
),
|
||||
)
|
||||
.child(Divider::horizontal())
|
||||
.child(
|
||||
v_flex()
|
||||
.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(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.bg(hovered_color)
|
||||
.border_1()
|
||||
.border_color(hovered_color.darken(0.2))
|
||||
.size_5()
|
||||
.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 FocusableView for ColorPicker {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ColorPicker {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let is_focused = self.focus_handle.is_focused(cx);
|
||||
let show_clean = self.cleanable && self.value.is_some();
|
||||
|
||||
let display_title = if let Some(value) = self.value {
|
||||
format!("{}", value.to_hex_string())
|
||||
let display_title: SharedString = if let Some(value) = self.value {
|
||||
value.to_hex_string()
|
||||
} 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()
|
||||
.id(self.id.clone())
|
||||
.key_context("ColorPicker")
|
||||
.key_context(KEY_CONTEXT)
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::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)
|
||||
.on_action(cx.listener(Self::on_escape))
|
||||
.child(
|
||||
div()
|
||||
h_flex()
|
||||
.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()
|
||||
.overflow_hidden()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.input_text_size(self.size)
|
||||
.when(is_focused, |this| this.outline(cx))
|
||||
.input_size(self.size)
|
||||
.when(!self.open, |this| {
|
||||
this.on_click(cx.listener(Self::toggle_picker))
|
||||
})
|
||||
.line_height(relative(1.))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_1()
|
||||
.child(self.render_item(value, false, cx))
|
||||
.child(div().flex_1().overflow_hidden().child(display_title))
|
||||
.when(show_clean, |this| {
|
||||
this.child(ClearButton::new(cx).on_click(cx.listener(Self::clean)))
|
||||
div()
|
||||
.id("color-picker-square")
|
||||
.bg(cx.theme().background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().input)
|
||||
.rounded(px(cx.theme().radius))
|
||||
.bg(cx.theme().background)
|
||||
.shadow_sm()
|
||||
.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| {
|
||||
this.child(
|
||||
Icon::new(IconName::Palette)
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
}),
|
||||
.tooltip(move |cx| Tooltip::new(display_title.clone(), cx)),
|
||||
)
|
||||
.when_some(self.label.clone(), |this, label| this.child(label))
|
||||
.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| {
|
||||
this.child(
|
||||
deferred(
|
||||
anchored().snap_to_window().child(
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.occlude()
|
||||
.absolute()
|
||||
.mt_1p5()
|
||||
.w_72()
|
||||
.overflow_hidden()
|
||||
.rounded_lg()
|
||||
.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.escape(&Escape, cx)),
|
||||
)
|
||||
.child(self.render_colors(cx)),
|
||||
),
|
||||
anchored()
|
||||
.anchor(self.anchor)
|
||||
.snap_to_window()
|
||||
.position(self.resolved_corner(self.bounds))
|
||||
.child(
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.occlude()
|
||||
.map(|this| match self.anchor {
|
||||
AnchorCorner::TopLeft | AnchorCorner::TopRight => {
|
||||
this.mt_1p5()
|
||||
}
|
||||
AnchorCorner::BottomLeft | AnchorCorner::BottomRight => {
|
||||
this.mb_1p5()
|
||||
}
|
||||
})
|
||||
.w_72()
|
||||
.overflow_hidden()
|
||||
.rounded_lg()
|
||||
.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 anyhow::Result;
|
||||
|
||||
pub trait ColorExt {
|
||||
pub(crate) trait ColorExt {
|
||||
fn to_hex_string(&self) -> String;
|
||||
fn parse_hex_string(hex: &str) -> Result<Hsla>;
|
||||
}
|
||||
|
||||
impl ColorExt for Hsla {
|
||||
|
|
@ -17,20 +18,41 @@ impl ColorExt for Hsla {
|
|||
if rgb.a < 1. {
|
||||
return format!(
|
||||
"#{:02X}{:02X}{:02X}{:02X}",
|
||||
u32::from((rgb.r * 255.) as u32),
|
||||
u32::from((rgb.g * 255.) as u32),
|
||||
u32::from((rgb.b * 255.) as u32),
|
||||
u32::from((self.a * 255.) as u32)
|
||||
((rgb.r * 255.) as u32),
|
||||
((rgb.g * 255.) as u32),
|
||||
((rgb.b * 255.) as u32),
|
||||
((self.a * 255.) as u32)
|
||||
);
|
||||
}
|
||||
|
||||
format!(
|
||||
"#{:02X}{:02X}{:02X}",
|
||||
u32::from((rgb.r * 255.) as u32),
|
||||
u32::from((rgb.g * 255.) as u32),
|
||||
u32::from((rgb.b * 255.) as u32)
|
||||
((rgb.r * 255.) as u32),
|
||||
((rgb.g * 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> =
|
||||
|
|
@ -211,6 +233,8 @@ color_methods!(rose);
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{rgb, rgba};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
|
@ -229,4 +253,28 @@ mod tests {
|
|||
assert_eq!(blue_400(), hsl(213.1, 93.9, 67.8));
|
||||
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(
|
||||
anchored().snap_to_window().child(
|
||||
div()
|
||||
.occlude()
|
||||
.map(|this| match self.menu_width {
|
||||
Length::Auto => this.w(bounds.size.width),
|
||||
Length::Definite(w) => this.w(w),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crate::{
|
|||
theme::{ActiveTheme, Colorize},
|
||||
};
|
||||
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.
|
||||
|
|
@ -238,15 +238,17 @@ pub trait StyleSized<T: Styled> {
|
|||
fn list_size(self, size: Size) -> Self;
|
||||
fn list_px(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 {
|
||||
fn input_text_size(self, size: Size) -> Self {
|
||||
match size {
|
||||
Size::XSmall => self.text_size(rems(0.75)),
|
||||
Size::Small => self.text_size(rems(0.8)),
|
||||
Size::Medium => self.text_size(rems(0.875)),
|
||||
Size::Large => self.text_size(rems(1.)),
|
||||
Size::XSmall => self.text_xs(),
|
||||
Size::Small => self.text_sm(),
|
||||
Size::Medium => self.text_base(),
|
||||
Size::Large => self.text_lg(),
|
||||
Size::Size(size) => self.text_size(size),
|
||||
}
|
||||
}
|
||||
|
|
@ -315,6 +317,16 @@ impl<T: Styled> StyleSized<T> for T {
|
|||
_ => 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 {
|
||||
|
|
|
|||
|
|
@ -76,9 +76,9 @@ impl Colorize for Hsla {
|
|||
/// 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.
|
||||
fn opacity(&self, opacity: f32) -> Hsla {
|
||||
fn opacity(&self, factor: f32) -> Hsla {
|
||||
Hsla {
|
||||
a: self.a * opacity,
|
||||
a: self.a * factor.clamp(0.0, 1.0),
|
||||
..*self
|
||||
}
|
||||
}
|
||||
|
|
@ -111,14 +111,16 @@ impl Colorize for Hsla {
|
|||
}
|
||||
}
|
||||
|
||||
fn lighten(&self, amount: f32) -> Hsla {
|
||||
let l = (self.l * (1.0 + amount)).min(1.0);
|
||||
/// Return a new color with the lightness increased by the given factor.
|
||||
fn lighten(&self, factor: f32) -> Hsla {
|
||||
let l = (self.l * 1.0 - factor.clamp(0.0, 1.0)).min(1.0);
|
||||
|
||||
Hsla { l, ..*self }
|
||||
}
|
||||
|
||||
fn darken(&self, amount: f32) -> Hsla {
|
||||
let l = (self.l * (1.0 - amount)).max(0.0);
|
||||
/// Return a new color with the darkness increased by the given factor.
|
||||
fn darken(&self, factor: f32) -> Hsla {
|
||||
let l = (self.l * 1.0 - factor.clamp(0.0, 1.0)).max(0.0);
|
||||
|
||||
Hsla { l, ..*self }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue