Hi everyone,
I'd like to share with you Terrocotta: a Roc native UI library.
I have been working on it for the past +1month and I think is in a good enough shape to share.
At its core terrocotta is a port of Clay to pure Roc, rendered using roc-ray. It is as faithful to the original implementation as possible and adds some niceties on top: MVU, event bindings, text style inheritance, etc.
The demo examples currently run at 300-600 fps (terrocotta: roc build --opt=speed, roc-ray: zig build -Doptimize=ReleaseFast) but there is probably quite some room for improvement since Clay advertises 10_000 fps.
I hope that it can help the UI effort to some extent. For those interested, I recommend reading the docs/architecture.md for internal explanations.
I also sometimes dump some Roc experience feedback to #24
Wow, looks great!
![]()
I guess I need to make a roc-ray release with an optimized host :sweat_smile:
Luke Boswell said:
I guess I need to make a roc-ray release with an optimized host :sweat_smile:
Here's a fresh release https://github.com/lukewilliamboswell/roc-ray/releases/tag/0.8.2
Does this build to both native and web? How might you compare/contrast this with something like iced? Big fan of these "write once, run anywhere" libraries esp. Elm-inspired, and I've been looking for a good roc library for this, but roc-iced seems to be only old roc compiler and unmaintained. If it supports web/wasm, I'll give Terracotta a shot!
Btw the code examples you have are so much easier to understand than the original Clay ones. Elm-architecture + Multiplatform + functional/no runtime panic guarantees should be the future IMO. Good stuff!
@Luke Boswell I looked into this the other day and pretty sure that roc-ray doesn't yet compile to WASM/web, though raylib itself can? Which I assume means it just hasn't been implemented yet. Was wondering if that was on the roadmap or if I'm thinking about that correctly?
It used to target web too, but I deleted all that when I migrated to the new compiler just to keep the scope simpler.
If anyone is interested in adding that I'd love it ... but it's not something I'm working on or anything
Luke Boswell said:
Luke Boswell said:
I guess I need to make a roc-ray release with an optimized host :sweat_smile:
Here's a fresh release https://github.com/lukewilliamboswell/roc-ray/releases/tag/0.8.2
Something went wrong and the host is still the debug version... I'm looking into it
Ok here's a better fix ... from 40 up to 900 frames per second
https://github.com/lukewilliamboswell/roc-ray/releases/tag/0.8.3
![]()
If anyone is interested in adding that I'd love it ... but it's not something I'm working on or anything
I might look into this at some point. Write once, run anywhere UI story in a functional language is very compelling (and if I'm not mistaken one of the reasons Roc was created, feeling the limitations of Elm to be only web, right?). I guess this is sort of the inverse
Also -- heres a PR that fixes the ARC bug and leaks in Roc these examples exposed https://github.com/roc-lang/roc/pull/10539
Thank you @Romain Lepert
As always you are quick on the draw Luke. Thank you !
blu said:
Does this build to both native and web? How might you compare/contrast this with something like iced? Big fan of these "write once, run anywhere" libraries esp. Elm-inspired, and I've been looking for a good roc library for this, but roc-iced seems to be only old roc compiler and unmaintained. If it supports web/wasm, I'll give Terracotta a shot!
It should be possible since the original clay has both a canvas renderer and a html/css renderer. Given the immediate mode nature only the canvas makes real sense but you lose the web accessibility, bot crawling, etc. The html/css renderer might only make sense for static websites maybe.
feeling the limitations of Elm to be only web
Yes indeed, although Roc was not made with a UI focus in mind as I remember.
yeah my feeling at the time was "I already have an Elm-like experience for Web UIs: Elm!" :smile:
that said, I do want something for native GUIs and I also appreciate that there's a lot of interest in having the same language on the server and on the client, including browser-based clients
Terracotta
Apparently the idea is:
Terracotta + Roc -> TerROCotta
that said, I do want something for native GUIs and I also appreciate that there's a lot of interest in having the same language on the server and on the client, including browser-based clients
Totally fair, and thinking about this more maybe the HTML/JS/CSS trifecta vs. native UI architectures are just too conceptually different to unify cleanly. I guess I was just reading the iced philosophy and started dreaming of UItopia.
And in that UItopia I'd rather write in a Roc-like friendly syntax than Rust :sweat_smile:
Contrary to immediate mode Terrocotta, iced.rs is retained mode. Nonetheless when targeting the web it uses WebGPU/WebGL, not HTML+DOM. That means it has the same restrictions as Terrocotta which hypothetically could do Canvas2D or WebGL or WebGPU.
By restrictions I mean things like: SEO, browser translation, printing, password managers, autofill. Things of that nature
Terrocotta just got its first release 0.1.0.
It is coming with some nice upgrades
roc-ray to 0.10.0.image() elementcanvas() elementand a suite example apps:
extended widget kit: roc examples/widgets.roc
![]()
Interactive tutorial: roc examples/tutorial/main.roc.
tutorial.webm
IDE example: roc examples/ide/main.roc.
ide.webm
Screwbot inverse-kinematics example: roc examples/screwbot/main.roc (thank you @Luke Boswell ).
![]()
Wow, this is really impressive! I particularly like the IDE demo. :star_struck:
FPS for the interested:
I will be using these apps as benchmark for potentially performance sensitive changes:
.from_quote/.from_interpolationAurélien Geron said:
Wow, this is really impressive! I particularly like the IDE demo. :star_struck:
thank you. I am quite happy with how the proof of concept IDE turned out. Also with the 240 FPS.
This looks really awesome, I was thinking Clay would be a good fit for a game engine I'm working on, so this certainly gives me confidence in the approach.
yes, clay is a good fit for that ! I do plan do add a game-like demo to show how it composes over a roc-ray game. wouldn't you want to contribute to roc-ray's 3D capabilities ?
I do like what roc-ray is doing and keen to contribute if I can! I only discovered roc-ray after working on an engine myself, I was building the engine part in Odin before building the Roc platform part. Rocco also has a different approach to roc-ray and aims to avoid side-effects by just updating a Model and Scene per game tick. That being said this is still very experimental and roc-ray is far further along and builds on an awesome game library.
@pixelsprout I really like that you made rendering be a pure function! In general I think "rendering is a pure function" takes the best advantage of Roc's design, although roc-ray unfortunately can't really do that because it maps directly to raylib's very side-effectful API :smile:
roc-ray could just as well expose draw commands as data instead of direct draw functions. In both cases the host must make the final GPU call. Unlike model update, rendering is not “pure” conceptually because it has side effects. Also a command list is an allocation with copy and realloc as capacity grows. So I am curious what makes it more adequate to roc’s design in your view ?
Yeah, roc-ray could take the same approach, but then it would stop being a faithful port of raylib. Useful software needs side effects, so in Rocco I push them to the edge, where the engine runs them. Drawing has side effects, but describing a frame doesn't, and the Roc side of Rocco stops at the describing part.
Returning a description also pays off on performance. The host interpolates between the last two Scenes, so it never needs to hold an old Model, and the Model stays at a refcount of 1. That lets Roc safely mutate it in place and skip the copies. As for the command list allocating, I measure that in CI: 2 or 3 allocations per step, 0 reallocs, and the host reads the list straight out of Roc memory without copying it.
Keeping step pure gives us replay as well: log the inputs, replay the log, and you end up in exactly the same state. It also means we can test the game without the engine at all, which is great for agents, because they don't need to see the game running to test how it behaves.
Romain Lepert said:
I am curious what makes it more adequate to roc’s design in your view ?
in general, Roc lets you do more with pure functions than you can do with effectful functions - you can run them at compile time, they're easier to test, those tests can never flake, there are caching benefits to tests, etc...
I/O absolutely requires effects, so there's no opportunity to get these benefits there. But rendering not only doesn't require I/O, it's almost always a mistake to do I/O in the middle of rendering
so because it doesn't require I/O, it's certainly possible (with tradeoffs of course) to have a rendering API that only uses pure functions.
and if you do that, you get all the benefits of pure functions!
so that's what I mean by that.
I thought I tried having a pure render and there was some reason why it didn't work well at the time. Maybe that is no longer the case. It might have been laying out fonts or something like that.
The app author facing side could also be pure and then the platform translate that into the hosted effects underneath. Basically there are a lot of different ways you could do things.
I've definitely tried a lot of different things -- I would not say roc-ray's approach is the best. I'm hoping people improve on that a lot. I'd consider it a successful project if people enjoy building things with it now, and then someone makes something better later that is inspired by it.
it's definitely harder to design a pure API on top of an effectful host API, especially while trying to keep performance high :smile:
so it's not like it's all upside, but I do think the benefits to app authors are great if you can pull it off!
Luke Boswell said:
I've definitely tried a lot of different things -- I would not say roc-ray's approach is the best. I'm hoping people improve on that a lot. I'd consider it a successful project if people enjoy building things with it now, and then someone makes something better later that is inspired by it.
I definitely learnt a bunch from roc-ray, I also think leaning into the API is a good choice imo. People who are already familiar with raylib can pick it up far more easily.
I'm finding working on rocco challenging at times as I don't want the design to limit the game making part too much.
It's making progress however, we now have PBR, specular highlights, tone mapping and currently working on shadows.
Screen Recording 2026-10-15 at 10.01.29 AM.mov
Rocco aims to avoid side-effects by just updating a
ModelandSceneper game tick
I believe roc-ray also aims to update Model and render it per game tick.
roc-ray update! is currently effectful only to allow certain synchronous effects like window_read_clipboard!, trace_begin!, etc. . It is not a very big difference.
The current main distinction is that roc-ray's render! : Frame, Model -> {} targets effectful draw call while rocco's view : Model -> Scene outputs a list of draw commands for the engine to execute.
pixelsprout said:
Returning a description also pays off on performance. The host interpolates between the last two Scenes, so it never needs to hold an old Model, and the Model stays at a refcount of 1. That lets Roc safely mutate it in place and skip the copies. As for the command list allocating, I measure that in CI: 2 or 3 allocations per step, 0 reallocs, and the host reads the list straight out of Roc memory without copying it.
I don't think the Scene approach is uniquely capable of keeping refcount of 1. roc-ray also keeps refcount 1 because update!() and render!() happen sequentially and render!() releases refcount for next update!() (aka no copies).
Regarding the command list allocating, I am not referring to the host boundary, but rather that view() will be appending draw values to the list continuously. Roc does a 2x geometric capacity growth anytime you exceed the current list capacity. For O(1000) elements which is likely for rich scenes, that is >10 growth, each of which will realloc and may require to move the list in memory : 0 → 2 → 4 → 8 → 16 → 32 → 64 → 128 → 192 → 288 → 432 → 648 → 972 → 1458. Toy CI tests probably won't show that. So code should instantiate List.with_capacity, avoid intermediate command lists and List combinators like map, keep_if.
Richard Feldman said:
in general, Roc lets you do more with pure functions than you can do with effectful functions - you can run them at compile time, they're easier to test, those tests can never flake, there are caching benefits to tests, etc...
I/O absolutely requires effects, so there's no opportunity to get these benefits there. But rendering not only doesn't require I/O, it's almost always a mistake to do I/O in the middle of rendering
render! has scoped capabilities so it would reject Files.read_text! for example.So all in all my read of the two rendering approaches is:
Pros:
Cons:
Is that fair ?
capabilities to restrict things like I/O help, yeah :+1:
Last updated: Oct 15 2026 at 17:56 UTC