diff --git a/Cargo.lock b/Cargo.lock index 981f59cb5eae41..858bbbcbac0998 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8592,9 +8592,9 @@ dependencies = [ [[package]] name = "jupyter-protocol" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c047f6b5e551563af2ddb13dafed833f0ec5a5b0f9621d5ad740a9ff1e1095" +checksum = "5be6360379f5a234046a4dea91a47324440a80f10aa0b440846d79486ed7ebc4" dependencies = [ "async-trait", "bytes 1.10.1", diff --git a/crates/repl/src/notebook/cell.rs b/crates/repl/src/notebook/cell.rs index 87b8e1d55ae85e..e90fd1b2053805 100644 --- a/crates/repl/src/notebook/cell.rs +++ b/crates/repl/src/notebook/cell.rs @@ -636,6 +636,9 @@ impl Render for CodeCell { Output::Image { content, .. } => { Some(content.clone().into_any_element()) } + Output::Svg { content, .. } => { + Some(content.clone().into_any_element()) + } Output::Message(message) => Some( div().child(message.clone()).into_any_element(), ), diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index b99562393a2bba..8168950b2c32b8 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -45,6 +45,9 @@ use ui::{ mod image; use image::ImageView; +mod svg; +use svg::SvgView; + mod markdown; use markdown::MarkdownView; @@ -62,6 +65,7 @@ use workspace::Workspace; fn rank_mime_type(mimetype: &MimeType) -> usize { match mimetype { MimeType::DataTable(_) => 6, + MimeType::Svg(_) => 5, MimeType::Png(_) => 4, MimeType::Jpeg(_) => 3, MimeType::Markdown(_) => 2, @@ -114,6 +118,10 @@ pub enum Output { content: Entity, display_id: Option, }, + Svg { + content: Entity, + display_id: Option, + }, ErrorOutput(ErrorView), Message(String), Table { @@ -211,6 +219,7 @@ impl Output { Self::Markdown { content, .. } => Some(content.clone().into_any_element()), Self::Stream { content, .. } => Some(content.clone().into_any_element()), Self::Image { content, .. } => Some(content.clone().into_any_element()), + Self::Svg { content, .. } => Some(content.clone().into_any_element()), Self::Message(message) => Some(div().child(message.clone()).into_any_element()), Self::Table { content, .. } => Some(content.clone().into_any_element()), Self::ErrorOutput(error_view) => error_view.render(window, cx), @@ -236,6 +245,9 @@ impl Output { Self::Image { content, .. } => { Self::render_output_controls(content.clone(), workspace, window, cx) } + Self::Svg { content, .. } => { + Self::render_output_controls(content.clone(), workspace, window, cx) + } Self::ErrorOutput(err) => { // Add buttons for the traceback section Some( @@ -332,6 +344,7 @@ impl Output { Output::Plain { display_id, .. } => display_id.clone(), Output::Stream { .. } => None, Output::Image { display_id, .. } => display_id.clone(), + Output::Svg { display_id, .. } => display_id.clone(), Output::ErrorOutput(_) => None, Output::Message(_) => None, Output::Table { display_id, .. } => display_id.clone(), @@ -365,6 +378,13 @@ impl Output { }, Err(error) => Output::Message(format!("Failed to load image: {}", error)), }, + Some(MimeType::Svg(data)) => match SvgView::from(data, cx) { + Ok(view) => Output::Svg { + content: cx.new(|_| view), + display_id, + }, + Err(error) => Output::Message(format!("Failed to load SVG: {}", error)), + }, Some(MimeType::DataTable(data)) => Output::Table { content: cx.new(|cx| TableView::new(data, window, cx)), display_id, diff --git a/crates/repl/src/outputs/svg.rs b/crates/repl/src/outputs/svg.rs new file mode 100644 index 00000000000000..8dce40e35776a9 --- /dev/null +++ b/crates/repl/src/outputs/svg.rs @@ -0,0 +1,78 @@ +use anyhow::{Context as _, Result}; +use gpui::{App, ClipboardItem, Pixels, RenderImage, Window, img, px}; +use std::sync::Arc; +use ui::{IntoElement, Styled, div, prelude::*}; + +use crate::outputs::OutputContent; + +const SVG_SCALE_FACTOR: f32 = 2.0; + +pub struct SvgView { + raw_svg: String, + width: Pixels, + height: Pixels, + image: Arc, +} + +impl SvgView { + pub fn from(svg_data: &str, cx: &App) -> Result { + let renderer = cx.svg_renderer(); + let image = renderer + .render_single_frame(svg_data.as_bytes(), 1.0, true) + .context("rendering SVG")?; + + let size = image.size(0); + let width = px(size.width.0 as f32 / SVG_SCALE_FACTOR); + let height = px(size.height.0 as f32 / SVG_SCALE_FACTOR); + + Ok(Self { + raw_svg: svg_data.to_string(), + width, + height, + image, + }) + } +} + +impl Render for SvgView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .h(self.height) + .w(self.width) + .child(img(self.image.clone())) + } +} + +impl OutputContent for SvgView { + fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option { + Some(ClipboardItem::new_string(self.raw_svg.clone())) + } + + fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool { + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const SIMPLE_SVG: &str = r#""#; + + #[gpui::test] + fn test_valid_svg(cx: &mut App) { + let result = SvgView::from(SIMPLE_SVG, cx); + assert!(result.is_ok()); + + let view = result.unwrap(); + assert_eq!(view.raw_svg, SIMPLE_SVG); + assert!(view.width > Pixels::ZERO); + assert!(view.height > Pixels::ZERO); + } + + #[gpui::test] + fn test_invalid_svg(cx: &mut App) { + let result = SvgView::from("not valid svg content", cx); + assert!(result.is_err()); + } +}