From 3288270b49b2529568410fd697d3c29d544e722f Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Sat, 29 Jun 2024 10:03:59 +0800 Subject: [PATCH] Add Popover (#6) --- Cargo.toml | 35 +++ README.md | 1 + crates/app/Cargo.toml | 3 + crates/ui-story/Cargo.toml | 3 + crates/ui-story/src/button_story.rs | 22 +- crates/ui-story/src/checkbox_story.rs | 5 +- crates/ui-story/src/lib.rs | 13 +- crates/ui-story/src/picker_story.rs | 5 +- crates/ui-story/src/popover_story.rs | 72 ++++++ crates/ui-story/src/switch_story.rs | 6 +- crates/ui/Cargo.toml | 3 + crates/ui/src/button.rs | 46 ++-- crates/ui/src/checkbox.rs | 12 +- crates/ui/src/divider.rs | 2 +- crates/ui/src/dropdown.rs | 12 +- crates/ui/src/event.rs | 2 +- crates/ui/src/icon.rs | 15 +- crates/ui/src/label.rs | 5 +- crates/ui/src/lib.rs | 2 + crates/ui/src/list/list_item.rs | 6 +- crates/ui/src/picker.rs | 7 +- crates/ui/src/popover.rs | 319 ++++++++++++++++++++++++++ crates/ui/src/prelude.rs | 1 + crates/ui/src/styled_ext.rs | 32 +-- crates/ui/src/tab/tab.rs | 1 - crates/ui/src/theme.rs | 2 +- crates/ui/src/tooltip.rs | 4 +- crates/util/Cargo.toml | 3 + crates/workspace/Cargo.toml | 3 + crates/workspace/src/app_state.rs | 8 +- crates/workspace/src/lib.rs | 10 +- 31 files changed, 569 insertions(+), 91 deletions(-) create mode 100644 crates/ui-story/src/popover_story.rs create mode 100644 crates/ui/src/popover.rs diff --git a/Cargo.toml b/Cargo.toml index 1c739852..c1708630 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,38 @@ anyhow = "1" log = "0.4" serde = "1.0.203" serde_json = "1" + + +[workspace.lints.clippy] +dbg_macro = "deny" +todo = "deny" + +# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so +# warning on this rule produces a lot of noise. +single_range_in_vec_init = "allow" + +# These are all of the rules that currently have violations in the Zed +# codebase. +# +# We'll want to drive this list down by either: +# 1. fixing violations of the rule and begin enforcing it +# 2. deciding we want to allow the rule permanently, at which point +# we should codify that separately above. +# +# This list shouldn't be added to; it should only get shorter. +# ============================================================================= + +# There are a bunch of rules currently failing in the `style` group, so +# allow all of those, for now. +style = "allow" + +# Individual rules that have violations in the codebase: +almost_complete_range = "allow" +arc_with_non_send_sync = "allow" +borrowed_box = "allow" +let_underscore_future = "allow" +map_entry = "allow" +non_canonical_partial_ord_impl = "allow" +reversed_empty_ranges = "allow" +type_complexity = "allow" +module_inception = "allow" diff --git a/README.md b/README.md index c3b3f791..ab9f1fa6 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ There have a part of UI components from [Zed](https://github.com/zed-industries/ - title_bar - picker +- popover (based on popup_menu) > I think we can discuss them with Zed team to change the license to MIT or Apache License for share to community. diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 701e4e5c..82a0b411 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -9,3 +9,6 @@ anyhow.workspace = true rust-embed = "8" log.workspace = true workspace.workspace = true + +[lints] +workspace = true diff --git a/crates/ui-story/Cargo.toml b/crates/ui-story/Cargo.toml index a625a570..c9ee1ca9 100644 --- a/crates/ui-story/Cargo.toml +++ b/crates/ui-story/Cargo.toml @@ -6,3 +6,6 @@ edition = "2021" [dependencies] ui.workspace = true gpui.workspace = true + +[lints] +workspace = true diff --git a/crates/ui-story/src/button_story.rs b/crates/ui-story/src/button_story.rs index 83d00cf2..8bd80682 100644 --- a/crates/ui-story/src/button_story.rs +++ b/crates/ui-story/src/button_story.rs @@ -5,7 +5,7 @@ use gpui::{ use ui::{ button::{Button, ButtonSize, ButtonStyle}, - h_flex, v_flex, Clickable, Disableable as _, Icon, IconName, + h_flex, v_flex, Clickable, Disableable as _, Icon, IconName, Selectable, }; use super::story_case; @@ -116,6 +116,26 @@ impl Render for ButtonStory { .size(ButtonSize::Small) .on_click(Self::on_click), ), + ) + .child( + h_flex() + .items_center() + .gap_6() + .child( + Button::new("button-6", "Selected Button") + .style(ButtonStyle::Primary) + .selected(true), + ) + .child( + Button::new("button-7", "Selected Button") + .style(ButtonStyle::Secondary) + .selected(true), + ) + .child( + Button::new("button-8", "Selected Button") + .style(ButtonStyle::Danger) + .selected(true), + ), ), ) } diff --git a/crates/ui-story/src/checkbox_story.rs b/crates/ui-story/src/checkbox_story.rs index 202455a5..58ac2357 100644 --- a/crates/ui-story/src/checkbox_story.rs +++ b/crates/ui-story/src/checkbox_story.rs @@ -1,7 +1,4 @@ -use gpui::{ - div, ClickEvent, IntoElement, ParentElement, Render, RenderOnce, Styled, ViewContext, - WindowContext, -}; +use gpui::{IntoElement, ParentElement, Render, Styled, ViewContext, WindowContext}; use ui::{checkbox::Checkbox, h_flex, v_flex, Disableable as _, Selection}; diff --git a/crates/ui-story/src/lib.rs b/crates/ui-story/src/lib.rs index fcb86c04..d8bf76e7 100644 --- a/crates/ui-story/src/lib.rs +++ b/crates/ui-story/src/lib.rs @@ -3,6 +3,7 @@ mod checkbox_story; mod dropdown_story; mod input_story; mod picker_story; +mod popover_story; mod switch_story; mod tooltip_stroy; @@ -25,6 +26,7 @@ use checkbox_story::CheckboxStory; use dropdown_story::DropdownStory; use input_story::InputStory; use picker_story::PickerStory; +use popover_story::PopoverStory; use switch_story::SwitchStory; use tooltip_stroy::TooltipStory; @@ -58,6 +60,7 @@ impl StoryContainer { impl RenderOnce for StoryContainer { fn render(self, _: &mut WindowContext) -> impl IntoElement { div() + .size_full() .flex() .flex_col() .gap_6() @@ -83,6 +86,7 @@ enum StoryType { Picker, Dropdown, Tooltip, + Popover, } impl Display for StoryType { @@ -95,6 +99,7 @@ impl Display for StoryType { Self::Picker => write!(f, "Picker"), Self::Dropdown => write!(f, "Dropdown"), Self::Tooltip => write!(f, "Tooltip"), + Self::Popover => write!(f, "Popover"), } } } @@ -109,6 +114,7 @@ pub struct Stories { picker_story: View, dropdown_story: View, tooltip_story: View, + popover_story: View, } impl Stories { @@ -119,9 +125,10 @@ impl Stories { checkbox_story: cx.new_view(|cx| CheckboxStory::new(cx)), input_story: cx.new_view(|cx| InputStory::new(cx)), switch_story: cx.new_view(|cx| SwitchStory::new(cx)), - picker_story: cx.new_view(|cx| PickerStory::new(cx)), - dropdown_story: cx.new_view(|cx| DropdownStory::new(cx)), tooltip_story: cx.new_view(|_| TooltipStory), + picker_story: cx.new_view(PickerStory::new), + dropdown_story: cx.new_view(DropdownStory::new), + popover_story: cx.new_view(PopoverStory::new), } } @@ -153,6 +160,7 @@ impl Stories { self.tab("story-picker", StoryType::Picker, None, cx), self.tab("story-dropdown", StoryType::Dropdown, None, cx), self.tab("story-tooltip", StoryType::Tooltip, None, cx), + self.tab("story-popover", StoryType::Popover, None, cx), ])) } @@ -196,6 +204,7 @@ impl Render for Stories { StoryType::Picker => this.child(self.picker_story.clone()), StoryType::Dropdown => this.child(self.dropdown_story.clone()), StoryType::Tooltip => this.child(self.tooltip_story.clone()), + StoryType::Popover => this.child(self.popover_story.clone()), }) } } diff --git a/crates/ui-story/src/picker_story.rs b/crates/ui-story/src/picker_story.rs index ec48da42..6bfa8b54 100644 --- a/crates/ui-story/src/picker_story.rs +++ b/crates/ui-story/src/picker_story.rs @@ -1,7 +1,6 @@ use gpui::{ - actions, div, prelude::FluentBuilder as _, px, Div, Empty, Entity, InteractiveElement as _, - IntoElement, ParentElement, Render, SharedString, Styled, Task, View, ViewContext, - VisualContext as _, WeakView, WindowContext, + actions, div, prelude::FluentBuilder as _, px, InteractiveElement as _, IntoElement, + ParentElement, Render, Styled, Task, View, ViewContext, VisualContext as _, WeakView, }; use ui::{ diff --git a/crates/ui-story/src/popover_story.rs b/crates/ui-story/src/popover_story.rs new file mode 100644 index 00000000..cab5e2d5 --- /dev/null +++ b/crates/ui-story/src/popover_story.rs @@ -0,0 +1,72 @@ +use gpui::{div, px, Element, IntoElement, ParentElement as _, Render, Styled as _, ViewContext}; +use ui::{ + button::{Button, ButtonSize}, + divider::Divider, + h_flex, + popover::{Popover, PopoverContent}, + v_flex, +}; + +use crate::story_case; + +pub struct PopoverStory {} + +impl PopoverStory { + pub fn new(_: &mut ViewContext) -> Self { + Self {} + } +} + +impl Render for PopoverStory { + fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { + story_case( + "Popover", + "Displays rich content in a portal, triggered by a button.", + ) + .child( + h_flex().items_center().justify_between().child( + Popover::new("info-top-left") + .trigger(Button::new("info", "Top Left").width(px(300.))) + .content(|cx| { + PopoverContent::new(|_| { + v_flex().gap_4().child("Hello, this is a Popover.") + .child(Divider::horizontal()) + .child( + Button::new("info1", "Yes") + .width(px(80.)) + .size(ButtonSize::Small) + ).into_any() + }, + cx, + ) + }), + ).child( + Popover::new("info-top-right") + .trigger(Button::new("info", "Top Right").width(px(300.))) + .content(|cx| { + PopoverContent::new(|_| + "Hello, this is a Popover.\nYou can click outside to dissmis.".into_any(), + cx, + ) + }), + ) + ) + .child( + div().absolute().bottom_4().left_0().w_full().h_10().child( + h_flex().items_center().justify_between().child( + Popover::new("info-bottom-left") + .trigger(Button::new("pop", "Bottom Left").width(px(300.))) + .content(|cx| { + PopoverContent::new(|_| "这是另外一个 Popover。\n你可以点击外部来关闭。\nThis popover has position bottom_4().left_0().w_full().h_10().".into_any(), cx) + })).child( + Popover::new("info-bottom-right") + .trigger(Button::new("pop", "Bottom Right").width(px(300.))) + .content(|cx| { + PopoverContent::new(|_| "这是另外一个 Popover。\n你可以点击外部来关闭。\nThis popover has position bottom_4().left_0().w_full().h_10().".into_any(), cx) + })) + + + ), + ) + } +} diff --git a/crates/ui-story/src/switch_story.rs b/crates/ui-story/src/switch_story.rs index 77f2b1e1..e9887fe5 100644 --- a/crates/ui-story/src/switch_story.rs +++ b/crates/ui-story/src/switch_story.rs @@ -21,7 +21,7 @@ pub struct SwitchStory { } impl SwitchStory { - pub(crate) fn new(cx: &mut WindowContext) -> Self { + pub(crate) fn new(_cx: &mut WindowContext) -> Self { Self { switch1: false, switch2: true, @@ -98,14 +98,14 @@ impl Render for SwitchStory { card(cx).v_flex() .items_start().child(title("Disabled Switchs")).child( h_flex().items_center() .gap_6() - .child(Switch::new("switch3").disabled(true).on_click(|ev, cx| { + .child(Switch::new("switch3").disabled(true).on_click(|ev, _| { println!("Switch value changed: {:?}", ev); })) .child( Switch::new("switch3_1").label("Airplane Mode") .checked(true) .disabled(true) - .on_click(|ev, cx| { + .on_click(|ev, _| { println!("Switch value changed: {:?}", ev); }), )) diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index d2bc1f0b..86741e06 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -13,3 +13,6 @@ wry = "0" smallvec = "1.13.2" windows = "0.57.0" unicode-segmentation = "1.11.0" + +[lints] +workspace = true diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index 80dd22a8..09443147 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -1,13 +1,12 @@ -use gpui::{ - div, prelude::FluentBuilder as _, px, AnyElement, ClickEvent, DefiniteLength, Div, ElementId, - Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString, - StatefulInteractiveElement as _, Styled, WindowContext, -}; - use crate::{ - h_flex, span, + h_flex, theme::{ActiveTheme, Colorize as _, ThemeMode}, - Clickable, Disableable, Icon, IconName, Selectable, + Clickable, Disableable, Icon, Selectable, +}; +use gpui::{ + div, prelude::FluentBuilder as _, px, ClickEvent, DefiniteLength, Div, ElementId, Hsla, + InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString, + StatefulInteractiveElement as _, Styled, WindowContext, }; pub enum ButtonRounded { @@ -134,6 +133,8 @@ impl RenderOnce for Button { .flex() .items_center() .justify_center() + .when_some(self.width, |this, width| this.w(width)) + .when_some(self.height, |this, height| this.h(height)) .map(|this| match self.size { ButtonSize::Small => this.px_3().py_2().h_6(), ButtonSize::Medium => this.px_4().py_2().h_8(), @@ -144,7 +145,12 @@ impl RenderOnce for Button { ButtonRounded::Large => this.rounded(px(cx.theme().radius * 2.0)), ButtonRounded::None => this.rounded_none(), }) - .when(!self.disabled, |this| { + .when(self.selected, |this| { + let selected_style = style.selected(cx); + this.bg(selected_style.bg) + .border_color(selected_style.border) + }) + .when(!self.disabled && !self.selected, |this| { this.hover(|this| { let hover_style = style.hovered(cx); this.bg(hover_style.bg).border_color(hover_style.border) @@ -174,7 +180,13 @@ impl RenderOnce for Button { }) .border_1() .map(|this| match theme.mode { - ThemeMode::Light => this.shadow_sm(), + ThemeMode::Light => { + if self.disabled { + this + } else { + this.shadow_sm() + } + } ThemeMode::Dark => this, }) .child({ @@ -251,15 +263,23 @@ impl ButtonStyle { fn active(&self, cx: &WindowContext) -> ButtonStyles { let bg = self.bg_color(cx).darken(0.05); - let border = self.border_color(cx).darken(0.05); + let border = self.border_color(cx); let fg = self.text_color(cx).darken(0.05); ButtonStyles { bg, border, fg } } + fn selected(&self, cx: &WindowContext) -> ButtonStyles { + let bg = self.bg_color(cx).darken(0.07); + let border = self.border_color(cx); + let fg = self.text_color(cx).darken(0.07); + + ButtonStyles { bg, border, fg } + } + fn disabled(&self, cx: &WindowContext) -> ButtonStyles { - let bg = self.bg_color(cx).grayscale(); - let border = self.border_color(cx).grayscale(); + let bg = self.bg_color(cx).grayscale().opacity(0.9); + let border = self.border_color(cx).grayscale().opacity(0.9); let fg = self.text_color(cx).grayscale(); ButtonStyles { bg, border, fg } diff --git a/crates/ui/src/checkbox.rs b/crates/ui/src/checkbox.rs index 7f4eda22..427a11d1 100644 --- a/crates/ui/src/checkbox.rs +++ b/crates/ui/src/checkbox.rs @@ -1,12 +1,10 @@ use gpui::{ - prelude::FluentBuilder as _, px, svg, ElementId, InteractiveElement, IntoElement, - ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, Styled as _, - WindowContext, + prelude::FluentBuilder as _, svg, ElementId, InteractiveElement, IntoElement, ParentElement, + RenderOnce, SharedString, StatefulInteractiveElement as _, Styled as _, WindowContext, }; use crate::{ disableable::Disableable, - label::Label, selectable::{Selectable, Selection}, stock::{h_flex, v_flex}, theme::{ActiveTheme, Colorize as _}, @@ -25,7 +23,7 @@ pub struct Checkbox { } impl Checkbox { - pub fn new(id: impl Into, cx: &mut WindowContext) -> Self { + pub fn new(id: impl Into, _: &mut WindowContext) -> Self { Self { id: id.into(), checked: Selection::Unselected, @@ -95,7 +93,7 @@ impl RenderOnce for Checkbox { .border_1() .border_color(color) .rounded_sm() - .size(px(16.)) + .size_4() .map(|this| match self.checked { Selection::Unselected => this.bg(theme.transparent), _ => this.bg(color), @@ -123,7 +121,7 @@ impl RenderOnce for Checkbox { ) .map(|this| { if let Some(label) = self.label { - this.child(Label::new(label).text_color(color)) + this.child(label).text_color(color) } else { this } diff --git a/crates/ui/src/divider.rs b/crates/ui/src/divider.rs index 75a29798..2d457f2c 100644 --- a/crates/ui/src/divider.rs +++ b/crates/ui/src/divider.rs @@ -1,5 +1,5 @@ use gpui::IntoElement; -use gpui::{div, prelude::FluentBuilder as _, Div, RenderOnce, Styled as _}; +use gpui::{div, prelude::FluentBuilder as _, RenderOnce, Styled as _}; use crate::theme::ActiveTheme; use crate::StyledExt as _; diff --git a/crates/ui/src/dropdown.rs b/crates/ui/src/dropdown.rs index 7036bb29..bb98bb9f 100644 --- a/crates/ui/src/dropdown.rs +++ b/crates/ui/src/dropdown.rs @@ -1,10 +1,8 @@ -use std::rc::Rc; - use gpui::{ - actions, div, prelude::FluentBuilder as _, px, AppContext, Div, ElementId, FocusHandle, + actions, div, prelude::FluentBuilder as _, px, AppContext, ElementId, FocusHandle, FocusableView, InteractiveElement, IntoElement, KeyBinding, ParentElement as _, Render, - RenderOnce, SharedString, Stateful, StatefulInteractiveElement as _, Styled as _, View, - ViewContext, VisualContext as _, WeakView, + SharedString, StatefulInteractiveElement as _, Styled as _, View, ViewContext, + VisualContext as _, WeakView, }; actions!(dropdown, [Up, Down, Enter, Escape]); @@ -24,7 +22,7 @@ use crate::{ list::ListItem, picker::{self, Picker, PickerDelegate}, theme::ActiveTheme, - v_flex, Icon, IconName, + Icon, IconName, }; /// A trait for items that can be displayed in a dropdown. @@ -96,7 +94,7 @@ where fn dismissed(&mut self, cx: &mut ViewContext>) { if let Some(view) = self.dropdown.upgrade() { - cx.update_view(&view, |view, cx| { + cx.update_view(&view, |view, _| { view.open = false; }); } diff --git a/crates/ui/src/event.rs b/crates/ui/src/event.rs index 81abcb82..d84d1b5a 100644 --- a/crates/ui/src/event.rs +++ b/crates/ui/src/event.rs @@ -1,4 +1,4 @@ -use gpui::{ClickEvent, Element, Focusable, InteractiveElement, Stateful, WindowContext}; +use gpui::{ClickEvent, Focusable, InteractiveElement, WindowContext}; pub trait InterativeElementExt: InteractiveElement { /// Set the listener for a double click event. diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index 36415a19..9ef0ecf8 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -1,7 +1,6 @@ use crate::theme::ActiveTheme; use gpui::{ - div, rgb, svg, AnyElement, Div, Hsla, InteractiveElement, IntoElement, ParentElement as _, - RenderOnce, SharedString, StyleRefinement, Styled, Svg, TextStyle, TextStyleRefinement, + svg, AnyElement, Hsla, IntoElement, RenderOnce, SharedString, StyleRefinement, Styled, Svg, WindowContext, }; @@ -33,9 +32,9 @@ impl IconName { } } -impl Into for IconName { - fn into(self) -> Icon { - Icon::new(self) +impl From for Icon { + fn from(val: IconName) -> Self { + Icon::new(val) } } @@ -89,8 +88,8 @@ impl RenderOnce for Icon { } } -impl Into for Icon { - fn into(self) -> AnyElement { - self.into_any_element() +impl From for AnyElement { + fn from(val: Icon) -> Self { + val.into_any_element() } } diff --git a/crates/ui/src/label.rs b/crates/ui/src/label.rs index 07a5e3c1..f874fa3a 100644 --- a/crates/ui/src/label.rs +++ b/crates/ui/src/label.rs @@ -1,7 +1,4 @@ -use gpui::{ - div, prelude::FluentBuilder as _, px, AbsoluteLength, DefiniteLength, Div, Hsla, IntoElement, - ParentElement, RenderOnce, SharedString, Styled, TextStyleRefinement, WindowContext, -}; +use gpui::{div, Div, IntoElement, ParentElement, RenderOnce, SharedString, Styled, WindowContext}; use crate::theme::ActiveTheme; diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 7fc6bfff..1f91b4a0 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -20,6 +20,7 @@ pub mod dropdown; pub mod input; pub mod list; pub mod picker; +pub mod popover; pub mod switch; pub mod tab; pub mod tooltip; @@ -37,4 +38,5 @@ pub fn init(cx: &mut gpui::AppContext) { input::init(cx); picker::init(cx); dropdown::init(cx); + popover::init(cx); } diff --git a/crates/ui/src/list/list_item.rs b/crates/ui/src/list/list_item.rs index f2abfbdb..db8da89d 100644 --- a/crates/ui/src/list/list_item.rs +++ b/crates/ui/src/list/list_item.rs @@ -1,7 +1,7 @@ use gpui::{ - div, prelude::FluentBuilder as _, AnyElement, ClickEvent, Div, ElementId, InteractiveElement, - IntoElement, MouseButton, MouseDownEvent, ParentElement, RenderOnce, SharedString, Stateful, - StatefulInteractiveElement as _, Style, Styled, WindowContext, + prelude::FluentBuilder as _, ClickEvent, Div, ElementId, InteractiveElement, IntoElement, + MouseButton, MouseDownEvent, ParentElement, RenderOnce, Stateful, + StatefulInteractiveElement as _, Styled, WindowContext, }; use crate::{h_flex, theme::ActiveTheme, Disableable, Icon, IconName, Selectable}; diff --git a/crates/ui/src/picker.rs b/crates/ui/src/picker.rs index 814a5fce..9e4fd87a 100644 --- a/crates/ui/src/picker.rs +++ b/crates/ui/src/picker.rs @@ -67,8 +67,8 @@ pub trait PickerDelegate: Sized + 'static { None } - fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>) {} - fn dismissed(&mut self, cx: &mut ViewContext>) {} + fn confirm(&mut self, _secondary: bool, _cx: &mut ViewContext>) {} + fn dismissed(&mut self, _cx: &mut ViewContext>) {} fn should_dismiss(&self) -> bool { true } @@ -94,7 +94,7 @@ pub trait PickerDelegate: Sized + 'static { fn separators_after_indices(&self) -> Vec { Vec::new() } - fn update_matches(&mut self, query: &str, cx: &mut ViewContext>) -> Task<()> { + fn update_matches(&mut self, _query: &str, _cx: &mut ViewContext>) -> Task<()> { Task::ready(()) } fn confirm_update_query(&mut self, _cx: &mut ViewContext>) -> Option { @@ -472,7 +472,6 @@ impl Picker { self.set_query(text, cx); self.refresh(cx); } - _ => {} } } } diff --git a/crates/ui/src/popover.rs b/crates/ui/src/popover.rs new file mode 100644 index 00000000..2dc5b77b --- /dev/null +++ b/crates/ui/src/popover.rs @@ -0,0 +1,319 @@ +use std::{cell::RefCell, rc::Rc}; + +use crate::theme::ActiveTheme; +use crate::{Clickable, Selectable, StyledExt}; +use gpui::{ + actions, anchored, deferred, div, prelude::FluentBuilder as _, AnchorCorner, AnyElement, + AppContext, Bounds, DismissEvent, Element, ElementId, EventEmitter, FocusHandle, FocusableView, + HitboxId, InteractiveElement, IntoElement, LayoutId, ManagedView, ParentElement, Pixels, + Render, Style, Styled, View, ViewContext, VisualContext, WindowContext, +}; +use gpui::{point, px, DispatchPhase, MouseDownEvent, Point}; + +actions!(popover, [Dismiss]); + +pub trait Triggerable: IntoElement + Clickable + Selectable + 'static {} +impl Triggerable for T {} + +pub fn init(_cx: &mut AppContext) {} + +pub struct PopoverContent { + focus_handle: FocusHandle, + content: Rc AnyElement>, +} + +impl PopoverContent { + pub fn new(content: B, cx: &mut WindowContext) -> View + where + B: Fn(&mut WindowContext) -> AnyElement + 'static, + { + cx.new_view(|cx| { + let focus_handle = cx.focus_handle(); + + Self { + focus_handle, + content: Rc::new(content), + } + }) + } +} +impl EventEmitter for PopoverContent {} + +impl FocusableView for PopoverContent { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for PopoverContent { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .id("test") + // .on_action(cx.listener(Self::dissmiss)) + .absolute() + .mt_2() + .elevation_2(cx) + .bg(cx.theme().popover) + .border_1() + .border_color(cx.theme().border) + .p_4() + .max_w_128() + .w_80() + .occlude() + .child(self.content.clone()(cx)) + .on_mouse_down_out(cx.listener(|_, _, cx| cx.emit(DismissEvent))) + } +} + +pub struct Popover { + id: ElementId, + anchor: AnchorCorner, + trigger_builder: Option< + Box< + dyn FnOnce( + Rc>>>, + Option View + 'static>>, + ) -> AnyElement + + 'static, + >, + >, + content_builder: Option View + 'static>>, +} + +impl Popover { + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + trigger_builder: None, + content_builder: None, + anchor: AnchorCorner::TopLeft, + } + } + + pub fn trigger(mut self, trigger: T) -> Self { + self.trigger_builder = Some(Box::new(|popover, builder| { + let open = popover.borrow_mut().is_some(); + trigger + .selected(open) + .when_some(builder, |this, builder| { + this.on_click(move |_, cx| Self::show_popover(&builder, &popover, cx)) + }) + .into_any_element() + })); + + self + } + + pub fn content( + mut self, + content_builder: impl Fn(&mut WindowContext) -> View + 'static, + ) -> Self { + self.content_builder = Some(Rc::new(content_builder)); + self + } + + /// anchor defines which corner of the menu to anchor to the attachment point + /// (by default the cursor position, but see attach) + pub fn anchor(mut self, anchor: AnchorCorner) -> Self { + self.anchor = anchor; + self + } + + fn show_popover( + builder: &Rc View + 'static>, + popover: &Rc>>>, + cx: &mut WindowContext, + ) { + let new_popover = (builder)(cx); + + let popover_cloned = popover.clone(); + let prev_focus_handle = cx.focused(); + + cx.subscribe(&new_popover, move |modal, _: &DismissEvent, cx| { + if modal.focus_handle(cx).contains_focused(cx) { + if let Some(prev_focus_handle) = prev_focus_handle.as_ref() { + cx.focus(prev_focus_handle); + } + } + *popover_cloned.borrow_mut() = None; + cx.refresh(); + }) + .detach(); + + cx.focus_view(&new_popover); + *popover.borrow_mut() = Some(new_popover); + cx.refresh(); + } + + fn resolved_attach(&self) -> AnchorCorner { + match self.anchor { + AnchorCorner::TopLeft => AnchorCorner::BottomLeft, + AnchorCorner::TopRight => AnchorCorner::BottomRight, + AnchorCorner::BottomLeft => AnchorCorner::TopLeft, + AnchorCorner::BottomRight => AnchorCorner::TopRight, + } + } + + fn resolved_offset(&self, _cx: &WindowContext) -> Point { + let offset = px(0.); + match self.anchor { + AnchorCorner::TopRight | AnchorCorner::BottomRight => point(offset, px(0.)), + AnchorCorner::TopLeft | AnchorCorner::BottomLeft => point(-offset, px(0.)), + } + } +} + +pub struct PopoverElementState { + trigger_bounds: Option>, + popover: Rc>>>, +} + +impl Clone for PopoverElementState { + fn clone(&self) -> Self { + Self { + popover: Rc::clone(&self.popover), + trigger_bounds: self.trigger_bounds, + } + } +} + +impl Default for PopoverElementState { + fn default() -> Self { + Self { + popover: Rc::default(), + trigger_bounds: None, + } + } +} + +pub struct PopoverFrameState { + trigger_layout_id: Option, + trigger_element: Option, + popover_element: Option, +} + +impl IntoElement for Popover { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for Popover { + type RequestLayoutState = PopoverFrameState; + type PrepaintState = Option; + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn request_layout( + &mut self, + global_id: Option<&gpui::GlobalElementId>, + cx: &mut WindowContext, + ) -> (gpui::LayoutId, Self::RequestLayoutState) { + cx.with_element_state( + global_id.unwrap(), + |element_state: Option>, cx| { + let element_state = element_state.unwrap_or_default(); + + let mut popover_layout_id = None; + let popover_element = element_state.popover.borrow_mut().as_mut().map(|popover| { + let mut anchored = anchored().snap_to_window().anchor(self.anchor); + if let Some(trigger_bounds) = element_state.trigger_bounds { + anchored = anchored.position( + self.resolved_attach().corner(trigger_bounds) + + self.resolved_offset(cx), + ); + } + let mut element = deferred(anchored.child(popover.clone())) + .with_priority(1) + .into_any(); + + popover_layout_id = Some(element.request_layout(cx)); + element + }); + + let mut trigger_element = self.trigger_builder.take().map(|builder| { + builder(element_state.popover.clone(), self.content_builder.clone()) + }); + + let trigger_layout_id = trigger_element + .as_mut() + .map(|trigger_element| trigger_element.request_layout(cx)); + + let layout_id = cx.request_layout( + Style::default(), + popover_layout_id.into_iter().chain(trigger_layout_id), + ); + + ( + ( + layout_id, + PopoverFrameState { + trigger_layout_id, + trigger_element, + popover_element, + }, + ), + element_state, + ) + }, + ) + } + + fn prepaint( + &mut self, + global_id: Option<&gpui::GlobalElementId>, + _: gpui::Bounds, + request_layout: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) -> Self::PrepaintState { + if let Some(element) = &mut request_layout.trigger_element { + element.prepaint(cx); + } + + if let Some(element) = &mut request_layout.popover_element { + element.prepaint(cx); + } + + request_layout.trigger_layout_id.map(|layout_id| { + let bounds = cx.layout_bounds(layout_id); + cx.with_element_state(global_id.unwrap(), |element_state, _cx| { + let mut element_state: PopoverElementState = element_state.unwrap(); + element_state.trigger_bounds = Some(bounds); + ((), element_state) + }); + + cx.insert_hitbox(bounds, false).id + }) + } + + fn paint( + &mut self, + _: Option<&gpui::GlobalElementId>, + _: gpui::Bounds, + request_layout: &mut Self::RequestLayoutState, + trigger_hitbox: &mut Option, + cx: &mut WindowContext, + ) { + if let Some(element) = &mut request_layout.trigger_element { + element.paint(cx); + } + + if let Some(element) = &mut request_layout.popover_element { + element.paint(cx); + + if let Some(hitbox) = *trigger_hitbox { + // Mouse-downing outside the menu dismisses it, so we don't + // want a click on the toggle to re-open it. + cx.on_mouse_event(move |_: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { + cx.stop_propagation() + } + }) + } + } + } +} diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index a005f170..f4d0f00f 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -1,6 +1,7 @@ //! The prelude of this crate. When building UI in Zed you almost always want to import this. pub use gpui::prelude::*; +#[allow(unused_imports)] pub use gpui::{ div, px, relative, rems, AbsoluteLength, DefiniteLength, Div, Element, ElementId, InteractiveElement, ParentElement, Pixels, Rems, RenderOnce, SharedString, Styled, ViewContext, diff --git a/crates/ui/src/styled_ext.rs b/crates/ui/src/styled_ext.rs index 8007c6e6..5d4b5270 100644 --- a/crates/ui/src/styled_ext.rs +++ b/crates/ui/src/styled_ext.rs @@ -1,10 +1,10 @@ -use crate::theme::{ActiveTheme, Colorize}; +use crate::theme::ActiveTheme; use gpui::{hsla, point, px, BoxShadow, Styled, WindowContext}; use smallvec::{smallvec, SmallVec}; pub enum ElevationIndex { Surface, - ElevatedSurface, + PopoverSurface, ModalSurface, } @@ -13,7 +13,7 @@ impl ElevationIndex { match self { ElevationIndex::Surface => smallvec![], - ElevationIndex::ElevatedSurface => smallvec![BoxShadow { + ElevationIndex::PopoverSurface => smallvec![BoxShadow { color: hsla(0., 0., 0., 0.12), offset: point(px(0.), px(2.)), blur_radius: px(3.), @@ -22,26 +22,18 @@ impl ElevationIndex { ElevationIndex::ModalSurface => smallvec![ BoxShadow { - color: hsla(0., 0., 0., 0.12), - offset: point(px(0.), px(2.)), - blur_radius: px(3.), - spread_radius: px(0.), - }, - BoxShadow { - color: hsla(0., 0., 0., 0.08), - offset: point(px(0.), px(3.)), + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(4.)), blur_radius: px(6.), - spread_radius: px(0.), + spread_radius: px(-1.), }, BoxShadow { - color: hsla(0., 0., 0., 0.04), - offset: point(px(0.), px(6.)), - blur_radius: px(12.), - spread_radius: px(0.), - }, + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(2.)), + blur_radius: px(4.), + spread_radius: px(-2.), + } ], - - _ => smallvec![], } } } @@ -79,7 +71,7 @@ pub trait StyledExt: Styled + Sized { /// Appear above most UI elements fn elevation_2(self, cx: &mut WindowContext) -> Self { - elevated(self, cx, ElevationIndex::ElevatedSurface) + elevated(self, cx, ElevationIndex::PopoverSurface) } // Above all other UI elements and are located above the wash layer diff --git a/crates/ui/src/tab/tab.rs b/crates/ui/src/tab/tab.rs index 803b9910..1cef2ac5 100644 --- a/crates/ui/src/tab/tab.rs +++ b/crates/ui/src/tab/tab.rs @@ -1,6 +1,5 @@ use crate::selectable::Selectable; use crate::theme::{ActiveTheme, Colorize}; -use crate::Icon; use gpui::prelude::FluentBuilder as _; use gpui::{ div, px, AnyElement, Div, IntoElement, ParentElement as _, RenderOnce, SharedString, Stateful, diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index 66a441d9..cb263972 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -182,7 +182,7 @@ impl Colors { Colors { title_bar_background: hsl(0., 0., 12.), background: hsl(0.0, 0.0, 6.0), - foreground: hsl(240., 5., 65.), + foreground: hsl(0., 0., 98.), card: hsl(299.0, 2., 9.), card_foreground: hsl(0.0, 0.0, 98.0), popover: hsl(240.0, 10.0, 3.9), diff --git a/crates/ui/src/tooltip.rs b/crates/ui/src/tooltip.rs index aebe6e55..8a26b673 100644 --- a/crates/ui/src/tooltip.rs +++ b/crates/ui/src/tooltip.rs @@ -3,7 +3,7 @@ use gpui::{ SharedString, Styled, ViewContext, VisualContext, WindowContext, }; -use crate::{h_flex, styled_ext::ElevationIndex, theme::ActiveTheme, v_flex}; +use crate::{h_flex, theme::ActiveTheme, v_flex, StyledExt}; pub struct Tooltip { title: SharedString, @@ -63,7 +63,7 @@ pub fn tooltip_container( .rounded(px(8.)) .border_1() .border_color(cx.theme().border) - .shadow(ElevationIndex::ElevatedSurface.shadow()) + .elevation_2(cx) .text_color(cx.theme().popover_foreground) .py_1p5() .px_2() diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 4de93d32..1c7b49a5 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -5,3 +5,6 @@ edition = "2021" [dependencies] log.workspace = true + +[lints] +workspace = true diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 4f2d2a6c..3111fdb7 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -9,3 +9,6 @@ ui.workspace = true ui-story.workspace = true anyhow.workspace = true util.workspace = true + +[lints] +workspace = true diff --git a/crates/workspace/src/app_state.rs b/crates/workspace/src/app_state.rs index f832993a..ea5b5bcd 100644 --- a/crates/workspace/src/app_state.rs +++ b/crates/workspace/src/app_state.rs @@ -1,15 +1,15 @@ -use std::sync::{Arc, Weak}; +use std::sync::Weak; use gpui::{AppContext, Global}; pub struct AppState {} -struct GlobalAppState(Weak); +struct GlobalAppState(); impl Global for GlobalAppState {} impl AppState { - pub fn set_global(app_state: Weak, cx: &mut AppContext) { - cx.set_global(GlobalAppState(app_state)); + pub fn set_global(_app_state: Weak, cx: &mut AppContext) { + cx.set_global(GlobalAppState()); } } diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 3f9e1e33..e16cb955 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -173,7 +173,6 @@ impl Render for Workspace { .label_side(LabelSide::Left) .label("Dark Mode") .on_click(move |_, cx| { - dbg!("theme-mode clicked"); let mode = match cx.theme().mode.is_dark() { false => ui::theme::ThemeMode::Dark, true => ui::theme::ThemeMode::Light, @@ -185,6 +184,13 @@ impl Render for Workspace { ), ) .children(self.render_notifications(cx)) - .child(div().flex().px_4().gap_2().child(self.stories.clone())) + .child( + div() + .flex() + .flex_1() + .px_4() + .gap_2() + .child(self.stories.clone()), + ) } }