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