Detailed changes
@@ -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<Tag: 'static, T: 'static + Default>(
&mut self,
- id: usize,
+ id: ElementStateId,
) -> ElementStateHandle<T> {
let key = (TypeId::of::<Tag>(), id);
self.cx
@@ -1699,7 +1701,7 @@ pub struct AppContext {
models: HashMap<usize, Box<dyn AnyModel>>,
views: HashMap<(usize, usize), Box<dyn AnyView>>,
windows: HashMap<usize, Window>,
- element_states: HashMap<(TypeId, usize), Box<dyn Any>>,
+ element_states: HashMap<(TypeId, ElementStateId), Box<dyn Any>>,
background: Arc<executor::Background>,
ref_counts: Arc<Mutex<RefCounts>>,
font_cache: Arc<FontCache>,
@@ -2977,6 +2979,10 @@ impl<T: View> WeakViewHandle<T> {
}
}
+ pub fn id(&self) -> usize {
+ self.view_id
+ }
+
pub fn upgrade(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
if cx.ref_counts.lock().is_entity_alive(self.view_id) {
Some(ViewHandle::new(
@@ -3000,15 +3006,30 @@ impl<T> Clone for WeakViewHandle<T> {
}
}
+#[derive(Clone, Copy, PartialEq, Eq, Hash)]
+pub struct ElementStateId(usize, usize);
+
+impl From<usize> 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<T> {
value_type: PhantomData<T>,
tag_type_id: TypeId,
- id: usize,
+ id: ElementStateId,
ref_counts: Weak<Mutex<RefCounts>>,
}
impl<T: 'static> ElementStateHandle<T> {
- fn new(tag_type_id: TypeId, id: usize, ref_counts: &Arc<Mutex<RefCounts>>) -> Self {
+ fn new(tag_type_id: TypeId, id: ElementStateId, ref_counts: &Arc<Mutex<RefCounts>>) -> 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<usize, usize>,
- element_state_counts: HashMap<(TypeId, usize), usize>,
+ element_state_counts: HashMap<(TypeId, ElementStateId), usize>,
dropped_models: HashSet<usize>,
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<usize>,
HashSet<(usize, usize)>,
- HashSet<(TypeId, usize)>,
+ HashSet<(TypeId, ElementStateId)>,
) {
let mut dropped_models = HashSet::new();
let mut dropped_views = HashSet::new();
@@ -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<Tag, F, C>(id: usize, cx: &mut C, render_child: F) -> Self
+ pub fn new<Tag, F, C, Id>(id: Id, cx: &mut C, render_child: F) -> Self
where
Tag: 'static,
F: FnOnce(&MouseState, &mut C) -> ElementBox,
C: DerefMut<Target = MutableAppContext>,
+ Id: Into<ElementStateId>,
{
- let state_handle = cx.element_state::<Tag, _>(id);
+ let state_handle = cx.element_state::<Tag, _>(id.into());
let child = state_handle.update(cx, |state, cx| render_child(state, cx));
Self {
state: state_handle,
@@ -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<F>
where
- F: Fn(Range<usize>, &mut Vec<ElementBox>, &AppContext),
+ F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut MutableAppContext),
{
state: UniformListState,
item_count: usize,
@@ -47,7 +47,7 @@ where
impl<F> UniformList<F>
where
- F: Fn(Range<usize>, &mut Vec<ElementBox>, &AppContext),
+ F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut MutableAppContext),
{
pub fn new(state: UniformListState, item_count: usize, append_items: F) -> Self {
Self {
@@ -102,7 +102,7 @@ where
impl<F> Element for UniformList<F>
where
- F: Fn(Range<usize>, &mut Vec<ElementBox>, &AppContext),
+ F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut MutableAppContext),
{
type LayoutState = LayoutState;
type PaintState = ();
@@ -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);
+}
@@ -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<usize>,
- render_selected_element: Box<dyn FnMut()>,
- render_elements: Box<dyn FnMut(Range<usize>, &mut RenderContext<Self>)>,
+ handle: WeakViewHandle<Self>,
+ render_item: Box<dyn Fn(usize, ItemType, bool, &AppContext) -> 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<F: 'static + Fn(usize, ItemType, bool, &AppContext) -> ElementBox>(
+ item_count: usize,
+ cx: &mut ViewContext<Self>,
+ 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>) {
+ self.item_count = count;
+ cx.notify();
+ }
+
+ fn toggle(&mut self, _: &ToggleSelect, cx: &mut ViewContext<Self>) {
+ self.is_open = !self.is_open;
+ cx.notify();
+ }
+
+ fn select_item(&mut self, action: &SelectItem, cx: &mut ViewContext<Self>) {
+ 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<Self>) -> ElementBox {
- todo!()
+ if self.item_count == 0 {
+ return Empty::new().boxed();
+ }
+
+ enum Header {}
+ enum Item {}
+
+ let mut result = Flex::column().with_child(
+ MouseEventHandler::new::<Header, _, _, _>(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::<Item, _, _, _>(
+ (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()
}
}
@@ -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"
@@ -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<Channel>, Subscription)>,
message_list: ListState,
input_editor: ViewHandle<Editor>,
+ channel_select: ViewHandle<Select>,
settings: watch::Receiver<Settings>,
}
@@ -38,11 +44,33 @@ impl ChatPanel {
cx: &mut ViewContext<Self>,
) -> Self {
let input_editor = cx.add_view(|cx| Editor::auto_height(settings.clone(), cx));
+ let channel_select = cx.add_view(|cx| {
+ let channel_list = channel_list.clone();
+ let theme = &settings.borrow().theme.chat_panel.channel_select;
+ Select::new(0, cx, {
+ let settings = settings.clone();
+ move |ix, item_type, is_hovered, cx| {
+ Self::render_channel_name(
+ &channel_list,
+ ix,
+ item_type,
+ is_hovered,
+ &settings.borrow().theme.chat_panel.channel_select,
+ cx,
+ )
+ }
+ })
+ .with_style(&SelectStyle {
+ header: theme.header.container.clone(),
+ menu: theme.menu.clone(),
+ })
+ });
let mut this = Self {
channel_list,
active_channel: Default::default(),
message_list: ListState::new(0, Orientation::Bottom),
input_editor,
+ channel_select,
settings,
};
@@ -56,23 +84,33 @@ impl ChatPanel {
}
fn init_active_channel(&mut self, cx: &mut ViewContext<Self>) {
- if self.active_channel.is_none() {
- let channel = self.channel_list.update(cx, |list, cx| {
- if let Some(channel_id) = list
- .available_channels()
- .and_then(|channels| channels.first())
- .map(|details| details.id)
- {
- return list.get_channel(channel_id, cx);
+ let (active_channel, channel_count) = self.channel_list.update(cx, |list, cx| {
+ let channel_count;
+ let mut active_channel = None;
+
+ if let Some(available_channels) = list.available_channels() {
+ channel_count = available_channels.len();
+ if self.active_channel.is_none() {
+ if let Some(channel_id) = available_channels.first().map(|channel| channel.id) {
+ active_channel = list.get_channel(channel_id, cx);
+ }
}
- None
- });
- if let Some(channel) = channel {
- self.set_active_channel(channel, cx);
+ } else {
+ channel_count = 0;
}
- } else if self.channel_list.read(cx).available_channels().is_none() {
+
+ (active_channel, channel_count)
+ });
+
+ if let Some(active_channel) = active_channel {
+ self.set_active_channel(active_channel, cx);
+ } else {
self.active_channel = None;
}
+
+ self.channel_select.update(cx, |select, cx| {
+ select.set_item_count(channel_count, cx);
+ });
}
fn set_active_channel(&mut self, channel: ModelHandle<Channel>, cx: &mut ViewContext<Self>) {
@@ -106,28 +144,6 @@ impl ChatPanel {
cx.notify();
}
- fn render_channel_name(&self, cx: &mut RenderContext<Self>) -> ElementBox {
- let settings = self.settings.borrow();
- let theme = &settings.theme.chat_panel;
- if let Some((channel, _)) = self.active_channel.as_ref() {
- let channel = channel.read(cx);
- Flex::row()
- .with_child(
- Container::new(
- Label::new("#".to_string(), theme.channel_name_hash.label.clone()).boxed(),
- )
- .with_style(&theme.channel_name_hash.container)
- .boxed(),
- )
- .with_child(
- Label::new(channel.name().to_string(), theme.channel_name.clone()).boxed(),
- )
- .boxed()
- } else {
- Empty::new().boxed()
- }
- }
-
fn render_active_channel_messages(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let messages = if let Some((channel, _)) = self.active_channel.as_ref() {
let channel = channel.read(cx);
@@ -155,7 +171,7 @@ impl ChatPanel {
Container::new(
Label::new(
message.sender.github_login.clone(),
- theme.sender.label.clone(),
+ theme.sender.text.clone(),
)
.boxed(),
)
@@ -166,7 +182,7 @@ impl ChatPanel {
Container::new(
Label::new(
format_timestamp(message.timestamp, now),
- theme.timestamp.label.clone(),
+ theme.timestamp.text.clone(),
)
.boxed(),
)
@@ -185,6 +201,36 @@ impl ChatPanel {
.boxed()
}
+ fn render_channel_name(
+ channel_list: &ModelHandle<ChannelList>,
+ ix: usize,
+ item_type: ItemType,
+ is_hovered: bool,
+ theme: &theme::ChannelSelect,
+ cx: &AppContext,
+ ) -> ElementBox {
+ let channel = &channel_list.read(cx).available_channels().unwrap()[ix];
+ let theme = match (item_type, is_hovered) {
+ (ItemType::Header, _) => &theme.header,
+ (ItemType::Selected, false) => &theme.active_item,
+ (ItemType::Selected, true) => &theme.hovered_active_item,
+ (ItemType::Unselected, false) => &theme.item,
+ (ItemType::Unselected, true) => &theme.hovered_item,
+ };
+ Container::new(
+ Flex::row()
+ .with_child(
+ Container::new(Label::new("#".to_string(), theme.hash.text.clone()).boxed())
+ .with_style(&theme.hash.container)
+ .boxed(),
+ )
+ .with_child(Label::new(channel.name.clone(), theme.name.clone()).boxed())
+ .boxed(),
+ )
+ .with_style(&theme.container)
+ .boxed()
+ }
+
fn send(&mut self, _: &Send, cx: &mut ViewContext<Self>) {
if let Some((channel, _)) = self.active_channel.as_ref() {
let body = self.input_editor.update(cx, |editor, cx| {
@@ -224,7 +270,11 @@ impl View for ChatPanel {
let theme = &self.settings.borrow().theme;
Container::new(
Flex::column()
- .with_child(self.render_channel_name(cx))
+ .with_child(
+ Container::new(ChildView::new(self.channel_select.id()).boxed())
+ .with_style(&theme.chat_panel.channel_select.container)
+ .boxed(),
+ )
.with_child(self.render_active_channel_messages(cx))
.with_child(self.render_input_box())
.boxed(),
@@ -68,15 +68,34 @@ pub struct ChatPanel {
#[serde(flatten)]
pub container: ContainerStyle,
pub message: ChatMessage,
- pub channel_name: TextStyle,
- pub channel_name_hash: ContainedLabel,
+ pub channel_select: ChannelSelect,
}
#[derive(Deserialize)]
pub struct ChatMessage {
pub body: TextStyle,
- pub sender: ContainedLabel,
- pub timestamp: ContainedLabel,
+ pub sender: ContainedText,
+ pub timestamp: ContainedText,
+}
+
+#[derive(Deserialize)]
+pub struct ChannelSelect {
+ #[serde(flatten)]
+ pub container: ContainerStyle,
+ pub header: ChannelName,
+ pub item: ChannelName,
+ pub active_item: ChannelName,
+ pub hovered_item: ChannelName,
+ pub hovered_active_item: ChannelName,
+ pub menu: ContainerStyle,
+}
+
+#[derive(Deserialize)]
+pub struct ChannelName {
+ #[serde(flatten)]
+ pub container: ContainerStyle,
+ pub hash: ContainedText,
+ pub name: TextStyle,
}
#[derive(Deserialize)]
@@ -90,6 +109,14 @@ pub struct Selector {
pub active_item: ContainedLabel,
}
+#[derive(Deserialize)]
+pub struct ContainedText {
+ #[serde(flatten)]
+ pub container: ContainerStyle,
+ #[serde(flatten)]
+ pub text: TextStyle,
+}
+
#[derive(Deserialize)]
pub struct ContainedLabel {
#[serde(flatten)]
@@ -193,7 +193,7 @@ impl Pane {
row.add_child(
Expanded::new(
1.0,
- MouseEventHandler::new::<Tab, _, _>(item.id(), cx, |mouse_state, cx| {
+ MouseEventHandler::new::<Tab, _, _, _>(item.id(), cx, |mouse_state, cx| {
let title = item.title(cx);
let mut border = border.clone();
@@ -317,7 +317,7 @@ impl Pane {
let close_color = current_color.unwrap_or(theme.workspace.tab.icon_close);
let icon = Svg::new("icons/x.svg").with_color(close_color);
- MouseEventHandler::new::<TabCloseButton, _, _>(item_id, cx, |mouse_state, _| {
+ MouseEventHandler::new::<TabCloseButton, _, _, _>(item_id, cx, |mouse_state, _| {
if mouse_state.hovered {
Container::new(icon.with_color(Color::white()).boxed())
.with_background_color(if mouse_state.clicked {
@@ -76,7 +76,7 @@ impl Sidebar {
&settings.theme.workspace.sidebar_icon
};
enum SidebarButton {}
- MouseEventHandler::new::<SidebarButton, _, _>(item.view.id(), cx, |_, _| {
+ MouseEventHandler::new::<SidebarButton, _, _, _>(item.view.id(), cx, |_, _| {
ConstrainedBox::new(
Align::new(
ConstrainedBox::new(
@@ -133,7 +133,7 @@ impl Sidebar {
) -> ElementBox {
let width = self.width.clone();
let side = self.side;
- MouseEventHandler::new::<Self, _, _>(self.side.id(), &mut cx, |_, _| {
+ MouseEventHandler::new::<Self, _, _, _>(self.side.id(), &mut cx, |_, _| {
Container::new(Empty::new().boxed())
.with_style(&settings.theme.workspace.sidebar.resize_handle)
.boxed()