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