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}