Merge branch 'main' into search2

This commit is contained in:
Piotr Osiewicz 2023-11-16 17:16:15 +01:00
commit 6b6a30c3da
85 changed files with 15481 additions and 3134 deletions

79
Cargo.lock generated
View File

@ -1829,6 +1829,47 @@ 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",
"ui2",
"util",
"workspace2",
"zed_actions2",
]
[[package]]
name = "collections"
version = "0.1.0"
@ -9163,6 +9204,39 @@ dependencies = [
"workspace",
]
[[package]]
name = "terminal_view2"
version = "0.1.0"
dependencies = [
"anyhow",
"client2",
"db2",
"dirs 4.0.0",
"editor2",
"futures 0.3.28",
"gpui2",
"itertools 0.10.5",
"language2",
"lazy_static",
"libc",
"mio-extras",
"ordered-float 2.10.0",
"procinfo",
"project2",
"rand 0.8.5",
"serde",
"serde_derive",
"settings2",
"shellexpand",
"smallvec",
"smol",
"terminal2",
"theme2",
"thiserror",
"util",
"workspace2",
]
[[package]]
name = "text"
version = "0.1.0"
@ -10087,6 +10161,7 @@ dependencies = [
"chrono",
"gpui2",
"itertools 0.11.0",
"menu2",
"rand 0.8.5",
"serde",
"settings2",
@ -11317,7 +11392,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.113.0"
version = "0.114.0"
dependencies = [
"activity_indicator",
"ai",
@ -11470,6 +11545,7 @@ dependencies = [
"chrono",
"cli",
"client2",
"collab_ui2",
"collections",
"command_palette2",
"copilot2",
@ -11522,6 +11598,7 @@ dependencies = [
"smol",
"sum_tree",
"tempdir",
"terminal_view2",
"text2",
"theme2",
"thiserror",

View File

@ -18,6 +18,7 @@ members = [
"crates/collab",
"crates/collab2",
"crates/collab_ui",
"crates/collab_ui2",
"crates/collections",
"crates/command_palette",
"crates/command_palette2",
@ -98,6 +99,7 @@ members = [
"crates/sum_tree",
"crates/terminal",
"crates/terminal2",
"crates/terminal_view2",
"crates/text",
"crates/theme",
"crates/theme2",
@ -204,6 +206,9 @@ core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "07
[profile.dev]
split-debuginfo = "unpacked"
[profile.dev.package.taffy]
opt-level = 3
[profile.release]
debug = true
lto = "thin"

View File

@ -220,7 +220,6 @@ impl TestServer {
languages: Arc::new(language_registry),
fs: fs.clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| gpui::Task::ready(Ok(())),
node_runtime: FakeNodeRuntime::new(),
});

View File

@ -0,0 +1,81 @@
[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" }
ui = { package = "ui2", path = "../ui2" }
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

View File

@ -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::<ChannelView>(cx)
// }
// pub struct ChannelView {
// pub editor: ViewHandle<Editor>,
// project: ModelHandle<Project>,
// channel_store: ModelHandle<ChannelStore>,
// channel_buffer: ModelHandle<ChannelBuffer>,
// remote_id: Option<ViewId>,
// _editor_event_subscription: Subscription,
// }
// impl ChannelView {
// pub fn open(
// channel_id: ChannelId,
// workspace: ViewHandle<Workspace>,
// cx: &mut AppContext,
// ) -> Task<Result<ViewHandle<Self>>> {
// 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<Pane>,
// workspace: ViewHandle<Workspace>,
// cx: &mut AppContext,
// ) -> Task<Result<ViewHandle<Self>>> {
// 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::<Self>()
// .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<Project>,
// channel_store: ModelHandle<ChannelStore>,
// channel_buffer: ModelHandle<ChannelBuffer>,
// cx: &mut ViewContext<Self>,
// ) -> 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<Arc<Channel>> {
// self.channel_buffer.read(cx).channel(cx)
// }
// fn handle_channel_buffer_event(
// &mut self,
// _: ModelHandle<ChannelBuffer>,
// event: &ChannelBufferEvent,
// cx: &mut ViewContext<Self>,
// ) {
// 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<Self>) -> AnyElement<Self> {
// ChildView::new(self.editor.as_any(), cx).into_any()
// }
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
// 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<Self>,
// _: &'a AppContext,
// ) -> Option<&'a AnyViewHandle> {
// if type_id == TypeId::of::<Self>() {
// Some(self_handle)
// } else if type_id == TypeId::of::<Editor>() {
// Some(&self.editor)
// } else {
// None
// }
// }
// fn tab_content<V: 'static>(
// &self,
// _: Option<usize>,
// style: &theme::Tab,
// cx: &gpui::AppContext,
// ) -> AnyElement<V> {
// 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<Self>) -> Option<Self> {
// 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<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
// self.editor
// .update(cx, |editor, cx| editor.navigate(data, cx))
// }
// fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
// self.editor
// .update(cx, |editor, cx| Item::deactivated(editor, cx))
// }
// fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
// self.editor
// .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
// }
// fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
// Some(Box::new(self.editor.clone()))
// }
// fn show_toolbar(&self) -> bool {
// true
// }
// fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
// 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<workspace::ViewId> {
// self.remote_id
// }
// fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
// 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::Pane>,
// workspace: ViewHandle<workspace::Workspace>,
// remote_id: workspace::ViewId,
// state: &mut Option<proto::view::Variant>,
// cx: &mut AppContext,
// ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
// 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<proto::update_view::Variant>,
// cx: &AppContext,
// ) -> bool {
// self.editor
// .read(cx)
// .add_event_to_update_proto(event, update, cx)
// }
// fn apply_update_proto(
// &mut self,
// project: &ModelHandle<Project>,
// message: proto::update_view::Variant,
// cx: &mut ViewContext<Self>,
// ) -> gpui::Task<anyhow::Result<()>> {
// self.editor.update(cx, |editor, cx| {
// editor.apply_update_proto(project, message, cx)
// })
// }
// fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
// 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<ChannelBuffer>);
// impl CollaborationHub for ChannelBufferCollaborationHub {
// fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
// self.0.read(cx).collaborators()
// }
// fn user_participant_indices<'a>(
// &self,
// cx: &'a AppContext,
// ) -> &'a HashMap<u64, ParticipantIndex> {
// self.0.read(cx).user_store().read(cx).participant_indices()
// }
// }

View File

@ -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<Client>,
// channel_store: ModelHandle<ChannelStore>,
// languages: Arc<LanguageRegistry>,
// active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
// message_list: ListState<ChatPanel>,
// input_editor: ViewHandle<MessageEditor>,
// channel_select: ViewHandle<Select>,
// local_timezone: UtcOffset,
// fs: Arc<dyn Fs>,
// width: Option<f32>,
// active: bool,
// pending_serialization: Task<Option<()>>,
// subscriptions: Vec<gpui::Subscription>,
// workspace: WeakViewHandle<Workspace>,
// is_scrolled_to_bottom: bool,
// has_focus: bool,
// markdown_data: HashMap<ChannelMessageId, RichText>,
// }
// #[derive(Serialize, Deserialize)]
// struct SerializedChatPanel {
// width: Option<f32>,
// }
// #[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<Workspace>) -> ViewHandle<Self> {
// 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::<Self>::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::<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);
// }
// 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<ModelHandle<ChannelChat>> {
// self.active_chat.as_ref().map(|(chat, _)| chat.clone())
// }
// pub fn load(
// workspace: WeakViewHandle<Workspace>,
// cx: AsyncAppContext,
// ) -> Task<Result<ViewHandle<Self>>> {
// 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::<SerializedChatPanel>(&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<Self>) {
// 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<Self>) {
// 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<ChannelChat>, cx: &mut ViewContext<Self>) {
// 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<ChannelChat>,
// event: &ChannelChatEvent,
// cx: &mut ViewContext<Self>,
// ) {
// 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<Self>) -> AnyElement<Self> {
// 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<Theme>) -> AnyElement<Self> {
// 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<Self>) -> AnyElement<Self> {
// 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::<MessageBackgroundHighlight, _>(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<LanguageRegistry>,
// 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::<Vec<_>>();
// rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
// }
// fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
// ChildView::new(&self.input_editor, cx)
// .contained()
// .with_style(theme.chat_panel.input_editor.container)
// .into_any()
// }
// fn render_channel_name(
// channel_store: &ModelHandle<ChannelStore>,
// ix: usize,
// item_type: ItemType,
// is_hovered: bool,
// workspace: WeakViewHandle<Workspace>,
// cx: &mut ViewContext<Select>,
// ) -> AnyElement<Select> {
// 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::<OpenChannelNotes, _>(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::<OpenChannelNotes>(
// channel_id as usize,
// "Open Notes",
// Some(Box::new(OpenChannelNotes)),
// tooltip_style.clone(),
// cx,
// )
// .flex_float(),
// MouseEventHandler::new::<ActiveCall, _>(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::<ActiveCall>(
// 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<Theme>,
// cx: &mut ViewContext<Self>,
// ) -> AnyElement<Self> {
// enum SignInPromptLabel {}
// MouseEventHandler::new::<SignInPromptLabel, _>(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<Self>) {
// 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<Self>) {
// 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<Self>) {
// 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<u64>,
// cx: &mut ViewContext<ChatPanel>,
// ) -> Task<Result<()>> {
// 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<Self>) {
// 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<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);
// }
// }
// }
// fn render_remove(
// message_id_to_remove: Option<u64>,
// cx: &mut ViewContext<'_, '_, ChatPanel>,
// theme: &Arc<Theme>,
// ) -> AnyElement<ChatPanel> {
// enum DeleteMessage {}
// message_id_to_remove
// .map(|id| {
// MouseEventHandler::new::<DeleteMessage, _>(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<Self>) -> AnyElement<Self> {
// 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>) {
// 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>) {
// self.has_focus = false;
// }
// }
// impl Panel for ChatPanel {
// fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
// settings::get::<ChatPanelSettings>(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<Self>) {
// settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
// settings.dock = Some(position)
// });
// }
// fn size(&self, cx: &gpui::WindowContext) -> f32 {
// self.width
// .unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
// }
// fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
// self.width = size;
// self.serialize(cx);
// cx.notify();
// }
// fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
// 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::<ChatPanelSettings>(cx).button && is_channels_feature_enabled(cx))
// .then(|| "icons/conversations.svg")
// }
// fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
// ("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<V: View>(style: &IconButton, svg_path: &'static str) -> impl Element<V> {
// 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», lets «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
// },
// ]
// );
// }
// }

View File

@ -0,0 +1,313 @@
use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
use client::UserId;
use collections::HashMap;
use editor::{AnchorRangeExt, Editor};
use gpui::{
elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
use lazy_static::lazy_static;
use project::search::SearchQuery;
use std::{sync::Arc, time::Duration};
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
lazy_static! {
static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex(
"@[-_\\w]+",
false,
false,
Default::default(),
Default::default()
)
.unwrap();
}
pub struct MessageEditor {
pub editor: ViewHandle<Editor>,
channel_store: ModelHandle<ChannelStore>,
users: HashMap<String, UserId>,
mentions: Vec<UserId>,
mentions_task: Option<Task<()>>,
channel_id: Option<ChannelId>,
}
impl MessageEditor {
pub fn new(
language_registry: Arc<LanguageRegistry>,
channel_store: ModelHandle<ChannelStore>,
editor: ViewHandle<Editor>,
cx: &mut ViewContext<Self>,
) -> Self {
editor.update(cx, |editor, cx| {
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
});
let buffer = editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.expect("message editor must be singleton");
cx.subscribe(&buffer, Self::on_buffer_event).detach();
let markdown = language_registry.language_for_name("Markdown");
cx.app_context()
.spawn(|mut cx| async move {
let markdown = markdown.await?;
buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx)
});
anyhow::Ok(())
})
.detach_and_log_err(cx);
Self {
editor,
channel_store,
users: HashMap::default(),
channel_id: None,
mentions: Vec::new(),
mentions_task: None,
}
}
pub fn set_channel(
&mut self,
channel_id: u64,
channel_name: Option<String>,
cx: &mut ViewContext<Self>,
) {
self.editor.update(cx, |editor, cx| {
if let Some(channel_name) = channel_name {
editor.set_placeholder_text(format!("Message #{}", channel_name), cx);
} else {
editor.set_placeholder_text(format!("Message Channel"), cx);
}
});
self.channel_id = Some(channel_id);
self.refresh_users(cx);
}
pub fn refresh_users(&mut self, cx: &mut ViewContext<Self>) {
if let Some(channel_id) = self.channel_id {
let members = self.channel_store.update(cx, |store, cx| {
store.get_channel_member_details(channel_id, cx)
});
cx.spawn(|this, mut cx| async move {
let members = members.await?;
this.update(&mut cx, |this, cx| this.set_members(members, cx))?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
}
pub fn set_members(&mut self, members: Vec<ChannelMembership>, _: &mut ViewContext<Self>) {
self.users.clear();
self.users.extend(
members
.into_iter()
.map(|member| (member.user.github_login.clone(), member.user.id)),
);
}
pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> MessageParams {
self.editor.update(cx, |editor, cx| {
let highlights = editor.text_highlights::<Self>(cx);
let text = editor.text(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
let mentions = if let Some((_, ranges)) = highlights {
ranges
.iter()
.map(|range| range.to_offset(&snapshot))
.zip(self.mentions.iter().copied())
.collect()
} else {
Vec::new()
};
editor.clear(cx);
self.mentions.clear();
MessageParams { text, mentions }
})
}
fn on_buffer_event(
&mut self,
buffer: ModelHandle<Buffer>,
event: &language::Event,
cx: &mut ViewContext<Self>,
) {
if let language::Event::Reparsed | language::Event::Edited = event {
let buffer = buffer.read(cx).snapshot();
self.mentions_task = Some(cx.spawn(|this, cx| async move {
cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
Self::find_mentions(this, buffer, cx).await;
}));
}
}
async fn find_mentions(
this: WeakViewHandle<MessageEditor>,
buffer: BufferSnapshot,
mut cx: AsyncAppContext,
) {
let (buffer, ranges) = cx
.background()
.spawn(async move {
let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
(buffer, ranges)
})
.await;
this.update(&mut cx, |this, cx| {
let mut anchor_ranges = Vec::new();
let mut mentioned_user_ids = Vec::new();
let mut text = String::new();
this.editor.update(cx, |editor, cx| {
let multi_buffer = editor.buffer().read(cx).snapshot(cx);
for range in ranges {
text.clear();
text.extend(buffer.text_for_range(range.clone()));
if let Some(username) = text.strip_prefix("@") {
if let Some(user_id) = this.users.get(username) {
let start = multi_buffer.anchor_after(range.start);
let end = multi_buffer.anchor_after(range.end);
mentioned_user_ids.push(*user_id);
anchor_ranges.push(start..end);
}
}
}
editor.clear_highlights::<Self>(cx);
editor.highlight_text::<Self>(
anchor_ranges,
theme::current(cx).chat_panel.rich_text.mention_highlight,
cx,
)
});
this.mentions = mentioned_user_ids;
this.mentions_task.take();
})
.ok();
}
}
impl Entity for MessageEditor {
type Event = ();
}
impl View for MessageEditor {
fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
ChildView::new(&self.editor, cx).into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.editor);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use client::{Client, User, UserStore};
use gpui::{TestAppContext, WindowHandle};
use language::{Language, LanguageConfig};
use rpc::proto;
use settings::SettingsStore;
use util::{http::FakeHttpClient, test::marked_text_ranges};
#[gpui::test]
async fn test_message_editor(cx: &mut TestAppContext) {
let editor = init_test(cx);
let editor = editor.root(cx);
editor.update(cx, |editor, cx| {
editor.set_members(
vec![
ChannelMembership {
user: Arc::new(User {
github_login: "a-b".into(),
id: 101,
avatar: None,
}),
kind: proto::channel_member::Kind::Member,
role: proto::ChannelRole::Member,
},
ChannelMembership {
user: Arc::new(User {
github_login: "C_D".into(),
id: 102,
avatar: None,
}),
kind: proto::channel_member::Kind::Member,
role: proto::ChannelRole::Member,
},
],
cx,
);
editor.editor.update(cx, |editor, cx| {
editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
});
});
cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
editor.update(cx, |editor, cx| {
let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
assert_eq!(
editor.take_message(cx),
MessageParams {
text,
mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
}
);
});
}
fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
cx.foreground().forbid_parking();
cx.update(|cx| {
let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone(), cx);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
language::init(cx);
editor::init(cx);
client::init(&client, cx);
channel::init(&client, user_store, cx);
});
let language_registry = Arc::new(LanguageRegistry::test());
language_registry.add(Arc::new(Language::new(
LanguageConfig {
name: "Markdown".into(),
..Default::default()
},
Some(tree_sitter_markdown::language()),
)));
let editor = cx.add_window(|cx| {
MessageEditor::new(
language_registry,
ChannelStore::global(cx),
cx.add_view(|cx| Editor::auto_height(4, None, cx)),
cx,
)
});
cx.foreground().run_until_parked();
editor
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,717 @@
use channel::{ChannelId, ChannelMembership, ChannelStore};
use client::{
proto::{self, ChannelRole, ChannelVisibility},
User, UserId, UserStore,
};
use context_menu::{ContextMenu, ContextMenuItem};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions,
elements::*,
platform::{CursorStyle, MouseButton},
AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
ViewHandle,
};
use picker::{Picker, PickerDelegate, PickerEvent};
use std::sync::Arc;
use util::TryFutureExt;
use workspace::Modal;
actions!(
channel_modal,
[
SelectNextControl,
ToggleMode,
ToggleMemberAdmin,
RemoveMember
]
);
pub fn init(cx: &mut AppContext) {
Picker::<ChannelModalDelegate>::init(cx);
cx.add_action(ChannelModal::toggle_mode);
cx.add_action(ChannelModal::toggle_member_admin);
cx.add_action(ChannelModal::remove_member);
cx.add_action(ChannelModal::dismiss);
}
pub struct ChannelModal {
picker: ViewHandle<Picker<ChannelModalDelegate>>,
channel_store: ModelHandle<ChannelStore>,
channel_id: ChannelId,
has_focus: bool,
}
impl ChannelModal {
pub fn new(
user_store: ModelHandle<UserStore>,
channel_store: ModelHandle<ChannelStore>,
channel_id: ChannelId,
mode: Mode,
members: Vec<ChannelMembership>,
cx: &mut ViewContext<Self>,
) -> Self {
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
let picker = cx.add_view(|cx| {
Picker::new(
ChannelModalDelegate {
matching_users: Vec::new(),
matching_member_indices: Vec::new(),
selected_index: 0,
user_store: user_store.clone(),
channel_store: channel_store.clone(),
channel_id,
match_candidates: Vec::new(),
members,
mode,
context_menu: cx.add_view(|cx| {
let mut menu = ContextMenu::new(cx.view_id(), cx);
menu.set_position_mode(OverlayPositionMode::Local);
menu
}),
},
cx,
)
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
});
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
let has_focus = picker.read(cx).has_focus();
Self {
picker,
channel_store,
channel_id,
has_focus,
}
}
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
let mode = match self.picker.read(cx).delegate().mode {
Mode::ManageMembers => Mode::InviteMembers,
Mode::InviteMembers => Mode::ManageMembers,
};
self.set_mode(mode, cx);
}
fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
let channel_store = self.channel_store.clone();
let channel_id = self.channel_id;
cx.spawn(|this, mut cx| async move {
if mode == Mode::ManageMembers {
let mut members = channel_store
.update(&mut cx, |channel_store, cx| {
channel_store.get_channel_member_details(channel_id, cx)
})
.await?;
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
this.update(&mut cx, |this, cx| {
this.picker
.update(cx, |picker, _| picker.delegate_mut().members = members);
})?;
}
this.update(&mut cx, |this, cx| {
this.picker.update(cx, |picker, cx| {
let delegate = picker.delegate_mut();
delegate.mode = mode;
delegate.selected_index = 0;
picker.set_query("", cx);
picker.update_matches(picker.query(cx), cx);
cx.notify()
});
cx.notify()
})
})
.detach();
}
fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
picker.delegate_mut().toggle_selected_member_admin(cx);
})
}
fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
picker.delegate_mut().remove_selected_member(cx);
});
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(PickerEvent::Dismiss);
}
}
impl Entity for ChannelModal {
type Event = PickerEvent;
}
impl View for ChannelModal {
fn ui_name() -> &'static str {
"ChannelModal"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).collab_panel.tabbed_modal;
let mode = self.picker.read(cx).delegate().mode;
let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
return Empty::new().into_any();
};
enum InviteMembers {}
enum ManageMembers {}
fn render_mode_button<T: 'static>(
mode: Mode,
text: &'static str,
current_mode: Mode,
theme: &theme::TabbedModal,
cx: &mut ViewContext<ChannelModal>,
) -> AnyElement<ChannelModal> {
let active = mode == current_mode;
MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
let contained_text = theme.tab_button.style_for(active, state);
Label::new(text, contained_text.text.clone())
.contained()
.with_style(contained_text.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
if !active {
this.set_mode(mode, cx);
}
})
.with_cursor_style(CursorStyle::PointingHand)
.into_any()
}
fn render_visibility(
channel_id: ChannelId,
visibility: ChannelVisibility,
theme: &theme::TabbedModal,
cx: &mut ViewContext<ChannelModal>,
) -> AnyElement<ChannelModal> {
enum TogglePublic {}
if visibility == ChannelVisibility::Members {
return Flex::row()
.with_child(
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
let style = theme.visibility_toggle.style_for(state);
Label::new(format!("{}", "Public access: OFF"), style.text.clone())
.contained()
.with_style(style.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
this.channel_store
.update(cx, |channel_store, cx| {
channel_store.set_channel_visibility(
channel_id,
ChannelVisibility::Public,
cx,
)
})
.detach_and_log_err(cx);
})
.with_cursor_style(CursorStyle::PointingHand),
)
.into_any();
}
Flex::row()
.with_child(
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
let style = theme.visibility_toggle.style_for(state);
Label::new(format!("{}", "Public access: ON"), style.text.clone())
.contained()
.with_style(style.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
this.channel_store
.update(cx, |channel_store, cx| {
channel_store.set_channel_visibility(
channel_id,
ChannelVisibility::Members,
cx,
)
})
.detach_and_log_err(cx);
})
.with_cursor_style(CursorStyle::PointingHand),
)
.with_spacing(14.0)
.with_child(
MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
let style = theme.channel_link.style_for(state);
Label::new(format!("{}", "copy link"), style.text.clone())
.contained()
.with_style(style.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(channel) =
this.channel_store.read(cx).channel_for_id(channel_id)
{
let item = ClipboardItem::new(channel.link());
cx.write_to_clipboard(item);
}
})
.with_cursor_style(CursorStyle::PointingHand),
)
.into_any()
}
Flex::column()
.with_child(
Flex::column()
.with_child(
Label::new(format!("#{}", channel.name), theme.title.text.clone())
.contained()
.with_style(theme.title.container.clone()),
)
.with_child(render_visibility(channel.id, channel.visibility, theme, cx))
.with_child(Flex::row().with_children([
render_mode_button::<InviteMembers>(
Mode::InviteMembers,
"Invite members",
mode,
theme,
cx,
),
render_mode_button::<ManageMembers>(
Mode::ManageMembers,
"Manage members",
mode,
theme,
cx,
),
]))
.expanded()
.contained()
.with_style(theme.header),
)
.with_child(
ChildView::new(&self.picker, cx)
.contained()
.with_style(theme.body),
)
.constrained()
.with_max_height(theme.max_height)
.with_max_width(theme.max_width)
.contained()
.with_style(theme.modal)
.into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_focus = true;
if cx.is_self_focused() {
cx.focus(&self.picker)
}
}
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
self.has_focus = false;
}
}
impl Modal for ChannelModal {
fn has_focus(&self) -> bool {
self.has_focus
}
fn dismiss_on_event(event: &Self::Event) -> bool {
match event {
PickerEvent::Dismiss => true,
}
}
}
#[derive(Copy, Clone, PartialEq)]
pub enum Mode {
ManageMembers,
InviteMembers,
}
pub struct ChannelModalDelegate {
matching_users: Vec<Arc<User>>,
matching_member_indices: Vec<usize>,
user_store: ModelHandle<UserStore>,
channel_store: ModelHandle<ChannelStore>,
channel_id: ChannelId,
selected_index: usize,
mode: Mode,
match_candidates: Vec<StringMatchCandidate>,
members: Vec<ChannelMembership>,
context_menu: ViewHandle<ContextMenu>,
}
impl PickerDelegate for ChannelModalDelegate {
fn placeholder_text(&self) -> Arc<str> {
"Search collaborator by username...".into()
}
fn match_count(&self) -> usize {
match self.mode {
Mode::ManageMembers => self.matching_member_indices.len(),
Mode::InviteMembers => self.matching_users.len(),
}
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
match self.mode {
Mode::ManageMembers => {
self.match_candidates.clear();
self.match_candidates
.extend(self.members.iter().enumerate().map(|(id, member)| {
StringMatchCandidate {
id,
string: member.user.github_login.clone(),
char_bag: member.user.github_login.chars().collect(),
}
}));
let matches = cx.background().block(match_strings(
&self.match_candidates,
&query,
true,
usize::MAX,
&Default::default(),
cx.background().clone(),
));
cx.spawn(|picker, mut cx| async move {
picker
.update(&mut cx, |picker, cx| {
let delegate = picker.delegate_mut();
delegate.matching_member_indices.clear();
delegate
.matching_member_indices
.extend(matches.into_iter().map(|m| m.candidate_id));
cx.notify();
})
.ok();
})
}
Mode::InviteMembers => {
let search_users = self
.user_store
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
cx.spawn(|picker, mut cx| async move {
async {
let users = search_users.await?;
picker.update(&mut cx, |picker, cx| {
let delegate = picker.delegate_mut();
delegate.matching_users = users;
cx.notify();
})?;
anyhow::Ok(())
}
.log_err()
.await;
})
}
}
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
match self.mode {
Mode::ManageMembers => {
self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
}
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
Some(proto::channel_member::Kind::Invitee) => {
self.remove_selected_member(cx);
}
Some(proto::channel_member::Kind::AncestorMember) | None => {
self.invite_member(selected_user, cx)
}
Some(proto::channel_member::Kind::Member) => {}
},
}
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
cx.emit(PickerEvent::Dismiss);
}
fn render_match(
&self,
ix: usize,
mouse_state: &mut MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> {
let full_theme = &theme::current(cx);
let theme = &full_theme.collab_panel.channel_modal;
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
let (user, role) = self.user_at_index(ix).unwrap();
let request_status = self.member_status(user.id, cx);
let style = tabbed_modal
.picker
.item
.in_state(selected)
.style_for(mouse_state);
let in_manage = matches!(self.mode, Mode::ManageMembers);
let mut result = Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Image::from_data(avatar)
.with_style(theme.contact_avatar)
.aligned()
.left()
}))
.with_child(
Label::new(user.github_login.clone(), style.label.clone())
.contained()
.with_style(theme.contact_username)
.aligned()
.left(),
)
.with_children({
(in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
|| {
Label::new("Invited", theme.member_tag.text.clone())
.contained()
.with_style(theme.member_tag.container)
.aligned()
.left()
},
)
})
.with_children(if in_manage && role == Some(ChannelRole::Admin) {
Some(
Label::new("Admin", theme.member_tag.text.clone())
.contained()
.with_style(theme.member_tag.container)
.aligned()
.left(),
)
} else if in_manage && role == Some(ChannelRole::Guest) {
Some(
Label::new("Guest", theme.member_tag.text.clone())
.contained()
.with_style(theme.member_tag.container)
.aligned()
.left(),
)
} else {
None
})
.with_children({
let svg = match self.mode {
Mode::ManageMembers => Some(
Svg::new("icons/ellipsis.svg")
.with_color(theme.member_icon.color)
.constrained()
.with_width(theme.member_icon.icon_width)
.aligned()
.constrained()
.with_width(theme.member_icon.button_width)
.with_height(theme.member_icon.button_width)
.contained()
.with_style(theme.member_icon.container),
),
Mode::InviteMembers => match request_status {
Some(proto::channel_member::Kind::Member) => Some(
Svg::new("icons/check.svg")
.with_color(theme.member_icon.color)
.constrained()
.with_width(theme.member_icon.icon_width)
.aligned()
.constrained()
.with_width(theme.member_icon.button_width)
.with_height(theme.member_icon.button_width)
.contained()
.with_style(theme.member_icon.container),
),
Some(proto::channel_member::Kind::Invitee) => Some(
Svg::new("icons/check.svg")
.with_color(theme.invitee_icon.color)
.constrained()
.with_width(theme.invitee_icon.icon_width)
.aligned()
.constrained()
.with_width(theme.invitee_icon.button_width)
.with_height(theme.invitee_icon.button_width)
.contained()
.with_style(theme.invitee_icon.container),
),
Some(proto::channel_member::Kind::AncestorMember) | None => None,
},
};
svg.map(|svg| svg.aligned().flex_float().into_any())
})
.contained()
.with_style(style.container)
.constrained()
.with_height(tabbed_modal.row_height)
.into_any();
if selected {
result = Stack::new()
.with_child(result)
.with_child(
ChildView::new(&self.context_menu, cx)
.aligned()
.top()
.right(),
)
.into_any();
}
result
}
}
impl ChannelModalDelegate {
fn member_status(
&self,
user_id: UserId,
cx: &AppContext,
) -> Option<proto::channel_member::Kind> {
self.members
.iter()
.find_map(|membership| (membership.user.id == user_id).then_some(membership.kind))
.or_else(|| {
self.channel_store
.read(cx)
.has_pending_channel_invite(self.channel_id, user_id)
.then_some(proto::channel_member::Kind::Invitee)
})
}
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<ChannelRole>)> {
match self.mode {
Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
let channel_membership = self.members.get(*ix)?;
Some((
channel_membership.user.clone(),
Some(channel_membership.role),
))
}),
Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
}
}
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
let (user, role) = self.user_at_index(self.selected_index)?;
let new_role = if role == Some(ChannelRole::Admin) {
ChannelRole::Member
} else {
ChannelRole::Admin
};
let update = self.channel_store.update(cx, |store, cx| {
store.set_member_role(self.channel_id, user.id, new_role, cx)
});
cx.spawn(|picker, mut cx| async move {
update.await?;
picker.update(&mut cx, |picker, cx| {
let this = picker.delegate_mut();
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
member.role = new_role;
}
cx.focus_self();
cx.notify();
})
})
.detach_and_log_err(cx);
Some(())
}
fn remove_selected_member(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
let (user, _) = self.user_at_index(self.selected_index)?;
let user_id = user.id;
let update = self.channel_store.update(cx, |store, cx| {
store.remove_member(self.channel_id, user_id, cx)
});
cx.spawn(|picker, mut cx| async move {
update.await?;
picker.update(&mut cx, |picker, cx| {
let this = picker.delegate_mut();
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
this.members.remove(ix);
this.matching_member_indices.retain_mut(|member_ix| {
if *member_ix == ix {
return false;
} else if *member_ix > ix {
*member_ix -= 1;
}
true
})
}
this.selected_index = this
.selected_index
.min(this.matching_member_indices.len().saturating_sub(1));
cx.focus_self();
cx.notify();
})
})
.detach_and_log_err(cx);
Some(())
}
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
let invite_member = self.channel_store.update(cx, |store, cx| {
store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
});
cx.spawn(|this, mut cx| async move {
invite_member.await?;
this.update(&mut cx, |this, cx| {
let new_member = ChannelMembership {
user,
kind: proto::channel_member::Kind::Invitee,
role: ChannelRole::Member,
};
let members = &mut this.delegate_mut().members;
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
Ok(ix) | Err(ix) => members.insert(ix, new_member),
}
cx.notify();
})
})
.detach_and_log_err(cx);
}
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
self.context_menu.update(cx, |context_menu, cx| {
context_menu.show(
Default::default(),
AnchorCorner::TopRight,
vec![
ContextMenuItem::action("Remove", RemoveMember),
ContextMenuItem::action(
if role == ChannelRole::Admin {
"Make non-admin"
} else {
"Make admin"
},
ToggleMemberAdmin,
),
],
cx,
)
})
}
}

View File

@ -0,0 +1,261 @@
use client::{ContactRequestStatus, User, UserStore};
use gpui::{
elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate, PickerEvent};
use std::sync::Arc;
use util::TryFutureExt;
use workspace::Modal;
pub fn init(cx: &mut AppContext) {
Picker::<ContactFinderDelegate>::init(cx);
cx.add_action(ContactFinder::dismiss)
}
pub struct ContactFinder {
picker: ViewHandle<Picker<ContactFinderDelegate>>,
has_focus: bool,
}
impl ContactFinder {
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
let picker = cx.add_view(|cx| {
Picker::new(
ContactFinderDelegate {
user_store,
potential_contacts: Arc::from([]),
selected_index: 0,
},
cx,
)
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
});
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
Self {
picker,
has_focus: false,
}
}
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
picker.set_query(query, cx);
});
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(PickerEvent::Dismiss);
}
}
impl Entity for ContactFinder {
type Event = PickerEvent;
}
impl View for ContactFinder {
fn ui_name() -> &'static str {
"ContactFinder"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let full_theme = &theme::current(cx);
let theme = &full_theme.collab_panel.tabbed_modal;
fn render_mode_button(
text: &'static str,
theme: &theme::TabbedModal,
_cx: &mut ViewContext<ContactFinder>,
) -> AnyElement<ContactFinder> {
let contained_text = &theme.tab_button.active_state().default;
Label::new(text, contained_text.text.clone())
.contained()
.with_style(contained_text.container.clone())
.into_any()
}
Flex::column()
.with_child(
Flex::column()
.with_child(
Label::new("Contacts", theme.title.text.clone())
.contained()
.with_style(theme.title.container.clone()),
)
.with_child(Flex::row().with_children([render_mode_button(
"Invite new contacts",
&theme,
cx,
)]))
.expanded()
.contained()
.with_style(theme.header),
)
.with_child(
ChildView::new(&self.picker, cx)
.contained()
.with_style(theme.body),
)
.constrained()
.with_max_height(theme.max_height)
.with_max_width(theme.max_width)
.contained()
.with_style(theme.modal)
.into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_focus = true;
if cx.is_self_focused() {
cx.focus(&self.picker)
}
}
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
self.has_focus = false;
}
}
impl Modal for ContactFinder {
fn has_focus(&self) -> bool {
self.has_focus
}
fn dismiss_on_event(event: &Self::Event) -> bool {
match event {
PickerEvent::Dismiss => true,
}
}
}
pub struct ContactFinderDelegate {
potential_contacts: Arc<[Arc<User>]>,
user_store: ModelHandle<UserStore>,
selected_index: usize,
}
impl PickerDelegate for ContactFinderDelegate {
fn placeholder_text(&self) -> Arc<str> {
"Search collaborator by username...".into()
}
fn match_count(&self) -> usize {
self.potential_contacts.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let search_users = self
.user_store
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
cx.spawn(|picker, mut cx| async move {
async {
let potential_contacts = search_users.await?;
picker.update(&mut cx, |picker, cx| {
picker.delegate_mut().potential_contacts = potential_contacts.into();
cx.notify();
})?;
anyhow::Ok(())
}
.log_err()
.await;
})
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(user) = self.potential_contacts.get(self.selected_index) {
let user_store = self.user_store.read(cx);
match user_store.contact_request_status(user) {
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
self.user_store
.update(cx, |store, cx| store.request_contact(user.id, cx))
.detach();
}
ContactRequestStatus::RequestSent => {
self.user_store
.update(cx, |store, cx| store.remove_contact(user.id, cx))
.detach();
}
_ => {}
}
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
cx.emit(PickerEvent::Dismiss);
}
fn render_match(
&self,
ix: usize,
mouse_state: &mut MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> {
let full_theme = &theme::current(cx);
let theme = &full_theme.collab_panel.contact_finder;
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
let user = &self.potential_contacts[ix];
let request_status = self.user_store.read(cx).contact_request_status(user);
let icon_path = match request_status {
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
Some("icons/check.svg")
}
ContactRequestStatus::RequestSent => Some("icons/x.svg"),
ContactRequestStatus::RequestAccepted => None,
};
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
&theme.disabled_contact_button
} else {
&theme.contact_button
};
let style = tabbed_modal
.picker
.item
.in_state(selected)
.style_for(mouse_state);
Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Image::from_data(avatar)
.with_style(theme.contact_avatar)
.aligned()
.left()
}))
.with_child(
Label::new(user.github_login.clone(), style.label.clone())
.contained()
.with_style(theme.contact_username)
.aligned()
.left(),
)
.with_children(icon_path.map(|icon_path| {
Svg::new(icon_path)
.with_color(button_style.color)
.constrained()
.with_width(button_style.icon_width)
.aligned()
.contained()
.with_style(button_style.container)
.constrained()
.with_width(button_style.button_width)
.with_height(button_style.button_width)
.aligned()
.flex_float()
}))
.contained()
.with_style(style.container)
.constrained()
.with_height(tabbed_modal.row_height)
.into_any()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,154 @@
pub mod channel_view;
pub mod chat_panel;
pub mod collab_panel;
mod collab_titlebar_item;
mod face_pile;
pub mod notification_panel;
pub mod notifications;
mod panel_settings;
use std::sync::Arc;
pub use collab_panel::CollabPanel;
pub use collab_titlebar_item::CollabTitlebarItem;
use gpui::AppContext;
pub use panel_settings::{
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
};
use settings::Settings;
use workspace::AppState;
// actions!(
// collab,
// [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
// );
pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
CollaborationPanelSettings::register(cx);
ChatPanelSettings::register(cx);
NotificationPanelSettings::register(cx);
// vcs_menu::init(cx);
collab_titlebar_item::init(cx);
collab_panel::init(cx);
// chat_panel::init(cx);
// notifications::init(&app_state, cx);
// cx.add_global_action(toggle_screen_sharing);
// cx.add_global_action(toggle_mute);
// cx.add_global_action(toggle_deafen);
}
// pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
// let call = ActiveCall::global(cx).read(cx);
// if let Some(room) = call.room().cloned() {
// let client = call.client();
// let toggle_screen_sharing = room.update(cx, |room, cx| {
// if room.is_screen_sharing() {
// report_call_event_for_room(
// "disable screen share",
// room.id(),
// room.channel_id(),
// &client,
// cx,
// );
// Task::ready(room.unshare_screen(cx))
// } else {
// report_call_event_for_room(
// "enable screen share",
// room.id(),
// room.channel_id(),
// &client,
// cx,
// );
// room.share_screen(cx)
// }
// });
// toggle_screen_sharing.detach_and_log_err(cx);
// }
// }
// pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
// let call = ActiveCall::global(cx).read(cx);
// if let Some(room) = call.room().cloned() {
// let client = call.client();
// room.update(cx, |room, cx| {
// let operation = if room.is_muted(cx) {
// "enable microphone"
// } else {
// "disable microphone"
// };
// report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
// room.toggle_mute(cx)
// })
// .map(|task| task.detach_and_log_err(cx))
// .log_err();
// }
// }
// pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
// if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
// room.update(cx, Room::toggle_deafen)
// .map(|task| task.detach_and_log_err(cx))
// .log_err();
// }
// }
// fn notification_window_options(
// screen: Rc<dyn Screen>,
// window_size: Vector2F,
// ) -> WindowOptions<'static> {
// const NOTIFICATION_PADDING: f32 = 16.;
// let screen_bounds = screen.content_bounds();
// WindowOptions {
// bounds: WindowBounds::Fixed(RectF::new(
// screen_bounds.upper_right()
// + vec2f(
// -NOTIFICATION_PADDING - window_size.x(),
// NOTIFICATION_PADDING,
// ),
// window_size,
// )),
// titlebar: None,
// center: false,
// focus: false,
// show: true,
// kind: WindowKind::PopUp,
// is_movable: false,
// screen: Some(screen),
// }
// }
// fn render_avatar<T: 'static>(
// avatar: Option<Arc<ImageData>>,
// avatar_style: &AvatarStyle,
// container: ContainerStyle,
// ) -> AnyElement<T> {
// avatar
// .map(|avatar| {
// Image::from_data(avatar)
// .with_style(avatar_style.image)
// .aligned()
// .contained()
// .with_corner_radius(avatar_style.outer_corner_radius)
// .constrained()
// .with_width(avatar_style.outer_width)
// .with_height(avatar_style.outer_width)
// .into_any()
// })
// .unwrap_or_else(|| {
// Empty::new()
// .constrained()
// .with_width(avatar_style.outer_width)
// .into_any()
// })
// .contained()
// .with_style(container)
// .into_any()
// }
// fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
// cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
// }

View File

@ -0,0 +1,113 @@
// use std::ops::Range;
// use gpui::{
// geometry::{
// rect::RectF,
// vector::{vec2f, Vector2F},
// },
// json::ToJson,
// serde_json::{self, json},
// AnyElement, Axis, Element, View, ViewContext,
// };
// pub(crate) struct FacePile<V: View> {
// overlap: f32,
// faces: Vec<AnyElement<V>>,
// }
// impl<V: View> FacePile<V> {
// pub fn new(overlap: f32) -> Self {
// Self {
// overlap,
// faces: Vec::new(),
// }
// }
// }
// impl<V: View> Element<V> for FacePile<V> {
// type LayoutState = ();
// type PaintState = ();
// fn layout(
// &mut self,
// constraint: gpui::SizeConstraint,
// view: &mut V,
// cx: &mut ViewContext<V>,
// ) -> (Vector2F, Self::LayoutState) {
// debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
// let mut width = 0.;
// let mut max_height = 0.;
// for face in &mut self.faces {
// let layout = face.layout(constraint, view, cx);
// width += layout.x();
// max_height = f32::max(max_height, layout.y());
// }
// width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
// (
// Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
// (),
// )
// }
// fn paint(
// &mut self,
// bounds: RectF,
// visible_bounds: RectF,
// _layout: &mut Self::LayoutState,
// view: &mut V,
// cx: &mut ViewContext<V>,
// ) -> Self::PaintState {
// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
// let origin_y = bounds.upper_right().y();
// let mut origin_x = bounds.upper_right().x();
// for face in self.faces.iter_mut().rev() {
// let size = face.size();
// origin_x -= size.x();
// let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
// cx.scene().push_layer(None);
// face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx);
// cx.scene().pop_layer();
// origin_x += self.overlap;
// }
// ()
// }
// fn rect_for_text_range(
// &self,
// _: Range<usize>,
// _: RectF,
// _: RectF,
// _: &Self::LayoutState,
// _: &Self::PaintState,
// _: &V,
// _: &ViewContext<V>,
// ) -> Option<RectF> {
// None
// }
// fn debug(
// &self,
// bounds: RectF,
// _: &Self::LayoutState,
// _: &Self::PaintState,
// _: &V,
// _: &ViewContext<V>,
// ) -> serde_json::Value {
// json!({
// "type": "FacePile",
// "bounds": bounds.to_json()
// })
// }
// }
// impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
// fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
// self.faces.extend(children);
// }
// }

View File

@ -0,0 +1,884 @@
// use crate::{chat_panel::ChatPanel, render_avatar, NotificationPanelSettings};
// use anyhow::Result;
// use channel::ChannelStore;
// use client::{Client, Notification, User, UserStore};
// use collections::HashMap;
// use db::kvp::KEY_VALUE_STORE;
// use futures::StreamExt;
// use gpui::{
// actions,
// elements::*,
// platform::{CursorStyle, MouseButton},
// serde_json, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View,
// ViewContext, ViewHandle, WeakViewHandle, WindowContext,
// };
// use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
// use project::Fs;
// use rpc::proto;
// use serde::{Deserialize, Serialize};
// use settings::SettingsStore;
// use std::{sync::Arc, time::Duration};
// use theme::{ui, Theme};
// use time::{OffsetDateTime, UtcOffset};
// use util::{ResultExt, TryFutureExt};
// use workspace::{
// dock::{DockPosition, Panel},
// Workspace,
// };
// const LOADING_THRESHOLD: usize = 30;
// const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
// const TOAST_DURATION: Duration = Duration::from_secs(5);
// const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";
// pub struct NotificationPanel {
// client: Arc<Client>,
// user_store: ModelHandle<UserStore>,
// channel_store: ModelHandle<ChannelStore>,
// notification_store: ModelHandle<NotificationStore>,
// fs: Arc<dyn Fs>,
// width: Option<f32>,
// active: bool,
// notification_list: ListState<Self>,
// pending_serialization: Task<Option<()>>,
// subscriptions: Vec<gpui::Subscription>,
// workspace: WeakViewHandle<Workspace>,
// current_notification_toast: Option<(u64, Task<()>)>,
// local_timezone: UtcOffset,
// has_focus: bool,
// mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
// }
// #[derive(Serialize, Deserialize)]
// struct SerializedNotificationPanel {
// width: Option<f32>,
// }
// #[derive(Debug)]
// pub enum Event {
// DockPositionChanged,
// Focus,
// Dismissed,
// }
// pub struct NotificationPresenter {
// pub actor: Option<Arc<client::User>>,
// pub text: String,
// pub icon: &'static str,
// pub needs_response: bool,
// pub can_navigate: bool,
// }
// actions!(notification_panel, [ToggleFocus]);
// pub fn init(_cx: &mut AppContext) {}
// impl NotificationPanel {
// pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
// let fs = workspace.app_state().fs.clone();
// let client = workspace.app_state().client.clone();
// let user_store = workspace.app_state().user_store.clone();
// let workspace_handle = workspace.weak_handle();
// cx.add_view(|cx| {
// let mut status = client.status();
// cx.spawn(|this, mut cx| async move {
// while let Some(_) = status.next().await {
// if this
// .update(&mut cx, |_, cx| {
// cx.notify();
// })
// .is_err()
// {
// break;
// }
// }
// })
// .detach();
// let mut notification_list =
// ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
// this.render_notification(ix, cx)
// .unwrap_or_else(|| Empty::new().into_any())
// });
// notification_list.set_scroll_handler(|visible_range, count, this, cx| {
// if count.saturating_sub(visible_range.end) < LOADING_THRESHOLD {
// if let Some(task) = this
// .notification_store
// .update(cx, |store, cx| store.load_more_notifications(false, cx))
// {
// task.detach();
// }
// }
// });
// let mut this = Self {
// fs,
// client,
// user_store,
// local_timezone: cx.platform().local_timezone(),
// channel_store: ChannelStore::global(cx),
// notification_store: NotificationStore::global(cx),
// notification_list,
// pending_serialization: Task::ready(None),
// workspace: workspace_handle,
// has_focus: false,
// current_notification_toast: None,
// subscriptions: Vec::new(),
// active: false,
// mark_as_read_tasks: HashMap::default(),
// width: None,
// };
// let mut old_dock_position = this.position(cx);
// this.subscriptions.extend([
// cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
// cx.subscribe(&this.notification_store, Self::on_notification_event),
// 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);
// }
// cx.notify();
// }),
// ]);
// this
// })
// }
// pub fn load(
// workspace: WeakViewHandle<Workspace>,
// cx: AsyncAppContext,
// ) -> Task<Result<ViewHandle<Self>>> {
// cx.spawn(|mut cx| async move {
// let serialized_panel = if let Some(panel) = cx
// .background()
// .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
// .await
// .log_err()
// .flatten()
// {
// Some(serde_json::from_str::<SerializedNotificationPanel>(&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<Self>) {
// let width = self.width;
// self.pending_serialization = cx.background().spawn(
// async move {
// KEY_VALUE_STORE
// .write_kvp(
// NOTIFICATION_PANEL_KEY.into(),
// serde_json::to_string(&SerializedNotificationPanel { width })?,
// )
// .await?;
// anyhow::Ok(())
// }
// .log_err(),
// );
// }
// fn render_notification(
// &mut self,
// ix: usize,
// cx: &mut ViewContext<Self>,
// ) -> Option<AnyElement<Self>> {
// let entry = self.notification_store.read(cx).notification_at(ix)?;
// let notification_id = entry.id;
// let now = OffsetDateTime::now_utc();
// let timestamp = entry.timestamp;
// let NotificationPresenter {
// actor,
// text,
// needs_response,
// can_navigate,
// ..
// } = self.present_notification(entry, cx)?;
// let theme = theme::current(cx);
// let style = &theme.notification_panel;
// let response = entry.response;
// let notification = entry.notification.clone();
// let message_style = if entry.is_read {
// style.read_text.clone()
// } else {
// style.unread_text.clone()
// };
// if self.active && !entry.is_read {
// self.did_render_notification(notification_id, &notification, cx);
// }
// enum Decline {}
// enum Accept {}
// Some(
// MouseEventHandler::new::<NotificationEntry, _>(ix, cx, |_, cx| {
// let container = message_style.container;
// Flex::row()
// .with_children(actor.map(|actor| {
// render_avatar(actor.avatar.clone(), &style.avatar, style.avatar_container)
// }))
// .with_child(
// Flex::column()
// .with_child(Text::new(text, message_style.text.clone()))
// .with_child(
// Flex::row()
// .with_child(
// Label::new(
// format_timestamp(timestamp, now, self.local_timezone),
// style.timestamp.text.clone(),
// )
// .contained()
// .with_style(style.timestamp.container),
// )
// .with_children(if let Some(is_accepted) = response {
// Some(
// Label::new(
// if is_accepted {
// "You accepted"
// } else {
// "You declined"
// },
// style.read_text.text.clone(),
// )
// .flex_float()
// .into_any(),
// )
// } else if needs_response {
// Some(
// Flex::row()
// .with_children([
// MouseEventHandler::new::<Decline, _>(
// ix,
// cx,
// |state, _| {
// let button =
// style.button.style_for(state);
// Label::new(
// "Decline",
// button.text.clone(),
// )
// .contained()
// .with_style(button.container)
// },
// )
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, {
// let notification = notification.clone();
// move |_, view, cx| {
// view.respond_to_notification(
// notification.clone(),
// false,
// cx,
// );
// }
// }),
// MouseEventHandler::new::<Accept, _>(
// ix,
// cx,
// |state, _| {
// let button =
// style.button.style_for(state);
// Label::new(
// "Accept",
// button.text.clone(),
// )
// .contained()
// .with_style(button.container)
// },
// )
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, {
// let notification = notification.clone();
// move |_, view, cx| {
// view.respond_to_notification(
// notification.clone(),
// true,
// cx,
// );
// }
// }),
// ])
// .flex_float()
// .into_any(),
// )
// } else {
// None
// }),
// )
// .flex(1.0, true),
// )
// .contained()
// .with_style(container)
// .into_any()
// })
// .with_cursor_style(if can_navigate {
// CursorStyle::PointingHand
// } else {
// CursorStyle::default()
// })
// .on_click(MouseButton::Left, {
// let notification = notification.clone();
// move |_, this, cx| this.did_click_notification(&notification, cx)
// })
// .into_any(),
// )
// }
// fn present_notification(
// &self,
// entry: &NotificationEntry,
// cx: &AppContext,
// ) -> Option<NotificationPresenter> {
// let user_store = self.user_store.read(cx);
// let channel_store = self.channel_store.read(cx);
// match entry.notification {
// Notification::ContactRequest { sender_id } => {
// let requester = user_store.get_cached_user(sender_id)?;
// Some(NotificationPresenter {
// icon: "icons/plus.svg",
// text: format!("{} wants to add you as a contact", requester.github_login),
// needs_response: user_store.has_incoming_contact_request(requester.id),
// actor: Some(requester),
// can_navigate: false,
// })
// }
// Notification::ContactRequestAccepted { responder_id } => {
// let responder = user_store.get_cached_user(responder_id)?;
// Some(NotificationPresenter {
// icon: "icons/plus.svg",
// text: format!("{} accepted your contact invite", responder.github_login),
// needs_response: false,
// actor: Some(responder),
// can_navigate: false,
// })
// }
// Notification::ChannelInvitation {
// ref channel_name,
// channel_id,
// inviter_id,
// } => {
// let inviter = user_store.get_cached_user(inviter_id)?;
// Some(NotificationPresenter {
// icon: "icons/hash.svg",
// text: format!(
// "{} invited you to join the #{channel_name} channel",
// inviter.github_login
// ),
// needs_response: channel_store.has_channel_invitation(channel_id),
// actor: Some(inviter),
// can_navigate: false,
// })
// }
// Notification::ChannelMessageMention {
// sender_id,
// channel_id,
// message_id,
// } => {
// let sender = user_store.get_cached_user(sender_id)?;
// let channel = channel_store.channel_for_id(channel_id)?;
// let message = self
// .notification_store
// .read(cx)
// .channel_message_for_id(message_id)?;
// Some(NotificationPresenter {
// icon: "icons/conversations.svg",
// text: format!(
// "{} mentioned you in #{}:\n{}",
// sender.github_login, channel.name, message.body,
// ),
// needs_response: false,
// actor: Some(sender),
// can_navigate: true,
// })
// }
// }
// }
// fn did_render_notification(
// &mut self,
// notification_id: u64,
// notification: &Notification,
// cx: &mut ViewContext<Self>,
// ) {
// let should_mark_as_read = match notification {
// Notification::ContactRequestAccepted { .. } => true,
// Notification::ContactRequest { .. }
// | Notification::ChannelInvitation { .. }
// | Notification::ChannelMessageMention { .. } => false,
// };
// if should_mark_as_read {
// self.mark_as_read_tasks
// .entry(notification_id)
// .or_insert_with(|| {
// let client = self.client.clone();
// cx.spawn(|this, mut cx| async move {
// cx.background().timer(MARK_AS_READ_DELAY).await;
// client
// .request(proto::MarkNotificationRead { notification_id })
// .await?;
// this.update(&mut cx, |this, _| {
// this.mark_as_read_tasks.remove(&notification_id);
// })?;
// Ok(())
// })
// });
// }
// }
// fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
// if let Notification::ChannelMessageMention {
// message_id,
// channel_id,
// ..
// } = notification.clone()
// {
// if let Some(workspace) = self.workspace.upgrade(cx) {
// cx.app_context().defer(move |cx| {
// workspace.update(cx, |workspace, cx| {
// if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
// panel.update(cx, |panel, cx| {
// panel
// .select_channel(channel_id, Some(message_id), cx)
// .detach_and_log_err(cx);
// });
// }
// });
// });
// }
// }
// }
// fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool {
// if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
// if let Some(workspace) = self.workspace.upgrade(cx) {
// return workspace
// .read_with(cx, |workspace, cx| {
// if let Some(panel) = workspace.panel::<ChatPanel>(cx) {
// return panel.read_with(cx, |panel, cx| {
// panel.is_scrolled_to_bottom()
// && panel.active_chat().map_or(false, |chat| {
// chat.read(cx).channel_id == *channel_id
// })
// });
// }
// false
// })
// .unwrap_or_default();
// }
// }
// false
// }
// fn render_sign_in_prompt(
// &self,
// theme: &Arc<Theme>,
// cx: &mut ViewContext<Self>,
// ) -> AnyElement<Self> {
// enum SignInPromptLabel {}
// MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
// Label::new(
// "Sign in to view your notifications".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(|_, cx| async move {
// client.authenticate_and_connect(true, &cx).log_err().await;
// })
// .detach();
// })
// .aligned()
// .into_any()
// }
// fn render_empty_state(
// &self,
// theme: &Arc<Theme>,
// _cx: &mut ViewContext<Self>,
// ) -> AnyElement<Self> {
// Label::new(
// "You have no notifications".to_string(),
// theme.chat_panel.sign_in_prompt.default.clone(),
// )
// .aligned()
// .into_any()
// }
// fn on_notification_event(
// &mut self,
// _: ModelHandle<NotificationStore>,
// event: &NotificationEvent,
// cx: &mut ViewContext<Self>,
// ) {
// match event {
// NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
// NotificationEvent::NotificationRemoved { entry }
// | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx),
// NotificationEvent::NotificationsUpdated {
// old_range,
// new_count,
// } => {
// self.notification_list.splice(old_range.clone(), *new_count);
// cx.notify();
// }
// }
// }
// fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
// if self.is_showing_notification(&entry.notification, cx) {
// return;
// }
// let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
// else {
// return;
// };
// let notification_id = entry.id;
// self.current_notification_toast = Some((
// notification_id,
// cx.spawn(|this, mut cx| async move {
// cx.background().timer(TOAST_DURATION).await;
// this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
// .ok();
// }),
// ));
// self.workspace
// .update(cx, |workspace, cx| {
// workspace.dismiss_notification::<NotificationToast>(0, cx);
// workspace.show_notification(0, cx, |cx| {
// let workspace = cx.weak_handle();
// cx.add_view(|_| NotificationToast {
// notification_id,
// actor,
// text,
// workspace,
// })
// })
// })
// .ok();
// }
// fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext<Self>) {
// if let Some((current_id, _)) = &self.current_notification_toast {
// if *current_id == notification_id {
// self.current_notification_toast.take();
// self.workspace
// .update(cx, |workspace, cx| {
// workspace.dismiss_notification::<NotificationToast>(0, cx)
// })
// .ok();
// }
// }
// }
// fn respond_to_notification(
// &mut self,
// notification: Notification,
// response: bool,
// cx: &mut ViewContext<Self>,
// ) {
// self.notification_store.update(cx, |store, cx| {
// store.respond_to_notification(notification, response, cx);
// });
// }
// }
// impl Entity for NotificationPanel {
// type Event = Event;
// }
// impl View for NotificationPanel {
// fn ui_name() -> &'static str {
// "NotificationPanel"
// }
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// let theme = theme::current(cx);
// let style = &theme.notification_panel;
// let element = if self.client.user_id().is_none() {
// self.render_sign_in_prompt(&theme, cx)
// } else if self.notification_list.item_count() == 0 {
// self.render_empty_state(&theme, cx)
// } else {
// Flex::column()
// .with_child(
// Flex::row()
// .with_child(Label::new("Notifications", style.title.text.clone()))
// .with_child(ui::svg(&style.title_icon).flex_float())
// .align_children_center()
// .contained()
// .with_style(style.title.container)
// .constrained()
// .with_height(style.title_height),
// )
// .with_child(
// List::new(self.notification_list.clone())
// .contained()
// .with_style(style.list)
// .flex(1., true),
// )
// .into_any()
// };
// element
// .contained()
// .with_style(style.container)
// .constrained()
// .with_min_width(150.)
// .into_any()
// }
// fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
// self.has_focus = true;
// }
// fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
// self.has_focus = false;
// }
// }
// impl Panel for NotificationPanel {
// fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
// settings::get::<NotificationPanelSettings>(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<Self>) {
// settings::update_settings_file::<NotificationPanelSettings>(
// self.fs.clone(),
// cx,
// move |settings| settings.dock = Some(position),
// );
// }
// fn size(&self, cx: &gpui::WindowContext) -> f32 {
// self.width
// .unwrap_or_else(|| settings::get::<NotificationPanelSettings>(cx).default_width)
// }
// fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
// self.width = size;
// self.serialize(cx);
// cx.notify();
// }
// fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
// self.active = active;
// if self.notification_store.read(cx).notification_count() == 0 {
// cx.emit(Event::Dismissed);
// }
// }
// fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
// (settings::get::<NotificationPanelSettings>(cx).button
// && self.notification_store.read(cx).notification_count() > 0)
// .then(|| "icons/bell.svg")
// }
// fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
// (
// "Notification Panel".to_string(),
// Some(Box::new(ToggleFocus)),
// )
// }
// fn icon_label(&self, cx: &WindowContext) -> Option<String> {
// let count = self.notification_store.read(cx).unread_notification_count();
// if count == 0 {
// None
// } else {
// Some(count.to_string())
// }
// }
// 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)
// }
// }
// pub struct NotificationToast {
// notification_id: u64,
// actor: Option<Arc<User>>,
// text: String,
// workspace: WeakViewHandle<Workspace>,
// }
// pub enum ToastEvent {
// Dismiss,
// }
// impl NotificationToast {
// fn focus_notification_panel(&self, cx: &mut AppContext) {
// let workspace = self.workspace.clone();
// let notification_id = self.notification_id;
// cx.defer(move |cx| {
// workspace
// .update(cx, |workspace, cx| {
// if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
// panel.update(cx, |panel, cx| {
// let store = panel.notification_store.read(cx);
// if let Some(entry) = store.notification_for_id(notification_id) {
// panel.did_click_notification(&entry.clone().notification, cx);
// }
// });
// }
// })
// .ok();
// })
// }
// }
// impl Entity for NotificationToast {
// type Event = ToastEvent;
// }
// impl View for NotificationToast {
// fn ui_name() -> &'static str {
// "ContactNotification"
// }
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// let user = self.actor.clone();
// let theme = theme::current(cx).clone();
// let theme = &theme.contact_notification;
// MouseEventHandler::new::<Self, _>(0, cx, |_, cx| {
// Flex::row()
// .with_children(user.and_then(|user| {
// Some(
// Image::from_data(user.avatar.clone()?)
// .with_style(theme.header_avatar)
// .aligned()
// .constrained()
// .with_height(
// cx.font_cache()
// .line_height(theme.header_message.text.font_size),
// )
// .aligned()
// .top(),
// )
// }))
// .with_child(
// Text::new(self.text.clone(), theme.header_message.text.clone())
// .contained()
// .with_style(theme.header_message.container)
// .aligned()
// .top()
// .left()
// .flex(1., true),
// )
// .with_child(
// MouseEventHandler::new::<ToastEvent, _>(0, cx, |state, _| {
// let style = theme.dismiss_button.style_for(state);
// Svg::new("icons/x.svg")
// .with_color(style.color)
// .constrained()
// .with_width(style.icon_width)
// .aligned()
// .contained()
// .with_style(style.container)
// .constrained()
// .with_width(style.button_width)
// .with_height(style.button_width)
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .with_padding(Padding::uniform(5.))
// .on_click(MouseButton::Left, move |_, _, cx| {
// cx.emit(ToastEvent::Dismiss)
// })
// .aligned()
// .constrained()
// .with_height(
// cx.font_cache()
// .line_height(theme.header_message.text.font_size),
// )
// .aligned()
// .top()
// .flex_float(),
// )
// .contained()
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, move |_, this, cx| {
// this.focus_notification_panel(cx);
// cx.emit(ToastEvent::Dismiss);
// })
// .into_any()
// }
// }
// impl workspace::notifications::Notification for NotificationToast {
// fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
// matches!(event, ToastEvent::Dismiss)
// }
// }
// 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();
// if date == today {
// let difference = now - timestamp;
// if difference >= Duration::from_secs(3600) {
// format!("{}h", difference.whole_seconds() / 3600)
// } else if difference >= Duration::from_secs(60) {
// format!("{}m", difference.whole_seconds() / 60)
// } else {
// "just now".to_string()
// }
// } else if date.next_day() == Some(today) {
// format!("yesterday")
// } else {
// format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
// }
// }

View File

@ -0,0 +1,11 @@
// use gpui::AppContext;
// use std::sync::Arc;
// use workspace::AppState;
// pub mod incoming_call_notification;
// pub mod project_shared_notification;
// pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
// incoming_call_notification::init(app_state, cx);
// project_shared_notification::init(app_state, cx);
// }

View File

@ -0,0 +1,213 @@
use crate::notification_window_options;
use call::{ActiveCall, IncomingCall};
use client::proto;
use futures::StreamExt;
use gpui::{
elements::*,
geometry::vector::vec2f,
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
};
use std::sync::{Arc, Weak};
use util::ResultExt;
use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
let app_state = Arc::downgrade(app_state);
let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
cx.spawn(|mut cx| async move {
let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new();
while let Some(incoming_call) = incoming_call.next().await {
for window in notification_windows.drain(..) {
window.remove(&mut cx);
}
if let Some(incoming_call) = incoming_call {
let window_size = cx.read(|cx| {
let theme = &theme::current(cx).incoming_call_notification;
vec2f(theme.window_width, theme.window_height)
});
for screen in cx.platform().screens() {
let window = cx
.add_window(notification_window_options(screen, window_size), |_| {
IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
});
notification_windows.push(window);
}
}
}
})
.detach();
}
#[derive(Clone, PartialEq)]
struct RespondToCall {
accept: bool,
}
pub struct IncomingCallNotification {
call: IncomingCall,
app_state: Weak<AppState>,
}
impl IncomingCallNotification {
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
Self { call, app_state }
}
fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
let active_call = ActiveCall::global(cx);
if accept {
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
let caller_user_id = self.call.calling_user.id;
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
let app_state = self.app_state.clone();
cx.app_context()
.spawn(|mut cx| async move {
join.await?;
if let Some(project_id) = initial_project_id {
cx.update(|cx| {
if let Some(app_state) = app_state.upgrade() {
workspace::join_remote_project(
project_id,
caller_user_id,
app_state,
cx,
)
.detach_and_log_err(cx);
}
});
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
} else {
active_call.update(cx, |active_call, cx| {
active_call.decline_incoming(cx).log_err();
});
}
}
fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).incoming_call_notification;
let default_project = proto::ParticipantProject::default();
let initial_project = self
.call
.initial_project
.as_ref()
.unwrap_or(&default_project);
Flex::row()
.with_children(self.call.calling_user.avatar.clone().map(|avatar| {
Image::from_data(avatar)
.with_style(theme.caller_avatar)
.aligned()
}))
.with_child(
Flex::column()
.with_child(
Label::new(
self.call.calling_user.github_login.clone(),
theme.caller_username.text.clone(),
)
.contained()
.with_style(theme.caller_username.container),
)
.with_child(
Label::new(
format!(
"is sharing a project in Zed{}",
if initial_project.worktree_root_names.is_empty() {
""
} else {
":"
}
),
theme.caller_message.text.clone(),
)
.contained()
.with_style(theme.caller_message.container),
)
.with_children(if initial_project.worktree_root_names.is_empty() {
None
} else {
Some(
Label::new(
initial_project.worktree_root_names.join(", "),
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container),
)
})
.contained()
.with_style(theme.caller_metadata)
.aligned(),
)
.contained()
.with_style(theme.caller_container)
.flex(1., true)
.into_any()
}
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum Accept {}
enum Decline {}
let theme = theme::current(cx);
Flex::column()
.with_child(
MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
let theme = &theme.incoming_call_notification;
Label::new("Accept", theme.accept_button.text.clone())
.aligned()
.contained()
.with_style(theme.accept_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
this.respond(true, cx);
})
.flex(1., true),
)
.with_child(
MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
let theme = &theme.incoming_call_notification;
Label::new("Decline", theme.decline_button.text.clone())
.aligned()
.contained()
.with_style(theme.decline_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
this.respond(false, cx);
})
.flex(1., true),
)
.constrained()
.with_width(theme.incoming_call_notification.button_width)
.into_any()
}
}
impl Entity for IncomingCallNotification {
type Event = ();
}
impl View for IncomingCallNotification {
fn ui_name() -> &'static str {
"IncomingCallNotification"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let background = theme::current(cx).incoming_call_notification.background;
Flex::row()
.with_child(self.render_caller(cx))
.with_child(self.render_buttons(cx))
.contained()
.with_background_color(background)
.expanded()
.into_any()
}
}

View File

@ -0,0 +1,217 @@
use crate::notification_window_options;
use call::{room, ActiveCall};
use client::User;
use collections::HashMap;
use gpui::{
elements::*,
geometry::vector::vec2f,
platform::{CursorStyle, MouseButton},
AppContext, Entity, View, ViewContext,
};
use std::sync::{Arc, Weak};
use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
let app_state = Arc::downgrade(app_state);
let active_call = ActiveCall::global(cx);
let mut notification_windows = HashMap::default();
cx.subscribe(&active_call, move |_, event, cx| match event {
room::Event::RemoteProjectShared {
owner,
project_id,
worktree_root_names,
} => {
let theme = &theme::current(cx).project_shared_notification;
let window_size = vec2f(theme.window_width, theme.window_height);
for screen in cx.platform().screens() {
let window =
cx.add_window(notification_window_options(screen, window_size), |_| {
ProjectSharedNotification::new(
owner.clone(),
*project_id,
worktree_root_names.clone(),
app_state.clone(),
)
});
notification_windows
.entry(*project_id)
.or_insert(Vec::new())
.push(window);
}
}
room::Event::RemoteProjectUnshared { project_id }
| room::Event::RemoteProjectJoined { project_id }
| room::Event::RemoteProjectInvitationDiscarded { project_id } => {
if let Some(windows) = notification_windows.remove(&project_id) {
for window in windows {
window.remove(cx);
}
}
}
room::Event::Left => {
for (_, windows) in notification_windows.drain() {
for window in windows {
window.remove(cx);
}
}
}
_ => {}
})
.detach();
}
pub struct ProjectSharedNotification {
project_id: u64,
worktree_root_names: Vec<String>,
owner: Arc<User>,
app_state: Weak<AppState>,
}
impl ProjectSharedNotification {
fn new(
owner: Arc<User>,
project_id: u64,
worktree_root_names: Vec<String>,
app_state: Weak<AppState>,
) -> Self {
Self {
project_id,
worktree_root_names,
owner,
app_state,
}
}
fn join(&mut self, cx: &mut ViewContext<Self>) {
if let Some(app_state) = self.app_state.upgrade() {
workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
.detach_and_log_err(cx);
}
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
if let Some(active_room) =
ActiveCall::global(cx).read_with(cx, |call, _| call.room().cloned())
{
active_room.update(cx, |_, cx| {
cx.emit(room::Event::RemoteProjectInvitationDiscarded {
project_id: self.project_id,
});
});
}
}
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).project_shared_notification;
Flex::row()
.with_children(self.owner.avatar.clone().map(|avatar| {
Image::from_data(avatar)
.with_style(theme.owner_avatar)
.aligned()
}))
.with_child(
Flex::column()
.with_child(
Label::new(
self.owner.github_login.clone(),
theme.owner_username.text.clone(),
)
.contained()
.with_style(theme.owner_username.container),
)
.with_child(
Label::new(
format!(
"is sharing a project in Zed{}",
if self.worktree_root_names.is_empty() {
""
} else {
":"
}
),
theme.message.text.clone(),
)
.contained()
.with_style(theme.message.container),
)
.with_children(if self.worktree_root_names.is_empty() {
None
} else {
Some(
Label::new(
self.worktree_root_names.join(", "),
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container),
)
})
.contained()
.with_style(theme.owner_metadata)
.aligned(),
)
.contained()
.with_style(theme.owner_container)
.flex(1., true)
.into_any()
}
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum Open {}
enum Dismiss {}
let theme = theme::current(cx);
Flex::column()
.with_child(
MouseEventHandler::new::<Open, _>(0, cx, |_, _| {
let theme = &theme.project_shared_notification;
Label::new("Open", theme.open_button.text.clone())
.aligned()
.contained()
.with_style(theme.open_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
.flex(1., true),
)
.with_child(
MouseEventHandler::new::<Dismiss, _>(0, cx, |_, _| {
let theme = &theme.project_shared_notification;
Label::new("Dismiss", theme.dismiss_button.text.clone())
.aligned()
.contained()
.with_style(theme.dismiss_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
this.dismiss(cx);
})
.flex(1., true),
)
.constrained()
.with_width(theme.project_shared_notification.button_width)
.into_any()
}
}
impl Entity for ProjectSharedNotification {
type Event = ();
}
impl View for ProjectSharedNotification {
fn ui_name() -> &'static str {
"ProjectSharedNotification"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
let background = theme::current(cx).project_shared_notification.background;
Flex::row()
.with_child(self.render_owner(cx))
.with_child(self.render_buttons(cx))
.contained()
.with_background_color(background)
.expanded()
.into_any()
}
}

View File

@ -0,0 +1,69 @@
use anyhow;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::Settings;
use workspace::dock::DockPosition;
#[derive(Deserialize, Debug)]
pub struct CollaborationPanelSettings {
pub button: bool,
pub dock: DockPosition,
pub default_width: f32,
}
#[derive(Deserialize, Debug)]
pub struct ChatPanelSettings {
pub button: bool,
pub dock: DockPosition,
pub default_width: f32,
}
#[derive(Deserialize, Debug)]
pub struct NotificationPanelSettings {
pub button: bool,
pub dock: DockPosition,
pub default_width: f32,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct PanelSettingsContent {
pub button: Option<bool>,
pub dock: Option<DockPosition>,
pub default_width: Option<f32>,
}
impl Settings for CollaborationPanelSettings {
const KEY: Option<&'static str> = Some("collaboration_panel");
type FileContent = PanelSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}
impl Settings for ChatPanelSettings {
const KEY: Option<&'static str> = Some("chat_panel");
type FileContent = PanelSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}
impl Settings for NotificationPanelSettings {
const KEY: Option<&'static str> = Some("notification_panel");
type FileContent = PanelSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}

View File

@ -354,129 +354,116 @@ impl std::fmt::Debug for Command {
}
}
// #[cfg(test)]
// mod tests {
// use std::sync::Arc;
#[cfg(test)]
mod tests {
use std::sync::Arc;
// use super::*;
// use editor::Editor;
// use gpui::{executor::Deterministic, TestAppContext};
// use project::Project;
// use workspace::{AppState, Workspace};
use super::*;
use editor::Editor;
use gpui::TestAppContext;
use project::Project;
use workspace::{AppState, Workspace};
// #[test]
// fn test_humanize_action_name() {
// assert_eq!(
// humanize_action_name("editor::GoToDefinition"),
// "editor: go to definition"
// );
// assert_eq!(
// humanize_action_name("editor::Backspace"),
// "editor: backspace"
// );
// assert_eq!(
// humanize_action_name("go_to_line::Deploy"),
// "go to line: deploy"
// );
// }
#[test]
fn test_humanize_action_name() {
assert_eq!(
humanize_action_name("editor::GoToDefinition"),
"editor: go to definition"
);
assert_eq!(
humanize_action_name("editor::Backspace"),
"editor: backspace"
);
assert_eq!(
humanize_action_name("go_to_line::Deploy"),
"go to line: deploy"
);
}
// #[gpui::test]
// async fn test_command_palette(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
// let app_state = init_test(cx);
#[gpui::test]
async fn test_command_palette(cx: &mut TestAppContext) {
let app_state = init_test(cx);
// let project = Project::test(app_state.fs.clone(), [], cx).await;
// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
// let workspace = window.root(cx);
// let editor = window.add_view(cx, |cx| {
// let mut editor = Editor::single_line(None, cx);
// editor.set_text("abc", cx);
// editor
// });
let project = Project::test(app_state.fs.clone(), [], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
// workspace.update(cx, |workspace, cx| {
// cx.focus(&editor);
// workspace.add_item(Box::new(editor.clone()), cx)
// });
let editor = cx.build_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_text("abc", cx);
editor
});
// workspace.update(cx, |workspace, cx| {
// toggle_command_palette(workspace, &Toggle, cx);
// });
workspace.update(cx, |workspace, cx| {
workspace.add_item(Box::new(editor.clone()), cx);
editor.update(cx, |editor, cx| editor.focus(cx))
});
// let palette = workspace.read_with(cx, |workspace, _| {
// workspace.modal::<CommandPalette>().unwrap()
// });
cx.simulate_keystrokes("cmd-shift-p");
// palette
// .update(cx, |palette, cx| {
// // Fill up palette's command list by running an empty query;
// // we only need it to subsequently assert that the palette is initially
// // sorted by command's name.
// palette.delegate_mut().update_matches("".to_string(), cx)
// })
// .await;
let palette = workspace.update(cx, |workspace, cx| {
workspace
.active_modal::<CommandPalette>(cx)
.unwrap()
.read(cx)
.picker
.clone()
});
// palette.update(cx, |palette, _| {
// let is_sorted =
// |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name);
// assert!(is_sorted(&palette.delegate().actions));
// });
palette.update(cx, |palette, _| {
assert!(palette.delegate.commands.len() > 5);
let is_sorted =
|actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name);
assert!(is_sorted(&palette.delegate.commands));
});
// palette
// .update(cx, |palette, cx| {
// palette
// .delegate_mut()
// .update_matches("bcksp".to_string(), cx)
// })
// .await;
cx.simulate_input("bcksp");
// palette.update(cx, |palette, cx| {
// assert_eq!(palette.delegate().matches[0].string, "editor: backspace");
// palette.confirm(&Default::default(), cx);
// });
// deterministic.run_until_parked();
// editor.read_with(cx, |editor, cx| {
// assert_eq!(editor.text(cx), "ab");
// });
palette.update(cx, |palette, _| {
assert_eq!(palette.delegate.matches[0].string, "editor: backspace");
});
// // Add namespace filter, and redeploy the palette
// cx.update(|cx| {
// cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
// filter.filtered_namespaces.insert("editor");
// })
// });
cx.simulate_keystrokes("enter");
// workspace.update(cx, |workspace, cx| {
// toggle_command_palette(workspace, &Toggle, cx);
// });
workspace.update(cx, |workspace, cx| {
assert!(workspace.active_modal::<CommandPalette>(cx).is_none());
assert_eq!(editor.read(cx).text(cx), "ab")
});
// // Assert editor command not present
// let palette = workspace.read_with(cx, |workspace, _| {
// workspace.modal::<CommandPalette>().unwrap()
// });
// Add namespace filter, and redeploy the palette
cx.update(|cx| {
cx.set_global(CommandPaletteFilter::default());
cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
filter.filtered_namespaces.insert("editor");
})
});
// palette
// .update(cx, |palette, cx| {
// palette
// .delegate_mut()
// .update_matches("bcksp".to_string(), cx)
// })
// .await;
cx.simulate_keystrokes("cmd-shift-p");
cx.simulate_input("bcksp");
// palette.update(cx, |palette, _| {
// assert!(palette.delegate().matches.is_empty())
// });
// }
let palette = workspace.update(cx, |workspace, cx| {
workspace
.active_modal::<CommandPalette>(cx)
.unwrap()
.read(cx)
.picker
.clone()
});
palette.update(cx, |palette, _| {
assert!(palette.delegate.matches.is_empty())
});
}
// fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
// cx.update(|cx| {
// let app_state = AppState::test(cx);
// theme::init(cx);
// language::init(cx);
// editor::init(cx);
// workspace::init(app_state.clone(), cx);
// init(cx);
// Project::init_settings(cx);
// app_state
// })
// }
// }
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
cx.update(|cx| {
let app_state = AppState::test(cx);
theme::init(cx);
language::init(cx);
editor::init(cx);
workspace::init(app_state.clone(), cx);
init(cx);
Project::init_settings(cx);
settings::load_default_keymap(cx);
app_state
})
}
}

View File

@ -31,7 +31,7 @@ pub use block_map::{
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
};
pub use self::fold_map::FoldPoint;
pub use self::fold_map::{Fold, FoldPoint};
pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@ -124,7 +124,7 @@ impl DisplayMap {
self.fold(
other
.folds_in_range(0..other.buffer_snapshot.len())
.map(|fold| fold.to_offset(&other.buffer_snapshot)),
.map(|fold| fold.range.to_offset(&other.buffer_snapshot)),
cx,
);
}
@ -723,7 +723,7 @@ impl DisplaySnapshot {
DisplayPoint(point)
}
pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Range<Anchor>>
pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Fold>
where
T: ToOffset,
{

View File

@ -3,15 +3,16 @@ use super::{
Highlights,
};
use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
use gpui::{HighlightStyle, Hsla};
use gpui::{ElementId, HighlightStyle, Hsla};
use language::{Chunk, Edit, Point, TextSummary};
use std::{
any::TypeId,
cmp::{self, Ordering},
iter,
ops::{Add, AddAssign, Range, Sub},
ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
};
use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
use util::post_inc;
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct FoldPoint(pub Point);
@ -90,12 +91,16 @@ impl<'a> FoldMapWriter<'a> {
}
// For now, ignore any ranges that span an excerpt boundary.
let fold = Fold(buffer.anchor_after(range.start)..buffer.anchor_before(range.end));
if fold.0.start.excerpt_id != fold.0.end.excerpt_id {
let fold_range =
FoldRange(buffer.anchor_after(range.start)..buffer.anchor_before(range.end));
if fold_range.0.start.excerpt_id != fold_range.0.end.excerpt_id {
continue;
}
folds.push(fold);
folds.push(Fold {
id: FoldId(post_inc(&mut self.0.next_fold_id.0)),
range: fold_range,
});
let inlay_range =
snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end);
@ -106,13 +111,13 @@ impl<'a> FoldMapWriter<'a> {
}
let buffer = &snapshot.buffer;
folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, buffer));
folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(&a.range, &b.range, buffer));
self.0.snapshot.folds = {
let mut new_tree = SumTree::new();
let mut cursor = self.0.snapshot.folds.cursor::<Fold>();
let mut cursor = self.0.snapshot.folds.cursor::<FoldRange>();
for fold in folds {
new_tree.append(cursor.slice(&fold, Bias::Right, buffer), buffer);
new_tree.append(cursor.slice(&fold.range, Bias::Right, buffer), buffer);
new_tree.push(fold, buffer);
}
new_tree.append(cursor.suffix(buffer), buffer);
@ -138,7 +143,8 @@ impl<'a> FoldMapWriter<'a> {
let mut folds_cursor =
intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive);
while let Some(fold) = folds_cursor.item() {
let offset_range = fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer);
let offset_range =
fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer);
if offset_range.end > offset_range.start {
let inlay_range = snapshot.to_inlay_offset(offset_range.start)
..snapshot.to_inlay_offset(offset_range.end);
@ -175,6 +181,7 @@ impl<'a> FoldMapWriter<'a> {
pub struct FoldMap {
snapshot: FoldSnapshot,
ellipses_color: Option<Hsla>,
next_fold_id: FoldId,
}
impl FoldMap {
@ -197,6 +204,7 @@ impl FoldMap {
ellipses_color: None,
},
ellipses_color: None,
next_fold_id: FoldId::default(),
};
let snapshot = this.snapshot.clone();
(this, snapshot)
@ -242,8 +250,8 @@ impl FoldMap {
while let Some(fold) = folds.next() {
if let Some(next_fold) = folds.peek() {
let comparison = fold
.0
.cmp(&next_fold.0, &self.snapshot.inlay_snapshot.buffer);
.range
.cmp(&next_fold.range, &self.snapshot.inlay_snapshot.buffer);
assert!(comparison.is_le());
}
}
@ -304,9 +312,9 @@ impl FoldMap {
let anchor = inlay_snapshot
.buffer
.anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start));
let mut folds_cursor = self.snapshot.folds.cursor::<Fold>();
let mut folds_cursor = self.snapshot.folds.cursor::<FoldRange>();
folds_cursor.seek(
&Fold(anchor..Anchor::max()),
&FoldRange(anchor..Anchor::max()),
Bias::Left,
&inlay_snapshot.buffer,
);
@ -315,8 +323,8 @@ impl FoldMap {
let inlay_snapshot = &inlay_snapshot;
move || {
let item = folds_cursor.item().map(|f| {
let buffer_start = f.0.start.to_offset(&inlay_snapshot.buffer);
let buffer_end = f.0.end.to_offset(&inlay_snapshot.buffer);
let buffer_start = f.range.start.to_offset(&inlay_snapshot.buffer);
let buffer_end = f.range.end.to_offset(&inlay_snapshot.buffer);
inlay_snapshot.to_inlay_offset(buffer_start)
..inlay_snapshot.to_inlay_offset(buffer_end)
});
@ -596,13 +604,13 @@ impl FoldSnapshot {
self.transforms.summary().output.longest_row
}
pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Range<Anchor>>
pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Fold>
where
T: ToOffset,
{
let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
iter::from_fn(move || {
let item = folds.item().map(|f| &f.0);
let item = folds.item();
folds.next(&self.inlay_snapshot.buffer);
item
})
@ -830,10 +838,39 @@ impl sum_tree::Summary for TransformSummary {
}
}
#[derive(Clone, Debug)]
struct Fold(Range<Anchor>);
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
pub struct FoldId(usize);
impl Default for Fold {
impl Into<ElementId> for FoldId {
fn into(self) -> ElementId {
ElementId::Integer(self.0)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Fold {
pub id: FoldId,
pub range: FoldRange,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FoldRange(Range<Anchor>);
impl Deref for FoldRange {
type Target = Range<Anchor>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for FoldRange {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Default for FoldRange {
fn default() -> Self {
Self(Anchor::min()..Anchor::max())
}
@ -844,17 +881,17 @@ impl sum_tree::Item for Fold {
fn summary(&self) -> Self::Summary {
FoldSummary {
start: self.0.start.clone(),
end: self.0.end.clone(),
min_start: self.0.start.clone(),
max_end: self.0.end.clone(),
start: self.range.start.clone(),
end: self.range.end.clone(),
min_start: self.range.start.clone(),
max_end: self.range.end.clone(),
count: 1,
}
}
}
#[derive(Clone, Debug)]
struct FoldSummary {
pub struct FoldSummary {
start: Anchor,
end: Anchor,
min_start: Anchor,
@ -900,14 +937,14 @@ impl sum_tree::Summary for FoldSummary {
}
}
impl<'a> sum_tree::Dimension<'a, FoldSummary> for Fold {
impl<'a> sum_tree::Dimension<'a, FoldSummary> for FoldRange {
fn add_summary(&mut self, summary: &'a FoldSummary, _: &MultiBufferSnapshot) {
self.0.start = summary.start.clone();
self.0.end = summary.end.clone();
}
}
impl<'a> sum_tree::SeekTarget<'a, FoldSummary, Fold> for Fold {
impl<'a> sum_tree::SeekTarget<'a, FoldSummary, FoldRange> for FoldRange {
fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering {
self.0.cmp(&other.0, buffer)
}
@ -1321,7 +1358,10 @@ mod tests {
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
let fold_ranges = snapshot
.folds_in_range(Point::new(1, 0)..Point::new(1, 3))
.map(|fold| fold.start.to_point(&buffer_snapshot)..fold.end.to_point(&buffer_snapshot))
.map(|fold| {
fold.range.start.to_point(&buffer_snapshot)
..fold.range.end.to_point(&buffer_snapshot)
})
.collect::<Vec<_>>();
assert_eq!(
fold_ranges,
@ -1553,10 +1593,9 @@ mod tests {
.filter(|fold| {
let start = buffer_snapshot.anchor_before(start);
let end = buffer_snapshot.anchor_after(end);
start.cmp(&fold.0.end, &buffer_snapshot) == Ordering::Less
&& end.cmp(&fold.0.start, &buffer_snapshot) == Ordering::Greater
start.cmp(&fold.range.end, &buffer_snapshot) == Ordering::Less
&& end.cmp(&fold.range.start, &buffer_snapshot) == Ordering::Greater
})
.map(|fold| fold.0)
.collect::<Vec<_>>();
assert_eq!(
@ -1639,10 +1678,10 @@ mod tests {
let buffer = &inlay_snapshot.buffer;
let mut folds = self.snapshot.folds.items(buffer);
// Ensure sorting doesn't change how folds get merged and displayed.
folds.sort_by(|a, b| a.0.cmp(&b.0, buffer));
folds.sort_by(|a, b| a.range.cmp(&b.range, buffer));
let mut fold_ranges = folds
.iter()
.map(|fold| fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer))
.map(|fold| fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer))
.peekable();
let mut merged_ranges = Vec::new();

View File

@ -39,12 +39,12 @@ use futures::FutureExt;
use fuzzy::{StringMatch, StringMatchCandidate};
use git::diff_hunk_to_display;
use gpui::{
action, actions, div, point, prelude::*, px, relative, rems, render_view, size, uniform_list,
AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem,
Component, Context, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight,
HighlightStyle, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels,
Render, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext,
VisualContext, WeakView, WindowContext,
action, actions, div, point, prelude::*, px, relative, rems, size, uniform_list, AnyElement,
AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle,
Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled,
Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext,
WeakView, WindowContext,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
@ -97,7 +97,7 @@ use text::{OffsetUtf16, Rope};
use theme::{
ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
};
use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, TextTooltip};
use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, Tooltip};
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{
item::{ItemEvent, ItemHandle},
@ -4372,69 +4372,42 @@ impl Editor {
}
}
// pub fn render_fold_indicators(
// &self,
// fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
// style: &EditorStyle,
// gutter_hovered: bool,
// line_height: f32,
// gutter_margin: f32,
// cx: &mut ViewContext<Self>,
// ) -> Vec<Option<AnyElement<Self>>> {
// enum FoldIndicators {}
// let style = style.folds.clone();
// fold_data
// .iter()
// .enumerate()
// .map(|(ix, fold_data)| {
// fold_data
// .map(|(fold_status, buffer_row, active)| {
// (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
// MouseEventHandler::new::<FoldIndicators, _>(
// ix as usize,
// cx,
// |mouse_state, _| {
// Svg::new(match fold_status {
// FoldStatus::Folded => style.folded_icon.clone(),
// FoldStatus::Foldable => style.foldable_icon.clone(),
// })
// .with_color(
// style
// .indicator
// .in_state(fold_status == FoldStatus::Folded)
// .style_for(mouse_state)
// .color,
// )
// .constrained()
// .with_width(gutter_margin * style.icon_margin_scale)
// .aligned()
// .constrained()
// .with_height(line_height)
// .with_width(gutter_margin)
// .aligned()
// },
// )
// .with_cursor_style(CursorStyle::PointingHand)
// .with_padding(Padding::uniform(3.))
// .on_click(MouseButton::Left, {
// move |_, editor, cx| match fold_status {
// FoldStatus::Folded => {
// editor.unfold_at(&UnfoldAt { buffer_row }, cx);
// }
// FoldStatus::Foldable => {
// editor.fold_at(&FoldAt { buffer_row }, cx);
// }
// }
// })
// .into_any()
// })
// })
// .flatten()
// })
// .collect()
// }
pub fn render_fold_indicators(
&self,
fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
style: &EditorStyle,
gutter_hovered: bool,
line_height: Pixels,
gutter_margin: Pixels,
cx: &mut ViewContext<Self>,
) -> Vec<Option<AnyElement<Self>>> {
fold_data
.iter()
.enumerate()
.map(|(ix, fold_data)| {
fold_data
.map(|(fold_status, buffer_row, active)| {
(active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
let icon = match fold_status {
FoldStatus::Folded => ui::Icon::ChevronRight,
FoldStatus::Foldable => ui::Icon::ChevronDown,
};
IconButton::new(ix as usize, icon)
.on_click(move |editor: &mut Editor, cx| match fold_status {
FoldStatus::Folded => {
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
}
FoldStatus::Foldable => {
editor.fold_at(&FoldAt { buffer_row }, cx);
}
})
.render()
})
})
.flatten()
})
.collect()
}
pub fn context_menu_visible(&self) -> bool {
self.context_menu
@ -5330,8 +5303,8 @@ impl Editor {
buffer.anchor_before(range_to_move.start)
..buffer.anchor_after(range_to_move.end),
) {
let mut start = fold.start.to_point(&buffer);
let mut end = fold.end.to_point(&buffer);
let mut start = fold.range.start.to_point(&buffer);
let mut end = fold.range.end.to_point(&buffer);
start.row -= row_delta;
end.row -= row_delta;
refold_ranges.push(start..end);
@ -5421,8 +5394,8 @@ impl Editor {
buffer.anchor_before(range_to_move.start)
..buffer.anchor_after(range_to_move.end),
) {
let mut start = fold.start.to_point(&buffer);
let mut end = fold.end.to_point(&buffer);
let mut start = fold.range.start.to_point(&buffer);
let mut end = fold.range.end.to_point(&buffer);
start.row += row_delta;
end.row += row_delta;
refold_ranges.push(start..end);
@ -7804,25 +7777,18 @@ impl Editor {
}
div()
.pl(cx.anchor_x)
.child(render_view(
.child(rename_editor.render_with(EditorElement::new(
&rename_editor,
EditorElement::new(
&rename_editor,
EditorStyle {
background: cx.theme().system().transparent,
local_player: cx.editor_style.local_player,
text: text_style,
scrollbar_width: cx
.editor_style
.scrollbar_width,
syntax: cx.editor_style.syntax.clone(),
diagnostic_style: cx
.editor_style
.diagnostic_style
.clone(),
},
),
))
EditorStyle {
background: cx.theme().system().transparent,
local_player: cx.editor_style.local_player,
text: text_style,
scrollbar_width: cx.editor_style.scrollbar_width,
syntax: cx.editor_style.syntax.clone(),
diagnostic_style:
cx.editor_style.diagnostic_style.clone(),
},
)))
.render()
}
}),
@ -9401,6 +9367,12 @@ pub struct EditorReleased(pub WeakView<Editor>);
//
impl EventEmitter<Event> for Editor {}
impl FocusableView for Editor {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for Editor {
type Element = EditorElement;
@ -10019,7 +9991,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
.on_click(move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new(message.clone()));
})
.tooltip(|_, cx| cx.build_view(|cx| TextTooltip::new("Copy diagnostic message")))
.tooltip(|_, cx| Tooltip::text("Copy diagnostic message", cx))
.render()
})
}

View File

@ -3851,12 +3851,12 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
});
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
view.condition::<crate::Event>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
.await;
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.change_selections(None, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
@ -3867,7 +3867,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| { view.selections.display_ranges(cx) }),
view.update(cx, |view, cx| { view.selections.display_ranges(cx) }),
&[
DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
@ -3875,50 +3875,50 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
]
);
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
&[
DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
]
);
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
&[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
);
// Trying to expand the selected syntax node one more time has no effect.
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
&[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
);
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
&[
DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
]
);
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
&[
DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
@ -3926,11 +3926,11 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
]
);
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
&[
DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
@ -3939,11 +3939,11 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
);
// Trying to shrink the selected syntax node one more time has no effect.
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
&[
DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
@ -3953,7 +3953,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
// Ensure that we keep expanding the selection if the larger selection starts or ends within
// a fold.
view.update(&mut cx, |view, cx| {
view.update(cx, |view, cx| {
view.fold_ranges(
vec![
Point::new(0, 21)..Point::new(0, 24),
@ -3965,7 +3965,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
});
assert_eq!(
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
&[
DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
@ -4017,8 +4017,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
});
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let cx = &mut cx;
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor
.condition::<crate::Event>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
.await;
@ -4583,8 +4582,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
});
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let cx = &mut cx;
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
.await;
@ -4734,8 +4732,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
});
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let cx = &mut cx;
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor
.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
.await;
@ -4957,8 +4954,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
let fake_server = fake_servers.next().await.unwrap();
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let cx = &mut cx;
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
assert!(cx.read(|cx| editor.is_dirty(cx)));
@ -5077,8 +5073,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
let fake_server = fake_servers.next().await.unwrap();
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let cx = &mut cx;
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
assert!(cx.read(|cx| editor.is_dirty(cx)));
@ -5205,8 +5200,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
let fake_server = fake_servers.next().await.unwrap();
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let cx = &mut cx;
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
let format = editor
@ -5993,8 +5987,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
multibuffer
});
let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
let cx = &mut cx;
let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
view.update(cx, |view, cx| {
assert_eq!(view.text(cx), "aaaa\nbbbb");
view.change_selections(None, cx, |s| {
@ -6064,8 +6057,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
multibuffer
});
let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
let cx = &mut cx;
let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
view.update(cx, |view, cx| {
let (expected_text, selection_ranges) = marked_text_ranges(
indoc! {"
@ -6302,8 +6294,7 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
});
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let cx = &mut cx;
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
.await;
@ -8112,8 +8103,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
let buffer_text = "one\ntwo\nthree\n";
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let cx = &mut cx;
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
editor

File diff suppressed because it is too large Load Diff

View File

@ -60,8 +60,8 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
let folds_end = Point::new(hunk.buffer_range.end + 2, 0);
let folds_range = folds_start..folds_end;
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold_range| {
let fold_point_range = fold_range.to_point(&snapshot.buffer_snapshot);
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| {
let fold_point_range = fold.range.to_point(&snapshot.buffer_snapshot);
let fold_point_range = fold_point_range.start..=fold_point_range.end;
let folded_start = fold_point_range.contains(&hunk_start_point);
@ -72,7 +72,7 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
});
if let Some(fold) = containing_fold {
let row = fold.start.to_display_point(snapshot).row();
let row = fold.range.start.to_display_point(snapshot).row();
DisplayDiffHunk::Folded { display_row: row }
} else {
let start = hunk_start_point.to_display_point(snapshot).row();

View File

@ -527,10 +527,6 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor)
}
impl Item for Editor {
fn focus_handle(&self) -> FocusHandle {
self.focus_handle.clone()
}
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
todo!();
// if let Ok(data) = data.downcast::<NavigationData>() {

View File

@ -426,7 +426,7 @@ impl Editor {
pub fn read_scroll_position_from_db(
&mut self,
item_id: usize,
item_id: u64,
workspace_id: WorkspaceId,
cx: &mut ViewContext<Editor>,
) {

File diff suppressed because it is too large Load Diff

View File

@ -54,6 +54,9 @@ pub trait Action: std::fmt::Debug + 'static {
where
Self: Sized;
fn build(value: Option<serde_json::Value>) -> Result<Box<dyn Action>>
where
Self: Sized;
fn is_registered() -> bool
where
Self: Sized;
@ -65,7 +68,7 @@ pub trait Action: std::fmt::Debug + 'static {
// Types become actions by satisfying a list of trait bounds.
impl<A> 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::<A>();
@ -88,6 +91,14 @@ where
Ok(Box::new(action))
}
fn is_registered() -> bool {
ACTION_REGISTRY
.read()
.names_by_type_id
.get(&TypeId::of::<A>())
.is_some()
}
fn partial_eq(&self, action: &dyn Action) -> bool {
action
.as_any()

View File

@ -1,7 +1,7 @@
use crate::{
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, ForegroundExecutor,
Model, ModelContext, Render, Result, Task, View, ViewContext, VisualContext, WindowContext,
WindowHandle,
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView,
ForegroundExecutor, Model, ModelContext, Render, Result, Task, View, ViewContext,
VisualContext, WindowContext, WindowHandle,
};
use anyhow::{anyhow, Context as _};
use derive_more::{Deref, DerefMut};
@ -307,4 +307,13 @@ impl VisualContext for AsyncWindowContext {
self.window
.update(self, |_, cx| cx.replace_root_view(build_view))
}
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
where
V: FocusableView,
{
self.window.update(self, |_, cx| {
view.read(cx).focus_handle(cx).clone().focus(cx);
})
}
}

View File

@ -68,6 +68,7 @@ impl EntityMap {
}
/// Move an entity to the stack.
#[track_caller]
pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> {
self.assert_valid_context(model);
let entity = Some(

View File

@ -1,8 +1,8 @@
use crate::{
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent,
Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View,
ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TestWindow,
View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
};
use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt};
@ -140,7 +140,7 @@ impl TestAppContext {
.any_handle
}
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, VisualTestContext)
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext)
where
F: FnOnce(&mut ViewContext<V>) -> V,
V: Render,
@ -149,7 +149,9 @@ impl TestAppContext {
let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window));
drop(cx);
let view = window.root_view(self).unwrap();
(view, VisualTestContext::from_window(*window.deref(), self))
let cx = Box::new(VisualTestContext::from_window(*window.deref(), self));
// it might be nice to try and cleanup these at the end of each test.
(view, Box::leak(cx))
}
pub fn simulate_new_path_selection(
@ -220,7 +222,35 @@ impl TestAppContext {
{
window
.update(self, |_, cx| cx.dispatch_action(action.boxed_clone()))
.unwrap()
.unwrap();
self.background_executor.run_until_parked()
}
/// simulate_keystrokes takes a space-separated list of keys to type.
/// cx.simulate_keystrokes("cmd-shift-p b k s p enter")
/// will run backspace on the current editor through the command palette.
pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
for keystroke in keystrokes
.split(" ")
.map(Keystroke::parse)
.map(Result::unwrap)
{
self.dispatch_keystroke(window, keystroke.into(), false);
}
self.background_executor.run_until_parked()
}
/// simulate_input takes a string of text to type.
/// cx.simulate_input("abc")
/// will type abc into your current editor.
pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) {
self.dispatch_keystroke(window, keystroke.into(), false);
}
self.background_executor.run_until_parked()
}
pub fn dispatch_keystroke(
@ -229,15 +259,41 @@ impl TestAppContext {
keystroke: Keystroke,
is_held: bool,
) {
let keystroke2 = keystroke.clone();
let handled = window
.update(self, |_, cx| {
cx.dispatch_event(InputEvent::KeyDown(KeyDownEvent { keystroke, is_held }))
})
.is_ok_and(|handled| handled);
if !handled {
// todo!() simluate input here
if handled {
return;
}
let input_handler = self.update_test_window(window, |window| window.input_handler.clone());
let Some(input_handler) = input_handler else {
panic!(
"dispatch_keystroke {:?} failed to dispatch action or input",
&keystroke2
);
};
let text = keystroke2.ime_key.unwrap_or(keystroke2.key);
input_handler.lock().replace_text_in_range(None, &text);
}
pub fn update_test_window<R>(
&mut self,
window: AnyWindowHandle,
f: impl FnOnce(&mut TestWindow) -> R,
) -> R {
window
.update(self, |_, cx| {
f(cx.window
.platform_window
.as_any_mut()
.downcast_mut::<TestWindow>()
.unwrap())
})
.unwrap()
}
pub fn notifications<T: 'static>(&mut self, entity: &Model<T>) -> impl Stream<Item = ()> {
@ -401,12 +457,24 @@ impl<'a> VisualTestContext<'a> {
Self { cx, window }
}
pub fn run_until_parked(&self) {
self.cx.background_executor.run_until_parked();
}
pub fn dispatch_action<A>(&mut self, action: A)
where
A: Action,
{
self.cx.dispatch_action(self.window, action)
}
pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
self.cx.simulate_keystrokes(self.window, keystrokes)
}
pub fn simulate_input(&mut self, input: &str) {
self.cx.simulate_input(self.window, input)
}
}
impl<'a> Context for VisualTestContext<'a> {
@ -494,6 +562,14 @@ impl<'a> VisualContext for VisualTestContext<'a> {
.update(self.cx, |_, cx| cx.replace_root_view(build_view))
.unwrap()
}
fn focus_view<V: crate::FocusableView>(&mut self, view: &View<V>) -> Self::Result<()> {
self.window
.update(self.cx, |_, cx| {
view.read(cx).focus_handle(cx).clone().focus(cx)
})
.unwrap()
}
}
impl AnyWindowHandle {

View File

@ -3,28 +3,19 @@ use crate::{
};
use derive_more::{Deref, DerefMut};
pub(crate) use smallvec::SmallVec;
use std::{any::Any, mem};
use std::{any::Any, fmt::Debug, mem};
pub trait Element<V: 'static> {
type ElementState: 'static;
fn element_id(&self) -> Option<ElementId>;
/// 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<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> Self::ElementState;
fn layout(
&mut self,
view_state: &mut V,
element_state: &mut Self::ElementState,
previous_element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> LayoutId;
) -> (LayoutId, Self::ElementState);
fn paint(
&mut self,
@ -33,6 +24,42 @@ pub trait Element<V: 'static> {
element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>,
);
fn draw<T, R>(
self,
origin: Point<Pixels>,
available_space: Size<T>,
view_state: &mut V,
cx: &mut ViewContext<V>,
f: impl FnOnce(&Self::ElementState, &mut ViewContext<V>) -> R,
) -> R
where
Self: Sized,
T: Clone + Default + Debug + Into<AvailableSpace>,
{
let mut element = RenderedElement {
element: self,
phase: ElementRenderPhase::Start,
};
element.draw(origin, available_space.map(Into::into), view_state, cx);
if let ElementRenderPhase::Painted { frame_state } = &element.phase {
if let Some(frame_state) = frame_state.as_ref() {
f(&frame_state, cx)
} else {
let element_id = element
.element
.element_id()
.expect("we either have some frame_state or some element_id");
cx.with_element_state(element_id, |element_state, cx| {
let element_state = element_state.unwrap();
let result = f(&element_state, cx);
(result, element_state)
})
}
} else {
unreachable!()
}
}
}
#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
@ -60,7 +87,6 @@ pub trait ParentComponent<V: 'static> {
}
trait ElementObject<V> {
fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId;
fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
fn measure(
@ -87,9 +113,6 @@ struct RenderedElement<V: 'static, E: Element<V>> {
enum ElementRenderPhase<V> {
#[default]
Start,
Initialized {
frame_state: Option<V>,
},
LayoutRequested {
layout_id: LayoutId,
frame_state: Option<V>,
@ -99,7 +122,9 @@ enum ElementRenderPhase<V> {
available_space: Size<AvailableSpace>,
frame_state: Option<V>,
},
Painted,
Painted {
frame_state: Option<V>,
},
}
/// Internal struct that wraps an element to store Layout and ElementState after the element is rendered.
@ -119,45 +144,22 @@ where
E: Element<V>,
E::ElementState: 'static,
{
fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
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<V>) -> 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 => {
| ElementRenderPhase::Painted { .. } => {
panic!("element rendered twice")
}
};
@ -192,7 +194,7 @@ where
self.element
.paint(bounds, view_state, frame_state.as_mut().unwrap(), cx);
}
ElementRenderPhase::Painted
ElementRenderPhase::Painted { frame_state }
}
_ => panic!("must call layout before paint"),
@ -206,10 +208,6 @@ where
cx: &mut ViewContext<V>,
) -> Size<Pixels> {
if matches!(&self.phase, ElementRenderPhase::Start) {
self.initialize(view_state, cx);
}
if matches!(&self.phase, ElementRenderPhase::Initialized { .. }) {
self.layout(view_state, cx);
}
@ -246,16 +244,13 @@ where
fn draw(
&mut self,
mut origin: Point<Pixels>,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
view_state: &mut V,
cx: &mut ViewContext<V>,
) {
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))
}
}
@ -271,10 +266,6 @@ impl<V> AnyElement<V> {
AnyElement(Box::new(RenderedElement::new(element)))
}
pub fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
self.0.initialize(view_state, cx);
}
pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
self.0.layout(view_state, cx)
}
@ -355,25 +346,16 @@ where
None
}
fn initialize(
&mut self,
view_state: &mut V,
_rendered_element: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> 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<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> 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(

View File

@ -6,15 +6,15 @@ use crate::{
SharedString, Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility,
};
use collections::HashMap;
use parking_lot::Mutex;
use refineable::Refineable;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
cell::RefCell,
fmt::Debug,
marker::PhantomData,
mem,
sync::Arc,
rc::Rc,
time::Duration,
};
use taffy::style::Overflow;
@ -22,7 +22,6 @@ use util::ResultExt;
const DRAG_THRESHOLD: f64 = 2.;
const TOOLTIP_DELAY: Duration = Duration::from_millis(500);
const TOOLTIP_OFFSET: Point<Pixels> = Point::new(px(10.0), px(8.0));
pub struct GroupStyle {
pub group: SharedString,
@ -229,6 +228,20 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
mut self,
listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
) -> Self {
// NOTE: this debug assert has the side-effect of working around
// a bug where a crate consisting only of action definitions does
// not register the actions in debug builds:
//
// https://github.com/rust-lang/rust/issues/47384
// https://github.com/mmastrac/rust-ctor/issues/280
//
// if we are relying on this side-effect still, removing the debug_assert!
// likely breaks the command_palette tests.
debug_assert!(
A::is_registered(),
"{:?} is not registered as an action",
A::qualified_name()
);
self.interactivity().action_listeners.push((
TypeId::of::<A>(),
Box::new(move |view, action, phase, cx| {
@ -394,21 +407,19 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo
self
}
fn tooltip<W>(
fn tooltip(
mut self,
build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
) -> Self
where
Self: Sized,
W: 'static + Render,
{
debug_assert!(
self.interactivity().tooltip_builder.is_none(),
"calling tooltip more than once on the same element is not supported"
);
self.interactivity().tooltip_builder = Some(Arc::new(move |view_state, cx| {
build_tooltip(view_state, cx).into()
}));
self.interactivity().tooltip_builder =
Some(Rc::new(move |view_state, cx| build_tooltip(view_state, cx)));
self
}
@ -423,14 +434,6 @@ pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> {
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,
@ -555,7 +558,7 @@ type DropListener<V> = dyn Fn(&mut V, AnyView, &mut ViewContext<V>) + 'static;
pub type HoverListener<V> = Box<dyn Fn(&mut V, bool, &mut ViewContext<V>) + 'static>;
pub type TooltipBuilder<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
pub type TooltipBuilder<V> = Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
pub type KeyDownListener<V> =
Box<dyn Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
@ -603,46 +606,36 @@ impl<V: 'static> Element<V> for Div<V> {
self.interactivity.element_id.clone()
}
fn initialize(
fn layout(
&mut self,
view_state: &mut V,
element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> 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<V>,
) -> 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::<SmallVec<_>>();
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(
@ -711,6 +704,12 @@ pub struct DivState {
interactive_state: InteractiveElementState,
}
impl DivState {
pub fn is_active(&self) -> bool {
self.interactive_state.pending_mouse_down.borrow().is_some()
}
}
pub struct Interactivity<V> {
pub element_id: Option<ElementId>,
pub key_context: KeyContext,
@ -720,7 +719,6 @@ pub struct Interactivity<V> {
pub group: Option<SharedString>,
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<GroupStyle>,
@ -746,11 +744,12 @@ impl<V> Interactivity<V>
where
V: 'static,
{
pub fn initialize(
pub fn layout(
&mut self,
element_state: Option<InteractiveElementState>,
cx: &mut ViewContext<V>,
) -> InteractiveElementState {
f: impl FnOnce(Style, &mut ViewContext<V>) -> 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.
@ -765,17 +764,9 @@ where
});
}
element_state
}
pub fn layout(
&mut self,
element_state: &mut InteractiveElementState,
cx: &mut ViewContext<V>,
f: impl FnOnce(Style, &mut ViewContext<V>) -> 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(
@ -876,7 +867,7 @@ where
if !click_listeners.is_empty() || drag_listener.is_some() {
let pending_mouse_down = element_state.pending_mouse_down.clone();
let mouse_down = pending_mouse_down.lock().clone();
let mouse_down = pending_mouse_down.borrow().clone();
if let Some(mouse_down) = mouse_down {
if let Some(drag_listener) = drag_listener {
let active_state = element_state.clicked_state.clone();
@ -890,7 +881,7 @@ where
&& bounds.contains_point(&event.position)
&& (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD
{
*active_state.lock() = ElementClickedState::default();
*active_state.borrow_mut() = ElementClickedState::default();
let cursor_offset = event.position - bounds.origin;
let drag = drag_listener(view_state, cursor_offset, cx);
cx.active_drag = Some(drag);
@ -910,13 +901,13 @@ where
listener(view_state, &mouse_click, cx);
}
}
*pending_mouse_down.lock() = None;
*pending_mouse_down.borrow_mut() = None;
cx.notify();
});
} else {
cx.on_mouse_event(move |_view_state, event: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
*pending_mouse_down.lock() = Some(event.clone());
*pending_mouse_down.borrow_mut() = Some(event.clone());
cx.notify();
}
});
@ -932,8 +923,8 @@ where
return;
}
let is_hovered =
bounds.contains_point(&event.position) && has_mouse_down.lock().is_none();
let mut was_hovered = was_hovered.lock();
bounds.contains_point(&event.position) && has_mouse_down.borrow().is_none();
let mut was_hovered = was_hovered.borrow_mut();
if is_hovered != was_hovered.clone() {
*was_hovered = is_hovered;
@ -954,13 +945,13 @@ where
}
let is_hovered =
bounds.contains_point(&event.position) && pending_mouse_down.lock().is_none();
bounds.contains_point(&event.position) && pending_mouse_down.borrow().is_none();
if !is_hovered {
active_tooltip.lock().take();
active_tooltip.borrow_mut().take();
return;
}
if active_tooltip.lock().is_none() {
if active_tooltip.borrow().is_none() {
let task = cx.spawn({
let active_tooltip = active_tooltip.clone();
let tooltip_builder = tooltip_builder.clone();
@ -968,11 +959,11 @@ where
move |view, mut cx| async move {
cx.background_executor().timer(TOOLTIP_DELAY).await;
view.update(&mut cx, move |view_state, cx| {
active_tooltip.lock().replace(ActiveTooltip {
active_tooltip.borrow_mut().replace(ActiveTooltip {
waiting: None,
tooltip: Some(AnyTooltip {
view: tooltip_builder(view_state, cx),
cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET,
cursor_offset: cx.mouse_position(),
}),
});
cx.notify();
@ -980,14 +971,14 @@ where
.ok();
}
});
active_tooltip.lock().replace(ActiveTooltip {
active_tooltip.borrow_mut().replace(ActiveTooltip {
waiting: Some(task),
tooltip: None,
});
}
});
if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() {
if let Some(active_tooltip) = element_state.active_tooltip.borrow().as_ref() {
if active_tooltip.tooltip.is_some() {
cx.active_tooltip = active_tooltip.tooltip.clone()
}
@ -995,10 +986,10 @@ where
}
let active_state = element_state.clicked_state.clone();
if !active_state.lock().is_clicked() {
if !active_state.borrow().is_clicked() {
cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| {
if phase == DispatchPhase::Capture {
*active_state.lock() = ElementClickedState::default();
*active_state.borrow_mut() = ElementClickedState::default();
cx.notify();
}
});
@ -1013,7 +1004,7 @@ where
.map_or(false, |bounds| bounds.contains_point(&down.position));
let element = bounds.contains_point(&down.position);
if group || element {
*active_state.lock() = ElementClickedState { group, element };
*active_state.borrow_mut() = ElementClickedState { group, element };
cx.notify();
}
}
@ -1024,14 +1015,14 @@ where
if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll {
let scroll_offset = element_state
.scroll_offset
.get_or_insert_with(Arc::default)
.get_or_insert_with(Rc::default)
.clone();
let line_height = cx.line_height();
let scroll_max = (content_size - bounds.size).max(&Size::default());
cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
let mut scroll_offset = scroll_offset.lock();
let mut scroll_offset = scroll_offset.borrow_mut();
let old_scroll_offset = *scroll_offset;
let delta = event.delta.pixel_delta(line_height);
@ -1060,7 +1051,7 @@ where
let scroll_offset = element_state
.scroll_offset
.as_ref()
.map(|scroll_offset| *scroll_offset.lock());
.map(|scroll_offset| *scroll_offset.borrow());
cx.with_key_dispatch(
self.key_context.clone(),
@ -1110,10 +1101,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);
}
@ -1159,7 +1146,7 @@ where
}
}
let clicked_state = element_state.clicked_state.lock();
let clicked_state = element_state.clicked_state.borrow();
if clicked_state.group {
if let Some(group) = self.group_active_style.as_ref() {
style.refine(&group.style)
@ -1186,7 +1173,6 @@ impl<V: 'static> Default for Interactivity<V> {
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,
@ -1213,11 +1199,11 @@ impl<V: 'static> Default for Interactivity<V> {
#[derive(Default)]
pub struct InteractiveElementState {
pub focus_handle: Option<FocusHandle>,
pub clicked_state: Arc<Mutex<ElementClickedState>>,
pub hover_state: Arc<Mutex<bool>>,
pub pending_mouse_down: Arc<Mutex<Option<MouseDownEvent>>>,
pub scroll_offset: Option<Arc<Mutex<Point<Pixels>>>>,
pub active_tooltip: Arc<Mutex<Option<ActiveTooltip>>>,
pub clicked_state: Rc<RefCell<ElementClickedState>>,
pub hover_state: Rc<RefCell<bool>>,
pub pending_mouse_down: Rc<RefCell<Option<MouseDownEvent>>>,
pub scroll_offset: Option<Rc<RefCell<Point<Pixels>>>>,
pub active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
}
pub struct ActiveTooltip {
@ -1307,21 +1293,12 @@ where
self.element.element_id()
}
fn initialize(
fn layout(
&mut self,
view_state: &mut V,
element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> 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<V>,
) -> LayoutId {
) -> (LayoutId, Self::ElementState) {
self.element.layout(view_state, element_state, cx)
}
@ -1402,21 +1379,12 @@ where
self.element.element_id()
}
fn initialize(
fn layout(
&mut self,
view_state: &mut V,
element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> 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<V>,
) -> LayoutId {
) -> (LayoutId, Self::ElementState) {
self.element.layout(view_state, element_state, cx)
}

View File

@ -48,21 +48,12 @@ impl<V> Element<V> for Img<V> {
self.interactivity.element_id.clone()
}
fn initialize(
fn layout(
&mut self,
_view_state: &mut V,
element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> Self::ElementState {
self.interactivity.initialize(element_state, cx)
}
fn layout(
&mut self,
_view_state: &mut V,
element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>,
) -> LayoutId {
) -> (LayoutId, Self::ElementState) {
self.interactivity.layout(element_state, cx, |style, cx| {
cx.request_layout(&style, None)
})

View File

@ -1,11 +1,13 @@
mod div;
mod img;
mod overlay;
mod svg;
mod text;
mod uniform_list;
pub use div::*;
pub use img::*;
pub use overlay::*;
pub use svg::*;
pub use text::*;
pub use uniform_list::*;

View File

@ -0,0 +1,203 @@
use smallvec::SmallVec;
use crate::{
point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentComponent, Pixels, Point,
Size, Style,
};
pub struct OverlayState {
child_layout_ids: SmallVec<[LayoutId; 4]>,
}
pub struct Overlay<V> {
children: SmallVec<[AnyElement<V>; 2]>,
anchor_corner: AnchorCorner,
fit_mode: OverlayFitMode,
// todo!();
// anchor_position: Option<Vector2F>,
// position_mode: OverlayPositionMode,
}
/// overlay gives you a floating element that will avoid overflowing the window bounds.
/// Its children should have no margin to avoid measurement issues.
pub fn overlay<V: 'static>() -> Overlay<V> {
Overlay {
children: SmallVec::new(),
anchor_corner: AnchorCorner::TopLeft,
fit_mode: OverlayFitMode::SwitchAnchor,
}
}
impl<V> Overlay<V> {
/// Sets which corner of the overlay should be anchored to the current position.
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
self.anchor_corner = anchor;
self
}
/// Snap to window edge instead of switching anchor corner when an overflow would occur.
pub fn snap_to_window(mut self) -> Self {
self.fit_mode = OverlayFitMode::SnapToWindow;
self
}
}
impl<V: 'static> ParentComponent<V> for Overlay<V> {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
&mut self.children
}
}
impl<V: 'static> Element<V> for Overlay<V> {
type ElementState = OverlayState;
fn element_id(&self) -> Option<crate::ElementId> {
None
}
fn layout(
&mut self,
view_state: &mut V,
_: Option<Self::ElementState>,
cx: &mut crate::ViewContext<V>,
) -> (crate::LayoutId, Self::ElementState) {
let child_layout_ids = self
.children
.iter_mut()
.map(|child| child.layout(view_state, cx))
.collect::<SmallVec<_>>();
let layout_id = cx.request_layout(&Style::default(), child_layout_ids.iter().copied());
(layout_id, OverlayState { child_layout_ids })
}
fn paint(
&mut self,
bounds: crate::Bounds<crate::Pixels>,
view_state: &mut V,
element_state: &mut Self::ElementState,
cx: &mut crate::ViewContext<V>,
) {
if element_state.child_layout_ids.is_empty() {
return;
}
let mut child_min = point(Pixels::MAX, Pixels::MAX);
let mut child_max = Point::default();
for child_layout_id in &element_state.child_layout_ids {
let child_bounds = cx.layout_bounds(*child_layout_id);
child_min = child_min.min(&child_bounds.origin);
child_max = child_max.max(&child_bounds.lower_right());
}
let size: Size<Pixels> = (child_max - child_min).into();
let origin = bounds.origin;
let mut desired = self.anchor_corner.get_bounds(origin, size);
let limits = Bounds {
origin: Point::zero(),
size: cx.viewport_size(),
};
match self.fit_mode {
OverlayFitMode::SnapToWindow => {
// Snap the horizontal edges of the overlay to the horizontal edges of the window if
// its horizontal bounds overflow
if desired.right() > limits.right() {
desired.origin.x -= desired.right() - limits.right();
} else if desired.left() < limits.left() {
desired.origin.x = limits.origin.x;
}
// Snap the vertical edges of the overlay to the vertical edges of the window if
// its vertical bounds overflow.
if desired.bottom() > limits.bottom() {
desired.origin.y -= desired.bottom() - limits.bottom();
} else if desired.top() < limits.top() {
desired.origin.y = limits.origin.y;
}
}
OverlayFitMode::SwitchAnchor => {
let mut anchor_corner = self.anchor_corner;
if desired.left() < limits.left() || desired.right() > limits.right() {
anchor_corner = anchor_corner.switch_axis(Axis::Horizontal);
}
if bounds.top() < limits.top() || bounds.bottom() > limits.bottom() {
anchor_corner = anchor_corner.switch_axis(Axis::Vertical);
}
// Update bounds if needed
if anchor_corner != self.anchor_corner {
desired = anchor_corner.get_bounds(origin, size)
}
}
OverlayFitMode::None => {}
}
cx.with_element_offset(desired.origin - bounds.origin, |cx| {
for child in &mut self.children {
child.paint(view_state, cx);
}
})
}
}
enum Axis {
Horizontal,
Vertical,
}
#[derive(Copy, Clone)]
pub enum OverlayFitMode {
SnapToWindow,
SwitchAnchor,
None,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AnchorCorner {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
impl AnchorCorner {
fn get_bounds(&self, origin: Point<Pixels>, size: Size<Pixels>) -> Bounds<Pixels> {
let origin = match self {
Self::TopLeft => origin,
Self::TopRight => Point {
x: origin.x - size.width,
y: origin.y,
},
Self::BottomLeft => Point {
x: origin.x,
y: origin.y - size.height,
},
Self::BottomRight => Point {
x: origin.x - size.width,
y: origin.y - size.height,
},
};
Bounds { origin, size }
}
fn switch_axis(self, axis: Axis) -> Self {
match axis {
Axis::Vertical => match self {
AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
AnchorCorner::TopRight => AnchorCorner::BottomRight,
AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
AnchorCorner::BottomRight => AnchorCorner::TopRight,
},
Axis::Horizontal => match self {
AnchorCorner::TopLeft => AnchorCorner::TopRight,
AnchorCorner::TopRight => AnchorCorner::TopLeft,
AnchorCorner::BottomLeft => AnchorCorner::BottomRight,
AnchorCorner::BottomRight => AnchorCorner::BottomLeft,
},
}
}
}

View File

@ -37,21 +37,12 @@ impl<V> Element<V> for Svg<V> {
self.interactivity.element_id.clone()
}
fn initialize(
fn layout(
&mut self,
_view_state: &mut V,
element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> Self::ElementState {
self.interactivity.initialize(element_state, cx)
}
fn layout(
&mut self,
_view_state: &mut V,
element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>,
) -> LayoutId {
) -> (LayoutId, Self::ElementState) {
self.interactivity.layout(element_state, cx, |style, cx| {
cx.request_layout(&style, None)
})

View File

@ -76,21 +76,13 @@ impl<V: 'static> Element<V> for Text<V> {
None
}
fn initialize(
&mut self,
_view_state: &mut V,
element_state: Option<Self::ElementState>,
_cx: &mut ViewContext<V>,
) -> Self::ElementState {
element_state.unwrap_or_default()
}
fn layout(
&mut self,
_view: &mut V,
element_state: &mut Self::ElementState,
element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> 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<V: 'static> Element<V> for Text<V> {
}
});
layout_id
(layout_id, element_state)
}
fn paint(

View File

@ -3,9 +3,8 @@ use crate::{
ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels,
Point, Size, StyleRefinement, Styled, ViewContext,
};
use parking_lot::Mutex;
use smallvec::SmallVec;
use std::{cmp, mem, ops::Range, sync::Arc};
use std::{cell::RefCell, cmp, mem, ops::Range, rc::Rc};
use taffy::style::Overflow;
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
@ -61,23 +60,23 @@ pub struct UniformList<V: 'static> {
}
#[derive(Clone, Default)]
pub struct UniformListScrollHandle(Arc<Mutex<Option<ScrollHandleState>>>);
pub struct UniformListScrollHandle(Rc<RefCell<Option<ScrollHandleState>>>);
#[derive(Clone, Debug)]
struct ScrollHandleState {
item_height: Pixels,
list_height: Pixels,
scroll_offset: Arc<Mutex<Point<Pixels>>>,
scroll_offset: Rc<RefCell<Point<Pixels>>>,
}
impl UniformListScrollHandle {
pub fn new() -> Self {
Self(Arc::new(Mutex::new(None)))
Self(Rc::new(RefCell::new(None)))
}
pub fn scroll_to_item(&self, ix: usize) {
if let Some(state) = &*self.0.lock() {
let mut scroll_offset = state.scroll_offset.lock();
if let Some(state) = &*self.0.borrow() {
let mut scroll_offset = state.scroll_offset.borrow_mut();
let item_top = state.item_height * ix;
let item_bottom = item_top + state.item_height;
let scroll_top = -scroll_offset.y;
@ -109,62 +108,54 @@ impl<V: 'static> Element<V> for UniformList<V> {
Some(self.id.clone())
}
fn initialize(
fn layout(
&mut self,
view_state: &mut V,
element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> 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: &mut Self::ElementState,
cx: &mut ViewContext<V>,
) -> LayoutId {
) -> (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<Option<Pixels>>,
available_space: Size<AvailableSpace>| {
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<Option<Pixels>>,
available_space: Size<AvailableSpace>| {
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(
@ -196,7 +187,7 @@ impl<V: 'static> Element<V> for UniformList<V> {
let shared_scroll_offset = element_state
.interactive
.scroll_offset
.get_or_insert_with(Arc::default)
.get_or_insert_with(Rc::default)
.clone();
interactivity.paint(
@ -222,7 +213,7 @@ impl<V: 'static> Element<V> for UniformList<V> {
.measure_item(view_state, Some(padded_bounds.size.width), cx)
.height;
if let Some(scroll_handle) = self.scroll_handle.clone() {
scroll_handle.0.lock().replace(ScrollHandleState {
scroll_handle.0.borrow_mut().replace(ScrollHandleState {
item_height,
list_height: padded_bounds.size.height,
scroll_offset: shared_scroll_offset,

View File

@ -335,6 +335,10 @@ where
};
Bounds { origin, size }
}
pub fn new(origin: Point<T>, size: Size<T>) -> Self {
Bounds { origin, size }
}
}
impl<T> Bounds<T>
@ -421,6 +425,22 @@ impl<T> Bounds<T>
where
T: Add<T, Output = T> + Clone + Default + Debug,
{
pub fn top(&self) -> T {
self.origin.y.clone()
}
pub fn bottom(&self) -> T {
self.origin.y.clone() + self.size.height.clone()
}
pub fn left(&self) -> T {
self.origin.x.clone()
}
pub fn right(&self) -> T {
self.origin.x.clone() + self.size.width.clone()
}
pub fn upper_right(&self) -> Point<T> {
Point {
x: self.origin.x.clone() + self.size.width.clone(),

View File

@ -135,6 +135,10 @@ pub trait VisualContext: Context {
) -> Self::Result<View<V>>
where
V: Render;
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
where
V: FocusableView;
}
pub trait Entity<T>: Sealed {

View File

@ -337,7 +337,6 @@ mod test {
.update(cx, |test_view, cx| cx.focus(&test_view.focus_handle))
.unwrap();
cx.dispatch_keystroke(*window, Keystroke::parse("space").unwrap(), false);
cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap(), false);
window

View File

@ -60,7 +60,7 @@ impl DispatchTree {
self.keystroke_matchers.clear();
}
pub fn push_node(&mut self, context: KeyContext, old_dispatcher: &mut Self) {
pub fn push_node(&mut self, context: KeyContext) {
let parent = self.node_stack.last().copied();
let node_id = DispatchNodeId(self.nodes.len());
self.nodes.push(DispatchNode {
@ -71,12 +71,6 @@ impl DispatchTree {
if !context.is_empty() {
self.active_node().context = context.clone();
self.context_stack.push(context);
if let Some((context_stack, matcher)) = old_dispatcher
.keystroke_matchers
.remove_entry(self.context_stack.as_slice())
{
self.keystroke_matchers.insert(context_stack, matcher);
}
}
}
@ -87,6 +81,33 @@ impl DispatchTree {
}
}
pub fn clear_keystroke_matchers(&mut self) {
self.keystroke_matchers.clear();
}
/// Preserve keystroke matchers from previous frames to support multi-stroke
/// bindings across multiple frames.
pub fn preserve_keystroke_matchers(&mut self, old_tree: &mut Self, focus_id: Option<FocusId>) {
if let Some(node_id) = focus_id.and_then(|focus_id| self.focusable_node_id(focus_id)) {
let dispatch_path = self.dispatch_path(node_id);
self.context_stack.clear();
for node_id in dispatch_path {
let node = self.node(node_id);
if !node.context.is_empty() {
self.context_stack.push(node.context.clone());
}
if let Some((context_stack, matcher)) = old_tree
.keystroke_matchers
.remove_entry(self.context_stack.as_slice())
{
self.keystroke_matchers.insert(context_stack, matcher);
}
}
}
}
pub fn on_key_event(&mut self, listener: KeyListener) {
self.active_node().key_listeners.push(listener);
}

View File

@ -22,7 +22,7 @@ pub struct TestWindow {
bounds: WindowBounds,
current_scene: Mutex<Option<Scene>>,
display: Rc<dyn PlatformDisplay>,
input_handler: Option<Box<dyn PlatformInputHandler>>,
pub(crate) input_handler: Option<Arc<Mutex<Box<dyn PlatformInputHandler>>>>,
handlers: Mutex<Handlers>,
platform: Weak<TestPlatform>,
sprite_atlas: Arc<dyn PlatformAtlas>,
@ -80,11 +80,11 @@ impl PlatformWindow for TestWindow {
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
todo!()
self
}
fn set_input_handler(&mut self, input_handler: Box<dyn crate::PlatformInputHandler>) {
self.input_handler = Some(input_handler);
self.input_handler = Some(Arc::new(Mutex::new(input_handler)));
}
fn prompt(

View File

@ -1,7 +1,8 @@
use crate::{
private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId,
Model, Pixels, Size, ViewContext, VisualContext, WeakModel, WindowContext,
BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, FocusHandle,
FocusableView, LayoutId, Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel,
WindowContext,
};
use anyhow::{Context, Result};
use std::{
@ -63,6 +64,23 @@ impl<V: 'static> View<V> {
pub fn read<'a>(&self, cx: &'a AppContext) -> &'a V {
self.model.read(cx)
}
pub fn render_with<C>(&self, component: C) -> RenderViewWith<C, V>
where
C: 'static + Component<V>,
{
RenderViewWith {
view: self.clone(),
component: Some(component),
}
}
pub fn focus_handle(&self, cx: &AppContext) -> FocusHandle
where
V: FocusableView,
{
self.read(cx).focus_handle(cx)
}
}
impl<V> Clone for View<V> {
@ -145,8 +163,7 @@ impl<V> Eq for WeakView<V> {}
#[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<dyn Any>),
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
}
@ -154,7 +171,6 @@ impl AnyView {
pub fn downgrade(&self) -> AnyWeakView {
AnyWeakView {
model: self.model.downgrade(),
initialize: self.initialize,
layout: self.layout,
paint: self.paint,
}
@ -165,7 +181,6 @@ impl AnyView {
Ok(model) => Ok(View { model }),
Err(model) => Err(Self {
model,
initialize: self.initialize,
layout: self.layout,
paint: self.paint,
}),
@ -176,13 +191,19 @@ impl AnyView {
self.model.entity_type
}
pub(crate) fn draw(&self, available_space: Size<AvailableSpace>, 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<Pixels>,
available_space: Size<AvailableSpace>,
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);
})
}
}
@ -196,7 +217,6 @@ impl<V: Render> From<View<V>> for AnyView {
fn from(value: View<V>) -> Self {
AnyView {
model: value.model.into_any(),
initialize: any_view::initialize::<V>,
layout: any_view::layout::<V>,
paint: any_view::paint::<V>,
}
@ -210,22 +230,13 @@ impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
Some(self.model.entity_id.into())
}
fn initialize(
fn layout(
&mut self,
_view_state: &mut ParentViewState,
_element_state: Option<Self::ElementState>,
cx: &mut ViewContext<ParentViewState>,
) -> Self::ElementState {
(self.initialize)(self, cx)
}
fn layout(
&mut self,
_view_state: &mut ParentViewState,
rendered_element: &mut Self::ElementState,
cx: &mut ViewContext<ParentViewState>,
) -> LayoutId {
(self.layout)(self, rendered_element, cx)
) -> (LayoutId, Self::ElementState) {
(self.layout)(self, cx)
}
fn paint(
@ -241,8 +252,7 @@ impl<ParentViewState: 'static> Element<ParentViewState> 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<dyn Any>),
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
}
@ -251,7 +261,6 @@ impl AnyWeakView {
let model = self.model.upgrade()?;
Some(AnyView {
model,
initialize: self.initialize,
layout: self.layout,
paint: self.paint,
})
@ -262,7 +271,6 @@ impl<V: Render> From<WeakView<V>> for AnyWeakView {
fn from(view: WeakView<V>) -> Self {
Self {
model: view.model.into(),
initialize: any_view::initialize::<V>,
layout: any_view::layout::<V>,
paint: any_view::paint::<V>,
}
@ -281,12 +289,12 @@ where
}
}
pub struct RenderView<C, V> {
pub struct RenderViewWith<C, V> {
view: View<V>,
component: Option<C>,
}
impl<C, ParentViewState, ViewState> Component<ParentViewState> for RenderView<C, ViewState>
impl<C, ParentViewState, ViewState> Component<ParentViewState> for RenderViewWith<C, ViewState>
where
C: 'static + Component<ViewState>,
ParentViewState: 'static,
@ -297,7 +305,7 @@ where
}
}
impl<C, ParentViewState, ViewState> Element<ParentViewState> for RenderView<C, ViewState>
impl<C, ParentViewState, ViewState> Element<ParentViewState> for RenderViewWith<C, ViewState>
where
C: 'static + Component<ViewState>,
ParentViewState: 'static,
@ -309,29 +317,16 @@ where
Some(self.view.entity_id().into())
}
fn initialize(
fn layout(
&mut self,
_: &mut ParentViewState,
_: Option<Self::ElementState>,
cx: &mut ViewContext<ParentViewState>,
) -> Self::ElementState {
cx.with_element_id(Some(self.view.entity_id()), |cx| {
self.view.update(cx, |view, cx| {
let mut element = self.component.take().unwrap().render();
element.initialize(view, cx);
element
})
})
}
fn layout(
&mut self,
_: &mut ParentViewState,
element: &mut Self::ElementState,
cx: &mut ViewContext<ParentViewState>,
) -> LayoutId {
cx.with_element_id(Some(self.view.entity_id()), |cx| {
self.view.update(cx, |view, cx| element.layout(view, cx))
) -> (LayoutId, Self::ElementState) {
self.view.update(cx, |view, cx| {
let mut element = self.component.take().unwrap().render();
let layout_id = element.layout(view, cx);
(layout_id, element)
})
}
@ -342,20 +337,7 @@ where
element: &mut Self::ElementState,
cx: &mut ViewContext<ParentViewState>,
) {
cx.with_element_id(Some(self.view.entity_id()), |cx| {
self.view.update(cx, |view, cx| element.paint(view, cx))
})
}
}
pub fn render_view<C, V>(view: &View<V>, component: C) -> RenderView<C, V>
where
C: 'static + Component<V>,
V: 'static,
{
RenderView {
view: view.clone(),
component: Some(component),
self.view.update(cx, |view, cx| element.paint(view, cx))
}
}
@ -363,27 +345,17 @@ mod any_view {
use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
use std::any::Any;
pub(crate) fn initialize<V: Render>(view: &AnyView, cx: &mut WindowContext) -> Box<dyn Any> {
cx.with_element_id(Some(view.model.entity_id), |cx| {
let view = view.clone().downcast::<V>().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<V: Render>(
view: &AnyView,
element: &mut Box<dyn Any>,
cx: &mut WindowContext,
) -> LayoutId {
) -> (LayoutId, Box<dyn Any>) {
cx.with_element_id(Some(view.model.entity_id), |cx| {
let view = view.clone().downcast::<V>().unwrap();
let element = element.downcast_mut::<AnyElement<V>>().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<dyn Any>)
})
})
}

View File

@ -185,11 +185,15 @@ impl Drop for FocusHandle {
}
}
pub trait FocusableView: Render {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
}
// Holds the state for a specific window.
pub struct Window {
pub(crate) handle: AnyWindowHandle,
pub(crate) removed: bool,
platform_window: Box<dyn PlatformWindow>,
pub(crate) platform_window: Box<dyn PlatformWindow>,
display_id: DisplayId,
sprite_atlas: Arc<dyn PlatformAtlas>,
rem_size: Pixels,
@ -216,7 +220,7 @@ pub struct Window {
// #[derive(Default)]
pub(crate) struct Frame {
element_states: HashMap<GlobalElementId, AnyBox>,
pub(crate) element_states: HashMap<GlobalElementId, AnyBox>,
mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
pub(crate) dispatch_tree: DispatchTree,
pub(crate) focus_listeners: Vec<AnyFocusListener>,
@ -393,6 +397,10 @@ impl<'a> WindowContext<'a> {
/// Move focus to the element associated with the given `FocusHandle`.
pub fn focus(&mut self, handle: &FocusHandle) {
if self.window.focus == Some(handle.id) {
return;
}
let focus_id = handle.id;
if self.window.last_blur.is_none() {
@ -400,6 +408,10 @@ impl<'a> WindowContext<'a> {
}
self.window.focus = Some(focus_id);
self.window
.current_frame
.dispatch_tree
.clear_keystroke_matchers();
self.app.push_effect(Effect::FocusChanged {
window_handle: self.window.handle,
focused: Some(focus_id),
@ -1068,29 +1080,33 @@ 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);
});
}
self.window
.current_frame
.dispatch_tree
.preserve_keystroke_matchers(
&mut self.window.previous_frame.dispatch_tree,
self.window.focus,
);
self.window.root_view = Some(root_view);
let scene = self.window.current_frame.scene_builder.build();
@ -1534,6 +1550,12 @@ impl VisualContext for WindowContext<'_> {
self.window.root_view = Some(view.clone().into());
view
}
fn focus_view<V: crate::FocusableView>(&mut self, view: &View<V>) -> Self::Result<()> {
self.update_view(view, |view, cx| {
view.focus_handle(cx).clone().focus(cx);
})
}
}
impl<'a> std::ops::Deref for WindowContext<'a> {
@ -1617,8 +1639,8 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
}
}
/// 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<R>(
&mut self,
offset: Point<Pixels>,
@ -1628,7 +1650,17 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
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<R>(
&mut self,
offset: Point<Pixels>,
f: impl FnOnce(&mut Self) -> R,
) -> R {
self.window_mut()
.current_frame
.element_offset_stack
@ -1798,8 +1830,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
self.view
}
pub fn model(&self) -> Model<V> {
self.view.model.clone()
pub fn model(&self) -> &Model<V> {
&self.view.model
}
/// Access the underlying window context.
@ -2093,7 +2125,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
window
.current_frame
.dispatch_tree
.push_node(context.clone(), &mut window.previous_frame.dispatch_tree);
.push_node(context.clone());
if let Some(focus_handle) = focus_handle.as_ref() {
window
.current_frame
@ -2131,7 +2163,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
pub fn observe_global<G: 'static>(
&mut self,
f: impl Fn(&mut V, &mut ViewContext<'_, V>) + 'static,
mut f: impl FnMut(&mut V, &mut ViewContext<'_, V>) + 'static,
) -> Subscription {
let window_handle = self.window.handle;
let view = self.view().downgrade();
@ -2197,9 +2229,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
.set_input_handler(Box::new(input_handler));
}
}
}
impl<V> ViewContext<'_, V> {
pub fn emit<Evt>(&mut self, event: Evt)
where
Evt: 'static,
@ -2212,6 +2242,13 @@ impl<V> ViewContext<'_, V> {
event: Box::new(event),
});
}
pub fn focus_self(&mut self)
where
V: FocusableView,
{
self.defer(|view, cx| view.focus_handle(cx).focus(cx))
}
}
impl<V> Context for ViewContext<'_, V> {
@ -2287,6 +2324,10 @@ impl<V: 'static> VisualContext for ViewContext<'_, V> {
{
self.window_cx.replace_root_view(build_view)
}
fn focus_view<W: FocusableView>(&mut self, view: &View<W>) -> Self::Result<()> {
self.window_cx.focus_view(view)
}
}
impl<'a, V> std::ops::Deref for ViewContext<'a, V> {
@ -2471,7 +2512,7 @@ impl From<SmallVec<[u32; 16]>> for StackingOrder {
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum ElementId {
View(EntityId),
Number(usize),
Integer(usize),
Name(SharedString),
FocusHandle(FocusId),
}
@ -2484,13 +2525,13 @@ impl From<EntityId> for ElementId {
impl From<usize> for ElementId {
fn from(id: usize) -> Self {
ElementId::Number(id)
ElementId::Integer(id)
}
}
impl From<i32> for ElementId {
fn from(id: i32) -> Self {
Self::Number(id as usize)
Self::Integer(id as usize)
}
}

View File

@ -1709,6 +1709,7 @@ impl Project {
self.open_remote_buffer_internal(&project_path.path, &worktree, cx)
};
let project_path = project_path.clone();
cx.spawn(move |this, mut cx| async move {
let load_result = load_buffer.await;
*tx.borrow_mut() = Some(this.update(&mut cx, |this, _| {
@ -1726,7 +1727,7 @@ impl Project {
cx.foreground().spawn(async move {
wait_for_loading_buffer(loading_watch)
.await
.map_err(|error| anyhow!("{}", error))
.map_err(|error| anyhow!("{project_path:?} opening failure: {error:#}"))
})
}

View File

@ -3694,7 +3694,7 @@ impl BackgroundScanner {
}
Err(err) => {
// TODO - create a special 'error' entry in the entries tree to mark this
log::error!("error reading file on event {:?}", err);
log::error!("error reading file {abs_path:?} on event: {err:#}");
}
}
}

View File

@ -1741,6 +1741,7 @@ impl Project {
self.open_remote_buffer_internal(&project_path.path, &worktree, cx)
};
let project_path = project_path.clone();
cx.spawn(move |this, mut cx| async move {
let load_result = load_buffer.await;
*tx.borrow_mut() = Some(this.update(&mut cx, |this, _| {
@ -1759,7 +1760,7 @@ impl Project {
cx.background_executor().spawn(async move {
wait_for_loading_buffer(loading_watch)
.await
.map_err(|error| anyhow!("{}", error))
.map_err(|error| anyhow!("{project_path:?} opening failure: {error:#}"))
})
}

View File

@ -3684,7 +3684,7 @@ impl BackgroundScanner {
}
Err(err) => {
// TODO - create a special 'error' entry in the entries tree to mark this
log::error!("error reading file on event {:?}", err);
log::error!("error reading file {abs_path:?} on event: {err:#}");
}
}
}

View File

@ -9,10 +9,10 @@ use file_associations::FileAssociations;
use anyhow::{anyhow, Result};
use gpui::{
actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, InteractiveComponent,
Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render, Stateful,
StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View, ViewContext,
VisualContext as _, WeakView, WindowContext,
ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, FocusableView,
InteractiveComponent, Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render,
Stateful, StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View,
ViewContext, VisualContext as _, WeakView, WindowContext,
};
use menu::{Confirm, SelectNext, SelectPrev};
use project::{
@ -32,7 +32,7 @@ use std::{
use theme::ActiveTheme as _;
use ui::{h_stack, v_stack, IconElement, Label};
use unicase::UniCase;
use util::{maybe, TryFutureExt};
use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, PanelEvent},
Workspace,
@ -130,6 +130,13 @@ pub fn init_settings(cx: &mut AppContext) {
pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
init_settings(cx);
file_associations::init(assets, cx);
cx.observe_new_views(|workspace: &mut Workspace, _| {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<ProjectPanel>(cx);
});
})
.detach();
}
#[derive(Debug)]
@ -303,32 +310,31 @@ impl ProjectPanel {
project_panel
}
pub fn load(
pub async fn load(
workspace: WeakView<Workspace>,
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
cx.spawn(|mut cx| async move {
// let serialized_panel = if let Some(panel) = cx
// .background_executor()
// .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
// .await
// .log_err()
// .flatten()
// {
// Some(serde_json::from_str::<SerializedProjectPanel>(&panel)?)
// } else {
// None
// };
workspace.update(&mut cx, |workspace, cx| {
let panel = ProjectPanel::new(workspace, cx);
// if let Some(serialized_panel) = serialized_panel {
// panel.update(cx, |panel, cx| {
// panel.width = serialized_panel.width;
// cx.notify();
// });
// }
panel
})
mut cx: AsyncWindowContext,
) -> Result<View<Self>> {
let serialized_panel = cx
.background_executor()
.spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
.await
.map_err(|e| anyhow!("Failed to load project panel: {}", e))
.log_err()
.flatten()
.map(|panel| serde_json::from_str::<SerializedProjectPanel>(&panel))
.transpose()
.log_err()
.flatten();
workspace.update(&mut cx, |workspace, cx| {
let panel = ProjectPanel::new(workspace, cx);
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width;
cx.notify();
});
}
panel
})
}
@ -1516,33 +1522,27 @@ impl workspace::dock::Panel for ProjectPanel {
cx.notify();
}
fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
Some("icons/project.svg")
fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
Some(ui::Icon::FileTree)
}
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
("Project Panel".into(), Some(Box::new(ToggleFocus)))
fn toggle_action(&self) -> Box<dyn Action> {
Box::new(ToggleFocus)
}
// fn should_change_position_on_event(event: &Self::Event) -> bool {
// matches!(event, Event::DockPositionChanged)
// }
fn has_focus(&self, _: &WindowContext) -> bool {
self.has_focus
}
fn persistent_name(&self) -> &'static str {
fn persistent_name() -> &'static str {
"Project Panel"
}
}
fn focus_handle(&self, _cx: &WindowContext) -> FocusHandle {
impl FocusableView for ProjectPanel {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
// fn is_focus_event(event: &Self::Event) -> bool {
// matches!(event, Event::Focus)
// }
}
impl ClipboardEntry {

View File

@ -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::<Result<Vec<_>>>()?;

View File

@ -164,6 +164,23 @@ impl Column for i64 {
}
}
impl StaticColumnCount for u64 {}
impl Bind for u64 {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind_int64(start_index, (*self) as i64)
.with_context(|| format!("Failed to bind i64 at index {start_index}"))?;
Ok(start_index + 1)
}
}
impl Column for u64 {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement.column_int64(start_index)? as u64;
Ok((result, start_index + 1))
}
}
impl StaticColumnCount for u32 {}
impl Bind for u32 {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {

View File

@ -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)

View File

@ -1,5 +1,6 @@
use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};
use theme2::ActiveTheme;
use ui::Tooltip;
pub struct ScrollStory;
@ -35,16 +36,18 @@ impl Render for ScrollStory {
} else {
color_2
};
div().id(id).bg(bg).size(px(100. as f32)).when(
row >= 5 && column >= 5,
|d| {
div()
.id(id)
.tooltip(move |_, cx| Tooltip::text(format!("{}, {}", row, column), cx))
.bg(bg)
.size(px(100. as f32))
.when(row >= 5 && column >= 5, |d| {
d.overflow_scroll()
.child(div().size(px(50.)).bg(color_1))
.child(div().size(px(50.)).bg(color_2))
.child(div().size(px(50.)).bg(color_1))
.child(div().size(px(50.)).bg(color_2))
},
)
})
}))
}))
}

View File

@ -1,4 +1,4 @@
use gpui::{AppContext, FontFeatures};
use gpui::{AppContext, FontFeatures, Pixels};
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use std::{collections::HashMap, path::PathBuf};
@ -15,7 +15,7 @@ pub enum TerminalDockPosition {
pub struct TerminalSettings {
pub shell: Shell,
pub working_directory: WorkingDirectory,
font_size: Option<f32>,
pub font_size: Option<Pixels>,
pub font_family: Option<String>,
pub line_height: TerminalLineHeight,
pub font_features: Option<FontFeatures>,
@ -90,14 +90,6 @@ pub struct TerminalSettingsContent {
pub detect_venv: Option<VenvSettings>,
}
impl TerminalSettings {
// todo!("move to terminal element")
// pub fn font_size(&self, cx: &AppContext) -> Option<f32> {
// self.font_size
// .map(|size| theme2::adjusted_font_size(size, cx))
// }
}
impl settings::Settings for TerminalSettings {
const KEY: Option<&'static str> = Some("terminal");

View File

@ -0,0 +1,45 @@
[package]
name = "terminal_view2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/terminal_view.rs"
doctest = false
[dependencies]
editor = { package = "editor2", path = "../editor2" }
language = { package = "language2", path = "../language2" }
gpui = { package = "gpui2", path = "../gpui2" }
project = { package = "project2", path = "../project2" }
# search = { package = "search2", path = "../search2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
util = { path = "../util" }
workspace = { package = "workspace2", path = "../workspace2" }
db = { package = "db2", path = "../db2" }
procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
terminal = { package = "terminal2", path = "../terminal2" }
smallvec.workspace = true
smol.workspace = true
mio-extras = "2.0.6"
futures.workspace = true
ordered-float.workspace = true
itertools = "0.10"
dirs = "4.0.0"
shellexpand = "2.1.0"
libc = "0.2"
anyhow.workspace = true
thiserror.workspace = true
lazy_static.workspace = true
serde.workspace = true
serde_derive.workspace = true
[dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
client = { package = "client2", path = "../client2", features = ["test-support"]}
project = { package = "project2", path = "../project2", features = ["test-support"]}
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
rand.workspace = true

View File

@ -0,0 +1,23 @@
Design notes:
This crate is split into two conceptual halves:
- The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with Alacritty and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here.
- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file.
ttys are created externally, and so can fail in unexpected ways. However, GPUI currently does not have an API for models than can fail to instantiate. `TerminalBuilder` solves this by using Rust's type system to split tty instantiation into a 2 step process: first attempt to create the file handles with `TerminalBuilder::new()`, check the result, then call `TerminalBuilder::subscribe(cx)` from within a model context.
The TerminalView struct abstracts over failed and successful terminals, passing focus through to the associated view and allowing clients to build a terminal without worrying about errors.
#Input
There are currently many distinct paths for getting keystrokes to the terminal:
1. Terminal specific characters and bindings. Things like ctrl-a mapping to ASCII control character 1, ANSI escape codes associated with the function keys, etc. These are caught with a raw key-down handler in the element and are processed immediately. This is done with the `try_keystroke()` method on Terminal
2. GPU Action handlers. GPUI clobbers a few vital keys by adding bindings to them in the global context. These keys are synthesized and then dispatched through the same `try_keystroke()` API as the above mappings
3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`.
4. Pasted text has a separate pathway.
Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal

View File

@ -0,0 +1,96 @@
#!/bin/bash
# Tom Hale, 2016. MIT Licence.
# Print out 256 colours, with each number printed in its corresponding colour
# See http://askubuntu.com/questions/821157/print-a-256-color-test-pattern-in-the-terminal/821163#821163
set -eu # Fail on errors or undeclared variables
printable_colours=256
# Return a colour that contrasts with the given colour
# Bash only does integer division, so keep it integral
function contrast_colour {
local r g b luminance
colour="$1"
if (( colour < 16 )); then # Initial 16 ANSI colours
(( colour == 0 )) && printf "15" || printf "0"
return
fi
# Greyscale # rgb_R = rgb_G = rgb_B = (number - 232) * 10 + 8
if (( colour > 231 )); then # Greyscale ramp
(( colour < 244 )) && printf "15" || printf "0"
return
fi
# All other colours:
# 6x6x6 colour cube = 16 + 36*R + 6*G + B # Where RGB are [0..5]
# See http://stackoverflow.com/a/27165165/5353461
# r=$(( (colour-16) / 36 ))
g=$(( ((colour-16) % 36) / 6 ))
# b=$(( (colour-16) % 6 ))
# If luminance is bright, print number in black, white otherwise.
# Green contributes 587/1000 to human perceived luminance - ITU R-REC-BT.601
(( g > 2)) && printf "0" || printf "15"
return
# Uncomment the below for more precise luminance calculations
# # Calculate perceived brightness
# # See https://www.w3.org/TR/AERT#color-contrast
# # and http://www.itu.int/rec/R-REC-BT.601
# # Luminance is in range 0..5000 as each value is 0..5
# luminance=$(( (r * 299) + (g * 587) + (b * 114) ))
# (( $luminance > 2500 )) && printf "0" || printf "15"
}
# Print a coloured block with the number of that colour
function print_colour {
local colour="$1" contrast
contrast=$(contrast_colour "$1")
printf "\e[48;5;%sm" "$colour" # Start block of colour
printf "\e[38;5;%sm%3d" "$contrast" "$colour" # In contrast, print number
printf "\e[0m " # Reset colour
}
# Starting at $1, print a run of $2 colours
function print_run {
local i
for (( i = "$1"; i < "$1" + "$2" && i < printable_colours; i++ )) do
print_colour "$i"
done
printf " "
}
# Print blocks of colours
function print_blocks {
local start="$1" i
local end="$2" # inclusive
local block_cols="$3"
local block_rows="$4"
local blocks_per_line="$5"
local block_length=$((block_cols * block_rows))
# Print sets of blocks
for (( i = start; i <= end; i += (blocks_per_line-1) * block_length )) do
printf "\n" # Space before each set of blocks
# For each block row
for (( row = 0; row < block_rows; row++ )) do
# Print block columns for all blocks on the line
for (( block = 0; block < blocks_per_line; block++ )) do
print_run $(( i + (block * block_length) )) "$block_cols"
done
(( i += block_cols )) # Prepare to print the next row
printf "\n"
done
done
}
print_run 0 16 # The first 16 colours are spread over the whole spectrum
printf "\n"
print_blocks 16 231 6 6 3 # 6x6x6 colour cube between 16 and 231 inclusive
print_blocks 232 255 12 2 1 # Not 50, but 24 Shades of Grey

View File

@ -0,0 +1,19 @@
#!/bin/bash
# Copied from: https://unix.stackexchange.com/a/696756
# Based on: https://gist.github.com/XVilka/8346728 and https://unix.stackexchange.com/a/404415/395213
awk -v term_cols="${width:-$(tput cols || echo 80)}" -v term_lines="${height:-1}" 'BEGIN{
s="/\\";
total_cols=term_cols*term_lines;
for (colnum = 0; colnum<total_cols; colnum++) {
r = 255-(colnum*255/total_cols);
g = (colnum*510/total_cols);
b = (colnum*255/total_cols);
if (g>255) g = 510-g;
printf "\033[48;2;%d;%d;%dm", r,g,b;
printf "\033[38;2;%d;%d;%dm", 255-r,255-g,255-b;
printf "%s\033[0m", substr(s,colnum%2+1,1);
if (colnum%term_cols==term_cols) printf "\n";
}
printf "\n";
}'

View File

@ -0,0 +1,71 @@
use std::path::PathBuf;
use db::{define_connection, query, sqlez_macros::sql};
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
define_connection! {
pub static ref TERMINAL_DB: TerminalDb<WorkspaceDb> =
&[sql!(
CREATE TABLE terminals (
workspace_id INTEGER,
item_id INTEGER UNIQUE,
working_directory BLOB,
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
),
// Remove the unique constraint on the item_id table
// SQLite doesn't have a way of doing this automatically, so
// we have to do this silly copying.
sql!(
CREATE TABLE terminals2 (
workspace_id INTEGER,
item_id INTEGER,
working_directory BLOB,
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
INSERT INTO terminals2 (workspace_id, item_id, working_directory)
SELECT workspace_id, item_id, working_directory FROM terminals;
DROP TABLE terminals;
ALTER TABLE terminals2 RENAME TO terminals;
)];
}
impl TerminalDb {
query! {
pub async fn update_workspace_id(
new_id: WorkspaceId,
old_id: WorkspaceId,
item_id: ItemId
) -> Result<()> {
UPDATE terminals
SET workspace_id = ?
WHERE workspace_id = ? AND item_id = ?
}
}
query! {
pub async fn save_working_directory(
item_id: ItemId,
workspace_id: WorkspaceId,
working_directory: PathBuf
) -> Result<()> {
INSERT OR REPLACE INTO terminals(item_id, workspace_id, working_directory)
VALUES (?, ?, ?)
}
}
query! {
pub fn get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
SELECT working_directory
FROM terminals
WHERE item_id = ? AND workspace_id = ?
}
}
}

View File

@ -0,0 +1,954 @@
// use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
// use gpui::{
// point, transparent_black, AnyElement, AppContext, Bounds, Component, CursorStyle, Element,
// FontStyle, FontWeight, HighlightStyle, Hsla, LayoutId, Line, ModelContext, MouseButton,
// Overlay, Pixels, Point, Quad, TextStyle, Underline, ViewContext, WeakModel, WindowContext,
// };
// use itertools::Itertools;
// use language::CursorShape;
// use ordered_float::OrderedFloat;
// use settings::Settings;
// use terminal::{
// alacritty_terminal::{
// ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
// grid::Dimensions,
// index::Point as AlacPoint,
// term::{cell::Flags, TermMode},
// },
// // mappings::colors::convert_color,
// terminal_settings::TerminalSettings,
// IndexedCell,
// Terminal,
// TerminalContent,
// TerminalSize,
// };
// use theme::ThemeSettings;
// use workspace::ElementId;
// use std::mem;
// use std::{fmt::Debug, ops::RangeInclusive};
// use crate::TerminalView;
// ///The information generated during layout that is necessary for painting
// pub struct LayoutState {
// cells: Vec<LayoutCell>,
// rects: Vec<LayoutRect>,
// relative_highlighted_ranges: Vec<(RangeInclusive<AlacPoint>, Hsla)>,
// cursor: Option<Cursor>,
// background_color: Hsla,
// size: TerminalSize,
// mode: TermMode,
// display_offset: usize,
// hyperlink_tooltip: Option<AnyElement<TerminalView>>,
// gutter: f32,
// }
// ///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
// struct DisplayCursor {
// line: i32,
// col: usize,
// }
// impl DisplayCursor {
// fn from(cursor_point: AlacPoint, display_offset: usize) -> Self {
// Self {
// line: cursor_point.line.0 + display_offset as i32,
// col: cursor_point.column.0,
// }
// }
// pub fn line(&self) -> i32 {
// self.line
// }
// pub fn col(&self) -> usize {
// self.col
// }
// }
// #[derive(Clone, Debug, Default)]
// struct LayoutCell {
// point: AlacPoint<i32, i32>,
// text: Line,
// }
// impl LayoutCell {
// fn new(point: AlacPoint<i32, i32>, text: Line) -> LayoutCell {
// LayoutCell { point, text }
// }
// fn paint(
// &self,
// origin: Point<Pixels>,
// layout: &LayoutState,
// _visible_bounds: Bounds<Pixels>,
// _view: &mut TerminalView,
// cx: &mut WindowContext,
// ) {
// let pos = {
// let point = self.point;
// Point::new(
// (origin.x + point.column as f32 * layout.size.cell_width).floor(),
// origin.y + point.line as f32 * layout.size.line_height,
// )
// };
// self.text.paint(pos, layout.size.line_height, cx);
// }
// }
// #[derive(Clone, Debug, Default)]
// struct LayoutRect {
// point: AlacPoint<i32, i32>,
// num_of_cells: usize,
// color: Hsla,
// }
// impl LayoutRect {
// fn new(point: AlacPoint<i32, i32>, num_of_cells: usize, color: Hsla) -> LayoutRect {
// LayoutRect {
// point,
// num_of_cells,
// color,
// }
// }
// fn extend(&self) -> Self {
// LayoutRect {
// point: self.point,
// num_of_cells: self.num_of_cells + 1,
// color: self.color,
// }
// }
// fn paint(
// &self,
// origin: Point<Pixels>,
// layout: &LayoutState,
// _view: &mut TerminalView,
// cx: &mut ViewContext<TerminalView>,
// ) {
// let position = {
// let alac_point = self.point;
// point(
// (origin.x + alac_point.column as f32 * layout.size.cell_width).floor(),
// origin.y + alac_point.line as f32 * layout.size.line_height,
// )
// };
// let size = point(
// (layout.size.cell_width * self.num_of_cells as f32).ceil(),
// layout.size.line_height,
// )
// .into();
// cx.paint_quad(
// Bounds::new(position, size),
// Default::default(),
// self.color,
// Default::default(),
// transparent_black(),
// );
// }
// }
// ///The GPUI element that paints the terminal.
// ///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection?
// pub struct TerminalElement {
// terminal: WeakModel<Terminal>,
// focused: bool,
// cursor_visible: bool,
// can_navigate_to_selected_word: bool,
// }
// impl TerminalElement {
// pub fn new(
// terminal: WeakModel<Terminal>,
// focused: bool,
// cursor_visible: bool,
// can_navigate_to_selected_word: bool,
// ) -> TerminalElement {
// TerminalElement {
// terminal,
// focused,
// cursor_visible,
// can_navigate_to_selected_word,
// }
// }
// //Vec<Range<AlacPoint>> -> Clip out the parts of the ranges
// fn layout_grid(
// grid: &Vec<IndexedCell>,
// text_style: &TextStyle,
// terminal_theme: &TerminalStyle,
// text_layout_cache: &TextLayoutCache,
// font_cache: &FontCache,
// hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
// ) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
// let mut cells = vec![];
// let mut rects = vec![];
// let mut cur_rect: Option<LayoutRect> = None;
// let mut cur_alac_color = None;
// let linegroups = grid.into_iter().group_by(|i| i.point.line);
// for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
// for cell in line {
// let mut fg = cell.fg;
// let mut bg = cell.bg;
// if cell.flags.contains(Flags::INVERSE) {
// mem::swap(&mut fg, &mut bg);
// }
// //Expand background rect range
// {
// if matches!(bg, Named(NamedColor::Background)) {
// //Continue to next cell, resetting variables if necessary
// cur_alac_color = None;
// if let Some(rect) = cur_rect {
// rects.push(rect);
// cur_rect = None
// }
// } else {
// match cur_alac_color {
// Some(cur_color) => {
// if bg == cur_color {
// cur_rect = cur_rect.take().map(|rect| rect.extend());
// } else {
// cur_alac_color = Some(bg);
// if cur_rect.is_some() {
// rects.push(cur_rect.take().unwrap());
// }
// cur_rect = Some(LayoutRect::new(
// AlacPoint::new(
// line_index as i32,
// cell.point.column.0 as i32,
// ),
// 1,
// convert_color(&bg, &terminal_theme),
// ));
// }
// }
// None => {
// cur_alac_color = Some(bg);
// cur_rect = Some(LayoutRect::new(
// AlacPoint::new(line_index as i32, cell.point.column.0 as i32),
// 1,
// convert_color(&bg, &terminal_theme),
// ));
// }
// }
// }
// }
// //Layout current cell text
// {
// let cell_text = &cell.c.to_string();
// if !is_blank(&cell) {
// let cell_style = TerminalElement::cell_style(
// &cell,
// fg,
// terminal_theme,
// text_style,
// font_cache,
// hyperlink,
// );
// let layout_cell = text_layout_cache.layout_str(
// cell_text,
// text_style.font_size,
// &[(cell_text.len(), cell_style)],
// );
// cells.push(LayoutCell::new(
// AlacPoint::new(line_index as i32, cell.point.column.0 as i32),
// layout_cell,
// ))
// };
// }
// }
// if cur_rect.is_some() {
// rects.push(cur_rect.take().unwrap());
// }
// }
// (cells, rects)
// }
// // Compute the cursor position and expected block width, may return a zero width if x_for_index returns
// // the same position for sequential indexes. Use em_width instead
// fn shape_cursor(
// cursor_point: DisplayCursor,
// size: TerminalSize,
// text_fragment: &Line,
// ) -> Option<(Point<Pixels>, Pixels)> {
// if cursor_point.line() < size.total_lines() as i32 {
// let cursor_width = if text_fragment.width == Pixels::ZERO {
// size.cell_width()
// } else {
// text_fragment.width
// };
// //Cursor should always surround as much of the text as possible,
// //hence when on pixel boundaries round the origin down and the width up
// Some((
// point(
// (cursor_point.col() as f32 * size.cell_width()).floor(),
// (cursor_point.line() as f32 * size.line_height()).floor(),
// ),
// cursor_width.ceil(),
// ))
// } else {
// None
// }
// }
// ///Convert the Alacritty cell styles to GPUI text styles and background color
// fn cell_style(
// indexed: &IndexedCell,
// fg: terminal::alacritty_terminal::ansi::Color,
// style: &TerminalStyle,
// text_style: &TextStyle,
// font_cache: &FontCache,
// hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
// ) -> RunStyle {
// let flags = indexed.cell.flags;
// let fg = convert_color(&fg, &style);
// let mut underline = flags
// .intersects(Flags::ALL_UNDERLINES)
// .then(|| Underline {
// color: Some(fg),
// squiggly: flags.contains(Flags::UNDERCURL),
// thickness: OrderedFloat(1.),
// })
// .unwrap_or_default();
// if indexed.cell.hyperlink().is_some() {
// if underline.thickness == OrderedFloat(0.) {
// underline.thickness = OrderedFloat(1.);
// }
// }
// let mut properties = Properties::new();
// if indexed.flags.intersects(Flags::BOLD | Flags::DIM_BOLD) {
// properties = *properties.weight(FontWeight::BOLD);
// }
// if indexed.flags.intersects(Flags::ITALIC) {
// properties = *properties.style(FontStyle::Italic);
// }
// let font_id = font_cache
// .select_font(text_style.font_family, &properties)
// .unwrap_or(text_style.font_id);
// let mut result = RunStyle {
// color: fg,
// font_id,
// underline,
// };
// if let Some((style, range)) = hyperlink {
// if range.contains(&indexed.point) {
// if let Some(underline) = style.underline {
// result.underline = underline;
// }
// if let Some(color) = style.color {
// result.color = color;
// }
// }
// }
// result
// }
// fn generic_button_handler<E>(
// connection: WeakModel<Terminal>,
// origin: Point<Pixels>,
// f: impl Fn(&mut Terminal, Point<Pixels>, E, &mut ModelContext<Terminal>),
// ) -> impl Fn(E, &mut TerminalView, &mut EventContext<TerminalView>) {
// move |event, _: &mut TerminalView, cx| {
// cx.focus_parent();
// if let Some(conn_handle) = connection.upgrade() {
// conn_handle.update(cx, |terminal, cx| {
// f(terminal, origin, event, cx);
// cx.notify();
// })
// }
// }
// }
// fn attach_mouse_handlers(
// &self,
// origin: Point<Pixels>,
// visible_bounds: Bounds<Pixels>,
// mode: TermMode,
// cx: &mut ViewContext<TerminalView>,
// ) {
// let connection = self.terminal;
// let mut region = MouseRegion::new::<Self>(cx.view_id(), 0, visible_bounds);
// // Terminal Emulator controlled behavior:
// region = region
// // Start selections
// .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| {
// let terminal_view = cx.handle();
// cx.focus(&terminal_view);
// v.context_menu.update(cx, |menu, _cx| menu.delay_cancel());
// if let Some(conn_handle) = connection.upgrade() {
// conn_handle.update(cx, |terminal, cx| {
// terminal.mouse_down(&event, origin);
// cx.notify();
// })
// }
// })
// // Update drag selections
// .on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| {
// if event.end {
// return;
// }
// if cx.is_self_focused() {
// if let Some(conn_handle) = connection.upgrade() {
// conn_handle.update(cx, |terminal, cx| {
// terminal.mouse_drag(event, origin);
// cx.notify();
// })
// }
// }
// })
// // Copy on up behavior
// .on_up(
// MouseButton::Left,
// TerminalElement::generic_button_handler(
// connection,
// origin,
// move |terminal, origin, e, cx| {
// terminal.mouse_up(&e, origin, cx);
// },
// ),
// )
// // Context menu
// .on_click(
// MouseButton::Right,
// move |event, view: &mut TerminalView, cx| {
// let mouse_mode = if let Some(conn_handle) = connection.upgrade() {
// conn_handle.update(cx, |terminal, _cx| terminal.mouse_mode(event.shift))
// } else {
// // If we can't get the model handle, probably can't deploy the context menu
// true
// };
// if !mouse_mode {
// view.deploy_context_menu(event.position, cx);
// }
// },
// )
// .on_move(move |event, _: &mut TerminalView, cx| {
// if cx.is_self_focused() {
// if let Some(conn_handle) = connection.upgrade() {
// conn_handle.update(cx, |terminal, cx| {
// terminal.mouse_move(&event, origin);
// cx.notify();
// })
// }
// }
// })
// .on_scroll(move |event, _: &mut TerminalView, cx| {
// if let Some(conn_handle) = connection.upgrade() {
// conn_handle.update(cx, |terminal, cx| {
// terminal.scroll_wheel(event, origin);
// cx.notify();
// })
// }
// });
// // Mouse mode handlers:
// // All mouse modes need the extra click handlers
// if mode.intersects(TermMode::MOUSE_MODE) {
// region = region
// .on_down(
// MouseButton::Right,
// TerminalElement::generic_button_handler(
// connection,
// origin,
// move |terminal, origin, e, _cx| {
// terminal.mouse_down(&e, origin);
// },
// ),
// )
// .on_down(
// MouseButton::Middle,
// TerminalElement::generic_button_handler(
// connection,
// origin,
// move |terminal, origin, e, _cx| {
// terminal.mouse_down(&e, origin);
// },
// ),
// )
// .on_up(
// MouseButton::Right,
// TerminalElement::generic_button_handler(
// connection,
// origin,
// move |terminal, origin, e, cx| {
// terminal.mouse_up(&e, origin, cx);
// },
// ),
// )
// .on_up(
// MouseButton::Middle,
// TerminalElement::generic_button_handler(
// connection,
// origin,
// move |terminal, origin, e, cx| {
// terminal.mouse_up(&e, origin, cx);
// },
// ),
// )
// }
// cx.scene().push_mouse_region(region);
// }
// }
// impl Element<TerminalView> for TerminalElement {
// type ElementState = LayoutState;
// fn layout(
// &mut self,
// view_state: &mut TerminalView,
// element_state: Option<Self::ElementState>,
// cx: &mut ViewContext<TerminalView>,
// ) -> (LayoutId, Self::ElementState) {
// let settings = ThemeSettings::get_global(cx);
// let terminal_settings = TerminalSettings::get_global(cx);
// //Setup layout information
// let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
// let link_style = settings.theme.editor.link_definition;
// let tooltip_style = settings.theme.tooltip.clone();
// let font_cache = cx.font_cache();
// let font_size = font_size(&terminal_settings, cx).unwrap_or(settings.buffer_font_size(cx));
// let font_family_name = terminal_settings
// .font_family
// .as_ref()
// .unwrap_or(&settings.buffer_font_family_name);
// let font_features = terminal_settings
// .font_features
// .as_ref()
// .unwrap_or(&settings.buffer_font_features);
// let family_id = font_cache
// .load_family(&[font_family_name], &font_features)
// .log_err()
// .unwrap_or(settings.buffer_font_family);
// let font_id = font_cache
// .select_font(family_id, &Default::default())
// .unwrap();
// let text_style = TextStyle {
// color: settings.theme.editor.text_color,
// font_family_id: family_id,
// font_family_name: font_cache.family_name(family_id).unwrap(),
// font_id,
// font_size,
// font_properties: Default::default(),
// underline: Default::default(),
// soft_wrap: false,
// };
// let selection_color = settings.theme.editor.selection.selection;
// let match_color = settings.theme.search.match_background;
// let gutter;
// let dimensions = {
// let line_height = text_style.font_size * terminal_settings.line_height.value();
// let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
// gutter = cell_width;
// let size = constraint.max - point(gutter, 0.);
// TerminalSize::new(line_height, cell_width, size)
// };
// let search_matches = if let Some(terminal_model) = self.terminal.upgrade() {
// terminal_model.read(cx).matches.clone()
// } else {
// Default::default()
// };
// let background_color = terminal_theme.background;
// let terminal_handle = self.terminal.upgrade().unwrap();
// let last_hovered_word = terminal_handle.update(cx, |terminal, cx| {
// terminal.set_size(dimensions);
// terminal.try_sync(cx);
// if self.can_navigate_to_selected_word && terminal.can_navigate_to_selected_word() {
// terminal.last_content.last_hovered_word.clone()
// } else {
// None
// }
// });
// let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| {
// let mut tooltip = Overlay::new(
// Empty::new()
// .contained()
// .constrained()
// .with_width(dimensions.width())
// .with_height(dimensions.height())
// .with_tooltip::<TerminalElement>(
// hovered_word.id,
// hovered_word.word,
// None,
// tooltip_style,
// cx,
// ),
// )
// .with_position_mode(gpui::OverlayPositionMode::Local)
// .into_any();
// tooltip.layout(
// SizeConstraint::new(Point::zero(), cx.window_size()),
// view_state,
// cx,
// );
// tooltip
// });
// let TerminalContent {
// cells,
// mode,
// display_offset,
// cursor_char,
// selection,
// cursor,
// ..
// } = { &terminal_handle.read(cx).last_content };
// // searches, highlights to a single range representations
// let mut relative_highlighted_ranges = Vec::new();
// for search_match in search_matches {
// relative_highlighted_ranges.push((search_match, match_color))
// }
// if let Some(selection) = selection {
// relative_highlighted_ranges.push((selection.start..=selection.end, selection_color));
// }
// // then have that representation be converted to the appropriate highlight data structure
// let (cells, rects) = TerminalElement::layout_grid(
// cells,
// &text_style,
// &terminal_theme,
// cx.text_layout_cache(),
// cx.font_cache(),
// last_hovered_word
// .as_ref()
// .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
// );
// //Layout cursor. Rectangle is used for IME, so we should lay it out even
// //if we don't end up showing it.
// let cursor = if let AlacCursorShape::Hidden = cursor.shape {
// None
// } else {
// let cursor_point = DisplayCursor::from(cursor.point, *display_offset);
// let cursor_text = {
// let str_trxt = cursor_char.to_string();
// let color = if self.focused {
// terminal_theme.background
// } else {
// terminal_theme.foreground
// };
// cx.text_layout_cache().layout_str(
// &str_trxt,
// text_style.font_size,
// &[(
// str_trxt.len(),
// RunStyle {
// font_id: text_style.font_id,
// color,
// underline: Default::default(),
// },
// )],
// )
// };
// let focused = self.focused;
// TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
// move |(cursor_position, block_width)| {
// let (shape, text) = match cursor.shape {
// AlacCursorShape::Block if !focused => (CursorShape::Hollow, None),
// AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)),
// AlacCursorShape::Underline => (CursorShape::Underscore, None),
// AlacCursorShape::Beam => (CursorShape::Bar, None),
// AlacCursorShape::HollowBlock => (CursorShape::Hollow, None),
// //This case is handled in the if wrapping the whole cursor layout
// AlacCursorShape::Hidden => unreachable!(),
// };
// Cursor::new(
// cursor_position,
// block_width,
// dimensions.line_height,
// terminal_theme.cursor,
// shape,
// text,
// )
// },
// )
// };
// //Done!
// (
// constraint.max,
// Self::ElementState {
// cells,
// cursor,
// background_color,
// size: dimensions,
// rects,
// relative_highlighted_ranges,
// mode: *mode,
// display_offset: *display_offset,
// hyperlink_tooltip,
// gutter,
// },
// )
// }
// fn paint(
// &mut self,
// bounds: Bounds<Pixels>,
// view_state: &mut TerminalView,
// element_state: &mut Self::ElementState,
// cx: &mut ViewContext<TerminalView>,
// ) {
// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
// //Setup element stuff
// let clip_bounds = Some(visible_bounds);
// cx.paint_layer(clip_bounds, |cx| {
// let origin = bounds.origin + point(element_state.gutter, 0.);
// // Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
// self.attach_mouse_handlers(origin, visible_bounds, element_state.mode, cx);
// cx.scene().push_cursor_region(gpui::CursorRegion {
// bounds,
// style: if element_state.hyperlink_tooltip.is_some() {
// CursorStyle::AlacPointingHand
// } else {
// CursorStyle::IBeam
// },
// });
// cx.paint_layer(clip_bounds, |cx| {
// //Start with a background color
// cx.scene().push_quad(Quad {
// bounds,
// background: Some(element_state.background_color),
// border: Default::default(),
// corner_radii: Default::default(),
// });
// for rect in &element_state.rects {
// rect.paint(origin, element_state, view_state, cx);
// }
// });
// //Draw Highlighted Backgrounds
// cx.paint_layer(clip_bounds, |cx| {
// for (relative_highlighted_range, color) in
// element_state.relative_highlighted_ranges.iter()
// {
// if let Some((start_y, highlighted_range_lines)) = to_highlighted_range_lines(
// relative_highlighted_range,
// element_state,
// origin,
// ) {
// let hr = HighlightedRange {
// start_y, //Need to change this
// line_height: element_state.size.line_height,
// lines: highlighted_range_lines,
// color: color.clone(),
// //Copied from editor. TODO: move to theme or something
// corner_radius: 0.15 * element_state.size.line_height,
// };
// hr.paint(bounds, cx);
// }
// }
// });
// //Draw the text cells
// cx.paint_layer(clip_bounds, |cx| {
// for cell in &element_state.cells {
// cell.paint(origin, element_state, visible_bounds, view_state, cx);
// }
// });
// //Draw cursor
// if self.cursor_visible {
// if let Some(cursor) = &element_state.cursor {
// cx.paint_layer(clip_bounds, |cx| {
// cursor.paint(origin, cx);
// })
// }
// }
// if let Some(element) = &mut element_state.hyperlink_tooltip {
// element.paint(origin, visible_bounds, view_state, cx)
// }
// });
// }
// fn element_id(&self) -> Option<ElementId> {
// todo!()
// }
// // todo!() remove?
// // fn metadata(&self) -> Option<&dyn std::any::Any> {
// // None
// // }
// // fn debug(
// // &self,
// // _: Bounds<Pixels>,
// // _: &Self::ElementState,
// // _: &Self::PaintState,
// // _: &TerminalView,
// // _: &gpui::ViewContext<TerminalView>,
// // ) -> gpui::serde_json::Value {
// // json!({
// // "type": "TerminalElement",
// // })
// // }
// // fn rect_for_text_range(
// // &self,
// // _: Range<usize>,
// // bounds: Bounds<Pixels>,
// // _: Bounds<Pixels>,
// // layout: &Self::ElementState,
// // _: &Self::PaintState,
// // _: &TerminalView,
// // _: &gpui::ViewContext<TerminalView>,
// // ) -> Option<Bounds<Pixels>> {
// // // Use the same origin that's passed to `Cursor::paint` in the paint
// // // method bove.
// // let mut origin = bounds.origin() + point(layout.size.cell_width, 0.);
// // // TODO - Why is it necessary to move downward one line to get correct
// // // positioning? I would think that we'd want the same rect that is
// // // painted for the cursor.
// // origin += point(0., layout.size.line_height);
// // Some(layout.cursor.as_ref()?.bounding_rect(origin))
// // }
// }
// impl Component<TerminalView> for TerminalElement {
// fn render(self) -> AnyElement<TerminalView> {
// todo!()
// }
// }
// fn is_blank(cell: &IndexedCell) -> bool {
// if cell.c != ' ' {
// return false;
// }
// if cell.bg != AnsiColor::Named(NamedColor::Background) {
// return false;
// }
// if cell.hyperlink().is_some() {
// return false;
// }
// if cell
// .flags
// .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT)
// {
// return false;
// }
// return true;
// }
// fn to_highlighted_range_lines(
// range: &RangeInclusive<AlacPoint>,
// layout: &LayoutState,
// origin: Point<Pixels>,
// ) -> Option<(Pixels, Vec<HighlightedRangeLine>)> {
// // Step 1. Normalize the points to be viewport relative.
// // When display_offset = 1, here's how the grid is arranged:
// //-2,0 -2,1...
// //--- Viewport top
// //-1,0 -1,1...
// //--------- Terminal Top
// // 0,0 0,1...
// // 1,0 1,1...
// //--- Viewport Bottom
// // 2,0 2,1...
// //--------- Terminal Bottom
// // Normalize to viewport relative, from terminal relative.
// // lines are i32s, which are negative above the top left corner of the terminal
// // If the user has scrolled, we use the display_offset to tell us which offset
// // of the grid data we should be looking at. But for the rendering step, we don't
// // want negatives. We want things relative to the 'viewport' (the area of the grid
// // which is currently shown according to the display offset)
// let unclamped_start = AlacPoint::new(
// range.start().line + layout.display_offset,
// range.start().column,
// );
// let unclamped_end =
// AlacPoint::new(range.end().line + layout.display_offset, range.end().column);
// // Step 2. Clamp range to viewport, and return None if it doesn't overlap
// if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.size.num_lines() as i32 {
// return None;
// }
// let clamped_start_line = unclamped_start.line.0.max(0) as usize;
// let clamped_end_line = unclamped_end.line.0.min(layout.size.num_lines() as i32) as usize;
// //Convert the start of the range to pixels
// let start_y = origin.y + clamped_start_line as f32 * layout.size.line_height;
// // Step 3. Expand ranges that cross lines into a collection of single-line ranges.
// // (also convert to pixels)
// let mut highlighted_range_lines = Vec::new();
// for line in clamped_start_line..=clamped_end_line {
// let mut line_start = 0;
// let mut line_end = layout.size.columns();
// if line == clamped_start_line {
// line_start = unclamped_start.column.0 as usize;
// }
// if line == clamped_end_line {
// line_end = unclamped_end.column.0 as usize + 1; //+1 for inclusive
// }
// highlighted_range_lines.push(HighlightedRangeLine {
// start_x: origin.x + line_start as f32 * layout.size.cell_width,
// end_x: origin.x + line_end as f32 * layout.size.cell_width,
// });
// }
// Some((start_y, highlighted_range_lines))
// }
// fn font_size(terminal_settings: &TerminalSettings, cx: &mut AppContext) -> Option<Pixels> {
// terminal_settings
// .font_size
// .map(|size| theme::adjusted_font_size(size, cx))
// }

View File

@ -0,0 +1,446 @@
use std::{path::PathBuf, sync::Arc};
use crate::TerminalView;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
actions, div, serde_json, AppContext, AsyncWindowContext, Div, Entity, EventEmitter,
FocusHandle, FocusableView, ParentComponent, Render, Subscription, Task, View, ViewContext,
VisualContext, WeakView, WindowContext,
};
use project::Fs;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings};
use util::{ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
item::Item,
pane,
ui::Icon,
Pane, Workspace,
};
use anyhow::Result;
const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
actions!(ToggleFocus);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
workspace.register_action(TerminalPanel::new_terminal);
workspace.register_action(TerminalPanel::open_terminal);
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<TerminalPanel>(cx);
});
},
)
.detach();
}
pub struct TerminalPanel {
pane: View<Pane>,
fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>,
width: Option<f32>,
height: Option<f32>,
pending_serialization: Task<Option<()>>,
_subscriptions: Vec<Subscription>,
}
impl TerminalPanel {
fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
let _weak_self = cx.view().downgrade();
let pane = cx.build_view(|cx| {
let _window = cx.window_handle();
let mut pane = Pane::new(
workspace.weak_handle(),
workspace.project().clone(),
Default::default(),
cx,
);
pane.set_can_split(false, cx);
pane.set_can_navigate(false, cx);
// todo!()
// pane.on_can_drop(move |drag_and_drop, cx| {
// drag_and_drop
// .currently_dragged::<DraggedItem>(window)
// .map_or(false, |(_, item)| {
// item.handle.act_as::<TerminalView>(cx).is_some()
// })
// });
// pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
// let this = weak_self.clone();
// Flex::row()
// .with_child(Pane::render_tab_bar_button(
// 0,
// "icons/plus.svg",
// false,
// Some(("New Terminal", Some(Box::new(workspace::NewTerminal)))),
// cx,
// move |_, cx| {
// let this = this.clone();
// cx.window_context().defer(move |cx| {
// if let Some(this) = this.upgrade() {
// this.update(cx, |this, cx| {
// this.add_terminal(None, cx);
// });
// }
// })
// },
// |_, _| {},
// None,
// ))
// .with_child(Pane::render_tab_bar_button(
// 1,
// if pane.is_zoomed() {
// "icons/minimize.svg"
// } else {
// "icons/maximize.svg"
// },
// pane.is_zoomed(),
// Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
// cx,
// move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
// |_, _| {},
// None,
// ))
// .into_any()
// });
// let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
// pane.toolbar()
// .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
pane
});
let subscriptions = vec![
cx.observe(&pane, |_, _, cx| cx.notify()),
cx.subscribe(&pane, Self::handle_pane_event),
];
let this = Self {
pane,
fs: workspace.app_state().fs.clone(),
workspace: workspace.weak_handle(),
pending_serialization: Task::ready(None),
width: None,
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
}
pub async fn load(
workspace: WeakView<Workspace>,
mut cx: AsyncWindowContext,
) -> Result<View<Self>> {
let serialized_panel = cx
.background_executor()
.spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
.await
.log_err()
.flatten()
.map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
.transpose()
.log_err()
.flatten();
let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
let panel = cx.build_view(|cx| TerminalPanel::new(workspace, cx));
let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
panel.update(cx, |panel, cx| {
cx.notify();
panel.height = serialized_panel.height;
panel.width = serialized_panel.width;
panel.pane.update(cx, |_, cx| {
serialized_panel
.items
.iter()
.map(|item_id| {
TerminalView::deserialize(
workspace.project().clone(),
workspace.weak_handle(),
workspace.database_id(),
*item_id,
cx,
)
})
.collect::<Vec<_>>()
})
})
} else {
Default::default()
};
let pane = panel.read(cx).pane.clone();
(panel, pane, items)
})?;
let pane = pane.downgrade();
let items = futures::future::join_all(items).await;
pane.update(&mut cx, |pane, cx| {
let active_item_id = serialized_panel
.as_ref()
.and_then(|panel| panel.active_item_id);
let mut active_ix = None;
for item in items {
if let Some(item) = item.log_err() {
let item_id = item.entity_id().as_u64();
pane.add_item(Box::new(item), false, false, None, cx);
if Some(item_id) == active_item_id {
active_ix = Some(pane.items_len() - 1);
}
}
}
if let Some(active_ix) = active_ix {
pane.activate_item(active_ix, false, false, cx)
}
})?;
Ok(panel)
}
fn handle_pane_event(
&mut self,
_pane: View<Pane>,
event: &pane::Event,
cx: &mut ViewContext<Self>,
) {
match event {
pane::Event::ActivateItem { .. } => self.serialize(cx),
pane::Event::RemoveItem { .. } => self.serialize(cx),
pane::Event::Remove => cx.emit(PanelEvent::Close),
pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
pane::Event::Focus => cx.emit(PanelEvent::Focus),
pane::Event::AddItem { item } => {
if let Some(workspace) = self.workspace.upgrade() {
let pane = self.pane.clone();
workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx))
}
}
_ => {}
}
}
pub fn open_terminal(
workspace: &mut Workspace,
action: &workspace::OpenTerminal,
cx: &mut ViewContext<Workspace>,
) {
let Some(this) = workspace.focus_panel::<Self>(cx) else {
return;
};
this.update(cx, |this, cx| {
this.add_terminal(Some(action.working_directory.clone()), cx)
})
}
///Create a new Terminal in the current working directory or the user's home directory
fn new_terminal(
workspace: &mut Workspace,
_: &workspace::NewTerminal,
cx: &mut ViewContext<Workspace>,
) {
let Some(this) = workspace.focus_panel::<Self>(cx) else {
return;
};
this.update(cx, |this, cx| this.add_terminal(None, cx))
}
fn add_terminal(&mut self, working_directory: Option<PathBuf>, cx: &mut ViewContext<Self>) {
let workspace = self.workspace.clone();
cx.spawn(|this, mut cx| async move {
let pane = this.update(&mut cx, |this, _| this.pane.clone())?;
workspace.update(&mut cx, |workspace, cx| {
let working_directory = if let Some(working_directory) = working_directory {
Some(working_directory)
} else {
let working_directory_strategy =
TerminalSettings::get_global(cx).working_directory.clone();
crate::get_working_directory(workspace, cx, working_directory_strategy)
};
let window = cx.window_handle();
if let Some(terminal) = workspace.project().update(cx, |project, cx| {
project
.create_terminal(working_directory, window, cx)
.log_err()
}) {
let terminal = Box::new(cx.build_view(|cx| {
TerminalView::new(
terminal,
workspace.weak_handle(),
workspace.database_id(),
cx,
)
}));
pane.update(cx, |pane, cx| {
let focus = pane.has_focus(cx);
pane.add_item(terminal, true, focus, None, cx);
});
}
})?;
this.update(&mut cx, |this, cx| this.serialize(cx))?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
let items = self
.pane
.read(cx)
.items()
.map(|item| item.id().as_u64())
.collect::<Vec<_>>();
let active_item_id = self
.pane
.read(cx)
.active_item()
.map(|item| item.id().as_u64());
let height = self.height;
let width = self.width;
self.pending_serialization = cx.background_executor().spawn(
async move {
KEY_VALUE_STORE
.write_kvp(
TERMINAL_PANEL_KEY.into(),
serde_json::to_string(&SerializedTerminalPanel {
items,
active_item_id,
height,
width,
})?,
)
.await?;
anyhow::Ok(())
}
.log_err(),
);
}
}
impl EventEmitter<PanelEvent> for TerminalPanel {}
impl Render for TerminalPanel {
type Element = Div<Self>;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
div().child(self.pane.clone())
}
}
impl FocusableView for TerminalPanel {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.pane.focus_handle(cx)
}
}
impl Panel for TerminalPanel {
fn position(&self, cx: &WindowContext) -> DockPosition {
match TerminalSettings::get_global(cx).dock {
TerminalDockPosition::Left => DockPosition::Left,
TerminalDockPosition::Bottom => DockPosition::Bottom,
TerminalDockPosition::Right => DockPosition::Right,
}
}
fn position_is_valid(&self, _: DockPosition) -> bool {
true
}
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
settings::update_settings_file::<TerminalSettings>(self.fs.clone(), cx, move |settings| {
let dock = match position {
DockPosition::Left => TerminalDockPosition::Left,
DockPosition::Bottom => TerminalDockPosition::Bottom,
DockPosition::Right => TerminalDockPosition::Right,
};
settings.dock = Some(dock);
});
}
fn size(&self, cx: &WindowContext) -> f32 {
let settings = TerminalSettings::get_global(cx);
match self.position(cx) {
DockPosition::Left | DockPosition::Right => {
self.width.unwrap_or_else(|| settings.default_width)
}
DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
}
}
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
match self.position(cx) {
DockPosition::Left | DockPosition::Right => self.width = size,
DockPosition::Bottom => self.height = size,
}
self.serialize(cx);
cx.notify();
}
fn is_zoomed(&self, cx: &WindowContext) -> bool {
self.pane.read(cx).is_zoomed()
}
fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
}
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if active && self.pane.read(cx).items_len() == 0 {
self.add_terminal(None, cx)
}
}
fn icon_label(&self, cx: &WindowContext) -> Option<String> {
let count = self.pane.read(cx).items_len();
if count == 0 {
None
} else {
Some(count.to_string())
}
}
fn has_focus(&self, cx: &WindowContext) -> bool {
self.pane.read(cx).has_focus(cx)
}
fn persistent_name() -> &'static str {
"TerminalPanel"
}
// todo!()
// fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
// ("Terminal Panel".into(), Some(Box::new(ToggleFocus)))
// }
fn icon(&self, _cx: &WindowContext) -> Option<Icon> {
Some(Icon::Terminal)
}
fn toggle_action(&self) -> Box<dyn gpui::Action> {
Box::new(ToggleFocus)
}
}
#[derive(Serialize, Deserialize)]
struct SerializedTerminalPanel {
items: Vec<u64>,
active_item_id: Option<u64>,
width: Option<f32>,
height: Option<f32>,
}

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ anyhow.workspace = true
chrono = "0.4"
gpui = { package = "gpui2", path = "../gpui2" }
itertools = { version = "0.11.0", optional = true }
menu = { package = "menu2", path = "../menu2"}
serde.workspace = true
settings2 = { path = "../settings2" }
smallvec.workspace = true

View File

@ -61,7 +61,7 @@ impl ButtonVariant {
}
}
pub type ClickHandler<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>) + Send + Sync>;
pub type ClickHandler<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>)>;
struct ButtonHandlers<V: 'static> {
click: Option<ClickHandler<V>>,

View File

@ -3,17 +3,29 @@ use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHea
pub enum ContextMenuItem {
Header(SharedString),
Entry(Label),
Entry(Label, Box<dyn gpui::Action>),
Separator,
}
impl Clone for ContextMenuItem {
fn clone(&self) -> Self {
match self {
ContextMenuItem::Header(name) => ContextMenuItem::Header(name.clone()),
ContextMenuItem::Entry(label, action) => {
ContextMenuItem::Entry(label.clone(), action.boxed_clone())
}
ContextMenuItem::Separator => ContextMenuItem::Separator,
}
}
}
impl ContextMenuItem {
fn to_list_item<V: 'static>(self) -> ListItem {
match self {
ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
ContextMenuItem::Entry(label) => {
ListEntry::new(label).variant(ListItemVariant::Inset).into()
}
ContextMenuItem::Entry(label, action) => ListEntry::new(label)
.variant(ListItemVariant::Inset)
.on_click(action)
.into(),
ContextMenuItem::Separator => ListSeparator::new().into(),
}
}
@ -26,12 +38,12 @@ impl ContextMenuItem {
Self::Separator
}
pub fn entry(label: Label) -> Self {
Self::Entry(label)
pub fn entry(label: Label, action: impl Action) -> Self {
Self::Entry(label, Box::new(action))
}
}
#[derive(Component)]
#[derive(Component, Clone)]
pub struct ContextMenu {
items: Vec<ContextMenuItem>,
}
@ -42,7 +54,12 @@ impl ContextMenu {
items: items.into_iter().collect(),
}
}
// todo!()
// cx.add_action(ContextMenu::select_first);
// cx.add_action(ContextMenu::select_last);
// cx.add_action(ContextMenu::select_next);
// cx.add_action(ContextMenu::select_prev);
// cx.add_action(ContextMenu::confirm);
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
v_stack()
.flex()
@ -55,9 +72,11 @@ impl ContextMenu {
.map(ContextMenuItem::to_list_item::<V>)
.collect(),
))
.on_mouse_down_out(|_, _, cx| cx.dispatch_action(Box::new(menu::Cancel)))
}
}
use gpui::Action;
#[cfg(feature = "stories")]
pub use stories::*;
@ -65,7 +84,7 @@ pub use stories::*;
mod stories {
use super::*;
use crate::story::Story;
use gpui::{Div, Render};
use gpui::{action, Div, Render};
pub struct ContextMenuStory;
@ -73,14 +92,22 @@ mod stories {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
#[action]
struct PrintCurrentDate {}
Story::container(cx)
.child(Story::title_for::<_, ContextMenu>(cx))
.child(Story::label(cx, "Default"))
.child(ContextMenu::new([
ContextMenuItem::header("Section header"),
ContextMenuItem::Separator,
ContextMenuItem::entry(Label::new("Some entry")),
ContextMenuItem::entry(Label::new("Print current time"), PrintCurrentDate {}),
]))
.on_action(|_, _: &PrintCurrentDate, _| {
if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
println!("Current Unix time is {:?}", unix_time.as_secs());
}
})
}
}
}

View File

@ -25,6 +25,7 @@ pub enum Icon {
ChevronRight,
ChevronUp,
Close,
Collab,
Dash,
Exit,
ExclamationTriangle,
@ -85,6 +86,7 @@ impl Icon {
Icon::ChevronRight => "icons/chevron_right.svg",
Icon::ChevronUp => "icons/chevron_up.svg",
Icon::Close => "icons/x.svg",
Icon::Collab => "icons/user_group_16.svg",
Icon::Dash => "icons/dash.svg",
Icon::Exit => "icons/exit.svg",
Icon::ExclamationTriangle => "icons/warning.svg",

View File

@ -1,5 +1,5 @@
use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement, TextTooltip};
use gpui::{prelude::*, MouseButton, VisualContext};
use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement};
use gpui::{prelude::*, AnyView, MouseButton};
use std::sync::Arc;
struct IconButtonHandlers<V: 'static> {
@ -19,7 +19,7 @@ pub struct IconButton<V: 'static> {
color: TextColor,
variant: ButtonVariant,
state: InteractionState,
tooltip: Option<SharedString>,
tooltip: Option<Box<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>>,
handlers: IconButtonHandlers<V>,
}
@ -56,22 +56,23 @@ impl<V: 'static> IconButton<V> {
self
}
pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
self.tooltip = Some(tooltip.into());
pub fn tooltip(
mut self,
tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
) -> Self {
self.tooltip = Some(Box::new(tooltip));
self
}
pub fn on_click(
mut self,
handler: impl 'static + Fn(&mut V, &mut ViewContext<V>) + Send + Sync,
) -> Self {
pub fn on_click(mut self, handler: impl 'static + Fn(&mut V, &mut ViewContext<V>)) -> Self {
self.handlers.click = Some(Arc::new(handler));
self
}
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
fn render(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let icon_color = match (self.state, self.color) {
(InteractionState::Disabled, _) => TextColor::Disabled,
(InteractionState::Active, _) => TextColor::Error,
_ => self.color,
};
@ -99,15 +100,16 @@ impl<V: 'static> IconButton<V> {
.child(IconElement::new(self.icon).color(icon_color));
if let Some(click_handler) = self.handlers.click.clone() {
button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
cx.stop_propagation();
click_handler(state, cx);
});
button = button
.on_mouse_down(MouseButton::Left, move |state, event, cx| {
cx.stop_propagation();
click_handler(state, cx);
})
.cursor_pointer();
}
if let Some(tooltip) = self.tooltip.clone() {
button =
button.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(tooltip.clone())));
if let Some(tooltip) = self.tooltip.take() {
button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx))
}
button

View File

@ -60,7 +60,7 @@ pub enum LineHeightStyle {
UILabel,
}
#[derive(Component)]
#[derive(Clone, Component)]
pub struct Label {
label: SharedString,
size: LabelSize,

View File

@ -1,11 +1,10 @@
use gpui::div;
use gpui::{div, Action};
use crate::prelude::*;
use crate::settings::user_settings;
use crate::{
disclosure_control, h_stack, v_stack, Avatar, GraphicSlot, Icon, IconElement, IconSize, Label,
TextColor, Toggle,
disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
};
use crate::{prelude::*, GraphicSlot};
#[derive(Clone, Copy, Default, Debug, PartialEq)]
pub enum ListItemVariant {
@ -232,6 +231,7 @@ pub struct ListEntry {
size: ListEntrySize,
toggle: Toggle,
variant: ListItemVariant,
on_click: Option<Box<dyn Action>>,
}
impl ListEntry {
@ -245,9 +245,15 @@ impl ListEntry {
size: ListEntrySize::default(),
toggle: Toggle::NotToggleable,
variant: ListItemVariant::default(),
on_click: Default::default(),
}
}
pub fn on_click(mut self, action: impl Into<Box<dyn Action>>) -> Self {
self.on_click = Some(action.into());
self
}
pub fn variant(mut self, variant: ListItemVariant) -> Self {
self.variant = variant;
self
@ -303,9 +309,21 @@ impl ListEntry {
ListEntrySize::Small => div().h_6(),
ListEntrySize::Medium => div().h_7(),
};
div()
.relative()
.hover(|mut style| {
style.background = Some(cx.theme().colors().editor_background.into());
style
})
.on_mouse_down(gpui::MouseButton::Left, {
let action = self.on_click.map(|action| action.boxed_clone());
move |entry: &mut V, event, cx| {
if let Some(action) = action.as_ref() {
cx.dispatch_action(action.boxed_clone());
}
}
})
.group("")
.bg(cx.theme().colors().surface_background)
// TODO: Add focus state
@ -401,7 +419,7 @@ impl List {
v_stack()
.w_full()
.py_1()
.children(self.header)
.children(self.header.map(|header| header))
.child(list_content)
}
}

View File

@ -1,17 +1,53 @@
use gpui::{Div, Render};
use gpui::{overlay, Action, AnyView, Overlay, Render, VisualContext};
use settings2::Settings;
use theme2::{ActiveTheme, ThemeSettings};
use crate::prelude::*;
use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor};
pub struct TextTooltip {
pub struct Tooltip {
title: SharedString,
meta: Option<SharedString>,
key_binding: Option<KeyBinding>,
}
impl TextTooltip {
impl Tooltip {
pub fn text(title: impl Into<SharedString>, cx: &mut WindowContext) -> AnyView {
cx.build_view(|cx| Self {
title: title.into(),
meta: None,
key_binding: None,
})
.into()
}
pub fn for_action(
title: impl Into<SharedString>,
action: &dyn Action,
cx: &mut WindowContext,
) -> AnyView {
cx.build_view(|cx| Self {
title: title.into(),
meta: None,
key_binding: KeyBinding::for_action(action, cx),
})
.into()
}
pub fn with_meta(
title: impl Into<SharedString>,
action: Option<&dyn Action>,
meta: impl Into<SharedString>,
cx: &mut WindowContext,
) -> AnyView {
cx.build_view(|cx| Self {
title: title.into(),
meta: Some(meta.into()),
key_binding: action.and_then(|action| KeyBinding::for_action(action, cx)),
})
.into()
}
pub fn new(title: impl Into<SharedString>) -> Self {
Self {
title: title.into(),
@ -31,31 +67,36 @@ impl TextTooltip {
}
}
impl Render for TextTooltip {
type Element = Div<Self>;
impl Render for Tooltip {
type Element = Overlay<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
v_stack()
.elevation_2(cx)
.font(ui_font)
.text_ui_sm()
.text_color(cx.theme().colors().text)
.py_1()
.px_2()
.child(
h_stack()
.child(self.title.clone())
.when_some(self.key_binding.clone(), |this, key_binding| {
this.justify_between().child(key_binding)
overlay().child(
// padding to avoid mouse cursor
div().pl_2().pt_2p5().child(
v_stack()
.elevation_2(cx)
.font(ui_font)
.text_ui_sm()
.text_color(cx.theme().colors().text)
.py_1()
.px_2()
.child(
h_stack()
.child(self.title.clone())
.when_some(self.key_binding.clone(), |this, key_binding| {
this.justify_between().child(key_binding)
}),
)
.when_some(self.meta.clone(), |this, meta| {
this.child(
Label::new(meta)
.size(LabelSize::Small)
.color(TextColor::Muted),
)
}),
)
.when_some(self.meta.clone(), |this, meta| {
this.child(
Label::new(meta)
.size(LabelSize::Small)
.color(TextColor::Muted),
)
})
),
)
}
}

View File

@ -1,12 +1,13 @@
use crate::{status_bar::StatusItemView, Axis, Workspace};
use gpui::{
div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter,
FocusHandle, ParentComponent, Render, Styled, Subscription, View, ViewContext, WeakView,
WindowContext,
FocusHandle, FocusableView, ParentComponent, Render, Styled, Subscription, View, ViewContext,
WeakView, WindowContext,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::{h_stack, IconButton, InteractionState, Tooltip};
pub enum PanelEvent {
ChangePosition,
@ -17,15 +18,15 @@ pub enum PanelEvent {
Focus,
}
pub trait Panel: Render + EventEmitter<PanelEvent> {
fn persistent_name(&self) -> &'static str;
pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
fn persistent_name() -> &'static str;
fn position(&self, cx: &WindowContext) -> DockPosition;
fn position_is_valid(&self, position: DockPosition) -> bool;
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
fn size(&self, cx: &WindowContext) -> f32;
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>);
fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>);
fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
fn toggle_action(&self) -> Box<dyn Action>;
fn icon_label(&self, _: &WindowContext) -> Option<String> {
None
}
@ -35,12 +36,11 @@ pub trait Panel: Render + EventEmitter<PanelEvent> {
fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
fn has_focus(&self, cx: &WindowContext) -> bool;
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
}
pub trait PanelHandle: Send + Sync {
fn id(&self) -> EntityId;
fn persistent_name(&self, cx: &WindowContext) -> &'static str;
fn persistent_name(&self) -> &'static str;
fn position(&self, cx: &WindowContext) -> DockPosition;
fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
fn set_position(&self, position: DockPosition, cx: &mut WindowContext);
@ -49,11 +49,11 @@ pub trait PanelHandle: Send + Sync {
fn set_active(&self, active: bool, cx: &mut WindowContext);
fn size(&self, cx: &WindowContext) -> f32;
fn set_size(&self, size: Option<f32>, cx: &mut WindowContext);
fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>);
fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action>;
fn icon_label(&self, cx: &WindowContext) -> Option<String>;
fn has_focus(&self, cx: &WindowContext) -> bool;
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
fn to_any(&self) -> AnyView;
}
@ -65,8 +65,8 @@ where
self.entity_id()
}
fn persistent_name(&self, cx: &WindowContext) -> &'static str {
self.read(cx).persistent_name()
fn persistent_name(&self) -> &'static str {
T::persistent_name()
}
fn position(&self, cx: &WindowContext) -> DockPosition {
@ -101,12 +101,12 @@ where
self.update(cx, |this, cx| this.set_size(size, cx))
}
fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> {
self.read(cx).icon_path(cx)
fn icon(&self, cx: &WindowContext) -> Option<ui::Icon> {
self.read(cx).icon(cx)
}
fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>) {
self.read(cx).icon_tooltip()
fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action> {
self.read(cx).toggle_action()
}
fn icon_label(&self, cx: &WindowContext) -> Option<String> {
@ -121,7 +121,7 @@ where
self.clone().into()
}
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.read(cx).focus_handle(cx).clone()
}
}
@ -139,6 +139,14 @@ pub struct Dock {
active_panel_index: usize,
}
impl FocusableView for Dock {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.panel_entries[self.active_panel_index]
.panel
.focus_handle(cx)
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum DockPosition {
@ -214,18 +222,20 @@ impl Dock {
// .find_map(|entry| entry.panel.as_any().clone().downcast())
// }
// pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
// self.panel_entries
// .iter()
// .position(|entry| entry.panel.as_any().is::<T>())
// }
pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
self.panel_entries
.iter()
.position(|entry| entry.panel.to_any().downcast::<T>().is_ok())
}
pub fn panel_index_for_ui_name(&self, _ui_name: &str, _cx: &AppContext) -> Option<usize> {
todo!()
// self.panel_entries.iter().position(|entry| {
// let panel = entry.panel.as_any();
// cx.view_ui_name(panel.window(), panel.id()) == Some(ui_name)
// })
pub fn panel_index_for_persistent_name(
&self,
ui_name: &str,
_cx: &AppContext,
) -> Option<usize> {
self.panel_entries
.iter()
.position(|entry| entry.panel.persistent_name() == ui_name)
}
pub fn active_panel_index(&self) -> usize {
@ -644,11 +654,28 @@ impl Render for PanelButtons {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
// todo!()
let dock = self.dock.read(cx);
div().children(
dock.panel_entries
.iter()
.map(|panel| panel.panel.persistent_name(cx)),
)
let active_index = dock.active_panel_index;
let is_open = dock.is_open;
let buttons = dock
.panel_entries
.iter()
.enumerate()
.filter_map(|(i, panel)| {
let icon = panel.panel.icon(cx)?;
let name = panel.panel.persistent_name();
let action = panel.panel.toggle_action(cx);
let action2 = action.boxed_clone();
let mut button = IconButton::new(panel.panel.persistent_name(), icon)
.when(i == active_index, |el| el.state(InteractionState::Active))
.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
.tooltip(move |_, cx| Tooltip::for_action(name, &*action2, cx));
Some(button)
});
h_stack().children(buttons)
}
}
@ -665,7 +692,7 @@ impl StatusItemView for PanelButtons {
#[cfg(any(test, feature = "test-support"))]
pub mod test {
use super::*;
use gpui::{div, Div, ViewContext, WindowContext};
use gpui::{actions, div, Div, ViewContext, WindowContext};
pub struct TestPanel {
pub position: DockPosition,
@ -674,6 +701,7 @@ pub mod test {
pub has_focus: bool,
pub size: f32,
}
actions!(ToggleTestPanel);
impl EventEmitter<PanelEvent> for TestPanel {}
@ -698,7 +726,7 @@ pub mod test {
}
impl Panel for TestPanel {
fn persistent_name(&self) -> &'static str {
fn persistent_name() -> &'static str {
"TestPanel"
}
@ -723,12 +751,12 @@ pub mod test {
self.size = size.unwrap_or(300.);
}
fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
Some("icons/test_panel.svg")
fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
None
}
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
("Test Panel".into(), None)
fn toggle_action(&self) -> Box<dyn Action> {
ToggleTestPanel.boxed_clone()
}
fn is_zoomed(&self, _: &WindowContext) -> bool {
@ -746,8 +774,10 @@ pub mod test {
fn has_focus(&self, _cx: &WindowContext) -> bool {
self.has_focus
}
}
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
impl FocusableView for TestPanel {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
unimplemented!()
}
}

View File

@ -12,8 +12,9 @@ use client2::{
Client,
};
use gpui::{
AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, HighlightStyle,
Model, Pixels, Point, Render, SharedString, Task, View, ViewContext, WeakView, WindowContext,
AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
HighlightStyle, Model, Pixels, Point, SharedString, Task, View, ViewContext, WeakView,
WindowContext,
};
use project2::{Project, ProjectEntryId, ProjectPath};
use schemars::JsonSchema;
@ -91,8 +92,7 @@ pub struct BreadcrumbText {
pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
}
pub trait Item: Render + EventEmitter<ItemEvent> {
fn focus_handle(&self) -> FocusHandle;
pub trait Item: FocusableView + EventEmitter<ItemEvent> {
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
@ -286,7 +286,7 @@ impl dyn ItemHandle {
impl<T: Item> ItemHandle for View<T> {
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
self.read(cx).focus_handle()
self.focus_handle(cx)
}
fn subscribe_to_item_events(

View File

@ -72,7 +72,7 @@ impl ModalLayer {
cx.notify();
}
pub fn current_modal<V>(&self) -> Option<View<V>>
pub fn active_modal<V>(&self) -> Option<View<V>>
where
V: 'static,
{

View File

@ -8,8 +8,8 @@ use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
use gpui::{
actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId,
EventEmitter, FocusHandle, Model, PromptLevel, Render, Task, View, ViewContext, VisualContext,
WeakView, WindowContext,
EventEmitter, FocusHandle, Focusable, FocusableView, Model, PromptLevel, Render, Task, View,
ViewContext, VisualContext, WeakView, WindowContext,
};
use parking_lot::Mutex;
use project2::{Project, ProjectEntryId, ProjectPath};
@ -25,7 +25,7 @@ use std::{
},
};
use ui::v_stack;
use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, TextTooltip};
use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, Tooltip};
use util::truncate_and_remove_front;
#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
@ -125,10 +125,6 @@ pub fn init(cx: &mut AppContext) {
// cx.add_async_action(Pane::close_items_to_the_left);
// cx.add_async_action(Pane::close_items_to_the_right);
// cx.add_async_action(Pane::close_all_items);
// cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx));
// cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
// cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
// cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
}
pub enum Event {
@ -183,7 +179,7 @@ pub struct Pane {
workspace: WeakView<Workspace>,
project: Model<Project>,
// can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
// can_split: bool,
can_split: bool,
// render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
}
@ -351,7 +347,7 @@ impl Pane {
workspace,
project,
// can_drop: Rc::new(|_, _| true),
// can_split: true,
can_split: true,
// render_tab_bar_buttons: Rc::new(move |pane, cx| {
// Flex::row()
// // New menu
@ -431,17 +427,17 @@ impl Pane {
// self.can_drop = Rc::new(can_drop);
// }
// pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
// self.can_split = can_split;
// cx.notify();
// }
pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
self.can_split = can_split;
cx.notify();
}
// pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
// self.toolbar.update(cx, |toolbar, cx| {
// toolbar.set_can_navigate(can_navigate, cx);
// });
// cx.notify();
// }
pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
self.toolbar.update(cx, |toolbar, cx| {
toolbar.set_can_navigate(can_navigate, cx);
});
cx.notify();
}
// pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
// where
@ -1017,7 +1013,11 @@ impl Pane {
.unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
let should_activate = activate_pane || self.has_focus(cx);
self.activate_item(index_to_activate, should_activate, should_activate, cx);
if self.items.len() == 1 && should_activate {
self.focus_handle.focus(cx);
} else {
self.activate_item(index_to_activate, should_activate, should_activate, cx);
}
}
let item = self.items.remove(item_index);
@ -1191,9 +1191,9 @@ impl Pane {
}
}
// pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
// cx.emit(Event::Split(direction));
// }
pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
cx.emit(Event::Split(direction));
}
// fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
// self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
@ -1392,8 +1392,9 @@ impl Pane {
.id(item.id())
.cursor_pointer()
.when_some(item.tab_tooltip_text(cx), |div, text| {
div.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(text.clone())))
div.tooltip(move |_, cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
})
.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::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
// .on_drop(|_view, state: View<DraggedTab>, cx| {
@ -1426,32 +1427,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())),
)
}
@ -1908,16 +1899,23 @@ impl Pane {
}
}
// impl Entity for Pane {
// type Event = Event;
// }
impl FocusableView for Pane {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for Pane {
type Element = Div<Self>;
type Element = Focusable<Self, Div<Self>>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
v_stack()
.key_context("Pane")
.track_focus(&self.focus_handle)
.on_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx))
.on_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx))
.on_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx))
.on_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx))
.size_full()
.on_action(|pane: &mut Self, action, cx| {
pane.close_active_item(action, cx)

View File

@ -148,6 +148,10 @@ impl PaneGroup {
self.root.collect_panes(&mut panes);
panes
}
pub(crate) fn first_pane(&self) -> View<Pane> {
self.root.first_pane()
}
}
#[derive(Clone, PartialEq)]
@ -181,6 +185,13 @@ impl Member {
}
}
fn first_pane(&self) -> View<Pane> {
match self {
Member::Axis(axis) => axis.members[0].first_pane(),
Member::Pane(pane) => pane.clone(),
}
}
pub fn render(
&self,
project: &Model<Project>,
@ -551,7 +562,32 @@ impl PaneAxis {
) -> AnyElement<Workspace> {
debug_assert!(self.members.len() == self.flexes.lock().len());
todo!()
div()
.flex()
.flex_auto()
.map(|s| match self.axis {
Axis::Vertical => s.flex_col(),
Axis::Horizontal => s.flex_row(),
})
.children(self.members.iter().enumerate().map(|(ix, member)| {
match member {
Member::Axis(axis) => axis
.render(
project,
basis,
follower_states,
active_call,
active_pane,
zoomed,
app_state,
cx,
)
.render(),
Member::Pane(pane) => pane.clone().render(),
}
}))
.render()
// let mut pane_axis = PaneAxisElement::new(
// self.axis,
// basis,

View File

@ -277,7 +277,7 @@ impl SerializedPane {
pub type GroupId = i64;
pub type PaneId = i64;
pub type ItemId = usize;
pub type ItemId = u64;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SerializedItem {

View File

@ -6,6 +6,7 @@ use gpui::{
WindowContext,
};
use theme2::ActiveTheme;
use ui::h_stack;
use util::ResultExt;
pub trait StatusItemView: Render {
@ -53,57 +54,20 @@ impl Render for StatusBar {
impl StatusBar {
fn render_left_tools(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
div()
.flex()
h_stack()
.items_center()
.gap_1()
.children(self.left_items.iter().map(|item| item.to_any()))
}
fn render_right_tools(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
div()
.flex()
h_stack()
.items_center()
.gap_2()
.children(self.right_items.iter().map(|item| item.to_any()))
}
}
// todo!()
// impl View for StatusBar {
// fn ui_name() -> &'static str {
// "StatusBar"
// }
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// let theme = &theme::current(cx).workspace.status_bar;
// StatusBarElement {
// left: Flex::row()
// .with_children(self.left_items.iter().map(|i| {
// ChildView::new(i.as_any(), cx)
// .aligned()
// .contained()
// .with_margin_right(theme.item_spacing)
// }))
// .into_any(),
// right: Flex::row()
// .with_children(self.right_items.iter().rev().map(|i| {
// ChildView::new(i.as_any(), cx)
// .aligned()
// .contained()
// .with_margin_left(theme.item_spacing)
// }))
// .into_any(),
// }
// .contained()
// .with_style(theme.container)
// .constrained()
// .with_height(theme.height)
// .into_any()
// }
// }
impl StatusBar {
pub fn new(active_pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Self {
let mut this = Self {
@ -223,80 +187,3 @@ impl From<&dyn StatusItemViewHandle> for AnyView {
val.to_any().clone()
}
}
// todo!()
// struct StatusBarElement {
// left: AnyElement<StatusBar>,
// right: AnyElement<StatusBar>,
// }
// todo!()
// impl Element<StatusBar> for StatusBarElement {
// type LayoutState = ();
// type PaintState = ();
// fn layout(
// &mut self,
// mut constraint: SizeConstraint,
// view: &mut StatusBar,
// cx: &mut ViewContext<StatusBar>,
// ) -> (Vector2F, Self::LayoutState) {
// let max_width = constraint.max.x();
// constraint.min = vec2f(0., constraint.min.y());
// let right_size = self.right.layout(constraint, view, cx);
// let constraint = SizeConstraint::new(
// vec2f(0., constraint.min.y()),
// vec2f(max_width - right_size.x(), constraint.max.y()),
// );
// self.left.layout(constraint, view, cx);
// (vec2f(max_width, right_size.y()), ())
// }
// fn paint(
// &mut self,
// bounds: RectF,
// visible_bounds: RectF,
// _: &mut Self::LayoutState,
// view: &mut StatusBar,
// cx: &mut ViewContext<StatusBar>,
// ) -> Self::PaintState {
// let origin_y = bounds.upper_right().y();
// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
// let left_origin = vec2f(bounds.lower_left().x(), origin_y);
// self.left.paint(left_origin, visible_bounds, view, cx);
// let right_origin = vec2f(bounds.upper_right().x() - self.right.size().x(), origin_y);
// self.right.paint(right_origin, visible_bounds, view, cx);
// }
// fn rect_for_text_range(
// &self,
// _: Range<usize>,
// _: RectF,
// _: RectF,
// _: &Self::LayoutState,
// _: &Self::PaintState,
// _: &StatusBar,
// _: &ViewContext<StatusBar>,
// ) -> Option<RectF> {
// None
// }
// fn debug(
// &self,
// bounds: RectF,
// _: &Self::LayoutState,
// _: &Self::PaintState,
// _: &StatusBar,
// _: &ViewContext<StatusBar>,
// ) -> serde_json::Value {
// json!({
// "type": "StatusBarElement",
// "bounds": bounds.to_json()
// })
// }
// }

View File

@ -15,13 +15,6 @@ mod status_bar;
mod toolbar;
mod workspace_settings;
pub use crate::persistence::{
model::{
DockData, DockStructure, ItemId, SerializedItem, SerializedPane, SerializedPaneGroup,
SerializedWorkspace,
},
WorkspaceDb,
};
use anyhow::{anyhow, Context as _, Result};
use call2::ActiveCall;
use client2::{
@ -29,18 +22,18 @@ use client2::{
Client, TypedEnvelope, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle as _};
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
use futures::{
channel::{mpsc, oneshot},
future::try_join_all,
Future, FutureExt, StreamExt,
};
use gpui::{
actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView,
AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId,
EventEmitter, FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent,
Point, Render, Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds,
WindowContext, WindowHandle, WindowOptions,
actions, div, point, register_action, size, Action, AnyModel, AnyView, AnyWeakView, AppContext,
AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter,
FocusHandle, FocusableView, GlobalPixels, InteractiveComponent, KeyContext, Model,
ModelContext, ParentComponent, Point, Render, Size, Styled, Subscription, Task, View,
ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
use itertools::Itertools;
@ -51,7 +44,10 @@ use node_runtime::NodeRuntime;
use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
pub use pane::*;
pub use pane_group::*;
use persistence::{model::WorkspaceLocation, DB};
pub use persistence::{
model::{ItemId, SerializedWorkspace, WorkspaceLocation},
WorkspaceDb, DB,
};
use postage::stream::Stream;
use project2::{Project, ProjectEntryId, ProjectPath, Worktree};
use serde::Deserialize;
@ -68,12 +64,16 @@ use std::{
};
use theme2::{ActiveTheme, ThemeSettings};
pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
use ui::TextColor;
use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextTooltip};
pub use ui;
use util::ResultExt;
use uuid::Uuid;
pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
use crate::persistence::model::{
DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup,
};
lazy_static! {
static ref ZED_WINDOW_SIZE: Option<Size<GlobalPixels>> = env::var("ZED_WINDOW_SIZE")
.ok()
@ -195,10 +195,11 @@ impl Clone for Toast {
}
}
// #[derive(Clone, Deserialize, PartialEq)]
// pub struct OpenTerminal {
// pub working_directory: PathBuf,
// }
#[register_action]
#[derive(Debug, Default, Clone, Deserialize, PartialEq)]
pub struct OpenTerminal {
pub working_directory: PathBuf,
}
// impl_actions!(
// workspace,
@ -208,7 +209,6 @@ impl Clone for Toast {
// SwapPaneInDirection,
// NewFileInDirection,
// Toast,
// OpenTerminal,
// SaveAll,
// Save,
// CloseAllItemsAndPanes,
@ -322,12 +322,6 @@ pub struct AppState {
pub fs: Arc<dyn fs2::Fs>,
pub build_window_options:
fn(Option<WindowBounds>, Option<Uuid>, &mut AppContext) -> WindowOptions,
pub initialize_workspace: fn(
WeakView<Workspace>,
bool,
Arc<AppState>,
AsyncWindowContext,
) -> Task<anyhow::Result<()>>,
pub node_runtime: Arc<dyn NodeRuntime>,
}
@ -373,7 +367,6 @@ impl AppState {
user_store,
workspace_store,
node_runtime: FakeNodeRuntime::new(),
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
build_window_options: |_, _, _| Default::default(),
})
}
@ -433,7 +426,6 @@ pub enum Event {
pub struct Workspace {
weak_self: WeakView<Self>,
focus_handle: FocusHandle,
workspace_actions: Vec<Box<dyn Fn(Div<Workspace>) -> Div<Workspace>>>,
zoomed: Option<AnyWeakView>,
zoomed_position: Option<DockPosition>,
@ -448,7 +440,7 @@ pub struct Workspace {
last_active_view_id: Option<proto::ViewId>,
status_bar: View<StatusBar>,
modal_layer: View<ModalLayer>,
// titlebar_item: Option<AnyViewHandle>,
titlebar_item: Option<AnyView>,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: Model<Project>,
follower_states: HashMap<View<Pane>, FollowerState>,
@ -651,7 +643,6 @@ impl Workspace {
cx.defer(|this, cx| this.update_window_title(cx));
Workspace {
weak_self: weak_handle.clone(),
focus_handle: cx.focus_handle(),
zoomed: None,
zoomed_position: None,
center: PaneGroup::new(center_pane.clone()),
@ -662,7 +653,7 @@ impl Workspace {
last_active_view_id: None,
status_bar,
modal_layer,
// titlebar_item: None,
titlebar_item: None,
notifications: Default::default(),
left_dock,
bottom_dock,
@ -687,7 +678,7 @@ impl Workspace {
fn new_local(
abs_paths: Vec<PathBuf>,
app_state: Arc<AppState>,
_requesting_window: Option<WindowHandle<Workspace>>,
requesting_window: Option<WindowHandle<Workspace>>,
cx: &mut AppContext,
) -> Task<
anyhow::Result<(
@ -705,7 +696,8 @@ impl Workspace {
);
cx.spawn(|mut cx| async move {
let serialized_workspace: Option<SerializedWorkspace> = None; //persistence::DB.workspace_for_roots(&abs_paths.as_slice());
let serialized_workspace: Option<SerializedWorkspace> =
persistence::DB.workspace_for_roots(&abs_paths.as_slice());
let paths_to_open = Arc::new(abs_paths);
@ -734,15 +726,14 @@ impl Workspace {
DB.next_id().await.unwrap_or(0)
};
// todo!()
let window = /*if let Some(window) = requesting_window {
let window = if let Some(window) = requesting_window {
cx.update_window(window.into(), |old_workspace, cx| {
cx.replace_root_view(|cx| {
Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
});
});
})?;
window
} else */ {
} else {
let window_bounds_override = window_bounds_env_override(&cx);
let (bounds, display) = if let Some(bounds) = window_bounds_override {
(Some(bounds), None)
@ -756,12 +747,13 @@ impl Workspace {
// Stored bounds are relative to the containing display.
// So convert back to global coordinates if that screen still exists
if let WindowBounds::Fixed(mut window_bounds) = bounds {
let screen =
cx.update(|cx|
cx.displays()
.into_iter()
.find(|display| display.uuid().ok() == Some(serialized_display))
).ok()??;
let screen = cx
.update(|cx| {
cx.displays().into_iter().find(|display| {
display.uuid().ok() == Some(serialized_display)
})
})
.ok()??;
let screen_bounds = screen.bounds();
window_bounds.origin.x += screen_bounds.origin.x;
window_bounds.origin.y += screen_bounds.origin.y;
@ -790,17 +782,17 @@ impl Workspace {
};
// todo!() Ask how to do this
let weak_view = window.update(&mut cx, |_, cx| cx.view().downgrade())?;
let async_cx = window.update(&mut cx, |_, cx| cx.to_async())?;
// let weak_view = window.update(&mut cx, |_, cx| cx.view().downgrade())?;
// let async_cx = window.update(&mut cx, |_, cx| cx.to_async())?;
(app_state.initialize_workspace)(
weak_view,
serialized_workspace.is_some(),
app_state.clone(),
async_cx,
)
.await
.log_err();
// (app_state.initialize_workspace)(
// weak_view,
// serialized_workspace.is_some(),
// app_state.clone(),
// async_cx,
// )
// .await
// .log_err();
window
.update(&mut cx, |_, cx| cx.activate_window())
@ -809,12 +801,7 @@ impl Workspace {
notify_if_database_failed(window, &mut cx);
let opened_items = window
.update(&mut cx, |_workspace, cx| {
open_items(
serialized_workspace,
project_paths,
app_state,
cx,
)
open_items(serialized_workspace, project_paths, app_state, cx)
})?
.await
.unwrap_or_default();
@ -1035,15 +1022,14 @@ impl Workspace {
&self.app_state.client
}
// todo!()
// pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
// self.titlebar_item = Some(item);
// cx.notify();
// }
pub fn set_titlebar_item(&mut self, item: AnyView, cx: &mut ViewContext<Self>) {
self.titlebar_item = Some(item);
cx.notify();
}
// pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
// self.titlebar_item.clone()
// }
pub fn titlebar_item(&self) -> Option<AnyView> {
self.titlebar_item.clone()
}
/// Call the given callback with a workspace whose project is local.
///
@ -1450,6 +1436,11 @@ impl Workspace {
self.active_pane().read(cx).active_item()
}
pub fn active_item_as<I: 'static>(&self, cx: &AppContext) -> Option<View<I>> {
let item = self.active_item(cx)?;
item.to_any().downcast::<I>().ok()
}
fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
self.active_item(cx).and_then(|item| item.project_path(cx))
}
@ -1570,7 +1561,7 @@ impl Workspace {
}
if focus_center {
cx.focus(&self.focus_handle);
self.active_pane.update(cx, |pane, cx| pane.focus(cx))
}
cx.notify();
@ -1592,60 +1583,58 @@ impl Workspace {
self.serialize_workspace(cx);
}
// /// Transfer focus to the panel of the given type.
// pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
// self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?
// .as_any()
// .clone()
// .downcast()
// }
/// Transfer focus to the panel of the given type.
pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
let panel = self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?;
panel.to_any().downcast().ok()
}
// /// Focus the panel of the given type if it isn't already focused. If it is
// /// already focused, then transfer focus back to the workspace center.
// pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
// self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
// }
/// Focus the panel of the given type if it isn't already focused. If it is
/// already focused, then transfer focus back to the workspace center.
pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
}
// /// Focus or unfocus the given panel type, depending on the given callback.
// fn focus_or_unfocus_panel<T: Panel>(
// &mut self,
// cx: &mut ViewContext<Self>,
// should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
// ) -> Option<Rc<dyn PanelHandle>> {
// for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
// if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
// let mut focus_center = false;
// let mut reveal_dock = false;
// let panel = dock.update(cx, |dock, cx| {
// dock.activate_panel(panel_index, cx);
/// Focus or unfocus the given panel type, depending on the given callback.
fn focus_or_unfocus_panel<T: Panel>(
&mut self,
cx: &mut ViewContext<Self>,
should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
) -> Option<Arc<dyn PanelHandle>> {
for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
let mut focus_center = false;
let mut reveal_dock = false;
let panel = dock.update(cx, |dock, cx| {
dock.activate_panel(panel_index, cx);
// let panel = dock.active_panel().cloned();
// if let Some(panel) = panel.as_ref() {
// if should_focus(&**panel, cx) {
// dock.set_open(true, cx);
// cx.focus(panel.as_any());
// reveal_dock = true;
// } else {
// // if panel.is_zoomed(cx) {
// // dock.set_open(false, cx);
// // }
// focus_center = true;
// }
// }
// panel
// });
let panel = dock.active_panel().cloned();
if let Some(panel) = panel.as_ref() {
if should_focus(&**panel, cx) {
dock.set_open(true, cx);
panel.focus_handle(cx).focus(cx);
reveal_dock = true;
} else {
// if panel.is_zoomed(cx) {
// dock.set_open(false, cx);
// }
focus_center = true;
}
}
panel
});
// if focus_center {
// cx.focus_self();
// }
if focus_center {
self.active_pane.update(cx, |pane, cx| pane.focus(cx))
}
// self.serialize_workspace(cx);
// cx.notify();
// return panel;
// }
// }
// None
// }
self.serialize_workspace(cx);
cx.notify();
return panel;
}
}
None
}
// pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
// for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
@ -1704,7 +1693,7 @@ impl Workspace {
}
if focus_center {
cx.focus(&self.focus_handle);
self.active_pane.update(cx, |pane, cx| pane.focus(cx))
}
if self.zoomed_position != dock_to_reveal {
@ -2445,75 +2434,6 @@ impl Workspace {
// .any(|state| state.leader_id == peer_id)
// }
fn render_titlebar(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
h_stack()
.id("titlebar")
.justify_between()
.when(
!matches!(cx.window_bounds(), WindowBounds::Fullscreen),
|s| s.pl_20(),
)
.w_full()
.h(rems(1.75))
.bg(cx.theme().colors().title_bar_background)
.on_click(|_, event, cx| {
if event.up.click_count == 2 {
cx.zoom_window();
}
})
.child(
h_stack()
// TODO - Add player menu
.child(
div()
.id("project_owner_indicator")
.child(
Button::new("player")
.variant(ButtonVariant::Ghost)
.color(Some(TextColor::Player(0))),
)
.tooltip(move |_, cx| {
cx.build_view(|cx| TextTooltip::new("Toggle following"))
}),
)
// TODO - Add project menu
.child(
div()
.id("titlebar_project_menu_button")
.child(Button::new("project_name").variant(ButtonVariant::Ghost))
.tooltip(move |_, cx| {
cx.build_view(|cx| TextTooltip::new("Recent Projects"))
}),
)
// TODO - Add git menu
.child(
div()
.id("titlebar_git_menu_button")
.child(
Button::new("branch_name")
.variant(ButtonVariant::Ghost)
.color(Some(TextColor::Muted)),
)
.tooltip(move |_, cx| {
// todo!() Replace with real action.
#[gpui::action]
struct NoAction {}
cx.build_view(|cx| {
TextTooltip::new("Recent Branches")
.key_binding(KeyBinding::new(gpui::KeyBinding::new(
"cmd-b",
NoAction {},
None,
)))
.meta("Only local branches shown")
})
}),
),
) // self.titlebar_item
.child(h_stack().child(Label::new("Right side titlebar item")))
}
fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
let active_entry = self.active_project_path(cx);
self.project
@ -3040,13 +2960,15 @@ impl Workspace {
cx.notify();
}
// fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
// self._schedule_serialize = Some(cx.spawn(|this, cx| async move {
// cx.background().timer(Duration::from_millis(100)).await;
// this.read_with(&cx, |this, cx| this.serialize_workspace(cx))
// .ok();
// }));
// }
fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
cx.background_executor()
.timer(Duration::from_millis(100))
.await;
this.update(&mut cx, |this, cx| this.serialize_workspace(cx))
.log_err();
}));
}
fn serialize_workspace(&self, cx: &mut ViewContext<Self>) {
fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
@ -3058,7 +2980,7 @@ impl Workspace {
.filter_map(|item_handle| {
Some(SerializedItem {
kind: Arc::from(item_handle.serialized_item_kind()?),
item_id: item_handle.id().as_u64() as usize,
item_id: item_handle.id().as_u64(),
active: Some(item_handle.id()) == active_item_id,
})
})
@ -3102,7 +3024,7 @@ impl Workspace {
let left_visible = left_dock.is_open();
let left_active_panel = left_dock
.visible_panel()
.and_then(|panel| Some(panel.persistent_name(cx).to_string()));
.and_then(|panel| Some(panel.persistent_name().to_string()));
let left_dock_zoom = left_dock
.visible_panel()
.map(|panel| panel.is_zoomed(cx))
@ -3112,7 +3034,7 @@ impl Workspace {
let right_visible = right_dock.is_open();
let right_active_panel = right_dock
.visible_panel()
.and_then(|panel| Some(panel.persistent_name(cx).to_string()));
.and_then(|panel| Some(panel.persistent_name().to_string()));
let right_dock_zoom = right_dock
.visible_panel()
.map(|panel| panel.is_zoomed(cx))
@ -3122,7 +3044,7 @@ impl Workspace {
let bottom_visible = bottom_dock.is_open();
let bottom_active_panel = bottom_dock
.visible_panel()
.and_then(|panel| Some(panel.persistent_name(cx).to_string()));
.and_then(|panel| Some(panel.persistent_name().to_string()));
let bottom_dock_zoom = bottom_dock
.visible_panel()
.map(|panel| panel.is_zoomed(cx))
@ -3228,45 +3150,34 @@ impl Workspace {
// Swap workspace center group
workspace.center = PaneGroup::with_root(center_group);
// Change the focus to the workspace first so that we retrigger focus in on the pane.
// todo!()
// cx.focus_self();
// if let Some(active_pane) = active_pane {
// cx.focus(&active_pane);
// } else {
// cx.focus(workspace.panes.last().unwrap());
// }
} else {
// todo!()
// let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade());
// if let Some(old_center_handle) = old_center_handle {
// cx.focus(&old_center_handle)
// } else {
// cx.focus_self()
// }
workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
if let Some(active_pane) = active_pane {
workspace.active_pane = active_pane;
cx.focus_self();
} else {
workspace.active_pane = workspace.center.first_pane().clone();
}
}
let docks = serialized_workspace.docks;
workspace.left_dock.update(cx, |dock, cx| {
dock.set_open(docks.left.visible, cx);
if let Some(active_panel) = docks.left.active_panel {
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) {
dock.activate_panel(ix, cx);
}
}
dock.active_panel()
.map(|panel| panel.set_zoomed(docks.left.zoom, cx));
if docks.left.visible && docks.left.zoom {
// todo!()
// cx.focus_self()
cx.focus_self()
}
});
// TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
workspace.right_dock.update(cx, |dock, cx| {
dock.set_open(docks.right.visible, cx);
if let Some(active_panel) = docks.right.active_panel {
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) {
dock.activate_panel(ix, cx);
}
}
@ -3274,14 +3185,13 @@ impl Workspace {
.map(|panel| panel.set_zoomed(docks.right.zoom, cx));
if docks.right.visible && docks.right.zoom {
// todo!()
// cx.focus_self()
cx.focus_self()
}
});
workspace.bottom_dock.update(cx, |dock, cx| {
dock.set_open(docks.bottom.visible, cx);
if let Some(active_panel) = docks.bottom.active_panel {
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) {
dock.activate_panel(ix, cx);
}
}
@ -3290,8 +3200,7 @@ impl Workspace {
.map(|panel| panel.set_zoomed(docks.bottom.zoom, cx));
if docks.bottom.visible && docks.bottom.zoom {
// todo!()
// cx.focus_self()
cx.focus_self()
}
});
@ -3353,7 +3262,6 @@ impl Workspace {
// },
// );
.on_action(|this, e: &ToggleLeftDock, cx| {
println!("TOGGLING DOCK");
this.toggle_dock(DockPosition::Left, cx);
})
// cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
@ -3417,7 +3325,6 @@ impl Workspace {
user_store,
fs: project.read(cx).fs().clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
node_runtime: FakeNodeRuntime::new(),
});
let workspace = Self::new(0, project, app_state, cx);
@ -3475,8 +3382,8 @@ impl Workspace {
div
}
pub fn current_modal<V: Modal + 'static>(&mut self, cx: &ViewContext<Self>) -> Option<View<V>> {
self.modal_layer.read(cx).current_modal()
pub fn active_modal<V: Modal + 'static>(&mut self, cx: &ViewContext<Self>) -> Option<View<V>> {
self.modal_layer.read(cx).active_modal()
}
pub fn toggle_modal<V: Modal, B>(&mut self, cx: &mut ViewContext<Self>, build: B)
@ -3696,6 +3603,12 @@ fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncA
impl EventEmitter<Event> for Workspace {}
impl FocusableView for Workspace {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.active_pane.focus_handle(cx)
}
}
impl Render for Workspace {
type Element = Div<Self>;
@ -3716,7 +3629,7 @@ impl Render for Workspace {
.items_start()
.text_color(cx.theme().colors().text)
.bg(cx.theme().colors().background)
.child(self.render_titlebar(cx))
.children(self.titlebar_item.clone())
.child(
// todo! should this be a component a view?
div()

View File

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.113.0"
version = "0.114.0"
publish = false
[lib]

View File

@ -23,11 +23,10 @@ ai = { package = "ai2", path = "../ai2"}
call = { package = "call2", path = "../call2" }
# channel = { path = "../channel" }
cli = { path = "../cli" }
# collab_ui = { path = "../collab_ui" }
collab_ui = { package = "collab_ui2", path = "../collab_ui2" }
collections = { path = "../collections" }
command_palette = { package="command_palette2", path = "../command_palette2" }
# component_test = { path = "../component_test" }
# context_menu = { path = "../context_menu" }
client = { package = "client2", path = "../client2" }
# clock = { path = "../clock" }
copilot = { package = "copilot2", path = "../copilot2" }
@ -66,7 +65,7 @@ feature_flags = { package = "feature_flags2", path = "../feature_flags2" }
sum_tree = { path = "../sum_tree" }
shellexpand = "2.1.0"
text = { package = "text2", path = "../text2" }
# terminal_view = { path = "../terminal_view" }
terminal_view = { package = "terminal_view2", path = "../terminal_view2" }
theme = { package = "theme2", path = "../theme2" }
# theme_selector = { path = "../theme_selector" }
util = { path = "../util" }

View File

@ -50,8 +50,8 @@ use util::{
use uuid::Uuid;
use workspace::{AppState, WorkspaceStore};
use zed2::{
build_window_options, ensure_only_instance, handle_cli_connection, init_zed_actions,
initialize_workspace, languages, Assets, IsOnlyInstance, OpenListener, OpenRequest,
build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace,
languages, Assets, IsOnlyInstance, OpenListener, OpenRequest,
};
mod open_listener;
@ -141,7 +141,6 @@ fn main() {
cx.set_global(client.clone());
theme::init(cx);
// context_menu::init(cx);
project::Project::init(&client, cx);
client::init(&client, cx);
command_palette::init(cx);
@ -176,7 +175,6 @@ fn main() {
user_store,
fs,
build_window_options,
initialize_workspace,
// background_actions: todo!("ask Mikayla"),
workspace_store,
node_runtime,
@ -199,7 +197,7 @@ fn main() {
search::init(cx);
// semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
// vim::init(cx);
// terminal_view::init(cx);
terminal_view::init(cx);
// journal2::init(app_state.clone(), cx);
// language_selector::init(cx);
@ -207,13 +205,13 @@ fn main() {
// activity_indicator::init(cx);
// language_tools::init(cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
// collab_ui::init(&app_state, cx);
collab_ui::init(&app_state, cx);
// feedback::init(cx);
// welcome::init(cx);
// zed::init(&app_state, cx);
// cx.set_menus(menus::menus());
init_zed_actions(app_state.clone(), cx);
initialize_workspace(app_state.clone(), cx);
if stdout_is_a_pty() {
cx.activate(true);

View File

@ -10,16 +10,17 @@ pub use assets::*;
use collections::VecDeque;
use editor::{Editor, MultiBuffer};
use gpui::{
actions, point, px, AppContext, AsyncWindowContext, Context, PromptLevel, Task,
TitlebarOptions, ViewContext, VisualContext, WeakView, WindowBounds, WindowKind, WindowOptions,
actions, point, px, AppContext, Context, PromptLevel, TitlebarOptions, ViewContext,
VisualContext, WindowBounds, WindowKind, WindowOptions,
};
pub use only_instance::*;
pub use open_listener::*;
use anyhow::{anyhow, Context as _, Result};
use anyhow::{anyhow, Context as _};
use project_panel::ProjectPanel;
use settings::{initial_local_settings_content, Settings};
use std::{borrow::Cow, ops::Deref, sync::Arc};
use terminal_view::terminal_panel::TerminalPanel;
use util::{
asset_str,
channel::ReleaseChannel,
@ -86,8 +87,147 @@ pub fn build_window_options(
}
}
pub fn init_zed_actions(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.observe_new_views(move |workspace: &mut Workspace, _cx| {
pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.observe_new_views(move |workspace: &mut Workspace, cx| {
let workspace_handle = cx.view().clone();
cx.subscribe(&workspace_handle, {
move |workspace, _, event, cx| {
if let workspace::Event::PaneAdded(pane) = event {
pane.update(cx, |pane, cx| {
pane.toolbar().update(cx, |toolbar, cx| {
// todo!()
// let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
// toolbar.add_item(breadcrumbs, cx);
let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
toolbar.add_item(buffer_search_bar.clone(), cx);
// let quick_action_bar = cx.add_view(|_| {
// QuickActionBar::new(buffer_search_bar, workspace)
// });
// toolbar.add_item(quick_action_bar, cx);
// let diagnostic_editor_controls =
// cx.add_view(|_| diagnostics2::ToolbarControls::new());
// toolbar.add_item(diagnostic_editor_controls, cx);
// let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
// toolbar.add_item(project_search_bar, cx);
// let submit_feedback_button =
// cx.add_view(|_| SubmitFeedbackButton::new());
// toolbar.add_item(submit_feedback_button, cx);
// let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
// toolbar.add_item(feedback_info_text, cx);
// let lsp_log_item =
// cx.add_view(|_| language_tools::LspLogToolbarItemView::new());
// toolbar.add_item(lsp_log_item, cx);
// let syntax_tree_item = cx
// .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new());
// toolbar.add_item(syntax_tree_item, cx);
})
});
}
}
})
.detach();
// cx.emit(workspace2::Event::PaneAdded(
// workspace.active_pane().clone(),
// ));
// let collab_titlebar_item =
// cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
// workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
// let copilot =
// cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
// let diagnostic_summary =
// cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
// let activity_indicator = activity_indicator::ActivityIndicator::new(
// workspace,
// app_state.languages.clone(),
// cx,
// );
// let active_buffer_language =
// cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
// let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx));
// let feedback_button = cx.add_view(|_| {
// feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
// });
// let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
workspace.status_bar().update(cx, |status_bar, cx| {
// status_bar.add_left_item(diagnostic_summary, cx);
// status_bar.add_left_item(activity_indicator, cx);
// status_bar.add_right_item(feedback_button, cx);
// status_bar.add_right_item(copilot, cx);
// status_bar.add_right_item(active_buffer_language, cx);
// status_bar.add_right_item(vim_mode_indicator, cx);
// status_bar.add_right_item(cursor_position, cx);
});
// auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
// vim::observe_keystrokes(cx);
// cx.on_window_should_close(|workspace, cx| {
// if let Some(task) = workspace.close(&Default::default(), cx) {
// task.detach_and_log_err(cx);
// }
// false
// });
cx.spawn(|workspace_handle, mut cx| async move {
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
// let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
let channels_panel =
collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
// let chat_panel =
// collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
// let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
// workspace_handle.clone(),
// cx.clone(),
// );
let (
project_panel,
terminal_panel,
// assistant_panel,
channels_panel,
// chat_panel,
// notification_panel,
) = futures::try_join!(
project_panel,
terminal_panel,
// assistant_panel,
channels_panel,
// chat_panel,
// notification_panel,
)?;
workspace_handle.update(&mut cx, |workspace, cx| {
let project_panel_position = project_panel.position(cx);
workspace.add_panel(project_panel, cx);
workspace.add_panel(terminal_panel, cx);
// workspace.add_panel(assistant_panel, cx);
workspace.add_panel(channels_panel, cx);
// workspace.add_panel(chat_panel, cx);
// workspace.add_panel(notification_panel, cx);
// if !was_deserialized
// && workspace
// .project()
// .read(cx)
// .visible_worktrees(cx)
// .any(|tree| {
// tree.read(cx)
// .root_entry()
// .map_or(false, |entry| entry.is_dir())
// })
// {
// workspace.toggle_dock(project_panel_position, cx);
// }
// cx.focus_self();
})
})
.detach();
workspace
.register_action(about)
.register_action(|_, _: &Hide, cx| {
@ -291,154 +431,6 @@ pub fn init_zed_actions(app_state: Arc<AppState>, cx: &mut AppContext) {
.detach();
}
pub fn initialize_workspace(
workspace_handle: WeakView<Workspace>,
was_deserialized: bool,
app_state: Arc<AppState>,
cx: AsyncWindowContext,
) -> Task<Result<()>> {
cx.spawn(|mut cx| async move {
workspace_handle.update(&mut cx, |workspace, cx| {
let workspace_handle = cx.view().clone();
cx.subscribe(&workspace_handle, {
move |workspace, _, event, cx| {
if let workspace::Event::PaneAdded(pane) = event {
pane.update(cx, |pane, cx| {
pane.toolbar().update(cx, |toolbar, cx| {
// todo!()
// let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
// toolbar.add_item(breadcrumbs, cx);
let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
toolbar.add_item(buffer_search_bar.clone(), cx);
// let quick_action_bar = cx.add_view(|_| {
// QuickActionBar::new(buffer_search_bar, workspace)
// });
// toolbar.add_item(quick_action_bar, cx);
// let diagnostic_editor_controls =
// cx.add_view(|_| diagnostics2::ToolbarControls::new());
// toolbar.add_item(diagnostic_editor_controls, cx);
// let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
// toolbar.add_item(project_search_bar, cx);
// let submit_feedback_button =
// cx.add_view(|_| SubmitFeedbackButton::new());
// toolbar.add_item(submit_feedback_button, cx);
// let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
// toolbar.add_item(feedback_info_text, cx);
// let lsp_log_item =
// cx.add_view(|_| language_tools::LspLogToolbarItemView::new());
// toolbar.add_item(lsp_log_item, cx);
// let syntax_tree_item = cx
// .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new());
// toolbar.add_item(syntax_tree_item, cx);
})
});
}
}
})
.detach();
// cx.emit(workspace2::Event::PaneAdded(
// workspace.active_pane().clone(),
// ));
// let collab_titlebar_item =
// cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
// workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
// let copilot =
// cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
// let diagnostic_summary =
// cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
// let activity_indicator = activity_indicator::ActivityIndicator::new(
// workspace,
// app_state.languages.clone(),
// cx,
// );
// let active_buffer_language =
// cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
// let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx));
// let feedback_button = cx.add_view(|_| {
// feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
// });
// let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
workspace.status_bar().update(cx, |status_bar, cx| {
// status_bar.add_left_item(diagnostic_summary, cx);
// status_bar.add_left_item(activity_indicator, cx);
// status_bar.add_right_item(feedback_button, cx);
// status_bar.add_right_item(copilot, cx);
// status_bar.add_right_item(active_buffer_language, cx);
// status_bar.add_right_item(vim_mode_indicator, cx);
// status_bar.add_right_item(cursor_position, cx);
});
// auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
// vim::observe_keystrokes(cx);
// cx.on_window_should_close(|workspace, cx| {
// if let Some(task) = workspace.close(&Default::default(), cx) {
// task.detach_and_log_err(cx);
// }
// false
// });
})?;
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
// let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
// let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
// let channels_panel =
// collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
// let chat_panel =
// collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
// let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
// workspace_handle.clone(),
// cx.clone(),
// );
let (
project_panel,
// terminal_panel,
// assistant_panel,
// channels_panel,
// chat_panel,
// notification_panel,
) = futures::try_join!(
project_panel,
// terminal_panel,
// assistant_panel,
// channels_panel,
// chat_panel,
// notification_panel,
)?;
workspace_handle.update(&mut cx, |workspace, cx| {
let project_panel_position = project_panel.position(cx);
workspace.add_panel(project_panel, cx);
// workspace.add_panel(terminal_panel, cx);
// workspace.add_panel(assistant_panel, cx);
// workspace.add_panel(channels_panel, cx);
// workspace.add_panel(chat_panel, cx);
// workspace.add_panel(notification_panel, cx);
// if !was_deserialized
// && workspace
// .project()
// .read(cx)
// .visible_worktrees(cx)
// .any(|tree| {
// tree.read(cx)
// .root_entry()
// .map_or(false, |entry| entry.is_dir())
// })
// {
workspace.toggle_dock(project_panel_position, cx);
// }
// cx.focus_self();
})?;
Ok(())
})
}
fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
let app_name = cx.global::<ReleaseChannel>().display_name();
let version = env!("CARGO_PKG_VERSION");