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}