{"id":31660435,"url":"https://github.com/lessp/mlui","last_synced_at":"2026-07-15T19:06:20.668Z","repository":{"id":317554498,"uuid":"1067920368","full_name":"lessp/mlui","owner":"lessp","description":null,"archived":false,"fork":false,"pushed_at":"2025-10-19T05:02:19.000Z","size":336,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-26T23:41:17.818Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"OCaml","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lessp.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-01T15:24:09.000Z","updated_at":"2025-10-20T04:14:07.000Z","dependencies_parsed_at":"2025-10-01T17:33:02.654Z","dependency_job_id":"c8e19fda-a974-4e2d-96a7-a25e6f789799","html_url":"https://github.com/lessp/mlui","commit_stats":null,"previous_names":["lessp/mlui"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/lessp/mlui","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lessp%2Fmlui","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lessp%2Fmlui/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lessp%2Fmlui/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lessp%2Fmlui/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lessp","download_url":"https://codeload.github.com/lessp/mlui/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lessp%2Fmlui/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35517555,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-15T02:00:06.706Z","response_time":131,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-10-07T17:06:56.127Z","updated_at":"2026-07-15T19:06:20.663Z","avatar_url":"https://github.com/lessp.png","language":"OCaml","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003e **⚠️ Experimental**: 50% vibes. Use at own risk. Contributions welcome!\n\n# Mlui\n\nA declarative UI framework for OCaml using The Elm Architecture (TEA) pattern, featuring flexbox layout, animations, and functional event handling.\n\n![ml-paint](https://github.com/user-attachments/assets/ddac898d-a54c-4459-965c-2b0485a6c795)\n\n## Getting started\n\n```\ngit clone https://github.com/lessp/mlui.git\ndune pkg lock\ndune build\n```\n\nRun any example, e.g.\n\n```\n./_build/default/examples/paint/main.exe\n```\n\n\n### Prerequisites\n\n#### macOS\n\n```\nbrew install sdl2\nbrew install sdl2_ttf\n```\n\n## Using the library\n\n```ocaml\nopen Mlui\n\nmodule Msg = struct\n  type t =\n    | Increment\n    | Decrement\n    | Reset\nend\n\nmodule Model = struct\n  type t = {\n    counter : int;\n  }\n\n  let init () = {\n    counter = 0;\n  }\nend\n\nlet update (msg: Msg.t) (model: Model.t): (Model.t * Cmd.t) =\n  match msg with\n  | Msg.Increment -\u003e\n      ({ counter = model.counter + 1 }, Cmd.none)\n  | Msg.Decrement -\u003e\n      ({ counter = model.counter - 1 }, Cmd.none)\n  | Msg.Reset -\u003e\n      ({ counter = 0 }, Cmd.none)\n\nmodule Styles = struct\n  let container =\n    Style.(\n          default\n          |\u003e with_flex_grow 1.0\n          |\u003e with_flex_direction Column\n          |\u003e with_justify_content Center\n          |\u003e with_align_items Center)\n\n  let counter =\n    Style.(\n          default\n          |\u003e with_flex_direction Column\n          |\u003e with_justify_content Center\n          |\u003e with_align_items Center\n          |\u003e with_padding 20)\n\n  let text = Style.(\n    default\n    |\u003e with_font_size 18.0\n    |\u003e with_text_color Color.white)\n\n  let button_container =\n    Style.(\n          default\n          |\u003e with_flex_direction Row\n          |\u003e with_justify_content Center\n          |\u003e with_align_items Center\n          |\u003e with_padding 10)\n\n  let button =\n    Style.(\n          default\n          |\u003e with_flex_direction Column\n          |\u003e with_justify_content Center\n          |\u003e with_align_items Center\n          |\u003e with_background Color.blue\n          |\u003e with_padding 15\n          |\u003e with_size ~width:120 ~height:50)\nend\n\nlet view (model : Model.t) =\n  view\n    ~style:Styles.container\n    [\n\n      view ~style:Styles.counter [\n        text ~style:Styles.text (Printf.sprintf \"Count: %d\" model.counter);\n      ];\n\n      view ~style:Styles.button_container [\n        view ~style:Styles.button ~on_click:(fun () -\u003e Some Msg.Increment) [\n          text ~style:Styles.text \"Increment\"\n        ];\n\n        view ~style:Styles.button ~on_click:(fun () -\u003e Some Msg.Decrement) [\n          text ~style:Styles.text \"Decrement\"\n        ];\n\n        view ~style:Styles.button ~on_click:(fun () -\u003e Some Msg.Reset) [\n          text ~style:Styles.text \"Reset\"\n        ]\n      ]\n    ]\n\nlet subscriptions _model = Sub.on_quit Msg.Reset\n\nlet run () =\n  let window = Window.make ~width:800 ~height:600 ~title:\"Counter\" () in\n  run ~window ~subscriptions ~init:(Model.init ()) ~update ~view ()\n\nlet () =\n  match run () with\n  | Ok () -\u003e\n      ()\n  | Error (`Msg msg) -\u003e\n      Printf.eprintf \"Error: %s\\n\" msg;\n      exit 1\n```\n\n## Animation\n\nPure functional animations inspired by [Revery](https://github.com/revery-ui/revery). Animations are **functions from time to values** - composable, type-safe, and stateless.\n\n### Quick Example\n\n```ocaml\n(* Create animation: 600ms from (0,0) to (100,200) with easing *)\nlet animation =\n  Animation.animate ~duration:0.6\n  |\u003e Animation.ease Easing.ease_out_back\n  |\u003e Animation.tween ~from:(0.0, 0.0) ~to_:(100.0, 200.0)\n       ~interpolate:Interpolate.position\n\n(* Evaluate at any time *)\nlet (x, y) = Animation.value_at ~time:elapsed animation\n```\n\n### How It Works\n\n```ocaml\n(* Store time in model *)\ntype model = { current_time : float }\n\n(* Handle AnimationFrame events (~16ms at 60fps) *)\nlet handle_event = function\n  | Ui.Event.AnimationFrame delta -\u003e Some (Tick delta)\n  | _ -\u003e None\n\n(* Update time each frame *)\nlet update (Tick delta) model =\n  { current_time = model.current_time +. delta }\n\n(* Create animation that loops *)\nlet position_animation =\n  Animation.animate ~duration:2.0\n  |\u003e Animation.repeat ~duration:2.0\n  |\u003e Animation.tween ~from:0.0 ~to_:100.0\n       ~interpolate:Interpolate.float\n\n(* Use in view *)\nlet view model =\n  let x = Animation.value_at ~time:model.current_time position_animation in\n  Ui.view\n    ~style:(Ui.Style.default\n            |\u003e Ui.Style.with_position_type Ui.Absolute\n            |\u003e Ui.Style.with_transform (Ui.Translate { x; y = 50.0 }))\n    []\n```\n\n## Component Composition\n\nUse `Ui.map_msg` to compose components with different message types:\n\n```ocaml\n(* Toolbar with its own message type *)\nlet toolbar_view model =\n  Toolbar.view model.toolbar\n  |\u003e Ui.map_msg (fun msg -\u003e ToolbarMsg msg)\n\n(* Color palette with its message type *)\nlet palette_view model =\n  ColorPalette.view model.palette\n  |\u003e Ui.map_msg (fun msg -\u003e PaletteMsg msg)\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flessp%2Fmlui","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flessp%2Fmlui","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flessp%2Fmlui/lists"}