bohonghuang / cl-gtk4

GTK4/Libadwaita/WebKit2 bindings for Common Lisp.
GNU Lesser General Public License v3.0
216 stars 9 forks source link

Working with GTK Picture objects #33

Closed jonathanabennett closed 1 year ago

jonathanabennett commented 1 year ago

I need to add images to my game. In cl-cffi-gtk, they did this via PixBufs but it seems like the Picture Class is the way I need to go. But I cannot get it to load. Here is my current code

  (let ((frame (gtk:make-frame :label (cu/full-name u)))
        (statblock (gtk:make-box :orientation gtk:+orientation-vertical+ :spacing 5))
        (img (gtk:make-picture :filename (namestring (cu/display u)))))
    (setf (gtk:frame-child frame) statblock)
    (gtk:box-append statblock img)
    frame)

frame is rendered in another function. This same code works when I replace gtk:make-picture with gtk:make-image, but it seems like make-image is for displaying images at a fixed size, not at natural size (which is what I want).

EDIT: Extraneous code rendering labels and other rows of this stat block have been removed. But the code fails whether they are commented out or not. And I am sure that (cu/display u) contains the path to the correct file because gtk:make-image puts a very tiny rendering of that image inside the frame whereas gtk:make-picture does not.

bohonghuang commented 1 year ago

I suspect that your pathname contains a ~ that cannot be expanded by GTK. To address this, you need to use (namestring (truename (cu/display u))) to expand it on the Lisp side.

jonathanabennett commented 1 year ago

That didn't help either, and when I (format t "~a" (cu/display u)), I get the full path /Users/jonathanbennett/.roswell/lisp/quicklisp/local-projects/megastrike/data/images/units/vehicles/Pumapat.png

bohonghuang commented 1 year ago

Can you load cl-gdk4 and try this in your application?

(let ((texture (gdk:make-texture :path "/path/to/your/image/file")))
  (format t "WIDTH: ~A HEIGHT: ~A~%" (gdk:texture-width texture) (gdk:texture-height texture)))

The function gdk:make-texture should signal an error when it fails to open the file. Once the texture is loaded successfully, you can create a picture with (gtk:make-picture :paintable texture).

jonathanabennett commented 1 year ago

the above snippet accurately reports the size of the image (84x72 px), but the image is still not rendering. It is a png file, is the a possible reason for the error?

EDIT: Using gtk:make-image once more renders it (when using a texture or a file). This makes me wonder if I need to do something to set the size of the image when using a picture, but I do not see anything that I could change to set the picture size.

bohonghuang commented 1 year ago

Have you tried a minimal example like this?

(define-application (:name picture-test
                     :id "org.bohonghuang.gtk4.issue-33")
  (define-main-window (window (make-application-window :application *application*))
    (setf (window-title window) "Picture Test"
          (window-child window) (make-picture :filename "/path/to/your/image/file"))
    (unless (widget-visible-p window)
      (window-present window))))

The snippet is tested to run on both Windows and GNU/Linux, and the size of the picture defaults to the dimensions of the image file.

jonathanabennett commented 1 year ago

The minimum example you just posted works on MacOS as well (just tested it now). Is it possible that you can't have a picture inside a box or inside a ListBoxChild?

bohonghuang commented 1 year ago

The minimum size of Picture defaults to zeros, so you could use (setf (widget-size-request picture) '(100 100)) to set the size manually after creating a picture.

jonathanabennett commented 1 year ago

Ok, I've got that working. I'm struggling to figure out how I could draw the picture in a drawing area.

EDIT: Ok, here is the code that has gotten me the closest to managing to draw the images on the screen.

(let ((unit (function-that-returns-nil-or-the-occupant-of-a-hex)))
    (when unit
      (with-gdk-rgba (color "#000000")
        (gdk:cairo-set-source-rgba cr color)
        (gdk:cairo-set-source-pixbuf cr
                                     (gdk:pixbuf-get-from-texture (cu/display unit)) ;; cu/display unit returns a GDK:Texture
                                     (point-x (nth 5 hex-points))
                                     (point-y (nth 5 hex-points))))
      cairo:stroke)))

In the screenshot attached, there should be an image inside the top-left hex (the one that would be labeled 0101, but instead it is completely empty. It put something there, because the text telling you the hex number is gone from that hex, but whatever is there is invisible.

Screenshot 2023-10-10 at 8 12 15

jonathanabennett commented 1 year ago

I have rewritten this down to the smallest possible code I can envision might do this:

(defun draw-test-window ()
  (let ((window (gtk:make-application-window :application gtk:*application*))
        (picture (gtk:make-drawing-area)))
    (setf (gtk:window-child window) picture
          (gtk:drawing-area-content-width picture) 500
          (gtk:drawing-area-content-height picture) 500
          (gtk:drawing-area-draw-func picture) (list (cffi:callback %draw-func)
                                                    (cffi:null-pointer)
                                                    (cffi:null-pointer)))
    (unless (gtk:widget-visible-p window)
      (gtk:window-present window))))

(defun draw-func (area cr width height)
  (declare (ignore area)
           (optimize (speed 3)
                     (debug 0)
                     (safety 0)))
  (with-gdk-rgba (color "#000000")
    (let ((texture (gdk:make-texture :path (namestring (merge-pathnames "images/units/mechs/Akuma.png" *data-folder*)))))
      (gdk:cairo-set-source-pixbuf cr
                                   (gdk:pixbuf-get-from-texture texture)
                                   (coerce (the fixnum 100) 'double-float)
                                   (coerce (the fixnum 100) 'double-float))
      (cairo:fill-path))))

Having (cairo:stroke) as the final line also does not draw anything on the screen. In both cases, I get a blank white screen which is 500 by 500 pixels. I expected the image to be displayed 100 pixel down from the top and over from the left.

EDIT:

Sorry, I forgot to include the callback function, but this was copy/pasted from your Cairo demo so I suspect you're familiar with it.

(cffi:defcallback %draw-func :void ((area :pointer)
                                    (cr :pointer)
                                    (width :int)
                                    (height :int)
                                    (data :pointer))
  (declare (ignore data))
  (let ((cairo:*context* (make-instance 'cairo:context
                                        :pointer cr
                                        :width width
                                        :height height
                                        :pixel-based-p nil)))
    (draw-func (make-instance 'gir::object-instance
                              :class (gir:nget gtk:*ns* "DrawingArea")
                              :this area)
               (make-instance 'gir::struct-instance
                              :class (gir:nget megastrike::*ns* "Context")
                              :this cr)
               width height)))
bohonghuang commented 1 year ago

Having (cairo:stroke) as the final line also does not draw anything on the screen. In both cases, I get a blank white screen which is 500 by 500 pixels. I expected the image to be displayed 100 pixel down from the top and over from the left.

You haven't set a path using functions like cairo:rectangle, so there won't be anything drawn. The following code can help you draw your image in the center of the canvas.

(defun draw-func (area cr canvas-width canvas-height)
    (declare (ignore area)
             (optimize (speed 3)
                       (debug 0)
                       (safety 0)))
  (let ((texture (gdk:make-texture :path (namestring (truename (merge-pathnames "images/units/mechs/Akuma.png" *data-folder*))))))
    (let* ((canvas-width (coerce (the fixnum canvas-width) 'single-float))
           (canvas-height (coerce (the fixnum canvas-height) 'single-float))
           (width (coerce (the fixnum (gdk:texture-width texture)) 'single-float))
           (height (coerce (the fixnum (gdk:texture-height texture)) 'single-float))
           (x (/ (- canvas-width width) 2.0))
           (y (/ (- canvas-height width) 2.0)))
      (cairo:rectangle x y width height)
      (gdk:cairo-set-source-pixbuf cr (gdk:pixbuf-get-from-texture texture) (coerce x 'double-float) (coerce y 'double-float))
      (cairo:fill-path)
      (with-gdk-rgba (color "#FF0000")
        (cairo:rectangle x y width height)
        (gdk:cairo-set-source-rgba cr color)
        (cairo:set-line-width 3.0)
        (cairo:stroke)))))

where x and y are the coordinates of the top-left corner of the image, you can change them to position the image wherever you want it to be drawn.

jonathanabennett commented 1 year ago

Ok, got that part working within my program. The problem now is a matter of scaling. Based on reading the GDK Documentation and a few examples in Python, it appears I need to use GdkPixbuf.Pixbuf.scale_simple, but I can't find it in GDK4. If I use (cairo:scale), it scales all the coordinates (obviously), meaning the x and y origin for the image shifts.

bohonghuang commented 1 year ago

Okay, previously there were no bindings generated for GdkPixbuf because I thought it had fewer use cases. Now, I have added it in commit 6ab0131af4e3007032053fb8eb807a3456a9fe3d. Please upgrade to use it.

jonathanabennett commented 1 year ago

Next question. The images I have are all black and white. I'd like to mark which team they are on by painting the unit the team's color (which I've got in the format returned by gdk:rgba-to-string). I looked at GdkPixbuf.Pixbuf.composite-color-simple, but it wants a GUINT32 rather than an RGBA color structure. Should I be controlling this in Cairo?

bohonghuang commented 1 year ago
  1. You can directly provide the color value in the format #xRRGGBBAA.
  2. If you are familiar with cffi, the conversion between GdkRGBA and the RGBA8888 should not be difficult:
    
    (cffi:defcstruct gdk-rgba
    (red :float)
    (green :float)
    (blue :float)
    (alpha :float))

(cffi:defcstruct rgba8888 (red :uint8) (green :uint8) (blue :uint8) (alpha :uint8))

(defun rgba-color-value (rgba) (cffi:with-foreign-slots (((r1 red) (g1 green) (b1 blue) (a1 alpha)) (gobj:object-pointer rgba) (:struct gdk-rgba)) (cffi:with-foreign-object (pointer '(:struct rgba8888)) (cffi:with-foreign-slots (((r2 red) (g2 green) (b2 blue) (a2 alpha)) pointer (:struct rgba8888)) (setf r2 (round ( r1 #xFF)) g2 (round ( g1 #xFF)) b2 (round ( b1 #xFF)) a2 (round ( a1 #xFF)))) (cffi:mem-ref pointer :uint32))))

(with-gdk-rgba (rgba "#FF0000FF") (assert (= (rgba-color-value rgba) #xFF0000FF)))