Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

image_viewer: Add ability to zoom and pan images #24547

Closed
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: add zoom controls to status bar
  • Loading branch information
kaf-lamed-beyt committed Feb 10, 2025
commit 80f06f170b69d0a06760d6959fa621124a3a49c2
57 changes: 39 additions & 18 deletions crates/image_viewer/src/image_viewer.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod image_info;
mod image_viewer_settings;
pub mod zoom_controls;

use std::path::PathBuf;

@@ -72,6 +73,38 @@ impl ImageView {
ImageItemEvent::ReloadNeeded => {}
}
}

pub fn update_zoom(&mut self, new_zoom: f32, center: Point<Pixels>, cx: &mut Context<Self>) {
let content_size = self.content_size(cx);
let scaled_width = content_size.width.0 * new_zoom;
let scaled_height = content_size.height.0 * new_zoom;

self.pan_offset = Point {
x: center.x - px(scaled_width),
y: center.y - px(scaled_height),
};
self.zoom_level = new_zoom;
cx.refresh_windows();
}

pub fn reset_view(&mut self, window_size: Size<Pixels>, cx: &mut Context<Self>) {
let center = Point::new(window_size.width, window_size.height);
self.zoom_level = 1.0;
self.pan_offset = center;
cx.refresh_windows();
}

fn content_size(&self, cx: &mut Context<Self>) -> Size<Pixels> {
self.image_item
.read(cx)
.image_metadata
.as_ref()
.map(|metadata| Size {
width: px(metadata.width as f32),
height: px(metadata.height as f32),
})
.unwrap_or_default()
}
}

pub enum ImageViewEvent {
@@ -293,6 +326,7 @@ impl Render for ImageView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let image = self.image_item.read(cx).image.clone();
let metadata = self.image_item.read(cx).image_metadata.as_ref();
let bounds = window.bounds();

let (rendered_width, rendered_height) = if let Some(meta) = metadata {
(
@@ -304,21 +338,9 @@ impl Render for ImageView {
};

if self.initial_layout.is_none() {
if let Some(meta) = self.image_item.read(cx).image_metadata.as_ref() {
let container_size = window.bounds().size;
let image_width = px(meta.width as f32);
let image_height = px(meta.height as f32);

let scale_width = container_size.width / image_width;
let scale_height = container_size.height / image_height;
let initial_zoom = scale_width.min(scale_height).min(px(1.0).into());

self.pan_offset = Point {
x: (container_size.width - (image_width * initial_zoom)) / 2.0,
y: (container_size.height - (image_height * initial_zoom)) / 2.0,
};
self.zoom_level = initial_zoom;
self.initial_layout = Some(container_size);
if let Some(_) = self.image_item.read(cx).image_metadata.as_ref() {
self.reset_view(bounds.size, cx);
self.initial_layout = Some(window.bounds().size);
}
}

@@ -414,7 +436,7 @@ impl Render for ImageView {
)
.on_mouse_up(
MouseButton::Left,
cx.listener(|this: &mut ImageView, event: &MouseUpEvent, _, cx| {
cx.listener(|this: &mut ImageView, _: &MouseUpEvent, _, cx| {
this.is_panning = false;
this.last_mouse_position = None;
cx.refresh_windows();
@@ -431,9 +453,8 @@ impl Render for ImageView {
let old_zoom = this.zoom_level;
let new_zoom = (old_zoom * (1.0 + delta * sensitivity)).clamp(0.1, 10.0);

if let Some(meta) = this.image_item.read(cx).image_metadata.as_ref() {
if let Some(_) = this.image_item.read(cx).image_metadata.as_ref() {
let mouse_pos = event.position;

let image_x = (mouse_pos.x - this.pan_offset.x) / old_zoom;
let image_y = (mouse_pos.y - this.pan_offset.y) / old_zoom;

123 changes: 123 additions & 0 deletions crates/image_viewer/src/zoom_controls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use crate::ImageView;
use gpui::{div, IntoElement, ParentElement, Point, Render, Size, Subscription, WeakEntity};
use ui::prelude::*;
use ui::{IconName, Tooltip};
use workspace::{ItemHandle, StatusItemView, Workspace};

pub struct ZoomControls {
zoom_level: f32,
active_image_view: Option<WeakEntity<ImageView>>,
_observe_active_image: Option<Subscription>,
}

impl ZoomControls {
pub fn new(_workspace: &Workspace) -> Self {
Self {
zoom_level: 1.0,
active_image_view: None,
_observe_active_image: None,
}
}

fn update_zoom_level(&mut self, cx: &mut Context<Self>) {
if let Some(image_view) = self.active_image_view.as_ref().and_then(|v| v.upgrade()) {
let current_zoom = image_view.read(cx).zoom_level;
if (self.zoom_level - current_zoom).abs() > f32::EPSILON {
self.zoom_level = current_zoom;
cx.notify();
}
}
}

fn handle_zoom_in(&mut self, window_size: Size<Pixels>, cx: &mut Context<Self>) {
if let Some(image_view) = self.active_image_view.as_ref().and_then(|v| v.upgrade()) {
image_view.update(cx, |view, cx| {
let new_zoom = (view.zoom_level * 1.2).clamp(0.1, 10.0);
let center = Point::new(window_size.width / 2.0, window_size.height / 2.0);
view.update_zoom(new_zoom, center, cx);
});
}
}

fn handle_zoom_out(&mut self, window_size: Size<Pixels>, cx: &mut Context<Self>) {
if let Some(image_view) = self.active_image_view.as_ref().and_then(|v| v.upgrade()) {
image_view.update(cx, |view, cx| {
let new_zoom = (view.zoom_level / 1.2).clamp(0.1, 10.0);
let center = Point::new(window_size.width / 2.0, window_size.height / 2.0);
view.update_zoom(new_zoom, center, cx);
});
}
}

fn handle_reset(&mut self, window_size: Size<Pixels>, cx: &mut Context<Self>) {
if let Some(image_view) = self.active_image_view.as_ref().and_then(|v| v.upgrade()) {
image_view.update(cx, |view, cx| {
view.reset_view(window_size, cx);
});
}
}
}

impl Render for ZoomControls {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let bounds = window.bounds();

match self.active_image_view.as_ref().and_then(|v| v.upgrade()) {
Some(_) => {
div()
.flex()
.gap_2()
.child(
IconButton::new("zoom-out", IconName::Dash)
.icon_size(IconSize::XSmall)
.tooltip(Tooltip::text("Click to Zoom Out"))
.on_click(cx.listener(move |this, _, _, cx| {
this.handle_zoom_out(bounds.size, cx)
})),
)
.child(
Button::new("zoom-level", format!("{:.0}%", self.zoom_level * 100.0))
.label_size(LabelSize::Small),
)
.child(
IconButton::new("zoom-in", IconName::Plus)
.icon_size(IconSize::XSmall)
.tooltip(Tooltip::text("Click to Zoom In"))
.on_click(cx.listener(move |this, _, _, cx| {
this.handle_zoom_in(bounds.size, cx)
})),
)
.child(
IconButton::new("reset-zoom", IconName::RotateCcw)
.icon_size(IconSize::XSmall)
.tooltip(Tooltip::text("Click to reset"))
.on_click(cx.listener(move |this, _, _, cx| {
this.handle_reset(bounds.size, cx)
})),
)
}
None => div(),
}
}
}

impl StatusItemView for ZoomControls {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.active_image_view = active_pane_item
.and_then(|item| item.act_as::<ImageView>(cx))
.map(|view| view.downgrade());

if let Some(image_view) = self.active_image_view.as_ref().and_then(|v| v.upgrade()) {
self._observe_active_image = Some(cx.observe(&image_view, |this, _, cx| {
this.update_zoom_level(cx);
}));
}

self.update_zoom_level(cx);
}
}
4 changes: 3 additions & 1 deletion crates/zed/src/zed.rs
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ use gpui::{
Entity, Focusable, KeyBinding, MenuItem, ParentElement, PathPromptOptions, PromptLevel,
ReadGlobal, SharedString, Styled, Task, TitlebarOptions, Window, WindowKind, WindowOptions,
};
use image_viewer::ImageInfo;
use image_viewer::{zoom_controls, ImageInfo};
pub use open_listener::*;
use outline_panel::OutlinePanel;
use paths::{local_settings_file_relative_path, local_tasks_file_relative_path};
@@ -204,6 +204,7 @@ pub fn initialize_workspace(
cx.new(|cx| toolchain_selector::ActiveToolchain::new(workspace, window, cx));
let vim_mode_indicator = cx.new(|cx| vim::ModeIndicator::new(window, cx));
let image_info = cx.new(|_cx| ImageInfo::new(workspace));
let zoom_controls = cx.new(|_cx| zoom_controls::ZoomControls::new(workspace));
let cursor_position =
cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
workspace.status_bar().update(cx, |status_bar, cx| {
@@ -215,6 +216,7 @@ pub fn initialize_workspace(
status_bar.add_right_item(vim_mode_indicator, window, cx);
status_bar.add_right_item(cursor_position, window, cx);
status_bar.add_right_item(image_info, window, cx);
status_bar.add_right_item(zoom_controls, window, cx);
});

let handle = cx.entity().downgrade();
Loading