Simple image viewer with Vala and GTK4

215 views Asked by At

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 :)

0

There are 0 answers