From 33a808a49b61f2efe6e221b305b3f6028ab420c2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Nov 2023 20:41:09 +0100 Subject: [PATCH 01/59] WIP --- crates/gpui2/src/element.rs | 88 ++++------------------ crates/gpui2/src/elements/div.rs | 91 +++++++---------------- crates/gpui2/src/elements/img.rs | 13 +--- crates/gpui2/src/elements/svg.rs | 13 +--- crates/gpui2/src/elements/text.rs | 16 +--- crates/gpui2/src/elements/uniform_list.rs | 4 +- crates/gpui2/src/view.rs | 87 ++++++++-------------- crates/gpui2/src/window.rs | 16 +++- 8 files changed, 96 insertions(+), 232 deletions(-) diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 9db256a572..3ee829df52 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -10,21 +10,12 @@ pub trait Element { fn element_id(&self) -> Option; - /// Called to initialize this element for the current frame. If this - /// element had state in a previous frame, it will be passed in for the 3rd argument. - fn initialize( - &mut self, - view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, - ) -> Self::ElementState; - fn layout( &mut self, view_state: &mut V, - element_state: &mut Self::ElementState, + previous_element_state: Option, cx: &mut ViewContext, - ) -> LayoutId; + ) -> (LayoutId, Self::ElementState); fn paint( &mut self, @@ -96,7 +87,6 @@ pub trait ParentComponent { } trait ElementObject { - fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext); fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId; fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext); fn measure( @@ -123,9 +113,6 @@ struct RenderedElement> { enum ElementRenderPhase { #[default] Start, - Initialized { - frame_state: Option, - }, LayoutRequested { layout_id: LayoutId, frame_state: Option, @@ -157,42 +144,19 @@ where E: Element, E::ElementState: 'static, { - fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext) { - let frame_state = if let Some(id) = self.element.element_id() { - cx.with_element_state(id, |element_state, cx| { - let element_state = self.element.initialize(view_state, element_state, cx); - ((), element_state) - }); - None - } else { - let frame_state = self.element.initialize(view_state, None, cx); - Some(frame_state) - }; - - self.phase = ElementRenderPhase::Initialized { frame_state }; - } - fn layout(&mut self, state: &mut V, cx: &mut ViewContext) -> LayoutId { - let layout_id; - let mut frame_state; - match mem::take(&mut self.phase) { - ElementRenderPhase::Initialized { - frame_state: initial_frame_state, - } => { - frame_state = initial_frame_state; + let (layout_id, frame_state) = match mem::take(&mut self.phase) { + ElementRenderPhase::Start => { if let Some(id) = self.element.element_id() { - layout_id = cx.with_element_state(id, |element_state, cx| { - let mut element_state = element_state.unwrap(); - let layout_id = self.element.layout(state, &mut element_state, cx); - (layout_id, element_state) + let layout_id = cx.with_element_state(id, |element_state, cx| { + self.element.layout(state, element_state, cx) }); + (layout_id, None) } else { - layout_id = self - .element - .layout(state, frame_state.as_mut().unwrap(), cx); + let (layout_id, frame_state) = self.element.layout(state, None, cx); + (layout_id, Some(frame_state)) } } - ElementRenderPhase::Start => panic!("must call initialize before layout"), ElementRenderPhase::LayoutRequested { .. } | ElementRenderPhase::LayoutComputed { .. } | ElementRenderPhase::Painted { .. } => { @@ -244,10 +208,6 @@ where cx: &mut ViewContext, ) -> Size { if matches!(&self.phase, ElementRenderPhase::Start) { - self.initialize(view_state, cx); - } - - if matches!(&self.phase, ElementRenderPhase::Initialized { .. }) { self.layout(view_state, cx); } @@ -290,10 +250,7 @@ where cx: &mut ViewContext, ) { self.measure(available_space, view_state, cx); - // Ignore the element offset when drawing this element, as the origin is already specified - // in absolute terms. - origin -= cx.element_offset(); - cx.with_element_offset(origin, |cx| self.paint(view_state, cx)) + cx.with_absolute_element_offset(origin, |cx| self.paint(view_state, cx)) } } @@ -309,10 +266,6 @@ impl AnyElement { AnyElement(Box::new(RenderedElement::new(element))) } - pub fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext) { - self.0.initialize(view_state, cx); - } - pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId { self.0.layout(view_state, cx) } @@ -393,25 +346,16 @@ where None } - fn initialize( - &mut self, - view_state: &mut V, - _rendered_element: Option, - cx: &mut ViewContext, - ) -> Self::ElementState { - let render = self.take().unwrap(); - let mut rendered_element = (render)(view_state, cx).render(); - rendered_element.initialize(view_state, cx); - rendered_element - } - fn layout( &mut self, view_state: &mut V, - rendered_element: &mut Self::ElementState, + _: Option, cx: &mut ViewContext, - ) -> LayoutId { - rendered_element.layout(view_state, cx) + ) -> (LayoutId, Self::ElementState) { + let render = self.take().unwrap(); + let mut rendered_element = (render)(view_state, cx).render(); + let layout_id = rendered_element.layout(view_state, cx); + (layout_id, rendered_element) } fn paint( diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 1f9b2b020a..3a3ad5936e 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -617,46 +617,36 @@ impl Element for Div { self.interactivity.element_id.clone() } - fn initialize( + fn layout( &mut self, view_state: &mut V, element_state: Option, cx: &mut ViewContext, - ) -> Self::ElementState { - let interactive_state = self - .interactivity - .initialize(element_state.map(|s| s.interactive_state), cx); - - for child in &mut self.children { - child.initialize(view_state, cx); - } - - DivState { - interactive_state, - child_layout_ids: SmallVec::new(), - } - } - - fn layout( - &mut self, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) -> crate::LayoutId { + ) -> (LayoutId, Self::ElementState) { + let mut child_layout_ids = SmallVec::new(); let mut interactivity = mem::take(&mut self.interactivity); - let layout_id = - interactivity.layout(&mut element_state.interactive_state, cx, |style, cx| { + let (layout_id, interactive_state) = interactivity.layout( + element_state.map(|s| s.interactive_state), + cx, + |style, cx| { cx.with_text_style(style.text_style().cloned(), |cx| { - element_state.child_layout_ids = self + child_layout_ids = self .children .iter_mut() .map(|child| child.layout(view_state, cx)) .collect::>(); - cx.request_layout(&style, element_state.child_layout_ids.iter().copied()) + cx.request_layout(&style, child_layout_ids.iter().copied()) }) - }); + }, + ); self.interactivity = interactivity; - layout_id + ( + layout_id, + DivState { + interactive_state, + child_layout_ids, + }, + ) } fn paint( @@ -766,11 +756,12 @@ impl Interactivity where V: 'static, { - pub fn initialize( + pub fn layout( &mut self, element_state: Option, cx: &mut ViewContext, - ) -> InteractiveElementState { + f: impl FnOnce(Style, &mut ViewContext) -> LayoutId, + ) -> (LayoutId, InteractiveElementState) { let mut element_state = element_state.unwrap_or_default(); // Ensure we store a focus handle in our element state if we're focusable. @@ -785,17 +776,9 @@ where }); } - element_state - } - - pub fn layout( - &mut self, - element_state: &mut InteractiveElementState, - cx: &mut ViewContext, - f: impl FnOnce(Style, &mut ViewContext) -> LayoutId, - ) -> LayoutId { - let style = self.compute_style(None, element_state, cx); - f(style, cx) + let style = self.compute_style(None, &mut element_state, cx); + let layout_id = f(style, cx); + (layout_id, element_state) } pub fn paint( @@ -1327,21 +1310,12 @@ where self.element.element_id() } - fn initialize( + fn layout( &mut self, view_state: &mut V, element_state: Option, cx: &mut ViewContext, - ) -> Self::ElementState { - self.element.initialize(view_state, element_state, cx) - } - - fn layout( - &mut self, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) -> LayoutId { + ) -> (LayoutId, Self::ElementState) { self.element.layout(view_state, element_state, cx) } @@ -1422,21 +1396,12 @@ where self.element.element_id() } - fn initialize( + fn layout( &mut self, view_state: &mut V, element_state: Option, cx: &mut ViewContext, - ) -> Self::ElementState { - self.element.initialize(view_state, element_state, cx) - } - - fn layout( - &mut self, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) -> LayoutId { + ) -> (LayoutId, Self::ElementState) { self.element.layout(view_state, element_state, cx) } diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index c5c5fb628e..1080135fe1 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -48,21 +48,12 @@ impl Element for Img { self.interactivity.element_id.clone() } - fn initialize( + fn layout( &mut self, _view_state: &mut V, element_state: Option, cx: &mut ViewContext, - ) -> Self::ElementState { - self.interactivity.initialize(element_state, cx) - } - - fn layout( - &mut self, - _view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) -> LayoutId { + ) -> (LayoutId, Self::ElementState) { self.interactivity.layout(element_state, cx, |style, cx| { cx.request_layout(&style, None) }) diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index 4b441ad425..c1c7691fbf 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -37,21 +37,12 @@ impl Element for Svg { self.interactivity.element_id.clone() } - fn initialize( + fn layout( &mut self, _view_state: &mut V, element_state: Option, cx: &mut ViewContext, - ) -> Self::ElementState { - self.interactivity.initialize(element_state, cx) - } - - fn layout( - &mut self, - _view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) -> LayoutId { + ) -> (LayoutId, Self::ElementState) { self.interactivity.layout(element_state, cx, |style, cx| { cx.request_layout(&style, None) }) diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 93d087833a..1081154e7d 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -76,21 +76,13 @@ impl Element for Text { None } - fn initialize( - &mut self, - _view_state: &mut V, - element_state: Option, - _cx: &mut ViewContext, - ) -> Self::ElementState { - element_state.unwrap_or_default() - } - fn layout( &mut self, _view: &mut V, - element_state: &mut Self::ElementState, + element_state: Option, cx: &mut ViewContext, - ) -> LayoutId { + ) -> (LayoutId, Self::ElementState) { + let element_state = element_state.unwrap_or_default(); let text_system = cx.text_system().clone(); let text_style = cx.text_style(); let font_size = text_style.font_size.to_pixels(cx.rem_size()); @@ -148,7 +140,7 @@ impl Element for Text { } }); - layout_id + (layout_id, element_state) } fn paint( diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 340a2cbf87..9736139619 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -131,9 +131,9 @@ impl Element for UniformList { fn layout( &mut self, _view_state: &mut V, - element_state: &mut Self::ElementState, + element_state: Option, cx: &mut ViewContext, - ) -> LayoutId { + ) -> (LayoutId, Self::ElementState) { let max_items = self.item_count; let item_size = element_state.item_size; let rem_size = cx.rem_size(); diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index 3edaa900b0..1ce1c4d349 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -1,7 +1,7 @@ use crate::{ private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, - Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId, Model, Pixels, - Size, ViewContext, VisualContext, WeakModel, WindowContext, + BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId, + Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel, WindowContext, }; use anyhow::{Context, Result}; use std::{ @@ -155,8 +155,7 @@ impl Eq for WeakView {} #[derive(Clone, Debug)] pub struct AnyView { model: AnyModel, - initialize: fn(&AnyView, &mut WindowContext) -> AnyBox, - layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId, + layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box), paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), } @@ -164,7 +163,6 @@ impl AnyView { pub fn downgrade(&self) -> AnyWeakView { AnyWeakView { model: self.model.downgrade(), - initialize: self.initialize, layout: self.layout, paint: self.paint, } @@ -175,7 +173,6 @@ impl AnyView { Ok(model) => Ok(View { model }), Err(model) => Err(Self { model, - initialize: self.initialize, layout: self.layout, paint: self.paint, }), @@ -186,13 +183,19 @@ impl AnyView { self.model.entity_type } - pub(crate) fn draw(&self, available_space: Size, cx: &mut WindowContext) { - let mut rendered_element = (self.initialize)(self, cx); - let layout_id = (self.layout)(self, &mut rendered_element, cx); - cx.window - .layout_engine - .compute_layout(layout_id, available_space); - (self.paint)(self, &mut rendered_element, cx); + pub(crate) fn draw( + &self, + origin: Point, + available_space: Size, + cx: &mut WindowContext, + ) { + cx.with_absolute_element_offset(origin, |cx| { + let (layout_id, mut rendered_element) = (self.layout)(self, cx); + cx.window + .layout_engine + .compute_layout(layout_id, available_space); + (self.paint)(self, &mut rendered_element, cx); + }) } } @@ -206,7 +209,6 @@ impl From> for AnyView { fn from(value: View) -> Self { AnyView { model: value.model.into_any(), - initialize: any_view::initialize::, layout: any_view::layout::, paint: any_view::paint::, } @@ -220,21 +222,12 @@ impl Element for AnyView { Some(self.model.entity_id.into()) } - fn initialize( - &mut self, - _view_state: &mut ParentViewState, - _element_state: Option, - cx: &mut ViewContext, - ) -> Self::ElementState { - (self.initialize)(self, cx) - } - fn layout( &mut self, _view_state: &mut ParentViewState, - rendered_element: &mut Self::ElementState, + rendered_element: Option, cx: &mut ViewContext, - ) -> LayoutId { + ) -> (LayoutId, Self::ElementState) { (self.layout)(self, rendered_element, cx) } @@ -251,8 +244,7 @@ impl Element for AnyView { pub struct AnyWeakView { model: AnyWeakModel, - initialize: fn(&AnyView, &mut WindowContext) -> AnyBox, - layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId, + layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box), paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), } @@ -261,7 +253,6 @@ impl AnyWeakView { let model = self.model.upgrade()?; Some(AnyView { model, - initialize: self.initialize, layout: self.layout, paint: self.paint, }) @@ -272,7 +263,6 @@ impl From> for AnyWeakView { fn from(view: WeakView) -> Self { Self { model: view.model.into(), - initialize: any_view::initialize::, layout: any_view::layout::, paint: any_view::paint::, } @@ -319,28 +309,19 @@ where Some(self.view.entity_id().into()) } - fn initialize( + fn layout( &mut self, _: &mut ParentViewState, _: Option, cx: &mut ViewContext, - ) -> Self::ElementState { + ) -> (LayoutId, Self::ElementState) { self.view.update(cx, |view, cx| { let mut element = self.component.take().unwrap().render(); - element.initialize(view, cx); - element + let layout_id = element.layout(view, cx); + (layout_id, element) }) } - fn layout( - &mut self, - _: &mut ParentViewState, - element: &mut Self::ElementState, - cx: &mut ViewContext, - ) -> LayoutId { - self.view.update(cx, |view, cx| element.layout(view, cx)) - } - fn paint( &mut self, _: Bounds, @@ -356,27 +337,17 @@ mod any_view { use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext}; use std::any::Any; - pub(crate) fn initialize(view: &AnyView, cx: &mut WindowContext) -> Box { - cx.with_element_id(Some(view.model.entity_id), |cx| { - let view = view.clone().downcast::().unwrap(); - let element = view.update(cx, |view, cx| { - let mut element = AnyElement::new(view.render(cx)); - element.initialize(view, cx); - element - }); - Box::new(element) - }) - } - pub(crate) fn layout( view: &AnyView, - element: &mut Box, cx: &mut WindowContext, - ) -> LayoutId { + ) -> (LayoutId, Box) { cx.with_element_id(Some(view.model.entity_id), |cx| { let view = view.clone().downcast::().unwrap(); - let element = element.downcast_mut::>().unwrap(); - view.update(cx, |view, cx| element.layout(view, cx)) + view.update(cx, |view, cx| { + let mut element = AnyElement::new(view.render(cx)); + let layout_id = element.layout(view, cx); + (layout_id, Box::new(element) as Box) + }) }) } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 0563c107c0..9c19512871 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1633,8 +1633,8 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { } } - /// Update the global element offset based on the given offset. This is used to implement - /// scrolling and position drag handles. + /// Update the global element offset relative to the current offset. This is used to implement + /// scrolling. fn with_element_offset( &mut self, offset: Point, @@ -1644,7 +1644,17 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { return f(self); }; - let offset = self.element_offset() + offset; + let abs_offset = self.element_offset() + offset; + self.with_absolute_element_offset(abs_offset, f) + } + + /// Update the global element offset based on the given offset. This is used to implement + /// drag handles and other manual painting of elements. + fn with_absolute_element_offset( + &mut self, + offset: Point, + f: impl FnOnce(&mut Self) -> R, + ) -> R { self.window_mut() .current_frame .element_offset_stack From c6b374ebc9ba9efe852bd2111da2cb0aaca9bbaf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Nov 2023 14:11:19 -0700 Subject: [PATCH 02/59] Remove initialize method from Element trait --- crates/editor2/src/element.rs | 14 ++-- crates/gpui2/src/element.rs | 2 +- crates/gpui2/src/elements/uniform_list.rs | 84 ++++++++++------------- crates/gpui2/src/view.rs | 4 +- crates/gpui2/src/window.rs | 20 +++--- 5 files changed, 53 insertions(+), 71 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 4d9a516f2b..de1b6f0622 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2398,21 +2398,14 @@ impl Element for EditorElement { Some(self.editor_id.into()) } - fn initialize( + fn layout( &mut self, editor: &mut Editor, element_state: Option, cx: &mut gpui::ViewContext, - ) -> Self::ElementState { + ) -> (gpui::LayoutId, Self::ElementState) { editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this. - } - fn layout( - &mut self, - editor: &mut Editor, - element_state: &mut Self::ElementState, - cx: &mut gpui::ViewContext, - ) -> gpui::LayoutId { let rem_size = cx.rem_size(); let mut style = Style::default(); style.size.width = relative(1.).into(); @@ -2421,7 +2414,8 @@ impl Element for EditorElement { EditorMode::AutoHeight { .. } => todo!(), EditorMode::Full => relative(1.).into(), }; - cx.request_layout(&style, None) + let layout_id = cx.request_layout(&style, None); + (layout_id, ()) } fn paint( diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 3ee829df52..221eb903fd 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -244,7 +244,7 @@ where fn draw( &mut self, - mut origin: Point, + origin: Point, available_space: Size, view_state: &mut V, cx: &mut ViewContext, diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 9736139619..773f9ec8aa 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -108,62 +108,54 @@ impl Element for UniformList { Some(self.id.clone()) } - fn initialize( + fn layout( &mut self, view_state: &mut V, element_state: Option, cx: &mut ViewContext, - ) -> Self::ElementState { - if let Some(mut element_state) = element_state { - element_state.interactive = self - .interactivity - .initialize(Some(element_state.interactive), cx); - element_state - } else { - let item_size = self.measure_item(view_state, None, cx); - UniformListState { - interactive: self.interactivity.initialize(None, cx), - item_size, - } - } - } - - fn layout( - &mut self, - _view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, ) -> (LayoutId, Self::ElementState) { let max_items = self.item_count; - let item_size = element_state.item_size; let rem_size = cx.rem_size(); + let item_size = element_state + .as_ref() + .map(|s| s.item_size) + .unwrap_or_else(|| self.measure_item(view_state, None, cx)); - self.interactivity - .layout(&mut element_state.interactive, cx, |style, cx| { - cx.request_measured_layout( - style, - rem_size, - move |known_dimensions: Size>, - available_space: Size| { - let desired_height = item_size.height * max_items; - let width = known_dimensions - .width - .unwrap_or(match available_space.width { - AvailableSpace::Definite(x) => x, + let (layout_id, interactive) = + self.interactivity + .layout(element_state.map(|s| s.interactive), cx, |style, cx| { + cx.request_measured_layout( + style, + rem_size, + move |known_dimensions: Size>, + available_space: Size| { + let desired_height = item_size.height * max_items; + let width = + known_dimensions + .width + .unwrap_or(match available_space.width { + AvailableSpace::Definite(x) => x, + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + item_size.width + } + }); + let height = match available_space.height { + AvailableSpace::Definite(x) => desired_height.min(x), AvailableSpace::MinContent | AvailableSpace::MaxContent => { - item_size.width + desired_height } - }); - let height = match available_space.height { - AvailableSpace::Definite(x) => desired_height.min(x), - AvailableSpace::MinContent | AvailableSpace::MaxContent => { - desired_height - } - }; - size(width, height) - }, - ) - }) + }; + size(width, height) + }, + ) + }); + + let element_state = UniformListState { + interactive, + item_size, + }; + + (layout_id, element_state) } fn paint( diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index 1ce1c4d349..c6ae9240ab 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -225,10 +225,10 @@ impl Element for AnyView { fn layout( &mut self, _view_state: &mut ParentViewState, - rendered_element: Option, + _element_state: Option, cx: &mut ViewContext, ) -> (LayoutId, Self::ElementState) { - (self.layout)(self, rendered_element, cx) + (self.layout)(self, cx) } fn paint( diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 9c19512871..2f223e8314 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1076,26 +1076,22 @@ impl<'a> WindowContext<'a> { self.with_z_index(0, |cx| { let available_space = cx.window.viewport_size.map(Into::into); - root_view.draw(available_space, cx); + root_view.draw(Point::zero(), available_space, cx); }); if let Some(active_drag) = self.app.active_drag.take() { self.with_z_index(1, |cx| { let offset = cx.mouse_position() - active_drag.cursor_offset; - cx.with_element_offset(offset, |cx| { - let available_space = - size(AvailableSpace::MinContent, AvailableSpace::MinContent); - active_drag.view.draw(available_space, cx); - cx.active_drag = Some(active_drag); - }); + let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); + active_drag.view.draw(offset, available_space, cx); + cx.active_drag = Some(active_drag); }); } else if let Some(active_tooltip) = self.app.active_tooltip.take() { self.with_z_index(1, |cx| { - cx.with_element_offset(active_tooltip.cursor_offset, |cx| { - let available_space = - size(AvailableSpace::MinContent, AvailableSpace::MinContent); - active_tooltip.view.draw(available_space, cx); - }); + let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); + active_tooltip + .view + .draw(active_tooltip.cursor_offset, available_space, cx); }); } From 4f096333793ad83577769b15a9203560aac67cfd Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Nov 2023 14:17:49 -0700 Subject: [PATCH 03/59] Remove focus_in styling helper --- crates/gpui2/src/elements/div.rs | 14 -------------- crates/storybook2/src/stories/focus.rs | 1 - 2 files changed, 15 deletions(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 3a3ad5936e..e098e8ef1a 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -437,14 +437,6 @@ pub trait FocusableComponent: InteractiveComponent { self } - fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.interactivity().focus_in_style = f(StyleRefinement::default()); - self - } - fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, @@ -730,7 +722,6 @@ pub struct Interactivity { pub group: Option, pub base_style: StyleRefinement, pub focus_style: StyleRefinement, - pub focus_in_style: StyleRefinement, pub in_focus_style: StyleRefinement, pub hover_style: StyleRefinement, pub group_hover_style: Option, @@ -1113,10 +1104,6 @@ where style.refine(&self.base_style); if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { - if focus_handle.contains_focused(cx) { - style.refine(&self.focus_in_style); - } - if focus_handle.within_focused(cx) { style.refine(&self.in_focus_style); } @@ -1189,7 +1176,6 @@ impl Default for Interactivity { group: None, base_style: StyleRefinement::default(), focus_style: StyleRefinement::default(), - focus_in_style: StyleRefinement::default(), in_focus_style: StyleRefinement::default(), hover_style: StyleRefinement::default(), group_hover_style: None, diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index a8794afdb8..571882f1f2 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -57,7 +57,6 @@ impl Render for FocusStory { .size_full() .bg(color_1) .focus(|style| style.bg(color_2)) - .focus_in(|style| style.bg(color_3)) .child( div() .track_focus(&self.child_1_focus) From faf93aed4e8a464017fabd0633d9950e13d26a8c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 15 Nov 2023 14:17:04 -0800 Subject: [PATCH 04/59] checkpoint --- crates/gpui2/src/action.rs | 2 +- crates/settings2/src/keymap_file.rs | 6 ++--- crates/workspace2/src/pane.rs | 41 +++++++++++------------------ crates/workspace2/src/workspace2.rs | 10 +++---- 4 files changed, 24 insertions(+), 35 deletions(-) diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index dbb510b1c8..a81bcfcdbc 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -68,7 +68,7 @@ pub trait Action: std::fmt::Debug + 'static { // Types become actions by satisfying a list of trait bounds. impl Action for A where - A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static, + A: for<'a> Deserialize<'a> + PartialEq + Default + Clone + std::fmt::Debug + 'static, { fn qualified_name() -> SharedString { let name = type_name::(); diff --git a/crates/settings2/src/keymap_file.rs b/crates/settings2/src/keymap_file.rs index 2b57af0fdb..9f279864ee 100644 --- a/crates/settings2/src/keymap_file.rs +++ b/crates/settings2/src/keymap_file.rs @@ -9,7 +9,7 @@ use schemars::{ }; use serde::Deserialize; use serde_json::Value; -use util::asset_str; +use util::{asset_str, ResultExt}; #[derive(Debug, Deserialize, Default, Clone, JsonSchema)] #[serde(transparent)] @@ -86,9 +86,7 @@ impl KeymapFile { "invalid binding value for keystroke {keystroke}, context {context:?}" ) }) - // todo!() - .ok() - // .log_err() + .log_err() .map(|action| KeyBinding::load(&keystroke, action, context.as_deref())) }) .collect::>>()?; diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 668ce2f207..7f3658260c 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1398,6 +1398,7 @@ impl Pane { .when_some(item.tab_tooltip_text(cx), |div, text| { div.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(text.clone()))) }) + .on_click(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx)) // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) // .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) // .on_drop(|_view, state: View, cx| { @@ -1430,32 +1431,22 @@ impl Pane { .items_center() .gap_1() .text_color(text_color) - .children(if item.has_conflict(cx) { - Some( - IconElement::new(Icon::ExclamationTriangle) - .size(ui::IconSize::Small) - .color(TextColor::Warning), - ) - } else if item.is_dirty(cx) { - Some( - IconElement::new(Icon::ExclamationTriangle) - .size(ui::IconSize::Small) - .color(TextColor::Info), - ) - } else { - None - }) - .children(if !close_right { - Some(close_icon()) - } else { - None - }) + .children( + item.has_conflict(cx) + .then(|| { + IconElement::new(Icon::ExclamationTriangle) + .size(ui::IconSize::Small) + .color(TextColor::Warning) + }) + .or(item.is_dirty(cx).then(|| { + IconElement::new(Icon::ExclamationTriangle) + .size(ui::IconSize::Small) + .color(TextColor::Info) + })), + ) + .children((!close_right).then(|| close_icon())) .child(label) - .children(if close_right { - Some(close_icon()) - } else { - None - }), + .children(close_right.then(|| close_icon())), ) } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 6c2d0c0ede..6a26fbdb5c 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -36,11 +36,11 @@ use futures::{ Future, FutureExt, StreamExt, }; use gpui::{ - actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView, - AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, - EventEmitter, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent, Point, Render, - Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds, WindowContext, - WindowHandle, WindowOptions, + actions, div, point, prelude::*, register_action, rems, size, Action, AnyModel, AnyView, + AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, + EntityId, EventEmitter, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent, Point, + Render, Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds, + WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; From 84bcbf11284dce95b14f23795d029f069153b940 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 15 Nov 2023 14:24:01 -0700 Subject: [PATCH 05/59] Add collab_ui2 --- Cargo.lock | 41 + crates/collab_ui2/Cargo.toml | 80 + crates/collab_ui2/src/channel_view.rs | 454 +++ crates/collab_ui2/src/chat_panel.rs | 983 +++++ .../src/chat_panel/message_editor.rs | 313 ++ crates/collab_ui2/src/collab_panel.rs | 3548 +++++++++++++++++ .../src/collab_panel/channel_modal.rs | 717 ++++ .../src/collab_panel/contact_finder.rs | 261 ++ crates/collab_ui2/src/collab_titlebar_item.rs | 1278 ++++++ crates/collab_ui2/src/collab_ui.rs | 165 + crates/collab_ui2/src/face_pile.rs | 113 + crates/collab_ui2/src/notification_panel.rs | 884 ++++ crates/collab_ui2/src/notifications.rs | 11 + .../incoming_call_notification.rs | 213 + .../project_shared_notification.rs | 217 + crates/collab_ui2/src/panel_settings.rs | 69 + crates/zed2/Cargo.toml | 2 +- 17 files changed, 9348 insertions(+), 1 deletion(-) create mode 100644 crates/collab_ui2/Cargo.toml create mode 100644 crates/collab_ui2/src/channel_view.rs create mode 100644 crates/collab_ui2/src/chat_panel.rs create mode 100644 crates/collab_ui2/src/chat_panel/message_editor.rs create mode 100644 crates/collab_ui2/src/collab_panel.rs create mode 100644 crates/collab_ui2/src/collab_panel/channel_modal.rs create mode 100644 crates/collab_ui2/src/collab_panel/contact_finder.rs create mode 100644 crates/collab_ui2/src/collab_titlebar_item.rs create mode 100644 crates/collab_ui2/src/collab_ui.rs create mode 100644 crates/collab_ui2/src/face_pile.rs create mode 100644 crates/collab_ui2/src/notification_panel.rs create mode 100644 crates/collab_ui2/src/notifications.rs create mode 100644 crates/collab_ui2/src/notifications/incoming_call_notification.rs create mode 100644 crates/collab_ui2/src/notifications/project_shared_notification.rs create mode 100644 crates/collab_ui2/src/panel_settings.rs diff --git a/Cargo.lock b/Cargo.lock index 69f402f2e7..de80a9a7b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1829,6 +1829,46 @@ dependencies = [ "zed-actions", ] +[[package]] +name = "collab_ui2" +version = "0.1.0" +dependencies = [ + "anyhow", + "call2", + "channel2", + "client2", + "clock", + "collections", + "db2", + "editor2", + "feature_flags2", + "futures 0.3.28", + "fuzzy", + "gpui2", + "language2", + "lazy_static", + "log", + "menu2", + "notifications2", + "picker2", + "postage", + "pretty_assertions", + "project2", + "rich_text2", + "rpc2", + "schemars", + "serde", + "serde_derive", + "settings2", + "smallvec", + "theme2", + "time", + "tree-sitter-markdown", + "util", + "workspace2", + "zed_actions2", +] + [[package]] name = "collections" version = "0.1.0" @@ -11441,6 +11481,7 @@ dependencies = [ "chrono", "cli", "client2", + "collab_ui2", "collections", "command_palette2", "copilot2", diff --git a/crates/collab_ui2/Cargo.toml b/crates/collab_ui2/Cargo.toml new file mode 100644 index 0000000000..8c48e09846 --- /dev/null +++ b/crates/collab_ui2/Cargo.toml @@ -0,0 +1,80 @@ +[package] +name = "collab_ui2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/collab_ui.rs" +doctest = false + +[features] +test-support = [ + "call/test-support", + "client/test-support", + "collections/test-support", + "editor/test-support", + "gpui/test-support", + "project/test-support", + "settings/test-support", + "util/test-support", + "workspace/test-support", +] + +[dependencies] +# auto_update = { path = "../auto_update" } +db = { package = "db2", path = "../db2" } +call = { package = "call2", path = "../call2" } +client = { package = "client2", path = "../client2" } +channel = { package = "channel2", path = "../channel2" } +clock = { path = "../clock" } +collections = { path = "../collections" } +# context_menu = { path = "../context_menu" } +# drag_and_drop = { path = "../drag_and_drop" } +editor = { package="editor2", path = "../editor2" } +#feedback = { path = "../feedback" } +fuzzy = { path = "../fuzzy" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +menu = { package = "menu2", path = "../menu2" } +notifications = { package = "notifications2", path = "../notifications2" } +rich_text = { package = "rich_text2", path = "../rich_text2" } +picker = { package = "picker2", path = "../picker2" } +project = { package = "project2", path = "../project2" } +# recent_projects = { path = "../recent_projects" } +rpc = { package ="rpc2", path = "../rpc2" } +settings = { package = "settings2", path = "../settings2" } +feature_flags = { package = "feature_flags2", path = "../feature_flags2"} +theme = { package = "theme2", path = "../theme2" } +# theme_selector = { path = "../theme_selector" } +# vcs_menu = { path = "../vcs_menu" } +util = { path = "../util" } +workspace = { package = "workspace2", path = "../workspace2" } +zed-actions = { package="zed_actions2", path = "../zed_actions2"} + +anyhow.workspace = true +futures.workspace = true +lazy_static.workspace = true +log.workspace = true +schemars.workspace = true +postage.workspace = true +serde.workspace = true +serde_derive.workspace = true +time.workspace = true +smallvec.workspace = true + +[dev-dependencies] +call = { package = "call2", path = "../call2", features = ["test-support"] } +client = { package = "client2", path = "../client2", features = ["test-support"] } +collections = { path = "../collections", features = ["test-support"] } +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +notifications = { package = "notifications2", path = "../notifications2", features = ["test-support"] } +project = { package = "project2", path = "../project2", features = ["test-support"] } +rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] } +settings = { package = "settings2", path = "../settings2", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } + +pretty_assertions.workspace = true +tree-sitter-markdown.workspace = true diff --git a/crates/collab_ui2/src/channel_view.rs b/crates/collab_ui2/src/channel_view.rs new file mode 100644 index 0000000000..fe46f3bb3e --- /dev/null +++ b/crates/collab_ui2/src/channel_view.rs @@ -0,0 +1,454 @@ +use anyhow::{anyhow, Result}; +use call::report_call_event_for_channel; +use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore}; +use client::{ + proto::{self, PeerId}, + Collaborator, ParticipantIndex, +}; +use collections::HashMap; +use editor::{CollaborationHub, Editor}; +use gpui::{ + actions, + elements::{ChildView, Label}, + geometry::vector::Vector2F, + AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View, + ViewContext, ViewHandle, +}; +use project::Project; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + sync::Arc, +}; +use util::ResultExt; +use workspace::{ + item::{FollowableItem, Item, ItemEvent, ItemHandle}, + register_followable_item, + searchable::SearchableItemHandle, + ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId, +}; + +actions!(channel_view, [Deploy]); + +pub fn init(cx: &mut AppContext) { + register_followable_item::(cx) +} + +pub struct ChannelView { + pub editor: ViewHandle, + project: ModelHandle, + channel_store: ModelHandle, + channel_buffer: ModelHandle, + remote_id: Option, + _editor_event_subscription: Subscription, +} + +impl ChannelView { + pub fn open( + channel_id: ChannelId, + workspace: ViewHandle, + cx: &mut AppContext, + ) -> Task>> { + let pane = workspace.read(cx).active_pane().clone(); + let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx); + cx.spawn(|mut cx| async move { + let channel_view = channel_view.await?; + pane.update(&mut cx, |pane, cx| { + report_call_event_for_channel( + "open channel notes", + channel_id, + &workspace.read(cx).app_state().client, + cx, + ); + pane.add_item(Box::new(channel_view.clone()), true, true, None, cx); + }); + anyhow::Ok(channel_view) + }) + } + + pub fn open_in_pane( + channel_id: ChannelId, + pane: ViewHandle, + workspace: ViewHandle, + cx: &mut AppContext, + ) -> Task>> { + let workspace = workspace.read(cx); + let project = workspace.project().to_owned(); + let channel_store = ChannelStore::global(cx); + let language_registry = workspace.app_state().languages.clone(); + let markdown = language_registry.language_for_name("Markdown"); + let channel_buffer = + channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx)); + + cx.spawn(|mut cx| async move { + let channel_buffer = channel_buffer.await?; + let markdown = markdown.await.log_err(); + + channel_buffer.update(&mut cx, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.set_language_registry(language_registry); + if let Some(markdown) = markdown { + buffer.set_language(Some(markdown), cx); + } + }) + }); + + pane.update(&mut cx, |pane, cx| { + let buffer_id = channel_buffer.read(cx).remote_id(cx); + + let existing_view = pane + .items_of_type::() + .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id); + + // If this channel buffer is already open in this pane, just return it. + if let Some(existing_view) = existing_view.clone() { + if existing_view.read(cx).channel_buffer == channel_buffer { + return existing_view; + } + } + + let view = cx.add_view(|cx| { + let mut this = Self::new(project, channel_store, channel_buffer, cx); + this.acknowledge_buffer_version(cx); + this + }); + + // If the pane contained a disconnected view for this channel buffer, + // replace that. + if let Some(existing_item) = existing_view { + if let Some(ix) = pane.index_for_item(&existing_item) { + pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx) + .detach(); + pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx); + } + } + + view + }) + .ok_or_else(|| anyhow!("pane was dropped")) + }) + } + + pub fn new( + project: ModelHandle, + channel_store: ModelHandle, + channel_buffer: ModelHandle, + cx: &mut ViewContext, + ) -> Self { + let buffer = channel_buffer.read(cx).buffer(); + let editor = cx.add_view(|cx| { + let mut editor = Editor::for_buffer(buffer, None, cx); + editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub( + channel_buffer.clone(), + ))); + editor.set_read_only( + !channel_buffer + .read(cx) + .channel(cx) + .is_some_and(|c| c.can_edit_notes()), + ); + editor + }); + let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); + + cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) + .detach(); + + Self { + editor, + project, + channel_store, + channel_buffer, + remote_id: None, + _editor_event_subscription, + } + } + + pub fn channel(&self, cx: &AppContext) -> Option> { + self.channel_buffer.read(cx).channel(cx) + } + + fn handle_channel_buffer_event( + &mut self, + _: ModelHandle, + event: &ChannelBufferEvent, + cx: &mut ViewContext, + ) { + match event { + ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| { + editor.set_read_only(true); + cx.notify(); + }), + ChannelBufferEvent::ChannelChanged => { + self.editor.update(cx, |editor, cx| { + editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes())); + cx.emit(editor::Event::TitleChanged); + cx.notify() + }); + } + ChannelBufferEvent::BufferEdited => { + if cx.is_self_focused() || self.editor.is_focused(cx) { + self.acknowledge_buffer_version(cx); + } else { + self.channel_store.update(cx, |store, cx| { + let channel_buffer = self.channel_buffer.read(cx); + store.notes_changed( + channel_buffer.channel_id, + channel_buffer.epoch(), + &channel_buffer.buffer().read(cx).version(), + cx, + ) + }); + } + } + ChannelBufferEvent::CollaboratorsChanged => {} + } + } + + fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) { + self.channel_store.update(cx, |store, cx| { + let channel_buffer = self.channel_buffer.read(cx); + store.acknowledge_notes_version( + channel_buffer.channel_id, + channel_buffer.epoch(), + &channel_buffer.buffer().read(cx).version(), + cx, + ) + }); + self.channel_buffer.update(cx, |buffer, cx| { + buffer.acknowledge_buffer_version(cx); + }); + } +} + +impl Entity for ChannelView { + type Event = editor::Event; +} + +impl View for ChannelView { + fn ui_name() -> &'static str { + "ChannelView" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + ChildView::new(self.editor.as_any(), cx).into_any() + } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + self.acknowledge_buffer_version(cx); + cx.focus(self.editor.as_any()) + } + } +} + +impl Item for ChannelView { + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a ViewHandle, + _: &'a AppContext, + ) -> Option<&'a AnyViewHandle> { + if type_id == TypeId::of::() { + Some(self_handle) + } else if type_id == TypeId::of::() { + Some(&self.editor) + } else { + None + } + } + + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + cx: &gpui::AppContext, + ) -> AnyElement { + let label = if let Some(channel) = self.channel(cx) { + match ( + channel.can_edit_notes(), + self.channel_buffer.read(cx).is_connected(), + ) { + (true, true) => format!("#{}", channel.name), + (false, true) => format!("#{} (read-only)", channel.name), + (_, false) => format!("#{} (disconnected)", channel.name), + } + } else { + format!("channel notes (disconnected)") + }; + Label::new(label, style.label.to_owned()).into_any() + } + + fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option { + Some(Self::new( + self.project.clone(), + self.channel_store.clone(), + self.channel_buffer.clone(), + cx, + )) + } + + fn is_singleton(&self, _cx: &AppContext) -> bool { + false + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + self.editor + .update(cx, |editor, cx| editor.navigate(data, cx)) + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::deactivated(editor, cx)) + } + + fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx)) + } + + fn as_searchable(&self, _: &ViewHandle) -> Option> { + Some(Box::new(self.editor.clone())) + } + + fn show_toolbar(&self) -> bool { + true + } + + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { + self.editor.read(cx).pixel_position_of_cursor(cx) + } + + fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { + editor::Editor::to_item_events(event) + } +} + +impl FollowableItem for ChannelView { + fn remote_id(&self) -> Option { + self.remote_id + } + + fn to_state_proto(&self, cx: &AppContext) -> Option { + let channel_buffer = self.channel_buffer.read(cx); + if !channel_buffer.is_connected() { + return None; + } + + Some(proto::view::Variant::ChannelView( + proto::view::ChannelView { + channel_id: channel_buffer.channel_id, + editor: if let Some(proto::view::Variant::Editor(proto)) = + self.editor.read(cx).to_state_proto(cx) + { + Some(proto) + } else { + None + }, + }, + )) + } + + fn from_state_proto( + pane: ViewHandle, + workspace: ViewHandle, + remote_id: workspace::ViewId, + state: &mut Option, + cx: &mut AppContext, + ) -> Option>>> { + let Some(proto::view::Variant::ChannelView(_)) = state else { + return None; + }; + let Some(proto::view::Variant::ChannelView(state)) = state.take() else { + unreachable!() + }; + + let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx); + + Some(cx.spawn(|mut cx| async move { + let this = open.await?; + + let task = this + .update(&mut cx, |this, cx| { + this.remote_id = Some(remote_id); + + if let Some(state) = state.editor { + Some(this.editor.update(cx, |editor, cx| { + editor.apply_update_proto( + &this.project, + proto::update_view::Variant::Editor(proto::update_view::Editor { + selections: state.selections, + pending_selection: state.pending_selection, + scroll_top_anchor: state.scroll_top_anchor, + scroll_x: state.scroll_x, + scroll_y: state.scroll_y, + ..Default::default() + }), + cx, + ) + })) + } else { + None + } + }) + .ok_or_else(|| anyhow!("window was closed"))?; + + if let Some(task) = task { + task.await?; + } + + Ok(this) + })) + } + + fn add_event_to_update_proto( + &self, + event: &Self::Event, + update: &mut Option, + cx: &AppContext, + ) -> bool { + self.editor + .read(cx) + .add_event_to_update_proto(event, update, cx) + } + + fn apply_update_proto( + &mut self, + project: &ModelHandle, + message: proto::update_view::Variant, + cx: &mut ViewContext, + ) -> gpui::Task> { + self.editor.update(cx, |editor, cx| { + editor.apply_update_proto(project, message, cx) + }) + } + + fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { + self.editor.update(cx, |editor, cx| { + editor.set_leader_peer_id(leader_peer_id, cx) + }) + } + + fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool { + Editor::should_unfollow_on_event(event, cx) + } + + fn is_project_item(&self, _cx: &AppContext) -> bool { + false + } +} + +struct ChannelBufferCollaborationHub(ModelHandle); + +impl CollaborationHub for ChannelBufferCollaborationHub { + fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { + self.0.read(cx).collaborators() + } + + fn user_participant_indices<'a>( + &self, + cx: &'a AppContext, + ) -> &'a HashMap { + self.0.read(cx).user_store().read(cx).participant_indices() + } +} diff --git a/crates/collab_ui2/src/chat_panel.rs b/crates/collab_ui2/src/chat_panel.rs new file mode 100644 index 0000000000..5a4dafb6d4 --- /dev/null +++ b/crates/collab_ui2/src/chat_panel.rs @@ -0,0 +1,983 @@ +use crate::{ + channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings, +}; +use anyhow::Result; +use call::ActiveCall; +use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore}; +use client::Client; +use collections::HashMap; +use db::kvp::KEY_VALUE_STORE; +use editor::Editor; +use gpui::{ + actions, + elements::*, + platform::{CursorStyle, MouseButton}, + serde_json, + views::{ItemType, Select, SelectStyle}, + AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, + ViewContext, ViewHandle, WeakViewHandle, +}; +use language::LanguageRegistry; +use menu::Confirm; +use message_editor::MessageEditor; +use project::Fs; +use rich_text::RichText; +use serde::{Deserialize, Serialize}; +use settings::SettingsStore; +use std::sync::Arc; +use theme::{IconButton, Theme}; +use time::{OffsetDateTime, UtcOffset}; +use util::{ResultExt, TryFutureExt}; +use workspace::{ + dock::{DockPosition, Panel}, + Workspace, +}; + +mod message_editor; + +const MESSAGE_LOADING_THRESHOLD: usize = 50; +const CHAT_PANEL_KEY: &'static str = "ChatPanel"; + +pub struct ChatPanel { + client: Arc, + channel_store: ModelHandle, + languages: Arc, + active_chat: Option<(ModelHandle, Subscription)>, + message_list: ListState, + input_editor: ViewHandle, + channel_select: ViewHandle, + ) -> AnyElement, - local_timezone: UtcOffset, - fs: Arc, - width: Option, - active: bool, - pending_serialization: Task>, - subscriptions: Vec, - workspace: WeakViewHandle, - is_scrolled_to_bottom: bool, - has_focus: bool, - markdown_data: HashMap, -} - -#[derive(Serialize, Deserialize)] -struct SerializedChatPanel { - width: Option, -} - -#[derive(Debug)] -pub enum Event { - DockPositionChanged, - Focus, - Dismissed, -} - -actions!( - chat_panel, - [LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall] -); - -pub fn init(cx: &mut AppContext) { - cx.add_action(ChatPanel::send); - cx.add_action(ChatPanel::load_more_messages); - cx.add_action(ChatPanel::open_notes); - cx.add_action(ChatPanel::join_call); -} - -impl ChatPanel { - pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { - let fs = workspace.app_state().fs.clone(); - let client = workspace.app_state().client.clone(); - let channel_store = ChannelStore::global(cx); - let languages = workspace.app_state().languages.clone(); - - let input_editor = cx.add_view(|cx| { - MessageEditor::new( - languages.clone(), - channel_store.clone(), - cx.add_view(|cx| { - Editor::auto_height( - 4, - Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())), - cx, - ) - }), - cx, - ) - }); - - let workspace_handle = workspace.weak_handle(); - - let channel_select = cx.add_view(|cx| { - let channel_store = channel_store.clone(); - let workspace = workspace_handle.clone(); - Select::new(0, cx, { - move |ix, item_type, is_hovered, cx| { - Self::render_channel_name( - &channel_store, - ix, - item_type, - is_hovered, - workspace, - cx, - ) - } - }) - .with_style(move |cx| { - let style = &theme::current(cx).chat_panel.channel_select; - SelectStyle { - header: Default::default(), - menu: style.menu, - } - }) - }); - - let mut message_list = - ListState::::new(0, Orientation::Bottom, 10., move |this, ix, cx| { - this.render_message(ix, cx) - }); - message_list.set_scroll_handler(|visible_range, count, this, cx| { - if visible_range.start < MESSAGE_LOADING_THRESHOLD { - this.load_more_messages(&LoadMoreMessages, cx); - } - this.is_scrolled_to_bottom = visible_range.end == count; - }); - - cx.add_view(|cx| { - let mut this = Self { - fs, - client, - channel_store, - languages, - active_chat: Default::default(), - pending_serialization: Task::ready(None), - message_list, - input_editor, - channel_select, - local_timezone: cx.platform().local_timezone(), - has_focus: false, - subscriptions: Vec::new(), - workspace: workspace_handle, - is_scrolled_to_bottom: true, - active: false, - width: None, - markdown_data: Default::default(), - }; - - let mut old_dock_position = this.position(cx); - this.subscriptions - .push( - cx.observe_global::(move |this: &mut Self, cx| { - let new_dock_position = this.position(cx); - if new_dock_position != old_dock_position { - old_dock_position = new_dock_position; - cx.emit(Event::DockPositionChanged); - } - cx.notify(); - }), - ); - - this.update_channel_count(cx); - cx.observe(&this.channel_store, |this, _, cx| { - this.update_channel_count(cx) - }) - .detach(); - - cx.observe(&this.channel_select, |this, channel_select, cx| { - let selected_ix = channel_select.read(cx).selected_index(); - - let selected_channel_id = this - .channel_store - .read(cx) - .channel_at(selected_ix) - .map(|e| e.id); - if let Some(selected_channel_id) = selected_channel_id { - this.select_channel(selected_channel_id, None, cx) - .detach_and_log_err(cx); - } - }) - .detach(); - - this - }) - } - - pub fn is_scrolled_to_bottom(&self) -> bool { - self.is_scrolled_to_bottom - } - - pub fn active_chat(&self) -> Option> { - self.active_chat.as_ref().map(|(chat, _)| chat.clone()) - } - - pub fn load( - workspace: WeakViewHandle, - cx: AsyncAppContext, - ) -> Task>> { - cx.spawn(|mut cx| async move { - let serialized_panel = if let Some(panel) = cx - .background() - .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) }) - .await - .log_err() - .flatten() - { - Some(serde_json::from_str::(&panel)?) - } else { - None - }; - - workspace.update(&mut cx, |workspace, cx| { - let panel = Self::new(workspace, cx); - if let Some(serialized_panel) = serialized_panel { - panel.update(cx, |panel, cx| { - panel.width = serialized_panel.width; - cx.notify(); - }); - } - panel - }) - }) - } - - fn serialize(&mut self, cx: &mut ViewContext) { - let width = self.width; - self.pending_serialization = cx.background().spawn( - async move { - KEY_VALUE_STORE - .write_kvp( - CHAT_PANEL_KEY.into(), - serde_json::to_string(&SerializedChatPanel { width })?, - ) - .await?; - anyhow::Ok(()) - } - .log_err(), - ); - } - - fn update_channel_count(&mut self, cx: &mut ViewContext) { - let channel_count = self.channel_store.read(cx).channel_count(); - self.channel_select.update(cx, |select, cx| { - select.set_item_count(channel_count, cx); - }); - } - - fn set_active_chat(&mut self, chat: ModelHandle, cx: &mut ViewContext) { - if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) { - let channel_id = chat.read(cx).channel_id; - { - self.markdown_data.clear(); - let chat = chat.read(cx); - self.message_list.reset(chat.message_count()); - - let channel_name = chat.channel(cx).map(|channel| channel.name.clone()); - self.input_editor.update(cx, |editor, cx| { - editor.set_channel(channel_id, channel_name, cx); - }); - }; - let subscription = cx.subscribe(&chat, Self::channel_did_change); - self.active_chat = Some((chat, subscription)); - self.acknowledge_last_message(cx); - self.channel_select.update(cx, |select, cx| { - if let Some(ix) = self.channel_store.read(cx).index_of_channel(channel_id) { - select.set_selected_index(ix, cx); - } - }); - cx.notify(); - } - } - - fn channel_did_change( - &mut self, - _: ModelHandle, - event: &ChannelChatEvent, - cx: &mut ViewContext, - ) { - match event { - ChannelChatEvent::MessagesUpdated { - old_range, - new_count, - } => { - self.message_list.splice(old_range.clone(), *new_count); - if self.active { - self.acknowledge_last_message(cx); - } - } - ChannelChatEvent::NewMessage { - channel_id, - message_id, - } => { - if !self.active { - self.channel_store.update(cx, |store, cx| { - store.new_message(*channel_id, *message_id, cx) - }) - } - } - } - cx.notify(); - } - - fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) { - if self.active && self.is_scrolled_to_bottom { - if let Some((chat, _)) = &self.active_chat { - chat.update(cx, |chat, cx| { - chat.acknowledge_last_message(cx); - }); - } - } - } - - fn render_channel(&self, cx: &mut ViewContext) -> AnyElement { - let theme = theme::current(cx); - Flex::column() - .with_child( - ChildView::new(&self.channel_select, cx) - .contained() - .with_style(theme.chat_panel.channel_select.container), - ) - .with_child(self.render_active_channel_messages(&theme)) - .with_child(self.render_input_box(&theme, cx)) - .into_any() - } - - fn render_active_channel_messages(&self, theme: &Arc) -> AnyElement { - let messages = if self.active_chat.is_some() { - List::new(self.message_list.clone()) - .contained() - .with_style(theme.chat_panel.list) - .into_any() - } else { - Empty::new().into_any() - }; - - messages.flex(1., true).into_any() - } - - fn render_message(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { - let (message, is_continuation, is_last, is_admin) = self - .active_chat - .as_ref() - .unwrap() - .0 - .update(cx, |active_chat, cx| { - let is_admin = self - .channel_store - .read(cx) - .is_channel_admin(active_chat.channel_id); - - let last_message = active_chat.message(ix.saturating_sub(1)); - let this_message = active_chat.message(ix).clone(); - let is_continuation = last_message.id != this_message.id - && this_message.sender.id == last_message.sender.id; - - if let ChannelMessageId::Saved(id) = this_message.id { - if this_message - .mentions - .iter() - .any(|(_, user_id)| Some(*user_id) == self.client.user_id()) - { - active_chat.acknowledge_message(id); - } - } - - ( - this_message, - is_continuation, - active_chat.message_count() == ix + 1, - is_admin, - ) - }); - - let is_pending = message.is_pending(); - let theme = theme::current(cx); - let text = self.markdown_data.entry(message.id).or_insert_with(|| { - Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message) - }); - - let now = OffsetDateTime::now_utc(); - - let style = if is_pending { - &theme.chat_panel.pending_message - } else if is_continuation { - &theme.chat_panel.continuation_message - } else { - &theme.chat_panel.message - }; - - let belongs_to_user = Some(message.sender.id) == self.client.user_id(); - let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) = - (message.id, belongs_to_user || is_admin) - { - Some(id) - } else { - None - }; - - enum MessageBackgroundHighlight {} - MouseEventHandler::new::(ix, cx, |state, cx| { - let container = style.style_for(state); - if is_continuation { - Flex::row() - .with_child( - text.element( - theme.editor.syntax.clone(), - theme.chat_panel.rich_text.clone(), - cx, - ) - .flex(1., true), - ) - .with_child(render_remove(message_id_to_remove, cx, &theme)) - .contained() - .with_style(*container) - .with_margin_bottom(if is_last { - theme.chat_panel.last_message_bottom_spacing - } else { - 0. - }) - .into_any() - } else { - Flex::column() - .with_child( - Flex::row() - .with_child( - Flex::row() - .with_child(render_avatar( - message.sender.avatar.clone(), - &theme.chat_panel.avatar, - theme.chat_panel.avatar_container, - )) - .with_child( - Label::new( - message.sender.github_login.clone(), - theme.chat_panel.message_sender.text.clone(), - ) - .contained() - .with_style(theme.chat_panel.message_sender.container), - ) - .with_child( - Label::new( - format_timestamp( - message.timestamp, - now, - self.local_timezone, - ), - theme.chat_panel.message_timestamp.text.clone(), - ) - .contained() - .with_style(theme.chat_panel.message_timestamp.container), - ) - .align_children_center() - .flex(1., true), - ) - .with_child(render_remove(message_id_to_remove, cx, &theme)) - .align_children_center(), - ) - .with_child( - Flex::row() - .with_child( - text.element( - theme.editor.syntax.clone(), - theme.chat_panel.rich_text.clone(), - cx, - ) - .flex(1., true), - ) - // Add a spacer to make everything line up - .with_child(render_remove(None, cx, &theme)), - ) - .contained() - .with_style(*container) - .with_margin_bottom(if is_last { - theme.chat_panel.last_message_bottom_spacing - } else { - 0. - }) - .into_any() - } - }) - .into_any() - } - - fn render_markdown_with_mentions( - language_registry: &Arc, - current_user_id: u64, - message: &channel::ChannelMessage, - ) -> RichText { - let mentions = message - .mentions - .iter() - .map(|(range, user_id)| rich_text::Mention { - range: range.clone(), - is_self_mention: *user_id == current_user_id, - }) - .collect::>(); - - rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None) - } - - fn render_input_box(&self, theme: &Arc, cx: &AppContext) -> AnyElement { - ChildView::new(&self.input_editor, cx) - .contained() - .with_style(theme.chat_panel.input_editor.container) - .into_any() - } - - fn render_channel_name( - channel_store: &ModelHandle, - ix: usize, - item_type: ItemType, - is_hovered: bool, - workspace: WeakViewHandle, - cx: &mut ViewContext { - let theme = theme::current(cx); - let tooltip_style = &theme.tooltip; - let theme = &theme.chat_panel; - let style = match (&item_type, is_hovered) { - (ItemType::Header, _) => &theme.channel_select.header, - (ItemType::Selected, _) => &theme.channel_select.active_item, - (ItemType::Unselected, false) => &theme.channel_select.item, - (ItemType::Unselected, true) => &theme.channel_select.hovered_item, - }; - - let channel = &channel_store.read(cx).channel_at(ix).unwrap(); - let channel_id = channel.id; - - let mut row = Flex::row() - .with_child( - Label::new("#".to_string(), style.hash.text.clone()) - .contained() - .with_style(style.hash.container), - ) - .with_child(Label::new(channel.name.clone(), style.name.clone())); - - if matches!(item_type, ItemType::Header) { - row.add_children([ - MouseEventHandler::new::(0, cx, |mouse_state, _| { - render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg") - }) - .on_click(MouseButton::Left, move |_, _, cx| { - if let Some(workspace) = workspace.upgrade(cx) { - ChannelView::open(channel_id, workspace, cx).detach(); - } - }) - .with_tooltip::( - channel_id as usize, - "Open Notes", - Some(Box::new(OpenChannelNotes)), - tooltip_style.clone(), - cx, - ) - .flex_float(), - MouseEventHandler::new::(0, cx, |mouse_state, _| { - render_icon_button( - theme.icon_button.style_for(mouse_state), - "icons/speaker-loud.svg", - ) - }) - .on_click(MouseButton::Left, move |_, _, cx| { - ActiveCall::global(cx) - .update(cx, |call, cx| call.join_channel(channel_id, cx)) - .detach_and_log_err(cx); - }) - .with_tooltip::( - channel_id as usize, - "Join Call", - Some(Box::new(JoinCall)), - tooltip_style.clone(), - cx, - ) - .flex_float(), - ]); - } - - row.align_children_center() - .contained() - .with_style(style.container) - .into_any() - } - - fn render_sign_in_prompt( - &self, - theme: &Arc, - cx: &mut ViewContext, - ) -> AnyElement { - enum SignInPromptLabel {} - - MouseEventHandler::new::(0, cx, |mouse_state, _| { - Label::new( - "Sign in to use chat".to_string(), - theme - .chat_panel - .sign_in_prompt - .style_for(mouse_state) - .clone(), - ) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - let client = this.client.clone(); - cx.spawn(|this, mut cx| async move { - if client - .authenticate_and_connect(true, &cx) - .log_err() - .await - .is_some() - { - this.update(&mut cx, |this, cx| { - if cx.handle().is_focused(cx) { - cx.focus(&this.input_editor); - } - }) - .ok(); - } - }) - .detach(); - }) - .aligned() - .into_any() - } - - fn send(&mut self, _: &Confirm, cx: &mut ViewContext) { - if let Some((chat, _)) = self.active_chat.as_ref() { - let message = self - .input_editor - .update(cx, |editor, cx| editor.take_message(cx)); - - if let Some(task) = chat - .update(cx, |chat, cx| chat.send_message(message, cx)) - .log_err() - { - task.detach(); - } - } - } - - fn remove_message(&mut self, id: u64, cx: &mut ViewContext) { - if let Some((chat, _)) = self.active_chat.as_ref() { - chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach()) - } - } - - fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext) { - if let Some((chat, _)) = self.active_chat.as_ref() { - chat.update(cx, |channel, cx| { - if let Some(task) = channel.load_more_messages(cx) { - task.detach(); - } - }) - } - } - - pub fn select_channel( - &mut self, - selected_channel_id: u64, - scroll_to_message_id: Option, - cx: &mut ViewContext, - ) -> Task> { - let open_chat = self - .active_chat - .as_ref() - .and_then(|(chat, _)| { - (chat.read(cx).channel_id == selected_channel_id) - .then(|| Task::ready(anyhow::Ok(chat.clone()))) - }) - .unwrap_or_else(|| { - self.channel_store.update(cx, |store, cx| { - store.open_channel_chat(selected_channel_id, cx) - }) - }); - - cx.spawn(|this, mut cx| async move { - let chat = open_chat.await?; - this.update(&mut cx, |this, cx| { - this.set_active_chat(chat.clone(), cx); - })?; - - if let Some(message_id) = scroll_to_message_id { - if let Some(item_ix) = - ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone()) - .await - { - this.update(&mut cx, |this, cx| { - if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) { - this.message_list.scroll_to(ListOffset { - item_ix, - offset_in_item: 0., - }); - cx.notify(); - } - })?; - } - } - - Ok(()) - }) - } - - fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext) { - if let Some((chat, _)) = &self.active_chat { - let channel_id = chat.read(cx).channel_id; - if let Some(workspace) = self.workspace.upgrade(cx) { - ChannelView::open(channel_id, workspace, cx).detach(); - } - } - } - - fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext) { - if let Some((chat, _)) = &self.active_chat { - let channel_id = chat.read(cx).channel_id; - ActiveCall::global(cx) - .update(cx, |call, cx| call.join_channel(channel_id, cx)) - .detach_and_log_err(cx); - } - } -} - -fn render_remove( - message_id_to_remove: Option, - cx: &mut ViewContext<'_, '_, ChatPanel>, - theme: &Arc, -) -> AnyElement { - enum DeleteMessage {} - - message_id_to_remove - .map(|id| { - MouseEventHandler::new::(id as usize, cx, |mouse_state, _| { - let button_style = theme.chat_panel.icon_button.style_for(mouse_state); - render_icon_button(button_style, "icons/x.svg") - .aligned() - .into_any() - }) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.remove_message(id, cx); - }) - .flex_float() - .into_any() - }) - .unwrap_or_else(|| { - let style = theme.chat_panel.icon_button.default; - - Empty::new() - .constrained() - .with_width(style.icon_width) - .aligned() - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - .contained() - .with_uniform_padding(2.) - .flex_float() - .into_any() - }) -} - -impl Entity for ChatPanel { - type Event = Event; -} - -impl View for ChatPanel { - fn ui_name() -> &'static str { - "ChatPanel" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = theme::current(cx); - let element = if self.client.user_id().is_some() { - self.render_channel(cx) - } else { - self.render_sign_in_prompt(&theme, cx) - }; - element - .contained() - .with_style(theme.chat_panel.container) - .constrained() - .with_min_width(150.) - .into_any() - } - - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - self.has_focus = true; - if matches!( - *self.client.status().borrow(), - client::Status::Connected { .. } - ) { - let editor = self.input_editor.read(cx).editor.clone(); - cx.focus(&editor); - } - } - - fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { - self.has_focus = false; - } -} - -impl Panel for ChatPanel { - fn position(&self, cx: &gpui::WindowContext) -> DockPosition { - settings::get::(cx).dock - } - - fn position_is_valid(&self, position: DockPosition) -> bool { - matches!(position, DockPosition::Left | DockPosition::Right) - } - - fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { - settings::update_settings_file::(self.fs.clone(), cx, move |settings| { - settings.dock = Some(position) - }); - } - - fn size(&self, cx: &gpui::WindowContext) -> f32 { - self.width - .unwrap_or_else(|| settings::get::(cx).default_width) - } - - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { - self.width = size; - self.serialize(cx); - cx.notify(); - } - - fn set_active(&mut self, active: bool, cx: &mut ViewContext) { - self.active = active; - if active { - self.acknowledge_last_message(cx); - if !is_channels_feature_enabled(cx) { - cx.emit(Event::Dismissed); - } - } - } - - fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> { - (settings::get::(cx).button && is_channels_feature_enabled(cx)) - .then(|| "icons/conversations.svg") - } - - fn icon_tooltip(&self) -> (String, Option>) { - ("Chat Panel".to_string(), Some(Box::new(ToggleFocus))) - } - - fn should_change_position_on_event(event: &Self::Event) -> bool { - matches!(event, Event::DockPositionChanged) - } - - fn should_close_on_event(event: &Self::Event) -> bool { - matches!(event, Event::Dismissed) - } - - fn has_focus(&self, _cx: &gpui::WindowContext) -> bool { - self.has_focus - } - - fn is_focus_event(event: &Self::Event) -> bool { - matches!(event, Event::Focus) - } -} - -fn format_timestamp( - mut timestamp: OffsetDateTime, - mut now: OffsetDateTime, - local_timezone: UtcOffset, -) -> String { - timestamp = timestamp.to_offset(local_timezone); - now = now.to_offset(local_timezone); - - let today = now.date(); - let date = timestamp.date(); - let mut hour = timestamp.hour(); - let mut part = "am"; - if hour > 12 { - hour -= 12; - part = "pm"; - } - if date == today { - format!("{:02}:{:02}{}", hour, timestamp.minute(), part) - } else if date.next_day() == Some(today) { - format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part) - } else { - format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year()) - } -} - -fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { - Svg::new(svg_path) - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - .contained() - .with_style(style.container) -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::fonts::HighlightStyle; - use pretty_assertions::assert_eq; - use rich_text::{BackgroundKind, Highlight, RenderedRegion}; - use util::test::marked_text_ranges; - - #[gpui::test] - fn test_render_markdown_with_mentions() { - let language_registry = Arc::new(LanguageRegistry::test()); - let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false); - let message = channel::ChannelMessage { - id: ChannelMessageId::Saved(0), - body, - timestamp: OffsetDateTime::now_utc(), - sender: Arc::new(client::User { - github_login: "fgh".into(), - avatar: None, - id: 103, - }), - nonce: 5, - mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)], - }; - - let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message); - - // Note that the "'" was replaced with ’ due to smart punctuation. - let (body, ranges) = marked_text_ranges("«hi», «@abc», let’s «call» «@fgh»", false); - assert_eq!(message.text, body); - assert_eq!( - message.highlights, - vec![ - ( - ranges[0].clone(), - HighlightStyle { - italic: Some(true), - ..Default::default() - } - .into() - ), - (ranges[1].clone(), Highlight::Mention), - ( - ranges[2].clone(), - HighlightStyle { - weight: Some(gpui::fonts::Weight::BOLD), - ..Default::default() - } - .into() - ), - (ranges[3].clone(), Highlight::SelfMention) - ] - ); - assert_eq!( - message.regions, - vec![ - RenderedRegion { - background_kind: Some(BackgroundKind::Mention), - link_url: None - }, - RenderedRegion { - background_kind: Some(BackgroundKind::SelfMention), - link_url: None - }, - ] - ); - } -} +// use crate::{ +// channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings, +// }; +// use anyhow::Result; +// use call::ActiveCall; +// use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore}; +// use client::Client; +// use collections::HashMap; +// use db::kvp::KEY_VALUE_STORE; +// use editor::Editor; +// use gpui::{ +// actions, +// elements::*, +// platform::{CursorStyle, MouseButton}, +// serde_json, +// views::{ItemType, Select, SelectStyle}, +// AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, +// ViewContext, ViewHandle, WeakViewHandle, +// }; +// use language::LanguageRegistry; +// use menu::Confirm; +// use message_editor::MessageEditor; +// use project::Fs; +// use rich_text::RichText; +// use serde::{Deserialize, Serialize}; +// use settings::SettingsStore; +// use std::sync::Arc; +// use theme::{IconButton, Theme}; +// use time::{OffsetDateTime, UtcOffset}; +// use util::{ResultExt, TryFutureExt}; +// use workspace::{ +// dock::{DockPosition, Panel}, +// Workspace, +// }; + +// mod message_editor; + +// const MESSAGE_LOADING_THRESHOLD: usize = 50; +// const CHAT_PANEL_KEY: &'static str = "ChatPanel"; + +// pub struct ChatPanel { +// client: Arc, +// channel_store: ModelHandle, +// languages: Arc, +// active_chat: Option<(ModelHandle, Subscription)>, +// message_list: ListState, +// input_editor: ViewHandle, +// channel_select: ViewHandle, +// ) -> AnyElement