diff --git a/Cargo.lock b/Cargo.lock index 634ef452a369b142c983302bcb471c00395e1a19..05701b7c56009e791c54b44d3e1917841b9394db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" @@ -7050,6 +7146,7 @@ dependencies = [ "collections", "command_palette", "contacts_panel", + "contacts_status_item", "context_menu", "ctor", "diagnostics", diff --git a/assets/icons/zed_22.svg b/assets/icons/zed_22.svg new file mode 100644 index 0000000000000000000000000000000000000000..68e7dc8e57c966d5723920bc027c313161b95af8 --- /dev/null +++ b/assets/icons/zed_22.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/crates/capture/Cargo.toml b/crates/capture/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..f8ed31097a81c8e96430c89df7b2a64b8e93a767 --- /dev/null +++ b/crates/capture/Cargo.toml @@ -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" diff --git a/crates/capture/build.rs b/crates/capture/build.rs new file mode 100644 index 0000000000000000000000000000000000000000..41f60ff48611e0b2e174e46de5455ae5516ed15f --- /dev/null +++ b/crates/capture/build.rs @@ -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"); +} diff --git a/crates/capture/src/live_kit_token.rs b/crates/capture/src/live_kit_token.rs new file mode 100644 index 0000000000000000000000000000000000000000..be4fc4f4a2ade98d8ed4d76b6553cba13d5ab18e --- /dev/null +++ b/crates/capture/src/live_kit_token.rs @@ -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, + room_join: Option, + room_list: Option, + room_record: Option, + room_admin: Option, + room: Option<&'a str>, + can_publish: Option, + can_subscribe: Option, + can_publish_data: Option, + hidden: Option, + recorder: Option, +} + +pub fn create_token( + api_key: &str, + secret_key: &str, + room_name: &str, + participant_name: &str, +) -> Result { + let secret_key: Hmac = 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)?) +} diff --git a/crates/capture/src/main.rs b/crates/capture/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..c34f451e41e14ae539259c231c05e2e6684e0408 --- /dev/null +++ b/crates/capture/src/main.rs @@ -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, + _room: Arc, +} + +impl gpui::Entity for ScreenCaptureView { + type Event = (); +} + +impl ScreenCaptureView { + pub fn new(room: Arc, cx: &mut ViewContext) -> 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) -> 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(); +} diff --git a/crates/contacts_status_item/Cargo.toml b/crates/contacts_status_item/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..df115a384220553afd75c37b6276931fe6794fb3 --- /dev/null +++ b/crates/contacts_status_item/Cargo.toml @@ -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"] } diff --git a/crates/contacts_status_item/src/contacts_popover.rs b/crates/contacts_status_item/src/contacts_popover.rs new file mode 100644 index 0000000000000000000000000000000000000000..2998d74ed8c3b608210c9d482831c3407a973c7f --- /dev/null +++ b/crates/contacts_status_item/src/contacts_popover.rs @@ -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, +} + +impl Entity for ContactsPopover { + type Event = Event; +} + +impl View for ContactsPopover { + fn ui_name() -> &'static str { + "ContactsPopover" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = &cx.global::().theme.contacts_popover; + + Flex::row() + .with_child( + ChildView::new(self.filter_editor.clone()) + .contained() + .with_style( + cx.global::() + .theme + .contacts_panel + .user_query_editor + .container, + ) + .flex(1., true) + .boxed(), + ) + // .with_child( + // MouseEventHandler::::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::() + .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 { + 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) { + if !is_active { + cx.emit(Event::Deactivated); + } + } +} diff --git a/crates/contacts_status_item/src/contacts_status_item.rs b/crates/contacts_status_item/src/contacts_status_item.rs new file mode 100644 index 0000000000000000000000000000000000000000..5d471abcdf51f2153a5cd98627ef5478fb3ceccb --- /dev/null +++ b/crates/contacts_status_item/src/contacts_status_item.rs @@ -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>, +} + +impl Entity for ContactsStatusItem { + type Event = (); +} + +impl View for ContactsStatusItem { + fn ui_name() -> &'static str { + "ContactsStatusItem" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let color = match cx.appearance { + Appearance::Light | Appearance::VibrantLight => Color::black(), + Appearance::Dark | Appearance::VibrantDark => Color::white(), + }; + MouseEventHandler::::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) { + 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, + event: &contacts_popover::Event, + cx: &mut ViewContext, + ) { + match event { + contacts_popover::Event::Deactivated => { + self.popover.take(); + cx.remove_window(popover.window_id()); + } + } + } +} diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 7387ec8fdc2d7bb8f8f3f68a6d2df1fd85aaf4a2..3111d7a9f17ea47b759a6749a317d1860f76516c 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -1150,7 +1150,7 @@ mod tests { editor: &ViewHandle, 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); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 43c1bdb6daeecf5f2b316e4cdcf07e8a4c7a0e82..1e1ab83063bfc31f9aa7aa964147007fa9e895ed 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2059,7 +2059,7 @@ mod tests { let layouts = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - let mut presenter = cx.build_presenter(window_id, 30.); + let mut presenter = cx.build_presenter(window_id, 30., Default::default()); let layout_cx = presenter.build_layout_context(Vector2F::zero(), false, cx); element.layout_line_numbers(0..6, &Default::default(), &snapshot, &layout_cx) }); @@ -2098,7 +2098,7 @@ mod tests { ); let mut scene = Scene::new(1.0); - let mut presenter = cx.build_presenter(window_id, 30.); + let mut presenter = cx.build_presenter(window_id, 30., Default::default()); let mut layout_cx = presenter.build_layout_context(Vector2F::zero(), false, cx); let (size, mut state) = element.layout( SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)), diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 72c65a4c337ff11fb828f324b5cef5995a6f5e72..51bc416e191b726b85121e28f681dd6550e81215 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -3,6 +3,7 @@ authors = ["Nathan Sobo "] 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" diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 73c3b3f478e8988ded07b78489dc41aa48f57b76..308ea6c831757326e799eb5fa7a36d035335d1da 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -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 { 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 { 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,42 +1927,56 @@ 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(&mut self, window_id: usize, build_root_view: F) -> ViewHandle + pub fn add_status_bar_item(&mut self, build_root_view: F) -> (usize, ViewHandle) where T: View, F: FnOnce(&mut ViewContext) -> 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)); - 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(); + let status_item = this.cx.platform.add_status_item(); + this.register_platform_window(window_id, status_item); + + (window_id, root_view) + }) } - 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, + ) { + let presenter = Rc::new(RefCell::new(self.build_presenter( + window_id, + window.titlebar_height(), + window.appearance(), + ))); { let mut app = self.upgrade(); @@ -2006,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(&mut self, window_id: usize, build_root_view: F) -> ViewHandle + where + T: View, + F: FnOnce(&mut ViewContext) -> 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(), @@ -2361,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 @@ -2425,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; @@ -3699,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, @@ -4031,6 +4097,7 @@ pub struct RenderParams { pub hovered_region_ids: HashSet, pub clicked_region_ids: Option<(HashSet, MouseButton)>, pub refreshing: bool, + pub appearance: Appearance, } pub struct RenderContext<'a, T: View> { @@ -4041,6 +4108,7 @@ pub struct RenderContext<'a, T: View> { pub(crate) clicked_region_ids: Option<(HashSet, MouseButton)>, pub app: &'a mut MutableAppContext, pub titlebar_height: f32, + pub appearance: Appearance, pub refreshing: bool, } @@ -4061,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, } } diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index df480005b58151f978e4fb370f413aff163c7e03..57436c256bd9ee241dc78d07206ba8c3655e2c54 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -659,7 +659,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 +759,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)) diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 92e38894c1d6170f65362f7e5ea1c71c0898ea14..45dc2dbae3343051e8b417fc6a12393757e125f7 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -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( diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 61cd025db5aea022fe65605ae3ac5342c690faa4..a50698070c1ecd472041d1a2db6c7ee2ed600fcd 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -39,6 +39,11 @@ pub trait Platform: Send + Sync { fn fonts(&self) -> Arc; 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, ) -> Box; fn key_window_id(&self) -> Option; - fn hide(&self); - fn hide_other_apps(&self); - fn unhide_other_apps(&self); - fn quit(&self); + + fn add_status_item(&self) -> Box; fn write_to_clipboard(&self, item: ClipboardItem); fn read_from_clipboard(&self) -> Option; @@ -107,7 +110,7 @@ pub trait InputHandler { fn rect_for_range(&self, range_utf16: Range) -> Option; } -pub trait Window: WindowContext { +pub trait Window { fn as_any_mut(&mut self) -> &mut dyn Any; fn on_event(&mut self, callback: Box bool>); fn on_active_status_change(&mut self, callback: Box); @@ -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); } #[derive(Debug)] pub struct WindowOptions<'a> { pub bounds: WindowBounds, + pub titlebar: Option>, + 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, } +#[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, @@ -168,6 +200,12 @@ pub enum CursorStyle { IBeam, } +impl Default for CursorStyle { + fn default() -> Self { + Self::Arrow + } +} + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct AppVersion { major: usize, @@ -175,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; @@ -247,9 +279,14 @@ impl<'a> Default for WindowOptions<'a> { fn default() -> Self { Self { bounds: WindowBounds::Maximized, - title: Default::default(), - titlebar_appears_transparent: Default::default(), - traffic_light_position: Default::default(), + titlebar: Some(TitlebarOptions { + title: Default::default(), + appears_transparent: Default::default(), + traffic_light_position: Default::default(), + }), + center: false, + kind: WindowKind::Normal, + is_movable: true, } } } diff --git a/crates/gpui/src/platform/mac.rs b/crates/gpui/src/platform/mac.rs index 8cf3f62874aac87bc6d1841fdfc79d7914a66b50..90b378e4a644dacb22f6550ea4783db98caa7237 100644 --- a/crates/gpui/src/platform/mac.rs +++ b/crates/gpui/src/platform/mac.rs @@ -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; diff --git a/crates/gpui/src/platform/mac/appearance.rs b/crates/gpui/src/platform/mac/appearance.rs new file mode 100644 index 0000000000000000000000000000000000000000..c0b8258a0e2a388075579a403deecba5b46ca85e --- /dev/null +++ b/crates/gpui/src/platform/mac/appearance.rs @@ -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; +} diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 58b1517cbca55eec432f7432be97c2e615b329c6..7732da2b3efe9da6e9c584e77d82c99cf85f0334 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -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, - ) -> Box { - Box::new(Window::open(id, options, executor, self.fonts())) - } - - fn key_window_id(&self) -> Option { - Window::key_window_id() - } - - fn fonts(&self) -> Arc { - 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, + ) -> Box { + Box::new(Window::open(id, options, executor, self.fonts())) + } + + fn key_window_id(&self) -> Option { + Window::key_window_id() + } + + fn add_status_item(&self) -> Box { + Box::new(StatusItem::add(self.fonts())) + } + + fn fonts(&self) -> Arc { + self.fonts.clone() + } + fn write_to_clipboard(&self, item: ClipboardItem) { unsafe { self.pasteboard.clearContents(); diff --git a/crates/gpui/src/platform/mac/renderer.rs b/crates/gpui/src/platform/mac/renderer.rs index 8fdbda15e84a8a2a212f80e424d6c722545e4322..ea094e998c112b4b875f129b2aef0c5d67fd8002 100644 --- a/crates/gpui/src/platform/mac/renderer.rs +++ b/crates/gpui/src/platform/mac/renderer.rs @@ -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, - ) -> Self { + pub fn new(is_opaque: bool, fonts: Arc) -> 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::() 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::(); + 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::() 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, diff --git a/crates/gpui/src/platform/mac/shaders/shaders.h b/crates/gpui/src/platform/mac/shaders/shaders.h index 3f5096f37c867cfa2d52f8a8c79cda439418b785..29be2c9e1e789ade67c4c05b53e8300779cdc884 100644 --- a/crates/gpui/src/platform/mac/shaders/shaders.h +++ b/crates/gpui/src/platform/mac/shaders/shaders.h @@ -1,122 +1,125 @@ #include -typedef struct -{ - vector_float2 viewport_size; +typedef struct { + vector_float2 viewport_size; } GPUIUniforms; -typedef enum -{ - GPUIQuadInputIndexVertices = 0, - GPUIQuadInputIndexQuads = 1, - GPUIQuadInputIndexUniforms = 2, +typedef enum { + GPUIQuadInputIndexVertices = 0, + GPUIQuadInputIndexQuads = 1, + GPUIQuadInputIndexUniforms = 2, } GPUIQuadInputIndex; -typedef struct -{ - vector_float2 origin; - vector_float2 size; - vector_uchar4 background_color; - float border_top; - float border_right; - float border_bottom; - float border_left; - vector_uchar4 border_color; - float corner_radius; +typedef struct { + vector_float2 origin; + vector_float2 size; + vector_uchar4 background_color; + float border_top; + float border_right; + float border_bottom; + float border_left; + vector_uchar4 border_color; + float corner_radius; } GPUIQuad; -typedef enum -{ - GPUIShadowInputIndexVertices = 0, - GPUIShadowInputIndexShadows = 1, - GPUIShadowInputIndexUniforms = 2, +typedef enum { + GPUIShadowInputIndexVertices = 0, + GPUIShadowInputIndexShadows = 1, + GPUIShadowInputIndexUniforms = 2, } GPUIShadowInputIndex; -typedef struct -{ - vector_float2 origin; - vector_float2 size; - float corner_radius; - float sigma; - vector_uchar4 color; +typedef struct { + vector_float2 origin; + vector_float2 size; + float corner_radius; + float sigma; + vector_uchar4 color; } GPUIShadow; -typedef enum -{ - GPUISpriteVertexInputIndexVertices = 0, - GPUISpriteVertexInputIndexSprites = 1, - GPUISpriteVertexInputIndexViewportSize = 2, - GPUISpriteVertexInputIndexAtlasSize = 3, +typedef enum { + GPUISpriteVertexInputIndexVertices = 0, + GPUISpriteVertexInputIndexSprites = 1, + GPUISpriteVertexInputIndexViewportSize = 2, + GPUISpriteVertexInputIndexAtlasSize = 3, } GPUISpriteVertexInputIndex; -typedef enum -{ - GPUISpriteFragmentInputIndexAtlas = 0, +typedef enum { + GPUISpriteFragmentInputIndexAtlas = 0, } GPUISpriteFragmentInputIndex; -typedef struct -{ - vector_float2 origin; - vector_float2 target_size; - vector_float2 source_size; - vector_float2 atlas_origin; - vector_uchar4 color; - uint8_t compute_winding; +typedef struct { + vector_float2 origin; + vector_float2 target_size; + vector_float2 source_size; + vector_float2 atlas_origin; + vector_uchar4 color; + uint8_t compute_winding; } GPUISprite; -typedef enum -{ - GPUIPathAtlasVertexInputIndexVertices = 0, - GPUIPathAtlasVertexInputIndexAtlasSize = 1, +typedef enum { + GPUIPathAtlasVertexInputIndexVertices = 0, + GPUIPathAtlasVertexInputIndexAtlasSize = 1, } GPUIPathAtlasVertexInputIndex; -typedef struct -{ - vector_float2 xy_position; - vector_float2 st_position; - vector_float2 clip_rect_origin; - vector_float2 clip_rect_size; +typedef struct { + vector_float2 xy_position; + vector_float2 st_position; + vector_float2 clip_rect_origin; + vector_float2 clip_rect_size; } GPUIPathVertex; -typedef enum -{ - GPUIImageVertexInputIndexVertices = 0, - GPUIImageVertexInputIndexImages = 1, - GPUIImageVertexInputIndexViewportSize = 2, - GPUIImageVertexInputIndexAtlasSize = 3, +typedef enum { + GPUIImageVertexInputIndexVertices = 0, + GPUIImageVertexInputIndexImages = 1, + GPUIImageVertexInputIndexViewportSize = 2, + GPUIImageVertexInputIndexAtlasSize = 3, } GPUIImageVertexInputIndex; -typedef enum -{ - GPUIImageFragmentInputIndexAtlas = 0, +typedef enum { + GPUIImageFragmentInputIndexAtlas = 0, } GPUIImageFragmentInputIndex; -typedef struct -{ - vector_float2 origin; - vector_float2 target_size; - vector_float2 source_size; - vector_float2 atlas_origin; - float border_top; - float border_right; - float border_bottom; - float border_left; - vector_uchar4 border_color; - float corner_radius; +typedef struct { + vector_float2 origin; + vector_float2 target_size; + vector_float2 source_size; + vector_float2 atlas_origin; + float border_top; + float border_right; + float border_bottom; + float border_left; + vector_uchar4 border_color; + float corner_radius; } GPUIImage; -typedef enum -{ - GPUIUnderlineInputIndexVertices = 0, - GPUIUnderlineInputIndexUnderlines = 1, - GPUIUnderlineInputIndexUniforms = 2, +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 -{ - vector_float2 origin; - vector_float2 size; - float thickness; - vector_uchar4 color; - uint8_t squiggly; +typedef struct { + vector_float2 origin; + vector_float2 size; + float thickness; + vector_uchar4 color; + uint8_t squiggly; } GPUIUnderline; diff --git a/crates/gpui/src/platform/mac/shaders/shaders.metal b/crates/gpui/src/platform/mac/shaders/shaders.metal index 2d79e69d569316f8a99e6c8375031006a9e9559d..795026e747e4ffa1f0083737c7d6b2158f79a1b9 100644 --- a/crates/gpui/src/platform/mac/shaders/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders/shaders.metal @@ -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 y_atlas [[ texture(GPUISurfaceFragmentInputIndexYAtlas) ]], + texture2d 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; diff --git a/crates/gpui/src/platform/mac/status_item.rs b/crates/gpui/src/platform/mac/status_item.rs new file mode 100644 index 0000000000000000000000000000000000000000..33feb4808f17c15136df7daeee287a90af921f3a --- /dev/null +++ b/crates/gpui/src/platform/mac/status_item.rs @@ -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>); + +struct StatusItemState { + native_item: StrongPtr, + native_view: StrongPtr, + renderer: Renderer, + scene: Option, + event_callback: Option bool>>, + appearance_changed_callback: Option>, +} + +impl StatusItem { + pub fn add(fonts: Arc) -> 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 bool>) { + self.0.borrow_mut().event_callback = Some(callback); + } + + fn on_appearance_changed(&mut self, callback: Box) { + self.0.borrow_mut().appearance_changed_callback = Some(callback); + } + + fn on_active_status_change(&mut self, _: Box) {} + + fn on_resize(&mut self, _: Box) {} + + fn on_fullscreen(&mut self, _: Box) {} + + fn on_should_close(&mut self, _: Box bool>) {} + + fn on_close(&mut self, _: Box) {} + + fn set_input_handler(&mut self, _: Box) {} + + fn prompt( + &self, + _: crate::PromptLevel, + _: &str, + _: &[&str], + ) -> postage::oneshot::Receiver { + 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> { + let raw: *mut c_void = *object.get_ivar(STATE_IVAR); + let weak1 = Weak::from_raw(raw as *mut RefCell); + 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); +} diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 9b52041d806bdbd60ec42139a2cca6619e9b53c2..1ce42db9ed68d3332d3bb931e0f49c58748cc09e 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -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>); ///Used to track what the IME does when we send it a keystroke. @@ -299,6 +323,7 @@ struct WindowState { fullscreen_callback: Option>, should_close_callback: Option bool>>, close_callback: Option>, + appearance_changed_callback: Option>, input_handler: Option>, pending_key_down: Option<(KeyDownEvent, Option)>, performed_key_equivalent: bool, @@ -306,9 +331,7 @@ struct WindowState { executor: Rc, scene_to_render: Option, renderer: Renderer, - command_queue: metal::CommandQueue, last_fresh_keydown: Option, - layer: id, traffic_light_position: Option, previous_modifiers_changed_event: Option, //State tracking what the IME did after the last request @@ -329,58 +352,59 @@ impl Window { executor: Rc, fonts: Arc, ) -> 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 - | NSWindowStyleMask::NSMiniaturizableWindowMask - | NSWindowStyleMask::NSResizableWindowMask - | NSWindowStyleMask::NSTitledWindowMask; - - if options.titlebar_appears_transparent { - style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask; + let mut style_mask; + if let Some(titlebar) = options.titlebar.as_ref() { + style_mask = NSWindowStyleMask::NSClosableWindowMask + | NSWindowStyleMask::NSMiniaturizableWindowMask + | NSWindowStyleMask::NSResizableWindowMask + | NSWindowStyleMask::NSTitledWindowMask; + + 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(); - native_window.setFrame_display_(screen.visibleFrame(), YES); + let screen = native_window.screen(); + match options.bounds { + WindowBounds::Maximized => { + native_window.setFrame_display_(screen.visibleFrame(), YES); + } + 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 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 - ]; - let native_view: id = msg_send![VIEW_CLASS, alloc]; let native_view = NSView::init(native_view); assert!(!native_view.is_null()); @@ -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); - native_window.center(); + 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(); } -} -impl platform::WindowContext for Window { - fn size(&self) -> Vector2F { - self.0.as_ref().borrow().size() + fn bounds(&self) -> RectF { + self.0.as_ref().borrow().bounds() + } + + 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) { + self.0.borrow_mut().appearance_changed_callback = Some(callback); + } } impl WindowState { @@ -701,10 +767,25 @@ impl WindowState { } } } -} -impl platform::WindowContext for WindowState { - fn size(&self) -> Vector2F { + 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) + } + } + + 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>, drag_id: usize, diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index 519ba931d10ca59fc1721a26b2566c9a8897b8b2..9a458a1dd96609c70c6acc4ffed2e69b6e44faa3 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -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 { + 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) {} } pub fn platform() -> Platform { diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index cba7d343782c09db731266f844181a53294a947b..eaccd8f4109a046cd0f8e01e05d444effb486820 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -12,10 +12,10 @@ use crate::{ 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}; @@ -41,12 +41,14 @@ pub struct Presenter { clicked_button: Option, mouse_position: Vector2F, titlebar_height: f32, + appearance: Appearance, } impl Presenter { pub fn new( window_id: usize, titlebar_height: f32, + appearance: Appearance, font_cache: Arc, text_layout_cache: TextLayoutCache, asset_cache: Arc, @@ -54,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, @@ -66,15 +68,18 @@ impl Presenter { 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); @@ -91,14 +96,20 @@ impl Presenter { .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 @@ -111,6 +122,7 @@ impl Presenter { .clicked_button .map(|button| (self.clicked_region_ids.clone(), button)), refreshing: true, + appearance, }) .unwrap(); } @@ -177,6 +189,7 @@ impl Presenter { .clicked_button .map(|button| (self.clicked_region_ids.clone(), button)), titlebar_height: self.titlebar_height, + appearance: self.appearance, window_size, app: cx, } @@ -550,6 +563,7 @@ pub struct LayoutContext<'a> { pub refreshing: bool, pub window_size: Vector2F, titlebar_height: f32, + appearance: Appearance, hovered_region_ids: HashSet, clicked_region_ids: Option<(HashSet, MouseButton)>, } @@ -624,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) }) diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index e287744cf2b17022a0739481194e080b619f983a..e676147fa90e72d0f9f36cd02e2356423f310be5 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -12,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::*; @@ -38,6 +38,7 @@ pub struct Layer { quads: Vec, underlines: Vec, images: Vec, + surfaces: Vec, shadows: Vec, glyphs: Vec, image_glyphs: Vec, @@ -272,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) } @@ -352,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(), @@ -420,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); diff --git a/crates/live_kit/Cargo.toml b/crates/live_kit/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..e88d4f7b24ccd21f9dd24480e031e01ca6078c31 --- /dev/null +++ b/crates/live_kit/Cargo.toml @@ -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"] } diff --git a/crates/live_kit/LiveKitBridge/.gitignore b/crates/live_kit/LiveKitBridge/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3b29812086f28a2b21884e57ead495ffd9434178 --- /dev/null +++ b/crates/live_kit/LiveKitBridge/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/crates/live_kit/LiveKitBridge/Package.resolved b/crates/live_kit/LiveKitBridge/Package.resolved new file mode 100644 index 0000000000000000000000000000000000000000..b19e2980a493bd24b9f94f4811bdda08fa4f4a40 --- /dev/null +++ b/crates/live_kit/LiveKitBridge/Package.resolved @@ -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 +} diff --git a/crates/live_kit/LiveKitBridge/Package.swift b/crates/live_kit/LiveKitBridge/Package.swift new file mode 100644 index 0000000000000000000000000000000000000000..76e528bda98a2c9495256d2a2f57e612d4480f21 --- /dev/null +++ b/crates/live_kit/LiveKitBridge/Package.swift @@ -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")]), + ] +) diff --git a/crates/live_kit/LiveKitBridge/README.md b/crates/live_kit/LiveKitBridge/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b982c672866a3748a40a8b2a40795b1d2b048196 --- /dev/null +++ b/crates/live_kit/LiveKitBridge/README.md @@ -0,0 +1,3 @@ +# LiveKitBridge + +A description of this package. diff --git a/crates/live_kit/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift b/crates/live_kit/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift new file mode 100644 index 0000000000000000000000000000000000000000..f59b82920376e3ab9ffb11aa11ed50f151305079 --- /dev/null +++ b/crates/live_kit/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift @@ -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.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.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.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.fromOpaque(room).takeUnretainedValue() + let track = Unmanaged.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.fromOpaque(track).takeUnretainedValue() as! VideoTrack + let renderer = Unmanaged.fromOpaque(renderer).takeRetainedValue() + track.add(videoRenderer: renderer) +} diff --git a/crates/live_kit/build.rs b/crates/live_kit/build.rs new file mode 100644 index 0000000000000000000000000000000000000000..79d7d84cdd96c63381e4ec9b29fb30e28b788e05 --- /dev/null +++ b/crates/live_kit/build.rs @@ -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, + pub runtime_library_import_paths: Vec, + 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()) + } +} diff --git a/crates/live_kit/src/live_kit.rs b/crates/live_kit/src/live_kit.rs new file mode 100644 index 0000000000000000000000000000000000000000..59ce860a7825108ac99aa8296e29fbd1a9876f14 --- /dev/null +++ b/crates/live_kit/src/live_kit.rs @@ -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>>>, + _delegate: RoomDelegate, +} + +impl Room { + pub fn new() -> Arc { + 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> { + 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> { + 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> { + 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>, + ) { + 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>) }; + 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) -> 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(&self, callback: F) + where + F: 'static + FnMut(CVImageBuffer), + { + extern "C" fn on_frame(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(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::, on_drop::); + 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, +} + +pub fn list_windows() -> Vec { + unsafe { + let dicts = CFArray::::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() + } +} diff --git a/crates/media/Cargo.toml b/crates/media/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..aad2b74c021273afa9abef6479ecb1fc9ed3829a --- /dev/null +++ b/crates/media/Cargo.toml @@ -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" diff --git a/crates/media/build.rs b/crates/media/build.rs new file mode 100644 index 0000000000000000000000000000000000000000..1ee8f23e41e97c9e9f24fd1eb163242256c1407c --- /dev/null +++ b/crates/media/build.rs @@ -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"); +} diff --git a/crates/media/src/bindings.h b/crates/media/src/bindings.h new file mode 100644 index 0000000000000000000000000000000000000000..4df283d0c199669b0ccecf9bd59254bdcd3c2ec3 --- /dev/null +++ b/crates/media/src/bindings.h @@ -0,0 +1,5 @@ +#import +#import +#import +#import +#import diff --git a/crates/media/src/bindings.rs b/crates/media/src/bindings.rs new file mode 100644 index 0000000000000000000000000000000000000000..a1c0b0da3ef5175063e48cd13d9b37d33c6369b3 --- /dev/null +++ b/crates/media/src/bindings.rs @@ -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")); diff --git a/crates/media/src/media.rs b/crates/media/src/media.rs new file mode 100644 index 0000000000000000000000000000000000000000..fe69e684e70df437573ecd60a17e29b984291d6d --- /dev/null +++ b/crates/media/src/media.rs @@ -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 { + 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 { + 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> { + unsafe { + let attachments = + CMSampleBufferGetSampleAttachmentsArray(self.as_concrete_TypeRef(), true); + CFArray::::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 { + 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 { + 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; + } +} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 657e3c59150ddedf15cdc704be5ac94d9a342b2d..4d692082a775172b3d19e4026952ca8a3e8bfce4 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -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, @@ -314,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)] diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6d2a617d4c81c3f7db5905b546e24df2287f9771..446139eaafbd25c8f4c4374b0f6538ee44586b58 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -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" } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index fcbd88b74f31256643c98adf2d30c9d9cb0ac458..cd906500eef218db07642db27bc5ea84bae22e90 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -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; @@ -331,9 +331,14 @@ pub fn initialize_workspace( pub fn build_window_options() -> WindowOptions<'static> { WindowOptions { bounds: WindowBounds::Maximized, - title: None, - titlebar_appears_transparent: true, - traffic_light_position: Some(vec2f(8., 8.)), + titlebar: Some(TitlebarOptions { + title: None, + appears_transparent: true, + traffic_light_position: Some(vec2f(8., 8.)), + }), + center: false, + kind: WindowKind::Normal, + is_movable: true, } } diff --git a/styles/package-lock.json b/styles/package-lock.json index 5499f1852cb4330467268dee6436b53589a90e9b..582f1c84968a5c1a25ddac5fd3c21ba907353c6d 100644 --- a/styles/package-lock.json +++ b/styles/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "styles", "version": "1.0.0", "license": "ISC", "dependencies": { diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 1345d4e8554658ea9fe1686884f7b9e98b271db1..a3ab4b654c6f6d2835804c834dab6a7435cd75f3 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -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), diff --git a/styles/src/styleTree/contactsPopover.ts b/styles/src/styleTree/contactsPopover.ts new file mode 100644 index 0000000000000000000000000000000000000000..e9de5dddaf627120d12ab452137fbd47508771a5 --- /dev/null +++ b/styles/src/styleTree/contactsPopover.ts @@ -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), + } +}