Detailed changes
@@ -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"
@@ -814,7 +825,7 @@ dependencies = [
"error-chain",
"glob 0.2.11",
"icns",
- "image",
+ "image 0.12.4",
"libflate",
"md5",
"msi",
@@ -2102,6 +2113,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 +2188,7 @@ dependencies = [
"font-kit",
"foreign-types",
"gpui_macros",
+ "image 0.23.14",
"lazy_static",
"log",
"metal",
@@ -2462,15 +2484,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 +3055,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"
@@ -5067,18 +5119,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",
@@ -5129,6 +5181,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 +5757,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"
@@ -5823,6 +5892,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"arrayvec 0.7.1",
+ "async-recursion",
"async-trait",
"async-tungstenite",
"cargo-bundle",
@@ -5836,6 +5906,7 @@ dependencies = [
"gpui",
"http-auth-basic",
"ignore",
+ "image 0.23.14",
"lazy_static",
"libc",
"log",
@@ -5855,6 +5926,7 @@ dependencies = [
"smol",
"surf",
"tempdir",
+ "thiserror",
"time 0.3.2",
"tiny_http",
"toml 0.5.8",
@@ -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"
@@ -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<F, Fut, S>(&self, f: F) -> Task<S>
+ where
+ F: FnOnce(WeakViewHandle<T>, AsyncAppContext) -> Fut,
+ Fut: 'static + Future<Output = S>,
+ S: 'static,
+ {
+ let handle = self.handle().downgrade();
+ self.app.spawn(|cx| f(handle, cx))
+ }
}
pub struct RenderContext<'a, T: View> {
@@ -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))
}
@@ -6,6 +6,7 @@ mod empty;
mod event_handler;
mod flex;
mod hook;
+mod image;
mod label;
mod line_box;
mod list;
@@ -16,27 +17,17 @@ 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},
+ geometry::{
+ rect::RectF,
+ vector::{vec2f, Vector2F},
+ },
json, DebugContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
};
use core::panic;
@@ -371,3 +362,13 @@ pub trait ParentElement<'a>: Extend<ElementBox> + Sized {
}
impl<'a, T> ParentElement<'a> for T where T: Extend<ElementBox> {}
+
+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())
+ }
+}
@@ -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,
@@ -0,0 +1,90 @@
+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;
+
+pub struct Image {
+ data: Arc<ImageData>,
+ style: ImageStyle,
+}
+
+#[derive(Copy, Clone, Default, Deserialize)]
+pub struct ImageStyle {
+ #[serde(default)]
+ border: Border,
+ #[serde(default)]
+ corner_radius: f32,
+}
+
+impl Image {
+ pub fn new(data: Arc<ImageData>) -> Self {
+ Self {
+ data,
+ style: Default::default(),
+ }
+ }
+
+ pub fn with_style(mut self, style: ImageStyle) -> Self {
+ self.style = style;
+ self
+ }
+}
+
+impl Element for Image {
+ type LayoutState = ();
+ type PaintState = ();
+
+ fn layout(
+ &mut self,
+ constraint: SizeConstraint,
+ _: &mut LayoutContext,
+ ) -> (Vector2F, Self::LayoutState) {
+ let size =
+ constrain_size_preserving_aspect_ratio(constraint.max, self.data.size().to_f32());
+ (size, ())
+ }
+
+ fn paint(
+ &mut self,
+ bounds: RectF,
+ _: RectF,
+ _: &mut Self::LayoutState,
+ cx: &mut PaintContext,
+ ) -> Self::PaintState {
+ cx.scene.push_image(scene::Image {
+ bounds,
+ border: self.style.border,
+ corner_radius: self.style.corner_radius,
+ data: self.data.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(),
+ })
+ }
+}
@@ -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),
@@ -0,0 +1,43 @@
+use crate::geometry::vector::{vec2i, Vector2I};
+use image::{Bgra, ImageBuffer};
+use std::{
+ fmt,
+ sync::{
+ atomic::{AtomicUsize, Ordering::SeqCst},
+ Arc,
+ },
+};
+
+pub struct ImageData {
+ pub id: usize,
+ data: ImageBuffer<Bgra<u8>, Vec<u8>>,
+}
+
+impl ImageData {
+ pub fn new(data: ImageBuffer<Bgra<u8>, Vec<u8>>) -> Arc<Self> {
+ 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)
+ }
+}
+
+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()
+ }
+}
@@ -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;
@@ -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<Option<(String, Vec<u8>)>>;
+ fn delete_credentials(&self, url: &str) -> Result<()>;
fn set_cursor_style(&self, style: CursorStyle);
@@ -3,6 +3,7 @@ mod dispatcher;
mod event;
mod fonts;
mod geometry;
+mod image_cache;
mod platform;
mod renderer;
mod sprite_cache;
@@ -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<Atlas>,
}
+#[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<Vector2I> {
- 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) {
@@ -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<usize, (AllocId, RectI)>,
+ curr_frame: HashMap<usize, (AllocId, RectI)>,
+ 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)
+ }
+}
@@ -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;
}
@@ -1,4 +1,4 @@
-use super::{atlas::AtlasAllocator, sprite_cache::SpriteCache};
+use super::{atlas::AtlasAllocator, image_cache::ImageCache, sprite_cache::SpriteCache};
use crate::{
color::Color,
geometry::{
@@ -6,8 +6,7 @@ use crate::{
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};
@@ -20,10 +19,12 @@ const INSTANCE_BUFFER_SIZE: usize = 1024 * 1024; // This is an arbitrary decisio
pub struct Renderer {
sprite_cache: SpriteCache,
+ image_cache: ImageCache,
path_atlases: AtlasAllocator,
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 +65,9 @@ impl Renderer {
);
let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), fonts);
- let path_atlases = build_path_atlas_allocator(MTLPixelFormat::R8Unorm, &device);
+ let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768));
+ let path_atlases =
+ AtlasAllocator::new(device.clone(), build_path_atlas_texture_descriptor());
let quad_pipeline_state = build_pipeline_state(
&device,
&library,
@@ -89,6 +92,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,
@@ -99,10 +110,12 @@ impl Renderer {
);
Self {
sprite_cache,
+ image_cache,
path_atlases,
quad_pipeline_state,
shadow_pipeline_state,
sprite_pipeline_state,
+ image_pipeline_state,
path_atlas_pipeline_state,
unit_vertices,
instances,
@@ -117,6 +130,7 @@ impl Renderer {
output: &metal::TextureRef,
) {
let mut offset = 0;
+
let path_sprites = self.render_path_atlases(scene, &mut offset, command_buffer);
self.render_layers(
scene,
@@ -130,6 +144,7 @@ impl Renderer {
location: 0,
length: offset as NSUInteger,
});
+ self.image_cache.finish_frame();
}
fn render_path_atlases(
@@ -146,11 +161,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 +177,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 +188,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 +331,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,
@@ -559,11 +581,6 @@ impl Renderer {
mem::size_of::<shaders::vector_float2>() 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::<shaders::vector_float2>() 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);
@@ -573,13 +590,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::<shaders::vector_float2>() 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),
@@ -602,6 +625,96 @@ 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 corner_radius = image.corner_radius * scale_factor;
+ let border_width = image.border.width * scale_factor;
+ let (alloc_id, atlas_bounds) = self.image_cache.render(&image.data);
+ 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(),
+ 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,
+ });
+ }
+
+ 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::<shaders::vector_float2>() 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::<shaders::GPUIImage>();
+ assert!(
+ next_offset <= INSTANCE_BUFFER_SIZE,
+ "instance buffer exhausted"
+ );
+
+ let texture = self.image_cache.atlas_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::<shaders::vector_float2>() 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 +821,15 @@ 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 align_offset(offset: &mut usize) {
@@ -803,9 +912,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"));
@@ -1,16 +1,19 @@
#include <simd/simd.h>
-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,43 @@ 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;
+ float border_top;
+ float border_right;
+ float border_bottom;
+ float border_left;
+ vector_uchar4 border_color;
+ float corner_radius;
+} GPUIImage;
@@ -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,13 @@ fragment float4 quad_fragment(
float4 color;
if (border_width == 0.) {
- color = coloru_to_colorf(input.background_color);
+ 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(
- coloru_to_colorf(input.border_color),
- coloru_to_colorf(input.background_color),
+ border_color,
+ input.background_color,
saturate(0.5 - inset_distance)
);
}
@@ -109,6 +83,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,6 +224,44 @@ fragment float4 sprite_fragment(
return color;
}
+vertex QuadFragmentInput 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 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(
+ QuadFragmentInput input [[stage_in]],
+ texture2d<float> atlas [[ texture(GPUIImageFragmentInputIndexAtlas) ]]
+) {
+ constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
+ input.background_color = atlas.sample(atlas_sampler, input.atlas_position);
+ return quad_sdf(input);
+}
+
struct PathAtlasVertexOutput {
float4 position [[position]];
float2 st_position;
@@ -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<dyn platform::FontSystem>,
- atlases: Vec<Atlas>,
+ atlases: AtlasAllocator,
glyphs: HashMap<GlyphDescriptor, Option<GlyphSprite>>,
icons: HashMap<IconDescriptor, IconSprite>,
}
@@ -56,21 +51,18 @@ impl SpriteCache {
size: Vector2I,
fonts: Arc<dyn platform::FontSystem>,
) -> 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::<Vec<_>>();
- 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<RectI> {
- 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)
}
}
@@ -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;
}
@@ -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<RectF>,
quads: Vec<Quad>,
underlines: Vec<Quad>,
+ images: Vec<Image>,
shadows: Vec<Shadow>,
glyphs: Vec<Glyph>,
icons: Vec<Icon>,
@@ -124,6 +126,13 @@ pub struct PathVertex {
pub st_position: Vector2F,
}
+pub struct Image {
+ pub bounds: RectF,
+ pub border: Border,
+ pub corner_radius: f32,
+ pub data: Arc<ImageData>,
+}
+
impl Scene {
pub fn new(scale_factor: f32) -> Self {
let stacking_context = StackingContext::new(None);
@@ -166,6 +175,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 +253,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 +281,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);
}
@@ -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(),
@@ -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<Arc<AppState>> 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)
}
}
}
@@ -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<ConnectionId, Connection>,
+ connections: HashMap<ConnectionId, ConnectionState>,
pub worktrees: HashMap<u64, Worktree>,
channels: HashMap<ChannelId, Channel>,
next_worktree_id: u64,
}
-struct Connection {
+struct ConnectionState {
user_id: UserId,
worktrees: HashSet<u64>,
channels: HashSet<ChannelId>,
@@ -133,7 +133,7 @@ impl Server {
pub fn handle_connection(
self: &Arc<Self>,
- connection: Conn,
+ connection: Connection,
addr: String,
user_id: UserId,
) -> impl Future<Output = ()> {
@@ -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(),
@@ -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
@@ -972,7 +972,7 @@ pub fn add_routes(app: &mut tide::Server<Arc<AppState>>, rpc: &Arc<Peer>) {
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,8 +1023,9 @@ mod tests {
editor::{Editor, Insert},
fs::{FakeFs, Fs as _},
language::LanguageRegistry,
- rpc::{self, Client},
+ rpc::{self, Client, Credentials, EstablishConnectionError},
settings,
+ test::FakeHttpClient,
user::UserStore,
worktree::Worktree,
};
@@ -1483,6 +1484,7 @@ 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;
@@ -1512,7 +1514,8 @@ mod tests {
.await
.unwrap();
- let user_store_a = Arc::new(UserStore::new(client_a.clone()));
+ 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())
@@ -1537,7 +1540,8 @@ mod tests {
})
.await;
- let user_store_b = Arc::new(UserStore::new(client_b.clone()));
+ 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())
@@ -1625,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;
@@ -1637,7 +1642,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(), 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())
@@ -1683,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;
@@ -1713,7 +1719,8 @@ mod tests {
.await
.unwrap();
- let user_store_a = Arc::new(UserStore::new(client_a.clone()));
+ 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())
@@ -1739,7 +1746,8 @@ mod tests {
})
.await;
- let user_store_b = Arc::new(UserStore::new(client_b.clone()));
+ 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())
@@ -1914,39 +1922,42 @@ 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(EstablishConnectionError::other(anyhow!(
+ "server is forbidding connections"
+ )))
+ } else {
+ 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(
+ server_conn,
+ client_name,
+ client_user_id,
+ ))
+ .detach();
+ Ok(client_conn)
+ }
+ })
+ });
client
.authenticate_and_connect(&cx.to_async())
@@ -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"] }
@@ -30,6 +31,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"
@@ -49,6 +51,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"
@@ -0,0 +1,3 @@
+<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -7,7 +7,14 @@ pane_divider = { width = 1, color = "$border.0" }
[workspace.titlebar]
border = { width = 1, bottom = true, color = "$border.0" }
-text = { extends = "$text.0" }
+title = "$text.0"
+avatar_width = 20
+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"
@@ -26,7 +33,7 @@ background = "$surface.1"
text = "$text.0"
[workspace.sidebar]
-padding = { left = 12, right = 12 }
+width = 32
border = { right = true, width = 1, color = "$border.0" }
[workspace.sidebar.resize_handle]
@@ -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,
@@ -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();
@@ -443,7 +443,7 @@ impl ChannelMessage {
message: proto::ChannelMessage,
user_store: &UserStore,
) -> Result<Self> {
- 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,
@@ -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 = Arc::new(UserStore::new(client.clone()));
+ 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));
@@ -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.)
@@ -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,
@@ -0,0 +1,26 @@
+pub use anyhow::{anyhow, Result};
+use futures::future::BoxFuture;
+use std::sync::Arc;
+pub use surf::{
+ http::{Method, Response as ServerResponse},
+ Request, Response, Url,
+};
+
+pub trait HttpClient: Send + Sync {
+ fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result<Response>>;
+}
+
+pub fn client() -> Arc<dyn HttpClient> {
+ Arc::new(surf::client())
+}
+
+impl HttpClient for surf::Client {
+ fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result<Response>> {
+ Box::pin(async move {
+ Ok(self
+ .send(req)
+ .await
+ .map_err(|e| anyhow!("http request failed: {}", e))?)
+ })
+ }
+}
@@ -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;
@@ -42,6 +43,7 @@ pub struct AppState {
pub languages: Arc<language::LanguageRegistry>,
pub themes: Arc<settings::ThemeRegistry>,
pub rpc: Arc<rpc::Client>,
+ pub user_store: Arc<user::UserStore>,
pub fs: Arc<dyn fs::Fs>,
pub channel_list: ModelHandle<ChannelList>,
}
@@ -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,14 +37,16 @@ fn main() {
app.run(move |cx| {
let rpc = rpc::Client::new();
- let user_store = Arc::new(UserStore::new(rpc.clone()));
+ 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)),
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),
});
@@ -1,6 +1,10 @@
use crate::util::ResultExt;
use anyhow::{anyhow, Context, Result};
-use async_tungstenite::tungstenite::http::Request;
+use async_recursion::async_recursion;
+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;
@@ -15,10 +19,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! {
@@ -29,37 +34,65 @@ lazy_static! {
pub struct Client {
peer: Arc<Peer>,
state: RwLock<ClientState>,
- auth_callback: Option<
- Box<dyn 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<(u64, String)>>>,
- >,
- connect_callback: Option<
- Box<dyn 'static + Send + Sync + Fn(u64, &str, &AsyncAppContext) -> Task<Result<Conn>>>,
+ authenticate:
+ Option<Box<dyn 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<Credentials>>>>,
+ establish_connection: Option<
+ Box<
+ dyn 'static
+ + Send
+ + Sync
+ + Fn(
+ &Credentials,
+ &AsyncAppContext,
+ ) -> Task<Result<Connection, EstablishConnectionError>>,
+ >,
>,
}
+#[derive(Error, Debug)]
+pub enum EstablishConnectionError {
+ #[error("unauthorized")]
+ Unauthorized,
+ #[error("{0}")]
+ Other(#[from] anyhow::Error),
+ #[error("{0}")]
+ Io(#[from] std::io::Error),
+ #[error("{0}")]
+ Http(#[from] async_tungstenite::tungstenite::http::Error),
+}
+
+impl From<WebsocketError> 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 {
+ pub fn other(error: impl Into<anyhow::Error> + Send + Sync) -> Self {
+ Self::Other(error.into())
+ }
+}
+
#[derive(Copy, Clone, Debug)]
pub enum Status {
- Disconnected,
+ 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<Credentials>,
status: (watch::Sender<Status>, watch::Receiver<Status>),
entity_id_extractors: HashMap<TypeId, Box<dyn Send + Sync + Fn(&dyn AnyTypedEnvelope) -> u64>>,
model_handlers: HashMap<
@@ -70,10 +103,17 @@ 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 {
- status: watch::channel_with(Status::Disconnected),
+ credentials: None,
+ status: watch::channel_with(Status::SignedOut),
entity_id_extractors: Default::default(),
model_handlers: Default::default(),
_maintain_connection: None,
@@ -107,22 +147,38 @@ 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<Login, Connect>(
- &mut self,
- login: Login,
- connect: Connect,
- ) where
- Login: 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<(u64, String)>>,
- Connect: 'static + Send + Sync + Fn(u64, &str, &AsyncAppContext) -> Task<Result<Conn>>,
+ pub fn override_authenticate<F>(&mut self, authenticate: F) -> &mut Self
+ where
+ F: 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<Credentials>>,
{
- 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<F>(&mut self, connect: F) -> &mut Self
+ where
+ F: 'static
+ + Send
+ + Sync
+ + Fn(&Credentials, &AsyncAppContext) -> Task<Result<Connection, EstablishConnectionError>>,
+ {
+ self.establish_connection = Some(Box::new(connect));
+ self
+ }
+
+ pub fn user_id(&self) -> Option<u64> {
+ self.state
+ .read()
+ .credentials
+ .as_ref()
+ .map(|credentials| credentials.user_id)
}
pub fn status(&self) -> watch::Receiver<Status> {
@@ -167,7 +223,7 @@ impl Client {
}
}));
}
- Status::Disconnected => {
+ Status::SignedOut => {
state._maintain_connection.take();
}
_ => {}
@@ -227,12 +283,13 @@ impl Client {
}
}
+ #[async_recursion(?Send)]
pub async fn authenticate_and_connect(
self: &Arc<Self>,
cx: &AsyncAppContext,
) -> anyhow::Result<()> {
let was_disconnected = match *self.status().borrow() {
- Status::Disconnected => true,
+ Status::SignedOut => true,
Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
false
}
@@ -249,33 +306,60 @@ 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 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,
+ 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;
+ if !read_from_keychain {
+ write_credentials_to_keychain(&credentials, cx).log_err();
+ }
+ self.set_connection(conn, cx).await;
Ok(())
}
Err(err) => {
- self.set_status(Status::ConnectionError, cx);
- Err(err)
+ if matches!(err, EstablishConnectionError::Unauthorized) {
+ self.state.write().credentials.take();
+ 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)?
+ }
}
}
}
- async fn set_connection(self: &Arc<Self>, user_id: u64, conn: Conn, cx: &AsyncAppContext) {
+ async fn set_connection(self: &Arc<Self>, conn: Connection, cx: &AsyncAppContext) {
let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn).await;
cx.foreground()
.spawn({
@@ -310,13 +394,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();
@@ -324,7 +402,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);
@@ -334,52 +412,49 @@ impl Client {
.detach();
}
- fn authenticate(self: &Arc<Self>, cx: &AsyncAppContext) -> Task<Result<(u64, String)>> {
- if let Some(callback) = self.auth_callback.as_ref() {
+ fn authenticate(self: &Arc<Self>, cx: &AsyncAppContext) -> Task<Result<Credentials>> {
+ if let Some(callback) = self.authenticate.as_ref() {
callback(cx)
} else {
self.authenticate_with_browser(cx)
}
}
- fn connect(
+ fn establish_connection(
self: &Arc<Self>,
- user_id: u64,
- access_token: &str,
+ credentials: &Credentials,
cx: &AsyncAppContext,
- ) -> Task<Result<Conn>> {
- if let Some(callback) = self.connect_callback.as_ref() {
- callback(user_id, access_token, cx)
+ ) -> Task<Result<Connection, EstablishConnectionError>> {
+ 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<Self>,
- user_id: u64,
- access_token: &str,
+ credentials: &Credentials,
cx: &AsyncAppContext,
- ) -> Task<Result<Conn>> {
- let request =
- Request::builder().header("Authorization", format!("{} {}", user_id, access_token));
+ ) -> Task<Result<Connection, EstablishConnectionError>> {
+ 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, _) = async_tungstenite::async_tls::client_async_tls(request, stream)
- .await
- .context("websocket handshake")?;
- Ok(Conn::new(stream))
+ 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?;
let request = request.uri(format!("ws://{}/rpc", host)).body(())?;
- let (stream, _) = async_tungstenite::client_async(request, stream)
- .await
- .context("websocket handshake")?;
- Ok(Conn::new(stream))
+ let (stream, _) = async_tungstenite::client_async(request, stream).await?;
+ Ok(Connection::new(stream))
} else {
- Err(anyhow!("invalid server url: {}", *ZED_SERVER_URL))
+ Err(anyhow!("invalid server url: {}", *ZED_SERVER_URL))?
}
})
}
@@ -387,19 +462,10 @@ impl Client {
pub fn authenticate_with_browser(
self: &Arc<Self>,
cx: &AsyncAppContext,
- ) -> Task<Result<(u64, String)>> {
+ ) -> Task<Result<Credentials>> {
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((user_id.parse()?, 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.
@@ -460,17 +526,18 @@ 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((user_id.parse()?, access_token))
+
+ Ok(Credentials {
+ user_id: user_id.parse()?,
+ access_token,
+ })
})
}
pub async fn disconnect(self: &Arc<Self>, 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(())
}
@@ -499,6 +566,26 @@ impl Client {
}
}
+fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
+ 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 {
@@ -561,6 +648,7 @@ mod tests {
status.recv().await,
Some(Status::Connected { .. })
));
+ assert_eq!(server.auth_count(), 1);
server.forbid_connections();
server.disconnect().await;
@@ -569,6 +657,20 @@ 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
+
+ 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]
@@ -2,28 +2,31 @@ use crate::{
assets::Assets,
channel::ChannelList,
fs::RealFs,
+ http::{HttpClient, Request, Response, ServerResponse},
language::LanguageRegistry,
- rpc::{self, Client},
+ rpc::{self, Client, Credentials, EstablishConnectionError},
settings::{self, ThemeRegistry},
time::ReplicaId,
user::UserStore,
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::{
- atomic::{AtomicBool, Ordering::SeqCst},
+ atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
Arc,
},
};
use tempdir::TempDir;
-use zrpc::{proto, Conn, ConnectionId, Peer, Receipt, TypedEnvelope};
+use zrpc::{proto, Connection, ConnectionId, Peer, Receipt, TypedEnvelope};
#[cfg(test)]
#[ctor::ctor]
@@ -164,14 +167,16 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
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 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,
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),
})
}
@@ -204,6 +209,9 @@ pub struct FakeServer {
incoming: Mutex<Option<mpsc::Receiver<Box<dyn proto::AnyTypedEnvelope>>>>,
connection_id: Mutex<Option<ConnectionId>>,
forbid_connections: AtomicBool,
+ auth_count: AtomicUsize,
+ access_token: AtomicUsize,
+ user_id: u64,
}
impl FakeServer {
@@ -212,40 +220,47 @@ impl FakeServer {
client: &mut Arc<Client>,
cx: &TestAppContext,
) -> Arc<Self> {
- 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(),
+ access_token: Default::default(),
+ user_id: client_user_id,
});
Arc::get_mut(client)
.unwrap()
- .set_login_and_connect_callbacks(
+ .override_authenticate({
+ let server = server.clone();
move |cx| {
- cx.spawn(|_| async move {
- let access_token = "the-token".to_string();
- Ok((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 }
+ server.auth_count.fetch_add(1, SeqCst);
+ let access_token = server.access_token.load(SeqCst).to_string();
+ cx.spawn(move |_| async move {
+ Ok(Credentials {
+ user_id: client_user_id,
+ access_token,
})
- }
- },
- );
+ })
+ }
+ })
+ .override_establish_connection({
+ let server = server.clone();
+ move |credentials, cx| {
+ let credentials = credentials.clone();
+ cx.spawn({
+ let server = server.clone();
+ move |cx| async move { server.establish_connection(&credentials, &cx).await }
+ })
+ }
+ });
client
.authenticate_and_connect(&cx.to_async())
.await
.unwrap();
- result
+ server
}
pub async fn disconnect(&self) {
@@ -254,17 +269,37 @@ impl FakeServer {
self.incoming.lock().take();
}
- async fn connect(&self, cx: &AsyncAppContext) -> Result<Conn> {
+ async fn establish_connection(
+ &self,
+ credentials: &Credentials,
+ cx: &AsyncAppContext,
+ ) -> Result<Connection, EstablishConnectionError> {
+ 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::Unauthorized)?
}
+
+ 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) {
@@ -312,3 +347,33 @@ impl FakeServer {
self.connection_id.lock().expect("not connected")
}
}
+
+pub struct FakeHttpClient {
+ handler:
+ Box<dyn 'static + Send + Sync + Fn(Request) -> BoxFuture<'static, Result<ServerResponse>>>,
+}
+
+impl FakeHttpClient {
+ pub fn new<Fut, F>(handler: F) -> Arc<dyn HttpClient>
+ where
+ Fut: 'static + Send + Future<Output = Result<ServerResponse>>,
+ 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<Response>> {
+ let future = (self.handler)(req);
+ Box::pin(async move { future.await.map(Into::into) })
+ }
+}
@@ -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,
};
@@ -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,24 @@ pub struct Workspace {
pub right_sidebar: Sidebar,
}
+#[derive(Clone, Deserialize)]
+pub struct Titlebar {
+ #[serde(flatten)]
+ pub container: ContainerStyle,
+ pub title: TextStyle,
+ pub avatar_width: f32,
+ 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)]
@@ -60,6 +78,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,
@@ -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)
@@ -1,22 +1,77 @@
-use crate::rpc::Client;
-use anyhow::{anyhow, Result};
+use crate::{
+ http::{HttpClient, Method, Request, Url},
+ rpc::{Client, Status},
+ util::TryFutureExt,
+};
+use anyhow::{anyhow, Context, Result};
+use futures::future;
+use gpui::{executor, ImageData, Task};
use parking_lot::Mutex;
-use std::{collections::HashMap, sync::Arc};
+use postage::{oneshot, prelude::Stream, sink::Sink, watch};
+use std::{
+ collections::HashMap,
+ sync::{Arc, Weak},
+};
use zrpc::proto;
-pub use proto::User;
+#[derive(Debug)]
+pub struct User {
+ pub id: u64,
+ pub github_login: String,
+ pub avatar: Option<Arc<ImageData>>,
+}
pub struct UserStore {
users: Mutex<HashMap<u64, Arc<User>>>,
+ current_user: watch::Receiver<Option<Arc<User>>>,
rpc: Arc<Client>,
+ http: Arc<dyn HttpClient>,
+ _maintain_current_user: Task<()>,
}
impl UserStore {
- pub fn new(rpc: Arc<Client>) -> Self {
- Self {
+ pub fn new(
+ rpc: Arc<Client>,
+ http: Arc<dyn HttpClient>,
+ executor: &executor::Background,
+ ) -> Arc<Self> {
+ let (mut current_user_tx, current_user_rx) = watch::channel();
+ let (mut this_tx, mut this_rx) = oneshot::channel::<Weak<Self>>();
+ let this = Arc::new(Self {
users: Default::default(),
- rpc,
- }
+ current_user: current_user_rx,
+ rpc: rpc.clone(),
+ http,
+ _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 {
+ 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
+ .ok();
+ }
+ }
+ Status::SignedOut => {
+ current_user_tx.send(None).await.ok();
+ }
+ _ => {}
+ }
+ }
+ }),
+ });
+ let weak = Arc::downgrade(&this);
+ executor
+ .spawn(async move { this_tx.send(weak).await })
+ .detach();
+ this
}
pub async fn load_users(&self, mut user_ids: Vec<u64>) -> Result<()> {
@@ -27,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));
}
}
@@ -36,24 +98,48 @@ impl UserStore {
Ok(())
}
- pub async fn get_user(&self, user_id: u64) -> Result<Arc<User>> {
+ pub async fn fetch_user(&self, user_id: u64) -> Result<Arc<User>> {
if let Some(user) = self.users.lock().get(&user_id).cloned() {
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<Option<Arc<User>>> {
+ &self.current_user
+ }
+}
+
+impl User {
+ async fn new(message: proto::User, http: &dyn HttpClient) -> Self {
+ User {
+ id: message.id,
+ github_login: message.github_login,
+ avatar: fetch_avatar(http, &message.avatar_url).log_err().await,
}
}
}
+
+async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
+ let url = Url::parse(url).with_context(|| format!("failed to parse avatar url {:?}", url))?;
+ let mut request = Request::new(Method::Get, url);
+ request.middleware(surf::middleware::Redirect::default());
+
+ 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))
+}
@@ -10,8 +10,9 @@ use crate::{
project_browser::ProjectBrowser,
rpc,
settings::Settings,
+ user,
worktree::{File, Worktree},
- AppState,
+ AppState, Authenticate,
};
use anyhow::{anyhow, Result};
use gpui::{
@@ -20,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,
@@ -28,9 +29,8 @@ 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::{
collections::{hash_map::Entry, HashMap, HashSet},
future::Future,
@@ -341,6 +341,7 @@ pub struct Workspace {
pub settings: watch::Receiver<Settings>,
languages: Arc<LanguageRegistry>,
rpc: Arc<rpc::Client>,
+ user_store: Arc<user::UserStore>,
fs: Arc<dyn Fs>,
modal: Option<AnyViewHandle>,
center: PaneGroup,
@@ -354,6 +355,7 @@ pub struct Workspace {
(usize, Arc<Path>),
postage::watch::Receiver<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
>,
+ _observe_current_user: Task<()>,
}
impl Workspace {
@@ -387,6 +389,23 @@ 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;
+ 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());
+ }
+ })
+ }
+ });
+
Workspace {
modal: None,
center: PaneGroup::new(pane.id()),
@@ -395,12 +414,14 @@ 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,
worktrees: Default::default(),
items: Default::default(),
loading_items: Default::default(),
+ _observe_current_user,
}
}
@@ -625,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| {
@@ -936,6 +957,68 @@ impl Workspace {
pub fn active_pane(&self) -> &ViewHandle<Pane> {
&self.active_pane
}
+
+ fn render_connection_status(&self) -> Option<ElementBox> {
+ let theme = &self.settings.borrow().theme;
+ match &*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<Self>) -> ElementBox {
+ let theme = &self.settings.borrow().theme;
+ let avatar = if let Some(avatar) = self
+ .user_store
+ .current_user()
+ .borrow()
+ .as_ref()
+ .and_then(|user| user.avatar.clone())
+ {
+ Image::new(avatar)
+ .with_style(theme.workspace.titlebar.avatar)
+ .boxed()
+ } else {
+ MouseEventHandler::new::<Authenticate, _, _, _>(0, cx, |_, _| {
+ Svg::new("icons/signed-out-12.svg")
+ .with_color(theme.workspace.titlebar.icon_color)
+ .boxed()
+ })
+ .on_click(|cx| cx.dispatch_action(Authenticate))
+ .with_cursor_style(CursorStyle::PointingHand)
+ .boxed()
+ };
+
+ ConstrainedBox::new(
+ Align::new(
+ ConstrainedBox::new(avatar)
+ .with_width(theme.workspace.titlebar.avatar_width)
+ .boxed(),
+ )
+ .boxed(),
+ )
+ .with_width(theme.workspace.right_sidebar.width)
+ .boxed()
+ }
}
impl Entity for Workspace {
@@ -955,15 +1038,30 @@ impl View for Workspace {
.with_child(
ConstrainedBox::new(
Container::new(
- Align::new(
- Label::new(
- "zed".into(),
- theme.workspace.titlebar.label.clone()
- ).boxed()
- )
- .boxed()
+ Stack::new()
+ .with_child(
+ Align::new(
+ Label::new(
+ "zed".into(),
+ theme.workspace.titlebar.title.clone(),
+ )
+ .boxed(),
+ )
+ .boxed(),
+ )
+ .with_child(
+ 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.)
@@ -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| {
@@ -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::<SidebarButton, _, _, _>(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::<SidebarButton, _, _, _>(
+ 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()
}
@@ -155,7 +165,7 @@ impl Sidebar {
let side = self.side;
MouseEventHandler::new::<Self, _, _, _>(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 {
@@ -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<dyn 'static + Send + Unpin + futures::Sink<WebSocketMessage, Error = WebSocketError>>,
pub(crate) rx: Box<
@@ -13,7 +13,7 @@ pub struct Conn {
>,
}
-impl Conn {
+impl Connection {
pub fn new<S>(stream: S) -> Self
where
S: 'static
@@ -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::*;
@@ -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<T: RequestMessage> TypedEnvelope<T> {
}
pub struct Peer {
- connections: RwLock<HashMap<ConnectionId, Connection>>,
+ connections: RwLock<HashMap<ConnectionId, ConnectionState>>,
next_connection_id: AtomicU32,
}
#[derive(Clone)]
-struct Connection {
+struct ConnectionState {
outgoing_tx: mpsc::Sender<proto::Envelope>,
next_message_id: Arc<AtomicU32>,
response_channels: Arc<Mutex<HashMap<u32, mpsc::Sender<proto::Envelope>>>>,
@@ -100,7 +100,7 @@ impl Peer {
pub async fn add_connection(
self: &Arc<Self>,
- conn: Conn,
+ connection: Connection,
) -> (
ConnectionId,
impl Future<Output = anyhow::Result<()>> + 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<Output = Result<()>> {
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<Output = Result<()>> {
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<Output = Result<()>> {
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<Output = Result<()>> {
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<Self>,
connection_id: ConnectionId,
- ) -> impl Future<Output = Result<Connection>> {
+ ) -> impl Future<Output = Result<ConnectionState>> {
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();