mod image_info;mod image_viewer_settings; use std::path::Path; use anyhow::Context as _;use editor::{ Editor, EditorEvent, EditorSettings, RevealInFileManager, actions::SelectAll, items::entry_git_aware_label_color,};use file_icons::FileIcons;use gpui::{ AnyElement, App, Bounds, Context, DispatchPhase, Element, ElementId, Entity, EventEmitter, FocusHandle, Focusable, Font, GlobalElementId, InspectorElementId, InteractiveElement, IntoElement, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, PinchEvent, Pixels, Point, Render, ScrollDelta, ScrollWheelEvent, Style, Styled, Subscription, Task, WeakEntity, Window, actions, checkerboard, div, img, point, px, size,};use language::File as _;use persistence::ImageViewerDb;use project::{ ImageItem, Project, ProjectPath, git_store::GitStoreEvent, image_store::ImageItemEvent,};use settings::Settings;use theme_settings::ThemeSettings;use ui::{Tooltip, prelude::*};use util::paths::PathExt;use workspace::{ ItemId, ItemSettings, Pane, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, delete_unloaded_items, invalid_item_view::InvalidItemView, item::{HighlightedText, Item, ItemHandle, ProjectItem, SerializableItem, TabContentParams},}; pub use crate::image_info::*;pub use crate::image_viewer_settings::*; actions!( image_viewer, [ /// Zoom in the image. ZoomIn, /// Zoom out the image. ZoomOut, /// Reset zoom to 100%. ResetZoom, /// Fit the image to view. FitToView, /// Zoom to actual size (100%). ZoomToActualSize ]); const MIN_ZOOM: f32 = 0.1;const MAX_ZOOM: f32 = 20.0;const ZOOM_STEP: f32 = 1.1;const SCROLL_LINE_MULTIPLIER: f32 = 20.0;const BASE_SQUARE_SIZE: f32 = 32.0;const ZOOM_EDITOR_MIN_DIGITS: usize = 3; // Reserve room for common values like 100%.const ZOOM_EDITOR_MAX_DIGITS: usize = 4; // MAX_ZOOM is 2000%.const ZOOM_EDITOR_APPROX_CHAR_WIDTH: f32 = 8.0; // Approximate width of one small UI digit.const ZOOM_EDITOR_HORIZONTAL_PADDING: f32 = 12.0; // Extra room for cursor and editor edge padding. pub struct ImageView { image_item: Entity<ImageItem>, project: Entity<Project>, focus_handle: FocusHandle, zoom_level: f32, pan_offset: Point<Pixels>, last_mouse_position: Option<Point<Pixels>>, container_bounds: Option<Bounds<Pixels>>, image_size: Option<(u32, u32)>,} impl ImageView { fn is_dragging(&self) -> bool { self.last_mouse_position.is_some() } pub fn new( image_item: Entity<ImageItem>, project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { // Start loading the image to render in the background to prevent the view // from flickering in most cases. let _ = image_item.update(cx, |image, cx| { image.image.clone().get_render_image(window, cx) }); cx.subscribe(&image_item, Self::on_image_event).detach(); let git_store = project.read(cx).git_store().clone(); cx.subscribe(&git_store, |_, _, event, cx| { if matches!(event, GitStoreEvent::DiffBaseChanged(_)) { cx.emit(ImageViewEvent::TitleChanged); } }) .detach(); cx.on_release_in(window, |this, window, cx| { let image_data = this.image_item.read(cx).image.clone(); if let Some(image) = image_data.clone().get_render_image(window, cx) { cx.drop_image(image, None); } image_data.remove_asset(cx); }) .detach(); let image_size = image_item .read(cx) .image_metadata .map(|m| (m.width, m.height)); Self { image_item, project, focus_handle: cx.focus_handle(), zoom_level: 1.0, pan_offset: Point::default(), last_mouse_position: None, container_bounds: None, image_size, } } fn on_image_event( &mut self, _: Entity<ImageItem>, event: &ImageItemEvent, cx: &mut Context<Self>, ) { match event { ImageItemEvent::MetadataUpdated | ImageItemEvent::FileHandleChanged | ImageItemEvent::Reloaded => { self.image_size = self .image_item .read(cx) .image_metadata .map(|m| (m.width, m.height)); cx.emit(ImageViewEvent::TitleChanged); cx.notify(); } ImageItemEvent::ReloadNeeded => {} } } fn zoom_in(&mut self, _: &ZoomIn, _window: &mut Window, cx: &mut Context<Self>) { self.set_zoom(self.zoom_level * ZOOM_STEP, None, cx); } fn zoom_out(&mut self, _: &ZoomOut, _window: &mut Window, cx: &mut Context<Self>) { self.set_zoom(self.zoom_level / ZOOM_STEP, None, cx); } fn reset_zoom(&mut self, _: &ResetZoom, _window: &mut Window, cx: &mut Context<Self>) { self.zoom_level = 1.0; self.pan_offset = Point::default(); cx.notify(); } fn fit_to_view(&mut self, _: &FitToView, _window: &mut Window, cx: &mut Context<Self>) { if let Some((bounds, image_size)) = self.container_bounds.zip(self.image_size) { self.zoom_level = ImageView::compute_fit_to_view_zoom(bounds, image_size); self.pan_offset = Point::default(); cx.notify(); } } fn compute_fit_to_view_zoom(container_bounds: Bounds<Pixels>, image_size: (u32, u32)) -> f32 { let (image_width, image_height) = image_size; let container_width: f32 = container_bounds.size.width.into(); let container_height: f32 = container_bounds.size.height.into(); let scale_x = container_width / image_width as f32; let scale_y = container_height / image_height as f32; scale_x.min(scale_y).min(1.0) } fn zoom_to_actual_size( &mut self, _: &ZoomToActualSize, _window: &mut Window, cx: &mut Context<Self>, ) { self.zoom_level = 1.0; self.pan_offset = Point::default(); cx.notify(); } fn reveal_in_file_manager( &mut self, _: &RevealInFileManager, _window: &mut Window, cx: &mut Context<Self>, ) { if let Some(path) = self.image_item.read(cx).abs_path(cx) { self.project .update(cx, |project, cx| project.reveal_path(&path, cx)); } } fn set_zoom( &mut self, new_zoom: f32, zoom_center: Option<Point<Pixels>>, cx: &mut Context<Self>, ) { let old_zoom = self.zoom_level; self.zoom_level = new_zoom.clamp(MIN_ZOOM, MAX_ZOOM); if let Some((center, bounds)) = zoom_center.zip(self.container_bounds) { let relative_center = point( center.x - bounds.origin.x - bounds.size.width / 2.0, center.y - bounds.origin.y - bounds.size.height / 2.0, ); let mouse_offset_from_image = relative_center - self.pan_offset; let zoom_ratio = self.zoom_level / old_zoom; self.pan_offset += mouse_offset_from_image * (1.0 - zoom_ratio); } cx.notify(); } fn handle_scroll_wheel( &mut self, event: &ScrollWheelEvent, _window: &mut Window, cx: &mut Context<Self>, ) { if event.modifiers.control || event.modifiers.platform { let delta: f32 = match event.delta { ScrollDelta::Pixels(pixels) => pixels.y.into(), ScrollDelta::Lines(lines) => lines.y * SCROLL_LINE_MULTIPLIER, }; let zoom_factor = if delta > 0.0 { 1.0 + delta.abs() * 0.01 } else { 1.0 / (1.0 + delta.abs() * 0.01) }; self.set_zoom(self.zoom_level * zoom_factor, Some(event.position), cx); } else { let delta = match event.delta { ScrollDelta::Pixels(pixels) => pixels, ScrollDelta::Lines(lines) => lines.map(|d| px(d * SCROLL_LINE_MULTIPLIER)), }; self.pan_offset += delta; cx.notify(); } } fn handle_mouse_down( &mut self, event: &MouseDownEvent, _window: &mut Window, cx: &mut Context<Self>, ) { if event.button == MouseButton::Left || event.button == MouseButton::Middle { self.last_mouse_position = Some(event.position); cx.notify(); } } fn handle_mouse_up( &mut self, _event: &MouseUpEvent, _window: &mut Window, cx: &mut Context<Self>, ) { self.last_mouse_position = None; cx.notify(); } fn handle_mouse_move( &mut self, event: &MouseMoveEvent, _window: &mut Window, cx: &mut Context<Self>, ) { if self.is_dragging() { if let Some(last_pos) = self.last_mouse_position { let delta = event.position - last_pos; self.pan_offset += delta; } self.last_mouse_position = Some(event.position); cx.notify(); } } fn handle_pinch(&mut self, event: &PinchEvent, _window: &mut Window, cx: &mut Context<Self>) { let zoom_factor = 1.0 + event.delta; self.set_zoom(self.zoom_level * zoom_factor, Some(event.position), cx); }} struct ImageContentElement { image_view: Entity<ImageView>,} impl ImageContentElement { fn new(image_view: Entity<ImageView>) -> Self { Self { image_view } }} impl IntoElement for ImageContentElement { type Element = Self; fn into_element(self) -> Self::Element { self }} impl Element for ImageContentElement { type RequestLayoutState = (); type PrepaintState = Option<(AnyElement, bool)>; fn id(&self) -> Option<ElementId> { None } fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { None } fn request_layout( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { ( window.request_layout( Style { size: size(relative(1.).into(), relative(1.).into()), ..Default::default() }, [], cx, ), (), ) } fn prepaint( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, bounds: Bounds<Pixels>, _request_layout: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App, ) -> Self::PrepaintState { let image_view = self.image_view.read(cx); let image = image_view.image_item.read(cx).image.clone(); let first_layout = image_view.container_bounds.is_none(); let initial_zoom_level = first_layout .then(|| { image_view .image_size .map(|image_size| ImageView::compute_fit_to_view_zoom(bounds, image_size)) }) .flatten(); let zoom_level = initial_zoom_level.unwrap_or(image_view.zoom_level); let pan_offset = image_view.pan_offset; let border_color = cx.theme().colors().border; let is_dragging = image_view.is_dragging(); let scaled_size = image_view .image_size .map(|(w, h)| (px(w as f32 * zoom_level), px(h as f32 * zoom_level))); let (mut left, mut top) = (px(0.0), px(0.0)); let mut scaled_width = px(0.0); let mut scaled_height = px(0.0); if let Some((width, height)) = scaled_size { scaled_width = width; scaled_height = height; let center_x = bounds.size.width / 2.0; let center_y = bounds.size.height / 2.0; left = center_x - (scaled_width / 2.0) + pan_offset.x; top = center_y - (scaled_height / 2.0) + pan_offset.y; } self.image_view.update(cx, |this, _| { this.container_bounds = Some(bounds); if let Some(initial_zoom_level) = initial_zoom_level { this.zoom_level = initial_zoom_level; } }); let mut image_content = div() .relative() .size_full() .child( div() .absolute() .left(left) .top(top) .w(scaled_width) .h(scaled_height) .child( div() .size_full() .absolute() .top_0() .left_0() .child(div().size_full().bg(checkerboard( cx.theme().colors().panel_background, BASE_SQUARE_SIZE * zoom_level, ))) .border_1() .border_color(border_color), ) .child({ img(image) .id(("image-viewer-image", self.image_view.entity_id())) .size_full() }), ) .into_any_element(); image_content.prepaint_as_root(bounds.origin, bounds.size.into(), window, cx); Some((image_content, is_dragging)) } fn paint( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, _bounds: Bounds<Pixels>, _request_layout: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { let Some((mut element, is_dragging)) = prepaint.take() else { return; }; if is_dragging { let image_view = self.image_view.downgrade(); window.on_mouse_event(move |_event: &MouseUpEvent, phase, _window, cx| { if phase == DispatchPhase::Bubble && let Some(entity) = image_view.upgrade() { entity.update(cx, |this, cx| { this.last_mouse_position = None; cx.notify(); }); } }); } element.paint(window, cx); }} pub enum ImageViewEvent { TitleChanged,} impl EventEmitter<ImageViewEvent> for ImageView {} impl Item for ImageView { type Event = ImageViewEvent; fn to_item_events(event: &Self::Event, f: &mut dyn FnMut(workspace::item::ItemEvent)) { match event { ImageViewEvent::TitleChanged => { f(workspace::item::ItemEvent::UpdateTab); f(workspace::item::ItemEvent::UpdateBreadcrumbs); } } } fn for_each_project_item( &self, cx: &App, f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), ) { f(self.image_item.entity_id(), self.image_item.read(cx)) } fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> { let abs_path = self.image_item.read(cx).abs_path(cx)?; let file_path = abs_path.compact().to_string_lossy().into_owned(); Some(file_path.into()) }Lines 1–500 of 1073. Load more
mod image_info; mod image_viewer_settings; use std::path::Path; use anyhow::Context as _; use editor::{ Editor, EditorEvent, EditorSettings, RevealInFileManager, actions::SelectAll, items::entry_git_aware_label_color, }; use file_icons::FileIcons; use gpui::{ AnyElement, App, Bounds, Context, DispatchPhase, Element, ElementId, Entity, EventEmitter, FocusHandle, Focusable, Font, GlobalElementId, InspectorElementId, InteractiveElement, IntoElement, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, PinchEvent, Pixels, Point, Render, ScrollDelta, ScrollWheelEvent, Style, Styled, Subscription, Task, WeakEntity, Window, actions, checkerboard, div, img, point, px, size, }; use language::File as _; use persistence::ImageViewerDb; use project::{ ImageItem, Project, ProjectPath, git_store::GitStoreEvent, image_store::ImageItemEvent, }; use settings::Settings; use theme_settings::ThemeSettings; use ui::{Tooltip, prelude::*}; use util::paths::PathExt; use workspace::{ ItemId, ItemSettings, Pane, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, delete_unloaded_items, invalid_item_view::InvalidItemView, item::{HighlightedText, Item, ItemHandle, ProjectItem, SerializableItem, TabContentParams}, }; pub use crate::image_info::*; pub use crate::image_viewer_settings::*; actions!( image_viewer, [ /// Zoom in the image. ZoomIn, /// Zoom out the image. ZoomOut, /// Reset zoom to 100%. ResetZoom, /// Fit the image to view. FitToView, /// Zoom to actual size (100%). ZoomToActualSize ] ); const MIN_ZOOM: f32 = 0.1; const MAX_ZOOM: f32 = 20.0; const ZOOM_STEP: f32 = 1.1; const SCROLL_LINE_MULTIPLIER: f32 = 20.0; const BASE_SQUARE_SIZE: f32 = 32.0; const ZOOM_EDITOR_MIN_DIGITS: usize = 3; // Reserve room for common values like 100%. const ZOOM_EDITOR_MAX_DIGITS: usize = 4; // MAX_ZOOM is 2000%. const ZOOM_EDITOR_APPROX_CHAR_WIDTH: f32 = 8.0; // Approximate width of one small UI digit. const ZOOM_EDITOR_HORIZONTAL_PADDING: f32 = 12.0; // Extra room for cursor and editor edge padding. pub struct ImageView { image_item: Entity<ImageItem>, project: Entity<Project>, focus_handle: FocusHandle, zoom_level: f32, pan_offset: Point<Pixels>, last_mouse_position: Option<Point<Pixels>>, container_bounds: Option<Bounds<Pixels>>, image_size: Option<(u32, u32)>, } impl ImageView { fn is_dragging(&self) -> bool { self.last_mouse_position.is_some() } pub fn new( image_item: Entity<ImageItem>, project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { // Start loading the image to render in the background to prevent the view // from flickering in most cases. let _ = image_item.update(cx, |image, cx| { image.image.clone().get_render_image(window, cx) }); cx.subscribe(&image_item, Self::on_image_event).detach(); let git_store = project.read(cx).git_store().clone(); cx.subscribe(&git_store, |_, _, event, cx| { if matches!(event, GitStoreEvent::DiffBaseChanged(_)) { cx.emit(ImageViewEvent::TitleChanged); } }) .detach(); cx.on_release_in(window, |this, window, cx| { let image_data = this.image_item.read(cx).image.clone(); if let Some(image) = image_data.clone().get_render_image(window, cx) { cx.drop_image(image, None); } image_data.remove_asset(cx); }) .detach(); let image_size = image_item .read(cx) .image_metadata .map(|m| (m.width, m.height)); Self { image_item, project, focus_handle: cx.focus_handle(), zoom_level: 1.0, pan_offset: Point::default(), last_mouse_position: None, container_bounds: None, image_size, } } fn on_image_event( &mut self, _: Entity<ImageItem>, event: &ImageItemEvent, cx: &mut Context<Self>, ) { match event { ImageItemEvent::MetadataUpdated | ImageItemEvent::FileHandleChanged | ImageItemEvent::Reloaded => { self.image_size = self .image_item .read(cx) .image_metadata .map(|m| (m.width, m.height)); cx.emit(ImageViewEvent::TitleChanged); cx.notify(); } ImageItemEvent::ReloadNeeded => {} } } fn zoom_in(&mut self, _: &ZoomIn, _window: &mut Window, cx: &mut Context<Self>) { self.set_zoom(self.zoom_level * ZOOM_STEP, None, cx); } fn zoom_out(&mut self, _: &ZoomOut, _window: &mut Window, cx: &mut Context<Self>) { self.set_zoom(self.zoom_level / ZOOM_STEP, None, cx); } fn reset_zoom(&mut self, _: &ResetZoom, _window: &mut Window, cx: &mut Context<Self>) { self.zoom_level = 1.0; self.pan_offset = Point::default(); cx.notify(); } fn fit_to_view(&mut self, _: &FitToView, _window: &mut Window, cx: &mut Context<Self>) { if let Some((bounds, image_size)) = self.container_bounds.zip(self.image_size) { self.zoom_level = ImageView::compute_fit_to_view_zoom(bounds, image_size); self.pan_offset = Point::default(); cx.notify(); } } fn compute_fit_to_view_zoom(container_bounds: Bounds<Pixels>, image_size: (u32, u32)) -> f32 { let (image_width, image_height) = image_size; let container_width: f32 = container_bounds.size.width.into(); let container_height: f32 = container_bounds.size.height.into(); let scale_x = container_width / image_width as f32; let scale_y = container_height / image_height as f32; scale_x.min(scale_y).min(1.0) } fn zoom_to_actual_size( &mut self, _: &ZoomToActualSize, _window: &mut Window, cx: &mut Context<Self>, ) { self.zoom_level = 1.0; self.pan_offset = Point::default(); cx.notify(); } fn reveal_in_file_manager( &mut self, _: &RevealInFileManager, _window: &mut Window, cx: &mut Context<Self>, ) { if let Some(path) = self.image_item.read(cx).abs_path(cx) { self.project .update(cx, |project, cx| project.reveal_path(&path, cx)); } } fn set_zoom( &mut self, new_zoom: f32, zoom_center: Option<Point<Pixels>>, cx: &mut Context<Self>, ) { let old_zoom = self.zoom_level; self.zoom_level = new_zoom.clamp(MIN_ZOOM, MAX_ZOOM); if let Some((center, bounds)) = zoom_center.zip(self.container_bounds) { let relative_center = point( center.x - bounds.origin.x - bounds.size.width / 2.0, center.y - bounds.origin.y - bounds.size.height / 2.0, ); let mouse_offset_from_image = relative_center - self.pan_offset; let zoom_ratio = self.zoom_level / old_zoom; self.pan_offset += mouse_offset_from_image * (1.0 - zoom_ratio); } cx.notify(); } fn handle_scroll_wheel( &mut self, event: &ScrollWheelEvent, _window: &mut Window, cx: &mut Context<Self>, ) { if event.modifiers.control || event.modifiers.platform { let delta: f32 = match event.delta { ScrollDelta::Pixels(pixels) => pixels.y.into(), ScrollDelta::Lines(lines) => lines.y * SCROLL_LINE_MULTIPLIER, }; let zoom_factor = if delta > 0.0 { 1.0 + delta.abs() * 0.01 } else { 1.0 / (1.0 + delta.abs() * 0.01) }; self.set_zoom(self.zoom_level * zoom_factor, Some(event.position), cx); } else { let delta = match event.delta { ScrollDelta::Pixels(pixels) => pixels, ScrollDelta::Lines(lines) => lines.map(|d| px(d * SCROLL_LINE_MULTIPLIER)), }; self.pan_offset += delta; cx.notify(); } } fn handle_mouse_down( &mut self, event: &MouseDownEvent, _window: &mut Window, cx: &mut Context<Self>, ) { if event.button == MouseButton::Left || event.button == MouseButton::Middle { self.last_mouse_position = Some(event.position); cx.notify(); } } fn handle_mouse_up( &mut self, _event: &MouseUpEvent, _window: &mut Window, cx: &mut Context<Self>, ) { self.last_mouse_position = None; cx.notify(); } fn handle_mouse_move( &mut self, event: &MouseMoveEvent, _window: &mut Window, cx: &mut Context<Self>, ) { if self.is_dragging() { if let Some(last_pos) = self.last_mouse_position { let delta = event.position - last_pos; self.pan_offset += delta; } self.last_mouse_position = Some(event.position); cx.notify(); } } fn handle_pinch(&mut self, event: &PinchEvent, _window: &mut Window, cx: &mut Context<Self>) { let zoom_factor = 1.0 + event.delta; self.set_zoom(self.zoom_level * zoom_factor, Some(event.position), cx); } } struct ImageContentElement { image_view: Entity<ImageView>, } impl ImageContentElement { fn new(image_view: Entity<ImageView>) -> Self { Self { image_view } } } impl IntoElement for ImageContentElement { type Element = Self; fn into_element(self) -> Self::Element { self } } impl Element for ImageContentElement { type RequestLayoutState = (); type PrepaintState = Option<(AnyElement, bool)>; fn id(&self) -> Option<ElementId> { None } fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { None } fn request_layout( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { ( window.request_layout( Style { size: size(relative(1.).into(), relative(1.).into()), ..Default::default() }, [], cx, ), (), ) } fn prepaint( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, bounds: Bounds<Pixels>, _request_layout: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App, ) -> Self::PrepaintState { let image_view = self.image_view.read(cx); let image = image_view.image_item.read(cx).image.clone(); let first_layout = image_view.container_bounds.is_none(); let initial_zoom_level = first_layout .then(|| { image_view .image_size .map(|image_size| ImageView::compute_fit_to_view_zoom(bounds, image_size)) }) .flatten(); let zoom_level = initial_zoom_level.unwrap_or(image_view.zoom_level); let pan_offset = image_view.pan_offset; let border_color = cx.theme().colors().border; let is_dragging = image_view.is_dragging(); let scaled_size = image_view .image_size .map(|(w, h)| (px(w as f32 * zoom_level), px(h as f32 * zoom_level))); let (mut left, mut top) = (px(0.0), px(0.0)); let mut scaled_width = px(0.0); let mut scaled_height = px(0.0); if let Some((width, height)) = scaled_size { scaled_width = width; scaled_height = height; let center_x = bounds.size.width / 2.0; let center_y = bounds.size.height / 2.0; left = center_x - (scaled_width / 2.0) + pan_offset.x; top = center_y - (scaled_height / 2.0) + pan_offset.y; } self.image_view.update(cx, |this, _| { this.container_bounds = Some(bounds); if let Some(initial_zoom_level) = initial_zoom_level { this.zoom_level = initial_zoom_level; } }); let mut image_content = div() .relative() .size_full() .child( div() .absolute() .left(left) .top(top) .w(scaled_width) .h(scaled_height) .child( div() .size_full() .absolute() .top_0() .left_0() .child(div().size_full().bg(checkerboard( cx.theme().colors().panel_background, BASE_SQUARE_SIZE * zoom_level, ))) .border_1() .border_color(border_color), ) .child({ img(image) .id(("image-viewer-image", self.image_view.entity_id())) .size_full() }), ) .into_any_element(); image_content.prepaint_as_root(bounds.origin, bounds.size.into(), window, cx); Some((image_content, is_dragging)) } fn paint( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, _bounds: Bounds<Pixels>, _request_layout: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { let Some((mut element, is_dragging)) = prepaint.take() else { return; }; if is_dragging { let image_view = self.image_view.downgrade(); window.on_mouse_event(move |_event: &MouseUpEvent, phase, _window, cx| { if phase == DispatchPhase::Bubble && let Some(entity) = image_view.upgrade() { entity.update(cx, |this, cx| { this.last_mouse_position = None; cx.notify(); }); } }); } element.paint(window, cx); } } pub enum ImageViewEvent { TitleChanged, } impl EventEmitter<ImageViewEvent> for ImageView {} impl Item for ImageView { type Event = ImageViewEvent; fn to_item_events(event: &Self::Event, f: &mut dyn FnMut(workspace::item::ItemEvent)) { match event { ImageViewEvent::TitleChanged => { f(workspace::item::ItemEvent::UpdateTab); f(workspace::item::ItemEvent::UpdateBreadcrumbs); } } } fn for_each_project_item( &self, cx: &App, f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), ) { f(self.image_item.entity_id(), self.image_item.read(cx)) } fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> { let abs_path = self.image_item.read(cx).abs_path(cx)?; let file_path = abs_path.compact().to_string_lossy().into_owned(); Some(file_path.into()) }