Checkpoint: start rendering images

This commit is contained in:
Antonio Scandurra 2023-10-04 15:03:21 +02:00
parent 5c750b6880
commit 1816ab95a0
11 changed files with 231 additions and 77 deletions

View file

@ -84,7 +84,6 @@ impl ImageCache {
let format = image::guess_format(&body)?; let format = image::guess_format(&body)?;
let image = let image =
image::load_from_memory_with_format(&body, format)?.into_bgra8(); image::load_from_memory_with_format(&body, format)?.into_bgra8();
Ok(ImageData::new(image)) Ok(ImageData::new(image))
} }
} }

View file

@ -8,9 +8,9 @@ pub use model_context::*;
use refineable::Refineable; use refineable::Refineable;
use crate::{ use crate::{
current_platform, run_on_main, spawn_on_main, AssetSource, Context, LayoutId, MainThread, current_platform, image_cache::ImageCache, run_on_main, spawn_on_main, AssetSource, Context,
MainThreadOnly, Platform, PlatformDispatcher, RootView, SvgRenderer, TextStyle, LayoutId, MainThread, MainThreadOnly, Platform, PlatformDispatcher, RootView, SvgRenderer,
TextStyleRefinement, TextSystem, Window, WindowContext, WindowHandle, WindowId, TextStyle, TextStyleRefinement, TextSystem, Window, WindowContext, WindowHandle, WindowId,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use collections::{HashMap, VecDeque}; use collections::{HashMap, VecDeque};
@ -23,24 +23,33 @@ use std::{
mem, mem,
sync::{Arc, Weak}, sync::{Arc, Weak},
}; };
use util::ResultExt; use util::{
http::{self, HttpClient},
ResultExt,
};
#[derive(Clone)] #[derive(Clone)]
pub struct App(Arc<Mutex<MainThread<AppContext>>>); pub struct App(Arc<Mutex<MainThread<AppContext>>>);
impl App { impl App {
pub fn production(asset_source: Arc<dyn AssetSource>) -> Self { pub fn production(asset_source: Arc<dyn AssetSource>) -> Self {
Self::new(current_platform(), asset_source) let http_client = http::client();
Self::new(current_platform(), asset_source, http_client)
} }
#[cfg(any(test, feature = "test"))] #[cfg(any(test, feature = "test"))]
pub fn test() -> Self { pub fn test() -> Self {
let platform = Arc::new(super::TestPlatform::new()); let platform = Arc::new(super::TestPlatform::new());
let asset_source = Arc::new(()); let asset_source = Arc::new(());
Self::new(platform, asset_source) let http_client = util::http::FakeHttpClient::with_404_response();
Self::new(platform, asset_source, http_client)
} }
fn new(platform: Arc<dyn Platform>, asset_source: Arc<dyn AssetSource>) -> Self { fn new(
platform: Arc<dyn Platform>,
asset_source: Arc<dyn AssetSource>,
http_client: Arc<dyn HttpClient>,
) -> Self {
let dispatcher = platform.dispatcher(); let dispatcher = platform.dispatcher();
let text_system = Arc::new(TextSystem::new(platform.text_system())); let text_system = Arc::new(TextSystem::new(platform.text_system()));
let entities = EntityMap::new(); let entities = EntityMap::new();
@ -52,6 +61,7 @@ impl App {
dispatcher, dispatcher,
text_system, text_system,
svg_renderer: SvgRenderer::new(asset_source), svg_renderer: SvgRenderer::new(asset_source),
image_cache: ImageCache::new(http_client),
pending_updates: 0, pending_updates: 0,
text_style_stack: Vec::new(), text_style_stack: Vec::new(),
state_stacks_by_type: HashMap::default(), state_stacks_by_type: HashMap::default(),
@ -87,6 +97,7 @@ pub struct AppContext {
text_system: Arc<TextSystem>, text_system: Arc<TextSystem>,
pending_updates: usize, pending_updates: usize,
pub(crate) svg_renderer: SvgRenderer, pub(crate) svg_renderer: SvgRenderer,
pub(crate) image_cache: ImageCache,
pub(crate) text_style_stack: Vec<TextStyleRefinement>, pub(crate) text_style_stack: Vec<TextStyleRefinement>,
pub(crate) state_stacks_by_type: HashMap<TypeId, Vec<Box<dyn Any + Send + Sync>>>, pub(crate) state_stacks_by_type: HashMap<TypeId, Vec<Box<dyn Any + Send + Sync>>>,
pub(crate) unit_entity: Handle<()>, pub(crate) unit_entity: Handle<()>,

View file

@ -3,7 +3,9 @@ use anyhow::anyhow;
use image::{Bgra, ImageBuffer}; use image::{Bgra, ImageBuffer};
use std::{ use std::{
borrow::Cow, borrow::Cow,
cmp::Ordering,
fmt, fmt,
hash::{Hash, Hasher},
sync::atomic::{AtomicUsize, Ordering::SeqCst}, sync::atomic::{AtomicUsize, Ordering::SeqCst},
}; };
@ -25,18 +27,21 @@ impl AssetSource for () {
} }
} }
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ImageId(usize);
pub struct ImageData { pub struct ImageData {
pub id: usize, pub id: ImageId,
data: ImageBuffer<Bgra<u8>, Vec<u8>>, data: ImageBuffer<Bgra<u8>, Vec<u8>>,
} }
impl ImageData { impl ImageData {
pub fn from_raw(size: Size<DevicePixels>, bytes: Vec<u8>) -> Self { pub fn new(data: ImageBuffer<Bgra<u8>, Vec<u8>>) -> Self {
static NEXT_ID: AtomicUsize = AtomicUsize::new(0); static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
Self { Self {
id: NEXT_ID.fetch_add(1, SeqCst), id: ImageId(NEXT_ID.fetch_add(1, SeqCst)),
data: ImageBuffer::from_raw(size.width.into(), size.height.into(), bytes).unwrap(), data,
} }
} }
@ -44,10 +49,6 @@ impl ImageData {
&self.data &self.data
} }
pub fn into_bytes(self) -> Vec<u8> {
self.data.into_raw()
}
pub fn size(&self) -> Size<DevicePixels> { pub fn size(&self) -> Size<DevicePixels> {
let (width, height) = self.data.dimensions(); let (width, height) = self.data.dimensions();
size(width.into(), height.into()) size(width.into(), height.into())

View file

@ -1,11 +1,14 @@
use crate::{Element, Layout, LayoutId, Result, Style, StyleHelpers, Styled}; use crate::{
Element, Layout, LayoutId, Result, SharedString, Style, StyleHelpers, Styled, ViewContext,
};
use futures::FutureExt;
use refineable::RefinementCascade; use refineable::RefinementCascade;
use std::marker::PhantomData; use std::marker::PhantomData;
use util::arc_cow::ArcCow; use util::ResultExt;
pub struct Img<S> { pub struct Img<S> {
style: RefinementCascade<Style>, style: RefinementCascade<Style>,
uri: Option<ArcCow<'static, str>>, uri: Option<SharedString>,
state_type: PhantomData<S>, state_type: PhantomData<S>,
} }
@ -18,7 +21,7 @@ pub fn img<S>() -> Img<S> {
} }
impl<S> Img<S> { impl<S> Img<S> {
pub fn uri(mut self, uri: impl Into<ArcCow<'static, str>>) -> Self { pub fn uri(mut self, uri: impl Into<SharedString>) -> Self {
self.uri = Some(uri.into()); self.uri = Some(uri.into());
self self
} }
@ -31,7 +34,7 @@ impl<S: 'static> Element for Img<S> {
fn layout( fn layout(
&mut self, &mut self,
_: &mut Self::State, _: &mut Self::State,
cx: &mut crate::ViewContext<Self::State>, cx: &mut ViewContext<Self::State>,
) -> anyhow::Result<(LayoutId, Self::FrameState)> ) -> anyhow::Result<(LayoutId, Self::FrameState)>
where where
Self: Sized, Self: Sized,
@ -46,7 +49,7 @@ impl<S: 'static> Element for Img<S> {
layout: Layout, layout: Layout,
_: &mut Self::State, _: &mut Self::State,
_: &mut Self::FrameState, _: &mut Self::FrameState,
cx: &mut crate::ViewContext<Self::State>, cx: &mut ViewContext<Self::State>,
) -> Result<()> { ) -> Result<()> {
let style = self.computed_style(); let style = self.computed_style();
let order = layout.order; let order = layout.order;
@ -54,36 +57,24 @@ impl<S: 'static> Element for Img<S> {
style.paint(order, bounds, cx); style.paint(order, bounds, cx);
// if let Some(uri) = &self.uri { if let Some(uri) = &self.uri {
// let image_future = cx.image_cache.get(uri.clone()); let image_future = cx.image_cache.get(uri.clone());
// if let Some(data) = image_future if let Some(data) = image_future
// .clone() .clone()
// .now_or_never() .now_or_never()
// .and_then(ResultExt::log_err) .and_then(ResultExt::log_err)
// { {
// let rem_size = cx.rem_size(); cx.paint_image(bounds, order, data, false)?;
// cx.scene().push_image(scene::Image { } else {
// bounds, log::warn!("image not loaded yet");
// border: gpui::Border { // cx.spawn(|this, mut cx| async move {
// color: style.border_color.unwrap_or_default().into(), // if image_future.await.log_err().is_some() {
// top: style.border_widths.top.to_pixels(rem_size), // this.update(&mut cx, |_, cx| cx.notify()).ok();
// right: style.border_widths.right.to_pixels(rem_size), // }
// bottom: style.border_widths.bottom.to_pixels(rem_size), // })
// left: style.border_widths.left.to_pixels(rem_size), // .detach();
// }, }
// corner_radii: style.corner_radii.to_gpui(bounds.size(), rem_size), }
// grayscale: false,
// data,
// })
// } else {
// cx.spawn(|this, mut cx| async move {
// if image_future.await.log_err().is_some() {
// this.update(&mut cx, |_, cx| cx.notify()).ok();
// }
// })
// .detach();
// }
// }
Ok(()) Ok(())
} }
} }

View file

@ -645,6 +645,10 @@ impl ScaledPixels {
pub fn floor(&self) -> Self { pub fn floor(&self) -> Self {
Self(self.0.floor()) Self(self.0.floor())
} }
pub fn ceil(&self) -> Self {
Self(self.0.ceil())
}
} }
impl Eq for ScaledPixels {} impl Eq for ScaledPixels {}

View file

@ -5,6 +5,7 @@ mod element;
mod elements; mod elements;
mod executor; mod executor;
mod geometry; mod geometry;
mod image_cache;
mod platform; mod platform;
mod scene; mod scene;
mod style; mod style;

View file

@ -0,0 +1,99 @@
use crate::{ImageData, ImageId, SharedString};
use collections::HashMap;
use futures::{
future::{BoxFuture, Shared},
AsyncReadExt, FutureExt,
};
use image::ImageError;
use parking_lot::Mutex;
use std::sync::Arc;
use thiserror::Error;
use util::http::{self, HttpClient};
#[derive(PartialEq, Eq, Hash, Clone)]
pub struct RenderImageParams {
pub(crate) image_id: ImageId,
}
#[derive(Debug, Error, Clone)]
pub enum Error {
#[error("http error: {0}")]
Client(#[from] http::Error),
#[error("IO error: {0}")]
Io(Arc<std::io::Error>),
#[error("unexpected http status: {status}, body: {body}")]
BadStatus {
status: http::StatusCode,
body: String,
},
#[error("image error: {0}")]
Image(Arc<ImageError>),
}
impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Error::Io(Arc::new(error))
}
}
impl From<ImageError> for Error {
fn from(error: ImageError) -> Self {
Error::Image(Arc::new(error))
}
}
pub struct ImageCache {
client: Arc<dyn HttpClient>,
images: Arc<Mutex<HashMap<SharedString, FetchImageFuture>>>,
}
type FetchImageFuture = Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>;
impl ImageCache {
pub fn new(client: Arc<dyn HttpClient>) -> Self {
ImageCache {
client,
images: Default::default(),
}
}
pub fn get(
&self,
uri: impl Into<SharedString>,
) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
let uri = uri.into();
let mut images = self.images.lock();
match images.get(&uri) {
Some(future) => future.clone(),
None => {
let client = self.client.clone();
let future = {
let uri = uri.clone();
async move {
let mut response = client.get(uri.as_ref(), ().into(), true).await?;
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
if !response.status().is_success() {
return Err(Error::BadStatus {
status: response.status(),
body: String::from_utf8_lossy(&body).into_owned(),
});
}
let format = image::guess_format(&body)?;
let image =
image::load_from_memory_with_format(&body, format)?.into_bgra8();
Ok(Arc::new(ImageData::new(image)))
}
}
.boxed()
.shared();
images.insert(uri, future.clone());
future
}
}
}
}

View file

@ -5,6 +5,7 @@ mod mac;
#[cfg(any(test, feature = "test"))] #[cfg(any(test, feature = "test"))]
mod test; mod test;
use crate::image_cache::RenderImageParams;
use crate::{ use crate::{
AnyWindowHandle, Bounds, DevicePixels, Font, FontId, FontMetrics, GlyphId, Pixels, Point, AnyWindowHandle, Bounds, DevicePixels, Font, FontId, FontMetrics, GlyphId, Pixels, Point,
RenderGlyphParams, RenderSvgParams, Result, Scene, ShapedLine, SharedString, Size, RenderGlyphParams, RenderSvgParams, Result, Scene, ShapedLine, SharedString, Size,
@ -14,6 +15,7 @@ use async_task::Runnable;
use futures::channel::oneshot; use futures::channel::oneshot;
use seahash::SeaHasher; use seahash::SeaHasher;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::ffi::c_void; use std::ffi::c_void;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::{ use std::{
@ -179,6 +181,7 @@ pub trait PlatformTextSystem: Send + Sync {
pub enum AtlasKey { pub enum AtlasKey {
Glyph(RenderGlyphParams), Glyph(RenderGlyphParams),
Svg(RenderSvgParams), Svg(RenderSvgParams),
Image(RenderImageParams),
} }
impl AtlasKey { impl AtlasKey {
@ -186,6 +189,7 @@ impl AtlasKey {
match self { match self {
AtlasKey::Glyph(params) => !params.is_emoji, AtlasKey::Glyph(params) => !params.is_emoji,
AtlasKey::Svg(_) => true, AtlasKey::Svg(_) => true,
AtlasKey::Image(_) => false,
} }
} }
} }
@ -202,11 +206,17 @@ impl From<RenderSvgParams> for AtlasKey {
} }
} }
impl From<RenderImageParams> for AtlasKey {
fn from(params: RenderImageParams) -> Self {
Self::Image(params)
}
}
pub trait PlatformAtlas: Send + Sync { pub trait PlatformAtlas: Send + Sync {
fn get_or_insert_with( fn get_or_insert_with<'a>(
&self, &self,
key: &AtlasKey, key: &AtlasKey,
build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Vec<u8>)>, build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Cow<'a, [u8]>)>,
) -> Result<AtlasTile>; ) -> Result<AtlasTile>;
fn clear(&self); fn clear(&self);

View file

@ -1,3 +1,5 @@
use std::borrow::Cow;
use crate::{ use crate::{
AtlasKey, AtlasTextureId, AtlasTile, Bounds, DevicePixels, PlatformAtlas, Point, Size, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DevicePixels, PlatformAtlas, Point, Size,
}; };
@ -31,10 +33,10 @@ struct MetalAtlasState {
} }
impl PlatformAtlas for MetalAtlas { impl PlatformAtlas for MetalAtlas {
fn get_or_insert_with( fn get_or_insert_with<'a>(
&self, &self,
key: &AtlasKey, key: &AtlasKey,
build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Vec<u8>)>, build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Cow<'a, [u8]>)>,
) -> Result<AtlasTile> { ) -> Result<AtlasTile> {
let mut lock = self.0.lock(); let mut lock = self.0.lock();
if let Some(tile) = lock.tiles_by_key.get(key) { if let Some(tile) = lock.tiles_by_key.get(key) {

View file

@ -1,14 +1,15 @@
use crate::{ use crate::{
px, AnyView, AppContext, AvailableSpace, BorrowAppContext, Bounds, Context, Corners, image_cache::RenderImageParams, px, AnyView, AppContext, AvailableSpace, BorrowAppContext,
DevicePixels, Effect, Element, EntityId, FontId, GlyphId, Handle, Hsla, IsZero, LayerId, Bounds, Context, Corners, DevicePixels, Effect, Element, EntityId, FontId, GlyphId, Handle,
LayoutId, MainThread, MainThreadOnly, MonochromeSprite, Pixels, PlatformAtlas, PlatformWindow, Hsla, ImageData, IsZero, LayerId, LayoutId, MainThread, MainThreadOnly, MonochromeSprite,
Point, PolychromeSprite, Reference, RenderGlyphParams, RenderSvgParams, ScaledPixels, Scene, Pixels, PlatformAtlas, PlatformWindow, Point, PolychromeSprite, Reference, RenderGlyphParams,
SharedString, Size, Style, TaffyLayoutEngine, WeakHandle, WindowOptions, SUBPIXEL_VARIANTS, RenderSvgParams, ScaledPixels, Scene, SharedString, Size, Style, TaffyLayoutEngine, WeakHandle,
WindowOptions, SUBPIXEL_VARIANTS,
}; };
use anyhow::Result; use anyhow::Result;
use futures::Future; use futures::Future;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{any::TypeId, marker::PhantomData, mem, sync::Arc}; use std::{any::TypeId, borrow::Cow, marker::PhantomData, mem, sync::Arc};
use util::ResultExt; use util::ResultExt;
pub struct AnyWindow {} pub struct AnyWindow {}
@ -234,12 +235,13 @@ impl<'a, 'w> WindowContext<'a, 'w> {
let raster_bounds = self.text_system().raster_bounds(&params)?; let raster_bounds = self.text_system().raster_bounds(&params)?;
if !raster_bounds.is_zero() { if !raster_bounds.is_zero() {
let layer_id = self.current_layer_id(); let layer_id = self.current_layer_id();
let tile = self let tile =
.window self.window
.sprite_atlas .sprite_atlas
.get_or_insert_with(&params.clone().into(), &mut || { .get_or_insert_with(&params.clone().into(), &mut || {
self.text_system().rasterize_glyph(&params) let (size, bytes) = self.text_system().rasterize_glyph(&params)?;
})?; Ok((size, Cow::Owned(bytes)))
})?;
let bounds = Bounds { let bounds = Bounds {
origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into), origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into),
size: tile.bounds.size.map(Into::into), size: tile.bounds.size.map(Into::into),
@ -283,12 +285,13 @@ impl<'a, 'w> WindowContext<'a, 'w> {
let raster_bounds = self.text_system().raster_bounds(&params)?; let raster_bounds = self.text_system().raster_bounds(&params)?;
if !raster_bounds.is_zero() { if !raster_bounds.is_zero() {
let layer_id = self.current_layer_id(); let layer_id = self.current_layer_id();
let tile = self let tile =
.window self.window
.sprite_atlas .sprite_atlas
.get_or_insert_with(&params.clone().into(), &mut || { .get_or_insert_with(&params.clone().into(), &mut || {
self.text_system().rasterize_glyph(&params) let (size, bytes) = self.text_system().rasterize_glyph(&params)?;
})?; Ok((size, Cow::Owned(bytes)))
})?;
let bounds = Bounds { let bounds = Bounds {
origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into), origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into),
size: tile.bounds.size.map(Into::into), size: tile.bounds.size.map(Into::into),
@ -331,7 +334,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
.sprite_atlas .sprite_atlas
.get_or_insert_with(&params.clone().into(), &mut || { .get_or_insert_with(&params.clone().into(), &mut || {
let bytes = self.svg_renderer.render(&params)?; let bytes = self.svg_renderer.render(&params)?;
Ok((params.size, bytes)) Ok((params.size, Cow::Owned(bytes)))
})?; })?;
let content_mask = self.content_mask().scale(scale_factor); let content_mask = self.content_mask().scale(scale_factor);
@ -349,6 +352,39 @@ impl<'a, 'w> WindowContext<'a, 'w> {
Ok(()) Ok(())
} }
pub fn paint_image(
&mut self,
bounds: Bounds<Pixels>,
order: u32,
data: Arc<ImageData>,
grayscale: bool,
) -> Result<()> {
let scale_factor = self.scale_factor();
let bounds = bounds.scale(scale_factor);
let params = RenderImageParams { image_id: data.id };
let layer_id = self.current_layer_id();
let tile = self
.window
.sprite_atlas
.get_or_insert_with(&params.clone().into(), &mut || {
Ok((data.size(), Cow::Borrowed(data.as_bytes())))
})?;
let content_mask = self.content_mask().scale(scale_factor);
self.window.scene.insert(
layer_id,
PolychromeSprite {
order,
bounds,
content_mask,
tile,
},
);
Ok(())
}
pub(crate) fn draw(&mut self) -> Result<()> { pub(crate) fn draw(&mut self) -> Result<()> {
let unit_entity = self.unit_entity.clone(); let unit_entity = self.unit_entity.clone();
self.update_entity(&unit_entity, |_, cx| { self.update_entity(&unit_entity, |_, cx| {

View file

@ -1,7 +1,7 @@
use crate::theme::{theme, Theme}; use crate::theme::{theme, Theme};
use gpui3::{ use gpui3::{
div, img, svg, view, AppContext, ArcCow, Context, Element, IntoAnyElement, ParentElement, div, img, svg, view, AppContext, ArcCow, Context, Element, IntoAnyElement, ParentElement,
ScrollState, StyleHelpers, View, ViewContext, WindowContext, ScrollState, SharedString, StyleHelpers, View, ViewContext, WindowContext,
}; };
pub struct CollabPanel { pub struct CollabPanel {
@ -144,7 +144,7 @@ impl CollabPanel {
fn list_item( fn list_item(
&self, &self,
avatar_uri: impl Into<ArcCow<'static, str>>, avatar_uri: impl Into<SharedString>,
label: impl IntoAnyElement<Self>, label: impl IntoAnyElement<Self>,
theme: &Theme, theme: &Theme,
) -> impl Element<State = Self> { ) -> impl Element<State = Self> {