Unicorn_jsoo
Unicorn provides a small algebra of seven combinators to define GUI applications:
empty
for when you don't want anything drawna & b
when you want to display two or more widgetsiso i w
to adapt an existing widget w
to a different type (similar to "map")on lens w
when you want to edit a product/tuple/recordinto prism w
for pattern matching over sum typesstateful s w
to hide the internal state of the widget w
dynamic
for complete control over the widget's position, movement, creation, history, etc.These combinators are the glue to assemble complex applications out of smaller widgets. The jsoo
(js_of_ocaml
) backend additionnally provides the primitive widgets and HTML support.
val run : ?id:string -> 'a t -> 'a -> unit
run w x
renders the widget w
with the initial state x
, then proceed to update it in reaction to user events. It's the main loop that drives the GUI application, so you probably want to call it only once.
?id
is specified, the widget renders in the HTML node with the corresponding id
attribute. Otherwise, the widget is appended to the HTML body of the webpage.val empty : 'a t
The empty
widget, invisible and ineffective.
a & b
displays both widgets a
and b
side by side.
∀ w. empty & w == w == w & empty
∀ a b c. a & (b & c) == a & b & c == (a & b) & c
b & a
displays the widgets in a different order.The polymorphic 'a
in an 'a t
widget is invariant: It is consummed when rendering, but also produced in reaction to user events. As such, we always need pair of functions to "map" over it:
iso i w
maps the isomorphism i
over the widget w
.
∀ i. iso i empty == empty
and ∀ w. map Iso.id w == w
∀ i j w. iso i (iso j w) == map (Iso.compose i j) w
∀ i a b. iso i a & iso i b == iso i (a & b)
val on : ( 'a, 'b ) Optic.lens -> 'b t -> 'a t
on lens w
zooms on a sub-field of the state 'a
with the lens
and renders/updates it with the widget w
.
∀ lens. on lens empty == empty
and ∀ w. on Lens.id w == w
∀ l1 l2 w. on l1 (on l2 w) == on (Lens.compose l1 l2) w
∀ lens a b. on lens a & on lens b == on lens (a & b)
val into : ( 'a, 'b ) Optic.prism -> 'b t -> 'a t
into prism w
pattern matches the state 'a
with the prism
, to render the widget w
. If the prism fails, then it renders empty
.
∀ prism. into prism empty == empty
and ∀ w. into Prism.id w == w
∀ p1 p2 w. into p1 (on p2 w) == into (Prism.compose p1 p2) w
∀ prism a b. into prism a & into prism b == into prism (a & b)
val case : ( 'a, 'b ) Optic.prism -> 'b t -> 'a t
case prism w
is the same as into prism w
, but the internal state of the widget w
is reset when the prism is not satisfied.
∀ p w. into p w == stateful w (case Prism.(product id p) dynamic)
stateful s w
encapsulates the internal state of the widget w
with an initial value s
.
∀ s. stateful s empty == empty
and ∀ s w. stateful s (on Lens.snd w) == w
∀ s1 s2 w. stateful s1 (stateful s2 w) == stateful (s1, s2) (iso Iso.assoc w)
∀ s a b. stateful s a & b == stateful s (a & on Lens.snd b)
and ∀ s a b. a & stateful s b == stateful s (on Lens.snd a & b)
∀ i s w. iso i (stateful s w) = stateful s (iso (Iso.product id i) w)
and same for lenses and prisms.However, internal state is not shared: stateful s a & stateful s b =/= stateful s (a & b)
.
dynamic
renders and updates a dynamic widget coming from the state.
∀ w. w == stateful w dynamic
The following are defined from the previous combinators.
cond p w
renders the widget w
when the predicate p
is satisfied.
∀ p. cond p empty == empty
and ∀ w. cond (fun _ -> true) w == w
and ∀ w. cond (fun _ -> false) w == empty
∀ p q w. cond p (cond q w) == cond (fun x -> p x && q x) w
∀ p a b. cond p (a & b) == cond p a & cond p b
ifte p if_true if_false
renders the widget if_true
when p
is satisfied, otherwise if_false
.
Same as cond
, but the internal state is lost when the predicate is unsatisfied (see case
).
stateful_by f w
is the same as stateful
, but the initial value is computed from the current state.
As OCaml is a strict language, recursive definitions need special support as this would fail to terminate:
let rec list w = into Prism.cons (w <*> list w)
and one should rather delay slightly the recursion:
let rec list w = into Prism.cons (w <*> apply list w)
of_lazy w
delays the lazy widget w
, such that of_lazy (lazy w) == w
fix f
uses of_lazy
to simplify the definition of recursive traversals.
val str : string -> 'a t
str s
displays the constant string s
as HTML.
val text : ( 'a -> string ) -> 'a t
text f
renders a dynamic string using the function f
on the current state.
val checkbox : bool t
checkbox
is a primitive widget to render and edit a boolean.
val input_int : int t
input_int
is a primitive widget to render and edit an integer as a text input.
val input_string : string t
input_string
is a primitive widget to render and edit a single-line string.
button w
is an HTML button using the widget w
for its contents. The reaction when the button is clicked can be specified with E.click
:
button (str "Click" & E.click (fun old_state -> new_state))
module H : sig ... end
HTML elements and nodes, like div, span, etc.
module A : sig ... end
HTML attributes and properties.
module E : sig ... end
HTML events.