1use gpui::{Context, Entity, IntoElement, ParentElement, Render, Subscription, div};
2use project::image_store::{ImageFormat, ImageMetadata};
3use settings::Settings;
4use ui::prelude::*;
5use workspace::{ItemHandle, StatusItemView, Workspace};
6
7use crate::{ImageFileSizeUnit, ImageView, ImageViewerSettings};
8
9pub struct ImageInfo {
10 metadata: Option<ImageMetadata>,
11 _observe_active_image: Option<Subscription>,
12 observe_image_item: Option<Subscription>,
13}
14
15impl ImageInfo {
16 pub fn new(_workspace: &Workspace) -> Self {
17 Self {
18 metadata: None,
19 _observe_active_image: None,
20 observe_image_item: None,
21 }
22 }
23
24 fn update_metadata(&mut self, image_view: &Entity<ImageView>, cx: &mut Context<Self>) {
25 let image_item = image_view.read(cx).image_item.clone();
26 let current_metadata = image_item.read(cx).image_metadata;
27 if current_metadata.is_some() {
28 self.metadata = current_metadata;
29 cx.notify();
30 } else {
31 self.observe_image_item = Some(cx.observe(&image_item, |this, item, cx| {
32 this.metadata = item.read(cx).image_metadata;
33 cx.notify();
34 }));
35 }
36 }
37}
38
39fn format_file_size(size: u64, image_unit_type: ImageFileSizeUnit) -> String {
40 match image_unit_type {
41 ImageFileSizeUnit::Binary => {
42 if size < 1024 {
43 format!("{size}B")
44 } else if size < 1024 * 1024 {
45 format!("{:.1}KiB", size as f64 / 1024.0)
46 } else {
47 format!("{:.1}MiB", size as f64 / (1024.0 * 1024.0))
48 }
49 }
50 ImageFileSizeUnit::Decimal => {
51 if size < 1000 {
52 format!("{size}B")
53 } else if size < 1000 * 1000 {
54 format!("{:.1}KB", size as f64 / 1000.0)
55 } else {
56 format!("{:.1}MB", size as f64 / (1000.0 * 1000.0))
57 }
58 }
59 }
60}
61
62impl Render for ImageInfo {
63 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
64 let settings = ImageViewerSettings::get_global(cx);
65
66 let Some(metadata) = self.metadata.as_ref() else {
67 return div();
68 };
69
70 let mut components = Vec::new();
71 components.push(format!("{}x{}", metadata.width, metadata.height));
72 components.push(format_file_size(metadata.file_size, settings.unit));
73
74 if let Some(colors) = metadata.colors {
75 components.push(format!(
76 "{} channels, {} bits per pixel",
77 colors.channels,
78 colors.bits_per_pixel()
79 ));
80 }
81
82 components.push(
83 match metadata.format {
84 ImageFormat::Png => "PNG",
85 ImageFormat::Jpeg => "JPEG",
86 ImageFormat::Gif => "GIF",
87 ImageFormat::WebP => "WebP",
88 ImageFormat::Tiff => "TIFF",
89 ImageFormat::Bmp => "BMP",
90 ImageFormat::Ico => "ICO",
91 ImageFormat::Avif => "Avif",
92 _ => "Unknown",
93 }
94 .to_string(),
95 );
96
97 div().child(
98 Button::new("image-metadata", components.join(" • ")).label_size(LabelSize::Small),
99 )
100 }
101}
102
103impl StatusItemView for ImageInfo {
104 fn set_active_pane_item(
105 &mut self,
106 active_pane_item: Option<&dyn ItemHandle>,
107 _window: &mut Window,
108 cx: &mut Context<Self>,
109 ) {
110 self._observe_active_image = None;
111 self.observe_image_item = None;
112
113 if let Some(image_view) = active_pane_item.and_then(|item| item.act_as::<ImageView>(cx)) {
114 self.update_metadata(&image_view, cx);
115
116 self._observe_active_image = Some(cx.observe(&image_view, |this, view, cx| {
117 this.update_metadata(&view, cx);
118 }));
119 } else {
120 self.metadata = None;
121 }
122 cx.notify();
123 }
124}