Add contact finder, change ui::List's on_click handler signature

Piotr Osiewicz created

Change summary

crates/collab_ui2/src/collab_panel.rs                |  68 ++-
crates/collab_ui2/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(-)

Detailed changes

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<Self>) {
-    //         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<Self>) {
+        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>) {
     //         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, _>(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(

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::<ContactFinderDelegate>::init(cx);
-    cx.add_action(ContactFinder::dismiss)
+    //Picker::<ContactFinderDelegate>::init(cx);
+    //cx.add_action(ContactFinder::dismiss)
 }
 
 pub struct ContactFinder {
-    picker: ViewHandle<Picker<ContactFinderDelegate>>,
+    picker: View<Picker<ContactFinderDelegate>>,
     has_focus: bool,
 }
 
 impl ContactFinder {
-    pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> 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<UserStore>, cx: &mut ViewContext<Self>) -> 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>) {
         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<Self>) {
-        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<Self>) -> AnyElement<Self> {
-        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<ContactFinder>,
-        ) -> AnyElement<ContactFinder> {
-            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>) -> 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>) {
-        self.has_focus = true;
-        if cx.is_self_focused() {
-            cx.focus(&self.picker)
-        }
-    }
+    // fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+    //     self.has_focus = true;
+    //     if cx.is_self_focused() {
+    //         cx.focus(&self.picker)
+    //     }
+    // }
 
-    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
-        self.has_focus = false;
-    }
+    // fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+    //     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<ContactFinder>,
     potential_contacts: Arc<[Arc<User>]>,
-    user_store: ModelHandle<UserStore>,
+    user_store: Model<UserStore>,
     selected_index: usize,
 }
 
-impl PickerDelegate for ContactFinderDelegate {
-    fn placeholder_text(&self) -> Arc<str> {
-        "Search collaborator by username...".into()
+impl EventEmitter<DismissEvent> 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<str> {
+        "Search collaborator by username...".into()
+    }
+
     fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> 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<Picker<Self>>) {
-        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<Picker<Self>> {
-        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<Picker<Self>>,
+    ) -> 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()
     }
 }

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

crates/ui2/src/components/context_menu.rs 🔗

@@ -12,7 +12,10 @@ use gpui::{
 pub enum ContextMenuItem {
     Separator,
     Header(SharedString),
-    Entry(SharedString, Rc<dyn Fn(&ClickEvent, &mut WindowContext)>),
+    Entry(
+        SharedString,
+        Rc<dyn Fn(&MouseDownEvent, &mut WindowContext)>,
+    ),
 }
 
 pub struct ContextMenu {
@@ -58,7 +61,7 @@ impl ContextMenu {
     pub fn entry(
         mut self,
         label: impl Into<SharedString>,
-        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)));

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<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+    on_click: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
     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<SharedString>) -> Self {
+    pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> 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)
+                            }
+                        }
+                    }),
             )
     }
 }

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),
 }