From 176a68f90f6d14dd67e6ac3e34b9d3d55c6d56fa Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 20 Nov 2023 10:46:23 -0500 Subject: [PATCH 01/35] kb --- assets/icons/arrow_down.svg | 3 + assets/icons/arrow_left.svg | 4 +- assets/icons/arrow_right.svg | 4 +- assets/icons/arrow_up.svg | 3 + assets/icons/command.svg | 3 + assets/icons/control.svg | 3 + assets/icons/option.svg | 3 + assets/icons/return.svg | 3 + assets/icons/shift.svg | 3 + crates/theme2/src/default_colors.rs | 24 +++---- crates/theme2/src/one_themes.rs | 4 +- crates/ui2/src/components/icon.rs | 18 ++++- crates/ui2/src/components/keybinding.rs | 90 +++++++++++++++++++------ 13 files changed, 126 insertions(+), 39 deletions(-) create mode 100644 assets/icons/arrow_down.svg create mode 100644 assets/icons/arrow_up.svg create mode 100644 assets/icons/command.svg create mode 100644 assets/icons/control.svg create mode 100644 assets/icons/option.svg create mode 100644 assets/icons/return.svg create mode 100644 assets/icons/shift.svg diff --git a/assets/icons/arrow_down.svg b/assets/icons/arrow_down.svg new file mode 100644 index 0000000000000000000000000000000000000000..7d78497e6d28f3088f035762c39e38ea9b0e9f7b --- /dev/null +++ b/assets/icons/arrow_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/arrow_left.svg b/assets/icons/arrow_left.svg index 186c9c7457c48405508de337fa5d1904f2563f59..57ee7504906134de8662732207eeb261fc6359f2 100644 --- a/assets/icons/arrow_left.svg +++ b/assets/icons/arrow_left.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/arrow_right.svg b/assets/icons/arrow_right.svg index 7bae7f4801a10b0ee04dfab93048bbdaf526045a..7a5b1174eb5a98250be404e052f3e197d95756b8 100644 --- a/assets/icons/arrow_right.svg +++ b/assets/icons/arrow_right.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/arrow_up.svg b/assets/icons/arrow_up.svg new file mode 100644 index 0000000000000000000000000000000000000000..81dfee8042609ff2a2a8b26026cddb43557b2be2 --- /dev/null +++ b/assets/icons/arrow_up.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/command.svg b/assets/icons/command.svg new file mode 100644 index 0000000000000000000000000000000000000000..d38389aea4b08fb19d1641e8735c960de9073562 --- /dev/null +++ b/assets/icons/command.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/control.svg b/assets/icons/control.svg new file mode 100644 index 0000000000000000000000000000000000000000..94189dc07dbe761cd1cc71b22598e1d2a2869ed5 --- /dev/null +++ b/assets/icons/control.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/option.svg b/assets/icons/option.svg new file mode 100644 index 0000000000000000000000000000000000000000..9d54a6f34b1546bc7b1ed907f231eeb80f0c1264 --- /dev/null +++ b/assets/icons/option.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/return.svg b/assets/icons/return.svg new file mode 100644 index 0000000000000000000000000000000000000000..683519c3066fef61cb256a103dcd219fe59ff9f1 --- /dev/null +++ b/assets/icons/return.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/shift.svg b/assets/icons/shift.svg new file mode 100644 index 0000000000000000000000000000000000000000..02321147773469108283afbe65a68b303665f4d3 --- /dev/null +++ b/assets/icons/shift.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 91efecbfb310103deeafb683becf31b9b7732c42..4a47bc05366c5c8a9063e1b4f8fd4be560b11195 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -23,15 +23,15 @@ impl ThemeColors { surface_background: neutral().light().step_2(), background: neutral().light().step_1(), element_background: neutral().light().step_3(), - element_hover: neutral().light().step_4(), - element_active: neutral().light().step_5(), - element_selected: neutral().light().step_5(), + element_hover: neutral().light_alpha().step_4(), + element_active: neutral().light_alpha().step_5(), + element_selected: neutral().light_alpha().step_5(), element_disabled: neutral().light_alpha().step_3(), drop_target_background: blue().light_alpha().step_2(), ghost_element_background: system.transparent, - ghost_element_hover: neutral().light().step_4(), - ghost_element_active: neutral().light().step_5(), - ghost_element_selected: neutral().light().step_5(), + ghost_element_hover: neutral().light_alpha().step_4(), + ghost_element_active: neutral().light_alpha().step_5(), + ghost_element_selected: neutral().light_alpha().step_5(), ghost_element_disabled: neutral().light_alpha().step_3(), text: yellow().light().step_9(), text_muted: neutral().light().step_11(), @@ -95,15 +95,15 @@ impl ThemeColors { surface_background: neutral().dark().step_2(), background: neutral().dark().step_1(), element_background: neutral().dark().step_3(), - element_hover: neutral().dark().step_4(), - element_active: neutral().dark().step_5(), - element_selected: neutral().dark().step_5(), + element_hover: neutral().dark_alpha().step_4(), + element_active: neutral().dark_alpha().step_5(), + element_selected: neutral().dark_alpha().step_5(), element_disabled: neutral().dark_alpha().step_3(), drop_target_background: blue().dark_alpha().step_2(), ghost_element_background: system.transparent, - ghost_element_hover: neutral().dark().step_4(), - ghost_element_active: neutral().dark().step_5(), - ghost_element_selected: neutral().dark().step_5(), + ghost_element_hover: neutral().dark_alpha().step_4(), + ghost_element_active: neutral().dark_alpha().step_5(), + ghost_element_selected: neutral().dark_alpha().step_5(), ghost_element_disabled: neutral().dark_alpha().step_3(), text: neutral().dark().step_12(), text_muted: neutral().dark().step_11(), diff --git a/crates/theme2/src/one_themes.rs b/crates/theme2/src/one_themes.rs index 733cd6c40b6c789f484890b7a7591e5ae3cc6d40..533323ce51119170fb591e9c5847efa967f38145 100644 --- a/crates/theme2/src/one_themes.rs +++ b/crates/theme2/src/one_themes.rs @@ -20,7 +20,7 @@ pub fn one_family() -> ThemeFamily { pub(crate) fn one_dark() -> Theme { let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.); let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.); - let elevated_surface = hsla(220. / 360., 12. / 100., 18. / 100., 1.); + let elevated_surface = hsla(225. / 360., 12. / 100., 17. / 100., 1.); let blue = hsla(207.8 / 360., 81. / 100., 66. / 100., 1.0); let gray = hsla(218.8 / 360., 10. / 100., 40. / 100., 1.0); @@ -48,7 +48,7 @@ pub(crate) fn one_dark() -> Theme { elevated_surface_background: elevated_surface, surface_background: bg, background: bg, - element_background: elevated_surface, + element_background: hsla(223.0 / 360., 13. / 100., 21. / 100., 1.0), element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0), element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 38b477792599920fc5cc90fc722f2fd4299018b0..48f60413c540570b0e068cae27d0a9969125cdda 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -14,6 +14,8 @@ pub enum IconSize { pub enum Icon { Ai, ArrowLeft, + ArrowUp, + ArrowDown, ArrowRight, ArrowUpRight, AtSign, @@ -66,6 +68,11 @@ pub enum Icon { SplitMessage, Terminal, XCircle, + Command, + Control, + Shift, + Option, + Return, } impl Icon { @@ -74,6 +81,8 @@ impl Icon { Icon::Ai => "icons/ai.svg", Icon::ArrowLeft => "icons/arrow_left.svg", Icon::ArrowRight => "icons/arrow_right.svg", + Icon::ArrowUp => "icons/arrow_up.svg", + Icon::ArrowDown => "icons/arrow_down.svg", Icon::ArrowUpRight => "icons/arrow_up_right.svg", Icon::AtSign => "icons/at-sign.svg", Icon::AudioOff => "icons/speaker-off.svg", @@ -125,6 +134,11 @@ impl Icon { Icon::SplitMessage => "icons/split_message.svg", Icon::Terminal => "icons/terminal.svg", Icon::XCircle => "icons/error.svg", + Icon::Command => "icons/command.svg", + Icon::Control => "icons/control.svg", + Icon::Shift => "icons/shift.svg", + Icon::Option => "icons/option.svg", + Icon::Return => "icons/return.svg", } } } @@ -165,8 +179,8 @@ impl IconElement { fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let svg_size = match self.size { - IconSize::Small => rems(0.75), - IconSize::Medium => rems(0.9375), + IconSize::Small => rems(14. / 16.), + IconSize::Medium => rems(16. / 16.), }; svg() diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index 69396274faba449d8e32c6eace4536e35c7a2085..c0690ce1c718917fa4768e5da2d931a2d42584c6 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -1,7 +1,7 @@ -use gpui::{actions, Action}; +use gpui::{actions, relative, rems, Action, Styled}; use strum::EnumIter; -use crate::prelude::*; +use crate::{h_stack, prelude::*, Icon, IconElement, IconSize}; #[derive(Component, Clone)] pub struct KeyBinding { @@ -24,20 +24,46 @@ impl KeyBinding { Self { key_binding } } + fn icon_for_key(key: &str) -> Option { + match key { + "left" => Some(Icon::ArrowLeft), + "right" => Some(Icon::ArrowRight), + "up" => Some(Icon::ArrowUp), + "down" => Some(Icon::ArrowDown), + _ => None, + } + } + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - div() - .flex() - .gap_2() + h_stack() + .flex_none() + .gap_1() .children(self.key_binding.keystrokes().iter().map(|keystroke| { - div() - .flex() - .gap_1() + let key_icon = Self::icon_for_key(&keystroke.key); + + h_stack() + .flex_none() + .gap_0p5() + .bg(cx.theme().colors().element_background) + .p_0p5() + .rounded_sm() .when(keystroke.modifiers.function, |el| el.child(Key::new("fn"))) - .when(keystroke.modifiers.control, |el| el.child(Key::new("^"))) - .when(keystroke.modifiers.alt, |el| el.child(Key::new("⌥"))) - .when(keystroke.modifiers.command, |el| el.child(Key::new("⌘"))) - .when(keystroke.modifiers.shift, |el| el.child(Key::new("⇧"))) - .child(Key::new(keystroke.key.clone())) + .when(keystroke.modifiers.control, |el| { + el.child(KeyIcon::new(Icon::Control)) + }) + .when(keystroke.modifiers.alt, |el| { + el.child(KeyIcon::new(Icon::Option)) + }) + .when(keystroke.modifiers.command, |el| { + el.child(KeyIcon::new(Icon::Command)) + }) + .when(keystroke.modifiers.shift, |el| { + el.child(KeyIcon::new(Icon::Shift)) + }) + .when_some(key_icon, |el, icon| el.child(KeyIcon::new(icon))) + .when(key_icon.is_none(), |el| { + el.child(Key::new(keystroke.key.to_uppercase().clone())) + }) })) } } @@ -53,25 +79,51 @@ impl Key { } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + let single_char = self.key.len() == 1; + div() - .px_2() + // .px_0p5() .py_0() - .rounded_md() - .text_ui_sm() + .when(single_char, |el| { + el.w(rems(14. / 16.)).flex().flex_none().justify_center() + }) + .when(!single_char, |el| el.px_0p5()) + .h(rems(14. / 16.)) + // .rounded_md() + .text_ui() + .line_height(relative(1.)) .text_color(cx.theme().colors().text) - .bg(cx.theme().colors().element_background) + // .bg(cx.theme().colors().element_background) .child(self.key.clone()) } } +#[derive(Component)] +pub struct KeyIcon { + icon: Icon, +} + +impl KeyIcon { + pub fn new(icon: Icon) -> Self { + Self { icon } + } + + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + div() + .w(rems(14. / 16.)) + // .bg(cx.theme().colors().element_background) + .child(IconElement::new(self.icon).size(IconSize::Small)) + } +} + // NOTE: The order the modifier keys appear in this enum impacts the order in // which they are rendered in the UI. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] pub enum ModifierKey { Control, - Alt, - Command, + Alt, // Option Shift, + Command, } actions!(NoAction); From 1acc6b462f4c671031faefe02c240ed32dcec0dd Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 27 Nov 2023 10:29:29 -0500 Subject: [PATCH 02/35] Start on completions styles --- crates/editor2/src/editor.rs | 22 +++++++++++++++++----- crates/ui2/src/components/popover.rs | 2 ++ crates/ui2/src/styled_ext.rs | 4 ++-- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 3a9a40328d2b6dc0a11cbc75004f2d9c36b865ee..a0f074221c4ba0eb2f2db2b148a59796e5ef140a 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1273,6 +1273,13 @@ impl CompletionsMenu { multiline_docs.map(|div| { div.id("multiline_docs") .max_h(max_height) + .flex_1() + .px_1p5() + .py_1() + .min_w(px(260.)) + .max_w(px(640.)) + .w(px(500.)) + .text_ui() .overflow_y_scroll() // Prevent a mouse down on documentation from being propagated to the editor, // because that would move the cursor. @@ -1327,13 +1334,18 @@ impl CompletionsMenu { div() .id(mat.candidate_id) - .min_w(px(300.)) - .max_w(px(700.)) + .min_w(px(220.)) + .max_w(px(640.)) .whitespace_nowrap() .overflow_hidden() - .bg(gpui::green()) - .hover(|style| style.bg(gpui::blue())) - .when(item_ix == selected_item, |div| div.bg(gpui::red())) + .text_ui() + .px_1() + .rounded(px(4.)) + .bg(cx.theme().colors().ghost_element_background) + .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .when(item_ix == selected_item, |div| { + div.bg(cx.theme().colors().ghost_element_selected) + }) .on_mouse_down( MouseButton::Left, cx.listener(move |editor, event, cx| { diff --git a/crates/ui2/src/components/popover.rs b/crates/ui2/src/components/popover.rs index ac407d23353bfb65e031b651d37b639d06c0fd42..3838e40bec527fb191c6a0e187c6982dcb643c59 100644 --- a/crates/ui2/src/components/popover.rs +++ b/crates/ui2/src/components/popover.rs @@ -3,6 +3,7 @@ use gpui::{ WindowContext, }; use smallvec::SmallVec; +use theme2::ActiveTheme; use crate::{v_stack, StyledExt}; @@ -58,6 +59,7 @@ impl RenderOnce for Popover { .ml_1() .absolute() .elevation_2(cx) + .bg(cx.theme().colors().surface_background) .p_1() .child(aside), ) diff --git a/crates/ui2/src/styled_ext.rs b/crates/ui2/src/styled_ext.rs index 9e81ab19ee37b4bebb4ba6845a27b8c28498a47d..d064312c327bc5cddfc7784b3ef0793690e0da79 100644 --- a/crates/ui2/src/styled_ext.rs +++ b/crates/ui2/src/styled_ext.rs @@ -1,4 +1,4 @@ -use gpui::{Styled, WindowContext}; +use gpui::{px, Styled, WindowContext}; use theme2::ActiveTheme; use crate::{ElevationIndex, UITextSize}; @@ -6,7 +6,7 @@ use crate::{ElevationIndex, UITextSize}; fn elevated(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E { this.bg(cx.theme().colors().elevated_surface_background) .z_index(index.z_index()) - .rounded_lg() + .rounded(px(8.)) .border() .border_color(cx.theme().colors().border_variant) .shadow(index.shadow()) From 8a35a028637e943d594526ef9b29e7c30b5a465d Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 27 Nov 2023 11:22:19 -0500 Subject: [PATCH 03/35] Checkpoint - try using overlay for completions popover [no ci] --- crates/editor2/src/editor.rs | 37 +++++++++++++++++++--------- crates/gpui2/src/elements/overlay.rs | 1 + crates/ui2/src/components/popover.rs | 19 ++++++-------- 3 files changed, 34 insertions(+), 23 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index a0f074221c4ba0eb2f2db2b148a59796e5ef140a..3aa53077182a26d0c8f813db3ef18fd8d8d048ff 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -39,12 +39,13 @@ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; use gpui::{ - actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement, - AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, ElementId, - EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle, - Hsla, InputHandler, InteractiveText, KeyContext, Model, MouseButton, ParentElement, Pixels, - Render, RenderOnce, SharedString, Styled, StyledText, Subscription, Task, TextRun, TextStyle, - UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext, + actions, div, overlay, point, prelude::*, px, relative, rems, size, uniform_list, Action, + AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, + ElementId, EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, + HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model, MouseButton, + ParentElement, Pixels, Render, RenderOnce, SharedString, Styled, StyledText, Subscription, + Task, TextRun, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, + WhiteSpace, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -1335,7 +1336,7 @@ impl CompletionsMenu { div() .id(mat.candidate_id) .min_w(px(220.)) - .max_w(px(640.)) + .max_w(px(540.)) .whitespace_nowrap() .overflow_hidden() .text_ui() @@ -1370,11 +1371,23 @@ impl CompletionsMenu { .track_scroll(self.scroll_handle.clone()) .with_width_from_item(widest_completion_ix); - Popover::new() - .child(list) - .when_some(multiline_docs, |popover, multiline_docs| { - popover.aside(multiline_docs) - }) + // Old: + // Popover::new() + // .child(list) + // .when_some(multiline_docs, |popover, multiline_docs| { + // popover.aside(multiline_docs) + // }) + // .into_any_element() + + overlay() + .anchor(gpui::AnchorCorner::TopLeft) + .child( + Popover::new() + .child(list) + .when_some(multiline_docs, |popover, multiline_docs| { + popover.aside(multiline_docs) + }), + ) .into_any_element() } diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index d8aad4a42f9413f60e05d59bfab5d0606d4dd5d2..3d5c7ac0b551d36a3045240c7afc1d783f684dcb 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -105,6 +105,7 @@ impl Element for Overlay { origin: Point::zero(), size: cx.viewport_size(), }; + dbg!(limits); match self.fit_mode { OverlayFitMode::SnapToWindow => { diff --git a/crates/ui2/src/components/popover.rs b/crates/ui2/src/components/popover.rs index 3838e40bec527fb191c6a0e187c6982dcb643c59..62b624de0e7291e5b62238ea8745ce059694ede3 100644 --- a/crates/ui2/src/components/popover.rs +++ b/crates/ui2/src/components/popover.rs @@ -1,11 +1,11 @@ use gpui::{ - AnyElement, Div, Element, ElementId, IntoElement, ParentElement, RenderOnce, Styled, + div, AnyElement, Div, Element, ElementId, IntoElement, ParentElement, RenderOnce, Styled, WindowContext, }; use smallvec::SmallVec; use theme2::ActiveTheme; -use crate::{v_stack, StyledExt}; +use crate::{h_stack, v_stack, StyledExt}; /// A popover is used to display a menu or show some options. /// @@ -44,21 +44,18 @@ impl RenderOnce for Popover { type Rendered = Div; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - v_stack() - .relative() - .elevation_2(cx) - .p_1() - .children(self.children) + div() + .flex() + .flex_none() + .gap_1() + .child(v_stack().elevation_2(cx).p_1().children(self.children)) .when_some(self.aside, |this, aside| { // TODO: This will statically position the aside to the top right of the popover. // We should update this to use gpui2::overlay avoid collisions with the window edges. this.child( v_stack() - .top_0() - .left_full() - .ml_1() - .absolute() .elevation_2(cx) + .flex_1() .bg(cx.theme().colors().surface_background) .p_1() .child(aside), From ab83f4319baa5af2307802aba88e901bce51b673 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 27 Nov 2023 11:51:42 -0500 Subject: [PATCH 04/35] revert popover changes [no ci] --- crates/ui2/src/components/popover.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/ui2/src/components/popover.rs b/crates/ui2/src/components/popover.rs index 62b624de0e7291e5b62238ea8745ce059694ede3..925fb30a5e81cbbb4048cee8309dec51458ecd3f 100644 --- a/crates/ui2/src/components/popover.rs +++ b/crates/ui2/src/components/popover.rs @@ -44,18 +44,21 @@ impl RenderOnce for Popover { type Rendered = Div; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - div() - .flex() - .flex_none() - .gap_1() - .child(v_stack().elevation_2(cx).p_1().children(self.children)) + v_stack() + .relative() + .elevation_2(cx) + .p_1() + .children(self.children) .when_some(self.aside, |this, aside| { // TODO: This will statically position the aside to the top right of the popover. // We should update this to use gpui2::overlay avoid collisions with the window edges. this.child( v_stack() + .top_0() + .left_full() + .ml_1() + .absolute() .elevation_2(cx) - .flex_1() .bg(cx.theme().colors().surface_background) .p_1() .child(aside), From 82f6f771172e123f1d853a2fe9de0d4dda03e51c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 27 Nov 2023 11:48:10 -0700 Subject: [PATCH 05/35] Use editor's overlay implementation --- crates/editor2/src/editor.rs | 35 +++++++++------------------- crates/ui2/src/components/popover.rs | 4 ++-- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index a15d18f030852101b36dfbe501b399849c19cae4..c713ab085b7e761af4108abb5a106c92c1c5deca 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -39,13 +39,12 @@ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; use gpui::{ - actions, div, overlay, point, prelude::*, px, relative, rems, size, uniform_list, Action, - AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, - ElementId, EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, - HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model, MouseButton, - ParentElement, Pixels, Render, RenderOnce, SharedString, Styled, StyledText, Subscription, - Task, TextRun, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, - WhiteSpace, WindowContext, + actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement, + AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, ElementId, + EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle, + Hsla, InputHandler, InteractiveText, KeyContext, Model, MouseButton, ParentElement, Pixels, + Render, RenderOnce, SharedString, Styled, StyledText, Subscription, Task, TextRun, TextStyle, + UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -1369,23 +1368,11 @@ impl CompletionsMenu { .track_scroll(self.scroll_handle.clone()) .with_width_from_item(widest_completion_ix); - // Old: - // Popover::new() - // .child(list) - // .when_some(multiline_docs, |popover, multiline_docs| { - // popover.aside(multiline_docs) - // }) - // .into_any_element() - - overlay() - .anchor(gpui::AnchorCorner::TopLeft) - .child( - Popover::new() - .child(list) - .when_some(multiline_docs, |popover, multiline_docs| { - popover.aside(multiline_docs) - }), - ) + Popover::new() + .child(list) + .when_some(multiline_docs, |popover, multiline_docs| { + popover.aside(multiline_docs) + }) .into_any_element() } diff --git a/crates/ui2/src/components/popover.rs b/crates/ui2/src/components/popover.rs index 925fb30a5e81cbbb4048cee8309dec51458ecd3f..3838e40bec527fb191c6a0e187c6982dcb643c59 100644 --- a/crates/ui2/src/components/popover.rs +++ b/crates/ui2/src/components/popover.rs @@ -1,11 +1,11 @@ use gpui::{ - div, AnyElement, Div, Element, ElementId, IntoElement, ParentElement, RenderOnce, Styled, + AnyElement, Div, Element, ElementId, IntoElement, ParentElement, RenderOnce, Styled, WindowContext, }; use smallvec::SmallVec; use theme2::ActiveTheme; -use crate::{h_stack, v_stack, StyledExt}; +use crate::{v_stack, StyledExt}; /// A popover is used to display a menu or show some options. /// From e31a8f05367857e30795bc4d1ff8cb2d8a9de4d3 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 27 Nov 2023 12:04:08 -0700 Subject: [PATCH 06/35] Scaffolding for update notification too --- crates/auto_update2/src/auto_update.rs | 11 +- .../auto_update2/src/update_notification.rs | 102 ++++++------------ 2 files changed, 43 insertions(+), 70 deletions(-) diff --git a/crates/auto_update2/src/auto_update.rs b/crates/auto_update2/src/auto_update.rs index aeff68965fd07ce7eda4cc0aac9bb8a7aaeb4649..d2eab15d09967a84c5c11004ec70419795e0bf01 100644 --- a/crates/auto_update2/src/auto_update.rs +++ b/crates/auto_update2/src/auto_update.rs @@ -84,8 +84,8 @@ impl Settings for AutoUpdateSetting { pub fn init(http_client: Arc, server_url: String, cx: &mut AppContext) { AutoUpdateSetting::register(cx); - cx.observe_new_views(|wokrspace: &mut Workspace, _cx| { - wokrspace + cx.observe_new_views(|workspace: &mut Workspace, _cx| { + workspace .register_action(|_, action: &Check, cx| check(action, cx)) .register_action(|_, _action: &CheckThatAutoUpdaterWorks, cx| { let prompt = cx.prompt(gpui::PromptLevel::Info, "It does!", &["Ok"]); @@ -94,6 +94,11 @@ pub fn init(http_client: Arc, server_url: String, cx: &mut AppCo }) .detach(); }); + + // @nate - code to trigger update notification on launch + // workspace.show_notification(0, _cx, |cx| { + // cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap())) + // }); }) .detach(); @@ -131,7 +136,7 @@ pub fn check(_: &Check, cx: &mut AppContext) { } } -fn _view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { +pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { if let Some(auto_updater) = AutoUpdater::get(cx) { let auto_updater = auto_updater.read(cx); let server_url = &auto_updater.server_url; diff --git a/crates/auto_update2/src/update_notification.rs b/crates/auto_update2/src/update_notification.rs index 9cb1550bd431eb81d7c0e0c8dc4a49655f5d73ce..d15d82e112e612143ef2aaff509965899818e95a 100644 --- a/crates/auto_update2/src/update_notification.rs +++ b/crates/auto_update2/src/update_notification.rs @@ -1,10 +1,12 @@ use gpui::{ - div, DismissEvent, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext, + div, DismissEvent, Div, EventEmitter, InteractiveElement, ParentElement, Render, + SemanticVersion, StatefulInteractiveElement, Styled, ViewContext, }; -use menu::Cancel; +use util::channel::ReleaseChannel; +use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt}; pub struct UpdateNotification { - _version: SemanticVersion, + version: SemanticVersion, } impl EventEmitter for UpdateNotification {} @@ -12,77 +14,43 @@ impl EventEmitter for UpdateNotification {} impl Render for UpdateNotification { type Element = Div; - fn render(&mut self, _cx: &mut gpui::ViewContext) -> Self::Element { - div().child("Updated zed!") - // let theme = theme::current(cx).clone(); - // let theme = &theme.update_notification; - - // let app_name = cx.global::().display_name(); - - // MouseEventHandler::new::(0, cx, |state, cx| { - // Flex::column() - // .with_child( - // Flex::row() - // .with_child( - // Text::new( - // format!("Updated to {app_name} {}", self.version), - // theme.message.text.clone(), - // ) - // .contained() - // .with_style(theme.message.container) - // .aligned() - // .top() - // .left() - // .flex(1., true), - // ) - // .with_child( - // MouseEventHandler::new::(0, cx, |state, _| { - // let style = theme.dismiss_button.style_for(state); - // Svg::new("icons/x.svg") - // .with_color(style.color) - // .constrained() - // .with_width(style.icon_width) - // .aligned() - // .contained() - // .with_style(style.container) - // .constrained() - // .with_width(style.button_width) - // .with_height(style.button_width) - // }) - // .with_padding(Padding::uniform(5.)) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.dismiss(&Default::default(), cx) - // }) - // .aligned() - // .constrained() - // .with_height(cx.font_cache().line_height(theme.message.text.font_size)) - // .aligned() - // .top() - // .flex_float(), - // ), - // ) - // .with_child({ - // let style = theme.action_message.style_for(state); - // Text::new("View the release notes", style.text.clone()) - // .contained() - // .with_style(style.container) - // }) - // .contained() - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, |_, _, cx| { - // crate::view_release_notes(&Default::default(), cx) - // }) - // .into_any_named("update notification") + fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { + let app_name = cx.global::().display_name(); + + v_stack() + .elevation_3(cx) + .p_4() + .child( + h_stack() + .justify_between() + .child(Label::new(format!( + "Updated to {app_name} {}", + self.version + ))) + .child( + div() + .id("cancel") + .child(IconElement::new(Icon::Close)) + .cursor_pointer() + .on_click(cx.listener(|this, _, cx| this.dismiss(cx))), + ), + ) + .child( + div() + .id("notes") + .child(Label::new("View the release notes")) + .cursor_pointer() + .on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)), + ) } } impl UpdateNotification { pub fn new(version: SemanticVersion) -> Self { - Self { _version: version } + Self { version } } - pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext) { + pub fn dismiss(&mut self, cx: &mut ViewContext) { cx.emit(DismissEvent::Dismiss); } } From 6f839a1b480684449fc6b5bc0ce0f7aeb7c04b70 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 27 Nov 2023 13:00:12 -0700 Subject: [PATCH 07/35] Add logged out collab panel --- crates/collab_ui2/src/collab_panel.rs | 91 ++++++++++++------- crates/collab_ui2/src/collab_titlebar_item.rs | 12 ++- 2 files changed, 65 insertions(+), 38 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index e9c17a6589dd79c73edc9b6e29bea4c76740b20e..c1c966972131ce3e065fb063b85a31be5d74317c 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -167,10 +167,11 @@ use gpui::{ use project::Fs; use serde_derive::{Deserialize, Serialize}; use settings::Settings; -use ui::{h_stack, Avatar, Label}; +use ui::{h_stack, v_stack, Avatar, Button, Label}; use util::ResultExt; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, + notifications::NotifyResultExt, Workspace, }; @@ -302,7 +303,7 @@ pub struct CollabPanel { // entries: Vec, // selection: Option, user_store: Model, - _client: Arc, + client: Arc, // channel_store: ModelHandle, // project: ModelHandle, // match_candidates: Vec, @@ -311,7 +312,7 @@ pub struct CollabPanel { // collapsed_sections: Vec
, // collapsed_channels: Vec, // drag_target_channel: ChannelDragTarget, - _workspace: WeakView, + workspace: WeakView, // context_menu_on_selected: bool, } @@ -604,8 +605,8 @@ impl CollabPanel { // match_candidates: Vec::default(), // collapsed_sections: vec![Section::Offline], // collapsed_channels: Vec::default(), - _workspace: workspace.weak_handle(), - _client: workspace.app_state().client.clone(), + workspace: workspace.weak_handle(), + client: workspace.app_state().client.clone(), // context_menu_on_selected: true, // drag_target_channel: ChannelDragTarget::None, // list_state, @@ -3252,6 +3253,53 @@ impl CollabPanel { // let item = ClipboardItem::new(channel.link()); // cx.write_to_clipboard(item) // } + + fn render_signed_out(&mut self, cx: &mut ViewContext) -> Div { + v_stack().child(Button::new("Sign in to collaborate").on_click(cx.listener( + |this, _, cx| { + let client = this.client.clone(); + cx.spawn(|_, mut cx| async move { + client + .authenticate_and_connect(true, &cx) + .await + .notify_async_err(&mut cx); + }) + .detach() + }, + ))) + } + + fn render_signed_in(&mut self, cx: &mut ViewContext) -> Div { + let contacts = self.contacts(cx).unwrap_or_default(); + let workspace = self.workspace.clone(); + + v_stack().children(contacts.into_iter().map(|contact| { + let id = contact.user.id; + h_stack() + .p_2() + .gap_2() + .children( + contact + .user + .avatar + .as_ref() + .map(|avatar| Avatar::data(avatar.clone())), + ) + .child(Label::new(contact.user.github_login.clone())) + .on_mouse_down(gpui::MouseButton::Left, { + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state() + .invite(id, None, cx) + .detach_and_log_err(cx) + }) + .log_err(); + } + }) + })) + } } // fn render_tree_branch( @@ -3303,37 +3351,14 @@ impl Render for CollabPanel { type Element = Focusable
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let contacts = self.contacts(cx).unwrap_or_default(); - let workspace = self._workspace.clone(); div() .key_context("CollabPanel") .track_focus(&self.focus_handle) - .children(contacts.into_iter().map(|contact| { - let id = contact.user.id; - h_stack() - .p_2() - .gap_2() - .children( - contact - .user - .avatar - .as_ref() - .map(|avatar| Avatar::data(avatar.clone())), - ) - .child(Label::new(contact.user.github_login.clone())) - .on_mouse_down(gpui::MouseButton::Left, { - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state() - .invite(id, None, cx) - .detach_and_log_err(cx) - }) - .log_err(); - } - }) - })) + .child(if self.user_store.read(cx).current_user().is_none() { + self.render_signed_out(cx) + } else { + self.render_signed_in(cx) + }) } } diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index d208eb91f18e7bdeef40ddf58c1f49628d9facb2..2307ba2fcbfb3d67b732cddae0c6305fdb4a92de 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -39,7 +39,7 @@ use project::Project; use theme::ActiveTheme; use ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip}; use util::ResultExt; -use workspace::Workspace; +use workspace::{notifications::NotifyResultExt, Workspace}; use crate::face_pile::FacePile; @@ -290,11 +290,13 @@ impl Render for CollabTitlebarItem { } else { this.child(Button::new("Sign in").on_click(move |_, cx| { let client = client.clone(); - cx.spawn(move |cx| async move { - client.authenticate_and_connect(true, &cx).await?; - Ok::<(), anyhow::Error>(()) + cx.spawn(move |mut cx| async move { + client + .authenticate_and_connect(true, &cx) + .await + .notify_async_err(&mut cx); }) - .detach_and_log_err(cx); + .detach(); })) } }) From 52119ca203c3d6a7b9e7deda382aedc93412117c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 27 Nov 2023 21:06:06 +0100 Subject: [PATCH 08/35] call: Restore mute_on_join behaviour --- crates/call2/src/room.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/call2/src/room.rs b/crates/call2/src/room.rs index b55d3348dcc37103b2b76e540ce878106d424fa2..e54455a87e5b9e5257c08a754e8fbed6ec25eb23 100644 --- a/crates/call2/src/room.rs +++ b/crates/call2/src/room.rs @@ -1,4 +1,7 @@ -use crate::participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}; +use crate::{ + call_settings::CallSettings, + participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}, +}; use anyhow::{anyhow, Result}; use audio::{Audio, Sound}; use client::{ @@ -18,6 +21,7 @@ use live_kit_client::{ }; use postage::{sink::Sink, stream::Stream, watch}; use project::Project; +use settings::Settings as _; use std::{future::Future, mem, sync::Arc, time::Duration}; use util::{post_inc, ResultExt, TryFutureExt}; @@ -328,10 +332,9 @@ impl Room { } } - pub fn mute_on_join(_cx: &AppContext) -> bool { + pub fn mute_on_join(cx: &AppContext) -> bool { // todo!() po: This should be uncommented, though then unmuting does not work - false - //CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() + CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() } fn from_join_response( From b0d9e3c8fad09e62fc07d3aef9ec75c5b78dd9c9 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 27 Nov 2023 21:44:53 +0100 Subject: [PATCH 09/35] Await toggle of mute --- crates/call2/src/call2.rs | 7 +++++-- crates/call2/src/room.rs | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs index 9a89ec7167f776ab2cb96ad60875c69293d6f0d0..7885ef6e3f3b92fc3b6947ce3407b092ee1bb82c 100644 --- a/crates/call2/src/call2.rs +++ b/crates/call2/src/call2.rs @@ -660,9 +660,12 @@ impl CallHandler for Call { self.active_call.as_ref().map(|call| { call.0.update(cx, |this, cx| { this.room().map(|room| { - room.update(cx, |this, cx| { - this.toggle_mute(cx).log_err(); + let room = room.clone(); + cx.spawn(|_, mut cx| async move { + room.update(&mut cx, |this, cx| this.toggle_mute(cx))?? + .await }) + .detach_and_log_err(cx); }) }) }); diff --git a/crates/call2/src/room.rs b/crates/call2/src/room.rs index e54455a87e5b9e5257c08a754e8fbed6ec25eb23..d091f801f86140a353e4fec02e76012c32a4763a 100644 --- a/crates/call2/src/room.rs +++ b/crates/call2/src/room.rs @@ -1268,7 +1268,6 @@ impl Room { .ok_or_else(|| anyhow!("live-kit was not initialized"))? .await }; - let publication = publish_track.await; this.upgrade() .ok_or_else(|| anyhow!("room was dropped"))? From f8614b5909ae973dddc18cacdeb7ab746684cb14 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 27 Nov 2023 21:46:03 +0100 Subject: [PATCH 10/35] fixup! Await toggle of mute --- crates/call2/src/room.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/call2/src/room.rs b/crates/call2/src/room.rs index d091f801f86140a353e4fec02e76012c32a4763a..694966abe9d509f5dc9cad5353a63aac074eaf50 100644 --- a/crates/call2/src/room.rs +++ b/crates/call2/src/room.rs @@ -333,7 +333,6 @@ impl Room { } pub fn mute_on_join(cx: &AppContext) -> bool { - // todo!() po: This should be uncommented, though then unmuting does not work CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() } From 4a25fae51efa030d8ba74903ce86c8daa2c8d93e Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 27 Nov 2023 16:22:01 -0700 Subject: [PATCH 11/35] TEMP --- crates/collab_ui2/src/collab_panel.rs | 62 +++++++++++++++++---------- crates/ui2/src/components/list.rs | 16 +++---- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index c1c966972131ce3e065fb063b85a31be5d74317c..5cce89416558a988dce5754044f710bc1a3394b2 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -155,19 +155,19 @@ actions!( const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; -use std::sync::Arc; +use std::{iter::once, sync::Arc}; use client::{Client, Contact, UserStore}; use db::kvp::KEY_VALUE_STORE; use gpui::{ actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle, - Focusable, FocusableView, InteractiveElement, Model, ParentElement, Render, Styled, View, - ViewContext, VisualContext, WeakView, + Focusable, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, + RenderOnce, Styled, View, ViewContext, VisualContext, WeakView, }; use project::Fs; use serde_derive::{Deserialize, Serialize}; use settings::Settings; -use ui::{h_stack, v_stack, Avatar, Button, Label}; +use ui::{h_stack, v_stack, Avatar, Button, Icon, IconButton, Label, List, ListHeader}; use util::ResultExt; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -309,7 +309,7 @@ pub struct CollabPanel { // match_candidates: Vec, // list_state: ListState, // subscriptions: Vec, - // collapsed_sections: Vec
, + collapsed_sections: Vec
, // collapsed_channels: Vec, // drag_target_channel: ChannelDragTarget, workspace: WeakView, @@ -336,16 +336,16 @@ struct SerializedCollabPanel { // Dismissed, // } -// #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] -// enum Section { -// ActiveCall, -// Channels, -// ChannelInvites, -// ContactRequests, -// Contacts, -// Online, -// Offline, -// } +#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] +enum Section { + // ActiveCall, + // Channels, + // ChannelInvites, + // ContactRequests, + Contacts, + // Online, + Offline, +} // #[derive(Clone, Debug)] // enum ListEntry { @@ -603,7 +603,7 @@ impl CollabPanel { // project: workspace.project().clone(), // subscriptions: Vec::default(), // match_candidates: Vec::default(), - // collapsed_sections: vec![Section::Offline], + collapsed_sections: vec![Section::Offline], // collapsed_channels: Vec::default(), workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), @@ -3269,11 +3269,23 @@ impl CollabPanel { ))) } - fn render_signed_in(&mut self, cx: &mut ViewContext) -> Div { + fn render_signed_in(&mut self, cx: &mut ViewContext) -> List { let contacts = self.contacts(cx).unwrap_or_default(); let workspace = self.workspace.clone(); - v_stack().children(contacts.into_iter().map(|contact| { + let children = once( + ListHeader::new("Contacts") + .right_button( + IconButton::new("add-contact", Icon::Plus).on_click(cx.listener( + |this, _, cx| { + todo!(); + //this.toggle_contact_finder(cx); + }, + )), + ) + .render(cx), + ) + .chain(contacts.into_iter().map(|contact| { let id = contact.user.id; h_stack() .p_2() @@ -3298,7 +3310,9 @@ impl CollabPanel { .log_err(); } }) - })) + })); + + List::new().children(children) } } @@ -3354,10 +3368,12 @@ impl Render for CollabPanel { div() .key_context("CollabPanel") .track_focus(&self.focus_handle) - .child(if self.user_store.read(cx).current_user().is_none() { - self.render_signed_out(cx) - } else { - self.render_signed_in(cx) + .map(|el| { + if self.user_store.read(cx).current_user().is_none() { + el.child(self.render_signed_out(cx)) + } else { + el.child(self.render_signed_in(cx)) + } }) } } diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 875ab6d97e2032ae8c6be9bb9e0445d99897d099..a674a5084c2ce6b8c7913fda97dbe11d2d40abb0 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -5,7 +5,8 @@ use smallvec::SmallVec; use std::rc::Rc; use crate::{ - disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle, + disclosure_control, h_stack, v_stack, Avatar, Icon, IconButton, IconElement, IconSize, Label, + Toggle, }; use crate::{prelude::*, GraphicSlot}; @@ -18,8 +19,7 @@ pub enum ListItemVariant { } pub enum ListHeaderMeta { - // TODO: These should be IconButtons - Tools(Vec), + Tools(Vec), // TODO: This should be a button Button(Label), Text(Label), @@ -45,11 +45,7 @@ impl RenderOnce for ListHeader { h_stack() .gap_2() .items_center() - .children(icons.into_iter().map(|i| { - IconElement::new(i) - .color(Color::Muted) - .size(IconSize::Small) - })), + .children(icons.into_iter().map(|i| i.color(Color::Muted))), ), Some(ListHeaderMeta::Button(label)) => div().child(label), Some(ListHeaderMeta::Text(label)) => div().child(label), @@ -113,6 +109,10 @@ impl ListHeader { self } + pub fn right_button(self, button: IconButton) -> Self { + self.meta(Some(ListHeaderMeta::Tools(vec![button]))) + } + pub fn meta(mut self, meta: Option) -> Self { self.meta = meta; self From 979ff70196ddd7286dedf8d2a27ebba3b160a57f Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 27 Nov 2023 20:12:20 -0500 Subject: [PATCH 12/35] Update popover.rs --- crates/ui2/src/components/popover.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/crates/ui2/src/components/popover.rs b/crates/ui2/src/components/popover.rs index 3838e40bec527fb191c6a0e187c6982dcb643c59..d9269b0ac414774e9dae618d082b4e27aeff92d3 100644 --- a/crates/ui2/src/components/popover.rs +++ b/crates/ui2/src/components/popover.rs @@ -1,5 +1,5 @@ use gpui::{ - AnyElement, Div, Element, ElementId, IntoElement, ParentElement, RenderOnce, Styled, + div, AnyElement, Div, Element, ElementId, IntoElement, ParentElement, RenderOnce, Styled, WindowContext, }; use smallvec::SmallVec; @@ -44,23 +44,16 @@ impl RenderOnce for Popover { type Rendered = Div; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - v_stack() - .relative() - .elevation_2(cx) - .p_1() - .children(self.children) + div() + .flex() + .gap_1() + .child(v_stack().elevation_2(cx).px_1().children(self.children)) .when_some(self.aside, |this, aside| { - // TODO: This will statically position the aside to the top right of the popover. - // We should update this to use gpui2::overlay avoid collisions with the window edges. this.child( v_stack() - .top_0() - .left_full() - .ml_1() - .absolute() .elevation_2(cx) .bg(cx.theme().colors().surface_background) - .p_1() + .px_1() .child(aside), ) }) From a760508080c9fe81ef5b8370d2142de086db7871 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 27 Nov 2023 18:46:56 -0800 Subject: [PATCH 13/35] Add uiua and nu languages --- Cargo.lock | 11 +++ Cargo.toml | 1 + crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 13 ++- crates/zed/src/languages/nu.rs | 81 +++++++++++++++++++ crates/zed/src/languages/uiua.rs | 55 +++++++++++++ crates/zed/src/languages/uiua/config.toml | 10 +++ crates/zed/src/languages/uiua/highlights.scm | 50 ++++++++++++ crates/zed/src/languages/uiua/indents.scm | 3 + crates/zed2/Cargo.toml | 1 + crates/zed2/src/languages.rs | 13 ++- crates/zed2/src/languages/nu.rs | 55 +++++++++++++ crates/zed2/src/languages/uiua.rs | 55 +++++++++++++ crates/zed2/src/languages/uiua/config.toml | 10 +++ crates/zed2/src/languages/uiua/highlights.scm | 50 ++++++++++++ crates/zed2/src/languages/uiua/indents.scm | 3 + 16 files changed, 410 insertions(+), 2 deletions(-) create mode 100644 crates/zed/src/languages/nu.rs create mode 100644 crates/zed/src/languages/uiua.rs create mode 100644 crates/zed/src/languages/uiua/config.toml create mode 100644 crates/zed/src/languages/uiua/highlights.scm create mode 100644 crates/zed/src/languages/uiua/indents.scm create mode 100644 crates/zed2/src/languages/nu.rs create mode 100644 crates/zed2/src/languages/uiua.rs create mode 100644 crates/zed2/src/languages/uiua/config.toml create mode 100644 crates/zed2/src/languages/uiua/highlights.scm create mode 100644 crates/zed2/src/languages/uiua/indents.scm diff --git a/Cargo.lock b/Cargo.lock index 4b63011f08ebced0e716dec978b01be30d1b8c77..10a945c52aa2bdf3505f43c144ad66dbf669f37e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10178,6 +10178,15 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-uiua" +version = "0.3.3" +source = "git+https://github.com/shnarazk/tree-sitter-uiua?rev=9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2#9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-vue" version = "0.0.1" @@ -11616,6 +11625,7 @@ dependencies = [ "tree-sitter-svelte", "tree-sitter-toml", "tree-sitter-typescript", + "tree-sitter-uiua", "tree-sitter-vue", "tree-sitter-yaml", "unindent", @@ -11738,6 +11748,7 @@ dependencies = [ "tree-sitter-svelte", "tree-sitter-toml", "tree-sitter-typescript", + "tree-sitter-uiua", "tree-sitter-vue", "tree-sitter-yaml", "unindent", diff --git a/Cargo.toml b/Cargo.toml index c5c75eacecfe6febe2da8f022a262a03d1e07559..1f6a291a2606e8d41feb18743ae9cc0da142acc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -195,6 +195,7 @@ tree-sitter-lua = "0.0.14" tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" } tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"} tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"} +tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"} [patch.crates-io] tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "3b0159d25559b603af566ade3c83d930bf466db1" } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 3603f1c7fd2b46056af99ec812f25b7922d3e6b0..297785fe9f51de2e8a40b81ebe945d8d781845cf 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -140,6 +140,7 @@ tree-sitter-lua.workspace = true tree-sitter-nix.workspace = true tree-sitter-nu.workspace = true tree-sitter-vue.workspace = true +tree-sitter-uiua.workspace = true url = "2.2" urlencoding = "2.1.2" diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 2398f81c78a8d9cf26c6282694b7353057f59ae9..5ade8cb30289d1b6114158c961d504d75c586cfb 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -17,6 +17,7 @@ mod json; #[cfg(feature = "plugin_runtime")] mod language_plugin; mod lua; +mod nu; mod php; mod python; mod ruby; @@ -24,6 +25,7 @@ mod rust; mod svelte; mod tailwind; mod typescript; +mod uiua; mod vue; mod yaml; @@ -210,12 +212,21 @@ pub fn init( language("elm", tree_sitter_elm::language(), vec![]); language("glsl", tree_sitter_glsl::language(), vec![]); language("nix", tree_sitter_nix::language(), vec![]); - language("nu", tree_sitter_nu::language(), vec![]); + language( + "nu", + tree_sitter_nu::language(), + vec![Arc::new(nu::NuLanguageServer {})], + ); language( "vue", tree_sitter_vue::language(), vec![Arc::new(vue::VueLspAdapter::new(node_runtime))], ); + language( + "uiua", + tree_sitter_uiua::language(), + vec![Arc::new(uiua::UiuaLanguageServer {})], + ); } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/zed/src/languages/nu.rs b/crates/zed/src/languages/nu.rs new file mode 100644 index 0000000000000000000000000000000000000000..16a3b0e4c0a33669306832af8a064d5d185c0089 --- /dev/null +++ b/crates/zed/src/languages/nu.rs @@ -0,0 +1,81 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use language::{CodeLabel, Language, LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; +use std::{any::Any, path::PathBuf, sync::Arc}; + +pub struct NuLanguageServer; + +#[async_trait] +impl LspAdapter for NuLanguageServer { + async fn name(&self) -> LanguageServerName { + LanguageServerName("nu".into()) + } + + fn short_name(&self) -> &'static str { + "nu" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(())) + } + + async fn fetch_server_binary( + &self, + _version: Box, + _container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + Err(anyhow!( + "nu v0.87.0 or greater must be installed and available in your $PATH" + )) + } + + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + Some(LanguageServerBinary { + path: "nu".into(), + arguments: vec!["--lsp".into()], + }) + } + + fn can_be_reinstalled(&self) -> bool { + false + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { + None + } + + async fn label_for_completion( + &self, + completion: &lsp::CompletionItem, + language: &Arc, + ) -> Option { + return Some(CodeLabel { + runs: language + .highlight_text(&completion.label.clone().into(), 0..completion.label.len()), + text: completion.label.clone(), + filter_range: 0..completion.label.len(), + }); + } + + async fn label_for_symbol( + &self, + name: &str, + _: lsp::SymbolKind, + language: &Arc, + ) -> Option { + Some(CodeLabel { + runs: language.highlight_text(&name.into(), 0..name.len()), + text: name.to_string(), + filter_range: 0..name.len(), + }) + } +} diff --git a/crates/zed/src/languages/uiua.rs b/crates/zed/src/languages/uiua.rs new file mode 100644 index 0000000000000000000000000000000000000000..0efdfdd70d400c79cb21d04102fcc1937b84c800 --- /dev/null +++ b/crates/zed/src/languages/uiua.rs @@ -0,0 +1,55 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; +use std::{any::Any, path::PathBuf}; + +pub struct UiuaLanguageServer; + +#[async_trait] +impl LspAdapter for UiuaLanguageServer { + async fn name(&self) -> LanguageServerName { + LanguageServerName("uiua".into()) + } + + fn short_name(&self) -> &'static str { + "uiua" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(())) + } + + async fn fetch_server_binary( + &self, + _version: Box, + _container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + Err(anyhow!( + "uiua must be installed and available in your $PATH" + )) + } + + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + Some(LanguageServerBinary { + path: "uiua".into(), + arguments: vec!["lsp".into()], + }) + } + + fn can_be_reinstalled(&self) -> bool { + false + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { + None + } +} diff --git a/crates/zed/src/languages/uiua/config.toml b/crates/zed/src/languages/uiua/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..87c0d8a9db4e4d93651f3acc764e9662f1ae5cbf --- /dev/null +++ b/crates/zed/src/languages/uiua/config.toml @@ -0,0 +1,10 @@ +name = "Uiua" +path_suffixes = ["ua"] +line_comment = "# " +autoclose_before = ")]}\"" +brackets = [ + { start = "{", end = "}", close = true, newline = false }, + { start = "[", end = "]", close = true, newline = false }, + { start = "(", end = ")", close = true, newline = false }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, +] diff --git a/crates/zed/src/languages/uiua/highlights.scm b/crates/zed/src/languages/uiua/highlights.scm new file mode 100644 index 0000000000000000000000000000000000000000..2c37f404e6767f0ef12c0f964d764daafeb1a9d9 --- /dev/null +++ b/crates/zed/src/languages/uiua/highlights.scm @@ -0,0 +1,50 @@ +[ + (openParen) + (closeParen) + (openCurly) + (closeCurly) + (openBracket) + (closeBracket) +] @punctuation.bracket + +[ + (branchSeparator) + (underscore) +] @constructor +; ] @punctuation.delimiter + +[ (character) ] @constant.character +[ (comment) ] @comment +[ (constant) ] @constant.numeric +[ (identifier) ] @variable +[ (leftArrow) ] @keyword +[ (function) ] @function +[ (modifier1) ] @operator +[ (modifier2) ] @operator +[ (number) ] @constant.numeric +[ (placeHolder) ] @special +[ (otherConstant) ] @string.special +[ (signature) ] @type +[ (system) ] @function.builtin +[ (tripleMinus) ] @module + +; planet +[ + "id" + "identity" + "∘" + "dip" + "⊙" + "gap" + "⋅" +] @tag + +[ + (string) + (multiLineString) +] @string + +; [ +; (deprecated) +; (identifierDeprecated) +; ] @warning diff --git a/crates/zed/src/languages/uiua/indents.scm b/crates/zed/src/languages/uiua/indents.scm new file mode 100644 index 0000000000000000000000000000000000000000..add68c723c4187d2ecdb63d927b8ad662ab2d2bf --- /dev/null +++ b/crates/zed/src/languages/uiua/indents.scm @@ -0,0 +1,3 @@ +[ + (array) +] @indent diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 3212b6182b7b64fe1d79a2f20097fffd64c603b5..d5cc2d40dc6acb2dde94d33a2a8e480ec29d2b11 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -136,6 +136,7 @@ tree-sitter-lua.workspace = true tree-sitter-nix.workspace = true tree-sitter-nu.workspace = true tree-sitter-vue.workspace = true +tree-sitter-uiua.workspace = true url = "2.2" urlencoding = "2.1.2" diff --git a/crates/zed2/src/languages.rs b/crates/zed2/src/languages.rs index 555f12dd0fdf7b8ec9c90d68da5f65944c704936..129dad8f48cfb145c022785f8aad4faf98b02dfd 100644 --- a/crates/zed2/src/languages.rs +++ b/crates/zed2/src/languages.rs @@ -18,6 +18,7 @@ mod json; #[cfg(feature = "plugin_runtime")] mod language_plugin; mod lua; +mod nu; mod php; mod python; mod ruby; @@ -25,6 +26,7 @@ mod rust; mod svelte; mod tailwind; mod typescript; +mod uiua; mod vue; mod yaml; @@ -211,12 +213,21 @@ pub fn init( language("elm", tree_sitter_elm::language(), vec![]); language("glsl", tree_sitter_glsl::language(), vec![]); language("nix", tree_sitter_nix::language(), vec![]); - language("nu", tree_sitter_nu::language(), vec![]); + language( + "nu", + tree_sitter_nu::language(), + vec![Arc::new(nu::NuLanguageServer {})], + ); language( "vue", tree_sitter_vue::language(), vec![Arc::new(vue::VueLspAdapter::new(node_runtime))], ); + language( + "uiua", + tree_sitter_uiua::language(), + vec![Arc::new(uiua::UiuaLanguageServer {})], + ); } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/zed2/src/languages/nu.rs b/crates/zed2/src/languages/nu.rs new file mode 100644 index 0000000000000000000000000000000000000000..a3631b8471d8f06dc88f0ef9fe8bbf48cb76d52d --- /dev/null +++ b/crates/zed2/src/languages/nu.rs @@ -0,0 +1,55 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; +use std::{any::Any, path::PathBuf}; + +pub struct NuLanguageServer; + +#[async_trait] +impl LspAdapter for NuLanguageServer { + async fn name(&self) -> LanguageServerName { + LanguageServerName("nu".into()) + } + + fn short_name(&self) -> &'static str { + "nu" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(())) + } + + async fn fetch_server_binary( + &self, + _version: Box, + _container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + Err(anyhow!( + "nu v0.87.0 or greater must be installed and available in your $PATH" + )) + } + + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + Some(LanguageServerBinary { + path: "nu".into(), + arguments: vec!["--lsp".into()], + }) + } + + fn can_be_reinstalled(&self) -> bool { + false + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { + None + } +} diff --git a/crates/zed2/src/languages/uiua.rs b/crates/zed2/src/languages/uiua.rs new file mode 100644 index 0000000000000000000000000000000000000000..0efdfdd70d400c79cb21d04102fcc1937b84c800 --- /dev/null +++ b/crates/zed2/src/languages/uiua.rs @@ -0,0 +1,55 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; +use std::{any::Any, path::PathBuf}; + +pub struct UiuaLanguageServer; + +#[async_trait] +impl LspAdapter for UiuaLanguageServer { + async fn name(&self) -> LanguageServerName { + LanguageServerName("uiua".into()) + } + + fn short_name(&self) -> &'static str { + "uiua" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(())) + } + + async fn fetch_server_binary( + &self, + _version: Box, + _container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + Err(anyhow!( + "uiua must be installed and available in your $PATH" + )) + } + + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + Some(LanguageServerBinary { + path: "uiua".into(), + arguments: vec!["lsp".into()], + }) + } + + fn can_be_reinstalled(&self) -> bool { + false + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { + None + } +} diff --git a/crates/zed2/src/languages/uiua/config.toml b/crates/zed2/src/languages/uiua/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..72fdc910408222e7a6c6e9aa1b48c45b0d7176d6 --- /dev/null +++ b/crates/zed2/src/languages/uiua/config.toml @@ -0,0 +1,10 @@ +name = "Uiua" +path_suffixes = ["ua"] +line_comment = "# " +autoclose_before = ")]}\"" +brackets = [ + { start = "{", end = "}", close = true, newline = false}, + { start = "[", end = "]", close = true, newline = false }, + { start = "(", end = ")", close = true, newline = false }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, +] diff --git a/crates/zed2/src/languages/uiua/highlights.scm b/crates/zed2/src/languages/uiua/highlights.scm new file mode 100644 index 0000000000000000000000000000000000000000..2c37f404e6767f0ef12c0f964d764daafeb1a9d9 --- /dev/null +++ b/crates/zed2/src/languages/uiua/highlights.scm @@ -0,0 +1,50 @@ +[ + (openParen) + (closeParen) + (openCurly) + (closeCurly) + (openBracket) + (closeBracket) +] @punctuation.bracket + +[ + (branchSeparator) + (underscore) +] @constructor +; ] @punctuation.delimiter + +[ (character) ] @constant.character +[ (comment) ] @comment +[ (constant) ] @constant.numeric +[ (identifier) ] @variable +[ (leftArrow) ] @keyword +[ (function) ] @function +[ (modifier1) ] @operator +[ (modifier2) ] @operator +[ (number) ] @constant.numeric +[ (placeHolder) ] @special +[ (otherConstant) ] @string.special +[ (signature) ] @type +[ (system) ] @function.builtin +[ (tripleMinus) ] @module + +; planet +[ + "id" + "identity" + "∘" + "dip" + "⊙" + "gap" + "⋅" +] @tag + +[ + (string) + (multiLineString) +] @string + +; [ +; (deprecated) +; (identifierDeprecated) +; ] @warning diff --git a/crates/zed2/src/languages/uiua/indents.scm b/crates/zed2/src/languages/uiua/indents.scm new file mode 100644 index 0000000000000000000000000000000000000000..add68c723c4187d2ecdb63d927b8ad662ab2d2bf --- /dev/null +++ b/crates/zed2/src/languages/uiua/indents.scm @@ -0,0 +1,3 @@ +[ + (array) +] @indent From 26121713b39549713d36ec58db864e61b54dbb67 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 27 Nov 2023 19:57:55 -0700 Subject: [PATCH 14/35] Show channels and users in the sidebar --- Cargo.lock | 1 + crates/collab_ui2/src/collab_panel.rs | 2606 +++++++++++++------------ crates/zed2/Cargo.toml | 2 +- crates/zed2/src/main.rs | 4 +- 4 files changed, 1315 insertions(+), 1298 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90cc0460ff22193eac28119ec9f8ff9e2dbb752c..7d8fb758bfc729d1e92ea52b9a76d78f7a840521 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11651,6 +11651,7 @@ dependencies = [ "auto_update2", "backtrace", "call2", + "channel2", "chrono", "cli", "client2", diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 5cce89416558a988dce5754044f710bc1a3394b2..0df51b02a2b7bea5246522b414d6550ba8f21743 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -1,3 +1,4 @@ +#![allow(unused)] // mod channel_modal; // mod contact_finder; @@ -155,20 +156,27 @@ actions!( const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; -use std::{iter::once, sync::Arc}; +use std::{iter::once, mem, sync::Arc}; -use client::{Client, Contact, UserStore}; +use call::ActiveCall; +use channel::{Channel, ChannelId, ChannelStore}; +use client::{Client, Contact, User, UserStore}; use db::kvp::KEY_VALUE_STORE; +use editor::Editor; +use feature_flags::{ChannelsAlpha, FeatureFlagAppExt}; +use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, - RenderOnce, Styled, View, ViewContext, VisualContext, WeakView, + RenderOnce, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, }; use project::Fs; use serde_derive::{Deserialize, Serialize}; use settings::Settings; -use ui::{h_stack, v_stack, Avatar, Button, Icon, IconButton, Label, List, ListHeader}; -use util::ResultExt; +use ui::{ + h_stack, v_stack, Avatar, Button, Icon, IconButton, Label, List, ListHeader, ListItem, Tooltip, +}; +use util::{maybe, ResultExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, notifications::NotifyResultExt, @@ -269,26 +277,26 @@ pub fn init(cx: &mut AppContext) { // ); } -// #[derive(Debug)] -// pub enum ChannelEditingState { -// Create { -// location: Option, -// pending_name: Option, -// }, -// Rename { -// location: ChannelId, -// pending_name: Option, -// }, -// } +#[derive(Debug)] +pub enum ChannelEditingState { + Create { + location: Option, + pending_name: Option, + }, + Rename { + location: ChannelId, + pending_name: Option, + }, +} -// impl ChannelEditingState { -// fn pending_name(&self) -> Option<&str> { -// match self { -// ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(), -// ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(), -// } -// } -// } +impl ChannelEditingState { + fn pending_name(&self) -> Option<&str> { + match self { + ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(), + ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(), + } + } +} pub struct CollabPanel { width: Option, @@ -297,20 +305,20 @@ pub struct CollabPanel { // channel_clipboard: Option, // pending_serialization: Task>, // context_menu: ViewHandle, - // filter_editor: ViewHandle, + filter_editor: View, // channel_name_editor: ViewHandle, - // channel_editing_state: Option, - // entries: Vec, + channel_editing_state: Option, + entries: Vec, // selection: Option, + channel_store: Model, user_store: Model, client: Arc, - // channel_store: ModelHandle, // project: ModelHandle, - // match_candidates: Vec, + match_candidates: Vec, // list_state: ListState, - // subscriptions: Vec, + subscriptions: Vec, collapsed_sections: Vec
, - // collapsed_channels: Vec, + collapsed_channels: Vec, // drag_target_channel: ChannelDragTarget, workspace: WeakView, // context_menu_on_selected: bool, @@ -338,56 +346,56 @@ struct SerializedCollabPanel { #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] enum Section { - // ActiveCall, - // Channels, - // ChannelInvites, - // ContactRequests, + ActiveCall, + Channels, + ChannelInvites, + ContactRequests, Contacts, - // Online, + Online, Offline, } -// #[derive(Clone, Debug)] -// enum ListEntry { -// Header(Section), -// CallParticipant { -// user: Arc, -// peer_id: Option, -// is_pending: bool, -// }, -// ParticipantProject { -// project_id: u64, -// worktree_root_names: Vec, -// host_user_id: u64, -// is_last: bool, -// }, -// ParticipantScreen { -// peer_id: Option, -// is_last: bool, -// }, -// IncomingRequest(Arc), -// OutgoingRequest(Arc), -// ChannelInvite(Arc), -// Channel { -// channel: Arc, -// depth: usize, -// has_children: bool, -// }, -// ChannelNotes { -// channel_id: ChannelId, -// }, -// ChannelChat { -// channel_id: ChannelId, -// }, -// ChannelEditor { -// depth: usize, -// }, -// Contact { -// contact: Arc, -// calling: bool, -// }, -// ContactPlaceholder, -// } +#[derive(Clone, Debug)] +enum ListEntry { + Header(Section), + // CallParticipant { + // user: Arc, + // peer_id: Option, + // is_pending: bool, + // }, + // ParticipantProject { + // project_id: u64, + // worktree_root_names: Vec, + // host_user_id: u64, + // is_last: bool, + // }, + // ParticipantScreen { + // peer_id: Option, + // is_last: bool, + // }, + IncomingRequest(Arc), + OutgoingRequest(Arc), + // ChannelInvite(Arc), + Channel { + channel: Arc, + depth: usize, + has_children: bool, + }, + // ChannelNotes { + // channel_id: ChannelId, + // }, + // ChannelChat { + // channel_id: ChannelId, + // }, + ChannelEditor { + depth: usize, + }, + Contact { + contact: Arc, + calling: bool, + }, + ContactPlaceholder, +} // impl Entity for CollabPanel { // type Event = Event; @@ -398,16 +406,11 @@ impl CollabPanel { cx.build_view(|cx| { // let view_id = cx.view_id(); - // let filter_editor = cx.add_view(|cx| { - // let mut editor = Editor::single_line( - // Some(Arc::new(|theme| { - // theme.collab_panel.user_query_editor.clone() - // })), - // cx, - // ); - // editor.set_placeholder_text("Filter channels, contacts", cx); - // editor - // }); + let filter_editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.set_placeholder_text("Filter channels, contacts", cx); + editor + }); // cx.subscribe(&filter_editor, |this, _, event, cx| { // if let editor::Event::BufferEdited = event { @@ -586,7 +589,7 @@ impl CollabPanel { // } // }); - let this = Self { + let mut this = Self { width: None, focus_handle: cx.focus_handle(), // channel_clipboard: None, @@ -594,17 +597,17 @@ impl CollabPanel { // pending_serialization: Task::ready(None), // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), // channel_name_editor, - // filter_editor, - // entries: Vec::default(), - // channel_editing_state: None, + filter_editor, + entries: Vec::default(), + channel_editing_state: None, // selection: None, + channel_store: ChannelStore::global(cx), user_store: workspace.user_store().clone(), - // channel_store: ChannelStore::global(cx), // project: workspace.project().clone(), - // subscriptions: Vec::default(), - // match_candidates: Vec::default(), + subscriptions: Vec::default(), + match_candidates: Vec::default(), collapsed_sections: vec![Section::Offline], - // collapsed_channels: Vec::default(), + collapsed_channels: Vec::default(), workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), // context_menu_on_selected: true, @@ -612,7 +615,7 @@ impl CollabPanel { // list_state, }; - // this.update_entries(false, cx); + this.update_entries(false, cx); // // Update the dock position when the setting changes. // let mut old_dock_position = this.position(cx); @@ -629,10 +632,10 @@ impl CollabPanel { // ); // let active_call = ActiveCall::global(cx); - // this.subscriptions - // .push(cx.observe(&this.user_store, |this, _, cx| { - // this.update_entries(true, cx) - // })); + this.subscriptions + .push(cx.observe(&this.user_store, |this, _, cx| { + this.update_entries(true, cx) + })); // this.subscriptions // .push(cx.observe(&this.channel_store, |this, _, cx| { // this.update_entries(true, cx) @@ -721,449 +724,449 @@ impl CollabPanel { // ); // } - // fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext) { - // let channel_store = self.channel_store.read(cx); - // let user_store = self.user_store.read(cx); - // let query = self.filter_editor.read(cx).text(cx); - // let executor = cx.background().clone(); - - // let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); - // let old_entries = mem::take(&mut self.entries); - // let mut scroll_to_top = false; - - // if let Some(room) = ActiveCall::global(cx).read(cx).room() { - // self.entries.push(ListEntry::Header(Section::ActiveCall)); - // if !old_entries - // .iter() - // .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall))) - // { - // scroll_to_top = true; - // } - - // if !self.collapsed_sections.contains(&Section::ActiveCall) { - // let room = room.read(cx); - - // if let Some(channel_id) = room.channel_id() { - // self.entries.push(ListEntry::ChannelNotes { channel_id }); - // self.entries.push(ListEntry::ChannelChat { channel_id }) - // } - - // // Populate the active user. - // if let Some(user) = user_store.current_user() { - // self.match_candidates.clear(); - // self.match_candidates.push(StringMatchCandidate { - // id: 0, - // string: user.github_login.clone(), - // char_bag: user.github_login.chars().collect(), - // }); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // if !matches.is_empty() { - // let user_id = user.id; - // self.entries.push(ListEntry::CallParticipant { - // user, - // peer_id: None, - // is_pending: false, - // }); - // let mut projects = room.local_participant().projects.iter().peekable(); - // while let Some(project) = projects.next() { - // self.entries.push(ListEntry::ParticipantProject { - // project_id: project.id, - // worktree_root_names: project.worktree_root_names.clone(), - // host_user_id: user_id, - // is_last: projects.peek().is_none() && !room.is_screen_sharing(), - // }); - // } - // if room.is_screen_sharing() { - // self.entries.push(ListEntry::ParticipantScreen { - // peer_id: None, - // is_last: true, - // }); - // } - // } - // } - - // // Populate remote participants. - // self.match_candidates.clear(); - // self.match_candidates - // .extend(room.remote_participants().iter().map(|(_, participant)| { - // StringMatchCandidate { - // id: participant.user.id as usize, - // string: participant.user.github_login.clone(), - // char_bag: participant.user.github_login.chars().collect(), - // } - // })); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // for mat in matches { - // let user_id = mat.candidate_id as u64; - // let participant = &room.remote_participants()[&user_id]; - // self.entries.push(ListEntry::CallParticipant { - // user: participant.user.clone(), - // peer_id: Some(participant.peer_id), - // is_pending: false, - // }); - // let mut projects = participant.projects.iter().peekable(); - // while let Some(project) = projects.next() { - // self.entries.push(ListEntry::ParticipantProject { - // project_id: project.id, - // worktree_root_names: project.worktree_root_names.clone(), - // host_user_id: participant.user.id, - // is_last: projects.peek().is_none() - // && participant.video_tracks.is_empty(), - // }); - // } - // if !participant.video_tracks.is_empty() { - // self.entries.push(ListEntry::ParticipantScreen { - // peer_id: Some(participant.peer_id), - // is_last: true, - // }); - // } - // } - - // // Populate pending participants. - // self.match_candidates.clear(); - // self.match_candidates - // .extend(room.pending_participants().iter().enumerate().map( - // |(id, participant)| StringMatchCandidate { - // id, - // string: participant.github_login.clone(), - // char_bag: participant.github_login.chars().collect(), - // }, - // )); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // self.entries - // .extend(matches.iter().map(|mat| ListEntry::CallParticipant { - // user: room.pending_participants()[mat.candidate_id].clone(), - // peer_id: None, - // is_pending: true, - // })); - // } - // } - - // let mut request_entries = Vec::new(); - - // if cx.has_flag::() { - // self.entries.push(ListEntry::Header(Section::Channels)); - - // if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { - // self.match_candidates.clear(); - // self.match_candidates - // .extend(channel_store.ordered_channels().enumerate().map( - // |(ix, (_, channel))| StringMatchCandidate { - // id: ix, - // string: channel.name.clone(), - // char_bag: channel.name.chars().collect(), - // }, - // )); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // if let Some(state) = &self.channel_editing_state { - // if matches!(state, ChannelEditingState::Create { location: None, .. }) { - // self.entries.push(ListEntry::ChannelEditor { depth: 0 }); - // } - // } - // let mut collapse_depth = None; - // for mat in matches { - // let channel = channel_store.channel_at_index(mat.candidate_id).unwrap(); - // let depth = channel.parent_path.len(); - - // if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { - // collapse_depth = Some(depth); - // } else if let Some(collapsed_depth) = collapse_depth { - // if depth > collapsed_depth { - // continue; - // } - // if self.is_channel_collapsed(channel.id) { - // collapse_depth = Some(depth); - // } else { - // collapse_depth = None; - // } - // } - - // let has_children = channel_store - // .channel_at_index(mat.candidate_id + 1) - // .map_or(false, |next_channel| { - // next_channel.parent_path.ends_with(&[channel.id]) - // }); - - // match &self.channel_editing_state { - // Some(ChannelEditingState::Create { - // location: parent_id, - // .. - // }) if *parent_id == Some(channel.id) => { - // self.entries.push(ListEntry::Channel { - // channel: channel.clone(), - // depth, - // has_children: false, - // }); - // self.entries - // .push(ListEntry::ChannelEditor { depth: depth + 1 }); - // } - // Some(ChannelEditingState::Rename { - // location: parent_id, - // .. - // }) if parent_id == &channel.id => { - // self.entries.push(ListEntry::ChannelEditor { depth }); - // } - // _ => { - // self.entries.push(ListEntry::Channel { - // channel: channel.clone(), - // depth, - // has_children, - // }); - // } - // } - // } - // } - - // let channel_invites = channel_store.channel_invitations(); - // if !channel_invites.is_empty() { - // self.match_candidates.clear(); - // self.match_candidates - // .extend(channel_invites.iter().enumerate().map(|(ix, channel)| { - // StringMatchCandidate { - // id: ix, - // string: channel.name.clone(), - // char_bag: channel.name.chars().collect(), - // } - // })); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // request_entries.extend(matches.iter().map(|mat| { - // ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone()) - // })); - - // if !request_entries.is_empty() { - // self.entries - // .push(ListEntry::Header(Section::ChannelInvites)); - // if !self.collapsed_sections.contains(&Section::ChannelInvites) { - // self.entries.append(&mut request_entries); - // } - // } - // } - // } - - // self.entries.push(ListEntry::Header(Section::Contacts)); - - // request_entries.clear(); - // let incoming = user_store.incoming_contact_requests(); - // if !incoming.is_empty() { - // self.match_candidates.clear(); - // self.match_candidates - // .extend( - // incoming - // .iter() - // .enumerate() - // .map(|(ix, user)| StringMatchCandidate { - // id: ix, - // string: user.github_login.clone(), - // char_bag: user.github_login.chars().collect(), - // }), - // ); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // request_entries.extend( - // matches - // .iter() - // .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())), - // ); - // } - - // let outgoing = user_store.outgoing_contact_requests(); - // if !outgoing.is_empty() { - // self.match_candidates.clear(); - // self.match_candidates - // .extend( - // outgoing - // .iter() - // .enumerate() - // .map(|(ix, user)| StringMatchCandidate { - // id: ix, - // string: user.github_login.clone(), - // char_bag: user.github_login.chars().collect(), - // }), - // ); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // request_entries.extend( - // matches - // .iter() - // .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), - // ); - // } - - // if !request_entries.is_empty() { - // self.entries - // .push(ListEntry::Header(Section::ContactRequests)); - // if !self.collapsed_sections.contains(&Section::ContactRequests) { - // self.entries.append(&mut request_entries); - // } - // } - - // let contacts = user_store.contacts(); - // if !contacts.is_empty() { - // self.match_candidates.clear(); - // self.match_candidates - // .extend( - // contacts - // .iter() - // .enumerate() - // .map(|(ix, contact)| StringMatchCandidate { - // id: ix, - // string: contact.user.github_login.clone(), - // char_bag: contact.user.github_login.chars().collect(), - // }), - // ); - - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - - // let (online_contacts, offline_contacts) = matches - // .iter() - // .partition::, _>(|mat| contacts[mat.candidate_id].online); - - // for (matches, section) in [ - // (online_contacts, Section::Online), - // (offline_contacts, Section::Offline), - // ] { - // if !matches.is_empty() { - // self.entries.push(ListEntry::Header(section)); - // if !self.collapsed_sections.contains(§ion) { - // let active_call = &ActiveCall::global(cx).read(cx); - // for mat in matches { - // let contact = &contacts[mat.candidate_id]; - // self.entries.push(ListEntry::Contact { - // contact: contact.clone(), - // calling: active_call.pending_invites().contains(&contact.user.id), - // }); - // } - // } - // } - // } - // } - - // if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() { - // self.entries.push(ListEntry::ContactPlaceholder); - // } - - // if select_same_item { - // if let Some(prev_selected_entry) = prev_selected_entry { - // self.selection.take(); - // for (ix, entry) in self.entries.iter().enumerate() { - // if *entry == prev_selected_entry { - // self.selection = Some(ix); - // break; - // } - // } - // } - // } else { - // self.selection = self.selection.and_then(|prev_selection| { - // if self.entries.is_empty() { - // None - // } else { - // Some(prev_selection.min(self.entries.len() - 1)) - // } - // }); - // } - - // let old_scroll_top = self.list_state.logical_scroll_top(); + fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext) { + let channel_store = self.channel_store.read(cx); + let user_store = self.user_store.read(cx); + let query = self.filter_editor.read(cx).text(cx); + let executor = cx.background_executor().clone(); + + // let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); + let _old_entries = mem::take(&mut self.entries); + // let mut scroll_to_top = false; + + // if let Some(room) = ActiveCall::global(cx).read(cx).room() { + // self.entries.push(ListEntry::Header(Section::ActiveCall)); + // if !old_entries + // .iter() + // .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall))) + // { + // scroll_to_top = true; + // } + + // if !self.collapsed_sections.contains(&Section::ActiveCall) { + // let room = room.read(cx); + + // if let Some(channel_id) = room.channel_id() { + // self.entries.push(ListEntry::ChannelNotes { channel_id }); + // self.entries.push(ListEntry::ChannelChat { channel_id }) + // } + + // // Populate the active user. + // if let Some(user) = user_store.current_user() { + // self.match_candidates.clear(); + // self.match_candidates.push(StringMatchCandidate { + // id: 0, + // string: user.github_login.clone(), + // char_bag: user.github_login.chars().collect(), + // }); + // let matches = executor.block(match_strings( + // &self.match_candidates, + // &query, + // true, + // usize::MAX, + // &Default::default(), + // executor.clone(), + // )); + // if !matches.is_empty() { + // let user_id = user.id; + // self.entries.push(ListEntry::CallParticipant { + // user, + // peer_id: None, + // is_pending: false, + // }); + // let mut projects = room.local_participant().projects.iter().peekable(); + // while let Some(project) = projects.next() { + // self.entries.push(ListEntry::ParticipantProject { + // project_id: project.id, + // worktree_root_names: project.worktree_root_names.clone(), + // host_user_id: user_id, + // is_last: projects.peek().is_none() && !room.is_screen_sharing(), + // }); + // } + // if room.is_screen_sharing() { + // self.entries.push(ListEntry::ParticipantScreen { + // peer_id: None, + // is_last: true, + // }); + // } + // } + // } + + // // Populate remote participants. + // self.match_candidates.clear(); + // self.match_candidates + // .extend(room.remote_participants().iter().map(|(_, participant)| { + // StringMatchCandidate { + // id: participant.user.id as usize, + // string: participant.user.github_login.clone(), + // char_bag: participant.user.github_login.chars().collect(), + // } + // })); + // let matches = executor.block(match_strings( + // &self.match_candidates, + // &query, + // true, + // usize::MAX, + // &Default::default(), + // executor.clone(), + // )); + // for mat in matches { + // let user_id = mat.candidate_id as u64; + // let participant = &room.remote_participants()[&user_id]; + // self.entries.push(ListEntry::CallParticipant { + // user: participant.user.clone(), + // peer_id: Some(participant.peer_id), + // is_pending: false, + // }); + // let mut projects = participant.projects.iter().peekable(); + // while let Some(project) = projects.next() { + // self.entries.push(ListEntry::ParticipantProject { + // project_id: project.id, + // worktree_root_names: project.worktree_root_names.clone(), + // host_user_id: participant.user.id, + // is_last: projects.peek().is_none() + // && participant.video_tracks.is_empty(), + // }); + // } + // if !participant.video_tracks.is_empty() { + // self.entries.push(ListEntry::ParticipantScreen { + // peer_id: Some(participant.peer_id), + // is_last: true, + // }); + // } + // } + + // // Populate pending participants. + // self.match_candidates.clear(); + // self.match_candidates + // .extend(room.pending_participants().iter().enumerate().map( + // |(id, participant)| StringMatchCandidate { + // id, + // string: participant.github_login.clone(), + // char_bag: participant.github_login.chars().collect(), + // }, + // )); + // let matches = executor.block(match_strings( + // &self.match_candidates, + // &query, + // true, + // usize::MAX, + // &Default::default(), + // executor.clone(), + // )); + // self.entries + // .extend(matches.iter().map(|mat| ListEntry::CallParticipant { + // user: room.pending_participants()[mat.candidate_id].clone(), + // peer_id: None, + // is_pending: true, + // })); + // } + // } + + let mut request_entries = Vec::new(); + + if cx.has_flag::() { + self.entries.push(ListEntry::Header(Section::Channels)); + + if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { + self.match_candidates.clear(); + self.match_candidates + .extend(channel_store.ordered_channels().enumerate().map( + |(ix, (_, channel))| StringMatchCandidate { + id: ix, + string: channel.name.clone(), + char_bag: channel.name.chars().collect(), + }, + )); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + if let Some(state) = &self.channel_editing_state { + if matches!(state, ChannelEditingState::Create { location: None, .. }) { + self.entries.push(ListEntry::ChannelEditor { depth: 0 }); + } + } + let mut collapse_depth = None; + for mat in matches { + let channel = channel_store.channel_at_index(mat.candidate_id).unwrap(); + let depth = channel.parent_path.len(); + + if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { + collapse_depth = Some(depth); + } else if let Some(collapsed_depth) = collapse_depth { + if depth > collapsed_depth { + continue; + } + if self.is_channel_collapsed(channel.id) { + collapse_depth = Some(depth); + } else { + collapse_depth = None; + } + } - // self.list_state.reset(self.entries.len()); + let has_children = channel_store + .channel_at_index(mat.candidate_id + 1) + .map_or(false, |next_channel| { + next_channel.parent_path.ends_with(&[channel.id]) + }); + + match &self.channel_editing_state { + Some(ChannelEditingState::Create { + location: parent_id, + .. + }) if *parent_id == Some(channel.id) => { + self.entries.push(ListEntry::Channel { + channel: channel.clone(), + depth, + has_children: false, + }); + self.entries + .push(ListEntry::ChannelEditor { depth: depth + 1 }); + } + Some(ChannelEditingState::Rename { + location: parent_id, + .. + }) if parent_id == &channel.id => { + self.entries.push(ListEntry::ChannelEditor { depth }); + } + _ => { + self.entries.push(ListEntry::Channel { + channel: channel.clone(), + depth, + has_children, + }); + } + } + } + } - // if scroll_to_top { - // self.list_state.scroll_to(ListOffset::default()); - // } else { - // // Attempt to maintain the same scroll position. - // if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) { - // let new_scroll_top = self - // .entries - // .iter() - // .position(|entry| entry == old_top_entry) - // .map(|item_ix| ListOffset { - // item_ix, - // offset_in_item: old_scroll_top.offset_in_item, - // }) - // .or_else(|| { - // let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?; - // let item_ix = self - // .entries - // .iter() - // .position(|entry| entry == entry_after_old_top)?; - // Some(ListOffset { - // item_ix, - // offset_in_item: 0., - // }) - // }) - // .or_else(|| { - // let entry_before_old_top = - // old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?; - // let item_ix = self - // .entries - // .iter() - // .position(|entry| entry == entry_before_old_top)?; - // Some(ListOffset { - // item_ix, - // offset_in_item: 0., - // }) - // }); + // let channel_invites = channel_store.channel_invitations(); + // if !channel_invites.is_empty() { + // self.match_candidates.clear(); + // self.match_candidates + // .extend(channel_invites.iter().enumerate().map(|(ix, channel)| { + // StringMatchCandidate { + // id: ix, + // string: channel.name.clone(), + // char_bag: channel.name.chars().collect(), + // } + // })); + // let matches = executor.block(match_strings( + // &self.match_candidates, + // &query, + // true, + // usize::MAX, + // &Default::default(), + // executor.clone(), + // )); + // request_entries.extend(matches.iter().map(|mat| { + // ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone()) + // })); - // self.list_state - // .scroll_to(new_scroll_top.unwrap_or(old_scroll_top)); - // } - // } + // if !request_entries.is_empty() { + // self.entries + // .push(ListEntry::Header(Section::ChannelInvites)); + // if !self.collapsed_sections.contains(&Section::ChannelInvites) { + // self.entries.append(&mut request_entries); + // } + // } + // } + } + + self.entries.push(ListEntry::Header(Section::Contacts)); + + request_entries.clear(); + let incoming = user_store.incoming_contact_requests(); + if !incoming.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + incoming + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + request_entries.extend( + matches + .iter() + .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())), + ); + } + + let outgoing = user_store.outgoing_contact_requests(); + if !outgoing.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + outgoing + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + request_entries.extend( + matches + .iter() + .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), + ); + } + + if !request_entries.is_empty() { + self.entries + .push(ListEntry::Header(Section::ContactRequests)); + if !self.collapsed_sections.contains(&Section::ContactRequests) { + self.entries.append(&mut request_entries); + } + } + + let contacts = user_store.contacts(); + if !contacts.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + contacts + .iter() + .enumerate() + .map(|(ix, contact)| StringMatchCandidate { + id: ix, + string: contact.user.github_login.clone(), + char_bag: contact.user.github_login.chars().collect(), + }), + ); + + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + + let (online_contacts, offline_contacts) = matches + .iter() + .partition::, _>(|mat| contacts[mat.candidate_id].online); + + for (matches, section) in [ + (online_contacts, Section::Online), + (offline_contacts, Section::Offline), + ] { + if !matches.is_empty() { + self.entries.push(ListEntry::Header(section)); + if !self.collapsed_sections.contains(§ion) { + let active_call = &ActiveCall::global(cx).read(cx); + for mat in matches { + let contact = &contacts[mat.candidate_id]; + self.entries.push(ListEntry::Contact { + contact: contact.clone(), + calling: active_call.pending_invites().contains(&contact.user.id), + }); + } + } + } + } + } + + if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() { + self.entries.push(ListEntry::ContactPlaceholder); + } + + // if select_same_item { + // if let Some(prev_selected_entry) = prev_selected_entry { + // self.selection.take(); + // for (ix, entry) in self.entries.iter().enumerate() { + // if *entry == prev_selected_entry { + // self.selection = Some(ix); + // break; + // } + // } + // } + // } else { + // self.selection = self.selection.and_then(|prev_selection| { + // if self.entries.is_empty() { + // None + // } else { + // Some(prev_selection.min(self.entries.len() - 1)) + // } + // }); + // } + + // let old_scroll_top = self.list_state.logical_scroll_top(); + + // self.list_state.reset(self.entries.len()); + + // if scroll_to_top { + // self.list_state.scroll_to(ListOffset::default()); + // } else { + // // Attempt to maintain the same scroll position. + // if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) { + // let new_scroll_top = self + // .entries + // .iter() + // .position(|entry| entry == old_top_entry) + // .map(|item_ix| ListOffset { + // item_ix, + // offset_in_item: old_scroll_top.offset_in_item, + // }) + // .or_else(|| { + // let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?; + // let item_ix = self + // .entries + // .iter() + // .position(|entry| entry == entry_after_old_top)?; + // Some(ListOffset { + // item_ix, + // offset_in_item: 0., + // }) + // }) + // .or_else(|| { + // let entry_before_old_top = + // old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?; + // let item_ix = self + // .entries + // .iter() + // .position(|entry| entry == entry_before_old_top)?; + // Some(ListOffset { + // item_ix, + // offset_in_item: 0., + // }) + // }); + + // self.list_state + // .scroll_to(new_scroll_top.unwrap_or(old_scroll_top)); + // } + // } - // cx.notify(); - // } + cx.notify(); + } // fn render_call_participant( // user: &User, @@ -1461,389 +1464,6 @@ impl CollabPanel { // } // } - // fn render_header( - // &self, - // section: Section, - // theme: &theme::Theme, - // is_selected: bool, - // is_collapsed: bool, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum Header {} - // enum LeaveCallContactList {} - // enum AddChannel {} - - // let tooltip_style = &theme.tooltip; - // let mut channel_link = None; - // let mut channel_tooltip_text = None; - // let mut channel_icon = None; - // let mut is_dragged_over = false; - - // let text = match section { - // Section::ActiveCall => { - // let channel_name = maybe!({ - // let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; - - // let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; - - // channel_link = Some(channel.link()); - // (channel_icon, channel_tooltip_text) = match channel.visibility { - // proto::ChannelVisibility::Public => { - // (Some("icons/public.svg"), Some("Copy public channel link.")) - // } - // proto::ChannelVisibility::Members => { - // (Some("icons/hash.svg"), Some("Copy private channel link.")) - // } - // }; - - // Some(channel.name.as_str()) - // }); - - // if let Some(name) = channel_name { - // Cow::Owned(format!("{}", name)) - // } else { - // Cow::Borrowed("Current Call") - // } - // } - // Section::ContactRequests => Cow::Borrowed("Requests"), - // Section::Contacts => Cow::Borrowed("Contacts"), - // Section::Channels => Cow::Borrowed("Channels"), - // Section::ChannelInvites => Cow::Borrowed("Invites"), - // Section::Online => Cow::Borrowed("Online"), - // Section::Offline => Cow::Borrowed("Offline"), - // }; - - // enum AddContact {} - // let button = match section { - // Section::ActiveCall => channel_link.map(|channel_link| { - // let channel_link_copy = channel_link.clone(); - // MouseEventHandler::new::(0, cx, |state, _| { - // render_icon_button( - // theme - // .collab_panel - // .leave_call_button - // .style_for(is_selected, state), - // "icons/link.svg", - // ) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, _, cx| { - // let item = ClipboardItem::new(channel_link_copy.clone()); - // cx.write_to_clipboard(item) - // }) - // .with_tooltip::( - // 0, - // channel_tooltip_text.unwrap(), - // None, - // tooltip_style.clone(), - // cx, - // ) - // }), - // Section::Contacts => Some( - // MouseEventHandler::new::(0, cx, |state, _| { - // render_icon_button( - // theme - // .collab_panel - // .add_contact_button - // .style_for(is_selected, state), - // "icons/plus.svg", - // ) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, |_, this, cx| { - // this.toggle_contact_finder(cx); - // }) - // .with_tooltip::( - // 0, - // "Search for new contact", - // None, - // tooltip_style.clone(), - // cx, - // ), - // ), - // Section::Channels => { - // if cx - // .global::>() - // .currently_dragged::(cx.window()) - // .is_some() - // && self.drag_target_channel == ChannelDragTarget::Root - // { - // is_dragged_over = true; - // } - - // Some( - // MouseEventHandler::new::(0, cx, |state, _| { - // render_icon_button( - // theme - // .collab_panel - // .add_contact_button - // .style_for(is_selected, state), - // "icons/plus.svg", - // ) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx)) - // .with_tooltip::( - // 0, - // "Create a channel", - // None, - // tooltip_style.clone(), - // cx, - // ), - // ) - // } - // _ => None, - // }; - - // let can_collapse = match section { - // Section::ActiveCall | Section::Channels | Section::Contacts => false, - // Section::ChannelInvites - // | Section::ContactRequests - // | Section::Online - // | Section::Offline => true, - // }; - // let icon_size = (&theme.collab_panel).section_icon_size; - // let mut result = MouseEventHandler::new::(section as usize, cx, |state, _| { - // let header_style = if can_collapse { - // theme - // .collab_panel - // .subheader_row - // .in_state(is_selected) - // .style_for(state) - // } else { - // &theme.collab_panel.header_row - // }; - - // Flex::row() - // .with_children(if can_collapse { - // Some( - // Svg::new(if is_collapsed { - // "icons/chevron_right.svg" - // } else { - // "icons/chevron_down.svg" - // }) - // .with_color(header_style.text.color) - // .constrained() - // .with_max_width(icon_size) - // .with_max_height(icon_size) - // .aligned() - // .constrained() - // .with_width(icon_size) - // .contained() - // .with_margin_right( - // theme.collab_panel.contact_username.container.margin.left, - // ), - // ) - // } else if let Some(channel_icon) = channel_icon { - // Some( - // Svg::new(channel_icon) - // .with_color(header_style.text.color) - // .constrained() - // .with_max_width(icon_size) - // .with_max_height(icon_size) - // .aligned() - // .constrained() - // .with_width(icon_size) - // .contained() - // .with_margin_right( - // theme.collab_panel.contact_username.container.margin.left, - // ), - // ) - // } else { - // None - // }) - // .with_child( - // Label::new(text, header_style.text.clone()) - // .aligned() - // .left() - // .flex(1., true), - // ) - // .with_children(button.map(|button| button.aligned().right())) - // .constrained() - // .with_height(theme.collab_panel.row_height) - // .contained() - // .with_style(if is_dragged_over { - // theme.collab_panel.dragged_over_header - // } else { - // header_style.container - // }) - // }); - - // result = result - // .on_move(move |_, this, cx| { - // if cx - // .global::>() - // .currently_dragged::(cx.window()) - // .is_some() - // { - // this.drag_target_channel = ChannelDragTarget::Root; - // cx.notify() - // } - // }) - // .on_up(MouseButton::Left, move |_, this, cx| { - // if let Some((_, dragged_channel)) = cx - // .global::>() - // .currently_dragged::(cx.window()) - // { - // this.channel_store - // .update(cx, |channel_store, cx| { - // channel_store.move_channel(dragged_channel.id, None, cx) - // }) - // .detach_and_log_err(cx) - // } - // }); - - // if can_collapse { - // result = result - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // if can_collapse { - // this.toggle_section_expanded(section, cx); - // } - // }) - // } - - // result.into_any() - // } - - // fn render_contact( - // contact: &Contact, - // calling: bool, - // project: &ModelHandle, - // theme: &theme::Theme, - // is_selected: bool, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum ContactTooltip {} - - // let collab_theme = &theme.collab_panel; - // let online = contact.online; - // let busy = contact.busy || calling; - // let user_id = contact.user.id; - // let github_login = contact.user.github_login.clone(); - // let initial_project = project.clone(); - - // let event_handler = - // MouseEventHandler::new::(contact.user.id as usize, cx, |state, cx| { - // Flex::row() - // .with_children(contact.user.avatar.clone().map(|avatar| { - // let status_badge = if contact.online { - // Some( - // Empty::new() - // .collapsed() - // .contained() - // .with_style(if busy { - // collab_theme.contact_status_busy - // } else { - // collab_theme.contact_status_free - // }) - // .aligned(), - // ) - // } else { - // None - // }; - // Stack::new() - // .with_child( - // Image::from_data(avatar) - // .with_style(collab_theme.contact_avatar) - // .aligned() - // .left(), - // ) - // .with_children(status_badge) - // })) - // .with_child( - // Label::new( - // contact.user.github_login.clone(), - // collab_theme.contact_username.text.clone(), - // ) - // .contained() - // .with_style(collab_theme.contact_username.container) - // .aligned() - // .left() - // .flex(1., true), - // ) - // .with_children(if state.hovered() { - // Some( - // MouseEventHandler::new::( - // contact.user.id as usize, - // cx, - // |mouse_state, _| { - // let button_style = - // collab_theme.contact_button.style_for(mouse_state); - // render_icon_button(button_style, "icons/x.svg") - // .aligned() - // .flex_float() - // }, - // ) - // .with_padding(Padding::uniform(2.)) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.remove_contact(user_id, &github_login, cx); - // }) - // .flex_float(), - // ) - // } else { - // None - // }) - // .with_children(if calling { - // Some( - // Label::new("Calling", collab_theme.calling_indicator.text.clone()) - // .contained() - // .with_style(collab_theme.calling_indicator.container) - // .aligned(), - // ) - // } else { - // None - // }) - // .constrained() - // .with_height(collab_theme.row_height) - // .contained() - // .with_style( - // *collab_theme - // .contact_row - // .in_state(is_selected) - // .style_for(state), - // ) - // }); - - // if online && !busy { - // let room = ActiveCall::global(cx).read(cx).room(); - // let label = if room.is_some() { - // format!("Invite {} to join call", contact.user.github_login) - // } else { - // format!("Call {}", contact.user.github_login) - // }; - - // event_handler - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.call(user_id, Some(initial_project.clone()), cx); - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .with_tooltip::( - // contact.user.id as usize, - // label, - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // } else { - // event_handler - // .with_tooltip::( - // contact.user.id as usize, - // format!( - // "{} is {}", - // contact.user.github_login, - // if busy { "on a call" } else { "offline" } - // ), - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // } - // } - // fn render_contact_placeholder( // &self, // theme: &theme::CollabPanel, @@ -1939,335 +1559,6 @@ impl CollabPanel { // .into_any() // } - // fn render_channel( - // &self, - // channel: &Channel, - // depth: usize, - // theme: &theme::Theme, - // is_selected: bool, - // has_children: bool, - // ix: usize, - // cx: &mut ViewContext, - // ) -> AnyElement { - // let channel_id = channel.id; - // let collab_theme = &theme.collab_panel; - // let is_public = self - // .channel_store - // .read(cx) - // .channel_for_id(channel_id) - // .map(|channel| channel.visibility) - // == Some(proto::ChannelVisibility::Public); - // let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id); - // let disclosed = - // has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); - - // let is_active = maybe!({ - // let call_channel = ActiveCall::global(cx) - // .read(cx) - // .room()? - // .read(cx) - // .channel_id()?; - // Some(call_channel == channel_id) - // }) - // .unwrap_or(false); - - // const FACEPILE_LIMIT: usize = 3; - - // enum ChannelCall {} - // enum ChannelNote {} - // enum NotesTooltip {} - // enum ChatTooltip {} - // enum ChannelTooltip {} - - // let mut is_dragged_over = false; - // if cx - // .global::>() - // .currently_dragged::(cx.window()) - // .is_some() - // && self.drag_target_channel == ChannelDragTarget::Channel(channel_id) - // { - // is_dragged_over = true; - // } - - // let has_messages_notification = channel.unseen_message_id.is_some(); - - // MouseEventHandler::new::(ix, cx, |state, cx| { - // let row_hovered = state.hovered(); - - // let mut select_state = |interactive: &Interactive| { - // if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() { - // interactive.clicked.as_ref().unwrap().clone() - // } else if state.hovered() || other_selected { - // interactive - // .hovered - // .as_ref() - // .unwrap_or(&interactive.default) - // .clone() - // } else { - // interactive.default.clone() - // } - // }; - - // Flex::::row() - // .with_child( - // Svg::new(if is_public { - // "icons/public.svg" - // } else { - // "icons/hash.svg" - // }) - // .with_color(collab_theme.channel_hash.color) - // .constrained() - // .with_width(collab_theme.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child({ - // let style = collab_theme.channel_name.inactive_state(); - // Flex::row() - // .with_child( - // Label::new(channel.name.clone(), style.text.clone()) - // .contained() - // .with_style(style.container) - // .aligned() - // .left() - // .with_tooltip::( - // ix, - // "Join channel", - // None, - // theme.tooltip.clone(), - // cx, - // ), - // ) - // .with_children({ - // let participants = - // self.channel_store.read(cx).channel_participants(channel_id); - - // if !participants.is_empty() { - // let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); - - // let result = FacePile::new(collab_theme.face_overlap) - // .with_children( - // participants - // .iter() - // .filter_map(|user| { - // Some( - // Image::from_data(user.avatar.clone()?) - // .with_style(collab_theme.channel_avatar), - // ) - // }) - // .take(FACEPILE_LIMIT), - // ) - // .with_children((extra_count > 0).then(|| { - // Label::new( - // format!("+{}", extra_count), - // collab_theme.extra_participant_label.text.clone(), - // ) - // .contained() - // .with_style(collab_theme.extra_participant_label.container) - // })); - - // Some(result) - // } else { - // None - // } - // }) - // .with_spacing(8.) - // .align_children_center() - // .flex(1., true) - // }) - // .with_child( - // MouseEventHandler::new::(ix, cx, move |mouse_state, _| { - // let container_style = collab_theme - // .disclosure - // .button - // .style_for(mouse_state) - // .container; - - // if channel.unseen_message_id.is_some() { - // Svg::new("icons/conversations.svg") - // .with_color(collab_theme.channel_note_active_color) - // .constrained() - // .with_width(collab_theme.channel_hash.width) - // .contained() - // .with_style(container_style) - // .with_uniform_padding(4.) - // .into_any() - // } else if row_hovered { - // Svg::new("icons/conversations.svg") - // .with_color(collab_theme.channel_hash.color) - // .constrained() - // .with_width(collab_theme.channel_hash.width) - // .contained() - // .with_style(container_style) - // .with_uniform_padding(4.) - // .into_any() - // } else { - // Empty::new().into_any() - // } - // }) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.join_channel_chat(&JoinChannelChat { channel_id }, cx); - // }) - // .with_tooltip::( - // ix, - // "Open channel chat", - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .contained() - // .with_margin_right(4.), - // ) - // .with_child( - // MouseEventHandler::new::(ix, cx, move |mouse_state, cx| { - // let container_style = collab_theme - // .disclosure - // .button - // .style_for(mouse_state) - // .container; - // if row_hovered || channel.unseen_note_version.is_some() { - // Svg::new("icons/file.svg") - // .with_color(if channel.unseen_note_version.is_some() { - // collab_theme.channel_note_active_color - // } else { - // collab_theme.channel_hash.color - // }) - // .constrained() - // .with_width(collab_theme.channel_hash.width) - // .contained() - // .with_style(container_style) - // .with_uniform_padding(4.) - // .with_margin_right(collab_theme.channel_hash.container.margin.left) - // .with_tooltip::( - // ix as usize, - // "Open channel notes", - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // } else if has_messages_notification { - // Empty::new() - // .constrained() - // .with_width(collab_theme.channel_hash.width) - // .contained() - // .with_uniform_padding(4.) - // .with_margin_right(collab_theme.channel_hash.container.margin.left) - // .into_any() - // } else { - // Empty::new().into_any() - // } - // }) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); - // }), - // ) - // .align_children_center() - // .styleable_component() - // .disclosable( - // disclosed, - // Box::new(ToggleCollapse { - // location: channel.id.clone(), - // }), - // ) - // .with_id(ix) - // .with_style(collab_theme.disclosure.clone()) - // .element() - // .constrained() - // .with_height(collab_theme.row_height) - // .contained() - // .with_style(select_state( - // collab_theme - // .channel_row - // .in_state(is_selected || is_active || is_dragged_over), - // )) - // .with_padding_left( - // collab_theme.channel_row.default_style().padding.left - // + collab_theme.channel_indent * depth as f32, - // ) - // }) - // .on_click(MouseButton::Left, move |_, this, cx| { - // if this.drag_target_channel == ChannelDragTarget::None { - // if is_active { - // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) - // } else { - // this.join_channel(channel_id, cx) - // } - // } - // }) - // .on_click(MouseButton::Right, { - // let channel = channel.clone(); - // move |e, this, cx| { - // this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx); - // } - // }) - // .on_up(MouseButton::Left, move |_, this, cx| { - // if let Some((_, dragged_channel)) = cx - // .global::>() - // .currently_dragged::(cx.window()) - // { - // this.channel_store - // .update(cx, |channel_store, cx| { - // channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) - // }) - // .detach_and_log_err(cx) - // } - // }) - // .on_move({ - // let channel = channel.clone(); - // move |_, this, cx| { - // if let Some((_, dragged_channel)) = cx - // .global::>() - // .currently_dragged::(cx.window()) - // { - // if channel.id != dragged_channel.id { - // this.drag_target_channel = ChannelDragTarget::Channel(channel.id); - // } - // cx.notify() - // } - // } - // }) - // .as_draggable::<_, Channel>( - // channel.clone(), - // move |_, channel, cx: &mut ViewContext| { - // let theme = &theme::current(cx).collab_panel; - - // Flex::::row() - // .with_child( - // Svg::new("icons/hash.svg") - // .with_color(theme.channel_hash.color) - // .constrained() - // .with_width(theme.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child( - // Label::new(channel.name.clone(), theme.channel_name.text.clone()) - // .contained() - // .with_style(theme.channel_name.container) - // .aligned() - // .left(), - // ) - // .align_children_center() - // .contained() - // .with_background_color( - // theme - // .container - // .background_color - // .unwrap_or(gpui::color::Color::transparent_black()), - // ) - // .contained() - // .with_padding_left( - // theme.channel_row.default_style().padding.left - // + theme.channel_indent * depth as f32, - // ) - // .into_any() - // }, - // ) - // .with_cursor_style(CursorStyle::PointingHand) - // .into_any() - // } - // fn render_channel_notes( // &self, // channel_id: ChannelId, @@ -2954,9 +2245,9 @@ impl CollabPanel { // cx.focus_self(); // } - // fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool { - // self.collapsed_channels.binary_search(&channel_id).is_ok() - // } + fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool { + self.collapsed_channels.binary_search(&channel_id).is_ok() + } // fn leave_call(cx: &mut ViewContext) { // ActiveCall::global(cx) @@ -3270,49 +2561,774 @@ impl CollabPanel { } fn render_signed_in(&mut self, cx: &mut ViewContext) -> List { - let contacts = self.contacts(cx).unwrap_or_default(); - let workspace = self.workspace.clone(); - - let children = once( - ListHeader::new("Contacts") - .right_button( - IconButton::new("add-contact", Icon::Plus).on_click(cx.listener( - |this, _, cx| { - todo!(); - //this.toggle_contact_finder(cx); - }, - )), - ) - .render(cx), - ) - .chain(contacts.into_iter().map(|contact| { - let id = contact.user.id; - h_stack() - .p_2() - .gap_2() - .children( - contact - .user - .avatar - .as_ref() - .map(|avatar| Avatar::data(avatar.clone())), + let is_selected = false; // todo!() this.selection == Some(ix); + + List::new().children(self.entries.clone().into_iter().map(|entry| { + match entry { + ListEntry::Header(section) => { + let is_collapsed = self.collapsed_sections.contains(§ion); + self.render_header(section, is_selected, is_collapsed, cx) + .into_any_element() + } + ListEntry::Contact { contact, calling } => self + .render_contact(&*contact, calling, is_selected, cx) + .into_any_element(), + ListEntry::ContactPlaceholder => self + .render_contact_placeholder(is_selected, cx) + .into_any_element(), + ListEntry::IncomingRequest(user) => self + .render_contact_request(user, true, is_selected, cx) + .into_any_element(), + ListEntry::OutgoingRequest(user) => self + .render_contact_request(user, false, is_selected, cx) + .into_any_element(), + ListEntry::Channel { + channel, + depth, + has_children, + } => self + .render_channel(&*channel, depth, has_children, is_selected, cx) + .into_any_element(), + ListEntry::ChannelEditor { depth } => todo!(), + } + })) + } + + fn render_header( + &mut self, + section: Section, + is_selected: bool, + is_collapsed: bool, + cx: &ViewContext, + ) -> impl IntoElement { + // let mut channel_link = None; + // let mut channel_tooltip_text = None; + // let mut channel_icon = None; + // let mut is_dragged_over = false; + + let text = match section { + Section::ActiveCall => { + // let channel_name = maybe!({ + // let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; + + // let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; + + // channel_link = Some(channel.link()); + // (channel_icon, channel_tooltip_text) = match channel.visibility { + // proto::ChannelVisibility::Public => { + // (Some("icons/public.svg"), Some("Copy public channel link.")) + // } + // proto::ChannelVisibility::Members => { + // (Some("icons/hash.svg"), Some("Copy private channel link.")) + // } + // }; + + // Some(channel.name.as_str()) + // }); + + // if let Some(name) = channel_name { + // SharedString::from(format!("{}", name)) + // } else { + // SharedString::from("Current Call") + // } + todo!() + } + Section::ContactRequests => SharedString::from("Requests"), + Section::Contacts => SharedString::from("Contacts"), + Section::Channels => SharedString::from("Channels"), + Section::ChannelInvites => SharedString::from("Invites"), + Section::Online => SharedString::from("Online"), + Section::Offline => SharedString::from("Offline"), + }; + + let button = match section { + Section::ActiveCall => + // channel_link.map(|channel_link| { + // let channel_link_copy = channel_link.clone(); + // MouseEventHandler::new::(0, cx, |state, _| { + // render_icon_button( + // theme + // .collab_panel + // .leave_call_button + // .style_for(is_selected, state), + // "icons/link.svg", + // ) + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, _, cx| { + // let item = ClipboardItem::new(channel_link_copy.clone()); + // cx.write_to_clipboard(item) + // }) + // .with_tooltip::( + // 0, + // channel_tooltip_text.unwrap(), + // None, + // tooltip_style.clone(), + // cx, + // ) + // }), + { + todo!() + } + Section::Contacts => Some( + IconButton::new("add-contact", Icon::Plus) + .on_click(cx.listener(|this, _, cx| { + todo!() + // this.toggle_contact_finder(cx) + })) + .tooltip(|cx| Tooltip::text("Search for new contact", cx)), + ), + Section::Channels => { + // todo!() + // if cx + // .global::>() + // .currently_dragged::(cx.window()) + // .is_some() + // && self.drag_target_channel == ChannelDragTarget::Root + // { + // is_dragged_over = true; + // } + + Some( + IconButton::new("add-channel", Icon::Plus) + .on_click(cx.listener(|this, _, cx| { + todo!() + // this.new_root_channel(cx) + })) + .tooltip(|cx| Tooltip::text("Create a channel", cx)), ) - .child(Label::new(contact.user.github_login.clone())) - .on_mouse_down(gpui::MouseButton::Left, { - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state() - .invite(id, None, cx) - .detach_and_log_err(cx) - }) - .log_err(); - } - }) - })); + } + _ => None, + }; + + let can_collapse = match section { + Section::ActiveCall | Section::Channels | Section::Contacts => false, + Section::ChannelInvites + | Section::ContactRequests + | Section::Online + | Section::Offline => true, + }; + + let mut header = ListHeader::new(text); + if let Some(button) = button { + header = header.right_button(button) + } + // todo!() is selected + if can_collapse { + // todo!() on click to toggle + header = header.toggle(ui::Toggle::Toggled(is_collapsed)); + } + + header + } + fn render_contact( + &mut self, + contact: &Contact, + calling: bool, + is_selected: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + enum ContactTooltip {} + + let online = contact.online; + let busy = contact.busy || calling; + let user_id = contact.user.id; + let github_login = SharedString::from(contact.user.github_login.clone()); + + let item = ListItem::new(github_login.clone()) + .child(Label::new(github_login.clone())) + .on_click(cx.listener(|this, _, cx| { + todo!(); + })); + + // let event_handler = + // MouseEventHandler::new::(contact.user.id as usize, cx, |state, cx| { + // Flex::row() + // .with_children(contact.user.avatar.clone().map(|avatar| { + // let status_badge = if contact.online { + // Some( + // Empty::new() + // .collapsed() + // .contained() + // .with_style(if busy { + // collab_theme.contact_status_busy + // } else { + // collab_theme.contact_status_free + // }) + // .aligned(), + // ) + // } else { + // None + // }; + // Stack::new() + // .with_child( + // Image::from_data(avatar) + // .with_style(collab_theme.contact_avatar) + // .aligned() + // .left(), + // ) + // .with_children(status_badge) + // })) + // .with_child( + // Label::new( + // contact.user.github_login.clone(), + // collab_theme.contact_username.text.clone(), + // ) + // .contained() + // .with_style(collab_theme.contact_username.container) + // .aligned() + // .left() + // .flex(1., true), + // ) + // .with_children(if state.hovered() { + // Some( + // MouseEventHandler::new::( + // contact.user.id as usize, + // cx, + // |mouse_state, _| { + // let button_style = + // collab_theme.contact_button.style_for(mouse_state); + // render_icon_button(button_style, "icons/x.svg") + // .aligned() + // .flex_float() + // }, + // ) + // .with_padding(Padding::uniform(2.)) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.remove_contact(user_id, &github_login, cx); + // }) + // .flex_float(), + // ) + // } else { + // None + // }) + // .with_children(if calling { + // Some( + // Label::new("Calling", collab_theme.calling_indicator.text.clone()) + // .contained() + // .with_style(collab_theme.calling_indicator.container) + // .aligned(), + // ) + // } else { + // None + // }) + // .constrained() + // .with_height(collab_theme.row_height) + // .contained() + // .with_style( + // *collab_theme + // .contact_row + // .in_state(is_selected) + // .style_for(state), + // ) + // }); + + // if online && !busy { + // let room = ActiveCall::global(cx).read(cx).room(); + // let label = if room.is_some() { + // format!("Invite {} to join call", contact.user.github_login) + // } else { + // format!("Call {}", contact.user.github_login) + // }; + + // event_handler + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.call(user_id, Some(initial_project.clone()), cx); + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .with_tooltip::( + // contact.user.id as usize, + // label, + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .into_any() + // } else { + // event_handler + // .with_tooltip::( + // contact.user.id as usize, + // format!( + // "{} is {}", + // contact.user.github_login, + // if busy { "on a call" } else { "offline" } + // ), + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .into_any() + // }; + + item + } + + fn render_contact_request( + &mut self, + user: Arc, + is_incoming: bool, + is_selected: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + let github_login = SharedString::from(user.github_login.clone()); + + let mut row = ListItem::new(github_login.clone()).child(Label::new(github_login.clone())); + + // .with_children(user.avatar.clone().map(|avatar| { + // Image::from_data(avatar) + // .with_style(theme.contact_avatar) + // .aligned() + // .left() + // })) + // .with_child( + // Label::new( + // user.github_login.clone(), + // theme.contact_username.text.clone(), + // ) + // .contained() + // .with_style(theme.contact_username.container) + // .aligned() + // .left() + // .flex(1., true), + // ); + + // let user_id = user.id; + // let github_login = user.github_login.clone(); + // let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); + // let button_spacing = theme.contact_button_spacing; + + // if is_incoming { + // row.add_child( + // MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + // let button_style = if is_contact_request_pending { + // &theme.disabled_button + // } else { + // theme.contact_button.style_for(mouse_state) + // }; + // render_icon_button(button_style, "icons/x.svg").aligned() + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.respond_to_contact_request(user_id, false, cx); + // }) + // .contained() + // .with_margin_right(button_spacing), + // ); + + // row.add_child( + // MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + // let button_style = if is_contact_request_pending { + // &theme.disabled_button + // } else { + // theme.contact_button.style_for(mouse_state) + // }; + // render_icon_button(button_style, "icons/check.svg") + // .aligned() + // .flex_float() + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.respond_to_contact_request(user_id, true, cx); + // }), + // ); + // } else { + // row.add_child( + // MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + // let button_style = if is_contact_request_pending { + // &theme.disabled_button + // } else { + // theme.contact_button.style_for(mouse_state) + // }; + // render_icon_button(button_style, "icons/x.svg") + // .aligned() + // .flex_float() + // }) + // .with_padding(Padding::uniform(2.)) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.remove_contact(user_id, &github_login, cx); + // }) + // .flex_float(), + // ); + // } + + // row.constrained() + // .with_height(theme.row_height) + // .contained() + // .with_style( + // *theme + // .contact_row + // .in_state(is_selected) + // .style_for(&mut Default::default()), + // ) + // .into_any() + row + } + + fn render_contact_placeholder( + &self, + is_selected: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + ListItem::new("contact-placeholder") + .child(Label::new("Add a Contact")) + .on_click(cx.listener(|this, _, cx| todo!())) + // enum AddContacts {} + // MouseEventHandler::new::(0, cx, |state, _| { + // let style = theme.list_empty_state.style_for(is_selected, state); + // Flex::row() + // .with_child( + // Svg::new("icons/plus.svg") + // .with_color(theme.list_empty_icon.color) + // .constrained() + // .with_width(theme.list_empty_icon.width) + // .aligned() + // .left(), + // ) + // .with_child( + // Label::new("Add a contact", style.text.clone()) + // .contained() + // .with_style(theme.list_empty_label_container), + // ) + // .align_children_center() + // .contained() + // .with_style(style.container) + // .into_any() + // }) + // .on_click(MouseButton::Left, |_, this, cx| { + // this.toggle_contact_finder(cx); + // }) + // .into_any() + } - List::new().children(children) + fn render_channel( + &self, + channel: &Channel, + depth: usize, + has_children: bool, + is_selected: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + let channel_id = channel.id; + ListItem::new(channel_id as usize).child(Label::new(channel.name.clone())) + // let channel_id = channel.id; + // let collab_theme = &theme.collab_panel; + // let is_public = self + // .channel_store + // .read(cx) + // .channel_for_id(channel_id) + // .map(|channel| channel.visibility) + // == Some(proto::ChannelVisibility::Public); + // let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id); + // let disclosed = + // has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); + + // let is_active = maybe!({ + // let call_channel = ActiveCall::global(cx) + // .read(cx) + // .room()? + // .read(cx) + // .channel_id()?; + // Some(call_channel == channel_id) + // }) + // .unwrap_or(false); + + // const FACEPILE_LIMIT: usize = 3; + + // enum ChannelCall {} + // enum ChannelNote {} + // enum NotesTooltip {} + // enum ChatTooltip {} + // enum ChannelTooltip {} + + // let mut is_dragged_over = false; + // if cx + // .global::>() + // .currently_dragged::(cx.window()) + // .is_some() + // && self.drag_target_channel == ChannelDragTarget::Channel(channel_id) + // { + // is_dragged_over = true; + // } + + // let has_messages_notification = channel.unseen_message_id.is_some(); + + // MouseEventHandler::new::(ix, cx, |state, cx| { + // let row_hovered = state.hovered(); + + // let mut select_state = |interactive: &Interactive| { + // if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() { + // interactive.clicked.as_ref().unwrap().clone() + // } else if state.hovered() || other_selected { + // interactive + // .hovered + // .as_ref() + // .unwrap_or(&interactive.default) + // .clone() + // } else { + // interactive.default.clone() + // } + // }; + + // Flex::::row() + // .with_child( + // Svg::new(if is_public { + // "icons/public.svg" + // } else { + // "icons/hash.svg" + // }) + // .with_color(collab_theme.channel_hash.color) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .aligned() + // .left(), + // ) + // .with_child({ + // let style = collab_theme.channel_name.inactive_state(); + // Flex::row() + // .with_child( + // Label::new(channel.name.clone(), style.text.clone()) + // .contained() + // .with_style(style.container) + // .aligned() + // .left() + // .with_tooltip::( + // ix, + // "Join channel", + // None, + // theme.tooltip.clone(), + // cx, + // ), + // ) + // .with_children({ + // let participants = + // self.channel_store.read(cx).channel_participants(channel_id); + + // if !participants.is_empty() { + // let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); + + // let result = FacePile::new(collab_theme.face_overlap) + // .with_children( + // participants + // .iter() + // .filter_map(|user| { + // Some( + // Image::from_data(user.avatar.clone()?) + // .with_style(collab_theme.channel_avatar), + // ) + // }) + // .take(FACEPILE_LIMIT), + // ) + // .with_children((extra_count > 0).then(|| { + // Label::new( + // format!("+{}", extra_count), + // collab_theme.extra_participant_label.text.clone(), + // ) + // .contained() + // .with_style(collab_theme.extra_participant_label.container) + // })); + + // Some(result) + // } else { + // None + // } + // }) + // .with_spacing(8.) + // .align_children_center() + // .flex(1., true) + // }) + // .with_child( + // MouseEventHandler::new::(ix, cx, move |mouse_state, _| { + // let container_style = collab_theme + // .disclosure + // .button + // .style_for(mouse_state) + // .container; + + // if channel.unseen_message_id.is_some() { + // Svg::new("icons/conversations.svg") + // .with_color(collab_theme.channel_note_active_color) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .contained() + // .with_style(container_style) + // .with_uniform_padding(4.) + // .into_any() + // } else if row_hovered { + // Svg::new("icons/conversations.svg") + // .with_color(collab_theme.channel_hash.color) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .contained() + // .with_style(container_style) + // .with_uniform_padding(4.) + // .into_any() + // } else { + // Empty::new().into_any() + // } + // }) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.join_channel_chat(&JoinChannelChat { channel_id }, cx); + // }) + // .with_tooltip::( + // ix, + // "Open channel chat", + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .contained() + // .with_margin_right(4.), + // ) + // .with_child( + // MouseEventHandler::new::(ix, cx, move |mouse_state, cx| { + // let container_style = collab_theme + // .disclosure + // .button + // .style_for(mouse_state) + // .container; + // if row_hovered || channel.unseen_note_version.is_some() { + // Svg::new("icons/file.svg") + // .with_color(if channel.unseen_note_version.is_some() { + // collab_theme.channel_note_active_color + // } else { + // collab_theme.channel_hash.color + // }) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .contained() + // .with_style(container_style) + // .with_uniform_padding(4.) + // .with_margin_right(collab_theme.channel_hash.container.margin.left) + // .with_tooltip::( + // ix as usize, + // "Open channel notes", + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .into_any() + // } else if has_messages_notification { + // Empty::new() + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .contained() + // .with_uniform_padding(4.) + // .with_margin_right(collab_theme.channel_hash.container.margin.left) + // .into_any() + // } else { + // Empty::new().into_any() + // } + // }) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); + // }), + // ) + // .align_children_center() + // .styleable_component() + // .disclosable( + // disclosed, + // Box::new(ToggleCollapse { + // location: channel.id.clone(), + // }), + // ) + // .with_id(ix) + // .with_style(collab_theme.disclosure.clone()) + // .element() + // .constrained() + // .with_height(collab_theme.row_height) + // .contained() + // .with_style(select_state( + // collab_theme + // .channel_row + // .in_state(is_selected || is_active || is_dragged_over), + // )) + // .with_padding_left( + // collab_theme.channel_row.default_style().padding.left + // + collab_theme.channel_indent * depth as f32, + // ) + // }) + // .on_click(MouseButton::Left, move |_, this, cx| { + // if this.drag_target_channel == ChannelDragTarget::None { + // if is_active { + // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) + // } else { + // this.join_channel(channel_id, cx) + // } + // } + // }) + // .on_click(MouseButton::Right, { + // let channel = channel.clone(); + // move |e, this, cx| { + // this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx); + // } + // }) + // .on_up(MouseButton::Left, move |_, this, cx| { + // if let Some((_, dragged_channel)) = cx + // .global::>() + // .currently_dragged::(cx.window()) + // { + // this.channel_store + // .update(cx, |channel_store, cx| { + // channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) + // }) + // .detach_and_log_err(cx) + // } + // }) + // .on_move({ + // let channel = channel.clone(); + // move |_, this, cx| { + // if let Some((_, dragged_channel)) = cx + // .global::>() + // .currently_dragged::(cx.window()) + // { + // if channel.id != dragged_channel.id { + // this.drag_target_channel = ChannelDragTarget::Channel(channel.id); + // } + // cx.notify() + // } + // } + // }) + // .as_draggable::<_, Channel>( + // channel.clone(), + // move |_, channel, cx: &mut ViewContext| { + // let theme = &theme::current(cx).collab_panel; + + // Flex::::row() + // .with_child( + // Svg::new("icons/hash.svg") + // .with_color(theme.channel_hash.color) + // .constrained() + // .with_width(theme.channel_hash.width) + // .aligned() + // .left(), + // ) + // .with_child( + // Label::new(channel.name.clone(), theme.channel_name.text.clone()) + // .contained() + // .with_style(theme.channel_name.container) + // .aligned() + // .left(), + // ) + // .align_children_center() + // .contained() + // .with_background_color( + // theme + // .container + // .background_color + // .unwrap_or(gpui::color::Color::transparent_black()), + // ) + // .contained() + // .with_padding_left( + // theme.channel_row.default_style().padding.left + // + theme.channel_indent * depth as f32, + // ) + // .into_any() + // }, + // ) + // .with_cursor_style(CursorStyle::PointingHand) + // .into_any() } } diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 3212b6182b7b64fe1d79a2f20097fffd64c603b5..9deec31d2132179194e6a56341ffc46f1b172963 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -21,7 +21,7 @@ audio = { package = "audio2", path = "../audio2" } auto_update = { package = "auto_update2", path = "../auto_update2" } # breadcrumbs = { path = "../breadcrumbs" } call = { package = "call2", path = "../call2" } -# channel = { path = "../channel" } +channel = { package = "channel2", path = "../channel2" } cli = { path = "../cli" } collab_ui = { package = "collab_ui2", path = "../collab_ui2" } collections = { path = "../collections" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index c9ed26436ab8e07529c2365d8bb91ec989eed51a..7b51c894fefe9f1cee1a9d741ec883f77e956791 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -189,7 +189,7 @@ fn main() { let app_state = Arc::new(AppState { languages, client: client.clone(), - user_store, + user_store: user_store.clone(), fs, build_window_options, call_factory: call::Call::new, @@ -210,7 +210,7 @@ fn main() { // outline::init(cx); // project_symbols::init(cx); project_panel::init(Assets, cx); - // channel::init(&client, user_store.clone(), cx); + channel::init(&client, user_store.clone(), cx); // diagnostics::init(cx); search::init(cx); // semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); From 17b5f9294ccca7de6e6d9758110ce7bdf8c2ecd6 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 27 Nov 2023 23:11:06 -0700 Subject: [PATCH 15/35] Fix hover state when element is occluded --- crates/gpui2/src/elements/div.rs | 29 ++++++++++++++++++------- crates/gpui2/src/window.rs | 37 ++++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 406f2ea31179d265202b7bd381c3b8b782b813c9..6bf24f750b3a2f6c0e1ea51d0bd1a3599b6eca33 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -755,6 +755,14 @@ impl Interactivity { ) { let style = self.compute_style(Some(bounds), element_state, cx); + if style + .background + .as_ref() + .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent())) + { + cx.with_z_index(style.z_index.unwrap_or(0), |cx| cx.add_opaque_layer(bounds)) + } + if let Some(mouse_cursor) = style.mouse_cursor { let hovered = bounds.contains_point(&cx.mouse_position()); if hovered { @@ -1098,19 +1106,21 @@ impl Interactivity { } } } - // if self.hover_style.is_some() { - if bounds.contains_point(&mouse_position) { - // eprintln!("div hovered {bounds:?} {mouse_position:?}"); - style.refine(&self.hover_style); - } else { - // eprintln!("div NOT hovered {bounds:?} {mouse_position:?}"); + if self.hover_style.is_some() { + if bounds + .intersect(&cx.content_mask().bounds) + .contains_point(&mouse_position) + && cx.was_top_layer(&mouse_position, cx.stacking_order()) + { + style.refine(&self.hover_style); + } } - // } if let Some(drag) = cx.active_drag.take() { for (state_type, group_drag_style) in &self.group_drag_over_styles { if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { if *state_type == drag.view.entity_type() + // todo!() needs to handle cx.content_mask() and cx.is_top() && group_bounds.contains_point(&mouse_position) { style.refine(&group_drag_style.style); @@ -1120,7 +1130,10 @@ impl Interactivity { for (state_type, drag_over_style) in &self.drag_over_styles { if *state_type == drag.view.entity_type() - && bounds.contains_point(&mouse_position) + && bounds + .intersect(&cx.content_mask().bounds) + .contains_point(&mouse_position) + && cx.was_top_layer(&mouse_position, cx.stacking_order()) { style.refine(drag_over_style); } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 20561c544368b6b9c41124194c07ada33145c968..0d444d762ecd80c7846aa71c062dcfeb755e5a7d 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -12,7 +12,7 @@ use crate::{ VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Context as _, Result}; -use collections::HashMap; +use collections::{BTreeMap, HashMap}; use derive_more::{Deref, DerefMut}; use futures::{ channel::{mpsc, oneshot}, @@ -39,8 +39,8 @@ use util::ResultExt; /// A global stacking order, which is created by stacking successive z-index values. /// Each z-index will always be interpreted in the context of its parent z-index. -#[derive(Deref, DerefMut, Ord, PartialOrd, Eq, PartialEq, Clone, Default)] -pub(crate) struct StackingOrder(pub(crate) SmallVec<[u32; 16]>); +#[derive(Deref, DerefMut, Ord, PartialOrd, Eq, PartialEq, Clone, Default, Debug)] +pub struct StackingOrder(pub(crate) SmallVec<[u32; 16]>); /// Represents the two different phases when dispatching events. #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] @@ -243,7 +243,8 @@ pub(crate) struct Frame { pub(crate) dispatch_tree: DispatchTree, pub(crate) focus_listeners: Vec, pub(crate) scene_builder: SceneBuilder, - z_index_stack: StackingOrder, + pub(crate) depth_map: BTreeMap>, + pub(crate) z_index_stack: StackingOrder, content_mask_stack: Vec>, element_offset_stack: Vec>, } @@ -257,6 +258,7 @@ impl Frame { focus_listeners: Vec::new(), scene_builder: SceneBuilder::default(), z_index_stack: StackingOrder::default(), + depth_map: Default::default(), content_mask_stack: Vec::new(), element_offset_stack: Vec::new(), } @@ -806,6 +808,32 @@ impl<'a> WindowContext<'a> { result } + /// Called during painting to track which z-index is on top at each pixel position + pub fn add_opaque_layer(&mut self, bounds: Bounds) { + let stacking_order = self.window.current_frame.z_index_stack.clone(); + self.window + .current_frame + .depth_map + .insert(stacking_order, bounds); + } + + /// Returns true if the top-most opaque layer painted over this point was part of the + /// same layer as the given stacking order. + pub fn was_top_layer(&self, point: &Point, level: &StackingOrder) -> bool { + for (stack, bounds) in self.window.previous_frame.depth_map.iter() { + if bounds.contains_point(point) { + return level.starts_with(stack) || stack.starts_with(level); + } + } + + false + } + + /// Called during painting to get the current stacking order. + pub fn stacking_order(&self) -> &StackingOrder { + &self.window.current_frame.z_index_stack + } + /// Paint one or more drop shadows into the scene for the current frame at the current z-index. pub fn paint_shadows( &mut self, @@ -1153,6 +1181,7 @@ impl<'a> WindowContext<'a> { frame.mouse_listeners.values_mut().for_each(Vec::clear); frame.focus_listeners.clear(); frame.dispatch_tree.clear(); + frame.depth_map.clear(); } /// Dispatch a mouse or keyboard event on the window. From ca0dcf741f7467ee31e0b6f25713557fa799f5f5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 28 Nov 2023 00:07:26 -0700 Subject: [PATCH 16/35] Use layers to correct mouse event handling too --- crates/gpui2/src/elements/div.rs | 102 +++++++++++++++++++-------- crates/picker2/src/picker2.rs | 1 - crates/workspace2/src/modal_layer.rs | 5 -- 3 files changed, 72 insertions(+), 36 deletions(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 6bf24f750b3a2f6c0e1ea51d0bd1a3599b6eca33..6185cd644364794eec85f6218c62c7e3643f5cb8 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -3,7 +3,8 @@ use crate::{ BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle, IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, ScrollWheelEvent, - SharedString, Size, Style, StyleRefinement, Styled, Task, View, Visibility, WindowContext, + SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, Task, View, Visibility, + WindowContext, }; use collections::HashMap; use refineable::Refineable; @@ -84,7 +85,7 @@ pub trait InteractiveElement: Sized + Element { move |event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && event.button == button - && bounds.contains_point(&event.position) + && bounds.visibly_contains(&event.position, cx) { (listener)(event, cx) } @@ -99,7 +100,7 @@ pub trait InteractiveElement: Sized + Element { ) -> Self { self.interactivity().mouse_down_listeners.push(Box::new( move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { (listener)(event, cx) } }, @@ -117,7 +118,7 @@ pub trait InteractiveElement: Sized + Element { .push(Box::new(move |event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && event.button == button - && bounds.contains_point(&event.position) + && bounds.visibly_contains(&event.position, cx) { (listener)(event, cx) } @@ -132,7 +133,7 @@ pub trait InteractiveElement: Sized + Element { self.interactivity() .mouse_up_listeners .push(Box::new(move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { (listener)(event, cx) } })); @@ -145,7 +146,8 @@ pub trait InteractiveElement: Sized + Element { ) -> Self { self.interactivity().mouse_down_listeners.push(Box::new( move |event, bounds, phase, cx| { - if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) { + if phase == DispatchPhase::Capture && !bounds.visibly_contains(&event.position, cx) + { (listener)(event, cx) } }, @@ -163,7 +165,7 @@ pub trait InteractiveElement: Sized + Element { .push(Box::new(move |event, bounds, phase, cx| { if phase == DispatchPhase::Capture && event.button == button - && !bounds.contains_point(&event.position) + && !bounds.visibly_contains(&event.position, cx) { (listener)(event, cx); } @@ -177,7 +179,7 @@ pub trait InteractiveElement: Sized + Element { ) -> Self { self.interactivity().mouse_move_listeners.push(Box::new( move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { (listener)(event, cx); } }, @@ -191,7 +193,7 @@ pub trait InteractiveElement: Sized + Element { ) -> Self { self.interactivity().scroll_wheel_listeners.push(Box::new( move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { (listener)(event, cx); } }, @@ -526,15 +528,15 @@ pub type FocusListeners = SmallVec<[FocusListener; 2]>; pub type FocusListener = Box; pub type MouseDownListener = - Box, DispatchPhase, &mut WindowContext) + 'static>; + Box; pub type MouseUpListener = - Box, DispatchPhase, &mut WindowContext) + 'static>; + Box; pub type MouseMoveListener = - Box, DispatchPhase, &mut WindowContext) + 'static>; + Box; pub type ScrollWheelListener = - Box, DispatchPhase, &mut WindowContext) + 'static>; + Box; pub type ClickListener = Box; @@ -719,6 +721,18 @@ pub struct Interactivity { pub tooltip_builder: Option, } +#[derive(Clone)] +pub struct InteractiveBounds { + bounds: Bounds, + stacking_order: StackingOrder, +} + +impl InteractiveBounds { + fn visibly_contains(&self, point: &Point, cx: &WindowContext) -> bool { + self.bounds.contains_point(point) && cx.was_top_layer(&point, &self.stacking_order) + } +} + impl Interactivity { pub fn layout( &mut self, @@ -763,34 +777,44 @@ impl Interactivity { cx.with_z_index(style.z_index.unwrap_or(0), |cx| cx.add_opaque_layer(bounds)) } + let interactive_bounds = Rc::new(InteractiveBounds { + bounds: bounds.intersect(&cx.content_mask().bounds), + stacking_order: cx.stacking_order().clone(), + }); + if let Some(mouse_cursor) = style.mouse_cursor { - let hovered = bounds.contains_point(&cx.mouse_position()); + let mouse_position = &cx.mouse_position(); + let hovered = interactive_bounds.visibly_contains(mouse_position, cx); if hovered { cx.set_cursor_style(mouse_cursor); } } for listener in self.mouse_down_listeners.drain(..) { + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { - listener(event, &bounds, phase, cx); + listener(event, &*interactive_bounds, phase, cx); }) } for listener in self.mouse_up_listeners.drain(..) { + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { - listener(event, &bounds, phase, cx); + listener(event, &*interactive_bounds, phase, cx); }) } for listener in self.mouse_move_listeners.drain(..) { + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { - listener(event, &bounds, phase, cx); + listener(event, &*interactive_bounds, phase, cx); }) } for listener in self.scroll_wheel_listeners.drain(..) { + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { - listener(event, &bounds, phase, cx); + listener(event, &*interactive_bounds, phase, cx); }) } @@ -800,6 +824,7 @@ impl Interactivity { .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx)); if let Some(group_bounds) = hover_group_bounds { + // todo!() needs cx.was_top_layer let hovered = group_bounds.contains_point(&cx.mouse_position()); cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if phase == DispatchPhase::Capture { @@ -813,10 +838,11 @@ impl Interactivity { if self.hover_style.is_some() || (cx.active_drag.is_some() && !self.drag_over_styles.is_empty()) { - let hovered = bounds.contains_point(&cx.mouse_position()); + let interactive_bounds = interactive_bounds.clone(); + let hovered = interactive_bounds.visibly_contains(&cx.mouse_position(), cx); cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if phase == DispatchPhase::Capture { - if bounds.contains_point(&event.position) != hovered { + if interactive_bounds.visibly_contains(&event.position, cx) != hovered { cx.notify(); } } @@ -825,8 +851,11 @@ impl Interactivity { if cx.active_drag.is_some() { let drop_listeners = mem::take(&mut self.drop_listeners); + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if phase == DispatchPhase::Bubble + && interactive_bounds.visibly_contains(&event.position, &cx) + { if let Some(drag_state_type) = cx.active_drag.as_ref().map(|drag| drag.view.entity_type()) { @@ -855,6 +884,7 @@ impl Interactivity { if let Some(mouse_down) = mouse_down { if let Some(drag_listener) = drag_listener { let active_state = element_state.clicked_state.clone(); + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if cx.active_drag.is_some() { @@ -862,7 +892,7 @@ impl Interactivity { cx.notify(); } } else if phase == DispatchPhase::Bubble - && bounds.contains_point(&event.position) + && interactive_bounds.visibly_contains(&event.position, cx) && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD { *active_state.borrow_mut() = ElementClickedState::default(); @@ -875,8 +905,11 @@ impl Interactivity { }); } + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if phase == DispatchPhase::Bubble + && interactive_bounds.visibly_contains(&event.position, cx) + { let mouse_click = ClickEvent { down: mouse_down.clone(), up: event.clone(), @@ -889,8 +922,11 @@ impl Interactivity { cx.notify(); }); } else { + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if phase == DispatchPhase::Bubble + && interactive_bounds.visibly_contains(&event.position, cx) + { *pending_mouse_down.borrow_mut() = Some(event.clone()); cx.notify(); } @@ -901,13 +937,14 @@ impl Interactivity { if let Some(hover_listener) = self.hover_listener.take() { let was_hovered = element_state.hover_state.clone(); let has_mouse_down = element_state.pending_mouse_down.clone(); + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; } - let is_hovered = - bounds.contains_point(&event.position) && has_mouse_down.borrow().is_none(); + let is_hovered = interactive_bounds.visibly_contains(&event.position, cx) + && has_mouse_down.borrow().is_none(); let mut was_hovered = was_hovered.borrow_mut(); if is_hovered != was_hovered.clone() { @@ -922,14 +959,15 @@ impl Interactivity { if let Some(tooltip_builder) = self.tooltip_builder.take() { let active_tooltip = element_state.active_tooltip.clone(); let pending_mouse_down = element_state.pending_mouse_down.clone(); + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; } - let is_hovered = - bounds.contains_point(&event.position) && pending_mouse_down.borrow().is_none(); + let is_hovered = interactive_bounds.visibly_contains(&event.position, cx) + && pending_mouse_down.borrow().is_none(); if !is_hovered { active_tooltip.borrow_mut().take(); return; @@ -987,11 +1025,12 @@ impl Interactivity { .group_active_style .as_ref() .and_then(|group_active| GroupBounds::get(&group_active.group, cx)); + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |down: &MouseDownEvent, phase, cx| { if phase == DispatchPhase::Bubble { let group = active_group_bounds .map_or(false, |bounds| bounds.contains_point(&down.position)); - let element = bounds.contains_point(&down.position); + let element = interactive_bounds.visibly_contains(&down.position, cx); if group || element { *active_state.borrow_mut() = ElementClickedState { group, element }; cx.notify(); @@ -1008,9 +1047,12 @@ impl Interactivity { .clone(); let line_height = cx.line_height(); let scroll_max = (content_size - bounds.size).max(&Size::default()); + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if phase == DispatchPhase::Bubble + && interactive_bounds.visibly_contains(&event.position, cx) + { let mut scroll_offset = scroll_offset.borrow_mut(); let old_scroll_offset = *scroll_offset; let delta = event.delta.pixel_delta(line_height); diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index dc6b77c7c7b21db00cc3cb1b6ae23588e07d6c36..70a8df21e138e85c6d9cfbc3528fba22667f1405 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -114,7 +114,6 @@ impl Picker { } fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - dbg!("canceling!"); self.delegate.dismissed(cx); } diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index 6d28a6299b9f6cbef4231eda2f2bf5d087a3e9d4..a9b6189fdc313394061f21abb1449d6664f3daed 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -46,7 +46,6 @@ impl ModalLayer { previous_focus_handle: cx.focused(), focus_handle: cx.focus_handle(), }); - dbg!("focusing"); cx.focus_view(&new_modal); cx.notify(); } @@ -96,10 +95,6 @@ impl Render for ModalLayer { .track_focus(&active_modal.focus_handle) .child( h_stack() - // needed to prevent mouse events leaking to the - // UI below. // todo! for gpui3. - .on_any_mouse_down(|_, cx| cx.stop_propagation()) - .on_any_mouse_up(|_, cx| cx.stop_propagation()) .on_mouse_down_out(cx.listener(|this, _, cx| { this.hide_modal(cx); })) From 6c37393dd163ccfd501fcd12678d9e297cfc8959 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 28 Nov 2023 14:16:51 +0100 Subject: [PATCH 17/35] Add contact finder, change ui::List's on_click handler signature --- crates/collab_ui2/src/collab_panel.rs | 68 +++-- .../src/collab_panel/contact_finder.rs | 264 ++++++++---------- crates/ui2/src/components/avatar.rs | 6 + crates/ui2/src/components/context_menu.rs | 7 +- crates/ui2/src/components/list.rs | 46 +-- crates/ui2/src/components/slot.rs | 4 +- 6 files changed, 191 insertions(+), 204 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 0df51b02a2b7bea5246522b414d6550ba8f21743..2d8cd2cd3c9969aebf4301d111a5daffa3fdfb08 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -1,6 +1,6 @@ #![allow(unused)] // mod channel_modal; -// mod contact_finder; +mod contact_finder; // use crate::{ // channel_view::{self, ChannelView}, @@ -16,7 +16,7 @@ // proto::{self, PeerId}, // Client, Contact, User, UserStore, // }; -// use contact_finder::ContactFinder; +use contact_finder::ContactFinder; // use context_menu::{ContextMenu, ContextMenuItem}; // use db::kvp::KEY_VALUE_STORE; // use drag_and_drop::{DragAndDrop, Draggable}; @@ -166,7 +166,7 @@ use editor::Editor; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle, + actions, div, img, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, RenderOnce, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, }; @@ -2255,19 +2255,17 @@ impl CollabPanel { // .detach_and_log_err(cx); // } - // fn toggle_contact_finder(&mut self, cx: &mut ViewContext) { - // if let Some(workspace) = self.workspace.upgrade(cx) { - // workspace.update(cx, |workspace, cx| { - // workspace.toggle_modal(cx, |_, cx| { - // cx.add_view(|cx| { - // let mut finder = ContactFinder::new(self.user_store.clone(), cx); - // finder.set_query(self.filter_editor.read(cx).text(cx), cx); - // finder - // }) - // }); - // }); - // } - // } + fn toggle_contact_finder(&mut self, cx: &mut ViewContext) { + if let Some(workspace) = self.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(cx, |cx| { + let mut finder = ContactFinder::new(self.user_store.clone(), cx); + finder.set_query(self.filter_editor.read(cx).text(cx), cx); + finder + }); + }); + } + } // fn new_root_channel(&mut self, cx: &mut ViewContext) { // self.channel_editing_state = Some(ChannelEditingState::Create { @@ -2672,10 +2670,7 @@ impl CollabPanel { } Section::Contacts => Some( IconButton::new("add-contact", Icon::Plus) - .on_click(cx.listener(|this, _, cx| { - todo!() - // this.toggle_contact_finder(cx) - })) + .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) .tooltip(|cx| Tooltip::text("Search for new contact", cx)), ), Section::Channels => { @@ -2734,13 +2729,20 @@ impl CollabPanel { let busy = contact.busy || calling; let user_id = contact.user.id; let github_login = SharedString::from(contact.user.github_login.clone()); - - let item = ListItem::new(github_login.clone()) - .child(Label::new(github_login.clone())) - .on_click(cx.listener(|this, _, cx| { - todo!(); - })); - + let mut item = ListItem::new(github_login.clone()) + .on_click(cx.listener(move |this, _, cx| { + this.workspace + .update(cx, |this, cx| { + this.call_state() + .invite(user_id, None, cx) + .detach_and_log_err(cx) + }) + .log_err(); + })) + .child(Label::new(github_login.clone())); + if let Some(avatar) = contact.user.avatar.clone() { + //item = item.left_avatar(avatar); + } // let event_handler = // MouseEventHandler::new::(contact.user.id as usize, cx, |state, cx| { // Flex::row() @@ -2873,8 +2875,14 @@ impl CollabPanel { ) -> impl IntoElement { let github_login = SharedString::from(user.github_login.clone()); - let mut row = ListItem::new(github_login.clone()).child(Label::new(github_login.clone())); - + let mut item = ListItem::new(github_login.clone()) + .child(Label::new(github_login.clone())) + .on_click(cx.listener(|this, _, cx| { + todo!(); + })); + if let Some(avatar) = user.avatar.clone() { + item = item.left_avatar(avatar); + } // .with_children(user.avatar.clone().map(|avatar| { // Image::from_data(avatar) // .with_style(theme.contact_avatar) @@ -2963,7 +2971,7 @@ impl CollabPanel { // .style_for(&mut Default::default()), // ) // .into_any() - row + item } fn render_contact_placeholder( diff --git a/crates/collab_ui2/src/collab_panel/contact_finder.rs b/crates/collab_ui2/src/collab_panel/contact_finder.rs index d0c12a7f90a430a70615f6c4b91ca555619081fe..80872db729c6cff103046ca4e1fe8f7282a59b4a 100644 --- a/crates/collab_ui2/src/collab_panel/contact_finder.rs +++ b/crates/collab_ui2/src/collab_panel/contact_finder.rs @@ -1,37 +1,34 @@ use client::{ContactRequestStatus, User, UserStore}; use gpui::{ - elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle, + div, img, svg, AnyElement, AppContext, DismissEvent, Div, Entity, EventEmitter, FocusHandle, + FocusableView, Img, IntoElement, Model, ParentElement as _, Render, Styled, Task, View, + ViewContext, VisualContext, WeakView, }; -use picker::{Picker, PickerDelegate, PickerEvent}; +use picker::{Picker, PickerDelegate}; use std::sync::Arc; -use util::TryFutureExt; -use workspace::Modal; +use theme::ActiveTheme as _; +use ui::{h_stack, v_stack, Label}; +use util::{ResultExt as _, TryFutureExt}; pub fn init(cx: &mut AppContext) { - Picker::::init(cx); - cx.add_action(ContactFinder::dismiss) + //Picker::::init(cx); + //cx.add_action(ContactFinder::dismiss) } pub struct ContactFinder { - picker: ViewHandle>, + picker: View>, has_focus: bool, } impl ContactFinder { - pub fn new(user_store: ModelHandle, cx: &mut ViewContext) -> Self { - let picker = cx.add_view(|cx| { - Picker::new( - ContactFinderDelegate { - user_store, - potential_contacts: Arc::from([]), - selected_index: 0, - }, - cx, - ) - .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone()) - }); - - cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach(); + pub fn new(user_store: Model, cx: &mut ViewContext) -> Self { + let delegate = ContactFinderDelegate { + parent: cx.view().downgrade(), + user_store, + potential_contacts: Arc::from([]), + selected_index: 0, + }; + let picker = cx.build_view(|cx| Picker::new(delegate, cx)); Self { picker, @@ -41,105 +38,72 @@ impl ContactFinder { pub fn set_query(&mut self, query: String, cx: &mut ViewContext) { self.picker.update(cx, |picker, cx| { - picker.set_query(query, cx); + // todo!() + // picker.set_query(query, cx); }); } - - fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(PickerEvent::Dismiss); - } -} - -impl Entity for ContactFinder { - type Event = PickerEvent; } -impl View for ContactFinder { - fn ui_name() -> &'static str { - "ContactFinder" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let full_theme = &theme::current(cx); - let theme = &full_theme.collab_panel.tabbed_modal; - - fn render_mode_button( - text: &'static str, - theme: &theme::TabbedModal, - _cx: &mut ViewContext, - ) -> AnyElement { - let contained_text = &theme.tab_button.active_state().default; - Label::new(text, contained_text.text.clone()) - .contained() - .with_style(contained_text.container.clone()) - .into_any() +impl Render for ContactFinder { + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + fn render_mode_button(text: &'static str) -> AnyElement { + Label::new(text).into_any_element() } - Flex::column() - .with_child( - Flex::column() - .with_child( - Label::new("Contacts", theme.title.text.clone()) - .contained() - .with_style(theme.title.container.clone()), - ) - .with_child(Flex::row().with_children([render_mode_button( - "Invite new contacts", - &theme, - cx, - )])) - .expanded() - .contained() - .with_style(theme.header), - ) - .with_child( - ChildView::new(&self.picker, cx) - .contained() - .with_style(theme.body), + v_stack() + .child( + v_stack() + .child(Label::new("Contacts")) + .child(h_stack().children([render_mode_button("Invite new contacts")])) + .bg(cx.theme().colors().element_background), ) - .constrained() - .with_max_height(theme.max_height) - .with_max_width(theme.max_width) - .contained() - .with_style(theme.modal) - .into_any() + .child(self.picker.clone()) + .w_96() } - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - self.has_focus = true; - if cx.is_self_focused() { - cx.focus(&self.picker) - } - } + // fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + // self.has_focus = true; + // if cx.is_self_focused() { + // cx.focus(&self.picker) + // } + // } - fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { - self.has_focus = false; - } + // fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { + // self.has_focus = false; + // } + + type Element = Div; } -impl Modal for ContactFinder { - fn has_focus(&self) -> bool { - self.has_focus - } +// impl Modal for ContactFinder { +// fn has_focus(&self) -> bool { +// self.has_focus +// } - fn dismiss_on_event(event: &Self::Event) -> bool { - match event { - PickerEvent::Dismiss => true, - } - } -} +// fn dismiss_on_event(event: &Self::Event) -> bool { +// match event { +// PickerEvent::Dismiss => true, +// } +// } +// } pub struct ContactFinderDelegate { + parent: WeakView, potential_contacts: Arc<[Arc]>, - user_store: ModelHandle, + user_store: Model, selected_index: usize, } -impl PickerDelegate for ContactFinderDelegate { - fn placeholder_text(&self) -> Arc { - "Search collaborator by username...".into() +impl EventEmitter for ContactFinder {} + +impl FocusableView for ContactFinder { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) } +} +impl PickerDelegate for ContactFinderDelegate { + type ListItem = Div; fn match_count(&self) -> usize { self.potential_contacts.len() } @@ -152,6 +116,10 @@ impl PickerDelegate for ContactFinderDelegate { self.selected_index = ix; } + fn placeholder_text(&self) -> Arc { + "Search collaborator by username...".into() + } + fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { let search_users = self .user_store @@ -161,7 +129,7 @@ impl PickerDelegate for ContactFinderDelegate { async { let potential_contacts = search_users.await?; picker.update(&mut cx, |picker, cx| { - picker.delegate_mut().potential_contacts = potential_contacts.into(); + picker.delegate.potential_contacts = potential_contacts.into(); cx.notify(); })?; anyhow::Ok(()) @@ -191,19 +159,18 @@ impl PickerDelegate for ContactFinderDelegate { } fn dismissed(&mut self, cx: &mut ViewContext>) { - cx.emit(PickerEvent::Dismiss); + //cx.emit(PickerEvent::Dismiss); + self.parent + .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) + .log_err(); } fn render_match( &self, ix: usize, - mouse_state: &mut MouseState, selected: bool, - cx: &gpui::AppContext, - ) -> AnyElement> { - let full_theme = &theme::current(cx); - let theme = &full_theme.collab_panel.contact_finder; - let tabbed_modal = &full_theme.collab_panel.tabbed_modal; + cx: &mut ViewContext>, + ) -> Self::ListItem { let user = &self.potential_contacts[ix]; let request_status = self.user_store.read(cx).contact_request_status(user); @@ -214,48 +181,45 @@ impl PickerDelegate for ContactFinderDelegate { ContactRequestStatus::RequestSent => Some("icons/x.svg"), ContactRequestStatus::RequestAccepted => None, }; - let button_style = if self.user_store.read(cx).is_contact_request_pending(user) { - &theme.disabled_contact_button - } else { - &theme.contact_button - }; - let style = tabbed_modal - .picker - .item - .in_state(selected) - .style_for(mouse_state); - Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::from_data(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - })) - .with_child( - Label::new(user.github_login.clone(), style.label.clone()) - .contained() - .with_style(theme.contact_username) - .aligned() - .left(), - ) - .with_children(icon_path.map(|icon_path| { - Svg::new(icon_path) - .with_color(button_style.color) - .constrained() - .with_width(button_style.icon_width) - .aligned() - .contained() - .with_style(button_style.container) - .constrained() - .with_width(button_style.button_width) - .with_height(button_style.button_width) - .aligned() - .flex_float() - })) - .contained() - .with_style(style.container) - .constrained() - .with_height(tabbed_modal.row_height) - .into_any() + dbg!(icon_path); + div() + .flex_1() + .justify_between() + .children(user.avatar.clone().map(|avatar| img().data(avatar))) + .child(Label::new(user.github_login.clone())) + .children(icon_path.map(|icon_path| svg().path(icon_path))) + // Flex::row() + // .with_children(user.avatar.clone().map(|avatar| { + // Image::from_data(avatar) + // .with_style(theme.contact_avatar) + // .aligned() + // .left() + // })) + // .with_child( + // Label::new(user.github_login.clone(), style.label.clone()) + // .contained() + // .with_style(theme.contact_username) + // .aligned() + // .left(), + // ) + // .with_children(icon_path.map(|icon_path| { + // Svg::new(icon_path) + // .with_color(button_style.color) + // .constrained() + // .with_width(button_style.icon_width) + // .aligned() + // .contained() + // .with_style(button_style.container) + // .constrained() + // .with_width(button_style.button_width) + // .with_height(button_style.button_width) + // .aligned() + // .flex_float() + // })) + // .contained() + // .with_style(style.container) + // .constrained() + // .with_height(tabbed_modal.row_height) + // .into_any() } } diff --git a/crates/ui2/src/components/avatar.rs b/crates/ui2/src/components/avatar.rs index d358b221da9287f488bd68dd95bcff659191b5a5..57aa17ebbaa3c43c559d14803e3ed74a3a2fdc19 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/crates/ui2/src/components/avatar.rs @@ -49,6 +49,12 @@ impl Avatar { } } + pub fn source(src: ImageSource) -> Self { + Self { + src, + shape: Shape::Circle, + } + } pub fn shape(mut self, shape: Shape) -> Self { self.shape = shape; self diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index a92c08d82fae0164d0488e350f273df86f7ae48f..c9048418382ff8034a6013e27362f7f036186688 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -12,7 +12,10 @@ use gpui::{ pub enum ContextMenuItem { Separator, Header(SharedString), - Entry(SharedString, Rc), + Entry( + SharedString, + Rc, + ), } pub struct ContextMenu { @@ -58,7 +61,7 @@ impl ContextMenu { pub fn entry( mut self, label: impl Into, - on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static, + on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, ) -> Self { self.items .push(ContextMenuItem::Entry(label.into(), Rc::new(on_click))); diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index a674a5084c2ce6b8c7913fda97dbe11d2d40abb0..712e5d4c7b065e81084ba898e5f0a71b25836d38 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -1,5 +1,6 @@ use gpui::{ - div, px, AnyElement, ClickEvent, Div, IntoElement, Stateful, StatefulInteractiveElement, + div, px, AnyElement, ClickEvent, Div, ImageSource, IntoElement, MouseButton, MouseDownEvent, + Stateful, StatefulInteractiveElement, }; use smallvec::SmallVec; use std::rc::Rc; @@ -250,7 +251,7 @@ pub struct ListItem { size: ListEntrySize, toggle: Toggle, variant: ListItemVariant, - on_click: Option>, + on_click: Option>, children: SmallVec<[AnyElement; 2]>, } @@ -270,7 +271,10 @@ impl ListItem { } } - pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self { + pub fn on_click( + mut self, + handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + ) -> Self { self.on_click = Some(Rc::new(handler)); self } @@ -300,7 +304,7 @@ impl ListItem { self } - pub fn left_avatar(mut self, left_avatar: impl Into) -> Self { + pub fn left_avatar(mut self, left_avatar: impl Into) -> Self { self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into())); self } @@ -323,7 +327,7 @@ impl RenderOnce for ListItem { .color(Color::Muted), ), ), - Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::uri(src))), + Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::source(src))), Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::uri(src))), None => None, }; @@ -335,25 +339,18 @@ impl RenderOnce for ListItem { div() .id(self.id) .relative() - .hover(|mut style| { - style.background = Some(cx.theme().colors().editor_background.into()); - style - }) - .on_click({ - let on_click = self.on_click.clone(); - move |event, cx| { - if let Some(on_click) = &on_click { - (on_click)(event, cx) - } - } - }) + .bg(cx.theme().colors().editor_background.clone()) + // .hover(|mut style| { + // style.background = Some(cx.theme().colors().editor_background.into()); + // style + // }) // TODO: Add focus state // .when(self.state == InteractionState::Focused, |this| { // this.border() // .border_color(cx.theme().colors().border_focused) // }) - .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) - .active(|style| style.bg(cx.theme().colors().ghost_element_active)) + //.hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) + //.active(|style| style.bg(cx.theme().colors().ghost_element_active)) .child( sized_item .when(self.variant == ListItemVariant::Inset, |this| this.px_2()) @@ -377,7 +374,16 @@ impl RenderOnce for ListItem { .relative() .child(disclosure_control(self.toggle)) .children(left_content) - .children(self.children), + .children(self.children) + .on_mouse_down(MouseButton::Left, { + let on_click = self.on_click.clone(); + move |event, cx| { + dbg!("Clicking!"); + if let Some(on_click) = &on_click { + (on_click)(event, cx) + } + } + }), ) } } diff --git a/crates/ui2/src/components/slot.rs b/crates/ui2/src/components/slot.rs index a672694dc5781c6a474211d621a71ee640e7dcaa..7c896bf85b23d604a41aec259de125f06c059514 100644 --- a/crates/ui2/src/components/slot.rs +++ b/crates/ui2/src/components/slot.rs @@ -1,4 +1,4 @@ -use gpui::SharedString; +use gpui::{ImageSource, SharedString}; use crate::Icon; @@ -9,6 +9,6 @@ use crate::Icon; /// Can be filled with a [] pub enum GraphicSlot { Icon(Icon), - Avatar(SharedString), + Avatar(ImageSource), PublicActor(SharedString), } From 9fb3cb6a697c96c09afae83fb570fc0d97057baa Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 28 Nov 2023 14:37:53 +0100 Subject: [PATCH 18/35] fixup! Add contact finder, change ui::List's on_click handler signature --- crates/ui2/src/components/context_menu.rs | 6 +++--- crates/ui2/src/components/list.rs | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index c9048418382ff8034a6013e27362f7f036186688..fda446a78b645de6f7237f3d74052fd72ab7a929 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -4,9 +4,9 @@ use std::rc::Rc; use crate::{prelude::*, v_stack, Label, List}; use crate::{ListItem, ListSeparator, ListSubHeader}; use gpui::{ - overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DismissEvent, - DispatchPhase, Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, - ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext, + overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, DismissEvent, DispatchPhase, + Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, ManagedView, MouseButton, + MouseDownEvent, Pixels, Point, Render, View, VisualContext, }; pub enum ContextMenuItem { diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 712e5d4c7b065e81084ba898e5f0a71b25836d38..ecdd32d168f2c4df4ba4d8f986bcd2409e191f8b 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -1,6 +1,5 @@ use gpui::{ - div, px, AnyElement, ClickEvent, Div, ImageSource, IntoElement, MouseButton, MouseDownEvent, - Stateful, StatefulInteractiveElement, + div, px, AnyElement, Div, ImageSource, IntoElement, MouseButton, MouseDownEvent, Stateful, }; use smallvec::SmallVec; use std::rc::Rc; @@ -378,7 +377,6 @@ impl RenderOnce for ListItem { .on_mouse_down(MouseButton::Left, { let on_click = self.on_click.clone(); move |event, cx| { - dbg!("Clicking!"); if let Some(on_click) = &on_click { (on_click)(event, cx) } From 63bd4ac999c3b3abf1f201e23b101294d32a35e0 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 28 Nov 2023 10:33:13 -0500 Subject: [PATCH 19/35] Allow `render_match` to return an `Option` to represent no matches --- .../command_palette2/src/command_palette.rs | 42 ++++++++++--------- crates/file_finder2/src/file_finder.rs | 30 ++++++------- crates/picker2/src/picker2.rs | 4 +- crates/storybook2/src/stories/picker.rs | 28 +++++++------ 4 files changed, 55 insertions(+), 49 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 07b819d3a10c054ea20cddac787d28e792d3b187..73dca81b9327c25ec4b2755c50516c40b9f93362 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -294,32 +294,34 @@ impl PickerDelegate for CommandPaletteDelegate { ix: usize, selected: bool, cx: &mut ViewContext>, - ) -> Self::ListItem { + ) -> Option { let colors = cx.theme().colors(); let Some(r#match) = self.matches.get(ix) else { - return div(); + return None; }; let Some(command) = self.commands.get(r#match.candidate_id) else { - return div(); + return None; }; - div() - .px_1() - .text_color(colors.text) - .text_ui() - .bg(colors.ghost_element_background) - .rounded_md() - .when(selected, |this| this.bg(colors.ghost_element_selected)) - .hover(|this| this.bg(colors.ghost_element_hover)) - .child( - h_stack() - .justify_between() - .child(HighlightedLabel::new( - command.name.clone(), - r#match.positions.clone(), - )) - .children(KeyBinding::for_action(&*command.action, cx)), - ) + Some( + div() + .px_1() + .text_color(colors.text) + .text_ui() + .bg(colors.ghost_element_background) + .rounded_md() + .when(selected, |this| this.bg(colors.ghost_element_selected)) + .hover(|this| this.bg(colors.ghost_element_hover)) + .child( + h_stack() + .justify_between() + .child(HighlightedLabel::new( + command.name.clone(), + r#match.positions.clone(), + )) + .children(KeyBinding::for_action(&*command.action, cx)), + ), + ) } } diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index ea578fbb0ebb5703f3f31ed142f5589a0345c814..c7d8e9eae0c1ed3d3c721af8e5a4dfb706637ddb 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -711,7 +711,7 @@ impl PickerDelegate for FileFinderDelegate { ix: usize, selected: bool, cx: &mut ViewContext>, - ) -> Self::ListItem { + ) -> Option { let path_match = self .matches .get(ix) @@ -722,19 +722,21 @@ impl PickerDelegate for FileFinderDelegate { let (file_name, file_name_positions, full_path, full_path_positions) = self.labels_for_match(path_match, cx, ix); - div() - .px_1() - .text_color(colors.text) - .text_ui() - .bg(colors.ghost_element_background) - .rounded_md() - .when(selected, |this| this.bg(colors.ghost_element_selected)) - .hover(|this| this.bg(colors.ghost_element_hover)) - .child( - v_stack() - .child(HighlightedLabel::new(file_name, file_name_positions)) - .child(HighlightedLabel::new(full_path, full_path_positions)), - ) + Some( + div() + .px_1() + .text_color(colors.text) + .text_ui() + .bg(colors.ghost_element_background) + .rounded_md() + .when(selected, |this| this.bg(colors.ghost_element_selected)) + .hover(|this| this.bg(colors.ghost_element_hover)) + .child( + v_stack() + .child(HighlightedLabel::new(file_name, file_name_positions)) + .child(HighlightedLabel::new(full_path, full_path_positions)), + ), + ) } } diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index dc6b77c7c7b21db00cc3cb1b6ae23588e07d6c36..ea31419201748ade52647504026d3d23e5ac27ea 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -32,7 +32,7 @@ pub trait PickerDelegate: Sized + 'static { ix: usize, selected: bool, cx: &mut ViewContext>, - ) -> Self::ListItem; + ) -> Option; } impl FocusableView for Picker { @@ -230,7 +230,7 @@ impl Render for Picker { ) }), ) - .child(picker.delegate.render_match( + .children(picker.delegate.render_match( ix, ix == selected_index, cx, diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index ae6a26161bd3a587c54aa7231336d40428dbbe69..f66fe7a93af28ddf5ba5274594fc03597299d915 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -51,25 +51,27 @@ impl PickerDelegate for Delegate { ix: usize, selected: bool, cx: &mut gpui::ViewContext>, - ) -> Self::ListItem { + ) -> Option { let colors = cx.theme().colors(); let Some(candidate_ix) = self.matches.get(ix) else { - return div(); + return None; }; // TASK: Make StringMatchCandidate::string a SharedString let candidate = SharedString::from(self.candidates[*candidate_ix].string.clone()); - div() - .text_color(colors.text) - .when(selected, |s| { - s.border_l_10().border_color(colors.terminal_ansi_yellow) - }) - .hover(|style| { - style - .bg(colors.element_active) - .text_color(colors.text_accent) - }) - .child(candidate) + Some( + div() + .text_color(colors.text) + .when(selected, |s| { + s.border_l_10().border_color(colors.terminal_ansi_yellow) + }) + .hover(|style| { + style + .bg(colors.element_active) + .text_color(colors.text_accent) + }) + .child(candidate), + ) } fn selected_index(&self) -> usize { From 1ee109cec723ae8e8816eae8961503fb052db0b3 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 28 Nov 2023 10:44:57 -0500 Subject: [PATCH 20/35] Use `ListItem` when rendering picker matches --- .../command_palette2/src/command_palette.rs | 37 +++++++------------ crates/file_finder2/src/file_finder.rs | 30 +++++---------- crates/ui2/src/components/list.rs | 30 ++++++--------- 3 files changed, 33 insertions(+), 64 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 73dca81b9327c25ec4b2755c50516c40b9f93362..5f3fb4a98595afc3b6930370bd552f0912af6344 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -1,17 +1,15 @@ use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, div, prelude::*, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, - FocusableView, Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, - WeakView, + actions, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, + Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use std::{ cmp::{self, Reverse}, sync::Arc, }; -use theme::ActiveTheme; -use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt}; +use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, @@ -141,7 +139,7 @@ impl CommandPaletteDelegate { } impl PickerDelegate for CommandPaletteDelegate { - type ListItem = Div; + type ListItem = ListItem; fn placeholder_text(&self) -> Arc { "Execute a command...".into() @@ -295,7 +293,6 @@ impl PickerDelegate for CommandPaletteDelegate { selected: bool, cx: &mut ViewContext>, ) -> Option { - let colors = cx.theme().colors(); let Some(r#match) = self.matches.get(ix) else { return None; }; @@ -304,23 +301,15 @@ impl PickerDelegate for CommandPaletteDelegate { }; Some( - div() - .px_1() - .text_color(colors.text) - .text_ui() - .bg(colors.ghost_element_background) - .rounded_md() - .when(selected, |this| this.bg(colors.ghost_element_selected)) - .hover(|this| this.bg(colors.ghost_element_hover)) - .child( - h_stack() - .justify_between() - .child(HighlightedLabel::new( - command.name.clone(), - r#match.positions.clone(), - )) - .children(KeyBinding::for_action(&*command.action, cx)), - ), + ListItem::new(ix).selected(selected).child( + h_stack() + .justify_between() + .child(HighlightedLabel::new( + command.name.clone(), + r#match.positions.clone(), + )) + .children(KeyBinding::for_action(&*command.action, cx)), + ), ) } } diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index c7d8e9eae0c1ed3d3c721af8e5a4dfb706637ddb..b54d28a400750b4caffbec99d163f561cf7f7784 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -2,9 +2,8 @@ use collections::HashMap; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ - actions, div, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, - InteractiveElement, IntoElement, Model, ParentElement, Render, Styled, Task, View, ViewContext, - VisualContext, WeakView, + actions, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Model, + ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; @@ -16,8 +15,7 @@ use std::{ }, }; use text::Point; -use theme::ActiveTheme; -use ui::{v_stack, HighlightedLabel, StyledExt}; +use ui::{v_stack, HighlightedLabel, ListItem}; use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; use workspace::Workspace; @@ -530,7 +528,7 @@ impl FileFinderDelegate { } impl PickerDelegate for FileFinderDelegate { - type ListItem = Div; + type ListItem = ListItem; fn placeholder_text(&self) -> Arc { "Search project files...".into() @@ -716,26 +714,16 @@ impl PickerDelegate for FileFinderDelegate { .matches .get(ix) .expect("Invalid matches state: no element for index {ix}"); - let theme = cx.theme(); - let colors = theme.colors(); let (file_name, file_name_positions, full_path, full_path_positions) = self.labels_for_match(path_match, cx, ix); Some( - div() - .px_1() - .text_color(colors.text) - .text_ui() - .bg(colors.ghost_element_background) - .rounded_md() - .when(selected, |this| this.bg(colors.ghost_element_selected)) - .hover(|this| this.bg(colors.ghost_element_hover)) - .child( - v_stack() - .child(HighlightedLabel::new(file_name, file_name_positions)) - .child(HighlightedLabel::new(full_path, full_path_positions)), - ), + ListItem::new(ix).selected(selected).child( + v_stack() + .child(HighlightedLabel::new(file_name, file_name_positions)) + .child(HighlightedLabel::new(full_path, full_path_positions)), + ), ) } } diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 875ab6d97e2032ae8c6be9bb9e0445d99897d099..642903f09b39ddbbe0ca0c71ec9b43b8bd9f93da 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -231,23 +231,16 @@ impl RenderOnce for ListSubHeader { } } -#[derive(Default, PartialEq, Copy, Clone)] -pub enum ListEntrySize { - #[default] - Small, - Medium, -} - #[derive(IntoElement)] pub struct ListItem { id: ElementId, disabled: bool, + selected: bool, // TODO: Reintroduce this // disclosure_control_style: DisclosureControlVisibility, indent_level: u32, left_slot: Option, overflow: OverflowStyle, - size: ListEntrySize, toggle: Toggle, variant: ListItemVariant, on_click: Option>, @@ -259,10 +252,10 @@ impl ListItem { Self { id: id.into(), disabled: false, + selected: false, indent_level: 0, left_slot: None, overflow: OverflowStyle::Hidden, - size: ListEntrySize::default(), toggle: Toggle::NotToggleable, variant: ListItemVariant::default(), on_click: Default::default(), @@ -290,6 +283,11 @@ impl ListItem { self } + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } + pub fn left_content(mut self, left_content: GraphicSlot) -> Self { self.left_slot = Some(left_content); self @@ -304,11 +302,6 @@ impl ListItem { self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into())); self } - - pub fn size(mut self, size: ListEntrySize) -> Self { - self.size = size; - self - } } impl RenderOnce for ListItem { @@ -328,10 +321,6 @@ impl RenderOnce for ListItem { None => None, }; - let sized_item = match self.size { - ListEntrySize::Small => div().h_6(), - ListEntrySize::Medium => div().h_7(), - }; div() .id(self.id) .relative() @@ -354,8 +343,11 @@ impl RenderOnce for ListItem { // }) .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) .active(|style| style.bg(cx.theme().colors().ghost_element_active)) + .when(self.selected, |this| { + this.bg(cx.theme().colors().ghost_element_selected) + }) .child( - sized_item + div() .when(self.variant == ListItemVariant::Inset, |this| this.px_2()) // .ml(rems(0.75 * self.indent_level as f32)) .children((0..self.indent_level).map(|_| { From ecb3bd7f5943d2bd00e9dfd3f64843399f2749d9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 28 Nov 2023 10:52:17 -0500 Subject: [PATCH 21/35] Use `ListItem`s in `Picker` story --- crates/storybook2/src/stories/picker.rs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index f66fe7a93af28ddf5ba5274594fc03597299d915..8bcfb8923dbaef403ec65916821ae2341367f136 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -5,6 +5,7 @@ use gpui::{ use picker::{Picker, PickerDelegate}; use std::sync::Arc; use theme2::ActiveTheme; +use ui::{Label, ListItem}; pub struct PickerStory { picker: View>, @@ -36,7 +37,7 @@ impl Delegate { } impl PickerDelegate for Delegate { - type ListItem = Div; + type ListItem = ListItem; fn match_count(&self) -> usize { self.candidates.len() @@ -52,7 +53,6 @@ impl PickerDelegate for Delegate { selected: bool, cx: &mut gpui::ViewContext>, ) -> Option { - let colors = cx.theme().colors(); let Some(candidate_ix) = self.matches.get(ix) else { return None; }; @@ -60,17 +60,9 @@ impl PickerDelegate for Delegate { let candidate = SharedString::from(self.candidates[*candidate_ix].string.clone()); Some( - div() - .text_color(colors.text) - .when(selected, |s| { - s.border_l_10().border_color(colors.terminal_ansi_yellow) - }) - .hover(|style| { - style - .bg(colors.element_active) - .text_color(colors.text_accent) - }) - .child(candidate), + ListItem::new(ix) + .selected(selected) + .child(Label::new(candidate)), ) } From 5451db9c96acefe842654e23b33230855cd0eb48 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 28 Nov 2023 18:39:03 +0200 Subject: [PATCH 22/35] Use proper npm arguments and clean its inherited env vars --- crates/node_runtime/src/node_runtime.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 2621c58120af8ae3e711637af7a941dcc24af150..ecabdeb71817af4120cd3f9118c66adc85bd91b3 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -73,6 +73,7 @@ impl RealNodeRuntime { let npm_file = node_dir.join("bin/npm"); let result = Command::new(&node_binary) + .env_clear() .arg(npm_file) .arg("--version") .stdin(Stdio::null()) @@ -149,6 +150,7 @@ impl NodeRuntime for RealNodeRuntime { } let mut command = Command::new(node_binary); + command.env_clear(); command.env("PATH", env_path); command.arg(npm_file).arg(subcommand); command.args(["--cache".into(), installation_path.join("cache")]); @@ -200,11 +202,11 @@ impl NodeRuntime for RealNodeRuntime { &[ name, "--json", - "-fetch-retry-mintimeout", + "--fetch-retry-mintimeout", "2000", - "-fetch-retry-maxtimeout", + "--fetch-retry-maxtimeout", "5000", - "-fetch-timeout", + "--fetch-timeout", "5000", ], ) @@ -229,11 +231,11 @@ impl NodeRuntime for RealNodeRuntime { let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect(); arguments.extend_from_slice(&[ - "-fetch-retry-mintimeout", + "--fetch-retry-mintimeout", "2000", - "-fetch-retry-maxtimeout", + "--fetch-retry-maxtimeout", "5000", - "-fetch-timeout", + "--fetch-timeout", "5000", ]); From a761e6ca0ea187061dee36e77ce61ac740c4f862 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 28 Nov 2023 09:59:58 -0700 Subject: [PATCH 23/35] More mouse occlusion work --- crates/editor2/src/element.rs | 73 +++++++++++++++++++++++------ crates/editor2/src/hover_popover.rs | 3 -- crates/gpui2/src/elements/div.rs | 6 +-- crates/gpui2/src/window.rs | 12 ++--- crates/workspace2/src/workspace2.rs | 11 ++--- 5 files changed, 73 insertions(+), 32 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 6a6afd5461b6abb1b2944635216b5fd8ff5270a2..74ca292ecf8b712588ab0f1713c2a93d9619d92d 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -22,10 +22,11 @@ use collections::{BTreeMap, HashMap}; use gpui::{ div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, - ElementInputHandler, Entity, EntityId, Hsla, InteractiveElement, IntoElement, LineLayout, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce, - ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, - TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine, + ElementInputHandler, Entity, EntityId, Hsla, InteractiveBounds, InteractiveElement, + IntoElement, LineLayout, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, + ParentElement, Pixels, RenderOnce, ScrollWheelEvent, ShapedLine, SharedString, Size, + StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, View, + ViewContext, WeakView, WindowContext, WrappedLine, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -316,6 +317,7 @@ impl EditorElement { position_map: &PositionMap, text_bounds: Bounds, gutter_bounds: Bounds, + stacking_order: &StackingOrder, cx: &mut ViewContext, ) -> bool { let mut click_count = event.click_count; @@ -326,6 +328,9 @@ impl EditorElement { } else if !text_bounds.contains_point(&event.position) { return false; } + if !cx.was_top_layer(&event.position, stacking_order) { + return false; + } let point_for_position = position_map.point_for_position(text_bounds, event.position); let position = point_for_position.previous_valid; @@ -384,6 +389,7 @@ impl EditorElement { event: &MouseUpEvent, position_map: &PositionMap, text_bounds: Bounds, + stacking_order: &StackingOrder, cx: &mut ViewContext, ) -> bool { let end_selection = editor.has_pending_selection(); @@ -396,6 +402,7 @@ impl EditorElement { if !pending_nonempty_selections && event.modifiers.command && text_bounds.contains_point(&event.position) + && cx.was_top_layer(&event.position, stacking_order) { let point = position_map.point_for_position(text_bounds, event.position); let could_be_inlay = point.as_valid().is_none(); @@ -418,6 +425,7 @@ impl EditorElement { position_map: &PositionMap, text_bounds: Bounds, gutter_bounds: Bounds, + stacking_order: &StackingOrder, cx: &mut ViewContext, ) -> bool { let modifiers = event.modifiers; @@ -457,10 +465,12 @@ impl EditorElement { let text_hovered = text_bounds.contains_point(&event.position); let gutter_hovered = gutter_bounds.contains_point(&event.position); + let was_top = cx.was_top_layer(&event.position, stacking_order); + editor.set_gutter_hovered(gutter_hovered, cx); // Don't trigger hover popover if mouse is hovering over context menu - if text_hovered { + if text_hovered && was_top { let point_for_position = position_map.point_for_position(text_bounds, event.position); match point_for_position.as_valid() { @@ -490,7 +500,7 @@ impl EditorElement { } else { update_go_to_definition_link(editor, None, modifiers.command, modifiers.shift, cx); hover_at(editor, None, cx); - gutter_hovered + gutter_hovered && was_top } } @@ -498,10 +508,10 @@ impl EditorElement { editor: &mut Editor, event: &ScrollWheelEvent, position_map: &PositionMap, - bounds: Bounds, + bounds: &InteractiveBounds, cx: &mut ViewContext, ) -> bool { - if !bounds.contains_point(&event.position) { + if !bounds.visibly_contains(&event.position, cx) { return false; } @@ -2282,10 +2292,15 @@ impl EditorElement { cx: &mut WindowContext, ) { let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); + let interactive_bounds = InteractiveBounds { + bounds: bounds.intersect(&cx.content_mask().bounds), + stacking_order: cx.stacking_order().clone(), + }; cx.on_mouse_event({ let position_map = layout.position_map.clone(); let editor = self.editor.clone(); + let interactive_bounds = interactive_bounds.clone(); move |event: &ScrollWheelEvent, phase, cx| { if phase != DispatchPhase::Bubble { @@ -2293,7 +2308,7 @@ impl EditorElement { } let should_cancel = editor.update(cx, |editor, cx| { - Self::scroll(editor, event, &position_map, bounds, cx) + Self::scroll(editor, event, &position_map, &interactive_bounds, cx) }); if should_cancel { cx.stop_propagation(); @@ -2304,6 +2319,7 @@ impl EditorElement { cx.on_mouse_event({ let position_map = layout.position_map.clone(); let editor = self.editor.clone(); + let stacking_order = cx.stacking_order().clone(); move |event: &MouseDownEvent, phase, cx| { if phase != DispatchPhase::Bubble { @@ -2311,7 +2327,15 @@ impl EditorElement { } let should_cancel = editor.update(cx, |editor, cx| { - Self::mouse_down(editor, event, &position_map, text_bounds, gutter_bounds, cx) + Self::mouse_down( + editor, + event, + &position_map, + text_bounds, + gutter_bounds, + &stacking_order, + cx, + ) }); if should_cancel { @@ -2323,9 +2347,18 @@ impl EditorElement { cx.on_mouse_event({ let position_map = layout.position_map.clone(); let editor = self.editor.clone(); + let stacking_order = cx.stacking_order().clone(); + move |event: &MouseUpEvent, phase, cx| { let should_cancel = editor.update(cx, |editor, cx| { - Self::mouse_up(editor, event, &position_map, text_bounds, cx) + Self::mouse_up( + editor, + event, + &position_map, + text_bounds, + &stacking_order, + cx, + ) }); if should_cancel { @@ -2351,13 +2384,23 @@ impl EditorElement { cx.on_mouse_event({ let position_map = layout.position_map.clone(); let editor = self.editor.clone(); + let stacking_order = cx.stacking_order().clone(); + move |event: &MouseMoveEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; } let stop_propogating = editor.update(cx, |editor, cx| { - Self::mouse_moved(editor, event, &position_map, text_bounds, gutter_bounds, cx) + Self::mouse_moved( + editor, + event, + &position_map, + text_bounds, + gutter_bounds, + &stacking_order, + cx, + ) }); if stop_propogating { @@ -2617,9 +2660,11 @@ impl Element for EditorElement { // We call with_z_index to establish a new stacking context. cx.with_z_index(0, |cx| { cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - // Paint mouse listeners first, so any elements we paint on top of the editor + // Paint mouse listeners at z-index 0 so any elements we paint on top of the editor // take precedence. - self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx); + cx.with_z_index(0, |cx| { + self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx); + }); let input_handler = ElementInputHandler::new(bounds, self.editor.clone(), cx); cx.handle_input(&focus_handle, input_handler); diff --git a/crates/editor2/src/hover_popover.rs b/crates/editor2/src/hover_popover.rs index 37c7df650b126c859c28339a09077a96f4844bf2..f80168ed250d3f3bdcbedb7ad1d61e9e173f15ef 100644 --- a/crates/editor2/src/hover_popover.rs +++ b/crates/editor2/src/hover_popover.rs @@ -483,9 +483,6 @@ impl InfoPopover { // Prevent a mouse move on the popover from being propagated to the editor, // because that would dismiss the popover. .on_mouse_move(|_, cx| cx.stop_propagation()) - // Prevent a mouse down on the popover from being propagated to the editor, - // because that would move the cursor. - .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) .child(crate::render_parsed_markdown( "content", &self.parsed_content, diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 6185cd644364794eec85f6218c62c7e3643f5cb8..635e8b634f0f6982fffa2d35ca46da5bfb736e90 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -723,12 +723,12 @@ pub struct Interactivity { #[derive(Clone)] pub struct InteractiveBounds { - bounds: Bounds, - stacking_order: StackingOrder, + pub bounds: Bounds, + pub stacking_order: StackingOrder, } impl InteractiveBounds { - fn visibly_contains(&self, point: &Point, cx: &WindowContext) -> bool { + pub fn visibly_contains(&self, point: &Point, cx: &WindowContext) -> bool { self.bounds.contains_point(point) && cx.was_top_layer(&point, &self.stacking_order) } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 0d444d762ecd80c7846aa71c062dcfeb755e5a7d..6f342f70654653808d6b9797c898682ba4c532e8 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -12,7 +12,7 @@ use crate::{ VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Context as _, Result}; -use collections::{BTreeMap, HashMap}; +use collections::HashMap; use derive_more::{Deref, DerefMut}; use futures::{ channel::{mpsc, oneshot}, @@ -243,7 +243,7 @@ pub(crate) struct Frame { pub(crate) dispatch_tree: DispatchTree, pub(crate) focus_listeners: Vec, pub(crate) scene_builder: SceneBuilder, - pub(crate) depth_map: BTreeMap>, + pub(crate) depth_map: Vec<(StackingOrder, Bounds)>, pub(crate) z_index_stack: StackingOrder, content_mask_stack: Vec>, element_offset_stack: Vec>, @@ -811,10 +811,10 @@ impl<'a> WindowContext<'a> { /// Called during painting to track which z-index is on top at each pixel position pub fn add_opaque_layer(&mut self, bounds: Bounds) { let stacking_order = self.window.current_frame.z_index_stack.clone(); - self.window - .current_frame - .depth_map - .insert(stacking_order, bounds); + let depth_map = &mut self.window.current_frame.depth_map; + match depth_map.binary_search_by(|(level, _)| stacking_order.cmp(&level)) { + Ok(i) | Err(i) => depth_map.insert(i, (stacking_order, bounds)), + } } /// Returns true if the top-most opaque layer painted over this point was part of the diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 50f8611c4cc645c414ad68c030867ab4b8f07dab..6fa722f7757615ad01acdddfc8d527f457f94411 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -2640,12 +2640,11 @@ impl Workspace { .flex_col() .justify_end() .gap_2() - .children(self.notifications.iter().map(|(_, _, notification)| { - div() - .on_any_mouse_down(|_, cx| cx.stop_propagation()) - .on_any_mouse_up(|_, cx| cx.stop_propagation()) - .child(notification.to_any()) - })), + .children( + self.notifications + .iter() + .map(|(_, _, notification)| notification.to_any()), + ), ) } } From 94118987206bbfb5da747034faaa5839dff0e51a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 28 Nov 2023 13:11:43 -0500 Subject: [PATCH 24/35] Use `ListItem`s in the project panel (#3421) This PR reworks the project panel to render its items using the `ListItem` component. There are a few hacks in here in order to get click handlers working for the `ListItem`, but we'll want to get these fixed in GPUI. Release Notes: - N/A --- crates/project_panel2/src/project_panel.rs | 66 +++----- crates/ui2/src/components/list.rs | 142 +++++++----------- .../ui2/src/components/stories/list_item.rs | 8 + 3 files changed, 84 insertions(+), 132 deletions(-) diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index b0272098702f7006649850c93f57148e557e6115..d4c63e75bf68057bffa8f616f47fa8488e28bedf 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -10,9 +10,8 @@ use anyhow::{anyhow, Result}; use gpui::{ actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, - IntoElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, - Render, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View, - ViewContext, VisualContext as _, WeakView, WindowContext, + Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, Stateful, Styled, + Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ @@ -30,7 +29,7 @@ use std::{ sync::Arc, }; use theme::ActiveTheme as _; -use ui::{h_stack, v_stack, IconElement, Label}; +use ui::{v_stack, IconElement, Label, ListItem}; use unicase::UniCase; use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ @@ -1335,13 +1334,19 @@ impl ProjectPanel { } } - fn render_entry_visual_element( - details: &EntryDetails, - editor: Option<&View>, - padding: Pixels, + fn render_entry( + &self, + entry_id: ProjectEntryId, + details: EntryDetails, + // dragged_entry_destination: &mut Option>, cx: &mut ViewContext, - ) -> Div { + ) -> ListItem { + let kind = details.kind; + let settings = ProjectPanelSettings::get_global(cx); let show_editor = details.is_editing && !details.is_processing; + let is_selected = self + .selection + .map_or(false, |selection| selection.entry_id == entry_id); let theme = cx.theme(); let filename_text_color = details @@ -1354,14 +1359,17 @@ impl ProjectPanel { }) .unwrap_or(theme.status().info); - h_stack() + ListItem::new(entry_id.to_proto() as usize) + .indent_level(details.depth) + .indent_step_size(px(settings.indent_size)) + .selected(is_selected) .child(if let Some(icon) = &details.icon { div().child(IconElement::from_path(icon.to_string())) } else { div() }) .child( - if let (Some(editor), true) = (editor, show_editor) { + if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) { div().w_full().child(editor.clone()) } else { div() @@ -1370,33 +1378,6 @@ impl ProjectPanel { } .ml_1(), ) - .pl(padding) - } - - fn render_entry( - &self, - entry_id: ProjectEntryId, - details: EntryDetails, - // dragged_entry_destination: &mut Option>, - cx: &mut ViewContext, - ) -> Stateful
{ - let kind = details.kind; - let settings = ProjectPanelSettings::get_global(cx); - const INDENT_SIZE: Pixels = px(16.0); - let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size); - let show_editor = details.is_editing && !details.is_processing; - let is_selected = self - .selection - .map_or(false, |selection| selection.entry_id == entry_id); - - Self::render_entry_visual_element(&details, Some(&self.filename_editor), padding, cx) - .id(entry_id.to_proto() as usize) - .w_full() - .cursor_pointer() - .when(is_selected, |this| { - this.bg(cx.theme().colors().element_selected) - }) - .hover(|style| style.bg(cx.theme().colors().element_hover)) .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| { if !show_editor { if kind.is_dir() { @@ -1410,12 +1391,9 @@ impl ProjectPanel { } } })) - .on_mouse_down( - MouseButton::Right, - cx.listener(move |this, event: &MouseDownEvent, cx| { - this.deploy_context_menu(event.position, entry_id, cx); - }), - ) + .on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| { + this.deploy_context_menu(event.position, entry_id, cx); + })) // .on_drop::(|this, event, cx| { // this.move_entry( // *dragged_entry, diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 642903f09b39ddbbe0ca0c71ec9b43b8bd9f93da..6683c9a0cff0f6640f1f837b36fbe03ad76fa176 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -1,8 +1,10 @@ +use std::rc::Rc; + use gpui::{ - div, px, AnyElement, ClickEvent, Div, IntoElement, Stateful, StatefulInteractiveElement, + div, px, AnyElement, ClickEvent, Div, IntoElement, MouseButton, MouseDownEvent, Pixels, + Stateful, StatefulInteractiveElement, }; use smallvec::SmallVec; -use std::rc::Rc; use crate::{ disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle, @@ -117,66 +119,6 @@ impl ListHeader { self.meta = meta; self } - - // before_ship!("delete") - // fn render(self, cx: &mut WindowContext) -> impl Element { - // let disclosure_control = disclosure_control(self.toggle); - - // let meta = match self.meta { - // Some(ListHeaderMeta::Tools(icons)) => div().child( - // h_stack() - // .gap_2() - // .items_center() - // .children(icons.into_iter().map(|i| { - // IconElement::new(i) - // .color(TextColor::Muted) - // .size(IconSize::Small) - // })), - // ), - // Some(ListHeaderMeta::Button(label)) => div().child(label), - // Some(ListHeaderMeta::Text(label)) => div().child(label), - // None => div(), - // }; - - // h_stack() - // .w_full() - // .bg(cx.theme().colors().surface_background) - // // TODO: Add focus state - // // .when(self.state == InteractionState::Focused, |this| { - // // this.border() - // // .border_color(cx.theme().colors().border_focused) - // // }) - // .relative() - // .child( - // div() - // .h_5() - // .when(self.variant == ListItemVariant::Inset, |this| this.px_2()) - // .flex() - // .flex_1() - // .items_center() - // .justify_between() - // .w_full() - // .gap_1() - // .child( - // h_stack() - // .gap_1() - // .child( - // div() - // .flex() - // .gap_1() - // .items_center() - // .children(self.left_icon.map(|i| { - // IconElement::new(i) - // .color(TextColor::Muted) - // .size(IconSize::Small) - // })) - // .child(Label::new(self.label.clone()).color(TextColor::Muted)), - // ) - // .child(disclosure_control), - // ) - // .child(meta), - // ) - // } } #[derive(IntoElement, Clone)] @@ -238,12 +180,14 @@ pub struct ListItem { selected: bool, // TODO: Reintroduce this // disclosure_control_style: DisclosureControlVisibility, - indent_level: u32, + indent_level: usize, + indent_step_size: Pixels, left_slot: Option, overflow: OverflowStyle, toggle: Toggle, variant: ListItemVariant, on_click: Option>, + on_secondary_mouse_down: Option>, children: SmallVec<[AnyElement; 2]>, } @@ -254,11 +198,13 @@ impl ListItem { disabled: false, selected: false, indent_level: 0, + indent_step_size: px(12.), left_slot: None, overflow: OverflowStyle::Hidden, toggle: Toggle::NotToggleable, variant: ListItemVariant::default(), - on_click: Default::default(), + on_click: None, + on_secondary_mouse_down: None, children: SmallVec::new(), } } @@ -268,16 +214,29 @@ impl ListItem { self } + pub fn on_secondary_mouse_down( + mut self, + handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + ) -> Self { + self.on_secondary_mouse_down = Some(Rc::new(handler)); + self + } + pub fn variant(mut self, variant: ListItemVariant) -> Self { self.variant = variant; self } - pub fn indent_level(mut self, indent_level: u32) -> Self { + pub fn indent_level(mut self, indent_level: usize) -> Self { self.indent_level = indent_level; self } + pub fn indent_step_size(mut self, indent_step_size: Pixels) -> Self { + self.indent_step_size = indent_step_size; + self + } + pub fn toggle(mut self, toggle: Toggle) -> Self { self.toggle = toggle; self @@ -328,14 +287,6 @@ impl RenderOnce for ListItem { style.background = Some(cx.theme().colors().editor_background.into()); style }) - .on_click({ - let on_click = self.on_click.clone(); - move |event, cx| { - if let Some(on_click) = &on_click { - (on_click)(event, cx) - } - } - }) // TODO: Add focus state // .when(self.state == InteractionState::Focused, |this| { // this.border() @@ -346,30 +297,45 @@ impl RenderOnce for ListItem { .when(self.selected, |this| { this.bg(cx.theme().colors().ghost_element_selected) }) + .when_some(self.on_click.clone(), |this, on_click| { + this.on_click(move |event, cx| { + // HACK: GPUI currently fires `on_click` with any mouse button, + // but we only care about the left button. + if event.down.button == MouseButton::Left { + (on_click)(event, cx) + } + }) + }) + .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| { + this.on_mouse_down(MouseButton::Right, move |event, cx| { + (on_mouse_down)(event, cx) + }) + }) .child( div() .when(self.variant == ListItemVariant::Inset, |this| this.px_2()) - // .ml(rems(0.75 * self.indent_level as f32)) - .children((0..self.indent_level).map(|_| { - div() - .w(px(4.)) - .h_full() - .flex() - .justify_center() - .group_hover("", |style| style.bg(cx.theme().colors().border_focused)) - .child( - h_stack() - .child(div().w_px().h_full()) - .child(div().w_px().h_full().bg(cx.theme().colors().border)), - ) - })) + .ml(self.indent_level as f32 * self.indent_step_size) .flex() .gap_1() .items_center() .relative() .child(disclosure_control(self.toggle)) .children(left_content) - .children(self.children), + .children(self.children) + // HACK: We need to attach the `on_click` handler to the child element in order to have the click + // event actually fire. + // Once this is fixed in GPUI we can remove this and rely on the `on_click` handler set above on the + // outer `div`. + .id("on_click_hack") + .when_some(self.on_click, |this, on_click| { + this.on_click(move |event, cx| { + // HACK: GPUI currently fires `on_click` with any mouse button, + // but we only care about the left button. + if event.down.button == MouseButton::Left { + (on_click)(event, cx) + } + }) + }), ) } } diff --git a/crates/ui2/src/components/stories/list_item.rs b/crates/ui2/src/components/stories/list_item.rs index ee1b4d0be6d274f79191c42542a01ee8c7b77931..ec0da7f07e8d8784aacf42fdb551b39521952c73 100644 --- a/crates/ui2/src/components/stories/list_item.rs +++ b/crates/ui2/src/components/stories/list_item.rs @@ -22,5 +22,13 @@ impl Render for ListItemStory { println!("Clicked!"); }), ) + .child(Story::label("With `on_secondary_mouse_down`")) + .child( + ListItem::new("with_on_secondary_mouse_down").on_secondary_mouse_down( + |_event, _cx| { + println!("Right mouse down!"); + }, + ), + ) } } From 874fde09ab85ade8fa66091e7d52f3bd1882e67a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 28 Nov 2023 14:27:19 -0500 Subject: [PATCH 25/35] Add inset variant to `ListItem` (#3422) This PR adds an inset variant to the `ListItem` component. We're now using this inset variant for the `ListItem`s we render in pickers. Release Notes: - N/A --- .../command_palette2/src/command_palette.rs | 2 +- crates/file_finder2/src/file_finder.rs | 2 +- crates/storybook2/src/stories/picker.rs | 1 + crates/ui2/src/components/list.rs | 104 +++++++++--------- 4 files changed, 54 insertions(+), 55 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 5f3fb4a98595afc3b6930370bd552f0912af6344..b0d18c593d8d7b5e48ef5977c2cb06c26d6f0074 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -301,7 +301,7 @@ impl PickerDelegate for CommandPaletteDelegate { }; Some( - ListItem::new(ix).selected(selected).child( + ListItem::new(ix).inset(true).selected(selected).child( h_stack() .justify_between() .child(HighlightedLabel::new( diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index b54d28a400750b4caffbec99d163f561cf7f7784..2f7b26dfb55c9a548eea0ab4fbc88a5544f29921 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -719,7 +719,7 @@ impl PickerDelegate for FileFinderDelegate { self.labels_for_match(path_match, cx, ix); Some( - ListItem::new(ix).selected(selected).child( + ListItem::new(ix).inset(true).selected(selected).child( v_stack() .child(HighlightedLabel::new(file_name, file_name_positions)) .child(HighlightedLabel::new(full_path, full_path_positions)), diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index 8bcfb8923dbaef403ec65916821ae2341367f136..80818946f6dc5234f177366a435d3e50dd4223b3 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -61,6 +61,7 @@ impl PickerDelegate for Delegate { Some( ListItem::new(ix) + .inset(true) .selected(selected) .child(Label::new(candidate)), ) diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index d17c77ea445659ff747f861192d75a2ff14311e0..ad04408e13ecec02c871d76b1bd0253259924c40 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -12,14 +12,6 @@ use crate::{ }; use crate::{prelude::*, GraphicSlot}; -#[derive(Clone, Copy, Default, Debug, PartialEq)] -pub enum ListItemVariant { - /// The list item extends to the far left and right of the list. - FullWidth, - #[default] - Inset, -} - pub enum ListHeaderMeta { Tools(Vec), // TODO: This should be a button @@ -32,8 +24,39 @@ pub struct ListHeader { label: SharedString, left_icon: Option, meta: Option, - variant: ListItemVariant, toggle: Toggle, + inset: bool, +} + +impl ListHeader { + pub fn new(label: impl Into) -> Self { + Self { + label: label.into(), + left_icon: None, + meta: None, + inset: false, + toggle: Toggle::NotToggleable, + } + } + + pub fn toggle(mut self, toggle: Toggle) -> Self { + self.toggle = toggle; + self + } + + pub fn left_icon(mut self, left_icon: Option) -> Self { + self.left_icon = left_icon; + self + } + + pub fn right_button(self, button: IconButton) -> Self { + self.meta(Some(ListHeaderMeta::Tools(vec![button]))) + } + + pub fn meta(mut self, meta: Option) -> Self { + self.meta = meta; + self + } } impl RenderOnce for ListHeader { @@ -61,7 +84,7 @@ impl RenderOnce for ListHeader { .child( div() .h_5() - .when(self.variant == ListItemVariant::Inset, |this| this.px_2()) + .when(self.inset, |this| this.px_2()) .flex() .flex_1() .items_center() @@ -90,42 +113,11 @@ impl RenderOnce for ListHeader { } } -impl ListHeader { - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - left_icon: None, - meta: None, - variant: ListItemVariant::default(), - toggle: Toggle::NotToggleable, - } - } - - pub fn toggle(mut self, toggle: Toggle) -> Self { - self.toggle = toggle; - self - } - - pub fn left_icon(mut self, left_icon: Option) -> Self { - self.left_icon = left_icon; - self - } - - pub fn right_button(self, button: IconButton) -> Self { - self.meta(Some(ListHeaderMeta::Tools(vec![button]))) - } - - pub fn meta(mut self, meta: Option) -> Self { - self.meta = meta; - self - } -} - #[derive(IntoElement, Clone)] pub struct ListSubHeader { label: SharedString, left_icon: Option, - variant: ListItemVariant, + inset: bool, } impl ListSubHeader { @@ -133,7 +125,7 @@ impl ListSubHeader { Self { label: label.into(), left_icon: None, - variant: ListItemVariant::default(), + inset: false, } } @@ -150,7 +142,7 @@ impl RenderOnce for ListSubHeader { h_stack().flex_1().w_full().relative().py_1().child( div() .h_6() - .when(self.variant == ListItemVariant::Inset, |this| this.px_2()) + .when(self.inset, |this| this.px_2()) .flex() .flex_1() .w_full() @@ -185,7 +177,7 @@ pub struct ListItem { left_slot: Option, overflow: OverflowStyle, toggle: Toggle, - variant: ListItemVariant, + inset: bool, on_click: Option>, on_secondary_mouse_down: Option>, children: SmallVec<[AnyElement; 2]>, @@ -202,7 +194,7 @@ impl ListItem { left_slot: None, overflow: OverflowStyle::Hidden, toggle: Toggle::NotToggleable, - variant: ListItemVariant::default(), + inset: false, on_click: None, on_secondary_mouse_down: None, children: SmallVec::new(), @@ -222,8 +214,8 @@ impl ListItem { self } - pub fn variant(mut self, variant: ListItemVariant) -> Self { - self.variant = variant; + pub fn inset(mut self, inset: bool) -> Self { + self.inset = inset; self } @@ -283,20 +275,26 @@ impl RenderOnce for ListItem { div() .id(self.id) .relative() - .hover(|mut style| { - style.background = Some(cx.theme().colors().editor_background.into()); - style - }) // TODO: Add focus state // .when(self.state == InteractionState::Focused, |this| { // this.border() // .border_color(cx.theme().colors().border_focused) // }) + .when(self.inset, |this| this.rounded_md()) .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) .active(|style| style.bg(cx.theme().colors().ghost_element_active)) .when(self.selected, |this| { this.bg(cx.theme().colors().ghost_element_selected) }) + .when_some(self.on_click.clone(), |this, on_click| { + this.on_click(move |event, cx| { + // HACK: GPUI currently fires `on_click` with any mouse button, + // but we only care about the left button. + if event.down.button == MouseButton::Left { + (on_click)(event, cx) + } + }) + }) .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| { this.on_mouse_down(MouseButton::Right, move |event, cx| { (on_mouse_down)(event, cx) @@ -304,7 +302,7 @@ impl RenderOnce for ListItem { }) .child( div() - .when(self.variant == ListItemVariant::Inset, |this| this.px_2()) + .when(self.inset, |this| this.px_2()) .ml(self.indent_level as f32 * self.indent_step_size) .flex() .gap_1() From a9cb6589dd2a7764db3648e5897fb77cf1c684a5 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 28 Nov 2023 14:40:26 -0500 Subject: [PATCH 26/35] Update keybinding.rs --- crates/ui2/src/components/keybinding.rs | 90 ++++++++++++++++++++----- 1 file changed, 74 insertions(+), 16 deletions(-) diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index 1da0425ad376aab0cb8db3a0c4c27052a3cda8b5..15e365e5264dc0d9eac57971f05929b36f6c2a42 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -1,5 +1,5 @@ -use crate::prelude::*; -use gpui::{Action, Div, IntoElement}; +use crate::{h_stack, prelude::*, Icon, IconElement, IconSize}; +use gpui::{relative, rems, Action, Div, IntoElement, Keystroke}; #[derive(IntoElement, Clone)] pub struct KeyBinding { @@ -14,19 +14,35 @@ impl RenderOnce for KeyBinding { type Rendered = Div; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - div() - .flex() + h_stack() + .flex_none() .gap_2() .children(self.key_binding.keystrokes().iter().map(|keystroke| { - div() - .flex() - .gap_1() + let key_icon = Self::icon_for_key(&keystroke); + + h_stack() + .flex_none() + .gap_0p5() + .bg(cx.theme().colors().element_background) + .p_0p5() + .rounded_sm() .when(keystroke.modifiers.function, |el| el.child(Key::new("fn"))) - .when(keystroke.modifiers.control, |el| el.child(Key::new("^"))) - .when(keystroke.modifiers.alt, |el| el.child(Key::new("⌥"))) - .when(keystroke.modifiers.command, |el| el.child(Key::new("⌘"))) - .when(keystroke.modifiers.shift, |el| el.child(Key::new("⇧"))) - .child(Key::new(keystroke.key.clone())) + .when(keystroke.modifiers.control, |el| { + el.child(KeyIcon::new(Icon::Control)) + }) + .when(keystroke.modifiers.alt, |el| { + el.child(KeyIcon::new(Icon::Option)) + }) + .when(keystroke.modifiers.command, |el| { + el.child(KeyIcon::new(Icon::Command)) + }) + .when(keystroke.modifiers.shift, |el| { + el.child(KeyIcon::new(Icon::Shift)) + }) + // .when_some(key_icon, |el, icon| el.child(KeyIcon::new(icon))) + .when(key_icon.is_none(), |el| { + el.child(Key::new(keystroke.key.to_uppercase().clone())) + }) })) } } @@ -39,6 +55,22 @@ impl KeyBinding { Some(Self::new(key_binding)) } + fn icon_for_key(keystroke: &Keystroke) -> Option { + let mut icon: Option = None; + + if keystroke.key == "left".to_string() { + icon = Some(Icon::ArrowLeft); + } else if keystroke.key == "right".to_string() { + icon = Some(Icon::ArrowRight); + } else if keystroke.key == "up".to_string() { + icon = Some(Icon::ArrowUp); + } else if keystroke.key == "down".to_string() { + icon = Some(Icon::ArrowDown); + } + + icon + } + pub fn new(key_binding: gpui::KeyBinding) -> Self { Self { key_binding } } @@ -53,13 +85,18 @@ impl RenderOnce for Key { type Rendered = Div; fn render(self, cx: &mut WindowContext) -> Self::Rendered { + let single_char = self.key.len() == 1; + div() - .px_2() .py_0() - .rounded_md() - .text_ui_sm() + .when(single_char, |el| { + el.w(rems(14. / 16.)).flex().flex_none().justify_center() + }) + .when(!single_char, |el| el.px_0p5()) + .h(rems(14. / 16.)) + .text_ui() + .line_height(relative(1.)) .text_color(cx.theme().colors().text) - .bg(cx.theme().colors().element_background) .child(self.key.clone()) } } @@ -69,3 +106,24 @@ impl Key { Self { key: key.into() } } } + +#[derive(IntoElement)] +pub struct KeyIcon { + icon: Icon, +} + +impl RenderOnce for KeyIcon { + type Rendered = Div; + + fn render(self, cx: &mut WindowContext) -> Self::Rendered { + div() + .w(rems(14. / 16.)) + .child(IconElement::new(self.icon).size(IconSize::Small)) + } +} + +impl KeyIcon { + pub fn new(icon: Icon) -> Self { + Self { icon } + } +} From 070674a4fd6a248471d8e0f759f2bc78e24181c6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 28 Nov 2023 14:44:19 -0500 Subject: [PATCH 27/35] ui2: Unsuppress and fix warnings (#3423) This PR unsupresses the warnings in `ui2` and summarily fixes them. Release Notes: - N/A --- crates/ui2/src/components/context_menu.rs | 2 +- crates/ui2/src/components/divider.rs | 13 ----- crates/ui2/src/components/icon.rs | 13 ----- crates/ui2/src/components/icon_button.rs | 6 +- crates/ui2/src/components/keybinding.rs | 2 +- crates/ui2/src/components/label.rs | 8 +-- crates/ui2/src/components/list.rs | 8 +-- crates/ui2/src/components/stories/avatar.rs | 2 +- crates/ui2/src/components/stories/button.rs | 4 +- .../src/components/stories/context_menu.rs | 6 +- crates/ui2/src/components/stories/icon.rs | 2 +- crates/ui2/src/components/stories/input.rs | 2 +- .../ui2/src/components/stories/keybinding.rs | 2 +- crates/ui2/src/components/stories/label.rs | 2 +- .../ui2/src/components/stories/list_item.rs | 2 +- crates/ui2/src/components/tooltip.rs | 2 +- crates/ui2/src/prelude.rs | 6 -- crates/ui2/src/ui2.rs | 2 - crates/ui2/src/utils/format_distance.rs | 55 +++++++++---------- 19 files changed, 46 insertions(+), 93 deletions(-) diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index a92c08d82fae0164d0488e350f273df86f7ae48f..8cd519f62916dd351f2056100acb8d396801f8c0 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -264,7 +264,7 @@ impl Element for MenuHandle { let new_menu = (builder)(cx); let menu2 = menu.clone(); - cx.subscribe(&new_menu, move |modal, e, cx| match e { + cx.subscribe(&new_menu, move |_modal, e, cx| match e { &DismissEvent::Dismiss => { *menu2.borrow_mut() = None; cx.notify(); diff --git a/crates/ui2/src/components/divider.rs b/crates/ui2/src/components/divider.rs index b203a0feda4dfac5546c62e387de8502fb558140..cb48ce00ae24f031807d8522510aa18bc586d638 100644 --- a/crates/ui2/src/components/divider.rs +++ b/crates/ui2/src/components/divider.rs @@ -49,17 +49,4 @@ impl Divider { self.inset = true; self } - - fn render(self, cx: &mut WindowContext) -> impl Element { - div() - .map(|this| match self.direction { - DividerDirection::Horizontal => { - this.h_px().w_full().when(self.inset, |this| this.mx_1p5()) - } - DividerDirection::Vertical => { - this.w_px().h_full().when(self.inset, |this| this.my_1p5()) - } - }) - .bg(cx.theme().colors().border_variant) - } } diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 575b4fdb2877b91da18ad104b0314a4a696efe70..8190871767d19f65b8cac52648d9a4c8c6fda865 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -189,17 +189,4 @@ impl IconElement { self.size = size; self } - - fn render(self, cx: &mut WindowContext) -> impl Element { - let svg_size = match self.size { - IconSize::Small => rems(0.75), - IconSize::Medium => rems(0.9375), - }; - - svg() - .size(svg_size) - .flex_none() - .path(self.path) - .text_color(self.color.color(cx)) - } } diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 1ab35384c9d4bd8d8659079f4debfa8c6d54427c..e23882f5f778934cfc4de37e280908ea11315ceb 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -23,15 +23,13 @@ impl RenderOnce for IconButton { _ => self.color, }; - let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant { + let (mut bg_color, bg_active_color) = match self.variant { ButtonVariant::Filled => ( cx.theme().colors().element_background, - cx.theme().colors().element_hover, cx.theme().colors().element_active, ), ButtonVariant::Ghost => ( cx.theme().colors().ghost_element_background, - cx.theme().colors().ghost_element_hover, cx.theme().colors().ghost_element_active, ), }; @@ -124,6 +122,6 @@ impl IconButton { } pub fn action(self, action: Box) -> Self { - self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone())) + self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone())) } } diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index 1da0425ad376aab0cb8db3a0c4c27052a3cda8b5..78300ebe54c25e5cb37a05d41acfc472ba68bf26 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -13,7 +13,7 @@ pub struct KeyBinding { impl RenderOnce for KeyBinding { type Rendered = Div; - fn render(self, cx: &mut WindowContext) -> Self::Rendered { + fn render(self, _cx: &mut WindowContext) -> Self::Rendered { div() .flex() .gap_2() diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index 627a95d9534d2a4aadda019d6be59b2977653ed3..562131a96975ad2f8c5986bac2bfdd723c20032d 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -1,6 +1,6 @@ use crate::prelude::*; use crate::styled_ext::StyledExt; -use gpui::{relative, Div, Hsla, IntoElement, StyledText, TextRun, WindowContext}; +use gpui::{relative, Div, IntoElement, StyledText, TextRun, WindowContext}; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] pub enum LabelSize { @@ -182,9 +182,3 @@ impl HighlightedLabel { self } } - -/// A run of text that receives the same style. -struct Run { - pub text: String, - pub color: Hsla, -} diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index ad04408e13ecec02c871d76b1bd0253259924c40..749de951d9a3055050e18198956eb71c75684cfe 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -138,7 +138,7 @@ impl ListSubHeader { impl RenderOnce for ListSubHeader { type Rendered = Div; - fn render(self, cx: &mut WindowContext) -> Self::Rendered { + fn render(self, _cx: &mut WindowContext) -> Self::Rendered { h_stack().flex_1().w_full().relative().py_1().child( div() .h_6() @@ -168,14 +168,12 @@ impl RenderOnce for ListSubHeader { #[derive(IntoElement)] pub struct ListItem { id: ElementId, - disabled: bool, selected: bool, // TODO: Reintroduce this // disclosure_control_style: DisclosureControlVisibility, indent_level: usize, indent_step_size: Pixels, left_slot: Option, - overflow: OverflowStyle, toggle: Toggle, inset: bool, on_click: Option>, @@ -187,12 +185,10 @@ impl ListItem { pub fn new(id: impl Into) -> Self { Self { id: id.into(), - disabled: false, selected: false, indent_level: 0, indent_step_size: px(12.), left_slot: None, - overflow: OverflowStyle::Hidden, toggle: Toggle::NotToggleable, inset: false, on_click: None, @@ -365,7 +361,7 @@ pub struct List { impl RenderOnce for List { type Rendered = Div; - fn render(self, cx: &mut WindowContext) -> Self::Rendered { + fn render(self, _cx: &mut WindowContext) -> Self::Rendered { let list_content = match (self.children.is_empty(), self.toggle) { (false, _) => div().children(self.children), (true, Toggle::Toggled(false)) => div(), diff --git a/crates/ui2/src/components/stories/avatar.rs b/crates/ui2/src/components/stories/avatar.rs index 505ede4ecc98d1248533f806b8fb47fc932a0028..3e830b8b79df059c4257cfad73691dbda938c6a3 100644 --- a/crates/ui2/src/components/stories/avatar.rs +++ b/crates/ui2/src/components/stories/avatar.rs @@ -9,7 +9,7 @@ pub struct AvatarStory; impl Render for AvatarStory { type Element = Div; - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { Story::container() .child(Story::title_for::()) .child(Story::label("Default")) diff --git a/crates/ui2/src/components/stories/button.rs b/crates/ui2/src/components/stories/button.rs index 6a23060c7813034434285e0e6af036a40e8a21b5..7339f95ae2e04afe8bd9aa705e93b219a8ba7869 100644 --- a/crates/ui2/src/components/stories/button.rs +++ b/crates/ui2/src/components/stories/button.rs @@ -10,7 +10,7 @@ pub struct ButtonStory; impl Render for ButtonStory { type Element = Div; - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { let states = InteractionState::iter(); Story::container() @@ -139,7 +139,7 @@ impl Render for ButtonStory { .child( Button::new("Label") .variant(ButtonVariant::Ghost) - .on_click(|_, cx| println!("Button clicked.")), + .on_click(|_, _cx| println!("Button clicked.")), ) } } diff --git a/crates/ui2/src/components/stories/context_menu.rs b/crates/ui2/src/components/stories/context_menu.rs index 98faea70aabdaec14a78757ce4a6a00919932654..9a8b7efbe666bb95317c98299b4ad72d0a1d23cd 100644 --- a/crates/ui2/src/components/stories/context_menu.rs +++ b/crates/ui2/src/components/stories/context_menu.rs @@ -10,11 +10,11 @@ fn build_menu(cx: &mut WindowContext, header: impl Into) -> View) -> Self::Element { + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { Story::container() .on_action(|_: &PrintCurrentDate, _| { println!("printing unix time!"); diff --git a/crates/ui2/src/components/stories/icon.rs b/crates/ui2/src/components/stories/icon.rs index bd3cafd5314d17b06d3b96e0a300cc774c489fd8..ee7dfe532cdf3619bee41c47d2fb94ec95a26b21 100644 --- a/crates/ui2/src/components/stories/icon.rs +++ b/crates/ui2/src/components/stories/icon.rs @@ -10,7 +10,7 @@ pub struct IconStory; impl Render for IconStory { type Element = Div; - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { let icons = Icon::iter(); Story::container() diff --git a/crates/ui2/src/components/stories/input.rs b/crates/ui2/src/components/stories/input.rs index f8eb553e7db6a50415983b9acff78b208be74286..d041543616072e943cb61f2e4e37f5e494e9b66a 100644 --- a/crates/ui2/src/components/stories/input.rs +++ b/crates/ui2/src/components/stories/input.rs @@ -9,7 +9,7 @@ pub struct InputStory; impl Render for InputStory { type Element = Div; - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { Story::container() .child(Story::title_for::()) .child(Story::label("Default")) diff --git a/crates/ui2/src/components/stories/keybinding.rs b/crates/ui2/src/components/stories/keybinding.rs index a1aba23b5930f7e64730bd4c25ed90a0e34308fc..b1b3401f17f5e30f5de28c12c6d72aef53c6a25d 100644 --- a/crates/ui2/src/components/stories/keybinding.rs +++ b/crates/ui2/src/components/stories/keybinding.rs @@ -16,7 +16,7 @@ pub fn binding(key: &str) -> gpui::KeyBinding { impl Render for KeybindingStory { type Element = Div; - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { let all_modifier_permutations = ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2); Story::container() diff --git a/crates/ui2/src/components/stories/label.rs b/crates/ui2/src/components/stories/label.rs index f19f643ad635c28090ab05cd09f73ff80ae2dd04..2417bee6e1cd77646ceddd2c4febe4c6e45b7e19 100644 --- a/crates/ui2/src/components/stories/label.rs +++ b/crates/ui2/src/components/stories/label.rs @@ -9,7 +9,7 @@ pub struct LabelStory; impl Render for LabelStory { type Element = Div; - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { Story::container() .child(Story::title_for::