Cargo.lock 🔗
@@ -16495,7 +16495,7 @@ dependencies = [
"editor",
"file_icons",
"gpui",
- "multi_buffer",
+ "language",
"ui",
"workspace",
]
Finn Evers created
Closes https://github.com/zed-industries/zed/issues/39104
This fixes an issue where the preview would not work for remote buffers
in the process.
Release Notes:
- Fixed an issue where the SVG preview would not work in remote
scenarios.
- The SVG preview will now rerender on every keypress instead of only on
saves.
Cargo.lock | 2
crates/gpui/src/elements/img.rs | 36 -
crates/gpui/src/gpui.rs | 2
crates/gpui/src/platform.rs | 12
crates/gpui/src/svg_renderer.rs | 44 ++
crates/gpui/src/window.rs | 2
crates/svg_preview/Cargo.toml | 2
crates/svg_preview/src/svg_preview_view.rs | 379 ++++++++++++-----------
crates/zlog/src/filter.rs | 3
9 files changed, 267 insertions(+), 215 deletions(-)
@@ -16495,7 +16495,7 @@ dependencies = [
"editor",
"file_icons",
"gpui",
- "multi_buffer",
+ "language",
"ui",
"workspace",
]
@@ -2,14 +2,13 @@ use crate::{
AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength, Element, ElementId,
Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId, InteractiveElement,
Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
- SMOOTH_SVG_SCALE_FACTOR, SharedString, SharedUri, StyleRefinement, Styled, SvgSize, Task,
- Window, px, swap_rgba_pa_to_bgra,
+ SharedString, SharedUri, StyleRefinement, Styled, Task, Window, px,
};
use anyhow::{Context as _, Result};
use futures::{AsyncReadExt, Future};
use image::{
- AnimationDecoder, DynamicImage, Frame, ImageBuffer, ImageError, ImageFormat, Rgba,
+ AnimationDecoder, DynamicImage, Frame, ImageError, ImageFormat, Rgba,
codecs::{gif::GifDecoder, webp::WebPDecoder},
};
use smallvec::SmallVec;
@@ -160,13 +159,15 @@ pub trait StyledImage: Sized {
self
}
- /// Set the object fit for the image.
+ /// Set a fallback function that will be invoked to render an error view should
+ /// the image fail to load.
fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
self.image_style().fallback = Some(Box::new(fallback));
self
}
- /// Set the object fit for the image.
+ /// Set a fallback function that will be invoked to render a view while the image
+ /// is still being loaded.
fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self {
self.image_style().loading = Some(Box::new(loading));
self
@@ -631,7 +632,7 @@ impl Asset for ImageAssetLoader {
}
};
- let data = if let Ok(format) = image::guess_format(&bytes) {
+ if let Ok(format) = image::guess_format(&bytes) {
let data = match format {
ImageFormat::Gif => {
let decoder = GifDecoder::new(Cursor::new(&bytes))?;
@@ -689,25 +690,12 @@ impl Asset for ImageAssetLoader {
}
};
- RenderImage::new(data)
+ Ok(Arc::new(RenderImage::new(data)))
} else {
- let pixmap =
- // TODO: Can we make svgs always rescale?
- svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(SMOOTH_SVG_SCALE_FACTOR))?;
-
- let mut buffer =
- ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
-
- for pixel in buffer.chunks_exact_mut(4) {
- swap_rgba_pa_to_bgra(pixel);
- }
-
- let mut image = RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1));
- image.scale_factor = SMOOTH_SVG_SCALE_FACTOR;
- image
- };
-
- Ok(Arc::new(data))
+ svg_renderer
+ .render_single_frame(&bytes, 1.0, true)
+ .map_err(Into::into)
+ }
}
}
}
@@ -95,7 +95,7 @@ pub use smol::Timer;
pub use style::*;
pub use styled::*;
pub use subscription::*;
-use svg_renderer::*;
+pub use svg_renderer::*;
pub(crate) use tab_stop::*;
pub use taffy::{AvailableSpace, LayoutId};
#[cfg(any(test, feature = "test-support"))]
@@ -40,7 +40,7 @@ use crate::{
DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun,
ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput,
Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene, ShapedGlyph,
- ShapedRun, SharedString, Size, SvgRenderer, SvgSize, SystemWindowTab, Task, TaskLabel, Window,
+ ShapedRun, SharedString, Size, SvgRenderer, SystemWindowTab, Task, TaskLabel, Window,
WindowControlArea, hash, point, px, size,
};
use anyhow::Result;
@@ -1825,13 +1825,9 @@ impl Image {
ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?,
ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?,
ImageFormat::Svg => {
- let pixmap = svg_renderer.render_pixmap(&self.bytes, SvgSize::ScaleFactor(1.0))?;
-
- let buffer =
- image::ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take())
- .unwrap();
-
- SmallVec::from_elem(Frame::new(buffer), 1)
+ return svg_renderer
+ .render_single_frame(&self.bytes, 1.0, false)
+ .map_err(Into::into);
}
};
@@ -1,5 +1,10 @@
-use crate::{AssetSource, DevicePixels, IsZero, Result, SharedString, Size};
+use crate::{
+ AssetSource, DevicePixels, IsZero, RenderImage, Result, SharedString, Size,
+ swap_rgba_pa_to_bgra,
+};
+use image::Frame;
use resvg::tiny_skia::Pixmap;
+use smallvec::SmallVec;
use std::{
hash::Hash,
sync::{Arc, LazyLock},
@@ -15,17 +20,22 @@ pub(crate) struct RenderSvgParams {
}
#[derive(Clone)]
+/// A struct holding everything necessary to render SVGs.
pub struct SvgRenderer {
asset_source: Arc<dyn AssetSource>,
usvg_options: Arc<usvg::Options<'static>>,
}
+/// The size in which to render the SVG.
pub enum SvgSize {
+ /// An absolute size in device pixels.
Size(Size<DevicePixels>),
+ /// A scaling factor to apply to the size provided by the SVG.
ScaleFactor(f32),
}
impl SvgRenderer {
+ /// Creates a new SVG renderer with the provided asset source.
pub fn new(asset_source: Arc<dyn AssetSource>) -> Self {
static FONT_DB: LazyLock<Arc<usvg::fontdb::Database>> = LazyLock::new(|| {
let mut db = usvg::fontdb::Database::new();
@@ -54,7 +64,35 @@ impl SvgRenderer {
}
}
- pub(crate) fn render(
+ /// Renders the given bytes into an image buffer.
+ pub fn render_single_frame(
+ &self,
+ bytes: &[u8],
+ scale_factor: f32,
+ to_brga: bool,
+ ) -> Result<Arc<RenderImage>, usvg::Error> {
+ self.render_pixmap(
+ bytes,
+ SvgSize::ScaleFactor(scale_factor * SMOOTH_SVG_SCALE_FACTOR),
+ )
+ .map(|pixmap| {
+ let mut buffer =
+ image::ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take())
+ .unwrap();
+
+ if to_brga {
+ for pixel in buffer.chunks_exact_mut(4) {
+ swap_rgba_pa_to_bgra(pixel);
+ }
+ }
+
+ let mut image = RenderImage::new(SmallVec::from_const([Frame::new(buffer)]));
+ image.scale_factor = SMOOTH_SVG_SCALE_FACTOR;
+ Arc::new(image)
+ })
+ }
+
+ pub(crate) fn render_alpha_mask(
&self,
params: &RenderSvgParams,
) -> Result<Option<(Size<DevicePixels>, Vec<u8>)>> {
@@ -80,7 +118,7 @@ impl SvgRenderer {
Ok(Some((size, alpha_mask)))
}
- pub fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, usvg::Error> {
+ fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, usvg::Error> {
let tree = usvg::Tree::from_data(bytes, &self.usvg_options)?;
let svg_size = tree.size();
let scale = match size {
@@ -3104,7 +3104,7 @@ impl Window {
let Some(tile) =
self.sprite_atlas
.get_or_insert_with(¶ms.clone().into(), &mut || {
- let Some((size, bytes)) = cx.svg_renderer.render(¶ms)? else {
+ let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(¶ms)? else {
return Ok(None);
};
Ok(Some((size, Cow::Owned(bytes))))
@@ -15,6 +15,6 @@ path = "src/svg_preview.rs"
editor.workspace = true
file_icons.workspace = true
gpui.workspace = true
-multi_buffer.workspace = true
+language.workspace = true
ui.workspace = true
workspace.workspace = true
@@ -1,13 +1,13 @@
-use std::path::PathBuf;
+use std::mem;
+use std::sync::Arc;
use editor::Editor;
use file_icons::FileIcons;
use gpui::{
- App, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageSource, IntoElement,
- ParentElement, Render, Resource, RetainAllImageCache, Styled, Subscription, WeakEntity, Window,
- div, img,
+ App, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render,
+ RenderImage, Styled, Subscription, Task, WeakEntity, Window, div, img,
};
-use multi_buffer::{Event as MultiBufferEvent, MultiBuffer};
+use language::{Buffer, BufferEvent};
use ui::prelude::*;
use workspace::item::Item;
use workspace::{Pane, Workspace};
@@ -16,9 +16,10 @@ use crate::{OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide};
pub struct SvgPreviewView {
focus_handle: FocusHandle,
- svg_path: Option<PathBuf>,
- image_cache: Entity<RetainAllImageCache>,
- _buffer_subscription: Subscription,
+ buffer: Option<Entity<Buffer>>,
+ current_svg: Option<Result<Arc<RenderImage>, SharedString>>,
+ _refresh: Task<()>,
+ _buffer_subscription: Option<Subscription>,
_workspace_subscription: Option<Subscription>,
}
@@ -31,6 +32,182 @@ pub enum SvgPreviewMode {
}
impl SvgPreviewView {
+ pub fn new(
+ mode: SvgPreviewMode,
+ active_editor: Entity<Editor>,
+ workspace_handle: WeakEntity<Workspace>,
+ window: &mut Window,
+ cx: &mut Context<Workspace>,
+ ) -> Entity<Self> {
+ cx.new(|cx| {
+ let workspace_subscription = if mode == SvgPreviewMode::Follow
+ && let Some(workspace) = workspace_handle.upgrade()
+ {
+ Some(Self::subscribe_to_workspace(workspace, window, cx))
+ } else {
+ None
+ };
+
+ let buffer = active_editor
+ .read(cx)
+ .buffer()
+ .clone()
+ .read_with(cx, |buffer, _cx| buffer.as_singleton());
+
+ let subscription = buffer
+ .as_ref()
+ .map(|buffer| Self::create_buffer_subscription(buffer, window, cx));
+
+ let mut this = Self {
+ focus_handle: cx.focus_handle(),
+ buffer,
+ current_svg: None,
+ _buffer_subscription: subscription,
+ _workspace_subscription: workspace_subscription,
+ _refresh: Task::ready(()),
+ };
+ this.render_image(window, cx);
+
+ this
+ })
+ }
+
+ fn subscribe_to_workspace(
+ workspace: Entity<Workspace>,
+ window: &Window,
+ cx: &mut Context<Self>,
+ ) -> Subscription {
+ cx.subscribe_in(
+ &workspace,
+ window,
+ move |this: &mut SvgPreviewView, workspace, event: &workspace::Event, window, cx| {
+ if let workspace::Event::ActiveItemChanged = event {
+ let workspace = workspace.read(cx);
+ if let Some(active_item) = workspace.active_item(cx)
+ && let Some(editor) = active_item.downcast::<Editor>()
+ && Self::is_svg_file(&editor, cx)
+ {
+ let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {
+ return;
+ };
+ if this.buffer.as_ref() != Some(&buffer) {
+ this._buffer_subscription =
+ Some(Self::create_buffer_subscription(&buffer, window, cx));
+ this.buffer = Some(buffer);
+ this.render_image(window, cx);
+ cx.notify();
+ }
+ } else {
+ this.set_current(None, window, cx);
+ }
+ }
+ },
+ )
+ }
+
+ fn render_image(&mut self, window: &Window, cx: &mut Context<Self>) {
+ let Some(buffer) = self.buffer.as_ref() else {
+ return;
+ };
+ const SCALE_FACTOR: f32 = 1.0;
+
+ let renderer = cx.svg_renderer();
+ let content = buffer.read(cx).snapshot();
+ let background_task = cx.background_spawn(async move {
+ renderer.render_single_frame(content.text().as_bytes(), SCALE_FACTOR, true)
+ });
+
+ self._refresh = cx.spawn_in(window, async move |this, cx| {
+ let result = background_task.await;
+
+ this.update_in(cx, |view, window, cx| {
+ let current = result.map_err(|e| e.to_string().into());
+ view.set_current(Some(current), window, cx);
+ })
+ .ok();
+ });
+ }
+
+ fn set_current(
+ &mut self,
+ image: Option<Result<Arc<RenderImage>, SharedString>>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if let Some(Ok(image)) = mem::replace(&mut self.current_svg, image) {
+ window.drop_image(image).ok();
+ }
+ cx.notify();
+ }
+
+ fn find_existing_preview_item_idx(
+ pane: &Pane,
+ editor: &Entity<Editor>,
+ cx: &App,
+ ) -> Option<usize> {
+ let buffer_id = editor.read(cx).buffer().entity_id();
+ pane.items_of_type::<SvgPreviewView>()
+ .find(|view| {
+ view.read(cx)
+ .buffer
+ .as_ref()
+ .is_some_and(|buffer| buffer.entity_id() == buffer_id)
+ })
+ .and_then(|view| pane.index_for_item(&view))
+ }
+
+ pub fn resolve_active_item_as_svg_editor(
+ workspace: &Workspace,
+ cx: &mut Context<Workspace>,
+ ) -> Option<Entity<Editor>> {
+ workspace
+ .active_item(cx)?
+ .act_as::<Editor>(cx)
+ .filter(|editor| Self::is_svg_file(&editor, cx))
+ }
+
+ fn create_svg_view(
+ mode: SvgPreviewMode,
+ workspace: &mut Workspace,
+ editor: Entity<Editor>,
+ window: &mut Window,
+ cx: &mut Context<Workspace>,
+ ) -> Entity<SvgPreviewView> {
+ let workspace_handle = workspace.weak_handle();
+ SvgPreviewView::new(mode, editor, workspace_handle, window, cx)
+ }
+
+ fn create_buffer_subscription(
+ buffer: &Entity<Buffer>,
+ window: &Window,
+ cx: &mut Context<Self>,
+ ) -> Subscription {
+ cx.subscribe_in(
+ buffer,
+ window,
+ move |this, _buffer, event: &BufferEvent, window, cx| match event {
+ BufferEvent::Edited | BufferEvent::Saved => {
+ this.render_image(window, cx);
+ }
+ _ => {}
+ },
+ )
+ }
+
+ pub fn is_svg_file(editor: &Entity<Editor>, cx: &App) -> bool {
+ editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .as_singleton()
+ .and_then(|buffer| buffer.read(cx).file())
+ .is_some_and(|file| {
+ file.path()
+ .extension()
+ .is_some_and(|ext| ext.eq_ignore_ascii_case("svg"))
+ })
+ }
+
pub fn register(workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context<Workspace>) {
workspace.register_action(move |workspace, _: &OpenPreview, window, cx| {
if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx)
@@ -104,154 +281,6 @@ impl SvgPreviewView {
}
});
}
-
- fn find_existing_preview_item_idx(
- pane: &Pane,
- editor: &Entity<Editor>,
- cx: &App,
- ) -> Option<usize> {
- let editor_path = Self::get_svg_path(editor.read(cx).buffer(), cx);
- pane.items_of_type::<SvgPreviewView>()
- .find(|view| {
- let view_read = view.read(cx);
- view_read.svg_path.is_some() && view_read.svg_path == editor_path
- })
- .and_then(|view| pane.index_for_item(&view))
- }
-
- pub fn resolve_active_item_as_svg_editor(
- workspace: &Workspace,
- cx: &mut Context<Workspace>,
- ) -> Option<Entity<Editor>> {
- let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
-
- if Self::is_svg_file(&editor, cx) {
- Some(editor)
- } else {
- None
- }
- }
-
- fn create_svg_view(
- mode: SvgPreviewMode,
- workspace: &mut Workspace,
- editor: Entity<Editor>,
- window: &mut Window,
- cx: &mut Context<Workspace>,
- ) -> Entity<SvgPreviewView> {
- let workspace_handle = workspace.weak_handle();
- SvgPreviewView::new(mode, editor, workspace_handle, window, cx)
- }
-
- pub fn new(
- mode: SvgPreviewMode,
- active_editor: Entity<Editor>,
- workspace_handle: WeakEntity<Workspace>,
- window: &mut Window,
- cx: &mut Context<Workspace>,
- ) -> Entity<Self> {
- cx.new(|cx| {
- let image_cache = RetainAllImageCache::new(cx);
- let buffer = active_editor.read(cx).buffer();
- let svg_path = Self::get_svg_path(buffer, cx);
- let subscription = Self::create_buffer_subscription(&buffer.clone(), window, cx);
-
- // Subscribe to workspace active item changes to follow SVG files
- let workspace_subscription = if mode == SvgPreviewMode::Follow {
- workspace_handle.upgrade().map(|workspace_handle| {
- cx.subscribe_in(
- &workspace_handle,
- window,
- |this: &mut SvgPreviewView,
- workspace,
- event: &workspace::Event,
- window,
- cx| {
- if let workspace::Event::ActiveItemChanged = event {
- let workspace_read = workspace.read(cx);
- if let Some(active_item) = workspace_read.active_item(cx)
- && let Some(editor) = active_item.downcast::<Editor>()
- && Self::is_svg_file(&editor, cx)
- {
- let buffer = editor.read(cx).buffer();
- let new_path = Self::get_svg_path(&buffer, cx);
- if this.svg_path != new_path {
- this.svg_path = new_path;
- this._buffer_subscription =
- Self::create_buffer_subscription(
- &buffer.clone(),
- window,
- cx,
- );
- cx.notify();
- }
- }
- }
- },
- )
- })
- } else {
- None
- };
-
- Self {
- focus_handle: cx.focus_handle(),
- svg_path,
- image_cache,
- _buffer_subscription: subscription,
- _workspace_subscription: workspace_subscription,
- }
- })
- }
-
- fn create_buffer_subscription(
- active_buffer: &Entity<MultiBuffer>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Subscription {
- cx.subscribe_in(
- active_buffer,
- window,
- |this: &mut SvgPreviewView, buffer, event: &MultiBufferEvent, window, cx| {
- let potential_path_change = event == &MultiBufferEvent::FileHandleChanged;
- if event == &MultiBufferEvent::Saved || potential_path_change {
- // Remove cached image to force reload
- if let Some(svg_path) = &this.svg_path {
- let resource = Resource::Path(svg_path.clone().into());
- this.image_cache.update(cx, |cache, cx| {
- cache.remove(&resource, window, cx);
- });
- }
-
- if potential_path_change {
- this.svg_path = Self::get_svg_path(buffer, cx);
- }
- cx.notify();
- }
- },
- )
- }
-
- pub fn is_svg_file(editor: &Entity<Editor>, cx: &App) -> bool {
- let buffer = editor.read(cx).buffer().read(cx);
- if let Some(buffer) = buffer.as_singleton()
- && let Some(file) = buffer.read(cx).file()
- {
- return file
- .path()
- .extension()
- .map(|ext| ext.eq_ignore_ascii_case("svg"))
- .unwrap_or(false);
- }
- false
- }
-
- fn get_svg_path(buffer: &Entity<MultiBuffer>, cx: &App) -> Option<PathBuf> {
- let buffer = buffer.read(cx).as_singleton()?;
- let file = buffer.read(cx).file()?;
- let local_file = file.as_local()?;
- Some(local_file.abs_path(cx))
- }
}
impl Render for SvgPreviewView {
@@ -265,20 +294,19 @@ impl Render for SvgPreviewView {
.flex()
.justify_center()
.items_center()
- .child(if let Some(svg_path) = &self.svg_path {
- img(ImageSource::from(svg_path.clone()))
- .image_cache(&self.image_cache)
- .max_w_full()
- .max_h_full()
- .with_fallback(|| {
- div()
+ .map(|this| match self.current_svg.clone() {
+ Some(Ok(image)) => {
+ this.child(img(image).max_w_full().max_h_full().with_fallback(|| {
+ h_flex()
.p_4()
- .child("Failed to load SVG file")
+ .gap_2()
+ .child(Icon::new(IconName::Warning))
+ .child("Failed to load SVG image")
.into_any_element()
- })
- .into_any_element()
- } else {
- div().p_4().child("No SVG file selected").into_any_element()
+ }))
+ }
+ Some(Err(e)) => this.child(div().p_4().child(e).into_any_element()),
+ None => this.child(div().p_4().child("No SVG file selected")),
})
}
}
@@ -295,20 +323,19 @@ impl Item for SvgPreviewView {
type Event = ();
fn tab_icon(&self, _window: &Window, cx: &App) -> Option<Icon> {
- // Use the same icon as SVG files in the file tree
- self.svg_path
+ self.buffer
.as_ref()
- .and_then(|svg_path| FileIcons::get_icon(svg_path, cx))
+ .and_then(|buffer| buffer.read(cx).file())
+ .and_then(|file| FileIcons::get_icon(file.path().as_std_path(), cx))
.map(Icon::from_path)
.or_else(|| Some(Icon::new(IconName::Image)))
}
- fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
- self.svg_path
+ fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
+ self.buffer
.as_ref()
- .and_then(|svg_path| svg_path.file_name())
- .map(|name| name.to_string_lossy())
- .map(|name| format!("Preview {}", name).into())
+ .and_then(|svg_path| svg_path.read(cx).file())
+ .map(|name| format!("Preview {}", name.file_name(cx)).into())
.unwrap_or_else(|| "SVG Preview".into())
}
@@ -41,6 +41,9 @@ const DEFAULT_FILTERS: &[(&str, log::LevelFilter)] = &[
("blade_graphics", log::LevelFilter::Warn),
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))]
("naga::back::spv::writer", log::LevelFilter::Warn),
+ // usvg prints a lot of warnings on rendering an SVG with partial errors, which
+ // can happen a lot with the SVG preview
+ ("usvg::parser::style", log::LevelFilter::Error),
];
pub fn init_env_filter(filter: env_config::EnvFilter) {