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

list-view has contract for obs? but docs say (maybe-obs/c list?) #13

Closed
benknoble opened this issue Apr 7, 2022 · 6 comments
Closed

Comments

@benknoble
Copy link
Contributor

So (list-view empty (const (text "hi"))) [a toy example] should work according to the docs but doesn't.

Which is correct? For now I will make sure the first argument is observable…

@Bogdanp Bogdanp closed this as completed in 6ad67ee Apr 8, 2022
@Bogdanp
Copy link
Owner

Bogdanp commented Apr 8, 2022

Thanks for the report! This should be fixed now.

@benknoble
Copy link
Contributor Author

You're welcome :) I've been using racket/gui/easy a lot lately for a personal project, and I'm really enjoying how easy it is to build components without much boilerplate. Thank you for putting this together.

I'm struggling to correctly design the state-management, though. I'm starting to discover ideas that help, but nothing too concrete yet. I should mention I haven't really worked with racket/gui at all.

Examples of struggles As an example, one place I struggle is with inputs and transitions; the function that creates an input component might also an observable so the caller can monitor the changes to the input. [This makes the components more standalone, at the cost of possibly increased complexity when combining them. But it feels more like traditional programming, designing small components and combining them.] However, when moving to a different point in the life-cycle of the application, I want to update completely different state based on the current value of the input. I cannot use derived observables for this because the state to be updated will be updated differently by different components later, and derived observables prevent that. [Think of creating a player in a game: initially, we fill out simple details about the character. Then, we make the full character state. Later, the game needs to update components of that state in different ways, so the full state cannot be derived (in the sense of `obs-map`) from the initial state. Instead, it needs to be created independently based on the value of the initial state at a particular point in time.] So far, most of my transitions are accomplished via buttons, so I can simply have the button handler do the update.

But a case that occurred recently that prompted a slight redesign of the GUI was dialogs: how can I effectively present a dialog and only do some update (based on observables reflecting the state of the dialog) when the dialog is closed? Two thoughts came to mind:

  1. Don't use a dialog if you can avoid it. In some situations this seems difficult.
  2. Structure call-backs on the observables (with obs-observe! or otherwise) or on the components themselves to update state in such a way that changing the state of the dialog effectively undoes the previous action of the dialog on the overall state before performing the new action. This way, when the dialog is closed, the correct transformations will already be in place. This isn't quite idempotency, I think, and may be non-trivial.

Using a button to close the dialog and trigger the state change seems difficult, too, since I can't seem to think of a way to to trigger closing a window.

I wonder if you have any thoughts or writing on effective ways to structure states and updates for gui/easy components? Particularly when state in one component needs to affect state in another without using derived observables?

Happy to take this to another place to discuss, be it a separate issue, the discourse, or elsewhere.

@Bogdanp
Copy link
Owner

Bogdanp commented Apr 8, 2022

Re. state management: one thing that has worked for me is a model where components rarely take in observables and, if they do, they never manipulate them directly. Instead, components always provide callbacks to the parent to decide how/when to manipulate the state. I think this is similar to what's known as "data down, actions up" in the frontend world.

Here's an example from one of my apps that shows this and how I deal with dialogs. I have a component for configuring connections to a Kafka server:

connection-dialog.rkt:

(define (connection-dialog [conf (make-connection-conf)]
                           #:ok-label [ok-label "Save"]
                           #:ok [ok-action void]
                           #:cancel [cancel-action void]
                           #:title [title "New Connection"])
  (define/obs @conf conf)
  (define ((make-updater setter) _event text)
    (@conf . <~ . (λ (c) (setter c text))))
  (define closed-via-action? #f)
  (define close-dialog void)
  (define mixin
    (compose1
     (make-on-close-mixin
      (λ ()
        (unless closed-via-action?
          (cancel-action void))))
     (make-closing-proc-mixin
      (λ (close-proc)
        (set! close-dialog (λ ()
                             (set! closed-via-action? #t)
                             (close-proc)))))))
  (dialog
   #:title title
   #:size '(500 #f)
   #:mixin mixin
   (vpanel
    #:margin '(5 5)
    (vpanel
     #:stretch '(#t #f)
     (labeled
      "Connection name:"
      (input
       (@conf . ~> . connection-conf-name)
       (make-updater set-connection-conf-name)))
     (hpanel
      (labeled
       "Bootstrap server:"
       (input
        (@conf . ~> . connection-conf-bootstrap-server)
        (make-updater set-connection-conf-bootstrap-server)))
      (hpanel
       #:stretch '(#f #t)
       #:alignment '(left center)
       (labeled
        "Secure?"
        #:width 70
        (checkbox
         #:checked? (@conf . ~> . connection-conf-secure?)
         (λ (secure?)
           (@conf . <~ . (λ (c) (set-connection-conf-secure? c secure?))))))))
     (hpanel
      (labeled
       "SASL username:"
       (input
        (@conf . ~> . connection-conf-sasl-username)
        (make-updater set-connection-conf-sasl-username)))
      (labeled
       "Password:"
       #:width 80
       (input
        #:style '(single password)
        (@conf . ~> . connection-conf-sasl-password)
        (make-updater set-connection-conf-sasl-password)))))
    (hpanel
     #:stretch '(#t #f)
     #:alignment '(right top)
     (ok-cancel-buttons
      (button #:style '(border) ok-label (λ () (ok-action (obs-peek @conf) close-dialog)))
      (button "Cancel" (λ () (cancel-action close-dialog))))))))

This is completely self-contained and I can run it on its own just to test it:

(module+ main
  (render
   (connection-dialog
    #:ok (lambda (conf close-dialog)
           (close-dialog)
           (println conf))
    #:cancel (lambda (close-dialog)
               (close-dialog)
               (println "canceled")))))

One level up, I have a component that lists sets of connections.

connection-list.rkt:

(define (connection-list @conns [action void])
  (canvas-list
   (@conns . ~> . (lambda (conns)
                    (append conns `(,nc))))
   (lambda (event item mouse-event)
     (case event
       [(commit)
        (if (eq? item nc)
            (action 'new #f #f)
            (action 'connect item #f))]
       [(context)
        (unless (eq? item nc)
          (action 'context item mouse-event))]))
   #:item-height 32
   #:paint (lambda (item state dc w h)
             (define label
               (if (eq? item nc)
                   "New Connection..."
                   (connection-conf-name item)))
             (p:draw-pict (item-pict label state w h) dc 0 0))))

This one does take an observable as input, but it doesn't change it directly. It just calls action and lets the parent deal w/ state changes. Because it's self-contained, I can test it on its own, or I can combine it with the connection-dialog component:

(module+ main
  (require "connection-dialog.rkt")
  (define/obs @connections
    (list
     (make-connection-conf #:name "Example 1")
     (make-connection-conf #:name "Example 2")))
  (define root
    (render
     (window
      #:title "Connections"
      #:size '(700 400)
      (connection-list
       @connections
       (lambda (event item mouse-event)
         (case event
           [(new)
            (render
             (connection-dialog
              #:ok (λ (conf close-dialog)
                     (@connections . <~ . (λ (conns) (cons conf conns)))
                     (close-dialog))
              #:cancel (λ (close-dialog)
                         (close-dialog)))
             root)])))))))

Here, my main module is the topmost level for this particular view hierarchy, so it's the level at which all the actions combine to actually manipulate the state (trivially, in this case, but you can imagine more complex examples).

The result looks like this:

Screen.Recording.2022-04-08.at.21.00.58.mov

Hopefully that makes sense and helps. These snippets are from an app that I haven't yet open sourced so I don't have a better way of sharing them right now.

@benknoble
Copy link
Contributor Author

It's going to take me some time to digest this, but that's certainly helpful. Thanks for putting the effort in. I think my lack of experience with racket/gui makes it harder to appreciate certain arguments (e.g., mixin) for racket/gui/easy components. The idea of callbacks makes sense, thanks for that.

@benknoble
Copy link
Contributor Author

Is it fair to say that make-on-close-mixin creates a mixin that overrides on-close from top-level-window<%>? And similarly for make-closing-proc-mixin, except that I cannot figure out what is overridden there…

@Bogdanp
Copy link
Owner

Bogdanp commented Apr 9, 2022

make-on-close-mixin creates a mixin that augments on-close so that it calls its proc argument, make-closing-proc-mixin creates a mixin that calls its proc argument with a function that will close the window when applied. Here they are:

(provide
 make-closing-proc-mixin)

;; Dialogs need to be closed, but rendering a dialog yields so there's
;; no way to retrieve a dialog's renderer from within itself.  This
;; may be another argument for gui-easy providing a managed
;; `current-renderer'.  In the mean time, we can abuse mixins for this
;; purpose.
(define ((make-closing-proc-mixin out) %)
  (class %
    (super-new)
    (out (lambda ()
           (when (send this can-close?)
             (send this on-close)
             (send this show #f))))))

(provide
 make-on-close-mixin)

(define ((make-on-close-mixin proc) %)
  (class %
    (super-new)
    (define/augment (on-close)
      (proc))))

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

No branches or pull requests

2 participants