shithub: neindaw

Download patch

ref: 0ac294decc984c7c3b0272db4de45728dadc38c5
author: Sigrid Haflínudóttir <ftrvxmtrx@gmail.com>
date: Fri Jan 17 06:20:17 EST 2020

Squashed 'microui/' content from commit 664a525

git-subtree-dir: microui
git-subtree-split: 664a52591895e867ebdac2a56588cf3595b326e8

--- /dev/null
+++ b/.gitignore
@@ -1,0 +1,2 @@
+[a0125678vqki].out
+*.[o0125678vqki]
--- /dev/null
+++ b/LICENSE
@@ -1,0 +1,20 @@
+Copyright (c) 2018-2019 rxi
+Copyright (c) 2020 ftrvxmtrx
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
--- /dev/null
+++ b/README.md
@@ -1,0 +1,30 @@
+# microui
+![screenshot](doc/microui_9front.png)
+
+A tiny, portable, immediate-mode UI library written in ANSI C.
+This is a fork of [microui](https://github.com/rxi/microui)
+targetting Plan 9 specifically.
+
+## Usage
+
+* See [`doc/usage.md`](doc/usage.md) for usage instructions
+* See the [`demo`](demo) directory for a usage example
+
+## Notes
+
+* The library expects the user to provide input and handle the resultant
+  drawing commands, it does not do any drawing itself
+* In contrast to other immediate mode ui libraries microui does not store
+  window, panel, header or treenode state internally. It is up to the
+  user to manage this data themselves
+
+## Contributing
+
+The library is designed to be lightweight, providing a foundation to which
+you can easily add custom controls and UI elements; pull requests adding
+additional features will likely not be merged. Bug reports are welcome.
+
+## License
+
+This library is free software; you can redistribute it and/or modify it
+under the terms of the MIT license. See [LICENSE](LICENSE) for details.
--- /dev/null
+++ b/demo/frame.c
@@ -1,0 +1,237 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <microui.h>
+
+static char logbuf[64000];
+static int logbuf_updated = 0;
+
+static void
+write_log(const char *text)
+{
+	if (logbuf[0])
+		strcat(logbuf, "\n");
+	strcat(logbuf, text);
+	logbuf_updated = 1;
+}
+
+#define text_width(s) (stringwidth(mu_style.font, s) + 6)
+#define text_height() mu_style.font->height
+#define max(a, b) ((a) > (b) ? (a) : (b))
+
+static void
+test_window(void)
+{
+	static mu_Container window;
+
+	/* init window manually so we can set its position and size */
+	if (!window.inited) {
+		mu_init_window(&window, 0);
+		window.rect = mu_rect(40, 40, 320, 500);
+	}
+
+	/* limit window to minimum size */
+	window.rect.w = max(window.rect.w, 240);
+	window.rect.h = max(window.rect.h, 300);
+
+	/* do window */
+	if (mu_begin_window(&window, "Demo Window")) {
+
+		/* window info */
+		static int show_info = 0;
+		if (mu_header(&show_info, "Window Info")) {
+			char buf[64];
+			const int widths[] = { text_width("Position:"), -1 };
+			mu_layout_row(2, widths, 0);
+			mu_label("Position:");
+			sprint(buf, "%d, %d", window.rect.x, window.rect.y); mu_label(buf);
+			mu_label("Size:");
+			sprint(buf, "%d, %d", window.rect.w, window.rect.h); mu_label(buf);
+		}
+
+		/* labels + buttons */
+		static int show_buttons = 1;
+		if (mu_header(&show_buttons, "Test Buttons")) {
+			const int widths[] = { text_width("Test buttons 2:"), -text_width("Button 2	"), -1 };
+			mu_layout_row(3, widths, 0);
+			mu_label("Test buttons 1:");
+			if (mu_button("Button 1")) { write_log("Pressed button 1"); }
+			if (mu_button("Button 2")) { write_log("Pressed button 2"); }
+			mu_label("Test buttons 2:");
+			if (mu_button("Button 3")) { write_log("Pressed button 3"); }
+			if (mu_button("Button 4")) { write_log("Pressed button 4"); }
+		}
+
+		/* tree */
+		static int show_tree = 1;
+		if (mu_header(&show_tree, "Tree and Text")) {
+			int widths[] = { text_width("Test 1a")+text_height()*2+text_width("Button 3")+6, -1 };
+			mu_layout_row(2, widths, 0);
+			mu_layout_begin_column();
+			static int states[8];
+			if (mu_begin_treenode(&states[0], "Test 1")) {
+				if (mu_begin_treenode(&states[1], "Test 1a")) {
+					mu_label("Hello");
+					mu_label("world");
+					mu_end_treenode();
+				}
+				if (mu_begin_treenode(&states[2], "Test 1b")) {
+					if (mu_button("Button 1")) { write_log("Pressed button 1"); }
+					if (mu_button("Button 2")) { write_log("Pressed button 2"); }
+					mu_end_treenode();
+				}
+				mu_end_treenode();
+			}
+			if (mu_begin_treenode(&states[3], "Test 2")) {
+				int widths[2];
+				widths[0] = widths[1] = text_width("Button 3");
+				mu_layout_row(2, widths, 0);
+				if (mu_button("Button 3")) { write_log("Pressed button 3"); }
+				if (mu_button("Button 4")) { write_log("Pressed button 4"); }
+				if (mu_button("Button 5")) { write_log("Pressed button 5"); }
+				if (mu_button("Button 6")) { write_log("Pressed button 6"); }
+				mu_end_treenode();
+			}
+			if (mu_begin_treenode(&states[4], "Test 3")) {
+				static int checks[3] = { 1, 0, 1 };
+				mu_checkbox(&checks[0], "Checkbox 1");
+				mu_checkbox(&checks[1], "Checkbox 2");
+				mu_checkbox(&checks[2], "Checkbox 3");
+				mu_end_treenode();
+			}
+			mu_layout_end_column();
+
+			mu_layout_begin_column();
+			widths[0] = -1;
+			mu_layout_row(1, widths, 0);
+			mu_text("Lorem ipsum dolor sit amet, consectetur adipiscing "
+				"elit. Maecenas lacinia, sem eu lacinia molestie, mi risus faucibus "
+				"ipsum, eu varius magna felis a nulla.");
+			mu_layout_end_column();
+		}
+
+		mu_end_window();
+	}
+}
+
+
+static void
+log_window(void)
+{
+	static mu_Container window;
+
+	/* init window manually so we can set its position and size */
+	if (!window.inited) {
+		mu_init_window(&window, 0);
+		window.rect = mu_rect(370, 40, 340, 200);
+	}
+
+	if (mu_begin_window(&window, "Log Window")) {
+		int widths[] = { -1, -1 };
+
+		/* output text panel */
+		static mu_Container panel;
+		mu_layout_row(1, widths, -28);
+		mu_begin_panel(&panel);
+		mu_layout_row(1, widths, -1);
+		mu_text(logbuf);
+		mu_end_panel();
+		if (logbuf_updated) {
+			panel.scroll.y = panel.content_size.y;
+			logbuf_updated = 0;
+		}
+
+		/* input textbox + submit button */
+		static char buf[128];
+		int submitted = 0;
+		widths[0] = -text_width("Submit")-8;
+		mu_layout_row(2, widths, -1);
+		if (mu_textbox(buf, sizeof(buf)) & MU_RES_SUBMIT) {
+			mu_set_focus(mu_ctx.last_id);
+			submitted = 1;
+		}
+		if (mu_button("Submit")) { submitted = 1; }
+		if (submitted) {
+			write_log(buf);
+			buf[0] = '\0';
+		}
+
+		mu_end_window();
+	}
+}
+
+
+static int
+uint8_slider(unsigned char *value, int low, int high)
+{
+	static double tmp;
+	mu_push_id(&value, sizeof(value));
+	tmp = *value;
+	int res = mu_slider_ex(&tmp, low, high, 0, "%.0f", MU_OPT_ALIGNCENTER);
+	*value = tmp;
+	mu_pop_id();
+	return res;
+}
+
+static void
+style_window(void)
+{
+	static mu_Container window;
+	static u8int cur[MU_COLOR_MAX][4], old[MU_COLOR_MAX][4];
+	static struct { const char *label; int idx; } colors[] = {
+		{ "background:", MU_COLOR_BG },
+		{ "text:", MU_COLOR_TEXT },
+		{ "border:", MU_COLOR_BORDER },
+		{ "windowbg:", MU_COLOR_WINDOWBG },
+		{ "titlebg:", MU_COLOR_TITLEBG },
+		{ "titletext:", MU_COLOR_TITLETEXT },
+		{ "panelbg:", MU_COLOR_PANELBG },
+		{ "button:", MU_COLOR_BUTTON },
+		{ "buttonhover:", MU_COLOR_BUTTONHOVER },
+		{ "buttonfocus:", MU_COLOR_BUTTONFOCUS },
+		{ "base:", MU_COLOR_BASE },
+		{ "basehover:", MU_COLOR_BASEHOVER },
+		{ "basefocus:", MU_COLOR_BASEFOCUS },
+		{ "scrollbase:", MU_COLOR_SCROLLBASE },
+		{ "scrollthumb:", MU_COLOR_SCROLLTHUMB },
+		{ nil }
+	};
+
+	/* init window manually so we can set its position and size */
+	if (!window.inited) {
+		mu_init_window(&window, 0);
+		window.rect = mu_rect(370, 250, 340, 290);
+		memmove(cur, defaultcolors, sizeof(cur));
+		memmove(old, defaultcolors, sizeof(old));
+	}
+
+	if (mu_begin_window(&window, "Style Editor")) {
+		int sw = max(text_width("255"), mu_get_container()->body.w * 0.14);
+		const int widths[] = { text_width("scrollthumb:"), sw, sw, sw, sw, -1 };
+		mu_layout_row(6, widths, 0);
+		for (int i = 0; colors[i].label; i++) {
+			mu_label(colors[i].label);
+			uint8_slider(&cur[i][0], 0, 255);
+			uint8_slider(&cur[i][1], 0, 255);
+			uint8_slider(&cur[i][2], 0, 255);
+			uint8_slider(&cur[i][3], 0, 255);
+			if (memcmp(cur[i], old[i], 4) != 0) {
+				freeimage(mu_style.colors[i]);
+				mu_style.colors[i] = mu_color(cur[i][0], cur[i][1], cur[i][2], cur[i][3]);
+				memmove(old[i], cur[i], 4);
+			}
+			mu_draw_rect(mu_layout_next(), mu_style.colors[i]);
+		}
+		mu_end_window();
+	}
+}
+
+void
+process_frame(void)
+{
+	mu_begin();
+	test_window();
+	log_window();
+	style_window();
+	mu_end();
+}
--- /dev/null
+++ b/demo/mkfile
@@ -1,0 +1,11 @@
+</$objtype/mkfile
+
+TARG=demo
+
+OFILES=\
+	frame.$O\
+	plan9.$O\
+
+default:V:	all
+
+</sys/src/cmd/mkone
--- /dev/null
+++ b/demo/plan9.c
@@ -1,0 +1,120 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <thread.h>
+#include <bio.h>
+#include <microui.h>
+
+void process_frame(void);
+
+void
+threadmain(int argc, char **argv)
+{
+	Mousectl *mctl;
+	char *s;
+	Biobuf *snarf;
+	Keyboardctl *kctl;
+	Rune key;
+	Mouse m;
+	Alt a[] = {
+		{ nil, &m, CHANRCV },
+		{ nil, nil, CHANRCV },
+		{ nil, &key, CHANRCV },
+		{ nil, nil,  CHANEND},
+	};
+	int oldbuttons, b, nkey, gotevent;
+	char text[5];
+
+	USED(argc); USED(argv);
+
+	if (initdraw(nil, nil, "microui demo") < 0)
+		sysfatal("initdraw: %r");
+	if ((mctl = initmouse(nil, screen)) == nil)
+		sysfatal("initmouse: %r");
+	if ((kctl = initkeyboard(nil)) == nil)
+		sysfatal("initkeyboard: %r");
+
+	a[0].c = mctl->c;
+	a[1].c = mctl->resizec;
+	a[2].c = kctl->c;
+
+	srand(time(0));
+	threadsetname("microui demo");
+
+	mu_init();
+	mu_style.font = font;
+	mu_style.size.y = font->height;
+	mu_style.title_height = mu_style.size.y + 6;
+	process_frame();
+
+	oldbuttons = 0;
+	for (;;) {
+		process_frame();
+		if (mu_render())
+			flushimage(display, 1);
+
+		gotevent = 1;
+		switch (alt(a)) {
+		case 0: /* mouse */
+			m.xy.x -= screen->r.min.x;
+			m.xy.y -= screen->r.min.y;
+			mu_input_mousemove(m.xy.x, m.xy.y);
+			if ((b = (m.buttons & 1)) != (oldbuttons & 1))
+				(b ? mu_input_mousedown : mu_input_mouseup)(m.xy.x, m.xy.y, MU_MOUSE_LEFT);
+			else if ((b = (m.buttons & 2)) != (oldbuttons & 2))
+				(b ? mu_input_mousedown : mu_input_mouseup)(m.xy.x, m.xy.y, MU_MOUSE_MIDDLE);
+			else if ((b = (m.buttons & 4)) != (oldbuttons & 4))
+				(b ? mu_input_mousedown : mu_input_mouseup)(m.xy.x, m.xy.y, MU_MOUSE_RIGHT);
+			if (m.buttons == 5 && (snarf = Bopen("/dev/snarf", OREAD)) != nil) {
+				if ((s = Brdstr(snarf, 0, 1)) != nil) {
+					mu_input_text(s);
+					free(s);
+				}
+				Bterm(snarf);
+			}
+			oldbuttons = m.buttons;
+			break;
+
+		case 1: /* resize */
+			getwindow(display, Refnone);
+			break;
+
+		case 2: /* keyboard */
+			nkey = -1;
+			switch (key) {
+			case Kdel: goto end;
+			case Kshift: nkey = MU_KEY_SHIFT; break;
+			case Kbs: nkey = MU_KEY_BACKSPACE; break;
+			case '\n': nkey = MU_KEY_RETURN; break;
+			case Knack: nkey = MU_KEY_NACK; break;
+			case Kleft: nkey = MU_KEY_LEFT; break;
+			case Kright: nkey = MU_KEY_RIGHT; break;
+			case Kesc: mu_set_focus(0); break;
+			default:
+				if (key < 0xf000 || key > 0xffff) {
+					memset(text, 0, sizeof(text));
+					if (runetochar(text, &key) > 0)
+						mu_input_text(text);
+				}
+				break;
+			}
+			if (nkey >= 0) {
+				mu_input_keydown(nkey);
+				mu_input_keyup(nkey);
+			}
+			break;
+
+		default:
+			gotevent = 0;
+			break;
+		}
+
+		if (gotevent)
+			process_frame();
+	}
+
+end:
+	threadexitsall(nil);
+}
binary files /dev/null b/doc/microui_9front.png differ
--- /dev/null
+++ b/doc/usage.md
@@ -1,0 +1,260 @@
+# Usage
+* **[Overview](#overview)**
+* **[Getting Started](#getting-started)**
+* **[Layout System](#layout-system)**
+* **[Style Customisation](#style-customisation)**
+* **[Custom Controls](#custom-controls)**
+
+## Overview
+The overall structure when using the library is as follows:
+```
+initialise `mu_Context`
+
+main loop:
+  call `mu_input_...` functions
+  call `mu_begin()`
+  process ui
+  call `mu_end()`
+  iterate commands using `mu_command_next()`
+```
+
+## Getting Started
+Before use a `mu_Context` should be initialised:
+```c
+mu_Context *ctx = malloc(sizeof(mu_Context));
+mu_init(ctx);
+```
+
+For font alignment and clipping to work correctly you should also set the
+context's `text_width` and `text_height` callback functions:
+```c
+ctx->text_width = text_width;
+ctx->text_height = text_height;
+```
+
+In your main loop you should first pass user input to microui using the
+`mu_input_...` functions. It is safe to call the input functions multiple
+times if the same input event occurs in a single frame.
+
+After handling the input the `mu_begin()` function must be called before
+processing your UI:
+```c
+mu_begin(ctx);
+```
+
+Before any controls can be used we must begin a window using one of the
+`mu_begin_window...` or `mu_begin_popup...` functions. The `mu_Container`
+for the window is expected to be either zeroed memory in which case
+it will be initialised automatically when used, or to have been
+initialised manually using the `mu_init_window()` function; once used,
+the `mu_Container`'s memory must remain valid until `mu_end()` is called
+at the end of the frame. The `mu_begin_...` window functions return a
+truthy value if the window is open, if this is not the case we should
+not process the window any further. When we are finished processing the
+window's ui the `mu_end_...` window function should be called.
+
+```c
+static mu_Container window;
+
+if (mu_begin_window(ctx, &window, "My Window")) {
+  /* process ui here... */
+  mu_end_window(ctx);
+}
+```
+
+It is safe to nest `mu_begin_window()` calls, this can be useful for
+things like context menus; the windows will still render separate from
+one another like normal.
+
+While inside a window block we can safely process controls. Controls
+that allow user interaction return a bitset of `MU_RES_...` values. Some
+controls — such as buttons — can only potentially return a single
+`MU_RES_...`, thus their return value can be treated as a boolean:
+```c
+if (mu_button(ctx, "My Button")) {
+  printf("'My Button' was pressed\n");
+}
+```
+
+The library generates unique IDs for controls internally to keep track
+of which are focused, hovered, etc. These are generated either from the
+pointer passed to the function (eg. for treenodes, checkboxes, textboxes
+and sliders), or the string/icon passed to the function (eg. buttons). An
+issue arises then if you have several buttons in a window or panel that
+use the same label. The `mu_push_id()` and `mu_pop_id()` functions are
+provided for such situations, allowing you to push additional data that
+will be mixed into the unique ID:
+```c
+for (int i = 0; i < 10; i++) {
+  mu_push_id(ctx, &i, sizeof(i));
+  if (mu_button(ctx, "x")) {
+    printf("Pressed button %d\n", i);
+  }
+  mu_pop_id(ctx);
+}
+```
+
+When we're finished processing the UI for this frame the `mu_end()`
+function should be called:
+```c
+mu_end(ctx);
+```
+
+When we're ready to draw the UI the `mu_next_command()` can be used
+to iterate the resultant commands. The function expects a `mu_Command`
+pointer initialised to `NULL`. It is safe to iterate through the commands
+list any number of times:
+```c
+mu_Command *cmd = NULL;
+while (mu_next_command(ctx, &cmd)) {
+  if (cmd->type == MU_COMMAND_TEXT) {
+    render_text(cmd->text.font, cmd->text.text, cmd->text.pos.x, cmd->text.pos.y, cmd->text.color);
+  }
+  if (cmd->type == MU_COMMAND_RECT) {
+    render_rect(cmd->rect.rect, cmd->rect.color);
+  }
+  if (cmd->type == MU_COMMAND_ICON) {
+    render_icon(cmd->icon.id, cmd->icon.rect, cmd->icon.color);
+  }
+  if (cmd->type == MU_COMMAND_CLIP) {
+    set_clip_rect(cmd->clip.rect);
+  }
+}
+```
+
+See the [`demo`](../demo) directory for a usage example.
+
+
+## Layout System
+The layout system is primarily based around *rows* — Each row
+can contain a number of *items* or *columns* each column can itself
+contain a number of rows and so forth. A row is initialised using the
+`mu_layout_row()` function, the user should specify the number of items
+on the row, an array containing the width of each item, and the height
+of the row:
+```c
+/* initialise a row of 3 items: the first item with a width
+** of 90 and the remaining two with the width of 100 */
+mu_layout_row(ctx, 3, (int[]) { 90, 100, 100 }, 0);
+```
+When a row is filled the next row is started, for example, in the above
+code 6 buttons immediately after would result in two rows. The function
+can be called again to begin a new row.
+
+As well as absolute values, width and height can be specified as `0`
+which will result in the Context's `style.size` value being used, or a
+negative value which will size the item relative to the right/bottom edge,
+thus if we wanted a row with a small button at the left, a textbox filling
+most the row and a larger button at the right, we could do the following:
+```c
+mu_layout_row(ctx, 3, (int[]) { 30, -90, -1 }, 0);
+mu_button(ctx, "X");
+mu_textbox(ctx, buf, sizeof(buf));
+mu_button(ctx, "Submit");
+```
+
+If the `items` parameter is `-1`, the `widths` parameter is ignored
+and controls will continue to be added to the row at the width last
+specified by `mu_layout_width()` or `style.size.x` if this function has
+not been called:
+```c
+mu_layout_row(ctx, -1, NULL, 0);
+mu_layout_width(ctx, -90);
+mu_textbox(ctx, buf, sizeof(buf));
+mu_layout_width(ctx, -1);
+mu_button(ctx, "Submit");
+```
+
+A column can be started at any point on a row using the
+`mu_layout_begin_column()` function. Once begun, rows will act inside
+the body of the column — all negative size values will be relative to
+the column's body as opposed to the body of the container. All new rows
+will be contained within this column until the `mu_layout_end_column()`
+function is called.
+
+Internally controls use the `mu_layout_next()` function to retrieve the
+next screen-positioned-Rect and advance the layout system, you should use
+this function when making custom controls or if you want to advance the
+layout system without placing a control.
+
+The `mu_layout_set_next()` function is provided to set the next layout
+Rect explicitly. This will be returned by `mu_layout_next()` when it is
+next called. By using the `relative` boolean you can choose to provide
+a screen-space Rect or a Rect which will have the container's position
+and scroll offset applied to it. You can peek the next Rect from the
+layout system by using the `mu_layout_next()` function to retrieve it,
+followed by `mu_layout_set_next()` to return it:
+```c
+mu_Rect rect = mu_layout_next(ctx);
+mu_layout_set_next(ctx, rect, 0);
+```
+
+If you want to position controls arbitrarily inside a container the
+`relative` argument of `mu_layout_set_next()` should be true:
+```c
+/* place a (40, 40) sized button at (300, 300) inside the container: */
+mu_layout_set_next(ctx, mu_rect(300, 300, 40, 40), 1);
+mu_button(ctx, "X");
+```
+A Rect set with `relative` true will also effect the `content_size`
+of the container, causing it to effect the scrollbars if it exceeds the
+width or height of the container's body.
+
+
+## Style Customisation
+The library provides styling support via the `mu_Style` struct and, if you
+want greater control over the look, the `draw_frame()` callback function.
+
+The `mu_Style` struct contains spacing and sizing information, as well
+as a `colors` array which maps `colorid` to `mu_Color`. The library uses
+the `style` pointer field of the context to resolve colors and spacing,
+it is safe to change this pointer or modify any fields of the resultant
+struct at any point. See [`microui.h`](../src/microui.h) for the struct's
+implementation.
+
+In addition to the style struct the context stores a `draw_frame()`
+callback function which is used whenever the *frame* of a control needs
+to be drawn, by default this function draws a rectangle using the color
+of the `colorid` argument, with a one-pixel border around it using the
+`MU_COLOR_BORDER` color.
+
+
+## Custom Controls
+The library exposes the functions used by built-in controls to allow the
+user to make custom controls. A control should take a `mu_Context*` value
+as its first argument and return a `MU_RES_...` value. Your control's
+implementation should use `mu_layout_next()` to get its destination
+Rect and advance the layout system. `mu_get_id()` should be used with
+some data unique to the control to generate an ID for that control and
+`mu_update_control()` should be used to update the context's `hover`
+and `focus` values based on the mouse input state.
+
+The `MU_OPT_HOLDFOCUS` opt value can be passed to `mu_update_control()`
+if we want the control to retain focus when the mouse button is released
+— this behaviour is used by textboxes which we want to stay focused
+to allow for text input.
+
+A control that acts as a button which displays an integer and, when
+clicked increments that integer, could be implemented as such:
+```c
+int incrementer(mu_Context *ctx, int *value) {
+  mu_Id     id = mu_get_id(ctx, &value, sizeof(value));
+  mu_Rect rect = mu_layout_next(ctx);
+  mu_update_control(ctx, id, rect, 0);
+
+  /* handle input */
+  int res = 0;
+  if (ctx->mouse_pressed == MU_MOUSE_LEFT && ctx->focus == id) {
+    (*value)++;
+    res |= MU_RES_CHANGE;
+  }
+
+  /* draw */
+  char buf[32];
+  sprintf(buf, "%d", *value);
+  mu_draw_control_frame(ctx, id, rect, MU_COLOR_BUTTON, 0);
+  mu_draw_control_text(ctx, buf, rect, MU_COLOR_TEXT, MU_OPT_ALIGNCENTER);
+
+  return res;
+}
+```
binary files /dev/null b/icons/mu_icon_check.png differ
binary files /dev/null b/icons/mu_icon_close.png differ
binary files /dev/null b/icons/mu_icon_collapsed.png differ
binary files /dev/null b/icons/mu_icon_expanded.png differ
binary files /dev/null b/icons/mu_icon_resize.png differ
--- /dev/null
+++ b/microui.c
@@ -1,0 +1,1501 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <microui.h>
+
+#define MU_REAL_FMT "%.3g"
+#define MU_SLIDER_FMT "%.2f"
+
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+#define CLAMP(x, a, b) MIN(b, MAX(a, x))
+
+static mu_Rect unclipped_rect = { 0, 0, 0x1000000, 0x1000000 };
+
+static u8int atlasraw[] = {
+	0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x0a, 0x20, 0x20, 0x20, 0x72, 0x38,
+	0x67, 0x38, 0x62, 0x38, 0x61, 0x38, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x30, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x30, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x33, 0x34, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x32, 0x35, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x32, 0x35, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x34, 0x33, 0x39, 0x20, 0x80,
+	0x00, 0x7c, 0x00, 0x7c, 0x00, 0x7c, 0x00, 0x78, 0x00, 0x7c, 0x00, 0x7c, 0x00, 0x7c, 0x00, 0x7c,
+	0x00, 0x7c, 0x00, 0x7c, 0x00, 0x7c, 0x00, 0x7c, 0x00, 0x7c, 0x00, 0x7c, 0x00, 0x34, 0x00, 0x80,
+	0x14, 0x00, 0x00, 0x80, 0x5c, 0x00, 0x00, 0x54, 0x1f, 0x04, 0x1b, 0x04, 0x23, 0x24, 0x13, 0x7c,
+	0x00, 0x3c, 0x00, 0x80, 0x20, 0x00, 0x00, 0x80, 0xc0, 0x00, 0x00, 0x04, 0x07, 0x44, 0x1f, 0x80,
+	0x5e, 0x00, 0x00, 0x80, 0xff, 0x00, 0x00, 0x80, 0x70, 0x00, 0x00, 0x34, 0x1b, 0x04, 0x13, 0x04,
+	0x1b, 0x04, 0x23, 0x24, 0x17, 0x7c, 0x00, 0x2c, 0x00, 0x80, 0x21, 0x00, 0x00, 0x80, 0xe0, 0x00,
+	0x00, 0x80, 0xea, 0x00, 0x00, 0x80, 0x2c, 0x00, 0x00, 0x54, 0x27, 0x14, 0x6f, 0x24, 0x8b, 0x44,
+	0x13, 0x14, 0x00, 0x7c, 0x00, 0x4c, 0x83, 0x80, 0x2d, 0x00, 0x00, 0x74, 0x2f, 0x24, 0x77, 0x74,
+	0x83, 0x7c, 0x00, 0x1c, 0x83, 0x80, 0xe1, 0x00, 0x00, 0x80, 0xeb, 0x00, 0x00, 0x7c, 0x83, 0x3c,
+	0x8b, 0x74, 0x83, 0x24, 0x00, 0x80, 0x0e, 0x00, 0x00, 0x7c, 0x83, 0x7c, 0x83, 0x7c, 0x87, 0x3c,
+	0x00, 0x14, 0x00, 0x80, 0x3d, 0x00, 0x00, 0x80, 0xed, 0x00, 0x00, 0x80, 0x45, 0x00, 0x00, 0x24,
+	0x17, 0x80, 0x22, 0x00, 0x00, 0x7c, 0x83, 0x7d, 0x97, 0x75, 0x97, 0x14, 0x00, 0x80, 0x13, 0x00,
+	0x00, 0x80, 0xd0, 0x00, 0x00, 0x80, 0xf6, 0x00, 0x00, 0x14, 0x8b, 0x24, 0x83, 0x80, 0x2e, 0x00,
+	0x00, 0x7c, 0x79, 0x7e, 0xa7, 0x54, 0x8b, 0x54, 0x8b, 0x80, 0x63, 0x00, 0x00, 0x7c, 0x83, 0x4c,
+	0x00, 0x7f, 0xb7, 0x3f, 0xb7, 0x54, 0x8b, 0x04, 0x2b, 0x80, 0xec, 0x00, 0x00, 0x7c, 0x83, 0x3c,
+	0x00, 0x80, 0x14, 0x00, 0x00, 0x80, 0x5c, 0x00, 0x00, 0x54, 0x1f, 0x04, 0x1b, 0x04, 0x23, 0x24,
+	0x13, 0x54, 0x8b, 0x80, 0xbc, 0x00, 0x00, 0x7c, 0x83, 0x7c, 0x00, 0x7c, 0x00, 0x0c, 0x00, 0x7c,
+	0x00, 0x7c, 0x00, 0x7c, 0x00, 0x7c, 0x00, 0x7c, 0x00, 0x7c, 0x00, 0x7c, 0x00, 0x7c, 0x00, 0x7c,
+	0x00, 0x7c, 0x00, 0x64, 0x00, 0x80, 0x5a, 0x00, 0x00, 0x06, 0x6b, 0x14, 0x07, 0x80, 0x8f, 0x00,
+	0x00, 0x44, 0x27, 0x7c, 0x00, 0x7c, 0x00, 0x64, 0x00, 0x80, 0x3c, 0x00, 0x00, 0x80, 0xaa, 0x00,
+	0x00, 0x04, 0x07, 0x04, 0x83, 0x54, 0x8b, 0x44, 0x17, 0x14, 0x2f, 0x7c, 0x65, 0x7c, 0x00, 0x34,
+	0x00, 0x54, 0x8b, 0x34, 0x17, 0x64, 0xb3, 0x7c, 0x00, 0x0c, 0x00, 0x80, 0x6e, 0x00, 0x00, 0x05,
+	0x03, 0x35, 0x0f, 0x64, 0x8b, 0x34, 0x17, 0x7c, 0x83, 0x7c, 0x00, 0x80, 0xa5, 0x00, 0x00, 0x15,
+	0x4f, 0x26, 0x1f, 0x64, 0x83, 0x74, 0xa3, 0x7c, 0x00, 0x7c, 0x00, 0x7c, 0x63, 0x81, 0x00, 0x00,
+	0x34, 0x27, 0x7c, 0x4d, 0x4c, 0x00, 0x80, 0x55, 0x00, 0x00, 0x80, 0x37, 0x00, 0x00, 0x04, 0x0b,
+	0x04, 0x07, 0x04, 0x0f, 0x04, 0x0b, 0x80, 0x1e, 0x00, 0x00, 0x04, 0x0b, 0x04, 0x07, 0x64, 0x83,
+	0x7c, 0x00, 0x7c, 0x00, 0x16, 0xef, 0x05, 0x8f, 0x75, 0x97, 0x54, 0x00, 0x7c, 0x00, 0x7c, 0x00,
+	0x7d, 0x0f, 0x0d, 0x0f, 0x64, 0x3f,
+};
+
+static mu_Rect default_atlas_icons[] = {
+	[MU_ICON_CHECK] = {0, 0, 18, 18},
+	[MU_ICON_CLOSE] = {18, 0, 16, 16},
+	[MU_ICON_COLLAPSED] = {27, 16, 5, 7},
+	[MU_ICON_EXPANDED] = {0, 18, 7, 5},
+	[MU_ICON_RESIZE] = {18, 16, 9, 9},
+	[ATLAS_DIMENSIONS] = {0, 0, 34, 25},
+};
+
+mu_Context mu_ctx;
+mu_Style mu_style = {
+	.font = nil,
+	.size = { 68, 10 },
+	.padding = 6,
+	.spacing = 4,
+	.indent = 24,
+	.title_height = 26,
+	.scrollbar_size = 12,
+	.thumb_size = 8,
+	.colors = {nil},
+};
+
+Image *atlasimage = nil;
+mu_Rect *atlasicons = default_atlas_icons;
+u8int defaultcolors[MU_COLOR_MAX][4] = {
+	[MU_COLOR_BG] = {119, 119, 119, 255},
+	[MU_COLOR_TEXT] = {230, 230, 230, 255},
+	[MU_COLOR_BORDER] = {25, 25, 25, 255},
+	[MU_COLOR_WINDOWBG] = {50, 50, 50, 255},
+	[MU_COLOR_TITLEBG] = {25, 25, 25, 255},
+	[MU_COLOR_TITLETEXT] = {240, 240, 240, 255},
+	[MU_COLOR_PANELBG] = {0, 0, 0, 0},
+	[MU_COLOR_BUTTON] = {75, 75, 75, 255},
+	[MU_COLOR_BUTTONHOVER] = {95, 95, 95, 255},
+	[MU_COLOR_BUTTONFOCUS] = {115, 115, 115, 255},
+	[MU_COLOR_BASE] = {30, 30, 30, 255},
+	[MU_COLOR_BASEHOVER] = {35, 35, 35, 255},
+	[MU_COLOR_BASEFOCUS] = {40, 40, 40, 255},
+	[MU_COLOR_SCROLLBASE] = {43, 43, 43, 255},
+	[MU_COLOR_SCROLLTHUMB] = {30, 30, 30, 255},
+};
+
+static void
+buffer_grow(void **buf, int onesz, int *bufmax, int bufnum)
+{
+	while (*bufmax <= bufnum) {
+		*bufmax = MAX(16, *bufmax) * 2;
+		if ((*buf = realloc(*buf, *bufmax*onesz)) == nil)
+			sysfatal("not enough memory for %d items (%d bytes)", *bufmax, *bufmax*onesz);
+	}
+}
+
+mu_Rect
+mu_rect(int x, int y, int w, int h)
+{
+	mu_Rect res;
+	res.x = x, res.y = y, res.w = w, res.h = h;
+	return res;
+}
+
+Image *
+mu_color(u8int r, u8int g, u8int b, u8int a)
+{
+	Image *c;
+	if ((c = allocimage(display, Rect(0, 0, 1, 1), RGBA32, 1, setalpha(r<<24 | g<<16 | b<<8, a))) == nil)
+		sysfatal("couldn't allocate color");
+	return c;
+}
+
+static Rectangle
+screenrect(mu_Rect r)
+{
+	Rectangle rect;
+	rect.min = screen->r.min;
+	rect.min.x += r.x;
+	rect.min.y += r.y;
+	rect.max = rect.min;
+	rect.max.x += r.w;
+	rect.max.y += r.h;
+	return rect;
+}
+
+static mu_Rect
+expand_rect(mu_Rect rect, int n) {
+	return mu_rect(rect.x - n, rect.y - n, rect.w + n * 2, rect.h + n * 2);
+}
+
+
+static mu_Rect
+clip_rect(mu_Rect r1, mu_Rect r2)
+{
+	int x1 = MAX(r1.x, r2.x);
+	int y1 = MAX(r1.y, r2.y);
+	int x2 = MAX(MIN(r1.x + r1.w, r2.x + r2.w), x1);
+	int y2 = MAX(MIN(r1.y + r1.h, r2.y + r2.h), y1);
+	return mu_rect(x1, y1, x2 - x1, y2 - y1);
+}
+
+
+static int
+rect_overlaps_vec2(mu_Rect r, Point p)
+{
+	return p.x >= r.x && p.x < r.x + r.w && p.y >= r.y && p.y < r.y + r.h;
+}
+
+static void
+draw_frame(mu_Rect rect, int colorid)
+{
+	mu_draw_rect(rect, mu_style.colors[colorid]);
+	if (colorid == MU_COLOR_SCROLLBASE	|| colorid == MU_COLOR_SCROLLTHUMB || colorid == MU_COLOR_TITLEBG)
+		return;
+	/* draw border */
+	mu_draw_box(expand_rect(rect, 1), mu_style.colors[MU_COLOR_BORDER]);
+}
+
+static int
+text_width(Font *font, const char *text, int len)
+{
+	return stringnwidth(font, text, len >= 0 ? len : strlen(text));
+}
+
+void
+mu_init(void)
+{
+	Rectangle r;
+	int res, i;
+
+	if (atlasimage == nil) {
+		r = Rect(0, 0, atlasicons[ATLAS_DIMENSIONS].w, atlasicons[ATLAS_DIMENSIONS].h);
+		atlasimage = allocimage(display, r, RGBA32, 1, DTransparent);
+		if (memcmp(atlasraw, "compressed\n", 11) == 0)
+			res = cloadimage(atlasimage, r, atlasraw+11+5*12, sizeof(atlasraw)-11-5*12);
+		else
+			res = loadimage(atlasimage, r, atlasraw, sizeof(atlasraw));
+		if (res < 0)
+			sysfatal("failed to load atlas: %r");
+	}
+
+	if (mu_style.colors[0] == nil) {
+		for (i = 0; i < MU_COLOR_MAX; i++) {
+			mu_style.colors[i] = mu_color(
+				defaultcolors[i][0],
+				defaultcolors[i][1],
+				defaultcolors[i][2],
+				defaultcolors[i][3]
+			);
+		}
+	}
+}
+
+void
+mu_begin(void)
+{
+	mu_ctx.cmdsnum = mu_ctx.rootnum = 0;
+	mu_ctx.strnum = 0;
+	mu_ctx.scroll_target = nil;
+	mu_ctx.last_hover_root = mu_ctx.hover_root;
+	mu_ctx.hover_root = nil;
+	mu_ctx.mouse_delta.x = mu_ctx.mouse_pos.x - mu_ctx.last_mouse_pos.x;
+	mu_ctx.mouse_delta.y = mu_ctx.mouse_pos.y - mu_ctx.last_mouse_pos.y;
+}
+
+static int
+compare_zindex(const void *a, const void *b)
+{
+	return (*(mu_Container**)a)->zindex - (*(mu_Container**)b)->zindex;
+}
+
+void
+mu_end(void)
+{
+	int i, n;
+	/* check stacks */
+	assert(mu_ctx.cntnum == 0);
+	assert(mu_ctx.clipnum == 0);
+	assert(mu_ctx.idsnum == 0);
+	assert(mu_ctx.layoutsnum == 0);
+
+	/* handle scroll input */
+	if (mu_ctx.scroll_target) {
+		mu_ctx.scroll_target->scroll.x += mu_ctx.scroll_delta.x;
+		mu_ctx.scroll_target->scroll.y += mu_ctx.scroll_delta.y;
+	}
+
+	/* unset focus if focus id was not touched this frame */
+	if (!mu_ctx.updated_focus)
+		mu_ctx.focus = 0;
+	mu_ctx.updated_focus = 0;
+
+	/* bring hover root to front if mouse was pressed */
+	if (mu_ctx.mouse_pressed && mu_ctx.hover_root && mu_ctx.hover_root->zindex < mu_ctx.last_zindex)
+		mu_bring_to_front(mu_ctx.hover_root);
+
+	/* reset input state */
+	mu_ctx.key_pressed = 0;
+	mu_ctx.text_input[0] = 0;
+	mu_ctx.mouse_pressed = 0;
+	mu_ctx.scroll_delta = ZP;
+	mu_ctx.last_mouse_pos = mu_ctx.mouse_pos;
+
+	/* sort root containers by zindex */
+	n = mu_ctx.rootnum;
+	qsort(mu_ctx.root, n, sizeof(*mu_ctx.root), compare_zindex);
+
+	/* set root container jump commands */
+	for (i = 0; i < n; i++) {
+		mu_Container *cnt = mu_ctx.root[i];
+		/* if this is the first container then make the first command jump to it.
+		** otherwise set the previous container's tail to jump to this one */
+		if (i == 0)
+			mu_ctx.cmds[0].jump.dst = cnt->head + 1;
+		else
+			mu_ctx.cmds[mu_ctx.root[i - 1]->tail].jump.dst = cnt->head + 1;
+
+		/* make the last container's tail jump to the end of command list */
+		if (i == n - 1)
+			mu_ctx.cmds[cnt->tail].jump.dst = mu_ctx.cmdsnum;
+	}
+}
+
+
+void
+mu_set_focus(mu_Id id)
+{
+	mu_ctx.focus = id;
+	mu_ctx.updated_focus = 1;
+}
+
+/* 32bit fnv-1a hash */
+#define HASH_INITIAL 2166136261
+
+static void
+hash(mu_Id *hash, const void *data, int size)
+{
+	const unsigned char *p = data;
+	while (size--) {
+		*hash = (*hash ^ *p++) * 16777619;
+	}
+}
+
+
+mu_Id
+mu_get_id(const void *data, int size)
+{
+	int idx = mu_ctx.idsnum;
+	mu_Id res = (idx > 0) ? mu_ctx.ids[idx - 1] : HASH_INITIAL;
+	hash(&res, data, size);
+	mu_ctx.last_id = res;
+	return res;
+}
+
+
+void
+mu_push_id(const void *data, int size)
+{
+	buffer_grow(&mu_ctx.ids, sizeof(*mu_ctx.ids), &mu_ctx.idsmax, mu_ctx.idsnum);
+	mu_ctx.ids[mu_ctx.idsnum++] = mu_get_id(data, size);
+}
+
+void
+mu_pop_id(void)
+{
+	mu_ctx.idsnum--;
+}
+
+void
+mu_push_clip_rect(mu_Rect rect)
+{
+	mu_Rect last = mu_get_clip_rect();
+	buffer_grow(&mu_ctx.clip, sizeof(*mu_ctx.clip), &mu_ctx.clipmax, mu_ctx.clipnum);
+	mu_ctx.clip[mu_ctx.clipnum++] = clip_rect(rect, last);
+}
+
+
+void
+mu_pop_clip_rect(void)
+{
+	mu_ctx.clipnum--;
+}
+
+
+mu_Rect
+mu_get_clip_rect(void)
+{
+	assert(mu_ctx.clipnum > 0);
+	return mu_ctx.clip[mu_ctx.clipnum - 1];
+}
+
+
+int
+mu_check_clip(mu_Rect r)
+{
+	mu_Rect cr = mu_get_clip_rect();
+	if (r.x > cr.x + cr.w || r.x + r.w < cr.x || r.y > cr.y + cr.h || r.y + r.h < cr.y)
+		return MU_CLIP_ALL;
+	if (r.x >= cr.x && r.x + r.w <= cr.x + cr.w && r.y >= cr.y && r.y + r.h <= cr.y + cr.h)
+		return MU_CLIP_NONE;
+	return MU_CLIP_PART;
+}
+
+
+static void
+push_layout(mu_Rect body, Point scroll)
+{
+	mu_Layout layout;
+	int width = 0;
+	memset(&layout, 0, sizeof(mu_Layout));
+	layout.body = mu_rect(body.x - scroll.x, body.y - scroll.y, body.w, body.h);
+	layout.max = Pt(-0x1000000, -0x1000000);
+	buffer_grow(&mu_ctx.layouts, sizeof(*mu_ctx.layouts), &mu_ctx.layoutsmax, mu_ctx.layoutsnum);
+	mu_ctx.layouts[mu_ctx.layoutsnum++] = layout;
+	mu_layout_row(1, &width, 0);
+}
+
+static mu_Layout *
+get_layout(void)
+{
+	return &mu_ctx.layouts[mu_ctx.layoutsnum - 1];
+}
+
+
+static void
+push_container(mu_Container *cnt)
+{
+	buffer_grow(&mu_ctx.cnt, sizeof(*mu_ctx.cnt), &mu_ctx.cntmax, mu_ctx.cntnum);
+	mu_ctx.cnt[mu_ctx.cntnum++] = cnt;
+	mu_push_id(&cnt, sizeof(mu_Container*));
+}
+
+
+static void
+pop_container(void)
+{
+	mu_Container *cnt = mu_get_container();
+	mu_Layout *layout = get_layout();
+	cnt->content_size.x = layout->max.x - layout->body.x;
+	cnt->content_size.y = layout->max.y - layout->body.y;
+	mu_ctx.cntnum--;
+	mu_ctx.layoutsnum--;
+	mu_pop_id();
+}
+
+
+mu_Container *
+mu_get_container(void)
+{
+	assert(mu_ctx.cntnum > 0);
+	return mu_ctx.cnt[mu_ctx.cntnum - 1];
+}
+
+
+void
+mu_init_window(mu_Container *cnt, int opt)
+{
+	memset(cnt, 0, sizeof(*cnt));
+	cnt->inited = 1;
+	cnt->open = opt & MU_OPT_CLOSED ? 0 : 1;
+	cnt->rect = mu_rect(100, 100, 300, 300);
+	mu_bring_to_front(cnt);
+}
+
+
+void
+mu_bring_to_front(mu_Container *cnt)
+{
+	cnt->zindex = ++mu_ctx.last_zindex;
+}
+
+
+/*============================================================================
+** input handlers
+**============================================================================*/
+
+void
+mu_input_mousemove(int x, int y)
+{
+	mu_ctx.mouse_pos = Pt(x, y);
+}
+
+
+void
+mu_input_mousedown(int x, int y, int btn)
+{
+	mu_input_mousemove(x, y);
+	mu_ctx.mouse_down |= btn;
+	mu_ctx.mouse_pressed |= btn;
+}
+
+
+void
+mu_input_mouseup(int x, int y, int btn)
+{
+	mu_input_mousemove(x, y);
+	mu_ctx.mouse_down &= ~btn;
+}
+
+
+void
+mu_input_scroll(int x, int y)
+{
+	mu_ctx.scroll_delta.x += x;
+	mu_ctx.scroll_delta.y += y;
+}
+
+
+void
+mu_input_keydown(int key)
+{
+	mu_ctx.key_pressed |= key;
+	mu_ctx.key_down |= key;
+}
+
+
+void
+mu_input_keyup(int key)
+{
+	mu_ctx.key_down &= ~key;
+}
+
+
+void
+mu_input_text(const char *text)
+{
+	int len = strlen(mu_ctx.text_input);
+	int size = strlen(text) + 1;
+	assert(len + size <= (int) sizeof(mu_ctx.text_input));
+	memcpy(mu_ctx.text_input + len, text, size);
+}
+
+/*============================================================================
+** commandlist
+**============================================================================*/
+
+mu_Command *
+mu_push_command(int type)
+{
+	mu_Command *cmd;
+
+	buffer_grow(&mu_ctx.cmds, sizeof(mu_Command), &mu_ctx.cmdsmax, mu_ctx.cmdsnum);
+	cmd = &mu_ctx.cmds[mu_ctx.cmdsnum++];
+	memset(cmd, 0, sizeof(*cmd));
+	cmd->type = type;
+
+	return cmd;
+}
+
+static int
+push_jump(int dst)
+{
+	mu_Command *cmd;
+	cmd = mu_push_command(MU_COMMAND_JUMP);
+	cmd->jump.dst = dst;
+	return mu_ctx.cmdsnum-1;
+}
+
+void
+mu_set_clip(mu_Rect rect)
+{
+	mu_Command *cmd;
+	cmd = mu_push_command(MU_COMMAND_CLIP);
+	cmd->clip.rect = rect;
+}
+
+void
+mu_draw_rect(mu_Rect rect, Image *color)
+{
+	mu_Command *cmd;
+	rect = clip_rect(rect, mu_get_clip_rect());
+	if (rect.w > 0 && rect.h > 0) {
+		cmd = mu_push_command(MU_COMMAND_RECT);
+		cmd->rect.rect = rect;
+		cmd->rect.color = color;
+	}
+}
+
+void
+mu_draw_box(mu_Rect rect, Image *color)
+{
+	mu_draw_rect(mu_rect(rect.x + 1, rect.y, rect.w - 2, 1), color);
+	mu_draw_rect(mu_rect(rect.x+1, rect.y + rect.h-1, rect.w-2, 1), color);
+	mu_draw_rect(mu_rect(rect.x, rect.y, 1, rect.h), color);
+	mu_draw_rect(mu_rect(rect.x + rect.w - 1, rect.y, 1, rect.h), color);
+}
+
+
+void
+mu_draw_text(Font *font, const char *s, int len, Point pos, Image *color)
+{
+	mu_Command *cmd;
+	mu_Rect rect;
+	int clipped;
+
+	if (len < 0)
+		len = strlen(s);
+	rect = mu_rect(pos.x, pos.y, text_width(font, s, len), font->height);
+	clipped = mu_check_clip(rect);
+
+	if (clipped == MU_CLIP_ALL )
+		return;
+	if (clipped == MU_CLIP_PART)
+		mu_set_clip(mu_get_clip_rect());
+
+	if (mu_ctx.strmax <= mu_ctx.strnum+len+1) {
+		mu_ctx.strmax = MAX(mu_ctx.strmax, mu_ctx.strnum+len+1) * 2;
+		if ((mu_ctx.str = realloc(mu_ctx.str, mu_ctx.strmax)) == nil)
+			sysfatal("not enough memory for %d chars", mu_ctx.strmax);
+	}
+	cmd = mu_push_command(MU_COMMAND_TEXT);
+	cmd->text.s = mu_ctx.strnum;
+	memmove(mu_ctx.str+mu_ctx.strnum, s, len);
+	mu_ctx.strnum += len;
+	mu_ctx.str[mu_ctx.strnum++] = 0;
+	cmd->text.pos = pos;
+	cmd->text.color = color;
+	cmd->text.font = font;
+
+	/* reset clipping if it was set */
+	if (clipped)
+		mu_set_clip(unclipped_rect);
+}
+
+
+void
+mu_draw_icon(int id, mu_Rect rect)
+{
+	mu_Command *cmd;
+	/* do clip command if the rect isn't fully contained within the cliprect */
+	int clipped = mu_check_clip(rect);
+	if (clipped == MU_CLIP_ALL )
+		return;
+	if (clipped == MU_CLIP_PART)
+		mu_set_clip(mu_get_clip_rect());
+	/* do icon command */
+	cmd = mu_push_command(MU_COMMAND_ICON);
+	cmd->icon.id = id;
+	cmd->icon.rect = rect;
+	/* reset clipping if it was set */
+	if (clipped)
+		mu_set_clip(unclipped_rect);
+}
+
+
+/*============================================================================
+** layout
+**============================================================================*/
+
+enum
+{
+	RELATIVE = 1,
+	ABSOLUTE,
+};
+
+
+void
+mu_layout_begin_column(void)
+{
+	push_layout(mu_layout_next(), ZP);
+}
+
+
+void
+mu_layout_end_column(void)
+{
+	mu_Layout *a, *b;
+	b = get_layout();
+	mu_ctx.layoutsnum--;
+	/* inherit position/next_row/max from child layout if they are greater */
+	a = get_layout();
+	a->position.x = MAX(a->position.x, b->position.x + b->body.x - a->body.x);
+	a->next_row = MAX(a->next_row, b->next_row + b->body.y - a->body.y);
+	a->max.x = MAX(a->max.x, b->max.x);
+	a->max.y = MAX(a->max.y, b->max.y);
+}
+
+
+void
+mu_layout_row(int items, const int *widths, int height)
+{
+	mu_Layout *layout = get_layout();
+	if (widths) {
+		assert(items <= MU_MAX_WIDTHS);
+		memcpy(layout->widths, widths, items * sizeof(widths[0]));
+	}
+	layout->items = items;
+	layout->position = Pt(layout->indent, layout->next_row);
+	layout->size.y = height;
+	layout->row_index = 0;
+}
+
+
+void
+mu_layout_width(int width)
+{
+	get_layout()->size.x = width;
+}
+
+
+void
+mu_layout_height(int height)
+{
+	get_layout()->size.y = height;
+}
+
+
+void
+mu_layout_set_next(mu_Rect r, int relative)
+{
+	mu_Layout *layout = get_layout();
+	layout->next = r;
+	layout->next_type = relative ? RELATIVE : ABSOLUTE;
+}
+
+
+mu_Rect
+mu_layout_next(void)
+{
+	mu_Layout *layout = get_layout();
+	mu_Rect res;
+
+	if (layout->next_type) {
+		/* handle rect set by `mu_layout_set_next` */
+		int type = layout->next_type;
+		layout->next_type = 0;
+		res = layout->next;
+		if (type == ABSOLUTE) {
+			mu_ctx.last_rect = res;
+			return res;
+		}
+
+	} else {
+		/* handle next row */
+		if (layout->row_index == layout->items)
+			mu_layout_row(layout->items, nil, layout->size.y);
+
+		/* position */
+		res.x = layout->position.x;
+		res.y = layout->position.y;
+
+		/* size */
+		res.w = layout->items > -1 ? layout->widths[layout->row_index] : layout->size.x;
+		res.h = layout->size.y;
+		if (res.w == 0)
+			res.w = mu_style.size.x + mu_style.padding * 2;
+		if (res.h == 0)
+			res.h = mu_style.size.y + mu_style.padding * 2;
+		if (res.w <	0)
+			res.w += layout->body.w - res.x + 1;
+		if (res.h <	0)
+			res.h += layout->body.h - res.y + 1;
+
+		layout->row_index++;
+	}
+
+	/* update position */
+	layout->position.x += res.w + mu_style.spacing;
+	layout->next_row = MAX(layout->next_row, res.y + res.h + mu_style.spacing);
+
+	/* apply body offset */
+	res.x += layout->body.x;
+	res.y += layout->body.y;
+
+	/* update max position */
+	layout->max.x = MAX(layout->max.x, res.x + res.w);
+	layout->max.y = MAX(layout->max.y, res.y + res.h);
+
+	mu_ctx.last_rect = res;
+	return res;
+}
+
+
+/*============================================================================
+** controls
+**============================================================================*/
+
+static int
+in_hover_root(void)
+{
+	int i = mu_ctx.cntnum;
+	while (i--) {
+		if (mu_ctx.cnt[i] == mu_ctx.last_hover_root)
+			return 1;
+		/* only root containers have their `head` field set; stop searching if we've
+		** reached the current root container */
+		if (mu_ctx.cnt[i]->head >= 0)
+			break;
+	}
+	return 0;
+}
+
+
+void
+mu_draw_control_frame(mu_Id id, mu_Rect rect, int colorid, int opt)
+{
+	if (opt & MU_OPT_NOFRAME)
+		return;
+	colorid += (mu_ctx.focus == id) ? 2 : (mu_ctx.hover == id) ? 1 : 0;
+	draw_frame(rect, colorid);
+}
+
+
+void
+mu_draw_control_text(const char *str, mu_Rect rect, int colorid, int opt)
+{
+	Point pos;
+	Font *font = mu_style.font;
+	int tw = text_width(font, str, -1);
+	mu_push_clip_rect(rect);
+	pos.y = rect.y + (rect.h - font->height) / 2;
+	if (opt & MU_OPT_ALIGNCENTER)
+		pos.x = rect.x + (rect.w - tw) / 2;
+	else if (opt & MU_OPT_ALIGNRIGHT)
+		pos.x = rect.x + rect.w - tw - mu_style.padding;
+	else
+		pos.x = rect.x + mu_style.padding;
+
+	mu_draw_text(font, str, -1, pos, mu_style.colors[colorid]);
+	mu_pop_clip_rect();
+}
+
+
+int
+mu_mouse_over(mu_Rect rect)
+{
+	return rect_overlaps_vec2(rect, mu_ctx.mouse_pos) &&
+		rect_overlaps_vec2(mu_get_clip_rect(), mu_ctx.mouse_pos) &&
+		in_hover_root();
+}
+
+
+void
+mu_update_control(mu_Id id, mu_Rect rect, int opt)
+{
+	int mouseover = mu_mouse_over(rect);
+
+	if (mu_ctx.focus == id)
+		mu_ctx.updated_focus = 1;
+	if (opt & MU_OPT_NOINTERACT)
+		return;
+	if (mouseover && !mu_ctx.mouse_down)
+		mu_ctx.hover = id;
+
+	if (mu_ctx.focus == id) {
+		if (mu_ctx.mouse_pressed && !mouseover)
+			mu_set_focus(0);
+		if (!mu_ctx.mouse_down && ~opt & MU_OPT_HOLDFOCUS)
+			mu_set_focus(0);
+	}
+
+	if (mu_ctx.hover == id) {
+		if (!mouseover)
+			mu_ctx.hover = 0;
+		else if (mu_ctx.mouse_pressed)
+			mu_set_focus(id);
+	}
+}
+
+
+void
+mu_text(const char *text)
+{
+	const char *start, *end, *p = text;
+	int width = -1;
+	Font *font = mu_style.font;
+	Image *color = mu_style.colors[MU_COLOR_TEXT];
+	mu_layout_begin_column();
+	mu_layout_row(1, &width, font->height);
+	do {
+		mu_Rect r = mu_layout_next();
+		int w = 0;
+		start = end = p;
+		do {
+			const char* word = p;
+			while (*p && *p != ' ' && *p != '\n')
+				p++;
+			w += text_width(font, word, p - word);
+			if (w > r.w && end != start)
+				break;
+			w += text_width(font, p, 1);
+			end = p++;
+		} while (*end && *end != '\n');
+		mu_draw_text(font, start, end - start, Pt(r.x, r.y), color);
+		p = end + 1;
+	} while (*end);
+	mu_layout_end_column();
+}
+
+
+void
+mu_label(const char *text)
+{
+	mu_draw_control_text(text, mu_layout_next(), MU_COLOR_TEXT, 0);
+}
+
+
+int
+mu_button_ex(const char *label, int icon, int opt) {
+	int res = 0;
+	mu_Id id = label ? mu_get_id(label, strlen(label)) : mu_get_id(&icon, sizeof(icon));
+	mu_Rect r = mu_layout_next();
+	mu_update_control(id, r, opt);
+	/* handle click */
+	if (mu_ctx.mouse_pressed == MU_MOUSE_LEFT && mu_ctx.focus == id)
+		res |= MU_RES_SUBMIT;
+
+	/* draw */
+	mu_draw_control_frame(id, r, MU_COLOR_BUTTON, opt);
+	if (label)
+		mu_draw_control_text(label, r, MU_COLOR_TEXT, opt);
+	if (icon)
+		mu_draw_icon(icon, r);
+	return res;
+}
+
+
+int
+mu_button(const char *label)
+{
+	return mu_button_ex(label, 0, MU_OPT_ALIGNCENTER);
+}
+
+
+int
+mu_checkbox(int *state, const char *label)
+{
+	int res = 0;
+	mu_Id id = mu_get_id(&state, sizeof(state));
+	mu_Rect r = mu_layout_next();
+	mu_Rect box = mu_rect(r.x, r.y, r.h, r.h);
+	mu_update_control(id, r, 0);
+	/* handle click */
+	if (mu_ctx.mouse_pressed == MU_MOUSE_LEFT && mu_ctx.focus == id) {
+		res |= MU_RES_CHANGE;
+		*state = !*state;
+	}
+	/* draw */
+	mu_draw_control_frame(id, box, MU_COLOR_BASE, 0);
+	if (*state)
+		mu_draw_icon(MU_ICON_CHECK, box);
+
+	r = mu_rect(r.x + box.w, r.y, r.w - box.w, r.h);
+	mu_draw_control_text(label, r, MU_COLOR_TEXT, 0);
+	return res;
+}
+
+
+int
+mu_textbox_raw(char *buf, int bufsz, mu_Id id, mu_Rect r, int opt)
+{
+	int res = 0;
+	mu_update_control(id, r, opt | MU_OPT_HOLDFOCUS);
+
+	if (mu_ctx.focus == id) {
+		/* handle text input */
+		int len = strlen(buf);
+		int n = MIN(bufsz - len - 1, (int)strlen(mu_ctx.text_input));
+		if (n > 0) {
+			memcpy(buf + len, mu_ctx.text_input, n);
+			len += n;
+			buf[len] = '\0';
+			res |= MU_RES_CHANGE;
+		}
+		/* handle backspace */
+		if (mu_ctx.key_pressed & MU_KEY_BACKSPACE && len > 0) {
+			/* skip utf-8 continuation bytes */
+			while ((buf[--len] & 0xc0) == 0x80 && len > 0);
+			buf[len] = '\0';
+			res |= MU_RES_CHANGE;
+		}
+		if (mu_ctx.key_pressed & MU_KEY_NACK && len > 0) {
+			buf[0] = '\0';
+			res |= MU_RES_CHANGE;
+		}
+		/* handle return */
+		if (mu_ctx.key_pressed & MU_KEY_RETURN) {
+			mu_set_focus(0);
+			res |= MU_RES_SUBMIT;
+		}
+	}
+
+	/* draw */
+	mu_draw_control_frame(id, r, MU_COLOR_BASE, opt);
+	if (mu_ctx.focus == id) {
+		Image *color = mu_style.colors[MU_COLOR_TEXT];
+		Font *font = mu_style.font;
+		int textw = text_width(font, buf, -1);
+		int texth = font->height;
+		int ofx = r.w - mu_style.padding - textw - 1;
+		int textx = r.x + MIN(ofx, mu_style.padding);
+		int texty = r.y + (r.h - texth) / 2;
+		mu_push_clip_rect(r);
+		mu_draw_text(font, buf, -1, Pt(textx, texty), color);
+		mu_draw_rect(mu_rect(textx + textw, texty, 1, texth), color);
+		mu_pop_clip_rect();
+	} else {
+		mu_draw_control_text(buf, r, MU_COLOR_TEXT, opt);
+	}
+
+	return res;
+}
+
+
+static int
+number_textbox(double *value, mu_Rect r, mu_Id id)
+{
+	if (((mu_ctx.mouse_pressed == MU_MOUSE_LEFT && mu_ctx.key_down & MU_KEY_SHIFT) || mu_ctx.mouse_pressed == MU_MOUSE_RIGHT) && mu_ctx.hover == id) {
+		mu_ctx.number_editing = id;
+		sprint(mu_ctx.number_buf, MU_REAL_FMT, *value);
+	}
+	if (mu_ctx.number_editing == id) {
+		int res = mu_textbox_raw(mu_ctx.number_buf, sizeof(mu_ctx.number_buf), id, r, 0);
+		if (res & MU_RES_SUBMIT || mu_ctx.focus != id) {
+			*value = strtod(mu_ctx.number_buf, nil);
+			mu_ctx.number_editing = 0;
+		} else {
+			return 1;
+		}
+	}
+	return 0;
+}
+
+
+int
+mu_textbox_ex(char *buf, int bufsz, int opt)
+{
+	mu_Id id = mu_get_id(&buf, sizeof(buf));
+	mu_Rect r = mu_layout_next();
+	return mu_textbox_raw(buf, bufsz, id, r, opt);
+}
+
+
+int
+mu_textbox(char *buf, int bufsz)
+{
+	return mu_textbox_ex(buf, bufsz, 0);
+}
+
+
+int
+mu_slider_ex(double *value, double low, double high, double step, const char *fmt, int opt)
+{
+	char buf[MU_MAX_FMT + 1];
+	mu_Rect thumb;
+	int w, res = 0;
+	double normalized, last = *value, v = last;
+	mu_Id id = mu_get_id(&value, sizeof(value));
+	mu_Rect base = mu_layout_next();
+
+	/* handle text input mode */
+	if (number_textbox(&v, base, id))
+		return res;
+
+	/* handle normal mode */
+	mu_update_control(id, base, opt);
+
+	/* handle input */
+	if (mu_ctx.focus == id) {
+		if (mu_ctx.mouse_down == MU_MOUSE_LEFT)
+			v = low + ((double)(mu_ctx.mouse_pos.x - base.x) / base.w) * (high - low);
+	} else if (mu_ctx.hover == id) {
+		if ((mu_ctx.key_pressed & (MU_KEY_LEFT | MU_KEY_RIGHT)) == MU_KEY_LEFT) {
+			v -= step ? step : 1;
+			if (v < low)
+				v = low;
+		} else if ((mu_ctx.key_pressed & (MU_KEY_LEFT | MU_KEY_RIGHT)) == MU_KEY_RIGHT) {
+			v += step ? step : 1;
+			if (v > high)
+				v = high;
+		}
+	}
+
+	if (step)
+		v = ((long)((v + (v > 0 ? step/2 : (-step/2))) / step)) * step;
+	/* clamp and store value, update res */
+	*value = v = CLAMP(v, low, high);
+	if (last != v)
+		res |= MU_RES_CHANGE;
+
+	/* draw base */
+	mu_draw_control_frame(id, base, MU_COLOR_BASE, opt);
+	/* draw thumb */
+	w = mu_style.thumb_size;
+	normalized = (v - low) / (high - low);
+	thumb = mu_rect(base.x + normalized * (base.w - w), base.y, w, base.h);
+	mu_draw_control_frame(id, thumb, MU_COLOR_BUTTON, opt);
+	/* draw text	*/
+	sprint(buf, fmt, v);
+	mu_draw_control_text(buf, base, MU_COLOR_TEXT, opt);
+
+	return res;
+}
+
+
+int
+mu_slider(double *value, double low, double high)
+{
+	return mu_slider_ex(value, low, high, 0, MU_SLIDER_FMT, MU_OPT_ALIGNCENTER);
+}
+
+
+int
+mu_number_ex(double *value, double step, const char *fmt, int opt)
+{
+	char buf[MU_MAX_FMT + 1];
+	int res = 0;
+	mu_Id id = mu_get_id(&value, sizeof(value));
+	mu_Rect base = mu_layout_next();
+	double last = *value;
+
+	/* handle text input mode */
+	if (number_textbox(value, base, id))
+		return res;
+
+	/* handle normal mode */
+	mu_update_control(id, base, opt);
+
+	/* handle input */
+	if (mu_ctx.focus == id && mu_ctx.mouse_down == MU_MOUSE_LEFT)
+		*value += mu_ctx.mouse_delta.x * step;
+
+	/* set flag if value changed */
+	if (*value != last)
+		res |= MU_RES_CHANGE;
+
+	/* draw base */
+	mu_draw_control_frame(id, base, MU_COLOR_BASE, opt);
+	/* draw text	*/
+	sprint(buf, fmt, *value);
+	mu_draw_control_text(buf, base, MU_COLOR_TEXT, opt);
+
+	return res;
+}
+
+
+int
+mu_number(double *value, double step)
+{
+	return mu_number_ex(value, step, MU_SLIDER_FMT, MU_OPT_ALIGNCENTER);
+}
+
+
+static int
+header(int *state, const char *label, int istreenode)
+{
+	mu_Rect r;
+	mu_Id id;
+	int width = -1;
+	mu_layout_row(1, &width, 0);
+	r = mu_layout_next();
+	id = mu_get_id(&state, sizeof(state));
+	mu_update_control(id, r, 0);
+	/* handle click */
+	if (mu_ctx.mouse_pressed == MU_MOUSE_LEFT && mu_ctx.focus == id)
+		*state = !(*state);
+
+	/* draw */
+	if (istreenode) {
+		if (mu_ctx.hover == id)
+			draw_frame(r, MU_COLOR_BUTTONHOVER);
+	} else {
+		mu_draw_control_frame(id, r, MU_COLOR_BUTTON, 0);
+	}
+	mu_draw_icon(
+		*state ? MU_ICON_EXPANDED : MU_ICON_COLLAPSED,
+		mu_rect(r.x, r.y, r.h, r.h)
+	);
+	r.x += r.h - mu_style.padding;
+	r.w -= r.h - mu_style.padding;
+	mu_draw_control_text(label, r, MU_COLOR_TEXT, 0);
+	return *state ? MU_RES_ACTIVE : 0;
+}
+
+
+int
+mu_header(int *state, const char *label)
+{
+	return header(state, label, 0);
+}
+
+
+int
+mu_begin_treenode(int *state, const char *label)
+{
+	int res = header(state, label, 1);
+	if (res & MU_RES_ACTIVE) {
+		get_layout()->indent += mu_style.indent;
+		mu_push_id(&state, sizeof(void*));
+	}
+	return res;
+}
+
+
+void
+mu_end_treenode(void)
+{
+	get_layout()->indent -= mu_style.indent;
+	mu_pop_id();
+}
+
+static void
+scrollbar(mu_Container *cnt, mu_Rect *b, Point cs, int v)
+{
+	/* only add scrollbar if content size is larger than body */
+	int maxscroll = v ? cs.y - b->h : cs.x - b->w;
+
+	if (maxscroll > 0 && (v ? b->h : b->x) > 0) {
+		mu_Rect base, thumb;
+		mu_Id id = mu_get_id(v ? "!scrollbary" : "!scrollbarx", 11);
+
+		/* get sizing / positioning */
+		base = *b;
+		if (v) {
+			base.x = b->x + b->w;
+			base.w = mu_style.scrollbar_size;
+		} else {
+			base.y = b->y + b->h;
+			base.h = mu_style.scrollbar_size;
+		}
+
+		/* handle input */
+		mu_update_control(id, base, 0);
+		if (mu_ctx.focus == id && mu_ctx.mouse_down == MU_MOUSE_LEFT) {
+			if (v)
+				cnt->scroll.y += mu_ctx.mouse_delta.y * cs.y / base.h;
+			else
+				cnt->scroll.x += mu_ctx.mouse_delta.x * cs.x / base.w;
+		}
+		/* clamp scroll to limits */
+		if (v)
+			cnt->scroll.y = CLAMP(cnt->scroll.y, 0, maxscroll);
+		else
+			cnt->scroll.x = CLAMP(cnt->scroll.x, 0, maxscroll);
+
+		/* draw base and thumb */
+		draw_frame(base, MU_COLOR_SCROLLBASE);
+		thumb = base;
+		if (v) {
+			thumb.h = MAX(mu_style.thumb_size, base.h * b->h / cs.y);
+			thumb.y += cnt->scroll.y * (base.h - thumb.h) / maxscroll;
+		} else {
+			thumb.w = MAX(mu_style.thumb_size, base.w * b->w / cs.x);
+			thumb.x += cnt->scroll.x * (base.w - thumb.w) / maxscroll;
+		}
+		draw_frame(thumb, MU_COLOR_SCROLLTHUMB);
+
+		/* set this as the scroll_target (will get scrolled on mousewheel) */
+		/* if the mouse is over it */
+		if (mu_mouse_over(*b))
+			mu_ctx.scroll_target = cnt;
+	} else if (v) {
+		cnt->scroll.y = 0;
+	} else {
+		cnt->scroll.x = 0;
+	}
+}
+
+static void
+scrollbars(mu_Container *cnt, mu_Rect *body)
+{
+	int sz = mu_style.scrollbar_size;
+	Point cs = cnt->content_size;
+	cs.x += mu_style.padding * 2;
+	cs.y += mu_style.padding * 2;
+	mu_push_clip_rect(*body);
+	/* resize body to make room for scrollbars */
+	if (cs.y > cnt->body.h)
+		body->w -= sz;
+	if (cs.x > cnt->body.w)
+		body->h -= sz;
+	/* to create a horizontal or vertical scrollbar almost-identical code is
+	** used; only the references to `x|y` `w|h` need to be switched */
+	scrollbar(cnt, body, cs, 1);
+	scrollbar(cnt, body, cs, 0);
+	mu_pop_clip_rect();
+}
+
+static void
+push_container_body(mu_Container *cnt, mu_Rect body, int opt)
+{
+	if (~opt & MU_OPT_NOSCROLL)
+		scrollbars(cnt, &body);
+	push_layout(expand_rect(body, -mu_style.padding), cnt->scroll);
+	cnt->body = body;
+}
+
+static void
+begin_root_container(mu_Container *cnt)
+{
+	push_container(cnt);
+
+	/* push container to roots list and push head command */
+	buffer_grow(&mu_ctx.root, sizeof(*mu_ctx.root), &mu_ctx.rootmax, mu_ctx.rootnum);
+	mu_ctx.root[mu_ctx.rootnum++] = cnt;
+	cnt->head = push_jump(-1);
+
+	/* set as hover root if the mouse is overlapping this container and it has a
+	** higher zindex than the current hover root */
+	if (rect_overlaps_vec2(cnt->rect, mu_ctx.mouse_pos) && (!mu_ctx.hover_root || cnt->zindex > mu_ctx.hover_root->zindex))
+		mu_ctx.hover_root = cnt;
+
+	/* clipping is reset here in case a root-container is made within
+	** another root-containers's begin/end block; this prevents the inner
+	** root-container being clipped to the outer */
+	buffer_grow(&mu_ctx.clip, sizeof(*mu_ctx.clip), &mu_ctx.clipmax, mu_ctx.clipnum);
+	mu_ctx.clip[mu_ctx.clipnum++] = unclipped_rect;
+}
+
+
+static void
+end_root_container(void)
+{
+	/* push tail 'goto' jump command and set head 'skip' command. the final steps
+	** on initing these are done in mu_end() */
+	mu_Container *cnt = mu_get_container();
+	cnt->tail = push_jump(-1);
+	mu_ctx.cmds[cnt->head].jump.dst = mu_ctx.cmdsnum;
+	/* pop base clip rect and container */
+	mu_pop_clip_rect();
+	pop_container();
+}
+
+
+int
+mu_begin_window_ex(mu_Container *cnt, const char *title, int opt)
+{
+	mu_Rect rect, body, titlerect;
+
+	if (!cnt->inited)
+		mu_init_window(cnt, opt);
+	if (!cnt->open)
+		return 0;
+
+	begin_root_container(cnt);
+	rect = cnt->rect;
+	body = rect;
+
+	/* draw frame */
+	if (~opt & MU_OPT_NOFRAME)
+		draw_frame(rect, MU_COLOR_WINDOWBG);
+
+	/* moving all windows by "dragging" background */
+	if (mu_ctx.last_hover_root == nil && mu_ctx.hover_root == nil && mu_ctx.mouse_pressed == MU_MOUSE_LEFT)
+		mu_ctx.moving = 1;
+	else if (mu_ctx.mouse_down != MU_MOUSE_LEFT)
+		mu_ctx.moving = 0;
+	if (mu_ctx.moving) {
+		cnt->rect.x += mu_ctx.mouse_delta.x;
+		cnt->rect.y += mu_ctx.mouse_delta.y;
+	}
+
+	/* do title bar */
+	titlerect = rect;
+	titlerect.h = mu_style.title_height;
+	if (~opt & MU_OPT_NOTITLE) {
+		mu_Id id = mu_get_id("!title", 6);
+
+		draw_frame(titlerect, MU_COLOR_TITLEBG);
+
+		/* do title text */
+		mu_update_control(id, titlerect, opt);
+		mu_draw_control_text(title, titlerect, MU_COLOR_TITLETEXT, opt);
+		if (id == mu_ctx.focus && mu_ctx.mouse_down == MU_MOUSE_LEFT) {
+			cnt->rect.x += mu_ctx.mouse_delta.x;
+			cnt->rect.y += mu_ctx.mouse_delta.y;
+		}
+		body.y += titlerect.h;
+		body.h -= titlerect.h;
+
+		/* do `close` button */
+		if (~opt & MU_OPT_NOCLOSE) {
+			mu_Id id = mu_get_id("!close", 6);
+			mu_Rect r = mu_rect(
+				titlerect.x + titlerect.w - titlerect.h,
+				titlerect.y, titlerect.h, titlerect.h
+			);
+			titlerect.w -= r.w;
+			mu_draw_icon(MU_ICON_CLOSE, r);
+			mu_update_control(id, r, opt);
+			if (mu_ctx.mouse_pressed == MU_MOUSE_LEFT && id == mu_ctx.focus)
+				cnt->open = 0;
+		}
+	}
+
+	push_container_body(cnt, body, opt);
+
+	/* do `resize` handle */
+	if (~opt & MU_OPT_NORESIZE) {
+		int sz = mu_style.scrollbar_size;
+		mu_Id id = mu_get_id("!resize", 7);
+		mu_Rect r = mu_rect(rect.x + rect.w - sz, rect.y + rect.h - sz, sz, sz);
+		mu_update_control(id, r, opt);
+		mu_draw_icon(MU_ICON_RESIZE, r);
+		if (id == mu_ctx.focus && mu_ctx.mouse_down == MU_MOUSE_LEFT) {
+			cnt->rect.w = MAX(96, cnt->rect.w + mu_ctx.mouse_delta.x);
+			cnt->rect.h = MAX(64, cnt->rect.h + mu_ctx.mouse_delta.y);
+		}
+	}
+
+	/* resize to content size */
+	if (opt & MU_OPT_AUTOSIZE) {
+		mu_Rect r = get_layout()->body;
+		if (opt & MU_OPT_AUTOSIZE_W)
+			cnt->rect.w = cnt->content_size.x + (cnt->rect.w - r.w);
+		if (opt & MU_OPT_AUTOSIZE_H)
+			cnt->rect.h = cnt->content_size.y + (cnt->rect.h - r.h);
+	}
+
+	/* close if this is a popup window and elsewhere was clicked */
+	if (opt & MU_OPT_POPUP && mu_ctx.mouse_pressed && mu_ctx.last_hover_root != cnt)
+		cnt->open = 0;
+
+	mu_push_clip_rect(cnt->body);
+	return MU_RES_ACTIVE;
+}
+
+
+int
+mu_begin_window(mu_Container *cnt, const char *title)
+{
+	return mu_begin_window_ex(cnt, title, 0);
+}
+
+
+void
+mu_end_window(void)
+{
+	mu_pop_clip_rect();
+	end_root_container();
+}
+
+
+void
+mu_open_popup(mu_Container *cnt)
+{
+	/* set as hover root so popup isn't closed in begin_window_ex() */
+	mu_ctx.last_hover_root = mu_ctx.hover_root = cnt;
+	/* init container if not inited */
+	if (!cnt->inited)
+		mu_init_window(cnt, 0);
+	/* position at mouse cursor, open and bring-to-front */
+	cnt->rect = mu_rect(mu_ctx.mouse_pos.x, mu_ctx.mouse_pos.y, 0, 0);
+	cnt->open = 1;
+	mu_bring_to_front(cnt);
+}
+
+
+int
+mu_begin_popup(mu_Container *cnt)
+{
+	return mu_begin_window_ex(cnt, "", MU_OPT_POPUP | MU_OPT_AUTOSIZE | MU_OPT_NORESIZE | MU_OPT_NOSCROLL | MU_OPT_NOTITLE | MU_OPT_CLOSED);
+}
+
+
+void
+mu_end_popup(void)
+{
+	mu_end_window();
+}
+
+
+void
+mu_begin_panel_ex(mu_Container *cnt, int opt)
+{
+	cnt->rect = mu_layout_next();
+	if (~opt & MU_OPT_NOFRAME)
+		draw_frame(cnt->rect, MU_COLOR_PANELBG);
+
+	push_container(cnt);
+	push_container_body(cnt, cnt->rect, opt);
+	mu_push_clip_rect(cnt->body);
+}
+
+
+void
+mu_begin_panel(mu_Container *cnt)
+{
+	mu_begin_panel_ex(cnt, 0);
+}
+
+
+void
+mu_end_panel(void)
+{
+	mu_pop_clip_rect();
+	pop_container();
+}
+
+int
+mu_render(void)
+{
+	mu_Command *cmd;
+	mu_Rect r, iconr;
+
+	if (memcmp(&mu_ctx.screen, &screen->r, sizeof(mu_ctx.screen)) != 0)
+		mu_ctx.screen = screen->r;
+	else if (mu_ctx.oldcmdsnum == mu_ctx.cmdsnum && memcmp(mu_ctx.oldcmds, mu_ctx.cmds, mu_ctx.cmdsnum*sizeof(mu_Command)) == 0)
+		if (mu_ctx.oldstrnum == mu_ctx.strnum && memcmp(mu_ctx.oldstr, mu_ctx.str, mu_ctx.strnum) == 0)
+			return 0;
+
+	if (mu_ctx.oldcmdsmax != mu_ctx.cmdsmax && (mu_ctx.oldcmds = realloc(mu_ctx.oldcmds, mu_ctx.cmdsmax*sizeof(mu_Command))) == nil)
+		sysfatal("couldn't allocate memory for old cmds");
+	mu_ctx.oldcmdsmax = mu_ctx.cmdsmax;
+	mu_ctx.oldcmdsnum = mu_ctx.cmdsnum;
+	memmove(mu_ctx.oldcmds, mu_ctx.cmds, mu_ctx.cmdsnum*sizeof(mu_Command));
+
+	if (mu_ctx.oldstrmax != mu_ctx.strmax && (mu_ctx.oldstr = realloc(mu_ctx.oldstr, mu_ctx.strmax)) == nil)
+		sysfatal("couldn't allocate memory for old strings");
+	mu_ctx.oldstrmax = mu_ctx.strmax;
+	mu_ctx.oldstrnum = mu_ctx.strnum;
+	memmove(mu_ctx.oldstr, mu_ctx.str, mu_ctx.strnum);
+
+	draw(screen, screen->r, mu_style.colors[MU_COLOR_BG], nil, ZP);
+
+	for (cmd = mu_ctx.cmds; cmd < mu_ctx.cmds + mu_ctx.cmdsnum;) {
+		switch (cmd->type) {
+		case MU_COMMAND_TEXT:
+			if (cmd->text.color != nil)
+				string(screen, addpt(screen->r.min, cmd->text.pos), cmd->text.color, ZP, mu_style.font, mu_ctx.str+cmd->text.s);
+			break;
+
+		case MU_COMMAND_RECT:
+			if (cmd->rect.color != nil)
+				draw(screen, screenrect(cmd->rect.rect), cmd->rect.color, nil, ZP);
+			break;
+
+		case MU_COMMAND_ICON:
+			r = cmd->icon.rect;
+			iconr = atlasicons[cmd->icon.id];
+			r.x += (r.w - iconr.w) / 2;
+			r.y += (r.h - iconr.h) / 2;
+			r.w = iconr.w;
+			r.h = iconr.h;
+			draw(screen, screenrect(r), atlasimage, nil, Pt(iconr.x, iconr.y));
+			break;
+
+		case MU_COMMAND_CLIP:
+			replclipr(screen, 0, screenrect(cmd->clip.rect));
+			break;
+
+		case MU_COMMAND_JUMP:
+			if (cmd->jump.dst < 0)
+				return 1;
+			cmd = &mu_ctx.cmds[cmd->jump.dst];
+			continue;
+		}
+
+		cmd++;
+	}
+
+	return 1;
+}
--- /dev/null
+++ b/microui.h
@@ -1,0 +1,322 @@
+#pragma lib "libmicroui.a"
+
+typedef unsigned mu_Id;
+
+typedef struct mu_Command mu_Command;
+typedef struct mu_Container mu_Container;
+typedef struct mu_Context mu_Context;
+typedef struct mu_Layout mu_Layout;
+typedef struct mu_Style mu_Style;
+typedef struct mu_Rect mu_Rect;
+
+enum
+{
+	MU_MAX_WIDTHS	= 16,
+	MU_MAX_FMT		= 127,
+};
+
+enum
+{
+	MU_CLIP_NONE,
+	MU_CLIP_PART,
+	MU_CLIP_ALL,
+};
+
+enum
+{
+	MU_COMMAND_JUMP,
+	MU_COMMAND_CLIP,
+	MU_COMMAND_RECT,
+	MU_COMMAND_TEXT,
+	MU_COMMAND_ICON,
+	MU_COMMAND_MAX,
+};
+
+enum
+{
+	MU_COLOR_BG,
+	MU_COLOR_TEXT,
+	MU_COLOR_BORDER,
+	MU_COLOR_WINDOWBG,
+	MU_COLOR_TITLEBG,
+	MU_COLOR_TITLETEXT,
+	MU_COLOR_PANELBG,
+	MU_COLOR_BUTTON,
+	MU_COLOR_BUTTONHOVER,
+	MU_COLOR_BUTTONFOCUS,
+	MU_COLOR_BASE,
+	MU_COLOR_BASEHOVER,
+	MU_COLOR_BASEFOCUS,
+	MU_COLOR_SCROLLBASE,
+	MU_COLOR_SCROLLTHUMB,
+	MU_COLOR_MAX,
+};
+
+enum
+{
+	MU_ICON_CHECK,
+	MU_ICON_CLOSE,
+	MU_ICON_COLLAPSED,
+	MU_ICON_EXPANDED,
+	MU_ICON_RESIZE,
+	ATLAS_DIMENSIONS,
+};
+
+enum
+{
+	MU_RES_ACTIVE	= 1<<0,
+	MU_RES_SUBMIT	= 1<<1,
+	MU_RES_CHANGE	= 1<<2
+};
+
+enum
+{
+	MU_OPT_ALIGNCENTER	= 1<<0,
+	MU_OPT_ALIGNRIGHT	= 1<<1,
+	MU_OPT_NOINTERACT	= 1<<2,
+	MU_OPT_NOFRAME		= 1<<3,
+	MU_OPT_NORESIZE		= 1<<4,
+	MU_OPT_NOSCROLL		= 1<<5,
+	MU_OPT_NOCLOSE		= 1<<6,
+	MU_OPT_NOTITLE		= 1<<7,
+	MU_OPT_HOLDFOCUS	= 1<<8,
+	MU_OPT_AUTOSIZE_W	= 1<<9,
+	MU_OPT_AUTOSIZE_H	= 1<<10,
+	MU_OPT_AUTOSIZE		= (MU_OPT_AUTOSIZE_W|MU_OPT_AUTOSIZE_H),
+	MU_OPT_POPUP		= 1<<10,
+	MU_OPT_CLOSED		= 1<<11,
+};
+
+enum
+{
+	MU_MOUSE_LEFT	= 1<<0,
+	MU_MOUSE_RIGHT	= 1<<1,
+	MU_MOUSE_MIDDLE	= 1<<2,
+};
+
+enum
+{
+	MU_KEY_SHIFT		= 1<<0,
+	MU_KEY_CTRL			= 1<<1,
+	MU_KEY_ALT			= 1<<2,
+	MU_KEY_BACKSPACE	= 1<<3,
+	MU_KEY_RETURN		= 1<<4,
+	MU_KEY_NACK			= 1<<5,
+	MU_KEY_LEFT			= 1<<6,
+	MU_KEY_RIGHT		= 1<<7,
+};
+
+struct mu_Rect
+{
+	int x;
+	int y;
+	int w;
+	int h;
+};
+
+struct mu_Command
+{
+	int type;
+
+	union
+	{
+		struct
+		{
+			int dst;
+		}jump;
+
+		struct
+		{
+			mu_Rect rect;
+		}clip;
+
+		struct
+		{
+			Image *color;
+			mu_Rect rect;
+		}rect;
+
+		struct
+		{
+			Font *font;
+			Image *color;
+			int s;
+			Point pos;
+		}text;
+
+		struct
+		{
+			mu_Rect rect;
+			int id;
+		}icon;
+	};
+};
+
+struct mu_Layout
+{
+	mu_Rect body;
+	mu_Rect next;
+	Point position;
+	Point size;
+	Point max;
+	int widths[MU_MAX_WIDTHS];
+	int items;
+	int row_index;
+	int next_row;
+	int next_type;
+	int indent;
+};
+
+struct mu_Container
+{
+	int head, tail;
+	mu_Rect rect;
+	mu_Rect body;
+	Point content_size;
+	Point scroll;
+	int inited;
+	int zindex;
+	int open;
+};
+
+struct mu_Style
+{
+	Font *font;
+	Point size;
+	int padding;
+	int spacing;
+	int indent;
+	int title_height;
+	int scrollbar_size;
+	int thumb_size;
+	Image *colors[MU_COLOR_MAX];
+};
+
+struct mu_Context
+{
+	/* core state */
+	mu_Id hover;
+	mu_Id focus;
+	mu_Id last_id;
+	mu_Rect last_rect;
+	int last_zindex;
+	int updated_focus;
+	mu_Container *hover_root;
+	mu_Container *last_hover_root;
+	mu_Container *scroll_target;
+	char number_buf[MU_MAX_FMT];
+	mu_Id number_editing;
+	Rectangle screen;
+	int moving;
+
+	/* buffers */
+	char *str, *oldstr;
+	int strmax, strnum, oldstrmax, oldstrnum;
+
+	mu_Container **root;
+	int rootmax, rootnum;
+
+	mu_Container **cnt;
+	int cntmax, cntnum;
+
+	mu_Rect *clip;
+	int clipmax, clipnum;
+
+	mu_Id *ids;
+	int idsmax, idsnum;
+
+	mu_Layout *layouts;
+	int layoutsmax, layoutsnum;
+
+	mu_Command *cmds, *oldcmds;
+	int cmdsmax, cmdsnum, oldcmdsmax, oldcmdsnum;
+
+	/* input state */
+	Point mouse_pos;
+	Point last_mouse_pos;
+	Point mouse_delta;
+	Point scroll_delta;
+	int mouse_down;
+	int mouse_pressed;
+	int key_down;
+	int key_pressed;
+	char text_input[64];
+};
+
+extern Image *atlasimage;
+extern mu_Rect *atlasicons;
+extern u8int defaultcolors[MU_COLOR_MAX][4];
+extern mu_Context mu_ctx;
+extern mu_Style mu_style;
+
+mu_Rect mu_rect(int x, int y, int w, int h);
+Image *mu_color(u8int r, u8int g, u8int b, u8int a);
+
+void mu_init(void);
+int mu_render(void);
+void mu_begin(void);
+void mu_end(void);
+void mu_set_focus(mu_Id id);
+mu_Id mu_get_id(const void *data, int size);
+void mu_push_id(const void *data, int size);
+void mu_pop_id(void);
+void mu_push_clip_rect(mu_Rect rect);
+void mu_pop_clip_rect(void);
+mu_Rect mu_get_clip_rect(void);
+int mu_check_clip(mu_Rect r);
+mu_Container *mu_get_container(void);
+void mu_init_window(mu_Container *cnt, int opt);
+void mu_bring_to_front(mu_Container *cnt);
+
+void mu_input_mousemove(int x, int y);
+void mu_input_mousedown(int x, int y, int btn);
+void mu_input_mouseup(int x, int y, int btn);
+void mu_input_scroll(int x, int y);
+void mu_input_keydown(int key);
+void mu_input_keyup(int key);
+void mu_input_text(const char *text);
+
+mu_Command *mu_push_command(int type);
+void mu_set_clip(mu_Rect rect);
+void mu_draw_rect(mu_Rect rect, Image *color);
+void mu_draw_box(mu_Rect rect, Image *color);
+void mu_draw_text(Font *font, const char *str, int len, Point pos, Image *color);
+void mu_draw_icon(int id, mu_Rect rect);
+
+void mu_layout_row(int items, const int *widths, int height);
+void mu_layout_width(int width);
+void mu_layout_height(int height);
+void mu_layout_begin_column(void);
+void mu_layout_end_column(void);
+void mu_layout_set_next(mu_Rect r, int relative);
+mu_Rect mu_layout_next(void);
+
+void mu_draw_control_frame(mu_Id id, mu_Rect rect, int colorid, int opt);
+void mu_draw_control_text(const char *str, mu_Rect rect, int colorid, int opt);
+int mu_mouse_over(mu_Rect rect);
+void mu_update_control(mu_Id id, mu_Rect rect, int opt);
+
+void mu_text(const char *text);
+void mu_label(const char *text);
+int mu_button_ex(const char *label, int icon, int opt);
+int mu_button(const char *label);
+int mu_checkbox(int *state, const char *label);
+int mu_textbox_raw(char *buf, int bufsz, mu_Id id, mu_Rect r, int opt);
+int mu_textbox_ex(char *buf, int bufsz, int opt);
+int mu_textbox(char *buf, int bufsz);
+int mu_slider_ex(double *value, double low, double high, double step, const char *fmt, int opt);
+int mu_slider(double *value, double low, double high);
+int mu_number_ex(double *value, double step, const char *fmt, int opt);
+int mu_number(double *value, double step);
+int mu_header(int *state, const char *label);
+int mu_begin_treenode(int *state, const char *label);
+void mu_end_treenode(void);
+int mu_begin_window_ex(mu_Container *cnt, const char *title, int opt);
+int mu_begin_window(mu_Container *cnt, const char *title);
+void mu_end_window(void);
+void mu_open_popup(mu_Container *cnt);
+int mu_begin_popup(mu_Container *cnt);
+void mu_end_popup(void);
+void mu_begin_panel_ex(mu_Container *cnt, int opt);
+void mu_begin_panel(mu_Container *cnt);
+void mu_end_panel(void);
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,13 @@
+</$objtype/mkfile
+LIB=/$objtype/lib/libmicroui.a
+
+OFILES=\
+	microui.$O\
+
+HFILES=\
+	/sys/include/microui.h\
+
+/sys/include/%.h: %.h
+	cp $stem.h /sys/include/$stem.h
+
+</sys/src/cmd/mksyslib