image viewer: Reuse existing tabs (#19717)

Co-authored-by: Kirill <kirill@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>

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 <kirill@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
Bennet Bo Fenner 2024-10-25 09:34:50 +02:00 committed by GitHub
parent ebc3031fd9
commit 6eb6788201
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,3 +1,4 @@
use anyhow::Context as _;
use gpui::{ use gpui::{
canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, Context, canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, Context,
EventEmitter, FocusHandle, FocusableView, Img, InteractiveElement, IntoElement, Model, EventEmitter, FocusHandle, FocusableView, Img, InteractiveElement, IntoElement, Model,
@ -19,6 +20,7 @@ use workspace::{
const IMAGE_VIEWER_KIND: &str = "ImageView"; const IMAGE_VIEWER_KIND: &str = "ImageView";
pub struct ImageItem { pub struct ImageItem {
id: ProjectEntryId,
path: PathBuf, path: PathBuf,
project_path: ProjectPath, project_path: ProjectPath,
} }
@ -48,9 +50,15 @@ impl project::Item for ImageItem {
.read_with(&cx, |project, cx| project.absolute_path(&path, cx))? .read_with(&cx, |project, cx| project.absolute_path(&path, cx))?
.ok_or_else(|| anyhow::anyhow!("Failed to find the absolute path"))?; .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 { cx.new_model(|_| ImageItem {
path: abs_path, path: abs_path,
project_path: path, project_path: path,
id,
}) })
})) }))
} else { } else {
@ -59,7 +67,7 @@ impl project::Item for ImageItem {
} }
fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> { fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
None Some(self.id)
} }
fn project_path(&self, _: &AppContext) -> Option<ProjectPath> { fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
@ -68,18 +76,30 @@ impl project::Item for ImageItem {
} }
pub struct ImageView { pub struct ImageView {
path: PathBuf, image: Model<ImageItem>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
} }
impl Item for ImageView { impl Item for ImageView {
type Event = (); type Event = ();
fn tab_content(&self, params: TabContentParams, _cx: &WindowContext) -> AnyElement { fn for_each_project_item(
let title = self &self,
.path 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() .file_name()
.unwrap_or_else(|| self.path.as_os_str()) .unwrap_or_else(|| path.as_os_str())
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string();
Label::new(title) Label::new(title)
@ -90,9 +110,10 @@ impl Item for ImageView {
} }
fn tab_icon(&self, cx: &WindowContext) -> Option<Icon> { fn tab_icon(&self, cx: &WindowContext) -> Option<Icon> {
let path = &self.image.read(cx).path;
ItemSettings::get_global(cx) ItemSettings::get_global(cx)
.file_icons .file_icons
.then(|| FileIcons::get_icon(self.path.as_path(), cx)) .then(|| FileIcons::get_icon(path.as_path(), cx))
.flatten() .flatten()
.map(Icon::from_path) .map(Icon::from_path)
} }
@ -106,7 +127,7 @@ impl Item for ImageView {
Self: Sized, Self: Sized,
{ {
Some(cx.new_view(|cx| Self { Some(cx.new_view(|cx| Self {
path: self.path.clone(), image: self.image.clone(),
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
})) }))
} }
@ -118,7 +139,7 @@ impl SerializableItem for ImageView {
} }
fn deserialize( fn deserialize(
_project: Model<Project>, project: Model<Project>,
_workspace: WeakView<Workspace>, _workspace: WeakView<Workspace>,
workspace_id: WorkspaceId, workspace_id: WorkspaceId,
item_id: ItemId, item_id: ItemId,
@ -129,10 +150,38 @@ impl SerializableItem for ImageView {
.get_image_path(item_id, workspace_id)? .get_image_path(item_id, workspace_id)?
.ok_or_else(|| anyhow::anyhow!("No image path found"))?; .ok_or_else(|| anyhow::anyhow!("No image path found"))?;
cx.new_view(|cx| ImageView { let (worktree, relative_path) = project
path: image_path, .update(&mut cx, |project, cx| {
focus_handle: cx.focus_handle(), 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()?; let workspace_id = workspace.database_id()?;
Some(cx.background_executor().spawn({ Some(cx.background_executor().spawn({
let image_path = self.path.clone(); let image_path = self.image.read(cx).path.clone();
async move { async move {
IMAGE_VIEWER IMAGE_VIEWER
.save_image_path(item_id, workspace_id, image_path) .save_image_path(item_id, workspace_id, image_path)
@ -177,6 +226,7 @@ impl FocusableView for ImageView {
impl Render for ImageView { impl Render for ImageView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let image_path = self.image.read(cx).path.clone();
let checkered_background = |bounds: Bounds<Pixels>, _, cx: &mut WindowContext| { let checkered_background = |bounds: Bounds<Pixels>, _, cx: &mut WindowContext| {
let square_size = 32.0; 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 // TODO: In browser based Tailwind & Flex this would be h-screen and we'd use w-full
.h_full() .h_full()
.child( .child(
img(self.path.clone()) img(image_path)
.object_fit(ObjectFit::ScaleDown) .object_fit(ObjectFit::ScaleDown)
.max_w_full() .max_w_full()
.max_h_full(), .max_h_full(),
@ -254,7 +304,7 @@ impl ProjectItem for ImageView {
Self: Sized, Self: Sized,
{ {
Self { Self {
path: item.read(cx).path.clone(), image: item,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
} }
} }