Code Monkey home page Code Monkey logo

racket-gui-easy's Introduction

gui-easy-lib

Declarative GUIs in Racket. This is still a work in progress so expect some breaking changes.

Installation

The package is available on the Racket Package Index.

$ raco pkg install gui-easy

Development

From the root of the repository:

$ raco pkg install --auto --skip-installed gui-easy-lib/ gui-easy/

Updating

From the root of the repository:

$ raco pkg update gui-easy-lib/ gui-easy/

Running the demos

After you have installed gui-easy-lib. You can run the example code from the examples/ folder using DrRacket or via the command-line.

License

gui-easy is licensed under the 3-Clause BSD license.

racket-gui-easy's People

Contributors

benknoble avatar bennn avatar bogdanp avatar cloudrac3r avatar dannypsnl avatar kengruven avatar mflatt avatar rfindler avatar samdphillips avatar soapdog avatar teevr avatar xioi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

racket-gui-easy's Issues

dyn-view with hidden observable dependencies

I had a function that looked like this:

(define (make-creature-view k @e)
  (define make-player-or-monster-group-view
    (match-lambda
      [(cons _ (? player?)) (make-player-view k @e)]
      [(cons _ (cons _ (? monster-group?))) (make-monster-group-view k @e)]))
  (dyn-view @e make-player-or-monster-group-view))

[Ignore the (cons _ patterns; I am still deciding how best to restructure the data for that part.]

The make-creature-view function is used in a list-view over an observable @creatures which holds items that are either player? or monster-group? (again, ignoring the initial cons bits).

The dyn-view lets me do the job of an if-view/cond-view but without constructing the sub-views of all conditional branches when the data doesn't match what is needed (e.g., with a cond-view here, I can't avoid that the monster-group-view gets a player? or vice-versa, because the sub-views are constructed greedily). At least, I think this is why I am using dyn-view; I can't remember.

But here's where things get interesting: the view returned by make-player-view has no dependencies other than @e, which (conveniently) the dyn-view also monitors and reacts to. On the other hand, make-monster-group-view creates a view that depends on an observable not mentioned here. I update it in response to other events and was expecting the GUI to update with new information, but it doesn't (presumably because dyn-view doesn't know about it, so can't forward updates, so nothing is redrawn).

There's a very silly hack to fix this (notice the obs-combine and the extra cons patterns):

(define (make-creature-view k @e)
  (define make-player-or-monster-group-view
    (match-lambda
      [(cons _ (cons _ (? player?))) (make-player-view k @e)]
      [(cons _ (cons _ (cons _ (? monster-group?)))) (make-monster-group-view k @e)]))
  (dyn-view
    ;; HACK: Combine @e with @ability-decks to register dyn-view dependency on
    ;; @ability-decks; but, the actual observable we care about is still just
    ;; @e. (You can see that we ignore the car of the resulting pair in the
    ;; match patterns above.) This is because make-monster-group-view creates
    ;; a view that depends on @ability-decks, but dyn-view isn't aware of this
    ;; dependency.
    (obs-combine cons @ability-decks @e)
    make-player-or-monster-group-view))

This works (!) but isn't scalable or maintainable: if more dependencies are added to make-monster-group-view, they need to balloon into the obs-combine and match, too. And if one is forgotten, there's a sort of "spooky action at a distance" bug like the lack of updates I described above.

I actually first tried a slightly better version that used (obs-combine (lambda (a e) e) @ability-decks @e), but that didn't work. It only improved the match, too; my points above still apply.

Have you encountered something like this before? Do you have a suggestion to alleviate this? It's like I want the static parts of if-view but the dynamic parts of dyn-view (again, because of the issue I have with disjoint data).

Error in remove-dependencies while updating conditional view within a list-view

I'm not sure how to debug this myself, but I do have the full error message and code for you to take a look at. I'm pretty sure this is a bug in your library rather than a bug in my sample code here.

Thank you!


The error message:

; send: target is not an object
;   target: #f
;   method name: remove-dependencies
; Context (plain; to see better errortrace context, re-run with C-u prefix):
;   /home/cadence/.racket/collects/racket/private/class-internal.rkt:4681:0 obj-error
;   /home/cadence/.racket/8.6/pkgs/gui-easy-lib/gui/easy/private/view/if.rkt:103:4 remove&destroy-then-view
;   /home/cadence/.racket/8.6/pkgs/gui-easy-lib/gui/easy/private/view/if.rkt:45:4 update method in if-view%
;   /home/cadence/.racket/8.6/pkgs/gui-easy-lib/gui/easy/private/view/container.rkt:21:4 update-children method in container%
;   /home/cadence/.racket/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:435:6
;   /home/cadence/.racket/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:460:2 yield

Full code to reproduce:

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

;; queue is a list of pairs. car of the pair is the name. cdr of the pair is the state.
;; queue goes into the list-view. each pair is rendered as an hpanel with text.

;; --- OBSERVABLES ---

(define/obs @queue '(("Row one" . 1) ("Row two" . 2)))

;; --- MUTATORS ---

(define (do-add-to-queue item)
  (<~ @queue
      (λ (queue)
        (append queue (list item)))))

(define (do-set-item-state target-item-name new-state)
  (@queue . <~ . (λ (queue)
                   (for/list ([item queue])
                     (if (equal? (car item) target-item-name)
                         (cons (car item) new-state)
                         item)))))

;; --- INTERFACE ---

(void
 (render
  (window #:size '(400 300)
          (list-view
           @queue
           #:key car
           (λ (k @item)
             (hpanel #:stretch '(#t #f)
                     ;; car of the pair is the name
                     (text k)
                     (spacer)
                     ;; cdr of the pair is the state. branch off it.
                     (if-view (@item . ~> . (λ (item) (eq? (cdr item) 1)))
                              (text "is in state one")
                              (text "is in state two")))))
          (button "row one -> state two" (λ () (do-set-item-state "Row one" 2)))
          (text "^ clicking this button shows an error in the logs ^")
          (text "Same outcome when calling (do-set-item-state ...) in the REPL."))))

Issue also occurs with cond-view and case-view. Just swap if-view out for one of these replacements and you'll see the same issue. (They probably call one another internally.)

                     (cond-view
                      [(@item . ~> . (λ (item) (eq? (cdr item) 1)))
                       (text "is in state one")]
                      [else
                       (text "is in state two")])
                     (case-view (@item . ~> . cdr)
                      [(1) (text "is in state one")]
                      [else (text "is in state two")]))))

Documentation for case-view condition doesn't match implementation

case-view documentation says that it uses equal? to compare the observable against the lit, but the implementation uses memv which is the eqv? condition. This makes it impossible to switch on strings because they can't be compared by eqv?.

Suggested solution: Change case-view's implementation to use member as the test.

Relevant code: private/view/if.rkt line 137.

As always, thank you for your amazing work!

Is it possible to detect when the main window closes?

My program runs a timer in the background to periodically refresh the information displayed in the GUI. When the window is closed, the timer remains running and the process keeps running, invisibly.

Is it possible to detect when the main window closes, so that I can stop the timer and end the process?

As always, thank you so much for everything you've done for the Racket world!

observable-view: change-children: cannot delete non-window area

I had some code in a button handler that did the equivalent of

(render
  (dialog
    (apply hpanel (map make-view (obs-peek @xs)))))

When auditing my use of obs-peek, I converted this to

(render
  (dialog
    (observable-view @xs (lambda (xs) (apply hpanel (map make-view xs))))))

This works, but with the following message when closing the dialog:

change-children: cannot delete non-window area
  area: (object:wx-make-pane% ...)
  container: (object:context-mixin ...)
  context...:
   /Applications/Racket v8.8/collects/ffi/unsafe/atomic.rkt:73:13
   /Users/Knoble/Library/Racket/8.8/pkgs/gui-easy-lib/gui/easy/private/view/window.rkt:91:4: destroy method in window-like%
   /Users/Knoble/Library/Racket/8.8/pkgs/gui-easy-lib/gui/easy/private/renderer.rkt:63:7
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:435:6
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:486:32
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:634:3

Here's a minimal example:

(require racket/gui/easy racket/gui/easy/operator)
(define/obs @xs (list 1 2 3 4))
(define (make-x-button x)
  (button (~a x) (lambda () (displayln x))))
(render
 (dialog
  (observable-view @xs (lambda (xs) (apply hpanel (map make-x-button xs))))))
; interact with the buttons if you wish, then close the dialog

output (I think the object line appearing after button outputs is an artifact of timing or something)

1
3
2
(object:renderer% ...)
change-children: cannot delete non-window area
  area: (object:wx-make-pane% ...)
  container: (object:context-mixin ...)
append: contract violation
  expected: list?
  given: #<void>
 [,bt for context]

I think something must be using a pane% or subclass somewhere, since those are the only default GUI widgets on which change-children would fail (https://docs.racket-lang.org/gui/area-container___.html#%28meth._%28%28%28lib._mred%2Fmain..rkt%29._area-container~3c~25~3e%29._change-children%29%29 and https://docs.racket-lang.org/gui/Windowing_Classes.html).

Scrolling list-views with the mouse

A quick glance at the racket/gui docs didn't get me too far, so: what would it take to make the view in list-view scrollable with the mouse (in addition to dragging the scrollbar)?

Is this a fundamental limitation of racket/gui that such things aren't supported automatically or even easily? Or is there a simple piece of plugin code that I can add as a mixin or even a PR here?

DrRacket's editor scrolls with the mouse, so I'm sure it must be possible. But if it's intrinsically difficult code, I can live without it.

Duplicate `gui-easy-lib` dependency

In the dependencies of gui-easy, the package gui-easy-lib is listed twice -- once as deps and once as build-deps. This ended up creating duplicate dependencies.

I am not sure whether this is related, but the last few lines seem to suggest that duplicate dependencies cause problems when updating packages:

Resolving "gui-easy-lib" via https://pkgs.racket-lang.org
The following out-of-date packages are listed as dependencies of gui-easy
and they will be automatically updated:
   gui-easy-lib
   gui-easy-lib
Using cached17005413591700541359311 for https://github.com/Bogdanp/racket-gui-easy.git?path=gui-easy-lib
...
Uninstalling to prepare re-install of gui-easy-lib
Moving gui-easy-lib to trash: <PATH>
Uninstalling to prepare re-install of gui-easy-lib
raco pkg update: package not currently installed
  package: gui-easy-lib
  current scope: installation

from https://racket.discourse.group/t/package-removed-after-trying-to-update-them/2521

text errors on #:color keyword

Noticed this when trying to set #:color on a text component.

This is from the examples folder:

% racket text-color.rkt                                                                                     
send: no such method
  method name: set-color
  class name: message%
  context...:
   /Applications/Racket v8.2/collects/racket/private/class-internal.rkt:4680:0: obj-error
   /Users/kyushu/Library/Racket/8.2/pkgs/gui-easy-lib/gui/easy/private/view/text.rkt:20:4: create method in text%
   /Users/kyushu/Library/Racket/8.2/pkgs/gui-easy-lib/gui/easy/private/view/window.rkt:38:4: create method in window-like%
   .../private/arrow-higher-order.rkt:379:33
   /Users/kyushu/Library/Racket/8.2/pkgs/gui-easy-lib/gui/easy/private/renderer.rkt:34:4: render method in renderer%
   /Users/kyushu/Library/Racket/8.2/pkgs/gui-easy-lib/gui/easy/private/renderer.rkt:55:0: render
   /Applications/Racket v8.2/collects/racket/contract/private/arrow-val-first.rkt:489:18
   body of "/Users/kyushu/github/racket-gui-easy/examples/text-color.rkt"

update-children in container%: hash-ref: no value found for key

When I run the following program and close the dialog, I consistently get the following output, with an error message:

(object:renderer% ...)
hash-ref: no value found for key
  key: (object:text% ...)
  context...:
   /Users/$USER/Library/Racket/8.5/pkgs/gui-easy-lib/gui/easy/private/view/container.rkt:22:4: update-children method in container%
   /Applications/Racket v8.5/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:435:6
   /Applications/Racket v8.5/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:486:32
   /Applications/Racket v8.5/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:634:3

Program:

#lang racket

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

;; calls `proc` when the window closes
(define ((make-on-close-mixin proc) %)
  (class %
    (super-new)
    (define/augment (on-close)
      (proc))))

(define/obs @thing (list 1))
(render
  (dialog
    #:mixin (make-on-close-mixin (thunk (<~ @thing rest)))
    (text (~> @thing (λ (t) (if (empty? t)
                                ""
                                (~a (first t))))))))

Even more bizarrely the indicated line (22 in gui-easy-lib/gui/easy/private/view/container.rkt) is

      (for ([c (in-list (hash-ref deps-to-children what null))])

which has a default value for the hash-ref? So perhaps this is an odd racket bug… unfortunately I cannot come up with a small hash-ref test-case that demonstrates the same bug, and the "context" elision is useless.

Default keymap for (input) should be (keymap:get-editor)

The current default is to have no keybinds, but I don't think this is very helpful. We all expect Ctrl+C/Ctrl+V et al to be available everywhere!

(keymap:get-editor) includes all the keybinds that I would expect for a plain text field and it makes sense as a default.

This would save me a couple lines of code and memorisation each time I make a little application with gui-easy :)

is there a way to close a dialog programmatically?

Right now closing a rendered dialog is non-trivial, or is too ugly.
Maybe there is a way, but I can only think of either using a mixin or this which doesn't work on dialog but does on frames.

(define (d r)
       (define (close-dialog) (send (renderer-root (force r)) show #f))
       (dialog
        (text (format "~a: first ~a characters" identity len))
        (input "" (λ (evt str)
                    (displayln evt)
                    (when (eq? evt 'return)
                      (cond
                        [(string=? str (substring password 0 len)) 
                         (close-dialog)
                         (next-dialog)]
                        [else
                         (close-dialog)
                         (error-dialog)]))))))
(define r (render (d (delay r)) (get-parent)))

This throws an error since r isn't defined because of how dialog's yield work.

Make custom observables (e.g., parameters)

I have a use-case where I would like to turn Racket parameters into observables. I can wrap the parameter in an observable, and then manage all the patterns of using it myself, but I managed to extract those patterns. The trouble is, I can't create my own thing and tell racket/gui/easy that it should count as observable and how it behaves under all the observable operations. The best I can do currently is provide a set of macros that I call obsp for observable parameter. Now the management is hidden away, but I still have to remember the right thing to call (<~ or <~p?).

The fundamental reason for this is that my custom observable parameter is really three values: the parameter itself, an observable of the parameter, and a derived observable that uses the parameter's value. This third one is what we are usually concerned with when reading, and the value we want to use when writing via an update function, but we can only do the writes through the second value.

If there were a prop: or gen: interface I could implement on a struct and have it co-operate with all of racket/gui/easy's observable procedures, that would be my ideal world. As it is, here's the set of macros for observable parameters (they essentially hide the second value from you, though it is accessible; in my examples, it would be @internal-@op).

#lang racket

(require racket/gui/easy
         racket/gui/easy/operator
         syntax/parse/define
         (for-syntax racket/syntax))

(define-for-syntax (@internal stx id)
  (format-id stx "@internal-~a" id #:source stx))

(define-syntax-parse-rule (define/obsp n:id p:expr)
  #:with @internal (@internal #'n (syntax-e #'n))
  (begin
    (define/obs @internal p)
    (define/obs n (@internal . ~> . (λ (the-p) (the-p))))))

(define (internal-obsp-update! internal o f)
  (internal . <~ . (λ (the-p) (the-p (f (the-p))) the-p))
  (obs-peek o))

(define-syntax-parse-rule (obsp-update! o:id f:expr)
  #:with @internal (@internal #'o (syntax-e #'o))
  (internal-obsp-update! @internal o f))
;; don't need p~>/obsp-map because ~>/obs-map already work
(define-syntax <~p (make-rename-transformer #'obsp-update!))
(define-syntax-parse-rule (:=p o:id v:expr) (<~p o (const v)))
(define-syntax-parse-rule (λ:=p o:id f:expr) (λ (v) (:=p o (f v))))
(define-syntax-parse-rule (λ<~p o:id f:expr) (thunk (<~p o f)))

(define p (make-parameter #f))
(define/obsp @op p)

(obs-observe! @op (λ (x) (printf "op: Value: Got ~s~n" x)))

(equal? (obs-peek @op) (p))

;; bypass @op
(p 1)

;; new versions
(obsp-update! @op add1) ;; 2
(<~p @op add1) ;; 3
(:=p @op 4) ;; 4
((λ:=p @op add1) (obs-peek @op)) ;; 5
((λ<~p @op add1)) ;; 6

At least :=p looks cute…

List view + dialog + slider appears not to update correctly

Demo:

#lang racket

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

(define/obs @counters '((0 . 0)
                        (1 . 10)
                        (2 . 30)))

(define (update-count counts k proc)
  (for/list ([entry (in-list counts)])
    (if (eq? (car entry) k)
      (cons k (proc (cdr entry)))
      entry)))

(define (counter @count action)
  (hpanel
    #:stretch '(#t #f)
    (text (@count . ~> . number->string))
    (button "Update" (thunk (render (dialog (slider @count action)))))))

(let ([@c (@ 0)])
  (render
    (window
      (counter @c (λ (new-count) (:= @c new-count))))))
(render
  (window
    #:size '(#f 200)
    (list-view
      @counters
      #:key car
      (λ (k @entry)
        (counter
          (~> @entry cdr)
          (λ (count)
            (<~ @counters (λ (counts)
                            (update-count counts k (const count))))))))))

Interact with the single counter window and you'll notice the slider and the count are (effectively) perfectly sync'd.

Interact with the other window and you'll noticed that only the first mouse click in a click-and-drag on the slider updates the display. Worse, closing the dialog and opening another sometimes updates the display of a previous counter to the correct value.

Adding "debug prints" shows that the state is in fact updated correctly; it is only the display that is not updated.

This is a MRE of a problem I'm facing in another project, hence the toy counters.

Containers which create internal panels are always stretchable

Hello! When I say something like this:

(cond-view
 [@foo? (text "foo is set!")]
 [else (text "no foo here!")])

then internally it will instantiate an if-view%, which in turn calls:

(new gui:panel%
             [parent parent]
             [min-width #f]
             [min-height #f]
             [stretchable-width #t]
             [stretchable-height #t]))

There's no way to inject your own arguments for this panel. That is, if you use a cond-view (or any of the similar containers), you can't have a non-stretchable containee. Even if you have a simple 1-line label (which isn't vertically stretchable), putting it in a cond-view causes it to become vertically stretchable, which is not what I want.

Whenever the library creates secret automatic panels, they should either mimic the layout arguments of their containees, or allow me to explicitly pass arguments for them.

Thanks!

How does one program GUIs to handle heterogeneous data, especially with sub-views that contain internal state?

In particular, let's say @e can be either a cat? or a dog? after something like

(struct cat [meows] #:transparent)
(struct dog [woofs] #:transparent)

And let's say we have two views cat-view and dog-view that expect (obs/c cat?) and (obs/c dog?)` respectively.

These are simple, and it would be easy to write a single view that mapped values into one of the struct's fields and then did something else, but imagine a much more complicated disparity (say, human game players and non-human played monster groups, with very different internal structures and views).

if-view and derived friends like cond-view immediately evaluate their sub-views, so they aren't as useful for heterogeneous data (the views that aren't being used will often still raise exceptions and break things).

I have found dyn-view, but it is tricky to use correctly, if I understand my problem correctly. The idea would be

(dyn-view @e
  (lambda (e)
    (cond
      [(cat? e) (cat-view @e)]
      [(dog? e) (dog-view @e)])))

since the following won't work

(cond-view
  [(~> @e cat?) (cat-view @e)]
  [(~> @e dog?) (dog-view @e)])

So far this all seems fine, but I have the following problem: if cat-view or dog-view create their own internal state for updating parts of the view (say, a tabs view with a selection), that internal state can lead to bugs in the application.

Why? It has taken me some time to reason through this, so I could be wrong, but:

  1. The dyn-view only remakes the view if @e changes, so if the internal state changes the view will not be re-made and therefore won't update (?). This I'm less sure about, but it's what I am observing in practice. That is, I can update the internal state and see it propagated to relevant observables, but sub-views are not correctly updated.
  2. Because the dyn-view recreates the entire view if @e does change, it also resets any internal state. For example, a tab selection would be reset. This makes sense, because it re-creates the internal state after throwing away the old one, since we are calling the function to create the view anew, and that function creates the internal state.

In my actual situation, I have a observable list of (cons/c id? (or/c cat? dog?)) (though again the structures are more complicated and not really unifiable), and I want to use dyn-view to build the entry-views for each item in the list (because of the issue with if-view I mentioned above). But the entry-view for one of the types (say dog?) of things in the list does maintain such internal state (recall the monster example we discussed, where the selection was internal to the view). Both types of entry-view allow callbacks; the actually used callbacks update the observable list by (functionally) modifying the corresponding entry in some way; this causes that entry to be remade by the dyn-view IIUC. For the cat? things, everything works pretty normally. For the dog? things, changing tabs updates the "highlighted" tab, but not the child view (which depends on an observable that is updating, as I have confirmed with obs-observe!). In addition, interacting with elements that trigger the callbacks which modify the dog?s does correctly modify them, but the state of that entry-view behaves oddly (usually resetting the highlighted tab to be the first one, as expected based on point 2 above, but without change to the child view as described here).

Without the dyn-view (say, creating a single (obs/c dog?), its dog-view, and then rendering it in a window) everything works fine.

This is a lot to digest, and I apologize for the lack of straightforward code examples, but it would take me some time to extract useful examples from my actual code. Unfortunately I will also be too busy to respond very much for the next couple of weeks :(

I suppose my question is: I can extract the internal tab-selection state so that the dyn-view correctly remakes the view when the selection state changes, too, but it's going to be a fair amount of effort. Is that really my only option? Will it solve both problems above?

I think I can solve (2) by having the dyn-view depend on a combined observable that mixes @e with the previously internal state, although in the cat? case that internal state is actually unused.

For (1), though, it's less clear why the sub-views aren't updating when the observables that control them are, so I'm not sure if this would help or not.

An alternate idea might be to have the id capture both the uniqueness criterion that it does now and the distinction of type; i.e., the new state would be (listof (cons/c (cons/c id? type?) (or/c cat? dog?))). The reason is that we have access to the key value in the list-view callback to make entry-views. I have two issues with this:

  1. It might work in my current design where, by the time this matters, a particular entry with a particular id? will always have the same type?, but future designs could possibly change that by allowing entries to be added, removed, or edited in some way that might change the type?.
  2. It feels error-prone, since I have to maintain state (the type?) that effectively already present just by asking if the entry is cat? or dog?.

Do you have experience with this kind of heterogeneity and internal state in gui-easy? Am I understanding my problem and my options right? Would my first solution solve the problem, or could there be a bug in the propagation of events to children of dyn-view (so that maybe the tabs should be updating but isn't)?

Grid View

Hello,

is it possible to build a grid layout / panel with racket-gui-easy? I looked at the documentation and examples. But it seems there is currently no grid panel available. The only options seems to rap https://docs.racket-lang.org/table-panel/index.html in a custom view. Anyways, I would preview a native solution.
If there currently is no grid panel in this library, I would like to request one as a feature.

Clearing an `input`?

GUI easy is really nice, being new to reactive programming, it somehow feels intuitive, so I think the design is quite user friendly!

On to the question I had- I ran into a curious behavior using the input UI element. Using the following code sample, when I type return, the input box text remains the same, though the observable the input box I think is observing has been cleared. Is there something different I could try?

Thanks!

$ racket --version
Welcome to Racket v8.4 [cs].
#lang racket

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

(define/obs @input "")

(define/obs @message "Type return to send a message")

(render
 (window
  (text @message)
  (input @input
         (λ (kind s)
           (when (symbol=? kind 'return)
             (:= @message (string-append "You typed: " s))
             (:= @input "")))))) ; Input box still shows text after updating this observable.

Canvas: `ctx: undefined; cannot use field before initialization`

I haven't found a small or deterministic reproduction for this yet, but you should be able to follow a few steps and see the issue.

First, drop the following in ring1.rkt:

#lang frosthaven-manager/aoe

 x x
x x x
 x x

Then, drop the following in bestiary.rkt:

#lang frosthaven-manager/bestiary

begin-monster "Ancient Artillery" ("Ancient Artillery")
  [0  normal  [HP  4]   [Move  0]  [Attack  2]  [Immunities  {"Muddle"}]]
  [0  elite   [HP  7]   [Move  0]  [Attack  3]  [Immunities  {"Muddle"}]]
  [1  normal  [HP  6]   [Move  0]  [Attack  2]  [Immunities  {"Muddle"}]]
  [1  elite   [HP  9]   [Move  0]  [Attack  3]  [Immunities  {"Muddle"}]]
  [2  normal  [HP  8]   [Move  0]  [Attack  2]  [Immunities  {"Muddle"}]]
  [2  elite   [HP  12]  [Move  0]  [Attack  3]  [Immunities  {"Muddle"}]]
  [3  normal  [HP  9]   [Move  0]  [Attack  3]  [Immunities  {"Muddle"}]]
  [3  elite   [HP  14]  [Move  0]  [Attack  4]  [Immunities  {"Muddle"}]]
  [4  normal  [HP  12]  [Move  0]  [Attack  3]  [Immunities  {"Muddle"}]]
  [4  elite   [HP  16]  [Move  0]  [Attack  5]  [Immunities  {"Muddle"}]]
  [5  normal  [HP  15]  [Move  0]  [Attack  3]  [Immunities  {"Muddle"}]]
  [5  elite   [HP  21]  [Move  0]  [Attack  5]  [Immunities  {"Muddle"}]]
  [6  normal  [HP  18]  [Move  0]  [Attack  4]  [Immunities  {"Muddle"}]]
  [6  elite   [HP  26]  [Move  0]  [Attack  6]  [Immunities  {"Muddle"}]]
  [7  normal  [HP  21]  [Move  0]  [Attack  5]  [Immunities  {"Muddle"}]]
  [7  elite   [HP  34]  [Move  0]  [Attack  7]  [Immunities  {"Muddle"}]]
end-monster

begin-ability-deck "Ancient Artillery"
  ["Massive Blast" 57 {"Push 1 all adjacent" "Attack -1, Range 3, aoe(ring1.rkt)"}]
  ["Massive Blast" 57 {"Push 1 all adjacent" "Attack -1, Range 3, aoe(ring1.rkt)"}]
  ["Massive Blast" 57 {"Push 1 all adjacent" "Attack -1, Range 3, aoe(ring1.rkt)"}]
  ["Massive Blast" 57 {"Push 1 all adjacent" "Attack -1, Range 3, aoe(ring1.rkt)"}]
  ["Massive Blast" 57 {"Push 1 all adjacent" "Attack -1, Range 3, aoe(ring1.rkt)"}]
  ["Massive Blast" 57 {"Push 1 all adjacent" "Attack -1, Range 3, aoe(ring1.rkt)"}]
  ["Massive Blast" 57 {"Push 1 all adjacent" "Attack -1, Range 3, aoe(ring1.rkt)"}]
  ["Massive Blast" 57 {"Push 1 all adjacent" "Attack -1, Range 3, aoe(ring1.rkt)"}]
end-ability-deck

Now, if you install the Frosthaven Manager and run it connected to a terminal window (to see error messages), you should be able to

  1. Skip all the inputs until you get to the "Monster DB" screen (click "Play" or "Next" until you can't).
  2. Click "Open Bestiary or Foes" and select the "bestiary.rkt" file from above.
  3. Click "Next".
  4. Click "Add Monster" and add a single Ancient Artillery (number and eliteness should not matter).
  5. Click "Next" and then "Draw Abilities". You should have one of the Massive Blast cards come up for Ancient Artillery; click the "AoE" button. Sometimes it triggers on the first click, sometimes not. But click and close the window until the window comes up blank, then check your error messages.

I see (for one click)

ctx: undefined;
 cannot use field before initialization
  context...:
   /Users/Knoble/Library/Racket/8.8/pkgs/gui-easy-lib/gui/easy/private/view/view.rkt:31:4: get-context method in context-mixin
   /Users/Knoble/Library/Racket/8.8/pkgs/gui-easy-lib/gui/easy/private/view/canvas.rkt:36:29
   /Applications/Racket v8.8/collects/racket/private/more-scheme.rkt:148:2: call-with-break-parameterization
   /Applications/Racket v8.8/collects/ffi/unsafe/atomic.rkt:73:13
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/canvas-mixin.rkt:144:4: do-on-paint method in canvas-mixin
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:435:6
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:486:32
   /Applications/Racket v8.8/collects/racket/private/more-scheme.rkt:148:2: call-with-break-parameterization
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:370:11: eventspace-handler-thread-proc
ctx: undefined;
 cannot use field before initialization
  context...:
   /Users/Knoble/Library/Racket/8.8/pkgs/gui-easy-lib/gui/easy/private/view/view.rkt:31:4: get-context method in context-mixin
   /Users/Knoble/Library/Racket/8.8/pkgs/gui-easy-lib/gui/easy/private/view/canvas.rkt:36:29
   /Applications/Racket v8.8/collects/racket/private/more-scheme.rkt:148:2: call-with-break-parameterization
   /Applications/Racket v8.8/collects/ffi/unsafe/atomic.rkt:73:13
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/canvas-mixin.rkt:144:4: do-on-paint method in canvas-mixin
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:435:6
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:486:32
   /Applications/Racket v8.8/collects/racket/private/more-scheme.rkt:148:2: call-with-break-parameterization
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:370:11: eventspace-handler-thread-proc
ctx: undefined;
 cannot use field before initialization
  context...:
   /Users/Knoble/Library/Racket/8.8/pkgs/gui-easy-lib/gui/easy/private/view/view.rkt:31:4: get-context method in context-mixin
   /Users/Knoble/Library/Racket/8.8/pkgs/gui-easy-lib/gui/easy/private/view/canvas.rkt:36:29
   /Applications/Racket v8.8/collects/racket/private/more-scheme.rkt:148:2: call-with-break-parameterization
   /Applications/Racket v8.8/collects/ffi/unsafe/atomic.rkt:73:13
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/canvas-mixin.rkt:144:4: do-on-paint method in canvas-mixin
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:435:6
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:486:32
   /Applications/Racket v8.8/collects/racket/private/more-scheme.rkt:148:2: call-with-break-parameterization
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:370:11: eventspace-handler-thread-proc

I can't explain the error, though, nor why it only shows up sometimes. Race condition?

Once you can reproduce the error, I recommend using File > Save Game to save the current game. Then you can reload it anytime with File > Load Game or, when launching the game through the command-line, by passing the save file as the first argument (e.g., frosthaven-manager <save-file>). Even here, sometimes it takes multiple "AoE" clicks to trigger the issue.

[Unfortunately the save files are not totally agnostic since they contain user-specific paths. If you wanted to create /Users/Knoble and paths under it for testing, I could send you a save file.]

button contract should allow bitmap%, not just label-text%

The underlying button% handles a bitmap% argument just fine, even when it's an observable of a bitmap! Try this minimal example where I import the private button file directly, bypassing the contract:

#lang racket/base
(require racket/class
         racket/function
         images/icons/control
         images/icons/style
         images/icons/stickman
         (only-in racket/gui timer%)
         (except-in racket/gui/easy button)
         racket/gui/easy/private/view/button
         racket/gui/easy/operator)

(define/obs @ms (current-inexact-milliseconds))
(new timer%
     [notify-callback (λ () (:= @ms (current-inexact-milliseconds)))]
     [interval 10])
(define/obs @stickman
  (@ms . ~> . (λ (ms)
                (running-stickman-icon
                 (/ ms 1000)
                 #:height 32
                 #:head-color run-icon-color
                 #:arm-color "white"
                 #:body-color run-icon-color))))

(render
 (window
  #:size '(200 200)
  (button @stickman (λ () (println (obs-peek @ms))))))

Your button contract should allow bitmap% too.

The original button% class can also accept another special thing that you may wish to support, see https://docs.racket-lang.org/gui/button_.html for the full contract.

popup-menu causing invalid memory reference errors

Every variation on the following program I have tried has failed:

#lang racket
(require racket/gui/easy)
(define root (window (button "Pop" (thunk (render-popup-window root (popup-menu (menu-item "Click Me" (thunk (displayln "Hi")))))))))

A blur of "invalid memory reference" errors flies by, and not even C-\ can stop them (I have to kill the process). This was based partly on the popup in your video in #13.

Platform details:

Welcome to Racket v8.4 [cs].
OS: macOS Catalina 10.15.7 19H1824 x86_64 
Host: MacBookPro16,1 
Kernel: 19.6.0 
Uptime: 8 days, 4 hours, 21 mins 
Packages: 211 (brew) 
Shell: zsh 5.7.1 
Resolution: 1792x1120, 3008x1692 
DE: Aqua 
WM: Quartz Compositor 
WM Theme: Purple (Dark) 
Terminal: alacritty 
CPU: Intel i9-9880H (16) @ 2.30GHz 
GPU: Intel UHD Graphics 630, AMD Radeon Pro 5500M 
Memory: 10087MiB / 16384MiB 

dialog with `close-button` style has no close button

Not sure if this is expected or what. The program in examples/modal.rkt sets the #:style of its dialog to '(close-button), but running it and clicking the button to render a dialog produces a dialog with no close button:

image

The only way to close the dialog is to press ESC.

Feature request: Checkboxes in menu items

Seems to be supported by racket/gui: https://docs.racket-lang.org/gui/checkable-menu-item_.html

I imagine the API would look something like:

(define/obs @high-contrast? #f)
(menu "View"
      (menu-item "High Contrast"
                 (λ (checked) (:= @high-contrast? checked)) ; <-- adding a checked parameter
                 #:checked @high-contrast? ; <-- #:checked observable
                 #:enabled? #t))

The callback action could do anything (but most likely be used to set an observable), and likewise, updating the #:checked observable would update the visible state of the menu item.

The function could be named menu-item or checkable-menu-item, but I think it's cuter to have checkable and non-checkable items both be called menu-item because then all the menu labels will align in the source code:

(menu-item "Debug..." ; <-- the left edge of this string...
           (λ () (show-debug-window)))
(menu-item "High Contrast" ; <-- ...lines up with the left edge of this string!
           (λ (checked) (:= @high-contrast? checked))
           #:checked @high-contrast?)

`obs-set!` function

Almost all of the observable "operators" are synonyms for named functions, but one common one is not: :=.

By convention with other observable functions, and with the rest of Racket, I think the obvious name for this would be obs-set!.

Maybe I'm just an old Lisp fart but I don't use the (a . f . b) syntax and I don't find it easy to mentally switch between prefix and infix notation, and I have a hard time remembering all the ><~=# characters, so a simple (obs-set! o v) would be really nice to have. Yeah, it's easy to write myself, or I could use (obs-update! o (λ (_) v)) ... I know this is a silly little request.

`group` does not accept styles shown in docs

The docs say that group accepts these values in the #:style argument:

(listof (or/c 'border 'deleted
              'hscroll 'auto-hscroll 'hide-hscroll
              'vscroll 'auto-vscroll 'hide-vscroll))

…but of these, only 'deleted is actually accepted. Using any of the other values results in:

initialization for group-box-panel%: invalid symbol in given style list
  invalid symbol: 'border
  given: '(border)

This makes sense, given that the underlying group-box-panel% also only accepts 'deleted for its style argument.

Clean way to implement a list of checkboxes?

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:

  • Despite all the line breaks through the program, there's still barely enough horizontal space for that final line and no good place to break it.
  • Inside list-view, each element of @food has to be extracted separately so it can be used in the checkbox. (I've encountered a similar frustration with list-view in other project.)
  • @food includes the key as its car, but I already have the key in k. If the key was excluded from @food, I wouldn't need to cdr, so the next line could be shortened down to (define name (@food . ~> . food^-name)), which is much better. (Though this wouldn't matter if the prior point could be solved directly.)
  • Having @food already available, but then having to dict-update on foods, adds more code. It would be nice if there was a shortcut to update @food itself. (This can't be done directly because it's derived, but maybe some kind of helper...?)

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))))
  • Less code and shorter lines overall!
  • Storing the @foods-checked status separately from @foods makes it much easier to extract and update the relevant properties inside list-view.
  • Extracting the name is easier: (define name (@food . ~> . cdr)) (though could be easier still if it was unpacked for me)
  • Minor inconvenience of having to join @foods and @foods-checked together in order to use them fully.
  • Something rubs me the wrong way about ignoring the argument to the checkbox action function.

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 :)

(progress #:range) contract

The contract for progress's #:range is documented as (maybe-obs gui:dimension-integer?) which should be equivalent to (maybe-obs (integer-in 0 1000000)).

However:

(render (window (progress 0 #:range 0)))

gives the following, which references positive-dimension-integer? in the #:range argument

progress: contract violation
  expected: positive-dimension-integer?
  given: 0
  in: a part of the or/c of
      the #:range argument of
      (->*
       ((or/c
         position-integer?
         (obs/c position-integer?)))
       (#:enabled?
        (or/c boolean? (obs/c boolean?))
        #:label
        (or/c label-string? (obs/c label-string?))
        #:min-size
        (or/c
         (list/c
          (or/c #f dimension-integer?)
          (or/c #f dimension-integer?))
         (obs/c
          (list/c
           (or/c #f dimension-integer?)
           (or/c #f dimension-integer?))))
        #:range
        (or/c
         positive-dimension-integer?
         (obs/c positive-dimension-integer?))
        #:stretch
        (or/c
         (list/c boolean? boolean?)
         (obs/c (list/c boolean? boolean?)))
        #:style
        (listof
         (or/c
          'horizontal
          'vertical
          'plain
          'vertical-label
          'horizontal-label
          'deleted)))
       (is-a?/c view<%>))
  contract from:
      <pkgs>/gui-easy-lib/gui/easy/view.rkt
  blaming: top-level
   (assuming the contract is correct)
  at: <pkgs>/gui-easy-lib/gui/easy/view.rkt:145:3

P.S. Hopefully you are taking these reports as signs of an enthusiastic user rather than gripes and complaints. As I play with things, often in the setting of a personal but real project, I run into questions or concerns. I plan to keep playing, because gui-easy is far more ergonomic for me than racket/gui ❤️

Render popup-menus relative to a child widget?

The render-popup-menu function takes x-y coordinates and renders a popup-menu (pum) relative to the root widget of a renderer.

Canvases are windows and can react to mouse events, so I can use a mixin with, say, pict-canvas to make right-clicks shows a popup-window if I have a renderer. The mouse-event's x-y coordinates are relative to the canvas (!).

I get a renderer only by calling render on the full tree of views, so its root widget is from the (window …) view.

This combines to result in the x-y coordinates being relative to the wrong part of the GUI, so the pum is in the wrong place.

Possible solutions I've come up with:

  1. Hack around it, duplicate some things from render-pum via reflection (benknoble/frosthaven-manager@53c567c)
  2. Translate the canvas x-y coordinates with respect to (renderer-root …). This would probably be much nicer, but I can't figure out how to do it.
  3. Manually compute approximate coordinates based on expected layout. This doesn't sit well with me, especially since I expect to re-use some views in very different compositions or layouts.

Have you run into this in the past? How did you solve it?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.