From 9e5438906a0db63fcd155d21d251381f9f3f21ca Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 29 Oct 2025 18:49:39 +0100 Subject: [PATCH] svg_preview: Update preview on every buffer edit (#41270) 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(-) diff --git a/Cargo.lock b/Cargo.lock index 23119d25344261d134cc583a7fde51cbfd8a2c5a..1e1ccad1a7dcb3257c9cf471c166655c23056a07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16495,7 +16495,7 @@ dependencies = [ "editor", "file_icons", "gpui", - "multi_buffer", + "language", "ui", "workspace", ] diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 075c7cf32beb400d800ca4f8970f51dae6da7afe..fcba6a6a4e5b3d82262129bc9f7d9bdc72c88da9 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -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) + } } } } diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 805dbbdfe740acbac4929170953e70c923403bb7..2e391b6e442126a74884046a5058976c0495abfd 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -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"))] diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index c2bb1b3c6458efeceda46020c8270a306a2117d9..20a135df51cc935ce725f88e3978abb9f3fc07c9 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -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); } }; diff --git a/crates/gpui/src/svg_renderer.rs b/crates/gpui/src/svg_renderer.rs index b2bf126967cd0c533eb6faac8c168508fe5c1d34..1e2e34897af0b550542f9af148bb7c19f8f8ed18 100644 --- a/crates/gpui/src/svg_renderer.rs +++ b/crates/gpui/src/svg_renderer.rs @@ -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, usvg_options: Arc>, } +/// The size in which to render the SVG. pub enum SvgSize { + /// An absolute size in device pixels. Size(Size), + /// 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) -> Self { static FONT_DB: LazyLock> = 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, 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, Vec)>> { @@ -80,7 +118,7 @@ impl SvgRenderer { Ok(Some((size, alpha_mask))) } - pub fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result { + fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result { let tree = usvg::Tree::from_data(bytes, &self.usvg_options)?; let svg_size = tree.size(); let scale = match size { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 80f6fdf9f537a3147455fae0dc8d9a5bab0ebdb6..c44b0d642a2970dfb803109591d8dc0e2c6cacc6 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -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)))) diff --git a/crates/svg_preview/Cargo.toml b/crates/svg_preview/Cargo.toml index f64e60afe282da0da6780cc45097c751a8e7e8c1..e78a042180a62d31fd74da659df7afe8baa2caa7 100644 --- a/crates/svg_preview/Cargo.toml +++ b/crates/svg_preview/Cargo.toml @@ -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 diff --git a/crates/svg_preview/src/svg_preview_view.rs b/crates/svg_preview/src/svg_preview_view.rs index 432e91648cf751744168e89823fc59bc168c6714..de27a7237d5f85b8dc18d47e09a6ac7fe22ee89f 100644 --- a/crates/svg_preview/src/svg_preview_view.rs +++ b/crates/svg_preview/src/svg_preview_view.rs @@ -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, - image_cache: Entity, - _buffer_subscription: Subscription, + buffer: Option>, + current_svg: Option, SharedString>>, + _refresh: Task<()>, + _buffer_subscription: Option, _workspace_subscription: Option, } @@ -31,6 +32,182 @@ pub enum SvgPreviewMode { } impl SvgPreviewView { + pub fn new( + mode: SvgPreviewMode, + active_editor: Entity, + workspace_handle: WeakEntity, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + 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, + window: &Window, + cx: &mut Context, + ) -> 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::() + && 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) { + 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, SharedString>>, + window: &mut Window, + cx: &mut Context, + ) { + 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, + cx: &App, + ) -> Option { + let buffer_id = editor.read(cx).buffer().entity_id(); + pane.items_of_type::() + .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, + ) -> Option> { + workspace + .active_item(cx)? + .act_as::(cx) + .filter(|editor| Self::is_svg_file(&editor, cx)) + } + + fn create_svg_view( + mode: SvgPreviewMode, + workspace: &mut Workspace, + editor: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + let workspace_handle = workspace.weak_handle(); + SvgPreviewView::new(mode, editor, workspace_handle, window, cx) + } + + fn create_buffer_subscription( + buffer: &Entity, + window: &Window, + cx: &mut Context, + ) -> 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, 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.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, - cx: &App, - ) -> Option { - let editor_path = Self::get_svg_path(editor.read(cx).buffer(), cx); - pane.items_of_type::() - .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, - ) -> Option> { - let editor = workspace.active_item(cx)?.act_as::(cx)?; - - if Self::is_svg_file(&editor, cx) { - Some(editor) - } else { - None - } - } - - fn create_svg_view( - mode: SvgPreviewMode, - workspace: &mut Workspace, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let workspace_handle = workspace.weak_handle(); - SvgPreviewView::new(mode, editor, workspace_handle, window, cx) - } - - pub fn new( - mode: SvgPreviewMode, - active_editor: Entity, - workspace_handle: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - 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::() - && 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, - window: &mut Window, - cx: &mut Context, - ) -> 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, 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, cx: &App) -> Option { - 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 { - // 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()) } diff --git a/crates/zlog/src/filter.rs b/crates/zlog/src/filter.rs index 9a2de13cb3d33a1a6f4d17f7eddd4754cae40ea3..e2ca04be60f4fe7eba7cdb2fc9eb983092d2331a 100644 --- a/crates/zlog/src/filter.rs +++ b/crates/zlog/src/filter.rs @@ -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) {