svg_preview_view.rs

  1use std::path::PathBuf;
  2
  3use editor::{Editor, EditorEvent};
  4use file_icons::FileIcons;
  5use gpui::{
  6    App, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageSource, IntoElement,
  7    ParentElement, Render, Resource, RetainAllImageCache, Styled, Subscription, WeakEntity, Window,
  8    div, img,
  9};
 10use project::ProjectPath;
 11use ui::prelude::*;
 12use workspace::item::Item;
 13use workspace::{Pane, Workspace};
 14use worktree::Event as WorktreeEvent;
 15
 16use crate::{OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide};
 17
 18pub struct SvgPreviewView {
 19    focus_handle: FocusHandle,
 20    svg_path: Option<PathBuf>,
 21    project_path: Option<ProjectPath>,
 22    image_cache: Entity<RetainAllImageCache>,
 23    workspace_handle: WeakEntity<Workspace>,
 24    _editor_subscription: Subscription,
 25    _workspace_subscription: Option<Subscription>,
 26    _project_subscription: Option<Subscription>,
 27}
 28
 29#[derive(Clone, Copy, Debug, PartialEq)]
 30pub enum SvgPreviewMode {
 31    /// The preview will always show the contents of the provided editor.
 32    Default,
 33    /// The preview will "follow" the last active editor of an SVG file.
 34    Follow,
 35}
 36
 37impl SvgPreviewView {
 38    pub fn register(workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context<Workspace>) {
 39        workspace.register_action(move |workspace, _: &OpenPreview, window, cx| {
 40            if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx)
 41                && Self::is_svg_file(&editor, cx)
 42            {
 43                let view = Self::create_svg_view(
 44                    SvgPreviewMode::Default,
 45                    workspace,
 46                    editor.clone(),
 47                    window,
 48                    cx,
 49                );
 50                workspace.active_pane().update(cx, |pane, cx| {
 51                    if let Some(existing_view_idx) =
 52                        Self::find_existing_preview_item_idx(pane, &editor, cx)
 53                    {
 54                        pane.activate_item(existing_view_idx, true, true, window, cx);
 55                    } else {
 56                        pane.add_item(Box::new(view), true, true, None, window, cx)
 57                    }
 58                });
 59                cx.notify();
 60            }
 61        });
 62
 63        workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, window, cx| {
 64            if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx)
 65                && Self::is_svg_file(&editor, cx)
 66            {
 67                let editor_clone = editor.clone();
 68                let view = Self::create_svg_view(
 69                    SvgPreviewMode::Default,
 70                    workspace,
 71                    editor_clone,
 72                    window,
 73                    cx,
 74                );
 75                let pane = workspace
 76                    .find_pane_in_direction(workspace::SplitDirection::Right, cx)
 77                    .unwrap_or_else(|| {
 78                        workspace.split_pane(
 79                            workspace.active_pane().clone(),
 80                            workspace::SplitDirection::Right,
 81                            window,
 82                            cx,
 83                        )
 84                    });
 85                pane.update(cx, |pane, cx| {
 86                    if let Some(existing_view_idx) =
 87                        Self::find_existing_preview_item_idx(pane, &editor, cx)
 88                    {
 89                        pane.activate_item(existing_view_idx, true, true, window, cx);
 90                    } else {
 91                        pane.add_item(Box::new(view), false, false, None, window, cx)
 92                    }
 93                });
 94                cx.notify();
 95            }
 96        });
 97
 98        workspace.register_action(move |workspace, _: &OpenFollowingPreview, window, cx| {
 99            if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx)
100                && Self::is_svg_file(&editor, cx)
101            {
102                let view =
103                    Self::create_svg_view(SvgPreviewMode::Follow, workspace, editor, window, cx);
104                workspace.active_pane().update(cx, |pane, cx| {
105                    pane.add_item(Box::new(view), true, true, None, window, cx)
106                });
107                cx.notify();
108            }
109        });
110    }
111
112    fn find_existing_preview_item_idx(
113        pane: &Pane,
114        editor: &Entity<Editor>,
115        cx: &App,
116    ) -> Option<usize> {
117        let editor_path = Self::get_svg_path(editor, cx);
118        pane.items_of_type::<SvgPreviewView>()
119            .find(|view| {
120                let view_read = view.read(cx);
121                view_read.svg_path.is_some() && view_read.svg_path == editor_path
122            })
123            .and_then(|view| pane.index_for_item(&view))
124    }
125
126    pub fn resolve_active_item_as_svg_editor(
127        workspace: &Workspace,
128        cx: &mut Context<Workspace>,
129    ) -> Option<Entity<Editor>> {
130        let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
131
132        if Self::is_svg_file(&editor, cx) {
133            Some(editor)
134        } else {
135            None
136        }
137    }
138
139    fn create_svg_view(
140        mode: SvgPreviewMode,
141        workspace: &mut Workspace,
142        editor: Entity<Editor>,
143        window: &mut Window,
144        cx: &mut Context<Workspace>,
145    ) -> Entity<SvgPreviewView> {
146        let workspace_handle = workspace.weak_handle();
147        SvgPreviewView::new(mode, editor, workspace_handle, window, cx)
148    }
149
150    pub fn new(
151        mode: SvgPreviewMode,
152        active_editor: Entity<Editor>,
153        workspace_handle: WeakEntity<Workspace>,
154        window: &mut Window,
155        cx: &mut Context<Workspace>,
156    ) -> Entity<Self> {
157        cx.new(|cx| {
158            let svg_path = Self::get_svg_path(&active_editor, cx);
159            let project_path = Self::get_project_path(&active_editor, cx);
160            let image_cache = RetainAllImageCache::new(cx);
161
162            let subscription = cx.subscribe_in(
163                &active_editor,
164                window,
165                |this: &mut SvgPreviewView, _editor, event: &EditorEvent, window, cx| {
166                    if event == &EditorEvent::Saved {
167                        // Remove cached image to force reload
168                        if let Some(svg_path) = &this.svg_path {
169                            let resource = Resource::Path(svg_path.clone().into());
170                            this.image_cache.update(cx, |cache, cx| {
171                                cache.remove(&resource, window, cx);
172                            });
173                        }
174                        cx.notify();
175                    }
176                },
177            );
178
179            // Subscribe to workspace active item changes to follow SVG files
180            let workspace_subscription = if mode == SvgPreviewMode::Follow {
181                workspace_handle.upgrade().map(|workspace_handle| {
182                    cx.subscribe_in(
183                        &workspace_handle,
184                        window,
185                        |this: &mut SvgPreviewView,
186                         workspace,
187                         event: &workspace::Event,
188                         _window,
189                         cx| {
190                            if let workspace::Event::ActiveItemChanged = event {
191                                let workspace_read = workspace.read(cx);
192                                if let Some(active_item) = workspace_read.active_item(cx)
193                                    && let Some(editor_entity) = active_item.downcast::<Editor>()
194                                    && Self::is_svg_file(&editor_entity, cx)
195                                {
196                                    let new_path = Self::get_svg_path(&editor_entity, cx);
197                                    if this.svg_path != new_path {
198                                        this.svg_path = new_path;
199                                        cx.notify();
200                                    }
201                                }
202                            }
203                        },
204                    )
205                })
206            } else {
207                None
208            };
209
210            // We'll set up the project subscription after the entity is created
211            let project_subscription = None;
212
213            let view = Self {
214                focus_handle: cx.focus_handle(),
215                svg_path,
216                project_path,
217                image_cache,
218                workspace_handle,
219                _editor_subscription: subscription,
220                _workspace_subscription: workspace_subscription,
221                _project_subscription: project_subscription,
222            };
223
224            view
225        })
226    }
227
228    fn setup_project_subscription(&mut self, window: &mut Window, cx: &mut Context<Self>) {
229        if let (Some(workspace), Some(project_path)) =
230            (self.workspace_handle.upgrade(), &self.project_path)
231        {
232            let project = workspace.read(cx).project().clone();
233            let worktree_id = project_path.worktree_id;
234            let worktree = {
235                let project_read = project.read(cx);
236                project_read
237                    .worktrees(cx)
238                    .find(|worktree| worktree.read(cx).id() == worktree_id)
239            };
240
241            if let Some(worktree) = worktree {
242                self._project_subscription = Some(cx.subscribe_in(
243                    &worktree,
244                    window,
245                    |this: &mut SvgPreviewView, _worktree, event: &WorktreeEvent, window, cx| {
246                        if let WorktreeEvent::UpdatedEntries(changes) = event {
247                            if let Some(project_path) = &this.project_path {
248                                // Check if our SVG file was modified
249                                for (path, _entry_id, _change) in changes.iter() {
250                                    if path.as_ref() == project_path.path.as_ref() {
251                                        // File was modified externally, clear cache and refresh
252                                        if let Some(svg_path) = &this.svg_path {
253                                            let resource = Resource::Path(svg_path.clone().into());
254                                            this.image_cache.update(cx, |cache, cx| {
255                                                cache.remove(&resource, window, cx);
256                                            });
257                                        }
258                                        cx.notify();
259                                        break;
260                                    }
261                                }
262                            }
263                        }
264                    },
265                ));
266            }
267        }
268    }
269
270    pub fn is_svg_file<C>(editor: &Entity<Editor>, cx: &C) -> bool
271    where
272        C: std::borrow::Borrow<App>,
273    {
274        let app = cx.borrow();
275        let buffer = editor.read(app).buffer().read(app);
276        if let Some(buffer) = buffer.as_singleton()
277            && let Some(file) = buffer.read(app).file()
278        {
279            return file
280                .path()
281                .extension()
282                .and_then(|ext| ext.to_str())
283                .map(|ext| ext.eq_ignore_ascii_case("svg"))
284                .unwrap_or(false);
285        }
286        false
287    }
288
289    fn get_svg_path<C>(editor: &Entity<Editor>, cx: &C) -> Option<PathBuf>
290    where
291        C: std::borrow::Borrow<App>,
292    {
293        let app = cx.borrow();
294        let buffer = editor.read(app).buffer().read(app).as_singleton()?;
295        let file = buffer.read(app).file()?;
296        let local_file = file.as_local()?;
297        Some(local_file.abs_path(app))
298    }
299
300    fn get_project_path<C>(editor: &Entity<Editor>, cx: &C) -> Option<ProjectPath>
301    where
302        C: std::borrow::Borrow<App>,
303    {
304        let app = cx.borrow();
305        let buffer = editor.read(app).buffer().read(app).as_singleton()?;
306        let file = buffer.read(app).file()?;
307        Some(ProjectPath {
308            worktree_id: file.worktree_id(app),
309            path: file.path().clone(),
310        })
311    }
312}
313
314impl Render for SvgPreviewView {
315    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
316        // Set up project subscription on first render if not already done
317        if self._project_subscription.is_none() {
318            self.setup_project_subscription(window, cx);
319        }
320        v_flex()
321            .id("SvgPreview")
322            .key_context("SvgPreview")
323            .track_focus(&self.focus_handle(cx))
324            .size_full()
325            .bg(cx.theme().colors().editor_background)
326            .flex()
327            .justify_center()
328            .items_center()
329            .child(if let Some(svg_path) = &self.svg_path {
330                img(ImageSource::from(svg_path.clone()))
331                    .image_cache(&self.image_cache)
332                    .max_w_full()
333                    .max_h_full()
334                    .with_fallback(|| {
335                        div()
336                            .p_4()
337                            .child("Failed to load SVG file")
338                            .into_any_element()
339                    })
340                    .into_any_element()
341            } else {
342                div().p_4().child("No SVG file selected").into_any_element()
343            })
344    }
345}
346
347impl Focusable for SvgPreviewView {
348    fn focus_handle(&self, _cx: &App) -> FocusHandle {
349        self.focus_handle.clone()
350    }
351}
352
353impl EventEmitter<()> for SvgPreviewView {}
354
355impl Item for SvgPreviewView {
356    type Event = ();
357
358    fn tab_icon(&self, _window: &Window, cx: &App) -> Option<Icon> {
359        // Use the same icon as SVG files in the file tree
360        self.svg_path
361            .as_ref()
362            .and_then(|svg_path| FileIcons::get_icon(svg_path, cx))
363            .map(Icon::from_path)
364            .or_else(|| Some(Icon::new(IconName::Image)))
365    }
366
367    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
368        self.svg_path
369            .as_ref()
370            .and_then(|svg_path| svg_path.file_name())
371            .map(|name| name.to_string_lossy())
372            .map(|name| format!("Preview {}", name).into())
373            .unwrap_or_else(|| "SVG Preview".into())
374    }
375
376    fn telemetry_event_text(&self) -> Option<&'static str> {
377        Some("svg preview: open")
378    }
379
380    fn to_item_events(_event: &Self::Event, _f: impl FnMut(workspace::item::ItemEvent)) {}
381}