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 ui::prelude::*;
 11use workspace::item::Item;
 12use workspace::{Pane, Workspace};
 13
 14use crate::{OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide};
 15
 16pub struct SvgPreviewView {
 17    focus_handle: FocusHandle,
 18    svg_path: Option<PathBuf>,
 19    image_cache: Entity<RetainAllImageCache>,
 20    _editor_subscription: Subscription,
 21    _workspace_subscription: Option<Subscription>,
 22}
 23
 24#[derive(Clone, Copy, Debug, PartialEq)]
 25pub enum SvgPreviewMode {
 26    /// The preview will always show the contents of the provided editor.
 27    Default,
 28    /// The preview will "follow" the last active editor of an SVG file.
 29    Follow,
 30}
 31
 32impl SvgPreviewView {
 33    pub fn register(workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context<Workspace>) {
 34        workspace.register_action(move |workspace, _: &OpenPreview, window, cx| {
 35            if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) {
 36                if Self::is_svg_file(&editor, cx) {
 37                    let view = Self::create_svg_view(
 38                        SvgPreviewMode::Default,
 39                        workspace,
 40                        editor.clone(),
 41                        window,
 42                        cx,
 43                    );
 44                    workspace.active_pane().update(cx, |pane, cx| {
 45                        if let Some(existing_view_idx) =
 46                            Self::find_existing_preview_item_idx(pane, &editor, cx)
 47                        {
 48                            pane.activate_item(existing_view_idx, true, true, window, cx);
 49                        } else {
 50                            pane.add_item(Box::new(view), true, true, None, window, cx)
 51                        }
 52                    });
 53                    cx.notify();
 54                }
 55            }
 56        });
 57
 58        workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, window, cx| {
 59            if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) {
 60                if Self::is_svg_file(&editor, cx) {
 61                    let editor_clone = editor.clone();
 62                    let view = Self::create_svg_view(
 63                        SvgPreviewMode::Default,
 64                        workspace,
 65                        editor_clone,
 66                        window,
 67                        cx,
 68                    );
 69                    let pane = workspace
 70                        .find_pane_in_direction(workspace::SplitDirection::Right, cx)
 71                        .unwrap_or_else(|| {
 72                            workspace.split_pane(
 73                                workspace.active_pane().clone(),
 74                                workspace::SplitDirection::Right,
 75                                window,
 76                                cx,
 77                            )
 78                        });
 79                    pane.update(cx, |pane, cx| {
 80                        if let Some(existing_view_idx) =
 81                            Self::find_existing_preview_item_idx(pane, &editor, cx)
 82                        {
 83                            pane.activate_item(existing_view_idx, true, true, window, cx);
 84                        } else {
 85                            pane.add_item(Box::new(view), false, false, None, window, cx)
 86                        }
 87                    });
 88                    cx.notify();
 89                }
 90            }
 91        });
 92
 93        workspace.register_action(move |workspace, _: &OpenFollowingPreview, window, cx| {
 94            if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) {
 95                if Self::is_svg_file(&editor, cx) {
 96                    let view = Self::create_svg_view(
 97                        SvgPreviewMode::Follow,
 98                        workspace,
 99                        editor,
100                        window,
101                        cx,
102                    );
103                    workspace.active_pane().update(cx, |pane, cx| {
104                        pane.add_item(Box::new(view), true, true, None, window, cx)
105                    });
106                    cx.notify();
107                }
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 image_cache = RetainAllImageCache::new(cx);
160
161            let subscription = cx.subscribe_in(
162                &active_editor,
163                window,
164                |this: &mut SvgPreviewView, _editor, event: &EditorEvent, window, cx| {
165                    match event {
166                        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            );
180
181            // Subscribe to workspace active item changes to follow SVG files
182            let workspace_subscription = if mode == SvgPreviewMode::Follow {
183                workspace_handle.upgrade().map(|workspace_handle| {
184                    cx.subscribe_in(
185                        &workspace_handle,
186                        window,
187                        |this: &mut SvgPreviewView,
188                         workspace,
189                         event: &workspace::Event,
190                         _window,
191                         cx| {
192                            match event {
193                                workspace::Event::ActiveItemChanged => {
194                                    let workspace_read = workspace.read(cx);
195                                    if let Some(active_item) = workspace_read.active_item(cx) {
196                                        if let Some(editor_entity) =
197                                            active_item.downcast::<Editor>()
198                                        {
199                                            if Self::is_svg_file(&editor_entity, cx) {
200                                                let new_path =
201                                                    Self::get_svg_path(&editor_entity, cx);
202                                                if this.svg_path != new_path {
203                                                    this.svg_path = new_path;
204                                                    cx.notify();
205                                                }
206                                            }
207                                        }
208                                    }
209                                }
210                                _ => {}
211                            }
212                        },
213                    )
214                })
215            } else {
216                None
217            };
218
219            Self {
220                focus_handle: cx.focus_handle(),
221                svg_path,
222                image_cache,
223                _editor_subscription: subscription,
224                _workspace_subscription: workspace_subscription,
225            }
226        })
227    }
228
229    pub fn is_svg_file<C>(editor: &Entity<Editor>, cx: &C) -> bool
230    where
231        C: std::borrow::Borrow<App>,
232    {
233        let app = cx.borrow();
234        let buffer = editor.read(app).buffer().read(app);
235        if let Some(buffer) = buffer.as_singleton() {
236            if let Some(file) = buffer.read(app).file() {
237                return file
238                    .path()
239                    .extension()
240                    .and_then(|ext| ext.to_str())
241                    .map(|ext| ext.eq_ignore_ascii_case("svg"))
242                    .unwrap_or(false);
243            }
244        }
245        false
246    }
247
248    fn get_svg_path<C>(editor: &Entity<Editor>, cx: &C) -> Option<PathBuf>
249    where
250        C: std::borrow::Borrow<App>,
251    {
252        let app = cx.borrow();
253        let buffer = editor.read(app).buffer().read(app).as_singleton()?;
254        let file = buffer.read(app).file()?;
255        let local_file = file.as_local()?;
256        Some(local_file.abs_path(app))
257    }
258}
259
260impl Render for SvgPreviewView {
261    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
262        v_flex()
263            .id("SvgPreview")
264            .key_context("SvgPreview")
265            .track_focus(&self.focus_handle(cx))
266            .size_full()
267            .bg(cx.theme().colors().editor_background)
268            .flex()
269            .justify_center()
270            .items_center()
271            .child(if let Some(svg_path) = &self.svg_path {
272                img(ImageSource::from(svg_path.clone()))
273                    .image_cache(&self.image_cache)
274                    .max_w_full()
275                    .max_h_full()
276                    .with_fallback(|| {
277                        div()
278                            .p_4()
279                            .child("Failed to load SVG file")
280                            .into_any_element()
281                    })
282                    .into_any_element()
283            } else {
284                div().p_4().child("No SVG file selected").into_any_element()
285            })
286    }
287}
288
289impl Focusable for SvgPreviewView {
290    fn focus_handle(&self, _cx: &App) -> FocusHandle {
291        self.focus_handle.clone()
292    }
293}
294
295impl EventEmitter<()> for SvgPreviewView {}
296
297impl Item for SvgPreviewView {
298    type Event = ();
299
300    fn tab_icon(&self, _window: &Window, cx: &App) -> Option<Icon> {
301        // Use the same icon as SVG files in the file tree
302        self.svg_path
303            .as_ref()
304            .and_then(|svg_path| FileIcons::get_icon(svg_path, cx))
305            .map(Icon::from_path)
306            .or_else(|| Some(Icon::new(IconName::Image)))
307    }
308
309    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
310        self.svg_path
311            .as_ref()
312            .and_then(|svg_path| svg_path.file_name())
313            .map(|name| name.to_string_lossy())
314            .map(|name| format!("Preview {}", name).into())
315            .unwrap_or_else(|| "SVG Preview".into())
316    }
317
318    fn telemetry_event_text(&self) -> Option<&'static str> {
319        Some("svg preview: open")
320    }
321
322    fn to_item_events(_event: &Self::Event, _f: impl FnMut(workspace::item::ItemEvent)) {}
323}