mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
Merge branch 'main' into search2
This commit is contained in:
commit
6b6a30c3da
79
Cargo.lock
generated
79
Cargo.lock
generated
@ -1829,6 +1829,47 @@ dependencies = [
|
|||||||
"zed-actions",
|
"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]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -9163,6 +9204,39 @@ dependencies = [
|
|||||||
"workspace",
|
"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]]
|
[[package]]
|
||||||
name = "text"
|
name = "text"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -10087,6 +10161,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"gpui2",
|
"gpui2",
|
||||||
"itertools 0.11.0",
|
"itertools 0.11.0",
|
||||||
|
"menu2",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"serde",
|
"serde",
|
||||||
"settings2",
|
"settings2",
|
||||||
@ -11317,7 +11392,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.113.0"
|
version = "0.114.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"ai",
|
"ai",
|
||||||
@ -11470,6 +11545,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"cli",
|
"cli",
|
||||||
"client2",
|
"client2",
|
||||||
|
"collab_ui2",
|
||||||
"collections",
|
"collections",
|
||||||
"command_palette2",
|
"command_palette2",
|
||||||
"copilot2",
|
"copilot2",
|
||||||
@ -11522,6 +11598,7 @@ dependencies = [
|
|||||||
"smol",
|
"smol",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
"tempdir",
|
"tempdir",
|
||||||
|
"terminal_view2",
|
||||||
"text2",
|
"text2",
|
||||||
"theme2",
|
"theme2",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
@ -18,6 +18,7 @@ members = [
|
|||||||
"crates/collab",
|
"crates/collab",
|
||||||
"crates/collab2",
|
"crates/collab2",
|
||||||
"crates/collab_ui",
|
"crates/collab_ui",
|
||||||
|
"crates/collab_ui2",
|
||||||
"crates/collections",
|
"crates/collections",
|
||||||
"crates/command_palette",
|
"crates/command_palette",
|
||||||
"crates/command_palette2",
|
"crates/command_palette2",
|
||||||
@ -98,6 +99,7 @@ members = [
|
|||||||
"crates/sum_tree",
|
"crates/sum_tree",
|
||||||
"crates/terminal",
|
"crates/terminal",
|
||||||
"crates/terminal2",
|
"crates/terminal2",
|
||||||
|
"crates/terminal_view2",
|
||||||
"crates/text",
|
"crates/text",
|
||||||
"crates/theme",
|
"crates/theme",
|
||||||
"crates/theme2",
|
"crates/theme2",
|
||||||
@ -204,6 +206,9 @@ core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "07
|
|||||||
[profile.dev]
|
[profile.dev]
|
||||||
split-debuginfo = "unpacked"
|
split-debuginfo = "unpacked"
|
||||||
|
|
||||||
|
[profile.dev.package.taffy]
|
||||||
|
opt-level = 3
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
debug = true
|
debug = true
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
|
@ -220,7 +220,6 @@ impl TestServer {
|
|||||||
languages: Arc::new(language_registry),
|
languages: Arc::new(language_registry),
|
||||||
fs: fs.clone(),
|
fs: fs.clone(),
|
||||||
build_window_options: |_, _, _| Default::default(),
|
build_window_options: |_, _, _| Default::default(),
|
||||||
initialize_workspace: |_, _, _, _| gpui::Task::ready(Ok(())),
|
|
||||||
node_runtime: FakeNodeRuntime::new(),
|
node_runtime: FakeNodeRuntime::new(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
81
crates/collab_ui2/Cargo.toml
Normal file
81
crates/collab_ui2/Cargo.toml
Normal 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
|
454
crates/collab_ui2/src/channel_view.rs
Normal file
454
crates/collab_ui2/src/channel_view.rs
Normal 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()
|
||||||
|
// }
|
||||||
|
// }
|
983
crates/collab_ui2/src/chat_panel.rs
Normal file
983
crates/collab_ui2/src/chat_panel.rs
Normal 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», let’s «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
|
||||||
|
// },
|
||||||
|
// ]
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
313
crates/collab_ui2/src/chat_panel/message_editor.rs
Normal file
313
crates/collab_ui2/src/chat_panel/message_editor.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
3574
crates/collab_ui2/src/collab_panel.rs
Normal file
3574
crates/collab_ui2/src/collab_panel.rs
Normal file
File diff suppressed because it is too large
Load Diff
717
crates/collab_ui2/src/collab_panel/channel_modal.rs
Normal file
717
crates/collab_ui2/src/collab_panel/channel_modal.rs
Normal 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,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
261
crates/collab_ui2/src/collab_panel/contact_finder.rs
Normal file
261
crates/collab_ui2/src/collab_panel/contact_finder.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
1367
crates/collab_ui2/src/collab_titlebar_item.rs
Normal file
1367
crates/collab_ui2/src/collab_titlebar_item.rs
Normal file
File diff suppressed because it is too large
Load Diff
154
crates/collab_ui2/src/collab_ui.rs
Normal file
154
crates/collab_ui2/src/collab_ui.rs
Normal 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>()
|
||||||
|
// }
|
113
crates/collab_ui2/src/face_pile.rs
Normal file
113
crates/collab_ui2/src/face_pile.rs
Normal 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);
|
||||||
|
// }
|
||||||
|
// }
|
884
crates/collab_ui2/src/notification_panel.rs
Normal file
884
crates/collab_ui2/src/notification_panel.rs
Normal 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, ¬ification, 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(¬ification, 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(¬ification_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, .. } = ¬ification {
|
||||||
|
// 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())
|
||||||
|
// }
|
||||||
|
// }
|
11
crates/collab_ui2/src/notifications.rs
Normal file
11
crates/collab_ui2/src/notifications.rs
Normal 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);
|
||||||
|
// }
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
69
crates/collab_ui2/src/panel_settings.rs
Normal file
69
crates/collab_ui2/src/panel_settings.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -354,129 +354,116 @@ impl std::fmt::Debug for Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[cfg(test)]
|
#[cfg(test)]
|
||||||
// mod tests {
|
mod tests {
|
||||||
// use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
// use super::*;
|
use super::*;
|
||||||
// use editor::Editor;
|
use editor::Editor;
|
||||||
// use gpui::{executor::Deterministic, TestAppContext};
|
use gpui::TestAppContext;
|
||||||
// use project::Project;
|
use project::Project;
|
||||||
// use workspace::{AppState, Workspace};
|
use workspace::{AppState, Workspace};
|
||||||
|
|
||||||
// #[test]
|
#[test]
|
||||||
// fn test_humanize_action_name() {
|
fn test_humanize_action_name() {
|
||||||
// assert_eq!(
|
assert_eq!(
|
||||||
// humanize_action_name("editor::GoToDefinition"),
|
humanize_action_name("editor::GoToDefinition"),
|
||||||
// "editor: go to definition"
|
"editor: go to definition"
|
||||||
// );
|
);
|
||||||
// assert_eq!(
|
assert_eq!(
|
||||||
// humanize_action_name("editor::Backspace"),
|
humanize_action_name("editor::Backspace"),
|
||||||
// "editor: backspace"
|
"editor: backspace"
|
||||||
// );
|
);
|
||||||
// assert_eq!(
|
assert_eq!(
|
||||||
// humanize_action_name("go_to_line::Deploy"),
|
humanize_action_name("go_to_line::Deploy"),
|
||||||
// "go to line: deploy"
|
"go to line: deploy"
|
||||||
// );
|
);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// #[gpui::test]
|
#[gpui::test]
|
||||||
// async fn test_command_palette(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
|
async fn test_command_palette(cx: &mut TestAppContext) {
|
||||||
// let app_state = init_test(cx);
|
let app_state = init_test(cx);
|
||||||
|
|
||||||
// let project = Project::test(app_state.fs.clone(), [], cx).await;
|
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||||
// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
let (workspace, cx) = cx.add_window_view(|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
|
|
||||||
// });
|
|
||||||
|
|
||||||
// workspace.update(cx, |workspace, cx| {
|
let editor = cx.build_view(|cx| {
|
||||||
// cx.focus(&editor);
|
let mut editor = Editor::single_line(cx);
|
||||||
// workspace.add_item(Box::new(editor.clone()), cx)
|
editor.set_text("abc", cx);
|
||||||
// });
|
editor
|
||||||
|
});
|
||||||
|
|
||||||
// workspace.update(cx, |workspace, cx| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
// toggle_command_palette(workspace, &Toggle, cx);
|
workspace.add_item(Box::new(editor.clone()), cx);
|
||||||
// });
|
editor.update(cx, |editor, cx| editor.focus(cx))
|
||||||
|
});
|
||||||
|
|
||||||
// let palette = workspace.read_with(cx, |workspace, _| {
|
cx.simulate_keystrokes("cmd-shift-p");
|
||||||
// workspace.modal::<CommandPalette>().unwrap()
|
|
||||||
// });
|
|
||||||
|
|
||||||
// palette
|
let palette = workspace.update(cx, |workspace, cx| {
|
||||||
// .update(cx, |palette, cx| {
|
workspace
|
||||||
// // Fill up palette's command list by running an empty query;
|
.active_modal::<CommandPalette>(cx)
|
||||||
// // we only need it to subsequently assert that the palette is initially
|
.unwrap()
|
||||||
// // sorted by command's name.
|
.read(cx)
|
||||||
// palette.delegate_mut().update_matches("".to_string(), cx)
|
.picker
|
||||||
// })
|
.clone()
|
||||||
// .await;
|
});
|
||||||
|
|
||||||
// palette.update(cx, |palette, _| {
|
palette.update(cx, |palette, _| {
|
||||||
// let is_sorted =
|
assert!(palette.delegate.commands.len() > 5);
|
||||||
// |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name);
|
let is_sorted =
|
||||||
// assert!(is_sorted(&palette.delegate().actions));
|
|actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name);
|
||||||
// });
|
assert!(is_sorted(&palette.delegate.commands));
|
||||||
|
});
|
||||||
|
|
||||||
// palette
|
cx.simulate_input("bcksp");
|
||||||
// .update(cx, |palette, cx| {
|
|
||||||
// palette
|
|
||||||
// .delegate_mut()
|
|
||||||
// .update_matches("bcksp".to_string(), cx)
|
|
||||||
// })
|
|
||||||
// .await;
|
|
||||||
|
|
||||||
// palette.update(cx, |palette, cx| {
|
palette.update(cx, |palette, _| {
|
||||||
// assert_eq!(palette.delegate().matches[0].string, "editor: backspace");
|
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");
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // Add namespace filter, and redeploy the palette
|
cx.simulate_keystrokes("enter");
|
||||||
// cx.update(|cx| {
|
|
||||||
// cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
|
|
||||||
// filter.filtered_namespaces.insert("editor");
|
|
||||||
// })
|
|
||||||
// });
|
|
||||||
|
|
||||||
// workspace.update(cx, |workspace, cx| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
// toggle_command_palette(workspace, &Toggle, cx);
|
assert!(workspace.active_modal::<CommandPalette>(cx).is_none());
|
||||||
// });
|
assert_eq!(editor.read(cx).text(cx), "ab")
|
||||||
|
});
|
||||||
|
|
||||||
// // Assert editor command not present
|
// Add namespace filter, and redeploy the palette
|
||||||
// let palette = workspace.read_with(cx, |workspace, _| {
|
cx.update(|cx| {
|
||||||
// workspace.modal::<CommandPalette>().unwrap()
|
cx.set_global(CommandPaletteFilter::default());
|
||||||
// });
|
cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
|
||||||
|
filter.filtered_namespaces.insert("editor");
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
// palette
|
cx.simulate_keystrokes("cmd-shift-p");
|
||||||
// .update(cx, |palette, cx| {
|
cx.simulate_input("bcksp");
|
||||||
// palette
|
|
||||||
// .delegate_mut()
|
|
||||||
// .update_matches("bcksp".to_string(), cx)
|
|
||||||
// })
|
|
||||||
// .await;
|
|
||||||
|
|
||||||
// palette.update(cx, |palette, _| {
|
let palette = workspace.update(cx, |workspace, cx| {
|
||||||
// assert!(palette.delegate().matches.is_empty())
|
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> {
|
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||||
// cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
// let app_state = AppState::test(cx);
|
let app_state = AppState::test(cx);
|
||||||
// theme::init(cx);
|
theme::init(cx);
|
||||||
// language::init(cx);
|
language::init(cx);
|
||||||
// editor::init(cx);
|
editor::init(cx);
|
||||||
// workspace::init(app_state.clone(), cx);
|
workspace::init(app_state.clone(), cx);
|
||||||
// init(cx);
|
init(cx);
|
||||||
// Project::init_settings(cx);
|
Project::init_settings(cx);
|
||||||
// app_state
|
settings::load_default_keymap(cx);
|
||||||
// })
|
app_state
|
||||||
// }
|
})
|
||||||
// }
|
}
|
||||||
|
}
|
||||||
|
@ -31,7 +31,7 @@ pub use block_map::{
|
|||||||
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
|
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};
|
pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
@ -124,7 +124,7 @@ impl DisplayMap {
|
|||||||
self.fold(
|
self.fold(
|
||||||
other
|
other
|
||||||
.folds_in_range(0..other.buffer_snapshot.len())
|
.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,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -723,7 +723,7 @@ impl DisplaySnapshot {
|
|||||||
DisplayPoint(point)
|
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
|
where
|
||||||
T: ToOffset,
|
T: ToOffset,
|
||||||
{
|
{
|
||||||
|
@ -3,15 +3,16 @@ use super::{
|
|||||||
Highlights,
|
Highlights,
|
||||||
};
|
};
|
||||||
use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
|
use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
|
||||||
use gpui::{HighlightStyle, Hsla};
|
use gpui::{ElementId, HighlightStyle, Hsla};
|
||||||
use language::{Chunk, Edit, Point, TextSummary};
|
use language::{Chunk, Edit, Point, TextSummary};
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering},
|
||||||
iter,
|
iter,
|
||||||
ops::{Add, AddAssign, Range, Sub},
|
ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
|
||||||
};
|
};
|
||||||
use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
|
use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
|
||||||
|
use util::post_inc;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||||
pub struct FoldPoint(pub Point);
|
pub struct FoldPoint(pub Point);
|
||||||
@ -90,12 +91,16 @@ impl<'a> FoldMapWriter<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For now, ignore any ranges that span an excerpt boundary.
|
// For now, ignore any ranges that span an excerpt boundary.
|
||||||
let fold = Fold(buffer.anchor_after(range.start)..buffer.anchor_before(range.end));
|
let fold_range =
|
||||||
if fold.0.start.excerpt_id != fold.0.end.excerpt_id {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
folds.push(fold);
|
folds.push(Fold {
|
||||||
|
id: FoldId(post_inc(&mut self.0.next_fold_id.0)),
|
||||||
|
range: fold_range,
|
||||||
|
});
|
||||||
|
|
||||||
let inlay_range =
|
let inlay_range =
|
||||||
snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end);
|
snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end);
|
||||||
@ -106,13 +111,13 @@ impl<'a> FoldMapWriter<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let buffer = &snapshot.buffer;
|
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 = {
|
self.0.snapshot.folds = {
|
||||||
let mut new_tree = SumTree::new();
|
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 {
|
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.push(fold, buffer);
|
||||||
}
|
}
|
||||||
new_tree.append(cursor.suffix(buffer), buffer);
|
new_tree.append(cursor.suffix(buffer), buffer);
|
||||||
@ -138,7 +143,8 @@ impl<'a> FoldMapWriter<'a> {
|
|||||||
let mut folds_cursor =
|
let mut folds_cursor =
|
||||||
intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive);
|
intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive);
|
||||||
while let Some(fold) = folds_cursor.item() {
|
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 {
|
if offset_range.end > offset_range.start {
|
||||||
let inlay_range = snapshot.to_inlay_offset(offset_range.start)
|
let inlay_range = snapshot.to_inlay_offset(offset_range.start)
|
||||||
..snapshot.to_inlay_offset(offset_range.end);
|
..snapshot.to_inlay_offset(offset_range.end);
|
||||||
@ -175,6 +181,7 @@ impl<'a> FoldMapWriter<'a> {
|
|||||||
pub struct FoldMap {
|
pub struct FoldMap {
|
||||||
snapshot: FoldSnapshot,
|
snapshot: FoldSnapshot,
|
||||||
ellipses_color: Option<Hsla>,
|
ellipses_color: Option<Hsla>,
|
||||||
|
next_fold_id: FoldId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FoldMap {
|
impl FoldMap {
|
||||||
@ -197,6 +204,7 @@ impl FoldMap {
|
|||||||
ellipses_color: None,
|
ellipses_color: None,
|
||||||
},
|
},
|
||||||
ellipses_color: None,
|
ellipses_color: None,
|
||||||
|
next_fold_id: FoldId::default(),
|
||||||
};
|
};
|
||||||
let snapshot = this.snapshot.clone();
|
let snapshot = this.snapshot.clone();
|
||||||
(this, snapshot)
|
(this, snapshot)
|
||||||
@ -242,8 +250,8 @@ impl FoldMap {
|
|||||||
while let Some(fold) = folds.next() {
|
while let Some(fold) = folds.next() {
|
||||||
if let Some(next_fold) = folds.peek() {
|
if let Some(next_fold) = folds.peek() {
|
||||||
let comparison = fold
|
let comparison = fold
|
||||||
.0
|
.range
|
||||||
.cmp(&next_fold.0, &self.snapshot.inlay_snapshot.buffer);
|
.cmp(&next_fold.range, &self.snapshot.inlay_snapshot.buffer);
|
||||||
assert!(comparison.is_le());
|
assert!(comparison.is_le());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -304,9 +312,9 @@ impl FoldMap {
|
|||||||
let anchor = inlay_snapshot
|
let anchor = inlay_snapshot
|
||||||
.buffer
|
.buffer
|
||||||
.anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start));
|
.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(
|
folds_cursor.seek(
|
||||||
&Fold(anchor..Anchor::max()),
|
&FoldRange(anchor..Anchor::max()),
|
||||||
Bias::Left,
|
Bias::Left,
|
||||||
&inlay_snapshot.buffer,
|
&inlay_snapshot.buffer,
|
||||||
);
|
);
|
||||||
@ -315,8 +323,8 @@ impl FoldMap {
|
|||||||
let inlay_snapshot = &inlay_snapshot;
|
let inlay_snapshot = &inlay_snapshot;
|
||||||
move || {
|
move || {
|
||||||
let item = folds_cursor.item().map(|f| {
|
let item = folds_cursor.item().map(|f| {
|
||||||
let buffer_start = f.0.start.to_offset(&inlay_snapshot.buffer);
|
let buffer_start = f.range.start.to_offset(&inlay_snapshot.buffer);
|
||||||
let buffer_end = f.0.end.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_start)
|
||||||
..inlay_snapshot.to_inlay_offset(buffer_end)
|
..inlay_snapshot.to_inlay_offset(buffer_end)
|
||||||
});
|
});
|
||||||
@ -596,13 +604,13 @@ impl FoldSnapshot {
|
|||||||
self.transforms.summary().output.longest_row
|
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
|
where
|
||||||
T: ToOffset,
|
T: ToOffset,
|
||||||
{
|
{
|
||||||
let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
|
let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
|
||||||
iter::from_fn(move || {
|
iter::from_fn(move || {
|
||||||
let item = folds.item().map(|f| &f.0);
|
let item = folds.item();
|
||||||
folds.next(&self.inlay_snapshot.buffer);
|
folds.next(&self.inlay_snapshot.buffer);
|
||||||
item
|
item
|
||||||
})
|
})
|
||||||
@ -830,10 +838,39 @@ impl sum_tree::Summary for TransformSummary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
|
||||||
struct Fold(Range<Anchor>);
|
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 {
|
fn default() -> Self {
|
||||||
Self(Anchor::min()..Anchor::max())
|
Self(Anchor::min()..Anchor::max())
|
||||||
}
|
}
|
||||||
@ -844,17 +881,17 @@ impl sum_tree::Item for Fold {
|
|||||||
|
|
||||||
fn summary(&self) -> Self::Summary {
|
fn summary(&self) -> Self::Summary {
|
||||||
FoldSummary {
|
FoldSummary {
|
||||||
start: self.0.start.clone(),
|
start: self.range.start.clone(),
|
||||||
end: self.0.end.clone(),
|
end: self.range.end.clone(),
|
||||||
min_start: self.0.start.clone(),
|
min_start: self.range.start.clone(),
|
||||||
max_end: self.0.end.clone(),
|
max_end: self.range.end.clone(),
|
||||||
count: 1,
|
count: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct FoldSummary {
|
pub struct FoldSummary {
|
||||||
start: Anchor,
|
start: Anchor,
|
||||||
end: Anchor,
|
end: Anchor,
|
||||||
min_start: 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) {
|
fn add_summary(&mut self, summary: &'a FoldSummary, _: &MultiBufferSnapshot) {
|
||||||
self.0.start = summary.start.clone();
|
self.0.start = summary.start.clone();
|
||||||
self.0.end = summary.end.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 {
|
fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering {
|
||||||
self.0.cmp(&other.0, buffer)
|
self.0.cmp(&other.0, buffer)
|
||||||
}
|
}
|
||||||
@ -1321,7 +1358,10 @@ mod tests {
|
|||||||
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
|
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
|
||||||
let fold_ranges = snapshot
|
let fold_ranges = snapshot
|
||||||
.folds_in_range(Point::new(1, 0)..Point::new(1, 3))
|
.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<_>>();
|
.collect::<Vec<_>>();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fold_ranges,
|
fold_ranges,
|
||||||
@ -1553,10 +1593,9 @@ mod tests {
|
|||||||
.filter(|fold| {
|
.filter(|fold| {
|
||||||
let start = buffer_snapshot.anchor_before(start);
|
let start = buffer_snapshot.anchor_before(start);
|
||||||
let end = buffer_snapshot.anchor_after(end);
|
let end = buffer_snapshot.anchor_after(end);
|
||||||
start.cmp(&fold.0.end, &buffer_snapshot) == Ordering::Less
|
start.cmp(&fold.range.end, &buffer_snapshot) == Ordering::Less
|
||||||
&& end.cmp(&fold.0.start, &buffer_snapshot) == Ordering::Greater
|
&& end.cmp(&fold.range.start, &buffer_snapshot) == Ordering::Greater
|
||||||
})
|
})
|
||||||
.map(|fold| fold.0)
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -1639,10 +1678,10 @@ mod tests {
|
|||||||
let buffer = &inlay_snapshot.buffer;
|
let buffer = &inlay_snapshot.buffer;
|
||||||
let mut folds = self.snapshot.folds.items(buffer);
|
let mut folds = self.snapshot.folds.items(buffer);
|
||||||
// Ensure sorting doesn't change how folds get merged and displayed.
|
// 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
|
let mut fold_ranges = folds
|
||||||
.iter()
|
.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();
|
.peekable();
|
||||||
|
|
||||||
let mut merged_ranges = Vec::new();
|
let mut merged_ranges = Vec::new();
|
||||||
|
@ -39,12 +39,12 @@ use futures::FutureExt;
|
|||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use git::diff_hunk_to_display;
|
use git::diff_hunk_to_display;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
action, actions, div, point, prelude::*, px, relative, rems, render_view, size, uniform_list,
|
action, actions, div, point, prelude::*, px, relative, rems, size, uniform_list, AnyElement,
|
||||||
AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem,
|
AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
|
||||||
Component, Context, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight,
|
EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle,
|
||||||
HighlightStyle, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels,
|
Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled,
|
||||||
Render, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext,
|
Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext,
|
||||||
VisualContext, WeakView, WindowContext,
|
WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||||
use hover_popover::{hide_hover, HoverState};
|
use hover_popover::{hide_hover, HoverState};
|
||||||
@ -97,7 +97,7 @@ use text::{OffsetUtf16, Rope};
|
|||||||
use theme::{
|
use theme::{
|
||||||
ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
|
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 util::{post_inc, RangeExt, ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{ItemEvent, ItemHandle},
|
item::{ItemEvent, ItemHandle},
|
||||||
@ -4372,69 +4372,42 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn render_fold_indicators(
|
pub fn render_fold_indicators(
|
||||||
// &self,
|
&self,
|
||||||
// fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
|
fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
|
||||||
// style: &EditorStyle,
|
style: &EditorStyle,
|
||||||
// gutter_hovered: bool,
|
gutter_hovered: bool,
|
||||||
// line_height: f32,
|
line_height: Pixels,
|
||||||
// gutter_margin: f32,
|
gutter_margin: Pixels,
|
||||||
// cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
// ) -> Vec<Option<AnyElement<Self>>> {
|
) -> Vec<Option<AnyElement<Self>>> {
|
||||||
// enum FoldIndicators {}
|
fold_data
|
||||||
|
.iter()
|
||||||
// let style = style.folds.clone();
|
.enumerate()
|
||||||
|
.map(|(ix, fold_data)| {
|
||||||
// fold_data
|
fold_data
|
||||||
// .iter()
|
.map(|(fold_status, buffer_row, active)| {
|
||||||
// .enumerate()
|
(active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
|
||||||
// .map(|(ix, fold_data)| {
|
let icon = match fold_status {
|
||||||
// fold_data
|
FoldStatus::Folded => ui::Icon::ChevronRight,
|
||||||
// .map(|(fold_status, buffer_row, active)| {
|
FoldStatus::Foldable => ui::Icon::ChevronDown,
|
||||||
// (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
|
};
|
||||||
// MouseEventHandler::new::<FoldIndicators, _>(
|
IconButton::new(ix as usize, icon)
|
||||||
// ix as usize,
|
.on_click(move |editor: &mut Editor, cx| match fold_status {
|
||||||
// cx,
|
FoldStatus::Folded => {
|
||||||
// |mouse_state, _| {
|
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
|
||||||
// Svg::new(match fold_status {
|
}
|
||||||
// FoldStatus::Folded => style.folded_icon.clone(),
|
FoldStatus::Foldable => {
|
||||||
// FoldStatus::Foldable => style.foldable_icon.clone(),
|
editor.fold_at(&FoldAt { buffer_row }, cx);
|
||||||
// })
|
}
|
||||||
// .with_color(
|
})
|
||||||
// style
|
.render()
|
||||||
// .indicator
|
})
|
||||||
// .in_state(fold_status == FoldStatus::Folded)
|
})
|
||||||
// .style_for(mouse_state)
|
.flatten()
|
||||||
// .color,
|
})
|
||||||
// )
|
.collect()
|
||||||
// .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 context_menu_visible(&self) -> bool {
|
pub fn context_menu_visible(&self) -> bool {
|
||||||
self.context_menu
|
self.context_menu
|
||||||
@ -5330,8 +5303,8 @@ impl Editor {
|
|||||||
buffer.anchor_before(range_to_move.start)
|
buffer.anchor_before(range_to_move.start)
|
||||||
..buffer.anchor_after(range_to_move.end),
|
..buffer.anchor_after(range_to_move.end),
|
||||||
) {
|
) {
|
||||||
let mut start = fold.start.to_point(&buffer);
|
let mut start = fold.range.start.to_point(&buffer);
|
||||||
let mut end = fold.end.to_point(&buffer);
|
let mut end = fold.range.end.to_point(&buffer);
|
||||||
start.row -= row_delta;
|
start.row -= row_delta;
|
||||||
end.row -= row_delta;
|
end.row -= row_delta;
|
||||||
refold_ranges.push(start..end);
|
refold_ranges.push(start..end);
|
||||||
@ -5421,8 +5394,8 @@ impl Editor {
|
|||||||
buffer.anchor_before(range_to_move.start)
|
buffer.anchor_before(range_to_move.start)
|
||||||
..buffer.anchor_after(range_to_move.end),
|
..buffer.anchor_after(range_to_move.end),
|
||||||
) {
|
) {
|
||||||
let mut start = fold.start.to_point(&buffer);
|
let mut start = fold.range.start.to_point(&buffer);
|
||||||
let mut end = fold.end.to_point(&buffer);
|
let mut end = fold.range.end.to_point(&buffer);
|
||||||
start.row += row_delta;
|
start.row += row_delta;
|
||||||
end.row += row_delta;
|
end.row += row_delta;
|
||||||
refold_ranges.push(start..end);
|
refold_ranges.push(start..end);
|
||||||
@ -7804,25 +7777,18 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
div()
|
div()
|
||||||
.pl(cx.anchor_x)
|
.pl(cx.anchor_x)
|
||||||
.child(render_view(
|
.child(rename_editor.render_with(EditorElement::new(
|
||||||
&rename_editor,
|
&rename_editor,
|
||||||
EditorElement::new(
|
EditorStyle {
|
||||||
&rename_editor,
|
background: cx.theme().system().transparent,
|
||||||
EditorStyle {
|
local_player: cx.editor_style.local_player,
|
||||||
background: cx.theme().system().transparent,
|
text: text_style,
|
||||||
local_player: cx.editor_style.local_player,
|
scrollbar_width: cx.editor_style.scrollbar_width,
|
||||||
text: text_style,
|
syntax: cx.editor_style.syntax.clone(),
|
||||||
scrollbar_width: cx
|
diagnostic_style:
|
||||||
.editor_style
|
cx.editor_style.diagnostic_style.clone(),
|
||||||
.scrollbar_width,
|
},
|
||||||
syntax: cx.editor_style.syntax.clone(),
|
)))
|
||||||
diagnostic_style: cx
|
|
||||||
.editor_style
|
|
||||||
.diagnostic_style
|
|
||||||
.clone(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.render()
|
.render()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@ -9401,6 +9367,12 @@ pub struct EditorReleased(pub WeakView<Editor>);
|
|||||||
//
|
//
|
||||||
impl EventEmitter<Event> for 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 {
|
impl Render for Editor {
|
||||||
type Element = EditorElement;
|
type Element = EditorElement;
|
||||||
|
|
||||||
@ -10019,7 +9991,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
|
|||||||
.on_click(move |_, _, cx| {
|
.on_click(move |_, _, cx| {
|
||||||
cx.write_to_clipboard(ClipboardItem::new(message.clone()));
|
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()
|
.render()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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)
|
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||||
});
|
});
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, 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))
|
view.condition::<crate::Event>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
view.update(&mut cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
view.change_selections(None, cx, |s| {
|
view.change_selections(None, cx, |s| {
|
||||||
s.select_display_ranges([
|
s.select_display_ranges([
|
||||||
DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
|
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);
|
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
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(0, 23)..DisplayPoint::new(0, 27),
|
||||||
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
|
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);
|
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
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(0, 16)..DisplayPoint::new(0, 28),
|
||||||
DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
|
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);
|
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
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)]
|
&[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Trying to expand the selected syntax node one more time has no effect.
|
// 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);
|
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
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)]
|
&[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);
|
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
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(0, 16)..DisplayPoint::new(0, 28),
|
||||||
DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
|
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);
|
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
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(0, 23)..DisplayPoint::new(0, 27),
|
||||||
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
|
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);
|
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
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(0, 25)..DisplayPoint::new(0, 25),
|
||||||
DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
|
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.
|
// 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);
|
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
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(0, 25)..DisplayPoint::new(0, 25),
|
||||||
DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
|
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
|
// Ensure that we keep expanding the selection if the larger selection starts or ends within
|
||||||
// a fold.
|
// a fold.
|
||||||
view.update(&mut cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
view.fold_ranges(
|
view.fold_ranges(
|
||||||
vec![
|
vec![
|
||||||
Point::new(0, 21)..Point::new(0, 24),
|
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);
|
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
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(0, 16)..DisplayPoint::new(0, 28),
|
||||||
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
|
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)
|
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||||
});
|
});
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, 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 (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
editor
|
editor
|
||||||
.condition::<crate::Event>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
|
.condition::<crate::Event>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
|
||||||
.await;
|
.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)
|
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||||
});
|
});
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, 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));
|
||||||
let cx = &mut cx;
|
|
||||||
view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||||
.await;
|
.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)
|
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||||
});
|
});
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, 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 (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
editor
|
editor
|
||||||
.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||||
.await;
|
.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 fake_server = fake_servers.next().await.unwrap();
|
||||||
|
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, 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 (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||||
assert!(cx.read(|cx| editor.is_dirty(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 fake_server = fake_servers.next().await.unwrap();
|
||||||
|
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, 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 (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||||
assert!(cx.read(|cx| editor.is_dirty(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 fake_server = fake_servers.next().await.unwrap();
|
||||||
|
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, 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 (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||||
|
|
||||||
let format = editor
|
let format = editor
|
||||||
@ -5993,8 +5987,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
|
|||||||
multibuffer
|
multibuffer
|
||||||
});
|
});
|
||||||
|
|
||||||
let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
|
let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
view.update(cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
assert_eq!(view.text(cx), "aaaa\nbbbb");
|
assert_eq!(view.text(cx), "aaaa\nbbbb");
|
||||||
view.change_selections(None, cx, |s| {
|
view.change_selections(None, cx, |s| {
|
||||||
@ -6064,8 +6057,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
|
|||||||
multibuffer
|
multibuffer
|
||||||
});
|
});
|
||||||
|
|
||||||
let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
|
let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
view.update(cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
let (expected_text, selection_ranges) = marked_text_ranges(
|
let (expected_text, selection_ranges) = marked_text_ranges(
|
||||||
indoc! {"
|
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)
|
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||||
});
|
});
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, 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));
|
||||||
let cx = &mut cx;
|
|
||||||
view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||||
.await;
|
.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_text = "one\ntwo\nthree\n";
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, 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 (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
|
editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
|
||||||
|
|
||||||
editor
|
editor
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -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_end = Point::new(hunk.buffer_range.end + 2, 0);
|
||||||
let folds_range = folds_start..folds_end;
|
let folds_range = folds_start..folds_end;
|
||||||
|
|
||||||
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold_range| {
|
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.range.to_point(&snapshot.buffer_snapshot);
|
||||||
let fold_point_range = fold_point_range.start..=fold_point_range.end;
|
let fold_point_range = fold_point_range.start..=fold_point_range.end;
|
||||||
|
|
||||||
let folded_start = fold_point_range.contains(&hunk_start_point);
|
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 {
|
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 }
|
DisplayDiffHunk::Folded { display_row: row }
|
||||||
} else {
|
} else {
|
||||||
let start = hunk_start_point.to_display_point(snapshot).row();
|
let start = hunk_start_point.to_display_point(snapshot).row();
|
||||||
|
@ -527,10 +527,6 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor)
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Item for Editor {
|
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 {
|
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||||
todo!();
|
todo!();
|
||||||
// if let Ok(data) = data.downcast::<NavigationData>() {
|
// if let Ok(data) = data.downcast::<NavigationData>() {
|
||||||
|
@ -426,7 +426,7 @@ impl Editor {
|
|||||||
|
|
||||||
pub fn read_scroll_position_from_db(
|
pub fn read_scroll_position_from_db(
|
||||||
&mut self,
|
&mut self,
|
||||||
item_id: usize,
|
item_id: u64,
|
||||||
workspace_id: WorkspaceId,
|
workspace_id: WorkspaceId,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut ViewContext<Editor>,
|
||||||
) {
|
) {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -54,6 +54,9 @@ pub trait Action: std::fmt::Debug + 'static {
|
|||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
fn build(value: Option<serde_json::Value>) -> Result<Box<dyn Action>>
|
fn build(value: Option<serde_json::Value>) -> Result<Box<dyn Action>>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
fn is_registered() -> bool
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
|
|
||||||
@ -65,7 +68,7 @@ pub trait Action: std::fmt::Debug + 'static {
|
|||||||
// Types become actions by satisfying a list of trait bounds.
|
// Types become actions by satisfying a list of trait bounds.
|
||||||
impl<A> Action for A
|
impl<A> Action for A
|
||||||
where
|
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 {
|
fn qualified_name() -> SharedString {
|
||||||
let name = type_name::<A>();
|
let name = type_name::<A>();
|
||||||
@ -88,6 +91,14 @@ where
|
|||||||
Ok(Box::new(action))
|
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 {
|
fn partial_eq(&self, action: &dyn Action) -> bool {
|
||||||
action
|
action
|
||||||
.as_any()
|
.as_any()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, ForegroundExecutor,
|
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView,
|
||||||
Model, ModelContext, Render, Result, Task, View, ViewContext, VisualContext, WindowContext,
|
ForegroundExecutor, Model, ModelContext, Render, Result, Task, View, ViewContext,
|
||||||
WindowHandle,
|
VisualContext, WindowContext, WindowHandle,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context as _};
|
use anyhow::{anyhow, Context as _};
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
@ -307,4 +307,13 @@ impl VisualContext for AsyncWindowContext {
|
|||||||
self.window
|
self.window
|
||||||
.update(self, |_, cx| cx.replace_root_view(build_view))
|
.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);
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,6 +68,7 @@ impl EntityMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Move an entity to the stack.
|
/// Move an entity to the stack.
|
||||||
|
#[track_caller]
|
||||||
pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> {
|
pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> {
|
||||||
self.assert_valid_context(model);
|
self.assert_valid_context(model);
|
||||||
let entity = Some(
|
let entity = Some(
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
|
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
|
||||||
BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent,
|
BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent,
|
||||||
Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View,
|
Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TestWindow,
|
||||||
ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
|
View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, bail};
|
use anyhow::{anyhow, bail};
|
||||||
use futures::{Stream, StreamExt};
|
use futures::{Stream, StreamExt};
|
||||||
@ -140,7 +140,7 @@ impl TestAppContext {
|
|||||||
.any_handle
|
.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
|
where
|
||||||
F: FnOnce(&mut ViewContext<V>) -> V,
|
F: FnOnce(&mut ViewContext<V>) -> V,
|
||||||
V: Render,
|
V: Render,
|
||||||
@ -149,7 +149,9 @@ impl TestAppContext {
|
|||||||
let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window));
|
let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window));
|
||||||
drop(cx);
|
drop(cx);
|
||||||
let view = window.root_view(self).unwrap();
|
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(
|
pub fn simulate_new_path_selection(
|
||||||
@ -220,7 +222,35 @@ impl TestAppContext {
|
|||||||
{
|
{
|
||||||
window
|
window
|
||||||
.update(self, |_, cx| cx.dispatch_action(action.boxed_clone()))
|
.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(
|
pub fn dispatch_keystroke(
|
||||||
@ -229,15 +259,41 @@ impl TestAppContext {
|
|||||||
keystroke: Keystroke,
|
keystroke: Keystroke,
|
||||||
is_held: bool,
|
is_held: bool,
|
||||||
) {
|
) {
|
||||||
|
let keystroke2 = keystroke.clone();
|
||||||
let handled = window
|
let handled = window
|
||||||
.update(self, |_, cx| {
|
.update(self, |_, cx| {
|
||||||
cx.dispatch_event(InputEvent::KeyDown(KeyDownEvent { keystroke, is_held }))
|
cx.dispatch_event(InputEvent::KeyDown(KeyDownEvent { keystroke, is_held }))
|
||||||
})
|
})
|
||||||
.is_ok_and(|handled| handled);
|
.is_ok_and(|handled| handled);
|
||||||
|
if handled {
|
||||||
if !handled {
|
return;
|
||||||
// todo!() simluate input here
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = ()> {
|
pub fn notifications<T: 'static>(&mut self, entity: &Model<T>) -> impl Stream<Item = ()> {
|
||||||
@ -401,12 +457,24 @@ impl<'a> VisualTestContext<'a> {
|
|||||||
Self { cx, window }
|
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)
|
pub fn dispatch_action<A>(&mut self, action: A)
|
||||||
where
|
where
|
||||||
A: Action,
|
A: Action,
|
||||||
{
|
{
|
||||||
self.cx.dispatch_action(self.window, 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> {
|
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))
|
.update(self.cx, |_, cx| cx.replace_root_view(build_view))
|
||||||
.unwrap()
|
.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 {
|
impl AnyWindowHandle {
|
||||||
|
@ -3,28 +3,19 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
pub(crate) use smallvec::SmallVec;
|
pub(crate) use smallvec::SmallVec;
|
||||||
use std::{any::Any, mem};
|
use std::{any::Any, fmt::Debug, mem};
|
||||||
|
|
||||||
pub trait Element<V: 'static> {
|
pub trait Element<V: 'static> {
|
||||||
type ElementState: 'static;
|
type ElementState: 'static;
|
||||||
|
|
||||||
fn element_id(&self) -> Option<ElementId>;
|
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(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
view_state: &mut V,
|
view_state: &mut V,
|
||||||
element_state: &mut Self::ElementState,
|
previous_element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> LayoutId;
|
) -> (LayoutId, Self::ElementState);
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
&mut self,
|
&mut self,
|
||||||
@ -33,6 +24,42 @@ pub trait Element<V: 'static> {
|
|||||||
element_state: &mut Self::ElementState,
|
element_state: &mut Self::ElementState,
|
||||||
cx: &mut ViewContext<V>,
|
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)]
|
#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
@ -60,7 +87,6 @@ pub trait ParentComponent<V: 'static> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trait ElementObject<V> {
|
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 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 paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
|
||||||
fn measure(
|
fn measure(
|
||||||
@ -87,9 +113,6 @@ struct RenderedElement<V: 'static, E: Element<V>> {
|
|||||||
enum ElementRenderPhase<V> {
|
enum ElementRenderPhase<V> {
|
||||||
#[default]
|
#[default]
|
||||||
Start,
|
Start,
|
||||||
Initialized {
|
|
||||||
frame_state: Option<V>,
|
|
||||||
},
|
|
||||||
LayoutRequested {
|
LayoutRequested {
|
||||||
layout_id: LayoutId,
|
layout_id: LayoutId,
|
||||||
frame_state: Option<V>,
|
frame_state: Option<V>,
|
||||||
@ -99,7 +122,9 @@ enum ElementRenderPhase<V> {
|
|||||||
available_space: Size<AvailableSpace>,
|
available_space: Size<AvailableSpace>,
|
||||||
frame_state: Option<V>,
|
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.
|
/// 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: Element<V>,
|
||||||
E::ElementState: 'static,
|
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 {
|
fn layout(&mut self, state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
|
||||||
let layout_id;
|
let (layout_id, frame_state) = match mem::take(&mut self.phase) {
|
||||||
let mut frame_state;
|
ElementRenderPhase::Start => {
|
||||||
match mem::take(&mut self.phase) {
|
|
||||||
ElementRenderPhase::Initialized {
|
|
||||||
frame_state: initial_frame_state,
|
|
||||||
} => {
|
|
||||||
frame_state = initial_frame_state;
|
|
||||||
if let Some(id) = self.element.element_id() {
|
if let Some(id) = self.element.element_id() {
|
||||||
layout_id = cx.with_element_state(id, |element_state, cx| {
|
let layout_id = cx.with_element_state(id, |element_state, cx| {
|
||||||
let mut element_state = element_state.unwrap();
|
self.element.layout(state, element_state, cx)
|
||||||
let layout_id = self.element.layout(state, &mut element_state, cx);
|
|
||||||
(layout_id, element_state)
|
|
||||||
});
|
});
|
||||||
|
(layout_id, None)
|
||||||
} else {
|
} else {
|
||||||
layout_id = self
|
let (layout_id, frame_state) = self.element.layout(state, None, cx);
|
||||||
.element
|
(layout_id, Some(frame_state))
|
||||||
.layout(state, frame_state.as_mut().unwrap(), cx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ElementRenderPhase::Start => panic!("must call initialize before layout"),
|
|
||||||
ElementRenderPhase::LayoutRequested { .. }
|
ElementRenderPhase::LayoutRequested { .. }
|
||||||
| ElementRenderPhase::LayoutComputed { .. }
|
| ElementRenderPhase::LayoutComputed { .. }
|
||||||
| ElementRenderPhase::Painted => {
|
| ElementRenderPhase::Painted { .. } => {
|
||||||
panic!("element rendered twice")
|
panic!("element rendered twice")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -192,7 +194,7 @@ where
|
|||||||
self.element
|
self.element
|
||||||
.paint(bounds, view_state, frame_state.as_mut().unwrap(), cx);
|
.paint(bounds, view_state, frame_state.as_mut().unwrap(), cx);
|
||||||
}
|
}
|
||||||
ElementRenderPhase::Painted
|
ElementRenderPhase::Painted { frame_state }
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => panic!("must call layout before paint"),
|
_ => panic!("must call layout before paint"),
|
||||||
@ -206,10 +208,6 @@ where
|
|||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> Size<Pixels> {
|
) -> Size<Pixels> {
|
||||||
if matches!(&self.phase, ElementRenderPhase::Start) {
|
if matches!(&self.phase, ElementRenderPhase::Start) {
|
||||||
self.initialize(view_state, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches!(&self.phase, ElementRenderPhase::Initialized { .. }) {
|
|
||||||
self.layout(view_state, cx);
|
self.layout(view_state, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,16 +244,13 @@ where
|
|||||||
|
|
||||||
fn draw(
|
fn draw(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut origin: Point<Pixels>,
|
origin: Point<Pixels>,
|
||||||
available_space: Size<AvailableSpace>,
|
available_space: Size<AvailableSpace>,
|
||||||
view_state: &mut V,
|
view_state: &mut V,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) {
|
) {
|
||||||
self.measure(available_space, view_state, cx);
|
self.measure(available_space, view_state, cx);
|
||||||
// Ignore the element offset when drawing this element, as the origin is already specified
|
cx.with_absolute_element_offset(origin, |cx| self.paint(view_state, cx))
|
||||||
// in absolute terms.
|
|
||||||
origin -= cx.element_offset();
|
|
||||||
cx.with_element_offset(origin, |cx| self.paint(view_state, cx))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,10 +266,6 @@ impl<V> AnyElement<V> {
|
|||||||
AnyElement(Box::new(RenderedElement::new(element)))
|
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 {
|
pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
|
||||||
self.0.layout(view_state, cx)
|
self.0.layout(view_state, cx)
|
||||||
}
|
}
|
||||||
@ -355,25 +346,16 @@ where
|
|||||||
None
|
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(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
view_state: &mut V,
|
view_state: &mut V,
|
||||||
rendered_element: &mut Self::ElementState,
|
_: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> LayoutId {
|
) -> (LayoutId, Self::ElementState) {
|
||||||
rendered_element.layout(view_state, cx)
|
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(
|
fn paint(
|
||||||
|
@ -6,15 +6,15 @@ use crate::{
|
|||||||
SharedString, Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility,
|
SharedString, Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility,
|
||||||
};
|
};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use parking_lot::Mutex;
|
|
||||||
use refineable::Refineable;
|
use refineable::Refineable;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
|
cell::RefCell,
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
mem,
|
mem,
|
||||||
sync::Arc,
|
rc::Rc,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use taffy::style::Overflow;
|
use taffy::style::Overflow;
|
||||||
@ -22,7 +22,6 @@ use util::ResultExt;
|
|||||||
|
|
||||||
const DRAG_THRESHOLD: f64 = 2.;
|
const DRAG_THRESHOLD: f64 = 2.;
|
||||||
const TOOLTIP_DELAY: Duration = Duration::from_millis(500);
|
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 struct GroupStyle {
|
||||||
pub group: SharedString,
|
pub group: SharedString,
|
||||||
@ -229,6 +228,20 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
|
|||||||
mut self,
|
mut self,
|
||||||
listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
|
listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
|
||||||
) -> Self {
|
) -> 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((
|
self.interactivity().action_listeners.push((
|
||||||
TypeId::of::<A>(),
|
TypeId::of::<A>(),
|
||||||
Box::new(move |view, action, phase, cx| {
|
Box::new(move |view, action, phase, cx| {
|
||||||
@ -394,21 +407,19 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tooltip<W>(
|
fn tooltip(
|
||||||
mut self,
|
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
|
) -> Self
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
W: 'static + Render,
|
|
||||||
{
|
{
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
self.interactivity().tooltip_builder.is_none(),
|
self.interactivity().tooltip_builder.is_none(),
|
||||||
"calling tooltip more than once on the same element is not supported"
|
"calling tooltip more than once on the same element is not supported"
|
||||||
);
|
);
|
||||||
self.interactivity().tooltip_builder = Some(Arc::new(move |view_state, cx| {
|
self.interactivity().tooltip_builder =
|
||||||
build_tooltip(view_state, cx).into()
|
Some(Rc::new(move |view_state, cx| build_tooltip(view_state, cx)));
|
||||||
}));
|
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -423,14 +434,6 @@ pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> {
|
|||||||
self
|
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
|
fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
||||||
where
|
where
|
||||||
Self: Sized,
|
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 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> =
|
pub type KeyDownListener<V> =
|
||||||
Box<dyn Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
|
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()
|
self.interactivity.element_id.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
view_state: &mut V,
|
view_state: &mut V,
|
||||||
element_state: Option<Self::ElementState>,
|
element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> Self::ElementState {
|
) -> (LayoutId, Self::ElementState) {
|
||||||
let interactive_state = self
|
let mut child_layout_ids = SmallVec::new();
|
||||||
.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 {
|
|
||||||
let mut interactivity = mem::take(&mut self.interactivity);
|
let mut interactivity = mem::take(&mut self.interactivity);
|
||||||
let layout_id =
|
let (layout_id, interactive_state) = interactivity.layout(
|
||||||
interactivity.layout(&mut element_state.interactive_state, cx, |style, cx| {
|
element_state.map(|s| s.interactive_state),
|
||||||
|
cx,
|
||||||
|
|style, cx| {
|
||||||
cx.with_text_style(style.text_style().cloned(), |cx| {
|
cx.with_text_style(style.text_style().cloned(), |cx| {
|
||||||
element_state.child_layout_ids = self
|
child_layout_ids = self
|
||||||
.children
|
.children
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.map(|child| child.layout(view_state, cx))
|
.map(|child| child.layout(view_state, cx))
|
||||||
.collect::<SmallVec<_>>();
|
.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;
|
self.interactivity = interactivity;
|
||||||
layout_id
|
(
|
||||||
|
layout_id,
|
||||||
|
DivState {
|
||||||
|
interactive_state,
|
||||||
|
child_layout_ids,
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
@ -711,6 +704,12 @@ pub struct DivState {
|
|||||||
interactive_state: InteractiveElementState,
|
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 struct Interactivity<V> {
|
||||||
pub element_id: Option<ElementId>,
|
pub element_id: Option<ElementId>,
|
||||||
pub key_context: KeyContext,
|
pub key_context: KeyContext,
|
||||||
@ -720,7 +719,6 @@ pub struct Interactivity<V> {
|
|||||||
pub group: Option<SharedString>,
|
pub group: Option<SharedString>,
|
||||||
pub base_style: StyleRefinement,
|
pub base_style: StyleRefinement,
|
||||||
pub focus_style: StyleRefinement,
|
pub focus_style: StyleRefinement,
|
||||||
pub focus_in_style: StyleRefinement,
|
|
||||||
pub in_focus_style: StyleRefinement,
|
pub in_focus_style: StyleRefinement,
|
||||||
pub hover_style: StyleRefinement,
|
pub hover_style: StyleRefinement,
|
||||||
pub group_hover_style: Option<GroupStyle>,
|
pub group_hover_style: Option<GroupStyle>,
|
||||||
@ -746,11 +744,12 @@ impl<V> Interactivity<V>
|
|||||||
where
|
where
|
||||||
V: 'static,
|
V: 'static,
|
||||||
{
|
{
|
||||||
pub fn initialize(
|
pub fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
element_state: Option<InteractiveElementState>,
|
element_state: Option<InteractiveElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> InteractiveElementState {
|
f: impl FnOnce(Style, &mut ViewContext<V>) -> LayoutId,
|
||||||
|
) -> (LayoutId, InteractiveElementState) {
|
||||||
let mut element_state = element_state.unwrap_or_default();
|
let mut element_state = element_state.unwrap_or_default();
|
||||||
|
|
||||||
// Ensure we store a focus handle in our element state if we're focusable.
|
// Ensure we store a focus handle in our element state if we're focusable.
|
||||||
@ -765,17 +764,9 @@ where
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
element_state
|
let style = self.compute_style(None, &mut element_state, cx);
|
||||||
}
|
let layout_id = f(style, cx);
|
||||||
|
(layout_id, 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn paint(
|
pub fn paint(
|
||||||
@ -876,7 +867,7 @@ where
|
|||||||
|
|
||||||
if !click_listeners.is_empty() || drag_listener.is_some() {
|
if !click_listeners.is_empty() || drag_listener.is_some() {
|
||||||
let pending_mouse_down = element_state.pending_mouse_down.clone();
|
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(mouse_down) = mouse_down {
|
||||||
if let Some(drag_listener) = drag_listener {
|
if let Some(drag_listener) = drag_listener {
|
||||||
let active_state = element_state.clicked_state.clone();
|
let active_state = element_state.clicked_state.clone();
|
||||||
@ -890,7 +881,7 @@ where
|
|||||||
&& bounds.contains_point(&event.position)
|
&& bounds.contains_point(&event.position)
|
||||||
&& (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD
|
&& (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 cursor_offset = event.position - bounds.origin;
|
||||||
let drag = drag_listener(view_state, cursor_offset, cx);
|
let drag = drag_listener(view_state, cursor_offset, cx);
|
||||||
cx.active_drag = Some(drag);
|
cx.active_drag = Some(drag);
|
||||||
@ -910,13 +901,13 @@ where
|
|||||||
listener(view_state, &mouse_click, cx);
|
listener(view_state, &mouse_click, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*pending_mouse_down.lock() = None;
|
*pending_mouse_down.borrow_mut() = None;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
cx.on_mouse_event(move |_view_state, event: &MouseDownEvent, phase, cx| {
|
cx.on_mouse_event(move |_view_state, event: &MouseDownEvent, phase, cx| {
|
||||||
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
|
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();
|
cx.notify();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -932,8 +923,8 @@ where
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let is_hovered =
|
let is_hovered =
|
||||||
bounds.contains_point(&event.position) && has_mouse_down.lock().is_none();
|
bounds.contains_point(&event.position) && has_mouse_down.borrow().is_none();
|
||||||
let mut was_hovered = was_hovered.lock();
|
let mut was_hovered = was_hovered.borrow_mut();
|
||||||
|
|
||||||
if is_hovered != was_hovered.clone() {
|
if is_hovered != was_hovered.clone() {
|
||||||
*was_hovered = is_hovered;
|
*was_hovered = is_hovered;
|
||||||
@ -954,13 +945,13 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
let is_hovered =
|
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 {
|
if !is_hovered {
|
||||||
active_tooltip.lock().take();
|
active_tooltip.borrow_mut().take();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if active_tooltip.lock().is_none() {
|
if active_tooltip.borrow().is_none() {
|
||||||
let task = cx.spawn({
|
let task = cx.spawn({
|
||||||
let active_tooltip = active_tooltip.clone();
|
let active_tooltip = active_tooltip.clone();
|
||||||
let tooltip_builder = tooltip_builder.clone();
|
let tooltip_builder = tooltip_builder.clone();
|
||||||
@ -968,11 +959,11 @@ where
|
|||||||
move |view, mut cx| async move {
|
move |view, mut cx| async move {
|
||||||
cx.background_executor().timer(TOOLTIP_DELAY).await;
|
cx.background_executor().timer(TOOLTIP_DELAY).await;
|
||||||
view.update(&mut cx, move |view_state, cx| {
|
view.update(&mut cx, move |view_state, cx| {
|
||||||
active_tooltip.lock().replace(ActiveTooltip {
|
active_tooltip.borrow_mut().replace(ActiveTooltip {
|
||||||
waiting: None,
|
waiting: None,
|
||||||
tooltip: Some(AnyTooltip {
|
tooltip: Some(AnyTooltip {
|
||||||
view: tooltip_builder(view_state, cx),
|
view: tooltip_builder(view_state, cx),
|
||||||
cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET,
|
cursor_offset: cx.mouse_position(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@ -980,14 +971,14 @@ where
|
|||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
active_tooltip.lock().replace(ActiveTooltip {
|
active_tooltip.borrow_mut().replace(ActiveTooltip {
|
||||||
waiting: Some(task),
|
waiting: Some(task),
|
||||||
tooltip: None,
|
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() {
|
if active_tooltip.tooltip.is_some() {
|
||||||
cx.active_tooltip = active_tooltip.tooltip.clone()
|
cx.active_tooltip = active_tooltip.tooltip.clone()
|
||||||
}
|
}
|
||||||
@ -995,10 +986,10 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
let active_state = element_state.clicked_state.clone();
|
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| {
|
cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| {
|
||||||
if phase == DispatchPhase::Capture {
|
if phase == DispatchPhase::Capture {
|
||||||
*active_state.lock() = ElementClickedState::default();
|
*active_state.borrow_mut() = ElementClickedState::default();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1013,7 +1004,7 @@ where
|
|||||||
.map_or(false, |bounds| bounds.contains_point(&down.position));
|
.map_or(false, |bounds| bounds.contains_point(&down.position));
|
||||||
let element = bounds.contains_point(&down.position);
|
let element = bounds.contains_point(&down.position);
|
||||||
if group || element {
|
if group || element {
|
||||||
*active_state.lock() = ElementClickedState { group, element };
|
*active_state.borrow_mut() = ElementClickedState { group, element };
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1024,14 +1015,14 @@ where
|
|||||||
if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll {
|
if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll {
|
||||||
let scroll_offset = element_state
|
let scroll_offset = element_state
|
||||||
.scroll_offset
|
.scroll_offset
|
||||||
.get_or_insert_with(Arc::default)
|
.get_or_insert_with(Rc::default)
|
||||||
.clone();
|
.clone();
|
||||||
let line_height = cx.line_height();
|
let line_height = cx.line_height();
|
||||||
let scroll_max = (content_size - bounds.size).max(&Size::default());
|
let scroll_max = (content_size - bounds.size).max(&Size::default());
|
||||||
|
|
||||||
cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| {
|
cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| {
|
||||||
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
|
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 old_scroll_offset = *scroll_offset;
|
||||||
let delta = event.delta.pixel_delta(line_height);
|
let delta = event.delta.pixel_delta(line_height);
|
||||||
|
|
||||||
@ -1060,7 +1051,7 @@ where
|
|||||||
let scroll_offset = element_state
|
let scroll_offset = element_state
|
||||||
.scroll_offset
|
.scroll_offset
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|scroll_offset| *scroll_offset.lock());
|
.map(|scroll_offset| *scroll_offset.borrow());
|
||||||
|
|
||||||
cx.with_key_dispatch(
|
cx.with_key_dispatch(
|
||||||
self.key_context.clone(),
|
self.key_context.clone(),
|
||||||
@ -1110,10 +1101,6 @@ where
|
|||||||
style.refine(&self.base_style);
|
style.refine(&self.base_style);
|
||||||
|
|
||||||
if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
|
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) {
|
if focus_handle.within_focused(cx) {
|
||||||
style.refine(&self.in_focus_style);
|
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 clicked_state.group {
|
||||||
if let Some(group) = self.group_active_style.as_ref() {
|
if let Some(group) = self.group_active_style.as_ref() {
|
||||||
style.refine(&group.style)
|
style.refine(&group.style)
|
||||||
@ -1186,7 +1173,6 @@ impl<V: 'static> Default for Interactivity<V> {
|
|||||||
group: None,
|
group: None,
|
||||||
base_style: StyleRefinement::default(),
|
base_style: StyleRefinement::default(),
|
||||||
focus_style: StyleRefinement::default(),
|
focus_style: StyleRefinement::default(),
|
||||||
focus_in_style: StyleRefinement::default(),
|
|
||||||
in_focus_style: StyleRefinement::default(),
|
in_focus_style: StyleRefinement::default(),
|
||||||
hover_style: StyleRefinement::default(),
|
hover_style: StyleRefinement::default(),
|
||||||
group_hover_style: None,
|
group_hover_style: None,
|
||||||
@ -1213,11 +1199,11 @@ impl<V: 'static> Default for Interactivity<V> {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct InteractiveElementState {
|
pub struct InteractiveElementState {
|
||||||
pub focus_handle: Option<FocusHandle>,
|
pub focus_handle: Option<FocusHandle>,
|
||||||
pub clicked_state: Arc<Mutex<ElementClickedState>>,
|
pub clicked_state: Rc<RefCell<ElementClickedState>>,
|
||||||
pub hover_state: Arc<Mutex<bool>>,
|
pub hover_state: Rc<RefCell<bool>>,
|
||||||
pub pending_mouse_down: Arc<Mutex<Option<MouseDownEvent>>>,
|
pub pending_mouse_down: Rc<RefCell<Option<MouseDownEvent>>>,
|
||||||
pub scroll_offset: Option<Arc<Mutex<Point<Pixels>>>>,
|
pub scroll_offset: Option<Rc<RefCell<Point<Pixels>>>>,
|
||||||
pub active_tooltip: Arc<Mutex<Option<ActiveTooltip>>>,
|
pub active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ActiveTooltip {
|
pub struct ActiveTooltip {
|
||||||
@ -1307,21 +1293,12 @@ where
|
|||||||
self.element.element_id()
|
self.element.element_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
view_state: &mut V,
|
view_state: &mut V,
|
||||||
element_state: Option<Self::ElementState>,
|
element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> Self::ElementState {
|
) -> (LayoutId, 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 {
|
|
||||||
self.element.layout(view_state, element_state, cx)
|
self.element.layout(view_state, element_state, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1402,21 +1379,12 @@ where
|
|||||||
self.element.element_id()
|
self.element.element_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
view_state: &mut V,
|
view_state: &mut V,
|
||||||
element_state: Option<Self::ElementState>,
|
element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> Self::ElementState {
|
) -> (LayoutId, 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 {
|
|
||||||
self.element.layout(view_state, element_state, cx)
|
self.element.layout(view_state, element_state, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,21 +48,12 @@ impl<V> Element<V> for Img<V> {
|
|||||||
self.interactivity.element_id.clone()
|
self.interactivity.element_id.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
_view_state: &mut V,
|
_view_state: &mut V,
|
||||||
element_state: Option<Self::ElementState>,
|
element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> Self::ElementState {
|
) -> (LayoutId, 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 {
|
|
||||||
self.interactivity.layout(element_state, cx, |style, cx| {
|
self.interactivity.layout(element_state, cx, |style, cx| {
|
||||||
cx.request_layout(&style, None)
|
cx.request_layout(&style, None)
|
||||||
})
|
})
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
mod div;
|
mod div;
|
||||||
mod img;
|
mod img;
|
||||||
|
mod overlay;
|
||||||
mod svg;
|
mod svg;
|
||||||
mod text;
|
mod text;
|
||||||
mod uniform_list;
|
mod uniform_list;
|
||||||
|
|
||||||
pub use div::*;
|
pub use div::*;
|
||||||
pub use img::*;
|
pub use img::*;
|
||||||
|
pub use overlay::*;
|
||||||
pub use svg::*;
|
pub use svg::*;
|
||||||
pub use text::*;
|
pub use text::*;
|
||||||
pub use uniform_list::*;
|
pub use uniform_list::*;
|
||||||
|
203
crates/gpui2/src/elements/overlay.rs
Normal file
203
crates/gpui2/src/elements/overlay.rs
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -37,21 +37,12 @@ impl<V> Element<V> for Svg<V> {
|
|||||||
self.interactivity.element_id.clone()
|
self.interactivity.element_id.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
_view_state: &mut V,
|
_view_state: &mut V,
|
||||||
element_state: Option<Self::ElementState>,
|
element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> Self::ElementState {
|
) -> (LayoutId, 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 {
|
|
||||||
self.interactivity.layout(element_state, cx, |style, cx| {
|
self.interactivity.layout(element_state, cx, |style, cx| {
|
||||||
cx.request_layout(&style, None)
|
cx.request_layout(&style, None)
|
||||||
})
|
})
|
||||||
|
@ -76,21 +76,13 @@ impl<V: 'static> Element<V> for Text<V> {
|
|||||||
None
|
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(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
_view: &mut V,
|
_view: &mut V,
|
||||||
element_state: &mut Self::ElementState,
|
element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
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_system = cx.text_system().clone();
|
||||||
let text_style = cx.text_style();
|
let text_style = cx.text_style();
|
||||||
let font_size = text_style.font_size.to_pixels(cx.rem_size());
|
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(
|
fn paint(
|
||||||
|
@ -3,9 +3,8 @@ use crate::{
|
|||||||
ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels,
|
ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels,
|
||||||
Point, Size, StyleRefinement, Styled, ViewContext,
|
Point, Size, StyleRefinement, Styled, ViewContext,
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
|
||||||
use smallvec::SmallVec;
|
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;
|
use taffy::style::Overflow;
|
||||||
|
|
||||||
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
|
/// 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)]
|
#[derive(Clone, Default)]
|
||||||
pub struct UniformListScrollHandle(Arc<Mutex<Option<ScrollHandleState>>>);
|
pub struct UniformListScrollHandle(Rc<RefCell<Option<ScrollHandleState>>>);
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct ScrollHandleState {
|
struct ScrollHandleState {
|
||||||
item_height: Pixels,
|
item_height: Pixels,
|
||||||
list_height: Pixels,
|
list_height: Pixels,
|
||||||
scroll_offset: Arc<Mutex<Point<Pixels>>>,
|
scroll_offset: Rc<RefCell<Point<Pixels>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UniformListScrollHandle {
|
impl UniformListScrollHandle {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self(Arc::new(Mutex::new(None)))
|
Self(Rc::new(RefCell::new(None)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll_to_item(&self, ix: usize) {
|
pub fn scroll_to_item(&self, ix: usize) {
|
||||||
if let Some(state) = &*self.0.lock() {
|
if let Some(state) = &*self.0.borrow() {
|
||||||
let mut scroll_offset = state.scroll_offset.lock();
|
let mut scroll_offset = state.scroll_offset.borrow_mut();
|
||||||
let item_top = state.item_height * ix;
|
let item_top = state.item_height * ix;
|
||||||
let item_bottom = item_top + state.item_height;
|
let item_bottom = item_top + state.item_height;
|
||||||
let scroll_top = -scroll_offset.y;
|
let scroll_top = -scroll_offset.y;
|
||||||
@ -109,62 +108,54 @@ impl<V: 'static> Element<V> for UniformList<V> {
|
|||||||
Some(self.id.clone())
|
Some(self.id.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
view_state: &mut V,
|
view_state: &mut V,
|
||||||
element_state: Option<Self::ElementState>,
|
element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> Self::ElementState {
|
) -> (LayoutId, 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 {
|
|
||||||
let max_items = self.item_count;
|
let max_items = self.item_count;
|
||||||
let item_size = element_state.item_size;
|
|
||||||
let rem_size = cx.rem_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
|
let (layout_id, interactive) =
|
||||||
.layout(&mut element_state.interactive, cx, |style, cx| {
|
self.interactivity
|
||||||
cx.request_measured_layout(
|
.layout(element_state.map(|s| s.interactive), cx, |style, cx| {
|
||||||
style,
|
cx.request_measured_layout(
|
||||||
rem_size,
|
style,
|
||||||
move |known_dimensions: Size<Option<Pixels>>,
|
rem_size,
|
||||||
available_space: Size<AvailableSpace>| {
|
move |known_dimensions: Size<Option<Pixels>>,
|
||||||
let desired_height = item_size.height * max_items;
|
available_space: Size<AvailableSpace>| {
|
||||||
let width = known_dimensions
|
let desired_height = item_size.height * max_items;
|
||||||
.width
|
let width =
|
||||||
.unwrap_or(match available_space.width {
|
known_dimensions
|
||||||
AvailableSpace::Definite(x) => x,
|
.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 => {
|
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||||
item_size.width
|
desired_height
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
let height = match available_space.height {
|
size(width, height)
|
||||||
AvailableSpace::Definite(x) => desired_height.min(x),
|
},
|
||||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
)
|
||||||
desired_height
|
});
|
||||||
}
|
|
||||||
};
|
let element_state = UniformListState {
|
||||||
size(width, height)
|
interactive,
|
||||||
},
|
item_size,
|
||||||
)
|
};
|
||||||
})
|
|
||||||
|
(layout_id, element_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
@ -196,7 +187,7 @@ impl<V: 'static> Element<V> for UniformList<V> {
|
|||||||
let shared_scroll_offset = element_state
|
let shared_scroll_offset = element_state
|
||||||
.interactive
|
.interactive
|
||||||
.scroll_offset
|
.scroll_offset
|
||||||
.get_or_insert_with(Arc::default)
|
.get_or_insert_with(Rc::default)
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
interactivity.paint(
|
interactivity.paint(
|
||||||
@ -222,7 +213,7 @@ impl<V: 'static> Element<V> for UniformList<V> {
|
|||||||
.measure_item(view_state, Some(padded_bounds.size.width), cx)
|
.measure_item(view_state, Some(padded_bounds.size.width), cx)
|
||||||
.height;
|
.height;
|
||||||
if let Some(scroll_handle) = self.scroll_handle.clone() {
|
if let Some(scroll_handle) = self.scroll_handle.clone() {
|
||||||
scroll_handle.0.lock().replace(ScrollHandleState {
|
scroll_handle.0.borrow_mut().replace(ScrollHandleState {
|
||||||
item_height,
|
item_height,
|
||||||
list_height: padded_bounds.size.height,
|
list_height: padded_bounds.size.height,
|
||||||
scroll_offset: shared_scroll_offset,
|
scroll_offset: shared_scroll_offset,
|
||||||
|
@ -335,6 +335,10 @@ where
|
|||||||
};
|
};
|
||||||
Bounds { origin, size }
|
Bounds { origin, size }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new(origin: Point<T>, size: Size<T>) -> Self {
|
||||||
|
Bounds { origin, size }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Bounds<T>
|
impl<T> Bounds<T>
|
||||||
@ -421,6 +425,22 @@ impl<T> Bounds<T>
|
|||||||
where
|
where
|
||||||
T: Add<T, Output = T> + Clone + Default + Debug,
|
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> {
|
pub fn upper_right(&self) -> Point<T> {
|
||||||
Point {
|
Point {
|
||||||
x: self.origin.x.clone() + self.size.width.clone(),
|
x: self.origin.x.clone() + self.size.width.clone(),
|
||||||
|
@ -135,6 +135,10 @@ pub trait VisualContext: Context {
|
|||||||
) -> Self::Result<View<V>>
|
) -> Self::Result<View<V>>
|
||||||
where
|
where
|
||||||
V: Render;
|
V: Render;
|
||||||
|
|
||||||
|
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||||
|
where
|
||||||
|
V: FocusableView;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Entity<T>: Sealed {
|
pub trait Entity<T>: Sealed {
|
||||||
|
@ -337,7 +337,6 @@ mod test {
|
|||||||
.update(cx, |test_view, cx| cx.focus(&test_view.focus_handle))
|
.update(cx, |test_view, cx| cx.focus(&test_view.focus_handle))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
cx.dispatch_keystroke(*window, Keystroke::parse("space").unwrap(), false);
|
|
||||||
cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap(), false);
|
cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap(), false);
|
||||||
|
|
||||||
window
|
window
|
||||||
|
@ -60,7 +60,7 @@ impl DispatchTree {
|
|||||||
self.keystroke_matchers.clear();
|
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 parent = self.node_stack.last().copied();
|
||||||
let node_id = DispatchNodeId(self.nodes.len());
|
let node_id = DispatchNodeId(self.nodes.len());
|
||||||
self.nodes.push(DispatchNode {
|
self.nodes.push(DispatchNode {
|
||||||
@ -71,12 +71,6 @@ impl DispatchTree {
|
|||||||
if !context.is_empty() {
|
if !context.is_empty() {
|
||||||
self.active_node().context = context.clone();
|
self.active_node().context = context.clone();
|
||||||
self.context_stack.push(context);
|
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) {
|
pub fn on_key_event(&mut self, listener: KeyListener) {
|
||||||
self.active_node().key_listeners.push(listener);
|
self.active_node().key_listeners.push(listener);
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ pub struct TestWindow {
|
|||||||
bounds: WindowBounds,
|
bounds: WindowBounds,
|
||||||
current_scene: Mutex<Option<Scene>>,
|
current_scene: Mutex<Option<Scene>>,
|
||||||
display: Rc<dyn PlatformDisplay>,
|
display: Rc<dyn PlatformDisplay>,
|
||||||
input_handler: Option<Box<dyn PlatformInputHandler>>,
|
pub(crate) input_handler: Option<Arc<Mutex<Box<dyn PlatformInputHandler>>>>,
|
||||||
handlers: Mutex<Handlers>,
|
handlers: Mutex<Handlers>,
|
||||||
platform: Weak<TestPlatform>,
|
platform: Weak<TestPlatform>,
|
||||||
sprite_atlas: Arc<dyn PlatformAtlas>,
|
sprite_atlas: Arc<dyn PlatformAtlas>,
|
||||||
@ -80,11 +80,11 @@ impl PlatformWindow for TestWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
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>) {
|
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(
|
fn prompt(
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
|
private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
|
||||||
BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId,
|
BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, FocusHandle,
|
||||||
Model, Pixels, Size, ViewContext, VisualContext, WeakModel, WindowContext,
|
FocusableView, LayoutId, Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel,
|
||||||
|
WindowContext,
|
||||||
};
|
};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::{
|
use std::{
|
||||||
@ -63,6 +64,23 @@ impl<V: 'static> View<V> {
|
|||||||
pub fn read<'a>(&self, cx: &'a AppContext) -> &'a V {
|
pub fn read<'a>(&self, cx: &'a AppContext) -> &'a V {
|
||||||
self.model.read(cx)
|
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> {
|
impl<V> Clone for View<V> {
|
||||||
@ -145,8 +163,7 @@ impl<V> Eq for WeakView<V> {}
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct AnyView {
|
pub struct AnyView {
|
||||||
model: AnyModel,
|
model: AnyModel,
|
||||||
initialize: fn(&AnyView, &mut WindowContext) -> AnyBox,
|
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
|
||||||
layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId,
|
|
||||||
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
|
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +171,6 @@ impl AnyView {
|
|||||||
pub fn downgrade(&self) -> AnyWeakView {
|
pub fn downgrade(&self) -> AnyWeakView {
|
||||||
AnyWeakView {
|
AnyWeakView {
|
||||||
model: self.model.downgrade(),
|
model: self.model.downgrade(),
|
||||||
initialize: self.initialize,
|
|
||||||
layout: self.layout,
|
layout: self.layout,
|
||||||
paint: self.paint,
|
paint: self.paint,
|
||||||
}
|
}
|
||||||
@ -165,7 +181,6 @@ impl AnyView {
|
|||||||
Ok(model) => Ok(View { model }),
|
Ok(model) => Ok(View { model }),
|
||||||
Err(model) => Err(Self {
|
Err(model) => Err(Self {
|
||||||
model,
|
model,
|
||||||
initialize: self.initialize,
|
|
||||||
layout: self.layout,
|
layout: self.layout,
|
||||||
paint: self.paint,
|
paint: self.paint,
|
||||||
}),
|
}),
|
||||||
@ -176,13 +191,19 @@ impl AnyView {
|
|||||||
self.model.entity_type
|
self.model.entity_type
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn draw(&self, available_space: Size<AvailableSpace>, cx: &mut WindowContext) {
|
pub(crate) fn draw(
|
||||||
let mut rendered_element = (self.initialize)(self, cx);
|
&self,
|
||||||
let layout_id = (self.layout)(self, &mut rendered_element, cx);
|
origin: Point<Pixels>,
|
||||||
cx.window
|
available_space: Size<AvailableSpace>,
|
||||||
.layout_engine
|
cx: &mut WindowContext,
|
||||||
.compute_layout(layout_id, available_space);
|
) {
|
||||||
(self.paint)(self, &mut rendered_element, cx);
|
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 {
|
fn from(value: View<V>) -> Self {
|
||||||
AnyView {
|
AnyView {
|
||||||
model: value.model.into_any(),
|
model: value.model.into_any(),
|
||||||
initialize: any_view::initialize::<V>,
|
|
||||||
layout: any_view::layout::<V>,
|
layout: any_view::layout::<V>,
|
||||||
paint: any_view::paint::<V>,
|
paint: any_view::paint::<V>,
|
||||||
}
|
}
|
||||||
@ -210,22 +230,13 @@ impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
|
|||||||
Some(self.model.entity_id.into())
|
Some(self.model.entity_id.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
_view_state: &mut ParentViewState,
|
_view_state: &mut ParentViewState,
|
||||||
_element_state: Option<Self::ElementState>,
|
_element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<ParentViewState>,
|
cx: &mut ViewContext<ParentViewState>,
|
||||||
) -> Self::ElementState {
|
) -> (LayoutId, Self::ElementState) {
|
||||||
(self.initialize)(self, cx)
|
(self.layout)(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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
@ -241,8 +252,7 @@ impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
|
|||||||
|
|
||||||
pub struct AnyWeakView {
|
pub struct AnyWeakView {
|
||||||
model: AnyWeakModel,
|
model: AnyWeakModel,
|
||||||
initialize: fn(&AnyView, &mut WindowContext) -> AnyBox,
|
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
|
||||||
layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId,
|
|
||||||
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
|
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,7 +261,6 @@ impl AnyWeakView {
|
|||||||
let model = self.model.upgrade()?;
|
let model = self.model.upgrade()?;
|
||||||
Some(AnyView {
|
Some(AnyView {
|
||||||
model,
|
model,
|
||||||
initialize: self.initialize,
|
|
||||||
layout: self.layout,
|
layout: self.layout,
|
||||||
paint: self.paint,
|
paint: self.paint,
|
||||||
})
|
})
|
||||||
@ -262,7 +271,6 @@ impl<V: Render> From<WeakView<V>> for AnyWeakView {
|
|||||||
fn from(view: WeakView<V>) -> Self {
|
fn from(view: WeakView<V>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
model: view.model.into(),
|
model: view.model.into(),
|
||||||
initialize: any_view::initialize::<V>,
|
|
||||||
layout: any_view::layout::<V>,
|
layout: any_view::layout::<V>,
|
||||||
paint: any_view::paint::<V>,
|
paint: any_view::paint::<V>,
|
||||||
}
|
}
|
||||||
@ -281,12 +289,12 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RenderView<C, V> {
|
pub struct RenderViewWith<C, V> {
|
||||||
view: View<V>,
|
view: View<V>,
|
||||||
component: Option<C>,
|
component: Option<C>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C, ParentViewState, ViewState> Component<ParentViewState> for RenderView<C, ViewState>
|
impl<C, ParentViewState, ViewState> Component<ParentViewState> for RenderViewWith<C, ViewState>
|
||||||
where
|
where
|
||||||
C: 'static + Component<ViewState>,
|
C: 'static + Component<ViewState>,
|
||||||
ParentViewState: 'static,
|
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
|
where
|
||||||
C: 'static + Component<ViewState>,
|
C: 'static + Component<ViewState>,
|
||||||
ParentViewState: 'static,
|
ParentViewState: 'static,
|
||||||
@ -309,29 +317,16 @@ where
|
|||||||
Some(self.view.entity_id().into())
|
Some(self.view.entity_id().into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &mut ParentViewState,
|
_: &mut ParentViewState,
|
||||||
_: Option<Self::ElementState>,
|
_: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<ParentViewState>,
|
cx: &mut ViewContext<ParentViewState>,
|
||||||
) -> Self::ElementState {
|
) -> (LayoutId, Self::ElementState) {
|
||||||
cx.with_element_id(Some(self.view.entity_id()), |cx| {
|
self.view.update(cx, |view, cx| {
|
||||||
self.view.update(cx, |view, cx| {
|
let mut element = self.component.take().unwrap().render();
|
||||||
let mut element = self.component.take().unwrap().render();
|
let layout_id = element.layout(view, cx);
|
||||||
element.initialize(view, cx);
|
(layout_id, element)
|
||||||
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))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,20 +337,7 @@ where
|
|||||||
element: &mut Self::ElementState,
|
element: &mut Self::ElementState,
|
||||||
cx: &mut ViewContext<ParentViewState>,
|
cx: &mut ViewContext<ParentViewState>,
|
||||||
) {
|
) {
|
||||||
cx.with_element_id(Some(self.view.entity_id()), |cx| {
|
self.view.update(cx, |view, cx| element.paint(view, 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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,27 +345,17 @@ mod any_view {
|
|||||||
use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
|
use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
|
||||||
use std::any::Any;
|
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>(
|
pub(crate) fn layout<V: Render>(
|
||||||
view: &AnyView,
|
view: &AnyView,
|
||||||
element: &mut Box<dyn Any>,
|
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> LayoutId {
|
) -> (LayoutId, Box<dyn Any>) {
|
||||||
cx.with_element_id(Some(view.model.entity_id), |cx| {
|
cx.with_element_id(Some(view.model.entity_id), |cx| {
|
||||||
let view = view.clone().downcast::<V>().unwrap();
|
let view = view.clone().downcast::<V>().unwrap();
|
||||||
let element = element.downcast_mut::<AnyElement<V>>().unwrap();
|
view.update(cx, |view, cx| {
|
||||||
view.update(cx, |view, cx| element.layout(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>)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.
|
// Holds the state for a specific window.
|
||||||
pub struct Window {
|
pub struct Window {
|
||||||
pub(crate) handle: AnyWindowHandle,
|
pub(crate) handle: AnyWindowHandle,
|
||||||
pub(crate) removed: bool,
|
pub(crate) removed: bool,
|
||||||
platform_window: Box<dyn PlatformWindow>,
|
pub(crate) platform_window: Box<dyn PlatformWindow>,
|
||||||
display_id: DisplayId,
|
display_id: DisplayId,
|
||||||
sprite_atlas: Arc<dyn PlatformAtlas>,
|
sprite_atlas: Arc<dyn PlatformAtlas>,
|
||||||
rem_size: Pixels,
|
rem_size: Pixels,
|
||||||
@ -216,7 +220,7 @@ pub struct Window {
|
|||||||
|
|
||||||
// #[derive(Default)]
|
// #[derive(Default)]
|
||||||
pub(crate) struct Frame {
|
pub(crate) struct Frame {
|
||||||
element_states: HashMap<GlobalElementId, AnyBox>,
|
pub(crate) element_states: HashMap<GlobalElementId, AnyBox>,
|
||||||
mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
|
mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
|
||||||
pub(crate) dispatch_tree: DispatchTree,
|
pub(crate) dispatch_tree: DispatchTree,
|
||||||
pub(crate) focus_listeners: Vec<AnyFocusListener>,
|
pub(crate) focus_listeners: Vec<AnyFocusListener>,
|
||||||
@ -393,6 +397,10 @@ impl<'a> WindowContext<'a> {
|
|||||||
|
|
||||||
/// Move focus to the element associated with the given `FocusHandle`.
|
/// Move focus to the element associated with the given `FocusHandle`.
|
||||||
pub fn focus(&mut self, handle: &FocusHandle) {
|
pub fn focus(&mut self, handle: &FocusHandle) {
|
||||||
|
if self.window.focus == Some(handle.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let focus_id = handle.id;
|
let focus_id = handle.id;
|
||||||
|
|
||||||
if self.window.last_blur.is_none() {
|
if self.window.last_blur.is_none() {
|
||||||
@ -400,6 +408,10 @@ impl<'a> WindowContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.window.focus = Some(focus_id);
|
self.window.focus = Some(focus_id);
|
||||||
|
self.window
|
||||||
|
.current_frame
|
||||||
|
.dispatch_tree
|
||||||
|
.clear_keystroke_matchers();
|
||||||
self.app.push_effect(Effect::FocusChanged {
|
self.app.push_effect(Effect::FocusChanged {
|
||||||
window_handle: self.window.handle,
|
window_handle: self.window.handle,
|
||||||
focused: Some(focus_id),
|
focused: Some(focus_id),
|
||||||
@ -1068,29 +1080,33 @@ impl<'a> WindowContext<'a> {
|
|||||||
|
|
||||||
self.with_z_index(0, |cx| {
|
self.with_z_index(0, |cx| {
|
||||||
let available_space = cx.window.viewport_size.map(Into::into);
|
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() {
|
if let Some(active_drag) = self.app.active_drag.take() {
|
||||||
self.with_z_index(1, |cx| {
|
self.with_z_index(1, |cx| {
|
||||||
let offset = cx.mouse_position() - active_drag.cursor_offset;
|
let offset = cx.mouse_position() - active_drag.cursor_offset;
|
||||||
cx.with_element_offset(offset, |cx| {
|
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||||
let available_space =
|
active_drag.view.draw(offset, available_space, cx);
|
||||||
size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
cx.active_drag = Some(active_drag);
|
||||||
active_drag.view.draw(available_space, cx);
|
|
||||||
cx.active_drag = Some(active_drag);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} else if let Some(active_tooltip) = self.app.active_tooltip.take() {
|
} else if let Some(active_tooltip) = self.app.active_tooltip.take() {
|
||||||
self.with_z_index(1, |cx| {
|
self.with_z_index(1, |cx| {
|
||||||
cx.with_element_offset(active_tooltip.cursor_offset, |cx| {
|
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||||
let available_space =
|
active_tooltip
|
||||||
size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
.view
|
||||||
active_tooltip.view.draw(available_space, cx);
|
.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);
|
self.window.root_view = Some(root_view);
|
||||||
let scene = self.window.current_frame.scene_builder.build();
|
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());
|
self.window.root_view = Some(view.clone().into());
|
||||||
view
|
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> {
|
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
|
/// Update the global element offset relative to the current offset. This is used to implement
|
||||||
/// scrolling and position drag handles.
|
/// scrolling.
|
||||||
fn with_element_offset<R>(
|
fn with_element_offset<R>(
|
||||||
&mut self,
|
&mut self,
|
||||||
offset: Point<Pixels>,
|
offset: Point<Pixels>,
|
||||||
@ -1628,7 +1650,17 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
|
|||||||
return f(self);
|
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()
|
self.window_mut()
|
||||||
.current_frame
|
.current_frame
|
||||||
.element_offset_stack
|
.element_offset_stack
|
||||||
@ -1798,8 +1830,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||||||
self.view
|
self.view
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn model(&self) -> Model<V> {
|
pub fn model(&self) -> &Model<V> {
|
||||||
self.view.model.clone()
|
&self.view.model
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Access the underlying window context.
|
/// Access the underlying window context.
|
||||||
@ -2093,7 +2125,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||||||
window
|
window
|
||||||
.current_frame
|
.current_frame
|
||||||
.dispatch_tree
|
.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() {
|
if let Some(focus_handle) = focus_handle.as_ref() {
|
||||||
window
|
window
|
||||||
.current_frame
|
.current_frame
|
||||||
@ -2131,7 +2163,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||||||
|
|
||||||
pub fn observe_global<G: 'static>(
|
pub fn observe_global<G: 'static>(
|
||||||
&mut self,
|
&mut self,
|
||||||
f: impl Fn(&mut V, &mut ViewContext<'_, V>) + 'static,
|
mut f: impl FnMut(&mut V, &mut ViewContext<'_, V>) + 'static,
|
||||||
) -> Subscription {
|
) -> Subscription {
|
||||||
let window_handle = self.window.handle;
|
let window_handle = self.window.handle;
|
||||||
let view = self.view().downgrade();
|
let view = self.view().downgrade();
|
||||||
@ -2197,9 +2229,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||||||
.set_input_handler(Box::new(input_handler));
|
.set_input_handler(Box::new(input_handler));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<V> ViewContext<'_, V> {
|
|
||||||
pub fn emit<Evt>(&mut self, event: Evt)
|
pub fn emit<Evt>(&mut self, event: Evt)
|
||||||
where
|
where
|
||||||
Evt: 'static,
|
Evt: 'static,
|
||||||
@ -2212,6 +2242,13 @@ impl<V> ViewContext<'_, V> {
|
|||||||
event: Box::new(event),
|
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> {
|
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)
|
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> {
|
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)]
|
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
pub enum ElementId {
|
pub enum ElementId {
|
||||||
View(EntityId),
|
View(EntityId),
|
||||||
Number(usize),
|
Integer(usize),
|
||||||
Name(SharedString),
|
Name(SharedString),
|
||||||
FocusHandle(FocusId),
|
FocusHandle(FocusId),
|
||||||
}
|
}
|
||||||
@ -2484,13 +2525,13 @@ impl From<EntityId> for ElementId {
|
|||||||
|
|
||||||
impl From<usize> for ElementId {
|
impl From<usize> for ElementId {
|
||||||
fn from(id: usize) -> Self {
|
fn from(id: usize) -> Self {
|
||||||
ElementId::Number(id)
|
ElementId::Integer(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<i32> for ElementId {
|
impl From<i32> for ElementId {
|
||||||
fn from(id: i32) -> Self {
|
fn from(id: i32) -> Self {
|
||||||
Self::Number(id as usize)
|
Self::Integer(id as usize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1709,6 +1709,7 @@ impl Project {
|
|||||||
self.open_remote_buffer_internal(&project_path.path, &worktree, cx)
|
self.open_remote_buffer_internal(&project_path.path, &worktree, cx)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let project_path = project_path.clone();
|
||||||
cx.spawn(move |this, mut cx| async move {
|
cx.spawn(move |this, mut cx| async move {
|
||||||
let load_result = load_buffer.await;
|
let load_result = load_buffer.await;
|
||||||
*tx.borrow_mut() = Some(this.update(&mut cx, |this, _| {
|
*tx.borrow_mut() = Some(this.update(&mut cx, |this, _| {
|
||||||
@ -1726,7 +1727,7 @@ impl Project {
|
|||||||
cx.foreground().spawn(async move {
|
cx.foreground().spawn(async move {
|
||||||
wait_for_loading_buffer(loading_watch)
|
wait_for_loading_buffer(loading_watch)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| anyhow!("{}", error))
|
.map_err(|error| anyhow!("{project_path:?} opening failure: {error:#}"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3694,7 +3694,7 @@ impl BackgroundScanner {
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// TODO - create a special 'error' entry in the entries tree to mark this
|
// 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:#}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1741,6 +1741,7 @@ impl Project {
|
|||||||
self.open_remote_buffer_internal(&project_path.path, &worktree, cx)
|
self.open_remote_buffer_internal(&project_path.path, &worktree, cx)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let project_path = project_path.clone();
|
||||||
cx.spawn(move |this, mut cx| async move {
|
cx.spawn(move |this, mut cx| async move {
|
||||||
let load_result = load_buffer.await;
|
let load_result = load_buffer.await;
|
||||||
*tx.borrow_mut() = Some(this.update(&mut cx, |this, _| {
|
*tx.borrow_mut() = Some(this.update(&mut cx, |this, _| {
|
||||||
@ -1759,7 +1760,7 @@ impl Project {
|
|||||||
cx.background_executor().spawn(async move {
|
cx.background_executor().spawn(async move {
|
||||||
wait_for_loading_buffer(loading_watch)
|
wait_for_loading_buffer(loading_watch)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| anyhow!("{}", error))
|
.map_err(|error| anyhow!("{project_path:?} opening failure: {error:#}"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3684,7 +3684,7 @@ impl BackgroundScanner {
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// TODO - create a special 'error' entry in the entries tree to mark this
|
// 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:#}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,10 @@ use file_associations::FileAssociations;
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
|
actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
|
||||||
ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, InteractiveComponent,
|
ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, FocusableView,
|
||||||
Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render, Stateful,
|
InteractiveComponent, Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render,
|
||||||
StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View, ViewContext,
|
Stateful, StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View,
|
||||||
VisualContext as _, WeakView, WindowContext,
|
ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use menu::{Confirm, SelectNext, SelectPrev};
|
use menu::{Confirm, SelectNext, SelectPrev};
|
||||||
use project::{
|
use project::{
|
||||||
@ -32,7 +32,7 @@ use std::{
|
|||||||
use theme::ActiveTheme as _;
|
use theme::ActiveTheme as _;
|
||||||
use ui::{h_stack, v_stack, IconElement, Label};
|
use ui::{h_stack, v_stack, IconElement, Label};
|
||||||
use unicase::UniCase;
|
use unicase::UniCase;
|
||||||
use util::{maybe, TryFutureExt};
|
use util::{maybe, ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, PanelEvent},
|
dock::{DockPosition, PanelEvent},
|
||||||
Workspace,
|
Workspace,
|
||||||
@ -130,6 +130,13 @@ pub fn init_settings(cx: &mut AppContext) {
|
|||||||
pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
|
pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
|
||||||
init_settings(cx);
|
init_settings(cx);
|
||||||
file_associations::init(assets, 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)]
|
#[derive(Debug)]
|
||||||
@ -303,32 +310,31 @@ impl ProjectPanel {
|
|||||||
project_panel
|
project_panel
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(
|
pub async fn load(
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
cx: AsyncWindowContext,
|
mut cx: AsyncWindowContext,
|
||||||
) -> Task<Result<View<Self>>> {
|
) -> Result<View<Self>> {
|
||||||
cx.spawn(|mut cx| async move {
|
let serialized_panel = cx
|
||||||
// let serialized_panel = if let Some(panel) = cx
|
.background_executor()
|
||||||
// .background_executor()
|
.spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
|
||||||
// .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
|
.await
|
||||||
// .await
|
.map_err(|e| anyhow!("Failed to load project panel: {}", e))
|
||||||
// .log_err()
|
.log_err()
|
||||||
// .flatten()
|
.flatten()
|
||||||
// {
|
.map(|panel| serde_json::from_str::<SerializedProjectPanel>(&panel))
|
||||||
// Some(serde_json::from_str::<SerializedProjectPanel>(&panel)?)
|
.transpose()
|
||||||
// } else {
|
.log_err()
|
||||||
// None
|
.flatten();
|
||||||
// };
|
|
||||||
workspace.update(&mut cx, |workspace, cx| {
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
let panel = ProjectPanel::new(workspace, cx);
|
let panel = ProjectPanel::new(workspace, cx);
|
||||||
// if let Some(serialized_panel) = serialized_panel {
|
if let Some(serialized_panel) = serialized_panel {
|
||||||
// panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
// panel.width = serialized_panel.width;
|
panel.width = serialized_panel.width;
|
||||||
// cx.notify();
|
cx.notify();
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
panel
|
panel
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1516,33 +1522,27 @@ impl workspace::dock::Panel for ProjectPanel {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
|
fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
|
||||||
Some("icons/project.svg")
|
Some(ui::Icon::FileTree)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
|
fn toggle_action(&self) -> Box<dyn Action> {
|
||||||
("Project Panel".into(), Some(Box::new(ToggleFocus)))
|
Box::new(ToggleFocus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fn should_change_position_on_event(event: &Self::Event) -> bool {
|
|
||||||
// matches!(event, Event::DockPositionChanged)
|
|
||||||
// }
|
|
||||||
|
|
||||||
fn has_focus(&self, _: &WindowContext) -> bool {
|
fn has_focus(&self, _: &WindowContext) -> bool {
|
||||||
self.has_focus
|
self.has_focus
|
||||||
}
|
}
|
||||||
|
|
||||||
fn persistent_name(&self) -> &'static str {
|
fn persistent_name() -> &'static str {
|
||||||
"Project Panel"
|
"Project Panel"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn focus_handle(&self, _cx: &WindowContext) -> FocusHandle {
|
impl FocusableView for ProjectPanel {
|
||||||
|
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||||
self.focus_handle.clone()
|
self.focus_handle.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
// fn is_focus_event(event: &Self::Event) -> bool {
|
|
||||||
// matches!(event, Event::Focus)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClipboardEntry {
|
impl ClipboardEntry {
|
||||||
|
@ -9,7 +9,7 @@ use schemars::{
|
|||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use util::asset_str;
|
use util::{asset_str, ResultExt};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
|
#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
@ -86,9 +86,7 @@ impl KeymapFile {
|
|||||||
"invalid binding value for keystroke {keystroke}, context {context:?}"
|
"invalid binding value for keystroke {keystroke}, context {context:?}"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
// todo!()
|
.log_err()
|
||||||
.ok()
|
|
||||||
// .log_err()
|
|
||||||
.map(|action| KeyBinding::load(&keystroke, action, context.as_deref()))
|
.map(|action| KeyBinding::load(&keystroke, action, context.as_deref()))
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>>>()?;
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
@ -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 StaticColumnCount for u32 {}
|
||||||
impl Bind for u32 {
|
impl Bind for u32 {
|
||||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||||
|
@ -57,7 +57,6 @@ impl Render for FocusStory {
|
|||||||
.size_full()
|
.size_full()
|
||||||
.bg(color_1)
|
.bg(color_1)
|
||||||
.focus(|style| style.bg(color_2))
|
.focus(|style| style.bg(color_2))
|
||||||
.focus_in(|style| style.bg(color_3))
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.track_focus(&self.child_1_focus)
|
.track_focus(&self.child_1_focus)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};
|
use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};
|
||||||
use theme2::ActiveTheme;
|
use theme2::ActiveTheme;
|
||||||
|
use ui::Tooltip;
|
||||||
|
|
||||||
pub struct ScrollStory;
|
pub struct ScrollStory;
|
||||||
|
|
||||||
@ -35,16 +36,18 @@ impl Render for ScrollStory {
|
|||||||
} else {
|
} else {
|
||||||
color_2
|
color_2
|
||||||
};
|
};
|
||||||
div().id(id).bg(bg).size(px(100. as f32)).when(
|
div()
|
||||||
row >= 5 && column >= 5,
|
.id(id)
|
||||||
|d| {
|
.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()
|
d.overflow_scroll()
|
||||||
.child(div().size(px(50.)).bg(color_1))
|
.child(div().size(px(50.)).bg(color_1))
|
||||||
.child(div().size(px(50.)).bg(color_2))
|
.child(div().size(px(50.)).bg(color_2))
|
||||||
.child(div().size(px(50.)).bg(color_1))
|
.child(div().size(px(50.)).bg(color_1))
|
||||||
.child(div().size(px(50.)).bg(color_2))
|
.child(div().size(px(50.)).bg(color_2))
|
||||||
},
|
})
|
||||||
)
|
|
||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use gpui::{AppContext, FontFeatures};
|
use gpui::{AppContext, FontFeatures, Pixels};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
@ -15,7 +15,7 @@ pub enum TerminalDockPosition {
|
|||||||
pub struct TerminalSettings {
|
pub struct TerminalSettings {
|
||||||
pub shell: Shell,
|
pub shell: Shell,
|
||||||
pub working_directory: WorkingDirectory,
|
pub working_directory: WorkingDirectory,
|
||||||
font_size: Option<f32>,
|
pub font_size: Option<Pixels>,
|
||||||
pub font_family: Option<String>,
|
pub font_family: Option<String>,
|
||||||
pub line_height: TerminalLineHeight,
|
pub line_height: TerminalLineHeight,
|
||||||
pub font_features: Option<FontFeatures>,
|
pub font_features: Option<FontFeatures>,
|
||||||
@ -90,14 +90,6 @@ pub struct TerminalSettingsContent {
|
|||||||
pub detect_venv: Option<VenvSettings>,
|
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 {
|
impl settings::Settings for TerminalSettings {
|
||||||
const KEY: Option<&'static str> = Some("terminal");
|
const KEY: Option<&'static str> = Some("terminal");
|
||||||
|
|
||||||
|
45
crates/terminal_view2/Cargo.toml
Normal file
45
crates/terminal_view2/Cargo.toml
Normal 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
|
23
crates/terminal_view2/README.md
Normal file
23
crates/terminal_view2/README.md
Normal 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
|
96
crates/terminal_view2/scripts/print256color.sh
Executable file
96
crates/terminal_view2/scripts/print256color.sh
Executable 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
|
19
crates/terminal_view2/scripts/truecolor.sh
Executable file
19
crates/terminal_view2/scripts/truecolor.sh
Executable 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";
|
||||||
|
}'
|
71
crates/terminal_view2/src/persistence.rs
Normal file
71
crates/terminal_view2/src/persistence.rs
Normal 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 = ?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
954
crates/terminal_view2/src/terminal_element.rs
Normal file
954
crates/terminal_view2/src/terminal_element.rs
Normal 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))
|
||||||
|
// }
|
446
crates/terminal_view2/src/terminal_panel.rs
Normal file
446
crates/terminal_view2/src/terminal_panel.rs
Normal 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>,
|
||||||
|
}
|
1202
crates/terminal_view2/src/terminal_view.rs
Normal file
1202
crates/terminal_view2/src/terminal_view.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,7 @@ anyhow.workspace = true
|
|||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { package = "gpui2", path = "../gpui2" }
|
||||||
itertools = { version = "0.11.0", optional = true }
|
itertools = { version = "0.11.0", optional = true }
|
||||||
|
menu = { package = "menu2", path = "../menu2"}
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
settings2 = { path = "../settings2" }
|
settings2 = { path = "../settings2" }
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
|
@ -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> {
|
struct ButtonHandlers<V: 'static> {
|
||||||
click: Option<ClickHandler<V>>,
|
click: Option<ClickHandler<V>>,
|
||||||
|
@ -3,17 +3,29 @@ use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHea
|
|||||||
|
|
||||||
pub enum ContextMenuItem {
|
pub enum ContextMenuItem {
|
||||||
Header(SharedString),
|
Header(SharedString),
|
||||||
Entry(Label),
|
Entry(Label, Box<dyn gpui::Action>),
|
||||||
Separator,
|
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 {
|
impl ContextMenuItem {
|
||||||
fn to_list_item<V: 'static>(self) -> ListItem {
|
fn to_list_item<V: 'static>(self) -> ListItem {
|
||||||
match self {
|
match self {
|
||||||
ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
|
ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
|
||||||
ContextMenuItem::Entry(label) => {
|
ContextMenuItem::Entry(label, action) => ListEntry::new(label)
|
||||||
ListEntry::new(label).variant(ListItemVariant::Inset).into()
|
.variant(ListItemVariant::Inset)
|
||||||
}
|
.on_click(action)
|
||||||
|
.into(),
|
||||||
ContextMenuItem::Separator => ListSeparator::new().into(),
|
ContextMenuItem::Separator => ListSeparator::new().into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -26,12 +38,12 @@ impl ContextMenuItem {
|
|||||||
Self::Separator
|
Self::Separator
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn entry(label: Label) -> Self {
|
pub fn entry(label: Label, action: impl Action) -> Self {
|
||||||
Self::Entry(label)
|
Self::Entry(label, Box::new(action))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component, Clone)]
|
||||||
pub struct ContextMenu {
|
pub struct ContextMenu {
|
||||||
items: Vec<ContextMenuItem>,
|
items: Vec<ContextMenuItem>,
|
||||||
}
|
}
|
||||||
@ -42,7 +54,12 @@ impl ContextMenu {
|
|||||||
items: items.into_iter().collect(),
|
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> {
|
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||||
v_stack()
|
v_stack()
|
||||||
.flex()
|
.flex()
|
||||||
@ -55,9 +72,11 @@ impl ContextMenu {
|
|||||||
.map(ContextMenuItem::to_list_item::<V>)
|
.map(ContextMenuItem::to_list_item::<V>)
|
||||||
.collect(),
|
.collect(),
|
||||||
))
|
))
|
||||||
|
.on_mouse_down_out(|_, _, cx| cx.dispatch_action(Box::new(menu::Cancel)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use gpui::Action;
|
||||||
#[cfg(feature = "stories")]
|
#[cfg(feature = "stories")]
|
||||||
pub use stories::*;
|
pub use stories::*;
|
||||||
|
|
||||||
@ -65,7 +84,7 @@ pub use stories::*;
|
|||||||
mod stories {
|
mod stories {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::story::Story;
|
use crate::story::Story;
|
||||||
use gpui::{Div, Render};
|
use gpui::{action, Div, Render};
|
||||||
|
|
||||||
pub struct ContextMenuStory;
|
pub struct ContextMenuStory;
|
||||||
|
|
||||||
@ -73,14 +92,22 @@ mod stories {
|
|||||||
type Element = Div<Self>;
|
type Element = Div<Self>;
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
|
#[action]
|
||||||
|
struct PrintCurrentDate {}
|
||||||
|
|
||||||
Story::container(cx)
|
Story::container(cx)
|
||||||
.child(Story::title_for::<_, ContextMenu>(cx))
|
.child(Story::title_for::<_, ContextMenu>(cx))
|
||||||
.child(Story::label(cx, "Default"))
|
.child(Story::label(cx, "Default"))
|
||||||
.child(ContextMenu::new([
|
.child(ContextMenu::new([
|
||||||
ContextMenuItem::header("Section header"),
|
ContextMenuItem::header("Section header"),
|
||||||
ContextMenuItem::Separator,
|
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());
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ pub enum Icon {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Close,
|
Close,
|
||||||
|
Collab,
|
||||||
Dash,
|
Dash,
|
||||||
Exit,
|
Exit,
|
||||||
ExclamationTriangle,
|
ExclamationTriangle,
|
||||||
@ -85,6 +86,7 @@ impl Icon {
|
|||||||
Icon::ChevronRight => "icons/chevron_right.svg",
|
Icon::ChevronRight => "icons/chevron_right.svg",
|
||||||
Icon::ChevronUp => "icons/chevron_up.svg",
|
Icon::ChevronUp => "icons/chevron_up.svg",
|
||||||
Icon::Close => "icons/x.svg",
|
Icon::Close => "icons/x.svg",
|
||||||
|
Icon::Collab => "icons/user_group_16.svg",
|
||||||
Icon::Dash => "icons/dash.svg",
|
Icon::Dash => "icons/dash.svg",
|
||||||
Icon::Exit => "icons/exit.svg",
|
Icon::Exit => "icons/exit.svg",
|
||||||
Icon::ExclamationTriangle => "icons/warning.svg",
|
Icon::ExclamationTriangle => "icons/warning.svg",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement, TextTooltip};
|
use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement};
|
||||||
use gpui::{prelude::*, MouseButton, VisualContext};
|
use gpui::{prelude::*, AnyView, MouseButton};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
struct IconButtonHandlers<V: 'static> {
|
struct IconButtonHandlers<V: 'static> {
|
||||||
@ -19,7 +19,7 @@ pub struct IconButton<V: 'static> {
|
|||||||
color: TextColor,
|
color: TextColor,
|
||||||
variant: ButtonVariant,
|
variant: ButtonVariant,
|
||||||
state: InteractionState,
|
state: InteractionState,
|
||||||
tooltip: Option<SharedString>,
|
tooltip: Option<Box<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>>,
|
||||||
handlers: IconButtonHandlers<V>,
|
handlers: IconButtonHandlers<V>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,22 +56,23 @@ impl<V: 'static> IconButton<V> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
|
pub fn tooltip(
|
||||||
self.tooltip = Some(tooltip.into());
|
mut self,
|
||||||
|
tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.tooltip = Some(Box::new(tooltip));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_click(
|
pub fn on_click(mut self, handler: impl 'static + Fn(&mut V, &mut ViewContext<V>)) -> Self {
|
||||||
mut self,
|
|
||||||
handler: impl 'static + Fn(&mut V, &mut ViewContext<V>) + Send + Sync,
|
|
||||||
) -> Self {
|
|
||||||
self.handlers.click = Some(Arc::new(handler));
|
self.handlers.click = Some(Arc::new(handler));
|
||||||
self
|
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) {
|
let icon_color = match (self.state, self.color) {
|
||||||
(InteractionState::Disabled, _) => TextColor::Disabled,
|
(InteractionState::Disabled, _) => TextColor::Disabled,
|
||||||
|
(InteractionState::Active, _) => TextColor::Error,
|
||||||
_ => self.color,
|
_ => self.color,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -99,15 +100,16 @@ impl<V: 'static> IconButton<V> {
|
|||||||
.child(IconElement::new(self.icon).color(icon_color));
|
.child(IconElement::new(self.icon).color(icon_color));
|
||||||
|
|
||||||
if let Some(click_handler) = self.handlers.click.clone() {
|
if let Some(click_handler) = self.handlers.click.clone() {
|
||||||
button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
|
button = button
|
||||||
cx.stop_propagation();
|
.on_mouse_down(MouseButton::Left, move |state, event, cx| {
|
||||||
click_handler(state, cx);
|
cx.stop_propagation();
|
||||||
});
|
click_handler(state, cx);
|
||||||
|
})
|
||||||
|
.cursor_pointer();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(tooltip) = self.tooltip.clone() {
|
if let Some(tooltip) = self.tooltip.take() {
|
||||||
button =
|
button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx))
|
||||||
button.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(tooltip.clone())));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button
|
button
|
||||||
|
@ -60,7 +60,7 @@ pub enum LineHeightStyle {
|
|||||||
UILabel,
|
UILabel,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Clone, Component)]
|
||||||
pub struct Label {
|
pub struct Label {
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
size: LabelSize,
|
size: LabelSize,
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
use gpui::div;
|
use gpui::{div, Action};
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::settings::user_settings;
|
use crate::settings::user_settings;
|
||||||
use crate::{
|
use crate::{
|
||||||
disclosure_control, h_stack, v_stack, Avatar, GraphicSlot, Icon, IconElement, IconSize, Label,
|
disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
|
||||||
TextColor, Toggle,
|
|
||||||
};
|
};
|
||||||
|
use crate::{prelude::*, GraphicSlot};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default, Debug, PartialEq)]
|
#[derive(Clone, Copy, Default, Debug, PartialEq)]
|
||||||
pub enum ListItemVariant {
|
pub enum ListItemVariant {
|
||||||
@ -232,6 +231,7 @@ pub struct ListEntry {
|
|||||||
size: ListEntrySize,
|
size: ListEntrySize,
|
||||||
toggle: Toggle,
|
toggle: Toggle,
|
||||||
variant: ListItemVariant,
|
variant: ListItemVariant,
|
||||||
|
on_click: Option<Box<dyn Action>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListEntry {
|
impl ListEntry {
|
||||||
@ -245,9 +245,15 @@ impl ListEntry {
|
|||||||
size: ListEntrySize::default(),
|
size: ListEntrySize::default(),
|
||||||
toggle: Toggle::NotToggleable,
|
toggle: Toggle::NotToggleable,
|
||||||
variant: ListItemVariant::default(),
|
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 {
|
pub fn variant(mut self, variant: ListItemVariant) -> Self {
|
||||||
self.variant = variant;
|
self.variant = variant;
|
||||||
self
|
self
|
||||||
@ -303,9 +309,21 @@ impl ListEntry {
|
|||||||
ListEntrySize::Small => div().h_6(),
|
ListEntrySize::Small => div().h_6(),
|
||||||
ListEntrySize::Medium => div().h_7(),
|
ListEntrySize::Medium => div().h_7(),
|
||||||
};
|
};
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.relative()
|
.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("")
|
.group("")
|
||||||
.bg(cx.theme().colors().surface_background)
|
.bg(cx.theme().colors().surface_background)
|
||||||
// TODO: Add focus state
|
// TODO: Add focus state
|
||||||
@ -401,7 +419,7 @@ impl List {
|
|||||||
v_stack()
|
v_stack()
|
||||||
.w_full()
|
.w_full()
|
||||||
.py_1()
|
.py_1()
|
||||||
.children(self.header)
|
.children(self.header.map(|header| header))
|
||||||
.child(list_content)
|
.child(list_content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,53 @@
|
|||||||
use gpui::{Div, Render};
|
use gpui::{overlay, Action, AnyView, Overlay, Render, VisualContext};
|
||||||
use settings2::Settings;
|
use settings2::Settings;
|
||||||
use theme2::{ActiveTheme, ThemeSettings};
|
use theme2::{ActiveTheme, ThemeSettings};
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor};
|
use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor};
|
||||||
|
|
||||||
pub struct TextTooltip {
|
pub struct Tooltip {
|
||||||
title: SharedString,
|
title: SharedString,
|
||||||
meta: Option<SharedString>,
|
meta: Option<SharedString>,
|
||||||
key_binding: Option<KeyBinding>,
|
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 {
|
pub fn new(title: impl Into<SharedString>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
title: title.into(),
|
title: title.into(),
|
||||||
@ -31,31 +67,36 @@ impl TextTooltip {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for TextTooltip {
|
impl Render for Tooltip {
|
||||||
type Element = Div<Self>;
|
type Element = Overlay<Self>;
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
|
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
|
||||||
v_stack()
|
overlay().child(
|
||||||
.elevation_2(cx)
|
// padding to avoid mouse cursor
|
||||||
.font(ui_font)
|
div().pl_2().pt_2p5().child(
|
||||||
.text_ui_sm()
|
v_stack()
|
||||||
.text_color(cx.theme().colors().text)
|
.elevation_2(cx)
|
||||||
.py_1()
|
.font(ui_font)
|
||||||
.px_2()
|
.text_ui_sm()
|
||||||
.child(
|
.text_color(cx.theme().colors().text)
|
||||||
h_stack()
|
.py_1()
|
||||||
.child(self.title.clone())
|
.px_2()
|
||||||
.when_some(self.key_binding.clone(), |this, key_binding| {
|
.child(
|
||||||
this.justify_between().child(key_binding)
|
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),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
use crate::{status_bar::StatusItemView, Axis, Workspace};
|
use crate::{status_bar::StatusItemView, Axis, Workspace};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter,
|
div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter,
|
||||||
FocusHandle, ParentComponent, Render, Styled, Subscription, View, ViewContext, WeakView,
|
FocusHandle, FocusableView, ParentComponent, Render, Styled, Subscription, View, ViewContext,
|
||||||
WindowContext,
|
WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use ui::{h_stack, IconButton, InteractionState, Tooltip};
|
||||||
|
|
||||||
pub enum PanelEvent {
|
pub enum PanelEvent {
|
||||||
ChangePosition,
|
ChangePosition,
|
||||||
@ -17,15 +18,15 @@ pub enum PanelEvent {
|
|||||||
Focus,
|
Focus,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Panel: Render + EventEmitter<PanelEvent> {
|
pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
|
||||||
fn persistent_name(&self) -> &'static str;
|
fn persistent_name() -> &'static str;
|
||||||
fn position(&self, cx: &WindowContext) -> DockPosition;
|
fn position(&self, cx: &WindowContext) -> DockPosition;
|
||||||
fn position_is_valid(&self, position: DockPosition) -> bool;
|
fn position_is_valid(&self, position: DockPosition) -> bool;
|
||||||
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
|
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
|
||||||
fn size(&self, cx: &WindowContext) -> f32;
|
fn size(&self, cx: &WindowContext) -> f32;
|
||||||
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>);
|
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>);
|
||||||
fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
|
fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
|
||||||
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>);
|
fn toggle_action(&self) -> Box<dyn Action>;
|
||||||
fn icon_label(&self, _: &WindowContext) -> Option<String> {
|
fn icon_label(&self, _: &WindowContext) -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@ -35,12 +36,11 @@ pub trait Panel: Render + EventEmitter<PanelEvent> {
|
|||||||
fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
|
fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
|
||||||
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
|
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
|
||||||
fn has_focus(&self, cx: &WindowContext) -> bool;
|
fn has_focus(&self, cx: &WindowContext) -> bool;
|
||||||
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait PanelHandle: Send + Sync {
|
pub trait PanelHandle: Send + Sync {
|
||||||
fn id(&self) -> EntityId;
|
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(&self, cx: &WindowContext) -> DockPosition;
|
||||||
fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
|
fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
|
||||||
fn set_position(&self, position: DockPosition, cx: &mut WindowContext);
|
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 set_active(&self, active: bool, cx: &mut WindowContext);
|
||||||
fn size(&self, cx: &WindowContext) -> f32;
|
fn size(&self, cx: &WindowContext) -> f32;
|
||||||
fn set_size(&self, size: Option<f32>, cx: &mut WindowContext);
|
fn set_size(&self, size: Option<f32>, cx: &mut WindowContext);
|
||||||
fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
|
fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
|
||||||
fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>);
|
fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action>;
|
||||||
fn icon_label(&self, cx: &WindowContext) -> Option<String>;
|
fn icon_label(&self, cx: &WindowContext) -> Option<String>;
|
||||||
fn has_focus(&self, cx: &WindowContext) -> bool;
|
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;
|
fn to_any(&self) -> AnyView;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,8 +65,8 @@ where
|
|||||||
self.entity_id()
|
self.entity_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn persistent_name(&self, cx: &WindowContext) -> &'static str {
|
fn persistent_name(&self) -> &'static str {
|
||||||
self.read(cx).persistent_name()
|
T::persistent_name()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn position(&self, cx: &WindowContext) -> DockPosition {
|
fn position(&self, cx: &WindowContext) -> DockPosition {
|
||||||
@ -101,12 +101,12 @@ where
|
|||||||
self.update(cx, |this, cx| this.set_size(size, cx))
|
self.update(cx, |this, cx| this.set_size(size, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> {
|
fn icon(&self, cx: &WindowContext) -> Option<ui::Icon> {
|
||||||
self.read(cx).icon_path(cx)
|
self.read(cx).icon(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>) {
|
fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action> {
|
||||||
self.read(cx).icon_tooltip()
|
self.read(cx).toggle_action()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_label(&self, cx: &WindowContext) -> Option<String> {
|
fn icon_label(&self, cx: &WindowContext) -> Option<String> {
|
||||||
@ -121,7 +121,7 @@ where
|
|||||||
self.clone().into()
|
self.clone().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
|
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||||
self.read(cx).focus_handle(cx).clone()
|
self.read(cx).focus_handle(cx).clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,6 +139,14 @@ pub struct Dock {
|
|||||||
active_panel_index: usize,
|
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)]
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum DockPosition {
|
pub enum DockPosition {
|
||||||
@ -214,18 +222,20 @@ impl Dock {
|
|||||||
// .find_map(|entry| entry.panel.as_any().clone().downcast())
|
// .find_map(|entry| entry.panel.as_any().clone().downcast())
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
|
pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
|
||||||
// self.panel_entries
|
self.panel_entries
|
||||||
// .iter()
|
.iter()
|
||||||
// .position(|entry| entry.panel.as_any().is::<T>())
|
.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> {
|
pub fn panel_index_for_persistent_name(
|
||||||
todo!()
|
&self,
|
||||||
// self.panel_entries.iter().position(|entry| {
|
ui_name: &str,
|
||||||
// let panel = entry.panel.as_any();
|
_cx: &AppContext,
|
||||||
// cx.view_ui_name(panel.window(), panel.id()) == Some(ui_name)
|
) -> Option<usize> {
|
||||||
// })
|
self.panel_entries
|
||||||
|
.iter()
|
||||||
|
.position(|entry| entry.panel.persistent_name() == ui_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn active_panel_index(&self) -> usize {
|
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 {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
// todo!()
|
// todo!()
|
||||||
let dock = self.dock.read(cx);
|
let dock = self.dock.read(cx);
|
||||||
div().children(
|
let active_index = dock.active_panel_index;
|
||||||
dock.panel_entries
|
let is_open = dock.is_open;
|
||||||
.iter()
|
|
||||||
.map(|panel| panel.panel.persistent_name(cx)),
|
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"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub mod test {
|
pub mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::{div, Div, ViewContext, WindowContext};
|
use gpui::{actions, div, Div, ViewContext, WindowContext};
|
||||||
|
|
||||||
pub struct TestPanel {
|
pub struct TestPanel {
|
||||||
pub position: DockPosition,
|
pub position: DockPosition,
|
||||||
@ -674,6 +701,7 @@ pub mod test {
|
|||||||
pub has_focus: bool,
|
pub has_focus: bool,
|
||||||
pub size: f32,
|
pub size: f32,
|
||||||
}
|
}
|
||||||
|
actions!(ToggleTestPanel);
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for TestPanel {}
|
impl EventEmitter<PanelEvent> for TestPanel {}
|
||||||
|
|
||||||
@ -698,7 +726,7 @@ pub mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Panel for TestPanel {
|
impl Panel for TestPanel {
|
||||||
fn persistent_name(&self) -> &'static str {
|
fn persistent_name() -> &'static str {
|
||||||
"TestPanel"
|
"TestPanel"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -723,12 +751,12 @@ pub mod test {
|
|||||||
self.size = size.unwrap_or(300.);
|
self.size = size.unwrap_or(300.);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
|
fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
|
||||||
Some("icons/test_panel.svg")
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
|
fn toggle_action(&self) -> Box<dyn Action> {
|
||||||
("Test Panel".into(), None)
|
ToggleTestPanel.boxed_clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_zoomed(&self, _: &WindowContext) -> bool {
|
fn is_zoomed(&self, _: &WindowContext) -> bool {
|
||||||
@ -746,8 +774,10 @@ pub mod test {
|
|||||||
fn has_focus(&self, _cx: &WindowContext) -> bool {
|
fn has_focus(&self, _cx: &WindowContext) -> bool {
|
||||||
self.has_focus
|
self.has_focus
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
|
impl FocusableView for TestPanel {
|
||||||
|
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,9 @@ use client2::{
|
|||||||
Client,
|
Client,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, HighlightStyle,
|
AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
|
||||||
Model, Pixels, Point, Render, SharedString, Task, View, ViewContext, WeakView, WindowContext,
|
HighlightStyle, Model, Pixels, Point, SharedString, Task, View, ViewContext, WeakView,
|
||||||
|
WindowContext,
|
||||||
};
|
};
|
||||||
use project2::{Project, ProjectEntryId, ProjectPath};
|
use project2::{Project, ProjectEntryId, ProjectPath};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
@ -91,8 +92,7 @@ pub struct BreadcrumbText {
|
|||||||
pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
|
pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Item: Render + EventEmitter<ItemEvent> {
|
pub trait Item: FocusableView + EventEmitter<ItemEvent> {
|
||||||
fn focus_handle(&self) -> FocusHandle;
|
|
||||||
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
|
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
|
||||||
fn workspace_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 {
|
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> {
|
impl<T: Item> ItemHandle for View<T> {
|
||||||
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
|
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
|
||||||
self.read(cx).focus_handle()
|
self.focus_handle(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscribe_to_item_events(
|
fn subscribe_to_item_events(
|
||||||
|
@ -72,7 +72,7 @@ impl ModalLayer {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_modal<V>(&self) -> Option<View<V>>
|
pub fn active_modal<V>(&self) -> Option<View<V>>
|
||||||
where
|
where
|
||||||
V: 'static,
|
V: 'static,
|
||||||
{
|
{
|
||||||
|
@ -8,8 +8,8 @@ use anyhow::Result;
|
|||||||
use collections::{HashMap, HashSet, VecDeque};
|
use collections::{HashMap, HashSet, VecDeque};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId,
|
actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId,
|
||||||
EventEmitter, FocusHandle, Model, PromptLevel, Render, Task, View, ViewContext, VisualContext,
|
EventEmitter, FocusHandle, Focusable, FocusableView, Model, PromptLevel, Render, Task, View,
|
||||||
WeakView, WindowContext,
|
ViewContext, VisualContext, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project2::{Project, ProjectEntryId, ProjectPath};
|
use project2::{Project, ProjectEntryId, ProjectPath};
|
||||||
@ -25,7 +25,7 @@ use std::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use ui::v_stack;
|
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;
|
use util::truncate_and_remove_front;
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
|
#[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_left);
|
||||||
// cx.add_async_action(Pane::close_items_to_the_right);
|
// cx.add_async_action(Pane::close_items_to_the_right);
|
||||||
// cx.add_async_action(Pane::close_all_items);
|
// 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 {
|
pub enum Event {
|
||||||
@ -183,7 +179,7 @@ pub struct Pane {
|
|||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
// can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
|
// 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>>,
|
// render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,7 +347,7 @@ impl Pane {
|
|||||||
workspace,
|
workspace,
|
||||||
project,
|
project,
|
||||||
// can_drop: Rc::new(|_, _| true),
|
// can_drop: Rc::new(|_, _| true),
|
||||||
// can_split: true,
|
can_split: true,
|
||||||
// render_tab_bar_buttons: Rc::new(move |pane, cx| {
|
// render_tab_bar_buttons: Rc::new(move |pane, cx| {
|
||||||
// Flex::row()
|
// Flex::row()
|
||||||
// // New menu
|
// // New menu
|
||||||
@ -431,17 +427,17 @@ impl Pane {
|
|||||||
// self.can_drop = Rc::new(can_drop);
|
// self.can_drop = Rc::new(can_drop);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
|
pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
|
||||||
// self.can_split = can_split;
|
self.can_split = can_split;
|
||||||
// cx.notify();
|
cx.notify();
|
||||||
// }
|
}
|
||||||
|
|
||||||
// pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
|
pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
|
||||||
// self.toolbar.update(cx, |toolbar, cx| {
|
self.toolbar.update(cx, |toolbar, cx| {
|
||||||
// toolbar.set_can_navigate(can_navigate, cx);
|
toolbar.set_can_navigate(can_navigate, cx);
|
||||||
// });
|
});
|
||||||
// cx.notify();
|
cx.notify();
|
||||||
// }
|
}
|
||||||
|
|
||||||
// pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
|
// pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
|
||||||
// where
|
// where
|
||||||
@ -1017,7 +1013,11 @@ impl Pane {
|
|||||||
.unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
|
.unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
|
||||||
|
|
||||||
let should_activate = activate_pane || self.has_focus(cx);
|
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);
|
let item = self.items.remove(item_index);
|
||||||
@ -1191,9 +1191,9 @@ impl Pane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
|
pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
|
||||||
// cx.emit(Event::Split(direction));
|
cx.emit(Event::Split(direction));
|
||||||
// }
|
}
|
||||||
|
|
||||||
// fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
|
// fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
// self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
|
// self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
|
||||||
@ -1392,8 +1392,9 @@ impl Pane {
|
|||||||
.id(item.id())
|
.id(item.id())
|
||||||
.cursor_pointer()
|
.cursor_pointer()
|
||||||
.when_some(item.tab_tooltip_text(cx), |div, text| {
|
.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))
|
// .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))
|
// .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
|
||||||
// .on_drop(|_view, state: View<DraggedTab>, cx| {
|
// .on_drop(|_view, state: View<DraggedTab>, cx| {
|
||||||
@ -1426,32 +1427,22 @@ impl Pane {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.text_color(text_color)
|
.text_color(text_color)
|
||||||
.children(if item.has_conflict(cx) {
|
.children(
|
||||||
Some(
|
item.has_conflict(cx)
|
||||||
IconElement::new(Icon::ExclamationTriangle)
|
.then(|| {
|
||||||
.size(ui::IconSize::Small)
|
IconElement::new(Icon::ExclamationTriangle)
|
||||||
.color(TextColor::Warning),
|
.size(ui::IconSize::Small)
|
||||||
)
|
.color(TextColor::Warning)
|
||||||
} else if item.is_dirty(cx) {
|
})
|
||||||
Some(
|
.or(item.is_dirty(cx).then(|| {
|
||||||
IconElement::new(Icon::ExclamationTriangle)
|
IconElement::new(Icon::ExclamationTriangle)
|
||||||
.size(ui::IconSize::Small)
|
.size(ui::IconSize::Small)
|
||||||
.color(TextColor::Info),
|
.color(TextColor::Info)
|
||||||
)
|
})),
|
||||||
} else {
|
)
|
||||||
None
|
.children((!close_right).then(|| close_icon()))
|
||||||
})
|
|
||||||
.children(if !close_right {
|
|
||||||
Some(close_icon())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
})
|
|
||||||
.child(label)
|
.child(label)
|
||||||
.children(if close_right {
|
.children(close_right.then(|| close_icon())),
|
||||||
Some(close_icon())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1908,16 +1899,23 @@ impl Pane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// impl Entity for Pane {
|
impl FocusableView for Pane {
|
||||||
// type Event = Event;
|
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||||
// }
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Render for Pane {
|
impl Render for Pane {
|
||||||
type Element = Div<Self>;
|
type Element = Focusable<Self, Div<Self>>;
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
v_stack()
|
v_stack()
|
||||||
.key_context("Pane")
|
.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()
|
.size_full()
|
||||||
.on_action(|pane: &mut Self, action, cx| {
|
.on_action(|pane: &mut Self, action, cx| {
|
||||||
pane.close_active_item(action, cx)
|
pane.close_active_item(action, cx)
|
||||||
|
@ -148,6 +148,10 @@ impl PaneGroup {
|
|||||||
self.root.collect_panes(&mut panes);
|
self.root.collect_panes(&mut panes);
|
||||||
panes
|
panes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn first_pane(&self) -> View<Pane> {
|
||||||
|
self.root.first_pane()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[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(
|
pub fn render(
|
||||||
&self,
|
&self,
|
||||||
project: &Model<Project>,
|
project: &Model<Project>,
|
||||||
@ -551,7 +562,32 @@ impl PaneAxis {
|
|||||||
) -> AnyElement<Workspace> {
|
) -> AnyElement<Workspace> {
|
||||||
debug_assert!(self.members.len() == self.flexes.lock().len());
|
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(
|
// let mut pane_axis = PaneAxisElement::new(
|
||||||
// self.axis,
|
// self.axis,
|
||||||
// basis,
|
// basis,
|
||||||
|
@ -277,7 +277,7 @@ impl SerializedPane {
|
|||||||
|
|
||||||
pub type GroupId = i64;
|
pub type GroupId = i64;
|
||||||
pub type PaneId = i64;
|
pub type PaneId = i64;
|
||||||
pub type ItemId = usize;
|
pub type ItemId = u64;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct SerializedItem {
|
pub struct SerializedItem {
|
||||||
|
@ -6,6 +6,7 @@ use gpui::{
|
|||||||
WindowContext,
|
WindowContext,
|
||||||
};
|
};
|
||||||
use theme2::ActiveTheme;
|
use theme2::ActiveTheme;
|
||||||
|
use ui::h_stack;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
pub trait StatusItemView: Render {
|
pub trait StatusItemView: Render {
|
||||||
@ -53,57 +54,20 @@ impl Render for StatusBar {
|
|||||||
|
|
||||||
impl StatusBar {
|
impl StatusBar {
|
||||||
fn render_left_tools(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
|
fn render_left_tools(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
|
||||||
div()
|
h_stack()
|
||||||
.flex()
|
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.children(self.left_items.iter().map(|item| item.to_any()))
|
.children(self.left_items.iter().map(|item| item.to_any()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_right_tools(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
|
fn render_right_tools(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
|
||||||
div()
|
h_stack()
|
||||||
.flex()
|
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.children(self.right_items.iter().map(|item| item.to_any()))
|
.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 {
|
impl StatusBar {
|
||||||
pub fn new(active_pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Self {
|
pub fn new(active_pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Self {
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
@ -223,80 +187,3 @@ impl From<&dyn StatusItemViewHandle> for AnyView {
|
|||||||
val.to_any().clone()
|
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()
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
@ -15,13 +15,6 @@ mod status_bar;
|
|||||||
mod toolbar;
|
mod toolbar;
|
||||||
mod workspace_settings;
|
mod workspace_settings;
|
||||||
|
|
||||||
pub use crate::persistence::{
|
|
||||||
model::{
|
|
||||||
DockData, DockStructure, ItemId, SerializedItem, SerializedPane, SerializedPaneGroup,
|
|
||||||
SerializedWorkspace,
|
|
||||||
},
|
|
||||||
WorkspaceDb,
|
|
||||||
};
|
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use call2::ActiveCall;
|
use call2::ActiveCall;
|
||||||
use client2::{
|
use client2::{
|
||||||
@ -29,18 +22,18 @@ use client2::{
|
|||||||
Client, TypedEnvelope, UserStore,
|
Client, TypedEnvelope, UserStore,
|
||||||
};
|
};
|
||||||
use collections::{hash_map, HashMap, HashSet};
|
use collections::{hash_map, HashMap, HashSet};
|
||||||
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle as _};
|
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::{mpsc, oneshot},
|
channel::{mpsc, oneshot},
|
||||||
future::try_join_all,
|
future::try_join_all,
|
||||||
Future, FutureExt, StreamExt,
|
Future, FutureExt, StreamExt,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView,
|
actions, div, point, register_action, size, Action, AnyModel, AnyView, AnyWeakView, AppContext,
|
||||||
AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId,
|
AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter,
|
||||||
EventEmitter, FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent,
|
FocusHandle, FocusableView, GlobalPixels, InteractiveComponent, KeyContext, Model,
|
||||||
Point, Render, Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds,
|
ModelContext, ParentComponent, Point, Render, Size, Styled, Subscription, Task, View,
|
||||||
WindowContext, WindowHandle, WindowOptions,
|
ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
|
||||||
};
|
};
|
||||||
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
|
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@ -51,7 +44,10 @@ use node_runtime::NodeRuntime;
|
|||||||
use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
|
use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
|
||||||
pub use pane::*;
|
pub use pane::*;
|
||||||
pub use pane_group::*;
|
pub use pane_group::*;
|
||||||
use persistence::{model::WorkspaceLocation, DB};
|
pub use persistence::{
|
||||||
|
model::{ItemId, SerializedWorkspace, WorkspaceLocation},
|
||||||
|
WorkspaceDb, DB,
|
||||||
|
};
|
||||||
use postage::stream::Stream;
|
use postage::stream::Stream;
|
||||||
use project2::{Project, ProjectEntryId, ProjectPath, Worktree};
|
use project2::{Project, ProjectEntryId, ProjectPath, Worktree};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@ -68,12 +64,16 @@ use std::{
|
|||||||
};
|
};
|
||||||
use theme2::{ActiveTheme, ThemeSettings};
|
use theme2::{ActiveTheme, ThemeSettings};
|
||||||
pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
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 util::ResultExt;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
|
pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
|
||||||
|
|
||||||
|
use crate::persistence::model::{
|
||||||
|
DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup,
|
||||||
|
};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref ZED_WINDOW_SIZE: Option<Size<GlobalPixels>> = env::var("ZED_WINDOW_SIZE")
|
static ref ZED_WINDOW_SIZE: Option<Size<GlobalPixels>> = env::var("ZED_WINDOW_SIZE")
|
||||||
.ok()
|
.ok()
|
||||||
@ -195,10 +195,11 @@ impl Clone for Toast {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[derive(Clone, Deserialize, PartialEq)]
|
#[register_action]
|
||||||
// pub struct OpenTerminal {
|
#[derive(Debug, Default, Clone, Deserialize, PartialEq)]
|
||||||
// pub working_directory: PathBuf,
|
pub struct OpenTerminal {
|
||||||
// }
|
pub working_directory: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
// impl_actions!(
|
// impl_actions!(
|
||||||
// workspace,
|
// workspace,
|
||||||
@ -208,7 +209,6 @@ impl Clone for Toast {
|
|||||||
// SwapPaneInDirection,
|
// SwapPaneInDirection,
|
||||||
// NewFileInDirection,
|
// NewFileInDirection,
|
||||||
// Toast,
|
// Toast,
|
||||||
// OpenTerminal,
|
|
||||||
// SaveAll,
|
// SaveAll,
|
||||||
// Save,
|
// Save,
|
||||||
// CloseAllItemsAndPanes,
|
// CloseAllItemsAndPanes,
|
||||||
@ -322,12 +322,6 @@ pub struct AppState {
|
|||||||
pub fs: Arc<dyn fs2::Fs>,
|
pub fs: Arc<dyn fs2::Fs>,
|
||||||
pub build_window_options:
|
pub build_window_options:
|
||||||
fn(Option<WindowBounds>, Option<Uuid>, &mut AppContext) -> WindowOptions,
|
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>,
|
pub node_runtime: Arc<dyn NodeRuntime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -373,7 +367,6 @@ impl AppState {
|
|||||||
user_store,
|
user_store,
|
||||||
workspace_store,
|
workspace_store,
|
||||||
node_runtime: FakeNodeRuntime::new(),
|
node_runtime: FakeNodeRuntime::new(),
|
||||||
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
|
||||||
build_window_options: |_, _, _| Default::default(),
|
build_window_options: |_, _, _| Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -433,7 +426,6 @@ pub enum Event {
|
|||||||
|
|
||||||
pub struct Workspace {
|
pub struct Workspace {
|
||||||
weak_self: WeakView<Self>,
|
weak_self: WeakView<Self>,
|
||||||
focus_handle: FocusHandle,
|
|
||||||
workspace_actions: Vec<Box<dyn Fn(Div<Workspace>) -> Div<Workspace>>>,
|
workspace_actions: Vec<Box<dyn Fn(Div<Workspace>) -> Div<Workspace>>>,
|
||||||
zoomed: Option<AnyWeakView>,
|
zoomed: Option<AnyWeakView>,
|
||||||
zoomed_position: Option<DockPosition>,
|
zoomed_position: Option<DockPosition>,
|
||||||
@ -448,7 +440,7 @@ pub struct Workspace {
|
|||||||
last_active_view_id: Option<proto::ViewId>,
|
last_active_view_id: Option<proto::ViewId>,
|
||||||
status_bar: View<StatusBar>,
|
status_bar: View<StatusBar>,
|
||||||
modal_layer: View<ModalLayer>,
|
modal_layer: View<ModalLayer>,
|
||||||
// titlebar_item: Option<AnyViewHandle>,
|
titlebar_item: Option<AnyView>,
|
||||||
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
|
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
follower_states: HashMap<View<Pane>, FollowerState>,
|
follower_states: HashMap<View<Pane>, FollowerState>,
|
||||||
@ -651,7 +643,6 @@ impl Workspace {
|
|||||||
cx.defer(|this, cx| this.update_window_title(cx));
|
cx.defer(|this, cx| this.update_window_title(cx));
|
||||||
Workspace {
|
Workspace {
|
||||||
weak_self: weak_handle.clone(),
|
weak_self: weak_handle.clone(),
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
zoomed: None,
|
zoomed: None,
|
||||||
zoomed_position: None,
|
zoomed_position: None,
|
||||||
center: PaneGroup::new(center_pane.clone()),
|
center: PaneGroup::new(center_pane.clone()),
|
||||||
@ -662,7 +653,7 @@ impl Workspace {
|
|||||||
last_active_view_id: None,
|
last_active_view_id: None,
|
||||||
status_bar,
|
status_bar,
|
||||||
modal_layer,
|
modal_layer,
|
||||||
// titlebar_item: None,
|
titlebar_item: None,
|
||||||
notifications: Default::default(),
|
notifications: Default::default(),
|
||||||
left_dock,
|
left_dock,
|
||||||
bottom_dock,
|
bottom_dock,
|
||||||
@ -687,7 +678,7 @@ impl Workspace {
|
|||||||
fn new_local(
|
fn new_local(
|
||||||
abs_paths: Vec<PathBuf>,
|
abs_paths: Vec<PathBuf>,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
_requesting_window: Option<WindowHandle<Workspace>>,
|
requesting_window: Option<WindowHandle<Workspace>>,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Task<
|
) -> Task<
|
||||||
anyhow::Result<(
|
anyhow::Result<(
|
||||||
@ -705,7 +696,8 @@ impl Workspace {
|
|||||||
);
|
);
|
||||||
|
|
||||||
cx.spawn(|mut cx| async move {
|
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);
|
let paths_to_open = Arc::new(abs_paths);
|
||||||
|
|
||||||
@ -734,15 +726,14 @@ impl Workspace {
|
|||||||
DB.next_id().await.unwrap_or(0)
|
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.update_window(window.into(), |old_workspace, cx| {
|
||||||
cx.replace_root_view(|cx| {
|
cx.replace_root_view(|cx| {
|
||||||
Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
|
Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
|
||||||
});
|
});
|
||||||
});
|
})?;
|
||||||
window
|
window
|
||||||
} else */ {
|
} else {
|
||||||
let window_bounds_override = window_bounds_env_override(&cx);
|
let window_bounds_override = window_bounds_env_override(&cx);
|
||||||
let (bounds, display) = if let Some(bounds) = window_bounds_override {
|
let (bounds, display) = if let Some(bounds) = window_bounds_override {
|
||||||
(Some(bounds), None)
|
(Some(bounds), None)
|
||||||
@ -756,12 +747,13 @@ impl Workspace {
|
|||||||
// Stored bounds are relative to the containing display.
|
// Stored bounds are relative to the containing display.
|
||||||
// So convert back to global coordinates if that screen still exists
|
// So convert back to global coordinates if that screen still exists
|
||||||
if let WindowBounds::Fixed(mut window_bounds) = bounds {
|
if let WindowBounds::Fixed(mut window_bounds) = bounds {
|
||||||
let screen =
|
let screen = cx
|
||||||
cx.update(|cx|
|
.update(|cx| {
|
||||||
cx.displays()
|
cx.displays().into_iter().find(|display| {
|
||||||
.into_iter()
|
display.uuid().ok() == Some(serialized_display)
|
||||||
.find(|display| display.uuid().ok() == Some(serialized_display))
|
})
|
||||||
).ok()??;
|
})
|
||||||
|
.ok()??;
|
||||||
let screen_bounds = screen.bounds();
|
let screen_bounds = screen.bounds();
|
||||||
window_bounds.origin.x += screen_bounds.origin.x;
|
window_bounds.origin.x += screen_bounds.origin.x;
|
||||||
window_bounds.origin.y += screen_bounds.origin.y;
|
window_bounds.origin.y += screen_bounds.origin.y;
|
||||||
@ -790,17 +782,17 @@ impl Workspace {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// todo!() Ask how to do this
|
// todo!() Ask how to do this
|
||||||
let weak_view = window.update(&mut cx, |_, cx| cx.view().downgrade())?;
|
// let weak_view = window.update(&mut cx, |_, cx| cx.view().downgrade())?;
|
||||||
let async_cx = window.update(&mut cx, |_, cx| cx.to_async())?;
|
// let async_cx = window.update(&mut cx, |_, cx| cx.to_async())?;
|
||||||
|
|
||||||
(app_state.initialize_workspace)(
|
// (app_state.initialize_workspace)(
|
||||||
weak_view,
|
// weak_view,
|
||||||
serialized_workspace.is_some(),
|
// serialized_workspace.is_some(),
|
||||||
app_state.clone(),
|
// app_state.clone(),
|
||||||
async_cx,
|
// async_cx,
|
||||||
)
|
// )
|
||||||
.await
|
// .await
|
||||||
.log_err();
|
// .log_err();
|
||||||
|
|
||||||
window
|
window
|
||||||
.update(&mut cx, |_, cx| cx.activate_window())
|
.update(&mut cx, |_, cx| cx.activate_window())
|
||||||
@ -809,12 +801,7 @@ impl Workspace {
|
|||||||
notify_if_database_failed(window, &mut cx);
|
notify_if_database_failed(window, &mut cx);
|
||||||
let opened_items = window
|
let opened_items = window
|
||||||
.update(&mut cx, |_workspace, cx| {
|
.update(&mut cx, |_workspace, cx| {
|
||||||
open_items(
|
open_items(serialized_workspace, project_paths, app_state, cx)
|
||||||
serialized_workspace,
|
|
||||||
project_paths,
|
|
||||||
app_state,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
@ -1035,15 +1022,14 @@ impl Workspace {
|
|||||||
&self.app_state.client
|
&self.app_state.client
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo!()
|
pub fn set_titlebar_item(&mut self, item: AnyView, cx: &mut ViewContext<Self>) {
|
||||||
// pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
self.titlebar_item = Some(item);
|
||||||
// self.titlebar_item = Some(item);
|
cx.notify();
|
||||||
// cx.notify();
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
|
pub fn titlebar_item(&self) -> Option<AnyView> {
|
||||||
// self.titlebar_item.clone()
|
self.titlebar_item.clone()
|
||||||
// }
|
}
|
||||||
|
|
||||||
/// Call the given callback with a workspace whose project is local.
|
/// Call the given callback with a workspace whose project is local.
|
||||||
///
|
///
|
||||||
@ -1450,6 +1436,11 @@ impl Workspace {
|
|||||||
self.active_pane().read(cx).active_item()
|
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> {
|
fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
|
||||||
self.active_item(cx).and_then(|item| item.project_path(cx))
|
self.active_item(cx).and_then(|item| item.project_path(cx))
|
||||||
}
|
}
|
||||||
@ -1570,7 +1561,7 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if focus_center {
|
if focus_center {
|
||||||
cx.focus(&self.focus_handle);
|
self.active_pane.update(cx, |pane, cx| pane.focus(cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@ -1592,60 +1583,58 @@ impl Workspace {
|
|||||||
self.serialize_workspace(cx);
|
self.serialize_workspace(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// /// Transfer focus to the panel of the given type.
|
/// Transfer focus to the panel of the given type.
|
||||||
// pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
|
pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
|
||||||
// self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?
|
let panel = self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?;
|
||||||
// .as_any()
|
panel.to_any().downcast().ok()
|
||||||
// .clone()
|
}
|
||||||
// .downcast()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// Focus the panel of the given type if it isn't already focused. If it is
|
/// 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.
|
/// already focused, then transfer focus back to the workspace center.
|
||||||
// pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
|
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));
|
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.
|
/// Focus or unfocus the given panel type, depending on the given callback.
|
||||||
// fn focus_or_unfocus_panel<T: Panel>(
|
fn focus_or_unfocus_panel<T: Panel>(
|
||||||
// &mut self,
|
&mut self,
|
||||||
// cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
// should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
|
should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
|
||||||
// ) -> Option<Rc<dyn PanelHandle>> {
|
) -> Option<Arc<dyn PanelHandle>> {
|
||||||
// for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
|
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>() {
|
if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
|
||||||
// let mut focus_center = false;
|
let mut focus_center = false;
|
||||||
// let mut reveal_dock = false;
|
let mut reveal_dock = false;
|
||||||
// let panel = dock.update(cx, |dock, cx| {
|
let panel = dock.update(cx, |dock, cx| {
|
||||||
// dock.activate_panel(panel_index, cx);
|
dock.activate_panel(panel_index, cx);
|
||||||
|
|
||||||
// let panel = dock.active_panel().cloned();
|
let panel = dock.active_panel().cloned();
|
||||||
// if let Some(panel) = panel.as_ref() {
|
if let Some(panel) = panel.as_ref() {
|
||||||
// if should_focus(&**panel, cx) {
|
if should_focus(&**panel, cx) {
|
||||||
// dock.set_open(true, cx);
|
dock.set_open(true, cx);
|
||||||
// cx.focus(panel.as_any());
|
panel.focus_handle(cx).focus(cx);
|
||||||
// reveal_dock = true;
|
reveal_dock = true;
|
||||||
// } else {
|
} else {
|
||||||
// // if panel.is_zoomed(cx) {
|
// if panel.is_zoomed(cx) {
|
||||||
// // dock.set_open(false, cx);
|
// dock.set_open(false, cx);
|
||||||
// // }
|
// }
|
||||||
// focus_center = true;
|
focus_center = true;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// panel
|
panel
|
||||||
// });
|
});
|
||||||
|
|
||||||
// if focus_center {
|
if focus_center {
|
||||||
// cx.focus_self();
|
self.active_pane.update(cx, |pane, cx| pane.focus(cx))
|
||||||
// }
|
}
|
||||||
|
|
||||||
// self.serialize_workspace(cx);
|
self.serialize_workspace(cx);
|
||||||
// cx.notify();
|
cx.notify();
|
||||||
// return panel;
|
return panel;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// None
|
None
|
||||||
// }
|
}
|
||||||
|
|
||||||
// pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
|
// pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
|
||||||
// for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
|
// for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
|
||||||
@ -1704,7 +1693,7 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if focus_center {
|
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 {
|
if self.zoomed_position != dock_to_reveal {
|
||||||
@ -2445,75 +2434,6 @@ impl Workspace {
|
|||||||
// .any(|state| state.leader_id == peer_id)
|
// .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>) {
|
fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let active_entry = self.active_project_path(cx);
|
let active_entry = self.active_project_path(cx);
|
||||||
self.project
|
self.project
|
||||||
@ -3040,13 +2960,15 @@ impl Workspace {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
// fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
|
fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
// self._schedule_serialize = Some(cx.spawn(|this, cx| async move {
|
self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
|
||||||
// cx.background().timer(Duration::from_millis(100)).await;
|
cx.background_executor()
|
||||||
// this.read_with(&cx, |this, cx| this.serialize_workspace(cx))
|
.timer(Duration::from_millis(100))
|
||||||
// .ok();
|
.await;
|
||||||
// }));
|
this.update(&mut cx, |this, cx| this.serialize_workspace(cx))
|
||||||
// }
|
.log_err();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
fn serialize_workspace(&self, cx: &mut ViewContext<Self>) {
|
fn serialize_workspace(&self, cx: &mut ViewContext<Self>) {
|
||||||
fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
|
fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
|
||||||
@ -3058,7 +2980,7 @@ impl Workspace {
|
|||||||
.filter_map(|item_handle| {
|
.filter_map(|item_handle| {
|
||||||
Some(SerializedItem {
|
Some(SerializedItem {
|
||||||
kind: Arc::from(item_handle.serialized_item_kind()?),
|
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,
|
active: Some(item_handle.id()) == active_item_id,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -3102,7 +3024,7 @@ impl Workspace {
|
|||||||
let left_visible = left_dock.is_open();
|
let left_visible = left_dock.is_open();
|
||||||
let left_active_panel = left_dock
|
let left_active_panel = left_dock
|
||||||
.visible_panel()
|
.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
|
let left_dock_zoom = left_dock
|
||||||
.visible_panel()
|
.visible_panel()
|
||||||
.map(|panel| panel.is_zoomed(cx))
|
.map(|panel| panel.is_zoomed(cx))
|
||||||
@ -3112,7 +3034,7 @@ impl Workspace {
|
|||||||
let right_visible = right_dock.is_open();
|
let right_visible = right_dock.is_open();
|
||||||
let right_active_panel = right_dock
|
let right_active_panel = right_dock
|
||||||
.visible_panel()
|
.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
|
let right_dock_zoom = right_dock
|
||||||
.visible_panel()
|
.visible_panel()
|
||||||
.map(|panel| panel.is_zoomed(cx))
|
.map(|panel| panel.is_zoomed(cx))
|
||||||
@ -3122,7 +3044,7 @@ impl Workspace {
|
|||||||
let bottom_visible = bottom_dock.is_open();
|
let bottom_visible = bottom_dock.is_open();
|
||||||
let bottom_active_panel = bottom_dock
|
let bottom_active_panel = bottom_dock
|
||||||
.visible_panel()
|
.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
|
let bottom_dock_zoom = bottom_dock
|
||||||
.visible_panel()
|
.visible_panel()
|
||||||
.map(|panel| panel.is_zoomed(cx))
|
.map(|panel| panel.is_zoomed(cx))
|
||||||
@ -3228,45 +3150,34 @@ impl Workspace {
|
|||||||
|
|
||||||
// Swap workspace center group
|
// Swap workspace center group
|
||||||
workspace.center = PaneGroup::with_root(center_group);
|
workspace.center = PaneGroup::with_root(center_group);
|
||||||
|
workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
|
||||||
// Change the focus to the workspace first so that we retrigger focus in on the pane.
|
if let Some(active_pane) = active_pane {
|
||||||
// todo!()
|
workspace.active_pane = active_pane;
|
||||||
// cx.focus_self();
|
cx.focus_self();
|
||||||
// if let Some(active_pane) = active_pane {
|
} else {
|
||||||
// cx.focus(&active_pane);
|
workspace.active_pane = workspace.center.first_pane().clone();
|
||||||
// } 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()
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let docks = serialized_workspace.docks;
|
let docks = serialized_workspace.docks;
|
||||||
workspace.left_dock.update(cx, |dock, cx| {
|
workspace.left_dock.update(cx, |dock, cx| {
|
||||||
dock.set_open(docks.left.visible, cx);
|
dock.set_open(docks.left.visible, cx);
|
||||||
if let Some(active_panel) = docks.left.active_panel {
|
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.activate_panel(ix, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dock.active_panel()
|
dock.active_panel()
|
||||||
.map(|panel| panel.set_zoomed(docks.left.zoom, cx));
|
.map(|panel| panel.set_zoomed(docks.left.zoom, cx));
|
||||||
if docks.left.visible && docks.left.zoom {
|
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
|
// TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
|
||||||
workspace.right_dock.update(cx, |dock, cx| {
|
workspace.right_dock.update(cx, |dock, cx| {
|
||||||
dock.set_open(docks.right.visible, cx);
|
dock.set_open(docks.right.visible, cx);
|
||||||
if let Some(active_panel) = docks.right.active_panel {
|
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);
|
dock.activate_panel(ix, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3274,14 +3185,13 @@ impl Workspace {
|
|||||||
.map(|panel| panel.set_zoomed(docks.right.zoom, cx));
|
.map(|panel| panel.set_zoomed(docks.right.zoom, cx));
|
||||||
|
|
||||||
if docks.right.visible && docks.right.zoom {
|
if docks.right.visible && docks.right.zoom {
|
||||||
// todo!()
|
cx.focus_self()
|
||||||
// cx.focus_self()
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
workspace.bottom_dock.update(cx, |dock, cx| {
|
workspace.bottom_dock.update(cx, |dock, cx| {
|
||||||
dock.set_open(docks.bottom.visible, cx);
|
dock.set_open(docks.bottom.visible, cx);
|
||||||
if let Some(active_panel) = docks.bottom.active_panel {
|
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);
|
dock.activate_panel(ix, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3290,8 +3200,7 @@ impl Workspace {
|
|||||||
.map(|panel| panel.set_zoomed(docks.bottom.zoom, cx));
|
.map(|panel| panel.set_zoomed(docks.bottom.zoom, cx));
|
||||||
|
|
||||||
if docks.bottom.visible && docks.bottom.zoom {
|
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| {
|
.on_action(|this, e: &ToggleLeftDock, cx| {
|
||||||
println!("TOGGLING DOCK");
|
|
||||||
this.toggle_dock(DockPosition::Left, cx);
|
this.toggle_dock(DockPosition::Left, cx);
|
||||||
})
|
})
|
||||||
// cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
|
// cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
|
||||||
@ -3417,7 +3325,6 @@ impl Workspace {
|
|||||||
user_store,
|
user_store,
|
||||||
fs: project.read(cx).fs().clone(),
|
fs: project.read(cx).fs().clone(),
|
||||||
build_window_options: |_, _, _| Default::default(),
|
build_window_options: |_, _, _| Default::default(),
|
||||||
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
|
||||||
node_runtime: FakeNodeRuntime::new(),
|
node_runtime: FakeNodeRuntime::new(),
|
||||||
});
|
});
|
||||||
let workspace = Self::new(0, project, app_state, cx);
|
let workspace = Self::new(0, project, app_state, cx);
|
||||||
@ -3475,8 +3382,8 @@ impl Workspace {
|
|||||||
div
|
div
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_modal<V: Modal + 'static>(&mut self, cx: &ViewContext<Self>) -> Option<View<V>> {
|
pub fn active_modal<V: Modal + 'static>(&mut self, cx: &ViewContext<Self>) -> Option<View<V>> {
|
||||||
self.modal_layer.read(cx).current_modal()
|
self.modal_layer.read(cx).active_modal()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_modal<V: Modal, B>(&mut self, cx: &mut ViewContext<Self>, build: B)
|
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 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 {
|
impl Render for Workspace {
|
||||||
type Element = Div<Self>;
|
type Element = Div<Self>;
|
||||||
|
|
||||||
@ -3716,7 +3629,7 @@ impl Render for Workspace {
|
|||||||
.items_start()
|
.items_start()
|
||||||
.text_color(cx.theme().colors().text)
|
.text_color(cx.theme().colors().text)
|
||||||
.bg(cx.theme().colors().background)
|
.bg(cx.theme().colors().background)
|
||||||
.child(self.render_titlebar(cx))
|
.children(self.titlebar_item.clone())
|
||||||
.child(
|
.child(
|
||||||
// todo! should this be a component a view?
|
// todo! should this be a component a view?
|
||||||
div()
|
div()
|
||||||
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
|||||||
description = "The fast, collaborative code editor."
|
description = "The fast, collaborative code editor."
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.113.0"
|
version = "0.114.0"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
@ -23,11 +23,10 @@ ai = { package = "ai2", path = "../ai2"}
|
|||||||
call = { package = "call2", path = "../call2" }
|
call = { package = "call2", path = "../call2" }
|
||||||
# channel = { path = "../channel" }
|
# channel = { path = "../channel" }
|
||||||
cli = { path = "../cli" }
|
cli = { path = "../cli" }
|
||||||
# collab_ui = { path = "../collab_ui" }
|
collab_ui = { package = "collab_ui2", path = "../collab_ui2" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
command_palette = { package="command_palette2", path = "../command_palette2" }
|
command_palette = { package="command_palette2", path = "../command_palette2" }
|
||||||
# component_test = { path = "../component_test" }
|
# component_test = { path = "../component_test" }
|
||||||
# context_menu = { path = "../context_menu" }
|
|
||||||
client = { package = "client2", path = "../client2" }
|
client = { package = "client2", path = "../client2" }
|
||||||
# clock = { path = "../clock" }
|
# clock = { path = "../clock" }
|
||||||
copilot = { package = "copilot2", path = "../copilot2" }
|
copilot = { package = "copilot2", path = "../copilot2" }
|
||||||
@ -66,7 +65,7 @@ feature_flags = { package = "feature_flags2", path = "../feature_flags2" }
|
|||||||
sum_tree = { path = "../sum_tree" }
|
sum_tree = { path = "../sum_tree" }
|
||||||
shellexpand = "2.1.0"
|
shellexpand = "2.1.0"
|
||||||
text = { package = "text2", path = "../text2" }
|
text = { package = "text2", path = "../text2" }
|
||||||
# terminal_view = { path = "../terminal_view" }
|
terminal_view = { package = "terminal_view2", path = "../terminal_view2" }
|
||||||
theme = { package = "theme2", path = "../theme2" }
|
theme = { package = "theme2", path = "../theme2" }
|
||||||
# theme_selector = { path = "../theme_selector" }
|
# theme_selector = { path = "../theme_selector" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
|
@ -50,8 +50,8 @@ use util::{
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use workspace::{AppState, WorkspaceStore};
|
use workspace::{AppState, WorkspaceStore};
|
||||||
use zed2::{
|
use zed2::{
|
||||||
build_window_options, ensure_only_instance, handle_cli_connection, init_zed_actions,
|
build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace,
|
||||||
initialize_workspace, languages, Assets, IsOnlyInstance, OpenListener, OpenRequest,
|
languages, Assets, IsOnlyInstance, OpenListener, OpenRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod open_listener;
|
mod open_listener;
|
||||||
@ -141,7 +141,6 @@ fn main() {
|
|||||||
cx.set_global(client.clone());
|
cx.set_global(client.clone());
|
||||||
|
|
||||||
theme::init(cx);
|
theme::init(cx);
|
||||||
// context_menu::init(cx);
|
|
||||||
project::Project::init(&client, cx);
|
project::Project::init(&client, cx);
|
||||||
client::init(&client, cx);
|
client::init(&client, cx);
|
||||||
command_palette::init(cx);
|
command_palette::init(cx);
|
||||||
@ -176,7 +175,6 @@ fn main() {
|
|||||||
user_store,
|
user_store,
|
||||||
fs,
|
fs,
|
||||||
build_window_options,
|
build_window_options,
|
||||||
initialize_workspace,
|
|
||||||
// background_actions: todo!("ask Mikayla"),
|
// background_actions: todo!("ask Mikayla"),
|
||||||
workspace_store,
|
workspace_store,
|
||||||
node_runtime,
|
node_runtime,
|
||||||
@ -199,7 +197,7 @@ fn main() {
|
|||||||
search::init(cx);
|
search::init(cx);
|
||||||
// semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
|
// semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
|
||||||
// vim::init(cx);
|
// vim::init(cx);
|
||||||
// terminal_view::init(cx);
|
terminal_view::init(cx);
|
||||||
|
|
||||||
// journal2::init(app_state.clone(), cx);
|
// journal2::init(app_state.clone(), cx);
|
||||||
// language_selector::init(cx);
|
// language_selector::init(cx);
|
||||||
@ -207,13 +205,13 @@ fn main() {
|
|||||||
// activity_indicator::init(cx);
|
// activity_indicator::init(cx);
|
||||||
// language_tools::init(cx);
|
// language_tools::init(cx);
|
||||||
call::init(app_state.client.clone(), app_state.user_store.clone(), 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);
|
// feedback::init(cx);
|
||||||
// welcome::init(cx);
|
// welcome::init(cx);
|
||||||
// zed::init(&app_state, cx);
|
// zed::init(&app_state, cx);
|
||||||
|
|
||||||
// cx.set_menus(menus::menus());
|
// cx.set_menus(menus::menus());
|
||||||
init_zed_actions(app_state.clone(), cx);
|
initialize_workspace(app_state.clone(), cx);
|
||||||
|
|
||||||
if stdout_is_a_pty() {
|
if stdout_is_a_pty() {
|
||||||
cx.activate(true);
|
cx.activate(true);
|
||||||
|
@ -10,16 +10,17 @@ pub use assets::*;
|
|||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
use editor::{Editor, MultiBuffer};
|
use editor::{Editor, MultiBuffer};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, point, px, AppContext, AsyncWindowContext, Context, PromptLevel, Task,
|
actions, point, px, AppContext, Context, PromptLevel, TitlebarOptions, ViewContext,
|
||||||
TitlebarOptions, ViewContext, VisualContext, WeakView, WindowBounds, WindowKind, WindowOptions,
|
VisualContext, WindowBounds, WindowKind, WindowOptions,
|
||||||
};
|
};
|
||||||
pub use only_instance::*;
|
pub use only_instance::*;
|
||||||
pub use open_listener::*;
|
pub use open_listener::*;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _};
|
||||||
use project_panel::ProjectPanel;
|
use project_panel::ProjectPanel;
|
||||||
use settings::{initial_local_settings_content, Settings};
|
use settings::{initial_local_settings_content, Settings};
|
||||||
use std::{borrow::Cow, ops::Deref, sync::Arc};
|
use std::{borrow::Cow, ops::Deref, sync::Arc};
|
||||||
|
use terminal_view::terminal_panel::TerminalPanel;
|
||||||
use util::{
|
use util::{
|
||||||
asset_str,
|
asset_str,
|
||||||
channel::ReleaseChannel,
|
channel::ReleaseChannel,
|
||||||
@ -86,8 +87,147 @@ pub fn build_window_options(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_zed_actions(app_state: Arc<AppState>, cx: &mut AppContext) {
|
pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
cx.observe_new_views(move |workspace: &mut Workspace, _cx| {
|
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
|
workspace
|
||||||
.register_action(about)
|
.register_action(about)
|
||||||
.register_action(|_, _: &Hide, cx| {
|
.register_action(|_, _: &Hide, cx| {
|
||||||
@ -291,154 +431,6 @@ pub fn init_zed_actions(app_state: Arc<AppState>, cx: &mut AppContext) {
|
|||||||
.detach();
|
.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>) {
|
fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
|
||||||
let app_name = cx.global::<ReleaseChannel>().display_name();
|
let app_name = cx.global::<ReleaseChannel>().display_name();
|
||||||
let version = env!("CARGO_PKG_VERSION");
|
let version = env!("CARGO_PKG_VERSION");
|
||||||
|
Loading…
Reference in New Issue
Block a user