Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:54:33.930Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

svg_preview_view.rs

341 lines · 11.9 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.