Merge branch 'main' into terminal-hyperlinks
This commit is contained in:
commit
a686a9f1d2
102 changed files with 5848 additions and 1966 deletions
103
Cargo.lock
generated
103
Cargo.lock
generated
|
@ -663,9 +663,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
|||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.1.0"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
|
||||
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
|
||||
|
||||
[[package]]
|
||||
name = "bzip2-sys"
|
||||
|
@ -750,6 +750,34 @@ dependencies = [
|
|||
"winx",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "capture"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bindgen",
|
||||
"block",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"cocoa",
|
||||
"core-foundation",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"futures",
|
||||
"gpui",
|
||||
"hmac 0.12.1",
|
||||
"jwt",
|
||||
"live_kit",
|
||||
"log",
|
||||
"media",
|
||||
"objc",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"serde",
|
||||
"sha2 0.10.2",
|
||||
"simplelog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.1.2"
|
||||
|
@ -1098,6 +1126,30 @@ dependencies = [
|
|||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "contacts_status_item"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"editor",
|
||||
"futures",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
"picker",
|
||||
"postage",
|
||||
"project",
|
||||
"serde",
|
||||
"settings",
|
||||
"theme",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "context_menu"
|
||||
version = "0.1.0"
|
||||
|
@ -2213,6 +2265,7 @@ dependencies = [
|
|||
"image",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"media",
|
||||
"metal",
|
||||
"num_cpus",
|
||||
"objc",
|
||||
|
@ -2722,6 +2775,21 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41ee439ee368ba4a77ac70d04f14015415af8600d6c894dc1f11bd79758c57d5"
|
||||
|
||||
[[package]]
|
||||
name = "jwt"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"crypto-common",
|
||||
"digest 0.10.3",
|
||||
"hmac 0.12.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kernel32-sys"
|
||||
version = "0.2.2"
|
||||
|
@ -2892,6 +2960,20 @@ dependencies = [
|
|||
"rand_chacha 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "live_kit"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"core-foundation",
|
||||
"core-graphics",
|
||||
"futures",
|
||||
"media",
|
||||
"parking_lot 0.11.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.7"
|
||||
|
@ -3008,6 +3090,20 @@ dependencies = [
|
|||
"digest 0.10.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "media"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bindgen",
|
||||
"block",
|
||||
"bytes",
|
||||
"core-foundation",
|
||||
"foreign-types",
|
||||
"metal",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
|
@ -7033,7 +7129,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.53.1"
|
||||
version = "0.54.1"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
@ -7052,6 +7148,7 @@ dependencies = [
|
|||
"collections",
|
||||
"command_palette",
|
||||
"contacts_panel",
|
||||
"contacts_status_item",
|
||||
"context_menu",
|
||||
"ctor",
|
||||
"diagnostics",
|
||||
|
|
4
assets/icons/zed_22.svg
Normal file
4
assets/icons/zed_22.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 11C5 14.3137 7.68629 17 11 17C14.3137 17 17 14.3137 17 11C17 7.68629 14.3137 5 11 5C7.68629 5 5 7.68629 5 11ZM11 3C6.58172 3 3 6.58172 3 11C3 15.4183 6.58172 19 11 19C15.4183 19 19 15.4183 19 11C19 6.58172 15.4183 3 11 3Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.09092 8.09088H14.6364L10.5511 12.4545H12.4546L13.9091 13.9091H7.36365L11.7273 9.54543H9.54547L8.09092 8.09088Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 571 B |
|
@ -309,8 +309,7 @@
|
|||
"cmd-shift-p": "command_palette::Toggle",
|
||||
"cmd-shift-m": "diagnostics::Deploy",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-alt-s": "workspace::SaveAll",
|
||||
"shift-escape": "terminal::DeployModal"
|
||||
"cmd-alt-s": "workspace::SaveAll"
|
||||
}
|
||||
},
|
||||
// Bindings from Sublime Text
|
||||
|
@ -394,10 +393,24 @@
|
|||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"shift-escape": "dock::FocusDock",
|
||||
"cmd-shift-c": "contacts_panel::ToggleFocus",
|
||||
"cmd-shift-b": "workspace::ToggleRightSidebar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-shift-k cmd-shift-right": "dock::AnchorDockRight",
|
||||
"cmd-shift-k cmd-shift-down": "dock::AnchorDockBottom",
|
||||
"cmd-shift-k cmd-shift-up": "dock::ExpandDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {
|
||||
"shift-escape": "dock::HideDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
|
@ -426,12 +439,5 @@
|
|||
"cmd-v": "terminal::Paste",
|
||||
"cmd-k": "terminal::Clear"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ModalTerminal",
|
||||
"bindings": {
|
||||
"ctrl-cmd-space": "terminal::ShowCharacterPalette",
|
||||
"shift-escape": "terminal::DeployModal"
|
||||
}
|
||||
}
|
||||
]
|
|
@ -32,6 +32,16 @@
|
|||
// 4. Save when idle for a certain amount of time:
|
||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||
"autosave": "off",
|
||||
// Where to place the dock by default. This setting can take three
|
||||
// values:
|
||||
//
|
||||
// 1. Position the dock attached to the bottom of the workspace
|
||||
// "default_dock_anchor": "bottom"
|
||||
// 2. Position the dock to the right of the workspace like a side panel
|
||||
// "default_dock_anchor": "right"
|
||||
// 3. Position the dock full screen over the entire workspace"
|
||||
// "default_dock_anchor": "expanded"
|
||||
"default_dock_anchor": "right",
|
||||
// How to auto-format modified buffers when saving them. This
|
||||
// setting can take three values:
|
||||
//
|
||||
|
|
|
@ -278,7 +278,7 @@ impl View for ActivityIndicator {
|
|||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let (icon, message, action) = self.content_to_render(cx);
|
||||
|
||||
let mut element = MouseEventHandler::new::<Self, _, _>(0, cx, |state, cx| {
|
||||
let mut element = MouseEventHandler::<Self>::new(0, cx, |state, cx| {
|
||||
let theme = &cx
|
||||
.global::<Settings>()
|
||||
.theme
|
||||
|
|
|
@ -29,7 +29,7 @@ impl View for UpdateNotification {
|
|||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let theme = &theme.update_notification;
|
||||
|
||||
MouseEventHandler::new::<ViewReleaseNotes, _, _>(0, cx, |state, cx| {
|
||||
MouseEventHandler::<ViewReleaseNotes>::new(0, cx, |state, cx| {
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
|
@ -47,7 +47,7 @@ impl View for UpdateNotification {
|
|||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Cancel, _, _>(0, cx, |state, _| {
|
||||
MouseEventHandler::<Cancel>::new(0, cx, |state, _| {
|
||||
let style = theme.dismiss_button.style_for(state, false);
|
||||
Svg::new("icons/x_mark_thin_8.svg")
|
||||
.with_color(style.color)
|
||||
|
|
32
crates/capture/Cargo.toml
Normal file
32
crates/capture/Cargo.toml
Normal file
|
@ -0,0 +1,32 @@
|
|||
[package]
|
||||
name = "capture"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "An example of screen capture"
|
||||
|
||||
[dependencies]
|
||||
gpui = { path = "../gpui" }
|
||||
live_kit = { path = "../live_kit" }
|
||||
media = { path = "../media" }
|
||||
|
||||
anyhow = "1.0.38"
|
||||
block = "0.1"
|
||||
bytes = "1.2"
|
||||
byteorder = "1.4"
|
||||
cocoa = "0.24"
|
||||
core-foundation = "0.9.3"
|
||||
core-graphics = "0.22.3"
|
||||
foreign-types = "0.3"
|
||||
futures = "0.3"
|
||||
hmac = "0.12"
|
||||
jwt = "0.16"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
objc = "0.2"
|
||||
parking_lot = "0.11.1"
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
sha2 = "0.10"
|
||||
simplelog = "0.9"
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.59.2"
|
7
crates/capture/build.rs
Normal file
7
crates/capture/build.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
fn main() {
|
||||
// Find WebRTC.framework as a sibling of the executable when running outside of an application bundle
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
|
||||
|
||||
// Register exported Objective-C selectors, protocols, etc
|
||||
println!("cargo:rustc-link-arg=-Wl,-ObjC");
|
||||
}
|
71
crates/capture/src/live_kit_token.rs
Normal file
71
crates/capture/src/live_kit_token.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use anyhow::Result;
|
||||
use hmac::{Hmac, Mac};
|
||||
use jwt::SignWithKey;
|
||||
use serde::Serialize;
|
||||
use sha2::Sha256;
|
||||
use std::{
|
||||
ops::Add,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
static DEFAULT_TTL: Duration = Duration::from_secs(6 * 60 * 60); // 6 hours
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ClaimGrants<'a> {
|
||||
iss: &'a str,
|
||||
sub: &'a str,
|
||||
iat: u64,
|
||||
exp: u64,
|
||||
nbf: u64,
|
||||
jwtid: &'a str,
|
||||
video: VideoGrant<'a>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct VideoGrant<'a> {
|
||||
room_create: Option<bool>,
|
||||
room_join: Option<bool>,
|
||||
room_list: Option<bool>,
|
||||
room_record: Option<bool>,
|
||||
room_admin: Option<bool>,
|
||||
room: Option<&'a str>,
|
||||
can_publish: Option<bool>,
|
||||
can_subscribe: Option<bool>,
|
||||
can_publish_data: Option<bool>,
|
||||
hidden: Option<bool>,
|
||||
recorder: Option<bool>,
|
||||
}
|
||||
|
||||
pub fn create_token(
|
||||
api_key: &str,
|
||||
secret_key: &str,
|
||||
room_name: &str,
|
||||
participant_name: &str,
|
||||
) -> Result<String> {
|
||||
let secret_key: Hmac<Sha256> = Hmac::new_from_slice(secret_key.as_bytes())?;
|
||||
|
||||
let now = SystemTime::now();
|
||||
|
||||
let claims = ClaimGrants {
|
||||
iss: api_key,
|
||||
sub: participant_name,
|
||||
iat: now.duration_since(UNIX_EPOCH).unwrap().as_secs(),
|
||||
exp: now
|
||||
.add(DEFAULT_TTL)
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
nbf: 0,
|
||||
jwtid: participant_name,
|
||||
video: VideoGrant {
|
||||
room: Some(room_name),
|
||||
room_join: Some(true),
|
||||
can_publish: Some(true),
|
||||
can_subscribe: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
Ok(claims.sign_with_key(&secret_key)?)
|
||||
}
|
143
crates/capture/src/main.rs
Normal file
143
crates/capture/src/main.rs
Normal file
|
@ -0,0 +1,143 @@
|
|||
mod live_kit_token;
|
||||
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{Canvas, *},
|
||||
keymap::Binding,
|
||||
platform::current::Surface,
|
||||
Menu, MenuItem, ViewContext,
|
||||
};
|
||||
use live_kit::{LocalVideoTrack, Room};
|
||||
use log::LevelFilter;
|
||||
use media::core_video::CVImageBuffer;
|
||||
use postage::watch;
|
||||
use simplelog::SimpleLogger;
|
||||
use std::sync::Arc;
|
||||
|
||||
actions!(capture, [Quit]);
|
||||
|
||||
fn main() {
|
||||
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
|
||||
|
||||
gpui::App::new(()).unwrap().run(|cx| {
|
||||
cx.platform().activate(true);
|
||||
cx.add_global_action(quit);
|
||||
|
||||
cx.add_bindings([Binding::new("cmd-q", Quit, None)]);
|
||||
cx.set_menus(vec![Menu {
|
||||
name: "Zed",
|
||||
items: vec![MenuItem::Action {
|
||||
name: "Quit",
|
||||
action: Box::new(Quit),
|
||||
}],
|
||||
}]);
|
||||
|
||||
let live_kit_url = std::env::var("LIVE_KIT_URL").unwrap();
|
||||
let live_kit_key = std::env::var("LIVE_KIT_KEY").unwrap();
|
||||
let live_kit_secret = std::env::var("LIVE_KIT_SECRET").unwrap();
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let user1_token = live_kit_token::create_token(
|
||||
&live_kit_key,
|
||||
&live_kit_secret,
|
||||
"test-room",
|
||||
"test-participant-1",
|
||||
)
|
||||
.unwrap();
|
||||
let room1 = Room::new();
|
||||
room1.connect(&live_kit_url, &user1_token).await.unwrap();
|
||||
|
||||
let user2_token = live_kit_token::create_token(
|
||||
&live_kit_key,
|
||||
&live_kit_secret,
|
||||
"test-room",
|
||||
"test-participant-2",
|
||||
)
|
||||
.unwrap();
|
||||
let room2 = Room::new();
|
||||
room2.connect(&live_kit_url, &user2_token).await.unwrap();
|
||||
cx.add_window(Default::default(), |cx| ScreenCaptureView::new(room2, cx));
|
||||
|
||||
let windows = live_kit::list_windows();
|
||||
let window = windows
|
||||
.iter()
|
||||
.find(|w| w.owner_name.as_deref() == Some("Safari"))
|
||||
.unwrap();
|
||||
let track = LocalVideoTrack::screen_share_for_window(window.id);
|
||||
room1.publish_video_track(&track).await.unwrap();
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
|
||||
struct ScreenCaptureView {
|
||||
image_buffer: Option<CVImageBuffer>,
|
||||
_room: Arc<Room>,
|
||||
}
|
||||
|
||||
impl gpui::Entity for ScreenCaptureView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl ScreenCaptureView {
|
||||
pub fn new(room: Arc<Room>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let mut remote_video_tracks = room.remote_video_tracks();
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
if let Some(video_track) = remote_video_tracks.next().await {
|
||||
let (mut frames_tx, mut frames_rx) = watch::channel_with(None);
|
||||
video_track.add_renderer(move |frame| *frames_tx.borrow_mut() = Some(frame));
|
||||
|
||||
while let Some(frame) = frames_rx.next().await {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.image_buffer = frame;
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
image_buffer: None,
|
||||
_room: room,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::View for ScreenCaptureView {
|
||||
fn ui_name() -> &'static str {
|
||||
"View"
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut gpui::RenderContext<Self>) -> gpui::ElementBox {
|
||||
let image_buffer = self.image_buffer.clone();
|
||||
let canvas = Canvas::new(move |bounds, _, cx| {
|
||||
if let Some(image_buffer) = image_buffer.clone() {
|
||||
cx.scene.push_surface(Surface {
|
||||
bounds,
|
||||
image_buffer,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(image_buffer) = self.image_buffer.as_ref() {
|
||||
canvas
|
||||
.constrained()
|
||||
.with_width(image_buffer.width() as f32)
|
||||
.with_height(image_buffer.height() as f32)
|
||||
.aligned()
|
||||
.boxed()
|
||||
} else {
|
||||
canvas.boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
|
||||
cx.platform().quit();
|
||||
}
|
|
@ -308,7 +308,7 @@ impl ChatPanel {
|
|||
enum SignInPromptLabel {}
|
||||
|
||||
Align::new(
|
||||
MouseEventHandler::new::<SignInPromptLabel, _, _>(0, cx, |mouse_state, _| {
|
||||
MouseEventHandler::<SignInPromptLabel>::new(0, cx, |mouse_state, _| {
|
||||
Label::new(
|
||||
"Sign in to use chat".to_string(),
|
||||
if mouse_state.hovered {
|
||||
|
|
|
@ -298,7 +298,8 @@ async fn test_host_disconnect(
|
|||
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
||||
|
||||
let (_, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
|
||||
let (_, workspace_b) =
|
||||
cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "b.txt"), true, cx)
|
||||
|
@ -2786,7 +2787,8 @@ async fn test_collaborating_with_code_actions(
|
|||
|
||||
// Join the project as client B.
|
||||
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
|
||||
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
|
||||
let (_window_b, workspace_b) =
|
||||
cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), true, cx)
|
||||
|
@ -3001,7 +3003,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
|||
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
|
||||
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
|
||||
|
||||
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
|
||||
let (_window_b, workspace_b) =
|
||||
cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "one.rs"), true, cx)
|
||||
|
@ -5224,6 +5227,7 @@ impl TestServer {
|
|||
fs: fs.clone(),
|
||||
build_window_options: Default::default,
|
||||
initialize_workspace: |_, _, _| unimplemented!(),
|
||||
default_item_factory: |_, _| unimplemented!(),
|
||||
});
|
||||
|
||||
Channel::init(&client);
|
||||
|
@ -5459,7 +5463,9 @@ impl TestClient {
|
|||
cx: &mut TestAppContext,
|
||||
) -> ViewHandle<Workspace> {
|
||||
let (_, root_view) = cx.add_window(|_| EmptyView);
|
||||
cx.add_view(&root_view, |cx| Workspace::new(project.clone(), cx))
|
||||
cx.add_view(&root_view, |cx| {
|
||||
Workspace::new(project.clone(), |_, _| unimplemented!(), cx)
|
||||
})
|
||||
}
|
||||
|
||||
async fn simulate_host(
|
||||
|
|
|
@ -350,7 +350,8 @@ mod tests {
|
|||
});
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
let editor = cx.add_view(&workspace, |cx| {
|
||||
let mut editor = Editor::single_line(None, cx);
|
||||
editor.set_text("abc", cx);
|
||||
|
|
|
@ -276,7 +276,7 @@ impl ContactsPanel {
|
|||
Section::Offline => "Offline",
|
||||
};
|
||||
let icon_size = theme.section_icon_size;
|
||||
MouseEventHandler::new::<Header, _, _>(section as usize, cx, |_, _| {
|
||||
MouseEventHandler::<Header>::new(section as usize, cx, |_, _| {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Svg::new(if is_collapsed {
|
||||
|
@ -375,7 +375,7 @@ impl ContactsPanel {
|
|||
let baseline_offset =
|
||||
row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
|
||||
|
||||
MouseEventHandler::new::<JoinProject, _, _>(project_id as usize, cx, |mouse_state, cx| {
|
||||
MouseEventHandler::<JoinProject>::new(project_id as usize, cx, |mouse_state, cx| {
|
||||
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
|
||||
let row = theme.project_row.style_for(mouse_state, is_selected);
|
||||
|
||||
|
@ -424,7 +424,7 @@ impl ContactsPanel {
|
|||
return None;
|
||||
}
|
||||
|
||||
let button = MouseEventHandler::new::<ToggleProjectOnline, _, _>(
|
||||
let button = MouseEventHandler::<ToggleProjectOnline>::new(
|
||||
project_id as usize,
|
||||
cx,
|
||||
|state, _| {
|
||||
|
@ -529,7 +529,7 @@ impl ContactsPanel {
|
|||
enum ToggleOnline {}
|
||||
|
||||
let project_id = project_handle.id();
|
||||
MouseEventHandler::new::<LocalProject, _, _>(project_id, cx, |state, cx| {
|
||||
MouseEventHandler::<LocalProject>::new(project_id, cx, |state, cx| {
|
||||
let row = theme.project_row.style_for(state, is_selected);
|
||||
let mut worktree_root_names = String::new();
|
||||
let project = if let Some(project) = project_handle.upgrade(cx.deref_mut()) {
|
||||
|
@ -548,7 +548,7 @@ impl ContactsPanel {
|
|||
Flex::row()
|
||||
.with_child({
|
||||
let button =
|
||||
MouseEventHandler::new::<ToggleOnline, _, _>(project_id, cx, |state, _| {
|
||||
MouseEventHandler::<ToggleOnline>::new(project_id, cx, |state, _| {
|
||||
let mut style = *theme.private_button.style_for(state, false);
|
||||
if is_going_online {
|
||||
style.color = theme.disabled_button.color;
|
||||
|
@ -636,7 +636,7 @@ impl ContactsPanel {
|
|||
|
||||
if is_incoming {
|
||||
row.add_children([
|
||||
MouseEventHandler::new::<Decline, _, _>(user.id as usize, cx, |mouse_state, _| {
|
||||
MouseEventHandler::<Decline>::new(user.id as usize, cx, |mouse_state, _| {
|
||||
let button_style = if is_contact_request_pending {
|
||||
&theme.disabled_button
|
||||
} else {
|
||||
|
@ -658,7 +658,7 @@ impl ContactsPanel {
|
|||
.contained()
|
||||
.with_margin_right(button_spacing)
|
||||
.boxed(),
|
||||
MouseEventHandler::new::<Accept, _, _>(user.id as usize, cx, |mouse_state, _| {
|
||||
MouseEventHandler::<Accept>::new(user.id as usize, cx, |mouse_state, _| {
|
||||
let button_style = if is_contact_request_pending {
|
||||
&theme.disabled_button
|
||||
} else {
|
||||
|
@ -680,7 +680,7 @@ impl ContactsPanel {
|
|||
]);
|
||||
} else {
|
||||
row.add_child(
|
||||
MouseEventHandler::new::<Cancel, _, _>(user.id as usize, cx, |mouse_state, _| {
|
||||
MouseEventHandler::<Cancel>::new(user.id as usize, cx, |mouse_state, _| {
|
||||
let button_style = if is_contact_request_pending {
|
||||
&theme.disabled_button
|
||||
} else {
|
||||
|
@ -1071,7 +1071,7 @@ impl View for ContactsPanel {
|
|||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<AddContact, _, _>(0, cx, |_, _| {
|
||||
MouseEventHandler::<AddContact>::new(0, cx, |_, _| {
|
||||
Svg::new("icons/user_plus_16.svg")
|
||||
.with_color(theme.add_contact_button.color)
|
||||
.constrained()
|
||||
|
@ -1102,10 +1102,7 @@ impl View for ContactsPanel {
|
|||
|
||||
if info.count > 0 {
|
||||
Some(
|
||||
MouseEventHandler::new::<InviteLink, _, _>(
|
||||
0,
|
||||
cx,
|
||||
|state, cx| {
|
||||
MouseEventHandler::<InviteLink>::new(0, cx, |state, cx| {
|
||||
let style =
|
||||
theme.invite_row.style_for(state, false).clone();
|
||||
|
||||
|
@ -1129,8 +1126,7 @@ impl View for ContactsPanel {
|
|||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
},
|
||||
)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new(
|
||||
|
@ -1247,7 +1243,8 @@ mod tests {
|
|||
.0
|
||||
.read_with(cx, |worktree, _| worktree.id().to_proto());
|
||||
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
||||
let panel = cx.add_view(&workspace, |cx| {
|
||||
ContactsPanel::new(
|
||||
user_store.clone(),
|
||||
|
|
|
@ -52,7 +52,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
|||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Dismiss, _, _>(user.id as usize, cx, |state, _| {
|
||||
MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| {
|
||||
render_icon_button(
|
||||
theme.dismiss_button.style_for(state, false),
|
||||
"icons/x_mark_thin_8.svg",
|
||||
|
@ -90,7 +90,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
|||
Flex::row()
|
||||
.with_children(buttons.into_iter().enumerate().map(
|
||||
|(ix, (message, action))| {
|
||||
MouseEventHandler::new::<Button, _, _>(ix, cx, |state, _| {
|
||||
MouseEventHandler::<Button>::new(ix, cx, |state, _| {
|
||||
let button = theme.button.style_for(state, false);
|
||||
Label::new(message.to_string(), button.text.clone())
|
||||
.contained()
|
||||
|
|
32
crates/contacts_status_item/Cargo.toml
Normal file
32
crates/contacts_status_item/Cargo.toml
Normal file
|
@ -0,0 +1,32 @@
|
|||
[package]
|
||||
name = "contacts_status_item"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/contacts_status_item.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
client = { path = "../client" }
|
||||
collections = { path = "../collections" }
|
||||
editor = { path = "../editor" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
menu = { path = "../menu" }
|
||||
picker = { path = "../picker" }
|
||||
project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
anyhow = "1.0"
|
||||
futures = "0.3"
|
||||
log = "0.4"
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
|
||||
[dev-dependencies]
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
94
crates/contacts_status_item/src/contacts_popover.rs
Normal file
94
crates/contacts_status_item/src/contacts_popover.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use editor::Editor;
|
||||
use gpui::{elements::*, Entity, RenderContext, View, ViewContext, ViewHandle};
|
||||
use settings::Settings;
|
||||
|
||||
pub enum Event {
|
||||
Deactivated,
|
||||
}
|
||||
|
||||
pub struct ContactsPopover {
|
||||
filter_editor: ViewHandle<Editor>,
|
||||
}
|
||||
|
||||
impl Entity for ContactsPopover {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for ContactsPopover {
|
||||
fn ui_name() -> &'static str {
|
||||
"ContactsPopover"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &cx.global::<Settings>().theme.contacts_popover;
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ChildView::new(self.filter_editor.clone())
|
||||
.contained()
|
||||
.with_style(
|
||||
cx.global::<Settings>()
|
||||
.theme
|
||||
.contacts_panel
|
||||
.user_query_editor
|
||||
.container,
|
||||
)
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
)
|
||||
// .with_child(
|
||||
// MouseEventHandler::<AddContact>::new(0, cx, |_, _| {
|
||||
// Svg::new("icons/user_plus_16.svg")
|
||||
// .with_color(theme.add_contact_button.color)
|
||||
// .constrained()
|
||||
// .with_height(16.)
|
||||
// .contained()
|
||||
// .with_style(theme.add_contact_button.container)
|
||||
// .aligned()
|
||||
// .boxed()
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, |_, cx| {
|
||||
// cx.dispatch_action(contact_finder::Toggle)
|
||||
// })
|
||||
// .boxed(),
|
||||
// )
|
||||
.constrained()
|
||||
.with_height(
|
||||
cx.global::<Settings>()
|
||||
.theme
|
||||
.contacts_panel
|
||||
.user_query_editor_height,
|
||||
)
|
||||
.aligned()
|
||||
.top()
|
||||
.contained()
|
||||
.with_background_color(theme.background)
|
||||
.with_uniform_padding(4.)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl ContactsPopover {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
cx.observe_window_activation(Self::window_activation_changed)
|
||||
.detach();
|
||||
|
||||
let filter_editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::single_line(
|
||||
Some(|theme| theme.contacts_panel.user_query_editor.clone()),
|
||||
cx,
|
||||
);
|
||||
editor.set_placeholder_text("Filter contacts", cx);
|
||||
editor
|
||||
});
|
||||
|
||||
Self { filter_editor }
|
||||
}
|
||||
|
||||
fn window_activation_changed(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
|
||||
if !is_active {
|
||||
cx.emit(Event::Deactivated);
|
||||
}
|
||||
}
|
||||
}
|
94
crates/contacts_status_item/src/contacts_status_item.rs
Normal file
94
crates/contacts_status_item/src/contacts_status_item.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
mod contacts_popover;
|
||||
|
||||
use contacts_popover::ContactsPopover;
|
||||
use gpui::{
|
||||
actions,
|
||||
color::Color,
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
Appearance, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext,
|
||||
ViewHandle, WindowKind,
|
||||
};
|
||||
|
||||
actions!(contacts_status_item, [ToggleContactsPopover]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(ContactsStatusItem::toggle_contacts_popover);
|
||||
}
|
||||
|
||||
pub struct ContactsStatusItem {
|
||||
popover: Option<ViewHandle<ContactsPopover>>,
|
||||
}
|
||||
|
||||
impl Entity for ContactsStatusItem {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for ContactsStatusItem {
|
||||
fn ui_name() -> &'static str {
|
||||
"ContactsStatusItem"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let color = match cx.appearance {
|
||||
Appearance::Light | Appearance::VibrantLight => Color::black(),
|
||||
Appearance::Dark | Appearance::VibrantDark => Color::white(),
|
||||
};
|
||||
MouseEventHandler::<Self>::new(0, cx, |_, _| {
|
||||
Svg::new("icons/zed_22.svg")
|
||||
.with_color(color)
|
||||
.aligned()
|
||||
.boxed()
|
||||
})
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(ToggleContactsPopover);
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl ContactsStatusItem {
|
||||
pub fn new() -> Self {
|
||||
Self { popover: None }
|
||||
}
|
||||
|
||||
fn toggle_contacts_popover(&mut self, _: &ToggleContactsPopover, cx: &mut ViewContext<Self>) {
|
||||
match self.popover.take() {
|
||||
Some(popover) => {
|
||||
cx.remove_window(popover.window_id());
|
||||
}
|
||||
None => {
|
||||
let window_bounds = cx.window_bounds();
|
||||
let size = vec2f(360., 460.);
|
||||
let origin = window_bounds.lower_left()
|
||||
+ vec2f(window_bounds.width() / 2. - size.x() / 2., 0.);
|
||||
let (_, popover) = cx.add_window(
|
||||
gpui::WindowOptions {
|
||||
bounds: gpui::WindowBounds::Fixed(RectF::new(origin, size)),
|
||||
titlebar: None,
|
||||
center: false,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
},
|
||||
|cx| ContactsPopover::new(cx),
|
||||
);
|
||||
cx.subscribe(&popover, Self::on_popover_event).detach();
|
||||
self.popover = Some(popover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_popover_event(
|
||||
&mut self,
|
||||
popover: ViewHandle<ContactsPopover>,
|
||||
event: &contacts_popover::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
contacts_popover::Event::Deactivated => {
|
||||
self.popover.take();
|
||||
cx.remove_window(popover.window_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,7 +22,6 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
cx.add_action(ContextMenu::cancel);
|
||||
}
|
||||
|
||||
//
|
||||
pub enum ContextMenuItem {
|
||||
Item {
|
||||
label: String,
|
||||
|
@ -57,7 +56,8 @@ impl ContextMenuItem {
|
|||
|
||||
pub struct ContextMenu {
|
||||
show_count: usize,
|
||||
position: Vector2F,
|
||||
anchor_position: Vector2F,
|
||||
anchor_corner: AnchorCorner,
|
||||
items: Vec<ContextMenuItem>,
|
||||
selected_index: Option<usize>,
|
||||
visible: bool,
|
||||
|
@ -100,9 +100,10 @@ impl View for ContextMenu {
|
|||
.boxed();
|
||||
|
||||
Overlay::new(expanded_menu)
|
||||
.hoverable(true)
|
||||
.fit_mode(OverlayFitMode::SnapToWindow)
|
||||
.with_abs_position(self.position)
|
||||
.with_hoverable(true)
|
||||
.with_fit_mode(OverlayFitMode::SnapToWindow)
|
||||
.with_anchor_position(self.anchor_position)
|
||||
.with_anchor_corner(self.anchor_corner)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
|
@ -115,7 +116,8 @@ impl ContextMenu {
|
|||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
show_count: 0,
|
||||
position: Default::default(),
|
||||
anchor_position: Default::default(),
|
||||
anchor_corner: AnchorCorner::TopLeft,
|
||||
items: Default::default(),
|
||||
selected_index: Default::default(),
|
||||
visible: Default::default(),
|
||||
|
@ -226,14 +228,16 @@ impl ContextMenu {
|
|||
|
||||
pub fn show(
|
||||
&mut self,
|
||||
position: Vector2F,
|
||||
anchor_position: Vector2F,
|
||||
anchor_corner: AnchorCorner,
|
||||
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.anchor_position = anchor_position;
|
||||
self.anchor_corner = anchor_corner;
|
||||
self.visible = true;
|
||||
self.show_count += 1;
|
||||
if !cx.is_self_focused() {
|
||||
|
@ -310,13 +314,13 @@ impl ContextMenu {
|
|||
enum Menu {}
|
||||
enum MenuItem {}
|
||||
let style = cx.global::<Settings>().theme.context_menu.clone();
|
||||
MouseEventHandler::new::<Menu, _, _>(0, cx, |_, cx| {
|
||||
MouseEventHandler::<Menu>::new(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, _| {
|
||||
MouseEventHandler::<MenuItem>::new(ix, cx, |state, _| {
|
||||
let style =
|
||||
style.item.style_for(state, Some(ix) == self.selected_index);
|
||||
|
||||
|
|
|
@ -776,7 +776,8 @@ mod tests {
|
|||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
||||
|
||||
// Create some diagnostics
|
||||
project.update(cx, |project, cx| {
|
||||
|
@ -1149,7 +1150,7 @@ mod tests {
|
|||
editor: &ViewHandle<Editor>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Vec<(u32, String)> {
|
||||
let mut presenter = cx.build_presenter(editor.id(), 0.);
|
||||
let mut presenter = cx.build_presenter(editor.id(), 0., Default::default());
|
||||
let mut cx = presenter.build_layout_context(Default::default(), false, cx);
|
||||
cx.render(editor, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
|
|
|
@ -89,7 +89,7 @@ impl View for DiagnosticIndicator {
|
|||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||
let in_progress = !self.in_progress_checks.is_empty();
|
||||
let mut element = Flex::row().with_child(
|
||||
MouseEventHandler::new::<Summary, _, _>(0, cx, |state, cx| {
|
||||
MouseEventHandler::<Summary>::new(0, cx, |state, cx| {
|
||||
let style = cx
|
||||
.global::<Settings>()
|
||||
.theme
|
||||
|
@ -190,7 +190,7 @@ impl View for DiagnosticIndicator {
|
|||
} else if let Some(diagnostic) = &self.current_diagnostic {
|
||||
let message_style = style.diagnostic_message.clone();
|
||||
element.add_child(
|
||||
MouseEventHandler::new::<Message, _, _>(1, cx, |state, _| {
|
||||
MouseEventHandler::<Message>::new(1, cx, |state, _| {
|
||||
Label::new(
|
||||
diagnostic.message.split('\n').next().unwrap().to_string(),
|
||||
message_style.style_for(state, false).text.clone(),
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::{any::Any, rc::Rc};
|
|||
|
||||
use collections::HashSet;
|
||||
use gpui::{
|
||||
elements::{Container, MouseEventHandler},
|
||||
elements::{MouseEventHandler, Overlay},
|
||||
geometry::vector::Vector2F,
|
||||
scene::DragRegionEvent,
|
||||
CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
|
||||
|
@ -114,15 +114,11 @@ impl<V: View> DragAndDrop<V> {
|
|||
|
||||
let position = position + region_offset;
|
||||
|
||||
enum DraggedElementHandler {}
|
||||
Some(
|
||||
MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
|
||||
Container::new(render(payload, cx))
|
||||
.with_margin_left(position.x())
|
||||
.with_margin_top(position.y())
|
||||
.aligned()
|
||||
.top()
|
||||
.left()
|
||||
.boxed()
|
||||
Overlay::new(
|
||||
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
|
||||
render(payload, cx)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::Arrow)
|
||||
.on_up(MouseButton::Left, |_, cx| {
|
||||
|
@ -140,6 +136,9 @@ impl<V: View> DragAndDrop<V> {
|
|||
.with_hoverable(false)
|
||||
.boxed(),
|
||||
)
|
||||
.with_anchor_position(position)
|
||||
.boxed(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -174,7 +173,7 @@ pub trait Draggable {
|
|||
Self: Sized;
|
||||
}
|
||||
|
||||
impl Draggable for MouseEventHandler {
|
||||
impl<Tag> Draggable for MouseEventHandler<Tag> {
|
||||
fn as_draggable<V: View, P: Any>(
|
||||
self,
|
||||
payload: P,
|
||||
|
|
|
@ -682,7 +682,7 @@ impl CompletionsMenu {
|
|||
let completion = &completions[mat.candidate_id];
|
||||
let item_ix = start_ix + ix;
|
||||
items.push(
|
||||
MouseEventHandler::new::<CompletionTag, _, _>(
|
||||
MouseEventHandler::<CompletionTag>::new(
|
||||
mat.candidate_id,
|
||||
cx,
|
||||
|state, _| {
|
||||
|
@ -830,7 +830,7 @@ impl CodeActionsMenu {
|
|||
for (ix, action) in actions[range].iter().enumerate() {
|
||||
let item_ix = start_ix + ix;
|
||||
items.push(
|
||||
MouseEventHandler::new::<ActionTag, _, _>(item_ix, cx, |state, _| {
|
||||
MouseEventHandler::<ActionTag>::new(item_ix, cx, |state, _| {
|
||||
let item_style = if item_ix == selected_item {
|
||||
style.autocomplete.selected_item
|
||||
} else if state.hovered {
|
||||
|
@ -1575,17 +1575,20 @@ impl Editor {
|
|||
let start;
|
||||
let end;
|
||||
let mode;
|
||||
let auto_scroll;
|
||||
match click_count {
|
||||
1 => {
|
||||
start = buffer.anchor_before(position.to_point(&display_map));
|
||||
end = start.clone();
|
||||
mode = SelectMode::Character;
|
||||
auto_scroll = true;
|
||||
}
|
||||
2 => {
|
||||
let range = movement::surrounding_word(&display_map, position);
|
||||
start = buffer.anchor_before(range.start.to_point(&display_map));
|
||||
end = buffer.anchor_before(range.end.to_point(&display_map));
|
||||
mode = SelectMode::Word(start.clone()..end.clone());
|
||||
auto_scroll = true;
|
||||
}
|
||||
3 => {
|
||||
let position = display_map
|
||||
|
@ -1599,15 +1602,17 @@ impl Editor {
|
|||
start = buffer.anchor_before(line_start);
|
||||
end = buffer.anchor_before(next_line_start);
|
||||
mode = SelectMode::Line(start.clone()..end.clone());
|
||||
auto_scroll = true;
|
||||
}
|
||||
_ => {
|
||||
start = buffer.anchor_before(0);
|
||||
end = buffer.anchor_before(buffer.len());
|
||||
mode = SelectMode::All;
|
||||
auto_scroll = false;
|
||||
}
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::Newest), cx, |s| {
|
||||
self.change_selections(auto_scroll.then(|| Autoscroll::Newest), cx, |s| {
|
||||
if !add {
|
||||
s.clear_disjoint();
|
||||
} else if click_count > 1 {
|
||||
|
@ -2735,7 +2740,7 @@ impl Editor {
|
|||
if self.available_code_actions.is_some() {
|
||||
enum Tag {}
|
||||
Some(
|
||||
MouseEventHandler::new::<Tag, _, _>(0, cx, |_, _| {
|
||||
MouseEventHandler::<Tag>::new(0, cx, |_, _| {
|
||||
Svg::new("icons/bolt_8.svg")
|
||||
.with_color(style.code_actions.indicator)
|
||||
.boxed()
|
||||
|
@ -7100,7 +7105,7 @@ mod tests {
|
|||
fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
use workspace::Item;
|
||||
let (_, pane) = cx.add_window(Default::default(), Pane::new);
|
||||
let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(None, cx));
|
||||
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
|
||||
|
||||
cx.add_view(&pane, |cx| {
|
||||
|
@ -7826,7 +7831,7 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
cx.set_state("one «two threeˇ» four");
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
|
||||
|
@ -7974,7 +7979,7 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_newline_below(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<Settings, _, _>(|settings, _| {
|
||||
settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap());
|
||||
|
@ -8050,7 +8055,7 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_tab(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<Settings, _, _>(|settings, _| {
|
||||
settings.editor_overrides.tab_size = Some(NonZeroU32::new(3).unwrap());
|
||||
|
@ -8081,7 +8086,7 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_tab_on_blank_line_auto_indents(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let language = Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig::default(),
|
||||
|
@ -8139,7 +8144,7 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
«oneˇ» «twoˇ»
|
||||
|
@ -8208,7 +8213,7 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<Settings, _, _>(|settings, _| {
|
||||
settings.editor_overrides.hard_tabs = Some(true);
|
||||
|
@ -8416,7 +8421,7 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_backspace(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
|
||||
// Basic backspace
|
||||
cx.set_state(indoc! {"
|
||||
|
@ -8463,7 +8468,7 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_delete(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
onˇe two three
|
||||
|
@ -8800,7 +8805,7 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_clipboard(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
|
||||
cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
|
||||
cx.update_editor(|e, cx| e.cut(&Cut, cx));
|
||||
|
@ -8876,7 +8881,7 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig::default(),
|
||||
Some(tree_sitter_rust::language()),
|
||||
|
@ -9305,7 +9310,7 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_select_next(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
cx.set_state("abc\nˇabc abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx));
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -20,6 +20,10 @@ use crate::{
|
|||
pub const HOVER_DELAY_MILLIS: u64 = 350;
|
||||
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
|
||||
|
||||
pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
|
||||
pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
|
||||
pub const HOVER_POPOVER_GAP: f32 = 10.;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct HoverAt {
|
||||
pub point: Option<DisplayPoint>,
|
||||
|
@ -312,7 +316,7 @@ pub struct InfoPopover {
|
|||
|
||||
impl InfoPopover {
|
||||
pub fn render(&self, style: &EditorStyle, cx: &mut RenderContext<Editor>) -> ElementBox {
|
||||
MouseEventHandler::new::<InfoPopover, _, _>(0, cx, |_, cx| {
|
||||
MouseEventHandler::<InfoPopover>::new(0, cx, |_, cx| {
|
||||
let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
|
||||
flex.extend(self.contents.iter().map(|content| {
|
||||
let project = self.project.read(cx);
|
||||
|
@ -350,10 +354,11 @@ impl InfoPopover {
|
|||
.with_style(style.hover_popover.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_move(|_, _| {})
|
||||
.with_cursor_style(CursorStyle::Arrow)
|
||||
.with_padding(Padding {
|
||||
bottom: 5.,
|
||||
top: 5.,
|
||||
bottom: HOVER_POPOVER_GAP,
|
||||
top: HOVER_POPOVER_GAP,
|
||||
..Default::default()
|
||||
})
|
||||
.boxed()
|
||||
|
@ -383,13 +388,19 @@ impl DiagnosticPopover {
|
|||
|
||||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||
|
||||
MouseEventHandler::new::<DiagnosticPopover, _, _>(0, cx, |_, _| {
|
||||
MouseEventHandler::<DiagnosticPopover>::new(0, cx, |_, _| {
|
||||
Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style)
|
||||
.with_soft_wrap(true)
|
||||
.contained()
|
||||
.with_style(container_style)
|
||||
.boxed()
|
||||
})
|
||||
.with_padding(Padding {
|
||||
top: HOVER_POPOVER_GAP,
|
||||
bottom: HOVER_POPOVER_GAP,
|
||||
..Default::default()
|
||||
})
|
||||
.on_move(|_, _| {})
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(GoToDiagnostic)
|
||||
})
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
use context_menu::ContextMenuItem;
|
||||
use gpui::{geometry::vector::Vector2F, impl_internal_actions, MutableAppContext, ViewContext};
|
||||
use gpui::{
|
||||
elements::AnchorCorner, geometry::vector::Vector2F, impl_internal_actions, MutableAppContext,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
|
||||
|
@ -46,6 +49,7 @@ pub fn deploy_context_menu(
|
|||
editor.mouse_context_menu.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
position,
|
||||
AnchorCorner::TopLeft,
|
||||
vec![
|
||||
ContextMenuItem::item("Rename Symbol", Rename),
|
||||
ContextMenuItem::item("Go To Definition", GoToDefinition),
|
||||
|
|
|
@ -88,7 +88,7 @@ pub struct EditorTestContext<'a> {
|
|||
}
|
||||
|
||||
impl<'a> EditorTestContext<'a> {
|
||||
pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
|
||||
pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
|
||||
let (window_id, editor) = cx.update(|cx| {
|
||||
cx.set_global(Settings::test(cx));
|
||||
crate::init(cx);
|
||||
|
@ -364,7 +364,8 @@ impl<'a> EditorLspTestContext<'a> {
|
|||
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
|
||||
.await;
|
||||
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root", true, cx)
|
||||
|
|
|
@ -316,7 +316,8 @@ mod tests {
|
|||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
cx.dispatch_action(window_id, Toggle);
|
||||
|
||||
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
||||
|
@ -370,7 +371,8 @@ mod tests {
|
|||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
|
||||
|
@ -444,7 +446,8 @@ mod tests {
|
|||
cx,
|
||||
)
|
||||
.await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
finder
|
||||
|
@ -468,7 +471,8 @@ mod tests {
|
|||
cx,
|
||||
)
|
||||
.await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
|
||||
|
@ -520,7 +524,8 @@ mod tests {
|
|||
cx,
|
||||
)
|
||||
.await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
|
||||
|
@ -558,7 +563,8 @@ mod tests {
|
|||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
finder
|
||||
|
|
|
@ -3,6 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
|||
edition = "2021"
|
||||
name = "gpui"
|
||||
version = "0.1.0"
|
||||
description = "A GPU-accelerated UI framework"
|
||||
|
||||
[lib]
|
||||
path = "src/gpui.rs"
|
||||
|
@ -59,6 +60,7 @@ png = "0.16"
|
|||
simplelog = "0.9"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
media = { path = "../media" }
|
||||
anyhow = "1"
|
||||
block = "0.1"
|
||||
cocoa = "0.24"
|
||||
|
|
|
@ -9,8 +9,8 @@ use crate::{
|
|||
platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
|
||||
presenter::Presenter,
|
||||
util::post_inc,
|
||||
AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton, MouseRegionId,
|
||||
PathPromptOptions, TextLayoutCache,
|
||||
Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton,
|
||||
MouseRegionId, PathPromptOptions, TextLayoutCache,
|
||||
};
|
||||
pub use action::*;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
@ -579,6 +579,7 @@ impl TestAppContext {
|
|||
hovered_region_ids: Default::default(),
|
||||
clicked_region_ids: None,
|
||||
refreshing: false,
|
||||
appearance: Appearance::Light,
|
||||
};
|
||||
f(view, &mut render_cx)
|
||||
})
|
||||
|
@ -1243,6 +1244,10 @@ impl MutableAppContext {
|
|||
.map_or(false, |window| window.is_fullscreen)
|
||||
}
|
||||
|
||||
pub fn window_bounds(&self, window_id: usize) -> RectF {
|
||||
self.presenters_and_platform_windows[&window_id].1.bounds()
|
||||
}
|
||||
|
||||
pub fn render_view(&mut self, params: RenderParams) -> Result<ElementBox> {
|
||||
let window_id = params.window_id;
|
||||
let view_id = params.view_id;
|
||||
|
@ -1260,6 +1265,7 @@ impl MutableAppContext {
|
|||
&mut self,
|
||||
window_id: usize,
|
||||
titlebar_height: f32,
|
||||
appearance: Appearance,
|
||||
) -> HashMap<usize, ElementBox> {
|
||||
self.start_frame();
|
||||
#[allow(clippy::needless_collect)]
|
||||
|
@ -1287,6 +1293,7 @@ impl MutableAppContext {
|
|||
hovered_region_ids: Default::default(),
|
||||
clicked_region_ids: None,
|
||||
refreshing: false,
|
||||
appearance,
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
|
@ -1920,46 +1927,61 @@ impl MutableAppContext {
|
|||
},
|
||||
);
|
||||
root_view.update(this, |view, cx| view.on_focus_in(cx.handle().into(), cx));
|
||||
this.open_platform_window(window_id, window_options);
|
||||
|
||||
let window =
|
||||
this.cx
|
||||
.platform
|
||||
.open_window(window_id, window_options, this.foreground.clone());
|
||||
this.register_platform_window(window_id, window);
|
||||
|
||||
(window_id, root_view)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn replace_root_view<T, F>(&mut self, window_id: usize, build_root_view: F) -> ViewHandle<T>
|
||||
pub fn add_status_bar_item<T, F>(&mut self, build_root_view: F) -> (usize, ViewHandle<T>)
|
||||
where
|
||||
T: View,
|
||||
F: FnOnce(&mut ViewContext<T>) -> T,
|
||||
{
|
||||
self.update(|this| {
|
||||
let window_id = post_inc(&mut this.next_window_id);
|
||||
let root_view = this
|
||||
.build_and_insert_view(window_id, ParentId::Root, |cx| Some(build_root_view(cx)))
|
||||
.unwrap();
|
||||
let window = this.cx.windows.get_mut(&window_id).unwrap();
|
||||
window.root_view = root_view.clone().into();
|
||||
window.focused_view_id = Some(root_view.id());
|
||||
root_view
|
||||
this.cx.windows.insert(
|
||||
window_id,
|
||||
Window {
|
||||
root_view: root_view.clone().into(),
|
||||
focused_view_id: Some(root_view.id()),
|
||||
is_active: false,
|
||||
invalidation: None,
|
||||
is_fullscreen: false,
|
||||
},
|
||||
);
|
||||
root_view.update(this, |view, cx| view.on_focus_in(cx.handle().into(), cx));
|
||||
|
||||
let status_item = this.cx.platform.add_status_item();
|
||||
this.register_platform_window(window_id, status_item);
|
||||
|
||||
(window_id, root_view)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove_window(&mut self, window_id: usize) {
|
||||
self.cx.windows.remove(&window_id);
|
||||
self.presenters_and_platform_windows.remove(&window_id);
|
||||
self.flush_effects();
|
||||
}
|
||||
|
||||
fn open_platform_window(&mut self, window_id: usize, window_options: WindowOptions) {
|
||||
let mut window =
|
||||
self.cx
|
||||
.platform
|
||||
.open_window(window_id, window_options, self.foreground.clone());
|
||||
let presenter = Rc::new(RefCell::new(
|
||||
self.build_presenter(window_id, window.titlebar_height()),
|
||||
));
|
||||
fn register_platform_window(
|
||||
&mut self,
|
||||
window_id: usize,
|
||||
mut window: Box<dyn platform::Window>,
|
||||
) {
|
||||
let presenter = Rc::new(RefCell::new(self.build_presenter(
|
||||
window_id,
|
||||
window.titlebar_height(),
|
||||
window.appearance(),
|
||||
)));
|
||||
|
||||
{
|
||||
let mut app = self.upgrade();
|
||||
let presenter = Rc::downgrade(&presenter);
|
||||
|
||||
window.on_event(Box::new(move |event| {
|
||||
app.update(|cx| {
|
||||
if let Some(presenter) = presenter.upgrade() {
|
||||
|
@ -2005,24 +2027,59 @@ impl MutableAppContext {
|
|||
}));
|
||||
}
|
||||
|
||||
{
|
||||
let mut app = self.upgrade();
|
||||
window.on_appearance_changed(Box::new(move || app.update(|cx| cx.refresh_windows())));
|
||||
}
|
||||
|
||||
window.set_input_handler(Box::new(WindowInputHandler {
|
||||
app: self.upgrade().0,
|
||||
window_id,
|
||||
}));
|
||||
|
||||
let scene =
|
||||
presenter
|
||||
.borrow_mut()
|
||||
.build_scene(window.size(), window.scale_factor(), false, self);
|
||||
let scene = presenter.borrow_mut().build_scene(
|
||||
window.content_size(),
|
||||
window.scale_factor(),
|
||||
false,
|
||||
self,
|
||||
);
|
||||
window.present_scene(scene);
|
||||
self.presenters_and_platform_windows
|
||||
.insert(window_id, (presenter.clone(), window));
|
||||
}
|
||||
|
||||
pub fn build_presenter(&mut self, window_id: usize, titlebar_height: f32) -> Presenter {
|
||||
pub fn replace_root_view<T, F>(&mut self, window_id: usize, build_root_view: F) -> ViewHandle<T>
|
||||
where
|
||||
T: View,
|
||||
F: FnOnce(&mut ViewContext<T>) -> T,
|
||||
{
|
||||
self.update(|this| {
|
||||
let root_view = this
|
||||
.build_and_insert_view(window_id, ParentId::Root, |cx| Some(build_root_view(cx)))
|
||||
.unwrap();
|
||||
let window = this.cx.windows.get_mut(&window_id).unwrap();
|
||||
window.root_view = root_view.clone().into();
|
||||
window.focused_view_id = Some(root_view.id());
|
||||
root_view
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove_window(&mut self, window_id: usize) {
|
||||
self.cx.windows.remove(&window_id);
|
||||
self.presenters_and_platform_windows.remove(&window_id);
|
||||
self.flush_effects();
|
||||
}
|
||||
|
||||
pub fn build_presenter(
|
||||
&mut self,
|
||||
window_id: usize,
|
||||
titlebar_height: f32,
|
||||
appearance: Appearance,
|
||||
) -> Presenter {
|
||||
Presenter::new(
|
||||
window_id,
|
||||
titlebar_height,
|
||||
appearance,
|
||||
self.cx.font_cache.clone(),
|
||||
TextLayoutCache::new(self.cx.platform.fonts()),
|
||||
self.assets.clone(),
|
||||
|
@ -2360,9 +2417,13 @@ impl MutableAppContext {
|
|||
{
|
||||
{
|
||||
let mut presenter = presenter.borrow_mut();
|
||||
presenter.invalidate(&mut invalidation, self);
|
||||
let scene =
|
||||
presenter.build_scene(window.size(), window.scale_factor(), false, self);
|
||||
presenter.invalidate(&mut invalidation, window.appearance(), self);
|
||||
let scene = presenter.build_scene(
|
||||
window.content_size(),
|
||||
window.scale_factor(),
|
||||
false,
|
||||
self,
|
||||
);
|
||||
window.present_scene(scene);
|
||||
}
|
||||
self.presenters_and_platform_windows
|
||||
|
@ -2424,9 +2485,11 @@ impl MutableAppContext {
|
|||
let mut presenter = presenter.borrow_mut();
|
||||
presenter.refresh(
|
||||
invalidation.as_mut().unwrap_or(&mut Default::default()),
|
||||
window.appearance(),
|
||||
self,
|
||||
);
|
||||
let scene = presenter.build_scene(window.size(), window.scale_factor(), true, self);
|
||||
let scene =
|
||||
presenter.build_scene(window.content_size(), window.scale_factor(), true, self);
|
||||
window.present_scene(scene);
|
||||
}
|
||||
self.presenters_and_platform_windows = presenters;
|
||||
|
@ -3698,6 +3761,10 @@ impl<'a, T: View> ViewContext<'a, T> {
|
|||
self.app.toggle_window_full_screen(self.window_id)
|
||||
}
|
||||
|
||||
pub fn window_bounds(&self) -> RectF {
|
||||
self.app.window_bounds(self.window_id)
|
||||
}
|
||||
|
||||
pub fn prompt(
|
||||
&self,
|
||||
level: PromptLevel,
|
||||
|
@ -4028,8 +4095,9 @@ pub struct RenderParams {
|
|||
pub view_id: usize,
|
||||
pub titlebar_height: f32,
|
||||
pub hovered_region_ids: HashSet<MouseRegionId>,
|
||||
pub clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
|
||||
pub clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
|
||||
pub refreshing: bool,
|
||||
pub appearance: Appearance,
|
||||
}
|
||||
|
||||
pub struct RenderContext<'a, T: View> {
|
||||
|
@ -4037,9 +4105,10 @@ pub struct RenderContext<'a, T: View> {
|
|||
pub(crate) view_id: usize,
|
||||
pub(crate) view_type: PhantomData<T>,
|
||||
pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
|
||||
pub(crate) clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
|
||||
pub(crate) clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
|
||||
pub app: &'a mut MutableAppContext,
|
||||
pub titlebar_height: f32,
|
||||
pub appearance: Appearance,
|
||||
pub refreshing: bool,
|
||||
}
|
||||
|
||||
|
@ -4060,6 +4129,7 @@ impl<'a, V: View> RenderContext<'a, V> {
|
|||
hovered_region_ids: params.hovered_region_ids.clone(),
|
||||
clicked_region_ids: params.clicked_region_ids.clone(),
|
||||
refreshing: params.refreshing,
|
||||
appearance: params.appearance,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4076,10 +4146,7 @@ impl<'a, V: View> RenderContext<'a, V> {
|
|||
}
|
||||
|
||||
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),
|
||||
};
|
||||
let region_id = MouseRegionId::new::<Tag>(self.view_id, region_id);
|
||||
MouseState {
|
||||
hovered: self.hovered_region_ids.contains(®ion_id),
|
||||
clicked: self.clicked_region_ids.as_ref().and_then(|(ids, button)| {
|
||||
|
@ -4092,9 +4159,10 @@ impl<'a, V: View> RenderContext<'a, V> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn element_state<Tag: 'static, T: 'static + Default>(
|
||||
pub fn element_state<Tag: 'static, T: 'static>(
|
||||
&mut self,
|
||||
element_id: usize,
|
||||
initial: T,
|
||||
) -> ElementStateHandle<T> {
|
||||
let id = ElementStateId {
|
||||
view_id: self.view_id(),
|
||||
|
@ -4104,9 +4172,16 @@ impl<'a, V: View> RenderContext<'a, V> {
|
|||
self.cx
|
||||
.element_states
|
||||
.entry(id)
|
||||
.or_insert_with(|| Box::new(T::default()));
|
||||
.or_insert_with(|| Box::new(initial));
|
||||
ElementStateHandle::new(id, self.frame_count, &self.cx.ref_counts)
|
||||
}
|
||||
|
||||
pub fn default_element_state<Tag: 'static, T: 'static + Default>(
|
||||
&mut self,
|
||||
element_id: usize,
|
||||
) -> ElementStateHandle<T> {
|
||||
self.element_state::<Tag, T>(element_id, T::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<AppContext> for &AppContext {
|
||||
|
@ -5229,6 +5304,10 @@ impl<T: 'static> ElementStateHandle<T> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> ElementStateId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T {
|
||||
cx.element_states
|
||||
.get(&self.id)
|
||||
|
@ -6032,12 +6111,12 @@ mod tests {
|
|||
}
|
||||
|
||||
impl super::View for View {
|
||||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
enum Handler {}
|
||||
let mouse_down_count = self.mouse_down_count.clone();
|
||||
EventHandler::new(Empty::new().boxed())
|
||||
.on_mouse_down(move |_| {
|
||||
MouseEventHandler::<Handler>::new(0, cx, |_, _| Empty::new().boxed())
|
||||
.on_down(MouseButton::Left, move |_, _| {
|
||||
mouse_down_count.fetch_add(1, SeqCst);
|
||||
true
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ mod canvas;
|
|||
mod constrained_box;
|
||||
mod container;
|
||||
mod empty;
|
||||
mod event_handler;
|
||||
mod expanded;
|
||||
mod flex;
|
||||
mod hook;
|
||||
|
@ -13,6 +12,7 @@ mod label;
|
|||
mod list;
|
||||
mod mouse_event_handler;
|
||||
mod overlay;
|
||||
mod resizable;
|
||||
mod stack;
|
||||
mod svg;
|
||||
mod text;
|
||||
|
@ -21,8 +21,8 @@ mod uniform_list;
|
|||
|
||||
use self::expanded::Expanded;
|
||||
pub use self::{
|
||||
align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*,
|
||||
hook::*, image::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*,
|
||||
align::*, canvas::*, constrained_box::*, container::*, empty::*, flex::*, hook::*, image::*,
|
||||
keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*, resizable::*,
|
||||
stack::*, svg::*, text::*, tooltip::*, uniform_list::*,
|
||||
};
|
||||
pub use crate::presenter::ChildView;
|
||||
|
@ -187,6 +187,27 @@ pub trait Element {
|
|||
{
|
||||
Tooltip::new::<Tag, T>(id, text, action, style, self.boxed(), cx)
|
||||
}
|
||||
|
||||
fn with_resize_handle<Tag: 'static, T: View>(
|
||||
self,
|
||||
element_id: usize,
|
||||
side: Side,
|
||||
handle_size: f32,
|
||||
initial_size: f32,
|
||||
cx: &mut RenderContext<T>,
|
||||
) -> Resizable
|
||||
where
|
||||
Self: 'static + Sized,
|
||||
{
|
||||
Resizable::new::<Tag, T>(
|
||||
self.boxed(),
|
||||
element_id,
|
||||
side,
|
||||
handle_size,
|
||||
initial_size,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Lifecycle<T: Element> {
|
||||
|
|
|
@ -373,6 +373,24 @@ pub struct Padding {
|
|||
pub right: f32,
|
||||
}
|
||||
|
||||
impl Padding {
|
||||
pub fn horizontal(padding: f32) -> Self {
|
||||
Self {
|
||||
left: padding,
|
||||
right: padding,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical(padding: f32) -> Self {
|
||||
Self {
|
||||
top: padding,
|
||||
bottom: padding,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Padding {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
|
|
|
@ -1,177 +0,0 @@
|
|||
use crate::{
|
||||
geometry::vector::Vector2F, presenter::MeasurementContext, CursorRegion, DebugContext, Element,
|
||||
ElementBox, Event, EventContext, LayoutContext, MouseButton, MouseButtonEvent, MouseRegion,
|
||||
NavigationDirection, PaintContext, SizeConstraint,
|
||||
};
|
||||
use pathfinder_geometry::rect::RectF;
|
||||
use serde_json::json;
|
||||
use std::{any::TypeId, ops::Range};
|
||||
|
||||
pub struct EventHandler {
|
||||
child: ElementBox,
|
||||
capture_all: Option<(TypeId, usize)>,
|
||||
mouse_down: Option<Box<dyn FnMut(&mut EventContext) -> bool>>,
|
||||
right_mouse_down: Option<Box<dyn FnMut(&mut EventContext) -> bool>>,
|
||||
navigate_mouse_down: Option<Box<dyn FnMut(NavigationDirection, &mut EventContext) -> bool>>,
|
||||
}
|
||||
|
||||
impl EventHandler {
|
||||
pub fn new(child: ElementBox) -> Self {
|
||||
Self {
|
||||
child,
|
||||
capture_all: None,
|
||||
mouse_down: None,
|
||||
right_mouse_down: None,
|
||||
navigate_mouse_down: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_mouse_down<F>(mut self, callback: F) -> Self
|
||||
where
|
||||
F: 'static + FnMut(&mut EventContext) -> bool,
|
||||
{
|
||||
self.mouse_down = Some(Box::new(callback));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_right_mouse_down<F>(mut self, callback: F) -> Self
|
||||
where
|
||||
F: 'static + FnMut(&mut EventContext) -> bool,
|
||||
{
|
||||
self.right_mouse_down = Some(Box::new(callback));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_navigate_mouse_down<F>(mut self, callback: F) -> Self
|
||||
where
|
||||
F: 'static + FnMut(NavigationDirection, &mut EventContext) -> bool,
|
||||
{
|
||||
self.navigate_mouse_down = Some(Box::new(callback));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn capture_all<T: 'static>(mut self, id: usize) -> Self {
|
||||
self.capture_all = Some((TypeId::of::<T>(), id));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for EventHandler {
|
||||
type LayoutState = ();
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
cx: &mut LayoutContext,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let size = self.child.layout(constraint, cx);
|
||||
(size, ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState {
|
||||
if let Some(discriminant) = self.capture_all {
|
||||
cx.scene.push_stacking_context(None);
|
||||
cx.scene.push_cursor_region(CursorRegion {
|
||||
bounds: visible_bounds,
|
||||
style: Default::default(),
|
||||
});
|
||||
cx.scene.push_mouse_region(MouseRegion::handle_all(
|
||||
cx.current_view_id(),
|
||||
Some(discriminant),
|
||||
visible_bounds,
|
||||
));
|
||||
cx.scene.pop_stacking_context();
|
||||
}
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
_: RectF,
|
||||
visible_bounds: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
if self.capture_all.is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.child.dispatch_event(event, cx) {
|
||||
true
|
||||
} else {
|
||||
match event {
|
||||
Event::MouseDown(MouseButtonEvent {
|
||||
button: MouseButton::Left,
|
||||
position,
|
||||
..
|
||||
}) => {
|
||||
if let Some(callback) = self.mouse_down.as_mut() {
|
||||
if visible_bounds.contains_point(*position) {
|
||||
return callback(cx);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
Event::MouseDown(MouseButtonEvent {
|
||||
button: MouseButton::Right,
|
||||
position,
|
||||
..
|
||||
}) => {
|
||||
if let Some(callback) = self.right_mouse_down.as_mut() {
|
||||
if visible_bounds.contains_point(*position) {
|
||||
return callback(cx);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
Event::MouseDown(MouseButtonEvent {
|
||||
button: MouseButton::Navigate(direction),
|
||||
position,
|
||||
..
|
||||
}) => {
|
||||
if let Some(callback) = self.navigate_mouse_down.as_mut() {
|
||||
if visible_bounds.contains_point(*position) {
|
||||
return callback(*direction, cx);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: Range<usize>,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
cx: &MeasurementContext,
|
||||
) -> Option<RectF> {
|
||||
self.child.rect_for_text_range(range_utf16, cx)
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
_: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
cx: &DebugContext,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "EventHandler",
|
||||
"child": self.child.debug(cx),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
use std::{any::Any, f32::INFINITY, ops::Range};
|
||||
use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
json::{self, ToJson, Value},
|
||||
presenter::MeasurementContext,
|
||||
Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext,
|
||||
LayoutContext, MouseMovedEvent, PaintContext, RenderContext, ScrollWheelEvent, SizeConstraint,
|
||||
Vector2FExt, View,
|
||||
LayoutContext, PaintContext, RenderContext, SizeConstraint, Vector2FExt, View,
|
||||
};
|
||||
use pathfinder_geometry::{
|
||||
rect::RectF,
|
||||
|
@ -15,14 +14,14 @@ use serde_json::json;
|
|||
|
||||
#[derive(Default)]
|
||||
struct ScrollState {
|
||||
scroll_to: Option<usize>,
|
||||
scroll_position: f32,
|
||||
scroll_to: Cell<Option<usize>>,
|
||||
scroll_position: Cell<f32>,
|
||||
}
|
||||
|
||||
pub struct Flex {
|
||||
axis: Axis,
|
||||
children: Vec<ElementBox>,
|
||||
scroll_state: Option<ElementStateHandle<ScrollState>>,
|
||||
scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
|
||||
}
|
||||
|
||||
impl Flex {
|
||||
|
@ -52,9 +51,9 @@ impl Flex {
|
|||
Tag: 'static,
|
||||
V: View,
|
||||
{
|
||||
let scroll_state = cx.element_state::<Tag, ScrollState>(element_id);
|
||||
scroll_state.update(cx, |scroll_state, _| scroll_state.scroll_to = scroll_to);
|
||||
self.scroll_state = Some(scroll_state);
|
||||
let scroll_state = cx.default_element_state::<Tag, Rc<ScrollState>>(element_id);
|
||||
scroll_state.read(cx).scroll_to.set(scroll_to);
|
||||
self.scroll_state = Some((scroll_state, cx.handle().id()));
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -202,9 +201,9 @@ impl Element for Flex {
|
|||
}
|
||||
|
||||
if let Some(scroll_state) = self.scroll_state.as_ref() {
|
||||
scroll_state.update(cx, |scroll_state, _| {
|
||||
scroll_state.0.update(cx, |scroll_state, _| {
|
||||
if let Some(scroll_to) = scroll_state.scroll_to.take() {
|
||||
let visible_start = scroll_state.scroll_position;
|
||||
let visible_start = scroll_state.scroll_position.get();
|
||||
let visible_end = visible_start + size.along(self.axis);
|
||||
if let Some(child) = self.children.get(scroll_to) {
|
||||
let child_start: f32 = self.children[..scroll_to]
|
||||
|
@ -213,15 +212,22 @@ impl Element for Flex {
|
|||
.sum();
|
||||
let child_end = child_start + child.size().along(self.axis);
|
||||
if child_start < visible_start {
|
||||
scroll_state.scroll_position = child_start;
|
||||
scroll_state.scroll_position.set(child_start);
|
||||
} else if child_end > visible_end {
|
||||
scroll_state.scroll_position = child_end - size.along(self.axis);
|
||||
scroll_state
|
||||
.scroll_position
|
||||
.set(child_end - size.along(self.axis));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scroll_state.scroll_position =
|
||||
scroll_state.scroll_position.min(-remaining_space).max(0.);
|
||||
scroll_state.scroll_position.set(
|
||||
scroll_state
|
||||
.scroll_position
|
||||
.get()
|
||||
.min(-remaining_space)
|
||||
.max(0.),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -242,9 +248,45 @@ impl Element for Flex {
|
|||
cx.scene.push_layer(Some(bounds));
|
||||
}
|
||||
|
||||
if let Some(scroll_state) = &self.scroll_state {
|
||||
cx.scene.push_mouse_region(
|
||||
crate::MouseRegion::new::<Self>(scroll_state.1, 0, bounds)
|
||||
.on_scroll({
|
||||
let scroll_state = scroll_state.0.read(cx).clone();
|
||||
let axis = self.axis;
|
||||
move |e, cx| {
|
||||
if remaining_space < 0. {
|
||||
let mut delta = match axis {
|
||||
Axis::Horizontal => {
|
||||
if e.delta.x() != 0. {
|
||||
e.delta.x()
|
||||
} else {
|
||||
e.delta.y()
|
||||
}
|
||||
}
|
||||
Axis::Vertical => e.delta.y(),
|
||||
};
|
||||
if !e.precise {
|
||||
delta *= 20.;
|
||||
}
|
||||
|
||||
scroll_state
|
||||
.scroll_position
|
||||
.set(scroll_state.scroll_position.get() - delta);
|
||||
|
||||
cx.notify();
|
||||
} else {
|
||||
cx.propogate_event();
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_move(|_, _| { /* Capture move events */ }),
|
||||
)
|
||||
}
|
||||
|
||||
let mut child_origin = bounds.origin();
|
||||
if let Some(scroll_state) = self.scroll_state.as_ref() {
|
||||
let scroll_position = scroll_state.read(cx).scroll_position;
|
||||
let scroll_position = scroll_state.0.read(cx).scroll_position.get();
|
||||
match self.axis {
|
||||
Axis::Horizontal => child_origin.set_x(child_origin.x() - scroll_position),
|
||||
Axis::Vertical => child_origin.set_y(child_origin.y() - scroll_position),
|
||||
|
@ -278,9 +320,9 @@ impl Element for Flex {
|
|||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
bounds: RectF,
|
||||
_: RectF,
|
||||
remaining_space: &mut Self::LayoutState,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
|
@ -288,50 +330,6 @@ impl Element for Flex {
|
|||
for child in &mut self.children {
|
||||
handled = child.dispatch_event(event, cx) || handled;
|
||||
}
|
||||
if !handled {
|
||||
if let &Event::ScrollWheel(ScrollWheelEvent {
|
||||
position,
|
||||
delta,
|
||||
precise,
|
||||
..
|
||||
}) = event
|
||||
{
|
||||
if *remaining_space < 0. && bounds.contains_point(position) {
|
||||
if let Some(scroll_state) = self.scroll_state.as_ref() {
|
||||
scroll_state.update(cx, |scroll_state, cx| {
|
||||
let mut delta = match self.axis {
|
||||
Axis::Horizontal => {
|
||||
if delta.x() != 0. {
|
||||
delta.x()
|
||||
} else {
|
||||
delta.y()
|
||||
}
|
||||
}
|
||||
Axis::Vertical => delta.y(),
|
||||
};
|
||||
if !precise {
|
||||
delta *= 20.;
|
||||
}
|
||||
|
||||
scroll_state.scroll_position -= delta;
|
||||
|
||||
handled = true;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !handled {
|
||||
if let &Event::MouseMoved(MouseMovedEvent { position, .. }) = event {
|
||||
// If this is a scrollable flex, and the mouse is over it, eat the scroll event to prevent
|
||||
// propogating it to the element below.
|
||||
if self.scroll_state.is_some() && bounds.contains_point(position) {
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handled
|
||||
}
|
||||
|
|
|
@ -5,8 +5,8 @@ use crate::{
|
|||
},
|
||||
json::json,
|
||||
presenter::MeasurementContext,
|
||||
DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
|
||||
RenderContext, ScrollWheelEvent, SizeConstraint, View, ViewContext,
|
||||
DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, MouseRegion,
|
||||
PaintContext, RenderContext, SizeConstraint, View, ViewContext,
|
||||
};
|
||||
use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
|
||||
use sum_tree::{Bias, SumTree};
|
||||
|
@ -263,6 +263,22 @@ impl Element for List {
|
|||
) {
|
||||
cx.scene.push_layer(Some(bounds));
|
||||
|
||||
cx.scene
|
||||
.push_mouse_region(MouseRegion::new::<Self>(10, 0, bounds).on_scroll({
|
||||
let state = self.state.clone();
|
||||
let height = bounds.height();
|
||||
let scroll_top = scroll_top.clone();
|
||||
move |e, cx| {
|
||||
state.0.borrow_mut().scroll(
|
||||
&scroll_top,
|
||||
height,
|
||||
e.platform_event.delta,
|
||||
e.platform_event.precise,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}));
|
||||
|
||||
let state = &mut *self.state.0.borrow_mut();
|
||||
for (mut element, origin) in state.visible_elements(bounds, scroll_top) {
|
||||
element.paint(origin, visible_bounds, cx);
|
||||
|
@ -312,20 +328,6 @@ impl Element for List {
|
|||
drop(cursor);
|
||||
state.items = new_items;
|
||||
|
||||
if let Event::ScrollWheel(ScrollWheelEvent {
|
||||
position,
|
||||
delta,
|
||||
precise,
|
||||
..
|
||||
}) = event
|
||||
{
|
||||
if bounds.contains_point(*position)
|
||||
&& state.scroll(scroll_top, bounds.height(), *delta, *precise, cx)
|
||||
{
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
handled
|
||||
}
|
||||
|
||||
|
@ -527,7 +529,7 @@ impl StateInner {
|
|||
mut delta: Vector2F,
|
||||
precise: bool,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
) {
|
||||
if !precise {
|
||||
delta *= 20.;
|
||||
}
|
||||
|
@ -554,9 +556,6 @@ impl StateInner {
|
|||
let visible_range = self.visible_range(height, scroll_top);
|
||||
self.scroll_handler.as_mut().unwrap()(visible_range, cx);
|
||||
}
|
||||
cx.notify();
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 {
|
||||
|
@ -659,7 +658,7 @@ mod tests {
|
|||
|
||||
#[crate::test(self)]
|
||||
fn test_layout(cx: &mut crate::MutableAppContext) {
|
||||
let mut presenter = cx.build_presenter(0, 0.);
|
||||
let mut presenter = cx.build_presenter(0, 0., Default::default());
|
||||
let (_, view) = cx.add_window(Default::default(), |_| TestView);
|
||||
let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.));
|
||||
|
||||
|
@ -759,7 +758,7 @@ mod tests {
|
|||
.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., Default::default());
|
||||
let mut next_id = 0;
|
||||
let elements = Rc::new(RefCell::new(
|
||||
(0..rng.gen_range(0..=20))
|
||||
|
|
|
@ -7,37 +7,39 @@ use crate::{
|
|||
platform::CursorStyle,
|
||||
scene::{
|
||||
ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent,
|
||||
HandlerSet, HoverRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
|
||||
HandlerSet, HoverRegionEvent, MoveRegionEvent, ScrollWheelRegionEvent, UpOutRegionEvent,
|
||||
UpRegionEvent,
|
||||
},
|
||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MeasurementContext,
|
||||
MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::{any::TypeId, ops::Range};
|
||||
use std::{marker::PhantomData, ops::Range};
|
||||
|
||||
pub struct MouseEventHandler {
|
||||
pub struct MouseEventHandler<Tag: 'static> {
|
||||
child: ElementBox,
|
||||
discriminant: (TypeId, usize),
|
||||
region_id: usize,
|
||||
cursor_style: Option<CursorStyle>,
|
||||
handlers: HandlerSet,
|
||||
hoverable: bool,
|
||||
padding: Padding,
|
||||
_tag: PhantomData<Tag>,
|
||||
}
|
||||
|
||||
impl MouseEventHandler {
|
||||
pub fn new<Tag, V, F>(id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
|
||||
impl<Tag> MouseEventHandler<Tag> {
|
||||
pub fn new<V, F>(region_id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
|
||||
where
|
||||
Tag: 'static,
|
||||
V: View,
|
||||
F: FnOnce(MouseState, &mut RenderContext<V>) -> ElementBox,
|
||||
{
|
||||
Self {
|
||||
child: render_child(cx.mouse_state::<Tag>(id), cx),
|
||||
child: render_child(cx.mouse_state::<Tag>(region_id), cx),
|
||||
region_id,
|
||||
cursor_style: None,
|
||||
discriminant: (TypeId::of::<Tag>(), id),
|
||||
handlers: Default::default(),
|
||||
hoverable: true,
|
||||
padding: Default::default(),
|
||||
_tag: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -121,6 +123,14 @@ impl MouseEventHandler {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn on_scroll(
|
||||
mut self,
|
||||
handler: impl Fn(ScrollWheelRegionEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.handlers = self.handlers.on_scroll(handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_hoverable(mut self, is_hoverable: bool) -> Self {
|
||||
self.hoverable = is_hoverable;
|
||||
self
|
||||
|
@ -140,7 +150,7 @@ impl MouseEventHandler {
|
|||
}
|
||||
}
|
||||
|
||||
impl Element for MouseEventHandler {
|
||||
impl<Tag> Element for MouseEventHandler<Tag> {
|
||||
type LayoutState = ();
|
||||
type PaintState = ();
|
||||
|
||||
|
@ -168,9 +178,9 @@ impl Element for MouseEventHandler {
|
|||
}
|
||||
|
||||
cx.scene.push_mouse_region(
|
||||
MouseRegion::from_handlers(
|
||||
MouseRegion::from_handlers::<Tag>(
|
||||
cx.current_view_id(),
|
||||
Some(self.discriminant),
|
||||
self.region_id,
|
||||
hit_bounds,
|
||||
self.handlers.clone(),
|
||||
)
|
||||
|
|
|
@ -4,14 +4,15 @@ use crate::{
|
|||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json::ToJson,
|
||||
presenter::MeasurementContext,
|
||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion,
|
||||
Axis, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion,
|
||||
PaintContext, SizeConstraint,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub struct Overlay {
|
||||
child: ElementBox,
|
||||
abs_position: Option<Vector2F>,
|
||||
anchor_position: Option<Vector2F>,
|
||||
anchor_corner: AnchorCorner,
|
||||
fit_mode: OverlayFitMode,
|
||||
hoverable: bool,
|
||||
}
|
||||
|
@ -19,31 +20,79 @@ pub struct Overlay {
|
|||
#[derive(Copy, Clone)]
|
||||
pub enum OverlayFitMode {
|
||||
SnapToWindow,
|
||||
FlipAlignment,
|
||||
SwitchAnchor,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AnchorCorner {
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomLeft,
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
impl AnchorCorner {
|
||||
fn get_bounds(&self, anchor_position: Vector2F, size: Vector2F) -> RectF {
|
||||
match self {
|
||||
Self::TopLeft => RectF::from_points(anchor_position, anchor_position + size),
|
||||
Self::TopRight => RectF::from_points(
|
||||
anchor_position - Vector2F::new(size.x(), 0.),
|
||||
anchor_position + Vector2F::new(0., size.y()),
|
||||
),
|
||||
Self::BottomLeft => RectF::from_points(
|
||||
anchor_position - Vector2F::new(0., size.y()),
|
||||
anchor_position + Vector2F::new(size.x(), 0.),
|
||||
),
|
||||
Self::BottomRight => RectF::from_points(anchor_position - size, anchor_position),
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_axis(self, axis: Axis) -> Self {
|
||||
match axis {
|
||||
Axis::Vertical => match self {
|
||||
AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
|
||||
AnchorCorner::TopRight => AnchorCorner::BottomRight,
|
||||
AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
|
||||
AnchorCorner::BottomRight => AnchorCorner::TopRight,
|
||||
},
|
||||
Axis::Horizontal => match self {
|
||||
AnchorCorner::TopLeft => AnchorCorner::TopRight,
|
||||
AnchorCorner::TopRight => AnchorCorner::TopLeft,
|
||||
AnchorCorner::BottomLeft => AnchorCorner::BottomRight,
|
||||
AnchorCorner::BottomRight => AnchorCorner::BottomLeft,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Overlay {
|
||||
pub fn new(child: ElementBox) -> Self {
|
||||
Self {
|
||||
child,
|
||||
abs_position: None,
|
||||
anchor_position: None,
|
||||
anchor_corner: AnchorCorner::TopLeft,
|
||||
fit_mode: OverlayFitMode::None,
|
||||
hoverable: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_abs_position(mut self, position: Vector2F) -> Self {
|
||||
self.abs_position = Some(position);
|
||||
pub fn with_anchor_position(mut self, position: Vector2F) -> Self {
|
||||
self.anchor_position = Some(position);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn fit_mode(mut self, fit_mode: OverlayFitMode) -> Self {
|
||||
pub fn with_anchor_corner(mut self, anchor_corner: AnchorCorner) -> Self {
|
||||
self.anchor_corner = anchor_corner;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_fit_mode(mut self, fit_mode: OverlayFitMode) -> Self {
|
||||
self.fit_mode = fit_mode;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn hoverable(mut self, hoverable: bool) -> Self {
|
||||
pub fn with_hoverable(mut self, hoverable: bool) -> Self {
|
||||
self.hoverable = hoverable;
|
||||
self
|
||||
}
|
||||
|
@ -58,7 +107,7 @@ impl Element for Overlay {
|
|||
constraint: SizeConstraint,
|
||||
cx: &mut LayoutContext,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let constraint = if self.abs_position.is_some() {
|
||||
let constraint = if self.anchor_position.is_some() {
|
||||
SizeConstraint::new(Vector2F::zero(), cx.window_size)
|
||||
} else {
|
||||
constraint
|
||||
|
@ -74,45 +123,75 @@ impl Element for Overlay {
|
|||
size: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) {
|
||||
let mut bounds = RectF::new(self.abs_position.unwrap_or_else(|| bounds.origin()), *size);
|
||||
cx.scene.push_stacking_context(None);
|
||||
|
||||
if self.hoverable {
|
||||
cx.scene.push_mouse_region(MouseRegion {
|
||||
view_id: cx.current_view_id(),
|
||||
bounds,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin());
|
||||
let mut bounds = self.anchor_corner.get_bounds(anchor_position, *size);
|
||||
|
||||
match self.fit_mode {
|
||||
OverlayFitMode::SnapToWindow => {
|
||||
// Snap the right edge of the overlay to the right edge of the window if
|
||||
// its horizontal bounds overflow.
|
||||
if bounds.lower_right().x() > cx.window_size.x() {
|
||||
bounds.set_origin_x((cx.window_size.x() - bounds.width()).max(0.));
|
||||
// Snap the horizontal edges of the overlay to the horizontal edges of the window if
|
||||
// its horizontal bounds overflow
|
||||
if bounds.max_x() > cx.window_size.x() {
|
||||
let mut lower_right = bounds.lower_right();
|
||||
lower_right.set_x(cx.window_size.x());
|
||||
bounds = RectF::from_points(lower_right - *size, lower_right);
|
||||
} else if bounds.min_x() < 0. {
|
||||
let mut upper_left = bounds.origin();
|
||||
upper_left.set_x(0.);
|
||||
bounds = RectF::from_points(upper_left, upper_left + *size);
|
||||
}
|
||||
|
||||
// Snap the bottom edge of the overlay to the bottom edge of the window if
|
||||
// Snap the vertical edges of the overlay to the vertical edges of the window if
|
||||
// its vertical bounds overflow.
|
||||
if bounds.lower_right().y() > cx.window_size.y() {
|
||||
bounds.set_origin_y((cx.window_size.y() - bounds.height()).max(0.));
|
||||
if bounds.max_y() > cx.window_size.y() {
|
||||
let mut lower_right = bounds.lower_right();
|
||||
lower_right.set_y(cx.window_size.y());
|
||||
bounds = RectF::from_points(lower_right - *size, lower_right);
|
||||
} else if bounds.min_y() < 0. {
|
||||
let mut upper_left = bounds.origin();
|
||||
upper_left.set_y(0.);
|
||||
bounds = RectF::from_points(upper_left, upper_left + *size);
|
||||
}
|
||||
}
|
||||
OverlayFitMode::FlipAlignment => {
|
||||
// Right-align overlay if its horizontal bounds overflow.
|
||||
if bounds.lower_right().x() > cx.window_size.x() {
|
||||
bounds.set_origin_x(bounds.origin_x() - bounds.width());
|
||||
OverlayFitMode::SwitchAnchor => {
|
||||
let mut anchor_corner = self.anchor_corner;
|
||||
|
||||
if bounds.max_x() > cx.window_size.x() {
|
||||
anchor_corner = anchor_corner.switch_axis(Axis::Horizontal);
|
||||
}
|
||||
|
||||
// Bottom-align overlay if its vertical bounds overflow.
|
||||
if bounds.lower_right().y() > cx.window_size.y() {
|
||||
bounds.set_origin_y(bounds.origin_y() - bounds.height());
|
||||
if bounds.max_y() > cx.window_size.y() {
|
||||
anchor_corner = anchor_corner.switch_axis(Axis::Vertical);
|
||||
}
|
||||
|
||||
if bounds.min_x() < 0. {
|
||||
anchor_corner = anchor_corner.switch_axis(Axis::Horizontal)
|
||||
}
|
||||
|
||||
if bounds.min_y() < 0. {
|
||||
anchor_corner = anchor_corner.switch_axis(Axis::Vertical)
|
||||
}
|
||||
|
||||
// Update bounds if needed
|
||||
if anchor_corner != self.anchor_corner {
|
||||
bounds = anchor_corner.get_bounds(anchor_position, *size)
|
||||
}
|
||||
}
|
||||
OverlayFitMode::None => {}
|
||||
}
|
||||
|
||||
cx.scene.push_stacking_context(None);
|
||||
|
||||
if self.hoverable {
|
||||
enum OverlayHoverCapture {}
|
||||
// Block hovers in lower stacking contexts
|
||||
cx.scene
|
||||
.push_mouse_region(MouseRegion::new::<OverlayHoverCapture>(
|
||||
cx.current_view_id(),
|
||||
cx.current_view_id(),
|
||||
bounds,
|
||||
));
|
||||
}
|
||||
|
||||
self.child.paint(bounds.origin(), bounds, cx);
|
||||
cx.scene.pop_stacking_context();
|
||||
}
|
||||
|
@ -150,7 +229,7 @@ impl Element for Overlay {
|
|||
) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "Overlay",
|
||||
"abs_position": self.abs_position.to_json(),
|
||||
"abs_position": self.anchor_position.to_json(),
|
||||
"child": self.child.debug(cx),
|
||||
})
|
||||
}
|
||||
|
|
225
crates/gpui/src/elements/resizable.rs
Normal file
225
crates/gpui/src/elements/resizable.rs
Normal file
|
@ -0,0 +1,225 @@
|
|||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
geometry::rect::RectF, scene::DragRegionEvent, Axis, CursorStyle, Element, ElementBox,
|
||||
ElementStateHandle, MouseButton, MouseRegion, RenderContext, View,
|
||||
};
|
||||
|
||||
use super::{ConstrainedBox, Hook};
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Side {
|
||||
Top,
|
||||
Bottom,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl Side {
|
||||
fn axis(&self) -> Axis {
|
||||
match self {
|
||||
Side::Left | Side::Right => Axis::Horizontal,
|
||||
Side::Top | Side::Bottom => Axis::Vertical,
|
||||
}
|
||||
}
|
||||
|
||||
/// 'before' is in reference to the standard english document ordering of left-to-right
|
||||
/// then top-to-bottom
|
||||
fn before_content(self) -> bool {
|
||||
match self {
|
||||
Side::Left | Side::Top => true,
|
||||
Side::Right | Side::Bottom => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn relevant_component(&self, vector: Vector2F) -> f32 {
|
||||
match self.axis() {
|
||||
Axis::Horizontal => vector.x(),
|
||||
Axis::Vertical => vector.y(),
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_delta(&self, e: DragRegionEvent) -> f32 {
|
||||
if self.before_content() {
|
||||
self.relevant_component(e.prev_mouse_position) - self.relevant_component(e.position)
|
||||
} else {
|
||||
self.relevant_component(e.position) - self.relevant_component(e.prev_mouse_position)
|
||||
}
|
||||
}
|
||||
|
||||
fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF {
|
||||
match self {
|
||||
Side::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
|
||||
Side::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())),
|
||||
Side::Bottom => {
|
||||
let mut origin = bounds.lower_left();
|
||||
origin.set_y(origin.y() - handle_size);
|
||||
RectF::new(origin, vec2f(bounds.width(), handle_size))
|
||||
}
|
||||
Side::Right => {
|
||||
let mut origin = bounds.upper_right();
|
||||
origin.set_x(origin.x() - handle_size);
|
||||
RectF::new(origin, vec2f(handle_size, bounds.height()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ResizeHandleState {
|
||||
actual_dimension: Cell<f32>,
|
||||
custom_dimension: Cell<f32>,
|
||||
}
|
||||
|
||||
pub struct Resizable {
|
||||
side: Side,
|
||||
handle_size: f32,
|
||||
child: ElementBox,
|
||||
state: Rc<ResizeHandleState>,
|
||||
_state_handle: ElementStateHandle<Rc<ResizeHandleState>>,
|
||||
}
|
||||
|
||||
impl Resizable {
|
||||
pub fn new<Tag: 'static, T: View>(
|
||||
child: ElementBox,
|
||||
element_id: usize,
|
||||
side: Side,
|
||||
handle_size: f32,
|
||||
initial_size: f32,
|
||||
cx: &mut RenderContext<T>,
|
||||
) -> Self {
|
||||
let state_handle = cx.element_state::<Tag, Rc<ResizeHandleState>>(
|
||||
element_id,
|
||||
Rc::new(ResizeHandleState {
|
||||
actual_dimension: Cell::new(initial_size),
|
||||
custom_dimension: Cell::new(initial_size),
|
||||
}),
|
||||
);
|
||||
|
||||
let state = state_handle.read(cx).clone();
|
||||
|
||||
let child = Hook::new({
|
||||
let constrained = ConstrainedBox::new(child);
|
||||
match side.axis() {
|
||||
Axis::Horizontal => constrained.with_max_width(state.custom_dimension.get()),
|
||||
Axis::Vertical => constrained.with_max_height(state.custom_dimension.get()),
|
||||
}
|
||||
.boxed()
|
||||
})
|
||||
.on_after_layout({
|
||||
let state = state.clone();
|
||||
move |size, _| {
|
||||
state.actual_dimension.set(side.relevant_component(size));
|
||||
}
|
||||
})
|
||||
.boxed();
|
||||
|
||||
Self {
|
||||
side,
|
||||
child,
|
||||
handle_size,
|
||||
state,
|
||||
_state_handle: state_handle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_size(&self) -> f32 {
|
||||
self.state.actual_dimension.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Resizable {
|
||||
type LayoutState = ();
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: crate::SizeConstraint,
|
||||
cx: &mut crate::LayoutContext,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
(self.child.layout(constraint, cx), ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: pathfinder_geometry::rect::RectF,
|
||||
visible_bounds: pathfinder_geometry::rect::RectF,
|
||||
_child_size: &mut Self::LayoutState,
|
||||
cx: &mut crate::PaintContext,
|
||||
) -> Self::PaintState {
|
||||
cx.scene.push_stacking_context(None);
|
||||
|
||||
let handle_region = self.side.of_rect(bounds, self.handle_size);
|
||||
|
||||
enum ResizeHandle {}
|
||||
cx.scene.push_mouse_region(
|
||||
MouseRegion::new::<ResizeHandle>(
|
||||
cx.current_view_id(),
|
||||
self.side as usize,
|
||||
handle_region,
|
||||
)
|
||||
.on_down(MouseButton::Left, |_, _| {}) // This prevents the mouse down event from being propagated elsewhere
|
||||
.on_drag(MouseButton::Left, {
|
||||
let state = self.state.clone();
|
||||
let side = self.side;
|
||||
move |e, cx| {
|
||||
let prev_width = state.actual_dimension.get();
|
||||
state
|
||||
.custom_dimension
|
||||
.set(0f32.max(prev_width + side.compute_delta(e)).round());
|
||||
cx.notify();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
cx.scene.push_cursor_region(crate::CursorRegion {
|
||||
bounds: handle_region,
|
||||
style: match self.side.axis() {
|
||||
Axis::Horizontal => CursorStyle::ResizeLeftRight,
|
||||
Axis::Vertical => CursorStyle::ResizeUpDown,
|
||||
},
|
||||
});
|
||||
|
||||
cx.scene.pop_stacking_context();
|
||||
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &crate::Event,
|
||||
_bounds: pathfinder_geometry::rect::RectF,
|
||||
_visible_bounds: pathfinder_geometry::rect::RectF,
|
||||
_layout: &mut Self::LayoutState,
|
||||
_paint: &mut Self::PaintState,
|
||||
cx: &mut crate::EventContext,
|
||||
) -> bool {
|
||||
self.child.dispatch_event(event, cx)
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: std::ops::Range<usize>,
|
||||
_bounds: pathfinder_geometry::rect::RectF,
|
||||
_visible_bounds: pathfinder_geometry::rect::RectF,
|
||||
_layout: &Self::LayoutState,
|
||||
_paint: &Self::PaintState,
|
||||
cx: &crate::MeasurementContext,
|
||||
) -> Option<pathfinder_geometry::rect::RectF> {
|
||||
self.child.rect_for_text_range(range_utf16, cx)
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
_bounds: pathfinder_geometry::rect::RectF,
|
||||
_layout: &Self::LayoutState,
|
||||
_paint: &Self::PaintState,
|
||||
cx: &crate::DebugContext,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"child": self.child.debug(cx),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -291,7 +291,7 @@ mod tests {
|
|||
#[crate::test(self)]
|
||||
fn test_soft_wrapping_with_carriage_returns(cx: &mut MutableAppContext) {
|
||||
let (window_id, _) = cx.add_window(Default::default(), |_| TestView);
|
||||
let mut presenter = cx.build_presenter(window_id, Default::default());
|
||||
let mut presenter = cx.build_presenter(window_id, Default::default(), Default::default());
|
||||
fonts::with_font_cache(cx.font_cache().clone(), || {
|
||||
let mut text = Text::new("Hello\r\n".into(), Default::default()).with_soft_wrap(true);
|
||||
let (_, state) = text.layout(
|
||||
|
|
|
@ -62,7 +62,7 @@ impl Tooltip {
|
|||
struct ElementState<Tag>(Tag);
|
||||
struct MouseEventHandlerState<Tag>(Tag);
|
||||
|
||||
let state_handle = cx.element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
|
||||
let state_handle = cx.default_element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
|
||||
let state = state_handle.read(cx).clone();
|
||||
let tooltip = if state.visible.get() {
|
||||
let mut collapsed_tooltip = Self::render_tooltip(
|
||||
|
@ -84,15 +84,14 @@ impl Tooltip {
|
|||
})
|
||||
.boxed(),
|
||||
)
|
||||
.fit_mode(OverlayFitMode::FlipAlignment)
|
||||
.with_abs_position(state.position.get())
|
||||
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||
.with_anchor_position(state.position.get())
|
||||
.boxed(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let child =
|
||||
MouseEventHandler::new::<MouseEventHandlerState<Tag>, _, _>(id, cx, |_, _| child)
|
||||
let child = MouseEventHandler::<MouseEventHandlerState<Tag>>::new(id, cx, |_, _| child)
|
||||
.on_hover(move |e, cx| {
|
||||
let position = e.position;
|
||||
let window_id = cx.window_id();
|
||||
|
|
|
@ -6,7 +6,8 @@ use crate::{
|
|||
},
|
||||
json::{self, json},
|
||||
presenter::MeasurementContext,
|
||||
ElementBox, RenderContext, ScrollWheelEvent, View,
|
||||
scene::ScrollWheelRegionEvent,
|
||||
ElementBox, MouseRegion, RenderContext, ScrollWheelEvent, View,
|
||||
};
|
||||
use json::ToJson;
|
||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||
|
@ -50,6 +51,7 @@ pub struct UniformList {
|
|||
padding_top: f32,
|
||||
padding_bottom: f32,
|
||||
get_width_from_item: Option<usize>,
|
||||
view_id: usize,
|
||||
}
|
||||
|
||||
impl UniformList {
|
||||
|
@ -77,6 +79,7 @@ impl UniformList {
|
|||
padding_top: 0.,
|
||||
padding_bottom: 0.,
|
||||
get_width_from_item: None,
|
||||
view_id: cx.handle().id(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,7 +99,7 @@ impl UniformList {
|
|||
}
|
||||
|
||||
fn scroll(
|
||||
&self,
|
||||
state: UniformListState,
|
||||
_: Vector2F,
|
||||
mut delta: Vector2F,
|
||||
precise: bool,
|
||||
|
@ -107,7 +110,7 @@ impl UniformList {
|
|||
delta *= 20.;
|
||||
}
|
||||
|
||||
let mut state = self.state.0.borrow_mut();
|
||||
let mut state = state.0.borrow_mut();
|
||||
state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max);
|
||||
cx.notify();
|
||||
|
||||
|
@ -283,6 +286,28 @@ impl Element for UniformList {
|
|||
) -> Self::PaintState {
|
||||
cx.scene.push_layer(Some(bounds));
|
||||
|
||||
cx.scene.push_mouse_region(
|
||||
MouseRegion::new::<Self>(self.view_id, 0, visible_bounds).on_scroll({
|
||||
let scroll_max = layout.scroll_max;
|
||||
let state = self.state.clone();
|
||||
move |ScrollWheelRegionEvent {
|
||||
platform_event:
|
||||
ScrollWheelEvent {
|
||||
position,
|
||||
delta,
|
||||
precise,
|
||||
..
|
||||
},
|
||||
..
|
||||
},
|
||||
cx| {
|
||||
if !Self::scroll(state.clone(), position, delta, precise, scroll_max, cx) {
|
||||
cx.propogate_event();
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let mut item_origin = bounds.origin()
|
||||
- vec2f(
|
||||
0.,
|
||||
|
@ -300,7 +325,7 @@ impl Element for UniformList {
|
|||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
bounds: RectF,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
layout: &mut Self::LayoutState,
|
||||
_: &mut Self::PaintState,
|
||||
|
@ -311,20 +336,6 @@ impl Element for UniformList {
|
|||
handled = item.dispatch_event(event, cx) || handled;
|
||||
}
|
||||
|
||||
if let Event::ScrollWheel(ScrollWheelEvent {
|
||||
position,
|
||||
delta,
|
||||
precise,
|
||||
..
|
||||
}) = event
|
||||
{
|
||||
if bounds.contains_point(*position)
|
||||
&& self.scroll(*position, *delta, *precise, layout.scroll_max, cx)
|
||||
{
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
handled
|
||||
}
|
||||
|
||||
|
|
|
@ -39,6 +39,11 @@ pub trait Platform: Send + Sync {
|
|||
fn fonts(&self) -> Arc<dyn FontSystem>;
|
||||
|
||||
fn activate(&self, ignoring_other_apps: bool);
|
||||
fn hide(&self);
|
||||
fn hide_other_apps(&self);
|
||||
fn unhide_other_apps(&self);
|
||||
fn quit(&self);
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
id: usize,
|
||||
|
@ -46,10 +51,8 @@ pub trait Platform: Send + Sync {
|
|||
executor: Rc<executor::Foreground>,
|
||||
) -> Box<dyn Window>;
|
||||
fn key_window_id(&self) -> Option<usize>;
|
||||
fn hide(&self);
|
||||
fn hide_other_apps(&self);
|
||||
fn unhide_other_apps(&self);
|
||||
fn quit(&self);
|
||||
|
||||
fn add_status_item(&self) -> Box<dyn Window>;
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||
|
@ -107,7 +110,7 @@ pub trait InputHandler {
|
|||
fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF>;
|
||||
}
|
||||
|
||||
pub trait Window: WindowContext {
|
||||
pub trait Window {
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
fn on_event(&mut self, callback: Box<dyn FnMut(Event) -> bool>);
|
||||
fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>);
|
||||
|
@ -124,23 +127,52 @@ pub trait Window: WindowContext {
|
|||
fn minimize(&self);
|
||||
fn zoom(&self);
|
||||
fn toggle_full_screen(&self);
|
||||
}
|
||||
|
||||
pub trait WindowContext {
|
||||
fn size(&self) -> Vector2F;
|
||||
fn bounds(&self) -> RectF;
|
||||
fn content_size(&self) -> Vector2F;
|
||||
fn scale_factor(&self) -> f32;
|
||||
fn titlebar_height(&self) -> f32;
|
||||
fn present_scene(&mut self, scene: Scene);
|
||||
fn appearance(&self) -> Appearance;
|
||||
fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WindowOptions<'a> {
|
||||
pub bounds: WindowBounds,
|
||||
pub titlebar: Option<TitlebarOptions<'a>>,
|
||||
pub center: bool,
|
||||
pub kind: WindowKind,
|
||||
pub is_movable: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TitlebarOptions<'a> {
|
||||
pub title: Option<&'a str>,
|
||||
pub titlebar_appears_transparent: bool,
|
||||
pub appears_transparent: bool,
|
||||
pub traffic_light_position: Option<Vector2F>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Appearance {
|
||||
Light,
|
||||
VibrantLight,
|
||||
Dark,
|
||||
VibrantDark,
|
||||
}
|
||||
|
||||
impl Default for Appearance {
|
||||
fn default() -> Self {
|
||||
Self::Light
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum WindowKind {
|
||||
Normal,
|
||||
PopUp,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum WindowBounds {
|
||||
Maximized,
|
||||
|
@ -163,10 +195,17 @@ pub enum PromptLevel {
|
|||
pub enum CursorStyle {
|
||||
Arrow,
|
||||
ResizeLeftRight,
|
||||
ResizeUpDown,
|
||||
PointingHand,
|
||||
IBeam,
|
||||
}
|
||||
|
||||
impl Default for CursorStyle {
|
||||
fn default() -> Self {
|
||||
Self::Arrow
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct AppVersion {
|
||||
major: usize,
|
||||
|
@ -174,12 +213,6 @@ pub struct AppVersion {
|
|||
patch: usize,
|
||||
}
|
||||
|
||||
impl Default for CursorStyle {
|
||||
fn default() -> Self {
|
||||
Self::Arrow
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AppVersion {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
|
@ -246,9 +279,14 @@ impl<'a> Default for WindowOptions<'a> {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
bounds: WindowBounds::Maximized,
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: Default::default(),
|
||||
titlebar_appears_transparent: Default::default(),
|
||||
appears_transparent: Default::default(),
|
||||
traffic_light_position: Default::default(),
|
||||
}),
|
||||
center: false,
|
||||
kind: WindowKind::Normal,
|
||||
is_movable: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
mod appearance;
|
||||
mod atlas;
|
||||
mod dispatcher;
|
||||
mod event;
|
||||
|
@ -7,12 +8,14 @@ mod image_cache;
|
|||
mod platform;
|
||||
mod renderer;
|
||||
mod sprite_cache;
|
||||
mod status_item;
|
||||
mod window;
|
||||
|
||||
use cocoa::base::{BOOL, NO, YES};
|
||||
pub use dispatcher::Dispatcher;
|
||||
pub use fonts::FontSystem;
|
||||
use platform::{MacForegroundPlatform, MacPlatform};
|
||||
pub use renderer::Surface;
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
use window::Window;
|
||||
|
||||
|
|
37
crates/gpui/src/platform/mac/appearance.rs
Normal file
37
crates/gpui/src/platform/mac/appearance.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
use std::ffi::CStr;
|
||||
|
||||
use cocoa::{
|
||||
appkit::{NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight},
|
||||
base::id,
|
||||
foundation::NSString,
|
||||
};
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
|
||||
use crate::Appearance;
|
||||
|
||||
impl Appearance {
|
||||
pub unsafe fn from_native(appearance: id) -> Self {
|
||||
let name: id = msg_send![appearance, name];
|
||||
if name == NSAppearanceNameVibrantLight {
|
||||
Self::VibrantLight
|
||||
} else if name == NSAppearanceNameVibrantDark {
|
||||
Self::VibrantDark
|
||||
} else if name == NSAppearanceNameAqua {
|
||||
Self::Light
|
||||
} else if name == NSAppearanceNameDarkAqua {
|
||||
Self::Dark
|
||||
} else {
|
||||
println!(
|
||||
"unknown appearance: {:?}",
|
||||
CStr::from_ptr(name.UTF8String())
|
||||
);
|
||||
Self::Light
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[link(name = "AppKit", kind = "framework")]
|
||||
extern "C" {
|
||||
pub static NSAppearanceNameAqua: id;
|
||||
pub static NSAppearanceNameDarkAqua: id;
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
use super::{event::key_to_native, BoolExt as _, Dispatcher, FontSystem, Window};
|
||||
use super::{
|
||||
event::key_to_native, status_item::StatusItem, BoolExt as _, Dispatcher, FontSystem, Window,
|
||||
};
|
||||
use crate::{
|
||||
executor, keymap,
|
||||
platform::{self, CursorStyle},
|
||||
|
@ -50,6 +52,9 @@ use time::UtcOffset;
|
|||
#[allow(non_upper_case_globals)]
|
||||
const NSUTF8StringEncoding: NSUInteger = 4;
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
|
||||
|
||||
const MAC_PLATFORM_IVAR: &str = "platform";
|
||||
static mut APP_CLASS: *const Class = ptr::null();
|
||||
static mut APP_DELEGATE_CLASS: *const Class = ptr::null();
|
||||
|
@ -439,23 +444,6 @@ impl platform::Platform for MacPlatform {
|
|||
}
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
id: usize,
|
||||
options: platform::WindowOptions,
|
||||
executor: Rc<executor::Foreground>,
|
||||
) -> Box<dyn platform::Window> {
|
||||
Box::new(Window::open(id, options, executor, self.fonts()))
|
||||
}
|
||||
|
||||
fn key_window_id(&self) -> Option<usize> {
|
||||
Window::key_window_id()
|
||||
}
|
||||
|
||||
fn fonts(&self) -> Arc<dyn platform::FontSystem> {
|
||||
self.fonts.clone()
|
||||
}
|
||||
|
||||
fn hide(&self) {
|
||||
unsafe {
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
|
@ -497,6 +485,27 @@ impl platform::Platform for MacPlatform {
|
|||
}
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
id: usize,
|
||||
options: platform::WindowOptions,
|
||||
executor: Rc<executor::Foreground>,
|
||||
) -> Box<dyn platform::Window> {
|
||||
Box::new(Window::open(id, options, executor, self.fonts()))
|
||||
}
|
||||
|
||||
fn key_window_id(&self) -> Option<usize> {
|
||||
Window::key_window_id()
|
||||
}
|
||||
|
||||
fn add_status_item(&self) -> Box<dyn platform::Window> {
|
||||
Box::new(StatusItem::add(self.fonts()))
|
||||
}
|
||||
|
||||
fn fonts(&self) -> Arc<dyn platform::FontSystem> {
|
||||
self.fonts.clone()
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
unsafe {
|
||||
self.pasteboard.clearContents();
|
||||
|
@ -681,6 +690,7 @@ impl platform::Platform for MacPlatform {
|
|||
let cursor: id = match style {
|
||||
CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
|
||||
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
|
||||
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
|
||||
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
|
||||
CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
|
||||
};
|
||||
|
|
|
@ -8,16 +8,26 @@ use crate::{
|
|||
platform,
|
||||
scene::{Glyph, Icon, Image, ImageGlyph, Layer, Quad, Scene, Shadow, Underline},
|
||||
};
|
||||
use cocoa::foundation::NSUInteger;
|
||||
use cocoa::{
|
||||
base::{NO, YES},
|
||||
foundation::NSUInteger,
|
||||
quartzcore::AutoresizingMask,
|
||||
};
|
||||
use core_foundation::base::TCFType;
|
||||
use foreign_types::ForeignTypeRef;
|
||||
use log::warn;
|
||||
use metal::{MTLPixelFormat, MTLResourceOptions, NSRange};
|
||||
use media::core_video::{self, CVMetalTextureCache};
|
||||
use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
|
||||
use objc::{self, msg_send, sel, sel_impl};
|
||||
use shaders::ToFloat2 as _;
|
||||
use std::{collections::HashMap, ffi::c_void, iter::Peekable, mem, sync::Arc, vec};
|
||||
use std::{collections::HashMap, ffi::c_void, iter::Peekable, mem, ptr, sync::Arc, vec};
|
||||
|
||||
const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
|
||||
const INSTANCE_BUFFER_SIZE: usize = 8192 * 1024; // This is an arbitrary decision. There's probably a more optimal value.
|
||||
|
||||
pub struct Renderer {
|
||||
layer: metal::MetalLayer,
|
||||
command_queue: CommandQueue,
|
||||
sprite_cache: SpriteCache,
|
||||
image_cache: ImageCache,
|
||||
path_atlases: AtlasAllocator,
|
||||
|
@ -25,10 +35,12 @@ pub struct Renderer {
|
|||
shadow_pipeline_state: metal::RenderPipelineState,
|
||||
sprite_pipeline_state: metal::RenderPipelineState,
|
||||
image_pipeline_state: metal::RenderPipelineState,
|
||||
surface_pipeline_state: metal::RenderPipelineState,
|
||||
path_atlas_pipeline_state: metal::RenderPipelineState,
|
||||
underline_pipeline_state: metal::RenderPipelineState,
|
||||
unit_vertices: metal::Buffer,
|
||||
instances: metal::Buffer,
|
||||
cv_texture_cache: core_video::CVMetalTextureCache,
|
||||
}
|
||||
|
||||
struct PathSprite {
|
||||
|
@ -37,13 +49,37 @@ struct PathSprite {
|
|||
shader_data: shaders::GPUISprite,
|
||||
}
|
||||
|
||||
pub struct Surface {
|
||||
pub bounds: RectF,
|
||||
pub image_buffer: core_video::CVImageBuffer,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
pub fn new(
|
||||
device: metal::Device,
|
||||
pixel_format: metal::MTLPixelFormat,
|
||||
scale_factor: f32,
|
||||
fonts: Arc<dyn platform::FontSystem>,
|
||||
) -> Self {
|
||||
pub fn new(is_opaque: bool, fonts: Arc<dyn platform::FontSystem>) -> Self {
|
||||
const PIXEL_FORMAT: MTLPixelFormat = MTLPixelFormat::BGRA8Unorm;
|
||||
|
||||
let device: metal::Device = if let Some(device) = metal::Device::system_default() {
|
||||
device
|
||||
} else {
|
||||
log::error!("unable to access a compatible graphics device");
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let layer = metal::MetalLayer::new();
|
||||
layer.set_device(&device);
|
||||
layer.set_pixel_format(PIXEL_FORMAT);
|
||||
layer.set_presents_with_transaction(true);
|
||||
layer.set_opaque(is_opaque);
|
||||
unsafe {
|
||||
let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
|
||||
let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
|
||||
let _: () = msg_send![
|
||||
&*layer,
|
||||
setAutoresizingMask: AutoresizingMask::WIDTH_SIZABLE
|
||||
| AutoresizingMask::HEIGHT_SIZABLE
|
||||
];
|
||||
}
|
||||
|
||||
let library = device
|
||||
.new_library_with_data(SHADERS_METALLIB)
|
||||
.expect("error building metal library");
|
||||
|
@ -66,13 +102,8 @@ impl Renderer {
|
|||
MTLResourceOptions::StorageModeManaged,
|
||||
);
|
||||
|
||||
let sprite_cache = SpriteCache::new(
|
||||
device.clone(),
|
||||
vec2i(1024, 768),
|
||||
scale_factor,
|
||||
fonts.clone(),
|
||||
);
|
||||
let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768), scale_factor, fonts);
|
||||
let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), 1., fonts.clone());
|
||||
let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768), 1., fonts);
|
||||
let path_atlases =
|
||||
AtlasAllocator::new(device.clone(), build_path_atlas_texture_descriptor());
|
||||
let quad_pipeline_state = build_pipeline_state(
|
||||
|
@ -81,7 +112,7 @@ impl Renderer {
|
|||
"quad",
|
||||
"quad_vertex",
|
||||
"quad_fragment",
|
||||
pixel_format,
|
||||
PIXEL_FORMAT,
|
||||
);
|
||||
let shadow_pipeline_state = build_pipeline_state(
|
||||
&device,
|
||||
|
@ -89,7 +120,7 @@ impl Renderer {
|
|||
"shadow",
|
||||
"shadow_vertex",
|
||||
"shadow_fragment",
|
||||
pixel_format,
|
||||
PIXEL_FORMAT,
|
||||
);
|
||||
let sprite_pipeline_state = build_pipeline_state(
|
||||
&device,
|
||||
|
@ -97,7 +128,7 @@ impl Renderer {
|
|||
"sprite",
|
||||
"sprite_vertex",
|
||||
"sprite_fragment",
|
||||
pixel_format,
|
||||
PIXEL_FORMAT,
|
||||
);
|
||||
let image_pipeline_state = build_pipeline_state(
|
||||
&device,
|
||||
|
@ -105,7 +136,15 @@ impl Renderer {
|
|||
"image",
|
||||
"image_vertex",
|
||||
"image_fragment",
|
||||
pixel_format,
|
||||
PIXEL_FORMAT,
|
||||
);
|
||||
let surface_pipeline_state = build_pipeline_state(
|
||||
&device,
|
||||
&library,
|
||||
"surface",
|
||||
"surface_vertex",
|
||||
"surface_fragment",
|
||||
PIXEL_FORMAT,
|
||||
);
|
||||
let path_atlas_pipeline_state = build_path_atlas_pipeline_state(
|
||||
&device,
|
||||
|
@ -121,9 +160,12 @@ impl Renderer {
|
|||
"underline",
|
||||
"underline_vertex",
|
||||
"underline_fragment",
|
||||
pixel_format,
|
||||
PIXEL_FORMAT,
|
||||
);
|
||||
let cv_texture_cache = CVMetalTextureCache::new(device.as_ptr()).unwrap();
|
||||
Self {
|
||||
layer,
|
||||
command_queue: device.new_command_queue(),
|
||||
sprite_cache,
|
||||
image_cache,
|
||||
path_atlases,
|
||||
|
@ -131,20 +173,26 @@ impl Renderer {
|
|||
shadow_pipeline_state,
|
||||
sprite_pipeline_state,
|
||||
image_pipeline_state,
|
||||
surface_pipeline_state,
|
||||
path_atlas_pipeline_state,
|
||||
underline_pipeline_state,
|
||||
unit_vertices,
|
||||
instances,
|
||||
cv_texture_cache,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
scene: &Scene,
|
||||
drawable_size: Vector2F,
|
||||
command_buffer: &metal::CommandBufferRef,
|
||||
output: &metal::TextureRef,
|
||||
) {
|
||||
pub fn layer(&self) -> &metal::MetalLayerRef {
|
||||
&*self.layer
|
||||
}
|
||||
|
||||
pub fn render(&mut self, scene: &Scene) {
|
||||
let layer = self.layer.clone();
|
||||
let drawable_size = layer.drawable_size();
|
||||
let drawable = layer.next_drawable().unwrap();
|
||||
let command_queue = self.command_queue.clone();
|
||||
let command_buffer = command_queue.new_command_buffer();
|
||||
|
||||
self.sprite_cache.set_scale_factor(scene.scale_factor());
|
||||
self.image_cache.set_scale_factor(scene.scale_factor());
|
||||
|
||||
|
@ -155,15 +203,19 @@ impl Renderer {
|
|||
scene,
|
||||
path_sprites,
|
||||
&mut offset,
|
||||
drawable_size,
|
||||
vec2f(drawable_size.width as f32, drawable_size.height as f32),
|
||||
command_buffer,
|
||||
output,
|
||||
drawable.texture(),
|
||||
);
|
||||
self.instances.did_modify_range(NSRange {
|
||||
location: 0,
|
||||
length: offset as NSUInteger,
|
||||
});
|
||||
self.image_cache.finish_frame();
|
||||
|
||||
command_buffer.commit();
|
||||
command_buffer.wait_until_completed();
|
||||
drawable.present();
|
||||
}
|
||||
|
||||
fn render_path_atlases(
|
||||
|
@ -312,7 +364,8 @@ impl Renderer {
|
|||
color_attachment.set_texture(Some(output));
|
||||
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
|
||||
color_attachment.set_store_action(metal::MTLStoreAction::Store);
|
||||
color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 1.));
|
||||
let alpha = if self.layer.is_opaque() { 1. } else { 0. };
|
||||
color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., alpha));
|
||||
let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
|
||||
|
||||
command_encoder.set_viewport(metal::MTLViewport {
|
||||
|
@ -372,6 +425,13 @@ impl Renderer {
|
|||
drawable_size,
|
||||
command_encoder,
|
||||
);
|
||||
self.render_surfaces(
|
||||
layer.surfaces(),
|
||||
scale_factor,
|
||||
offset,
|
||||
drawable_size,
|
||||
command_encoder,
|
||||
);
|
||||
}
|
||||
|
||||
command_encoder.end_encoding();
|
||||
|
@ -768,6 +828,111 @@ impl Renderer {
|
|||
}
|
||||
}
|
||||
|
||||
fn render_surfaces(
|
||||
&mut self,
|
||||
surfaces: &[Surface],
|
||||
scale_factor: f32,
|
||||
offset: &mut usize,
|
||||
drawable_size: Vector2F,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
) {
|
||||
if surfaces.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
command_encoder.set_render_pipeline_state(&self.surface_pipeline_state);
|
||||
command_encoder.set_vertex_buffer(
|
||||
shaders::GPUISurfaceVertexInputIndex_GPUISurfaceVertexInputIndexVertices as u64,
|
||||
Some(&self.unit_vertices),
|
||||
0,
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
shaders::GPUISurfaceVertexInputIndex_GPUISurfaceVertexInputIndexViewportSize as u64,
|
||||
mem::size_of::<shaders::vector_float2>() as u64,
|
||||
[drawable_size.to_float2()].as_ptr() as *const c_void,
|
||||
);
|
||||
|
||||
for surface in surfaces {
|
||||
let origin = surface.bounds.origin() * scale_factor;
|
||||
let source_size = vec2i(
|
||||
surface.image_buffer.width() as i32,
|
||||
surface.image_buffer.height() as i32,
|
||||
);
|
||||
let target_size = surface.bounds.size() * scale_factor;
|
||||
|
||||
assert_eq!(
|
||||
surface.image_buffer.pixel_format_type(),
|
||||
core_video::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
|
||||
);
|
||||
|
||||
let y_texture = self
|
||||
.cv_texture_cache
|
||||
.create_texture_from_image(
|
||||
surface.image_buffer.as_concrete_TypeRef(),
|
||||
ptr::null(),
|
||||
MTLPixelFormat::R8Unorm,
|
||||
surface.image_buffer.plane_width(0),
|
||||
surface.image_buffer.plane_height(0),
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
let cb_cr_texture = self
|
||||
.cv_texture_cache
|
||||
.create_texture_from_image(
|
||||
surface.image_buffer.as_concrete_TypeRef(),
|
||||
ptr::null(),
|
||||
MTLPixelFormat::RG8Unorm,
|
||||
surface.image_buffer.plane_width(1),
|
||||
surface.image_buffer.plane_height(1),
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
align_offset(offset);
|
||||
let next_offset = *offset + mem::size_of::<shaders::GPUISurface>();
|
||||
assert!(
|
||||
next_offset <= INSTANCE_BUFFER_SIZE,
|
||||
"instance buffer exhausted"
|
||||
);
|
||||
|
||||
command_encoder.set_vertex_buffer(
|
||||
shaders::GPUISurfaceVertexInputIndex_GPUISurfaceVertexInputIndexSurfaces as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
shaders::GPUISurfaceVertexInputIndex_GPUISurfaceVertexInputIndexAtlasSize as u64,
|
||||
mem::size_of::<shaders::vector_float2>() as u64,
|
||||
[source_size.to_float2()].as_ptr() as *const c_void,
|
||||
);
|
||||
command_encoder.set_fragment_texture(
|
||||
shaders::GPUISurfaceFragmentInputIndex_GPUISurfaceFragmentInputIndexYAtlas as u64,
|
||||
Some(y_texture.as_texture_ref()),
|
||||
);
|
||||
command_encoder.set_fragment_texture(
|
||||
shaders::GPUISurfaceFragmentInputIndex_GPUISurfaceFragmentInputIndexCbCrAtlas
|
||||
as u64,
|
||||
Some(cb_cr_texture.as_texture_ref()),
|
||||
);
|
||||
|
||||
unsafe {
|
||||
let buffer_contents = (self.instances.contents() as *mut u8).add(*offset)
|
||||
as *mut shaders::GPUISurface;
|
||||
std::ptr::write(
|
||||
buffer_contents,
|
||||
shaders::GPUISurface {
|
||||
origin: origin.to_float2(),
|
||||
target_size: target_size.to_float2(),
|
||||
source_size: source_size.to_float2(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6);
|
||||
*offset = next_offset;
|
||||
}
|
||||
}
|
||||
|
||||
fn render_path_sprites(
|
||||
&mut self,
|
||||
layer_id: usize,
|
||||
|
|
|
@ -1,19 +1,16 @@
|
|||
#include <simd/simd.h>
|
||||
|
||||
typedef struct
|
||||
{
|
||||
typedef struct {
|
||||
vector_float2 viewport_size;
|
||||
} GPUIUniforms;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
typedef enum {
|
||||
GPUIQuadInputIndexVertices = 0,
|
||||
GPUIQuadInputIndexQuads = 1,
|
||||
GPUIQuadInputIndexUniforms = 2,
|
||||
} GPUIQuadInputIndex;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
typedef struct {
|
||||
vector_float2 origin;
|
||||
vector_float2 size;
|
||||
vector_uchar4 background_color;
|
||||
|
@ -25,15 +22,13 @@ typedef struct
|
|||
float corner_radius;
|
||||
} GPUIQuad;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
typedef enum {
|
||||
GPUIShadowInputIndexVertices = 0,
|
||||
GPUIShadowInputIndexShadows = 1,
|
||||
GPUIShadowInputIndexUniforms = 2,
|
||||
} GPUIShadowInputIndex;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
typedef struct {
|
||||
vector_float2 origin;
|
||||
vector_float2 size;
|
||||
float corner_radius;
|
||||
|
@ -41,21 +36,18 @@ typedef struct
|
|||
vector_uchar4 color;
|
||||
} GPUIShadow;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
typedef enum {
|
||||
GPUISpriteVertexInputIndexVertices = 0,
|
||||
GPUISpriteVertexInputIndexSprites = 1,
|
||||
GPUISpriteVertexInputIndexViewportSize = 2,
|
||||
GPUISpriteVertexInputIndexAtlasSize = 3,
|
||||
} GPUISpriteVertexInputIndex;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
typedef enum {
|
||||
GPUISpriteFragmentInputIndexAtlas = 0,
|
||||
} GPUISpriteFragmentInputIndex;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
typedef struct {
|
||||
vector_float2 origin;
|
||||
vector_float2 target_size;
|
||||
vector_float2 source_size;
|
||||
|
@ -64,35 +56,30 @@ typedef struct
|
|||
uint8_t compute_winding;
|
||||
} GPUISprite;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
typedef enum {
|
||||
GPUIPathAtlasVertexInputIndexVertices = 0,
|
||||
GPUIPathAtlasVertexInputIndexAtlasSize = 1,
|
||||
} GPUIPathAtlasVertexInputIndex;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
typedef struct {
|
||||
vector_float2 xy_position;
|
||||
vector_float2 st_position;
|
||||
vector_float2 clip_rect_origin;
|
||||
vector_float2 clip_rect_size;
|
||||
} GPUIPathVertex;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
typedef enum {
|
||||
GPUIImageVertexInputIndexVertices = 0,
|
||||
GPUIImageVertexInputIndexImages = 1,
|
||||
GPUIImageVertexInputIndexViewportSize = 2,
|
||||
GPUIImageVertexInputIndexAtlasSize = 3,
|
||||
} GPUIImageVertexInputIndex;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
typedef enum {
|
||||
GPUIImageFragmentInputIndexAtlas = 0,
|
||||
} GPUIImageFragmentInputIndex;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
typedef struct {
|
||||
vector_float2 origin;
|
||||
vector_float2 target_size;
|
||||
vector_float2 source_size;
|
||||
|
@ -105,15 +92,31 @@ typedef struct
|
|||
float corner_radius;
|
||||
} GPUIImage;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
typedef enum {
|
||||
GPUISurfaceVertexInputIndexVertices = 0,
|
||||
GPUISurfaceVertexInputIndexSurfaces = 1,
|
||||
GPUISurfaceVertexInputIndexViewportSize = 2,
|
||||
GPUISurfaceVertexInputIndexAtlasSize = 3,
|
||||
} GPUISurfaceVertexInputIndex;
|
||||
|
||||
typedef enum {
|
||||
GPUISurfaceFragmentInputIndexYAtlas = 0,
|
||||
GPUISurfaceFragmentInputIndexCbCrAtlas = 1,
|
||||
} GPUISurfaceFragmentInputIndex;
|
||||
|
||||
typedef struct {
|
||||
vector_float2 origin;
|
||||
vector_float2 target_size;
|
||||
vector_float2 source_size;
|
||||
} GPUISurface;
|
||||
|
||||
typedef enum {
|
||||
GPUIUnderlineInputIndexVertices = 0,
|
||||
GPUIUnderlineInputIndexUnderlines = 1,
|
||||
GPUIUnderlineInputIndexUniforms = 2,
|
||||
} GPUIUnderlineInputIndex;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
typedef struct {
|
||||
vector_float2 origin;
|
||||
vector_float2 size;
|
||||
float thickness;
|
||||
|
|
|
@ -263,6 +263,54 @@ fragment float4 image_fragment(
|
|||
return quad_sdf(input);
|
||||
}
|
||||
|
||||
vertex QuadFragmentInput surface_vertex(
|
||||
uint unit_vertex_id [[vertex_id]],
|
||||
uint image_id [[instance_id]],
|
||||
constant float2 *unit_vertices [[buffer(GPUISurfaceVertexInputIndexVertices)]],
|
||||
constant GPUISurface *images [[buffer(GPUISurfaceVertexInputIndexSurfaces)]],
|
||||
constant float2 *viewport_size [[buffer(GPUISurfaceVertexInputIndexViewportSize)]],
|
||||
constant float2 *atlas_size [[buffer(GPUISurfaceVertexInputIndexAtlasSize)]]
|
||||
) {
|
||||
float2 unit_vertex = unit_vertices[unit_vertex_id];
|
||||
GPUISurface image = images[image_id];
|
||||
float2 position = unit_vertex * image.target_size + image.origin;
|
||||
float4 device_position = to_device_position(position, *viewport_size);
|
||||
float2 atlas_position = (unit_vertex * image.source_size) / *atlas_size;
|
||||
|
||||
return QuadFragmentInput {
|
||||
device_position,
|
||||
atlas_position,
|
||||
image.origin,
|
||||
image.target_size,
|
||||
float4(0.),
|
||||
0.,
|
||||
0.,
|
||||
0.,
|
||||
0.,
|
||||
float4(0.),
|
||||
0.,
|
||||
};
|
||||
}
|
||||
|
||||
fragment float4 surface_fragment(
|
||||
QuadFragmentInput input [[stage_in]],
|
||||
texture2d<float> y_atlas [[ texture(GPUISurfaceFragmentInputIndexYAtlas) ]],
|
||||
texture2d<float> cb_cr_atlas [[ texture(GPUISurfaceFragmentInputIndexCbCrAtlas) ]]
|
||||
) {
|
||||
constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
|
||||
const float4x4 ycbcrToRGBTransform = float4x4(
|
||||
float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f),
|
||||
float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f),
|
||||
float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f),
|
||||
float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f)
|
||||
);
|
||||
float4 ycbcr = float4(y_atlas.sample(atlas_sampler, input.atlas_position).r,
|
||||
cb_cr_atlas.sample(atlas_sampler, input.atlas_position).rg, 1.0);
|
||||
|
||||
input.background_color = ycbcrToRGBTransform * ycbcr;
|
||||
return quad_sdf(input);
|
||||
}
|
||||
|
||||
struct PathAtlasVertexOutput {
|
||||
float4 position [[position]];
|
||||
float2 st_position;
|
||||
|
|
367
crates/gpui/src/platform/mac/status_item.rs
Normal file
367
crates/gpui/src/platform/mac/status_item.rs
Normal file
|
@ -0,0 +1,367 @@
|
|||
use crate::{
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
platform::{
|
||||
self,
|
||||
mac::{platform::NSViewLayerContentsRedrawDuringViewResize, renderer::Renderer},
|
||||
},
|
||||
Event, FontSystem, Scene,
|
||||
};
|
||||
use cocoa::{
|
||||
appkit::{NSScreen, NSSquareStatusItemLength, NSStatusBar, NSStatusItem, NSView, NSWindow},
|
||||
base::{id, nil, YES},
|
||||
foundation::{NSPoint, NSRect, NSSize},
|
||||
};
|
||||
use ctor::ctor;
|
||||
use foreign_types::ForeignTypeRef;
|
||||
use objc::{
|
||||
class,
|
||||
declare::ClassDecl,
|
||||
msg_send,
|
||||
rc::StrongPtr,
|
||||
runtime::{Class, Object, Protocol, Sel},
|
||||
sel, sel_impl,
|
||||
};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
ffi::c_void,
|
||||
ptr,
|
||||
rc::{Rc, Weak},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
static mut VIEW_CLASS: *const Class = ptr::null();
|
||||
const STATE_IVAR: &str = "state";
|
||||
|
||||
#[ctor]
|
||||
unsafe fn build_classes() {
|
||||
VIEW_CLASS = {
|
||||
let mut decl = ClassDecl::new("GPUIStatusItemView", class!(NSView)).unwrap();
|
||||
decl.add_ivar::<*mut c_void>(STATE_IVAR);
|
||||
|
||||
decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel));
|
||||
|
||||
decl.add_method(
|
||||
sel!(mouseDown:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(mouseUp:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(rightMouseDown:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(rightMouseUp:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(otherMouseDown:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(otherMouseUp:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(mouseMoved:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(mouseDragged:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(scrollWheel:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(flagsChanged:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(makeBackingLayer),
|
||||
make_backing_layer as extern "C" fn(&Object, Sel) -> id,
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(viewDidChangeEffectiveAppearance),
|
||||
view_did_change_effective_appearance as extern "C" fn(&Object, Sel),
|
||||
);
|
||||
|
||||
decl.add_protocol(Protocol::get("CALayerDelegate").unwrap());
|
||||
decl.add_method(
|
||||
sel!(displayLayer:),
|
||||
display_layer as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
|
||||
decl.register()
|
||||
};
|
||||
}
|
||||
|
||||
pub struct StatusItem(Rc<RefCell<StatusItemState>>);
|
||||
|
||||
struct StatusItemState {
|
||||
native_item: StrongPtr,
|
||||
native_view: StrongPtr,
|
||||
renderer: Renderer,
|
||||
scene: Option<Scene>,
|
||||
event_callback: Option<Box<dyn FnMut(Event) -> bool>>,
|
||||
appearance_changed_callback: Option<Box<dyn FnMut()>>,
|
||||
}
|
||||
|
||||
impl StatusItem {
|
||||
pub fn add(fonts: Arc<dyn FontSystem>) -> Self {
|
||||
unsafe {
|
||||
let renderer = Renderer::new(false, fonts);
|
||||
let status_bar = NSStatusBar::systemStatusBar(nil);
|
||||
let native_item =
|
||||
StrongPtr::retain(status_bar.statusItemWithLength_(NSSquareStatusItemLength));
|
||||
|
||||
let button = native_item.button();
|
||||
let _: () = msg_send![button, setHidden: YES];
|
||||
|
||||
let native_view = msg_send![VIEW_CLASS, alloc];
|
||||
let state = Rc::new(RefCell::new(StatusItemState {
|
||||
native_item,
|
||||
native_view: StrongPtr::new(native_view),
|
||||
renderer,
|
||||
scene: None,
|
||||
event_callback: None,
|
||||
appearance_changed_callback: None,
|
||||
}));
|
||||
|
||||
let parent_view = button.superview().superview();
|
||||
NSView::initWithFrame_(
|
||||
native_view,
|
||||
NSRect::new(NSPoint::new(0., 0.), NSView::frame(parent_view).size),
|
||||
);
|
||||
(*native_view).set_ivar(
|
||||
STATE_IVAR,
|
||||
Weak::into_raw(Rc::downgrade(&state)) as *const c_void,
|
||||
);
|
||||
native_view.setWantsBestResolutionOpenGLSurface_(YES);
|
||||
native_view.setWantsLayer(YES);
|
||||
let _: () = msg_send![
|
||||
native_view,
|
||||
setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize
|
||||
];
|
||||
|
||||
parent_view.addSubview_(native_view);
|
||||
|
||||
{
|
||||
let state = state.borrow();
|
||||
let layer = state.renderer.layer();
|
||||
let scale_factor = state.scale_factor();
|
||||
let size = state.content_size() * scale_factor;
|
||||
layer.set_contents_scale(scale_factor.into());
|
||||
layer.set_drawable_size(metal::CGSize::new(size.x().into(), size.y().into()));
|
||||
}
|
||||
|
||||
Self(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::Window for StatusItem {
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn on_event(&mut self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
|
||||
self.0.borrow_mut().event_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>) {
|
||||
self.0.borrow_mut().appearance_changed_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn on_active_status_change(&mut self, _: Box<dyn FnMut(bool)>) {}
|
||||
|
||||
fn on_resize(&mut self, _: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_fullscreen(&mut self, _: Box<dyn FnMut(bool)>) {}
|
||||
|
||||
fn on_should_close(&mut self, _: Box<dyn FnMut() -> bool>) {}
|
||||
|
||||
fn on_close(&mut self, _: Box<dyn FnOnce()>) {}
|
||||
|
||||
fn set_input_handler(&mut self, _: Box<dyn crate::InputHandler>) {}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_: crate::PromptLevel,
|
||||
_: &str,
|
||||
_: &[&str],
|
||||
) -> postage::oneshot::Receiver<usize> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn activate(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_title(&mut self, _: &str) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_edited(&mut self, _: bool) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn show_character_palette(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn minimize(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn zoom(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn toggle_full_screen(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn bounds(&self) -> RectF {
|
||||
self.0.borrow().bounds()
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Vector2F {
|
||||
self.0.borrow().content_size()
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
self.0.borrow().scale_factor()
|
||||
}
|
||||
|
||||
fn titlebar_height(&self) -> f32 {
|
||||
0.
|
||||
}
|
||||
|
||||
fn present_scene(&mut self, scene: Scene) {
|
||||
self.0.borrow_mut().scene = Some(scene);
|
||||
unsafe {
|
||||
let _: () = msg_send![*self.0.borrow().native_view, setNeedsDisplay: YES];
|
||||
}
|
||||
}
|
||||
|
||||
fn appearance(&self) -> crate::Appearance {
|
||||
unsafe {
|
||||
let appearance: id =
|
||||
msg_send![self.0.borrow().native_item.button(), effectiveAppearance];
|
||||
crate::Appearance::from_native(appearance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemState {
|
||||
fn bounds(&self) -> RectF {
|
||||
unsafe {
|
||||
let window: id = msg_send![self.native_item.button(), window];
|
||||
let screen_frame = window.screen().visibleFrame();
|
||||
let window_frame = NSWindow::frame(window);
|
||||
let origin = vec2f(
|
||||
window_frame.origin.x as f32,
|
||||
(window_frame.origin.y - screen_frame.size.height - window_frame.size.height)
|
||||
as f32,
|
||||
);
|
||||
let size = vec2f(
|
||||
window_frame.size.width as f32,
|
||||
window_frame.size.height as f32,
|
||||
);
|
||||
RectF::new(origin, size)
|
||||
}
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Vector2F {
|
||||
unsafe {
|
||||
let NSSize { width, height, .. } =
|
||||
NSView::frame(self.native_item.button().superview().superview()).size;
|
||||
vec2f(width as f32, height as f32)
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
unsafe {
|
||||
let window: id = msg_send![self.native_item.button(), window];
|
||||
NSScreen::backingScaleFactor(window.screen()) as f32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn dealloc_view(this: &Object, _: Sel) {
|
||||
unsafe {
|
||||
drop_state(this);
|
||||
|
||||
let _: () = msg_send![super(this, class!(NSView)), dealloc];
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
||||
unsafe {
|
||||
if let Some(state) = get_state(this).upgrade() {
|
||||
let mut state_borrow = state.as_ref().borrow_mut();
|
||||
if let Some(event) =
|
||||
Event::from_native(native_event, Some(state_borrow.content_size().y()))
|
||||
{
|
||||
if let Some(mut callback) = state_borrow.event_callback.take() {
|
||||
drop(state_borrow);
|
||||
callback(event);
|
||||
state.borrow_mut().event_callback = Some(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id {
|
||||
if let Some(state) = unsafe { get_state(this).upgrade() } {
|
||||
let state = state.borrow();
|
||||
state.renderer.layer().as_ptr() as id
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
|
||||
unsafe {
|
||||
if let Some(state) = get_state(this).upgrade() {
|
||||
let mut state = state.borrow_mut();
|
||||
if let Some(scene) = state.scene.take() {
|
||||
state.renderer.render(&scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) {
|
||||
unsafe {
|
||||
if let Some(state) = get_state(this).upgrade() {
|
||||
let mut state_borrow = state.as_ref().borrow_mut();
|
||||
if let Some(mut callback) = state_borrow.appearance_changed_callback.take() {
|
||||
drop(state_borrow);
|
||||
callback();
|
||||
state.borrow_mut().appearance_changed_callback = Some(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_state(object: &Object) -> Weak<RefCell<StatusItemState>> {
|
||||
let raw: *mut c_void = *object.get_ivar(STATE_IVAR);
|
||||
let weak1 = Weak::from_raw(raw as *mut RefCell<StatusItemState>);
|
||||
let weak2 = weak1.clone();
|
||||
let _ = Weak::into_raw(weak1);
|
||||
weak2
|
||||
}
|
||||
|
||||
unsafe fn drop_state(object: &Object) {
|
||||
let raw: *const c_void = *object.get_ivar(STATE_IVAR);
|
||||
Weak::from_raw(raw as *const RefCell<StatusItemState>);
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
use super::{geometry::RectFExt, renderer::Renderer};
|
||||
use crate::{
|
||||
executor,
|
||||
geometry::{
|
||||
|
@ -6,25 +5,30 @@ use crate::{
|
|||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
keymap::Keystroke,
|
||||
platform::{self, Event, WindowBounds, WindowContext},
|
||||
mac::platform::NSViewLayerContentsRedrawDuringViewResize,
|
||||
platform::{
|
||||
self,
|
||||
mac::{geometry::RectFExt, renderer::Renderer},
|
||||
Event, WindowBounds,
|
||||
},
|
||||
InputHandler, KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
|
||||
MouseMovedEvent, Scene,
|
||||
MouseMovedEvent, Scene, WindowKind,
|
||||
};
|
||||
use block::ConcreteBlock;
|
||||
use cocoa::{
|
||||
appkit::{
|
||||
CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable,
|
||||
NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowStyleMask,
|
||||
NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
|
||||
NSWindowStyleMask,
|
||||
},
|
||||
base::{id, nil},
|
||||
foundation::{
|
||||
NSAutoreleasePool, NSInteger, NSNotFound, NSPoint, NSRect, NSSize, NSString, NSUInteger,
|
||||
},
|
||||
quartzcore::AutoresizingMask,
|
||||
};
|
||||
use core_graphics::display::CGRect;
|
||||
use ctor::ctor;
|
||||
use foreign_types::ForeignType as _;
|
||||
use foreign_types::ForeignTypeRef;
|
||||
use objc::{
|
||||
class,
|
||||
declare::ClassDecl,
|
||||
|
@ -51,8 +55,25 @@ use std::{
|
|||
const WINDOW_STATE_IVAR: &str = "windowState";
|
||||
|
||||
static mut WINDOW_CLASS: *const Class = ptr::null();
|
||||
static mut PANEL_CLASS: *const Class = ptr::null();
|
||||
static mut VIEW_CLASS: *const Class = ptr::null();
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
|
||||
unsafe { NSWindowStyleMask::from_bits_unchecked(1 << 7) };
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSNormalWindowLevel: NSInteger = 0;
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSPopUpWindowLevel: NSInteger = 101;
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSTrackingMouseMoved: NSUInteger = 0x02;
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSTrackingActiveAlways: NSUInteger = 0x80;
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSTrackingInVisibleRect: NSUInteger = 0x200;
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSWindowAnimationBehaviorUtilityWindow: NSInteger = 4;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
struct NSRange {
|
||||
|
@ -103,55 +124,10 @@ unsafe impl objc::Encode for NSRange {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
|
||||
|
||||
#[ctor]
|
||||
unsafe fn build_classes() {
|
||||
WINDOW_CLASS = {
|
||||
let mut decl = ClassDecl::new("GPUIWindow", class!(NSWindow)).unwrap();
|
||||
decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
|
||||
decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(&Object, Sel));
|
||||
decl.add_method(
|
||||
sel!(canBecomeMainWindow),
|
||||
yes as extern "C" fn(&Object, Sel) -> BOOL,
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(canBecomeKeyWindow),
|
||||
yes as extern "C" fn(&Object, Sel) -> BOOL,
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(sendEvent:),
|
||||
send_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(windowDidResize:),
|
||||
window_did_resize as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(windowWillEnterFullScreen:),
|
||||
window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(windowWillExitFullScreen:),
|
||||
window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(windowDidBecomeKey:),
|
||||
window_did_change_key_status as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(windowDidResignKey:),
|
||||
window_did_change_key_status as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(windowShouldClose:),
|
||||
window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
|
||||
);
|
||||
decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel));
|
||||
decl.register()
|
||||
};
|
||||
|
||||
WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow));
|
||||
PANEL_CLASS = build_window_class("GPUIPanel", class!(NSPanel));
|
||||
VIEW_CLASS = {
|
||||
let mut decl = ClassDecl::new("GPUIView", class!(NSView)).unwrap();
|
||||
decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
|
||||
|
@ -265,6 +241,10 @@ unsafe fn build_classes() {
|
|||
attributed_substring_for_proposed_range
|
||||
as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id,
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(viewDidChangeEffectiveAppearance),
|
||||
view_did_change_effective_appearance as extern "C" fn(&Object, Sel),
|
||||
);
|
||||
|
||||
// Suppress beep on keystrokes with modifier keys.
|
||||
decl.add_method(
|
||||
|
@ -276,6 +256,50 @@ unsafe fn build_classes() {
|
|||
};
|
||||
}
|
||||
|
||||
unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const Class {
|
||||
let mut decl = ClassDecl::new(name, superclass).unwrap();
|
||||
decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
|
||||
decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(&Object, Sel));
|
||||
decl.add_method(
|
||||
sel!(canBecomeMainWindow),
|
||||
yes as extern "C" fn(&Object, Sel) -> BOOL,
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(canBecomeKeyWindow),
|
||||
yes as extern "C" fn(&Object, Sel) -> BOOL,
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(sendEvent:),
|
||||
send_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(windowDidResize:),
|
||||
window_did_resize as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(windowWillEnterFullScreen:),
|
||||
window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(windowWillExitFullScreen:),
|
||||
window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(windowDidBecomeKey:),
|
||||
window_did_change_key_status as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(windowDidResignKey:),
|
||||
window_did_change_key_status as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(windowShouldClose:),
|
||||
window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
|
||||
);
|
||||
decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel));
|
||||
decl.register()
|
||||
}
|
||||
|
||||
pub struct Window(Rc<RefCell<WindowState>>);
|
||||
|
||||
///Used to track what the IME does when we send it a keystroke.
|
||||
|
@ -299,6 +323,7 @@ struct WindowState {
|
|||
fullscreen_callback: Option<Box<dyn FnMut(bool)>>,
|
||||
should_close_callback: Option<Box<dyn FnMut() -> bool>>,
|
||||
close_callback: Option<Box<dyn FnOnce()>>,
|
||||
appearance_changed_callback: Option<Box<dyn FnMut()>>,
|
||||
input_handler: Option<Box<dyn InputHandler>>,
|
||||
pending_key_down: Option<(KeyDownEvent, Option<InsertText>)>,
|
||||
performed_key_equivalent: bool,
|
||||
|
@ -306,9 +331,7 @@ struct WindowState {
|
|||
executor: Rc<executor::Foreground>,
|
||||
scene_to_render: Option<Scene>,
|
||||
renderer: Renderer,
|
||||
command_queue: metal::CommandQueue,
|
||||
last_fresh_keydown: Option<Keystroke>,
|
||||
layer: id,
|
||||
traffic_light_position: Option<Vector2F>,
|
||||
previous_modifiers_changed_event: Option<Event>,
|
||||
//State tracking what the IME did after the last request
|
||||
|
@ -329,57 +352,58 @@ impl Window {
|
|||
executor: Rc<executor::Foreground>,
|
||||
fonts: Arc<dyn platform::FontSystem>,
|
||||
) -> Self {
|
||||
const PIXEL_FORMAT: metal::MTLPixelFormat = metal::MTLPixelFormat::BGRA8Unorm;
|
||||
|
||||
unsafe {
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
|
||||
let frame = match options.bounds {
|
||||
WindowBounds::Maximized => RectF::new(Default::default(), vec2f(1024., 768.)),
|
||||
WindowBounds::Fixed(rect) => rect,
|
||||
}
|
||||
.to_ns_rect();
|
||||
let mut style_mask = NSWindowStyleMask::NSClosableWindowMask
|
||||
let mut style_mask;
|
||||
if let Some(titlebar) = options.titlebar.as_ref() {
|
||||
style_mask = NSWindowStyleMask::NSClosableWindowMask
|
||||
| NSWindowStyleMask::NSMiniaturizableWindowMask
|
||||
| NSWindowStyleMask::NSResizableWindowMask
|
||||
| NSWindowStyleMask::NSTitledWindowMask;
|
||||
|
||||
if options.titlebar_appears_transparent {
|
||||
if titlebar.appears_transparent {
|
||||
style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask;
|
||||
}
|
||||
} else {
|
||||
style_mask = NSWindowStyleMask::NSTitledWindowMask
|
||||
| NSWindowStyleMask::NSFullSizeContentViewWindowMask;
|
||||
}
|
||||
|
||||
let native_window: id = msg_send![WINDOW_CLASS, alloc];
|
||||
let native_window: id = match options.kind {
|
||||
WindowKind::Normal => msg_send![WINDOW_CLASS, alloc],
|
||||
WindowKind::PopUp => {
|
||||
style_mask |= NSWindowStyleMaskNonactivatingPanel;
|
||||
msg_send![PANEL_CLASS, alloc]
|
||||
}
|
||||
};
|
||||
let native_window = native_window.initWithContentRect_styleMask_backing_defer_(
|
||||
frame,
|
||||
RectF::new(Default::default(), vec2f(1024., 768.)).to_ns_rect(),
|
||||
style_mask,
|
||||
NSBackingStoreBuffered,
|
||||
NO,
|
||||
);
|
||||
assert!(!native_window.is_null());
|
||||
|
||||
if matches!(options.bounds, WindowBounds::Maximized) {
|
||||
let screen = native_window.screen();
|
||||
match options.bounds {
|
||||
WindowBounds::Maximized => {
|
||||
native_window.setFrame_display_(screen.visibleFrame(), YES);
|
||||
}
|
||||
|
||||
let device: metal::Device = if let Some(device) = metal::Device::system_default() {
|
||||
device
|
||||
} else {
|
||||
log::error!("unable to access a compatible graphics device");
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let layer: id = msg_send![class!(CAMetalLayer), layer];
|
||||
let _: () = msg_send![layer, setDevice: device.as_ptr()];
|
||||
let _: () = msg_send![layer, setPixelFormat: PIXEL_FORMAT];
|
||||
let _: () = msg_send![layer, setAllowsNextDrawableTimeout: NO];
|
||||
let _: () = msg_send![layer, setNeedsDisplayOnBoundsChange: YES];
|
||||
let _: () = msg_send![layer, setPresentsWithTransaction: YES];
|
||||
let _: () = msg_send![
|
||||
layer,
|
||||
setAutoresizingMask: AutoresizingMask::WIDTH_SIZABLE
|
||||
| AutoresizingMask::HEIGHT_SIZABLE
|
||||
];
|
||||
WindowBounds::Fixed(top_left_bounds) => {
|
||||
let frame = screen.visibleFrame();
|
||||
let bottom_left_bounds = RectF::new(
|
||||
vec2f(
|
||||
top_left_bounds.origin_x(),
|
||||
frame.size.height as f32
|
||||
- top_left_bounds.origin_y()
|
||||
- top_left_bounds.height(),
|
||||
),
|
||||
top_left_bounds.size(),
|
||||
);
|
||||
native_window.setFrame_display_(bottom_left_bounds.to_ns_rect(), YES);
|
||||
}
|
||||
}
|
||||
|
||||
let native_view: id = msg_send![VIEW_CLASS, alloc];
|
||||
let native_view = NSView::init(native_view);
|
||||
|
@ -394,22 +418,19 @@ impl Window {
|
|||
close_callback: None,
|
||||
activate_callback: None,
|
||||
fullscreen_callback: None,
|
||||
appearance_changed_callback: None,
|
||||
input_handler: None,
|
||||
pending_key_down: None,
|
||||
performed_key_equivalent: false,
|
||||
synthetic_drag_counter: 0,
|
||||
executor,
|
||||
scene_to_render: Default::default(),
|
||||
renderer: Renderer::new(
|
||||
device.clone(),
|
||||
PIXEL_FORMAT,
|
||||
get_scale_factor(native_window),
|
||||
fonts,
|
||||
),
|
||||
command_queue: device.new_command_queue(),
|
||||
renderer: Renderer::new(true, fonts),
|
||||
last_fresh_keydown: None,
|
||||
layer,
|
||||
traffic_light_position: options.traffic_light_position,
|
||||
traffic_light_position: options
|
||||
.titlebar
|
||||
.as_ref()
|
||||
.and_then(|titlebar| titlebar.traffic_light_position),
|
||||
previous_modifiers_changed_event: None,
|
||||
ime_state: ImeState::None,
|
||||
ime_text: None,
|
||||
|
@ -425,13 +446,28 @@ impl Window {
|
|||
Rc::into_raw(window.0.clone()) as *const c_void,
|
||||
);
|
||||
|
||||
if let Some(title) = options.title.as_ref() {
|
||||
if let Some(title) = options.titlebar.as_ref().and_then(|t| t.title) {
|
||||
native_window.setTitle_(NSString::alloc(nil).init_str(title));
|
||||
}
|
||||
if options.titlebar_appears_transparent {
|
||||
|
||||
native_window.setMovable_(options.is_movable as BOOL);
|
||||
|
||||
if options
|
||||
.titlebar
|
||||
.map_or(true, |titlebar| titlebar.appears_transparent)
|
||||
{
|
||||
native_window.setTitlebarAppearsTransparent_(YES);
|
||||
}
|
||||
native_window.setAcceptsMouseMovedEvents_(YES);
|
||||
|
||||
let tracking_area: id = msg_send![class!(NSTrackingArea), alloc];
|
||||
let _: () = msg_send![
|
||||
tracking_area,
|
||||
initWithRect: NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.))
|
||||
options: NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect
|
||||
owner: native_view
|
||||
userInfo: nil
|
||||
];
|
||||
let _: () = msg_send![native_view, addTrackingArea: tracking_area.autorelease()];
|
||||
|
||||
native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
|
||||
native_view.setWantsBestResolutionOpenGLSurface_(YES);
|
||||
|
@ -450,7 +486,24 @@ impl Window {
|
|||
native_window.setContentView_(native_view.autorelease());
|
||||
native_window.makeFirstResponder_(native_view);
|
||||
|
||||
if options.center {
|
||||
native_window.center();
|
||||
}
|
||||
|
||||
match options.kind {
|
||||
WindowKind::Normal => native_window.setLevel_(NSNormalWindowLevel),
|
||||
WindowKind::PopUp => {
|
||||
native_window.setLevel_(NSPopUpWindowLevel);
|
||||
let _: () = msg_send![
|
||||
native_window,
|
||||
setAnimationBehavior: NSWindowAnimationBehaviorUtilityWindow
|
||||
];
|
||||
native_window.setCollectionBehavior_(
|
||||
NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces |
|
||||
NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary
|
||||
);
|
||||
}
|
||||
}
|
||||
native_window.makeKeyAndOrderFront_(nil);
|
||||
|
||||
window.0.borrow().move_traffic_light();
|
||||
|
@ -637,11 +690,13 @@ impl platform::Window for Window {
|
|||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn bounds(&self) -> RectF {
|
||||
self.0.as_ref().borrow().bounds()
|
||||
}
|
||||
|
||||
impl platform::WindowContext for Window {
|
||||
fn size(&self) -> Vector2F {
|
||||
self.0.as_ref().borrow().size()
|
||||
fn content_size(&self) -> Vector2F {
|
||||
self.0.as_ref().borrow().content_size()
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
|
@ -655,6 +710,17 @@ impl platform::WindowContext for Window {
|
|||
fn titlebar_height(&self) -> f32 {
|
||||
self.0.as_ref().borrow().titlebar_height()
|
||||
}
|
||||
|
||||
fn appearance(&self) -> crate::Appearance {
|
||||
unsafe {
|
||||
let appearance: id = msg_send![self.0.borrow().native_window, effectiveAppearance];
|
||||
crate::Appearance::from_native(appearance)
|
||||
}
|
||||
}
|
||||
|
||||
fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>) {
|
||||
self.0.borrow_mut().appearance_changed_callback = Some(callback);
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowState {
|
||||
|
@ -701,10 +767,25 @@ impl WindowState {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn bounds(&self) -> RectF {
|
||||
unsafe {
|
||||
let screen_frame = self.native_window.screen().visibleFrame();
|
||||
let window_frame = NSWindow::frame(self.native_window);
|
||||
let origin = vec2f(
|
||||
window_frame.origin.x as f32,
|
||||
(window_frame.origin.y - screen_frame.size.height - window_frame.size.height)
|
||||
as f32,
|
||||
);
|
||||
let size = vec2f(
|
||||
window_frame.size.width as f32,
|
||||
window_frame.size.height as f32,
|
||||
);
|
||||
RectF::new(origin, size)
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::WindowContext for WindowState {
|
||||
fn size(&self) -> Vector2F {
|
||||
fn content_size(&self) -> Vector2F {
|
||||
let NSSize { width, height, .. } =
|
||||
unsafe { NSView::frame(self.native_window.contentView()) }.size;
|
||||
vec2f(width as f32, height as f32)
|
||||
|
@ -781,7 +862,8 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
|||
|
||||
let mut window_state_borrow = window_state.as_ref().borrow_mut();
|
||||
|
||||
let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) };
|
||||
let event =
|
||||
unsafe { Event::from_native(native_event, Some(window_state_borrow.content_size().y())) };
|
||||
|
||||
if let Some(event) = event {
|
||||
if key_equivalent {
|
||||
|
@ -873,7 +955,8 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
|||
let weak_window_state = Rc::downgrade(&window_state);
|
||||
let mut window_state_borrow = window_state.as_ref().borrow_mut();
|
||||
|
||||
let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) };
|
||||
let event =
|
||||
unsafe { Event::from_native(native_event, Some(window_state_borrow.content_size().y())) };
|
||||
if let Some(event) = event {
|
||||
match &event {
|
||||
Event::MouseMoved(
|
||||
|
@ -992,16 +1075,30 @@ fn window_fullscreen_changed(this: &Object, is_fullscreen: bool) {
|
|||
}
|
||||
|
||||
extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
|
||||
let is_active = if selector == sel!(windowDidBecomeKey:) {
|
||||
true
|
||||
} else if selector == sel!(windowDidResignKey:) {
|
||||
false
|
||||
} else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let executor = window_state.as_ref().borrow().executor.clone();
|
||||
let window_state_borrow = window_state.borrow();
|
||||
let is_active = unsafe { window_state_borrow.native_window.isKeyWindow() == YES };
|
||||
|
||||
// When opening a pop-up while the application isn't active, Cocoa sends a spurious
|
||||
// `windowDidBecomeKey` message to the previous key window even though that window
|
||||
// isn't actually key. This causes a bug if the application is later activated while
|
||||
// the pop-up is still open, making it impossible to activate the previous key window
|
||||
// even if the pop-up gets closed. The only way to activate it again is to de-activate
|
||||
// the app and re-activate it, which is a pretty bad UX.
|
||||
// The following code detects the spurious event and invokes `resignKeyWindow`:
|
||||
// in theory, we're not supposed to invoke this method manually but it balances out
|
||||
// the spurious `becomeKeyWindow` event and helps us work around that bug.
|
||||
if selector == sel!(windowDidBecomeKey:) {
|
||||
if !is_active {
|
||||
unsafe {
|
||||
let _: () = msg_send![window_state_borrow.native_window, resignKeyWindow];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let executor = window_state_borrow.executor.clone();
|
||||
drop(window_state_borrow);
|
||||
executor
|
||||
.spawn(async move {
|
||||
let mut window_state_borrow = window_state.as_ref().borrow_mut();
|
||||
|
@ -1049,7 +1146,7 @@ extern "C" fn close_window(this: &Object, _: Sel) {
|
|||
extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let window_state = window_state.as_ref().borrow();
|
||||
window_state.layer
|
||||
window_state.renderer.layer().as_ptr() as id
|
||||
}
|
||||
|
||||
extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) {
|
||||
|
@ -1058,14 +1155,20 @@ extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) {
|
|||
|
||||
unsafe {
|
||||
let scale_factor = window_state_borrow.scale_factor() as f64;
|
||||
let size = window_state_borrow.size();
|
||||
let size = window_state_borrow.content_size();
|
||||
let drawable_size: NSSize = NSSize {
|
||||
width: size.x() as f64 * scale_factor,
|
||||
height: size.y() as f64 * scale_factor,
|
||||
};
|
||||
|
||||
let _: () = msg_send![window_state_borrow.layer, setContentsScale: scale_factor];
|
||||
let _: () = msg_send![window_state_borrow.layer, setDrawableSize: drawable_size];
|
||||
let _: () = msg_send![
|
||||
window_state_borrow.renderer.layer(),
|
||||
setContentsScale: scale_factor
|
||||
];
|
||||
let _: () = msg_send![
|
||||
window_state_borrow.renderer.layer(),
|
||||
setDrawableSize: drawable_size
|
||||
];
|
||||
}
|
||||
|
||||
if let Some(mut callback) = window_state_borrow.resize_callback.take() {
|
||||
|
@ -1079,7 +1182,7 @@ extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
|
|||
let window_state = unsafe { get_window_state(this) };
|
||||
let window_state_borrow = window_state.as_ref().borrow();
|
||||
|
||||
if window_state_borrow.size() == vec2f(size.width as f32, size.height as f32) {
|
||||
if window_state_borrow.content_size() == vec2f(size.width as f32, size.height as f32) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1094,7 +1197,10 @@ extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
|
|||
};
|
||||
|
||||
unsafe {
|
||||
let _: () = msg_send![window_state_borrow.layer, setDrawableSize: drawable_size];
|
||||
let _: () = msg_send![
|
||||
window_state_borrow.renderer.layer(),
|
||||
setDrawableSize: drawable_size
|
||||
];
|
||||
}
|
||||
|
||||
drop(window_state_borrow);
|
||||
|
@ -1110,25 +1216,8 @@ extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
|
|||
unsafe {
|
||||
let window_state = get_window_state(this);
|
||||
let mut window_state = window_state.as_ref().borrow_mut();
|
||||
|
||||
if let Some(scene) = window_state.scene_to_render.take() {
|
||||
let drawable: &metal::MetalDrawableRef = msg_send![window_state.layer, nextDrawable];
|
||||
let command_queue = window_state.command_queue.clone();
|
||||
let command_buffer = command_queue.new_command_buffer();
|
||||
|
||||
let size = window_state.size();
|
||||
let scale_factor = window_state.scale_factor();
|
||||
|
||||
window_state.renderer.render(
|
||||
&scene,
|
||||
size * scale_factor,
|
||||
command_buffer,
|
||||
drawable.texture(),
|
||||
);
|
||||
|
||||
command_buffer.commit();
|
||||
command_buffer.wait_until_completed();
|
||||
drawable.present();
|
||||
window_state.renderer.render(&scene);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1301,6 +1390,18 @@ extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) {
|
|||
}
|
||||
}
|
||||
|
||||
extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) {
|
||||
unsafe {
|
||||
let state = get_window_state(this);
|
||||
let mut state_borrow = state.as_ref().borrow_mut();
|
||||
if let Some(mut callback) = state_borrow.appearance_changed_callback.take() {
|
||||
drop(state_borrow);
|
||||
callback();
|
||||
state.borrow_mut().appearance_changed_callback = Some(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn synthetic_drag(
|
||||
window_state: Weak<RefCell<WindowState>>,
|
||||
drag_id: usize,
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
use super::{AppVersion, CursorStyle, WindowBounds};
|
||||
use crate::{
|
||||
geometry::vector::{vec2f, Vector2F},
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
keymap, Action, ClipboardItem,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
|
@ -120,6 +123,14 @@ impl super::Platform for Platform {
|
|||
|
||||
fn activate(&self, _ignoring_other_apps: bool) {}
|
||||
|
||||
fn hide(&self) {}
|
||||
|
||||
fn hide_other_apps(&self) {}
|
||||
|
||||
fn unhide_other_apps(&self) {}
|
||||
|
||||
fn quit(&self) {}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
_: usize,
|
||||
|
@ -136,13 +147,9 @@ impl super::Platform for Platform {
|
|||
None
|
||||
}
|
||||
|
||||
fn hide(&self) {}
|
||||
|
||||
fn hide_other_apps(&self) {}
|
||||
|
||||
fn unhide_other_apps(&self) {}
|
||||
|
||||
fn quit(&self) {}
|
||||
fn add_status_item(&self) -> Box<dyn crate::Window> {
|
||||
Box::new(Window::new(vec2f(24., 24.)))
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
*self.current_clipboard_item.lock() = Some(item);
|
||||
|
@ -224,24 +231,6 @@ impl super::Dispatcher for Dispatcher {
|
|||
}
|
||||
}
|
||||
|
||||
impl super::WindowContext for Window {
|
||||
fn size(&self) -> Vector2F {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
self.scale_factor
|
||||
}
|
||||
|
||||
fn titlebar_height(&self) -> f32 {
|
||||
24.
|
||||
}
|
||||
|
||||
fn present_scene(&mut self, scene: crate::Scene) {
|
||||
self.current_scene = Some(scene);
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Window for Window {
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
|
@ -296,6 +285,32 @@ impl super::Window for Window {
|
|||
fn zoom(&self) {}
|
||||
|
||||
fn toggle_full_screen(&self) {}
|
||||
|
||||
fn bounds(&self) -> RectF {
|
||||
RectF::new(Default::default(), self.size)
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Vector2F {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
self.scale_factor
|
||||
}
|
||||
|
||||
fn titlebar_height(&self) -> f32 {
|
||||
24.
|
||||
}
|
||||
|
||||
fn present_scene(&mut self, scene: crate::Scene) {
|
||||
self.current_scene = Some(scene);
|
||||
}
|
||||
|
||||
fn appearance(&self) -> crate::Appearance {
|
||||
crate::Appearance::Light
|
||||
}
|
||||
|
||||
fn on_appearance_changed(&mut self, _: Box<dyn FnMut()>) {}
|
||||
}
|
||||
|
||||
pub fn platform() -> Platform {
|
||||
|
|
|
@ -8,13 +8,14 @@ use crate::{
|
|||
platform::{CursorStyle, Event},
|
||||
scene::{
|
||||
ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent,
|
||||
HoverRegionEvent, MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
|
||||
HoverRegionEvent, MouseRegionEvent, MoveRegionEvent, ScrollWheelRegionEvent,
|
||||
UpOutRegionEvent, UpRegionEvent,
|
||||
},
|
||||
text_layout::TextLayoutCache,
|
||||
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, Entity,
|
||||
FontSystem, ModelHandle, MouseButton, MouseMovedEvent, MouseRegion, MouseRegionId, ParentId,
|
||||
ReadModel, ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle,
|
||||
View, ViewHandle, WeakModelHandle, WeakViewHandle,
|
||||
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, Appearance, AssetCache, ElementBox,
|
||||
Entity, FontSystem, ModelHandle, MouseButton, MouseMovedEvent, MouseRegion, MouseRegionId,
|
||||
ParentId, ReadModel, ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle,
|
||||
UpgradeViewHandle, View, ViewHandle, WeakModelHandle, WeakViewHandle,
|
||||
};
|
||||
use collections::{HashMap, HashSet};
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||
|
@ -36,16 +37,18 @@ pub struct Presenter {
|
|||
asset_cache: Arc<AssetCache>,
|
||||
last_mouse_moved_event: Option<Event>,
|
||||
hovered_region_ids: HashSet<MouseRegionId>,
|
||||
clicked_regions: Vec<MouseRegion>,
|
||||
clicked_region_ids: HashSet<MouseRegionId>,
|
||||
clicked_button: Option<MouseButton>,
|
||||
mouse_position: Vector2F,
|
||||
titlebar_height: f32,
|
||||
appearance: Appearance,
|
||||
}
|
||||
|
||||
impl Presenter {
|
||||
pub fn new(
|
||||
window_id: usize,
|
||||
titlebar_height: f32,
|
||||
appearance: Appearance,
|
||||
font_cache: Arc<FontCache>,
|
||||
text_layout_cache: TextLayoutCache,
|
||||
asset_cache: Arc<AssetCache>,
|
||||
|
@ -53,7 +56,7 @@ impl Presenter {
|
|||
) -> Self {
|
||||
Self {
|
||||
window_id,
|
||||
rendered_views: cx.render_views(window_id, titlebar_height),
|
||||
rendered_views: cx.render_views(window_id, titlebar_height, appearance),
|
||||
cursor_regions: Default::default(),
|
||||
mouse_regions: Default::default(),
|
||||
font_cache,
|
||||
|
@ -61,19 +64,22 @@ impl Presenter {
|
|||
asset_cache,
|
||||
last_mouse_moved_event: None,
|
||||
hovered_region_ids: Default::default(),
|
||||
clicked_regions: Vec::new(),
|
||||
clicked_region_ids: Default::default(),
|
||||
clicked_button: None,
|
||||
mouse_position: vec2f(0., 0.),
|
||||
titlebar_height,
|
||||
appearance,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate(
|
||||
&mut self,
|
||||
invalidation: &mut WindowInvalidation,
|
||||
appearance: Appearance,
|
||||
cx: &mut MutableAppContext,
|
||||
) {
|
||||
cx.start_frame();
|
||||
self.appearance = appearance;
|
||||
for view_id in &invalidation.removed {
|
||||
invalidation.updated.remove(view_id);
|
||||
self.rendered_views.remove(view_id);
|
||||
|
@ -86,24 +92,24 @@ impl Presenter {
|
|||
view_id: *view_id,
|
||||
titlebar_height: self.titlebar_height,
|
||||
hovered_region_ids: self.hovered_region_ids.clone(),
|
||||
clicked_region_ids: self.clicked_button.map(|button| {
|
||||
(
|
||||
self.clicked_regions
|
||||
.iter()
|
||||
.filter_map(MouseRegion::id)
|
||||
.collect(),
|
||||
button,
|
||||
)
|
||||
}),
|
||||
clicked_region_ids: self
|
||||
.clicked_button
|
||||
.map(|button| (self.clicked_region_ids.clone(), button)),
|
||||
refreshing: false,
|
||||
appearance,
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh(&mut self, invalidation: &mut WindowInvalidation, cx: &mut MutableAppContext) {
|
||||
self.invalidate(invalidation, cx);
|
||||
pub fn refresh(
|
||||
&mut self,
|
||||
invalidation: &mut WindowInvalidation,
|
||||
appearance: Appearance,
|
||||
cx: &mut MutableAppContext,
|
||||
) {
|
||||
self.invalidate(invalidation, appearance, cx);
|
||||
for (view_id, view) in &mut self.rendered_views {
|
||||
if !invalidation.updated.contains(view_id) {
|
||||
*view = cx
|
||||
|
@ -112,16 +118,11 @@ impl Presenter {
|
|||
view_id: *view_id,
|
||||
titlebar_height: self.titlebar_height,
|
||||
hovered_region_ids: self.hovered_region_ids.clone(),
|
||||
clicked_region_ids: self.clicked_button.map(|button| {
|
||||
(
|
||||
self.clicked_regions
|
||||
.iter()
|
||||
.filter_map(MouseRegion::id)
|
||||
.collect(),
|
||||
button,
|
||||
)
|
||||
}),
|
||||
clicked_region_ids: self
|
||||
.clicked_button
|
||||
.map(|button| (self.clicked_region_ids.clone(), button)),
|
||||
refreshing: true,
|
||||
appearance,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
@ -184,16 +185,11 @@ impl Presenter {
|
|||
view_stack: Vec::new(),
|
||||
refreshing,
|
||||
hovered_region_ids: self.hovered_region_ids.clone(),
|
||||
clicked_region_ids: self.clicked_button.map(|button| {
|
||||
(
|
||||
self.clicked_regions
|
||||
.iter()
|
||||
.filter_map(MouseRegion::id)
|
||||
.collect(),
|
||||
button,
|
||||
)
|
||||
}),
|
||||
clicked_region_ids: self
|
||||
.clicked_button
|
||||
.map(|button| (self.clicked_region_ids.clone(), button)),
|
||||
titlebar_height: self.titlebar_height,
|
||||
appearance: self.appearance,
|
||||
window_size,
|
||||
app: cx,
|
||||
}
|
||||
|
@ -235,6 +231,7 @@ impl Presenter {
|
|||
) -> bool {
|
||||
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
|
||||
let mut events_to_send = Vec::new();
|
||||
let mut invalidated_views: HashSet<usize> = Default::default();
|
||||
|
||||
// 1. Allocate the correct set of GPUI events generated from the platform events
|
||||
// -> These are usually small: [Mouse Down] or [Mouse up, Click] or [Mouse Moved, Mouse Dragged?]
|
||||
|
@ -248,16 +245,23 @@ impl Presenter {
|
|||
|
||||
// If there is already clicked_button stored, don't replace it.
|
||||
if self.clicked_button.is_none() {
|
||||
self.clicked_regions = self
|
||||
self.clicked_region_ids = self
|
||||
.mouse_regions
|
||||
.iter()
|
||||
.filter_map(|(region, _)| {
|
||||
region
|
||||
.bounds
|
||||
.contains_point(e.position)
|
||||
.then(|| region.clone())
|
||||
if region.bounds.contains_point(e.position) {
|
||||
Some(region.id())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Clicked status is used when rendering views via the RenderContext.
|
||||
// So when it changes, these views need to be rerendered
|
||||
for clicked_region_id in self.clicked_region_ids.iter() {
|
||||
invalidated_views.insert(clicked_region_id.view_id());
|
||||
}
|
||||
self.clicked_button = Some(e.button);
|
||||
}
|
||||
|
||||
|
@ -341,6 +345,12 @@ impl Presenter {
|
|||
|
||||
self.last_mouse_moved_event = Some(event.clone());
|
||||
}
|
||||
Event::ScrollWheel(e) => {
|
||||
events_to_send.push(MouseRegionEvent::ScrollWheel(ScrollWheelRegionEvent {
|
||||
region: Default::default(),
|
||||
platform_event: e.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
@ -349,7 +359,6 @@ impl Presenter {
|
|||
self.mouse_position = position;
|
||||
}
|
||||
|
||||
let mut invalidated_views: HashSet<usize> = Default::default();
|
||||
let mut any_event_handled = false;
|
||||
// 2. Process the raw mouse events into region events
|
||||
for mut region_event in events_to_send {
|
||||
|
@ -375,23 +384,21 @@ impl Presenter {
|
|||
top_most_depth = Some(depth);
|
||||
}
|
||||
|
||||
if let Some(region_id) = region.id() {
|
||||
// This unwrap relies on short circuiting boolean expressions
|
||||
// The right side of the && is only executed when contains_mouse
|
||||
// is true, and we know above that when contains_mouse is true
|
||||
// top_most_depth is set
|
||||
if contains_mouse && depth == top_most_depth.unwrap() {
|
||||
//Ensure that hover entrance events aren't sent twice
|
||||
if self.hovered_region_ids.insert(region_id) {
|
||||
if self.hovered_region_ids.insert(region.id()) {
|
||||
valid_regions.push(region.clone());
|
||||
invalidated_views.insert(region.view_id);
|
||||
invalidated_views.insert(region.id().view_id());
|
||||
}
|
||||
} else {
|
||||
// Ensure that hover exit events aren't sent twice
|
||||
if self.hovered_region_ids.remove(®ion_id) {
|
||||
if self.hovered_region_ids.remove(®ion.id()) {
|
||||
valid_regions.push(region.clone());
|
||||
invalidated_views.insert(region.view_id);
|
||||
}
|
||||
invalidated_views.insert(region.id().view_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -404,21 +411,30 @@ impl Presenter {
|
|||
.unwrap_or(false)
|
||||
{
|
||||
// Clear clicked regions and clicked button
|
||||
let clicked_regions =
|
||||
std::mem::replace(&mut self.clicked_regions, Vec::new());
|
||||
let clicked_region_ids =
|
||||
std::mem::replace(&mut self.clicked_region_ids, Default::default());
|
||||
// Clicked status is used when rendering views via the RenderContext.
|
||||
// So when it changes, these views need to be rerendered
|
||||
for clicked_region_id in clicked_region_ids.iter() {
|
||||
invalidated_views.insert(clicked_region_id.view_id());
|
||||
}
|
||||
self.clicked_button = None;
|
||||
|
||||
// Find regions which still overlap with the mouse since the last MouseDown happened
|
||||
for clicked_region in clicked_regions.into_iter().rev() {
|
||||
if clicked_region.bounds.contains_point(e.position) {
|
||||
valid_regions.push(clicked_region);
|
||||
for (mouse_region, _) in self.mouse_regions.iter().rev() {
|
||||
if clicked_region_ids.contains(&mouse_region.id()) {
|
||||
if mouse_region.bounds.contains_point(self.mouse_position) {
|
||||
valid_regions.push(mouse_region.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MouseRegionEvent::Drag(_) => {
|
||||
for clicked_region in self.clicked_regions.iter().rev() {
|
||||
valid_regions.push(clicked_region.clone());
|
||||
for (mouse_region, _) in self.mouse_regions.iter().rev() {
|
||||
if self.clicked_region_ids.contains(&mouse_region.id()) {
|
||||
valid_regions.push(mouse_region.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -447,18 +463,18 @@ impl Presenter {
|
|||
|
||||
region_event.set_region(valid_region.bounds);
|
||||
if let MouseRegionEvent::Hover(e) = &mut region_event {
|
||||
e.started = valid_region
|
||||
.id()
|
||||
.map(|region_id| hovered_region_ids.contains(®ion_id))
|
||||
.unwrap_or(false)
|
||||
e.started = hovered_region_ids.contains(&valid_region.id())
|
||||
}
|
||||
// Handle Down events if the MouseRegion has a Click handler. This makes the api more intuitive as you would
|
||||
// Handle Down events if the MouseRegion has a Click or Drag handler. This makes the api more intuitive as you would
|
||||
// not expect a MouseRegion to be transparent to Down events if it also has a Click handler.
|
||||
// This behavior can be overridden by adding a Down handler that calls cx.propogate_event
|
||||
if let MouseRegionEvent::Down(e) = ®ion_event {
|
||||
if valid_region
|
||||
.handlers
|
||||
.contains_handler(MouseRegionEvent::click_disc(), Some(e.button))
|
||||
|| valid_region
|
||||
.handlers
|
||||
.contains_handler(MouseRegionEvent::drag_disc(), Some(e.button))
|
||||
{
|
||||
event_cx.handled = true;
|
||||
}
|
||||
|
@ -466,8 +482,10 @@ impl Presenter {
|
|||
|
||||
if let Some(callback) = valid_region.handlers.get(®ion_event.handler_key()) {
|
||||
event_cx.handled = true;
|
||||
event_cx.invalidated_views.insert(valid_region.view_id);
|
||||
event_cx.with_current_view(valid_region.view_id, {
|
||||
event_cx
|
||||
.invalidated_views
|
||||
.insert(valid_region.id().view_id());
|
||||
event_cx.with_current_view(valid_region.id().view_id(), {
|
||||
let region_event = region_event.clone();
|
||||
|cx| {
|
||||
callback(region_event, cx);
|
||||
|
@ -545,8 +563,9 @@ pub struct LayoutContext<'a> {
|
|||
pub refreshing: bool,
|
||||
pub window_size: Vector2F,
|
||||
titlebar_height: f32,
|
||||
appearance: Appearance,
|
||||
hovered_region_ids: HashSet<MouseRegionId>,
|
||||
clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
|
||||
clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
|
||||
}
|
||||
|
||||
impl<'a> LayoutContext<'a> {
|
||||
|
@ -619,6 +638,7 @@ impl<'a> LayoutContext<'a> {
|
|||
hovered_region_ids: self.hovered_region_ids.clone(),
|
||||
clicked_region_ids: self.clicked_region_ids.clone(),
|
||||
refreshing: self.refreshing,
|
||||
appearance: self.appearance,
|
||||
};
|
||||
f(view, &mut render_cx)
|
||||
})
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
mod mouse_region;
|
||||
mod mouse_region_event;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
use collections::HashSet;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
@ -10,7 +12,7 @@ use crate::{
|
|||
fonts::{FontId, GlyphId},
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json::ToJson,
|
||||
platform::CursorStyle,
|
||||
platform::{current::Surface, CursorStyle},
|
||||
ImageData,
|
||||
};
|
||||
pub use mouse_region::*;
|
||||
|
@ -20,6 +22,8 @@ pub struct Scene {
|
|||
scale_factor: f32,
|
||||
stacking_contexts: Vec<StackingContext>,
|
||||
active_stacking_context_stack: Vec<usize>,
|
||||
#[cfg(debug_assertions)]
|
||||
mouse_region_ids: HashSet<MouseRegionId>,
|
||||
}
|
||||
|
||||
struct StackingContext {
|
||||
|
@ -34,6 +38,7 @@ pub struct Layer {
|
|||
quads: Vec<Quad>,
|
||||
underlines: Vec<Underline>,
|
||||
images: Vec<Image>,
|
||||
surfaces: Vec<Surface>,
|
||||
shadows: Vec<Shadow>,
|
||||
glyphs: Vec<Glyph>,
|
||||
image_glyphs: Vec<ImageGlyph>,
|
||||
|
@ -177,6 +182,8 @@ impl Scene {
|
|||
scale_factor,
|
||||
stacking_contexts: vec![stacking_context],
|
||||
active_stacking_context_stack: vec![0],
|
||||
#[cfg(debug_assertions)]
|
||||
mouse_region_ids: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -241,7 +248,24 @@ impl Scene {
|
|||
|
||||
pub fn push_mouse_region(&mut self, region: MouseRegion) {
|
||||
if can_draw(region.bounds) {
|
||||
self.active_layer().push_mouse_region(region);
|
||||
// Ensure that Regions cannot be added to a scene with the same region id.
|
||||
#[cfg(debug_assertions)]
|
||||
let region_id;
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
region_id = region.id();
|
||||
}
|
||||
|
||||
if self.active_layer().push_mouse_region(region) {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if !self.mouse_region_ids.insert(region_id) {
|
||||
let tag_name = region_id.tag_type_name();
|
||||
panic!("Same MouseRegionId: {region_id:?} inserted multiple times to the same scene. \
|
||||
Will cause problems! Look for MouseRegion that uses Tag: {tag_name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,6 +273,10 @@ impl Scene {
|
|||
self.active_layer().push_image(image)
|
||||
}
|
||||
|
||||
pub fn push_surface(&mut self, surface: Surface) {
|
||||
self.active_layer().push_surface(surface)
|
||||
}
|
||||
|
||||
pub fn push_underline(&mut self, underline: Underline) {
|
||||
self.active_layer().push_underline(underline)
|
||||
}
|
||||
|
@ -329,6 +357,7 @@ impl Layer {
|
|||
quads: Default::default(),
|
||||
underlines: Default::default(),
|
||||
images: Default::default(),
|
||||
surfaces: Default::default(),
|
||||
shadows: Default::default(),
|
||||
image_glyphs: Default::default(),
|
||||
glyphs: Default::default(),
|
||||
|
@ -364,15 +393,17 @@ impl Layer {
|
|||
}
|
||||
}
|
||||
|
||||
fn push_mouse_region(&mut self, region: MouseRegion) {
|
||||
fn push_mouse_region(&mut self, region: MouseRegion) -> bool {
|
||||
if let Some(bounds) = region
|
||||
.bounds
|
||||
.intersection(self.clip_bounds.unwrap_or(region.bounds))
|
||||
{
|
||||
if can_draw(bounds) {
|
||||
self.mouse_regions.push(region);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn push_underline(&mut self, underline: Underline) {
|
||||
|
@ -395,6 +426,16 @@ impl Layer {
|
|||
self.images.as_slice()
|
||||
}
|
||||
|
||||
fn push_surface(&mut self, surface: Surface) {
|
||||
if can_draw(surface.bounds) {
|
||||
self.surfaces.push(surface);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn surfaces(&self) -> &[Surface] {
|
||||
self.surfaces.as_slice()
|
||||
}
|
||||
|
||||
fn push_shadow(&mut self, shadow: Shadow) {
|
||||
if can_draw(shadow.bounds) {
|
||||
self.shadows.push(shadow);
|
||||
|
@ -536,11 +577,8 @@ impl ToJson for Border {
|
|||
}
|
||||
|
||||
impl MouseRegion {
|
||||
pub fn id(&self) -> Option<MouseRegionId> {
|
||||
self.discriminant.map(|discriminant| MouseRegionId {
|
||||
view_id: self.view_id,
|
||||
discriminant,
|
||||
})
|
||||
pub fn id(&self) -> MouseRegionId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::{any::TypeId, mem::Discriminant, rc::Rc};
|
||||
use std::{any::TypeId, fmt::Debug, mem::Discriminant, rc::Rc};
|
||||
|
||||
use collections::HashMap;
|
||||
|
||||
|
@ -6,54 +6,55 @@ use pathfinder_geometry::rect::RectF;
|
|||
|
||||
use crate::{EventContext, MouseButton};
|
||||
|
||||
use super::mouse_region_event::{
|
||||
use super::{
|
||||
mouse_region_event::{
|
||||
ClickRegionEvent, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, HoverRegionEvent,
|
||||
MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
|
||||
},
|
||||
ScrollWheelRegionEvent,
|
||||
};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
#[derive(Clone)]
|
||||
pub struct MouseRegion {
|
||||
pub view_id: usize,
|
||||
pub discriminant: Option<(TypeId, usize)>,
|
||||
pub id: MouseRegionId,
|
||||
pub bounds: RectF,
|
||||
pub handlers: HandlerSet,
|
||||
pub hoverable: bool,
|
||||
}
|
||||
|
||||
impl MouseRegion {
|
||||
pub fn new(view_id: usize, discriminant: Option<(TypeId, usize)>, bounds: RectF) -> Self {
|
||||
Self::from_handlers(view_id, discriminant, bounds, Default::default())
|
||||
/// Region ID is used to track semantically equivalent mouse regions across render passes.
|
||||
/// e.g. if you have mouse handlers attached to a list item type, then each item of the list
|
||||
/// should pass a different (consistent) region_id. If you have one big region that covers your
|
||||
/// whole component, just pass the view_id again.
|
||||
pub fn new<Tag: 'static>(view_id: usize, region_id: usize, bounds: RectF) -> Self {
|
||||
Self::from_handlers::<Tag>(view_id, region_id, bounds, Default::default())
|
||||
}
|
||||
|
||||
pub fn from_handlers(
|
||||
pub fn handle_all<Tag: 'static>(view_id: usize, region_id: usize, bounds: RectF) -> Self {
|
||||
Self::from_handlers::<Tag>(view_id, region_id, bounds, HandlerSet::capture_all())
|
||||
}
|
||||
|
||||
pub fn from_handlers<Tag: 'static>(
|
||||
view_id: usize,
|
||||
discriminant: Option<(TypeId, usize)>,
|
||||
region_id: usize,
|
||||
bounds: RectF,
|
||||
handlers: HandlerSet,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: MouseRegionId {
|
||||
view_id,
|
||||
discriminant,
|
||||
tag: TypeId::of::<Tag>(),
|
||||
region_id,
|
||||
#[cfg(debug_assertions)]
|
||||
tag_type_name: std::any::type_name::<Tag>(),
|
||||
},
|
||||
bounds,
|
||||
handlers,
|
||||
hoverable: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_all(
|
||||
view_id: usize,
|
||||
discriminant: Option<(TypeId, usize)>,
|
||||
bounds: RectF,
|
||||
) -> Self {
|
||||
Self {
|
||||
view_id,
|
||||
discriminant,
|
||||
bounds,
|
||||
handlers: HandlerSet::capture_all(),
|
||||
hoverable: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_down(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
|
@ -124,6 +125,14 @@ impl MouseRegion {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn on_scroll(
|
||||
mut self,
|
||||
handler: impl Fn(ScrollWheelRegionEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.handlers = self.handlers.on_scroll(handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_hoverable(mut self, is_hoverable: bool) -> Self {
|
||||
self.hoverable = is_hoverable;
|
||||
self
|
||||
|
@ -132,8 +141,32 @@ impl MouseRegion {
|
|||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub struct MouseRegionId {
|
||||
pub view_id: usize,
|
||||
pub discriminant: (TypeId, usize),
|
||||
view_id: usize,
|
||||
tag: TypeId,
|
||||
region_id: usize,
|
||||
#[cfg(debug_assertions)]
|
||||
tag_type_name: &'static str,
|
||||
}
|
||||
|
||||
impl MouseRegionId {
|
||||
pub(crate) fn new<Tag: 'static>(view_id: usize, region_id: usize) -> Self {
|
||||
MouseRegionId {
|
||||
view_id,
|
||||
region_id,
|
||||
tag: TypeId::of::<Tag>(),
|
||||
#[cfg(debug_assertions)]
|
||||
tag_type_name: std::any::type_name::<Tag>(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_id(&self) -> usize {
|
||||
self.view_id
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn tag_type_name(&self) -> &'static str {
|
||||
self.tag_type_name
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
|
@ -345,4 +378,22 @@ impl HandlerSet {
|
|||
}));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_scroll(
|
||||
mut self,
|
||||
handler: impl Fn(ScrollWheelRegionEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.set.insert((MouseRegionEvent::scroll_wheel_disc(), None),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseRegionEvent::ScrollWheel(e) = region_event {
|
||||
handler(e, cx);
|
||||
} else {
|
||||
panic!(
|
||||
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::ScrollWheel, found {:?}",
|
||||
region_event
|
||||
);
|
||||
}
|
||||
}));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,7 +168,7 @@ impl MouseRegionEvent {
|
|||
pub fn is_capturable(&self) -> bool {
|
||||
match self {
|
||||
MouseRegionEvent::Move(_) => true,
|
||||
MouseRegionEvent::Drag(_) => false,
|
||||
MouseRegionEvent::Drag(_) => true,
|
||||
MouseRegionEvent::Hover(_) => false,
|
||||
MouseRegionEvent::Down(_) => true,
|
||||
MouseRegionEvent::Up(_) => true,
|
||||
|
|
|
@ -109,7 +109,7 @@ impl View for Select {
|
|||
Default::default()
|
||||
};
|
||||
let mut result = Flex::column().with_child(
|
||||
MouseEventHandler::new::<Header, _, _>(self.handle.id(), cx, |mouse_state, cx| {
|
||||
MouseEventHandler::<Header>::new(self.handle.id(), cx, |mouse_state, cx| {
|
||||
Container::new((self.render_item)(
|
||||
self.selected_item_ix,
|
||||
ItemType::Header,
|
||||
|
@ -137,10 +137,7 @@ impl View for Select {
|
|||
let selected_item_ix = this.selected_item_ix;
|
||||
range.end = range.end.min(this.item_count);
|
||||
items.extend(range.map(|ix| {
|
||||
MouseEventHandler::new::<Item, _, _>(
|
||||
ix,
|
||||
cx,
|
||||
|mouse_state, cx| {
|
||||
MouseEventHandler::<Item>::new(ix, cx, |mouse_state, cx| {
|
||||
(this.render_item)(
|
||||
ix,
|
||||
if ix == selected_item_ix {
|
||||
|
@ -151,8 +148,7 @@ impl View for Select {
|
|||
mouse_state.hovered,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(SelectItem(ix))
|
||||
})
|
||||
|
|
22
crates/live_kit/Cargo.toml
Normal file
22
crates/live_kit/Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "live_kit"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Bindings to LiveKit Swift client SDK"
|
||||
|
||||
[lib]
|
||||
path = "src/live_kit.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
media = { path = "../media" }
|
||||
|
||||
anyhow = "1.0.38"
|
||||
core-foundation = "0.9.3"
|
||||
core-graphics = "0.22.3"
|
||||
futures = "0.3"
|
||||
parking_lot = "0.11.1"
|
||||
|
||||
[build-dependencies]
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
9
crates/live_kit/LiveKitBridge/.gitignore
vendored
Normal file
9
crates/live_kit/LiveKitBridge/.gitignore
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
52
crates/live_kit/LiveKitBridge/Package.resolved
Normal file
52
crates/live_kit/LiveKitBridge/Package.resolved
Normal file
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "LiveKit",
|
||||
"repositoryURL": "https://github.com/livekit/client-sdk-swift.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "5cc3c001779ab147199ce3ea0dce465b846368b4",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Promises",
|
||||
"repositoryURL": "https://github.com/google/promises.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "3e4e743631e86c8c70dbc6efdc7beaa6e90fd3bb",
|
||||
"version": "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "WebRTC",
|
||||
"repositoryURL": "https://github.com/webrtc-sdk/Specs.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "5225f2de4b6d0098803b3a0e55b255a41f293dad",
|
||||
"version": "104.5112.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-log",
|
||||
"repositoryURL": "https://github.com/apple/swift-log.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "6fe203dc33195667ce1759bf0182975e4653ba1c",
|
||||
"version": "1.4.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SwiftProtobuf",
|
||||
"repositoryURL": "https://github.com/apple/swift-protobuf.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b8230909dedc640294d7324d37f4c91ad3dcf177",
|
||||
"version": "1.20.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
27
crates/live_kit/LiveKitBridge/Package.swift
Normal file
27
crates/live_kit/LiveKitBridge/Package.swift
Normal file
|
@ -0,0 +1,27 @@
|
|||
// swift-tools-version: 5.5
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "LiveKitBridge",
|
||||
platforms: [
|
||||
.macOS(.v10_15)
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "LiveKitBridge",
|
||||
type: .static,
|
||||
targets: ["LiveKitBridge"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/livekit/client-sdk-swift.git", revision: "5cc3c001779ab147199ce3ea0dce465b846368b4"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "LiveKitBridge",
|
||||
dependencies: [.product(name: "LiveKit", package: "client-sdk-swift")]),
|
||||
]
|
||||
)
|
3
crates/live_kit/LiveKitBridge/README.md
Normal file
3
crates/live_kit/LiveKitBridge/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# LiveKitBridge
|
||||
|
||||
A description of this package.
|
|
@ -0,0 +1,105 @@
|
|||
import Foundation
|
||||
import LiveKit
|
||||
import WebRTC
|
||||
|
||||
class LKRoomDelegate: RoomDelegate {
|
||||
var data: UnsafeRawPointer
|
||||
var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, UnsafeRawPointer) -> Void
|
||||
|
||||
init(data: UnsafeRawPointer, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer) -> Void) {
|
||||
self.data = data
|
||||
self.onDidSubscribeToRemoteVideoTrack = onDidSubscribeToRemoteVideoTrack
|
||||
}
|
||||
|
||||
func room(_ room: Room, participant: RemoteParticipant, didSubscribe publication: RemoteTrackPublication, track: Track) {
|
||||
if track.kind == .video {
|
||||
self.onDidSubscribeToRemoteVideoTrack(self.data, Unmanaged.passRetained(track).toOpaque())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LKVideoRenderer: NSObject, VideoRenderer {
|
||||
var data: UnsafeRawPointer
|
||||
var onFrame: @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Void
|
||||
var onDrop: @convention(c) (UnsafeRawPointer) -> Void
|
||||
var adaptiveStreamIsEnabled: Bool = false
|
||||
var adaptiveStreamSize: CGSize = .zero
|
||||
|
||||
init(data: UnsafeRawPointer, onFrame: @escaping @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Void, onDrop: @escaping @convention(c) (UnsafeRawPointer) -> Void) {
|
||||
self.data = data
|
||||
self.onFrame = onFrame
|
||||
self.onDrop = onDrop
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.onDrop(self.data)
|
||||
}
|
||||
|
||||
func setSize(_ size: CGSize) {
|
||||
print("Called setSize", size);
|
||||
}
|
||||
|
||||
func renderFrame(_ frame: RTCVideoFrame?) {
|
||||
let buffer = frame?.buffer as? RTCCVPixelBuffer
|
||||
if let pixelBuffer = buffer?.pixelBuffer {
|
||||
self.onFrame(self.data, pixelBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("LKRelease")
|
||||
public func LKRelease(ptr: UnsafeRawPointer) {
|
||||
let _ = Unmanaged<AnyObject>.fromOpaque(ptr).takeRetainedValue()
|
||||
}
|
||||
|
||||
@_cdecl("LKRoomDelegateCreate")
|
||||
public func LKRoomDelegateCreate(data: UnsafeRawPointer, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer) -> Void) -> UnsafeMutableRawPointer {
|
||||
let delegate = LKRoomDelegate(data: data, onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack)
|
||||
return Unmanaged.passRetained(delegate).toOpaque()
|
||||
}
|
||||
|
||||
@_cdecl("LKRoomCreate")
|
||||
public func LKRoomCreate(delegate: UnsafeRawPointer) -> UnsafeMutableRawPointer {
|
||||
let delegate = Unmanaged<LKRoomDelegate>.fromOpaque(delegate).takeUnretainedValue()
|
||||
return Unmanaged.passRetained(Room(delegate: delegate)).toOpaque()
|
||||
}
|
||||
|
||||
@_cdecl("LKRoomConnect")
|
||||
public func LKRoomConnect(room: UnsafeRawPointer, url: CFString, token: CFString, callback: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void, callback_data: UnsafeRawPointer) {
|
||||
let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
|
||||
|
||||
room.connect(url as String, token as String).then { _ in
|
||||
callback(callback_data, UnsafeRawPointer(nil) as! CFString?)
|
||||
}.catch { error in
|
||||
callback(callback_data, error.localizedDescription as CFString)
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("LKRoomPublishVideoTrack")
|
||||
public func LKRoomPublishVideoTrack(room: UnsafeRawPointer, track: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void, callback_data: UnsafeRawPointer) {
|
||||
let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
|
||||
let track = Unmanaged<LocalVideoTrack>.fromOpaque(track).takeUnretainedValue()
|
||||
room.localParticipant?.publishVideoTrack(track: track).then { _ in
|
||||
callback(callback_data, UnsafeRawPointer(nil) as! CFString?)
|
||||
}.catch { error in
|
||||
callback(callback_data, error.localizedDescription as CFString)
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("LKCreateScreenShareTrackForWindow")
|
||||
public func LKCreateScreenShareTrackForWindow(windowId: uint32) -> UnsafeMutableRawPointer {
|
||||
let track = LocalVideoTrack.createMacOSScreenShareTrack(source: .window(id: windowId))
|
||||
return Unmanaged.passRetained(track).toOpaque()
|
||||
}
|
||||
|
||||
@_cdecl("LKVideoRendererCreate")
|
||||
public func LKVideoRendererCreate(data: UnsafeRawPointer, onFrame: @escaping @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Void, onDrop: @escaping @convention(c) (UnsafeRawPointer) -> Void) -> UnsafeMutableRawPointer {
|
||||
Unmanaged.passRetained(LKVideoRenderer(data: data, onFrame: onFrame, onDrop: onDrop)).toOpaque()
|
||||
}
|
||||
|
||||
@_cdecl("LKVideoTrackAddRenderer")
|
||||
public func LKVideoTrackAddRenderer(track: UnsafeRawPointer, renderer: UnsafeRawPointer) {
|
||||
let track = Unmanaged<Track>.fromOpaque(track).takeUnretainedValue() as! VideoTrack
|
||||
let renderer = Unmanaged<LKVideoRenderer>.fromOpaque(renderer).takeRetainedValue()
|
||||
track.add(videoRenderer: renderer)
|
||||
}
|
148
crates/live_kit/build.rs
Normal file
148
crates/live_kit/build.rs
Normal file
|
@ -0,0 +1,148 @@
|
|||
use serde::Deserialize;
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
const SWIFT_PACKAGE_NAME: &'static str = "LiveKitBridge";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwiftTargetInfo {
|
||||
pub triple: String,
|
||||
pub unversioned_triple: String,
|
||||
pub module_triple: String,
|
||||
pub swift_runtime_compatibility_version: String,
|
||||
#[serde(rename = "librariesRequireRPath")]
|
||||
pub libraries_require_rpath: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwiftPaths {
|
||||
pub runtime_library_paths: Vec<String>,
|
||||
pub runtime_library_import_paths: Vec<String>,
|
||||
pub runtime_resource_path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SwiftTarget {
|
||||
pub target: SwiftTargetInfo,
|
||||
pub paths: SwiftPaths,
|
||||
}
|
||||
|
||||
const MACOS_TARGET_VERSION: &str = "10.15";
|
||||
|
||||
fn main() {
|
||||
let swift_target = get_swift_target();
|
||||
|
||||
build_bridge(&swift_target);
|
||||
link_swift_stdlib(&swift_target);
|
||||
link_webrtc_framework(&swift_target);
|
||||
}
|
||||
|
||||
fn build_bridge(swift_target: &SwiftTarget) {
|
||||
println!("cargo:rerun-if-changed={}/Sources", SWIFT_PACKAGE_NAME);
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}/Package.swift",
|
||||
SWIFT_PACKAGE_NAME
|
||||
);
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}/Package.resolved",
|
||||
SWIFT_PACKAGE_NAME
|
||||
);
|
||||
let swift_package_root = swift_package_root();
|
||||
if !Command::new("swift")
|
||||
.arg("build")
|
||||
.args(&["--configuration", &env::var("PROFILE").unwrap()])
|
||||
.args(&["--triple", &swift_target.target.triple])
|
||||
.current_dir(&swift_package_root)
|
||||
.status()
|
||||
.unwrap()
|
||||
.success()
|
||||
{
|
||||
panic!(
|
||||
"Failed to compile swift package in {}",
|
||||
swift_package_root.display()
|
||||
);
|
||||
}
|
||||
|
||||
println!(
|
||||
"cargo:rustc-link-search=native={}",
|
||||
swift_target.out_dir_path().display()
|
||||
);
|
||||
println!("cargo:rustc-link-lib=static={}", SWIFT_PACKAGE_NAME);
|
||||
}
|
||||
|
||||
fn link_swift_stdlib(swift_target: &SwiftTarget) {
|
||||
swift_target
|
||||
.paths
|
||||
.runtime_library_paths
|
||||
.iter()
|
||||
.for_each(|path| {
|
||||
println!("cargo:rustc-link-search=native={}", path);
|
||||
});
|
||||
}
|
||||
|
||||
fn link_webrtc_framework(swift_target: &SwiftTarget) {
|
||||
let swift_out_dir_path = swift_target.out_dir_path();
|
||||
println!("cargo:rustc-link-lib=framework=WebRTC");
|
||||
println!(
|
||||
"cargo:rustc-link-search=framework={}",
|
||||
swift_out_dir_path.display()
|
||||
);
|
||||
// Find WebRTC.framework as a sibling of the executable when running tests.
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
|
||||
|
||||
let source_path = swift_out_dir_path.join("WebRTC.framework");
|
||||
let deps_dir_path =
|
||||
PathBuf::from(env::var("OUT_DIR").unwrap()).join("../../../deps/WebRTC.framework");
|
||||
let target_dir_path =
|
||||
PathBuf::from(env::var("OUT_DIR").unwrap()).join("../../../WebRTC.framework");
|
||||
copy_dir(&source_path, &deps_dir_path);
|
||||
copy_dir(&source_path, &target_dir_path);
|
||||
}
|
||||
|
||||
fn get_swift_target() -> SwiftTarget {
|
||||
let mut arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
|
||||
if arch == "aarch64" {
|
||||
arch = "arm64".into();
|
||||
}
|
||||
let target = format!("{}-apple-macosx{}", arch, MACOS_TARGET_VERSION);
|
||||
|
||||
let swift_target_info_str = Command::new("swift")
|
||||
.args(&["-target", &target, "-print-target-info"])
|
||||
.output()
|
||||
.unwrap()
|
||||
.stdout;
|
||||
|
||||
serde_json::from_slice(&swift_target_info_str).unwrap()
|
||||
}
|
||||
|
||||
fn swift_package_root() -> PathBuf {
|
||||
env::current_dir().unwrap().join(SWIFT_PACKAGE_NAME)
|
||||
}
|
||||
|
||||
fn copy_dir(source: &Path, destination: &Path) {
|
||||
assert!(
|
||||
Command::new("cp")
|
||||
.arg("-r")
|
||||
.args(&[source, destination])
|
||||
.status()
|
||||
.unwrap()
|
||||
.success(),
|
||||
"could not copy {:?} to {:?}",
|
||||
source,
|
||||
destination
|
||||
);
|
||||
}
|
||||
|
||||
impl SwiftTarget {
|
||||
fn out_dir_path(&self) -> PathBuf {
|
||||
swift_package_root()
|
||||
.join(".build")
|
||||
.join(&self.target.unversioned_triple)
|
||||
.join(env::var("PROFILE").unwrap())
|
||||
}
|
||||
}
|
276
crates/live_kit/src/live_kit.rs
Normal file
276
crates/live_kit/src/live_kit.rs
Normal file
|
@ -0,0 +1,276 @@
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use core_foundation::{
|
||||
array::CFArray,
|
||||
base::{TCFType, TCFTypeRef},
|
||||
dictionary::CFDictionary,
|
||||
number::CFNumber,
|
||||
string::{CFString, CFStringRef},
|
||||
};
|
||||
use core_graphics::window::{
|
||||
kCGNullWindowID, kCGWindowListOptionExcludeDesktopElements, kCGWindowListOptionOnScreenOnly,
|
||||
kCGWindowNumber, kCGWindowOwnerName, kCGWindowOwnerPID, CGWindowListCopyWindowInfo,
|
||||
};
|
||||
use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
Future,
|
||||
};
|
||||
use media::core_video::{CVImageBuffer, CVImageBufferRef};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
ffi::c_void,
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
fn LKRelease(object: *const c_void);
|
||||
|
||||
fn LKRoomDelegateCreate(
|
||||
callback_data: *mut c_void,
|
||||
on_did_subscribe_to_remote_video_track: extern "C" fn(
|
||||
callback_data: *mut c_void,
|
||||
remote_track: *const c_void,
|
||||
),
|
||||
) -> *const c_void;
|
||||
|
||||
fn LKRoomCreate(delegate: *const c_void) -> *const c_void;
|
||||
fn LKRoomConnect(
|
||||
room: *const c_void,
|
||||
url: CFStringRef,
|
||||
token: CFStringRef,
|
||||
callback: extern "C" fn(*mut c_void, CFStringRef),
|
||||
callback_data: *mut c_void,
|
||||
);
|
||||
fn LKRoomPublishVideoTrack(
|
||||
room: *const c_void,
|
||||
track: *const c_void,
|
||||
callback: extern "C" fn(*mut c_void, CFStringRef),
|
||||
callback_data: *mut c_void,
|
||||
);
|
||||
|
||||
fn LKVideoRendererCreate(
|
||||
callback_data: *mut c_void,
|
||||
on_frame: extern "C" fn(callback_data: *mut c_void, frame: CVImageBufferRef),
|
||||
on_drop: extern "C" fn(callback_data: *mut c_void),
|
||||
) -> *const c_void;
|
||||
|
||||
fn LKVideoTrackAddRenderer(track: *const c_void, renderer: *const c_void);
|
||||
|
||||
fn LKCreateScreenShareTrackForWindow(windowId: u32) -> *const c_void;
|
||||
}
|
||||
|
||||
pub struct Room {
|
||||
native_room: *const c_void,
|
||||
remote_video_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<Arc<RemoteVideoTrack>>>>,
|
||||
_delegate: RoomDelegate,
|
||||
}
|
||||
|
||||
impl Room {
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new_cyclic(|weak_room| {
|
||||
let delegate = RoomDelegate::new(weak_room.clone());
|
||||
Self {
|
||||
native_room: unsafe { LKRoomCreate(delegate.native_delegate) },
|
||||
remote_video_track_subscribers: Default::default(),
|
||||
_delegate: delegate,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn connect(&self, url: &str, token: &str) -> impl Future<Output = Result<()>> {
|
||||
let url = CFString::new(url);
|
||||
let token = CFString::new(token);
|
||||
let (did_connect, tx, rx) = Self::build_done_callback();
|
||||
unsafe {
|
||||
LKRoomConnect(
|
||||
self.native_room,
|
||||
url.as_concrete_TypeRef(),
|
||||
token.as_concrete_TypeRef(),
|
||||
did_connect,
|
||||
tx,
|
||||
)
|
||||
}
|
||||
|
||||
async { rx.await.unwrap().context("error connecting to room") }
|
||||
}
|
||||
|
||||
pub fn publish_video_track(&self, track: &LocalVideoTrack) -> impl Future<Output = Result<()>> {
|
||||
let (did_publish, tx, rx) = Self::build_done_callback();
|
||||
unsafe {
|
||||
LKRoomPublishVideoTrack(self.native_room, track.0, did_publish, tx);
|
||||
}
|
||||
async { rx.await.unwrap().context("error publishing video track") }
|
||||
}
|
||||
|
||||
pub fn remote_video_tracks(&self) -> mpsc::UnboundedReceiver<Arc<RemoteVideoTrack>> {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
self.remote_video_track_subscribers.lock().push(tx);
|
||||
rx
|
||||
}
|
||||
|
||||
fn did_subscribe_to_remote_video_track(&self, track: RemoteVideoTrack) {
|
||||
let track = Arc::new(track);
|
||||
self.remote_video_track_subscribers
|
||||
.lock()
|
||||
.retain(|tx| tx.unbounded_send(track.clone()).is_ok());
|
||||
}
|
||||
|
||||
fn build_done_callback() -> (
|
||||
extern "C" fn(*mut c_void, CFStringRef),
|
||||
*mut c_void,
|
||||
oneshot::Receiver<Result<()>>,
|
||||
) {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
extern "C" fn done_callback(tx: *mut c_void, error: CFStringRef) {
|
||||
let tx = unsafe { Box::from_raw(tx as *mut oneshot::Sender<Result<()>>) };
|
||||
if error.is_null() {
|
||||
let _ = tx.send(Ok(()));
|
||||
} else {
|
||||
let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
|
||||
let _ = tx.send(Err(anyhow!(error)));
|
||||
}
|
||||
}
|
||||
(
|
||||
done_callback,
|
||||
Box::into_raw(Box::new(tx)) as *mut c_void,
|
||||
rx,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Room {
|
||||
fn drop(&mut self) {
|
||||
unsafe { LKRelease(self.native_room) }
|
||||
}
|
||||
}
|
||||
|
||||
struct RoomDelegate {
|
||||
native_delegate: *const c_void,
|
||||
weak_room: *const Room,
|
||||
}
|
||||
|
||||
impl RoomDelegate {
|
||||
fn new(weak_room: Weak<Room>) -> Self {
|
||||
let weak_room = Weak::into_raw(weak_room);
|
||||
let native_delegate = unsafe {
|
||||
LKRoomDelegateCreate(
|
||||
weak_room as *mut c_void,
|
||||
Self::on_did_subscribe_to_remote_video_track,
|
||||
)
|
||||
};
|
||||
Self {
|
||||
native_delegate,
|
||||
weak_room,
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn on_did_subscribe_to_remote_video_track(room: *mut c_void, track: *const c_void) {
|
||||
let room = unsafe { Weak::from_raw(room as *mut Room) };
|
||||
let track = RemoteVideoTrack(track);
|
||||
if let Some(room) = room.upgrade() {
|
||||
room.did_subscribe_to_remote_video_track(track);
|
||||
}
|
||||
let _ = Weak::into_raw(room);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RoomDelegate {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
LKRelease(self.native_delegate);
|
||||
let _ = Weak::from_raw(self.weak_room);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LocalVideoTrack(*const c_void);
|
||||
|
||||
impl LocalVideoTrack {
|
||||
pub fn screen_share_for_window(window_id: u32) -> Self {
|
||||
Self(unsafe { LKCreateScreenShareTrackForWindow(window_id) })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LocalVideoTrack {
|
||||
fn drop(&mut self) {
|
||||
unsafe { LKRelease(self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RemoteVideoTrack(*const c_void);
|
||||
|
||||
impl RemoteVideoTrack {
|
||||
pub fn add_renderer<F>(&self, callback: F)
|
||||
where
|
||||
F: 'static + FnMut(CVImageBuffer),
|
||||
{
|
||||
extern "C" fn on_frame<F>(callback_data: *mut c_void, frame: CVImageBufferRef)
|
||||
where
|
||||
F: FnMut(CVImageBuffer),
|
||||
{
|
||||
unsafe {
|
||||
let buffer = CVImageBuffer::wrap_under_get_rule(frame);
|
||||
let callback = &mut *(callback_data as *mut F);
|
||||
callback(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn on_drop<F>(callback_data: *mut c_void) {
|
||||
unsafe {
|
||||
let _ = Box::from_raw(callback_data as *mut F);
|
||||
}
|
||||
}
|
||||
|
||||
let callback_data = Box::into_raw(Box::new(callback));
|
||||
unsafe {
|
||||
let renderer =
|
||||
LKVideoRendererCreate(callback_data as *mut c_void, on_frame::<F>, on_drop::<F>);
|
||||
LKVideoTrackAddRenderer(self.0, renderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RemoteVideoTrack {
|
||||
fn drop(&mut self) {
|
||||
unsafe { LKRelease(self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WindowInfo {
|
||||
pub id: u32,
|
||||
pub owner_pid: i32,
|
||||
pub owner_name: Option<String>,
|
||||
}
|
||||
|
||||
pub fn list_windows() -> Vec<WindowInfo> {
|
||||
unsafe {
|
||||
let dicts = CFArray::<CFDictionary>::wrap_under_get_rule(CGWindowListCopyWindowInfo(
|
||||
kCGWindowListOptionOnScreenOnly | kCGWindowListOptionExcludeDesktopElements,
|
||||
kCGNullWindowID,
|
||||
));
|
||||
|
||||
dicts
|
||||
.iter()
|
||||
.map(|dict| {
|
||||
let id =
|
||||
CFNumber::wrap_under_get_rule(*dict.get(kCGWindowNumber.as_void_ptr()) as _)
|
||||
.to_i64()
|
||||
.unwrap() as u32;
|
||||
|
||||
let owner_pid =
|
||||
CFNumber::wrap_under_get_rule(*dict.get(kCGWindowOwnerPID.as_void_ptr()) as _)
|
||||
.to_i32()
|
||||
.unwrap();
|
||||
|
||||
let owner_name = dict
|
||||
.find(kCGWindowOwnerName.as_void_ptr())
|
||||
.map(|name| CFString::wrap_under_get_rule(*name as _).to_string());
|
||||
WindowInfo {
|
||||
id,
|
||||
owner_pid,
|
||||
owner_name,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
20
crates/media/Cargo.toml
Normal file
20
crates/media/Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "media"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/media.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
block = "0.1"
|
||||
bytes = "1.2"
|
||||
core-foundation = "0.9.3"
|
||||
foreign-types = "0.3"
|
||||
metal = "0.21.0"
|
||||
objc = "0.2"
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.59.2"
|
39
crates/media/build.rs
Normal file
39
crates/media/build.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
use std::{env, path::PathBuf, process::Command};
|
||||
|
||||
fn main() {
|
||||
let sdk_path = String::from_utf8(
|
||||
Command::new("xcrun")
|
||||
.args(&["--sdk", "macosx", "--show-sdk-path"])
|
||||
.output()
|
||||
.unwrap()
|
||||
.stdout,
|
||||
)
|
||||
.unwrap();
|
||||
let sdk_path = sdk_path.trim_end();
|
||||
|
||||
println!("cargo:rerun-if-changed=src/bindings.h");
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header("src/bindings.h")
|
||||
.clang_arg(format!("-isysroot{}", sdk_path))
|
||||
.clang_arg("-xobjective-c")
|
||||
.allowlist_type("CMItemIndex")
|
||||
.allowlist_type("CMSampleTimingInfo")
|
||||
.allowlist_type("CMVideoCodecType")
|
||||
.allowlist_type("VTEncodeInfoFlags")
|
||||
.allowlist_function("CMTimeMake")
|
||||
.allowlist_var("kCVPixelFormatType_.*")
|
||||
.allowlist_var("kCVReturn.*")
|
||||
.allowlist_var("VTEncodeInfoFlags_.*")
|
||||
.allowlist_var("kCMVideoCodecType_.*")
|
||||
.allowlist_var("kCMTime.*")
|
||||
.allowlist_var("kCMSampleAttachmentKey_.*")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
|
||||
.layout_tests(false)
|
||||
.generate()
|
||||
.expect("unable to generate bindings");
|
||||
|
||||
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
bindings
|
||||
.write_to_file(out_path.join("bindings.rs"))
|
||||
.expect("couldn't write dispatch bindings");
|
||||
}
|
5
crates/media/src/bindings.h
Normal file
5
crates/media/src/bindings.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
#import <CoreMedia/CMFormatDescription.h>
|
||||
#import <CoreMedia/CMSampleBuffer.h>
|
||||
#import <CoreVideo/CVPixelFormatDescription.h>
|
||||
#import <CoreVideo/CVReturn.h>
|
||||
#import <VideoToolbox/VTCompressionSession.h>
|
8
crates/media/src/bindings.rs
Normal file
8
crates/media/src/bindings.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(unused)]
|
||||
|
||||
use objc::*;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
534
crates/media/src/media.rs
Normal file
534
crates/media/src/media.rs
Normal file
|
@ -0,0 +1,534 @@
|
|||
#![allow(non_snake_case)]
|
||||
#![allow(non_camel_case_types)]
|
||||
|
||||
mod bindings;
|
||||
|
||||
use core_foundation::{
|
||||
base::{CFTypeID, TCFType},
|
||||
declare_TCFType, impl_CFTypeDescription, impl_TCFType,
|
||||
};
|
||||
use std::ffi::c_void;
|
||||
|
||||
pub mod io_surface {
|
||||
use super::*;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct __IOSurface(c_void);
|
||||
// The ref type must be a pointer to the underlying struct.
|
||||
pub type IOSurfaceRef = *const __IOSurface;
|
||||
|
||||
declare_TCFType!(IOSurface, IOSurfaceRef);
|
||||
impl_TCFType!(IOSurface, IOSurfaceRef, IOSurfaceGetTypeID);
|
||||
impl_CFTypeDescription!(IOSurface);
|
||||
|
||||
#[link(name = "IOSurface", kind = "framework")]
|
||||
extern "C" {
|
||||
fn IOSurfaceGetTypeID() -> CFTypeID;
|
||||
}
|
||||
}
|
||||
|
||||
pub mod core_video {
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use super::*;
|
||||
pub use crate::bindings::{
|
||||
kCVPixelFormatType_32BGRA, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
|
||||
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, kCVPixelFormatType_420YpCbCr8Planar,
|
||||
};
|
||||
use crate::bindings::{kCVReturnSuccess, CVReturn, OSType};
|
||||
use anyhow::{anyhow, Result};
|
||||
use core_foundation::{
|
||||
base::kCFAllocatorDefault, dictionary::CFDictionaryRef, mach_port::CFAllocatorRef,
|
||||
};
|
||||
use foreign_types::ForeignTypeRef;
|
||||
use io_surface::{IOSurface, IOSurfaceRef};
|
||||
use metal::{MTLDevice, MTLPixelFormat};
|
||||
use std::ptr;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct __CVImageBuffer(c_void);
|
||||
// The ref type must be a pointer to the underlying struct.
|
||||
pub type CVImageBufferRef = *const __CVImageBuffer;
|
||||
|
||||
declare_TCFType!(CVImageBuffer, CVImageBufferRef);
|
||||
impl_TCFType!(CVImageBuffer, CVImageBufferRef, CVImageBufferGetTypeID);
|
||||
impl_CFTypeDescription!(CVImageBuffer);
|
||||
|
||||
impl CVImageBuffer {
|
||||
pub fn io_surface(&self) -> IOSurface {
|
||||
unsafe {
|
||||
IOSurface::wrap_under_get_rule(CVPixelBufferGetIOSurface(
|
||||
self.as_concrete_TypeRef(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
unsafe { CVPixelBufferGetWidth(self.as_concrete_TypeRef()) }
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
unsafe { CVPixelBufferGetHeight(self.as_concrete_TypeRef()) }
|
||||
}
|
||||
|
||||
pub fn plane_width(&self, plane: usize) -> usize {
|
||||
unsafe { CVPixelBufferGetWidthOfPlane(self.as_concrete_TypeRef(), plane) }
|
||||
}
|
||||
|
||||
pub fn plane_height(&self, plane: usize) -> usize {
|
||||
unsafe { CVPixelBufferGetHeightOfPlane(self.as_concrete_TypeRef(), plane) }
|
||||
}
|
||||
|
||||
pub fn pixel_format_type(&self) -> OSType {
|
||||
unsafe { CVPixelBufferGetPixelFormatType(self.as_concrete_TypeRef()) }
|
||||
}
|
||||
}
|
||||
|
||||
#[link(name = "CoreVideo", kind = "framework")]
|
||||
extern "C" {
|
||||
fn CVImageBufferGetTypeID() -> CFTypeID;
|
||||
fn CVPixelBufferGetIOSurface(buffer: CVImageBufferRef) -> IOSurfaceRef;
|
||||
fn CVPixelBufferGetWidth(buffer: CVImageBufferRef) -> usize;
|
||||
fn CVPixelBufferGetHeight(buffer: CVImageBufferRef) -> usize;
|
||||
fn CVPixelBufferGetWidthOfPlane(buffer: CVImageBufferRef, plane: usize) -> usize;
|
||||
fn CVPixelBufferGetHeightOfPlane(buffer: CVImageBufferRef, plane: usize) -> usize;
|
||||
fn CVPixelBufferGetPixelFormatType(buffer: CVImageBufferRef) -> OSType;
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct __CVMetalTextureCache(c_void);
|
||||
pub type CVMetalTextureCacheRef = *const __CVMetalTextureCache;
|
||||
|
||||
declare_TCFType!(CVMetalTextureCache, CVMetalTextureCacheRef);
|
||||
impl_TCFType!(
|
||||
CVMetalTextureCache,
|
||||
CVMetalTextureCacheRef,
|
||||
CVMetalTextureCacheGetTypeID
|
||||
);
|
||||
impl_CFTypeDescription!(CVMetalTextureCache);
|
||||
|
||||
impl CVMetalTextureCache {
|
||||
pub fn new(metal_device: *mut MTLDevice) -> Result<Self> {
|
||||
unsafe {
|
||||
let mut this = ptr::null();
|
||||
let result = CVMetalTextureCacheCreate(
|
||||
kCFAllocatorDefault,
|
||||
ptr::null_mut(),
|
||||
metal_device,
|
||||
ptr::null_mut(),
|
||||
&mut this,
|
||||
);
|
||||
if result == kCVReturnSuccess {
|
||||
Ok(CVMetalTextureCache::wrap_under_create_rule(this))
|
||||
} else {
|
||||
Err(anyhow!("could not create texture cache, code: {}", result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_texture_from_image(
|
||||
&self,
|
||||
source: CVImageBufferRef,
|
||||
texture_attributes: CFDictionaryRef,
|
||||
pixel_format: MTLPixelFormat,
|
||||
width: usize,
|
||||
height: usize,
|
||||
plane_index: usize,
|
||||
) -> Result<CVMetalTexture> {
|
||||
unsafe {
|
||||
let mut this = ptr::null();
|
||||
let result = CVMetalTextureCacheCreateTextureFromImage(
|
||||
kCFAllocatorDefault,
|
||||
self.as_concrete_TypeRef(),
|
||||
source,
|
||||
texture_attributes,
|
||||
pixel_format,
|
||||
width,
|
||||
height,
|
||||
plane_index,
|
||||
&mut this,
|
||||
);
|
||||
if result == kCVReturnSuccess {
|
||||
Ok(CVMetalTexture::wrap_under_create_rule(this))
|
||||
} else {
|
||||
Err(anyhow!("could not create texture, code: {}", result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[link(name = "CoreVideo", kind = "framework")]
|
||||
extern "C" {
|
||||
fn CVMetalTextureCacheGetTypeID() -> CFTypeID;
|
||||
fn CVMetalTextureCacheCreate(
|
||||
allocator: CFAllocatorRef,
|
||||
cache_attributes: CFDictionaryRef,
|
||||
metal_device: *const MTLDevice,
|
||||
texture_attributes: CFDictionaryRef,
|
||||
cache_out: *mut CVMetalTextureCacheRef,
|
||||
) -> CVReturn;
|
||||
fn CVMetalTextureCacheCreateTextureFromImage(
|
||||
allocator: CFAllocatorRef,
|
||||
texture_cache: CVMetalTextureCacheRef,
|
||||
source_image: CVImageBufferRef,
|
||||
texture_attributes: CFDictionaryRef,
|
||||
pixel_format: MTLPixelFormat,
|
||||
width: usize,
|
||||
height: usize,
|
||||
plane_index: usize,
|
||||
texture_out: *mut CVMetalTextureRef,
|
||||
) -> CVReturn;
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct __CVMetalTexture(c_void);
|
||||
pub type CVMetalTextureRef = *const __CVMetalTexture;
|
||||
|
||||
declare_TCFType!(CVMetalTexture, CVMetalTextureRef);
|
||||
impl_TCFType!(CVMetalTexture, CVMetalTextureRef, CVMetalTextureGetTypeID);
|
||||
impl_CFTypeDescription!(CVMetalTexture);
|
||||
|
||||
impl CVMetalTexture {
|
||||
pub fn as_texture_ref(&self) -> &metal::TextureRef {
|
||||
unsafe {
|
||||
let texture = CVMetalTextureGetTexture(self.as_concrete_TypeRef());
|
||||
&metal::TextureRef::from_ptr(texture as *mut _)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[link(name = "CoreVideo", kind = "framework")]
|
||||
extern "C" {
|
||||
fn CVMetalTextureGetTypeID() -> CFTypeID;
|
||||
fn CVMetalTextureGetTexture(texture: CVMetalTextureRef) -> *mut c_void;
|
||||
}
|
||||
}
|
||||
|
||||
pub mod core_media {
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
pub use crate::bindings::{
|
||||
kCMSampleAttachmentKey_NotSync, kCMTimeInvalid, kCMVideoCodecType_H264, CMItemIndex,
|
||||
CMSampleTimingInfo, CMTime, CMTimeMake, CMVideoCodecType,
|
||||
};
|
||||
use crate::core_video::{CVImageBuffer, CVImageBufferRef};
|
||||
use anyhow::{anyhow, Result};
|
||||
use core_foundation::{
|
||||
array::{CFArray, CFArrayRef},
|
||||
base::{CFTypeID, OSStatus, TCFType},
|
||||
declare_TCFType,
|
||||
dictionary::CFDictionary,
|
||||
impl_CFTypeDescription, impl_TCFType,
|
||||
string::CFString,
|
||||
};
|
||||
use std::{ffi::c_void, ptr};
|
||||
|
||||
#[repr(C)]
|
||||
pub struct __CMSampleBuffer(c_void);
|
||||
// The ref type must be a pointer to the underlying struct.
|
||||
pub type CMSampleBufferRef = *const __CMSampleBuffer;
|
||||
|
||||
declare_TCFType!(CMSampleBuffer, CMSampleBufferRef);
|
||||
impl_TCFType!(CMSampleBuffer, CMSampleBufferRef, CMSampleBufferGetTypeID);
|
||||
impl_CFTypeDescription!(CMSampleBuffer);
|
||||
|
||||
impl CMSampleBuffer {
|
||||
pub fn attachments(&self) -> Vec<CFDictionary<CFString>> {
|
||||
unsafe {
|
||||
let attachments =
|
||||
CMSampleBufferGetSampleAttachmentsArray(self.as_concrete_TypeRef(), true);
|
||||
CFArray::<CFDictionary>::wrap_under_get_rule(attachments)
|
||||
.into_iter()
|
||||
.map(|attachments| {
|
||||
CFDictionary::wrap_under_get_rule(attachments.as_concrete_TypeRef())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn image_buffer(&self) -> CVImageBuffer {
|
||||
unsafe {
|
||||
CVImageBuffer::wrap_under_get_rule(CMSampleBufferGetImageBuffer(
|
||||
self.as_concrete_TypeRef(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sample_timing_info(&self, index: usize) -> Result<CMSampleTimingInfo> {
|
||||
unsafe {
|
||||
let mut timing_info = CMSampleTimingInfo {
|
||||
duration: kCMTimeInvalid,
|
||||
presentationTimeStamp: kCMTimeInvalid,
|
||||
decodeTimeStamp: kCMTimeInvalid,
|
||||
};
|
||||
let result = CMSampleBufferGetSampleTimingInfo(
|
||||
self.as_concrete_TypeRef(),
|
||||
index as CMItemIndex,
|
||||
&mut timing_info,
|
||||
);
|
||||
|
||||
if result == 0 {
|
||||
Ok(timing_info)
|
||||
} else {
|
||||
Err(anyhow!("error getting sample timing info, code {}", result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_description(&self) -> CMFormatDescription {
|
||||
unsafe {
|
||||
CMFormatDescription::wrap_under_get_rule(CMSampleBufferGetFormatDescription(
|
||||
self.as_concrete_TypeRef(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn data(&self) -> CMBlockBuffer {
|
||||
unsafe {
|
||||
CMBlockBuffer::wrap_under_get_rule(CMSampleBufferGetDataBuffer(
|
||||
self.as_concrete_TypeRef(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[link(name = "CoreMedia", kind = "framework")]
|
||||
extern "C" {
|
||||
fn CMSampleBufferGetTypeID() -> CFTypeID;
|
||||
fn CMSampleBufferGetSampleAttachmentsArray(
|
||||
buffer: CMSampleBufferRef,
|
||||
create_if_necessary: bool,
|
||||
) -> CFArrayRef;
|
||||
fn CMSampleBufferGetImageBuffer(buffer: CMSampleBufferRef) -> CVImageBufferRef;
|
||||
fn CMSampleBufferGetSampleTimingInfo(
|
||||
buffer: CMSampleBufferRef,
|
||||
index: CMItemIndex,
|
||||
timing_info_out: *mut CMSampleTimingInfo,
|
||||
) -> OSStatus;
|
||||
fn CMSampleBufferGetFormatDescription(buffer: CMSampleBufferRef) -> CMFormatDescriptionRef;
|
||||
fn CMSampleBufferGetDataBuffer(sample_buffer: CMSampleBufferRef) -> CMBlockBufferRef;
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct __CMFormatDescription(c_void);
|
||||
pub type CMFormatDescriptionRef = *const __CMFormatDescription;
|
||||
|
||||
declare_TCFType!(CMFormatDescription, CMFormatDescriptionRef);
|
||||
impl_TCFType!(
|
||||
CMFormatDescription,
|
||||
CMFormatDescriptionRef,
|
||||
CMFormatDescriptionGetTypeID
|
||||
);
|
||||
impl_CFTypeDescription!(CMFormatDescription);
|
||||
|
||||
impl CMFormatDescription {
|
||||
pub fn h264_parameter_set_count(&self) -> usize {
|
||||
unsafe {
|
||||
let mut count = 0;
|
||||
let result = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
|
||||
self.as_concrete_TypeRef(),
|
||||
0,
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
&mut count,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
assert_eq!(result, 0);
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
pub fn h264_parameter_set_at_index(&self, index: usize) -> Result<&[u8]> {
|
||||
unsafe {
|
||||
let mut bytes = ptr::null();
|
||||
let mut len = 0;
|
||||
let result = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
|
||||
self.as_concrete_TypeRef(),
|
||||
index,
|
||||
&mut bytes,
|
||||
&mut len,
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
);
|
||||
if result == 0 {
|
||||
Ok(std::slice::from_raw_parts(bytes, len))
|
||||
} else {
|
||||
Err(anyhow!("error getting parameter set, code: {}", result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[link(name = "CoreMedia", kind = "framework")]
|
||||
extern "C" {
|
||||
fn CMFormatDescriptionGetTypeID() -> CFTypeID;
|
||||
fn CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
|
||||
video_desc: CMFormatDescriptionRef,
|
||||
parameter_set_index: usize,
|
||||
parameter_set_pointer_out: *mut *const u8,
|
||||
parameter_set_size_out: *mut usize,
|
||||
parameter_set_count_out: *mut usize,
|
||||
NALUnitHeaderLengthOut: *mut isize,
|
||||
) -> OSStatus;
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct __CMBlockBuffer(c_void);
|
||||
pub type CMBlockBufferRef = *const __CMBlockBuffer;
|
||||
|
||||
declare_TCFType!(CMBlockBuffer, CMBlockBufferRef);
|
||||
impl_TCFType!(CMBlockBuffer, CMBlockBufferRef, CMBlockBufferGetTypeID);
|
||||
impl_CFTypeDescription!(CMBlockBuffer);
|
||||
|
||||
impl CMBlockBuffer {
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
unsafe {
|
||||
let mut bytes = ptr::null();
|
||||
let mut len = 0;
|
||||
let result = CMBlockBufferGetDataPointer(
|
||||
self.as_concrete_TypeRef(),
|
||||
0,
|
||||
&mut 0,
|
||||
&mut len,
|
||||
&mut bytes,
|
||||
);
|
||||
assert!(result == 0, "could not get block buffer data");
|
||||
std::slice::from_raw_parts(bytes, len)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[link(name = "CoreMedia", kind = "framework")]
|
||||
extern "C" {
|
||||
fn CMBlockBufferGetTypeID() -> CFTypeID;
|
||||
fn CMBlockBufferGetDataPointer(
|
||||
buffer: CMBlockBufferRef,
|
||||
offset: usize,
|
||||
length_at_offset_out: *mut usize,
|
||||
total_length_out: *mut usize,
|
||||
data_pointer_out: *mut *const u8,
|
||||
) -> OSStatus;
|
||||
}
|
||||
}
|
||||
|
||||
pub mod video_toolbox {
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
core_media::{CMSampleBufferRef, CMTime, CMVideoCodecType},
|
||||
core_video::CVImageBufferRef,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
pub use bindings::VTEncodeInfoFlags;
|
||||
use core_foundation::{base::OSStatus, dictionary::CFDictionaryRef, mach_port::CFAllocatorRef};
|
||||
use std::ptr;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct __VTCompressionSession(c_void);
|
||||
// The ref type must be a pointer to the underlying struct.
|
||||
pub type VTCompressionSessionRef = *const __VTCompressionSession;
|
||||
|
||||
declare_TCFType!(VTCompressionSession, VTCompressionSessionRef);
|
||||
impl_TCFType!(
|
||||
VTCompressionSession,
|
||||
VTCompressionSessionRef,
|
||||
VTCompressionSessionGetTypeID
|
||||
);
|
||||
impl_CFTypeDescription!(VTCompressionSession);
|
||||
|
||||
impl VTCompressionSession {
|
||||
pub fn new(
|
||||
width: usize,
|
||||
height: usize,
|
||||
codec: CMVideoCodecType,
|
||||
callback: VTCompressionOutputCallback,
|
||||
callback_data: *const c_void,
|
||||
) -> Result<Self> {
|
||||
unsafe {
|
||||
let mut this = ptr::null();
|
||||
let result = VTCompressionSessionCreate(
|
||||
ptr::null(),
|
||||
width as i32,
|
||||
height as i32,
|
||||
codec,
|
||||
ptr::null(),
|
||||
ptr::null(),
|
||||
ptr::null(),
|
||||
callback,
|
||||
callback_data,
|
||||
&mut this,
|
||||
);
|
||||
|
||||
if result == 0 {
|
||||
Ok(Self::wrap_under_create_rule(this))
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"error creating compression session, code {}",
|
||||
result
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_frame(
|
||||
&self,
|
||||
buffer: CVImageBufferRef,
|
||||
presentation_timestamp: CMTime,
|
||||
duration: CMTime,
|
||||
) -> Result<()> {
|
||||
unsafe {
|
||||
let result = VTCompressionSessionEncodeFrame(
|
||||
self.as_concrete_TypeRef(),
|
||||
buffer,
|
||||
presentation_timestamp,
|
||||
duration,
|
||||
ptr::null(),
|
||||
ptr::null(),
|
||||
ptr::null_mut(),
|
||||
);
|
||||
if result == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("error encoding frame, code {}", result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type VTCompressionOutputCallback = Option<
|
||||
unsafe extern "C" fn(
|
||||
outputCallbackRefCon: *mut c_void,
|
||||
sourceFrameRefCon: *mut c_void,
|
||||
status: OSStatus,
|
||||
infoFlags: VTEncodeInfoFlags,
|
||||
sampleBuffer: CMSampleBufferRef,
|
||||
),
|
||||
>;
|
||||
|
||||
#[link(name = "VideoToolbox", kind = "framework")]
|
||||
extern "C" {
|
||||
fn VTCompressionSessionGetTypeID() -> CFTypeID;
|
||||
fn VTCompressionSessionCreate(
|
||||
allocator: CFAllocatorRef,
|
||||
width: i32,
|
||||
height: i32,
|
||||
codec_type: CMVideoCodecType,
|
||||
encoder_specification: CFDictionaryRef,
|
||||
source_image_buffer_attributes: CFDictionaryRef,
|
||||
compressed_data_allocator: CFAllocatorRef,
|
||||
output_callback: VTCompressionOutputCallback,
|
||||
output_callback_ref_con: *const c_void,
|
||||
compression_session_out: *mut VTCompressionSessionRef,
|
||||
) -> OSStatus;
|
||||
fn VTCompressionSessionEncodeFrame(
|
||||
session: VTCompressionSessionRef,
|
||||
image_buffer: CVImageBufferRef,
|
||||
presentation_timestamp: CMTime,
|
||||
duration: CMTime,
|
||||
frame_properties: CFDictionaryRef,
|
||||
source_frame_ref_con: *const c_void,
|
||||
output_flags: *mut VTEncodeInfoFlags,
|
||||
) -> OSStatus;
|
||||
}
|
||||
}
|
|
@ -85,7 +85,7 @@ impl<D: PickerDelegate> View for Picker<D> {
|
|||
let selected_ix = delegate.read(cx).selected_index();
|
||||
range.end = cmp::min(range.end, delegate.read(cx).match_count());
|
||||
items.extend(range.map(move |ix| {
|
||||
MouseEventHandler::new::<D, _, _>(ix, cx, |state, cx| {
|
||||
MouseEventHandler::<D>::new(ix, cx, |state, cx| {
|
||||
delegate
|
||||
.read(cx)
|
||||
.render_match(ix, state, ix == selected_ix, cx)
|
||||
|
|
|
@ -5,8 +5,8 @@ use gpui::{
|
|||
actions,
|
||||
anyhow::{anyhow, Result},
|
||||
elements::{
|
||||
ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement,
|
||||
ScrollTarget, Stack, Svg, UniformList, UniformListState,
|
||||
AnchorCorner, ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler,
|
||||
ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
|
||||
},
|
||||
geometry::vector::Vector2F,
|
||||
impl_internal_actions, keymap,
|
||||
|
@ -302,7 +302,7 @@ impl ProjectPanel {
|
|||
}
|
||||
|
||||
self.context_menu.update(cx, |menu, cx| {
|
||||
menu.show(action.position, menu_entries, cx);
|
||||
menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx);
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
|
@ -1012,7 +1012,7 @@ impl ProjectPanel {
|
|||
) -> ElementBox {
|
||||
let kind = details.kind;
|
||||
let show_editor = details.is_editing && !details.is_processing;
|
||||
MouseEventHandler::new::<Self, _, _>(entry_id.to_usize(), cx, |state, _| {
|
||||
MouseEventHandler::<Self>::new(entry_id.to_usize(), cx, |state, _| {
|
||||
let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
|
||||
let mut style = theme.entry.style_for(state, details.is_selected).clone();
|
||||
if details.is_ignored {
|
||||
|
@ -1107,7 +1107,7 @@ impl View for ProjectPanel {
|
|||
let last_worktree_root_id = self.last_worktree_root_id;
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Tag, _, _>(0, cx, |_, cx| {
|
||||
MouseEventHandler::<Tag>::new(0, cx, |_, cx| {
|
||||
UniformList::new(
|
||||
self.list.clone(),
|
||||
self.visible_entries
|
||||
|
@ -1243,7 +1243,8 @@ mod tests {
|
|||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
||||
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
|
@ -1335,7 +1336,8 @@ mod tests {
|
|||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
||||
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
|
||||
|
||||
select_path(&panel, "root1", cx);
|
||||
|
|
|
@ -319,7 +319,7 @@ impl BufferSearchBar {
|
|||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||
let is_active = self.is_search_option_enabled(option);
|
||||
Some(
|
||||
MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
|
||||
MouseEventHandler::<Self>::new(option as usize, cx, |state, cx| {
|
||||
let style = &cx
|
||||
.global::<Settings>()
|
||||
.theme
|
||||
|
@ -367,7 +367,7 @@ impl BufferSearchBar {
|
|||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||
|
||||
enum NavButton {}
|
||||
MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
|
||||
MouseEventHandler::<NavButton>::new(direction as usize, cx, |state, cx| {
|
||||
let style = &cx
|
||||
.global::<Settings>()
|
||||
.theme
|
||||
|
|
|
@ -176,7 +176,7 @@ impl View for ProjectSearchView {
|
|||
} else {
|
||||
"No results"
|
||||
};
|
||||
MouseEventHandler::new::<Status, _, _>(0, cx, |_, _| {
|
||||
MouseEventHandler::<Status>::new(0, cx, |_, _| {
|
||||
Label::new(text.to_string(), theme.search.results_status.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
|
@ -723,7 +723,7 @@ impl ProjectSearchBar {
|
|||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||
|
||||
enum NavButton {}
|
||||
MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
|
||||
MouseEventHandler::<NavButton>::new(direction as usize, cx, |state, cx| {
|
||||
let style = &cx
|
||||
.global::<Settings>()
|
||||
.theme
|
||||
|
@ -758,7 +758,7 @@ impl ProjectSearchBar {
|
|||
) -> ElementBox {
|
||||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||
let is_active = self.is_option_enabled(option, cx);
|
||||
MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
|
||||
MouseEventHandler::<Self>::new(option as usize, cx, |state, cx| {
|
||||
let style = &cx
|
||||
.global::<Settings>()
|
||||
.theme
|
||||
|
|
|
@ -29,6 +29,7 @@ pub struct Settings {
|
|||
pub show_completions_on_input: bool,
|
||||
pub vim_mode: bool,
|
||||
pub autosave: Autosave,
|
||||
pub default_dock_anchor: DockAnchor,
|
||||
pub editor_defaults: EditorSettings,
|
||||
pub editor_overrides: EditorSettings,
|
||||
pub terminal_defaults: TerminalSettings,
|
||||
|
@ -151,6 +152,15 @@ pub enum WorkingDirectory {
|
|||
Always { directory: String },
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DockAnchor {
|
||||
#[default]
|
||||
Bottom,
|
||||
Right,
|
||||
Expanded,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
|
||||
pub struct SettingsFileContent {
|
||||
pub experiments: Option<FeatureFlags>,
|
||||
|
@ -168,6 +178,8 @@ pub struct SettingsFileContent {
|
|||
pub vim_mode: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub autosave: Option<Autosave>,
|
||||
#[serde(default)]
|
||||
pub default_dock_anchor: Option<DockAnchor>,
|
||||
#[serde(flatten)]
|
||||
pub editor: EditorSettings,
|
||||
#[serde(default)]
|
||||
|
@ -217,6 +229,7 @@ impl Settings {
|
|||
projects_online_by_default: defaults.projects_online_by_default.unwrap(),
|
||||
vim_mode: defaults.vim_mode.unwrap(),
|
||||
autosave: defaults.autosave.unwrap(),
|
||||
default_dock_anchor: defaults.default_dock_anchor.unwrap(),
|
||||
editor_defaults: EditorSettings {
|
||||
tab_size: required(defaults.editor.tab_size),
|
||||
hard_tabs: required(defaults.editor.hard_tabs),
|
||||
|
@ -269,6 +282,8 @@ impl Settings {
|
|||
merge(&mut self.autosave, data.autosave);
|
||||
merge(&mut self.experiments, data.experiments);
|
||||
merge(&mut self.staff_mode, data.staff_mode);
|
||||
merge(&mut self.default_dock_anchor, data.default_dock_anchor);
|
||||
|
||||
// Ensure terminal font is loaded, so we can request it in terminal_element layout
|
||||
if let Some(terminal_font) = &data.terminal.font_family {
|
||||
font_cache.load_family(&[terminal_font]).log_err();
|
||||
|
@ -337,6 +352,7 @@ impl Settings {
|
|||
show_completions_on_input: true,
|
||||
vim_mode: false,
|
||||
autosave: Autosave::Off,
|
||||
default_dock_anchor: DockAnchor::Bottom,
|
||||
editor_defaults: EditorSettings {
|
||||
tab_size: Some(4.try_into().unwrap()),
|
||||
hard_tabs: Some(false),
|
||||
|
|
|
@ -6,6 +6,7 @@ use alacritty_terminal::grid::Dimensions;
|
|||
/// with modifications for our circumstances
|
||||
use alacritty_terminal::index::{Column as GridCol, Line as GridLine, Point, Side};
|
||||
use alacritty_terminal::term::TermMode;
|
||||
use gpui::scene::ScrollWheelRegionEvent;
|
||||
use gpui::{geometry::vector::Vector2F, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
|
||||
|
||||
use crate::TerminalSize;
|
||||
|
@ -114,7 +115,7 @@ impl MouseButton {
|
|||
pub fn scroll_report(
|
||||
point: Point,
|
||||
scroll_lines: i32,
|
||||
e: &ScrollWheelEvent,
|
||||
e: &ScrollWheelRegionEvent,
|
||||
mode: TermMode,
|
||||
) -> Option<impl Iterator<Item = Vec<u8>>> {
|
||||
if mode.intersects(TermMode::MOUSE_MODE) {
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
use gpui::{ModelHandle, ViewContext};
|
||||
use settings::{Settings, WorkingDirectory};
|
||||
use workspace::{programs::ProgramManager, Workspace};
|
||||
|
||||
use crate::{
|
||||
terminal_container_view::{
|
||||
get_working_directory, DeployModal, TerminalContainer, TerminalContainerContent,
|
||||
},
|
||||
Event, Terminal,
|
||||
};
|
||||
|
||||
pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext<Workspace>) {
|
||||
let window = cx.window_id();
|
||||
|
||||
// Pull the terminal connection out of the global if it has been stored
|
||||
let possible_terminal = ProgramManager::remove::<Terminal, _>(window, cx);
|
||||
|
||||
if let Some(terminal_handle) = possible_terminal {
|
||||
workspace.toggle_modal(cx, |_, cx| {
|
||||
// Create a view from the stored connection if the terminal modal is not already shown
|
||||
cx.add_view(|cx| TerminalContainer::from_terminal(terminal_handle.clone(), true, cx))
|
||||
});
|
||||
// Toggle Modal will dismiss the terminal modal if it is currently shown, so we must
|
||||
// store the terminal back in the global
|
||||
ProgramManager::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
|
||||
} else {
|
||||
// No connection was stored, create a new terminal
|
||||
if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| {
|
||||
// No terminal modal visible, construct a new one.
|
||||
let wd_strategy = cx
|
||||
.global::<Settings>()
|
||||
.terminal_overrides
|
||||
.working_directory
|
||||
.clone()
|
||||
.unwrap_or(WorkingDirectory::CurrentProjectDirectory);
|
||||
|
||||
let working_directory = get_working_directory(workspace, cx, wd_strategy);
|
||||
|
||||
let this = cx.add_view(|cx| TerminalContainer::new(working_directory, true, cx));
|
||||
|
||||
if let TerminalContainerContent::Connected(connected) = &this.read(cx).content {
|
||||
let terminal_handle = connected.read(cx).handle();
|
||||
cx.subscribe(&terminal_handle, on_event).detach();
|
||||
// Set the global immediately if terminal construction was successful,
|
||||
// in case the user opens the command palette
|
||||
ProgramManager::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
|
||||
}
|
||||
|
||||
this
|
||||
}) {
|
||||
// Terminal modal was dismissed and the terminal view is connected, store the terminal
|
||||
if let TerminalContainerContent::Connected(connected) =
|
||||
&closed_terminal_handle.read(cx).content
|
||||
{
|
||||
let terminal_handle = connected.read(cx).handle();
|
||||
// Set the global immediately if terminal construction was successful,
|
||||
// in case the user opens the command palette
|
||||
ProgramManager::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_event(
|
||||
workspace: &mut Workspace,
|
||||
_: ModelHandle<Terminal>,
|
||||
event: &Event,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
// Dismiss the modal if the terminal quit
|
||||
if let Event::CloseTerminal = event {
|
||||
ProgramManager::remove::<Terminal, _>(cx.window_id(), cx);
|
||||
|
||||
if workspace.modal::<TerminalContainer>().is_some() {
|
||||
workspace.dismiss_modal(cx)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
pub mod mappings;
|
||||
pub mod modal;
|
||||
pub mod terminal_container_view;
|
||||
pub mod terminal_element;
|
||||
pub mod terminal_view;
|
||||
|
@ -32,7 +31,6 @@ use futures::{
|
|||
use mappings::mouse::{
|
||||
alt_scroll, grid_point, mouse_button_report, mouse_moved_report, mouse_side, scroll_report,
|
||||
};
|
||||
use modal::deploy_modal;
|
||||
|
||||
use procinfo::LocalProcessInfo;
|
||||
use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
|
||||
|
@ -55,9 +53,10 @@ use thiserror::Error;
|
|||
use gpui::{
|
||||
geometry::vector::{vec2f, Vector2F},
|
||||
keymap::Keystroke,
|
||||
scene::{ClickRegionEvent, DownRegionEvent, DragRegionEvent, UpRegionEvent},
|
||||
ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext,
|
||||
ScrollWheelEvent, Task,
|
||||
scene::{
|
||||
ClickRegionEvent, DownRegionEvent, DragRegionEvent, ScrollWheelRegionEvent, UpRegionEvent,
|
||||
},
|
||||
ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext, Task,
|
||||
};
|
||||
|
||||
use crate::mappings::{
|
||||
|
@ -68,8 +67,6 @@ use lazy_static::lazy_static;
|
|||
|
||||
///Initialize and register all of our action handlers
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(deploy_modal);
|
||||
|
||||
terminal_view::init(cx);
|
||||
terminal_container_view::init(cx);
|
||||
}
|
||||
|
@ -1016,10 +1013,10 @@ impl Terminal {
|
|||
}
|
||||
|
||||
///Scroll the terminal
|
||||
pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, origin: Vector2F) {
|
||||
pub fn scroll_wheel(&mut self, e: ScrollWheelRegionEvent, origin: Vector2F) {
|
||||
let mouse_mode = self.mouse_mode(e.shift);
|
||||
|
||||
if let Some(scroll_lines) = self.determine_scroll_lines(e, mouse_mode) {
|
||||
if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) {
|
||||
if mouse_mode {
|
||||
let point = grid_point(
|
||||
e.position.sub(origin),
|
||||
|
@ -1028,7 +1025,7 @@ impl Terminal {
|
|||
);
|
||||
|
||||
if let Some(scrolls) =
|
||||
scroll_report(point, scroll_lines as i32, e, self.last_content.mode)
|
||||
scroll_report(point, scroll_lines as i32, &e, self.last_content.mode)
|
||||
{
|
||||
for scroll in scrolls {
|
||||
self.pty_tx.notify(scroll);
|
||||
|
@ -1051,7 +1048,11 @@ impl Terminal {
|
|||
}
|
||||
}
|
||||
|
||||
fn determine_scroll_lines(&mut self, e: &ScrollWheelEvent, mouse_mode: bool) -> Option<i32> {
|
||||
fn determine_scroll_lines(
|
||||
&mut self,
|
||||
e: &ScrollWheelRegionEvent,
|
||||
mouse_mode: bool,
|
||||
) -> Option<i32> {
|
||||
let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
|
||||
|
||||
match e.phase {
|
||||
|
|
|
@ -373,7 +373,7 @@ impl TerminalElement {
|
|||
) {
|
||||
let connection = self.terminal;
|
||||
|
||||
let mut region = MouseRegion::new(view_id, None, visible_bounds);
|
||||
let mut region = MouseRegion::new::<Self>(view_id, view_id, visible_bounds);
|
||||
|
||||
// Terminal Emulator controlled behavior:
|
||||
region = region
|
||||
|
@ -444,7 +444,14 @@ impl TerminalElement {
|
|||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.on_scroll(TerminalElement::generic_button_handler(
|
||||
connection,
|
||||
origin,
|
||||
move |terminal, origin, e, _cx| {
|
||||
terminal.scroll_wheel(e, origin);
|
||||
},
|
||||
));
|
||||
|
||||
// Mouse mode handlers:
|
||||
// All mouse modes need the extra click handlers
|
||||
|
@ -745,25 +752,13 @@ impl Element for TerminalElement {
|
|||
fn dispatch_event(
|
||||
&mut self,
|
||||
event: &gpui::Event,
|
||||
bounds: gpui::geometry::rect::RectF,
|
||||
visible_bounds: gpui::geometry::rect::RectF,
|
||||
layout: &mut Self::LayoutState,
|
||||
_bounds: gpui::geometry::rect::RectF,
|
||||
_visible_bounds: gpui::geometry::rect::RectF,
|
||||
_layout: &mut Self::LayoutState,
|
||||
_paint: &mut Self::PaintState,
|
||||
cx: &mut gpui::EventContext,
|
||||
) -> bool {
|
||||
match event {
|
||||
Event::ScrollWheel(e) => visible_bounds
|
||||
.contains_point(e.position)
|
||||
.then(|| {
|
||||
let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
|
||||
|
||||
if let Some(terminal) = self.terminal.upgrade(cx.app) {
|
||||
terminal.update(cx.app, |term, _| term.scroll_wheel(e, origin));
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.is_some(),
|
||||
Event::KeyDown(KeyDownEvent { keystroke, .. }) => {
|
||||
if let Event::KeyDown(KeyDownEvent { keystroke, .. }) = event {
|
||||
if !cx.is_parent_view_focused() {
|
||||
return false;
|
||||
}
|
||||
|
@ -789,8 +784,8 @@ impl Element for TerminalElement {
|
|||
})
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
_ => false,
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ use alacritty_terminal::{index::Point, term::TermMode};
|
|||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{ChildView, ParentElement, Stack},
|
||||
elements::{AnchorCorner, ChildView, ParentElement, Stack},
|
||||
geometry::vector::Vector2F,
|
||||
impl_internal_actions,
|
||||
keymap::Keystroke,
|
||||
|
@ -139,8 +139,9 @@ impl TerminalView {
|
|||
ContextMenuItem::item("Close Terminal", pane::CloseActiveItem),
|
||||
];
|
||||
|
||||
self.context_menu
|
||||
.update(cx, |menu, cx| menu.show(action.position, menu_entries, cx));
|
||||
self.context_menu.update(cx, |menu, cx| {
|
||||
menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx)
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
|
|
@ -28,7 +28,9 @@ impl<'a> TerminalTestContext<'a> {
|
|||
let params = self.cx.update(AppState::test);
|
||||
|
||||
let project = Project::test(params.fs.clone(), [], self.cx).await;
|
||||
let (_, workspace) = self.cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
let (_, workspace) = self
|
||||
.cx
|
||||
.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
||||
|
||||
(project, workspace)
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ pub struct Theme {
|
|||
pub workspace: Workspace,
|
||||
pub context_menu: ContextMenu,
|
||||
pub chat_panel: ChatPanel,
|
||||
pub contacts_popover: ContactsPopover,
|
||||
pub contacts_panel: ContactsPanel,
|
||||
pub contact_finder: ContactFinder,
|
||||
pub project_panel: ProjectPanel,
|
||||
|
@ -48,7 +49,7 @@ pub struct Workspace {
|
|||
pub pane_divider: Border,
|
||||
pub leader_border_opacity: f32,
|
||||
pub leader_border_width: f32,
|
||||
pub sidebar_resize_handle: ContainerStyle,
|
||||
pub sidebar: Sidebar,
|
||||
pub status_bar: StatusBar,
|
||||
pub toolbar: Toolbar,
|
||||
pub disconnected_overlay: ContainedText,
|
||||
|
@ -57,6 +58,7 @@ pub struct Workspace {
|
|||
pub notifications: Notifications,
|
||||
pub joining_project_avatar: ImageStyle,
|
||||
pub joining_project_message: ContainedText,
|
||||
pub dock: Dock,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
|
@ -79,6 +81,7 @@ pub struct TabBar {
|
|||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub pane_button: Interactive<IconButton>,
|
||||
pub pane_button_container: ContainerStyle,
|
||||
pub active_pane: TabStyles,
|
||||
pub inactive_pane: TabStyles,
|
||||
pub dragged_tab: Tab,
|
||||
|
@ -149,6 +152,15 @@ pub struct Toolbar {
|
|||
pub nav_button: Interactive<IconButton>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct Dock {
|
||||
pub initial_size_right: f32,
|
||||
pub initial_size_bottom: f32,
|
||||
pub wash_color: Color,
|
||||
pub panel: ContainerStyle,
|
||||
pub maximized: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct Notifications {
|
||||
#[serde(flatten)]
|
||||
|
@ -231,7 +243,9 @@ pub struct StatusBarLspStatus {
|
|||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct Sidebar {
|
||||
pub resize_handle: ContainerStyle,
|
||||
pub initial_size: f32,
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Deserialize, Default)]
|
||||
|
@ -301,6 +315,11 @@ pub struct CommandPalette {
|
|||
pub keystroke_spacing: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct ContactsPopover {
|
||||
pub background: Color,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct ContactsPanel {
|
||||
#[serde(flatten)]
|
||||
|
@ -558,6 +577,7 @@ pub struct CodeActions {
|
|||
pub struct Interactive<T> {
|
||||
pub default: T,
|
||||
pub hover: Option<T>,
|
||||
pub clicked: Option<T>,
|
||||
pub active: Option<T>,
|
||||
pub disabled: Option<T>,
|
||||
}
|
||||
|
@ -566,6 +586,8 @@ impl<T> Interactive<T> {
|
|||
pub fn style_for(&self, state: MouseState, active: bool) -> &T {
|
||||
if active {
|
||||
self.active.as_ref().unwrap_or(&self.default)
|
||||
} else if state.clicked == Some(gpui::MouseButton::Left) && self.clicked.is_some() {
|
||||
self.clicked.as_ref().unwrap()
|
||||
} else if state.hovered {
|
||||
self.hover.as_ref().unwrap_or(&self.default)
|
||||
} else {
|
||||
|
@ -588,6 +610,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
|||
#[serde(flatten)]
|
||||
default: Value,
|
||||
hover: Option<Value>,
|
||||
clicked: Option<Value>,
|
||||
active: Option<Value>,
|
||||
disabled: Option<Value>,
|
||||
}
|
||||
|
@ -614,6 +637,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
|||
};
|
||||
|
||||
let hover = deserialize_state(json.hover)?;
|
||||
let clicked = deserialize_state(json.clicked)?;
|
||||
let active = deserialize_state(json.active)?;
|
||||
let disabled = deserialize_state(json.disabled)?;
|
||||
let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
|
||||
|
@ -621,6 +645,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
|||
Ok(Interactive {
|
||||
default,
|
||||
hover,
|
||||
clicked,
|
||||
active,
|
||||
disabled,
|
||||
})
|
||||
|
|
|
@ -39,7 +39,8 @@ impl<'a> VimTestContext<'a> {
|
|||
.insert_tree("/root", json!({ "dir": { "test.txt": "" } }))
|
||||
.await;
|
||||
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
||||
|
||||
// Setup search toolbars
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
|
|
718
crates/workspace/src/dock.rs
Normal file
718
crates/workspace/src/dock.rs
Normal file
|
@ -0,0 +1,718 @@
|
|||
use collections::HashMap;
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{ChildView, Container, Empty, MouseEventHandler, Side, Svg},
|
||||
impl_internal_actions, Border, CursorStyle, Element, ElementBox, Entity, MouseButton,
|
||||
MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use settings::{DockAnchor, Settings};
|
||||
use theme::Theme;
|
||||
|
||||
use crate::{sidebar::SidebarSide, ItemHandle, Pane, StatusItemView, Workspace};
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize)]
|
||||
pub struct MoveDock(pub DockAnchor);
|
||||
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub struct AddDefaultItemToDock;
|
||||
|
||||
actions!(
|
||||
dock,
|
||||
[
|
||||
FocusDock,
|
||||
HideDock,
|
||||
AnchorDockRight,
|
||||
AnchorDockBottom,
|
||||
ExpandDock
|
||||
]
|
||||
);
|
||||
impl_internal_actions!(dock, [MoveDock, AddDefaultItemToDock]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(Dock::focus_dock);
|
||||
cx.add_action(Dock::hide_dock);
|
||||
cx.add_action(Dock::move_dock);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace, _: &AnchorDockRight, cx: &mut ViewContext<Workspace>| {
|
||||
Dock::move_dock(workspace, &MoveDock(DockAnchor::Right), cx)
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace, _: &AnchorDockBottom, cx: &mut ViewContext<Workspace>| {
|
||||
Dock::move_dock(workspace, &MoveDock(DockAnchor::Bottom), cx)
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace, _: &ExpandDock, cx: &mut ViewContext<Workspace>| {
|
||||
Dock::move_dock(workspace, &MoveDock(DockAnchor::Expanded), cx)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum DockPosition {
|
||||
Shown(DockAnchor),
|
||||
Hidden(DockAnchor),
|
||||
}
|
||||
|
||||
impl Default for DockPosition {
|
||||
fn default() -> Self {
|
||||
DockPosition::Hidden(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon_for_dock_anchor(anchor: DockAnchor) -> &'static str {
|
||||
match anchor {
|
||||
DockAnchor::Right => "icons/dock_right_12.svg",
|
||||
DockAnchor::Bottom => "icons/dock_bottom_12.svg",
|
||||
DockAnchor::Expanded => "icons/dock_modal_12.svg",
|
||||
}
|
||||
}
|
||||
|
||||
impl DockPosition {
|
||||
fn is_visible(&self) -> bool {
|
||||
match self {
|
||||
DockPosition::Shown(_) => true,
|
||||
DockPosition::Hidden(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn anchor(&self) -> DockAnchor {
|
||||
match self {
|
||||
DockPosition::Shown(anchor) | DockPosition::Hidden(anchor) => *anchor,
|
||||
}
|
||||
}
|
||||
|
||||
fn hide(self) -> Self {
|
||||
match self {
|
||||
DockPosition::Shown(anchor) => DockPosition::Hidden(anchor),
|
||||
DockPosition::Hidden(_) => self,
|
||||
}
|
||||
}
|
||||
|
||||
fn show(self) -> Self {
|
||||
match self {
|
||||
DockPosition::Hidden(anchor) => DockPosition::Shown(anchor),
|
||||
DockPosition::Shown(_) => self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type DefaultItemFactory =
|
||||
fn(&mut Workspace, &mut ViewContext<Workspace>) -> Box<dyn ItemHandle>;
|
||||
|
||||
pub struct Dock {
|
||||
position: DockPosition,
|
||||
panel_sizes: HashMap<DockAnchor, f32>,
|
||||
pane: ViewHandle<Pane>,
|
||||
default_item_factory: DefaultItemFactory,
|
||||
}
|
||||
|
||||
impl Dock {
|
||||
pub fn new(cx: &mut ViewContext<Workspace>, default_item_factory: DefaultItemFactory) -> Self {
|
||||
let anchor = cx.global::<Settings>().default_dock_anchor;
|
||||
let pane = cx.add_view(|cx| Pane::new(Some(anchor), cx));
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.set_active(false, cx);
|
||||
});
|
||||
let pane_id = pane.id();
|
||||
cx.subscribe(&pane, move |workspace, _, event, cx| {
|
||||
workspace.handle_pane_event(pane_id, event, cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
pane,
|
||||
panel_sizes: Default::default(),
|
||||
position: DockPosition::Hidden(anchor),
|
||||
default_item_factory,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pane(&self) -> &ViewHandle<Pane> {
|
||||
&self.pane
|
||||
}
|
||||
|
||||
pub fn visible_pane(&self) -> Option<&ViewHandle<Pane>> {
|
||||
self.position.is_visible().then(|| self.pane())
|
||||
}
|
||||
|
||||
pub fn is_anchored_at(&self, anchor: DockAnchor) -> bool {
|
||||
self.position.is_visible() && self.position.anchor() == anchor
|
||||
}
|
||||
|
||||
fn set_dock_position(
|
||||
workspace: &mut Workspace,
|
||||
new_position: DockPosition,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
workspace.dock.position = new_position;
|
||||
// Tell the pane about the new anchor position
|
||||
workspace.dock.pane.update(cx, |pane, cx| {
|
||||
pane.set_docked(Some(new_position.anchor()), cx)
|
||||
});
|
||||
|
||||
if workspace.dock.position.is_visible() {
|
||||
// Close the right sidebar if the dock is on the right side and the right sidebar is open
|
||||
if workspace.dock.position.anchor() == DockAnchor::Right {
|
||||
if workspace.right_sidebar().read(cx).is_open() {
|
||||
workspace.toggle_sidebar(SidebarSide::Right, cx);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the pane has at least one item or construct a default item to put in it
|
||||
let pane = workspace.dock.pane.clone();
|
||||
if pane.read(cx).items().next().is_none() {
|
||||
let item_to_add = (workspace.dock.default_item_factory)(workspace, cx);
|
||||
// Adding the item focuses the pane by default
|
||||
Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
|
||||
} else {
|
||||
cx.focus(pane);
|
||||
}
|
||||
} else if let Some(last_active_center_pane) = workspace.last_active_center_pane.clone() {
|
||||
cx.focus(last_active_center_pane);
|
||||
}
|
||||
cx.emit(crate::Event::DockAnchorChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn hide(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
Self::set_dock_position(workspace, workspace.dock.position.hide(), cx);
|
||||
}
|
||||
|
||||
pub fn show(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
Self::set_dock_position(workspace, workspace.dock.position.show(), cx);
|
||||
}
|
||||
|
||||
pub fn hide_on_sidebar_shown(
|
||||
workspace: &mut Workspace,
|
||||
sidebar_side: SidebarSide,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
if (sidebar_side == SidebarSide::Right && workspace.dock.is_anchored_at(DockAnchor::Right))
|
||||
|| workspace.dock.is_anchored_at(DockAnchor::Expanded)
|
||||
{
|
||||
Self::hide(workspace, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext<Workspace>) {
|
||||
Self::set_dock_position(workspace, workspace.dock.position.show(), cx);
|
||||
}
|
||||
|
||||
fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext<Workspace>) {
|
||||
Self::set_dock_position(workspace, workspace.dock.position.hide(), cx);
|
||||
}
|
||||
|
||||
fn move_dock(
|
||||
workspace: &mut Workspace,
|
||||
&MoveDock(new_anchor): &MoveDock,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), cx);
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
anchor: DockAnchor,
|
||||
cx: &mut RenderContext<Workspace>,
|
||||
) -> Option<ElementBox> {
|
||||
let style = &theme.workspace.dock;
|
||||
|
||||
self.position
|
||||
.is_visible()
|
||||
.then(|| self.position.anchor())
|
||||
.filter(|current_anchor| *current_anchor == anchor)
|
||||
.map(|anchor| match anchor {
|
||||
DockAnchor::Bottom | DockAnchor::Right => {
|
||||
let mut panel_style = style.panel.clone();
|
||||
let (resize_side, initial_size) = if anchor == DockAnchor::Bottom {
|
||||
panel_style.border = Border {
|
||||
top: true,
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
..panel_style.border
|
||||
};
|
||||
|
||||
(Side::Top, style.initial_size_bottom)
|
||||
} else {
|
||||
panel_style.border = Border {
|
||||
top: false,
|
||||
bottom: false,
|
||||
left: true,
|
||||
right: false,
|
||||
..panel_style.border
|
||||
};
|
||||
(Side::Left, style.initial_size_right)
|
||||
};
|
||||
|
||||
enum DockResizeHandle {}
|
||||
|
||||
let resizable = Container::new(ChildView::new(self.pane.clone()).boxed())
|
||||
.with_style(panel_style)
|
||||
.with_resize_handle::<DockResizeHandle, _>(
|
||||
resize_side as usize,
|
||||
resize_side,
|
||||
4.,
|
||||
self.panel_sizes
|
||||
.get(&anchor)
|
||||
.copied()
|
||||
.unwrap_or(initial_size),
|
||||
cx,
|
||||
);
|
||||
|
||||
let size = resizable.current_size();
|
||||
let workspace = cx.handle();
|
||||
cx.defer(move |cx| {
|
||||
if let Some(workspace) = workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, _| {
|
||||
workspace.dock.panel_sizes.insert(anchor, size);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
resizable.flex(5., false).boxed()
|
||||
}
|
||||
DockAnchor::Expanded => {
|
||||
enum ExpandedDockWash {}
|
||||
enum ExpandedDockPane {}
|
||||
Container::new(
|
||||
MouseEventHandler::<ExpandedDockWash>::new(0, cx, |_state, cx| {
|
||||
MouseEventHandler::<ExpandedDockPane>::new(0, cx, |_state, _cx| {
|
||||
ChildView::new(self.pane.clone()).boxed()
|
||||
})
|
||||
.capture_all()
|
||||
.contained()
|
||||
.with_style(style.maximized)
|
||||
.boxed()
|
||||
})
|
||||
.capture_all()
|
||||
.on_down(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(HideDock);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::Arrow)
|
||||
.boxed(),
|
||||
)
|
||||
.with_background_color(style.wash_color)
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ToggleDockButton {
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
}
|
||||
|
||||
impl ToggleDockButton {
|
||||
pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
|
||||
// When dock moves, redraw so that the icon and toggle status matches.
|
||||
cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach();
|
||||
|
||||
Self {
|
||||
workspace: workspace.downgrade(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ToggleDockButton {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for ToggleDockButton {
|
||||
fn ui_name() -> &'static str {
|
||||
"Dock Toggle"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
|
||||
let workspace = self.workspace.upgrade(cx);
|
||||
|
||||
if workspace.is_none() {
|
||||
return Empty::new().boxed();
|
||||
}
|
||||
|
||||
let dock_position = workspace.unwrap().read(cx).dock.position;
|
||||
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let button = MouseEventHandler::<Self>::new(0, cx, {
|
||||
let theme = theme.clone();
|
||||
move |state, _| {
|
||||
let style = theme
|
||||
.workspace
|
||||
.status_bar
|
||||
.sidebar_buttons
|
||||
.item
|
||||
.style_for(state, dock_position.is_visible());
|
||||
|
||||
Svg::new(icon_for_dock_anchor(dock_position.anchor()))
|
||||
.with_color(style.icon_color)
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.with_height(style.icon_size)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand);
|
||||
|
||||
if dock_position.is_visible() {
|
||||
button
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(HideDock);
|
||||
})
|
||||
.with_tooltip::<Self, _>(
|
||||
0,
|
||||
"Hide Dock".into(),
|
||||
Some(Box::new(HideDock)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
button
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(FocusDock);
|
||||
})
|
||||
.with_tooltip::<Self, _>(
|
||||
0,
|
||||
"Focus Dock".into(),
|
||||
Some(Box::new(FocusDock)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for ToggleDockButton {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
_active_pane_item: Option<&dyn crate::ItemHandle>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
//Not applicable
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use gpui::{AppContext, TestAppContext, UpdateView, ViewContext};
|
||||
use project::{FakeFs, Project};
|
||||
use settings::Settings;
|
||||
|
||||
use super::*;
|
||||
use crate::{sidebar::Sidebar, tests::TestItem, ItemHandle, Workspace};
|
||||
|
||||
pub fn default_item_factory(
|
||||
_workspace: &mut Workspace,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Box<dyn ItemHandle> {
|
||||
Box::new(cx.add_view(|_| TestItem::new()))
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dock_hides_when_pane_empty(cx: &mut TestAppContext) {
|
||||
let mut cx = DockTestContext::new(cx).await;
|
||||
|
||||
// Closing the last item in the dock hides the dock
|
||||
cx.move_dock(DockAnchor::Right);
|
||||
let old_items = cx.dock_items();
|
||||
assert!(!old_items.is_empty());
|
||||
cx.close_dock_items().await;
|
||||
cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right));
|
||||
|
||||
// Reopening the dock adds a new item
|
||||
cx.move_dock(DockAnchor::Right);
|
||||
let new_items = cx.dock_items();
|
||||
assert!(!new_items.is_empty());
|
||||
assert!(new_items
|
||||
.into_iter()
|
||||
.all(|new_item| !old_items.contains(&new_item)));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dock_panel_collisions(cx: &mut TestAppContext) {
|
||||
let mut cx = DockTestContext::new(cx).await;
|
||||
|
||||
// Dock closes when expanded for either panel
|
||||
cx.move_dock(DockAnchor::Expanded);
|
||||
cx.open_sidebar(SidebarSide::Left);
|
||||
cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
|
||||
cx.close_sidebar(SidebarSide::Left);
|
||||
cx.move_dock(DockAnchor::Expanded);
|
||||
cx.open_sidebar(SidebarSide::Right);
|
||||
cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
|
||||
|
||||
// Dock closes in the right position if the right sidebar is opened
|
||||
cx.move_dock(DockAnchor::Right);
|
||||
cx.open_sidebar(SidebarSide::Left);
|
||||
cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
|
||||
cx.open_sidebar(SidebarSide::Right);
|
||||
cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right));
|
||||
cx.close_sidebar(SidebarSide::Right);
|
||||
|
||||
// Dock in bottom position ignores sidebars
|
||||
cx.move_dock(DockAnchor::Bottom);
|
||||
cx.open_sidebar(SidebarSide::Left);
|
||||
cx.open_sidebar(SidebarSide::Right);
|
||||
cx.assert_dock_position(DockPosition::Shown(DockAnchor::Bottom));
|
||||
|
||||
// Opening the dock in the right position closes the right sidebar
|
||||
cx.move_dock(DockAnchor::Right);
|
||||
cx.assert_sidebar_closed(SidebarSide::Right);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_focusing_panes_shows_and_hides_dock(cx: &mut TestAppContext) {
|
||||
let mut cx = DockTestContext::new(cx).await;
|
||||
|
||||
// Focusing an item not in the dock when expanded hides the dock
|
||||
let center_item = cx.add_item_to_center_pane();
|
||||
cx.move_dock(DockAnchor::Expanded);
|
||||
let dock_item = cx
|
||||
.dock_items()
|
||||
.get(0)
|
||||
.cloned()
|
||||
.expect("Dock should have an item at this point");
|
||||
center_item.update(&mut cx, |_, cx| cx.focus_self());
|
||||
cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
|
||||
|
||||
// Focusing an item not in the dock when not expanded, leaves the dock open but inactive
|
||||
cx.move_dock(DockAnchor::Right);
|
||||
center_item.update(&mut cx, |_, cx| cx.focus_self());
|
||||
cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
|
||||
cx.assert_dock_pane_inactive();
|
||||
cx.assert_workspace_pane_active();
|
||||
|
||||
// Focusing an item in the dock activates it's pane
|
||||
dock_item.update(&mut cx, |_, cx| cx.focus_self());
|
||||
cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
|
||||
cx.assert_dock_pane_active();
|
||||
cx.assert_workspace_pane_inactive();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_dock_focus(cx: &mut TestAppContext) {
|
||||
let cx = DockTestContext::new(cx).await;
|
||||
|
||||
cx.move_dock(DockAnchor::Right);
|
||||
cx.assert_dock_pane_active();
|
||||
cx.hide_dock();
|
||||
cx.move_dock(DockAnchor::Right);
|
||||
cx.assert_dock_pane_active();
|
||||
}
|
||||
|
||||
struct DockTestContext<'a> {
|
||||
pub cx: &'a mut TestAppContext,
|
||||
pub window_id: usize,
|
||||
pub workspace: ViewHandle<Workspace>,
|
||||
}
|
||||
|
||||
impl<'a> DockTestContext<'a> {
|
||||
pub async fn new(cx: &'a mut TestAppContext) -> DockTestContext<'a> {
|
||||
Settings::test_async(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
cx.update(|cx| init(cx));
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let left_panel = cx.add_view(|_| TestItem::new());
|
||||
workspace.left_sidebar().update(cx, |sidebar, cx| {
|
||||
sidebar.add_item(
|
||||
"icons/folder_tree_16.svg",
|
||||
"Left Test Panel".to_string(),
|
||||
left_panel.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let right_panel = cx.add_view(|_| TestItem::new());
|
||||
workspace.right_sidebar().update(cx, |sidebar, cx| {
|
||||
sidebar.add_item(
|
||||
"icons/folder_tree_16.svg",
|
||||
"Right Test Panel".to_string(),
|
||||
right_panel.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Self {
|
||||
cx,
|
||||
window_id,
|
||||
workspace,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn workspace<F, T>(&self, read: F) -> T
|
||||
where
|
||||
F: FnOnce(&Workspace, &AppContext) -> T,
|
||||
{
|
||||
self.workspace.read_with(self.cx, read)
|
||||
}
|
||||
|
||||
pub fn update_workspace<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
|
||||
{
|
||||
self.workspace.update(self.cx, update)
|
||||
}
|
||||
|
||||
pub fn sidebar<F, T>(&self, sidebar_side: SidebarSide, read: F) -> T
|
||||
where
|
||||
F: FnOnce(&Sidebar, &AppContext) -> T,
|
||||
{
|
||||
self.workspace(|workspace, cx| {
|
||||
let sidebar = match sidebar_side {
|
||||
SidebarSide::Left => workspace.left_sidebar(),
|
||||
SidebarSide::Right => workspace.right_sidebar(),
|
||||
}
|
||||
.read(cx);
|
||||
|
||||
read(sidebar, cx)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn center_pane_handle(&self) -> ViewHandle<Pane> {
|
||||
self.workspace(|workspace, _| {
|
||||
workspace
|
||||
.last_active_center_pane
|
||||
.clone()
|
||||
.unwrap_or_else(|| workspace.center.panes()[0].clone())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_item_to_center_pane(&mut self) -> ViewHandle<TestItem> {
|
||||
self.update_workspace(|workspace, cx| {
|
||||
let item = cx.add_view(|_| TestItem::new());
|
||||
let pane = workspace
|
||||
.last_active_center_pane
|
||||
.clone()
|
||||
.unwrap_or_else(|| workspace.center.panes()[0].clone());
|
||||
Pane::add_item(
|
||||
workspace,
|
||||
&pane,
|
||||
Box::new(item.clone()),
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
item
|
||||
})
|
||||
}
|
||||
|
||||
pub fn dock_pane<F, T>(&self, read: F) -> T
|
||||
where
|
||||
F: FnOnce(&Pane, &AppContext) -> T,
|
||||
{
|
||||
self.workspace(|workspace, cx| {
|
||||
let dock_pane = workspace.dock_pane().read(cx);
|
||||
read(dock_pane, cx)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_dock(&self, anchor: DockAnchor) {
|
||||
self.cx.dispatch_action(self.window_id, MoveDock(anchor));
|
||||
}
|
||||
|
||||
pub fn hide_dock(&self) {
|
||||
self.cx.dispatch_action(self.window_id, HideDock);
|
||||
}
|
||||
|
||||
pub fn open_sidebar(&mut self, sidebar_side: SidebarSide) {
|
||||
if !self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) {
|
||||
self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_sidebar(&mut self, sidebar_side: SidebarSide) {
|
||||
if self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) {
|
||||
self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dock_items(&self) -> Vec<ViewHandle<TestItem>> {
|
||||
self.dock_pane(|pane, cx| {
|
||||
pane.items()
|
||||
.map(|item| {
|
||||
item.act_as::<TestItem>(cx)
|
||||
.expect("Dock Test Context uses TestItems in the dock")
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn close_dock_items(&mut self) {
|
||||
self.update_workspace(|workspace, cx| {
|
||||
Pane::close_items(workspace, workspace.dock_pane().clone(), cx, |_| true)
|
||||
})
|
||||
.await
|
||||
.expect("Could not close dock items")
|
||||
}
|
||||
|
||||
pub fn assert_dock_position(&self, expected_position: DockPosition) {
|
||||
self.workspace(|workspace, _| assert_eq!(workspace.dock.position, expected_position));
|
||||
}
|
||||
|
||||
pub fn assert_sidebar_closed(&self, sidebar_side: SidebarSide) {
|
||||
assert!(!self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()));
|
||||
}
|
||||
|
||||
pub fn assert_workspace_pane_active(&self) {
|
||||
assert!(self
|
||||
.center_pane_handle()
|
||||
.read_with(self.cx, |pane, _| pane.is_active()));
|
||||
}
|
||||
|
||||
pub fn assert_workspace_pane_inactive(&self) {
|
||||
assert!(!self
|
||||
.center_pane_handle()
|
||||
.read_with(self.cx, |pane, _| pane.is_active()));
|
||||
}
|
||||
|
||||
pub fn assert_dock_pane_active(&self) {
|
||||
assert!(self.dock_pane(|pane, _| pane.is_active()))
|
||||
}
|
||||
|
||||
pub fn assert_dock_pane_inactive(&self) {
|
||||
assert!(!self.dock_pane(|pane, _| pane.is_active()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for DockTestContext<'a> {
|
||||
type Target = gpui::TestAppContext;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DerefMut for DockTestContext<'a> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> UpdateView for DockTestContext<'a> {
|
||||
fn update_view<T, S>(
|
||||
&mut self,
|
||||
handle: &ViewHandle<T>,
|
||||
update: &mut dyn FnMut(&mut T, &mut ViewContext<T>) -> S,
|
||||
) -> S
|
||||
where
|
||||
T: View,
|
||||
{
|
||||
handle.update(self.cx, update)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,9 @@
|
|||
use super::{ItemHandle, SplitDirection};
|
||||
use crate::{toolbar::Toolbar, Item, NewFile, NewSearch, NewTerminal, WeakItemHandle, Workspace};
|
||||
use crate::{
|
||||
dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, ExpandDock, HideDock},
|
||||
toolbar::Toolbar,
|
||||
Item, NewFile, NewSearch, NewTerminal, WeakItemHandle, Workspace,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
|
@ -15,13 +19,13 @@ use gpui::{
|
|||
},
|
||||
impl_actions, impl_internal_actions,
|
||||
platform::{CursorStyle, NavigationDirection},
|
||||
AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
|
||||
Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
|
||||
ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use project::{Project, ProjectEntryId, ProjectPath};
|
||||
use serde::Deserialize;
|
||||
use settings::{Autosave, Settings};
|
||||
use settings::{Autosave, DockAnchor, Settings};
|
||||
use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
|
||||
use theme::Theme;
|
||||
use util::ResultExt;
|
||||
|
@ -76,13 +80,27 @@ pub struct DeploySplitMenu {
|
|||
position: Vector2F,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct DeployDockMenu {
|
||||
position: Vector2F,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct DeployNewMenu {
|
||||
position: Vector2F,
|
||||
}
|
||||
|
||||
impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
|
||||
impl_internal_actions!(pane, [CloseItem, DeploySplitMenu, DeployNewMenu, MoveItem]);
|
||||
impl_internal_actions!(
|
||||
pane,
|
||||
[
|
||||
CloseItem,
|
||||
DeploySplitMenu,
|
||||
DeployNewMenu,
|
||||
DeployDockMenu,
|
||||
MoveItem
|
||||
]
|
||||
);
|
||||
|
||||
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
|
||||
|
||||
|
@ -141,6 +159,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
|
||||
cx.add_action(Pane::deploy_split_menu);
|
||||
cx.add_action(Pane::deploy_new_menu);
|
||||
cx.add_action(Pane::deploy_dock_menu);
|
||||
cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
|
||||
Pane::reopen_closed_item(workspace, cx).detach();
|
||||
});
|
||||
|
@ -168,6 +187,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Event {
|
||||
Focused,
|
||||
ActivateItem { local: bool },
|
||||
|
@ -185,7 +205,8 @@ pub struct Pane {
|
|||
autoscroll: bool,
|
||||
nav_history: Rc<RefCell<NavHistory>>,
|
||||
toolbar: ViewHandle<Toolbar>,
|
||||
context_menu: ViewHandle<ContextMenu>,
|
||||
tab_bar_context_menu: ViewHandle<ContextMenu>,
|
||||
docked: Option<DockAnchor>,
|
||||
}
|
||||
|
||||
pub struct ItemNavHistory {
|
||||
|
@ -235,7 +256,7 @@ pub enum ReorderBehavior {
|
|||
}
|
||||
|
||||
impl Pane {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let handle = cx.weak_handle();
|
||||
let context_menu = cx.add_view(ContextMenu::new);
|
||||
Self {
|
||||
|
@ -253,15 +274,25 @@ impl Pane {
|
|||
pane: handle.clone(),
|
||||
})),
|
||||
toolbar: cx.add_view(|_| Toolbar::new(handle)),
|
||||
context_menu,
|
||||
tab_bar_context_menu: context_menu,
|
||||
docked,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.is_active
|
||||
}
|
||||
|
||||
pub fn set_active(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
|
||||
self.is_active = is_active;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_docked(&mut self, docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) {
|
||||
self.docked = docked;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
|
||||
ItemNavHistory {
|
||||
history: self.nav_history.clone(),
|
||||
|
@ -675,7 +706,7 @@ impl Pane {
|
|||
pane: ViewHandle<Pane>,
|
||||
item_id_to_close: usize,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Task<Result<bool>> {
|
||||
) -> Task<Result<()>> {
|
||||
Self::close_items(workspace, pane, cx, move |view_id| {
|
||||
view_id == item_id_to_close
|
||||
})
|
||||
|
@ -686,7 +717,7 @@ impl Pane {
|
|||
pane: ViewHandle<Pane>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
should_close: impl 'static + Fn(usize) -> bool,
|
||||
) -> Task<Result<bool>> {
|
||||
) -> Task<Result<()>> {
|
||||
let project = workspace.project().clone();
|
||||
|
||||
// Find the items to close.
|
||||
|
@ -759,7 +790,7 @@ impl Pane {
|
|||
}
|
||||
|
||||
pane.update(&mut cx, |_, cx| cx.notify());
|
||||
Ok(true)
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -962,9 +993,10 @@ impl Pane {
|
|||
}
|
||||
|
||||
fn deploy_split_menu(&mut self, action: &DeploySplitMenu, cx: &mut ViewContext<Self>) {
|
||||
self.context_menu.update(cx, |menu, cx| {
|
||||
self.tab_bar_context_menu.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
action.position,
|
||||
AnchorCorner::TopRight,
|
||||
vec![
|
||||
ContextMenuItem::item("Split Right", SplitRight),
|
||||
ContextMenuItem::item("Split Left", SplitLeft),
|
||||
|
@ -976,10 +1008,26 @@ impl Pane {
|
|||
});
|
||||
}
|
||||
|
||||
fn deploy_new_menu(&mut self, action: &DeployNewMenu, cx: &mut ViewContext<Self>) {
|
||||
self.context_menu.update(cx, |menu, cx| {
|
||||
fn deploy_dock_menu(&mut self, action: &DeployDockMenu, cx: &mut ViewContext<Self>) {
|
||||
self.tab_bar_context_menu.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
action.position,
|
||||
AnchorCorner::TopRight,
|
||||
vec![
|
||||
ContextMenuItem::item("Anchor Dock Right", AnchorDockRight),
|
||||
ContextMenuItem::item("Anchor Dock Bottom", AnchorDockBottom),
|
||||
ContextMenuItem::item("Expand Dock", ExpandDock),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn deploy_new_menu(&mut self, action: &DeployNewMenu, cx: &mut ViewContext<Self>) {
|
||||
self.tab_bar_context_menu.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
action.position,
|
||||
AnchorCorner::TopRight,
|
||||
vec![
|
||||
ContextMenuItem::item("New File", NewFile),
|
||||
ContextMenuItem::item("New Terminal", NewTerminal),
|
||||
|
@ -1004,7 +1052,7 @@ impl Pane {
|
|||
});
|
||||
}
|
||||
|
||||
fn render_tab_bar(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
|
||||
fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let filler_index = self.items.len();
|
||||
|
||||
|
@ -1012,7 +1060,7 @@ impl Pane {
|
|||
enum Tab {}
|
||||
enum Filler {}
|
||||
let pane = cx.handle();
|
||||
MouseEventHandler::new::<Tabs, _, _>(0, cx, |_, cx| {
|
||||
MouseEventHandler::<Tabs>::new(0, cx, |_, cx| {
|
||||
let autoscroll = if mem::take(&mut self.autoscroll) {
|
||||
Some(self.active_item_index)
|
||||
} else {
|
||||
|
@ -1033,7 +1081,7 @@ impl Pane {
|
|||
let tab_active = ix == self.active_item_index;
|
||||
|
||||
row.add_child({
|
||||
MouseEventHandler::new::<Tab, _, _>(ix, cx, {
|
||||
MouseEventHandler::<Tab>::new(ix, cx, {
|
||||
let item = item.clone();
|
||||
let pane = pane.clone();
|
||||
let detail = detail.clone();
|
||||
|
@ -1047,6 +1095,7 @@ impl Pane {
|
|||
Self::render_tab(
|
||||
&item,
|
||||
pane,
|
||||
ix == 0,
|
||||
detail,
|
||||
hovered,
|
||||
Self::tab_overlay_color(hovered, theme.as_ref(), cx),
|
||||
|
@ -1091,6 +1140,7 @@ impl Pane {
|
|||
Self::render_tab(
|
||||
&dragged_item.item,
|
||||
dragged_item.pane.clone(),
|
||||
false,
|
||||
detail,
|
||||
false,
|
||||
None,
|
||||
|
@ -1108,7 +1158,7 @@ impl Pane {
|
|||
// the filler
|
||||
let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
|
||||
row.add_child(
|
||||
MouseEventHandler::new::<Filler, _, _>(0, cx, |mouse_state, cx| {
|
||||
MouseEventHandler::<Filler>::new(0, cx, |mouse_state, cx| {
|
||||
let mut filler = Empty::new()
|
||||
.contained()
|
||||
.with_style(filler_style.container)
|
||||
|
@ -1172,6 +1222,7 @@ impl Pane {
|
|||
fn render_tab<V: View>(
|
||||
item: &Box<dyn ItemHandle>,
|
||||
pane: WeakViewHandle<Pane>,
|
||||
first: bool,
|
||||
detail: Option<usize>,
|
||||
hovered: bool,
|
||||
overlay: Option<Color>,
|
||||
|
@ -1179,6 +1230,10 @@ impl Pane {
|
|||
cx: &mut RenderContext<V>,
|
||||
) -> ElementBox {
|
||||
let title = item.tab_content(detail, &tab_style, cx);
|
||||
let mut container = tab_style.container.clone();
|
||||
if first {
|
||||
container.border.left = false;
|
||||
}
|
||||
|
||||
let mut tab = Flex::row()
|
||||
.with_child(
|
||||
|
@ -1230,17 +1285,13 @@ impl Pane {
|
|||
let item_id = item.id();
|
||||
enum TabCloseButton {}
|
||||
let icon = Svg::new("icons/x_mark_thin_8.svg");
|
||||
MouseEventHandler::new::<TabCloseButton, _, _>(
|
||||
item_id,
|
||||
cx,
|
||||
|mouse_state, _| {
|
||||
MouseEventHandler::<TabCloseButton>::new(item_id, cx, |mouse_state, _| {
|
||||
if mouse_state.hovered {
|
||||
icon.with_color(tab_style.icon_close_active).boxed()
|
||||
} else {
|
||||
icon.with_color(tab_style.icon_close).boxed()
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
.with_padding(Padding::uniform(4.))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, {
|
||||
|
@ -1263,7 +1314,7 @@ impl Pane {
|
|||
.boxed(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(tab_style.container);
|
||||
.with_style(container);
|
||||
|
||||
if let Some(overlay) = overlay {
|
||||
tab = tab.with_overlay_color(overlay);
|
||||
|
@ -1316,94 +1367,89 @@ impl View for Pane {
|
|||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
enum SplitIcon {}
|
||||
|
||||
let this = cx.handle();
|
||||
|
||||
enum MouseNavigationHandler {}
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
EventHandler::new(if let Some(active_item) = self.active_item() {
|
||||
MouseEventHandler::<MouseNavigationHandler>::new(0, cx, |_, cx| {
|
||||
if let Some(active_item) = self.active_item() {
|
||||
Flex::column()
|
||||
.with_child({
|
||||
let mut tab_row = Flex::row()
|
||||
.with_child(self.render_tab_bar(cx).flex(1., true).named("tabs"));
|
||||
.with_child(self.render_tabs(cx).flex(1.0, true).named("tabs"));
|
||||
|
||||
// Render pane buttons
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
if self.is_active {
|
||||
tab_row.add_children([
|
||||
MouseEventHandler::new::<SplitIcon, _, _>(
|
||||
tab_row.add_child(
|
||||
Flex::row()
|
||||
// New menu
|
||||
.with_child(tab_bar_button(
|
||||
0,
|
||||
"icons/plus_12.svg",
|
||||
cx,
|
||||
|mouse_state, cx| {
|
||||
let theme =
|
||||
&cx.global::<Settings>().theme.workspace.tab_bar;
|
||||
let style =
|
||||
theme.pane_button.style_for(mouse_state, false);
|
||||
Svg::new("icons/plus_12.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.aligned()
|
||||
.boxed()
|
||||
},
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_down(MouseButton::Left, |e, cx| {
|
||||
cx.dispatch_action(DeployNewMenu {
|
||||
position: e.position,
|
||||
});
|
||||
})
|
||||
.boxed(),
|
||||
MouseEventHandler::new::<SplitIcon, _, _>(
|
||||
|position| DeployNewMenu { position },
|
||||
))
|
||||
.with_child(
|
||||
self.docked
|
||||
.map(|anchor| {
|
||||
// Add the dock menu button if this pane is a dock
|
||||
let dock_icon =
|
||||
icon_for_dock_anchor(anchor);
|
||||
|
||||
tab_bar_button(
|
||||
1,
|
||||
dock_icon,
|
||||
cx,
|
||||
|mouse_state, cx| {
|
||||
let theme =
|
||||
&cx.global::<Settings>().theme.workspace.tab_bar;
|
||||
let style =
|
||||
theme.pane_button.style_for(mouse_state, false);
|
||||
Svg::new("icons/split_12.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.aligned()
|
||||
.boxed()
|
||||
},
|
||||
|position| DeployDockMenu { position },
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_down(MouseButton::Left, |e, cx| {
|
||||
cx.dispatch_action(DeploySplitMenu {
|
||||
position: e.position,
|
||||
});
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
// Add the split menu if this pane is not a dock
|
||||
tab_bar_button(
|
||||
2,
|
||||
"icons/split_12.svg",
|
||||
cx,
|
||||
|position| DeploySplitMenu { position },
|
||||
)
|
||||
}),
|
||||
)
|
||||
// Add the close dock button if this pane is a dock
|
||||
.with_children(self.docked.map(|_| {
|
||||
tab_bar_button(
|
||||
3,
|
||||
"icons/x_mark_thin_8.svg",
|
||||
cx,
|
||||
|_| HideDock,
|
||||
)
|
||||
}))
|
||||
.contained()
|
||||
.with_style(
|
||||
theme.workspace.tab_bar.pane_button_container,
|
||||
)
|
||||
.flex(1., false)
|
||||
.boxed(),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
tab_row
|
||||
.constrained()
|
||||
.with_height(cx.global::<Settings>().theme.workspace.tab_bar.height)
|
||||
.with_height(theme.workspace.tab_bar.height)
|
||||
.contained()
|
||||
.with_style(theme.workspace.tab_bar.container)
|
||||
.flex(1., false)
|
||||
.named("tab bar")
|
||||
})
|
||||
.with_child(ChildView::new(&self.toolbar).boxed())
|
||||
.with_child(ChildView::new(&self.toolbar).expanded().boxed())
|
||||
.with_child(ChildView::new(active_item).flex(1., true).boxed())
|
||||
.boxed()
|
||||
} else {
|
||||
enum EmptyPane {}
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
|
||||
MouseEventHandler::new::<EmptyPane, _, _>(0, cx, |_, _| {
|
||||
MouseEventHandler::<EmptyPane>::new(0, cx, |_, _| {
|
||||
Empty::new()
|
||||
.contained()
|
||||
.with_background_color(theme.workspace.background)
|
||||
|
@ -1412,24 +1458,32 @@ impl View for Pane {
|
|||
.on_down(MouseButton::Left, |_, cx| {
|
||||
cx.focus_parent_view();
|
||||
})
|
||||
.boxed()
|
||||
.on_up(MouseButton::Left, {
|
||||
let pane = this.clone();
|
||||
move |_, cx: &mut EventContext| Pane::handle_dropped_item(&pane, 0, cx)
|
||||
})
|
||||
.on_navigate_mouse_down(move |direction, cx| {
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.on_down(MouseButton::Navigate(NavigationDirection::Back), {
|
||||
let this = this.clone();
|
||||
match direction {
|
||||
NavigationDirection::Back => {
|
||||
cx.dispatch_action(GoBack { pane: Some(this) })
|
||||
move |_, cx| {
|
||||
cx.dispatch_action(GoBack {
|
||||
pane: Some(this.clone()),
|
||||
});
|
||||
}
|
||||
NavigationDirection::Forward => {
|
||||
cx.dispatch_action(GoForward { pane: Some(this) })
|
||||
})
|
||||
.on_down(MouseButton::Navigate(NavigationDirection::Forward), {
|
||||
let this = this.clone();
|
||||
move |_, cx| {
|
||||
cx.dispatch_action(GoForward {
|
||||
pane: Some(this.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(ChildView::new(&self.context_menu).boxed())
|
||||
.with_child(ChildView::new(&self.tab_bar_context_menu).boxed())
|
||||
.named("pane")
|
||||
}
|
||||
|
||||
|
@ -1451,6 +1505,36 @@ impl View for Pane {
|
|||
}
|
||||
}
|
||||
|
||||
fn tab_bar_button<A: Action>(
|
||||
index: usize,
|
||||
icon: &'static str,
|
||||
cx: &mut RenderContext<Pane>,
|
||||
action_builder: impl 'static + Fn(Vector2F) -> A,
|
||||
) -> ElementBox {
|
||||
enum TabBarButton {}
|
||||
|
||||
MouseEventHandler::<TabBarButton>::new(index, cx, |mouse_state, cx| {
|
||||
let theme = &cx.global::<Settings>().theme.workspace.tab_bar;
|
||||
let style = theme.pane_button.style_for(mouse_state, false);
|
||||
Svg::new(icon)
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
// .aligned()
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |e, cx| {
|
||||
cx.dispatch_action(action_builder(e.region.lower_right()));
|
||||
})
|
||||
.flex(1., false)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
impl ItemNavHistory {
|
||||
pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut MutableAppContext) {
|
||||
self.history.borrow_mut().push(data, self.item.clone(), cx);
|
||||
|
@ -1566,7 +1650,8 @@ mod tests {
|
|||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// 1. Add with a destination index
|
||||
|
@ -1654,7 +1739,8 @@ mod tests {
|
|||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// 1. Add with a destination index
|
||||
|
@ -1730,7 +1816,8 @@ mod tests {
|
|||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// singleton view
|
||||
|
|
|
@ -38,6 +38,10 @@ impl PaneGroup {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns:
|
||||
/// - Ok(true) if it found and removed a pane
|
||||
/// - Ok(false) if it found but did not remove the pane
|
||||
/// - Err(_) if it did not find the pane
|
||||
pub fn remove(&mut self, pane: &ViewHandle<Pane>) -> Result<bool> {
|
||||
match &mut self.root {
|
||||
Member::Pane(_) => Ok(false),
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
// TODO: Need to put this basic structure in workspace, and make 'program handles'
|
||||
// based off of the 'searchable item' pattern except with models. This way, the workspace's clients
|
||||
// can register their models as programs with a specific identity and capable of notifying the workspace
|
||||
// Programs are:
|
||||
// - Kept alive by the program manager, they need to emit an event to get dropped from it
|
||||
// - Can be interacted with directly, (closed, activated, etc.) by the program manager, bypassing
|
||||
// associated view(s)
|
||||
// - Have special rendering methods that the program manager requires them to implement to fill out
|
||||
// the status bar
|
||||
// - Can emit events for the program manager which:
|
||||
// - Add a jewel (notification, change, etc.)
|
||||
// - Drop the program
|
||||
// - ???
|
||||
// - Program Manager is kept in a global, listens for window drop so it can drop all it's program handles
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{AnyModelHandle, Entity, ModelHandle, View, ViewContext};
|
||||
|
||||
/// This struct is going to be the starting point for the 'program manager' feature that will
|
||||
/// eventually be implemented to provide a collaborative way of engaging with identity-having
|
||||
/// features like the terminal.
|
||||
pub struct ProgramManager {
|
||||
// TODO: Make this a hashset or something
|
||||
modals: HashMap<usize, AnyModelHandle>,
|
||||
}
|
||||
|
||||
impl ProgramManager {
|
||||
pub fn insert_or_replace<T: Entity, V: View>(
|
||||
window: usize,
|
||||
program: ModelHandle<T>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Option<AnyModelHandle> {
|
||||
cx.update_global::<ProgramManager, _, _>(|pm, _| {
|
||||
pm.insert_or_replace_internal::<T>(window, program)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove<T: Entity, V: View>(
|
||||
window: usize,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Option<ModelHandle<T>> {
|
||||
cx.update_global::<ProgramManager, _, _>(|pm, _| pm.remove_internal::<T>(window))
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
modals: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts or replaces the model at the given location.
|
||||
fn insert_or_replace_internal<T: Entity>(
|
||||
&mut self,
|
||||
window: usize,
|
||||
program: ModelHandle<T>,
|
||||
) -> Option<AnyModelHandle> {
|
||||
self.modals.insert(window, AnyModelHandle::from(program))
|
||||
}
|
||||
|
||||
/// Remove the program associated with this window, if it's of the given type
|
||||
fn remove_internal<T: Entity>(&mut self, window: usize) -> Option<ModelHandle<T>> {
|
||||
let program = self.modals.remove(&window);
|
||||
if let Some(program) = program {
|
||||
if program.is::<T>() {
|
||||
// Guaranteed to be some, but leave it in the option
|
||||
// anyway for the API
|
||||
program.downcast()
|
||||
} else {
|
||||
// Model is of the incorrect type, put it back
|
||||
self.modals.insert(window, program);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,14 +5,15 @@ use gpui::{
|
|||
};
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use theme::Theme;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub trait SidebarItem: View {
|
||||
fn should_activate_item_on_event(&self, _: &Self::Event, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
fn should_show_badge(&self, cx: &AppContext) -> bool;
|
||||
fn should_show_badge(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
fn contains_focused_view(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
@ -53,20 +54,27 @@ impl From<&dyn SidebarItemHandle> for AnyViewHandle {
|
|||
}
|
||||
|
||||
pub struct Sidebar {
|
||||
side: Side,
|
||||
sidebar_side: SidebarSide,
|
||||
items: Vec<Item>,
|
||||
is_open: bool,
|
||||
active_item_ix: usize,
|
||||
actual_width: Rc<RefCell<f32>>,
|
||||
custom_width: Rc<RefCell<f32>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
||||
pub enum Side {
|
||||
pub enum SidebarSide {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl SidebarSide {
|
||||
fn to_resizable_side(self) -> Side {
|
||||
match self {
|
||||
Self::Left => Side::Right,
|
||||
Self::Right => Side::Left,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Item {
|
||||
icon_path: &'static str,
|
||||
tooltip: String,
|
||||
|
@ -80,21 +88,19 @@ pub struct SidebarButtons {
|
|||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
pub struct ToggleSidebarItem {
|
||||
pub side: Side,
|
||||
pub sidebar_side: SidebarSide,
|
||||
pub item_index: usize,
|
||||
}
|
||||
|
||||
impl_actions!(workspace, [ToggleSidebarItem]);
|
||||
|
||||
impl Sidebar {
|
||||
pub fn new(side: Side) -> Self {
|
||||
pub fn new(sidebar_side: SidebarSide) -> Self {
|
||||
Self {
|
||||
side,
|
||||
sidebar_side,
|
||||
items: Default::default(),
|
||||
active_item_ix: 0,
|
||||
is_open: false,
|
||||
actual_width: Rc::new(RefCell::new(260.)),
|
||||
custom_width: Rc::new(RefCell::new(260.)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,38 +177,6 @@ impl Sidebar {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let actual_width = self.actual_width.clone();
|
||||
let custom_width = self.custom_width.clone();
|
||||
let side = self.side;
|
||||
MouseEventHandler::new::<Self, _, _>(side as usize, cx, |_, _| {
|
||||
Empty::new()
|
||||
.contained()
|
||||
.with_style(theme.workspace.sidebar_resize_handle)
|
||||
.boxed()
|
||||
})
|
||||
.with_padding(Padding {
|
||||
left: 4.,
|
||||
right: 4.,
|
||||
..Default::default()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::ResizeLeftRight)
|
||||
.on_down(MouseButton::Left, |_, _| {}) // This prevents the mouse down event from being propagated elsewhere
|
||||
.on_drag(MouseButton::Left, move |e, cx| {
|
||||
let delta = e.position.x() - e.prev_mouse_position.x();
|
||||
let prev_width = *actual_width.borrow();
|
||||
*custom_width.borrow_mut() = 0f32
|
||||
.max(match side {
|
||||
Side::Left => prev_width + delta,
|
||||
Side::Right => prev_width - delta,
|
||||
})
|
||||
.round();
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for Sidebar {
|
||||
|
@ -215,31 +189,20 @@ impl View for Sidebar {
|
|||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
if let Some(active_item) = self.active_item() {
|
||||
let mut container = Flex::row();
|
||||
if matches!(self.side, Side::Right) {
|
||||
container.add_child(self.render_resize_handle(&theme, cx));
|
||||
}
|
||||
|
||||
container.add_child(
|
||||
Hook::new(
|
||||
enum ResizeHandleTag {}
|
||||
let style = &cx.global::<Settings>().theme.workspace.sidebar;
|
||||
ChildView::new(active_item.to_any())
|
||||
.constrained()
|
||||
.with_max_width(*self.custom_width.borrow())
|
||||
.boxed(),
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.with_resize_handle::<ResizeHandleTag, _>(
|
||||
self.sidebar_side as usize,
|
||||
self.sidebar_side.to_resizable_side(),
|
||||
4.,
|
||||
style.initial_size,
|
||||
cx,
|
||||
)
|
||||
.on_after_layout({
|
||||
let actual_width = self.actual_width.clone();
|
||||
move |size, _| *actual_width.borrow_mut() = size.x()
|
||||
})
|
||||
.flex(1., false)
|
||||
.boxed(),
|
||||
);
|
||||
if matches!(self.side, Side::Left) {
|
||||
container.add_child(self.render_resize_handle(&theme, cx));
|
||||
}
|
||||
container.boxed()
|
||||
.boxed()
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
|
@ -271,10 +234,10 @@ impl View for SidebarButtons {
|
|||
let badge_style = theme.badge;
|
||||
let active_ix = sidebar.active_item_ix;
|
||||
let is_open = sidebar.is_open;
|
||||
let side = sidebar.side;
|
||||
let group_style = match side {
|
||||
Side::Left => theme.group_left,
|
||||
Side::Right => theme.group_right,
|
||||
let sidebar_side = sidebar.sidebar_side;
|
||||
let group_style = match sidebar_side {
|
||||
SidebarSide::Left => theme.group_left,
|
||||
SidebarSide::Right => theme.group_right,
|
||||
};
|
||||
|
||||
#[allow(clippy::needless_collect)]
|
||||
|
@ -288,10 +251,10 @@ impl View for SidebarButtons {
|
|||
.with_children(items.into_iter().enumerate().map(
|
||||
|(ix, (icon_path, tooltip, item_view))| {
|
||||
let action = ToggleSidebarItem {
|
||||
side,
|
||||
sidebar_side,
|
||||
item_index: ix,
|
||||
};
|
||||
MouseEventHandler::new::<Self, _, _>(ix, cx, move |state, cx| {
|
||||
MouseEventHandler::<Self>::new(ix, cx, move |state, cx| {
|
||||
let is_active = is_open && ix == active_ix;
|
||||
let style = item_style.style_for(state, is_active);
|
||||
Stack::new()
|
||||
|
|
|
@ -166,7 +166,7 @@ fn nav_button<A: Action + Clone>(
|
|||
action_name: &str,
|
||||
cx: &mut RenderContext<Toolbar>,
|
||||
) -> ElementBox {
|
||||
MouseEventHandler::new::<A, _, _>(0, cx, |state, _| {
|
||||
MouseEventHandler::<A>::new(0, cx, |state, _| {
|
||||
let style = if enabled {
|
||||
style.style_for(state, false)
|
||||
} else {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{sidebar::Side, AppState, ToggleFollow, Workspace};
|
||||
use crate::{sidebar::SidebarSide, AppState, ToggleFollow, Workspace};
|
||||
use anyhow::Result;
|
||||
use client::{proto, Client, Contact};
|
||||
use gpui::{
|
||||
|
@ -74,8 +74,7 @@ impl WaitingRoom {
|
|||
) -> Self {
|
||||
let project_id = contact.projects[project_index].id;
|
||||
let client = app_state.client.clone();
|
||||
let _join_task =
|
||||
cx.spawn_weak({
|
||||
let _join_task = cx.spawn_weak({
|
||||
let contact = contact.clone();
|
||||
|this, mut cx| async move {
|
||||
let project = Project::remote(
|
||||
|
@ -95,17 +94,20 @@ impl WaitingRoom {
|
|||
match project {
|
||||
Ok(project) => {
|
||||
cx.replace_root_view(|cx| {
|
||||
let mut workspace = Workspace::new(project, cx);
|
||||
let mut workspace =
|
||||
Workspace::new(project, app_state.default_item_factory, cx);
|
||||
(app_state.initialize_workspace)(
|
||||
&mut workspace,
|
||||
&app_state,
|
||||
cx,
|
||||
);
|
||||
workspace.toggle_sidebar(Side::Left, cx);
|
||||
if let Some((host_peer_id, _)) =
|
||||
workspace.project.read(cx).collaborators().iter().find(
|
||||
|(_, collaborator)| collaborator.replica_id == 0,
|
||||
)
|
||||
workspace.toggle_sidebar(SidebarSide::Left, cx);
|
||||
if let Some((host_peer_id, _)) = workspace
|
||||
.project
|
||||
.read(cx)
|
||||
.collaborators()
|
||||
.iter()
|
||||
.find(|(_, collaborator)| collaborator.replica_id == 0)
|
||||
{
|
||||
if let Some(follow) = workspace
|
||||
.toggle_follow(&ToggleFollow(*host_peer_id), cx)
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
///
|
||||
/// This may cause issues when you're trying to write tests that use workspace focus to add items at
|
||||
/// specific locations.
|
||||
pub mod dock;
|
||||
pub mod pane;
|
||||
pub mod pane_group;
|
||||
pub mod programs;
|
||||
pub mod searchable;
|
||||
pub mod sidebar;
|
||||
mod status_bar;
|
||||
|
@ -18,6 +18,7 @@ use client::{
|
|||
};
|
||||
use clock::ReplicaId;
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use dock::{DefaultItemFactory, Dock, ToggleDockButton};
|
||||
use drag_and_drop::DragAndDrop;
|
||||
use futures::{channel::oneshot, FutureExt};
|
||||
use gpui::{
|
||||
|
@ -33,16 +34,15 @@ use gpui::{
|
|||
RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use log::error;
|
||||
use log::{error, warn};
|
||||
pub use pane::*;
|
||||
pub use pane_group::*;
|
||||
use postage::prelude::Stream;
|
||||
use programs::ProgramManager;
|
||||
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
|
||||
use searchable::SearchableItemHandle;
|
||||
use serde::Deserialize;
|
||||
use settings::{Autosave, Settings};
|
||||
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
|
||||
use settings::{Autosave, DockAnchor, Settings};
|
||||
use sidebar::{Sidebar, SidebarButtons, SidebarSide, ToggleSidebarItem};
|
||||
use smallvec::SmallVec;
|
||||
use status_bar::StatusBar;
|
||||
pub use status_bar::StatusItemView;
|
||||
|
@ -146,10 +146,8 @@ impl_internal_actions!(
|
|||
impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]);
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
// Initialize the program manager immediately
|
||||
cx.set_global(ProgramManager::new());
|
||||
|
||||
pane::init(cx);
|
||||
dock::init(cx);
|
||||
|
||||
cx.add_global_action(open);
|
||||
cx.add_global_action({
|
||||
|
@ -217,10 +215,10 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
|||
workspace.activate_next_pane(cx)
|
||||
});
|
||||
cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| {
|
||||
workspace.toggle_sidebar(Side::Left, cx);
|
||||
workspace.toggle_sidebar(SidebarSide::Left, cx);
|
||||
});
|
||||
cx.add_action(|workspace: &mut Workspace, _: &ToggleRightSidebar, cx| {
|
||||
workspace.toggle_sidebar(Side::Right, cx);
|
||||
workspace.toggle_sidebar(SidebarSide::Right, cx);
|
||||
});
|
||||
cx.add_action(Workspace::activate_pane_at_index);
|
||||
|
||||
|
@ -265,6 +263,7 @@ pub struct AppState {
|
|||
pub fs: Arc<dyn fs::Fs>,
|
||||
pub build_window_options: fn() -> WindowOptions<'static>,
|
||||
pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
|
||||
pub default_item_factory: DefaultItemFactory,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash)]
|
||||
|
@ -870,11 +869,13 @@ impl AppState {
|
|||
project_store,
|
||||
initialize_workspace: |_, _, _| {},
|
||||
build_window_options: Default::default,
|
||||
default_item_factory: |_, _| unimplemented!(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
DockAnchorChanged,
|
||||
PaneAdded(ViewHandle<Pane>),
|
||||
ContactRequestedJoin(u64),
|
||||
}
|
||||
|
@ -892,7 +893,9 @@ pub struct Workspace {
|
|||
panes: Vec<ViewHandle<Pane>>,
|
||||
panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
|
||||
active_pane: ViewHandle<Pane>,
|
||||
last_active_center_pane: Option<ViewHandle<Pane>>,
|
||||
status_bar: ViewHandle<StatusBar>,
|
||||
dock: Dock,
|
||||
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
|
||||
project: ModelHandle<Project>,
|
||||
leader_state: LeaderState,
|
||||
|
@ -922,7 +925,11 @@ enum FollowerItem {
|
|||
}
|
||||
|
||||
impl Workspace {
|
||||
pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(
|
||||
project: ModelHandle<Project>,
|
||||
dock_default_factory: DefaultItemFactory,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.observe_fullscreen(|_, _, cx| cx.notify()).detach();
|
||||
|
||||
cx.observe_window_activation(Self::on_window_activation_changed)
|
||||
|
@ -949,14 +956,14 @@ impl Workspace {
|
|||
})
|
||||
.detach();
|
||||
|
||||
let pane = cx.add_view(Pane::new);
|
||||
let pane_id = pane.id();
|
||||
cx.subscribe(&pane, move |this, _, event, cx| {
|
||||
let center_pane = cx.add_view(|cx| Pane::new(None, cx));
|
||||
let pane_id = center_pane.id();
|
||||
cx.subscribe(¢er_pane, move |this, _, event, cx| {
|
||||
this.handle_pane_event(pane_id, event, cx)
|
||||
})
|
||||
.detach();
|
||||
cx.focus(&pane);
|
||||
cx.emit(Event::PaneAdded(pane.clone()));
|
||||
cx.focus(¢er_pane);
|
||||
cx.emit(Event::PaneAdded(center_pane.clone()));
|
||||
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let user_store = project.read(cx).user_store();
|
||||
|
@ -978,33 +985,44 @@ impl Workspace {
|
|||
}
|
||||
});
|
||||
|
||||
let weak_self = cx.weak_handle();
|
||||
let handle = cx.handle();
|
||||
let weak_handle = cx.weak_handle();
|
||||
|
||||
cx.emit_global(WorkspaceCreated(weak_self.clone()));
|
||||
cx.emit_global(WorkspaceCreated(weak_handle.clone()));
|
||||
|
||||
let left_sidebar = cx.add_view(|_| Sidebar::new(Side::Left));
|
||||
let right_sidebar = cx.add_view(|_| Sidebar::new(Side::Right));
|
||||
let dock = Dock::new(cx, dock_default_factory);
|
||||
let dock_pane = dock.pane().clone();
|
||||
|
||||
let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left));
|
||||
let right_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Right));
|
||||
let left_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), cx));
|
||||
let toggle_dock = cx.add_view(|cx| ToggleDockButton::new(handle, cx));
|
||||
let right_sidebar_buttons =
|
||||
cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx));
|
||||
let status_bar = cx.add_view(|cx| {
|
||||
let mut status_bar = StatusBar::new(&pane.clone(), cx);
|
||||
let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
|
||||
status_bar.add_left_item(left_sidebar_buttons, cx);
|
||||
status_bar.add_right_item(right_sidebar_buttons, cx);
|
||||
status_bar.add_right_item(toggle_dock, cx);
|
||||
status_bar
|
||||
});
|
||||
|
||||
cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
|
||||
drag_and_drop.register_container(weak_self.clone());
|
||||
drag_and_drop.register_container(weak_handle.clone());
|
||||
});
|
||||
|
||||
let mut this = Workspace {
|
||||
modal: None,
|
||||
weak_self,
|
||||
center: PaneGroup::new(pane.clone()),
|
||||
panes: vec![pane.clone()],
|
||||
weak_self: weak_handle,
|
||||
center: PaneGroup::new(center_pane.clone()),
|
||||
dock,
|
||||
// When removing an item, the last element remaining in this array
|
||||
// is used to find where focus should fallback to. As such, the order
|
||||
// of these two variables is important.
|
||||
panes: vec![dock_pane, center_pane.clone()],
|
||||
panes_by_item: Default::default(),
|
||||
active_pane: pane.clone(),
|
||||
active_pane: center_pane.clone(),
|
||||
last_active_center_pane: Some(center_pane.clone()),
|
||||
status_bar,
|
||||
notifications: Default::default(),
|
||||
client,
|
||||
|
@ -1078,6 +1096,7 @@ impl Workspace {
|
|||
app_state.fs.clone(),
|
||||
cx,
|
||||
),
|
||||
app_state.default_item_factory,
|
||||
cx,
|
||||
);
|
||||
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
||||
|
@ -1459,24 +1478,31 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn toggle_sidebar(&mut self, side: Side, cx: &mut ViewContext<Self>) {
|
||||
let sidebar = match side {
|
||||
Side::Left => &mut self.left_sidebar,
|
||||
Side::Right => &mut self.right_sidebar,
|
||||
pub fn toggle_sidebar(&mut self, sidebar_side: SidebarSide, cx: &mut ViewContext<Self>) {
|
||||
let sidebar = match sidebar_side {
|
||||
SidebarSide::Left => &mut self.left_sidebar,
|
||||
SidebarSide::Right => &mut self.right_sidebar,
|
||||
};
|
||||
sidebar.update(cx, |sidebar, cx| {
|
||||
sidebar.set_open(!sidebar.is_open(), cx);
|
||||
let open = sidebar.update(cx, |sidebar, cx| {
|
||||
let open = !sidebar.is_open();
|
||||
sidebar.set_open(open, cx);
|
||||
open
|
||||
});
|
||||
|
||||
if open {
|
||||
Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
|
||||
}
|
||||
|
||||
cx.focus_self();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
|
||||
let sidebar = match action.side {
|
||||
Side::Left => &mut self.left_sidebar,
|
||||
Side::Right => &mut self.right_sidebar,
|
||||
let sidebar = match action.sidebar_side {
|
||||
SidebarSide::Left => &mut self.left_sidebar,
|
||||
SidebarSide::Right => &mut self.right_sidebar,
|
||||
};
|
||||
let active_item = sidebar.update(cx, |sidebar, cx| {
|
||||
let active_item = sidebar.update(cx, move |sidebar, cx| {
|
||||
if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
|
||||
sidebar.set_open(false, cx);
|
||||
None
|
||||
|
@ -1486,7 +1512,10 @@ impl Workspace {
|
|||
sidebar.active_item().cloned()
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(active_item) = active_item {
|
||||
Dock::hide_on_sidebar_shown(self, action.sidebar_side, cx);
|
||||
|
||||
if active_item.is_focused(cx) {
|
||||
cx.focus_self();
|
||||
} else {
|
||||
|
@ -1500,13 +1529,13 @@ impl Workspace {
|
|||
|
||||
pub fn toggle_sidebar_item_focus(
|
||||
&mut self,
|
||||
side: Side,
|
||||
sidebar_side: SidebarSide,
|
||||
item_index: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let sidebar = match side {
|
||||
Side::Left => &mut self.left_sidebar,
|
||||
Side::Right => &mut self.right_sidebar,
|
||||
let sidebar = match sidebar_side {
|
||||
SidebarSide::Left => &mut self.left_sidebar,
|
||||
SidebarSide::Right => &mut self.right_sidebar,
|
||||
};
|
||||
let active_item = sidebar.update(cx, |sidebar, cx| {
|
||||
sidebar.set_open(true, cx);
|
||||
|
@ -1514,6 +1543,8 @@ impl Workspace {
|
|||
sidebar.active_item().cloned()
|
||||
});
|
||||
if let Some(active_item) = active_item {
|
||||
Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
|
||||
|
||||
if active_item.is_focused(cx) {
|
||||
cx.focus_self();
|
||||
} else {
|
||||
|
@ -1529,7 +1560,7 @@ impl Workspace {
|
|||
}
|
||||
|
||||
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
|
||||
let pane = cx.add_view(Pane::new);
|
||||
let pane = cx.add_view(|cx| Pane::new(None, cx));
|
||||
let pane_id = pane.id();
|
||||
cx.subscribe(&pane, move |this, _, event, cx| {
|
||||
this.handle_pane_event(pane_id, event, cx)
|
||||
|
@ -1682,6 +1713,15 @@ impl Workspace {
|
|||
status_bar.set_active_pane(&self.active_pane, cx);
|
||||
});
|
||||
self.active_item_path_changed(cx);
|
||||
|
||||
if &pane == self.dock_pane() {
|
||||
Dock::show(self, cx);
|
||||
} else {
|
||||
self.last_active_center_pane = Some(pane.clone());
|
||||
if self.dock.is_anchored_at(DockAnchor::Expanded) {
|
||||
Dock::hide(self, cx);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -1701,21 +1741,19 @@ impl Workspace {
|
|||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(pane) = self.pane(pane_id) {
|
||||
let is_dock = &pane == self.dock.pane();
|
||||
match event {
|
||||
pane::Event::Split(direction) => {
|
||||
pane::Event::Split(direction) if !is_dock => {
|
||||
self.split_pane(pane, *direction, cx);
|
||||
}
|
||||
pane::Event::Remove => {
|
||||
self.remove_pane(pane, cx);
|
||||
}
|
||||
pane::Event::Focused => {
|
||||
self.handle_pane_focused(pane, cx);
|
||||
}
|
||||
pane::Event::Remove if !is_dock => self.remove_pane(pane, cx),
|
||||
pane::Event::Remove if is_dock => Dock::hide(self, cx),
|
||||
pane::Event::Focused => self.handle_pane_focused(pane, cx),
|
||||
pane::Event::ActivateItem { local } => {
|
||||
if *local {
|
||||
self.unfollow(&pane, cx);
|
||||
}
|
||||
if pane == self.active_pane {
|
||||
if &pane == self.active_pane() {
|
||||
self.active_item_path_changed(cx);
|
||||
}
|
||||
}
|
||||
|
@ -1733,8 +1771,9 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
} else if self.dock.visible_pane().is_none() {
|
||||
error!("pane {} not found", pane_id);
|
||||
}
|
||||
}
|
||||
|
@ -1745,6 +1784,11 @@ impl Workspace {
|
|||
direction: SplitDirection,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<ViewHandle<Pane>> {
|
||||
if &pane == self.dock_pane() {
|
||||
warn!("Can't split dock pane.");
|
||||
return None;
|
||||
}
|
||||
|
||||
pane.read(cx).active_item().map(|item| {
|
||||
let new_pane = self.add_pane(cx);
|
||||
if let Some(clone) = item.clone_on_split(cx.as_mut()) {
|
||||
|
@ -1765,6 +1809,10 @@ impl Workspace {
|
|||
for removed_item in pane.read(cx).items() {
|
||||
self.panes_by_item.remove(&removed_item.id());
|
||||
}
|
||||
if self.last_active_center_pane == Some(pane) {
|
||||
self.last_active_center_pane = None;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
} else {
|
||||
self.active_item_path_changed(cx);
|
||||
|
@ -1783,6 +1831,10 @@ impl Workspace {
|
|||
&self.active_pane
|
||||
}
|
||||
|
||||
pub fn dock_pane(&self) -> &ViewHandle<Pane> {
|
||||
self.dock.pane()
|
||||
}
|
||||
|
||||
fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
|
||||
if let Some(remote_id) = remote_id {
|
||||
self.remote_entity_subscription =
|
||||
|
@ -1975,8 +2027,9 @@ impl Workspace {
|
|||
theme.workspace.titlebar.container
|
||||
};
|
||||
|
||||
enum TitleBar {}
|
||||
ConstrainedBox::new(
|
||||
MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
|
||||
MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
|
||||
Container::new(
|
||||
Stack::new()
|
||||
.with_child(
|
||||
|
@ -2105,7 +2158,7 @@ impl Workspace {
|
|||
None
|
||||
} else {
|
||||
Some(
|
||||
MouseEventHandler::new::<Authenticate, _, _>(0, cx, |state, _| {
|
||||
MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
|
||||
let style = theme
|
||||
.workspace
|
||||
.titlebar
|
||||
|
@ -2165,7 +2218,7 @@ impl Workspace {
|
|||
.boxed();
|
||||
|
||||
if let Some((peer_id, peer_github_login)) = peer {
|
||||
MouseEventHandler::new::<ToggleFollow, _, _>(replica_id.into(), cx, move |_, _| content)
|
||||
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleFollow(peer_id))
|
||||
|
@ -2191,7 +2244,7 @@ impl Workspace {
|
|||
if self.project.read(cx).is_read_only() {
|
||||
enum DisconnectedOverlay {}
|
||||
Some(
|
||||
MouseEventHandler::new::<DisconnectedOverlay, _, _>(0, cx, |_, cx| {
|
||||
MouseEventHandler::<DisconnectedOverlay>::new(0, cx, |_, cx| {
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
Label::new(
|
||||
"Your connection to the remote project has been lost.".to_string(),
|
||||
|
@ -2202,6 +2255,7 @@ impl Workspace {
|
|||
.with_style(theme.workspace.disconnected_overlay.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::Arrow)
|
||||
.capture_all()
|
||||
.boxed(),
|
||||
)
|
||||
|
@ -2556,6 +2610,9 @@ impl View for Workspace {
|
|||
None
|
||||
},
|
||||
)
|
||||
.with_child(
|
||||
FlexItem::new(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
FlexItem::new(self.center.render(
|
||||
&theme,
|
||||
|
@ -2565,6 +2622,17 @@ impl View for Workspace {
|
|||
.flex(1., true)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(self.dock.render(
|
||||
&theme,
|
||||
DockAnchor::Bottom,
|
||||
cx,
|
||||
))
|
||||
.boxed(),
|
||||
)
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(self.dock.render(&theme, DockAnchor::Right, cx))
|
||||
.with_children(
|
||||
if self.right_sidebar.read(cx).active_item().is_some() {
|
||||
Some(
|
||||
|
@ -2578,6 +2646,14 @@ impl View for Workspace {
|
|||
)
|
||||
.boxed()
|
||||
})
|
||||
.with_child(
|
||||
Overlay::new(
|
||||
Stack::new()
|
||||
.with_children(self.dock.render(
|
||||
&theme,
|
||||
DockAnchor::Expanded,
|
||||
cx,
|
||||
))
|
||||
.with_children(self.modal.as_ref().map(|m| {
|
||||
ChildView::new(m)
|
||||
.contained()
|
||||
|
@ -2587,6 +2663,10 @@ impl View for Workspace {
|
|||
.boxed()
|
||||
}))
|
||||
.with_children(self.render_notifications(&theme.workspace))
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.flex(1.0, true)
|
||||
.boxed(),
|
||||
)
|
||||
|
@ -2605,6 +2685,14 @@ impl View for Workspace {
|
|||
cx.focus(&self.active_pane);
|
||||
}
|
||||
}
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
|
||||
let mut keymap = Self::default_keymap_context();
|
||||
if self.active_pane() == self.dock_pane() {
|
||||
keymap.set.insert("Dock".into());
|
||||
}
|
||||
keymap
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WorkspaceHandle {
|
||||
|
@ -2785,10 +2873,10 @@ pub fn open_paths(
|
|||
cx,
|
||||
);
|
||||
new_project = Some(project.clone());
|
||||
let mut workspace = Workspace::new(project, cx);
|
||||
let mut workspace = Workspace::new(project, app_state.default_item_factory, cx);
|
||||
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
||||
if contains_directory {
|
||||
workspace.toggle_sidebar(Side::Left, cx);
|
||||
workspace.toggle_sidebar(SidebarSide::Left, cx);
|
||||
}
|
||||
workspace
|
||||
})
|
||||
|
@ -2846,6 +2934,7 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
|
|||
app_state.fs.clone(),
|
||||
cx,
|
||||
),
|
||||
app_state.default_item_factory,
|
||||
cx,
|
||||
);
|
||||
(app_state.initialize_workspace)(&mut workspace, app_state, cx);
|
||||
|
@ -2858,11 +2947,20 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
|
|||
mod tests {
|
||||
use std::cell::Cell;
|
||||
|
||||
use crate::sidebar::SidebarItem;
|
||||
|
||||
use super::*;
|
||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
|
||||
use project::{FakeFs, Project, ProjectEntryId};
|
||||
use serde_json::json;
|
||||
|
||||
pub fn default_item_factory(
|
||||
_workspace: &mut Workspace,
|
||||
_cx: &mut ViewContext<Workspace>,
|
||||
) -> Box<dyn ItemHandle> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tab_disambiguation(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
@ -2870,7 +2968,8 @@ mod tests {
|
|||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
|
||||
|
||||
// Adding an item with no ambiguity renders the tab without detail.
|
||||
let item1 = cx.add_view(&workspace, |_| {
|
||||
|
@ -2934,7 +3033,8 @@ mod tests {
|
|||
.await;
|
||||
|
||||
let project = Project::test(fs, ["root1".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
|
||||
let worktree_id = project.read_with(cx, |project, cx| {
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
});
|
||||
|
@ -3030,7 +3130,8 @@ mod tests {
|
|||
fs.insert_tree("/root", json!({ "one": "" })).await;
|
||||
|
||||
let project = Project::test(fs, ["root".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
|
||||
|
||||
// When there are no dirty items, there's nothing to do.
|
||||
let item1 = cx.add_view(&workspace, |_| TestItem::new());
|
||||
|
@ -3070,7 +3171,8 @@ mod tests {
|
|||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
|
||||
|
||||
let item1 = cx.add_view(&workspace, |_| {
|
||||
let mut item = TestItem::new();
|
||||
|
@ -3165,7 +3267,8 @@ mod tests {
|
|||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
|
||||
|
||||
// Create several workspace items with single project entries, and two
|
||||
// workspace items with multiple project entries.
|
||||
|
@ -3266,7 +3369,8 @@ mod tests {
|
|||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
|
||||
|
||||
let item = cx.add_view(&workspace, |_| {
|
||||
let mut item = TestItem::new();
|
||||
|
@ -3383,7 +3487,7 @@ mod tests {
|
|||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
|
||||
|
||||
let item = cx.add_view(&workspace, |_| {
|
||||
let mut item = TestItem::new();
|
||||
|
@ -3635,4 +3739,6 @@ mod tests {
|
|||
vec![ItemEvent::UpdateTab, ItemEvent::Edit]
|
||||
}
|
||||
}
|
||||
|
||||
impl SidebarItem for TestItem {}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
|||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.53.1"
|
||||
version = "0.54.1"
|
||||
|
||||
[lib]
|
||||
name = "zed"
|
||||
|
@ -27,6 +27,7 @@ context_menu = { path = "../context_menu" }
|
|||
client = { path = "../client" }
|
||||
clock = { path = "../clock" }
|
||||
contacts_panel = { path = "../contacts_panel" }
|
||||
contacts_status_item = { path = "../contacts_status_item" }
|
||||
diagnostics = { path = "../diagnostics" }
|
||||
editor = { path = "../editor" }
|
||||
file_finder = { path = "../file_finder" }
|
||||
|
|
|
@ -21,7 +21,7 @@ impl View for FeedbackLink {
|
|||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> gpui::ElementBox {
|
||||
MouseEventHandler::new::<Self, _, _>(0, cx, |state, cx| {
|
||||
MouseEventHandler::<Self>::new(0, cx, |state, cx| {
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
let theme = &theme.workspace.status_bar.feedback;
|
||||
Text::new(
|
||||
|
|
|
@ -1,26 +1,22 @@
|
|||
use super::installation::{npm_install_packages, npm_package_latest_version};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use super::installation::{latest_github_release, GitHubLspBinaryVersion};
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_trait::async_trait;
|
||||
use client::http::HttpClient;
|
||||
use collections::HashMap;
|
||||
use futures::StreamExt;
|
||||
use futures::{io::BufReader, StreamExt};
|
||||
use language::{LanguageServerName, LspAdapter};
|
||||
use serde_json::json;
|
||||
use smol::fs;
|
||||
use std::{any::Any, path::PathBuf, sync::Arc};
|
||||
use smol::fs::{self, File};
|
||||
use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct JsonLspAdapter;
|
||||
|
||||
impl JsonLspAdapter {
|
||||
const BIN_PATH: &'static str =
|
||||
"node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LspAdapter for JsonLspAdapter {
|
||||
async fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("vscode-json-languageserver".into())
|
||||
LanguageServerName("json-language-server".into())
|
||||
}
|
||||
|
||||
async fn server_args(&self) -> Vec<String> {
|
||||
|
@ -29,28 +25,46 @@ impl LspAdapter for JsonLspAdapter {
|
|||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: Arc<dyn HttpClient>,
|
||||
) -> Result<Box<dyn 'static + Any + Send>> {
|
||||
Ok(Box::new(npm_package_latest_version("vscode-json-languageserver").await?) as Box<_>)
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> Result<Box<dyn 'static + Send + Any>> {
|
||||
let release = latest_github_release("zed-industries/json-language-server", http).await?;
|
||||
let asset_name = format!("json-language-server-darwin-{}.gz", consts::ARCH);
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
|
||||
let version = GitHubLspBinaryVersion {
|
||||
name: release.name,
|
||||
url: asset.browser_download_url.clone(),
|
||||
};
|
||||
Ok(Box::new(version) as Box<_>)
|
||||
}
|
||||
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
version: Box<dyn 'static + Send + Any>,
|
||||
_: Arc<dyn HttpClient>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
container_dir: PathBuf,
|
||||
) -> Result<PathBuf> {
|
||||
let version = version.downcast::<String>().unwrap();
|
||||
let version_dir = container_dir.join(version.as_str());
|
||||
fs::create_dir_all(&version_dir)
|
||||
.await
|
||||
.context("failed to create version directory")?;
|
||||
let binary_path = version_dir.join(Self::BIN_PATH);
|
||||
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
|
||||
let destination_path = container_dir.join(format!(
|
||||
"json-language-server-{}-{}",
|
||||
version.name,
|
||||
consts::ARCH
|
||||
));
|
||||
|
||||
if fs::metadata(&binary_path).await.is_err() {
|
||||
npm_install_packages(
|
||||
[("vscode-json-languageserver", version.as_str())],
|
||||
&version_dir,
|
||||
if fs::metadata(&destination_path).await.is_err() {
|
||||
let mut response = http
|
||||
.get(&version.url, Default::default(), true)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error downloading release: {}", err))?;
|
||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
||||
let mut file = File::create(&destination_path).await?;
|
||||
futures::io::copy(decompressed_bytes, &mut file).await?;
|
||||
fs::set_permissions(
|
||||
&destination_path,
|
||||
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -58,37 +72,25 @@ impl LspAdapter for JsonLspAdapter {
|
|||
while let Some(entry) = entries.next().await {
|
||||
if let Some(entry) = entry.log_err() {
|
||||
let entry_path = entry.path();
|
||||
if entry_path.as_path() != version_dir {
|
||||
fs::remove_dir_all(&entry_path).await.log_err();
|
||||
if entry_path.as_path() != destination_path {
|
||||
fs::remove_file(&entry_path).await.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(binary_path)
|
||||
Ok(destination_path)
|
||||
}
|
||||
|
||||
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
|
||||
(|| async move {
|
||||
let mut last_version_dir = None;
|
||||
let mut last = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let bin_path = last_version_dir.join(Self::BIN_PATH);
|
||||
if bin_path.exists() {
|
||||
Ok(bin_path)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
last = Some(entry?.path());
|
||||
}
|
||||
last.ok_or_else(|| anyhow!("no cached binary"))
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
|
|
|
@ -19,20 +19,21 @@ use futures::{
|
|||
channel::{mpsc, oneshot},
|
||||
FutureExt, SinkExt, StreamExt,
|
||||
};
|
||||
use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task};
|
||||
use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task, ViewContext};
|
||||
use isahc::{config::Configurable, AsyncBody, Request};
|
||||
use language::LanguageRegistry;
|
||||
use log::LevelFilter;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Fs, ProjectStore};
|
||||
use serde_json::json;
|
||||
use settings::{self, KeymapFileContent, Settings, SettingsFileContent};
|
||||
use settings::{self, KeymapFileContent, Settings, SettingsFileContent, WorkingDirectory};
|
||||
use smol::process::Command;
|
||||
use std::{env, ffi::OsStr, fs, panic, path::PathBuf, sync::Arc, thread, time::Duration};
|
||||
use terminal::terminal_container_view::{get_working_directory, TerminalContainer};
|
||||
|
||||
use theme::ThemeRegistry;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{self, AppState, NewFile, OpenPaths};
|
||||
use workspace::{self, AppState, ItemHandle, NewFile, OpenPaths, Workspace};
|
||||
use zed::{
|
||||
self, build_window_options,
|
||||
fs::RealFs,
|
||||
|
@ -148,6 +149,7 @@ fn main() {
|
|||
fs,
|
||||
build_window_options,
|
||||
initialize_workspace,
|
||||
default_item_factory,
|
||||
});
|
||||
auto_update::init(db, http, client::ZED_SERVER_URL.clone(), cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
|
@ -591,3 +593,20 @@ async fn handle_cli_connection(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_item_factory(
|
||||
workspace: &mut Workspace,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Box<dyn ItemHandle> {
|
||||
let strategy = cx
|
||||
.global::<Settings>()
|
||||
.terminal_overrides
|
||||
.working_directory
|
||||
.clone()
|
||||
.unwrap_or(WorkingDirectory::CurrentProjectDirectory);
|
||||
|
||||
let working_directory = get_working_directory(workspace, cx, strategy);
|
||||
|
||||
let terminal_handle = cx.add_view(|cx| TerminalContainer::new(working_directory, false, cx));
|
||||
Box::new(terminal_handle)
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ use gpui::{
|
|||
geometry::vector::vec2f,
|
||||
impl_actions,
|
||||
platform::{WindowBounds, WindowOptions},
|
||||
AssetSource, AsyncAppContext, ViewContext,
|
||||
AssetSource, AsyncAppContext, TitlebarOptions, ViewContext, WindowKind,
|
||||
};
|
||||
use language::Rope;
|
||||
pub use lsp;
|
||||
|
@ -33,7 +33,7 @@ use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
|
|||
use std::{env, path::Path, str, sync::Arc};
|
||||
use util::ResultExt;
|
||||
pub use workspace;
|
||||
use workspace::{sidebar::Side, AppState, Workspace};
|
||||
use workspace::{sidebar::SidebarSide, AppState, Workspace};
|
||||
|
||||
#[derive(Deserialize, Clone, PartialEq)]
|
||||
struct OpenBrowser {
|
||||
|
@ -204,14 +204,14 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
|||
|workspace: &mut Workspace,
|
||||
_: &project_panel::ToggleFocus,
|
||||
cx: &mut ViewContext<Workspace>| {
|
||||
workspace.toggle_sidebar_item_focus(Side::Left, 0, cx);
|
||||
workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace,
|
||||
_: &contacts_panel::ToggleFocus,
|
||||
cx: &mut ViewContext<Workspace>| {
|
||||
workspace.toggle_sidebar_item_focus(Side::Right, 0, cx);
|
||||
workspace.toggle_sidebar_item_focus(SidebarSide::Right, 0, cx);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -243,6 +243,7 @@ pub fn initialize_workspace(
|
|||
.detach();
|
||||
|
||||
cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
|
||||
cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone()));
|
||||
|
||||
let settings = cx.global::<Settings>();
|
||||
|
||||
|
@ -330,9 +331,14 @@ pub fn initialize_workspace(
|
|||
pub fn build_window_options() -> WindowOptions<'static> {
|
||||
WindowOptions {
|
||||
bounds: WindowBounds::Maximized,
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: None,
|
||||
titlebar_appears_transparent: true,
|
||||
appears_transparent: true,
|
||||
traffic_light_position: Some(vec2f(8., 8.)),
|
||||
}),
|
||||
center: false,
|
||||
kind: WindowKind::Normal,
|
||||
is_movable: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -723,7 +729,8 @@ mod tests {
|
|||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
|
||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||
let file1 = entries[0].clone();
|
||||
|
@ -842,7 +849,8 @@ mod tests {
|
|||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
|
||||
// Open a file within an existing worktree.
|
||||
cx.update(|cx| {
|
||||
|
@ -1001,7 +1009,8 @@ mod tests {
|
|||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
|
||||
// Open a file within an existing worktree.
|
||||
cx.update(|cx| {
|
||||
|
@ -1043,7 +1052,8 @@ mod tests {
|
|||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
|
||||
|
||||
// Create a new untitled buffer
|
||||
|
@ -1132,7 +1142,8 @@ mod tests {
|
|||
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
|
||||
// Create a new untitled buffer
|
||||
cx.dispatch_action(window_id, NewFile);
|
||||
|
@ -1185,7 +1196,8 @@ mod tests {
|
|||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
|
||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||
let file1 = entries[0].clone();
|
||||
|
@ -1221,7 +1233,7 @@ mod tests {
|
|||
|
||||
cx.foreground().run_until_parked();
|
||||
workspace.read_with(cx, |workspace, _| {
|
||||
assert_eq!(workspace.panes().len(), 1);
|
||||
assert_eq!(workspace.panes().len(), 2); //Center pane + Dock pane
|
||||
assert_eq!(workspace.active_pane(), &pane_1);
|
||||
});
|
||||
|
||||
|
@ -1231,6 +1243,7 @@ mod tests {
|
|||
cx.foreground().run_until_parked();
|
||||
|
||||
workspace.read_with(cx, |workspace, cx| {
|
||||
assert_eq!(workspace.panes().len(), 2);
|
||||
assert!(workspace.active_item(cx).is_none());
|
||||
});
|
||||
|
||||
|
@ -1258,7 +1271,8 @@ mod tests {
|
|||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
||||
|
||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||
let file1 = entries[0].clone();
|
||||
|
@ -1522,7 +1536,8 @@ mod tests {
|
|||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
set -e
|
||||
|
||||
export ZED_BUNDLE=true
|
||||
export MACOSX_DEPLOYMENT_TARGET=10.14
|
||||
|
||||
echo "Installing cargo bundle"
|
||||
cargo install cargo-bundle --version 0.5.0
|
||||
|
|
1
styles/package-lock.json
generated
1
styles/package-lock.json
generated
|
@ -5,6 +5,7 @@
|
|||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "styles",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
|
|
@ -3,6 +3,7 @@ import chatPanel from "./chatPanel";
|
|||
import { text } from "./components";
|
||||
import contactFinder from "./contactFinder";
|
||||
import contactsPanel from "./contactsPanel";
|
||||
import contactsPopover from "./contactsPopover";
|
||||
import commandPalette from "./commandPalette";
|
||||
import editor from "./editor";
|
||||
import projectPanel from "./projectPanel";
|
||||
|
@ -34,6 +35,7 @@ export default function app(theme: Theme): Object {
|
|||
commandPalette: commandPalette(theme),
|
||||
projectPanel: projectPanel(theme),
|
||||
chatPanel: chatPanel(theme),
|
||||
contactsPopover: contactsPopover(theme),
|
||||
contactsPanel: contactsPanel(theme),
|
||||
contactFinder: contactFinder(theme),
|
||||
search: search(theme),
|
||||
|
|
8
styles/src/styleTree/contactsPopover.ts
Normal file
8
styles/src/styleTree/contactsPopover.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import Theme from "../themes/common/theme";
|
||||
import { backgroundColor } from "./components";
|
||||
|
||||
export default function workspace(theme: Theme) {
|
||||
return {
|
||||
background: backgroundColor(theme, 300),
|
||||
}
|
||||
}
|
|
@ -46,6 +46,11 @@ export default function search(theme: Theme) {
|
|||
background: backgroundColor(theme, "on500", "active"),
|
||||
border: border(theme, "muted"),
|
||||
},
|
||||
clicked: {
|
||||
...text(theme, "mono", "active"),
|
||||
background: backgroundColor(theme, "on300", "active"),
|
||||
border: border(theme, "secondary"),
|
||||
},
|
||||
hover: {
|
||||
...text(theme, "mono", "active"),
|
||||
background: backgroundColor(theme, "on500", "hovered"),
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue