From 96ade8668f8317a44a539e21c959454f6e1aadc4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Sep 2021 16:48:44 +0200 Subject: [PATCH 01/20] Start on image rendering --- Cargo.lock | 65 +++++++- gpui/Cargo.toml | 1 + gpui/src/elements.rs | 24 +-- gpui/src/elements/image.rs | 65 ++++++++ gpui/src/image_data.rs | 31 ++++ gpui/src/lib.rs | 2 + gpui/src/platform/mac/atlas.rs | 84 ++++++++-- gpui/src/platform/mac/renderer.rs | 173 +++++++++++++++++--- gpui/src/platform/mac/shaders/shaders.h | 51 ++++-- gpui/src/platform/mac/shaders/shaders.metal | 33 ++++ gpui/src/scene.rs | 22 ++- zed/Cargo.toml | 1 + zed/src/workspace.rs | 33 ++-- 13 files changed, 510 insertions(+), 75 deletions(-) create mode 100644 gpui/src/elements/image.rs create mode 100644 gpui/src/image_data.rs diff --git a/Cargo.lock b/Cargo.lock index 562da86feec01872e2eb71718d775b41f454dde7..c749b35f2dc5e329fea6b282a3cab690f7156e6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -814,7 +814,7 @@ dependencies = [ "error-chain", "glob 0.2.11", "icns", - "image", + "image 0.12.4", "libflate", "md5", "msi", @@ -2102,6 +2102,16 @@ dependencies = [ "lzw", ] +[[package]] +name = "gif" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a668f699973d0f573d15749b7002a9ac9e1f9c6b220e7b165601334c173d8de" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.23.0" @@ -2167,6 +2177,7 @@ dependencies = [ "font-kit", "foreign-types", "gpui_macros", + "image 0.23.14", "lazy_static", "log", "metal", @@ -2462,15 +2473,34 @@ checksum = "d95816db758249fe16f23a4e23f1a3a817fe11892dbfd1c5836f625324702158" dependencies = [ "byteorder", "enum_primitive", - "gif", + "gif 0.9.2", "jpeg-decoder", "num-iter", - "num-rational", + "num-rational 0.1.42", "num-traits 0.1.43", "png 0.6.2", "scoped_threadpool", ] +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif 0.11.2", + "jpeg-decoder", + "num-iter", + "num-rational 0.3.2", + "num-traits 0.2.14", + "png 0.16.8", + "scoped_threadpool", + "tiff", +] + [[package]] name = "indexmap" version = "1.6.2" @@ -3014,6 +3044,17 @@ dependencies = [ "num-traits 0.2.14", ] +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg 1.0.1", + "num-integer", + "num-traits 0.2.14", +] + [[package]] name = "num-traits" version = "0.1.43" @@ -5129,6 +5170,17 @@ dependencies = [ "tide", ] +[[package]] +name = "tiff" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +dependencies = [ + "jpeg-decoder", + "miniz_oxide 0.4.4", + "weezl", +] + [[package]] name = "time" version = "0.1.44" @@ -5694,6 +5746,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "weezl" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e" + [[package]] name = "wepoll-sys" version = "3.0.1" @@ -5836,6 +5894,7 @@ dependencies = [ "gpui", "http-auth-basic", "ignore", + "image 0.23.14", "lazy_static", "libc", "log", diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 11a855cd40c087c4aed6fc278adfbdb7ded92952..7cc202e0b642dd6a22caa58b6008b8f9dee7b03c 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -11,6 +11,7 @@ backtrace = "0.3" ctor = "0.1" etagere = "0.2" gpui_macros = { path = "../gpui_macros" } +image = "0.23" lazy_static = "1.4.0" log = "0.4" num_cpus = "1.13" diff --git a/gpui/src/elements.rs b/gpui/src/elements.rs index 6d7429222c7c1bf74d259932cbc5b3b614892ce3..252cebd7334f57874a8fd6164da15ad0ba3c2986 100644 --- a/gpui/src/elements.rs +++ b/gpui/src/elements.rs @@ -6,6 +6,7 @@ mod empty; mod event_handler; mod flex; mod hook; +mod image; mod label; mod line_box; mod list; @@ -16,25 +17,12 @@ mod svg; mod text; mod uniform_list; +pub use self::{ + align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*, + hook::*, image::*, label::*, line_box::*, list::*, mouse_event_handler::*, overlay::*, + stack::*, svg::*, text::*, uniform_list::*, +}; pub use crate::presenter::ChildView; -pub use align::*; -pub use canvas::*; -pub use constrained_box::*; -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, DebugContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, diff --git a/gpui/src/elements/image.rs b/gpui/src/elements/image.rs new file mode 100644 index 0000000000000000000000000000000000000000..44f39d8a3078173ac64f946f6c6a18cf4b4d5684 --- /dev/null +++ b/gpui/src/elements/image.rs @@ -0,0 +1,65 @@ +use crate::{ + geometry::{rect::RectF, vector::Vector2F}, + json::{json, ToJson}, + scene, DebugContext, Element, Event, EventContext, ImageData, LayoutContext, PaintContext, + SizeConstraint, +}; +use std::sync::Arc; + +pub struct Image(Arc); + +impl Image { + pub fn new(data: Arc) -> Self { + Self(data) + } +} + +impl Element for Image { + type LayoutState = (); + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + _: &mut LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + (constraint.max, ()) + } + + fn paint( + &mut self, + bounds: RectF, + _: RectF, + _: &mut Self::LayoutState, + cx: &mut PaintContext, + ) -> Self::PaintState { + cx.scene.push_image(scene::Image { + bounds, + data: self.0.clone(), + }); + } + + 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, + ) -> serde_json::Value { + json!({ + "type": "Image", + "bounds": bounds.to_json(), + }) + } +} diff --git a/gpui/src/image_data.rs b/gpui/src/image_data.rs new file mode 100644 index 0000000000000000000000000000000000000000..352393e3b57328f8373134c23edcb9b9ed5790ae --- /dev/null +++ b/gpui/src/image_data.rs @@ -0,0 +1,31 @@ +use crate::geometry::vector::{vec2i, Vector2I}; +use image::{Bgra, ImageBuffer}; +use std::sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, +}; + +pub struct ImageData { + pub id: usize, + data: ImageBuffer, Vec>, +} + +impl ImageData { + pub fn new(data: ImageBuffer, Vec>) -> Arc { + static NEXT_ID: AtomicUsize = AtomicUsize::new(0); + + Arc::new(Self { + id: NEXT_ID.fetch_add(1, SeqCst), + data, + }) + } + + pub fn as_bytes(&self) -> &[u8] { + &self.data + } + + pub fn size(&self) -> Vector2I { + let (width, height) = self.data.dimensions(); + vec2i(width as i32, height as i32) + } +} diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index 6cb1c6f39d7d5caee1b94516461a371335924231..4b4d5f25d55060585df09f267c9bad55a258ced2 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -7,6 +7,8 @@ mod test; pub use assets::*; pub mod elements; pub mod font_cache; +mod image_data; +pub use crate::image_data::ImageData; pub mod views; pub use font_cache::FontCache; mod clipboard; diff --git a/gpui/src/platform/mac/atlas.rs b/gpui/src/platform/mac/atlas.rs index c9d910a586d686b3691f8710fa49c85923b67886..e23a045d47e4ea5f28bda6e1c951a2724a9f0258 100644 --- a/gpui/src/platform/mac/atlas.rs +++ b/gpui/src/platform/mac/atlas.rs @@ -1,4 +1,7 @@ -use crate::geometry::vector::{vec2i, Vector2I}; +use crate::geometry::{ + rect::RectI, + vector::{vec2i, Vector2I}, +}; use etagere::BucketedAtlasAllocator; use foreign_types::ForeignType; use metal::{self, Device, TextureDescriptor}; @@ -11,6 +14,12 @@ pub struct AtlasAllocator { free_atlases: Vec, } +#[derive(Copy, Clone)] +pub struct AllocId { + pub atlas_id: usize, + alloc_id: etagere::AllocId, +} + impl AtlasAllocator { pub fn new(device: Device, texture_descriptor: TextureDescriptor) -> Self { let mut me = Self { @@ -31,20 +40,40 @@ impl AtlasAllocator { ) } - pub fn allocate(&mut self, requested_size: Vector2I) -> anyhow::Result<(usize, Vector2I)> { - let origin = self + pub fn allocate(&mut self, requested_size: Vector2I) -> (AllocId, Vector2I) { + let (alloc_id, origin) = self .atlases .last_mut() .unwrap() .allocate(requested_size) .unwrap_or_else(|| { let mut atlas = self.new_atlas(requested_size); - let origin = atlas.allocate(requested_size).unwrap(); + let (id, origin) = atlas.allocate(requested_size).unwrap(); self.atlases.push(atlas); - origin + (id, origin) }); - Ok((self.atlases.len() - 1, origin)) + let id = AllocId { + atlas_id: self.atlases.len() - 1, + alloc_id, + }; + (id, origin) + } + + pub fn upload(&mut self, size: Vector2I, bytes: &[u8]) -> (AllocId, RectI) { + let (alloc_id, origin) = self.allocate(size); + let bounds = RectI::new(origin, size); + self.atlases[alloc_id.atlas_id].upload(bounds, bytes); + (alloc_id, bounds) + } + + pub fn deallocate(&mut self, id: AllocId) { + if let Some(atlas) = self.atlases.get_mut(id.atlas_id) { + atlas.deallocate(id.alloc_id); + if atlas.is_empty() { + self.free_atlases.push(self.atlases.remove(id.atlas_id)); + } + } } pub fn clear(&mut self) { @@ -102,13 +131,44 @@ impl Atlas { vec2i(size.width, size.height) } - fn allocate(&mut self, size: Vector2I) -> Option { - let origin = self + fn allocate(&mut self, size: Vector2I) -> Option<(etagere::AllocId, Vector2I)> { + let alloc = self .allocator - .allocate(etagere::Size::new(size.x(), size.y()))? - .rectangle - .min; - Some(vec2i(origin.x, origin.y)) + .allocate(etagere::Size::new(size.x(), size.y()))?; + let origin = alloc.rectangle.min; + Some((alloc.id, vec2i(origin.x, origin.y))) + } + + fn upload(&mut self, bounds: RectI, bytes: &[u8]) { + let region = metal::MTLRegion::new_2d( + bounds.origin().x() as u64, + bounds.origin().y() as u64, + bounds.size().x() as u64, + bounds.size().y() as u64, + ); + self.texture.replace_region( + region, + 0, + bytes.as_ptr() as *const _, + (bounds.size().x() * self.bytes_per_pixel() as i32) as u64, + ); + } + + fn bytes_per_pixel(&self) -> u8 { + use metal::MTLPixelFormat::*; + match self.texture.pixel_format() { + A8Unorm | R8Unorm => 1, + RGBA8Unorm | BGRA8Unorm => 4, + _ => unimplemented!(), + } + } + + fn deallocate(&mut self, id: etagere::AllocId) { + self.allocator.deallocate(id); + } + + fn is_empty(&self) -> bool { + self.allocator.is_empty() } fn clear(&mut self) { diff --git a/gpui/src/platform/mac/renderer.rs b/gpui/src/platform/mac/renderer.rs index e12a52d6134dbd2c5d110a570f52920c449a287d..215863d881c9f2bce84e6b6cbb20fe16b0481447 100644 --- a/gpui/src/platform/mac/renderer.rs +++ b/gpui/src/platform/mac/renderer.rs @@ -1,13 +1,15 @@ -use super::{atlas::AtlasAllocator, sprite_cache::SpriteCache}; +use super::{ + atlas::{self, AtlasAllocator}, + sprite_cache::SpriteCache, +}; use crate::{ color::Color, geometry::{ - rect::RectF, + rect::{RectF, RectI}, vector::{vec2f, vec2i, Vector2F}, }, platform, - scene::{Glyph, Icon, Layer, Quad, Shadow}, - Scene, + scene::{Glyph, Icon, Image, Layer, Quad, Scene, Shadow}, }; use cocoa::foundation::NSUInteger; use metal::{MTLPixelFormat, MTLResourceOptions, NSRange}; @@ -21,9 +23,13 @@ const INSTANCE_BUFFER_SIZE: usize = 1024 * 1024; // This is an arbitrary decisio pub struct Renderer { sprite_cache: SpriteCache, path_atlases: AtlasAllocator, + image_atlases: AtlasAllocator, + prev_rendered_images: HashMap, + curr_rendered_images: HashMap, quad_pipeline_state: metal::RenderPipelineState, shadow_pipeline_state: metal::RenderPipelineState, sprite_pipeline_state: metal::RenderPipelineState, + image_pipeline_state: metal::RenderPipelineState, path_atlas_pipeline_state: metal::RenderPipelineState, unit_vertices: metal::Buffer, instances: metal::Buffer, @@ -64,7 +70,10 @@ impl Renderer { ); let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), fonts); - let path_atlases = build_path_atlas_allocator(MTLPixelFormat::R8Unorm, &device); + let path_atlases = + AtlasAllocator::new(device.clone(), build_path_atlas_texture_descriptor()); + let image_atlases = + AtlasAllocator::new(device.clone(), build_image_atlas_texture_descriptor()); let quad_pipeline_state = build_pipeline_state( &device, &library, @@ -89,6 +98,14 @@ impl Renderer { "sprite_fragment", pixel_format, ); + let image_pipeline_state = build_pipeline_state( + &device, + &library, + "image", + "image_vertex", + "image_fragment", + pixel_format, + ); let path_atlas_pipeline_state = build_path_atlas_pipeline_state( &device, &library, @@ -100,9 +117,13 @@ impl Renderer { Self { sprite_cache, path_atlases, + image_atlases, + prev_rendered_images: Default::default(), + curr_rendered_images: Default::default(), quad_pipeline_state, shadow_pipeline_state, sprite_pipeline_state, + image_pipeline_state, path_atlas_pipeline_state, unit_vertices, instances, @@ -117,6 +138,12 @@ impl Renderer { output: &metal::TextureRef, ) { let mut offset = 0; + + mem::swap( + &mut self.curr_rendered_images, + &mut self.prev_rendered_images, + ); + let path_sprites = self.render_path_atlases(scene, &mut offset, command_buffer); self.render_layers( scene, @@ -130,6 +157,11 @@ impl Renderer { location: 0, length: offset as NSUInteger, }); + + for (id, _) in self.prev_rendered_images.values() { + self.image_atlases.deallocate(*id); + } + self.prev_rendered_images.clear(); } fn render_path_atlases( @@ -146,11 +178,11 @@ impl Renderer { for path in layer.paths() { let origin = path.bounds.origin() * scene.scale_factor(); let size = (path.bounds.size() * scene.scale_factor()).ceil(); - let (atlas_id, atlas_origin) = self.path_atlases.allocate(size.to_i32()).unwrap(); + let (alloc_id, atlas_origin) = self.path_atlases.allocate(size.to_i32()); let atlas_origin = atlas_origin.to_f32(); sprites.push(PathSprite { layer_id, - atlas_id, + atlas_id: alloc_id.atlas_id, shader_data: shaders::GPUISprite { origin: origin.floor().to_float2(), target_size: size.to_float2(), @@ -162,7 +194,7 @@ impl Renderer { }); if let Some(current_atlas_id) = current_atlas_id { - if atlas_id != current_atlas_id { + if alloc_id.atlas_id != current_atlas_id { self.render_paths_to_atlas( offset, &vertices, @@ -173,7 +205,7 @@ impl Renderer { } } - current_atlas_id = Some(atlas_id); + current_atlas_id = Some(alloc_id.atlas_id); for vertex in &path.vertices { let xy_position = @@ -316,6 +348,13 @@ impl Renderer { drawable_size, command_encoder, ); + self.render_images( + layer.images(), + scale_factor, + offset, + drawable_size, + command_encoder, + ); self.render_quads( layer.underlines(), scale_factor, @@ -602,6 +641,97 @@ impl Renderer { } } + fn render_images( + &mut self, + images: &[Image], + scale_factor: f32, + offset: &mut usize, + drawable_size: Vector2F, + command_encoder: &metal::RenderCommandEncoderRef, + ) { + if images.is_empty() { + return; + } + + let mut images_by_atlas = HashMap::new(); + for image in images { + let origin = image.bounds.origin() * scale_factor; + let target_size = image.bounds.size() * scale_factor; + let (alloc_id, atlas_bounds) = self + .prev_rendered_images + .remove(&image.data.id) + .or_else(|| self.curr_rendered_images.get(&image.data.id).copied()) + .unwrap_or_else(|| { + self.image_atlases + .upload(image.data.size(), image.data.as_bytes()) + }); + self.curr_rendered_images + .insert(image.data.id, (alloc_id, atlas_bounds)); + images_by_atlas + .entry(alloc_id.atlas_id) + .or_insert_with(Vec::new) + .push(shaders::GPUIImage { + origin: origin.to_float2(), + target_size: target_size.to_float2(), + source_size: atlas_bounds.size().to_float2(), + atlas_origin: atlas_bounds.origin().to_float2(), + }); + } + + command_encoder.set_render_pipeline_state(&self.image_pipeline_state); + command_encoder.set_vertex_buffer( + shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexVertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_bytes( + shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexViewportSize as u64, + mem::size_of::() as u64, + [drawable_size.to_float2()].as_ptr() as *const c_void, + ); + + for (atlas_id, images) in images_by_atlas { + align_offset(offset); + let next_offset = *offset + images.len() * mem::size_of::(); + assert!( + next_offset <= INSTANCE_BUFFER_SIZE, + "instance buffer exhausted" + ); + + let texture = self.image_atlases.texture(atlas_id).unwrap(); + command_encoder.set_vertex_buffer( + shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexImages as u64, + Some(&self.instances), + *offset as u64, + ); + command_encoder.set_vertex_bytes( + shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexAtlasSize as u64, + mem::size_of::() as u64, + [vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr() + as *const c_void, + ); + command_encoder.set_fragment_texture( + shaders::GPUIImageFragmentInputIndex_GPUIImageFragmentInputIndexAtlas as u64, + Some(texture), + ); + + unsafe { + let buffer_contents = (self.instances.contents() as *mut u8) + .offset(*offset as isize) + as *mut shaders::GPUIImage; + std::ptr::copy_nonoverlapping(images.as_ptr(), buffer_contents, images.len()); + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + images.len() as u64, + ); + *offset = next_offset; + } + } + fn render_path_sprites( &mut self, layer_id: usize, @@ -708,19 +838,23 @@ impl Renderer { } } -fn build_path_atlas_allocator( - pixel_format: MTLPixelFormat, - device: &metal::Device, -) -> AtlasAllocator { +fn build_path_atlas_texture_descriptor() -> metal::TextureDescriptor { let texture_descriptor = metal::TextureDescriptor::new(); texture_descriptor.set_width(2048); texture_descriptor.set_height(2048); - texture_descriptor.set_pixel_format(pixel_format); + texture_descriptor.set_pixel_format(MTLPixelFormat::R8Unorm); texture_descriptor .set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead); texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private); - let path_atlases = AtlasAllocator::new(device.clone(), texture_descriptor); - path_atlases + texture_descriptor +} + +fn build_image_atlas_texture_descriptor() -> metal::TextureDescriptor { + let texture_descriptor = metal::TextureDescriptor::new(); + texture_descriptor.set_width(2048); + texture_descriptor.set_height(2048); + texture_descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm); + texture_descriptor } fn align_offset(offset: &mut usize) { @@ -803,9 +937,10 @@ mod shaders { #![allow(non_camel_case_types)] #![allow(non_snake_case)] - use pathfinder_geometry::vector::Vector2I; - - use crate::{color::Color, geometry::vector::Vector2F}; + use crate::{ + color::Color, + geometry::vector::{Vector2F, Vector2I}, + }; use std::mem; include!(concat!(env!("OUT_DIR"), "/shaders.rs")); diff --git a/gpui/src/platform/mac/shaders/shaders.h b/gpui/src/platform/mac/shaders/shaders.h index 5f49bfca64004ec67abc9c58c641b631cfd58375..b06e2e565fbb88ec7b2c4e3b580fd6d222b9cb1b 100644 --- a/gpui/src/platform/mac/shaders/shaders.h +++ b/gpui/src/platform/mac/shaders/shaders.h @@ -1,16 +1,19 @@ #include -typedef struct { +typedef struct +{ vector_float2 viewport_size; } GPUIUniforms; -typedef enum { +typedef enum +{ GPUIQuadInputIndexVertices = 0, GPUIQuadInputIndexQuads = 1, GPUIQuadInputIndexUniforms = 2, } GPUIQuadInputIndex; -typedef struct { +typedef struct +{ vector_float2 origin; vector_float2 size; vector_uchar4 background_color; @@ -22,13 +25,15 @@ typedef struct { float corner_radius; } GPUIQuad; -typedef enum { +typedef enum +{ GPUIShadowInputIndexVertices = 0, GPUIShadowInputIndexShadows = 1, GPUIShadowInputIndexUniforms = 2, } GPUIShadowInputIndex; -typedef struct { +typedef struct +{ vector_float2 origin; vector_float2 size; float corner_radius; @@ -36,18 +41,21 @@ typedef struct { vector_uchar4 color; } GPUIShadow; -typedef enum { +typedef enum +{ GPUISpriteVertexInputIndexVertices = 0, GPUISpriteVertexInputIndexSprites = 1, GPUISpriteVertexInputIndexViewportSize = 2, GPUISpriteVertexInputIndexAtlasSize = 3, } GPUISpriteVertexInputIndex; -typedef enum { +typedef enum +{ GPUISpriteFragmentInputIndexAtlas = 0, } GPUISpriteFragmentInputIndex; -typedef struct { +typedef struct +{ vector_float2 origin; vector_float2 target_size; vector_float2 source_size; @@ -56,14 +64,37 @@ typedef struct { uint8_t compute_winding; } GPUISprite; -typedef enum { +typedef enum +{ GPUIPathAtlasVertexInputIndexVertices = 0, GPUIPathAtlasVertexInputIndexAtlasSize = 1, } GPUIPathAtlasVertexInputIndex; -typedef struct { +typedef struct +{ vector_float2 xy_position; vector_float2 st_position; vector_float2 clip_rect_origin; vector_float2 clip_rect_size; } GPUIPathVertex; + +typedef enum +{ + GPUIImageVertexInputIndexVertices = 0, + GPUIImageVertexInputIndexImages = 1, + GPUIImageVertexInputIndexViewportSize = 2, + GPUIImageVertexInputIndexAtlasSize = 3, +} GPUIImageVertexInputIndex; + +typedef enum +{ + GPUIImageFragmentInputIndexAtlas = 0, +} GPUIImageFragmentInputIndex; + +typedef struct +{ + vector_float2 origin; + vector_float2 target_size; + vector_float2 source_size; + vector_float2 atlas_origin; +} GPUIImage; diff --git a/gpui/src/platform/mac/shaders/shaders.metal b/gpui/src/platform/mac/shaders/shaders.metal index 91e5ea129577d9443ee0f395c0e01df72f37b702..c83bac43ad59e3cb0d1acde7347aa55cab0962db 100644 --- a/gpui/src/platform/mac/shaders/shaders.metal +++ b/gpui/src/platform/mac/shaders/shaders.metal @@ -217,6 +217,39 @@ fragment float4 sprite_fragment( return color; } +struct ImageFragmentInput { + float4 position [[position]]; + float2 atlas_position; +}; + +vertex ImageFragmentInput image_vertex( + uint unit_vertex_id [[vertex_id]], + uint image_id [[instance_id]], + constant float2 *unit_vertices [[buffer(GPUIImageVertexInputIndexVertices)]], + constant GPUIImage *images [[buffer(GPUIImageVertexInputIndexImages)]], + constant float2 *viewport_size [[buffer(GPUIImageVertexInputIndexViewportSize)]], + constant float2 *atlas_size [[buffer(GPUIImageVertexInputIndexAtlasSize)]] +) { + float2 unit_vertex = unit_vertices[unit_vertex_id]; + GPUIImage image = images[image_id]; + float2 position = unit_vertex * image.target_size + image.origin; + float4 device_position = to_device_position(position, *viewport_size); + float2 atlas_position = (unit_vertex * image.source_size + image.atlas_origin) / *atlas_size; + + return ImageFragmentInput { + device_position, + atlas_position, + }; +} + +fragment float4 image_fragment( + ImageFragmentInput input [[stage_in]], + texture2d atlas [[ texture(GPUIImageFragmentInputIndexAtlas) ]] +) { + constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear); + return atlas.sample(atlas_sampler, input.atlas_position); +} + struct PathAtlasVertexOutput { float4 position [[position]]; float2 st_position; diff --git a/gpui/src/scene.rs b/gpui/src/scene.rs index 401918c5fe014426a98496d7f4e2d31b903bb274..ab6ca71a06465d9c1ef2a02260f543730734349c 100644 --- a/gpui/src/scene.rs +++ b/gpui/src/scene.rs @@ -1,12 +1,13 @@ use serde::Deserialize; use serde_json::json; -use std::borrow::Cow; +use std::{borrow::Cow, sync::Arc}; use crate::{ color::Color, fonts::{FontId, GlyphId}, geometry::{rect::RectF, vector::Vector2F}, json::ToJson, + ImageData, }; pub struct Scene { @@ -25,6 +26,7 @@ pub struct Layer { clip_bounds: Option, quads: Vec, underlines: Vec, + images: Vec, shadows: Vec, glyphs: Vec, icons: Vec, @@ -124,6 +126,11 @@ pub struct PathVertex { pub st_position: Vector2F, } +pub struct Image { + pub bounds: RectF, + pub data: Arc, +} + impl Scene { pub fn new(scale_factor: f32) -> Self { let stacking_context = StackingContext::new(None); @@ -166,6 +173,10 @@ impl Scene { self.active_layer().push_quad(quad) } + pub fn push_image(&mut self, image: Image) { + self.active_layer().push_image(image) + } + pub fn push_underline(&mut self, underline: Quad) { self.active_layer().push_underline(underline) } @@ -240,6 +251,7 @@ impl Layer { clip_bounds, quads: Vec::new(), underlines: Vec::new(), + images: Vec::new(), shadows: Vec::new(), glyphs: Vec::new(), icons: Vec::new(), @@ -267,6 +279,14 @@ impl Layer { self.underlines.as_slice() } + fn push_image(&mut self, image: Image) { + self.images.push(image); + } + + pub fn images(&self) -> &[Image] { + self.images.as_slice() + } + fn push_shadow(&mut self, shadow: Shadow) { self.shadows.push(shadow); } diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 985901c50cf8a14d4331ed73f89a98fd1323d28d..2b36db9ba86152b040fa6edcf74da5c6bc68a0d2 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -30,6 +30,7 @@ futures = "0.3" gpui = { path = "../gpui" } http-auth-basic = "0.1.3" ignore = "0.4" +image = "0.23" lazy_static = "1.4.0" libc = "0.2" log = "0.4" diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index df10aef54d641b316c8fc567963ee249f1f4abd6..92e05d7a8e75ac9c2d52dc2834b731127a257e6c 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -21,7 +21,7 @@ use gpui::{ json::to_string_pretty, keymap::Binding, platform::WindowOptions, - AnyViewHandle, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext, + AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, WeakModelHandle, }; @@ -354,10 +354,19 @@ pub struct Workspace { (usize, Arc), postage::watch::Receiver, Arc>>>, >, + image: Arc, } impl Workspace { pub fn new(app_state: &AppState, cx: &mut ViewContext) -> Self { + let image_bytes = crate::assets::Assets::get("images/as-cii.jpeg").unwrap(); + let image = image::io::Reader::new(std::io::Cursor::new(&*image_bytes.data)) + .with_guessed_format() + .unwrap() + .decode() + .unwrap() + .into_bgra8(); + let pane = cx.add_view(|_| Pane::new(app_state.settings.clone())); let pane_id = pane.id(); cx.subscribe(&pane, move |me, _, event, cx| { @@ -401,6 +410,7 @@ impl Workspace { worktrees: Default::default(), items: Default::default(), loading_items: Default::default(), + image: ImageData::new(image), } } @@ -954,17 +964,16 @@ impl View for Workspace { Flex::column() .with_child( ConstrainedBox::new( - Container::new( - Align::new( - Label::new( - "zed".into(), - theme.workspace.titlebar.label.clone() - ).boxed() - ) - .boxed() - ) - .with_style(&theme.workspace.titlebar.container) - .boxed(), + Image::new(self.image.clone()).boxed() + // Container::new( + // Align::new( + // Label::new("zed".into(), theme.workspace.titlebar.label.clone()) + // .boxed(), + // ) + // .boxed(), + // ) + // .with_style(&theme.workspace.titlebar.container) + // .boxed(), ) .with_height(32.) .named("titlebar"), From 95da665095297f5f58a09a3eb57045bdfcfec2a8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Sep 2021 17:49:11 +0200 Subject: [PATCH 02/20] Allow passing a corner radius and borders to rendered images Co-Authored-By: Nathan Sobo --- gpui/src/color.rs | 4 + gpui/src/elements/image.rs | 30 +++++- gpui/src/platform/mac/renderer.rs | 8 ++ gpui/src/platform/mac/shaders/shaders.h | 6 ++ gpui/src/platform/mac/shaders/shaders.metal | 101 +++++++++++--------- gpui/src/scene.rs | 2 + 6 files changed, 101 insertions(+), 50 deletions(-) diff --git a/gpui/src/color.rs b/gpui/src/color.rs index 9c6de6247a62c5fb96b793c1f6332e495eb442a2..9e31530b27f8b9f93c42df203aa88f99d9b9eff0 100644 --- a/gpui/src/color.rs +++ b/gpui/src/color.rs @@ -29,6 +29,10 @@ impl Color { Self(ColorU::white()) } + pub fn red() -> Self { + Self(ColorU::from_u32(0xff0000ff)) + } + pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { Self(ColorU::new(r, g, b, a)) } diff --git a/gpui/src/elements/image.rs b/gpui/src/elements/image.rs index 44f39d8a3078173ac64f946f6c6a18cf4b4d5684..2611478fe0b85e93ee9f55147e0b758e53427153 100644 --- a/gpui/src/elements/image.rs +++ b/gpui/src/elements/image.rs @@ -1,16 +1,34 @@ use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::{json, ToJson}, - scene, DebugContext, Element, Event, EventContext, ImageData, LayoutContext, PaintContext, - SizeConstraint, + scene, Border, DebugContext, Element, Event, EventContext, ImageData, LayoutContext, + PaintContext, SizeConstraint, }; use std::sync::Arc; -pub struct Image(Arc); +pub struct Image { + data: Arc, + border: Border, + corner_radius: f32, +} impl Image { pub fn new(data: Arc) -> Self { - Self(data) + Self { + data, + border: Default::default(), + corner_radius: Default::default(), + } + } + + pub fn with_corner_radius(mut self, corner_radius: f32) -> Self { + self.corner_radius = corner_radius; + self + } + + pub fn with_border(mut self, border: Border) -> Self { + self.border = border; + self } } @@ -35,7 +53,9 @@ impl Element for Image { ) -> Self::PaintState { cx.scene.push_image(scene::Image { bounds, - data: self.0.clone(), + border: self.border, + corner_radius: self.corner_radius, + data: self.data.clone(), }); } diff --git a/gpui/src/platform/mac/renderer.rs b/gpui/src/platform/mac/renderer.rs index 215863d881c9f2bce84e6b6cbb20fe16b0481447..6f712f29b3bbbfdf647e7b0ddeaebbcd43594b0c 100644 --- a/gpui/src/platform/mac/renderer.rs +++ b/gpui/src/platform/mac/renderer.rs @@ -657,6 +657,8 @@ impl Renderer { for image in images { let origin = image.bounds.origin() * scale_factor; let target_size = image.bounds.size() * scale_factor; + let corner_radius = image.corner_radius * scale_factor; + let border_width = image.border.width * scale_factor; let (alloc_id, atlas_bounds) = self .prev_rendered_images .remove(&image.data.id) @@ -675,6 +677,12 @@ impl Renderer { target_size: target_size.to_float2(), source_size: atlas_bounds.size().to_float2(), atlas_origin: atlas_bounds.origin().to_float2(), + border_top: border_width * (image.border.top as usize as f32), + border_right: border_width * (image.border.right as usize as f32), + border_bottom: border_width * (image.border.bottom as usize as f32), + border_left: border_width * (image.border.left as usize as f32), + border_color: image.border.color.to_uchar4(), + corner_radius, }); } diff --git a/gpui/src/platform/mac/shaders/shaders.h b/gpui/src/platform/mac/shaders/shaders.h index b06e2e565fbb88ec7b2c4e3b580fd6d222b9cb1b..1b6ad3f26f98122b11afeaff02006dd2ef0e8daa 100644 --- a/gpui/src/platform/mac/shaders/shaders.h +++ b/gpui/src/platform/mac/shaders/shaders.h @@ -97,4 +97,10 @@ typedef struct vector_float2 target_size; vector_float2 source_size; vector_float2 atlas_origin; + float border_top; + float border_right; + float border_bottom; + float border_left; + vector_uchar4 border_color; + float corner_radius; } GPUIImage; diff --git a/gpui/src/platform/mac/shaders/shaders.metal b/gpui/src/platform/mac/shaders/shaders.metal index c83bac43ad59e3cb0d1acde7347aa55cab0962db..00338b30e71c234147f88916f011f923221e11fc 100644 --- a/gpui/src/platform/mac/shaders/shaders.metal +++ b/gpui/src/platform/mac/shaders/shaders.metal @@ -34,46 +34,19 @@ float blur_along_x(float x, float y, float sigma, float corner, float2 halfSize) struct QuadFragmentInput { float4 position [[position]]; - vector_float2 origin; - vector_float2 size; - vector_uchar4 background_color; + float2 atlas_position; // only used in the image shader + float2 origin; + float2 size; + float4 background_color; float border_top; float border_right; float border_bottom; float border_left; - vector_uchar4 border_color; + float4 border_color; float corner_radius; }; -vertex QuadFragmentInput quad_vertex( - uint unit_vertex_id [[vertex_id]], - uint quad_id [[instance_id]], - constant float2 *unit_vertices [[buffer(GPUIQuadInputIndexVertices)]], - constant GPUIQuad *quads [[buffer(GPUIQuadInputIndexQuads)]], - constant GPUIUniforms *uniforms [[buffer(GPUIQuadInputIndexUniforms)]] -) { - float2 unit_vertex = unit_vertices[unit_vertex_id]; - GPUIQuad quad = quads[quad_id]; - float2 position = unit_vertex * quad.size + quad.origin; - float4 device_position = to_device_position(position, uniforms->viewport_size); - - return QuadFragmentInput { - device_position, - quad.origin, - quad.size, - quad.background_color, - quad.border_top, - quad.border_right, - quad.border_bottom, - quad.border_left, - quad.border_color, - quad.corner_radius, - }; -} - -fragment float4 quad_fragment( - QuadFragmentInput input [[stage_in]] -) { +float4 quad_sdf(QuadFragmentInput input) { float2 half_size = input.size / 2.; float2 center = input.origin + half_size; float2 center_to_point = input.position.xy - center; @@ -95,12 +68,12 @@ fragment float4 quad_fragment( float4 color; if (border_width == 0.) { - color = coloru_to_colorf(input.background_color); + color = input.background_color; } else { float inset_distance = distance + border_width; color = mix( - coloru_to_colorf(input.border_color), - coloru_to_colorf(input.background_color), + input.border_color, + input.background_color, saturate(0.5 - inset_distance) ); } @@ -109,6 +82,39 @@ fragment float4 quad_fragment( return coverage * color; } +vertex QuadFragmentInput quad_vertex( + uint unit_vertex_id [[vertex_id]], + uint quad_id [[instance_id]], + constant float2 *unit_vertices [[buffer(GPUIQuadInputIndexVertices)]], + constant GPUIQuad *quads [[buffer(GPUIQuadInputIndexQuads)]], + constant GPUIUniforms *uniforms [[buffer(GPUIQuadInputIndexUniforms)]] +) { + float2 unit_vertex = unit_vertices[unit_vertex_id]; + GPUIQuad quad = quads[quad_id]; + float2 position = unit_vertex * quad.size + quad.origin; + float4 device_position = to_device_position(position, uniforms->viewport_size); + + return QuadFragmentInput { + device_position, + float2(0., 0.), + quad.origin, + quad.size, + coloru_to_colorf(quad.background_color), + quad.border_top, + quad.border_right, + quad.border_bottom, + quad.border_left, + coloru_to_colorf(quad.border_color), + quad.corner_radius, + }; +} + +fragment float4 quad_fragment( + QuadFragmentInput input [[stage_in]] +) { + return quad_sdf(input); +} + struct ShadowFragmentInput { float4 position [[position]]; vector_float2 origin; @@ -217,12 +223,7 @@ fragment float4 sprite_fragment( return color; } -struct ImageFragmentInput { - float4 position [[position]]; - float2 atlas_position; -}; - -vertex ImageFragmentInput image_vertex( +vertex QuadFragmentInput image_vertex( uint unit_vertex_id [[vertex_id]], uint image_id [[instance_id]], constant float2 *unit_vertices [[buffer(GPUIImageVertexInputIndexVertices)]], @@ -236,18 +237,28 @@ vertex ImageFragmentInput image_vertex( float4 device_position = to_device_position(position, *viewport_size); float2 atlas_position = (unit_vertex * image.source_size + image.atlas_origin) / *atlas_size; - return ImageFragmentInput { + return QuadFragmentInput { device_position, atlas_position, + image.origin, + image.target_size, + float4(0.), + image.border_top, + image.border_right, + image.border_bottom, + image.border_left, + coloru_to_colorf(image.border_color), + image.corner_radius, }; } fragment float4 image_fragment( - ImageFragmentInput input [[stage_in]], + QuadFragmentInput input [[stage_in]], texture2d atlas [[ texture(GPUIImageFragmentInputIndexAtlas) ]] ) { constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear); - return atlas.sample(atlas_sampler, input.atlas_position); + input.background_color = atlas.sample(atlas_sampler, input.atlas_position); + return quad_sdf(input); } struct PathAtlasVertexOutput { diff --git a/gpui/src/scene.rs b/gpui/src/scene.rs index ab6ca71a06465d9c1ef2a02260f543730734349c..1b9c863647205a09fcafde18c2abdda1aa36e922 100644 --- a/gpui/src/scene.rs +++ b/gpui/src/scene.rs @@ -128,6 +128,8 @@ pub struct PathVertex { pub struct Image { pub bounds: RectF, + pub border: Border, + pub corner_radius: f32, pub data: Arc, } From d15eda53f67ed6489be55f458a336c7d6a53d7df Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Sep 2021 17:57:01 +0200 Subject: [PATCH 03/20] Use `AtlasAllocator` in `SpriteCache` Co-Authored-By: Nathan Sobo --- gpui/src/platform/mac/renderer.rs | 13 ++-- gpui/src/platform/mac/sprite_cache.rs | 101 ++++---------------------- 2 files changed, 20 insertions(+), 94 deletions(-) diff --git a/gpui/src/platform/mac/renderer.rs b/gpui/src/platform/mac/renderer.rs index 6f712f29b3bbbfdf647e7b0ddeaebbcd43594b0c..d650c2d9170ee98d6fdad9fe11d1040b1900ee04 100644 --- a/gpui/src/platform/mac/renderer.rs +++ b/gpui/src/platform/mac/renderer.rs @@ -598,11 +598,6 @@ impl Renderer { mem::size_of::() as u64, [drawable_size.to_float2()].as_ptr() as *const c_void, ); - command_encoder.set_vertex_bytes( - shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexAtlasSize as u64, - mem::size_of::() as u64, - [self.sprite_cache.atlas_size().to_float2()].as_ptr() as *const c_void, - ); for (atlas_id, sprites) in sprites_by_atlas { align_offset(offset); @@ -612,13 +607,19 @@ impl Renderer { "instance buffer exhausted" ); + let texture = self.sprite_cache.atlas_texture(atlas_id).unwrap(); command_encoder.set_vertex_buffer( shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexSprites as u64, Some(&self.instances), *offset as u64, ); + command_encoder.set_vertex_bytes( + shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexAtlasSize as u64, + mem::size_of::() as u64, + [vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr() + as *const c_void, + ); - let texture = self.sprite_cache.atlas_texture(atlas_id).unwrap(); command_encoder.set_fragment_texture( shaders::GPUISpriteFragmentInputIndex_GPUISpriteFragmentInputIndexAtlas as u64, Some(texture), diff --git a/gpui/src/platform/mac/sprite_cache.rs b/gpui/src/platform/mac/sprite_cache.rs index 4c764ae1ca518a52e6f45b6bd80093345576fe26..7d11a3d2760dc46e42a08747cb922d9a9ef42e75 100644 --- a/gpui/src/platform/mac/sprite_cache.rs +++ b/gpui/src/platform/mac/sprite_cache.rs @@ -1,12 +1,9 @@ +use super::atlas::AtlasAllocator; use crate::{ fonts::{FontId, GlyphId}, - geometry::{ - rect::RectI, - vector::{vec2f, vec2i, Vector2F, Vector2I}, - }, + geometry::vector::{vec2f, Vector2F, Vector2I}, platform, }; -use etagere::BucketedAtlasAllocator; use metal::{MTLPixelFormat, TextureDescriptor}; use ordered_float::OrderedFloat; use std::{borrow::Cow, collections::HashMap, sync::Arc}; @@ -42,10 +39,8 @@ pub struct IconSprite { } pub struct SpriteCache { - device: metal::Device, - atlas_size: Vector2I, fonts: Arc, - atlases: Vec, + atlases: AtlasAllocator, glyphs: HashMap>, icons: HashMap, } @@ -56,21 +51,18 @@ impl SpriteCache { size: Vector2I, fonts: Arc, ) -> Self { - let atlases = vec![Atlas::new(&device, size)]; + let descriptor = TextureDescriptor::new(); + descriptor.set_pixel_format(MTLPixelFormat::A8Unorm); + descriptor.set_width(size.x() as u64); + descriptor.set_height(size.y() as u64); Self { - device, - atlas_size: size, fonts, - atlases, + atlases: AtlasAllocator::new(device, descriptor), glyphs: Default::default(), icons: Default::default(), } } - pub fn atlas_size(&self) -> Vector2I { - self.atlas_size - } - pub fn render_glyph( &mut self, font_id: FontId, @@ -84,8 +76,6 @@ impl SpriteCache { let target_position = target_position * scale_factor; let fonts = &self.fonts; let atlases = &mut self.atlases; - let atlas_size = self.atlas_size; - let device = &self.device; let subpixel_variant = ( (target_position.x().fract() * SUBPIXEL_VARIANTS as f32).round() as u8 % SUBPIXEL_VARIANTS, @@ -111,22 +101,10 @@ impl SpriteCache { subpixel_shift, scale_factor, )?; - assert!(glyph_bounds.width() < atlas_size.x()); - assert!(glyph_bounds.height() < atlas_size.y()); - - let atlas_bounds = atlases - .last_mut() - .unwrap() - .try_insert(glyph_bounds.size(), &mask) - .unwrap_or_else(|| { - let mut atlas = Atlas::new(device, atlas_size); - let bounds = atlas.try_insert(glyph_bounds.size(), &mask).unwrap(); - atlases.push(atlas); - bounds - }); + let (alloc_id, atlas_bounds) = atlases.upload(glyph_bounds.size(), &mask); Some(GlyphSprite { - atlas_id: atlases.len() - 1, + atlas_id: alloc_id.atlas_id, atlas_origin: atlas_bounds.origin(), offset: glyph_bounds.origin(), size: glyph_bounds.size(), @@ -142,10 +120,6 @@ impl SpriteCache { svg: usvg::Tree, ) -> IconSprite { let atlases = &mut self.atlases; - let atlas_size = self.atlas_size; - let device = &self.device; - assert!(size.x() < atlas_size.x()); - assert!(size.y() < atlas_size.y()); self.icons .entry(IconDescriptor { path, @@ -161,19 +135,9 @@ impl SpriteCache { .map(|a| a.alpha()) .collect::>(); - let atlas_bounds = atlases - .last_mut() - .unwrap() - .try_insert(size, &mask) - .unwrap_or_else(|| { - let mut atlas = Atlas::new(device, atlas_size); - let bounds = atlas.try_insert(size, &mask).unwrap(); - atlases.push(atlas); - bounds - }); - + let (alloc_id, atlas_bounds) = atlases.upload(size, &mask); IconSprite { - atlas_id: atlases.len() - 1, + atlas_id: alloc_id.atlas_id, atlas_origin: atlas_bounds.origin(), size, } @@ -182,45 +146,6 @@ impl SpriteCache { } pub fn atlas_texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> { - self.atlases.get(atlas_id).map(|a| a.texture.as_ref()) - } -} - -struct Atlas { - allocator: BucketedAtlasAllocator, - texture: metal::Texture, -} - -impl Atlas { - fn new(device: &metal::DeviceRef, size: Vector2I) -> Self { - let descriptor = TextureDescriptor::new(); - descriptor.set_pixel_format(MTLPixelFormat::A8Unorm); - descriptor.set_width(size.x() as u64); - descriptor.set_height(size.y() as u64); - - Self { - allocator: BucketedAtlasAllocator::new(etagere::Size::new(size.x(), size.y())), - texture: device.new_texture(&descriptor), - } - } - - fn try_insert(&mut self, size: Vector2I, mask: &[u8]) -> Option { - let allocation = self - .allocator - .allocate(etagere::size2(size.x() + 1, size.y() + 1))?; - - let bounds = allocation.rectangle; - let region = metal::MTLRegion::new_2d( - bounds.min.x as u64, - bounds.min.y as u64, - size.x() as u64, - size.y() as u64, - ); - self.texture - .replace_region(region, 0, mask.as_ptr() as *const _, size.x() as u64); - Some(RectI::from_points( - vec2i(bounds.min.x, bounds.min.y), - vec2i(bounds.max.x, bounds.max.y), - )) + self.atlases.texture(atlas_id) } } From bd4d73bb2714f9ad4d41110c4e7b67c08612408d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Sep 2021 18:11:59 +0200 Subject: [PATCH 04/20] Extract image rasterization into `ImageCache` Co-Authored-By: Nathan Sobo --- gpui/src/platform/mac.rs | 1 + gpui/src/platform/mac/image_cache.rs | 49 +++++++++++++++++++++++++++ gpui/src/platform/mac/renderer.rs | 50 +++++----------------------- 3 files changed, 58 insertions(+), 42 deletions(-) create mode 100644 gpui/src/platform/mac/image_cache.rs diff --git a/gpui/src/platform/mac.rs b/gpui/src/platform/mac.rs index 016d3cb4448b1034f5b56b96140e62e13e027476..8cf3f62874aac87bc6d1841fdfc79d7914a66b50 100644 --- a/gpui/src/platform/mac.rs +++ b/gpui/src/platform/mac.rs @@ -3,6 +3,7 @@ mod dispatcher; mod event; mod fonts; mod geometry; +mod image_cache; mod platform; mod renderer; mod sprite_cache; diff --git a/gpui/src/platform/mac/image_cache.rs b/gpui/src/platform/mac/image_cache.rs new file mode 100644 index 0000000000000000000000000000000000000000..dac2e1a38b24051e983f54152a6e57c2926819b3 --- /dev/null +++ b/gpui/src/platform/mac/image_cache.rs @@ -0,0 +1,49 @@ +use metal::{MTLPixelFormat, TextureDescriptor, TextureRef}; + +use super::atlas::{AllocId, AtlasAllocator}; +use crate::{ + geometry::{rect::RectI, vector::Vector2I}, + ImageData, +}; +use std::{collections::HashMap, mem}; + +pub struct ImageCache { + prev_frame: HashMap, + curr_frame: HashMap, + atlases: AtlasAllocator, +} + +impl ImageCache { + pub fn new(device: metal::Device, size: Vector2I) -> Self { + let descriptor = TextureDescriptor::new(); + descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm); + descriptor.set_width(size.x() as u64); + descriptor.set_height(size.y() as u64); + Self { + prev_frame: Default::default(), + curr_frame: Default::default(), + atlases: AtlasAllocator::new(device, descriptor), + } + } + + pub fn render(&mut self, image: &ImageData) -> (AllocId, RectI) { + let (alloc_id, atlas_bounds) = self + .prev_frame + .remove(&image.id) + .or_else(|| self.curr_frame.get(&image.id).copied()) + .unwrap_or_else(|| self.atlases.upload(image.size(), image.as_bytes())); + self.curr_frame.insert(image.id, (alloc_id, atlas_bounds)); + (alloc_id, atlas_bounds) + } + + pub fn finish_frame(&mut self) { + mem::swap(&mut self.prev_frame, &mut self.curr_frame); + for (_, (id, _)) in self.curr_frame.drain() { + self.atlases.deallocate(id); + } + } + + pub fn atlas_texture(&self, atlas_id: usize) -> Option<&TextureRef> { + self.atlases.texture(atlas_id) + } +} diff --git a/gpui/src/platform/mac/renderer.rs b/gpui/src/platform/mac/renderer.rs index d650c2d9170ee98d6fdad9fe11d1040b1900ee04..369696d47838d25afc105dcfa2426e905c19b5c2 100644 --- a/gpui/src/platform/mac/renderer.rs +++ b/gpui/src/platform/mac/renderer.rs @@ -1,11 +1,8 @@ -use super::{ - atlas::{self, AtlasAllocator}, - sprite_cache::SpriteCache, -}; +use super::{atlas::AtlasAllocator, image_cache::ImageCache, sprite_cache::SpriteCache}; use crate::{ color::Color, geometry::{ - rect::{RectF, RectI}, + rect::RectF, vector::{vec2f, vec2i, Vector2F}, }, platform, @@ -22,10 +19,8 @@ const INSTANCE_BUFFER_SIZE: usize = 1024 * 1024; // This is an arbitrary decisio pub struct Renderer { sprite_cache: SpriteCache, + image_cache: ImageCache, path_atlases: AtlasAllocator, - image_atlases: AtlasAllocator, - prev_rendered_images: HashMap, - curr_rendered_images: HashMap, quad_pipeline_state: metal::RenderPipelineState, shadow_pipeline_state: metal::RenderPipelineState, sprite_pipeline_state: metal::RenderPipelineState, @@ -70,10 +65,9 @@ impl Renderer { ); let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), fonts); + let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768)); let path_atlases = AtlasAllocator::new(device.clone(), build_path_atlas_texture_descriptor()); - let image_atlases = - AtlasAllocator::new(device.clone(), build_image_atlas_texture_descriptor()); let quad_pipeline_state = build_pipeline_state( &device, &library, @@ -116,10 +110,8 @@ impl Renderer { ); Self { sprite_cache, + image_cache, path_atlases, - image_atlases, - prev_rendered_images: Default::default(), - curr_rendered_images: Default::default(), quad_pipeline_state, shadow_pipeline_state, sprite_pipeline_state, @@ -139,11 +131,6 @@ impl Renderer { ) { let mut offset = 0; - mem::swap( - &mut self.curr_rendered_images, - &mut self.prev_rendered_images, - ); - let path_sprites = self.render_path_atlases(scene, &mut offset, command_buffer); self.render_layers( scene, @@ -157,11 +144,7 @@ impl Renderer { location: 0, length: offset as NSUInteger, }); - - for (id, _) in self.prev_rendered_images.values() { - self.image_atlases.deallocate(*id); - } - self.prev_rendered_images.clear(); + self.image_cache.finish_frame(); } fn render_path_atlases( @@ -660,16 +643,7 @@ impl Renderer { let target_size = image.bounds.size() * scale_factor; let corner_radius = image.corner_radius * scale_factor; let border_width = image.border.width * scale_factor; - let (alloc_id, atlas_bounds) = self - .prev_rendered_images - .remove(&image.data.id) - .or_else(|| self.curr_rendered_images.get(&image.data.id).copied()) - .unwrap_or_else(|| { - self.image_atlases - .upload(image.data.size(), image.data.as_bytes()) - }); - self.curr_rendered_images - .insert(image.data.id, (alloc_id, atlas_bounds)); + let (alloc_id, atlas_bounds) = self.image_cache.render(&image.data); images_by_atlas .entry(alloc_id.atlas_id) .or_insert_with(Vec::new) @@ -707,7 +681,7 @@ impl Renderer { "instance buffer exhausted" ); - let texture = self.image_atlases.texture(atlas_id).unwrap(); + let texture = self.image_cache.atlas_texture(atlas_id).unwrap(); command_encoder.set_vertex_buffer( shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexImages as u64, Some(&self.instances), @@ -858,14 +832,6 @@ fn build_path_atlas_texture_descriptor() -> metal::TextureDescriptor { texture_descriptor } -fn build_image_atlas_texture_descriptor() -> metal::TextureDescriptor { - let texture_descriptor = metal::TextureDescriptor::new(); - texture_descriptor.set_width(2048); - texture_descriptor.set_height(2048); - texture_descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm); - texture_descriptor -} - fn align_offset(offset: &mut usize) { let r = *offset % 256; if r > 0 { From 2cf1c697c2120146d29e6aea625f8c4edc1e471b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Sep 2021 18:53:41 +0200 Subject: [PATCH 05/20] Render a signed out icon in titlebar Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- zed/assets/icons/signed-out-12.svg | 3 ++ zed/assets/themes/_base.toml | 8 ++-- zed/src/theme.rs | 12 +++++- zed/src/workspace.rs | 59 +++++++++++++++++---------- zed/src/workspace/sidebar.rs | 64 +++++++++++++++++------------- 5 files changed, 94 insertions(+), 52 deletions(-) create mode 100644 zed/assets/icons/signed-out-12.svg diff --git a/zed/assets/icons/signed-out-12.svg b/zed/assets/icons/signed-out-12.svg new file mode 100644 index 0000000000000000000000000000000000000000..3cecfe9dd3dfaee05bf4bab6fab223c729fc4a52 --- /dev/null +++ b/zed/assets/icons/signed-out-12.svg @@ -0,0 +1,3 @@ + + + diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 6c0c37fa9876961734c5d56473162edd51fc4d2c..124aa178909a107dae4ab26d0c2296566db3e97c 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -7,7 +7,9 @@ pane_divider = { width = 1, color = "$border.0" } [workspace.titlebar] border = { width = 1, bottom = true, color = "$border.0" } -text = { extends = "$text.0" } +title = "$text.0" +icon_width = 16 +icon_signed_out = "$text.2.color" [workspace.tab] text = "$text.2" @@ -26,7 +28,7 @@ background = "$surface.1" text = "$text.0" [workspace.sidebar] -padding = { left = 12, right = 12 } +width = 36 border = { right = true, width = 1, color = "$border.0" } [workspace.sidebar.resize_handle] @@ -35,7 +37,7 @@ background = "$border.0" [workspace.sidebar.icon] color = "$text.2.color" -height = 18 +height = 16 [workspace.sidebar.active_icon] extends = "$workspace.sidebar.icon" diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 12496251f4c006de4db4841dd927d51cad1bef07..f0c17a1cb8876811177f0de76311cd9d818a83a4 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -34,7 +34,7 @@ pub struct SyntaxTheme { #[derive(Deserialize)] pub struct Workspace { pub background: Color, - pub titlebar: ContainedLabel, + pub titlebar: Titlebar, pub tab: Tab, pub active_tab: Tab, pub pane_divider: Border, @@ -42,6 +42,15 @@ pub struct Workspace { pub right_sidebar: Sidebar, } +#[derive(Clone, Deserialize)] +pub struct Titlebar { + #[serde(flatten)] + pub container: ContainerStyle, + pub title: TextStyle, + pub icon_width: f32, + pub icon_signed_out: Color, +} + #[derive(Clone, Deserialize)] pub struct Tab { #[serde(flatten)] @@ -60,6 +69,7 @@ pub struct Tab { pub struct Sidebar { #[serde(flatten)] pub container: ContainerStyle, + pub width: f32, pub icon: SidebarIcon, pub active_icon: SidebarIcon, pub resize_handle: ContainerStyle, diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 92e05d7a8e75ac9c2d52dc2834b731127a257e6c..1dc672f728c3055ce046e3798836b1fd7f97ddb2 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -21,7 +21,7 @@ use gpui::{ json::to_string_pretty, keymap::Binding, platform::WindowOptions, - AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelHandle, MutableAppContext, + AnyViewHandle, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, WeakModelHandle, }; @@ -354,19 +354,10 @@ pub struct Workspace { (usize, Arc), postage::watch::Receiver, Arc>>>, >, - image: Arc, } impl Workspace { pub fn new(app_state: &AppState, cx: &mut ViewContext) -> Self { - let image_bytes = crate::assets::Assets::get("images/as-cii.jpeg").unwrap(); - let image = image::io::Reader::new(std::io::Cursor::new(&*image_bytes.data)) - .with_guessed_format() - .unwrap() - .decode() - .unwrap() - .into_bgra8(); - let pane = cx.add_view(|_| Pane::new(app_state.settings.clone())); let pane_id = pane.id(); cx.subscribe(&pane, move |me, _, event, cx| { @@ -410,7 +401,6 @@ impl Workspace { worktrees: Default::default(), items: Default::default(), loading_items: Default::default(), - image: ImageData::new(image), } } @@ -946,6 +936,24 @@ impl Workspace { pub fn active_pane(&self) -> &ViewHandle { &self.active_pane } + + fn render_account_status(&self, cx: &mut RenderContext) -> ElementBox { + let theme = &self.settings.borrow().theme; + ConstrainedBox::new( + Align::new( + ConstrainedBox::new( + Svg::new("icons/signed-out-12.svg") + .with_color(theme.workspace.titlebar.icon_signed_out) + .boxed(), + ) + .with_width(theme.workspace.titlebar.icon_width) + .boxed(), + ) + .boxed(), + ) + .with_width(theme.workspace.right_sidebar.width) + .boxed() + } } impl Entity for Workspace { @@ -964,16 +972,25 @@ impl View for Workspace { Flex::column() .with_child( ConstrainedBox::new( - Image::new(self.image.clone()).boxed() - // Container::new( - // Align::new( - // Label::new("zed".into(), theme.workspace.titlebar.label.clone()) - // .boxed(), - // ) - // .boxed(), - // ) - // .with_style(&theme.workspace.titlebar.container) - // .boxed(), + Container::new( + Stack::new() + .with_child( + Align::new( + Label::new( + "zed".into(), + theme.workspace.titlebar.title.clone(), + ) + .boxed(), + ) + .boxed(), + ) + .with_child( + Align::new(self.render_account_status(cx)).right().boxed(), + ) + .boxed(), + ) + .with_style(&theme.workspace.titlebar.container) + .boxed(), ) .with_height(32.) .named("titlebar"), diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index 9ee1190eefadb869e631bd18d079ae3a0395574d..7cdb6447a7ff55f8deece33ab5dcbf9738badeb0 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -75,38 +75,48 @@ impl Sidebar { ); let theme = self.theme(settings); - Container::new( - Flex::column() - .with_children(self.items.iter().enumerate().map(|(item_index, item)| { - let theme = if Some(item_index) == self.active_item_ix { - &theme.active_icon - } else { - &theme.icon - }; - enum SidebarButton {} - MouseEventHandler::new::(item.view.id(), cx, |_, _| { - ConstrainedBox::new( - Align::new( + ConstrainedBox::new( + Container::new( + Flex::column() + .with_children(self.items.iter().enumerate().map(|(item_index, item)| { + let theme = if Some(item_index) == self.active_item_ix { + &theme.active_icon + } else { + &theme.icon + }; + enum SidebarButton {} + MouseEventHandler::new::( + item.view.id(), + cx, + |_, _| { ConstrainedBox::new( - Svg::new(item.icon_path).with_color(theme.color).boxed(), + Align::new( + ConstrainedBox::new( + Svg::new(item.icon_path) + .with_color(theme.color) + .boxed(), + ) + .with_height(theme.height) + .boxed(), + ) + .boxed(), ) - .with_height(theme.height) - .boxed(), - ) - .boxed(), + .with_height(line_height + 16.0) + .boxed() + }, ) - .with_height(line_height + 16.0) + .with_cursor_style(CursorStyle::PointingHand) + .on_mouse_down(move |cx| { + cx.dispatch_action(ToggleSidebarItem(ToggleArg { side, item_index })) + }) .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_mouse_down(move |cx| { - cx.dispatch_action(ToggleSidebarItem(ToggleArg { side, item_index })) - }) - .boxed() - })) - .boxed(), + })) + .boxed(), + ) + .with_style(&theme.container) + .boxed(), ) - .with_style(&theme.container) + .with_width(theme.width) .boxed() } From 428c491542b32061910013b8d9576991ce4c92ba Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Sep 2021 19:21:03 +0200 Subject: [PATCH 06/20] WIP: Start on rendering avatars Co-Authored-By: Max Brunsfeld Co-Authored-By: Nathan Sobo --- zed/src/channel.rs | 2 +- zed/src/lib.rs | 1 + zed/src/main.rs | 3 ++- zed/src/test.rs | 3 ++- zed/src/user.rs | 2 +- zed/src/workspace.rs | 3 +++ 6 files changed, 10 insertions(+), 4 deletions(-) diff --git a/zed/src/channel.rs b/zed/src/channel.rs index aa182c0540997e55ed8b8165b752ebaf7f30ac1a..8e8b2964ff6e7dad478f9dcf109fac049f153649 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -443,7 +443,7 @@ impl ChannelMessage { message: proto::ChannelMessage, user_store: &UserStore, ) -> Result { - let sender = user_store.get_user(message.sender_id).await?; + let sender = user_store.fetch_user(message.sender_id).await?; Ok(ChannelMessage { id: message.id, body: message.body, diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 162c69c1db9e7085107d4f252cb7f3b1c6825992..d672d234bfbcce21a64714f13ba46cd23926fe91 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -42,6 +42,7 @@ pub struct AppState { pub languages: Arc, pub themes: Arc, pub rpc: Arc, + pub user_store: Arc, pub fs: Arc, pub channel_list: ModelHandle, } diff --git a/zed/src/main.rs b/zed/src/main.rs index a7dc346e367ec96641e6a005a4f2d0244f3a8e6a..852bfaec2340d0dbb4635617b4e0b76d4d475080 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -43,8 +43,9 @@ fn main() { settings_tx: Arc::new(Mutex::new(settings_tx)), settings, themes, - channel_list: cx.add_model(|cx| ChannelList::new(user_store, rpc.clone(), cx)), + channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), rpc.clone(), cx)), rpc, + user_store, fs: Arc::new(RealFs), }); diff --git a/zed/src/test.rs b/zed/src/test.rs index ce865bbfe58d64c267267cbfbe85321a87a7ca37..3557c0e3c4a6d65f0e85d2a77846a7fa494c41e5 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -170,8 +170,9 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { settings, themes, languages: languages.clone(), - channel_list: cx.add_model(|cx| ChannelList::new(user_store, rpc.clone(), cx)), + channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), rpc.clone(), cx)), rpc, + user_store, fs: Arc::new(RealFs), }) } diff --git a/zed/src/user.rs b/zed/src/user.rs index df98707a8ec907373613d2d6db266934a406149e..9f821f048c57832e3f1e5f371e3bd881f2edce47 100644 --- a/zed/src/user.rs +++ b/zed/src/user.rs @@ -36,7 +36,7 @@ impl UserStore { Ok(()) } - pub async fn get_user(&self, user_id: u64) -> Result> { + pub async fn fetch_user(&self, user_id: u64) -> Result> { if let Some(user) = self.users.lock().get(&user_id).cloned() { return Ok(user); } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 1dc672f728c3055ce046e3798836b1fd7f97ddb2..c603ec5bd474071336845afe46e5ae3ad6ac1c81 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -10,6 +10,7 @@ use crate::{ project_browser::ProjectBrowser, rpc, settings::Settings, + user, worktree::{File, Worktree}, AppState, }; @@ -341,6 +342,7 @@ pub struct Workspace { pub settings: watch::Receiver, languages: Arc, rpc: Arc, + user_store: Arc, fs: Arc, modal: Option, center: PaneGroup, @@ -395,6 +397,7 @@ impl Workspace { settings: app_state.settings.clone(), languages: app_state.languages.clone(), rpc: app_state.rpc.clone(), + user_store: app_state.user_store.clone(), fs: app_state.fs.clone(), left_sidebar, right_sidebar, From f0019e3725d32f72d7fdbaae956cf1d787b8f19c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 12:29:56 -0600 Subject: [PATCH 07/20] WIP --- gpui/src/app.rs | 10 +++++ server/src/rpc.rs | 10 ++--- zed/src/channel.rs | 4 +- zed/src/main.rs | 2 +- zed/src/rpc.rs | 12 +++--- zed/src/test.rs | 2 +- zed/src/user.rs | 88 +++++++++++++++++++++++++++++++++++++++++--- zed/src/workspace.rs | 38 ++++++++++++++----- 8 files changed, 135 insertions(+), 31 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index c1ce8fdba0d266726a8db83eaba36fe3fe3f4de6..4b18af06b4bdb0fef7c3fb47e819fa3153e6c8f2 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -2282,6 +2282,16 @@ impl<'a, T: View> ViewContext<'a, T> { let handle = self.handle(); self.app.spawn(|cx| f(handle, cx)) } + + pub fn spawn_weak(&self, f: F) -> Task + where + F: FnOnce(WeakViewHandle, AsyncAppContext) -> Fut, + Fut: 'static + Future, + S: 'static, + { + let handle = self.handle().downgrade(); + self.app.spawn(|cx| f(handle, cx)) + } } pub struct RenderContext<'a, T: View> { diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 2bd0eac625e836d32bf4417be107a11a766dadb0..539068f2d3d55e8cb11c085c52c0a84957ce4b6b 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1512,7 +1512,7 @@ mod tests { .await .unwrap(); - let user_store_a = Arc::new(UserStore::new(client_a.clone())); + let user_store_a = UserStore::new(client_a.clone(), cx_a.background().as_ref()); 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()) @@ -1537,7 +1537,7 @@ mod tests { }) .await; - let user_store_b = Arc::new(UserStore::new(client_b.clone())); + let user_store_b = UserStore::new(client_b.clone(), cx_b.background().as_ref()); 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()) @@ -1637,7 +1637,7 @@ mod tests { .await .unwrap(); - let user_store_a = Arc::new(UserStore::new(client_a.clone())); + let user_store_a = UserStore::new(client_a.clone(), cx_a.background().as_ref()); 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()) @@ -1713,7 +1713,7 @@ mod tests { .await .unwrap(); - let user_store_a = Arc::new(UserStore::new(client_a.clone())); + let user_store_a = UserStore::new(client_a.clone(), cx_a.background().as_ref()); 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()) @@ -1739,7 +1739,7 @@ mod tests { }) .await; - let user_store_b = Arc::new(UserStore::new(client_b.clone())); + let user_store_b = UserStore::new(client_b.clone(), cx_b.background().as_ref()); 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()) diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 8e8b2964ff6e7dad478f9dcf109fac049f153649..bf1237d8359f4e4c857b419fbc663b6182325c60 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -118,7 +118,7 @@ impl ChannelList { cx.notify(); }); } - rpc::Status::Disconnected { .. } => { + rpc::Status::SignedOut { .. } => { this.update(&mut cx, |this, cx| { this.available_channels = None; this.channels.clear(); @@ -503,7 +503,7 @@ mod tests { let user_id = 5; let mut client = Client::new(); let server = FakeServer::for_client(user_id, &mut client, &cx).await; - let user_store = Arc::new(UserStore::new(client.clone())); + let user_store = UserStore::new(client.clone(), cx.background().as_ref()); 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)); diff --git a/zed/src/main.rs b/zed/src/main.rs index 852bfaec2340d0dbb4635617b4e0b76d4d475080..f585ed5b82e4afc0bd261f244a38504a58e2c5fb 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -37,7 +37,7 @@ fn main() { app.run(move |cx| { let rpc = rpc::Client::new(); - let user_store = Arc::new(UserStore::new(rpc.clone())); + let user_store = UserStore::new(rpc.clone(), cx.background()); let app_state = Arc::new(AppState { languages: languages.clone(), settings_tx: Arc::new(Mutex::new(settings_tx)), diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 501779c2b027913126b09a82f43bbde415bbc6c2..69bc33a62e1c4065f7c946b239e72d7039ef5c49 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -39,7 +39,7 @@ pub struct Client { #[derive(Copy, Clone, Debug)] pub enum Status { - Disconnected, + SignedOut, Authenticating, Connecting { user_id: u64, @@ -73,7 +73,7 @@ struct ClientState { impl Default for ClientState { fn default() -> Self { Self { - status: watch::channel_with(Status::Disconnected), + status: watch::channel_with(Status::SignedOut), entity_id_extractors: Default::default(), model_handlers: Default::default(), _maintain_connection: None, @@ -167,7 +167,7 @@ impl Client { } })); } - Status::Disconnected => { + Status::SignedOut => { state._maintain_connection.take(); } _ => {} @@ -232,7 +232,7 @@ impl Client { cx: &AsyncAppContext, ) -> anyhow::Result<()> { let was_disconnected = match *self.status().borrow() { - Status::Disconnected => true, + Status::SignedOut => true, Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => { false } @@ -324,7 +324,7 @@ impl Client { cx.foreground() .spawn(async move { match handle_io.await { - Ok(()) => this.set_status(Status::Disconnected, &cx), + Ok(()) => this.set_status(Status::SignedOut, &cx), Err(err) => { log::error!("connection error: {:?}", err); this.set_status(Status::ConnectionLost, &cx); @@ -470,7 +470,7 @@ impl Client { pub async fn disconnect(self: &Arc, cx: &AsyncAppContext) -> Result<()> { let conn_id = self.connection_id()?; self.peer.disconnect(conn_id).await; - self.set_status(Status::Disconnected, cx); + self.set_status(Status::SignedOut, cx); Ok(()) } diff --git a/zed/src/test.rs b/zed/src/test.rs index 3557c0e3c4a6d65f0e85d2a77846a7fa494c41e5..019969ee4f75ee1e463c3abbcc3f75950c9ac635 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -164,7 +164,7 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { let languages = Arc::new(LanguageRegistry::new()); let themes = ThemeRegistry::new(Assets, cx.font_cache().clone()); let rpc = rpc::Client::new(); - let user_store = Arc::new(UserStore::new(rpc.clone())); + let user_store = UserStore::new(rpc.clone(), cx.background()); Arc::new(AppState { settings_tx: Arc::new(Mutex::new(settings_tx)), settings, diff --git a/zed/src/user.rs b/zed/src/user.rs index 9f821f048c57832e3f1e5f371e3bd881f2edce47..6c02c0a284e6dcca3ebd32d3ba601f15808fb948 100644 --- a/zed/src/user.rs +++ b/zed/src/user.rs @@ -1,22 +1,73 @@ -use crate::rpc::Client; +use crate::{ + rpc::{Client, Status}, + util::TryFutureExt, +}; use anyhow::{anyhow, Result}; +use gpui::{elements::Image, executor, ImageData, Task}; use parking_lot::Mutex; +use postage::{prelude::Stream, sink::Sink, watch}; use std::{collections::HashMap, sync::Arc}; +use surf::{ + http::{Method, Request}, + HttpClient, Url, +}; use zrpc::proto; -pub use proto::User; +pub struct User { + id: u64, + github_login: String, + avatar: Option, +} pub struct UserStore { users: Mutex>>, + current_user: watch::Receiver>>, rpc: Arc, + http: Arc, + _maintain_current_user: Option>, } impl UserStore { - pub fn new(rpc: Arc) -> Self { - Self { + pub fn new( + rpc: Arc, + http: Arc, + executor: &executor::Background, + ) -> Arc { + let (mut current_user_tx, current_user_rx) = watch::channel(); + + let mut this = Arc::new(Self { users: Default::default(), - rpc, - } + current_user: current_user_rx, + rpc: rpc.clone(), + http, + _maintain_current_user: None, + }); + + let task = { + let this = Arc::downgrade(&this); + executor.spawn(async move { + let mut status = rpc.status(); + while let Some(status) = status.recv().await { + match status { + Status::Connected { user_id, .. } => { + if let Some(this) = this.upgrade() { + current_user_tx + .send(this.fetch_user(user_id).log_err().await) + .await + .ok(); + } + } + Status::SignedOut => { + current_user_tx.send(None).await.ok(); + } + _ => {} + } + } + }) + }; + Arc::get_mut(&mut this).unwrap()._maintain_current_user = Some(task); + + this } pub async fn load_users(&self, mut user_ids: Vec) -> Result<()> { @@ -56,4 +107,29 @@ impl UserStore { Err(anyhow!("server responded with no users")) } } + + pub fn current_user(&self) -> &watch::Receiver>> { + &self.current_user + } +} + +impl User { + async fn new(message: proto::User, http: &dyn HttpClient) -> Self { + let avatar = fetch_avatar(http, &message.avatar_url).await.log_err(); + User { + id: message.id, + github_login: message.github_login, + avatar, + } + } +} + +async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result> { + let url = Url::parse(url)?; + let request = Request::new(Method::Get, url); + let response = http.send(request).await?; + let bytes = response.body_bytes().await?; + let format = image::guess_format(&bytes)?; + let image = image::load_from_memory_with_format(&bytes, format)?.into_bgra8(); + Ok(ImageData::new(image)) } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index c603ec5bd474071336845afe46e5ae3ad6ac1c81..1410b6ece55ea3a0bee1c87d9b67377ec07cbaa6 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -29,7 +29,7 @@ use gpui::{ use log::error; pub use pane::*; pub use pane_group::*; -use postage::watch; +use postage::{prelude::Stream, watch}; use sidebar::{Side, Sidebar, ToggleSidebarItem}; use smol::prelude::*; use std::{ @@ -356,6 +356,7 @@ pub struct Workspace { (usize, Arc), postage::watch::Receiver, Arc>>>, >, + _observe_current_user: Task<()>, } impl Workspace { @@ -389,6 +390,18 @@ impl Workspace { ); right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into()); + let mut current_user = app_state.user_store.current_user().clone(); + let _observe_current_user = cx.spawn_weak(|this, mut cx| async move { + current_user.recv().await; + while current_user.recv().await.is_some() { + cx.update(|cx| { + if let Some(this) = this.upgrade(&cx) { + this.update(cx, |_, cx| cx.notify()); + } + }) + } + }); + Workspace { modal: None, center: PaneGroup::new(pane.id()), @@ -404,6 +417,7 @@ impl Workspace { worktrees: Default::default(), items: Default::default(), loading_items: Default::default(), + _observe_current_user, } } @@ -940,17 +954,21 @@ impl Workspace { &self.active_pane } - fn render_account_status(&self, cx: &mut RenderContext) -> ElementBox { + fn render_current_user(&self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme; + let avatar = if let Some(current_user) = self.user_store.current_user().borrow().as_ref() { + todo!() + } else { + Svg::new("icons/signed-out-12.svg") + .with_color(theme.workspace.titlebar.icon_signed_out) + .boxed() + }; + ConstrainedBox::new( Align::new( - ConstrainedBox::new( - Svg::new("icons/signed-out-12.svg") - .with_color(theme.workspace.titlebar.icon_signed_out) - .boxed(), - ) - .with_width(theme.workspace.titlebar.icon_width) - .boxed(), + ConstrainedBox::new(avatar) + .with_width(theme.workspace.titlebar.icon_width) + .boxed(), ) .boxed(), ) @@ -988,7 +1006,7 @@ impl View for Workspace { .boxed(), ) .with_child( - Align::new(self.render_account_status(cx)).right().boxed(), + Align::new(self.render_current_user(cx)).right().boxed(), ) .boxed(), ) From 84d4bb6186ad4a1acc4357a2aacc446f92c6edf0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 14 Sep 2021 12:09:49 -0700 Subject: [PATCH 08/20] Introduce HttpClient trait, use it to fetch avatars in UserStore * Add a FakeHttpClient for tests --- gpui/src/image_data.rs | 18 ++++++-- server/src/rpc.rs | 2 + zed/src/channel.rs | 8 ++-- zed/src/http.rs | 26 ++++++++++++ zed/src/lib.rs | 1 + zed/src/main.rs | 5 ++- zed/src/test.rs | 36 +++++++++++++++- zed/src/user.rs | 94 +++++++++++++++++++++++------------------- zed/src/workspace.rs | 10 ++++- 9 files changed, 146 insertions(+), 54 deletions(-) create mode 100644 zed/src/http.rs diff --git a/gpui/src/image_data.rs b/gpui/src/image_data.rs index 352393e3b57328f8373134c23edcb9b9ed5790ae..d97820ab51b3f09ad8f2241243837ce1535dc6af 100644 --- a/gpui/src/image_data.rs +++ b/gpui/src/image_data.rs @@ -1,8 +1,11 @@ use crate::geometry::vector::{vec2i, Vector2I}; use image::{Bgra, ImageBuffer}; -use std::sync::{ - atomic::{AtomicUsize, Ordering::SeqCst}, - Arc, +use std::{ + fmt, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, + }, }; pub struct ImageData { @@ -29,3 +32,12 @@ impl ImageData { vec2i(width as i32, height as i32) } } + +impl fmt::Debug for ImageData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ImageData") + .field("id", &self.id) + .field("size", &self.data.dimensions()) + .finish() + } +} diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 539068f2d3d55e8cb11c085c52c0a84957ce4b6b..14e520d58973f09e9850e4b35dc56f8930a7f91d 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1025,6 +1025,7 @@ mod tests { language::LanguageRegistry, rpc::{self, Client}, settings, + test::FakeHttpClient, user::UserStore, worktree::Worktree, }; @@ -1486,6 +1487,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; + let mut http = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) }); 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; diff --git a/zed/src/channel.rs b/zed/src/channel.rs index bf1237d8359f4e4c857b419fbc663b6182325c60..42727ea54b7d001c0df3f1384950b2c3ff1917ec 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -46,7 +46,7 @@ pub struct Channel { _subscription: rpc::Subscription, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct ChannelMessage { pub id: u64, pub body: String, @@ -495,15 +495,17 @@ impl<'a> sum_tree::SeekDimension<'a, ChannelMessageSummary> for Count { #[cfg(test)] mod tests { use super::*; - use crate::test::FakeServer; + use crate::test::{FakeHttpClient, FakeServer}; use gpui::TestAppContext; + use surf::http::Response; #[gpui::test] async fn test_channel_messages(mut cx: TestAppContext) { let user_id = 5; let mut client = Client::new(); + let http_client = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) }); let server = FakeServer::for_client(user_id, &mut client, &cx).await; - let user_store = UserStore::new(client.clone(), cx.background().as_ref()); + let user_store = UserStore::new(client.clone(), http_client, cx.background().as_ref()); 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)); diff --git a/zed/src/http.rs b/zed/src/http.rs new file mode 100644 index 0000000000000000000000000000000000000000..68f1610c2749c8e374ddd5c8a81beef0281b06f9 --- /dev/null +++ b/zed/src/http.rs @@ -0,0 +1,26 @@ +pub use anyhow::{anyhow, Result}; +use futures::future::BoxFuture; +use std::sync::Arc; +pub use surf::{ + http::{Method, Request, Response as ServerResponse}, + Response, Url, +}; + +pub trait HttpClient: Send + Sync { + fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result>; +} + +pub fn client() -> Arc { + Arc::new(surf::client()) +} + +impl HttpClient for surf::Client { + fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result> { + Box::pin(async move { + Ok(self + .send(req) + .await + .map_err(|e| anyhow!("http request failed: {}", e))?) + }) + } +} diff --git a/zed/src/lib.rs b/zed/src/lib.rs index d672d234bfbcce21a64714f13ba46cd23926fe91..c9cec56f46da05851471cd20f9e4adb9133dc18c 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -5,6 +5,7 @@ pub mod editor; pub mod file_finder; pub mod fs; mod fuzzy; +pub mod http; pub mod language; pub mod menus; pub mod project_browser; diff --git a/zed/src/main.rs b/zed/src/main.rs index f585ed5b82e4afc0bd261f244a38504a58e2c5fb..9e73a961265e7347d652a6d016ece75ea513d3ce 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -13,7 +13,7 @@ use zed::{ channel::ChannelList, chat_panel, editor, file_finder, fs::RealFs, - language, menus, rpc, settings, theme_selector, + http, language, menus, rpc, settings, theme_selector, user::UserStore, workspace::{self, OpenNew, OpenParams, OpenPaths}, AppState, @@ -37,7 +37,8 @@ fn main() { app.run(move |cx| { let rpc = rpc::Client::new(); - let user_store = UserStore::new(rpc.clone(), cx.background()); + let http = http::client(); + let user_store = UserStore::new(rpc.clone(), http.clone(), cx.background()); let app_state = Arc::new(AppState { languages: languages.clone(), settings_tx: Arc::new(Mutex::new(settings_tx)), diff --git a/zed/src/test.rs b/zed/src/test.rs index 019969ee4f75ee1e463c3abbcc3f75950c9ac635..e8527a4ed762a057fbcd72355fa4783024e57f40 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -2,6 +2,7 @@ use crate::{ assets::Assets, channel::ChannelList, fs::RealFs, + http::{HttpClient, Request, Response, ServerResponse}, language::LanguageRegistry, rpc::{self, Client}, settings::{self, ThemeRegistry}, @@ -10,11 +11,13 @@ use crate::{ AppState, }; use anyhow::{anyhow, Result}; +use futures::{future::BoxFuture, Future}; use gpui::{AsyncAppContext, Entity, ModelHandle, MutableAppContext, TestAppContext}; use parking_lot::Mutex; use postage::{mpsc, prelude::Stream as _}; use smol::channel; use std::{ + fmt, marker::PhantomData, path::{Path, PathBuf}, sync::{ @@ -164,7 +167,8 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { let languages = Arc::new(LanguageRegistry::new()); let themes = ThemeRegistry::new(Assets, cx.font_cache().clone()); let rpc = rpc::Client::new(); - let user_store = UserStore::new(rpc.clone(), cx.background()); + let http = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) }); + let user_store = UserStore::new(rpc.clone(), http, cx.background()); Arc::new(AppState { settings_tx: Arc::new(Mutex::new(settings_tx)), settings, @@ -313,3 +317,33 @@ impl FakeServer { self.connection_id.lock().expect("not connected") } } + +pub struct FakeHttpClient { + handler: + Box BoxFuture<'static, Result>>, +} + +impl FakeHttpClient { + pub fn new(handler: F) -> Arc + where + Fut: 'static + Send + Future>, + F: 'static + Send + Sync + Fn(Request) -> Fut, + { + Arc::new(Self { + handler: Box::new(move |req| Box::pin(handler(req))), + }) + } +} + +impl fmt::Debug for FakeHttpClient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FakeHttpClient").finish() + } +} + +impl HttpClient for FakeHttpClient { + fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result> { + let future = (self.handler)(req); + Box::pin(async move { future.await.map(Into::into) }) + } +} diff --git a/zed/src/user.rs b/zed/src/user.rs index 6c02c0a284e6dcca3ebd32d3ba601f15808fb948..ee9915ac3e6b21215fd98d3fb141e712a01c3907 100644 --- a/zed/src/user.rs +++ b/zed/src/user.rs @@ -1,22 +1,24 @@ use crate::{ + http::{HttpClient, Method, Request, Url}, rpc::{Client, Status}, util::TryFutureExt, }; -use anyhow::{anyhow, Result}; -use gpui::{elements::Image, executor, ImageData, Task}; +use anyhow::{anyhow, Context, Result}; +use futures::future; +use gpui::{executor, ImageData, Task}; use parking_lot::Mutex; -use postage::{prelude::Stream, sink::Sink, watch}; -use std::{collections::HashMap, sync::Arc}; -use surf::{ - http::{Method, Request}, - HttpClient, Url, +use postage::{oneshot, prelude::Stream, sink::Sink, watch}; +use std::{ + collections::HashMap, + sync::{Arc, Weak}, }; use zrpc::proto; +#[derive(Debug)] pub struct User { - id: u64, - github_login: String, - avatar: Option, + pub id: u64, + pub github_login: String, + pub avatar: Option>, } pub struct UserStore { @@ -24,7 +26,7 @@ pub struct UserStore { current_user: watch::Receiver>>, rpc: Arc, http: Arc, - _maintain_current_user: Option>, + _maintain_current_user: Task<()>, } impl UserStore { @@ -34,18 +36,18 @@ impl UserStore { executor: &executor::Background, ) -> Arc { let (mut current_user_tx, current_user_rx) = watch::channel(); - - let mut this = Arc::new(Self { + let (mut this_tx, mut this_rx) = oneshot::channel::>(); + let this = Arc::new(Self { users: Default::default(), current_user: current_user_rx, rpc: rpc.clone(), http, - _maintain_current_user: None, - }); - - let task = { - let this = Arc::downgrade(&this); - executor.spawn(async move { + _maintain_current_user: executor.spawn(async move { + let this = if let Some(this) = this_rx.recv().await { + this + } else { + return; + }; let mut status = rpc.status(); while let Some(status) = status.recv().await { match status { @@ -63,10 +65,12 @@ impl UserStore { _ => {} } } - }) - }; - Arc::get_mut(&mut this).unwrap()._maintain_current_user = Some(task); - + }), + }); + let weak = Arc::downgrade(&this); + executor + .spawn(async move { this_tx.send(weak).await }) + .detach(); this } @@ -78,8 +82,15 @@ impl UserStore { if !user_ids.is_empty() { let response = self.rpc.request(proto::GetUsers { user_ids }).await?; + let new_users = future::join_all( + response + .users + .into_iter() + .map(|user| User::new(user, self.http.as_ref())), + ) + .await; let mut users = self.users.lock(); - for user in response.users { + for user in new_users { users.insert(user.id, Arc::new(user)); } } @@ -92,20 +103,12 @@ impl UserStore { return Ok(user); } - let response = self - .rpc - .request(proto::GetUsers { - user_ids: vec![user_id], - }) - .await?; - - if let Some(user) = response.users.into_iter().next() { - let user = Arc::new(user); - self.users.lock().insert(user_id, user.clone()); - Ok(user) - } else { - Err(anyhow!("server responded with no users")) - } + self.load_users(vec![user_id]).await?; + self.users + .lock() + .get(&user_id) + .cloned() + .ok_or_else(|| anyhow!("server responded with no users")) } pub fn current_user(&self) -> &watch::Receiver>> { @@ -115,20 +118,25 @@ impl UserStore { impl User { async fn new(message: proto::User, http: &dyn HttpClient) -> Self { - let avatar = fetch_avatar(http, &message.avatar_url).await.log_err(); User { id: message.id, github_login: message.github_login, - avatar, + avatar: fetch_avatar(http, &message.avatar_url).log_err().await, } } } async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result> { - let url = Url::parse(url)?; + let url = Url::parse(url).with_context(|| format!("failed to parse avatar url {:?}", url))?; let request = Request::new(Method::Get, url); - let response = http.send(request).await?; - let bytes = response.body_bytes().await?; + let mut response = http + .send(request) + .await + .map_err(|e| anyhow!("failed to send user avatar request: {}", e))?; + let bytes = response + .body_bytes() + .await + .map_err(|e| anyhow!("failed to read user avatar response body: {}", e))?; let format = image::guess_format(&bytes)?; let image = image::load_from_memory_with_format(&bytes, format)?.into_bgra8(); Ok(ImageData::new(image)) diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 1410b6ece55ea3a0bee1c87d9b67377ec07cbaa6..c38a2d78bb89acc4e23bccb2209639c59ddf8e0b 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -956,8 +956,14 @@ impl Workspace { fn render_current_user(&self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme; - let avatar = if let Some(current_user) = self.user_store.current_user().borrow().as_ref() { - todo!() + let avatar = if let Some(avatar) = self + .user_store + .current_user() + .borrow() + .as_ref() + .and_then(|user| user.avatar.clone()) + { + Image::new(avatar).boxed() } else { Svg::new("icons/signed-out-12.svg") .with_color(theme.workspace.titlebar.icon_signed_out) From e0e0bdbc3aa2d8b35a69616acd169e0cba978d6d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 16:28:26 -0600 Subject: [PATCH 09/20] Synthesize GitHub avatar URL and follow redirects when fetching it Co-Authored-By: Max Brunsfeld --- server/src/rpc.rs | 2 +- zed/src/http.rs | 4 ++-- zed/src/user.rs | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 14e520d58973f09e9850e4b35dc56f8930a7f91d..d4cc596810d51acf40fd2267112c093c3955d2eb 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -558,8 +558,8 @@ impl Server { .into_iter() .map(|user| proto::User { id: user.id.to_proto(), + avatar_url: format!("https://github.com/{}.png?size=128", user.github_login), github_login: user.github_login, - avatar_url: String::new(), }) .collect(); self.peer diff --git a/zed/src/http.rs b/zed/src/http.rs index 68f1610c2749c8e374ddd5c8a81beef0281b06f9..30a7a08a519950beee6f28729f2af2f6c9df363a 100644 --- a/zed/src/http.rs +++ b/zed/src/http.rs @@ -2,8 +2,8 @@ pub use anyhow::{anyhow, Result}; use futures::future::BoxFuture; use std::sync::Arc; pub use surf::{ - http::{Method, Request, Response as ServerResponse}, - Response, Url, + http::{Method, Response as ServerResponse}, + Request, Response, Url, }; pub trait HttpClient: Send + Sync { diff --git a/zed/src/user.rs b/zed/src/user.rs index ee9915ac3e6b21215fd98d3fb141e712a01c3907..3a119474e1e7397b5c5352ffb13cddd27f98ae9c 100644 --- a/zed/src/user.rs +++ b/zed/src/user.rs @@ -128,7 +128,9 @@ impl User { async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result> { let url = Url::parse(url).with_context(|| format!("failed to parse avatar url {:?}", url))?; - let request = Request::new(Method::Get, url); + let mut request = Request::new(Method::Get, url); + request.middleware(surf::middleware::Redirect::default()); + let mut response = http .send(request) .await From b63b717eacc7ea579c53a382232db12ad800a665 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 16:39:35 -0600 Subject: [PATCH 10/20] Preserve aspect ratio when scaling images Co-Authored-By: Max Brunsfeld --- gpui/src/elements.rs | 15 ++++++++++++++- gpui/src/elements/image.rs | 6 +++++- gpui/src/elements/svg.rs | 21 ++++++--------------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/gpui/src/elements.rs b/gpui/src/elements.rs index 252cebd7334f57874a8fd6164da15ad0ba3c2986..42e9810cfbff21ea9c96017b54689cb5a77f48cd 100644 --- a/gpui/src/elements.rs +++ b/gpui/src/elements.rs @@ -24,7 +24,10 @@ pub use self::{ }; pub use crate::presenter::ChildView; use crate::{ - geometry::{rect::RectF, vector::Vector2F}, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, json, DebugContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; use core::panic; @@ -359,3 +362,13 @@ pub trait ParentElement<'a>: Extend + Sized { } impl<'a, T> ParentElement<'a> for T where T: Extend {} + +fn constrain_size_preserving_aspect_ratio(max_size: Vector2F, size: Vector2F) -> Vector2F { + if max_size.x().is_infinite() && max_size.y().is_infinite() { + size + } else if max_size.x().is_infinite() || max_size.x() / max_size.y() > size.x() / size.y() { + vec2f(size.x() * max_size.y() / size.y(), max_size.y()) + } else { + vec2f(max_size.x(), size.y() * max_size.x() / size.x()) + } +} diff --git a/gpui/src/elements/image.rs b/gpui/src/elements/image.rs index 2611478fe0b85e93ee9f55147e0b758e53427153..f9908a76fb77b9ce3c14b1fc2f7073401ed84a00 100644 --- a/gpui/src/elements/image.rs +++ b/gpui/src/elements/image.rs @@ -6,6 +6,8 @@ use crate::{ }; use std::sync::Arc; +use super::constrain_size_preserving_aspect_ratio; + pub struct Image { data: Arc, border: Border, @@ -41,7 +43,9 @@ impl Element for Image { constraint: SizeConstraint, _: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - (constraint.max, ()) + let size = + constrain_size_preserving_aspect_ratio(constraint.max, self.data.size().to_f32()); + (size, ()) } fn paint( diff --git a/gpui/src/elements/svg.rs b/gpui/src/elements/svg.rs index 8adb285b99a73c1e2ab661976d4f2d58d4d635db..55e11a92531341262e69e79ff6365a2c20cfa5b6 100644 --- a/gpui/src/elements/svg.rs +++ b/gpui/src/elements/svg.rs @@ -41,21 +41,10 @@ impl Element for Svg { ) -> (Vector2F, Self::LayoutState) { match cx.asset_cache.svg(&self.path) { Ok(tree) => { - let size = if constraint.max.x().is_infinite() && constraint.max.y().is_infinite() { - let rect = from_usvg_rect(tree.svg_node().view_box.rect); - rect.size() - } else { - let max_size = constraint.max; - let svg_size = from_usvg_rect(tree.svg_node().view_box.rect).size(); - - if max_size.x().is_infinite() - || max_size.x() / max_size.y() > svg_size.x() / svg_size.y() - { - vec2f(svg_size.x() * max_size.y() / svg_size.y(), max_size.y()) - } else { - vec2f(max_size.x(), svg_size.y() * max_size.x() / svg_size.x()) - } - }; + let size = constrain_size_preserving_aspect_ratio( + constraint.max, + from_usvg_rect(tree.svg_node().view_box.rect).size(), + ); (size, Some(tree)) } Err(error) => { @@ -111,6 +100,8 @@ impl Element for Svg { use crate::json::ToJson; +use super::constrain_size_preserving_aspect_ratio; + fn from_usvg_rect(rect: usvg::Rect) -> RectF { RectF::new( vec2f(rect.x() as f32, rect.y() as f32), From 426d52d8c1394bf0452d19f8d3534dfe11ad8214 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 16:59:09 -0600 Subject: [PATCH 11/20] Mix quad border color with background color based on its alpha channel Co-Authored-By: Max Brunsfeld --- gpui/src/platform/mac/shaders/shaders.metal | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gpui/src/platform/mac/shaders/shaders.metal b/gpui/src/platform/mac/shaders/shaders.metal index 00338b30e71c234147f88916f011f923221e11fc..13d2720fad7788ff93a58a8143dde44ced25cf41 100644 --- a/gpui/src/platform/mac/shaders/shaders.metal +++ b/gpui/src/platform/mac/shaders/shaders.metal @@ -70,9 +70,10 @@ float4 quad_sdf(QuadFragmentInput input) { if (border_width == 0.) { color = input.background_color; } else { + float4 border_color = float4(mix(float3(input.background_color), float3(input.border_color), input.border_color.a), 1.); float inset_distance = distance + border_width; color = mix( - input.border_color, + border_color, input.background_color, saturate(0.5 - inset_distance) ); From 0f415a594f86f9da113f69bb4054a2fa2adaa0fe Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 16:59:38 -0600 Subject: [PATCH 12/20] Style avatar image with border and rounded corners Co-Authored-By: Max Brunsfeld --- gpui/src/elements/image.rs | 27 ++++++++++++++------------- zed/assets/themes/_base.toml | 7 ++++--- zed/src/theme.rs | 5 +++-- zed/src/workspace.rs | 6 ++++-- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/gpui/src/elements/image.rs b/gpui/src/elements/image.rs index f9908a76fb77b9ce3c14b1fc2f7073401ed84a00..421e18ec95ce7bc64df77d675797a10c41ff3541 100644 --- a/gpui/src/elements/image.rs +++ b/gpui/src/elements/image.rs @@ -1,16 +1,23 @@ +use super::constrain_size_preserving_aspect_ratio; use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::{json, ToJson}, scene, Border, DebugContext, Element, Event, EventContext, ImageData, LayoutContext, PaintContext, SizeConstraint, }; +use serde::Deserialize; use std::sync::Arc; -use super::constrain_size_preserving_aspect_ratio; - pub struct Image { data: Arc, + style: ImageStyle, +} + +#[derive(Copy, Clone, Default, Deserialize)] +pub struct ImageStyle { + #[serde(default)] border: Border, + #[serde(default)] corner_radius: f32, } @@ -18,18 +25,12 @@ impl Image { pub fn new(data: Arc) -> Self { Self { data, - border: Default::default(), - corner_radius: Default::default(), + style: Default::default(), } } - pub fn with_corner_radius(mut self, corner_radius: f32) -> Self { - self.corner_radius = corner_radius; - self - } - - pub fn with_border(mut self, border: Border) -> Self { - self.border = border; + pub fn with_style(mut self, style: ImageStyle) -> Self { + self.style = style; self } } @@ -57,8 +58,8 @@ impl Element for Image { ) -> Self::PaintState { cx.scene.push_image(scene::Image { bounds, - border: self.border, - corner_radius: self.corner_radius, + border: self.style.border, + corner_radius: self.style.corner_radius, data: self.data.clone(), }); } diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 124aa178909a107dae4ab26d0c2296566db3e97c..dfd27f396d35b5f68b0e5744c9ed424103be7839 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -8,8 +8,9 @@ pane_divider = { width = 1, color = "$border.0" } [workspace.titlebar] border = { width = 1, bottom = true, color = "$border.0" } title = "$text.0" -icon_width = 16 +avatar_width = 20 icon_signed_out = "$text.2.color" +avatar = { corner_radius = 10, border = { width = 1, color = "#00000088" } } [workspace.tab] text = "$text.2" @@ -28,7 +29,7 @@ background = "$surface.1" text = "$text.0" [workspace.sidebar] -width = 36 +width = 38 border = { right = true, width = 1, color = "$border.0" } [workspace.sidebar.resize_handle] @@ -37,7 +38,7 @@ background = "$border.0" [workspace.sidebar.icon] color = "$text.2.color" -height = 16 +height = 18 [workspace.sidebar.active_icon] extends = "$workspace.sidebar.icon" diff --git a/zed/src/theme.rs b/zed/src/theme.rs index f0c17a1cb8876811177f0de76311cd9d818a83a4..96e562a55ffc45ebb593010699fed06bedf65160 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -4,7 +4,7 @@ mod theme_registry; use anyhow::Result; use gpui::{ color::Color, - elements::{ContainerStyle, LabelStyle}, + elements::{ContainerStyle, ImageStyle, LabelStyle}, fonts::{HighlightStyle, TextStyle}, Border, }; @@ -47,8 +47,9 @@ pub struct Titlebar { #[serde(flatten)] pub container: ContainerStyle, pub title: TextStyle, - pub icon_width: f32, + pub avatar_width: f32, pub icon_signed_out: Color, + pub avatar: ImageStyle, } #[derive(Clone, Deserialize)] diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index c38a2d78bb89acc4e23bccb2209639c59ddf8e0b..1cef8caef082f45462d0a226f49a7a0b6f5bf019 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -963,7 +963,9 @@ impl Workspace { .as_ref() .and_then(|user| user.avatar.clone()) { - Image::new(avatar).boxed() + Image::new(avatar) + .with_style(theme.workspace.titlebar.avatar) + .boxed() } else { Svg::new("icons/signed-out-12.svg") .with_color(theme.workspace.titlebar.icon_signed_out) @@ -973,7 +975,7 @@ impl Workspace { ConstrainedBox::new( Align::new( ConstrainedBox::new(avatar) - .with_width(theme.workspace.titlebar.icon_width) + .with_width(theme.workspace.titlebar.avatar_width) .boxed(), ) .boxed(), From e212461dfee3a9f175ef3dc5ed619ecfc5b82549 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 17:06:40 -0600 Subject: [PATCH 13/20] Authenticate when clicking on unauthenticated avatar in titlebar Co-Authored-By: Max Brunsfeld --- zed/src/workspace.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 1cef8caef082f45462d0a226f49a7a0b6f5bf019..bb69d18424b58e14ebcdf0f3ffd24837d53d48d2 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -12,7 +12,7 @@ use crate::{ settings::Settings, user, worktree::{File, Worktree}, - AppState, + AppState, Authenticate, }; use anyhow::{anyhow, Result}; use gpui::{ @@ -21,7 +21,7 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f}, json::to_string_pretty, keymap::Binding, - platform::WindowOptions, + platform::{CursorStyle, WindowOptions}, AnyViewHandle, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, WeakModelHandle, @@ -967,9 +967,14 @@ impl Workspace { .with_style(theme.workspace.titlebar.avatar) .boxed() } else { - Svg::new("icons/signed-out-12.svg") - .with_color(theme.workspace.titlebar.icon_signed_out) - .boxed() + MouseEventHandler::new::(0, cx, |_, _| { + Svg::new("icons/signed-out-12.svg") + .with_color(theme.workspace.titlebar.icon_signed_out) + .boxed() + }) + .on_click(|cx| cx.dispatch_action(Authenticate)) + .with_cursor_style(CursorStyle::PointingHand) + .boxed() }; ConstrainedBox::new( From 99a2dc4880636900173b400d5405fd4bd27cb51e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 17:47:43 -0600 Subject: [PATCH 14/20] Render an offline icon in titlebar when connection is lost Co-Authored-By: Max Brunsfeld --- gpui/src/elements/container.rs | 12 ++++---- gpui/src/views/select.rs | 4 +-- zed/assets/icons/offline-14.svg | 3 ++ zed/assets/themes/_base.toml | 8 +++-- zed/src/chat_panel.rs | 16 +++++----- zed/src/file_finder.rs | 8 ++--- zed/src/theme.rs | 10 ++++++- zed/src/theme_selector.rs | 8 ++--- zed/src/workspace.rs | 52 ++++++++++++++++++++++++++++----- zed/src/workspace/pane.rs | 17 ++++++++--- zed/src/workspace/sidebar.rs | 4 +-- 11 files changed, 102 insertions(+), 40 deletions(-) create mode 100644 zed/assets/icons/offline-14.svg diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index 48dcfa1b137986df78690a1adaf8e06249dae314..eeda6f206d139c210259744f9604fb30f90d1fae 100644 --- a/gpui/src/elements/container.rs +++ b/gpui/src/elements/container.rs @@ -13,7 +13,7 @@ use crate::{ Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Copy, Debug, Default, Deserialize)] pub struct ContainerStyle { #[serde(default)] pub margin: Margin, @@ -42,8 +42,8 @@ impl Container { } } - pub fn with_style(mut self, style: &ContainerStyle) -> Self { - self.style = style.clone(); + pub fn with_style(mut self, style: ContainerStyle) -> Self { + self.style = style; self } @@ -242,7 +242,7 @@ impl ToJson for ContainerStyle { } } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] pub struct Margin { pub top: f32, pub left: f32, @@ -269,7 +269,7 @@ impl ToJson for Margin { } } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] pub struct Padding { pub top: f32, pub left: f32, @@ -367,7 +367,7 @@ impl ToJson for Padding { } } -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Copy, Debug, Default, Deserialize)] pub struct Shadow { #[serde(default, deserialize_with = "deserialize_vec2f")] offset: Vector2F, diff --git a/gpui/src/views/select.rs b/gpui/src/views/select.rs index b9e099a75c17f954a294f3150c6e264ea612ad17..e257455a7afc3ede7f02fc3489d3855eb5444438 100644 --- a/gpui/src/views/select.rs +++ b/gpui/src/views/select.rs @@ -111,7 +111,7 @@ impl View for Select { mouse_state.hovered, cx, )) - .with_style(&style.header) + .with_style(style.header) .boxed() }) .on_click(move |cx| cx.dispatch_action(ToggleSelect)) @@ -158,7 +158,7 @@ impl View for Select { .with_max_height(200.) .boxed(), ) - .with_style(&style.menu) + .with_style(style.menu) .boxed(), ) .boxed(), diff --git a/zed/assets/icons/offline-14.svg b/zed/assets/icons/offline-14.svg new file mode 100644 index 0000000000000000000000000000000000000000..5349f65ead5f2ef87331f97352ef770ca0f33656 --- /dev/null +++ b/zed/assets/icons/offline-14.svg @@ -0,0 +1,3 @@ + + + diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index dfd27f396d35b5f68b0e5744c9ed424103be7839..485d3bf2a79e7269fb24462c1160d370c7bf1b89 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -9,9 +9,13 @@ pane_divider = { width = 1, color = "$border.0" } border = { width = 1, bottom = true, color = "$border.0" } title = "$text.0" avatar_width = 20 -icon_signed_out = "$text.2.color" +icon_color = "$text.2.color" avatar = { corner_radius = 10, border = { width = 1, color = "#00000088" } } +[workspace.titlebar.offline_icon] +padding = { right = 4 } +width = 16 + [workspace.tab] text = "$text.2" padding = { left = 10, right = 10 } @@ -29,7 +33,7 @@ background = "$surface.1" text = "$text.0" [workspace.sidebar] -width = 38 +width = 32 border = { right = true, width = 1, color = "$border.0" } [workspace.sidebar.resize_handle] diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 200a35fccae69b89be1e35e5655eb03642cf8aa2..5ccc014ca787119f6866254b20adc1ae79cab98f 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -209,7 +209,7 @@ impl ChatPanel { Flex::column() .with_child( Container::new(ChildView::new(self.channel_select.id()).boxed()) - .with_style(&theme.chat_panel.channel_select.container) + .with_style(theme.chat_panel.channel_select.container) .boxed(), ) .with_child(self.render_active_channel_messages()) @@ -243,7 +243,7 @@ impl ChatPanel { ) .boxed(), ) - .with_style(&theme.sender.container) + .with_style(theme.sender.container) .boxed(), ) .with_child( @@ -254,7 +254,7 @@ impl ChatPanel { ) .boxed(), ) - .with_style(&theme.timestamp.container) + .with_style(theme.timestamp.container) .boxed(), ) .boxed(), @@ -262,14 +262,14 @@ impl ChatPanel { .with_child(Text::new(message.body.clone(), theme.body.clone()).boxed()) .boxed(), ) - .with_style(&theme.container) + .with_style(theme.container) .boxed() } fn render_input_box(&self) -> ElementBox { let theme = &self.settings.borrow().theme; Container::new(ChildView::new(self.input_editor.id()).boxed()) - .with_style(&theme.chat_panel.input_editor.container) + .with_style(theme.chat_panel.input_editor.container) .boxed() } @@ -293,13 +293,13 @@ impl ChatPanel { Flex::row() .with_child( Container::new(Label::new("#".to_string(), theme.hash.text.clone()).boxed()) - .with_style(&theme.hash.container) + .with_style(theme.hash.container) .boxed(), ) .with_child(Label::new(channel.name.clone(), theme.name.clone()).boxed()) .boxed(), ) - .with_style(&theme.container) + .with_style(theme.container) .boxed() } @@ -387,7 +387,7 @@ impl View for ChatPanel { }; ConstrainedBox::new( Container::new(element) - .with_style(&theme.chat_panel.container) + .with_style(theme.chat_panel.container) .boxed(), ) .with_min_width(150.) diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 7e552d4748eef5368ade73f14b279c2ae834de7f..23bec79e0076046f3d483f9baa4972c7e418a2f1 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -88,13 +88,13 @@ impl View for FileFinder { Flex::new(Axis::Vertical) .with_child( Container::new(ChildView::new(self.query_editor.id()).boxed()) - .with_style(&settings.theme.selector.input_editor.container) + .with_style(settings.theme.selector.input_editor.container) .boxed(), ) .with_child(Flexible::new(1.0, self.render_matches()).boxed()) .boxed(), ) - .with_style(&settings.theme.selector.container) + .with_style(settings.theme.selector.container) .boxed(), ) .with_max_width(500.0) @@ -127,7 +127,7 @@ impl FileFinder { ) .boxed(), ) - .with_style(&settings.theme.selector.empty.container) + .with_style(settings.theme.selector.empty.container) .named("empty matches"); } @@ -200,7 +200,7 @@ impl FileFinder { ) .boxed(), ) - .with_style(&style.container); + .with_style(style.container); let action = Select(Entry { worktree_id: path_match.tree_id, diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 96e562a55ffc45ebb593010699fed06bedf65160..88f385a05461b18a0db3f6182e9b3d08effb97b2 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -48,10 +48,18 @@ pub struct Titlebar { pub container: ContainerStyle, pub title: TextStyle, pub avatar_width: f32, - pub icon_signed_out: Color, + pub offline_icon: OfflineIcon, + pub icon_color: Color, pub avatar: ImageStyle, } +#[derive(Clone, Deserialize)] +pub struct OfflineIcon { + #[serde(flatten)] + pub container: ContainerStyle, + pub width: f32, +} + #[derive(Clone, Deserialize)] pub struct Tab { #[serde(flatten)] diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index 09f259281a45e3451b2d9810230f4bddfa35fb52..6a623b120e6a3fcdc5eb679b2fcdd22c18ba6ec4 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -214,7 +214,7 @@ impl ThemeSelector { ) .boxed(), ) - .with_style(&settings.theme.selector.empty.container) + .with_style(settings.theme.selector.empty.container) .named("empty matches"); } @@ -259,9 +259,9 @@ impl ThemeSelector { .boxed(), ) .with_style(if index == self.selected_index { - &theme.selector.active_item.container + theme.selector.active_item.container } else { - &theme.selector.item.container + theme.selector.item.container }); container.boxed() @@ -288,7 +288,7 @@ impl View for ThemeSelector { .with_child(Flexible::new(1.0, self.render_matches(cx)).boxed()) .boxed(), ) - .with_style(&settings.theme.selector.container) + .with_style(settings.theme.selector.container) .boxed(), ) .with_max_width(600.0) diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index bb69d18424b58e14ebcdf0f3ffd24837d53d48d2..cbdf3b149a532421b92c37a39e1c07106cccbcfd 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -31,7 +31,6 @@ pub use pane::*; pub use pane_group::*; use postage::{prelude::Stream, watch}; use sidebar::{Side, Sidebar, ToggleSidebarItem}; -use smol::prelude::*; use std::{ collections::{hash_map::Entry, HashMap, HashSet}, future::Future, @@ -391,9 +390,14 @@ impl Workspace { right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into()); let mut current_user = app_state.user_store.current_user().clone(); + let mut connection_status = app_state.rpc.status().clone(); let _observe_current_user = cx.spawn_weak(|this, mut cx| async move { current_user.recv().await; - while current_user.recv().await.is_some() { + connection_status.recv().await; + let mut stream = + Stream::map(current_user, drop).merge(Stream::map(connection_status, drop)); + + while stream.recv().await.is_some() { cx.update(|cx| { if let Some(this) = this.upgrade(&cx) { this.update(cx, |_, cx| cx.notify()); @@ -642,7 +646,7 @@ impl Workspace { if let Some(load_result) = watch.borrow().as_ref() { break load_result.clone(); } - watch.next().await; + watch.recv().await; }; this.update(&mut cx, |this, cx| { @@ -954,7 +958,34 @@ impl Workspace { &self.active_pane } - fn render_current_user(&self, cx: &mut RenderContext) -> ElementBox { + fn render_connection_status(&self) -> Option { + let theme = &self.settings.borrow().theme; + match dbg!(&*self.rpc.status().borrow()) { + rpc::Status::ConnectionError + | rpc::Status::ConnectionLost + | rpc::Status::Reauthenticating + | rpc::Status::Reconnecting { .. } + | rpc::Status::ReconnectionError { .. } => Some( + Container::new( + Align::new( + ConstrainedBox::new( + Svg::new("icons/offline-14.svg") + .with_color(theme.workspace.titlebar.icon_color) + .boxed(), + ) + .with_width(theme.workspace.titlebar.offline_icon.width) + .boxed(), + ) + .boxed(), + ) + .with_style(theme.workspace.titlebar.offline_icon.container) + .boxed(), + ), + _ => None, + } + } + + fn render_avatar(&self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme; let avatar = if let Some(avatar) = self .user_store @@ -969,7 +1000,7 @@ impl Workspace { } else { MouseEventHandler::new::(0, cx, |_, _| { Svg::new("icons/signed-out-12.svg") - .with_color(theme.workspace.titlebar.icon_signed_out) + .with_color(theme.workspace.titlebar.icon_color) .boxed() }) .on_click(|cx| cx.dispatch_action(Authenticate)) @@ -1019,11 +1050,18 @@ impl View for Workspace { .boxed(), ) .with_child( - Align::new(self.render_current_user(cx)).right().boxed(), + Align::new( + Flex::row() + .with_children(self.render_connection_status()) + .with_child(self.render_avatar(cx)) + .boxed(), + ) + .right() + .boxed(), ) .boxed(), ) - .with_style(&theme.workspace.titlebar.container) + .with_style(theme.workspace.titlebar.container) .boxed(), ) .with_height(32.) diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 66a062fb65d5355e3c98f8c62f41153f71fe1f42..c0cd6bb9fd7e47141cc633a55cb9eeb00e48ca81 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -1,6 +1,14 @@ use super::{ItemViewHandle, SplitDirection}; use crate::settings::Settings; -use gpui::{Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle, action, color::Color, elements::*, geometry::{rect::RectF, vector::vec2f}, keymap::Binding, platform::CursorStyle}; +use gpui::{ + action, + color::Color, + elements::*, + geometry::{rect::RectF, vector::vec2f}, + keymap::Binding, + platform::CursorStyle, + Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle, +}; use postage::watch; use std::{cmp, path::Path, sync::Arc}; @@ -256,7 +264,7 @@ impl Pane { ) .boxed(), ) - .with_style(&ContainerStyle { + .with_style(ContainerStyle { margin: Margin { left: style.spacing, right: style.spacing, @@ -283,7 +291,8 @@ impl Pane { icon.with_color(style.icon_close).boxed() } }, - ).with_cursor_style(CursorStyle::PointingHand) + ) + .with_cursor_style(CursorStyle::PointingHand) .on_click(move |cx| { cx.dispatch_action(CloseItem(item_id)) }) @@ -298,7 +307,7 @@ impl Pane { ) .boxed(), ) - .with_style(&style.container) + .with_style(style.container) .boxed(), ) .on_mouse_down(move |cx| { diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index 7cdb6447a7ff55f8deece33ab5dcbf9738badeb0..3ceba15df0051701873c472a993ced0fe0b0ed64 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -113,7 +113,7 @@ impl Sidebar { })) .boxed(), ) - .with_style(&theme.container) + .with_style(theme.container) .boxed(), ) .with_width(theme.width) @@ -165,7 +165,7 @@ impl Sidebar { let side = self.side; MouseEventHandler::new::(self.side.id(), &mut cx, |_, _| { Container::new(Empty::new().boxed()) - .with_style(&self.theme(settings).resize_handle) + .with_style(self.theme(settings).resize_handle) .boxed() }) .with_padding(Padding { From aa7c1bfa2d2f418a97c8206e19ff46ee78f34afc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 14 Sep 2021 17:15:17 -0700 Subject: [PATCH 15/20] Fix type errors in server tests --- server/src/rpc.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index d4cc596810d51acf40fd2267112c093c3955d2eb..912f57fcf714e8a892c27accf73ee14ee5c76ba3 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1484,10 +1484,10 @@ mod tests { #[gpui::test] async fn test_basic_chat(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); + let http = FakeHttpClient::new(|_| async move { Ok(surf::http::Response::new(404)) }); // Connect to a server as 2 clients. let mut server = TestServer::start().await; - let mut http = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) }); 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; @@ -1514,7 +1514,8 @@ mod tests { .await .unwrap(); - let user_store_a = UserStore::new(client_a.clone(), cx_a.background().as_ref()); + let user_store_a = + UserStore::new(client_a.clone(), http.clone(), cx_a.background().as_ref()); 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()) @@ -1539,7 +1540,8 @@ mod tests { }) .await; - let user_store_b = UserStore::new(client_b.clone(), cx_b.background().as_ref()); + let user_store_b = + UserStore::new(client_b.clone(), http.clone(), cx_b.background().as_ref()); 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()) @@ -1627,6 +1629,7 @@ mod tests { #[gpui::test] async fn test_chat_message_validation(mut cx_a: TestAppContext) { cx_a.foreground().forbid_parking(); + let http = FakeHttpClient::new(|_| async move { Ok(surf::http::Response::new(404)) }); let mut server = TestServer::start().await; let (user_id_a, client_a) = server.create_client(&mut cx_a, "user_a").await; @@ -1639,7 +1642,7 @@ mod tests { .await .unwrap(); - let user_store_a = UserStore::new(client_a.clone(), cx_a.background().as_ref()); + let user_store_a = UserStore::new(client_a.clone(), http, cx_a.background().as_ref()); 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()) @@ -1685,6 +1688,7 @@ mod tests { #[gpui::test] async fn test_chat_reconnection(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); + let http = FakeHttpClient::new(|_| async move { Ok(surf::http::Response::new(404)) }); // Connect to a server as 2 clients. let mut server = TestServer::start().await; @@ -1715,7 +1719,8 @@ mod tests { .await .unwrap(); - let user_store_a = UserStore::new(client_a.clone(), cx_a.background().as_ref()); + let user_store_a = + UserStore::new(client_a.clone(), http.clone(), cx_a.background().as_ref()); 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()) @@ -1741,7 +1746,8 @@ mod tests { }) .await; - let user_store_b = UserStore::new(client_b.clone(), cx_b.background().as_ref()); + let user_store_b = + UserStore::new(client_b.clone(), http.clone(), cx_b.background().as_ref()); 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()) From 44a457e8b6e01fdeca6fbade1bdf2119d657214d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 18:21:46 -0600 Subject: [PATCH 16/20] Cache credentials in memory separately from connection status This prevents us from re-prompting for keychain access when we retry connections after the connection is lost. Co-Authored-By: Max Brunsfeld --- server/src/rpc.rs | 67 ++++++++++----------- zed/src/rpc.rs | 146 ++++++++++++++++++++++++++-------------------- zed/src/test.rs | 39 +++++++------ zed/src/user.rs | 4 +- 4 files changed, 139 insertions(+), 117 deletions(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 912f57fcf714e8a892c27accf73ee14ee5c76ba3..86f369fb8a12296569ab8100baa67ce33f9e45d1 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1023,7 +1023,7 @@ mod tests { editor::{Editor, Insert}, fs::{FakeFs, Fs as _}, language::LanguageRegistry, - rpc::{self, Client}, + rpc::{self, Client, Credentials}, settings, test::FakeHttpClient, user::UserStore, @@ -1922,39 +1922,40 @@ mod tests { let forbid_connections = self.forbid_connections.clone(); Arc::get_mut(&mut client) .unwrap() - .set_login_and_connect_callbacks( - move |cx| { - cx.spawn(|_| async move { - let access_token = "the-token".to_string(); - Ok((client_user_id.0 as u64, access_token)) + .override_authenticate(move |cx| { + cx.spawn(|_| async move { + let access_token = "the-token".to_string(); + Ok(Credentials { + user_id: client_user_id.0 as u64, + access_token, }) - }, - move |user_id, access_token, cx| { - assert_eq!(user_id, client_user_id.0 as u64); - assert_eq!(access_token, "the-token"); - - let server = server.clone(); - let connection_killers = connection_killers.clone(); - let forbid_connections = forbid_connections.clone(); - let client_name = client_name.clone(); - cx.spawn(move |cx| async move { - if forbid_connections.load(SeqCst) { - Err(anyhow!("server is forbidding connections")) - } else { - let (client_conn, server_conn, kill_conn) = Conn::in_memory(); - connection_killers.lock().insert(client_user_id, kill_conn); - cx.background() - .spawn(server.handle_connection( - server_conn, - client_name, - client_user_id, - )) - .detach(); - Ok(client_conn) - } - }) - }, - ); + }) + }) + .override_establish_connection(move |credentials, cx| { + assert_eq!(credentials.user_id, client_user_id.0 as u64); + assert_eq!(credentials.access_token, "the-token"); + + let server = server.clone(); + let connection_killers = connection_killers.clone(); + let forbid_connections = forbid_connections.clone(); + let client_name = client_name.clone(); + cx.spawn(move |cx| async move { + if forbid_connections.load(SeqCst) { + Err(anyhow!("server is forbidding connections")) + } else { + let (client_conn, server_conn, kill_conn) = Conn::in_memory(); + connection_killers.lock().insert(client_user_id, kill_conn); + cx.background() + .spawn(server.handle_connection( + server_conn, + client_name, + client_user_id, + )) + .detach(); + Ok(client_conn) + } + }) + }); client .authenticate_and_connect(&cx.to_async()) diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 69bc33a62e1c4065f7c946b239e72d7039ef5c49..bc6b41dd5f523e40e239f78465ed246b38c77bfd 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -29,11 +29,10 @@ lazy_static! { pub struct Client { peer: Arc, state: RwLock, - auth_callback: Option< - Box Task>>, - >, - connect_callback: Option< - Box Task>>, + authenticate: + Option Task>>>, + establish_connection: Option< + Box Task>>, >, } @@ -41,25 +40,17 @@ pub struct Client { pub enum Status { SignedOut, Authenticating, - Connecting { - user_id: u64, - }, + Connecting, ConnectionError, - Connected { - connection_id: ConnectionId, - user_id: u64, - }, + Connected { connection_id: ConnectionId }, ConnectionLost, Reauthenticating, - Reconnecting { - user_id: u64, - }, - ReconnectionError { - next_reconnection: Instant, - }, + Reconnecting, + ReconnectionError { next_reconnection: Instant }, } struct ClientState { + credentials: Option, status: (watch::Sender, watch::Receiver), entity_id_extractors: HashMap u64>>, model_handlers: HashMap< @@ -70,9 +61,16 @@ struct ClientState { heartbeat_interval: Duration, } +#[derive(Clone)] +pub struct Credentials { + pub user_id: u64, + pub access_token: String, +} + impl Default for ClientState { fn default() -> Self { Self { + credentials: None, status: watch::channel_with(Status::SignedOut), entity_id_extractors: Default::default(), model_handlers: Default::default(), @@ -107,22 +105,35 @@ impl Client { Arc::new(Self { peer: Peer::new(), state: Default::default(), - auth_callback: None, - connect_callback: None, + authenticate: None, + establish_connection: None, }) } #[cfg(any(test, feature = "test-support"))] - pub fn set_login_and_connect_callbacks( - &mut self, - login: Login, - connect: Connect, - ) where - Login: 'static + Send + Sync + Fn(&AsyncAppContext) -> Task>, - Connect: 'static + Send + Sync + Fn(u64, &str, &AsyncAppContext) -> Task>, + pub fn override_authenticate(&mut self, authenticate: F) -> &mut Self + where + F: 'static + Send + Sync + Fn(&AsyncAppContext) -> Task>, { - self.auth_callback = Some(Box::new(login)); - self.connect_callback = Some(Box::new(connect)); + self.authenticate = Some(Box::new(authenticate)); + self + } + + #[cfg(any(test, feature = "test-support"))] + pub fn override_establish_connection(&mut self, connect: F) -> &mut Self + where + F: 'static + Send + Sync + Fn(&Credentials, &AsyncAppContext) -> Task>, + { + self.establish_connection = Some(Box::new(connect)); + self + } + + pub fn user_id(&self) -> Option { + self.state + .read() + .credentials + .as_ref() + .map(|credentials| credentials.user_id) } pub fn status(&self) -> watch::Receiver { @@ -249,23 +260,31 @@ impl Client { self.set_status(Status::Reauthenticating, cx) } - let (user_id, access_token) = match self.authenticate(&cx).await { - Ok(result) => result, - Err(err) => { - self.set_status(Status::ConnectionError, cx); - return Err(err); - } + let credentials = self.state.read().credentials.clone(); + let credentials = if let Some(credentials) = credentials { + credentials + } else { + let credentials = match self.authenticate(&cx).await { + Ok(credentials) => credentials, + Err(err) => { + self.set_status(Status::ConnectionError, cx); + return Err(err); + } + }; + self.state.write().credentials = Some(credentials.clone()); + credentials }; if was_disconnected { - self.set_status(Status::Connecting { user_id }, cx); + self.set_status(Status::Connecting, cx); } else { - self.set_status(Status::Reconnecting { user_id }, cx); + self.set_status(Status::Reconnecting, cx); } - match self.connect(user_id, &access_token, cx).await { + + match self.establish_connection(&credentials, cx).await { Ok(conn) => { log::info!("connected to rpc address {}", *ZED_SERVER_URL); - self.set_connection(user_id, conn, cx).await; + self.set_connection(conn, cx).await; Ok(()) } Err(err) => { @@ -275,7 +294,7 @@ impl Client { } } - async fn set_connection(self: &Arc, user_id: u64, conn: Conn, cx: &AsyncAppContext) { + async fn set_connection(self: &Arc, conn: Conn, cx: &AsyncAppContext) { let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn).await; cx.foreground() .spawn({ @@ -310,13 +329,7 @@ impl Client { }) .detach(); - self.set_status( - Status::Connected { - connection_id, - user_id, - }, - cx, - ); + self.set_status(Status::Connected { connection_id }, cx); let handle_io = cx.background().spawn(handle_io); let this = self.clone(); @@ -334,35 +347,35 @@ impl Client { .detach(); } - fn authenticate(self: &Arc, cx: &AsyncAppContext) -> Task> { - if let Some(callback) = self.auth_callback.as_ref() { + fn authenticate(self: &Arc, cx: &AsyncAppContext) -> Task> { + if let Some(callback) = self.authenticate.as_ref() { callback(cx) } else { self.authenticate_with_browser(cx) } } - fn connect( + fn establish_connection( self: &Arc, - user_id: u64, - access_token: &str, + credentials: &Credentials, cx: &AsyncAppContext, ) -> Task> { - if let Some(callback) = self.connect_callback.as_ref() { - callback(user_id, access_token, cx) + if let Some(callback) = self.establish_connection.as_ref() { + callback(credentials, cx) } else { - self.connect_with_websocket(user_id, access_token, cx) + self.establish_websocket_connection(credentials, cx) } } - fn connect_with_websocket( + fn establish_websocket_connection( self: &Arc, - user_id: u64, - access_token: &str, + credentials: &Credentials, cx: &AsyncAppContext, ) -> Task> { - let request = - Request::builder().header("Authorization", format!("{} {}", user_id, access_token)); + let request = Request::builder().header( + "Authorization", + format!("{} {}", credentials.user_id, credentials.access_token), + ); cx.background().spawn(async move { if let Some(host) = ZED_SERVER_URL.strip_prefix("https://") { let stream = smol::net::TcpStream::connect(host).await?; @@ -387,7 +400,7 @@ impl Client { pub fn authenticate_with_browser( self: &Arc, cx: &AsyncAppContext, - ) -> Task> { + ) -> Task> { let platform = cx.platform(); let executor = cx.background(); executor.clone().spawn(async move { @@ -397,7 +410,10 @@ impl Client { .flatten() { log::info!("already signed in. user_id: {}", user_id); - return Ok((user_id.parse()?, String::from_utf8(access_token).unwrap())); + return Ok(Credentials { + user_id: user_id.parse()?, + access_token: String::from_utf8(access_token).unwrap(), + }); } // Generate a pair of asymmetric encryption keys. The public key will be used by the @@ -463,7 +479,11 @@ impl Client { platform .write_credentials(&ZED_SERVER_URL, &user_id, access_token.as_bytes()) .log_err(); - Ok((user_id.parse()?, access_token)) + + Ok(Credentials { + user_id: user_id.parse()?, + access_token, + }) }) } diff --git a/zed/src/test.rs b/zed/src/test.rs index e8527a4ed762a057fbcd72355fa4783024e57f40..b9948cc460f66d7891b3321dc6bb09d34b207c5a 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -4,7 +4,7 @@ use crate::{ fs::RealFs, http::{HttpClient, Request, Response, ServerResponse}, language::LanguageRegistry, - rpc::{self, Client}, + rpc::{self, Client, Credentials}, settings::{self, ThemeRegistry}, time::ReplicaId, user::UserStore, @@ -226,25 +226,26 @@ impl FakeServer { Arc::get_mut(client) .unwrap() - .set_login_and_connect_callbacks( - move |cx| { - cx.spawn(|_| async move { - let access_token = "the-token".to_string(); - Ok((client_user_id, access_token)) + .override_authenticate(move |cx| { + cx.spawn(|_| async move { + let access_token = "the-token".to_string(); + Ok(Credentials { + user_id: client_user_id, + access_token, }) - }, - { - let server = result.clone(); - move |user_id, access_token, cx| { - assert_eq!(user_id, client_user_id); - assert_eq!(access_token, "the-token"); - cx.spawn({ - let server = server.clone(); - move |cx| async move { server.connect(&cx).await } - }) - } - }, - ); + }) + }) + .override_establish_connection({ + let server = result.clone(); + move |credentials, cx| { + assert_eq!(credentials.user_id, client_user_id); + assert_eq!(credentials.access_token, "the-token"); + cx.spawn({ + let server = server.clone(); + move |cx| async move { server.connect(&cx).await } + }) + } + }); client .authenticate_and_connect(&cx.to_async()) diff --git a/zed/src/user.rs b/zed/src/user.rs index 3a119474e1e7397b5c5352ffb13cddd27f98ae9c..06aab321934dd1ffb985430157b7cfd02385dbc7 100644 --- a/zed/src/user.rs +++ b/zed/src/user.rs @@ -51,8 +51,8 @@ impl UserStore { let mut status = rpc.status(); while let Some(status) = status.recv().await { match status { - Status::Connected { user_id, .. } => { - if let Some(this) = this.upgrade() { + Status::Connected { .. } => { + if let Some((this, user_id)) = this.upgrade().zip(rpc.user_id()) { current_user_tx .send(this.fetch_user(user_id).log_err().await) .await From 77a4a36eb3a483e7e4e5a7c91f5b4924db347123 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 18:30:17 -0600 Subject: [PATCH 17/20] Test that we reuse credentials when reconnecting Co-Authored-By: Max Brunsfeld --- zed/src/rpc.rs | 2 ++ zed/src/test.rs | 32 +++++++++++++++++++++----------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index bc6b41dd5f523e40e239f78465ed246b38c77bfd..8846b02f4bdfaff4adefea9e7060e6fc48480a09 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -581,6 +581,7 @@ mod tests { status.recv().await, Some(Status::Connected { .. }) )); + assert_eq!(server.auth_count(), 1); server.forbid_connections(); server.disconnect().await; @@ -589,6 +590,7 @@ mod tests { server.allow_connections(); cx.foreground().advance_clock(Duration::from_secs(10)); while !matches!(status.recv().await, Some(Status::Connected { .. })) {} + assert_eq!(server.auth_count(), 1); // Client reused the cached credentials when reconnecting } #[test] diff --git a/zed/src/test.rs b/zed/src/test.rs index b9948cc460f66d7891b3321dc6bb09d34b207c5a..4f28db9d2808874ee336a64ca3f0f496bd9aeaa7 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -21,7 +21,7 @@ use std::{ marker::PhantomData, path::{Path, PathBuf}, sync::{ - atomic::{AtomicBool, Ordering::SeqCst}, + atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, Arc, }, }; @@ -209,6 +209,7 @@ pub struct FakeServer { incoming: Mutex>>>, connection_id: Mutex>, forbid_connections: AtomicBool, + auth_count: AtomicUsize, } impl FakeServer { @@ -217,26 +218,31 @@ impl FakeServer { client: &mut Arc, cx: &TestAppContext, ) -> Arc { - let result = Arc::new(Self { + let server = Arc::new(Self { peer: Peer::new(), incoming: Default::default(), connection_id: Default::default(), forbid_connections: Default::default(), + auth_count: Default::default(), }); Arc::get_mut(client) .unwrap() - .override_authenticate(move |cx| { - cx.spawn(|_| async move { - let access_token = "the-token".to_string(); - Ok(Credentials { - user_id: client_user_id, - access_token, + .override_authenticate({ + let server = server.clone(); + move |cx| { + server.auth_count.fetch_add(1, SeqCst); + cx.spawn(move |_| async move { + let access_token = "the-token".to_string(); + Ok(Credentials { + user_id: client_user_id, + access_token, + }) }) - }) + } }) .override_establish_connection({ - let server = result.clone(); + let server = server.clone(); move |credentials, cx| { assert_eq!(credentials.user_id, client_user_id); assert_eq!(credentials.access_token, "the-token"); @@ -251,7 +257,7 @@ impl FakeServer { .authenticate_and_connect(&cx.to_async()) .await .unwrap(); - result + server } pub async fn disconnect(&self) { @@ -273,6 +279,10 @@ impl FakeServer { } } + pub fn auth_count(&self) -> usize { + self.auth_count.load(SeqCst) + } + pub fn forbid_connections(&self) { self.forbid_connections.store(true, SeqCst); } From 4a9918979e77ef03357fa3ec76e23a523ca126b0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 19:19:11 -0600 Subject: [PATCH 18/20] WIP: Clear cached credentials if authentication fails Still need to actually handle an HTTP response from the server indicating there was an invalid token. Co-Authored-By: Max Brunsfeld --- Cargo.lock | 9 ++--- server/src/rpc.rs | 20 ++++++----- zed/Cargo.toml | 1 + zed/src/rpc.rs | 91 ++++++++++++++++++++++++++++++++++++++--------- zed/src/test.rs | 49 +++++++++++++++++-------- zrpc/src/conn.rs | 4 +-- zrpc/src/lib.rs | 2 +- zrpc/src/peer.rs | 40 ++++++++++----------- 8 files changed, 149 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c749b35f2dc5e329fea6b282a3cab690f7156e6f..aea0c49da87ae5aeed63e66f65aad561b1471870 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5108,18 +5108,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.24" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.24" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c" dependencies = [ "proc-macro2", "quote", @@ -5914,6 +5914,7 @@ dependencies = [ "smol", "surf", "tempdir", + "thiserror", "time 0.3.2", "tiny_http", "toml 0.5.8", diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 86f369fb8a12296569ab8100baa67ce33f9e45d1..1e0fe2465cafbe2d7034e941500145cda25ebd1d 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -27,7 +27,7 @@ use time::OffsetDateTime; use zrpc::{ auth::random_token, proto::{self, AnyTypedEnvelope, EnvelopedMessage}, - Conn, ConnectionId, Peer, TypedEnvelope, + Connection, ConnectionId, Peer, TypedEnvelope, }; type ReplicaId = u16; @@ -48,13 +48,13 @@ pub struct Server { #[derive(Default)] struct ServerState { - connections: HashMap, + connections: HashMap, pub worktrees: HashMap, channels: HashMap, next_worktree_id: u64, } -struct Connection { +struct ConnectionState { user_id: UserId, worktrees: HashSet, channels: HashSet, @@ -133,7 +133,7 @@ impl Server { pub fn handle_connection( self: &Arc, - connection: Conn, + connection: Connection, addr: String, user_id: UserId, ) -> impl Future { @@ -211,7 +211,7 @@ impl Server { async fn add_connection(&self, connection_id: ConnectionId, user_id: UserId) { self.state.write().await.connections.insert( connection_id, - Connection { + ConnectionState { user_id, worktrees: Default::default(), channels: Default::default(), @@ -972,7 +972,7 @@ pub fn add_routes(app: &mut tide::Server>, rpc: &Arc) { 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 { - server.handle_connection(Conn::new(WebSocketStream::from_raw_socket(stream, Role::Server, None).await), addr, user_id).await; + server.handle_connection(Connection::new(WebSocketStream::from_raw_socket(stream, Role::Server, None).await), addr, user_id).await; } }); @@ -1023,7 +1023,7 @@ mod tests { editor::{Editor, Insert}, fs::{FakeFs, Fs as _}, language::LanguageRegistry, - rpc::{self, Client, Credentials}, + rpc::{self, Client, Credentials, EstablishConnectionError}, settings, test::FakeHttpClient, user::UserStore, @@ -1941,9 +1941,11 @@ mod tests { let client_name = client_name.clone(); cx.spawn(move |cx| async move { if forbid_connections.load(SeqCst) { - Err(anyhow!("server is forbidding connections")) + Err(EstablishConnectionError::other(anyhow!( + "server is forbidding connections" + ))) } else { - let (client_conn, server_conn, kill_conn) = Conn::in_memory(); + let (client_conn, server_conn, kill_conn) = Connection::in_memory(); connection_killers.lock().insert(client_user_id, kill_conn); cx.background() .spawn(server.handle_connection( diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 2b36db9ba86152b040fa6edcf74da5c6bc68a0d2..d9c2cc6a584535caace7d267b5a04b10dfffb721 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -50,6 +50,7 @@ smallvec = { version = "1.6", features = ["union"] } smol = "1.2.5" surf = "2.2" tempdir = { version = "0.3.7", optional = true } +thiserror = "1.0.29" time = { version = "0.3" } tiny_http = "0.8" toml = "0.5" diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 8846b02f4bdfaff4adefea9e7060e6fc48480a09..3526381cde572ba127775a01aca2faa194cf1f1e 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -15,10 +15,11 @@ use std::{ time::{Duration, Instant}, }; use surf::Url; +use thiserror::Error; pub use zrpc::{proto, ConnectionId, PeerId, TypedEnvelope}; use zrpc::{ proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage}, - Conn, Peer, Receipt, + Connection, Peer, Receipt, }; lazy_static! { @@ -32,10 +33,32 @@ pub struct Client { authenticate: Option Task>>>, establish_connection: Option< - Box Task>>, + Box< + dyn 'static + + Send + + Sync + + Fn( + &Credentials, + &AsyncAppContext, + ) -> Task>, + >, >, } +#[derive(Error, Debug)] +pub enum EstablishConnectionError { + #[error("invalid access token")] + InvalidAccessToken, + #[error("{0}")] + Other(anyhow::Error), +} + +impl EstablishConnectionError { + pub fn other(error: impl Into + Send + Sync) -> Self { + Self::Other(error.into()) + } +} + #[derive(Copy, Clone, Debug)] pub enum Status { SignedOut, @@ -122,7 +145,10 @@ impl Client { #[cfg(any(test, feature = "test-support"))] pub fn override_establish_connection(&mut self, connect: F) -> &mut Self where - F: 'static + Send + Sync + Fn(&Credentials, &AsyncAppContext) -> Task>, + F: 'static + + Send + + Sync + + Fn(&Credentials, &AsyncAppContext) -> Task>, { self.establish_connection = Some(Box::new(connect)); self @@ -288,13 +314,18 @@ impl Client { Ok(()) } Err(err) => { + eprintln!("error in authenticate and connect {}", err); + if matches!(err, EstablishConnectionError::InvalidAccessToken) { + eprintln!("nuking credentials"); + self.state.write().credentials.take(); + } self.set_status(Status::ConnectionError, cx); - Err(err) + Err(err)? } } } - async fn set_connection(self: &Arc, conn: Conn, cx: &AsyncAppContext) { + async fn set_connection(self: &Arc, conn: Connection, cx: &AsyncAppContext) { let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn).await; cx.foreground() .spawn({ @@ -359,7 +390,7 @@ impl Client { self: &Arc, credentials: &Credentials, cx: &AsyncAppContext, - ) -> Task> { + ) -> Task> { if let Some(callback) = self.establish_connection.as_ref() { callback(credentials, cx) } else { @@ -371,28 +402,43 @@ impl Client { self: &Arc, credentials: &Credentials, cx: &AsyncAppContext, - ) -> Task> { + ) -> Task> { let request = Request::builder().header( "Authorization", format!("{} {}", credentials.user_id, credentials.access_token), ); cx.background().spawn(async move { if let Some(host) = ZED_SERVER_URL.strip_prefix("https://") { - let stream = smol::net::TcpStream::connect(host).await?; - let request = request.uri(format!("wss://{}/rpc", host)).body(())?; + let stream = smol::net::TcpStream::connect(host) + .await + .map_err(EstablishConnectionError::other)?; + let request = request + .uri(format!("wss://{}/rpc", host)) + .body(()) + .map_err(EstablishConnectionError::other)?; let (stream, _) = async_tungstenite::async_tls::client_async_tls(request, stream) .await - .context("websocket handshake")?; - Ok(Conn::new(stream)) + .context("websocket handshake") + .map_err(EstablishConnectionError::other)?; + Ok(Connection::new(stream)) } else if let Some(host) = ZED_SERVER_URL.strip_prefix("http://") { - let stream = smol::net::TcpStream::connect(host).await?; - let request = request.uri(format!("ws://{}/rpc", host)).body(())?; + let stream = smol::net::TcpStream::connect(host) + .await + .map_err(EstablishConnectionError::other)?; + let request = request + .uri(format!("ws://{}/rpc", host)) + .body(()) + .map_err(EstablishConnectionError::other)?; let (stream, _) = async_tungstenite::client_async(request, stream) .await - .context("websocket handshake")?; - Ok(Conn::new(stream)) + .context("websocket handshake") + .map_err(EstablishConnectionError::other)?; + Ok(Connection::new(stream)) } else { - Err(anyhow!("invalid server url: {}", *ZED_SERVER_URL)) + Err(EstablishConnectionError::other(anyhow!( + "invalid server url: {}", + *ZED_SERVER_URL + ))) } }) } @@ -591,6 +637,19 @@ mod tests { cx.foreground().advance_clock(Duration::from_secs(10)); while !matches!(status.recv().await, Some(Status::Connected { .. })) {} assert_eq!(server.auth_count(), 1); // Client reused the cached credentials when reconnecting + + server.forbid_connections(); + server.disconnect().await; + while !matches!(status.recv().await, Some(Status::ReconnectionError { .. })) {} + + // Clear cached credentials after authentication fails + server.roll_access_token(); + server.allow_connections(); + cx.foreground().advance_clock(Duration::from_secs(10)); + assert_eq!(server.auth_count(), 1); + cx.foreground().advance_clock(Duration::from_secs(10)); + while !matches!(status.recv().await, Some(Status::Connected { .. })) {} + assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token } #[test] diff --git a/zed/src/test.rs b/zed/src/test.rs index 4f28db9d2808874ee336a64ca3f0f496bd9aeaa7..e5ab3154f526e3090774fbf02ae338c6749f03ab 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -4,7 +4,7 @@ use crate::{ fs::RealFs, http::{HttpClient, Request, Response, ServerResponse}, language::LanguageRegistry, - rpc::{self, Client, Credentials}, + rpc::{self, Client, Credentials, EstablishConnectionError}, settings::{self, ThemeRegistry}, time::ReplicaId, user::UserStore, @@ -26,7 +26,7 @@ use std::{ }, }; use tempdir::TempDir; -use zrpc::{proto, Conn, ConnectionId, Peer, Receipt, TypedEnvelope}; +use zrpc::{proto, Connection, ConnectionId, Peer, Receipt, TypedEnvelope}; #[cfg(test)] #[ctor::ctor] @@ -210,6 +210,8 @@ pub struct FakeServer { connection_id: Mutex>, forbid_connections: AtomicBool, auth_count: AtomicUsize, + access_token: AtomicUsize, + user_id: u64, } impl FakeServer { @@ -224,6 +226,8 @@ impl FakeServer { connection_id: Default::default(), forbid_connections: Default::default(), auth_count: Default::default(), + access_token: Default::default(), + user_id: client_user_id, }); Arc::get_mut(client) @@ -232,8 +236,8 @@ impl FakeServer { let server = server.clone(); move |cx| { server.auth_count.fetch_add(1, SeqCst); + let access_token = server.access_token.load(SeqCst).to_string(); cx.spawn(move |_| async move { - let access_token = "the-token".to_string(); Ok(Credentials { user_id: client_user_id, access_token, @@ -244,11 +248,10 @@ impl FakeServer { .override_establish_connection({ let server = server.clone(); move |credentials, cx| { - assert_eq!(credentials.user_id, client_user_id); - assert_eq!(credentials.access_token, "the-token"); + let credentials = credentials.clone(); cx.spawn({ let server = server.clone(); - move |cx| async move { server.connect(&cx).await } + move |cx| async move { server.establish_connection(&credentials, &cx).await } }) } }); @@ -266,23 +269,39 @@ impl FakeServer { self.incoming.lock().take(); } - async fn connect(&self, cx: &AsyncAppContext) -> Result { + async fn establish_connection( + &self, + credentials: &Credentials, + cx: &AsyncAppContext, + ) -> Result { + assert_eq!(credentials.user_id, self.user_id); + if self.forbid_connections.load(SeqCst) { - Err(anyhow!("server is forbidding connections")) - } else { - let (client_conn, server_conn, _) = Conn::in_memory(); - let (connection_id, io, incoming) = self.peer.add_connection(server_conn).await; - cx.background().spawn(io).detach(); - *self.incoming.lock() = Some(incoming); - *self.connection_id.lock() = Some(connection_id); - Ok(client_conn) + Err(EstablishConnectionError::Other(anyhow!( + "server is forbidding connections" + )))? + } + + if credentials.access_token != self.access_token.load(SeqCst).to_string() { + Err(EstablishConnectionError::InvalidAccessToken)? } + + let (client_conn, server_conn, _) = Connection::in_memory(); + let (connection_id, io, incoming) = self.peer.add_connection(server_conn).await; + cx.background().spawn(io).detach(); + *self.incoming.lock() = Some(incoming); + *self.connection_id.lock() = Some(connection_id); + Ok(client_conn) } pub fn auth_count(&self) -> usize { self.auth_count.load(SeqCst) } + pub fn roll_access_token(&self) { + self.access_token.fetch_add(1, SeqCst); + } + pub fn forbid_connections(&self) { self.forbid_connections.store(true, SeqCst); } diff --git a/zrpc/src/conn.rs b/zrpc/src/conn.rs index e67b4fa58708232ff7197175f476191ef2153ec5..5ca845d13f1d489861fe076b2258a8d84bf8d615 100644 --- a/zrpc/src/conn.rs +++ b/zrpc/src/conn.rs @@ -2,7 +2,7 @@ use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSock use futures::{channel::mpsc, SinkExt as _, Stream, StreamExt as _}; use std::{io, task::Poll}; -pub struct Conn { +pub struct Connection { pub(crate) tx: Box>, pub(crate) rx: Box< @@ -13,7 +13,7 @@ pub struct Conn { >, } -impl Conn { +impl Connection { pub fn new(stream: S) -> Self where S: 'static diff --git a/zrpc/src/lib.rs b/zrpc/src/lib.rs index b3973cae19ddf6d1b18ef447547e0bc56b6aa98d..a7bb44774b8e700443f753e3fb47c1176ef80142 100644 --- a/zrpc/src/lib.rs +++ b/zrpc/src/lib.rs @@ -2,5 +2,5 @@ pub mod auth; mod conn; mod peer; pub mod proto; -pub use conn::Conn; +pub use conn::Connection; pub use peer::*; diff --git a/zrpc/src/peer.rs b/zrpc/src/peer.rs index 75db257f55bd23752e0f1e2e72bd616f1df8ed1c..eeda034e9581ce215ee01821cff3e82bab70ed25 100644 --- a/zrpc/src/peer.rs +++ b/zrpc/src/peer.rs @@ -1,5 +1,5 @@ use super::proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, RequestMessage}; -use super::Conn; +use super::Connection; use anyhow::{anyhow, Context, Result}; use async_lock::{Mutex, RwLock}; use futures::FutureExt as _; @@ -79,12 +79,12 @@ impl TypedEnvelope { } pub struct Peer { - connections: RwLock>, + connections: RwLock>, next_connection_id: AtomicU32, } #[derive(Clone)] -struct Connection { +struct ConnectionState { outgoing_tx: mpsc::Sender, next_message_id: Arc, response_channels: Arc>>>, @@ -100,7 +100,7 @@ impl Peer { pub async fn add_connection( self: &Arc, - conn: Conn, + connection: Connection, ) -> ( ConnectionId, impl Future> + Send, @@ -112,16 +112,16 @@ impl Peer { ); let (mut incoming_tx, incoming_rx) = mpsc::channel(64); let (outgoing_tx, mut outgoing_rx) = mpsc::channel(64); - let connection = Connection { + let connection_state = ConnectionState { outgoing_tx, next_message_id: Default::default(), response_channels: Default::default(), }; - let mut writer = MessageStream::new(conn.tx); - let mut reader = MessageStream::new(conn.rx); + let mut writer = MessageStream::new(connection.tx); + let mut reader = MessageStream::new(connection.rx); let this = self.clone(); - let response_channels = connection.response_channels.clone(); + let response_channels = connection_state.response_channels.clone(); let handle_io = async move { loop { let read_message = reader.read_message().fuse(); @@ -179,7 +179,7 @@ impl Peer { self.connections .write() .await - .insert(connection_id, connection); + .insert(connection_id, connection_state); (connection_id, handle_io, incoming_rx) } @@ -218,7 +218,7 @@ impl Peer { let this = self.clone(); let (tx, mut rx) = mpsc::channel(1); async move { - let mut connection = this.connection(receiver_id).await?; + let mut connection = this.connection_state(receiver_id).await?; let message_id = connection .next_message_id .fetch_add(1, atomic::Ordering::SeqCst); @@ -252,7 +252,7 @@ impl Peer { ) -> impl Future> { let this = self.clone(); async move { - let mut connection = this.connection(receiver_id).await?; + let mut connection = this.connection_state(receiver_id).await?; let message_id = connection .next_message_id .fetch_add(1, atomic::Ordering::SeqCst); @@ -272,7 +272,7 @@ impl Peer { ) -> impl Future> { let this = self.clone(); async move { - let mut connection = this.connection(receiver_id).await?; + let mut connection = this.connection_state(receiver_id).await?; let message_id = connection .next_message_id .fetch_add(1, atomic::Ordering::SeqCst); @@ -291,7 +291,7 @@ impl Peer { ) -> impl Future> { let this = self.clone(); async move { - let mut connection = this.connection(receipt.sender_id).await?; + let mut connection = this.connection_state(receipt.sender_id).await?; let message_id = connection .next_message_id .fetch_add(1, atomic::Ordering::SeqCst); @@ -310,7 +310,7 @@ impl Peer { ) -> impl Future> { let this = self.clone(); async move { - let mut connection = this.connection(receipt.sender_id).await?; + let mut connection = this.connection_state(receipt.sender_id).await?; let message_id = connection .next_message_id .fetch_add(1, atomic::Ordering::SeqCst); @@ -322,10 +322,10 @@ impl Peer { } } - fn connection( + fn connection_state( self: &Arc, connection_id: ConnectionId, - ) -> impl Future> { + ) -> impl Future> { let this = self.clone(); async move { let connections = this.connections.read().await; @@ -352,12 +352,12 @@ mod tests { let client1 = Peer::new(); let client2 = Peer::new(); - let (client1_to_server_conn, server_to_client_1_conn, _) = Conn::in_memory(); + let (client1_to_server_conn, server_to_client_1_conn, _) = Connection::in_memory(); let (client1_conn_id, io_task1, _) = client1.add_connection(client1_to_server_conn).await; let (_, io_task2, incoming1) = server.add_connection(server_to_client_1_conn).await; - let (client2_to_server_conn, server_to_client_2_conn, _) = Conn::in_memory(); + let (client2_to_server_conn, server_to_client_2_conn, _) = Connection::in_memory(); let (client2_conn_id, io_task3, _) = client2.add_connection(client2_to_server_conn).await; let (_, io_task4, incoming2) = server.add_connection(server_to_client_2_conn).await; @@ -486,7 +486,7 @@ mod tests { #[test] fn test_disconnect() { smol::block_on(async move { - let (client_conn, mut server_conn, _) = Conn::in_memory(); + let (client_conn, mut server_conn, _) = Connection::in_memory(); let client = Peer::new(); let (connection_id, io_handler, mut incoming) = @@ -520,7 +520,7 @@ mod tests { #[test] fn test_io_error() { smol::block_on(async move { - let (client_conn, server_conn, _) = Conn::in_memory(); + let (client_conn, server_conn, _) = Connection::in_memory(); drop(server_conn); let client = Peer::new(); From 7e4d5b7d04dba42f3096a2025e719e5881687db4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 20:36:03 -0600 Subject: [PATCH 19/20] Clear cached credentials when establishing a websocket connection with an invalid token --- gpui/src/platform.rs | 1 + gpui/src/platform/mac/platform.rs | 20 ++++++++++ gpui/src/platform/test.rs | 4 ++ server/src/auth.rs | 6 ++- zed/src/rpc.rs | 65 +++++++++++++++---------------- zed/src/test.rs | 2 +- zed/src/workspace.rs | 2 +- 7 files changed, 63 insertions(+), 37 deletions(-) diff --git a/gpui/src/platform.rs b/gpui/src/platform.rs index a4c86eab2f9f696754c11677dabb8d32849ba33b..cd972021a57c0084c38145b7222813be515d8fa2 100644 --- a/gpui/src/platform.rs +++ b/gpui/src/platform.rs @@ -48,6 +48,7 @@ pub trait Platform: Send + Sync { fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()>; fn read_credentials(&self, url: &str) -> Result)>>; + fn delete_credentials(&self, url: &str) -> Result<()>; fn set_cursor_style(&self, style: CursorStyle); diff --git a/gpui/src/platform/mac/platform.rs b/gpui/src/platform/mac/platform.rs index 7015cbc713cecc528e742fe902a86c840c25cfd1..c956a199989ea35cfc62a678caac03240d44775d 100644 --- a/gpui/src/platform/mac/platform.rs +++ b/gpui/src/platform/mac/platform.rs @@ -551,6 +551,25 @@ impl platform::Platform for MacPlatform { } } + fn delete_credentials(&self, url: &str) -> Result<()> { + let url = CFString::from(url); + + unsafe { + use security::*; + + let mut query_attrs = CFMutableDictionary::with_capacity(2); + query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); + query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + + let status = SecItemDelete(query_attrs.as_concrete_TypeRef()); + + if status != errSecSuccess { + return Err(anyhow!("delete password failed: {}", status)); + } + } + Ok(()) + } + fn set_cursor_style(&self, style: CursorStyle) { unsafe { let cursor: id = match style { @@ -676,6 +695,7 @@ mod security { pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus; pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus; + pub fn SecItemDelete(query: CFDictionaryRef) -> OSStatus; pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus; } diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 85afff49994607fc2c16fd9705f5c207f6a6969e..d705a277e54f6d278d5d8fb02ad2c9ccf28014fb 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -137,6 +137,10 @@ impl super::Platform for Platform { Ok(None) } + fn delete_credentials(&self, _: &str) -> Result<()> { + Ok(()) + } + fn set_cursor_style(&self, style: CursorStyle) { *self.cursor.lock() = style; } diff --git a/server/src/auth.rs b/server/src/auth.rs index 5a3e301d27537a1e031d804341af29d071afdd95..1f6ec5f1db176638ffc52106d129cc6793f75c6e 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -17,7 +17,7 @@ use scrypt::{ }; use serde::{Deserialize, Serialize}; use std::{borrow::Cow, convert::TryFrom, sync::Arc}; -use surf::Url; +use surf::{StatusCode, Url}; use tide::Server; use zrpc::auth as zed_auth; @@ -73,7 +73,9 @@ impl tide::Middleware> for VerifyToken { request.set_ext(user_id); Ok(next.run(request).await) } else { - Err(anyhow!("invalid credentials").into()) + let mut response = tide::Response::new(StatusCode::Unauthorized); + response.set_body("invalid credentials"); + Ok(response) } } } diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 3526381cde572ba127775a01aca2faa194cf1f1e..9596b671edee8daea398292d3ac9f662877a6481 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -1,6 +1,9 @@ use crate::util::ResultExt; use anyhow::{anyhow, Context, Result}; -use async_tungstenite::tungstenite::http::Request; +use async_tungstenite::tungstenite::{ + error::Error as WebsocketError, + http::{Request, StatusCode}, +}; use gpui::{AsyncAppContext, Entity, ModelContext, Task}; use lazy_static::lazy_static; use parking_lot::RwLock; @@ -47,10 +50,25 @@ pub struct Client { #[derive(Error, Debug)] pub enum EstablishConnectionError { - #[error("invalid access token")] - InvalidAccessToken, + #[error("unauthorized")] + Unauthorized, + #[error("{0}")] + Other(#[from] anyhow::Error), #[error("{0}")] - Other(anyhow::Error), + Io(#[from] std::io::Error), + #[error("{0}")] + Http(#[from] async_tungstenite::tungstenite::http::Error), +} + +impl From for EstablishConnectionError { + fn from(error: WebsocketError) -> Self { + if let WebsocketError::Http(response) = &error { + if response.status() == StatusCode::UNAUTHORIZED { + return EstablishConnectionError::Unauthorized; + } + } + EstablishConnectionError::Other(error.into()) + } } impl EstablishConnectionError { @@ -314,10 +332,9 @@ impl Client { Ok(()) } Err(err) => { - eprintln!("error in authenticate and connect {}", err); - if matches!(err, EstablishConnectionError::InvalidAccessToken) { - eprintln!("nuking credentials"); + if matches!(err, EstablishConnectionError::Unauthorized) { self.state.write().credentials.take(); + cx.platform().delete_credentials(&ZED_SERVER_URL).ok(); } self.set_status(Status::ConnectionError, cx); Err(err)? @@ -409,36 +426,18 @@ impl Client { ); cx.background().spawn(async move { if let Some(host) = ZED_SERVER_URL.strip_prefix("https://") { - let stream = smol::net::TcpStream::connect(host) - .await - .map_err(EstablishConnectionError::other)?; - let request = request - .uri(format!("wss://{}/rpc", host)) - .body(()) - .map_err(EstablishConnectionError::other)?; - let (stream, _) = async_tungstenite::async_tls::client_async_tls(request, stream) - .await - .context("websocket handshake") - .map_err(EstablishConnectionError::other)?; + let stream = smol::net::TcpStream::connect(host).await?; + let request = request.uri(format!("wss://{}/rpc", host)).body(())?; + let (stream, _) = + async_tungstenite::async_tls::client_async_tls(request, stream).await?; Ok(Connection::new(stream)) } else if let Some(host) = ZED_SERVER_URL.strip_prefix("http://") { - let stream = smol::net::TcpStream::connect(host) - .await - .map_err(EstablishConnectionError::other)?; - let request = request - .uri(format!("ws://{}/rpc", host)) - .body(()) - .map_err(EstablishConnectionError::other)?; - let (stream, _) = async_tungstenite::client_async(request, stream) - .await - .context("websocket handshake") - .map_err(EstablishConnectionError::other)?; + let stream = smol::net::TcpStream::connect(host).await?; + let request = request.uri(format!("ws://{}/rpc", host)).body(())?; + let (stream, _) = async_tungstenite::client_async(request, stream).await?; Ok(Connection::new(stream)) } else { - Err(EstablishConnectionError::other(anyhow!( - "invalid server url: {}", - *ZED_SERVER_URL - ))) + Err(anyhow!("invalid server url: {}", *ZED_SERVER_URL))? } }) } diff --git a/zed/src/test.rs b/zed/src/test.rs index e5ab3154f526e3090774fbf02ae338c6749f03ab..7d027a8a1771b41fd2b68844802a2f6939b6448b 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -283,7 +283,7 @@ impl FakeServer { } if credentials.access_token != self.access_token.load(SeqCst).to_string() { - Err(EstablishConnectionError::InvalidAccessToken)? + Err(EstablishConnectionError::Unauthorized)? } let (client_conn, server_conn, _) = Connection::in_memory(); diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index cbdf3b149a532421b92c37a39e1c07106cccbcfd..9ce67c2f8a678eb344b91118cde78a5bd0000d3d 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -960,7 +960,7 @@ impl Workspace { fn render_connection_status(&self) -> Option { let theme = &self.settings.borrow().theme; - match dbg!(&*self.rpc.status().borrow()) { + match &*self.rpc.status().borrow() { rpc::Status::ConnectionError | rpc::Status::ConnectionLost | rpc::Status::Reauthenticating From 603f1d820d7164f803118da98296fd775b68031e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Sep 2021 11:45:08 +0200 Subject: [PATCH 20/20] Authenticate via the browser if keychain credentials are invalid Co-Authored-By: Nathan Sobo --- Cargo.lock | 12 +++++++++++ zed/Cargo.toml | 1 + zed/src/rpc.rs | 58 ++++++++++++++++++++++++++++++++++---------------- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aea0c49da87ae5aeed63e66f65aad561b1471870..60b2864c5bd3d70dcbe44bc1547f85d68e852869 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -344,6 +344,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "async-recursion" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-rustls" version = "0.1.2" @@ -5881,6 +5892,7 @@ version = "0.1.0" dependencies = [ "anyhow", "arrayvec 0.7.1", + "async-recursion", "async-trait", "async-tungstenite", "cargo-bundle", diff --git a/zed/Cargo.toml b/zed/Cargo.toml index d9c2cc6a584535caace7d267b5a04b10dfffb721..bb4fab264e589d96a83960db49ad69d6b88488a2 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -18,6 +18,7 @@ test-support = ["tempdir", "zrpc/test-support"] [dependencies] anyhow = "1.0.38" +async-recursion = "0.3" async-trait = "0.1" arrayvec = "0.7.1" async-tungstenite = { version = "0.14", features = ["async-tls"] } diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 9596b671edee8daea398292d3ac9f662877a6481..a01d14193fa833d6e3151eefaff37d555acf4b60 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -1,5 +1,6 @@ use crate::util::ResultExt; use anyhow::{anyhow, Context, Result}; +use async_recursion::async_recursion; use async_tungstenite::tungstenite::{ error::Error as WebsocketError, http::{Request, StatusCode}, @@ -282,6 +283,7 @@ impl Client { } } + #[async_recursion(?Send)] pub async fn authenticate_and_connect( self: &Arc, cx: &AsyncAppContext, @@ -304,9 +306,13 @@ impl Client { self.set_status(Status::Reauthenticating, cx) } + let mut read_from_keychain = false; let credentials = self.state.read().credentials.clone(); let credentials = if let Some(credentials) = credentials { credentials + } else if let Some(credentials) = read_credentials_from_keychain(cx) { + read_from_keychain = true; + credentials } else { let credentials = match self.authenticate(&cx).await { Ok(credentials) => credentials, @@ -328,16 +334,27 @@ impl Client { match self.establish_connection(&credentials, cx).await { Ok(conn) => { log::info!("connected to rpc address {}", *ZED_SERVER_URL); + if !read_from_keychain { + write_credentials_to_keychain(&credentials, cx).log_err(); + } self.set_connection(conn, cx).await; Ok(()) } Err(err) => { if matches!(err, EstablishConnectionError::Unauthorized) { self.state.write().credentials.take(); - cx.platform().delete_credentials(&ZED_SERVER_URL).ok(); + cx.platform().delete_credentials(&ZED_SERVER_URL).log_err(); + if read_from_keychain { + self.set_status(Status::SignedOut, cx); + self.authenticate_and_connect(cx).await + } else { + self.set_status(Status::ConnectionError, cx); + Err(err)? + } + } else { + self.set_status(Status::ConnectionError, cx); + Err(err)? } - self.set_status(Status::ConnectionError, cx); - Err(err)? } } } @@ -449,18 +466,6 @@ impl Client { let platform = cx.platform(); let executor = cx.background(); executor.clone().spawn(async move { - if let Some((user_id, access_token)) = platform - .read_credentials(&ZED_SERVER_URL) - .log_err() - .flatten() - { - log::info!("already signed in. user_id: {}", user_id); - return Ok(Credentials { - user_id: user_id.parse()?, - access_token: String::from_utf8(access_token).unwrap(), - }); - } - // Generate a pair of asymmetric encryption keys. The public key will be used by the // zed server to encrypt the user's access token, so that it can'be intercepted by // any other app running on the user's device. @@ -521,9 +526,6 @@ impl Client { .decrypt_string(&access_token) .context("failed to decrypt access token")?; platform.activate(true); - platform - .write_credentials(&ZED_SERVER_URL, &user_id, access_token.as_bytes()) - .log_err(); Ok(Credentials { user_id: user_id.parse()?, @@ -564,6 +566,26 @@ impl Client { } } +fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option { + let (user_id, access_token) = cx + .platform() + .read_credentials(&ZED_SERVER_URL) + .log_err() + .flatten()?; + Some(Credentials { + user_id: user_id.parse().ok()?, + access_token: String::from_utf8(access_token).ok()?, + }) +} + +fn write_credentials_to_keychain(credentials: &Credentials, cx: &AsyncAppContext) -> Result<()> { + cx.platform().write_credentials( + &ZED_SERVER_URL, + &credentials.user_id.to_string(), + credentials.access_token.as_bytes(), + ) +} + const WORKTREE_URL_PREFIX: &'static str = "zed://worktrees/"; pub fn encode_worktree_url(id: u64, access_token: &str) -> String {