Implement following and more collaboration panel features in zed2 (#3504)

Max Brunsfeld created

Change summary

crates/collab_ui2/src/collab_panel.rs           | 226 +++---------------
crates/diagnostics2/src/diagnostics.rs          |  47 ---
crates/editor2/src/editor.rs                    |  27 -
crates/editor2/src/editor_tests.rs              |   4 
crates/editor2/src/items.rs                     |  75 ++++-
crates/gpui2/src/elements/canvas.rs             |  10 
crates/gpui2/src/platform/mac/display_linker.rs |   3 
crates/gpui2/src/platform/test/platform.rs      |  29 +
crates/gpui2/src/window.rs                      |   6 
crates/terminal_view2/src/terminal_view.rs      |   6 
crates/welcome2/src/welcome.rs                  |   6 
crates/workspace2/src/item.rs                   |  48 ++-
crates/workspace2/src/shared_screen.rs          |  10 
crates/workspace2/src/workspace2.rs             |   4 
14 files changed, 193 insertions(+), 308 deletions(-)

Detailed changes

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -169,7 +169,7 @@ use editor::Editor;
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions, canvas, div, img, overlay, point, prelude::*, px, rems, serde_json, Action,
+    actions, canvas, div, img, overlay, point, prelude::*, px, rems, serde_json, size, Action,
     AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter,
     FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement, Length, Model,
     MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render, RenderOnce,
@@ -1204,14 +1204,9 @@ impl CollabPanel {
                         .detach_and_log_err(cx);
                 });
             }))
-            .left_child(IconButton::new(0, Icon::Folder))
-            .child(
-                h_stack()
-                    .w_full()
-                    .justify_between()
-                    .child(render_tree_branch(is_last, cx))
-                    .child(Label::new(project_name.clone())),
-            )
+            .left_child(render_tree_branch(is_last, cx))
+            .child(IconButton::new(0, Icon::Folder))
+            .child(Label::new(project_name.clone()))
             .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
 
         //         enum JoinProject {}
@@ -1299,70 +1294,20 @@ impl CollabPanel {
         is_last: bool,
         cx: &mut ViewContext<Self>,
     ) -> impl IntoElement {
-        //         enum OpenSharedScreen {}
-
-        //         let host_avatar_width = theme
-        //             .contact_avatar
-        //             .width
-        //             .or(theme.contact_avatar.height)
-        //             .unwrap_or(0.);
-        //         let tree_branch = theme.tree_branch;
-
-        //         let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
-        //             peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
-        //             cx,
-        //             |mouse_state, cx| {
-        //                 let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
-        //                 let row = theme
-        //                     .project_row
-        //                     .in_state(is_selected)
-        //                     .style_for(mouse_state);
-
-        //                 Flex::row()
-        //                     .with_child(render_tree_branch(
-        //                         tree_branch,
-        //                         &row.name.text,
-        //                         is_last,
-        //                         vec2f(host_avatar_width, theme.row_height),
-        //                         cx.font_cache(),
-        //                     ))
-        //                     .with_child(
-        //                         Svg::new("icons/desktop.svg")
-        //                             .with_color(theme.channel_hash.color)
-        //                             .constrained()
-        //                             .with_width(theme.channel_hash.width)
-        //                             .aligned()
-        //                             .left(),
-        //                     )
-        //                     .with_child(
-        //                         Label::new("Screen", row.name.text.clone())
-        //                             .aligned()
-        //                             .left()
-        //                             .contained()
-        //                             .with_style(row.name.container)
-        //                             .flex(1., false),
-        //                     )
-        //                     .constrained()
-        //                     .with_height(theme.row_height)
-        //                     .contained()
-        //                     .with_style(row.container)
-        //             },
-        //         );
-        //         if peer_id.is_none() {
-        //             return handler.into_any();
-        //         }
-        //         handler
-        //             .with_cursor_style(CursorStyle::PointingHand)
-        //             .on_click(MouseButton::Left, move |_, this, cx| {
-        //                 if let Some(workspace) = this.workspace.upgrade(cx) {
-        //                     workspace.update(cx, |workspace, cx| {
-        //                         workspace.open_shared_screen(peer_id.unwrap(), cx)
-        //                     });
-        //                 }
-        //             })
-        //             .into_any()
-
-        div()
+        let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
+
+        ListItem::new(("screen", id))
+            .left_child(render_tree_branch(is_last, cx))
+            .child(IconButton::new(0, Icon::Screen))
+            .child(Label::new("Screen"))
+            .when_some(peer_id, |this, _| {
+                this.on_click(cx.listener(move |this, _, cx| {
+                    this.workspace.update(cx, |workspace, cx| {
+                        workspace.open_shared_screen(peer_id.unwrap(), cx)
+                    });
+                }))
+                .tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx))
+            })
     }
 
     fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
@@ -1415,54 +1360,14 @@ impl CollabPanel {
         channel_id: ChannelId,
         cx: &mut ViewContext<Self>,
     ) -> impl IntoElement {
-        //         enum ChannelNotes {}
-        //         let host_avatar_width = theme
-        //             .contact_avatar
-        //             .width
-        //             .or(theme.contact_avatar.height)
-        //             .unwrap_or(0.);
-
-        //         MouseEventHandler::new::<ChannelNotes, _>(ix as usize, cx, |state, cx| {
-        //             let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
-        //             let row = theme.project_row.in_state(is_selected).style_for(state);
-
-        //             Flex::<Self>::row()
-        //                 .with_child(render_tree_branch(
-        //                     tree_branch,
-        //                     &row.name.text,
-        //                     false,
-        //                     vec2f(host_avatar_width, theme.row_height),
-        //                     cx.font_cache(),
-        //                 ))
-        //                 .with_child(
-        //                     Svg::new("icons/file.svg")
-        //                         .with_color(theme.channel_hash.color)
-        //                         .constrained()
-        //                         .with_width(theme.channel_hash.width)
-        //                         .aligned()
-        //                         .left(),
-        //                 )
-        //                 .with_child(
-        //                     Label::new("notes", theme.channel_name.text.clone())
-        //                         .contained()
-        //                         .with_style(theme.channel_name.container)
-        //                         .aligned()
-        //                         .left()
-        //                         .flex(1., true),
-        //                 )
-        //                 .constrained()
-        //                 .with_height(theme.row_height)
-        //                 .contained()
-        //                 .with_style(*theme.channel_row.style_for(is_selected, state))
-        //                 .with_padding_left(theme.channel_row.default_style().padding.left)
-        //         })
-        //         .on_click(MouseButton::Left, move |_, this, cx| {
-        //             this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
-        //         })
-        //         .with_cursor_style(CursorStyle::PointingHand)
-        //         .into_any()
-
-        div()
+        ListItem::new("channel-notes")
+            .on_click(cx.listener(move |this, _, cx| {
+                this.open_channel_notes(channel_id, cx);
+            }))
+            .left_child(render_tree_branch(false, cx))
+            .child(IconButton::new(0, Icon::File))
+            .child(Label::new("notes"))
+            .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
     }
 
     fn render_channel_chat(
@@ -1470,53 +1375,14 @@ impl CollabPanel {
         channel_id: ChannelId,
         cx: &mut ViewContext<Self>,
     ) -> impl IntoElement {
-        //         enum ChannelChat {}
-        //         let host_avatar_width = theme
-        //             .contact_avatar
-        //             .width
-        //             .or(theme.contact_avatar.height)
-        //             .unwrap_or(0.);
-
-        //         MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
-        //             let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
-        //             let row = theme.project_row.in_state(is_selected).style_for(state);
-
-        //             Flex::<Self>::row()
-        //                 .with_child(render_tree_branch(
-        //                     tree_branch,
-        //                     &row.name.text,
-        //                     true,
-        //                     vec2f(host_avatar_width, theme.row_height),
-        //                     cx.font_cache(),
-        //                 ))
-        //                 .with_child(
-        //                     Svg::new("icons/conversations.svg")
-        //                         .with_color(theme.channel_hash.color)
-        //                         .constrained()
-        //                         .with_width(theme.channel_hash.width)
-        //                         .aligned()
-        //                         .left(),
-        //                 )
-        //                 .with_child(
-        //                     Label::new("chat", theme.channel_name.text.clone())
-        //                         .contained()
-        //                         .with_style(theme.channel_name.container)
-        //                         .aligned()
-        //                         .left()
-        //                         .flex(1., true),
-        //                 )
-        //                 .constrained()
-        //                 .with_height(theme.row_height)
-        //                 .contained()
-        //                 .with_style(*theme.channel_row.style_for(is_selected, state))
-        //                 .with_padding_left(theme.channel_row.default_style().padding.left)
-        //         })
-        //         .on_click(MouseButton::Left, move |_, this, cx| {
-        //             this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
-        //         })
-        //         .with_cursor_style(CursorStyle::PointingHand)
-        //         .into_any()
-        div()
+        ListItem::new("channel-chat")
+            .on_click(cx.listener(move |this, _, cx| {
+                this.join_channel_chat(channel_id, cx);
+            }))
+            .left_child(render_tree_branch(true, cx))
+            .child(IconButton::new(0, Icon::MessageBubbles))
+            .child(Label::new("chat"))
+            .tooltip(move |cx| Tooltip::text("Open Chat", cx))
     }
 
     //     fn render_channel_invite(
@@ -3119,30 +2985,24 @@ impl CollabPanel {
 }
 
 fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement {
-    let text_style = cx.text_style();
     let rem_size = cx.rem_size();
-    let text_system = cx.text_system();
-    let font_id = text_system.font_id(&text_style.font()).unwrap();
-    let font_size = text_style.font_size.to_pixels(rem_size);
-    let line_height = text_style.line_height_in_pixels(rem_size);
-    let cap_height = text_system.cap_height(font_id, font_size);
-    let baseline_offset = text_system.baseline_offset(font_id, font_size, line_height);
-    let width = cx.rem_size() * 2.5;
+    let line_height = cx.text_style().line_height_in_pixels(rem_size);
+    let width = rem_size * 1.5;
     let thickness = px(2.);
     let color = cx.theme().colors().text;
 
     canvas(move |bounds, cx| {
-        let start_x = bounds.left() + (bounds.size.width / 2.) - (width / 2.);
-        let end_x = bounds.right();
-        let start_y = bounds.top();
-        let end_y = bounds.top() + baseline_offset - (cap_height / 2.);
+        let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
+        let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
+        let right = bounds.right();
+        let top = bounds.top();
 
         cx.paint_quad(
             Bounds::from_corners(
-                point(start_x, start_y),
+                point(start_x, top),
                 point(
                     start_x + thickness,
-                    if is_last { end_y } else { bounds.bottom() },
+                    if is_last { start_y } else { bounds.bottom() },
                 ),
             ),
             Default::default(),
@@ -3151,7 +3011,7 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement
             Hsla::transparent_black(),
         );
         cx.paint_quad(
-            Bounds::from_corners(point(start_x, end_y), point(end_x, end_y + thickness)),
+            Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
             Default::default(),
             color,
             Default::default(),

crates/diagnostics2/src/diagnostics.rs 🔗

@@ -88,7 +88,7 @@ struct DiagnosticGroupState {
     block_count: usize,
 }
 
-impl EventEmitter<ItemEvent> for ProjectDiagnosticsEditor {}
+impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
 
 impl Render for ProjectDiagnosticsEditor {
     type Element = Focusable<Div>;
@@ -158,7 +158,7 @@ impl ProjectDiagnosticsEditor {
         });
         let editor_event_subscription =
             cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
-                Self::emit_item_event_for_editor_event(event, cx);
+                cx.emit(event.clone());
                 if event == &EditorEvent::Focused && this.path_states.is_empty() {
                     cx.focus(&this.focus_handle);
                 }
@@ -183,40 +183,6 @@ impl ProjectDiagnosticsEditor {
         this
     }
 
-    fn emit_item_event_for_editor_event(event: &EditorEvent, cx: &mut ViewContext<Self>) {
-        match event {
-            EditorEvent::Closed => cx.emit(ItemEvent::CloseItem),
-
-            EditorEvent::Saved | EditorEvent::TitleChanged => {
-                cx.emit(ItemEvent::UpdateTab);
-                cx.emit(ItemEvent::UpdateBreadcrumbs);
-            }
-
-            EditorEvent::Reparsed => {
-                cx.emit(ItemEvent::UpdateBreadcrumbs);
-            }
-
-            EditorEvent::SelectionsChanged { local } if *local => {
-                cx.emit(ItemEvent::UpdateBreadcrumbs);
-            }
-
-            EditorEvent::DirtyChanged => {
-                cx.emit(ItemEvent::UpdateTab);
-            }
-
-            EditorEvent::BufferEdited => {
-                cx.emit(ItemEvent::Edit);
-                cx.emit(ItemEvent::UpdateBreadcrumbs);
-            }
-
-            EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => {
-                cx.emit(ItemEvent::Edit);
-            }
-
-            _ => {}
-        }
-    }
-
     fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
         if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
             workspace.activate_item(&existing, cx);
@@ -333,8 +299,7 @@ impl ProjectDiagnosticsEditor {
 
                 this.update(&mut cx, |this, cx| {
                     this.summary = this.project.read(cx).diagnostic_summary(false, cx);
-                    cx.emit(ItemEvent::UpdateTab);
-                    cx.emit(ItemEvent::UpdateBreadcrumbs);
+                    cx.emit(EditorEvent::TitleChanged);
                 })?;
                 anyhow::Ok(())
             }
@@ -649,6 +614,12 @@ impl FocusableView for ProjectDiagnosticsEditor {
 }
 
 impl Item for ProjectDiagnosticsEditor {
+    type Event = EditorEvent;
+
+    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
+        Editor::to_item_events(event, f)
+    }
+
     fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
         self.editor.update(cx, |editor, cx| editor.deactivated(cx));
     }

crates/editor2/src/editor.rs 🔗

@@ -1675,8 +1675,7 @@ impl Editor {
             if let Some(project) = project.as_ref() {
                 if buffer.read(cx).is_singleton() {
                     project_subscriptions.push(cx.observe(project, |_, _, cx| {
-                        cx.emit(ItemEvent::UpdateTab);
-                        cx.emit(ItemEvent::UpdateBreadcrumbs);
+                        cx.emit(EditorEvent::TitleChanged);
                     }));
                 }
                 project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| {
@@ -2141,10 +2140,6 @@ impl Editor {
             cx.emit(SearchEvent::ActiveMatchChanged)
         }
 
-        if local {
-            cx.emit(ItemEvent::UpdateBreadcrumbs);
-        }
-
         cx.notify();
     }
 
@@ -8573,8 +8568,6 @@ impl Editor {
                     self.update_visible_copilot_suggestion(cx);
                 }
                 cx.emit(EditorEvent::BufferEdited);
-                cx.emit(ItemEvent::Edit);
-                cx.emit(ItemEvent::UpdateBreadcrumbs);
                 cx.emit(SearchEvent::MatchesInvalidated);
 
                 if *sigleton_buffer_edited {
@@ -8622,20 +8615,14 @@ impl Editor {
                 self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
                 cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
             }
-            multi_buffer::Event::Reparsed => {
-                cx.emit(ItemEvent::UpdateBreadcrumbs);
-            }
-            multi_buffer::Event::DirtyChanged => {
-                cx.emit(ItemEvent::UpdateTab);
-            }
-            multi_buffer::Event::Saved
-            | multi_buffer::Event::FileHandleChanged
-            | multi_buffer::Event::Reloaded => {
-                cx.emit(ItemEvent::UpdateTab);
-                cx.emit(ItemEvent::UpdateBreadcrumbs);
+            multi_buffer::Event::Reparsed => cx.emit(EditorEvent::Reparsed),
+            multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged),
+            multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved),
+            multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded => {
+                cx.emit(EditorEvent::TitleChanged)
             }
             multi_buffer::Event::DiffBaseChanged => cx.emit(EditorEvent::DiffBaseChanged),
-            multi_buffer::Event::Closed => cx.emit(ItemEvent::CloseItem),
+            multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed),
             multi_buffer::Event::DiagnosticsUpdated => {
                 self.refresh_active_diagnostics(cx);
             }

crates/editor2/src/editor_tests.rs 🔗

@@ -32,7 +32,7 @@ use util::{
     test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
 };
 use workspace::{
-    item::{FollowEvent, FollowableEvents, FollowableItem, Item, ItemHandle},
+    item::{FollowEvent, FollowableItem, Item, ItemHandle},
     NavigationEntry, ViewId,
 };
 
@@ -6478,7 +6478,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
             cx.subscribe(
                 &follower.root_view(cx).unwrap(),
                 move |_, _, event: &EditorEvent, cx| {
-                    if matches!(event.to_follow_event(), Some(FollowEvent::Unfollow)) {
+                    if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
                         *is_still_following.borrow_mut() = false;
                     }
 

crates/editor2/src/items.rs 🔗

@@ -35,7 +35,7 @@ use theme::{ActiveTheme, Theme};
 use ui::{h_stack, Color, Label};
 use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
 use workspace::{
-    item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle},
+    item::{BreadcrumbText, FollowEvent, FollowableItemHandle},
     StatusItemView,
 };
 use workspace::{
@@ -46,27 +46,7 @@ use workspace::{
 
 pub const MAX_TAB_TITLE_LEN: usize = 24;
 
-impl FollowableEvents for EditorEvent {
-    fn to_follow_event(&self) -> Option<workspace::item::FollowEvent> {
-        match self {
-            EditorEvent::Edited => Some(FollowEvent::Unfollow),
-            EditorEvent::SelectionsChanged { local }
-            | EditorEvent::ScrollPositionChanged { local, .. } => {
-                if *local {
-                    Some(FollowEvent::Unfollow)
-                } else {
-                    None
-                }
-            }
-            _ => None,
-        }
-    }
-}
-
-impl EventEmitter<ItemEvent> for Editor {}
-
 impl FollowableItem for Editor {
-    type FollowableEvent = EditorEvent;
     fn remote_id(&self) -> Option<ViewId> {
         self.remote_id
     }
@@ -241,9 +221,24 @@ impl FollowableItem for Editor {
         }))
     }
 
+    fn to_follow_event(event: &EditorEvent) -> Option<workspace::item::FollowEvent> {
+        match event {
+            EditorEvent::Edited => Some(FollowEvent::Unfollow),
+            EditorEvent::SelectionsChanged { local }
+            | EditorEvent::ScrollPositionChanged { local, .. } => {
+                if *local {
+                    Some(FollowEvent::Unfollow)
+                } else {
+                    None
+                }
+            }
+            _ => None,
+        }
+    }
+
     fn add_event_to_update_proto(
         &self,
-        event: &Self::FollowableEvent,
+        event: &EditorEvent,
         update: &mut Option<proto::update_view::Variant>,
         cx: &WindowContext,
     ) -> bool {
@@ -528,6 +523,8 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor)
 }
 
 impl Item for Editor {
+    type Event = EditorEvent;
+
     fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
         if let Ok(data) = data.downcast::<NavigationData>() {
             let newest_selection = self.selections.newest::<Point>(cx);
@@ -838,6 +835,40 @@ impl Item for Editor {
         Some("Editor")
     }
 
+    fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) {
+        match event {
+            EditorEvent::Closed => f(ItemEvent::CloseItem),
+
+            EditorEvent::Saved | EditorEvent::TitleChanged => {
+                f(ItemEvent::UpdateTab);
+                f(ItemEvent::UpdateBreadcrumbs);
+            }
+
+            EditorEvent::Reparsed => {
+                f(ItemEvent::UpdateBreadcrumbs);
+            }
+
+            EditorEvent::SelectionsChanged { local } if *local => {
+                f(ItemEvent::UpdateBreadcrumbs);
+            }
+
+            EditorEvent::DirtyChanged => {
+                f(ItemEvent::UpdateTab);
+            }
+
+            EditorEvent::BufferEdited => {
+                f(ItemEvent::Edit);
+                f(ItemEvent::UpdateBreadcrumbs);
+            }
+
+            EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => {
+                f(ItemEvent::Edit);
+            }
+
+            _ => {}
+        }
+    }
+
     fn deserialize(
         project: Model<Project>,
         _workspace: WeakView<Workspace>,

crates/gpui2/src/elements/canvas.rs 🔗

@@ -1,9 +1,11 @@
-use crate::{Bounds, Element, IntoElement, Pixels, StyleRefinement, Styled, WindowContext};
+use refineable::Refineable as _;
+
+use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext};
 
 pub fn canvas(callback: impl 'static + FnOnce(Bounds<Pixels>, &mut WindowContext)) -> Canvas {
     Canvas {
         paint_callback: Box::new(callback),
-        style: Default::default(),
+        style: StyleRefinement::default(),
     }
 }
 
@@ -32,7 +34,9 @@ impl Element for Canvas {
         _: Option<Self::State>,
         cx: &mut WindowContext,
     ) -> (crate::LayoutId, Self::State) {
-        let layout_id = cx.request_layout(&self.style.clone().into(), []);
+        let mut style = Style::default();
+        style.refine(&self.style);
+        let layout_id = cx.request_layout(&style, []);
         (layout_id, ())
     }
 

crates/gpui2/src/platform/mac/display_linker.rs 🔗

@@ -7,6 +7,7 @@ use std::{
 use crate::DisplayId;
 use collections::HashMap;
 use parking_lot::Mutex;
+pub use sys::CVSMPTETime as SmtpeTime;
 pub use sys::CVTimeStamp as VideoTimestamp;
 
 pub(crate) struct MacDisplayLinker {
@@ -153,7 +154,7 @@ mod sys {
         kCVTimeStampTopField | kCVTimeStampBottomField;
 
     #[repr(C)]
-    #[derive(Clone, Copy)]
+    #[derive(Clone, Copy, Default)]
     pub struct CVSMPTETime {
         pub subframes: i16,
         pub subframe_divisor: i16,

crates/gpui2/src/platform/test/platform.rs 🔗

@@ -147,18 +147,25 @@ impl Platform for TestPlatform {
     fn set_display_link_output_callback(
         &self,
         _display_id: DisplayId,
-        _callback: Box<dyn FnMut(&crate::VideoTimestamp, &crate::VideoTimestamp) + Send>,
+        mut callback: Box<dyn FnMut(&crate::VideoTimestamp, &crate::VideoTimestamp) + Send>,
     ) {
-        unimplemented!()
-    }
-
-    fn start_display_link(&self, _display_id: DisplayId) {
-        unimplemented!()
-    }
-
-    fn stop_display_link(&self, _display_id: DisplayId) {
-        unimplemented!()
-    }
+        let timestamp = crate::VideoTimestamp {
+            version: 0,
+            video_time_scale: 0,
+            video_time: 0,
+            host_time: 0,
+            rate_scalar: 0.0,
+            video_refresh_period: 0,
+            smpte_time: crate::SmtpeTime::default(),
+            flags: 0,
+            reserved: 0,
+        };
+        callback(&timestamp, &timestamp)
+    }
+
+    fn start_display_link(&self, _display_id: DisplayId) {}
+
+    fn stop_display_link(&self, _display_id: DisplayId) {}
 
     fn open_url(&self, _url: &str) {
         unimplemented!()

crates/gpui2/src/window.rs 🔗

@@ -2816,3 +2816,9 @@ impl From<(&'static str, EntityId)> for ElementId {
         ElementId::NamedInteger(name.into(), id.as_u64() as usize)
     }
 }
+
+impl From<(&'static str, usize)> for ElementId {
+    fn from((name, id): (&'static str, usize)) -> Self {
+        ElementId::NamedInteger(name.into(), id)
+    }
+}

crates/terminal_view2/src/terminal_view.rs 🔗

@@ -736,6 +736,8 @@ impl InputHandler for TerminalView {
 }
 
 impl Item for TerminalView {
+    type Event = ItemEvent;
+
     fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
         Some(self.terminal().read(cx).title().into())
     }
@@ -843,6 +845,10 @@ impl Item for TerminalView {
         //     .detach();
         self.workspace_id = workspace.database_id();
     }
+
+    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
+        f(*event)
+    }
 }
 
 impl SearchableItem for TerminalView {

crates/welcome2/src/welcome.rs 🔗

@@ -259,6 +259,8 @@ impl FocusableView for WelcomePage {
 }
 
 impl Item for WelcomePage {
+    type Event = ItemEvent;
+
     fn tab_content(&self, _: Option<usize>, _: &WindowContext) -> AnyElement {
         "Welcome to Zed!".into_any()
     }
@@ -278,4 +280,8 @@ impl Item for WelcomePage {
             _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
         }))
     }
+
+    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
+        f(*event)
+    }
 }

crates/workspace2/src/item.rs 🔗

@@ -78,7 +78,7 @@ impl Settings for ItemSettings {
     }
 }
 
-#[derive(Eq, PartialEq, Hash, Debug)]
+#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
 pub enum ItemEvent {
     CloseItem,
     UpdateTab,
@@ -92,7 +92,9 @@ pub struct BreadcrumbText {
     pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
 }
 
-pub trait Item: FocusableView + EventEmitter<ItemEvent> {
+pub trait Item: FocusableView + EventEmitter<Self::Event> {
+    type Event;
+
     fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
     fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
     fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
@@ -155,6 +157,8 @@ pub trait Item: FocusableView + EventEmitter<ItemEvent> {
         unimplemented!("reload() must be implemented if can_save() returns true")
     }
 
+    fn to_item_events(event: &Self::Event, f: impl FnMut(ItemEvent));
+
     fn act_as_type<'a>(
         &'a self,
         type_id: TypeId,
@@ -206,12 +210,12 @@ pub trait Item: FocusableView + EventEmitter<ItemEvent> {
 }
 
 pub trait ItemHandle: 'static + Send {
-    fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
     fn subscribe_to_item_events(
         &self,
         cx: &mut WindowContext,
-        handler: Box<dyn Fn(&ItemEvent, &mut WindowContext) + Send>,
+        handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
     ) -> gpui::Subscription;
+    fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
     fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
     fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString>;
     fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement;
@@ -285,20 +289,20 @@ impl dyn ItemHandle {
 }
 
 impl<T: Item> ItemHandle for View<T> {
-    fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
-        self.focus_handle(cx)
-    }
-
     fn subscribe_to_item_events(
         &self,
         cx: &mut WindowContext,
-        handler: Box<dyn Fn(&ItemEvent, &mut WindowContext) + Send>,
+        handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
     ) -> gpui::Subscription {
         cx.subscribe(self, move |_, event, cx| {
-            handler(event, cx);
+            T::to_item_events(event, |item_event| handler(item_event, cx));
         })
     }
 
+    fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
+        self.focus_handle(cx)
+    }
+
     fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
         self.read(cx).tab_tooltip_text(cx)
     }
@@ -461,7 +465,7 @@ impl<T: Item> ItemHandle for View<T> {
                         }
                     }
 
-                    match event {
+                    T::to_item_events(event, |event| match event {
                         ItemEvent::CloseItem => {
                             pane.update(cx, |pane, cx| {
                                 pane.close_item_by_id(item.item_id(), crate::SaveIntent::Close, cx)
@@ -489,7 +493,7 @@ impl<T: Item> ItemHandle for View<T> {
                         }
 
                         _ => {}
-                    }
+                    });
                 }));
 
             cx.on_blur(&self.focus_handle(cx), move |workspace, cx| {
@@ -655,12 +659,7 @@ pub enum FollowEvent {
     Unfollow,
 }
 
-pub trait FollowableEvents {
-    fn to_follow_event(&self) -> Option<FollowEvent>;
-}
-
 pub trait FollowableItem: Item {
-    type FollowableEvent: FollowableEvents;
     fn remote_id(&self) -> Option<ViewId>;
     fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
     fn from_state_proto(
@@ -670,9 +669,10 @@ pub trait FollowableItem: Item {
         state: &mut Option<proto::view::Variant>,
         cx: &mut WindowContext,
     ) -> Option<Task<Result<View<Self>>>>;
+    fn to_follow_event(event: &Self::Event) -> Option<FollowEvent>;
     fn add_event_to_update_proto(
         &self,
-        event: &Self::FollowableEvent,
+        event: &Self::Event,
         update: &mut Option<proto::update_view::Variant>,
         cx: &WindowContext,
     ) -> bool;
@@ -683,7 +683,6 @@ pub trait FollowableItem: Item {
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>>;
     fn is_project_item(&self, cx: &WindowContext) -> bool;
-
     fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
 }
 
@@ -739,10 +738,7 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
     }
 
     fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent> {
-        event
-            .downcast_ref()
-            .map(T::FollowableEvent::to_follow_event)
-            .flatten()
+        T::to_follow_event(event.downcast_ref()?)
     }
 
     fn apply_update_proto(
@@ -929,6 +925,12 @@ pub mod test {
     }
 
     impl Item for TestItem {
+        type Event = ItemEvent;
+
+        fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
+            f(*event)
+        }
+
         fn tab_description(&self, detail: usize, _: &AppContext) -> Option<SharedString> {
             self.tab_descriptions.as_ref().and_then(|descriptions| {
                 let description = *descriptions.get(detail).or_else(|| descriptions.last())?;

crates/workspace2/src/shared_screen.rs 🔗

@@ -59,7 +59,6 @@ impl SharedScreen {
 }
 
 impl EventEmitter<Event> for SharedScreen {}
-impl EventEmitter<ItemEvent> for SharedScreen {}
 
 impl FocusableView for SharedScreen {
     fn focus_handle(&self, _: &AppContext) -> FocusHandle {
@@ -79,9 +78,12 @@ impl Render for SharedScreen {
 }
 
 impl Item for SharedScreen {
+    type Event = Event;
+
     fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
         Some(format!("{}'s screen", self.user.github_login).into())
     }
+
     fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
         if let Some(nav_history) = self.nav_history.as_mut() {
             nav_history.push::<()>(None, cx);
@@ -111,4 +113,10 @@ impl Item for SharedScreen {
         let track = self.track.upgrade()?;
         Some(cx.build_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx)))
     }
+
+    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
+        match event {
+            Event::Close => f(ItemEvent::CloseItem),
+        }
+    }
 }

crates/workspace2/src/workspace2.rs 🔗

@@ -2625,8 +2625,6 @@ impl Workspace {
         update: proto::UpdateFollowers,
         cx: &mut AsyncWindowContext,
     ) -> Result<()> {
-        dbg!("process_leader_update", &update);
-
         match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
             proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
                 this.update(cx, |this, _| {
@@ -3880,8 +3878,6 @@ impl WorkspaceStore {
         let leader_id = envelope.original_sender_id()?;
         let update = envelope.payload;
 
-        dbg!("handle_upate_followers");
-
         this.update(&mut cx, |this, cx| {
             for workspace in &this.workspaces {
                 workspace.update(cx, |workspace, cx| {