From ec24bba69ca476ed4795b9aefb5234f59f3030ef Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 15 Jan 2025 09:11:21 +0700 Subject: [PATCH] wip: design --- crates/app/src/views/app.rs | 2 +- crates/app/src/views/chat/message.rs | 19 +- crates/app/src/views/chat/mod.rs | 9 +- crates/app/src/views/onboarding/mod.rs | 7 +- crates/app/src/views/sidebar/contact_list.rs | 12 +- crates/app/src/views/sidebar/inbox.rs | 3 +- crates/app/src/views/sidebar/mod.rs | 5 +- crates/app/src/views/welcome.rs | 9 +- crates/ui/src/badge.rs | 123 --- crates/ui/src/button.rs | 93 +-- crates/ui/src/checkbox.rs | 34 +- crates/ui/src/divider.rs | 10 +- crates/ui/src/dock/tiles.rs | 702 ------------------ crates/ui/src/{dock => dock_area}/dock.rs | 35 +- .../src/{dock => dock_area}/invalid_panel.rs | 13 +- crates/ui/src/{dock => dock_area}/mod.rs | 169 ++--- crates/ui/src/{dock => dock_area}/panel.rs | 9 +- .../ui/src/{dock => dock_area}/stack_panel.rs | 27 +- crates/ui/src/{dock => dock_area}/state.rs | 15 +- .../ui/src/{dock => dock_area}/tab_panel.rs | 14 +- crates/ui/src/dropdown.rs | 30 +- crates/ui/src/icon.rs | 9 +- crates/ui/src/input/clear_button.rs | 10 +- crates/ui/src/input/element.rs | 15 +- crates/ui/src/input/input.rs | 255 +++++-- crates/ui/src/label.rs | 93 --- crates/ui/src/lib.rs | 6 +- crates/ui/src/list/list.rs | 29 +- crates/ui/src/list/list_item.rs | 32 +- crates/ui/src/modal.rs | 15 +- crates/ui/src/scroll/scrollbar.rs | 6 +- crates/ui/src/styled.rs | 2 +- crates/ui/src/switch.rs | 13 +- crates/ui/src/tab/mod.rs | 43 +- crates/ui/src/theme/mod.rs | 232 +----- 35 files changed, 534 insertions(+), 1566 deletions(-) delete mode 100644 crates/ui/src/badge.rs delete mode 100644 crates/ui/src/dock/tiles.rs rename crates/ui/src/{dock => dock_area}/dock.rs (95%) rename crates/ui/src/{dock => dock_area}/invalid_panel.rs (82%) rename crates/ui/src/{dock => dock_area}/mod.rs (84%) rename crates/ui/src/{dock => dock_area}/panel.rs (97%) rename crates/ui/src/{dock => dock_area}/stack_panel.rs (95%) rename crates/ui/src/{dock => dock_area}/state.rs (93%) rename crates/ui/src/{dock => dock_area}/tab_panel.rs (98%) delete mode 100644 crates/ui/src/label.rs diff --git a/crates/app/src/views/app.rs b/crates/app/src/views/app.rs index 88b60b8..51df303 100644 --- a/crates/app/src/views/app.rs +++ b/crates/app/src/views/app.rs @@ -10,7 +10,7 @@ use gpui::{ use serde::Deserialize; use std::sync::Arc; use ui::{ - dock::{DockArea, DockItem, DockPlacement}, + dock_area::{dock::DockPlacement, DockArea, DockItem}, indicator::Indicator, notification::NotificationType, theme::Theme, diff --git a/crates/app/src/views/chat/message.rs b/crates/app/src/views/chat/message.rs index c33dfe7..4569b4b 100644 --- a/crates/app/src/views/chat/message.rs +++ b/crates/app/src/views/chat/message.rs @@ -3,7 +3,10 @@ use gpui::{ div, img, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, Styled, WindowContext, }; -use ui::{theme::ActiveTheme, StyledExt}; +use ui::{ + theme::{scale::ColorScaleStep, ActiveTheme}, + StyledExt, +}; #[derive(Clone, Debug, IntoElement)] pub struct Message { @@ -32,9 +35,8 @@ impl RenderOnce for Message { .border_l_2() .border_color(cx.theme().background) .hover(|this| { - this.bg(cx.theme().muted) - .border_color(cx.theme().primary_active) - .text_color(cx.theme().muted_foreground) + this.bg(cx.theme().base.step(cx, ColorScaleStep::TWO)) + .border_color(cx.theme().accent.step(cx, ColorScaleStep::NINE)) }) .child( img(self.member.avatar()) @@ -58,15 +60,10 @@ impl RenderOnce for Message { .child( div() .child(self.ago) - .text_color(cx.theme().muted_foreground), + .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)), ), ) - .child( - div() - .text_sm() - .text_color(cx.theme().foreground) - .child(self.content), - ), + .child(div().text_sm().child(self.content)), ) } } diff --git a/crates/app/src/views/chat/mod.rs b/crates/app/src/views/chat/mod.rs index 6e5c9ad..229a7a6 100644 --- a/crates/app/src/views/chat/mod.rs +++ b/crates/app/src/views/chat/mod.rs @@ -15,10 +15,13 @@ use nostr_sdk::prelude::*; use std::sync::Arc; use ui::{ button::{Button, ButtonVariants}, - dock::{Panel, PanelEvent, PanelState}, + dock_area::{ + panel::{Panel, PanelEvent}, + state::PanelState, + }, input::{InputEvent, TextInput}, popup_menu::PopupMenu, - theme::ActiveTheme, + theme::{scale::ColorScaleStep, ActiveTheme}, v_flex, Icon, IconName, }; @@ -374,7 +377,7 @@ impl Render for ChatPanel { div() .flex_1() .flex() - .bg(cx.theme().muted) + .bg(cx.theme().base.step(cx, ColorScaleStep::FOUR)) .rounded(px(cx.theme().radius)) .px_2() .child(self.input.clone()), diff --git a/crates/app/src/views/onboarding/mod.rs b/crates/app/src/views/onboarding/mod.rs index 4fc0320..d330020 100644 --- a/crates/app/src/views/onboarding/mod.rs +++ b/crates/app/src/views/onboarding/mod.rs @@ -1,10 +1,7 @@ use crate::{constants::KEYRING_SERVICE, get_client, states::app::AppRegistry}; use gpui::{div, IntoElement, ParentElement, Render, Styled, View, ViewContext, VisualContext}; use nostr_sdk::prelude::*; -use ui::{ - input::{InputEvent, TextInput}, - label::Label, -}; +use ui::input::{InputEvent, TextInput}; pub struct Onboarding { input: View, @@ -70,7 +67,7 @@ impl Render for Onboarding { .flex() .flex_col() .gap_1() - .child(Label::new("Private Key").text_sm()) + .child(div().child("Private Key").text_sm()) .child(self.input.clone()), ) } diff --git a/crates/app/src/views/sidebar/contact_list.rs b/crates/app/src/views/sidebar/contact_list.rs index d6221fc..e70fa03 100644 --- a/crates/app/src/views/sidebar/contact_list.rs +++ b/crates/app/src/views/sidebar/contact_list.rs @@ -9,7 +9,7 @@ use serde::Deserialize; use std::collections::{BTreeSet, HashSet}; use ui::{ prelude::FluentBuilder, - theme::{ActiveTheme, Colorize}, + theme::{scale::ColorScaleStep, ActiveTheme}, Icon, IconName, Selectable, StyledExt, }; @@ -95,12 +95,12 @@ impl RenderOnce for ContactListItem { this.child( Icon::new(IconName::CircleCheck) .size_4() - .text_color(cx.theme().colors.primary), + .text_color(cx.theme().accent.step(cx, ColorScaleStep::NINE)), ) }) .hover(|this| { - this.bg(cx.theme().muted.darken(0.1)) - .text_color(cx.theme().muted_foreground.darken(0.1)) + this.bg(cx.theme().base.step(cx, ColorScaleStep::FOUR)) + .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) }) .on_click(move |_, cx| { cx.dispatch_action(Box::new(SelectContact(self.public_key))); @@ -239,8 +239,8 @@ impl Render for ContactList { .child( div() .p_1() - .bg(cx.theme().muted) - .text_color(cx.theme().muted_foreground) + .bg(cx.theme().base.step(cx, ColorScaleStep::THREE)) + .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) .rounded_lg() .child(list(self.list.clone()).h(px(300.))), ) diff --git a/crates/app/src/views/sidebar/inbox.rs b/crates/app/src/views/sidebar/inbox.rs index fe808dc..b7f416d 100644 --- a/crates/app/src/views/sidebar/inbox.rs +++ b/crates/app/src/views/sidebar/inbox.rs @@ -8,6 +8,7 @@ use gpui::{ ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, ViewContext, }; use ui::{ + dock_area::dock::DockPlacement, skeleton::Skeleton, theme::{scale::ColorScaleStep, ActiveTheme}, v_flex, Collapsible, Icon, IconName, StyledExt, @@ -106,7 +107,7 @@ impl Inbox { fn action(&self, id: u64, cx: &mut ViewContext) { cx.dispatch_action(Box::new(AddPanel { panel: PanelKind::Room(id), - position: ui::dock::DockPlacement::Center, + position: DockPlacement::Center, })) } } diff --git a/crates/app/src/views/sidebar/mod.rs b/crates/app/src/views/sidebar/mod.rs index 7a71fb3..ca546a2 100644 --- a/crates/app/src/views/sidebar/mod.rs +++ b/crates/app/src/views/sidebar/mod.rs @@ -7,7 +7,10 @@ use gpui::{ }; use ui::{ button::{Button, ButtonRounded, ButtonVariants}, - dock::{Panel, PanelEvent, PanelState}, + dock_area::{ + panel::{Panel, PanelEvent}, + state::PanelState, + }, popup_menu::PopupMenu, scroll::ScrollbarAxis, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt, diff --git a/crates/app/src/views/welcome.rs b/crates/app/src/views/welcome.rs index 74d74e1..84768c5 100644 --- a/crates/app/src/views/welcome.rs +++ b/crates/app/src/views/welcome.rs @@ -4,9 +4,12 @@ use gpui::{ }; use ui::{ button::Button, - dock::{Panel, PanelEvent, PanelState}, + dock_area::{ + panel::{Panel, PanelEvent}, + state::PanelState, + }, popup_menu::PopupMenu, - theme::{ActiveTheme, Colorize}, + theme::{scale::ColorScaleStep, ActiveTheme}, StyledExt, }; @@ -78,7 +81,7 @@ impl Render for WelcomePanel { .items_center() .justify_center() .child("coop on nostr.") - .text_color(cx.theme().muted.darken(0.1)) + .text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)) .font_black() .text_sm() } diff --git a/crates/ui/src/badge.rs b/crates/ui/src/badge.rs deleted file mode 100644 index 43a33f9..0000000 --- a/crates/ui/src/badge.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::{theme::ActiveTheme as _, Sizable, Size}; -use gpui::{ - div, prelude::FluentBuilder as _, relative, Div, Hsla, InteractiveElement as _, IntoElement, - ParentElement, RenderOnce, Styled, -}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum BadgeVariant { - #[default] - Primary, - Secondary, - Outline, - Destructive, - Custom { - color: Hsla, - foreground: Hsla, - border: Hsla, - }, -} -impl BadgeVariant { - fn bg(&self, cx: &gpui::WindowContext) -> Hsla { - match self { - Self::Primary => cx.theme().colors.primary, - Self::Secondary => cx.theme().secondary, - Self::Outline => gpui::transparent_black(), - Self::Destructive => cx.theme().danger, - Self::Custom { color, .. } => *color, - } - } - - fn border(&self, cx: &gpui::WindowContext) -> Hsla { - match self { - Self::Primary => cx.theme().colors.primary, - Self::Secondary => cx.theme().secondary, - Self::Outline => cx.theme().border, - Self::Destructive => cx.theme().danger, - Self::Custom { border, .. } => *border, - } - } - - fn fg(&self, cx: &gpui::WindowContext) -> Hsla { - match self { - Self::Primary => cx.theme().primary_foreground, - Self::Secondary => cx.theme().secondary_foreground, - Self::Outline => cx.theme().foreground, - Self::Destructive => cx.theme().danger_foreground, - Self::Custom { foreground, .. } => *foreground, - } - } -} - -/// Badge is a small status indicator for UI elements. -/// -/// Only support: Medium, Small -#[derive(IntoElement)] -pub struct Badge { - base: Div, - veriant: BadgeVariant, - size: Size, -} -impl Badge { - fn new() -> Self { - Self { - base: div().flex().items_center().rounded_md().border_1(), - veriant: BadgeVariant::default(), - size: Size::Medium, - } - } - - pub fn with_variant(mut self, variant: BadgeVariant) -> Self { - self.veriant = variant; - self - } - - pub fn primary() -> Self { - Self::new().with_variant(BadgeVariant::Primary) - } - - pub fn secondary() -> Self { - Self::new().with_variant(BadgeVariant::Secondary) - } - - pub fn outline() -> Self { - Self::new().with_variant(BadgeVariant::Outline) - } - - pub fn destructive() -> Self { - Self::new().with_variant(BadgeVariant::Destructive) - } - - pub fn custom(color: Hsla, foreground: Hsla, border: Hsla) -> Self { - Self::new().with_variant(BadgeVariant::Custom { - color, - foreground, - border, - }) - } -} -impl Sizable for Badge { - fn with_size(mut self, size: impl Into) -> Self { - self.size = size.into(); - self - } -} -impl ParentElement for Badge { - fn extend(&mut self, elements: impl IntoIterator) { - self.base.extend(elements); - } -} -impl RenderOnce for Badge { - fn render(self, cx: &mut gpui::WindowContext) -> impl IntoElement { - self.base - .line_height(relative(1.3)) - .map(|this| match self.size { - Size::XSmall | Size::Small => this.text_xs().px_1p5().py_0(), - _ => this.text_xs().px_2p5().py_0p5(), - }) - .bg(self.veriant.bg(cx)) - .text_color(self.veriant.fg(cx)) - .border_color(self.veriant.border(cx)) - .hover(|this| this.opacity(0.9)) - } -} diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index b91525f..132901f 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -1,6 +1,6 @@ use crate::{ indicator::Indicator, - theme::{scale::ColorScaleStep, ActiveTheme, Colorize as _}, + theme::{scale::ColorScaleStep, ActiveTheme}, tooltip::Tooltip, Disableable, Icon, Selectable, Sizable, Size, StyledExt, }; @@ -42,16 +42,6 @@ pub trait ButtonVariants: Sized { self.with_variant(ButtonVariant::Primary) } - /// With the danger style for the Button. - fn danger(self) -> Self { - self.with_variant(ButtonVariant::Danger) - } - - /// With the outline style for the Button. - fn outline(self) -> Self { - self.with_variant(ButtonVariant::Outline) - } - /// With the ghost style for the Button. fn ghost(self) -> Self { self.with_variant(ButtonVariant::Ghost) @@ -116,12 +106,10 @@ impl ButtonCustomVariant { } } -/// The veriant of the Button. +/// The variant of the Button. #[derive(Clone, Copy, PartialEq, Eq)] pub enum ButtonVariant { Primary, - Danger, - Outline, Ghost, Link, Text, @@ -406,9 +394,7 @@ impl RenderOnce for Button { .when(normal_style.underline, |this| this.text_decoration_1()) .hover(|this| { let hover_style = style.hovered(cx); - this.bg(hover_style.bg) - .border_color(hover_style.border) - .text_color(cx.theme().danger) + this.bg(hover_style.bg).border_color(hover_style.border) }) .active(|this| { let active_style = style.active(cx); @@ -494,7 +480,6 @@ impl ButtonVariant { fn bg_color(&self, cx: &WindowContext) -> Hsla { match self { ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::NINE), - ButtonVariant::Danger => cx.theme().danger, ButtonVariant::Custom(colors) => colors.color, _ => cx.theme().transparent, } @@ -511,9 +496,7 @@ impl ButtonVariant { fn border_color(&self, cx: &WindowContext) -> Hsla { match self { - ButtonVariant::Primary => cx.theme().colors.primary, - ButtonVariant::Danger => cx.theme().danger, - ButtonVariant::Outline => cx.theme().border, + ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::NINE), ButtonVariant::Ghost | ButtonVariant::Link | ButtonVariant::Text => { cx.theme().transparent } @@ -527,7 +510,7 @@ impl ButtonVariant { fn shadow(&self, _: &WindowContext) -> bool { match self { - ButtonVariant::Primary | ButtonVariant::Danger => true, + ButtonVariant::Primary => true, ButtonVariant::Custom(c) => c.shadow, _ => false, } @@ -552,8 +535,6 @@ impl ButtonVariant { fn hovered(&self, cx: &WindowContext) -> ButtonVariantStyle { let bg = match self { ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::TEN), - ButtonVariant::Outline => cx.theme().secondary_hover, - ButtonVariant::Danger => cx.theme().danger_hover, ButtonVariant::Ghost => cx.theme().base.step(cx, ColorScaleStep::FOUR), ButtonVariant::Link => cx.theme().transparent, ButtonVariant::Text => cx.theme().transparent, @@ -561,7 +542,7 @@ impl ButtonVariant { }; let border = self.border_color(cx); let fg = match self { - ButtonVariant::Link => cx.theme().link_hover, + ButtonVariant::Link => cx.theme().accent.step(cx, ColorScaleStep::TEN), _ => self.text_color(cx), }; let underline = self.underline(cx); @@ -578,26 +559,20 @@ impl ButtonVariant { fn active(&self, cx: &WindowContext) -> ButtonVariantStyle { let bg = match self { - ButtonVariant::Primary => cx.theme().primary_active, - ButtonVariant::Outline => cx.theme().secondary_active, - ButtonVariant::Ghost => { - if cx.theme().appearance.is_dark() { - cx.theme().secondary.lighten(0.2).opacity(0.8) - } else { - cx.theme().secondary.darken(0.2).opacity(0.8) - } - } - ButtonVariant::Danger => cx.theme().danger_active, + ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::TEN), + ButtonVariant::Ghost => cx.theme().base.step(cx, ColorScaleStep::FOUR), ButtonVariant::Link => cx.theme().transparent, ButtonVariant::Text => cx.theme().transparent, ButtonVariant::Custom(colors) => colors.active, }; - let border = self.border_color(cx); + let fg = match self { - ButtonVariant::Link => cx.theme().link_active, - ButtonVariant::Text => cx.theme().foreground.opacity(0.7), + ButtonVariant::Link => cx.theme().accent.step(cx, ColorScaleStep::NINE), + ButtonVariant::Text => cx.theme().base.step(cx, ColorScaleStep::ELEVEN), _ => self.text_color(cx), }; + + let border = self.border_color(cx); let underline = self.underline(cx); let shadow = self.shadow(cx); @@ -612,26 +587,20 @@ impl ButtonVariant { fn selected(&self, cx: &WindowContext) -> ButtonVariantStyle { let bg = match self { - ButtonVariant::Primary => cx.theme().primary_active, - ButtonVariant::Outline => cx.theme().secondary_active, - ButtonVariant::Ghost => { - if cx.theme().appearance.is_dark() { - cx.theme().secondary.lighten(0.2).opacity(0.8) - } else { - cx.theme().secondary.darken(0.2).opacity(0.8) - } - } - ButtonVariant::Danger => cx.theme().danger_active, + ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::TEN), + ButtonVariant::Ghost => cx.theme().base.step(cx, ColorScaleStep::FOUR), ButtonVariant::Link => cx.theme().transparent, ButtonVariant::Text => cx.theme().transparent, ButtonVariant::Custom(colors) => colors.active, }; - let border = self.border_color(cx); + let fg = match self { - ButtonVariant::Link => cx.theme().link_active, - ButtonVariant::Text => cx.theme().foreground.opacity(0.7), + ButtonVariant::Link => cx.theme().accent.step(cx, ColorScaleStep::TEN), + ButtonVariant::Text => cx.theme().accent.step(cx, ColorScaleStep::TEN), _ => self.text_color(cx), }; + + let border = self.border_color(cx); let underline = self.underline(cx); let shadow = self.shadow(cx); @@ -646,26 +615,14 @@ impl ButtonVariant { fn disabled(&self, cx: &WindowContext) -> ButtonVariantStyle { let bg = match self { - ButtonVariant::Link - | ButtonVariant::Ghost - | ButtonVariant::Outline - | ButtonVariant::Text => cx.theme().transparent, - ButtonVariant::Primary => cx.theme().colors.primary.opacity(0.15), - ButtonVariant::Danger => cx.theme().danger.opacity(0.15), - ButtonVariant::Custom(style) => style.color.opacity(0.15), - }; - let fg = match self { - ButtonVariant::Link | ButtonVariant::Text | ButtonVariant::Ghost => { - cx.theme().link.grayscale() + ButtonVariant::Link | ButtonVariant::Ghost | ButtonVariant::Text => { + cx.theme().transparent } - _ => cx.theme().secondary_foreground.opacity(0.5).grayscale(), - }; - - let border = match self { - ButtonVariant::Outline => cx.theme().border.opacity(0.5), - _ => bg, + _ => cx.theme().base.step(cx, ColorScaleStep::FOUR), }; + let fg = cx.theme().base.step(cx, ColorScaleStep::ELEVEN); + let border = bg; let underline = self.underline(cx); let shadow = false; diff --git a/crates/ui/src/checkbox.rs b/crates/ui/src/checkbox.rs index bbd3c22..083c363 100644 --- a/crates/ui/src/checkbox.rs +++ b/crates/ui/src/checkbox.rs @@ -1,4 +1,8 @@ -use crate::{h_flex, theme::ActiveTheme, v_flex, Disableable, IconName, Selectable}; +use crate::{ + h_flex, + theme::{scale::ColorScaleStep, ActiveTheme}, + v_flex, Disableable, IconName, Selectable, +}; use gpui::{ div, prelude::FluentBuilder as _, relative, svg, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, Styled as _, @@ -65,11 +69,14 @@ impl RenderOnce for Checkbox { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let (color, icon_color) = if self.disabled { ( - cx.theme().colors.primary.opacity(0.5), - cx.theme().primary_foreground.opacity(0.5), + cx.theme().base.step(cx, ColorScaleStep::THREE), + cx.theme().base.step(cx, ColorScaleStep::ELEVEN), ) } else { - (cx.theme().colors.primary, cx.theme().primary_foreground) + ( + cx.theme().accent.step(cx, ColorScaleStep::NINE), + cx.theme().accent.step(cx, ColorScaleStep::ONE), + ) }; h_flex() @@ -104,21 +111,22 @@ impl RenderOnce for Checkbox { ) .map(|this| { if let Some(label) = self.label { - this.text_color(cx.theme().foreground).child( - div() - .w_full() - .overflow_x_hidden() - .text_ellipsis() - .line_height(relative(1.)) - .child(label), - ) + this.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) + .child( + div() + .w_full() + .overflow_x_hidden() + .text_ellipsis() + .line_height(relative(1.)) + .child(label), + ) } else { this } }) .when(self.disabled, |this| { this.cursor_not_allowed() - .text_color(cx.theme().muted_foreground) + .text_color(cx.theme().base.step(cx, ColorScaleStep::TEN)) }) .when_some( self.on_click.filter(|_| !self.disabled), diff --git a/crates/ui/src/divider.rs b/crates/ui/src/divider.rs index a665c4d..82771bf 100644 --- a/crates/ui/src/divider.rs +++ b/crates/ui/src/divider.rs @@ -1,10 +1,9 @@ +use crate::theme::{scale::ColorScaleStep, ActiveTheme}; use gpui::{ div, prelude::FluentBuilder as _, px, Axis, Div, Hsla, IntoElement, ParentElement, RenderOnce, SharedString, Styled, }; -use crate::theme::ActiveTheme; - /// A divider that can be either vertical or horizontal. #[derive(IntoElement)] pub struct Divider { @@ -52,8 +51,6 @@ impl Styled for Divider { impl RenderOnce for Divider { fn render(self, cx: &mut gpui::WindowContext) -> impl IntoElement { - let theme = cx.theme(); - self.base .flex() .flex_shrink_0() @@ -66,7 +63,9 @@ impl RenderOnce for Divider { Axis::Vertical => this.w(px(1.)).h_full(), Axis::Horizontal => this.h(px(1.)).w_full(), }) - .bg(self.color.unwrap_or(cx.theme().border)), + .bg(self + .color + .unwrap_or(cx.theme().base.step(cx, ColorScaleStep::THREE))), ) .when_some(self.label, |this, label| { this.child( @@ -76,7 +75,6 @@ impl RenderOnce for Divider { .mx_auto() .text_xs() .bg(cx.theme().background) - .text_color(theme.muted_foreground) .child(label), ) }) diff --git a/crates/ui/src/dock/tiles.rs b/crates/ui/src/dock/tiles.rs deleted file mode 100644 index ff6550c..0000000 --- a/crates/ui/src/dock/tiles.rs +++ /dev/null @@ -1,702 +0,0 @@ -use gpui::*; -use std::{ - cell::Cell, - fmt::{Debug, Formatter}, - rc::Rc, - sync::Arc, -}; - -use super::{DockArea, Panel, PanelEvent, PanelInfo, PanelState, PanelView, TabPanel, TileMeta}; -use crate::{ - h_flex, - scroll::{Scrollbar, ScrollbarState}, - theme::ActiveTheme, - v_flex, Icon, IconName, -}; - -const MINIMUM_SIZE: Size = size(px(100.), px(100.)); -const DRAG_BAR_HEIGHT: Pixels = px(30.); -const HANDLE_SIZE: Pixels = px(20.0); - -#[derive(Clone, Render)] -pub struct DragMoving(EntityId); - -#[derive(Clone, Render)] -pub struct DragResizing(EntityId); - -#[derive(Clone)] -struct ResizeDrag { - axis: ResizeAxis, - last_position: Point, - last_bounds: Bounds, -} - -#[derive(Clone, PartialEq)] -enum ResizeAxis { - Horizontal, - Vertical, - Both, -} - -/// TileItem is a moveable and resizable panel that can be added to a Tiles view. -#[derive(Clone)] -pub struct TileItem { - pub(crate) panel: Arc, - bounds: Bounds, - z_index: usize, -} - -impl Debug for TileItem { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("TileItem") - .field("bounds", &self.bounds) - .field("z_index", &self.z_index) - .finish() - } -} - -impl TileItem { - pub fn new(panel: Arc, bounds: Bounds) -> Self { - Self { - panel, - bounds, - z_index: 0, - } - } - - pub fn z_index(mut self, z_index: usize) -> Self { - self.z_index = z_index; - self - } -} - -/// Tiles is a canvas that can contain multiple panels, each of which can be dragged and resized. -pub struct Tiles { - focus_handle: FocusHandle, - pub(crate) panels: Vec, - dragging_index: Option, - dragging_initial_mouse: Point, - dragging_initial_bounds: Bounds, - resizing_index: Option, - resizing_drag_data: Option, - bounds: Bounds, - - scroll_state: Rc>, - scroll_handle: ScrollHandle, -} - -impl Panel for Tiles { - fn panel_id(&self) -> SharedString { - "Tiles".into() - } - - fn title(&self, _cx: &WindowContext) -> AnyElement { - "Tiles".into_any_element() - } - - fn dump(&self, cx: &AppContext) -> PanelState { - let panels = self - .panels - .iter() - .map(|item: &TileItem| item.panel.dump(cx)) - .collect(); - - let metas = self - .panels - .iter() - .map(|item: &TileItem| TileMeta { - bounds: item.bounds, - z_index: item.z_index, - }) - .collect(); - - let mut state = PanelState::new(self); - state.panel_name = self.panel_id().to_string(); - state.children = panels; - state.info = PanelInfo::Tiles { metas }; - state - } -} - -impl Tiles { - pub fn new(cx: &mut ViewContext) -> Self { - Self { - focus_handle: cx.focus_handle(), - panels: vec![], - dragging_index: None, - dragging_initial_mouse: Point::default(), - dragging_initial_bounds: Bounds::default(), - resizing_index: None, - resizing_drag_data: None, - bounds: Bounds::default(), - scroll_state: Rc::new(Cell::new(ScrollbarState::default())), - scroll_handle: ScrollHandle::default(), - } - } - - fn sorted_panels(&self) -> Vec { - let mut items: Vec<(usize, TileItem)> = self.panels.iter().cloned().enumerate().collect(); - items.sort_by(|a, b| a.1.z_index.cmp(&b.1.z_index).then_with(|| a.0.cmp(&b.0))); - items.into_iter().map(|(_, item)| item).collect() - } - - /// Return the index of the panel. - #[inline] - pub(crate) fn index_of(&self, panel: Arc) -> Option { - #[allow(clippy::op_ref)] - self.panels.iter().position(|p| &p.panel == &panel) - } - - /// Remove panel from the children. - pub fn remove(&mut self, panel: Arc, cx: &mut ViewContext) { - if let Some(ix) = self.index_of(panel.clone()) { - self.panels.remove(ix); - - cx.emit(PanelEvent::LayoutChanged); - } - } - - fn update_initial_position(&mut self, position: Point, cx: &mut ViewContext<'_, Self>) { - let Some((index, item)) = self.find_at_position(position) else { - return; - }; - - let inner_pos = position - self.bounds.origin; - let bounds = item.bounds; - self.dragging_index = Some(index); - self.dragging_initial_mouse = inner_pos; - self.dragging_initial_bounds = bounds; - cx.notify(); - } - - fn update_position(&mut self, pos: Point, cx: &mut ViewContext<'_, Self>) { - let Some(index) = self.dragging_index else { - return; - }; - - let Some(item) = self.panels.get_mut(index) else { - return; - }; - - let adjusted_position = pos - self.bounds.origin; - let delta = adjusted_position - self.dragging_initial_mouse; - let mut new_origin = self.dragging_initial_bounds.origin + delta; - - new_origin.x = new_origin.x.max(px(0.0)); - new_origin.y = new_origin.y.max(px(0.0)); - - item.bounds.origin = round_point_to_nearest_ten(new_origin); - cx.notify(); - } - - fn update_resizing_drag(&mut self, drag_data: ResizeDrag, cx: &mut ViewContext<'_, Self>) { - if let Some((index, _item)) = self.find_at_position(drag_data.last_position) { - self.resizing_index = Some(index); - self.resizing_drag_data = Some(drag_data); - cx.notify(); - } - } - - fn resize_width(&mut self, new_width: Pixels, cx: &mut ViewContext<'_, Self>) { - if let Some(index) = self.resizing_index { - if let Some(item) = self.panels.get_mut(index) { - item.bounds.size.width = round_to_nearest_ten(new_width); - cx.notify(); - } - } - } - - fn resize_height(&mut self, new_height: Pixels, cx: &mut ViewContext<'_, Self>) { - if let Some(index) = self.resizing_index { - if let Some(item) = self.panels.get_mut(index) { - item.bounds.size.height = round_to_nearest_ten(new_height); - cx.notify(); - } - } - } - - pub fn add_item( - &mut self, - item: TileItem, - dock_area: &WeakView, - cx: &mut ViewContext, - ) { - self.panels.push(item.clone()); - - cx.window_context().defer({ - let panel = item.panel.clone(); - let dock_area = dock_area.clone(); - - move |cx| { - // Subscribe to the panel's layout change event. - _ = dock_area.update(cx, |this, cx| { - if let Ok(tab_panel) = panel.view().downcast::() { - this.subscribe_panel(&tab_panel, cx); - } - }); - } - }); - - cx.emit(PanelEvent::LayoutChanged); - cx.notify(); - } - - /// Find the panel at a given position, considering z-index - fn find_at_position(&self, position: Point) -> Option<(usize, &TileItem)> { - let inner_pos = position - self.bounds.origin; - let mut panels_with_indices: Vec<(usize, &TileItem)> = - self.panels.iter().enumerate().collect(); - - panels_with_indices - .sort_by(|a, b| b.1.z_index.cmp(&a.1.z_index).then_with(|| b.0.cmp(&a.0))); - - for (index, item) in panels_with_indices { - let extended_bounds = Bounds::new( - item.bounds.origin, - item.bounds.size + size(HANDLE_SIZE, HANDLE_SIZE) / 2.0, - ); - if extended_bounds.contains(&inner_pos) { - return Some((index, item)); - } - } - - None - } - - #[inline] - fn reset_current_index(&mut self) { - self.dragging_index = None; - self.resizing_index = None; - } - - /// Bring the panel of target_index to front, returns (old_index, new_index) if successful - fn bring_to_front(&mut self, target_index: Option) -> Option<(usize, usize)> { - if let Some(old_index) = target_index { - if old_index < self.panels.len() { - let item = self.panels.remove(old_index); - self.panels.push(item); - let new_index = self.panels.len() - 1; - self.reset_current_index(); - return Some((old_index, new_index)); - } - } - None - } - - /// Produce a vector of AnyElement representing the three possible resize handles - fn render_resize_handles( - &mut self, - cx: &mut ViewContext, - entity_id: EntityId, - item: &TileItem, - is_occluded: impl Fn(&Bounds) -> bool, - ) -> Vec { - let panel_bounds = item.bounds; - let right_handle_bounds = Bounds::new( - panel_bounds.origin + point(panel_bounds.size.width - HANDLE_SIZE.half(), px(0.0)), - size(HANDLE_SIZE.half(), panel_bounds.size.height), - ); - - let bottom_handle_bounds = Bounds::new( - panel_bounds.origin + point(px(0.0), panel_bounds.size.height - HANDLE_SIZE.half()), - size(panel_bounds.size.width, HANDLE_SIZE.half()), - ); - - let corner_handle_bounds = Bounds::new( - panel_bounds.origin - + point( - panel_bounds.size.width - HANDLE_SIZE.half(), - panel_bounds.size.height - HANDLE_SIZE.half(), - ), - size(HANDLE_SIZE.half(), HANDLE_SIZE.half()), - ); - - let mut elements = Vec::new(); - - // Right resize handle - elements.push(if !is_occluded(&right_handle_bounds) { - div() - .id("right-resize-handle") - .cursor_col_resize() - .absolute() - .top(px(0.0)) - .right(-HANDLE_SIZE.half()) - .w(HANDLE_SIZE) - .h(panel_bounds.size.height) - .on_mouse_down( - MouseButton::Left, - cx.listener({ - move |this, event: &MouseDownEvent, cx| { - let last_position = event.position; - let drag_data = ResizeDrag { - axis: ResizeAxis::Horizontal, - last_position, - last_bounds: panel_bounds, - }; - this.update_resizing_drag(drag_data, cx); - if let Some((_, new_ix)) = this.bring_to_front(this.resizing_index) { - this.resizing_index = Some(new_ix); - } - } - }), - ) - .on_drag(DragResizing(entity_id), |drag, _, cx| { - cx.stop_propagation(); - cx.new_view(|_| drag.clone()) - }) - .on_drag_move( - cx.listener(move |this, e: &DragMoveEvent, cx| { - match e.drag(cx) { - DragResizing(id) => { - if *id != entity_id { - return; - } - - if let Some(ref drag_data) = this.resizing_drag_data { - if drag_data.axis != ResizeAxis::Horizontal { - return; - } - let pos = e.event.position; - let delta = pos.x - drag_data.last_position.x; - let new_width = (drag_data.last_bounds.size.width + delta) - .max(MINIMUM_SIZE.width); - this.resize_width(new_width, cx); - } - } - } - }), - ) - .into_any_element() - } else { - div().into_any_element() - }); - - // Bottom resize handle - elements.push(if !is_occluded(&bottom_handle_bounds) { - div() - .id("bottom-resize-handle") - .cursor_row_resize() - .absolute() - .left(px(0.0)) - .bottom(-HANDLE_SIZE.half()) - .w(panel_bounds.size.width) - .h(HANDLE_SIZE) - .on_mouse_down( - MouseButton::Left, - cx.listener({ - move |this, event: &MouseDownEvent, cx| { - let last_position = event.position; - let drag_data = ResizeDrag { - axis: ResizeAxis::Vertical, - last_position, - last_bounds: panel_bounds, - }; - this.update_resizing_drag(drag_data, cx); - if let Some((_, new_ix)) = this.bring_to_front(this.resizing_index) { - this.resizing_index = Some(new_ix); - } - } - }), - ) - .on_drag(DragResizing(entity_id), |drag, _, cx| { - cx.stop_propagation(); - cx.new_view(|_| drag.clone()) - }) - .on_drag_move( - cx.listener(move |this, e: &DragMoveEvent, cx| { - match e.drag(cx) { - DragResizing(id) => { - if *id != entity_id { - return; - } - - if let Some(ref drag_data) = this.resizing_drag_data { - let pos = e.event.position; - let delta = pos.y - drag_data.last_position.y; - let new_height = (drag_data.last_bounds.size.height + delta) - .max(MINIMUM_SIZE.width); - this.resize_height(new_height, cx); - } - } - } - }), - ) - .into_any_element() - } else { - div().into_any_element() - }); - - // Corner resize handle - elements.push(if !is_occluded(&corner_handle_bounds) { - div() - .id("corner-resize-handle") - .cursor_nwse_resize() - .absolute() - .right(-HANDLE_SIZE.half()) - .bottom(-HANDLE_SIZE.half()) - .w(HANDLE_SIZE) - .h(HANDLE_SIZE) - .child( - Icon::new(IconName::ResizeCorner) - .size(HANDLE_SIZE.half()) - .text_color(cx.theme().foreground.opacity(0.3)), - ) - .on_mouse_down( - MouseButton::Left, - cx.listener({ - move |this, event: &MouseDownEvent, cx| { - let last_position = event.position; - let drag_data = ResizeDrag { - axis: ResizeAxis::Both, - last_position, - last_bounds: panel_bounds, - }; - this.update_resizing_drag(drag_data, cx); - if let Some((_, new_ix)) = this.bring_to_front(this.resizing_index) { - this.resizing_index = Some(new_ix); - } - } - }), - ) - .on_drag(DragResizing(entity_id), |drag, _, cx| { - cx.stop_propagation(); - cx.new_view(|_| drag.clone()) - }) - .on_drag_move( - cx.listener(move |this, e: &DragMoveEvent, cx| { - match e.drag(cx) { - DragResizing(id) => { - if *id != entity_id { - return; - } - - if let Some(ref drag_data) = this.resizing_drag_data { - if drag_data.axis != ResizeAxis::Both { - return; - } - let pos = e.event.position; - let delta_x = pos.x - drag_data.last_position.x; - let delta_y = pos.y - drag_data.last_position.y; - let new_width = (drag_data.last_bounds.size.width + delta_x) - .max(MINIMUM_SIZE.width); - let new_height = (drag_data.last_bounds.size.height + delta_y) - .max(MINIMUM_SIZE.height); - this.resize_height(new_height, cx); - this.resize_width(new_width, cx); - } - } - } - }), - ) - .into_any_element() - } else { - div().into_any_element() - }); - - elements - } - - /// Produce the drag-bar element for the given panel item - fn render_drag_bar( - &mut self, - cx: &mut ViewContext, - entity_id: EntityId, - item: &TileItem, - is_occluded: &impl Fn(&Bounds) -> bool, - ) -> AnyElement { - let drag_bar_bounds = Bounds::new( - item.bounds.origin, - Size { - width: item.bounds.size.width, - height: DRAG_BAR_HEIGHT, - }, - ); - - if !is_occluded(&drag_bar_bounds) { - h_flex() - .id("drag-bar") - .cursor_grab() - .absolute() - .w_full() - .h(DRAG_BAR_HEIGHT) - .bg(cx.theme().transparent) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, event: &MouseDownEvent, cx| { - let last_position = event.position; - this.update_initial_position(last_position, cx); - if let Some((_, new_ix)) = this.bring_to_front(this.dragging_index) { - this.dragging_index = Some(new_ix); - } - }), - ) - .on_drag(DragMoving(entity_id), |drag, _, cx| { - cx.stop_propagation(); - cx.new_view(|_| drag.clone()) - }) - .on_drag_move(cx.listener(move |this, e: &DragMoveEvent, cx| { - match e.drag(cx) { - DragMoving(id) => { - if *id != entity_id { - return; - } - this.update_position(e.event.position, cx); - } - } - })) - .into_any_element() - } else { - div().into_any_element() - } - } - - fn render_panel( - &mut self, - item: &TileItem, - ix: usize, - cx: &mut ViewContext, - ) -> impl IntoElement { - let entity_id = cx.entity_id(); - let panel_view = item.panel.view(); - let is_occluded = { - let panels = self.panels.clone(); - move |bounds: &Bounds| { - let this_z = panels[ix].z_index; - let this_ix = ix; - panels.iter().enumerate().any(|(sub_ix, other_item)| { - if sub_ix == this_ix { - return false; - } - let other_is_above = (other_item.z_index > this_z) - || (other_item.z_index == this_z && sub_ix > this_ix); - - other_is_above && other_item.bounds.intersects(bounds) - }) - } - }; - - v_flex() - .bg(cx.theme().background) - .border_1() - .border_color(cx.theme().border) - .absolute() - .left(item.bounds.origin.x - px(1.)) - .top(item.bounds.origin.y - px(1.)) - .w(item.bounds.size.width + px(1.)) - .h(item.bounds.size.height + px(1.)) - .child( - h_flex() - .w_full() - .h_full() - .overflow_hidden() - .child(panel_view), - ) - .children(self.render_resize_handles(cx, entity_id, item, &is_occluded)) - .child(self.render_drag_bar(cx, entity_id, item, &is_occluded)) - } -} - -#[inline] -fn round_to_nearest_ten(value: Pixels) -> Pixels { - px((value.0 / 10.0).round() * 10.0) -} - -#[inline] -fn round_point_to_nearest_ten(point: Point) -> Point { - Point::new(round_to_nearest_ten(point.x), round_to_nearest_ten(point.y)) -} - -impl FocusableView for Tiles { - fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { - self.focus_handle.clone() - } -} -impl EventEmitter for Tiles {} -impl EventEmitter for Tiles {} -impl Render for Tiles { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let view = cx.view().clone(); - let view_id = view.entity_id(); - let panels = self.sorted_panels(); - let scroll_bounds = - self.panels - .iter() - .fold(Bounds::default(), |acc: Bounds, item| Bounds { - origin: Point { - x: acc.origin.x.min(item.bounds.origin.x), - y: acc.origin.y.min(item.bounds.origin.y), - }, - size: Size { - width: acc.size.width.max(item.bounds.right()), - height: acc.size.height.max(item.bounds.bottom()), - }, - }); - let scroll_size = scroll_bounds.size - size(scroll_bounds.origin.x, scroll_bounds.origin.y); - - div() - .relative() - .bg(cx.theme().background) - .child( - div() - .id("tiles") - .track_scroll(&self.scroll_handle) - .size_full() - .overflow_scroll() - .children( - panels - .into_iter() - .enumerate() - .map(|(ix, item)| self.render_panel(&item, ix, cx)), - ) - .child({ - canvas( - move |bounds, cx| view.update(cx, |r, _| r.bounds = bounds), - |_, _, _| {}, - ) - .absolute() - .size_full() - }), - ) - .on_mouse_up( - MouseButton::Left, - cx.listener(move |this, _event: &MouseUpEvent, cx| { - if this.dragging_index.is_some() - || this.resizing_index.is_some() - || this.resizing_drag_data.is_some() - { - this.reset_current_index(); - this.resizing_drag_data = None; - cx.emit(PanelEvent::LayoutChanged); - cx.notify(); - } - }), - ) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, event: &MouseDownEvent, cx| { - if this.resizing_index.is_none() && this.dragging_index.is_none() { - let position = event.position; - if let Some((index, _)) = this.find_at_position(position) { - this.bring_to_front(Some(index)); - cx.notify(); - } - } - }), - ) - .child( - div() - .absolute() - .top_0() - .left_0() - .right_0() - .bottom_0() - .child(Scrollbar::both( - view_id, - self.scroll_state.clone(), - self.scroll_handle.clone(), - scroll_size, - )), - ) - .size_full() - } -} diff --git a/crates/ui/src/dock/dock.rs b/crates/ui/src/dock_area/dock.rs similarity index 95% rename from crates/ui/src/dock/dock.rs rename to crates/ui/src/dock_area/dock.rs index 7325aaf..ebab635 100644 --- a/crates/ui/src/dock/dock.rs +++ b/crates/ui/src/dock_area/dock.rs @@ -1,3 +1,10 @@ +use super::{DockArea, DockItem}; +use crate::{ + dock_area::{panel::PanelView, tab_panel::TabPanel}, + resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE}, + theme::{scale::ColorScaleStep, ActiveTheme as _}, + AxisExt as _, StyledExt, +}; use gpui::{ div, prelude::FluentBuilder as _, px, Axis, Element, Entity, InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, @@ -7,14 +14,6 @@ use gpui::{ use serde::{Deserialize, Serialize}; use std::sync::Arc; -use crate::{ - resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE}, - theme::ActiveTheme as _, - AxisExt as _, StyledExt, -}; - -use super::{DockArea, DockItem, PanelView, TabPanel}; - #[derive(Clone, Render)] struct ResizePanel; @@ -190,16 +189,6 @@ impl Dock { } }); } - DockItem::Tiles { view, .. } => { - cx.defer({ - let view = view.clone(); - move |cx| { - _ = dock_area.update(cx, |this, cx| { - this.subscribe_panel(&view, cx); - }); - } - }); - } DockItem::Panel { .. } => { // Not supported } @@ -285,7 +274,7 @@ impl Dock { }) .child( div() - .bg(cx.theme().border) + .bg(cx.theme().base.step(cx, ColorScaleStep::THREE)) .when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE)) .when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)), ) @@ -383,8 +372,6 @@ impl Render for Dock { DockItem::Split { view, .. } => this.child(view.clone()), DockItem::Tabs { view, .. } => this.child(view.clone()), DockItem::Panel { view, .. } => this.child(view.clone().view()), - // Not support to render Tiles and Tile into Dock - DockItem::Tiles { .. } => this, }) .child(self.render_resize_handle(cx)) .child(DockElement { @@ -416,7 +403,7 @@ impl Element for DockElement { fn request_layout( &mut self, _: Option<&gpui::GlobalElementId>, - cx: &mut gpui::WindowContext, + cx: &mut WindowContext, ) -> (gpui::LayoutId, Self::RequestLayoutState) { (cx.request_layout(Style::default(), None), ()) } @@ -426,7 +413,7 @@ impl Element for DockElement { _: Option<&gpui::GlobalElementId>, _: gpui::Bounds, _: &mut Self::RequestLayoutState, - _: &mut gpui::WindowContext, + _: &mut WindowContext, ) -> Self::PrepaintState { } @@ -436,7 +423,7 @@ impl Element for DockElement { _: gpui::Bounds, _: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, - cx: &mut gpui::WindowContext, + cx: &mut WindowContext, ) { cx.on_mouse_event({ let view = self.view.clone(); diff --git a/crates/ui/src/dock/invalid_panel.rs b/crates/ui/src/dock_area/invalid_panel.rs similarity index 82% rename from crates/ui/src/dock/invalid_panel.rs rename to crates/ui/src/dock_area/invalid_panel.rs index e558bc5..a0c6f81 100644 --- a/crates/ui/src/dock/invalid_panel.rs +++ b/crates/ui/src/dock_area/invalid_panel.rs @@ -1,11 +1,14 @@ +use super::PanelEvent; +use crate::{ + dock_area::panel::Panel, + dock_area::state::PanelState, + theme::{scale::ColorScaleStep, ActiveTheme}, +}; use gpui::{ AppContext, EventEmitter, FocusHandle, FocusableView, ParentElement as _, Render, SharedString, Styled as _, WindowContext, }; -use super::{Panel, PanelEvent, PanelState}; -use crate::theme::ActiveTheme; - pub(crate) struct InvalidPanel { name: SharedString, focus_handle: FocusHandle, @@ -27,7 +30,7 @@ impl Panel for InvalidPanel { "InvalidPanel".into() } - fn dump(&self, _cx: &AppContext) -> super::PanelState { + fn dump(&self, _cx: &AppContext) -> PanelState { self.old_state.clone() } } @@ -49,7 +52,7 @@ impl Render for InvalidPanel { .flex_col() .items_center() .justify_center() - .text_color(cx.theme().muted_foreground) + .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) .child(format!( "The `{}` panel type is not registered in PanelRegistry.", self.name.clone() diff --git a/crates/ui/src/dock/mod.rs b/crates/ui/src/dock_area/mod.rs similarity index 84% rename from crates/ui/src/dock/mod.rs rename to crates/ui/src/dock_area/mod.rs index 69f4c65..42c9fa5 100644 --- a/crates/ui/src/dock/mod.rs +++ b/crates/ui/src/dock_area/mod.rs @@ -1,12 +1,13 @@ -#[allow(clippy::module_inception)] -mod dock; -mod invalid_panel; -mod panel; -mod stack_panel; -mod state; -mod tab_panel; -mod tiles; - +use crate::{ + dock_area::{ + dock::{Dock, DockPlacement}, + panel::{Panel, PanelEvent, PanelStyle, PanelView}, + stack_panel::StackPanel, + state::{DockAreaState, DockState}, + tab_panel::TabPanel, + }, + theme::{scale::ColorScaleStep, ActiveTheme}, +}; use anyhow::Result; use gpui::{ actions, canvas, div, prelude::FluentBuilder, AnyElement, AnyView, AppContext, Axis, Bounds, @@ -14,16 +15,15 @@ use gpui::{ ParentElement as _, Pixels, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowContext, }; +use panel::PanelRegistry; use std::sync::Arc; -pub use dock::*; -pub use panel::*; -pub use stack_panel::*; -pub use state::*; -pub use tab_panel::*; -pub use tiles::*; - -use crate::theme::{scale::ColorScaleStep, ActiveTheme}; +pub mod dock; +pub mod invalid_panel; +pub mod panel; +pub mod stack_panel; +pub mod state; +pub mod tab_panel; pub fn init(cx: &mut AppContext) { cx.set_global(PanelRegistry::new()); @@ -88,11 +88,6 @@ pub enum DockItem { }, /// Panel layout Panel { view: Arc }, - /// Tiles layout - Tiles { - items: Vec, - view: View, - }, } impl DockItem { @@ -159,51 +154,6 @@ impl DockItem { Self::Panel { view: panel } } - /// Create DockItem with tiles layout - /// - /// This items and metas should have the same length. - pub fn tiles( - items: Vec, - metas: Vec + Copy>, - dock_area: &WeakView, - cx: &mut WindowContext, - ) -> Self { - assert!(items.len() == metas.len()); - - let tile_panel = cx.new_view(|cx| { - let mut tiles = Tiles::new(cx); - for (ix, item) in items.clone().into_iter().enumerate() { - match item { - DockItem::Tabs { view, .. } => { - let meta: TileMeta = metas[ix].into(); - let tile_item = - TileItem::new(Arc::new(view), meta.bounds).z_index(meta.z_index); - tiles.add_item(tile_item, dock_area, cx); - } - _ => { - // Ignore non-tabs items - } - } - } - tiles - }); - - cx.defer({ - let tile_panel = tile_panel.clone(); - let dock_area = dock_area.clone(); - move |cx| { - _ = dock_area.update(cx, |this, cx| { - this.subscribe_panel(&tile_panel, cx); - }); - } - }); - - Self::Tiles { - items: tile_panel.read(cx).panels.clone(), - view: tile_panel, - } - } - /// Create DockItem with tabs layout, items are displayed as tabs. /// /// The `active_ix` is the index of the active tab, if `None` the first tab is active. @@ -256,7 +206,6 @@ impl DockItem { match self { Self::Split { view, .. } => Arc::new(view.clone()), Self::Tabs { view, .. } => Arc::new(view.clone()), - Self::Tiles { view, .. } => Arc::new(view.clone()), Self::Panel { view, .. } => view.clone(), } } @@ -269,14 +218,6 @@ impl DockItem { } Self::Tabs { items, .. } => items.iter().find(|item| *item == &panel).cloned(), Self::Panel { view } => Some(view.clone()), - Self::Tiles { items, .. } => items.iter().find_map(|item| { - #[allow(clippy::op_ref)] - if &item.panel == &panel { - Some(item.panel.clone()) - } else { - None - } - }), } } @@ -312,7 +253,6 @@ impl DockItem { stack_panel.add_panel(new_item.view(), None, dock_area.clone(), cx); }); } - Self::Tiles { .. } => {} Self::Panel { .. } => {} } } @@ -330,7 +270,6 @@ impl DockItem { item.set_collapsed(collapsed, cx); } } - DockItem::Tiles { .. } => {} DockItem::Panel { .. } => {} } } @@ -340,7 +279,6 @@ impl DockItem { match self { DockItem::Tabs { view, .. } => Some(view.clone()), DockItem::Split { view, .. } => view.read(cx).left_top_tab_panel(true, cx), - DockItem::Tiles { .. } => None, DockItem::Panel { .. } => None, } } @@ -350,7 +288,6 @@ impl DockItem { match self { DockItem::Tabs { view, .. } => Some(view.clone()), DockItem::Split { view, .. } => view.read(cx).right_top_tab_panel(true, cx), - DockItem::Tiles { .. } => None, DockItem::Panel { .. } => None, } } @@ -403,13 +340,7 @@ impl DockArea { cx.notify(); } - // FIXME: Remove this method after 2025-01-01 - #[deprecated(note = "Use `set_center` instead")] - pub fn set_root(&mut self, item: DockItem, cx: &mut ViewContext) { - self.set_center(item, cx); - } - - /// The the DockItem as the center of the dock area. + /// The DockItem as the center of the dock area. /// /// This is used to render at the Center of the DockArea. pub fn set_center(&mut self, item: DockItem, cx: &mut ViewContext) { @@ -722,9 +653,6 @@ impl DockArea { DockItem::Tabs { .. } => { // We subscribe to the tab panel event in StackPanel's insert_panel } - DockItem::Tiles { .. } => { - // We subscribe to the tab panel event in Tiles's [`add_item`](Tiles::add_item) - } DockItem::Panel { .. } => { // Not supported } @@ -794,7 +722,6 @@ impl DockArea { match &self.items { DockItem::Split { view, .. } => view.clone().into_any_element(), DockItem::Tabs { view, .. } => view.clone().into_any_element(), - DockItem::Tiles { view, .. } => view.clone().into_any_element(), DockItem::Panel { view, .. } => view.clone().view().into_any_element(), } } @@ -842,49 +769,41 @@ impl Render for DockArea { if let Some(zoom_view) = self.zoom_view.clone() { this.child(zoom_view) } else { - match &self.items { - DockItem::Tiles { view, .. } => { - // render tiles - this.child(view.clone()) - } - _ => { - // render dock - this.child( + // render dock + this.child( + div() + .flex() + .flex_row() + .h_full() + // Left dock + .when_some(self.left_dock.clone(), |this, dock| { + this.bg(cx.theme().base.step(cx, ColorScaleStep::ONE)) + .child(div().flex().flex_none().child(dock)) + }) + // Center + .child( div() .flex() - .flex_row() - .h_full() - // Left dock - .when_some(self.left_dock.clone(), |this, dock| { - this.bg(cx.theme().base.step(cx, ColorScaleStep::ONE)) - .child(div().flex().flex_none().child(dock)) - }) - // Center + .flex_1() + .flex_col() + .overflow_hidden() + // Top center .child( div() - .flex() .flex_1() - .flex_col() .overflow_hidden() - // Top center - .child( - div() - .flex_1() - .overflow_hidden() - .child(self.render_items(cx)), - ) - // Bottom Dock - .when_some(self.bottom_dock.clone(), |this, dock| { - this.child(dock) - }), + .child(self.render_items(cx)), ) - // Right Dock - .when_some(self.right_dock.clone(), |this, dock| { - this.child(div().flex().flex_none().child(dock)) + // Bottom Dock + .when_some(self.bottom_dock.clone(), |this, dock| { + this.child(dock) }), ) - } - } + // Right Dock + .when_some(self.right_dock.clone(), |this, dock| { + this.child(div().flex().flex_none().child(dock)) + }), + ) } }) } diff --git a/crates/ui/src/dock/panel.rs b/crates/ui/src/dock_area/panel.rs similarity index 97% rename from crates/ui/src/dock/panel.rs rename to crates/ui/src/dock_area/panel.rs index 8f81f5d..04a8c7c 100644 --- a/crates/ui/src/dock/panel.rs +++ b/crates/ui/src/dock_area/panel.rs @@ -1,3 +1,9 @@ +use super::DockArea; +use crate::{ + button::Button, + dock_area::state::{PanelInfo, PanelState}, + popup_menu::PopupMenu, +}; use gpui::{ AnyElement, AnyView, AppContext, EventEmitter, FocusHandle, FocusableView, Global, Hsla, IntoElement, SharedString, View, WeakView, WindowContext, @@ -5,9 +11,6 @@ use gpui::{ use nostr_sdk::prelude::Metadata; use std::{collections::HashMap, sync::Arc}; -use super::{DockArea, PanelInfo, PanelState}; -use crate::{button::Button, popup_menu::PopupMenu}; - pub enum PanelEvent { ZoomIn, ZoomOut, diff --git a/crates/ui/src/dock/stack_panel.rs b/crates/ui/src/dock_area/stack_panel.rs similarity index 95% rename from crates/ui/src/dock/stack_panel.rs rename to crates/ui/src/dock_area/stack_panel.rs index 677ccbd..36b0afe 100644 --- a/crates/ui/src/dock/stack_panel.rs +++ b/crates/ui/src/dock_area/stack_panel.rs @@ -1,19 +1,25 @@ -use gpui::*; -use prelude::FluentBuilder; -use smallvec::SmallVec; -use std::sync::Arc; - -use super::{DockArea, Panel, PanelEvent, PanelState, PanelView, TabPanel}; +use super::{DockArea, PanelEvent}; use crate::{ - dock::PanelInfo, + dock_area::{ + panel::{Panel, PanelView}, + state::{PanelInfo, PanelState}, + tab_panel::TabPanel, + }, h_flex, resizable::{ h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent, ResizablePanelGroup, }, - theme::ActiveTheme, + theme::{scale::ColorScaleStep, ActiveTheme}, AxisExt as _, Placement, }; +use gpui::{ + prelude::FluentBuilder, AppContext, Axis, DismissEvent, EventEmitter, FocusHandle, + FocusableView, IntoElement, ParentElement, Pixels, Render, SharedString, Styled, Subscription, + View, ViewContext, VisualContext as _, WeakView, +}; +use smallvec::SmallVec; +use std::sync::Arc; pub struct StackPanel { pub(super) parent: Option>, @@ -362,14 +368,17 @@ impl FocusableView for StackPanel { self.focus_handle.clone() } } + impl EventEmitter for StackPanel {} + impl EventEmitter for StackPanel {} + impl Render for StackPanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { h_flex() .size_full() .overflow_hidden() - .bg(cx.theme().tab_bar) + .bg(cx.theme().base.step(cx, ColorScaleStep::THREE)) .child(self.panel_group.clone()) } } diff --git a/crates/ui/src/dock/state.rs b/crates/ui/src/dock_area/state.rs similarity index 93% rename from crates/ui/src/dock/state.rs rename to crates/ui/src/dock_area/state.rs index 82ecb40..15e9368 100644 --- a/crates/ui/src/dock/state.rs +++ b/crates/ui/src/dock_area/state.rs @@ -1,3 +1,5 @@ +use super::{invalid_panel::InvalidPanel, Dock, DockArea, DockItem, PanelRegistry}; +use crate::dock_area::{dock::DockPlacement, panel::Panel}; use gpui::{ point, px, size, AppContext, Axis, Bounds, Pixels, View, VisualContext as _, WeakView, WindowContext, @@ -5,15 +7,11 @@ use gpui::{ use itertools::Itertools as _; use serde::{Deserialize, Serialize}; -use super::{ - invalid_panel::InvalidPanel, Dock, DockArea, DockItem, DockPlacement, Panel, PanelRegistry, -}; - /// Used to serialize and deserialize the DockArea #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] pub struct DockAreaState { /// The version is used to mark this persisted state is compatible with the current version - /// For example, some times we many totally changed the structure of the Panel, + /// For example, sometimes we many totally changed the structure of the Panel, /// then we can compare the version to decide whether we can use the state or ignore. #[serde(default)] pub version: Option, @@ -106,8 +104,6 @@ pub enum PanelInfo { Tabs { active_index: usize }, #[serde(rename = "panel")] Panel(serde_json::Value), - #[serde(rename = "tiles")] - Tiles { metas: Vec }, } impl PanelInfo { @@ -126,10 +122,6 @@ impl PanelInfo { Self::Panel(info) } - pub fn tiles(metas: Vec) -> Self { - Self::Tiles { metas } - } - pub fn axis(&self) -> Option { match self { Self::Stack { axis, .. } => Some(if *axis == 0 { @@ -232,7 +224,6 @@ impl PanelState { DockItem::tabs(vec![view.into()], None, &dock_area, cx) } - PanelInfo::Tiles { metas } => DockItem::tiles(items, metas, &dock_area, cx), } } } diff --git a/crates/ui/src/dock/tab_panel.rs b/crates/ui/src/dock_area/tab_panel.rs similarity index 98% rename from crates/ui/src/dock/tab_panel.rs rename to crates/ui/src/dock_area/tab_panel.rs index 2d6328d..4165a32 100644 --- a/crates/ui/src/dock/tab_panel.rs +++ b/crates/ui/src/dock_area/tab_panel.rs @@ -1,10 +1,14 @@ use super::{ - ClosePanel, DockArea, DockPlacement, Panel, PanelEvent, PanelState, PanelStyle, PanelView, - StackPanel, ToggleZoom, + panel::PanelView, stack_panel::StackPanel, ClosePanel, DockArea, PanelEvent, PanelStyle, + ToggleZoom, }; use crate::{ button::{Button, ButtonVariants as _}, - dock::PanelInfo, + dock_area::{ + dock::DockPlacement, + panel::Panel, + state::{PanelInfo, PanelState}, + }, h_flex, popup_menu::{PopupMenu, PopupMenuExt}, tab::{tab_bar::TabBar, Tab}, @@ -613,7 +617,7 @@ impl TabPanel { this.rounded_l_none() .border_l_2() .border_r_0() - .border_color(cx.theme().base.step(cx, ColorScaleStep::TWO)) + .border_color(cx.theme().base.step(cx, ColorScaleStep::THREE)) }) .on_drop(cx.listener( move |this, drag: &DragPanel, cx| { @@ -688,7 +692,7 @@ impl TabPanel { div() .invisible() .absolute() - .bg(cx.theme().drop_target) + .bg(cx.theme().base.step(cx, ColorScaleStep::THREE)) .map(|this| match self.will_split_placement { Some(placement) => { let size = DefiniteLength::Fraction(0.35); diff --git a/crates/ui/src/dropdown.rs b/crates/ui/src/dropdown.rs index 1d8906b..d909356 100644 --- a/crates/ui/src/dropdown.rs +++ b/crates/ui/src/dropdown.rs @@ -2,7 +2,7 @@ use crate::{ h_flex, input::ClearButton, list::{self, List, ListDelegate, ListItem}, - theme::ActiveTheme, + theme::{scale::ColorScaleStep, ActiveTheme}, v_flex, Disableable, Icon, IconName, Sizable, Size, StyleSized, StyledExt, }; use gpui::{ @@ -197,7 +197,7 @@ where h_flex() .justify_center() .py_6() - .text_color(cx.theme().muted_foreground.opacity(0.6)) + .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) .child(Icon::new(IconName::Inbox).size(px(28.))) .into_any_element() } @@ -525,16 +525,18 @@ where .when_some(self.title_prefix.clone(), |this, prefix| this.child(prefix)) .child(title.clone()) } else { - div().text_color(cx.theme().accent_foreground).child( - self.placeholder - .clone() - .unwrap_or_else(|| "Please select".into()), - ) + div() + .text_color(cx.theme().accent.step(cx, ColorScaleStep::ELEVEN)) + .child( + self.placeholder + .clone() + .unwrap_or_else(|| "Please select".into()), + ) }; title.when(self.disabled, |this| { this.cursor_not_allowed() - .text_color(cx.theme().muted_foreground) + .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) }) } } @@ -602,7 +604,7 @@ where .justify_between() .bg(cx.theme().background) .border_1() - .border_color(cx.theme().input) + .border_color(cx.theme().base.step(cx, ColorScaleStep::FOUR)) .rounded(px(cx.theme().radius)) .when(cx.theme().shadow, |this| this.shadow_sm()) .map(|this| { @@ -660,8 +662,10 @@ where Icon::new(icon) .xsmall() .text_color(match self.disabled { - true => cx.theme().muted_foreground.opacity(0.5), - false => cx.theme().muted_foreground, + true => cx.theme().base.step(cx, ColorScaleStep::TEN), + false => { + cx.theme().base.step(cx, ColorScaleStep::ELEVEN) + } }) .when(self.disabled, |this| this.cursor_not_allowed()), ) @@ -692,7 +696,9 @@ where .mt_1p5() .bg(cx.theme().background) .border_1() - .border_color(cx.theme().border) + .border_color( + cx.theme().base.step(cx, ColorScaleStep::FOUR), + ) .rounded(px(cx.theme().radius)) .shadow_md() .on_mouse_down_out(|_, cx| { diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index ea0a27c..0a6a60f 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -1,4 +1,7 @@ -use crate::{theme::ActiveTheme, Sizable, Size}; +use crate::{ + theme::{scale::ColorScaleStep, ActiveTheme}, + Sizable, Size, +}; use gpui::{ prelude::FluentBuilder as _, svg, AnyElement, Hsla, IntoElement, Radians, Render, RenderOnce, SharedString, StyleRefinement, Styled, Svg, Transformation, View, VisualContext, WindowContext, @@ -313,7 +316,9 @@ impl From for AnyElement { impl Render for Icon { fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { - let text_color = self.text_color.unwrap_or_else(|| cx.theme().foreground); + let text_color = self + .text_color + .unwrap_or_else(|| cx.theme().base.step(cx, ColorScaleStep::ELEVEN)); svg() .flex_none() diff --git a/crates/ui/src/input/clear_button.rs b/crates/ui/src/input/clear_button.rs index 6de6617..373dcaa 100644 --- a/crates/ui/src/input/clear_button.rs +++ b/crates/ui/src/input/clear_button.rs @@ -1,10 +1,9 @@ -use gpui::{Styled, WindowContext}; - use crate::{ button::{Button, ButtonVariants as _}, - theme::ActiveTheme as _, + theme::{scale::ColorScaleStep, ActiveTheme as _}, Icon, IconName, Sizable as _, }; +use gpui::{Styled, WindowContext}; pub(crate) struct ClearButton {} @@ -12,7 +11,10 @@ impl ClearButton { #[allow(clippy::new_ret_no_self)] pub fn new(cx: &mut WindowContext) -> Button { Button::new("clean") - .icon(Icon::new(IconName::CircleX).text_color(cx.theme().muted_foreground)) + .icon( + Icon::new(IconName::CircleX) + .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)), + ) .ghost() .xsmall() } diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index 8efb6cb..76dea7a 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -1,5 +1,5 @@ use super::TextInput; -use crate::theme::ActiveTheme as _; +use crate::theme::{scale::ColorScaleStep, ActiveTheme as _}; use gpui::{ fill, point, px, relative, size, Bounds, Corners, Element, ElementId, ElementInputHandler, GlobalElementId, IntoElement, LayoutId, MouseButton, MouseMoveEvent, PaintQuad, Path, Pixels, @@ -144,7 +144,7 @@ impl TextElement { ), size(px(1.5), line_height), ), - cx.theme().colors.primary, + cx.theme().accent.step(cx, ColorScaleStep::NINE), )) }; } @@ -357,14 +357,17 @@ impl Element for TextElement { let mut bounds = bounds; let (display_text, text_color) = if text.is_empty() { - (placeholder, cx.theme().muted_foreground) + ( + placeholder, + cx.theme().base.step(cx, ColorScaleStep::ELEVEN), + ) } else if input.masked { ( "*".repeat(text.chars().count()).into(), - cx.theme().foreground, + cx.theme().base.step(cx, ColorScaleStep::TWELVE), ) } else { - (text, cx.theme().foreground) + (text, cx.theme().base.step(cx, ColorScaleStep::TWELVE)) }; let run = TextRun { @@ -480,7 +483,7 @@ impl Element for TextElement { // Paint selections if let Some(path) = prepaint.selection_path.take() { - cx.paint_path(path, cx.theme().selection); + cx.paint_path(path, cx.theme().accent.step(cx, ColorScaleStep::FIVE)); } // Paint multi line text diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs index 17430c6..ddef0fb 100644 --- a/crates/ui/src/input/input.rs +++ b/crates/ui/src/input/input.rs @@ -23,18 +23,15 @@ use gpui::{ // - Press Up,Down to move cursor up, down line if multi-line // - Move cursor to skip line eof empty chars. -use super::blink_cursor::BlinkCursor; -use super::change::Change; -use super::element::TextElement; -use super::ClearButton; +use super::{blink_cursor::BlinkCursor, change::Change, element::TextElement, ClearButton}; -use crate::history::History; -use crate::indicator::Indicator; -use crate::scroll::{Scrollbar, ScrollbarAxis, ScrollbarState}; -use crate::theme::ActiveTheme; -use crate::Size; -use crate::StyledExt; -use crate::{Sizable, StyleSized}; +use crate::{ + history::History, + indicator::Indicator, + scroll::{Scrollbar, ScrollbarAxis, ScrollbarState}, + theme::{scale::ColorScaleStep, ActiveTheme}, + Sizable, Size, StyleSized, StyledExt, +}; actions!( input, @@ -55,7 +52,9 @@ actions!( SelectAll, Home, End, - SelectToHome, + SelectToStartOfLine, + SelectToEndOfLine, + SelectToStart, SelectToEnd, ShowCharacterPalette, Copy, @@ -65,6 +64,8 @@ actions!( Redo, MoveToStartOfLine, MoveToEndOfLine, + MoveToStart, + MoveToEnd, TextChanged, ] ); @@ -98,16 +99,16 @@ pub fn init(cx: &mut AppContext) { KeyBinding::new("shift-down", SelectDown, Some(CONTEXT)), KeyBinding::new("home", Home, Some(CONTEXT)), KeyBinding::new("end", End, Some(CONTEXT)), - KeyBinding::new("shift-home", SelectToHome, Some(CONTEXT)), - KeyBinding::new("shift-end", SelectToEnd, Some(CONTEXT)), + KeyBinding::new("shift-home", SelectToStartOfLine, Some(CONTEXT)), + KeyBinding::new("shift-end", SelectToEndOfLine, Some(CONTEXT)), #[cfg(target_os = "macos")] - KeyBinding::new("ctrl-shift-a", SelectToHome, Some(CONTEXT)), + KeyBinding::new("ctrl-shift-a", SelectToStartOfLine, Some(CONTEXT)), #[cfg(target_os = "macos")] - KeyBinding::new("ctrl-shift-e", SelectToEnd, Some(CONTEXT)), + KeyBinding::new("ctrl-shift-e", SelectToEndOfLine, Some(CONTEXT)), #[cfg(target_os = "macos")] - KeyBinding::new("shift-cmd-left", SelectToHome, Some(CONTEXT)), + KeyBinding::new("shift-cmd-left", SelectToStartOfLine, Some(CONTEXT)), #[cfg(target_os = "macos")] - KeyBinding::new("shift-cmd-right", SelectToEnd, Some(CONTEXT)), + KeyBinding::new("shift-cmd-right", SelectToEndOfLine, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, Some(CONTEXT)), #[cfg(target_os = "macos")] @@ -138,6 +139,14 @@ pub fn init(cx: &mut AppContext) { KeyBinding::new("cmd-z", Undo, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-shift-z", Redo, Some(CONTEXT)), + #[cfg(target_os = "macos")] + KeyBinding::new("cmd-up", MoveToStart, Some(CONTEXT)), + #[cfg(target_os = "macos")] + KeyBinding::new("cmd-down", MoveToEnd, Some(CONTEXT)), + #[cfg(target_os = "macos")] + KeyBinding::new("cmd-shift-up", SelectToStart, Some(CONTEXT)), + #[cfg(target_os = "macos")] + KeyBinding::new("cmd-shift-down", SelectToEnd, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-z", Undo, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] @@ -145,7 +154,8 @@ pub fn init(cx: &mut AppContext) { ]); } -type Affixes = Option) -> AnyElement + 'static>>; +type TextInputPrefix = Option) -> AnyElement + 'static>>; +type TextInputSuffix = Option) -> AnyElement + 'static>>; type Validate = Option bool + 'static>>; pub struct TextInput { @@ -154,8 +164,8 @@ pub struct TextInput { multi_line: bool, pub(super) history: History, pub(super) blink_cursor: Model, - pub(super) prefix: Affixes, - pub(super) suffix: Affixes, + pub(super) prefix: TextInputPrefix, + pub(super) suffix: TextInputSuffix, pub(super) loading: bool, pub(super) placeholder: SharedString, pub(super) selected_range: Range, @@ -186,6 +196,8 @@ pub struct TextInput { scrollbar_state: Rc>, /// The size of the scrollable content. pub(crate) scroll_size: gpui::Size, + /// To remember the horizontal column (x-coordinate) of the cursor position. + preferred_x_offset: Option, } impl EventEmitter for TextInput {} @@ -228,6 +240,7 @@ impl TextInput { scroll_handle: ScrollHandle::new(), scrollbar_state: Rc::new(Cell::new(ScrollbarState::default())), scroll_size: gpui::size(px(0.), px(0.)), + preferred_x_offset: None, }; // Observe the blink cursor to repaint the view when it changes. @@ -258,6 +271,133 @@ impl TextInput { self } + /// Called after moving the cursor. Updates preferred_x_offset if we know where the cursor now is. + fn update_preferred_x_offset(&mut self, _cx: &mut ViewContext) { + if let (Some(lines), Some(bounds)) = (&self.last_layout, &self.last_bounds) { + let offset = self.cursor_offset(); + let line_height = self.last_line_height; + + // Find which line and sub-line the cursor is on and its position + let (_line_index, _sub_line_index, cursor_pos) = + self.line_and_position_for_offset(offset, lines, line_height); + + if let Some(pos) = cursor_pos { + // Adjust by scroll offset + let scroll_offset = bounds.origin; + self.preferred_x_offset = Some(pos.x + scroll_offset.x); + } + } + } + + /// Find which line and sub-line the given offset belongs to, along with the position within that sub-line. + fn line_and_position_for_offset( + &self, + offset: usize, + lines: &[WrappedLine], + line_height: Pixels, + ) -> (usize, usize, Option>) { + let mut prev_lines_offset = 0; + let mut y_offset = px(0.); + for (line_index, line) in lines.iter().enumerate() { + let local_offset = offset.saturating_sub(prev_lines_offset); + if let Some(pos) = line.position_for_index(local_offset, line_height) { + let sub_line_index = (pos.y.0 / line_height.0) as usize; + let adjusted_pos = point(pos.x, pos.y + y_offset); + return (line_index, sub_line_index, Some(adjusted_pos)); + } + + y_offset += line.size(line_height).height; + prev_lines_offset += line.len() + 1; + } + (0, 0, None) + } + + /// Move the cursor vertically by one line (up or down) while preserving the column if possible. + /// direction: -1 for up, +1 for down + fn move_vertical(&mut self, direction: i32, cx: &mut ViewContext) { + if self.is_single_line() { + return; + } + + let (Some(lines), Some(bounds)) = (&self.last_layout, &self.last_bounds) else { + return; + }; + + let offset = self.cursor_offset(); + let line_height = self.last_line_height; + let (current_line_index, current_sub_line, current_pos) = + self.line_and_position_for_offset(offset, lines, line_height); + + let Some(current_pos) = current_pos else { + return; + }; + + let current_x = self + .preferred_x_offset + .unwrap_or_else(|| current_pos.x + bounds.origin.x); + + let mut new_line_index = current_line_index; + let mut new_sub_line = current_sub_line as i32; + + new_sub_line += direction; + + // Handle moving above the first line + if direction == -1 && new_line_index == 0 && new_sub_line < 0 { + // Move cursor to the beginning of the text + self.move_to(0, cx); + return; + } + + if new_sub_line < 0 { + if new_line_index > 0 { + new_line_index -= 1; + new_sub_line = lines[new_line_index].wrap_boundaries.len() as i32; + } else { + new_sub_line = 0; + } + } else { + let max_sub_line = lines[new_line_index].wrap_boundaries.len() as i32; + if new_sub_line > max_sub_line { + if new_line_index < lines.len() - 1 { + new_line_index += 1; + new_sub_line = 0; + } else { + new_sub_line = max_sub_line; + } + } + } + + // If after adjustment, still at the same position, do not proceed + if new_line_index == current_line_index && new_sub_line == current_sub_line as i32 { + return; + } + + let target_line = &lines[new_line_index]; + let line_x = current_x - bounds.origin.x; + let target_sub_line = new_sub_line as usize; + + let approx_pos = point(line_x, px(target_sub_line as f32 * line_height.0)); + let index_res = target_line.index_for_position(approx_pos, line_height); + + let new_local_index = match index_res { + Ok(i) => i + 1, + Err(i) => i, + }; + + let mut prev_lines_offset = 0; + for (i, l) in lines.iter().enumerate() { + if i == new_line_index { + break; + } + prev_lines_offset += l.len() + 1; + } + + let new_offset = (prev_lines_offset + new_local_index).min(self.text.len()); + self.selected_range = new_offset..new_offset; + self.pause_blink_cursor(cx); + cx.notify(); + } + #[inline] pub(super) fn is_multi_line(&self) -> bool { self.multi_line @@ -444,9 +584,7 @@ impl TextInput { return; } self.pause_blink_cursor(cx); - - let offset = self.start_of_line(cx).saturating_sub(1); - self.move_to(offset, cx); + self.move_vertical(-1, cx); } fn down(&mut self, _: &Down, cx: &mut ViewContext) { @@ -454,9 +592,7 @@ impl TextInput { return; } self.pause_blink_cursor(cx); - - let offset = (self.end_of_line(cx) + 1).min(self.text.len()); - self.move_to(offset, cx); + self.move_vertical(1, cx); } fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext) { @@ -500,12 +636,30 @@ impl TextInput { self.move_to(offset, cx); } - fn select_to_home(&mut self, _: &SelectToHome, cx: &mut ViewContext) { + fn move_to_start(&mut self, _: &MoveToStart, cx: &mut ViewContext) { + self.move_to(0, cx); + } + + fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext) { + let end = self.text.len(); + self.move_to(end, cx); + } + + fn select_to_start(&mut self, _: &SelectToStart, cx: &mut ViewContext) { + self.select_to(0, cx); + } + + fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext) { + let end = self.text.len(); + self.select_to(end, cx); + } + + fn select_to_start_of_line(&mut self, _: &SelectToStartOfLine, cx: &mut ViewContext) { let offset = self.start_of_line(cx); self.select_to(offset, cx); } - fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext) { + fn select_to_end_of_line(&mut self, _: &SelectToEndOfLine, cx: &mut ViewContext) { let offset = self.end_of_line(cx); self.select_to(offset, cx); } @@ -594,10 +748,15 @@ impl TextInput { fn enter(&mut self, _: &Enter, cx: &mut ViewContext) { if self.is_multi_line() { + let is_eof = self.selected_range.end == self.text.len(); self.replace_text_in_range(None, "\n", cx); + // Move cursor to the start of the next line - // TODO: To be test this line is valid - self.move_to(self.next_boundary(self.cursor_offset()) - 1, cx); + let mut new_offset = self.next_boundary(self.cursor_offset()) - 1; + if is_eof { + new_offset += 1; + } + self.move_to(new_offset, cx); } cx.emit(InputEvent::PressEnter); @@ -721,6 +880,7 @@ impl TextInput { fn move_to(&mut self, offset: usize, cx: &mut ViewContext) { self.selected_range = offset..offset; self.pause_blink_cursor(cx); + self.update_preferred_x_offset(cx); cx.notify() } @@ -848,7 +1008,9 @@ impl TextInput { self.selected_range.end = word_range.end; } } - + if self.selected_range.is_empty() { + self.update_preferred_x_offset(cx); + } cx.notify() } @@ -1084,6 +1246,7 @@ impl ViewInputHandler for TextInput { self.text = pending_text; self.selected_range = range.start + new_text.len()..range.start + new_text.len(); self.marked_range.take(); + self.update_preferred_x_offset(cx); cx.emit(InputEvent::Change(self.text.clone())); cx.notify(); } @@ -1192,11 +1355,21 @@ impl Render for TextInput { .on_action(cx.listener(Self::right)) .on_action(cx.listener(Self::select_left)) .on_action(cx.listener(Self::select_right)) + .when(self.multi_line, |this| { + this.on_action(cx.listener(Self::up)) + .on_action(cx.listener(Self::down)) + .on_action(cx.listener(Self::select_up)) + .on_action(cx.listener(Self::select_down)) + }) .on_action(cx.listener(Self::select_all)) - .on_action(cx.listener(Self::select_to_home)) - .on_action(cx.listener(Self::select_to_end)) + .on_action(cx.listener(Self::select_to_start_of_line)) + .on_action(cx.listener(Self::select_to_end_of_line)) .on_action(cx.listener(Self::home)) .on_action(cx.listener(Self::end)) + .on_action(cx.listener(Self::move_to_start)) + .on_action(cx.listener(Self::move_to_end)) + .on_action(cx.listener(Self::select_to_start)) + .on_action(cx.listener(Self::select_to_end)) .on_action(cx.listener(Self::show_character_palette)) .on_action(cx.listener(Self::copy)) .on_action(cx.listener(Self::paste)) @@ -1214,21 +1387,13 @@ impl Render for TextInput { .input_h(self.size) .input_text_size(self.text_size) .cursor_text() - .when(self.multi_line, |this| { - this.on_action(cx.listener(Self::up)) - .on_action(cx.listener(Self::down)) - .on_action(cx.listener(Self::select_up)) - .on_action(cx.listener(Self::select_down)) - .h_auto() - }) + .when(self.multi_line, |this| this.h_auto()) .when(self.appearance, |this| { this.bg(if self.disabled { - cx.theme().muted + cx.theme().base.step(cx, ColorScaleStep::FOUR) } else { cx.theme().background }) - .border_color(cx.theme().input) - .border_1() .rounded(px(cx.theme().radius)) .when(cx.theme().shadow, |this| this.shadow_sm()) .when(focused, |this| this.outline(cx)) @@ -1246,7 +1411,7 @@ impl Render for TextInput { .child(TextElement::new(cx.view().clone())), ) .when(self.loading, |this| { - this.child(Indicator::new().color(cx.theme().muted_foreground)) + this.child(Indicator::new().color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))) }) .when( self.cleanable && !self.loading && !self.text.is_empty() && self.is_single_line(), diff --git a/crates/ui/src/label.rs b/crates/ui/src/label.rs deleted file mode 100644 index 23c36a2..0000000 --- a/crates/ui/src/label.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::{h_flex, theme::ActiveTheme}; -use gpui::{ - div, prelude::FluentBuilder, rems, Div, IntoElement, ParentElement, RenderOnce, SharedString, - Styled, WindowContext, -}; - -const MASKED: &str = "•"; - -#[derive(Default, PartialEq, Eq)] -pub enum TextAlign { - #[default] - Left, - Center, - Right, -} - -#[derive(IntoElement)] -pub struct Label { - base: Div, - label: SharedString, - align: TextAlign, - marked: bool, -} - -impl Label { - pub fn new(label: impl Into) -> Self { - Self { - base: h_flex().line_height(rems(1.25)), - label: label.into(), - align: TextAlign::default(), - marked: false, - } - } - - pub fn text_align(mut self, align: TextAlign) -> Self { - self.align = align; - self - } - - pub fn text_left(mut self) -> Self { - self.align = TextAlign::Left; - self - } - - pub fn text_center(mut self) -> Self { - self.align = TextAlign::Center; - self - } - - pub fn text_right(mut self) -> Self { - self.align = TextAlign::Right; - self - } - - pub fn masked(mut self, masked: bool) -> Self { - self.marked = masked; - self - } -} - -impl Styled for Label { - fn style(&mut self) -> &mut gpui::StyleRefinement { - self.base.style() - } -} - -impl RenderOnce for Label { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - let text = self.label; - - let text_display = if self.marked { - MASKED.repeat(text.chars().count()) - } else { - text.to_string() - }; - - div().text_color(cx.theme().foreground).child( - self.base - .map(|this| match self.align { - TextAlign::Left => this.justify_start(), - TextAlign::Center => this.justify_center(), - TextAlign::Right => this.justify_end(), - }) - .map(|this| { - if self.align == TextAlign::Left { - this.child(div().size_full().child(text_display)) - } else { - this.child(text_display) - } - }), - ) - } -} diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index c4f8e54..ffb09c1 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -1,17 +1,15 @@ pub mod animation; -pub mod badge; pub mod button; pub mod button_group; pub mod checkbox; pub mod clipboard; pub mod context_menu; pub mod divider; -pub mod dock; +pub mod dock_area; pub mod dropdown; pub mod history; pub mod indicator; pub mod input; -pub mod label; pub mod list; pub mod modal; pub mod notification; @@ -51,7 +49,7 @@ mod window_border; /// You can initialize the UI module at your application's entry point. pub fn init(cx: &mut gpui::AppContext) { theme::init(cx); - dock::init(cx); + dock_area::init(cx); dropdown::init(cx); input::init(cx); list::init(cx); diff --git a/crates/ui/src/list/list.rs b/crates/ui/src/list/list.rs index eb3240e..1927367 100644 --- a/crates/ui/src/list/list.rs +++ b/crates/ui/src/list/list.rs @@ -1,21 +1,17 @@ -use std::time::Duration; -use std::{cell::Cell, rc::Rc}; - -use crate::Icon; use crate::{ input::{InputEvent, TextInput}, scroll::{Scrollbar, ScrollbarState}, - theme::ActiveTheme, - v_flex, IconName, Size, + theme::{scale::ColorScaleStep, ActiveTheme}, + v_flex, Icon, IconName, Size, }; use gpui::{ - actions, div, prelude::FluentBuilder, uniform_list, AnyElement, AppContext, Entity, + actions, div, prelude::FluentBuilder, px, uniform_list, AnyElement, AppContext, Entity, FocusHandle, FocusableView, InteractiveElement, IntoElement, KeyBinding, Length, - ListSizingBehavior, MouseButton, ParentElement, Render, SharedString, Styled, Task, - UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, + ListSizingBehavior, MouseButton, ParentElement, Render, ScrollStrategy, SharedString, Styled, + Task, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, }; -use gpui::{px, ScrollStrategy}; use smol::Timer; +use std::{cell::Cell, rc::Rc, time::Duration}; actions!(list, [Cancel, Confirm, SelectPrev, SelectNext]); @@ -105,7 +101,10 @@ where let query_input = cx.new_view(|cx| { TextInput::new(cx) .appearance(false) - .prefix(|cx| Icon::new(IconName::Search).text_color(cx.theme().muted_foreground)) + .prefix(|cx| { + Icon::new(IconName::Search) + .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) + }) .placeholder("Search...") .cleanable() }); @@ -326,9 +325,9 @@ where .left(px(0.)) .right(px(0.)) .bottom(px(0.)) - .bg(cx.theme().list_active) + .bg(cx.theme().accent.step(cx, ColorScaleStep::SIX)) .border_1() - .border_color(cx.theme().list_active_border), + .border_color(cx.theme().accent.step(cx, ColorScaleStep::NINE)), ) }) }) @@ -341,7 +340,7 @@ where .right(px(0.)) .bottom(px(0.)) .border_1() - .border_color(cx.theme().list_active_border), + .border_color(cx.theme().accent.step(cx, ColorScaleStep::NINE)), ) }) .on_mouse_down( @@ -418,7 +417,7 @@ where _ => this.py_1().px_2(), }) .border_b_1() - .border_color(cx.theme().border) + .border_color(cx.theme().base.step(cx, ColorScaleStep::THREE)) .child(input), ) }) diff --git a/crates/ui/src/list/list_item.rs b/crates/ui/src/list/list_item.rs index 06c6838..aff4f7e 100644 --- a/crates/ui/src/list/list_item.rs +++ b/crates/ui/src/list/list_item.rs @@ -1,4 +1,8 @@ -use crate::{h_flex, theme::ActiveTheme, Disableable, Icon, IconName, Selectable, Sizable as _}; +use crate::{ + h_flex, + theme::{scale::ColorScaleStep, ActiveTheme}, + Disableable, Icon, IconName, Selectable, Sizable as _, +}; use gpui::{ div, prelude::FluentBuilder as _, AnyElement, ClickEvent, Div, ElementId, InteractiveElement, IntoElement, MouseButton, MouseMoveEvent, ParentElement, RenderOnce, Stateful, @@ -123,7 +127,7 @@ impl RenderOnce for ListItem { let is_active = self.selected || self.confirmed; self.base - .text_color(cx.theme().foreground) + .text_color(cx.theme().base.step(cx, ColorScaleStep::TWELVE)) .relative() .items_center() .justify_between() @@ -138,9 +142,11 @@ impl RenderOnce for ListItem { this } }) - .when(is_active, |this| this.bg(cx.theme().list_active)) + .when(is_active, |this| { + this.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE)) + }) .when(!is_active && !self.disabled, |this| { - this.hover(|this| this.bg(cx.theme().list_hover)) + this.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::TWO))) }) // Mouse enter .when_some(self.on_mouse_enter, |this, on_mouse_enter| { @@ -158,14 +164,16 @@ impl RenderOnce for ListItem { .gap_x_1() .child(div().w_full().children(self.children)) .when_some(self.check_icon, |this, icon| { - this.child( - div().w_5().items_center().justify_center().when( - self.confirmed, - |this| { - this.child(icon.small().text_color(cx.theme().muted_foreground)) - }, - ), - ) + this.child(div().w_5().items_center().justify_center().when( + self.confirmed, + |this| { + this.child( + icon.small().text_color( + cx.theme().base.step(cx, ColorScaleStep::ELEVEN), + ), + ) + }, + )) }), ) .when_some(self.suffix, |this, suffix| this.child(suffix(cx))) diff --git a/crates/ui/src/modal.rs b/crates/ui/src/modal.rs index 0907656..31ac969 100644 --- a/crates/ui/src/modal.rs +++ b/crates/ui/src/modal.rs @@ -1,3 +1,9 @@ +use crate::{ + animation::cubic_bezier, + button::{Button, ButtonVariants as _}, + theme::{scale::ColorScaleStep, ActiveTheme as _}, + v_flex, ContextModal, IconName, Sizable as _, +}; use gpui::{ actions, anchored, div, hsla, point, prelude::FluentBuilder, px, relative, Animation, AnimationExt as _, AnyElement, AppContext, Bounds, ClickEvent, Div, FocusHandle, Hsla, @@ -6,13 +12,6 @@ use gpui::{ }; use std::{rc::Rc, time::Duration}; -use crate::{ - animation::cubic_bezier, - button::{Button, ButtonVariants as _}, - theme::ActiveTheme as _, - v_flex, ContextModal, IconName, Sizable as _, -}; - actions!(modal, [Escape]); const CONTEXT: &str = "Modal"; @@ -59,7 +58,7 @@ impl Modal { let base = v_flex() .bg(cx.theme().background) .border_1() - .border_color(cx.theme().border) + .border_color(cx.theme().base.step(cx, ColorScaleStep::THREE)) .rounded_lg() .shadow_xl() .min_h_48() diff --git a/crates/ui/src/scroll/scrollbar.rs b/crates/ui/src/scroll/scrollbar.rs index b783baf..d9521ae 100644 --- a/crates/ui/src/scroll/scrollbar.rs +++ b/crates/ui/src/scroll/scrollbar.rs @@ -2,7 +2,7 @@ use gpui::*; use serde::{Deserialize, Serialize}; use std::{cell::Cell, rc::Rc, time::Instant}; -use crate::theme::ActiveTheme; +use crate::theme::{scale::ColorScaleStep, ActiveTheme}; /// Scrollbar show mode. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)] @@ -294,7 +294,7 @@ impl Scrollbar { ( cx.theme().scrollbar_thumb_hover, cx.theme().scrollbar, - cx.theme().border, + cx.theme().base.step(cx, ColorScaleStep::THREE), THUMB_INSET - px(1.), THUMB_RADIUS, ) @@ -304,7 +304,7 @@ impl Scrollbar { ( cx.theme().scrollbar_thumb_hover, cx.theme().scrollbar, - cx.theme().border, + cx.theme().base.step(cx, ColorScaleStep::THREE), THUMB_INSET - px(1.), THUMB_RADIUS, ) diff --git a/crates/ui/src/styled.rs b/crates/ui/src/styled.rs index 82f43ec..3f7a9d6 100644 --- a/crates/ui/src/styled.rs +++ b/crates/ui/src/styled.rs @@ -39,7 +39,7 @@ pub trait StyledExt: Styled + Sized { /// Render a border with a width of 1px, color ring color fn outline(self, cx: &WindowContext) -> Self { - self.border_color(cx.theme().ring) + self.border_color(cx.theme().accent.step(cx, ColorScaleStep::NINE)) } /// Wraps the element in a ScrollView. diff --git a/crates/ui/src/switch.rs b/crates/ui/src/switch.rs index 788ec27..78167c3 100644 --- a/crates/ui/src/switch.rs +++ b/crates/ui/src/switch.rs @@ -1,4 +1,8 @@ -use crate::{h_flex, theme::ActiveTheme, Disableable, Side, Sizable, Size}; +use crate::{ + h_flex, + theme::{scale::ColorScaleStep, ActiveTheme}, + Disableable, Side, Sizable, Size, +}; use gpui::{ div, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, AnyElement, Element, ElementId, GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _, @@ -105,8 +109,11 @@ impl Element for Switch { let on_click = self.on_click.clone(); let (bg, toggle_bg) = match self.checked { - true => (theme.colors.primary, theme.background), - false => (theme.input, theme.background), + true => ( + theme.accent.step(cx, ColorScaleStep::NINE), + theme.background, + ), + false => (theme.base.step(cx, ColorScaleStep::FOUR), theme.background), }; let (bg, toggle_bg) = match self.disabled { diff --git a/crates/ui/src/tab/mod.rs b/crates/ui/src/tab/mod.rs index 38a429c..db72134 100644 --- a/crates/ui/src/tab/mod.rs +++ b/crates/ui/src/tab/mod.rs @@ -1,3 +1,4 @@ +use crate::theme::scale::ColorScaleStep; use crate::theme::ActiveTheme; use crate::Selectable; use gpui::prelude::FluentBuilder; @@ -28,7 +29,7 @@ impl Tab { Self { id: id.clone(), - base: div().id(id).gap_1().py_1p5().px_3().h(px(30.)), + base: div().id(id), label: label.into_any_element(), metadata, disabled: false, @@ -85,30 +86,58 @@ impl Styled for Tab { impl RenderOnce for Tab { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let (text_color, bg_color) = match (self.selected, self.disabled) { - (true, false) => (cx.theme().tab_active_foreground, cx.theme().tab_active), - (false, false) => (cx.theme().muted_foreground, cx.theme().tab), + (true, false) => ( + cx.theme().base.step(cx, ColorScaleStep::TWELVE), + cx.theme().background, + ), + (false, false) => ( + cx.theme().base.step(cx, ColorScaleStep::ELEVEN), + cx.theme().base.step(cx, ColorScaleStep::TWO), + ), // disabled - (true, true) => (cx.theme().muted_foreground, cx.theme().tab_active), - (false, true) => (cx.theme().muted_foreground, cx.theme().tab), + (true, true) => ( + cx.theme().base.step(cx, ColorScaleStep::ELEVEN), + cx.theme().base.step(cx, ColorScaleStep::TWO), + ), + (false, true) => ( + cx.theme().base.step(cx, ColorScaleStep::ELEVEN), + cx.theme().base.step(cx, ColorScaleStep::TWO), + ), }; self.base + .h(px(30.)) + .relative() .flex() .items_center() .flex_shrink_0() .cursor_pointer() .overflow_hidden() + .text_sm() .text_color(text_color) .bg(bg_color) .border_x_1() .border_color(cx.theme().transparent) - .when(self.selected, |this| this.border_color(cx.theme().border)) - .text_sm() + .when(self.selected, |this| { + this.border_color(cx.theme().base.step(cx, ColorScaleStep::THREE)) + }) + .when(!self.selected, |this| { + this.child( + div() + .absolute() + .left_0() + .bottom_0() + .size_full() + .border_b_1() + .border_color(cx.theme().base.step(cx, ColorScaleStep::THREE)), + ) + }) .when_some(self.prefix, |this, prefix| { this.child(prefix).text_color(text_color) }) .child( div() + .px_3() .flex() .items_center() .gap_1() diff --git a/crates/ui/src/theme/mod.rs b/crates/ui/src/theme/mod.rs index 8c4ad09..4f8dbe4 100644 --- a/crates/ui/src/theme/mod.rs +++ b/crates/ui/src/theme/mod.rs @@ -1,8 +1,8 @@ use crate::scroll::ScrollbarShow; use colors::{default_color_scales, hsl}; use gpui::{ - blue, hsla, transparent_black, AppContext, Global, Hsla, ModelContext, SharedString, - ViewContext, WindowAppearance, WindowContext, + AppContext, Global, Hsla, ModelContext, SharedString, ViewContext, WindowAppearance, + WindowContext, }; use scale::ColorScaleSet; use std::ops::{Deref, DerefMut}; @@ -13,249 +13,33 @@ pub mod scale; #[derive(Debug, Clone, Copy, Default)] pub struct ThemeColors { pub background: Hsla, - pub border: Hsla, - pub window_border: Hsla, - pub accent: Hsla, - pub accent_foreground: 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 transparent: 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 window_border: Hsla, } impl ThemeColors { pub fn light() -> Self { Self { background: hsl(0.0, 0.0, 100.), - accent: hsl(240.0, 5.0, 96.0), - accent_foreground: hsl(240.0, 5.9, 10.0), - border: hsl(240.0, 5.9, 90.0), + transparent: Hsla::transparent_black(), 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), } } pub fn dark() -> Self { Self { background: hsl(0.0, 0.0, 8.0), - accent: hsl(240.0, 3.7, 15.9), - accent_foreground: hsl(0.0, 0.0, 78.0), - border: hsl(240.0, 3.7, 16.9), + transparent: Hsla::transparent_black(), 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), - } - } -} - -pub trait Colorize { - fn opacity(&self, opacity: f32) -> Hsla; - fn divide(&self, divisor: f32) -> Hsla; - fn invert(&self) -> Hsla; - fn invert_l(&self) -> Hsla; - fn lighten(&self, amount: f32) -> Hsla; - fn darken(&self, amount: f32) -> Hsla; - fn apply(&self, base_color: Hsla) -> Hsla; -} - -impl Colorize for Hsla { - /// Returns a new color with the given opacity. - /// - /// The opacity is a value between 0.0 and 1.0, where 0.0 is fully transparent and 1.0 is fully opaque. - fn opacity(&self, factor: f32) -> Hsla { - Hsla { - a: self.a * factor.clamp(0.0, 1.0), - ..*self - } - } - - /// Returns a new color with each channel divided by the given divisor. - /// - /// The divisor in range of 0.0 .. 1.0 - fn divide(&self, divisor: f32) -> Hsla { - Hsla { - a: divisor, - ..*self - } - } - - /// Return inverted color - fn invert(&self) -> Hsla { - Hsla { - h: (self.h + 1.8) % 3.6, - s: 1.0 - self.s, - l: 1.0 - self.l, - a: self.a, - } - } - - /// Return inverted lightness - fn invert_l(&self) -> Hsla { - Hsla { - l: 1.0 - self.l, - ..*self - } - } - - /// Return a new color with the lightness increased by the given factor. - /// - /// factor range: 0.0 .. 1.0 - fn lighten(&self, factor: f32) -> Hsla { - let l = self.l * (1.0 + factor.clamp(0.0, 1.0)); - - Hsla { l, ..*self } - } - - /// Return a new color with the darkness increased by the given factor. - /// - /// factor range: 0.0 .. 1.0 - fn darken(&self, factor: f32) -> Hsla { - let l = self.l * (1.0 - factor.clamp(0.0, 1.0)); - - Hsla { l, ..*self } - } - - /// Return a new color with the same lightness and alpha but different hue and saturation. - fn apply(&self, new_color: Hsla) -> Hsla { - Hsla { - h: new_color.h, - s: new_color.s, - l: self.l, - a: self.a, } } } @@ -293,7 +77,7 @@ pub fn init(cx: &mut AppContext) { } pub struct Theme { - pub colors: ThemeColors, + colors: ThemeColors, /// Base colors. pub base: ColorScaleSet, /// Accent colors. @@ -304,7 +88,6 @@ pub struct Theme { pub font_size: f32, pub radius: f32, pub shadow: bool, - pub transparent: Hsla, /// Show the scrollbar mode, default: Scrolling pub scrollbar_show: ScrollbarShow, } @@ -370,7 +153,6 @@ impl From for Theme { base: color_scales.gray, accent: color_scales.yellow, appearance: Appearance::default(), - transparent: Hsla::transparent_black(), font_size: 16.0, font_family: if cfg!(target_os = "macos") { ".SystemUIFont".into()