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