chat panel ++ (#4044)

- Update chat panel with current channel
- Open chat panel for guests
- Open chat when joining a channel with guests
- Some tweaks for chat panels
- Don't lose focus on default panel state
- Make chat prettier (to my eyes at least)
- Fix multiple mentions in one message
- Show a border when scrolled in chat
- Fix re-docking chat panel
- Move settings subscription to dock

[[PR Description]]

Release Notes:

- Opens chat by default when joining a public channel
- Improves chat panel UI
This commit is contained in:
Conrad Irwin 2024-01-14 13:54:10 -07:00 committed by GitHub
commit 29ce109211
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 273 additions and 254 deletions

View File

@ -40,7 +40,7 @@ use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset a
use project::Project;
use search::{buffer_search::DivRegistrar, BufferSearchBar};
use semantic_index::{SemanticIndex, SemanticIndexStatus};
use settings::{Settings, SettingsStore};
use settings::Settings;
use std::{
cell::Cell,
cmp,
@ -165,7 +165,7 @@ impl AssistantPanel {
cx.on_focus_in(&focus_handle, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, Self::focus_out).detach();
let mut this = Self {
Self {
workspace: workspace_handle,
active_editor_index: Default::default(),
prev_active_editor_index: Default::default(),
@ -190,20 +190,7 @@ impl AssistantPanel {
_watch_saved_conversations,
semantic_index,
retrieve_context_in_next_inline_assist: false,
};
let mut old_dock_position = this.position(cx);
this.subscriptions =
vec![cx.observe_global::<SettingsStore>(move |this, cx| {
let new_dock_position = this.position(cx);
if new_dock_position != old_dock_position {
old_dock_position = new_dock_position;
cx.emit(PanelEvent::ChangePosition);
}
cx.notify();
})];
this
})
})
})
@ -3133,6 +3120,7 @@ mod tests {
use crate::MessageId;
use ai::test::FakeCompletionProvider;
use gpui::AppContext;
use settings::SettingsStore;
#[gpui::test]
fn test_inserting_and_removing_messages(cx: &mut AppContext) {

View File

@ -442,6 +442,8 @@ impl ActiveCall {
.location
.as_ref()
.and_then(|location| location.upgrade());
let channel_id = room.read(cx).channel_id();
cx.emit(Event::RoomJoined { channel_id });
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
}
} else {

View File

@ -26,6 +26,9 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
RoomJoined {
channel_id: Option<u64>,
},
ParticipantLocationChanged {
participant_id: proto::PeerId,
},
@ -49,7 +52,9 @@ pub enum Event {
RemoteProjectInvitationDiscarded {
project_id: u64,
},
Left,
Left {
channel_id: Option<u64>,
},
}
pub struct Room {
@ -357,7 +362,9 @@ impl Room {
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
cx.emit(Event::Left);
cx.emit(Event::Left {
channel_id: self.channel_id(),
});
self.leave_internal(cx)
}
@ -598,6 +605,14 @@ impl Room {
.map(|participant| participant.role)
}
pub fn contains_guests(&self) -> bool {
self.local_participant.role == proto::ChannelRole::Guest
|| self
.remote_participants
.values()
.any(|p| p.role == proto::ChannelRole::Guest)
}
pub fn local_participant_is_admin(&self) -> bool {
self.local_participant.role == proto::ChannelRole::Admin
}

View File

@ -144,7 +144,7 @@ impl ChannelChat {
message: MessageParams,
cx: &mut ModelContext<Self>,
) -> Result<Task<Result<u64>>> {
if message.text.is_empty() {
if message.text.trim().is_empty() {
Err(anyhow!("message body can't be empty"))?;
}
@ -174,6 +174,8 @@ impl ChannelChat {
let user_store = self.user_store.clone();
let rpc = self.rpc.clone();
let outgoing_messages_lock = self.outgoing_messages_lock.clone();
// todo - handle messages that fail to send (e.g. >1024 chars)
Ok(cx.spawn(move |this, mut cx| async move {
let outgoing_message_guard = outgoing_messages_lock.lock().await;
let request = rpc.request(proto::SendChannelMessage {

View File

@ -256,6 +256,7 @@ impl Database {
message_id = result.last_insert_id;
let mentioned_user_ids =
mentions.iter().map(|m| m.user_id).collect::<HashSet<_>>();
let mentions = mentions
.iter()
.filter_map(|mention| {

View File

@ -1,15 +1,15 @@
use crate::{channel_view::ChannelView, is_channels_feature_enabled, ChatPanelSettings};
use crate::{collab_panel, is_channels_feature_enabled, ChatPanelSettings};
use anyhow::Result;
use call::ActiveCall;
use call::{room, 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, div, list, prelude::*, px, AnyElement, AppContext, AsyncWindowContext, ClickEvent,
ElementId, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState, Model, Render,
Subscription, Task, View, ViewContext, VisualContext, WeakView,
actions, div, list, prelude::*, px, Action, AppContext, AsyncWindowContext, DismissEvent,
ElementId, EventEmitter, FocusHandle, FocusableView, FontWeight, ListOffset, ListScrollEvent,
ListState, Model, Render, Subscription, Task, View, ViewContext, VisualContext, WeakView,
};
use language::LanguageRegistry;
use menu::Confirm;
@ -17,10 +17,13 @@ use message_editor::MessageEditor;
use project::Fs;
use rich_text::RichText;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use settings::Settings;
use std::sync::Arc;
use time::{OffsetDateTime, UtcOffset};
use ui::{prelude::*, Avatar, Button, IconButton, IconName, Label, TabBar, Tooltip};
use ui::{
popover_menu, prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label,
TabBar,
};
use util::{ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
@ -54,9 +57,10 @@ pub struct ChatPanel {
active: bool,
pending_serialization: Task<Option<()>>,
subscriptions: Vec<gpui::Subscription>,
workspace: WeakView<Workspace>,
is_scrolled_to_bottom: bool,
markdown_data: HashMap<ChannelMessageId, RichText>,
focus_handle: FocusHandle,
open_context_menu: Option<(u64, Subscription)>,
}
#[derive(Serialize, Deserialize)]
@ -64,13 +68,6 @@ struct SerializedChatPanel {
width: Option<Pixels>,
}
#[derive(Debug)]
pub enum Event {
DockPositionChanged,
Focus,
Dismissed,
}
actions!(chat_panel, [ToggleFocus]);
impl ChatPanel {
@ -89,8 +86,6 @@ impl ChatPanel {
)
});
let workspace_handle = workspace.weak_handle();
cx.new_view(|cx: &mut ViewContext<Self>| {
let view = cx.view().downgrade();
let message_list =
@ -108,7 +103,7 @@ impl ChatPanel {
if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
this.load_more_messages(cx);
}
this.is_scrolled_to_bottom = event.visible_range.end == event.count;
this.is_scrolled_to_bottom = !event.is_scrolled;
}));
let mut this = Self {
@ -122,22 +117,37 @@ impl ChatPanel {
message_editor: input_editor,
local_timezone: cx.local_timezone(),
subscriptions: Vec::new(),
workspace: workspace_handle,
is_scrolled_to_bottom: true,
active: false,
width: None,
markdown_data: Default::default(),
focus_handle: cx.focus_handle(),
open_context_menu: None,
};
let mut old_dock_position = this.position(cx);
this.subscriptions.push(cx.observe_global::<SettingsStore>(
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);
this.subscriptions.push(cx.subscribe(
&ActiveCall::global(cx),
move |this: &mut Self, call, event: &room::Event, cx| match event {
room::Event::RoomJoined { channel_id } => {
if let Some(channel_id) = channel_id {
this.select_channel(*channel_id, None, cx)
.detach_and_log_err(cx);
if call
.read(cx)
.room()
.is_some_and(|room| room.read(cx).contains_guests())
{
cx.emit(PanelEvent::Activate)
}
cx.notify();
}
}
room::Event::Left { channel_id } => {
if channel_id == &this.channel_id(cx) {
cx.emit(PanelEvent::Close)
}
}
_ => {}
},
));
@ -145,6 +155,12 @@ impl ChatPanel {
})
}
pub fn channel_id(&self, cx: &AppContext) -> Option<u64> {
self.active_chat
.as_ref()
.map(|(chat, _)| chat.read(cx).channel_id)
}
pub fn is_scrolled_to_bottom(&self) -> bool {
self.is_scrolled_to_bottom
}
@ -259,53 +275,9 @@ impl ChatPanel {
}
}
fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement {
v_stack()
.full()
.on_action(cx.listener(Self::send))
.child(
h_stack().z_index(1).child(
TabBar::new("chat_header")
.child(
h_stack()
.w_full()
.h(rems(ui::Tab::HEIGHT_IN_REMS))
.px_2()
.child(Label::new(
self.active_chat
.as_ref()
.and_then(|c| {
Some(format!("#{}", c.0.read(cx).channel(cx)?.name))
})
.unwrap_or_default(),
)),
)
.end_child(
IconButton::new("notes", IconName::File)
.on_click(cx.listener(Self::open_notes))
.tooltip(|cx| Tooltip::text("Open notes", cx)),
)
.end_child(
IconButton::new("call", IconName::AudioOn)
.on_click(cx.listener(Self::join_call))
.tooltip(|cx| Tooltip::text("Join call", cx)),
),
),
)
.child(div().flex_grow().px_2().py_1().map(|this| {
if self.active_chat.is_some() {
this.child(list(self.message_list.clone()).full())
} else {
this
}
}))
.child(h_stack().p_2().child(self.message_editor.clone()))
.into_any()
}
fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
let active_chat = &self.active_chat.as_ref().unwrap().0;
let (message, is_continuation_from_previous, is_continuation_to_next, is_admin) =
let (message, is_continuation_from_previous, is_admin) =
active_chat.update(cx, |active_chat, cx| {
let is_admin = self
.channel_store
@ -314,13 +286,9 @@ impl ChatPanel {
let last_message = active_chat.message(ix.saturating_sub(1));
let this_message = active_chat.message(ix).clone();
let next_message =
active_chat.message(ix.saturating_add(1).min(active_chat.message_count() - 1));
let is_continuation_from_previous = last_message.id != this_message.id
&& last_message.sender.id == this_message.sender.id;
let is_continuation_to_next = this_message.id != next_message.id
&& this_message.sender.id == next_message.sender.id;
if let ChannelMessageId::Saved(id) = this_message.id {
if this_message
@ -332,12 +300,7 @@ impl ChatPanel {
}
}
(
this_message,
is_continuation_from_previous,
is_continuation_to_next,
is_admin,
)
(this_message, is_continuation_from_previous, is_admin)
});
let _is_pending = message.is_pending();
@ -360,50 +323,100 @@ impl ChatPanel {
ChannelMessageId::Saved(id) => ("saved-message", id).into(),
ChannelMessageId::Pending(id) => ("pending-message", id).into(),
};
let this = cx.view().clone();
v_stack()
.w_full()
.id(element_id)
.relative()
.overflow_hidden()
.group("")
.when(!is_continuation_from_previous, |this| {
this.child(
this.pt_3().child(
h_stack()
.gap_2()
.child(Avatar::new(message.sender.avatar_uri.clone()))
.child(Label::new(message.sender.github_login.clone()))
.child(
div().absolute().child(
Avatar::new(message.sender.avatar_uri.clone())
.size(cx.rem_size() * 1.5),
),
)
.child(
div()
.pl(cx.rem_size() * 1.5 + px(6.0))
.pr(px(8.0))
.font_weight(FontWeight::BOLD)
.child(Label::new(message.sender.github_login.clone())),
)
.child(
Label::new(format_timestamp(
message.timestamp,
now,
self.local_timezone,
))
.size(LabelSize::Small)
.color(Color::Muted),
),
)
})
.when(!is_continuation_to_next, |this|
// HACK: This should really be a margin, but margins seem to get collapsed.
this.pb_2())
.when(is_continuation_from_previous, |this| this.pt_1())
.child(
v_stack()
.w_full()
.text_ui_sm()
.id(element_id)
.group("")
.child(text.element("body".into(), cx))
.child(
div()
.absolute()
.top_1()
.right_2()
.w_8()
.visible_on_hover("")
.z_index(1)
.right_0()
.w_6()
.bg(cx.theme().colors().panel_background)
.when(!self.has_open_menu(message_id_to_remove), |el| {
el.visible_on_hover("")
})
.children(message_id_to_remove.map(|message_id| {
IconButton::new(("remove", message_id), IconName::XCircle).on_click(
cx.listener(move |this, _, cx| {
this.remove_message(message_id, cx);
}),
)
popover_menu(("menu", message_id))
.trigger(IconButton::new(
("trigger", message_id),
IconName::Ellipsis,
))
.menu(move |cx| {
Some(Self::render_message_menu(&this, message_id, cx))
})
})),
),
)
}
fn has_open_menu(&self, message_id: Option<u64>) -> bool {
match self.open_context_menu.as_ref() {
Some((id, _)) => Some(*id) == message_id,
None => false,
}
}
fn render_message_menu(
this: &View<Self>,
message_id: u64,
cx: &mut WindowContext,
) -> View<ContextMenu> {
let menu = {
let this = this.clone();
ContextMenu::build(cx, move |menu, _| {
menu.entry("Delete message", None, move |cx| {
this.update(cx, |this, cx| this.remove_message(message_id, cx))
})
})
};
this.update(cx, |this, cx| {
let subscription = cx.subscribe(&menu, |this: &mut Self, _, _: &DismissEvent, _| {
this.open_context_menu = None;
});
this.open_context_menu = Some((message_id, subscription));
});
menu
}
fn render_markdown_with_mentions(
language_registry: &Arc<LanguageRegistry>,
current_user_id: u64,
@ -421,44 +434,6 @@ impl ChatPanel {
rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
}
fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_stack()
.gap_2()
.p_4()
.child(
Button::new("sign-in", "Sign in")
.style(ButtonStyle::Filled)
.icon_color(Color::Muted)
.icon(IconName::Github)
.icon_position(IconPosition::Start)
.full_width()
.on_click(cx.listener(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, |_, cx| {
cx.focus_self();
})
.ok();
}
})
.detach();
})),
)
.child(
div().flex().w_full().items_center().child(
Label::new("Sign in to chat.")
.color(Color::Muted)
.size(LabelSize::Small),
),
)
}
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some((chat, _)) = self.active_chat.as_ref() {
let message = self
@ -535,50 +510,93 @@ impl ChatPanel {
Ok(())
})
}
fn open_notes(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
if let Some((chat, _)) = &self.active_chat {
let channel_id = chat.read(cx).channel_id;
if let Some(workspace) = self.workspace.upgrade() {
ChannelView::open(channel_id, workspace, cx).detach();
}
}
}
fn join_call(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
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);
}
}
}
impl EventEmitter<Event> for ChatPanel {}
impl Render for ChatPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_stack()
.size_full()
.map(|this| match (self.client.user_id(), self.active_chat()) {
(Some(_), Some(_)) => this.child(self.render_channel(cx)),
(Some(_), None) => this.child(
div().p_4().child(
.track_focus(&self.focus_handle)
.full()
.on_action(cx.listener(Self::send))
.child(
h_stack().z_index(1).child(
TabBar::new("chat_header").child(
h_stack()
.w_full()
.h(rems(ui::Tab::HEIGHT_IN_REMS))
.px_2()
.child(Label::new(
self.active_chat
.as_ref()
.and_then(|c| {
Some(format!("#{}", c.0.read(cx).channel(cx)?.name))
})
.unwrap_or("Chat".to_string()),
)),
),
),
)
.child(div().flex_grow().px_2().pt_1().map(|this| {
if self.active_chat.is_some() {
this.child(list(self.message_list.clone()).full())
} else {
this.child(
div()
.p_4()
.child(
Label::new("Select a channel to chat in.")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
div().pt_1().w_full().items_center().child(
Button::new("toggle-collab", "Open")
.full_width()
.key_binding(KeyBinding::for_action(
&collab_panel::ToggleFocus,
cx,
))
.on_click(|_, cx| {
cx.dispatch_action(
collab_panel::ToggleFocus.boxed_clone(),
)
}),
),
),
(None, _) => this.child(self.render_sign_in_prompt(cx)),
)
}
}))
.child(
h_stack()
.when(!self.is_scrolled_to_bottom, |el| {
el.border_t_1().border_color(cx.theme().colors().border)
})
.min_w(px(150.))
.p_2()
.map(|el| {
if self.active_chat.is_some() {
el.child(self.message_editor.clone())
} else {
el.child(
div()
.rounded_md()
.h_7()
.w_full()
.bg(cx.theme().colors().editor_background),
)
}
}),
)
.into_any()
}
}
impl FocusableView for ChatPanel {
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
if self.active_chat.is_some() {
self.message_editor.read(cx).focus_handle(cx)
} else {
self.focus_handle.clone()
}
}
}
@ -613,7 +631,7 @@ impl Panel for ChatPanel {
if active {
self.acknowledge_last_message(cx);
if !is_channels_feature_enabled(cx) {
cx.emit(Event::Dismissed);
cx.emit(PanelEvent::Close);
}
}
}

View File

@ -26,7 +26,7 @@ use menu::{Cancel, Confirm, SelectNext, SelectPrev};
use project::{Fs, Project};
use rpc::proto::{self, PeerId};
use serde_derive::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use settings::Settings;
use smallvec::SmallVec;
use std::{mem, sync::Arc};
use theme::{ActiveTheme, ThemeSettings};
@ -254,19 +254,6 @@ impl CollabPanel {
this.update_entries(false, cx);
// Update the dock position when the setting changes.
let mut old_dock_position = this.position(cx);
this.subscriptions.push(cx.observe_global::<SettingsStore>(
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(PanelEvent::ChangePosition);
}
cx.notify();
},
));
let active_call = ActiveCall::global(cx);
this.subscriptions
.push(cx.observe(&this.user_store, |this, _, cx| {

View File

@ -58,7 +58,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
}
}
room::Event::Left => {
room::Event::Left { .. } => {
for (_, windows) in notification_windows.drain() {
for window in windows {
window

View File

@ -43,6 +43,7 @@ pub enum ListAlignment {
pub struct ListScrollEvent {
pub visible_range: Range<usize>,
pub count: usize,
pub is_scrolled: bool,
}
#[derive(Clone)]
@ -253,6 +254,7 @@ impl StateInner {
&ListScrollEvent {
visible_range,
count: self.items.summary().count,
is_scrolled: self.logical_scroll_top.is_some(),
},
cx,
);

View File

@ -1,7 +1,7 @@
use crate::{
self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position,
SharedString, StyleRefinement, Visibility, WhiteSpace,
DefiniteLength, Display, Fill, FlexDirection, FontWeight, Hsla, JustifyContent, Length,
Position, SharedString, StyleRefinement, Visibility, WhiteSpace,
};
use crate::{BoxShadow, TextStyleRefinement};
use smallvec::{smallvec, SmallVec};
@ -494,6 +494,13 @@ pub trait Styled: Sized {
self
}
fn font_weight(mut self, weight: FontWeight) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
.font_weight = Some(weight);
self
}
fn text_bg(mut self, bg: impl Into<Hsla>) -> Self {
self.text_style()
.get_or_insert_with(Default::default)

View File

@ -1,6 +1,6 @@
pub mod file_associations;
mod project_panel_settings;
use settings::{Settings, SettingsStore};
use settings::Settings;
use db::kvp::KEY_VALUE_STORE;
use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor};
@ -246,18 +246,6 @@ impl ProjectPanel {
};
this.update_visible_entries(None, cx);
// Update the dock position when the setting changes.
let mut old_dock_position = this.position(cx);
ProjectPanelSettings::register(cx);
cx.observe_global::<SettingsStore>(move |this, cx| {
let new_dock_position = this.position(cx);
if new_dock_position != old_dock_position {
old_dock_position = new_dock_position;
cx.emit(PanelEvent::ChangePosition);
}
})
.detach();
this
});

View File

@ -39,6 +39,7 @@ pub struct RichText {
/// Allows one to specify extra links to the rendered markdown, which can be used
/// for e.g. mentions.
#[derive(Debug)]
pub struct Mention {
pub range: Range<usize>,
pub is_self_mention: bool,
@ -113,8 +114,10 @@ pub fn render_markdown_mut(
if let Some(language) = &current_language {
render_code(text, highlights, t.as_ref(), language);
} else {
if let Some(mention) = mentions.first() {
if source_range.contains_inclusive(&mention.range) {
while let Some(mention) = mentions.first() {
if !source_range.contains_inclusive(&mention.range) {
break;
}
mentions = &mentions[1..];
let range = (prev_len + mention.range.start - source_range.start)
..(prev_len + mention.range.end - source_range.start);
@ -127,7 +130,6 @@ pub fn render_markdown_mut(
},
));
}
}
text.push_str(t.as_ref());
let mut style = HighlightStyle::default();

View File

@ -11,7 +11,7 @@ use itertools::Itertools;
use project::{Fs, ProjectEntryId};
use search::{buffer_search::DivRegistrar, BufferSearchBar};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use settings::Settings;
use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings};
use ui::{h_stack, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip};
use util::{ResultExt, TryFutureExt};
@ -159,15 +159,6 @@ impl TerminalPanel {
height: None,
_subscriptions: subscriptions,
};
let mut old_dock_position = this.position(cx);
cx.observe_global::<SettingsStore>(move |this, cx| {
let new_dock_position = this.position(cx);
if new_dock_position != old_dock_position {
old_dock_position = new_dock_position;
cx.emit(PanelEvent::ChangePosition);
}
})
.detach();
this
}

View File

@ -26,6 +26,7 @@ pub enum AvatarShape {
#[derive(IntoElement)]
pub struct Avatar {
image: Img,
size: Option<Pixels>,
border_color: Option<Hsla>,
is_available: Option<bool>,
}
@ -36,7 +37,7 @@ impl RenderOnce for Avatar {
self = self.shape(AvatarShape::Circle);
}
let size = cx.rem_size();
let size = self.size.unwrap_or_else(|| cx.rem_size());
div()
.size(size + px(2.))
@ -78,6 +79,7 @@ impl Avatar {
image: img(src),
is_available: None,
border_color: None,
size: None,
}
}
@ -124,4 +126,10 @@ impl Avatar {
self.is_available = is_available.into();
self
}
/// Size overrides the avatar size. By default they are 1rem.
pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self {
self.size = size.into();
self
}
}

View File

@ -7,6 +7,7 @@ use gpui::{
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::SettingsStore;
use std::sync::Arc;
use ui::{h_stack, ContextMenu, IconButton, Tooltip};
use ui::{prelude::*, right_click_menu};
@ -14,7 +15,6 @@ use ui::{prelude::*, right_click_menu};
const RESIZE_HANDLE_SIZE: Pixels = Pixels(6.);
pub enum PanelEvent {
ChangePosition,
ZoomIn,
ZoomOut,
Activate,
@ -177,7 +177,7 @@ impl DockPosition {
struct PanelEntry {
panel: Arc<dyn PanelHandle>,
_subscriptions: [Subscription; 2],
_subscriptions: [Subscription; 3],
}
pub struct PanelButtons {
@ -321,9 +321,15 @@ impl Dock {
) {
let subscriptions = [
cx.observe(&panel, |_, _, cx| cx.notify()),
cx.subscribe(&panel, move |this, panel, event, cx| match event {
PanelEvent::ChangePosition => {
cx.observe_global::<SettingsStore>({
let workspace = workspace.clone();
let panel = panel.clone();
move |this, cx| {
let new_position = panel.read(cx).position(cx);
if new_position == this.position {
return;
}
let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
if panel.is_zoomed(cx) {
@ -354,6 +360,8 @@ impl Dock {
}
});
}
}),
cx.subscribe(&panel, move |this, panel, event, cx| match event {
PanelEvent::ZoomIn => {
this.set_panel_zoomed(&panel.to_any(), true, cx);
if !panel.focus_handle(cx).contains_focused(cx) {
@ -737,7 +745,7 @@ pub mod test {
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
self.position = position;
cx.emit(PanelEvent::ChangePosition);
cx.update_global::<SettingsStore, _>(|_, _| {});
}
fn size(&self, _: &WindowContext) -> Pixels {