Merge pull request #1081 from zed-industries/project-panel-with-new-mouse-events
Introduce context menu to project panel
This commit is contained in:
commit
da46d78ea5
67 changed files with 2493 additions and 673 deletions
27
Cargo.lock
generated
27
Cargo.lock
generated
|
@ -665,6 +665,7 @@ dependencies = [
|
||||||
"client",
|
"client",
|
||||||
"editor",
|
"editor",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"menu",
|
||||||
"postage",
|
"postage",
|
||||||
"settings",
|
"settings",
|
||||||
"theme",
|
"theme",
|
||||||
|
@ -961,6 +962,7 @@ dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"log",
|
"log",
|
||||||
|
"menu",
|
||||||
"picker",
|
"picker",
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
|
@ -971,6 +973,17 @@ dependencies = [
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "context_menu"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"gpui",
|
||||||
|
"menu",
|
||||||
|
"settings",
|
||||||
|
"smallvec",
|
||||||
|
"theme",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.3"
|
version = "0.9.3"
|
||||||
|
@ -1513,6 +1526,7 @@ dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"menu",
|
||||||
"picker",
|
"picker",
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
|
@ -1885,6 +1899,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"editor",
|
"editor",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"menu",
|
||||||
"postage",
|
"postage",
|
||||||
"settings",
|
"settings",
|
||||||
"text",
|
"text",
|
||||||
|
@ -2679,6 +2694,13 @@ dependencies = [
|
||||||
"autocfg 1.0.1",
|
"autocfg 1.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "menu"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"gpui",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "metal"
|
name = "metal"
|
||||||
version = "0.21.0"
|
version = "0.21.0"
|
||||||
|
@ -3184,6 +3206,7 @@ dependencies = [
|
||||||
"editor",
|
"editor",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"menu",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
"theme",
|
"theme",
|
||||||
|
@ -3391,9 +3414,11 @@ dependencies = [
|
||||||
name = "project_panel"
|
name = "project_panel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"context_menu",
|
||||||
"editor",
|
"editor",
|
||||||
"futures",
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"menu",
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -4065,6 +4090,7 @@ dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"log",
|
"log",
|
||||||
|
"menu",
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -5950,6 +5976,7 @@ dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"command_palette",
|
"command_palette",
|
||||||
"contacts_panel",
|
"contacts_panel",
|
||||||
|
"context_menu",
|
||||||
"ctor",
|
"ctor",
|
||||||
"diagnostics",
|
"diagnostics",
|
||||||
"dirs 3.0.1",
|
"dirs 3.0.1",
|
||||||
|
|
|
@ -353,6 +353,10 @@
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"left": "project_panel::CollapseSelectedEntry",
|
"left": "project_panel::CollapseSelectedEntry",
|
||||||
"right": "project_panel::ExpandSelectedEntry",
|
"right": "project_panel::ExpandSelectedEntry",
|
||||||
|
"cmd-x": "project_panel::Cut",
|
||||||
|
"cmd-c": "project_panel::Copy",
|
||||||
|
"cmd-v": "project_panel::Paste",
|
||||||
|
"cmd-alt-c": "project_panel::CopyPath",
|
||||||
"f2": "project_panel::Rename",
|
"f2": "project_panel::Rename",
|
||||||
"backspace": "project_panel::Delete"
|
"backspace": "project_panel::Delete"
|
||||||
}
|
}
|
||||||
|
|
|
@ -270,7 +270,7 @@ impl View for AutoUpdateIndicator {
|
||||||
)
|
)
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.on_click(|_, cx| cx.dispatch_action(DismissErrorMessage))
|
.on_click(|_, _, cx| cx.dispatch_action(DismissErrorMessage))
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
AutoUpdateStatus::Idle => Empty::new().boxed(),
|
AutoUpdateStatus::Idle => Empty::new().boxed(),
|
||||||
|
|
|
@ -11,6 +11,7 @@ doctest = false
|
||||||
client = { path = "../client" }
|
client = { path = "../client" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
|
menu = { path = "../menu" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
|
|
|
@ -11,12 +11,12 @@ use gpui::{
|
||||||
AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
|
AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
|
||||||
ViewContext, ViewHandle,
|
ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
|
use menu::Confirm;
|
||||||
use postage::prelude::Stream;
|
use postage::prelude::Stream;
|
||||||
use settings::{Settings, SoftWrap};
|
use settings::{Settings, SoftWrap};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use time::{OffsetDateTime, UtcOffset};
|
use time::{OffsetDateTime, UtcOffset};
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
use workspace::menu::Confirm;
|
|
||||||
|
|
||||||
const MESSAGE_LOADING_THRESHOLD: usize = 50;
|
const MESSAGE_LOADING_THRESHOLD: usize = 50;
|
||||||
|
|
||||||
|
@ -75,9 +75,9 @@ impl ChatPanel {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut message_list = ListState::new(0, Orientation::Bottom, 1000., {
|
let mut message_list = ListState::new(0, Orientation::Bottom, 1000., cx, {
|
||||||
let this = cx.weak_handle();
|
let this = cx.weak_handle();
|
||||||
move |ix, cx| {
|
move |_, ix, cx| {
|
||||||
let this = this.upgrade(cx).unwrap().read(cx);
|
let this = this.upgrade(cx).unwrap().read(cx);
|
||||||
let message = this.active_channel.as_ref().unwrap().0.read(cx).message(ix);
|
let message = this.active_channel.as_ref().unwrap().0.read(cx).message(ix);
|
||||||
this.render_message(message, cx)
|
this.render_message(message, cx)
|
||||||
|
@ -320,7 +320,7 @@ impl ChatPanel {
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(move |_, cx| {
|
.on_click(move |_, _, cx| {
|
||||||
let rpc = rpc.clone();
|
let rpc = rpc.clone();
|
||||||
let this = this.clone();
|
let this = this.clone();
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
|
|
|
@ -656,6 +656,9 @@ async fn test_fs_operations(
|
||||||
cx_b: &mut TestAppContext,
|
cx_b: &mut TestAppContext,
|
||||||
) {
|
) {
|
||||||
executor.forbid_parking();
|
executor.forbid_parking();
|
||||||
|
let fs = FakeFs::new(cx_a.background());
|
||||||
|
|
||||||
|
// Connect to a server as 2 clients.
|
||||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||||
let mut client_a = server.create_client(cx_a, "user_a").await;
|
let mut client_a = server.create_client(cx_a, "user_a").await;
|
||||||
let mut client_b = server.create_client(cx_b, "user_b").await;
|
let mut client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
@ -663,7 +666,7 @@ async fn test_fs_operations(
|
||||||
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
|
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let fs = FakeFs::new(cx_a.background());
|
// Share a project as client A
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
"/dir",
|
"/dir",
|
||||||
json!({
|
json!({
|
||||||
|
@ -759,6 +762,110 @@ async fn test_fs_operations(
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
project_b
|
||||||
|
.update(cx_b, |project, cx| {
|
||||||
|
project
|
||||||
|
.create_entry((worktree_id, "DIR/e.txt"), false, cx)
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
project_b
|
||||||
|
.update(cx_b, |project, cx| {
|
||||||
|
project
|
||||||
|
.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
project_b
|
||||||
|
.update(cx_b, |project, cx| {
|
||||||
|
project
|
||||||
|
.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
worktree_a.read_with(cx_a, |worktree, _| {
|
||||||
|
assert_eq!(
|
||||||
|
worktree
|
||||||
|
.paths()
|
||||||
|
.map(|p| p.to_string_lossy())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
[
|
||||||
|
"DIR",
|
||||||
|
"DIR/SUBDIR",
|
||||||
|
"DIR/SUBDIR/f.txt",
|
||||||
|
"DIR/e.txt",
|
||||||
|
"a.txt",
|
||||||
|
"b.txt",
|
||||||
|
"d.txt"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
worktree_b.read_with(cx_b, |worktree, _| {
|
||||||
|
assert_eq!(
|
||||||
|
worktree
|
||||||
|
.paths()
|
||||||
|
.map(|p| p.to_string_lossy())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
[
|
||||||
|
"DIR",
|
||||||
|
"DIR/SUBDIR",
|
||||||
|
"DIR/SUBDIR/f.txt",
|
||||||
|
"DIR/e.txt",
|
||||||
|
"a.txt",
|
||||||
|
"b.txt",
|
||||||
|
"d.txt"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
project_b
|
||||||
|
.update(cx_b, |project, cx| {
|
||||||
|
project
|
||||||
|
.copy_entry(entry.id, Path::new("f.txt"), cx)
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
worktree_a.read_with(cx_a, |worktree, _| {
|
||||||
|
assert_eq!(
|
||||||
|
worktree
|
||||||
|
.paths()
|
||||||
|
.map(|p| p.to_string_lossy())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
[
|
||||||
|
"DIR",
|
||||||
|
"DIR/SUBDIR",
|
||||||
|
"DIR/SUBDIR/f.txt",
|
||||||
|
"DIR/e.txt",
|
||||||
|
"a.txt",
|
||||||
|
"b.txt",
|
||||||
|
"d.txt",
|
||||||
|
"f.txt"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
worktree_b.read_with(cx_b, |worktree, _| {
|
||||||
|
assert_eq!(
|
||||||
|
worktree
|
||||||
|
.paths()
|
||||||
|
.map(|p| p.to_string_lossy())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
[
|
||||||
|
"DIR",
|
||||||
|
"DIR/SUBDIR",
|
||||||
|
"DIR/SUBDIR/f.txt",
|
||||||
|
"DIR/e.txt",
|
||||||
|
"a.txt",
|
||||||
|
"b.txt",
|
||||||
|
"d.txt",
|
||||||
|
"f.txt"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
project_b
|
project_b
|
||||||
.update(cx_b, |project, cx| {
|
.update(cx_b, |project, cx| {
|
||||||
project.delete_entry(dir_entry.id, cx).unwrap()
|
project.delete_entry(dir_entry.id, cx).unwrap()
|
||||||
|
@ -771,7 +878,7 @@ async fn test_fs_operations(
|
||||||
.paths()
|
.paths()
|
||||||
.map(|p| p.to_string_lossy())
|
.map(|p| p.to_string_lossy())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
["a.txt", "b.txt", "d.txt"]
|
["a.txt", "b.txt", "d.txt", "f.txt"]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
worktree_b.read_with(cx_b, |worktree, _| {
|
worktree_b.read_with(cx_b, |worktree, _| {
|
||||||
|
@ -780,7 +887,7 @@ async fn test_fs_operations(
|
||||||
.paths()
|
.paths()
|
||||||
.map(|p| p.to_string_lossy())
|
.map(|p| p.to_string_lossy())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
["a.txt", "b.txt", "d.txt"]
|
["a.txt", "b.txt", "d.txt", "f.txt"]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -796,7 +903,7 @@ async fn test_fs_operations(
|
||||||
.paths()
|
.paths()
|
||||||
.map(|p| p.to_string_lossy())
|
.map(|p| p.to_string_lossy())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
["a.txt", "b.txt"]
|
["a.txt", "b.txt", "f.txt"]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
worktree_b.read_with(cx_b, |worktree, _| {
|
worktree_b.read_with(cx_b, |worktree, _| {
|
||||||
|
@ -805,7 +912,7 @@ async fn test_fs_operations(
|
||||||
.paths()
|
.paths()
|
||||||
.map(|p| p.to_string_lossy())
|
.map(|p| p.to_string_lossy())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
["a.txt", "b.txt"]
|
["a.txt", "b.txt", "f.txt"]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,7 @@ pub fn init_tracing(config: &Config) -> Option<()> {
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
let rust_log = config.rust_log.clone()?;
|
let rust_log = config.rust_log.clone()?;
|
||||||
|
|
||||||
|
println!("HEY!");
|
||||||
LogTracer::init().log_err()?;
|
LogTracer::init().log_err()?;
|
||||||
|
|
||||||
let subscriber = tracing_subscriber::Registry::default()
|
let subscriber = tracing_subscriber::Registry::default()
|
||||||
|
|
|
@ -171,6 +171,7 @@ impl Server {
|
||||||
.add_request_handler(Server::forward_project_request::<proto::FormatBuffers>)
|
.add_request_handler(Server::forward_project_request::<proto::FormatBuffers>)
|
||||||
.add_request_handler(Server::forward_project_request::<proto::CreateProjectEntry>)
|
.add_request_handler(Server::forward_project_request::<proto::CreateProjectEntry>)
|
||||||
.add_request_handler(Server::forward_project_request::<proto::RenameProjectEntry>)
|
.add_request_handler(Server::forward_project_request::<proto::RenameProjectEntry>)
|
||||||
|
.add_request_handler(Server::forward_project_request::<proto::CopyProjectEntry>)
|
||||||
.add_request_handler(Server::forward_project_request::<proto::DeleteProjectEntry>)
|
.add_request_handler(Server::forward_project_request::<proto::DeleteProjectEntry>)
|
||||||
.add_request_handler(Server::update_buffer)
|
.add_request_handler(Server::update_buffer)
|
||||||
.add_message_handler(Server::update_buffer_file)
|
.add_message_handler(Server::update_buffer_file)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
elements::{ChildView, Flex, Label, MouseState, ParentElement},
|
elements::{ChildView, Flex, Label, ParentElement},
|
||||||
keymap::Keystroke,
|
keymap::Keystroke,
|
||||||
Action, Element, Entity, MutableAppContext, View, ViewContext, ViewHandle,
|
Action, Element, Entity, MouseState, MutableAppContext, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
@ -203,7 +203,7 @@ impl PickerDelegate for CommandPalette {
|
||||||
fn render_match(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
mouse_state: &MouseState,
|
mouse_state: MouseState,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
cx: &gpui::AppContext,
|
cx: &gpui::AppContext,
|
||||||
) -> gpui::ElementBox {
|
) -> gpui::ElementBox {
|
||||||
|
|
|
@ -12,6 +12,7 @@ client = { path = "../client" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
|
menu = { path = "../menu" }
|
||||||
picker = { path = "../picker" }
|
picker = { path = "../picker" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use client::{ContactRequestStatus, User, UserStore};
|
use client::{ContactRequestStatus, User, UserStore};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, elements::*, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
|
actions, elements::*, Entity, ModelHandle, MouseState, MutableAppContext, RenderContext, Task,
|
||||||
ViewContext, ViewHandle,
|
View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
@ -105,7 +105,7 @@ impl PickerDelegate for ContactFinder {
|
||||||
fn render_match(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
mouse_state: &MouseState,
|
mouse_state: MouseState,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
cx: &gpui::AppContext,
|
cx: &gpui::AppContext,
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
|
|
|
@ -12,19 +12,16 @@ use gpui::{
|
||||||
geometry::{rect::RectF, vector::vec2f},
|
geometry::{rect::RectF, vector::vec2f},
|
||||||
impl_actions, impl_internal_actions,
|
impl_actions, impl_internal_actions,
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
AppContext, ClipboardItem, Element, ElementBox, Entity, LayoutContext, ModelHandle,
|
AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MutableAppContext,
|
||||||
MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
|
RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use join_project_notification::JoinProjectNotification;
|
use join_project_notification::JoinProjectNotification;
|
||||||
|
use menu::{Confirm, SelectNext, SelectPrev};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use theme::IconButton;
|
use theme::IconButton;
|
||||||
use workspace::{
|
use workspace::{sidebar::SidebarItem, JoinProject, Workspace};
|
||||||
menu::{Confirm, SelectNext, SelectPrev},
|
|
||||||
sidebar::SidebarItem,
|
|
||||||
JoinProject, Workspace,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl_actions!(
|
impl_actions!(
|
||||||
contacts_panel,
|
contacts_panel,
|
||||||
|
@ -184,11 +181,8 @@ impl ContactsPanel {
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
list_state: ListState::new(0, Orientation::Top, 1000., {
|
list_state: ListState::new(0, Orientation::Top, 1000., cx, {
|
||||||
let this = cx.weak_handle();
|
move |this, ix, cx| {
|
||||||
move |ix, cx| {
|
|
||||||
let this = this.upgrade(cx).unwrap();
|
|
||||||
let this = this.read(cx);
|
|
||||||
let theme = cx.global::<Settings>().theme.clone();
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
let theme = &theme.contacts_panel;
|
let theme = &theme.contacts_panel;
|
||||||
let current_user_id =
|
let current_user_id =
|
||||||
|
@ -258,11 +252,11 @@ impl ContactsPanel {
|
||||||
theme: &theme::ContactsPanel,
|
theme: &theme::ContactsPanel,
|
||||||
is_selected: bool,
|
is_selected: bool,
|
||||||
is_collapsed: bool,
|
is_collapsed: bool,
|
||||||
cx: &mut LayoutContext,
|
cx: &mut RenderContext<Self>,
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
enum Header {}
|
enum Header {}
|
||||||
|
|
||||||
let header_style = theme.header_row.style_for(&Default::default(), is_selected);
|
let header_style = theme.header_row.style_for(Default::default(), is_selected);
|
||||||
let text = match section {
|
let text = match section {
|
||||||
Section::Requests => "Requests",
|
Section::Requests => "Requests",
|
||||||
Section::Online => "Online",
|
Section::Online => "Online",
|
||||||
|
@ -302,7 +296,7 @@ impl ContactsPanel {
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(move |_, cx| cx.dispatch_action(ToggleExpanded(section)))
|
.on_click(move |_, _, cx| cx.dispatch_action(ToggleExpanded(section)))
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,11 +328,7 @@ impl ContactsPanel {
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_height(theme.row_height)
|
.with_height(theme.row_height)
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(
|
.with_style(*theme.contact_row.style_for(Default::default(), is_selected))
|
||||||
*theme
|
|
||||||
.contact_row
|
|
||||||
.style_for(&Default::default(), is_selected),
|
|
||||||
)
|
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,7 +339,7 @@ impl ContactsPanel {
|
||||||
theme: &theme::ContactsPanel,
|
theme: &theme::ContactsPanel,
|
||||||
is_last_project: bool,
|
is_last_project: bool,
|
||||||
is_selected: bool,
|
is_selected: bool,
|
||||||
cx: &mut LayoutContext,
|
cx: &mut RenderContext<Self>,
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
let project = &contact.projects[project_index];
|
let project = &contact.projects[project_index];
|
||||||
let project_id = project.id;
|
let project_id = project.id;
|
||||||
|
@ -445,7 +435,7 @@ impl ContactsPanel {
|
||||||
} else {
|
} else {
|
||||||
CursorStyle::Arrow
|
CursorStyle::Arrow
|
||||||
})
|
})
|
||||||
.on_click(move |_, cx| {
|
.on_click(move |_, _, cx| {
|
||||||
if !is_host {
|
if !is_host {
|
||||||
cx.dispatch_global_action(JoinProject {
|
cx.dispatch_global_action(JoinProject {
|
||||||
contact: contact.clone(),
|
contact: contact.clone(),
|
||||||
|
@ -462,7 +452,7 @@ impl ContactsPanel {
|
||||||
theme: &theme::ContactsPanel,
|
theme: &theme::ContactsPanel,
|
||||||
is_incoming: bool,
|
is_incoming: bool,
|
||||||
is_selected: bool,
|
is_selected: bool,
|
||||||
cx: &mut LayoutContext,
|
cx: &mut RenderContext<ContactsPanel>,
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
enum Decline {}
|
enum Decline {}
|
||||||
enum Accept {}
|
enum Accept {}
|
||||||
|
@ -507,7 +497,7 @@ impl ContactsPanel {
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(move |_, cx| {
|
.on_click(move |_, _, cx| {
|
||||||
cx.dispatch_action(RespondToContactRequest {
|
cx.dispatch_action(RespondToContactRequest {
|
||||||
user_id,
|
user_id,
|
||||||
accept: false,
|
accept: false,
|
||||||
|
@ -529,7 +519,7 @@ impl ContactsPanel {
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(move |_, cx| {
|
.on_click(move |_, _, cx| {
|
||||||
cx.dispatch_action(RespondToContactRequest {
|
cx.dispatch_action(RespondToContactRequest {
|
||||||
user_id,
|
user_id,
|
||||||
accept: true,
|
accept: true,
|
||||||
|
@ -552,7 +542,7 @@ impl ContactsPanel {
|
||||||
})
|
})
|
||||||
.with_padding(Padding::uniform(2.))
|
.with_padding(Padding::uniform(2.))
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(move |_, cx| cx.dispatch_action(RemoveContact(user_id)))
|
.on_click(move |_, _, cx| cx.dispatch_action(RemoveContact(user_id)))
|
||||||
.flex_float()
|
.flex_float()
|
||||||
.boxed(),
|
.boxed(),
|
||||||
);
|
);
|
||||||
|
@ -561,11 +551,7 @@ impl ContactsPanel {
|
||||||
row.constrained()
|
row.constrained()
|
||||||
.with_height(theme.row_height)
|
.with_height(theme.row_height)
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(
|
.with_style(*theme.contact_row.style_for(Default::default(), is_selected))
|
||||||
*theme
|
|
||||||
.contact_row
|
|
||||||
.style_for(&Default::default(), is_selected),
|
|
||||||
)
|
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -865,7 +851,7 @@ impl View for ContactsPanel {
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(|_, cx| cx.dispatch_action(contact_finder::Toggle))
|
.on_click(|_, _, cx| cx.dispatch_action(contact_finder::Toggle))
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.constrained()
|
.constrained()
|
||||||
|
@ -913,7 +899,7 @@ impl View for ContactsPanel {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(move |_, cx| {
|
.on_click(move |_, _, cx| {
|
||||||
cx.write_to_clipboard(ClipboardItem::new(
|
cx.write_to_clipboard(ClipboardItem::new(
|
||||||
info.url.to_string(),
|
info.url.to_string(),
|
||||||
));
|
));
|
||||||
|
|
|
@ -61,7 +61,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.with_padding(Padding::uniform(5.))
|
.with_padding(Padding::uniform(5.))
|
||||||
.on_click(move |_, cx| cx.dispatch_any_action(dismiss_action.boxed_clone()))
|
.on_click(move |_, _, cx| cx.dispatch_any_action(dismiss_action.boxed_clone()))
|
||||||
.aligned()
|
.aligned()
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_height(
|
.with_height(
|
||||||
|
@ -76,10 +76,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||||
.named("contact notification header"),
|
.named("contact notification header"),
|
||||||
)
|
)
|
||||||
.with_children(body.map(|body| {
|
.with_children(body.map(|body| {
|
||||||
Label::new(
|
Label::new(body.to_string(), theme.body_message.text.clone())
|
||||||
body.to_string(),
|
|
||||||
theme.body_message.text.clone(),
|
|
||||||
)
|
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(theme.body_message.container)
|
.with_style(theme.body_message.container)
|
||||||
.boxed()
|
.boxed()
|
||||||
|
@ -99,7 +96,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(move |_, cx| cx.dispatch_any_action(action.boxed_clone()))
|
.on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone()))
|
||||||
.boxed()
|
.boxed()
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
|
|
15
crates/context_menu/Cargo.toml
Normal file
15
crates/context_menu/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "context_menu"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/context_menu.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
menu = { path = "../menu" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
smallvec = "1.6"
|
332
crates/context_menu/src/context_menu.rs
Normal file
332
crates/context_menu/src/context_menu.rs
Normal file
|
@ -0,0 +1,332 @@
|
||||||
|
use std::{any::TypeId, time::Duration};
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
elements::*, geometry::vector::Vector2F, keymap, platform::CursorStyle, Action, AppContext,
|
||||||
|
Axis, Entity, MutableAppContext, RenderContext, SizeConstraint, Subscription, View,
|
||||||
|
ViewContext,
|
||||||
|
};
|
||||||
|
use menu::*;
|
||||||
|
use settings::Settings;
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
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);
|
||||||
|
cx.add_action(ContextMenu::cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ContextMenuItem {
|
||||||
|
Item {
|
||||||
|
label: String,
|
||||||
|
action: Box<dyn Action>,
|
||||||
|
},
|
||||||
|
Separator,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextMenuItem {
|
||||||
|
pub fn item(label: impl ToString, action: impl 'static + Action) -> Self {
|
||||||
|
Self::Item {
|
||||||
|
label: label.to_string(),
|
||||||
|
action: Box::new(action),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn separator() -> Self {
|
||||||
|
Self::Separator
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_separator(&self) -> bool {
|
||||||
|
matches!(self, Self::Separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_id(&self) -> Option<TypeId> {
|
||||||
|
match self {
|
||||||
|
ContextMenuItem::Item { action, .. } => Some(action.id()),
|
||||||
|
ContextMenuItem::Separator => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ContextMenu {
|
||||||
|
position: Vector2F,
|
||||||
|
items: Vec<ContextMenuItem>,
|
||||||
|
selected_index: Option<usize>,
|
||||||
|
visible: bool,
|
||||||
|
previously_focused_view_id: Option<usize>,
|
||||||
|
_actions_observation: Subscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for ContextMenu {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for ContextMenu {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"ContextMenu"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
||||||
|
let mut cx = Self::default_keymap_context();
|
||||||
|
cx.set.insert("menu".into());
|
||||||
|
cx
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
if !self.visible {
|
||||||
|
return Empty::new().boxed();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the menu once at minimum width.
|
||||||
|
let mut collapsed_menu = self.render_menu_for_measurement(cx).boxed();
|
||||||
|
let expanded_menu = self
|
||||||
|
.render_menu(cx)
|
||||||
|
.constrained()
|
||||||
|
.dynamically(move |constraint, cx| {
|
||||||
|
SizeConstraint::strict_along(
|
||||||
|
Axis::Horizontal,
|
||||||
|
collapsed_menu.layout(constraint, cx).x(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.boxed();
|
||||||
|
|
||||||
|
Overlay::new(expanded_menu)
|
||||||
|
.with_abs_position(self.position)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
self.reset(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextMenu {
|
||||||
|
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
Self {
|
||||||
|
position: Default::default(),
|
||||||
|
items: Default::default(),
|
||||||
|
selected_index: Default::default(),
|
||||||
|
visible: Default::default(),
|
||||||
|
previously_focused_view_id: Default::default(),
|
||||||
|
_actions_observation: cx.observe_actions(Self::action_dispatched),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_dispatched(&mut self, action_id: TypeId, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(ix) = self
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.position(|item| item.action_id() == Some(action_id))
|
||||||
|
{
|
||||||
|
self.selected_index = Some(ix);
|
||||||
|
cx.notify();
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
cx.background().timer(Duration::from_millis(100)).await;
|
||||||
|
this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx));
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(ix) = self.selected_index {
|
||||||
|
if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
|
||||||
|
let window_id = cx.window_id();
|
||||||
|
let view_id = cx.view_id();
|
||||||
|
cx.dispatch_action_at(window_id, view_id, action.as_ref());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||||
|
self.reset(cx);
|
||||||
|
cx.defer(|this, cx| {
|
||||||
|
if cx.handle().is_focused(cx) {
|
||||||
|
let window_id = cx.window_id();
|
||||||
|
(**cx).focus(window_id, this.previously_focused_view_id.take());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
self.items.clear();
|
||||||
|
self.visible = false;
|
||||||
|
self.selected_index.take();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
|
||||||
|
self.selected_index = self.items.iter().position(|item| !item.is_separator());
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
|
||||||
|
for (ix, item) in self.items.iter().enumerate().rev() {
|
||||||
|
if !item.is_separator() {
|
||||||
|
self.selected_index = Some(ix);
|
||||||
|
cx.notify();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(ix) = self.selected_index {
|
||||||
|
for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
|
||||||
|
if !item.is_separator() {
|
||||||
|
self.selected_index = Some(ix);
|
||||||
|
cx.notify();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.select_first(&Default::default(), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(ix) = self.selected_index {
|
||||||
|
for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
|
||||||
|
if !item.is_separator() {
|
||||||
|
self.selected_index = Some(ix);
|
||||||
|
cx.notify();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.select_last(&Default::default(), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(
|
||||||
|
&mut self,
|
||||||
|
position: Vector2F,
|
||||||
|
items: impl IntoIterator<Item = ContextMenuItem>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
let mut items = items.into_iter().peekable();
|
||||||
|
if items.peek().is_some() {
|
||||||
|
self.items = items.collect();
|
||||||
|
self.position = position;
|
||||||
|
self.visible = true;
|
||||||
|
if !cx.is_self_focused() {
|
||||||
|
self.previously_focused_view_id = cx.focused_view_id(cx.window_id());
|
||||||
|
}
|
||||||
|
cx.focus_self();
|
||||||
|
} else {
|
||||||
|
self.visible = false;
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
|
||||||
|
let style = cx.global::<Settings>().theme.context_menu.clone();
|
||||||
|
Flex::row()
|
||||||
|
.with_child(
|
||||||
|
Flex::column()
|
||||||
|
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||||
|
match item {
|
||||||
|
ContextMenuItem::Item { label, .. } => {
|
||||||
|
let style = style
|
||||||
|
.item
|
||||||
|
.style_for(Default::default(), Some(ix) == self.selected_index);
|
||||||
|
Label::new(label.to_string(), style.label.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
ContextMenuItem::Separator => Empty::new()
|
||||||
|
.collapsed()
|
||||||
|
.contained()
|
||||||
|
.with_style(style.separator)
|
||||||
|
.constrained()
|
||||||
|
.with_height(1.)
|
||||||
|
.boxed(),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Flex::column()
|
||||||
|
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||||
|
match item {
|
||||||
|
ContextMenuItem::Item { action, .. } => {
|
||||||
|
let style = style
|
||||||
|
.item
|
||||||
|
.style_for(Default::default(), Some(ix) == self.selected_index);
|
||||||
|
KeystrokeLabel::new(
|
||||||
|
action.boxed_clone(),
|
||||||
|
style.keystroke.container,
|
||||||
|
style.keystroke.text.clone(),
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
ContextMenuItem::Separator => Empty::new()
|
||||||
|
.collapsed()
|
||||||
|
.constrained()
|
||||||
|
.with_height(1.)
|
||||||
|
.contained()
|
||||||
|
.with_style(style.separator)
|
||||||
|
.boxed(),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_menu(&self, cx: &mut RenderContext<Self>) -> impl Element {
|
||||||
|
enum Menu {}
|
||||||
|
enum MenuItem {}
|
||||||
|
let style = cx.global::<Settings>().theme.context_menu.clone();
|
||||||
|
MouseEventHandler::new::<Menu, _, _>(0, cx, |_, cx| {
|
||||||
|
Flex::column()
|
||||||
|
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||||
|
match item {
|
||||||
|
ContextMenuItem::Item { label, action } => {
|
||||||
|
let action = action.boxed_clone();
|
||||||
|
MouseEventHandler::new::<MenuItem, _, _>(ix, cx, |state, _| {
|
||||||
|
let style =
|
||||||
|
style.item.style_for(state, Some(ix) == self.selected_index);
|
||||||
|
Flex::row()
|
||||||
|
.with_child(
|
||||||
|
Label::new(label.to_string(), style.label.clone()).boxed(),
|
||||||
|
)
|
||||||
|
.with_child({
|
||||||
|
KeystrokeLabel::new(
|
||||||
|
action.boxed_clone(),
|
||||||
|
style.keystroke.container,
|
||||||
|
style.keystroke.text.clone(),
|
||||||
|
)
|
||||||
|
.flex_float()
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(move |_, _, cx| {
|
||||||
|
cx.dispatch_any_action(action.boxed_clone());
|
||||||
|
cx.dispatch_action(Cancel);
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
ContextMenuItem::Separator => Empty::new()
|
||||||
|
.constrained()
|
||||||
|
.with_height(1.)
|
||||||
|
.contained()
|
||||||
|
.with_style(style.separator)
|
||||||
|
.boxed(),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.on_mouse_down_out(|_, cx| cx.dispatch_action(Cancel))
|
||||||
|
.on_right_mouse_down_out(|_, cx| cx.dispatch_action(Cancel))
|
||||||
|
}
|
||||||
|
}
|
|
@ -159,7 +159,7 @@ impl View for DiagnosticIndicator {
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(|_, cx| cx.dispatch_action(crate::Deploy))
|
.on_click(|_, _, cx| cx.dispatch_action(crate::Deploy))
|
||||||
.aligned()
|
.aligned()
|
||||||
.boxed(),
|
.boxed(),
|
||||||
);
|
);
|
||||||
|
@ -192,7 +192,7 @@ impl View for DiagnosticIndicator {
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(|_, cx| cx.dispatch_action(GoToNextDiagnostic))
|
.on_click(|_, _, cx| cx.dispatch_action(GoToNextDiagnostic))
|
||||||
.boxed(),
|
.boxed(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -592,11 +592,11 @@ impl ContextMenu {
|
||||||
&self,
|
&self,
|
||||||
cursor_position: DisplayPoint,
|
cursor_position: DisplayPoint,
|
||||||
style: EditorStyle,
|
style: EditorStyle,
|
||||||
cx: &AppContext,
|
cx: &mut RenderContext<Editor>,
|
||||||
) -> (DisplayPoint, ElementBox) {
|
) -> (DisplayPoint, ElementBox) {
|
||||||
match self {
|
match self {
|
||||||
ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)),
|
ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)),
|
||||||
ContextMenu::CodeActions(menu) => menu.render(cursor_position, style),
|
ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -633,14 +633,18 @@ impl CompletionsMenu {
|
||||||
!self.matches.is_empty()
|
!self.matches.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&self, style: EditorStyle, _: &AppContext) -> ElementBox {
|
fn render(&self, style: EditorStyle, cx: &mut RenderContext<Editor>) -> ElementBox {
|
||||||
enum CompletionTag {}
|
enum CompletionTag {}
|
||||||
|
|
||||||
let completions = self.completions.clone();
|
let completions = self.completions.clone();
|
||||||
let matches = self.matches.clone();
|
let matches = self.matches.clone();
|
||||||
let selected_item = self.selected_item;
|
let selected_item = self.selected_item;
|
||||||
let container_style = style.autocomplete.container;
|
let container_style = style.autocomplete.container;
|
||||||
UniformList::new(self.list.clone(), matches.len(), move |range, items, cx| {
|
UniformList::new(
|
||||||
|
self.list.clone(),
|
||||||
|
matches.len(),
|
||||||
|
cx,
|
||||||
|
move |_, range, items, cx| {
|
||||||
let start_ix = range.start;
|
let start_ix = range.start;
|
||||||
for (ix, mat) in matches[range].iter().enumerate() {
|
for (ix, mat) in matches[range].iter().enumerate() {
|
||||||
let completion = &completions[mat.candidate_id];
|
let completion = &completions[mat.candidate_id];
|
||||||
|
@ -663,7 +667,10 @@ impl CompletionsMenu {
|
||||||
.with_highlights(combine_syntax_and_fuzzy_match_highlights(
|
.with_highlights(combine_syntax_and_fuzzy_match_highlights(
|
||||||
&completion.label.text,
|
&completion.label.text,
|
||||||
style.text.color.into(),
|
style.text.color.into(),
|
||||||
styled_runs_for_code_label(&completion.label, &style.syntax),
|
styled_runs_for_code_label(
|
||||||
|
&completion.label,
|
||||||
|
&style.syntax,
|
||||||
|
),
|
||||||
&mat.positions,
|
&mat.positions,
|
||||||
))
|
))
|
||||||
.contained()
|
.contained()
|
||||||
|
@ -672,7 +679,7 @@ impl CompletionsMenu {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_mouse_down(move |cx| {
|
.on_mouse_down(move |_, cx| {
|
||||||
cx.dispatch_action(ConfirmCompletion {
|
cx.dispatch_action(ConfirmCompletion {
|
||||||
item_ix: Some(item_ix),
|
item_ix: Some(item_ix),
|
||||||
});
|
});
|
||||||
|
@ -680,7 +687,8 @@ impl CompletionsMenu {
|
||||||
.boxed(),
|
.boxed(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
.with_width_from_item(
|
.with_width_from_item(
|
||||||
self.matches
|
self.matches
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -772,14 +780,18 @@ impl CodeActionsMenu {
|
||||||
&self,
|
&self,
|
||||||
mut cursor_position: DisplayPoint,
|
mut cursor_position: DisplayPoint,
|
||||||
style: EditorStyle,
|
style: EditorStyle,
|
||||||
|
cx: &mut RenderContext<Editor>,
|
||||||
) -> (DisplayPoint, ElementBox) {
|
) -> (DisplayPoint, ElementBox) {
|
||||||
enum ActionTag {}
|
enum ActionTag {}
|
||||||
|
|
||||||
let container_style = style.autocomplete.container;
|
let container_style = style.autocomplete.container;
|
||||||
let actions = self.actions.clone();
|
let actions = self.actions.clone();
|
||||||
let selected_item = self.selected_item;
|
let selected_item = self.selected_item;
|
||||||
let element =
|
let element = UniformList::new(
|
||||||
UniformList::new(self.list.clone(), actions.len(), move |range, items, cx| {
|
self.list.clone(),
|
||||||
|
actions.len(),
|
||||||
|
cx,
|
||||||
|
move |_, range, items, cx| {
|
||||||
let start_ix = range.start;
|
let start_ix = range.start;
|
||||||
for (ix, action) in actions[range].iter().enumerate() {
|
for (ix, action) in actions[range].iter().enumerate() {
|
||||||
let item_ix = start_ix + ix;
|
let item_ix = start_ix + ix;
|
||||||
|
@ -800,7 +812,7 @@ impl CodeActionsMenu {
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_mouse_down(move |cx| {
|
.on_mouse_down(move |_, cx| {
|
||||||
cx.dispatch_action(ConfirmCodeAction {
|
cx.dispatch_action(ConfirmCodeAction {
|
||||||
item_ix: Some(item_ix),
|
item_ix: Some(item_ix),
|
||||||
});
|
});
|
||||||
|
@ -808,7 +820,8 @@ impl CodeActionsMenu {
|
||||||
.boxed(),
|
.boxed(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
.with_width_from_item(
|
.with_width_from_item(
|
||||||
self.actions
|
self.actions
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -2572,7 +2585,7 @@ impl Editor {
|
||||||
pub fn render_code_actions_indicator(
|
pub fn render_code_actions_indicator(
|
||||||
&self,
|
&self,
|
||||||
style: &EditorStyle,
|
style: &EditorStyle,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut RenderContext<Self>,
|
||||||
) -> Option<ElementBox> {
|
) -> Option<ElementBox> {
|
||||||
if self.available_code_actions.is_some() {
|
if self.available_code_actions.is_some() {
|
||||||
enum Tag {}
|
enum Tag {}
|
||||||
|
@ -2584,7 +2597,7 @@ impl Editor {
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.with_padding(Padding::uniform(3.))
|
.with_padding(Padding::uniform(3.))
|
||||||
.on_mouse_down(|cx| {
|
.on_mouse_down(|_, cx| {
|
||||||
cx.dispatch_action(ToggleCodeActions {
|
cx.dispatch_action(ToggleCodeActions {
|
||||||
deployed_from_indicator: true,
|
deployed_from_indicator: true,
|
||||||
});
|
});
|
||||||
|
@ -2606,7 +2619,7 @@ impl Editor {
|
||||||
&self,
|
&self,
|
||||||
cursor_position: DisplayPoint,
|
cursor_position: DisplayPoint,
|
||||||
style: EditorStyle,
|
style: EditorStyle,
|
||||||
cx: &AppContext,
|
cx: &mut RenderContext<Editor>,
|
||||||
) -> Option<(DisplayPoint, ElementBox)> {
|
) -> Option<(DisplayPoint, ElementBox)> {
|
||||||
self.context_menu
|
self.context_menu
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
|
@ -21,8 +21,9 @@ use gpui::{
|
||||||
json::{self, ToJson},
|
json::{self, ToJson},
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
text_layout::{self, Line, RunStyle, TextLayoutCache},
|
text_layout::{self, Line, RunStyle, TextLayoutCache},
|
||||||
AppContext, Axis, Border, Element, ElementBox, Event, EventContext, LayoutContext,
|
AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext,
|
||||||
MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
|
LayoutContext, MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext,
|
||||||
|
WeakViewHandle,
|
||||||
};
|
};
|
||||||
use json::json;
|
use json::json;
|
||||||
use language::{Bias, DiagnosticSeverity, Selection};
|
use language::{Bias, DiagnosticSeverity, Selection};
|
||||||
|
@ -362,7 +363,10 @@ impl EditorElement {
|
||||||
let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
|
let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
|
||||||
|
|
||||||
cx.scene.push_layer(Some(bounds));
|
cx.scene.push_layer(Some(bounds));
|
||||||
cx.scene.push_cursor_style(bounds, CursorStyle::IBeam);
|
cx.scene.push_cursor_region(CursorRegion {
|
||||||
|
bounds,
|
||||||
|
style: CursorStyle::IBeam,
|
||||||
|
});
|
||||||
|
|
||||||
for (range, color) in &layout.highlighted_ranges {
|
for (range, color) in &layout.highlighted_ranges {
|
||||||
self.paint_highlighted_range(
|
self.paint_highlighted_range(
|
||||||
|
@ -1041,8 +1045,6 @@ impl Element for EditorElement {
|
||||||
max_row.saturating_sub(1) as f32,
|
max_row.saturating_sub(1) as f32,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut context_menu = None;
|
|
||||||
let mut code_actions_indicator = None;
|
|
||||||
self.update_view(cx.app, |view, cx| {
|
self.update_view(cx.app, |view, cx| {
|
||||||
let clamped = view.clamp_scroll_left(scroll_max.x());
|
let clamped = view.clamp_scroll_left(scroll_max.x());
|
||||||
let autoscrolled;
|
let autoscrolled;
|
||||||
|
@ -1062,7 +1064,11 @@ impl Element for EditorElement {
|
||||||
if clamped || autoscrolled {
|
if clamped || autoscrolled {
|
||||||
snapshot = view.snapshot(cx);
|
snapshot = view.snapshot(cx);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut context_menu = None;
|
||||||
|
let mut code_actions_indicator = None;
|
||||||
|
cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
|
||||||
let newest_selection_head = view
|
let newest_selection_head = view
|
||||||
.selections
|
.selections
|
||||||
.newest::<usize>(cx)
|
.newest::<usize>(cx)
|
||||||
|
@ -1549,7 +1555,7 @@ mod tests {
|
||||||
let layouts = editor.update(cx, |editor, cx| {
|
let layouts = editor.update(cx, |editor, cx| {
|
||||||
let snapshot = editor.snapshot(cx);
|
let snapshot = editor.snapshot(cx);
|
||||||
let mut presenter = cx.build_presenter(window_id, 30.);
|
let mut presenter = cx.build_presenter(window_id, 30.);
|
||||||
let mut layout_cx = presenter.build_layout_context(false, cx);
|
let mut layout_cx = presenter.build_layout_context(Vector2F::zero(), false, cx);
|
||||||
element.layout_line_numbers(0..6, &Default::default(), &snapshot, &mut layout_cx)
|
element.layout_line_numbers(0..6, &Default::default(), &snapshot, &mut layout_cx)
|
||||||
});
|
});
|
||||||
assert_eq!(layouts.len(), 6);
|
assert_eq!(layouts.len(), 6);
|
||||||
|
@ -1587,7 +1593,7 @@ mod tests {
|
||||||
|
|
||||||
let mut scene = Scene::new(1.0);
|
let mut scene = Scene::new(1.0);
|
||||||
let mut presenter = cx.build_presenter(window_id, 30.);
|
let mut presenter = cx.build_presenter(window_id, 30.);
|
||||||
let mut layout_cx = presenter.build_layout_context(false, cx);
|
let mut layout_cx = presenter.build_layout_context(Vector2F::zero(), false, cx);
|
||||||
let (size, mut state) = element.layout(
|
let (size, mut state) = element.layout(
|
||||||
SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
|
SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
|
||||||
&mut layout_cx,
|
&mut layout_cx,
|
||||||
|
|
|
@ -11,6 +11,7 @@ doctest = false
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
|
menu = { path = "../menu" }
|
||||||
picker = { path = "../picker" }
|
picker = { path = "../picker" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use fuzzy::PathMatch;
|
use fuzzy::PathMatch;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task,
|
actions, elements::*, AppContext, Entity, ModelHandle, MouseState, MutableAppContext,
|
||||||
View, ViewContext, ViewHandle,
|
RenderContext, Task, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::{Project, ProjectPath, WorktreeId};
|
use project::{Project, ProjectPath, WorktreeId};
|
||||||
|
@ -226,7 +226,7 @@ impl PickerDelegate for FileFinder {
|
||||||
fn render_match(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
mouse_state: &MouseState,
|
mouse_state: MouseState,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
|
@ -257,11 +257,9 @@ impl PickerDelegate for FileFinder {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use editor::{Editor, Input};
|
use editor::{Editor, Input};
|
||||||
|
use menu::{Confirm, SelectNext};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use workspace::{
|
use workspace::{AppState, Workspace};
|
||||||
menu::{Confirm, SelectNext},
|
|
||||||
AppState, Workspace,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[ctor::ctor]
|
#[ctor::ctor]
|
||||||
fn init_logger() {
|
fn init_logger() {
|
||||||
|
|
|
@ -8,9 +8,10 @@ path = "src/go_to_line.rs"
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
text = { path = "../text" }
|
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
|
menu = { path = "../menu" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
|
text = { path = "../text" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { path = "../workspace" }
|
||||||
postage = { version = "0.4", features = ["futures-traits"] }
|
postage = { version = "0.4", features = ["futures-traits"] }
|
||||||
|
|
|
@ -3,12 +3,10 @@ use gpui::{
|
||||||
actions, elements::*, geometry::vector::Vector2F, Axis, Entity, MutableAppContext,
|
actions, elements::*, geometry::vector::Vector2F, Axis, Entity, MutableAppContext,
|
||||||
RenderContext, View, ViewContext, ViewHandle,
|
RenderContext, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
|
use menu::{Cancel, Confirm};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use text::{Bias, Point};
|
use text::{Bias, Point};
|
||||||
use workspace::{
|
use workspace::Workspace;
|
||||||
menu::{Cancel, Confirm},
|
|
||||||
Workspace,
|
|
||||||
};
|
|
||||||
|
|
||||||
actions!(go_to_line, [Toggle]);
|
actions!(go_to_line, [Toggle]);
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,8 @@ use crate::{
|
||||||
platform::{self, Platform, PromptLevel, WindowOptions},
|
platform::{self, Platform, PromptLevel, WindowOptions},
|
||||||
presenter::Presenter,
|
presenter::Presenter,
|
||||||
util::post_inc,
|
util::post_inc,
|
||||||
AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache,
|
AssetCache, AssetSource, ClipboardItem, FontCache, MouseRegionId, PathPromptOptions,
|
||||||
|
TextLayoutCache,
|
||||||
};
|
};
|
||||||
pub use action::*;
|
pub use action::*;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
@ -126,26 +127,6 @@ pub trait UpdateView {
|
||||||
T: View;
|
T: View;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ElementStateContext: DerefMut<Target = MutableAppContext> {
|
|
||||||
fn current_view_id(&self) -> usize;
|
|
||||||
|
|
||||||
fn element_state<Tag: 'static, T: 'static + Default>(
|
|
||||||
&mut self,
|
|
||||||
element_id: usize,
|
|
||||||
) -> ElementStateHandle<T> {
|
|
||||||
let id = ElementStateId {
|
|
||||||
view_id: self.current_view_id(),
|
|
||||||
element_id,
|
|
||||||
tag: TypeId::of::<Tag>(),
|
|
||||||
};
|
|
||||||
self.cx
|
|
||||||
.element_states
|
|
||||||
.entry(id)
|
|
||||||
.or_insert_with(|| Box::new(T::default()));
|
|
||||||
ElementStateHandle::new(id, self.frame_count, &self.cx.ref_counts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Menu<'a> {
|
pub struct Menu<'a> {
|
||||||
pub name: &'a str,
|
pub name: &'a str,
|
||||||
pub items: Vec<MenuItem<'a>>,
|
pub items: Vec<MenuItem<'a>>,
|
||||||
|
@ -467,6 +448,27 @@ impl TestAppContext {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render<F, V, T>(&mut self, handle: &ViewHandle<V>, f: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut V, &mut RenderContext<V>) -> T,
|
||||||
|
V: View,
|
||||||
|
{
|
||||||
|
handle.update(&mut *self.cx.borrow_mut(), |view, cx| {
|
||||||
|
let mut render_cx = RenderContext {
|
||||||
|
app: cx,
|
||||||
|
window_id: handle.window_id(),
|
||||||
|
view_id: handle.id(),
|
||||||
|
view_type: PhantomData,
|
||||||
|
titlebar_height: 0.,
|
||||||
|
hovered_region_ids: Default::default(),
|
||||||
|
clicked_region_id: None,
|
||||||
|
right_clicked_region_id: None,
|
||||||
|
refreshing: false,
|
||||||
|
};
|
||||||
|
f(view, &mut render_cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_async(&self) -> AsyncAppContext {
|
pub fn to_async(&self) -> AsyncAppContext {
|
||||||
AsyncAppContext(self.cx.clone())
|
AsyncAppContext(self.cx.clone())
|
||||||
}
|
}
|
||||||
|
@ -768,6 +770,7 @@ type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
|
||||||
type FocusObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
|
type FocusObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
|
||||||
type GlobalObservationCallback = Box<dyn FnMut(&mut MutableAppContext)>;
|
type GlobalObservationCallback = Box<dyn FnMut(&mut MutableAppContext)>;
|
||||||
type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
|
type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
|
||||||
|
type ActionObservationCallback = Box<dyn FnMut(TypeId, &mut MutableAppContext)>;
|
||||||
type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
|
type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
|
||||||
|
|
||||||
pub struct MutableAppContext {
|
pub struct MutableAppContext {
|
||||||
|
@ -793,6 +796,7 @@ pub struct MutableAppContext {
|
||||||
global_observations:
|
global_observations:
|
||||||
Arc<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalObservationCallback>>>>>,
|
Arc<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalObservationCallback>>>>>,
|
||||||
release_observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>,
|
release_observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>,
|
||||||
|
action_dispatch_observations: Arc<Mutex<BTreeMap<usize, ActionObservationCallback>>>,
|
||||||
presenters_and_platform_windows:
|
presenters_and_platform_windows:
|
||||||
HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
|
HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
|
||||||
foreground: Rc<executor::Foreground>,
|
foreground: Rc<executor::Foreground>,
|
||||||
|
@ -845,6 +849,7 @@ impl MutableAppContext {
|
||||||
focus_observations: Default::default(),
|
focus_observations: Default::default(),
|
||||||
release_observations: Default::default(),
|
release_observations: Default::default(),
|
||||||
global_observations: Default::default(),
|
global_observations: Default::default(),
|
||||||
|
action_dispatch_observations: Default::default(),
|
||||||
presenters_and_platform_windows: HashMap::new(),
|
presenters_and_platform_windows: HashMap::new(),
|
||||||
foreground,
|
foreground,
|
||||||
pending_effects: VecDeque::new(),
|
pending_effects: VecDeque::new(),
|
||||||
|
@ -1051,19 +1056,15 @@ impl MutableAppContext {
|
||||||
.map_or(false, |window| window.is_active)
|
.map_or(false, |window| window.is_active)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_view(
|
pub fn render_view(&mut self, params: RenderParams) -> Result<ElementBox> {
|
||||||
&mut self,
|
let window_id = params.window_id;
|
||||||
window_id: usize,
|
let view_id = params.view_id;
|
||||||
view_id: usize,
|
|
||||||
titlebar_height: f32,
|
|
||||||
refreshing: bool,
|
|
||||||
) -> Result<ElementBox> {
|
|
||||||
let mut view = self
|
let mut view = self
|
||||||
.cx
|
.cx
|
||||||
.views
|
.views
|
||||||
.remove(&(window_id, view_id))
|
.remove(&(params.window_id, params.view_id))
|
||||||
.ok_or(anyhow!("view not found"))?;
|
.ok_or(anyhow!("view not found"))?;
|
||||||
let element = view.render(window_id, view_id, titlebar_height, refreshing, self);
|
let element = view.render(params, self);
|
||||||
self.cx.views.insert((window_id, view_id), view);
|
self.cx.views.insert((window_id, view_id), view);
|
||||||
Ok(element)
|
Ok(element)
|
||||||
}
|
}
|
||||||
|
@ -1090,7 +1091,15 @@ impl MutableAppContext {
|
||||||
.map(|view_id| {
|
.map(|view_id| {
|
||||||
(
|
(
|
||||||
view_id,
|
view_id,
|
||||||
self.render_view(window_id, view_id, titlebar_height, false)
|
self.render_view(RenderParams {
|
||||||
|
window_id,
|
||||||
|
view_id,
|
||||||
|
titlebar_height,
|
||||||
|
hovered_region_ids: Default::default(),
|
||||||
|
clicked_region_id: None,
|
||||||
|
right_clicked_region_id: None,
|
||||||
|
refreshing: false,
|
||||||
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -1322,6 +1331,20 @@ impl MutableAppContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn observe_actions<F>(&mut self, callback: F) -> Subscription
|
||||||
|
where
|
||||||
|
F: 'static + FnMut(TypeId, &mut MutableAppContext),
|
||||||
|
{
|
||||||
|
let id = post_inc(&mut self.next_subscription_id);
|
||||||
|
self.action_dispatch_observations
|
||||||
|
.lock()
|
||||||
|
.insert(id, Box::new(callback));
|
||||||
|
Subscription::ActionObservation {
|
||||||
|
id,
|
||||||
|
observations: Some(Arc::downgrade(&self.action_dispatch_observations)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) {
|
pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) {
|
||||||
self.pending_effects.push_back(Effect::Deferred {
|
self.pending_effects.push_back(Effect::Deferred {
|
||||||
callback: Box::new(callback),
|
callback: Box::new(callback),
|
||||||
|
@ -1374,7 +1397,10 @@ impl MutableAppContext {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.0
|
.0
|
||||||
.clone();
|
.clone();
|
||||||
let dispatch_path = presenter.borrow().dispatch_path_from(view_id);
|
let mut dispatch_path = Vec::new();
|
||||||
|
presenter
|
||||||
|
.borrow()
|
||||||
|
.compute_dispatch_path_from(view_id, &mut dispatch_path);
|
||||||
for view_id in dispatch_path {
|
for view_id in dispatch_path {
|
||||||
if let Some(view) = self.views.get(&(window_id, view_id)) {
|
if let Some(view) = self.views.get(&(window_id, view_id)) {
|
||||||
let view_type = view.as_any().type_id();
|
let view_type = view.as_any().type_id();
|
||||||
|
@ -1421,6 +1447,29 @@ impl MutableAppContext {
|
||||||
self.global_actions.contains_key(&action_type)
|
self.global_actions.contains_key(&action_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return keystrokes that would dispatch the given action closest to the focused view, if there are any.
|
||||||
|
pub(crate) fn keystrokes_for_action(
|
||||||
|
&self,
|
||||||
|
window_id: usize,
|
||||||
|
dispatch_path: &[usize],
|
||||||
|
action: &dyn Action,
|
||||||
|
) -> Option<SmallVec<[Keystroke; 2]>> {
|
||||||
|
for view_id in dispatch_path.iter().rev() {
|
||||||
|
let view = self
|
||||||
|
.cx
|
||||||
|
.views
|
||||||
|
.get(&(window_id, *view_id))
|
||||||
|
.expect("view in responder chain does not exist");
|
||||||
|
let cx = view.keymap_context(self.as_ref());
|
||||||
|
let keystrokes = self.keystroke_matcher.keystrokes_for_action(action, &cx);
|
||||||
|
if keystrokes.is_some() {
|
||||||
|
return keystrokes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
pub fn dispatch_action_at(&mut self, window_id: usize, view_id: usize, action: &dyn Action) {
|
pub fn dispatch_action_at(&mut self, window_id: usize, view_id: usize, action: &dyn Action) {
|
||||||
let presenter = self
|
let presenter = self
|
||||||
.presenters_and_platform_windows
|
.presenters_and_platform_windows
|
||||||
|
@ -1428,7 +1477,10 @@ impl MutableAppContext {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.0
|
.0
|
||||||
.clone();
|
.clone();
|
||||||
let dispatch_path = presenter.borrow().dispatch_path_from(view_id);
|
let mut dispatch_path = Vec::new();
|
||||||
|
presenter
|
||||||
|
.borrow()
|
||||||
|
.compute_dispatch_path_from(view_id, &mut dispatch_path);
|
||||||
self.dispatch_action_any(window_id, &dispatch_path, action);
|
self.dispatch_action_any(window_id, &dispatch_path, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1486,6 +1538,11 @@ impl MutableAppContext {
|
||||||
if !this.halt_action_dispatch {
|
if !this.halt_action_dispatch {
|
||||||
this.halt_action_dispatch = this.dispatch_global_action_any(action);
|
this.halt_action_dispatch = this.dispatch_global_action_any(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.pending_effects
|
||||||
|
.push_back(Effect::ActionDispatchNotification {
|
||||||
|
action_id: action.id(),
|
||||||
|
});
|
||||||
this.halt_action_dispatch
|
this.halt_action_dispatch
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1760,23 +1817,6 @@ impl MutableAppContext {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_render_context<V: View>(
|
|
||||||
&mut self,
|
|
||||||
window_id: usize,
|
|
||||||
view_id: usize,
|
|
||||||
titlebar_height: f32,
|
|
||||||
refreshing: bool,
|
|
||||||
) -> RenderContext<V> {
|
|
||||||
RenderContext {
|
|
||||||
app: self,
|
|
||||||
titlebar_height,
|
|
||||||
refreshing,
|
|
||||||
window_id,
|
|
||||||
view_id,
|
|
||||||
view_type: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_view<T, F>(&mut self, window_id: usize, build_view: F) -> ViewHandle<T>
|
pub fn add_view<T, F>(&mut self, window_id: usize, build_view: F) -> ViewHandle<T>
|
||||||
where
|
where
|
||||||
T: View,
|
T: View,
|
||||||
|
@ -1951,6 +1991,9 @@ impl MutableAppContext {
|
||||||
Effect::RefreshWindows => {
|
Effect::RefreshWindows => {
|
||||||
refreshing = true;
|
refreshing = true;
|
||||||
}
|
}
|
||||||
|
Effect::ActionDispatchNotification { action_id } => {
|
||||||
|
self.handle_action_dispatch_notification_effect(action_id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.pending_notifications.clear();
|
self.pending_notifications.clear();
|
||||||
self.remove_dropped_entities();
|
self.remove_dropped_entities();
|
||||||
|
@ -2226,6 +2269,13 @@ impl MutableAppContext {
|
||||||
observed_window_id: usize,
|
observed_window_id: usize,
|
||||||
observed_view_id: usize,
|
observed_view_id: usize,
|
||||||
) {
|
) {
|
||||||
|
let callbacks = self.observations.lock().remove(&observed_view_id);
|
||||||
|
|
||||||
|
if self
|
||||||
|
.cx
|
||||||
|
.views
|
||||||
|
.contains_key(&(observed_window_id, observed_view_id))
|
||||||
|
{
|
||||||
if let Some(window) = self.cx.windows.get_mut(&observed_window_id) {
|
if let Some(window) = self.cx.windows.get_mut(&observed_window_id) {
|
||||||
window
|
window
|
||||||
.invalidation
|
.invalidation
|
||||||
|
@ -2234,13 +2284,7 @@ impl MutableAppContext {
|
||||||
.insert(observed_view_id);
|
.insert(observed_view_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
let callbacks = self.observations.lock().remove(&observed_view_id);
|
|
||||||
if let Some(callbacks) = callbacks {
|
if let Some(callbacks) = callbacks {
|
||||||
if self
|
|
||||||
.cx
|
|
||||||
.views
|
|
||||||
.contains_key(&(observed_window_id, observed_view_id))
|
|
||||||
{
|
|
||||||
for (id, callback) in callbacks {
|
for (id, callback) in callbacks {
|
||||||
if let Some(mut callback) = callback {
|
if let Some(mut callback) = callback {
|
||||||
let alive = callback(self);
|
let alive = callback(self);
|
||||||
|
@ -2389,7 +2433,15 @@ impl MutableAppContext {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focus(&mut self, window_id: usize, view_id: Option<usize>) {
|
fn handle_action_dispatch_notification_effect(&mut self, action_id: TypeId) {
|
||||||
|
let mut callbacks = mem::take(&mut *self.action_dispatch_observations.lock());
|
||||||
|
for (_, callback) in &mut callbacks {
|
||||||
|
callback(action_id, self);
|
||||||
|
}
|
||||||
|
self.action_dispatch_observations.lock().extend(callbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus(&mut self, window_id: usize, view_id: Option<usize>) {
|
||||||
if let Some(pending_focus_index) = self.pending_focus_index {
|
if let Some(pending_focus_index) = self.pending_focus_index {
|
||||||
self.pending_effects.remove(pending_focus_index);
|
self.pending_effects.remove(pending_focus_index);
|
||||||
}
|
}
|
||||||
|
@ -2763,6 +2815,9 @@ pub enum Effect {
|
||||||
is_active: bool,
|
is_active: bool,
|
||||||
},
|
},
|
||||||
RefreshWindows,
|
RefreshWindows,
|
||||||
|
ActionDispatchNotification {
|
||||||
|
action_id: TypeId,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Effect {
|
impl Debug for Effect {
|
||||||
|
@ -2839,6 +2894,10 @@ impl Debug for Effect {
|
||||||
.field("view_id", view_id)
|
.field("view_id", view_id)
|
||||||
.field("subscription_id", subscription_id)
|
.field("subscription_id", subscription_id)
|
||||||
.finish(),
|
.finish(),
|
||||||
|
Effect::ActionDispatchNotification { action_id, .. } => f
|
||||||
|
.debug_struct("Effect::ActionDispatchNotification")
|
||||||
|
.field("action_id", action_id)
|
||||||
|
.finish(),
|
||||||
Effect::ResizeWindow { window_id } => f
|
Effect::ResizeWindow { window_id } => f
|
||||||
.debug_struct("Effect::RefreshWindow")
|
.debug_struct("Effect::RefreshWindow")
|
||||||
.field("window_id", window_id)
|
.field("window_id", window_id)
|
||||||
|
@ -2899,14 +2958,7 @@ pub trait AnyView {
|
||||||
cx: &mut MutableAppContext,
|
cx: &mut MutableAppContext,
|
||||||
) -> Option<Pin<Box<dyn 'static + Future<Output = ()>>>>;
|
) -> Option<Pin<Box<dyn 'static + Future<Output = ()>>>>;
|
||||||
fn ui_name(&self) -> &'static str;
|
fn ui_name(&self) -> &'static str;
|
||||||
fn render<'a>(
|
fn render<'a>(&mut self, params: RenderParams, cx: &mut MutableAppContext) -> ElementBox;
|
||||||
&mut self,
|
|
||||||
window_id: usize,
|
|
||||||
view_id: usize,
|
|
||||||
titlebar_height: f32,
|
|
||||||
refreshing: bool,
|
|
||||||
cx: &mut MutableAppContext,
|
|
||||||
) -> ElementBox;
|
|
||||||
fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize);
|
fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize);
|
||||||
fn on_blur(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize);
|
fn on_blur(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize);
|
||||||
fn keymap_context(&self, cx: &AppContext) -> keymap::Context;
|
fn keymap_context(&self, cx: &AppContext) -> keymap::Context;
|
||||||
|
@ -2940,25 +2992,8 @@ where
|
||||||
T::ui_name()
|
T::ui_name()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render<'a>(
|
fn render<'a>(&mut self, params: RenderParams, cx: &mut MutableAppContext) -> ElementBox {
|
||||||
&mut self,
|
View::render(self, &mut RenderContext::new(params, cx))
|
||||||
window_id: usize,
|
|
||||||
view_id: usize,
|
|
||||||
titlebar_height: f32,
|
|
||||||
refreshing: bool,
|
|
||||||
cx: &mut MutableAppContext,
|
|
||||||
) -> ElementBox {
|
|
||||||
View::render(
|
|
||||||
self,
|
|
||||||
&mut RenderContext {
|
|
||||||
window_id,
|
|
||||||
view_id,
|
|
||||||
app: cx,
|
|
||||||
view_type: PhantomData::<T>,
|
|
||||||
titlebar_height,
|
|
||||||
refreshing,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize) {
|
fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize) {
|
||||||
|
@ -3266,6 +3301,10 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||||
self.app.focus(self.window_id, Some(self.view_id));
|
self.app.focus(self.window_id, Some(self.view_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_self_focused(&self) -> bool {
|
||||||
|
self.app.focused_view_id(self.window_id) == Some(self.view_id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn blur(&mut self) {
|
pub fn blur(&mut self) {
|
||||||
self.app.focus(self.window_id, None);
|
self.app.focus(self.window_id, None);
|
||||||
}
|
}
|
||||||
|
@ -3390,6 +3429,20 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn observe_actions<F>(&mut self, mut callback: F) -> Subscription
|
||||||
|
where
|
||||||
|
F: 'static + FnMut(&mut T, TypeId, &mut ViewContext<T>),
|
||||||
|
{
|
||||||
|
let observer = self.weak_handle();
|
||||||
|
self.app.observe_actions(move |action_id, cx| {
|
||||||
|
if let Some(observer) = observer.upgrade(cx) {
|
||||||
|
observer.update(cx, |observer, cx| {
|
||||||
|
callback(observer, action_id, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn emit(&mut self, payload: T::Event) {
|
pub fn emit(&mut self, payload: T::Event) {
|
||||||
self.app.pending_effects.push_back(Effect::Event {
|
self.app.pending_effects.push_back(Effect::Event {
|
||||||
entity_id: self.view_id,
|
entity_id: self.view_id,
|
||||||
|
@ -3447,23 +3500,85 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct RenderParams {
|
||||||
|
pub window_id: usize,
|
||||||
|
pub view_id: usize,
|
||||||
|
pub titlebar_height: f32,
|
||||||
|
pub hovered_region_ids: HashSet<MouseRegionId>,
|
||||||
|
pub clicked_region_id: Option<MouseRegionId>,
|
||||||
|
pub right_clicked_region_id: Option<MouseRegionId>,
|
||||||
|
pub refreshing: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct RenderContext<'a, T: View> {
|
pub struct RenderContext<'a, T: View> {
|
||||||
|
pub(crate) window_id: usize,
|
||||||
|
pub(crate) view_id: usize,
|
||||||
|
pub(crate) view_type: PhantomData<T>,
|
||||||
|
pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
|
||||||
|
pub(crate) clicked_region_id: Option<MouseRegionId>,
|
||||||
|
pub(crate) right_clicked_region_id: Option<MouseRegionId>,
|
||||||
pub app: &'a mut MutableAppContext,
|
pub app: &'a mut MutableAppContext,
|
||||||
pub titlebar_height: f32,
|
pub titlebar_height: f32,
|
||||||
pub refreshing: bool,
|
pub refreshing: bool,
|
||||||
window_id: usize,
|
|
||||||
view_id: usize,
|
|
||||||
view_type: PhantomData<T>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T: View> RenderContext<'a, T> {
|
#[derive(Clone, Copy, Default)]
|
||||||
pub fn handle(&self) -> WeakViewHandle<T> {
|
pub struct MouseState {
|
||||||
|
pub hovered: bool,
|
||||||
|
pub clicked: bool,
|
||||||
|
pub right_clicked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, V: View> RenderContext<'a, V> {
|
||||||
|
fn new(params: RenderParams, app: &'a mut MutableAppContext) -> Self {
|
||||||
|
Self {
|
||||||
|
app,
|
||||||
|
window_id: params.window_id,
|
||||||
|
view_id: params.view_id,
|
||||||
|
view_type: PhantomData,
|
||||||
|
titlebar_height: params.titlebar_height,
|
||||||
|
hovered_region_ids: params.hovered_region_ids.clone(),
|
||||||
|
clicked_region_id: params.clicked_region_id,
|
||||||
|
right_clicked_region_id: params.right_clicked_region_id,
|
||||||
|
refreshing: params.refreshing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle(&self) -> WeakViewHandle<V> {
|
||||||
WeakViewHandle::new(self.window_id, self.view_id)
|
WeakViewHandle::new(self.window_id, self.view_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view_id(&self) -> usize {
|
pub fn view_id(&self) -> usize {
|
||||||
self.view_id
|
self.view_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mouse_state<Tag: 'static>(&self, region_id: usize) -> MouseState {
|
||||||
|
let region_id = MouseRegionId {
|
||||||
|
view_id: self.view_id,
|
||||||
|
discriminant: (TypeId::of::<Tag>(), region_id),
|
||||||
|
};
|
||||||
|
MouseState {
|
||||||
|
hovered: self.hovered_region_ids.contains(®ion_id),
|
||||||
|
clicked: self.clicked_region_id == Some(region_id),
|
||||||
|
right_clicked: self.right_clicked_region_id == Some(region_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn element_state<Tag: 'static, T: 'static + Default>(
|
||||||
|
&mut self,
|
||||||
|
element_id: usize,
|
||||||
|
) -> ElementStateHandle<T> {
|
||||||
|
let id = ElementStateId {
|
||||||
|
view_id: self.view_id(),
|
||||||
|
element_id,
|
||||||
|
tag: TypeId::of::<Tag>(),
|
||||||
|
};
|
||||||
|
self.cx
|
||||||
|
.element_states
|
||||||
|
.entry(id)
|
||||||
|
.or_insert_with(|| Box::new(T::default()));
|
||||||
|
ElementStateHandle::new(id, self.frame_count, &self.cx.ref_counts)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<AppContext> for &AppContext {
|
impl AsRef<AppContext> for &AppContext {
|
||||||
|
@ -3508,12 +3623,6 @@ impl<V: View> ReadView for RenderContext<'_, V> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> ElementStateContext for RenderContext<'_, V> {
|
|
||||||
fn current_view_id(&self) -> usize {
|
|
||||||
self.view_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M> AsRef<AppContext> for ViewContext<'_, M> {
|
impl<M> AsRef<AppContext> for ViewContext<'_, M> {
|
||||||
fn as_ref(&self) -> &AppContext {
|
fn as_ref(&self) -> &AppContext {
|
||||||
&self.app.cx
|
&self.app.cx
|
||||||
|
@ -3573,6 +3682,16 @@ impl<V> UpgradeViewHandle for ViewContext<'_, V> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<V: View> UpgradeViewHandle for RenderContext<'_, V> {
|
||||||
|
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
|
||||||
|
self.cx.upgrade_view_handle(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
|
||||||
|
self.cx.upgrade_any_view_handle(handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<V: View> UpdateModel for ViewContext<'_, V> {
|
impl<V: View> UpdateModel for ViewContext<'_, V> {
|
||||||
fn update_model<T: Entity, O>(
|
fn update_model<T: Entity, O>(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -3602,12 +3721,6 @@ impl<V: View> UpdateView for ViewContext<'_, V> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> ElementStateContext for ViewContext<'_, V> {
|
|
||||||
fn current_view_id(&self) -> usize {
|
|
||||||
self.view_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Handle<T> {
|
pub trait Handle<T> {
|
||||||
type Weak: 'static;
|
type Weak: 'static;
|
||||||
fn id(&self) -> usize;
|
fn id(&self) -> usize;
|
||||||
|
@ -4636,6 +4749,10 @@ pub enum Subscription {
|
||||||
observations:
|
observations:
|
||||||
Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>>,
|
Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>>,
|
||||||
},
|
},
|
||||||
|
ActionObservation {
|
||||||
|
id: usize,
|
||||||
|
observations: Option<Weak<Mutex<BTreeMap<usize, ActionObservationCallback>>>>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Subscription {
|
impl Subscription {
|
||||||
|
@ -4659,6 +4776,9 @@ impl Subscription {
|
||||||
Subscription::FocusObservation { observations, .. } => {
|
Subscription::FocusObservation { observations, .. } => {
|
||||||
observations.take();
|
observations.take();
|
||||||
}
|
}
|
||||||
|
Subscription::ActionObservation { observations, .. } => {
|
||||||
|
observations.take();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4767,6 +4887,11 @@ impl Drop for Subscription {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Subscription::ActionObservation { id, observations } => {
|
||||||
|
if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
|
||||||
|
observations.lock().remove(&id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6200,7 +6325,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Default, Deserialize)]
|
||||||
pub struct Action(pub String);
|
pub struct Action(pub String);
|
||||||
|
|
||||||
impl_actions!(test, [Action]);
|
impl_actions!(test, [Action]);
|
||||||
|
@ -6265,6 +6390,13 @@ mod tests {
|
||||||
let view_3 = cx.add_view(window_id, |_| ViewA { id: 3 });
|
let view_3 = cx.add_view(window_id, |_| ViewA { id: 3 });
|
||||||
let view_4 = cx.add_view(window_id, |_| ViewB { id: 4 });
|
let view_4 = cx.add_view(window_id, |_| ViewB { id: 4 });
|
||||||
|
|
||||||
|
let observed_actions = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
cx.observe_actions({
|
||||||
|
let observed_actions = observed_actions.clone();
|
||||||
|
move |action_id, _| observed_actions.borrow_mut().push(action_id)
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
cx.dispatch_action(
|
cx.dispatch_action(
|
||||||
window_id,
|
window_id,
|
||||||
vec![view_1.id(), view_2.id(), view_3.id(), view_4.id()],
|
vec![view_1.id(), view_2.id(), view_3.id(), view_4.id()],
|
||||||
|
@ -6285,6 +6417,7 @@ mod tests {
|
||||||
"1 b"
|
"1 b"
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
assert_eq!(*observed_actions.borrow(), [Action::default().id()]);
|
||||||
|
|
||||||
// Remove view_1, which doesn't propagate the action
|
// Remove view_1, which doesn't propagate the action
|
||||||
actions.borrow_mut().clear();
|
actions.borrow_mut().clear();
|
||||||
|
@ -6307,6 +6440,10 @@ mod tests {
|
||||||
"global"
|
"global"
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
*observed_actions.borrow(),
|
||||||
|
[Action::default().id(), Action::default().id()]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[crate::test(self)]
|
#[crate::test(self)]
|
||||||
|
|
|
@ -8,6 +8,7 @@ mod expanded;
|
||||||
mod flex;
|
mod flex;
|
||||||
mod hook;
|
mod hook;
|
||||||
mod image;
|
mod image;
|
||||||
|
mod keystroke_label;
|
||||||
mod label;
|
mod label;
|
||||||
mod list;
|
mod list;
|
||||||
mod mouse_event_handler;
|
mod mouse_event_handler;
|
||||||
|
@ -20,8 +21,8 @@ mod uniform_list;
|
||||||
use self::expanded::Expanded;
|
use self::expanded::Expanded;
|
||||||
pub use self::{
|
pub use self::{
|
||||||
align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*,
|
align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*,
|
||||||
hook::*, image::*, label::*, list::*, mouse_event_handler::*, overlay::*, stack::*, svg::*,
|
hook::*, image::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*,
|
||||||
text::*, uniform_list::*,
|
stack::*, svg::*, text::*, uniform_list::*,
|
||||||
};
|
};
|
||||||
pub use crate::presenter::ChildView;
|
pub use crate::presenter::ChildView;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
|
@ -9,46 +9,121 @@ use crate::{
|
||||||
|
|
||||||
pub struct ConstrainedBox {
|
pub struct ConstrainedBox {
|
||||||
child: ElementBox,
|
child: ElementBox,
|
||||||
constraint: SizeConstraint,
|
constraint: Constraint,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Constraint {
|
||||||
|
Static(SizeConstraint),
|
||||||
|
Dynamic(Box<dyn FnMut(SizeConstraint, &mut LayoutContext) -> SizeConstraint>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToJson for Constraint {
|
||||||
|
fn to_json(&self) -> serde_json::Value {
|
||||||
|
match self {
|
||||||
|
Constraint::Static(constraint) => constraint.to_json(),
|
||||||
|
Constraint::Dynamic(_) => "dynamic".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConstrainedBox {
|
impl ConstrainedBox {
|
||||||
pub fn new(child: ElementBox) -> Self {
|
pub fn new(child: ElementBox) -> Self {
|
||||||
Self {
|
Self {
|
||||||
child,
|
child,
|
||||||
constraint: SizeConstraint {
|
constraint: Constraint::Static(Default::default()),
|
||||||
min: Vector2F::zero(),
|
|
||||||
max: Vector2F::splat(f32::INFINITY),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dynamically(
|
||||||
|
mut self,
|
||||||
|
constraint: impl 'static + FnMut(SizeConstraint, &mut LayoutContext) -> SizeConstraint,
|
||||||
|
) -> Self {
|
||||||
|
self.constraint = Constraint::Dynamic(Box::new(constraint));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_min_width(mut self, min_width: f32) -> Self {
|
pub fn with_min_width(mut self, min_width: f32) -> Self {
|
||||||
self.constraint.min.set_x(min_width);
|
if let Constraint::Dynamic(_) = self.constraint {
|
||||||
|
self.constraint = Constraint::Static(Default::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Constraint::Static(constraint) = &mut self.constraint {
|
||||||
|
constraint.min.set_x(min_width);
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_max_width(mut self, max_width: f32) -> Self {
|
pub fn with_max_width(mut self, max_width: f32) -> Self {
|
||||||
self.constraint.max.set_x(max_width);
|
if let Constraint::Dynamic(_) = self.constraint {
|
||||||
|
self.constraint = Constraint::Static(Default::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Constraint::Static(constraint) = &mut self.constraint {
|
||||||
|
constraint.max.set_x(max_width);
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_max_height(mut self, max_height: f32) -> Self {
|
pub fn with_max_height(mut self, max_height: f32) -> Self {
|
||||||
self.constraint.max.set_y(max_height);
|
if let Constraint::Dynamic(_) = self.constraint {
|
||||||
|
self.constraint = Constraint::Static(Default::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Constraint::Static(constraint) = &mut self.constraint {
|
||||||
|
constraint.max.set_y(max_height);
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_width(mut self, width: f32) -> Self {
|
pub fn with_width(mut self, width: f32) -> Self {
|
||||||
self.constraint.min.set_x(width);
|
if let Constraint::Dynamic(_) = self.constraint {
|
||||||
self.constraint.max.set_x(width);
|
self.constraint = Constraint::Static(Default::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Constraint::Static(constraint) = &mut self.constraint {
|
||||||
|
constraint.min.set_x(width);
|
||||||
|
constraint.max.set_x(width);
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_height(mut self, height: f32) -> Self {
|
pub fn with_height(mut self, height: f32) -> Self {
|
||||||
self.constraint.min.set_y(height);
|
if let Constraint::Dynamic(_) = self.constraint {
|
||||||
self.constraint.max.set_y(height);
|
self.constraint = Constraint::Static(Default::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Constraint::Static(constraint) = &mut self.constraint {
|
||||||
|
constraint.min.set_y(height);
|
||||||
|
constraint.max.set_y(height);
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn constraint(
|
||||||
|
&mut self,
|
||||||
|
input_constraint: SizeConstraint,
|
||||||
|
cx: &mut LayoutContext,
|
||||||
|
) -> SizeConstraint {
|
||||||
|
match &mut self.constraint {
|
||||||
|
Constraint::Static(constraint) => *constraint,
|
||||||
|
Constraint::Dynamic(compute_constraint) => compute_constraint(input_constraint, cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element for ConstrainedBox {
|
impl Element for ConstrainedBox {
|
||||||
|
@ -57,13 +132,14 @@ impl Element for ConstrainedBox {
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut constraint: SizeConstraint,
|
mut parent_constraint: SizeConstraint,
|
||||||
cx: &mut LayoutContext,
|
cx: &mut LayoutContext,
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
) -> (Vector2F, Self::LayoutState) {
|
||||||
constraint.min = constraint.min.max(self.constraint.min);
|
let constraint = self.constraint(parent_constraint, cx);
|
||||||
constraint.max = constraint.max.min(self.constraint.max);
|
parent_constraint.min = parent_constraint.min.max(constraint.min);
|
||||||
constraint.max = constraint.max.max(constraint.min);
|
parent_constraint.max = parent_constraint.max.min(constraint.max);
|
||||||
let size = self.child.layout(constraint, cx);
|
parent_constraint.max = parent_constraint.max.max(parent_constraint.min);
|
||||||
|
let size = self.child.layout(parent_constraint, cx);
|
||||||
(size, ())
|
(size, ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +172,6 @@ impl Element for ConstrainedBox {
|
||||||
_: &Self::PaintState,
|
_: &Self::PaintState,
|
||||||
cx: &DebugContext,
|
cx: &DebugContext,
|
||||||
) -> json::Value {
|
) -> json::Value {
|
||||||
json!({"type": "ConstrainedBox", "set_constraint": self.constraint.to_json(), "child": self.child.debug(cx)})
|
json!({"type": "ConstrainedBox", "assigned_constraint": self.constraint.to_json(), "child": self.child.debug(cx)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
json::ToJson,
|
json::ToJson,
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
scene::{self, Border, Quad},
|
scene::{self, Border, CursorRegion, Quad},
|
||||||
Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
|
Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -213,7 +213,10 @@ impl Element for Container {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(style) = self.style.cursor {
|
if let Some(style) = self.style.cursor {
|
||||||
cx.scene.push_cursor_style(quad_bounds, style);
|
cx.scene.push_cursor_region(CursorRegion {
|
||||||
|
bounds: quad_bounds,
|
||||||
|
style,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let child_origin =
|
let child_origin =
|
||||||
|
|
|
@ -2,8 +2,8 @@ use std::{any::Any, f32::INFINITY};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
json::{self, ToJson, Value},
|
json::{self, ToJson, Value},
|
||||||
Axis, DebugContext, Element, ElementBox, ElementStateContext, ElementStateHandle, Event,
|
Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext,
|
||||||
EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
|
LayoutContext, PaintContext, RenderContext, SizeConstraint, Vector2FExt, View,
|
||||||
};
|
};
|
||||||
use pathfinder_geometry::{
|
use pathfinder_geometry::{
|
||||||
rect::RectF,
|
rect::RectF,
|
||||||
|
@ -40,15 +40,15 @@ impl Flex {
|
||||||
Self::new(Axis::Vertical)
|
Self::new(Axis::Vertical)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scrollable<Tag, C>(
|
pub fn scrollable<Tag, V>(
|
||||||
mut self,
|
mut self,
|
||||||
element_id: usize,
|
element_id: usize,
|
||||||
scroll_to: Option<usize>,
|
scroll_to: Option<usize>,
|
||||||
cx: &mut C,
|
cx: &mut RenderContext<V>,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
Tag: 'static,
|
Tag: 'static,
|
||||||
C: ElementStateContext,
|
V: View,
|
||||||
{
|
{
|
||||||
let scroll_state = cx.element_state::<Tag, ScrollState>(element_id);
|
let scroll_state = cx.element_state::<Tag, ScrollState>(element_id);
|
||||||
scroll_state.update(cx, |scroll_state, _| scroll_state.scroll_to = scroll_to);
|
scroll_state.update(cx, |scroll_state, _| scroll_state.scroll_to = scroll_to);
|
||||||
|
|
95
crates/gpui/src/elements/keystroke_label.rs
Normal file
95
crates/gpui/src/elements/keystroke_label.rs
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
use crate::{
|
||||||
|
elements::*,
|
||||||
|
fonts::TextStyle,
|
||||||
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
|
Action, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use super::ContainerStyle;
|
||||||
|
|
||||||
|
pub struct KeystrokeLabel {
|
||||||
|
action: Box<dyn Action>,
|
||||||
|
container_style: ContainerStyle,
|
||||||
|
text_style: TextStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeystrokeLabel {
|
||||||
|
pub fn new(
|
||||||
|
action: Box<dyn Action>,
|
||||||
|
container_style: ContainerStyle,
|
||||||
|
text_style: TextStyle,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
action,
|
||||||
|
container_style,
|
||||||
|
text_style,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for KeystrokeLabel {
|
||||||
|
type LayoutState = ElementBox;
|
||||||
|
type PaintState = ();
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
constraint: SizeConstraint,
|
||||||
|
cx: &mut LayoutContext,
|
||||||
|
) -> (Vector2F, ElementBox) {
|
||||||
|
let mut element = if let Some(keystrokes) = cx.keystrokes_for_action(self.action.as_ref()) {
|
||||||
|
Flex::row()
|
||||||
|
.with_children(keystrokes.iter().map(|keystroke| {
|
||||||
|
Label::new(
|
||||||
|
keystroke.to_string().to_uppercase(),
|
||||||
|
self.text_style.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(self.container_style)
|
||||||
|
.boxed()
|
||||||
|
}))
|
||||||
|
.boxed()
|
||||||
|
} else {
|
||||||
|
Empty::new().collapsed().boxed()
|
||||||
|
};
|
||||||
|
|
||||||
|
let size = element.layout(constraint, cx);
|
||||||
|
(size, element)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
bounds: RectF,
|
||||||
|
visible_bounds: RectF,
|
||||||
|
element: &mut ElementBox,
|
||||||
|
cx: &mut PaintContext,
|
||||||
|
) {
|
||||||
|
element.paint(bounds.origin(), visible_bounds, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch_event(
|
||||||
|
&mut self,
|
||||||
|
event: &Event,
|
||||||
|
_: RectF,
|
||||||
|
_: RectF,
|
||||||
|
element: &mut ElementBox,
|
||||||
|
_: &mut (),
|
||||||
|
cx: &mut EventContext,
|
||||||
|
) -> bool {
|
||||||
|
element.dispatch_event(event, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug(
|
||||||
|
&self,
|
||||||
|
_: RectF,
|
||||||
|
element: &ElementBox,
|
||||||
|
_: &(),
|
||||||
|
cx: &crate::DebugContext,
|
||||||
|
) -> serde_json::Value {
|
||||||
|
json!({
|
||||||
|
"type": "KeystrokeLabel",
|
||||||
|
"action": self.action.name(),
|
||||||
|
"child": element.debug(cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
json::json,
|
json::json,
|
||||||
DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
|
DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
|
||||||
SizeConstraint,
|
RenderContext, SizeConstraint, View, ViewContext,
|
||||||
};
|
};
|
||||||
use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
|
use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
|
||||||
use sum_tree::{Bias, SumTree};
|
use sum_tree::{Bias, SumTree};
|
||||||
|
@ -26,7 +26,7 @@ pub enum Orientation {
|
||||||
|
|
||||||
struct StateInner {
|
struct StateInner {
|
||||||
last_layout_width: Option<f32>,
|
last_layout_width: Option<f32>,
|
||||||
render_item: Box<dyn FnMut(usize, &mut LayoutContext) -> ElementBox>,
|
render_item: Box<dyn FnMut(usize, &mut LayoutContext) -> Option<ElementBox>>,
|
||||||
rendered_range: Range<usize>,
|
rendered_range: Range<usize>,
|
||||||
items: SumTree<ListItem>,
|
items: SumTree<ListItem>,
|
||||||
logical_scroll_top: Option<ListOffset>,
|
logical_scroll_top: Option<ListOffset>,
|
||||||
|
@ -131,14 +131,28 @@ impl Element for List {
|
||||||
let mut cursor = old_items.cursor::<Count>();
|
let mut cursor = old_items.cursor::<Count>();
|
||||||
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
|
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
|
||||||
for (ix, item) in cursor.by_ref().enumerate() {
|
for (ix, item) in cursor.by_ref().enumerate() {
|
||||||
if rendered_height - scroll_top.offset_in_item >= size.y() + state.overdraw {
|
let visible_height = rendered_height - scroll_top.offset_in_item;
|
||||||
|
if visible_height >= size.y() + state.overdraw {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let element = state.render_item(scroll_top.item_ix + ix, item, item_constraint, cx);
|
// Force re-render if the item is visible, but attempt to re-use an existing one
|
||||||
|
// if we are inside the overdraw.
|
||||||
|
let existing_element = if visible_height >= size.y() {
|
||||||
|
Some(item)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(element) = state.render_item(
|
||||||
|
scroll_top.item_ix + ix,
|
||||||
|
existing_element,
|
||||||
|
item_constraint,
|
||||||
|
cx,
|
||||||
|
) {
|
||||||
rendered_height += element.size().y();
|
rendered_height += element.size().y();
|
||||||
rendered_items.push_back(ListItem::Rendered(element));
|
rendered_items.push_back(ListItem::Rendered(element));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare to start walking upward from the item at the scroll top.
|
// Prepare to start walking upward from the item at the scroll top.
|
||||||
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
|
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
|
||||||
|
@ -148,10 +162,13 @@ impl Element for List {
|
||||||
if rendered_height - scroll_top.offset_in_item < size.y() {
|
if rendered_height - scroll_top.offset_in_item < size.y() {
|
||||||
while rendered_height < size.y() {
|
while rendered_height < size.y() {
|
||||||
cursor.prev(&());
|
cursor.prev(&());
|
||||||
if let Some(item) = cursor.item() {
|
if cursor.item().is_some() {
|
||||||
let element = state.render_item(cursor.start().0, item, item_constraint, cx);
|
if let Some(element) =
|
||||||
|
state.render_item(cursor.start().0, None, item_constraint, cx)
|
||||||
|
{
|
||||||
rendered_height += element.size().y();
|
rendered_height += element.size().y();
|
||||||
rendered_items.push_front(ListItem::Rendered(element));
|
rendered_items.push_front(ListItem::Rendered(element));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -182,9 +199,12 @@ impl Element for List {
|
||||||
while leading_overdraw < state.overdraw {
|
while leading_overdraw < state.overdraw {
|
||||||
cursor.prev(&());
|
cursor.prev(&());
|
||||||
if let Some(item) = cursor.item() {
|
if let Some(item) = cursor.item() {
|
||||||
let element = state.render_item(cursor.start().0, item, item_constraint, cx);
|
if let Some(element) =
|
||||||
|
state.render_item(cursor.start().0, Some(item), item_constraint, cx)
|
||||||
|
{
|
||||||
leading_overdraw += element.size().y();
|
leading_overdraw += element.size().y();
|
||||||
rendered_items.push_front(ListItem::Rendered(element));
|
rendered_items.push_front(ListItem::Rendered(element));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -330,20 +350,26 @@ impl Element for List {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListState {
|
impl ListState {
|
||||||
pub fn new<F>(
|
pub fn new<F, V>(
|
||||||
element_count: usize,
|
element_count: usize,
|
||||||
orientation: Orientation,
|
orientation: Orientation,
|
||||||
overdraw: f32,
|
overdraw: f32,
|
||||||
render_item: F,
|
cx: &mut ViewContext<V>,
|
||||||
|
mut render_item: F,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(usize, &mut LayoutContext) -> ElementBox,
|
V: View,
|
||||||
|
F: 'static + FnMut(&mut V, usize, &mut RenderContext<V>) -> ElementBox,
|
||||||
{
|
{
|
||||||
let mut items = SumTree::new();
|
let mut items = SumTree::new();
|
||||||
items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
|
items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
|
||||||
|
let handle = cx.weak_handle();
|
||||||
Self(Rc::new(RefCell::new(StateInner {
|
Self(Rc::new(RefCell::new(StateInner {
|
||||||
last_layout_width: None,
|
last_layout_width: None,
|
||||||
render_item: Box::new(render_item),
|
render_item: Box::new(move |ix, cx| {
|
||||||
|
let handle = handle.upgrade(cx)?;
|
||||||
|
Some(cx.render(&handle, |view, cx| render_item(view, ix, cx)))
|
||||||
|
}),
|
||||||
rendered_range: 0..0,
|
rendered_range: 0..0,
|
||||||
items,
|
items,
|
||||||
logical_scroll_top: None,
|
logical_scroll_top: None,
|
||||||
|
@ -411,16 +437,16 @@ impl StateInner {
|
||||||
fn render_item(
|
fn render_item(
|
||||||
&mut self,
|
&mut self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
existing_item: &ListItem,
|
existing_element: Option<&ListItem>,
|
||||||
constraint: SizeConstraint,
|
constraint: SizeConstraint,
|
||||||
cx: &mut LayoutContext,
|
cx: &mut LayoutContext,
|
||||||
) -> ElementRc {
|
) -> Option<ElementRc> {
|
||||||
if let ListItem::Rendered(element) = existing_item {
|
if let Some(ListItem::Rendered(element)) = existing_element {
|
||||||
element.clone()
|
Some(element.clone())
|
||||||
} else {
|
} else {
|
||||||
let mut element = (self.render_item)(ix, cx);
|
let mut element = (self.render_item)(ix, cx)?;
|
||||||
element.layout(constraint, cx);
|
element.layout(constraint, cx);
|
||||||
element.into()
|
Some(element.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -593,26 +619,33 @@ impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::geometry::vector::vec2f;
|
use crate::{elements::Empty, geometry::vector::vec2f, Entity};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
#[crate::test(self)]
|
#[crate::test(self)]
|
||||||
fn test_layout(cx: &mut crate::MutableAppContext) {
|
fn test_layout(cx: &mut crate::MutableAppContext) {
|
||||||
let mut presenter = cx.build_presenter(0, 0.);
|
let mut presenter = cx.build_presenter(0, 0.);
|
||||||
|
let (_, view) = cx.add_window(Default::default(), |_| TestView);
|
||||||
let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.));
|
let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.));
|
||||||
|
|
||||||
let elements = Rc::new(RefCell::new(vec![(0, 20.), (1, 30.), (2, 100.)]));
|
let elements = Rc::new(RefCell::new(vec![(0, 20.), (1, 30.), (2, 100.)]));
|
||||||
let state = ListState::new(elements.borrow().len(), Orientation::Top, 1000.0, {
|
|
||||||
|
let state = view.update(cx, |_, cx| {
|
||||||
|
ListState::new(elements.borrow().len(), Orientation::Top, 1000.0, cx, {
|
||||||
let elements = elements.clone();
|
let elements = elements.clone();
|
||||||
move |ix, _| {
|
move |_, ix, _| {
|
||||||
let (id, height) = elements.borrow()[ix];
|
let (id, height) = elements.borrow()[ix];
|
||||||
TestElement::new(id, height).boxed()
|
TestElement::new(id, height).boxed()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut list = List::new(state.clone());
|
let mut list = List::new(state.clone());
|
||||||
let (size, _) = list.layout(constraint, &mut presenter.build_layout_context(false, cx));
|
let (size, _) = list.layout(
|
||||||
|
constraint,
|
||||||
|
&mut presenter.build_layout_context(vec2f(100., 40.), false, cx),
|
||||||
|
);
|
||||||
assert_eq!(size, vec2f(100., 40.));
|
assert_eq!(size, vec2f(100., 40.));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
state.0.borrow().items.summary().clone(),
|
state.0.borrow().items.summary().clone(),
|
||||||
|
@ -634,8 +667,10 @@ mod tests {
|
||||||
true,
|
true,
|
||||||
&mut presenter.build_event_context(cx),
|
&mut presenter.build_event_context(cx),
|
||||||
);
|
);
|
||||||
let (_, logical_scroll_top) =
|
let (_, logical_scroll_top) = list.layout(
|
||||||
list.layout(constraint, &mut presenter.build_layout_context(false, cx));
|
constraint,
|
||||||
|
&mut presenter.build_layout_context(vec2f(100., 40.), false, cx),
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
logical_scroll_top,
|
logical_scroll_top,
|
||||||
ListOffset {
|
ListOffset {
|
||||||
|
@ -659,8 +694,10 @@ mod tests {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
let (size, logical_scroll_top) =
|
let (size, logical_scroll_top) = list.layout(
|
||||||
list.layout(constraint, &mut presenter.build_layout_context(false, cx));
|
constraint,
|
||||||
|
&mut presenter.build_layout_context(vec2f(100., 40.), false, cx),
|
||||||
|
);
|
||||||
assert_eq!(size, vec2f(100., 40.));
|
assert_eq!(size, vec2f(100., 40.));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
state.0.borrow().items.summary().clone(),
|
state.0.borrow().items.summary().clone(),
|
||||||
|
@ -687,6 +724,7 @@ mod tests {
|
||||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||||
.unwrap_or(10);
|
.unwrap_or(10);
|
||||||
|
|
||||||
|
let (_, view) = cx.add_window(Default::default(), |_| TestView);
|
||||||
let mut presenter = cx.build_presenter(0, 0.);
|
let mut presenter = cx.build_presenter(0, 0.);
|
||||||
let mut next_id = 0;
|
let mut next_id = 0;
|
||||||
let elements = Rc::new(RefCell::new(
|
let elements = Rc::new(RefCell::new(
|
||||||
|
@ -702,12 +740,15 @@ mod tests {
|
||||||
.choose(&mut rng)
|
.choose(&mut rng)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let overdraw = rng.gen_range(1..=100) as f32;
|
let overdraw = rng.gen_range(1..=100) as f32;
|
||||||
let state = ListState::new(elements.borrow().len(), orientation, overdraw, {
|
|
||||||
|
let state = view.update(cx, |_, cx| {
|
||||||
|
ListState::new(elements.borrow().len(), orientation, overdraw, cx, {
|
||||||
let elements = elements.clone();
|
let elements = elements.clone();
|
||||||
move |ix, _| {
|
move |_, ix, _| {
|
||||||
let (id, height) = elements.borrow()[ix];
|
let (id, height) = elements.borrow()[ix];
|
||||||
TestElement::new(id, height).boxed()
|
TestElement::new(id, height).boxed()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut width = rng.gen_range(0..=2000) as f32 / 2.;
|
let mut width = rng.gen_range(0..=2000) as f32 / 2.;
|
||||||
|
@ -770,11 +811,12 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut list = List::new(state.clone());
|
let mut list = List::new(state.clone());
|
||||||
|
let window_size = vec2f(width, height);
|
||||||
let (size, logical_scroll_top) = list.layout(
|
let (size, logical_scroll_top) = list.layout(
|
||||||
SizeConstraint::new(vec2f(0., 0.), vec2f(width, height)),
|
SizeConstraint::new(vec2f(0., 0.), window_size),
|
||||||
&mut presenter.build_layout_context(false, cx),
|
&mut presenter.build_layout_context(window_size, false, cx),
|
||||||
);
|
);
|
||||||
assert_eq!(size, vec2f(width, height));
|
assert_eq!(size, window_size);
|
||||||
last_logical_scroll_top = Some(logical_scroll_top);
|
last_logical_scroll_top = Some(logical_scroll_top);
|
||||||
|
|
||||||
let state = state.0.borrow();
|
let state = state.0.borrow();
|
||||||
|
@ -843,6 +885,22 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TestView;
|
||||||
|
|
||||||
|
impl Entity for TestView {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for TestView {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"TestView"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, _: &mut RenderContext<'_, Self>) -> ElementBox {
|
||||||
|
Empty::new().boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct TestElement {
|
struct TestElement {
|
||||||
id: usize,
|
id: usize,
|
||||||
size: Vector2F,
|
size: Vector2F,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::{any::TypeId, rc::Rc};
|
||||||
|
|
||||||
use super::Padding;
|
use super::Padding;
|
||||||
use crate::{
|
use crate::{
|
||||||
geometry::{
|
geometry::{
|
||||||
|
@ -5,44 +7,46 @@ use crate::{
|
||||||
vector::{vec2f, Vector2F},
|
vector::{vec2f, Vector2F},
|
||||||
},
|
},
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
DebugContext, Element, ElementBox, ElementStateContext, ElementStateHandle, Event,
|
scene::CursorRegion,
|
||||||
EventContext, LayoutContext, PaintContext, SizeConstraint,
|
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion, MouseState,
|
||||||
|
PaintContext, RenderContext, SizeConstraint, View,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
pub struct MouseEventHandler {
|
pub struct MouseEventHandler {
|
||||||
state: ElementStateHandle<MouseState>,
|
|
||||||
child: ElementBox,
|
child: ElementBox,
|
||||||
|
tag: TypeId,
|
||||||
|
id: usize,
|
||||||
cursor_style: Option<CursorStyle>,
|
cursor_style: Option<CursorStyle>,
|
||||||
mouse_down_handler: Option<Box<dyn FnMut(&mut EventContext)>>,
|
mouse_down: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||||
click_handler: Option<Box<dyn FnMut(usize, &mut EventContext)>>,
|
click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
|
||||||
drag_handler: Option<Box<dyn FnMut(Vector2F, &mut EventContext)>>,
|
right_mouse_down: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||||
|
right_click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
|
||||||
|
mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||||
|
right_mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||||
|
drag: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||||
padding: Padding,
|
padding: Padding,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct MouseState {
|
|
||||||
pub hovered: bool,
|
|
||||||
pub clicked: bool,
|
|
||||||
prev_drag_position: Option<Vector2F>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MouseEventHandler {
|
impl MouseEventHandler {
|
||||||
pub fn new<Tag, C, F>(id: usize, cx: &mut C, render_child: F) -> Self
|
pub fn new<Tag, V, F>(id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
|
||||||
where
|
where
|
||||||
Tag: 'static,
|
Tag: 'static,
|
||||||
C: ElementStateContext,
|
V: View,
|
||||||
F: FnOnce(&MouseState, &mut C) -> ElementBox,
|
F: FnOnce(MouseState, &mut RenderContext<V>) -> ElementBox,
|
||||||
{
|
{
|
||||||
let state_handle = cx.element_state::<Tag, _>(id);
|
|
||||||
let child = state_handle.update(cx, |state, cx| render_child(state, cx));
|
|
||||||
Self {
|
Self {
|
||||||
state: state_handle,
|
id,
|
||||||
child,
|
tag: TypeId::of::<Tag>(),
|
||||||
|
child: render_child(cx.mouse_state::<Tag>(id), cx),
|
||||||
cursor_style: None,
|
cursor_style: None,
|
||||||
mouse_down_handler: None,
|
mouse_down: None,
|
||||||
click_handler: None,
|
click: None,
|
||||||
drag_handler: None,
|
right_mouse_down: None,
|
||||||
|
right_click: None,
|
||||||
|
mouse_down_out: None,
|
||||||
|
right_mouse_down_out: None,
|
||||||
|
drag: None,
|
||||||
padding: Default::default(),
|
padding: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,18 +56,56 @@ impl MouseEventHandler {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_mouse_down(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self {
|
pub fn on_mouse_down(
|
||||||
self.mouse_down_handler = Some(Box::new(handler));
|
mut self,
|
||||||
|
handler: impl Fn(Vector2F, &mut EventContext) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.mouse_down = Some(Rc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_click(mut self, handler: impl FnMut(usize, &mut EventContext) + 'static) -> Self {
|
pub fn on_click(
|
||||||
self.click_handler = Some(Box::new(handler));
|
mut self,
|
||||||
|
handler: impl Fn(Vector2F, usize, &mut EventContext) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.click = Some(Rc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_drag(mut self, handler: impl FnMut(Vector2F, &mut EventContext) + 'static) -> Self {
|
pub fn on_right_mouse_down(
|
||||||
self.drag_handler = Some(Box::new(handler));
|
mut self,
|
||||||
|
handler: impl Fn(Vector2F, &mut EventContext) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.right_mouse_down = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_right_click(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(Vector2F, usize, &mut EventContext) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.right_click = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_mouse_down_out(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(Vector2F, &mut EventContext) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.mouse_down_out = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_right_mouse_down_out(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(Vector2F, &mut EventContext) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.right_mouse_down_out = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_drag(mut self, handler: impl Fn(Vector2F, &mut EventContext) + 'static) -> Self {
|
||||||
|
self.drag = Some(Rc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,10 +142,27 @@ impl Element for MouseEventHandler {
|
||||||
_: &mut Self::LayoutState,
|
_: &mut Self::LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) -> Self::PaintState {
|
) -> Self::PaintState {
|
||||||
if let Some(cursor_style) = self.cursor_style {
|
if let Some(style) = self.cursor_style {
|
||||||
cx.scene
|
cx.scene.push_cursor_region(CursorRegion {
|
||||||
.push_cursor_style(self.hit_bounds(bounds), cursor_style);
|
bounds: self.hit_bounds(bounds),
|
||||||
|
style,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cx.scene.push_mouse_region(MouseRegion {
|
||||||
|
view_id: cx.current_view_id(),
|
||||||
|
discriminant: Some((self.tag, self.id)),
|
||||||
|
bounds: self.hit_bounds(bounds),
|
||||||
|
hover: None,
|
||||||
|
click: self.click.clone(),
|
||||||
|
mouse_down: self.mouse_down.clone(),
|
||||||
|
right_click: self.right_click.clone(),
|
||||||
|
right_mouse_down: self.right_mouse_down.clone(),
|
||||||
|
mouse_down_out: self.mouse_down_out.clone(),
|
||||||
|
right_mouse_down_out: self.right_mouse_down_out.clone(),
|
||||||
|
drag: self.drag.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,81 +170,12 @@ impl Element for MouseEventHandler {
|
||||||
&mut self,
|
&mut self,
|
||||||
event: &Event,
|
event: &Event,
|
||||||
_: RectF,
|
_: RectF,
|
||||||
visible_bounds: RectF,
|
_: RectF,
|
||||||
_: &mut Self::LayoutState,
|
_: &mut Self::LayoutState,
|
||||||
_: &mut Self::PaintState,
|
_: &mut Self::PaintState,
|
||||||
cx: &mut EventContext,
|
cx: &mut EventContext,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let hit_bounds = self.hit_bounds(visible_bounds);
|
self.child.dispatch_event(event, cx)
|
||||||
let mouse_down_handler = self.mouse_down_handler.as_mut();
|
|
||||||
let click_handler = self.click_handler.as_mut();
|
|
||||||
let drag_handler = self.drag_handler.as_mut();
|
|
||||||
|
|
||||||
let handled_in_child = self.child.dispatch_event(event, cx);
|
|
||||||
|
|
||||||
self.state.update(cx, |state, cx| match event {
|
|
||||||
Event::MouseMoved {
|
|
||||||
position,
|
|
||||||
left_mouse_down,
|
|
||||||
} => {
|
|
||||||
if !left_mouse_down {
|
|
||||||
let mouse_in = hit_bounds.contains_point(*position);
|
|
||||||
if state.hovered != mouse_in {
|
|
||||||
state.hovered = mouse_in;
|
|
||||||
cx.notify();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handled_in_child
|
|
||||||
}
|
|
||||||
Event::LeftMouseDown { position, .. } => {
|
|
||||||
if !handled_in_child && hit_bounds.contains_point(*position) {
|
|
||||||
state.clicked = true;
|
|
||||||
state.prev_drag_position = Some(*position);
|
|
||||||
cx.notify();
|
|
||||||
if let Some(handler) = mouse_down_handler {
|
|
||||||
handler(cx);
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
handled_in_child
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Event::LeftMouseUp {
|
|
||||||
position,
|
|
||||||
click_count,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
state.prev_drag_position = None;
|
|
||||||
if !handled_in_child && state.clicked {
|
|
||||||
state.clicked = false;
|
|
||||||
cx.notify();
|
|
||||||
if let Some(handler) = click_handler {
|
|
||||||
if hit_bounds.contains_point(*position) {
|
|
||||||
handler(*click_count, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
handled_in_child
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Event::LeftMouseDragged { position, .. } => {
|
|
||||||
if !handled_in_child && state.clicked {
|
|
||||||
let prev_drag_position = state.prev_drag_position.replace(*position);
|
|
||||||
if let Some((handler, prev_position)) = drag_handler.zip(prev_drag_position) {
|
|
||||||
let delta = *position - prev_position;
|
|
||||||
if !delta.is_zero() {
|
|
||||||
(handler)(delta, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
handled_in_child
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => handled_in_child,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn debug(
|
fn debug(
|
||||||
|
|
|
@ -1,16 +1,28 @@
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
|
json::ToJson,
|
||||||
SizeConstraint,
|
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion,
|
||||||
|
PaintContext, SizeConstraint,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Overlay {
|
pub struct Overlay {
|
||||||
child: ElementBox,
|
child: ElementBox,
|
||||||
|
abs_position: Option<Vector2F>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Overlay {
|
impl Overlay {
|
||||||
pub fn new(child: ElementBox) -> Self {
|
pub fn new(child: ElementBox) -> Self {
|
||||||
Self { child }
|
Self {
|
||||||
|
child,
|
||||||
|
abs_position: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_abs_position(mut self, position: Vector2F) -> Self {
|
||||||
|
self.abs_position = Some(position);
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +35,11 @@ impl Element for Overlay {
|
||||||
constraint: SizeConstraint,
|
constraint: SizeConstraint,
|
||||||
cx: &mut LayoutContext,
|
cx: &mut LayoutContext,
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
) -> (Vector2F, Self::LayoutState) {
|
||||||
|
let constraint = if self.abs_position.is_some() {
|
||||||
|
SizeConstraint::new(Vector2F::zero(), cx.window_size)
|
||||||
|
} else {
|
||||||
|
constraint
|
||||||
|
};
|
||||||
let size = self.child.layout(constraint, cx);
|
let size = self.child.layout(constraint, cx);
|
||||||
(Vector2F::zero(), size)
|
(Vector2F::zero(), size)
|
||||||
}
|
}
|
||||||
|
@ -34,9 +51,15 @@ impl Element for Overlay {
|
||||||
size: &mut Self::LayoutState,
|
size: &mut Self::LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) {
|
) {
|
||||||
let bounds = RectF::new(bounds.origin(), *size);
|
let origin = self.abs_position.unwrap_or(bounds.origin());
|
||||||
|
let visible_bounds = RectF::new(origin, *size);
|
||||||
cx.scene.push_stacking_context(None);
|
cx.scene.push_stacking_context(None);
|
||||||
self.child.paint(bounds.origin(), bounds, cx);
|
cx.scene.push_mouse_region(MouseRegion {
|
||||||
|
view_id: cx.current_view_id(),
|
||||||
|
bounds: visible_bounds,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
self.child.paint(origin, visible_bounds, cx);
|
||||||
cx.scene.pop_stacking_context();
|
cx.scene.pop_stacking_context();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,6 +82,10 @@ impl Element for Overlay {
|
||||||
_: &Self::PaintState,
|
_: &Self::PaintState,
|
||||||
cx: &DebugContext,
|
cx: &DebugContext,
|
||||||
) -> serde_json::Value {
|
) -> serde_json::Value {
|
||||||
self.child.debug(cx)
|
json!({
|
||||||
|
"type": "Overlay",
|
||||||
|
"abs_position": self.abs_position.to_json(),
|
||||||
|
"child": self.child.debug(cx),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
vector::{vec2f, Vector2F},
|
vector::{vec2f, Vector2F},
|
||||||
},
|
},
|
||||||
json::{self, json},
|
json::{self, json},
|
||||||
ElementBox,
|
ElementBox, RenderContext, View,
|
||||||
};
|
};
|
||||||
use json::ToJson;
|
use json::ToJson;
|
||||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||||
|
@ -41,27 +41,37 @@ pub struct LayoutState {
|
||||||
items: Vec<ElementBox>,
|
items: Vec<ElementBox>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UniformList<F>
|
pub struct UniformList {
|
||||||
where
|
|
||||||
F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext),
|
|
||||||
{
|
|
||||||
state: UniformListState,
|
state: UniformListState,
|
||||||
item_count: usize,
|
item_count: usize,
|
||||||
append_items: F,
|
append_items: Box<dyn Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext)>,
|
||||||
padding_top: f32,
|
padding_top: f32,
|
||||||
padding_bottom: f32,
|
padding_bottom: f32,
|
||||||
get_width_from_item: Option<usize>,
|
get_width_from_item: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F> UniformList<F>
|
impl UniformList {
|
||||||
|
pub fn new<F, V>(
|
||||||
|
state: UniformListState,
|
||||||
|
item_count: usize,
|
||||||
|
cx: &mut RenderContext<V>,
|
||||||
|
append_items: F,
|
||||||
|
) -> Self
|
||||||
where
|
where
|
||||||
F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext),
|
V: View,
|
||||||
|
F: 'static + Fn(&mut V, Range<usize>, &mut Vec<ElementBox>, &mut RenderContext<V>),
|
||||||
{
|
{
|
||||||
pub fn new(state: UniformListState, item_count: usize, append_items: F) -> Self {
|
let handle = cx.handle();
|
||||||
Self {
|
Self {
|
||||||
state,
|
state,
|
||||||
item_count,
|
item_count,
|
||||||
append_items,
|
append_items: Box::new(move |range, items, cx| {
|
||||||
|
if let Some(handle) = handle.upgrade(cx) {
|
||||||
|
cx.render(&handle, |view, cx| {
|
||||||
|
append_items(view, range, items, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
padding_top: 0.,
|
padding_top: 0.,
|
||||||
padding_bottom: 0.,
|
padding_bottom: 0.,
|
||||||
get_width_from_item: None,
|
get_width_from_item: None,
|
||||||
|
@ -144,10 +154,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F> Element for UniformList<F>
|
impl Element for UniformList {
|
||||||
where
|
|
||||||
F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext),
|
|
||||||
{
|
|
||||||
type LayoutState = LayoutState;
|
type LayoutState = LayoutState;
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
|
||||||
|
@ -162,8 +169,7 @@ where
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.item_count == 0 {
|
let no_items = (
|
||||||
return (
|
|
||||||
constraint.min,
|
constraint.min,
|
||||||
LayoutState {
|
LayoutState {
|
||||||
item_height: 0.,
|
item_height: 0.,
|
||||||
|
@ -171,24 +177,32 @@ where
|
||||||
items: Default::default(),
|
items: Default::default(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if self.item_count == 0 {
|
||||||
|
return no_items;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
let mut size = constraint.max;
|
let mut size = constraint.max;
|
||||||
let mut item_size;
|
let mut item_size;
|
||||||
let sample_item_ix;
|
let sample_item_ix;
|
||||||
let mut sample_item;
|
let sample_item;
|
||||||
if let Some(sample_ix) = self.get_width_from_item {
|
if let Some(sample_ix) = self.get_width_from_item {
|
||||||
(self.append_items)(sample_ix..sample_ix + 1, &mut items, cx);
|
(self.append_items)(sample_ix..sample_ix + 1, &mut items, cx);
|
||||||
sample_item_ix = sample_ix;
|
sample_item_ix = sample_ix;
|
||||||
sample_item = items.pop().unwrap();
|
|
||||||
item_size = sample_item.layout(constraint, cx);
|
if let Some(mut item) = items.pop() {
|
||||||
|
item_size = item.layout(constraint, cx);
|
||||||
size.set_x(item_size.x());
|
size.set_x(item_size.x());
|
||||||
|
sample_item = item;
|
||||||
|
} else {
|
||||||
|
return no_items;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
(self.append_items)(0..1, &mut items, cx);
|
(self.append_items)(0..1, &mut items, cx);
|
||||||
sample_item_ix = 0;
|
sample_item_ix = 0;
|
||||||
sample_item = items.pop().unwrap();
|
if let Some(mut item) = items.pop() {
|
||||||
item_size = sample_item.layout(
|
item_size = item.layout(
|
||||||
SizeConstraint::new(
|
SizeConstraint::new(
|
||||||
vec2f(constraint.max.x(), 0.0),
|
vec2f(constraint.max.x(), 0.0),
|
||||||
vec2f(constraint.max.x(), f32::INFINITY),
|
vec2f(constraint.max.x(), f32::INFINITY),
|
||||||
|
@ -196,6 +210,10 @@ where
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
item_size.set_x(size.x());
|
item_size.set_x(size.x());
|
||||||
|
sample_item = item
|
||||||
|
} else {
|
||||||
|
return no_items;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let item_constraint = SizeConstraint {
|
let item_constraint = SizeConstraint {
|
||||||
|
|
|
@ -16,7 +16,7 @@ pub mod fonts;
|
||||||
pub mod geometry;
|
pub mod geometry;
|
||||||
mod presenter;
|
mod presenter;
|
||||||
mod scene;
|
mod scene;
|
||||||
pub use scene::{Border, Quad, Scene};
|
pub use scene::{Border, CursorRegion, MouseRegion, MouseRegionId, Quad, Scene};
|
||||||
pub mod text_layout;
|
pub mod text_layout;
|
||||||
pub use text_layout::TextLayoutCache;
|
pub use text_layout::TextLayoutCache;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
|
@ -30,9 +30,9 @@ pub struct Keymap {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Binding {
|
pub struct Binding {
|
||||||
keystrokes: Vec<Keystroke>,
|
keystrokes: SmallVec<[Keystroke; 2]>,
|
||||||
action: Box<dyn Action>,
|
action: Box<dyn Action>,
|
||||||
context: Option<ContextPredicate>,
|
context_predicate: Option<ContextPredicate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
@ -146,7 +146,11 @@ impl Matcher {
|
||||||
let mut retain_pending = false;
|
let mut retain_pending = false;
|
||||||
for binding in self.keymap.bindings.iter().rev() {
|
for binding in self.keymap.bindings.iter().rev() {
|
||||||
if binding.keystrokes.starts_with(&pending.keystrokes)
|
if binding.keystrokes.starts_with(&pending.keystrokes)
|
||||||
&& binding.context.as_ref().map(|c| c.eval(cx)).unwrap_or(true)
|
&& binding
|
||||||
|
.context_predicate
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.eval(cx))
|
||||||
|
.unwrap_or(true)
|
||||||
{
|
{
|
||||||
if binding.keystrokes.len() == pending.keystrokes.len() {
|
if binding.keystrokes.len() == pending.keystrokes.len() {
|
||||||
self.pending.remove(&view_id);
|
self.pending.remove(&view_id);
|
||||||
|
@ -165,6 +169,24 @@ impl Matcher {
|
||||||
MatchResult::None
|
MatchResult::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn keystrokes_for_action(
|
||||||
|
&self,
|
||||||
|
action: &dyn Action,
|
||||||
|
cx: &Context,
|
||||||
|
) -> Option<SmallVec<[Keystroke; 2]>> {
|
||||||
|
for binding in self.keymap.bindings.iter().rev() {
|
||||||
|
if binding.action.id() == action.id()
|
||||||
|
&& binding
|
||||||
|
.context_predicate
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |predicate| predicate.eval(cx))
|
||||||
|
{
|
||||||
|
return Some(binding.keystrokes.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Matcher {
|
impl Default for Matcher {
|
||||||
|
@ -236,7 +258,7 @@ impl Binding {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
keystrokes,
|
keystrokes,
|
||||||
action,
|
action,
|
||||||
context,
|
context_predicate: context,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,6 +311,34 @@ impl Keystroke {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Keystroke {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if self.ctrl {
|
||||||
|
write!(f, "{}", "^")?;
|
||||||
|
}
|
||||||
|
if self.alt {
|
||||||
|
write!(f, "{}", "⎇")?;
|
||||||
|
}
|
||||||
|
if self.cmd {
|
||||||
|
write!(f, "{}", "⌘")?;
|
||||||
|
}
|
||||||
|
if self.shift {
|
||||||
|
write!(f, "{}", "⇧")?;
|
||||||
|
}
|
||||||
|
let key = match self.key.as_str() {
|
||||||
|
"backspace" => "⌫",
|
||||||
|
"up" => "↑",
|
||||||
|
"down" => "↓",
|
||||||
|
"left" => "←",
|
||||||
|
"right" => "→",
|
||||||
|
"tab" => "⇥",
|
||||||
|
"escape" => "⎋",
|
||||||
|
key => key,
|
||||||
|
};
|
||||||
|
write!(f, "{}", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
pub fn extend(&mut self, other: &Context) {
|
pub fn extend(&mut self, other: &Context) {
|
||||||
for v in &other.set {
|
for v in &other.set {
|
||||||
|
|
|
@ -43,6 +43,7 @@ pub enum Event {
|
||||||
},
|
},
|
||||||
RightMouseUp {
|
RightMouseUp {
|
||||||
position: Vector2F,
|
position: Vector2F,
|
||||||
|
click_count: usize,
|
||||||
},
|
},
|
||||||
NavigateMouseDown {
|
NavigateMouseDown {
|
||||||
position: Vector2F,
|
position: Vector2F,
|
||||||
|
@ -72,7 +73,7 @@ impl Event {
|
||||||
| Event::LeftMouseUp { position, .. }
|
| Event::LeftMouseUp { position, .. }
|
||||||
| Event::LeftMouseDragged { position }
|
| Event::LeftMouseDragged { position }
|
||||||
| Event::RightMouseDown { position, .. }
|
| Event::RightMouseDown { position, .. }
|
||||||
| Event::RightMouseUp { position }
|
| Event::RightMouseUp { position, .. }
|
||||||
| Event::NavigateMouseDown { position, .. }
|
| Event::NavigateMouseDown { position, .. }
|
||||||
| Event::NavigateMouseUp { position, .. }
|
| Event::NavigateMouseUp { position, .. }
|
||||||
| Event::MouseMoved { position, .. } => Some(*position),
|
| Event::MouseMoved { position, .. } => Some(*position),
|
||||||
|
|
|
@ -178,6 +178,7 @@ impl Event {
|
||||||
native_event.locationInWindow().x as f32,
|
native_event.locationInWindow().x as f32,
|
||||||
window_height - native_event.locationInWindow().y as f32,
|
window_height - native_event.locationInWindow().y as f32,
|
||||||
),
|
),
|
||||||
|
click_count: native_event.clickCount() as usize,
|
||||||
}),
|
}),
|
||||||
NSEventType::NSOtherMouseDown => {
|
NSEventType::NSOtherMouseDown => {
|
||||||
let direction = match native_event.buttonNumber() {
|
let direction = match native_event.buttonNumber() {
|
||||||
|
|
|
@ -4,16 +4,21 @@ use crate::{
|
||||||
font_cache::FontCache,
|
font_cache::FontCache,
|
||||||
geometry::rect::RectF,
|
geometry::rect::RectF,
|
||||||
json::{self, ToJson},
|
json::{self, ToJson},
|
||||||
|
keymap::Keystroke,
|
||||||
platform::{CursorStyle, Event},
|
platform::{CursorStyle, Event},
|
||||||
|
scene::CursorRegion,
|
||||||
text_layout::TextLayoutCache,
|
text_layout::TextLayoutCache,
|
||||||
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox,
|
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, Entity,
|
||||||
ElementStateContext, Entity, FontSystem, ModelHandle, ReadModel, ReadView, Scene,
|
FontSystem, ModelHandle, MouseRegion, MouseRegionId, ReadModel, ReadView, RenderContext,
|
||||||
UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle, WeakViewHandle,
|
RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle,
|
||||||
|
WeakViewHandle,
|
||||||
};
|
};
|
||||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
|
marker::PhantomData,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
@ -22,11 +27,16 @@ pub struct Presenter {
|
||||||
window_id: usize,
|
window_id: usize,
|
||||||
pub(crate) rendered_views: HashMap<usize, ElementBox>,
|
pub(crate) rendered_views: HashMap<usize, ElementBox>,
|
||||||
parents: HashMap<usize, usize>,
|
parents: HashMap<usize, usize>,
|
||||||
cursor_styles: Vec<(RectF, CursorStyle)>,
|
cursor_regions: Vec<CursorRegion>,
|
||||||
|
mouse_regions: Vec<(MouseRegion, usize)>,
|
||||||
font_cache: Arc<FontCache>,
|
font_cache: Arc<FontCache>,
|
||||||
text_layout_cache: TextLayoutCache,
|
text_layout_cache: TextLayoutCache,
|
||||||
asset_cache: Arc<AssetCache>,
|
asset_cache: Arc<AssetCache>,
|
||||||
last_mouse_moved_event: Option<Event>,
|
last_mouse_moved_event: Option<Event>,
|
||||||
|
hovered_region_ids: HashSet<MouseRegionId>,
|
||||||
|
clicked_region: Option<MouseRegion>,
|
||||||
|
right_clicked_region: Option<MouseRegion>,
|
||||||
|
prev_drag_position: Option<Vector2F>,
|
||||||
titlebar_height: f32,
|
titlebar_height: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,32 +53,35 @@ impl Presenter {
|
||||||
window_id,
|
window_id,
|
||||||
rendered_views: cx.render_views(window_id, titlebar_height),
|
rendered_views: cx.render_views(window_id, titlebar_height),
|
||||||
parents: HashMap::new(),
|
parents: HashMap::new(),
|
||||||
cursor_styles: Default::default(),
|
cursor_regions: Default::default(),
|
||||||
|
mouse_regions: Default::default(),
|
||||||
font_cache,
|
font_cache,
|
||||||
text_layout_cache,
|
text_layout_cache,
|
||||||
asset_cache,
|
asset_cache,
|
||||||
last_mouse_moved_event: None,
|
last_mouse_moved_event: None,
|
||||||
|
hovered_region_ids: Default::default(),
|
||||||
|
clicked_region: None,
|
||||||
|
right_clicked_region: None,
|
||||||
|
prev_drag_position: None,
|
||||||
titlebar_height,
|
titlebar_height,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dispatch_path(&self, app: &AppContext) -> Vec<usize> {
|
pub fn dispatch_path(&self, app: &AppContext) -> Vec<usize> {
|
||||||
|
let mut path = Vec::new();
|
||||||
if let Some(view_id) = app.focused_view_id(self.window_id) {
|
if let Some(view_id) = app.focused_view_id(self.window_id) {
|
||||||
self.dispatch_path_from(view_id)
|
self.compute_dispatch_path_from(view_id, &mut path)
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
}
|
}
|
||||||
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn dispatch_path_from(&self, mut view_id: usize) -> Vec<usize> {
|
pub(crate) fn compute_dispatch_path_from(&self, mut view_id: usize, path: &mut Vec<usize>) {
|
||||||
let mut path = Vec::new();
|
|
||||||
path.push(view_id);
|
path.push(view_id);
|
||||||
while let Some(parent_id) = self.parents.get(&view_id).copied() {
|
while let Some(parent_id) = self.parents.get(&view_id).copied() {
|
||||||
path.push(parent_id);
|
path.push(parent_id);
|
||||||
view_id = parent_id;
|
view_id = parent_id;
|
||||||
}
|
}
|
||||||
path.reverse();
|
path.reverse();
|
||||||
path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn invalidate(
|
pub fn invalidate(
|
||||||
|
@ -85,7 +98,18 @@ impl Presenter {
|
||||||
for view_id in &invalidation.updated {
|
for view_id in &invalidation.updated {
|
||||||
self.rendered_views.insert(
|
self.rendered_views.insert(
|
||||||
*view_id,
|
*view_id,
|
||||||
cx.render_view(self.window_id, *view_id, self.titlebar_height, false)
|
cx.render_view(RenderParams {
|
||||||
|
window_id: self.window_id,
|
||||||
|
view_id: *view_id,
|
||||||
|
titlebar_height: self.titlebar_height,
|
||||||
|
hovered_region_ids: self.hovered_region_ids.clone(),
|
||||||
|
clicked_region_id: self.clicked_region.as_ref().and_then(MouseRegion::id),
|
||||||
|
right_clicked_region_id: self
|
||||||
|
.right_clicked_region
|
||||||
|
.as_ref()
|
||||||
|
.and_then(MouseRegion::id),
|
||||||
|
refreshing: false,
|
||||||
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -96,7 +120,18 @@ impl Presenter {
|
||||||
for (view_id, view) in &mut self.rendered_views {
|
for (view_id, view) in &mut self.rendered_views {
|
||||||
if !invalidation.updated.contains(view_id) {
|
if !invalidation.updated.contains(view_id) {
|
||||||
*view = cx
|
*view = cx
|
||||||
.render_view(self.window_id, *view_id, self.titlebar_height, true)
|
.render_view(RenderParams {
|
||||||
|
window_id: self.window_id,
|
||||||
|
view_id: *view_id,
|
||||||
|
titlebar_height: self.titlebar_height,
|
||||||
|
hovered_region_ids: self.hovered_region_ids.clone(),
|
||||||
|
clicked_region_id: self.clicked_region.as_ref().and_then(MouseRegion::id),
|
||||||
|
right_clicked_region_id: self
|
||||||
|
.right_clicked_region
|
||||||
|
.as_ref()
|
||||||
|
.and_then(MouseRegion::id),
|
||||||
|
refreshing: true,
|
||||||
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -120,7 +155,8 @@ impl Presenter {
|
||||||
RectF::new(Vector2F::zero(), window_size),
|
RectF::new(Vector2F::zero(), window_size),
|
||||||
);
|
);
|
||||||
self.text_layout_cache.finish_frame();
|
self.text_layout_cache.finish_frame();
|
||||||
self.cursor_styles = scene.cursor_styles();
|
self.cursor_regions = scene.cursor_regions();
|
||||||
|
self.mouse_regions = scene.mouse_regions();
|
||||||
|
|
||||||
if cx.window_is_active(self.window_id) {
|
if cx.window_is_active(self.window_id) {
|
||||||
if let Some(event) = self.last_mouse_moved_event.clone() {
|
if let Some(event) = self.last_mouse_moved_event.clone() {
|
||||||
|
@ -134,27 +170,34 @@ impl Presenter {
|
||||||
scene
|
scene
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout(&mut self, size: Vector2F, refreshing: bool, cx: &mut MutableAppContext) {
|
fn layout(&mut self, window_size: Vector2F, refreshing: bool, cx: &mut MutableAppContext) {
|
||||||
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
|
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
|
||||||
self.build_layout_context(refreshing, cx)
|
self.build_layout_context(window_size, refreshing, cx)
|
||||||
.layout(root_view_id, SizeConstraint::strict(size));
|
.layout(root_view_id, SizeConstraint::strict(window_size));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_layout_context<'a>(
|
pub fn build_layout_context<'a>(
|
||||||
&'a mut self,
|
&'a mut self,
|
||||||
|
window_size: Vector2F,
|
||||||
refreshing: bool,
|
refreshing: bool,
|
||||||
cx: &'a mut MutableAppContext,
|
cx: &'a mut MutableAppContext,
|
||||||
) -> LayoutContext<'a> {
|
) -> LayoutContext<'a> {
|
||||||
LayoutContext {
|
LayoutContext {
|
||||||
|
window_id: self.window_id,
|
||||||
rendered_views: &mut self.rendered_views,
|
rendered_views: &mut self.rendered_views,
|
||||||
parents: &mut self.parents,
|
parents: &mut self.parents,
|
||||||
refreshing,
|
|
||||||
font_cache: &self.font_cache,
|
font_cache: &self.font_cache,
|
||||||
font_system: cx.platform().fonts(),
|
font_system: cx.platform().fonts(),
|
||||||
text_layout_cache: &self.text_layout_cache,
|
text_layout_cache: &self.text_layout_cache,
|
||||||
asset_cache: &self.asset_cache,
|
asset_cache: &self.asset_cache,
|
||||||
view_stack: Vec::new(),
|
view_stack: Vec::new(),
|
||||||
|
refreshing,
|
||||||
|
hovered_region_ids: self.hovered_region_ids.clone(),
|
||||||
|
clicked_region_id: self.clicked_region.as_ref().and_then(MouseRegion::id),
|
||||||
|
right_clicked_region_id: self.right_clicked_region.as_ref().and_then(MouseRegion::id),
|
||||||
|
titlebar_height: self.titlebar_height,
|
||||||
|
window_size,
|
||||||
app: cx,
|
app: cx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,13 +212,80 @@ impl Presenter {
|
||||||
font_cache: &self.font_cache,
|
font_cache: &self.font_cache,
|
||||||
text_layout_cache: &self.text_layout_cache,
|
text_layout_cache: &self.text_layout_cache,
|
||||||
rendered_views: &mut self.rendered_views,
|
rendered_views: &mut self.rendered_views,
|
||||||
|
view_stack: Vec::new(),
|
||||||
app: cx,
|
app: cx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) {
|
pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) {
|
||||||
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
|
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
|
||||||
|
let mut invalidated_views = Vec::new();
|
||||||
|
let mut hovered_regions = Vec::new();
|
||||||
|
let mut unhovered_regions = Vec::new();
|
||||||
|
let mut mouse_down_out_handlers = Vec::new();
|
||||||
|
let mut mouse_down_region = None;
|
||||||
|
let mut clicked_region = None;
|
||||||
|
let mut right_mouse_down_region = None;
|
||||||
|
let mut right_clicked_region = None;
|
||||||
|
let mut dragged_region = None;
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
|
Event::LeftMouseDown { position, .. } => {
|
||||||
|
let mut hit = false;
|
||||||
|
for (region, _) in self.mouse_regions.iter().rev() {
|
||||||
|
if region.bounds.contains_point(position) {
|
||||||
|
if !hit {
|
||||||
|
hit = true;
|
||||||
|
invalidated_views.push(region.view_id);
|
||||||
|
mouse_down_region = Some((region.clone(), position));
|
||||||
|
self.clicked_region = Some(region.clone());
|
||||||
|
self.prev_drag_position = Some(position);
|
||||||
|
}
|
||||||
|
} else if let Some(handler) = region.mouse_down_out.clone() {
|
||||||
|
mouse_down_out_handlers.push((handler, region.view_id, position));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::LeftMouseUp {
|
||||||
|
position,
|
||||||
|
click_count,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.prev_drag_position.take();
|
||||||
|
if let Some(region) = self.clicked_region.take() {
|
||||||
|
invalidated_views.push(region.view_id);
|
||||||
|
if region.bounds.contains_point(position) {
|
||||||
|
clicked_region = Some((region, position, click_count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::RightMouseDown { position, .. } => {
|
||||||
|
let mut hit = false;
|
||||||
|
for (region, _) in self.mouse_regions.iter().rev() {
|
||||||
|
if region.bounds.contains_point(position) {
|
||||||
|
if !hit {
|
||||||
|
hit = true;
|
||||||
|
invalidated_views.push(region.view_id);
|
||||||
|
right_mouse_down_region = Some((region.clone(), position));
|
||||||
|
self.right_clicked_region = Some(region.clone());
|
||||||
|
}
|
||||||
|
} else if let Some(handler) = region.right_mouse_down_out.clone() {
|
||||||
|
mouse_down_out_handlers.push((handler, region.view_id, position));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::RightMouseUp {
|
||||||
|
position,
|
||||||
|
click_count,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if let Some(region) = self.right_clicked_region.take() {
|
||||||
|
invalidated_views.push(region.view_id);
|
||||||
|
if region.bounds.contains_point(position) {
|
||||||
|
right_clicked_region = Some((region, position, click_count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Event::MouseMoved {
|
Event::MouseMoved {
|
||||||
position,
|
position,
|
||||||
left_mouse_down,
|
left_mouse_down,
|
||||||
|
@ -184,16 +294,50 @@ impl Presenter {
|
||||||
|
|
||||||
if !left_mouse_down {
|
if !left_mouse_down {
|
||||||
let mut style_to_assign = CursorStyle::Arrow;
|
let mut style_to_assign = CursorStyle::Arrow;
|
||||||
for (bounds, style) in self.cursor_styles.iter().rev() {
|
for region in self.cursor_regions.iter().rev() {
|
||||||
if bounds.contains_point(position) {
|
if region.bounds.contains_point(position) {
|
||||||
style_to_assign = *style;
|
style_to_assign = region.style;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cx.platform().set_cursor_style(style_to_assign);
|
cx.platform().set_cursor_style(style_to_assign);
|
||||||
|
|
||||||
|
let mut hover_depth = None;
|
||||||
|
for (region, depth) in self.mouse_regions.iter().rev() {
|
||||||
|
if region.bounds.contains_point(position)
|
||||||
|
&& hover_depth.map_or(true, |hover_depth| hover_depth == *depth)
|
||||||
|
{
|
||||||
|
hover_depth = Some(*depth);
|
||||||
|
if let Some(region_id) = region.id() {
|
||||||
|
if !self.hovered_region_ids.contains(®ion_id) {
|
||||||
|
invalidated_views.push(region.view_id);
|
||||||
|
hovered_regions.push(region.clone());
|
||||||
|
self.hovered_region_ids.insert(region_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some(region_id) = region.id() {
|
||||||
|
if self.hovered_region_ids.contains(®ion_id) {
|
||||||
|
invalidated_views.push(region.view_id);
|
||||||
|
unhovered_regions.push(region.clone());
|
||||||
|
self.hovered_region_ids.remove(®ion_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::LeftMouseDragged { position } => {
|
Event::LeftMouseDragged { position } => {
|
||||||
|
if let Some((clicked_region, prev_drag_position)) = self
|
||||||
|
.clicked_region
|
||||||
|
.as_ref()
|
||||||
|
.zip(self.prev_drag_position.as_mut())
|
||||||
|
{
|
||||||
|
dragged_region =
|
||||||
|
Some((clicked_region.clone(), position - *prev_drag_position));
|
||||||
|
*prev_drag_position = position;
|
||||||
|
}
|
||||||
|
|
||||||
self.last_mouse_moved_event = Some(Event::MouseMoved {
|
self.last_mouse_moved_event = Some(Event::MouseMoved {
|
||||||
position,
|
position,
|
||||||
left_mouse_down: true,
|
left_mouse_down: true,
|
||||||
|
@ -203,16 +347,92 @@ impl Presenter {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut event_cx = self.build_event_context(cx);
|
let mut event_cx = self.build_event_context(cx);
|
||||||
event_cx.dispatch_event(root_view_id, &event);
|
let mut handled = false;
|
||||||
|
for unhovered_region in unhovered_regions {
|
||||||
|
handled = true;
|
||||||
|
if let Some(hover_callback) = unhovered_region.hover {
|
||||||
|
event_cx.with_current_view(unhovered_region.view_id, |event_cx| {
|
||||||
|
hover_callback(false, event_cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let invalidated_views = event_cx.invalidated_views;
|
for hovered_region in hovered_regions {
|
||||||
|
handled = true;
|
||||||
|
if let Some(hover_callback) = hovered_region.hover {
|
||||||
|
event_cx.with_current_view(hovered_region.view_id, |event_cx| {
|
||||||
|
hover_callback(true, event_cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (handler, view_id, position) in mouse_down_out_handlers {
|
||||||
|
event_cx.with_current_view(view_id, |event_cx| handler(position, event_cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((mouse_down_region, position)) = mouse_down_region {
|
||||||
|
handled = true;
|
||||||
|
if let Some(mouse_down_callback) = mouse_down_region.mouse_down {
|
||||||
|
event_cx.with_current_view(mouse_down_region.view_id, |event_cx| {
|
||||||
|
mouse_down_callback(position, event_cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((clicked_region, position, click_count)) = clicked_region {
|
||||||
|
handled = true;
|
||||||
|
if let Some(click_callback) = clicked_region.click {
|
||||||
|
event_cx.with_current_view(clicked_region.view_id, |event_cx| {
|
||||||
|
click_callback(position, click_count, event_cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((right_mouse_down_region, position)) = right_mouse_down_region {
|
||||||
|
handled = true;
|
||||||
|
if let Some(right_mouse_down_callback) = right_mouse_down_region.right_mouse_down {
|
||||||
|
event_cx.with_current_view(right_mouse_down_region.view_id, |event_cx| {
|
||||||
|
right_mouse_down_callback(position, event_cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((right_clicked_region, position, click_count)) = right_clicked_region {
|
||||||
|
handled = true;
|
||||||
|
if let Some(right_click_callback) = right_clicked_region.right_click {
|
||||||
|
event_cx.with_current_view(right_clicked_region.view_id, |event_cx| {
|
||||||
|
right_click_callback(position, click_count, event_cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((dragged_region, delta)) = dragged_region {
|
||||||
|
handled = true;
|
||||||
|
if let Some(drag_callback) = dragged_region.drag {
|
||||||
|
event_cx.with_current_view(dragged_region.view_id, |event_cx| {
|
||||||
|
drag_callback(delta, event_cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !handled {
|
||||||
|
event_cx.dispatch_event(root_view_id, &event);
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidated_views.extend(event_cx.invalidated_views);
|
||||||
let dispatch_directives = event_cx.dispatched_actions;
|
let dispatch_directives = event_cx.dispatched_actions;
|
||||||
|
|
||||||
for view_id in invalidated_views {
|
for view_id in invalidated_views {
|
||||||
cx.notify_view(self.window_id, view_id);
|
cx.notify_view(self.window_id, view_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut dispatch_path = Vec::new();
|
||||||
for directive in dispatch_directives {
|
for directive in dispatch_directives {
|
||||||
cx.dispatch_action_any(self.window_id, &directive.path, directive.action.as_ref());
|
dispatch_path.clear();
|
||||||
|
if let Some(view_id) = directive.dispatcher_view_id {
|
||||||
|
self.compute_dispatch_path_from(view_id, &mut dispatch_path);
|
||||||
|
}
|
||||||
|
cx.dispatch_action_any(self.window_id, &dispatch_path, directive.action.as_ref());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,23 +470,37 @@ impl Presenter {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DispatchDirective {
|
pub struct DispatchDirective {
|
||||||
pub path: Vec<usize>,
|
pub dispatcher_view_id: Option<usize>,
|
||||||
pub action: Box<dyn Action>,
|
pub action: Box<dyn Action>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LayoutContext<'a> {
|
pub struct LayoutContext<'a> {
|
||||||
|
window_id: usize,
|
||||||
rendered_views: &'a mut HashMap<usize, ElementBox>,
|
rendered_views: &'a mut HashMap<usize, ElementBox>,
|
||||||
parents: &'a mut HashMap<usize, usize>,
|
parents: &'a mut HashMap<usize, usize>,
|
||||||
view_stack: Vec<usize>,
|
view_stack: Vec<usize>,
|
||||||
pub refreshing: bool,
|
|
||||||
pub font_cache: &'a Arc<FontCache>,
|
pub font_cache: &'a Arc<FontCache>,
|
||||||
pub font_system: Arc<dyn FontSystem>,
|
pub font_system: Arc<dyn FontSystem>,
|
||||||
pub text_layout_cache: &'a TextLayoutCache,
|
pub text_layout_cache: &'a TextLayoutCache,
|
||||||
pub asset_cache: &'a AssetCache,
|
pub asset_cache: &'a AssetCache,
|
||||||
pub app: &'a mut MutableAppContext,
|
pub app: &'a mut MutableAppContext,
|
||||||
|
pub refreshing: bool,
|
||||||
|
pub window_size: Vector2F,
|
||||||
|
titlebar_height: f32,
|
||||||
|
hovered_region_ids: HashSet<MouseRegionId>,
|
||||||
|
clicked_region_id: Option<MouseRegionId>,
|
||||||
|
right_clicked_region_id: Option<MouseRegionId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> LayoutContext<'a> {
|
impl<'a> LayoutContext<'a> {
|
||||||
|
pub(crate) fn keystrokes_for_action(
|
||||||
|
&self,
|
||||||
|
action: &dyn Action,
|
||||||
|
) -> Option<SmallVec<[Keystroke; 2]>> {
|
||||||
|
self.app
|
||||||
|
.keystrokes_for_action(self.window_id, &self.view_stack, action)
|
||||||
|
}
|
||||||
|
|
||||||
fn layout(&mut self, view_id: usize, constraint: SizeConstraint) -> Vector2F {
|
fn layout(&mut self, view_id: usize, constraint: SizeConstraint) -> Vector2F {
|
||||||
if let Some(parent_id) = self.view_stack.last() {
|
if let Some(parent_id) = self.view_stack.last() {
|
||||||
self.parents.insert(view_id, *parent_id);
|
self.parents.insert(view_id, *parent_id);
|
||||||
|
@ -278,6 +512,27 @@ impl<'a> LayoutContext<'a> {
|
||||||
self.view_stack.pop();
|
self.view_stack.pop();
|
||||||
size
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render<F, V, T>(&mut self, handle: &ViewHandle<V>, f: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut V, &mut RenderContext<V>) -> T,
|
||||||
|
V: View,
|
||||||
|
{
|
||||||
|
handle.update(self.app, |view, cx| {
|
||||||
|
let mut render_cx = RenderContext {
|
||||||
|
app: cx,
|
||||||
|
window_id: handle.window_id(),
|
||||||
|
view_id: handle.id(),
|
||||||
|
view_type: PhantomData,
|
||||||
|
titlebar_height: self.titlebar_height,
|
||||||
|
hovered_region_ids: self.hovered_region_ids.clone(),
|
||||||
|
clicked_region_id: self.clicked_region_id,
|
||||||
|
right_clicked_region_id: self.right_clicked_region_id,
|
||||||
|
refreshing: self.refreshing,
|
||||||
|
};
|
||||||
|
f(view, &mut render_cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Deref for LayoutContext<'a> {
|
impl<'a> Deref for LayoutContext<'a> {
|
||||||
|
@ -333,14 +588,9 @@ impl<'a> UpgradeViewHandle for LayoutContext<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ElementStateContext for LayoutContext<'a> {
|
|
||||||
fn current_view_id(&self) -> usize {
|
|
||||||
*self.view_stack.last().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PaintContext<'a> {
|
pub struct PaintContext<'a> {
|
||||||
rendered_views: &'a mut HashMap<usize, ElementBox>,
|
rendered_views: &'a mut HashMap<usize, ElementBox>,
|
||||||
|
view_stack: Vec<usize>,
|
||||||
pub scene: &'a mut Scene,
|
pub scene: &'a mut Scene,
|
||||||
pub font_cache: &'a FontCache,
|
pub font_cache: &'a FontCache,
|
||||||
pub text_layout_cache: &'a TextLayoutCache,
|
pub text_layout_cache: &'a TextLayoutCache,
|
||||||
|
@ -350,10 +600,16 @@ pub struct PaintContext<'a> {
|
||||||
impl<'a> PaintContext<'a> {
|
impl<'a> PaintContext<'a> {
|
||||||
fn paint(&mut self, view_id: usize, origin: Vector2F, visible_bounds: RectF) {
|
fn paint(&mut self, view_id: usize, origin: Vector2F, visible_bounds: RectF) {
|
||||||
if let Some(mut tree) = self.rendered_views.remove(&view_id) {
|
if let Some(mut tree) = self.rendered_views.remove(&view_id) {
|
||||||
|
self.view_stack.push(view_id);
|
||||||
tree.paint(origin, visible_bounds, self);
|
tree.paint(origin, visible_bounds, self);
|
||||||
self.rendered_views.insert(view_id, tree);
|
self.rendered_views.insert(view_id, tree);
|
||||||
|
self.view_stack.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn current_view_id(&self) -> usize {
|
||||||
|
*self.view_stack.last().unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Deref for PaintContext<'a> {
|
impl<'a> Deref for PaintContext<'a> {
|
||||||
|
@ -378,9 +634,8 @@ pub struct EventContext<'a> {
|
||||||
impl<'a> EventContext<'a> {
|
impl<'a> EventContext<'a> {
|
||||||
fn dispatch_event(&mut self, view_id: usize, event: &Event) -> bool {
|
fn dispatch_event(&mut self, view_id: usize, event: &Event) -> bool {
|
||||||
if let Some(mut element) = self.rendered_views.remove(&view_id) {
|
if let Some(mut element) = self.rendered_views.remove(&view_id) {
|
||||||
self.view_stack.push(view_id);
|
let result =
|
||||||
let result = element.dispatch_event(event, self);
|
self.with_current_view(view_id, |this| element.dispatch_event(event, this));
|
||||||
self.view_stack.pop();
|
|
||||||
self.rendered_views.insert(view_id, element);
|
self.rendered_views.insert(view_id, element);
|
||||||
result
|
result
|
||||||
} else {
|
} else {
|
||||||
|
@ -388,9 +643,19 @@ impl<'a> EventContext<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn with_current_view<F, T>(&mut self, view_id: usize, f: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Self) -> T,
|
||||||
|
{
|
||||||
|
self.view_stack.push(view_id);
|
||||||
|
let result = f(self);
|
||||||
|
self.view_stack.pop();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
pub fn dispatch_any_action(&mut self, action: Box<dyn Action>) {
|
pub fn dispatch_any_action(&mut self, action: Box<dyn Action>) {
|
||||||
self.dispatched_actions.push(DispatchDirective {
|
self.dispatched_actions.push(DispatchDirective {
|
||||||
path: self.view_stack.clone(),
|
dispatcher_view_id: self.view_stack.last().copied(),
|
||||||
action,
|
action,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -521,6 +786,15 @@ impl SizeConstraint {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for SizeConstraint {
|
||||||
|
fn default() -> Self {
|
||||||
|
SizeConstraint {
|
||||||
|
min: Vector2F::zero(),
|
||||||
|
max: Vector2F::splat(f32::INFINITY),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ToJson for SizeConstraint {
|
impl ToJson for SizeConstraint {
|
||||||
fn to_json(&self) -> serde_json::Value {
|
fn to_json(&self) -> serde_json::Value {
|
||||||
json!({
|
json!({
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{borrow::Cow, sync::Arc};
|
use std::{any::TypeId, borrow::Cow, rc::Rc, sync::Arc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
color::Color,
|
color::Color,
|
||||||
|
@ -8,7 +8,7 @@ use crate::{
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
json::ToJson,
|
json::ToJson,
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
ImageData,
|
EventContext, ImageData,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Scene {
|
pub struct Scene {
|
||||||
|
@ -20,6 +20,7 @@ pub struct Scene {
|
||||||
struct StackingContext {
|
struct StackingContext {
|
||||||
layers: Vec<Layer>,
|
layers: Vec<Layer>,
|
||||||
active_layer_stack: Vec<usize>,
|
active_layer_stack: Vec<usize>,
|
||||||
|
depth: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -33,7 +34,35 @@ pub struct Layer {
|
||||||
image_glyphs: Vec<ImageGlyph>,
|
image_glyphs: Vec<ImageGlyph>,
|
||||||
icons: Vec<Icon>,
|
icons: Vec<Icon>,
|
||||||
paths: Vec<Path>,
|
paths: Vec<Path>,
|
||||||
cursor_styles: Vec<(RectF, CursorStyle)>,
|
cursor_regions: Vec<CursorRegion>,
|
||||||
|
mouse_regions: Vec<MouseRegion>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct CursorRegion {
|
||||||
|
pub bounds: RectF,
|
||||||
|
pub style: CursorStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct MouseRegion {
|
||||||
|
pub view_id: usize,
|
||||||
|
pub discriminant: Option<(TypeId, usize)>,
|
||||||
|
pub bounds: RectF,
|
||||||
|
pub hover: Option<Rc<dyn Fn(bool, &mut EventContext)>>,
|
||||||
|
pub mouse_down: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||||
|
pub click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
|
||||||
|
pub right_mouse_down: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||||
|
pub right_click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
|
||||||
|
pub drag: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||||
|
pub mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||||
|
pub right_mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub struct MouseRegionId {
|
||||||
|
pub view_id: usize,
|
||||||
|
pub discriminant: (TypeId, usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
|
@ -159,7 +188,7 @@ pub struct Image {
|
||||||
|
|
||||||
impl Scene {
|
impl Scene {
|
||||||
pub fn new(scale_factor: f32) -> Self {
|
pub fn new(scale_factor: f32) -> Self {
|
||||||
let stacking_context = StackingContext::new(None);
|
let stacking_context = StackingContext::new(0, None);
|
||||||
Scene {
|
Scene {
|
||||||
scale_factor,
|
scale_factor,
|
||||||
stacking_contexts: vec![stacking_context],
|
stacking_contexts: vec![stacking_context],
|
||||||
|
@ -175,18 +204,32 @@ impl Scene {
|
||||||
self.stacking_contexts.iter().flat_map(|s| &s.layers)
|
self.stacking_contexts.iter().flat_map(|s| &s.layers)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cursor_styles(&self) -> Vec<(RectF, CursorStyle)> {
|
pub fn cursor_regions(&self) -> Vec<CursorRegion> {
|
||||||
self.layers()
|
self.layers()
|
||||||
.flat_map(|layer| &layer.cursor_styles)
|
.flat_map(|layer| &layer.cursor_regions)
|
||||||
.copied()
|
.copied()
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mouse_regions(&self) -> Vec<(MouseRegion, usize)> {
|
||||||
|
let mut regions = Vec::new();
|
||||||
|
for stacking_context in self.stacking_contexts.iter() {
|
||||||
|
for layer in &stacking_context.layers {
|
||||||
|
for mouse_region in &layer.mouse_regions {
|
||||||
|
regions.push((mouse_region.clone(), stacking_context.depth));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
regions.sort_by_key(|(_, depth)| *depth);
|
||||||
|
regions
|
||||||
|
}
|
||||||
|
|
||||||
pub fn push_stacking_context(&mut self, clip_bounds: Option<RectF>) {
|
pub fn push_stacking_context(&mut self, clip_bounds: Option<RectF>) {
|
||||||
|
let depth = self.active_stacking_context().depth + 1;
|
||||||
self.active_stacking_context_stack
|
self.active_stacking_context_stack
|
||||||
.push(self.stacking_contexts.len());
|
.push(self.stacking_contexts.len());
|
||||||
self.stacking_contexts
|
self.stacking_contexts
|
||||||
.push(StackingContext::new(clip_bounds))
|
.push(StackingContext::new(depth, clip_bounds))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pop_stacking_context(&mut self) {
|
pub fn pop_stacking_context(&mut self) {
|
||||||
|
@ -206,8 +249,12 @@ impl Scene {
|
||||||
self.active_layer().push_quad(quad)
|
self.active_layer().push_quad(quad)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_cursor_style(&mut self, bounds: RectF, style: CursorStyle) {
|
pub fn push_cursor_region(&mut self, region: CursorRegion) {
|
||||||
self.active_layer().push_cursor_style(bounds, style);
|
self.active_layer().push_cursor_region(region);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_mouse_region(&mut self, region: MouseRegion) {
|
||||||
|
self.active_layer().push_mouse_region(region);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_image(&mut self, image: Image) {
|
pub fn push_image(&mut self, image: Image) {
|
||||||
|
@ -249,10 +296,11 @@ impl Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StackingContext {
|
impl StackingContext {
|
||||||
fn new(clip_bounds: Option<RectF>) -> Self {
|
fn new(depth: usize, clip_bounds: Option<RectF>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
layers: vec![Layer::new(clip_bounds)],
|
layers: vec![Layer::new(clip_bounds)],
|
||||||
active_layer_stack: vec![0],
|
active_layer_stack: vec![0],
|
||||||
|
depth,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -298,7 +346,8 @@ impl Layer {
|
||||||
glyphs: Default::default(),
|
glyphs: Default::default(),
|
||||||
icons: Default::default(),
|
icons: Default::default(),
|
||||||
paths: Default::default(),
|
paths: Default::default(),
|
||||||
cursor_styles: Default::default(),
|
cursor_regions: Default::default(),
|
||||||
|
mouse_regions: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,10 +365,24 @@ impl Layer {
|
||||||
self.quads.as_slice()
|
self.quads.as_slice()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_cursor_style(&mut self, bounds: RectF, style: CursorStyle) {
|
fn push_cursor_region(&mut self, region: CursorRegion) {
|
||||||
if let Some(bounds) = bounds.intersection(self.clip_bounds.unwrap_or(bounds)) {
|
if let Some(bounds) = region
|
||||||
|
.bounds
|
||||||
|
.intersection(self.clip_bounds.unwrap_or(region.bounds))
|
||||||
|
{
|
||||||
if can_draw(bounds) {
|
if can_draw(bounds) {
|
||||||
self.cursor_styles.push((bounds, style));
|
self.cursor_regions.push(region);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_mouse_region(&mut self, region: MouseRegion) {
|
||||||
|
if let Some(bounds) = region
|
||||||
|
.bounds
|
||||||
|
.intersection(self.clip_bounds.unwrap_or(region.bounds))
|
||||||
|
{
|
||||||
|
if can_draw(bounds) {
|
||||||
|
self.mouse_regions.push(region);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -484,6 +547,15 @@ impl ToJson for Border {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MouseRegion {
|
||||||
|
pub fn id(&self) -> Option<MouseRegionId> {
|
||||||
|
self.discriminant.map(|discriminant| MouseRegionId {
|
||||||
|
view_id: self.view_id,
|
||||||
|
discriminant,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn can_draw(bounds: RectF) -> bool {
|
fn can_draw(bounds: RectF) -> bool {
|
||||||
let size = bounds.size();
|
let size = bounds.size();
|
||||||
size.x() > 0. && size.y() > 0.
|
size.x() > 0. && size.y() > 0.
|
||||||
|
|
|
@ -119,11 +119,10 @@ impl View for Select {
|
||||||
.with_style(style.header)
|
.with_style(style.header)
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.on_click(move |_, cx| cx.dispatch_action(ToggleSelect))
|
.on_click(move |_, _, cx| cx.dispatch_action(ToggleSelect))
|
||||||
.boxed(),
|
.boxed(),
|
||||||
);
|
);
|
||||||
if self.is_open {
|
if self.is_open {
|
||||||
let handle = self.handle.clone();
|
|
||||||
result.add_child(
|
result.add_child(
|
||||||
Overlay::new(
|
Overlay::new(
|
||||||
Container::new(
|
Container::new(
|
||||||
|
@ -131,9 +130,8 @@ impl View for Select {
|
||||||
UniformList::new(
|
UniformList::new(
|
||||||
self.list_state.clone(),
|
self.list_state.clone(),
|
||||||
self.item_count,
|
self.item_count,
|
||||||
move |mut range, items, cx| {
|
cx,
|
||||||
let handle = handle.upgrade(cx).unwrap();
|
move |this, mut range, items, cx| {
|
||||||
let this = handle.read(cx);
|
|
||||||
let selected_item_ix = this.selected_item_ix;
|
let selected_item_ix = this.selected_item_ix;
|
||||||
range.end = range.end.min(this.item_count);
|
range.end = range.end.min(this.item_count);
|
||||||
items.extend(range.map(|ix| {
|
items.extend(range.map(|ix| {
|
||||||
|
@ -141,7 +139,7 @@ impl View for Select {
|
||||||
ix,
|
ix,
|
||||||
cx,
|
cx,
|
||||||
|mouse_state, cx| {
|
|mouse_state, cx| {
|
||||||
(handle.read(cx).render_item)(
|
(this.render_item)(
|
||||||
ix,
|
ix,
|
||||||
if ix == selected_item_ix {
|
if ix == selected_item_ix {
|
||||||
ItemType::Selected
|
ItemType::Selected
|
||||||
|
@ -153,7 +151,9 @@ impl View for Select {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.on_click(move |_, cx| cx.dispatch_action(SelectItem(ix)))
|
.on_click(move |_, _, cx| {
|
||||||
|
cx.dispatch_action(SelectItem(ix))
|
||||||
|
})
|
||||||
.boxed()
|
.boxed()
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
11
crates/menu/Cargo.toml
Normal file
11
crates/menu/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "menu"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/menu.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gpui = { path = "../gpui" }
|
|
@ -4,8 +4,8 @@ use editor::{
|
||||||
};
|
};
|
||||||
use fuzzy::StringMatch;
|
use fuzzy::StringMatch;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, elements::*, geometry::vector::Vector2F, AppContext, Entity, MutableAppContext,
|
actions, elements::*, geometry::vector::Vector2F, AppContext, Entity, MouseState,
|
||||||
RenderContext, Task, View, ViewContext, ViewHandle,
|
MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use language::Outline;
|
use language::Outline;
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
|
@ -231,7 +231,7 @@ impl PickerDelegate for OutlineView {
|
||||||
fn render_match(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
mouse_state: &MouseState,
|
mouse_state: MouseState,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
|
|
|
@ -10,6 +10,7 @@ doctest = false
|
||||||
[dependencies]
|
[dependencies]
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
|
menu = { path = "../menu" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
|
|
|
@ -1,20 +1,18 @@
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::{
|
elements::{
|
||||||
ChildView, Flex, Label, MouseEventHandler, MouseState, ParentElement, ScrollTarget,
|
ChildView, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget, UniformList,
|
||||||
UniformList, UniformListState,
|
UniformListState,
|
||||||
},
|
},
|
||||||
geometry::vector::{vec2f, Vector2F},
|
geometry::vector::{vec2f, Vector2F},
|
||||||
keymap,
|
keymap,
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, Task, View,
|
AppContext, Axis, Element, ElementBox, Entity, MouseState, MutableAppContext, RenderContext,
|
||||||
ViewContext, ViewHandle, WeakViewHandle,
|
Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
|
use menu::{Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
use workspace::menu::{
|
|
||||||
Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Picker<D: PickerDelegate> {
|
pub struct Picker<D: PickerDelegate> {
|
||||||
delegate: WeakViewHandle<D>,
|
delegate: WeakViewHandle<D>,
|
||||||
|
@ -34,7 +32,7 @@ pub trait PickerDelegate: View {
|
||||||
fn render_match(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
state: &MouseState,
|
state: MouseState,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> ElementBox;
|
) -> ElementBox;
|
||||||
|
@ -54,6 +52,7 @@ impl<D: PickerDelegate> View for Picker<D> {
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
|
||||||
let settings = cx.global::<Settings>();
|
let settings = cx.global::<Settings>();
|
||||||
|
let container_style = settings.theme.picker.container;
|
||||||
let delegate = self.delegate.clone();
|
let delegate = self.delegate.clone();
|
||||||
let match_count = if let Some(delegate) = delegate.upgrade(cx.app) {
|
let match_count = if let Some(delegate) = delegate.upgrade(cx.app) {
|
||||||
delegate.read(cx).match_count()
|
delegate.read(cx).match_count()
|
||||||
|
@ -80,8 +79,9 @@ impl<D: PickerDelegate> View for Picker<D> {
|
||||||
UniformList::new(
|
UniformList::new(
|
||||||
self.list_state.clone(),
|
self.list_state.clone(),
|
||||||
match_count,
|
match_count,
|
||||||
move |mut range, items, cx| {
|
cx,
|
||||||
let delegate = delegate.upgrade(cx).unwrap();
|
move |this, mut range, items, cx| {
|
||||||
|
let delegate = this.delegate.upgrade(cx).unwrap();
|
||||||
let selected_ix = delegate.read(cx).selected_index();
|
let selected_ix = delegate.read(cx).selected_index();
|
||||||
range.end = cmp::min(range.end, delegate.read(cx).match_count());
|
range.end = cmp::min(range.end, delegate.read(cx).match_count());
|
||||||
items.extend(range.map(move |ix| {
|
items.extend(range.map(move |ix| {
|
||||||
|
@ -90,7 +90,7 @@ impl<D: PickerDelegate> View for Picker<D> {
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.render_match(ix, state, ix == selected_ix, cx)
|
.render_match(ix, state, ix == selected_ix, cx)
|
||||||
})
|
})
|
||||||
.on_mouse_down(move |cx| cx.dispatch_action(SelectIndex(ix)))
|
.on_mouse_down(move |_, cx| cx.dispatch_action(SelectIndex(ix)))
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.boxed()
|
.boxed()
|
||||||
}));
|
}));
|
||||||
|
@ -103,7 +103,7 @@ impl<D: PickerDelegate> View for Picker<D> {
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(settings.theme.picker.container)
|
.with_style(container_style)
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_max_width(self.max_size.x())
|
.with_max_width(self.max_size.x())
|
||||||
.with_max_height(self.max_size.y())
|
.with_max_height(self.max_size.y())
|
||||||
|
|
|
@ -15,6 +15,7 @@ use text::Rope;
|
||||||
pub trait Fs: Send + Sync {
|
pub trait Fs: Send + Sync {
|
||||||
async fn create_dir(&self, path: &Path) -> Result<()>;
|
async fn create_dir(&self, path: &Path) -> Result<()>;
|
||||||
async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
|
async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
|
||||||
|
async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
|
||||||
async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
|
async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
|
||||||
async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
|
async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
|
||||||
async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
|
async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
|
||||||
|
@ -44,6 +45,12 @@ pub struct CreateOptions {
|
||||||
pub ignore_if_exists: bool,
|
pub ignore_if_exists: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Default)]
|
||||||
|
pub struct CopyOptions {
|
||||||
|
pub overwrite: bool,
|
||||||
|
pub ignore_if_exists: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default)]
|
#[derive(Copy, Clone, Default)]
|
||||||
pub struct RenameOptions {
|
pub struct RenameOptions {
|
||||||
pub overwrite: bool,
|
pub overwrite: bool,
|
||||||
|
@ -84,6 +91,35 @@ impl Fs for RealFs {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
|
||||||
|
if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
|
||||||
|
if options.ignore_if_exists {
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
return Err(anyhow!("{target:?} already exists"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = smol::fs::metadata(source).await?;
|
||||||
|
let _ = smol::fs::remove_dir_all(target).await;
|
||||||
|
if metadata.is_dir() {
|
||||||
|
self.create_dir(target).await?;
|
||||||
|
let mut children = smol::fs::read_dir(source).await?;
|
||||||
|
while let Some(child) = children.next().await {
|
||||||
|
if let Ok(child) = child {
|
||||||
|
let child_source_path = child.path();
|
||||||
|
let child_target_path = target.join(child.file_name());
|
||||||
|
self.copy(&child_source_path, &child_target_path, options)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
smol::fs::copy(source, target).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
|
async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
|
||||||
if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
|
if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
|
||||||
if options.ignore_if_exists {
|
if options.ignore_if_exists {
|
||||||
|
@ -511,6 +547,40 @@ impl Fs for FakeFs {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
|
||||||
|
let source = normalize_path(source);
|
||||||
|
let target = normalize_path(target);
|
||||||
|
|
||||||
|
let mut state = self.state.lock().await;
|
||||||
|
state.validate_path(&source)?;
|
||||||
|
state.validate_path(&target)?;
|
||||||
|
|
||||||
|
if !options.overwrite && state.entries.contains_key(&target) {
|
||||||
|
if options.ignore_if_exists {
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
return Err(anyhow!("{target:?} already exists"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_entries = Vec::new();
|
||||||
|
for (path, entry) in &state.entries {
|
||||||
|
if let Ok(relative_path) = path.strip_prefix(&source) {
|
||||||
|
new_entries.push((relative_path.to_path_buf(), entry.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut events = Vec::new();
|
||||||
|
for (relative_path, entry) in new_entries {
|
||||||
|
let new_path = normalize_path(&target.join(relative_path));
|
||||||
|
events.push(new_path.clone());
|
||||||
|
state.entries.insert(new_path, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.emit_event(&events).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn remove_dir(&self, dir_path: &Path, options: RemoveOptions) -> Result<()> {
|
async fn remove_dir(&self, dir_path: &Path, options: RemoveOptions) -> Result<()> {
|
||||||
let dir_path = normalize_path(dir_path);
|
let dir_path = normalize_path(dir_path);
|
||||||
let mut state = self.state.lock().await;
|
let mut state = self.state.lock().await;
|
||||||
|
|
|
@ -282,6 +282,7 @@ impl Project {
|
||||||
client.add_model_message_handler(Self::handle_update_worktree);
|
client.add_model_message_handler(Self::handle_update_worktree);
|
||||||
client.add_model_request_handler(Self::handle_create_project_entry);
|
client.add_model_request_handler(Self::handle_create_project_entry);
|
||||||
client.add_model_request_handler(Self::handle_rename_project_entry);
|
client.add_model_request_handler(Self::handle_rename_project_entry);
|
||||||
|
client.add_model_request_handler(Self::handle_copy_project_entry);
|
||||||
client.add_model_request_handler(Self::handle_delete_project_entry);
|
client.add_model_request_handler(Self::handle_delete_project_entry);
|
||||||
client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
|
client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
|
||||||
client.add_model_request_handler(Self::handle_apply_code_action);
|
client.add_model_request_handler(Self::handle_apply_code_action);
|
||||||
|
@ -779,6 +780,49 @@ impl Project {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn copy_entry(
|
||||||
|
&mut self,
|
||||||
|
entry_id: ProjectEntryId,
|
||||||
|
new_path: impl Into<Arc<Path>>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Option<Task<Result<Entry>>> {
|
||||||
|
let worktree = self.worktree_for_entry(entry_id, cx)?;
|
||||||
|
let new_path = new_path.into();
|
||||||
|
if self.is_local() {
|
||||||
|
worktree.update(cx, |worktree, cx| {
|
||||||
|
worktree
|
||||||
|
.as_local_mut()
|
||||||
|
.unwrap()
|
||||||
|
.copy_entry(entry_id, new_path, cx)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let client = self.client.clone();
|
||||||
|
let project_id = self.remote_id().unwrap();
|
||||||
|
|
||||||
|
Some(cx.spawn_weak(|_, mut cx| async move {
|
||||||
|
let response = client
|
||||||
|
.request(proto::CopyProjectEntry {
|
||||||
|
project_id,
|
||||||
|
entry_id: entry_id.to_proto(),
|
||||||
|
new_path: new_path.as_os_str().as_bytes().to_vec(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let entry = response
|
||||||
|
.entry
|
||||||
|
.ok_or_else(|| anyhow!("missing entry in response"))?;
|
||||||
|
worktree
|
||||||
|
.update(&mut cx, |worktree, cx| {
|
||||||
|
worktree.as_remote().unwrap().insert_entry(
|
||||||
|
entry,
|
||||||
|
response.worktree_scan_id as usize,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn rename_entry(
|
pub fn rename_entry(
|
||||||
&mut self,
|
&mut self,
|
||||||
entry_id: ProjectEntryId,
|
entry_id: ProjectEntryId,
|
||||||
|
@ -4037,6 +4081,34 @@ impl Project {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_copy_project_entry(
|
||||||
|
this: ModelHandle<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::CopyProjectEntry>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<proto::ProjectEntryResponse> {
|
||||||
|
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
|
||||||
|
let worktree = this.read_with(&cx, |this, cx| {
|
||||||
|
this.worktree_for_entry(entry_id, cx)
|
||||||
|
.ok_or_else(|| anyhow!("worktree not found"))
|
||||||
|
})?;
|
||||||
|
let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id());
|
||||||
|
let entry = worktree
|
||||||
|
.update(&mut cx, |worktree, cx| {
|
||||||
|
let new_path = PathBuf::from(OsString::from_vec(envelope.payload.new_path));
|
||||||
|
worktree
|
||||||
|
.as_local_mut()
|
||||||
|
.unwrap()
|
||||||
|
.copy_entry(entry_id, new_path, cx)
|
||||||
|
.ok_or_else(|| anyhow!("invalid entry"))
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
Ok(proto::ProjectEntryResponse {
|
||||||
|
entry: Some((&entry).into()),
|
||||||
|
worktree_scan_id: worktree_scan_id as u64,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_delete_project_entry(
|
async fn handle_delete_project_entry(
|
||||||
this: ModelHandle<Self>,
|
this: ModelHandle<Self>,
|
||||||
envelope: TypedEnvelope<proto::DeleteProjectEntry>,
|
envelope: TypedEnvelope<proto::DeleteProjectEntry>,
|
||||||
|
|
|
@ -774,6 +774,46 @@ impl LocalWorktree {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn copy_entry(
|
||||||
|
&self,
|
||||||
|
entry_id: ProjectEntryId,
|
||||||
|
new_path: impl Into<Arc<Path>>,
|
||||||
|
cx: &mut ModelContext<Worktree>,
|
||||||
|
) -> Option<Task<Result<Entry>>> {
|
||||||
|
let old_path = self.entry_for_id(entry_id)?.path.clone();
|
||||||
|
let new_path = new_path.into();
|
||||||
|
let abs_old_path = self.absolutize(&old_path);
|
||||||
|
let abs_new_path = self.absolutize(&new_path);
|
||||||
|
let copy = cx.background().spawn({
|
||||||
|
let fs = self.fs.clone();
|
||||||
|
let abs_new_path = abs_new_path.clone();
|
||||||
|
async move {
|
||||||
|
fs.copy(&abs_old_path, &abs_new_path, Default::default())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(cx.spawn(|this, mut cx| async move {
|
||||||
|
copy.await?;
|
||||||
|
let entry = this
|
||||||
|
.update(&mut cx, |this, cx| {
|
||||||
|
this.as_local_mut().unwrap().refresh_entry(
|
||||||
|
new_path.clone(),
|
||||||
|
abs_new_path,
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.poll_snapshot(cx);
|
||||||
|
this.as_local().unwrap().broadcast_snapshot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
Ok(entry)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
fn write_entry_internal(
|
fn write_entry_internal(
|
||||||
&self,
|
&self,
|
||||||
path: impl Into<Arc<Path>>,
|
path: impl Into<Arc<Path>>,
|
||||||
|
@ -1162,8 +1202,23 @@ impl Snapshot {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_entry(&mut self, entry_id: ProjectEntryId) -> bool {
|
fn delete_entry(&mut self, entry_id: ProjectEntryId) -> bool {
|
||||||
if let Some(entry) = self.entries_by_id.remove(&entry_id, &()) {
|
if let Some(removed_entry) = self.entries_by_id.remove(&entry_id, &()) {
|
||||||
self.entries_by_path.remove(&PathKey(entry.path), &());
|
self.entries_by_path = {
|
||||||
|
let mut cursor = self.entries_by_path.cursor();
|
||||||
|
let mut new_entries_by_path =
|
||||||
|
cursor.slice(&TraversalTarget::Path(&removed_entry.path), Bias::Left, &());
|
||||||
|
while let Some(entry) = cursor.item() {
|
||||||
|
if entry.path.starts_with(&removed_entry.path) {
|
||||||
|
self.entries_by_id.remove(&entry.id, &());
|
||||||
|
cursor.next(&());
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new_entries_by_path.push_tree(cursor.suffix(&()), &());
|
||||||
|
new_entries_by_path
|
||||||
|
};
|
||||||
|
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
|
|
@ -8,8 +8,10 @@ path = "src/project_panel.rs"
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
context_menu = { path = "../context_menu" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
|
menu = { path = "../menu" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use context_menu::{ContextMenu, ContextMenuItem};
|
||||||
use editor::{Cancel, Editor};
|
use editor::{Cancel, Editor};
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -5,13 +6,15 @@ use gpui::{
|
||||||
anyhow::{anyhow, Result},
|
anyhow::{anyhow, Result},
|
||||||
elements::{
|
elements::{
|
||||||
ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement,
|
ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement,
|
||||||
ScrollTarget, Svg, UniformList, UniformListState,
|
ScrollTarget, Stack, Svg, UniformList, UniformListState,
|
||||||
},
|
},
|
||||||
|
geometry::vector::Vector2F,
|
||||||
impl_internal_actions, keymap,
|
impl_internal_actions, keymap,
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, Task,
|
AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MutableAppContext,
|
||||||
View, ViewContext, ViewHandle, WeakViewHandle,
|
PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
|
use menu::{Confirm, SelectNext, SelectPrev};
|
||||||
use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -19,12 +22,10 @@ use std::{
|
||||||
collections::{hash_map, HashMap},
|
collections::{hash_map, HashMap},
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
use unicase::UniCase;
|
use unicase::UniCase;
|
||||||
use workspace::{
|
use workspace::Workspace;
|
||||||
menu::{Confirm, SelectNext, SelectPrev},
|
|
||||||
Workspace,
|
|
||||||
};
|
|
||||||
|
|
||||||
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
|
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
|
||||||
|
|
||||||
|
@ -36,7 +37,8 @@ pub struct ProjectPanel {
|
||||||
selection: Option<Selection>,
|
selection: Option<Selection>,
|
||||||
edit_state: Option<EditState>,
|
edit_state: Option<EditState>,
|
||||||
filename_editor: ViewHandle<Editor>,
|
filename_editor: ViewHandle<Editor>,
|
||||||
handle: WeakViewHandle<Self>,
|
clipboard_entry: Option<ClipboardEntry>,
|
||||||
|
context_menu: ViewHandle<ContextMenu>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
|
@ -54,6 +56,18 @@ struct EditState {
|
||||||
processing_filename: Option<String>,
|
processing_filename: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub enum ClipboardEntry {
|
||||||
|
Copied {
|
||||||
|
worktree_id: WorktreeId,
|
||||||
|
entry_id: ProjectEntryId,
|
||||||
|
},
|
||||||
|
Cut {
|
||||||
|
worktree_id: WorktreeId,
|
||||||
|
entry_id: ProjectEntryId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
struct EntryDetails {
|
struct EntryDetails {
|
||||||
filename: String,
|
filename: String,
|
||||||
|
@ -64,6 +78,7 @@ struct EntryDetails {
|
||||||
is_selected: bool,
|
is_selected: bool,
|
||||||
is_editing: bool,
|
is_editing: bool,
|
||||||
is_processing: bool,
|
is_processing: bool,
|
||||||
|
is_cut: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -75,6 +90,12 @@ pub struct Open {
|
||||||
pub change_focus: bool,
|
pub change_focus: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DeployContextMenu {
|
||||||
|
pub position: Vector2F,
|
||||||
|
pub entry_id: Option<ProjectEntryId>,
|
||||||
|
}
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
project_panel,
|
project_panel,
|
||||||
[
|
[
|
||||||
|
@ -82,13 +103,18 @@ actions!(
|
||||||
CollapseSelectedEntry,
|
CollapseSelectedEntry,
|
||||||
AddDirectory,
|
AddDirectory,
|
||||||
AddFile,
|
AddFile,
|
||||||
|
Copy,
|
||||||
|
CopyPath,
|
||||||
|
Cut,
|
||||||
|
Paste,
|
||||||
Delete,
|
Delete,
|
||||||
Rename
|
Rename
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
impl_internal_actions!(project_panel, [Open, ToggleExpanded]);
|
impl_internal_actions!(project_panel, [Open, ToggleExpanded, DeployContextMenu]);
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
cx.add_action(ProjectPanel::deploy_context_menu);
|
||||||
cx.add_action(ProjectPanel::expand_selected_entry);
|
cx.add_action(ProjectPanel::expand_selected_entry);
|
||||||
cx.add_action(ProjectPanel::collapse_selected_entry);
|
cx.add_action(ProjectPanel::collapse_selected_entry);
|
||||||
cx.add_action(ProjectPanel::toggle_expanded);
|
cx.add_action(ProjectPanel::toggle_expanded);
|
||||||
|
@ -101,6 +127,14 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_async_action(ProjectPanel::delete);
|
cx.add_async_action(ProjectPanel::delete);
|
||||||
cx.add_async_action(ProjectPanel::confirm);
|
cx.add_async_action(ProjectPanel::confirm);
|
||||||
cx.add_action(ProjectPanel::cancel);
|
cx.add_action(ProjectPanel::cancel);
|
||||||
|
cx.add_action(ProjectPanel::copy);
|
||||||
|
cx.add_action(ProjectPanel::copy_path);
|
||||||
|
cx.add_action(ProjectPanel::cut);
|
||||||
|
cx.add_action(
|
||||||
|
|this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext<ProjectPanel>| {
|
||||||
|
this.paste(action, cx);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
|
@ -156,7 +190,8 @@ impl ProjectPanel {
|
||||||
selection: None,
|
selection: None,
|
||||||
edit_state: None,
|
edit_state: None,
|
||||||
filename_editor,
|
filename_editor,
|
||||||
handle: cx.weak_handle(),
|
clipboard_entry: None,
|
||||||
|
context_menu: cx.add_view(|cx| ContextMenu::new(cx)),
|
||||||
};
|
};
|
||||||
this.update_visible_entries(None, cx);
|
this.update_visible_entries(None, cx);
|
||||||
this
|
this
|
||||||
|
@ -195,6 +230,63 @@ impl ProjectPanel {
|
||||||
project_panel
|
project_panel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
|
||||||
|
let mut menu_entries = Vec::new();
|
||||||
|
|
||||||
|
if let Some(entry_id) = action.entry_id {
|
||||||
|
if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
|
||||||
|
self.selection = Some(Selection {
|
||||||
|
worktree_id,
|
||||||
|
entry_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
||||||
|
let is_root = Some(entry) == worktree.root_entry();
|
||||||
|
if !self.project.read(cx).is_remote() {
|
||||||
|
menu_entries.push(ContextMenuItem::item(
|
||||||
|
"Add Folder to Project",
|
||||||
|
workspace::AddFolderToProject,
|
||||||
|
));
|
||||||
|
if is_root {
|
||||||
|
menu_entries.push(ContextMenuItem::item(
|
||||||
|
"Remove Folder from Project",
|
||||||
|
workspace::RemoveFolderFromProject(worktree_id),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
menu_entries.push(ContextMenuItem::item("New File", AddFile));
|
||||||
|
menu_entries.push(ContextMenuItem::item("New Folder", AddDirectory));
|
||||||
|
menu_entries.push(ContextMenuItem::Separator);
|
||||||
|
menu_entries.push(ContextMenuItem::item("Copy", Copy));
|
||||||
|
menu_entries.push(ContextMenuItem::item("Copy Path", CopyPath));
|
||||||
|
menu_entries.push(ContextMenuItem::item("Cut", Cut));
|
||||||
|
if let Some(clipboard_entry) = self.clipboard_entry {
|
||||||
|
if clipboard_entry.worktree_id() == worktree.id() {
|
||||||
|
menu_entries.push(ContextMenuItem::item("Paste", Paste));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
menu_entries.push(ContextMenuItem::Separator);
|
||||||
|
menu_entries.push(ContextMenuItem::item("Rename", Rename));
|
||||||
|
if !is_root {
|
||||||
|
menu_entries.push(ContextMenuItem::item("Delete", Delete));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.selection.take();
|
||||||
|
menu_entries.push(ContextMenuItem::item(
|
||||||
|
"Add Folder to Project",
|
||||||
|
workspace::AddFolderToProject,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.context_menu.update(cx, |menu, cx| {
|
||||||
|
menu.show(action.position, menu_entries, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
|
fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
|
||||||
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
||||||
let expanded_dir_ids =
|
let expanded_dir_ids =
|
||||||
|
@ -541,6 +633,92 @@ impl ProjectPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
||||||
|
self.clipboard_entry = Some(ClipboardEntry::Cut {
|
||||||
|
worktree_id: worktree.id(),
|
||||||
|
entry_id: entry.id,
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
||||||
|
self.clipboard_entry = Some(ClipboardEntry::Copied {
|
||||||
|
worktree_id: worktree.id(),
|
||||||
|
entry_id: entry.id,
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) -> Option<()> {
|
||||||
|
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
||||||
|
let clipboard_entry = self.clipboard_entry?;
|
||||||
|
if clipboard_entry.worktree_id() != worktree.id() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let clipboard_entry_file_name = self
|
||||||
|
.project
|
||||||
|
.read(cx)
|
||||||
|
.path_for_entry(clipboard_entry.entry_id(), cx)?
|
||||||
|
.path
|
||||||
|
.file_name()?
|
||||||
|
.to_os_string();
|
||||||
|
|
||||||
|
let mut new_path = entry.path.to_path_buf();
|
||||||
|
if entry.is_file() {
|
||||||
|
new_path.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
new_path.push(&clipboard_entry_file_name);
|
||||||
|
let extension = new_path.extension().map(|e| e.to_os_string());
|
||||||
|
let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?;
|
||||||
|
let mut ix = 0;
|
||||||
|
while worktree.entry_for_path(&new_path).is_some() {
|
||||||
|
new_path.pop();
|
||||||
|
|
||||||
|
let mut new_file_name = file_name_without_extension.to_os_string();
|
||||||
|
new_file_name.push(" copy");
|
||||||
|
if ix > 0 {
|
||||||
|
new_file_name.push(format!(" {}", ix));
|
||||||
|
}
|
||||||
|
new_path.push(new_file_name);
|
||||||
|
if let Some(extension) = extension.as_ref() {
|
||||||
|
new_path.set_extension(&extension);
|
||||||
|
}
|
||||||
|
ix += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.clipboard_entry.take();
|
||||||
|
if clipboard_entry.is_cut() {
|
||||||
|
self.project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.rename_entry(clipboard_entry.entry_id(), new_path, cx)
|
||||||
|
})
|
||||||
|
.map(|task| task.detach_and_log_err(cx));
|
||||||
|
} else {
|
||||||
|
self.project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.copy_entry(clipboard_entry.entry_id(), new_path, cx)
|
||||||
|
})
|
||||||
|
.map(|task| task.detach_and_log_err(cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
||||||
|
let mut path = PathBuf::new();
|
||||||
|
path.push(worktree.root_name());
|
||||||
|
path.push(&entry.path);
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new(path.to_string_lossy().to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> {
|
fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> {
|
||||||
let mut worktree_index = 0;
|
let mut worktree_index = 0;
|
||||||
let mut entry_index = 0;
|
let mut entry_index = 0;
|
||||||
|
@ -706,8 +884,8 @@ impl ProjectPanel {
|
||||||
fn for_each_visible_entry(
|
fn for_each_visible_entry(
|
||||||
&self,
|
&self,
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
cx: &mut ViewContext<ProjectPanel>,
|
cx: &mut RenderContext<ProjectPanel>,
|
||||||
mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<ProjectPanel>),
|
mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut RenderContext<ProjectPanel>),
|
||||||
) {
|
) {
|
||||||
let mut ix = 0;
|
let mut ix = 0;
|
||||||
for (worktree_id, visible_worktree_entries) in &self.visible_entries {
|
for (worktree_id, visible_worktree_entries) in &self.visible_entries {
|
||||||
|
@ -747,6 +925,9 @@ impl ProjectPanel {
|
||||||
}),
|
}),
|
||||||
is_editing: false,
|
is_editing: false,
|
||||||
is_processing: false,
|
is_processing: false,
|
||||||
|
is_cut: self
|
||||||
|
.clipboard_entry
|
||||||
|
.map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
|
||||||
};
|
};
|
||||||
if let Some(edit_state) = &self.edit_state {
|
if let Some(edit_state) = &self.edit_state {
|
||||||
let is_edited_entry = if edit_state.is_new_entry {
|
let is_edited_entry = if edit_state.is_new_entry {
|
||||||
|
@ -780,7 +961,7 @@ impl ProjectPanel {
|
||||||
details: EntryDetails,
|
details: EntryDetails,
|
||||||
editor: &ViewHandle<Editor>,
|
editor: &ViewHandle<Editor>,
|
||||||
theme: &theme::ProjectPanel,
|
theme: &theme::ProjectPanel,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut RenderContext<Self>,
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
let kind = details.kind;
|
let kind = details.kind;
|
||||||
let show_editor = details.is_editing && !details.is_processing;
|
let show_editor = details.is_editing && !details.is_processing;
|
||||||
|
@ -791,6 +972,10 @@ impl ProjectPanel {
|
||||||
style.text.color.fade_out(theme.ignored_entry_fade);
|
style.text.color.fade_out(theme.ignored_entry_fade);
|
||||||
style.icon_color.fade_out(theme.ignored_entry_fade);
|
style.icon_color.fade_out(theme.ignored_entry_fade);
|
||||||
}
|
}
|
||||||
|
if details.is_cut {
|
||||||
|
style.text.color.fade_out(theme.cut_entry_fade);
|
||||||
|
style.icon_color.fade_out(theme.cut_entry_fade);
|
||||||
|
}
|
||||||
let row_container_style = if show_editor {
|
let row_container_style = if show_editor {
|
||||||
theme.filename_editor.container
|
theme.filename_editor.container
|
||||||
} else {
|
} else {
|
||||||
|
@ -841,7 +1026,7 @@ impl ProjectPanel {
|
||||||
.with_padding_left(padding)
|
.with_padding_left(padding)
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.on_click(move |click_count, cx| {
|
.on_click(move |_, click_count, cx| {
|
||||||
if kind == EntryKind::Dir {
|
if kind == EntryKind::Dir {
|
||||||
cx.dispatch_action(ToggleExpanded(entry_id))
|
cx.dispatch_action(ToggleExpanded(entry_id))
|
||||||
} else {
|
} else {
|
||||||
|
@ -851,6 +1036,12 @@ impl ProjectPanel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.on_right_mouse_down(move |position, cx| {
|
||||||
|
cx.dispatch_action(DeployContextMenu {
|
||||||
|
entry_id: Some(entry_id),
|
||||||
|
position,
|
||||||
|
})
|
||||||
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
@ -862,20 +1053,22 @@ impl View for ProjectPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
|
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
|
||||||
|
enum Tag {}
|
||||||
let theme = &cx.global::<Settings>().theme.project_panel;
|
let theme = &cx.global::<Settings>().theme.project_panel;
|
||||||
let mut container_style = theme.container;
|
let mut container_style = theme.container;
|
||||||
let padding = std::mem::take(&mut container_style.padding);
|
let padding = std::mem::take(&mut container_style.padding);
|
||||||
let handle = self.handle.clone();
|
Stack::new()
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::new::<Tag, _, _>(0, cx, |_, cx| {
|
||||||
UniformList::new(
|
UniformList::new(
|
||||||
self.list.clone(),
|
self.list.clone(),
|
||||||
self.visible_entries
|
self.visible_entries
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(_, worktree_entries)| worktree_entries.len())
|
.map(|(_, worktree_entries)| worktree_entries.len())
|
||||||
.sum(),
|
.sum(),
|
||||||
move |range, items, cx| {
|
cx,
|
||||||
|
move |this, range, items, cx| {
|
||||||
let theme = cx.global::<Settings>().theme.clone();
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
let this = handle.upgrade(cx).unwrap();
|
|
||||||
this.update(cx.app, |this, cx| {
|
|
||||||
this.for_each_visible_entry(range.clone(), cx, |id, details, cx| {
|
this.for_each_visible_entry(range.clone(), cx, |id, details, cx| {
|
||||||
items.push(Self::render_entry(
|
items.push(Self::render_entry(
|
||||||
id,
|
id,
|
||||||
|
@ -885,13 +1078,24 @@ impl View for ProjectPanel {
|
||||||
cx,
|
cx,
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
})
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.with_padding_top(padding.top)
|
.with_padding_top(padding.top)
|
||||||
.with_padding_bottom(padding.bottom)
|
.with_padding_bottom(padding.bottom)
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(container_style)
|
.with_style(container_style)
|
||||||
|
.expanded()
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.on_right_mouse_down(move |position, cx| {
|
||||||
|
cx.dispatch_action(DeployContextMenu {
|
||||||
|
entry_id: None,
|
||||||
|
position,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_child(ChildView::new(&self.context_menu).boxed())
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -912,6 +1116,27 @@ impl workspace::sidebar::SidebarItem for ProjectPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ClipboardEntry {
|
||||||
|
fn is_cut(&self) -> bool {
|
||||||
|
matches!(self, Self::Cut { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry_id(&self) -> ProjectEntryId {
|
||||||
|
match self {
|
||||||
|
ClipboardEntry::Copied { entry_id, .. } | ClipboardEntry::Cut { entry_id, .. } => {
|
||||||
|
*entry_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn worktree_id(&self) -> WorktreeId {
|
||||||
|
match self {
|
||||||
|
ClipboardEntry::Copied { worktree_id, .. }
|
||||||
|
| ClipboardEntry::Cut { worktree_id, .. } => *worktree_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -1343,7 +1568,7 @@ mod tests {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut project_entries = HashSet::new();
|
let mut project_entries = HashSet::new();
|
||||||
let mut has_editor = false;
|
let mut has_editor = false;
|
||||||
panel.update(cx, |panel, cx| {
|
cx.render(panel, |panel, cx| {
|
||||||
panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
|
panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
|
||||||
if details.is_editing {
|
if details.is_editing {
|
||||||
assert!(!has_editor, "duplicate editor entry");
|
assert!(!has_editor, "duplicate editor entry");
|
||||||
|
|
|
@ -3,8 +3,8 @@ use editor::{
|
||||||
};
|
};
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task,
|
actions, elements::*, AppContext, Entity, ModelHandle, MouseState, MutableAppContext,
|
||||||
View, ViewContext, ViewHandle,
|
RenderContext, Task, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
|
@ -221,7 +221,7 @@ impl PickerDelegate for ProjectSymbolsView {
|
||||||
fn render_match(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
mouse_state: &MouseState,
|
mouse_state: MouseState,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
|
|
|
@ -41,66 +41,67 @@ message Envelope {
|
||||||
|
|
||||||
CreateProjectEntry create_project_entry = 33;
|
CreateProjectEntry create_project_entry = 33;
|
||||||
RenameProjectEntry rename_project_entry = 34;
|
RenameProjectEntry rename_project_entry = 34;
|
||||||
DeleteProjectEntry delete_project_entry = 35;
|
CopyProjectEntry copy_project_entry = 35;
|
||||||
ProjectEntryResponse project_entry_response = 36;
|
DeleteProjectEntry delete_project_entry = 36;
|
||||||
|
ProjectEntryResponse project_entry_response = 37;
|
||||||
|
|
||||||
UpdateDiagnosticSummary update_diagnostic_summary = 37;
|
UpdateDiagnosticSummary update_diagnostic_summary = 38;
|
||||||
StartLanguageServer start_language_server = 38;
|
StartLanguageServer start_language_server = 39;
|
||||||
UpdateLanguageServer update_language_server = 39;
|
UpdateLanguageServer update_language_server = 40;
|
||||||
|
|
||||||
OpenBufferById open_buffer_by_id = 40;
|
OpenBufferById open_buffer_by_id = 41;
|
||||||
OpenBufferByPath open_buffer_by_path = 41;
|
OpenBufferByPath open_buffer_by_path = 42;
|
||||||
OpenBufferResponse open_buffer_response = 42;
|
OpenBufferResponse open_buffer_response = 43;
|
||||||
UpdateBuffer update_buffer = 43;
|
UpdateBuffer update_buffer = 44;
|
||||||
UpdateBufferFile update_buffer_file = 44;
|
UpdateBufferFile update_buffer_file = 45;
|
||||||
SaveBuffer save_buffer = 45;
|
SaveBuffer save_buffer = 46;
|
||||||
BufferSaved buffer_saved = 46;
|
BufferSaved buffer_saved = 47;
|
||||||
BufferReloaded buffer_reloaded = 47;
|
BufferReloaded buffer_reloaded = 48;
|
||||||
ReloadBuffers reload_buffers = 48;
|
ReloadBuffers reload_buffers = 49;
|
||||||
ReloadBuffersResponse reload_buffers_response = 49;
|
ReloadBuffersResponse reload_buffers_response = 50;
|
||||||
FormatBuffers format_buffers = 50;
|
FormatBuffers format_buffers = 51;
|
||||||
FormatBuffersResponse format_buffers_response = 51;
|
FormatBuffersResponse format_buffers_response = 52;
|
||||||
GetCompletions get_completions = 52;
|
GetCompletions get_completions = 53;
|
||||||
GetCompletionsResponse get_completions_response = 53;
|
GetCompletionsResponse get_completions_response = 54;
|
||||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 54;
|
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 55;
|
||||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 55;
|
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 56;
|
||||||
GetCodeActions get_code_actions = 56;
|
GetCodeActions get_code_actions = 57;
|
||||||
GetCodeActionsResponse get_code_actions_response = 57;
|
GetCodeActionsResponse get_code_actions_response = 58;
|
||||||
ApplyCodeAction apply_code_action = 58;
|
ApplyCodeAction apply_code_action = 59;
|
||||||
ApplyCodeActionResponse apply_code_action_response = 59;
|
ApplyCodeActionResponse apply_code_action_response = 60;
|
||||||
PrepareRename prepare_rename = 60;
|
PrepareRename prepare_rename = 61;
|
||||||
PrepareRenameResponse prepare_rename_response = 61;
|
PrepareRenameResponse prepare_rename_response = 62;
|
||||||
PerformRename perform_rename = 62;
|
PerformRename perform_rename = 63;
|
||||||
PerformRenameResponse perform_rename_response = 63;
|
PerformRenameResponse perform_rename_response = 64;
|
||||||
SearchProject search_project = 64;
|
SearchProject search_project = 65;
|
||||||
SearchProjectResponse search_project_response = 65;
|
SearchProjectResponse search_project_response = 66;
|
||||||
|
|
||||||
GetChannels get_channels = 66;
|
GetChannels get_channels = 67;
|
||||||
GetChannelsResponse get_channels_response = 67;
|
GetChannelsResponse get_channels_response = 68;
|
||||||
JoinChannel join_channel = 68;
|
JoinChannel join_channel = 69;
|
||||||
JoinChannelResponse join_channel_response = 69;
|
JoinChannelResponse join_channel_response = 70;
|
||||||
LeaveChannel leave_channel = 70;
|
LeaveChannel leave_channel = 71;
|
||||||
SendChannelMessage send_channel_message = 71;
|
SendChannelMessage send_channel_message = 72;
|
||||||
SendChannelMessageResponse send_channel_message_response = 72;
|
SendChannelMessageResponse send_channel_message_response = 73;
|
||||||
ChannelMessageSent channel_message_sent = 73;
|
ChannelMessageSent channel_message_sent = 74;
|
||||||
GetChannelMessages get_channel_messages = 74;
|
GetChannelMessages get_channel_messages = 75;
|
||||||
GetChannelMessagesResponse get_channel_messages_response = 75;
|
GetChannelMessagesResponse get_channel_messages_response = 76;
|
||||||
|
|
||||||
UpdateContacts update_contacts = 76;
|
UpdateContacts update_contacts = 77;
|
||||||
UpdateInviteInfo update_invite_info = 77;
|
UpdateInviteInfo update_invite_info = 78;
|
||||||
ShowContacts show_contacts = 78;
|
ShowContacts show_contacts = 79;
|
||||||
|
|
||||||
GetUsers get_users = 79;
|
GetUsers get_users = 80;
|
||||||
FuzzySearchUsers fuzzy_search_users = 80;
|
FuzzySearchUsers fuzzy_search_users = 81;
|
||||||
UsersResponse users_response = 81;
|
UsersResponse users_response = 82;
|
||||||
RequestContact request_contact = 82;
|
RequestContact request_contact = 83;
|
||||||
RespondToContactRequest respond_to_contact_request = 83;
|
RespondToContactRequest respond_to_contact_request = 84;
|
||||||
RemoveContact remove_contact = 84;
|
RemoveContact remove_contact = 85;
|
||||||
|
|
||||||
Follow follow = 85;
|
Follow follow = 86;
|
||||||
FollowResponse follow_response = 86;
|
FollowResponse follow_response = 87;
|
||||||
UpdateFollowers update_followers = 87;
|
UpdateFollowers update_followers = 88;
|
||||||
Unfollow unfollow = 88;
|
Unfollow unfollow = 89;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,6 +211,12 @@ message RenameProjectEntry {
|
||||||
bytes new_path = 3;
|
bytes new_path = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message CopyProjectEntry {
|
||||||
|
uint64 project_id = 1;
|
||||||
|
uint64 entry_id = 2;
|
||||||
|
bytes new_path = 3;
|
||||||
|
}
|
||||||
|
|
||||||
message DeleteProjectEntry {
|
message DeleteProjectEntry {
|
||||||
uint64 project_id = 1;
|
uint64 project_id = 1;
|
||||||
uint64 entry_id = 2;
|
uint64 entry_id = 2;
|
||||||
|
|
|
@ -84,6 +84,7 @@ messages!(
|
||||||
(BufferSaved, Foreground),
|
(BufferSaved, Foreground),
|
||||||
(RemoveContact, Foreground),
|
(RemoveContact, Foreground),
|
||||||
(ChannelMessageSent, Foreground),
|
(ChannelMessageSent, Foreground),
|
||||||
|
(CopyProjectEntry, Foreground),
|
||||||
(CreateProjectEntry, Foreground),
|
(CreateProjectEntry, Foreground),
|
||||||
(DeleteProjectEntry, Foreground),
|
(DeleteProjectEntry, Foreground),
|
||||||
(Error, Foreground),
|
(Error, Foreground),
|
||||||
|
@ -167,6 +168,7 @@ request_messages!(
|
||||||
ApplyCompletionAdditionalEdits,
|
ApplyCompletionAdditionalEdits,
|
||||||
ApplyCompletionAdditionalEditsResponse
|
ApplyCompletionAdditionalEditsResponse
|
||||||
),
|
),
|
||||||
|
(CopyProjectEntry, ProjectEntryResponse),
|
||||||
(CreateProjectEntry, ProjectEntryResponse),
|
(CreateProjectEntry, ProjectEntryResponse),
|
||||||
(DeleteProjectEntry, ProjectEntryResponse),
|
(DeleteProjectEntry, ProjectEntryResponse),
|
||||||
(Follow, FollowResponse),
|
(Follow, FollowResponse),
|
||||||
|
@ -211,8 +213,8 @@ entity_messages!(
|
||||||
ApplyCompletionAdditionalEdits,
|
ApplyCompletionAdditionalEdits,
|
||||||
BufferReloaded,
|
BufferReloaded,
|
||||||
BufferSaved,
|
BufferSaved,
|
||||||
|
CopyProjectEntry,
|
||||||
CreateProjectEntry,
|
CreateProjectEntry,
|
||||||
RenameProjectEntry,
|
|
||||||
DeleteProjectEntry,
|
DeleteProjectEntry,
|
||||||
Follow,
|
Follow,
|
||||||
FormatBuffers,
|
FormatBuffers,
|
||||||
|
@ -233,6 +235,7 @@ entity_messages!(
|
||||||
ProjectUnshared,
|
ProjectUnshared,
|
||||||
ReloadBuffers,
|
ReloadBuffers,
|
||||||
RemoveProjectCollaborator,
|
RemoveProjectCollaborator,
|
||||||
|
RenameProjectEntry,
|
||||||
RequestJoinProject,
|
RequestJoinProject,
|
||||||
SaveBuffer,
|
SaveBuffer,
|
||||||
SearchProject,
|
SearchProject,
|
||||||
|
|
|
@ -6,4 +6,4 @@ pub use conn::Connection;
|
||||||
pub use peer::*;
|
pub use peer::*;
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
pub const PROTOCOL_VERSION: u32 = 20;
|
pub const PROTOCOL_VERSION: u32 = 21;
|
||||||
|
|
|
@ -12,6 +12,7 @@ collections = { path = "../collections" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
|
menu = { path = "../menu" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
|
|
|
@ -290,7 +290,7 @@ impl BufferSearchBar {
|
||||||
.with_style(style.container)
|
.with_style(style.container)
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.on_click(move |_, cx| cx.dispatch_action(ToggleSearchOption(search_option)))
|
.on_click(move |_, _, cx| cx.dispatch_action(ToggleSearchOption(search_option)))
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
@ -314,7 +314,7 @@ impl BufferSearchBar {
|
||||||
.with_style(style.container)
|
.with_style(style.container)
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.on_click(move |_, cx| match direction {
|
.on_click(move |_, _, cx| match direction {
|
||||||
Direction::Prev => cx.dispatch_action(SelectPrevMatch),
|
Direction::Prev => cx.dispatch_action(SelectPrevMatch),
|
||||||
Direction::Next => cx.dispatch_action(SelectNextMatch),
|
Direction::Next => cx.dispatch_action(SelectNextMatch),
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,6 +9,7 @@ use gpui::{
|
||||||
ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext,
|
ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext,
|
||||||
ViewHandle, WeakModelHandle, WeakViewHandle,
|
ViewHandle, WeakModelHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
|
use menu::Confirm;
|
||||||
use project::{search::SearchQuery, Project};
|
use project::{search::SearchQuery, Project};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
@ -19,8 +20,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
menu::Confirm, Item, ItemHandle, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView,
|
Item, ItemHandle, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||||
Workspace,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
actions!(project_search, [Deploy, SearchInNew, ToggleFocus]);
|
actions!(project_search, [Deploy, SearchInNew, ToggleFocus]);
|
||||||
|
@ -672,7 +672,7 @@ impl ProjectSearchBar {
|
||||||
.with_style(style.container)
|
.with_style(style.container)
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.on_click(move |_, cx| match direction {
|
.on_click(move |_, _, cx| match direction {
|
||||||
Direction::Prev => cx.dispatch_action(SelectPrevMatch),
|
Direction::Prev => cx.dispatch_action(SelectPrevMatch),
|
||||||
Direction::Next => cx.dispatch_action(SelectNextMatch),
|
Direction::Next => cx.dispatch_action(SelectNextMatch),
|
||||||
})
|
})
|
||||||
|
@ -699,7 +699,7 @@ impl ProjectSearchBar {
|
||||||
.with_style(style.container)
|
.with_style(style.container)
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.on_click(move |_, cx| cx.dispatch_action(ToggleSearchOption(option)))
|
.on_click(move |_, _, cx| cx.dispatch_action(ToggleSearchOption(option)))
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,9 @@ mod theme_registry;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
color::Color,
|
color::Color,
|
||||||
elements::{ContainerStyle, ImageStyle, LabelStyle, MouseState},
|
elements::{ContainerStyle, ImageStyle, LabelStyle},
|
||||||
fonts::{HighlightStyle, TextStyle},
|
fonts::{HighlightStyle, TextStyle},
|
||||||
Border,
|
Border, MouseState,
|
||||||
};
|
};
|
||||||
use serde::{de::DeserializeOwned, Deserialize};
|
use serde::{de::DeserializeOwned, Deserialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
@ -19,6 +19,7 @@ pub struct Theme {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub workspace: Workspace,
|
pub workspace: Workspace,
|
||||||
|
pub context_menu: ContextMenu,
|
||||||
pub chat_panel: ChatPanel,
|
pub chat_panel: ChatPanel,
|
||||||
pub contacts_panel: ContactsPanel,
|
pub contacts_panel: ContactsPanel,
|
||||||
pub contact_finder: ContactFinder,
|
pub contact_finder: ContactFinder,
|
||||||
|
@ -223,6 +224,7 @@ pub struct ProjectPanel {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub container: ContainerStyle,
|
pub container: ContainerStyle,
|
||||||
pub entry: Interactive<ProjectPanelEntry>,
|
pub entry: Interactive<ProjectPanelEntry>,
|
||||||
|
pub cut_entry_fade: f32,
|
||||||
pub ignored_entry_fade: f32,
|
pub ignored_entry_fade: f32,
|
||||||
pub filename_editor: FieldEditor,
|
pub filename_editor: FieldEditor,
|
||||||
pub indent_width: f32,
|
pub indent_width: f32,
|
||||||
|
@ -239,6 +241,22 @@ pub struct ProjectPanelEntry {
|
||||||
pub icon_spacing: f32,
|
pub icon_spacing: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Default)]
|
||||||
|
pub struct ContextMenu {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub container: ContainerStyle,
|
||||||
|
pub item: Interactive<ContextMenuItem>,
|
||||||
|
pub separator: ContainerStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Default)]
|
||||||
|
pub struct ContextMenuItem {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub container: ContainerStyle,
|
||||||
|
pub label: TextStyle,
|
||||||
|
pub keystroke: ContainedText,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Default)]
|
#[derive(Debug, Deserialize, Default)]
|
||||||
pub struct CommandPalette {
|
pub struct CommandPalette {
|
||||||
pub key: Interactive<ContainedLabel>,
|
pub key: Interactive<ContainedLabel>,
|
||||||
|
@ -488,7 +506,7 @@ pub struct Interactive<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Interactive<T> {
|
impl<T> Interactive<T> {
|
||||||
pub fn style_for(&self, state: &MouseState, active: bool) -> &T {
|
pub fn style_for(&self, state: MouseState, active: bool) -> &T {
|
||||||
if active {
|
if active {
|
||||||
if state.hovered {
|
if state.hovered {
|
||||||
self.active_hover
|
self.active_hover
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, elements::*, AppContext, Element, ElementBox, Entity, MutableAppContext,
|
actions, elements::*, AppContext, Element, ElementBox, Entity, MouseState, MutableAppContext,
|
||||||
RenderContext, View, ViewContext, ViewHandle,
|
RenderContext, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
|
@ -213,7 +213,7 @@ impl PickerDelegate for ThemeSelector {
|
||||||
fn render_match(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
mouse_state: &MouseState,
|
mouse_state: MouseState,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
|
|
|
@ -168,7 +168,8 @@ impl View for LspStatus {
|
||||||
self.failed.join(", "),
|
self.failed.join(", "),
|
||||||
if self.failed.len() > 1 { "s" } else { "" }
|
if self.failed.len() > 1 { "s" } else { "" }
|
||||||
);
|
);
|
||||||
handler = Some(|_, cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage));
|
handler =
|
||||||
|
Some(|_, _, cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage));
|
||||||
} else {
|
} else {
|
||||||
return Empty::new().boxed();
|
return Empty::new().boxed();
|
||||||
}
|
}
|
||||||
|
|
|
@ -702,6 +702,7 @@ impl Pane {
|
||||||
let theme = cx.global::<Settings>().theme.clone();
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
|
|
||||||
enum Tabs {}
|
enum Tabs {}
|
||||||
|
enum Tab {}
|
||||||
let pane = cx.handle();
|
let pane = cx.handle();
|
||||||
let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
|
let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
|
||||||
let autoscroll = if mem::take(&mut self.autoscroll) {
|
let autoscroll = if mem::take(&mut self.autoscroll) {
|
||||||
|
@ -730,7 +731,7 @@ impl Pane {
|
||||||
style.container.border.left = false;
|
style.container.border.left = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
EventHandler::new(
|
MouseEventHandler::new::<Tab, _, _>(ix, cx, |_, cx| {
|
||||||
Container::new(
|
Container::new(
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
|
@ -801,7 +802,7 @@ impl Pane {
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click({
|
.on_click({
|
||||||
let pane = pane.clone();
|
let pane = pane.clone();
|
||||||
move |_, cx| {
|
move |_, _, cx| {
|
||||||
cx.dispatch_action(CloseItem {
|
cx.dispatch_action(CloseItem {
|
||||||
item_id,
|
item_id,
|
||||||
pane: pane.clone(),
|
pane: pane.clone(),
|
||||||
|
@ -820,11 +821,10 @@ impl Pane {
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.with_style(style.container)
|
.with_style(style.container)
|
||||||
.boxed(),
|
.boxed()
|
||||||
)
|
})
|
||||||
.on_mouse_down(move |cx| {
|
.on_mouse_down(move |_, cx| {
|
||||||
cx.dispatch_action(ActivateItem(ix));
|
cx.dispatch_action(ActivateItem(ix));
|
||||||
true
|
|
||||||
})
|
})
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
|
|
|
@ -165,6 +165,7 @@ impl Sidebar {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::ResizeLeftRight)
|
.with_cursor_style(CursorStyle::ResizeLeftRight)
|
||||||
|
.on_mouse_down(|_, _| {}) // This prevents the mouse down event from being propagated elsewhere
|
||||||
.on_drag(move |delta, cx| {
|
.on_drag(move |delta, cx| {
|
||||||
let prev_width = *actual_width.borrow();
|
let prev_width = *actual_width.borrow();
|
||||||
*custom_width.borrow_mut() = 0f32
|
*custom_width.borrow_mut() = 0f32
|
||||||
|
@ -293,7 +294,7 @@ impl View for SidebarButtons {
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(move |_, cx| {
|
.on_click(move |_, _, cx| {
|
||||||
cx.dispatch_action(ToggleSidebarItem {
|
cx.dispatch_action(ToggleSidebarItem {
|
||||||
side,
|
side,
|
||||||
item_index: ix,
|
item_index: ix,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
pub mod lsp_status;
|
pub mod lsp_status;
|
||||||
pub mod menu;
|
|
||||||
pub mod pane;
|
pub mod pane;
|
||||||
pub mod pane_group;
|
pub mod pane_group;
|
||||||
pub mod sidebar;
|
pub mod sidebar;
|
||||||
|
@ -30,7 +29,7 @@ use log::error;
|
||||||
pub use pane::*;
|
pub use pane::*;
|
||||||
pub use pane_group::*;
|
pub use pane_group::*;
|
||||||
use postage::prelude::Stream;
|
use postage::prelude::Stream;
|
||||||
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree};
|
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem, ToggleSidebarItemFocus};
|
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem, ToggleSidebarItemFocus};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
@ -73,6 +72,9 @@ type FollowableItemBuilders = HashMap<
|
||||||
),
|
),
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RemoveFolderFromProject(pub WorktreeId);
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
workspace,
|
workspace,
|
||||||
[
|
[
|
||||||
|
@ -105,7 +107,15 @@ pub struct JoinProject {
|
||||||
pub project_index: usize,
|
pub project_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_internal_actions!(workspace, [OpenPaths, ToggleFollow, JoinProject]);
|
impl_internal_actions!(
|
||||||
|
workspace,
|
||||||
|
[
|
||||||
|
OpenPaths,
|
||||||
|
ToggleFollow,
|
||||||
|
JoinProject,
|
||||||
|
RemoveFolderFromProject
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
pane::init(cx);
|
pane::init(cx);
|
||||||
|
@ -149,6 +159,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
cx.add_async_action(Workspace::close);
|
cx.add_async_action(Workspace::close);
|
||||||
cx.add_async_action(Workspace::save_all);
|
cx.add_async_action(Workspace::save_all);
|
||||||
cx.add_action(Workspace::add_folder_to_project);
|
cx.add_action(Workspace::add_folder_to_project);
|
||||||
|
cx.add_action(Workspace::remove_folder_from_project);
|
||||||
cx.add_action(
|
cx.add_action(
|
||||||
|workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
|
|workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
|
||||||
let pane = workspace.active_pane().clone();
|
let pane = workspace.active_pane().clone();
|
||||||
|
@ -1034,6 +1045,15 @@ impl Workspace {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remove_folder_from_project(
|
||||||
|
&mut self,
|
||||||
|
RemoveFolderFromProject(worktree_id): &RemoveFolderFromProject,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
self.project
|
||||||
|
.update(cx, |project, cx| project.remove_worktree(*worktree_id, cx));
|
||||||
|
}
|
||||||
|
|
||||||
fn project_path_for_path(
|
fn project_path_for_path(
|
||||||
&self,
|
&self,
|
||||||
abs_path: &Path,
|
abs_path: &Path,
|
||||||
|
@ -1777,7 +1797,7 @@ impl Workspace {
|
||||||
.with_style(style.container)
|
.with_style(style.container)
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.on_click(|_, cx| cx.dispatch_action(Authenticate))
|
.on_click(|_, _, cx| cx.dispatch_action(Authenticate))
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.aligned()
|
.aligned()
|
||||||
.boxed(),
|
.boxed(),
|
||||||
|
@ -1828,7 +1848,7 @@ impl Workspace {
|
||||||
if let Some(peer_id) = peer_id {
|
if let Some(peer_id) = peer_id {
|
||||||
MouseEventHandler::new::<ToggleFollow, _, _>(replica_id.into(), cx, move |_, _| content)
|
MouseEventHandler::new::<ToggleFollow, _, _>(replica_id.into(), cx, move |_, _| content)
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(move |_, cx| cx.dispatch_action(ToggleFollow(peer_id)))
|
.on_click(move |_, _, cx| cx.dispatch_action(ToggleFollow(peer_id)))
|
||||||
.boxed()
|
.boxed()
|
||||||
} else {
|
} else {
|
||||||
content
|
content
|
||||||
|
|
|
@ -22,6 +22,7 @@ chat_panel = { path = "../chat_panel" }
|
||||||
cli = { path = "../cli" }
|
cli = { path = "../cli" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
command_palette = { path = "../command_palette" }
|
command_palette = { path = "../command_palette" }
|
||||||
|
context_menu = { path = "../context_menu" }
|
||||||
client = { path = "../client" }
|
client = { path = "../client" }
|
||||||
clock = { path = "../clock" }
|
clock = { path = "../clock" }
|
||||||
contacts_panel = { path = "../contacts_panel" }
|
contacts_panel = { path = "../contacts_panel" }
|
||||||
|
|
|
@ -134,6 +134,7 @@ fn main() {
|
||||||
let mut languages = languages::build_language_registry(login_shell_env_loaded);
|
let mut languages = languages::build_language_registry(login_shell_env_loaded);
|
||||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
|
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
|
||||||
|
|
||||||
|
context_menu::init(cx);
|
||||||
auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
|
auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
|
||||||
project::Project::init(&client);
|
project::Project::init(&client);
|
||||||
client::Channel::init(&client);
|
client::Channel::init(&client);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import projectPanel from "./projectPanel";
|
||||||
import search from "./search";
|
import search from "./search";
|
||||||
import picker from "./picker";
|
import picker from "./picker";
|
||||||
import workspace from "./workspace";
|
import workspace from "./workspace";
|
||||||
|
import contextMenu from "./contextMenu";
|
||||||
import projectDiagnostics from "./projectDiagnostics";
|
import projectDiagnostics from "./projectDiagnostics";
|
||||||
import contactNotification from "./contactNotification";
|
import contactNotification from "./contactNotification";
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ export default function app(theme: Theme): Object {
|
||||||
return {
|
return {
|
||||||
picker: picker(theme),
|
picker: picker(theme),
|
||||||
workspace: workspace(theme),
|
workspace: workspace(theme),
|
||||||
|
contextMenu: contextMenu(theme),
|
||||||
editor: editor(theme),
|
editor: editor(theme),
|
||||||
projectDiagnostics: projectDiagnostics(theme),
|
projectDiagnostics: projectDiagnostics(theme),
|
||||||
commandPalette: commandPalette(theme),
|
commandPalette: commandPalette(theme),
|
||||||
|
|
36
styles/src/styleTree/contextMenu.ts
Normal file
36
styles/src/styleTree/contextMenu.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import Theme from "../themes/common/theme";
|
||||||
|
import { backgroundColor, borderColor, shadow, text } from "./components";
|
||||||
|
|
||||||
|
export default function contextMenu(theme: Theme) {
|
||||||
|
return {
|
||||||
|
background: backgroundColor(theme, 300, "base"),
|
||||||
|
cornerRadius: 6,
|
||||||
|
padding: 6,
|
||||||
|
shadow: shadow(theme),
|
||||||
|
item: {
|
||||||
|
padding: { left: 4, right: 4, top: 2, bottom: 2 },
|
||||||
|
cornerRadius: 6,
|
||||||
|
label: text(theme, "sans", "secondary", { size: "sm" }),
|
||||||
|
keystroke: {
|
||||||
|
margin: { left: 60 },
|
||||||
|
...text(theme, "sans", "muted", { size: "sm", weight: "bold" })
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
background: backgroundColor(theme, 300, "hovered"),
|
||||||
|
text: text(theme, "sans", "primary", { size: "sm" }),
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
background: backgroundColor(theme, 300, "active"),
|
||||||
|
text: text(theme, "sans", "primary", { size: "sm" }),
|
||||||
|
},
|
||||||
|
activeHover: {
|
||||||
|
background: backgroundColor(theme, 300, "hovered"),
|
||||||
|
text: text(theme, "sans", "active", { size: "sm" }),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
background: borderColor(theme, "primary"),
|
||||||
|
margin: { top: 2, bottom: 2 }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ export default function projectPanel(theme: Theme) {
|
||||||
text: text(theme, "mono", "active", { size: "sm" }),
|
text: text(theme, "mono", "active", { size: "sm" }),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
cutEntryFade: 0.4,
|
||||||
ignoredEntryFade: 0.6,
|
ignoredEntryFade: 0.6,
|
||||||
filenameEditor: {
|
filenameEditor: {
|
||||||
background: backgroundColor(theme, 500, "active"),
|
background: backgroundColor(theme, 500, "active"),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue