A declarative macOS UI toolkit built on Elixir, GLFW, and Apple Metal.

UI is defined as XML, parsed into an AST, and re-rendered every frame by a C++ engine process (mac_ui_engine) that talks to the Elixir runtime over stdio. The engine draws rects, lines, text, PNG images, and 3D wireframes with Metal, and reports mouse/keyboard input back as JSON events.

Platform: macOS only. Requires a macOS 15+ SDK (Metal shader toolchain) and GLFW (brew install glfw). One window per VM.

Features

  • XML-driven UI – Describe windows, layouts, and widgets in XML (parsed with xmerl).
  • Immediate-mode rendering – The AST is re-rendered each frame (~60 fps) through a continuous render loop.
  • Layout primitivesWindow, Frame, VStack, HStack, Spacer.
  • WidgetsText, Button, Rect, Line, TextField (focus, text editing, blinking cursor), Checkbox, Toggle, Slider, ProgressBar, Dropdown, Tabs, List, Table.
  • ContainersScrollView (scissor-clipped with scrollbar), SplitView (draggable divider), Modal, ContextMenu, Tooltip.
  • ImagesImage renders PNG textures via Metal (MTKTextureLoader), auto-detecting size from the PNG header.
  • 3D supportView3D / Cube3D wireframe cubes with real-time rotation.
  • Event-driven – GenServer architecture: MacUi.Window owns the engine port and input; MacUi.Controller manages application state and reacts to UI events.
  • Live updates – Update element text or attributes by ID without reloading the whole UI.
  • Resizable – The window opens at the <Window> size and tracks drag-resizes; the drawable and scissor regions scale with the framebuffer (including retina).

Requirements

  • macOS with Xcode Command Line Tools (for the Metal toolchain: metal, metallib)
  • GLFW (brew install glfw)
  • Elixir ~> 1.14

Installation

The package can be installed by adding mac_ui to your list of dependencies in mix.exs:

def deps do
  [
    {:mac_ui, "~> 0.1.0"}
  ]
end

mix deps.get && mix compile builds the native engine (via elixir_make) and compiles the Metal shader library.

Opt-in startup

mac_ui does not start automatically. Add MacUi.Application to your supervision tree to launch the engine (a GLFW window) and the event controller:

children = [
  MacUi.Application
]

Supervisor.start_link(children, strategy: :one_for_one)

Usage

Load a UI XML file, then subscribe the calling process to events:

MacUi.load_file(Application.app_dir(:mac_ui, "priv/sample_ui.xml"))
MacUi.subscribe()

The bundled sample_ui.xml shows the full component set: a header bar with a search field, three aligned panels (settings controls, tabs with a list, a selectable table), a scrollable log area, a draggable split view, a right-click context menu, a PNG image, and a modal dialog.

Defining UI in XML

<Window width="1024" height="600" bg="#1E1E1E">
  <VStack padding="20" gap="15">
    <Text size="24">Hello, MacUi!</Text>
    <HStack gap="15">
      <Button id="btn_save" on_click="save_document" width="120" height="35" bg="#007ACC">Save</Button>
      <TextField id="input_field" w="240" h="35" placeholder="Type here..." />
      <Spacer />
    </HStack>
    <Image src="logo.png" width="64" height="64" />
    <View3D>
      <Cube3D size="120" color="#FFFFFF" rotate_x="0.45" rotate_y="0.55" />
    </View3D>
  </VStack>
</Window>

Notes:

  • Colors accept #RRGGBB and #RRGGBBAA.
  • VStack/HStack render their children in a flow; use gap/padding and a fixed width/height on the stack so Spacer absorbs the leftover space.
  • Image resolves src relative to priv/ (or an absolute path).
  • Multiple children of Frame, ScrollView, Tab, Panel, and Modal overlap at the same origin — wrap them in a VStack/HStack.

Widget reference

ComponentKey attributes
Textsize (line height; glyphs are 8×16 px)
Buttonon_click, bg, width, height
TextFieldw, h, placeholder, value
Checkboxchecked, size, color, tooltip
Toggleon, w, h, track_on, track_off
Slidervalue, min, max, color, track
ProgressBarvalue, max, color, track
Dropdownid, value, width, height; <Option id value> children
Tabsid, active (matches tab title); <Tab id title> children
Listid, selected, width; <Item id> children
Tablecolumn_width, selected; Column/Row/Cell children
ScrollViewid, width, height (single child, typically a VStack)
SplitViewid, orientation (horizontal/vertical), width, height; two Panel children
Modalid, visible, width, height
ContextMenuid, width, height; <MenuItem id action> children (opens on right-click)
Imagesrc, x, y, width, height, color

Handling events

Subscribe to UI events from any process:

MacUi.subscribe()

MacUi.Controller already does this. Events arrive as {:mac_ui_event, event}:

# Button / context-menu item clicked
{:mac_ui_event, %{event: :click, id: "btn_save", action: "save_document", x: ..., y: ...}}

# Interactive widget changed
{:mac_ui_event, %{event: :change, id: "sld_vol", type: "Slider", value: 55.0}}

# Modal dismissed by clicking the dimmed area
{:mac_ui_event, %{event: :close, id: "modal_info"}}

Controller-driven widget state

Interactive widgets (Checkbox, Toggle, Slider, Dropdown, Tabs, List, Table) are controller-driven: clicking them emits a :change event instead of mutating the AST directly. MacUi.Controller echoes the new value back so the rendered UI stays in sync. To drive the state yourself, subscribe to :change events and echo the value back:

def handle_info({:mac_ui_event, %{event: :change, id: id, type: "Slider", value: v}}, state) do
  MacUi.set_attr(id, "value", to_string(v))
  {:noreply, state}
end

Tooltip text appears after hovering an interactive widget for ~400 ms.

Public API

Architecture

 Elixir VM 
  MacUi.Controller (GenServer)   handles events, owns state    
         subscribe / set_attr / update_element_text             
  MacUi.Window (GenServer)  xmerl parse  AST  render loop    
         Port.command (newline-delimited draw commands)         

  priv/mac_ui_engine (C++/Obj-C++)                               
  GLFW window + Metal renderer, command parser, input  JSON     

The Elixir side renders the AST into draw commands every tick and pipes them to the engine over stdio. The engine parses clear, window:w,h, clip:x,y,w,h, clip_off, line:, pixel:, rect:, stroke_rect:, text:, and image: commands into Metal draw calls. It reports input as JSON lines (click, key, move, scroll, resize).

  • lib/mac_ui/window.ex – engine port lifecycle, XML parsing, AST walking, input event handling, FPS-tracked render loop.
  • lib/mac_ui/controller.ex – subscribes to events and echoes controller-driven widget state.
  • lib/mac_ui/components/ – one module per widget plus the layout containers.
  • c_src/main.mm – native engine: GLFW window, Metal renderer, command parser.
  • c_src/Shaders.metal – Metal vertex/fragment shaders (compiled to priv/default.metallib).

Testing

mix test

Notes

  • The engine renders to a CAMetalLayer; shaders are compiled at build time with xcrun -sdk macosx metal. The layer contentsScale and drawableSize track the window so content stays crisp on retina and follows resize.
  • Keycodes are remapped in MacUi.Window (e.g. backspace, space) for TextField editing.
  • Interaction state is split: semantic widget values are controller-driven (:change events + set_attr), while transient state (scroll offset, hover, drag, open popups, context menu, tooltip) is owned by MacUi.Window.