Liking cljdoc? Tell your friends :D

Tutorial

Getting started
Hello World
Coordinates
Graphics
    Text
    Lines and Shapes
    Color
    Transforms
    Groups
    Simple Layouts
    Common UI Elements
Events
    Event flow
        Simply display a child element and ignore effects that would be generated by a mouse-down event.
        Ignore all events from a child element
        Filter some events
        Transform some events
    Event Handlers
        :mouse-down
        :mouse-up
        :mouse-move
        :mouse-event
        :key-event
        :key-press
        :clipboard-copy
        :clipboard-cut
        :clipboard-paste
    Effect bubbling
    Side effects in event handlers
Components
    Running components
    File Selector example
Examples of form defui understands
Component gotchas

Getting started

Add this dependency to your project:

[com.phronemophobic/membrane "0.9.0-beta"]

Hello World

All examples below will use the following namespace requires.

(:require #?(:clj [membrane.skia :as skia])
            [membrane.ui :as ui
             :refer [vertical-layout
                     translate
                     horizontal-layout
                     button
                     label
                     with-color
                     bounds
                     spacer
                     on]]
            [membrane.component :as component
             :refer [defui run-ui run-ui-sync defeffect]]
            [membrane.basic-components :as basic])

Below is the "Hello World!" program for membrane:

(skia/run #(ui/label "Hello World!"))

This will pop up a window that says "Hello World!".

membrane.skia/run takes a 0 argument function that returns a value that implements the IDraw protocol. Implementations of the IDraw protocol can be tricky and platform dependent. Thankfully, the implementations for many graphical primitives are provided by the membrane graphics backends.

Currently, there are two supported graphics backends, skia for desktop and webgl for web.

Coordinates

Coordinates

Coordinates are represented as a vector of two numbers [x, y].

Graphics

The membrane.ui namespace provides graphical primitives you can use to specify your UI.

See below for quick reference of some of the graphical primitives available in membrane.ui. For more examples, check out the kitchen sink or if you have the membrane library as a dependency, run it with lein run -m membrane.example.kitchen-sink.

Text

Label Default

;; label using default font
(ui/label "Hello\nWorld!")

Label with font

;; label with specified font
;; font will check the default System font folder
;; on Mac osx, check /System/Library/Fonts/ for available fonts
(ui/label "Hello\nWorld!" (ui/font "Menlo.ttc" 22))

Label with font

;; Use the default font, but change the size
(ui/label "Hello\nWorld!" (ui/font nil 22))

Lines and Shapes

Star

;; filled polygon
(ui/path [24.20 177.98]
         [199.82 37.93]
         [102.36 240.31]
         [102.36 15.68]
         [199.82 218.06]
         [24.20 78.01]
         [243.2 127.99]
         [24.20 177.98])

Star

;; line
(ui/with-style :membrane.ui/style-stroke
  (ui/with-stroke-width 3
    (ui/with-color [1 0 0]
      (apply
       ui/path
       (for [i (range 10)]
         [(* 30 i)
          (if (even? i) 0 30)])))))

rectangle

;; draw a filled  rectangle
(ui/with-style :membrane.ui/style-stroke
  (ui/with-stroke-width 3
    (ui/with-color [0.5 0 0.5]
      (ui/rectangle 100 200))))


rounded rectangle

;; rounded rectangle
(ui/rounded-rectangle 200 100 10)

Color

label with color

;; colors are vectors of [red green blue] or [red green blue alpha]
;; with values from 0 - 1 inclusive

(ui/with-color [1 0 0]
  (ui/label "Hello"))

Groups

To draw multiple elements, simply use a vector. The elements will be drawn in order.

label with color

[(ui/with-color [1 0 0 0.75]
   (ui/label "red"))
 (ui/with-color [0 1 0 0.75]
   (ui/label "green"))
 (ui/with-color [0 0 1 0.75]
   (ui/label "blue"))]

Transforms

translate

[(ui/with-style :membrane.ui/style-stroke
   [(ui/path [0 0] [0 100])
    (ui/path [0 0] [60 0])])
 (ui/rectangle 30 50)
 (ui/translate 30 50
               (ui/rectangle 30 50))]

scale

(ui/scale 3 10
    (ui/label "sx: 3, sy: 10"))

Basic Layout

vertical layout

;; vertical
(ui/vertical-layout
 (ui/button "hello")
 (ui/button "world"))

horizontal layout

;; horizontal
(ui/horizontal-layout
 (ui/button "hello")
 (ui/button "world"))

horizontal layout spacing

(apply ui/horizontal-layout
       (interpose
        (spacer 10 0)
        (for [i (range 1 5)]
          (ui/with-color [0 0 0 (/ i 5.0)]
            (ui/rectangle 100 50)))))

centering

;; most layouts can be created just by using bounds
(defn center [elem [width height]]
  (let [[ewidth eheight] (bounds elem)]
    (translate (int (- (/ width 2)
                       (/ ewidth 2)))
               (int (- (/ height 2)
                       (/ eheight 2)))
               elem)))
(ui/with-style :membrane.ui/style-stroke
  (let [rect (ui/rectangle 100 50)
        line (center (ui/path [0 0] [75 0])
                     (ui/bounds rect))]
    [rect line]))

Common UI Elements

Typically, the graphics for elements and their event handling code is intertwined. These elements are simply views and have no default behavior associated with them. If you want both, check out membrane.component/button and membrane.component/checkbox.

checkbox

(ui/horizontal-layout
 (ui/padding 10 10
             (ui/checkbox false))
 (ui/padding 10 10
             (ui/checkbox true)))

button

(ui/horizontal-layout
 (ui/padding 10 10
             (ui/button "button"))
 (ui/padding 10 10
             (ui/button "button" nil true)))

Events

In membrane, events handlers are pure functions that take in events and return a sequence of effects. Effects are a data description of what to do rather than a side effect.

;; mouse-down event at location [15 15]
(let [mpos [15 15]]
  (ui/mouse-down
   (ui/translate 10 10
                 (ui/on :mouse-down (fn [[mx my]]
                                   ;;return a sequence of effects
                                   [[:say-hello]])
                     (ui/label "Hello")))
   mpos))
>> ([:say-hello])

Event flow

Conceptually, event handling is hierarchical. When an event occurs, it asks the root element what effects should take place. The root element may

  1. simply return effects
  2. delegate to its child elements by asking them what effects should take place and returning those effects
  3. partially delegate by asking its child elements what effects should take place and transforming or filtering those effects before returning

The default behavior for most container components is #2, delegating to their child components. However, the parent component always has the final say over what effects should occur over its child components. This is the functional equivalent of the browser's event.preventDefault and event.stopPropagation.

Here are few illustrative examples:

Simply display a child element and ignore effects that would be generated by a mouse-down event.

(let [elem (ui/on
            :mouse-down (fn [_] nil)
            (ui/button "Big Red Button"
                       (fn []
                         [[:self-destruct!]])))]
     (ui/mouse-down elem [20 20]))
>>> ()

Ignore all events from a child element

It's useful for graphical GUI builders and testing to be able to silence components.

(let [elem (ui/no-events
            (ui/button "Big Red Button"
                       (fn []
                         [[:self-destruct!]])))]
  (ui/mouse-down elem [20 20]))
>>> ()

Filter some events

(let [lower-case-letters (set (map str "abcdefghijklmnopqrstuvwxyz"))
      child-elem (ui/on :key-press
                        (fn [s]
                          [[:child-effect1 s]
                           [:child-effect2 s]])
                        (ui/label "child elem"))
      elem (ui/on
            :key-press (fn [s]
                         (when (contains? lower-case-letters s)
                           (ui/key-press child-elem s)))
            child-elem)]
  {"a" (ui/key-press elem "a")
   "." (ui/key-press elem ".")})
>>> {"a" [[:child-effect1 "a"] [:child-effect2 "a"]], 
     "." nil}

Transform some events

(let [child-elem (ui/on :key-press
                        (fn [s]
                          [[:child-effect1 s]
                           [:child-effect2 s]])
                        (ui/label "child elem"))
      elem (ui/on
            :key-press (fn [s]
                        (if (= s ".")
                          [[:do-something-special]]
                          (ui/key-press child-elem s)
                          ))
            child-elem)]
  {"a" (ui/key-press elem "a")
   "." (ui/key-press elem ".")})
>>> {"a" [[:child-effect1 "a"] [:child-effect2 "a"]],
     "." [[:do-something-special]]}

Event Handlers

Event handlers can be provided using membrane.ui/on.

:mouse-down

:mouse-down [[mx my]] mpos is a vector of [mx, my] in the elements local coordinates.

Will only be called if [mx my] is within the element's bounds

(on :mouse-down (fn [[mx my :as mpos]]
                  ;;return a sequence of effects
                  [[:hello mx my]])
    (ui/label "Hello"))

:mouse-up

:mouse-up [[mx my]] mpos is a vector of [mx, my] in the elements local coordinates.

Will only be called if [mx my] is within the element's bounds

(on :mouse-up (fn [[mx my :as mpos]]
                ;;return a sequence of effects
                [[:hello mx my]])
    (ui/label "Hello"))

:mouse-move

:mouse-move [[mx my]] mpos is a vector of [mx, my] in the elements local coordinates.

Will only be called if [mx my] is within the element's bounds

(on :mouse-move (fn [[mx my :as mpos]]
                  ;;return a sequence of effects
                  [[:hello mx my]])
    (ui/label "Hello"))

:mouse-event

:mouse-event [mpos button mouse-down? mods] mpos is a vector of [mx, my] in the elements local coordinates. button is 0 if left click, 1 if right click. greater than 1 for more exotic mouse buttons mouse-down? is true if button is pressed down, false if the button is being released mods is an integer mask. masks are

SHIFT   0x0001
CONTROL   0x0002
ALT   0x0004
SUPER   0x0008
CAPS_LOCK   0x0010
NUM_LOCK   0x0020

Will only be called if [mx my] is within the element's bounds

(on :mouse-event (fn [[mpos button mouse-down? mods]]
                   ;;return a sequence of effects
                   [[:hello mx my]])
    (ui/label "Hello"))

:key-event

:key-event [key scancode action mods] key is a string if it is printable otherwise one of

:grave_accent :world_1 :world_2 :escape :enter :backspace :insert :delete :right :left :down :up :page_up :page_down :home :end :caps_lock :scroll_lock :num_lock :print_screen :pause :f1 :f2 :f3 :f4 :f5 :f6 :f7 :f8 :f9 :f10 :f11 :f12 :f13 :f14 :f15 :f16 :f17 :f18 :f19 :f20 :f21 :f22 :f23 :f24 :f25 :kp_0 :kp_1 :kp_2 :kp_3 :kp_4 :kp_5 :kp_6 :kp_7 :kp_8 :kp_9 :kp_decimal :kp_divide :kp_multiply :kp_subtract :kp_add :kp_enter :kp_equal :left_shift :left_control :left_alt :left_super :right_shift :right_control :right_alt :right_super :menu

scancode The scancode is unique for every key, regardless of whether it has a key token. Scancodes are platform-specific but consistent over time, so keys will have different scancodes depending on the platform but they are safe to save to disk. action one of :press, :repeat, :release, or :unknown if the underlying platform documentation has lied. mods is an integer mask. masks are

SHIFT 0x0001 CONTROL 0x0002 ALT 0x0004 SUPER 0x0008 CAPS LOCK 0x0010 NUM LOCK 0x0020

(on :key-event (fn [key scancode action mods]
                 [[:hello key scancode action mods]])
    (ui/label "Hello"))

:key-press

:key-press [key] key is a string if it is printable otherwise one of

:grave_accent :world_1 :world_2 :escape :enter :backspace :insert :delete :right :left :down :up :page_up :page_down :home :end :caps_lock :scroll_lock :num_lock :print_screen :pause :f1 :f2 :f3 :f4 :f5 :f6 :f7 :f8 :f9 :f10 :f11 :f12 :f13 :f14 :f15 :f16 :f17 :f18 :f19 :f20 :f21 :f22 :f23 :f24 :f25 :kp_0 :kp_1 :kp_2 :kp_3 :kp_4 :kp_5 :kp_6 :kp_7 :kp_8 :kp_9 :kp_decimal :kp_divide :kp_multiply :kp_subtract :kp_add :kp_enter :kp_equal :left_shift :left_control :left_alt :left_super :right_shift :right_control :right_alt :right_super :menu

(on :key-press (fn [key] [[:respond-to-key key]]) (ui/label "Hello"))

:clipboard-copy

:clipboard-copy [] Called when a clipboard copy event occurs.

:clipboard-cut

:clipboard-cut Called when a cliboard cut event occurs.

:clipboard-paste

:clipboard-paste [s] s is the string being pasted Called when a clipboard paste event occurs.

Effect bubbling

Another benefit of having a value based event system is that you can also easily transform and filter effects.

(defn search-bar [s]
  (horizontal-layout
   (on
    :mouse-down (fn [_]
                  [[:search s]])
    (ui/button "Search"))
   (ui/label s)))

(let [selected-search-type :full-text
      bar (search-bar "clojure")
      elem (on :search
               (fn [s]
                 [[:search selected-search-type s]])
               bar)]
  (ui/mouse-down elem
                 [10 10]))
>>> ([:search :full-text "clojure"])

Side effects in event handlers

The effects returned by event handlers are meant to be used in conjuction with UI frameworks. If you're making a simple UI, then you can just put your side effects in the event handler. Just note that you'll want to return nil from the handler since the event machinery expects a sequence of events. Nothing bad will happen if you don't, but you may see IllegalArgumentExceptions with the message "Don't know how to create ISeq from: ...".

(ui/translate 10 10
              (on :mouse-down (fn [[mx my]]
                                ;; side effect
                                (println "hi" mx my)
                                
                                ;; return nil to prevent error messages
                                nil)
                  (ui/label "Hello")))

Components

While using membrane does not require any specific UI framework, it does provide its own builtin in UI framework, membrane.component.

The purpose of a UI framework is to provide tools to handle events and manage state. If the job is simply visualizing data, then all you need is a function that accepts data and returns what to display. It's the interactivity that makes things more complicated.

For an interactive interface, you want to combine what to draw with how to respond to events.

To explain how membrane.component helps, let's a take a simple example of creating an ugly checkbox.

Without using any framework, your code might look something like this:

(def app-state (atom false))

(defn checkbox [checked?]
  (on
   :mouse-down
   (fn [_]
     (swap! app-state not)
     nil)
   (ui/label (if checked?
               "X"
               "O"))))

(comment (skia/run #(checkbox @app-state)))

This works great for this simple example, but your checkbox has to know exactly how to update the checked? value. If you have a different data model, then you have to change your checkbox code and we would really like our checkbox to be more reusable.

Let's see what this same ugly checkbox would look like with membrane.component

(defui checkbox [& {:keys [checked?]}]
  (on
   :mouse-down
   (fn [_]
     [[::toggle $checked?]])
   (ui/label (if checked?
               "X"
               "O"))))

(defeffect ::toggle [$checked?]
  (dispatch! :update $checked? not))

(defui checkbox-test [& {:keys [x y z]}]
  (vertical-layout
   (checkbox :checked? x)
   (checkbox :checked? y)
   (checkbox :checked? z)
   (ui/label
    (with-out-str (clojure.pprint/pprint
                   {:x x
                    :y y
                    :z z})))))

(comment (membrane.component/run-ui #'checkbox-test {:x false :y true :z false}))

Here's what the above looks like

checkboxes

You'll notice a few differences in the code.

  1. The defns have been replaced with defuis.
  2. The positional parameters have been converted to keyword parameters.
  3. The mouse down handler no longer has any side effects. Instead, it returns a value.
  4. The value returned by the mouse down handler includes a mysterious symbol, $checked?
  5. There is a ::toggle effect defined by defeffect
  6. The app is started with membrane.component/run-ui rather than membrane.ui/run

The most interesting part of this example is the :mouse-down event handler which returns [[::toggle $checked?]]. Loosely translated, this means "when a :mouse-down event occurs, the checkbox proposes 1 effect which toggles the value of checked?." It does not specify how the ::toggle effect is implemented.

What is exactly is "$checked?"? The symbol $checked? is replaced by the defui macro with a value that specifies the path to checked?. In fact, the defui macro will replace all symbols that start with "$" that derive from a keyword parameter with a value that the path of the corresponding symbol.

You can find the implementation of the ::toggle effect by checking its defeffect.

(defeffect ::toggle [$checked?]
  (dispatch! :update $checked? not))

membrane.component/defeffect: [type args & body] is a macro that does 3 things:

  1. It registers a global effect handler of type. type should be a keyword and since it is registered globally, should be namespaced
  2. It will define a var in the current namespace of effect-*type* where type is the name of the type keyword. This can be useful if you want to be able to use your effect functions in isolation
  3. It will implicitly add an additional argument as the first parameter named dispatch

The arglist for dispatch! is [type & args]. Calling dispatch! will invoke the effect of type with args. The role of dispatch! is to allow effects to define themselves in terms of other effects. Effects should not be called directly because while the default for an application is to use all the globally defined effects, this can be overridden for testing, development, or otherwise.

Running components

Every component can be run on its own. The goal is to build complex components and applications out of simpler components.

To run a component, use membrane.component/run-ui or membrane.component/run-ui-sync. Both run-ui functions have the same arglist ([ui-var] [ui-var initial-state] [ui-var initial-state handler]).

ui-var The var for a component initial-state The initial state of the component to run or an atom that contains the initial state. handler The effect handler for your UI. The handler will be called with all effects returned by the event handlers of your ui.

If handler is nil or an arity that doesn't specify handler is used, then a default handler using all of the globally defined effects from defeffect will be used. In addition to the globally defined effects the handler will provide 3 additional effects:

:update similar to update except instead of a keypath, takes a more generic path. example: [:update $path inc]

:set sets the value given a $path example: [:set $path value]

:delete deletes value at $path example: [:delete $path]

return value: The run-ui* functions both return the state atom used by the ui.

The only difference between run-ui and run-ui-sync is that run-ui-sync will wait until the window is closed before returning.

File Selector example

For this example, we'll build a file selector. Usage will be as follows: Call (file-selector "/path/to/folder") to open up a window with a file selector. The selected filenames will be returned as a set when the window is closed.

First, we'll build a generic item selector. Our item selector will display a row of checkboxes and filenames.

Let's create the component to display and select individual items.

(defui item-row [ & {:keys [item-name selected?]}]
  (on
   :mouse-down
   (fn [_]
     [[:update $selected? not]])
   ;; put the items side by side
   (horizontal-layout
    (translate 5 5
               ;; checkbox in `membrane.ui` is non interactive.
               (ui/checkbox selected?))
    (spacer 5 0)
    (ui/label item-name))))

(comment
 ;; It's a very common workflow to work on sub components one piece at a time.
  (component/run-ui #'item-row {:item-name "my item" :selected? false}))

Next, we'll build a generic item selector. For our item selector, we'll have a vertical list of items. Additionally, we'll have a textarea that let's us filter for only names that have the textarea's substring.

item selector item selector filtered

(defui item-selector
  "`item-names` a vector of choices to select from
`selected` a set of selected items
`str-filter` filter out item names that don't contain a case insensitive match for `str-filter` as a substring
"
  [& {:keys [item-names selected str-filter]
      :or {str-filter ""
           selected #{}}}]
  (let [filter-fnames (filter #(clojure.string/includes? (clojure.string/lower-case %) str-filter) item-names)]
    (apply
     vertical-layout
     (basic/textarea :text str-filter)
     (for [fname filter-fnames]
       ;; override the default behaviour of updating the `selected?` value directly
       ;; instead, we'll keep the list of selected items in a set
       (on :update
           (fn [& args]
             [[:update $selected (fn [selected]
                                   (if (contains? selected fname)
                                     (disj selected fname)
                                     (conj selected fname)))]])
           (item-row :item-name fname :selected? (get selected fname)))))))

(comment
  (run-ui #'item-selector {:item-names (->> (clojure.java.io/file ".")
                                      (.listFiles)
                                      (map #(.getName %)))} ))

Finally, we'll define a file-selector function using our item-selector ui.

(defn file-selector [path]
  (:selected
   @(component/run-ui-sync #'item-selector {:item-names (->> (clojure.java.io/file path)
                                                           (.listFiles)
                                                           (map #(.getName %))
                                                           sort)})))

Since run-ui-sync will wait until the window is closed and returns the app state atom. All we need to do is derefence the returned atom and grab the value for the :selected key.

Examples of form defui understands

Component gotchas

using a declared component rather than a defined one.

Can you improve this documentation?Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close