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

Could you make the library interface more declarative? #39

Open
Filipp-Druan opened this issue Oct 21, 2023 · 11 comments
Open

Could you make the library interface more declarative? #39

Filipp-Druan opened this issue Oct 21, 2023 · 11 comments
Labels
enhancement New feature or request

Comments

@Filipp-Druan
Copy link

Filipp-Druan commented Oct 21, 2023

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
Copy link
Owner

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.

@bohonghuang bohonghuang added the enhancement New feature or request label Oct 22, 2023
@Filipp-Druan
Copy link
Author

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
Copy link
Owner

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
Copy link

bigos commented Oct 22, 2023

@Filipp-Druan
Copy link
Author

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
Copy link
Owner

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
Copy link

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
Copy link
Author

Thanks!

@Filipp-Druan
Copy link
Author

Hello!
Do you have some progress in declarativety?

@Filipp-Druan
Copy link
Author

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

@bohonghuang
Copy link
Owner

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants