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                && Self::is_svg_file(&editor, cx)
 37            {
 38                let view = Self::create_svg_view(
 39                    SvgPreviewMode::Default,
 40                    workspace,
 41                    editor.clone(),
 42                    window,
 43                    cx,
 44                );
 45                workspace.active_pane().update(cx, |pane, cx| {
 46                    if let Some(existing_view_idx) =
 47                        Self::find_existing_preview_item_idx(pane, &editor, cx)
 48                    {
 49                        pane.activate_item(existing_view_idx, true, true, window, cx);
 50                    } else {
 51                        pane.add_item(Box::new(view), true, true, None, window, cx)
 52                    }
 53                });
 54                cx.notify();
 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                && Self::is_svg_file(&editor, cx)
 61            {
 62                let editor_clone = editor.clone();
 63                let view = Self::create_svg_view(
 64                    SvgPreviewMode::Default,
 65                    workspace,
 66                    editor_clone,
 67                    window,
 68                    cx,
 69                );
 70                let pane = workspace
 71                    .find_pane_in_direction(workspace::SplitDirection::Right, cx)
 72                    .unwrap_or_else(|| {
 73                        workspace.split_pane(
 74                            workspace.active_pane().clone(),
 75                            workspace::SplitDirection::Right,
 76                            window,
 77                            cx,
 78                        )
 79                    });
 80                pane.update(cx, |pane, cx| {
 81                    if let Some(existing_view_idx) =
 82                        Self::find_existing_preview_item_idx(pane, &editor, cx)
 83                    {
 84                        pane.activate_item(existing_view_idx, true, true, window, cx);
 85                    } else {
 86                        pane.add_item(Box::new(view), false, false, None, window, cx)
 87                    }
 88                });
 89                cx.notify();
 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                && Self::is_svg_file(&editor, cx)
 96            {
 97                let view =
 98                    Self::create_svg_view(SvgPreviewMode::Follow, workspace, editor, window, cx);
 99                workspace.active_pane().update(cx, |pane, cx| {
100                    pane.add_item(Box::new(view), true, true, None, window, cx)
101                });
102                cx.notify();
103            }
104        });
105    }
106
107    fn find_existing_preview_item_idx(
108        pane: &Pane,
109        editor: &Entity<Editor>,
110        cx: &App,
111    ) -> Option<usize> {
112        let editor_path = Self::get_svg_path(editor, cx);
113        pane.items_of_type::<SvgPreviewView>()
114            .find(|view| {
115                let view_read = view.read(cx);
116                view_read.svg_path.is_some() && view_read.svg_path == editor_path
117            })
118            .and_then(|view| pane.index_for_item(&view))
119    }
120
121    pub fn resolve_active_item_as_svg_editor(
122        workspace: &Workspace,
123        cx: &mut Context<Workspace>,
124    ) -> Option<Entity<Editor>> {
125        let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
126
127        if Self::is_svg_file(&editor, cx) {
128            Some(editor)
129        } else {
130            None
131        }
132    }
133
134    fn create_svg_view(
135        mode: SvgPreviewMode,
136        workspace: &mut Workspace,
137        editor: Entity<Editor>,
138        window: &mut Window,
139        cx: &mut Context<Workspace>,
140    ) -> Entity<SvgPreviewView> {
141        let workspace_handle = workspace.weak_handle();
142        SvgPreviewView::new(mode, editor, workspace_handle, window, cx)
143    }
144
145    pub fn new(
146        mode: SvgPreviewMode,
147        active_editor: Entity<Editor>,
148        workspace_handle: WeakEntity<Workspace>,
149        window: &mut Window,
150        cx: &mut Context<Workspace>,
151    ) -> Entity<Self> {
152        cx.new(|cx| {
153            let svg_path = Self::get_svg_path(&active_editor, cx);
154            let image_cache = RetainAllImageCache::new(cx);
155
156            let subscription = cx.subscribe_in(
157                &active_editor,
158                window,
159                |this: &mut SvgPreviewView, _editor, event: &EditorEvent, window, cx| {
160                    match event {
161                        EditorEvent::Saved => {
162                            // Remove cached image to force reload
163                            if let Some(svg_path) = &this.svg_path {
164                                let resource = Resource::Path(svg_path.clone().into());
165                                this.image_cache.update(cx, |cache, cx| {
166                                    cache.remove(&resource, window, cx);
167                                });
168                            }
169                            cx.notify();
170                        }
171                        _ => {}
172                    }
173                },
174            );
175
176            // Subscribe to workspace active item changes to follow SVG files
177            let workspace_subscription = if mode == SvgPreviewMode::Follow {
178                workspace_handle.upgrade().map(|workspace_handle| {
179                    cx.subscribe_in(
180                        &workspace_handle,
181                        window,
182                        |this: &mut SvgPreviewView,
183                         workspace,
184                         event: &workspace::Event,
185                         _window,
186                         cx| {
187                            match event {
188                                workspace::Event::ActiveItemChanged => {
189                                    let workspace_read = workspace.read(cx);
190                                    if let Some(active_item) = workspace_read.active_item(cx)
191                                        && let Some(editor_entity) =
192                                            active_item.downcast::<Editor>()
193                                        && Self::is_svg_file(&editor_entity, cx)
194                                    {
195                                        let new_path = Self::get_svg_path(&editor_entity, cx);
196                                        if this.svg_path != new_path {
197                                            this.svg_path = new_path;
198                                            cx.notify();
199                                        }
200                                    }
201                                }
202                                _ => {}
203                            }
204                        },
205                    )
206                })
207            } else {
208                None
209            };
210
211            Self {
212                focus_handle: cx.focus_handle(),
213                svg_path,
214                image_cache,
215                _editor_subscription: subscription,
216                _workspace_subscription: workspace_subscription,
217            }
218        })
219    }
220
221    pub fn is_svg_file<C>(editor: &Entity<Editor>, cx: &C) -> bool
222    where
223        C: std::borrow::Borrow<App>,
224    {
225        let app = cx.borrow();
226        let buffer = editor.read(app).buffer().read(app);
227        if let Some(buffer) = buffer.as_singleton()
228            && let Some(file) = buffer.read(app).file()
229        {
230            return file
231                .path()
232                .extension()
233                .and_then(|ext| ext.to_str())
234                .map(|ext| ext.eq_ignore_ascii_case("svg"))
235                .unwrap_or(false);
236        }
237        false
238    }
239
240    fn get_svg_path<C>(editor: &Entity<Editor>, cx: &C) -> Option<PathBuf>
241    where
242        C: std::borrow::Borrow<App>,
243    {
244        let app = cx.borrow();
245        let buffer = editor.read(app).buffer().read(app).as_singleton()?;
246        let file = buffer.read(app).file()?;
247        let local_file = file.as_local()?;
248        Some(local_file.abs_path(app))
249    }
250}
251
252impl Render for SvgPreviewView {
253    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
254        v_flex()
255            .id("SvgPreview")
256            .key_context("SvgPreview")
257            .track_focus(&self.focus_handle(cx))
258            .size_full()
259            .bg(cx.theme().colors().editor_background)
260            .flex()
261            .justify_center()
262            .items_center()
263            .child(if let Some(svg_path) = &self.svg_path {
264                img(ImageSource::from(svg_path.clone()))
265                    .image_cache(&self.image_cache)
266                    .max_w_full()
267                    .max_h_full()
268                    .with_fallback(|| {
269                        div()
270                            .p_4()
271                            .child("Failed to load SVG file")
272                            .into_any_element()
273                    })
274                    .into_any_element()
275            } else {
276                div().p_4().child("No SVG file selected").into_any_element()
277            })
278    }
279}
280
281impl Focusable for SvgPreviewView {
282    fn focus_handle(&self, _cx: &App) -> FocusHandle {
283        self.focus_handle.clone()
284    }
285}
286
287impl EventEmitter<()> for SvgPreviewView {}
288
289impl Item for SvgPreviewView {
290    type Event = ();
291
292    fn tab_icon(&self, _window: &Window, cx: &App) -> Option<Icon> {
293        // Use the same icon as SVG files in the file tree
294        self.svg_path
295            .as_ref()
296            .and_then(|svg_path| FileIcons::get_icon(svg_path, cx))
297            .map(Icon::from_path)
298            .or_else(|| Some(Icon::new(IconName::Image)))
299    }
300
301    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
302        self.svg_path
303            .as_ref()
304            .and_then(|svg_path| svg_path.file_name())
305            .map(|name| name.to_string_lossy())
306            .map(|name| format!("Preview {}", name).into())
307            .unwrap_or_else(|| "SVG Preview".into())
308    }
309
310    fn telemetry_event_text(&self) -> Option<&'static str> {
311        Some("svg preview: open")
312    }
313
314    fn to_item_events(_event: &Self::Event, _f: impl FnMut(workspace::item::ItemEvent)) {}
315}