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
Add this dependency to your project:
[com.phronemophobic/membrane "0.9.0-beta"]
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 are represented as a vector of two numbers [x, y]
.
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
.
;; label using default font
(ui/label "Hello\nWorld!")
;; 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))
;; Use the default font, but change the size
(ui/label "Hello\nWorld!" (ui/font nil 22))
;; 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])
;; 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)])))))
;; 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
(ui/rounded-rectangle 200 100 10)
;; 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"))
To draw multiple elements, simply use a vector. The elements will be drawn in order.
[(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"))]
[(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))]
(ui/scale 3 10
(ui/label "sx: 3, sy: 10"))
;; vertical
(ui/vertical-layout
(ui/button "hello")
(ui/button "world"))
;; horizontal
(ui/horizontal-layout
(ui/button "hello")
(ui/button "world"))
(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)))))
;; 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]))
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
.
(ui/horizontal-layout
(ui/padding 10 10
(ui/checkbox false))
(ui/padding 10 10
(ui/checkbox true)))
(ui/horizontal-layout
(ui/padding 10 10
(ui/button "button"))
(ui/padding 10 10
(ui/button "button" nil true)))
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])
Conceptually, event handling is hierarchical. When an event occurs, it asks the root element what effects should take place. The root element may
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:
(let [elem (ui/on
:mouse-down (fn [_] nil)
(ui/button "Big Red Button"
(fn []
[[:self-destruct!]])))]
(ui/mouse-down elem [20 20]))
>>> ()
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]))
>>> ()
(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}
(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 can be provided using membrane.ui/on
.
: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 [[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 [[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 [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 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]
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 [] Called when a clipboard copy event occurs.
:clipboard-cut Called when a cliboard cut event occurs.
:clipboard-paste [s]
s
is the string being pasted
Called when a clipboard paste event occurs.
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"])
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")))
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
You'll notice a few differences in the code.
defn
s have been replaced with defui
s.$checked?
::toggle
effect defined by defeffect
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:
type
. type
should be a keyword and since it is registered globally, should be namespacedeffect-*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 isolationdispatch
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.
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.
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.
(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.
defui
understandsusing 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