Add basic ico support (#40822)
Closes https://github.com/zed-industries/zed/discussions/40763 <img width="867" height="1088" alt="Screenshot 2025-10-21 at 23 14 47" src="https://github.com/user-attachments/assets/d691fb2a-afc6-4445-a335-054ef164e0d3" /> Also improves error handling on image open failure: <img width="864" height="1083" alt="Screenshot 2025-10-21 at 23 14 30" src="https://github.com/user-attachments/assets/d5388b61-995f-441b-b375-ad5136d1533b" /> Release Notes: - Added basic ico support, improved unsupported image handling
This commit is contained in:
@@ -62,7 +62,7 @@ use util::{
|
||||
use workspace::{
|
||||
CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
|
||||
OpenOptions, ViewId,
|
||||
invalid_buffer_view::InvalidBufferView,
|
||||
invalid_item_view::InvalidItemView,
|
||||
item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
|
||||
register_project_item,
|
||||
};
|
||||
@@ -26251,7 +26251,7 @@ async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
|
||||
|
||||
assert_eq!(
|
||||
handle.to_any().entity_type(),
|
||||
TypeId::of::<InvalidBufferView>()
|
||||
TypeId::of::<InvalidItemView>()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ use ui::{IconDecorationKind, prelude::*};
|
||||
use util::{ResultExt, TryFutureExt, paths::PathExt};
|
||||
use workspace::{
|
||||
CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
||||
invalid_buffer_view::InvalidBufferView,
|
||||
invalid_item_view::InvalidItemView,
|
||||
item::{FollowableItem, Item, ItemBufferKind, ItemEvent, ProjectItem, SaveOptions},
|
||||
searchable::{
|
||||
Direction, FilteredSearchRange, SearchEvent, SearchableItem, SearchableItemHandle,
|
||||
@@ -1392,8 +1392,8 @@ impl ProjectItem for Editor {
|
||||
e: &anyhow::Error,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<InvalidBufferView> {
|
||||
Some(InvalidBufferView::new(abs_path, is_local, e, window, cx))
|
||||
) -> Option<InvalidItemView> {
|
||||
Some(InvalidItemView::new(abs_path, is_local, e, window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1644,6 +1644,8 @@ pub enum ImageFormat {
|
||||
Bmp,
|
||||
/// .tif or .tiff
|
||||
Tiff,
|
||||
/// .ico
|
||||
Ico,
|
||||
}
|
||||
|
||||
impl ImageFormat {
|
||||
@@ -1657,6 +1659,7 @@ impl ImageFormat {
|
||||
ImageFormat::Svg => "image/svg+xml",
|
||||
ImageFormat::Bmp => "image/bmp",
|
||||
ImageFormat::Tiff => "image/tiff",
|
||||
ImageFormat::Ico => "image/ico",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1670,6 +1673,7 @@ impl ImageFormat {
|
||||
"image/svg+xml" => Some(Self::Svg),
|
||||
"image/bmp" => Some(Self::Bmp),
|
||||
"image/tiff" | "image/tif" => Some(Self::Tiff),
|
||||
"image/ico" => Some(Self::Ico),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1776,6 +1780,7 @@ impl Image {
|
||||
ImageFormat::Webp => frames_for_image(&self.bytes, image::ImageFormat::WebP)?,
|
||||
ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?,
|
||||
ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?,
|
||||
ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?,
|
||||
ImageFormat::Svg => {
|
||||
let pixmap = svg_renderer.render_pixmap(&self.bytes, SvgSize::ScaleFactor(1.0))?;
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ x11rb::atom_manager! {
|
||||
SVG__MIME: ImageFormat::mime_type(ImageFormat::Svg ).as_bytes(),
|
||||
BMP__MIME: ImageFormat::mime_type(ImageFormat::Bmp ).as_bytes(),
|
||||
TIFF_MIME: ImageFormat::mime_type(ImageFormat::Tiff).as_bytes(),
|
||||
ICO__MIME: ImageFormat::mime_type(ImageFormat::Ico ).as_bytes(),
|
||||
|
||||
// This is just some random name for the property on our window, into which
|
||||
// the clipboard owner writes the data we requested.
|
||||
@@ -1003,6 +1004,7 @@ impl Clipboard {
|
||||
ImageFormat::Svg => self.inner.atoms.SVG__MIME,
|
||||
ImageFormat::Bmp => self.inner.atoms.BMP__MIME,
|
||||
ImageFormat::Tiff => self.inner.atoms.TIFF_MIME,
|
||||
ImageFormat::Ico => self.inner.atoms.ICO__MIME,
|
||||
};
|
||||
let data = vec![ClipboardData {
|
||||
bytes: image.bytes,
|
||||
|
||||
@@ -1607,6 +1607,7 @@ impl From<ImageFormat> for UTType {
|
||||
ImageFormat::Gif => Self::gif(),
|
||||
ImageFormat::Bmp => Self::bmp(),
|
||||
ImageFormat::Svg => Self::svg(),
|
||||
ImageFormat::Ico => Self::ico(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1645,6 +1646,11 @@ impl UTType {
|
||||
Self(unsafe { ns_string("public.svg-image") })
|
||||
}
|
||||
|
||||
pub fn ico() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/ico
|
||||
Self(unsafe { ns_string("com.microsoft.ico") })
|
||||
}
|
||||
|
||||
pub fn tiff() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/tiff
|
||||
Self(unsafe { NSPasteboardTypeTIFF }) // This is a rare case where there's a built-in NSPasteboardType
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
mod image_info;
|
||||
mod image_viewer_settings;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use editor::{EditorSettings, items::entry_git_aware_label_color};
|
||||
use file_icons::FileIcons;
|
||||
@@ -18,6 +20,7 @@ use ui::prelude::*;
|
||||
use util::paths::PathExt;
|
||||
use workspace::{
|
||||
ItemId, ItemSettings, Pane, ToolbarItemLocation, Workspace, WorkspaceId, delete_unloaded_items,
|
||||
invalid_item_view::InvalidItemView,
|
||||
item::{BreadcrumbText, Item, ProjectItem, SerializableItem, TabContentParams},
|
||||
};
|
||||
|
||||
@@ -389,6 +392,19 @@ impl ProjectItem for ImageView {
|
||||
{
|
||||
Self::new(item, project, window, cx)
|
||||
}
|
||||
|
||||
fn for_broken_project_item(
|
||||
abs_path: &Path,
|
||||
is_local: bool,
|
||||
e: &anyhow::Error,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<InvalidItemView>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(InvalidItemView::new(abs_path, is_local, e, window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
|
||||
@@ -687,6 +687,7 @@ fn create_gpui_image(content: Vec<u8>) -> anyhow::Result<Arc<gpui::Image>> {
|
||||
image::ImageFormat::Gif => gpui::ImageFormat::Gif,
|
||||
image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
|
||||
image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
|
||||
image::ImageFormat::Ico => gpui::ImageFormat::Ico,
|
||||
format => anyhow::bail!("Image format {format:?} not supported"),
|
||||
},
|
||||
content,
|
||||
|
||||
+7
-6
@@ -11,7 +11,8 @@ use zed_actions::workspace::OpenWithSystem;
|
||||
use crate::Item;
|
||||
|
||||
/// A view to display when a certain buffer fails to open.
|
||||
pub struct InvalidBufferView {
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidItemView {
|
||||
/// Which path was attempted to open.
|
||||
pub abs_path: Arc<Path>,
|
||||
/// An error message, happened when opening the buffer.
|
||||
@@ -20,7 +21,7 @@ pub struct InvalidBufferView {
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl InvalidBufferView {
|
||||
impl InvalidItemView {
|
||||
pub fn new(
|
||||
abs_path: &Path,
|
||||
is_local: bool,
|
||||
@@ -37,7 +38,7 @@ impl InvalidBufferView {
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for InvalidBufferView {
|
||||
impl Item for InvalidItemView {
|
||||
type Event = ();
|
||||
|
||||
fn tab_content_text(&self, mut detail: usize, _: &App) -> SharedString {
|
||||
@@ -66,15 +67,15 @@ impl Item for InvalidBufferView {
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for InvalidBufferView {}
|
||||
impl EventEmitter<()> for InvalidItemView {}
|
||||
|
||||
impl Focusable for InvalidBufferView {
|
||||
impl Focusable for InvalidItemView {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for InvalidBufferView {
|
||||
impl Render for InvalidItemView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
|
||||
let abs_path = self.abs_path.clone();
|
||||
v_flex()
|
||||
@@ -51,6 +51,7 @@ impl ImageView {
|
||||
image::ImageFormat::WebP => ImageFormat::Webp,
|
||||
image::ImageFormat::Tiff => ImageFormat::Tiff,
|
||||
image::ImageFormat::Bmp => ImageFormat::Bmp,
|
||||
image::ImageFormat::Ico => ImageFormat::Ico,
|
||||
format => {
|
||||
anyhow::bail!("unsupported image format {format:?}");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use gpui::{EventEmitter, FocusHandle, Focusable};
|
||||
use ui::{
|
||||
App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, InteractiveElement,
|
||||
KeyBinding, Label, LabelCommon, LabelSize, ParentElement, Render, SharedString, Styled as _,
|
||||
Window, h_flex, v_flex,
|
||||
};
|
||||
use zed_actions::workspace::OpenWithSystem;
|
||||
|
||||
use crate::Item;
|
||||
|
||||
/// A view to display when a certain buffer/image/other item fails to open.
|
||||
pub struct InvalidItemView {
|
||||
/// Which path was attempted to open.
|
||||
pub abs_path: Arc<Path>,
|
||||
/// An error message, happened when opening the item.
|
||||
pub error: SharedString,
|
||||
is_local: bool,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl InvalidItemView {
|
||||
pub fn new(
|
||||
abs_path: &Path,
|
||||
is_local: bool,
|
||||
e: &anyhow::Error,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
Self {
|
||||
is_local,
|
||||
abs_path: Arc::from(abs_path),
|
||||
error: format!("{}", e.root_cause()).into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for InvalidItemView {
|
||||
type Event = ();
|
||||
|
||||
fn tab_content_text(&self, mut detail: usize, _: &App) -> SharedString {
|
||||
// Ensure we always render at least the filename.
|
||||
detail += 1;
|
||||
|
||||
let path = self.abs_path.as_ref();
|
||||
|
||||
let mut prefix = path;
|
||||
while detail > 0 {
|
||||
if let Some(parent) = prefix.parent() {
|
||||
prefix = parent;
|
||||
detail -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let path = if detail > 0 {
|
||||
path
|
||||
} else {
|
||||
path.strip_prefix(prefix).unwrap_or(path)
|
||||
};
|
||||
|
||||
SharedString::new(path.to_string_lossy())
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for InvalidItemView {}
|
||||
|
||||
impl Focusable for InvalidItemView {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for InvalidItemView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
|
||||
let abs_path = self.abs_path.clone();
|
||||
v_flex()
|
||||
.size_full()
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.flex_none()
|
||||
.justify_center()
|
||||
.overflow_hidden()
|
||||
.key_context("InvalidItem")
|
||||
.child(
|
||||
h_flex().size_full().justify_center().child(
|
||||
v_flex()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(h_flex().justify_center().child("Could not open file"))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_center()
|
||||
.child(Label::new(self.error.clone()).size(LabelSize::Small)),
|
||||
)
|
||||
.when(self.is_local, |contents| {
|
||||
contents.child(
|
||||
h_flex().justify_center().child(
|
||||
Button::new("open-with-system", "Open in Default App")
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.open_with_system(&abs_path);
|
||||
})
|
||||
.style(ButtonStyle::Outlined)
|
||||
.key_binding(KeyBinding::for_action(
|
||||
&OpenWithSystem,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
CollaboratorId, DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory,
|
||||
SerializableItemRegistry, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
||||
invalid_buffer_view::InvalidBufferView,
|
||||
invalid_item_view::InvalidItemView,
|
||||
pane::{self, Pane},
|
||||
persistence::model::ItemId,
|
||||
searchable::SearchableItemHandle,
|
||||
@@ -1062,7 +1062,7 @@ pub trait ProjectItem: Item {
|
||||
_e: &anyhow::Error,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Option<InvalidBufferView>
|
||||
) -> Option<InvalidItemView>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible,
|
||||
SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace,
|
||||
WorkspaceItemBuilder,
|
||||
invalid_buffer_view::InvalidBufferView,
|
||||
invalid_item_view::InvalidItemView,
|
||||
item::{
|
||||
ActivateOnClose, ClosePosition, Item, ItemBufferKind, ItemHandle, ItemSettings,
|
||||
PreviewTabsSettings, ProjectItemKind, SaveOptions, ShowCloseButton, ShowDiagnostics,
|
||||
@@ -992,11 +992,11 @@ impl Pane {
|
||||
|
||||
let new_item = build_item(self, window, cx);
|
||||
// A special case that won't ever get a `project_entry_id` but has to be deduplicated nonetheless.
|
||||
if let Some(invalid_buffer_view) = new_item.downcast::<InvalidBufferView>() {
|
||||
if let Some(invalid_buffer_view) = new_item.downcast::<InvalidItemView>() {
|
||||
let mut already_open_view = None;
|
||||
let mut views_to_close = HashSet::default();
|
||||
for existing_error_view in self
|
||||
.items_of_type::<InvalidBufferView>()
|
||||
.items_of_type::<InvalidItemView>()
|
||||
.filter(|item| item.read(cx).abs_path == invalid_buffer_view.read(cx).abs_path)
|
||||
{
|
||||
if already_open_view.is_none()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pub mod dock;
|
||||
pub mod history_manager;
|
||||
pub mod invalid_buffer_view;
|
||||
pub mod invalid_item_view;
|
||||
pub mod item;
|
||||
mod modal_layer;
|
||||
pub mod notifications;
|
||||
|
||||
Reference in New Issue
Block a user