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}