tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Definitions

⎇ 53dbdf0f1deea10d6fc567ea2b43e1e75d6d60cd crates/apps/image_viewer/src/lib.rs Rust · 148 lines · 4.8 KB History Who touched what raw

chore: monorepo inicial con arje + minga + yahweh absorbidos 53dbdf0f1 · Sergio ·

1//! `yahweh_image_viewer` — visor de imágenes.
2//!
3//! Suscribe al `AppBus` y, en cada `EntitySelected` cuyo provider sea
4//! `local_fs` y la extensión sugiera imagen (jpg, png, webp, gif), pasa el
5//! path a `gpui::img(...)` que se encarga del decode + cache. Para otros
6//! providers o extensiones desconocidas, muestra un mensaje neutro sin
7//! intentar render (evita binarios random pasando por el decoder).
8//!
9//! Detección por extensión: lista de extensiones soportadas en
10//! [`is_image_path`]. Para discriminar por mime real (sin importar la
11//! extensión) habría que invocar `image::guess_format` en un task —
12//! valdrá la pena cuando carguemos imágenes desde SQLite blobs.
13
14use std::path::Path;
15
16use gpui::{
17    Context, Entity, IntoElement, Render, SharedString, Window, div, img, prelude::*, px,
18};
19
20use yahweh_bus::{AppBus, AppEvent};
21use yahweh_theme::Theme;
22
23const FS_PROVIDER: &str = "local_fs";
24
25pub struct ImageViewer {
26    /// Path actualmente mostrado (si lo hay).
27    current_path: Option<String>,
28    /// Mensaje a mostrar cuando no se puede renderear (extensión no
29    /// reconocida, provider sin soporte, etc.).
30    notice: Option<SharedString>,
31}
32
33impl ImageViewer {
34    pub fn new(bus: Entity<AppBus>, cx: &mut Context<Self>) -> Self {
35        cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
36        cx.subscribe(&bus, |this: &mut ImageViewer, _, ev, cx| {
37            this.on_app_event(ev, cx);
38        })
39        .detach();
40        Self {
41            current_path: None,
42            notice: None,
43        }
44    }
45
46    fn on_app_event(&mut self, event: &AppEvent, cx: &mut Context<Self>) {
47        let (provider, id) = match event {
48            AppEvent::EntitySelected { provider, id, .. }
49            | AppEvent::EntityOpened { provider, id, .. } => (provider, id),
50        };
51
52        if provider != FS_PROVIDER {
53            self.current_path = None;
54            self.notice = Some(
55                format!("provider '{}' no soportado por ImageViewer", provider).into(),
56            );
57            cx.notify();
58            return;
59        }
60
61        if !is_image_path(id) {
62            self.current_path = None;
63            self.notice = Some("(no es una imagen reconocible)".into());
64            cx.notify();
65            return;
66        }
67
68        self.current_path = Some(id.clone());
69        self.notice = None;
70        cx.notify();
71    }
72}
73
74fn is_image_path(path: &str) -> bool {
75    let ext = Path::new(path)
76        .extension()
77        .and_then(|e| e.to_str())
78        .unwrap_or("")
79        .to_ascii_lowercase();
80    matches!(
81        ext.as_str(),
82        "png" | "jpg" | "jpeg" | "webp" | "gif" | "bmp" | "ico" | "tiff" | "tif"
83    )
84}
85
86impl Render for ImageViewer {
87    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
88        let theme = Theme::global(cx).clone();
89
90        let header_text = match (&self.current_path, &self.notice) {
91            (Some(p), _) => format!("[image] {}", p),
92            (None, Some(n)) => n.to_string(),
93            (None, None) => "(ninguna imagen seleccionada)".to_string(),
94        };
95
96        let body: gpui::AnyElement = match (&self.current_path, &self.notice) {
97            (Some(path), _) => {
98                let path_buf = std::path::PathBuf::from(path);
99                div()
100                    .flex_grow()
101                    .min_h(px(0.0))
102                    .flex()
103                    .items_center()
104                    .justify_center()
105                    .p(px(12.0))
106                    .child(img(path_buf).max_w_full().max_h_full())
107                    .into_any_element()
108            }
109            (None, Some(n)) => div()
110                .flex_grow()
111                .flex()
112                .items_center()
113                .justify_center()
114                .text_color(theme.fg_muted)
115                .text_size(px(11.0))
116                .child(n.clone())
117                .into_any_element(),
118            (None, None) => div()
119                .flex_grow()
120                .flex()
121                .items_center()
122                .justify_center()
123                .text_color(theme.fg_muted)
124                .text_size(px(11.0))
125                .child("doble click sobre una imagen en el FileExplorer.")
126                .into_any_element(),
127        };
128
129        div()
130            .size_full()
131            .bg(theme.bg_panel.clone())
132            .flex()
133            .flex_col()
134            .child(
135                div()
136                    .h(px(28.0))
137                    .px(px(10.0))
138                    .border_b_1()
139                    .border_color(theme.border)
140                    .flex()
141                    .items_center()
142                    .text_size(px(11.0))
143                    .text_color(theme.fg_muted)
144                    .child(SharedString::from(header_text)),
145            )
146            .child(body)
147    }
148}