shamansir / tron-gui / Tron.Build

The Builder helps you define the structure of your GUI. It is made as abstract from the representation as possible, so that any control could be changed to a better or alternative one without the requirement for you to change the definition. What you specify is the type of the value to be changed and the message to in the case of such change. Plus grouping, as deep as you want and as wide as the free space allows.

See Tutorial for the details on how to use it.

This way works the optional connection with dat.gui: dat.gui operates over the similar set of types of values — we can replace them with our controls and vice versa, groups are interchanged with folders. Of course, this definition also can be translated to plain HTML forms, as the set of possible <input>’s is limited to the same set of types.

Every control may:

Shapes

For choice... and nest... items you may specify both the shape of the panel (in cells) and shape of every cell (they only can be all of one shape).

Shape of the panel may be requested to be calculated automatically (Shape.auto) or asked to have the desired number of rows (Shape.rows 3) or columns (Shape.columns 1) and so auto-calculate the other side, or specified manually using by function (Shape.by 4 3).

Cell shapes are usually just 1x1 (CellShape.single), but in some specific cases you may want controls to be twice smaller (CellShape.half) or horizontal (CellShape.oneByTwice) or vertical (CellShape.twiceByOne). Some controls take completely another form when their cell shape is changed.

The Naming

In the examples below, we use Build. as a prefix in the places where we reference functions from this module. This assumes that you did something like this in your code:

import Tron exposing (Tron)
import Tron.Build as Build exposing (..)

However, it is ok to use any name you like, for sure. Be it Tron. or Gui. or Def. or whatever...

Defining your interface

To define the structure of your interface, you need to have the function with this type:

for : Model -> Tron Msg

Where Msg is the message of your application.

This module contains all the helpers you need to build your own interface. Here is the excerpt from OneKnob example:

type alias Amount = Float


type Msg
    = AmountChanged Amount


type alias Model = Amount


for : Model -> Tron Msg
for amount =
    Build.root
        [
            ( "amount"
            , Build.float
                { min = 0, max = 1, step = 0.01 }
                amount
                AmountChanged
            )
        ]

Tron msg is the type that represents any cell in your GUI. If it's a nesting, it also contains recursively other instances of Tron msg.

Use the methods in the module as the helpers in building your own grid structure:

Build.root
    [ ( "int", Build.int ... )
    , ( "float", Build.float ... )
    ,
        ( "nest"
        , Build.nest
            [ ( "button", Build.button ... )
            , ...
            ]
        )
    ]

Using Tron msg together with Tron.map you may build your GUI from several modules with different messages.

Build.root
    [ ( "one", ModuleOne.gui model.moduleOne |> Tron.map ModuleOne )
    , ( "two", ModuleTwo.gui model.moduleTwo |> Tron.map ModuleTwo )
    , ( "three", ModuleThree.gui model.moduleThree |> Tron.map ModuleThree )
    ]

For more information, see the examples folder in the source code.

Sets

Set msg is just the list of components' definitions together with their labels. It is what Build.root, Build.nest and Build.choice get as an argument. Set msg is exposed as a separate type to help you in the cases where you build your GUI from several modules, but want to join them in a single panel rather than nesting every module separately.

Build.nest
    <| (ModuleOne.gui |> List.map (Tuple.mapSecond <| Tron.map ToModuleOne))
        ++ (ModuleTwo.gui |> List.map (Tuple.mapSecond <| Tron.map ToModuleTwo))
        ++ (ModuleThree.gui |> List.map (Tuple.mapSecond <| Tron.map ToModuleThree))

Sets


type alias Set msg =
List ( Label
, Tron msg 
}

Set msg is just the list of components' definitions together with their labels. It is what Build.root and Build.nest get as an argument. Set msg is exposed as a separate type to help you in the cases where you build your GUI from several modules, but want to join them in a single panel rather than nesting every module separately.

Build.nest
    <| (ModuleOne.gui |> List.map (Tuple.mapSecond <| Build.map ToModuleOne))
        ++ (ModuleTwo.gui |> List.map (Tuple.mapSecond <| Build.map ToModuleTwo))
        ++ (ModuleThree.gui |> List.map (Tuple.mapSecond <| Build.map ToModuleThree))

See also: Build.map.

mapSet : (a -> b) -> Set a -> Set b

map all the items in the set with one function.

toSet : (a -> Label) -> List (Tron a) -> Set a

Convert a list of components to a set by adding labels.

Root

root : Set msg -> Tron msg

Use the root only once, to mark the first visible row of your UI, and put anything else inside.

Actually it is just an alias for the nested row of controls, always expanded.

for myModel =
    Build.root
        [
            ( "octave"
            , Build.int { min = 1, max = 8, step = 1 } myModel.octave ChangeOctave
            )
        ,
            ( "note"
            , Build.int { min = 1, max = 127, step = 1 } myModel.midiNote ChangeMidiNote
            )
        ,
            ( "shape"
            , Build.nest

                [
                    ( "sine"
                    , Build.button
                        (always <| ChangeShape Sine)
                        |> Build.face
                            (Build.icon <| Url.Build.relative [ "sawwave.svg" ] [])

                    )
                ,
                    ( "square"
                    , Build.button
                        (always <| ChangeShape Square)
                        |> Build.face
                            (Build.icon <| Url.Build.relative [ "sawwave.svg" ] [])
                    )
                ,
                    ( "saw"
                    , Build.button
                        (always <| ChangeShape Saw)
                        |> Build.face
                            (Build.icon <| Url.Build.relative [ "sawwave.svg" ] [])

                    )
                ]

            )
        ]

Items

none : Tron msg

Similar to Cmd.none, Sub.none etc., makes it easier to use expressions in the definition.

For example:

if user |> User.is Root then
    Build.button RemoveAllFiles
else
    Build.none

int : { min : Basics.Int, max : Basics.Int, step : Basics.Int } -> Basics.Int -> (Basics.Int -> msg) -> Tron msg

int creates a control over an integer number value, with a minimum, maximum and a step.

Build.int { min = 1, max = 8, step = 1 } myModel.octave ChangeOctave

float : Axis -> Basics.Float -> (Basics.Float -> msg) -> Tron msg

float creates a control over a rational number value, with a minimum, maximum and a step.

Build.float { min = 0, max = 44000, step = 1 } myModel.frequency ChangeFrequency

number : Axis -> Basics.Float -> (Basics.Float -> msg) -> Tron msg

number is the alias for Builde.float.

Build.number { min = 0, max = 44000, step = 1 } myModel.frequency ChangeFrequency

xy : ( Axis, Axis ) -> ( Basics.Float, Basics.Float ) -> (( Basics.Float, Basics.Float ) -> msg) -> Tron msg

xy creates a control over a pair of two number values or anything that can be translated to them.

Build.xy
    ( { min = 0, max = scene.width, step = 0.1 }
    , { min = 0, max = scene.height, step = 0.1 }
    )
    myModel.lightDirection
    PointLightTo

coord : ( Axis, Axis ) -> ( Basics.Float, Basics.Float ) -> (( Basics.Float, Basics.Float ) -> msg) -> Tron msg

coord is the alias for Build.xy

color : Color -> (Color -> msg) -> Tron msg

color creates a control over a color, for the moment it is Hue/Saturation in 2D space, same as xy, but with different representation, but we may improve it later. Or you may change it to choice with your own palette.

The Color type here is from avh4/elm-color module.

Build.color model.lightColor AdjustColor

text : String -> (String -> msg) -> Tron msg

text creates a control over a String value.

Build.text model.elfName RenameElf

input : (a -> String) -> (String -> Maybe a) -> a -> (a -> msg) -> Tron msg

input creates a control over a value which can be translated to String and parsed from String. It is just a helper over text control.

Build.input
    (\color ->
        case color of
            Red -> "red"
            Green -> "green"
            Blue -> "blue"
    )
    (\val ->
        case val of
            "red" -> Just Red
            "green" -> Just Green
            "blue" -> Just Blue
            _ -> Nothing
    )
    model.selectedColor
    ChangeColor

button : (() -> msg) -> Tron msg

button creates a control over a unit () value. Type science aside, when you receive the unit value () in the handler, it just means that this button was pushed.

Build.button <| always DoABang

buttonWith : Face -> (() -> msg) -> Tron msg

Create a button with a given face. Use icon, iconAt, themedIcon, themedIconAt to create faces.

toggle : Basics.Bool -> (Basics.Bool -> msg) -> Tron msg

toggle creates a control over a boolean value.

Build.toggle model.lightOn SwitchLight

bool : Basics.Bool -> (Basics.Bool -> msg) -> Tron msg

bool is the alias for Build.toggle

Groups

nest : Set msg -> Tron msg

nest lets you group other controls (including other nestings) under a button which expands a group. Also, this group can be detached if GUI is confugured so.

Handler receives the state of the group, like if it is exapanded or collapsed or detached, but usually it's fine just to make it always NoOp.

Build.nest
    [
        ( "red"
        , Build.float { min = 0, max = 255, step = 0.1 } model.red <| AdjustColor Red
        )
    ,
        ( "green"
        , Build.float { min = 0, max = 255, step = 0.1 } model.blue <| AdjustColor Green
        )
    ,
        ( "blue"
        , Build.float { min = 0, max = 255, step = 0.1 } model.blue <| AdjustColor Blue
        )
    ]

See also: Style.Shape, Style.CellShape

choice : Set comparable -> comparable -> (comparable -> msg) -> Tron msg

choice defines a list of options for user to choose between. Consider it as <select> tag with <option>s. When some option is chosen by user, the handler gets the corresponding value. Notice that we ask for comparable type here.

Build.choice
    ([ 128, 256, 512 ]
        |> Build.buttons
        |> Build.toSet String.fromInt
    )
    model.bitrate
    ChangeBitrate

NB: If you don't want to use comparable types, but rather want to specify you own compare function, use choiceBy.

NB: If you want to add icons to the buttons, use buttons |> List.map (Tron.map (face << myIcon)), where myIcon : a -> Face, for colors use [ Color.white, Color.red, Color.yellow, ... ] |> buttons |> List.map (Tron.map (face << useColor)).

See also: Build.choiceBy, Build.strings, Build.palette, Style.Shape, Style.CellShape

choiceBy : Set a -> a -> (a -> a -> Basics.Bool) -> (a -> msg) -> Tron msg

choiceBy is identical to choice, but asks user for a custom comparison function instead of requiring comparable values.

Build.choiceBy
    ([ Sine, Square, Triangle, Saw ]
        |> Build.buttons
        |> Build. waveToString
    )
    model.waveShape
    compareWaves -- sometimes just (==) works, but it's better not to rely on it
    ChangeWaveShape

See also: Build.strings, Build.palette, Style.Shape, Style.CellShape

strings : List String -> String -> (String -> msg) -> Tron msg

strings is a helper to create choice over string values.

Build.strings
    greekChildrenNames
    model.currentChildName
    ChangeChildName

labels : (a -> Label) -> List a -> a -> msg -> (a -> msg) -> Tron msg

labels is a helper to create choice over the values that could be converted to string/labels and compared using those.

Requires a message that is a fallback for a case when comparison failed.

palette : List ( Label, Color ) -> Color -> (Color -> msg) -> Tron msg

palette is a helper to create choice over color values.

Build.palette
    [ Color.aqua, Color.rouge, Color.vanilla ]
    model.iceCreamColor
    RepaintIceCream

Buttons

buttons : List a -> List (Tron a)

buttons is the helper to translate a list of anything to a list of buttons.

It could be useful to pass such list to choice or nest:

Build.choiceBy
    ([ Sine, Square, Triangle, Saw ]
        |> Build.buttons
        |> Build.toSet waveToString
    )
    model.waveShape
    compareWaves -- sometimes just (==) works, but it's better not to rely on it
    ChangeWaveShape

Or:

Build.nest
    ([ Sine, Square, Triangle, Saw ]
        |> Build.buttons
        |> Build.toSet waveToString -- `toSet` is just another name for `addLabels`
        |> Build.handleWith ChangeWaveShape
    )

useColor : Color -> Face

Create a button face representing a color:

Build.button (always NoOp) |> Build.face (Build.useColor Color.green)

[ Color.white, Color.red, Color.yellow ]
    |> Build.buttons
    |> List.map (Tron.with (Build.face << Build.useColor))
    |> Build.toSet Color.colorToHexWithAlpha

face : Face -> Tron msg -> Tron msg

Set face for the button, nest, or choice, it can be icon or color:

Build.button (always DoABang)
    |> Build.face (Build.iconAt [ "assets", "myIcon.svg" ])

Build.nest
    ... |> Build.face (Build.iconAt [ "assets", "myIcon.svg" ])

Build.choice
    ... |> Build.face (Build.iconAt [ "assets", "myIcon.svg" ])

Build.buttons ...
    |> Tron.map (Build.face << Build.icon)


type alias Face =
Tron.Control.Impl.Button.Face

Icons


type alias Icon =
Tron.Control.Impl.Button.Icon

icon : Url -> Face

Create an Icon from its URL or filename.

import Url.Builder as Url

Build.icon
    <| makeUrl <| Url.relative [ "assets", "myicon.svg" ] []

See also: Build.iconAt, Build.themedIcon, Build.themedIconAt

iconAt : List String -> Face

Create an Icon using its relative local path.

Build.iconAt [ "assets", "myicon.svg" ]

See also: Build.themedIconAt

themedIcon : (Tron.Style.Theme.Theme -> Maybe Url) -> Face

Create a themed Icon from its URL or filename.

import Url.Builder as Url

Build.themedIcon
    <| \theme ->
        makeUrl <| Url.relative [ "assets", "myicon_" ++ Theme.toString theme ++ ".svg" ] []

themedIconAt : (Tron.Style.Theme.Theme -> List String) -> Face

Create a themed Icon using its relative local path.

Build.themedIconAt
    <| \theme -> [ "assets", "myicon_" ++ Theme.toString theme ++ ".svg" ]

Force expand / collapse for nesting

expand : Tron msg -> Tron msg

Forcefully expand the nesting:

Build.nest ... |> Build.expand
Build.choice ... |> Build.expand

collapse : Tron msg -> Tron msg

Forcefully collapse the nesting:

Build.nest ... |> Build.collapse
Build.choice ... |> Build.collapse

Shape

shape : Tron.Style.PanelShape.PanelShape -> Tron msg -> Tron msg

Changes panel shape for nest and choice panels:

Build.nest ... |> Buidler.shape (CS.cols 2)

Build.choice ... |> Buidler.shape (CS.rows 1)

Build.choice ... |> Buidler.shape (CS.by 2 3)

cells : Tron.Style.CellShape.CellShape -> Tron msg -> Tron msg

Changes cell shape for nest and choice panels:

Build.nest ... |> Buidler.cells PS.single

Build.choice ... |> Buidler.shape PS.halfByTwo

Build.choice ... |> Buidler.shape PS.halfByHalf

Live

Usually in your for function you set the default value to the control, but if you change the control with live, then you'll be able to pass some dynamic value from your model to it.

live : Tron msg -> Tron msg

Convert any control to update its value live (i.e. on every change take them from you model)

Build.knob ... |> Build.live

Conversion between types of controls + helpers

toChoice : (Tron.Control.Impl.Nest.ItemId -> msg) -> Tron msg -> Tron msg

Convert a nest control to choice control. It can be done easily by specifying a handler:

Build.nest
    ([ Sine, Square, Triangle, Saw ]
        |> buttons
        |> toSet waveToString -- `toSet` is just another name for `addLabels`
        |> handleWith (always NoOp)
    )
|> toChoice ChangeShapeById

toKnob : Tron msg -> Tron msg

Convert choice control to the knob form:

Build.choice ... |> Build.toKnob

toSwitch : Tron msg -> Tron msg

Convert choice control to a switch by click form:

Build.choice ... |> Build.toSwitch

handleWith : (a -> msg) -> Set a -> Set msg

Handle a set of items with a converter of item to a message

Build.nest
    ([ Sine, Square, Triangle, Saw ]
        |> Build.buttons
        |> Build.toSet waveToString -- `toSet` is just another name for `addLabels`
        |> Build.handleWith ChangeWaveShape
    )

Alias for Tron.mapSet