# MacUi

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](https://www.glfw.org/) (`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 primitives** – `Window`, `Frame`, `VStack`, `HStack`, `Spacer`.
- **Widgets** – `Text`, `Button`, `Rect`, `Line`, `TextField` (focus, text editing, blinking cursor), `Checkbox`, `Toggle`, `Slider`, `ProgressBar`, `Dropdown`, `Tabs`, `List`, `Table`.
- **Containers** – `ScrollView` (scissor-clipped with scrollbar), `SplitView` (draggable divider), `Modal`, `ContextMenu`, `Tooltip`.
- **Images** – `Image` renders PNG textures via Metal (`MTKTextureLoader`), auto-detecting size from the PNG header.
- **3D support** – `View3D` / `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](https://www.glfw.org/) (`brew install glfw`)
- Elixir ~> 1.14

## Installation

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

```elixir
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:

```elixir
children = [
  MacUi.Application
]

Supervisor.start_link(children, strategy: :one_for_one)
```

## Usage

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

```elixir
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

```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

| Component | Key attributes |
|---|---|
| `Text` | `size` (line height; glyphs are 8×16 px) |
| `Button` | `on_click`, `bg`, `width`, `height` |
| `TextField` | `w`, `h`, `placeholder`, `value` |
| `Checkbox` | `checked`, `size`, `color`, `tooltip` |
| `Toggle` | `on`, `w`, `h`, `track_on`, `track_off` |
| `Slider` | `value`, `min`, `max`, `color`, `track` |
| `ProgressBar` | `value`, `max`, `color`, `track` |
| `Dropdown` | `id`, `value`, `width`, `height`; `<Option id value>` children |
| `Tabs` | `id`, `active` (matches tab `title`); `<Tab id title>` children |
| `List` | `id`, `selected`, `width`; `<Item id>` children |
| `Table` | `column_width`, `selected`; `Column`/`Row`/`Cell` children |
| `ScrollView` | `id`, `width`, `height` (single child, typically a `VStack`) |
| `SplitView` | `id`, `orientation` (`horizontal`/`vertical`), `width`, `height`; two `Panel` children |
| `Modal` | `id`, `visible`, `width`, `height` |
| `ContextMenu` | `id`, `width`, `height`; `<MenuItem id action>` children (opens on right-click) |
| `Image` | `src`, `x`, `y`, `width`, `height`, `color` |

### Handling events

Subscribe to UI events from any process:

```elixir
MacUi.subscribe()
```

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

```elixir
# 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:

```elixir
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

- `MacUi.load_file/1` – read and render a UI XML file.
- `MacUi.render_xml/1` – render an XML string directly.
- `MacUi.update_element_text/2` – update text of an element by ID.
- `MacUi.set_attr/3` and `set_attrs/2` – update one or more attributes of an element by ID.
- `MacUi.subscribe/0` – receive UI events as messages.
- `MacUi.Window.clear/0`, `draw_pixel/5`, `draw_line/7`, `draw_text/3` – low-level draw commands.
- `MacUi.Components.render_node/2` – render a single component node (used by tests).

## 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

```bash
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`.
