Checkpoint: render SVGs

This commit is contained in:
Antonio Scandurra 2023-10-04 10:51:47 +02:00
parent a1ee2db6d1
commit 4cf2ba20c2
13 changed files with 187 additions and 175 deletions

View file

@ -8,9 +8,9 @@ pub use model_context::*;
use refineable::Refineable;
use crate::{
current_platform, run_on_main, spawn_on_main, Context, LayoutId, MainThread, MainThreadOnly,
Platform, PlatformDispatcher, RootView, TextStyle, TextStyleRefinement, TextSystem, Window,
WindowContext, WindowHandle, WindowId,
current_platform, run_on_main, spawn_on_main, AssetSource, Context, LayoutId, MainThread,
MainThreadOnly, Platform, PlatformDispatcher, RootView, SvgRenderer, TextStyle,
TextStyleRefinement, TextSystem, Window, WindowContext, WindowHandle, WindowId,
};
use anyhow::{anyhow, Result};
use collections::{HashMap, VecDeque};
@ -29,16 +29,18 @@ use util::ResultExt;
pub struct App(Arc<Mutex<MainThread<AppContext>>>);
impl App {
pub fn production() -> Self {
Self::new(current_platform())
pub fn production(asset_source: Arc<dyn AssetSource>) -> Self {
Self::new(current_platform(), asset_source)
}
#[cfg(any(test, feature = "test"))]
pub fn test() -> Self {
Self::new(Arc::new(super::TestPlatform::new()))
let platform = Arc::new(super::TestPlatform::new());
let asset_source = Arc::new(());
Self::new(platform, asset_source)
}
fn new(platform: Arc<dyn Platform>) -> Self {
fn new(platform: Arc<dyn Platform>, asset_source: Arc<dyn AssetSource>) -> Self {
let dispatcher = platform.dispatcher();
let text_system = Arc::new(TextSystem::new(platform.text_system()));
let entities = EntityMap::new();
@ -49,6 +51,7 @@ impl App {
platform: MainThreadOnly::new(platform, dispatcher.clone()),
dispatcher,
text_system,
svg_renderer: SvgRenderer::new(asset_source),
pending_updates: 0,
text_style_stack: Vec::new(),
state_stacks_by_type: HashMap::default(),
@ -83,6 +86,7 @@ pub struct AppContext {
dispatcher: Arc<dyn PlatformDispatcher>,
text_system: Arc<TextSystem>,
pending_updates: usize,
pub(crate) svg_renderer: SvgRenderer,
pub(crate) text_style_stack: Vec<TextStyleRefinement>,
pub(crate) state_stacks_by_type: HashMap<TypeId, Vec<Box<dyn Any + Send + Sync>>>,
pub(crate) unit_entity: Handle<()>,

View file

@ -44,6 +44,10 @@ impl ImageData {
&self.data
}
pub fn into_bytes(self) -> Vec<u8> {
self.data.into_raw()
}
pub fn size(&self) -> Size<DevicePixels> {
let (width, height) = self.data.dimensions();
size(width.into(), height.into())

View file

@ -1,9 +1,9 @@
use crate::{Element, Layout, LayoutId, Result, Style, StyleHelpers, Styled};
use crate::{Element, Layout, LayoutId, Result, SharedString, Style, StyleHelpers, Styled};
use refineable::RefinementCascade;
use std::{borrow::Cow, marker::PhantomData};
use std::marker::PhantomData;
pub struct Svg<S> {
path: Option<Cow<'static, str>>,
path: Option<SharedString>,
style: RefinementCascade<Style>,
state_type: PhantomData<S>,
}
@ -17,7 +17,7 @@ pub fn svg<S>() -> Svg<S> {
}
impl<S> Svg<S> {
pub fn path(mut self, path: impl Into<Cow<'static, str>>) -> Self {
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
self.path = Some(path.into());
self
}
@ -41,28 +41,18 @@ impl<S: 'static> Element for Svg<S> {
fn paint(
&mut self,
_layout: Layout,
layout: Layout,
_: &mut Self::State,
_: &mut Self::FrameState,
_cx: &mut crate::ViewContext<S>,
cx: &mut crate::ViewContext<S>,
) -> Result<()>
where
Self: Sized,
{
// todo!
// let fill_color = self.computed_style().fill.and_then(|fill| fill.color());
// if let Some((path, fill_color)) = self.path.as_ref().zip(fill_color) {
// if let Some(svg_tree) = cx.asset_cache.svg(path).log_err() {
// let icon = scene::Icon {
// bounds: layout.bounds + parent_origin,
// svg: svg_tree,
// path: path.clone(),
// color: Rgba::from(fill_color).into(),
// };
// cx.scene().push_icon(icon);
// }
// }
let fill_color = self.computed_style().fill.and_then(|fill| fill.color());
if let Some((path, fill_color)) = self.path.as_ref().zip(fill_color) {
cx.paint_svg(layout.bounds, layout.order, path.clone(), fill_color)?;
}
Ok(())
}
}

View file

@ -10,7 +10,7 @@ mod scene;
mod style;
mod style_helpers;
mod styled;
mod svg_library;
mod svg_renderer;
mod taffy;
mod text_system;
mod util;
@ -26,7 +26,7 @@ pub use elements::*;
pub use executor::*;
pub use geometry::*;
pub use gpui3_macros::*;
pub use svg_library::*;
pub use svg_renderer::*;
pub use platform::*;
pub use refineable::*;

View file

@ -7,7 +7,7 @@ mod test;
use crate::{
AnyWindowHandle, Bounds, DevicePixels, Font, FontId, FontMetrics, GlyphId, Pixels, Point,
RenderGlyphParams, Result, Scene, ShapedLine, SharedString, Size,
RenderGlyphParams, RenderSvgParams, Result, Scene, ShapedLine, SharedString, Size,
};
use anyhow::anyhow;
use async_task::Runnable;
@ -147,7 +147,7 @@ pub trait PlatformWindow {
fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
fn draw(&self, scene: Scene);
fn glyph_atlas(&self) -> Arc<dyn PlatformAtlas>;
fn monochrome_sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
}
pub trait PlatformDispatcher: Send + Sync {
@ -178,7 +178,7 @@ pub trait PlatformTextSystem: Send + Sync {
#[derive(PartialEq, Eq, Hash, Clone)]
pub enum AtlasKey {
Glyph(RenderGlyphParams),
// Svg(RenderSvgParams),
Svg(RenderSvgParams),
}
impl From<RenderGlyphParams> for AtlasKey {
@ -187,6 +187,12 @@ impl From<RenderGlyphParams> for AtlasKey {
}
}
impl From<RenderSvgParams> for AtlasKey {
fn from(params: RenderSvgParams) -> Self {
Self::Svg(params)
}
}
pub trait PlatformAtlas: Send + Sync {
fn get_or_insert_with(
&self,

View file

@ -20,7 +20,7 @@ pub struct MetalRenderer {
sprites_pipeline_state: metal::RenderPipelineState,
unit_vertices: metal::Buffer,
instances: metal::Buffer,
glyph_atlas: Arc<MetalAtlas>,
monochrome_sprite_atlas: Arc<MetalAtlas>,
}
impl MetalRenderer {
@ -99,7 +99,7 @@ impl MetalRenderer {
);
let command_queue = device.new_command_queue();
let glyph_atlas = Arc::new(MetalAtlas::new(
let monochrome_sprite_atlas = Arc::new(MetalAtlas::new(
Size {
width: DevicePixels(1024),
height: DevicePixels(768),
@ -115,7 +115,7 @@ impl MetalRenderer {
sprites_pipeline_state,
unit_vertices,
instances,
glyph_atlas,
monochrome_sprite_atlas,
}
}
@ -123,8 +123,8 @@ impl MetalRenderer {
&*self.layer
}
pub fn glyph_atlas(&self) -> &Arc<MetalAtlas> {
&self.glyph_atlas
pub fn monochrome_sprite_atlas(&self) -> &Arc<MetalAtlas> {
&self.monochrome_sprite_atlas
}
pub fn draw(&mut self, scene: &mut Scene) {
@ -277,7 +277,7 @@ impl MetalRenderer {
}
align_offset(offset);
let texture = self.glyph_atlas.texture(texture_id);
let texture = self.monochrome_sprite_atlas.texture(texture_id);
let texture_size = size(
DevicePixels(texture.width() as i32),
DevicePixels(texture.height() as i32),

View file

@ -886,8 +886,8 @@ impl PlatformWindow for MacWindow {
}
}
fn glyph_atlas(&self) -> Arc<dyn PlatformAtlas> {
self.0.lock().renderer.glyph_atlas().clone()
fn monochrome_sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
self.0.lock().renderer.monochrome_sprite_atlas().clone()
}
}

View file

@ -1,102 +0,0 @@
use crate::{AssetSource, DevicePixels, ImageData, IsZero, Result, SharedString, Size};
use anyhow::anyhow;
use collections::HashMap;
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use std::hash::Hash;
use std::sync::Arc;
use usvg::Tree as SvgTree;
#[derive(Clone, PartialEq, Hash, Eq)]
pub struct SvgRenderParams {
path: SharedString,
size: Size<DevicePixels>,
}
pub struct SvgRenderer {
asset_source: Arc<dyn AssetSource>,
trees_by_path: RwLock<HashMap<SharedString, SvgTree>>,
rendered: RwLock<HashMap<SvgRenderParams, Arc<ImageData>>>,
}
impl SvgRenderer {
pub fn render(&self, params: SvgRenderParams) -> Result<Arc<ImageData>> {
if params.size.is_zero() {
return Err(anyhow!("can't render at a zero size"));
}
let rendered = self.rendered.upgradable_read();
if let Some(image_data) = rendered.get(&params) {
Ok(image_data.clone())
} else {
// There's no rendered SVG for the path at the requested size.
// Have we already loaded a tree for the path?
let trees_by_path = self.trees_by_path.upgradable_read();
let tree = if let Some(tree) = trees_by_path.get(&params.path) {
tree.clone()
} else {
// Load the tree
let bytes = self.asset_source.load(&params.path)?;
let tree = usvg::Tree::from_data(&bytes, &usvg::Options::default())?;
let mut trees_by_path = RwLockUpgradableReadGuard::upgrade(trees_by_path);
trees_by_path.insert(params.path.clone(), tree.clone());
tree
};
// Render the SVG to a pixmap with the specified width and height.
// Convert the pixmap's pixels into an image data and cache it in `rendered`.
let mut pixmap =
tiny_skia::Pixmap::new(params.size.width.into(), params.size.height.into())
.unwrap();
resvg::render(
&tree,
usvg::FitTo::Width(params.size.width.into()),
pixmap.as_mut(),
);
let alpha_mask = pixmap
.pixels()
.iter()
.map(|p| p.alpha())
.collect::<Vec<_>>();
let mut rendered = RwLockUpgradableReadGuard::upgrade(rendered);
let image_data = Arc::new(ImageData::from_raw(params.size, alpha_mask));
rendered.insert(params, image_data.clone());
Ok(image_data)
}
}
}
// impl SvgRenderer {
// pub fn render_svg(
// &mut self,
// size: Vector2I,
// path: Cow<'static, str>,
// svg: usvg::Tree,
// ) -> Option<IconSprite> {
// let mut pixmap = tiny_skia::Pixmap::new(size.x() as u32, size.y() as u32)?;
// resvg::render(&svg, usvg::FitTo::Width(size.x() as u32), pixmap.as_mut());
// let atlases = &mut self.atlases;
// match self.icons.entry(IconDescriptor {
// path,
// width: size.x(),
// height: size.y(),
// }) {
// Entry::Occupied(entry) => Some(entry.get().clone()),
// Entry::Vacant(entry) => {
// let mask = pixmap
// .pixels()
// .iter()
// .map(|a| a.alpha())
// .collect::<Vec<_>>();
// let (alloc_id, atlas_bounds) = atlases.upload(size, &mask)?;
// let icon_sprite = IconSprite {
// atlas_id: alloc_id.atlas_id,
// atlas_origin: atlas_bounds.origin(),
// size,
// };
// Some(entry.insert(icon_sprite).clone())
// }
// }
// }
// }

View file

@ -0,0 +1,47 @@
use crate::{AssetSource, DevicePixels, IsZero, Result, SharedString, Size};
use anyhow::anyhow;
use std::hash::Hash;
use std::sync::Arc;
#[derive(Clone, PartialEq, Hash, Eq)]
pub struct RenderSvgParams {
pub(crate) path: SharedString,
pub(crate) size: Size<DevicePixels>,
}
pub struct SvgRenderer {
asset_source: Arc<dyn AssetSource>,
}
impl SvgRenderer {
pub fn new(asset_source: Arc<dyn AssetSource>) -> Self {
Self { asset_source }
}
pub fn render(&self, params: &RenderSvgParams) -> Result<Vec<u8>> {
if params.size.is_zero() {
return Err(anyhow!("can't render at a zero size"));
}
// Load the tree.
let bytes = self.asset_source.load(&params.path)?;
let tree = usvg::Tree::from_data(&bytes, &usvg::Options::default())?;
// Render the SVG to a pixmap with the specified width and height.
let mut pixmap =
tiny_skia::Pixmap::new(params.size.width.into(), params.size.height.into()).unwrap();
resvg::render(
&tree,
usvg::FitTo::Width(params.size.width.into()),
pixmap.as_mut(),
);
// Convert the pixmap's pixels into an alpha mask.
let alpha_mask = pixmap
.pixels()
.iter()
.map(|p| p.alpha())
.collect::<Vec<_>>();
Ok(alpha_mask)
}
}

View file

@ -1,9 +1,9 @@
use crate::{
px, AnyView, AppContext, AvailableSpace, BorrowAppContext, Bounds, Context, Corners, Effect,
Element, EntityId, FontId, GlyphId, Handle, Hsla, IsZero, LayerId, LayoutId, MainThread,
MainThreadOnly, MonochromeSprite, Pixels, PlatformAtlas, PlatformWindow, Point, Reference,
RenderGlyphParams, ScaledPixels, Scene, Size, Style, TaffyLayoutEngine, WeakHandle,
WindowOptions, SUBPIXEL_VARIANTS,
px, AnyView, AppContext, AvailableSpace, BorrowAppContext, Bounds, Context, Corners,
DevicePixels, Effect, Element, EntityId, FontId, GlyphId, Handle, Hsla, IsZero, LayerId,
LayoutId, MainThread, MainThreadOnly, MonochromeSprite, Pixels, PlatformAtlas, PlatformWindow,
Point, Reference, RenderGlyphParams, RenderSvgParams, ScaledPixels, Scene, SharedString, Size,
Style, TaffyLayoutEngine, WeakHandle, WindowOptions, SUBPIXEL_VARIANTS,
};
use anyhow::Result;
use futures::Future;
@ -16,7 +16,7 @@ pub struct AnyWindow {}
pub struct Window {
handle: AnyWindowHandle,
platform_window: MainThreadOnly<Box<dyn PlatformWindow>>,
glyph_atlas: Arc<dyn PlatformAtlas>,
monochrome_sprite_atlas: Arc<dyn PlatformAtlas>,
rem_size: Pixels,
content_size: Size<Pixels>,
layout_engine: TaffyLayoutEngine,
@ -35,7 +35,7 @@ impl Window {
cx: &mut MainThread<AppContext>,
) -> Self {
let platform_window = cx.platform().open_window(handle, options);
let glyph_atlas = platform_window.glyph_atlas();
let monochrome_sprite_atlas = platform_window.monochrome_sprite_atlas();
let mouse_position = platform_window.mouse_position();
let content_size = platform_window.content_size();
let scale_factor = platform_window.scale_factor();
@ -58,7 +58,7 @@ impl Window {
Window {
handle,
platform_window,
glyph_atlas,
monochrome_sprite_atlas,
rem_size: px(16.),
content_size,
layout_engine: TaffyLayoutEngine::new(),
@ -235,7 +235,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
let layer_id = self.current_layer_id();
let tile = self
.window
.glyph_atlas
.monochrome_sprite_atlas
.get_or_insert_with(&params.clone().into(), &mut || {
self.text_system().rasterize_glyph(&params)
})?;
@ -259,6 +259,47 @@ impl<'a, 'w> WindowContext<'a, 'w> {
Ok(())
}
pub fn paint_svg(
&mut self,
bounds: Bounds<Pixels>,
order: u32,
path: SharedString,
color: Hsla,
) -> Result<()> {
let scale_factor = self.scale_factor();
let bounds = bounds.scale(scale_factor);
// Render the SVG at twice the size to get a higher quality result.
let params = RenderSvgParams {
path,
size: bounds
.size
.map(|pixels| DevicePixels::from((pixels.0 * 2.).ceil() as i32)),
};
let layer_id = self.current_layer_id();
let tile = self.window.monochrome_sprite_atlas.get_or_insert_with(
&params.clone().into(),
&mut || {
let bytes = self.svg_renderer.render(&params)?;
Ok((params.size, bytes))
},
)?;
let content_mask = self.content_mask().scale(scale_factor);
self.window.scene.insert(
layer_id,
MonochromeSprite {
order,
bounds,
content_mask,
color,
tile,
},
);
Ok(())
}
pub(crate) fn draw(&mut self) -> Result<()> {
let unit_entity = self.unit_entity.clone();
self.update_entity(&unit_entity, |_, cx| {

View file

@ -0,0 +1,30 @@
use std::borrow::Cow;
use anyhow::{anyhow, Result};
use gpui3::{AssetSource, SharedString};
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "../../assets"]
#[include = "fonts/**/*"]
#[include = "icons/**/*"]
#[include = "themes/**/*"]
#[include = "sounds/**/*"]
#[include = "*.md"]
#[exclude = "*.DS_Store"]
pub struct Assets;
impl AssetSource for Assets {
fn load(&self, path: &SharedString) -> Result<Cow<[u8]>> {
Self::get(path.as_ref())
.map(|f| f.data)
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
}
fn list(&self, path: &SharedString) -> Result<Vec<SharedString>> {
Ok(Self::iter()
.filter(|p| p.starts_with(path.as_ref()))
.map(SharedString::from)
.collect())
}
}

View file

@ -1,9 +1,13 @@
#![allow(dead_code, unused_variables)]
use assets::Assets;
use gpui3::{px, size, Bounds, WindowBounds, WindowOptions};
use log::LevelFilter;
use simplelog::SimpleLogger;
use std::sync::Arc;
use workspace::workspace;
mod assets;
mod collab_panel;
mod theme;
mod themes;
@ -19,7 +23,8 @@ fn main() {
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
gpui3::App::production().run(|cx| {
let asset_source = Arc::new(Assets);
gpui3::App::production(asset_source).run(|cx| {
let window = cx.open_window(
WindowOptions {
bounds: WindowBounds::Fixed(Bounds {
@ -35,29 +40,6 @@ fn main() {
});
}
use rust_embed::RustEmbed;
use workspace::workspace;
#[derive(RustEmbed)]
#[folder = "../../assets"]
#[include = "themes/**/*"]
#[include = "fonts/**/*"]
#[include = "icons/**/*"]
#[exclude = "*.DS_Store"]
pub struct Assets;
// impl AssetSource for Assets {
// fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
// Self::get(path)
// .map(|f| f.data)
// .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
// }
// fn list(&self, path: &str) -> Vec<std::borrow::Cow<'static, str>> {
// Self::iter().filter(|p| p.starts_with(path)).collect()
// }
// }
// fn load_embedded_fonts(platform: &dyn gpui2::Platform) {
// let font_paths = Assets.list("fonts");
// let mut embedded_fonts = Vec::new();

View file

@ -1,4 +1,5 @@
use std::{
borrow::Cow,
fmt::{self, Debug},
sync::Arc,
};
@ -47,6 +48,15 @@ impl From<String> for ArcCow<'_, str> {
}
}
impl<'a> From<Cow<'a, str>> for ArcCow<'a, str> {
fn from(value: Cow<'a, str>) -> Self {
match value {
Cow::Borrowed(borrowed) => Self::Borrowed(borrowed),
Cow::Owned(owned) => Self::Owned(owned.into()),
}
}
}
impl<'a, T: ?Sized + ToOwned> std::borrow::Borrow<T> for ArcCow<'a, T> {
fn borrow(&self) -> &T {
match self {