Reactive markup is a haskell library for declaratively defining user interfaces. It is currently still in development phase, so drastic changes happen regularly.
Some of the features of this library are:
- Declarative components
- UI as a function from model to components
- Automatic updating of the UI on model changes
- Contexts to constrain where components can be used
- Customizable interpretation of components (currently, only a GTK backend is available)
This library tries to disallow all UI errors at compile time, so compiled code will definitely produce a working UI.
Here is a small code example and an image of the corresponding GTK UI:
import ReactiveMarkup.Target.Gtk
import ReactiveMarkup
import Data.Void
main :: IO ()
main = do
runGtk app
renderGUI :: Markup Gtk Root Void
renderGUI = bold "Hello Reactive Markup"
app :: App Gtk EmptyF Void
app =
App
{ appRender = \_ -> renderGUI,
appHandleEvent = absurd,
appInitialState = EmptyF,
appName = "Hello Reactive Markup"
}
First and foremost, you need to have GTK 4 installed! Then the following should do the trick:
git clone https://github.com/Simre1/reactive-markup.git
cd reactive-markup
cabal build all
This will build the reactive-markup
library, the reactive-markup-gtk
backend as well as the GTK examples. Initial compilation will take quite a while due to the GTK dependencies.
To use Reactive Markup as a library, you will have to clone the git repository and add the reactive-markup folder manually to your build environment. Otherwise, it can be used like any other haskell library.
You can define your UI by builing up the Markup
type using the available components like text
, button
, column
and so on. For example, to create a list consisting of some text and a button:
textAndButton :: Markup Gtk Common Void
textAndButton = column
[ italic "Some text",
button "Click me"
]
You can use functions and let-expressions to factor out code and make reusable UI components.
textAndButton :: Markup Gtk Common Void
textAndButton =
let boldText = bold "Bold text"
in column
[ boldText,
italic boldText,
button "Click me"
]
Assuming that you create your UI as a function from model state to components, then the UI will automatically update itself on model state changes. However, some boilerplate is needed.
Here is an example:
searchComponent :: Dynamic Gtk Bool -> Markup Gtk Common Void
searchComponent isBool = dynamicMarkup isBool $ \actualIsBool -> row
[if actualIsBool then "Active" else "Inactive"]
The Dynamic
part means that the Bool
value may change. To actually get at the Bool
value, you need to use dynamicMarkup
which gives you access to the Bool
value in the function argument actualIsBool
. Whenever the Bool
value changes, dynamicMarkup
will use the given function to determine the new UI.
Components can spawn events which are then passed upwards implicitly through the component hierarchy.
Here is a button which emits an event of type Text
and the value "Event message":
buttonWithTextEvent :: Markup Gtk Common Text
buttonWithTextEvent = button "Click" (#click ?= "Event message")
Take note that the event type is part of the Markup
type. This means that by looking at the type we can determine the type of the events that a component can spawn. buttonWithTextEvent
spawns events of type Text
.
If we use buttonWithTextEvent
within another component, then the resuling component also spawns events of type Text
.
columnWithTextEvent :: Markup Gtk Common Text
columnWithTextEvent = column [ buttonWithTextEvent ]
You cannot directly create Dynamic
values and you cannot directly interact with events either. However, there are components which you can use to do so.
For example simpleLocalState
:
countingButton :: Markup Gtk Common Void
countingButton = simpleLocalState handleButtonClick initialState buttonWithNumber
where
initialState :: Int
initialState = 0
handleButtonClick :: () -> Int -> SimpleUpdate Int Void
handleButtonClick () state = setSimpleUpdate (state + 1) defSimpleUpdate
buttonWithNumber :: Dynamic Gtk Int -> Markup Gtk Common ()
buttonWithNumber int = dynamicMarkup int $ \i -> button (string $ show i) (#click ?~ ())
simpleLocalState
allows you to have some local state for a component and update it based on events happening within that component. In this case, the local state is an int which stores how many times the button has been clicked. Whenever the button has been clicked handleButtonClick
is used to increase the state by 1. buttonWithNumber
defines how to create UI components based on the local state.