mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-31 19:05:00 +07:00
Merge branch 'render-images'
This commit is contained in:
commit
f6bf0792ce
45 changed files with 1450 additions and 512 deletions
86
Cargo.lock
generated
86
Cargo.lock
generated
|
|
@ -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,
|
||||
|
|
|
|||
90
gpui/src/elements/image.rs
Normal file
90
gpui/src/elements/image.rs
Normal file
|
|
@ -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),
|
||||
|
|
|
|||
43
gpui/src/image_data.rs
Normal file
43
gpui/src/image_data.rs
Normal file
|
|
@ -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) {
|
||||
|
|
|
|||
49
gpui/src/platform/mac/image_cache.rs
Normal file
49
gpui/src/platform/mac/image_cache.rs
Normal file
|
|
@ -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");
|
||||
})
|
||||
})
|
||||
.override_establish_connection(move |credentials, cx| {
|
||||
assert_eq!(credentials.user_id, client_user_id.0 as u64);
|
||||
assert_eq!(credentials.access_token, "the-token");
|
||||
|
||||
let server = server.clone();
|
||||
let connection_killers = connection_killers.clone();
|
||||
let forbid_connections = forbid_connections.clone();
|
||||
let client_name = client_name.clone();
|
||||
cx.spawn(move |cx| async move {
|
||||
if forbid_connections.load(SeqCst) {
|
||||
Err(anyhow!("server is forbidding connections"))
|
||||
} else {
|
||||
let (client_conn, server_conn, kill_conn) = Conn::in_memory();
|
||||
connection_killers.lock().insert(client_user_id, kill_conn);
|
||||
cx.background()
|
||||
.spawn(server.handle_connection(
|
||||
server_conn,
|
||||
client_name,
|
||||
client_user_id,
|
||||
))
|
||||
.detach();
|
||||
Ok(client_conn)
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
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"
|
||||
|
|
|
|||
3
zed/assets/icons/offline-14.svg
Normal file
3
zed/assets/icons/offline-14.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.14992 9.84972C1.99189 9.84972 1.04997 8.90759 1.04997 7.74977C1.04997 6.87042 1.61368 6.07944 2.45278 5.78173L3.15692 5.53193L3.15307 4.98616L2.15704 4.18643C2.12729 4.39861 2.10016 4.59111 2.1017 4.79235C0.880227 5.22546 0 6.38043 0 7.74977C0 9.48879 1.41024 10.8997 3.14992 10.8997H10.6988L9.3592 9.84972H3.14992ZM13.0569 10.0816C13.6343 9.5391 13.9996 8.77787 13.9996 7.92477C13.9996 6.58321 13.1056 5.46171 11.8855 5.09203C11.8888 5.04391 11.8997 4.99797 11.8997 4.94985C11.8997 3.59582 10.8038 2.49991 9.44976 2.49991C9.20017 2.49991 8.96436 2.54819 8.73752 2.61753C8.06948 1.70171 6.99544 1.09995 5.77485 1.09995C4.71394 1.09995 3.76678 1.55668 3.1018 2.27679L0.849166 0.51172C0.752699 0.436537 0.638515 0.399963 0.525643 0.399963C0.369897 0.399963 0.215398 0.468999 0.112194 0.600946C-0.0669139 0.82914 -0.0272556 1.15944 0.201048 1.33816L13.1509 11.4881C13.3806 11.6676 13.71 11.6265 13.8879 11.3989C14.067 11.1705 14.0273 10.8405 13.799 10.6617L13.0569 10.0816ZM12.2169 9.42317L3.92865 2.92427C4.40114 2.44916 5.05081 2.14992 5.77485 2.14992C6.61483 2.14992 7.38547 2.54585 7.88923 3.23642L8.32979 3.84016L9.04442 3.62167C10.1132 3.29487 10.8869 4.18078 10.8373 5.03039L10.7893 5.85549L11.58 6.09589C12.3984 6.34544 12.9497 7.08042 12.9497 7.92477C12.9497 8.53288 12.6609 9.0688 12.2169 9.42317Z" fill="#B3B3B3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
3
zed/assets/icons/signed-out-12.svg
Normal file
3
zed/assets/icons/signed-out-12.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 3.04688C5.00332 3.04688 4.19531 3.85488 4.19531 4.85156C4.19531 5.84824 5.00332 6.65625 6 6.65625C6.99668 6.65625 7.80469 5.84824 7.80469 4.85156C7.80469 3.85488 6.99668 3.04688 6 3.04688ZM6 5.67188C5.5476 5.67188 5.17969 5.30376 5.17969 4.85156C5.17969 4.39834 5.54678 4.03125 6 4.03125C6.45322 4.03125 6.82031 4.39916 6.82031 4.85156C6.82031 5.30479 6.45322 5.67188 6 5.67188ZM6 0.75C3.1002 0.75 0.75 3.1002 0.75 6C0.75 8.8998 3.1002 11.25 6 11.25C8.8998 11.25 11.25 8.8998 11.25 6C11.25 3.1002 8.8998 0.75 6 0.75ZM6 10.2656C5.04167 10.2656 4.15922 9.94406 3.44678 9.4086C3.80156 8.72754 4.49062 8.29688 5.26582 8.29688H6.73603C7.5102 8.29688 8.19844 8.72774 8.55466 9.4086C7.8416 9.94365 6.95771 10.2656 6 10.2656ZM9.28535 8.71729C8.73164 7.85186 7.78828 7.3125 6.73418 7.3125H5.26582C4.21254 7.3125 3.26938 7.85083 2.71465 8.71688C2.1027 7.979 1.73438 7.03154 1.73438 6C1.73438 3.64775 3.64796 1.73438 6 1.73438C8.35204 1.73438 10.2656 3.64796 10.2656 6C10.2656 7.03154 9.89648 7.979 9.28535 8.71729Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -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,
|
||||
|
|
|
|||
26
zed/src/http.rs
Normal file
26
zed/src/http.rs
Normal file
|
|
@ -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),
|
||||
});
|
||||
|
||||
|
|
|
|||
292
zed/src/rpc.rs
292
zed/src/rpc.rs
|
|
@ -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]
|
||||
|
|
|
|||
131
zed/src/test.rs
131
zed/src/test.rs
|
|
@ -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)
|
||||
|
|
|
|||
130
zed/src/user.rs
130
zed/src/user.rs
|
|
@ -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?;
|
||||
self.load_users(vec![user_id]).await?;
|
||||
self.users
|
||||
.lock()
|
||||
.get(&user_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("server responded with no users"))
|
||||
}
|
||||
|
||||
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"))
|
||||
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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue