From 6eb67882019a5171c336d5dbcc8ec1313e8a456f Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 25 Oct 2024 09:34:50 +0200 Subject: [PATCH] image viewer: Reuse existing tabs (#19717) Co-authored-by: Kirill Co-authored-by: Mikayla Fixes #9896 Release Notes: - Fixed an issue where clicking on an image inside the project panel would not re-use an existing image tab Co-authored-by: Kirill Co-authored-by: Mikayla --- crates/image_viewer/src/image_viewer.rs | 82 ++++++++++++++++++++----- 1 file changed, 66 insertions(+), 16 deletions(-) diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 5691119dea..607a4a4abe 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -1,3 +1,4 @@ +use anyhow::Context as _; use gpui::{ canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, Context, EventEmitter, FocusHandle, FocusableView, Img, InteractiveElement, IntoElement, Model, @@ -19,6 +20,7 @@ use workspace::{ const IMAGE_VIEWER_KIND: &str = "ImageView"; pub struct ImageItem { + id: ProjectEntryId, path: PathBuf, project_path: ProjectPath, } @@ -48,9 +50,15 @@ impl project::Item for ImageItem { .read_with(&cx, |project, cx| project.absolute_path(&path, cx))? .ok_or_else(|| anyhow::anyhow!("Failed to find the absolute path"))?; + let id = project + .update(&mut cx, |project, cx| project.entry_for_path(&path, cx))? + .context("Entry not found")? + .id; + cx.new_model(|_| ImageItem { path: abs_path, project_path: path, + id, }) })) } else { @@ -59,7 +67,7 @@ impl project::Item for ImageItem { } fn entry_id(&self, _: &AppContext) -> Option { - None + Some(self.id) } fn project_path(&self, _: &AppContext) -> Option { @@ -68,18 +76,30 @@ impl project::Item for ImageItem { } pub struct ImageView { - path: PathBuf, + image: Model, focus_handle: FocusHandle, } impl Item for ImageView { type Event = (); - fn tab_content(&self, params: TabContentParams, _cx: &WindowContext) -> AnyElement { - let title = self - .path + fn for_each_project_item( + &self, + cx: &AppContext, + f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item), + ) { + f(self.image.entity_id(), self.image.read(cx)) + } + + fn is_singleton(&self, _cx: &AppContext) -> bool { + true + } + + fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement { + let path = &self.image.read(cx).path; + let title = path .file_name() - .unwrap_or_else(|| self.path.as_os_str()) + .unwrap_or_else(|| path.as_os_str()) .to_string_lossy() .to_string(); Label::new(title) @@ -90,9 +110,10 @@ impl Item for ImageView { } fn tab_icon(&self, cx: &WindowContext) -> Option { + let path = &self.image.read(cx).path; ItemSettings::get_global(cx) .file_icons - .then(|| FileIcons::get_icon(self.path.as_path(), cx)) + .then(|| FileIcons::get_icon(path.as_path(), cx)) .flatten() .map(Icon::from_path) } @@ -106,7 +127,7 @@ impl Item for ImageView { Self: Sized, { Some(cx.new_view(|cx| Self { - path: self.path.clone(), + image: self.image.clone(), focus_handle: cx.focus_handle(), })) } @@ -118,7 +139,7 @@ impl SerializableItem for ImageView { } fn deserialize( - _project: Model, + project: Model, _workspace: WeakView, workspace_id: WorkspaceId, item_id: ItemId, @@ -129,10 +150,38 @@ impl SerializableItem for ImageView { .get_image_path(item_id, workspace_id)? .ok_or_else(|| anyhow::anyhow!("No image path found"))?; - cx.new_view(|cx| ImageView { - path: image_path, - focus_handle: cx.focus_handle(), - }) + let (worktree, relative_path) = project + .update(&mut cx, |project, cx| { + project.find_or_create_worktree(image_path.clone(), false, cx) + })? + .await + .context("Path not found")?; + let worktree_id = worktree.update(&mut cx, |worktree, _cx| worktree.id())?; + + let project_path = ProjectPath { + worktree_id, + path: relative_path.into(), + }; + + let id = project + .update(&mut cx, |project, cx| { + project.entry_for_path(&project_path, cx) + })? + .context("No entry found")? + .id; + + cx.update(|cx| { + let image = cx.new_model(|_| ImageItem { + id, + path: image_path, + project_path, + }); + + Ok(cx.new_view(|cx| ImageView { + image, + focus_handle: cx.focus_handle(), + })) + })? }) } @@ -154,7 +203,7 @@ impl SerializableItem for ImageView { let workspace_id = workspace.database_id()?; Some(cx.background_executor().spawn({ - let image_path = self.path.clone(); + let image_path = self.image.read(cx).path.clone(); async move { IMAGE_VIEWER .save_image_path(item_id, workspace_id, image_path) @@ -177,6 +226,7 @@ impl FocusableView for ImageView { impl Render for ImageView { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let image_path = self.image.read(cx).path.clone(); let checkered_background = |bounds: Bounds, _, cx: &mut WindowContext| { let square_size = 32.0; @@ -233,7 +283,7 @@ impl Render for ImageView { // TODO: In browser based Tailwind & Flex this would be h-screen and we'd use w-full .h_full() .child( - img(self.path.clone()) + img(image_path) .object_fit(ObjectFit::ScaleDown) .max_w_full() .max_h_full(), @@ -254,7 +304,7 @@ impl ProjectItem for ImageView { Self: Sized, { Self { - path: item.read(cx).path.clone(), + image: item, focus_handle: cx.focus_handle(), } }