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}