Bogdan Popa <bogdan@defn.io>
The goal of GUI Easy is to simplify user interface construction in Racket by wrapping the existing imperative API (racket/gui) in a functional shell.
[γγ¬γΌγ ]
gui-easy can be broadly split up into two parts: observables and views.
Observables contain values and notify subscribed observers of changes to their contents. Views are representations of racket/gui widget trees that, when rendered, produce concrete instances of those trees and handle the details of wiring state and widgets together.
The core abstractions of observables and views correspond to a model-view-controller (MVC) architecture for graphical applications as popularized by Smalltalk-80.
The code above describes a view hierarchy rooted in a window that contains the text “Hello, World!”. By itself, it doesn’t do much, but you can take it and pass it to render to convert it into a native GUI:
State in gui-easy is held by observables.
racket/gui/easy/operator)
Here we define an observable called @count that holds the current value of a counter. The two button s change the value of the counter when clicked and the text view displays its current string value via a derived observable. The three widgets are laid out horizontally by the hpanel .
Since views are at their core just descriptions of a GUI, it’s easy to abstract over them and make them reusable.
racket/gui/easy/operator)
Taking the previous example further, we can render a dynamic list of counters.
racket/gui/easy/operator)entry)))@counters#:keycar(counter
Here the @counters observable holds a list of pairs where the first element of a pair is the id of each counter and the second is its count. When the “Add counter” button is clicked, a new counter is added to the list. The list-view renders each individual counter by passing in a derived observable to its make-view argument.
For more information about the core concepts of gui-easy and its design, see [knoblepopa23]. For more examples, see the "examples" directory in the [repo-link].
See Geometry Management in the racket/gui docs for details on how views get laid out.
Containers, Windows & Dialogs take optional keyword arguments that allow you to control the #:spacing and #:alignment of their children and their own #:min-size, #:stretch and #:margin. All of these arguments can be passed as either regular values or as observables, in which case the properties they control will vary with changes to the observables.
You can create your own views by implementing the view<%> interface.
As an example, let’s wrap Jeffrey Massung’s canvas-list<%>. I find it helps to work backwards from the API you’d like to end up with. In this case, that would be:
(canvas-list@entries
A canvas-list takes an observable of a list of entries, a function that knows how to draw each entry to a gui:dc<%> and a callback for when the user double-clicks an entry. The canvas-list function should then look something like this:
[@entries@entries][drawdraw][actionaction]))
All it needs to do is abstract over the instantiation of the underlying view<%> . Next, we can define a skeleton implementation of canvas-list-view%:
Views must communicate what observables they depend on to their parents. Since the only dependency a canvas list has is its set of entries. That’s straightforward:
When a view is rendered, its parent is in charge of calling its create method. That method must instantiate a GUI object, associate it with the passed-in parent, perform any initialization steps and then return it. In our case:
[parentparent](drawentrystatedcwh))](actionitem))]))
When the observables the view depends on change, its parent will call its update method with the GUI object that the view returned from its create method, the observable that changed and the observable’s value when it changed. The view is then in charge of modifying its GUI object appropriately.
Windows are a special case: the resources they manage only get disposed of when renderer-destroy is called, or when the program exits.
Finally, when a view is no longer visible, its destroy method is called to dispose of the GUI object and perform any teardown actions. In our case, there’s nothing to tear down so we can let garbage collection take care of destroying the canvas-list% object:
When the view becomes visible again, its create method will be called again and the whole cycle will repeat itself.
That’s all there is to it when it comes to custom controls. See the "examples/hn.rkt" example for a program that uses a custom view.
Containers are slightly more complicated to implement than controls. They must collect all their children’s unique dependencies and list them in their dependencies method. Additionally, their update method is in charge of dispatching updates to their children.
See "gui-easy-lib/gui/easy/private/view/panel.rkt" for an example.
Some views take a #:mixin argument that can be used to alter the behavior of the underlying widget. These are intended to be used as “escape hatches” when the library doesn’t provide a piece of functionality you need, but that functionality is available on the native widget.
See "examples/close-window.rkt" for an example of using a mixin to programmatically toggle a window’s visibility.
Renderers convert view definitions to GUI elements.
Use this function when you need to embed one or more view<%> s within an existing racket/gui application. Otherwise, use render .
When a parent renderer is provided, renders the view as a child of the root view of parent. This is useful when you need to render a modal dialog on top of an existing window.
procedure
( render-popup-menu parentviewxy)→void?
parent:renderer?
procedure
( renderer-root r)→any/c
procedure
( renderer-destroy r)→void?
Views are functions that return a view<%> instance.
Views might wrap a specific GUI widget, like a text message or button, or they might construct a tree of smaller views, forming a larger component.
Views are typically Observable-aware in ways that make sense for each individual view. For instance the text view takes as input an observable string and the rendered text label updates with changes to that observable.
Many racket/gui widgets are already wrapped by GUI Easy, but programmers can implement the view<%> interface themselves in order to integrate arbitrary widgets, such as those from 3rd-party packages in the Racket ecosystem, into their projects.
procedure
#:sizesize#:alignmentalignment#:positionposition#:min-sizemin-size#:stretchstretch#:stylestyle#:mixinmix]style :'no-system-menu'hide-menu-bar'toolbar-button'float'metal'fullscreen-button'fullscreen-aux))= null
procedure
#:sizesize#:alignmentalignment#:positionposition#:min-sizemin-size#:stretchstretch#:stylestyle#:mixinmix]
procedure
( popup-menu menu-or-item...)→(is-a?/c popup-menu-view<%> )
(menu"File"
(menu"File"(menu"Help"
Changed in version 0.15 of package gui-easy-lib: The #:enabled? argument.
Changed in version 0.15 of package gui-easy-lib: The #:enabled? and #:help arguments.
procedure
[ action#:enabled?enabled?#:helphelp-text
Changed in version 0.15 of package gui-easy-lib: The #:enabled?, #:help and #:shortcut arguments.
procedure
[ action#:checked?checked?#:enabled?enabled?#:helphelp-text
Added in version 0.18 of package gui-easy-lib.
procedure
( menu-item-separator )→(is-a?/c view<%> )
procedure
#:stylestyle#:enabled?enabled?#:spacingspacing#:marginmargin#:min-sizemin-size#:stretchstretch#:mixinmix]style :'hscroll'auto-hscroll'hide-hscroll'vscroll'auto-vscroll'hide-vscroll))= null
Changed in version 0.13 of package gui-easy-lib: Added the #:mixin argument.
procedure
#:stylestyle#:enabled?enabled?#:spacingspacing#:marginmargin#:min-sizemin-size#:stretchstretch#:mixinmix]style :'hscroll'auto-hscroll'hide-hscroll'vscroll'auto-vscroll'hide-vscroll))= null
Changed in version 0.13 of package gui-easy-lib: Added the #:mixin argument.
procedure
[ #:alignmentalignment#:stylestyle#:enabled?enabled?#:spacingspacing#:marginmargin#:min-sizemin-size#:stretchstretch#:mixinmix]
Changed in version 0.13 of package gui-easy-lib: Added the #:mixin argument.
procedure
actionchild...[ #:choice->labelchoice->label#:choice=?choice=?#:selectionselection#:alignmentalignment#:enabled?enabled?#:stylestyle#:spacingspacing#:marginmargin#:min-sizemin-size
The #:choice->label argument controls how each choice is displayed and the #:choice=? argument controls how the current #:selection is compared against the list of choices to determine the currently selected tab.
On user interaction, action is called with a symbol representing the event, the set of choices at the moment the action occurred and the current selection. The selection may be adjusted depending on the event (eg. when the current tab is closed, the selection changes to an adjacent tab). When tabs are reordered, the choices provided to the action represent the new tab order.
See "examples/tabs.rkt" for an example.
Changed in version 0.3 of package gui-easy-lib: Added the #:choice=? argument.
Changed in version 0.3: The selection is now a value in the set of choices instead of an index.
syntax
( if-view cond-ethen-eelse-e)
Changed in version 0.4 of package gui-easy-lib: The if-view form was converted from a procedure into a syntactic form.
procedure
make-view[ #:keykey#:alignmentalignment#:enabled?enabled?#:stylestyle#:spacingspacing#:marginmargin#:min-sizemin-size#:stretchstretchstyle :'hscroll'auto-hscroll'hide-hscroll'vscroll'auto-vscroll'hide-vscroll))= '(verticalauto-vscroll)
See "examples/list.rkt" for an example.
procedure
[ make-view
Added in version 0.9 of package gui-easy-lib.
procedure
draw[ #:labellabel#:enabled?enabled?#:stylestyle#:marginmargin#:min-sizemin-size#:stretchstretch
procedure
make-pict[ #:labellabel#:enabled?enabled?#:stylestyle#:marginmargin#:min-sizemin-size#:stretchstretch
procedure
make-snip[ #:labellabel#:enabled?enabled?#:stylestyle#:marginmargin#:min-sizemin-size#:stretchstretch
procedure
make-snip[ update-snip#:labellabel#:enabled?enabled?#:stylestyle#:marginmargin#:min-sizemin-size#:stretchstretch
procedure
action[ #:enabled?enabled?#:stylestyle#:fontfont#:marginmargin#:min-sizemin-sizelabel :
procedure
[ #:labellabel#:checked?checked?
procedure
action[ #:choice->labelchoice->label#:choice=?choice=?#:selectionselection#:labellabel#:stylestyle#:enabled?enabled?#:min-sizemin-size
The #:choice->label argument controls how each choice is displayed and the #:choice=? argument controls how the current #:selection is compared against the list of choices to determine the selection index.
The #:mode argument controls how the image stretches to fill its container. If the mode is 'fit, then the image will preserve its aspect ratio, otherwise it will stretch to fill the container.
Changed in version 0.11.1 of package gui-easy-lib: The canvas background is now
'transparent. Now passes #t to the
#:try-@2x? argument of gui:read-bitmap .
Changed in version 0.17: The first argument may now be a
gui:bitmap% .
procedure
[ action#:labellabel#:enabled?enabled?#:background-colorbackground-color#:stylestyle#:fontfont#:keymapkeymap#:marginmargin#:min-sizemin-size#:stretchstretch#:mixinmix#:value=?value=?= #f= '(single)
The #:value=? argument controls when changes to the input data are reflected in the contents of the field. The contents of the input field only change when the new value of the underlying observable is not value=? to the previous one. The only exception to this is when the textual value (via #:value->text) of the observable is the empty string, in which case the input is cleared regardless of the value of the underlying observable.
The #:value->text argument controls how the input values are rendered to strings. If not provided, value must be either a string? or an observable of strings.
Changed in version 0.21 of package gui-easy-lib: input also responds to the 'has-focus and 'lost-focus events.
procedure
[ #:labellabel#:enabled?enabled?#:stylestyle#:rangerange#:min-sizemin-size= '(horizontal)
procedure
action[ #:choice->labelchoice->label#:choice=?choice=?#:selectionselection#:labellabel#:stylestyle#:enabled?enabled?#:min-sizemin-size
The #:choice->label argument controls how each choice is displayed and the #:choice=? argument controls how the current #:selection is compared against the list of choices to determine the selection index.
Unlike choice , the set of choices cannot be changed.
procedure
action[ #:labellabel#:enabled?enabled?#:stylestyle#:min-valuemin-value#:max-valuemax-value#:min-sizemin-size= '(horizontal)
procedure
entries[ action#:entry->rowentry->row#:selectionselection#:labellabel#:enabled?enabled?#:stylestyle#:fontfont#:marginmargin#:min-sizemin-size#:stretchstretch#:column-widthscolumn-widthsselection : = #fstyle :'vertical-label'horizontal-label'variable-columns'column-headers'clickable-headers'reorderable-headers'deleted))= '(singlecolumnn-headersclickable-headersreorderable-headers)column-widths := null
The #:entry->row argument converts each row in the input data for display in the table.
The #:column-widths argument controls the widths of the columns. Column lengths can be specified either as a list of the column index (starting from 0) and the default width or a list of the column index, the column width, the minimum width and the maximum width.
Changed in version 0.13 of package gui-easy-lib: Added the #:mixin argument.
= #f
Added in version 0.14 of package gui-easy-lib.
interface
method
(send a-view dependencies )→(listof obs? )
Returns the set of observers that this view depends on.method
(send a-view create parent)→(is-a?/c gui:area<%> )
Instantiates the underlying GUI object, associates it with parent and returns it so that the parent of this view<%> can manage it.Responds to a change to the contents of dep. The val argument is the most recent value of dep and the v argument is the GUI object created by create.Destroys the GUI object v and performs any necessary cleanup.
interface
Returns a new gui:top-level-window<%> belonging to parent.method
(send a-window-view is-dialog? )→boolean?
Returns #t if this view is a dialog.
interface
parent:#fReturns a new gui:popup-menu% .
mixin
context-mixin :(class? . -> .class? )
interface
method
(send a-context set-context kv)→void?
k:any/cv:any/cStores v under k within the context, overriding any existing values.method
(send a-context set-context* kv......)→void?
k:any/cv:any/cStores each v under each k within the context.method
(send a-context get-context k[default])→any/c
k:any/cReturns the value stored under k from the context. If there is no value, the result is determined by default:
If default is a procedure? , it is called with no arguments to produce a result.
Otherwise, default is returned unchanged.
method
(send a-context get-context! kdefault)→any/c
k:any/cdefault:any/cLike get-context, but if there is no value stored under k, the default value is computed as in get-context, stored in the context under k and then returned.method
(send a-context remove-context k)→void?
k:any/cRemoves the value stored under k from the context.method
(send a-context clear-context )→void?
Removes all stored values from the context.
Observables are containers for values that may change over time. Their changes may be observed by arbitrary functions.
observer 1 got 1
observer 2 got 1
1
Derived observables are observables whose values depend on other observables. Derived observables cannot be updated using obs-update! .
> @strs(obs "1" #:name 'anon #:derived? #t)
obs-update!: contract violation
expected: (not/c obs-derived?)
given: (obs "1" #:name 'anon #:derived? #t)
Internally, every observable has a unique handle and two observables are equal? when their handles are eq? . This means that equality (via equal? ) is preserved for impersonated observables, such as those guarded by obs/c .
v:any/c
The #:name of an observable is visible when the observable is printed so using a custom name can come in handy while debugging code.
The #:derived? argument controls whether or not the observable may be updated.
Observables implement prop:object-name , so the name of an observable is also accessible generically via object-name .
Added in version 0.20 of package gui-easy-lib.
procedure
( obs-rename oname)→obs?
o:obs?name:symbol?
Added in version 0.16 of package gui-easy-lib.
Added in version 0.11 of package gui-easy-lib.
Added in version 0.11 of package gui-easy-lib.
This combinator retains a strong reference to each of the last values of the respective observables that are being combined until they change.
procedure
( obs-debounce o[#:durationduration-ms])→obs?
o:obs?
procedure
( obs-throttle o[#:durationduration-ms])→obs?
o:obs?
procedure
[ #:refref-proco:obs?
Added in version 0.19 of package gui-easy-lib.
syntax
( define/obs nameinit-expr)
Added in version 0.11 of package gui-easy-lib.
value
value
value
value
value
value
procedure
( maybe-obs/c c)→contract?