Skip to repository content341 lines · 11.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:06:22.295Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
svg_preview_view.rs
1use std::mem;
2use std::sync::Arc;
3
4use file_icons::FileIcons;
5use gpui::{
6 App, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render,
7 RenderImage, Styled, Subscription, Task, WeakEntity, Window, div, img,
8};
9use language::{Buffer, BufferEvent};
10use multi_buffer::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 buffer: Option<Entity<Buffer>>,
20 current_svg: Option<Result<Arc<RenderImage>, SharedString>>,
21 _refresh: Task<()>,
22 _buffer_subscription: Option<Subscription>,
23 _workspace_subscription: Option<Subscription>,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq)]
27pub enum SvgPreviewMode {
28 /// The preview will always show the contents of the provided editor.
29 Default,
30 /// The preview will "follow" the last active editor of an SVG file.
31 Follow,
32}
33
34impl SvgPreviewView {
35 pub fn new(
36 mode: SvgPreviewMode,
37 active_buffer: Entity<MultiBuffer>,
38 workspace_handle: WeakEntity<Workspace>,
39 window: &mut Window,
40 cx: &mut Context<Workspace>,
41 ) -> Entity<Self> {
42 cx.new(|cx| {
43 let workspace_subscription = if mode == SvgPreviewMode::Follow
44 && let Some(workspace) = workspace_handle.upgrade()
45 {
46 Some(Self::subscribe_to_workspace(workspace, window, cx))
47 } else {
48 None
49 };
50
51 let buffer = active_buffer.read_with(cx, |buffer, _cx| buffer.as_singleton());
52
53 let subscription = buffer
54 .as_ref()
55 .map(|buffer| Self::create_buffer_subscription(buffer, window, cx));
56
57 let mut this = Self {
58 focus_handle: cx.focus_handle(),
59 buffer,
60 current_svg: None,
61 _buffer_subscription: subscription,
62 _workspace_subscription: workspace_subscription,
63 _refresh: Task::ready(()),
64 };
65 this.render_image(window, cx);
66
67 this
68 })
69 }
70
71 fn subscribe_to_workspace(
72 workspace: Entity<Workspace>,
73 window: &Window,
74 cx: &mut Context<Self>,
75 ) -> Subscription {
76 cx.subscribe_in(
77 &workspace,
78 window,
79 move |this: &mut SvgPreviewView, workspace, event: &workspace::Event, window, cx| {
80 if let workspace::Event::ActiveItemChanged = event {
81 let workspace = workspace.read(cx);
82 if let Some(active_item) = workspace.active_item(cx)
83 && let Some(buffer) = active_item.downcast::<MultiBuffer>()
84 && Self::is_svg_file(&buffer, cx)
85 {
86 let Some(buffer) = buffer.read(cx).as_singleton() else {
87 return;
88 };
89 if this.buffer.as_ref() != Some(&buffer) {
90 this._buffer_subscription =
91 Some(Self::create_buffer_subscription(&buffer, window, cx));
92 this.buffer = Some(buffer);
93 this.render_image(window, cx);
94 cx.notify();
95 }
96 } else {
97 this.set_current(None, window, cx);
98 }
99 }
100 },
101 )
102 }
103
104 fn render_image(&mut self, window: &Window, cx: &mut Context<Self>) {
105 let Some(buffer) = self.buffer.as_ref() else {
106 return;
107 };
108 const SCALE_FACTOR: f32 = 1.0;
109
110 let renderer = cx.svg_renderer();
111 let content = buffer.read(cx).snapshot();
112 let background_task = cx.background_spawn(async move {
113 renderer.render_single_frame(content.text().as_bytes(), SCALE_FACTOR)
114 });
115
116 self._refresh = cx.spawn_in(window, async move |this, cx| {
117 let result = background_task.await;
118
119 this.update_in(cx, |view, window, cx| {
120 let current = result.map_err(|e| e.to_string().into());
121 view.set_current(Some(current), window, cx);
122 })
123 .ok();
124 });
125 }
126
127 fn set_current(
128 &mut self,
129 image: Option<Result<Arc<RenderImage>, SharedString>>,
130 window: &mut Window,
131 cx: &mut Context<Self>,
132 ) {
133 if let Some(Ok(image)) = mem::replace(&mut self.current_svg, image) {
134 window.drop_image(image).ok();
135 }
136 cx.notify();
137 }
138
139 fn find_existing_preview_item_idx(
140 pane: &Pane,
141 buffer: &Entity<MultiBuffer>,
142 cx: &App,
143 ) -> Option<usize> {
144 let buffer_id = buffer.read(cx).as_singleton()?.entity_id();
145 pane.items_of_type::<SvgPreviewView>()
146 .find(|view| {
147 view.read(cx)
148 .buffer
149 .as_ref()
150 .is_some_and(|buffer| buffer.entity_id() == buffer_id)
151 })
152 .and_then(|view| pane.index_for_item(&view))
153 }
154
155 pub fn resolve_active_item_as_svg_buffer(
156 workspace: &Workspace,
157 cx: &mut Context<Workspace>,
158 ) -> Option<Entity<MultiBuffer>> {
159 workspace
160 .active_item(cx)?
161 .act_as::<MultiBuffer>(cx)
162 .filter(|buffer| Self::is_svg_file(&buffer, cx))
163 }
164
165 fn create_svg_view(
166 mode: SvgPreviewMode,
167 workspace: &mut Workspace,
168 buffer: Entity<MultiBuffer>,
169 window: &mut Window,
170 cx: &mut Context<Workspace>,
171 ) -> Entity<SvgPreviewView> {
172 let workspace_handle = workspace.weak_handle();
173 SvgPreviewView::new(mode, buffer, workspace_handle, window, cx)
174 }
175
176 fn create_buffer_subscription(
177 buffer: &Entity<Buffer>,
178 window: &Window,
179 cx: &mut Context<Self>,
180 ) -> Subscription {
181 cx.subscribe_in(
182 buffer,
183 window,
184 move |this, _buffer, event: &BufferEvent, window, cx| match event {
185 BufferEvent::Edited { .. } | BufferEvent::Saved => {
186 this.render_image(window, cx);
187 }
188 _ => {}
189 },
190 )
191 }
192
193 pub fn is_svg_file(buffer: &Entity<MultiBuffer>, cx: &App) -> bool {
194 buffer
195 .read(cx)
196 .as_singleton()
197 .and_then(|buffer| buffer.read(cx).file())
198 .is_some_and(|file| {
199 std::path::Path::new(file.file_name(cx))
200 .extension()
201 .is_some_and(|ext| ext.eq_ignore_ascii_case("svg"))
202 })
203 }
204
205 pub fn open_preview_in_pane(
206 workspace: &mut Workspace,
207 buffer: Entity<MultiBuffer>,
208 pane: Entity<Pane>,
209 window: &mut Window,
210 cx: &mut Context<Workspace>,
211 ) {
212 Self::activate_or_add_preview(workspace, buffer, pane, true, window, cx);
213 }
214
215 pub fn open_preview_to_the_side_of_pane(
216 workspace: &mut Workspace,
217 buffer: Entity<MultiBuffer>,
218 origin_pane: Entity<Pane>,
219 window: &mut Window,
220 cx: &mut Context<Workspace>,
221 ) {
222 let target_pane = workspace.adjacent_pane_of(&origin_pane, window, cx);
223 Self::activate_or_add_preview(workspace, buffer, target_pane, false, window, cx);
224 }
225
226 fn activate_or_add_preview(
227 workspace: &mut Workspace,
228 buffer: Entity<MultiBuffer>,
229 pane: Entity<Pane>,
230 focus: bool,
231 window: &mut Window,
232 cx: &mut Context<Workspace>,
233 ) {
234 let existing_view_idx = Self::find_existing_preview_item_idx(pane.read(cx), &buffer, cx);
235 if let Some(existing_view_idx) = existing_view_idx {
236 pane.update(cx, |pane, cx| {
237 pane.activate_item(existing_view_idx, focus, focus, window, cx);
238 });
239 } else {
240 let view =
241 Self::create_svg_view(SvgPreviewMode::Default, workspace, buffer, window, cx);
242 pane.update(cx, |pane, cx| {
243 pane.add_item(Box::new(view), focus, focus, None, window, cx)
244 });
245 }
246 cx.notify();
247 }
248
249 pub fn register(workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context<Workspace>) {
250 workspace.register_action(move |workspace, _: &OpenPreview, window, cx| {
251 if let Some(buffer) = Self::resolve_active_item_as_svg_buffer(workspace, cx) {
252 let pane = workspace.active_pane().clone();
253 Self::open_preview_in_pane(workspace, buffer, pane, window, cx);
254 }
255 });
256
257 workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, window, cx| {
258 if let Some(buffer) = Self::resolve_active_item_as_svg_buffer(workspace, cx) {
259 let pane = workspace.active_pane().clone();
260 Self::open_preview_to_the_side_of_pane(workspace, buffer, pane, window, cx);
261 }
262 });
263
264 workspace.register_action(move |workspace, _: &OpenFollowingPreview, window, cx| {
265 if let Some(editor) = Self::resolve_active_item_as_svg_buffer(workspace, cx)
266 && Self::is_svg_file(&editor, cx)
267 {
268 let view =
269 Self::create_svg_view(SvgPreviewMode::Follow, workspace, editor, window, cx);
270 workspace.active_pane().update(cx, |pane, cx| {
271 pane.add_item(Box::new(view), true, true, None, window, cx)
272 });
273 cx.notify();
274 }
275 });
276 }
277}
278
279impl Render for SvgPreviewView {
280 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
281 v_flex()
282 .id("SvgPreview")
283 .key_context("SvgPreview")
284 .track_focus(&self.focus_handle(cx))
285 .size_full()
286 .bg(cx.theme().colors().editor_background)
287 .flex()
288 .justify_center()
289 .items_center()
290 .map(|this| match self.current_svg.clone() {
291 Some(Ok(image)) => {
292 this.child(img(image).max_w_full().max_h_full().with_fallback(|| {
293 h_flex()
294 .p_4()
295 .gap_2()
296 .child(Icon::new(IconName::Warning))
297 .child("Failed to load SVG image")
298 .into_any_element()
299 }))
300 }
301 Some(Err(e)) => this.child(div().p_4().child(e).into_any_element()),
302 None => this.child(div().p_4().child("No SVG file selected")),
303 })
304 }
305}
306
307impl Focusable for SvgPreviewView {
308 fn focus_handle(&self, _cx: &App) -> FocusHandle {
309 self.focus_handle.clone()
310 }
311}
312
313impl EventEmitter<()> for SvgPreviewView {}
314
315impl Item for SvgPreviewView {
316 type Event = ();
317
318 fn tab_icon(&self, _window: &Window, cx: &App) -> Option<Icon> {
319 self.buffer
320 .as_ref()
321 .and_then(|buffer| buffer.read(cx).file())
322 .and_then(|file| FileIcons::get_icon(file.path().as_std_path(), cx))
323 .map(Icon::from_path)
324 .or_else(|| Some(Icon::new(IconName::Image)))
325 }
326
327 fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
328 self.buffer
329 .as_ref()
330 .and_then(|svg_path| svg_path.read(cx).file())
331 .map(|name| format!("Preview {}", name.file_name(cx)).into())
332 .unwrap_or_else(|| "SVG Preview".into())
333 }
334
335 fn telemetry_event_text(&self) -> Option<&'static str> {
336 Some("svg preview: open")
337 }
338
339 fn to_item_events(_event: &Self::Event, _f: &mut dyn FnMut(workspace::item::ItemEvent)) {}
340}
341