image_viewer.rs

  1use gpui::{
  2    canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, Context,
  3    EventEmitter, FocusHandle, FocusableView, Img, InteractiveElement, IntoElement, Model,
  4    ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
  5};
  6use persistence::IMAGE_VIEWER;
  7use ui::{h_flex, prelude::*};
  8
  9use project::{Project, ProjectEntryId, ProjectPath};
 10use std::{ffi::OsStr, path::PathBuf};
 11use util::ResultExt;
 12use workspace::{
 13    item::{Item, ProjectItem},
 14    ItemId, Pane, Workspace, WorkspaceId,
 15};
 16
 17const IMAGE_VIEWER_KIND: &str = "ImageView";
 18
 19pub struct ImageItem {
 20    path: PathBuf,
 21    project_path: ProjectPath,
 22}
 23
 24impl project::Item for ImageItem {
 25    fn try_open(
 26        project: &Model<Project>,
 27        path: &ProjectPath,
 28        cx: &mut AppContext,
 29    ) -> Option<Task<gpui::Result<Model<Self>>>> {
 30        let path = path.clone();
 31        let project = project.clone();
 32
 33        let ext = path
 34            .path
 35            .extension()
 36            .and_then(OsStr::to_str)
 37            .unwrap_or_default();
 38
 39        if Img::extensions().contains(&ext) {
 40            Some(cx.spawn(|mut cx| async move {
 41                let abs_path = project
 42                    .read_with(&cx, |project, cx| project.absolute_path(&path, cx))?
 43                    .ok_or_else(|| anyhow::anyhow!("Failed to find the absolute path"))?;
 44
 45                cx.new_model(|_| ImageItem {
 46                    path: abs_path,
 47                    project_path: path,
 48                })
 49            }))
 50        } else {
 51            None
 52        }
 53    }
 54
 55    fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
 56        None
 57    }
 58
 59    fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
 60        Some(self.project_path.clone())
 61    }
 62}
 63
 64pub struct ImageView {
 65    path: PathBuf,
 66    focus_handle: FocusHandle,
 67}
 68
 69impl Item for ImageView {
 70    type Event = ();
 71
 72    fn tab_content(
 73        &self,
 74        _detail: Option<usize>,
 75        selected: bool,
 76        _cx: &WindowContext,
 77    ) -> AnyElement {
 78        let title = self
 79            .path
 80            .file_name()
 81            .unwrap_or_else(|| self.path.as_os_str())
 82            .to_string_lossy()
 83            .to_string();
 84        Label::new(title)
 85            .color(if selected {
 86                Color::Default
 87            } else {
 88                Color::Muted
 89            })
 90            .into_any_element()
 91    }
 92
 93    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
 94        let item_id = cx.entity_id().as_u64();
 95        let workspace_id = workspace.database_id();
 96        let image_path = self.path.clone();
 97
 98        cx.background_executor()
 99            .spawn({
100                let image_path = image_path.clone();
101                async move {
102                    IMAGE_VIEWER
103                        .save_image_path(item_id, workspace_id, image_path)
104                        .await
105                        .log_err();
106                }
107            })
108            .detach();
109    }
110
111    fn serialized_item_kind() -> Option<&'static str> {
112        Some(IMAGE_VIEWER_KIND)
113    }
114
115    fn deserialize(
116        _project: Model<Project>,
117        _workspace: WeakView<Workspace>,
118        workspace_id: WorkspaceId,
119        item_id: ItemId,
120        cx: &mut ViewContext<Pane>,
121    ) -> Task<anyhow::Result<View<Self>>> {
122        cx.spawn(|_pane, mut cx| async move {
123            let image_path = IMAGE_VIEWER
124                .get_image_path(item_id, workspace_id)?
125                .ok_or_else(|| anyhow::anyhow!("No image path found"))?;
126
127            cx.new_view(|cx| ImageView {
128                path: image_path,
129                focus_handle: cx.focus_handle(),
130            })
131        })
132    }
133
134    fn clone_on_split(
135        &self,
136        _workspace_id: WorkspaceId,
137        cx: &mut ViewContext<Self>,
138    ) -> Option<View<Self>>
139    where
140        Self: Sized,
141    {
142        Some(cx.new_view(|cx| Self {
143            path: self.path.clone(),
144            focus_handle: cx.focus_handle(),
145        }))
146    }
147}
148
149impl EventEmitter<()> for ImageView {}
150impl FocusableView for ImageView {
151    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
152        self.focus_handle.clone()
153    }
154}
155
156impl Render for ImageView {
157    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
158        div()
159            .track_focus(&self.focus_handle)
160            .size_full()
161            .child(
162                // Checkered background behind the image
163                canvas(
164                    |_, _| (),
165                    |bounds, _, cx| {
166                        let square_size = 32.0;
167
168                        let start_y = bounds.origin.y.0;
169                        let height = bounds.size.height.0;
170                        let start_x = bounds.origin.x.0;
171                        let width = bounds.size.width.0;
172
173                        let mut y = start_y;
174                        let mut x = start_x;
175                        let mut color_swapper = true;
176                        // draw checkerboard pattern
177                        while y <= start_y + height {
178                            // Keeping track of the grid in order to be resilient to resizing
179                            let start_swap = color_swapper;
180                            while x <= start_x + width {
181                                let rect = Bounds::new(
182                                    point(px(x), px(y)),
183                                    size(px(square_size), px(square_size)),
184                                );
185
186                                let color = if color_swapper {
187                                    opaque_grey(0.6, 0.4)
188                                } else {
189                                    opaque_grey(0.7, 0.4)
190                                };
191
192                                cx.paint_quad(fill(rect, color));
193                                color_swapper = !color_swapper;
194                                x += square_size;
195                            }
196                            x = start_x;
197                            color_swapper = !start_swap;
198                            y += square_size;
199                        }
200                    },
201                )
202                .border_2()
203                .border_color(cx.theme().styles.colors.border)
204                .size_full()
205                .absolute()
206                .top_0()
207                .left_0(),
208            )
209            .child(
210                v_flex().h_full().justify_around().child(
211                    h_flex()
212                        .w_full()
213                        .justify_around()
214                        .child(img(self.path.clone())),
215                ),
216            )
217    }
218}
219
220impl ProjectItem for ImageView {
221    type Item = ImageItem;
222
223    fn for_project_item(
224        _project: Model<Project>,
225        item: Model<Self::Item>,
226        cx: &mut ViewContext<Self>,
227    ) -> Self
228    where
229        Self: Sized,
230    {
231        Self {
232            path: item.read(cx).path.clone(),
233            focus_handle: cx.focus_handle(),
234        }
235    }
236}
237
238pub fn init(cx: &mut AppContext) {
239    workspace::register_project_item::<ImageView>(cx);
240    workspace::register_deserializable_item::<ImageView>(cx)
241}
242
243mod persistence {
244    use std::path::PathBuf;
245
246    use db::{define_connection, query, sqlez_macros::sql};
247    use workspace::{ItemId, WorkspaceDb, WorkspaceId};
248
249    define_connection! {
250        pub static ref IMAGE_VIEWER: ImageViewerDb<WorkspaceDb> =
251            &[sql!(
252                CREATE TABLE image_viewers (
253                    workspace_id INTEGER,
254                    item_id INTEGER UNIQUE,
255
256                    image_path BLOB,
257
258                    PRIMARY KEY(workspace_id, item_id),
259                    FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
260                    ON DELETE CASCADE
261                ) STRICT;
262            )];
263    }
264
265    impl ImageViewerDb {
266        query! {
267           pub async fn update_workspace_id(
268                new_id: WorkspaceId,
269                old_id: WorkspaceId,
270                item_id: ItemId
271            ) -> Result<()> {
272                UPDATE image_viewers
273                SET workspace_id = ?
274                WHERE workspace_id = ? AND item_id = ?
275            }
276        }
277
278        query! {
279            pub async fn save_image_path(
280                item_id: ItemId,
281                workspace_id: WorkspaceId,
282                image_path: PathBuf
283            ) -> Result<()> {
284                INSERT OR REPLACE INTO image_viewers(item_id, workspace_id, image_path)
285                VALUES (?, ?, ?)
286            }
287        }
288
289        query! {
290            pub fn get_image_path(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
291                SELECT image_path
292                FROM image_viewers
293                WHERE item_id = ? AND workspace_id = ?
294            }
295        }
296    }
297}