svg_preview_view.rs

  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}