⚠️ Experimental: 50% vibes. Use at own risk. Contributions welcome!
A declarative UI framework for OCaml using The Elm Architecture (TEA) pattern, featuring flexbox layout, animations, and functional event handling.
git clone https://github.com/lessp/mlui.git
dune pkg lock
dune build
Run any example, e.g.
./_build/default/examples/paint/main.exe
brew install sdl2
brew install sdl2_ttf
open Mlui
module Msg = struct
type t =
| Increment
| Decrement
| Reset
end
module Model = struct
type t = {
counter : int;
}
let init () = {
counter = 0;
}
end
let update (msg: Msg.t) (model: Model.t): (Model.t * Cmd.t) =
match msg with
| Msg.Increment ->
({ counter = model.counter + 1 }, Cmd.none)
| Msg.Decrement ->
({ counter = model.counter - 1 }, Cmd.none)
| Msg.Reset ->
({ counter = 0 }, Cmd.none)
module Styles = struct
let container =
Style.(
default
|> with_flex_grow 1.0
|> with_flex_direction Column
|> with_justify_content Center
|> with_align_items Center)
let counter =
Style.(
default
|> with_flex_direction Column
|> with_justify_content Center
|> with_align_items Center
|> with_padding 20)
let text = Style.(
default
|> with_font_size 18.0
|> with_text_color Color.white)
let button_container =
Style.(
default
|> with_flex_direction Row
|> with_justify_content Center
|> with_align_items Center
|> with_padding 10)
let button =
Style.(
default
|> with_flex_direction Column
|> with_justify_content Center
|> with_align_items Center
|> with_background Color.blue
|> with_padding 15
|> with_size ~width:120 ~height:50)
end
let view (model : Model.t) =
view
~style:Styles.container
[
view ~style:Styles.counter [
text ~style:Styles.text (Printf.sprintf "Count: %d" model.counter);
];
view ~style:Styles.button_container [
view ~style:Styles.button ~on_click:(fun () -> Some Msg.Increment) [
text ~style:Styles.text "Increment"
];
view ~style:Styles.button ~on_click:(fun () -> Some Msg.Decrement) [
text ~style:Styles.text "Decrement"
];
view ~style:Styles.button ~on_click:(fun () -> Some Msg.Reset) [
text ~style:Styles.text "Reset"
]
]
]
let subscriptions _model = Sub.on_quit Msg.Reset
let run () =
let window = Window.make ~width:800 ~height:600 ~title:"Counter" () in
run ~window ~subscriptions ~init:(Model.init ()) ~update ~view ()
let () =
match run () with
| Ok () ->
()
| Error (`Msg msg) ->
Printf.eprintf "Error: %s\n" msg;
exit 1Pure functional animations inspired by Revery. Animations are functions from time to values - composable, type-safe, and stateless.
(* Create animation: 600ms from (0,0) to (100,200) with easing *)
let animation =
Animation.animate ~duration:0.6
|> Animation.ease Easing.ease_out_back
|> Animation.tween ~from:(0.0, 0.0) ~to_:(100.0, 200.0)
~interpolate:Interpolate.position
(* Evaluate at any time *)
let (x, y) = Animation.value_at ~time:elapsed animation(* Store time in model *)
type model = { current_time : float }
(* Handle AnimationFrame events (~16ms at 60fps) *)
let handle_event = function
| Ui.Event.AnimationFrame delta -> Some (Tick delta)
| _ -> None
(* Update time each frame *)
let update (Tick delta) model =
{ current_time = model.current_time +. delta }
(* Create animation that loops *)
let position_animation =
Animation.animate ~duration:2.0
|> Animation.repeat ~duration:2.0
|> Animation.tween ~from:0.0 ~to_:100.0
~interpolate:Interpolate.float
(* Use in view *)
let view model =
let x = Animation.value_at ~time:model.current_time position_animation in
Ui.view
~style:(Ui.Style.default
|> Ui.Style.with_position_type Ui.Absolute
|> Ui.Style.with_transform (Ui.Translate { x; y = 50.0 }))
[]Use Ui.map_msg to compose components with different message types:
(* Toolbar with its own message type *)
let toolbar_view model =
Toolbar.view model.toolbar
|> Ui.map_msg (fun msg -> ToolbarMsg msg)
(* Color palette with its message type *)
let palette_view model =
ColorPalette.view model.palette
|> Ui.map_msg (fun msg -> PaletteMsg msg)MIT
