-
-
Notifications
You must be signed in to change notification settings - Fork 17
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
Comments
Thanks for the report! This should be fixed now. |
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 strugglesAs 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:
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. |
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:
(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.
(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 (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 The result looks like this: Screen.Recording.2022-04-08.at.21.00.58.movHopefully 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. |
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., |
Is it fair to say that |
(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)))) |
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…
The text was updated successfully, but these errors were encountered: