diff --git a/gpui/src/app.rs b/gpui/src/app.rs index edfed5af1de39c272ca39ac13942a7b881bb2656..5b6d3d87a35aef0c0d38aeb346fcd532c41f1c26 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -294,7 +294,9 @@ impl App { let platform = self.0.borrow().foreground_platform.clone(); platform.run(Box::new(move || { let mut cx = self.0.borrow_mut(); - on_finish_launching(&mut *cx); + let cx = &mut *cx; + crate::views::init(cx); + on_finish_launching(cx); })) } @@ -1306,7 +1308,7 @@ impl MutableAppContext { pub fn element_state( &mut self, - id: usize, + id: ElementStateId, ) -> ElementStateHandle { let key = (TypeId::of::(), id); self.cx @@ -1699,7 +1701,7 @@ pub struct AppContext { models: HashMap>, views: HashMap<(usize, usize), Box>, windows: HashMap, - element_states: HashMap<(TypeId, usize), Box>, + element_states: HashMap<(TypeId, ElementStateId), Box>, background: Arc, ref_counts: Arc>, font_cache: Arc, @@ -2977,6 +2979,10 @@ impl WeakViewHandle { } } + pub fn id(&self) -> usize { + self.view_id + } + pub fn upgrade(&self, cx: &AppContext) -> Option> { if cx.ref_counts.lock().is_entity_alive(self.view_id) { Some(ViewHandle::new( @@ -3000,15 +3006,30 @@ impl Clone for WeakViewHandle { } } +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct ElementStateId(usize, usize); + +impl From for ElementStateId { + fn from(id: usize) -> Self { + Self(id, 0) + } +} + +impl From<(usize, usize)> for ElementStateId { + fn from(id: (usize, usize)) -> Self { + Self(id.0, id.1) + } +} + pub struct ElementStateHandle { value_type: PhantomData, tag_type_id: TypeId, - id: usize, + id: ElementStateId, ref_counts: Weak>, } impl ElementStateHandle { - fn new(tag_type_id: TypeId, id: usize, ref_counts: &Arc>) -> Self { + fn new(tag_type_id: TypeId, id: ElementStateId, ref_counts: &Arc>) -> Self { ref_counts.lock().inc_element_state(tag_type_id, id); Self { value_type: PhantomData, @@ -3128,10 +3149,10 @@ impl Drop for Subscription { #[derive(Default)] struct RefCounts { entity_counts: HashMap, - element_state_counts: HashMap<(TypeId, usize), usize>, + element_state_counts: HashMap<(TypeId, ElementStateId), usize>, dropped_models: HashSet, dropped_views: HashSet<(usize, usize)>, - dropped_element_states: HashSet<(TypeId, usize)>, + dropped_element_states: HashSet<(TypeId, ElementStateId)>, } impl RefCounts { @@ -3155,11 +3176,14 @@ impl RefCounts { } } - fn inc_element_state(&mut self, tag_type_id: TypeId, id: usize) { - *self - .element_state_counts - .entry((tag_type_id, id)) - .or_insert(0) += 1; + fn inc_element_state(&mut self, tag_type_id: TypeId, id: ElementStateId) { + match self.element_state_counts.entry((tag_type_id, id)) { + Entry::Occupied(mut entry) => *entry.get_mut() += 1, + Entry::Vacant(entry) => { + entry.insert(1); + self.dropped_element_states.remove(&(tag_type_id, id)); + } + } } fn dec_model(&mut self, model_id: usize) { @@ -3180,7 +3204,7 @@ impl RefCounts { } } - fn dec_element_state(&mut self, tag_type_id: TypeId, id: usize) { + fn dec_element_state(&mut self, tag_type_id: TypeId, id: ElementStateId) { let key = (tag_type_id, id); let count = self.element_state_counts.get_mut(&key).unwrap(); *count -= 1; @@ -3199,7 +3223,7 @@ impl RefCounts { ) -> ( HashSet, HashSet<(usize, usize)>, - HashSet<(TypeId, usize)>, + HashSet<(TypeId, ElementStateId)>, ) { let mut dropped_models = HashSet::new(); let mut dropped_views = HashSet::new(); diff --git a/gpui/src/elements/mouse_event_handler.rs b/gpui/src/elements/mouse_event_handler.rs index 9336c8b84a20178c10f57cb5632359b108d98d5c..c9d9bb7a0100b9206674f7e1b847bee88994b8ea 100644 --- a/gpui/src/elements/mouse_event_handler.rs +++ b/gpui/src/elements/mouse_event_handler.rs @@ -3,8 +3,8 @@ use std::ops::DerefMut; use crate::{ geometry::{rect::RectF, vector::Vector2F}, platform::CursorStyle, - CursorStyleHandle, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext, - LayoutContext, MutableAppContext, PaintContext, SizeConstraint, + CursorStyleHandle, DebugContext, Element, ElementBox, ElementStateHandle, ElementStateId, + Event, EventContext, LayoutContext, MutableAppContext, PaintContext, SizeConstraint, }; use serde_json::json; @@ -25,13 +25,14 @@ pub struct MouseState { } impl MouseEventHandler { - pub fn new(id: usize, cx: &mut C, render_child: F) -> Self + pub fn new(id: Id, cx: &mut C, render_child: F) -> Self where Tag: 'static, F: FnOnce(&MouseState, &mut C) -> ElementBox, C: DerefMut, + Id: Into, { - let state_handle = cx.element_state::(id); + let state_handle = cx.element_state::(id.into()); let child = state_handle.update(cx, |state, cx| render_child(state, cx)); Self { state: state_handle, diff --git a/gpui/src/elements/uniform_list.rs b/gpui/src/elements/uniform_list.rs index 04567d4223a7d12e28ab846bf97619ad36eaa4fa..62490e6f9b61bb0d6952eb16e00d403ff4dd58eb 100644 --- a/gpui/src/elements/uniform_list.rs +++ b/gpui/src/elements/uniform_list.rs @@ -5,7 +5,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{self, json}, - AppContext, ElementBox, + ElementBox, MutableAppContext, }; use json::ToJson; use parking_lot::Mutex; @@ -38,7 +38,7 @@ pub struct LayoutState { pub struct UniformList where - F: Fn(Range, &mut Vec, &AppContext), + F: Fn(Range, &mut Vec, &mut MutableAppContext), { state: UniformListState, item_count: usize, @@ -47,7 +47,7 @@ where impl UniformList where - F: Fn(Range, &mut Vec, &AppContext), + F: Fn(Range, &mut Vec, &mut MutableAppContext), { pub fn new(state: UniformListState, item_count: usize, append_items: F) -> Self { Self { @@ -102,7 +102,7 @@ where impl Element for UniformList where - F: Fn(Range, &mut Vec, &AppContext), + F: Fn(Range, &mut Vec, &mut MutableAppContext), { type LayoutState = LayoutState; type PaintState = (); diff --git a/gpui/src/views.rs b/gpui/src/views.rs index 470d1d019052dc6db768192f6d2800c734676d75..73f8a5751830f6d4f09044883c31d50ffb346b78 100644 --- a/gpui/src/views.rs +++ b/gpui/src/views.rs @@ -1,3 +1,7 @@ mod select; -pub use select::*; +pub use select::{ItemType, Select, SelectStyle}; + +pub fn init(cx: &mut super::MutableAppContext) { + select::init(cx); +} diff --git a/gpui/src/views/select.rs b/gpui/src/views/select.rs index a97aa201c8038b4698c3154fda44729a674b7fd7..3a40133665f95afbae302564921d4e12b1c5935e 100644 --- a/gpui/src/views/select.rs +++ b/gpui/src/views/select.rs @@ -1,14 +1,79 @@ -use crate::{elements::*, Entity, RenderContext, View}; -use std::ops::Range; +use crate::{ + action, elements::*, AppContext, Entity, MutableAppContext, RenderContext, View, ViewContext, + WeakViewHandle, +}; pub struct Select { - selected_ix: Option, - render_selected_element: Box, - render_elements: Box, &mut RenderContext)>, + handle: WeakViewHandle, + render_item: Box ElementBox>, + selected_item_ix: usize, + item_count: usize, + is_open: bool, + list_state: UniformListState, + style: SelectStyle, } +#[derive(Clone, Default)] +pub struct SelectStyle { + pub header: ContainerStyle, + pub menu: ContainerStyle, +} + +pub enum ItemType { + Header, + Selected, + Unselected, +} + +action!(ToggleSelect); +action!(SelectItem, usize); + pub enum Event {} +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(Select::toggle); + cx.add_action(Select::select_item); +} + +impl Select { + pub fn new ElementBox>( + item_count: usize, + cx: &mut ViewContext, + render_item: F, + ) -> Self { + Self { + handle: cx.handle().downgrade(), + render_item: Box::new(render_item), + selected_item_ix: 0, + item_count, + is_open: false, + list_state: UniformListState::default(), + style: Default::default(), + } + } + + pub fn with_style(mut self, style: &SelectStyle) -> Self { + self.style = style.clone(); + self + } + + pub fn set_item_count(&mut self, count: usize, cx: &mut ViewContext) { + self.item_count = count; + cx.notify(); + } + + fn toggle(&mut self, _: &ToggleSelect, cx: &mut ViewContext) { + self.is_open = !self.is_open; + cx.notify(); + } + + fn select_item(&mut self, action: &SelectItem, cx: &mut ViewContext) { + self.selected_item_ix = action.0; + self.is_open = false; + cx.notify(); + } +} + impl Entity for Select { type Event = Event; } @@ -19,6 +84,74 @@ impl View for Select { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - todo!() + if self.item_count == 0 { + return Empty::new().boxed(); + } + + enum Header {} + enum Item {} + + let mut result = Flex::column().with_child( + MouseEventHandler::new::(self.handle.id(), cx, |mouse_state, cx| { + Container::new((self.render_item)( + self.selected_item_ix, + ItemType::Header, + mouse_state.hovered, + cx, + )) + .with_style(&self.style.header) + .boxed() + }) + .on_click(move |cx| cx.dispatch_action(ToggleSelect)) + .boxed(), + ); + if self.is_open { + let handle = self.handle.clone(); + result.add_child( + Overlay::new( + Container::new( + ConstrainedBox::new( + UniformList::new( + self.list_state.clone(), + self.item_count, + move |mut range, items, mut cx| { + let handle = handle.upgrade(cx).unwrap(); + let this = handle.read(cx); + let selected_item_ix = this.selected_item_ix; + range.end = range.end.min(this.item_count); + items.extend(range.map(|ix| { + MouseEventHandler::new::( + (handle.id(), ix), + &mut cx, + |mouse_state, cx| { + (handle.read(*cx).render_item)( + ix, + if ix == selected_item_ix { + ItemType::Selected + } else { + ItemType::Unselected + }, + mouse_state.hovered, + cx, + ) + }, + ) + .on_click(move |cx| cx.dispatch_action(SelectItem(ix))) + .boxed() + })) + }, + ) + .boxed(), + ) + .with_max_height(200.) + .boxed(), + ) + .with_style(&self.style.menu) + .boxed(), + ) + .boxed(), + ) + } + result.boxed() } } diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 8900c34fb0390982015bb59ddb4729fc1c620798..abc886a5257586e19446cdb9e8bac4c74cf718a7 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -26,15 +26,42 @@ color = "$text.2.color" color = "$text.0.color" [chat_panel] -padding = { top = 10.0, bottom = 10.0, left = 10.0, right = 10.0 } channel_name = { extends = "$text.0", weight = "bold" } -channel_name_hash = { text = "$text.2", padding.right = 5.0 } +channel_name_hash = { text = "$text.2", padding.right = 5 } [chat_panel.message] body = "$text.1" -sender.margin.right = 10.0 -sender.text = { extends = "$text.0", weight = "bold" } -timestamp.text = "$text.2" +sender = { extends = "$text.0", weight = "bold", margin.right = 10.0 } +timestamp = "$text.2" +padding = { top = 10, bottom = 10, left = 10, right = 10 } + +[chat_panel.channel_select.item] +padding = { top = 4, bottom = 4, left = 4, right = 4 } +name = "$text.1" +hash = { extends = "$text.2", margin.right = 5.0 } + +[chat_panel.channel_select.hovered_item] +extends = "$chat_panel.channel_select.item" +background = "$surface.2" +corner_radius = 6.0 + +[chat_panel.channel_select.active_item] +extends = "$chat_panel.channel_select.item" +name = "$text.0" + +[chat_panel.channel_select.hovered_active_item] +extends = "$chat_panel.channel_select.hovered_item" +name = "$text.0" + +[chat_panel.channel_select.header] +extends = "$chat_panel.channel_select.active_item" +padding.bottom = 0 + +[chat_panel.channel_select.menu] +padding = { top = 4, bottom = 4, left = 4, right = 4 } +corner_radius = 6.0 +border = { color = "#000000", width = 1.0 } +background = "$surface.0" [selector] background = "$surface.2" diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 20b6b52525a2b7e7e8bcb53ef0c6690be31e8f9a..0eef94529cac28f58a87cdfce8397e5855634fb9 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -1,12 +1,17 @@ use crate::{ channel::{Channel, ChannelEvent, ChannelList, ChannelMessage}, editor::Editor, + theme, util::ResultExt, Settings, }; use gpui::{ - action, elements::*, keymap::Binding, Entity, ModelHandle, MutableAppContext, RenderContext, - Subscription, View, ViewContext, ViewHandle, + action, + elements::*, + keymap::Binding, + views::{ItemType, Select, SelectStyle}, + AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, View, + ViewContext, ViewHandle, }; use postage::watch; use time::{OffsetDateTime, UtcOffset}; @@ -16,6 +21,7 @@ pub struct ChatPanel { active_channel: Option<(ModelHandle, Subscription)>, message_list: ListState, input_editor: ViewHandle, + channel_select: ViewHandle