From 37d0f06e07147e4d995ffc3701f2ee8f717a90c2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 19 Sep 2023 20:55:13 -0600 Subject: [PATCH] Checkpoint --- Cargo.lock | 42 +- crates/gpui3/Cargo.toml | 79 +- crates/gpui3/build.rs | 24 + crates/gpui3/src/app.rs | 4 +- crates/gpui3/src/executor.rs | 1030 +++++++++++ crates/gpui3/src/fonts.rs | 6 +- crates/gpui3/src/geometry.rs | 21 +- crates/gpui3/src/gpui3.rs | 22 +- crates/gpui3/src/platform.rs | 111 +- crates/gpui3/src/platform/events.rs | 204 +++ crates/gpui3/src/platform/keystroke.rs | 121 ++ crates/gpui3/src/platform/mac.rs | 144 ++ crates/gpui3/src/platform/mac/dispatch.h | 1 + crates/gpui3/src/platform/mac/dispatcher.rs | 42 + crates/gpui3/src/platform/mac/events.rs | 354 ++++ crates/gpui3/src/platform/mac/platform.rs | 1102 +++++++++++ crates/gpui3/src/platform/mac/screen.rs | 145 ++ crates/gpui3/src/platform/mac/window.rs | 1619 +++++++++++++++++ .../src/platform/mac/window_appearence.rs | 35 + crates/gpui3/src/platform/test.rs | 6 +- crates/gpui3/src/style.rs | 2 +- crates/gpui3/src/text.rs | 10 +- crates/gpui3/src/util.rs | 49 + crates/gpui3/src/window.rs | 9 +- 24 files changed, 5133 insertions(+), 49 deletions(-) create mode 100644 crates/gpui3/build.rs create mode 100644 crates/gpui3/src/executor.rs create mode 100644 crates/gpui3/src/platform/events.rs create mode 100644 crates/gpui3/src/platform/keystroke.rs create mode 100644 crates/gpui3/src/platform/mac.rs create mode 100644 crates/gpui3/src/platform/mac/dispatch.h create mode 100644 crates/gpui3/src/platform/mac/dispatcher.rs create mode 100644 crates/gpui3/src/platform/mac/events.rs create mode 100644 crates/gpui3/src/platform/mac/platform.rs create mode 100644 crates/gpui3/src/platform/mac/screen.rs create mode 100644 crates/gpui3/src/platform/mac/window.rs create mode 100644 crates/gpui3/src/platform/mac/window_appearence.rs create mode 100644 crates/gpui3/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 2980dcd1f0401f794e88a9c4262db50039663ccb..5ea28ebacd7971d9a7dd6865363e7a9c6ae01693 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3345,23 +3345,61 @@ name = "gpui3" version = "0.1.0" dependencies = [ "anyhow", + "async-task", + "backtrace", + "bindgen 0.65.1", + "block", "bytemuck", + "cocoa", + "collections", + "core-foundation", + "core-graphics", + "core-text", + "ctor", "derive_more", + "dhat", + "env_logger 0.9.3", + "etagere", "font-kit", - "itertools 0.11.0", + "foreign-types 0.3.2", + "futures 0.3.28", + "gpui_macros", + "image", + "itertools 0.10.5", + "lazy_static", "log", + "media", + "num_cpus", + "objc", + "ordered-float", + "parking", "parking_lot 0.11.2", "plane-split", + "png", + "postage", + "rand 0.8.5", "raw-window-handle", "refineable", - "rust-embed", + "resvg", "schemars", + "seahash", "serde", + "serde_derive", + "serde_json", "simplelog", "slotmap", "smallvec", + "smol", + "sqlez", + "sum_tree", "taffy", + "thiserror", + "time 0.3.27", + "tiny-skia", + "usvg", "util", + "uuid 1.4.1", + "waker-fn", "wgpu", ] diff --git a/crates/gpui3/Cargo.toml b/crates/gpui3/Cargo.toml index f0a8debee290b77ea7ca4841a1041e1a8647abfc..7d7450c0960080b92035aa7cd7bc7a7436a1658c 100644 --- a/crates/gpui3/Cargo.toml +++ b/crates/gpui3/Cargo.toml @@ -2,30 +2,83 @@ name = "gpui3" version = "0.1.0" edition = "2021" +authors = ["Nathan Sobo "] +description = "The next version of Zed's GPU-accelerated UI framework" +publish = false [features] -test = [] +test-support = ["backtrace", "dhat", "env_logger", "collections/test-support"] [lib] path = "src/gpui3.rs" +doctest = false [dependencies] -anyhow.workspace = true -bytemuck = "1.14.0" +collections = { path = "../collections" } +gpui_macros = { path = "../gpui_macros" } +util = { path = "../util" } +sum_tree = { path = "../sum_tree" } +sqlez = { path = "../sqlez" } +async-task = "4.0.3" +backtrace = { version = "0.3", optional = true } +ctor.workspace = true derive_more.workspace = true -font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18" } -itertools = "0.11.0" +dhat = { version = "0.3", optional = true } +env_logger = { version = "0.9", optional = true } +etagere = "0.2" +futures.workspace = true +image = "0.23" +itertools = "0.10" +lazy_static.workspace = true log.workspace = true +num_cpus = "1.13" +ordered-float.workspace = true +parking = "2.0.0" parking_lot.workspace = true -plane-split = "0.18.0" -raw-window-handle = "0.5.2" -refineable = { path = "../refineable" } -rust-embed.workspace = true -schemars = "0.8" +postage.workspace = true +rand.workspace = true +refineable.workspace = true +resvg = "0.14" +seahash = "4.1" serde.workspace = true -simplelog = "0.9" -slotmap = "1.0.6" +serde_derive.workspace = true +serde_json.workspace = true smallvec.workspace = true +smol.workspace = true taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e" } -util = { path = "../util" } +thiserror.workspace = true +time.workspace = true +tiny-skia = "0.5" +usvg = { version = "0.14", features = [] } +uuid = { version = "1.1.2", features = ["v4"] } +waker-fn = "1.1.0" +slotmap = "1.0.6" +bytemuck = "1.14.0" +schemars.workspace = true +raw-window-handle = "0.5.2" wgpu = "0.17.0" +plane-split = "0.18.0" + +[dev-dependencies] +backtrace = "0.3" +collections = { path = "../collections", features = ["test-support"] } +dhat = "0.3" +env_logger.workspace = true +png = "0.16" +simplelog = "0.9" + +[build-dependencies] +bindgen = "0.65.1" + +[target.'cfg(target_os = "macos")'.dependencies] +media = { path = "../media" } +anyhow.workspace = true +block = "0.1" +cocoa = "0.24" +core-foundation = { version = "0.9.3", features = ["with-uuid"] } +core-graphics = "0.22.3" +core-text = "19.2" +font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18" } +foreign-types = "0.3" +log.workspace = true +objc = "0.2" diff --git a/crates/gpui3/build.rs b/crates/gpui3/build.rs new file mode 100644 index 0000000000000000000000000000000000000000..0aa3ab251ec34624a8ceb316540c3cc404330daa --- /dev/null +++ b/crates/gpui3/build.rs @@ -0,0 +1,24 @@ +use std::{env, path::PathBuf}; + +fn main() { + generate_dispatch_bindings(); +} + +fn generate_dispatch_bindings() { + println!("cargo:rustc-link-lib=framework=System"); + println!("cargo:rerun-if-changed=src/platform/mac/dispatch.h"); + + let bindings = bindgen::Builder::default() + .header("src/platform/mac/dispatch.h") + .allowlist_var("_dispatch_main_q") + .allowlist_function("dispatch_async_f") + .parse_callbacks(Box::new(bindgen::CargoCallbacks)) + .layout_tests(false) + .generate() + .expect("unable to generate bindings"); + + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("dispatch_sys.rs")) + .expect("couldn't write dispatch bindings"); +} diff --git a/crates/gpui3/src/app.rs b/crates/gpui3/src/app.rs index b826b165ee041dfa9c1140a4abe810bbb2029075..a80564423d2cc21c883c5cb3db5c39495e2a269d 100644 --- a/crates/gpui3/src/app.rs +++ b/crates/gpui3/src/app.rs @@ -51,9 +51,9 @@ impl AppContext { ) -> WindowHandle { let id = self.windows.insert(None); let handle = WindowHandle::new(id); - self.platform.open_window(handle.into(), options); + let platform_window = self.platform.open_window(handle.into(), options); - let mut window = Window::new(id); + let mut window = Window::new(id, platform_window); let root_view = build_root_view(&mut WindowContext::mutable(self, &mut window)); window.root_view.replace(Box::new(root_view)); diff --git a/crates/gpui3/src/executor.rs b/crates/gpui3/src/executor.rs new file mode 100644 index 0000000000000000000000000000000000000000..f465bd3c3dadf3fcf245a71870cd11ea3cffe321 --- /dev/null +++ b/crates/gpui3/src/executor.rs @@ -0,0 +1,1030 @@ +use crate::util; +use anyhow::{anyhow, Result}; +use async_task::Runnable; +use futures::channel::mpsc; +use smol::{channel, prelude::*, Executor}; +use std::{ + any::Any, + fmt::{self}, + marker::PhantomData, + mem, + pin::Pin, + rc::Rc, + sync::Arc, + task::{Context, Poll}, + thread, + time::Duration, +}; + +use crate::PlatformDispatcher; + +pub enum ForegroundExecutor { + Platform { + dispatcher: Arc, + _not_send_or_sync: PhantomData>, + }, + #[cfg(any(test, feature = "test"))] + Deterministic { + cx_id: usize, + executor: Arc, + }, +} + +pub enum BackgroundExecutor { + #[cfg(any(test, feature = "test"))] + Deterministic { executor: Arc }, + Production { + executor: Arc>, + _stop: channel::Sender<()>, + }, +} + +type AnyLocalFuture = Pin>>>; +type AnyFuture = Pin>>>; +type AnyTask = async_task::Task>; +type AnyLocalTask = async_task::Task>; + +#[must_use] +pub enum Task { + Ready(Option), + Local { + any_task: AnyLocalTask, + result_type: PhantomData, + }, + Send { + any_task: AnyTask, + result_type: PhantomData, + }, +} + +unsafe impl Send for Task {} + +#[cfg(any(test, feature = "test"))] +struct DeterministicState { + rng: rand::prelude::StdRng, + seed: u64, + scheduled_from_foreground: collections::HashMap>, + scheduled_from_background: Vec, + forbid_parking: bool, + block_on_ticks: std::ops::RangeInclusive, + now: std::time::Instant, + next_timer_id: usize, + pending_timers: Vec<(usize, std::time::Instant, postage::barrier::Sender)>, + waiting_backtrace: Option, + next_runnable_id: usize, + poll_history: Vec, + previous_poll_history: Option>, + enable_runnable_backtraces: bool, + runnable_backtraces: collections::HashMap, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ExecutorEvent { + PollRunnable { id: usize }, + EnqueuRunnable { id: usize }, +} + +#[cfg(any(test, feature = "test"))] +struct ForegroundRunnable { + id: usize, + runnable: Runnable, + main: bool, +} + +#[cfg(any(test, feature = "test"))] +struct BackgroundRunnable { + id: usize, + runnable: Runnable, +} + +#[cfg(any(test, feature = "test"))] +pub struct Deterministic { + state: Arc>, + parker: parking_lot::Mutex, +} + +#[must_use] +pub enum Timer { + Production(smol::Timer), + #[cfg(any(test, feature = "test"))] + Deterministic(DeterministicTimer), +} + +#[cfg(any(test, feature = "test"))] +pub struct DeterministicTimer { + rx: postage::barrier::Receiver, + id: usize, + state: Arc>, +} + +#[cfg(any(test, feature = "test"))] +impl Deterministic { + pub fn new(seed: u64) -> Arc { + use rand::prelude::*; + + Arc::new(Self { + state: Arc::new(parking_lot::Mutex::new(DeterministicState { + rng: StdRng::seed_from_u64(seed), + seed, + scheduled_from_foreground: Default::default(), + scheduled_from_background: Default::default(), + forbid_parking: false, + block_on_ticks: 0..=1000, + now: std::time::Instant::now(), + next_timer_id: Default::default(), + pending_timers: Default::default(), + waiting_backtrace: None, + next_runnable_id: 0, + poll_history: Default::default(), + previous_poll_history: Default::default(), + enable_runnable_backtraces: false, + runnable_backtraces: Default::default(), + })), + parker: Default::default(), + }) + } + + pub fn execution_history(&self) -> Vec { + self.state.lock().poll_history.clone() + } + + pub fn set_previous_execution_history(&self, history: Option>) { + self.state.lock().previous_poll_history = history; + } + + pub fn enable_runnable_backtrace(&self) { + self.state.lock().enable_runnable_backtraces = true; + } + + pub fn runnable_backtrace(&self, runnable_id: usize) -> backtrace::Backtrace { + let mut backtrace = self.state.lock().runnable_backtraces[&runnable_id].clone(); + backtrace.resolve(); + backtrace + } + + pub fn build_background(self: &Arc) -> Arc { + Arc::new(BackgroundExecutor::Deterministic { + executor: self.clone(), + }) + } + + pub fn build_foreground(self: &Arc, id: usize) -> Rc { + Rc::new(ForegroundExecutor::Deterministic { + cx_id: id, + executor: self.clone(), + }) + } + + fn spawn_from_foreground( + &self, + cx_id: usize, + future: AnyLocalFuture, + main: bool, + ) -> AnyLocalTask { + let state = self.state.clone(); + let id; + { + let mut state = state.lock(); + id = util::post_inc(&mut state.next_runnable_id); + if state.enable_runnable_backtraces { + state + .runnable_backtraces + .insert(id, backtrace::Backtrace::new_unresolved()); + } + } + + let unparker = self.parker.lock().unparker(); + let (runnable, task) = async_task::spawn_local(future, move |runnable| { + let mut state = state.lock(); + state.push_to_history(ExecutorEvent::EnqueuRunnable { id }); + state + .scheduled_from_foreground + .entry(cx_id) + .or_default() + .push(ForegroundRunnable { id, runnable, main }); + unparker.unpark(); + }); + runnable.schedule(); + task + } + + fn spawn(&self, future: AnyFuture) -> AnyTask { + let state = self.state.clone(); + let id; + { + let mut state = state.lock(); + id = util::post_inc(&mut state.next_runnable_id); + if state.enable_runnable_backtraces { + state + .runnable_backtraces + .insert(id, backtrace::Backtrace::new_unresolved()); + } + } + + let unparker = self.parker.lock().unparker(); + let (runnable, task) = async_task::spawn(future, move |runnable| { + let mut state = state.lock(); + state + .poll_history + .push(ExecutorEvent::EnqueuRunnable { id }); + state + .scheduled_from_background + .push(BackgroundRunnable { id, runnable }); + unparker.unpark(); + }); + runnable.schedule(); + task + } + + fn run<'a>( + &self, + cx_id: usize, + main_future: Pin>>>, + ) -> Box { + use std::sync::atomic::{AtomicBool, Ordering::SeqCst}; + + let woken = Arc::new(AtomicBool::new(false)); + + let state = self.state.clone(); + let id; + { + let mut state = state.lock(); + id = util::post_inc(&mut state.next_runnable_id); + if state.enable_runnable_backtraces { + state + .runnable_backtraces + .insert(id, backtrace::Backtrace::new_unresolved()); + } + } + + let unparker = self.parker.lock().unparker(); + let (runnable, mut main_task) = unsafe { + async_task::spawn_unchecked(main_future, move |runnable| { + let state = &mut *state.lock(); + state + .scheduled_from_foreground + .entry(cx_id) + .or_default() + .push(ForegroundRunnable { + id: util::post_inc(&mut state.next_runnable_id), + runnable, + main: true, + }); + unparker.unpark(); + }) + }; + runnable.schedule(); + + loop { + if let Some(result) = self.run_internal(woken.clone(), Some(&mut main_task)) { + return result; + } + + if !woken.load(SeqCst) { + self.state.lock().will_park(); + } + + woken.store(false, SeqCst); + self.parker.lock().park(); + } + } + + pub fn run_until_parked(&self) { + use std::sync::atomic::AtomicBool; + let woken = Arc::new(AtomicBool::new(false)); + self.run_internal(woken, None); + } + + fn run_internal( + &self, + woken: Arc, + mut main_task: Option<&mut AnyLocalTask>, + ) -> Option> { + use rand::prelude::*; + use std::sync::atomic::Ordering::SeqCst; + + let unparker = self.parker.lock().unparker(); + let waker = waker_fn::waker_fn(move || { + woken.store(true, SeqCst); + unparker.unpark(); + }); + + let mut cx = Context::from_waker(&waker); + loop { + let mut state = self.state.lock(); + + if state.scheduled_from_foreground.is_empty() + && state.scheduled_from_background.is_empty() + { + if let Some(main_task) = main_task { + if let Poll::Ready(result) = main_task.poll(&mut cx) { + return Some(result); + } + } + + return None; + } + + if !state.scheduled_from_background.is_empty() && state.rng.gen() { + let background_len = state.scheduled_from_background.len(); + let ix = state.rng.gen_range(0..background_len); + let background_runnable = state.scheduled_from_background.remove(ix); + state.push_to_history(ExecutorEvent::PollRunnable { + id: background_runnable.id, + }); + drop(state); + background_runnable.runnable.run(); + } else if !state.scheduled_from_foreground.is_empty() { + let available_cx_ids = state + .scheduled_from_foreground + .keys() + .copied() + .collect::>(); + let cx_id_to_run = *available_cx_ids.iter().choose(&mut state.rng).unwrap(); + let scheduled_from_cx = state + .scheduled_from_foreground + .get_mut(&cx_id_to_run) + .unwrap(); + let foreground_runnable = scheduled_from_cx.remove(0); + if scheduled_from_cx.is_empty() { + state.scheduled_from_foreground.remove(&cx_id_to_run); + } + state.push_to_history(ExecutorEvent::PollRunnable { + id: foreground_runnable.id, + }); + + drop(state); + + foreground_runnable.runnable.run(); + if let Some(main_task) = main_task.as_mut() { + if foreground_runnable.main { + if let Poll::Ready(result) = main_task.poll(&mut cx) { + return Some(result); + } + } + } + } + } + } + + fn block(&self, future: &mut F, max_ticks: usize) -> Option + where + F: Unpin + Future, + { + use rand::prelude::*; + + let unparker = self.parker.lock().unparker(); + let waker = waker_fn::waker_fn(move || { + unparker.unpark(); + }); + + let mut cx = Context::from_waker(&waker); + for _ in 0..max_ticks { + let mut state = self.state.lock(); + let runnable_count = state.scheduled_from_background.len(); + let ix = state.rng.gen_range(0..=runnable_count); + if ix < state.scheduled_from_background.len() { + let background_runnable = state.scheduled_from_background.remove(ix); + state.push_to_history(ExecutorEvent::PollRunnable { + id: background_runnable.id, + }); + drop(state); + background_runnable.runnable.run(); + } else { + drop(state); + if let Poll::Ready(result) = future.poll(&mut cx) { + return Some(result); + } + let mut state = self.state.lock(); + if state.scheduled_from_background.is_empty() { + state.will_park(); + drop(state); + self.parker.lock().park(); + } + + continue; + } + } + + None + } + + pub fn timer(&self, duration: Duration) -> Timer { + let (tx, rx) = postage::barrier::channel(); + let mut state = self.state.lock(); + let wakeup_at = state.now + duration; + let id = util::post_inc(&mut state.next_timer_id); + match state + .pending_timers + .binary_search_by_key(&wakeup_at, |e| e.1) + { + Ok(ix) | Err(ix) => state.pending_timers.insert(ix, (id, wakeup_at, tx)), + } + let state = self.state.clone(); + Timer::Deterministic(DeterministicTimer { rx, id, state }) + } + + pub fn now(&self) -> std::time::Instant { + let state = self.state.lock(); + state.now + } + + pub fn advance_clock(&self, duration: Duration) { + let new_now = self.state.lock().now + duration; + loop { + self.run_until_parked(); + let mut state = self.state.lock(); + + if let Some((_, wakeup_time, _)) = state.pending_timers.first() { + let wakeup_time = *wakeup_time; + if wakeup_time <= new_now { + let timer_count = state + .pending_timers + .iter() + .take_while(|(_, t, _)| *t == wakeup_time) + .count(); + state.now = wakeup_time; + let timers_to_wake = state + .pending_timers + .drain(0..timer_count) + .collect::>(); + drop(state); + drop(timers_to_wake); + continue; + } + } + + break; + } + + self.state.lock().now = new_now; + } + + pub fn start_waiting(&self) { + self.state.lock().waiting_backtrace = Some(backtrace::Backtrace::new_unresolved()); + } + + pub fn finish_waiting(&self) { + self.state.lock().waiting_backtrace.take(); + } + + pub fn forbid_parking(&self) { + use rand::prelude::*; + + let mut state = self.state.lock(); + state.forbid_parking = true; + state.rng = StdRng::seed_from_u64(state.seed); + } + + pub fn allow_parking(&self) { + use rand::prelude::*; + + let mut state = self.state.lock(); + state.forbid_parking = false; + state.rng = StdRng::seed_from_u64(state.seed); + } + + pub async fn simulate_random_delay(&self) { + use rand::prelude::*; + use smol::future::yield_now; + if self.state.lock().rng.gen_bool(0.2) { + let yields = self.state.lock().rng.gen_range(1..=10); + for _ in 0..yields { + yield_now().await; + } + } + } + + pub fn record_backtrace(&self) { + let mut state = self.state.lock(); + if state.enable_runnable_backtraces { + let current_id = state + .poll_history + .iter() + .rev() + .find_map(|event| match event { + ExecutorEvent::PollRunnable { id } => Some(*id), + _ => None, + }); + if let Some(id) = current_id { + state + .runnable_backtraces + .insert(id, backtrace::Backtrace::new_unresolved()); + } + } + } +} + +impl Drop for Timer { + fn drop(&mut self) { + #[cfg(any(test, feature = "test"))] + if let Timer::Deterministic(DeterministicTimer { state, id, .. }) = self { + state + .lock() + .pending_timers + .retain(|(timer_id, _, _)| timer_id != id) + } + } +} + +impl Future for Timer { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match &mut *self { + #[cfg(any(test, feature = "test"))] + Self::Deterministic(DeterministicTimer { rx, .. }) => { + use postage::stream::{PollRecv, Stream as _}; + smol::pin!(rx); + match rx.poll_recv(&mut postage::Context::from_waker(cx.waker())) { + PollRecv::Ready(()) | PollRecv::Closed => Poll::Ready(()), + PollRecv::Pending => Poll::Pending, + } + } + Self::Production(timer) => { + smol::pin!(timer); + match timer.poll(cx) { + Poll::Ready(_) => Poll::Ready(()), + Poll::Pending => Poll::Pending, + } + } + } + } +} + +#[cfg(any(test, feature = "test"))] +impl DeterministicState { + fn push_to_history(&mut self, event: ExecutorEvent) { + use std::fmt::Write as _; + + self.poll_history.push(event); + if let Some(prev_history) = &self.previous_poll_history { + let ix = self.poll_history.len() - 1; + let prev_event = prev_history[ix]; + if event != prev_event { + let mut message = String::new(); + writeln!( + &mut message, + "current runnable backtrace:\n{:?}", + self.runnable_backtraces.get_mut(&event.id()).map(|trace| { + trace.resolve(); + crate::util::CwdBacktrace(trace) + }) + ) + .unwrap(); + writeln!( + &mut message, + "previous runnable backtrace:\n{:?}", + self.runnable_backtraces + .get_mut(&prev_event.id()) + .map(|trace| { + trace.resolve(); + util::CwdBacktrace(trace) + }) + ) + .unwrap(); + panic!("detected non-determinism after {ix}. {message}"); + } + } + } + + fn will_park(&mut self) { + if self.forbid_parking { + let mut backtrace_message = String::new(); + #[cfg(any(test, feature = "test"))] + if let Some(backtrace) = self.waiting_backtrace.as_mut() { + backtrace.resolve(); + backtrace_message = format!( + "\nbacktrace of waiting future:\n{:?}", + util::CwdBacktrace(backtrace) + ); + } + + panic!( + "deterministic executor parked after a call to forbid_parking{}", + backtrace_message + ); + } + } +} + +#[cfg(any(test, feature = "test"))] +impl ExecutorEvent { + pub fn id(&self) -> usize { + match self { + ExecutorEvent::PollRunnable { id } => *id, + ExecutorEvent::EnqueuRunnable { id } => *id, + } + } +} + +impl ForegroundExecutor { + pub fn platform(dispatcher: Arc) -> Result { + if dispatcher.is_main_thread() { + Ok(Self::Platform { + dispatcher, + _not_send_or_sync: PhantomData, + }) + } else { + Err(anyhow!("must be constructed on main thread")) + } + } + + pub fn spawn(&self, future: impl Future + 'static) -> Task { + let future = any_local_future(future); + let any_task = match self { + #[cfg(any(test, feature = "test"))] + Self::Deterministic { cx_id, executor } => { + executor.spawn_from_foreground(*cx_id, future, false) + } + Self::Platform { dispatcher, .. } => { + fn spawn_inner( + future: AnyLocalFuture, + dispatcher: &Arc, + ) -> AnyLocalTask { + let dispatcher = dispatcher.clone(); + let schedule = + move |runnable: Runnable| dispatcher.run_on_main_thread(runnable); + let (runnable, task) = async_task::spawn_local(future, schedule); + runnable.schedule(); + task + } + spawn_inner(future, dispatcher) + } + }; + Task::local(any_task) + } + + #[cfg(any(test, feature = "test"))] + pub fn run(&self, future: impl Future) -> T { + let future = async move { Box::new(future.await) as Box }.boxed_local(); + let result = match self { + Self::Deterministic { cx_id, executor } => executor.run(*cx_id, future), + Self::Platform { .. } => panic!("you can't call run on a platform foreground executor"), + }; + *result.downcast().unwrap() + } + + #[cfg(any(test, feature = "test"))] + pub fn run_until_parked(&self) { + match self { + Self::Deterministic { executor, .. } => executor.run_until_parked(), + _ => panic!("this method can only be called on a deterministic executor"), + } + } + + #[cfg(any(test, feature = "test"))] + pub fn parking_forbidden(&self) -> bool { + match self { + Self::Deterministic { executor, .. } => executor.state.lock().forbid_parking, + _ => panic!("this method can only be called on a deterministic executor"), + } + } + + #[cfg(any(test, feature = "test"))] + pub fn start_waiting(&self) { + match self { + Self::Deterministic { executor, .. } => executor.start_waiting(), + _ => panic!("this method can only be called on a deterministic executor"), + } + } + + #[cfg(any(test, feature = "test"))] + pub fn finish_waiting(&self) { + match self { + Self::Deterministic { executor, .. } => executor.finish_waiting(), + _ => panic!("this method can only be called on a deterministic executor"), + } + } + + #[cfg(any(test, feature = "test"))] + pub fn forbid_parking(&self) { + match self { + Self::Deterministic { executor, .. } => executor.forbid_parking(), + _ => panic!("this method can only be called on a deterministic executor"), + } + } + + #[cfg(any(test, feature = "test"))] + pub fn allow_parking(&self) { + match self { + Self::Deterministic { executor, .. } => executor.allow_parking(), + _ => panic!("this method can only be called on a deterministic executor"), + } + } + + #[cfg(any(test, feature = "test"))] + pub fn advance_clock(&self, duration: Duration) { + match self { + Self::Deterministic { executor, .. } => executor.advance_clock(duration), + _ => panic!("this method can only be called on a deterministic executor"), + } + } + + #[cfg(any(test, feature = "test"))] + pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive) { + match self { + Self::Deterministic { executor, .. } => executor.state.lock().block_on_ticks = range, + _ => panic!("this method can only be called on a deterministic executor"), + } + } +} + +impl BackgroundExecutor { + pub fn new() -> Self { + let executor = Arc::new(Executor::new()); + let stop = channel::unbounded::<()>(); + + for i in 0..2 * num_cpus::get() { + let executor = executor.clone(); + let stop = stop.1.clone(); + thread::Builder::new() + .name(format!("background-executor-{}", i)) + .spawn(move || smol::block_on(executor.run(stop.recv()))) + .unwrap(); + } + + Self::Production { + executor, + _stop: stop.0, + } + } + + pub fn num_cpus(&self) -> usize { + num_cpus::get() + } + + pub fn spawn(&self, future: F) -> Task + where + T: 'static + Send, + F: Send + Future + 'static, + { + let future = any_future(future); + let any_task = match self { + Self::Production { executor, .. } => executor.spawn(future), + #[cfg(any(test, feature = "test"))] + Self::Deterministic { executor } => executor.spawn(future), + }; + Task::send(any_task) + } + + pub fn block(&self, future: F) -> T + where + F: Future, + { + smol::pin!(future); + match self { + Self::Production { .. } => smol::block_on(&mut future), + #[cfg(any(test, feature = "test"))] + Self::Deterministic { executor, .. } => { + executor.block(&mut future, usize::MAX).unwrap() + } + } + } + + pub fn block_with_timeout( + &self, + timeout: Duration, + future: F, + ) -> Result> + where + T: 'static, + F: 'static + Unpin + Future, + { + let mut future = any_local_future(future); + if !timeout.is_zero() { + let output = match self { + Self::Production { .. } => smol::block_on(util::timeout(timeout, &mut future)).ok(), + #[cfg(any(test, feature = "test"))] + Self::Deterministic { executor, .. } => { + use rand::prelude::*; + let max_ticks = { + let mut state = executor.state.lock(); + let range = state.block_on_ticks.clone(); + state.rng.gen_range(range) + }; + executor.block(&mut future, max_ticks) + } + }; + if let Some(output) = output { + return Ok(*output.downcast().unwrap()); + } + } + Err(async { *future.await.downcast().unwrap() }) + } + + pub async fn scoped<'scope, F>(self: &Arc, scheduler: F) + where + F: FnOnce(&mut Scope<'scope>), + { + let mut scope = Scope::new(self.clone()); + (scheduler)(&mut scope); + let spawned = mem::take(&mut scope.futures) + .into_iter() + .map(|f| self.spawn(f)) + .collect::>(); + for task in spawned { + task.await; + } + } + + pub fn timer(&self, duration: Duration) -> Timer { + match self { + BackgroundExecutor::Production { .. } => { + Timer::Production(smol::Timer::after(duration)) + } + #[cfg(any(test, feature = "test"))] + BackgroundExecutor::Deterministic { executor } => executor.timer(duration), + } + } + + pub fn now(&self) -> std::time::Instant { + match self { + BackgroundExecutor::Production { .. } => std::time::Instant::now(), + #[cfg(any(test, feature = "test"))] + BackgroundExecutor::Deterministic { executor } => executor.now(), + } + } + + #[cfg(any(test, feature = "test"))] + pub fn rng<'a>(&'a self) -> impl 'a + std::ops::DerefMut { + match self { + Self::Deterministic { executor, .. } => { + parking_lot::lock_api::MutexGuard::map(executor.state.lock(), |s| &mut s.rng) + } + _ => panic!("this method can only be called on a deterministic executor"), + } + } + + #[cfg(any(test, feature = "test"))] + pub async fn simulate_random_delay(&self) { + match self { + Self::Deterministic { executor, .. } => { + executor.simulate_random_delay().await; + } + _ => { + panic!("this method can only be called on a deterministic executor") + } + } + } + + #[cfg(any(test, feature = "test"))] + pub fn record_backtrace(&self) { + match self { + Self::Deterministic { executor, .. } => executor.record_backtrace(), + _ => { + panic!("this method can only be called on a deterministic executor") + } + } + } + + #[cfg(any(test, feature = "test"))] + pub fn start_waiting(&self) { + match self { + Self::Deterministic { executor, .. } => executor.start_waiting(), + _ => panic!("this method can only be called on a deterministic executor"), + } + } +} + +impl Default for BackgroundExecutor { + fn default() -> Self { + Self::new() + } +} + +pub struct Scope<'a> { + executor: Arc, + futures: Vec + Send + 'static>>>, + tx: Option>, + rx: mpsc::Receiver<()>, + _phantom: PhantomData<&'a ()>, +} + +impl<'a> Scope<'a> { + fn new(executor: Arc) -> Self { + let (tx, rx) = mpsc::channel(1); + Self { + executor, + tx: Some(tx), + rx, + futures: Default::default(), + _phantom: PhantomData, + } + } + + pub fn spawn(&mut self, f: F) + where + F: Future + Send + 'a, + { + let tx = self.tx.clone().unwrap(); + + // Safety: The 'a lifetime is guaranteed to outlive any of these futures because + // dropping this `Scope` blocks until all of the futures have resolved. + let f = unsafe { + mem::transmute::< + Pin + Send + 'a>>, + Pin + Send + 'static>>, + >(Box::pin(async move { + f.await; + drop(tx); + })) + }; + self.futures.push(f); + } +} + +impl<'a> Drop for Scope<'a> { + fn drop(&mut self) { + self.tx.take().unwrap(); + + // Wait until the channel is closed, which means that all of the spawned + // futures have resolved. + self.executor.block(self.rx.next()); + } +} + +impl Task { + pub fn ready(value: T) -> Self { + Self::Ready(Some(value)) + } + + fn local(any_task: AnyLocalTask) -> Self { + Self::Local { + any_task, + result_type: PhantomData, + } + } + + pub fn detach(self) { + match self { + Task::Ready(_) => {} + Task::Local { any_task, .. } => any_task.detach(), + Task::Send { any_task, .. } => any_task.detach(), + } + } +} + +// impl Task> { +// #[track_caller] +// pub fn detach_and_log_err(self, cx: &mut AppContext) { +// let caller = Location::caller(); +// cx.spawn(|_| async move { +// if let Err(err) = self.await { +// log::error!("{}:{}: {:#}", caller.file(), caller.line(), err); +// } +// }) +// .detach(); +// } +// } + +impl Task { + fn send(any_task: AnyTask) -> Self { + Self::Send { + any_task, + result_type: PhantomData, + } + } +} + +impl fmt::Debug for Task { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Task::Ready(value) => value.fmt(f), + Task::Local { any_task, .. } => any_task.fmt(f), + Task::Send { any_task, .. } => any_task.fmt(f), + } + } +} + +impl Future for Task { + type Output = T; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match unsafe { self.get_unchecked_mut() } { + Task::Ready(value) => Poll::Ready(value.take().unwrap()), + Task::Local { any_task, .. } => { + any_task.poll(cx).map(|value| *value.downcast().unwrap()) + } + Task::Send { any_task, .. } => { + any_task.poll(cx).map(|value| *value.downcast().unwrap()) + } + } + } +} + +fn any_future(future: F) -> AnyFuture +where + T: 'static + Send, + F: Future + Send + 'static, +{ + async { Box::new(future.await) as Box }.boxed() +} + +fn any_local_future(future: F) -> AnyLocalFuture +where + T: 'static, + F: Future + 'static, +{ + async { Box::new(future.await) as Box }.boxed_local() +} diff --git a/crates/gpui3/src/fonts.rs b/crates/gpui3/src/fonts.rs index 00b5d8d1c3ac149f7df4ff915d4cb504be2f6ba5..36d29c7eeac92d765358782c9a6bb65d09c062dc 100644 --- a/crates/gpui3/src/fonts.rs +++ b/crates/gpui3/src/fonts.rs @@ -1,4 +1,4 @@ -use crate::{px, Bounds, LineWrapper, Pixels, PlatformFontSystem, Result, Size}; +use crate::{px, Bounds, LineWrapper, Pixels, PlatformTextSystem, Result, Size}; use anyhow::anyhow; pub use font_kit::properties::{ Properties as FontProperties, Stretch as FontStretch, Style as FontStyle, Weight as FontWeight, @@ -79,7 +79,7 @@ struct Family { pub struct FontCache(RwLock); pub struct FontCacheState { - font_system: Arc, + font_system: Arc, families: Vec, default_family: Option, font_selections: HashMap>, @@ -90,7 +90,7 @@ pub struct FontCacheState { unsafe impl Send for FontCache {} impl FontCache { - pub fn new(fonts: Arc) -> Self { + pub fn new(fonts: Arc) -> Self { Self(RwLock::new(FontCacheState { font_system: fonts, families: Default::default(), diff --git a/crates/gpui3/src/geometry.rs b/crates/gpui3/src/geometry.rs index f95cc1fe037d628cf11b1fd3984a3b0ebca00492..ff45bcfb0a83a352f4af1978d005313c1ebc6f0c 100644 --- a/crates/gpui3/src/geometry.rs +++ b/crates/gpui3/src/geometry.rs @@ -20,6 +20,13 @@ impl Point { pub fn new(x: T, y: T) -> Self { Self { x, y } } + + pub fn map U>(&self, f: F) -> Point { + Point { + x: f(self.x.clone()), + y: f(self.y.clone()), + } + } } impl Clone for Point { @@ -42,6 +49,10 @@ pub struct Size { pub height: T, } +pub fn size(width: T, height: T) -> Size { + Size { width, height } +} + impl Size { pub fn full() -> Self { Self { @@ -157,6 +168,12 @@ impl Edges { #[repr(transparent)] pub struct Pixels(pub(crate) f32); +impl From for f64 { + fn from(pixels: Pixels) -> Self { + pixels.0.into() + } +} + impl Mul for Pixels { type Output = Pixels; @@ -326,11 +343,11 @@ pub fn relative>(fraction: f32) -> T { } pub fn rems(rems: f32) -> Rems { - Rems(rems).into() + Rems(rems) } pub fn px(pixels: f32) -> Pixels { - Pixels(pixels).into() + Pixels(pixels) } pub fn auto() -> Length { diff --git a/crates/gpui3/src/gpui3.rs b/crates/gpui3/src/gpui3.rs index 2b03f3b3dfb4bf0b3c6f8468f6b72a43c9ce0f36..d7f7f305bf2f819cd083828664b4a29983d1f4be 100644 --- a/crates/gpui3/src/gpui3.rs +++ b/crates/gpui3/src/gpui3.rs @@ -2,6 +2,7 @@ mod app; mod color; mod element; mod elements; +mod executor; mod fonts; mod geometry; mod platform; @@ -10,6 +11,7 @@ mod scene; mod style; mod taffy; mod text; +mod util; mod window; use anyhow::Result; @@ -17,10 +19,12 @@ pub use app::*; pub use color::*; pub use element::*; pub use elements::*; +pub use executor::*; pub use fonts::*; pub use geometry::*; pub use platform::*; pub use scene::*; +pub use smol::Timer; use std::ops::{Deref, DerefMut}; pub use style::*; pub use taffy::LayoutId; @@ -47,6 +51,12 @@ pub trait Context { #[derive(Clone, Eq, PartialEq)] pub struct SharedString(ArcCow<'static, str>); +impl AsRef for SharedString { + fn as_ref(&self) -> &str { + &self.0 + } +} + impl std::fmt::Debug for SharedString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) @@ -98,7 +108,7 @@ mod tests { let workspace = cx.entity(|cx| Workspace { left_panel: collab_panel(cx).into_any(), }); - view(workspace, |workspace, cx| { + view(workspace, |workspace, _cx| { div().child(workspace.left_panel.clone()) }) } @@ -109,7 +119,7 @@ mod tests { fn collab_panel(cx: &mut WindowContext) -> View { let panel = cx.entity(|cx| CollabPanel::new(cx)); - view(panel, |panel, cx| { + view(panel, |panel, _cx| { div().child(div()).child( field(panel.filter_editor.clone()).placeholder_text("Search channels, contacts"), ) @@ -124,14 +134,6 @@ mod tests { } } - struct Editor {} - - impl Editor { - pub fn new(cx: &mut ViewContext) -> Self { - Self {} - } - } - #[test] fn test() { let mut cx = AppContext::test(); diff --git a/crates/gpui3/src/platform.rs b/crates/gpui3/src/platform.rs index 1dba1a3907eb019ccf003a2c1cb93b203bffc6d7..5298a8fe4969cfb736655c670c333ea1a55ad7ff 100644 --- a/crates/gpui3/src/platform.rs +++ b/crates/gpui3/src/platform.rs @@ -1,17 +1,30 @@ +mod events; +mod keystroke; +#[cfg(target_os = "macos")] +mod mac; #[cfg(any(test, feature = "test"))] mod test; + use crate::{ AnyWindowHandle, Bounds, FontFeatures, FontId, FontMetrics, FontStyle, FontWeight, GlyphId, - LineLayout, Pixels, Point, RunStyle, SharedString, + LineLayout, Pixels, Point, RunStyle, SharedString, Size, }; +use async_task::Runnable; +use futures::channel::oneshot; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; -use std::sync::Arc; +use std::{any::Any, fmt::Debug, ops::Range, rc::Rc, sync::Arc}; +use uuid::Uuid; +pub use events::*; +pub use keystroke::*; +#[cfg(target_os = "macos")] +pub use mac::*; #[cfg(any(test, feature = "test"))] pub use test::*; pub trait Platform { - fn font_system(&self) -> Arc; + fn dispatcher(&self) -> Arc; + fn font_system(&self) -> Arc; fn open_window( &self, @@ -20,9 +33,53 @@ pub trait Platform { ) -> Box; } -pub trait PlatformWindow: HasRawWindowHandle + HasRawDisplayHandle {} +pub trait PlatformScreen: Debug { + fn as_any(&self) -> &dyn Any; + fn bounds(&self) -> Bounds; + fn content_bounds(&self) -> Bounds; + fn display_uuid(&self) -> Option; +} + +pub trait PlatformWindow: HasRawWindowHandle + HasRawDisplayHandle { + fn bounds(&self) -> WindowBounds; + fn content_size(&self) -> Size; + fn scale_factor(&self) -> f32; + fn titlebar_height(&self) -> Pixels; + fn appearance(&self) -> WindowAppearance; + fn screen(&self) -> Rc; + fn mouse_position(&self) -> Point; + fn as_any_mut(&mut self) -> &mut dyn Any; + fn set_input_handler(&mut self, input_handler: Box); + fn prompt( + &self, + level: WindowPromptLevel, + msg: &str, + answers: &[&str], + ) -> oneshot::Receiver; + fn activate(&self); + fn set_title(&mut self, title: &str); + fn set_edited(&mut self, edited: bool); + fn show_character_palette(&self); + fn minimize(&self); + fn zoom(&self); + fn toggle_full_screen(&self); + fn on_event(&mut self, callback: Box bool>); + fn on_active_status_change(&mut self, callback: Box); + fn on_resize(&mut self, callback: Box); + fn on_fullscreen(&mut self, callback: Box); + fn on_moved(&mut self, callback: Box); + fn on_should_close(&mut self, callback: Box bool>); + fn on_close(&mut self, callback: Box); + fn on_appearance_changed(&mut self, callback: Box); + fn is_topmost_for_position(&self, position: Point) -> bool; +} + +pub trait PlatformDispatcher: Send + Sync { + fn is_main_thread(&self) -> bool; + fn run_on_main_thread(&self, task: Runnable); +} -pub trait PlatformFontSystem: Send + Sync { +pub trait PlatformTextSystem: Send + Sync { fn add_fonts(&self, fonts: &[Arc>]) -> anyhow::Result<()>; fn all_families(&self) -> Vec; fn load_family(&self, name: &str, features: &FontFeatures) -> anyhow::Result>; @@ -59,6 +116,21 @@ pub trait PlatformFontSystem: Send + Sync { ) -> Vec; } +pub trait InputHandler { + fn selected_text_range(&self) -> Option>; + fn marked_text_range(&self) -> Option>; + fn text_for_range(&self, range_utf16: Range) -> Option; + fn replace_text_in_range(&mut self, replacement_range: Option>, text: &str); + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + new_selected_range: Option>, + ); + fn unmark_text(&mut self); + fn bounds_for_range(&self, range_utf16: Range) -> Option>; +} + #[derive(Copy, Clone, Debug)] pub enum RasterizationOptions { Alpha, @@ -74,6 +146,7 @@ pub struct WindowOptions { pub show: bool, pub kind: WindowKind, pub is_movable: bool, + pub screen: Option>, } impl Default for WindowOptions { @@ -90,16 +163,16 @@ impl Default for WindowOptions { show: true, kind: WindowKind::Normal, is_movable: true, + screen: None, } } } - #[derive(Debug, Default)] pub struct TitlebarOptions { pub title: Option, pub appears_transparent: bool, - pub traffic_light_position: Option>, + pub traffic_light_position: Option>, } #[derive(Copy, Clone, Debug)] @@ -127,5 +200,27 @@ pub enum WindowBounds { Fullscreen, #[default] Maximized, - Fixed(Bounds), + Fixed(Bounds), +} + +#[derive(Copy, Clone, Debug)] +pub enum WindowAppearance { + Light, + VibrantLight, + Dark, + VibrantDark, +} + +impl Default for WindowAppearance { + fn default() -> Self { + Self::Light + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Default)] +pub enum WindowPromptLevel { + #[default] + Info, + Warning, + Critical, } diff --git a/crates/gpui3/src/platform/events.rs b/crates/gpui3/src/platform/events.rs new file mode 100644 index 0000000000000000000000000000000000000000..3918f349f6a0b59372ebe1b7f7251384b6bc346e --- /dev/null +++ b/crates/gpui3/src/platform/events.rs @@ -0,0 +1,204 @@ +use crate::{point, Keystroke, Modifiers, Pixels, Point}; +use std::{any::Any, ops::Deref}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct KeyDownEvent { + pub keystroke: Keystroke, + pub is_held: bool, +} + +#[derive(Clone, Debug)] +pub struct KeyUpEvent { + pub keystroke: Keystroke, +} + +#[derive(Clone, Debug, Default)] +pub struct ModifiersChangedEvent { + pub modifiers: Modifiers, +} + +impl Deref for ModifiersChangedEvent { + type Target = Modifiers; + + fn deref(&self) -> &Self::Target { + &self.modifiers + } +} + +/// The phase of a touch motion event. +/// Based on the winit enum of the same name, +#[derive(Clone, Copy, Debug)] +pub enum TouchPhase { + Started, + Moved, + Ended, +} + +#[derive(Clone, Copy, Debug)] +pub enum ScrollDelta { + Pixels(Point), + Lines(Point), +} + +impl Default for ScrollDelta { + fn default() -> Self { + Self::Lines(Default::default()) + } +} + +impl ScrollDelta { + pub fn precise(&self) -> bool { + match self { + ScrollDelta::Pixels(_) => true, + ScrollDelta::Lines(_) => false, + } + } + + pub fn pixel_delta(&self, line_height: Pixels) -> Point { + match self { + ScrollDelta::Pixels(delta) => *delta, + ScrollDelta::Lines(delta) => point(line_height * delta.x, line_height * delta.y), + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct ScrollWheelEvent { + pub position: Point, + pub delta: ScrollDelta, + pub modifiers: Modifiers, + /// If the platform supports returning the phase of a scroll wheel event, it will be stored here + pub phase: Option, +} + +impl Deref for ScrollWheelEvent { + type Target = Modifiers; + + fn deref(&self) -> &Self::Target { + &self.modifiers + } +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)] +pub enum NavigationDirection { + Back, + Forward, +} + +impl Default for NavigationDirection { + fn default() -> Self { + Self::Back + } +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)] +pub enum MouseButton { + Left, + Right, + Middle, + Navigate(NavigationDirection), +} + +impl MouseButton { + pub fn all() -> Vec { + vec![ + MouseButton::Left, + MouseButton::Right, + MouseButton::Middle, + MouseButton::Navigate(NavigationDirection::Back), + MouseButton::Navigate(NavigationDirection::Forward), + ] + } +} + +impl Default for MouseButton { + fn default() -> Self { + Self::Left + } +} + +#[derive(Clone, Debug, Default)] +pub struct MouseDownEvent { + pub button: MouseButton, + pub position: Point, + pub modifiers: Modifiers, + pub click_count: usize, +} + +#[derive(Clone, Debug, Default)] +pub struct MouseUpEvent { + pub button: MouseButton, + pub position: Point, + pub modifiers: Modifiers, + pub click_count: usize, +} + +#[derive(Clone, Debug, Default)] +pub struct MouseUp { + pub button: MouseButton, + pub position: Point, + pub modifiers: Modifiers, + pub click_count: usize, +} + +#[derive(Clone, Debug, Default)] +pub struct MouseMovedEvent { + pub position: Point, + pub pressed_button: Option, + pub modifiers: Modifiers, +} + +#[derive(Clone, Debug, Default)] +pub struct MouseExitedEvent { + pub position: Point, + pub pressed_button: Option, + pub modifiers: Modifiers, +} + +impl Deref for MouseExitedEvent { + type Target = Modifiers; + + fn deref(&self) -> &Self::Target { + &self.modifiers + } +} + +#[derive(Clone, Debug)] +pub enum Event { + KeyDown(KeyDownEvent), + KeyUp(KeyUpEvent), + ModifiersChanged(ModifiersChangedEvent), + MouseDown(MouseDownEvent), + MouseUp(MouseUpEvent), + MouseMoved(MouseMovedEvent), + MouseExited(MouseExitedEvent), + ScrollWheel(ScrollWheelEvent), +} + +impl Event { + pub fn position(&self) -> Option> { + match self { + Event::KeyDown { .. } => None, + Event::KeyUp { .. } => None, + Event::ModifiersChanged { .. } => None, + Event::MouseDown(event) => Some(event.position), + Event::MouseUp(event) => Some(event.position), + Event::MouseMoved(event) => Some(event.position), + Event::MouseExited(event) => Some(event.position), + Event::ScrollWheel(event) => Some(event.position), + } + } + + pub fn mouse_event<'a>(&'a self) -> Option<&'a dyn Any> { + match self { + Event::KeyDown { .. } => None, + Event::KeyUp { .. } => None, + Event::ModifiersChanged { .. } => None, + Event::MouseDown(event) => Some(event), + Event::MouseUp(event) => Some(event), + Event::MouseMoved(event) => Some(event), + Event::MouseExited(event) => Some(event), + Event::ScrollWheel(event) => Some(event), + } + } +} diff --git a/crates/gpui3/src/platform/keystroke.rs b/crates/gpui3/src/platform/keystroke.rs new file mode 100644 index 0000000000000000000000000000000000000000..d406083f1d27209a084fcf20b6f2715644408a9b --- /dev/null +++ b/crates/gpui3/src/platform/keystroke.rs @@ -0,0 +1,121 @@ +use anyhow::anyhow; +use serde::Deserialize; +use std::fmt::Write; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Default, Deserialize, Hash)] +pub struct Modifiers { + pub control: bool, + pub alt: bool, + pub shift: bool, + pub command: bool, + pub function: bool, +} + +impl Modifiers { + pub fn modified(&self) -> bool { + self.control || self.alt || self.shift || self.command || self.function + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Hash)] +pub struct Keystroke { + pub key: String, + pub modifiers: Modifiers, +} + +impl Keystroke { + pub fn parse(source: &str) -> anyhow::Result { + let mut control = false; + let mut alt = false; + let mut shift = false; + let mut command = false; + let mut function = false; + let mut key = None; + + let mut components = source.split('-').peekable(); + while let Some(component) = components.next() { + match component { + "ctrl" => control = true, + "alt" => alt = true, + "shift" => shift = true, + "cmd" => command = true, + "fn" => function = true, + _ => { + if let Some(component) = components.peek() { + if component.is_empty() && source.ends_with('-') { + key = Some(String::from("-")); + break; + } else { + return Err(anyhow!("Invalid keystroke `{}`", source)); + } + } else { + key = Some(String::from(component)); + } + } + } + } + + let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?; + + Ok(Keystroke { + modifiers: Modifiers { + control, + alt, + shift, + command, + function, + }, + key, + }) + } + + pub fn modified(&self) -> bool { + self.modifiers.modified() + } +} + +impl std::fmt::Display for Keystroke { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Modifiers { + control, + alt, + shift, + command, + function, + } = self.modifiers; + + if control { + f.write_char('^')?; + } + if alt { + f.write_char('⎇')?; + } + if command { + f.write_char('⌘')?; + } + if shift { + f.write_char('⇧')?; + } + if function { + f.write_char('𝙛')?; + } + + let key = match self.key.as_str() { + "backspace" => '⌫', + "up" => '↑', + "down" => '↓', + "left" => '←', + "right" => '→', + "tab" => '⇥', + "escape" => '⎋', + key => { + if key.len() == 1 { + key.chars().next().unwrap().to_ascii_uppercase() + } else { + return f.write_str(key); + } + } + }; + f.write_char(key) + } +} diff --git a/crates/gpui3/src/platform/mac.rs b/crates/gpui3/src/platform/mac.rs new file mode 100644 index 0000000000000000000000000000000000000000..d7a948267158e445529c10f0187bf1e78bf57ea2 --- /dev/null +++ b/crates/gpui3/src/platform/mac.rs @@ -0,0 +1,144 @@ +///! Macos screen have a y axis that goings up from the bottom of the screen and +///! an origin at the bottom left of the main display. +mod dispatcher; +mod events; +mod platform; +mod screen; +mod window; +mod window_appearence; + +use std::{ + ffi::{c_char, CStr, OsStr}, + ops::Range, + os::unix::prelude::OsStrExt, + path::PathBuf, +}; + +use crate::{px, size, Pixels, Size}; +use anyhow::anyhow; +use cocoa::{ + base::{id, nil}, + foundation::{NSAutoreleasePool, NSNotFound, NSRect, NSSize, NSString, NSUInteger, NSURL}, +}; +use objc::{ + msg_send, + runtime::{BOOL, NO, YES}, + sel, sel_impl, +}; +pub use platform::*; +pub use screen::*; +pub use window::*; +use window_appearence::*; + +trait BoolExt { + fn to_objc(self) -> BOOL; +} + +impl BoolExt for bool { + fn to_objc(self) -> BOOL { + if self { + YES + } else { + NO + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct NSRange { + pub location: NSUInteger, + pub length: NSUInteger, +} + +impl NSRange { + fn invalid() -> Self { + Self { + location: NSNotFound as NSUInteger, + length: 0, + } + } + + fn is_valid(&self) -> bool { + self.location != NSNotFound as NSUInteger + } + + fn to_range(self) -> Option> { + if self.is_valid() { + let start = self.location as usize; + let end = start + self.length as usize; + Some(start..end) + } else { + None + } + } +} + +impl From> for NSRange { + fn from(range: Range) -> Self { + NSRange { + location: range.start as NSUInteger, + length: range.len() as NSUInteger, + } + } +} + +unsafe impl objc::Encode for NSRange { + fn encode() -> objc::Encoding { + let encoding = format!( + "{{NSRange={}{}}}", + NSUInteger::encode().as_str(), + NSUInteger::encode().as_str() + ); + unsafe { objc::Encoding::from_str(&encoding) } + } +} + +unsafe fn ns_string(string: &str) -> id { + NSString::alloc(nil).init_str(string).autorelease() +} + +impl From for Size { + fn from(value: NSSize) -> Self { + Size { + width: px(value.width as f32), + height: px(value.height as f32), + } + } +} + +pub trait NSRectExt { + fn size(&self) -> Size; + fn intersects(&self, other: Self) -> bool; +} + +impl NSRectExt for NSRect { + fn size(&self) -> Size { + size(px(self.size.width as f32), px(self.size.height as f32)) + } + + fn intersects(&self, other: Self) -> bool { + self.size.width > 0. + && self.size.height > 0. + && other.size.width > 0. + && other.size.height > 0. + && self.origin.x <= other.origin.x + other.size.width + && self.origin.x + self.size.width >= other.origin.x + && self.origin.y <= other.origin.y + other.size.height + && self.origin.y + self.size.height >= other.origin.y + } +} + +unsafe fn ns_url_to_path(url: id) -> crate::Result { + let path: *mut c_char = msg_send![url, fileSystemRepresentation]; + if path.is_null() { + Err(anyhow!( + "url is not a file path: {}", + CStr::from_ptr(url.absoluteString().UTF8String()).to_string_lossy() + )) + } else { + Ok(PathBuf::from(OsStr::from_bytes( + CStr::from_ptr(path).to_bytes(), + ))) + } +} diff --git a/crates/gpui3/src/platform/mac/dispatch.h b/crates/gpui3/src/platform/mac/dispatch.h new file mode 100644 index 0000000000000000000000000000000000000000..f56a0eae33db5f4fd3715024591c455c9b985098 --- /dev/null +++ b/crates/gpui3/src/platform/mac/dispatch.h @@ -0,0 +1 @@ +#include diff --git a/crates/gpui3/src/platform/mac/dispatcher.rs b/crates/gpui3/src/platform/mac/dispatcher.rs new file mode 100644 index 0000000000000000000000000000000000000000..f20ba602b0de3c4e510f1a069fdde557ad259b0e --- /dev/null +++ b/crates/gpui3/src/platform/mac/dispatcher.rs @@ -0,0 +1,42 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +use crate::PlatformDispatcher; +use async_task::Runnable; +use objc::{ + class, msg_send, + runtime::{BOOL, YES}, + sel, sel_impl, +}; +use std::ffi::c_void; + +include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs")); + +pub fn dispatch_get_main_queue() -> dispatch_queue_t { + unsafe { &_dispatch_main_q as *const _ as dispatch_queue_t } +} + +pub struct MacDispatcher; + +impl PlatformDispatcher for MacDispatcher { + fn is_main_thread(&self) -> bool { + let is_main_thread: BOOL = unsafe { msg_send![class!(NSThread), isMainThread] }; + is_main_thread == YES + } + + fn run_on_main_thread(&self, runnable: Runnable) { + unsafe { + dispatch_async_f( + dispatch_get_main_queue(), + runnable.into_raw() as *mut c_void, + Some(trampoline), + ); + } + + extern "C" fn trampoline(runnable: *mut c_void) { + let task = unsafe { Runnable::from_raw(runnable as *mut ()) }; + task.run(); + } + } +} diff --git a/crates/gpui3/src/platform/mac/events.rs b/crates/gpui3/src/platform/mac/events.rs new file mode 100644 index 0000000000000000000000000000000000000000..55853c10a4658ebba35bbfd12a66de0a026393cf --- /dev/null +++ b/crates/gpui3/src/platform/mac/events.rs @@ -0,0 +1,354 @@ +use crate::{ + point, px, Event, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, + MouseButton, MouseDownEvent, MouseExitedEvent, MouseMovedEvent, MouseUpEvent, + NavigationDirection, Pixels, ScrollDelta, ScrollWheelEvent, TouchPhase, +}; +use cocoa::{ + appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType}, + base::{id, YES}, + foundation::NSString as _, +}; +use core_graphics::{ + event::{CGEvent, CGEventFlags, CGKeyCode}, + event_source::{CGEventSource, CGEventSourceStateID}, +}; +use ctor::ctor; +use foreign_types::ForeignType; +use objc::{class, msg_send, sel, sel_impl}; +use std::{borrow::Cow, ffi::CStr, mem, os::raw::c_char, ptr}; + +const BACKSPACE_KEY: u16 = 0x7f; +const SPACE_KEY: u16 = b' ' as u16; +const ENTER_KEY: u16 = 0x0d; +const NUMPAD_ENTER_KEY: u16 = 0x03; +const ESCAPE_KEY: u16 = 0x1b; +const TAB_KEY: u16 = 0x09; +const SHIFT_TAB_KEY: u16 = 0x19; + +static mut EVENT_SOURCE: core_graphics::sys::CGEventSourceRef = ptr::null_mut(); + +#[ctor] +unsafe fn build_event_source() { + let source = CGEventSource::new(CGEventSourceStateID::Private).unwrap(); + EVENT_SOURCE = source.as_ptr(); + mem::forget(source); +} + +pub fn key_to_native(key: &str) -> Cow { + use cocoa::appkit::*; + let code = match key { + "space" => SPACE_KEY, + "backspace" => BACKSPACE_KEY, + "up" => NSUpArrowFunctionKey, + "down" => NSDownArrowFunctionKey, + "left" => NSLeftArrowFunctionKey, + "right" => NSRightArrowFunctionKey, + "pageup" => NSPageUpFunctionKey, + "pagedown" => NSPageDownFunctionKey, + "home" => NSHomeFunctionKey, + "end" => NSEndFunctionKey, + "delete" => NSDeleteFunctionKey, + "f1" => NSF1FunctionKey, + "f2" => NSF2FunctionKey, + "f3" => NSF3FunctionKey, + "f4" => NSF4FunctionKey, + "f5" => NSF5FunctionKey, + "f6" => NSF6FunctionKey, + "f7" => NSF7FunctionKey, + "f8" => NSF8FunctionKey, + "f9" => NSF9FunctionKey, + "f10" => NSF10FunctionKey, + "f11" => NSF11FunctionKey, + "f12" => NSF12FunctionKey, + _ => return Cow::Borrowed(key), + }; + Cow::Owned(String::from_utf16(&[code]).unwrap()) +} + +unsafe fn read_modifiers(native_event: id) -> Modifiers { + let modifiers = native_event.modifierFlags(); + let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); + let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); + let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); + let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); + let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask); + + Modifiers { + control, + alt, + shift, + command, + function, + } +} + +impl Event { + pub unsafe fn from_native(native_event: id, window_height: Option) -> Option { + let event_type = native_event.eventType(); + + // Filter out event types that aren't in the NSEventType enum. + // See https://github.com/servo/cocoa-rs/issues/155#issuecomment-323482792 for details. + match event_type as u64 { + 0 | 21 | 32 | 33 | 35 | 36 | 37 => { + return None; + } + _ => {} + } + + match event_type { + NSEventType::NSFlagsChanged => Some(Self::ModifiersChanged(ModifiersChangedEvent { + modifiers: read_modifiers(native_event), + })), + NSEventType::NSKeyDown => Some(Self::KeyDown(KeyDownEvent { + keystroke: parse_keystroke(native_event), + is_held: native_event.isARepeat() == YES, + })), + NSEventType::NSKeyUp => Some(Self::KeyUp(KeyUpEvent { + keystroke: parse_keystroke(native_event), + })), + NSEventType::NSLeftMouseDown + | NSEventType::NSRightMouseDown + | NSEventType::NSOtherMouseDown => { + let button = match native_event.buttonNumber() { + 0 => MouseButton::Left, + 1 => MouseButton::Right, + 2 => MouseButton::Middle, + 3 => MouseButton::Navigate(NavigationDirection::Back), + 4 => MouseButton::Navigate(NavigationDirection::Forward), + // Other mouse buttons aren't tracked currently + _ => return None, + }; + window_height.map(|window_height| { + Self::MouseDown(MouseDownEvent { + button, + position: point( + px(native_event.locationInWindow().x as f32), + // MacOS screen coordinates are relative to bottom left + window_height - px(native_event.locationInWindow().y as f32), + ), + modifiers: read_modifiers(native_event), + click_count: native_event.clickCount() as usize, + }) + }) + } + NSEventType::NSLeftMouseUp + | NSEventType::NSRightMouseUp + | NSEventType::NSOtherMouseUp => { + let button = match native_event.buttonNumber() { + 0 => MouseButton::Left, + 1 => MouseButton::Right, + 2 => MouseButton::Middle, + 3 => MouseButton::Navigate(NavigationDirection::Back), + 4 => MouseButton::Navigate(NavigationDirection::Forward), + // Other mouse buttons aren't tracked currently + _ => return None, + }; + + window_height.map(|window_height| { + Self::MouseUp(MouseUpEvent { + button, + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + modifiers: read_modifiers(native_event), + click_count: native_event.clickCount() as usize, + }) + }) + } + NSEventType::NSScrollWheel => window_height.map(|window_height| { + let phase = match native_event.phase() { + NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => { + Some(TouchPhase::Started) + } + NSEventPhase::NSEventPhaseEnded => Some(TouchPhase::Ended), + _ => Some(TouchPhase::Moved), + }; + + let raw_data = point( + native_event.scrollingDeltaX() as f32, + native_event.scrollingDeltaY() as f32, + ); + + let delta = if native_event.hasPreciseScrollingDeltas() == YES { + ScrollDelta::Pixels(raw_data.map(px)) + } else { + ScrollDelta::Lines(raw_data) + }; + + Self::ScrollWheel(ScrollWheelEvent { + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + delta, + phase, + modifiers: read_modifiers(native_event), + }) + }), + NSEventType::NSLeftMouseDragged + | NSEventType::NSRightMouseDragged + | NSEventType::NSOtherMouseDragged => { + let pressed_button = match native_event.buttonNumber() { + 0 => MouseButton::Left, + 1 => MouseButton::Right, + 2 => MouseButton::Middle, + 3 => MouseButton::Navigate(NavigationDirection::Back), + 4 => MouseButton::Navigate(NavigationDirection::Forward), + // Other mouse buttons aren't tracked currently + _ => return None, + }; + + window_height.map(|window_height| { + Self::MouseMoved(MouseMovedEvent { + pressed_button: Some(pressed_button), + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + modifiers: read_modifiers(native_event), + }) + }) + } + NSEventType::NSMouseMoved => window_height.map(|window_height| { + Self::MouseMoved(MouseMovedEvent { + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + pressed_button: None, + modifiers: read_modifiers(native_event), + }) + }), + NSEventType::NSMouseExited => window_height.map(|window_height| { + Self::MouseExited(MouseExitedEvent { + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + + pressed_button: None, + modifiers: read_modifiers(native_event), + }) + }), + _ => None, + } + } +} + +unsafe fn parse_keystroke(native_event: id) -> Keystroke { + use cocoa::appkit::*; + + let mut chars_ignoring_modifiers = + CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char) + .to_str() + .unwrap() + .to_string(); + let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16); + let modifiers = native_event.modifierFlags(); + + let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); + let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); + let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); + let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); + let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask) + && first_char.map_or(true, |ch| { + !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch) + }); + + #[allow(non_upper_case_globals)] + let key = match first_char { + Some(SPACE_KEY) => "space".to_string(), + Some(BACKSPACE_KEY) => "backspace".to_string(), + Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter".to_string(), + Some(ESCAPE_KEY) => "escape".to_string(), + Some(TAB_KEY) => "tab".to_string(), + Some(SHIFT_TAB_KEY) => "tab".to_string(), + Some(NSUpArrowFunctionKey) => "up".to_string(), + Some(NSDownArrowFunctionKey) => "down".to_string(), + Some(NSLeftArrowFunctionKey) => "left".to_string(), + Some(NSRightArrowFunctionKey) => "right".to_string(), + Some(NSPageUpFunctionKey) => "pageup".to_string(), + Some(NSPageDownFunctionKey) => "pagedown".to_string(), + Some(NSHomeFunctionKey) => "home".to_string(), + Some(NSEndFunctionKey) => "end".to_string(), + Some(NSDeleteFunctionKey) => "delete".to_string(), + Some(NSF1FunctionKey) => "f1".to_string(), + Some(NSF2FunctionKey) => "f2".to_string(), + Some(NSF3FunctionKey) => "f3".to_string(), + Some(NSF4FunctionKey) => "f4".to_string(), + Some(NSF5FunctionKey) => "f5".to_string(), + Some(NSF6FunctionKey) => "f6".to_string(), + Some(NSF7FunctionKey) => "f7".to_string(), + Some(NSF8FunctionKey) => "f8".to_string(), + Some(NSF9FunctionKey) => "f9".to_string(), + Some(NSF10FunctionKey) => "f10".to_string(), + Some(NSF11FunctionKey) => "f11".to_string(), + Some(NSF12FunctionKey) => "f12".to_string(), + _ => { + let mut chars_ignoring_modifiers_and_shift = + chars_for_modified_key(native_event.keyCode(), false, false); + + // Honor ⌘ when Dvorak-QWERTY is used. + let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false); + if command && chars_ignoring_modifiers_and_shift != chars_with_cmd { + chars_ignoring_modifiers = + chars_for_modified_key(native_event.keyCode(), true, shift); + chars_ignoring_modifiers_and_shift = chars_with_cmd; + } + + if shift { + if chars_ignoring_modifiers_and_shift + == chars_ignoring_modifiers.to_ascii_lowercase() + { + chars_ignoring_modifiers_and_shift + } else if chars_ignoring_modifiers_and_shift != chars_ignoring_modifiers { + shift = false; + chars_ignoring_modifiers + } else { + chars_ignoring_modifiers + } + } else { + chars_ignoring_modifiers + } + } + }; + + Keystroke { + modifiers: Modifiers { + control, + alt, + shift, + command, + function, + }, + key, + } +} + +fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String { + // Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that + // always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing + // an event with the given flags instead lets us access `characters`, which always + // returns a valid string. + let source = unsafe { core_graphics::event_source::CGEventSource::from_ptr(EVENT_SOURCE) }; + let event = CGEvent::new_keyboard_event(source.clone(), code, true).unwrap(); + mem::forget(source); + + let mut flags = CGEventFlags::empty(); + if cmd { + flags |= CGEventFlags::CGEventFlagCommand; + } + if shift { + flags |= CGEventFlags::CGEventFlagShift; + } + event.set_flags(flags); + + unsafe { + let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event]; + CStr::from_ptr(event.characters().UTF8String()) + .to_str() + .unwrap() + .to_string() + } +} diff --git a/crates/gpui3/src/platform/mac/platform.rs b/crates/gpui3/src/platform/mac/platform.rs new file mode 100644 index 0000000000000000000000000000000000000000..39a9f70566ad78044f93d592b7ca54a4f71570a2 --- /dev/null +++ b/crates/gpui3/src/platform/mac/platform.rs @@ -0,0 +1,1102 @@ +// use anyhow::{anyhow, Result}; +// use block::ConcreteBlock; +// use cocoa::{ +// appkit::{ +// NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, +// NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, +// NSSavePanel, NSWindow, +// }, +// base::{id, nil, selector, BOOL, YES}, +// foundation::{NSArray, NSAutoreleasePool, NSData, NSInteger, NSString, NSUInteger, NSURL}, +// }; +// use core_foundation::{ +// base::{CFTypeRef, OSStatus}, +// dictionary::CFDictionaryRef, +// string::CFStringRef, +// }; +// use ctor::ctor; +// use objc::{ +// class, +// declare::ClassDecl, +// msg_send, +// runtime::{Class, Object, Sel}, +// sel, sel_impl, +// }; + +// use postage::oneshot; +// use ptr::null_mut; +// use std::{ +// cell::{Cell, RefCell}, +// convert::TryInto, +// ffi::{c_void, CStr, OsStr}, +// os::{raw::c_char, unix::ffi::OsStrExt}, +// path::{Path, PathBuf}, +// process::Command, +// ptr, +// rc::Rc, +// slice, str, +// sync::Arc, +// }; + +// use crate::Event; + +// #[allow(non_upper_case_globals)] +// const NSUTF8StringEncoding: NSUInteger = 4; + +// const MAC_PLATFORM_IVAR: &str = "platform"; +// static mut APP_CLASS: *const Class = ptr::null(); +// static mut APP_DELEGATE_CLASS: *const Class = ptr::null(); + +// #[ctor] +// unsafe fn build_classes() { +// APP_CLASS = { +// let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap(); +// decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR); +// decl.add_method( +// sel!(sendEvent:), +// send_event as extern "C" fn(&mut Object, Sel, id), +// ); +// decl.register() +// }; + +// APP_DELEGATE_CLASS = { +// let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap(); +// decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR); +// decl.add_method( +// sel!(applicationDidFinishLaunching:), +// did_finish_launching as extern "C" fn(&mut Object, Sel, id), +// ); +// decl.add_method( +// sel!(applicationShouldHandleReopen:hasVisibleWindows:), +// should_handle_reopen as extern "C" fn(&mut Object, Sel, id, bool), +// ); +// decl.add_method( +// sel!(applicationDidBecomeActive:), +// did_become_active as extern "C" fn(&mut Object, Sel, id), +// ); +// decl.add_method( +// sel!(applicationDidResignActive:), +// did_resign_active as extern "C" fn(&mut Object, Sel, id), +// ); +// decl.add_method( +// sel!(applicationWillTerminate:), +// will_terminate as extern "C" fn(&mut Object, Sel, id), +// ); +// decl.add_method( +// sel!(handleGPUIMenuItem:), +// handle_menu_item as extern "C" fn(&mut Object, Sel, id), +// ); +// // Add menu item handlers so that OS save panels have the correct key commands +// decl.add_method( +// sel!(cut:), +// handle_menu_item as extern "C" fn(&mut Object, Sel, id), +// ); +// decl.add_method( +// sel!(copy:), +// handle_menu_item as extern "C" fn(&mut Object, Sel, id), +// ); +// decl.add_method( +// sel!(paste:), +// handle_menu_item as extern "C" fn(&mut Object, Sel, id), +// ); +// decl.add_method( +// sel!(selectAll:), +// handle_menu_item as extern "C" fn(&mut Object, Sel, id), +// ); +// decl.add_method( +// sel!(undo:), +// handle_menu_item as extern "C" fn(&mut Object, Sel, id), +// ); +// decl.add_method( +// sel!(redo:), +// handle_menu_item as extern "C" fn(&mut Object, Sel, id), +// ); +// decl.add_method( +// sel!(validateMenuItem:), +// validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool, +// ); +// decl.add_method( +// sel!(menuWillOpen:), +// menu_will_open as extern "C" fn(&mut Object, Sel, id), +// ); +// decl.add_method( +// sel!(application:openURLs:), +// open_urls as extern "C" fn(&mut Object, Sel, id, id), +// ); +// decl.register() +// } +// } + +// pub struct MacForegroundPlatformState { +// dispatcher: Arc, +// fonts: Arc, +// pasteboard: id, +// text_hash_pasteboard_type: id, +// metadata_pasteboard_type: id, +// become_active: Option>, +// resign_active: Option>, +// reopen: Option>, +// quit: Option>, +// event: Option bool>>, +// // menu_command: Option>, +// validate_menu_command: Option bool>>, +// will_open_menu: Option>, +// open_urls: Option)>>, +// finish_launching: Option>, +// // menu_actions: Vec>, +// // foreground: Rc, +// } + +// impl MacForegroundPlatform { +// pub fn new(foreground: Rc) -> Self { +// Self(RefCell::new(MacForegroundPlatformState { +// become_active: None, +// resign_active: None, +// reopen: None, +// quit: None, +// event: None, +// // menu_command: None, +// validate_menu_command: None, +// will_open_menu: None, +// open_urls: None, +// finish_launching: None, +// menu_actions: Default::default(), +// foreground, +// })) +// } + +// unsafe fn create_menu_bar( +// &self, +// menus: Vec, +// delegate: id, +// actions: &mut Vec>, +// keystroke_matcher: &KeymapMatcher, +// ) -> id { +// let application_menu = NSMenu::new(nil).autorelease(); +// application_menu.setDelegate_(delegate); + +// for menu_config in menus { +// let menu = NSMenu::new(nil).autorelease(); +// menu.setTitle_(ns_string(menu_config.name)); +// menu.setDelegate_(delegate); + +// for item_config in menu_config.items { +// menu.addItem_(self.create_menu_item( +// item_config, +// delegate, +// actions, +// keystroke_matcher, +// )); +// } + +// let menu_item = NSMenuItem::new(nil).autorelease(); +// menu_item.setSubmenu_(menu); +// application_menu.addItem_(menu_item); + +// if menu_config.name == "Window" { +// let app: id = msg_send![APP_CLASS, sharedApplication]; +// app.setWindowsMenu_(menu); +// } +// } + +// application_menu +// } + +// // unsafe fn create_menu_item( +// // &self, +// // item: MenuItem, +// // delegate: id, +// // actions: &mut Vec>, +// // keystroke_matcher: &KeymapMatcher, +// // ) -> id { +// // match item { +// // MenuItem::Separator => NSMenuItem::separatorItem(nil), +// // MenuItem::Action { +// // name, +// // action, +// // os_action, +// // } => { +// // // TODO +// // let keystrokes = keystroke_matcher +// // .bindings_for_action(action.id()) +// // .find(|binding| binding.action().eq(action.as_ref())) +// // .map(|binding| binding.keystrokes()); +// // let selector = match os_action { +// // Some(crate::OsAction::Cut) => selector("cut:"), +// // Some(crate::OsAction::Copy) => selector("copy:"), +// // Some(crate::OsAction::Paste) => selector("paste:"), +// // Some(crate::OsAction::SelectAll) => selector("selectAll:"), +// // Some(crate::OsAction::Undo) => selector("undo:"), +// // Some(crate::OsAction::Redo) => selector("redo:"), +// // None => selector("handleGPUIMenuItem:"), +// // }; + +// // let item; +// // if let Some(keystrokes) = keystrokes { +// // if keystrokes.len() == 1 { +// // let keystroke = &keystrokes[0]; +// // let mut mask = NSEventModifierFlags::empty(); +// // for (modifier, flag) in &[ +// // (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask), +// // (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask), +// // (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask), +// // (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask), +// // ] { +// // if *modifier { +// // mask |= *flag; +// // } +// // } + +// // item = NSMenuItem::alloc(nil) +// // .initWithTitle_action_keyEquivalent_( +// // ns_string(name), +// // selector, +// // ns_string(key_to_native(&keystroke.key).as_ref()), +// // ) +// // .autorelease(); +// // item.setKeyEquivalentModifierMask_(mask); +// // } +// // // For multi-keystroke bindings, render the keystroke as part of the title. +// // else { +// // use std::fmt::Write; + +// // let mut name = format!("{name} ["); +// // for (i, keystroke) in keystrokes.iter().enumerate() { +// // if i > 0 { +// // name.push(' '); +// // } +// // write!(&mut name, "{}", keystroke).unwrap(); +// // } +// // name.push(']'); + +// // item = NSMenuItem::alloc(nil) +// // .initWithTitle_action_keyEquivalent_( +// // ns_string(&name), +// // selector, +// // ns_string(""), +// // ) +// // .autorelease(); +// // } +// // } else { +// // item = NSMenuItem::alloc(nil) +// // .initWithTitle_action_keyEquivalent_( +// // ns_string(name), +// // selector, +// // ns_string(""), +// // ) +// // .autorelease(); +// // } + +// // let tag = actions.len() as NSInteger; +// // let _: () = msg_send![item, setTag: tag]; +// // actions.push(action); +// // item +// // } +// // MenuItem::Submenu(Menu { name, items }) => { +// // let item = NSMenuItem::new(nil).autorelease(); +// // let submenu = NSMenu::new(nil).autorelease(); +// // submenu.setDelegate_(delegate); +// // for item in items { +// // submenu.addItem_(self.create_menu_item( +// // item, +// // delegate, +// // actions, +// // keystroke_matcher, +// // )); +// // } +// // item.setSubmenu_(submenu); +// // item.setTitle_(ns_string(name)); +// // item +// // } +// // } +// // } + +// unsafe fn read_from_pasteboard(&self, kind: id) -> Option<&[u8]> { +// let data = self.pasteboard.dataForType(kind); +// if data == nil { +// None +// } else { +// Some(slice::from_raw_parts( +// data.bytes() as *mut u8, +// data.length() as usize, +// )) +// } +// } +// } + +// // impl platform::ForegroundPlatform for MacForegroundPlatform { +// // fn on_become_active(&self, callback: Box) { +// // self.0.borrow_mut().become_active = Some(callback); +// // } + +// // fn on_resign_active(&self, callback: Box) { +// // self.0.borrow_mut().resign_active = Some(callback); +// // } + +// // fn on_quit(&self, callback: Box) { +// // self.0.borrow_mut().quit = Some(callback); +// // } + +// // fn on_reopen(&self, callback: Box) { +// // self.0.borrow_mut().reopen = Some(callback); +// // } + +// // fn on_event(&self, callback: Box bool>) { +// // self.0.borrow_mut().event = Some(callback); +// // } + +// // fn on_open_urls(&self, callback: Box)>) { +// // self.0.borrow_mut().open_urls = Some(callback); +// // } + +// // fn run(&self, on_finish_launching: Box) { +// // self.0.borrow_mut().finish_launching = Some(on_finish_launching); + +// // unsafe { +// // let app: id = msg_send![APP_CLASS, sharedApplication]; +// // let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new]; +// // app.setDelegate_(app_delegate); + +// // let self_ptr = self as *const Self as *const c_void; +// // (*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr); +// // (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr); + +// // let pool = NSAutoreleasePool::new(nil); +// // app.run(); +// // pool.drain(); + +// // (*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::()); +// // (*app.delegate()).set_ivar(MAC_PLATFORM_IVAR, null_mut::()); +// // } +// // } + +// // fn on_menu_command(&self, callback: Box) { +// // self.0.borrow_mut().menu_command = Some(callback); +// // } + +// // fn on_will_open_menu(&self, callback: Box) { +// // self.0.borrow_mut().will_open_menu = Some(callback); +// // } + +// // fn on_validate_menu_command(&self, callback: Box bool>) { +// // self.0.borrow_mut().validate_menu_command = Some(callback); +// // } + +// // fn set_menus(&self, menus: Vec, keystroke_matcher: &KeymapMatcher) { +// // unsafe { +// // let app: id = msg_send![APP_CLASS, sharedApplication]; +// // let mut state = self.0.borrow_mut(); +// // let actions = &mut state.menu_actions; +// // app.setMainMenu_(self.create_menu_bar( +// // menus, +// // app.delegate(), +// // actions, +// // keystroke_matcher, +// // )); +// // } +// // } + +// // fn prompt_for_paths( +// // &self, +// // options: platform::PathPromptOptions, +// // ) -> oneshot::Receiver>> { +// // unsafe { +// // let panel = NSOpenPanel::openPanel(nil); +// // panel.setCanChooseDirectories_(options.directories.to_objc()); +// // panel.setCanChooseFiles_(options.files.to_objc()); +// // panel.setAllowsMultipleSelection_(options.multiple.to_objc()); +// // panel.setResolvesAliases_(false.to_objc()); +// // let (done_tx, done_rx) = oneshot::channel(); +// // let done_tx = Cell::new(Some(done_tx)); +// // let block = ConcreteBlock::new(move |response: NSModalResponse| { +// // let result = if response == NSModalResponse::NSModalResponseOk { +// // let mut result = Vec::new(); +// // let urls = panel.URLs(); +// // for i in 0..urls.count() { +// // let url = urls.objectAtIndex(i); +// // if url.isFileURL() == YES { +// // if let Ok(path) = ns_url_to_path(url) { +// // result.push(path) +// // } +// // } +// // } +// // Some(result) +// // } else { +// // None +// // }; + +// // if let Some(mut done_tx) = done_tx.take() { +// // let _ = postage::sink::Sink::try_send(&mut done_tx, result); +// // } +// // }); +// // let block = block.copy(); +// // let _: () = msg_send![panel, beginWithCompletionHandler: block]; +// // done_rx +// // } +// // } + +// // fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver> { +// // unsafe { +// // let panel = NSSavePanel::savePanel(nil); +// // let path = ns_string(directory.to_string_lossy().as_ref()); +// // let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc()); +// // panel.setDirectoryURL(url); + +// // let (done_tx, done_rx) = oneshot::channel(); +// // let done_tx = Cell::new(Some(done_tx)); +// // let block = ConcreteBlock::new(move |response: NSModalResponse| { +// // let mut result = None; +// // if response == NSModalResponse::NSModalResponseOk { +// // let url = panel.URL(); +// // if url.isFileURL() == YES { +// // result = ns_url_to_path(panel.URL()).ok() +// // } +// // } + +// // if let Some(mut done_tx) = done_tx.take() { +// // let _ = postage::sink::Sink::try_send(&mut done_tx, result); +// // } +// // }); +// // let block = block.copy(); +// // let _: () = msg_send![panel, beginWithCompletionHandler: block]; +// // done_rx +// // } +// // } + +// // fn reveal_path(&self, path: &Path) { +// // unsafe { +// // let path = path.to_path_buf(); +// // self.0 +// // .borrow() +// // .foreground +// // .spawn(async move { +// // let full_path = ns_string(path.to_str().unwrap_or("")); +// // let root_full_path = ns_string(""); +// // let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; +// // let _: BOOL = msg_send![ +// // workspace, +// // selectFile: full_path +// // inFileViewerRootedAtPath: root_full_path +// // ]; +// // }) +// // .detach(); +// // } +// // } +// // } + +// // impl Platform for MacPlatform { +// // fn dispatcher(&self) -> Arc { +// // self.dispatcher.clone() +// // } + +// // fn fonts(&self) -> Arc { +// // self.fonts.clone() +// // } + +// // fn activate(&self, ignoring_other_apps: bool) { +// // unsafe { +// // let app = NSApplication::sharedApplication(nil); +// // app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc()); +// // } +// // } + +// // fn hide(&self) { +// // unsafe { +// // let app = NSApplication::sharedApplication(nil); +// // let _: () = msg_send![app, hide: nil]; +// // } +// // } + +// // fn hide_other_apps(&self) { +// // unsafe { +// // let app = NSApplication::sharedApplication(nil); +// // let _: () = msg_send![app, hideOtherApplications: nil]; +// // } +// // } + +// // fn unhide_other_apps(&self) { +// // unsafe { +// // let app = NSApplication::sharedApplication(nil); +// // let _: () = msg_send![app, unhideAllApplications: nil]; +// // } +// // } + +// // fn quit(&self) { +// // // Quitting the app causes us to close windows, which invokes `Window::on_close` callbacks +// // // synchronously before this method terminates. If we call `Platform::quit` while holding a +// // // borrow of the app state (which most of the time we will do), we will end up +// // // double-borrowing the app state in the `on_close` callbacks for our open windows. To solve +// // // this, we make quitting the application asynchronous so that we aren't holding borrows to +// // // the app state on the stack when we actually terminate the app. + +// // use super::dispatcher::{dispatch_async_f, dispatch_get_main_queue}; + +// // unsafe { +// // dispatch_async_f(dispatch_get_main_queue(), ptr::null_mut(), Some(quit)); +// // } + +// // unsafe extern "C" fn quit(_: *mut c_void) { +// // let app = NSApplication::sharedApplication(nil); +// // let _: () = msg_send![app, terminate: nil]; +// // } +// // } + +// // fn screen_by_id(&self, id: uuid::Uuid) -> Option> { +// // Screen::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>) +// // } + +// // fn screens(&self) -> Vec> { +// // Screen::all() +// // .into_iter() +// // .map(|screen| Rc::new(screen) as Rc<_>) +// // .collect() +// // } + +// // fn open_window( +// // &self, +// // handle: AnyWindowHandle, +// // options: platform::WindowOptions, +// // executor: Rc, +// // ) -> Box { +// // Box::new(MacWindow::open(handle, options, executor, self.fonts())) +// // } + +// // fn main_window(&self) -> Option { +// // MacWindow::main_window() +// // } + +// // fn add_status_item(&self, _handle: AnyWindowHandle) -> Box { +// // Box::new(StatusItem::add(self.fonts())) +// // } + +// // fn write_to_clipboard(&self, item: ClipboardItem) { +// // unsafe { +// // self.pasteboard.clearContents(); + +// // let text_bytes = NSData::dataWithBytes_length_( +// // nil, +// // item.text.as_ptr() as *const c_void, +// // item.text.len() as u64, +// // ); +// // self.pasteboard +// // .setData_forType(text_bytes, NSPasteboardTypeString); + +// // if let Some(metadata) = item.metadata.as_ref() { +// // let hash_bytes = ClipboardItem::text_hash(&item.text).to_be_bytes(); +// // let hash_bytes = NSData::dataWithBytes_length_( +// // nil, +// // hash_bytes.as_ptr() as *const c_void, +// // hash_bytes.len() as u64, +// // ); +// // self.pasteboard +// // .setData_forType(hash_bytes, self.text_hash_pasteboard_type); + +// // let metadata_bytes = NSData::dataWithBytes_length_( +// // nil, +// // metadata.as_ptr() as *const c_void, +// // metadata.len() as u64, +// // ); +// // self.pasteboard +// // .setData_forType(metadata_bytes, self.metadata_pasteboard_type); +// // } +// // } +// // } + +// // fn read_from_clipboard(&self) -> Option { +// // unsafe { +// // if let Some(text_bytes) = self.read_from_pasteboard(NSPasteboardTypeString) { +// // let text = String::from_utf8_lossy(text_bytes).to_string(); +// // let hash_bytes = self +// // .read_from_pasteboard(self.text_hash_pasteboard_type) +// // .and_then(|bytes| bytes.try_into().ok()) +// // .map(u64::from_be_bytes); +// // let metadata_bytes = self +// // .read_from_pasteboard(self.metadata_pasteboard_type) +// // .and_then(|bytes| String::from_utf8(bytes.to_vec()).ok()); + +// // if let Some((hash, metadata)) = hash_bytes.zip(metadata_bytes) { +// // if hash == ClipboardItem::text_hash(&text) { +// // Some(ClipboardItem { +// // text, +// // metadata: Some(metadata), +// // }) +// // } else { +// // Some(ClipboardItem { +// // text, +// // metadata: None, +// // }) +// // } +// // } else { +// // Some(ClipboardItem { +// // text, +// // metadata: None, +// // }) +// // } +// // } else { +// // None +// // } +// // } +// // } + +// // fn open_url(&self, url: &str) { +// // unsafe { +// // let url = NSURL::alloc(nil) +// // .initWithString_(ns_string(url)) +// // .autorelease(); +// // let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; +// // msg_send![workspace, openURL: url] +// // } +// // } + +// // fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> { +// // let url = CFString::from(url); +// // let username = CFString::from(username); +// // let password = CFData::from_buffer(password); + +// // unsafe { +// // use security::*; + +// // // First, check if there are already credentials for the given server. If so, then +// // // update the username and password. +// // let mut verb = "updating"; +// // let mut query_attrs = CFMutableDictionary::with_capacity(2); +// // query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); +// // query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + +// // let mut attrs = CFMutableDictionary::with_capacity(4); +// // attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); +// // attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); +// // attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef()); +// // attrs.set(kSecValueData as *const _, password.as_CFTypeRef()); + +// // let mut status = SecItemUpdate( +// // query_attrs.as_concrete_TypeRef(), +// // attrs.as_concrete_TypeRef(), +// // ); + +// // // If there were no existing credentials for the given server, then create them. +// // if status == errSecItemNotFound { +// // verb = "creating"; +// // status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut()); +// // } + +// // if status != errSecSuccess { +// // return Err(anyhow!("{} password failed: {}", verb, status)); +// // } +// // } +// // Ok(()) +// // } + +// // fn read_credentials(&self, url: &str) -> Result)>> { +// // let url = CFString::from(url); +// // let cf_true = CFBoolean::true_value().as_CFTypeRef(); + +// // unsafe { +// // use security::*; + +// // // Find any credentials for the given server URL. +// // let mut attrs = CFMutableDictionary::with_capacity(5); +// // attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); +// // attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); +// // attrs.set(kSecReturnAttributes as *const _, cf_true); +// // attrs.set(kSecReturnData as *const _, cf_true); + +// // let mut result = CFTypeRef::from(ptr::null()); +// // let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result); +// // match status { +// // security::errSecSuccess => {} +// // security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None), +// // _ => return Err(anyhow!("reading password failed: {}", status)), +// // } + +// // let result = CFType::wrap_under_create_rule(result) +// // .downcast::() +// // .ok_or_else(|| anyhow!("keychain item was not a dictionary"))?; +// // let username = result +// // .find(kSecAttrAccount as *const _) +// // .ok_or_else(|| anyhow!("account was missing from keychain item"))?; +// // let username = CFType::wrap_under_get_rule(*username) +// // .downcast::() +// // .ok_or_else(|| anyhow!("account was not a string"))?; +// // let password = result +// // .find(kSecValueData as *const _) +// // .ok_or_else(|| anyhow!("password was missing from keychain item"))?; +// // let password = CFType::wrap_under_get_rule(*password) +// // .downcast::() +// // .ok_or_else(|| anyhow!("password was not a string"))?; + +// // Ok(Some((username.to_string(), password.bytes().to_vec()))) +// // } +// // } + +// // fn delete_credentials(&self, url: &str) -> Result<()> { +// // let url = CFString::from(url); + +// // unsafe { +// // use security::*; + +// // let mut query_attrs = CFMutableDictionary::with_capacity(2); +// // query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); +// // query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + +// // let status = SecItemDelete(query_attrs.as_concrete_TypeRef()); + +// // if status != errSecSuccess { +// // return Err(anyhow!("delete password failed: {}", status)); +// // } +// // } +// // Ok(()) +// // } + +// // fn set_cursor_style(&self, style: CursorStyle) { +// // unsafe { +// // let new_cursor: id = match style { +// // CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor], +// // CursorStyle::ResizeLeftRight => { +// // msg_send![class!(NSCursor), resizeLeftRightCursor] +// // } +// // CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor], +// // CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor], +// // CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor], +// // }; + +// // let old_cursor: id = msg_send![class!(NSCursor), currentCursor]; +// // if new_cursor != old_cursor { +// // let _: () = msg_send![new_cursor, set]; +// // } +// // } +// // } + +// // fn should_auto_hide_scrollbars(&self) -> bool { +// // #[allow(non_upper_case_globals)] +// // const NSScrollerStyleOverlay: NSInteger = 1; + +// // unsafe { +// // let style: NSInteger = msg_send![class!(NSScroller), preferredScrollerStyle]; +// // style == NSScrollerStyleOverlay +// // } +// // } + +// // fn local_timezone(&self) -> UtcOffset { +// // unsafe { +// // let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone]; +// // let seconds_from_gmt: NSInteger = msg_send![local_timezone, secondsFromGMT]; +// // UtcOffset::from_whole_seconds(seconds_from_gmt.try_into().unwrap()).unwrap() +// // } +// // } + +// // fn path_for_auxiliary_executable(&self, name: &str) -> Result { +// // unsafe { +// // let bundle: id = NSBundle::mainBundle(); +// // if bundle.is_null() { +// // Err(anyhow!("app is not running inside a bundle")) +// // } else { +// // let name = ns_string(name); +// // let url: id = msg_send![bundle, URLForAuxiliaryExecutable: name]; +// // if url.is_null() { +// // Err(anyhow!("resource not found")) +// // } else { +// // ns_url_to_path(url) +// // } +// // } +// // } +// // } + +// // fn app_path(&self) -> Result { +// // unsafe { +// // let bundle: id = NSBundle::mainBundle(); +// // if bundle.is_null() { +// // Err(anyhow!("app is not running inside a bundle")) +// // } else { +// // Ok(path_from_objc(msg_send![bundle, bundlePath])) +// // } +// // } +// // } + +// // fn app_version(&self) -> Result { +// // unsafe { +// // let bundle: id = NSBundle::mainBundle(); +// // if bundle.is_null() { +// // Err(anyhow!("app is not running inside a bundle")) +// // } else { +// // let version: id = msg_send![bundle, objectForInfoDictionaryKey: ns_string("CFBundleShortVersionString")]; +// // let len = msg_send![version, lengthOfBytesUsingEncoding: NSUTF8StringEncoding]; +// // let bytes = version.UTF8String() as *const u8; +// // let version = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap(); +// // version.parse() +// // } +// // } +// // } + +// // fn os_name(&self) -> &'static str { +// // "macOS" +// // } + +// // fn os_version(&self) -> Result { +// // unsafe { +// // let process_info = NSProcessInfo::processInfo(nil); +// // let version = process_info.operatingSystemVersion(); +// // Ok(AppVersion { +// // major: version.majorVersion as usize, +// // minor: version.minorVersion as usize, +// // patch: version.patchVersion as usize, +// // }) +// // } +// // } + +// // fn restart(&self) { +// // use std::os::unix::process::CommandExt as _; + +// // let app_pid = std::process::id().to_string(); +// // let app_path = self +// // .app_path() +// // .ok() +// // // When the app is not bundled, `app_path` returns the +// // // directory containing the executable. Disregard this +// // // and get the path to the executable itself. +// // .and_then(|path| (path.extension()?.to_str()? == "app").then_some(path)) +// // .unwrap_or_else(|| std::env::current_exe().unwrap()); + +// // // Wait until this process has exited and then re-open this path. +// // let script = r#" +// // while kill -0 $0 2> /dev/null; do +// // sleep 0.1 +// // done +// // open "$1" +// // "#; + +// // let restart_process = Command::new("/bin/bash") +// // .arg("-c") +// // .arg(script) +// // .arg(app_pid) +// // .arg(app_path) +// // .process_group(0) +// // .spawn(); + +// // match restart_process { +// // Ok(_) => self.quit(), +// // Err(e) => log::error!("failed to spawn restart script: {:?}", e), +// // } +// // } +// // } + +// unsafe fn path_from_objc(path: id) -> PathBuf { +// let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding]; +// let bytes = path.UTF8String() as *const u8; +// let path = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap(); +// PathBuf::from(path) +// } + +// unsafe fn get_foreground_platform(object: &mut Object) -> &MacForegroundPlatform { +// let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR); +// assert!(!platform_ptr.is_null()); +// &*(platform_ptr as *const MacForegroundPlatform) +// } + +// extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) { +// unsafe { +// if let Some(event) = Event::from_native(native_event, None) { +// let platform = get_foreground_platform(this); +// if let Some(callback) = platform.0.borrow_mut().event.as_mut() { +// if callback(event) { +// return; +// } +// } +// } +// msg_send![super(this, class!(NSApplication)), sendEvent: native_event] +// } +// } + +// extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) { +// unsafe { +// let app: id = msg_send![APP_CLASS, sharedApplication]; +// app.setActivationPolicy_(NSApplicationActivationPolicyRegular); + +// let platform = get_foreground_platform(this); +// let callback = platform.0.borrow_mut().finish_launching.take(); +// if let Some(callback) = callback { +// callback(); +// } +// } +// } + +// extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) { +// if !has_open_windows { +// let platform = unsafe { get_foreground_platform(this) }; +// if let Some(callback) = platform.0.borrow_mut().reopen.as_mut() { +// callback(); +// } +// } +// } + +// extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) { +// let platform = unsafe { get_foreground_platform(this) }; +// if let Some(callback) = platform.0.borrow_mut().become_active.as_mut() { +// callback(); +// } +// } + +// extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) { +// let platform = unsafe { get_foreground_platform(this) }; +// if let Some(callback) = platform.0.borrow_mut().resign_active.as_mut() { +// callback(); +// } +// } + +// extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) { +// let platform = unsafe { get_foreground_platform(this) }; +// if let Some(callback) = platform.0.borrow_mut().quit.as_mut() { +// callback(); +// } +// } + +// extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { +// let urls = unsafe { +// (0..urls.count()) +// .into_iter() +// .filter_map(|i| { +// let url = urls.objectAtIndex(i); +// match CStr::from_ptr(url.absoluteString().UTF8String() as *mut c_char).to_str() { +// Ok(string) => Some(string.to_string()), +// Err(err) => { +// log::error!("error converting path to string: {}", err); +// None +// } +// } +// }) +// .collect::>() +// }; +// let platform = unsafe { get_foreground_platform(this) }; +// if let Some(callback) = platform.0.borrow_mut().open_urls.as_mut() { +// callback(urls); +// } +// } + +// extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { +// unsafe { +// let platform = get_foreground_platform(this); +// let mut platform = platform.0.borrow_mut(); +// if let Some(mut callback) = platform.menu_command.take() { +// let tag: NSInteger = msg_send![item, tag]; +// let index = tag as usize; +// if let Some(action) = platform.menu_actions.get(index) { +// callback(action.as_ref()); +// } +// platform.menu_command = Some(callback); +// } +// } +// } + +// extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool { +// unsafe { +// let mut result = false; +// let platform = get_foreground_platform(this); +// let mut platform = platform.0.borrow_mut(); +// if let Some(mut callback) = platform.validate_menu_command.take() { +// let tag: NSInteger = msg_send![item, tag]; +// let index = tag as usize; +// if let Some(action) = platform.menu_actions.get(index) { +// result = callback(action.as_ref()); +// } +// platform.validate_menu_command = Some(callback); +// } +// result +// } +// } + +// extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) { +// unsafe { +// let platform = get_foreground_platform(this); +// let mut platform = platform.0.borrow_mut(); +// if let Some(mut callback) = platform.will_open_menu.take() { +// callback(); +// platform.will_open_menu = Some(callback); +// } +// } +// } + +// unsafe fn ns_string(string: &str) -> id { +// NSString::alloc(nil).init_str(string).autorelease() +// } + +// unsafe fn ns_url_to_path(url: id) -> Result { +// let path: *mut c_char = msg_send![url, fileSystemRepresentation]; +// if path.is_null() { +// Err(anyhow!( +// "url is not a file path: {}", +// CStr::from_ptr(url.absoluteString().UTF8String()).to_string_lossy() +// )) +// } else { +// Ok(PathBuf::from(OsStr::from_bytes( +// CStr::from_ptr(path).to_bytes(), +// ))) +// } +// } + +// mod security { +// #![allow(non_upper_case_globals)] +// use super::*; + +// #[link(name = "Security", kind = "framework")] +// extern "C" { +// pub static kSecClass: CFStringRef; +// pub static kSecClassInternetPassword: CFStringRef; +// pub static kSecAttrServer: CFStringRef; +// pub static kSecAttrAccount: CFStringRef; +// pub static kSecValueData: CFStringRef; +// pub static kSecReturnAttributes: CFStringRef; +// pub static kSecReturnData: CFStringRef; + +// pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus; +// pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus; +// pub fn SecItemDelete(query: CFDictionaryRef) -> OSStatus; +// pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus; +// } + +// pub const errSecSuccess: OSStatus = 0; +// pub const errSecUserCanceled: OSStatus = -128; +// pub const errSecItemNotFound: OSStatus = -25300; +// } + +// #[cfg(test)] +// mod tests { +// use crate::platform::Platform; + +// use super::*; + +// #[test] +// fn test_clipboard() { +// let platform = build_platform(); +// assert_eq!(platform.read_from_clipboard(), None); + +// let item = ClipboardItem::new("1".to_string()); +// platform.write_to_clipboard(item.clone()); +// assert_eq!(platform.read_from_clipboard(), Some(item)); + +// let item = ClipboardItem::new("2".to_string()).with_metadata(vec![3, 4]); +// platform.write_to_clipboard(item.clone()); +// assert_eq!(platform.read_from_clipboard(), Some(item)); + +// let text_from_other_app = "text from other app"; +// unsafe { +// let bytes = NSData::dataWithBytes_length_( +// nil, +// text_from_other_app.as_ptr() as *const c_void, +// text_from_other_app.len() as u64, +// ); +// platform +// .pasteboard +// .setData_forType(bytes, NSPasteboardTypeString); +// } +// assert_eq!( +// platform.read_from_clipboard(), +// Some(ClipboardItem::new(text_from_other_app.to_string())) +// ); +// } + +// fn build_platform() -> MacPlatform { +// let mut platform = MacPlatform::new(); +// platform.pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) }; +// platform +// } +// } diff --git a/crates/gpui3/src/platform/mac/screen.rs b/crates/gpui3/src/platform/mac/screen.rs new file mode 100644 index 0000000000000000000000000000000000000000..259f4f4a16656c081bf3bd819a14357eb0bcd144 --- /dev/null +++ b/crates/gpui3/src/platform/mac/screen.rs @@ -0,0 +1,145 @@ +use super::ns_string; +use crate::{platform, point, px, size, Bounds, Pixels, PlatformScreen}; +use cocoa::{ + appkit::NSScreen, + base::{id, nil}, + foundation::{NSArray, NSDictionary, NSPoint, NSRect, NSSize}, +}; +use core_foundation::{ + number::{kCFNumberIntType, CFNumberGetValue, CFNumberRef}, + uuid::{CFUUIDGetUUIDBytes, CFUUIDRef}, +}; +use core_graphics::display::CGDirectDisplayID; +use std::{any::Any, ffi::c_void}; +use uuid::Uuid; + +#[link(name = "ApplicationServices", kind = "framework")] +extern "C" { + pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; +} + +#[derive(Debug)] +pub struct MacScreen { + pub(crate) native_screen: id, +} + +impl MacScreen { + /// Get the screen with the given UUID. + pub fn find_by_id(uuid: Uuid) -> Option { + Self::all().find(|screen| platform::MacScreen::display_uuid(screen) == Some(uuid)) + } + + /// Get the primary screen - the one with the menu bar, and whose bottom left + /// corner is at the origin of the AppKit coordinate system. + fn primary() -> Self { + Self::all().next().unwrap() + } + + pub fn all() -> impl Iterator { + unsafe { + let native_screens = NSScreen::screens(nil); + (0..NSArray::count(native_screens)).map(move |ix| MacScreen { + native_screen: native_screens.objectAtIndex(ix), + }) + } + } + + /// Convert the given rectangle in screen coordinates from GPUI's + /// coordinate system to the AppKit coordinate system. + /// + /// In GPUI's coordinates, the origin is at the top left of the primary screen, with + /// the Y axis pointing downward. In the AppKit coordindate system, the origin is at the + /// bottom left of the primary screen, with the Y axis pointing upward. + pub(crate) fn screen_bounds_to_native(bounds: Bounds) -> NSRect { + let primary_screen_height = + px(unsafe { Self::primary().native_screen.frame().size.height } as f32); + + NSRect::new( + NSPoint::new( + bounds.origin.x.into(), + (primary_screen_height - bounds.origin.y - bounds.size.height).into(), + ), + NSSize::new(bounds.size.width.into(), bounds.size.height.into()), + ) + } + + /// Convert the given rectangle in screen coordinates from the AppKit + /// coordinate system to GPUI's coordinate system. + /// + /// In GPUI's coordinates, the origin is at the top left of the primary screen, with + /// the Y axis pointing downward. In the AppKit coordindate system, the origin is at the + /// bottom left of the primary screen, with the Y axis pointing upward. + pub(crate) fn screen_bounds_from_native(rect: NSRect) -> Bounds { + let primary_screen_height = unsafe { Self::primary().native_screen.frame().size.height }; + Bounds { + origin: point( + px(rect.origin.x as f32), + px((primary_screen_height - rect.origin.y - rect.size.height) as f32), + ), + size: size(px(rect.size.width as f32), px(rect.size.height as f32)), + } + } +} + +impl PlatformScreen for MacScreen { + fn as_any(&self) -> &dyn Any { + self + } + + fn display_uuid(&self) -> Option { + unsafe { + // Screen ids are not stable. Further, the default device id is also unstable across restarts. + // CGDisplayCreateUUIDFromDisplayID is stable but not exposed in the bindings we use. + // This approach is similar to that which winit takes + // https://github.com/rust-windowing/winit/blob/402cbd55f932e95dbfb4e8b5e8551c49e56ff9ac/src/platform_impl/macos/monitor.rs#L99 + let device_description = self.native_screen.deviceDescription(); + + let key = ns_string("NSScreenNumber"); + let device_id_obj = device_description.objectForKey_(key); + if device_id_obj.is_null() { + // Under some circumstances, especially display re-arrangements or display locking, we seem to get a null pointer + // to the device id. See: https://linear.app/zed-industries/issue/Z-257/lock-screen-crash-with-multiple-monitors + return None; + } + + let mut device_id: u32 = 0; + CFNumberGetValue( + device_id_obj as CFNumberRef, + kCFNumberIntType, + (&mut device_id) as *mut _ as *mut c_void, + ); + let cfuuid = CGDisplayCreateUUIDFromDisplayID(device_id as CGDirectDisplayID); + if cfuuid.is_null() { + return None; + } + + let bytes = CFUUIDGetUUIDBytes(cfuuid); + Some(Uuid::from_bytes([ + bytes.byte0, + bytes.byte1, + bytes.byte2, + bytes.byte3, + bytes.byte4, + bytes.byte5, + bytes.byte6, + bytes.byte7, + bytes.byte8, + bytes.byte9, + bytes.byte10, + bytes.byte11, + bytes.byte12, + bytes.byte13, + bytes.byte14, + bytes.byte15, + ])) + } + } + + fn bounds(&self) -> Bounds { + unsafe { Self::screen_bounds_from_native(self.native_screen.frame()) } + } + + fn content_bounds(&self) -> Bounds { + unsafe { Self::screen_bounds_from_native(self.native_screen.visibleFrame()) } + } +} diff --git a/crates/gpui3/src/platform/mac/window.rs b/crates/gpui3/src/platform/mac/window.rs new file mode 100644 index 0000000000000000000000000000000000000000..ae7ac3df61b8d29ab4dff79587274913a64c6076 --- /dev/null +++ b/crates/gpui3/src/platform/mac/window.rs @@ -0,0 +1,1619 @@ +use crate::{ + point, px, size, AnyWindowHandle, Bounds, Event, ForegroundExecutor, InputHandler, + KeyDownEvent, Keystroke, MacScreen, Modifiers, ModifiersChangedEvent, MouseButton, + MouseDownEvent, MouseMovedEvent, MouseUpEvent, NSRectExt, Pixels, PlatformScreen, + PlatformTextSystem, PlatformWindow, Point, Size, Timer, WindowAppearance, WindowBounds, + WindowKind, WindowOptions, WindowPromptLevel, +}; +use block::ConcreteBlock; +use cocoa::{ + appkit::{ + CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable, + NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior, + NSWindowStyleMask, NSWindowTitleVisibility, + }, + base::{id, nil}, + foundation::{NSAutoreleasePool, NSInteger, NSPoint, NSRect, NSSize, NSString, NSUInteger}, +}; +use core_graphics::display::CGRect; +use ctor::ctor; +use futures::channel::oneshot; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{Class, Object, Protocol, Sel, BOOL, NO, YES}, + sel, sel_impl, +}; +use raw_window_handle::{ + AppKitDisplayHandle, AppKitWindowHandle, HasRawDisplayHandle, HasRawWindowHandle, + RawDisplayHandle, RawWindowHandle, +}; +use std::{ + any::Any, + cell::{Cell, RefCell}, + ffi::{c_void, CStr}, + mem, + ops::Range, + os::raw::c_char, + ptr, + rc::{Rc, Weak}, + sync::Arc, + time::Duration, +}; + +use super::{ns_string, NSRange}; + +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 NSTrackingMouseEnteredAndExited: NSUInteger = 0x01; +#[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; +#[allow(non_upper_case_globals)] +const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2; + +#[ctor] +unsafe fn build_classes() { + 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); + + decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel)); + + decl.add_method( + sel!(performKeyEquivalent:), + handle_key_equivalent as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + decl.add_method( + sel!(keyDown:), + handle_key_down as extern "C" fn(&Object, Sel, id), + ); + 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!(mouseExited:), + 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!(cancelOperation:), + cancel_operation as extern "C" fn(&Object, Sel, id), + ); + + // decl.add_method( + // sel!(makeBackingLayer), + // make_backing_layer as extern "C" fn(&Object, Sel) -> id, + // ); + + decl.add_protocol(Protocol::get("CALayerDelegate").unwrap()); + decl.add_method( + sel!(viewDidChangeBackingProperties), + view_did_change_backing_properties as extern "C" fn(&Object, Sel), + ); + decl.add_method( + sel!(setFrameSize:), + set_frame_size as extern "C" fn(&Object, Sel, NSSize), + ); + decl.add_method( + sel!(displayLayer:), + display_layer as extern "C" fn(&Object, Sel, id), + ); + + decl.add_protocol(Protocol::get("NSTextInputClient").unwrap()); + decl.add_method( + sel!(validAttributesForMarkedText), + valid_attributes_for_marked_text as extern "C" fn(&Object, Sel) -> id, + ); + decl.add_method( + sel!(hasMarkedText), + has_marked_text as extern "C" fn(&Object, Sel) -> BOOL, + ); + decl.add_method( + sel!(markedRange), + marked_range as extern "C" fn(&Object, Sel) -> NSRange, + ); + decl.add_method( + sel!(selectedRange), + selected_range as extern "C" fn(&Object, Sel) -> NSRange, + ); + decl.add_method( + sel!(firstRectForCharacterRange:actualRange:), + first_rect_for_character_range as extern "C" fn(&Object, Sel, NSRange, id) -> NSRect, + ); + decl.add_method( + sel!(insertText:replacementRange:), + insert_text as extern "C" fn(&Object, Sel, id, NSRange), + ); + decl.add_method( + sel!(setMarkedText:selectedRange:replacementRange:), + set_marked_text as extern "C" fn(&Object, Sel, id, NSRange, NSRange), + ); + decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(&Object, Sel)); + decl.add_method( + sel!(attributedSubstringForProposedRange:actualRange:), + 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( + sel!(doCommandBySelector:), + do_command_by_selector as extern "C" fn(&Object, Sel, Sel), + ); + + decl.add_method( + sel!(acceptsFirstMouse:), + accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL, + ); + + decl.register() + }; +} + +pub fn convert_mouse_position(position: NSPoint, window_height: Pixels) -> Point { + point( + px(position.x as f32), + // MacOS screen coordinates are relative to bottom left + window_height - px(position.y as f32), + ) +} + +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!(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!(windowDidMove:), + window_did_move 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() +} + +///Used to track what the IME does when we send it a keystroke. +///This is only used to handle the case where the IME mysteriously +///swallows certain keys. +/// +///Basically a direct copy of the approach that WezTerm uses in: +///github.com/wez/wezterm : d5755f3e : window/src/os/macos/window.rs +enum ImeState { + Continue, + Acted, + None, +} + +struct InsertText { + replacement_range: Option>, + text: String, +} + +struct WindowState { + handle: AnyWindowHandle, + native_window: id, + kind: WindowKind, + event_callback: Option bool>>, + activate_callback: Option>, + resize_callback: Option>, + fullscreen_callback: Option>, + moved_callback: Option>, + should_close_callback: Option bool>>, + close_callback: Option>, + appearance_changed_callback: Option>, + input_handler: Option>, + pending_key_down: Option<(KeyDownEvent, Option)>, + last_key_equivalent: Option, + synthetic_drag_counter: usize, + executor: Rc, + last_fresh_keydown: Option, + traffic_light_position: Option>, + previous_modifiers_changed_event: Option, + // State tracking what the IME did after the last request + ime_state: ImeState, + // Retains the last IME Text + ime_text: Option, +} + +impl WindowState { + fn move_traffic_light(&self) { + if let Some(traffic_light_position) = self.traffic_light_position { + let titlebar_height = self.titlebar_height(); + + unsafe { + let close_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowCloseButton + ]; + let min_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton + ]; + let zoom_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowZoomButton + ]; + + let mut close_button_frame: CGRect = msg_send![close_button, frame]; + let mut min_button_frame: CGRect = msg_send![min_button, frame]; + let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame]; + let mut origin = point( + traffic_light_position.x, + titlebar_height + - traffic_light_position.y + - px(close_button_frame.size.height as f32), + ); + let button_spacing = + px((min_button_frame.origin.x - close_button_frame.origin.x) as f32); + + close_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into()); + let _: () = msg_send![close_button, setFrame: close_button_frame]; + origin.x += button_spacing; + + min_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into()); + let _: () = msg_send![min_button, setFrame: min_button_frame]; + origin.x += button_spacing; + + zoom_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into()); + let _: () = msg_send![zoom_button, setFrame: zoom_button_frame]; + origin.x += button_spacing; + } + } + } + + fn is_fullscreen(&self) -> bool { + unsafe { + let style_mask = self.native_window.styleMask(); + style_mask.contains(NSWindowStyleMask::NSFullScreenWindowMask) + } + } + + fn bounds(&self) -> WindowBounds { + unsafe { + if self.is_fullscreen() { + return WindowBounds::Fullscreen; + } + + let frame = self.frame(); + let screen_size = self.native_window.screen().visibleFrame().size(); + if frame.size == screen_size { + WindowBounds::Maximized + } else { + WindowBounds::Fixed(frame) + } + } + } + + fn frame(&self) -> Bounds { + unsafe { + let frame = NSWindow::frame(self.native_window); + MacScreen::screen_bounds_from_native(frame) + } + } + + fn content_size(&self) -> Size { + let NSSize { width, height, .. } = + unsafe { NSView::frame(self.native_window.contentView()) }.size; + size(px(width as f32), px(height as f32)) + } + + fn scale_factor(&self) -> f32 { + get_scale_factor(self.native_window) + } + + fn titlebar_height(&self) -> Pixels { + unsafe { + let frame = NSWindow::frame(self.native_window); + let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect]; + px((frame.size.height - content_layout_rect.size.height) as f32) + } + } + + fn to_screen_ns_point(&self, point: Point) -> NSPoint { + unsafe { + let point = NSPoint::new( + point.x.into(), + (self.content_size().height - point.y).into(), + ); + msg_send![self.native_window, convertPointToScreen: point] + } + } +} + +pub struct MacWindow(Rc>); + +impl MacWindow { + pub fn open( + handle: AnyWindowHandle, + options: WindowOptions, + executor: Rc, + text_system: Arc, + ) -> Self { + unsafe { + let pool = NSAutoreleasePool::new(nil); + + 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 = 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_screen_( + NSRect::new(NSPoint::new(0., 0.), NSSize::new(1024., 768.)), + style_mask, + NSBackingStoreBuffered, + NO, + options + .screen + .and_then(|screen| { + Some(screen.as_any().downcast_ref::()?.native_screen) + }) + .unwrap_or(nil), + ); + assert!(!native_window.is_null()); + + let screen = native_window.screen(); + match options.bounds { + WindowBounds::Fullscreen => { + native_window.toggleFullScreen_(nil); + } + WindowBounds::Maximized => { + native_window.setFrame_display_(screen.visibleFrame(), YES); + } + WindowBounds::Fixed(bounds) => { + let bounds = MacScreen::screen_bounds_to_native(bounds); + let screen_bounds = screen.visibleFrame(); + if bounds.intersects(screen_bounds) { + native_window.setFrame_display_(bounds, YES); + } else { + native_window.setFrame_display_(screen_bounds, YES); + } + } + } + + let native_view: id = msg_send![VIEW_CLASS, alloc]; + let native_view = NSView::init(native_view); + + assert!(!native_view.is_null()); + + let window = Self(Rc::new(RefCell::new(WindowState { + handle, + native_window, + kind: options.kind, + event_callback: None, + resize_callback: None, + should_close_callback: None, + close_callback: None, + activate_callback: None, + fullscreen_callback: None, + moved_callback: None, + appearance_changed_callback: None, + input_handler: None, + pending_key_down: None, + last_key_equivalent: None, + synthetic_drag_counter: 0, + executor, + last_fresh_keydown: None, + 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, + }))); + + (*native_window).set_ivar( + WINDOW_STATE_IVAR, + Rc::into_raw(window.0.clone()) as *const c_void, + ); + native_window.setDelegate_(native_window); + (*native_view).set_ivar( + WINDOW_STATE_IVAR, + Rc::into_raw(window.0.clone()) as *const c_void, + ); + + if let Some(title) = options + .titlebar + .as_ref() + .and_then(|t| t.title.as_ref().map(AsRef::as_ref)) + { + native_window.setTitle_(NSString::alloc(nil).init_str(title)); + } + + native_window.setMovable_(options.is_movable as BOOL); + + if options + .titlebar + .map_or(true, |titlebar| titlebar.appears_transparent) + { + native_window.setTitlebarAppearsTransparent_(YES); + native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden); + } + + native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); + native_view.setWantsBestResolutionOpenGLSurface_(YES); + + // From winit crate: On Mojave, views automatically become layer-backed shortly after + // being added to a native_window. Changing the layer-backedness of a view breaks the + // association between the view and its associated OpenGL context. To work around this, + // on we explicitly make the view layer-backed up front so that AppKit doesn't do it + // itself and break the association with its context. + native_view.setWantsLayer(YES); + let _: () = msg_send![ + native_view, + setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize + ]; + + native_window.setContentView_(native_view.autorelease()); + native_window.makeFirstResponder_(native_view); + + if options.center { + native_window.center(); + } + + match options.kind { + WindowKind::Normal => { + native_window.setLevel_(NSNormalWindowLevel); + native_window.setAcceptsMouseMovedEvents_(YES); + } + WindowKind::PopUp => { + // Use a tracking area to allow receiving MouseMoved events even when + // the window or application aren't active, which is often the case + // e.g. for notification windows. + 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: NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect + owner: native_view + userInfo: nil + ]; + let _: () = + msg_send![native_view, addTrackingArea: tracking_area.autorelease()]; + + native_window.setLevel_(NSPopUpWindowLevel); + let _: () = msg_send![ + native_window, + setAnimationBehavior: NSWindowAnimationBehaviorUtilityWindow + ]; + native_window.setCollectionBehavior_( + NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces | + NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary + ); + } + } + if options.focus { + native_window.makeKeyAndOrderFront_(nil); + } else if options.show { + native_window.orderFront_(nil); + } + + window.0.borrow().move_traffic_light(); + pool.drain(); + + window + } + } + + pub fn main_window() -> Option { + unsafe { + let app = NSApplication::sharedApplication(nil); + let main_window: id = msg_send![app, mainWindow]; + if msg_send![main_window, isKindOfClass: WINDOW_CLASS] { + let handle = get_window_state(&*main_window).borrow().handle; + Some(handle) + } else { + None + } + } + } +} + +impl Drop for MacWindow { + fn drop(&mut self) { + let this = self.0.borrow(); + let window = this.native_window; + this.executor + .spawn(async move { + unsafe { + window.close(); + } + }) + .detach(); + } +} + +unsafe impl HasRawWindowHandle for MacWindow { + fn raw_window_handle(&self) -> RawWindowHandle { + let ns_window = self.0.borrow().native_window; + let ns_view = unsafe { ns_window.contentView() }; + let mut handle = AppKitWindowHandle::empty(); + handle.ns_window = ns_window as *mut c_void; + handle.ns_view = ns_view as *mut c_void; + handle.into() + } +} + +unsafe impl HasRawDisplayHandle for MacWindow { + fn raw_display_handle(&self) -> RawDisplayHandle { + AppKitDisplayHandle::empty().into() + } +} + +impl PlatformWindow for MacWindow { + fn bounds(&self) -> WindowBounds { + self.0.as_ref().borrow().bounds() + } + + fn content_size(&self) -> Size { + self.0.as_ref().borrow().content_size().into() + } + + fn scale_factor(&self) -> f32 { + self.0.as_ref().borrow().scale_factor() + } + + fn titlebar_height(&self) -> Pixels { + self.0.as_ref().borrow().titlebar_height() + } + + fn appearance(&self) -> WindowAppearance { + unsafe { + let appearance: id = msg_send![self.0.borrow().native_window, effectiveAppearance]; + WindowAppearance::from_native(appearance) + } + } + + fn screen(&self) -> Rc { + unsafe { + Rc::new(MacScreen { + native_screen: self.0.as_ref().borrow().native_window.screen(), + }) + } + } + + fn mouse_position(&self) -> Point { + let position = unsafe { + self.0 + .borrow() + .native_window + .mouseLocationOutsideOfEventStream() + }; + convert_mouse_position(position, self.content_size().height) + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn set_input_handler(&mut self, input_handler: Box) { + self.0.as_ref().borrow_mut().input_handler = Some(input_handler); + } + + fn prompt( + &self, + level: WindowPromptLevel, + msg: &str, + answers: &[&str], + ) -> oneshot::Receiver { + // macOs applies overrides to modal window buttons after they are added. + // Two most important for this logic are: + // * Buttons with "Cancel" title will be displayed as the last buttons in the modal + // * Last button added to the modal via `addButtonWithTitle` stays focused + // * Focused buttons react on "space"/" " keypresses + // * Usage of `keyEquivalent`, `makeFirstResponder` or `setInitialFirstResponder` does not change the focus + // + // See also https://developer.apple.com/documentation/appkit/nsalert/1524532-addbuttonwithtitle#discussion + // ``` + // By default, the first button has a key equivalent of Return, + // any button with a title of “Cancel” has a key equivalent of Escape, + // and any button with the title “Don’t Save” has a key equivalent of Command-D (but only if it’s not the first button). + // ``` + // + // To avoid situations when the last element added is "Cancel" and it gets the focus + // (hence stealing both ESC and Space shortcuts), we find and add one non-Cancel button + // last, so it gets focus and a Space shortcut. + // This way, "Save this file? Yes/No/Cancel"-ish modals will get all three buttons mapped with a key. + let latest_non_cancel_label = answers + .iter() + .enumerate() + .rev() + .find(|(_, &label)| label != "Cancel") + .filter(|&(label_index, _)| label_index > 0); + + unsafe { + let alert: id = msg_send![class!(NSAlert), alloc]; + let alert: id = msg_send![alert, init]; + let alert_style = match level { + WindowPromptLevel::Info => 1, + WindowPromptLevel::Warning => 0, + WindowPromptLevel::Critical => 2, + }; + let _: () = msg_send![alert, setAlertStyle: alert_style]; + let _: () = msg_send![alert, setMessageText: ns_string(msg)]; + + for (ix, answer) in answers + .iter() + .enumerate() + .filter(|&(ix, _)| Some(ix) != latest_non_cancel_label.map(|(ix, _)| ix)) + { + let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)]; + let _: () = msg_send![button, setTag: ix as NSInteger]; + } + if let Some((ix, answer)) = latest_non_cancel_label { + let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)]; + let _: () = msg_send![button, setTag: ix as NSInteger]; + } + + let (done_tx, done_rx) = oneshot::channel(); + let done_tx = Cell::new(Some(done_tx)); + let block = ConcreteBlock::new(move |answer: NSInteger| { + if let Some(done_tx) = done_tx.take() { + let _ = done_tx.send(answer.try_into().unwrap()); + } + }); + let block = block.copy(); + let native_window = self.0.borrow().native_window; + self.0 + .borrow() + .executor + .spawn(async move { + let _: () = msg_send![ + alert, + beginSheetModalForWindow: native_window + completionHandler: block + ]; + }) + .detach(); + + done_rx + } + } + + fn activate(&self) { + let window = self.0.borrow().native_window; + self.0 + .borrow() + .executor + .spawn(async move { + unsafe { + let _: () = msg_send![window, makeKeyAndOrderFront: nil]; + } + }) + .detach(); + } + + fn set_title(&mut self, title: &str) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let window = self.0.borrow().native_window; + let title = ns_string(title); + let _: () = msg_send![app, changeWindowsItem:window title:title filename:false]; + let _: () = msg_send![window, setTitle: title]; + self.0.borrow().move_traffic_light(); + } + } + + fn set_edited(&mut self, edited: bool) { + unsafe { + let window = self.0.borrow().native_window; + msg_send![window, setDocumentEdited: edited as BOOL] + } + + // Changing the document edited state resets the traffic light position, + // so we have to move it again. + self.0.borrow().move_traffic_light(); + } + + fn show_character_palette(&self) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let window = self.0.borrow().native_window; + let _: () = msg_send![app, orderFrontCharacterPalette: window]; + } + } + + fn minimize(&self) { + let window = self.0.borrow().native_window; + unsafe { + window.miniaturize_(nil); + } + } + + fn zoom(&self) { + let this = self.0.borrow(); + let window = this.native_window; + this.executor + .spawn(async move { + unsafe { + window.zoom_(nil); + } + }) + .detach(); + } + + fn toggle_full_screen(&self) { + let this = self.0.borrow(); + let window = this.native_window; + this.executor + .spawn(async move { + unsafe { + window.toggleFullScreen_(nil); + } + }) + .detach(); + } + + fn on_event(&mut self, callback: Box bool>) { + self.0.as_ref().borrow_mut().event_callback = Some(callback); + } + + fn on_active_status_change(&mut self, callback: Box) { + self.0.as_ref().borrow_mut().activate_callback = Some(callback); + } + + fn on_resize(&mut self, callback: Box) { + self.0.as_ref().borrow_mut().resize_callback = Some(callback); + } + + fn on_fullscreen(&mut self, callback: Box) { + self.0.as_ref().borrow_mut().fullscreen_callback = Some(callback); + } + + fn on_moved(&mut self, callback: Box) { + self.0.as_ref().borrow_mut().moved_callback = Some(callback); + } + + fn on_should_close(&mut self, callback: Box bool>) { + self.0.as_ref().borrow_mut().should_close_callback = Some(callback); + } + + fn on_close(&mut self, callback: Box) { + self.0.as_ref().borrow_mut().close_callback = Some(callback); + } + + fn on_appearance_changed(&mut self, callback: Box) { + self.0.borrow_mut().appearance_changed_callback = Some(callback); + } + + fn is_topmost_for_position(&self, position: Point) -> bool { + let self_borrow = self.0.borrow(); + let self_handle = self_borrow.handle; + + unsafe { + let app = NSApplication::sharedApplication(nil); + + // Convert back to screen coordinates + let screen_point = self_borrow.to_screen_ns_point(position); + + let window_number: NSInteger = msg_send![class!(NSWindow), windowNumberAtPoint:screen_point belowWindowWithWindowNumber:0]; + let top_most_window: id = msg_send![app, windowWithWindowNumber: window_number]; + + let is_panel: BOOL = msg_send![top_most_window, isKindOfClass: PANEL_CLASS]; + let is_window: BOOL = msg_send![top_most_window, isKindOfClass: WINDOW_CLASS]; + if is_panel == YES || is_window == YES { + let topmost_window = get_window_state(&*top_most_window).borrow().handle; + topmost_window == self_handle + } else { + // Someone else's window is on top + false + } + } + } +} + +fn get_scale_factor(native_window: id) -> f32 { + unsafe { + let screen: id = msg_send![native_window, screen]; + NSScreen::backingScaleFactor(screen) as f32 + } +} + +unsafe fn get_window_state(object: &Object) -> Rc> { + let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR); + let rc1 = Rc::from_raw(raw as *mut RefCell); + let rc2 = rc1.clone(); + mem::forget(rc1); + rc2 +} + +unsafe fn drop_window_state(object: &Object) { + let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR); + Rc::from_raw(raw as *mut RefCell); +} + +extern "C" fn yes(_: &Object, _: Sel) -> BOOL { + YES +} + +extern "C" fn dealloc_window(this: &Object, _: Sel) { + unsafe { + drop_window_state(this); + let _: () = msg_send![super(this, class!(NSWindow)), dealloc]; + } +} + +extern "C" fn dealloc_view(this: &Object, _: Sel) { + unsafe { + drop_window_state(this); + let _: () = msg_send![super(this, class!(NSView)), dealloc]; + } +} + +extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> BOOL { + handle_key_event(this, native_event, true) +} + +extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) { + handle_key_event(this, native_event, false); +} + +extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL { + let window_state = unsafe { get_window_state(this) }; + let mut window_state_borrow = window_state.as_ref().borrow_mut(); + + let window_height = window_state_borrow.content_size().height; + let event = unsafe { Event::from_native(native_event, Some(window_height)) }; + + if let Some(Event::KeyDown(event)) = event { + // For certain keystrokes, macOS will first dispatch a "key equivalent" event. + // If that event isn't handled, it will then dispatch a "key down" event. GPUI + // makes no distinction between these two types of events, so we need to ignore + // the "key down" event if we've already just processed its "key equivalent" version. + if key_equivalent { + window_state_borrow.last_key_equivalent = Some(event.clone()); + } else if window_state_borrow.last_key_equivalent.take().as_ref() == Some(&event) { + return NO; + } + + let keydown = event.keystroke.clone(); + let fn_modifier = keydown.modifiers.function; + // Ignore events from held-down keys after some of the initially-pressed keys + // were released. + if event.is_held { + if window_state_borrow.last_fresh_keydown.as_ref() != Some(&keydown) { + return YES; + } + } else { + window_state_borrow.last_fresh_keydown = Some(keydown); + } + window_state_borrow.pending_key_down = Some((event, None)); + drop(window_state_borrow); + + // Send the event to the input context for IME handling, unless the `fn` modifier is + // being pressed. + if !fn_modifier { + unsafe { + let input_context: id = msg_send![this, inputContext]; + let _: BOOL = msg_send![input_context, handleEvent: native_event]; + } + } + + let mut handled = false; + let mut window_state_borrow = window_state.borrow_mut(); + let ime_text = window_state_borrow.ime_text.clone(); + if let Some((event, insert_text)) = window_state_borrow.pending_key_down.take() { + let is_held = event.is_held; + if let Some(mut callback) = window_state_borrow.event_callback.take() { + drop(window_state_borrow); + + let is_composing = + with_input_handler(this, |input_handler| input_handler.marked_text_range()) + .flatten() + .is_some(); + if !is_composing { + // if the IME has changed the key, we'll first emit an event with the character + // generated by the IME system; then fallback to the keystroke if that is not + // handled. + // cases that we have working: + // - " on a brazillian layout by typing + // - ctrl-` on a brazillian layout by typing + // - $ on a czech QWERTY layout by typing + // - 4 on a czech QWERTY layout by typing + // - ctrl-4 on a czech QWERTY layout by typing (or ) + if ime_text.is_some() && ime_text.as_ref() != Some(&event.keystroke.key) { + let event_with_ime_text = KeyDownEvent { + is_held: false, + keystroke: Keystroke { + // we match ctrl because some use-cases need it. + // we don't match alt because it's often used to generate the optional character + // we don't match shift because we're not here with letters (usually) + // we don't match cmd/fn because they don't seem to use IME + modifiers: Default::default(), + key: ime_text.clone().unwrap(), + }, + }; + handled = callback(Event::KeyDown(event_with_ime_text)); + } + if !handled { + // empty key happens when you type a deadkey in input composition. + // (e.g. on a brazillian keyboard typing quote is a deadkey) + if !event.keystroke.key.is_empty() { + handled = callback(Event::KeyDown(event)); + } + } + } + + if !handled { + if let Some(insert) = insert_text { + handled = true; + with_input_handler(this, |input_handler| { + input_handler + .replace_text_in_range(insert.replacement_range, &insert.text) + }); + } else if !is_composing && is_held { + if let Some(last_insert_text) = ime_text { + //MacOS IME is a bit funky, and even when you've told it there's nothing to + //inter it will still swallow certain keys (e.g. 'f', 'j') and not others + //(e.g. 'n'). This is a problem for certain kinds of views, like the terminal + with_input_handler(this, |input_handler| { + if input_handler.selected_text_range().is_none() { + handled = true; + input_handler.replace_text_in_range(None, &last_insert_text) + } + }); + } + } + } + + window_state.borrow_mut().event_callback = Some(callback); + } + } else { + handled = true; + } + + handled as BOOL + } else { + NO + } +} + +extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { + let window_state = unsafe { get_window_state(this) }; + let weak_window_state = Rc::downgrade(&window_state); + let mut window_state_borrow = window_state.as_ref().borrow_mut(); + let is_active = unsafe { window_state_borrow.native_window.isKeyWindow() == YES }; + + let window_height = window_state_borrow.content_size().height; + let event = unsafe { Event::from_native(native_event, Some(window_height)) }; + + if let Some(mut event) = event { + let synthesized_second_event = match &mut event { + Event::MouseDown( + event @ MouseDownEvent { + button: MouseButton::Left, + modifiers: Modifiers { control: true, .. }, + .. + }, + ) => { + *event = MouseDownEvent { + button: MouseButton::Right, + modifiers: Modifiers { + control: false, + ..event.modifiers + }, + click_count: 1, + ..*event + }; + + Some(Event::MouseDown(MouseDownEvent { + button: MouseButton::Right, + ..*event + })) + } + + // Because we map a ctrl-left_down to a right_down -> right_up let's ignore + // the ctrl-left_up to avoid having a mismatch in button down/up events if the + // user is still holding ctrl when releasing the left mouse button + Event::MouseUp(MouseUpEvent { + button: MouseButton::Left, + modifiers: Modifiers { control: true, .. }, + .. + }) => { + window_state_borrow.synthetic_drag_counter += 1; + return; + } + + _ => None, + }; + + match &event { + Event::MouseMoved( + event @ MouseMovedEvent { + pressed_button: Some(_), + .. + }, + ) => { + window_state_borrow.synthetic_drag_counter += 1; + window_state_borrow + .executor + .spawn(synthetic_drag( + weak_window_state, + window_state_borrow.synthetic_drag_counter, + event.clone(), + )) + .detach(); + } + + Event::MouseMoved(_) + if !(is_active || window_state_borrow.kind == WindowKind::PopUp) => + { + return + } + + Event::MouseUp(MouseUpEvent { + button: MouseButton::Left, + .. + }) => { + window_state_borrow.synthetic_drag_counter += 1; + } + + Event::ModifiersChanged(ModifiersChangedEvent { modifiers }) => { + // Only raise modifiers changed event when they have actually changed + if let Some(Event::ModifiersChanged(ModifiersChangedEvent { + modifiers: prev_modifiers, + })) = &window_state_borrow.previous_modifiers_changed_event + { + if prev_modifiers == modifiers { + return; + } + } + + window_state_borrow.previous_modifiers_changed_event = Some(event.clone()); + } + + _ => {} + } + + if let Some(mut callback) = window_state_borrow.event_callback.take() { + drop(window_state_borrow); + callback(event); + if let Some(event) = synthesized_second_event { + callback(event); + } + window_state.borrow_mut().event_callback = Some(callback); + } + } +} + +// Allows us to receive `cmd-.` (the shortcut for closing a dialog) +// https://bugs.eclipse.org/bugs/show_bug.cgi?id=300620#c6 +extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { + let window_state = unsafe { get_window_state(this) }; + let mut window_state_borrow = window_state.as_ref().borrow_mut(); + + let keystroke = Keystroke { + modifiers: Default::default(), + key: ".".into(), + }; + let event = Event::KeyDown(KeyDownEvent { + keystroke: keystroke.clone(), + is_held: false, + }); + + window_state_borrow.last_fresh_keydown = Some(keystroke); + if let Some(mut callback) = window_state_borrow.event_callback.take() { + drop(window_state_borrow); + callback(event); + window_state.borrow_mut().event_callback = Some(callback); + } +} + +extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + window_state.as_ref().borrow().move_traffic_light(); +} + +extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { + window_fullscreen_changed(this, true); +} + +extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) { + window_fullscreen_changed(this, false); +} + +fn window_fullscreen_changed(this: &Object, is_fullscreen: bool) { + let window_state = unsafe { get_window_state(this) }; + let mut window_state_borrow = window_state.as_ref().borrow_mut(); + if let Some(mut callback) = window_state_borrow.fullscreen_callback.take() { + drop(window_state_borrow); + callback(is_fullscreen); + window_state.borrow_mut().fullscreen_callback = Some(callback); + } +} + +extern "C" fn window_did_move(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + let mut window_state_borrow = window_state.as_ref().borrow_mut(); + if let Some(mut callback) = window_state_borrow.moved_callback.take() { + drop(window_state_borrow); + callback(); + window_state.borrow_mut().moved_callback = Some(callback); + } +} + +extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + 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(); + if let Some(mut callback) = window_state_borrow.activate_callback.take() { + drop(window_state_borrow); + callback(is_active); + window_state.borrow_mut().activate_callback = Some(callback); + }; + }) + .detach(); +} + +extern "C" fn window_should_close(this: &Object, _: Sel, _: id) -> BOOL { + let window_state = unsafe { get_window_state(this) }; + let mut window_state_borrow = window_state.as_ref().borrow_mut(); + if let Some(mut callback) = window_state_borrow.should_close_callback.take() { + drop(window_state_borrow); + let should_close = callback(); + window_state.borrow_mut().should_close_callback = Some(callback); + should_close as BOOL + } else { + YES + } +} + +extern "C" fn close_window(this: &Object, _: Sel) { + unsafe { + let close_callback = { + let window_state = get_window_state(this); + window_state + .as_ref() + .try_borrow_mut() + .ok() + .and_then(|mut window_state| window_state.close_callback.take()) + }; + + if let Some(callback) = close_callback { + callback(); + } + + let _: () = msg_send![super(this, class!(NSWindow)), close]; + } +} + +// 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.renderer.layer().as_ptr() as id +// } + +extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) { + let window_state = unsafe { get_window_state(this) }; + let mut window_state_borrow = window_state.as_ref().borrow_mut(); + + // unsafe { + // let scale_factor = window_state_borrow.scale_factor() as f64; + // let size = window_state_borrow.content_size(); + // let drawable_size: NSSize = NSSize { + // width: f64::from(size.width) * scale_factor, + // height: f64::from(size.height) * scale_factor, + // }; + + // // 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() { + drop(window_state_borrow); + callback(); + window_state.as_ref().borrow_mut().resize_callback = Some(callback); + }; +} + +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.content_size() == size.into() { + return; + } + + unsafe { + let _: () = msg_send![super(this, class!(NSView)), setFrameSize: size]; + } + + // let scale_factor = window_state_borrow.scale_factor() as f64; + // let drawable_size: NSSize = NSSize { + // width: size.width * scale_factor, + // height: size.height * scale_factor, + // }; + // + // unsafe { + // let _: () = msg_send![ + // window_state_borrow.renderer.layer(), + // setDrawableSize: drawable_size + // ]; + // } + + drop(window_state_borrow); + let mut window_state_borrow = window_state.borrow_mut(); + if let Some(mut callback) = window_state_borrow.resize_callback.take() { + drop(window_state_borrow); + callback(); + window_state.borrow_mut().resize_callback = Some(callback); + }; +} + +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() { + // window_state.renderer.render(&scene); + // }; + } +} + +extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id { + unsafe { msg_send![class!(NSArray), array] } +} + +extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL { + with_input_handler(this, |input_handler| input_handler.marked_text_range()) + .flatten() + .is_some() as BOOL +} + +extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange { + with_input_handler(this, |input_handler| input_handler.marked_text_range()) + .flatten() + .map_or(NSRange::invalid(), |range| range.into()) +} + +extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange { + with_input_handler(this, |input_handler| input_handler.selected_text_range()) + .flatten() + .map_or(NSRange::invalid(), |range| range.into()) +} + +extern "C" fn first_rect_for_character_range( + this: &Object, + _: Sel, + range: NSRange, + _: id, +) -> NSRect { + let frame = unsafe { + let window = get_window_state(this).borrow().native_window; + NSView::frame(window) + }; + with_input_handler(this, |input_handler| { + input_handler.bounds_for_range(range.to_range()?) + }) + .flatten() + .map_or( + NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)), + |bounds| { + NSRect::new( + NSPoint::new( + frame.origin.x + bounds.origin.x as f64, + frame.origin.y + frame.size.height - bounds.origin.y as f64, + ), + NSSize::new(bounds.size.width as f64, bounds.size.height as f64), + ) + }, + ) +} + +extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NSRange) { + unsafe { + let window_state = get_window_state(this); + let mut window_state_borrow = window_state.borrow_mut(); + let pending_key_down = window_state_borrow.pending_key_down.take(); + drop(window_state_borrow); + + let is_attributed_string: BOOL = + msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; + let text: id = if is_attributed_string == YES { + msg_send![text, string] + } else { + text + }; + let text = CStr::from_ptr(text.UTF8String() as *mut c_char) + .to_str() + .unwrap(); + let replacement_range = replacement_range.to_range(); + + window_state.borrow_mut().ime_text = Some(text.to_string()); + window_state.borrow_mut().ime_state = ImeState::Acted; + + let is_composing = + with_input_handler(this, |input_handler| input_handler.marked_text_range()) + .flatten() + .is_some(); + + if is_composing || text.chars().count() > 1 || pending_key_down.is_none() { + with_input_handler(this, |input_handler| { + input_handler.replace_text_in_range(replacement_range, text) + }); + } else { + let mut pending_key_down = pending_key_down.unwrap(); + pending_key_down.1 = Some(InsertText { + replacement_range, + text: text.to_string(), + }); + window_state.borrow_mut().pending_key_down = Some(pending_key_down); + } + } +} + +extern "C" fn set_marked_text( + this: &Object, + _: Sel, + text: id, + selected_range: NSRange, + replacement_range: NSRange, +) { + unsafe { + let window_state = get_window_state(this); + window_state.borrow_mut().pending_key_down.take(); + + let is_attributed_string: BOOL = + msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; + let text: id = if is_attributed_string == YES { + msg_send![text, string] + } else { + text + }; + let selected_range = selected_range.to_range(); + let replacement_range = replacement_range.to_range(); + let text = CStr::from_ptr(text.UTF8String() as *mut c_char) + .to_str() + .unwrap(); + + window_state.borrow_mut().ime_state = ImeState::Acted; + window_state.borrow_mut().ime_text = Some(text.to_string()); + + with_input_handler(this, |input_handler| { + input_handler.replace_and_mark_text_in_range(replacement_range, text, selected_range); + }); + } +} + +extern "C" fn unmark_text(this: &Object, _: Sel) { + unsafe { + let state = get_window_state(this); + let mut borrow = state.borrow_mut(); + borrow.ime_state = ImeState::Acted; + borrow.ime_text.take(); + } + + with_input_handler(this, |input_handler| input_handler.unmark_text()); +} + +extern "C" fn attributed_substring_for_proposed_range( + this: &Object, + _: Sel, + range: NSRange, + _actual_range: *mut c_void, +) -> id { + with_input_handler(this, |input_handler| { + let range = range.to_range()?; + if range.is_empty() { + return None; + } + + let selected_text = input_handler.text_for_range(range)?; + unsafe { + let string: id = msg_send![class!(NSAttributedString), alloc]; + let string: id = msg_send![string, initWithString: ns_string(&selected_text)]; + Some(string) + } + }) + .flatten() + .unwrap_or(nil) +} + +extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) { + unsafe { + let state = get_window_state(this); + let mut borrow = state.borrow_mut(); + borrow.ime_state = ImeState::Continue; + borrow.ime_text.take(); + } +} + +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); + } + } +} + +extern "C" fn accepts_first_mouse(this: &Object, _: Sel, _: id) -> BOOL { + unsafe { + let state = get_window_state(this); + let state_borrow = state.as_ref().borrow(); + return if state_borrow.kind == WindowKind::PopUp { + YES + } else { + NO + }; + } +} + +async fn synthetic_drag( + window_state: Weak>, + drag_id: usize, + event: MouseMovedEvent, +) { + loop { + Timer::after(Duration::from_millis(16)).await; + if let Some(window_state) = window_state.upgrade() { + let mut window_state_borrow = window_state.borrow_mut(); + if window_state_borrow.synthetic_drag_counter == drag_id { + if let Some(mut callback) = window_state_borrow.event_callback.take() { + drop(window_state_borrow); + callback(Event::MouseMoved(event.clone())); + window_state.borrow_mut().event_callback = Some(callback); + } + } else { + break; + } + } + } +} + +fn with_input_handler(window: &Object, f: F) -> Option +where + F: FnOnce(&mut dyn InputHandler) -> R, +{ + let window_state = unsafe { get_window_state(window) }; + let mut window_state_borrow = window_state.as_ref().borrow_mut(); + if let Some(mut input_handler) = window_state_borrow.input_handler.take() { + drop(window_state_borrow); + let result = f(input_handler.as_mut()); + window_state.borrow_mut().input_handler = Some(input_handler); + Some(result) + } else { + None + } +} diff --git a/crates/gpui3/src/platform/mac/window_appearence.rs b/crates/gpui3/src/platform/mac/window_appearence.rs new file mode 100644 index 0000000000000000000000000000000000000000..2edc896289ef8056424a0399d38ff937155adad2 --- /dev/null +++ b/crates/gpui3/src/platform/mac/window_appearence.rs @@ -0,0 +1,35 @@ +use crate::WindowAppearance; +use cocoa::{ + appkit::{NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight}, + base::id, + foundation::NSString, +}; +use objc::{msg_send, sel, sel_impl}; +use std::ffi::CStr; + +impl WindowAppearance { + 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/gpui3/src/platform/test.rs b/crates/gpui3/src/platform/test.rs index a62db5b2f1bfccbf8c8860eb953852ee578c40c7..f0520325910d695ce66ab2b1cd9762f4bd986610 100644 --- a/crates/gpui3/src/platform/test.rs +++ b/crates/gpui3/src/platform/test.rs @@ -9,7 +9,7 @@ impl TestPlatform { } impl Platform for TestPlatform { - fn font_system(&self) -> std::sync::Arc { + fn font_system(&self) -> std::sync::Arc { todo!() } @@ -20,4 +20,8 @@ impl Platform for TestPlatform { ) -> Box { todo!() } + + fn dispatcher(&self) -> std::sync::Arc { + todo!() + } } diff --git a/crates/gpui3/src/style.rs b/crates/gpui3/src/style.rs index 49c5ebe8ec3d41e83c4abc3b4c109d2af882f27c..fbd7d5fcea3f6fd2a020867e68e87f8c3de9da89 100644 --- a/crates/gpui3/src/style.rs +++ b/crates/gpui3/src/style.rs @@ -3,7 +3,7 @@ use super::{ Hsla, Length, Pixels, Point, PointRefinement, Rems, Result, RunStyle, SharedString, Size, SizeRefinement, ViewContext, WindowContext, }; -use crate::{FontCache}; +use crate::FontCache; use refineable::Refineable; pub use taffy::style::{ AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent, diff --git a/crates/gpui3/src/text.rs b/crates/gpui3/src/text.rs index 0e6d7500ee1f8fb247f4540f9022f4eb3b92631f..e5a8da3c72a71eca23b22cd7d92523aa1d106858 100644 --- a/crates/gpui3/src/text.rs +++ b/crates/gpui3/src/text.rs @@ -1,7 +1,7 @@ use crate::{black, px}; use super::{ - point, Bounds, FontId, Glyph, Hsla, Pixels, PlatformFontSystem, Point, UnderlineStyle, + point, Bounds, FontId, Glyph, Hsla, Pixels, PlatformTextSystem, Point, UnderlineStyle, WindowContext, }; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; @@ -17,7 +17,7 @@ use std::{ pub struct TextLayoutCache { prev_frame: Mutex>>, curr_frame: RwLock>>, - fonts: Arc, + fonts: Arc, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -28,7 +28,7 @@ pub struct RunStyle { } impl TextLayoutCache { - pub fn new(fonts: Arc) -> Self { + pub fn new(fonts: Arc) -> Self { Self { prev_frame: Mutex::new(HashMap::new()), curr_frame: RwLock::new(HashMap::new()), @@ -520,7 +520,7 @@ impl Boundary { } pub struct LineWrapper { - font_system: Arc, + font_system: Arc, pub(crate) font_id: FontId, pub(crate) font_size: Pixels, cached_ascii_char_widths: [Option; 128], @@ -533,7 +533,7 @@ impl LineWrapper { pub fn new( font_id: FontId, font_size: Pixels, - font_system: Arc, + font_system: Arc, ) -> Self { Self { font_system, diff --git a/crates/gpui3/src/util.rs b/crates/gpui3/src/util.rs new file mode 100644 index 0000000000000000000000000000000000000000..11cdfdc3ab7db1862c1e5e0827e94ebbbcc4e3d8 --- /dev/null +++ b/crates/gpui3/src/util.rs @@ -0,0 +1,49 @@ +use smol::future::FutureExt; +use std::{future::Future, time::Duration}; +pub use util::*; + +pub fn post_inc(value: &mut usize) -> usize { + let prev = *value; + *value += 1; + prev +} + +pub async fn timeout(timeout: Duration, f: F) -> Result +where + F: Future, +{ + let timer = async { + smol::Timer::after(timeout).await; + Err(()) + }; + let future = async move { Ok(f.await) }; + timer.race(future).await +} + +#[cfg(any(test, feature = "test"))] +pub struct CwdBacktrace<'a>(pub &'a backtrace::Backtrace); + +#[cfg(any(test, feature = "test"))] +impl<'a> std::fmt::Debug for CwdBacktrace<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use backtrace::{BacktraceFmt, BytesOrWideString}; + + let cwd = std::env::current_dir().unwrap(); + let cwd = cwd.parent().unwrap(); + let mut print_path = |fmt: &mut std::fmt::Formatter<'_>, path: BytesOrWideString<'_>| { + std::fmt::Display::fmt(&path, fmt) + }; + let mut fmt = BacktraceFmt::new(f, backtrace::PrintFmt::Full, &mut print_path); + for frame in self.0.frames() { + let mut formatted_frame = fmt.frame(); + if frame + .symbols() + .iter() + .any(|s| s.filename().map_or(false, |f| f.starts_with(&cwd))) + { + formatted_frame.backtrace_frame(frame)?; + } + } + fmt.finish() + } +} diff --git a/crates/gpui3/src/window.rs b/crates/gpui3/src/window.rs index 8b3389260173959ecd358104059e5241d389f5d9..999d3df8f28245af22297bbc56dbdec52699ba66 100644 --- a/crates/gpui3/src/window.rs +++ b/crates/gpui3/src/window.rs @@ -1,3 +1,5 @@ +use crate::PlatformWindow; + use super::{ px, taffy::LayoutId, AppContext, Bounds, Context, EntityId, Handle, Pixels, Reference, Style, TaffyLayoutEngine, @@ -13,17 +15,19 @@ pub struct AnyWindow {} pub struct Window { id: WindowId, + platform_window: Box, rem_size: Pixels, layout_engine: TaffyLayoutEngine, pub(crate) root_view: Option>, } impl Window { - pub fn new(id: WindowId) -> Window { + pub fn new(id: WindowId, platform_window: Box) -> Window { Window { id, - layout_engine: TaffyLayoutEngine::new(), + platform_window, rem_size: px(16.), + layout_engine: TaffyLayoutEngine::new(), root_view: None, } } @@ -239,6 +243,7 @@ impl Into for WindowHandle { } } +#[derive(Copy, Clone, PartialEq, Eq)] pub struct AnyWindowHandle { id: WindowId, state_type: TypeId,