wip: refactor
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -5621,7 +5621,6 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"paste",
|
"paste",
|
||||||
"regex",
|
"regex",
|
||||||
"rust-embed",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
|||||||
@@ -60,44 +60,30 @@ impl Inbox {
|
|||||||
.justify_between()
|
.justify_between()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.rounded_md()
|
.rounded_md()
|
||||||
.hover(|this| {
|
.hover(|this| this.bg(cx.theme().list_hover))
|
||||||
this.bg(cx.theme().sidebar_accent)
|
.child(div().font_medium().map(|this| {
|
||||||
.text_color(cx.theme().sidebar_accent_foreground)
|
if room.is_group {
|
||||||
})
|
this.flex()
|
||||||
.child(
|
.items_center()
|
||||||
div()
|
.gap_2()
|
||||||
.font_medium()
|
.child(img("brand/avatar.png").size_6().rounded_full())
|
||||||
.text_color(cx.theme().sidebar_accent_foreground)
|
.child(room.name())
|
||||||
.map(|this| {
|
} else {
|
||||||
if room.is_group {
|
this.when_some(room.members.first(), |this, sender| {
|
||||||
this.flex()
|
this.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
img("brand/avatar.png").size_6().rounded_full(),
|
img(sender.avatar())
|
||||||
)
|
.size_6()
|
||||||
.child(room.name())
|
.rounded_full()
|
||||||
} else {
|
.flex_shrink_0(),
|
||||||
this.when_some(room.members.first(), |this, sender| {
|
)
|
||||||
this.flex()
|
.child(sender.name())
|
||||||
.items_center()
|
})
|
||||||
.gap_2()
|
}
|
||||||
.child(
|
}))
|
||||||
img(sender.avatar())
|
.child(div().child(ago))
|
||||||
.size_6()
|
|
||||||
.rounded_full()
|
|
||||||
.flex_shrink_0(),
|
|
||||||
)
|
|
||||||
.child(sender.name())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.child(ago)
|
|
||||||
.text_color(cx.theme().sidebar_accent_foreground.opacity(0.7)),
|
|
||||||
)
|
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
this.action(id, cx);
|
this.action(id, cx);
|
||||||
}))
|
}))
|
||||||
@@ -143,8 +129,7 @@ impl Render for Inbox {
|
|||||||
.rounded_md()
|
.rounded_md()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.text_color(cx.theme().sidebar_foreground.opacity(0.7))
|
.hover(|this| this.bg(cx.theme().list_hover))
|
||||||
.hover(|this| this.bg(cx.theme().sidebar_accent.opacity(0.7)))
|
|
||||||
.on_click(cx.listener(move |view, _event, cx| {
|
.on_click(cx.listener(move |view, _event, cx| {
|
||||||
view.is_collapsed = !view.is_collapsed;
|
view.is_collapsed = !view.is_collapsed;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
|
||||||
rust-embed.workspace = true
|
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ impl BadgeVariant {
|
|||||||
Self::Primary => cx.theme().primary,
|
Self::Primary => cx.theme().primary,
|
||||||
Self::Secondary => cx.theme().secondary,
|
Self::Secondary => cx.theme().secondary,
|
||||||
Self::Outline => gpui::transparent_black(),
|
Self::Outline => gpui::transparent_black(),
|
||||||
Self::Destructive => cx.theme().destructive,
|
Self::Destructive => cx.theme().danger,
|
||||||
Self::Custom { color, .. } => *color,
|
Self::Custom { color, .. } => *color,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ impl BadgeVariant {
|
|||||||
Self::Primary => cx.theme().primary,
|
Self::Primary => cx.theme().primary,
|
||||||
Self::Secondary => cx.theme().secondary,
|
Self::Secondary => cx.theme().secondary,
|
||||||
Self::Outline => cx.theme().border,
|
Self::Outline => cx.theme().border,
|
||||||
Self::Destructive => cx.theme().destructive,
|
Self::Destructive => cx.theme().danger,
|
||||||
Self::Custom { border, .. } => *border,
|
Self::Custom { border, .. } => *border,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ impl BadgeVariant {
|
|||||||
Self::Primary => cx.theme().primary_foreground,
|
Self::Primary => cx.theme().primary_foreground,
|
||||||
Self::Secondary => cx.theme().secondary_foreground,
|
Self::Secondary => cx.theme().secondary_foreground,
|
||||||
Self::Outline => cx.theme().foreground,
|
Self::Outline => cx.theme().foreground,
|
||||||
Self::Destructive => cx.theme().destructive_foreground,
|
Self::Destructive => cx.theme().danger_foreground,
|
||||||
Self::Custom { foreground, .. } => *foreground,
|
Self::Custom { foreground, .. } => *foreground,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::{
|
|
||||||
div, prelude::FluentBuilder as _, ClickEvent, ElementId, InteractiveElement as _, IntoElement,
|
|
||||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, WindowContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{h_flex, theme::ActiveTheme, Icon, IconName};
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct Breadcrumb {
|
|
||||||
items: Vec<BreadcrumbItem>,
|
|
||||||
}
|
|
||||||
|
|
||||||
type OnClick = Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>;
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct BreadcrumbItem {
|
|
||||||
id: ElementId,
|
|
||||||
text: SharedString,
|
|
||||||
on_click: OnClick,
|
|
||||||
disabled: bool,
|
|
||||||
is_last: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BreadcrumbItem {
|
|
||||||
pub fn new(id: impl Into<ElementId>, text: impl Into<SharedString>) -> Self {
|
|
||||||
Self {
|
|
||||||
id: id.into(),
|
|
||||||
text: text.into(),
|
|
||||||
on_click: None,
|
|
||||||
disabled: false,
|
|
||||||
is_last: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
|
||||||
self.disabled = disabled;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_click(
|
|
||||||
mut self,
|
|
||||||
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.on_click = Some(Rc::new(on_click));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For internal use only.
|
|
||||||
#[allow(clippy::wrong_self_convention)]
|
|
||||||
fn is_last(mut self, is_last: bool) -> Self {
|
|
||||||
self.is_last = is_last;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for BreadcrumbItem {
|
|
||||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
|
||||||
div()
|
|
||||||
.id(self.id)
|
|
||||||
.child(self.text)
|
|
||||||
.text_color(cx.theme().muted_foreground)
|
|
||||||
.when(self.is_last, |this| this.text_color(cx.theme().foreground))
|
|
||||||
.when(self.disabled, |this| {
|
|
||||||
this.text_color(cx.theme().muted_foreground)
|
|
||||||
})
|
|
||||||
.when(!self.disabled, |this| {
|
|
||||||
this.when_some(self.on_click, |this, on_click| {
|
|
||||||
this.cursor_pointer().on_click(move |event, cx| {
|
|
||||||
on_click(event, cx);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Breadcrumb {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self { items: Vec::new() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add an item to the breadcrumb.
|
|
||||||
pub fn item(mut self, item: BreadcrumbItem) -> Self {
|
|
||||||
self.items.push(item);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Breadcrumb {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
struct BreadcrumbSeparator;
|
|
||||||
impl RenderOnce for BreadcrumbSeparator {
|
|
||||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
|
||||||
Icon::new(IconName::ChevronRight)
|
|
||||||
.text_color(cx.theme().muted_foreground)
|
|
||||||
.size_3p5()
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for Breadcrumb {
|
|
||||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
|
||||||
let items_count = self.items.len();
|
|
||||||
|
|
||||||
let mut children = vec![];
|
|
||||||
for (ix, item) in self.items.into_iter().enumerate() {
|
|
||||||
let is_last = ix == items_count - 1;
|
|
||||||
|
|
||||||
children.push(item.is_last(is_last).into_any_element());
|
|
||||||
if !is_last {
|
|
||||||
children.push(BreadcrumbSeparator.into_any_element());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().muted_foreground)
|
|
||||||
.children(children)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
use gpui::{
|
|
||||||
div, prelude::FluentBuilder as _, px, relative, AnyElement, ClickEvent, Corners, Div, Edges,
|
|
||||||
ElementId, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels,
|
|
||||||
RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, WindowContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
indicator::Indicator,
|
indicator::Indicator,
|
||||||
theme::{ActiveTheme, Colorize as _},
|
theme::{ActiveTheme, Colorize as _},
|
||||||
tooltip::Tooltip,
|
tooltip::Tooltip,
|
||||||
Disableable, Icon, Selectable, Sizable, Size, StyledExt,
|
Disableable, Icon, Selectable, Sizable, Size, StyledExt,
|
||||||
};
|
};
|
||||||
|
use gpui::{
|
||||||
|
div, prelude::FluentBuilder as _, px, relative, AnyElement, ClickEvent, Corners, Div, Edges,
|
||||||
|
ElementId, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels,
|
||||||
|
RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, WindowContext,
|
||||||
|
};
|
||||||
|
|
||||||
pub enum ButtonRounded {
|
pub enum ButtonRounded {
|
||||||
None,
|
None,
|
||||||
@@ -418,7 +417,7 @@ impl RenderOnce for Button {
|
|||||||
let hover_style = style.hovered(cx);
|
let hover_style = style.hovered(cx);
|
||||||
this.bg(hover_style.bg)
|
this.bg(hover_style.bg)
|
||||||
.border_color(hover_style.border)
|
.border_color(hover_style.border)
|
||||||
.text_color(crate::red_400())
|
.text_color(cx.theme().danger)
|
||||||
})
|
})
|
||||||
.active(|this| {
|
.active(|this| {
|
||||||
let active_style = style.active(cx);
|
let active_style = style.active(cx);
|
||||||
@@ -505,7 +504,7 @@ impl ButtonVariant {
|
|||||||
match self {
|
match self {
|
||||||
ButtonVariant::Primary => cx.theme().primary,
|
ButtonVariant::Primary => cx.theme().primary,
|
||||||
ButtonVariant::Secondary => cx.theme().secondary,
|
ButtonVariant::Secondary => cx.theme().secondary,
|
||||||
ButtonVariant::Danger => cx.theme().destructive,
|
ButtonVariant::Danger => cx.theme().danger,
|
||||||
ButtonVariant::Outline
|
ButtonVariant::Outline
|
||||||
| ButtonVariant::Ghost
|
| ButtonVariant::Ghost
|
||||||
| ButtonVariant::Link
|
| ButtonVariant::Link
|
||||||
@@ -520,7 +519,7 @@ impl ButtonVariant {
|
|||||||
ButtonVariant::Secondary | ButtonVariant::Outline | ButtonVariant::Ghost => {
|
ButtonVariant::Secondary | ButtonVariant::Outline | ButtonVariant::Ghost => {
|
||||||
cx.theme().secondary_foreground
|
cx.theme().secondary_foreground
|
||||||
}
|
}
|
||||||
ButtonVariant::Danger => cx.theme().destructive_foreground,
|
ButtonVariant::Danger => cx.theme().danger_foreground,
|
||||||
ButtonVariant::Link => cx.theme().link,
|
ButtonVariant::Link => cx.theme().link,
|
||||||
ButtonVariant::Text => cx.theme().foreground,
|
ButtonVariant::Text => cx.theme().foreground,
|
||||||
ButtonVariant::Custom(colors) => colors.foreground,
|
ButtonVariant::Custom(colors) => colors.foreground,
|
||||||
@@ -531,7 +530,7 @@ impl ButtonVariant {
|
|||||||
match self {
|
match self {
|
||||||
ButtonVariant::Primary => cx.theme().primary,
|
ButtonVariant::Primary => cx.theme().primary,
|
||||||
ButtonVariant::Secondary => cx.theme().border,
|
ButtonVariant::Secondary => cx.theme().border,
|
||||||
ButtonVariant::Danger => cx.theme().destructive,
|
ButtonVariant::Danger => cx.theme().danger,
|
||||||
ButtonVariant::Outline => cx.theme().border,
|
ButtonVariant::Outline => cx.theme().border,
|
||||||
ButtonVariant::Ghost | ButtonVariant::Link | ButtonVariant::Text => {
|
ButtonVariant::Ghost | ButtonVariant::Link | ButtonVariant::Text => {
|
||||||
cx.theme().transparent
|
cx.theme().transparent
|
||||||
@@ -572,7 +571,7 @@ impl ButtonVariant {
|
|||||||
let bg = match self {
|
let bg = match self {
|
||||||
ButtonVariant::Primary => cx.theme().primary_hover,
|
ButtonVariant::Primary => cx.theme().primary_hover,
|
||||||
ButtonVariant::Secondary | ButtonVariant::Outline => cx.theme().secondary_hover,
|
ButtonVariant::Secondary | ButtonVariant::Outline => cx.theme().secondary_hover,
|
||||||
ButtonVariant::Danger => cx.theme().destructive_hover,
|
ButtonVariant::Danger => cx.theme().danger_hover,
|
||||||
ButtonVariant::Ghost => {
|
ButtonVariant::Ghost => {
|
||||||
if cx.theme().mode.is_dark() {
|
if cx.theme().mode.is_dark() {
|
||||||
cx.theme().secondary.lighten(0.1).opacity(0.8)
|
cx.theme().secondary.lighten(0.1).opacity(0.8)
|
||||||
@@ -612,7 +611,7 @@ impl ButtonVariant {
|
|||||||
cx.theme().secondary.darken(0.2).opacity(0.8)
|
cx.theme().secondary.darken(0.2).opacity(0.8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ButtonVariant::Danger => cx.theme().destructive_active,
|
ButtonVariant::Danger => cx.theme().danger_active,
|
||||||
ButtonVariant::Link => cx.theme().transparent,
|
ButtonVariant::Link => cx.theme().transparent,
|
||||||
ButtonVariant::Text => cx.theme().transparent,
|
ButtonVariant::Text => cx.theme().transparent,
|
||||||
ButtonVariant::Custom(colors) => colors.active,
|
ButtonVariant::Custom(colors) => colors.active,
|
||||||
@@ -646,7 +645,7 @@ impl ButtonVariant {
|
|||||||
cx.theme().secondary.darken(0.2).opacity(0.8)
|
cx.theme().secondary.darken(0.2).opacity(0.8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ButtonVariant::Danger => cx.theme().destructive_active,
|
ButtonVariant::Danger => cx.theme().danger_active,
|
||||||
ButtonVariant::Link => cx.theme().transparent,
|
ButtonVariant::Link => cx.theme().transparent,
|
||||||
ButtonVariant::Text => cx.theme().transparent,
|
ButtonVariant::Text => cx.theme().transparent,
|
||||||
ButtonVariant::Custom(colors) => colors.active,
|
ButtonVariant::Custom(colors) => colors.active,
|
||||||
@@ -676,7 +675,7 @@ impl ButtonVariant {
|
|||||||
| ButtonVariant::Outline
|
| ButtonVariant::Outline
|
||||||
| ButtonVariant::Text => cx.theme().transparent,
|
| ButtonVariant::Text => cx.theme().transparent,
|
||||||
ButtonVariant::Primary => cx.theme().primary.opacity(0.15),
|
ButtonVariant::Primary => cx.theme().primary.opacity(0.15),
|
||||||
ButtonVariant::Danger => cx.theme().destructive.opacity(0.15),
|
ButtonVariant::Danger => cx.theme().danger.opacity(0.15),
|
||||||
ButtonVariant::Secondary => cx.theme().secondary.opacity(1.5),
|
ButtonVariant::Secondary => cx.theme().secondary.opacity(1.5),
|
||||||
ButtonVariant::Custom(style) => style.color.opacity(0.15),
|
ButtonVariant::Custom(style) => style.color.opacity(0.15),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,371 +0,0 @@
|
|||||||
use gpui::{
|
|
||||||
anchored, canvas, deferred, div, prelude::FluentBuilder as _, px, relative, AppContext, Bounds,
|
|
||||||
Corner, ElementId, EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement as _,
|
|
||||||
IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, Render, SharedString,
|
|
||||||
StatefulInteractiveElement as _, Styled, View, ViewContext, VisualContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
divider::Divider,
|
|
||||||
h_flex,
|
|
||||||
input::{InputEvent, TextInput},
|
|
||||||
popover::Escape,
|
|
||||||
theme::{ActiveTheme as _, Colorize},
|
|
||||||
tooltip::Tooltip,
|
|
||||||
v_flex, ColorExt as _, Sizable, Size, StyleSized,
|
|
||||||
};
|
|
||||||
|
|
||||||
const KEY_CONTEXT: &str = "ColorPicker";
|
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
|
||||||
cx.bind_keys([KeyBinding::new("escape", Escape, Some(KEY_CONTEXT))])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum ColorPickerEvent {
|
|
||||||
Change(Option<Hsla>),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn color_palettes() -> Vec<Vec<Hsla>> {
|
|
||||||
use crate::colors::DEFAULT_COLOR;
|
|
||||||
use itertools::Itertools as _;
|
|
||||||
|
|
||||||
macro_rules! c {
|
|
||||||
($color:tt) => {
|
|
||||||
DEFAULT_COLOR
|
|
||||||
.$color
|
|
||||||
.keys()
|
|
||||||
.sorted()
|
|
||||||
.map(|k| DEFAULT_COLOR.$color.get(k).map(|c| c.hsla).unwrap())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
vec![
|
|
||||||
c!(stone),
|
|
||||||
c!(red),
|
|
||||||
c!(orange),
|
|
||||||
c!(yellow),
|
|
||||||
c!(green),
|
|
||||||
c!(cyan),
|
|
||||||
c!(blue),
|
|
||||||
c!(purple),
|
|
||||||
c!(pink),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ColorPicker {
|
|
||||||
id: ElementId,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
value: Option<Hsla>,
|
|
||||||
featured_colors: Vec<Hsla>,
|
|
||||||
hovered_color: Option<Hsla>,
|
|
||||||
label: Option<SharedString>,
|
|
||||||
size: Size,
|
|
||||||
anchor: Corner,
|
|
||||||
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(),
|
|
||||||
crate::yellow_600(),
|
|
||||||
crate::green_600(),
|
|
||||||
crate::blue_600(),
|
|
||||||
crate::indigo_600(),
|
|
||||||
crate::purple_600(),
|
|
||||||
],
|
|
||||||
value: None,
|
|
||||||
hovered_color: None,
|
|
||||||
size: Size::Medium,
|
|
||||||
label: None,
|
|
||||||
anchor: Corner::TopLeft,
|
|
||||||
color_input,
|
|
||||||
open: false,
|
|
||||||
bounds: Bounds::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the anchor corner of the color picker.
|
|
||||||
///
|
|
||||||
/// Default is `Corner::TopLeft`.
|
|
||||||
pub fn anchor(mut self, anchor: Corner) -> Self {
|
|
||||||
self.anchor = anchor;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
|
|
||||||
cx.propagate();
|
|
||||||
|
|
||||||
self.open = false;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toggle_picker(&mut self, _: &gpui::ClickEvent, cx: &mut ViewContext<Self>) {
|
|
||||||
self.open = !self.open;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_value(&mut self, value: Option<Hsla>, emit: bool, cx: &mut ViewContext<Self>) {
|
|
||||||
self.value = 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_item(
|
|
||||||
&self,
|
|
||||||
color: Hsla,
|
|
||||||
clickable: bool,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
div()
|
|
||||||
.id(SharedString::from(format!(
|
|
||||||
"color-{}",
|
|
||||||
color.to_hex_string()
|
|
||||||
)))
|
|
||||||
.h_5()
|
|
||||||
.w_5()
|
|
||||||
.bg(color)
|
|
||||||
.border_1()
|
|
||||||
.border_color(color.darken(0.1))
|
|
||||||
.when(clickable, |this| {
|
|
||||||
this.cursor_pointer()
|
|
||||||
.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), true, cx);
|
|
||||||
view.open = false;
|
|
||||||
cx.notify();
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_colors(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.gap_3()
|
|
||||||
.child(
|
|
||||||
h_flex().gap_1().children(
|
|
||||||
self.featured_colors
|
|
||||||
.iter()
|
|
||||||
.map(|color| self.render_item(*color, true, cx)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(Divider::horizontal())
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1()
|
|
||||||
.children(color_palettes().iter().map(|sub_colors| {
|
|
||||||
h_flex().gap_1().children(
|
|
||||||
sub_colors
|
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.map(|color| self.render_item(*color, true, cx)),
|
|
||||||
)
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.when_some(self.hovered_color, |this, hovered_color| {
|
|
||||||
this.child(Divider::horizontal()).child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.items_center()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.bg(hovered_color)
|
|
||||||
.flex_shrink_0()
|
|
||||||
.border_1()
|
|
||||||
.border_color(hovered_color.darken(0.2))
|
|
||||||
.size_5()
|
|
||||||
.rounded(px(cx.theme().radius)),
|
|
||||||
)
|
|
||||||
.child(self.color_input.clone()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolved_corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
|
|
||||||
bounds.corner(match self.anchor {
|
|
||||||
Corner::TopLeft => Corner::BottomLeft,
|
|
||||||
Corner::TopRight => Corner::BottomRight,
|
|
||||||
Corner::BottomLeft => Corner::TopLeft,
|
|
||||||
Corner::BottomRight => Corner::TopRight,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, _: &AppContext) -> FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ColorPicker {
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
||||||
let display_title: SharedString = if let Some(value) = self.value {
|
|
||||||
value.to_hex_string()
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
}
|
|
||||||
.into();
|
|
||||||
|
|
||||||
let view = cx.view().clone();
|
|
||||||
|
|
||||||
div()
|
|
||||||
.id(self.id.clone())
|
|
||||||
.key_context(KEY_CONTEXT)
|
|
||||||
.track_focus(&self.focus_handle)
|
|
||||||
.on_action(cx.listener(Self::on_escape))
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.id("color-picker-input")
|
|
||||||
.cursor_pointer()
|
|
||||||
.gap_2()
|
|
||||||
.items_center()
|
|
||||||
.input_text_size(self.size)
|
|
||||||
.line_height(relative(1.))
|
|
||||||
.child(
|
|
||||||
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))
|
|
||||||
})
|
|
||||||
.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()
|
|
||||||
.anchor(self.anchor)
|
|
||||||
.snap_to_window_with_margin(px(8.))
|
|
||||||
.position(self.resolved_corner(self.bounds))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.occlude()
|
|
||||||
.map(|this| match self.anchor {
|
|
||||||
Corner::TopLeft | Corner::TopRight => this.mt_1p5(),
|
|
||||||
Corner::BottomLeft | Corner::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(1),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use gpui::Hsla;
|
|
||||||
use serde::{de::Error, Deserialize, Deserializer};
|
|
||||||
|
|
||||||
use crate::theme::hsl;
|
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
pub(crate) trait ColorExt {
|
|
||||||
fn to_hex_string(&self) -> String;
|
|
||||||
fn parse_hex_string(hex: &str) -> Result<Hsla>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ColorExt for Hsla {
|
|
||||||
fn to_hex_string(&self) -> String {
|
|
||||||
let rgb = self.to_rgb();
|
|
||||||
|
|
||||||
if rgb.a < 1. {
|
|
||||||
return format!(
|
|
||||||
"#{:02X}{:02X}{:02X}{:02X}",
|
|
||||||
((rgb.r * 255.) as u32),
|
|
||||||
((rgb.g * 255.) as u32),
|
|
||||||
((rgb.b * 255.) as u32),
|
|
||||||
((self.a * 255.) as u32)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
format!(
|
|
||||||
"#{:02X}{:02X}{:02X}",
|
|
||||||
((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<ShadcnColors> =
|
|
||||||
once_cell::sync::Lazy::new(|| {
|
|
||||||
serde_json::from_str(include_str!("../colors.json")).expect("failed to parse default-json")
|
|
||||||
});
|
|
||||||
|
|
||||||
type ColorScales = HashMap<usize, ShadcnColor>;
|
|
||||||
|
|
||||||
mod color_scales {
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use super::{ColorScales, ShadcnColor};
|
|
||||||
|
|
||||||
use serde::de::{Deserialize, Deserializer};
|
|
||||||
|
|
||||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<ColorScales, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let mut map = HashMap::new();
|
|
||||||
for color in Vec::<ShadcnColor>::deserialize(deserializer)? {
|
|
||||||
map.insert(color.scale, color);
|
|
||||||
}
|
|
||||||
Ok(map)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
|
|
||||||
pub(crate) struct ShadcnColors {
|
|
||||||
pub(crate) black: ShadcnColor,
|
|
||||||
pub(crate) white: ShadcnColor,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) slate: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) gray: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) zinc: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) neutral: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) stone: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) red: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) orange: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) amber: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) yellow: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) lime: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) green: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) emerald: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) teal: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) cyan: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) sky: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) blue: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) indigo: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) violet: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) purple: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) fuchsia: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) pink: ColorScales,
|
|
||||||
#[serde(with = "color_scales")]
|
|
||||||
pub(crate) rose: ColorScales,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize)]
|
|
||||||
pub(crate) struct ShadcnColor {
|
|
||||||
#[serde(default)]
|
|
||||||
pub(crate) scale: usize,
|
|
||||||
#[serde(deserialize_with = "from_hsl_channel", alias = "hslChannel")]
|
|
||||||
pub(crate) hsla: Hsla,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deserialize Hsla from a string in the format "210 40% 98%"
|
|
||||||
fn from_hsl_channel<'de, D>(deserializer: D) -> Result<Hsla, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let s: String = Deserialize::deserialize(deserializer).unwrap();
|
|
||||||
|
|
||||||
let mut parts = s.split_whitespace();
|
|
||||||
if parts.clone().count() != 3 {
|
|
||||||
return Err(D::Error::custom(
|
|
||||||
"expected hslChannel has 3 parts, e.g: '210 40% 98%'",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_number(s: &str) -> f32 {
|
|
||||||
s.trim_end_matches('%')
|
|
||||||
.parse()
|
|
||||||
.expect("failed to parse number")
|
|
||||||
}
|
|
||||||
|
|
||||||
let (h, s, l) = (
|
|
||||||
parse_number(parts.next().unwrap()),
|
|
||||||
parse_number(parts.next().unwrap()),
|
|
||||||
parse_number(parts.next().unwrap()),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(hsl(h, s, l))
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! color_method {
|
|
||||||
($color:tt, $scale:tt) => {
|
|
||||||
paste::paste! {
|
|
||||||
#[inline]
|
|
||||||
#[allow(unused)]
|
|
||||||
pub fn [<$color _ $scale>]() -> Hsla {
|
|
||||||
if let Some(color) = DEFAULT_COLOR.$color.get(&($scale as usize)) {
|
|
||||||
return color.hsla;
|
|
||||||
}
|
|
||||||
|
|
||||||
black()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! color_methods {
|
|
||||||
($color:tt) => {
|
|
||||||
paste::paste! {
|
|
||||||
/// Get color by scale number.
|
|
||||||
///
|
|
||||||
/// The possible scale numbers are:
|
|
||||||
/// 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950
|
|
||||||
///
|
|
||||||
/// If the scale number is not found, it will return black color.
|
|
||||||
#[inline]
|
|
||||||
pub fn [<$color>](scale: usize) -> Hsla {
|
|
||||||
if let Some(color) = DEFAULT_COLOR.$color.get(&scale) {
|
|
||||||
return color.hsla;
|
|
||||||
}
|
|
||||||
|
|
||||||
black()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
color_method!($color, 50);
|
|
||||||
color_method!($color, 100);
|
|
||||||
color_method!($color, 200);
|
|
||||||
color_method!($color, 300);
|
|
||||||
color_method!($color, 400);
|
|
||||||
color_method!($color, 500);
|
|
||||||
color_method!($color, 600);
|
|
||||||
color_method!($color, 700);
|
|
||||||
color_method!($color, 800);
|
|
||||||
color_method!($color, 900);
|
|
||||||
color_method!($color, 950);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn black() -> Hsla {
|
|
||||||
DEFAULT_COLOR.black.hsla
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn white() -> Hsla {
|
|
||||||
DEFAULT_COLOR.white.hsla
|
|
||||||
}
|
|
||||||
|
|
||||||
color_methods!(slate);
|
|
||||||
color_methods!(gray);
|
|
||||||
color_methods!(zinc);
|
|
||||||
color_methods!(neutral);
|
|
||||||
color_methods!(stone);
|
|
||||||
color_methods!(red);
|
|
||||||
color_methods!(orange);
|
|
||||||
color_methods!(amber);
|
|
||||||
color_methods!(yellow);
|
|
||||||
color_methods!(lime);
|
|
||||||
color_methods!(green);
|
|
||||||
color_methods!(emerald);
|
|
||||||
color_methods!(teal);
|
|
||||||
color_methods!(cyan);
|
|
||||||
color_methods!(sky);
|
|
||||||
color_methods!(blue);
|
|
||||||
color_methods!(indigo);
|
|
||||||
color_methods!(violet);
|
|
||||||
color_methods!(purple);
|
|
||||||
color_methods!(fuchsia);
|
|
||||||
color_methods!(pink);
|
|
||||||
color_methods!(rose);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use gpui::{rgb, rgba};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_default_colors() {
|
|
||||||
assert_eq!(white(), hsl(0.0, 0.0, 100.0));
|
|
||||||
assert_eq!(black(), hsl(0.0, 0.0, 0.0));
|
|
||||||
|
|
||||||
assert_eq!(slate_50(), hsl(210.0, 40.0, 98.0));
|
|
||||||
assert_eq!(slate_100(), hsl(210.0, 40.0, 96.1));
|
|
||||||
assert_eq!(slate_900(), hsl(222.2, 47.4, 11.2));
|
|
||||||
|
|
||||||
assert_eq!(red_50(), hsl(0.0, 85.7, 97.3));
|
|
||||||
assert_eq!(yellow_100(), hsl(54.9, 96.7, 88.0));
|
|
||||||
assert_eq!(green_200(), hsl(141.0, 78.9, 85.1));
|
|
||||||
assert_eq!(cyan_300(), hsl(187.0, 92.4, 69.0));
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -856,8 +856,8 @@ impl Render for DockArea {
|
|||||||
.h_full()
|
.h_full()
|
||||||
// Left dock
|
// Left dock
|
||||||
.when_some(self.left_dock.clone(), |this, dock| {
|
.when_some(self.left_dock.clone(), |this, dock| {
|
||||||
this.bg(cx.theme().sidebar)
|
this.bg(cx.theme().muted)
|
||||||
.text_color(cx.theme().sidebar_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.child(div().flex().flex_none().child(dock))
|
.child(div().flex().flex_none().child(dock))
|
||||||
})
|
})
|
||||||
// Center
|
// Center
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::{
|
|||||||
dock::PanelInfo,
|
dock::PanelInfo,
|
||||||
h_flex,
|
h_flex,
|
||||||
popup_menu::{PopupMenu, PopupMenuExt},
|
popup_menu::{PopupMenu, PopupMenuExt},
|
||||||
tab::{Tab, TabBar},
|
tab::{tab_bar::TabBar, Tab},
|
||||||
theme::ActiveTheme,
|
theme::ActiveTheme,
|
||||||
v_flex, AxisExt, IconName, Placement, Selectable, Sizable,
|
v_flex, AxisExt, IconName, Placement, Selectable, Sizable,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use crate::{Icon, IconName, Sizable, Size};
|
use crate::{Icon, IconName, Sizable, Size};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, ease_in_out, percentage, prelude::FluentBuilder as _, Animation, AnimationExt as _, Hsla,
|
div, ease_in_out, percentage, prelude::FluentBuilder as _, Animation, AnimationExt as _, Hsla,
|
||||||
IntoElement, ParentElement, RenderOnce, Styled as _, Transformation, WindowContext,
|
IntoElement, ParentElement, RenderOnce, Styled as _, Transformation, WindowContext,
|
||||||
};
|
};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Indicator {
|
pub struct Indicator {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use super::TextInput;
|
||||||
|
use crate::theme::ActiveTheme as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
fill, point, px, relative, size, Bounds, Corners, Element, ElementId, ElementInputHandler,
|
fill, point, px, relative, size, Bounds, Corners, Element, ElementId, ElementInputHandler,
|
||||||
GlobalElementId, IntoElement, LayoutId, MouseButton, MouseMoveEvent, PaintQuad, Path, Pixels,
|
GlobalElementId, IntoElement, LayoutId, MouseButton, MouseMoveEvent, PaintQuad, Path, Pixels,
|
||||||
@@ -5,10 +7,6 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::theme::ActiveTheme as _;
|
|
||||||
|
|
||||||
use super::TextInput;
|
|
||||||
|
|
||||||
const RIGHT_MARGIN: Pixels = px(5.);
|
const RIGHT_MARGIN: Pixels = px(5.);
|
||||||
const CURSOR_INSET: Pixels = px(0.5);
|
const CURSOR_INSET: Pixels = px(0.5);
|
||||||
|
|
||||||
@@ -146,7 +144,7 @@ impl TextElement {
|
|||||||
),
|
),
|
||||||
size(px(1.5), line_height),
|
size(px(1.5), line_height),
|
||||||
),
|
),
|
||||||
crate::blue_500(),
|
cx.theme().primary,
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ mod clear_button;
|
|||||||
mod element;
|
mod element;
|
||||||
#[allow(clippy::module_inception)]
|
#[allow(clippy::module_inception)]
|
||||||
mod input;
|
mod input;
|
||||||
mod otp_input;
|
|
||||||
|
|
||||||
pub(crate) use clear_button::*;
|
pub(crate) use clear_button::*;
|
||||||
pub use input::*;
|
pub use input::*;
|
||||||
pub use otp_input::*;
|
|
||||||
|
|||||||
@@ -1,274 +0,0 @@
|
|||||||
use gpui::{
|
|
||||||
div, prelude::FluentBuilder, px, AnyElement, Context, EventEmitter, FocusHandle, FocusableView,
|
|
||||||
InteractiveElement, IntoElement, KeyDownEvent, Model, MouseButton, MouseDownEvent,
|
|
||||||
ParentElement as _, Render, SharedString, Styled as _, ViewContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{h_flex, theme::ActiveTheme, v_flex, Icon, IconName, Sizable, Size};
|
|
||||||
|
|
||||||
use super::{blink_cursor::BlinkCursor, InputEvent};
|
|
||||||
|
|
||||||
pub enum InputOptEvent {
|
|
||||||
/// When all OTP input have filled, this event will be triggered.
|
|
||||||
Change(SharedString),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A One Time Password (OTP) input element.
|
|
||||||
///
|
|
||||||
/// This can accept a fixed length number and can be masked.
|
|
||||||
///
|
|
||||||
/// Use case example:
|
|
||||||
///
|
|
||||||
/// - SMS OTP
|
|
||||||
/// - Authenticator OTP
|
|
||||||
pub struct OtpInput {
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
length: usize,
|
|
||||||
number_of_groups: usize,
|
|
||||||
masked: bool,
|
|
||||||
value: SharedString,
|
|
||||||
blink_cursor: Model<BlinkCursor>,
|
|
||||||
size: Size,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OtpInput {
|
|
||||||
pub fn new(length: usize, cx: &mut ViewContext<Self>) -> Self {
|
|
||||||
let focus_handle = cx.focus_handle();
|
|
||||||
let blink_cursor = cx.new_model(|_| BlinkCursor::new());
|
|
||||||
let input = Self {
|
|
||||||
focus_handle: focus_handle.clone(),
|
|
||||||
length,
|
|
||||||
number_of_groups: 2,
|
|
||||||
value: SharedString::default(),
|
|
||||||
masked: false,
|
|
||||||
blink_cursor: blink_cursor.clone(),
|
|
||||||
size: Size::Medium,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Observe the blink cursor to repaint the view when it changes.
|
|
||||||
cx.observe(&blink_cursor, |_, _, cx| cx.notify()).detach();
|
|
||||||
// Blink the cursor when the window is active, pause when it's not.
|
|
||||||
cx.observe_window_activation(|this, cx| {
|
|
||||||
if cx.is_window_active() {
|
|
||||||
let focus_handle = this.focus_handle.clone();
|
|
||||||
if focus_handle.is_focused(cx) {
|
|
||||||
this.blink_cursor.update(cx, |blink_cursor, cx| {
|
|
||||||
blink_cursor.start(cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
cx.on_focus(&focus_handle, Self::on_focus).detach();
|
|
||||||
cx.on_blur(&focus_handle, Self::on_blur).detach();
|
|
||||||
|
|
||||||
input
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set number of groups in the OTP Input.
|
|
||||||
pub fn groups(mut self, n: usize) -> Self {
|
|
||||||
self.number_of_groups = n;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set default value of the OTP Input.
|
|
||||||
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
|
|
||||||
self.value = value.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set value of the OTP Input.
|
|
||||||
pub fn set_value(&mut self, value: impl Into<SharedString>, cx: &mut ViewContext<Self>) {
|
|
||||||
self.value = value.into();
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the value of the OTP Input.
|
|
||||||
pub fn value(&self) -> SharedString {
|
|
||||||
self.value.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set masked to true use masked input.
|
|
||||||
pub fn masked(mut self, masked: bool) -> Self {
|
|
||||||
self.masked = masked;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set masked to true use masked input.
|
|
||||||
pub fn set_masked(&mut self, masked: bool, cx: &mut ViewContext<Self>) {
|
|
||||||
self.masked = masked;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn focus(&self, cx: &mut ViewContext<Self>) {
|
|
||||||
self.focus_handle.focus(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_input_mouse_down(&mut self, _: &MouseDownEvent, cx: &mut ViewContext<Self>) {
|
|
||||||
cx.focus(&self.focus_handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext<Self>) {
|
|
||||||
let mut chars: Vec<char> = self.value.chars().collect();
|
|
||||||
let ix = chars.len();
|
|
||||||
|
|
||||||
let key = event.keystroke.key.as_str();
|
|
||||||
|
|
||||||
match key {
|
|
||||||
"backspace" => {
|
|
||||||
if ix > 0 {
|
|
||||||
let ix = ix - 1;
|
|
||||||
chars.remove(ix);
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.prevent_default();
|
|
||||||
cx.stop_propagation();
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let c = key.chars().next().unwrap();
|
|
||||||
if !c.is_ascii_digit() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ix >= self.length {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
chars.push(c);
|
|
||||||
|
|
||||||
cx.prevent_default();
|
|
||||||
cx.stop_propagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.pause_blink_cursor(cx);
|
|
||||||
self.value = SharedString::from(chars.iter().collect::<String>());
|
|
||||||
|
|
||||||
if self.value.chars().count() == self.length {
|
|
||||||
cx.emit(InputEvent::Change(self.value.clone()));
|
|
||||||
}
|
|
||||||
cx.notify()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
|
||||||
self.blink_cursor.update(cx, |cursor, cx| {
|
|
||||||
cursor.start(cx);
|
|
||||||
});
|
|
||||||
cx.emit(InputEvent::Focus);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
|
|
||||||
self.blink_cursor.update(cx, |cursor, cx| {
|
|
||||||
cursor.stop(cx);
|
|
||||||
});
|
|
||||||
cx.emit(InputEvent::Blur);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pause_blink_cursor(&mut self, cx: &mut ViewContext<Self>) {
|
|
||||||
self.blink_cursor.update(cx, |cursor, cx| {
|
|
||||||
cursor.pause(cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sizable for OtpInput {
|
|
||||||
fn with_size(mut self, size: impl Into<crate::Size>) -> Self {
|
|
||||||
self.size = size.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FocusableView for OtpInput {
|
|
||||||
fn focus_handle(&self, _: &gpui::AppContext) -> FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl EventEmitter<InputEvent> for OtpInput {}
|
|
||||||
|
|
||||||
impl Render for OtpInput {
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
||||||
let blink_show = self.blink_cursor.read(cx).visible();
|
|
||||||
let is_focused = self.focus_handle.is_focused(cx);
|
|
||||||
|
|
||||||
let text_size = match self.size {
|
|
||||||
Size::XSmall => px(14.),
|
|
||||||
Size::Small => px(14.),
|
|
||||||
Size::Medium => px(16.),
|
|
||||||
Size::Large => px(18.),
|
|
||||||
Size::Size(v) => v * 0.5,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut groups: Vec<Vec<AnyElement>> = Vec::with_capacity(self.number_of_groups);
|
|
||||||
let mut group_ix = 0;
|
|
||||||
let group_items_count = self.length / self.number_of_groups;
|
|
||||||
for _ in 0..self.number_of_groups {
|
|
||||||
groups.push(vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0..self.length {
|
|
||||||
let c = self.value.chars().nth(i);
|
|
||||||
if i % group_items_count == 0 && i != 0 {
|
|
||||||
group_ix += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_input_focused = i == self.value.chars().count() && is_focused;
|
|
||||||
|
|
||||||
groups[group_ix].push(
|
|
||||||
h_flex()
|
|
||||||
.id(("input-otp", i))
|
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().input)
|
|
||||||
.bg(cx.theme().background)
|
|
||||||
.when(is_input_focused, |this| this.border_color(cx.theme().ring))
|
|
||||||
.when(cx.theme().shadow, |this| this.shadow_sm())
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.rounded_md()
|
|
||||||
.text_size(text_size)
|
|
||||||
.map(|this| match self.size {
|
|
||||||
Size::XSmall => this.w_6().h_6(),
|
|
||||||
Size::Small => this.w_6().h_6(),
|
|
||||||
Size::Medium => this.w_8().h_8(),
|
|
||||||
Size::Large => this.w_11().h_11(),
|
|
||||||
Size::Size(px) => this.w(px).h(px),
|
|
||||||
})
|
|
||||||
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_input_mouse_down))
|
|
||||||
.map(|this| match c {
|
|
||||||
Some(c) => {
|
|
||||||
if self.masked {
|
|
||||||
this.child(
|
|
||||||
Icon::new(IconName::Asterisk)
|
|
||||||
.text_color(cx.theme().secondary_foreground)
|
|
||||||
.with_size(text_size),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.child(c.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => this.when(is_input_focused && blink_show, |this| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.h_4()
|
|
||||||
.w_0()
|
|
||||||
.border_l_3()
|
|
||||||
.border_color(crate::blue_500()),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.into_any_element(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.track_focus(&self.focus_handle)
|
|
||||||
.on_key_down(cx.listener(Self::on_key_down))
|
|
||||||
.items_center()
|
|
||||||
.child(
|
|
||||||
h_flex().items_center().gap_5().children(
|
|
||||||
groups
|
|
||||||
.into_iter()
|
|
||||||
.map(|inputs| h_flex().items_center().gap_1().children(inputs)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
pub mod animation;
|
pub mod animation;
|
||||||
pub mod badge;
|
pub mod badge;
|
||||||
pub mod breadcrumb;
|
|
||||||
pub mod button;
|
pub mod button;
|
||||||
pub mod button_group;
|
pub mod button_group;
|
||||||
pub mod checkbox;
|
pub mod checkbox;
|
||||||
pub mod clipboard;
|
pub mod clipboard;
|
||||||
pub mod color_picker;
|
|
||||||
pub mod context_menu;
|
pub mod context_menu;
|
||||||
pub mod divider;
|
pub mod divider;
|
||||||
pub mod dock;
|
pub mod dock;
|
||||||
@@ -14,11 +12,9 @@ pub mod history;
|
|||||||
pub mod indicator;
|
pub mod indicator;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod label;
|
pub mod label;
|
||||||
pub mod link;
|
|
||||||
pub mod list;
|
pub mod list;
|
||||||
pub mod modal;
|
pub mod modal;
|
||||||
pub mod notification;
|
pub mod notification;
|
||||||
pub mod number_input;
|
|
||||||
pub mod popover;
|
pub mod popover;
|
||||||
pub mod popup_menu;
|
pub mod popup_menu;
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
@@ -26,7 +22,6 @@ pub mod progress;
|
|||||||
pub mod radio;
|
pub mod radio;
|
||||||
pub mod resizable;
|
pub mod resizable;
|
||||||
pub mod scroll;
|
pub mod scroll;
|
||||||
pub mod sidebar;
|
|
||||||
pub mod skeleton;
|
pub mod skeleton;
|
||||||
pub mod slider;
|
pub mod slider;
|
||||||
pub mod switch;
|
pub mod switch;
|
||||||
@@ -34,7 +29,7 @@ pub mod tab;
|
|||||||
pub mod theme;
|
pub mod theme;
|
||||||
pub mod tooltip;
|
pub mod tooltip;
|
||||||
|
|
||||||
pub use colors::*;
|
pub use crate::Disableable;
|
||||||
pub use event::InteractiveElementExt;
|
pub use event::InteractiveElementExt;
|
||||||
pub use focusable::FocusableCycle;
|
pub use focusable::FocusableCycle;
|
||||||
pub use icon::*;
|
pub use icon::*;
|
||||||
@@ -43,9 +38,6 @@ pub use styled::*;
|
|||||||
pub use title_bar::*;
|
pub use title_bar::*;
|
||||||
pub use window_border::{window_border, WindowBorder};
|
pub use window_border::{window_border, WindowBorder};
|
||||||
|
|
||||||
pub use crate::Disableable;
|
|
||||||
|
|
||||||
mod colors;
|
|
||||||
mod event;
|
mod event;
|
||||||
mod focusable;
|
mod focusable;
|
||||||
mod icon;
|
mod icon;
|
||||||
@@ -54,12 +46,6 @@ mod styled;
|
|||||||
mod title_bar;
|
mod title_bar;
|
||||||
mod window_border;
|
mod window_border;
|
||||||
|
|
||||||
use rust_embed::RustEmbed;
|
|
||||||
|
|
||||||
#[derive(RustEmbed)]
|
|
||||||
#[folder = "../../assets"]
|
|
||||||
pub struct Assets;
|
|
||||||
|
|
||||||
/// Initialize the UI module.
|
/// Initialize the UI module.
|
||||||
///
|
///
|
||||||
/// This must be called before using any of the UI components.
|
/// This must be called before using any of the UI components.
|
||||||
@@ -69,7 +55,6 @@ pub fn init(cx: &mut gpui::AppContext) {
|
|||||||
dock::init(cx);
|
dock::init(cx);
|
||||||
dropdown::init(cx);
|
dropdown::init(cx);
|
||||||
input::init(cx);
|
input::init(cx);
|
||||||
number_input::init(cx);
|
|
||||||
list::init(cx);
|
list::init(cx);
|
||||||
modal::init(cx);
|
modal::init(cx);
|
||||||
popover::init(cx);
|
popover::init(cx);
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
use gpui::{
|
|
||||||
div, ClickEvent, Div, ElementId, InteractiveElement, IntoElement, MouseButton, ParentElement,
|
|
||||||
RenderOnce, SharedString, Stateful, StatefulInteractiveElement, Styled,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::theme::ActiveTheme as _;
|
|
||||||
|
|
||||||
type OnClick = Option<Box<dyn Fn(&ClickEvent, &mut gpui::WindowContext) + 'static>>;
|
|
||||||
|
|
||||||
/// A Link element like a `<a>` tag in HTML.
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct Link {
|
|
||||||
base: Stateful<Div>,
|
|
||||||
href: Option<SharedString>,
|
|
||||||
disabled: bool,
|
|
||||||
on_click: OnClick,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Link {
|
|
||||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
|
||||||
Self {
|
|
||||||
base: div().id(id),
|
|
||||||
href: None,
|
|
||||||
on_click: None,
|
|
||||||
disabled: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn href(mut self, href: impl Into<SharedString>) -> Self {
|
|
||||||
self.href = Some(href.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_click(
|
|
||||||
mut self,
|
|
||||||
handler: impl Fn(&ClickEvent, &mut gpui::WindowContext) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.on_click = Some(Box::new(handler));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
|
||||||
self.disabled = disabled;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Styled for Link {
|
|
||||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
|
||||||
self.base.style()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParentElement for Link {
|
|
||||||
fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
|
|
||||||
self.base.extend(elements)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for Link {
|
|
||||||
fn render(self, cx: &mut gpui::WindowContext) -> impl IntoElement {
|
|
||||||
let href = self.href.clone();
|
|
||||||
let on_click = self.on_click;
|
|
||||||
|
|
||||||
div()
|
|
||||||
.text_color(cx.theme().link)
|
|
||||||
.text_decoration_1()
|
|
||||||
.text_decoration_color(cx.theme().link)
|
|
||||||
.hover(|this| {
|
|
||||||
this.text_color(cx.theme().link.opacity(0.8))
|
|
||||||
.text_decoration_1()
|
|
||||||
})
|
|
||||||
.cursor_pointer()
|
|
||||||
.child(
|
|
||||||
self.base
|
|
||||||
.active(|this| {
|
|
||||||
this.text_color(cx.theme().link.opacity(0.6))
|
|
||||||
.text_decoration_1()
|
|
||||||
})
|
|
||||||
.on_mouse_down(MouseButton::Left, |_, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
})
|
|
||||||
.on_click({
|
|
||||||
move |e, cx| {
|
|
||||||
if let Some(href) = &href {
|
|
||||||
cx.open_url(&href.clone());
|
|
||||||
}
|
|
||||||
if let Some(on_click) = &on_click {
|
|
||||||
on_click(e, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
use std::{any::TypeId, collections::VecDeque, sync::Arc, time::Duration};
|
use crate::{
|
||||||
|
animation::cubic_bezier,
|
||||||
|
button::{Button, ButtonVariants as _},
|
||||||
|
h_flex,
|
||||||
|
theme::{
|
||||||
|
colors::{blue, green, red, yellow},
|
||||||
|
scale::ColorScaleStep,
|
||||||
|
ActiveTheme as _,
|
||||||
|
},
|
||||||
|
v_flex, Icon, IconName, Sizable as _, StyledExt,
|
||||||
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, prelude::FluentBuilder, px, Animation, AnimationExt, ClickEvent, DismissEvent, ElementId,
|
div, prelude::FluentBuilder, px, Animation, AnimationExt, ClickEvent, DismissEvent, ElementId,
|
||||||
EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _, Render, SharedString,
|
EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _, Render, SharedString,
|
||||||
StatefulInteractiveElement, Styled, View, ViewContext, VisualContext, WindowContext,
|
StatefulInteractiveElement, Styled, View, ViewContext, VisualContext, WindowContext,
|
||||||
};
|
};
|
||||||
use smol::Timer;
|
use smol::Timer;
|
||||||
|
use std::{any::TypeId, collections::VecDeque, sync::Arc, time::Duration};
|
||||||
use crate::{
|
|
||||||
animation::cubic_bezier,
|
|
||||||
button::{Button, ButtonVariants as _},
|
|
||||||
h_flex,
|
|
||||||
theme::ActiveTheme as _,
|
|
||||||
v_flex, Icon, IconName, Sizable as _, StyledExt,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub enum NotificationType {
|
pub enum NotificationType {
|
||||||
Info,
|
Info,
|
||||||
@@ -206,16 +208,16 @@ impl Render for Notification {
|
|||||||
let icon = match self.icon.clone() {
|
let icon = match self.icon.clone() {
|
||||||
Some(icon) => icon,
|
Some(icon) => icon,
|
||||||
None => match self.type_ {
|
None => match self.type_ {
|
||||||
NotificationType::Info => Icon::new(IconName::Info).text_color(crate::blue_500()),
|
NotificationType::Info => {
|
||||||
NotificationType::Success => {
|
Icon::new(IconName::Info).text_color(blue().step(cx, ColorScaleStep::FIVE))
|
||||||
Icon::new(IconName::CircleCheck).text_color(crate::green_500())
|
|
||||||
}
|
|
||||||
NotificationType::Warning => {
|
|
||||||
Icon::new(IconName::TriangleAlert).text_color(crate::yellow_500())
|
|
||||||
}
|
}
|
||||||
NotificationType::Error => {
|
NotificationType::Error => {
|
||||||
Icon::new(IconName::CircleX).text_color(crate::red_500())
|
Icon::new(IconName::CircleX).text_color(red().step(cx, ColorScaleStep::FIVE))
|
||||||
}
|
}
|
||||||
|
NotificationType::Success => Icon::new(IconName::CircleCheck)
|
||||||
|
.text_color(green().step(cx, ColorScaleStep::FIVE)),
|
||||||
|
NotificationType::Warning => Icon::new(IconName::TriangleAlert)
|
||||||
|
.text_color(yellow().step(cx, ColorScaleStep::FIVE)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
use gpui::{
|
|
||||||
actions, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement,
|
|
||||||
KeyBinding, ParentElement, Render, SharedString, Styled, Subscription, View, ViewContext,
|
|
||||||
VisualContext,
|
|
||||||
};
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
button::{Button, ButtonVariants as _},
|
|
||||||
h_flex,
|
|
||||||
input::{InputEvent, TextInput},
|
|
||||||
prelude::FluentBuilder,
|
|
||||||
theme::ActiveTheme,
|
|
||||||
IconName, Sizable, Size, StyledExt,
|
|
||||||
};
|
|
||||||
|
|
||||||
actions!(number_input, [Increment, Decrement]);
|
|
||||||
|
|
||||||
const KEY_CONTENT: &str = "NumberInput";
|
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
|
||||||
cx.bind_keys(vec![
|
|
||||||
KeyBinding::new("up", Increment, Some(KEY_CONTENT)),
|
|
||||||
KeyBinding::new("down", Decrement, Some(KEY_CONTENT)),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct NumberInput {
|
|
||||||
input: View<TextInput>,
|
|
||||||
_subscriptions: Vec<Subscription>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NumberInput {
|
|
||||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
|
||||||
// Default pattern for the number input.
|
|
||||||
let pattern = Regex::new(r"^-?(\d+)?\.?(\d+)?$").unwrap();
|
|
||||||
|
|
||||||
let input = cx.new_view(|cx| TextInput::new(cx).pattern(pattern).appearance(false));
|
|
||||||
|
|
||||||
let _subscriptions = vec![cx.subscribe(&input, |_, _, event: &InputEvent, cx| {
|
|
||||||
cx.emit(NumberInputEvent::Input(event.clone()));
|
|
||||||
})];
|
|
||||||
|
|
||||||
Self {
|
|
||||||
input,
|
|
||||||
_subscriptions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn placeholder(
|
|
||||||
self,
|
|
||||||
placeholder: impl Into<SharedString>,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) -> Self {
|
|
||||||
self.input
|
|
||||||
.update(cx, |input, _| input.set_placeholder(placeholder));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_placeholder(&self, text: impl Into<SharedString>, cx: &mut ViewContext<Self>) {
|
|
||||||
self.input.update(cx, |input, _| {
|
|
||||||
input.set_placeholder(text);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pattern(self, pattern: regex::Regex, cx: &mut ViewContext<Self>) -> Self {
|
|
||||||
self.input.update(cx, |input, _| input.set_pattern(pattern));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_size(self, size: Size, cx: &mut ViewContext<Self>) -> Self {
|
|
||||||
self.input.update(cx, |input, cx| input.set_size(size, cx));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn small(self, cx: &mut ViewContext<Self>) -> Self {
|
|
||||||
self.set_size(Size::Small, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn xsmall(self, cx: &mut ViewContext<Self>) -> Self {
|
|
||||||
self.set_size(Size::XSmall, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn large(self, cx: &mut ViewContext<Self>) -> Self {
|
|
||||||
self.set_size(Size::Large, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_value(&self, text: impl Into<SharedString>, cx: &mut ViewContext<Self>) {
|
|
||||||
self.input.update(cx, |input, cx| input.set_text(text, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_disabled(&self, disabled: bool, cx: &mut ViewContext<Self>) {
|
|
||||||
self.input
|
|
||||||
.update(cx, |input, cx| input.set_disabled(disabled, cx));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn increment(&mut self, cx: &mut ViewContext<Self>) {
|
|
||||||
self.handle_increment(&Increment, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decrement(&mut self, cx: &mut ViewContext<Self>) {
|
|
||||||
self.handle_decrement(&Decrement, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_increment(&mut self, _: &Increment, cx: &mut ViewContext<Self>) {
|
|
||||||
self.on_step(StepAction::Increment, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_decrement(&mut self, _: &Decrement, cx: &mut ViewContext<Self>) {
|
|
||||||
self.on_step(StepAction::Decrement, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_step(&mut self, action: StepAction, cx: &mut ViewContext<Self>) {
|
|
||||||
cx.emit(NumberInputEvent::Step(action));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FocusableView for NumberInput {
|
|
||||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
|
||||||
self.input.focus_handle(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum StepAction {
|
|
||||||
Decrement,
|
|
||||||
Increment,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum NumberInputEvent {
|
|
||||||
Input(InputEvent),
|
|
||||||
Step(StepAction),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<NumberInputEvent> for NumberInput {}
|
|
||||||
|
|
||||||
impl Render for NumberInput {
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
||||||
let focused = self.input.focus_handle(cx).is_focused(cx);
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.key_context(KEY_CONTENT)
|
|
||||||
.on_action(cx.listener(Self::handle_increment))
|
|
||||||
.on_action(cx.listener(Self::handle_decrement))
|
|
||||||
.flex_1()
|
|
||||||
.px_1()
|
|
||||||
.gap_x_3()
|
|
||||||
.bg(cx.theme().background)
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.border_1()
|
|
||||||
.rounded_md()
|
|
||||||
.when(focused, |this| this.outline(cx))
|
|
||||||
.child(
|
|
||||||
Button::new("minus")
|
|
||||||
.ghost()
|
|
||||||
.xsmall()
|
|
||||||
.icon(IconName::Minus)
|
|
||||||
.on_click(cx.listener(|this, _, cx| this.on_step(StepAction::Decrement, cx))),
|
|
||||||
)
|
|
||||||
.child(self.input.clone())
|
|
||||||
.child(
|
|
||||||
Button::new("plus")
|
|
||||||
.ghost()
|
|
||||||
.xsmall()
|
|
||||||
.icon(IconName::Plus)
|
|
||||||
.on_click(cx.listener(|this, _, cx| this.on_step(StepAction::Increment, cx))),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
use gpui::*;
|
|
||||||
use prelude::FluentBuilder;
|
|
||||||
use std::{cell::Cell, ops::Deref, rc::Rc};
|
|
||||||
|
|
||||||
use crate::scroll::{Scrollbar, ScrollbarState};
|
use crate::scroll::{Scrollbar, ScrollbarState};
|
||||||
use crate::StyledExt;
|
use crate::StyledExt;
|
||||||
use crate::{
|
use crate::{
|
||||||
button::Button, h_flex, list::ListItem, popover::Popover, theme::ActiveTheme, v_flex, Icon,
|
button::Button, h_flex, list::ListItem, popover::Popover, theme::ActiveTheme, v_flex, Icon,
|
||||||
IconName, Selectable, Sizable as _,
|
IconName, Selectable, Sizable as _,
|
||||||
};
|
};
|
||||||
|
use gpui::*;
|
||||||
|
use prelude::FluentBuilder;
|
||||||
|
use std::{cell::Cell, ops::Deref, rc::Rc};
|
||||||
|
|
||||||
actions!(menu, [Confirm, Dismiss, SelectNext, SelectPrev]);
|
actions!(menu, [Confirm, Dismiss, SelectNext, SelectPrev]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
//! The prelude of this crate. When building UI in Zed you almost always want to import this.
|
|
||||||
|
|
||||||
pub use gpui::prelude::*;
|
pub use gpui::prelude::*;
|
||||||
#[allow(unused_imports)]
|
|
||||||
pub use gpui::{
|
pub use gpui::{
|
||||||
div, px, relative, rems, AbsoluteLength, DefiniteLength, Div, Element, ElementId,
|
div, px, relative, rems, AbsoluteLength, DefiniteLength, Div, Element, ElementId,
|
||||||
InteractiveElement, ParentElement, Pixels, Rems, RenderOnce, SharedString, Styled, ViewContext,
|
InteractiveElement, ParentElement, Pixels, Rems, RenderOnce, SharedString, Styled, ViewContext,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use gpui::{Axis, ViewContext};
|
use gpui::{Axis, ViewContext};
|
||||||
|
|
||||||
mod panel;
|
mod panel;
|
||||||
mod resize_handle;
|
mod resize_handle;
|
||||||
|
|
||||||
pub use panel::*;
|
pub use panel::*;
|
||||||
pub(crate) use resize_handle::*;
|
pub(crate) use resize_handle::*;
|
||||||
|
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
use gpui::{
|
|
||||||
prelude::FluentBuilder as _, Div, ElementId, InteractiveElement, IntoElement, ParentElement,
|
|
||||||
RenderOnce, SharedString, Styled,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{h_flex, popup_menu::PopupMenuExt, theme::ActiveTheme as _, Collapsible, Selectable};
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct SidebarFooter {
|
|
||||||
id: ElementId,
|
|
||||||
base: Div,
|
|
||||||
selected: bool,
|
|
||||||
is_collapsed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SidebarFooter {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
id: SharedString::from("sidebar-footer").into(),
|
|
||||||
base: h_flex().gap_2().w_full(),
|
|
||||||
selected: false,
|
|
||||||
is_collapsed: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SidebarFooter {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Selectable for SidebarFooter {
|
|
||||||
fn selected(mut self, selected: bool) -> Self {
|
|
||||||
self.selected = selected;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn element_id(&self) -> &gpui::ElementId {
|
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Collapsible for SidebarFooter {
|
|
||||||
fn is_collapsed(&self) -> bool {
|
|
||||||
self.is_collapsed
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collapsed(mut self, collapsed: bool) -> Self {
|
|
||||||
self.is_collapsed = collapsed;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParentElement for SidebarFooter {
|
|
||||||
fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
|
|
||||||
self.base.extend(elements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Styled for SidebarFooter {
|
|
||||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
|
||||||
self.base.style()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PopupMenuExt for SidebarFooter {}
|
|
||||||
|
|
||||||
impl RenderOnce for SidebarFooter {
|
|
||||||
fn render(self, cx: &mut gpui::WindowContext) -> impl gpui::IntoElement {
|
|
||||||
h_flex()
|
|
||||||
.id(self.id)
|
|
||||||
.gap_2()
|
|
||||||
.p_2()
|
|
||||||
.w_full()
|
|
||||||
.justify_between()
|
|
||||||
.cursor_pointer()
|
|
||||||
.rounded_md()
|
|
||||||
.hover(|this| {
|
|
||||||
this.bg(cx.theme().sidebar_accent)
|
|
||||||
.text_color(cx.theme().sidebar_accent_foreground)
|
|
||||||
})
|
|
||||||
.when(self.selected, |this| {
|
|
||||||
this.bg(cx.theme().sidebar_accent)
|
|
||||||
.text_color(cx.theme().sidebar_accent_foreground)
|
|
||||||
})
|
|
||||||
.child(self.base)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
use crate::{theme::ActiveTheme, v_flex, Collapsible};
|
|
||||||
use gpui::{
|
|
||||||
div, prelude::FluentBuilder as _, Div, IntoElement, ParentElement, RenderOnce, SharedString,
|
|
||||||
Styled as _, WindowContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A sidebar group
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct SidebarGroup<E: Collapsible + IntoElement + 'static> {
|
|
||||||
base: Div,
|
|
||||||
label: SharedString,
|
|
||||||
is_collapsed: bool,
|
|
||||||
children: Vec<E>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Collapsible + IntoElement> SidebarGroup<E> {
|
|
||||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
|
||||||
Self {
|
|
||||||
base: div().gap_2().flex_col(),
|
|
||||||
label: label.into(),
|
|
||||||
is_collapsed: false,
|
|
||||||
children: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn child(mut self, child: E) -> Self {
|
|
||||||
self.children.push(child);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn children(mut self, children: impl IntoIterator<Item = E>) -> Self {
|
|
||||||
self.children.extend(children);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Collapsible + IntoElement> Collapsible for SidebarGroup<E> {
|
|
||||||
fn is_collapsed(&self) -> bool {
|
|
||||||
self.is_collapsed
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collapsed(mut self, collapsed: bool) -> Self {
|
|
||||||
self.is_collapsed = collapsed;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Collapsible + IntoElement> RenderOnce for SidebarGroup<E> {
|
|
||||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.relative()
|
|
||||||
.p_2()
|
|
||||||
.when(!self.is_collapsed, |this| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.flex_shrink_0()
|
|
||||||
.px_2()
|
|
||||||
.rounded_md()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().sidebar_foreground.opacity(0.7))
|
|
||||||
.h_8()
|
|
||||||
.child(self.label),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
self.base.children(
|
|
||||||
self.children
|
|
||||||
.into_iter()
|
|
||||||
.map(|child| child.collapsed(self.is_collapsed)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
use gpui::{
|
|
||||||
prelude::FluentBuilder as _, Div, ElementId, InteractiveElement, IntoElement, ParentElement,
|
|
||||||
RenderOnce, SharedString, Styled,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{h_flex, popup_menu::PopupMenuExt, theme::ActiveTheme as _, Collapsible, Selectable};
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct SidebarHeader {
|
|
||||||
id: ElementId,
|
|
||||||
base: Div,
|
|
||||||
selected: bool,
|
|
||||||
is_collapsed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SidebarHeader {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
id: SharedString::from("sidebar-header").into(),
|
|
||||||
base: h_flex().gap_2().w_full(),
|
|
||||||
selected: false,
|
|
||||||
is_collapsed: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SidebarHeader {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Selectable for SidebarHeader {
|
|
||||||
fn selected(mut self, selected: bool) -> Self {
|
|
||||||
self.selected = selected;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn element_id(&self) -> &gpui::ElementId {
|
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Collapsible for SidebarHeader {
|
|
||||||
fn is_collapsed(&self) -> bool {
|
|
||||||
self.is_collapsed
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collapsed(mut self, collapsed: bool) -> Self {
|
|
||||||
self.is_collapsed = collapsed;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParentElement for SidebarHeader {
|
|
||||||
fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
|
|
||||||
self.base.extend(elements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Styled for SidebarHeader {
|
|
||||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
|
||||||
self.base.style()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PopupMenuExt for SidebarHeader {}
|
|
||||||
|
|
||||||
impl RenderOnce for SidebarHeader {
|
|
||||||
fn render(self, cx: &mut gpui::WindowContext) -> impl gpui::IntoElement {
|
|
||||||
h_flex()
|
|
||||||
.id(self.id)
|
|
||||||
.gap_2()
|
|
||||||
.p_2()
|
|
||||||
.w_full()
|
|
||||||
.justify_between()
|
|
||||||
.cursor_pointer()
|
|
||||||
.rounded_md()
|
|
||||||
.hover(|this| {
|
|
||||||
this.bg(cx.theme().sidebar_accent)
|
|
||||||
.text_color(cx.theme().sidebar_accent_foreground)
|
|
||||||
})
|
|
||||||
.when(self.selected, |this| {
|
|
||||||
this.bg(cx.theme().sidebar_accent)
|
|
||||||
.text_color(cx.theme().sidebar_accent_foreground)
|
|
||||||
})
|
|
||||||
.child(self.base)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
use crate::{h_flex, theme::ActiveTheme as _, v_flex, Collapsible, Icon, IconName, StyledExt};
|
|
||||||
use gpui::{
|
|
||||||
div, percentage, prelude::FluentBuilder as _, ClickEvent, InteractiveElement as _, IntoElement,
|
|
||||||
ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _, Styled as _,
|
|
||||||
WindowContext,
|
|
||||||
};
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct SidebarMenu {
|
|
||||||
is_collapsed: bool,
|
|
||||||
items: Vec<SidebarMenuItem>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SidebarMenu {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
items: Vec::new(),
|
|
||||||
is_collapsed: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn menu(
|
|
||||||
mut self,
|
|
||||||
label: impl Into<SharedString>,
|
|
||||||
icon: Option<Icon>,
|
|
||||||
active: bool,
|
|
||||||
handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.items.push(SidebarMenuItem::Item {
|
|
||||||
icon,
|
|
||||||
label: label.into(),
|
|
||||||
handler: Rc::new(handler),
|
|
||||||
active,
|
|
||||||
is_collapsed: self.is_collapsed,
|
|
||||||
});
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn submenu(
|
|
||||||
mut self,
|
|
||||||
label: impl Into<SharedString>,
|
|
||||||
icon: Option<Icon>,
|
|
||||||
open: bool,
|
|
||||||
items: impl FnOnce(SidebarMenu) -> Self,
|
|
||||||
handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
let menu = SidebarMenu::new();
|
|
||||||
let menu = items(menu);
|
|
||||||
self.items.push(SidebarMenuItem::Submenu {
|
|
||||||
icon,
|
|
||||||
label: label.into(),
|
|
||||||
items: menu.items,
|
|
||||||
is_open: open,
|
|
||||||
is_collapsed: self.is_collapsed,
|
|
||||||
handler: Rc::new(handler),
|
|
||||||
});
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SidebarMenu {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Collapsible for SidebarMenu {
|
|
||||||
fn is_collapsed(&self) -> bool {
|
|
||||||
self.is_collapsed
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collapsed(mut self, collapsed: bool) -> Self {
|
|
||||||
self.is_collapsed = collapsed;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl RenderOnce for SidebarMenu {
|
|
||||||
fn render(self, _: &mut WindowContext) -> impl IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.gap_2()
|
|
||||||
.children(self.items.into_iter().map(|mut item| {
|
|
||||||
match &mut item {
|
|
||||||
SidebarMenuItem::Item { is_collapsed, .. } => *is_collapsed = self.is_collapsed,
|
|
||||||
SidebarMenuItem::Submenu { is_collapsed, .. } => {
|
|
||||||
*is_collapsed = self.is_collapsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Handler = Rc<dyn Fn(&ClickEvent, &mut WindowContext)>;
|
|
||||||
|
|
||||||
/// A sidebar menu item
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
enum SidebarMenuItem {
|
|
||||||
Item {
|
|
||||||
icon: Option<Icon>,
|
|
||||||
label: SharedString,
|
|
||||||
handler: Handler,
|
|
||||||
active: bool,
|
|
||||||
is_collapsed: bool,
|
|
||||||
},
|
|
||||||
Submenu {
|
|
||||||
icon: Option<Icon>,
|
|
||||||
label: SharedString,
|
|
||||||
handler: Handler,
|
|
||||||
items: Vec<SidebarMenuItem>,
|
|
||||||
is_open: bool,
|
|
||||||
is_collapsed: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SidebarMenuItem {
|
|
||||||
fn is_submenu(&self) -> bool {
|
|
||||||
matches!(self, SidebarMenuItem::Submenu { .. })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn icon(&self) -> Option<Icon> {
|
|
||||||
match self {
|
|
||||||
SidebarMenuItem::Item { icon, .. } => icon.clone(),
|
|
||||||
SidebarMenuItem::Submenu { icon, .. } => icon.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn label(&self) -> SharedString {
|
|
||||||
match self {
|
|
||||||
SidebarMenuItem::Item { label, .. } => label.clone(),
|
|
||||||
SidebarMenuItem::Submenu { label, .. } => label.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_active(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
SidebarMenuItem::Item { active, .. } => *active,
|
|
||||||
SidebarMenuItem::Submenu { .. } => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_open(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
SidebarMenuItem::Item { .. } => false,
|
|
||||||
SidebarMenuItem::Submenu { is_open, items, .. } => {
|
|
||||||
*is_open || items.iter().any(|item| item.is_active())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_collapsed(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
SidebarMenuItem::Item { is_collapsed, .. } => *is_collapsed,
|
|
||||||
SidebarMenuItem::Submenu { is_collapsed, .. } => *is_collapsed,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_menu_item(
|
|
||||||
&self,
|
|
||||||
is_submenu: bool,
|
|
||||||
is_active: bool,
|
|
||||||
is_open: bool,
|
|
||||||
cx: &WindowContext,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let handler = match &self {
|
|
||||||
SidebarMenuItem::Item { handler, .. } => Some(handler.clone()),
|
|
||||||
SidebarMenuItem::Submenu { handler, .. } => Some(handler.clone()),
|
|
||||||
};
|
|
||||||
let is_collapsed = self.is_collapsed();
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.id("sidebar-menu-item")
|
|
||||||
.overflow_hidden()
|
|
||||||
.flex_shrink_0()
|
|
||||||
.p_2()
|
|
||||||
.gap_2()
|
|
||||||
.items_center()
|
|
||||||
.rounded_md()
|
|
||||||
.text_sm()
|
|
||||||
.cursor_pointer()
|
|
||||||
.hover(|this| {
|
|
||||||
this.bg(cx.theme().sidebar_accent)
|
|
||||||
.text_color(cx.theme().sidebar_accent_foreground)
|
|
||||||
})
|
|
||||||
.when(is_active, |this| {
|
|
||||||
this.font_medium()
|
|
||||||
.bg(cx.theme().sidebar_accent)
|
|
||||||
.text_color(cx.theme().sidebar_accent_foreground)
|
|
||||||
})
|
|
||||||
.when_some(self.icon(), |this, icon| this.child(icon.size_4()))
|
|
||||||
.when(is_collapsed, |this| {
|
|
||||||
this.justify_center().size_7().mx_auto()
|
|
||||||
})
|
|
||||||
.when(!is_collapsed, |this| {
|
|
||||||
this.h_7()
|
|
||||||
.child(div().flex_1().child(self.label()))
|
|
||||||
.when(is_submenu, |this| {
|
|
||||||
this.child(
|
|
||||||
Icon::new(IconName::ChevronRight)
|
|
||||||
.size_4()
|
|
||||||
.when(is_open, |this| this.rotate(percentage(90. / 360.))),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.when_some(handler, |this, handler| {
|
|
||||||
this.on_click(move |ev, cx| handler(ev, cx))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for SidebarMenuItem {
|
|
||||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
|
||||||
let is_submenu = self.is_submenu();
|
|
||||||
let is_active = self.is_active();
|
|
||||||
let is_open = self.is_open();
|
|
||||||
|
|
||||||
div()
|
|
||||||
.w_full()
|
|
||||||
.child(self.render_menu_item(is_submenu, is_active, is_open, cx))
|
|
||||||
.when(is_open, |this| {
|
|
||||||
this.map(|this| match self {
|
|
||||||
SidebarMenuItem::Submenu {
|
|
||||||
items,
|
|
||||||
is_collapsed,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
if is_collapsed {
|
|
||||||
this
|
|
||||||
} else {
|
|
||||||
this.child(
|
|
||||||
v_flex()
|
|
||||||
.border_l_1()
|
|
||||||
.border_color(cx.theme().sidebar_border)
|
|
||||||
.gap_1()
|
|
||||||
.mx_3p5()
|
|
||||||
.px_2p5()
|
|
||||||
.py_0p5()
|
|
||||||
.children(items),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => this,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
button::{Button, ButtonVariants},
|
|
||||||
h_flex,
|
|
||||||
scroll::ScrollbarAxis,
|
|
||||||
theme::ActiveTheme,
|
|
||||||
v_flex, Collapsible, Icon, IconName, Side, Sizable, StyledExt,
|
|
||||||
};
|
|
||||||
use gpui::{
|
|
||||||
div, prelude::FluentBuilder, px, AnyElement, ClickEvent, Entity, EntityId,
|
|
||||||
InteractiveElement as _, IntoElement, ParentElement, Pixels, Render, RenderOnce, Styled, View,
|
|
||||||
WindowContext,
|
|
||||||
};
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
mod footer;
|
|
||||||
mod group;
|
|
||||||
mod header;
|
|
||||||
mod menu;
|
|
||||||
pub use footer::*;
|
|
||||||
pub use group::*;
|
|
||||||
pub use header::*;
|
|
||||||
pub use menu::*;
|
|
||||||
|
|
||||||
const DEFAULT_WIDTH: Pixels = px(255.);
|
|
||||||
const COLLAPSED_WIDTH: Pixels = px(48.);
|
|
||||||
|
|
||||||
/// A sidebar
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct Sidebar<E: Collapsible + IntoElement + 'static> {
|
|
||||||
/// The parent view id
|
|
||||||
view_id: EntityId,
|
|
||||||
content: Vec<E>,
|
|
||||||
/// header view
|
|
||||||
header: Option<AnyElement>,
|
|
||||||
/// footer view
|
|
||||||
footer: Option<AnyElement>,
|
|
||||||
/// The side of the sidebar
|
|
||||||
side: Side,
|
|
||||||
collapsible: bool,
|
|
||||||
width: Pixels,
|
|
||||||
is_collapsed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Collapsible + IntoElement> Sidebar<E> {
|
|
||||||
fn new(view_id: EntityId, side: Side) -> Self {
|
|
||||||
Self {
|
|
||||||
view_id,
|
|
||||||
content: vec![],
|
|
||||||
header: None,
|
|
||||||
footer: None,
|
|
||||||
side,
|
|
||||||
collapsible: true,
|
|
||||||
width: DEFAULT_WIDTH,
|
|
||||||
is_collapsed: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn left<V: Render + 'static>(view: &View<V>) -> Self {
|
|
||||||
Self::new(view.entity_id(), Side::Left)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn right<V: Render + 'static>(view: &View<V>) -> Self {
|
|
||||||
Self::new(view.entity_id(), Side::Right)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the width of the sidebar
|
|
||||||
pub fn width(mut self, width: Pixels) -> Self {
|
|
||||||
self.width = width;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the sidebar to be collapsible, default is true
|
|
||||||
pub fn collapsible(mut self, collapsible: bool) -> Self {
|
|
||||||
self.collapsible = collapsible;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the sidebar to be collapsed
|
|
||||||
pub fn collapsed(mut self, collapsed: bool) -> Self {
|
|
||||||
self.is_collapsed = collapsed;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the header of the sidebar.
|
|
||||||
pub fn header(mut self, header: impl IntoElement) -> Self {
|
|
||||||
self.header = Some(header.into_any_element());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the footer of the sidebar.
|
|
||||||
pub fn footer(mut self, footer: impl IntoElement) -> Self {
|
|
||||||
self.footer = Some(footer.into_any_element());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a child element to the sidebar, the child must implement `Collapsible`
|
|
||||||
pub fn child(mut self, child: E) -> Self {
|
|
||||||
self.content.push(child);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add multiple children to the sidebar, the children must implement `Collapsible`
|
|
||||||
pub fn children(mut self, children: impl IntoIterator<Item = E>) -> Self {
|
|
||||||
self.content.extend(children);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type OnClick = Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>;
|
|
||||||
|
|
||||||
/// Sidebar collapse button with Icon.
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct SidebarToggleButton {
|
|
||||||
btn: Button,
|
|
||||||
is_collapsed: bool,
|
|
||||||
side: Side,
|
|
||||||
on_click: OnClick,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SidebarToggleButton {
|
|
||||||
fn new(side: Side) -> Self {
|
|
||||||
Self {
|
|
||||||
btn: Button::new("sidebar-collapse").ghost().small(),
|
|
||||||
is_collapsed: false,
|
|
||||||
side,
|
|
||||||
on_click: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn left() -> Self {
|
|
||||||
Self::new(Side::Left)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn right() -> Self {
|
|
||||||
Self::new(Side::Right)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn collapsed(mut self, is_collapsed: bool) -> Self {
|
|
||||||
self.is_collapsed = is_collapsed;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_click(
|
|
||||||
mut self,
|
|
||||||
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.on_click = Some(Rc::new(on_click));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for SidebarToggleButton {
|
|
||||||
fn render(self, _: &mut WindowContext) -> impl IntoElement {
|
|
||||||
let is_collapsed = self.is_collapsed;
|
|
||||||
let on_click = self.on_click.clone();
|
|
||||||
|
|
||||||
let icon = if is_collapsed {
|
|
||||||
if self.side.is_left() {
|
|
||||||
IconName::PanelLeftOpen
|
|
||||||
} else {
|
|
||||||
IconName::PanelRightOpen
|
|
||||||
}
|
|
||||||
} else if self.side.is_left() {
|
|
||||||
IconName::PanelLeftClose
|
|
||||||
} else {
|
|
||||||
IconName::PanelRightClose
|
|
||||||
};
|
|
||||||
|
|
||||||
self.btn
|
|
||||||
.when_some(on_click, |this, on_click| {
|
|
||||||
this.on_click(move |ev, cx| {
|
|
||||||
on_click(ev, cx);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.icon(Icon::new(icon).size_4())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Collapsible + IntoElement> RenderOnce for Sidebar<E> {
|
|
||||||
fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
|
|
||||||
let is_collaped = self.is_collapsed;
|
|
||||||
v_flex()
|
|
||||||
.id("sidebar")
|
|
||||||
.w(self.width)
|
|
||||||
.when(self.is_collapsed, |this| this.w(COLLAPSED_WIDTH))
|
|
||||||
.flex_shrink_0()
|
|
||||||
.h_full()
|
|
||||||
.overflow_hidden()
|
|
||||||
.relative()
|
|
||||||
.bg(cx.theme().sidebar)
|
|
||||||
.text_color(cx.theme().sidebar_foreground)
|
|
||||||
.border_color(cx.theme().sidebar_border)
|
|
||||||
.map(|this| match self.side {
|
|
||||||
Side::Left => this.border_r_1(),
|
|
||||||
Side::Right => this.text_2xl(),
|
|
||||||
})
|
|
||||||
.when_some(self.header.take(), |this, header| {
|
|
||||||
this.child(h_flex().id("header").p_2().gap_2().child(header))
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
v_flex().id("content").flex_1().min_h_0().child(
|
|
||||||
div()
|
|
||||||
.children(self.content.into_iter().map(|c| c.collapsed(is_collaped)))
|
|
||||||
.gap_2()
|
|
||||||
.scrollable(self.view_id, ScrollbarAxis::Vertical),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.when_some(self.footer.take(), |this, footer| {
|
|
||||||
this.child(h_flex().id("footer").gap_2().p_2().child(footer))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
use gpui::{
|
|
||||||
div, px, Axis, Div, Element, ElementId, EntityId, FocusHandle, Pixels, Styled, WindowContext,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fmt::{self, Display, Formatter};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
scroll::{Scrollable, ScrollbarAxis},
|
scroll::{Scrollable, ScrollbarAxis},
|
||||||
theme::ActiveTheme,
|
theme::ActiveTheme,
|
||||||
};
|
};
|
||||||
|
use gpui::{div, px, Axis, Div, Element, ElementId, EntityId, Pixels, Styled, WindowContext};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
/// Returns a `Div` as horizontal flex layout.
|
/// Returns a `Div` as horizontal flex layout.
|
||||||
pub fn h_flex() -> Div {
|
pub fn h_flex() -> Div {
|
||||||
@@ -40,64 +37,6 @@ pub trait StyledExt: Styled + Sized {
|
|||||||
self.flex().flex_col()
|
self.flex().flex_col()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render a border with a width of 1px, color red
|
|
||||||
fn debug_red(self) -> Self {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
self.border_1().border_color(crate::red_500())
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a border with a width of 1px, color blue
|
|
||||||
fn debug_blue(self) -> Self {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
self.border_1().border_color(crate::blue_500())
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a border with a width of 1px, color yellow
|
|
||||||
fn debug_yellow(self) -> Self {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
self.border_1().border_color(crate::yellow_500())
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a border with a width of 1px, color green
|
|
||||||
fn debug_green(self) -> Self {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
self.border_1().border_color(crate::green_500())
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a border with a width of 1px, color pink
|
|
||||||
fn debug_pink(self) -> Self {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
self.border_1().border_color(crate::pink_500())
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a 1px blue border, when if the element is focused
|
|
||||||
fn debug_focused(self, focus_handle: &FocusHandle, cx: &WindowContext) -> Self {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
if focus_handle.contains_focused(cx) {
|
|
||||||
self.debug_blue()
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a border with a width of 1px, color ring color
|
/// Render a border with a width of 1px, color ring color
|
||||||
fn outline(self, cx: &WindowContext) -> Self {
|
fn outline(self, cx: &WindowContext) -> Self {
|
||||||
self.border_color(cx.theme().ring)
|
self.border_color(cx.theme().ring)
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
#[allow(clippy::module_inception)]
|
|
||||||
mod tab;
|
|
||||||
mod tab_bar;
|
|
||||||
|
|
||||||
pub use tab::*;
|
|
||||||
pub use tab_bar::*;
|
|
||||||
@@ -4,6 +4,8 @@ use gpui::prelude::FluentBuilder;
|
|||||||
use gpui::*;
|
use gpui::*;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
pub mod tab_bar;
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Tab {
|
pub struct Tab {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
2195
crates/ui/src/theme/colors.rs
Normal file
2195
crates/ui/src/theme/colors.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,73 +1,186 @@
|
|||||||
|
use crate::scroll::ScrollbarShow;
|
||||||
|
use colors::hsl;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
hsla, point, AppContext, BoxShadow, Global, Hsla, ModelContext, Pixels, SharedString,
|
blue, hsla, transparent_black, AppContext, Global, Hsla, ModelContext, SharedString,
|
||||||
ViewContext, WindowAppearance, WindowContext,
|
ViewContext, WindowAppearance, WindowContext,
|
||||||
};
|
};
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
use crate::scroll::ScrollbarShow;
|
pub mod colors;
|
||||||
|
pub mod scale;
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
Theme::sync_system_appearance(cx)
|
pub struct ThemeColors {
|
||||||
|
pub border: Hsla,
|
||||||
|
pub window_border: Hsla,
|
||||||
|
pub accent: Hsla,
|
||||||
|
pub accent_foreground: Hsla,
|
||||||
|
pub background: Hsla,
|
||||||
|
pub card: Hsla,
|
||||||
|
pub card_foreground: Hsla,
|
||||||
|
pub danger: Hsla,
|
||||||
|
pub danger_active: Hsla,
|
||||||
|
pub danger_foreground: Hsla,
|
||||||
|
pub danger_hover: Hsla,
|
||||||
|
pub drag_border: Hsla,
|
||||||
|
pub drop_target: Hsla,
|
||||||
|
pub foreground: Hsla,
|
||||||
|
pub input: Hsla,
|
||||||
|
pub link: Hsla,
|
||||||
|
pub link_active: Hsla,
|
||||||
|
pub link_hover: Hsla,
|
||||||
|
pub list: Hsla,
|
||||||
|
pub list_active: Hsla,
|
||||||
|
pub list_active_border: Hsla,
|
||||||
|
pub list_even: Hsla,
|
||||||
|
pub list_head: Hsla,
|
||||||
|
pub list_hover: Hsla,
|
||||||
|
pub muted: Hsla,
|
||||||
|
pub muted_foreground: Hsla,
|
||||||
|
pub popover: Hsla,
|
||||||
|
pub popover_foreground: Hsla,
|
||||||
|
pub primary: Hsla,
|
||||||
|
pub primary_active: Hsla,
|
||||||
|
pub primary_foreground: Hsla,
|
||||||
|
pub primary_hover: Hsla,
|
||||||
|
pub progress_bar: Hsla,
|
||||||
|
pub ring: Hsla,
|
||||||
|
pub scrollbar: Hsla,
|
||||||
|
pub scrollbar_thumb: Hsla,
|
||||||
|
pub scrollbar_thumb_hover: Hsla,
|
||||||
|
pub secondary: Hsla,
|
||||||
|
pub secondary_active: Hsla,
|
||||||
|
pub secondary_foreground: Hsla,
|
||||||
|
pub secondary_hover: Hsla,
|
||||||
|
pub selection: Hsla,
|
||||||
|
pub skeleton: Hsla,
|
||||||
|
pub slider_bar: Hsla,
|
||||||
|
pub slider_thumb: Hsla,
|
||||||
|
pub tab: Hsla,
|
||||||
|
pub tab_active: Hsla,
|
||||||
|
pub tab_active_foreground: Hsla,
|
||||||
|
pub tab_bar: Hsla,
|
||||||
|
pub tab_foreground: Hsla,
|
||||||
|
pub title_bar: Hsla,
|
||||||
|
pub title_bar_border: Hsla,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ActiveTheme {
|
impl ThemeColors {
|
||||||
fn theme(&self) -> &Theme;
|
pub fn light() -> Self {
|
||||||
}
|
Self {
|
||||||
|
accent: hsl(240.0, 5.0, 96.0),
|
||||||
|
accent_foreground: hsl(240.0, 5.9, 10.0),
|
||||||
|
background: hsl(0.0, 0.0, 100.),
|
||||||
|
border: hsl(240.0, 5.9, 90.0),
|
||||||
|
window_border: hsl(240.0, 5.9, 78.0),
|
||||||
|
card: hsl(0.0, 0.0, 100.0),
|
||||||
|
card_foreground: hsl(240.0, 10.0, 3.9),
|
||||||
|
danger: hsl(0.0, 84.2, 60.2),
|
||||||
|
danger_active: hsl(0.0, 84.2, 47.0),
|
||||||
|
danger_foreground: hsl(0.0, 0.0, 98.0),
|
||||||
|
danger_hover: hsl(0.0, 84.2, 65.0),
|
||||||
|
drag_border: blue(),
|
||||||
|
drop_target: hsl(235.0, 30., 44.0).opacity(0.25),
|
||||||
|
foreground: hsl(240.0, 10., 3.9),
|
||||||
|
input: hsl(240.0, 5.9, 90.0),
|
||||||
|
link: hsl(221.0, 83.0, 53.0),
|
||||||
|
link_active: hsl(221.0, 83.0, 53.0).darken(0.2),
|
||||||
|
link_hover: hsl(221.0, 83.0, 53.0).lighten(0.2),
|
||||||
|
list: hsl(0.0, 0.0, 100.),
|
||||||
|
list_active: hsl(211.0, 97.0, 85.0).opacity(0.2),
|
||||||
|
list_active_border: hsl(211.0, 97.0, 85.0),
|
||||||
|
list_even: hsl(240.0, 5.0, 96.0),
|
||||||
|
list_head: hsl(0.0, 0.0, 100.),
|
||||||
|
list_hover: hsl(240.0, 4.8, 95.0),
|
||||||
|
muted: hsl(240.0, 4.8, 95.9),
|
||||||
|
muted_foreground: hsl(240.0, 3.8, 46.1),
|
||||||
|
popover: hsl(0.0, 0.0, 100.0),
|
||||||
|
popover_foreground: hsl(240.0, 10.0, 3.9),
|
||||||
|
primary: hsl(223.0, 5.9, 10.0),
|
||||||
|
primary_active: hsl(223.0, 1.9, 25.0),
|
||||||
|
primary_foreground: hsl(223.0, 0.0, 98.0),
|
||||||
|
primary_hover: hsl(223.0, 5.9, 15.0),
|
||||||
|
progress_bar: hsl(223.0, 5.9, 10.0),
|
||||||
|
ring: hsl(240.0, 5.9, 65.0),
|
||||||
|
scrollbar: hsl(0., 0., 97.).opacity(0.75),
|
||||||
|
scrollbar_thumb: hsl(0., 0., 69.).opacity(0.9),
|
||||||
|
scrollbar_thumb_hover: hsl(0., 0., 59.),
|
||||||
|
secondary: hsl(240.0, 5.9, 96.9),
|
||||||
|
secondary_active: hsl(240.0, 5.9, 90.),
|
||||||
|
secondary_foreground: hsl(240.0, 59.0, 10.),
|
||||||
|
secondary_hover: hsl(240.0, 5.9, 98.),
|
||||||
|
selection: hsl(211.0, 97.0, 85.0),
|
||||||
|
skeleton: hsl(223.0, 5.9, 10.0).opacity(0.1),
|
||||||
|
slider_bar: hsl(223.0, 5.9, 10.0),
|
||||||
|
slider_thumb: hsl(0.0, 0.0, 100.0),
|
||||||
|
tab: transparent_black(),
|
||||||
|
tab_active: hsl(0.0, 0.0, 100.0),
|
||||||
|
tab_active_foreground: hsl(240.0, 10., 3.9),
|
||||||
|
tab_bar: hsl(240.0, 4.8, 95.9),
|
||||||
|
tab_foreground: hsl(240.0, 10., 3.9),
|
||||||
|
title_bar: hsl(0.0, 0.0, 98.0),
|
||||||
|
title_bar_border: hsl(220.0, 13.0, 91.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ActiveTheme for AppContext {
|
pub fn dark() -> Self {
|
||||||
fn theme(&self) -> &Theme {
|
Self {
|
||||||
Theme::global(self)
|
accent: hsl(240.0, 3.7, 15.9),
|
||||||
|
accent_foreground: hsl(0.0, 0.0, 78.0),
|
||||||
|
background: hsl(0.0, 0.0, 8.0),
|
||||||
|
border: hsl(240.0, 3.7, 16.9),
|
||||||
|
window_border: hsl(240.0, 3.7, 28.0),
|
||||||
|
card: hsl(0.0, 0.0, 8.0),
|
||||||
|
card_foreground: hsl(0.0, 0.0, 78.0),
|
||||||
|
danger: hsl(0.0, 62.8, 30.6),
|
||||||
|
danger_active: hsl(0.0, 62.8, 20.6),
|
||||||
|
danger_foreground: hsl(0.0, 0.0, 78.0),
|
||||||
|
danger_hover: hsl(0.0, 62.8, 35.6),
|
||||||
|
drag_border: blue(),
|
||||||
|
drop_target: hsl(235.0, 30., 44.0).opacity(0.1),
|
||||||
|
foreground: hsl(0., 0., 78.),
|
||||||
|
input: hsl(240.0, 3.7, 15.9),
|
||||||
|
link: hsl(221.0, 83.0, 53.0),
|
||||||
|
link_active: hsl(221.0, 83.0, 53.0).darken(0.2),
|
||||||
|
link_hover: hsl(221.0, 83.0, 53.0).lighten(0.2),
|
||||||
|
list: hsl(0.0, 0.0, 8.0),
|
||||||
|
list_active: hsl(240.0, 3.7, 15.0).opacity(0.2),
|
||||||
|
list_active_border: hsl(240.0, 5.9, 35.5),
|
||||||
|
list_even: hsl(240.0, 3.7, 10.0),
|
||||||
|
list_head: hsl(0.0, 0.0, 8.0),
|
||||||
|
list_hover: hsl(240.0, 3.7, 15.9),
|
||||||
|
muted: hsl(240.0, 3.7, 15.9),
|
||||||
|
muted_foreground: hsl(240.0, 5.0, 64.9),
|
||||||
|
popover: hsl(0.0, 0.0, 10.),
|
||||||
|
popover_foreground: hsl(0.0, 0.0, 78.0),
|
||||||
|
primary: hsl(223.0, 0.0, 98.0),
|
||||||
|
primary_active: hsl(223.0, 0.0, 80.0),
|
||||||
|
primary_foreground: hsl(223.0, 5.9, 10.0),
|
||||||
|
primary_hover: hsl(223.0, 0.0, 90.0),
|
||||||
|
progress_bar: hsl(223.0, 0.0, 98.0),
|
||||||
|
ring: hsl(240.0, 4.9, 83.9),
|
||||||
|
scrollbar: hsl(240., 1., 15.).opacity(0.75),
|
||||||
|
scrollbar_thumb: hsl(0., 0., 48.).opacity(0.9),
|
||||||
|
scrollbar_thumb_hover: hsl(0., 0., 68.),
|
||||||
|
secondary: hsl(240.0, 0., 13.0),
|
||||||
|
secondary_active: hsl(240.0, 0., 10.),
|
||||||
|
secondary_foreground: hsl(0.0, 0.0, 78.0),
|
||||||
|
secondary_hover: hsl(240.0, 0., 15.),
|
||||||
|
selection: hsl(211.0, 97.0, 22.0),
|
||||||
|
skeleton: hsla(223.0, 0.0, 98.0, 0.1),
|
||||||
|
slider_bar: hsl(223.0, 0.0, 98.0),
|
||||||
|
slider_thumb: hsl(0.0, 0.0, 8.0),
|
||||||
|
tab: transparent_black(),
|
||||||
|
tab_active: hsl(0.0, 0.0, 8.0),
|
||||||
|
tab_active_foreground: hsl(0., 0., 78.),
|
||||||
|
tab_bar: hsl(299.0, 0., 5.5),
|
||||||
|
tab_foreground: hsl(0., 0., 78.),
|
||||||
|
title_bar: hsl(240.0, 0.0, 10.0),
|
||||||
|
title_bar_border: hsl(240.0, 3.7, 15.9),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V> ActiveTheme for ViewContext<'_, V> {
|
|
||||||
fn theme(&self) -> &Theme {
|
|
||||||
self.deref().theme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V> ActiveTheme for ModelContext<'_, V> {
|
|
||||||
fn theme(&self) -> &Theme {
|
|
||||||
self.deref().theme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveTheme for WindowContext<'_> {
|
|
||||||
fn theme(&self) -> &Theme {
|
|
||||||
self.deref().theme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Make a [gpui::Hsla] color.
|
|
||||||
///
|
|
||||||
/// - h: 0..360.0
|
|
||||||
/// - s: 0.0..100.0
|
|
||||||
/// - l: 0.0..100.0
|
|
||||||
pub fn hsl(h: f32, s: f32, l: f32) -> Hsla {
|
|
||||||
hsla(h / 360., s / 100.0, l / 100.0, 1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Make a BoxShadow like CSS
|
|
||||||
///
|
|
||||||
/// e.g:
|
|
||||||
///
|
|
||||||
/// If CSS is `box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);`
|
|
||||||
///
|
|
||||||
/// Then the equivalent in Rust is `box_shadow(0., 0., 10., 0., hsla(0., 0., 0., 0.1))`
|
|
||||||
pub fn box_shadow(
|
|
||||||
x: impl Into<Pixels>,
|
|
||||||
y: impl Into<Pixels>,
|
|
||||||
blur: impl Into<Pixels>,
|
|
||||||
spread: impl Into<Pixels>,
|
|
||||||
color: Hsla,
|
|
||||||
) -> BoxShadow {
|
|
||||||
BoxShadow {
|
|
||||||
offset: point(x.into(), y.into()),
|
|
||||||
blur_radius: blur.into(),
|
|
||||||
spread_radius: spread.into(),
|
|
||||||
color,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub trait Colorize {
|
pub trait Colorize {
|
||||||
fn opacity(&self, opacity: f32) -> Hsla;
|
fn opacity(&self, opacity: f32) -> Hsla;
|
||||||
fn divide(&self, divisor: f32) -> Hsla;
|
fn divide(&self, divisor: f32) -> Hsla;
|
||||||
@@ -145,206 +258,42 @@ impl Colorize for Hsla {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
|
||||||
pub struct ThemeColor {
|
pub trait ActiveTheme {
|
||||||
pub accent: Hsla,
|
fn theme(&self) -> &Theme;
|
||||||
pub accent_foreground: Hsla,
|
|
||||||
pub background: Hsla,
|
|
||||||
pub border: Hsla,
|
|
||||||
pub window_border: Hsla,
|
|
||||||
pub card: Hsla,
|
|
||||||
pub card_foreground: Hsla,
|
|
||||||
pub destructive: Hsla,
|
|
||||||
pub destructive_active: Hsla,
|
|
||||||
pub destructive_foreground: Hsla,
|
|
||||||
pub destructive_hover: Hsla,
|
|
||||||
pub drag_border: Hsla,
|
|
||||||
pub drop_target: Hsla,
|
|
||||||
pub foreground: Hsla,
|
|
||||||
pub input: Hsla,
|
|
||||||
pub link: Hsla,
|
|
||||||
pub link_active: Hsla,
|
|
||||||
pub link_hover: Hsla,
|
|
||||||
pub list: Hsla,
|
|
||||||
pub list_active: Hsla,
|
|
||||||
pub list_active_border: Hsla,
|
|
||||||
pub list_even: Hsla,
|
|
||||||
pub list_head: Hsla,
|
|
||||||
pub list_hover: Hsla,
|
|
||||||
pub muted: Hsla,
|
|
||||||
pub muted_foreground: Hsla,
|
|
||||||
pub panel: Hsla,
|
|
||||||
pub popover: Hsla,
|
|
||||||
pub popover_foreground: Hsla,
|
|
||||||
pub primary: Hsla,
|
|
||||||
pub primary_active: Hsla,
|
|
||||||
pub primary_foreground: Hsla,
|
|
||||||
pub primary_hover: Hsla,
|
|
||||||
pub progress_bar: Hsla,
|
|
||||||
pub ring: Hsla,
|
|
||||||
pub scrollbar: Hsla,
|
|
||||||
pub scrollbar_thumb: Hsla,
|
|
||||||
pub scrollbar_thumb_hover: Hsla,
|
|
||||||
pub secondary: Hsla,
|
|
||||||
pub secondary_active: Hsla,
|
|
||||||
pub secondary_foreground: Hsla,
|
|
||||||
pub secondary_hover: Hsla,
|
|
||||||
pub selection: Hsla,
|
|
||||||
pub skeleton: Hsla,
|
|
||||||
pub slider_bar: Hsla,
|
|
||||||
pub slider_thumb: Hsla,
|
|
||||||
pub tab: Hsla,
|
|
||||||
pub tab_active: Hsla,
|
|
||||||
pub tab_active_foreground: Hsla,
|
|
||||||
pub tab_bar: Hsla,
|
|
||||||
pub tab_foreground: Hsla,
|
|
||||||
pub title_bar: Hsla,
|
|
||||||
pub title_bar_border: Hsla,
|
|
||||||
pub sidebar: Hsla,
|
|
||||||
pub sidebar_accent: Hsla,
|
|
||||||
pub sidebar_accent_foreground: Hsla,
|
|
||||||
pub sidebar_border: Hsla,
|
|
||||||
pub sidebar_foreground: Hsla,
|
|
||||||
pub sidebar_primary: Hsla,
|
|
||||||
pub sidebar_primary_foreground: Hsla,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThemeColor {
|
impl ActiveTheme for AppContext {
|
||||||
pub fn light() -> Self {
|
fn theme(&self) -> &Theme {
|
||||||
Self {
|
Theme::global(self)
|
||||||
accent: hsl(240.0, 5.0, 96.0),
|
|
||||||
accent_foreground: hsl(240.0, 5.9, 10.0),
|
|
||||||
background: hsl(0.0, 0.0, 100.),
|
|
||||||
border: hsl(240.0, 5.9, 90.0),
|
|
||||||
window_border: hsl(240.0, 5.9, 78.0),
|
|
||||||
card: hsl(0.0, 0.0, 100.0),
|
|
||||||
card_foreground: hsl(240.0, 10.0, 3.9),
|
|
||||||
destructive: hsl(0.0, 84.2, 60.2),
|
|
||||||
destructive_active: hsl(0.0, 84.2, 47.0),
|
|
||||||
destructive_foreground: hsl(0.0, 0.0, 98.0),
|
|
||||||
destructive_hover: hsl(0.0, 84.2, 65.0),
|
|
||||||
drag_border: crate::blue_500(),
|
|
||||||
drop_target: hsl(235.0, 30., 44.0).opacity(0.25),
|
|
||||||
foreground: hsl(240.0, 10., 3.9),
|
|
||||||
input: hsl(240.0, 5.9, 90.0),
|
|
||||||
link: hsl(221.0, 83.0, 53.0),
|
|
||||||
link_active: hsl(221.0, 83.0, 53.0).darken(0.2),
|
|
||||||
link_hover: hsl(221.0, 83.0, 53.0).lighten(0.2),
|
|
||||||
list: hsl(0.0, 0.0, 100.),
|
|
||||||
list_active: hsl(211.0, 97.0, 85.0).opacity(0.2),
|
|
||||||
list_active_border: hsl(211.0, 97.0, 85.0),
|
|
||||||
list_even: hsl(240.0, 5.0, 96.0),
|
|
||||||
list_head: hsl(0.0, 0.0, 100.),
|
|
||||||
list_hover: hsl(240.0, 4.8, 95.0),
|
|
||||||
muted: hsl(240.0, 4.8, 95.9),
|
|
||||||
muted_foreground: hsl(240.0, 3.8, 46.1),
|
|
||||||
panel: hsl(0.0, 0.0, 100.0),
|
|
||||||
popover: hsl(0.0, 0.0, 100.0),
|
|
||||||
popover_foreground: hsl(240.0, 10.0, 3.9),
|
|
||||||
primary: hsl(223.0, 5.9, 10.0),
|
|
||||||
primary_active: hsl(223.0, 1.9, 25.0),
|
|
||||||
primary_foreground: hsl(223.0, 0.0, 98.0),
|
|
||||||
primary_hover: hsl(223.0, 5.9, 15.0),
|
|
||||||
progress_bar: hsl(223.0, 5.9, 10.0),
|
|
||||||
ring: hsl(240.0, 5.9, 65.0),
|
|
||||||
scrollbar: hsl(0., 0., 97.).opacity(0.75),
|
|
||||||
scrollbar_thumb: hsl(0., 0., 69.).opacity(0.9),
|
|
||||||
scrollbar_thumb_hover: hsl(0., 0., 59.),
|
|
||||||
secondary: hsl(240.0, 5.9, 96.9),
|
|
||||||
secondary_active: hsl(240.0, 5.9, 90.),
|
|
||||||
secondary_foreground: hsl(240.0, 59.0, 10.),
|
|
||||||
secondary_hover: hsl(240.0, 5.9, 98.),
|
|
||||||
selection: hsl(211.0, 97.0, 85.0),
|
|
||||||
skeleton: hsl(223.0, 5.9, 10.0).opacity(0.1),
|
|
||||||
slider_bar: hsl(223.0, 5.9, 10.0),
|
|
||||||
slider_thumb: hsl(0.0, 0.0, 100.0),
|
|
||||||
tab: gpui::transparent_black(),
|
|
||||||
tab_active: hsl(0.0, 0.0, 100.0),
|
|
||||||
tab_active_foreground: hsl(240.0, 10., 3.9),
|
|
||||||
tab_bar: hsl(240.0, 4.8, 95.9),
|
|
||||||
tab_foreground: hsl(240.0, 10., 3.9),
|
|
||||||
title_bar: hsl(0.0, 0.0, 98.0),
|
|
||||||
title_bar_border: hsl(220.0, 13.0, 91.0),
|
|
||||||
sidebar: hsl(0.0, 0.0, 98.0),
|
|
||||||
sidebar_accent: hsl(240.0, 4.8, 92.),
|
|
||||||
sidebar_accent_foreground: hsl(240.0, 5.9, 10.0),
|
|
||||||
sidebar_border: hsl(220.0, 13.0, 91.0),
|
|
||||||
sidebar_foreground: hsl(240.0, 5.3, 26.1),
|
|
||||||
sidebar_primary: hsl(240.0, 5.9, 10.0),
|
|
||||||
sidebar_primary_foreground: hsl(0.0, 0.0, 98.0),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn dark() -> Self {
|
impl<V> ActiveTheme for ViewContext<'_, V> {
|
||||||
Self {
|
fn theme(&self) -> &Theme {
|
||||||
accent: hsl(240.0, 3.7, 15.9),
|
self.deref().theme()
|
||||||
accent_foreground: hsl(0.0, 0.0, 78.0),
|
|
||||||
background: hsl(0.0, 0.0, 8.0),
|
|
||||||
border: hsl(240.0, 3.7, 16.9),
|
|
||||||
window_border: hsl(240.0, 3.7, 28.0),
|
|
||||||
card: hsl(0.0, 0.0, 8.0),
|
|
||||||
card_foreground: hsl(0.0, 0.0, 78.0),
|
|
||||||
destructive: hsl(0.0, 62.8, 30.6),
|
|
||||||
destructive_active: hsl(0.0, 62.8, 20.6),
|
|
||||||
destructive_foreground: hsl(0.0, 0.0, 78.0),
|
|
||||||
destructive_hover: hsl(0.0, 62.8, 35.6),
|
|
||||||
drag_border: crate::blue_500(),
|
|
||||||
drop_target: hsl(235.0, 30., 44.0).opacity(0.1),
|
|
||||||
foreground: hsl(0., 0., 78.),
|
|
||||||
input: hsl(240.0, 3.7, 15.9),
|
|
||||||
link: hsl(221.0, 83.0, 53.0),
|
|
||||||
link_active: hsl(221.0, 83.0, 53.0).darken(0.2),
|
|
||||||
link_hover: hsl(221.0, 83.0, 53.0).lighten(0.2),
|
|
||||||
list: hsl(0.0, 0.0, 8.0),
|
|
||||||
list_active: hsl(240.0, 3.7, 15.0).opacity(0.2),
|
|
||||||
list_active_border: hsl(240.0, 5.9, 35.5),
|
|
||||||
list_even: hsl(240.0, 3.7, 10.0),
|
|
||||||
list_head: hsl(0.0, 0.0, 8.0),
|
|
||||||
list_hover: hsl(240.0, 3.7, 15.9),
|
|
||||||
muted: hsl(240.0, 3.7, 15.9),
|
|
||||||
muted_foreground: hsl(240.0, 5.0, 64.9),
|
|
||||||
panel: hsl(299.0, 2., 11.),
|
|
||||||
popover: hsl(0.0, 0.0, 10.),
|
|
||||||
popover_foreground: hsl(0.0, 0.0, 78.0),
|
|
||||||
primary: hsl(223.0, 0.0, 98.0),
|
|
||||||
primary_active: hsl(223.0, 0.0, 80.0),
|
|
||||||
primary_foreground: hsl(223.0, 5.9, 10.0),
|
|
||||||
primary_hover: hsl(223.0, 0.0, 90.0),
|
|
||||||
progress_bar: hsl(223.0, 0.0, 98.0),
|
|
||||||
ring: hsl(240.0, 4.9, 83.9),
|
|
||||||
scrollbar: hsl(240., 1., 15.).opacity(0.75),
|
|
||||||
scrollbar_thumb: hsl(0., 0., 48.).opacity(0.9),
|
|
||||||
scrollbar_thumb_hover: hsl(0., 0., 68.),
|
|
||||||
secondary: hsl(240.0, 0., 13.0),
|
|
||||||
secondary_active: hsl(240.0, 0., 10.),
|
|
||||||
secondary_foreground: hsl(0.0, 0.0, 78.0),
|
|
||||||
secondary_hover: hsl(240.0, 0., 15.),
|
|
||||||
selection: hsl(211.0, 97.0, 22.0),
|
|
||||||
skeleton: hsla(223.0, 0.0, 98.0, 0.1),
|
|
||||||
slider_bar: hsl(223.0, 0.0, 98.0),
|
|
||||||
slider_thumb: hsl(0.0, 0.0, 8.0),
|
|
||||||
tab: gpui::transparent_black(),
|
|
||||||
tab_active: hsl(0.0, 0.0, 8.0),
|
|
||||||
tab_active_foreground: hsl(0., 0., 78.),
|
|
||||||
tab_bar: hsl(299.0, 0., 5.5),
|
|
||||||
tab_foreground: hsl(0., 0., 78.),
|
|
||||||
title_bar: hsl(240.0, 0.0, 10.0),
|
|
||||||
title_bar_border: hsl(240.0, 3.7, 15.9),
|
|
||||||
sidebar: hsl(240.0, 0.0, 10.0),
|
|
||||||
sidebar_accent: hsl(240.0, 3.7, 15.9),
|
|
||||||
sidebar_accent_foreground: hsl(240.0, 4.8, 95.9),
|
|
||||||
sidebar_border: hsl(240.0, 3.7, 15.9),
|
|
||||||
sidebar_foreground: hsl(240.0, 4.8, 95.9),
|
|
||||||
sidebar_primary: hsl(0.0, 0.0, 98.0),
|
|
||||||
sidebar_primary_foreground: hsl(240.0, 5.9, 10.0),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<V> ActiveTheme for ModelContext<'_, V> {
|
||||||
|
fn theme(&self) -> &Theme {
|
||||||
|
self.deref().theme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveTheme for WindowContext<'_> {
|
||||||
|
fn theme(&self) -> &Theme {
|
||||||
|
self.deref().theme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
Theme::sync_system_appearance(cx)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Theme {
|
pub struct Theme {
|
||||||
colors: ThemeColor,
|
pub colors: ThemeColors,
|
||||||
|
|
||||||
pub mode: ThemeMode,
|
pub mode: ThemeMode,
|
||||||
pub font_family: SharedString,
|
pub font_family: SharedString,
|
||||||
pub font_size: f32,
|
pub font_size: f32,
|
||||||
@@ -356,7 +305,7 @@ pub struct Theme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for Theme {
|
impl Deref for Theme {
|
||||||
type Target = ThemeColor;
|
type Target = ThemeColors;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.colors
|
&self.colors
|
||||||
@@ -410,7 +359,6 @@ impl Theme {
|
|||||||
self.scrollbar = self.scrollbar.apply(mask_color);
|
self.scrollbar = self.scrollbar.apply(mask_color);
|
||||||
self.scrollbar_thumb = self.scrollbar_thumb.apply(mask_color);
|
self.scrollbar_thumb = self.scrollbar_thumb.apply(mask_color);
|
||||||
self.scrollbar_thumb_hover = self.scrollbar_thumb_hover.apply(mask_color);
|
self.scrollbar_thumb_hover = self.scrollbar_thumb_hover.apply(mask_color);
|
||||||
self.panel = self.panel.apply(mask_color);
|
|
||||||
self.drag_border = self.drag_border.apply(mask_color);
|
self.drag_border = self.drag_border.apply(mask_color);
|
||||||
self.drop_target = self.drop_target.apply(mask_color);
|
self.drop_target = self.drop_target.apply(mask_color);
|
||||||
self.tab_bar = self.tab_bar.apply(mask_color);
|
self.tab_bar = self.tab_bar.apply(mask_color);
|
||||||
@@ -433,13 +381,6 @@ impl Theme {
|
|||||||
self.skeleton = self.skeleton.apply(mask_color);
|
self.skeleton = self.skeleton.apply(mask_color);
|
||||||
self.title_bar = self.title_bar.apply(mask_color);
|
self.title_bar = self.title_bar.apply(mask_color);
|
||||||
self.title_bar_border = self.title_bar_border.apply(mask_color);
|
self.title_bar_border = self.title_bar_border.apply(mask_color);
|
||||||
self.sidebar = self.sidebar.apply(mask_color);
|
|
||||||
self.sidebar_accent = self.sidebar_accent.apply(mask_color);
|
|
||||||
self.sidebar_accent_foreground = self.sidebar_accent_foreground.apply(mask_color);
|
|
||||||
self.sidebar_border = self.sidebar_border.apply(mask_color);
|
|
||||||
self.sidebar_foreground = self.sidebar_foreground.apply(mask_color);
|
|
||||||
self.sidebar_primary = self.sidebar_primary.apply(mask_color);
|
|
||||||
self.sidebar_primary_foreground = self.sidebar_primary_foreground.apply(mask_color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync the theme with the system appearance
|
/// Sync the theme with the system appearance
|
||||||
@@ -456,8 +397,8 @@ impl Theme {
|
|||||||
|
|
||||||
pub fn change(mode: ThemeMode, cx: &mut AppContext) {
|
pub fn change(mode: ThemeMode, cx: &mut AppContext) {
|
||||||
let colors = match mode {
|
let colors = match mode {
|
||||||
ThemeMode::Light => ThemeColor::light(),
|
ThemeMode::Light => ThemeColors::light(),
|
||||||
ThemeMode::Dark => ThemeColor::dark(),
|
ThemeMode::Dark => ThemeColors::dark(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut theme = Theme::from(colors);
|
let mut theme = Theme::from(colors);
|
||||||
@@ -468,8 +409,8 @@ impl Theme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ThemeColor> for Theme {
|
impl From<ThemeColors> for Theme {
|
||||||
fn from(colors: ThemeColor) -> Self {
|
fn from(colors: ThemeColors) -> Self {
|
||||||
Theme {
|
Theme {
|
||||||
mode: ThemeMode::default(),
|
mode: ThemeMode::default(),
|
||||||
transparent: Hsla::transparent_black(),
|
transparent: Hsla::transparent_black(),
|
||||||
@@ -481,7 +422,7 @@ impl From<ThemeColor> for Theme {
|
|||||||
} else {
|
} else {
|
||||||
"FreeMono".into()
|
"FreeMono".into()
|
||||||
},
|
},
|
||||||
radius: 5.0,
|
radius: 6.0,
|
||||||
shadow: false,
|
shadow: false,
|
||||||
scrollbar_show: ScrollbarShow::default(),
|
scrollbar_show: ScrollbarShow::default(),
|
||||||
colors,
|
colors,
|
||||||
@@ -491,8 +432,8 @@ impl From<ThemeColor> for Theme {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd, Eq)]
|
#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd, Eq)]
|
||||||
pub enum ThemeMode {
|
pub enum ThemeMode {
|
||||||
Light,
|
|
||||||
#[default]
|
#[default]
|
||||||
|
Light,
|
||||||
Dark,
|
Dark,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,28 +442,3 @@ impl ThemeMode {
|
|||||||
matches!(self, Self::Dark)
|
matches!(self, Self::Dark)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::theme::Colorize as _;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_lighten() {
|
|
||||||
let color = super::hsl(240.0, 5.0, 30.0);
|
|
||||||
let color = color.lighten(0.5);
|
|
||||||
assert_eq!(color.l, 0.45000002);
|
|
||||||
let color = color.lighten(0.5);
|
|
||||||
assert_eq!(color.l, 0.675);
|
|
||||||
let color = color.lighten(0.1);
|
|
||||||
assert_eq!(color.l, 0.7425);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_darken() {
|
|
||||||
let color = super::hsl(240.0, 5.0, 96.0);
|
|
||||||
let color = color.darken(0.5);
|
|
||||||
assert_eq!(color.l, 0.48);
|
|
||||||
let color = color.darken(0.5);
|
|
||||||
assert_eq!(color.l, 0.24);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
295
crates/ui/src/theme/scale.rs
Normal file
295
crates/ui/src/theme/scale.rs
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
use crate::theme::{ActiveTheme, ThemeMode};
|
||||||
|
use gpui::{AppContext, Hsla, SharedString};
|
||||||
|
|
||||||
|
/// A collection of colors that are used to style the UI.
|
||||||
|
///
|
||||||
|
/// Each step has a semantic meaning, and is used to style different parts of the UI.
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||||
|
pub struct ColorScaleStep(usize);
|
||||||
|
|
||||||
|
impl ColorScaleStep {
|
||||||
|
pub const ONE: Self = Self(1);
|
||||||
|
pub const TWO: Self = Self(2);
|
||||||
|
pub const THREE: Self = Self(3);
|
||||||
|
pub const FOUR: Self = Self(4);
|
||||||
|
pub const FIVE: Self = Self(5);
|
||||||
|
pub const SIX: Self = Self(6);
|
||||||
|
pub const SEVEN: Self = Self(7);
|
||||||
|
pub const EIGHT: Self = Self(8);
|
||||||
|
pub const NINE: Self = Self(9);
|
||||||
|
pub const TEN: Self = Self(10);
|
||||||
|
pub const ELEVEN: Self = Self(11);
|
||||||
|
pub const TWELVE: Self = Self(12);
|
||||||
|
|
||||||
|
/// All of the steps in a [`ColorScale`].
|
||||||
|
pub const ALL: [ColorScaleStep; 12] = [
|
||||||
|
Self::ONE,
|
||||||
|
Self::TWO,
|
||||||
|
Self::THREE,
|
||||||
|
Self::FOUR,
|
||||||
|
Self::FIVE,
|
||||||
|
Self::SIX,
|
||||||
|
Self::SEVEN,
|
||||||
|
Self::EIGHT,
|
||||||
|
Self::NINE,
|
||||||
|
Self::TEN,
|
||||||
|
Self::ELEVEN,
|
||||||
|
Self::TWELVE,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A scale of colors for a given [`ColorScaleSet`].
|
||||||
|
///
|
||||||
|
/// Each [`ColorScale`] contains exactly 12 colors. Refer to
|
||||||
|
/// [`ColorScaleStep`] for a reference of what each step is used for.
|
||||||
|
pub struct ColorScale(Vec<Hsla>);
|
||||||
|
|
||||||
|
impl FromIterator<Hsla> for ColorScale {
|
||||||
|
fn from_iter<T: IntoIterator<Item = Hsla>>(iter: T) -> Self {
|
||||||
|
Self(Vec::from_iter(iter))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorScale {
|
||||||
|
/// Returns the specified step in the [`ColorScale`].
|
||||||
|
#[inline]
|
||||||
|
pub fn step(&self, step: ColorScaleStep) -> Hsla {
|
||||||
|
// Steps are one-based, so we need convert to the zero-based vec index.
|
||||||
|
self.0[step.0 - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Step 1` - Used for main application backgrounds.
|
||||||
|
///
|
||||||
|
/// This step provides a neutral base for any overlaying components, ideal for applications' main backdrop or empty spaces such as canvas areas.
|
||||||
|
///
|
||||||
|
#[inline]
|
||||||
|
pub fn step_1(&self) -> Hsla {
|
||||||
|
self.step(ColorScaleStep::ONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Step 2` - Used for both main application backgrounds and subtle component backgrounds.
|
||||||
|
///
|
||||||
|
/// Like `Step 1`, this step allows variations in background styles, from striped tables, sidebar backgrounds, to card backgrounds.
|
||||||
|
#[inline]
|
||||||
|
pub fn step_2(&self) -> Hsla {
|
||||||
|
self.step(ColorScaleStep::TWO)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Step 3` - Used for UI component backgrounds in their normal states.
|
||||||
|
///
|
||||||
|
/// This step maintains accessibility by guaranteeing a contrast ratio of 4.5:1 with steps 11 and 12 for text. It could also suit hover states for transparent components.
|
||||||
|
#[inline]
|
||||||
|
pub fn step_3(&self) -> Hsla {
|
||||||
|
self.step(ColorScaleStep::THREE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Step 4` - Used for UI component backgrounds in their hover states.
|
||||||
|
///
|
||||||
|
/// Also suited for pressed or selected states of components with a transparent background.
|
||||||
|
#[inline]
|
||||||
|
pub fn step_4(&self) -> Hsla {
|
||||||
|
self.step(ColorScaleStep::FOUR)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Step 5` - Used for UI component backgrounds in their pressed or selected states.
|
||||||
|
#[inline]
|
||||||
|
pub fn step_5(&self) -> Hsla {
|
||||||
|
self.step(ColorScaleStep::FIVE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Step 6` - Used for subtle borders on non-interactive components.
|
||||||
|
///
|
||||||
|
/// Its usage spans from sidebars' borders, headers' dividers, cards' outlines, to alerts' edges and separators.
|
||||||
|
#[inline]
|
||||||
|
pub fn step_6(&self) -> Hsla {
|
||||||
|
self.step(ColorScaleStep::SIX)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Step 7` - Used for subtle borders on interactive components.
|
||||||
|
///
|
||||||
|
/// This step subtly delineates the boundary of elements users interact with.
|
||||||
|
#[inline]
|
||||||
|
pub fn step_7(&self) -> Hsla {
|
||||||
|
self.step(ColorScaleStep::SEVEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Step 8` - Used for stronger borders on interactive components and focus rings.
|
||||||
|
///
|
||||||
|
/// It strengthens the visibility and accessibility of active elements and their focus states.
|
||||||
|
#[inline]
|
||||||
|
pub fn step_8(&self) -> Hsla {
|
||||||
|
self.step(ColorScaleStep::EIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Step 9` - Used for solid backgrounds.
|
||||||
|
///
|
||||||
|
/// `Step 9` is the most saturated step, having the least mix of white or black.
|
||||||
|
///
|
||||||
|
/// Due to its high chroma, `Step 9` is versatile and particularly useful for semantic colors such as
|
||||||
|
/// error, warning, and success indicators.
|
||||||
|
#[inline]
|
||||||
|
pub fn step_9(&self) -> Hsla {
|
||||||
|
self.step(ColorScaleStep::NINE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Step 10` - Used for hovered or active solid backgrounds, particularly when `Step 9` is their normal state.
|
||||||
|
///
|
||||||
|
/// May also be used for extremely low contrast text. This should be used sparingly, as it may be difficult to read.
|
||||||
|
#[inline]
|
||||||
|
pub fn step_10(&self) -> Hsla {
|
||||||
|
self.step(ColorScaleStep::TEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Step 11` - Used for text and icons requiring low contrast or less emphasis.
|
||||||
|
#[inline]
|
||||||
|
pub fn step_11(&self) -> Hsla {
|
||||||
|
self.step(ColorScaleStep::ELEVEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Step 12` - Used for text and icons requiring high contrast or prominence.
|
||||||
|
#[inline]
|
||||||
|
pub fn step_12(&self) -> Hsla {
|
||||||
|
self.step(ColorScaleStep::TWELVE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ColorScales {
|
||||||
|
pub gray: ColorScaleSet,
|
||||||
|
pub mauve: ColorScaleSet,
|
||||||
|
pub slate: ColorScaleSet,
|
||||||
|
pub sage: ColorScaleSet,
|
||||||
|
pub olive: ColorScaleSet,
|
||||||
|
pub sand: ColorScaleSet,
|
||||||
|
pub gold: ColorScaleSet,
|
||||||
|
pub bronze: ColorScaleSet,
|
||||||
|
pub brown: ColorScaleSet,
|
||||||
|
pub yellow: ColorScaleSet,
|
||||||
|
pub amber: ColorScaleSet,
|
||||||
|
pub orange: ColorScaleSet,
|
||||||
|
pub tomato: ColorScaleSet,
|
||||||
|
pub red: ColorScaleSet,
|
||||||
|
pub ruby: ColorScaleSet,
|
||||||
|
pub crimson: ColorScaleSet,
|
||||||
|
pub pink: ColorScaleSet,
|
||||||
|
pub plum: ColorScaleSet,
|
||||||
|
pub purple: ColorScaleSet,
|
||||||
|
pub violet: ColorScaleSet,
|
||||||
|
pub iris: ColorScaleSet,
|
||||||
|
pub indigo: ColorScaleSet,
|
||||||
|
pub blue: ColorScaleSet,
|
||||||
|
pub cyan: ColorScaleSet,
|
||||||
|
pub teal: ColorScaleSet,
|
||||||
|
pub jade: ColorScaleSet,
|
||||||
|
pub green: ColorScaleSet,
|
||||||
|
pub grass: ColorScaleSet,
|
||||||
|
pub lime: ColorScaleSet,
|
||||||
|
pub mint: ColorScaleSet,
|
||||||
|
pub sky: ColorScaleSet,
|
||||||
|
pub black: ColorScaleSet,
|
||||||
|
pub white: ColorScaleSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for ColorScales {
|
||||||
|
type Item = ColorScaleSet;
|
||||||
|
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
vec![
|
||||||
|
self.gray,
|
||||||
|
self.mauve,
|
||||||
|
self.slate,
|
||||||
|
self.sage,
|
||||||
|
self.olive,
|
||||||
|
self.sand,
|
||||||
|
self.gold,
|
||||||
|
self.bronze,
|
||||||
|
self.brown,
|
||||||
|
self.yellow,
|
||||||
|
self.amber,
|
||||||
|
self.orange,
|
||||||
|
self.tomato,
|
||||||
|
self.red,
|
||||||
|
self.ruby,
|
||||||
|
self.crimson,
|
||||||
|
self.pink,
|
||||||
|
self.plum,
|
||||||
|
self.purple,
|
||||||
|
self.violet,
|
||||||
|
self.iris,
|
||||||
|
self.indigo,
|
||||||
|
self.blue,
|
||||||
|
self.cyan,
|
||||||
|
self.teal,
|
||||||
|
self.jade,
|
||||||
|
self.green,
|
||||||
|
self.grass,
|
||||||
|
self.lime,
|
||||||
|
self.mint,
|
||||||
|
self.sky,
|
||||||
|
self.black,
|
||||||
|
self.white,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provides groups of [`ColorScale`]s for light and dark themes, as well as transparent versions of each scale.
|
||||||
|
pub struct ColorScaleSet {
|
||||||
|
name: SharedString,
|
||||||
|
light: ColorScale,
|
||||||
|
dark: ColorScale,
|
||||||
|
light_alpha: ColorScale,
|
||||||
|
dark_alpha: ColorScale,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorScaleSet {
|
||||||
|
pub fn new(
|
||||||
|
name: impl Into<SharedString>,
|
||||||
|
light: ColorScale,
|
||||||
|
light_alpha: ColorScale,
|
||||||
|
dark: ColorScale,
|
||||||
|
dark_alpha: ColorScale,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
light,
|
||||||
|
light_alpha,
|
||||||
|
dark,
|
||||||
|
dark_alpha,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &SharedString {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn light(&self) -> &ColorScale {
|
||||||
|
&self.light
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn light_alpha(&self) -> &ColorScale {
|
||||||
|
&self.light_alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dark(&self) -> &ColorScale {
|
||||||
|
&self.dark
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dark_alpha(&self) -> &ColorScale {
|
||||||
|
&self.dark_alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step(&self, cx: &AppContext, step: ColorScaleStep) -> Hsla {
|
||||||
|
match cx.theme().mode {
|
||||||
|
ThemeMode::Light => self.light().step(step),
|
||||||
|
ThemeMode::Dark => self.dark().step(step),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step_alpha(&self, cx: &AppContext, step: ColorScaleStep) -> Hsla {
|
||||||
|
match cx.theme().mode {
|
||||||
|
ThemeMode::Light => self.light_alpha.step(step),
|
||||||
|
ThemeMode::Dark => self.dark_alpha.step(step),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
|
use crate::{h_flex, theme::ActiveTheme, Icon, IconName, InteractiveElementExt as _, Sizable as _};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, prelude::FluentBuilder as _, px, relative, AnyElement, ClickEvent, Div, Element, Hsla,
|
black, div, prelude::FluentBuilder as _, px, relative, white, AnyElement, ClickEvent, Div,
|
||||||
InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce, Stateful,
|
Element, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels,
|
||||||
StatefulInteractiveElement as _, Style, Styled, WindowContext,
|
RenderOnce, Rgba, Stateful, StatefulInteractiveElement as _, Style, Styled, WindowContext,
|
||||||
};
|
};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::{h_flex, theme::ActiveTheme, Icon, IconName, InteractiveElementExt as _, Sizable as _};
|
|
||||||
|
|
||||||
pub const HEIGHT: Pixels = px(34.);
|
pub const HEIGHT: Pixels = px(34.);
|
||||||
pub const TITLE_BAR_HEIGHT: Pixels = px(35.);
|
pub const TITLE_BAR_HEIGHT: Pixels = px(35.);
|
||||||
|
|
||||||
@@ -108,31 +107,42 @@ impl ControlIcon {
|
|||||||
|
|
||||||
fn fg(&self, cx: &WindowContext) -> Hsla {
|
fn fg(&self, cx: &WindowContext) -> Hsla {
|
||||||
if cx.theme().mode.is_dark() {
|
if cx.theme().mode.is_dark() {
|
||||||
crate::white()
|
white()
|
||||||
} else {
|
} else {
|
||||||
crate::black()
|
black()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hover_fg(&self, cx: &WindowContext) -> Hsla {
|
fn hover_fg(&self, cx: &WindowContext) -> Hsla {
|
||||||
if self.is_close() || cx.theme().mode.is_dark() {
|
if self.is_close() || cx.theme().mode.is_dark() {
|
||||||
crate::white()
|
white()
|
||||||
} else {
|
} else {
|
||||||
crate::black()
|
black()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hover_bg(&self, cx: &WindowContext) -> Hsla {
|
fn hover_bg(&self, cx: &WindowContext) -> Rgba {
|
||||||
if self.is_close() {
|
if self.is_close() {
|
||||||
if cx.theme().mode.is_dark() {
|
Rgba {
|
||||||
crate::red_800()
|
r: 232.0 / 255.0,
|
||||||
} else {
|
g: 17.0 / 255.0,
|
||||||
crate::red_600()
|
b: 32.0 / 255.0,
|
||||||
|
a: 1.0,
|
||||||
}
|
}
|
||||||
} else if cx.theme().mode.is_dark() {
|
} else if cx.theme().mode.is_dark() {
|
||||||
crate::stone_700()
|
Rgba {
|
||||||
|
r: 0.9,
|
||||||
|
g: 0.9,
|
||||||
|
b: 0.9,
|
||||||
|
a: 0.1,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
crate::stone_200()
|
Rgba {
|
||||||
|
r: 0.1,
|
||||||
|
g: 0.1,
|
||||||
|
b: 0.1,
|
||||||
|
a: 0.2,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,7 +188,7 @@ impl RenderOnce for ControlIcon {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.hover(|style| style.bg(hover_bg).text_color(hover_fg))
|
.hover(|style| style.bg(hover_bg).text_color(hover_fg))
|
||||||
.active(|style| style.bg(hover_bg.opacity(0.7)))
|
.active(|style| style.bg(hover_bg))
|
||||||
.child(Icon::new(self.icon()).small())
|
.child(Icon::new(self.icon()).small())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ impl Render for Tooltip {
|
|||||||
.m_3()
|
.m_3()
|
||||||
.bg(cx.theme().popover)
|
.bg(cx.theme().popover)
|
||||||
.text_color(cx.theme().popover_foreground)
|
.text_color(cx.theme().popover_foreground)
|
||||||
.bg(cx.theme().popover)
|
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.shadow_md()
|
.shadow_md()
|
||||||
|
|||||||
Reference in New Issue
Block a user