Bogdanp / racket-gui-easy

Declarative GUIs in Racket.
https://docs.racket-lang.org/gui-easy/index.html
134 stars 18 forks source link

Clean way to implement a list of checkboxes? #42

Open cloudrac3r opened 1 year ago

cloudrac3r commented 1 year ago

Hiya! This is an open-ended issue without a strict definition of completed.

I'm coding a couple of approaches to make a list of checkboxes. My example program lets people select foods they like from a list. Changes to the interface are stored in @foods, and changes to @foods are reflected back to the interface. My goal is to write code that looks nice without writing too much.

Hopefully the insights from this will either make me better at using gui-easy, or will help gui-easy become easier to use.

First attempt:

#lang racket
(require racket/gui/easy
         racket/gui/easy/operator)

(struct food^ (name checked?) #:transparent)

(define/obs @foods `((1 . ,(food^ "Apple" #t))
                     (2 . ,(food^ "Banana" #f))
                     (3 . ,(food^ "Broccoli" #f))
                     (4 . ,(food^ "Ice Cream" #t))))
(obs-observe! @foods println)

(render
 (window
  #:size '(250 250)
  (list-view
   @foods
   #:key car
   (λ (k @food)
     (define name (@food . ~> . (λ (food) (food^-name (cdr food)))))
     (define checked? (@food . ~> . (λ (food) (food^-checked? (cdr food)))))
     (checkbox
      #:label name
      #:checked? checked?
      (λ (checked?)
        (<~ @foods
            (λ (foods)
              (dict-update foods k (λ (food) (struct-copy food^ food [checked? checked?])))))))))))

;; C-x C-e this to toggle a checkbox: (@foods . <~ . (λ (foods) (dict-update foods 2 (λ (food) (struct-copy food^ food [checked? (not (food^-checked? food))])))))

(My style is to use ^ to notate struct definitions.)

Things I don't like about this:

Second attempt:

#lang racket
(require racket/gui/easy
         racket/gui/easy/operator)

(define/obs @foods `((1 . "Apple")
                     (2 . "Banana")
                     (3 . "Broccoli")
                     (4 . "Ice Cream")))
(define/obs @foods-checked (set 1 4))
(obs-observe! @foods-checked println)

(render
 (window
  #:size '(250 250)
  (list-view
   @foods
   #:key car
   (λ (k @food)
     (define name (@food . ~> . cdr))
     (define checked? (@foods-checked . ~> . (curryr set-member? k)))
     (checkbox
      #:label name
      #:checked? checked?
      (λ _ (@foods-checked . <~ . (curry set-symmetric-difference (set k)))))))))

;; C-x C-e to toggle some checkboxes: (@foods-checked . <~ . (λ (fc) (set-symmetric-difference fc (set 1 2))))

Overall I think there's still some room for improvement here, both in my code and in gui-easy, but I don't know what to change in order to improve it. One idea that I think has potential is if there was a version of list-view that included pattern-matching in order to unpack the list items for me. For example:

(define/obs @items '((1 "Red" "#ff0000" #t) (2 "Green" "#00ff00" #f)))
(list-view/match
 @items
 #:key car
 [(list k name hex checked?)
  (hpanel (text name #:color hex) (checkbox #:checked? checked?))]

or

(struct color^ (id name hex) #:transparent)
(define/obs @items (list (color^ 1 "Red" "#ff0000" #t) (color^ 2 "Green" "#00ff00" #f)))
(list-view/match
 @items
 #:key color^-id
 [(color^ k name hex checked?)
  (hpanel (text name #:color hex) (checkbox #:checked? checked?))]

But that's just a theory. Do you have any thoughts or ideas on my long-winded post?

As always, keep up the great work :)

Bogdanp commented 1 year ago

Re. your first example, I would extract helper functions to help reduce the indentation:

#lang racket/gui/easy

(require racket/dict
         racket/function)

(struct food^ (name checked?) #:transparent)

(define (set-food^-checked? f checked?)
  (struct-copy food^ f [checked? checked?]))

(define (update-food foods k checked?)
  (dict-update foods k (curryr set-food^-checked? checked?)))

(define/obs @foods
  `((1 . ,(food^ "Apple" #t))
    (2 . ,(food^ "Banana" #f))
    (3 . ,(food^ "Broccoli" #f))
    (4 . ,(food^ "Ice Cream" #t))))
(obs-observe! @foods println)

(render
 (window
  #:size '(250 250)
  (list-view
   @foods
   #:key car
   (λ (k @id+food)
     (define @food (@id+food . ~> . cdr))
     (checkbox
      #:label (@food . ~> . food^-name)
      #:checked? (@food . ~> . food^-checked?)
      (λ (checked?)
        (@foods . <~ . (curryr update-food k checked?))))))))

Re. the match idea: I can't think of a way to write a match expander that produces derived observables, so I don't think that can work with regular match. I don't have any better ideas about how to further improve this code at the moment, either.

cloudrac3r commented 1 year ago

Thanks for the reply! You're right, extracting those food operations to functions does make it easier to read and write the code. In the future, I might play around with making a limited version of list-view/match which just covers unpacking lists and structs, since those are the only data types I've found myself using in list-views.