diff --git a/crates/app/src/story_workspace.rs b/crates/app/src/story_workspace.rs index 338264de..5a69c4b0 100644 --- a/crates/app/src/story_workspace.rs +++ b/crates/app/src/story_workspace.rs @@ -23,6 +23,7 @@ pub fn init(_app_state: Arc, cx: &mut AppContext) { Theme::init(cx); ui::init(cx); + story::init(cx); } pub struct StoryWorkspace { diff --git a/crates/story/src/button_story.rs b/crates/story/src/button_story.rs index 31535e37..e0221be1 100644 --- a/crates/story/src/button_story.rs +++ b/crates/story/src/button_story.rs @@ -34,19 +34,19 @@ impl Render for ButtonStory { .child( section("Normal Button", cx) .child( - Button::new("button-1") + Button::new("button-1", cx) .label("Primary Button") .style(ButtonStyle::Primary) .on_click(Self::on_click), ) .child( - Button::new("button-2") + Button::new("button-2", cx) .label("Secondary Button") .style(ButtonStyle::Secondary) .on_click(Self::on_click), ) .child( - Button::new("button-4") + Button::new("button-4", cx) .label("Danger Button") .style(ButtonStyle::Danger) .on_click(Self::on_click), @@ -55,21 +55,21 @@ impl Render for ButtonStory { .child( section("Button with Icon", cx) .child( - Button::new("button-icon-1") + Button::new("button-icon-1", cx) .label("Confirm") .icon(IconName::Check) .style(ButtonStyle::Primary) .on_click(Self::on_click), ) .child( - Button::new("button-icon-2") + Button::new("button-icon-2", cx) .label("Abort") .icon(IconName::Close) .style(ButtonStyle::Secondary) .on_click(Self::on_click), ) .child( - Button::new("button-icon-3") + Button::new("button-icon-3", cx) .label("Maximize") .icon(Icon::new(IconName::Maximize)) .style(ButtonStyle::Secondary) @@ -83,21 +83,21 @@ impl Render for ButtonStory { .child( section("Small Size", cx) .child( - Button::new("button-6") + Button::new("button-6", cx) .label("Primary Button") .style(ButtonStyle::Primary) .size(ButtonSize::Small) .on_click(Self::on_click), ) .child( - Button::new("button-7") + Button::new("button-7", cx) .label("Secondary Button") .style(ButtonStyle::Secondary) .size(ButtonSize::Small) .on_click(Self::on_click), ) .child( - Button::new("button-8") + Button::new("button-8", cx) .label("Danger Button") .style(ButtonStyle::Danger) .size(ButtonSize::Small) @@ -107,21 +107,21 @@ impl Render for ButtonStory { .child( section("XSmall Size", cx) .child( - Button::new("button-xs-1") + Button::new("button-xs-1", cx) .label("Primary Button") .style(ButtonStyle::Primary) .size(ButtonSize::XSmall) .on_click(Self::on_click), ) .child( - Button::new("button-xs-2") + Button::new("button-xs-2", cx) .label("Secondary Button") .style(ButtonStyle::Secondary) .size(ButtonSize::XSmall) .on_click(Self::on_click), ) .child( - Button::new("button-xs-3") + Button::new("button-xs-3", cx) .label("Danger Button") .style(ButtonStyle::Danger) .size(ButtonSize::XSmall) @@ -135,21 +135,21 @@ impl Render for ButtonStory { .child( section("Disabled Button", cx) .child( - Button::new("button-disabled1") + Button::new("button-disabled1", cx) .label("Disabled Button") .style(ButtonStyle::Primary) .on_click(Self::on_click) .disabled(true), ) .child( - Button::new("button-disabled1") + Button::new("button-disabled1", cx) .label("Disabled Button") .style(ButtonStyle::Secondary) .on_click(Self::on_click) .disabled(true), ) .child( - Button::new("button-disabled1") + Button::new("button-disabled1", cx) .label("Disabled Button") .style(ButtonStyle::Danger) .on_click(Self::on_click) @@ -159,19 +159,19 @@ impl Render for ButtonStory { .child( section("Selected Style", cx) .child( - Button::new("button-selected-1") + Button::new("button-selected-1", cx) .label("Selected Button") .style(ButtonStyle::Primary) .selected(true), ) .child( - Button::new("button-selected-2") + Button::new("button-selected-2", cx) .label("Selected Button") .style(ButtonStyle::Secondary) .selected(true), ) .child( - Button::new("button-selected-3") + Button::new("button-selected-3", cx) .label("Selected Button") .style(ButtonStyle::Danger) .selected(true), @@ -181,46 +181,46 @@ impl Render for ButtonStory { .child( section("Icon Button", cx) .child( - Button::new("icon-button-0") + Button::new("icon-button-0", cx) .icon(IconName::Search) .style(ButtonStyle::Primary), ) - .child(Button::new("icon-button-1").icon(IconName::Info)) + .child(Button::new("icon-button-1", cx).icon(IconName::Info)) .child( - Button::new("icon-button-2") + Button::new("icon-button-2", cx) .icon(IconName::Close) .style(ButtonStyle::Danger), ) .child( - Button::new("icon-button-3") + Button::new("icon-button-3", cx) .icon(IconName::Search) .size(ButtonSize::Small) .style(ButtonStyle::Primary), ) .child( - Button::new("icon-button-4") + Button::new("icon-button-4", cx) .icon(IconName::Info) .size(ButtonSize::Small), ) .child( - Button::new("icon-button-5") + Button::new("icon-button-5", cx) .icon(IconName::Close) .size(ButtonSize::Small) .style(ButtonStyle::Danger), ) .child( - Button::new("icon-button-6") + Button::new("icon-button-6", cx) .icon(IconName::Search) .size(ButtonSize::XSmall) .style(ButtonStyle::Primary), ) .child( - Button::new("icon-button-7") + Button::new("icon-button-7", cx) .icon(IconName::Info) .size(ButtonSize::XSmall), ) .child( - Button::new("icon-button-8") + Button::new("icon-button-8", cx) .icon(IconName::Close) .size(ButtonSize::XSmall) .style(ButtonStyle::Danger), diff --git a/crates/story/src/input_story.rs b/crates/story/src/input_story.rs index 7e30fc8b..912e2f26 100644 --- a/crates/story/src/input_story.rs +++ b/crates/story/src/input_story.rs @@ -1,13 +1,24 @@ use gpui::{ - ClickEvent, IntoElement, ParentElement as _, Render, Styled as _, View, ViewContext, - VisualContext, WindowContext, + actions, AppContext, ClickEvent, FocusHandle, FocusableView, InteractiveElement, IntoElement, + KeyBinding, ParentElement as _, Render, Styled, View, ViewContext, VisualContext, + WindowContext, }; -use ui::{input::TextInput, v_flex, IconName}; +use ui::{button::Button, h_flex, input::TextInput, v_flex, FocusableCycle, IconName}; use crate::section; +actions!(input_story, [Tab, TabPrev]); + +pub fn init(cx: &mut AppContext) { + cx.bind_keys([ + KeyBinding::new("shift-tab", TabPrev, Some("InputStory")), + KeyBinding::new("tab", Tab, Some("InputStory")), + ]) +} + pub struct InputStory { + focus_handle: FocusHandle, input1: View, input2: View, mash_input: View, @@ -54,6 +65,7 @@ impl InputStory { }); Self { + focus_handle: cx.focus_handle(), input1, input2: cx.new_view(|cx| { let mut input = TextInput::new(cx); @@ -77,11 +89,46 @@ impl InputStory { fn on_change(ev: &ClickEvent, cx: &mut WindowContext) { println!("Input changed: {:?}", ev); } + + fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { + self.cycle_focus(true, cx); + } + + fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext) { + self.cycle_focus(false, cx); + } +} + +impl FocusableCycle for InputStory { + fn cycle_focus_handles(&self, cx: &mut ViewContext) -> Vec { + [ + &self.input1, + &self.input2, + &self.disabled_input, + &self.mash_input, + &self.prefix_input1, + &self.both_input1, + &self.suffix_input1, + ] + .iter() + .map(|v| v.focus_handle(cx)) + .collect() + } +} + +impl FocusableView for InputStory { + fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle { + self.focus_handle.clone() + } } impl Render for InputStory { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { v_flex() + .key_context("InputStory") + .track_focus(&self.focus_handle) + .on_action(cx.listener(Self::tab)) + .on_action(cx.listener(Self::tab_prev)) .size_full() .p_4() .justify_start() @@ -102,5 +149,23 @@ impl Render for InputStory { .child(self.both_input1.clone()) .child(self.suffix_input1.clone()), ) + .child( + h_flex() + .items_center() + .w_full() + .gap_3() + .child( + Button::new("btn-submit", cx) + .w_full() + .style(ui::button::ButtonStyle::Primary) + .label("Submit"), + ) + .child( + Button::new("btn-cancel", cx) + .w_full() + .label("Cancel") + .into_element(), + ), + ) } } diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs index 4a05cea1..d7d2b68f 100644 --- a/crates/story/src/lib.rs +++ b/crates/story/src/lib.rs @@ -21,10 +21,10 @@ pub use switch_story::SwitchStory; pub use tooltip_story::TooltipStory; use gpui::{ - div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Div, Element, - EventEmitter, FocusableView, InteractiveElement, IntoElement, ParentElement, Pixels, Render, - ScrollHandle, SharedString, StatefulInteractiveElement, Styled as _, Task, View, ViewContext, - VisualContext, WindowContext, + div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Div, EventEmitter, + FocusableView, InteractiveElement, IntoElement, ParentElement, Pixels, Render, ScrollHandle, + SharedString, StatefulInteractiveElement, Styled as _, Task, View, ViewContext, VisualContext, + WindowContext, }; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -33,7 +33,11 @@ use workspace::{ }; use anyhow::Result; -use ui::{divider::Divider, h_flex, label::Label, v_flex, StyledExt}; +use ui::{divider::Divider, h_flex, label::Label, v_flex}; + +pub fn init(cx: &mut AppContext) { + input_story::init(cx); +} pub fn story_case( name: &'static str, @@ -286,7 +290,6 @@ impl Panel for MyPanel { } fn set_size(&mut self, size: Option, cx: &mut ViewContext) { - dbg!("================ set_size", size); if let Some(size) = size { self.update_size(size) } diff --git a/crates/story/src/list_story.rs b/crates/story/src/list_story.rs index 91d93b27..163a2ee1 100644 --- a/crates/story/src/list_story.rs +++ b/crates/story/src/list_story.rs @@ -2,9 +2,9 @@ use core::time; use fake::Fake; use gpui::{ - actions, div, prelude::FluentBuilder as _, px, ElementId, FocusHandle, FocusableView, - InteractiveElement, IntoElement, ParentElement, Render, RenderOnce, Styled, Timer, View, - ViewContext, VisualContext, WindowContext, + actions, div, px, ElementId, FocusHandle, FocusableView, InteractiveElement, IntoElement, + ParentElement, Render, RenderOnce, Styled, Timer, View, ViewContext, VisualContext, + WindowContext, }; use ui::{ diff --git a/crates/story/src/picker_story.rs b/crates/story/src/picker_story.rs index 63c86f10..e8beeb9f 100644 --- a/crates/story/src/picker_story.rs +++ b/crates/story/src/picker_story.rs @@ -197,7 +197,7 @@ impl Render for PickerStory { .gap_6() .child( v_flex().items_start().child( - Button::new("show-picker") + Button::new("show-picker", cx) .label("Show Picker...") .icon(IconName::Search) .style(ButtonStyle::Primary) diff --git a/crates/story/src/popover_story.rs b/crates/story/src/popover_story.rs index 9ad1fe42..5009da0e 100644 --- a/crates/story/src/popover_story.rs +++ b/crates/story/src/popover_story.rs @@ -40,7 +40,7 @@ impl Render for Form { .child("This is a form container.") .child(self.input1.clone()) .child( - Button::primary("submit", "Submit") + Button::primary("submit", "Submit", cx) .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))), ) } @@ -62,7 +62,7 @@ impl PopoverStory { } impl Render for PopoverStory { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let form = self.form.clone(); v_flex() @@ -76,15 +76,15 @@ impl Render for PopoverStory { .child( v_flex().gap_4().child( Popover::new("info-top-left") - .trigger(Button::new("info-top-left").label("Top Left")) + .trigger(Button::new("info-top-left", cx).label("Top Left")) .content(|cx| { - PopoverContent::new(cx, |_| { + PopoverContent::new(cx, |cx| { v_flex() .gap_4() .child("Hello, this is a Popover.") .child(Divider::horizontal()) .child( - Button::new("info1") + Button::new("info1", cx) .label("Yes") .width(px(80.)) .size(ButtonSize::Small), @@ -97,15 +97,15 @@ impl Render for PopoverStory { .child( Popover::new("info-top-right") .anchor(AnchorCorner::TopRight) - .trigger(Button::new("info-top-right").label("Top Right")) + .trigger(Button::new("info-top-right", cx).label("Top Right")) .content(|cx| { - PopoverContent::new(cx, |_| { + PopoverContent::new(cx, |cx| { v_flex() .gap_4() .child("Hello, this is a Popover on the Top Right.") .child(Divider::horizontal()) .child( - Button::new("info1") + Button::new("info1", cx) .label("Yes") .width(px(80.)) .size(ButtonSize::Small), @@ -124,7 +124,9 @@ impl Render for PopoverStory { Popover::new("info-bottom-left") .anchor(AnchorCorner::BottomLeft) .trigger( - Button::new("pop").label("Popup with Form").width(px(300.)), + Button::new("pop", cx) + .label("Popup with Form") + .width(px(300.)), ) .content(move |_| form.clone()), ) @@ -133,18 +135,18 @@ impl Render for PopoverStory { .anchor(AnchorCorner::BottomRight) .mouse_button(MouseButton::Right) .trigger( - Button::new("pop") + Button::new("pop", cx) .label("Mouse Right Click") .width(px(300.)), ) .content(|cx| { - PopoverContent::new(cx, |_| { + PopoverContent::new(cx, |cx| { v_flex() .gap_4() .child("Hello, this is a Popover on the Bottom Right.") .child(Divider::horizontal()) .child( - Button::new("info1") + Button::new("info1", cx) .label("Yes") .width(px(80.)) .size(ButtonSize::Small), diff --git a/crates/story/src/tooltip_story.rs b/crates/story/src/tooltip_story.rs index 3ae98465..c1dc6216 100644 --- a/crates/story/src/tooltip_story.rs +++ b/crates/story/src/tooltip_story.rs @@ -1,5 +1,5 @@ use gpui::{ - div, px, CursorStyle, InteractiveElement, ParentElement, Render, StatefulInteractiveElement, + div, CursorStyle, InteractiveElement, ParentElement, Render, StatefulInteractiveElement, Styled, View, VisualContext as _, WindowContext, }; @@ -25,7 +25,7 @@ impl TooltipStory { } impl Render for TooltipStory { - fn render(&mut self, _cx: &mut gpui::ViewContext) -> impl gpui::IntoElement { + fn render(&mut self, cx: &mut gpui::ViewContext) -> impl gpui::IntoElement { v_flex() .p_4() .gap_5() @@ -33,7 +33,7 @@ impl Render for TooltipStory { div() .cursor(CursorStyle::PointingHand) .child( - Button::new("button") + Button::new("button", cx) .label("Hover me") .style(ButtonStyle::Primary), ) diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index 73f0521f..1c308ba7 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -1,12 +1,13 @@ use crate::{ h_flex, theme::{ActiveTheme, Colorize as _, ThemeMode}, - Clickable, Disableable, Icon, Selectable, + Clickable, Disableable, Icon, Selectable, StyledExt, }; use gpui::{ - div, prelude::FluentBuilder as _, px, ClickEvent, DefiniteLength, Div, ElementId, Hsla, - InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString, - StatefulInteractiveElement as _, Styled, WindowContext, + div, prelude::FluentBuilder as _, px, ClickEvent, DefiniteLength, Div, ElementId, FocusHandle, + Focusable, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Render, + RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, View, ViewContext, + VisualContext, WindowContext, }; pub enum ButtonRounded { @@ -34,6 +35,7 @@ pub enum ButtonStyle { pub struct Button { pub base: Div, id: ElementId, + focus_handle: FocusHandle, icon: Option, label: Option, disabled: bool, @@ -48,9 +50,10 @@ pub struct Button { } impl Button { - pub fn new(id: impl Into) -> Self { + pub fn new(id: impl Into, cx: &mut WindowContext) -> Self { Self { base: div(), + focus_handle: cx.focus_handle(), id: id.into(), icon: None, label: None, @@ -66,16 +69,28 @@ impl Button { } } - pub fn primary(id: impl Into, label: impl Into) -> Self { - Self::new(id).label(label).style(ButtonStyle::Primary) + pub fn primary( + id: impl Into, + label: impl Into, + cx: &mut WindowContext, + ) -> Self { + Self::new(id, cx).label(label).style(ButtonStyle::Primary) } - pub fn danger(id: impl Into, label: impl Into) -> Self { - Self::new(id).label(label).style(ButtonStyle::Danger) + pub fn danger( + id: impl Into, + label: impl Into, + cx: &mut WindowContext, + ) -> Self { + Self::new(id, cx).label(label).style(ButtonStyle::Danger) } - pub fn small(id: impl Into, label: impl Into) -> Self { - Self::new(id).label(label).size(ButtonSize::Small) + pub fn small( + id: impl Into, + label: impl Into, + cx: &mut WindowContext, + ) -> Self { + Self::new(id, cx).label(label).size(ButtonSize::Small) } pub fn width(mut self, width: impl Into) -> Self { @@ -141,14 +156,22 @@ impl Clickable for Button { } } +impl Styled for Button { + fn style(&mut self) -> &mut gpui::StyleRefinement { + self.base.style() + } +} + impl RenderOnce for Button { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let theme = cx.theme(); let style: ButtonStyle = self.style; let normal_style = style.normal(cx); + let focused = self.focus_handle.is_focused(cx); self.base .id(self.id) + .track_focus(&self.focus_handle) .flex() .items_center() .justify_center() @@ -192,6 +215,9 @@ impl RenderOnce for Button { .border_color(normal_style.border) .bg(normal_style.bg) }) + .when(focused, |this| { + this.border_color(cx.theme().ring).debug_blue() + }) .when_some( self.on_click.filter(|_| !self.disabled), |this, on_click| { diff --git a/crates/ui/src/dropdown.rs b/crates/ui/src/dropdown.rs index b1fe8043..24e4b066 100644 --- a/crates/ui/src/dropdown.rs +++ b/crates/ui/src/dropdown.rs @@ -23,7 +23,7 @@ use crate::{ list::ListItem, picker::{self, Picker, PickerDelegate}, theme::ActiveTheme, - Icon, IconName, + Icon, IconName, StyledExt, }; /// A trait for items that can be displayed in a dropdown. @@ -221,13 +221,12 @@ where D: DropdownDelegate + 'static, { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let group_id = format!("dropdown-group:{}", self.id); let title = self.title.clone().unwrap_or_else(|| "Select...".into()); let focused = self.focus_handle.is_focused(cx); div() .key_context("Dropdown") - .group(group_id.clone()) + .group(format!("dropdown-group:{}", self.id)) .track_focus(&self.focus_handle) .on_action(cx.listener(Self::up)) .on_action(cx.listener(Self::down)) @@ -248,9 +247,9 @@ where .border_color(cx.theme().input) .rounded(px(cx.theme().radius)) .shadow_sm() + .when(focused, |this| this.outline(cx)) .px_3() .py_2() - .when(focused, |this| this.border_color(cx.theme().ring)) .on_click(cx.listener(|this, _, cx| { this.open = !this.open; cx.notify(); @@ -280,20 +279,12 @@ struct DropdownMenuElement { dropdown: View>, } +#[derive(Default)] struct DropdownMenuElementState { menu_element: Option, layout_id: Option, } -impl Default for DropdownMenuElementState { - fn default() -> Self { - Self { - menu_element: None, - layout_id: None, - } - } -} - impl IntoElement for DropdownMenuElement where D: DropdownDelegate + 'static, diff --git a/crates/ui/src/focusable.rs b/crates/ui/src/focusable.rs new file mode 100644 index 00000000..70b0cd96 --- /dev/null +++ b/crates/ui/src/focusable.rs @@ -0,0 +1,40 @@ +use gpui::{FocusHandle, ViewContext}; + +/// A trait for views that can cycle focus between its children. +/// +/// This will provide a default implementation for the `cycle_focus` method that will cycle focus. +/// +/// You should implement the `cycle_focus_handles` method to return a list of focus handles that +/// should be cycled, and the cycle will follow the order of the list. +pub trait FocusableCycle { + /// Returns a list of focus handles that should be cycled. + fn cycle_focus_handles(&self, cx: &mut ViewContext) -> Vec + where + Self: Sized; + + /// Cycles focus between the focus handles returned by `cycle_focus_handles`. + /// If `is_next` is `true`, it will cycle to the next focus handle, otherwise it will cycle to prev. + fn cycle_focus(&self, is_next: bool, cx: &mut ViewContext) + where + Self: Sized, + { + let focused_handle = cx.focused(); + let handles = self.cycle_focus_handles(cx); + let handles = if is_next { + handles + } else { + handles.into_iter().rev().collect() + }; + + let fallback_handle = handles[0].clone(); + let target_focus_handle = handles + .into_iter() + .skip_while(|handle| Some(handle) != focused_handle.as_ref()) + .skip(1) + .next() + .unwrap_or(fallback_handle); + + target_focus_handle.focus(cx); + cx.stop_propagation(); + } +} diff --git a/crates/ui/src/input.rs b/crates/ui/src/input.rs index 6fbc386e..f74a0798 100644 --- a/crates/ui/src/input.rs +++ b/crates/ui/src/input.rs @@ -2,6 +2,7 @@ use std::ops::Range; use crate::event::InterativeElementExt as _; use crate::theme::ActiveTheme; +use crate::StyledExt as _; use blink_cursor::BlinkCursor; use gpui::*; use prelude::FluentBuilder as _; @@ -679,14 +680,11 @@ impl Render for TextInput { .h_10() .when(self.appearance, |this| { this.bg(cx.theme().input) - .border_color(if focused { - cx.theme().ring - } else { - cx.theme().input - }) + .border_color(cx.theme().input) .border_1() .rounded(px(cx.theme().radius)) .shadow_sm() + .when(focused, |this| this.outline(cx)) .px_3() .bg(if self.disabled { cx.theme().muted diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index a7444b9e..fd4d1ff7 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -1,6 +1,7 @@ mod clickable; mod disableable; mod event; +mod focusable; mod icon; mod scrollbar; mod selectable; @@ -28,6 +29,7 @@ pub mod tooltip; pub use clickable::Clickable; pub use disableable::Disableable; pub use event::InterativeElementExt; +pub use focusable::FocusableCycle; pub use selectable::{Selectable, Selection}; pub use icon::*; diff --git a/crates/ui/src/styled_ext.rs b/crates/ui/src/styled_ext.rs index 6cf03dec..e4bb783e 100644 --- a/crates/ui/src/styled_ext.rs +++ b/crates/ui/src/styled_ext.rs @@ -1,5 +1,5 @@ use crate::theme::{hsl, ActiveTheme}; -use gpui::{hsla, point, px, BoxShadow, Styled, WindowContext}; +use gpui::{div, hsla, point, px, BoxShadow, Styled, WindowContext}; use smallvec::{smallvec, SmallVec}; pub enum ElevationIndex { @@ -96,6 +96,18 @@ pub trait StyledExt: Styled + Sized { fn debug_pink(self) -> Self { self.border_1().border_color(hsl(300., 100., 47.)) } + + /// Render a border with a width of 1px, color ring color + /// + /// Please ensure this after the shadow + fn outline(self, cx: &WindowContext) -> Self { + self.shadow(smallvec![BoxShadow { + color: cx.theme().ring, + offset: point(px(0.), px(0.)), + blur_radius: px(0.1), + spread_radius: px(1.), + }]) + } } impl StyledExt for E {} diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index f59115f9..15e9c51d 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -167,7 +167,7 @@ impl Colors { accent_foreground: hsl(240.0, 5.9, 10.0), border: hsl(240.0, 5.9, 90.0), input: hsl(240.0, 5.9, 90.0), - ring: hsl(240.0, 5.9, 10.0), + ring: hsl(240.0, 5.9, 65.0), selection: hsl(211.0, 97.0, 85.0), scrollbar: Hsla::transparent_black(), scrollbar_thumb: hsl(240.0, 5.9, 90.0).opacity(0.7),