Detailed changes
@@ -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",
]
@@ -2,30 +2,83 @@
name = "gpui3"
version = "0.1.0"
edition = "2021"
+authors = ["Nathan Sobo <nathansobo@gmail.com>"]
+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"
@@ -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");
+}
@@ -51,9 +51,9 @@ impl AppContext {
) -> WindowHandle<S> {
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));
@@ -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<dyn PlatformDispatcher>,
+ _not_send_or_sync: PhantomData<Rc<()>>,
+ },
+ #[cfg(any(test, feature = "test"))]
+ Deterministic {
+ cx_id: usize,
+ executor: Arc<Deterministic>,
+ },
+}
+
+pub enum BackgroundExecutor {
+ #[cfg(any(test, feature = "test"))]
+ Deterministic { executor: Arc<Deterministic> },
+ Production {
+ executor: Arc<smol::Executor<'static>>,
+ _stop: channel::Sender<()>,
+ },
+}
+
+type AnyLocalFuture = Pin<Box<dyn 'static + Future<Output = Box<dyn Any + 'static>>>>;
+type AnyFuture = Pin<Box<dyn 'static + Send + Future<Output = Box<dyn Any + Send + 'static>>>>;
+type AnyTask = async_task::Task<Box<dyn Any + Send + 'static>>;
+type AnyLocalTask = async_task::Task<Box<dyn Any + 'static>>;
+
+#[must_use]
+pub enum Task<T> {
+ Ready(Option<T>),
+ Local {
+ any_task: AnyLocalTask,
+ result_type: PhantomData<T>,
+ },
+ Send {
+ any_task: AnyTask,
+ result_type: PhantomData<T>,
+ },
+}
+
+unsafe impl<T: Send> Send for Task<T> {}
+
+#[cfg(any(test, feature = "test"))]
+struct DeterministicState {
+ rng: rand::prelude::StdRng,
+ seed: u64,
+ scheduled_from_foreground: collections::HashMap<usize, Vec<ForegroundRunnable>>,
+ scheduled_from_background: Vec<BackgroundRunnable>,
+ forbid_parking: bool,
+ block_on_ticks: std::ops::RangeInclusive<usize>,
+ now: std::time::Instant,
+ next_timer_id: usize,
+ pending_timers: Vec<(usize, std::time::Instant, postage::barrier::Sender)>,
+ waiting_backtrace: Option<backtrace::Backtrace>,
+ next_runnable_id: usize,
+ poll_history: Vec<ExecutorEvent>,
+ previous_poll_history: Option<Vec<ExecutorEvent>>,
+ enable_runnable_backtraces: bool,
+ runnable_backtraces: collections::HashMap<usize, backtrace::Backtrace>,
+}
+
+#[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<parking_lot::Mutex<DeterministicState>>,
+ parker: parking_lot::Mutex<parking::Parker>,
+}
+
+#[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<parking_lot::Mutex<DeterministicState>>,
+}
+
+#[cfg(any(test, feature = "test"))]
+impl Deterministic {
+ pub fn new(seed: u64) -> Arc<Self> {
+ 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<ExecutorEvent> {
+ self.state.lock().poll_history.clone()
+ }
+
+ pub fn set_previous_execution_history(&self, history: Option<Vec<ExecutorEvent>>) {
+ 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<Self>) -> Arc<BackgroundExecutor> {
+ Arc::new(BackgroundExecutor::Deterministic {
+ executor: self.clone(),
+ })
+ }
+
+ pub fn build_foreground(self: &Arc<Self>, id: usize) -> Rc<ForegroundExecutor> {
+ 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<dyn 'a + Future<Output = Box<dyn Any>>>>,
+ ) -> Box<dyn Any> {
+ 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<std::sync::atomic::AtomicBool>,
+ mut main_task: Option<&mut AnyLocalTask>,
+ ) -> Option<Box<dyn Any>> {
+ 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::<Vec<_>>();
+ 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<F, T>(&self, future: &mut F, max_ticks: usize) -> Option<T>
+ where
+ F: Unpin + Future<Output = T>,
+ {
+ 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::<Vec<_>>();
+ 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<Self::Output> {
+ 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<dyn PlatformDispatcher>) -> Result<Self> {
+ 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<T: 'static>(&self, future: impl Future<Output = T> + 'static) -> Task<T> {
+ 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<dyn PlatformDispatcher>,
+ ) -> 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<T: 'static>(&self, future: impl Future<Output = T>) -> T {
+ let future = async move { Box::new(future.await) as Box<dyn Any> }.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<usize>) {
+ 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<T, F>(&self, future: F) -> Task<T>
+ where
+ T: 'static + Send,
+ F: Send + Future<Output = T> + '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<F, T>(&self, future: F) -> T
+ where
+ F: Future<Output = T>,
+ {
+ 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<F, T>(
+ &self,
+ timeout: Duration,
+ future: F,
+ ) -> Result<T, impl Future<Output = T>>
+ where
+ T: 'static,
+ F: 'static + Unpin + Future<Output = T>,
+ {
+ 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<Self>, 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::<Vec<_>>();
+ 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<Target = rand::prelude::StdRng> {
+ 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<BackgroundExecutor>,
+ futures: Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
+ tx: Option<mpsc::Sender<()>>,
+ rx: mpsc::Receiver<()>,
+ _phantom: PhantomData<&'a ()>,
+}
+
+impl<'a> Scope<'a> {
+ fn new(executor: Arc<BackgroundExecutor>) -> Self {
+ let (tx, rx) = mpsc::channel(1);
+ Self {
+ executor,
+ tx: Some(tx),
+ rx,
+ futures: Default::default(),
+ _phantom: PhantomData,
+ }
+ }
+
+ pub fn spawn<F>(&mut self, f: F)
+ where
+ F: Future<Output = ()> + 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<Box<dyn Future<Output = ()> + Send + 'a>>,
+ Pin<Box<dyn Future<Output = ()> + 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<T> Task<T> {
+ 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<T: 'static, E: 'static + Display> Task<Result<T, E>> {
+// #[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<T: Send> Task<T> {
+ fn send(any_task: AnyTask) -> Self {
+ Self::Send {
+ any_task,
+ result_type: PhantomData,
+ }
+ }
+}
+
+impl<T: fmt::Debug> fmt::Debug for Task<T> {
+ 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<T: 'static> Future for Task<T> {
+ type Output = T;
+
+ fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+ 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<T, F>(future: F) -> AnyFuture
+where
+ T: 'static + Send,
+ F: Future<Output = T> + Send + 'static,
+{
+ async { Box::new(future.await) as Box<dyn Any + Send> }.boxed()
+}
+
+fn any_local_future<T, F>(future: F) -> AnyLocalFuture
+where
+ T: 'static,
+ F: Future<Output = T> + 'static,
+{
+ async { Box::new(future.await) as Box<dyn Any> }.boxed_local()
+}
@@ -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<FontCacheState>);
pub struct FontCacheState {
- font_system: Arc<dyn PlatformFontSystem>,
+ font_system: Arc<dyn PlatformTextSystem>,
families: Vec<Family>,
default_family: Option<FontFamilyId>,
font_selections: HashMap<FontFamilyId, HashMap<(FontWeight, FontStyle), FontId>>,
@@ -90,7 +90,7 @@ pub struct FontCacheState {
unsafe impl Send for FontCache {}
impl FontCache {
- pub fn new(fonts: Arc<dyn PlatformFontSystem>) -> Self {
+ pub fn new(fonts: Arc<dyn PlatformTextSystem>) -> Self {
Self(RwLock::new(FontCacheState {
font_system: fonts,
families: Default::default(),
@@ -20,6 +20,13 @@ impl<T: Clone + Debug> Point<T> {
pub fn new(x: T, y: T) -> Self {
Self { x, y }
}
+
+ pub fn map<U: Clone + Debug, F: Fn(T) -> U>(&self, f: F) -> Point<U> {
+ Point {
+ x: f(self.x.clone()),
+ y: f(self.y.clone()),
+ }
+ }
}
impl<T: Clone + Debug> Clone for Point<T> {
@@ -42,6 +49,10 @@ pub struct Size<T: Clone + Debug> {
pub height: T,
}
+pub fn size<T: Clone + Debug>(width: T, height: T) -> Size<T> {
+ Size { width, height }
+}
+
impl Size<Length> {
pub fn full() -> Self {
Self {
@@ -157,6 +168,12 @@ impl Edges<Pixels> {
#[repr(transparent)]
pub struct Pixels(pub(crate) f32);
+impl From<Pixels> for f64 {
+ fn from(pixels: Pixels) -> Self {
+ pixels.0.into()
+ }
+}
+
impl Mul<f32> for Pixels {
type Output = Pixels;
@@ -326,11 +343,11 @@ pub fn relative<T: From<DefiniteLength>>(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 {
@@ -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<str> 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<CollabPanel> {
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 {
- Self {}
- }
- }
-
#[test]
fn test() {
let mut cx = AppContext::test();
@@ -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<dyn PlatformFontSystem>;
+ fn dispatcher(&self) -> Arc<dyn PlatformDispatcher>;
+ fn font_system(&self) -> Arc<dyn PlatformTextSystem>;
fn open_window(
&self,
@@ -20,9 +33,53 @@ pub trait Platform {
) -> Box<dyn PlatformWindow>;
}
-pub trait PlatformWindow: HasRawWindowHandle + HasRawDisplayHandle {}
+pub trait PlatformScreen: Debug {
+ fn as_any(&self) -> &dyn Any;
+ fn bounds(&self) -> Bounds<Pixels>;
+ fn content_bounds(&self) -> Bounds<Pixels>;
+ fn display_uuid(&self) -> Option<Uuid>;
+}
+
+pub trait PlatformWindow: HasRawWindowHandle + HasRawDisplayHandle {
+ fn bounds(&self) -> WindowBounds;
+ fn content_size(&self) -> Size<Pixels>;
+ fn scale_factor(&self) -> f32;
+ fn titlebar_height(&self) -> Pixels;
+ fn appearance(&self) -> WindowAppearance;
+ fn screen(&self) -> Rc<dyn PlatformScreen>;
+ fn mouse_position(&self) -> Point<Pixels>;
+ fn as_any_mut(&mut self) -> &mut dyn Any;
+ fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>);
+ fn prompt(
+ &self,
+ level: WindowPromptLevel,
+ msg: &str,
+ answers: &[&str],
+ ) -> oneshot::Receiver<usize>;
+ 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<dyn FnMut(Event) -> bool>);
+ fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>);
+ fn on_resize(&mut self, callback: Box<dyn FnMut()>);
+ fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>);
+ fn on_moved(&mut self, callback: Box<dyn FnMut()>);
+ fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>);
+ fn on_close(&mut self, callback: Box<dyn FnOnce()>);
+ fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>);
+ fn is_topmost_for_position(&self, position: Point<Pixels>) -> 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<Vec<u8>>]) -> anyhow::Result<()>;
fn all_families(&self) -> Vec<String>;
fn load_family(&self, name: &str, features: &FontFeatures) -> anyhow::Result<Vec<FontId>>;
@@ -59,6 +116,21 @@ pub trait PlatformFontSystem: Send + Sync {
) -> Vec<usize>;
}
+pub trait InputHandler {
+ fn selected_text_range(&self) -> Option<Range<usize>>;
+ fn marked_text_range(&self) -> Option<Range<usize>>;
+ fn text_for_range(&self, range_utf16: Range<usize>) -> Option<String>;
+ fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str);
+ fn replace_and_mark_text_in_range(
+ &mut self,
+ range_utf16: Option<Range<usize>>,
+ new_text: &str,
+ new_selected_range: Option<Range<usize>>,
+ );
+ fn unmark_text(&mut self);
+ fn bounds_for_range(&self, range_utf16: Range<usize>) -> Option<Bounds<f32>>;
+}
+
#[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<Rc<dyn PlatformScreen>>,
}
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<SharedString>,
pub appears_transparent: bool,
- pub traffic_light_position: Option<Point<f32>>,
+ pub traffic_light_position: Option<Point<Pixels>>,
}
#[derive(Copy, Clone, Debug)]
@@ -127,5 +200,27 @@ pub enum WindowBounds {
Fullscreen,
#[default]
Maximized,
- Fixed(Bounds<f32>),
+ Fixed(Bounds<Pixels>),
+}
+
+#[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,
}
@@ -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<Pixels>),
+ Lines(Point<f32>),
+}
+
+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<Pixels> {
+ 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<Pixels>,
+ 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<TouchPhase>,
+}
+
+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<Self> {
+ 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<Pixels>,
+ pub modifiers: Modifiers,
+ pub click_count: usize,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct MouseUpEvent {
+ pub button: MouseButton,
+ pub position: Point<Pixels>,
+ pub modifiers: Modifiers,
+ pub click_count: usize,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct MouseUp {
+ pub button: MouseButton,
+ pub position: Point<Pixels>,
+ pub modifiers: Modifiers,
+ pub click_count: usize,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct MouseMovedEvent {
+ pub position: Point<Pixels>,
+ pub pressed_button: Option<MouseButton>,
+ pub modifiers: Modifiers,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct MouseExitedEvent {
+ pub position: Point<Pixels>,
+ pub pressed_button: Option<MouseButton>,
+ 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<Point<Pixels>> {
+ 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),
+ }
+ }
+}
@@ -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<Self> {
+ 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)
+ }
+}
@@ -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<Range<usize>> {
+ if self.is_valid() {
+ let start = self.location as usize;
+ let end = start + self.length as usize;
+ Some(start..end)
+ } else {
+ None
+ }
+ }
+}
+
+impl From<Range<usize>> for NSRange {
+ fn from(range: Range<usize>) -> 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<NSSize> for Size<Pixels> {
+ 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<Pixels>;
+ fn intersects(&self, other: Self) -> bool;
+}
+
+impl NSRectExt for NSRect {
+ fn size(&self) -> Size<Pixels> {
+ 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<PathBuf> {
+ 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(),
+ )))
+ }
+}
@@ -0,0 +1 @@
+#include <dispatch/dispatch.h>
@@ -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();
+ }
+ }
+}
@@ -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<str> {
+ 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<Pixels>) -> Option<Self> {
+ 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()
+ }
+}
@@ -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<MacDispatcher>,
+// fonts: Arc<MacFontSystem>,
+// pasteboard: id,
+// text_hash_pasteboard_type: id,
+// metadata_pasteboard_type: id,
+// become_active: Option<Box<dyn FnMut()>>,
+// resign_active: Option<Box<dyn FnMut()>>,
+// reopen: Option<Box<dyn FnMut()>>,
+// quit: Option<Box<dyn FnMut()>>,
+// event: Option<Box<dyn FnMut(Event) -> bool>>,
+// // menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
+// validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
+// will_open_menu: Option<Box<dyn FnMut()>>,
+// open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
+// finish_launching: Option<Box<dyn FnOnce()>>,
+// // menu_actions: Vec<Box<dyn Action>>,
+// // foreground: Rc<executor::Foreground>,
+// }
+
+// impl MacForegroundPlatform {
+// pub fn new(foreground: Rc<executor::Foreground>) -> 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<Menu>,
+// delegate: id,
+// actions: &mut Vec<Box<dyn Action>>,
+// 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<Box<dyn Action>>,
+// // 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<dyn FnMut()>) {
+// // self.0.borrow_mut().become_active = Some(callback);
+// // }
+
+// // fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
+// // self.0.borrow_mut().resign_active = Some(callback);
+// // }
+
+// // fn on_quit(&self, callback: Box<dyn FnMut()>) {
+// // self.0.borrow_mut().quit = Some(callback);
+// // }
+
+// // fn on_reopen(&self, callback: Box<dyn FnMut()>) {
+// // self.0.borrow_mut().reopen = Some(callback);
+// // }
+
+// // fn on_event(&self, callback: Box<dyn FnMut(platform::Event) -> bool>) {
+// // self.0.borrow_mut().event = Some(callback);
+// // }
+
+// // fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
+// // self.0.borrow_mut().open_urls = Some(callback);
+// // }
+
+// // fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
+// // 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::<c_void>());
+// // (*app.delegate()).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
+// // }
+// // }
+
+// // fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>) {
+// // self.0.borrow_mut().menu_command = Some(callback);
+// // }
+
+// // fn on_will_open_menu(&self, callback: Box<dyn FnMut()>) {
+// // self.0.borrow_mut().will_open_menu = Some(callback);
+// // }
+
+// // fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
+// // self.0.borrow_mut().validate_menu_command = Some(callback);
+// // }
+
+// // fn set_menus(&self, menus: Vec<Menu>, 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<Option<Vec<PathBuf>>> {
+// // 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<Option<PathBuf>> {
+// // 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<dyn platform::Dispatcher> {
+// // self.dispatcher.clone()
+// // }
+
+// // fn fonts(&self) -> Arc<dyn platform::FontSystem> {
+// // 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<Rc<dyn platform::Screen>> {
+// // Screen::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>)
+// // }
+
+// // fn screens(&self) -> Vec<Rc<dyn platform::Screen>> {
+// // Screen::all()
+// // .into_iter()
+// // .map(|screen| Rc::new(screen) as Rc<_>)
+// // .collect()
+// // }
+
+// // fn open_window(
+// // &self,
+// // handle: AnyWindowHandle,
+// // options: platform::WindowOptions,
+// // executor: Rc<executor::Foreground>,
+// // ) -> Box<dyn platform::Window> {
+// // Box::new(MacWindow::open(handle, options, executor, self.fonts()))
+// // }
+
+// // fn main_window(&self) -> Option<AnyWindowHandle> {
+// // MacWindow::main_window()
+// // }
+
+// // fn add_status_item(&self, _handle: AnyWindowHandle) -> Box<dyn platform::Window> {
+// // 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<ClipboardItem> {
+// // 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<Option<(String, Vec<u8>)>> {
+// // 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::<CFDictionary>()
+// // .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::<CFString>()
+// // .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::<CFData>()
+// // .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<PathBuf> {
+// // 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<PathBuf> {
+// // 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<platform::AppVersion> {
+// // 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<crate::platform::AppVersion> {
+// // 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::<Vec<_>>()
+// };
+// 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<PathBuf> {
+// 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
+// }
+// }
@@ -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> {
+ 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<Item = Self> {
+ 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<Pixels>) -> 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<Pixels> {
+ 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<uuid::Uuid> {
+ 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<Pixels> {
+ unsafe { Self::screen_bounds_from_native(self.native_screen.frame()) }
+ }
+
+ fn content_bounds(&self) -> Bounds<Pixels> {
+ unsafe { Self::screen_bounds_from_native(self.native_screen.visibleFrame()) }
+ }
+}
@@ -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<Pixels> {
+ 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<Range<usize>>,
+ text: String,
+}
+
+struct WindowState {
+ handle: AnyWindowHandle,
+ native_window: id,
+ kind: WindowKind,
+ event_callback: Option<Box<dyn FnMut(Event) -> bool>>,
+ activate_callback: Option<Box<dyn FnMut(bool)>>,
+ resize_callback: Option<Box<dyn FnMut()>>,
+ fullscreen_callback: Option<Box<dyn FnMut(bool)>>,
+ moved_callback: Option<Box<dyn FnMut()>>,
+ should_close_callback: Option<Box<dyn FnMut() -> bool>>,
+ close_callback: Option<Box<dyn FnOnce()>>,
+ appearance_changed_callback: Option<Box<dyn FnMut()>>,
+ input_handler: Option<Box<dyn InputHandler>>,
+ pending_key_down: Option<(KeyDownEvent, Option<InsertText>)>,
+ last_key_equivalent: Option<KeyDownEvent>,
+ synthetic_drag_counter: usize,
+ executor: Rc<ForegroundExecutor>,
+ last_fresh_keydown: Option<Keystroke>,
+ traffic_light_position: Option<Point<Pixels>>,
+ previous_modifiers_changed_event: Option<Event>,
+ // State tracking what the IME did after the last request
+ ime_state: ImeState,
+ // Retains the last IME Text
+ ime_text: Option<String>,
+}
+
+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<Pixels> {
+ unsafe {
+ let frame = NSWindow::frame(self.native_window);
+ MacScreen::screen_bounds_from_native(frame)
+ }
+ }
+
+ fn content_size(&self) -> Size<Pixels> {
+ 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<Pixels>) -> 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<RefCell<WindowState>>);
+
+impl MacWindow {
+ pub fn open(
+ handle: AnyWindowHandle,
+ options: WindowOptions,
+ executor: Rc<ForegroundExecutor>,
+ text_system: Arc<dyn PlatformTextSystem>,
+ ) -> 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::<MacScreen>()?.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<AnyWindowHandle> {
+ 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<Pixels> {
+ 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<dyn PlatformScreen> {
+ unsafe {
+ Rc::new(MacScreen {
+ native_screen: self.0.as_ref().borrow().native_window.screen(),
+ })
+ }
+ }
+
+ fn mouse_position(&self) -> Point<Pixels> {
+ 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<dyn InputHandler>) {
+ self.0.as_ref().borrow_mut().input_handler = Some(input_handler);
+ }
+
+ fn prompt(
+ &self,
+ level: WindowPromptLevel,
+ msg: &str,
+ answers: &[&str],
+ ) -> oneshot::Receiver<usize> {
+ // 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<dyn FnMut(Event) -> bool>) {
+ self.0.as_ref().borrow_mut().event_callback = Some(callback);
+ }
+
+ fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>) {
+ self.0.as_ref().borrow_mut().activate_callback = Some(callback);
+ }
+
+ fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
+ self.0.as_ref().borrow_mut().resize_callback = Some(callback);
+ }
+
+ fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>) {
+ self.0.as_ref().borrow_mut().fullscreen_callback = Some(callback);
+ }
+
+ fn on_moved(&mut self, callback: Box<dyn FnMut()>) {
+ self.0.as_ref().borrow_mut().moved_callback = Some(callback);
+ }
+
+ fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>) {
+ self.0.as_ref().borrow_mut().should_close_callback = Some(callback);
+ }
+
+ fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
+ self.0.as_ref().borrow_mut().close_callback = Some(callback);
+ }
+
+ fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>) {
+ self.0.borrow_mut().appearance_changed_callback = Some(callback);
+ }
+
+ fn is_topmost_for_position(&self, position: Point<Pixels>) -> 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<RefCell<WindowState>> {
+ let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
+ let rc1 = Rc::from_raw(raw as *mut RefCell<WindowState>);
+ 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<WindowState>);
+}
+
+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 <quote><space>
+ // - ctrl-` on a brazillian layout by typing <ctrl-`>
+ // - $ on a czech QWERTY layout by typing <alt-4>
+ // - 4 on a czech QWERTY layout by typing <shift-4>
+ // - ctrl-4 on a czech QWERTY layout by typing <ctrl-alt-4> (or <ctrl-shift-4>)
+ 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<RefCell<WindowState>>,
+ 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<F, R>(window: &Object, f: F) -> Option<R>
+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
+ }
+}
@@ -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;
+}
@@ -9,7 +9,7 @@ impl TestPlatform {
}
impl Platform for TestPlatform {
- fn font_system(&self) -> std::sync::Arc<dyn crate::PlatformFontSystem> {
+ fn font_system(&self) -> std::sync::Arc<dyn crate::PlatformTextSystem> {
todo!()
}
@@ -20,4 +20,8 @@ impl Platform for TestPlatform {
) -> Box<dyn crate::PlatformWindow> {
todo!()
}
+
+ fn dispatcher(&self) -> std::sync::Arc<dyn crate::PlatformDispatcher> {
+ todo!()
+ }
}
@@ -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,
@@ -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<HashMap<CacheKeyValue, Arc<LineLayout>>>,
curr_frame: RwLock<HashMap<CacheKeyValue, Arc<LineLayout>>>,
- fonts: Arc<dyn PlatformFontSystem>,
+ fonts: Arc<dyn PlatformTextSystem>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -28,7 +28,7 @@ pub struct RunStyle {
}
impl TextLayoutCache {
- pub fn new(fonts: Arc<dyn PlatformFontSystem>) -> Self {
+ pub fn new(fonts: Arc<dyn PlatformTextSystem>) -> 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<dyn PlatformFontSystem>,
+ font_system: Arc<dyn PlatformTextSystem>,
pub(crate) font_id: FontId,
pub(crate) font_size: Pixels,
cached_ascii_char_widths: [Option<Pixels>; 128],
@@ -533,7 +533,7 @@ impl LineWrapper {
pub fn new(
font_id: FontId,
font_size: Pixels,
- font_system: Arc<dyn PlatformFontSystem>,
+ font_system: Arc<dyn PlatformTextSystem>,
) -> Self {
Self {
font_system,
@@ -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<F, T>(timeout: Duration, f: F) -> Result<T, ()>
+where
+ F: Future<Output = T>,
+{
+ 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()
+ }
+}
@@ -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<dyn PlatformWindow>,
rem_size: Pixels,
layout_engine: TaffyLayoutEngine,
pub(crate) root_view: Option<Box<dyn Any>>,
}
impl Window {
- pub fn new(id: WindowId) -> Window {
+ pub fn new(id: WindowId, platform_window: Box<dyn PlatformWindow>) -> 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<S: 'static> Into<AnyWindowHandle> for WindowHandle<S> {
}
}
+#[derive(Copy, Clone, PartialEq, Eq)]
pub struct AnyWindowHandle {
id: WindowId,
state_type: TypeId,