Collab ui2 (#3357)

* Clickable context menus & movable panels – what will they think of
next?!

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin 2023-11-17 13:33:18 -07:00 committed by GitHub
commit 624bd0a05a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 302 additions and 150 deletions

View file

@ -71,11 +71,12 @@ impl EntityMap {
#[track_caller] #[track_caller]
pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> { pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> {
self.assert_valid_context(model); self.assert_valid_context(model);
let entity = Some( let entity = Some(self.entities.remove(model.entity_id).unwrap_or_else(|| {
self.entities panic!(
.remove(model.entity_id) "Circular entity lease of {}. Is it already being updated?",
.expect("Circular entity lease. Is the entity already being updated?"), std::any::type_name::<T>()
); )
}));
Lease { Lease {
model, model,
entity, entity,

View file

@ -1124,9 +1124,14 @@ where
} }
} }
} }
// if self.hover_style.is_some() {
if bounds.contains_point(&mouse_position) { if bounds.contains_point(&mouse_position) {
// eprintln!("div hovered {bounds:?} {mouse_position:?}");
style.refine(&self.hover_style); style.refine(&self.hover_style);
} else {
// eprintln!("div NOT hovered {bounds:?} {mouse_position:?}");
} }
// }
if let Some(drag) = cx.active_drag.take() { if let Some(drag) = cx.active_drag.take() {
for (state_type, group_drag_style) in &self.group_drag_over_styles { for (state_type, group_drag_style) in &self.group_drag_over_styles {

View file

@ -1205,10 +1205,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
InputEvent::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return, InputEvent::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
InputEvent::MouseUp(MouseUpEvent { InputEvent::MouseUp(MouseUpEvent { .. }) => {
button: MouseButton::Left,
..
}) => {
lock.synthetic_drag_counter += 1; lock.synthetic_drag_counter += 1;
} }

View file

@ -191,6 +191,10 @@ impl AnyView {
self.model.entity_type self.model.entity_type
} }
pub fn entity_id(&self) -> EntityId {
self.model.entity_id()
}
pub(crate) fn draw( pub(crate) fn draw(
&self, &self,
origin: Point<Pixels>, origin: Point<Pixels>,

View file

@ -1,6 +1,6 @@
pub mod file_associations; pub mod file_associations;
mod project_panel_settings; mod project_panel_settings;
use settings::Settings; use settings::{Settings, SettingsStore};
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor}; use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor};
@ -34,7 +34,7 @@ use ui::{h_stack, v_stack, IconElement, Label};
use unicase::UniCase; use unicase::UniCase;
use util::{maybe, ResultExt, TryFutureExt}; use util::{maybe, ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
Workspace, Workspace,
}; };
@ -148,7 +148,6 @@ pub enum Event {
SplitEntry { SplitEntry {
entry_id: ProjectEntryId, entry_id: ProjectEntryId,
}, },
DockPositionChanged,
Focus, Focus,
NewSearchInDirectory { NewSearchInDirectory {
dir_entry: Entry, dir_entry: Entry,
@ -244,16 +243,17 @@ impl ProjectPanel {
this.update_visible_entries(None, cx); this.update_visible_entries(None, cx);
// Update the dock position when the setting changes. // Update the dock position when the setting changes.
// todo!() let mut old_dock_position = this.position(cx);
// let mut old_dock_position = this.position(cx); ProjectPanelSettings::register(cx);
// cx.observe_global::<SettingsStore, _>(move |this, cx| { cx.observe_global::<SettingsStore>(move |this, cx| {
// let new_dock_position = this.position(cx); dbg!("OLA!");
// if new_dock_position != old_dock_position { let new_dock_position = this.position(cx);
// old_dock_position = new_dock_position; if new_dock_position != old_dock_position {
// cx.emit(Event::DockPositionChanged); old_dock_position = new_dock_position;
// } cx.emit(PanelEvent::ChangePosition);
// }) }
// .detach(); })
.detach();
this this
}); });
@ -1485,7 +1485,7 @@ impl EventEmitter<Event> for ProjectPanel {}
impl EventEmitter<PanelEvent> for ProjectPanel {} impl EventEmitter<PanelEvent> for ProjectPanel {}
impl workspace::dock::Panel for ProjectPanel { impl Panel for ProjectPanel {
fn position(&self, cx: &WindowContext) -> DockPosition { fn position(&self, cx: &WindowContext) -> DockPosition {
match ProjectPanelSettings::get_global(cx).dock { match ProjectPanelSettings::get_global(cx).dock {
ProjectPanelDockPosition::Left => DockPosition::Left, ProjectPanelDockPosition::Left => DockPosition::Left,

View file

@ -77,6 +77,7 @@ pub fn handle_settings_file_changes(
}); });
cx.spawn(move |mut cx| async move { cx.spawn(move |mut cx| async move {
while let Some(user_settings_content) = user_settings_file_rx.next().await { while let Some(user_settings_content) = user_settings_file_rx.next().await {
eprintln!("settings file changed");
let result = cx.update_global(|store: &mut SettingsStore, cx| { let result = cx.update_global(|store: &mut SettingsStore, cx| {
store store
.set_user_settings(&user_settings_content, cx) .set_user_settings(&user_settings_content, cx)

View file

@ -32,7 +32,7 @@ use workspace::{
notifications::NotifyResultExt, notifications::NotifyResultExt,
register_deserializable_item, register_deserializable_item,
searchable::{SearchEvent, SearchOptions, SearchableItem}, searchable::{SearchEvent, SearchOptions, SearchableItem},
ui::{ContextMenu, Label}, ui::{ContextMenu, Label, ListEntry},
CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
}; };
@ -85,7 +85,7 @@ pub struct TerminalView {
has_new_content: bool, has_new_content: bool,
//Currently using iTerm bell, show bell emoji in tab until input is received //Currently using iTerm bell, show bell emoji in tab until input is received
has_bell: bool, has_bell: bool,
context_menu: Option<View<ContextMenu>>, context_menu: Option<View<ContextMenu<Self>>>,
blink_state: bool, blink_state: bool,
blinking_on: bool, blinking_on: bool,
blinking_paused: bool, blinking_paused: bool,
@ -300,11 +300,10 @@ impl TerminalView {
position: gpui::Point<Pixels>, position: gpui::Point<Pixels>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
self.context_menu = Some(cx.build_view(|cx| { self.context_menu = Some(ContextMenu::build(cx, |menu, _| {
ContextMenu::new(cx) menu.action(ListEntry::new(Label::new("Clear")), Box::new(Clear))
.entry(Label::new("Clear"), Box::new(Clear)) .action(
.entry( ListEntry::new(Label::new("Close")),
Label::new("Close"),
Box::new(CloseActiveItem { save_intent: None }), Box::new(CloseActiveItem { save_intent: None }),
) )
})); }));

View file

@ -6,42 +6,74 @@ use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHea
use gpui::{ use gpui::{
overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div, overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div,
FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View,
VisualContext, WeakView,
}; };
pub struct ContextMenu { pub enum ContextMenuItem<V> {
items: Vec<ListItem>, Separator(ListSeparator),
focus_handle: FocusHandle, Header(ListSubHeader),
Entry(
ListEntry<ContextMenu<V>>,
Rc<dyn Fn(&mut V, &mut ViewContext<V>)>,
),
} }
impl ManagedView for ContextMenu { pub struct ContextMenu<V> {
items: Vec<ContextMenuItem<V>>,
focus_handle: FocusHandle,
handle: WeakView<V>,
}
impl<V: Render> ManagedView for ContextMenu<V> {
fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle { fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle {
self.focus_handle.clone() self.focus_handle.clone()
} }
} }
impl ContextMenu { impl<V: Render> ContextMenu<V> {
pub fn new(cx: &mut WindowContext) -> Self { pub fn build(
Self { cx: &mut ViewContext<V>,
items: Default::default(), f: impl FnOnce(Self, &mut ViewContext<Self>) -> Self,
focus_handle: cx.focus_handle(), ) -> View<Self> {
} let handle = cx.view().downgrade();
cx.build_view(|cx| {
f(
Self {
handle,
items: Default::default(),
focus_handle: cx.focus_handle(),
},
cx,
)
})
} }
pub fn header(mut self, title: impl Into<SharedString>) -> Self { pub fn header(mut self, title: impl Into<SharedString>) -> Self {
self.items.push(ListItem::Header(ListSubHeader::new(title))); self.items
.push(ContextMenuItem::Header(ListSubHeader::new(title)));
self self
} }
pub fn separator(mut self) -> Self { pub fn separator(mut self) -> Self {
self.items.push(ListItem::Separator(ListSeparator)); self.items.push(ContextMenuItem::Separator(ListSeparator));
self self
} }
pub fn entry(mut self, label: Label, action: Box<dyn Action>) -> Self { pub fn entry(
self.items.push(ListEntry::new(label).action(action).into()); mut self,
view: ListEntry<Self>,
on_click: impl Fn(&mut V, &mut ViewContext<V>) + 'static,
) -> Self {
self.items
.push(ContextMenuItem::Entry(view, Rc::new(on_click)));
self self
} }
pub fn action(self, view: ListEntry<Self>, action: Box<dyn Action>) -> Self {
// todo: add the keybindings to the list entry
self.entry(view, move |_, cx| cx.dispatch_action(action.boxed_clone()))
}
pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) { pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
// todo!() // todo!()
cx.emit(Dismiss); cx.emit(Dismiss);
@ -52,9 +84,9 @@ impl ContextMenu {
} }
} }
impl Render for ContextMenu { impl<V: Render> Render for ContextMenu<V> {
type Element = Div<Self>; type Element = Div<Self>;
// todo!()
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div().elevation_2(cx).flex().flex_row().child( div().elevation_2(cx).flex().flex_row().child(
v_stack() v_stack()
@ -71,7 +103,25 @@ impl Render for ContextMenu {
// .bg(cx.theme().colors().elevated_surface_background) // .bg(cx.theme().colors().elevated_surface_background)
// .border() // .border()
// .border_color(cx.theme().colors().border) // .border_color(cx.theme().colors().border)
.child(List::new(self.items.clone())), .child(List::new(
self.items
.iter()
.map(|item| match item {
ContextMenuItem::Separator(separator) => {
ListItem::Separator(separator.clone())
}
ContextMenuItem::Header(header) => ListItem::Header(header.clone()),
ContextMenuItem::Entry(entry, callback) => {
let callback = callback.clone();
let handle = self.handle.clone();
ListItem::Entry(entry.clone().on_click(move |this, cx| {
handle.update(cx, |view, cx| callback(view, cx)).ok();
cx.emit(Dismiss);
}))
}
})
.collect(),
)),
) )
} }
} }
@ -232,6 +282,7 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
} }
}) })
.detach(); .detach();
cx.focus_view(&new_menu);
*menu.borrow_mut() = Some(new_menu); *menu.borrow_mut() = Some(new_menu);
*position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() { *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
@ -260,16 +311,25 @@ pub use stories::*;
mod stories { mod stories {
use super::*; use super::*;
use crate::story::Story; use crate::story::Story;
use gpui::{actions, Div, Render, VisualContext}; use gpui::{actions, Div, Render};
actions!(PrintCurrentDate); actions!(PrintCurrentDate, PrintBestFood);
fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<ContextMenu> { fn build_menu<V: Render>(
cx.build_view(|cx| { cx: &mut ViewContext<V>,
ContextMenu::new(cx).header(header).separator().entry( header: impl Into<SharedString>,
Label::new("Print current time"), ) -> View<ContextMenu<V>> {
PrintCurrentDate.boxed_clone(), let handle = cx.view().clone();
) ContextMenu::build(cx, |menu, _| {
menu.header(header)
.separator()
.entry(ListEntry::new(Label::new("Print current time")), |v, cx| {
println!("dispatching PrintCurrentTime action");
cx.dispatch_action(PrintCurrentDate.boxed_clone())
})
.entry(ListEntry::new(Label::new("Print best food")), |v, cx| {
cx.dispatch_action(PrintBestFood.boxed_clone())
})
}) })
} }
@ -281,10 +341,14 @@ mod stories {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx) Story::container(cx)
.on_action(|_, _: &PrintCurrentDate, _| { .on_action(|_, _: &PrintCurrentDate, _| {
println!("printing unix time!");
if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() { if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
println!("Current Unix time is {:?}", unix_time.as_secs()); println!("Current Unix time is {:?}", unix_time.as_secs());
} }
}) })
.on_action(|_, _: &PrintBestFood, _| {
println!("burrito");
})
.flex() .flex()
.flex_row() .flex_row()
.justify_between() .justify_between()

View file

@ -1,4 +1,6 @@
use gpui::{div, Action}; use std::rc::Rc;
use gpui::{div, Div, Stateful, StatefulInteractiveComponent};
use crate::settings::user_settings; use crate::settings::user_settings;
use crate::{ use crate::{
@ -172,35 +174,35 @@ pub enum ListEntrySize {
Medium, Medium,
} }
#[derive(Component, Clone)] #[derive(Clone)]
pub enum ListItem { pub enum ListItem<V: 'static> {
Entry(ListEntry), Entry(ListEntry<V>),
Separator(ListSeparator), Separator(ListSeparator),
Header(ListSubHeader), Header(ListSubHeader),
} }
impl From<ListEntry> for ListItem { impl<V: 'static> From<ListEntry<V>> for ListItem<V> {
fn from(entry: ListEntry) -> Self { fn from(entry: ListEntry<V>) -> Self {
Self::Entry(entry) Self::Entry(entry)
} }
} }
impl From<ListSeparator> for ListItem { impl<V: 'static> From<ListSeparator> for ListItem<V> {
fn from(entry: ListSeparator) -> Self { fn from(entry: ListSeparator) -> Self {
Self::Separator(entry) Self::Separator(entry)
} }
} }
impl From<ListSubHeader> for ListItem { impl<V: 'static> From<ListSubHeader> for ListItem<V> {
fn from(entry: ListSubHeader) -> Self { fn from(entry: ListSubHeader) -> Self {
Self::Header(entry) Self::Header(entry)
} }
} }
impl ListItem { impl<V: 'static> ListItem<V> {
fn render<V: 'static>(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> { fn render(self, view: &mut V, ix: usize, cx: &mut ViewContext<V>) -> impl Component<V> {
match self { match self {
ListItem::Entry(entry) => div().child(entry.render(view, cx)), ListItem::Entry(entry) => div().child(entry.render(ix, cx)),
ListItem::Separator(separator) => div().child(separator.render(view, cx)), ListItem::Separator(separator) => div().child(separator.render(view, cx)),
ListItem::Header(header) => div().child(header.render(view, cx)), ListItem::Header(header) => div().child(header.render(view, cx)),
} }
@ -210,7 +212,7 @@ impl ListItem {
Self::Entry(ListEntry::new(label)) Self::Entry(ListEntry::new(label))
} }
pub fn as_entry(&mut self) -> Option<&mut ListEntry> { pub fn as_entry(&mut self) -> Option<&mut ListEntry<V>> {
if let Self::Entry(entry) = self { if let Self::Entry(entry) = self {
Some(entry) Some(entry)
} else { } else {
@ -219,8 +221,7 @@ impl ListItem {
} }
} }
#[derive(Component)] pub struct ListEntry<V> {
pub struct ListEntry {
disabled: bool, disabled: bool,
// TODO: Reintroduce this // TODO: Reintroduce this
// disclosure_control_style: DisclosureControlVisibility, // disclosure_control_style: DisclosureControlVisibility,
@ -231,15 +232,13 @@ pub struct ListEntry {
size: ListEntrySize, size: ListEntrySize,
toggle: Toggle, toggle: Toggle,
variant: ListItemVariant, variant: ListItemVariant,
on_click: Option<Box<dyn Action>>, on_click: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) + 'static>>,
} }
impl Clone for ListEntry { impl<V> Clone for ListEntry<V> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
disabled: self.disabled, disabled: self.disabled,
// TODO: Reintroduce this
// disclosure_control_style: DisclosureControlVisibility,
indent_level: self.indent_level, indent_level: self.indent_level,
label: self.label.clone(), label: self.label.clone(),
left_slot: self.left_slot.clone(), left_slot: self.left_slot.clone(),
@ -247,12 +246,12 @@ impl Clone for ListEntry {
size: self.size, size: self.size,
toggle: self.toggle, toggle: self.toggle,
variant: self.variant, variant: self.variant,
on_click: self.on_click.as_ref().map(|opt| opt.boxed_clone()), on_click: self.on_click.clone(),
} }
} }
} }
impl ListEntry { impl<V: 'static> ListEntry<V> {
pub fn new(label: Label) -> Self { pub fn new(label: Label) -> Self {
Self { Self {
disabled: false, disabled: false,
@ -267,8 +266,8 @@ impl ListEntry {
} }
} }
pub fn action(mut self, action: impl Into<Box<dyn Action>>) -> Self { pub fn on_click(mut self, handler: impl Fn(&mut V, &mut ViewContext<V>) + 'static) -> Self {
self.on_click = Some(action.into()); self.on_click = Some(Rc::new(handler));
self self
} }
@ -307,7 +306,7 @@ impl ListEntry {
self self
} }
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> { fn render(self, ix: usize, cx: &mut ViewContext<V>) -> Stateful<V, Div<V>> {
let settings = user_settings(cx); let settings = user_settings(cx);
let left_content = match self.left_slot.clone() { let left_content = match self.left_slot.clone() {
@ -328,21 +327,21 @@ impl ListEntry {
ListEntrySize::Medium => div().h_7(), ListEntrySize::Medium => div().h_7(),
}; };
div() div()
.id(ix)
.relative() .relative()
.hover(|mut style| { .hover(|mut style| {
style.background = Some(cx.theme().colors().editor_background.into()); style.background = Some(cx.theme().colors().editor_background.into());
style style
}) })
.on_mouse_down(gpui::MouseButton::Left, { .on_click({
let action = self.on_click.map(|action| action.boxed_clone()); let on_click = self.on_click.clone();
move |entry: &mut V, event, cx| { move |view: &mut V, event, cx| {
if let Some(action) = action.as_ref() { if let Some(on_click) = &on_click {
cx.dispatch_action(action.boxed_clone()); (on_click)(view, cx)
} }
} }
}) })
.group("")
.bg(cx.theme().colors().surface_background) .bg(cx.theme().colors().surface_background)
// TODO: Add focus state // TODO: Add focus state
// .when(self.state == InteractionState::Focused, |this| { // .when(self.state == InteractionState::Focused, |this| {
@ -391,8 +390,8 @@ impl ListSeparator {
} }
#[derive(Component)] #[derive(Component)]
pub struct List { pub struct List<V: 'static> {
items: Vec<ListItem>, items: Vec<ListItem<V>>,
/// Message to display when the list is empty /// Message to display when the list is empty
/// Defaults to "No items" /// Defaults to "No items"
empty_message: SharedString, empty_message: SharedString,
@ -400,8 +399,8 @@ pub struct List {
toggle: Toggle, toggle: Toggle,
} }
impl List { impl<V: 'static> List<V> {
pub fn new(items: Vec<ListItem>) -> Self { pub fn new(items: Vec<ListItem<V>>) -> Self {
Self { Self {
items, items,
empty_message: "No items".into(), empty_message: "No items".into(),
@ -425,9 +424,14 @@ impl List {
self self
} }
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> { fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let list_content = match (self.items.is_empty(), self.toggle) { let list_content = match (self.items.is_empty(), self.toggle) {
(false, _) => div().children(self.items), (false, _) => div().children(
self.items
.into_iter()
.enumerate()
.map(|(ix, item)| item.render(view, ix, cx)),
),
(true, Toggle::Toggled(false)) => div(), (true, Toggle::Toggled(false)) => div(),
(true, _) => { (true, _) => {
div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted)) div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))

View file

@ -478,7 +478,7 @@ pub fn static_new_notification_items_2<V: 'static>() -> Vec<Notification<V>> {
] ]
} }
pub fn static_project_panel_project_items() -> Vec<ListItem> { pub fn static_project_panel_project_items<V>() -> Vec<ListItem<V>> {
vec![ vec![
ListEntry::new(Label::new("zed")) ListEntry::new(Label::new("zed"))
.left_icon(Icon::FolderOpen.into()) .left_icon(Icon::FolderOpen.into())
@ -605,7 +605,7 @@ pub fn static_project_panel_project_items() -> Vec<ListItem> {
.collect() .collect()
} }
pub fn static_project_panel_single_items() -> Vec<ListItem> { pub fn static_project_panel_single_items<V>() -> Vec<ListItem<V>> {
vec![ vec![
ListEntry::new(Label::new("todo.md")) ListEntry::new(Label::new("todo.md"))
.left_icon(Icon::FileDoc.into()) .left_icon(Icon::FileDoc.into())
@ -622,7 +622,7 @@ pub fn static_project_panel_single_items() -> Vec<ListItem> {
.collect() .collect()
} }
pub fn static_collab_panel_current_call() -> Vec<ListItem> { pub fn static_collab_panel_current_call<V>() -> Vec<ListItem<V>> {
vec![ vec![
ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"), ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"),
ListEntry::new(Label::new("nathansobo")) ListEntry::new(Label::new("nathansobo"))
@ -635,7 +635,7 @@ pub fn static_collab_panel_current_call() -> Vec<ListItem> {
.collect() .collect()
} }
pub fn static_collab_panel_channels() -> Vec<ListItem> { pub fn static_collab_panel_channels<V>() -> Vec<ListItem<V>> {
vec![ vec![
ListEntry::new(Label::new("zed")) ListEntry::new(Label::new("zed"))
.left_icon(Icon::Hash.into()) .left_icon(Icon::Hash.into())

View file

@ -8,7 +8,9 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use theme2::ActiveTheme; use theme2::ActiveTheme;
use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip}; use ui::{
h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Label, ListEntry, Tooltip,
};
pub enum PanelEvent { pub enum PanelEvent {
ChangePosition, ChangePosition,
@ -40,7 +42,7 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
} }
pub trait PanelHandle: Send + Sync { pub trait PanelHandle: Send + Sync {
fn id(&self) -> EntityId; fn entity_id(&self) -> EntityId;
fn persistent_name(&self) -> &'static str; fn persistent_name(&self) -> &'static str;
fn position(&self, cx: &WindowContext) -> DockPosition; fn position(&self, cx: &WindowContext) -> DockPosition;
fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool; fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
@ -62,8 +64,8 @@ impl<T> PanelHandle for View<T>
where where
T: Panel, T: Panel,
{ {
fn id(&self) -> EntityId { fn entity_id(&self) -> EntityId {
self.entity_id() Entity::entity_id(self)
} }
fn persistent_name(&self) -> &'static str { fn persistent_name(&self) -> &'static str {
@ -254,20 +256,19 @@ impl Dock {
} }
} }
// todo!() pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext<Self>) {
// pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext<Self>) { for entry in &mut self.panel_entries {
// for entry in &mut self.panel_entries { if entry.panel.entity_id() == panel.entity_id() {
// if entry.panel.as_any() == panel { if zoomed != entry.panel.is_zoomed(cx) {
// if zoomed != entry.panel.is_zoomed(cx) { entry.panel.set_zoomed(zoomed, cx);
// entry.panel.set_zoomed(zoomed, cx); }
// } } else if entry.panel.is_zoomed(cx) {
// } else if entry.panel.is_zoomed(cx) { entry.panel.set_zoomed(false, cx);
// entry.panel.set_zoomed(false, cx); }
// } }
// }
// cx.notify(); cx.notify();
// } }
pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) { pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
for entry in &mut self.panel_entries { for entry in &mut self.panel_entries {
@ -277,42 +278,91 @@ impl Dock {
} }
} }
pub(crate) fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>) { pub(crate) fn add_panel<T: Panel>(
&mut self,
panel: View<T>,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
) {
let subscriptions = [ let subscriptions = [
cx.observe(&panel, |_, _, cx| cx.notify()), cx.observe(&panel, |_, _, cx| cx.notify()),
cx.subscribe(&panel, |this, panel, event, cx| { cx.subscribe(&panel, move |this, panel, event, cx| match event {
match event { PanelEvent::ChangePosition => {
PanelEvent::ChangePosition => { let new_position = panel.read(cx).position(cx);
//todo!()
// see: Workspace::add_panel_with_extra_event_handler let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
} if panel.is_zoomed(cx) {
PanelEvent::ZoomIn => { workspace.zoomed_position = Some(new_position);
//todo!()
// see: Workspace::add_panel_with_extra_event_handler
}
PanelEvent::ZoomOut => {
// todo!()
// // see: Workspace::add_panel_with_extra_event_handler
}
PanelEvent::Activate => {
if let Some(ix) = this
.panel_entries
.iter()
.position(|entry| entry.panel.id() == panel.id())
{
this.set_open(true, cx);
this.activate_panel(ix, cx);
//` todo!()
// cx.focus(&panel);
} }
} match new_position {
PanelEvent::Close => { DockPosition::Left => &workspace.left_dock,
if this.visible_panel().map_or(false, |p| p.id() == panel.id()) { DockPosition::Bottom => &workspace.bottom_dock,
this.set_open(false, cx); DockPosition::Right => &workspace.right_dock,
} }
} .clone()
PanelEvent::Focus => todo!(), }) else {
return;
};
let was_visible = this.is_open()
&& this.visible_panel().map_or(false, |active_panel| {
active_panel.entity_id() == Entity::entity_id(&panel)
});
this.remove_panel(&panel, cx);
new_dock.update(cx, |new_dock, cx| {
new_dock.add_panel(panel.clone(), workspace.clone(), cx);
if was_visible {
new_dock.set_open(true, cx);
new_dock.activate_panel(this.panels_len() - 1, cx);
}
});
} }
PanelEvent::ZoomIn => {
this.set_panel_zoomed(&panel.to_any(), true, cx);
if !panel.has_focus(cx) {
cx.focus_view(&panel);
}
workspace
.update(cx, |workspace, cx| {
workspace.zoomed = Some(panel.downgrade().into());
workspace.zoomed_position = Some(panel.read(cx).position(cx));
})
.ok();
}
PanelEvent::ZoomOut => {
this.set_panel_zoomed(&panel.to_any(), false, cx);
workspace
.update(cx, |workspace, cx| {
if workspace.zoomed_position == Some(this.position) {
workspace.zoomed = None;
workspace.zoomed_position = None;
}
cx.notify();
})
.ok();
}
PanelEvent::Activate => {
if let Some(ix) = this
.panel_entries
.iter()
.position(|entry| entry.panel.entity_id() == Entity::entity_id(&panel))
{
this.set_open(true, cx);
this.activate_panel(ix, cx);
cx.focus_view(&panel);
}
}
PanelEvent::Close => {
if this
.visible_panel()
.map_or(false, |p| p.entity_id() == Entity::entity_id(&panel))
{
this.set_open(false, cx);
}
}
PanelEvent::Focus => todo!(),
}), }),
]; ];
@ -335,7 +385,7 @@ impl Dock {
if let Some(panel_ix) = self if let Some(panel_ix) = self
.panel_entries .panel_entries
.iter() .iter()
.position(|entry| entry.panel.id() == panel.id()) .position(|entry| entry.panel.entity_id() == Entity::entity_id(panel))
{ {
if panel_ix == self.active_panel_index { if panel_ix == self.active_panel_index {
self.active_panel_index = 0; self.active_panel_index = 0;
@ -396,7 +446,7 @@ impl Dock {
pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> { pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
self.panel_entries self.panel_entries
.iter() .iter()
.find(|entry| entry.panel.id() == panel.id()) .find(|entry| entry.panel.entity_id() == panel.entity_id())
.map(|entry| entry.panel.size(cx)) .map(|entry| entry.panel.size(cx))
} }
@ -620,6 +670,7 @@ impl Render for PanelButtons {
let dock = self.dock.read(cx); let dock = self.dock.read(cx);
let active_index = dock.active_panel_index; let active_index = dock.active_panel_index;
let is_open = dock.is_open; let is_open = dock.is_open;
let dock_position = dock.position;
let (menu_anchor, menu_attach) = match dock.position { let (menu_anchor, menu_attach) = match dock.position {
DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft), DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft),
@ -632,9 +683,10 @@ impl Render for PanelButtons {
.panel_entries .panel_entries
.iter() .iter()
.enumerate() .enumerate()
.filter_map(|(i, panel)| { .filter_map(|(i, entry)| {
let icon = panel.panel.icon(cx)?; let icon = entry.panel.icon(cx)?;
let name = panel.panel.persistent_name(); let name = entry.panel.persistent_name();
let panel = entry.panel.clone();
let mut button: IconButton<Self> = if i == active_index && is_open { let mut button: IconButton<Self> = if i == active_index && is_open {
let action = dock.toggle_action(); let action = dock.toggle_action();
@ -645,7 +697,7 @@ impl Render for PanelButtons {
.action(action.boxed_clone()) .action(action.boxed_clone())
.tooltip(move |_, cx| Tooltip::for_action(tooltip.clone(), &*action, cx)) .tooltip(move |_, cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
} else { } else {
let action = panel.panel.toggle_action(cx); let action = entry.panel.toggle_action(cx);
IconButton::new(name, icon) IconButton::new(name, icon)
.action(action.boxed_clone()) .action(action.boxed_clone())
@ -656,7 +708,30 @@ impl Render for PanelButtons {
menu_handle() menu_handle()
.id(name) .id(name)
.menu(move |_, cx| { .menu(move |_, cx| {
cx.build_view(|cx| ContextMenu::new(cx).header("SECTION")) const POSITIONS: [DockPosition; 3] = [
DockPosition::Left,
DockPosition::Right,
DockPosition::Bottom,
];
ContextMenu::build(cx, |mut menu, cx| {
for position in POSITIONS {
if position != dock_position
&& panel.position_is_valid(position, cx)
{
let panel = panel.clone();
menu = menu.entry(
ListEntry::new(Label::new(format!(
"Dock {}",
position.to_label()
))),
move |_, cx| {
panel.set_position(position, cx);
},
)
}
}
menu
})
}) })
.anchor(menu_anchor) .anchor(menu_anchor)
.attach(menu_attach) .attach(menu_attach)

View file

@ -813,7 +813,9 @@ impl Workspace {
DockPosition::Right => &self.right_dock, DockPosition::Right => &self.right_dock,
}; };
dock.update(cx, |dock, cx| dock.add_panel(panel, cx)); dock.update(cx, |dock, cx| {
dock.add_panel(panel, self.weak_self.clone(), cx)
});
} }
pub fn status_bar(&self) -> &View<StatusBar> { pub fn status_bar(&self) -> &View<StatusBar> {