diff --git a/.vscode/launch.json b/.vscode/launch.json index aea73eeaa235a52aaf2a41ad1d688d3c0276281c..668019e696d7a0b4ac2e05382d46df143bddd53e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,6 +8,9 @@ "type": "lldb", "request": "launch", "name": "Debug executable 'Zed'", + "env": { + "ZED_SERVER_URL": "http://localhost:8080" + }, "cargo": { "args": [ "build", diff --git a/Cargo.lock b/Cargo.lock index a24b7b81edf21c93cbd8cd71fa934dc8daf79f50..562da86feec01872e2eb71718d775b41f454dde7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,6 +162,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "arrayvec" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4dc07131ffa69b8072d35f5007352af944213cde02545e2103680baed38fcd" + [[package]] name = "ascii" version = "1.0.0" @@ -626,7 +632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.5.2", "constant_time_eq", ] @@ -637,7 +643,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.5.2", "cc", "cfg-if 0.1.10", "constant_time_eq", @@ -2145,6 +2151,7 @@ name = "gpui" version = "0.1.0" dependencies = [ "anyhow", + "arrayvec 0.7.1", "async-task", "backtrace", "bindgen", @@ -2160,6 +2167,7 @@ dependencies = [ "font-kit", "foreign-types", "gpui_macros", + "lazy_static", "log", "metal", "num_cpus", @@ -2179,6 +2187,7 @@ dependencies = [ "simplelog", "smallvec", "smol", + "time 0.3.2", "tiny-skia", "tree-sitter", "usvg", @@ -2632,7 +2641,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e30b1df631d23875f230ed3ddd1a88c231f269a04b2044eb6ca87e763b5f4c42" dependencies = [ - "arrayvec", + "arrayvec 0.5.2", ] [[package]] @@ -2665,7 +2674,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" dependencies = [ - "arrayvec", + "arrayvec 0.5.2", "bitflags 1.2.1", "cfg-if 1.0.0", "ryu", @@ -2728,6 +2737,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "lipsum" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ee7271c76a89032dcc7e595c0a739a9c5514eab483deb0e82981fe2098c56a" +dependencies = [ + "rand 0.8.3", + "rand_chacha 0.3.0", +] + [[package]] name = "lock_api" version = "0.4.2" @@ -4021,9 +4040,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "5.9.0" +version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fe1fe6aac5d6bb9e1ffd81002340363272a7648234ec7bdfac5ee202cb65523" +checksum = "1be44a6694859b7cfc955699935944a6844aa9fe416aeda5d40829e3e38dfee6" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -4032,9 +4051,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "5.9.0" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed91c41c42ef7bf687384439c312e75e0da9c149b0390889b94de3c7d9d9e66" +checksum = "f567ca01565c50c67b29e535f5f67b8ea8aeadaeed16a88f10792ab57438b957" dependencies = [ "proc-macro2", "quote", @@ -4045,10 +4064,12 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "5.1.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a512219132473ab0a77b52077059f1c47ce4af7fbdc94503e9862a34422876d" +checksum = "6116e7ab9ea963f60f2f20291d8fcf6c7273192cdd7273b3c80729a9605c97b2" dependencies = [ + "glob 0.3.0", + "sha2", "walkdir", ] @@ -4699,6 +4720,7 @@ dependencies = [ "sqlx-rt 0.5.5", "stringprep", "thiserror", + "time 0.2.25", "url", "webpki", "webpki-roots", @@ -5133,6 +5155,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "time" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0a10c9a9fb3a5dce8c2239ed670f1a2569fcf42da035f5face1b19860d52b0" +dependencies = [ + "libc", +] + [[package]] name = "time-macros" version = "0.1.1" @@ -5163,7 +5194,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bf81f2900d2e235220e6f31ec9f63ade6a7f59090c556d74fe949bb3b15e9fe" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.5.2", "bytemuck", "cfg-if 1.0.0", "png 0.16.8", @@ -5791,7 +5822,7 @@ name = "zed" version = "0.1.0" dependencies = [ "anyhow", - "arrayvec", + "arrayvec 0.7.1", "async-trait", "async-tungstenite", "cargo-bundle", @@ -5817,12 +5848,14 @@ dependencies = [ "seahash", "serde 1.0.125", "serde_json 1.0.64", + "serde_path_to_error", "similar", "simplelog", "smallvec", "smol", "surf", "tempdir", + "time 0.3.2", "tiny_http", "toml 0.5.8", "tree-sitter", @@ -5852,6 +5885,7 @@ dependencies = [ "http-auth-basic", "jwt-simple", "lazy_static", + "lipsum", "oauth2", "oauth2-surf", "parking_lot", @@ -5866,6 +5900,7 @@ dependencies = [ "surf", "tide", "tide-compress", + "time 0.2.25", "toml 0.5.8", "zed", "zrpc", diff --git a/Dockerfile b/Dockerfile index 6f168a9a91e5081b68dd56a8ab805abf67efd9e5..18623704e287191995a432d11da9c14651e2c829 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ RUN --mount=type=cache,target=./script/node_modules \ RUN --mount=type=cache,target=./script/node_modules \ --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=./target \ - cargo build --release --bin zed-server + cargo build --release --package zed-server --bin zed-server # Copy server binary out of cached directory RUN --mount=type=cache,target=./target \ diff --git a/Dockerfile.migrator b/Dockerfile.migrator index 76b7e2b729478e8bae1b5ef2e997118f7d431a29..99c21b2230387b2ab1d31016a5c9494d573d21c0 100644 --- a/Dockerfile.migrator +++ b/Dockerfile.migrator @@ -4,7 +4,7 @@ FROM rust as builder WORKDIR app RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=./target \ - cargo install sqlx-cli --root=/app --target-dir=/app/target --version 0.5.5 + cargo install sqlx-cli --root=/app --target-dir=/app/target --version 0.5.7 FROM debian:buster-slim as runtime RUN apt-get update; \ diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 535fe16155ba9f2a1b16397cf2dcf27f050578cd..11a855cd40c087c4aed6fc278adfbdb7ded92952 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -5,11 +5,13 @@ name = "gpui" version = "0.1.0" [dependencies] +arrayvec = "0.7.1" async-task = "4.0.3" backtrace = "0.3" ctor = "0.1" etagere = "0.2" gpui_macros = { path = "../gpui_macros" } +lazy_static = "1.4.0" log = "0.4" num_cpus = "1.13" ordered-float = "2.1.1" @@ -25,6 +27,7 @@ serde = { version = "1.0.125", features = ["derive"] } serde_json = "1.0.64" smallvec = { version = "1.6", features = ["union"] } smol = "1.2" +time = { version = "0.3" } tiny-skia = "0.5" tree-sitter = "0.19" usvg = "0.14" diff --git a/gpui/examples/text.rs b/gpui/examples/text.rs index 45aa5931129ed43f9e6bf76a1063392827063ed1..6c82b2d88a3df891ce9ae9b120d0e24496f790fb 100644 --- a/gpui/examples/text.rs +++ b/gpui/examples/text.rs @@ -1,6 +1,7 @@ use gpui::{ color::Color, fonts::{Properties, Weight}, + text_layout::RunStyle, DebugContext, Element as _, Quad, }; use log::LevelFilter; @@ -12,7 +13,7 @@ fn main() { gpui::App::new(()).unwrap().run(|cx| { cx.platform().activate(true); - cx.add_window(|_| TextView); + cx.add_window(Default::default(), |_| TextView); }); } @@ -28,7 +29,7 @@ impl gpui::View for TextView { "View" } - fn render(&self, _: &gpui::RenderContext) -> gpui::ElementBox { + fn render(&mut self, _: &mut gpui::RenderContext) -> gpui::ElementBox { TextElement.boxed() } } @@ -46,56 +47,57 @@ impl gpui::Element for TextElement { (constraint.max, ()) } - fn after_layout( - &mut self, - _: pathfinder_geometry::vector::Vector2F, - _: &mut Self::LayoutState, - _: &mut gpui::AfterLayoutContext, - ) { - } - fn paint( &mut self, bounds: RectF, + visible_bounds: RectF, _: &mut Self::LayoutState, cx: &mut gpui::PaintContext, ) -> Self::PaintState { let font_size = 12.; let family = cx.font_cache.load_family(&["SF Pro Display"]).unwrap(); - let normal = cx - .font_cache - .select_font(family, &Default::default()) - .unwrap(); - let bold = cx - .font_cache - .select_font( - family, - &Properties { - weight: Weight::BOLD, - ..Default::default() - }, - ) - .unwrap(); + let normal = RunStyle { + font_id: cx + .font_cache + .select_font(family, &Default::default()) + .unwrap(), + color: Color::default(), + underline: false, + }; + let bold = RunStyle { + font_id: cx + .font_cache + .select_font( + family, + &Properties { + weight: Weight::BOLD, + ..Default::default() + }, + ) + .unwrap(), + color: Color::default(), + underline: false, + }; let text = "Hello world!"; let line = cx.text_layout_cache.layout_str( text, font_size, &[ - (1, normal, Color::default()), - (1, bold, Color::default()), - (1, normal, Color::default()), - (1, bold, Color::default()), - (text.len() - 4, normal, Color::default()), + (1, normal.clone()), + (1, bold.clone()), + (1, normal.clone()), + (1, bold.clone()), + (text.len() - 4, normal.clone()), ], ); cx.scene.push_quad(Quad { - bounds: bounds, + bounds, background: Some(Color::white()), ..Default::default() }); - line.paint(bounds.origin(), bounds, cx); + line.paint(bounds.origin(), visible_bounds, bounds.height(), cx); } fn dispatch_event( diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 579bf5c4682dba2fcfbe15d48166396f3a9ea81d..c1ce8fdba0d266726a8db83eaba36fe3fe3f4de6 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -2,35 +2,37 @@ use crate::{ elements::ElementBox, executor, keymap::{self, Keystroke}, - platform::{self, Platform, PromptLevel, WindowOptions}, + platform::{self, CursorStyle, Platform, PromptLevel, WindowOptions}, presenter::Presenter, util::{post_inc, timeout}, - AssetCache, AssetSource, ClipboardItem, EventContext, FontCache, PathPromptOptions, - TextLayoutCache, + AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache, }; use anyhow::{anyhow, Result}; use async_task::Task; use keymap::MatchResult; -use parking_lot::{Mutex, RwLock}; -use pathfinder_geometry::{rect::RectF, vector::vec2f}; +use parking_lot::Mutex; use platform::Event; use postage::{mpsc, sink::Sink as _, stream::Stream as _}; use smol::prelude::*; use std::{ any::{type_name, Any, TypeId}, cell::RefCell, - collections::{hash_map::Entry, HashMap, HashSet, VecDeque}, + collections::{hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque}, fmt::{self, Debug}, hash::{Hash, Hasher}, marker::PhantomData, + mem, ops::{Deref, DerefMut}, path::{Path, PathBuf}, rc::{self, Rc}, - sync::{Arc, Weak}, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, Weak, + }, time::Duration, }; -pub trait Entity: 'static + Send + Sync { +pub trait Entity: 'static { type Event; fn release(&mut self, _: &mut MutableAppContext) {} @@ -38,7 +40,7 @@ pub trait Entity: 'static + Send + Sync { pub trait View: Entity + Sized { fn ui_name() -> &'static str; - fn render(&self, cx: &RenderContext<'_, Self>) -> ElementBox; + fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox; fn on_focus(&mut self, _: &mut ViewContext) {} fn on_blur(&mut self, _: &mut ViewContext) {} fn keymap_context(&self, _: &AppContext) -> keymap::Context { @@ -70,6 +72,11 @@ pub trait UpdateModel { F: FnOnce(&mut T, &mut ModelContext) -> S; } +pub trait UpgradeModelHandle { + fn upgrade_model_handle(&self, handle: WeakModelHandle) + -> Option>; +} + pub trait ReadView { fn read_view(&self, handle: &ViewHandle) -> &T; } @@ -88,6 +95,83 @@ pub trait UpdateView { F: FnOnce(&mut T, &mut ViewContext) -> S; } +pub trait Action: 'static + AnyAction { + type Argument: 'static + Clone; +} + +pub trait AnyAction { + fn id(&self) -> TypeId; + fn name(&self) -> &'static str; + fn as_any(&self) -> &dyn Any; + fn boxed_clone(&self) -> Box; + fn boxed_clone_as_any(&self) -> Box; +} + +#[macro_export] +macro_rules! action { + ($name:ident, $arg:ty) => { + #[derive(Clone)] + pub struct $name(pub $arg); + + impl $crate::Action for $name { + type Argument = $arg; + } + + impl $crate::AnyAction for $name { + fn id(&self) -> std::any::TypeId { + std::any::TypeId::of::<$name>() + } + + fn name(&self) -> &'static str { + stringify!($name) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn boxed_clone_as_any(&self) -> Box { + Box::new(self.clone()) + } + } + }; + + ($name:ident) => { + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct $name; + + impl $crate::Action for $name { + type Argument = (); + } + + impl $crate::AnyAction for $name { + fn id(&self) -> std::any::TypeId { + std::any::TypeId::of::<$name>() + } + + fn name(&self) -> &'static str { + stringify!($name) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn boxed_clone_as_any(&self) -> Box { + Box::new(self.clone()) + } + } + }; +} + pub struct Menu<'a> { pub name: &'a str, pub items: Vec>, @@ -97,8 +181,7 @@ pub enum MenuItem<'a> { Action { name: &'a str, keystroke: Option<&'a str>, - action: &'a str, - arg: Option>, + action: Box, }, Separator, } @@ -132,19 +215,19 @@ impl App { )))); let cx = app.0.clone(); - foreground_platform.on_menu_command(Box::new(move |command, arg| { + foreground_platform.on_menu_command(Box::new(move |action| { let mut cx = cx.borrow_mut(); if let Some(key_window_id) = cx.cx.platform.key_window_id() { if let Some((presenter, _)) = cx.presenters_and_platform_windows.get(&key_window_id) { let presenter = presenter.clone(); let path = presenter.borrow().dispatch_path(cx.as_ref()); - cx.dispatch_action_any(key_window_id, &path, command, arg.unwrap_or(&())); + cx.dispatch_action_any(key_window_id, &path, action); } else { - cx.dispatch_global_action_any(command, arg.unwrap_or(&())); + cx.dispatch_global_action_any(action); } } else { - cx.dispatch_global_action_any(command, arg.unwrap_or(&())); + cx.dispatch_global_action_any(action); } })); @@ -211,10 +294,16 @@ impl App { let platform = self.0.borrow().foreground_platform.clone(); platform.run(Box::new(move || { let mut cx = self.0.borrow_mut(); - on_finish_launching(&mut *cx); + let cx = &mut *cx; + crate::views::init(cx); + on_finish_launching(cx); })) } + pub fn platform(&self) -> Arc { + self.0.borrow().platform() + } + pub fn font_cache(&self) -> Arc { self.0.borrow().cx.font_cache.clone() } @@ -254,23 +343,19 @@ impl TestAppContext { cx } - pub fn dispatch_action( + pub fn dispatch_action( &self, window_id: usize, responder_chain: Vec, - name: &str, - arg: T, + action: A, ) { - self.cx.borrow_mut().dispatch_action_any( - window_id, - &responder_chain, - name, - Box::new(arg).as_ref(), - ); + self.cx + .borrow_mut() + .dispatch_action_any(window_id, &responder_chain, &action); } - pub fn dispatch_global_action(&self, name: &str, arg: T) { - self.cx.borrow_mut().dispatch_global_action(name, arg); + pub fn dispatch_global_action(&self, action: A) { + self.cx.borrow_mut().dispatch_global_action(action); } pub fn dispatch_keystroke( @@ -300,7 +385,9 @@ impl TestAppContext { T: View, F: FnOnce(&mut ViewContext) -> T, { - self.cx.borrow_mut().add_window(build_root_view) + self.cx + .borrow_mut() + .add_window(Default::default(), build_root_view) } pub fn window_ids(&self) -> Vec { @@ -457,6 +544,15 @@ impl UpdateModel for AsyncAppContext { } } +impl UpgradeModelHandle for AsyncAppContext { + fn upgrade_model_handle( + &self, + handle: WeakModelHandle, + ) -> Option> { + self.0.borrow_mut().upgrade_model_handle(handle) + } +} + impl ReadModelWith for AsyncAppContext { fn read_model_with T, T>( &self, @@ -548,23 +644,25 @@ impl ReadViewWith for TestAppContext { } type ActionCallback = - dyn FnMut(&mut dyn AnyView, &dyn Any, &mut MutableAppContext, usize, usize) -> bool; + dyn FnMut(&mut dyn AnyView, &dyn AnyAction, &mut MutableAppContext, usize, usize) -> bool; +type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext); -type GlobalActionCallback = dyn FnMut(&dyn Any, &mut MutableAppContext); +type SubscriptionCallback = Box bool>; +type ObservationCallback = Box bool>; pub struct MutableAppContext { weak_self: Option>>, foreground_platform: Rc, assets: Arc, cx: AppContext, - actions: HashMap>>>, - global_actions: HashMap>>, + actions: HashMap>>>, + global_actions: HashMap>>, keystroke_matcher: keymap::Matcher, next_entity_id: usize, next_window_id: usize, - subscriptions: HashMap>, - model_observations: HashMap>, - view_observations: HashMap>, + next_subscription_id: usize, + subscriptions: Arc>>>, + observations: Arc>>>, presenters_and_platform_windows: HashMap>, Box)>, debug_elements_callbacks: HashMap crate::json::Value>>, @@ -572,6 +670,7 @@ pub struct MutableAppContext { pending_effects: VecDeque, pending_flushes: usize, flushing_effects: bool, + next_cursor_style_handle_id: Arc, } impl MutableAppContext { @@ -591,7 +690,7 @@ impl MutableAppContext { models: Default::default(), views: Default::default(), windows: Default::default(), - values: Default::default(), + element_states: Default::default(), ref_counts: Arc::new(Mutex::new(RefCounts::default())), background, font_cache, @@ -602,15 +701,16 @@ impl MutableAppContext { keystroke_matcher: keymap::Matcher::default(), next_entity_id: 0, next_window_id: 0, - subscriptions: HashMap::new(), - model_observations: HashMap::new(), - view_observations: HashMap::new(), + next_subscription_id: 0, + subscriptions: Default::default(), + observations: Default::default(), presenters_and_platform_windows: HashMap::new(), debug_elements_callbacks: HashMap::new(), foreground, pending_effects: VecDeque::new(), pending_flushes: 0, flushing_effects: false, + next_cursor_style_handle_id: Default::default(), } } @@ -648,69 +748,53 @@ impl MutableAppContext { .map(|debug_elements| debug_elements(&self.cx)) } - pub fn add_action(&mut self, name: S, mut handler: F) + pub fn add_action(&mut self, mut handler: F) where - S: Into, + A: Action, V: View, - T: Any, - F: 'static + FnMut(&mut V, &T, &mut ViewContext), + F: 'static + FnMut(&mut V, &A, &mut ViewContext), { - let name = name.into(); - let name_clone = name.clone(); let handler = Box::new( move |view: &mut dyn AnyView, - arg: &dyn Any, + action: &dyn AnyAction, cx: &mut MutableAppContext, window_id: usize, view_id: usize| { - match arg.downcast_ref() { - Some(arg) => { - let mut cx = ViewContext::new(cx, window_id, view_id); - handler( - view.as_any_mut() - .downcast_mut() - .expect("downcast is type safe"), - arg, - &mut cx, - ); - cx.halt_action_dispatch - } - None => { - log::error!("Could not downcast argument for action {}", name_clone); - false - } - } + let action = action.as_any().downcast_ref().unwrap(); + let mut cx = ViewContext::new(cx, window_id, view_id); + handler( + view.as_any_mut() + .downcast_mut() + .expect("downcast is type safe"), + action, + &mut cx, + ); + cx.halt_action_dispatch }, ); self.actions .entry(TypeId::of::()) .or_default() - .entry(name) + .entry(TypeId::of::()) .or_default() .push(handler); } - pub fn add_global_action(&mut self, name: S, mut handler: F) + pub fn add_global_action(&mut self, mut handler: F) where - S: Into, - T: 'static + Any, - F: 'static + FnMut(&T, &mut MutableAppContext), + A: Action, + F: 'static + FnMut(&A, &mut MutableAppContext), { - let name = name.into(); - let name_clone = name.clone(); - let handler = Box::new(move |arg: &dyn Any, cx: &mut MutableAppContext| { - if let Some(arg) = arg.downcast_ref() { - handler(arg, cx); - } else { - log::error!( - "Could not downcast argument for global action {}", - name_clone - ); - } + let handler = Box::new(move |action: &dyn AnyAction, cx: &mut MutableAppContext| { + let action = action.as_any().downcast_ref().unwrap(); + handler(action, cx); }); - self.global_actions.entry(name).or_default().push(handler); + self.global_actions + .entry(TypeId::of::()) + .or_default() + .push(handler); } pub fn window_ids(&self) -> impl Iterator + '_ { @@ -732,12 +816,49 @@ impl MutableAppContext { self.cx.focused_view_id(window_id) } - pub fn render_view(&self, window_id: usize, view_id: usize) -> Result { - self.cx.render_view(window_id, view_id) + pub fn render_view( + &mut self, + window_id: usize, + view_id: usize, + titlebar_height: f32, + refreshing: bool, + ) -> Result { + let mut view = self + .cx + .views + .remove(&(window_id, view_id)) + .ok_or(anyhow!("view not found"))?; + let element = view.render(window_id, view_id, titlebar_height, refreshing, self); + self.cx.views.insert((window_id, view_id), view); + Ok(element) } - pub fn render_views(&self, window_id: usize) -> HashMap { - self.cx.render_views(window_id) + pub fn render_views( + &mut self, + window_id: usize, + titlebar_height: f32, + ) -> HashMap { + let view_ids = self + .views + .keys() + .filter_map(|(win_id, view_id)| { + if *win_id == window_id { + Some(*view_id) + } else { + None + } + }) + .collect::>(); + view_ids + .into_iter() + .map(|view_id| { + ( + view_id, + self.render_view(window_id, view_id, titlebar_height, false) + .unwrap(), + ) + }) + .collect() } pub fn update T>(&mut self, callback: F) -> T { @@ -808,37 +929,112 @@ impl MutableAppContext { ); } + pub fn subscribe(&mut self, handle: &H, mut callback: F) -> Subscription + where + E: Entity, + E::Event: 'static, + H: Handle, + F: 'static + FnMut(H, &E::Event, &mut Self), + { + self.subscribe_internal(handle, move |handle, event, cx| { + callback(handle, event, cx); + true + }) + } + + fn observe(&mut self, handle: &H, mut callback: F) -> Subscription + where + E: Entity, + E::Event: 'static, + H: Handle, + F: 'static + FnMut(H, &mut Self), + { + self.observe_internal(handle, move |handle, cx| { + callback(handle, cx); + true + }) + } + + pub fn subscribe_internal(&mut self, handle: &H, mut callback: F) -> Subscription + where + E: Entity, + E::Event: 'static, + H: Handle, + F: 'static + FnMut(H, &E::Event, &mut Self) -> bool, + { + let id = post_inc(&mut self.next_subscription_id); + let emitter = handle.downgrade(); + self.subscriptions + .lock() + .entry(handle.id()) + .or_default() + .insert( + id, + Box::new(move |payload, cx| { + if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) { + let payload = payload.downcast_ref().expect("downcast is type safe"); + callback(emitter, payload, cx) + } else { + false + } + }), + ); + Subscription::Subscription { + id, + entity_id: handle.id(), + subscriptions: Some(Arc::downgrade(&self.subscriptions)), + } + } + + fn observe_internal(&mut self, handle: &H, mut callback: F) -> Subscription + where + E: Entity, + E::Event: 'static, + H: Handle, + F: 'static + FnMut(H, &mut Self) -> bool, + { + let id = post_inc(&mut self.next_subscription_id); + let observed = handle.downgrade(); + self.observations + .lock() + .entry(handle.id()) + .or_default() + .insert( + id, + Box::new(move |cx| { + if let Some(observed) = H::upgrade_from(&observed, cx) { + callback(observed, cx) + } else { + false + } + }), + ); + Subscription::Observation { + id, + entity_id: handle.id(), + observations: Some(Arc::downgrade(&self.observations)), + } + } + pub(crate) fn notify_view(&mut self, window_id: usize, view_id: usize) { self.pending_effects .push_back(Effect::ViewNotification { window_id, view_id }); } - pub(crate) fn notify_all_views(&mut self) { - let notifications = self - .views - .keys() - .copied() - .map(|(window_id, view_id)| Effect::ViewNotification { window_id, view_id }) - .collect::>(); - self.pending_effects.extend(notifications); - } - - pub fn dispatch_action( + pub fn dispatch_action( &mut self, window_id: usize, responder_chain: Vec, - name: &str, - arg: T, + action: &A, ) { - self.dispatch_action_any(window_id, &responder_chain, name, Box::new(arg).as_ref()); + self.dispatch_action_any(window_id, &responder_chain, action); } pub(crate) fn dispatch_action_any( &mut self, window_id: usize, path: &[usize], - name: &str, - arg: &dyn Any, + action: &dyn AnyAction, ) -> bool { self.pending_flushes += 1; let mut halted_dispatch = false; @@ -850,10 +1046,11 @@ impl MutableAppContext { if let Some((name, mut handlers)) = self .actions .get_mut(&type_id) - .and_then(|h| h.remove_entry(name)) + .and_then(|h| h.remove_entry(&action.id())) { for handler in handlers.iter_mut().rev() { - let halt_dispatch = handler(view.as_mut(), arg, self, window_id, *view_id); + let halt_dispatch = + handler(view.as_mut(), action, self, window_id, *view_id); if halt_dispatch { halted_dispatch = true; break; @@ -874,22 +1071,22 @@ impl MutableAppContext { } if !halted_dispatch { - self.dispatch_global_action_any(name, arg); + self.dispatch_global_action_any(action); } self.flush_effects(); halted_dispatch } - pub fn dispatch_global_action(&mut self, name: &str, arg: T) { - self.dispatch_global_action_any(name, Box::new(arg).as_ref()); + pub fn dispatch_global_action(&mut self, action: A) { + self.dispatch_global_action_any(&action); } - fn dispatch_global_action_any(&mut self, name: &str, arg: &dyn Any) { - if let Some((name, mut handlers)) = self.global_actions.remove_entry(name) { + fn dispatch_global_action_any(&mut self, action: &dyn AnyAction) { + if let Some((name, mut handlers)) = self.global_actions.remove_entry(&action.id()) { self.pending_flushes += 1; for handler in handlers.iter_mut().rev() { - handler(arg, self); + handler(action, self); } self.global_actions.insert(name, handlers); self.flush_effects(); @@ -928,13 +1125,9 @@ impl MutableAppContext { { MatchResult::None => {} MatchResult::Pending => pending = true, - MatchResult::Action { name, arg } => { - if self.dispatch_action_any( - window_id, - &responder_chain[0..=i], - &name, - arg.as_ref().map(|arg| arg.as_ref()).unwrap_or(&()), - ) { + MatchResult::Action(action) => { + if self.dispatch_action_any(window_id, &responder_chain[0..=i], action.as_ref()) + { return Ok(true); } } @@ -959,7 +1152,11 @@ impl MutableAppContext { handle } - pub fn add_window(&mut self, build_root_view: F) -> (usize, ViewHandle) + pub fn add_window( + &mut self, + window_options: WindowOptions, + build_root_view: F, + ) -> (usize, ViewHandle) where T: View, F: FnOnce(&mut ViewContext) -> T, @@ -976,7 +1173,7 @@ impl MutableAppContext { invalidation: None, }, ); - self.open_platform_window(window_id); + self.open_platform_window(window_id, window_options); root_view.update(self, |view, cx| { view.on_focus(cx); cx.notify(); @@ -992,23 +1189,14 @@ impl MutableAppContext { self.remove_dropped_entities(); } - fn open_platform_window(&mut self, window_id: usize) { - let mut window = self.cx.platform.open_window( - window_id, - WindowOptions { - bounds: RectF::new(vec2f(0., 0.), vec2f(1024., 768.)), - title: "Zed".into(), - }, - self.foreground.clone(), - ); - let text_layout_cache = TextLayoutCache::new(self.cx.platform.fonts()); - let presenter = Rc::new(RefCell::new(Presenter::new( - window_id, - self.cx.font_cache.clone(), - text_layout_cache, - self.assets.clone(), - self, - ))); + fn open_platform_window(&mut self, window_id: usize, window_options: WindowOptions) { + let mut window = + self.cx + .platform + .open_window(window_id, window_options, self.foreground.clone()); + let presenter = Rc::new(RefCell::new( + self.build_presenter(window_id, window.titlebar_height()), + )); { let mut app = self.upgrade(); @@ -1035,16 +1223,8 @@ impl MutableAppContext { { let mut app = self.upgrade(); - let presenter = presenter.clone(); - window.on_resize(Box::new(move |window| { - app.update(|cx| { - let scene = presenter.borrow_mut().build_scene( - window.size(), - window.scale_factor(), - cx, - ); - window.present_scene(scene); - }) + window.on_resize(Box::new(move || { + app.update(|cx| cx.resize_window(window_id)) })); } @@ -1063,6 +1243,34 @@ impl MutableAppContext { }); } + pub fn build_presenter(&mut self, window_id: usize, titlebar_height: f32) -> Presenter { + Presenter::new( + window_id, + titlebar_height, + self.cx.font_cache.clone(), + TextLayoutCache::new(self.cx.platform.fonts()), + self.assets.clone(), + self, + ) + } + + pub fn build_render_context( + &mut self, + window_id: usize, + view_id: usize, + titlebar_height: f32, + refreshing: bool, + ) -> RenderContext { + RenderContext { + app: self, + titlebar_height, + refreshing, + window_id, + view_id, + view_type: PhantomData, + } + } + pub fn add_view(&mut self, window_id: usize, build_view: F) -> ViewHandle where T: View, @@ -1102,24 +1310,39 @@ impl MutableAppContext { handle } + pub fn element_state( + &mut self, + id: ElementStateId, + ) -> ElementStateHandle { + let key = (TypeId::of::(), id); + self.cx + .element_states + .entry(key) + .or_insert_with(|| Box::new(T::default())); + ElementStateHandle::new(TypeId::of::(), id, &self.cx.ref_counts) + } + fn remove_dropped_entities(&mut self) { loop { - let (dropped_models, dropped_views, dropped_values) = + let (dropped_models, dropped_views, dropped_element_states) = self.cx.ref_counts.lock().take_dropped(); - if dropped_models.is_empty() && dropped_views.is_empty() && dropped_values.is_empty() { + if dropped_models.is_empty() + && dropped_views.is_empty() + && dropped_element_states.is_empty() + { break; } for model_id in dropped_models { - self.subscriptions.remove(&model_id); - self.model_observations.remove(&model_id); + self.subscriptions.lock().remove(&model_id); + self.observations.lock().remove(&model_id); let mut model = self.cx.models.remove(&model_id).unwrap(); model.release(self); } for (window_id, view_id) in dropped_views { - self.subscriptions.remove(&view_id); - self.model_observations.remove(&view_id); + self.subscriptions.lock().remove(&view_id); + self.observations.lock().remove(&view_id); let mut view = self.cx.views.remove(&(window_id, view_id)).unwrap(); view.release(self); let change_focus_to = self.cx.windows.get_mut(&window_id).and_then(|window| { @@ -1140,9 +1363,8 @@ impl MutableAppContext { } } - let mut values = self.cx.values.write(); - for key in dropped_values { - values.remove(&key); + for key in dropped_element_states { + self.cx.element_states.remove(&key); } } } @@ -1153,6 +1375,7 @@ impl MutableAppContext { if !self.flushing_effects && self.pending_flushes == 0 { self.flushing_effects = true; + let mut refreshing = false; loop { if let Some(effect) = self.pending_effects.pop_front() { match effect { @@ -1166,15 +1389,31 @@ impl MutableAppContext { Effect::Focus { window_id, view_id } => { self.focus(window_id, view_id); } + Effect::ResizeWindow { window_id } => { + if let Some(window) = self.cx.windows.get_mut(&window_id) { + window + .invalidation + .get_or_insert(WindowInvalidation::default()); + } + } + Effect::RefreshWindows => { + refreshing = true; + } } self.remove_dropped_entities(); } else { self.remove_dropped_entities(); - self.update_windows(); + if refreshing { + self.perform_window_refresh(); + } else { + self.update_windows(); + } if self.pending_effects.is_empty() { self.flushing_effects = false; break; + } else { + refreshing = false; } } } @@ -1195,8 +1434,9 @@ impl MutableAppContext { { { let mut presenter = presenter.borrow_mut(); - presenter.invalidate(invalidation, self.as_ref()); - let scene = presenter.build_scene(window.size(), window.scale_factor(), self); + presenter.invalidate(invalidation, self); + let scene = + presenter.build_scene(window.size(), window.scale_factor(), false, self); window.present_scene(scene); } self.presenters_and_platform_windows @@ -1205,134 +1445,101 @@ impl MutableAppContext { } } - fn emit_event(&mut self, entity_id: usize, payload: Box) { - if let Some(subscriptions) = self.subscriptions.remove(&entity_id) { - for mut subscription in subscriptions { - let alive = match &mut subscription { - Subscription::FromModel { model_id, callback } => { - if let Some(mut model) = self.cx.models.remove(model_id) { - callback(model.as_any_mut(), payload.as_ref(), self, *model_id); - self.cx.models.insert(*model_id, model); - true - } else { - false - } - } - Subscription::FromView { - window_id, - view_id, - callback, - } => { - if let Some(mut view) = self.cx.views.remove(&(*window_id, *view_id)) { - callback( - view.as_any_mut(), - payload.as_ref(), - self, - *window_id, - *view_id, - ); - self.cx.views.insert((*window_id, *view_id), view); - true - } else { - false - } - } - }; + fn resize_window(&mut self, window_id: usize) { + self.pending_effects + .push_back(Effect::ResizeWindow { window_id }); + } + + pub fn refresh_windows(&mut self) { + self.pending_effects.push_back(Effect::RefreshWindows); + } + + fn perform_window_refresh(&mut self) { + let mut presenters = mem::take(&mut self.presenters_and_platform_windows); + for (window_id, (presenter, window)) in &mut presenters { + let invalidation = self + .cx + .windows + .get_mut(&window_id) + .unwrap() + .invalidation + .take(); + let mut presenter = presenter.borrow_mut(); + presenter.refresh(invalidation, self); + let scene = presenter.build_scene(window.size(), window.scale_factor(), true, self); + window.present_scene(scene); + } + self.presenters_and_platform_windows = presenters; + } + pub fn set_cursor_style(&mut self, style: CursorStyle) -> CursorStyleHandle { + self.platform.set_cursor_style(style); + let id = self.next_cursor_style_handle_id.fetch_add(1, SeqCst); + CursorStyleHandle { + id, + next_cursor_style_handle_id: self.next_cursor_style_handle_id.clone(), + platform: self.platform(), + } + } + + fn emit_event(&mut self, entity_id: usize, payload: Box) { + let callbacks = self.subscriptions.lock().remove(&entity_id); + if let Some(callbacks) = callbacks { + for (id, mut callback) in callbacks { + let alive = callback(payload.as_ref(), self); if alive { self.subscriptions + .lock() .entry(entity_id) .or_default() - .push(subscription); + .insert(id, callback); } } } } fn notify_model_observers(&mut self, observed_id: usize) { - if let Some(observations) = self.model_observations.remove(&observed_id) { + let callbacks = self.observations.lock().remove(&observed_id); + if let Some(callbacks) = callbacks { if self.cx.models.contains_key(&observed_id) { - for mut observation in observations { - let alive = match &mut observation { - ModelObservation::FromModel { model_id, callback } => { - if let Some(mut model) = self.cx.models.remove(model_id) { - callback(model.as_any_mut(), observed_id, self, *model_id); - self.cx.models.insert(*model_id, model); - true - } else { - false - } - } - ModelObservation::FromView { - window_id, - view_id, - callback, - } => { - if let Some(mut view) = self.cx.views.remove(&(*window_id, *view_id)) { - callback( - view.as_any_mut(), - observed_id, - self, - *window_id, - *view_id, - ); - self.cx.views.insert((*window_id, *view_id), view); - true - } else { - false - } - } - }; - + for (id, mut callback) in callbacks { + let alive = callback(self); if alive { - self.model_observations + self.observations + .lock() .entry(observed_id) .or_default() - .push(observation); + .insert(id, callback); } } } } } - fn notify_view_observers(&mut self, window_id: usize, view_id: usize) { - if let Some(window) = self.cx.windows.get_mut(&window_id) { + fn notify_view_observers(&mut self, observed_window_id: usize, observed_view_id: usize) { + if let Some(window) = self.cx.windows.get_mut(&observed_window_id) { window .invalidation .get_or_insert_with(Default::default) .updated - .insert(view_id); + .insert(observed_view_id); } - if let Some(observations) = self.view_observations.remove(&view_id) { - if self.cx.views.contains_key(&(window_id, view_id)) { - for mut observation in observations { - let alive = if let Some(mut view) = self - .cx - .views - .remove(&(observation.window_id, observation.view_id)) - { - (observation.callback)( - view.as_any_mut(), - view_id, - window_id, - self, - observation.window_id, - observation.view_id, - ); - self.cx - .views - .insert((observation.window_id, observation.view_id), view); - true - } else { - false - }; - + let callbacks = self.observations.lock().remove(&observed_view_id); + if let Some(callbacks) = callbacks { + if self + .cx + .views + .contains_key(&(observed_window_id, observed_view_id)) + { + for (id, mut callback) in callbacks { + let alive = callback(self); if alive { - self.view_observations - .entry(view_id) + self.observations + .lock() + .entry(observed_view_id) .or_default() - .push(observation); + .insert(id, callback); } } } @@ -1434,6 +1641,15 @@ impl UpdateModel for MutableAppContext { } } +impl UpgradeModelHandle for MutableAppContext { + fn upgrade_model_handle( + &self, + handle: WeakModelHandle, + ) -> Option> { + self.cx.upgrade_model_handle(handle) + } +} + impl ReadView for MutableAppContext { fn read_view(&self, handle: &ViewHandle) -> &T { if let Some(view) = self.cx.views.get(&(handle.window_id, handle.view_id)) { @@ -1490,7 +1706,7 @@ pub struct AppContext { models: HashMap>, views: HashMap<(usize, usize), Box>, windows: HashMap, - values: RwLock>>, + element_states: HashMap<(TypeId, ElementStateId), Box>, background: Arc, ref_counts: Arc>, font_cache: Arc, @@ -1510,26 +1726,6 @@ impl AppContext { .map(|window| window.focused_view_id) } - pub fn render_view(&self, window_id: usize, view_id: usize) -> Result { - self.views - .get(&(window_id, view_id)) - .map(|v| v.render(window_id, view_id, self)) - .ok_or(anyhow!("view not found")) - } - - pub fn render_views(&self, window_id: usize) -> HashMap { - self.views - .iter() - .filter_map(|((win_id, view_id), view)| { - if *win_id == window_id { - Some((*view_id, view.render(*win_id, *view_id, self))) - } else { - None - } - }) - .collect::>() - } - pub fn background(&self) -> &Arc { &self.background } @@ -1541,15 +1737,6 @@ impl AppContext { pub fn platform(&self) -> &Arc { &self.platform } - - pub fn value(&self, id: usize) -> ValueHandle { - let key = (TypeId::of::(), id); - self.values - .write() - .entry(key) - .or_insert_with(|| Box::new(T::default())); - ValueHandle::new(TypeId::of::(), id, &self.ref_counts) - } } impl ReadModel for AppContext { @@ -1565,6 +1752,19 @@ impl ReadModel for AppContext { } } +impl UpgradeModelHandle for AppContext { + fn upgrade_model_handle( + &self, + handle: WeakModelHandle, + ) -> Option> { + if self.models.contains_key(&handle.model_id) { + Some(ModelHandle::new(handle.model_id, &self.ref_counts)) + } else { + None + } + } +} + impl ReadView for AppContext { fn read_view(&self, handle: &ViewHandle) -> &T { if let Some(view) = self.views.get(&(handle.window_id, handle.view_id)) { @@ -1605,6 +1805,10 @@ pub enum Effect { window_id: usize, view_id: usize, }, + ResizeWindow { + window_id: usize, + }, + RefreshWindows, } impl Debug for Effect { @@ -1628,11 +1832,16 @@ impl Debug for Effect { .field("window_id", window_id) .field("view_id", view_id) .finish(), + Effect::ResizeWindow { window_id } => f + .debug_struct("Effect::RefreshWindow") + .field("window_id", window_id) + .finish(), + Effect::RefreshWindows => f.debug_struct("Effect::FullViewRefresh").finish(), } } } -pub trait AnyModel: Send + Sync { +pub trait AnyModel { fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; fn release(&mut self, cx: &mut MutableAppContext); @@ -1655,12 +1864,19 @@ where } } -pub trait AnyView: Send + Sync { +pub trait AnyView { fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; fn release(&mut self, cx: &mut MutableAppContext); fn ui_name(&self) -> &'static str; - fn render<'a>(&self, window_id: usize, view_id: usize, cx: &AppContext) -> ElementBox; + fn render<'a>( + &mut self, + window_id: usize, + view_id: usize, + titlebar_height: f32, + refreshing: bool, + cx: &mut MutableAppContext, + ) -> ElementBox; fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); fn on_blur(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); fn keymap_context(&self, cx: &AppContext) -> keymap::Context; @@ -1686,14 +1902,23 @@ where T::ui_name() } - fn render<'a>(&self, window_id: usize, view_id: usize, cx: &AppContext) -> ElementBox { + fn render<'a>( + &mut self, + window_id: usize, + view_id: usize, + titlebar_height: f32, + refreshing: bool, + cx: &mut MutableAppContext, + ) -> ElementBox { View::render( self, - &RenderContext { + &mut RenderContext { window_id, view_id, app: cx, view_type: PhantomData::, + titlebar_height, + refreshing, }, ) } @@ -1750,26 +1975,6 @@ impl<'a, T: Entity> ModelContext<'a, T> { self.app.add_model(build_model) } - pub fn subscribe(&mut self, handle: &ModelHandle, mut callback: F) - where - S::Event: 'static, - F: 'static + FnMut(&mut T, &S::Event, &mut ModelContext), - { - self.app - .subscriptions - .entry(handle.model_id) - .or_default() - .push(Subscription::FromModel { - model_id: self.model_id, - callback: Box::new(move |model, payload, app, model_id| { - let model = model.downcast_mut().expect("downcast is type safe"); - let payload = payload.downcast_ref().expect("downcast is type safe"); - let mut cx = ModelContext::new(app, model_id); - callback(model, payload, &mut cx); - }), - }); - } - pub fn emit(&mut self, payload: T::Event) { self.app.pending_effects.push_back(Effect::Event { entity_id: self.model_id, @@ -1777,26 +1982,6 @@ impl<'a, T: Entity> ModelContext<'a, T> { }); } - pub fn observe(&mut self, handle: &ModelHandle, mut callback: F) - where - S: Entity, - F: 'static + FnMut(&mut T, ModelHandle, &mut ModelContext), - { - self.app - .model_observations - .entry(handle.model_id) - .or_default() - .push(ModelObservation::FromModel { - model_id: self.model_id, - callback: Box::new(move |model, observed_id, app, model_id| { - let model = model.downcast_mut().expect("downcast is type safe"); - let observed = ModelHandle::new(observed_id, &app.cx.ref_counts); - let mut cx = ModelContext::new(app, model_id); - callback(model, observed, &mut cx); - }), - }); - } - pub fn notify(&mut self) { self.app .pending_effects @@ -1805,6 +1990,47 @@ impl<'a, T: Entity> ModelContext<'a, T> { }); } + pub fn subscribe( + &mut self, + handle: &ModelHandle, + mut callback: F, + ) -> Subscription + where + S::Event: 'static, + F: 'static + FnMut(&mut T, ModelHandle, &S::Event, &mut ModelContext), + { + let subscriber = self.handle().downgrade(); + self.app + .subscribe_internal(handle, move |emitter, event, cx| { + if let Some(subscriber) = subscriber.upgrade(cx) { + subscriber.update(cx, |subscriber, cx| { + callback(subscriber, emitter, event, cx); + }); + true + } else { + false + } + }) + } + + pub fn observe(&mut self, handle: &ModelHandle, mut callback: F) -> Subscription + where + S: Entity, + F: 'static + FnMut(&mut T, ModelHandle, &mut ModelContext), + { + let observer = self.handle().downgrade(); + self.app.observe_internal(handle, move |observed, cx| { + if let Some(observer) = observer.upgrade(cx) { + observer.update(cx, |observer, cx| { + callback(observer, observed, cx); + }); + true + } else { + false + } + }) + } + pub fn handle(&self) -> ModelHandle { ModelHandle::new(self.model_id, &self.app.cx.ref_counts) } @@ -1858,6 +2084,15 @@ impl UpdateModel for ModelContext<'_, M> { } } +impl UpgradeModelHandle for ModelContext<'_, M> { + fn upgrade_model_handle( + &self, + handle: WeakModelHandle, + ) -> Option> { + self.cx.upgrade_model_handle(handle) + } +} + impl Deref for ModelContext<'_, M> { type Target = MutableAppContext; @@ -1983,54 +2218,44 @@ impl<'a, T: View> ViewContext<'a, T> { self.app.add_option_view(self.window_id, build_view) } - pub fn subscribe_to_model(&mut self, handle: &ModelHandle, mut callback: F) + pub fn subscribe(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, E::Event: 'static, - F: 'static + FnMut(&mut T, ModelHandle, &E::Event, &mut ViewContext), - { - let emitter_handle = handle.downgrade(); - self.subscribe(handle, move |model, payload, cx| { - if let Some(emitter_handle) = emitter_handle.upgrade(cx.as_ref()) { - callback(model, emitter_handle, payload, cx); - } - }); - } - - pub fn subscribe_to_view(&mut self, handle: &ViewHandle, mut callback: F) - where - V: View, - V::Event: 'static, - F: 'static + FnMut(&mut T, ViewHandle, &V::Event, &mut ViewContext), + H: Handle, + F: 'static + FnMut(&mut T, H, &E::Event, &mut ViewContext), { - let emitter_handle = handle.downgrade(); - self.subscribe(handle, move |view, payload, cx| { - if let Some(emitter_handle) = emitter_handle.upgrade(cx.as_ref()) { - callback(view, emitter_handle, payload, cx); - } - }); + let subscriber = self.handle().downgrade(); + self.app + .subscribe_internal(handle, move |emitter, event, cx| { + if let Some(subscriber) = subscriber.upgrade(cx) { + subscriber.update(cx, |subscriber, cx| { + callback(subscriber, emitter, event, cx); + }); + true + } else { + false + } + }) } - pub fn subscribe(&mut self, handle: &impl Handle, mut callback: F) + pub fn observe(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, - E::Event: 'static, - F: 'static + FnMut(&mut T, &E::Event, &mut ViewContext), + H: Handle, + F: 'static + FnMut(&mut T, H, &mut ViewContext), { - self.app - .subscriptions - .entry(handle.id()) - .or_default() - .push(Subscription::FromView { - window_id: self.window_id, - view_id: self.view_id, - callback: Box::new(move |entity, payload, app, window_id, view_id| { - let entity = entity.downcast_mut().expect("downcast is type safe"); - let payload = payload.downcast_ref().expect("downcast is type safe"); - let mut cx = ViewContext::new(app, window_id, view_id); - callback(entity, payload, &mut cx); - }), - }); + let observer = self.handle().downgrade(); + self.app.observe_internal(handle, move |observed, cx| { + if let Some(observer) = observer.upgrade(cx) { + observer.update(cx, |observer, cx| { + callback(observer, observed, cx); + }); + true + } else { + false + } + }) } pub fn emit(&mut self, payload: T::Event) { @@ -2040,67 +2265,10 @@ impl<'a, T: View> ViewContext<'a, T> { }); } - pub fn observe_model(&mut self, handle: &ModelHandle, mut callback: F) - where - S: Entity, - F: 'static + FnMut(&mut T, ModelHandle, &mut ViewContext), - { - self.app - .model_observations - .entry(handle.id()) - .or_default() - .push(ModelObservation::FromView { - window_id: self.window_id, - view_id: self.view_id, - callback: Box::new(move |view, observed_id, app, window_id, view_id| { - let view = view.downcast_mut().expect("downcast is type safe"); - let observed = ModelHandle::new(observed_id, &app.cx.ref_counts); - let mut cx = ViewContext::new(app, window_id, view_id); - callback(view, observed, &mut cx); - }), - }); - } - - pub fn observe_view(&mut self, handle: &ViewHandle, mut callback: F) - where - S: View, - F: 'static + FnMut(&mut T, ViewHandle, &mut ViewContext), - { - self.app - .view_observations - .entry(handle.id()) - .or_default() - .push(ViewObservation { - window_id: self.window_id, - view_id: self.view_id, - callback: Box::new( - move |view, - observed_view_id, - observed_window_id, - app, - observing_window_id, - observing_view_id| { - let view = view.downcast_mut().expect("downcast is type safe"); - let observed_handle = ViewHandle::new( - observed_view_id, - observed_window_id, - &app.cx.ref_counts, - ); - let mut cx = ViewContext::new(app, observing_window_id, observing_view_id); - callback(view, observed_handle, &mut cx); - }, - ), - }); - } - pub fn notify(&mut self) { self.app.notify_view(self.window_id, self.view_id); } - pub fn notify_all(&mut self) { - self.app.notify_all_views(); - } - pub fn propagate_action(&mut self) { self.halt_action_dispatch = false; } @@ -2117,7 +2285,9 @@ impl<'a, T: View> ViewContext<'a, T> { } pub struct RenderContext<'a, T: View> { - pub app: &'a AppContext, + pub app: &'a mut MutableAppContext, + pub titlebar_height: f32, + pub refreshing: bool, window_id: usize, view_id: usize, view_type: PhantomData, @@ -2136,10 +2306,22 @@ impl AsRef for &AppContext { } impl Deref for RenderContext<'_, V> { - type Target = AppContext; + type Target = MutableAppContext; fn deref(&self) -> &Self::Target { - &self.app + self.app + } +} + +impl DerefMut for RenderContext<'_, V> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.app + } +} + +impl ReadModel for RenderContext<'_, V> { + fn read_model(&self, handle: &ModelHandle) -> &T { + self.app.read_model(handle) } } @@ -2175,6 +2357,15 @@ impl ReadModel for ViewContext<'_, V> { } } +impl UpgradeModelHandle for ViewContext<'_, V> { + fn upgrade_model_handle( + &self, + handle: WeakModelHandle, + ) -> Option> { + self.cx.upgrade_model_handle(handle) + } +} + impl UpdateModel for ViewContext<'_, V> { fn update_model(&mut self, handle: &ModelHandle, update: F) -> S where @@ -2202,8 +2393,13 @@ impl UpdateView for ViewContext<'_, V> { } pub trait Handle { + type Weak: 'static; fn id(&self) -> usize; fn location(&self) -> EntityLocation; + fn downgrade(&self) -> Self::Weak; + fn upgrade_from(weak: &Self::Weak, cx: &AppContext) -> Option + where + Self: Sized; } #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] @@ -2256,6 +2452,53 @@ impl ModelHandle { cx.update_model(self, update) } + pub fn next_notification(&self, cx: &TestAppContext) -> impl Future { + let (mut tx, mut rx) = mpsc::channel(1); + let mut cx = cx.cx.borrow_mut(); + let subscription = cx.observe(self, move |_, _| { + tx.blocking_send(()).ok(); + }); + + let duration = if std::env::var("CI").is_ok() { + Duration::from_secs(5) + } else { + Duration::from_secs(1) + }; + + async move { + let notification = timeout(duration, rx.recv()) + .await + .expect("next notification timed out"); + drop(subscription); + notification.expect("model dropped while test was waiting for its next notification") + } + } + + pub fn next_event(&self, cx: &TestAppContext) -> impl Future + where + T::Event: Clone, + { + let (mut tx, mut rx) = mpsc::channel(1); + let mut cx = cx.cx.borrow_mut(); + let subscription = cx.subscribe(self, move |_, event, _| { + tx.blocking_send(event.clone()).ok(); + }); + + let duration = if std::env::var("CI").is_ok() { + Duration::from_secs(5) + } else { + Duration::from_secs(1) + }; + + async move { + let event = timeout(duration, rx.recv()) + .await + .expect("next event timed out"); + drop(subscription); + event.expect("model dropped while test was waiting for its next event") + } + } + pub fn condition( &self, cx: &TestAppContext, @@ -2264,20 +2507,20 @@ impl ModelHandle { let (tx, mut rx) = mpsc::channel(1024); let mut cx = cx.cx.borrow_mut(); - self.update(&mut *cx, |_, cx| { + let subscriptions = ( cx.observe(self, { let mut tx = tx.clone(); - move |_, _, _| { + move |_, _| { tx.blocking_send(()).ok(); } - }); + }), cx.subscribe(self, { let mut tx = tx.clone(); move |_, _, _| { tx.blocking_send(()).ok(); } - }) - }); + }), + ); let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap(); let handle = self.downgrade(); @@ -2311,6 +2554,7 @@ impl ModelHandle { }) .await .expect("condition timed out"); + drop(subscriptions); } } } @@ -2363,7 +2607,9 @@ impl Drop for ModelHandle { } } -impl Handle for ModelHandle { +impl Handle for ModelHandle { + type Weak = WeakModelHandle; + fn id(&self) -> usize { self.model_id } @@ -2371,6 +2617,17 @@ impl Handle for ModelHandle { fn location(&self) -> EntityLocation { EntityLocation::Model(self.model_id) } + + fn downgrade(&self) -> Self::Weak { + self.downgrade() + } + + fn upgrade_from(weak: &Self::Weak, cx: &AppContext) -> Option + where + Self: Sized, + { + weak.upgrade(cx) + } } pub struct WeakModelHandle { @@ -2378,6 +2635,9 @@ pub struct WeakModelHandle { model_type: PhantomData, } +unsafe impl Send for WeakModelHandle {} +unsafe impl Sync for WeakModelHandle {} + impl WeakModelHandle { fn new(model_id: usize) -> Self { Self { @@ -2386,13 +2646,8 @@ impl WeakModelHandle { } } - pub fn upgrade(&self, cx: impl AsRef) -> Option> { - let cx = cx.as_ref(); - if cx.models.contains_key(&self.model_id) { - Some(ModelHandle::new(self.model_id, &cx.ref_counts)) - } else { - None - } + pub fn upgrade(self, cx: &impl UpgradeModelHandle) -> Option> { + cx.upgrade_model_handle(self) } } @@ -2419,6 +2674,8 @@ impl Clone for WeakModelHandle { } } +impl Copy for WeakModelHandle {} + pub struct ViewHandle { window_id: usize, view_id: usize, @@ -2482,20 +2739,21 @@ impl ViewHandle { let (tx, mut rx) = mpsc::channel(1024); let mut cx = cx.cx.borrow_mut(); - self.update(&mut *cx, |_, cx| { - cx.observe_view(self, { - let mut tx = tx.clone(); - move |_, _, _| { - tx.blocking_send(()).ok(); - } - }); - - cx.subscribe(self, { - let mut tx = tx.clone(); - move |_, _, _| { - tx.blocking_send(()).ok(); - } - }) + let subscriptions = self.update(&mut *cx, |_, cx| { + ( + cx.observe(self, { + let mut tx = tx.clone(); + move |_, _, _| { + tx.blocking_send(()).ok(); + } + }), + cx.subscribe(self, { + let mut tx = tx.clone(); + move |_, _, _, _| { + tx.blocking_send(()).ok(); + } + }), + ) }); let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap(); @@ -2530,6 +2788,7 @@ impl ViewHandle { }) .await .expect("condition timed out"); + drop(subscriptions); } } } @@ -2573,7 +2832,9 @@ impl Drop for ViewHandle { } } -impl Handle for ViewHandle { +impl Handle for ViewHandle { + type Weak = WeakViewHandle; + fn id(&self) -> usize { self.view_id } @@ -2581,6 +2842,17 @@ impl Handle for ViewHandle { fn location(&self) -> EntityLocation { EntityLocation::View(self.window_id, self.view_id) } + + fn downgrade(&self) -> Self::Weak { + self.downgrade() + } + + fn upgrade_from(weak: &Self::Weak, cx: &AppContext) -> Option + where + Self: Sized, + { + weak.upgrade(cx) + } } pub struct AnyViewHandle { @@ -2632,6 +2904,12 @@ impl Clone for AnyViewHandle { } } +impl From<&AnyViewHandle> for AnyViewHandle { + fn from(handle: &AnyViewHandle) -> Self { + handle.clone() + } +} + impl From<&ViewHandle> for AnyViewHandle { fn from(handle: &ViewHandle) -> Self { handle @@ -2706,6 +2984,10 @@ impl WeakViewHandle { } } + pub fn id(&self) -> usize { + self.view_id + } + pub fn upgrade(&self, cx: &AppContext) -> Option> { if cx.ref_counts.lock().is_entity_alive(self.view_id) { Some(ViewHandle::new( @@ -2729,16 +3011,31 @@ impl Clone for WeakViewHandle { } } -pub struct ValueHandle { +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct ElementStateId(usize, usize); + +impl From for ElementStateId { + fn from(id: usize) -> Self { + Self(id, 0) + } +} + +impl From<(usize, usize)> for ElementStateId { + fn from(id: (usize, usize)) -> Self { + Self(id.0, id.1) + } +} + +pub struct ElementStateHandle { value_type: PhantomData, tag_type_id: TypeId, - id: usize, + id: ElementStateId, ref_counts: Weak>, } -impl ValueHandle { - fn new(tag_type_id: TypeId, id: usize, ref_counts: &Arc>) -> Self { - ref_counts.lock().inc_value(tag_type_id, id); +impl ElementStateHandle { + fn new(tag_type_id: TypeId, id: ElementStateId, ref_counts: &Arc>) -> Self { + ref_counts.lock().inc_element_state(tag_type_id, id); Self { value_type: PhantomData, tag_type_id, @@ -2747,41 +3044,109 @@ impl ValueHandle { } } - pub fn read(&self, cx: &AppContext, f: impl FnOnce(&T) -> R) -> R { - f(cx.values - .read() + pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T { + cx.element_states .get(&(self.tag_type_id, self.id)) .unwrap() .downcast_ref() - .unwrap()) + .unwrap() } - pub fn update( - &self, - cx: &mut EventContext, - f: impl FnOnce(&mut T, &mut EventContext) -> R, - ) -> R { - let mut value = cx - .app + pub fn update(&self, cx: &mut C, f: impl FnOnce(&mut T, &mut C) -> R) -> R + where + C: DerefMut, + { + let mut element_state = cx + .deref_mut() .cx - .values - .write() + .element_states .remove(&(self.tag_type_id, self.id)) .unwrap(); - let result = f(value.downcast_mut().unwrap(), cx); - cx.app + let result = f(element_state.downcast_mut().unwrap(), cx); + cx.deref_mut() .cx - .values - .write() - .insert((self.tag_type_id, self.id), value); + .element_states + .insert((self.tag_type_id, self.id), element_state); result } } -impl Drop for ValueHandle { +impl Drop for ElementStateHandle { fn drop(&mut self) { if let Some(ref_counts) = self.ref_counts.upgrade() { - ref_counts.lock().dec_value(self.tag_type_id, self.id); + ref_counts + .lock() + .dec_element_state(self.tag_type_id, self.id); + } + } +} + +pub struct CursorStyleHandle { + id: usize, + next_cursor_style_handle_id: Arc, + platform: Arc, +} + +impl Drop for CursorStyleHandle { + fn drop(&mut self) { + if self.id + 1 == self.next_cursor_style_handle_id.load(SeqCst) { + self.platform.set_cursor_style(CursorStyle::Arrow); + } + } +} + +#[must_use] +pub enum Subscription { + Subscription { + id: usize, + entity_id: usize, + subscriptions: Option>>>>, + }, + Observation { + id: usize, + entity_id: usize, + observations: Option>>>>, + }, +} + +impl Subscription { + pub fn detach(&mut self) { + match self { + Subscription::Subscription { subscriptions, .. } => { + subscriptions.take(); + } + Subscription::Observation { observations, .. } => { + observations.take(); + } + } + } +} + +impl Drop for Subscription { + fn drop(&mut self) { + match self { + Subscription::Observation { + id, + entity_id, + observations, + } => { + if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) { + if let Some(observations) = observations.lock().get_mut(entity_id) { + observations.remove(id); + } + } + } + Subscription::Subscription { + id, + entity_id, + subscriptions, + } => { + if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) { + if let Some(subscriptions) = subscriptions.lock().get_mut(entity_id) { + subscriptions.remove(id); + } + } + } } } } @@ -2789,10 +3154,10 @@ impl Drop for ValueHandle { #[derive(Default)] struct RefCounts { entity_counts: HashMap, - value_counts: HashMap<(TypeId, usize), usize>, + element_state_counts: HashMap<(TypeId, ElementStateId), usize>, dropped_models: HashSet, dropped_views: HashSet<(usize, usize)>, - dropped_values: HashSet<(TypeId, usize)>, + dropped_element_states: HashSet<(TypeId, ElementStateId)>, } impl RefCounts { @@ -2816,8 +3181,14 @@ impl RefCounts { } } - fn inc_value(&mut self, tag_type_id: TypeId, id: usize) { - *self.value_counts.entry((tag_type_id, id)).or_insert(0) += 1; + fn inc_element_state(&mut self, tag_type_id: TypeId, id: ElementStateId) { + match self.element_state_counts.entry((tag_type_id, id)) { + Entry::Occupied(mut entry) => *entry.get_mut() += 1, + Entry::Vacant(entry) => { + entry.insert(1); + self.dropped_element_states.remove(&(tag_type_id, id)); + } + } } fn dec_model(&mut self, model_id: usize) { @@ -2838,13 +3209,13 @@ impl RefCounts { } } - fn dec_value(&mut self, tag_type_id: TypeId, id: usize) { + fn dec_element_state(&mut self, tag_type_id: TypeId, id: ElementStateId) { let key = (tag_type_id, id); - let count = self.value_counts.get_mut(&key).unwrap(); + let count = self.element_state_counts.get_mut(&key).unwrap(); *count -= 1; if *count == 0 { - self.value_counts.remove(&key); - self.dropped_values.insert(key); + self.element_state_counts.remove(&key); + self.dropped_element_states.insert(key); } } @@ -2857,48 +3228,21 @@ impl RefCounts { ) -> ( HashSet, HashSet<(usize, usize)>, - HashSet<(TypeId, usize)>, + HashSet<(TypeId, ElementStateId)>, ) { let mut dropped_models = HashSet::new(); let mut dropped_views = HashSet::new(); - let mut dropped_values = HashSet::new(); + let mut dropped_element_states = HashSet::new(); std::mem::swap(&mut self.dropped_models, &mut dropped_models); std::mem::swap(&mut self.dropped_views, &mut dropped_views); - std::mem::swap(&mut self.dropped_values, &mut dropped_values); - (dropped_models, dropped_views, dropped_values) + std::mem::swap( + &mut self.dropped_element_states, + &mut dropped_element_states, + ); + (dropped_models, dropped_views, dropped_element_states) } } -enum Subscription { - FromModel { - model_id: usize, - callback: Box, - }, - FromView { - window_id: usize, - view_id: usize, - callback: Box, - }, -} - -enum ModelObservation { - FromModel { - model_id: usize, - callback: Box, - }, - FromView { - window_id: usize, - view_id: usize, - callback: Box, - }, -} - -struct ViewObservation { - window_id: usize, - view_id: usize, - callback: Box, -} - #[cfg(test)] mod tests { use super::*; @@ -2922,10 +3266,12 @@ mod tests { if let Some(other) = other.as_ref() { cx.observe(other, |me, _, _| { me.events.push("notified".into()); - }); - cx.subscribe(other, |me, event, _| { + }) + .detach(); + cx.subscribe(other, |me, _, event, _| { me.events.push(format!("observed event {}", event)); - }); + }) + .detach(); } Self { @@ -2961,8 +3307,8 @@ mod tests { }); assert_eq!(cx.cx.models.len(), 1); - assert!(cx.subscriptions.is_empty()); - assert!(cx.model_observations.is_empty()); + assert!(cx.subscriptions.lock().is_empty()); + assert!(cx.observations.lock().is_empty()); } #[crate::test(self)] @@ -2981,20 +3327,22 @@ mod tests { let handle_2b = handle_2.clone(); handle_1.update(cx, |_, c| { - c.subscribe(&handle_2, move |model: &mut Model, event, c| { + c.subscribe(&handle_2, move |model: &mut Model, _, event, c| { model.events.push(*event); - c.subscribe(&handle_2b, |model, event, _| { + c.subscribe(&handle_2b, |model, _, event, _| { model.events.push(*event * 2); - }); - }); + }) + .detach(); + }) + .detach(); }); handle_2.update(cx, |_, c| c.emit(7)); assert_eq!(handle_1.read(cx).events, vec![7]); handle_2.update(cx, |_, c| c.emit(5)); - assert_eq!(handle_1.read(cx).events, vec![7, 10, 5]); + assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]); } #[crate::test(self)] @@ -3018,8 +3366,10 @@ mod tests { model.events.push(observed.read(c).count); c.observe(&handle_2b, |model, observed, c| { model.events.push(observed.read(c).count * 2); - }); - }); + }) + .detach(); + }) + .detach(); }); handle_2.update(cx, |model, c| { @@ -3032,7 +3382,7 @@ mod tests { model.count = 5; c.notify() }); - assert_eq!(handle_1.read(cx).events, vec![7, 10, 5]) + assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]) } #[crate::test(self)] @@ -3047,7 +3397,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3059,9 +3409,10 @@ mod tests { impl View { fn new(other: Option>, cx: &mut ViewContext) -> Self { if let Some(other) = other.as_ref() { - cx.subscribe_to_view(other, |me, _, event, _| { + cx.subscribe(other, |me, _, event, _| { me.events.push(format!("observed event {}", event)); - }); + }) + .detach(); } Self { other, @@ -3070,7 +3421,7 @@ mod tests { } } - let (window_id, _) = cx.add_window(|cx| View::new(None, cx)); + let (window_id, _) = cx.add_window(Default::default(), |cx| View::new(None, cx)); let handle_1 = cx.add_view(window_id, |cx| View::new(None, cx)); let handle_2 = cx.add_view(window_id, |cx| View::new(Some(handle_1.clone()), cx)); assert_eq!(cx.cx.views.len(), 3); @@ -3095,8 +3446,8 @@ mod tests { }); assert_eq!(cx.cx.views.len(), 2); - assert!(cx.subscriptions.is_empty()); - assert!(cx.model_observations.is_empty()); + assert!(cx.subscriptions.lock().is_empty()); + assert!(cx.observations.lock().is_empty()); } #[crate::test(self)] @@ -3110,7 +3461,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { let mouse_down_count = self.mouse_down_count.clone(); EventHandler::new(Empty::new().boxed()) .on_mouse_down(move |_| { @@ -3126,7 +3477,7 @@ mod tests { } let mouse_down_count = Arc::new(AtomicUsize::new(0)); - let (window_id, _) = cx.add_window(|_| View { + let (window_id, _) = cx.add_window(Default::default(), |_| View { mouse_down_count: mouse_down_count.clone(), }); let presenter = cx.presenters_and_platform_windows[&window_id].0.clone(); @@ -3172,7 +3523,7 @@ mod tests { "View" } - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -3184,7 +3535,7 @@ mod tests { released: model_released.clone(), }); - let (window_id, _) = cx.add_window(|_| View { + let (window_id, _) = cx.add_window(Default::default(), |_| View { released: view_released.clone(), }); @@ -3212,7 +3563,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3227,33 +3578,36 @@ mod tests { type Event = usize; } - let (window_id, handle_1) = cx.add_window(|_| View::default()); + let (window_id, handle_1) = cx.add_window(Default::default(), |_| View::default()); let handle_2 = cx.add_view(window_id, |_| View::default()); let handle_2b = handle_2.clone(); let handle_3 = cx.add_model(|_| Model); handle_1.update(cx, |_, c| { - c.subscribe_to_view(&handle_2, move |me, _, event, c| { + c.subscribe(&handle_2, move |me, _, event, c| { me.events.push(*event); - c.subscribe_to_view(&handle_2b, |me, _, event, _| { + c.subscribe(&handle_2b, |me, _, event, _| { me.events.push(*event * 2); - }); - }); + }) + .detach(); + }) + .detach(); - c.subscribe_to_model(&handle_3, |me, _, event, _| { + c.subscribe(&handle_3, |me, _, event, _| { me.events.push(*event); }) + .detach(); }); handle_2.update(cx, |_, c| c.emit(7)); assert_eq!(handle_1.read(cx).events, vec![7]); handle_2.update(cx, |_, c| c.emit(5)); - assert_eq!(handle_1.read(cx).events, vec![7, 10, 5]); + assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]); handle_3.update(cx, |_, c| c.emit(9)); - assert_eq!(handle_1.read(cx).events, vec![7, 10, 5, 9]); + assert_eq!(handle_1.read(cx).events, vec![7, 5, 10, 9]); } #[crate::test(self)] @@ -3265,7 +3619,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3280,18 +3634,18 @@ mod tests { type Event = (); } - let (window_id, _) = cx.add_window(|_| View); + let (window_id, _) = cx.add_window(Default::default(), |_| View); let observing_view = cx.add_view(window_id, |_| View); let emitting_view = cx.add_view(window_id, |_| View); let observing_model = cx.add_model(|_| Model); let observed_model = cx.add_model(|_| Model); observing_view.update(cx, |_, cx| { - cx.subscribe_to_view(&emitting_view, |_, _, _, _| {}); - cx.subscribe_to_model(&observed_model, |_, _, _, _| {}); + cx.subscribe(&emitting_view, |_, _, _, _| {}).detach(); + cx.subscribe(&observed_model, |_, _, _, _| {}).detach(); }); observing_model.update(cx, |_, cx| { - cx.subscribe(&observed_model, |_, _, _| {}); + cx.subscribe(&observed_model, |_, _, _, _| {}).detach(); }); cx.update(|| { @@ -3315,7 +3669,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3333,13 +3687,14 @@ mod tests { type Event = (); } - let (_, view) = cx.add_window(|_| View::default()); + let (_, view) = cx.add_window(Default::default(), |_| View::default()); let model = cx.add_model(|_| Model::default()); view.update(cx, |_, c| { - c.observe_model(&model, |me, observed, c| { + c.observe(&model, |me, observed, c| { me.events.push(observed.read(c).count) - }); + }) + .detach(); }); model.update(cx, |model, c| { @@ -3358,7 +3713,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3373,16 +3728,16 @@ mod tests { type Event = (); } - let (window_id, _) = cx.add_window(|_| View); + let (window_id, _) = cx.add_window(Default::default(), |_| View); let observing_view = cx.add_view(window_id, |_| View); let observing_model = cx.add_model(|_| Model); let observed_model = cx.add_model(|_| Model); observing_view.update(cx, |_, cx| { - cx.observe_model(&observed_model, |_, _, _| {}); + cx.observe(&observed_model, |_, _, _| {}).detach(); }); observing_model.update(cx, |_, cx| { - cx.observe(&observed_model, |_, _, _| {}); + cx.observe(&observed_model, |_, _, _| {}).detach(); }); cx.update(|| { @@ -3405,7 +3760,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3423,7 +3778,7 @@ mod tests { } let events: Arc>> = Default::default(); - let (window_id, view_1) = cx.add_window(|_| View { + let (window_id, view_1) = cx.add_window(Default::default(), |_| View { events: events.clone(), name: "view 1".to_string(), }); @@ -3463,7 +3818,7 @@ mod tests { } impl View for ViewA { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3481,7 +3836,7 @@ mod tests { } impl View for ViewB { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3490,31 +3845,29 @@ mod tests { } } - struct ActionArg { - foo: String, - } + action!(Action, &'static str); let actions = Rc::new(RefCell::new(Vec::new())); let actions_clone = actions.clone(); - cx.add_global_action("action", move |_: &ActionArg, _: &mut MutableAppContext| { + cx.add_global_action(move |_: &Action, _: &mut MutableAppContext| { actions_clone.borrow_mut().push("global a".to_string()); }); let actions_clone = actions.clone(); - cx.add_global_action("action", move |_: &ActionArg, _: &mut MutableAppContext| { + cx.add_global_action(move |_: &Action, _: &mut MutableAppContext| { actions_clone.borrow_mut().push("global b".to_string()); }); let actions_clone = actions.clone(); - cx.add_action("action", move |view: &mut ViewA, arg: &ActionArg, cx| { - assert_eq!(arg.foo, "bar"); + cx.add_action(move |view: &mut ViewA, action: &Action, cx| { + assert_eq!(action.0, "bar"); cx.propagate_action(); actions_clone.borrow_mut().push(format!("{} a", view.id)); }); let actions_clone = actions.clone(); - cx.add_action("action", move |view: &mut ViewA, _: &ActionArg, cx| { + cx.add_action(move |view: &mut ViewA, _: &Action, cx| { if view.id != 1 { cx.propagate_action(); } @@ -3522,18 +3875,18 @@ mod tests { }); let actions_clone = actions.clone(); - cx.add_action("action", move |view: &mut ViewB, _: &ActionArg, cx| { + cx.add_action(move |view: &mut ViewB, _: &Action, cx| { cx.propagate_action(); actions_clone.borrow_mut().push(format!("{} c", view.id)); }); let actions_clone = actions.clone(); - cx.add_action("action", move |view: &mut ViewB, _: &ActionArg, cx| { + cx.add_action(move |view: &mut ViewB, _: &Action, cx| { cx.propagate_action(); actions_clone.borrow_mut().push(format!("{} d", view.id)); }); - let (window_id, view_1) = cx.add_window(|_| ViewA { id: 1 }); + let (window_id, view_1) = cx.add_window(Default::default(), |_| ViewA { id: 1 }); let view_2 = cx.add_view(window_id, |_| ViewB { id: 2 }); let view_3 = cx.add_view(window_id, |_| ViewA { id: 3 }); let view_4 = cx.add_view(window_id, |_| ViewB { id: 4 }); @@ -3541,8 +3894,7 @@ mod tests { cx.dispatch_action( window_id, vec![view_1.id(), view_2.id(), view_3.id(), view_4.id()], - "action", - ActionArg { foo: "bar".into() }, + &Action("bar"), ); assert_eq!( @@ -3555,8 +3907,7 @@ mod tests { cx.dispatch_action( window_id, vec![view_2.id(), view_3.id(), view_4.id()], - "action", - ActionArg { foo: "bar".into() }, + &Action("bar"), ); assert_eq!( @@ -3569,10 +3920,7 @@ mod tests { fn test_dispatch_keystroke(cx: &mut MutableAppContext) { use std::cell::Cell; - #[derive(Clone)] - struct ActionArg { - key: String, - } + action!(Action, &'static str); struct View { id: usize, @@ -3584,7 +3932,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3613,22 +3961,24 @@ mod tests { view_2.keymap_context.set.insert("b".into()); view_3.keymap_context.set.insert("c".into()); - let (window_id, view_1) = cx.add_window(|_| view_1); + let (window_id, view_1) = cx.add_window(Default::default(), |_| view_1); let view_2 = cx.add_view(window_id, |_| view_2); let view_3 = cx.add_view(window_id, |_| view_3); // This keymap's only binding dispatches an action on view 2 because that view will have // "a" and "b" in its context, but not "c". - let binding = keymap::Binding::new("a", "action", Some("a && b && !c")) - .with_arg(ActionArg { key: "a".into() }); - cx.add_bindings(vec![binding]); + cx.add_bindings(vec![keymap::Binding::new( + "a", + Action("a"), + Some("a && b && !c"), + )]); let handled_action = Rc::new(Cell::new(false)); let handled_action_clone = handled_action.clone(); - cx.add_action("action", move |view: &mut View, arg: &ActionArg, _| { + cx.add_action(move |view: &mut View, action: &Action, _| { handled_action_clone.set(true); assert_eq!(view.id, 2); - assert_eq!(arg.key, "a"); + assert_eq!(action.0, "a"); }); cx.dispatch_keystroke( @@ -3717,7 +4067,7 @@ mod tests { "test view" } - fn render(&self, _: &RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -3762,7 +4112,7 @@ mod tests { "test view" } - fn render(&self, _: &RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -3785,7 +4135,7 @@ mod tests { "test view" } - fn render(&self, _: &RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } } diff --git a/gpui/src/elements.rs b/gpui/src/elements.rs index 3d0357409484aafb8916bcc70b3c3bf561f2fa17..6d7429222c7c1bf74d259932cbc5b3b614892ce3 100644 --- a/gpui/src/elements.rs +++ b/gpui/src/elements.rs @@ -5,11 +5,15 @@ mod container; mod empty; mod event_handler; mod flex; +mod hook; mod label; mod line_box; +mod list; mod mouse_event_handler; +mod overlay; mod stack; mod svg; +mod text; mod uniform_list; pub use crate::presenter::ChildView; @@ -20,26 +24,35 @@ pub use container::*; pub use empty::*; pub use event_handler::*; pub use flex::*; +pub use hook::*; pub use label::*; pub use line_box::*; +pub use list::*; pub use mouse_event_handler::*; +pub use overlay::*; pub use stack::*; pub use svg::*; +pub use text::*; pub use uniform_list::*; use crate::{ geometry::{rect::RectF, vector::Vector2F}, - json, AfterLayoutContext, DebugContext, Event, EventContext, LayoutContext, PaintContext, - SizeConstraint, + json, DebugContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; use core::panic; use json::ToJson; -use std::{any::Any, borrow::Cow, mem}; +use std::{ + any::Any, + borrow::Cow, + cell::RefCell, + mem, + ops::{Deref, DerefMut}, + rc::Rc, +}; trait AnyElement { fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F; - fn after_layout(&mut self, _: &mut AfterLayoutContext) {} - fn paint(&mut self, origin: Vector2F, cx: &mut PaintContext); + fn paint(&mut self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext); fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool; fn debug(&self, cx: &DebugContext) -> serde_json::Value; @@ -57,16 +70,10 @@ pub trait Element { cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState); - fn after_layout( - &mut self, - size: Vector2F, - layout: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ); - fn paint( &mut self, bounds: RectF, + visible_bounds: RectF, layout: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState; @@ -96,20 +103,20 @@ pub trait Element { where Self: 'static + Sized, { - ElementBox { + ElementBox(ElementRc { name: None, - element: Box::new(Lifecycle::Init { element: self }), - } + element: Rc::new(RefCell::new(Lifecycle::Init { element: self })), + }) } fn named(self, name: impl Into>) -> ElementBox where Self: 'static + Sized, { - ElementBox { + ElementBox(ElementRc { name: Some(name.into()), - element: Box::new(Lifecycle::Init { element: self }), - } + element: Rc::new(RefCell::new(Lifecycle::Init { element: self })), + }) } } @@ -132,9 +139,12 @@ pub enum Lifecycle { paint: T::PaintState, }, } -pub struct ElementBox { +pub struct ElementBox(ElementRc); + +#[derive(Clone)] +pub struct ElementRc { name: Option>, - element: Box, + element: Rc>, } impl AnyElement for Lifecycle { @@ -161,40 +171,49 @@ impl AnyElement for Lifecycle { result } - fn after_layout(&mut self, cx: &mut AfterLayoutContext) { - if let Lifecycle::PostLayout { - element, - size, - layout, - .. - } = self - { - element.after_layout(*size, layout, cx); - } else { - panic!("invalid element lifecycle state"); - } - } - - fn paint(&mut self, origin: Vector2F, cx: &mut PaintContext) { - *self = if let Lifecycle::PostLayout { - mut element, - constraint, - size, - mut layout, - } = mem::take(self) - { - let bounds = RectF::new(origin, size); - let paint = element.paint(bounds, &mut layout, cx); + fn paint(&mut self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext) { + *self = match mem::take(self) { + Lifecycle::PostLayout { + mut element, + constraint, + size, + mut layout, + } => { + let bounds = RectF::new(origin, size); + let visible_bounds = visible_bounds + .intersection(bounds) + .unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default())); + let paint = element.paint(bounds, visible_bounds, &mut layout, cx); + Lifecycle::PostPaint { + element, + constraint, + bounds, + layout, + paint, + } + } Lifecycle::PostPaint { - element, + mut element, constraint, bounds, - layout, - paint, + mut layout, + .. + } => { + let bounds = RectF::new(origin, bounds.size()); + let visible_bounds = visible_bounds + .intersection(bounds) + .unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default())); + let paint = element.paint(bounds, visible_bounds, &mut layout, cx); + Lifecycle::PostPaint { + element, + constraint, + bounds, + layout, + paint, + } } - } else { - panic!("invalid element lifecycle state"); - }; + _ => panic!("invalid element lifecycle state"), + } } fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool { @@ -264,32 +283,51 @@ impl Default for Lifecycle { } impl ElementBox { - pub fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F { - self.element.layout(constraint, cx) + pub fn metadata(&self) -> Option<&T> { + let element = unsafe { &*self.0.element.as_ptr() }; + element.metadata().and_then(|m| m.downcast_ref()) } +} - pub fn after_layout(&mut self, cx: &mut AfterLayoutContext) { - self.element.after_layout(cx); +impl Into for ElementBox { + fn into(self) -> ElementRc { + self.0 } +} + +impl Deref for ElementBox { + type Target = ElementRc; - pub fn paint(&mut self, origin: Vector2F, cx: &mut PaintContext) { - self.element.paint(origin, cx); + fn deref(&self) -> &Self::Target { + &self.0 } +} - pub fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool { - self.element.dispatch_event(event, cx) +impl DerefMut for ElementBox { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } +} - pub fn size(&self) -> Vector2F { - self.element.size() +impl ElementRc { + pub fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F { + self.element.borrow_mut().layout(constraint, cx) + } + + pub fn paint(&mut self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext) { + self.element.borrow_mut().paint(origin, visible_bounds, cx); + } + + pub fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool { + self.element.borrow_mut().dispatch_event(event, cx) } - pub fn metadata(&self) -> Option<&dyn Any> { - self.element.metadata() + pub fn size(&self) -> Vector2F { + self.element.borrow().size() } pub fn debug(&self, cx: &DebugContext) -> json::Value { - let mut value = self.element.debug(cx); + let mut value = self.element.borrow().debug(cx); if let Some(name) = &self.name { if let json::Value::Object(map) = &mut value { @@ -302,6 +340,15 @@ impl ElementBox { value } + + pub fn with_metadata(&self, f: F) -> R + where + T: 'static, + F: FnOnce(Option<&T>) -> R, + { + let element = self.element.borrow(); + f(element.metadata().and_then(|m| m.downcast_ref())) + } } pub trait ParentElement<'a>: Extend + Sized { diff --git a/gpui/src/elements/align.rs b/gpui/src/elements/align.rs index 5b3fd5d0b51f62c7ca36ab85afe2900d783efb60..652a014ddad275faa7e509be73f6bfe98099e096 100644 --- a/gpui/src/elements/align.rs +++ b/gpui/src/elements/align.rs @@ -1,9 +1,10 @@ use crate::{ - json, AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext, - LayoutContext, PaintContext, SizeConstraint, + geometry::{rect::RectF, vector::Vector2F}, + json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, }; use json::ToJson; -use pathfinder_geometry::vector::Vector2F; + use serde_json::json; pub struct Align { @@ -51,18 +52,10 @@ impl Element for Align { (size, ()) } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - self.child.after_layout(cx); - } - fn paint( &mut self, - bounds: pathfinder_geometry::rect::RectF, + bounds: RectF, + visible_bounds: RectF, _: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { @@ -72,8 +65,11 @@ impl Element for Align { let child_center = self.child.size() / 2.; let child_target = child_center + child_center * self.alignment; - self.child - .paint(bounds.origin() - (child_target - my_target), cx); + self.child.paint( + bounds.origin() - (child_target - my_target), + visible_bounds, + cx, + ); } fn dispatch_event( diff --git a/gpui/src/elements/canvas.rs b/gpui/src/elements/canvas.rs index e90c377be13c1285e5ff508e042089b6984efb46..d6220fd45f481a97e427ea1092512be3a1f0481d 100644 --- a/gpui/src/elements/canvas.rs +++ b/gpui/src/elements/canvas.rs @@ -9,13 +9,11 @@ use pathfinder_geometry::{ vector::{vec2f, Vector2F}, }; -pub struct Canvas(F) -where - F: FnMut(RectF, &mut PaintContext); +pub struct Canvas(F); impl Canvas where - F: FnMut(RectF, &mut PaintContext), + F: FnMut(RectF, RectF, &mut PaintContext), { pub fn new(f: F) -> Self { Self(f) @@ -24,7 +22,7 @@ where impl Element for Canvas where - F: FnMut(RectF, &mut PaintContext), + F: FnMut(RectF, RectF, &mut PaintContext), { type LayoutState = (); type PaintState = (); @@ -50,18 +48,11 @@ where fn paint( &mut self, bounds: RectF, + visible_bounds: RectF, _: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - self.0(bounds, cx) - } - - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - _: &mut crate::AfterLayoutContext, - ) { + self.0(bounds, visible_bounds, cx) } fn dispatch_event( diff --git a/gpui/src/elements/constrained_box.rs b/gpui/src/elements/constrained_box.rs index 3d50b70a57fb8a74c25a8b68f001acb7aa7ddf55..c712d71a9b7bac276568202108d0b2236ebcb60c 100644 --- a/gpui/src/elements/constrained_box.rs +++ b/gpui/src/elements/constrained_box.rs @@ -3,8 +3,8 @@ use serde_json::json; use crate::{ geometry::{rect::RectF, vector::Vector2F}, - json, AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext, - LayoutContext, PaintContext, SizeConstraint, + json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, }; pub struct ConstrainedBox { @@ -67,22 +67,14 @@ impl Element for ConstrainedBox { (size, ()) } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - self.child.after_layout(cx); - } - fn paint( &mut self, bounds: RectF, + visible_bounds: RectF, _: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - self.child.paint(bounds.origin(), cx); + self.child.paint(bounds.origin(), visible_bounds, cx); } fn dispatch_event( diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index ae13b5d82116539ac1cefc2ef5c8b66bbf1ff644..464776b5c3c606deec8d12ca94c1d6f2fec31c8d 100644 --- a/gpui/src/elements/container.rs +++ b/gpui/src/elements/container.rs @@ -10,8 +10,7 @@ use crate::{ }, json::ToJson, scene::{self, Border, Quad}, - AfterLayoutContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, - SizeConstraint, + Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; #[derive(Clone, Debug, Default, Deserialize)] @@ -167,18 +166,10 @@ impl Element for Container { (child_size + size_buffer, ()) } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - self.child.after_layout(cx); - } - fn paint( &mut self, bounds: RectF, + visible_bounds: RectF, _: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { @@ -208,7 +199,7 @@ impl Element for Container { self.style.border.left_width(), self.style.border.top_width(), ); - self.child.paint(child_origin, cx); + self.child.paint(child_origin, visible_bounds, cx); } fn dispatch_event( @@ -251,15 +242,11 @@ impl ToJson for ContainerStyle { } } -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Default)] pub struct Margin { - #[serde(default)] top: f32, - #[serde(default)] left: f32, - #[serde(default)] bottom: f32, - #[serde(default)] right: f32, } @@ -282,18 +269,85 @@ impl ToJson for Margin { } } -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Default)] pub struct Padding { - #[serde(default)] top: f32, - #[serde(default)] left: f32, - #[serde(default)] bottom: f32, - #[serde(default)] right: f32, } +impl<'de> Deserialize<'de> for Padding { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let spacing = Spacing::deserialize(deserializer)?; + Ok(match spacing { + Spacing::Uniform(size) => Padding { + top: size, + left: size, + bottom: size, + right: size, + }, + Spacing::Specific { + top, + left, + bottom, + right, + } => Padding { + top, + left, + bottom, + right, + }, + }) + } +} + +impl<'de> Deserialize<'de> for Margin { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let spacing = Spacing::deserialize(deserializer)?; + Ok(match spacing { + Spacing::Uniform(size) => Margin { + top: size, + left: size, + bottom: size, + right: size, + }, + Spacing::Specific { + top, + left, + bottom, + right, + } => Margin { + top, + left, + bottom, + right, + }, + }) + } +} +#[derive(Deserialize)] +#[serde(untagged)] +enum Spacing { + Uniform(f32), + Specific { + #[serde(default)] + top: f32, + #[serde(default)] + left: f32, + #[serde(default)] + bottom: f32, + #[serde(default)] + right: f32, + }, +} + impl ToJson for Padding { fn to_json(&self) -> serde_json::Value { let mut value = json!({}); diff --git a/gpui/src/elements/empty.rs b/gpui/src/elements/empty.rs index fe9ff3c9b90862443be9b7c6eb17ff7d23251808..d6041bd9587f0c5694f374e7a152285876f85b5a 100644 --- a/gpui/src/elements/empty.rs +++ b/gpui/src/elements/empty.rs @@ -6,9 +6,7 @@ use crate::{ json::{json, ToJson}, DebugContext, }; -use crate::{ - AfterLayoutContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, -}; +use crate::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint}; pub struct Empty; @@ -41,12 +39,10 @@ impl Element for Empty { (vec2f(x, y), ()) } - fn after_layout(&mut self, _: Vector2F, _: &mut Self::LayoutState, _: &mut AfterLayoutContext) { - } - fn paint( &mut self, _: RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut PaintContext, ) -> Self::PaintState { diff --git a/gpui/src/elements/event_handler.rs b/gpui/src/elements/event_handler.rs index a66778f8b7c91d979ee13eb4fd1de804702f4eb0..f880b0bb6cf85f976204547041f5964c51110d56 100644 --- a/gpui/src/elements/event_handler.rs +++ b/gpui/src/elements/event_handler.rs @@ -2,8 +2,8 @@ use pathfinder_geometry::rect::RectF; use serde_json::json; use crate::{ - geometry::vector::Vector2F, AfterLayoutContext, DebugContext, Element, ElementBox, Event, - EventContext, LayoutContext, PaintContext, SizeConstraint, + geometry::vector::Vector2F, DebugContext, Element, ElementBox, Event, EventContext, + LayoutContext, PaintContext, SizeConstraint, }; pub struct EventHandler { @@ -41,22 +41,14 @@ impl Element for EventHandler { (size, ()) } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - self.child.after_layout(cx); - } - fn paint( &mut self, bounds: RectF, + visible_bounds: RectF, _: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - self.child.paint(bounds.origin(), cx); + self.child.paint(bounds.origin(), visible_bounds, cx); } fn dispatch_event( diff --git a/gpui/src/elements/flex.rs b/gpui/src/elements/flex.rs index 1ed9204de4796fc45056acaf16f550de7a181287..e2bd7eb1c97f8838d5acaf348fda08a748b15e54 100644 --- a/gpui/src/elements/flex.rs +++ b/gpui/src/elements/flex.rs @@ -2,8 +2,8 @@ use std::{any::Any, f32::INFINITY}; use crate::{ json::{self, ToJson, Value}, - AfterLayoutContext, Axis, DebugContext, Element, ElementBox, Event, EventContext, - LayoutContext, PaintContext, SizeConstraint, Vector2FExt, + Axis, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, Vector2FExt, }; use pathfinder_geometry::{ rect::RectF, @@ -32,11 +32,46 @@ impl Flex { Self::new(Axis::Vertical) } - fn child_flex<'b>(child: &ElementBox) -> Option { - child - .metadata() - .and_then(|d| d.downcast_ref::()) - .map(|data| data.flex) + fn layout_flex_children( + &mut self, + expanded: bool, + constraint: SizeConstraint, + remaining_space: &mut f32, + remaining_flex: &mut f32, + cross_axis_max: &mut f32, + cx: &mut LayoutContext, + ) { + let cross_axis = self.axis.invert(); + for child in &mut self.children { + if let Some(metadata) = child.metadata::() { + if metadata.expanded != expanded { + continue; + } + + let flex = metadata.flex; + let child_max = if *remaining_flex == 0.0 { + *remaining_space + } else { + let space_per_flex = *remaining_space / *remaining_flex; + space_per_flex * flex + }; + let child_min = if expanded { child_max } else { 0. }; + let child_constraint = match self.axis { + Axis::Horizontal => SizeConstraint::new( + vec2f(child_min, constraint.min.y()), + vec2f(child_max, constraint.max.y()), + ), + Axis::Vertical => SizeConstraint::new( + vec2f(constraint.min.x(), child_min), + vec2f(constraint.max.x(), child_max), + ), + }; + let child_size = child.layout(child_constraint, cx); + *remaining_space -= child_size.along(self.axis); + *remaining_flex -= flex; + *cross_axis_max = cross_axis_max.max(child_size.along(cross_axis)); + } + } } } @@ -47,7 +82,7 @@ impl Extend for Flex { } impl Element for Flex { - type LayoutState = (); + type LayoutState = bool; type PaintState = (); fn layout( @@ -55,14 +90,14 @@ impl Element for Flex { constraint: SizeConstraint, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - let mut total_flex = 0.0; + let mut total_flex = None; let mut fixed_space = 0.0; let cross_axis = self.axis.invert(); let mut cross_axis_max: f32 = 0.0; for child in &mut self.children { - if let Some(flex) = Self::child_flex(&child) { - total_flex += flex; + if let Some(metadata) = child.metadata::() { + *total_flex.get_or_insert(0.) += metadata.flex; } else { let child_constraint = match self.axis { Axis::Horizontal => SizeConstraint::new( @@ -80,37 +115,28 @@ impl Element for Flex { } } - let mut size = if total_flex > 0.0 { + let mut size = if let Some(mut remaining_flex) = total_flex { if constraint.max_along(self.axis).is_infinite() { panic!("flex contains flexible children but has an infinite constraint along the flex axis"); } let mut remaining_space = constraint.max_along(self.axis) - fixed_space; - let mut remaining_flex = total_flex; - for child in &mut self.children { - if let Some(flex) = Self::child_flex(&child) { - let child_max = if remaining_flex == 0.0 { - remaining_space - } else { - let space_per_flex = remaining_space / remaining_flex; - space_per_flex * flex - }; - let child_constraint = match self.axis { - Axis::Horizontal => SizeConstraint::new( - vec2f(0.0, constraint.min.y()), - vec2f(child_max, constraint.max.y()), - ), - Axis::Vertical => SizeConstraint::new( - vec2f(constraint.min.x(), 0.0), - vec2f(constraint.max.x(), child_max), - ), - }; - let child_size = child.layout(child_constraint, cx); - remaining_space -= child_size.along(self.axis); - remaining_flex -= flex; - cross_axis_max = cross_axis_max.max(child_size.along(cross_axis)); - } - } + self.layout_flex_children( + false, + constraint, + &mut remaining_space, + &mut remaining_flex, + &mut cross_axis_max, + cx, + ); + self.layout_flex_children( + true, + constraint, + &mut remaining_space, + &mut remaining_flex, + &mut cross_axis_max, + cx, + ); match self.axis { Axis::Horizontal => vec2f(constraint.max.x() - remaining_space, cross_axis_max), @@ -126,39 +152,44 @@ impl Element for Flex { if constraint.min.x().is_finite() { size.set_x(size.x().max(constraint.min.x())); } - if constraint.min.y().is_finite() { size.set_y(size.y().max(constraint.min.y())); } - (size, ()) - } - - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - for child in &mut self.children { - child.after_layout(cx); + let mut overflowing = false; + if size.x() > constraint.max.x() { + size.set_x(constraint.max.x()); + overflowing = true; + } + if size.y() > constraint.max.y() { + size.set_y(constraint.max.y()); + overflowing = true; } + + (size, overflowing) } fn paint( &mut self, bounds: RectF, - _: &mut Self::LayoutState, + visible_bounds: RectF, + overflowing: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { + if *overflowing { + cx.scene.push_layer(Some(bounds)); + } let mut child_origin = bounds.origin(); for child in &mut self.children { - child.paint(child_origin, cx); + child.paint(child_origin, visible_bounds, cx); match self.axis { Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0), Axis::Vertical => child_origin += vec2f(0.0, child.size().y()), } } + if *overflowing { + cx.scene.pop_layer(); + } } fn dispatch_event( @@ -194,6 +225,7 @@ impl Element for Flex { struct FlexParentData { flex: f32, + expanded: bool, } pub struct Expanded { @@ -204,7 +236,10 @@ pub struct Expanded { impl Expanded { pub fn new(flex: f32, child: ElementBox) -> Self { Expanded { - metadata: FlexParentData { flex }, + metadata: FlexParentData { + flex, + expanded: true, + }, child, } } @@ -223,22 +258,84 @@ impl Element for Expanded { (size, ()) } - fn after_layout( + fn paint( &mut self, - _: Vector2F, + bounds: RectF, + visible_bounds: RectF, _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - self.child.after_layout(cx); + cx: &mut PaintContext, + ) -> Self::PaintState { + self.child.paint(bounds.origin(), visible_bounds, cx) + } + + fn dispatch_event( + &mut self, + event: &Event, + _: RectF, + _: &mut Self::LayoutState, + _: &mut Self::PaintState, + cx: &mut EventContext, + ) -> bool { + self.child.dispatch_event(event, cx) + } + + fn metadata(&self) -> Option<&dyn Any> { + Some(&self.metadata) + } + + fn debug( + &self, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &DebugContext, + ) -> Value { + json!({ + "type": "Expanded", + "flex": self.metadata.flex, + "child": self.child.debug(cx) + }) + } +} + +pub struct Flexible { + metadata: FlexParentData, + child: ElementBox, +} + +impl Flexible { + pub fn new(flex: f32, child: ElementBox) -> Self { + Flexible { + metadata: FlexParentData { + flex, + expanded: false, + }, + child, + } + } +} + +impl Element for Flexible { + type LayoutState = (); + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + cx: &mut LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + let size = self.child.layout(constraint, cx); + (size, ()) } fn paint( &mut self, bounds: RectF, + visible_bounds: RectF, _: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - self.child.paint(bounds.origin(), cx) + self.child.paint(bounds.origin(), visible_bounds, cx) } fn dispatch_event( @@ -264,7 +361,7 @@ impl Element for Expanded { cx: &DebugContext, ) -> Value { json!({ - "type": "Expanded", + "type": "Flexible", "flex": self.metadata.flex, "child": self.child.debug(cx) }) diff --git a/gpui/src/elements/hook.rs b/gpui/src/elements/hook.rs new file mode 100644 index 0000000000000000000000000000000000000000..994d5fe281ca1dae5984dd8456ffa32ce5be97a1 --- /dev/null +++ b/gpui/src/elements/hook.rs @@ -0,0 +1,79 @@ +use crate::{ + geometry::{rect::RectF, vector::Vector2F}, + json::json, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, +}; + +pub struct Hook { + child: ElementBox, + after_layout: Option>, +} + +impl Hook { + pub fn new(child: ElementBox) -> Self { + Self { + child, + after_layout: None, + } + } + + pub fn on_after_layout( + mut self, + f: impl 'static + FnMut(Vector2F, &mut LayoutContext), + ) -> Self { + self.after_layout = Some(Box::new(f)); + self + } +} + +impl Element for Hook { + type LayoutState = (); + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + cx: &mut LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + let size = self.child.layout(constraint, cx); + if let Some(handler) = self.after_layout.as_mut() { + handler(size, cx); + } + (size, ()) + } + + fn paint( + &mut self, + bounds: RectF, + visible_bounds: RectF, + _: &mut Self::LayoutState, + cx: &mut PaintContext, + ) { + self.child.paint(bounds.origin(), visible_bounds, cx); + } + + fn dispatch_event( + &mut self, + event: &Event, + _: RectF, + _: &mut Self::LayoutState, + _: &mut Self::PaintState, + cx: &mut EventContext, + ) -> bool { + self.child.dispatch_event(event, cx) + } + + fn debug( + &self, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &DebugContext, + ) -> serde_json::Value { + json!({ + "type": "Hooks", + "child": self.child.debug(cx), + }) + } +} diff --git a/gpui/src/elements/label.rs b/gpui/src/elements/label.rs index 72f755905cad91b179b9b9421c5feeec0a3b7c5f..acfbb5abd9e7dfa9c33004bc34522b16a7bb59a7 100644 --- a/gpui/src/elements/label.rs +++ b/gpui/src/elements/label.rs @@ -1,15 +1,12 @@ use crate::{ - color::Color, - font_cache::FamilyId, - fonts::{FontId, TextStyle}, + fonts::TextStyle, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }, json::{ToJson, Value}, - text_layout::Line, - AfterLayoutContext, DebugContext, Element, Event, EventContext, FontCache, LayoutContext, - PaintContext, SizeConstraint, + text_layout::{Line, RunStyle}, + DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; use serde::Deserialize; use serde_json::json; @@ -17,85 +14,85 @@ use smallvec::{smallvec, SmallVec}; pub struct Label { text: String, - family_id: FamilyId, - font_size: f32, style: LabelStyle, highlight_indices: Vec, } -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct LabelStyle { pub text: TextStyle, pub highlight_text: Option, } +impl From for LabelStyle { + fn from(text: TextStyle) -> Self { + LabelStyle { + text, + highlight_text: None, + } + } +} + impl Label { - pub fn new(text: String, family_id: FamilyId, font_size: f32) -> Self { + pub fn new(text: String, style: impl Into) -> Self { Self { text, - family_id, - font_size, highlight_indices: Default::default(), - style: Default::default(), + style: style.into(), } } - pub fn with_style(mut self, style: &LabelStyle) -> Self { - self.style = style.clone(); - self - } - - pub fn with_default_color(mut self, color: Color) -> Self { - self.style.text.color = color; - self - } - pub fn with_highlights(mut self, indices: Vec) -> Self { self.highlight_indices = indices; self } - fn compute_runs( - &self, - font_cache: &FontCache, - font_id: FontId, - ) -> SmallVec<[(usize, FontId, Color); 8]> { + fn compute_runs(&self) -> SmallVec<[(usize, RunStyle); 8]> { + let font_id = self.style.text.font_id; if self.highlight_indices.is_empty() { - return smallvec![(self.text.len(), font_id, self.style.text.color)]; + return smallvec![( + self.text.len(), + RunStyle { + font_id, + color: self.style.text.color, + underline: self.style.text.underline, + } + )]; } let highlight_font_id = self .style .highlight_text .as_ref() - .and_then(|style| { - font_cache - .select_font(self.family_id, &style.font_properties) - .ok() - }) - .unwrap_or(font_id); + .map_or(font_id, |style| style.font_id); let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); let mut runs = SmallVec::new(); + let highlight_style = self + .style + .highlight_text + .as_ref() + .unwrap_or(&self.style.text); for (char_ix, c) in self.text.char_indices() { let mut font_id = font_id; let mut color = self.style.text.color; + let mut underline = self.style.text.underline; if let Some(highlight_ix) = highlight_indices.peek() { if char_ix == *highlight_ix { font_id = highlight_font_id; - color = self - .style - .highlight_text - .as_ref() - .unwrap_or(&self.style.text) - .color; + color = highlight_style.color; + underline = highlight_style.underline; highlight_indices.next(); } } - let push_new_run = if let Some((last_len, last_font_id, last_color)) = runs.last_mut() { - if font_id == *last_font_id && color == *last_color { + let last_run: Option<&mut (usize, RunStyle)> = runs.last_mut(); + let push_new_run = if let Some((last_len, last_style)) = last_run { + if font_id == last_style.font_id + && color == last_style.color + && underline == last_style.underline + { *last_len += c.len_utf8(); false } else { @@ -106,7 +103,14 @@ impl Label { }; if push_new_run { - runs.push((c.len_utf8(), font_id, color)); + runs.push(( + c.len_utf8(), + RunStyle { + font_id, + color, + underline, + }, + )); } } @@ -123,37 +127,31 @@ impl Element for Label { constraint: SizeConstraint, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - let font_id = cx - .font_cache - .select_font(self.family_id, &self.style.text.font_properties) - .unwrap(); - let runs = self.compute_runs(&cx.font_cache, font_id); - let line = - cx.text_layout_cache - .layout_str(self.text.as_str(), self.font_size, runs.as_slice()); + let runs = self.compute_runs(); + let line = cx.text_layout_cache.layout_str( + self.text.as_str(), + self.style.text.font_size, + runs.as_slice(), + ); let size = vec2f( line.width().max(constraint.min.x()).min(constraint.max.x()), - cx.font_cache.line_height(font_id, self.font_size).ceil(), + cx.font_cache + .line_height(self.style.text.font_id, self.style.text.font_size) + .ceil(), ); (size, line) } - fn after_layout(&mut self, _: Vector2F, _: &mut Self::LayoutState, _: &mut AfterLayoutContext) { - } - fn paint( &mut self, bounds: RectF, + visible_bounds: RectF, line: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - line.paint( - bounds.origin(), - RectF::new(vec2f(0., 0.), bounds.size()), - cx, - ) + line.paint(bounds.origin(), visible_bounds, bounds.size().y(), cx) } fn dispatch_event( @@ -172,15 +170,13 @@ impl Element for Label { bounds: RectF, _: &Self::LayoutState, _: &Self::PaintState, - cx: &DebugContext, + _: &DebugContext, ) -> Value { json!({ "type": "Label", "bounds": bounds.to_json(), "text": &self.text, "highlight_indices": self.highlight_indices, - "font_family": cx.font_cache.family_name(self.family_id).unwrap(), - "font_size": self.font_size, "style": self.style.to_json(), }) } @@ -200,52 +196,65 @@ impl ToJson for LabelStyle { #[cfg(test)] mod tests { use super::*; + use crate::color::Color; use crate::fonts::{Properties as FontProperties, Weight}; #[crate::test(self)] fn test_layout_label_with_highlights(cx: &mut crate::MutableAppContext) { - let menlo = cx.font_cache().load_family(&["Menlo"]).unwrap(); - let menlo_regular = cx - .font_cache() - .select_font(menlo, &FontProperties::new()) - .unwrap(); - let menlo_bold = cx - .font_cache() - .select_font(menlo, FontProperties::new().weight(Weight::BOLD)) - .unwrap(); - let black = Color::black(); - let red = Color::new(255, 0, 0, 255); - - let label = Label::new(".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(), menlo, 12.0) - .with_style(&LabelStyle { - text: TextStyle { - color: black, - font_properties: Default::default(), - }, - highlight_text: Some(TextStyle { - color: red, - font_properties: *FontProperties::new().weight(Weight::BOLD), - }), - }) - .with_highlights(vec![ - ".α".len(), - ".αβ".len(), - ".αβγδ".len(), - ".αβγδε.ⓐ".len(), - ".αβγδε.ⓐⓑ".len(), - ]); - - let runs = label.compute_runs(cx.font_cache().as_ref(), menlo_regular); + let default_style = TextStyle::new( + "Menlo", + 12., + Default::default(), + false, + Color::black(), + cx.font_cache(), + ) + .unwrap(); + let highlight_style = TextStyle::new( + "Menlo", + 12., + *FontProperties::new().weight(Weight::BOLD), + false, + Color::new(255, 0, 0, 255), + cx.font_cache(), + ) + .unwrap(); + let label = Label::new( + ".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(), + LabelStyle { + text: default_style.clone(), + highlight_text: Some(highlight_style.clone()), + }, + ) + .with_highlights(vec![ + ".α".len(), + ".αβ".len(), + ".αβγδ".len(), + ".αβγδε.ⓐ".len(), + ".αβγδε.ⓐⓑ".len(), + ]); + + let default_run_style = RunStyle { + font_id: default_style.font_id, + color: default_style.color, + underline: default_style.underline, + }; + let highlight_run_style = RunStyle { + font_id: highlight_style.font_id, + color: highlight_style.color, + underline: highlight_style.underline, + }; + let runs = label.compute_runs(); assert_eq!( runs.as_slice(), &[ - (".α".len(), menlo_regular, black), - ("βγ".len(), menlo_bold, red), - ("δ".len(), menlo_regular, black), - ("ε".len(), menlo_bold, red), - (".ⓐ".len(), menlo_regular, black), - ("ⓑⓒ".len(), menlo_bold, red), - ("ⓓⓔ.abcde.".len(), menlo_regular, black), + (".α".len(), default_run_style), + ("βγ".len(), highlight_run_style), + ("δ".len(), default_run_style), + ("ε".len(), highlight_run_style), + (".ⓐ".len(), default_run_style), + ("ⓑⓒ".len(), highlight_run_style), + ("ⓓⓔ.abcde.".len(), default_run_style), ] ); } diff --git a/gpui/src/elements/line_box.rs b/gpui/src/elements/line_box.rs index 16baf6e00ba5d483891a41088248bf32fd4b1270..33fd2510c8869fce725cf4ab7482e5468b142c9a 100644 --- a/gpui/src/elements/line_box.rs +++ b/gpui/src/elements/line_box.rs @@ -1,30 +1,22 @@ use crate::{ - font_cache::FamilyId, - fonts::Properties, + fonts::TextStyle, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }, json::{json, ToJson}, - AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, - PaintContext, SizeConstraint, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, }; pub struct LineBox { child: ElementBox, - family_id: FamilyId, - font_size: f32, - font_properties: Properties, + style: TextStyle, } impl LineBox { - pub fn new(family_id: FamilyId, font_size: f32, child: ElementBox) -> Self { - Self { - child, - family_id, - font_size, - font_properties: Properties::default(), - } + pub fn new(child: ElementBox, style: TextStyle) -> Self { + Self { child, style } } } @@ -37,52 +29,41 @@ impl Element for LineBox { constraint: SizeConstraint, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - match cx + let line_height = cx .font_cache - .select_font(self.family_id, &self.font_properties) - { - Ok(font_id) => { - let line_height = cx.font_cache.line_height(font_id, self.font_size); - let character_height = cx.font_cache.ascent(font_id, self.font_size) - + cx.font_cache.descent(font_id, self.font_size); - let child_max = vec2f(constraint.max.x(), character_height); - let child_size = self.child.layout( - SizeConstraint::new(constraint.min.min(child_max), child_max), - cx, - ); - let size = vec2f(child_size.x(), line_height); - (size, (line_height - character_height) / 2.) - } - Err(error) => { - log::error!("can't find font for LineBox: {}", error); - (constraint.min, 0.0) - } - } - } - - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - self.child.after_layout(cx); + .line_height(self.style.font_id, self.style.font_size); + let character_height = cx + .font_cache + .ascent(self.style.font_id, self.style.font_size) + + cx.font_cache + .descent(self.style.font_id, self.style.font_size); + let child_max = vec2f(constraint.max.x(), character_height); + let child_size = self.child.layout( + SizeConstraint::new(constraint.min.min(child_max), child_max), + cx, + ); + let size = vec2f(child_size.x(), line_height); + (size, (line_height - character_height) / 2.) } fn paint( &mut self, - bounds: pathfinder_geometry::rect::RectF, + bounds: RectF, + visible_bounds: RectF, padding_top: &mut f32, cx: &mut PaintContext, ) -> Self::PaintState { - self.child - .paint(bounds.origin() + vec2f(0., *padding_top), cx); + self.child.paint( + bounds.origin() + vec2f(0., *padding_top), + visible_bounds, + cx, + ); } fn dispatch_event( &mut self, event: &Event, - _: pathfinder_geometry::rect::RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, cx: &mut EventContext, @@ -99,9 +80,7 @@ impl Element for LineBox { ) -> serde_json::Value { json!({ "bounds": bounds.to_json(), - "font_family": cx.font_cache.family_name(self.family_id).unwrap(), - "font_size": self.font_size, - "font_properties": self.font_properties.to_json(), + "style": self.style.to_json(), "child": self.child.debug(cx), }) } diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs new file mode 100644 index 0000000000000000000000000000000000000000..1a86e2935cd774837d2dbf03f12acd089c4e487b --- /dev/null +++ b/gpui/src/elements/list.rs @@ -0,0 +1,864 @@ +use crate::{ + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, + json::json, + sum_tree::{self, Bias, SumTree}, + DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, +}; +use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc}; + +pub struct List { + state: ListState, +} + +#[derive(Clone)] +pub struct ListState(Rc>); + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Orientation { + Top, + Bottom, +} + +struct StateInner { + last_layout_width: Option, + render_item: Box ElementBox>, + rendered_range: Range, + items: SumTree, + logical_scroll_top: Option, + orientation: Orientation, + overdraw: f32, + scroll_handler: Option, &mut EventContext)>>, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct ListOffset { + item_ix: usize, + offset_in_item: f32, +} + +#[derive(Clone)] +enum ListItem { + Unrendered, + Rendered(ElementRc), + Removed(f32), +} + +impl std::fmt::Debug for ListItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Unrendered => write!(f, "Unrendered"), + Self::Rendered(_) => f.debug_tuple("Rendered").finish(), + Self::Removed(height) => f.debug_tuple("Removed").field(height).finish(), + } + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +struct ListItemSummary { + count: usize, + rendered_count: usize, + unrendered_count: usize, + height: f32, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +struct Count(usize); + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +struct RenderedCount(usize); + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +struct UnrenderedCount(usize); + +#[derive(Clone, Debug, Default)] +struct Height(f32); + +impl List { + pub fn new(state: ListState) -> Self { + Self { state } + } +} + +impl Element for List { + type LayoutState = ListOffset; + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + cx: &mut LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + let state = &mut *self.state.0.borrow_mut(); + let size = constraint.max; + let mut item_constraint = constraint; + item_constraint.min.set_y(0.); + item_constraint.max.set_y(f32::INFINITY); + + if cx.refreshing || state.last_layout_width != Some(size.x()) { + state.rendered_range = 0..0; + state.items = SumTree::from_iter( + (0..state.items.summary().count).map(|_| ListItem::Unrendered), + &(), + ) + } + + let old_items = state.items.clone(); + let mut new_items = SumTree::new(); + let mut rendered_items = VecDeque::new(); + let mut rendered_height = 0.; + let mut scroll_top = state + .logical_scroll_top + .unwrap_or_else(|| match state.orientation { + Orientation::Top => ListOffset { + item_ix: 0, + offset_in_item: 0., + }, + Orientation::Bottom => ListOffset { + item_ix: state.items.summary().count, + offset_in_item: 0., + }, + }); + + // Render items after the scroll top, including those in the trailing overdraw. + let mut cursor = old_items.cursor::(); + cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + for (ix, item) in cursor.by_ref().enumerate() { + if rendered_height - scroll_top.offset_in_item >= size.y() + state.overdraw { + break; + } + + let element = state.render_item(scroll_top.item_ix + ix, item, item_constraint, cx); + rendered_height += element.size().y(); + rendered_items.push_back(ListItem::Rendered(element)); + } + + // Prepare to start walking upward from the item at the scroll top. + cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + + // If the rendered items do not fill the visible region, then adjust + // the scroll top upward. + if rendered_height - scroll_top.offset_in_item < size.y() { + while rendered_height < size.y() { + cursor.prev(&()); + if let Some(item) = cursor.item() { + let element = + state.render_item(cursor.seek_start().0, item, item_constraint, cx); + rendered_height += element.size().y(); + rendered_items.push_front(ListItem::Rendered(element)); + } else { + break; + } + } + + scroll_top = ListOffset { + item_ix: cursor.seek_start().0, + offset_in_item: rendered_height - size.y(), + }; + + match state.orientation { + Orientation::Top => { + scroll_top.offset_in_item = scroll_top.offset_in_item.max(0.); + state.logical_scroll_top = Some(scroll_top); + } + Orientation::Bottom => { + scroll_top = ListOffset { + item_ix: cursor.seek_start().0, + offset_in_item: rendered_height - size.y(), + }; + state.logical_scroll_top = None; + } + }; + } + + // Render items in the leading overdraw. + let mut leading_overdraw = scroll_top.offset_in_item; + while leading_overdraw < state.overdraw { + cursor.prev(&()); + if let Some(item) = cursor.item() { + let element = state.render_item(cursor.seek_start().0, item, item_constraint, cx); + leading_overdraw += element.size().y(); + rendered_items.push_front(ListItem::Rendered(element)); + } else { + break; + } + } + + let new_rendered_range = + cursor.seek_start().0..(cursor.seek_start().0 + rendered_items.len()); + + let mut cursor = old_items.cursor::(); + + if state.rendered_range.start < new_rendered_range.start { + new_items.push_tree( + cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()), + &(), + ); + let remove_to = state.rendered_range.end.min(new_rendered_range.start); + while cursor.seek_start().0 < remove_to { + new_items.push(cursor.item().unwrap().remove(), &()); + cursor.next(&()); + } + } + new_items.push_tree( + cursor.slice(&Count(new_rendered_range.start), Bias::Right, &()), + &(), + ); + + new_items.extend(rendered_items, &()); + cursor.seek(&Count(new_rendered_range.end), Bias::Right, &()); + + if new_rendered_range.end < state.rendered_range.start { + new_items.push_tree( + cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()), + &(), + ); + } + while cursor.seek_start().0 < state.rendered_range.end { + new_items.push(cursor.item().unwrap().remove(), &()); + cursor.next(&()); + } + + new_items.push_tree(cursor.suffix(&()), &()); + + state.items = new_items; + state.rendered_range = new_rendered_range; + state.last_layout_width = Some(size.x()); + (size, scroll_top) + } + + fn paint( + &mut self, + bounds: RectF, + visible_bounds: RectF, + scroll_top: &mut ListOffset, + cx: &mut PaintContext, + ) { + cx.scene.push_layer(Some(bounds)); + + let state = &mut *self.state.0.borrow_mut(); + for (mut element, origin) in state.visible_elements(bounds, scroll_top) { + element.paint(origin, visible_bounds, cx); + } + + cx.scene.pop_layer(); + } + + fn dispatch_event( + &mut self, + event: &Event, + bounds: RectF, + scroll_top: &mut ListOffset, + _: &mut (), + cx: &mut EventContext, + ) -> bool { + let mut handled = false; + + let mut state = self.state.0.borrow_mut(); + for (mut element, _) in state.visible_elements(bounds, scroll_top) { + handled = element.dispatch_event(event, cx) || handled; + } + + match event { + Event::ScrollWheel { + position, + delta, + precise, + } => { + if bounds.contains_point(*position) { + if state.scroll(scroll_top, bounds.height(), *delta, *precise, cx) { + handled = true; + } + } + } + _ => {} + } + + handled + } + + fn debug( + &self, + bounds: RectF, + scroll_top: &Self::LayoutState, + _: &(), + cx: &DebugContext, + ) -> serde_json::Value { + let state = self.state.0.borrow_mut(); + let visible_elements = state + .visible_elements(bounds, scroll_top) + .map(|e| e.0.debug(cx)) + .collect::>(); + let visible_range = scroll_top.item_ix..(scroll_top.item_ix + visible_elements.len()); + json!({ + "visible_range": visible_range, + "visible_elements": visible_elements, + "scroll_top": state.logical_scroll_top.map(|top| (top.item_ix, top.offset_in_item)), + }) + } +} + +impl ListState { + pub fn new( + element_count: usize, + orientation: Orientation, + overdraw: f32, + render_item: F, + ) -> Self + where + F: 'static + FnMut(usize, &mut LayoutContext) -> ElementBox, + { + let mut items = SumTree::new(); + items.extend((0..element_count).map(|_| ListItem::Unrendered), &()); + Self(Rc::new(RefCell::new(StateInner { + last_layout_width: None, + render_item: Box::new(render_item), + rendered_range: 0..0, + items, + logical_scroll_top: None, + orientation, + overdraw, + scroll_handler: None, + }))) + } + + pub fn reset(&self, element_count: usize) { + let state = &mut *self.0.borrow_mut(); + state.rendered_range = 0..0; + state.logical_scroll_top = None; + state.items = SumTree::new(); + state + .items + .extend((0..element_count).map(|_| ListItem::Unrendered), &()); + } + + pub fn splice(&self, old_range: Range, count: usize) { + let state = &mut *self.0.borrow_mut(); + + if let Some(ListOffset { + item_ix, + offset_in_item, + }) = state.logical_scroll_top.as_mut() + { + if old_range.contains(item_ix) { + *item_ix = old_range.start; + *offset_in_item = 0.; + } else if old_range.end <= *item_ix { + *item_ix = *item_ix - (old_range.end - old_range.start) + count; + } + } + + let new_end = old_range.start + count; + if old_range.start < state.rendered_range.start { + state.rendered_range.start = + new_end + state.rendered_range.start.saturating_sub(old_range.end); + } + if old_range.start < state.rendered_range.end { + state.rendered_range.end = + new_end + state.rendered_range.end.saturating_sub(old_range.end); + } + + let mut old_heights = state.items.cursor::(); + let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &()); + old_heights.seek_forward(&Count(old_range.end), Bias::Right, &()); + + new_heights.extend((0..count).map(|_| ListItem::Unrendered), &()); + new_heights.push_tree(old_heights.suffix(&()), &()); + drop(old_heights); + state.items = new_heights; + } + + pub fn set_scroll_handler( + &mut self, + handler: impl FnMut(Range, &mut EventContext) + 'static, + ) { + self.0.borrow_mut().scroll_handler = Some(Box::new(handler)) + } +} + +impl StateInner { + fn render_item( + &mut self, + ix: usize, + existing_item: &ListItem, + constraint: SizeConstraint, + cx: &mut LayoutContext, + ) -> ElementRc { + if let ListItem::Rendered(element) = existing_item { + element.clone() + } else { + let mut element = (self.render_item)(ix, cx); + element.layout(constraint, cx); + element.into() + } + } + + fn visible_range(&self, height: f32, scroll_top: &ListOffset) -> Range { + let mut cursor = self.items.cursor::(); + cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + let start_y = cursor.sum_start().0 + scroll_top.offset_in_item; + let mut cursor = cursor.swap_dimensions(); + cursor.seek_forward(&Height(start_y + height), Bias::Left, &()); + scroll_top.item_ix..cursor.sum_start().0 + 1 + } + + fn visible_elements<'a>( + &'a self, + bounds: RectF, + scroll_top: &ListOffset, + ) -> impl Iterator + 'a { + let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item); + let mut cursor = self.items.cursor::(); + cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + std::iter::from_fn(move || { + while let Some(item) = cursor.item() { + if item_origin.y() > bounds.max_y() { + break; + } + + if let ListItem::Rendered(element) = item { + let result = (element.clone(), item_origin); + item_origin.set_y(item_origin.y() + element.size().y()); + cursor.next(&()); + return Some(result); + } + + cursor.next(&()); + } + + None + }) + } + + fn scroll( + &mut self, + scroll_top: &ListOffset, + height: f32, + mut delta: Vector2F, + precise: bool, + cx: &mut EventContext, + ) -> bool { + if !precise { + delta *= 20.; + } + + let scroll_max = (self.items.summary().height - height).max(0.); + let new_scroll_top = (self.scroll_top(scroll_top) - delta.y()) + .max(0.) + .min(scroll_max); + + if self.orientation == Orientation::Bottom && new_scroll_top == scroll_max { + self.logical_scroll_top = None; + } else { + let mut cursor = self.items.cursor::(); + cursor.seek(&Height(new_scroll_top), Bias::Right, &()); + let item_ix = cursor.sum_start().0; + let offset_in_item = new_scroll_top - cursor.seek_start().0; + self.logical_scroll_top = Some(ListOffset { + item_ix, + offset_in_item, + }); + } + + if self.scroll_handler.is_some() { + let visible_range = self.visible_range(height, scroll_top); + self.scroll_handler.as_mut().unwrap()(visible_range, cx); + } + cx.notify(); + + true + } + + fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 { + let mut cursor = self.items.cursor::(); + cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &()); + cursor.sum_start().0 + logical_scroll_top.offset_in_item + } +} + +impl ListItem { + fn remove(&self) -> Self { + match self { + ListItem::Unrendered => ListItem::Unrendered, + ListItem::Rendered(element) => ListItem::Removed(element.size().y()), + ListItem::Removed(height) => ListItem::Removed(*height), + } + } +} + +impl sum_tree::Item for ListItem { + type Summary = ListItemSummary; + + fn summary(&self) -> Self::Summary { + match self { + ListItem::Unrendered => ListItemSummary { + count: 1, + rendered_count: 0, + unrendered_count: 1, + height: 0., + }, + ListItem::Rendered(element) => ListItemSummary { + count: 1, + rendered_count: 1, + unrendered_count: 0, + height: element.size().y(), + }, + ListItem::Removed(height) => ListItemSummary { + count: 1, + rendered_count: 0, + unrendered_count: 1, + height: *height, + }, + } + } +} + +impl sum_tree::Summary for ListItemSummary { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &()) { + self.count += summary.count; + self.rendered_count += summary.rendered_count; + self.unrendered_count += summary.unrendered_count; + self.height += summary.height; + } +} + +impl<'a> sum_tree::Dimension<'a, ListItemSummary> for ListItemSummary { + fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) { + sum_tree::Summary::add_summary(self, summary, &()); + } +} + +impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count { + fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) { + self.0 += summary.count; + } +} + +impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount { + fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) { + self.0 += summary.rendered_count; + } +} + +impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount { + fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) { + self.0 += summary.unrendered_count; + } +} + +impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height { + fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) { + self.0 += summary.height; + } +} + +impl<'a> sum_tree::SeekDimension<'a, ListItemSummary> for Height { + fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering { + self.0.partial_cmp(&other.0).unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::geometry::vector::vec2f; + use rand::prelude::*; + use std::env; + + #[crate::test(self)] + fn test_layout(cx: &mut crate::MutableAppContext) { + let mut presenter = cx.build_presenter(0, 0.); + let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)); + + let elements = Rc::new(RefCell::new(vec![(0, 20.), (1, 30.), (2, 100.)])); + let state = ListState::new(elements.borrow().len(), Orientation::Top, 1000.0, { + let elements = elements.clone(); + move |ix, _| { + let (id, height) = elements.borrow()[ix]; + TestElement::new(id, height).boxed() + } + }); + + let mut list = List::new(state.clone()); + let (size, _) = list.layout(constraint, &mut presenter.build_layout_context(false, cx)); + assert_eq!(size, vec2f(100., 40.)); + assert_eq!( + state.0.borrow().items.summary(), + ListItemSummary { + count: 3, + rendered_count: 3, + unrendered_count: 0, + height: 150. + } + ); + + state.0.borrow_mut().scroll( + &ListOffset { + item_ix: 0, + offset_in_item: 0., + }, + 40., + vec2f(0., 54.), + true, + &mut presenter.build_event_context(cx), + ); + let (_, logical_scroll_top) = + list.layout(constraint, &mut presenter.build_layout_context(false, cx)); + assert_eq!( + logical_scroll_top, + ListOffset { + item_ix: 2, + offset_in_item: 4. + } + ); + assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 54.); + + elements.borrow_mut().splice(1..2, vec![(3, 40.), (4, 50.)]); + elements.borrow_mut().push((5, 60.)); + state.splice(1..2, 2); + state.splice(4..4, 1); + assert_eq!( + state.0.borrow().items.summary(), + ListItemSummary { + count: 5, + rendered_count: 2, + unrendered_count: 3, + height: 120. + } + ); + + let (size, logical_scroll_top) = + list.layout(constraint, &mut presenter.build_layout_context(false, cx)); + assert_eq!(size, vec2f(100., 40.)); + assert_eq!( + state.0.borrow().items.summary(), + ListItemSummary { + count: 5, + rendered_count: 5, + unrendered_count: 0, + height: 270. + } + ); + assert_eq!( + logical_scroll_top, + ListOffset { + item_ix: 3, + offset_in_item: 4. + } + ); + assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 114.); + } + + #[crate::test(self, iterations = 10000, seed = 0)] + fn test_random(cx: &mut crate::MutableAppContext, mut rng: StdRng) { + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let mut presenter = cx.build_presenter(0, 0.); + let mut next_id = 0; + let elements = Rc::new(RefCell::new( + (0..rng.gen_range(0..=20)) + .map(|_| { + let id = next_id; + next_id += 1; + (id, rng.gen_range(0..=200) as f32 / 2.0) + }) + .collect::>(), + )); + let orientation = *[Orientation::Top, Orientation::Bottom] + .choose(&mut rng) + .unwrap(); + let overdraw = rng.gen_range(1..=100) as f32; + let state = ListState::new(elements.borrow().len(), orientation, overdraw, { + let elements = elements.clone(); + move |ix, _| { + let (id, height) = elements.borrow()[ix]; + TestElement::new(id, height).boxed() + } + }); + + let mut width = rng.gen_range(0..=2000) as f32 / 2.; + let mut height = rng.gen_range(0..=2000) as f32 / 2.; + log::info!("orientation: {:?}", orientation); + log::info!("overdraw: {}", overdraw); + log::info!("elements: {:?}", elements.borrow()); + log::info!("size: ({:?}, {:?})", width, height); + log::info!("=================="); + + let mut last_logical_scroll_top = None; + for _ in 0..operations { + match rng.gen_range(0..=100) { + 0..=29 if last_logical_scroll_top.is_some() => { + let delta = vec2f(0., rng.gen_range(-overdraw..=overdraw)); + log::info!( + "Scrolling by {:?}, previous scroll top: {:?}", + delta, + last_logical_scroll_top.unwrap() + ); + state.0.borrow_mut().scroll( + last_logical_scroll_top.as_ref().unwrap(), + height, + delta, + true, + &mut presenter.build_event_context(cx), + ); + } + 30..=34 => { + width = rng.gen_range(0..=2000) as f32 / 2.; + log::info!("changing width: {:?}", width); + } + 35..=54 => { + height = rng.gen_range(0..=1000) as f32 / 2.; + log::info!("changing height: {:?}", height); + } + _ => { + let mut elements = elements.borrow_mut(); + let end_ix = rng.gen_range(0..=elements.len()); + let start_ix = rng.gen_range(0..=end_ix); + let new_elements = (0..rng.gen_range(0..10)) + .map(|_| { + let id = next_id; + next_id += 1; + (id, rng.gen_range(0..=200) as f32 / 2.) + }) + .collect::>(); + log::info!("splice({:?}, {:?})", start_ix..end_ix, new_elements); + state.splice(start_ix..end_ix, new_elements.len()); + elements.splice(start_ix..end_ix, new_elements); + for (ix, item) in state.0.borrow().items.cursor::<(), ()>().enumerate() { + if let ListItem::Rendered(element) = item { + let (expected_id, _) = elements[ix]; + element.with_metadata(|metadata: Option<&usize>| { + assert_eq!(*metadata.unwrap(), expected_id); + }); + } + } + } + } + + let mut list = List::new(state.clone()); + let (size, logical_scroll_top) = list.layout( + SizeConstraint::new(vec2f(0., 0.), vec2f(width, height)), + &mut presenter.build_layout_context(false, cx), + ); + assert_eq!(size, vec2f(width, height)); + last_logical_scroll_top = Some(logical_scroll_top); + + let state = state.0.borrow(); + log::info!("items {:?}", state.items.items(&())); + + let scroll_top = state.scroll_top(&logical_scroll_top); + let rendered_top = (scroll_top - overdraw).max(0.); + let rendered_bottom = scroll_top + height + overdraw; + let mut item_top = 0.; + + log::info!( + "rendered top {:?}, rendered bottom {:?}, scroll top {:?}", + rendered_top, + rendered_bottom, + scroll_top, + ); + + let mut first_rendered_element_top = None; + let mut last_rendered_element_bottom = None; + assert_eq!(state.items.summary().count, elements.borrow().len()); + for (ix, item) in state.items.cursor::<(), ()>().enumerate() { + match item { + ListItem::Unrendered => { + let item_bottom = item_top; + assert!(item_bottom <= rendered_top || item_top >= rendered_bottom); + item_top = item_bottom; + } + ListItem::Removed(height) => { + let (id, expected_height) = elements.borrow()[ix]; + assert_eq!( + *height, expected_height, + "element {} height didn't match", + id + ); + let item_bottom = item_top + height; + assert!(item_bottom <= rendered_top || item_top >= rendered_bottom); + item_top = item_bottom; + } + ListItem::Rendered(element) => { + let (expected_id, expected_height) = elements.borrow()[ix]; + element.with_metadata(|metadata: Option<&usize>| { + assert_eq!(*metadata.unwrap(), expected_id); + }); + assert_eq!(element.size().y(), expected_height); + let item_bottom = item_top + element.size().y(); + first_rendered_element_top.get_or_insert(item_top); + last_rendered_element_bottom = Some(item_bottom); + assert!(item_bottom > rendered_top || item_top < rendered_bottom); + item_top = item_bottom; + } + } + } + + match orientation { + Orientation::Top => { + if let Some(first_rendered_element_top) = first_rendered_element_top { + assert!(first_rendered_element_top <= scroll_top); + } + } + Orientation::Bottom => { + if let Some(last_rendered_element_bottom) = last_rendered_element_bottom { + assert!(last_rendered_element_bottom >= scroll_top + height); + } + } + } + } + } + + struct TestElement { + id: usize, + size: Vector2F, + } + + impl TestElement { + fn new(id: usize, height: f32) -> Self { + Self { + id, + size: vec2f(100., height), + } + } + } + + impl Element for TestElement { + type LayoutState = (); + type PaintState = (); + + fn layout(&mut self, _: SizeConstraint, _: &mut LayoutContext) -> (Vector2F, ()) { + (self.size, ()) + } + + fn paint(&mut self, _: RectF, _: RectF, _: &mut (), _: &mut PaintContext) { + todo!() + } + + fn dispatch_event( + &mut self, + _: &Event, + _: RectF, + _: &mut (), + _: &mut (), + _: &mut EventContext, + ) -> bool { + todo!() + } + + fn debug(&self, _: RectF, _: &(), _: &(), _: &DebugContext) -> serde_json::Value { + self.id.into() + } + + fn metadata(&self) -> Option<&dyn std::any::Any> { + Some(&self.id) + } + } +} diff --git a/gpui/src/elements/mouse_event_handler.rs b/gpui/src/elements/mouse_event_handler.rs index 2fc310e8252b88d79217e5783dea6ba3ca53a207..1d0907517910e863c30f3acab4f214a89587ae77 100644 --- a/gpui/src/elements/mouse_event_handler.rs +++ b/gpui/src/elements/mouse_event_handler.rs @@ -1,42 +1,69 @@ +use std::ops::DerefMut; + use crate::{ geometry::{rect::RectF, vector::Vector2F}, - AfterLayoutContext, AppContext, DebugContext, Element, ElementBox, Event, EventContext, - LayoutContext, PaintContext, SizeConstraint, ValueHandle, + platform::CursorStyle, + CursorStyleHandle, DebugContext, Element, ElementBox, ElementStateHandle, ElementStateId, + Event, EventContext, LayoutContext, MutableAppContext, PaintContext, SizeConstraint, }; use serde_json::json; pub struct MouseEventHandler { - state: ValueHandle, + state: ElementStateHandle, child: ElementBox, + cursor_style: Option, + mouse_down_handler: Option>, click_handler: Option>, + drag_handler: Option>, } -#[derive(Clone, Copy, Debug, Default)] +#[derive(Default)] pub struct MouseState { pub hovered: bool, pub clicked: bool, + prev_drag_position: Option, + cursor_style_handle: Option, } impl MouseEventHandler { - pub fn new(id: usize, cx: &AppContext, render_child: F) -> Self + pub fn new(id: Id, cx: &mut C, render_child: F) -> Self where Tag: 'static, - F: FnOnce(MouseState) -> ElementBox, + F: FnOnce(&MouseState, &mut C) -> ElementBox, + C: DerefMut, + Id: Into, { - let state_handle = cx.value::(id); - let state = state_handle.read(cx.as_ref(), |state| *state); - let child = render_child(state); + let state_handle = cx.element_state::(id.into()); + let child = state_handle.update(cx, |state, cx| render_child(state, cx)); Self { state: state_handle, child, + cursor_style: None, + mouse_down_handler: None, click_handler: None, + drag_handler: None, } } + pub fn with_cursor_style(mut self, cursor: CursorStyle) -> Self { + self.cursor_style = Some(cursor); + self + } + + pub fn on_mouse_down(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self { + self.mouse_down_handler = Some(Box::new(handler)); + self + } + pub fn on_click(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self { self.click_handler = Some(Box::new(handler)); self } + + pub fn on_drag(mut self, handler: impl FnMut(Vector2F, &mut EventContext) + 'static) -> Self { + self.drag_handler = Some(Box::new(handler)); + self + } } impl Element for MouseEventHandler { @@ -51,22 +78,14 @@ impl Element for MouseEventHandler { (self.child.layout(constraint, cx), ()) } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - self.child.after_layout(cx); - } - fn paint( &mut self, bounds: RectF, + visible_bounds: RectF, _: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - self.child.paint(bounds.origin(), cx); + self.child.paint(bounds.origin(), visible_bounds, cx); } fn dispatch_event( @@ -77,33 +96,58 @@ impl Element for MouseEventHandler { _: &mut Self::PaintState, cx: &mut EventContext, ) -> bool { + let cursor_style = self.cursor_style; + let mouse_down_handler = self.mouse_down_handler.as_mut(); let click_handler = self.click_handler.as_mut(); + let drag_handler = self.drag_handler.as_mut(); let handled_in_child = self.child.dispatch_event(event, cx); self.state.update(cx, |state, cx| match event { - Event::MouseMoved { position } => { - let mouse_in = bounds.contains_point(*position); - if state.hovered != mouse_in { - state.hovered = mouse_in; - cx.notify(); - true - } else { - handled_in_child + Event::MouseMoved { + position, + left_mouse_down, + } => { + if !left_mouse_down { + let mouse_in = bounds.contains_point(*position); + if state.hovered != mouse_in { + state.hovered = mouse_in; + if let Some(cursor_style) = cursor_style { + if !state.clicked { + if state.hovered { + state.cursor_style_handle = + Some(cx.set_cursor_style(cursor_style)); + } else { + state.cursor_style_handle = None; + } + } + } + cx.notify(); + return true; + } } + handled_in_child } Event::LeftMouseDown { position, .. } => { if !handled_in_child && bounds.contains_point(*position) { state.clicked = true; + state.prev_drag_position = Some(*position); cx.notify(); + if let Some(handler) = mouse_down_handler { + handler(cx); + } true } else { handled_in_child } } Event::LeftMouseUp { position, .. } => { + state.prev_drag_position = None; if !handled_in_child && state.clicked { state.clicked = false; + if !state.hovered { + state.cursor_style_handle = None; + } cx.notify(); if let Some(handler) = click_handler { if bounds.contains_point(*position) { @@ -115,6 +159,20 @@ impl Element for MouseEventHandler { handled_in_child } } + Event::LeftMouseDragged { position, .. } => { + if !handled_in_child && state.clicked { + let prev_drag_position = state.prev_drag_position.replace(*position); + if let Some((handler, prev_position)) = drag_handler.zip(prev_drag_position) { + let delta = *position - prev_position; + if !delta.is_zero() { + (handler)(delta, cx); + } + } + true + } else { + handled_in_child + } + } _ => handled_in_child, }) } diff --git a/gpui/src/elements/overlay.rs b/gpui/src/elements/overlay.rs new file mode 100644 index 0000000000000000000000000000000000000000..79ab71c07d7cd4da4ae25991881887ed13ec2473 --- /dev/null +++ b/gpui/src/elements/overlay.rs @@ -0,0 +1,63 @@ +use crate::{ + geometry::{rect::RectF, vector::Vector2F}, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, +}; + +pub struct Overlay { + child: ElementBox, +} + +impl Overlay { + pub fn new(child: ElementBox) -> Self { + Self { child } + } +} + +impl Element for Overlay { + type LayoutState = Vector2F; + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + cx: &mut LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + let size = self.child.layout(constraint, cx); + (Vector2F::zero(), size) + } + + fn paint( + &mut self, + bounds: RectF, + _: RectF, + size: &mut Self::LayoutState, + cx: &mut PaintContext, + ) { + let bounds = RectF::new(bounds.origin(), *size); + cx.scene.push_stacking_context(None); + self.child.paint(bounds.origin(), bounds, cx); + cx.scene.pop_stacking_context(); + } + + fn dispatch_event( + &mut self, + event: &Event, + _: RectF, + _: &mut Self::LayoutState, + _: &mut Self::PaintState, + cx: &mut EventContext, + ) -> bool { + self.child.dispatch_event(event, cx) + } + + fn debug( + &self, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &DebugContext, + ) -> serde_json::Value { + self.child.debug(cx) + } +} diff --git a/gpui/src/elements/stack.rs b/gpui/src/elements/stack.rs index cfc4d9cc6cfc486a55d26f9c4df333bfe47b346b..dd36b9c4b5aebcbebd8c98253084861202d02378 100644 --- a/gpui/src/elements/stack.rs +++ b/gpui/src/elements/stack.rs @@ -1,8 +1,8 @@ use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::{self, json, ToJson}, - AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, - PaintContext, SizeConstraint, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, }; pub struct Stack { @@ -33,26 +33,16 @@ impl Element for Stack { (size, ()) } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - for child in &mut self.children { - child.after_layout(cx); - } - } - fn paint( &mut self, bounds: RectF, + visible_bounds: RectF, _: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { for child in &mut self.children { cx.scene.push_layer(None); - child.paint(bounds.origin(), cx); + child.paint(bounds.origin(), visible_bounds, cx); cx.scene.pop_layer(); } } diff --git a/gpui/src/elements/svg.rs b/gpui/src/elements/svg.rs index 93d26f9656a45ba2328972c99226cc6a3c58588b..8adb285b99a73c1e2ab661976d4f2d58d4d635db 100644 --- a/gpui/src/elements/svg.rs +++ b/gpui/src/elements/svg.rs @@ -8,8 +8,7 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, - scene, AfterLayoutContext, DebugContext, Element, Event, EventContext, LayoutContext, - PaintContext, SizeConstraint, + scene, DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; pub struct Svg { @@ -66,10 +65,13 @@ impl Element for Svg { } } - fn after_layout(&mut self, _: Vector2F, _: &mut Self::LayoutState, _: &mut AfterLayoutContext) { - } - - fn paint(&mut self, bounds: RectF, svg: &mut Self::LayoutState, cx: &mut PaintContext) { + fn paint( + &mut self, + bounds: RectF, + _visible_bounds: RectF, + svg: &mut Self::LayoutState, + cx: &mut PaintContext, + ) { if let Some(svg) = svg.clone() { cx.scene.push_icon(scene::Icon { bounds, diff --git a/gpui/src/elements/text.rs b/gpui/src/elements/text.rs new file mode 100644 index 0000000000000000000000000000000000000000..623af72af6444892b30c75448eea17e379b14a02 --- /dev/null +++ b/gpui/src/elements/text.rs @@ -0,0 +1,131 @@ +use crate::{ + color::Color, + fonts::TextStyle, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, + json::{ToJson, Value}, + text_layout::{Line, ShapedBoundary}, + DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, +}; +use serde_json::json; + +pub struct Text { + text: String, + style: TextStyle, +} + +pub struct LayoutState { + lines: Vec<(Line, Vec)>, + line_height: f32, +} + +impl Text { + pub fn new(text: String, style: TextStyle) -> Self { + Self { text, style } + } + + pub fn with_default_color(mut self, color: Color) -> Self { + self.style.color = color; + self + } +} + +impl Element for Text { + type LayoutState = LayoutState; + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + cx: &mut LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + let font_id = self.style.font_id; + let line_height = cx.font_cache.line_height(font_id, self.style.font_size); + + let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size); + let mut lines = Vec::new(); + let mut line_count = 0; + let mut max_line_width = 0_f32; + for line in self.text.lines() { + let shaped_line = cx.text_layout_cache.layout_str( + line, + self.style.font_size, + &[(line.len(), self.style.to_run())], + ); + let wrap_boundaries = wrapper + .wrap_shaped_line(line, &shaped_line, constraint.max.x()) + .collect::>(); + + max_line_width = max_line_width.max(shaped_line.width()); + line_count += wrap_boundaries.len() + 1; + lines.push((shaped_line, wrap_boundaries)); + } + + let size = vec2f( + max_line_width + .ceil() + .max(constraint.min.x()) + .min(constraint.max.x()), + (line_height * line_count as f32).ceil(), + ); + (size, LayoutState { lines, line_height }) + } + + fn paint( + &mut self, + bounds: RectF, + visible_bounds: RectF, + layout: &mut Self::LayoutState, + cx: &mut PaintContext, + ) -> Self::PaintState { + let mut origin = bounds.origin(); + for (line, wrap_boundaries) in &layout.lines { + let wrapped_line_boundaries = RectF::new( + origin, + vec2f( + bounds.width(), + (wrap_boundaries.len() + 1) as f32 * layout.line_height, + ), + ); + + if wrapped_line_boundaries.intersects(visible_bounds) { + line.paint_wrapped( + origin, + visible_bounds, + layout.line_height, + wrap_boundaries.iter().copied(), + cx, + ); + } + origin.set_y(wrapped_line_boundaries.max_y()); + } + } + + fn dispatch_event( + &mut self, + _: &Event, + _: RectF, + _: &mut Self::LayoutState, + _: &mut Self::PaintState, + _: &mut EventContext, + ) -> bool { + false + } + + fn debug( + &self, + bounds: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &DebugContext, + ) -> Value { + json!({ + "type": "Text", + "bounds": bounds.to_json(), + "text": &self.text, + "style": self.style.to_json(), + }) + } +} diff --git a/gpui/src/elements/uniform_list.rs b/gpui/src/elements/uniform_list.rs index 74ebccdf379080beb603f1464d19a08e132154a0..c82d8aa3d6fc740c3179f50b367348b32816cc76 100644 --- a/gpui/src/elements/uniform_list.rs +++ b/gpui/src/elements/uniform_list.rs @@ -1,13 +1,11 @@ -use super::{ - AfterLayoutContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, -}; +use super::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint}; use crate::{ geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }, json::{self, json}, - AppContext, ElementBox, + ElementBox, MutableAppContext, }; use json::ToJson; use parking_lot::Mutex; @@ -40,7 +38,7 @@ pub struct LayoutState { pub struct UniformList where - F: Fn(Range, &mut Vec, &AppContext), + F: Fn(Range, &mut Vec, &mut MutableAppContext), { state: UniformListState, item_count: usize, @@ -49,7 +47,7 @@ where impl UniformList where - F: Fn(Range, &mut Vec, &AppContext), + F: Fn(Range, &mut Vec, &mut MutableAppContext), { pub fn new(state: UniformListState, item_count: usize, append_items: F) -> Self { Self { @@ -62,13 +60,13 @@ where fn scroll( &self, _: Vector2F, - delta: Vector2F, + mut delta: Vector2F, precise: bool, scroll_max: f32, cx: &mut EventContext, ) -> bool { if !precise { - todo!("still need to handle non-precise scroll events from a mouse wheel"); + delta *= 20.; } let mut state = self.state.0.lock(); @@ -104,7 +102,7 @@ where impl Element for UniformList where - F: Fn(Range, &mut Vec, &AppContext), + F: Fn(Range, &mut Vec, &mut MutableAppContext), { type LayoutState = LayoutState; type PaintState = (); @@ -152,6 +150,8 @@ where for item in &mut items { item.layout(item_constraint, cx); } + } else { + size = constraint.min; } ( @@ -164,20 +164,10 @@ where ) } - fn after_layout( - &mut self, - _: Vector2F, - layout: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - for item in &mut layout.items { - item.after_layout(cx); - } - } - fn paint( &mut self, bounds: RectF, + visible_bounds: RectF, layout: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { @@ -187,7 +177,7 @@ where bounds.origin() - vec2f(0.0, self.state.scroll_top() % layout.item_height); for item in &mut layout.items { - item.paint(item_origin, cx); + item.paint(item_origin, visible_bounds, cx); item_origin += vec2f(0.0, layout.item_height); } diff --git a/gpui/src/executor.rs b/gpui/src/executor.rs index 78cb77c6b9521779228633f0a7f604febce63420..c848cff9c59bae7566baee89e47f94817e6df058 100644 --- a/gpui/src/executor.rs +++ b/gpui/src/executor.rs @@ -122,9 +122,14 @@ impl Deterministic { smol::pin!(future); let unparker = self.parker.lock().unparker(); - let waker = waker_fn(move || { - unparker.unpark(); - }); + let woken = Arc::new(AtomicBool::new(false)); + let waker = { + let woken = woken.clone(); + waker_fn(move || { + woken.store(true, SeqCst); + unparker.unpark(); + }) + }; let mut cx = Context::from_waker(&waker); let mut trace = Trace::default(); @@ -166,10 +171,11 @@ impl Deterministic { && state.scheduled_from_background.is_empty() && state.spawned_from_foreground.is_empty() { - if state.forbid_parking { + if state.forbid_parking && !woken.load(SeqCst) { panic!("deterministic executor parked after a call to forbid_parking"); } drop(state); + woken.store(false, SeqCst); self.parker.lock().park(); } diff --git a/gpui/src/font_cache.rs b/gpui/src/font_cache.rs index 75ee206b35e7e3c35400ed584216da3bc2e512ff..3c11b9659cb26441ab7746bac6a6f306fdbcef42 100644 --- a/gpui/src/font_cache.rs +++ b/gpui/src/font_cache.rs @@ -2,10 +2,16 @@ use crate::{ fonts::{FontId, Metrics, Properties}, geometry::vector::{vec2f, Vector2F}, platform, + text_layout::LineWrapper, }; use anyhow::{anyhow, Result}; +use ordered_float::OrderedFloat; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::HashMap, + ops::{Deref, DerefMut}, + sync::Arc, +}; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct FamilyId(usize); @@ -22,6 +28,12 @@ pub struct FontCacheState { families: Vec, font_selections: HashMap>, metrics: HashMap, + wrapper_pool: HashMap<(FontId, OrderedFloat), Vec>, +} + +pub struct LineWrapperHandle { + wrapper: Option, + font_cache: Arc, } unsafe impl Send for FontCache {} @@ -30,9 +42,10 @@ impl FontCache { pub fn new(fonts: Arc) -> Self { Self(RwLock::new(FontCacheState { fonts, - families: Vec::new(), - font_selections: HashMap::new(), - metrics: HashMap::new(), + families: Default::default(), + font_selections: Default::default(), + metrics: Default::default(), + wrapper_pool: Default::default(), })) } @@ -134,9 +147,13 @@ impl FontCache { } pub fn em_width(&self, font_id: FontId, font_size: f32) -> f32 { - let state = self.0.read(); - let glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap(); - let bounds = state.fonts.typographic_bounds(font_id, glyph_id).unwrap(); + let glyph_id; + let bounds; + { + let state = self.0.read(); + glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap(); + bounds = state.fonts.typographic_bounds(font_id, glyph_id).unwrap(); + } self.scale_metric(bounds.width(), font_id, font_size) } @@ -160,6 +177,47 @@ impl FontCache { pub fn scale_metric(&self, metric: f32, font_id: FontId, font_size: f32) -> f32 { metric * font_size / self.metric(font_id, |m| m.units_per_em as f32) } + + pub fn line_wrapper(self: &Arc, font_id: FontId, font_size: f32) -> LineWrapperHandle { + let mut state = self.0.write(); + let wrappers = state + .wrapper_pool + .entry((font_id, OrderedFloat(font_size))) + .or_default(); + let wrapper = wrappers + .pop() + .unwrap_or_else(|| LineWrapper::new(font_id, font_size, state.fonts.clone())); + LineWrapperHandle { + wrapper: Some(wrapper), + font_cache: self.clone(), + } + } +} + +impl Drop for LineWrapperHandle { + fn drop(&mut self) { + let mut state = self.font_cache.0.write(); + let wrapper = self.wrapper.take().unwrap(); + state + .wrapper_pool + .get_mut(&(wrapper.font_id, OrderedFloat(wrapper.font_size))) + .unwrap() + .push(wrapper); + } +} + +impl Deref for LineWrapperHandle { + type Target = LineWrapper; + + fn deref(&self) -> &Self::Target { + self.wrapper.as_ref().unwrap() + } +} + +impl DerefMut for LineWrapperHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + self.wrapper.as_mut().unwrap() + } } #[cfg(test)] diff --git a/gpui/src/fonts.rs b/gpui/src/fonts.rs index e9f84676e791f19fdd7badf3a7e52de277f22a99..96248c167577326cb74ed3ee6c1d7934f385f362 100644 --- a/gpui/src/fonts.rs +++ b/gpui/src/fonts.rs @@ -1,23 +1,38 @@ use crate::{ color::Color, json::{json, ToJson}, + text_layout::RunStyle, + FontCache, }; +use anyhow::anyhow; pub use font_kit::{ metrics::Metrics, properties::{Properties, Stretch, Style, Weight}, }; use serde::{de, Deserialize}; use serde_json::Value; +use std::{cell::RefCell, sync::Arc}; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct FontId(pub usize); pub type GlyphId = u32; -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug)] pub struct TextStyle { pub color: Color, + pub font_family_name: Arc, + pub font_id: FontId, + pub font_size: f32, pub font_properties: Properties, + pub underline: bool, +} + +#[derive(Clone, Debug, Default)] +pub struct HighlightStyle { + pub color: Color, + pub font_properties: Properties, + pub underline: bool, } #[allow(non_camel_case_types)] @@ -34,76 +49,160 @@ enum WeightJson { black, } +thread_local! { + static FONT_CACHE: RefCell>> = Default::default(); +} + #[derive(Deserialize)] struct TextStyleJson { color: Color, + family: String, weight: Option, + size: f32, #[serde(default)] italic: bool, + #[serde(default)] + underline: bool, } -impl<'de> Deserialize<'de> for TextStyle { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let json = Value::deserialize(deserializer)?; - if json.is_object() { - let style_json: TextStyleJson = - serde_json::from_value(json).map_err(de::Error::custom)?; - Ok(style_json.into()) - } else { - Ok(Self { - color: serde_json::from_value(json).map_err(de::Error::custom)?, - font_properties: Properties::new(), - }) +#[derive(Deserialize)] +struct HighlightStyleJson { + color: Color, + weight: Option, + #[serde(default)] + italic: bool, + #[serde(default)] + underline: bool, +} + +impl TextStyle { + pub fn new( + font_family_name: impl Into>, + font_size: f32, + font_properties: Properties, + underline: bool, + color: Color, + font_cache: &FontCache, + ) -> anyhow::Result { + let font_family_name = font_family_name.into(); + let family_id = font_cache.load_family(&[&font_family_name])?; + let font_id = font_cache.select_font(family_id, &font_properties)?; + Ok(Self { + color, + font_family_name, + font_id, + font_size, + font_properties, + underline, + }) + } + + pub fn to_run(&self) -> RunStyle { + RunStyle { + font_id: self.font_id, + color: self.color, + underline: self.underline, + } + } + + fn from_json(json: TextStyleJson) -> anyhow::Result { + FONT_CACHE.with(|font_cache| { + if let Some(font_cache) = font_cache.borrow().as_ref() { + let font_properties = properties_from_json(json.weight, json.italic); + Self::new( + json.family, + json.size, + font_properties, + json.underline, + json.color, + font_cache, + ) + } else { + Err(anyhow!( + "TextStyle can only be deserialized within a call to with_font_cache" + )) + } + }) + } +} + +impl HighlightStyle { + fn from_json(json: HighlightStyleJson) -> Self { + let font_properties = properties_from_json(json.weight, json.italic); + Self { + color: json.color, + font_properties, + underline: json.underline, } } } -impl From for TextStyle { +impl From for HighlightStyle { fn from(color: Color) -> Self { Self { color, font_properties: Default::default(), + underline: false, } } } +impl<'de> Deserialize<'de> for TextStyle { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Self::from_json(TextStyleJson::deserialize(deserializer)?) + .map_err(|e| de::Error::custom(e))?) + } +} + impl ToJson for TextStyle { fn to_json(&self) -> Value { json!({ "color": self.color.to_json(), + "font_family": self.font_family_name.as_ref(), "font_properties": self.font_properties.to_json(), }) } } -impl Into for TextStyleJson { - fn into(self) -> TextStyle { - let weight = match self.weight.unwrap_or(WeightJson::normal) { - WeightJson::thin => Weight::THIN, - WeightJson::extra_light => Weight::EXTRA_LIGHT, - WeightJson::light => Weight::LIGHT, - WeightJson::normal => Weight::NORMAL, - WeightJson::medium => Weight::MEDIUM, - WeightJson::semibold => Weight::SEMIBOLD, - WeightJson::bold => Weight::BOLD, - WeightJson::extra_bold => Weight::EXTRA_BOLD, - WeightJson::black => Weight::BLACK, - }; - let style = if self.italic { - Style::Italic +impl<'de> Deserialize<'de> for HighlightStyle { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let json = serde_json::Value::deserialize(deserializer)?; + if json.is_object() { + Ok(Self::from_json( + serde_json::from_value(json).map_err(de::Error::custom)?, + )) } else { - Style::Normal - }; - TextStyle { - color: self.color, - font_properties: *Properties::new().weight(weight).style(style), + Ok(Self { + color: serde_json::from_value(json).map_err(de::Error::custom)?, + font_properties: Properties::new(), + underline: false, + }) } } } +fn properties_from_json(weight: Option, italic: bool) -> Properties { + let weight = match weight.unwrap_or(WeightJson::normal) { + WeightJson::thin => Weight::THIN, + WeightJson::extra_light => Weight::EXTRA_LIGHT, + WeightJson::light => Weight::LIGHT, + WeightJson::normal => Weight::NORMAL, + WeightJson::medium => Weight::MEDIUM, + WeightJson::semibold => Weight::SEMIBOLD, + WeightJson::bold => Weight::BOLD, + WeightJson::extra_bold => Weight::EXTRA_BOLD, + WeightJson::black => Weight::BLACK, + }; + let style = if italic { Style::Italic } else { Style::Normal }; + *Properties::new().weight(weight).style(style) +} + impl ToJson for Properties { fn to_json(&self) -> crate::json::Value { json!({ @@ -155,3 +254,15 @@ impl ToJson for Stretch { json!(self.0) } } + +pub fn with_font_cache(font_cache: Arc, callback: F) -> T +where + F: FnOnce() -> T, +{ + FONT_CACHE.with(|cache| { + *cache.borrow_mut() = Some(font_cache); + let result = callback(); + cache.borrow_mut().take(); + result + }) +} diff --git a/gpui/src/keymap.rs b/gpui/src/keymap.rs index a3a2cbf58c1078d9e20a9b2975f4bf7cdc8c9a25..b1082123bccc772a8f820744a427e491db06bd19 100644 --- a/gpui/src/keymap.rs +++ b/gpui/src/keymap.rs @@ -2,9 +2,12 @@ use anyhow::anyhow; use std::{ any::Any, collections::{HashMap, HashSet}, + fmt::Debug, }; use tree_sitter::{Language, Node, Parser}; +use crate::{Action, AnyAction}; + extern "C" { fn tree_sitter_context_predicate() -> Language; } @@ -24,8 +27,7 @@ pub struct Keymap(Vec); pub struct Binding { keystrokes: Vec, - action: String, - action_arg: Option>, + action: Box, context: Option, } @@ -70,10 +72,7 @@ where pub enum MatchResult { None, Pending, - Action { - name: String, - arg: Option>, - }, + Action(Box), } impl Matcher { @@ -117,10 +116,7 @@ impl Matcher { { if binding.keystrokes.len() == pending.keystrokes.len() { self.pending.remove(&view_id); - return MatchResult::Action { - name: binding.action.clone(), - arg: binding.action_arg.as_ref().map(|arg| (*arg).boxed_clone()), - }; + return MatchResult::Action(binding.action.boxed_clone()); } else { retain_pending = true; pending.context = Some(cx.clone()); @@ -153,19 +149,26 @@ impl Keymap { } } +pub mod menu { + use crate::action; + + action!(SelectPrev); + action!(SelectNext); +} + impl Default for Keymap { fn default() -> Self { Self(vec![ - Binding::new("up", "menu:select_prev", Some("menu")), - Binding::new("ctrl-p", "menu:select_prev", Some("menu")), - Binding::new("down", "menu:select_next", Some("menu")), - Binding::new("ctrl-n", "menu:select_next", Some("menu")), + Binding::new("up", menu::SelectPrev, Some("menu")), + Binding::new("ctrl-p", menu::SelectPrev, Some("menu")), + Binding::new("down", menu::SelectNext, Some("menu")), + Binding::new("ctrl-n", menu::SelectNext, Some("menu")), ]) } } impl Binding { - pub fn new>(keystrokes: &str, action: S, context: Option<&str>) -> Self { + pub fn new(keystrokes: &str, action: A, context: Option<&str>) -> Self { let context = if let Some(context) = context { Some(ContextPredicate::parse(context).unwrap()) } else { @@ -177,16 +180,10 @@ impl Binding { .split_whitespace() .map(|key| Keystroke::parse(key).unwrap()) .collect(), - action: action.into(), - action_arg: None, + action: Box::new(action), context, } } - - pub fn with_arg(mut self, arg: T) -> Self { - self.action_arg = Some(Box::new(arg)); - self - } } impl Keystroke { @@ -328,6 +325,8 @@ impl ContextPredicate { #[cfg(test)] mod tests { + use crate::action; + use super::*; #[test] @@ -417,15 +416,31 @@ mod tests { #[test] fn test_matcher() -> anyhow::Result<()> { + action!(A, &'static str); + action!(B); + action!(Ab); + + impl PartialEq for A { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } + } + impl Eq for A {} + impl Debug for A { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "A({:?})", &self.0) + } + } + #[derive(Clone, Debug, Eq, PartialEq)] struct ActionArg { a: &'static str, } let keymap = Keymap(vec![ - Binding::new("a", "a", Some("a")).with_arg(ActionArg { a: "b" }), - Binding::new("b", "b", Some("a")), - Binding::new("a b", "a_b", Some("a || b")), + Binding::new("a", A("x"), Some("a")), + Binding::new("b", B, Some("a")), + Binding::new("a b", Ab, Some("a || b")), ]); let mut ctx_a = Context::default(); @@ -437,57 +452,40 @@ mod tests { let mut matcher = Matcher::new(keymap); // Basic match - assert_eq!( - matcher.test_keystroke("a", 1, &ctx_a), - Some(("a".to_string(), Some(ActionArg { a: "b" }))) - ); + assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x"))); // Multi-keystroke match - assert_eq!(matcher.test_keystroke::<()>("a", 1, &ctx_b), None); - assert_eq!( - matcher.test_keystroke::<()>("b", 1, &ctx_b), - Some(("a_b".to_string(), None)) - ); + assert_eq!(matcher.test_keystroke::("a", 1, &ctx_b), None); + assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab)); // Failed matches don't interfere with matching subsequent keys - assert_eq!(matcher.test_keystroke::<()>("x", 1, &ctx_a), None); - assert_eq!( - matcher.test_keystroke("a", 1, &ctx_a), - Some(("a".to_string(), Some(ActionArg { a: "b" }))) - ); + assert_eq!(matcher.test_keystroke::("x", 1, &ctx_a), None); + assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x"))); // Pending keystrokes are cleared when the context changes - assert_eq!(matcher.test_keystroke::<()>("a", 1, &ctx_b), None); - assert_eq!( - matcher.test_keystroke::<()>("b", 1, &ctx_a), - Some(("b".to_string(), None)) - ); + assert_eq!(matcher.test_keystroke::("a", 1, &ctx_b), None); + assert_eq!(matcher.test_keystroke("b", 1, &ctx_a), Some(B)); let mut ctx_c = Context::default(); ctx_c.set.insert("c".into()); // Pending keystrokes are maintained per-view - assert_eq!(matcher.test_keystroke::<()>("a", 1, &ctx_b), None); - assert_eq!(matcher.test_keystroke::<()>("a", 2, &ctx_c), None); - assert_eq!( - matcher.test_keystroke::<()>("b", 1, &ctx_b), - Some(("a_b".to_string(), None)) - ); + assert_eq!(matcher.test_keystroke::("a", 1, &ctx_b), None); + assert_eq!(matcher.test_keystroke::("a", 2, &ctx_c), None); + assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab)); Ok(()) } impl Matcher { - fn test_keystroke( - &mut self, - keystroke: &str, - view_id: usize, - cx: &Context, - ) -> Option<(String, Option)> { - if let MatchResult::Action { name, arg } = + fn test_keystroke(&mut self, keystroke: &str, view_id: usize, cx: &Context) -> Option + where + A: Action + Debug + Eq, + { + if let MatchResult::Action(action) = self.push_keystroke(Keystroke::parse(keystroke).unwrap(), view_id, cx) { - Some((name, arg.and_then(|arg| arg.downcast_ref::().cloned()))) + Some(*action.boxed_clone_as_any().downcast().unwrap()) } else { None } diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index f2f935d142719da38d7ccfe6d6c57193e136646e..6cb1c6f39d7d5caee1b94516461a371335924231 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -1,11 +1,13 @@ mod app; pub use app::*; mod assets; +pub mod sum_tree; #[cfg(test)] mod test; pub use assets::*; pub mod elements; pub mod font_cache; +pub mod views; pub use font_cache::FontCache; mod clipboard; pub use clipboard::ClipboardItem; @@ -17,7 +19,7 @@ pub use scene::{Border, Quad, Scene}; pub mod text_layout; pub use text_layout::TextLayoutCache; mod util; -pub use elements::{Element, ElementBox}; +pub use elements::{Element, ElementBox, ElementRc}; pub mod executor; pub use executor::Task; pub mod color; @@ -28,6 +30,5 @@ pub use gpui_macros::test; pub use platform::FontSystem; pub use platform::{Event, PathPromptOptions, Platform, PromptLevel}; pub use presenter::{ - AfterLayoutContext, Axis, DebugContext, EventContext, LayoutContext, PaintContext, - SizeConstraint, Vector2FExt, + Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt, }; diff --git a/gpui/src/platform.rs b/gpui/src/platform.rs index 7107d7763dd373383d874cb90421c4eedd2d0ca2..a4c86eab2f9f696754c11677dabb8d32849ba33b 100644 --- a/gpui/src/platform.rs +++ b/gpui/src/platform.rs @@ -8,16 +8,16 @@ pub mod current { } use crate::{ - color::Color, executor, fonts::{FontId, GlyphId, Metrics as FontMetrics, Properties as FontProperties}, geometry::{ rect::{RectF, RectI}, - vector::Vector2F, + vector::{vec2f, Vector2F}, }, - text_layout::LineLayout, - ClipboardItem, Menu, Scene, + text_layout::{LineLayout, RunStyle}, + AnyAction, ClipboardItem, Menu, Scene, }; +use anyhow::Result; use async_task::Runnable; pub use event::Event; use std::{ @@ -26,6 +26,7 @@ use std::{ rc::Rc, sync::Arc, }; +use time::UtcOffset; pub trait Platform: Send + Sync { fn dispatcher(&self) -> Arc; @@ -45,8 +46,12 @@ pub trait Platform: Send + Sync { fn read_from_clipboard(&self) -> Option; fn open_url(&self, url: &str); - fn write_credentials(&self, url: &str, username: &str, password: &[u8]); - fn read_credentials(&self, url: &str) -> Option<(String, Vec)>; + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()>; + fn read_credentials(&self, url: &str) -> Result)>>; + + fn set_cursor_style(&self, style: CursorStyle); + + fn local_timezone(&self) -> UtcOffset; } pub(crate) trait ForegroundPlatform { @@ -56,7 +61,7 @@ pub(crate) trait ForegroundPlatform { fn on_open_files(&self, callback: Box)>); fn run(&self, on_finish_launching: Box ()>); - fn on_menu_command(&self, callback: Box)>); + fn on_menu_command(&self, callback: Box); fn set_menus(&self, menus: Vec); fn prompt_for_paths( &self, @@ -78,7 +83,7 @@ pub trait Dispatcher: Send + Sync { pub trait Window: WindowContext { fn as_any_mut(&mut self) -> &mut dyn Any; fn on_event(&mut self, callback: Box); - fn on_resize(&mut self, callback: Box); + fn on_resize(&mut self, callback: Box); fn on_close(&mut self, callback: Box); fn prompt( &self, @@ -92,12 +97,15 @@ pub trait Window: WindowContext { pub trait WindowContext { fn size(&self) -> Vector2F; fn scale_factor(&self) -> f32; + fn titlebar_height(&self) -> f32; fn present_scene(&mut self, scene: Scene); } pub struct WindowOptions<'a> { pub bounds: RectF, pub title: Option<&'a str>, + pub titlebar_appears_transparent: bool, + pub traffic_light_position: Option, } pub struct PathPromptOptions { @@ -112,7 +120,15 @@ pub enum PromptLevel { Critical, } +#[derive(Copy, Clone, Debug)] +pub enum CursorStyle { + Arrow, + ResizeLeftRight, + PointingHand, +} + pub trait FontSystem: Send + Sync { + fn add_fonts(&self, fonts: &[Arc>]) -> anyhow::Result<()>; fn load_family(&self, name: &str) -> anyhow::Result>; fn select_font( &self, @@ -130,11 +146,17 @@ pub trait FontSystem: Send + Sync { subpixel_shift: Vector2F, scale_factor: f32, ) -> Option<(RectI, Vec)>; - fn layout_line( - &self, - text: &str, - font_size: f32, - runs: &[(usize, FontId, Color)], - ) -> LineLayout; + fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout; fn wrap_line(&self, text: &str, font_id: FontId, font_size: f32, width: f32) -> Vec; } + +impl<'a> Default for WindowOptions<'a> { + fn default() -> Self { + Self { + bounds: RectF::new(Default::default(), vec2f(1024.0, 768.0)), + title: Default::default(), + titlebar_appears_transparent: Default::default(), + traffic_light_position: Default::default(), + } + } +} diff --git a/gpui/src/platform/event.rs b/gpui/src/platform/event.rs index 15ee7172f67084595793e2a607a2930d49d3eb78..fba7b812e6c8ac87479932f9556a267581551dfe 100644 --- a/gpui/src/platform/event.rs +++ b/gpui/src/platform/event.rs @@ -24,5 +24,6 @@ pub enum Event { }, MouseMoved { position: Vector2F, + left_mouse_down: bool, }, } diff --git a/gpui/src/platform/mac/event.rs b/gpui/src/platform/mac/event.rs index d3f9d9accd8ef69dbbb92d51d7fdfd6a8dea8c6a..04712d599ffc85de2a98e9af761f6764d2a3b0c4 100644 --- a/gpui/src/platform/mac/event.rs +++ b/gpui/src/platform/mac/event.rs @@ -6,8 +6,8 @@ use cocoa::appkit::{ NSUpArrowFunctionKey as ARROW_UP_KEY, }; use cocoa::{ - appkit::{NSEvent as _, NSEventModifierFlags, NSEventType}, - base::{id, YES}, + appkit::{NSEvent, NSEventModifierFlags, NSEventType}, + base::{id, nil, YES}, foundation::NSString as _, }; use std::{ffi::CStr, os::raw::c_char}; @@ -116,6 +116,7 @@ impl Event { native_event.locationInWindow().x as f32, window_height - native_event.locationInWindow().y as f32, ), + left_mouse_down: NSEvent::pressedMouseButtons(nil) & 1 != 0, }), _ => None, } diff --git a/gpui/src/platform/mac/fonts.rs b/gpui/src/platform/mac/fonts.rs index ba9e3ae3cf764d15fcb2bb8178ccd256819335ce..c01700ce22817b341dca6caf634490a3a0b24666 100644 --- a/gpui/src/platform/mac/fonts.rs +++ b/gpui/src/platform/mac/fonts.rs @@ -1,5 +1,4 @@ use crate::{ - color::Color, fonts::{FontId, GlyphId, Metrics, Properties}, geometry::{ rect::{RectF, RectI}, @@ -7,7 +6,7 @@ use crate::{ vector::{vec2f, vec2i, Vector2F}, }, platform, - text_layout::{Glyph, LineLayout, Run}, + text_layout::{Glyph, LineLayout, Run, RunStyle}, }; use cocoa::appkit::{CGFloat, CGPoint}; use core_foundation::{ @@ -21,9 +20,12 @@ use core_graphics::{ base::CGGlyph, color_space::CGColorSpace, context::CGContext, geometry::CGAffineTransform, }; use core_text::{line::CTLine, string_attributes::kCTFontAttributeName}; -use font_kit::{canvas::RasterizationOptions, hinting::HintingOptions, source::SystemSource}; +use font_kit::{ + canvas::RasterizationOptions, handle::Handle, hinting::HintingOptions, source::SystemSource, + sources::mem::MemSource, +}; use parking_lot::RwLock; -use std::{cell::RefCell, char, cmp, convert::TryFrom, ffi::c_void}; +use std::{cell::RefCell, char, cmp, convert::TryFrom, ffi::c_void, sync::Arc}; #[allow(non_upper_case_globals)] const kCGImageAlphaOnly: u32 = 7; @@ -31,20 +33,26 @@ const kCGImageAlphaOnly: u32 = 7; pub struct FontSystem(RwLock); struct FontSystemState { - source: SystemSource, + memory_source: MemSource, + system_source: SystemSource, fonts: Vec, } impl FontSystem { pub fn new() -> Self { Self(RwLock::new(FontSystemState { - source: SystemSource::new(), + memory_source: MemSource::empty(), + system_source: SystemSource::new(), fonts: Vec::new(), })) } } impl platform::FontSystem for FontSystem { + fn add_fonts(&self, fonts: &[Arc>]) -> anyhow::Result<()> { + self.0.write().add_fonts(fonts) + } + fn load_family(&self, name: &str) -> anyhow::Result> { self.0.write().load_family(name) } @@ -78,12 +86,7 @@ impl platform::FontSystem for FontSystem { .rasterize_glyph(font_id, font_size, glyph_id, subpixel_shift, scale_factor) } - fn layout_line( - &self, - text: &str, - font_size: f32, - runs: &[(usize, FontId, Color)], - ) -> LineLayout { + fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout { self.0.read().layout_line(text, font_size, runs) } @@ -93,9 +96,23 @@ impl platform::FontSystem for FontSystem { } impl FontSystemState { + fn add_fonts(&mut self, fonts: &[Arc>]) -> anyhow::Result<()> { + self.memory_source.add_fonts( + fonts + .iter() + .map(|bytes| Handle::from_memory(bytes.clone(), 0)), + )?; + Ok(()) + } + fn load_family(&mut self, name: &str) -> anyhow::Result> { let mut font_ids = Vec::new(); - for font in self.source.select_family_by_name(name)?.fonts() { + + let family = self + .memory_source + .select_family_by_name(name) + .or_else(|_| self.system_source.select_family_by_name(name))?; + for font in family.fonts() { let font = font.load()?; font_ids.push(FontId(self.fonts.len())); self.fonts.push(font); @@ -187,12 +204,7 @@ impl FontSystemState { } } - fn layout_line( - &self, - text: &str, - font_size: f32, - runs: &[(usize, FontId, Color)], - ) -> LineLayout { + fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout { let font_id_attr_name = CFString::from_static_string("zed_font_id"); // Construct the attributed string, converting UTF8 ranges to UTF16 ranges. @@ -204,20 +216,20 @@ impl FontSystemState { let last_run: RefCell> = Default::default(); let font_runs = runs .iter() - .filter_map(|(len, font_id, _)| { + .filter_map(|(len, style)| { let mut last_run = last_run.borrow_mut(); if let Some((last_len, last_font_id)) = last_run.as_mut() { - if font_id == last_font_id { + if style.font_id == *last_font_id { *last_len += *len; None } else { let result = (*last_len, *last_font_id); *last_len = *len; - *last_font_id = *font_id; + *last_font_id = style.font_id; Some(result) } } else { - *last_run = Some((*len, *font_id)); + *last_run = Some((*len, style.font_id)); None } }) @@ -392,9 +404,8 @@ extern "C" { #[cfg(test)] mod tests { - use crate::MutableAppContext; - use super::*; + use crate::MutableAppContext; use font_kit::properties::{Style, Weight}; use platform::FontSystem as _; @@ -403,13 +414,25 @@ mod tests { // This is failing intermittently on CI and we don't have time to figure it out let fonts = FontSystem::new(); let menlo = fonts.load_family("Menlo").unwrap(); - let menlo_regular = fonts.select_font(&menlo, &Properties::new()).unwrap(); - let menlo_italic = fonts - .select_font(&menlo, &Properties::new().style(Style::Italic)) - .unwrap(); - let menlo_bold = fonts - .select_font(&menlo, &Properties::new().weight(Weight::BOLD)) - .unwrap(); + let menlo_regular = RunStyle { + font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(), + color: Default::default(), + underline: false, + }; + let menlo_italic = RunStyle { + font_id: fonts + .select_font(&menlo, &Properties::new().style(Style::Italic)) + .unwrap(), + color: Default::default(), + underline: false, + }; + let menlo_bold = RunStyle { + font_id: fonts + .select_font(&menlo, &Properties::new().weight(Weight::BOLD)) + .unwrap(), + color: Default::default(), + underline: false, + }; assert_ne!(menlo_regular, menlo_italic); assert_ne!(menlo_regular, menlo_bold); assert_ne!(menlo_italic, menlo_bold); @@ -417,18 +440,14 @@ mod tests { let line = fonts.layout_line( "hello world", 16.0, - &[ - (2, menlo_bold, Default::default()), - (4, menlo_italic, Default::default()), - (5, menlo_regular, Default::default()), - ], + &[(2, menlo_bold), (4, menlo_italic), (5, menlo_regular)], ); assert_eq!(line.runs.len(), 3); - assert_eq!(line.runs[0].font_id, menlo_bold); + assert_eq!(line.runs[0].font_id, menlo_bold.font_id); assert_eq!(line.runs[0].glyphs.len(), 2); - assert_eq!(line.runs[1].font_id, menlo_italic); + assert_eq!(line.runs[1].font_id, menlo_italic.font_id); assert_eq!(line.runs[1].glyphs.len(), 4); - assert_eq!(line.runs[2].font_id, menlo_regular); + assert_eq!(line.runs[2].font_id, menlo_regular.font_id); assert_eq!(line.runs[2].glyphs.len(), 5); } @@ -436,18 +455,26 @@ mod tests { fn test_glyph_offsets() -> anyhow::Result<()> { let fonts = FontSystem::new(); let zapfino = fonts.load_family("Zapfino")?; - let zapfino_regular = fonts.select_font(&zapfino, &Properties::new())?; + let zapfino_regular = RunStyle { + font_id: fonts.select_font(&zapfino, &Properties::new())?, + color: Default::default(), + underline: false, + }; let menlo = fonts.load_family("Menlo")?; - let menlo_regular = fonts.select_font(&menlo, &Properties::new())?; + let menlo_regular = RunStyle { + font_id: fonts.select_font(&menlo, &Properties::new())?, + color: Default::default(), + underline: false, + }; let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈"; let line = fonts.layout_line( text, 16.0, &[ - (9, zapfino_regular, Color::default()), - (13, menlo_regular, Color::default()), - (text.len() - 22, zapfino_regular, Color::default()), + (9, zapfino_regular), + (13, menlo_regular), + (text.len() - 22, zapfino_regular), ], ); assert_eq!( @@ -513,15 +540,19 @@ mod tests { fn test_layout_line_bom_char() { let fonts = FontSystem::new(); let font_ids = fonts.load_family("Helvetica").unwrap(); - let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap(); + let style = RunStyle { + font_id: fonts.select_font(&font_ids, &Default::default()).unwrap(), + color: Default::default(), + underline: false, + }; let line = "\u{feff}"; - let layout = fonts.layout_line(line, 16., &[(line.len(), font_id, Default::default())]); + let layout = fonts.layout_line(line, 16., &[(line.len(), style)]); assert_eq!(layout.len, line.len()); assert!(layout.runs.is_empty()); let line = "a\u{feff}b"; - let layout = fonts.layout_line(line, 16., &[(line.len(), font_id, Default::default())]); + let layout = fonts.layout_line(line, 16., &[(line.len(), style)]); assert_eq!(layout.len, line.len()); assert_eq!(layout.runs.len(), 1); assert_eq!(layout.runs[0].glyphs.len(), 2); diff --git a/gpui/src/platform/mac/platform.rs b/gpui/src/platform/mac/platform.rs index 794debe4b162d499ff404ad37880cbc9739413de..7015cbc713cecc528e742fe902a86c840c25cfd1 100644 --- a/gpui/src/platform/mac/platform.rs +++ b/gpui/src/platform/mac/platform.rs @@ -1,5 +1,11 @@ use super::{BoolExt as _, Dispatcher, FontSystem, Window}; -use crate::{executor, keymap::Keystroke, platform, ClipboardItem, Event, Menu, MenuItem}; +use crate::{ + executor, + keymap::Keystroke, + platform::{self, CursorStyle}, + AnyAction, ClipboardItem, Event, Menu, MenuItem, +}; +use anyhow::{anyhow, Result}; use block::ConcreteBlock; use cocoa::{ appkit::{ @@ -27,7 +33,6 @@ use objc::{ }; use ptr::null_mut; use std::{ - any::Any, cell::{Cell, RefCell}, convert::TryInto, ffi::{c_void, CStr}, @@ -38,6 +43,7 @@ use std::{ slice, str, sync::Arc, }; +use time::UtcOffset; const MAC_PLATFORM_IVAR: &'static str = "platform"; static mut APP_CLASS: *const Class = ptr::null(); @@ -90,10 +96,10 @@ pub struct MacForegroundPlatformState { become_active: Option>, resign_active: Option>, event: Option bool>>, - menu_command: Option)>>, + menu_command: Option>, open_files: Option)>>, finish_launching: Option ()>>, - menu_actions: Vec<(String, Option>)>, + menu_actions: Vec>, } impl MacForegroundPlatform { @@ -121,7 +127,6 @@ impl MacForegroundPlatform { name, keystroke, action, - arg, } => { if let Some(keystroke) = keystroke { let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| { @@ -162,7 +167,7 @@ impl MacForegroundPlatform { let tag = state.menu_actions.len() as NSInteger; let _: () = msg_send![item, setTag: tag]; - state.menu_actions.push((action.to_string(), arg)); + state.menu_actions.push(action); } } @@ -215,7 +220,7 @@ impl platform::ForegroundPlatform for MacForegroundPlatform { } } - fn on_menu_command(&self, callback: Box)>) { + fn on_menu_command(&self, callback: Box) { self.0.borrow_mut().menu_command = Some(callback); } @@ -465,7 +470,7 @@ impl platform::Platform for MacPlatform { } } - fn write_credentials(&self, url: &str, username: &str, password: &[u8]) { + 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); @@ -498,12 +503,13 @@ impl platform::Platform for MacPlatform { } if status != errSecSuccess { - panic!("{} password failed: {}", verb, status); + return Err(anyhow!("{} password failed: {}", verb, status)); } } + Ok(()) } - fn read_credentials(&self, url: &str) -> Option<(String, Vec)> { + fn read_credentials(&self, url: &str) -> Result)>> { let url = CFString::from(url); let cf_true = CFBoolean::true_value().as_CFTypeRef(); @@ -521,27 +527,46 @@ impl platform::Platform for MacPlatform { let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result); match status { security::errSecSuccess => {} - security::errSecItemNotFound | security::errSecUserCanceled => return None, - _ => panic!("reading password failed: {}", status), + security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None), + _ => return Err(anyhow!("reading password failed: {}", status)), } let result = CFType::wrap_under_create_rule(result) .downcast::() - .expect("keychain item was not a dictionary"); + .ok_or_else(|| anyhow!("keychain item was not a dictionary"))?; let username = result .find(kSecAttrAccount as *const _) - .expect("account was missing from keychain item"); + .ok_or_else(|| anyhow!("account was missing from keychain item"))?; let username = CFType::wrap_under_get_rule(*username) .downcast::() - .expect("account was not a string"); + .ok_or_else(|| anyhow!("account was not a string"))?; let password = result .find(kSecValueData as *const _) - .expect("password was missing from keychain item"); + .ok_or_else(|| anyhow!("password was missing from keychain item"))?; let password = CFType::wrap_under_get_rule(*password) .downcast::() - .expect("password was not a string"); + .ok_or_else(|| anyhow!("password was not a string"))?; - Some((username.to_string(), password.bytes().to_vec())) + Ok(Some((username.to_string(), password.bytes().to_vec()))) + } + } + + fn set_cursor_style(&self, style: CursorStyle) { + unsafe { + let cursor: id = match style { + CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor], + CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor], + CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor], + }; + let _: () = msg_send![cursor, set]; + } + } + + 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() } } } @@ -623,8 +648,8 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { 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, arg)) = platform.menu_actions.get(index) { - callback(action, arg.as_ref().map(Box::as_ref)); + if let Some(action) = platform.menu_actions.get(index) { + callback(action.as_ref()); } platform.menu_command = Some(callback); } diff --git a/gpui/src/platform/mac/renderer.rs b/gpui/src/platform/mac/renderer.rs index 82e80790cb83d0dfe542cb2aaf4a50ef4d41aff6..e12a52d6134dbd2c5d110a570f52920c449a287d 100644 --- a/gpui/src/platform/mac/renderer.rs +++ b/gpui/src/platform/mac/renderer.rs @@ -6,7 +6,7 @@ use crate::{ vector::{vec2f, vec2i, Vector2F}, }, platform, - scene::Layer, + scene::{Glyph, Icon, Layer, Quad, Shadow}, Scene, }; use cocoa::foundation::NSUInteger; @@ -142,7 +142,7 @@ impl Renderer { let mut sprites = Vec::new(); let mut vertices = Vec::::new(); let mut current_atlas_id = None; - for (layer_id, layer) in scene.layers().iter().enumerate() { + for (layer_id, layer) in scene.layers().enumerate() { for path in layer.paths() { let origin = path.bounds.origin() * scene.scale_factor(); let size = (path.bounds.size() * scene.scale_factor()).ceil(); @@ -283,12 +283,24 @@ impl Renderer { zfar: 1.0, }); + let scale_factor = scene.scale_factor(); let mut path_sprites = path_sprites.into_iter().peekable(); - - for (layer_id, layer) in scene.layers().iter().enumerate() { + for (layer_id, layer) in scene.layers().enumerate() { self.clip(scene, layer, drawable_size, command_encoder); - self.render_shadows(scene, layer, offset, drawable_size, command_encoder); - self.render_quads(scene, layer, offset, drawable_size, command_encoder); + self.render_shadows( + layer.shadows(), + scale_factor, + offset, + drawable_size, + command_encoder, + ); + self.render_quads( + layer.quads(), + scale_factor, + offset, + drawable_size, + command_encoder, + ); self.render_path_sprites( layer_id, &mut path_sprites, @@ -296,7 +308,21 @@ impl Renderer { drawable_size, command_encoder, ); - self.render_sprites(scene, layer, offset, drawable_size, command_encoder); + self.render_sprites( + layer.glyphs(), + layer.icons(), + scale_factor, + offset, + drawable_size, + command_encoder, + ); + self.render_quads( + layer.underlines(), + scale_factor, + offset, + drawable_size, + command_encoder, + ); } command_encoder.end_encoding(); @@ -324,18 +350,18 @@ impl Renderer { fn render_shadows( &mut self, - scene: &Scene, - layer: &Layer, + shadows: &[Shadow], + scale_factor: f32, offset: &mut usize, drawable_size: Vector2F, command_encoder: &metal::RenderCommandEncoderRef, ) { - if layer.shadows().is_empty() { + if shadows.is_empty() { return; } align_offset(offset); - let next_offset = *offset + layer.shadows().len() * mem::size_of::(); + let next_offset = *offset + shadows.len() * mem::size_of::(); assert!( next_offset <= INSTANCE_BUFFER_SIZE, "instance buffer exhausted" @@ -365,12 +391,12 @@ impl Renderer { (self.instances.contents() as *mut u8).offset(*offset as isize) as *mut shaders::GPUIShadow }; - for (ix, shadow) in layer.shadows().iter().enumerate() { - let shape_bounds = shadow.bounds * scene.scale_factor(); + for (ix, shadow) in shadows.iter().enumerate() { + let shape_bounds = shadow.bounds * scale_factor; let shader_shadow = shaders::GPUIShadow { origin: shape_bounds.origin().to_float2(), size: shape_bounds.size().to_float2(), - corner_radius: shadow.corner_radius * scene.scale_factor(), + corner_radius: shadow.corner_radius * scale_factor, sigma: shadow.sigma, color: shadow.color.to_uchar4(), }; @@ -383,24 +409,24 @@ impl Renderer { metal::MTLPrimitiveType::Triangle, 0, 6, - layer.shadows().len() as u64, + shadows.len() as u64, ); *offset = next_offset; } fn render_quads( &mut self, - scene: &Scene, - layer: &Layer, + quads: &[Quad], + scale_factor: f32, offset: &mut usize, drawable_size: Vector2F, command_encoder: &metal::RenderCommandEncoderRef, ) { - if layer.quads().is_empty() { + if quads.is_empty() { return; } align_offset(offset); - let next_offset = *offset + layer.quads().len() * mem::size_of::(); + let next_offset = *offset + quads.len() * mem::size_of::(); assert!( next_offset <= INSTANCE_BUFFER_SIZE, "instance buffer exhausted" @@ -430,9 +456,9 @@ impl Renderer { (self.instances.contents() as *mut u8).offset(*offset as isize) as *mut shaders::GPUIQuad }; - for (ix, quad) in layer.quads().iter().enumerate() { - let bounds = quad.bounds * scene.scale_factor(); - let border_width = quad.border.width * scene.scale_factor(); + for (ix, quad) in quads.iter().enumerate() { + let bounds = quad.bounds * scale_factor; + let border_width = quad.border.width * scale_factor; let shader_quad = shaders::GPUIQuad { origin: bounds.origin().round().to_float2(), size: bounds.size().round().to_float2(), @@ -445,7 +471,7 @@ impl Renderer { border_bottom: border_width * (quad.border.bottom as usize as f32), border_left: border_width * (quad.border.left as usize as f32), border_color: quad.border.color.to_uchar4(), - corner_radius: quad.corner_radius * scene.scale_factor(), + corner_radius: quad.corner_radius * scale_factor, }; unsafe { *(buffer_contents.offset(ix as isize)) = shader_quad; @@ -456,35 +482,36 @@ impl Renderer { metal::MTLPrimitiveType::Triangle, 0, 6, - layer.quads().len() as u64, + quads.len() as u64, ); *offset = next_offset; } fn render_sprites( &mut self, - scene: &Scene, - layer: &Layer, + glyphs: &[Glyph], + icons: &[Icon], + scale_factor: f32, offset: &mut usize, drawable_size: Vector2F, command_encoder: &metal::RenderCommandEncoderRef, ) { - if layer.glyphs().is_empty() && layer.icons().is_empty() { + if glyphs.is_empty() && icons.is_empty() { return; } let mut sprites_by_atlas = HashMap::new(); - for glyph in layer.glyphs() { + for glyph in glyphs { if let Some(sprite) = self.sprite_cache.render_glyph( glyph.font_id, glyph.font_size, glyph.id, glyph.origin, - scene.scale_factor(), + scale_factor, ) { // Snap sprite to pixel grid. - let origin = (glyph.origin * scene.scale_factor()).floor() + sprite.offset.to_f32(); + let origin = (glyph.origin * scale_factor).floor() + sprite.offset.to_f32(); sprites_by_atlas .entry(sprite.atlas_id) .or_insert_with(Vec::new) @@ -499,9 +526,9 @@ impl Renderer { } } - for icon in layer.icons() { - let origin = icon.bounds.origin() * scene.scale_factor(); - let target_size = icon.bounds.size() * scene.scale_factor(); + for icon in icons { + let origin = icon.bounds.origin() * scale_factor; + let target_size = icon.bounds.size() * scale_factor; let source_size = (target_size * 2.).ceil().to_i32(); let sprite = diff --git a/gpui/src/platform/mac/window.rs b/gpui/src/platform/mac/window.rs index 4139842eda7964fdd536cad54d5f1171ccc460a0..efceee0ba3d3156d12744d485f98d731da91e57c 100644 --- a/gpui/src/platform/mac/window.rs +++ b/gpui/src/platform/mac/window.rs @@ -8,13 +8,14 @@ use crate::{ use block::ConcreteBlock; use cocoa::{ appkit::{ - NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable, - NSViewWidthSizable, NSWindow, NSWindowStyleMask, + CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable, + NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowStyleMask, }, base::{id, nil}, foundation::{NSAutoreleasePool, NSInteger, NSSize, NSString}, quartzcore::AutoresizingMask, }; +use core_graphics::display::CGRect; use ctor::ctor; use foreign_types::ForeignType as _; use objc::{ @@ -65,6 +66,10 @@ unsafe fn build_classes() { sel!(sendEvent:), send_event as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(windowDidResize:), + window_did_resize as extern "C" fn(&Object, Sel, id), + ); decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel)); decl.register() }; @@ -129,7 +134,7 @@ struct WindowState { id: usize, native_window: id, event_callback: Option>, - resize_callback: Option>, + resize_callback: Option>, close_callback: Option>, synthetic_drag_counter: usize, executor: Rc, @@ -138,6 +143,7 @@ struct WindowState { command_queue: metal::CommandQueue, last_fresh_keydown: Option<(Keystroke, String)>, layer: id, + traffic_light_position: Option, } impl Window { @@ -153,11 +159,15 @@ impl Window { let pool = NSAutoreleasePool::new(nil); let frame = options.bounds.to_ns_rect(); - let style_mask = NSWindowStyleMask::NSClosableWindowMask + let mut style_mask = NSWindowStyleMask::NSClosableWindowMask | NSWindowStyleMask::NSMiniaturizableWindowMask | NSWindowStyleMask::NSResizableWindowMask | NSWindowStyleMask::NSTitledWindowMask; + if options.titlebar_appears_transparent { + style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask; + } + let native_window: id = msg_send![WINDOW_CLASS, alloc]; let native_window = native_window.initWithContentRect_styleMask_backing_defer_( frame, @@ -199,12 +209,14 @@ impl Window { command_queue: device.new_command_queue(), last_fresh_keydown: None, layer, + traffic_light_position: options.traffic_light_position, }))); (*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, @@ -213,6 +225,9 @@ impl Window { if let Some(title) = options.title.as_ref() { native_window.setTitle_(NSString::alloc(nil).init_str(title)); } + if options.titlebar_appears_transparent { + native_window.setTitlebarAppearsTransparent_(YES); + } native_window.setAcceptsMouseMovedEvents_(YES); native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); @@ -235,6 +250,7 @@ impl Window { native_window.center(); native_window.makeKeyAndOrderFront_(nil); + window.0.borrow().move_traffic_light(); pool.drain(); window @@ -272,7 +288,7 @@ impl platform::Window for Window { self.0.as_ref().borrow_mut().event_callback = Some(callback); } - fn on_resize(&mut self, callback: Box) { + fn on_resize(&mut self, callback: Box) { self.0.as_ref().borrow_mut().resize_callback = Some(callback); } @@ -329,6 +345,56 @@ impl platform::WindowContext for Window { fn present_scene(&mut self, scene: Scene) { self.0.as_ref().borrow_mut().present_scene(scene); } + + fn titlebar_height(&self) -> f32 { + self.0.as_ref().borrow().titlebar_height() + } +} + +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 = vec2f( + traffic_light_position.x(), + titlebar_height + - traffic_light_position.y() + - close_button_frame.size.height as f32, + ); + let button_spacing = + (min_button_frame.origin.x - close_button_frame.origin.x) as f32; + + close_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64); + let _: () = msg_send![close_button, setFrame: close_button_frame]; + origin.set_x(origin.x() + button_spacing); + + min_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64); + let _: () = msg_send![min_button, setFrame: min_button_frame]; + origin.set_x(origin.x() + button_spacing); + + zoom_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64); + let _: () = msg_send![zoom_button, setFrame: zoom_button_frame]; + } + } + } } impl platform::WindowContext for WindowState { @@ -345,6 +411,14 @@ impl platform::WindowContext for WindowState { } } + fn titlebar_height(&self) -> f32 { + unsafe { + let frame = NSWindow::frame(self.native_window); + let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect]; + (frame.size.height - content_layout_rect.size.height) as f32 + } + } + fn present_scene(&mut self, scene: Scene) { self.scene_to_render = Some(scene); unsafe { @@ -442,6 +516,11 @@ extern "C" fn send_event(this: &Object, _: Sel, native_event: id) { } } +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 close_window(this: &Object, _: Sel) { unsafe { let close_callback = { @@ -469,24 +548,24 @@ extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id { extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) { let window_state = unsafe { get_window_state(this) }; - let mut window_state = window_state.as_ref().borrow_mut(); + let mut window_state_borrow = window_state.as_ref().borrow_mut(); unsafe { - let _: () = - msg_send![window_state.layer, setContentsScale: window_state.scale_factor() as f64]; + let _: () = msg_send![window_state_borrow.layer, setContentsScale: window_state_borrow.scale_factor() as f64]; } - if let Some(mut callback) = window_state.resize_callback.take() { - callback(&mut *window_state); - window_state.resize_callback = Some(callback); + 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 mut window_state = window_state.as_ref().borrow_mut(); + let mut window_state_borrow = window_state.as_ref().borrow_mut(); - if window_state.size() == vec2f(size.width as f32, size.height as f32) { + if window_state_borrow.size() == vec2f(size.width as f32, size.height as f32) { return; } @@ -494,19 +573,20 @@ extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) { let _: () = msg_send![super(this, class!(NSView)), setFrameSize: size]; } - let scale_factor = window_state.scale_factor() as f64; + 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.layer, setDrawableSize: drawable_size]; + let _: () = msg_send![window_state_borrow.layer, setDrawableSize: drawable_size]; } - if let Some(mut callback) = window_state.resize_callback.take() { - callback(&mut *window_state); - window_state.resize_callback = Some(callback); + if let Some(mut callback) = window_state_borrow.resize_callback.take() { + drop(window_state_borrow); + callback(); + window_state.borrow_mut().resize_callback = Some(callback); }; } diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 77e23d5aa6773c5790545f04c47dca4291f22b40..85afff49994607fc2c16fd9705f5c207f6a6969e 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -1,4 +1,6 @@ -use crate::ClipboardItem; +use super::CursorStyle; +use crate::{AnyAction, ClipboardItem}; +use anyhow::Result; use parking_lot::Mutex; use pathfinder_geometry::vector::Vector2F; use std::{ @@ -8,11 +10,13 @@ use std::{ rc::Rc, sync::Arc, }; +use time::UtcOffset; pub struct Platform { dispatcher: Arc, fonts: Arc, current_clipboard_item: Mutex>, + cursor: Mutex, } #[derive(Default)] @@ -27,7 +31,7 @@ pub struct Window { scale_factor: f32, current_scene: Option, event_handlers: Vec>, - resize_handlers: Vec>, + resize_handlers: Vec>, close_handlers: Vec>, pub(crate) last_prompt: RefCell>>, } @@ -62,7 +66,7 @@ impl super::ForegroundPlatform for ForegroundPlatform { unimplemented!() } - fn on_menu_command(&self, _: Box)>) {} + fn on_menu_command(&self, _: Box) {} fn set_menus(&self, _: Vec) {} @@ -84,6 +88,7 @@ impl Platform { dispatcher: Arc::new(Dispatcher), fonts: Arc::new(super::current::FontSystem::new()), current_clipboard_item: Default::default(), + cursor: Mutex::new(CursorStyle::Arrow), } } } @@ -124,10 +129,20 @@ impl super::Platform for Platform { fn open_url(&self, _: &str) {} - fn write_credentials(&self, _: &str, _: &str, _: &[u8]) {} + fn write_credentials(&self, _: &str, _: &str, _: &[u8]) -> Result<()> { + Ok(()) + } - fn read_credentials(&self, _: &str) -> Option<(String, Vec)> { - None + fn read_credentials(&self, _: &str) -> Result)>> { + Ok(None) + } + + fn set_cursor_style(&self, style: CursorStyle) { + *self.cursor.lock() = style; + } + + fn local_timezone(&self) -> UtcOffset { + UtcOffset::UTC } } @@ -164,6 +179,10 @@ impl super::WindowContext for Window { self.scale_factor } + fn titlebar_height(&self) -> f32 { + 24. + } + fn present_scene(&mut self, scene: crate::Scene) { self.current_scene = Some(scene); } @@ -178,7 +197,7 @@ impl super::Window for Window { self.event_handlers.push(callback); } - fn on_resize(&mut self, callback: Box) { + fn on_resize(&mut self, callback: Box) { self.resize_handlers.push(callback); } diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index 844dc0502432dc2c96fabca95a92aac36ac202dd..b2fa59b848aa2bb3ca53c9e5565104a6186bb1dc 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -2,16 +2,18 @@ use crate::{ app::{AppContext, MutableAppContext, WindowInvalidation}, elements::Element, font_cache::FontCache, + geometry::rect::RectF, json::{self, ToJson}, platform::Event, text_layout::TextLayoutCache, - AssetCache, ElementBox, Scene, + Action, AnyAction, AssetCache, ElementBox, Entity, FontSystem, ModelHandle, ReadModel, + ReadView, Scene, View, ViewHandle, }; use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; use std::{ - any::Any, collections::{HashMap, HashSet}, + ops::{Deref, DerefMut}, sync::Arc, }; @@ -23,24 +25,27 @@ pub struct Presenter { text_layout_cache: TextLayoutCache, asset_cache: Arc, last_mouse_moved_event: Option, + titlebar_height: f32, } impl Presenter { pub fn new( window_id: usize, + titlebar_height: f32, font_cache: Arc, text_layout_cache: TextLayoutCache, asset_cache: Arc, - cx: &MutableAppContext, + cx: &mut MutableAppContext, ) -> Self { Self { window_id, - rendered_views: cx.render_views(window_id), + rendered_views: cx.render_views(window_id, titlebar_height), parents: HashMap::new(), font_cache, text_layout_cache, asset_cache, last_mouse_moved_event: None, + titlebar_height, } } @@ -55,15 +60,37 @@ impl Presenter { path } - pub fn invalidate(&mut self, mut invalidation: WindowInvalidation, cx: &AppContext) { + pub fn invalidate(&mut self, mut invalidation: WindowInvalidation, cx: &mut MutableAppContext) { for view_id in invalidation.removed { invalidation.updated.remove(&view_id); self.rendered_views.remove(&view_id); self.parents.remove(&view_id); } for view_id in invalidation.updated { - self.rendered_views - .insert(view_id, cx.render_view(self.window_id, view_id).unwrap()); + self.rendered_views.insert( + view_id, + cx.render_view(self.window_id, view_id, self.titlebar_height, false) + .unwrap(), + ); + } + } + + pub fn refresh( + &mut self, + invalidation: Option, + cx: &mut MutableAppContext, + ) { + if let Some(invalidation) = invalidation { + for view_id in invalidation.removed { + self.rendered_views.remove(&view_id); + self.parents.remove(&view_id); + } + } + + for (view_id, view) in &mut self.rendered_views { + *view = cx + .render_view(self.window_id, *view_id, self.titlebar_height, true) + .unwrap(); } } @@ -71,13 +98,13 @@ impl Presenter { &mut self, window_size: Vector2F, scale_factor: f32, + refreshing: bool, cx: &mut MutableAppContext, ) -> Scene { let mut scene = Scene::new(scale_factor); if let Some(root_view_id) = cx.root_view_id(self.window_id) { - self.layout(window_size, cx); - self.after_layout(cx); + self.layout(window_size, refreshing, cx); let mut paint_cx = PaintContext { scene: &mut scene, font_cache: &self.font_cache, @@ -85,7 +112,11 @@ impl Presenter { rendered_views: &mut self.rendered_views, app: cx.as_ref(), }; - paint_cx.paint(root_view_id, Vector2F::zero()); + paint_cx.paint( + root_view_id, + Vector2F::zero(), + RectF::new(Vector2F::zero(), window_size), + ); self.text_layout_cache.finish_frame(); if let Some(event) = self.last_mouse_moved_event.clone() { @@ -98,67 +129,76 @@ impl Presenter { scene } - fn layout(&mut self, size: Vector2F, cx: &mut MutableAppContext) { + fn layout(&mut self, size: Vector2F, refreshing: bool, cx: &mut MutableAppContext) { if let Some(root_view_id) = cx.root_view_id(self.window_id) { - let mut layout_ctx = LayoutContext { - rendered_views: &mut self.rendered_views, - parents: &mut self.parents, - font_cache: &self.font_cache, - text_layout_cache: &self.text_layout_cache, - asset_cache: &self.asset_cache, - view_stack: Vec::new(), - app: cx, - }; - layout_ctx.layout(root_view_id, SizeConstraint::strict(size)); + self.build_layout_context(refreshing, cx) + .layout(root_view_id, SizeConstraint::strict(size)); } } - fn after_layout(&mut self, cx: &mut MutableAppContext) { - if let Some(root_view_id) = cx.root_view_id(self.window_id) { - let mut layout_cx = AfterLayoutContext { - rendered_views: &mut self.rendered_views, - font_cache: &self.font_cache, - text_layout_cache: &self.text_layout_cache, - app: cx, - }; - layout_cx.after_layout(root_view_id); + pub fn build_layout_context<'a>( + &'a mut self, + refreshing: bool, + cx: &'a mut MutableAppContext, + ) -> LayoutContext<'a> { + LayoutContext { + rendered_views: &mut self.rendered_views, + parents: &mut self.parents, + refreshing, + font_cache: &self.font_cache, + font_system: cx.platform().fonts(), + text_layout_cache: &self.text_layout_cache, + asset_cache: &self.asset_cache, + view_stack: Vec::new(), + app: cx, } } pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) { if let Some(root_view_id) = cx.root_view_id(self.window_id) { - if matches!(event, Event::MouseMoved { .. }) { - self.last_mouse_moved_event = Some(event.clone()); + match event { + Event::MouseMoved { .. } => { + self.last_mouse_moved_event = Some(event.clone()); + } + Event::LeftMouseDragged { position } => { + self.last_mouse_moved_event = Some(Event::MouseMoved { + position, + left_mouse_down: true, + }); + } + _ => {} } - let mut event_cx = EventContext { - rendered_views: &mut self.rendered_views, - actions: Default::default(), - font_cache: &self.font_cache, - text_layout_cache: &self.text_layout_cache, - view_stack: Default::default(), - invalidated_views: Default::default(), - app: cx, - }; + let mut event_cx = self.build_event_context(cx); event_cx.dispatch_event(root_view_id, &event); let invalidated_views = event_cx.invalidated_views; - let actions = event_cx.actions; + let dispatch_directives = event_cx.dispatched_actions; for view_id in invalidated_views { cx.notify_view(self.window_id, view_id); } - for action in actions { - cx.dispatch_action_any( - self.window_id, - &action.path, - action.name, - action.arg.as_ref(), - ); + for directive in dispatch_directives { + cx.dispatch_action_any(self.window_id, &directive.path, directive.action.as_ref()); } } } + pub fn build_event_context<'a>( + &'a mut self, + cx: &'a mut MutableAppContext, + ) -> EventContext<'a> { + EventContext { + rendered_views: &mut self.rendered_views, + dispatched_actions: Default::default(), + font_cache: &self.font_cache, + text_layout_cache: &self.text_layout_cache, + view_stack: Default::default(), + invalidated_views: Default::default(), + app: cx, + } + } + pub fn debug_elements(&self, cx: &AppContext) -> Option { cx.root_view_id(self.window_id) .and_then(|root_view_id| self.rendered_views.get(&root_view_id)) @@ -172,20 +212,21 @@ impl Presenter { } } -pub struct ActionToDispatch { +pub struct DispatchDirective { pub path: Vec, - pub name: &'static str, - pub arg: Box, + pub action: Box, } pub struct LayoutContext<'a> { rendered_views: &'a mut HashMap, parents: &'a mut HashMap, - pub font_cache: &'a FontCache, + view_stack: Vec, + pub refreshing: bool, + pub font_cache: &'a Arc, + pub font_system: Arc, pub text_layout_cache: &'a TextLayoutCache, pub asset_cache: &'a AssetCache, pub app: &'a mut MutableAppContext, - view_stack: Vec, } impl<'a> LayoutContext<'a> { @@ -202,19 +243,29 @@ impl<'a> LayoutContext<'a> { } } -pub struct AfterLayoutContext<'a> { - rendered_views: &'a mut HashMap, - pub font_cache: &'a FontCache, - pub text_layout_cache: &'a TextLayoutCache, - pub app: &'a mut MutableAppContext, +impl<'a> Deref for LayoutContext<'a> { + type Target = MutableAppContext; + + fn deref(&self) -> &Self::Target { + self.app + } } -impl<'a> AfterLayoutContext<'a> { - fn after_layout(&mut self, view_id: usize) { - if let Some(mut view) = self.rendered_views.remove(&view_id) { - view.after_layout(self); - self.rendered_views.insert(view_id, view); - } +impl<'a> DerefMut for LayoutContext<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.app + } +} + +impl<'a> ReadView for LayoutContext<'a> { + fn read_view(&self, handle: &ViewHandle) -> &T { + self.app.read_view(handle) + } +} + +impl<'a> ReadModel for LayoutContext<'a> { + fn read_model(&self, handle: &ModelHandle) -> &T { + self.app.read_model(handle) } } @@ -227,9 +278,9 @@ pub struct PaintContext<'a> { } impl<'a> PaintContext<'a> { - fn paint(&mut self, view_id: usize, origin: Vector2F) { + fn paint(&mut self, view_id: usize, origin: Vector2F, visible_bounds: RectF) { if let Some(mut tree) = self.rendered_views.remove(&view_id) { - tree.paint(origin, self); + tree.paint(origin, visible_bounds, self); self.rendered_views.insert(view_id, tree); } } @@ -237,7 +288,7 @@ impl<'a> PaintContext<'a> { pub struct EventContext<'a> { rendered_views: &'a mut HashMap, - actions: Vec, + dispatched_actions: Vec, pub font_cache: &'a FontCache, pub text_layout_cache: &'a TextLayoutCache, pub app: &'a mut MutableAppContext, @@ -258,17 +309,31 @@ impl<'a> EventContext<'a> { } } - pub fn dispatch_action(&mut self, name: &'static str, arg: A) { - self.actions.push(ActionToDispatch { + pub fn dispatch_action(&mut self, action: A) { + self.dispatched_actions.push(DispatchDirective { path: self.view_stack.clone(), - name, - arg: Box::new(arg), + action: Box::new(action), }); } pub fn notify(&mut self) { - self.invalidated_views - .insert(*self.view_stack.last().unwrap()); + if let Some(view_id) = self.view_stack.last() { + self.invalidated_views.insert(*view_id); + } + } +} + +impl<'a> Deref for EventContext<'a> { + type Target = MutableAppContext; + + fn deref(&self) -> &Self::Target { + self.app + } +} + +impl<'a> DerefMut for EventContext<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.app } } @@ -352,6 +417,13 @@ impl SizeConstraint { Axis::Vertical => self.max.y(), } } + + pub fn min_along(&self, axis: Axis) -> f32 { + match axis { + Axis::Horizontal => self.min.x(), + Axis::Vertical => self.min.y(), + } + } } impl ToJson for SizeConstraint { @@ -386,28 +458,20 @@ impl Element for ChildView { (size, ()) } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - cx.after_layout(self.view_id); - } - fn paint( &mut self, - bounds: pathfinder_geometry::rect::RectF, + bounds: RectF, + visible_bounds: RectF, _: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - cx.paint(self.view_id, bounds.origin()); + cx.paint(self.view_id, bounds.origin(), visible_bounds); } fn dispatch_event( &mut self, event: &Event, - _: pathfinder_geometry::rect::RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, cx: &mut EventContext, @@ -417,7 +481,7 @@ impl Element for ChildView { fn debug( &self, - bounds: pathfinder_geometry::rect::RectF, + bounds: RectF, _: &Self::LayoutState, _: &Self::PaintState, cx: &DebugContext, diff --git a/gpui/src/scene.rs b/gpui/src/scene.rs index 3818a0870120ba7a0183db69ad3d9dfc1ecb8b60..401918c5fe014426a98496d7f4e2d31b903bb274 100644 --- a/gpui/src/scene.rs +++ b/gpui/src/scene.rs @@ -11,6 +11,11 @@ use crate::{ pub struct Scene { scale_factor: f32, + stacking_contexts: Vec, + active_stacking_context_stack: Vec, +} + +struct StackingContext { layers: Vec, active_layer_stack: Vec, } @@ -19,6 +24,7 @@ pub struct Scene { pub struct Layer { clip_bounds: Option, quads: Vec, + underlines: Vec, shadows: Vec, glyphs: Vec, icons: Vec, @@ -120,10 +126,11 @@ pub struct PathVertex { impl Scene { pub fn new(scale_factor: f32) -> Self { + let stacking_context = StackingContext::new(None); Scene { scale_factor, - layers: vec![Layer::new(None)], - active_layer_stack: vec![0], + stacking_contexts: vec![stacking_context], + active_stacking_context_stack: vec![0], } } @@ -131,25 +138,38 @@ impl Scene { self.scale_factor } - pub fn layers(&self) -> &[Layer] { - self.layers.as_slice() + pub fn layers(&self) -> impl Iterator { + self.stacking_contexts.iter().flat_map(|s| &s.layers) + } + + pub fn push_stacking_context(&mut self, clip_bounds: Option) { + self.active_stacking_context_stack + .push(self.stacking_contexts.len()); + self.stacking_contexts + .push(StackingContext::new(clip_bounds)) + } + + pub fn pop_stacking_context(&mut self) { + self.active_stacking_context_stack.pop(); + assert!(!self.active_stacking_context_stack.is_empty()); } pub fn push_layer(&mut self, clip_bounds: Option) { - let ix = self.layers.len(); - self.layers.push(Layer::new(clip_bounds)); - self.active_layer_stack.push(ix); + self.active_stacking_context().push_layer(clip_bounds); } pub fn pop_layer(&mut self) { - assert!(self.active_layer_stack.len() > 1); - self.active_layer_stack.pop(); + self.active_stacking_context().pop_layer(); } pub fn push_quad(&mut self, quad: Quad) { self.active_layer().push_quad(quad) } + pub fn push_underline(&mut self, underline: Quad) { + self.active_layer().push_underline(underline) + } + pub fn push_shadow(&mut self, shadow: Shadow) { self.active_layer().push_shadow(shadow) } @@ -166,9 +186,52 @@ impl Scene { self.active_layer().push_path(path); } + fn active_stacking_context(&mut self) -> &mut StackingContext { + let ix = *self.active_stacking_context_stack.last().unwrap(); + &mut self.stacking_contexts[ix] + } + + fn active_layer(&mut self) -> &mut Layer { + self.active_stacking_context().active_layer() + } +} + +impl StackingContext { + fn new(clip_bounds: Option) -> Self { + Self { + layers: vec![Layer::new(clip_bounds)], + active_layer_stack: vec![0], + } + } + fn active_layer(&mut self) -> &mut Layer { &mut self.layers[*self.active_layer_stack.last().unwrap()] } + + fn push_layer(&mut self, clip_bounds: Option) { + let parent_clip_bounds = self.active_layer().clip_bounds(); + let clip_bounds = clip_bounds + .map(|clip_bounds| { + clip_bounds + .intersection(parent_clip_bounds.unwrap_or(clip_bounds)) + .unwrap_or_else(|| { + if !clip_bounds.is_empty() { + log::warn!("specified clip bounds are disjoint from parent layer"); + } + RectF::default() + }) + }) + .or(parent_clip_bounds); + + let ix = self.layers.len(); + self.layers.push(Layer::new(clip_bounds)); + self.active_layer_stack.push(ix); + } + + fn pop_layer(&mut self) { + self.active_layer_stack.pop().unwrap(); + assert!(!self.active_layer_stack.is_empty()); + } } impl Layer { @@ -176,6 +239,7 @@ impl Layer { Self { clip_bounds, quads: Vec::new(), + underlines: Vec::new(), shadows: Vec::new(), glyphs: Vec::new(), icons: Vec::new(), @@ -195,6 +259,14 @@ impl Layer { self.quads.as_slice() } + fn push_underline(&mut self, underline: Quad) { + self.underlines.push(underline); + } + + pub fn underlines(&self) -> &[Quad] { + self.underlines.as_slice() + } + fn push_shadow(&mut self, shadow: Shadow) { self.shadows.push(shadow); } diff --git a/zed/src/sum_tree.rs b/gpui/src/sum_tree.rs similarity index 94% rename from zed/src/sum_tree.rs rename to gpui/src/sum_tree.rs index eb13c2a86c5d19b5ee286e9113fb7787a01dd801..c250845b3e9f07d0c15f2a7810316c22fdf64e0f 100644 --- a/zed/src/sum_tree.rs +++ b/gpui/src/sum_tree.rs @@ -1,6 +1,5 @@ mod cursor; -use crate::util::Bias; use arrayvec::ArrayVec; pub use cursor::Cursor; pub use cursor::FilterCursor; @@ -11,7 +10,7 @@ const TREE_BASE: usize = 2; #[cfg(not(test))] const TREE_BASE: usize = 6; -pub trait Item: Clone + fmt::Debug { +pub trait Item: Clone { type Summary: Summary; fn summary(&self) -> Self::Summary; @@ -47,6 +46,29 @@ impl<'a, S: Summary, T: Dimension<'a, S> + Ord> SeekDimension<'a, S> for T { } } +#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] +pub enum Bias { + Left, + Right, +} + +impl PartialOrd for Bias { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Bias { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (Self::Left, Self::Left) => Ordering::Equal, + (Self::Left, Self::Right) => Ordering::Less, + (Self::Right, Self::Right) => Ordering::Equal, + (Self::Right, Self::Left) => Ordering::Greater, + } + } +} + #[derive(Debug, Clone)] pub struct SumTree(Arc>); @@ -65,6 +87,15 @@ impl SumTree { tree } + pub fn from_iter>( + iter: I, + cx: &::Context, + ) -> Self { + let mut tree = Self::new(); + tree.extend(iter, cx); + tree + } + #[allow(unused)] pub fn items(&self, cx: &::Context) -> Vec { let mut items = Vec::new(); @@ -253,8 +284,8 @@ impl SumTree { summary.add_summary(other_node.summary(), cx); let height_delta = *height - other_node.height(); - let mut summaries_to_append = ArrayVec::<[T::Summary; 2 * TREE_BASE]>::new(); - let mut trees_to_append = ArrayVec::<[SumTree; 2 * TREE_BASE]>::new(); + let mut summaries_to_append = ArrayVec::::new(); + let mut trees_to_append = ArrayVec::, { 2 * TREE_BASE }>::new(); if height_delta == 0 { summaries_to_append.extend(other_node.child_summaries().iter().cloned()); trees_to_append.extend(other_node.child_trees().iter().cloned()); @@ -277,8 +308,8 @@ impl SumTree { let child_count = child_trees.len() + trees_to_append.len(); if child_count > 2 * TREE_BASE { - let left_summaries: ArrayVec<_>; - let right_summaries: ArrayVec<_>; + let left_summaries: ArrayVec<_, { 2 * TREE_BASE }>; + let right_summaries: ArrayVec<_, { 2 * TREE_BASE }>; let left_trees; let right_trees; @@ -323,7 +354,7 @@ impl SumTree { let left_items; let right_items; let left_summaries; - let right_summaries: ArrayVec<[T::Summary; 2 * TREE_BASE]>; + let right_summaries: ArrayVec; let midpoint = (child_count + child_count % 2) / 2; { @@ -491,13 +522,13 @@ pub enum Node { Internal { height: u8, summary: T::Summary, - child_summaries: ArrayVec<[T::Summary; 2 * TREE_BASE]>, - child_trees: ArrayVec<[SumTree; 2 * TREE_BASE]>, + child_summaries: ArrayVec, + child_trees: ArrayVec, { 2 * TREE_BASE }>, }, Leaf { summary: T::Summary, - items: ArrayVec<[T; 2 * TREE_BASE]>, - item_summaries: ArrayVec<[T::Summary; 2 * TREE_BASE]>, + items: ArrayVec, + item_summaries: ArrayVec, }, } @@ -532,14 +563,14 @@ impl Node { } } - fn child_trees(&self) -> &ArrayVec<[SumTree; 2 * TREE_BASE]> { + fn child_trees(&self) -> &ArrayVec, { 2 * TREE_BASE }> { match self { Node::Internal { child_trees, .. } => child_trees, Node::Leaf { .. } => panic!("Leaf nodes have no child trees"), } } - fn items(&self) -> &ArrayVec<[T; 2 * TREE_BASE]> { + fn items(&self) -> &ArrayVec { match self { Node::Leaf { items, .. } => items, Node::Internal { .. } => panic!("Internal nodes have no items"), @@ -603,7 +634,7 @@ mod tests { ); } - #[gpui::test(iterations = 100)] + #[crate::test(self, iterations = 100)] fn test_random(mut rng: StdRng) { let rng = &mut rng; let mut tree = SumTree::::new(); diff --git a/zed/src/sum_tree/cursor.rs b/gpui/src/sum_tree/cursor.rs similarity index 95% rename from zed/src/sum_tree/cursor.rs rename to gpui/src/sum_tree/cursor.rs index 8a62cf4b489d59e0a33b474199d8daf18713e3ca..8b529ca5222c1a809dec74eb4c0c8e246b47f04f 100644 --- a/zed/src/sum_tree/cursor.rs +++ b/gpui/src/sum_tree/cursor.rs @@ -10,10 +10,26 @@ struct StackEntry<'a, T: Item, S, U> { sum_dimension: U, } +impl<'a, T, S, U> StackEntry<'a, T, S, U> +where + T: Item, + S: SeekDimension<'a, T::Summary>, + U: SeekDimension<'a, T::Summary>, +{ + fn swap_dimensions(self) -> StackEntry<'a, T, U, S> { + StackEntry { + tree: self.tree, + index: self.index, + seek_dimension: self.sum_dimension, + sum_dimension: self.seek_dimension, + } + } +} + #[derive(Clone)] pub struct Cursor<'a, T: Item, S, U> { tree: &'a SumTree, - stack: ArrayVec<[StackEntry<'a, T, S, U>; 16]>, + stack: ArrayVec, 16>, seek_dimension: S, sum_dimension: U, did_seek: bool, @@ -147,7 +163,6 @@ where None } - #[allow(unused)] pub fn prev(&mut self, cx: &::Context) { assert!(self.did_seek, "Must seek before calling this method"); @@ -495,8 +510,8 @@ where ref item_summaries, .. } => { - let mut slice_items = ArrayVec::<[T; 2 * TREE_BASE]>::new(); - let mut slice_item_summaries = ArrayVec::<[T::Summary; 2 * TREE_BASE]>::new(); + let mut slice_items = ArrayVec::::new(); + let mut slice_item_summaries = ArrayVec::::new(); let mut slice_items_summary = match aggregate { SeekAggregate::Slice(_) => Some(T::Summary::default()), _ => None, @@ -602,6 +617,28 @@ where } } +impl<'a, T, S, U> Cursor<'a, T, S, U> +where + T: Item, + S: SeekDimension<'a, T::Summary>, + U: SeekDimension<'a, T::Summary>, +{ + pub fn swap_dimensions(self) -> Cursor<'a, T, U, S> { + Cursor { + tree: self.tree, + stack: self + .stack + .into_iter() + .map(StackEntry::swap_dimensions) + .collect(), + seek_dimension: self.sum_dimension, + sum_dimension: self.seek_dimension, + did_seek: self.did_seek, + at_end: self.at_end, + } + } +} + pub struct FilterCursor<'a, F: Fn(&T::Summary) -> bool, T: Item, U> { cursor: Cursor<'a, T, (), U>, filter_node: F, diff --git a/gpui/src/test.rs b/gpui/src/test.rs index 721052980702f0245c17ae6c621894d9b0b2372f..4fabec46d7d61941dbfe9fd07f64654012613b06 100644 --- a/gpui/src/test.rs +++ b/gpui/src/test.rs @@ -2,5 +2,7 @@ use ctor::ctor; #[ctor] fn init_logger() { - env_logger::init(); + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .init(); } diff --git a/gpui/src/text_layout.rs b/gpui/src/text_layout.rs index 57556d61b6b25b25ee28bbda20893dd2c0c196b0..c74949b9981a90ba0554b883dcdd6a254c21adaf 100644 --- a/gpui/src/text_layout.rs +++ b/gpui/src/text_layout.rs @@ -5,7 +5,7 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, - platform, scene, PaintContext, + platform, scene, FontSystem, PaintContext, }; use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; @@ -14,6 +14,7 @@ use std::{ borrow::Borrow, collections::HashMap, hash::{Hash, Hasher}, + iter, sync::Arc, }; @@ -23,6 +24,13 @@ pub struct TextLayoutCache { fonts: Arc, } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct RunStyle { + pub color: Color, + pub font_id: FontId, + pub underline: bool, +} + impl TextLayoutCache { pub fn new(fonts: Arc) -> Self { Self { @@ -43,7 +51,7 @@ impl TextLayoutCache { &'a self, text: &'a str, font_size: f32, - runs: &'a [(usize, FontId, Color)], + runs: &'a [(usize, RunStyle)], ) -> Line { let key = &CacheKeyRef { text, @@ -94,7 +102,7 @@ impl<'a> Hash for (dyn CacheKey + 'a) { struct CacheKeyValue { text: String, font_size: OrderedFloat, - runs: SmallVec<[(usize, FontId, Color); 1]>, + runs: SmallVec<[(usize, RunStyle); 1]>, } impl CacheKey for CacheKeyValue { @@ -119,11 +127,11 @@ impl<'a> Borrow for CacheKeyValue { } } -#[derive(Copy, Clone, PartialEq, Eq, Hash)] +#[derive(Copy, Clone)] struct CacheKeyRef<'a> { text: &'a str, font_size: OrderedFloat, - runs: &'a [(usize, FontId, Color)], + runs: &'a [(usize, RunStyle)], } impl<'a> CacheKey for CacheKeyRef<'a> { @@ -132,10 +140,34 @@ impl<'a> CacheKey for CacheKeyRef<'a> { } } +impl<'a> PartialEq for CacheKeyRef<'a> { + fn eq(&self, other: &Self) -> bool { + self.text == other.text + && self.font_size == other.font_size + && self.runs.len() == other.runs.len() + && self.runs.iter().zip(other.runs.iter()).all( + |((len_a, style_a), (len_b, style_b))| { + len_a == len_b && style_a.font_id == style_b.font_id + }, + ) + } +} + +impl<'a> Hash for CacheKeyRef<'a> { + fn hash(&self, state: &mut H) { + self.text.hash(state); + self.font_size.hash(state); + for (len, style_id) in self.runs { + len.hash(state); + style_id.font_id.hash(state); + } + } +} + #[derive(Default, Debug)] pub struct Line { layout: Arc, - color_runs: SmallVec<[(u32, Color); 32]>, + style_runs: SmallVec<[(u32, Color, bool); 32]>, } #[derive(Default, Debug)] @@ -162,12 +194,16 @@ pub struct Glyph { } impl Line { - fn new(layout: Arc, runs: &[(usize, FontId, Color)]) -> Self { - let mut color_runs = SmallVec::new(); - for (len, _, color) in runs { - color_runs.push((*len as u32, *color)); + fn new(layout: Arc, runs: &[(usize, RunStyle)]) -> Self { + let mut style_runs = SmallVec::new(); + for (len, style) in runs { + style_runs.push((*len as u32, style.color, style.underline)); } - Self { layout, color_runs } + Self { layout, style_runs } + } + + pub fn runs(&self) -> &[Run] { + &self.layout.runs } pub fn width(&self) -> f32 { @@ -200,13 +236,20 @@ impl Line { } } - pub fn paint(&self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext) { - let padding_top = (visible_bounds.height() - self.layout.ascent - self.layout.descent) / 2.; - let baseline_origin = vec2f(0., padding_top + self.layout.ascent); + pub fn paint( + &self, + origin: Vector2F, + visible_bounds: RectF, + line_height: f32, + cx: &mut PaintContext, + ) { + let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.; + let baseline_offset = vec2f(0., padding_top + self.layout.ascent); - let mut color_runs = self.color_runs.iter(); - let mut color_end = 0; + let mut style_runs = self.style_runs.iter(); + let mut run_end = 0; let mut color = Color::black(); + let mut underline_start = None; for run in &self.layout.runs { let max_glyph_width = cx @@ -215,7 +258,7 @@ impl Line { .x(); for glyph in &run.glyphs { - let glyph_origin = baseline_origin + glyph.position; + let glyph_origin = origin + baseline_offset + glyph.position; if glyph_origin.x() + max_glyph_width < visible_bounds.origin().x() { continue; @@ -224,13 +267,43 @@ impl Line { break; } - if glyph.index >= color_end { - if let Some(next_run) = color_runs.next() { - color_end += next_run.0 as usize; - color = next_run.1; + if glyph.index >= run_end { + if let Some((run_len, run_color, run_underlined)) = style_runs.next() { + if let Some(underline_origin) = underline_start { + if !*run_underlined || *run_color != color { + cx.scene.push_underline(scene::Quad { + bounds: RectF::from_points( + underline_origin, + glyph_origin + vec2f(0., 1.), + ), + background: Some(color), + border: Default::default(), + corner_radius: 0., + }); + underline_start = None; + } + } + + if *run_underlined { + underline_start.get_or_insert(glyph_origin); + } + + run_end += *run_len as usize; + color = *run_color; } else { - color_end = self.layout.len; + run_end = self.layout.len; color = Color::black(); + if let Some(underline_origin) = underline_start.take() { + cx.scene.push_underline(scene::Quad { + bounds: RectF::from_points( + underline_origin, + glyph_origin + vec2f(0., 1.), + ), + background: Some(color), + border: Default::default(), + corner_radius: 0., + }); + } } } @@ -238,10 +311,417 @@ impl Line { font_id: run.font_id, font_size: self.layout.font_size, id: glyph.id, - origin: origin + glyph_origin, + origin: glyph_origin, color, }); } } + + if let Some(underline_start) = underline_start.take() { + let line_end = origin + baseline_offset + vec2f(self.layout.width, 0.); + + cx.scene.push_underline(scene::Quad { + bounds: RectF::from_points(underline_start, line_end + vec2f(0., 1.)), + background: Some(color), + border: Default::default(), + corner_radius: 0., + }); + } + } + + pub fn paint_wrapped( + &self, + origin: Vector2F, + visible_bounds: RectF, + line_height: f32, + boundaries: impl IntoIterator, + cx: &mut PaintContext, + ) { + let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.; + let baseline_origin = vec2f(0., padding_top + self.layout.ascent); + + let mut boundaries = boundaries.into_iter().peekable(); + let mut color_runs = self.style_runs.iter(); + let mut color_end = 0; + let mut color = Color::black(); + + let mut glyph_origin = vec2f(0., 0.); + let mut prev_position = 0.; + for run in &self.layout.runs { + for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { + if boundaries.peek().map_or(false, |b| b.glyph_ix == glyph_ix) { + boundaries.next(); + glyph_origin = vec2f(0., glyph_origin.y() + line_height); + } else { + glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position); + } + prev_position = glyph.position.x(); + + if glyph.index >= color_end { + if let Some(next_run) = color_runs.next() { + color_end += next_run.0 as usize; + color = next_run.1; + } else { + color_end = self.layout.len; + color = Color::black(); + } + } + + let glyph_bounds = RectF::new( + origin + glyph_origin, + cx.font_cache + .bounding_box(run.font_id, self.layout.font_size), + ); + if glyph_bounds.intersects(visible_bounds) { + cx.scene.push_glyph(scene::Glyph { + font_id: run.font_id, + font_size: self.layout.font_size, + id: glyph.id, + origin: glyph_bounds.origin() + baseline_origin, + color, + }); + } + } + } + } +} + +impl Run { + pub fn glyphs(&self) -> &[Glyph] { + &self.glyphs + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Boundary { + pub ix: usize, + pub next_indent: u32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct ShapedBoundary { + pub run_ix: usize, + pub glyph_ix: usize, +} + +impl Boundary { + fn new(ix: usize, next_indent: u32) -> Self { + Self { ix, next_indent } + } +} + +pub struct LineWrapper { + font_system: Arc, + pub(crate) font_id: FontId, + pub(crate) font_size: f32, + cached_ascii_char_widths: [f32; 128], + cached_other_char_widths: HashMap, +} + +impl LineWrapper { + pub const MAX_INDENT: u32 = 256; + + pub fn new(font_id: FontId, font_size: f32, font_system: Arc) -> Self { + Self { + font_system, + font_id, + font_size, + cached_ascii_char_widths: [f32::NAN; 128], + cached_other_char_widths: HashMap::new(), + } + } + + pub fn wrap_line<'a>( + &'a mut self, + line: &'a str, + wrap_width: f32, + ) -> impl Iterator + 'a { + let mut width = 0.0; + let mut first_non_whitespace_ix = None; + let mut indent = None; + let mut last_candidate_ix = 0; + let mut last_candidate_width = 0.0; + let mut last_wrap_ix = 0; + let mut prev_c = '\0'; + let mut char_indices = line.char_indices(); + iter::from_fn(move || { + while let Some((ix, c)) = char_indices.next() { + if c == '\n' { + continue; + } + + if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() { + last_candidate_ix = ix; + last_candidate_width = width; + } + + if c != ' ' && first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(ix); + } + + let char_width = self.width_for_char(c); + width += char_width; + if width > wrap_width && ix > last_wrap_ix { + if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix) + { + indent = Some( + Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32), + ); + } + + if last_candidate_ix > 0 { + last_wrap_ix = last_candidate_ix; + width -= last_candidate_width; + last_candidate_ix = 0; + } else { + last_wrap_ix = ix; + width = char_width; + } + + let indent_width = + indent.map(|indent| indent as f32 * self.width_for_char(' ')); + width += indent_width.unwrap_or(0.); + + return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0))); + } + prev_c = c; + } + + None + }) + } + + pub fn wrap_shaped_line<'a>( + &'a mut self, + str: &'a str, + line: &'a Line, + wrap_width: f32, + ) -> impl Iterator + 'a { + let mut first_non_whitespace_ix = None; + let mut last_candidate_ix = None; + let mut last_candidate_x = 0.0; + let mut last_wrap_ix = ShapedBoundary { + run_ix: 0, + glyph_ix: 0, + }; + let mut last_wrap_x = 0.; + let mut prev_c = '\0'; + let mut glyphs = line + .runs() + .iter() + .enumerate() + .flat_map(move |(run_ix, run)| { + run.glyphs() + .iter() + .enumerate() + .map(move |(glyph_ix, glyph)| { + let character = str[glyph.index..].chars().next().unwrap(); + ( + ShapedBoundary { run_ix, glyph_ix }, + character, + glyph.position.x(), + ) + }) + }) + .peekable(); + + iter::from_fn(move || { + while let Some((ix, c, x)) = glyphs.next() { + if c == '\n' { + continue; + } + + if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() { + last_candidate_ix = Some(ix); + last_candidate_x = x; + } + + if c != ' ' && first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(ix); + } + + let next_x = glyphs.peek().map_or(line.width(), |(_, _, x)| *x); + let width = next_x - last_wrap_x; + if width > wrap_width && ix > last_wrap_ix { + if let Some(last_candidate_ix) = last_candidate_ix.take() { + last_wrap_ix = last_candidate_ix; + last_wrap_x = last_candidate_x; + } else { + last_wrap_ix = ix; + last_wrap_x = x; + } + + return Some(last_wrap_ix); + } + prev_c = c; + } + + None + }) + } + + fn is_boundary(&self, prev: char, next: char) -> bool { + (prev == ' ') && (next != ' ') + } + + #[inline(always)] + fn width_for_char(&mut self, c: char) -> f32 { + if (c as u32) < 128 { + let mut width = self.cached_ascii_char_widths[c as usize]; + if width.is_nan() { + width = self.compute_width_for_char(c); + self.cached_ascii_char_widths[c as usize] = width; + } + width + } else { + let mut width = self + .cached_other_char_widths + .get(&c) + .copied() + .unwrap_or(f32::NAN); + if width.is_nan() { + width = self.compute_width_for_char(c); + self.cached_other_char_widths.insert(c, width); + } + width + } + } + + fn compute_width_for_char(&self, c: char) -> f32 { + self.font_system + .layout_line( + &c.to_string(), + self.font_size, + &[( + 1, + RunStyle { + font_id: self.font_id, + color: Default::default(), + underline: false, + }, + )], + ) + .width + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fonts::{Properties, Weight}; + + #[crate::test(self)] + fn test_wrap_line(cx: &mut crate::MutableAppContext) { + let font_cache = cx.font_cache().clone(); + let font_system = cx.platform().fonts(); + let family = font_cache.load_family(&["Courier"]).unwrap(); + let font_id = font_cache.select_font(family, &Default::default()).unwrap(); + + let mut wrapper = LineWrapper::new(font_id, 16., font_system); + assert_eq!( + wrapper + .wrap_line("aa bbb cccc ddddd eeee", 72.0) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(12, 0), + Boundary::new(18, 0) + ], + ); + assert_eq!( + wrapper + .wrap_line("aaa aaaaaaaaaaaaaaaaaa", 72.0) + .collect::>(), + &[ + Boundary::new(4, 0), + Boundary::new(11, 0), + Boundary::new(18, 0) + ], + ); + assert_eq!( + wrapper.wrap_line(" aaaaaaa", 72.).collect::>(), + &[ + Boundary::new(7, 5), + Boundary::new(9, 5), + Boundary::new(11, 5), + ] + ); + assert_eq!( + wrapper + .wrap_line(" ", 72.) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(14, 0), + Boundary::new(21, 0) + ] + ); + assert_eq!( + wrapper + .wrap_line(" aaaaaaaaaaaaaa", 72.) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(14, 3), + Boundary::new(18, 3), + Boundary::new(22, 3), + ] + ); + } + + #[crate::test(self)] + fn test_wrap_shaped_line(cx: &mut crate::MutableAppContext) { + let font_cache = cx.font_cache().clone(); + let font_system = cx.platform().fonts(); + let text_layout_cache = TextLayoutCache::new(font_system.clone()); + + let family = font_cache.load_family(&["Helvetica"]).unwrap(); + let font_id = font_cache.select_font(family, &Default::default()).unwrap(); + let normal = RunStyle { + font_id, + color: Default::default(), + underline: false, + }; + let bold = RunStyle { + font_id: font_cache + .select_font( + family, + &Properties { + weight: Weight::BOLD, + ..Default::default() + }, + ) + .unwrap(), + color: Default::default(), + underline: false, + }; + + let text = "aa bbb cccc ddddd eeee"; + let line = text_layout_cache.layout_str( + text, + 16.0, + &[(4, normal), (5, bold), (6, normal), (1, bold), (7, normal)], + ); + + let mut wrapper = LineWrapper::new(font_id, 16., font_system); + assert_eq!( + wrapper + .wrap_shaped_line(&text, &line, 72.0) + .collect::>(), + &[ + ShapedBoundary { + run_ix: 1, + glyph_ix: 3 + }, + ShapedBoundary { + run_ix: 2, + glyph_ix: 3 + }, + ShapedBoundary { + run_ix: 4, + glyph_ix: 2 + } + ], + ); } } diff --git a/gpui/src/views.rs b/gpui/src/views.rs new file mode 100644 index 0000000000000000000000000000000000000000..73f8a5751830f6d4f09044883c31d50ffb346b78 --- /dev/null +++ b/gpui/src/views.rs @@ -0,0 +1,7 @@ +mod select; + +pub use select::{ItemType, Select, SelectStyle}; + +pub fn init(cx: &mut super::MutableAppContext) { + select::init(cx); +} diff --git a/gpui/src/views/select.rs b/gpui/src/views/select.rs new file mode 100644 index 0000000000000000000000000000000000000000..b9e099a75c17f954a294f3150c6e264ea612ad17 --- /dev/null +++ b/gpui/src/views/select.rs @@ -0,0 +1,169 @@ +use crate::{ + action, elements::*, AppContext, Entity, MutableAppContext, RenderContext, View, ViewContext, + WeakViewHandle, +}; + +pub struct Select { + handle: WeakViewHandle, + render_item: Box ElementBox>, + selected_item_ix: usize, + item_count: usize, + is_open: bool, + list_state: UniformListState, + build_style: Option SelectStyle>>, +} + +#[derive(Clone, Default)] +pub struct SelectStyle { + pub header: ContainerStyle, + pub menu: ContainerStyle, +} + +pub enum ItemType { + Header, + Selected, + Unselected, +} + +action!(ToggleSelect); +action!(SelectItem, usize); + +pub enum Event {} + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(Select::toggle); + cx.add_action(Select::select_item); +} + +impl Select { + pub fn new ElementBox>( + item_count: usize, + cx: &mut ViewContext, + render_item: F, + ) -> Self { + Self { + handle: cx.handle().downgrade(), + render_item: Box::new(render_item), + selected_item_ix: 0, + item_count, + is_open: false, + list_state: UniformListState::default(), + build_style: Default::default(), + } + } + + pub fn with_style( + mut self, + f: impl 'static + FnMut(&mut MutableAppContext) -> SelectStyle, + ) -> Self { + self.build_style = Some(Box::new(f)); + self + } + + pub fn set_item_count(&mut self, count: usize, cx: &mut ViewContext) { + self.item_count = count; + cx.notify(); + } + + fn toggle(&mut self, _: &ToggleSelect, cx: &mut ViewContext) { + self.is_open = !self.is_open; + cx.notify(); + } + + fn select_item(&mut self, action: &SelectItem, cx: &mut ViewContext) { + self.selected_item_ix = action.0; + self.is_open = false; + cx.notify(); + } + + pub fn selected_index(&self) -> usize { + self.selected_item_ix + } +} + +impl Entity for Select { + type Event = Event; +} + +impl View for Select { + fn ui_name() -> &'static str { + "Select" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + if self.item_count == 0 { + return Empty::new().boxed(); + } + + enum Header {} + enum Item {} + + let style = if let Some(build_style) = self.build_style.as_mut() { + (build_style)(cx) + } else { + Default::default() + }; + let mut result = Flex::column().with_child( + MouseEventHandler::new::(self.handle.id(), cx, |mouse_state, cx| { + Container::new((self.render_item)( + self.selected_item_ix, + ItemType::Header, + mouse_state.hovered, + cx, + )) + .with_style(&style.header) + .boxed() + }) + .on_click(move |cx| cx.dispatch_action(ToggleSelect)) + .boxed(), + ); + if self.is_open { + let handle = self.handle.clone(); + result.add_child( + Overlay::new( + Container::new( + ConstrainedBox::new( + UniformList::new( + self.list_state.clone(), + self.item_count, + move |mut range, items, mut cx| { + let handle = handle.upgrade(cx).unwrap(); + let this = handle.read(cx); + let selected_item_ix = this.selected_item_ix; + range.end = range.end.min(this.item_count); + items.extend(range.map(|ix| { + MouseEventHandler::new::( + (handle.id(), ix), + &mut cx, + |mouse_state, cx| { + (handle.read(*cx).render_item)( + ix, + if ix == selected_item_ix { + ItemType::Selected + } else { + ItemType::Unselected + }, + mouse_state.hovered, + cx, + ) + }, + ) + .on_click(move |cx| cx.dispatch_action(SelectItem(ix))) + .boxed() + })) + }, + ) + .boxed(), + ) + .with_max_height(200.) + .boxed(), + ) + .with_style(&style.menu) + .boxed(), + ) + .boxed(), + ) + } + result.boxed() + } +} diff --git a/script/seed-db b/script/seed-db new file mode 100755 index 0000000000000000000000000000000000000000..195dc5fb8d49c40bb6abf8306def6211111ac56f --- /dev/null +++ b/script/seed-db @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e +cd server +cargo run --features seed-support --bin seed diff --git a/script/sqlx b/script/sqlx index 080e0d843a213dc1ac4e5dbddf5c89df8ff4c086..590aad67ebeb79734884d70096a42688b8d0555d 100755 --- a/script/sqlx +++ b/script/sqlx @@ -3,7 +3,9 @@ set -e # Install sqlx-cli if needed -[[ "$(sqlx --version)" == "sqlx-cli 0.5.5" ]] || cargo install sqlx-cli --version 0.5.5 +[[ "$(sqlx --version)" == "sqlx-cli 0.5.7" ]] || cargo install sqlx-cli --version 0.5.7 + +cd server # Export contents of .env.toml eval "$(cargo run --bin dotenv)" diff --git a/server/Cargo.toml b/server/Cargo.toml index 6d26f66054912768dc708b1d56ccbe8a25791614..b73c70102a311ecf1813a5bd85efa315e344dd93 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -5,6 +5,10 @@ edition = "2018" name = "zed-server" version = "0.1.0" +[[bin]] +name = "seed" +required-features = ["seed-support"] + [dependencies] anyhow = "1.0.40" async-std = { version = "1.8.0", features = ["attributes"] } @@ -19,18 +23,20 @@ futures = "0.3" handlebars = "3.5" http-auth-basic = "0.1.3" jwt-simple = "0.10.0" +lipsum = { version = "0.8", optional = true } oauth2 = { version = "4.0.0", default_features = false } oauth2-surf = "0.1.1" parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } rand = "0.8" -rust-embed = "5.9.0" +rust-embed = { version = "6.2", features = ["include-exclude"] } scrypt = "0.7" serde = { version = "1.0", features = ["derive"] } sha-1 = "0.9" surf = "2.2.0" tide = "0.16.0" tide-compress = "0.9.0" +time = "0.2" toml = "0.5.8" zrpc = { path = "../zrpc" } @@ -41,10 +47,13 @@ default-features = false [dependencies.sqlx] version = "0.5.2" -features = ["runtime-async-std-rustls", "postgres"] +features = ["runtime-async-std-rustls", "postgres", "time"] [dev-dependencies] gpui = { path = "../gpui" } -zed = { path = "../zed", features = ["test-support"] } lazy_static = "1.4" serde_json = { version = "1.0.64", features = ["preserve_order"] } +zed = { path = "../zed", features = ["test-support"] } + +[features] +seed-support = ["lipsum"] diff --git a/server/migrations/20210805175147_create_chat_tables.sql b/server/migrations/20210805175147_create_chat_tables.sql new file mode 100644 index 0000000000000000000000000000000000000000..5bba4689d9c21e65d989cf05e2e1eedb0151621d --- /dev/null +++ b/server/migrations/20210805175147_create_chat_tables.sql @@ -0,0 +1,46 @@ +CREATE TABLE IF NOT EXISTS "orgs" ( + "id" SERIAL PRIMARY KEY, + "name" VARCHAR NOT NULL, + "slug" VARCHAR NOT NULL +); + +CREATE UNIQUE INDEX "index_orgs_slug" ON "orgs" ("slug"); + +CREATE TABLE IF NOT EXISTS "org_memberships" ( + "id" SERIAL PRIMARY KEY, + "org_id" INTEGER REFERENCES orgs (id) NOT NULL, + "user_id" INTEGER REFERENCES users (id) NOT NULL, + "admin" BOOLEAN NOT NULL +); + +CREATE INDEX "index_org_memberships_user_id" ON "org_memberships" ("user_id"); +CREATE UNIQUE INDEX "index_org_memberships_org_id_and_user_id" ON "org_memberships" ("org_id", "user_id"); + +CREATE TABLE IF NOT EXISTS "channels" ( + "id" SERIAL PRIMARY KEY, + "owner_id" INTEGER NOT NULL, + "owner_is_user" BOOLEAN NOT NULL, + "name" VARCHAR NOT NULL +); + +CREATE UNIQUE INDEX "index_channels_owner_and_name" ON "channels" ("owner_is_user", "owner_id", "name"); + +CREATE TABLE IF NOT EXISTS "channel_memberships" ( + "id" SERIAL PRIMARY KEY, + "channel_id" INTEGER REFERENCES channels (id) NOT NULL, + "user_id" INTEGER REFERENCES users (id) NOT NULL, + "admin" BOOLEAN NOT NULL +); + +CREATE INDEX "index_channel_memberships_user_id" ON "channel_memberships" ("user_id"); +CREATE UNIQUE INDEX "index_channel_memberships_channel_id_and_user_id" ON "channel_memberships" ("channel_id", "user_id"); + +CREATE TABLE IF NOT EXISTS "channel_messages" ( + "id" SERIAL PRIMARY KEY, + "channel_id" INTEGER REFERENCES channels (id) NOT NULL, + "sender_id" INTEGER REFERENCES users (id) NOT NULL, + "body" TEXT NOT NULL, + "sent_at" TIMESTAMP +); + +CREATE INDEX "index_channel_messages_channel_id" ON "channel_messages" ("channel_id"); diff --git a/server/src/admin.rs b/server/src/admin.rs index 3f379ff56f9a1e2f2c5d34d41b20a24dfa683c7a..47b29b5d0294168be720749e94e4f8ed838b802e 100644 --- a/server/src/admin.rs +++ b/server/src/admin.rs @@ -1,7 +1,6 @@ -use crate::{auth::RequestExt as _, AppState, DbPool, LayoutData, Request, RequestExt as _}; +use crate::{auth::RequestExt as _, db, AppState, LayoutData, Request, RequestExt as _}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; -use sqlx::{Executor, FromRow}; use std::sync::Arc; use surf::http::mime; @@ -41,23 +40,8 @@ pub fn add_routes(app: &mut tide::Server>) { struct AdminData { #[serde(flatten)] layout: Arc, - users: Vec, - signups: Vec, -} - -#[derive(Debug, FromRow, Serialize)] -pub struct User { - pub id: i32, - pub github_login: String, - pub admin: bool, -} - -#[derive(Debug, FromRow, Serialize)] -pub struct Signup { - pub id: i32, - pub github_login: String, - pub email_address: String, - pub about: String, + users: Vec, + signups: Vec, } async fn get_admin_page(mut request: Request) -> tide::Result { @@ -65,12 +49,8 @@ async fn get_admin_page(mut request: Request) -> tide::Result { let data = AdminData { layout: request.layout_data().await?, - users: sqlx::query_as("SELECT * FROM users ORDER BY github_login ASC") - .fetch_all(request.db()) - .await?, - signups: sqlx::query_as("SELECT * FROM signups ORDER BY id DESC") - .fetch_all(request.db()) - .await?, + users: request.db().get_all_users().await?, + signups: request.db().get_all_signups().await?, }; Ok(tide::Response::builder(200) @@ -96,7 +76,7 @@ async fn post_user(mut request: Request) -> tide::Result { .unwrap_or(&form.github_login); if !github_login.is_empty() { - create_user(request.db(), github_login, form.admin).await?; + request.db().create_user(github_login, form.admin).await?; } Ok(tide::Redirect::new("/admin").into()) @@ -105,7 +85,7 @@ async fn post_user(mut request: Request) -> tide::Result { async fn put_user(mut request: Request) -> tide::Result { request.require_admin().await?; - let user_id = request.param("id")?.parse::()?; + let user_id = request.param("id")?.parse()?; #[derive(Deserialize)] struct Body { @@ -116,11 +96,7 @@ async fn put_user(mut request: Request) -> tide::Result { request .db() - .execute( - sqlx::query("UPDATE users SET admin = $1 WHERE id = $2;") - .bind(body.admin) - .bind(user_id), - ) + .set_user_is_admin(db::UserId(user_id), body.admin) .await?; Ok(tide::Response::builder(200).build()) @@ -128,33 +104,14 @@ async fn put_user(mut request: Request) -> tide::Result { async fn delete_user(request: Request) -> tide::Result { request.require_admin().await?; - - let user_id = request.param("id")?.parse::()?; - request - .db() - .execute(sqlx::query("DELETE FROM users WHERE id = $1;").bind(user_id)) - .await?; - + let user_id = db::UserId(request.param("id")?.parse()?); + request.db().delete_user(user_id).await?; Ok(tide::Redirect::new("/admin").into()) } -pub async fn create_user(db: &DbPool, github_login: &str, admin: bool) -> tide::Result { - let id: i32 = - sqlx::query_scalar("INSERT INTO users (github_login, admin) VALUES ($1, $2) RETURNING id;") - .bind(github_login) - .bind(admin) - .fetch_one(db) - .await?; - Ok(id) -} - async fn delete_signup(request: Request) -> tide::Result { request.require_admin().await?; - let signup_id = request.param("id")?.parse::()?; - request - .db() - .execute(sqlx::query("DELETE FROM signups WHERE id = $1;").bind(signup_id)) - .await?; - + let signup_id = db::SignupId(request.param("id")?.parse()?); + request.db().delete_signup(signup_id).await?; Ok(tide::Redirect::new("/admin").into()) } diff --git a/server/src/assets.rs b/server/src/assets.rs index a53be8ed95f23d9f0666cf6a2b24faecb47fc2e6..4e79af8c600d2b2766b5dc6170aaec38d37ba006 100644 --- a/server/src/assets.rs +++ b/server/src/assets.rs @@ -26,6 +26,6 @@ async fn get_static_asset(request: Request) -> tide::Result { Ok(tide::Response::builder(200) .content_type(content_type) - .body(content.as_ref()) + .body(content.data.as_ref()) .build()) } diff --git a/server/src/auth.rs b/server/src/auth.rs index 4a7107e550c2adc4838a703308554c93ea21464d..5a3e301d27537a1e031d804341af29d071afdd95 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -1,7 +1,9 @@ -use super::errors::TideResultExt; -use crate::{github, rpc, AppState, DbPool, Request, RequestExt as _}; +use super::{ + db::{self, UserId}, + errors::TideResultExt, +}; +use crate::{github, AppState, Request, RequestExt as _}; use anyhow::{anyhow, Context}; -use async_std::stream::StreamExt; use async_trait::async_trait; pub use oauth2::basic::BasicClient as Client; use oauth2::{ @@ -14,11 +16,10 @@ use scrypt::{ Scrypt, }; use serde::{Deserialize, Serialize}; -use sqlx::FromRow; use std::{borrow::Cow, convert::TryFrom, sync::Arc}; use surf::Url; use tide::Server; -use zrpc::{auth as zed_auth, proto, Peer}; +use zrpc::auth as zed_auth; static CURRENT_GITHUB_USER: &'static str = "current_github_user"; static GITHUB_AUTH_URL: &'static str = "https://github.com/login/oauth/authorize"; @@ -34,9 +35,6 @@ pub struct User { pub struct VerifyToken; -#[derive(Clone, Copy)] -pub struct UserId(pub i32); - #[async_trait] impl tide::Middleware> for VerifyToken { async fn handle( @@ -51,33 +49,28 @@ impl tide::Middleware> for VerifyToken { .as_str() .split_whitespace(); - let user_id: i32 = auth_header - .next() - .ok_or_else(|| anyhow!("missing user id in authorization header"))? - .parse()?; + let user_id = UserId( + auth_header + .next() + .ok_or_else(|| anyhow!("missing user id in authorization header"))? + .parse()?, + ); let access_token = auth_header .next() .ok_or_else(|| anyhow!("missing access token in authorization header"))?; let state = request.state().clone(); - let mut password_hashes = - sqlx::query_scalar::<_, String>("SELECT hash FROM access_tokens WHERE user_id = $1") - .bind(&user_id) - .fetch_many(&state.db); - let mut credentials_valid = false; - while let Some(password_hash) = password_hashes.next().await { - if let either::Either::Right(password_hash) = password_hash? { - if verify_access_token(&access_token, &password_hash)? { - credentials_valid = true; - break; - } + for password_hash in state.db.get_access_token_hashes(user_id).await? { + if verify_access_token(&access_token, &password_hash)? { + credentials_valid = true; + break; } } if credentials_valid { - request.set_ext(UserId(user_id)); + request.set_ext(user_id); Ok(next.run(request).await) } else { Err(anyhow!("invalid credentials").into()) @@ -94,25 +87,12 @@ pub trait RequestExt { impl RequestExt for Request { async fn current_user(&self) -> tide::Result> { if let Some(details) = self.session().get::(CURRENT_GITHUB_USER) { - #[derive(FromRow)] - struct UserRow { - admin: bool, - } - - let user_row: Option = - sqlx::query_as("SELECT admin FROM users WHERE github_login = $1") - .bind(&details.login) - .fetch_optional(self.db()) - .await?; - - let is_insider = user_row.is_some(); - let is_admin = user_row.map_or(false, |row| row.admin); - + let user = self.db().get_user_by_github_login(&details.login).await?; Ok(Some(User { github_login: details.login, avatar_url: details.avatar_url, - is_insider, - is_admin, + is_insider: user.is_some(), + is_admin: user.map_or(false, |user| user.admin), })) } else { Ok(None) @@ -120,43 +100,6 @@ impl RequestExt for Request { } } -#[async_trait] -pub trait PeerExt { - async fn sign_out( - self: &Arc, - connection_id: zrpc::ConnectionId, - state: &AppState, - ) -> tide::Result<()>; -} - -#[async_trait] -impl PeerExt for Peer { - async fn sign_out( - self: &Arc, - connection_id: zrpc::ConnectionId, - state: &AppState, - ) -> tide::Result<()> { - self.disconnect(connection_id).await; - let worktree_ids = state.rpc.write().await.remove_connection(connection_id); - for worktree_id in worktree_ids { - let state = state.rpc.read().await; - if let Some(worktree) = state.worktrees.get(&worktree_id) { - rpc::broadcast(connection_id, worktree.connection_ids(), |conn_id| { - self.send( - conn_id, - proto::RemovePeer { - worktree_id, - peer_id: connection_id.0, - }, - ) - }) - .await?; - } - } - Ok(()) - } -} - pub fn build_client(client_id: &str, client_secret: &str) -> Client { Client::new( ClientId::new(client_id.to_string()), @@ -265,9 +208,9 @@ async fn get_auth_callback(mut request: Request) -> tide::Result { .await .context("failed to fetch user")?; - let user_id: Option = sqlx::query_scalar("SELECT id from users where github_login = $1") - .bind(&user_details.login) - .fetch_optional(request.db()) + let user = request + .db() + .get_user_by_github_login(&user_details.login) .await?; request @@ -276,8 +219,8 @@ async fn get_auth_callback(mut request: Request) -> tide::Result { // When signing in from the native app, generate a new access token for the current user. Return // a redirect so that the user's browser sends this access token to the locally-running app. - if let Some((user_id, app_sign_in_params)) = user_id.zip(query.native_app_sign_in_params) { - let access_token = create_access_token(request.db(), user_id).await?; + if let Some((user, app_sign_in_params)) = user.zip(query.native_app_sign_in_params) { + let access_token = create_access_token(request.db(), user.id).await?; let native_app_public_key = zed_auth::PublicKey::try_from(app_sign_in_params.native_app_public_key.clone()) .context("failed to parse app public key")?; @@ -287,7 +230,7 @@ async fn get_auth_callback(mut request: Request) -> tide::Result { return Ok(tide::Redirect::new(&format!( "http://127.0.0.1:{}?user_id={}&access_token={}", - app_sign_in_params.native_app_port, user_id, encrypted_access_token, + app_sign_in_params.native_app_port, user.id.0, encrypted_access_token, )) .into()); } @@ -300,14 +243,11 @@ async fn post_sign_out(mut request: Request) -> tide::Result { Ok(tide::Redirect::new("/").into()) } -pub async fn create_access_token(db: &DbPool, user_id: i32) -> tide::Result { +pub async fn create_access_token(db: &db::Db, user_id: UserId) -> tide::Result { let access_token = zed_auth::random_token(); let access_token_hash = hash_access_token(&access_token).context("failed to hash access token")?; - sqlx::query("INSERT INTO access_tokens (user_id, hash) values ($1, $2)") - .bind(user_id) - .bind(access_token_hash) - .fetch_optional(db) + db.create_access_token_hash(user_id, access_token_hash) .await?; Ok(access_token) } diff --git a/server/src/bin/seed.rs b/server/src/bin/seed.rs new file mode 100644 index 0000000000000000000000000000000000000000..b259dc4c14b24ea8b1278be56a6610f2e5fa1f64 --- /dev/null +++ b/server/src/bin/seed.rs @@ -0,0 +1,91 @@ +use db::{Db, UserId}; +use rand::prelude::*; +use tide::log; +use time::{Duration, OffsetDateTime}; + +#[allow(unused)] +#[path = "../db.rs"] +mod db; +#[path = "../env.rs"] +mod env; + +#[async_std::main] +async fn main() { + if let Err(error) = env::load_dotenv() { + log::error!( + "error loading .env.toml (this is expected in production): {}", + error + ); + } + + let mut rng = StdRng::from_entropy(); + let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var"); + let db = Db::new(&database_url, 5) + .await + .expect("failed to connect to postgres database"); + + let zed_users = ["nathansobo", "maxbrunsfeld", "as-cii", "iamnbutler"]; + let mut zed_user_ids = Vec::::new(); + for zed_user in zed_users { + if let Some(user_id) = db.get_user(zed_user).await.expect("failed to fetch user") { + zed_user_ids.push(user_id); + } else { + zed_user_ids.push( + db.create_user(zed_user, true) + .await + .expect("failed to insert user"), + ); + } + } + + let zed_org_id = if let Some(org) = db + .find_org_by_slug("zed") + .await + .expect("failed to fetch org") + { + org.id + } else { + db.create_org("Zed", "zed") + .await + .expect("failed to insert org") + }; + + let general_channel_id = if let Some(channel) = db + .get_org_channels(zed_org_id) + .await + .expect("failed to fetch channels") + .iter() + .find(|c| c.name == "General") + { + channel.id + } else { + let channel_id = db + .create_org_channel(zed_org_id, "General") + .await + .expect("failed to insert channel"); + + let now = OffsetDateTime::now_utc(); + let max_seconds = Duration::days(100).as_seconds_f64(); + let mut timestamps = (0..1000) + .map(|_| now - Duration::seconds_f64(rng.gen_range(0_f64..=max_seconds))) + .collect::>(); + timestamps.sort(); + for timestamp in timestamps { + let sender_id = *zed_user_ids.choose(&mut rng).unwrap(); + let body = lipsum::lipsum_words(rng.gen_range(1..=50)); + db.create_channel_message(channel_id, sender_id, &body, timestamp) + .await + .expect("failed to insert message"); + } + channel_id + }; + + for user_id in zed_user_ids { + db.add_org_member(zed_org_id, user_id, true) + .await + .expect("failed to insert org membership"); + db.add_channel_member(general_channel_id, user_id, true) + .await + .expect("failed to insert channel membership"); + } +} diff --git a/server/src/db.rs b/server/src/db.rs new file mode 100644 index 0000000000000000000000000000000000000000..62797f6b81b414410a0ca238545e01b47716da4e --- /dev/null +++ b/server/src/db.rs @@ -0,0 +1,594 @@ +use anyhow::Context; +use async_std::task::{block_on, yield_now}; +use serde::Serialize; +use sqlx::{FromRow, Result}; +use time::OffsetDateTime; + +pub use async_sqlx_session::PostgresSessionStore as SessionStore; +pub use sqlx::postgres::PgPoolOptions as DbOptions; + +macro_rules! test_support { + ($self:ident, { $($token:tt)* }) => {{ + let body = async { + $($token)* + }; + if $self.test_mode { + yield_now().await; + block_on(body) + } else { + body.await + } + }}; +} + +#[derive(Clone)] +pub struct Db { + pool: sqlx::PgPool, + test_mode: bool, +} + +impl Db { + pub async fn new(url: &str, max_connections: u32) -> tide::Result { + let pool = DbOptions::new() + .max_connections(max_connections) + .connect(url) + .await + .context("failed to connect to postgres database")?; + Ok(Self { + pool, + test_mode: false, + }) + } + + // signups + + pub async fn create_signup( + &self, + github_login: &str, + email_address: &str, + about: &str, + ) -> Result { + test_support!(self, { + let query = " + INSERT INTO signups (github_login, email_address, about) + VALUES ($1, $2, $3) + RETURNING id + "; + sqlx::query_scalar(query) + .bind(github_login) + .bind(email_address) + .bind(about) + .fetch_one(&self.pool) + .await + .map(SignupId) + }) + } + + pub async fn get_all_signups(&self) -> Result> { + test_support!(self, { + let query = "SELECT * FROM users ORDER BY github_login ASC"; + sqlx::query_as(query).fetch_all(&self.pool).await + }) + } + + pub async fn delete_signup(&self, id: SignupId) -> Result<()> { + test_support!(self, { + let query = "DELETE FROM signups WHERE id = $1"; + sqlx::query(query) + .bind(id.0) + .execute(&self.pool) + .await + .map(drop) + }) + } + + // users + + #[allow(unused)] // Help rust-analyzer + #[cfg(any(test, feature = "seed-support"))] + pub async fn get_user(&self, github_login: &str) -> Result> { + test_support!(self, { + let query = " + SELECT id + FROM users + WHERE github_login = $1 + "; + sqlx::query_scalar(query) + .bind(github_login) + .fetch_optional(&self.pool) + .await + }) + } + + pub async fn create_user(&self, github_login: &str, admin: bool) -> Result { + test_support!(self, { + let query = " + INSERT INTO users (github_login, admin) + VALUES ($1, $2) + RETURNING id + "; + sqlx::query_scalar(query) + .bind(github_login) + .bind(admin) + .fetch_one(&self.pool) + .await + .map(UserId) + }) + } + + pub async fn get_all_users(&self) -> Result> { + test_support!(self, { + let query = "SELECT * FROM users ORDER BY github_login ASC"; + sqlx::query_as(query).fetch_all(&self.pool).await + }) + } + + pub async fn get_users_by_ids( + &self, + requester_id: UserId, + ids: impl Iterator, + ) -> Result> { + test_support!(self, { + // Only return users that are in a common channel with the requesting user. + let query = " + SELECT users.* + FROM + users, channel_memberships + WHERE + users.id = ANY ($1) AND + channel_memberships.user_id = users.id AND + channel_memberships.channel_id IN ( + SELECT channel_id + FROM channel_memberships + WHERE channel_memberships.user_id = $2 + ) + "; + + sqlx::query_as(query) + .bind(&ids.map(|id| id.0).collect::>()) + .bind(requester_id) + .fetch_all(&self.pool) + .await + }) + } + + pub async fn get_user_by_github_login(&self, github_login: &str) -> Result> { + test_support!(self, { + let query = "SELECT * FROM users WHERE github_login = $1 LIMIT 1"; + sqlx::query_as(query) + .bind(github_login) + .fetch_optional(&self.pool) + .await + }) + } + + pub async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()> { + test_support!(self, { + let query = "UPDATE users SET admin = $1 WHERE id = $2"; + sqlx::query(query) + .bind(is_admin) + .bind(id.0) + .execute(&self.pool) + .await + .map(drop) + }) + } + + pub async fn delete_user(&self, id: UserId) -> Result<()> { + test_support!(self, { + let query = "DELETE FROM users WHERE id = $1;"; + sqlx::query(query) + .bind(id.0) + .execute(&self.pool) + .await + .map(drop) + }) + } + + // access tokens + + pub async fn create_access_token_hash( + &self, + user_id: UserId, + access_token_hash: String, + ) -> Result<()> { + test_support!(self, { + let query = " + INSERT INTO access_tokens (user_id, hash) + VALUES ($1, $2) + "; + sqlx::query(query) + .bind(user_id.0) + .bind(access_token_hash) + .execute(&self.pool) + .await + .map(drop) + }) + } + + pub async fn get_access_token_hashes(&self, user_id: UserId) -> Result> { + test_support!(self, { + let query = "SELECT hash FROM access_tokens WHERE user_id = $1"; + sqlx::query_scalar(query) + .bind(user_id.0) + .fetch_all(&self.pool) + .await + }) + } + + // orgs + + #[allow(unused)] // Help rust-analyzer + #[cfg(any(test, feature = "seed-support"))] + pub async fn find_org_by_slug(&self, slug: &str) -> Result> { + test_support!(self, { + let query = " + SELECT * + FROM orgs + WHERE slug = $1 + "; + sqlx::query_as(query) + .bind(slug) + .fetch_optional(&self.pool) + .await + }) + } + + #[cfg(any(test, feature = "seed-support"))] + pub async fn create_org(&self, name: &str, slug: &str) -> Result { + test_support!(self, { + let query = " + INSERT INTO orgs (name, slug) + VALUES ($1, $2) + RETURNING id + "; + sqlx::query_scalar(query) + .bind(name) + .bind(slug) + .fetch_one(&self.pool) + .await + .map(OrgId) + }) + } + + #[cfg(any(test, feature = "seed-support"))] + pub async fn add_org_member( + &self, + org_id: OrgId, + user_id: UserId, + is_admin: bool, + ) -> Result<()> { + test_support!(self, { + let query = " + INSERT INTO org_memberships (org_id, user_id, admin) + VALUES ($1, $2, $3) + ON CONFLICT DO NOTHING + "; + sqlx::query(query) + .bind(org_id.0) + .bind(user_id.0) + .bind(is_admin) + .execute(&self.pool) + .await + .map(drop) + }) + } + + // channels + + #[cfg(any(test, feature = "seed-support"))] + pub async fn create_org_channel(&self, org_id: OrgId, name: &str) -> Result { + test_support!(self, { + let query = " + INSERT INTO channels (owner_id, owner_is_user, name) + VALUES ($1, false, $2) + RETURNING id + "; + sqlx::query_scalar(query) + .bind(org_id.0) + .bind(name) + .fetch_one(&self.pool) + .await + .map(ChannelId) + }) + } + + #[allow(unused)] // Help rust-analyzer + #[cfg(any(test, feature = "seed-support"))] + pub async fn get_org_channels(&self, org_id: OrgId) -> Result> { + test_support!(self, { + let query = " + SELECT * + FROM channels + WHERE + channels.owner_is_user = false AND + channels.owner_id = $1 + "; + sqlx::query_as(query) + .bind(org_id.0) + .fetch_all(&self.pool) + .await + }) + } + + pub async fn get_accessible_channels(&self, user_id: UserId) -> Result> { + test_support!(self, { + let query = " + SELECT + channels.id, channels.name + FROM + channel_memberships, channels + WHERE + channel_memberships.user_id = $1 AND + channel_memberships.channel_id = channels.id + "; + sqlx::query_as(query) + .bind(user_id.0) + .fetch_all(&self.pool) + .await + }) + } + + pub async fn can_user_access_channel( + &self, + user_id: UserId, + channel_id: ChannelId, + ) -> Result { + test_support!(self, { + let query = " + SELECT id + FROM channel_memberships + WHERE user_id = $1 AND channel_id = $2 + LIMIT 1 + "; + sqlx::query_scalar::<_, i32>(query) + .bind(user_id.0) + .bind(channel_id.0) + .fetch_optional(&self.pool) + .await + .map(|e| e.is_some()) + }) + } + + #[cfg(any(test, feature = "seed-support"))] + pub async fn add_channel_member( + &self, + channel_id: ChannelId, + user_id: UserId, + is_admin: bool, + ) -> Result<()> { + test_support!(self, { + let query = " + INSERT INTO channel_memberships (channel_id, user_id, admin) + VALUES ($1, $2, $3) + ON CONFLICT DO NOTHING + "; + sqlx::query(query) + .bind(channel_id.0) + .bind(user_id.0) + .bind(is_admin) + .execute(&self.pool) + .await + .map(drop) + }) + } + + // messages + + pub async fn create_channel_message( + &self, + channel_id: ChannelId, + sender_id: UserId, + body: &str, + timestamp: OffsetDateTime, + ) -> Result { + test_support!(self, { + let query = " + INSERT INTO channel_messages (channel_id, sender_id, body, sent_at) + VALUES ($1, $2, $3, $4) + RETURNING id + "; + sqlx::query_scalar(query) + .bind(channel_id.0) + .bind(sender_id.0) + .bind(body) + .bind(timestamp) + .fetch_one(&self.pool) + .await + .map(MessageId) + }) + } + + pub async fn get_channel_messages( + &self, + channel_id: ChannelId, + count: usize, + before_id: Option, + ) -> Result> { + test_support!(self, { + let query = r#" + SELECT * FROM ( + SELECT + id, sender_id, body, sent_at AT TIME ZONE 'UTC' as sent_at + FROM + channel_messages + WHERE + channel_id = $1 AND + id < $2 + ORDER BY id DESC + LIMIT $3 + ) as recent_messages + ORDER BY id ASC + "#; + sqlx::query_as(query) + .bind(channel_id.0) + .bind(before_id.unwrap_or(MessageId::MAX)) + .bind(count as i64) + .fetch_all(&self.pool) + .await + }) + } +} + +macro_rules! id_type { + ($name:ident) => { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, sqlx::Type, Serialize)] + #[sqlx(transparent)] + #[serde(transparent)] + pub struct $name(pub i32); + + impl $name { + #[allow(unused)] + pub const MAX: Self = Self(i32::MAX); + + #[allow(unused)] + pub fn from_proto(value: u64) -> Self { + Self(value as i32) + } + + #[allow(unused)] + pub fn to_proto(&self) -> u64 { + self.0 as u64 + } + } + }; +} + +id_type!(UserId); +#[derive(Debug, FromRow, Serialize)] +pub struct User { + pub id: UserId, + pub github_login: String, + pub admin: bool, +} + +id_type!(OrgId); +#[derive(FromRow)] +pub struct Org { + pub id: OrgId, + pub name: String, + pub slug: String, +} + +id_type!(SignupId); +#[derive(Debug, FromRow, Serialize)] +pub struct Signup { + pub id: SignupId, + pub github_login: String, + pub email_address: String, + pub about: String, +} + +id_type!(ChannelId); +#[derive(Debug, FromRow, Serialize)] +pub struct Channel { + pub id: ChannelId, + pub name: String, +} + +id_type!(MessageId); +#[derive(Debug, FromRow)] +pub struct ChannelMessage { + pub id: MessageId, + pub sender_id: UserId, + pub body: String, + pub sent_at: OffsetDateTime, +} + +#[cfg(test)] +pub mod tests { + use super::*; + use rand::prelude::*; + use sqlx::{ + migrate::{MigrateDatabase, Migrator}, + Postgres, + }; + use std::path::Path; + + pub struct TestDb { + pub db: Db, + pub name: String, + pub url: String, + } + + impl TestDb { + pub fn new() -> Self { + // Enable tests to run in parallel by serializing the creation of each test database. + lazy_static::lazy_static! { + static ref DB_CREATION: std::sync::Mutex<()> = std::sync::Mutex::new(()); + } + + let mut rng = StdRng::from_entropy(); + let name = format!("zed-test-{}", rng.gen::()); + let url = format!("postgres://postgres@localhost/{}", name); + let migrations_path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/migrations")); + let db = block_on(async { + { + let _lock = DB_CREATION.lock(); + Postgres::create_database(&url) + .await + .expect("failed to create test db"); + } + let mut db = Db::new(&url, 5).await.unwrap(); + db.test_mode = true; + let migrator = Migrator::new(migrations_path).await.unwrap(); + migrator.run(&db.pool).await.unwrap(); + db + }); + + Self { db, name, url } + } + + pub fn db(&self) -> &Db { + &self.db + } + } + + impl Drop for TestDb { + fn drop(&mut self) { + block_on(async { + let query = " + SELECT pg_terminate_backend(pg_stat_activity.pid) + FROM pg_stat_activity + WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid(); + "; + sqlx::query(query) + .bind(&self.name) + .execute(&self.db.pool) + .await + .unwrap(); + self.db.pool.close().await; + Postgres::drop_database(&self.url).await.unwrap(); + }); + } + } + + #[gpui::test] + async fn test_recent_channel_messages() { + let test_db = TestDb::new(); + let db = test_db.db(); + let user = db.create_user("user", false).await.unwrap(); + let org = db.create_org("org", "org").await.unwrap(); + let channel = db.create_org_channel(org, "channel").await.unwrap(); + for i in 0..10 { + db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc()) + .await + .unwrap(); + } + + let messages = db.get_channel_messages(channel, 5, None).await.unwrap(); + assert_eq!( + messages.iter().map(|m| &m.body).collect::>(), + ["5", "6", "7", "8", "9"] + ); + + let prev_messages = db + .get_channel_messages(channel, 4, Some(messages[0].id)) + .await + .unwrap(); + assert_eq!( + prev_messages.iter().map(|m| &m.body).collect::>(), + ["1", "2", "3", "4"] + ); + } +} diff --git a/server/src/home.rs b/server/src/home.rs index b4b8c24bf607302db7c15b109bd784bf38e667e2..25adde3a0f2fed3d7b11c107e23dcbfdf1d67bac 100644 --- a/server/src/home.rs +++ b/server/src/home.rs @@ -3,7 +3,6 @@ use crate::{ }; use comrak::ComrakOptions; use serde::{Deserialize, Serialize}; -use sqlx::Executor as _; use std::sync::Arc; use tide::{http::mime, log, Server}; @@ -76,14 +75,7 @@ async fn post_signup(mut request: Request) -> tide::Result { // Save signup in the database request .db() - .execute( - sqlx::query( - "INSERT INTO signups (github_login, email_address, about) VALUES ($1, $2, $3);", - ) - .bind(&form.github_login) - .bind(&form.email_address) - .bind(&form.about), - ) + .create_signup(&form.github_login, &form.email_address, &form.about) .await?; let layout_data = request.layout_data().await?; diff --git a/server/src/main.rs b/server/src/main.rs index ebd52b0a8bd0fd1e42beeaa505245852f545dd6f..ba54f05f1c4d33fe5947f30f4257d962b3348ba8 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,6 +1,7 @@ mod admin; mod assets; mod auth; +mod db; mod env; mod errors; mod expiring; @@ -8,20 +9,17 @@ mod github; mod home; mod rpc; mod team; -#[cfg(test)] -mod tests; use self::errors::TideResultExt as _; -use anyhow::{Context, Result}; -use async_sqlx_session::PostgresSessionStore; -use async_std::{net::TcpListener, sync::RwLock as AsyncRwLock}; +use anyhow::Result; +use async_std::net::TcpListener; use async_trait::async_trait; use auth::RequestExt as _; +use db::Db; use handlebars::{Handlebars, TemplateRenderError}; use parking_lot::RwLock; use rust_embed::RustEmbed; use serde::{Deserialize, Serialize}; -use sqlx::postgres::{PgPool, PgPoolOptions}; use std::sync::Arc; use surf::http::cookies::SameSite; use tide::{log, sessions::SessionMiddleware}; @@ -29,7 +27,6 @@ use tide_compress::CompressMiddleware; use zrpc::Peer; type Request = tide::Request>; -type DbPool = PgPool; #[derive(RustEmbed)] #[folder = "templates"] @@ -47,23 +44,17 @@ pub struct Config { } pub struct AppState { - db: sqlx::PgPool, + db: Db, handlebars: RwLock>, auth_client: auth::Client, github_client: Arc, repo_client: github::RepoClient, - rpc: AsyncRwLock, config: Config, } impl AppState { async fn new(config: Config) -> tide::Result> { - let db = PgPoolOptions::new() - .max_connections(5) - .connect(&config.database_url) - .await - .context("failed to connect to postgres database")?; - + let db = Db::new(&config.database_url, 5).await?; let github_client = github::AppClient::new(config.github_app_id, config.github_private_key.clone()); let repo_client = github_client @@ -77,7 +68,6 @@ impl AppState { auth_client: auth::build_client(&config.github_client_id, &config.github_client_secret), github_client, repo_client, - rpc: Default::default(), config, }; this.register_partials(); @@ -93,7 +83,7 @@ impl AppState { let partial = Templates::get(path.as_ref()).unwrap(); self.handlebars .write() - .register_partial(partial_name, std::str::from_utf8(partial.as_ref()).unwrap()) + .register_partial(partial_name, std::str::from_utf8(&partial.data).unwrap()) .unwrap() } } @@ -108,7 +98,7 @@ impl AppState { self.register_partials(); self.handlebars.read().render_template( - std::str::from_utf8(Templates::get(path).unwrap().as_ref()).unwrap(), + std::str::from_utf8(&Templates::get(path).unwrap().data).unwrap(), data, ) } @@ -117,7 +107,7 @@ impl AppState { #[async_trait] trait RequestExt { async fn layout_data(&mut self) -> tide::Result>; - fn db(&self) -> &DbPool; + fn db(&self) -> &Db; } #[async_trait] @@ -131,7 +121,7 @@ impl RequestExt for Request { Ok(self.ext::>().unwrap().clone()) } - fn db(&self) -> &DbPool { + fn db(&self) -> &Db { &self.state().db } } @@ -173,7 +163,7 @@ pub async fn run_server( web.with(CompressMiddleware::new()); web.with( SessionMiddleware::new( - PostgresSessionStore::new_with_table_name(&state.config.database_url, "sessions") + db::SessionStore::new_with_table_name(&state.config.database_url, "sessions") .await .unwrap(), state.config.session_secret.as_bytes(), diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 3c189833b252e2354e43683e63f4e2fba6db54c6..34f5f378d931e5ddbedca9f4e2a98a0709da329c 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1,15 +1,20 @@ -use crate::auth::{self, UserId}; - -use super::{auth::PeerExt as _, AppState}; +use super::{ + auth, + db::{ChannelId, MessageId, UserId}, + AppState, +}; use anyhow::anyhow; -use async_std::task; +use async_std::{sync::RwLock, task}; use async_tungstenite::{ tungstenite::{protocol::Role, Error as WebSocketError, Message as WebSocketMessage}, WebSocketStream, }; +use futures::{future::BoxFuture, FutureExt}; +use postage::{mpsc, prelude::Sink as _, prelude::Stream as _}; use sha1::{Digest as _, Sha1}; use std::{ - collections::{HashMap, HashSet}, + any::TypeId, + collections::{hash_map, HashMap, HashSet}, future::Future, mem, sync::Arc, @@ -21,27 +26,44 @@ use tide::{ http::headers::{HeaderName, CONNECTION, UPGRADE}, Request, Response, }; +use time::OffsetDateTime; use zrpc::{ auth::random_token, - proto::{self, EnvelopedMessage}, - ConnectionId, Peer, Router, TypedEnvelope, + proto::{self, AnyTypedEnvelope, EnvelopedMessage}, + ConnectionId, Peer, TypedEnvelope, }; type ReplicaId = u16; +type MessageHandler = Box< + dyn Send + + Sync + + Fn(Arc, Box) -> BoxFuture<'static, tide::Result<()>>, +>; + +pub struct Server { + peer: Arc, + state: RwLock, + app_state: Arc, + handlers: HashMap, + notifications: Option>, +} + #[derive(Default)] -pub struct State { - connections: HashMap, - pub worktrees: HashMap, +struct ServerState { + connections: HashMap, + pub worktrees: HashMap, + channels: HashMap, next_worktree_id: u64, } -struct ConnectionState { - _user_id: i32, +struct Connection { + user_id: UserId, worktrees: HashSet, + channels: HashSet, } -pub struct WorktreeState { +struct Worktree { host_connection_id: Option, guest_connection_ids: HashMap, active_replica_ids: HashSet, @@ -50,40 +72,174 @@ pub struct WorktreeState { entries: HashMap, } -impl WorktreeState { - pub fn connection_ids(&self) -> Vec { - self.guest_connection_ids - .keys() - .copied() - .chain(self.host_connection_id) - .collect() +#[derive(Default)] +struct Channel { + connection_ids: HashSet, +} + +const MESSAGE_COUNT_PER_PAGE: usize = 100; +const MAX_MESSAGE_LEN: usize = 1024; + +impl Server { + pub fn new( + app_state: Arc, + peer: Arc, + notifications: Option>, + ) -> Arc { + let mut server = Self { + peer, + app_state, + state: Default::default(), + handlers: Default::default(), + notifications, + }; + + server + .add_handler(Server::share_worktree) + .add_handler(Server::join_worktree) + .add_handler(Server::update_worktree) + .add_handler(Server::close_worktree) + .add_handler(Server::open_buffer) + .add_handler(Server::close_buffer) + .add_handler(Server::update_buffer) + .add_handler(Server::buffer_saved) + .add_handler(Server::save_buffer) + .add_handler(Server::get_channels) + .add_handler(Server::get_users) + .add_handler(Server::join_channel) + .add_handler(Server::leave_channel) + .add_handler(Server::send_channel_message) + .add_handler(Server::get_channel_messages); + + Arc::new(server) } - fn host_connection_id(&self) -> tide::Result { - Ok(self - .host_connection_id - .ok_or_else(|| anyhow!("host disconnected from worktree"))?) + fn add_handler(&mut self, handler: F) -> &mut Self + where + F: 'static + Send + Sync + Fn(Arc, TypedEnvelope) -> Fut, + Fut: 'static + Send + Future>, + M: EnvelopedMessage, + { + let prev_handler = self.handlers.insert( + TypeId::of::(), + Box::new(move |server, envelope| { + let envelope = envelope.into_any().downcast::>().unwrap(); + (handler)(server, *envelope).boxed() + }), + ); + if prev_handler.is_some() { + panic!("registered a handler for the same message twice"); + } + self + } + + pub fn handle_connection( + self: &Arc, + connection: Conn, + addr: String, + user_id: UserId, + ) -> impl Future + where + Conn: 'static + + futures::Sink + + futures::Stream> + + Send + + Unpin, + { + let this = self.clone(); + async move { + let (connection_id, handle_io, mut incoming_rx) = + this.peer.add_connection(connection).await; + this.add_connection(connection_id, user_id).await; + + let handle_io = handle_io.fuse(); + futures::pin_mut!(handle_io); + loop { + let next_message = incoming_rx.recv().fuse(); + futures::pin_mut!(next_message); + futures::select_biased! { + message = next_message => { + if let Some(message) = message { + let start_time = Instant::now(); + log::info!("RPC message received: {}", message.payload_type_name()); + if let Some(handler) = this.handlers.get(&message.payload_type_id()) { + if let Err(err) = (handler)(this.clone(), message).await { + log::error!("error handling message: {:?}", err); + } else { + log::info!("RPC message handled. duration:{:?}", start_time.elapsed()); + } + + if let Some(mut notifications) = this.notifications.clone() { + let _ = notifications.send(()).await; + } + } else { + log::warn!("unhandled message: {}", message.payload_type_name()); + } + } else { + log::info!("rpc connection closed {:?}", addr); + break; + } + } + handle_io = handle_io => { + if let Err(err) = handle_io { + log::error!("error handling rpc connection {:?} - {:?}", addr, err); + } + break; + } + } + } + + if let Err(err) = this.sign_out(connection_id).await { + log::error!("error signing out connection {:?} - {:?}", addr, err); + } + } + } + + async fn sign_out(self: &Arc, connection_id: zrpc::ConnectionId) -> tide::Result<()> { + self.peer.disconnect(connection_id).await; + let worktree_ids = self.remove_connection(connection_id).await; + for worktree_id in worktree_ids { + let state = self.state.read().await; + if let Some(worktree) = state.worktrees.get(&worktree_id) { + broadcast(connection_id, worktree.connection_ids(), |conn_id| { + self.peer.send( + conn_id, + proto::RemovePeer { + worktree_id, + peer_id: connection_id.0, + }, + ) + }) + .await?; + } + } + Ok(()) } -} -impl State { // Add a new connection associated with a given user. - pub fn add_connection(&mut self, connection_id: ConnectionId, _user_id: i32) { - self.connections.insert( + async fn add_connection(&self, connection_id: ConnectionId, user_id: UserId) { + self.state.write().await.connections.insert( connection_id, - ConnectionState { - _user_id, + Connection { + user_id, worktrees: Default::default(), + channels: Default::default(), }, ); } // Remove the given connection and its association with any worktrees. - pub fn remove_connection(&mut self, connection_id: ConnectionId) -> Vec { + async fn remove_connection(&self, connection_id: ConnectionId) -> Vec { let mut worktree_ids = Vec::new(); - if let Some(connection_state) = self.connections.remove(&connection_id) { - for worktree_id in connection_state.worktrees { - if let Some(worktree) = self.worktrees.get_mut(&worktree_id) { + let mut state = self.state.write().await; + if let Some(connection) = state.connections.remove(&connection_id) { + for channel_id in connection.channels { + if let Some(channel) = state.channels.get_mut(&channel_id) { + channel.connection_ids.remove(&connection_id); + } + } + for worktree_id in connection.worktrees { + if let Some(worktree) = state.worktrees.get_mut(&worktree_id) { if worktree.host_connection_id == Some(connection_id) { worktree_ids.push(worktree_id); } else if let Some(replica_id) = @@ -98,28 +254,615 @@ impl State { worktree_ids } + async fn share_worktree( + self: Arc, + mut request: TypedEnvelope, + ) -> tide::Result<()> { + let mut state = self.state.write().await; + let worktree_id = state.next_worktree_id; + state.next_worktree_id += 1; + let access_token = random_token(); + let worktree = request + .payload + .worktree + .as_mut() + .ok_or_else(|| anyhow!("missing worktree"))?; + let entries = mem::take(&mut worktree.entries) + .into_iter() + .map(|entry| (entry.id, entry)) + .collect(); + state.worktrees.insert( + worktree_id, + Worktree { + host_connection_id: Some(request.sender_id), + guest_connection_ids: Default::default(), + active_replica_ids: Default::default(), + access_token: access_token.clone(), + root_name: mem::take(&mut worktree.root_name), + entries, + }, + ); + + self.peer + .respond( + request.receipt(), + proto::ShareWorktreeResponse { + worktree_id, + access_token, + }, + ) + .await?; + Ok(()) + } + + async fn join_worktree( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let worktree_id = request.payload.worktree_id; + let access_token = &request.payload.access_token; + + let mut state = self.state.write().await; + if let Some((peer_replica_id, worktree)) = + state.join_worktree(request.sender_id, worktree_id, access_token) + { + let mut peers = Vec::new(); + if let Some(host_connection_id) = worktree.host_connection_id { + peers.push(proto::Peer { + peer_id: host_connection_id.0, + replica_id: 0, + }); + } + for (peer_conn_id, peer_replica_id) in &worktree.guest_connection_ids { + if *peer_conn_id != request.sender_id { + peers.push(proto::Peer { + peer_id: peer_conn_id.0, + replica_id: *peer_replica_id as u32, + }); + } + } + + broadcast(request.sender_id, worktree.connection_ids(), |conn_id| { + self.peer.send( + conn_id, + proto::AddPeer { + worktree_id, + peer: Some(proto::Peer { + peer_id: request.sender_id.0, + replica_id: peer_replica_id as u32, + }), + }, + ) + }) + .await?; + self.peer + .respond( + request.receipt(), + proto::OpenWorktreeResponse { + worktree_id, + worktree: Some(proto::Worktree { + root_name: worktree.root_name.clone(), + entries: worktree.entries.values().cloned().collect(), + }), + replica_id: peer_replica_id as u32, + peers, + }, + ) + .await?; + } else { + self.peer + .respond( + request.receipt(), + proto::OpenWorktreeResponse { + worktree_id, + worktree: None, + replica_id: 0, + peers: Vec::new(), + }, + ) + .await?; + } + + Ok(()) + } + + async fn update_worktree( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + { + let mut state = self.state.write().await; + let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?; + for entry_id in &request.payload.removed_entries { + worktree.entries.remove(&entry_id); + } + + for entry in &request.payload.updated_entries { + worktree.entries.insert(entry.id, entry.clone()); + } + } + + self.broadcast_in_worktree(request.payload.worktree_id, &request) + .await?; + Ok(()) + } + + async fn close_worktree( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let connection_ids; + { + let mut state = self.state.write().await; + let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?; + connection_ids = worktree.connection_ids(); + if worktree.host_connection_id == Some(request.sender_id) { + worktree.host_connection_id = None; + } else if let Some(replica_id) = + worktree.guest_connection_ids.remove(&request.sender_id) + { + worktree.active_replica_ids.remove(&replica_id); + } + } + + broadcast(request.sender_id, connection_ids, |conn_id| { + self.peer.send( + conn_id, + proto::RemovePeer { + worktree_id: request.payload.worktree_id, + peer_id: request.sender_id.0, + }, + ) + }) + .await?; + + Ok(()) + } + + async fn open_buffer( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let receipt = request.receipt(); + let worktree_id = request.payload.worktree_id; + let host_connection_id = self + .state + .read() + .await + .read_worktree(worktree_id, request.sender_id)? + .host_connection_id()?; + + let response = self + .peer + .forward_request(request.sender_id, host_connection_id, request.payload) + .await?; + self.peer.respond(receipt, response).await?; + Ok(()) + } + + async fn close_buffer( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let host_connection_id = self + .state + .read() + .await + .read_worktree(request.payload.worktree_id, request.sender_id)? + .host_connection_id()?; + + self.peer + .forward_send(request.sender_id, host_connection_id, request.payload) + .await?; + + Ok(()) + } + + async fn save_buffer( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let host; + let guests; + { + let state = self.state.read().await; + let worktree = state.read_worktree(request.payload.worktree_id, request.sender_id)?; + host = worktree.host_connection_id()?; + guests = worktree + .guest_connection_ids + .keys() + .copied() + .collect::>(); + } + + let sender = request.sender_id; + let receipt = request.receipt(); + let response = self + .peer + .forward_request(sender, host, request.payload.clone()) + .await?; + + broadcast(host, guests, |conn_id| { + let response = response.clone(); + let peer = &self.peer; + async move { + if conn_id == sender { + peer.respond(receipt, response).await + } else { + peer.forward_send(host, conn_id, response).await + } + } + }) + .await?; + + Ok(()) + } + + async fn update_buffer( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + self.broadcast_in_worktree(request.payload.worktree_id, &request) + .await + } + + async fn buffer_saved( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + self.broadcast_in_worktree(request.payload.worktree_id, &request) + .await + } + + async fn get_channels( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let user_id = self + .state + .read() + .await + .user_id_for_connection(request.sender_id)?; + let channels = self.app_state.db.get_accessible_channels(user_id).await?; + self.peer + .respond( + request.receipt(), + proto::GetChannelsResponse { + channels: channels + .into_iter() + .map(|chan| proto::Channel { + id: chan.id.to_proto(), + name: chan.name, + }) + .collect(), + }, + ) + .await?; + Ok(()) + } + + async fn get_users( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let user_id = self + .state + .read() + .await + .user_id_for_connection(request.sender_id)?; + let receipt = request.receipt(); + let user_ids = request.payload.user_ids.into_iter().map(UserId::from_proto); + let users = self + .app_state + .db + .get_users_by_ids(user_id, user_ids) + .await? + .into_iter() + .map(|user| proto::User { + id: user.id.to_proto(), + github_login: user.github_login, + avatar_url: String::new(), + }) + .collect(); + self.peer + .respond(receipt, proto::GetUsersResponse { users }) + .await?; + Ok(()) + } + + async fn join_channel( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let user_id = self + .state + .read() + .await + .user_id_for_connection(request.sender_id)?; + let channel_id = ChannelId::from_proto(request.payload.channel_id); + if !self + .app_state + .db + .can_user_access_channel(user_id, channel_id) + .await? + { + Err(anyhow!("access denied"))?; + } + + self.state + .write() + .await + .join_channel(request.sender_id, channel_id); + let messages = self + .app_state + .db + .get_channel_messages(channel_id, MESSAGE_COUNT_PER_PAGE, None) + .await? + .into_iter() + .map(|msg| proto::ChannelMessage { + id: msg.id.to_proto(), + body: msg.body, + timestamp: msg.sent_at.unix_timestamp() as u64, + sender_id: msg.sender_id.to_proto(), + }) + .collect::>(); + self.peer + .respond( + request.receipt(), + proto::JoinChannelResponse { + done: messages.len() < MESSAGE_COUNT_PER_PAGE, + messages, + }, + ) + .await?; + Ok(()) + } + + async fn leave_channel( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let user_id = self + .state + .read() + .await + .user_id_for_connection(request.sender_id)?; + let channel_id = ChannelId::from_proto(request.payload.channel_id); + if !self + .app_state + .db + .can_user_access_channel(user_id, channel_id) + .await? + { + Err(anyhow!("access denied"))?; + } + + self.state + .write() + .await + .leave_channel(request.sender_id, channel_id); + + Ok(()) + } + + async fn send_channel_message( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let receipt = request.receipt(); + let channel_id = ChannelId::from_proto(request.payload.channel_id); + let user_id; + let connection_ids; + { + let state = self.state.read().await; + user_id = state.user_id_for_connection(request.sender_id)?; + if let Some(channel) = state.channels.get(&channel_id) { + connection_ids = channel.connection_ids(); + } else { + return Ok(()); + } + } + + // Validate the message body. + let body = request.payload.body.trim().to_string(); + if body.len() > MAX_MESSAGE_LEN { + self.peer + .respond_with_error( + receipt, + proto::Error { + message: "message is too long".to_string(), + }, + ) + .await?; + return Ok(()); + } + if body.is_empty() { + self.peer + .respond_with_error( + receipt, + proto::Error { + message: "message can't be blank".to_string(), + }, + ) + .await?; + return Ok(()); + } + + let timestamp = OffsetDateTime::now_utc(); + let message_id = self + .app_state + .db + .create_channel_message(channel_id, user_id, &body, timestamp) + .await? + .to_proto(); + let message = proto::ChannelMessage { + sender_id: user_id.to_proto(), + id: message_id, + body, + timestamp: timestamp.unix_timestamp() as u64, + }; + broadcast(request.sender_id, connection_ids, |conn_id| { + self.peer.send( + conn_id, + proto::ChannelMessageSent { + channel_id: channel_id.to_proto(), + message: Some(message.clone()), + }, + ) + }) + .await?; + self.peer + .respond( + receipt, + proto::SendChannelMessageResponse { + message: Some(message), + }, + ) + .await?; + Ok(()) + } + + async fn get_channel_messages( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let user_id = self + .state + .read() + .await + .user_id_for_connection(request.sender_id)?; + let channel_id = ChannelId::from_proto(request.payload.channel_id); + if !self + .app_state + .db + .can_user_access_channel(user_id, channel_id) + .await? + { + Err(anyhow!("access denied"))?; + } + + let messages = self + .app_state + .db + .get_channel_messages( + channel_id, + MESSAGE_COUNT_PER_PAGE, + Some(MessageId::from_proto(request.payload.before_message_id)), + ) + .await? + .into_iter() + .map(|msg| proto::ChannelMessage { + id: msg.id.to_proto(), + body: msg.body, + timestamp: msg.sent_at.unix_timestamp() as u64, + sender_id: msg.sender_id.to_proto(), + }) + .collect::>(); + self.peer + .respond( + request.receipt(), + proto::GetChannelMessagesResponse { + done: messages.len() < MESSAGE_COUNT_PER_PAGE, + messages, + }, + ) + .await?; + Ok(()) + } + + async fn broadcast_in_worktree( + &self, + worktree_id: u64, + message: &TypedEnvelope, + ) -> tide::Result<()> { + let connection_ids = self + .state + .read() + .await + .read_worktree(worktree_id, message.sender_id)? + .connection_ids(); + + broadcast(message.sender_id, connection_ids, |conn_id| { + self.peer + .forward_send(message.sender_id, conn_id, message.payload.clone()) + }) + .await?; + + Ok(()) + } +} + +pub async fn broadcast( + sender_id: ConnectionId, + receiver_ids: Vec, + mut f: F, +) -> anyhow::Result<()> +where + F: FnMut(ConnectionId) -> T, + T: Future>, +{ + let futures = receiver_ids + .into_iter() + .filter(|id| *id != sender_id) + .map(|id| f(id)); + futures::future::try_join_all(futures).await?; + Ok(()) +} + +impl ServerState { + fn join_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) { + if let Some(connection) = self.connections.get_mut(&connection_id) { + connection.channels.insert(channel_id); + self.channels + .entry(channel_id) + .or_default() + .connection_ids + .insert(connection_id); + } + } + + fn leave_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) { + if let Some(connection) = self.connections.get_mut(&connection_id) { + connection.channels.remove(&channel_id); + if let hash_map::Entry::Occupied(mut entry) = self.channels.entry(channel_id) { + entry.get_mut().connection_ids.remove(&connection_id); + if entry.get_mut().connection_ids.is_empty() { + entry.remove(); + } + } + } + } + + fn user_id_for_connection(&self, connection_id: ConnectionId) -> tide::Result { + Ok(self + .connections + .get(&connection_id) + .ok_or_else(|| anyhow!("unknown connection"))? + .user_id) + } + // Add the given connection as a guest of the given worktree - pub fn join_worktree( + fn join_worktree( &mut self, connection_id: ConnectionId, worktree_id: u64, access_token: &str, - ) -> Option<(ReplicaId, &WorktreeState)> { - if let Some(worktree_state) = self.worktrees.get_mut(&worktree_id) { - if access_token == worktree_state.access_token { - if let Some(connection_state) = self.connections.get_mut(&connection_id) { - connection_state.worktrees.insert(worktree_id); + ) -> Option<(ReplicaId, &Worktree)> { + if let Some(worktree) = self.worktrees.get_mut(&worktree_id) { + if access_token == worktree.access_token { + if let Some(connection) = self.connections.get_mut(&connection_id) { + connection.worktrees.insert(worktree_id); } let mut replica_id = 1; - while worktree_state.active_replica_ids.contains(&replica_id) { + while worktree.active_replica_ids.contains(&replica_id) { replica_id += 1; } - worktree_state.active_replica_ids.insert(replica_id); - worktree_state + worktree.active_replica_ids.insert(replica_id); + worktree .guest_connection_ids .insert(connection_id, replica_id); - Some((replica_id, worktree_state)) + Some((replica_id, worktree)) } else { None } @@ -132,7 +875,7 @@ impl State { &self, worktree_id: u64, connection_id: ConnectionId, - ) -> tide::Result<&WorktreeState> { + ) -> tide::Result<&Worktree> { let worktree = self .worktrees .get(&worktree_id) @@ -155,7 +898,7 @@ impl State { &mut self, worktree_id: u64, connection_id: ConnectionId, - ) -> tide::Result<&mut WorktreeState> { + ) -> tide::Result<&mut Worktree> { let worktree = self .worktrees .get_mut(&worktree_id) @@ -175,95 +918,33 @@ impl State { } } -trait MessageHandler<'a, M: proto::EnvelopedMessage> { - type Output: 'a + Send + Future>; - - fn handle( - &self, - message: TypedEnvelope, - rpc: &'a Arc, - app_state: &'a Arc, - ) -> Self::Output; -} - -impl<'a, M, F, Fut> MessageHandler<'a, M> for F -where - M: proto::EnvelopedMessage, - F: Fn(TypedEnvelope, &'a Arc, &'a Arc) -> Fut, - Fut: 'a + Send + Future>, -{ - type Output = Fut; - - fn handle( - &self, - message: TypedEnvelope, - rpc: &'a Arc, - app_state: &'a Arc, - ) -> Self::Output { - (self)(message, rpc, app_state) +impl Worktree { + pub fn connection_ids(&self) -> Vec { + self.guest_connection_ids + .keys() + .copied() + .chain(self.host_connection_id) + .collect() } -} - -fn on_message(router: &mut Router, rpc: &Arc, app_state: &Arc, handler: H) -where - M: EnvelopedMessage, - H: 'static + Clone + Send + Sync + for<'a> MessageHandler<'a, M>, -{ - let rpc = rpc.clone(); - let handler = handler.clone(); - let app_state = app_state.clone(); - router.add_message_handler(move |message| { - let rpc = rpc.clone(); - let handler = handler.clone(); - let app_state = app_state.clone(); - async move { - let sender_id = message.sender_id; - let message_id = message.message_id; - let start_time = Instant::now(); - log::info!( - "RPC message received. id: {}.{}, type:{}", - sender_id, - message_id, - M::NAME - ); - if let Err(err) = handler.handle(message, &rpc, &app_state).await { - log::error!("error handling message: {:?}", err); - } else { - log::info!( - "RPC message handled. id:{}.{}, duration:{:?}", - sender_id, - message_id, - start_time.elapsed() - ); - } - Ok(()) - } - }); + fn host_connection_id(&self) -> tide::Result { + Ok(self + .host_connection_id + .ok_or_else(|| anyhow!("host disconnected from worktree"))?) + } } -pub fn add_rpc_routes(router: &mut Router, state: &Arc, rpc: &Arc) { - on_message(router, rpc, state, share_worktree); - on_message(router, rpc, state, join_worktree); - on_message(router, rpc, state, update_worktree); - on_message(router, rpc, state, close_worktree); - on_message(router, rpc, state, open_buffer); - on_message(router, rpc, state, close_buffer); - on_message(router, rpc, state, update_buffer); - on_message(router, rpc, state, buffer_saved); - on_message(router, rpc, state, save_buffer); +impl Channel { + fn connection_ids(&self) -> Vec { + self.connection_ids.iter().copied().collect() + } } pub fn add_routes(app: &mut tide::Server>, rpc: &Arc) { - let mut router = Router::new(); - add_rpc_routes(&mut router, app.state(), rpc); - let router = Arc::new(router); - - let rpc = rpc.clone(); + let server = Server::new(app.state().clone(), rpc.clone(), None); app.at("/rpc").with(auth::VerifyToken).get(move |request: Request>| { let user_id = request.ext::().copied(); - let rpc = rpc.clone(); - let router = router.clone(); + let server = server.clone(); async move { const WEBSOCKET_GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; @@ -290,12 +971,11 @@ pub fn add_routes(app: &mut tide::Server>, rpc: &Arc) { let http_res: &mut tide::http::Response = response.as_mut(); let upgrade_receiver = http_res.recv_upgrade().await; let addr = request.remote().unwrap_or("unknown").to_string(); - let state = request.state().clone(); - let user_id = user_id.ok_or_else(|| anyhow!("user_id is not present on request. ensure auth::VerifyToken middleware is present"))?.0; + let user_id = user_id.ok_or_else(|| anyhow!("user_id is not present on request. ensure auth::VerifyToken middleware is present"))?; task::spawn(async move { if let Some(stream) = upgrade_receiver.await { let stream = WebSocketStream::from_raw_socket(stream, Role::Server, None).await; - handle_connection(rpc, router, state, addr, stream, user_id).await; + server.handle_connection(stream, addr, user_id).await; } }); @@ -304,349 +984,794 @@ pub fn add_routes(app: &mut tide::Server>, rpc: &Arc) { }); } -pub async fn handle_connection( - rpc: Arc, - router: Arc, - state: Arc, - addr: String, - stream: Conn, - user_id: i32, -) where - Conn: 'static - + futures::Sink - + futures::Stream> - + Send - + Unpin, -{ - log::info!("accepted rpc connection: {:?}", addr); - let (connection_id, handle_io, handle_messages) = rpc.add_connection(stream, router).await; - state - .rpc - .write() - .await - .add_connection(connection_id, user_id); +fn header_contains_ignore_case( + request: &tide::Request, + header_name: HeaderName, + value: &str, +) -> bool { + request + .header(header_name) + .map(|h| { + h.as_str() + .split(',') + .any(|s| s.trim().eq_ignore_ascii_case(value.trim())) + }) + .unwrap_or(false) +} - let handle_messages = async move { - handle_messages.await; - Ok(()) +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + auth, + db::{tests::TestDb, UserId}, + github, AppState, Config, }; + use async_std::{sync::RwLockReadGuard, task}; + use gpui::TestAppContext; + use postage::mpsc; + use serde_json::json; + use sqlx::types::time::OffsetDateTime; + use std::{path::Path, sync::Arc, time::Duration}; + use zed::{ + channel::{Channel, ChannelDetails, ChannelList}, + editor::{Editor, Insert}, + fs::{FakeFs, Fs as _}, + language::LanguageRegistry, + rpc::Client, + settings, test, + user::UserStore, + worktree::Worktree, + }; + use zrpc::Peer; - if let Err(e) = futures::try_join!(handle_messages, handle_io) { - log::error!("error handling rpc connection {:?} - {:?}", addr, e); - } + #[gpui::test] + async fn test_share_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + let (window_b, _) = cx_b.add_window(|_| EmptyView); + let settings = cx_b.read(settings::test).1; + let lang_registry = Arc::new(LanguageRegistry::new()); + + // Connect to a server as 2 clients. + let mut server = TestServer::start().await; + let (_, client_a) = server.create_client(&mut cx_a, "user_a").await; + let (_, client_b) = server.create_client(&mut cx_b, "user_b").await; + + cx_a.foreground().forbid_parking(); - log::info!("closing connection to {:?}", addr); - if let Err(e) = rpc.sign_out(connection_id, &state).await { - log::error!("error signing out connection {:?} - {:?}", addr, e); + // Share a local worktree as client A + let fs = Arc::new(FakeFs::new()); + fs.insert_tree( + "/a", + json!({ + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; + let worktree_a = Worktree::open_local( + "/a".as_ref(), + lang_registry.clone(), + fs, + &mut cx_a.to_async(), + ) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let (worktree_id, worktree_token) = worktree_a + .update(&mut cx_a, |tree, cx| { + tree.as_local_mut().unwrap().share(client_a.clone(), cx) + }) + .await + .unwrap(); + + // Join that worktree as client B, and see that a guest has joined as client A. + let worktree_b = Worktree::open_remote( + client_b.clone(), + worktree_id, + worktree_token, + lang_registry.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + let replica_id_b = worktree_b.read_with(&cx_b, |tree, _| tree.replica_id()); + worktree_a + .condition(&cx_a, |tree, _| { + tree.peers() + .values() + .any(|replica_id| *replica_id == replica_id_b) + }) + .await; + + // Open the same file as client B and client A. + let buffer_b = worktree_b + .update(&mut cx_b, |worktree, cx| worktree.open_buffer("b.txt", cx)) + .await + .unwrap(); + buffer_b.read_with(&cx_b, |buf, _| assert_eq!(buf.text(), "b-contents")); + worktree_a.read_with(&cx_a, |tree, cx| assert!(tree.has_open_buffer("b.txt", cx))); + let buffer_a = worktree_a + .update(&mut cx_a, |tree, cx| tree.open_buffer("b.txt", cx)) + .await + .unwrap(); + + // Create a selection set as client B and see that selection set as client A. + let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, settings, cx)); + buffer_a + .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1) + .await; + + // Edit the buffer as client B and see that edit as client A. + editor_b.update(&mut cx_b, |editor, cx| { + editor.insert(&Insert("ok, ".into()), cx) + }); + buffer_a + .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents") + .await; + + // Remove the selection set as client B, see those selections disappear as client A. + cx_b.update(move |_| drop(editor_b)); + buffer_a + .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0) + .await; + + // Close the buffer as client A, see that the buffer is closed. + drop(buffer_a); + worktree_a + .condition(&cx_a, |tree, cx| !tree.has_open_buffer("b.txt", cx)) + .await; + + // Dropping the worktree removes client B from client A's peers. + cx_b.update(move |_| drop(worktree_b)); + worktree_a + .condition(&cx_a, |tree, _| tree.peers().is_empty()) + .await; } -} -async fn share_worktree( - mut request: TypedEnvelope, - rpc: &Arc, - state: &Arc, -) -> tide::Result<()> { - let mut state = state.rpc.write().await; - let worktree_id = state.next_worktree_id; - state.next_worktree_id += 1; - let access_token = random_token(); - let worktree = request - .payload - .worktree - .as_mut() - .ok_or_else(|| anyhow!("missing worktree"))?; - let entries = mem::take(&mut worktree.entries) - .into_iter() - .map(|entry| (entry.id, entry)) - .collect(); - state.worktrees.insert( - worktree_id, - WorktreeState { - host_connection_id: Some(request.sender_id), - guest_connection_ids: Default::default(), - active_replica_ids: Default::default(), - access_token: access_token.clone(), - root_name: mem::take(&mut worktree.root_name), - entries, - }, - ); - - rpc.respond( - request.receipt(), - proto::ShareWorktreeResponse { + #[gpui::test] + async fn test_propagate_saves_and_fs_changes_in_shared_worktree( + mut cx_a: TestAppContext, + mut cx_b: TestAppContext, + mut cx_c: TestAppContext, + ) { + cx_a.foreground().forbid_parking(); + let lang_registry = Arc::new(LanguageRegistry::new()); + + // Connect to a server as 3 clients. + let mut server = TestServer::start().await; + let (_, client_a) = server.create_client(&mut cx_a, "user_a").await; + let (_, client_b) = server.create_client(&mut cx_b, "user_b").await; + let (_, client_c) = server.create_client(&mut cx_c, "user_c").await; + + let fs = Arc::new(FakeFs::new()); + + // Share a worktree as client A. + fs.insert_tree( + "/a", + json!({ + "file1": "", + "file2": "" + }), + ) + .await; + + let worktree_a = Worktree::open_local( + "/a".as_ref(), + lang_registry.clone(), + fs.clone(), + &mut cx_a.to_async(), + ) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let (worktree_id, worktree_token) = worktree_a + .update(&mut cx_a, |tree, cx| { + tree.as_local_mut().unwrap().share(client_a.clone(), cx) + }) + .await + .unwrap(); + + // Join that worktree as clients B and C. + let worktree_b = Worktree::open_remote( + client_b.clone(), worktree_id, - access_token, - }, - ) - .await?; - Ok(()) -} + worktree_token.clone(), + lang_registry.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + let worktree_c = Worktree::open_remote( + client_c.clone(), + worktree_id, + worktree_token, + lang_registry.clone(), + &mut cx_c.to_async(), + ) + .await + .unwrap(); -async fn join_worktree( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, -) -> tide::Result<()> { - let worktree_id = request.payload.worktree_id; - let access_token = &request.payload.access_token; - - let mut state = state.rpc.write().await; - if let Some((peer_replica_id, worktree)) = - state.join_worktree(request.sender_id, worktree_id, access_token) - { - let mut peers = Vec::new(); - if let Some(host_connection_id) = worktree.host_connection_id { - peers.push(proto::Peer { - peer_id: host_connection_id.0, - replica_id: 0, - }); - } - for (peer_conn_id, peer_replica_id) in &worktree.guest_connection_ids { - if *peer_conn_id != request.sender_id { - peers.push(proto::Peer { - peer_id: peer_conn_id.0, - replica_id: *peer_replica_id as u32, - }); - } - } + // Open and edit a buffer as both guests B and C. + let buffer_b = worktree_b + .update(&mut cx_b, |tree, cx| tree.open_buffer("file1", cx)) + .await + .unwrap(); + let buffer_c = worktree_c + .update(&mut cx_c, |tree, cx| tree.open_buffer("file1", cx)) + .await + .unwrap(); + buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "i-am-b, ", cx)); + buffer_c.update(&mut cx_c, |buf, cx| buf.edit([0..0], "i-am-c, ", cx)); - broadcast(request.sender_id, worktree.connection_ids(), |conn_id| { - rpc.send( - conn_id, - proto::AddPeer { - worktree_id, - peer: Some(proto::Peer { - peer_id: request.sender_id.0, - replica_id: peer_replica_id as u32, - }), - }, + // Open and edit that buffer as the host. + let buffer_a = worktree_a + .update(&mut cx_a, |tree, cx| tree.open_buffer("file1", cx)) + .await + .unwrap(); + + buffer_a + .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ") + .await; + buffer_a.update(&mut cx_a, |buf, cx| { + buf.edit([buf.len()..buf.len()], "i-am-a", cx) + }); + + // Wait for edits to propagate + buffer_a + .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") + .await; + buffer_b + .condition(&mut cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") + .await; + buffer_c + .condition(&mut cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") + .await; + + // Edit the buffer as the host and concurrently save as guest B. + let save_b = buffer_b.update(&mut cx_b, |buf, cx| buf.save(cx).unwrap()); + buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "hi-a, ", cx)); + save_b.await.unwrap(); + assert_eq!( + fs.load("/a/file1".as_ref()).await.unwrap(), + "hi-a, i-am-c, i-am-b, i-am-a" + ); + buffer_a.read_with(&cx_a, |buf, _| assert!(!buf.is_dirty())); + buffer_b.read_with(&cx_b, |buf, _| assert!(!buf.is_dirty())); + buffer_c.condition(&cx_c, |buf, _| !buf.is_dirty()).await; + + // Make changes on host's file system, see those changes on the guests. + fs.rename("/a/file2".as_ref(), "/a/file3".as_ref()) + .await + .unwrap(); + fs.insert_file(Path::new("/a/file4"), "4".into()) + .await + .unwrap(); + + worktree_b + .condition(&cx_b, |tree, _| tree.file_count() == 3) + .await; + worktree_c + .condition(&cx_c, |tree, _| tree.file_count() == 3) + .await; + worktree_b.read_with(&cx_b, |tree, _| { + assert_eq!( + tree.paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + &["file1", "file3", "file4"] ) - }) - .await?; - rpc.respond( - request.receipt(), - proto::OpenWorktreeResponse { - worktree_id, - worktree: Some(proto::Worktree { - root_name: worktree.root_name.clone(), - entries: worktree.entries.values().cloned().collect(), - }), - replica_id: peer_replica_id as u32, - peers, - }, + }); + worktree_c.read_with(&cx_c, |tree, _| { + assert_eq!( + tree.paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + &["file1", "file3", "file4"] + ) + }); + } + + #[gpui::test] + async fn test_buffer_conflict_after_save(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + cx_a.foreground().forbid_parking(); + let lang_registry = Arc::new(LanguageRegistry::new()); + + // Connect to a server as 2 clients. + let mut server = TestServer::start().await; + let (_, client_a) = server.create_client(&mut cx_a, "user_a").await; + let (_, client_b) = server.create_client(&mut cx_b, "user_b").await; + + // Share a local worktree as client A + let fs = Arc::new(FakeFs::new()); + fs.save(Path::new("/a.txt"), &"a-contents".into()) + .await + .unwrap(); + let worktree_a = Worktree::open_local( + "/".as_ref(), + lang_registry.clone(), + fs, + &mut cx_a.to_async(), ) - .await?; - } else { - rpc.respond( - request.receipt(), - proto::OpenWorktreeResponse { - worktree_id, - worktree: None, - replica_id: 0, - peers: Vec::new(), - }, + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let (worktree_id, worktree_token) = worktree_a + .update(&mut cx_a, |tree, cx| { + tree.as_local_mut().unwrap().share(client_a.clone(), cx) + }) + .await + .unwrap(); + + // Join that worktree as client B, and see that a guest has joined as client A. + let worktree_b = Worktree::open_remote( + client_b.clone(), + worktree_id, + worktree_token, + lang_registry.clone(), + &mut cx_b.to_async(), ) - .await?; - } + .await + .unwrap(); - Ok(()) -} + let buffer_b = worktree_b + .update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx)) + .await + .unwrap(); + let mtime = buffer_b.read_with(&cx_b, |buf, _| buf.file().unwrap().mtime); -async fn update_worktree( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, -) -> tide::Result<()> { - { - let mut state = state.rpc.write().await; - let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?; - for entry_id in &request.payload.removed_entries { - worktree.entries.remove(&entry_id); - } + buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "world ", cx)); + buffer_b.read_with(&cx_b, |buf, _| { + assert!(buf.is_dirty()); + assert!(!buf.has_conflict()); + }); - for entry in &request.payload.updated_entries { - worktree.entries.insert(entry.id, entry.clone()); - } + buffer_b + .update(&mut cx_b, |buf, cx| buf.save(cx)) + .unwrap() + .await + .unwrap(); + worktree_b + .condition(&cx_b, |_, cx| { + buffer_b.read(cx).file().unwrap().mtime != mtime + }) + .await; + buffer_b.read_with(&cx_b, |buf, _| { + assert!(!buf.is_dirty()); + assert!(!buf.has_conflict()); + }); + + buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "hello ", cx)); + buffer_b.read_with(&cx_b, |buf, _| { + assert!(buf.is_dirty()); + assert!(!buf.has_conflict()); + }); } - broadcast_in_worktree(request.payload.worktree_id, request, rpc, state).await?; - Ok(()) -} + #[gpui::test] + async fn test_editing_while_guest_opens_buffer( + mut cx_a: TestAppContext, + mut cx_b: TestAppContext, + ) { + cx_a.foreground().forbid_parking(); + let lang_registry = Arc::new(LanguageRegistry::new()); -async fn close_worktree( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, -) -> tide::Result<()> { - let connection_ids; - { - let mut state = state.rpc.write().await; - let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?; - connection_ids = worktree.connection_ids(); - if worktree.host_connection_id == Some(request.sender_id) { - worktree.host_connection_id = None; - } else if let Some(replica_id) = worktree.guest_connection_ids.remove(&request.sender_id) { - worktree.active_replica_ids.remove(&replica_id); - } - } + // Connect to a server as 2 clients. + let mut server = TestServer::start().await; + let (_, client_a) = server.create_client(&mut cx_a, "user_a").await; + let (_, client_b) = server.create_client(&mut cx_b, "user_b").await; - broadcast(request.sender_id, connection_ids, |conn_id| { - rpc.send( - conn_id, - proto::RemovePeer { - worktree_id: request.payload.worktree_id, - peer_id: request.sender_id.0, - }, + // Share a local worktree as client A + let fs = Arc::new(FakeFs::new()); + fs.save(Path::new("/a.txt"), &"a-contents".into()) + .await + .unwrap(); + let worktree_a = Worktree::open_local( + "/".as_ref(), + lang_registry.clone(), + fs, + &mut cx_a.to_async(), ) - }) - .await?; + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let (worktree_id, worktree_token) = worktree_a + .update(&mut cx_a, |tree, cx| { + tree.as_local_mut().unwrap().share(client_a.clone(), cx) + }) + .await + .unwrap(); - Ok(()) -} + // Join that worktree as client B, and see that a guest has joined as client A. + let worktree_b = Worktree::open_remote( + client_b.clone(), + worktree_id, + worktree_token, + lang_registry.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + + let buffer_a = worktree_a + .update(&mut cx_a, |tree, cx| tree.open_buffer("a.txt", cx)) + .await + .unwrap(); + let buffer_b = cx_b + .background() + .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx))); + + task::yield_now().await; + buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "z", cx)); + + let text = buffer_a.read_with(&cx_a, |buf, _| buf.text()); + let buffer_b = buffer_b.await.unwrap(); + buffer_b.condition(&cx_b, |buf, _| buf.text() == text).await; + } -async fn open_buffer( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, -) -> tide::Result<()> { - let receipt = request.receipt(); - let worktree_id = request.payload.worktree_id; - let host_connection_id = state - .rpc - .read() + #[gpui::test] + async fn test_peer_disconnection(mut cx_a: TestAppContext, cx_b: TestAppContext) { + cx_a.foreground().forbid_parking(); + let lang_registry = Arc::new(LanguageRegistry::new()); + + // Connect to a server as 2 clients. + let mut server = TestServer::start().await; + let (_, client_a) = server.create_client(&mut cx_a, "user_a").await; + let (_, client_b) = server.create_client(&mut cx_a, "user_b").await; + + // Share a local worktree as client A + let fs = Arc::new(FakeFs::new()); + fs.insert_tree( + "/a", + json!({ + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; + let worktree_a = Worktree::open_local( + "/a".as_ref(), + lang_registry.clone(), + fs, + &mut cx_a.to_async(), + ) .await - .read_worktree(worktree_id, request.sender_id)? - .host_connection_id()?; + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let (worktree_id, worktree_token) = worktree_a + .update(&mut cx_a, |tree, cx| { + tree.as_local_mut().unwrap().share(client_a.clone(), cx) + }) + .await + .unwrap(); - let response = rpc - .forward_request(request.sender_id, host_connection_id, request.payload) - .await?; - rpc.respond(receipt, response).await?; - Ok(()) -} + // Join that worktree as client B, and see that a guest has joined as client A. + let _worktree_b = Worktree::open_remote( + client_b.clone(), + worktree_id, + worktree_token, + lang_registry.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + worktree_a + .condition(&cx_a, |tree, _| tree.peers().len() == 1) + .await; + + // Drop client B's connection and ensure client A observes client B leaving the worktree. + client_b.disconnect().await.unwrap(); + worktree_a + .condition(&cx_a, |tree, _| tree.peers().len() == 0) + .await; + } + + #[gpui::test] + async fn test_basic_chat(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + cx_a.foreground().forbid_parking(); + + // Connect to a server as 2 clients. + let mut server = TestServer::start().await; + let (user_id_a, client_a) = server.create_client(&mut cx_a, "user_a").await; + let (user_id_b, client_b) = server.create_client(&mut cx_b, "user_b").await; -async fn close_buffer( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, -) -> tide::Result<()> { - let host_connection_id = state - .rpc - .read() + // Create an org that includes these 2 users. + let db = &server.app_state.db; + let org_id = db.create_org("Test Org", "test-org").await.unwrap(); + db.add_org_member(org_id, user_id_a, false).await.unwrap(); + db.add_org_member(org_id, user_id_b, false).await.unwrap(); + + // Create a channel that includes all the users. + let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap(); + db.add_channel_member(channel_id, user_id_a, false) + .await + .unwrap(); + db.add_channel_member(channel_id, user_id_b, false) + .await + .unwrap(); + db.create_channel_message( + channel_id, + user_id_b, + "hello A, it's B.", + OffsetDateTime::now_utc(), + ) .await - .read_worktree(request.payload.worktree_id, request.sender_id)? - .host_connection_id()?; + .unwrap(); - rpc.forward_send(request.sender_id, host_connection_id, request.payload) - .await?; + let user_store_a = Arc::new(UserStore::new(client_a.clone())); + let channels_a = cx_a.add_model(|cx| ChannelList::new(user_store_a, client_a, cx)); + channels_a + .condition(&mut cx_a, |list, _| list.available_channels().is_some()) + .await; + channels_a.read_with(&cx_a, |list, _| { + assert_eq!( + list.available_channels().unwrap(), + &[ChannelDetails { + id: channel_id.to_proto(), + name: "test-channel".to_string() + }] + ) + }); + let channel_a = channels_a.update(&mut cx_a, |this, cx| { + this.get_channel(channel_id.to_proto(), cx).unwrap() + }); + channel_a.read_with(&cx_a, |channel, _| assert!(channel.messages().is_empty())); + channel_a + .condition(&cx_a, |channel, _| { + channel_messages(channel) + == [("user_b".to_string(), "hello A, it's B.".to_string())] + }) + .await; - Ok(()) -} + let user_store_b = Arc::new(UserStore::new(client_b.clone())); + let channels_b = cx_b.add_model(|cx| ChannelList::new(user_store_b, client_b, cx)); + channels_b + .condition(&mut cx_b, |list, _| list.available_channels().is_some()) + .await; + channels_b.read_with(&cx_b, |list, _| { + assert_eq!( + list.available_channels().unwrap(), + &[ChannelDetails { + id: channel_id.to_proto(), + name: "test-channel".to_string() + }] + ) + }); -async fn save_buffer( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, -) -> tide::Result<()> { - let host; - let guests; - { - let state = state.rpc.read().await; - let worktree = state.read_worktree(request.payload.worktree_id, request.sender_id)?; - host = worktree.host_connection_id()?; - guests = worktree - .guest_connection_ids - .keys() - .copied() - .collect::>(); + let channel_b = channels_b.update(&mut cx_b, |this, cx| { + this.get_channel(channel_id.to_proto(), cx).unwrap() + }); + channel_b.read_with(&cx_b, |channel, _| assert!(channel.messages().is_empty())); + channel_b + .condition(&cx_b, |channel, _| { + channel_messages(channel) + == [("user_b".to_string(), "hello A, it's B.".to_string())] + }) + .await; + + channel_a + .update(&mut cx_a, |channel, cx| { + channel + .send_message("oh, hi B.".to_string(), cx) + .unwrap() + .detach(); + let task = channel.send_message("sup".to_string(), cx).unwrap(); + assert_eq!( + channel + .pending_messages() + .iter() + .map(|m| &m.body) + .collect::>(), + &["oh, hi B.", "sup"] + ); + task + }) + .await + .unwrap(); + + channel_a + .condition(&cx_a, |channel, _| channel.pending_messages().is_empty()) + .await; + channel_b + .condition(&cx_b, |channel, _| { + channel_messages(channel) + == [ + ("user_b".to_string(), "hello A, it's B.".to_string()), + ("user_a".to_string(), "oh, hi B.".to_string()), + ("user_a".to_string(), "sup".to_string()), + ] + }) + .await; + + assert_eq!( + server.state().await.channels[&channel_id] + .connection_ids + .len(), + 2 + ); + cx_b.update(|_| drop(channel_b)); + server + .condition(|state| state.channels[&channel_id].connection_ids.len() == 1) + .await; + + cx_a.update(|_| drop(channel_a)); + server + .condition(|state| !state.channels.contains_key(&channel_id)) + .await; + + fn channel_messages(channel: &Channel) -> Vec<(String, String)> { + channel + .messages() + .cursor::<(), ()>() + .map(|m| (m.sender.github_login.clone(), m.body.clone())) + .collect() + } } - let sender = request.sender_id; - let receipt = request.receipt(); - let response = rpc - .forward_request(sender, host, request.payload.clone()) - .await?; + #[gpui::test] + async fn test_chat_message_validation(mut cx_a: TestAppContext) { + cx_a.foreground().forbid_parking(); - broadcast(host, guests, |conn_id| { - let response = response.clone(); - async move { - if conn_id == sender { - rpc.respond(receipt, response).await - } else { - rpc.forward_send(host, conn_id, response).await + let mut server = TestServer::start().await; + let (user_id_a, client_a) = server.create_client(&mut cx_a, "user_a").await; + + let db = &server.app_state.db; + let org_id = db.create_org("Test Org", "test-org").await.unwrap(); + let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap(); + db.add_org_member(org_id, user_id_a, false).await.unwrap(); + db.add_channel_member(channel_id, user_id_a, false) + .await + .unwrap(); + + let user_store_a = Arc::new(UserStore::new(client_a.clone())); + let channels_a = cx_a.add_model(|cx| ChannelList::new(user_store_a, client_a, cx)); + channels_a + .condition(&mut cx_a, |list, _| list.available_channels().is_some()) + .await; + let channel_a = channels_a.update(&mut cx_a, |this, cx| { + this.get_channel(channel_id.to_proto(), cx).unwrap() + }); + + // Messages aren't allowed to be too long. + channel_a + .update(&mut cx_a, |channel, cx| { + let long_body = "this is long.\n".repeat(1024); + channel.send_message(long_body, cx).unwrap() + }) + .await + .unwrap_err(); + + // Messages aren't allowed to be blank. + channel_a.update(&mut cx_a, |channel, cx| { + channel.send_message(String::new(), cx).unwrap_err() + }); + + // Leading and trailing whitespace are trimmed. + channel_a + .update(&mut cx_a, |channel, cx| { + channel + .send_message("\n surrounded by whitespace \n".to_string(), cx) + .unwrap() + }) + .await + .unwrap(); + assert_eq!( + db.get_channel_messages(channel_id, 10, None) + .await + .unwrap() + .iter() + .map(|m| &m.body) + .collect::>(), + &["surrounded by whitespace"] + ); + } + + struct TestServer { + peer: Arc, + app_state: Arc, + server: Arc, + notifications: mpsc::Receiver<()>, + _test_db: TestDb, + } + + impl TestServer { + async fn start() -> Self { + let test_db = TestDb::new(); + let app_state = Self::build_app_state(&test_db).await; + let peer = Peer::new(); + let notifications = mpsc::channel(128); + let server = Server::new(app_state.clone(), peer.clone(), Some(notifications.0)); + Self { + peer, + app_state, + server, + notifications: notifications.1, + _test_db: test_db, } } - }) - .await?; - Ok(()) -} + async fn create_client( + &mut self, + cx: &mut TestAppContext, + name: &str, + ) -> (UserId, Arc) { + let user_id = self.app_state.db.create_user(name, false).await.unwrap(); + let client = Client::new(); + let (client_conn, server_conn) = test::Channel::bidirectional(); + cx.background() + .spawn( + self.server + .handle_connection(server_conn, name.to_string(), user_id), + ) + .detach(); + client + .add_connection(user_id.to_proto(), client_conn, &cx.to_async()) + .await + .unwrap(); + (user_id, client) + } -async fn update_buffer( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, -) -> tide::Result<()> { - broadcast_in_worktree(request.payload.worktree_id, request, rpc, state).await -} + async fn build_app_state(test_db: &TestDb) -> Arc { + let mut config = Config::default(); + config.session_secret = "a".repeat(32); + config.database_url = test_db.url.clone(); + let github_client = github::AppClient::test(); + Arc::new(AppState { + db: test_db.db().clone(), + handlebars: Default::default(), + auth_client: auth::build_client("", ""), + repo_client: github::RepoClient::test(&github_client), + github_client, + config, + }) + } -async fn buffer_saved( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, -) -> tide::Result<()> { - broadcast_in_worktree(request.payload.worktree_id, request, rpc, state).await -} + async fn state<'a>(&'a self) -> RwLockReadGuard<'a, ServerState> { + self.server.state.read().await + } -async fn broadcast_in_worktree( - worktree_id: u64, - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, -) -> tide::Result<()> { - let connection_ids = state - .rpc - .read() - .await - .read_worktree(worktree_id, request.sender_id)? - .connection_ids(); + async fn condition(&mut self, mut predicate: F) + where + F: FnMut(&ServerState) -> bool, + { + async_std::future::timeout(Duration::from_millis(500), async { + while !(predicate)(&*self.server.state.read().await) { + self.notifications.recv().await; + } + }) + .await + .expect("condition timed out"); + } + } - broadcast(request.sender_id, connection_ids, |conn_id| { - rpc.forward_send(request.sender_id, conn_id, request.payload.clone()) - }) - .await?; + impl Drop for TestServer { + fn drop(&mut self) { + task::block_on(self.peer.reset()); + } + } - Ok(()) -} + struct EmptyView; -pub async fn broadcast( - sender_id: ConnectionId, - receiver_ids: Vec, - mut f: F, -) -> anyhow::Result<()> -where - F: FnMut(ConnectionId) -> T, - T: Future>, -{ - let futures = receiver_ids - .into_iter() - .filter(|id| *id != sender_id) - .map(|id| f(id)); - futures::future::try_join_all(futures).await?; - Ok(()) -} + impl gpui::Entity for EmptyView { + type Event = (); + } -fn header_contains_ignore_case( - request: &tide::Request, - header_name: HeaderName, - value: &str, -) -> bool { - request - .header(header_name) - .map(|h| { - h.as_str() - .split(',') - .any(|s| s.trim().eq_ignore_ascii_case(value.trim())) - }) - .unwrap_or(false) + impl gpui::View for EmptyView { + fn ui_name() -> &'static str { + "empty view" + } + + fn render(&mut self, _: &mut gpui::RenderContext) -> gpui::ElementBox { + gpui::Element::boxed(gpui::elements::Empty) + } + } } diff --git a/server/src/tests.rs b/server/src/tests.rs deleted file mode 100644 index 66d904746772c5c4d0813ac85ab33b108c074207..0000000000000000000000000000000000000000 --- a/server/src/tests.rs +++ /dev/null @@ -1,613 +0,0 @@ -use crate::{ - admin, auth, github, - rpc::{self, add_rpc_routes}, - AppState, Config, -}; -use async_std::task; -use gpui::TestAppContext; -use rand::prelude::*; -use serde_json::json; -use sqlx::{ - migrate::{MigrateDatabase, Migrator}, - postgres::PgPoolOptions, - Executor as _, Postgres, -}; -use std::{path::Path, sync::Arc}; -use zed::{ - editor::Editor, - fs::{FakeFs, Fs as _}, - language::LanguageRegistry, - rpc::Client, - settings, - test::Channel, - worktree::Worktree, -}; -use zrpc::{ForegroundRouter, Peer, Router}; - -#[gpui::test] -async fn test_share_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { - let (window_b, _) = cx_b.add_window(|_| EmptyView); - let settings = settings::channel(&cx_b.font_cache()).unwrap().1; - let lang_registry = Arc::new(LanguageRegistry::new()); - - // Connect to a server as 2 clients. - let mut server = TestServer::start().await; - let client_a = server.create_client(&mut cx_a, "user_a").await; - let client_b = server.create_client(&mut cx_b, "user_b").await; - - cx_a.foreground().forbid_parking(); - - // Share a local worktree as client A - let fs = Arc::new(FakeFs::new()); - fs.insert_tree( - "/a", - json!({ - "a.txt": "a-contents", - "b.txt": "b-contents", - }), - ) - .await; - let worktree_a = Worktree::open_local( - "/a".as_ref(), - lang_registry.clone(), - fs, - &mut cx_a.to_async(), - ) - .await - .unwrap(); - worktree_a - .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) - .await; - let (worktree_id, worktree_token) = worktree_a - .update(&mut cx_a, |tree, cx| { - tree.as_local_mut().unwrap().share(client_a.clone(), cx) - }) - .await - .unwrap(); - - // Join that worktree as client B, and see that a guest has joined as client A. - let worktree_b = Worktree::open_remote( - client_b.clone(), - worktree_id, - worktree_token, - lang_registry.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); - let replica_id_b = worktree_b.read_with(&cx_b, |tree, _| tree.replica_id()); - worktree_a - .condition(&cx_a, |tree, _| { - tree.peers() - .values() - .any(|replica_id| *replica_id == replica_id_b) - }) - .await; - - // Open the same file as client B and client A. - let buffer_b = worktree_b - .update(&mut cx_b, |worktree, cx| worktree.open_buffer("b.txt", cx)) - .await - .unwrap(); - buffer_b.read_with(&cx_b, |buf, _| assert_eq!(buf.text(), "b-contents")); - worktree_a.read_with(&cx_a, |tree, cx| assert!(tree.has_open_buffer("b.txt", cx))); - let buffer_a = worktree_a - .update(&mut cx_a, |tree, cx| tree.open_buffer("b.txt", cx)) - .await - .unwrap(); - - // Create a selection set as client B and see that selection set as client A. - let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, settings, cx)); - buffer_a - .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1) - .await; - - // Edit the buffer as client B and see that edit as client A. - editor_b.update(&mut cx_b, |editor, cx| { - editor.insert(&"ok, ".to_string(), cx) - }); - buffer_a - .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents") - .await; - - // Remove the selection set as client B, see those selections disappear as client A. - cx_b.update(move |_| drop(editor_b)); - buffer_a - .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0) - .await; - - // Close the buffer as client A, see that the buffer is closed. - drop(buffer_a); - worktree_a - .condition(&cx_a, |tree, cx| !tree.has_open_buffer("b.txt", cx)) - .await; - - // Dropping the worktree removes client B from client A's peers. - cx_b.update(move |_| drop(worktree_b)); - worktree_a - .condition(&cx_a, |tree, _| tree.peers().is_empty()) - .await; -} - -#[gpui::test] -async fn test_propagate_saves_and_fs_changes_in_shared_worktree( - mut cx_a: TestAppContext, - mut cx_b: TestAppContext, - mut cx_c: TestAppContext, -) { - let lang_registry = Arc::new(LanguageRegistry::new()); - - // Connect to a server as 3 clients. - let mut server = TestServer::start().await; - let client_a = server.create_client(&mut cx_a, "user_a").await; - let client_b = server.create_client(&mut cx_b, "user_b").await; - let client_c = server.create_client(&mut cx_c, "user_c").await; - - cx_a.foreground().forbid_parking(); - - let fs = Arc::new(FakeFs::new()); - - // Share a worktree as client A. - fs.insert_tree( - "/a", - json!({ - "file1": "", - "file2": "" - }), - ) - .await; - - let worktree_a = Worktree::open_local( - "/a".as_ref(), - lang_registry.clone(), - fs.clone(), - &mut cx_a.to_async(), - ) - .await - .unwrap(); - worktree_a - .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) - .await; - let (worktree_id, worktree_token) = worktree_a - .update(&mut cx_a, |tree, cx| { - tree.as_local_mut().unwrap().share(client_a.clone(), cx) - }) - .await - .unwrap(); - - // Join that worktree as clients B and C. - let worktree_b = Worktree::open_remote( - client_b.clone(), - worktree_id, - worktree_token.clone(), - lang_registry.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); - let worktree_c = Worktree::open_remote( - client_c.clone(), - worktree_id, - worktree_token, - lang_registry.clone(), - &mut cx_c.to_async(), - ) - .await - .unwrap(); - - // Open and edit a buffer as both guests B and C. - let buffer_b = worktree_b - .update(&mut cx_b, |tree, cx| tree.open_buffer("file1", cx)) - .await - .unwrap(); - let buffer_c = worktree_c - .update(&mut cx_c, |tree, cx| tree.open_buffer("file1", cx)) - .await - .unwrap(); - buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "i-am-b, ", cx)); - buffer_c.update(&mut cx_c, |buf, cx| buf.edit([0..0], "i-am-c, ", cx)); - - // Open and edit that buffer as the host. - let buffer_a = worktree_a - .update(&mut cx_a, |tree, cx| tree.open_buffer("file1", cx)) - .await - .unwrap(); - - buffer_a - .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ") - .await; - buffer_a.update(&mut cx_a, |buf, cx| { - buf.edit([buf.len()..buf.len()], "i-am-a", cx) - }); - - // Wait for edits to propagate - buffer_a - .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") - .await; - buffer_b - .condition(&mut cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") - .await; - buffer_c - .condition(&mut cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") - .await; - - // Edit the buffer as the host and concurrently save as guest B. - let save_b = buffer_b.update(&mut cx_b, |buf, cx| buf.save(cx).unwrap()); - buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "hi-a, ", cx)); - save_b.await.unwrap(); - assert_eq!( - fs.load("/a/file1".as_ref()).await.unwrap(), - "hi-a, i-am-c, i-am-b, i-am-a" - ); - buffer_a.read_with(&cx_a, |buf, _| assert!(!buf.is_dirty())); - buffer_b.read_with(&cx_b, |buf, _| assert!(!buf.is_dirty())); - buffer_c.condition(&cx_c, |buf, _| !buf.is_dirty()).await; - - // Make changes on host's file system, see those changes on the guests. - fs.rename("/a/file2".as_ref(), "/a/file3".as_ref()) - .await - .unwrap(); - fs.insert_file(Path::new("/a/file4"), "4".into()) - .await - .unwrap(); - - worktree_b - .condition(&cx_b, |tree, _| tree.file_count() == 3) - .await; - worktree_c - .condition(&cx_c, |tree, _| tree.file_count() == 3) - .await; - worktree_b.read_with(&cx_b, |tree, _| { - assert_eq!( - tree.paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - &["file1", "file3", "file4"] - ) - }); - worktree_c.read_with(&cx_c, |tree, _| { - assert_eq!( - tree.paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - &["file1", "file3", "file4"] - ) - }); -} - -#[gpui::test] -async fn test_buffer_conflict_after_save(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { - let lang_registry = Arc::new(LanguageRegistry::new()); - - // Connect to a server as 2 clients. - let mut server = TestServer::start().await; - let client_a = server.create_client(&mut cx_a, "user_a").await; - let client_b = server.create_client(&mut cx_b, "user_b").await; - - cx_a.foreground().forbid_parking(); - - // Share a local worktree as client A - let fs = Arc::new(FakeFs::new()); - fs.save(Path::new("/a.txt"), &"a-contents".into()) - .await - .unwrap(); - let worktree_a = Worktree::open_local( - "/".as_ref(), - lang_registry.clone(), - fs, - &mut cx_a.to_async(), - ) - .await - .unwrap(); - worktree_a - .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) - .await; - let (worktree_id, worktree_token) = worktree_a - .update(&mut cx_a, |tree, cx| { - tree.as_local_mut().unwrap().share(client_a.clone(), cx) - }) - .await - .unwrap(); - - // Join that worktree as client B, and see that a guest has joined as client A. - let worktree_b = Worktree::open_remote( - client_b.clone(), - worktree_id, - worktree_token, - lang_registry.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); - - let buffer_b = worktree_b - .update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx)) - .await - .unwrap(); - let mtime = buffer_b.read_with(&cx_b, |buf, _| buf.file().unwrap().mtime); - - buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "world ", cx)); - buffer_b.read_with(&cx_b, |buf, _| { - assert!(buf.is_dirty()); - assert!(!buf.has_conflict()); - }); - - buffer_b - .update(&mut cx_b, |buf, cx| buf.save(cx)) - .unwrap() - .await - .unwrap(); - worktree_b - .condition(&cx_b, |_, cx| { - buffer_b.read(cx).file().unwrap().mtime != mtime - }) - .await; - buffer_b.read_with(&cx_b, |buf, _| { - assert!(!buf.is_dirty()); - assert!(!buf.has_conflict()); - }); - - buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "hello ", cx)); - buffer_b.read_with(&cx_b, |buf, _| { - assert!(buf.is_dirty()); - assert!(!buf.has_conflict()); - }); -} - -#[gpui::test] -async fn test_editing_while_guest_opens_buffer(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { - let lang_registry = Arc::new(LanguageRegistry::new()); - - // Connect to a server as 2 clients. - let mut server = TestServer::start().await; - let client_a = server.create_client(&mut cx_a, "user_a").await; - let client_b = server.create_client(&mut cx_b, "user_b").await; - - cx_a.foreground().forbid_parking(); - - // Share a local worktree as client A - let fs = Arc::new(FakeFs::new()); - fs.save(Path::new("/a.txt"), &"a-contents".into()) - .await - .unwrap(); - let worktree_a = Worktree::open_local( - "/".as_ref(), - lang_registry.clone(), - fs, - &mut cx_a.to_async(), - ) - .await - .unwrap(); - worktree_a - .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) - .await; - let (worktree_id, worktree_token) = worktree_a - .update(&mut cx_a, |tree, cx| { - tree.as_local_mut().unwrap().share(client_a.clone(), cx) - }) - .await - .unwrap(); - - // Join that worktree as client B, and see that a guest has joined as client A. - let worktree_b = Worktree::open_remote( - client_b.clone(), - worktree_id, - worktree_token, - lang_registry.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); - - let buffer_a = worktree_a - .update(&mut cx_a, |tree, cx| tree.open_buffer("a.txt", cx)) - .await - .unwrap(); - let buffer_b = cx_b - .background() - .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx))); - - task::yield_now().await; - buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "z", cx)); - - let text = buffer_a.read_with(&cx_a, |buf, _| buf.text()); - let buffer_b = buffer_b.await.unwrap(); - buffer_b.condition(&cx_b, |buf, _| buf.text() == text).await; -} - -#[gpui::test] -async fn test_peer_disconnection(mut cx_a: TestAppContext, cx_b: TestAppContext) { - let lang_registry = Arc::new(LanguageRegistry::new()); - - // Connect to a server as 2 clients. - let mut server = TestServer::start().await; - let client_a = server.create_client(&mut cx_a, "user_a").await; - let client_b = server.create_client(&mut cx_a, "user_b").await; - - cx_a.foreground().forbid_parking(); - - // Share a local worktree as client A - let fs = Arc::new(FakeFs::new()); - fs.insert_tree( - "/a", - json!({ - "a.txt": "a-contents", - "b.txt": "b-contents", - }), - ) - .await; - let worktree_a = Worktree::open_local( - "/a".as_ref(), - lang_registry.clone(), - fs, - &mut cx_a.to_async(), - ) - .await - .unwrap(); - worktree_a - .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) - .await; - let (worktree_id, worktree_token) = worktree_a - .update(&mut cx_a, |tree, cx| { - tree.as_local_mut().unwrap().share(client_a.clone(), cx) - }) - .await - .unwrap(); - - // Join that worktree as client B, and see that a guest has joined as client A. - let _worktree_b = Worktree::open_remote( - client_b.clone(), - worktree_id, - worktree_token, - lang_registry.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); - worktree_a - .condition(&cx_a, |tree, _| tree.peers().len() == 1) - .await; - - // Drop client B's connection and ensure client A observes client B leaving the worktree. - client_b.disconnect().await.unwrap(); - worktree_a - .condition(&cx_a, |tree, _| tree.peers().len() == 0) - .await; -} - -struct TestServer { - peer: Arc, - app_state: Arc, - db_name: String, - router: Arc, -} - -impl TestServer { - async fn start() -> Self { - let mut rng = StdRng::from_entropy(); - let db_name = format!("zed-test-{}", rng.gen::()); - let app_state = Self::build_app_state(&db_name).await; - let peer = Peer::new(); - let mut router = Router::new(); - add_rpc_routes(&mut router, &app_state, &peer); - Self { - peer, - router: Arc::new(router), - app_state, - db_name, - } - } - - async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> Client { - let user_id = admin::create_user(&self.app_state.db, name, false) - .await - .unwrap(); - let lang_registry = Arc::new(LanguageRegistry::new()); - let client = Client::new(lang_registry.clone()); - let mut client_router = ForegroundRouter::new(); - cx.update(|cx| zed::worktree::init(cx, &client, &mut client_router)); - - let (client_conn, server_conn) = Channel::bidirectional(); - cx.background() - .spawn(rpc::handle_connection( - self.peer.clone(), - self.router.clone(), - self.app_state.clone(), - name.to_string(), - server_conn, - user_id, - )) - .detach(); - client - .add_connection(client_conn, Arc::new(client_router), cx.to_async()) - .await - .unwrap(); - - client - } - - async fn build_app_state(db_name: &str) -> Arc { - let mut config = Config::default(); - config.session_secret = "a".repeat(32); - config.database_url = format!("postgres://postgres@localhost/{}", db_name); - - Self::create_db(&config.database_url).await; - let db = PgPoolOptions::new() - .max_connections(5) - .connect(&config.database_url) - .await - .expect("failed to connect to postgres database"); - let migrator = Migrator::new(Path::new(concat!( - env!("CARGO_MANIFEST_DIR"), - "/migrations" - ))) - .await - .unwrap(); - migrator.run(&db).await.unwrap(); - - let github_client = github::AppClient::test(); - Arc::new(AppState { - db, - handlebars: Default::default(), - auth_client: auth::build_client("", ""), - repo_client: github::RepoClient::test(&github_client), - github_client, - rpc: Default::default(), - config, - }) - } - - async fn create_db(url: &str) { - // Enable tests to run in parallel by serializing the creation of each test database. - lazy_static::lazy_static! { - static ref DB_CREATION: async_std::sync::Mutex<()> = async_std::sync::Mutex::new(()); - } - - let _lock = DB_CREATION.lock().await; - Postgres::create_database(url) - .await - .expect("failed to create test database"); - } -} - -impl Drop for TestServer { - fn drop(&mut self) { - task::block_on(async { - self.peer.reset().await; - self.app_state - .db - .execute( - format!( - " - SELECT pg_terminate_backend(pg_stat_activity.pid) - FROM pg_stat_activity - WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid();", - self.db_name, - ) - .as_str(), - ) - .await - .unwrap(); - self.app_state.db.close().await; - Postgres::drop_database(&self.app_state.config.database_url) - .await - .unwrap(); - }); - } -} - -struct EmptyView; - -impl gpui::Entity for EmptyView { - type Event = (); -} - -impl gpui::View for EmptyView { - fn ui_name() -> &'static str { - "empty view" - } - - fn render<'a>(&self, _: &gpui::RenderContext) -> gpui::ElementBox { - gpui::Element::boxed(gpui::elements::Empty) - } -} diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 25aafaac5f9f71adecd8b4f99bc1425194cbc755..985901c50cf8a14d4331ed73f89a98fd1323d28d 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -18,8 +18,8 @@ test-support = ["tempdir", "zrpc/test-support"] [dependencies] anyhow = "1.0.38" -arrayvec = "0.5.2" async-trait = "0.1" +arrayvec = "0.7.1" async-tungstenite = { version = "0.14", features = ["async-tls"] } crossbeam-channel = "0.5.0" ctor = "0.1.20" @@ -38,16 +38,18 @@ parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } rand = "0.8.3" rsa = "0.4" -rust-embed = "5.9.0" +rust-embed = { version = "6.2", features = ["include-exclude"] } seahash = "4.1" serde = { version = "1", features = ["derive"] } serde_json = { version = "1.0.64", features = ["preserve_order"] } +serde_path_to_error = "0.1.4" similar = "1.3" simplelog = "0.9" smallvec = { version = "1.6", features = ["union"] } smol = "1.2.5" surf = "2.2" tempdir = { version = "0.3.7", optional = true } +time = { version = "0.3" } tiny_http = "0.8" toml = "0.5" tree-sitter = "0.19.5" @@ -61,6 +63,7 @@ env_logger = "0.8" serde_json = { version = "1.0.64", features = ["preserve_order"] } tempdir = { version = "0.3.7" } unindent = "0.1.7" +zrpc = { path = "../zrpc", features = ["test-support"] } [package.metadata.bundle] icon = ["app-icon@2x.png", "app-icon.png"] diff --git a/zed/assets/fonts/inconsolata/Inconsolata-Bold.ttf b/zed/assets/fonts/inconsolata/Inconsolata-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e8aad4c3cd21d9811f33b08fef26502b36441641 Binary files /dev/null and b/zed/assets/fonts/inconsolata/Inconsolata-Bold.ttf differ diff --git a/zed/assets/fonts/inconsolata/Inconsolata-Regular.ttf b/zed/assets/fonts/inconsolata/Inconsolata-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..00ffc946a5aaccdbc563b2725b2c921aad1979a0 Binary files /dev/null and b/zed/assets/fonts/inconsolata/Inconsolata-Regular.ttf differ diff --git a/zed/assets/icons/comment-16.svg b/zed/assets/icons/comment-16.svg new file mode 100644 index 0000000000000000000000000000000000000000..6316d3a4a9033d8c799f28ff0a2336f072fbcb2b --- /dev/null +++ b/zed/assets/icons/comment-16.svg @@ -0,0 +1,3 @@ + + + diff --git a/zed/assets/icons/folder-tree-16.svg b/zed/assets/icons/folder-tree-16.svg new file mode 100644 index 0000000000000000000000000000000000000000..f22773b159ccf24fa410f37be92123fc2fa30601 --- /dev/null +++ b/zed/assets/icons/folder-tree-16.svg @@ -0,0 +1,3 @@ + + + diff --git a/zed/assets/icons/user-16.svg b/zed/assets/icons/user-16.svg new file mode 100644 index 0000000000000000000000000000000000000000..4ec153f5380a62e4529dc1e38e47d8531a0b07cb --- /dev/null +++ b/zed/assets/icons/user-16.svg @@ -0,0 +1,3 @@ + + + diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 50a1b15e0c504d36d32c4fb312b9fb7d7d2b5236..08fb222f6de66262c8c4f5672e6b39f366a8be07 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -1,47 +1,115 @@ -[ui] -background = "$surfaces.1" +[workspace] +background = "$surface.0" -[ui.tab] -background = "$surfaces.2" -text = "$text_colors.dull" -border = { color = "#000000", width = 1.0 } +[workspace.tab] +text = "$text.2" padding = { left = 10, right = 10 } -icon_close = "#383839" -icon_dirty = "#556de8" -icon_conflict = "#e45349" - -[ui.active_tab] -extends = "$ui.tab" -background = "$surfaces.3" -text = "$text_colors.bright" - -[ui.selector] -background = "$surfaces.4" -text = "$text_colors.bright" -padding = { top = 6.0, bottom = 6.0, left = 6.0, right = 6.0 } -margin.top = 12.0 -corner_radius = 6.0 -shadow = { offset = [0.0, 0.0], blur = 12.0, color = "#00000088" } - -[ui.selector.item] +icon_close = "$text.0.color" +icon_dirty = "$status.info" +icon_conflict = "$status.warn" + +[workspace.active_tab] +extends = "$workspace.tab" +background = "$surface.1" +text = "$text.0" + +[workspace.sidebar.icons] +padding = { left = 10, right = 10 } + +[workspace.sidebar.resize_handle] +margin = { left = 6 } + +[workspace.sidebar_icon] +color = "$text.2.color" + +[workspace.active_sidebar_icon] +color = "$text.0.color" + +[chat_panel] +channel_name = { extends = "$text.0", weight = "bold" } +channel_name_hash = { text = "$text.2", padding.right = 5 } +padding = 10 + +[chat_panel.message] +body = "$text.1" +sender = { extends = "$text.0", weight = "bold", margin.right = 10 } +timestamp = "$text.2" +padding.bottom = 10 + +[chat_panel.channel_select.item] +padding = 4 +name = "$text.1" +hash = { extends = "$text.2", margin.right = 5 } + +[chat_panel.channel_select.hovered_item] +extends = "$chat_panel.channel_select.item" +background = "$surface.2" +corner_radius = 6 + +[chat_panel.channel_select.active_item] +extends = "$chat_panel.channel_select.item" +name = "$text.0" + +[chat_panel.channel_select.hovered_active_item] +extends = "$chat_panel.channel_select.hovered_item" +name = "$text.0" + +[chat_panel.channel_select.header] +extends = "$chat_panel.channel_select.active_item" +padding.bottom = 0 +padding.left = 0 + +[chat_panel.channel_select.menu] +padding = 4 +corner_radius = 6 +border = { color = "#000000", width = 1 } +background = "$surface.0" + +[chat_panel.input_editor_container] +background = "$surface.1" +corner_radius = 6 +padding = 6 + +[chat_panel.input_editor] +text = "$text.1.color" +placeholder_text = "$text.2.color" +background = "$surface.1" +selection = "$selection.host" + +[chat_panel.sign_in_prompt] +extends = "$text.0" +underline = true + +[chat_panel.hovered_sign_in_prompt] +extends = "$chat_panel.sign_in_prompt" +color = "$text.1.color" + +[selector] +background = "$surface.2" +text = "$text.0" +padding = 6 +margin.top = 12 +corner_radius = 6 +shadow = { offset = [0, 0], blur = 12, color = "#00000088" } +input_editor = "$chat_panel.input_editor" + +[selector.item] background = "#424344" -text = "#cccccc" -highlight_text = { color = "#18a3ff", weight = "bold" } -border = { color = "#000000", width = 1.0 } -padding = { top = 6.0, bottom = 6.0, left = 6.0, right = 6.0 } +text = "$text.1" +highlight_text = { extends = "$text.base", color = "#18a3ff", weight = "bold" } +border = { color = "#000000", width = 1 } +padding = 6 -[ui.selector.active_item] -extends = "$ui.selector.item" +[selector.active_item] +extends = "$selector.item" background = "#094771" [editor] -background = "$surfaces.3" -gutter_background = "$surfaces.3" -active_line_background = "$surfaces.4" -line_number = "$text_colors.dull" -line_number_active = "$text_colors.bright" -text = "$text_colors.normal" -replicas = [ - { selection = "#264f78", cursor = "$text_colors.bright" }, - { selection = "#504f31", cursor = "#fcf154" }, -] +text = "$text.1.color" +background = "$surface.1" +gutter_background = "$surface.1" +active_line_background = "$surface.2" +line_number = "$text.2.color" +line_number_active = "$text.0.color" +selection = "$selection.host" +guest_selections = "$selection.guests" diff --git a/zed/assets/themes/dark.toml b/zed/assets/themes/dark.toml index 58872cc75970df0d62878017e0d4bd98a4e52285..6f7a8d6e80f432a9a766f6afac931855d78616c2 100644 --- a/zed/assets/themes/dark.toml +++ b/zed/assets/themes/dark.toml @@ -1,19 +1,29 @@ extends = "_base" -[surfaces] -1 = "#050101" +[surface] +0 = "#222324" +1 = "#141516" 2 = "#131415" -3 = "#1c1d1e" -4 = "#3a3b3c" -[text_colors] -dull = "#5a5a5b" -bright = "#ffffff" -normal = "#d4d4d4" +[text] +base = { family = "Inconsolata", size = 14 } +0 = { extends = "$text.base", color = "#ffffff" } +1 = { extends = "$text.base", color = "#b3b3b3" } +2 = { extends = "$text.base", color = "#7b7d80" } + +[selection] +host = { selection = "#264f78", cursor = "$text.0.color" } +guests = [{ selection = "#504f31", cursor = "#fcf154" }] + +[status] +good = "#4fac63" +info = "#3c5dd4" +warn = "#faca50" +bad = "#b7372e" [syntax] keyword = { color = "#0086c0", weight = "bold" } -function = "#dcdcaa" +function = { color = "#dcdcaa", underline = true } string = "#cb8f77" type = "#4ec9b0" number = "#b5cea8" diff --git a/zed/assets/themes/light.toml b/zed/assets/themes/light.toml index 1cdb2c51ddf9010f41c2283ac03d57a295aa6edf..94274997a14e49b7bfd90a41eee2491c8a095e4e 100644 --- a/zed/assets/themes/light.toml +++ b/zed/assets/themes/light.toml @@ -1,15 +1,26 @@ extends = "_base" -[surfaces] -1 = "#ffffff" -2 = "#f3f3f3" -3 = "#ececec" -4 = "#3a3b3c" +[surface] +0 = "#ffffff" +1 = "#f3f3f3" +2 = "#ececec" +3 = "#3a3b3c" -[text_colors] -dull = "#acacac" -bright = "#111111" -normal = "#333333" +[text] +base = { family = "Inconsolata", size = 14 } +0 = { extends = "$text.base", color = "#acacac" } +1 = { extends = "$text.base", color = "#111111" } +2 = { extends = "$text.base", color = "#333333" } + +[selection] +host = { selection = "#264f78", cursor = "$text.0.color" } +guests = [{ selection = "#504f31", cursor = "#fcf154" }] + +[status] +good = "#4fac63" +info = "#3c5dd4" +warn = "#faca50" +bad = "#b7372e" [syntax] keyword = "#0000fa" diff --git a/zed/languages/rust/highlights.scm b/zed/languages/rust/highlights.scm index 3276182cd609760ed0869a2ce8eb9e396794a4d7..116be758424216869f66772a09b11b9ab90f9da1 100644 --- a/zed/languages/rust/highlights.scm +++ b/zed/languages/rust/highlights.scm @@ -32,7 +32,7 @@ ; Assume all-caps names are constants ((identifier) @constant - (#match? @constant "^[A-Z][A-Z\\d_]+$'")) + (#match? @constant "^[A-Z][A-Z\\d_]+$")) [ "as" diff --git a/zed/src/assets.rs b/zed/src/assets.rs index e7c0103421620b6888ee2dd28b723ed22d4daa11..c0f3a1fbfc4726ef0cdbe0734518b3b3737c691e 100644 --- a/zed/src/assets.rs +++ b/zed/src/assets.rs @@ -4,11 +4,14 @@ use rust_embed::RustEmbed; #[derive(RustEmbed)] #[folder = "assets"] +#[exclude = "*.DS_Store"] pub struct Assets; impl AssetSource for Assets { fn load(&self, path: &str) -> Result> { - Self::get(path).ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path)) + Self::get(path) + .map(|f| f.data) + .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path)) } fn list(&self, path: &str) -> Vec> { diff --git a/zed/src/channel.rs b/zed/src/channel.rs new file mode 100644 index 0000000000000000000000000000000000000000..24997d49642648e5fc59c301270346cb75007a3b --- /dev/null +++ b/zed/src/channel.rs @@ -0,0 +1,708 @@ +use crate::{ + rpc::{self, Client}, + user::{User, UserStore}, + util::TryFutureExt, +}; +use anyhow::{anyhow, Context, Result}; +use gpui::{ + sum_tree::{self, Bias, SumTree}, + Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle, +}; +use postage::prelude::Stream; +use std::{ + collections::{HashMap, HashSet}, + ops::Range, + sync::Arc, +}; +use time::OffsetDateTime; +use zrpc::{ + proto::{self, ChannelMessageSent}, + TypedEnvelope, +}; + +pub struct ChannelList { + available_channels: Option>, + channels: HashMap>, + rpc: Arc, + user_store: Arc, + _task: Task>, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct ChannelDetails { + pub id: u64, + pub name: String, +} + +pub struct Channel { + details: ChannelDetails, + messages: SumTree, + loaded_all_messages: bool, + pending_messages: Vec, + next_local_message_id: u64, + user_store: Arc, + rpc: Arc, + _subscription: rpc::Subscription, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct ChannelMessage { + pub id: u64, + pub body: String, + pub timestamp: OffsetDateTime, + pub sender: Arc, +} + +pub struct PendingChannelMessage { + pub body: String, + local_id: u64, +} + +#[derive(Clone, Debug, Default)] +pub struct ChannelMessageSummary { + max_id: u64, + count: usize, +} + +#[derive(Copy, Clone, Debug, Default)] +struct Count(usize); + +pub enum ChannelListEvent {} + +#[derive(Clone, Debug, PartialEq)] +pub enum ChannelEvent { + MessagesAdded { + old_range: Range, + new_count: usize, + }, +} + +impl Entity for ChannelList { + type Event = ChannelListEvent; +} + +impl ChannelList { + pub fn new( + user_store: Arc, + rpc: Arc, + cx: &mut ModelContext, + ) -> Self { + let _task = cx.spawn(|this, mut cx| { + let rpc = rpc.clone(); + async move { + let mut user_id = rpc.user_id(); + loop { + let available_channels = if user_id.recv().await.unwrap().is_some() { + Some( + rpc.request(proto::GetChannels {}) + .await + .context("failed to fetch available channels")? + .channels + .into_iter() + .map(Into::into) + .collect(), + ) + } else { + None + }; + + this.update(&mut cx, |this, cx| { + if available_channels.is_none() { + if this.available_channels.is_none() { + return; + } + this.channels.clear(); + } + this.available_channels = available_channels; + cx.notify(); + }); + } + } + .log_err() + }); + + Self { + available_channels: None, + channels: Default::default(), + user_store, + rpc, + _task, + } + } + + pub fn available_channels(&self) -> Option<&[ChannelDetails]> { + self.available_channels.as_ref().map(Vec::as_slice) + } + + pub fn get_channel( + &mut self, + id: u64, + cx: &mut MutableAppContext, + ) -> Option> { + if let Some(channel) = self.channels.get(&id).and_then(|c| c.upgrade(cx)) { + return Some(channel); + } + + let channels = self.available_channels.as_ref()?; + let details = channels.iter().find(|details| details.id == id)?.clone(); + let channel = + cx.add_model(|cx| Channel::new(details, self.user_store.clone(), self.rpc.clone(), cx)); + self.channels.insert(id, channel.downgrade()); + Some(channel) + } +} + +impl Entity for Channel { + type Event = ChannelEvent; + + fn release(&mut self, cx: &mut MutableAppContext) { + let rpc = self.rpc.clone(); + let channel_id = self.details.id; + cx.foreground() + .spawn(async move { + if let Err(error) = rpc.send(proto::LeaveChannel { channel_id }).await { + log::error!("error leaving channel: {}", error); + }; + }) + .detach() + } +} + +impl Channel { + pub fn new( + details: ChannelDetails, + user_store: Arc, + rpc: Arc, + cx: &mut ModelContext, + ) -> Self { + let _subscription = rpc.subscribe_from_model(details.id, cx, Self::handle_message_sent); + + { + let user_store = user_store.clone(); + let rpc = rpc.clone(); + let channel_id = details.id; + cx.spawn(|channel, mut cx| { + async move { + let response = rpc.request(proto::JoinChannel { channel_id }).await?; + let messages = messages_from_proto(response.messages, &user_store).await?; + let loaded_all_messages = response.done; + + channel.update(&mut cx, |channel, cx| { + channel.insert_messages(messages, cx); + channel.loaded_all_messages = loaded_all_messages; + }); + + Ok(()) + } + .log_err() + }) + .detach(); + } + + Self { + details, + user_store, + rpc, + messages: Default::default(), + pending_messages: Default::default(), + loaded_all_messages: false, + next_local_message_id: 0, + _subscription, + } + } + + pub fn name(&self) -> &str { + &self.details.name + } + + pub fn send_message( + &mut self, + body: String, + cx: &mut ModelContext, + ) -> Result>> { + if body.is_empty() { + Err(anyhow!("message body can't be empty"))?; + } + + let channel_id = self.details.id; + let local_id = self.next_local_message_id; + self.next_local_message_id += 1; + self.pending_messages.push(PendingChannelMessage { + local_id, + body: body.clone(), + }); + let user_store = self.user_store.clone(); + let rpc = self.rpc.clone(); + Ok(cx.spawn(|this, mut cx| async move { + let request = rpc.request(proto::SendChannelMessage { channel_id, body }); + let response = request.await?; + let message = ChannelMessage::from_proto( + response.message.ok_or_else(|| anyhow!("invalid message"))?, + &user_store, + ) + .await?; + this.update(&mut cx, |this, cx| { + if let Ok(i) = this + .pending_messages + .binary_search_by_key(&local_id, |msg| msg.local_id) + { + this.pending_messages.remove(i); + this.insert_messages(SumTree::from_item(message, &()), cx); + } + Ok(()) + }) + })) + } + + pub fn load_more_messages(&mut self, cx: &mut ModelContext) -> bool { + if !self.loaded_all_messages { + let rpc = self.rpc.clone(); + let user_store = self.user_store.clone(); + let channel_id = self.details.id; + if let Some(before_message_id) = self.messages.first().map(|message| message.id) { + cx.spawn(|this, mut cx| { + async move { + let response = rpc + .request(proto::GetChannelMessages { + channel_id, + before_message_id, + }) + .await?; + let loaded_all_messages = response.done; + let messages = messages_from_proto(response.messages, &user_store).await?; + this.update(&mut cx, |this, cx| { + this.loaded_all_messages = loaded_all_messages; + this.insert_messages(messages, cx); + }); + Ok(()) + } + .log_err() + }) + .detach(); + return true; + } + } + false + } + + pub fn message_count(&self) -> usize { + self.messages.summary().count + } + + pub fn messages(&self) -> &SumTree { + &self.messages + } + + pub fn message(&self, ix: usize) -> &ChannelMessage { + let mut cursor = self.messages.cursor::(); + cursor.seek(&Count(ix), Bias::Right, &()); + cursor.item().unwrap() + } + + pub fn messages_in_range(&self, range: Range) -> impl Iterator { + let mut cursor = self.messages.cursor::(); + cursor.seek(&Count(range.start), Bias::Right, &()); + cursor.take(range.len()) + } + + pub fn pending_messages(&self) -> &[PendingChannelMessage] { + &self.pending_messages + } + + fn handle_message_sent( + &mut self, + message: TypedEnvelope, + _: Arc, + cx: &mut ModelContext, + ) -> Result<()> { + let user_store = self.user_store.clone(); + let message = message + .payload + .message + .ok_or_else(|| anyhow!("empty message"))?; + + cx.spawn(|this, mut cx| { + async move { + let message = ChannelMessage::from_proto(message, &user_store).await?; + this.update(&mut cx, |this, cx| { + this.insert_messages(SumTree::from_item(message, &()), cx) + }); + Ok(()) + } + .log_err() + }) + .detach(); + Ok(()) + } + + fn insert_messages(&mut self, messages: SumTree, cx: &mut ModelContext) { + if let Some((first_message, last_message)) = messages.first().zip(messages.last()) { + let mut old_cursor = self.messages.cursor::(); + let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left, &()); + let start_ix = old_cursor.sum_start().0; + let removed_messages = old_cursor.slice(&last_message.id, Bias::Right, &()); + let removed_count = removed_messages.summary().count; + let new_count = messages.summary().count; + let end_ix = start_ix + removed_count; + + new_messages.push_tree(messages, &()); + new_messages.push_tree(old_cursor.suffix(&()), &()); + drop(old_cursor); + self.messages = new_messages; + + cx.emit(ChannelEvent::MessagesAdded { + old_range: start_ix..end_ix, + new_count, + }); + cx.notify(); + } + } +} + +async fn messages_from_proto( + proto_messages: Vec, + user_store: &UserStore, +) -> Result> { + let unique_user_ids = proto_messages + .iter() + .map(|m| m.sender_id) + .collect::>() + .into_iter() + .collect(); + user_store.load_users(unique_user_ids).await?; + + let mut messages = Vec::with_capacity(proto_messages.len()); + for message in proto_messages { + messages.push(ChannelMessage::from_proto(message, &user_store).await?); + } + let mut result = SumTree::new(); + result.extend(messages, &()); + Ok(result) +} + +impl From for ChannelDetails { + fn from(message: proto::Channel) -> Self { + Self { + id: message.id, + name: message.name, + } + } +} + +impl ChannelMessage { + pub async fn from_proto( + message: proto::ChannelMessage, + user_store: &UserStore, + ) -> Result { + let sender = user_store.get_user(message.sender_id).await?; + Ok(ChannelMessage { + id: message.id, + body: message.body, + timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?, + sender, + }) + } +} + +impl sum_tree::Item for ChannelMessage { + type Summary = ChannelMessageSummary; + + fn summary(&self) -> Self::Summary { + ChannelMessageSummary { + max_id: self.id, + count: 1, + } + } +} + +impl sum_tree::Summary for ChannelMessageSummary { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &()) { + self.max_id = summary.max_id; + self.count += summary.count; + } +} + +impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for u64 { + fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) { + debug_assert!(summary.max_id > *self); + *self = summary.max_id; + } +} + +impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count { + fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) { + self.0 += summary.count; + } +} + +impl<'a> sum_tree::SeekDimension<'a, ChannelMessageSummary> for Count { + fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering { + Ord::cmp(&self.0, &other.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::TestAppContext; + use postage::mpsc::Receiver; + use zrpc::{test::Channel, ConnectionId, Peer, Receipt}; + + #[gpui::test] + async fn test_channel_messages(mut cx: TestAppContext) { + let user_id = 5; + let client = Client::new(); + let mut server = FakeServer::for_client(user_id, &client, &cx).await; + let user_store = Arc::new(UserStore::new(client.clone())); + + let channel_list = cx.add_model(|cx| ChannelList::new(user_store, client.clone(), cx)); + channel_list.read_with(&cx, |list, _| assert_eq!(list.available_channels(), None)); + + // Get the available channels. + let get_channels = server.receive::().await; + server + .respond( + get_channels.receipt(), + proto::GetChannelsResponse { + channels: vec![proto::Channel { + id: 5, + name: "the-channel".to_string(), + }], + }, + ) + .await; + channel_list.next_notification(&cx).await; + channel_list.read_with(&cx, |list, _| { + assert_eq!( + list.available_channels().unwrap(), + &[ChannelDetails { + id: 5, + name: "the-channel".into(), + }] + ) + }); + + // Join a channel and populate its existing messages. + let channel = channel_list + .update(&mut cx, |list, cx| { + let channel_id = list.available_channels().unwrap()[0].id; + list.get_channel(channel_id, cx) + }) + .unwrap(); + channel.read_with(&cx, |channel, _| assert!(channel.messages().is_empty())); + let join_channel = server.receive::().await; + server + .respond( + join_channel.receipt(), + proto::JoinChannelResponse { + messages: vec![ + proto::ChannelMessage { + id: 10, + body: "a".into(), + timestamp: 1000, + sender_id: 5, + }, + proto::ChannelMessage { + id: 11, + body: "b".into(), + timestamp: 1001, + sender_id: 6, + }, + ], + done: false, + }, + ) + .await; + + // Client requests all users for the received messages + let mut get_users = server.receive::().await; + get_users.payload.user_ids.sort(); + assert_eq!(get_users.payload.user_ids, vec![5, 6]); + server + .respond( + get_users.receipt(), + proto::GetUsersResponse { + users: vec![ + proto::User { + id: 5, + github_login: "nathansobo".into(), + avatar_url: "http://avatar.com/nathansobo".into(), + }, + proto::User { + id: 6, + github_login: "maxbrunsfeld".into(), + avatar_url: "http://avatar.com/maxbrunsfeld".into(), + }, + ], + }, + ) + .await; + + assert_eq!( + channel.next_event(&cx).await, + ChannelEvent::MessagesAdded { + old_range: 0..0, + new_count: 2, + } + ); + channel.read_with(&cx, |channel, _| { + assert_eq!( + channel + .messages_in_range(0..2) + .map(|message| (message.sender.github_login.clone(), message.body.clone())) + .collect::>(), + &[ + ("nathansobo".into(), "a".into()), + ("maxbrunsfeld".into(), "b".into()) + ] + ); + }); + + // Receive a new message. + server + .send(proto::ChannelMessageSent { + channel_id: channel.read_with(&cx, |channel, _| channel.details.id), + message: Some(proto::ChannelMessage { + id: 12, + body: "c".into(), + timestamp: 1002, + sender_id: 7, + }), + }) + .await; + + // Client requests user for message since they haven't seen them yet + let get_users = server.receive::().await; + assert_eq!(get_users.payload.user_ids, vec![7]); + server + .respond( + get_users.receipt(), + proto::GetUsersResponse { + users: vec![proto::User { + id: 7, + github_login: "as-cii".into(), + avatar_url: "http://avatar.com/as-cii".into(), + }], + }, + ) + .await; + + assert_eq!( + channel.next_event(&cx).await, + ChannelEvent::MessagesAdded { + old_range: 2..2, + new_count: 1, + } + ); + channel.read_with(&cx, |channel, _| { + assert_eq!( + channel + .messages_in_range(2..3) + .map(|message| (message.sender.github_login.clone(), message.body.clone())) + .collect::>(), + &[("as-cii".into(), "c".into())] + ) + }); + + // Scroll up to view older messages. + channel.update(&mut cx, |channel, cx| { + assert!(channel.load_more_messages(cx)); + }); + let get_messages = server.receive::().await; + assert_eq!(get_messages.payload.channel_id, 5); + assert_eq!(get_messages.payload.before_message_id, 10); + server + .respond( + get_messages.receipt(), + proto::GetChannelMessagesResponse { + done: true, + messages: vec![ + proto::ChannelMessage { + id: 8, + body: "y".into(), + timestamp: 998, + sender_id: 5, + }, + proto::ChannelMessage { + id: 9, + body: "z".into(), + timestamp: 999, + sender_id: 6, + }, + ], + }, + ) + .await; + + assert_eq!( + channel.next_event(&cx).await, + ChannelEvent::MessagesAdded { + old_range: 0..0, + new_count: 2, + } + ); + channel.read_with(&cx, |channel, _| { + assert_eq!( + channel + .messages_in_range(0..2) + .map(|message| (message.sender.github_login.clone(), message.body.clone())) + .collect::>(), + &[ + ("nathansobo".into(), "y".into()), + ("maxbrunsfeld".into(), "z".into()) + ] + ); + }); + } + + struct FakeServer { + peer: Arc, + incoming: Receiver>, + connection_id: ConnectionId, + } + + impl FakeServer { + async fn for_client(user_id: u64, client: &Arc, cx: &TestAppContext) -> Self { + let (client_conn, server_conn) = Channel::bidirectional(); + let peer = Peer::new(); + let (connection_id, io, incoming) = peer.add_connection(server_conn).await; + cx.background().spawn(io).detach(); + + client + .add_connection(user_id, client_conn, &cx.to_async()) + .await + .unwrap(); + + Self { + peer, + incoming, + connection_id, + } + } + + async fn send(&self, message: T) { + self.peer.send(self.connection_id, message).await.unwrap(); + } + + async fn receive(&mut self) -> TypedEnvelope { + *self + .incoming + .recv() + .await + .unwrap() + .into_any() + .downcast::>() + .unwrap() + } + + async fn respond( + &self, + receipt: Receipt, + response: T::Response, + ) { + self.peer.respond(receipt, response).await.unwrap() + } + } +} diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs new file mode 100644 index 0000000000000000000000000000000000000000..c8aaac4f38845b4a52bf4261b76ad4f50d6d8c60 --- /dev/null +++ b/zed/src/chat_panel.rs @@ -0,0 +1,421 @@ +use std::sync::Arc; + +use crate::{ + channel::{Channel, ChannelEvent, ChannelList, ChannelMessage}, + editor::Editor, + rpc::Client, + theme, + util::{ResultExt, TryFutureExt}, + Settings, +}; +use gpui::{ + action, + elements::*, + keymap::Binding, + platform::CursorStyle, + views::{ItemType, Select, SelectStyle}, + AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, View, + ViewContext, ViewHandle, +}; +use postage::watch; +use time::{OffsetDateTime, UtcOffset}; + +const MESSAGE_LOADING_THRESHOLD: usize = 50; + +pub struct ChatPanel { + rpc: Arc, + channel_list: ModelHandle, + active_channel: Option<(ModelHandle, Subscription)>, + message_list: ListState, + input_editor: ViewHandle, + channel_select: ViewHandle