I'm trying to code a very simple image viewer component with zoom and pan features. My code looks like this so far:
namespace Image {
public class ImageViewerPanningArea : Gtk.Widget {
construct {
set_layout_manager(new Gtk.BinLayout());
vexpand = hexpand = true;
}
private ImageViewer viewer;
private Gtk.ScrolledWindow scrolled_window;
private bool mouse_pressed = false;
private double last_mouse_x;
private double last_mouse_y;
public ImageViewerPanningArea(ImageViewer viewer) {
this.viewer = viewer;
// var dimensions = viewer.get_dimensions ();
this.scrolled_window = new Gtk.ScrolledWindow ();
scrolled_window.set_parent (this);
scrolled_window.set_child (viewer);
scrolled_window.set_policy (Gtk.PolicyType.ALWAYS, Gtk.PolicyType.ALWAYS);
this.scrolled_window.notify["width"].connect(() => {
int width = scrolled_window.get_width();
int height = scrolled_window.get_height();
stdout.printf("Scrolled Window Resized: %d x %d\n", width, height);
// Handle the resizing...
});
var button_controller = new Gtk.GestureClick ();
scrolled_window.add_controller(button_controller);
button_controller.pressed.connect((button, x, y) => {
if (button == 1) {
mouse_pressed = true;
last_mouse_x = x;
last_mouse_y = y;
}
});
button_controller.released.connect((button, x, y) => {
if (button == 1) {
mouse_pressed = false;
}
});
var motion_controller = new Gtk.EventControllerMotion ();
scrolled_window.add_controller (motion_controller);
motion_controller.motion.connect((x, y) => {
if (mouse_pressed) {
double dx = x - last_mouse_x;
double dy = y - last_mouse_y;
scrolled_window.hadjustment.set_value(scrolled_window.hadjustment.get_value() - dx);
scrolled_window.vadjustment.set_value(scrolled_window.vadjustment.get_value() - dy);
last_mouse_x = x;
last_mouse_y = y;
scrolled_window.queue_draw ();
}
});
var key_controller = new Gtk.EventControllerKey();
scrolled_window.add_controller (key_controller);
key_controller.key_pressed.connect((keyval, keycode, state) => {
if (keyval == Gdk.Key.Control_L || keyval == Gdk.Key.Control_R) {
stdout.printf ("AAAAAAAAAAAAAAAAAAAAAAAAAAa\n");
}
return false;
});
// viewer.dimensions_changed.connect(update_adjustments);
// viewer.allocated_size.connect(update_adjustments);
}
// private void update_adjustments(int width, int height) {
// scrolled_window.hadjustment.set_page_size(width);
// scrolled_window.vadjustment.set_page_size(height);
// }
public override void size_allocate (int width, int height, int baseline) {
base.size_allocate(width, height, baseline);
stdout.printf ("changed size to: %d x %d\n", width, height);
}
protected override void snapshot (Gtk.Snapshot sn) {
base.snapshot(sn);
double container_width = get_width();
double container_height = get_height();
double image_current_width = viewer.natural_width * viewer.zoom;
double image_current_height = viewer.natural_height * viewer.zoom;
double width_ratio = container_width / image_current_width;
double height_ratio = container_height / image_current_height;
// By default, we don't want to upscale
double new_scale = 1.0;
if (width_ratio < 1.0 || height_ratio < 1.0) {
new_scale = (height_ratio < width_ratio) ? height_ratio : width_ratio;
}
viewer.resize_scale = new_scale;
stdout.printf ("changed size to: %d x %d\n", get_width(), get_height());
}
}
protected struct ImageDimensions {
int width;
int height;
}
public class ImageViewer : Gtk.DrawingArea {
public signal void dimensions_changed(ImageDimensions new_dimensions);
public signal void zoom_changed(double new_zoom_value);
public signal void allocated_size(int width, int height);
private const double ZOOM_TICK = 0.1;
private Gdk.Pixbuf pixbuf;
private Gdk.Texture texture;
public int natural_width {
get;
private set;
}
public int natural_height {
get;
private set;
}
internal double resize_scale = 1.0;
private double zoom_level = 1.0;
public double zoom {
get { return zoom_level; }
set {
zoom_level = value;
this.queue_resize();
this.queue_draw();
zoom_changed(value);
dimensions_changed(get_dimensions());
}
}
construct {
hexpand = vexpand = true;
}
public ImageViewer(Gdk.Pixbuf pixbuf) {
this.pixbuf = pixbuf;
this.texture = Gdk.Texture.for_pixbuf (pixbuf);
this.natural_width = pixbuf.get_width ();
this.natural_height = pixbuf.get_height ();
}
public void zoom_in() {
zoom += ZOOM_TICK;
}
public void zoom_out() {
if (zoom > ZOOM_TICK) {
zoom -= ZOOM_TICK;
}
}
protected override void snapshot(Gtk.Snapshot snapshot) {
if (texture == null) {
return;
}
int width = get_allocated_width();
int height = get_allocated_height();
draw_checker_board(snapshot, width, height);
int scaled_width = (int)(texture.get_width() * zoom * resize_scale);
int scaled_height = (int)(texture.get_height() * zoom * resize_scale);
int x_offset = (width - scaled_width) / 2;
int y_offset = (height - scaled_height) / 2;
var rect = Graphene.Rect();
rect.init(x_offset, y_offset, scaled_width, scaled_height);
snapshot.append_texture(texture, rect);
}
private void draw_checker_board (Gtk.Snapshot snapshot, int width, int height) {
// Define the size of each square in the checkerboard
int square_size = 20; // You can adjust this value as needed
// Colors for the checkerboard
Gdk.RGBA color1 = Gdk.RGBA() { red = 0.8f, green = 0.8f, blue = 0.8f, alpha = 0.6f }; // light gray
Gdk.RGBA color2 = Gdk.RGBA() { red = 0.6f, green = 0.6f, blue = 0.6f, alpha = 0.6f }; // darker gray
bool useFirstColor;
for (int x = 0; x < width; x += square_size) {
useFirstColor = (x / square_size) % 2 == 0; // Alternate starting color for each row
for (int y = 0; y < height; y += square_size) {
var rect = Graphene.Rect();
rect.init(x, y, square_size, square_size);
snapshot.append_color(useFirstColor ? color1 : color2, rect);
// Alternate the color for the next square in the row
useFirstColor = !useFirstColor;
}
}
}
protected override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) {
if (pixbuf == null) {
minimum = natural = 0;
minimum_baseline = natural_baseline = -1; // Explicitly set to -1.
return;
}
var dimensions = get_dimensions();
stdout.printf("dimensions width = %d, height = %d \n", dimensions.width, dimensions.height);
if (orientation == Gtk.Orientation.HORIZONTAL) {
minimum = natural = dimensions.width;
} else {
minimum = natural = dimensions.height;
}
minimum_baseline = natural_baseline = -1; // Explicitly set to -1.
}
public ImageDimensions get_dimensions () {
var width = (int)(texture.get_width () * zoom_level * resize_scale);
var height = (int)(texture.get_height () * zoom_level * resize_scale);
stdout.printf("width = %d, height = %d \n", width, height);
return {
width: width,
height: height
};
}
}
}
And you can use it like this:
var pixbuf = new Gdk.Pixbuf.from_file("cat1.jpg");
var viewer = new Image.ImageViewer(pixbuf);
// viewer.zoom = 2;
var viewer_pan = new Image.ImageViewerPanningArea(viewer);
box.append (viewer_pan);
I'm trying to basically get the similar behavior like this app: https://github.com/elementary/photos but in very simplified manner.
The problem I'm having right now is I cannot make the image shrink together with the parent container :( I'm doing circles right now and running out of ideas already...
I appreaciate any help :)