1use std::mem;
2use std::sync::Arc;
3
4use file_icons::FileIcons;
5use gpui::{
6 App, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render,
7 RenderImage, Styled, Subscription, Task, WeakEntity, Window, div, img,
8};
9use language::{Buffer, BufferEvent};
10use multi_buffer::MultiBuffer;
11use ui::prelude::*;
12use workspace::item::Item;
13use workspace::{Pane, Workspace};
14
15use crate::{OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide};
16
17pub struct SvgPreviewView {
18 focus_handle: FocusHandle,
19 buffer: Option<Entity<Buffer>>,
20 current_svg: Option<Result<Arc<RenderImage>, SharedString>>,
21 _refresh: Task<()>,
22 _buffer_subscription: Option<Subscription>,
23 _workspace_subscription: Option<Subscription>,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq)]
27pub enum SvgPreviewMode {
28 /// The preview will always show the contents of the provided editor.
29 Default,
30 /// The preview will "follow" the last active editor of an SVG file.
31 Follow,
32}
33
34impl SvgPreviewView {
35 pub fn new(
36 mode: SvgPreviewMode,
37 active_buffer: Entity<MultiBuffer>,
38 workspace_handle: WeakEntity<Workspace>,
39 window: &mut Window,
40 cx: &mut Context<Workspace>,
41 ) -> Entity<Self> {
42 cx.new(|cx| {
43 let workspace_subscription = if mode == SvgPreviewMode::Follow
44 && let Some(workspace) = workspace_handle.upgrade()
45 {
46 Some(Self::subscribe_to_workspace(workspace, window, cx))
47 } else {
48 None
49 };
50
51 let buffer = active_buffer.read_with(cx, |buffer, _cx| buffer.as_singleton());
52
53 let subscription = buffer
54 .as_ref()
55 .map(|buffer| Self::create_buffer_subscription(buffer, window, cx));
56
57 let mut this = Self {
58 focus_handle: cx.focus_handle(),
59 buffer,
60 current_svg: None,
61 _buffer_subscription: subscription,
62 _workspace_subscription: workspace_subscription,
63 _refresh: Task::ready(()),
64 };
65 this.render_image(window, cx);
66
67 this
68 })
69 }
70
71 fn subscribe_to_workspace(
72 workspace: Entity<Workspace>,
73 window: &Window,
74 cx: &mut Context<Self>,
75 ) -> Subscription {
76 cx.subscribe_in(
77 &workspace,
78 window,
79 move |this: &mut SvgPreviewView, workspace, event: &workspace::Event, window, cx| {
80 if let workspace::Event::ActiveItemChanged = event {
81 let workspace = workspace.read(cx);
82 if let Some(active_item) = workspace.active_item(cx)
83 && let Some(buffer) = active_item.downcast::<MultiBuffer>()
84 && Self::is_svg_file(&buffer, cx)
85 {
86 let Some(buffer) = buffer.read(cx).as_singleton() else {
87 return;
88 };
89 if this.buffer.as_ref() != Some(&buffer) {
90 this._buffer_subscription =
91 Some(Self::create_buffer_subscription(&buffer, window, cx));
92 this.buffer = Some(buffer);
93 this.render_image(window, cx);
94 cx.notify();
95 }
96 } else {
97 this.set_current(None, window, cx);
98 }
99 }
100 },
101 )
102 }
103
104 fn render_image(&mut self, window: &Window, cx: &mut Context<Self>) {
105 let Some(buffer) = self.buffer.as_ref() else {
106 return;
107 };
108 const SCALE_FACTOR: f32 = 1.0;
109
110 let renderer = cx.svg_renderer();
111 let content = buffer.read(cx).snapshot();
112 let background_task = cx.background_spawn(async move {
113 renderer.render_single_frame(content.text().as_bytes(), SCALE_FACTOR, true)
114 });
115
116 self._refresh = cx.spawn_in(window, async move |this, cx| {
117 let result = background_task.await;
118
119 this.update_in(cx, |view, window, cx| {
120 let current = result.map_err(|e| e.to_string().into());
121 view.set_current(Some(current), window, cx);
122 })
123 .ok();
124 });
125 }
126
127 fn set_current(
128 &mut self,
129 image: Option<Result<Arc<RenderImage>, SharedString>>,
130 window: &mut Window,
131 cx: &mut Context<Self>,
132 ) {
133 if let Some(Ok(image)) = mem::replace(&mut self.current_svg, image) {
134 window.drop_image(image).ok();
135 }
136 cx.notify();
137 }
138
139 fn find_existing_preview_item_idx(
140 pane: &Pane,
141 buffer: &Entity<MultiBuffer>,
142 cx: &App,
143 ) -> Option<usize> {
144 let buffer_id = buffer.entity_id();
145 pane.items_of_type::<SvgPreviewView>()
146 .find(|view| {
147 view.read(cx)
148 .buffer
149 .as_ref()
150 .is_some_and(|buffer| buffer.entity_id() == buffer_id)
151 })
152 .and_then(|view| pane.index_for_item(&view))
153 }
154
155 pub fn resolve_active_item_as_svg_buffer(
156 workspace: &Workspace,
157 cx: &mut Context<Workspace>,
158 ) -> Option<Entity<MultiBuffer>> {
159 workspace
160 .active_item(cx)?
161 .act_as::<MultiBuffer>(cx)
162 .filter(|buffer| Self::is_svg_file(&buffer, cx))
163 }
164
165 fn create_svg_view(
166 mode: SvgPreviewMode,
167 workspace: &mut Workspace,
168 buffer: Entity<MultiBuffer>,
169 window: &mut Window,
170 cx: &mut Context<Workspace>,
171 ) -> Entity<SvgPreviewView> {
172 let workspace_handle = workspace.weak_handle();
173 SvgPreviewView::new(mode, buffer, workspace_handle, window, cx)
174 }
175
176 fn create_buffer_subscription(
177 buffer: &Entity<Buffer>,
178 window: &Window,
179 cx: &mut Context<Self>,
180 ) -> Subscription {
181 cx.subscribe_in(
182 buffer,
183 window,
184 move |this, _buffer, event: &BufferEvent, window, cx| match event {
185 BufferEvent::Edited | BufferEvent::Saved => {
186 this.render_image(window, cx);
187 }
188 _ => {}
189 },
190 )
191 }
192
193 pub fn is_svg_file(buffer: &Entity<MultiBuffer>, cx: &App) -> bool {
194 buffer
195 .read(cx)
196 .as_singleton()
197 .and_then(|buffer| buffer.read(cx).file())
198 .is_some_and(|file| {
199 file.path()
200 .extension()
201 .is_some_and(|ext| ext.eq_ignore_ascii_case("svg"))
202 })
203 }
204
205 pub fn register(workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context<Workspace>) {
206 workspace.register_action(move |workspace, _: &OpenPreview, window, cx| {
207 if let Some(buffer) = Self::resolve_active_item_as_svg_buffer(workspace, cx)
208 && Self::is_svg_file(&buffer, cx)
209 {
210 let view = Self::create_svg_view(
211 SvgPreviewMode::Default,
212 workspace,
213 buffer.clone(),
214 window,
215 cx,
216 );
217 workspace.active_pane().update(cx, |pane, cx| {
218 if let Some(existing_view_idx) =
219 Self::find_existing_preview_item_idx(pane, &buffer, cx)
220 {
221 pane.activate_item(existing_view_idx, true, true, window, cx);
222 } else {
223 pane.add_item(Box::new(view), true, true, None, window, cx)
224 }
225 });
226 cx.notify();
227 }
228 });
229
230 workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, window, cx| {
231 if let Some(editor) = Self::resolve_active_item_as_svg_buffer(workspace, cx)
232 && Self::is_svg_file(&editor, cx)
233 {
234 let editor_clone = editor.clone();
235 let view = Self::create_svg_view(
236 SvgPreviewMode::Default,
237 workspace,
238 editor_clone,
239 window,
240 cx,
241 );
242 let pane = workspace
243 .find_pane_in_direction(workspace::SplitDirection::Right, cx)
244 .unwrap_or_else(|| {
245 workspace.split_pane(
246 workspace.active_pane().clone(),
247 workspace::SplitDirection::Right,
248 window,
249 cx,
250 )
251 });
252 pane.update(cx, |pane, cx| {
253 if let Some(existing_view_idx) =
254 Self::find_existing_preview_item_idx(pane, &editor, cx)
255 {
256 pane.activate_item(existing_view_idx, true, true, window, cx);
257 } else {
258 pane.add_item(Box::new(view), false, false, None, window, cx)
259 }
260 });
261 cx.notify();
262 }
263 });
264
265 workspace.register_action(move |workspace, _: &OpenFollowingPreview, window, cx| {
266 if let Some(editor) = Self::resolve_active_item_as_svg_buffer(workspace, cx)
267 && Self::is_svg_file(&editor, cx)
268 {
269 let view =
270 Self::create_svg_view(SvgPreviewMode::Follow, workspace, editor, window, cx);
271 workspace.active_pane().update(cx, |pane, cx| {
272 pane.add_item(Box::new(view), true, true, None, window, cx)
273 });
274 cx.notify();
275 }
276 });
277 }
278}
279
280impl Render for SvgPreviewView {
281 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
282 v_flex()
283 .id("SvgPreview")
284 .key_context("SvgPreview")
285 .track_focus(&self.focus_handle(cx))
286 .size_full()
287 .bg(cx.theme().colors().editor_background)
288 .flex()
289 .justify_center()
290 .items_center()
291 .map(|this| match self.current_svg.clone() {
292 Some(Ok(image)) => {
293 this.child(img(image).max_w_full().max_h_full().with_fallback(|| {
294 h_flex()
295 .p_4()
296 .gap_2()
297 .child(Icon::new(IconName::Warning))
298 .child("Failed to load SVG image")
299 .into_any_element()
300 }))
301 }
302 Some(Err(e)) => this.child(div().p_4().child(e).into_any_element()),
303 None => this.child(div().p_4().child("No SVG file selected")),
304 })
305 }
306}
307
308impl Focusable for SvgPreviewView {
309 fn focus_handle(&self, _cx: &App) -> FocusHandle {
310 self.focus_handle.clone()
311 }
312}
313
314impl EventEmitter<()> for SvgPreviewView {}
315
316impl Item for SvgPreviewView {
317 type Event = ();
318
319 fn tab_icon(&self, _window: &Window, cx: &App) -> Option<Icon> {
320 self.buffer
321 .as_ref()
322 .and_then(|buffer| buffer.read(cx).file())
323 .and_then(|file| FileIcons::get_icon(file.path().as_std_path(), cx))
324 .map(Icon::from_path)
325 .or_else(|| Some(Icon::new(IconName::Image)))
326 }
327
328 fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
329 self.buffer
330 .as_ref()
331 .and_then(|svg_path| svg_path.read(cx).file())
332 .map(|name| format!("Preview {}", name.file_name(cx)).into())
333 .unwrap_or_else(|| "SVG Preview".into())
334 }
335
336 fn telemetry_event_text(&self) -> Option<&'static str> {
337 Some("svg preview: open")
338 }
339
340 fn to_item_events(_event: &Self::Event, _f: impl FnMut(workspace::item::ItemEvent)) {}
341}