bohonghuang / cl-gtk4

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

Could you make the library interface more declarative? #39

Open Filipp-Druan opened 1 year ago

Filipp-Druan commented 1 year ago

Hello! I'm comparing application code on your library and on CL-CFFI-GTK4. The second library provides a more declarative interface. For example:

(defun do-level-bar (&optional application)
  (let* ((vbox (make-instance 'gtk:box
                              :orientation :vertical
                              :margin-top 12
                              :margin-bottom 12
                              :margin-start 12
                              :margin-end 12
                              :spacing 12))
         (window (make-instance 'gtk:window
                                :title "Level bar"
                                :child vbox
                                :application application
                                :default-width 420
                                :default-height 240))
         (adj (make-instance 'gtk:adjustment
                             :value 0.0
                             :lower 0.0
                             :upper 10.0
                             :step-increment 0.1))
         (scale (make-instance 'gtk:scale
                               :orientation :horizontal
                               :digits 1
                               :value-pos :top
                               :draw-value t
                               :adjustment adj))
         (levelbar1 (create-level-bar :horizontal :continuous))
         (levelbar2 (create-level-bar :horizontal :discrete)))
    ;; Bind adjustment value for the scale to the level bar values
    (g:object-bind-property adj "value" levelbar1 "value" :default)
    (g:object-bind-property adj "value" levelbar2 "value" :default)
    ;; Pack and show the widgets
    (gtk:box-append vbox (make-instance 'gtk:label
                                        :xalign 0.0
                                        :use-markup t
                                        :label "<b>Continuous mode</b>"))
    (gtk:box-append vbox levelbar1)
    (gtk:box-append vbox (make-instance 'gtk:label
                                        :xalign 0.0
                                        :use-markup t
                                        :label "<b>Discrete mode</b>"))
    (gtk:box-append vbox levelbar2)
    (gtk:box-append vbox (make-instance 'gtk:label
                                        :use-markup t
                                        :xalign 0.0
                                        :label "<b>Change value</b>"))
    (gtk:box-append vbox scale)
    (gtk:widget-show window)))

Your code, sorry for the directness, merges into one whole when you look at it. It is very difficult to distinguish one part from another. I wish the code was more like a tree:

(defun main ()
    (ltk:with-ltk ()
        (let* (
               (control-frame (make-instance 'ltk:frame :width 100
                                                        :height 100))
               (text-frame (make-instance 'ltk:frame :width 100
                                                     :height 100))

               (speed-spinbox (make-instance 'ltk:spinbox
                                             :from 0 :to 10000
                                             :master control-frame))

               (text-field (make-instance 'ltk:scrolled-text :master text-frame
                                                             :width 1 
                                                             :height 1)) 
               (load-button  (make-instance 'ltk:button
                                            :text "Load"
                                            :master control-frame
                                            :command #'(lambda ()
                                                           (let* ((path-to-space-config (ltk:get-open-file :filetypes '(("S-expression" ".sxp"))))
                                                                  (space-config (if (equal path-to-space-config "")
                                                                                    nil
                                                                                    (alexandria:read-file-into-string path-to-space-config))))
                                                               (when space-config
                                                                   (setf (ltk:text text-field)
                                                                         space-config)))))) 

               (start-button (make-instance 'ltk:button
                                            :text "Start"
                                            :master control-frame
                                            :command #'(lambda ()
                                                           (visualise (parse-space-from-string (ltk:text text-field))
                                                                      :speed (parse-integer (ltk:text speed-spinbox)))))))
            (ltk:grid text-frame 0 0)
            (ltk:grid text-field 0 0)
            (ltk:grid control-frame 1 0) (ltk:grid speed-spinbox 0 0)
            (ltk:grid load-button 1 0)
            (ltk:grid start-button 1 1))))

So you can immediately see what is responsible for what. Best wishes Filipp

bohonghuang commented 1 year ago

I understand what you mean. You're saying that the make-instance style constructor allows us to directly pass object properties during construction, right? However, GTK classes often have their own constructors, such as new, new_from_file, new_from_gicon, and so on. I prefer not to mix properties with the parameters of these constructors, so currently, I'm using the defstruct style constructor.

If we want to achieve declarative UI, I believe we should use separate macros, just like in Relm4, instead of mixing them with the imperative API.

Filipp-Druan commented 1 year ago

I looked at the examples on Reml4, I like the view! macro, would it be difficult to write something like this? How long will it take? Unfortunately, I won't be able to write code for the library myself.

bohonghuang commented 1 year ago

I looked at the examples on Reml4, I like the view! macro, would it be difficult to write something like this? How long will it take? Unfortunately, I won't be able to write code for the library myself.

Implementing macros in Lisp is always easier than in other languages. I might work on it after I finish dealing with my job hunting. Of course, you are also welcome to try implementing it yourself and submit a pull request.

bigos commented 1 year ago

By any chance, are you looking for a declarative menu?

https://github.com/bigos/Pyrulis/blob/85a6485bdf7ce2e57634aac966ef1753ddc53543/Lisp/better-menu.lisp#L187C3-L187C3

Filipp-Druan commented 1 year ago

I'm guess, what I can write view!-like macro myself, but I'm afraid, what I'll write a bad variant, and nobody more competent won't write better interface because my already is.

bohonghuang commented 1 year ago

I'm guess, what I can write view!-like macro myself, but I'm afraid, what I'll write a bad variant, and nobody more competent won't write better interface because my already is.

Don't worry. You can try implementing your own version first, and I believe you will learn a lot of Lisp techniques in the process. After I finish my busy period, I will also implement one, and then we can discuss the strengths and weaknesses of both and learn from each other.

seigakaku commented 1 year ago

I have written a simple declarative macro for defining UIs with cl-gtk4, i'm not really good with macros or anything and i haven't done exhaustive testing, but i've been using it successfully in my projects, so i thought I might share it here.

The code is here: https://codeberg.org/seigakaku/gtk4-defui

Here's the fibonacci example adapted to use it:

(define-interface fibonacci
    (container box (make-box :orientation +orientation-vertical+ :spacing 4)
     (label label (make-label :str "0")
      :properties (:hexpand t :vexpand t))
     (nil box (make-box :orientation +orientation-horizontal+ :spacing 4)
      :properties (:hexpand t :halign +align-center+)
      (nil label (make-label :str "n: "))
      (nil entry (make-entry)
       :init (lambda (entry)
               (setf (entry-buffer-text (entry-buffer entry)) (format nil "~A" n)))
       :properties (:hexpand t :halign +align-fill+)
       :connect (("changed" (lambda (entry)
                              (setf n (ignore-errors (parse-integer
                                                      (entry-buffer-text
                                                       (entry-buffer entry))))))))))
     (nil button (make-button :label "Calculate")
      :connect (("clicked" (lambda (button)
                             (bt:make-thread
                              (lambda ()
                                (when n
                                  (run-in-main-event-loop ()
                                    (setf (button-label button) "Calculating..."
                                          (widget-sensitive-p button) nil))
                                  (let ((result (fib n)))
                                    (run-in-main-event-loop ()
                                      (setf (label-text label) (format nil "~A" result)
                                            (button-label button) "Calculate"
                                            (widget-sensitive-p button) t)))))))))))
  (n :type (or null fixnum) :initform 40))

(define-application (:name fibonacci :id "org.bohonghuang.gtk4-example.fibonacci")
  (defun fib (n)
    (if (<= n 2) 1 (+ (fib (- n 1)) (fib (- n 2)))))
  (define-main-window (window (make-application-window :application *application*))
    (setf (window-title window) "Fibonacci Calculator")
    (let* ((fibonacci (make-instance 'fibonacci))
           (container (fibonacci-container fibonacci)))
      (setf (window-child window) container)
      (unless (widget-visible-p window)
        (window-present window)))))
Filipp-Druan commented 1 year ago

Thanks!

Filipp-Druan commented 8 months ago

Hello! Do you have some progress in declarativety?

Filipp-Druan commented 8 months ago

There is a good example: https://docs.racket-lang.org/gui-easy/index.html

bohonghuang commented 8 months ago

This is a draft I completed earlier:

(ql:quickload :symbol-munger)
(ql:quickload :trivial-arguments)

(in-package #:gtk4.example)

(defun expand-gui-definition (name fields &aux (package (symbol-package name)))
  (alexandria:when-let* ((name-space-symbol (find-symbol (symbol-name '#:*ns*) package))
                         (gir-class (gir:nget (symbol-value name-space-symbol) (symbol-munger:lisp->studly-caps name)))
                         (constructor (find-symbol (format nil "~A-~A" '#:make name) package)))
    (alexandria:with-gensyms (instance)
      `(let ((,instance (,constructor . ,(loop :with keywords := (mapcar #'caar (nth-value 3 (alexandria:parse-ordinary-lambda-list
                                                                                              (trivial-arguments:arglist constructor))))
                                               :for (key value) :on fields :by #'cddr
                                               :when (member key keywords)
                                                 :nconc (list key value)))))
         ,@(loop :for (key value) :on fields :by #'cddr
                 :nconc (loop :for class := gir-class :then (gir:parent-of class)
                              :for symbol := (when class
                                               (find-symbol
                                                (format
                                                 nil "~A-~A"
                                                 (symbol-munger:camel-case->lisp-name
                                                  (gir:info-get-name (gir::info-of class))
                                                  :capitalize t)
                                                 key)
                                                package))
                              :for setf-function := (ignore-errors (fdefinition `(setf ,symbol)))
                              :for function := (ignore-errors (fdefinition symbol))
                              :for value-form := (or (and (listp value) (expand-gui-definition (car value) (cdr value))) value)
                              :while class
                              :when setf-function
                                :return (list `(setf (,symbol ,instance) ,value-form))
                              :when (and function (= (length (trivial-arguments:arglist function)) 2))
                                :return (list `(,symbol ,instance ,value-form))))
         ,instance))))

(defmacro gui ((name &body body))
  (expand-gui-definition name body))

(define-application (:name simple-counter
                     :id "org.bohonghuang.gtk4-example.simple-counter")
  (define-main-window (window (make-application-window :application *application*))
    (setf (window-title window) "Simple Counter"
          (window-child window) (gui (box
                                       :orientation +orientation-vertical+
                                       :spacing 4
                                       :append (label :str "0"
                                                      :hexpand-p t
                                                      :vexpand-p t)
                                       :append (button :label "Add"))))

    (unless (widget-visible-p window)
      (window-present window))))

Perhaps you can continue to improve it.