svg_preview_view.rs

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