⎇ 53dbdf0f1deea10d6fc567ea2b43e1e75d6d60cd
crates/apps/image_viewer/src/lib.rs
History
Who touched what
raw
chore: monorepo inicial con arje + minga + yahweh absorbidos
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}