Improve focus (#14)

- Add `FocusableCycle` trait to help impl cycle focus.
- Add `outline` style to StyledExt.
This commit is contained in:
Jason Lee 2024-07-04 14:05:14 +08:00 committed by GitHub
parent 24348ec874
commit 3ccadf6cc3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 226 additions and 86 deletions

View file

@ -23,6 +23,7 @@ pub fn init(_app_state: Arc<AppState>, cx: &mut AppContext) {
Theme::init(cx); Theme::init(cx);
ui::init(cx); ui::init(cx);
story::init(cx);
} }
pub struct StoryWorkspace { pub struct StoryWorkspace {

View file

@ -34,19 +34,19 @@ impl Render for ButtonStory {
.child( .child(
section("Normal Button", cx) section("Normal Button", cx)
.child( .child(
Button::new("button-1") Button::new("button-1", cx)
.label("Primary Button") .label("Primary Button")
.style(ButtonStyle::Primary) .style(ButtonStyle::Primary)
.on_click(Self::on_click), .on_click(Self::on_click),
) )
.child( .child(
Button::new("button-2") Button::new("button-2", cx)
.label("Secondary Button") .label("Secondary Button")
.style(ButtonStyle::Secondary) .style(ButtonStyle::Secondary)
.on_click(Self::on_click), .on_click(Self::on_click),
) )
.child( .child(
Button::new("button-4") Button::new("button-4", cx)
.label("Danger Button") .label("Danger Button")
.style(ButtonStyle::Danger) .style(ButtonStyle::Danger)
.on_click(Self::on_click), .on_click(Self::on_click),
@ -55,21 +55,21 @@ impl Render for ButtonStory {
.child( .child(
section("Button with Icon", cx) section("Button with Icon", cx)
.child( .child(
Button::new("button-icon-1") Button::new("button-icon-1", cx)
.label("Confirm") .label("Confirm")
.icon(IconName::Check) .icon(IconName::Check)
.style(ButtonStyle::Primary) .style(ButtonStyle::Primary)
.on_click(Self::on_click), .on_click(Self::on_click),
) )
.child( .child(
Button::new("button-icon-2") Button::new("button-icon-2", cx)
.label("Abort") .label("Abort")
.icon(IconName::Close) .icon(IconName::Close)
.style(ButtonStyle::Secondary) .style(ButtonStyle::Secondary)
.on_click(Self::on_click), .on_click(Self::on_click),
) )
.child( .child(
Button::new("button-icon-3") Button::new("button-icon-3", cx)
.label("Maximize") .label("Maximize")
.icon(Icon::new(IconName::Maximize)) .icon(Icon::new(IconName::Maximize))
.style(ButtonStyle::Secondary) .style(ButtonStyle::Secondary)
@ -83,21 +83,21 @@ impl Render for ButtonStory {
.child( .child(
section("Small Size", cx) section("Small Size", cx)
.child( .child(
Button::new("button-6") Button::new("button-6", cx)
.label("Primary Button") .label("Primary Button")
.style(ButtonStyle::Primary) .style(ButtonStyle::Primary)
.size(ButtonSize::Small) .size(ButtonSize::Small)
.on_click(Self::on_click), .on_click(Self::on_click),
) )
.child( .child(
Button::new("button-7") Button::new("button-7", cx)
.label("Secondary Button") .label("Secondary Button")
.style(ButtonStyle::Secondary) .style(ButtonStyle::Secondary)
.size(ButtonSize::Small) .size(ButtonSize::Small)
.on_click(Self::on_click), .on_click(Self::on_click),
) )
.child( .child(
Button::new("button-8") Button::new("button-8", cx)
.label("Danger Button") .label("Danger Button")
.style(ButtonStyle::Danger) .style(ButtonStyle::Danger)
.size(ButtonSize::Small) .size(ButtonSize::Small)
@ -107,21 +107,21 @@ impl Render for ButtonStory {
.child( .child(
section("XSmall Size", cx) section("XSmall Size", cx)
.child( .child(
Button::new("button-xs-1") Button::new("button-xs-1", cx)
.label("Primary Button") .label("Primary Button")
.style(ButtonStyle::Primary) .style(ButtonStyle::Primary)
.size(ButtonSize::XSmall) .size(ButtonSize::XSmall)
.on_click(Self::on_click), .on_click(Self::on_click),
) )
.child( .child(
Button::new("button-xs-2") Button::new("button-xs-2", cx)
.label("Secondary Button") .label("Secondary Button")
.style(ButtonStyle::Secondary) .style(ButtonStyle::Secondary)
.size(ButtonSize::XSmall) .size(ButtonSize::XSmall)
.on_click(Self::on_click), .on_click(Self::on_click),
) )
.child( .child(
Button::new("button-xs-3") Button::new("button-xs-3", cx)
.label("Danger Button") .label("Danger Button")
.style(ButtonStyle::Danger) .style(ButtonStyle::Danger)
.size(ButtonSize::XSmall) .size(ButtonSize::XSmall)
@ -135,21 +135,21 @@ impl Render for ButtonStory {
.child( .child(
section("Disabled Button", cx) section("Disabled Button", cx)
.child( .child(
Button::new("button-disabled1") Button::new("button-disabled1", cx)
.label("Disabled Button") .label("Disabled Button")
.style(ButtonStyle::Primary) .style(ButtonStyle::Primary)
.on_click(Self::on_click) .on_click(Self::on_click)
.disabled(true), .disabled(true),
) )
.child( .child(
Button::new("button-disabled1") Button::new("button-disabled1", cx)
.label("Disabled Button") .label("Disabled Button")
.style(ButtonStyle::Secondary) .style(ButtonStyle::Secondary)
.on_click(Self::on_click) .on_click(Self::on_click)
.disabled(true), .disabled(true),
) )
.child( .child(
Button::new("button-disabled1") Button::new("button-disabled1", cx)
.label("Disabled Button") .label("Disabled Button")
.style(ButtonStyle::Danger) .style(ButtonStyle::Danger)
.on_click(Self::on_click) .on_click(Self::on_click)
@ -159,19 +159,19 @@ impl Render for ButtonStory {
.child( .child(
section("Selected Style", cx) section("Selected Style", cx)
.child( .child(
Button::new("button-selected-1") Button::new("button-selected-1", cx)
.label("Selected Button") .label("Selected Button")
.style(ButtonStyle::Primary) .style(ButtonStyle::Primary)
.selected(true), .selected(true),
) )
.child( .child(
Button::new("button-selected-2") Button::new("button-selected-2", cx)
.label("Selected Button") .label("Selected Button")
.style(ButtonStyle::Secondary) .style(ButtonStyle::Secondary)
.selected(true), .selected(true),
) )
.child( .child(
Button::new("button-selected-3") Button::new("button-selected-3", cx)
.label("Selected Button") .label("Selected Button")
.style(ButtonStyle::Danger) .style(ButtonStyle::Danger)
.selected(true), .selected(true),
@ -181,46 +181,46 @@ impl Render for ButtonStory {
.child( .child(
section("Icon Button", cx) section("Icon Button", cx)
.child( .child(
Button::new("icon-button-0") Button::new("icon-button-0", cx)
.icon(IconName::Search) .icon(IconName::Search)
.style(ButtonStyle::Primary), .style(ButtonStyle::Primary),
) )
.child(Button::new("icon-button-1").icon(IconName::Info)) .child(Button::new("icon-button-1", cx).icon(IconName::Info))
.child( .child(
Button::new("icon-button-2") Button::new("icon-button-2", cx)
.icon(IconName::Close) .icon(IconName::Close)
.style(ButtonStyle::Danger), .style(ButtonStyle::Danger),
) )
.child( .child(
Button::new("icon-button-3") Button::new("icon-button-3", cx)
.icon(IconName::Search) .icon(IconName::Search)
.size(ButtonSize::Small) .size(ButtonSize::Small)
.style(ButtonStyle::Primary), .style(ButtonStyle::Primary),
) )
.child( .child(
Button::new("icon-button-4") Button::new("icon-button-4", cx)
.icon(IconName::Info) .icon(IconName::Info)
.size(ButtonSize::Small), .size(ButtonSize::Small),
) )
.child( .child(
Button::new("icon-button-5") Button::new("icon-button-5", cx)
.icon(IconName::Close) .icon(IconName::Close)
.size(ButtonSize::Small) .size(ButtonSize::Small)
.style(ButtonStyle::Danger), .style(ButtonStyle::Danger),
) )
.child( .child(
Button::new("icon-button-6") Button::new("icon-button-6", cx)
.icon(IconName::Search) .icon(IconName::Search)
.size(ButtonSize::XSmall) .size(ButtonSize::XSmall)
.style(ButtonStyle::Primary), .style(ButtonStyle::Primary),
) )
.child( .child(
Button::new("icon-button-7") Button::new("icon-button-7", cx)
.icon(IconName::Info) .icon(IconName::Info)
.size(ButtonSize::XSmall), .size(ButtonSize::XSmall),
) )
.child( .child(
Button::new("icon-button-8") Button::new("icon-button-8", cx)
.icon(IconName::Close) .icon(IconName::Close)
.size(ButtonSize::XSmall) .size(ButtonSize::XSmall)
.style(ButtonStyle::Danger), .style(ButtonStyle::Danger),

View file

@ -1,13 +1,24 @@
use gpui::{ use gpui::{
ClickEvent, IntoElement, ParentElement as _, Render, Styled as _, View, ViewContext, actions, AppContext, ClickEvent, FocusHandle, FocusableView, InteractiveElement, IntoElement,
VisualContext, WindowContext, 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; 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 { pub struct InputStory {
focus_handle: FocusHandle,
input1: View<TextInput>, input1: View<TextInput>,
input2: View<TextInput>, input2: View<TextInput>,
mash_input: View<TextInput>, mash_input: View<TextInput>,
@ -54,6 +65,7 @@ impl InputStory {
}); });
Self { Self {
focus_handle: cx.focus_handle(),
input1, input1,
input2: cx.new_view(|cx| { input2: cx.new_view(|cx| {
let mut input = TextInput::new(cx); let mut input = TextInput::new(cx);
@ -77,11 +89,46 @@ impl InputStory {
fn on_change(ev: &ClickEvent, cx: &mut WindowContext) { fn on_change(ev: &ClickEvent, cx: &mut WindowContext) {
println!("Input changed: {:?}", ev); println!("Input changed: {:?}", ev);
} }
fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
self.cycle_focus(true, cx);
}
fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext<Self>) {
self.cycle_focus(false, cx);
}
}
impl FocusableCycle for InputStory {
fn cycle_focus_handles(&self, cx: &mut ViewContext<Self>) -> Vec<FocusHandle> {
[
&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 { impl Render for InputStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex() 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() .size_full()
.p_4() .p_4()
.justify_start() .justify_start()
@ -102,5 +149,23 @@ impl Render for InputStory {
.child(self.both_input1.clone()) .child(self.both_input1.clone())
.child(self.suffix_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(),
),
)
} }
} }

View file

@ -21,10 +21,10 @@ pub use switch_story::SwitchStory;
pub use tooltip_story::TooltipStory; pub use tooltip_story::TooltipStory;
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Div, Element, div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Div, EventEmitter,
EventEmitter, FocusableView, InteractiveElement, IntoElement, ParentElement, Pixels, Render, FocusableView, InteractiveElement, IntoElement, ParentElement, Pixels, Render, ScrollHandle,
ScrollHandle, SharedString, StatefulInteractiveElement, Styled as _, Task, View, ViewContext, SharedString, StatefulInteractiveElement, Styled as _, Task, View, ViewContext, VisualContext,
VisualContext, WindowContext, WindowContext,
}; };
use workspace::{ use workspace::{
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
@ -33,7 +33,11 @@ use workspace::{
}; };
use anyhow::Result; 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( pub fn story_case(
name: &'static str, name: &'static str,
@ -286,7 +290,6 @@ impl Panel for MyPanel {
} }
fn set_size(&mut self, size: Option<gpui::Pixels>, cx: &mut ViewContext<Self>) { fn set_size(&mut self, size: Option<gpui::Pixels>, cx: &mut ViewContext<Self>) {
dbg!("================ set_size", size);
if let Some(size) = size { if let Some(size) = size {
self.update_size(size) self.update_size(size)
} }

View file

@ -2,9 +2,9 @@ use core::time;
use fake::Fake; use fake::Fake;
use gpui::{ use gpui::{
actions, div, prelude::FluentBuilder as _, px, ElementId, FocusHandle, FocusableView, actions, div, px, ElementId, FocusHandle, FocusableView, InteractiveElement, IntoElement,
InteractiveElement, IntoElement, ParentElement, Render, RenderOnce, Styled, Timer, View, ParentElement, Render, RenderOnce, Styled, Timer, View, ViewContext, VisualContext,
ViewContext, VisualContext, WindowContext, WindowContext,
}; };
use ui::{ use ui::{

View file

@ -197,7 +197,7 @@ impl Render for PickerStory {
.gap_6() .gap_6()
.child( .child(
v_flex().items_start().child( v_flex().items_start().child(
Button::new("show-picker") Button::new("show-picker", cx)
.label("Show Picker...") .label("Show Picker...")
.icon(IconName::Search) .icon(IconName::Search)
.style(ButtonStyle::Primary) .style(ButtonStyle::Primary)

View file

@ -40,7 +40,7 @@ impl Render for Form {
.child("This is a form container.") .child("This is a form container.")
.child(self.input1.clone()) .child(self.input1.clone())
.child( .child(
Button::primary("submit", "Submit") Button::primary("submit", "Submit", cx)
.on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))), .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
) )
} }
@ -62,7 +62,7 @@ impl PopoverStory {
} }
impl Render for PopoverStory { impl Render for PopoverStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let form = self.form.clone(); let form = self.form.clone();
v_flex() v_flex()
@ -76,15 +76,15 @@ impl Render for PopoverStory {
.child( .child(
v_flex().gap_4().child( v_flex().gap_4().child(
Popover::new("info-top-left") 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| { .content(|cx| {
PopoverContent::new(cx, |_| { PopoverContent::new(cx, |cx| {
v_flex() v_flex()
.gap_4() .gap_4()
.child("Hello, this is a Popover.") .child("Hello, this is a Popover.")
.child(Divider::horizontal()) .child(Divider::horizontal())
.child( .child(
Button::new("info1") Button::new("info1", cx)
.label("Yes") .label("Yes")
.width(px(80.)) .width(px(80.))
.size(ButtonSize::Small), .size(ButtonSize::Small),
@ -97,15 +97,15 @@ impl Render for PopoverStory {
.child( .child(
Popover::new("info-top-right") Popover::new("info-top-right")
.anchor(AnchorCorner::TopRight) .anchor(AnchorCorner::TopRight)
.trigger(Button::new("info-top-right").label("Top Right")) .trigger(Button::new("info-top-right", cx).label("Top Right"))
.content(|cx| { .content(|cx| {
PopoverContent::new(cx, |_| { PopoverContent::new(cx, |cx| {
v_flex() v_flex()
.gap_4() .gap_4()
.child("Hello, this is a Popover on the Top Right.") .child("Hello, this is a Popover on the Top Right.")
.child(Divider::horizontal()) .child(Divider::horizontal())
.child( .child(
Button::new("info1") Button::new("info1", cx)
.label("Yes") .label("Yes")
.width(px(80.)) .width(px(80.))
.size(ButtonSize::Small), .size(ButtonSize::Small),
@ -124,7 +124,9 @@ impl Render for PopoverStory {
Popover::new("info-bottom-left") Popover::new("info-bottom-left")
.anchor(AnchorCorner::BottomLeft) .anchor(AnchorCorner::BottomLeft)
.trigger( .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()), .content(move |_| form.clone()),
) )
@ -133,18 +135,18 @@ impl Render for PopoverStory {
.anchor(AnchorCorner::BottomRight) .anchor(AnchorCorner::BottomRight)
.mouse_button(MouseButton::Right) .mouse_button(MouseButton::Right)
.trigger( .trigger(
Button::new("pop") Button::new("pop", cx)
.label("Mouse Right Click") .label("Mouse Right Click")
.width(px(300.)), .width(px(300.)),
) )
.content(|cx| { .content(|cx| {
PopoverContent::new(cx, |_| { PopoverContent::new(cx, |cx| {
v_flex() v_flex()
.gap_4() .gap_4()
.child("Hello, this is a Popover on the Bottom Right.") .child("Hello, this is a Popover on the Bottom Right.")
.child(Divider::horizontal()) .child(Divider::horizontal())
.child( .child(
Button::new("info1") Button::new("info1", cx)
.label("Yes") .label("Yes")
.width(px(80.)) .width(px(80.))
.size(ButtonSize::Small), .size(ButtonSize::Small),

View file

@ -1,5 +1,5 @@
use gpui::{ use gpui::{
div, px, CursorStyle, InteractiveElement, ParentElement, Render, StatefulInteractiveElement, div, CursorStyle, InteractiveElement, ParentElement, Render, StatefulInteractiveElement,
Styled, View, VisualContext as _, WindowContext, Styled, View, VisualContext as _, WindowContext,
}; };
@ -25,7 +25,7 @@ impl TooltipStory {
} }
impl Render for TooltipStory { impl Render for TooltipStory {
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement { fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
v_flex() v_flex()
.p_4() .p_4()
.gap_5() .gap_5()
@ -33,7 +33,7 @@ impl Render for TooltipStory {
div() div()
.cursor(CursorStyle::PointingHand) .cursor(CursorStyle::PointingHand)
.child( .child(
Button::new("button") Button::new("button", cx)
.label("Hover me") .label("Hover me")
.style(ButtonStyle::Primary), .style(ButtonStyle::Primary),
) )

View file

@ -1,12 +1,13 @@
use crate::{ use crate::{
h_flex, h_flex,
theme::{ActiveTheme, Colorize as _, ThemeMode}, theme::{ActiveTheme, Colorize as _, ThemeMode},
Clickable, Disableable, Icon, Selectable, Clickable, Disableable, Icon, Selectable, StyledExt,
}; };
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, ClickEvent, DefiniteLength, Div, ElementId, Hsla, div, prelude::FluentBuilder as _, px, ClickEvent, DefiniteLength, Div, ElementId, FocusHandle,
InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString, Focusable, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Render,
StatefulInteractiveElement as _, Styled, WindowContext, RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, View, ViewContext,
VisualContext, WindowContext,
}; };
pub enum ButtonRounded { pub enum ButtonRounded {
@ -34,6 +35,7 @@ pub enum ButtonStyle {
pub struct Button { pub struct Button {
pub base: Div, pub base: Div,
id: ElementId, id: ElementId,
focus_handle: FocusHandle,
icon: Option<Icon>, icon: Option<Icon>,
label: Option<SharedString>, label: Option<SharedString>,
disabled: bool, disabled: bool,
@ -48,9 +50,10 @@ pub struct Button {
} }
impl Button { impl Button {
pub fn new(id: impl Into<ElementId>) -> Self { pub fn new(id: impl Into<ElementId>, cx: &mut WindowContext) -> Self {
Self { Self {
base: div(), base: div(),
focus_handle: cx.focus_handle(),
id: id.into(), id: id.into(),
icon: None, icon: None,
label: None, label: None,
@ -66,16 +69,28 @@ impl Button {
} }
} }
pub fn primary(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self { pub fn primary(
Self::new(id).label(label).style(ButtonStyle::Primary) id: impl Into<ElementId>,
label: impl Into<SharedString>,
cx: &mut WindowContext,
) -> Self {
Self::new(id, cx).label(label).style(ButtonStyle::Primary)
} }
pub fn danger(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self { pub fn danger(
Self::new(id).label(label).style(ButtonStyle::Danger) id: impl Into<ElementId>,
label: impl Into<SharedString>,
cx: &mut WindowContext,
) -> Self {
Self::new(id, cx).label(label).style(ButtonStyle::Danger)
} }
pub fn small(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self { pub fn small(
Self::new(id).label(label).size(ButtonSize::Small) id: impl Into<ElementId>,
label: impl Into<SharedString>,
cx: &mut WindowContext,
) -> Self {
Self::new(id, cx).label(label).size(ButtonSize::Small)
} }
pub fn width(mut self, width: impl Into<DefiniteLength>) -> Self { pub fn width(mut self, width: impl Into<DefiniteLength>) -> 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 { impl RenderOnce for Button {
fn render(self, cx: &mut WindowContext) -> impl IntoElement { fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let theme = cx.theme(); let theme = cx.theme();
let style: ButtonStyle = self.style; let style: ButtonStyle = self.style;
let normal_style = style.normal(cx); let normal_style = style.normal(cx);
let focused = self.focus_handle.is_focused(cx);
self.base self.base
.id(self.id) .id(self.id)
.track_focus(&self.focus_handle)
.flex() .flex()
.items_center() .items_center()
.justify_center() .justify_center()
@ -192,6 +215,9 @@ impl RenderOnce for Button {
.border_color(normal_style.border) .border_color(normal_style.border)
.bg(normal_style.bg) .bg(normal_style.bg)
}) })
.when(focused, |this| {
this.border_color(cx.theme().ring).debug_blue()
})
.when_some( .when_some(
self.on_click.filter(|_| !self.disabled), self.on_click.filter(|_| !self.disabled),
|this, on_click| { |this, on_click| {

View file

@ -23,7 +23,7 @@ use crate::{
list::ListItem, list::ListItem,
picker::{self, Picker, PickerDelegate}, picker::{self, Picker, PickerDelegate},
theme::ActiveTheme, theme::ActiveTheme,
Icon, IconName, Icon, IconName, StyledExt,
}; };
/// A trait for items that can be displayed in a dropdown. /// A trait for items that can be displayed in a dropdown.
@ -221,13 +221,12 @@ where
D: DropdownDelegate + 'static, D: DropdownDelegate + 'static,
{ {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let group_id = format!("dropdown-group:{}", self.id);
let title = self.title.clone().unwrap_or_else(|| "Select...".into()); let title = self.title.clone().unwrap_or_else(|| "Select...".into());
let focused = self.focus_handle.is_focused(cx); let focused = self.focus_handle.is_focused(cx);
div() div()
.key_context("Dropdown") .key_context("Dropdown")
.group(group_id.clone()) .group(format!("dropdown-group:{}", self.id))
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.on_action(cx.listener(Self::up)) .on_action(cx.listener(Self::up))
.on_action(cx.listener(Self::down)) .on_action(cx.listener(Self::down))
@ -248,9 +247,9 @@ where
.border_color(cx.theme().input) .border_color(cx.theme().input)
.rounded(px(cx.theme().radius)) .rounded(px(cx.theme().radius))
.shadow_sm() .shadow_sm()
.when(focused, |this| this.outline(cx))
.px_3() .px_3()
.py_2() .py_2()
.when(focused, |this| this.border_color(cx.theme().ring))
.on_click(cx.listener(|this, _, cx| { .on_click(cx.listener(|this, _, cx| {
this.open = !this.open; this.open = !this.open;
cx.notify(); cx.notify();
@ -280,20 +279,12 @@ struct DropdownMenuElement<D: DropdownDelegate + 'static> {
dropdown: View<Dropdown<D>>, dropdown: View<Dropdown<D>>,
} }
#[derive(Default)]
struct DropdownMenuElementState { struct DropdownMenuElementState {
menu_element: Option<AnyElement>, menu_element: Option<AnyElement>,
layout_id: Option<LayoutId>, layout_id: Option<LayoutId>,
} }
impl Default for DropdownMenuElementState {
fn default() -> Self {
Self {
menu_element: None,
layout_id: None,
}
}
}
impl<D> IntoElement for DropdownMenuElement<D> impl<D> IntoElement for DropdownMenuElement<D>
where where
D: DropdownDelegate + 'static, D: DropdownDelegate + 'static,

View file

@ -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<Self>) -> Vec<FocusHandle>
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<Self>)
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();
}
}

View file

@ -2,6 +2,7 @@ use std::ops::Range;
use crate::event::InterativeElementExt as _; use crate::event::InterativeElementExt as _;
use crate::theme::ActiveTheme; use crate::theme::ActiveTheme;
use crate::StyledExt as _;
use blink_cursor::BlinkCursor; use blink_cursor::BlinkCursor;
use gpui::*; use gpui::*;
use prelude::FluentBuilder as _; use prelude::FluentBuilder as _;
@ -679,14 +680,11 @@ impl Render for TextInput {
.h_10() .h_10()
.when(self.appearance, |this| { .when(self.appearance, |this| {
this.bg(cx.theme().input) this.bg(cx.theme().input)
.border_color(if focused { .border_color(cx.theme().input)
cx.theme().ring
} else {
cx.theme().input
})
.border_1() .border_1()
.rounded(px(cx.theme().radius)) .rounded(px(cx.theme().radius))
.shadow_sm() .shadow_sm()
.when(focused, |this| this.outline(cx))
.px_3() .px_3()
.bg(if self.disabled { .bg(if self.disabled {
cx.theme().muted cx.theme().muted

View file

@ -1,6 +1,7 @@
mod clickable; mod clickable;
mod disableable; mod disableable;
mod event; mod event;
mod focusable;
mod icon; mod icon;
mod scrollbar; mod scrollbar;
mod selectable; mod selectable;
@ -28,6 +29,7 @@ pub mod tooltip;
pub use clickable::Clickable; pub use clickable::Clickable;
pub use disableable::Disableable; pub use disableable::Disableable;
pub use event::InterativeElementExt; pub use event::InterativeElementExt;
pub use focusable::FocusableCycle;
pub use selectable::{Selectable, Selection}; pub use selectable::{Selectable, Selection};
pub use icon::*; pub use icon::*;

View file

@ -1,5 +1,5 @@
use crate::theme::{hsl, ActiveTheme}; 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}; use smallvec::{smallvec, SmallVec};
pub enum ElevationIndex { pub enum ElevationIndex {
@ -96,6 +96,18 @@ pub trait StyledExt: Styled + Sized {
fn debug_pink(self) -> Self { fn debug_pink(self) -> Self {
self.border_1().border_color(hsl(300., 100., 47.)) 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<E: Styled> StyledExt for E {} impl<E: Styled> StyledExt for E {}

View file

@ -167,7 +167,7 @@ impl Colors {
accent_foreground: hsl(240.0, 5.9, 10.0), accent_foreground: hsl(240.0, 5.9, 10.0),
border: hsl(240.0, 5.9, 90.0), border: hsl(240.0, 5.9, 90.0),
input: 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), selection: hsl(211.0, 97.0, 85.0),
scrollbar: Hsla::transparent_black(), scrollbar: Hsla::transparent_black(),
scrollbar_thumb: hsl(240.0, 5.9, 90.0).opacity(0.7), scrollbar_thumb: hsl(240.0, 5.9, 90.0).opacity(0.7),