1073 lines · 34.8 KB · Rust
RawDownload
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.