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