From 70e4841d311d68689724768157cc9cbfbde7a9fc Mon Sep 17 00:00:00 2001
From: StapleButter <thetotalworm@gmail.com>
Date: Sat, 9 Sep 2017 02:30:51 +0200
Subject: another UI attempt, I guess. sorry.

---
 src/libui_sdl/libui/darwin/CMakeLists.txt   |  79 +++
 src/libui_sdl/libui/darwin/alloc.m          |  89 ++++
 src/libui_sdl/libui/darwin/area.m           | 475 +++++++++++++++++
 src/libui_sdl/libui/darwin/areaevents.m     | 159 ++++++
 src/libui_sdl/libui/darwin/autolayout.m     | 161 ++++++
 src/libui_sdl/libui/darwin/box.m            | 469 ++++++++++++++++
 src/libui_sdl/libui/darwin/button.m         | 113 ++++
 src/libui_sdl/libui/darwin/checkbox.m       | 129 +++++
 src/libui_sdl/libui/darwin/colorbutton.m    | 159 ++++++
 src/libui_sdl/libui/darwin/combobox.m       | 145 +++++
 src/libui_sdl/libui/darwin/control.m        |  84 +++
 src/libui_sdl/libui/darwin/datetimepicker.m |  42 ++
 src/libui_sdl/libui/darwin/debug.m          |  19 +
 src/libui_sdl/libui/darwin/draw.m           | 454 ++++++++++++++++
 src/libui_sdl/libui/darwin/drawtext.m       | 655 +++++++++++++++++++++++
 src/libui_sdl/libui/darwin/editablecombo.m  | 185 +++++++
 src/libui_sdl/libui/darwin/entry.m          | 251 +++++++++
 src/libui_sdl/libui/darwin/fontbutton.m     | 218 ++++++++
 src/libui_sdl/libui/darwin/form.m           | 561 +++++++++++++++++++
 src/libui_sdl/libui/darwin/grid.m           | 800 ++++++++++++++++++++++++++++
 src/libui_sdl/libui/darwin/group.m          | 194 +++++++
 src/libui_sdl/libui/darwin/image.m          |  82 +++
 src/libui_sdl/libui/darwin/label.m          |  43 ++
 src/libui_sdl/libui/darwin/main.m           | 239 +++++++++
 src/libui_sdl/libui/darwin/map.m            |  59 ++
 src/libui_sdl/libui/darwin/menu.m           | 368 +++++++++++++
 src/libui_sdl/libui/darwin/multilineentry.m | 233 ++++++++
 src/libui_sdl/libui/darwin/progressbar.m    |  78 +++
 src/libui_sdl/libui/darwin/radiobuttons.m   | 207 +++++++
 src/libui_sdl/libui/darwin/scrollview.m     |  61 +++
 src/libui_sdl/libui/darwin/separator.m      |  45 ++
 src/libui_sdl/libui/darwin/slider.m         | 147 +++++
 src/libui_sdl/libui/darwin/spinbox.m        | 214 ++++++++
 src/libui_sdl/libui/darwin/stddialogs.m     | 123 +++++
 src/libui_sdl/libui/darwin/tab.m            | 292 ++++++++++
 src/libui_sdl/libui/darwin/text.m           |  19 +
 src/libui_sdl/libui/darwin/uipriv_darwin.h  | 146 +++++
 src/libui_sdl/libui/darwin/util.m           |  15 +
 src/libui_sdl/libui/darwin/window.m         | 407 ++++++++++++++
 src/libui_sdl/libui/darwin/winmoveresize.m  | 253 +++++++++
 40 files changed, 8472 insertions(+)
 create mode 100644 src/libui_sdl/libui/darwin/CMakeLists.txt
 create mode 100644 src/libui_sdl/libui/darwin/alloc.m
 create mode 100644 src/libui_sdl/libui/darwin/area.m
 create mode 100644 src/libui_sdl/libui/darwin/areaevents.m
 create mode 100644 src/libui_sdl/libui/darwin/autolayout.m
 create mode 100644 src/libui_sdl/libui/darwin/box.m
 create mode 100644 src/libui_sdl/libui/darwin/button.m
 create mode 100644 src/libui_sdl/libui/darwin/checkbox.m
 create mode 100644 src/libui_sdl/libui/darwin/colorbutton.m
 create mode 100644 src/libui_sdl/libui/darwin/combobox.m
 create mode 100644 src/libui_sdl/libui/darwin/control.m
 create mode 100644 src/libui_sdl/libui/darwin/datetimepicker.m
 create mode 100644 src/libui_sdl/libui/darwin/debug.m
 create mode 100644 src/libui_sdl/libui/darwin/draw.m
 create mode 100644 src/libui_sdl/libui/darwin/drawtext.m
 create mode 100644 src/libui_sdl/libui/darwin/editablecombo.m
 create mode 100644 src/libui_sdl/libui/darwin/entry.m
 create mode 100644 src/libui_sdl/libui/darwin/fontbutton.m
 create mode 100644 src/libui_sdl/libui/darwin/form.m
 create mode 100644 src/libui_sdl/libui/darwin/grid.m
 create mode 100644 src/libui_sdl/libui/darwin/group.m
 create mode 100644 src/libui_sdl/libui/darwin/image.m
 create mode 100644 src/libui_sdl/libui/darwin/label.m
 create mode 100644 src/libui_sdl/libui/darwin/main.m
 create mode 100644 src/libui_sdl/libui/darwin/map.m
 create mode 100644 src/libui_sdl/libui/darwin/menu.m
 create mode 100644 src/libui_sdl/libui/darwin/multilineentry.m
 create mode 100644 src/libui_sdl/libui/darwin/progressbar.m
 create mode 100644 src/libui_sdl/libui/darwin/radiobuttons.m
 create mode 100644 src/libui_sdl/libui/darwin/scrollview.m
 create mode 100644 src/libui_sdl/libui/darwin/separator.m
 create mode 100644 src/libui_sdl/libui/darwin/slider.m
 create mode 100644 src/libui_sdl/libui/darwin/spinbox.m
 create mode 100644 src/libui_sdl/libui/darwin/stddialogs.m
 create mode 100644 src/libui_sdl/libui/darwin/tab.m
 create mode 100644 src/libui_sdl/libui/darwin/text.m
 create mode 100644 src/libui_sdl/libui/darwin/uipriv_darwin.h
 create mode 100644 src/libui_sdl/libui/darwin/util.m
 create mode 100644 src/libui_sdl/libui/darwin/window.m
 create mode 100644 src/libui_sdl/libui/darwin/winmoveresize.m

(limited to 'src/libui_sdl/libui/darwin')

diff --git a/src/libui_sdl/libui/darwin/CMakeLists.txt b/src/libui_sdl/libui/darwin/CMakeLists.txt
new file mode 100644
index 0000000..dbef5d4
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/CMakeLists.txt
@@ -0,0 +1,79 @@
+# 3 june 2016
+
+list(APPEND _LIBUI_SOURCES
+	darwin/alloc.m
+	darwin/area.m
+	darwin/areaevents.m
+	darwin/autolayout.m
+	darwin/box.m
+	darwin/button.m
+	darwin/checkbox.m
+	darwin/colorbutton.m
+	darwin/combobox.m
+	darwin/control.m
+	darwin/datetimepicker.m
+	darwin/debug.m
+	darwin/draw.m
+	darwin/drawtext.m
+	darwin/editablecombo.m
+	darwin/entry.m
+	darwin/fontbutton.m
+	darwin/form.m
+	darwin/grid.m
+	darwin/group.m
+	darwin/image.m
+	darwin/label.m
+	darwin/main.m
+	darwin/map.m
+	darwin/menu.m
+	darwin/multilineentry.m
+	darwin/progressbar.m
+	darwin/radiobuttons.m
+	darwin/scrollview.m
+	darwin/separator.m
+	darwin/slider.m
+	darwin/spinbox.m
+	darwin/stddialogs.m
+	darwin/tab.m
+	darwin/text.m
+	darwin/util.m
+	darwin/window.m
+	darwin/winmoveresize.m
+)
+set(_LIBUI_SOURCES ${_LIBUI_SOURCES} PARENT_SCOPE)
+
+list(APPEND _LIBUI_INCLUDEDIRS
+	darwin
+)
+set(_LIBUI_INCLUDEDIRS _LIBUI_INCLUDEDIRS PARENT_SCOPE)
+
+set(_LIBUINAME libui PARENT_SCOPE)
+if(NOT BUILD_SHARED_LIBS)
+	set(_LIBUINAME libui-temporary PARENT_SCOPE)
+endif()
+# thanks to Mr-Hide in irc.freenode.net/#cmake
+macro(_handle_static)
+	set_target_properties(${_LIBUINAME} PROPERTIES
+		ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
+	set(_aname $<TARGET_FILE:${_LIBUINAME}>)
+	set(_lname libui-combined.list)
+	set(_oname libui-combined.o)
+	add_custom_command(
+		OUTPUT ${_oname}
+		COMMAND
+			nm -m ${_aname} | sed -E -n "'s/^[0-9a-f]* \\([A-Z_]+,[a-z_]+\\) external //p'" > ${_lname}
+		COMMAND
+			ld -exported_symbols_list ${_lname} -r -all_load ${_aname} -o ${_oname}
+		COMMENT "Removing hidden symbols")
+	add_library(libui STATIC ${_oname})
+	# otherwise cmake won't know which linker to use
+	set_target_properties(libui PROPERTIES
+		LINKER_LANGUAGE C)
+	set(_aname)
+	set(_lname)
+	set(_oname)
+endmacro()
+
+set(_LIBUI_LIBS
+	objc "-framework Foundation" "-framework AppKit"
+PARENT_SCOPE)
diff --git a/src/libui_sdl/libui/darwin/alloc.m b/src/libui_sdl/libui/darwin/alloc.m
new file mode 100644
index 0000000..e271b90
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/alloc.m
@@ -0,0 +1,89 @@
+// 4 december 2014
+#import <stdlib.h>
+#import "uipriv_darwin.h"
+
+static NSMutableArray *allocations;
+NSMutableArray *delegates;
+
+void initAlloc(void)
+{
+	allocations = [NSMutableArray new];
+	delegates = [NSMutableArray new];
+}
+
+#define UINT8(p) ((uint8_t *) (p))
+#define PVOID(p) ((void *) (p))
+#define EXTRA (sizeof (size_t) + sizeof (const char **))
+#define DATA(p) PVOID(UINT8(p) + EXTRA)
+#define BASE(p) PVOID(UINT8(p) - EXTRA)
+#define SIZE(p) ((size_t *) (p))
+#define CCHAR(p) ((const char **) (p))
+#define TYPE(p) CCHAR(UINT8(p) + sizeof (size_t))
+
+void uninitAlloc(void)
+{
+	NSMutableString *str;
+	NSValue *v;
+
+	[delegates release];
+	if ([allocations count] == 0) {
+		[allocations release];
+		return;
+	}
+	str = [NSMutableString new];
+	for (v in allocations) {
+		void *ptr;
+
+		ptr = [v pointerValue];
+		[str appendString:[NSString stringWithFormat:@"%p %s\n", ptr, *TYPE(ptr)]];
+	}
+	userbug("Some data was leaked; either you left a uiControl lying around or there's a bug in libui itself. Leaked data:\n%s", [str UTF8String]);
+	[str release];
+}
+
+void *uiAlloc(size_t size, const char *type)
+{
+	void *out;
+
+	out = malloc(EXTRA + size);
+	if (out == NULL) {
+		fprintf(stderr, "memory exhausted in uiAlloc()\n");
+		abort();
+	}
+	memset(DATA(out), 0, size);
+	*SIZE(out) = size;
+	*TYPE(out) = type;
+	[allocations addObject:[NSValue valueWithPointer:out]];
+	return DATA(out);
+}
+
+void *uiRealloc(void *p, size_t new, const char *type)
+{
+	void *out;
+	size_t *s;
+
+	if (p == NULL)
+		return uiAlloc(new, type);
+	p = BASE(p);
+	out = realloc(p, EXTRA + new);
+	if (out == NULL) {
+		fprintf(stderr, "memory exhausted in uiRealloc()\n");
+		abort();
+	}
+	s = SIZE(out);
+	if (new <= *s)
+		memset(((uint8_t *) DATA(out)) + *s, 0, new - *s);
+	*s = new;
+	[allocations removeObject:[NSValue valueWithPointer:p]];
+	[allocations addObject:[NSValue valueWithPointer:out]];
+	return DATA(out);
+}
+
+void uiFree(void *p)
+{
+	if (p == NULL)
+		implbug("attempt to uiFree(NULL)");
+	p = BASE(p);
+	free(p);
+	[allocations removeObject:[NSValue valueWithPointer:p]];
+}
diff --git a/src/libui_sdl/libui/darwin/area.m b/src/libui_sdl/libui/darwin/area.m
new file mode 100644
index 0000000..23162e6
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/area.m
@@ -0,0 +1,475 @@
+// 9 september 2015
+#import "uipriv_darwin.h"
+
+// 10.8 fixups
+#define NSEventModifierFlags NSUInteger
+
+@interface areaView : NSView {
+	uiArea *libui_a;
+	NSTrackingArea *libui_ta;
+	NSSize libui_ss;
+	BOOL libui_enabled;
+}
+- (id)initWithFrame:(NSRect)r area:(uiArea *)a;
+- (uiModifiers)parseModifiers:(NSEvent *)e;
+- (void)doMouseEvent:(NSEvent *)e;
+- (int)sendKeyEvent:(uiAreaKeyEvent *)ke;
+- (int)doKeyDownUp:(NSEvent *)e up:(int)up;
+- (int)doKeyDown:(NSEvent *)e;
+- (int)doKeyUp:(NSEvent *)e;
+- (int)doFlagsChanged:(NSEvent *)e;
+- (void)setupNewTrackingArea;
+- (void)setScrollingSize:(NSSize)s;
+- (BOOL)isEnabled;
+- (void)setEnabled:(BOOL)e;
+@end
+
+struct uiArea {
+	uiDarwinControl c;
+	NSView *view;			// either sv or area depending on whether it is scrolling
+	NSScrollView *sv;
+	areaView *area;
+	struct scrollViewData *d;
+	uiAreaHandler *ah;
+	BOOL scrolling;
+	NSEvent *dragevent;
+};
+
+@implementation areaView
+
+- (id)initWithFrame:(NSRect)r area:(uiArea *)a
+{
+	self = [super initWithFrame:r];
+	if (self) {
+		self->libui_a = a;
+		[self setupNewTrackingArea];
+		self->libui_ss = r.size;
+		self->libui_enabled = YES;
+	}
+	return self;
+}
+
+- (void)drawRect:(NSRect)r
+{
+	uiArea *a = self->libui_a;
+	CGContextRef c;
+	uiAreaDrawParams dp;
+
+	c = (CGContextRef) [[NSGraphicsContext currentContext] graphicsPort];
+	// see draw.m under text for why we need the height
+	dp.Context = newContext(c, [self bounds].size.height);
+
+	dp.AreaWidth = 0;
+	dp.AreaHeight = 0;
+	if (!a->scrolling) {
+		dp.AreaWidth = [self frame].size.width;
+		dp.AreaHeight = [self frame].size.height;
+	}
+
+	dp.ClipX = r.origin.x;
+	dp.ClipY = r.origin.y;
+	dp.ClipWidth = r.size.width;
+	dp.ClipHeight = r.size.height;
+
+	// no need to save or restore the graphics state to reset transformations; Cocoa creates a brand-new context each time
+	(*(a->ah->Draw))(a->ah, a, &dp);
+
+	freeContext(dp.Context);
+}
+
+- (BOOL)isFlipped
+{
+	return YES;
+}
+
+- (BOOL)acceptsFirstResponder
+{
+	return YES;
+}
+
+- (uiModifiers)parseModifiers:(NSEvent *)e
+{
+	NSEventModifierFlags mods;
+	uiModifiers m;
+
+	m = 0;
+	mods = [e modifierFlags];
+	if ((mods & NSControlKeyMask) != 0)
+		m |= uiModifierCtrl;
+	if ((mods & NSAlternateKeyMask) != 0)
+		m |= uiModifierAlt;
+	if ((mods & NSShiftKeyMask) != 0)
+		m |= uiModifierShift;
+	if ((mods & NSCommandKeyMask) != 0)
+		m |= uiModifierSuper;
+	return m;
+}
+
+- (void)setupNewTrackingArea
+{
+	self->libui_ta = [[NSTrackingArea alloc] initWithRect:[self bounds]
+		options:(NSTrackingMouseEnteredAndExited |
+			NSTrackingMouseMoved |
+			NSTrackingActiveAlways |
+			NSTrackingInVisibleRect |
+			NSTrackingEnabledDuringMouseDrag)
+		owner:self
+		userInfo:nil];
+	[self addTrackingArea:self->libui_ta];
+}
+
+- (void)updateTrackingAreas
+{
+	[self removeTrackingArea:self->libui_ta];
+	[self->libui_ta release];
+	[self setupNewTrackingArea];
+}
+
+// capture on drag is done automatically on OS X
+- (void)doMouseEvent:(NSEvent *)e
+{
+	uiArea *a = self->libui_a;
+	uiAreaMouseEvent me;
+	NSPoint point;
+	int buttonNumber;
+	NSUInteger pmb;
+	unsigned int i, max;
+
+	// this will convert point to drawing space
+	// thanks swillits in irc.freenode.net/#macdev
+	point = [self convertPoint:[e locationInWindow] fromView:nil];
+	me.X = point.x;
+	me.Y = point.y;
+
+	me.AreaWidth = 0;
+	me.AreaHeight = 0;
+	if (!a->scrolling) {
+		me.AreaWidth = [self frame].size.width;
+		me.AreaHeight = [self frame].size.height;
+	}
+
+	buttonNumber = [e buttonNumber] + 1;
+	// swap button numbers 2 and 3 (right and middle)
+	if (buttonNumber == 2)
+		buttonNumber = 3;
+	else if (buttonNumber == 3)
+		buttonNumber = 2;
+
+	me.Down = 0;
+	me.Up = 0;
+	me.Count = 0;
+	switch ([e type]) {
+	case NSLeftMouseDown:
+	case NSRightMouseDown:
+	case NSOtherMouseDown:
+		me.Down = buttonNumber;
+		me.Count = [e clickCount];
+		break;
+	case NSLeftMouseUp:
+	case NSRightMouseUp:
+	case NSOtherMouseUp:
+		me.Up = buttonNumber;
+		break;
+	case NSLeftMouseDragged:
+	case NSRightMouseDragged:
+	case NSOtherMouseDragged:
+		// we include the button that triggered the dragged event in the Held fields
+		buttonNumber = 0;
+		break;
+	}
+
+	me.Modifiers = [self parseModifiers:e];
+
+	pmb = [NSEvent pressedMouseButtons];
+	me.Held1To64 = 0;
+	if (buttonNumber != 1 && (pmb & 1) != 0)
+		me.Held1To64 |= 1;
+	if (buttonNumber != 2 && (pmb & 4) != 0)
+		me.Held1To64 |= 2;
+	if (buttonNumber != 3 && (pmb & 2) != 0)
+		me.Held1To64 |= 4;
+	// buttons 4..32
+	// https://developer.apple.com/library/mac/documentation/Carbon/Reference/QuartzEventServicesRef/index.html#//apple_ref/c/tdef/CGMouseButton says Quartz only supports up to 32 buttons
+	max = 32;
+	for (i = 4; i <= max; i++) {
+		uint64_t j;
+
+		if (buttonNumber == i)
+			continue;
+		j = 1 << (i - 1);
+		if ((pmb & j) != 0)
+			me.Held1To64 |= j;
+	}
+
+	if (self->libui_enabled) {
+		// and allow dragging here
+		a->dragevent = e;
+		(*(a->ah->MouseEvent))(a->ah, a, &me);
+		a->dragevent = nil;
+	}
+}
+
+#define mouseEvent(name) \
+	- (void)name:(NSEvent *)e \
+	{ \
+		[self doMouseEvent:e]; \
+	}
+mouseEvent(mouseMoved)
+mouseEvent(mouseDragged)
+mouseEvent(rightMouseDragged)
+mouseEvent(otherMouseDragged)
+mouseEvent(mouseDown)
+mouseEvent(rightMouseDown)
+mouseEvent(otherMouseDown)
+mouseEvent(mouseUp)
+mouseEvent(rightMouseUp)
+mouseEvent(otherMouseUp)
+
+- (void)mouseEntered:(NSEvent *)e
+{
+	uiArea *a = self->libui_a;
+
+	if (self->libui_enabled)
+		(*(a->ah->MouseCrossed))(a->ah, a, 0);
+}
+
+- (void)mouseExited:(NSEvent *)e
+{
+	uiArea *a = self->libui_a;
+
+	if (self->libui_enabled)
+		(*(a->ah->MouseCrossed))(a->ah, a, 1);
+}
+
+// note: there is no equivalent to WM_CAPTURECHANGED on Mac OS X; there literally is no way to break a grab like that
+// even if I invoke the task switcher and switch processes, the mouse grab will still be held until I let go of all buttons
+// therefore, no DragBroken()
+
+- (int)sendKeyEvent:(uiAreaKeyEvent *)ke
+{
+	uiArea *a = self->libui_a;
+
+	return (*(a->ah->KeyEvent))(a->ah, a, ke);
+}
+
+- (int)doKeyDownUp:(NSEvent *)e up:(int)up
+{
+	uiAreaKeyEvent ke;
+
+	ke.Key = 0;
+	ke.ExtKey = 0;
+	ke.Modifier = 0;
+
+	ke.Modifiers = [self parseModifiers:e];
+
+	ke.Up = up;
+
+	if (!fromKeycode([e keyCode], &ke))
+		return 0;
+	return [self sendKeyEvent:&ke];
+}
+
+- (int)doKeyDown:(NSEvent *)e
+{
+	return [self doKeyDownUp:e up:0];
+}
+
+- (int)doKeyUp:(NSEvent *)e
+{
+	return [self doKeyDownUp:e up:1];
+}
+
+- (int)doFlagsChanged:(NSEvent *)e
+{
+	uiAreaKeyEvent ke;
+	uiModifiers whichmod;
+
+	ke.Key = 0;
+	ke.ExtKey = 0;
+
+	// Mac OS X sends this event on both key up and key down.
+	// Fortunately -[e keyCode] IS valid here, so we can simply map from key code to Modifiers, get the value of [e modifierFlags], and check if the respective bit is set or not — that will give us the up/down state
+	if (!keycodeModifier([e keyCode], &whichmod))
+		return 0;
+	ke.Modifier = whichmod;
+	ke.Modifiers = [self parseModifiers:e];
+	ke.Up = (ke.Modifiers & ke.Modifier) == 0;
+	// and then drop the current modifier from Modifiers
+	ke.Modifiers &= ~ke.Modifier;
+	return [self sendKeyEvent:&ke];
+}
+
+- (void)setFrameSize:(NSSize)size
+{
+	uiArea *a = self->libui_a;
+
+	[super setFrameSize:size];
+	if (!a->scrolling)
+		// we must redraw everything on resize because Windows requires it
+		[self setNeedsDisplay:YES];
+}
+
+// TODO does this update the frame?
+- (void)setScrollingSize:(NSSize)s
+{
+	self->libui_ss = s;
+	[self invalidateIntrinsicContentSize];
+}
+
+- (NSSize)intrinsicContentSize
+{
+	if (!self->libui_a->scrolling)
+		return [super intrinsicContentSize];
+	return self->libui_ss;
+}
+
+- (BOOL)becomeFirstResponder
+{
+	return [self isEnabled];
+}
+
+- (BOOL)isEnabled
+{
+	return self->libui_enabled;
+}
+
+- (void)setEnabled:(BOOL)e
+{
+	self->libui_enabled = e;
+	if (!self->libui_enabled && [self window] != nil)
+		if ([[self window] firstResponder] == self)
+			[[self window] makeFirstResponder:nil];
+}
+
+@end
+
+uiDarwinControlAllDefaultsExceptDestroy(uiArea, view)
+
+static void uiAreaDestroy(uiControl *c)
+{
+	uiArea *a = uiArea(c);
+
+	if (a->scrolling)
+		scrollViewFreeData(a->sv, a->d);
+	[a->area release];
+	if (a->scrolling)
+		[a->sv release];
+	uiFreeControl(uiControl(a));
+}
+
+// called by subclasses of -[NSApplication sendEvent:]
+// by default, NSApplication eats some key events
+// this prevents that from happening with uiArea
+// see http://stackoverflow.com/questions/24099063/how-do-i-detect-keyup-in-my-nsview-with-the-command-key-held and http://lists.apple.com/archives/cocoa-dev/2003/Oct/msg00442.html
+int sendAreaEvents(NSEvent *e)
+{
+	NSEventType type;
+	id focused;
+	areaView *view;
+
+	type = [e type];
+	if (type != NSKeyDown && type != NSKeyUp && type != NSFlagsChanged)
+		return 0;
+	focused = [[e window] firstResponder];
+	if (focused == nil)
+		return 0;
+	if (![focused isKindOfClass:[areaView class]])
+		return 0;
+	view = (areaView *) focused;
+	switch (type) {
+	case NSKeyDown:
+		return [view doKeyDown:e];
+	case NSKeyUp:
+		return [view doKeyUp:e];
+	case NSFlagsChanged:
+		return [view doFlagsChanged:e];
+	}
+	return 0;
+}
+
+void uiAreaSetSize(uiArea *a, int width, int height)
+{
+	if (!a->scrolling)
+		userbug("You cannot call uiAreaSetSize() on a non-scrolling uiArea. (area: %p)", a);
+	[a->area setScrollingSize:NSMakeSize(width, height)];
+}
+
+void uiAreaQueueRedrawAll(uiArea *a)
+{
+	[a->area setNeedsDisplay:YES];
+}
+
+void uiAreaScrollTo(uiArea *a, double x, double y, double width, double height)
+{
+	if (!a->scrolling)
+		userbug("You cannot call uiAreaScrollTo() on a non-scrolling uiArea. (area: %p)", a);
+	[a->area scrollRectToVisible:NSMakeRect(x, y, width, height)];
+	// don't worry about the return value; it just says whether scrolling was needed
+}
+
+void uiAreaBeginUserWindowMove(uiArea *a)
+{
+	libuiNSWindow *w;
+
+	w = (libuiNSWindow *) [a->area window];
+	if (w == nil)
+		return;		// TODO
+	if (a->dragevent == nil)
+		return;		// TODO
+	[w libui_doMove:a->dragevent];
+}
+
+void uiAreaBeginUserWindowResize(uiArea *a, uiWindowResizeEdge edge)
+{
+	libuiNSWindow *w;
+
+	w = (libuiNSWindow *) [a->area window];
+	if (w == nil)
+		return;		// TODO
+	if (a->dragevent == nil)
+		return;		// TODO
+	[w libui_doResize:a->dragevent on:edge];
+}
+
+uiArea *uiNewArea(uiAreaHandler *ah)
+{
+	uiArea *a;
+
+	uiDarwinNewControl(uiArea, a);
+
+	a->ah = ah;
+	a->scrolling = NO;
+
+	a->area = [[areaView alloc] initWithFrame:NSZeroRect area:a];
+
+	a->view = a->area;
+
+	return a;
+}
+
+uiArea *uiNewScrollingArea(uiAreaHandler *ah, int width, int height)
+{
+	uiArea *a;
+	struct scrollViewCreateParams p;
+
+	uiDarwinNewControl(uiArea, a);
+
+	a->ah = ah;
+	a->scrolling = YES;
+
+	a->area = [[areaView alloc] initWithFrame:NSMakeRect(0, 0, width, height)
+		area:a];
+
+	memset(&p, 0, sizeof (struct scrollViewCreateParams));
+	p.DocumentView = a->area;
+	p.BackgroundColor = [NSColor controlColor];
+	p.DrawsBackground = 1;
+	p.Bordered = NO;
+	p.HScroll = YES;
+	p.VScroll = YES;
+	a->sv = mkScrollView(&p, &(a->d));
+
+	a->view = a->sv;
+
+	return a;
+}
diff --git a/src/libui_sdl/libui/darwin/areaevents.m b/src/libui_sdl/libui/darwin/areaevents.m
new file mode 100644
index 0000000..d7ceaaa
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/areaevents.m
@@ -0,0 +1,159 @@
+// 30 march 2014
+#import "uipriv_darwin.h"
+
+/*
+Mac OS X uses its own set of hardware key codes that are different from PC keyboard scancodes, but are positional (like PC keyboard scancodes). These are defined in <HIToolbox/Events.h>, a Carbon header. As far as I can tell, there's no way to include this header without either using an absolute path or linking Carbon into the program, so the constant values are used here instead.
+
+The Cocoa docs do guarantee that -[NSEvent keyCode] results in key codes that are the same as those returned by Carbon; that is, these codes.
+*/
+
+// use uintptr_t to be safe
+static const struct {
+	uintptr_t keycode;
+	char equiv;
+} keycodeKeys[] = {
+	{ 0x00, 'a' },
+	{ 0x01, 's' },
+	{ 0x02, 'd' },
+	{ 0x03, 'f' },
+	{ 0x04, 'h' },
+	{ 0x05, 'g' },
+	{ 0x06, 'z' },
+	{ 0x07, 'x' },
+	{ 0x08, 'c' },
+	{ 0x09, 'v' },
+	{ 0x0B, 'b' },
+	{ 0x0C, 'q' },
+	{ 0x0D, 'w' },
+	{ 0x0E, 'e' },
+	{ 0x0F, 'r' },
+	{ 0x10, 'y' },
+	{ 0x11, 't' },
+	{ 0x12, '1' },
+	{ 0x13, '2' },
+	{ 0x14, '3' },
+	{ 0x15, '4' },
+	{ 0x16, '6' },
+	{ 0x17, '5' },
+	{ 0x18, '=' },
+	{ 0x19, '9' },
+	{ 0x1A, '7' },
+	{ 0x1B, '-' },
+	{ 0x1C, '8' },
+	{ 0x1D, '0' },
+	{ 0x1E, ']' },
+	{ 0x1F, 'o' },
+	{ 0x20, 'u' },
+	{ 0x21, '[' },
+	{ 0x22, 'i' },
+	{ 0x23, 'p' },
+	{ 0x25, 'l' },
+	{ 0x26, 'j' },
+	{ 0x27, '\'' },
+	{ 0x28, 'k' },
+	{ 0x29, ';' },
+	{ 0x2A, '\\' },
+	{ 0x2B, ',' },
+	{ 0x2C, '/' },
+	{ 0x2D, 'n' },
+	{ 0x2E, 'm' },
+	{ 0x2F, '.' },
+	{ 0x32, '`' },
+	{ 0x24, '\n' },
+	{ 0x30, '\t' },
+	{ 0x31, ' ' },
+	{ 0x33, '\b' },
+	{ 0xFFFF, 0 },
+};
+
+static const struct {
+	uintptr_t keycode;
+	uiExtKey equiv;
+} keycodeExtKeys[] = {
+	{ 0x41, uiExtKeyNDot },
+	{ 0x43, uiExtKeyNMultiply },
+	{ 0x45, uiExtKeyNAdd },
+	{ 0x4B, uiExtKeyNDivide },
+	{ 0x4C, uiExtKeyNEnter },
+	{ 0x4E, uiExtKeyNSubtract },
+	{ 0x52, uiExtKeyN0 },
+	{ 0x53, uiExtKeyN1 },
+	{ 0x54, uiExtKeyN2 },
+	{ 0x55, uiExtKeyN3 },
+	{ 0x56, uiExtKeyN4 },
+	{ 0x57, uiExtKeyN5 },
+	{ 0x58, uiExtKeyN6 },
+	{ 0x59, uiExtKeyN7 },
+	{ 0x5B, uiExtKeyN8 },
+	{ 0x5C, uiExtKeyN9 },
+	{ 0x35, uiExtKeyEscape },
+	{ 0x60, uiExtKeyF5 },
+	{ 0x61, uiExtKeyF6 },
+	{ 0x62, uiExtKeyF7 },
+	{ 0x63, uiExtKeyF3 },
+	{ 0x64, uiExtKeyF8 },
+	{ 0x65, uiExtKeyF9 },
+	{ 0x67, uiExtKeyF11 },
+	{ 0x6D, uiExtKeyF10 },
+	{ 0x6F, uiExtKeyF12 },
+	{ 0x72, uiExtKeyInsert },		// listed as the Help key but it's in the same position on an Apple keyboard as the Insert key on a Windows keyboard; thanks to SeanieB from irc.badnik.net and Psy in irc.freenode.net/#macdev for confirming they have the same code
+	{ 0x73, uiExtKeyHome },
+	{ 0x74, uiExtKeyPageUp },
+	{ 0x75, uiExtKeyDelete },
+	{ 0x76, uiExtKeyF4 },
+	{ 0x77, uiExtKeyEnd },
+	{ 0x78, uiExtKeyF2 },
+	{ 0x79, uiExtKeyPageDown },
+	{ 0x7A, uiExtKeyF1 },
+	{ 0x7B, uiExtKeyLeft },
+	{ 0x7C, uiExtKeyRight },
+	{ 0x7D, uiExtKeyDown },
+	{ 0x7E, uiExtKeyUp },
+	{ 0xFFFF, 0 },
+};
+
+static const struct {
+	uintptr_t keycode;
+	uiModifiers equiv;
+} keycodeModifiers[] = {
+	{ 0x37, uiModifierSuper },		// left command
+	{ 0x38, uiModifierShift },		// left shift
+	{ 0x3A, uiModifierAlt },		// left option
+	{ 0x3B, uiModifierCtrl },		// left control
+	{ 0x3C, uiModifierShift },		// right shift
+	{ 0x3D, uiModifierAlt },		// right alt
+	{ 0x3E, uiModifierCtrl },		// right control
+	// the following is not in Events.h for some reason
+	// thanks to Nicole and jedivulcan from irc.badnik.net
+	{ 0x36, uiModifierSuper },		// right command
+	{ 0xFFFF, 0 },
+};
+
+BOOL fromKeycode(unsigned short keycode, uiAreaKeyEvent *ke)
+{
+	int i;
+
+	for (i = 0; keycodeKeys[i].keycode != 0xFFFF; i++)
+		if (keycodeKeys[i].keycode == keycode) {
+			ke->Key = keycodeKeys[i].equiv;
+			return YES;
+		}
+	for (i = 0; keycodeExtKeys[i].keycode != 0xFFFF; i++)
+		if (keycodeExtKeys[i].keycode == keycode) {
+			ke->ExtKey = keycodeExtKeys[i].equiv;
+			return YES;
+		}
+	return NO;
+}
+
+BOOL keycodeModifier(unsigned short keycode, uiModifiers *mod)
+{
+	int i;
+
+	for (i = 0; keycodeModifiers[i].keycode != 0xFFFF; i++)
+		if (keycodeModifiers[i].keycode == keycode) {
+			*mod = keycodeModifiers[i].equiv;
+			return YES;
+		}
+	return NO;
+}
diff --git a/src/libui_sdl/libui/darwin/autolayout.m b/src/libui_sdl/libui/darwin/autolayout.m
new file mode 100644
index 0000000..9964155
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/autolayout.m
@@ -0,0 +1,161 @@
+// 15 august 2015
+#import "uipriv_darwin.h"
+
+NSLayoutConstraint *mkConstraint(id view1, NSLayoutAttribute attr1, NSLayoutRelation relation, id view2, NSLayoutAttribute attr2, CGFloat multiplier, CGFloat c, NSString *desc)
+{
+	NSLayoutConstraint *constraint;
+
+	constraint = [NSLayoutConstraint constraintWithItem:view1
+		attribute:attr1
+		relatedBy:relation
+		toItem:view2
+		attribute:attr2
+		multiplier:multiplier
+		constant:c];
+	// apparently only added in 10.9
+	if ([constraint respondsToSelector:@selector(setIdentifier:)])
+		[((id) constraint) setIdentifier:desc];
+	return constraint;
+}
+
+CGFloat uiDarwinMarginAmount(void *reserved)
+{
+	return 20.0;
+}
+
+CGFloat uiDarwinPaddingAmount(void *reserved)
+{
+	return 8.0;
+}
+
+// this is needed for NSSplitView to work properly; see http://stackoverflow.com/questions/34574478/how-can-i-set-the-position-of-a-nssplitview-nowadays-setpositionofdivideratind (stal in irc.freenode.net/#macdev came up with the exact combination)
+// turns out it also works on NSTabView and NSBox too, possibly others!
+// and for bonus points, it even seems to fix unsatisfiable-constraint-autoresizing-mask issues with NSTabView and NSBox too!!! this is nuts
+void jiggleViewLayout(NSView *view)
+{
+	[view setNeedsLayout:YES];
+	[view layoutSubtreeIfNeeded];
+}
+
+static CGFloat margins(int margined)
+{
+	if (!margined)
+		return 0.0;
+	return uiDarwinMarginAmount(NULL);
+}
+
+void singleChildConstraintsEstablish(struct singleChildConstraints *c, NSView *contentView, NSView *childView, BOOL hugsTrailing, BOOL hugsBottom, int margined, NSString *desc)
+{
+	CGFloat margin;
+
+	margin = margins(margined);
+
+	c->leadingConstraint = mkConstraint(contentView, NSLayoutAttributeLeading,
+		NSLayoutRelationEqual,
+		childView, NSLayoutAttributeLeading,
+		1, -margin,
+		[desc stringByAppendingString:@" leading constraint"]);
+	[contentView addConstraint:c->leadingConstraint];
+	[c->leadingConstraint retain];
+
+	c->topConstraint = mkConstraint(contentView, NSLayoutAttributeTop,
+		NSLayoutRelationEqual,
+		childView, NSLayoutAttributeTop,
+		1, -margin,
+		[desc stringByAppendingString:@" top constraint"]);
+	[contentView addConstraint:c->topConstraint];
+	[c->topConstraint retain];
+
+	c->trailingConstraintGreater = mkConstraint(contentView, NSLayoutAttributeTrailing,
+		NSLayoutRelationGreaterThanOrEqual,
+		childView, NSLayoutAttributeTrailing,
+		1, margin,
+		[desc stringByAppendingString:@" trailing >= constraint"]);
+	if (hugsTrailing)
+		[c->trailingConstraintGreater setPriority:NSLayoutPriorityDefaultLow];
+	[contentView addConstraint:c->trailingConstraintGreater];
+	[c->trailingConstraintGreater retain];
+
+	c->trailingConstraintEqual = mkConstraint(contentView, NSLayoutAttributeTrailing,
+		NSLayoutRelationEqual,
+		childView, NSLayoutAttributeTrailing,
+		1, margin,
+		[desc stringByAppendingString:@" trailing == constraint"]);
+	if (!hugsTrailing)
+		[c->trailingConstraintEqual setPriority:NSLayoutPriorityDefaultLow];
+	[contentView addConstraint:c->trailingConstraintEqual];
+	[c->trailingConstraintEqual retain];
+
+	c->bottomConstraintGreater = mkConstraint(contentView, NSLayoutAttributeBottom,
+		NSLayoutRelationGreaterThanOrEqual,
+		childView, NSLayoutAttributeBottom,
+		1, margin,
+		[desc stringByAppendingString:@" bottom >= constraint"]);
+	if (hugsBottom)
+		[c->bottomConstraintGreater setPriority:NSLayoutPriorityDefaultLow];
+	[contentView addConstraint:c->bottomConstraintGreater];
+	[c->bottomConstraintGreater retain];
+
+	c->bottomConstraintEqual = mkConstraint(contentView, NSLayoutAttributeBottom,
+		NSLayoutRelationEqual,
+		childView, NSLayoutAttributeBottom,
+		1, margin,
+		[desc stringByAppendingString:@" bottom == constraint"]);
+	if (!hugsBottom)
+		[c->bottomConstraintEqual setPriority:NSLayoutPriorityDefaultLow];
+	[contentView addConstraint:c->bottomConstraintEqual];
+	[c->bottomConstraintEqual retain];
+}
+
+void singleChildConstraintsRemove(struct singleChildConstraints *c, NSView *cv)
+{
+	if (c->leadingConstraint != nil) {
+		[cv removeConstraint:c->leadingConstraint];
+		[c->leadingConstraint release];
+		c->leadingConstraint = nil;
+	}
+	if (c->topConstraint != nil) {
+		[cv removeConstraint:c->topConstraint];
+		[c->topConstraint release];
+		c->topConstraint = nil;
+	}
+	if (c->trailingConstraintGreater != nil) {
+		[cv removeConstraint:c->trailingConstraintGreater];
+		[c->trailingConstraintGreater release];
+		c->trailingConstraintGreater = nil;
+	}
+	if (c->trailingConstraintEqual != nil) {
+		[cv removeConstraint:c->trailingConstraintEqual];
+		[c->trailingConstraintEqual release];
+		c->trailingConstraintEqual = nil;
+	}
+	if (c->bottomConstraintGreater != nil) {
+		[cv removeConstraint:c->bottomConstraintGreater];
+		[c->bottomConstraintGreater release];
+		c->bottomConstraintGreater = nil;
+	}
+	if (c->bottomConstraintEqual != nil) {
+		[cv removeConstraint:c->bottomConstraintEqual];
+		[c->bottomConstraintEqual release];
+		c->bottomConstraintEqual = nil;
+	}
+}
+
+void singleChildConstraintsSetMargined(struct singleChildConstraints *c, int margined)
+{
+	CGFloat margin;
+
+	margin = margins(margined);
+	if (c->leadingConstraint != nil)
+		[c->leadingConstraint setConstant:-margin];
+	if (c->topConstraint != nil)
+		[c->topConstraint setConstant:-margin];
+	if (c->trailingConstraintGreater != nil)
+		[c->trailingConstraintGreater setConstant:margin];
+	if (c->trailingConstraintEqual != nil)
+		[c->trailingConstraintEqual setConstant:margin];
+	if (c->bottomConstraintGreater != nil)
+		[c->bottomConstraintGreater setConstant:margin];
+	if (c->bottomConstraintEqual != nil)
+		[c->bottomConstraintEqual setConstant:margin];
+}
diff --git a/src/libui_sdl/libui/darwin/box.m b/src/libui_sdl/libui/darwin/box.m
new file mode 100644
index 0000000..18d536d
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/box.m
@@ -0,0 +1,469 @@
+// 15 august 2015
+#import "uipriv_darwin.h"
+
+// TODO hiding all stretchy controls still hugs trailing edge
+
+@interface boxChild : NSObject
+@property uiControl *c;
+@property BOOL stretchy;
+@property NSLayoutPriority oldPrimaryHuggingPri;
+@property NSLayoutPriority oldSecondaryHuggingPri;
+- (NSView *)view;
+@end
+
+@interface boxView : NSView {
+	uiBox *b;
+	NSMutableArray *children;
+	BOOL vertical;
+	int padded;
+
+	NSLayoutConstraint *first;
+	NSMutableArray *inBetweens;
+	NSLayoutConstraint *last;
+	NSMutableArray *otherConstraints;
+
+	NSLayoutAttribute primaryStart;
+	NSLayoutAttribute primaryEnd;
+	NSLayoutAttribute secondaryStart;
+	NSLayoutAttribute secondaryEnd;
+	NSLayoutAttribute primarySize;
+	NSLayoutConstraintOrientation primaryOrientation;
+	NSLayoutConstraintOrientation secondaryOrientation;
+}
+- (id)initWithVertical:(BOOL)vert b:(uiBox *)bb;
+- (void)onDestroy;
+- (void)removeOurConstraints;
+- (void)syncEnableStates:(int)enabled;
+- (CGFloat)paddingAmount;
+- (void)establishOurConstraints;
+- (void)append:(uiControl *)c stretchy:(int)stretchy;
+- (void)delete:(int)n;
+- (int)isPadded;
+- (void)setPadded:(int)p;
+- (BOOL)hugsTrailing;
+- (BOOL)hugsBottom;
+- (int)nStretchy;
+@end
+
+struct uiBox {
+	uiDarwinControl c;
+	boxView *view;
+};
+
+@implementation boxChild
+
+- (NSView *)view
+{
+	return (NSView *) uiControlHandle(self.c);
+}
+
+@end
+
+@implementation boxView
+
+- (id)initWithVertical:(BOOL)vert b:(uiBox *)bb
+{
+	self = [super initWithFrame:NSZeroRect];
+	if (self != nil) {
+		// the weird names vert and bb are to shut the compiler up about shadowing because implicit this/self is stupid
+		self->b = bb;
+		self->vertical = vert;
+		self->padded = 0;
+		self->children = [NSMutableArray new];
+
+		self->inBetweens = [NSMutableArray new];
+		self->otherConstraints = [NSMutableArray new];
+
+		if (self->vertical) {
+			self->primaryStart = NSLayoutAttributeTop;
+			self->primaryEnd = NSLayoutAttributeBottom;
+			self->secondaryStart = NSLayoutAttributeLeading;
+			self->secondaryEnd = NSLayoutAttributeTrailing;
+			self->primarySize = NSLayoutAttributeHeight;
+			self->primaryOrientation = NSLayoutConstraintOrientationVertical;
+			self->secondaryOrientation = NSLayoutConstraintOrientationHorizontal;
+		} else {
+			self->primaryStart = NSLayoutAttributeLeading;
+			self->primaryEnd = NSLayoutAttributeTrailing;
+			self->secondaryStart = NSLayoutAttributeTop;
+			self->secondaryEnd = NSLayoutAttributeBottom;
+			self->primarySize = NSLayoutAttributeWidth;
+			self->primaryOrientation = NSLayoutConstraintOrientationHorizontal;
+			self->secondaryOrientation = NSLayoutConstraintOrientationVertical;
+		}
+	}
+	return self;
+}
+
+- (void)onDestroy
+{
+	boxChild *bc;
+
+	[self removeOurConstraints];
+	[self->inBetweens release];
+	[self->otherConstraints release];
+
+	for (bc in self->children) {
+		uiControlSetParent(bc.c, NULL);
+		uiDarwinControlSetSuperview(uiDarwinControl(bc.c), nil);
+		uiControlDestroy(bc.c);
+	}
+	[self->children release];
+}
+
+- (void)removeOurConstraints
+{
+	if (self->first != nil) {
+		[self removeConstraint:self->first];
+		[self->first release];
+		self->first = nil;
+	}
+	if ([self->inBetweens count] != 0) {
+		[self removeConstraints:self->inBetweens];
+		[self->inBetweens removeAllObjects];
+	}
+	if (self->last != nil) {
+		[self removeConstraint:self->last];
+		[self->last release];
+		self->last = nil;
+	}
+	if ([self->otherConstraints count] != 0) {
+		[self removeConstraints:self->otherConstraints];
+		[self->otherConstraints removeAllObjects];
+	}
+}
+
+- (void)syncEnableStates:(int)enabled
+{
+	boxChild *bc;
+
+	for (bc in self->children)
+		uiDarwinControlSyncEnableState(uiDarwinControl(bc.c), enabled);
+}
+
+- (CGFloat)paddingAmount
+{
+	if (!self->padded)
+		return 0.0;
+	return uiDarwinPaddingAmount(NULL);
+}
+
+- (void)establishOurConstraints
+{
+	boxChild *bc;
+	CGFloat padding;
+	NSView *prev;
+	NSLayoutConstraint *c;
+	BOOL (*hugsSecondary)(uiDarwinControl *);
+
+	[self removeOurConstraints];
+	if ([self->children count] == 0)
+		return;
+	padding = [self paddingAmount];
+
+	// first arrange in the primary direction
+	prev = nil;
+	for (bc in self->children) {
+		if (!uiControlVisible(bc.c))
+			continue;
+		if (prev == nil) {			// first view
+			self->first = mkConstraint(self, self->primaryStart,
+				NSLayoutRelationEqual,
+				[bc view], self->primaryStart,
+				1, 0,
+				@"uiBox first primary constraint");
+			[self addConstraint:self->first];
+			[self->first retain];
+			prev = [bc view];
+			continue;
+		}
+		// not the first; link it
+		c = mkConstraint(prev, self->primaryEnd,
+			NSLayoutRelationEqual,
+			[bc view], self->primaryStart,
+			1, -padding,
+			@"uiBox in-between primary constraint");
+		[self addConstraint:c];
+		[self->inBetweens addObject:c];
+		prev = [bc view];
+	}
+	if (prev == nil)		// no control visible; act as if no controls
+		return;
+	self->last = mkConstraint(prev, self->primaryEnd,
+		NSLayoutRelationEqual,
+		self, self->primaryEnd,
+		1, 0,
+		@"uiBox last primary constraint");
+	[self addConstraint:self->last];
+	[self->last retain];
+
+	// then arrange in the secondary direction
+	hugsSecondary = uiDarwinControlHugsTrailingEdge;
+	if (!self->vertical)
+		hugsSecondary = uiDarwinControlHugsBottom;
+	for (bc in self->children) {
+		if (!uiControlVisible(bc.c))
+			continue;
+		c = mkConstraint(self, self->secondaryStart,
+			NSLayoutRelationEqual,
+			[bc view], self->secondaryStart,
+			1, 0,
+			@"uiBox secondary start constraint");
+		[self addConstraint:c];
+		[self->otherConstraints addObject:c];
+		c = mkConstraint([bc view], self->secondaryEnd,
+			NSLayoutRelationLessThanOrEqual,
+			self, self->secondaryEnd,
+			1, 0,
+			@"uiBox secondary end <= constraint");
+		if ((*hugsSecondary)(uiDarwinControl(bc.c)))
+			[c setPriority:NSLayoutPriorityDefaultLow];
+		[self addConstraint:c];
+		[self->otherConstraints addObject:c];
+		c = mkConstraint([bc view], self->secondaryEnd,
+			NSLayoutRelationEqual,
+			self, self->secondaryEnd,
+			1, 0,
+			@"uiBox secondary end == constraint");
+		if (!(*hugsSecondary)(uiDarwinControl(bc.c)))
+			[c setPriority:NSLayoutPriorityDefaultLow];
+		[self addConstraint:c];
+		[self->otherConstraints addObject:c];
+	}
+
+	// and make all stretchy controls the same size
+	if ([self nStretchy] == 0)
+		return;
+	prev = nil;		// first stretchy view
+	for (bc in self->children) {
+		if (!uiControlVisible(bc.c))
+			continue;
+		if (!bc.stretchy)
+			continue;
+		if (prev == nil) {
+			prev = [bc view];
+			continue;
+		}
+		c = mkConstraint(prev, self->primarySize,
+			NSLayoutRelationEqual,
+			[bc view], self->primarySize,
+			1, 0,
+			@"uiBox stretchy size constraint");
+		[self addConstraint:c];
+		[self->otherConstraints addObject:c];
+	}
+}
+
+- (void)append:(uiControl *)c stretchy:(int)stretchy
+{
+	boxChild *bc;
+	NSLayoutPriority priority;
+	int oldnStretchy;
+
+	bc = [boxChild new];
+	bc.c = c;
+	bc.stretchy = stretchy;
+	bc.oldPrimaryHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(bc.c), self->primaryOrientation);
+	bc.oldSecondaryHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(bc.c), self->secondaryOrientation);
+
+	uiControlSetParent(bc.c, uiControl(self->b));
+	uiDarwinControlSetSuperview(uiDarwinControl(bc.c), self);
+	uiDarwinControlSyncEnableState(uiDarwinControl(bc.c), uiControlEnabledToUser(uiControl(self->b)));
+
+	// if a control is stretchy, it should not hug in the primary direction
+	// otherwise, it should *forcibly* hug
+	if (bc.stretchy)
+		priority = NSLayoutPriorityDefaultLow;
+	else
+		// LONGTERM will default high work?
+		priority = NSLayoutPriorityRequired;
+	uiDarwinControlSetHuggingPriority(uiDarwinControl(bc.c), priority, self->primaryOrientation);
+	// make sure controls don't hug their secondary direction so they fill the width of the view
+	uiDarwinControlSetHuggingPriority(uiDarwinControl(bc.c), NSLayoutPriorityDefaultLow, self->secondaryOrientation);
+
+	oldnStretchy = [self nStretchy];
+	[self->children addObject:bc];
+
+	[self establishOurConstraints];
+	if (bc.stretchy)
+		if (oldnStretchy == 0)
+			uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl(self->b));
+
+	[bc release];		// we don't need the initial reference now
+}
+
+- (void)delete:(int)n
+{
+	boxChild *bc;
+	int stretchy;
+
+	bc = (boxChild *) [self->children objectAtIndex:n];
+	stretchy = bc.stretchy;
+
+	uiControlSetParent(bc.c, NULL);
+	uiDarwinControlSetSuperview(uiDarwinControl(bc.c), nil);
+
+	uiDarwinControlSetHuggingPriority(uiDarwinControl(bc.c), bc.oldPrimaryHuggingPri, self->primaryOrientation);
+	uiDarwinControlSetHuggingPriority(uiDarwinControl(bc.c), bc.oldSecondaryHuggingPri, self->secondaryOrientation);
+
+	[self->children removeObjectAtIndex:n];
+
+	[self establishOurConstraints];
+	if (stretchy)
+		if ([self nStretchy] == 0)
+			uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl(self->b));
+}
+
+- (int)isPadded
+{
+	return self->padded;
+}
+
+- (void)setPadded:(int)p
+{
+	CGFloat padding;
+	NSLayoutConstraint *c;
+
+	self->padded = p;
+	padding = [self paddingAmount];
+	for (c in self->inBetweens)
+		[c setConstant:-padding];
+}
+
+- (BOOL)hugsTrailing
+{
+	if (self->vertical)		// always hug if vertical
+		return YES;
+	return [self nStretchy] != 0;
+}
+
+- (BOOL)hugsBottom
+{
+	if (!self->vertical)		// always hug if horizontal
+		return YES;
+	return [self nStretchy] != 0;
+}
+
+- (int)nStretchy
+{
+	boxChild *bc;
+	int n;
+
+	n = 0;
+	for (bc in self->children) {
+		if (!uiControlVisible(bc.c))
+			continue;
+		if (bc.stretchy)
+			n++;
+	}
+	return n;
+}
+
+@end
+
+static void uiBoxDestroy(uiControl *c)
+{
+	uiBox *b = uiBox(c);
+
+	[b->view onDestroy];
+	[b->view release];
+	uiFreeControl(uiControl(b));
+}
+
+uiDarwinControlDefaultHandle(uiBox, view)
+uiDarwinControlDefaultParent(uiBox, view)
+uiDarwinControlDefaultSetParent(uiBox, view)
+uiDarwinControlDefaultToplevel(uiBox, view)
+uiDarwinControlDefaultVisible(uiBox, view)
+uiDarwinControlDefaultShow(uiBox, view)
+uiDarwinControlDefaultHide(uiBox, view)
+uiDarwinControlDefaultEnabled(uiBox, view)
+uiDarwinControlDefaultEnable(uiBox, view)
+uiDarwinControlDefaultDisable(uiBox, view)
+
+static void uiBoxSyncEnableState(uiDarwinControl *c, int enabled)
+{
+	uiBox *b = uiBox(c);
+
+	if (uiDarwinShouldStopSyncEnableState(uiDarwinControl(b), enabled))
+		return;
+	[b->view syncEnableStates:enabled];
+}
+
+uiDarwinControlDefaultSetSuperview(uiBox, view)
+
+static BOOL uiBoxHugsTrailingEdge(uiDarwinControl *c)
+{
+	uiBox *b = uiBox(c);
+
+	return [b->view hugsTrailing];
+}
+
+static BOOL uiBoxHugsBottom(uiDarwinControl *c)
+{
+	uiBox *b = uiBox(c);
+
+	return [b->view hugsBottom];
+}
+
+static void uiBoxChildEdgeHuggingChanged(uiDarwinControl *c)
+{
+	uiBox *b = uiBox(c);
+
+	[b->view establishOurConstraints];
+}
+
+uiDarwinControlDefaultHuggingPriority(uiBox, view)
+uiDarwinControlDefaultSetHuggingPriority(uiBox, view)
+
+static void uiBoxChildVisibilityChanged(uiDarwinControl *c)
+{
+	uiBox *b = uiBox(c);
+
+	[b->view establishOurConstraints];
+}
+
+void uiBoxAppend(uiBox *b, uiControl *c, int stretchy)
+{
+	// LONGTERM on other platforms
+	// or at leat allow this and implicitly turn it into a spacer
+	if (c == NULL)
+		userbug("You cannot add NULL to a uiBox.");
+	[b->view append:c stretchy:stretchy];
+}
+
+void uiBoxDelete(uiBox *b, int n)
+{
+	[b->view delete:n];
+}
+
+int uiBoxPadded(uiBox *b)
+{
+	return [b->view isPadded];
+}
+
+void uiBoxSetPadded(uiBox *b, int padded)
+{
+	[b->view setPadded:padded];
+}
+
+static uiBox *finishNewBox(BOOL vertical)
+{
+	uiBox *b;
+
+	uiDarwinNewControl(uiBox, b);
+
+	b->view = [[boxView alloc] initWithVertical:vertical b:b];
+
+	return b;
+}
+
+uiBox *uiNewHorizontalBox(void)
+{
+	return finishNewBox(NO);
+}
+
+uiBox *uiNewVerticalBox(void)
+{
+	return finishNewBox(YES);
+}
diff --git a/src/libui_sdl/libui/darwin/button.m b/src/libui_sdl/libui/darwin/button.m
new file mode 100644
index 0000000..baccabb
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/button.m
@@ -0,0 +1,113 @@
+// 13 august 2015
+#import "uipriv_darwin.h"
+
+struct uiButton {
+	uiDarwinControl c;
+	NSButton *button;
+	void (*onClicked)(uiButton *, void *);
+	void *onClickedData;
+};
+
+@interface buttonDelegateClass : NSObject {
+	struct mapTable *buttons;
+}
+- (IBAction)onClicked:(id)sender;
+- (void)registerButton:(uiButton *)b;
+- (void)unregisterButton:(uiButton *)b;
+@end
+
+@implementation buttonDelegateClass
+
+- (id)init
+{
+	self = [super init];
+	if (self)
+		self->buttons = newMap();
+	return self;
+}
+
+- (void)dealloc
+{
+	mapDestroy(self->buttons);
+	[super dealloc];
+}
+
+- (IBAction)onClicked:(id)sender
+{
+	uiButton *b;
+
+	b = (uiButton *) mapGet(self->buttons, sender);
+	(*(b->onClicked))(b, b->onClickedData);
+}
+
+- (void)registerButton:(uiButton *)b
+{
+	mapSet(self->buttons, b->button, b);
+	[b->button setTarget:self];
+	[b->button setAction:@selector(onClicked:)];
+}
+
+- (void)unregisterButton:(uiButton *)b
+{
+	[b->button setTarget:nil];
+	mapDelete(self->buttons, b->button);
+}
+
+@end
+
+static buttonDelegateClass *buttonDelegate = nil;
+
+uiDarwinControlAllDefaultsExceptDestroy(uiButton, button)
+
+static void uiButtonDestroy(uiControl *c)
+{
+	uiButton *b = uiButton(c);
+
+	[buttonDelegate unregisterButton:b];
+	[b->button release];
+	uiFreeControl(uiControl(b));
+}
+
+char *uiButtonText(uiButton *b)
+{
+	return uiDarwinNSStringToText([b->button title]);
+}
+
+void uiButtonSetText(uiButton *b, const char *text)
+{
+	[b->button setTitle:toNSString(text)];
+}
+
+void uiButtonOnClicked(uiButton *b, void (*f)(uiButton *, void *), void *data)
+{
+	b->onClicked = f;
+	b->onClickedData = data;
+}
+
+static void defaultOnClicked(uiButton *b, void *data)
+{
+	// do nothing
+}
+
+uiButton *uiNewButton(const char *text)
+{
+	uiButton *b;
+
+	uiDarwinNewControl(uiButton, b);
+
+	b->button = [[NSButton alloc] initWithFrame:NSZeroRect];
+	[b->button setTitle:toNSString(text)];
+	[b->button setButtonType:NSMomentaryPushInButton];
+	[b->button setBordered:YES];
+	[b->button setBezelStyle:NSRoundedBezelStyle];
+	uiDarwinSetControlFont(b->button, NSRegularControlSize);
+
+	if (buttonDelegate == nil) {
+		buttonDelegate = [[buttonDelegateClass new] autorelease];
+		[delegates addObject:buttonDelegate];
+	}
+	[buttonDelegate registerButton:b];
+	uiButtonOnClicked(b, defaultOnClicked, NULL);
+
+	return b;
+}
diff --git a/src/libui_sdl/libui/darwin/checkbox.m b/src/libui_sdl/libui/darwin/checkbox.m
new file mode 100644
index 0000000..dd1ce09
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/checkbox.m
@@ -0,0 +1,129 @@
+// 14 august 2015
+#import "uipriv_darwin.h"
+
+struct uiCheckbox {
+	uiDarwinControl c;
+	NSButton *button;
+	void (*onToggled)(uiCheckbox *, void *);
+	void *onToggledData;
+};
+
+@interface checkboxDelegateClass : NSObject {
+	struct mapTable *buttons;
+}
+- (IBAction)onToggled:(id)sender;
+- (void)registerCheckbox:(uiCheckbox *)c;
+- (void)unregisterCheckbox:(uiCheckbox *)c;
+@end
+
+@implementation checkboxDelegateClass
+
+- (id)init
+{
+	self = [super init];
+	if (self)
+		self->buttons = newMap();
+	return self;
+}
+
+- (void)dealloc
+{
+	mapDestroy(self->buttons);
+	[super dealloc];
+}
+
+- (IBAction)onToggled:(id)sender
+{
+	uiCheckbox *c;
+
+	c = (uiCheckbox *) mapGet(self->buttons, sender);
+	(*(c->onToggled))(c, c->onToggledData);
+}
+
+- (void)registerCheckbox:(uiCheckbox *)c
+{
+	mapSet(self->buttons, c->button, c);
+	[c->button setTarget:self];
+	[c->button setAction:@selector(onToggled:)];
+}
+
+- (void)unregisterCheckbox:(uiCheckbox *)c
+{
+	[c->button setTarget:nil];
+	mapDelete(self->buttons, c->button);
+}
+
+@end
+
+static checkboxDelegateClass *checkboxDelegate = nil;
+
+uiDarwinControlAllDefaultsExceptDestroy(uiCheckbox, button)
+
+static void uiCheckboxDestroy(uiControl *cc)
+{
+	uiCheckbox *c = uiCheckbox(cc);
+
+	[checkboxDelegate unregisterCheckbox:c];
+	[c->button release];
+	uiFreeControl(uiControl(c));
+}
+
+char *uiCheckboxText(uiCheckbox *c)
+{
+	return uiDarwinNSStringToText([c->button title]);
+}
+
+void uiCheckboxSetText(uiCheckbox *c, const char *text)
+{
+	[c->button setTitle:toNSString(text)];
+}
+
+void uiCheckboxOnToggled(uiCheckbox *c, void (*f)(uiCheckbox *, void *), void *data)
+{
+	c->onToggled = f;
+	c->onToggledData = data;
+}
+
+int uiCheckboxChecked(uiCheckbox *c)
+{
+	return [c->button state] == NSOnState;
+}
+
+void uiCheckboxSetChecked(uiCheckbox *c, int checked)
+{
+	NSInteger state;
+
+	state = NSOnState;
+	if (!checked)
+		state = NSOffState;
+	[c->button setState:state];
+}
+
+static void defaultOnToggled(uiCheckbox *c, void *data)
+{
+	// do nothing
+}
+
+uiCheckbox *uiNewCheckbox(const char *text)
+{
+	uiCheckbox *c;
+
+	uiDarwinNewControl(uiCheckbox, c);
+
+	c->button = [[NSButton alloc] initWithFrame:NSZeroRect];
+	[c->button setTitle:toNSString(text)];
+	[c->button setButtonType:NSSwitchButton];
+	// doesn't seem to have an associated bezel style
+	[c->button setBordered:NO];
+	[c->button setTransparent:NO];
+	uiDarwinSetControlFont(c->button, NSRegularControlSize);
+
+	if (checkboxDelegate == nil) {
+		checkboxDelegate = [[checkboxDelegateClass new] autorelease];
+		[delegates addObject:checkboxDelegate];
+	}
+	[checkboxDelegate registerCheckbox:c];
+	uiCheckboxOnToggled(c, defaultOnToggled, NULL);
+
+	return c;
+}
diff --git a/src/libui_sdl/libui/darwin/colorbutton.m b/src/libui_sdl/libui/darwin/colorbutton.m
new file mode 100644
index 0000000..83b6157
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/colorbutton.m
@@ -0,0 +1,159 @@
+// 15 may 2016
+#import "uipriv_darwin.h"
+
+// TODO no intrinsic height?
+
+@interface colorButton : NSColorWell {
+	uiColorButton *libui_b;
+	BOOL libui_changing;
+	BOOL libui_setting;
+}
+- (id)initWithFrame:(NSRect)frame libuiColorButton:(uiColorButton *)b;
+- (void)deactivateOnClose:(NSNotification *)note;
+- (void)libuiColor:(double *)r g:(double *)g b:(double *)b a:(double *)a;
+- (void)libuiSetColor:(double)r g:(double)g b:(double)b a:(double)a;
+@end
+
+// only one may be active at one time
+static colorButton *activeColorButton = nil;
+
+struct uiColorButton {
+	uiDarwinControl c;
+	colorButton *button;
+	void (*onChanged)(uiColorButton *, void *);
+	void *onChangedData;
+};
+
+@implementation colorButton
+
+- (id)initWithFrame:(NSRect)frame libuiColorButton:(uiColorButton *)b
+{
+	self = [super initWithFrame:frame];
+	if (self) {
+		// the default color is white; set it to black first (see -setColor: below for why we do it first)
+		[self libuiSetColor:0.0 g:0.0 b:0.0 a:1.0];
+
+		self->libui_b = b;
+		self->libui_changing = NO;
+	}
+	return self;
+}
+
+- (void)activate:(BOOL)exclusive
+{
+	if (activeColorButton != nil)
+		activeColorButton->libui_changing = YES;
+	[NSColorPanel setPickerMask:NSColorPanelAllModesMask];
+	[[NSColorPanel sharedColorPanel] setShowsAlpha:YES];
+	[super activate:YES];
+	activeColorButton = self;
+	// see stddialogs.m for details
+	[[NSColorPanel sharedColorPanel] setWorksWhenModal:NO];
+	[[NSNotificationCenter defaultCenter] addObserver:self
+		selector:@selector(deactivateOnClose:)
+		name:NSWindowWillCloseNotification
+		object:[NSColorPanel sharedColorPanel]];
+}
+
+- (void)deactivate
+{
+	[super deactivate];
+	activeColorButton = nil;
+	if (!self->libui_changing)
+		[[NSColorPanel sharedColorPanel] orderOut:nil];
+	[[NSNotificationCenter defaultCenter] removeObserver:self
+		name:NSWindowWillCloseNotification
+		object:[NSColorPanel sharedColorPanel]];
+	self->libui_changing = NO;
+}
+
+- (void)deactivateOnClose:(NSNotification *)note
+{
+	[self deactivate];
+}
+
+- (void)setColor:(NSColor *)color
+{
+	uiColorButton *b = self->libui_b;
+
+	[super setColor:color];
+	// this is called by NSColorWell's init, so we have to guard
+	// also don't signal during a programmatic change
+	if (b != nil && !self->libui_setting)
+		(*(b->onChanged))(b, b->onChangedData);
+}
+
+- (void)libuiColor:(double *)r g:(double *)g b:(double *)b a:(double *)a
+{
+	NSColor *rgba;
+	CGFloat cr, cg, cb, ca;
+
+	// the given color may not be an RGBA color, which will cause the -getRed:green:blue:alpha: call to throw an exception
+	rgba = [[self color] colorUsingColorSpace:[NSColorSpace sRGBColorSpace]];
+	[rgba getRed:&cr green:&cg blue:&cb alpha:&ca];
+	*r = cr;
+	*g = cg;
+	*b = cb;
+	*a = ca;
+	// rgba will be autoreleased since it isn't a new or init call
+}
+
+- (void)libuiSetColor:(double)r g:(double)g b:(double)b a:(double)a
+{
+	self->libui_setting = YES;
+	[self setColor:[NSColor colorWithSRGBRed:r green:g blue:b alpha:a]];
+	self->libui_setting = NO;
+}
+
+// NSColorWell has no intrinsic size by default; give it the default Interface Builder size.
+- (NSSize)intrinsicContentSize
+{
+	return NSMakeSize(44, 23);
+}
+
+@end
+
+uiDarwinControlAllDefaults(uiColorButton, button)
+
+// we do not want color change events to be sent to any controls other than the color buttons
+// see main.m for more details
+BOOL colorButtonInhibitSendAction(SEL sel, id from, id to)
+{
+	if (sel != @selector(changeColor:))
+		return NO;
+	return ![to isKindOfClass:[colorButton class]];
+}
+
+static void defaultOnChanged(uiColorButton *b, void *data)
+{
+	// do nothing
+}
+
+void uiColorButtonColor(uiColorButton *b, double *r, double *g, double *bl, double *a)
+{
+	[b->button libuiColor:r g:g b:bl a:a];
+}
+
+void uiColorButtonSetColor(uiColorButton *b, double r, double g, double bl, double a)
+{
+	[b->button libuiSetColor:r g:g b:bl a:a];
+}
+
+void uiColorButtonOnChanged(uiColorButton *b, void (*f)(uiColorButton *, void *), void *data)
+{
+	b->onChanged = f;
+	b->onChangedData = data;
+}
+
+uiColorButton *uiNewColorButton(void)
+{
+	uiColorButton *b;
+
+	uiDarwinNewControl(uiColorButton, b);
+
+	b->button = [[colorButton alloc] initWithFrame:NSZeroRect libuiColorButton:b];
+
+	uiColorButtonOnChanged(b, defaultOnChanged, NULL);
+
+	return b;
+}
diff --git a/src/libui_sdl/libui/darwin/combobox.m b/src/libui_sdl/libui/darwin/combobox.m
new file mode 100644
index 0000000..89a2e28
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/combobox.m
@@ -0,0 +1,145 @@
+// 14 august 2015
+#import "uipriv_darwin.h"
+
+// NSComboBoxes have no intrinsic width; we'll use the default Interface Builder width for them.
+// NSPopUpButton is fine.
+#define comboboxWidth 96
+
+struct uiCombobox {
+	uiDarwinControl c;
+	NSPopUpButton *pb;
+	NSArrayController *pbac;
+	void (*onSelected)(uiCombobox *, void *);
+	void *onSelectedData;
+};
+
+@interface comboboxDelegateClass : NSObject {
+	struct mapTable *comboboxes;
+}
+- (IBAction)onSelected:(id)sender;
+- (void)registerCombobox:(uiCombobox *)c;
+- (void)unregisterCombobox:(uiCombobox *)c;
+@end
+
+@implementation comboboxDelegateClass
+
+- (id)init
+{
+	self = [super init];
+	if (self)
+		self->comboboxes = newMap();
+	return self;
+}
+
+- (void)dealloc
+{
+	mapDestroy(self->comboboxes);
+	[super dealloc];
+}
+
+- (IBAction)onSelected:(id)sender
+{
+	uiCombobox *c;
+
+	c = uiCombobox(mapGet(self->comboboxes, sender));
+	(*(c->onSelected))(c, c->onSelectedData);
+}
+
+- (void)registerCombobox:(uiCombobox *)c
+{
+	mapSet(self->comboboxes, c->pb, c);
+	[c->pb setTarget:self];
+	[c->pb setAction:@selector(onSelected:)];
+}
+
+- (void)unregisterCombobox:(uiCombobox *)c
+{
+	[c->pb setTarget:nil];
+	mapDelete(self->comboboxes, c->pb);
+}
+
+@end
+
+static comboboxDelegateClass *comboboxDelegate = nil;
+
+uiDarwinControlAllDefaultsExceptDestroy(uiCombobox, pb)
+
+static void uiComboboxDestroy(uiControl *cc)
+{
+	uiCombobox *c = uiCombobox(cc);
+
+	[comboboxDelegate unregisterCombobox:c];
+	[c->pb unbind:@"contentObjects"];
+	[c->pb unbind:@"selectedIndex"];
+	[c->pbac release];
+	[c->pb release];
+	uiFreeControl(uiControl(c));
+}
+
+void uiComboboxAppend(uiCombobox *c, const char *text)
+{
+	[c->pbac addObject:toNSString(text)];
+}
+
+int uiComboboxSelected(uiCombobox *c)
+{
+	return [c->pb indexOfSelectedItem];
+}
+
+void uiComboboxSetSelected(uiCombobox *c, int n)
+{
+	[c->pb selectItemAtIndex:n];
+}
+
+void uiComboboxOnSelected(uiCombobox *c, void (*f)(uiCombobox *c, void *data), void *data)
+{
+	c->onSelected = f;
+	c->onSelectedData = data;
+}
+
+static void defaultOnSelected(uiCombobox *c, void *data)
+{
+	// do nothing
+}
+
+uiCombobox *uiNewCombobox(void)
+{
+	uiCombobox *c;
+	NSPopUpButtonCell *pbcell;
+
+	uiDarwinNewControl(uiCombobox, c);
+
+	c->pb = [[NSPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO];
+	[c->pb setPreferredEdge:NSMinYEdge];
+	pbcell = (NSPopUpButtonCell *) [c->pb cell];
+	[pbcell setArrowPosition:NSPopUpArrowAtBottom];
+	// the font defined by Interface Builder is Menu 13, which is lol
+	// just use the regular control size for consistency
+	uiDarwinSetControlFont(c->pb, NSRegularControlSize);
+
+	// NSPopUpButton doesn't work like a combobox
+	// - it automatically selects the first item
+	// - it doesn't support duplicates
+	// but we can use a NSArrayController and Cocoa bindings to bypass these restrictions
+	c->pbac = [NSArrayController new];
+	[c->pbac setAvoidsEmptySelection:NO];
+	[c->pbac setSelectsInsertedObjects:NO];
+	[c->pbac setAutomaticallyRearrangesObjects:NO];
+	[c->pb bind:@"contentValues"
+		toObject:c->pbac
+		withKeyPath:@"arrangedObjects"
+		options:nil];
+	[c->pb bind:@"selectedIndex"
+		toObject:c->pbac
+		withKeyPath:@"selectionIndex"
+		options:nil];
+
+	if (comboboxDelegate == nil) {
+		comboboxDelegate = [[comboboxDelegateClass new] autorelease];
+		[delegates addObject:comboboxDelegate];
+	}
+	[comboboxDelegate registerCombobox:c];
+	uiComboboxOnSelected(c, defaultOnSelected, NULL);
+
+	return c;
+}
diff --git a/src/libui_sdl/libui/darwin/control.m b/src/libui_sdl/libui/darwin/control.m
new file mode 100644
index 0000000..9eaf47a
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/control.m
@@ -0,0 +1,84 @@
+// 16 august 2015
+#import "uipriv_darwin.h"
+
+void uiDarwinControlSyncEnableState(uiDarwinControl *c, int state)
+{
+	(*(c->SyncEnableState))(c, state);
+}
+
+void uiDarwinControlSetSuperview(uiDarwinControl *c, NSView *superview)
+{
+	(*(c->SetSuperview))(c, superview);
+}
+
+BOOL uiDarwinControlHugsTrailingEdge(uiDarwinControl *c)
+{
+	return (*(c->HugsTrailingEdge))(c);
+}
+
+BOOL uiDarwinControlHugsBottom(uiDarwinControl *c)
+{
+	return (*(c->HugsBottom))(c);
+}
+
+void uiDarwinControlChildEdgeHuggingChanged(uiDarwinControl *c)
+{
+	(*(c->ChildEdgeHuggingChanged))(c);
+}
+
+NSLayoutPriority uiDarwinControlHuggingPriority(uiDarwinControl *c, NSLayoutConstraintOrientation orientation)
+{
+	return (*(c->HuggingPriority))(c, orientation);
+}
+
+void uiDarwinControlSetHuggingPriority(uiDarwinControl *c, NSLayoutPriority priority, NSLayoutConstraintOrientation orientation)
+{
+	(*(c->SetHuggingPriority))(c, priority, orientation);
+}
+
+void uiDarwinControlChildVisibilityChanged(uiDarwinControl *c)
+{
+	(*(c->ChildVisibilityChanged))(c);
+}
+
+void uiDarwinSetControlFont(NSControl *c, NSControlSize size)
+{
+	[c setFont:[NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:size]]];
+}
+
+#define uiDarwinControlSignature 0x44617277
+
+uiDarwinControl *uiDarwinAllocControl(size_t n, uint32_t typesig, const char *typenamestr)
+{
+	return uiDarwinControl(uiAllocControl(n, uiDarwinControlSignature, typesig, typenamestr));
+}
+
+BOOL uiDarwinShouldStopSyncEnableState(uiDarwinControl *c, BOOL enabled)
+{
+	int ce;
+
+	ce = uiControlEnabled(uiControl(c));
+	// only stop if we're going from disabled back to enabled; don't stop under any other condition
+	// (if we stop when going from enabled to disabled then enabled children of a disabled control won't get disabled at the OS level)
+	if (!ce && enabled)
+		return YES;
+	return NO;
+}
+
+void uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl *c)
+{
+	uiControl *parent;
+
+	parent = uiControlParent(uiControl(c));
+	if (parent != NULL)
+		uiDarwinControlChildEdgeHuggingChanged(uiDarwinControl(parent));
+}
+
+void uiDarwinNotifyVisibilityChanged(uiDarwinControl *c)
+{
+	uiControl *parent;
+
+	parent = uiControlParent(uiControl(c));
+	if (parent != NULL)
+		uiDarwinControlChildVisibilityChanged(uiDarwinControl(parent));
+}
diff --git a/src/libui_sdl/libui/darwin/datetimepicker.m b/src/libui_sdl/libui/darwin/datetimepicker.m
new file mode 100644
index 0000000..44364d9
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/datetimepicker.m
@@ -0,0 +1,42 @@
+// 14 august 2015
+#import "uipriv_darwin.h"
+
+struct uiDateTimePicker {
+	uiDarwinControl c;
+	NSDatePicker *dp;
+};
+
+uiDarwinControlAllDefaults(uiDateTimePicker, dp)
+
+static uiDateTimePicker *finishNewDateTimePicker(NSDatePickerElementFlags elements)
+{
+	uiDateTimePicker *d;
+
+	uiDarwinNewControl(uiDateTimePicker, d);
+
+	d->dp = [[NSDatePicker alloc] initWithFrame:NSZeroRect];
+	[d->dp setBordered:NO];
+	[d->dp setBezeled:YES];
+	[d->dp setDrawsBackground:YES];
+	[d->dp setDatePickerStyle:NSTextFieldAndStepperDatePickerStyle];
+	[d->dp setDatePickerElements:elements];
+	[d->dp setDatePickerMode:NSSingleDateMode];
+	uiDarwinSetControlFont(d->dp, NSRegularControlSize);
+
+	return d;
+}
+
+uiDateTimePicker *uiNewDateTimePicker(void)
+{
+	return finishNewDateTimePicker(NSYearMonthDayDatePickerElementFlag | NSHourMinuteSecondDatePickerElementFlag);
+}
+
+uiDateTimePicker *uiNewDatePicker(void)
+{
+	return finishNewDateTimePicker(NSYearMonthDayDatePickerElementFlag);
+}
+
+uiDateTimePicker *uiNewTimePicker(void)
+{
+	return finishNewDateTimePicker(NSHourMinuteSecondDatePickerElementFlag);
+}
diff --git a/src/libui_sdl/libui/darwin/debug.m b/src/libui_sdl/libui/darwin/debug.m
new file mode 100644
index 0000000..c91c6a7
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/debug.m
@@ -0,0 +1,19 @@
+// 13 may 2016
+#import "uipriv_darwin.h"
+
+// LONGTERM don't halt on release builds
+
+void realbug(const char *file, const char *line, const char *func, const char *prefix, const char *format, va_list ap)
+{
+	NSMutableString *str;
+	NSString *formatted;
+
+	str = [NSMutableString new];
+	[str appendString:[NSString stringWithFormat:@"[libui] %s:%s:%s() %s", file, line, func, prefix]];
+	formatted = [[NSString alloc] initWithFormat:[NSString stringWithUTF8String:format] arguments:ap];
+	[str appendString:formatted];
+	[formatted release];
+	NSLog(@"%@", str);
+	[str release];
+	__builtin_trap();
+}
diff --git a/src/libui_sdl/libui/darwin/draw.m b/src/libui_sdl/libui/darwin/draw.m
new file mode 100644
index 0000000..262ad3e
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/draw.m
@@ -0,0 +1,454 @@
+// 6 september 2015
+#import "uipriv_darwin.h"
+
+struct uiDrawPath {
+	CGMutablePathRef path;
+	uiDrawFillMode fillMode;
+	BOOL ended;
+};
+
+uiDrawPath *uiDrawNewPath(uiDrawFillMode mode)
+{
+	uiDrawPath *p;
+
+	p = uiNew(uiDrawPath);
+	p->path = CGPathCreateMutable();
+	p->fillMode = mode;
+	return p;
+}
+
+void uiDrawFreePath(uiDrawPath *p)
+{
+	CGPathRelease((CGPathRef) (p->path));
+	uiFree(p);
+}
+
+void uiDrawPathNewFigure(uiDrawPath *p, double x, double y)
+{
+	if (p->ended)
+		userbug("You cannot call uiDrawPathNewFigure() on a uiDrawPath that has already been ended. (path; %p)", p);
+	CGPathMoveToPoint(p->path, NULL, x, y);
+}
+
+void uiDrawPathNewFigureWithArc(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double sweep, int negative)
+{
+	double sinStart, cosStart;
+	double startx, starty;
+
+	if (p->ended)
+		userbug("You cannot call uiDrawPathNewFigureWithArc() on a uiDrawPath that has already been ended. (path; %p)", p);
+	sinStart = sin(startAngle);
+	cosStart = cos(startAngle);
+	startx = xCenter + radius * cosStart;
+	starty = yCenter + radius * sinStart;
+	CGPathMoveToPoint(p->path, NULL, startx, starty);
+	uiDrawPathArcTo(p, xCenter, yCenter, radius, startAngle, sweep, negative);
+}
+
+void uiDrawPathLineTo(uiDrawPath *p, double x, double y)
+{
+	// TODO refine this to require being in a path
+	if (p->ended)
+		implbug("attempt to add line to ended path in uiDrawPathLineTo()");
+	CGPathAddLineToPoint(p->path, NULL, x, y);
+}
+
+void uiDrawPathArcTo(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double sweep, int negative)
+{
+	bool cw;
+
+	// TODO likewise
+	if (p->ended)
+		implbug("attempt to add arc to ended path in uiDrawPathArcTo()");
+	if (sweep > 2 * uiPi)
+		sweep = 2 * uiPi;
+	cw = false;
+	if (negative)
+		cw = true;
+	CGPathAddArc(p->path, NULL,
+		xCenter, yCenter,
+		radius,
+		startAngle, startAngle + sweep,
+		cw);
+}
+
+void uiDrawPathBezierTo(uiDrawPath *p, double c1x, double c1y, double c2x, double c2y, double endX, double endY)
+{
+	// TODO likewise
+	if (p->ended)
+		implbug("attempt to add bezier to ended path in uiDrawPathBezierTo()");
+	CGPathAddCurveToPoint(p->path, NULL,
+		c1x, c1y,
+		c2x, c2y,
+		endX, endY);
+}
+
+void uiDrawPathCloseFigure(uiDrawPath *p)
+{
+	// TODO likewise
+	if (p->ended)
+		implbug("attempt to close figure of ended path in uiDrawPathCloseFigure()");
+	CGPathCloseSubpath(p->path);
+}
+
+void uiDrawPathAddRectangle(uiDrawPath *p, double x, double y, double width, double height)
+{
+	if (p->ended)
+		userbug("You cannot call uiDrawPathAddRectangle() on a uiDrawPath that has already been ended. (path; %p)", p);
+	CGPathAddRect(p->path, NULL, CGRectMake(x, y, width, height));
+}
+
+void uiDrawPathEnd(uiDrawPath *p)
+{
+	p->ended = TRUE;
+}
+
+struct uiDrawContext {
+	CGContextRef c;
+	CGFloat height;				// needed for text; see below
+};
+
+uiDrawContext *newContext(CGContextRef ctxt, CGFloat height)
+{
+	uiDrawContext *c;
+
+	c = uiNew(uiDrawContext);
+	c->c = ctxt;
+	c->height = height;
+	return c;
+}
+
+void freeContext(uiDrawContext *c)
+{
+	uiFree(c);
+}
+
+// a stroke is identical to a fill of a stroked path
+// we need to do this in order to stroke with a gradient; see http://stackoverflow.com/a/25034854/3408572
+// doing this for other brushes works too
+void uiDrawStroke(uiDrawContext *c, uiDrawPath *path, uiDrawBrush *b, uiDrawStrokeParams *p)
+{
+	CGLineCap cap;
+	CGLineJoin join;
+	CGPathRef dashPath;
+	CGFloat *dashes;
+	size_t i;
+	uiDrawPath p2;
+
+	if (!path->ended)
+		userbug("You cannot call uiDrawStroke() on a uiDrawPath that has not been ended. (path: %p)", path);
+
+	switch (p->Cap) {
+	case uiDrawLineCapFlat:
+		cap = kCGLineCapButt;
+		break;
+	case uiDrawLineCapRound:
+		cap = kCGLineCapRound;
+		break;
+	case uiDrawLineCapSquare:
+		cap = kCGLineCapSquare;
+		break;
+	}
+	switch (p->Join) {
+	case uiDrawLineJoinMiter:
+		join = kCGLineJoinMiter;
+		break;
+	case uiDrawLineJoinRound:
+		join = kCGLineJoinRound;
+		break;
+	case uiDrawLineJoinBevel:
+		join = kCGLineJoinBevel;
+		break;
+	}
+
+	// create a temporary path identical to the previous one
+	dashPath = (CGPathRef) path->path;
+	if (p->NumDashes != 0) {
+		dashes = (CGFloat *) uiAlloc(p->NumDashes * sizeof (CGFloat), "CGFloat[]");
+		for (i = 0; i < p->NumDashes; i++)
+			dashes[i] = p->Dashes[i];
+		dashPath = CGPathCreateCopyByDashingPath(path->path,
+			NULL,
+			p->DashPhase,
+			dashes,
+			p->NumDashes);
+		uiFree(dashes);
+	}
+	// the documentation is wrong: this produces a path suitable for calling CGPathCreateCopyByStrokingPath(), not for filling directly
+	// the cast is safe; we never modify the CGPathRef and always cast it back to a CGPathRef anyway
+	p2.path = (CGMutablePathRef) CGPathCreateCopyByStrokingPath(dashPath,
+		NULL,
+		p->Thickness,
+		cap,
+		join,
+		p->MiterLimit);
+	if (p->NumDashes != 0)
+		CGPathRelease(dashPath);
+
+	// always draw stroke fills using the winding rule
+	// otherwise intersecting figures won't draw correctly
+	p2.fillMode = uiDrawFillModeWinding;
+	p2.ended = path->ended;
+	uiDrawFill(c, &p2, b);
+	// and clean up
+	CGPathRelease((CGPathRef) (p2.path));
+}
+
+// for a solid fill, we can merely have Core Graphics fill directly
+static void fillSolid(CGContextRef ctxt, uiDrawPath *p, uiDrawBrush *b)
+{
+	// TODO this uses DeviceRGB; switch to sRGB
+	CGContextSetRGBFillColor(ctxt, b->R, b->G, b->B, b->A);
+	switch (p->fillMode) {
+	case uiDrawFillModeWinding:
+		CGContextFillPath(ctxt);
+		break;
+	case uiDrawFillModeAlternate:
+		CGContextEOFillPath(ctxt);
+		break;
+	}
+}
+
+// for a gradient fill, we need to clip to the path and then draw the gradient
+// see http://stackoverflow.com/a/25034854/3408572
+static void fillGradient(CGContextRef ctxt, uiDrawPath *p, uiDrawBrush *b)
+{
+	CGGradientRef gradient;
+	CGColorSpaceRef colorspace;
+	CGFloat *colors;
+	CGFloat *locations;
+	size_t i;
+
+	// gradients need a color space
+	// for consistency with windows, use sRGB
+	colorspace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
+
+	// make the gradient
+	colors = uiAlloc(b->NumStops * 4 * sizeof (CGFloat), "CGFloat[]");
+	locations = uiAlloc(b->NumStops * sizeof (CGFloat), "CGFloat[]");
+	for (i = 0; i < b->NumStops; i++) {
+		colors[i * 4 + 0] = b->Stops[i].R;
+		colors[i * 4 + 1] = b->Stops[i].G;
+		colors[i * 4 + 2] = b->Stops[i].B;
+		colors[i * 4 + 3] = b->Stops[i].A;
+		locations[i] = b->Stops[i].Pos;
+	}
+	gradient = CGGradientCreateWithColorComponents(colorspace, colors, locations, b->NumStops);
+	uiFree(locations);
+	uiFree(colors);
+
+	// because we're mucking with clipping, we need to save the graphics state and restore it later
+	CGContextSaveGState(ctxt);
+
+	// clip
+	switch (p->fillMode) {
+	case uiDrawFillModeWinding:
+		CGContextClip(ctxt);
+		break;
+	case uiDrawFillModeAlternate:
+		CGContextEOClip(ctxt);
+		break;
+	}
+
+	// draw the gradient
+	switch (b->Type) {
+	case uiDrawBrushTypeLinearGradient:
+		CGContextDrawLinearGradient(ctxt,
+			gradient,
+			CGPointMake(b->X0, b->Y0),
+			CGPointMake(b->X1, b->Y1),
+			kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
+		break;
+	case uiDrawBrushTypeRadialGradient:
+		CGContextDrawRadialGradient(ctxt,
+			gradient,
+			CGPointMake(b->X0, b->Y0),
+			// make the start circle radius 0 to make it a point
+			0,
+			CGPointMake(b->X1, b->Y1),
+			b->OuterRadius,
+			kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
+		break;
+	}
+
+	// and clean up
+	CGContextRestoreGState(ctxt);
+	CGGradientRelease(gradient);
+	CGColorSpaceRelease(colorspace);
+}
+
+void uiDrawFill(uiDrawContext *c, uiDrawPath *path, uiDrawBrush *b)
+{
+	if (!path->ended)
+		userbug("You cannot call uiDrawStroke() on a uiDrawPath that has not been ended. (path: %p)", path);
+	CGContextAddPath(c->c, (CGPathRef) (path->path));
+	switch (b->Type) {
+	case uiDrawBrushTypeSolid:
+		fillSolid(c->c, path, b);
+		return;
+	case uiDrawBrushTypeLinearGradient:
+	case uiDrawBrushTypeRadialGradient:
+		fillGradient(c->c, path, b);
+		return;
+//	case uiDrawBrushTypeImage:
+		// TODO
+		return;
+	}
+	userbug("Unknown brush type %d passed to uiDrawFill().", b->Type);
+}
+
+static void m2c(uiDrawMatrix *m, CGAffineTransform *c)
+{
+	c->a = m->M11;
+	c->b = m->M12;
+	c->c = m->M21;
+	c->d = m->M22;
+	c->tx = m->M31;
+	c->ty = m->M32;
+}
+
+static void c2m(CGAffineTransform *c, uiDrawMatrix *m)
+{
+	m->M11 = c->a;
+	m->M12 = c->b;
+	m->M21 = c->c;
+	m->M22 = c->d;
+	m->M31 = c->tx;
+	m->M32 = c->ty;
+}
+
+void uiDrawMatrixTranslate(uiDrawMatrix *m, double x, double y)
+{
+	CGAffineTransform c;
+
+	m2c(m, &c);
+	c = CGAffineTransformTranslate(c, x, y);
+	c2m(&c, m);
+}
+
+void uiDrawMatrixScale(uiDrawMatrix *m, double xCenter, double yCenter, double x, double y)
+{
+	CGAffineTransform c;
+	double xt, yt;
+
+	m2c(m, &c);
+	xt = x;
+	yt = y;
+	scaleCenter(xCenter, yCenter, &xt, &yt);
+	c = CGAffineTransformTranslate(c, xt, yt);
+	c = CGAffineTransformScale(c, x, y);
+	c = CGAffineTransformTranslate(c, -xt, -yt);
+	c2m(&c, m);
+}
+
+void uiDrawMatrixRotate(uiDrawMatrix *m, double x, double y, double amount)
+{
+	CGAffineTransform c;
+
+	m2c(m, &c);
+	c = CGAffineTransformTranslate(c, x, y);
+	c = CGAffineTransformRotate(c, amount);
+	c = CGAffineTransformTranslate(c, -x, -y);
+	c2m(&c, m);
+}
+
+void uiDrawMatrixSkew(uiDrawMatrix *m, double x, double y, double xamount, double yamount)
+{
+	fallbackSkew(m, x, y, xamount, yamount);
+}
+
+void uiDrawMatrixMultiply(uiDrawMatrix *dest, uiDrawMatrix *src)
+{
+	CGAffineTransform c;
+	CGAffineTransform d;
+
+	m2c(dest, &c);
+	m2c(src, &d);
+	c = CGAffineTransformConcat(c, d);
+	c2m(&c, dest);
+}
+
+// there is no test for invertibility; CGAffineTransformInvert() is merely documented as returning the matrix unchanged if it isn't invertible
+// therefore, special care must be taken to catch matrices who are their own inverses
+// TODO figure out which matrices these are and do so
+int uiDrawMatrixInvertible(uiDrawMatrix *m)
+{
+	CGAffineTransform c, d;
+
+	m2c(m, &c);
+	d = CGAffineTransformInvert(c);
+	return CGAffineTransformEqualToTransform(c, d) == false;
+}
+
+int uiDrawMatrixInvert(uiDrawMatrix *m)
+{
+	CGAffineTransform c, d;
+
+	m2c(m, &c);
+	d = CGAffineTransformInvert(c);
+	if (CGAffineTransformEqualToTransform(c, d))
+		return 0;
+	c2m(&d, m);
+	return 1;
+}
+
+void uiDrawMatrixTransformPoint(uiDrawMatrix *m, double *x, double *y)
+{
+	CGAffineTransform c;
+	CGPoint p;
+
+	m2c(m, &c);
+	p = CGPointApplyAffineTransform(CGPointMake(*x, *y), c);
+	*x = p.x;
+	*y = p.y;
+}
+
+void uiDrawMatrixTransformSize(uiDrawMatrix *m, double *x, double *y)
+{
+	CGAffineTransform c;
+	CGSize s;
+
+	m2c(m, &c);
+	s = CGSizeApplyAffineTransform(CGSizeMake(*x, *y), c);
+	*x = s.width;
+	*y = s.height;
+}
+
+void uiDrawTransform(uiDrawContext *c, uiDrawMatrix *m)
+{
+	CGAffineTransform cm;
+
+	m2c(m, &cm);
+	CGContextConcatCTM(c->c, cm);
+}
+
+void uiDrawClip(uiDrawContext *c, uiDrawPath *path)
+{
+	if (!path->ended)
+		userbug("You cannot call uiDrawCilp() on a uiDrawPath that has not been ended. (path: %p)", path);
+	CGContextAddPath(c->c, (CGPathRef) (path->path));
+	switch (path->fillMode) {
+	case uiDrawFillModeWinding:
+		CGContextClip(c->c);
+		break;
+	case uiDrawFillModeAlternate:
+		CGContextEOClip(c->c);
+		break;
+	}
+}
+
+// TODO figure out what besides transforms these save/restore on all platforms
+void uiDrawSave(uiDrawContext *c)
+{
+	CGContextSaveGState(c->c);
+}
+
+void uiDrawRestore(uiDrawContext *c)
+{
+	CGContextRestoreGState(c->c);
+}
+
+void uiDrawText(uiDrawContext *c, double x, double y, uiDrawTextLayout *layout)
+{
+	doDrawText(c->c, c->height, x, y, layout);
+}
diff --git a/src/libui_sdl/libui/darwin/drawtext.m b/src/libui_sdl/libui/darwin/drawtext.m
new file mode 100644
index 0000000..c376536
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/drawtext.m
@@ -0,0 +1,655 @@
+// 6 september 2015
+#import "uipriv_darwin.h"
+
+// TODO
+#define complain(...) implbug(__VA_ARGS__)
+
+// TODO double-check that we are properly handling allocation failures (or just toll free bridge from cocoa)
+struct uiDrawFontFamilies {
+	CFArrayRef fonts;
+};
+
+uiDrawFontFamilies *uiDrawListFontFamilies(void)
+{
+	uiDrawFontFamilies *ff;
+
+	ff = uiNew(uiDrawFontFamilies);
+	ff->fonts = CTFontManagerCopyAvailableFontFamilyNames();
+	if (ff->fonts == NULL)
+		implbug("error getting available font names (no reason specified) (TODO)");
+	return ff;
+}
+
+int uiDrawFontFamiliesNumFamilies(uiDrawFontFamilies *ff)
+{
+	return CFArrayGetCount(ff->fonts);
+}
+
+char *uiDrawFontFamiliesFamily(uiDrawFontFamilies *ff, int n)
+{
+	CFStringRef familystr;
+	char *family;
+
+	familystr = (CFStringRef) CFArrayGetValueAtIndex(ff->fonts, n);
+	// toll-free bridge
+	family = uiDarwinNSStringToText((NSString *) familystr);
+	// Get Rule means we do not free familystr
+	return family;
+}
+
+void uiDrawFreeFontFamilies(uiDrawFontFamilies *ff)
+{
+	CFRelease(ff->fonts);
+	uiFree(ff);
+}
+
+struct uiDrawTextFont {
+	CTFontRef f;
+};
+
+uiDrawTextFont *mkTextFont(CTFontRef f, BOOL retain)
+{
+	uiDrawTextFont *font;
+
+	font = uiNew(uiDrawTextFont);
+	font->f = f;
+	if (retain)
+		CFRetain(font->f);
+	return font;
+}
+
+uiDrawTextFont *mkTextFontFromNSFont(NSFont *f)
+{
+	// toll-free bridging; we do retain, though
+	return mkTextFont((CTFontRef) f, YES);
+}
+
+static CFMutableDictionaryRef newAttrList(void)
+{
+	CFMutableDictionaryRef attr;
+
+	attr = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+	if (attr == NULL)
+		complain("error creating attribute dictionary in newAttrList()()");
+	return attr;
+}
+
+static void addFontFamilyAttr(CFMutableDictionaryRef attr, const char *family)
+{
+	CFStringRef cfstr;
+
+	cfstr = CFStringCreateWithCString(NULL, family, kCFStringEncodingUTF8);
+	if (cfstr == NULL)
+		complain("error creating font family name CFStringRef in addFontFamilyAttr()");
+	CFDictionaryAddValue(attr, kCTFontFamilyNameAttribute, cfstr);
+	CFRelease(cfstr);			// dictionary holds its own reference
+}
+
+static void addFontSizeAttr(CFMutableDictionaryRef attr, double size)
+{
+	CFNumberRef n;
+
+	n = CFNumberCreate(NULL, kCFNumberDoubleType, &size);
+	CFDictionaryAddValue(attr, kCTFontSizeAttribute, n);
+	CFRelease(n);
+}
+
+#if 0
+TODO
+// See http://stackoverflow.com/questions/4810409/does-coretext-support-small-caps/4811371#4811371 and https://git.gnome.org/browse/pango/tree/pango/pangocoretext-fontmap.c for what these do
+// And fortunately, unlike the traits (see below), unmatched features are simply ignored without affecting the other features :D
+static void addFontSmallCapsAttr(CFMutableDictionaryRef attr)
+{
+	CFMutableArrayRef outerArray;
+	CFMutableDictionaryRef innerDict;
+	CFNumberRef numType, numSelector;
+	int num;
+
+	outerArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
+	if (outerArray == NULL)
+		complain("error creating outer CFArray for adding small caps attributes in addFontSmallCapsAttr()");
+
+	// Apple's headers say these are deprecated, but a few fonts still rely on them
+	num = kLetterCaseType;
+	numType = CFNumberCreate(NULL, kCFNumberIntType, &num);
+	num = kSmallCapsSelector;
+	numSelector = CFNumberCreate(NULL, kCFNumberIntType, &num);
+	innerDict = newAttrList();
+	CFDictionaryAddValue(innerDict, kCTFontFeatureTypeIdentifierKey, numType);
+	CFRelease(numType);
+	CFDictionaryAddValue(innerDict, kCTFontFeatureSelectorIdentifierKey, numSelector);
+	CFRelease(numSelector);
+	CFArrayAppendValue(outerArray, innerDict);
+	CFRelease(innerDict);		// and likewise for CFArray
+
+	// these are the non-deprecated versions of the above; some fonts have these instead
+	num = kLowerCaseType;
+	numType = CFNumberCreate(NULL, kCFNumberIntType, &num);
+	num = kLowerCaseSmallCapsSelector;
+	numSelector = CFNumberCreate(NULL, kCFNumberIntType, &num);
+	innerDict = newAttrList();
+	CFDictionaryAddValue(innerDict, kCTFontFeatureTypeIdentifierKey, numType);
+	CFRelease(numType);
+	CFDictionaryAddValue(innerDict, kCTFontFeatureSelectorIdentifierKey, numSelector);
+	CFRelease(numSelector);
+	CFArrayAppendValue(outerArray, innerDict);
+	CFRelease(innerDict);		// and likewise for CFArray
+
+	CFDictionaryAddValue(attr, kCTFontFeatureSettingsAttribute, outerArray);
+	CFRelease(outerArray);
+}
+#endif
+
+// Named constants for these were NOT added until 10.11, and even then they were added as external symbols instead of macros, so we can't use them directly :(
+// kode54 got these for me before I had access to El Capitan; thanks to him.
+#define ourNSFontWeightUltraLight -0.800000
+#define ourNSFontWeightThin -0.600000
+#define ourNSFontWeightLight -0.400000
+#define ourNSFontWeightRegular 0.000000
+#define ourNSFontWeightMedium 0.230000
+#define ourNSFontWeightSemibold 0.300000
+#define ourNSFontWeightBold 0.400000
+#define ourNSFontWeightHeavy 0.560000
+#define ourNSFontWeightBlack 0.620000
+static const CGFloat ctWeights[] = {
+	// yeah these two have their names swapped; blame Pango
+	[uiDrawTextWeightThin] = ourNSFontWeightUltraLight,
+	[uiDrawTextWeightUltraLight] = ourNSFontWeightThin,
+	[uiDrawTextWeightLight] = ourNSFontWeightLight,
+	// for this one let's go between Light and Regular
+	// we're doing nearest so if there happens to be an exact value hopefully it's close enough
+	[uiDrawTextWeightBook] = ourNSFontWeightLight + ((ourNSFontWeightRegular - ourNSFontWeightLight) / 2),
+	[uiDrawTextWeightNormal] = ourNSFontWeightRegular,
+	[uiDrawTextWeightMedium] = ourNSFontWeightMedium,
+	[uiDrawTextWeightSemiBold] = ourNSFontWeightSemibold,
+	[uiDrawTextWeightBold] = ourNSFontWeightBold,
+	// for this one let's go between Bold and Heavy
+	[uiDrawTextWeightUltraBold] = ourNSFontWeightBold + ((ourNSFontWeightHeavy - ourNSFontWeightBold) / 2),
+	[uiDrawTextWeightHeavy] = ourNSFontWeightHeavy,
+	[uiDrawTextWeightUltraHeavy] = ourNSFontWeightBlack,
+};
+
+// Unfortunately there are still no named constants for these.
+// Let's just use normalized widths.
+// As far as I can tell (OS X only ships with condensed fonts, not expanded fonts; TODO), regardless of condensed or expanded, negative means condensed and positive means expanded.
+// TODO verify this is correct
+static const CGFloat ctStretches[] = {
+	[uiDrawTextStretchUltraCondensed] = -1.0,
+	[uiDrawTextStretchExtraCondensed] = -0.75,
+	[uiDrawTextStretchCondensed] = -0.5,
+	[uiDrawTextStretchSemiCondensed] = -0.25,
+	[uiDrawTextStretchNormal] = 0.0,
+	[uiDrawTextStretchSemiExpanded] = 0.25,
+	[uiDrawTextStretchExpanded] = 0.5,
+	[uiDrawTextStretchExtraExpanded] = 0.75,
+	[uiDrawTextStretchUltraExpanded] = 1.0,
+};
+
+struct closeness {
+	CFIndex index;
+	CGFloat weight;
+	CGFloat italic;
+	CGFloat stretch;
+	CGFloat distance;
+};
+
+// Stupidity: CTFont requires an **exact match for the entire traits dictionary**, otherwise it will **drop ALL the traits**.
+// We have to implement the closest match ourselves.
+// Also we have to do this before adding the small caps flags, because the matching descriptors won't have those.
+CTFontDescriptorRef matchTraits(CTFontDescriptorRef against, uiDrawTextWeight weight, uiDrawTextItalic italic, uiDrawTextStretch stretch)
+{
+	CGFloat targetWeight;
+	CGFloat italicCloseness, obliqueCloseness, normalCloseness;
+	CGFloat targetStretch;
+	CFArrayRef matching;
+	CFIndex i, n;
+	struct closeness *closeness;
+	CTFontDescriptorRef current;
+	CTFontDescriptorRef out;
+
+	targetWeight = ctWeights[weight];
+	switch (italic) {
+	case uiDrawTextItalicNormal:
+		italicCloseness = 1;
+		obliqueCloseness = 1;
+		normalCloseness = 0;
+		break;
+	case uiDrawTextItalicOblique:
+		italicCloseness = 0.5;
+		obliqueCloseness = 0;
+		normalCloseness = 1;
+		break;
+	case uiDrawTextItalicItalic:
+		italicCloseness = 0;
+		obliqueCloseness = 0.5;
+		normalCloseness = 1;
+		break;
+	}
+	targetStretch = ctStretches[stretch];
+
+	matching = CTFontDescriptorCreateMatchingFontDescriptors(against, NULL);
+	if (matching == NULL)
+		// no matches; give the original back and hope for the best
+		return against;
+	n = CFArrayGetCount(matching);
+	if (n == 0) {
+		// likewise
+		CFRelease(matching);
+		return against;
+	}
+
+	closeness = (struct closeness *) uiAlloc(n * sizeof (struct closeness), "struct closeness[]");
+	for (i = 0; i < n; i++) {
+		CFDictionaryRef traits;
+		CFNumberRef cfnum;
+		CTFontSymbolicTraits symbolic;
+
+		closeness[i].index = i;
+
+		current = CFArrayGetValueAtIndex(matching, i);
+		traits = CTFontDescriptorCopyAttribute(current, kCTFontTraitsAttribute);
+		if (traits == NULL) {
+			// couldn't get traits; be safe by ranking it lowest
+			// LONGTERM figure out what the longest possible distances are
+			closeness[i].weight = 3;
+			closeness[i].italic = 2;
+			closeness[i].stretch = 3;
+			continue;
+		}
+
+		symbolic = 0;			// assume no symbolic traits if none are listed
+		cfnum = CFDictionaryGetValue(traits, kCTFontSymbolicTrait);
+		if (cfnum != NULL) {
+			SInt32 s;
+
+			if (CFNumberGetValue(cfnum, kCFNumberSInt32Type, &s) == false)
+				complain("error getting symbolic traits in matchTraits()");
+			symbolic = (CTFontSymbolicTraits) s;
+			// Get rule; do not release cfnum
+		}
+
+		// now try weight
+		cfnum = CFDictionaryGetValue(traits, kCTFontWeightTrait);
+		if (cfnum != NULL) {
+			CGFloat val;
+
+			// LONGTERM instead of complaining for this and width and possibly also symbolic traits above, should we just fall through to the default?
+			if (CFNumberGetValue(cfnum, kCFNumberCGFloatType, &val) == false)
+				complain("error getting weight value in matchTraits()");
+			closeness[i].weight = val - targetWeight;
+		} else
+			// okay there's no weight key; let's try the literal meaning of the symbolic constant
+			// LONGTERM is the weight key guaranteed?
+			if ((symbolic & kCTFontBoldTrait) != 0)
+				closeness[i].weight = ourNSFontWeightBold - targetWeight;
+			else
+				closeness[i].weight = ourNSFontWeightRegular - targetWeight;
+
+		// italics is a bit harder because Core Text doesn't expose a concept of obliqueness
+		// Pango just does a g_strrstr() (backwards case-sensitive search) for "Oblique" in the font's style name (see https://git.gnome.org/browse/pango/tree/pango/pangocoretext-fontmap.c); let's do that too I guess
+		if ((symbolic & kCTFontItalicTrait) != 0)
+			closeness[i].italic = italicCloseness;
+		else {
+			CFStringRef styleName;
+			BOOL isOblique;
+
+			isOblique = NO;		// default value
+			styleName = CTFontDescriptorCopyAttribute(current, kCTFontStyleNameAttribute);
+			if (styleName != NULL) {
+				CFRange range;
+
+				// note the use of the toll-free bridge for the string literal, since CFSTR() *can* return NULL
+				range = CFStringFind(styleName, (CFStringRef) @"Oblique", kCFCompareBackwards);
+				if (range.location != kCFNotFound)
+					isOblique = YES;
+				CFRelease(styleName);
+			}
+			if (isOblique)
+				closeness[i].italic = obliqueCloseness;
+			else
+				closeness[i].italic = normalCloseness;
+		}
+
+		// now try width
+		// TODO this does not seem to be enough for Skia's extended variants; the width trait is 0 but the Expanded flag is on
+		// TODO verify the rest of this matrix (what matrix?)
+		cfnum = CFDictionaryGetValue(traits, kCTFontWidthTrait);
+		if (cfnum != NULL) {
+			CGFloat val;
+
+			if (CFNumberGetValue(cfnum, kCFNumberCGFloatType, &val) == false)
+				complain("error getting width value in matchTraits()");
+			closeness[i].stretch = val - targetStretch;
+		} else
+			// okay there's no width key; let's try the literal meaning of the symbolic constant
+			// LONGTERM is the width key guaranteed?
+			if ((symbolic & kCTFontExpandedTrait) != 0)
+				closeness[i].stretch = 1.0 - targetStretch;
+			else if ((symbolic & kCTFontCondensedTrait) != 0)
+				closeness[i].stretch = -1.0 - targetStretch;
+			else
+				closeness[i].stretch = 0.0 - targetStretch;
+
+		CFRelease(traits);
+	}
+
+	// now figure out the 3-space difference between the three and sort by that
+	for (i = 0; i < n; i++) {
+		CGFloat weight, italic, stretch;
+
+		weight = closeness[i].weight;
+		weight *= weight;
+		italic = closeness[i].italic;
+		italic *= italic;
+		stretch = closeness[i].stretch;
+		stretch *= stretch;
+		closeness[i].distance = sqrt(weight + italic + stretch);
+	}
+	qsort_b(closeness, n, sizeof (struct closeness), ^(const void *aa, const void *bb) {
+		const struct closeness *a = (const struct closeness *) aa;
+		const struct closeness *b = (const struct closeness *) bb;
+
+		// via http://www.gnu.org/software/libc/manual/html_node/Comparison-Functions.html#Comparison-Functions
+		// LONGTERM is this really the best way? isn't it the same as if (*a < *b) return -1; if (*a > *b) return 1; return 0; ?
+		return (a->distance > b->distance) - (a->distance < b->distance);
+	});
+	// and the first element of the sorted array is what we want
+	out = CFArrayGetValueAtIndex(matching, closeness[0].index);
+	CFRetain(out);			// get rule
+
+	// release everything
+	uiFree(closeness);
+	CFRelease(matching);
+	// and release the original descriptor since we no longer need it
+	CFRelease(against);
+
+	return out;
+}
+
+// Now remember what I said earlier about having to add the small caps traits after calling the above? This gets a dictionary back so we can do so.
+CFMutableDictionaryRef extractAttributes(CTFontDescriptorRef desc)
+{
+	CFDictionaryRef dict;
+	CFMutableDictionaryRef mdict;
+
+	dict = CTFontDescriptorCopyAttributes(desc);
+	// this might not be mutable, so make a mutable copy
+	mdict = CFDictionaryCreateMutableCopy(NULL, 0, dict);
+	CFRelease(dict);
+	return mdict;
+}
+
+uiDrawTextFont *uiDrawLoadClosestFont(const uiDrawTextFontDescriptor *desc)
+{
+	CTFontRef f;
+	CFMutableDictionaryRef attr;
+	CTFontDescriptorRef cfdesc;
+
+	attr = newAttrList();
+	addFontFamilyAttr(attr, desc->Family);
+	addFontSizeAttr(attr, desc->Size);
+
+	// now we have to do the traits matching, so create a descriptor, match the traits, and then get the attributes back
+	cfdesc = CTFontDescriptorCreateWithAttributes(attr);
+	// TODO release attr?
+	cfdesc = matchTraits(cfdesc, desc->Weight, desc->Italic, desc->Stretch);
+
+	// specify the initial size again just to be safe
+	f = CTFontCreateWithFontDescriptor(cfdesc, desc->Size, NULL);
+	// TODO release cfdesc?
+
+	return mkTextFont(f, NO);		// we hold the initial reference; no need to retain again
+}
+
+void uiDrawFreeTextFont(uiDrawTextFont *font)
+{
+	CFRelease(font->f);
+	uiFree(font);
+}
+
+uintptr_t uiDrawTextFontHandle(uiDrawTextFont *font)
+{
+	return (uintptr_t) (font->f);
+}
+
+void uiDrawTextFontDescribe(uiDrawTextFont *font, uiDrawTextFontDescriptor *desc)
+{
+	// TODO
+}
+
+// text sizes and user space points are identical:
+// - https://developer.apple.com/library/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/TypoFeatures/TextSystemFeatures.html#//apple_ref/doc/uid/TP40009459-CH6-51627-BBCCHIFF text points are 72 per inch
+// - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CocoaDrawingGuide/Transforms/Transforms.html#//apple_ref/doc/uid/TP40003290-CH204-SW5 user space points are 72 per inch
+void uiDrawTextFontGetMetrics(uiDrawTextFont *font, uiDrawTextFontMetrics *metrics)
+{
+	metrics->Ascent = CTFontGetAscent(font->f);
+	metrics->Descent = CTFontGetDescent(font->f);
+	metrics->Leading = CTFontGetLeading(font->f);
+	metrics->UnderlinePos = CTFontGetUnderlinePosition(font->f);
+	metrics->UnderlineThickness = CTFontGetUnderlineThickness(font->f);
+}
+
+struct uiDrawTextLayout {
+	CFMutableAttributedStringRef mas;
+	CFRange *charsToRanges;
+	double width;
+};
+
+uiDrawTextLayout *uiDrawNewTextLayout(const char *str, uiDrawTextFont *defaultFont, double width)
+{
+	uiDrawTextLayout *layout;
+	CFAttributedStringRef immutable;
+	CFMutableDictionaryRef attr;
+	CFStringRef backing;
+	CFIndex i, j, n;
+
+	layout = uiNew(uiDrawTextLayout);
+
+	// TODO docs say we need to use a different set of key callbacks
+	// TODO see if the font attribute key callbacks need to be the same
+	attr = newAttrList();
+	// this will retain defaultFont->f; no need to worry
+	CFDictionaryAddValue(attr, kCTFontAttributeName, defaultFont->f);
+
+	immutable = CFAttributedStringCreate(NULL, (CFStringRef) [NSString stringWithUTF8String:str], attr);
+	if (immutable == NULL)
+		complain("error creating immutable attributed string in uiDrawNewTextLayout()");
+	CFRelease(attr);
+
+	layout->mas = CFAttributedStringCreateMutableCopy(NULL, 0, immutable);
+	if (layout->mas == NULL)
+		complain("error creating attributed string in uiDrawNewTextLayout()");
+	CFRelease(immutable);
+
+	uiDrawTextLayoutSetWidth(layout, width);
+
+	// unfortunately the CFRanges for attributes expect UTF-16 codepoints
+	// we want graphemes
+	// fortunately CFStringGetRangeOfComposedCharactersAtIndex() is here for us
+	// https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Strings/Articles/stringsClusters.html says that this does work on all multi-codepoint graphemes (despite the name), and that this is the preferred function for this particular job anyway
+	backing = CFAttributedStringGetString(layout->mas);
+	n = CFStringGetLength(backing);
+	// allocate one extra, just to be safe
+	layout->charsToRanges = (CFRange *) uiAlloc((n + 1) * sizeof (CFRange), "CFRange[]");
+	i = 0;
+	j = 0;
+	while (i < n) {
+		CFRange range;
+
+		range = CFStringGetRangeOfComposedCharactersAtIndex(backing, i);
+		i = range.location + range.length;
+		layout->charsToRanges[j] = range;
+		j++;
+	}
+	// and set the last one
+	layout->charsToRanges[j].location = i;
+	layout->charsToRanges[j].length = 0;
+
+	return layout;
+}
+
+void uiDrawFreeTextLayout(uiDrawTextLayout *layout)
+{
+	uiFree(layout->charsToRanges);
+	CFRelease(layout->mas);
+	uiFree(layout);
+}
+
+void uiDrawTextLayoutSetWidth(uiDrawTextLayout *layout, double width)
+{
+	layout->width = width;
+}
+
+struct framesetter {
+	CTFramesetterRef fs;
+	CFMutableDictionaryRef frameAttrib;
+	CGSize extents;
+};
+
+// TODO CTFrameProgression for RTL/LTR
+// TODO kCTParagraphStyleSpecifierMaximumLineSpacing, kCTParagraphStyleSpecifierMinimumLineSpacing, kCTParagraphStyleSpecifierLineSpacingAdjustment for line spacing
+static void mkFramesetter(uiDrawTextLayout *layout, struct framesetter *fs)
+{
+	CFRange fitRange;
+	CGFloat width;
+
+	fs->fs = CTFramesetterCreateWithAttributedString(layout->mas);
+	if (fs->fs == NULL)
+		complain("error creating CTFramesetter object in mkFramesetter()");
+
+	// TODO kCTFramePathWidthAttributeName?
+	fs->frameAttrib = NULL;
+
+	width = layout->width;
+	if (layout->width < 0)
+		width = CGFLOAT_MAX;
+	// TODO these seem to be floor()'d or truncated?
+	fs->extents = CTFramesetterSuggestFrameSizeWithConstraints(fs->fs,
+		CFRangeMake(0, 0),
+		fs->frameAttrib,
+		CGSizeMake(width, CGFLOAT_MAX),
+		&fitRange);		// not documented as accepting NULL
+}
+
+static void freeFramesetter(struct framesetter *fs)
+{
+	if (fs->frameAttrib != NULL)
+		CFRelease(fs->frameAttrib);
+	CFRelease(fs->fs);
+}
+
+// LONGTERM allow line separation and leading to be factored into a wrapping text layout
+
+// TODO reconcile differences in character wrapping on platforms
+void uiDrawTextLayoutExtents(uiDrawTextLayout *layout, double *width, double *height)
+{
+	struct framesetter fs;
+
+	mkFramesetter(layout, &fs);
+	*width = fs.extents.width;
+	*height = fs.extents.height;
+	freeFramesetter(&fs);
+}
+
+// Core Text doesn't draw onto a flipped view correctly; we have to do this
+// see the iOS bits of the first example at https://developer.apple.com/library/mac/documentation/StringsTextFonts/Conceptual/CoreText_Programming/LayoutOperations/LayoutOperations.html#//apple_ref/doc/uid/TP40005533-CH12-SW1 (iOS is naturally flipped)
+// TODO how is this affected by the CTM?
+static void prepareContextForText(CGContextRef c, CGFloat cheight, double *y)
+{
+	CGContextSaveGState(c);
+	CGContextTranslateCTM(c, 0, cheight);
+	CGContextScaleCTM(c, 1.0, -1.0);
+	CGContextSetTextMatrix(c, CGAffineTransformIdentity);
+
+	// wait, that's not enough; we need to offset y values to account for our new flipping
+	*y = cheight - *y;
+}
+
+// TODO placement is incorrect for Helvetica
+void doDrawText(CGContextRef c, CGFloat cheight, double x, double y, uiDrawTextLayout *layout)
+{
+	struct framesetter fs;
+	CGRect rect;
+	CGPathRef path;
+	CTFrameRef frame;
+
+	prepareContextForText(c, cheight, &y);
+	mkFramesetter(layout, &fs);
+
+	// oh, and since we're flipped, y is the bottom-left coordinate of the rectangle, not the top-left
+	// since we are flipped, we subtract
+	y -= fs.extents.height;
+
+	rect.origin = CGPointMake(x, y);
+	rect.size = fs.extents;
+	path = CGPathCreateWithRect(rect, NULL);
+
+	frame = CTFramesetterCreateFrame(fs.fs,
+		CFRangeMake(0, 0),
+		path,
+		fs.frameAttrib);
+	if (frame == NULL)
+		complain("error creating CTFrame object in doDrawText()");
+	CTFrameDraw(frame, c);
+	CFRelease(frame);
+
+	CFRelease(path);
+
+	freeFramesetter(&fs);
+	CGContextRestoreGState(c);
+}
+
+// LONGTERM provide an equivalent to CTLineGetTypographicBounds() on uiDrawTextLayout?
+
+// LONGTERM keep this for later features and documentation purposes
+#if 0
+		w = CTLineGetTypographicBounds(line, &ascent, &descent, NULL);
+		// though CTLineGetTypographicBounds() returns 0 on error, it also returns 0 on an empty string, so we can't reasonably check for error
+		CFRelease(line);
+
+	// LONGTERM provide a way to get the image bounds as a separate function later
+	bounds = CTLineGetImageBounds(line, c);
+	// though CTLineGetImageBounds() returns CGRectNull on error, it also returns CGRectNull on an empty string, so we can't reasonably check for error
+
+	// CGContextSetTextPosition() positions at the baseline in the case of CTLineDraw(); we need the top-left corner instead
+	CTLineGetTypographicBounds(line, &yoff, NULL, NULL);
+	// remember that we're flipped, so we subtract
+	y -= yoff;
+	CGContextSetTextPosition(c, x, y);
+#endif
+
+static CFRange charsToRange(uiDrawTextLayout *layout, int startChar, int endChar)
+{
+	CFRange start, end;
+	CFRange out;
+
+	start = layout->charsToRanges[startChar];
+	end = layout->charsToRanges[endChar];
+	out.location = start.location;
+	out.length = end.location - start.location;
+	return out;
+}
+
+#define rangeToCFRange() charsToRange(layout, startChar, endChar)
+
+void uiDrawTextLayoutSetColor(uiDrawTextLayout *layout, int startChar, int endChar, double r, double g, double b, double a)
+{
+	CGColorSpaceRef colorspace;
+	CGFloat components[4];
+	CGColorRef color;
+
+	// for consistency with windows, use sRGB
+	colorspace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
+	components[0] = r;
+	components[1] = g;
+	components[2] = b;
+	components[3] = a;
+	color = CGColorCreate(colorspace, components);
+	CGColorSpaceRelease(colorspace);
+
+	CFAttributedStringSetAttribute(layout->mas,
+		rangeToCFRange(),
+		kCTForegroundColorAttributeName,
+		color);
+	CGColorRelease(color);		// TODO safe?
+}
diff --git a/src/libui_sdl/libui/darwin/editablecombo.m b/src/libui_sdl/libui/darwin/editablecombo.m
new file mode 100644
index 0000000..434add7
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/editablecombo.m
@@ -0,0 +1,185 @@
+// 14 august 2015
+#import "uipriv_darwin.h"
+
+// So why did I split uiCombobox into uiCombobox and uiEditableCombobox? Here's (90% of the; the other 10% is GTK+ events) answer:
+// When you type a value into a NSComboBox that just happens to be in the list, it will autoselect that item!
+// I can't seem to find a workaround.
+// Fortunately, there's other weird behaviors that made this split worth it.
+// And besides, selected items make little sense with editable comboboxes... you either separate or combine them with the text entry :V
+
+// NSComboBoxes have no intrinsic width; we'll use the default Interface Builder width for them.
+#define comboboxWidth 96
+
+@interface libui_intrinsicWidthNSComboBox : NSComboBox
+@end
+
+@implementation libui_intrinsicWidthNSComboBox
+
+- (NSSize)intrinsicContentSize
+{
+	NSSize s;
+
+	s = [super intrinsicContentSize];
+	s.width = comboboxWidth;
+	return s;
+}
+
+@end
+
+struct uiEditableCombobox {
+	uiDarwinControl c;
+	NSComboBox *cb;
+	void (*onChanged)(uiEditableCombobox *, void *);
+	void *onChangedData;
+};
+
+@interface editableComboboxDelegateClass : NSObject<NSComboBoxDelegate> {
+	struct mapTable *comboboxes;
+}
+- (void)controlTextDidChange:(NSNotification *)note;
+- (void)comboBoxSelectionDidChange:(NSNotification *)note;
+- (void)registerCombobox:(uiEditableCombobox *)c;
+- (void)unregisterCombobox:(uiEditableCombobox *)c;
+@end
+
+@implementation editableComboboxDelegateClass
+
+- (id)init
+{
+	self = [super init];
+	if (self)
+		self->comboboxes = newMap();
+	return self;
+}
+
+- (void)dealloc
+{
+	mapDestroy(self->comboboxes);
+	[super dealloc];
+}
+
+- (void)controlTextDidChange:(NSNotification *)note
+{
+	uiEditableCombobox *c;
+
+	c = uiEditableCombobox(mapGet(self->comboboxes, [note object]));
+	(*(c->onChanged))(c, c->onChangedData);
+}
+
+// the above doesn't handle when an item is selected; this will
+- (void)comboBoxSelectionDidChange:(NSNotification *)note
+{
+	// except this is sent BEFORE the entry is changed, and that doesn't send the above, so
+	// this is via http://stackoverflow.com/a/21059819/3408572 - it avoids the need to manage selected items
+	// this still isn't perfect — I get residual changes to the same value while navigating the list — but it's good enough
+	[self performSelector:@selector(controlTextDidChange:)
+		withObject:note
+		afterDelay:0];
+}
+
+- (void)registerCombobox:(uiEditableCombobox *)c
+{
+	mapSet(self->comboboxes, c->cb, c);
+	[c->cb setDelegate:self];
+}
+
+- (void)unregisterCombobox:(uiEditableCombobox *)c
+{
+	[c->cb setDelegate:nil];
+	mapDelete(self->comboboxes, c->cb);
+}
+
+@end
+
+static editableComboboxDelegateClass *comboboxDelegate = nil;
+
+uiDarwinControlAllDefaultsExceptDestroy(uiEditableCombobox, cb)
+
+static void uiEditableComboboxDestroy(uiControl *cc)
+{
+	uiEditableCombobox *c = uiEditableCombobox(cc);
+
+	[comboboxDelegate unregisterCombobox:c];
+	[c->cb release];
+	uiFreeControl(uiControl(c));
+}
+
+void uiEditableComboboxAppend(uiEditableCombobox *c, const char *text)
+{
+	[c->cb addItemWithObjectValue:toNSString(text)];
+}
+
+char *uiEditableComboboxText(uiEditableCombobox *c)
+{
+	return uiDarwinNSStringToText([c->cb stringValue]);
+}
+
+void uiEditableComboboxSetText(uiEditableCombobox *c, const char *text)
+{
+	NSString *t;
+
+	t = toNSString(text);
+	[c->cb setStringValue:t];
+	// yes, let's imitate the behavior that caused uiEditableCombobox to be separate in the first place!
+	// just to avoid confusion when users see an option in the list in the text field but not selected in the list
+	[c->cb selectItemWithObjectValue:t];
+}
+
+#if 0
+// LONGTERM
+void uiEditableComboboxSetSelected(uiEditableCombobox *c, int n)
+{
+	if (c->editable) {
+		// see https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ComboBox/Tasks/SettingComboBoxValue.html#//apple_ref/doc/uid/20000256
+		id delegate;
+
+		// this triggers the delegate; turn it off for now
+		delegate = [c->cb delegate];
+		[c->cb setDelegate:nil];
+
+		// this seems to work fine for -1 too
+		[c->cb selectItemAtIndex:n];
+		if (n == -1)
+			[c->cb setObjectValue:@""];
+		else
+			[c->cb setObjectValue:[c->cb objectValueOfSelectedItem]];
+
+		[c->cb setDelegate:delegate];
+		return;
+	}
+	[c->pb selectItemAtIndex:n];
+}
+#endif
+
+void uiEditableComboboxOnChanged(uiEditableCombobox *c, void (*f)(uiEditableCombobox *c, void *data), void *data)
+{
+	c->onChanged = f;
+	c->onChangedData = data;
+}
+
+static void defaultOnChanged(uiEditableCombobox *c, void *data)
+{
+	// do nothing
+}
+
+uiEditableCombobox *uiNewEditableCombobox(void)
+{
+	uiEditableCombobox *c;
+
+	uiDarwinNewControl(uiEditableCombobox, c);
+
+	c->cb = [[libui_intrinsicWidthNSComboBox alloc] initWithFrame:NSZeroRect];
+	[c->cb setUsesDataSource:NO];
+	[c->cb setButtonBordered:YES];
+	[c->cb setCompletes:NO];
+	uiDarwinSetControlFont(c->cb, NSRegularControlSize);
+
+	if (comboboxDelegate == nil) {
+		comboboxDelegate = [[editableComboboxDelegateClass new] autorelease];
+		[delegates addObject:comboboxDelegate];
+	}
+	[comboboxDelegate registerCombobox:c];
+	uiEditableComboboxOnChanged(c, defaultOnChanged, NULL);
+
+	return c;
+}
diff --git a/src/libui_sdl/libui/darwin/entry.m b/src/libui_sdl/libui/darwin/entry.m
new file mode 100644
index 0000000..219d080
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/entry.m
@@ -0,0 +1,251 @@
+// 14 august 2015
+#import "uipriv_darwin.h"
+
+// Text fields for entering text have no intrinsic width; we'll use the default Interface Builder width for them.
+#define textfieldWidth 96
+
+@interface libui_intrinsicWidthNSTextField : NSTextField
+@end
+
+@implementation libui_intrinsicWidthNSTextField
+
+- (NSSize)intrinsicContentSize
+{
+	NSSize s;
+
+	s = [super intrinsicContentSize];
+	s.width = textfieldWidth;
+	return s;
+}
+
+@end
+
+// TODO does this have one on its own?
+@interface libui_intrinsicWidthNSSecureTextField : NSSecureTextField
+@end
+
+@implementation libui_intrinsicWidthNSSecureTextField
+
+- (NSSize)intrinsicContentSize
+{
+	NSSize s;
+
+	s = [super intrinsicContentSize];
+	s.width = textfieldWidth;
+	return s;
+}
+
+@end
+
+// TODO does this have one on its own?
+@interface libui_intrinsicWidthNSSearchField : NSSearchField
+@end
+
+@implementation libui_intrinsicWidthNSSearchField
+
+- (NSSize)intrinsicContentSize
+{
+	NSSize s;
+
+	s = [super intrinsicContentSize];
+	s.width = textfieldWidth;
+	return s;
+}
+
+@end
+
+struct uiEntry {
+	uiDarwinControl c;
+	NSTextField *textfield;
+	void (*onChanged)(uiEntry *, void *);
+	void *onChangedData;
+};
+
+static BOOL isSearchField(NSTextField *tf)
+{
+	return [tf isKindOfClass:[NSSearchField class]];
+}
+
+@interface entryDelegateClass : NSObject<NSTextFieldDelegate> {
+	struct mapTable *entries;
+}
+- (void)controlTextDidChange:(NSNotification *)note;
+- (IBAction)onSearch:(id)sender;
+- (void)registerEntry:(uiEntry *)e;
+- (void)unregisterEntry:(uiEntry *)e;
+@end
+
+@implementation entryDelegateClass
+
+- (id)init
+{
+	self = [super init];
+	if (self)
+		self->entries = newMap();
+	return self;
+}
+
+- (void)dealloc
+{
+	mapDestroy(self->entries);
+	[super dealloc];
+}
+
+- (void)controlTextDidChange:(NSNotification *)note
+{
+	[self onSearch:[note object]];
+}
+
+- (IBAction)onSearch:(id)sender
+{
+	uiEntry *e;
+
+	e = (uiEntry *) mapGet(self->entries, sender);
+	(*(e->onChanged))(e, e->onChangedData);
+}
+
+- (void)registerEntry:(uiEntry *)e
+{
+	mapSet(self->entries, e->textfield, e);
+	if (isSearchField(e->textfield)) {
+		[e->textfield setTarget:self];
+		[e->textfield setAction:@selector(onSearch:)];
+	} else
+		[e->textfield setDelegate:self];
+}
+
+- (void)unregisterEntry:(uiEntry *)e
+{
+	if (isSearchField(e->textfield))
+		[e->textfield setTarget:nil];
+	else
+		[e->textfield setDelegate:nil];
+	mapDelete(self->entries, e->textfield);
+}
+
+@end
+
+static entryDelegateClass *entryDelegate = nil;
+
+uiDarwinControlAllDefaultsExceptDestroy(uiEntry, textfield)
+
+static void uiEntryDestroy(uiControl *c)
+{
+	uiEntry *e = uiEntry(c);
+
+	[entryDelegate unregisterEntry:e];
+	[e->textfield release];
+	uiFreeControl(uiControl(e));
+}
+
+char *uiEntryText(uiEntry *e)
+{
+	return uiDarwinNSStringToText([e->textfield stringValue]);
+}
+
+void uiEntrySetText(uiEntry *e, const char *text)
+{
+	[e->textfield setStringValue:toNSString(text)];
+	// don't queue the control for resize; entry sizes are independent of their contents
+}
+
+void uiEntryOnChanged(uiEntry *e, void (*f)(uiEntry *, void *), void *data)
+{
+	e->onChanged = f;
+	e->onChangedData = data;
+}
+
+int uiEntryReadOnly(uiEntry *e)
+{
+	return [e->textfield isEditable] == NO;
+}
+
+void uiEntrySetReadOnly(uiEntry *e, int readonly)
+{
+	BOOL editable;
+
+	editable = YES;
+	if (readonly)
+		editable = NO;
+	[e->textfield setEditable:editable];
+}
+
+static void defaultOnChanged(uiEntry *e, void *data)
+{
+	// do nothing
+}
+
+// these are based on interface builder defaults; my comments in the old code weren't very good so I don't really know what talked about what, sorry :/
+void finishNewTextField(NSTextField *t, BOOL isEntry)
+{
+	uiDarwinSetControlFont(t, NSRegularControlSize);
+
+	// THE ORDER OF THESE CALLS IS IMPORTANT; CHANGE IT AND THE BORDERS WILL DISAPPEAR
+	[t setBordered:NO];
+	[t setBezelStyle:NSTextFieldSquareBezel];
+	[t setBezeled:isEntry];
+
+	// we don't need to worry about substitutions/autocorrect here; see window_darwin.m for details
+
+	[[t cell] setLineBreakMode:NSLineBreakByClipping];
+	[[t cell] setScrollable:YES];
+}
+
+static NSTextField *realNewEditableTextField(Class class)
+{
+	NSTextField *tf;
+
+	tf = [[class alloc] initWithFrame:NSZeroRect];
+	[tf setSelectable:YES];		// otherwise the setting is masked by the editable default of YES
+	finishNewTextField(tf, YES);
+	return tf;
+}
+
+NSTextField *newEditableTextField(void)
+{
+	return realNewEditableTextField([libui_intrinsicWidthNSTextField class]);
+}
+
+static uiEntry *finishNewEntry(Class class)
+{
+	uiEntry *e;
+
+	uiDarwinNewControl(uiEntry, e);
+
+	e->textfield = realNewEditableTextField(class);
+
+	if (entryDelegate == nil) {
+		entryDelegate = [[entryDelegateClass new] autorelease];
+		[delegates addObject:entryDelegate];
+	}
+	[entryDelegate registerEntry:e];
+	uiEntryOnChanged(e, defaultOnChanged, NULL);
+
+	return e;
+}
+
+uiEntry *uiNewEntry(void)
+{
+	return finishNewEntry([libui_intrinsicWidthNSTextField class]);
+}
+
+uiEntry *uiNewPasswordEntry(void)
+{
+	return finishNewEntry([libui_intrinsicWidthNSSecureTextField class]);
+}
+
+uiEntry *uiNewSearchEntry(void)
+{
+	uiEntry *e;
+	NSSearchField *s;
+
+	e = finishNewEntry([libui_intrinsicWidthNSSearchField class]);
+	s = (NSSearchField *) (e->textfield);
+	// TODO these are only on 10.10
+//	[s setSendsSearchStringImmediately:NO];
+//	[s setSendsWholeSearchString:NO];
+	[s setBordered:NO];
+	[s setBezelStyle:NSTextFieldRoundedBezel];
+	[s setBezeled:YES];
+	return e;
+}
diff --git a/src/libui_sdl/libui/darwin/fontbutton.m b/src/libui_sdl/libui/darwin/fontbutton.m
new file mode 100644
index 0000000..22bc646
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/fontbutton.m
@@ -0,0 +1,218 @@
+// 14 april 2016
+#import "uipriv_darwin.h"
+
+@interface fontButton : NSButton {
+	uiFontButton *libui_b;
+	NSFont *libui_font;
+}
+- (id)initWithFrame:(NSRect)frame libuiFontButton:(uiFontButton *)b;
+- (void)updateFontButtonLabel;
+- (IBAction)fontButtonClicked:(id)sender;
+- (void)activateFontButton;
+- (void)deactivateFontButton:(BOOL)activatingAnother;
+- (void)deactivateOnClose:(NSNotification *)note;
+- (uiDrawTextFont *)libuiFont;
+@end
+
+// only one may be active at one time
+static fontButton *activeFontButton = nil;
+
+struct uiFontButton {
+	uiDarwinControl c;
+	fontButton *button;
+	void (*onChanged)(uiFontButton *, void *);
+	void *onChangedData;
+};
+
+@implementation fontButton
+
+- (id)initWithFrame:(NSRect)frame libuiFontButton:(uiFontButton *)b
+{
+	self = [super initWithFrame:frame];
+	if (self) {
+		self->libui_b = b;
+
+		// imitate a NSColorWell in appearance
+		[self setButtonType:NSPushOnPushOffButton];
+		[self setBordered:YES];
+		[self setBezelStyle:NSShadowlessSquareBezelStyle];
+
+		// default font values according to the CTFontDescriptor reference
+		// this is autoreleased (thanks swillits in irc.freenode.net/#macdev)
+		self->libui_font = [[NSFont fontWithName:@"Helvetica" size:12.0] retain];
+		[self updateFontButtonLabel];
+
+		// for when clicked
+		[self setTarget:self];
+		[self setAction:@selector(fontButtonClicked:)];
+	}
+	return self;
+}
+
+- (void)dealloc
+{
+	// clean up notifications
+	if (activeFontButton == self)
+		[self deactivateFontButton:NO];
+	[self->libui_font release];
+	[super dealloc];
+}
+
+- (void)updateFontButtonLabel
+{
+	NSString *title;
+
+	title = [NSString stringWithFormat:@"%@ %g",
+		[self->libui_font displayName],
+		[self->libui_font pointSize]];
+	[self setTitle:title];
+}
+
+- (IBAction)fontButtonClicked:(id)sender
+{
+	if ([self state] == NSOnState)
+		[self activateFontButton];
+	else
+		[self deactivateFontButton:NO];
+}
+
+- (void)activateFontButton
+{
+	NSFontManager *sfm;
+
+	sfm = [NSFontManager sharedFontManager];
+	if (activeFontButton != nil)
+		[activeFontButton deactivateFontButton:YES];
+	[sfm setTarget:self];
+	[sfm setSelectedFont:self->libui_font isMultiple:NO];
+	[sfm orderFrontFontPanel:self];
+	activeFontButton = self;
+	[[NSNotificationCenter defaultCenter] addObserver:self
+		selector:@selector(deactivateOnClose:)
+		name:NSWindowWillCloseNotification
+		object:[NSFontPanel sharedFontPanel]];
+	[self setState:NSOnState];
+}
+
+- (void)deactivateFontButton:(BOOL)activatingAnother
+{
+	NSFontManager *sfm;
+
+	sfm = [NSFontManager sharedFontManager];
+	[sfm setTarget:nil];
+	if (!activatingAnother)
+		[[NSFontPanel sharedFontPanel] orderOut:self];
+	activeFontButton = nil;
+	[[NSNotificationCenter defaultCenter] removeObserver:self
+		name:NSWindowWillCloseNotification
+		object:[NSFontPanel sharedFontPanel]];
+	[self setState:NSOffState];
+}
+
+- (void)deactivateOnClose:(NSNotification *)note
+{
+	[self deactivateFontButton:NO];
+}
+
+- (void)changeFont:(id)sender
+{
+	NSFontManager *fm;
+	NSFont *old;
+	uiFontButton *b = self->libui_b;
+
+	fm = (NSFontManager *) sender;
+	old = self->libui_font;
+	self->libui_font = [sender convertFont:self->libui_font];
+	// do this even if it returns the same; we don't own anything that isn't from a new or alloc/init
+	[self->libui_font retain];
+	// do this second just in case
+	[old release];
+	[self updateFontButtonLabel];
+	(*(b->onChanged))(b, b->onChangedData);
+}
+
+- (NSUInteger)validModesForFontPanel:(NSFontPanel *)panel
+{
+	return NSFontPanelFaceModeMask |
+		NSFontPanelSizeModeMask |
+		NSFontPanelCollectionModeMask;
+}
+
+- (uiDrawTextFont *)libuiFont
+{
+	return mkTextFontFromNSFont(self->libui_font);
+}
+
+@end
+
+uiDarwinControlAllDefaults(uiFontButton, button)
+
+// we do not want font change events to be sent to any controls other than the font buttons
+// see main.m for more details
+BOOL fontButtonInhibitSendAction(SEL sel, id from, id to)
+{
+	if (sel != @selector(changeFont:))
+		return NO;
+	return ![to isKindOfClass:[fontButton class]];
+}
+
+// we do not want NSFontPanelValidation messages to be sent to any controls other than the font buttons when a font button is active
+// see main.m for more details
+BOOL fontButtonOverrideTargetForAction(SEL sel, id from, id to, id *override)
+{
+	if (activeFontButton == nil)
+		return NO;
+	if (sel != @selector(validModesForFontPanel:))
+		return NO;
+	*override = activeFontButton;
+	return YES;
+}
+
+// we also don't want the panel to be usable when there's a dialog running; see stddialogs.m for more details on that
+// unfortunately the panel seems to ignore -setWorksWhenModal: so we'll have to do things ourselves
+@interface nonModalFontPanel : NSFontPanel
+@end
+
+@implementation nonModalFontPanel
+
+- (BOOL)worksWhenModal
+{
+	return NO;
+}
+
+@end
+
+void setupFontPanel(void)
+{
+	[NSFontManager setFontPanelFactory:[nonModalFontPanel class]];
+}
+
+static void defaultOnChanged(uiFontButton *b, void *data)
+{
+	// do nothing
+}
+
+uiDrawTextFont *uiFontButtonFont(uiFontButton *b)
+{
+	return [b->button libuiFont];
+}
+
+void uiFontButtonOnChanged(uiFontButton *b, void (*f)(uiFontButton *, void *), void *data)
+{
+	b->onChanged = f;
+	b->onChangedData = data;
+}
+
+uiFontButton *uiNewFontButton(void)
+{
+	uiFontButton *b;
+
+	uiDarwinNewControl(uiFontButton, b);
+
+	b->button = [[fontButton alloc] initWithFrame:NSZeroRect libuiFontButton:b];
+	uiDarwinSetControlFont(b->button, NSRegularControlSize);
+
+	uiFontButtonOnChanged(b, defaultOnChanged, NULL);
+
+	return b;
+}
diff --git a/src/libui_sdl/libui/darwin/form.m b/src/libui_sdl/libui/darwin/form.m
new file mode 100644
index 0000000..7cdb965
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/form.m
@@ -0,0 +1,561 @@
+// 7 june 2016
+#import "uipriv_darwin.h"
+
+// TODO in the test program, sometimes one of the radio buttons can disappear (try when spaced)
+
+@interface formChild : NSView
+@property uiControl *c;
+@property (strong) NSTextField *label;
+@property BOOL stretchy;
+@property NSLayoutPriority oldHorzHuggingPri;
+@property NSLayoutPriority oldVertHuggingPri;
+@property (strong) NSLayoutConstraint *baseline;
+@property (strong) NSLayoutConstraint *leading;
+@property (strong) NSLayoutConstraint *top;
+@property (strong) NSLayoutConstraint *trailing;
+@property (strong) NSLayoutConstraint *bottom;
+- (id)initWithLabel:(NSTextField *)l;
+- (void)onDestroy;
+- (NSView *)view;
+@end
+
+@interface formView : NSView {
+	uiForm *f;
+	NSMutableArray *children;
+	int padded;
+
+	NSLayoutConstraint *first;
+	NSMutableArray *inBetweens;
+	NSLayoutConstraint *last;
+	NSMutableArray *widths;
+	NSMutableArray *leadings;
+	NSMutableArray *middles;
+	NSMutableArray *trailings;
+}
+- (id)initWithF:(uiForm *)ff;
+- (void)onDestroy;
+- (void)removeOurConstraints;
+- (void)syncEnableStates:(int)enabled;
+- (CGFloat)paddingAmount;
+- (void)establishOurConstraints;
+- (void)append:(NSString *)label c:(uiControl *)c stretchy:(int)stretchy;
+- (void)delete:(int)n;
+- (int)isPadded;
+- (void)setPadded:(int)p;
+- (BOOL)hugsTrailing;
+- (BOOL)hugsBottom;
+- (int)nStretchy;
+@end
+
+struct uiForm {
+	uiDarwinControl c;
+	formView *view;
+};
+
+@implementation formChild
+
+- (id)initWithLabel:(NSTextField *)l
+{
+	self = [super initWithFrame:NSZeroRect];
+	if (self) {
+		self.label = l;
+		[self.label setTranslatesAutoresizingMaskIntoConstraints:NO];
+		[self.label setContentHuggingPriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal];
+		[self.label setContentHuggingPriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical];
+		[self.label setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal];
+		[self.label setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical];
+		[self addSubview:self.label];
+
+		self.leading = mkConstraint(self.label, NSLayoutAttributeLeading,
+			NSLayoutRelationGreaterThanOrEqual,
+			self, NSLayoutAttributeLeading,
+			1, 0,
+			@"uiForm label leading");
+		[self addConstraint:self.leading];
+		self.top = mkConstraint(self.label, NSLayoutAttributeTop,
+			NSLayoutRelationEqual,
+			self, NSLayoutAttributeTop,
+			1, 0,
+			@"uiForm label top");
+		[self addConstraint:self.top];
+		self.trailing = mkConstraint(self.label, NSLayoutAttributeTrailing,
+			NSLayoutRelationEqual,
+			self, NSLayoutAttributeTrailing,
+			1, 0,
+			@"uiForm label trailing");
+		[self addConstraint:self.trailing];
+		self.bottom = mkConstraint(self.label, NSLayoutAttributeBottom,
+			NSLayoutRelationEqual,
+			self, NSLayoutAttributeBottom,
+			1, 0,
+			@"uiForm label bottom");
+		[self addConstraint:self.bottom];
+	}
+	return self;
+}
+
+- (void)onDestroy
+{
+	[self removeConstraint:self.trailing];
+	self.trailing = nil;
+	[self removeConstraint:self.top];
+	self.top = nil;
+	[self removeConstraint:self.bottom];
+	self.bottom = nil;
+
+	[self.label removeFromSuperview];
+	self.label = nil;
+}
+
+- (NSView *)view
+{
+	return (NSView *) uiControlHandle(self.c);
+}
+
+@end
+
+@implementation formView
+
+- (id)initWithF:(uiForm *)ff
+{
+	self = [super initWithFrame:NSZeroRect];
+	if (self != nil) {
+		self->f = ff;
+		self->padded = 0;
+		self->children = [NSMutableArray new];
+
+		self->inBetweens = [NSMutableArray new];
+		self->widths = [NSMutableArray new];
+		self->leadings = [NSMutableArray new];
+		self->middles = [NSMutableArray new];
+		self->trailings = [NSMutableArray new];
+	}
+	return self;
+}
+
+- (void)onDestroy
+{
+	formChild *fc;
+
+	[self removeOurConstraints];
+	[self->inBetweens release];
+	[self->widths release];
+	[self->leadings release];
+	[self->middles release];
+	[self->trailings release];
+
+	for (fc in self->children) {
+		[self removeConstraint:fc.baseline];
+		fc.baseline = nil;
+		uiControlSetParent(fc.c, NULL);
+		uiDarwinControlSetSuperview(uiDarwinControl(fc.c), nil);
+		uiControlDestroy(fc.c);
+		[fc onDestroy];
+		[fc removeFromSuperview];
+	}
+	[self->children release];
+}
+
+- (void)removeOurConstraints
+{
+	if (self->first != nil) {
+		[self removeConstraint:self->first];
+		[self->first release];
+		self->first = nil;
+	}
+	if ([self->inBetweens count] != 0) {
+		[self removeConstraints:self->inBetweens];
+		[self->inBetweens removeAllObjects];
+	}
+	if (self->last != nil) {
+		[self removeConstraint:self->last];
+		[self->last release];
+		self->last = nil;
+	}
+	if ([self->widths count] != 0) {
+		[self removeConstraints:self->widths];
+		[self->widths removeAllObjects];
+	}
+	if ([self->leadings count] != 0) {
+		[self removeConstraints:self->leadings];
+		[self->leadings removeAllObjects];
+	}
+	if ([self->middles count] != 0) {
+		[self removeConstraints:self->middles];
+		[self->middles removeAllObjects];
+	}
+	if ([self->trailings count] != 0) {
+		[self removeConstraints:self->trailings];
+		[self->trailings removeAllObjects];
+	}
+}
+
+- (void)syncEnableStates:(int)enabled
+{
+	formChild *fc;
+
+	for (fc in self->children)
+		uiDarwinControlSyncEnableState(uiDarwinControl(fc.c), enabled);
+}
+
+- (CGFloat)paddingAmount
+{
+	if (!self->padded)
+		return 0.0;
+	return uiDarwinPaddingAmount(NULL);
+}
+
+- (void)establishOurConstraints
+{
+	formChild *fc;
+	CGFloat padding;
+	NSView *prev, *prevlabel;
+	NSLayoutConstraint *c;
+
+	[self removeOurConstraints];
+	if ([self->children count] == 0)
+		return;
+	padding = [self paddingAmount];
+
+	// first arrange the children vertically and make them the same width
+	prev = nil;
+	for (fc in self->children) {
+		[fc setHidden:!uiControlVisible(fc.c)];
+		if (!uiControlVisible(fc.c))
+			continue;
+		if (prev == nil) {			// first view
+			self->first = mkConstraint(self, NSLayoutAttributeTop,
+				NSLayoutRelationEqual,
+				[fc view], NSLayoutAttributeTop,
+				1, 0,
+				@"uiForm first vertical constraint");
+			[self addConstraint:self->first];
+			[self->first retain];
+			prev = [fc view];
+			prevlabel = fc;
+			continue;
+		}
+		// not the first; link it
+		c = mkConstraint(prev, NSLayoutAttributeBottom,
+			NSLayoutRelationEqual,
+			[fc view], NSLayoutAttributeTop,
+			1, -padding,
+			@"uiForm in-between vertical constraint");
+		[self addConstraint:c];
+		[self->inBetweens addObject:c];
+		// and make the same width
+		c = mkConstraint(prev, NSLayoutAttributeWidth,
+			NSLayoutRelationEqual,
+			[fc view], NSLayoutAttributeWidth,
+			1, 0,
+			@"uiForm control width constraint");
+		[self addConstraint:c];
+		[self->widths addObject:c];
+		c = mkConstraint(prevlabel, NSLayoutAttributeWidth,
+			NSLayoutRelationEqual,
+			fc, NSLayoutAttributeWidth,
+			1, 0,
+			@"uiForm label lwidth constraint");
+		[self addConstraint:c];
+		[self->widths addObject:c];
+		prev = [fc view];
+		prevlabel = fc;
+	}
+	if (prev == nil)		// all hidden; act as if nothing there
+		return;
+	self->last = mkConstraint(prev, NSLayoutAttributeBottom,
+		NSLayoutRelationEqual,
+		self, NSLayoutAttributeBottom,
+		1, 0,
+		@"uiForm last vertical constraint");
+	[self addConstraint:self->last];
+	[self->last retain];
+
+	// now arrange the controls horizontally
+	for (fc in self->children) {
+		if (!uiControlVisible(fc.c))
+			continue;
+		c = mkConstraint(self, NSLayoutAttributeLeading,
+			NSLayoutRelationEqual,
+			fc, NSLayoutAttributeLeading,
+			1, 0,
+			@"uiForm leading constraint");
+		[self addConstraint:c];
+		[self->leadings addObject:c];
+		// coerce the control to be as wide as possible
+		// see http://stackoverflow.com/questions/37710892/in-auto-layout-i-set-up-labels-that-shouldnt-grow-horizontally-and-controls-th
+		c = mkConstraint(self, NSLayoutAttributeLeading,
+			NSLayoutRelationEqual,
+			[fc view], NSLayoutAttributeLeading,
+			1, 0,
+			@"uiForm leading constraint");
+		[c setPriority:NSLayoutPriorityDefaultHigh];
+		[self addConstraint:c];
+		[self->leadings addObject:c];
+		c = mkConstraint(fc, NSLayoutAttributeTrailing,
+			NSLayoutRelationEqual,
+			[fc view], NSLayoutAttributeLeading,
+			1, -padding,
+			@"uiForm middle constraint");
+		[self addConstraint:c];
+		[self->middles addObject:c];
+		c = mkConstraint([fc view], NSLayoutAttributeTrailing,
+			NSLayoutRelationEqual,
+			self, NSLayoutAttributeTrailing,
+			1, 0,
+			@"uiForm trailing constraint");
+		[self addConstraint:c];
+		[self->trailings addObject:c];
+		// TODO
+		c = mkConstraint(fc, NSLayoutAttributeBottom,
+			NSLayoutRelationLessThanOrEqual,
+			self, NSLayoutAttributeBottom,
+			1, 0,
+			@"TODO");
+		[self addConstraint:c];
+		[self->trailings addObject:c];
+	}
+
+	// and make all stretchy controls have the same height
+	prev = nil;
+	for (fc in self->children) {
+		if (!uiControlVisible(fc.c))
+			continue;
+		if (!fc.stretchy)
+			continue;
+		if (prev == nil) {
+			prev = [fc view];
+			continue;
+		}
+		c = mkConstraint([fc view], NSLayoutAttributeHeight,
+			NSLayoutRelationEqual,
+			prev, NSLayoutAttributeHeight,
+			1, 0,
+			@"uiForm stretchy constraint");
+		[self addConstraint:c];
+		// TODO make a dedicated array for this
+		[self->leadings addObject:c];
+	}
+
+	// we don't arrange the labels vertically; that's done when we add the control since those constraints don't need to change (they just need to be at their baseline)
+}
+
+- (void)append:(NSString *)label c:(uiControl *)c stretchy:(int)stretchy
+{
+	formChild *fc;
+	NSLayoutPriority priority;
+	NSLayoutAttribute attribute;
+	int oldnStretchy;
+
+	fc = [[formChild alloc] initWithLabel:newLabel(label)];
+	fc.c = c;
+	fc.stretchy = stretchy;
+	fc.oldHorzHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(fc.c), NSLayoutConstraintOrientationHorizontal);
+	fc.oldVertHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(fc.c), NSLayoutConstraintOrientationVertical);
+	[fc setTranslatesAutoresizingMaskIntoConstraints:NO];
+	[self addSubview:fc];
+
+	uiControlSetParent(fc.c, uiControl(self->f));
+	uiDarwinControlSetSuperview(uiDarwinControl(fc.c), self);
+	uiDarwinControlSyncEnableState(uiDarwinControl(fc.c), uiControlEnabledToUser(uiControl(self->f)));
+
+	// if a control is stretchy, it should not hug vertically
+	// otherwise, it should *forcibly* hug
+	if (fc.stretchy)
+		priority = NSLayoutPriorityDefaultLow;
+	else
+		// LONGTERM will default high work?
+		priority = NSLayoutPriorityRequired;
+	uiDarwinControlSetHuggingPriority(uiDarwinControl(fc.c), priority, NSLayoutConstraintOrientationVertical);
+	// make sure controls don't hug their horizontal direction so they fill the width of the view
+	uiDarwinControlSetHuggingPriority(uiDarwinControl(fc.c), NSLayoutPriorityDefaultLow, NSLayoutConstraintOrientationHorizontal);
+
+	// and constrain the baselines to position the label vertically
+	// if the view is a scroll view, align tops, not baselines
+	// this is what Interface Builder does
+	attribute = NSLayoutAttributeBaseline;
+	if ([[fc view] isKindOfClass:[NSScrollView class]])
+		attribute = NSLayoutAttributeTop;
+	fc.baseline = mkConstraint(fc.label, attribute,
+		NSLayoutRelationEqual,
+		[fc view], attribute,
+		1, 0,
+		@"uiForm baseline constraint");
+	[self addConstraint:fc.baseline];
+
+	oldnStretchy = [self nStretchy];
+	[self->children addObject:fc];
+
+	[self establishOurConstraints];
+	if (fc.stretchy)
+		if (oldnStretchy == 0)
+			uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl(self->f));
+
+	[fc release];		// we don't need the initial reference now
+}
+
+- (void)delete:(int)n
+{
+	formChild *fc;
+	int stretchy;
+
+	fc = (formChild *) [self->children objectAtIndex:n];
+	stretchy = fc.stretchy;
+
+	uiControlSetParent(fc.c, NULL);
+	uiDarwinControlSetSuperview(uiDarwinControl(fc.c), nil);
+
+	uiDarwinControlSetHuggingPriority(uiDarwinControl(fc.c), fc.oldHorzHuggingPri, NSLayoutConstraintOrientationHorizontal);
+	uiDarwinControlSetHuggingPriority(uiDarwinControl(fc.c), fc.oldVertHuggingPri, NSLayoutConstraintOrientationVertical);
+
+	[fc onDestroy];
+	[self->children removeObjectAtIndex:n];
+
+	[self establishOurConstraints];
+	if (stretchy)
+		if ([self nStretchy] == 0)
+			uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl(self->f));
+}
+
+- (int)isPadded
+{
+	return self->padded;
+}
+
+- (void)setPadded:(int)p
+{
+	CGFloat padding;
+	NSLayoutConstraint *c;
+
+	self->padded = p;
+	padding = [self paddingAmount];
+	for (c in self->inBetweens)
+		[c setConstant:-padding];
+	for (c in self->middles)
+		[c setConstant:-padding];
+}
+
+- (BOOL)hugsTrailing
+{
+	return YES;			// always hug trailing
+}
+
+- (BOOL)hugsBottom
+{
+	// only hug if we have stretchy
+	return [self nStretchy] != 0;
+}
+
+- (int)nStretchy
+{
+	formChild *fc;
+	int n;
+
+	n = 0;
+	for (fc in self->children) {
+		if (!uiControlVisible(fc.c))
+			continue;
+		if (fc.stretchy)
+			n++;
+	}
+	return n;
+}
+
+@end
+
+static void uiFormDestroy(uiControl *c)
+{
+	uiForm *f = uiForm(c);
+
+	[f->view onDestroy];
+	[f->view release];
+	uiFreeControl(uiControl(f));
+}
+
+uiDarwinControlDefaultHandle(uiForm, view)
+uiDarwinControlDefaultParent(uiForm, view)
+uiDarwinControlDefaultSetParent(uiForm, view)
+uiDarwinControlDefaultToplevel(uiForm, view)
+uiDarwinControlDefaultVisible(uiForm, view)
+uiDarwinControlDefaultShow(uiForm, view)
+uiDarwinControlDefaultHide(uiForm, view)
+uiDarwinControlDefaultEnabled(uiForm, view)
+uiDarwinControlDefaultEnable(uiForm, view)
+uiDarwinControlDefaultDisable(uiForm, view)
+
+static void uiFormSyncEnableState(uiDarwinControl *c, int enabled)
+{
+	uiForm *f = uiForm(c);
+
+	if (uiDarwinShouldStopSyncEnableState(uiDarwinControl(f), enabled))
+		return;
+	[f->view syncEnableStates:enabled];
+}
+
+uiDarwinControlDefaultSetSuperview(uiForm, view)
+
+static BOOL uiFormHugsTrailingEdge(uiDarwinControl *c)
+{
+	uiForm *f = uiForm(c);
+
+	return [f->view hugsTrailing];
+}
+
+static BOOL uiFormHugsBottom(uiDarwinControl *c)
+{
+	uiForm *f = uiForm(c);
+
+	return [f->view hugsBottom];
+}
+
+static void uiFormChildEdgeHuggingChanged(uiDarwinControl *c)
+{
+	uiForm *f = uiForm(c);
+
+	[f->view establishOurConstraints];
+}
+
+uiDarwinControlDefaultHuggingPriority(uiForm, view)
+uiDarwinControlDefaultSetHuggingPriority(uiForm, view)
+
+static void uiFormChildVisibilityChanged(uiDarwinControl *c)
+{
+	uiForm *f = uiForm(c);
+
+	[f->view establishOurConstraints];
+}
+
+void uiFormAppend(uiForm *f, const char *label, uiControl *c, int stretchy)
+{
+	// LONGTERM on other platforms
+	// or at leat allow this and implicitly turn it into a spacer
+	if (c == NULL)
+		userbug("You cannot add NULL to a uiForm.");
+	[f->view append:toNSString(label) c:c stretchy:stretchy];
+}
+
+void uiFormDelete(uiForm *f, int n)
+{
+	[f->view delete:n];
+}
+
+int uiFormPadded(uiForm *f)
+{
+	return [f->view isPadded];
+}
+
+void uiFormSetPadded(uiForm *f, int padded)
+{
+	[f->view setPadded:padded];
+}
+
+uiForm *uiNewForm(void)
+{
+	uiForm *f;
+
+	uiDarwinNewControl(uiForm, f);
+
+	f->view = [[formView alloc] initWithF:f];
+
+	return f;
+}
diff --git a/src/libui_sdl/libui/darwin/grid.m b/src/libui_sdl/libui/darwin/grid.m
new file mode 100644
index 0000000..d5c5fb1
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/grid.m
@@ -0,0 +1,800 @@
+// 11 june 2016
+#import "uipriv_darwin.h"
+
+// TODO the assorted test doesn't work right at all
+
+@interface gridChild : NSView
+@property uiControl *c;
+@property int left;
+@property int top;
+@property int xspan;
+@property int yspan;
+@property int hexpand;
+@property uiAlign halign;
+@property int vexpand;
+@property uiAlign valign;
+
+@property (strong) NSLayoutConstraint *leadingc;
+@property (strong) NSLayoutConstraint *topc;
+@property (strong) NSLayoutConstraint *trailingc;
+@property (strong) NSLayoutConstraint *bottomc;
+@property (strong) NSLayoutConstraint *xcenterc;
+@property (strong) NSLayoutConstraint *ycenterc;
+
+@property NSLayoutPriority oldHorzHuggingPri;
+@property NSLayoutPriority oldVertHuggingPri;
+- (void)setC:(uiControl *)c grid:(uiGrid *)g;
+- (void)onDestroy;
+- (NSView *)view;
+@end
+
+@interface gridView : NSView {
+	uiGrid *g;
+	NSMutableArray *children;
+	int padded;
+
+	NSMutableArray *edges;
+	NSMutableArray *inBetweens;
+
+	NSMutableArray *emptyCellViews;
+}
+- (id)initWithG:(uiGrid *)gg;
+- (void)onDestroy;
+- (void)removeOurConstraints;
+- (void)syncEnableStates:(int)enabled;
+- (CGFloat)paddingAmount;
+- (void)establishOurConstraints;
+- (void)append:(gridChild *)gc;
+- (void)insert:(gridChild *)gc after:(uiControl *)c at:(uiAt)at;
+- (int)isPadded;
+- (void)setPadded:(int)p;
+- (BOOL)hugsTrailing;
+- (BOOL)hugsBottom;
+- (int)nhexpand;
+- (int)nvexpand;
+@end
+
+struct uiGrid {
+	uiDarwinControl c;
+	gridView *view;
+};
+
+@implementation gridChild
+
+- (void)setC:(uiControl *)c grid:(uiGrid *)g
+{
+	self.c = c;
+	self.oldHorzHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(self.c), NSLayoutConstraintOrientationHorizontal);
+	self.oldVertHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(self.c), NSLayoutConstraintOrientationVertical);
+
+	uiControlSetParent(self.c, uiControl(g));
+	uiDarwinControlSetSuperview(uiDarwinControl(self.c), self);
+	uiDarwinControlSyncEnableState(uiDarwinControl(self.c), uiControlEnabledToUser(uiControl(g)));
+
+	if (self.halign == uiAlignStart || self.halign == uiAlignFill) {
+		self.leadingc = mkConstraint(self, NSLayoutAttributeLeading,
+			NSLayoutRelationEqual,
+			[self view], NSLayoutAttributeLeading,
+			1, 0,
+			@"uiGrid child horizontal alignment start constraint");
+		[self addConstraint:self.leadingc];
+	}
+	if (self.halign == uiAlignCenter) {
+		self.xcenterc = mkConstraint(self, NSLayoutAttributeCenterX,
+			NSLayoutRelationEqual,
+			[self view], NSLayoutAttributeCenterX,
+			1, 0,
+			@"uiGrid child horizontal alignment center constraint");
+		[self addConstraint:self.xcenterc];
+	}
+	if (self.halign == uiAlignEnd || self.halign == uiAlignFill) {
+		self.trailingc = mkConstraint(self, NSLayoutAttributeTrailing,
+			NSLayoutRelationEqual,
+			[self view], NSLayoutAttributeTrailing,
+			1, 0,
+			@"uiGrid child horizontal alignment end constraint");
+		[self addConstraint:self.trailingc];
+	}
+
+	if (self.valign == uiAlignStart || self.valign == uiAlignFill) {
+		self.topc = mkConstraint(self, NSLayoutAttributeTop,
+			NSLayoutRelationEqual,
+			[self view], NSLayoutAttributeTop,
+			1, 0,
+			@"uiGrid child vertical alignment start constraint");
+		[self addConstraint:self.topc];
+	}
+	if (self.valign == uiAlignCenter) {
+		self.ycenterc = mkConstraint(self, NSLayoutAttributeCenterY,
+			NSLayoutRelationEqual,
+			[self view], NSLayoutAttributeCenterY,
+			1, 0,
+			@"uiGrid child vertical alignment center constraint");
+		[self addConstraint:self.ycenterc];
+	}
+	if (self.valign == uiAlignEnd || self.valign == uiAlignFill) {
+		self.bottomc = mkConstraint(self, NSLayoutAttributeBottom,
+			NSLayoutRelationEqual,
+			[self view], NSLayoutAttributeBottom,
+			1, 0,
+			@"uiGrid child vertical alignment end constraint");
+		[self addConstraint:self.bottomc];
+	}
+}
+
+- (void)onDestroy
+{
+	if (self.leadingc != nil) {
+		[self removeConstraint:self.leadingc];
+		self.leadingc = nil;
+	}
+	if (self.topc != nil) {
+		[self removeConstraint:self.topc];
+		self.topc = nil;
+	}
+	if (self.trailingc != nil) {
+		[self removeConstraint:self.trailingc];
+		self.trailingc = nil;
+	}
+	if (self.bottomc != nil) {
+		[self removeConstraint:self.bottomc];
+		self.bottomc = nil;
+	}
+	if (self.xcenterc != nil) {
+		[self removeConstraint:self.xcenterc];
+		self.xcenterc = nil;
+	}
+	if (self.ycenterc != nil) {
+		[self removeConstraint:self.ycenterc];
+		self.ycenterc = nil;
+	}
+
+	uiControlSetParent(self.c, NULL);
+	uiDarwinControlSetSuperview(uiDarwinControl(self.c), nil);
+	uiDarwinControlSetHuggingPriority(uiDarwinControl(self.c), self.oldHorzHuggingPri, NSLayoutConstraintOrientationHorizontal);
+	uiDarwinControlSetHuggingPriority(uiDarwinControl(self.c), self.oldVertHuggingPri, NSLayoutConstraintOrientationVertical);
+}
+
+- (NSView *)view
+{
+	return (NSView *) uiControlHandle(self.c);
+}
+
+@end
+
+@implementation gridView
+
+- (id)initWithG:(uiGrid *)gg
+{
+	self = [super initWithFrame:NSZeroRect];
+	if (self != nil) {
+		self->g = gg;
+		self->padded = 0;
+		self->children = [NSMutableArray new];
+
+		self->edges = [NSMutableArray new];
+		self->inBetweens = [NSMutableArray new];
+
+		self->emptyCellViews = [NSMutableArray new];
+	}
+	return self;
+}
+
+- (void)onDestroy
+{
+	gridChild *gc;
+
+	[self removeOurConstraints];
+	[self->edges release];
+	[self->inBetweens release];
+
+	[self->emptyCellViews release];
+
+	for (gc in self->children) {
+		[gc onDestroy];
+		uiControlDestroy(gc.c);
+		[gc removeFromSuperview];
+	}
+	[self->children release];
+}
+
+- (void)removeOurConstraints
+{
+	NSView *v;
+
+	if ([self->edges count] != 0) {
+		[self removeConstraints:self->edges];
+		[self->edges removeAllObjects];
+	}
+	if ([self->inBetweens count] != 0) {
+		[self removeConstraints:self->inBetweens];
+		[self->inBetweens removeAllObjects];
+	}
+
+	for (v in self->emptyCellViews)
+		[v removeFromSuperview];
+	[self->emptyCellViews removeAllObjects];
+}
+
+- (void)syncEnableStates:(int)enabled
+{
+	gridChild *gc;
+
+	for (gc in self->children)
+		uiDarwinControlSyncEnableState(uiDarwinControl(gc.c), enabled);
+}
+
+- (CGFloat)paddingAmount
+{
+	if (!self->padded)
+		return 0.0;
+	return uiDarwinPaddingAmount(NULL);
+}
+
+// LONGTERM stop early if all controls are hidden
+- (void)establishOurConstraints
+{
+	gridChild *gc;
+	CGFloat padding;
+	int xmin, ymin;
+	int xmax, ymax;
+	int xcount, ycount;
+	BOOL first;
+	int **gg;
+	NSView ***gv;
+	BOOL **gspan;
+	int x, y;
+	int i;
+	NSLayoutConstraint *c;
+	int firstx, firsty;
+	BOOL *hexpand, *vexpand;
+	BOOL doit;
+	BOOL onlyEmptyAndSpanning;
+
+	[self removeOurConstraints];
+	if ([self->children count] == 0)
+		return;
+	padding = [self paddingAmount];
+
+	// first, figure out the minimum and maximum row and column numbers
+	// ignore hidden controls
+	first = YES;
+	for (gc in self->children) {
+		// this bit is important: it ensures row ymin and column xmin have at least one cell to draw, so the onlyEmptyAndSpanning logic below will never run on those rows
+		if (!uiControlVisible(gc.c))
+			continue;
+		if (first) {
+			xmin = gc.left;
+			ymin = gc.top;
+			xmax = gc.left + gc.xspan;
+			ymax = gc.top + gc.yspan;
+			first = NO;
+			continue;
+		}
+		if (xmin > gc.left)
+			xmin = gc.left;
+		if (ymin > gc.top)
+			ymin = gc.top;
+		if (xmax < (gc.left + gc.xspan))
+			xmax = gc.left + gc.xspan;
+		if (ymax < (gc.top + gc.yspan))
+			ymax = gc.top + gc.yspan;
+	}
+	if (first != NO)		// the entire grid is hidden; do nothing
+		return;
+	xcount = xmax - xmin;
+	ycount = ymax - ymin;
+
+	// now build a topological map of the grid gg[y][x]
+	// also figure out which cells contain spanned views so they can be ignored later
+	// treat hidden controls by keeping the indices -1
+	gg = (int **) uiAlloc(ycount * sizeof (int *), "int[][]");
+	gspan = (BOOL **) uiAlloc(ycount * sizeof (BOOL *), "BOOL[][]");
+	for (y = 0; y < ycount; y++) {
+		gg[y] = (int *) uiAlloc(xcount * sizeof (int), "int[]");
+		gspan[y] = (BOOL *) uiAlloc(xcount * sizeof (BOOL), "BOOL[]");
+		for (x = 0; x < xcount; x++)
+			gg[y][x] = -1;		// empty
+	}
+	for (i = 0; i < [self->children count]; i++) {
+		gc = (gridChild *) [self->children objectAtIndex:i];
+		if (!uiControlVisible(gc.c))
+			continue;
+		for (y = gc.top; y < gc.top + gc.yspan; y++)
+			for (x = gc.left; x < gc.left + gc.xspan; x++) {
+				gg[y - ymin][x - xmin] = i;
+				if (x != gc.left || y != gc.top)
+					gspan[y - ymin][x - xmin] = YES;
+			}
+	}
+
+	// if a row or column only contains emptys and spanning cells of a opposite-direction spannings, remove it by duplicating the previous row or column
+	for (y = 0; y < ycount; y++) {
+		onlyEmptyAndSpanning = YES;
+		for (x = 0; x < xcount; x++)
+			if (gg[y][x] != -1) {
+				gc = (gridChild *) [self->children objectAtIndex:gg[y][x]];
+				if (gc.yspan == 1 || gc.top - ymin == y) {
+					onlyEmptyAndSpanning = NO;
+					break;
+				}
+			}
+		if (onlyEmptyAndSpanning)
+			for (x = 0; x < xcount; x++) {
+				gg[y][x] = gg[y - 1][x];
+				gspan[y][x] = YES;
+			}
+	}
+	for (x = 0; x < xcount; x++) {
+		onlyEmptyAndSpanning = YES;
+		for (y = 0; y < ycount; y++)
+			if (gg[y][x] != -1) {
+				gc = (gridChild *) [self->children objectAtIndex:gg[y][x]];
+				if (gc.xspan == 1 || gc.left - xmin == x) {
+					onlyEmptyAndSpanning = NO;
+					break;
+				}
+			}
+		if (onlyEmptyAndSpanning)
+			for (y = 0; y < ycount; y++) {
+				gg[y][x] = gg[y][x - 1];
+				gspan[y][x] = YES;
+			}
+	}
+
+	// now build a topological map of the grid's views gv[y][x]
+	// for any empty cell, create a dummy view
+	gv = (NSView ***) uiAlloc(ycount * sizeof (NSView **), "NSView *[][]");
+	for (y = 0; y < ycount; y++) {
+		gv[y] = (NSView **) uiAlloc(xcount * sizeof (NSView *), "NSView *[]");
+		for (x = 0; x < xcount; x++)
+			if (gg[y][x] == -1) {
+				gv[y][x] = [[NSView alloc] initWithFrame:NSZeroRect];
+				[gv[y][x] setTranslatesAutoresizingMaskIntoConstraints:NO];
+				[self addSubview:gv[y][x]];
+				[self->emptyCellViews addObject:gv[y][x]];
+			} else {
+				gc = (gridChild *) [self->children objectAtIndex:gg[y][x]];
+				gv[y][x] = gc;
+			}
+	}
+
+	// now figure out which rows and columns really expand
+	hexpand = (BOOL *) uiAlloc(xcount * sizeof (BOOL), "BOOL[]");
+	vexpand = (BOOL *) uiAlloc(ycount * sizeof (BOOL), "BOOL[]");
+	// first, which don't span
+	for (gc in self->children) {
+		if (!uiControlVisible(gc.c))
+			continue;
+		if (gc.hexpand && gc.xspan == 1)
+			hexpand[gc.left - xmin] = YES;
+		if (gc.vexpand && gc.yspan == 1)
+			vexpand[gc.top - ymin] = YES;
+	}
+	// second, which do span
+	// the way we handle this is simple: if none of the spanned rows/columns expand, make all rows/columns expand
+	for (gc in self->children) {
+		if (!uiControlVisible(gc.c))
+			continue;
+		if (gc.hexpand && gc.xspan != 1) {
+			doit = YES;
+			for (x = gc.left; x < gc.left + gc.xspan; x++)
+				if (hexpand[x - xmin]) {
+					doit = NO;
+					break;
+				}
+			if (doit)
+				for (x = gc.left; x < gc.left + gc.xspan; x++)
+					hexpand[x - xmin] = YES;
+		}
+		if (gc.vexpand && gc.yspan != 1) {
+			doit = YES;
+			for (y = gc.top; y < gc.top + gc.yspan; y++)
+				if (vexpand[y - ymin]) {
+					doit = NO;
+					break;
+				}
+			if (doit)
+				for (y = gc.top; y < gc.top + gc.yspan; y++)
+					vexpand[y - ymin] = YES;
+		}
+	}
+
+	// now establish all the edge constraints
+	// leading and trailing edges
+	for (y = 0; y < ycount; y++) {
+		c = mkConstraint(self, NSLayoutAttributeLeading,
+			NSLayoutRelationEqual,
+			gv[y][0], NSLayoutAttributeLeading,
+			1, 0,
+			@"uiGrid leading edge constraint");
+		[self addConstraint:c];
+		[self->edges addObject:c];
+		c = mkConstraint(self, NSLayoutAttributeTrailing,
+			NSLayoutRelationEqual,
+			gv[y][xcount - 1], NSLayoutAttributeTrailing,
+			1, 0,
+			@"uiGrid trailing edge constraint");
+		[self addConstraint:c];
+		[self->edges addObject:c];
+	}
+	// top and bottom edges
+	for (x = 0; x < xcount; x++) {
+		c = mkConstraint(self, NSLayoutAttributeTop,
+			NSLayoutRelationEqual,
+			gv[0][x], NSLayoutAttributeTop,
+			1, 0,
+			@"uiGrid top edge constraint");
+		[self addConstraint:c];
+		[self->edges addObject:c];
+		c = mkConstraint(self, NSLayoutAttributeBottom,
+			NSLayoutRelationEqual,
+			gv[ycount - 1][x], NSLayoutAttributeBottom,
+			1, 0,
+			@"uiGrid bottom edge constraint");
+		[self addConstraint:c];
+		[self->edges addObject:c];
+	}
+
+	// now align leading and top edges
+	// do NOT align spanning cells!
+	for (x = 0; x < xcount; x++) {
+		for (y = 0; y < ycount; y++)
+			if (!gspan[y][x])
+				break;
+		firsty = y;
+		for (y++; y < ycount; y++) {
+			if (gspan[y][x])
+				continue;
+			c = mkConstraint(gv[firsty][x], NSLayoutAttributeLeading,
+				NSLayoutRelationEqual,
+				gv[y][x], NSLayoutAttributeLeading,
+				1, 0,
+				@"uiGrid column leading constraint");
+			[self addConstraint:c];
+			[self->edges addObject:c];
+		}
+	}
+	for (y = 0; y < ycount; y++) {
+		for (x = 0; x < xcount; x++)
+			if (!gspan[y][x])
+				break;
+		firstx = x;
+		for (x++; x < xcount; x++) {
+			if (gspan[y][x])
+				continue;
+			c = mkConstraint(gv[y][firstx], NSLayoutAttributeTop,
+				NSLayoutRelationEqual,
+				gv[y][x], NSLayoutAttributeTop,
+				1, 0,
+				@"uiGrid row top constraint");
+			[self addConstraint:c];
+			[self->edges addObject:c];
+		}
+	}
+
+	// now string adjacent views together
+	for (y = 0; y < ycount; y++)
+		for (x = 1; x < xcount; x++)
+			if (gv[y][x - 1] != gv[y][x]) {
+				c = mkConstraint(gv[y][x - 1], NSLayoutAttributeTrailing,
+					NSLayoutRelationEqual,
+					gv[y][x], NSLayoutAttributeLeading,
+					1, -padding,
+					@"uiGrid internal horizontal constraint");
+				[self addConstraint:c];
+				[self->inBetweens addObject:c];
+			}
+	for (x = 0; x < xcount; x++)
+		for (y = 1; y < ycount; y++)
+			if (gv[y - 1][x] != gv[y][x]) {
+				c = mkConstraint(gv[y - 1][x], NSLayoutAttributeBottom,
+					NSLayoutRelationEqual,
+					gv[y][x], NSLayoutAttributeTop,
+					1, -padding,
+					@"uiGrid internal vertical constraint");
+				[self addConstraint:c];
+				[self->inBetweens addObject:c];
+			}
+
+	// now set priorities for all widgets that expand or not
+	// if a cell is in an expanding row, OR If it spans, then it must be willing to stretch
+	// otherwise, it tries not to
+	// note we don't use NSLayoutPriorityRequired as that will cause things to squish when they shouldn't
+	for (gc in self->children) {
+		NSLayoutPriority priority;
+
+		if (!uiControlVisible(gc.c))
+			continue;
+		if (hexpand[gc.left - xmin] || gc.xspan != 1)
+			priority = NSLayoutPriorityDefaultLow;
+		else
+			priority = NSLayoutPriorityDefaultHigh;
+		uiDarwinControlSetHuggingPriority(uiDarwinControl(gc.c), priority, NSLayoutConstraintOrientationHorizontal);
+		// same for vertical direction
+		if (vexpand[gc.top - ymin] || gc.yspan != 1)
+			priority = NSLayoutPriorityDefaultLow;
+		else
+			priority = NSLayoutPriorityDefaultHigh;
+		uiDarwinControlSetHuggingPriority(uiDarwinControl(gc.c), priority, NSLayoutConstraintOrientationVertical);
+	}
+
+	// TODO make all expanding rows/columns the same height/width
+
+	// and finally clean up
+	uiFree(hexpand);
+	uiFree(vexpand);
+	for (y = 0; y < ycount; y++) {
+		uiFree(gg[y]);
+		uiFree(gv[y]);
+		uiFree(gspan[y]);
+	}
+	uiFree(gg);
+	uiFree(gv);
+	uiFree(gspan);
+}
+
+- (void)append:(gridChild *)gc
+{
+	BOOL update;
+	int oldnh, oldnv;
+
+	[gc setTranslatesAutoresizingMaskIntoConstraints:NO];
+	[self addSubview:gc];
+
+	// no need to set priority here; that's done in establishOurConstraints
+
+	oldnh = [self nhexpand];
+	oldnv = [self nvexpand];
+	[self->children addObject:gc];
+
+	[self establishOurConstraints];
+	update = NO;
+	if (gc.hexpand)
+		if (oldnh == 0)
+			update = YES;
+	if (gc.vexpand)
+		if (oldnv == 0)
+			update = YES;
+	if (update)
+		uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl(self->g));
+
+	[gc release];		// we don't need the initial reference now
+}
+
+- (void)insert:(gridChild *)gc after:(uiControl *)c at:(uiAt)at
+{
+	gridChild *other;
+	BOOL found;
+
+	found = NO;
+	for (other in self->children)
+		if (other.c == c) {
+			found = YES;
+			break;
+		}
+	if (!found)
+		userbug("Existing control %p is not in grid %p; you cannot add other controls next to it", c, self->g);
+
+	switch (at) {
+	case uiAtLeading:
+		gc.left = other.left - gc.xspan;
+		gc.top = other.top;
+		break;
+	case uiAtTop:
+		gc.left = other.left;
+		gc.top = other.top - gc.yspan;
+		break;
+	case uiAtTrailing:
+		gc.left = other.left + other.xspan;
+		gc.top = other.top;
+		break;
+	case uiAtBottom:
+		gc.left = other.left;
+		gc.top = other.top + other.yspan;
+		break;
+	// TODO add error checks to ALL enums
+	}
+
+	[self append:gc];
+}
+
+- (int)isPadded
+{
+	return self->padded;
+}
+
+- (void)setPadded:(int)p
+{
+	CGFloat padding;
+	NSLayoutConstraint *c;
+
+#if 0 /* TODO */
+dispatch_after(
+dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC),
+dispatch_get_main_queue(),
+^{ [[self window] visualizeConstraints:[self constraints]]; }
+);
+#endif
+	self->padded = p;
+	padding = [self paddingAmount];
+	for (c in self->inBetweens)
+		switch ([c firstAttribute]) {
+		case NSLayoutAttributeLeading:
+		case NSLayoutAttributeTop:
+			[c setConstant:padding];
+			break;
+		case NSLayoutAttributeTrailing:
+		case NSLayoutAttributeBottom:
+			[c setConstant:-padding];
+			break;
+		}
+}
+
+- (BOOL)hugsTrailing
+{
+	// only hug if we have horizontally expanding
+	return [self nhexpand] != 0;
+}
+
+- (BOOL)hugsBottom
+{
+	// only hug if we have vertically expanding
+	return [self nvexpand] != 0;
+}
+
+- (int)nhexpand
+{
+	gridChild *gc;
+	int n;
+
+	n = 0;
+	for (gc in self->children) {
+		if (!uiControlVisible(gc.c))
+			continue;
+		if (gc.hexpand)
+			n++;
+	}
+	return n;
+}
+
+- (int)nvexpand
+{
+	gridChild *gc;
+	int n;
+
+	n = 0;
+	for (gc in self->children) {
+		if (!uiControlVisible(gc.c))
+			continue;
+		if (gc.vexpand)
+			n++;
+	}
+	return n;
+}
+
+@end
+
+static void uiGridDestroy(uiControl *c)
+{
+	uiGrid *g = uiGrid(c);
+
+	[g->view onDestroy];
+	[g->view release];
+	uiFreeControl(uiControl(g));
+}
+
+uiDarwinControlDefaultHandle(uiGrid, view)
+uiDarwinControlDefaultParent(uiGrid, view)
+uiDarwinControlDefaultSetParent(uiGrid, view)
+uiDarwinControlDefaultToplevel(uiGrid, view)
+uiDarwinControlDefaultVisible(uiGrid, view)
+uiDarwinControlDefaultShow(uiGrid, view)
+uiDarwinControlDefaultHide(uiGrid, view)
+uiDarwinControlDefaultEnabled(uiGrid, view)
+uiDarwinControlDefaultEnable(uiGrid, view)
+uiDarwinControlDefaultDisable(uiGrid, view)
+
+static void uiGridSyncEnableState(uiDarwinControl *c, int enabled)
+{
+	uiGrid *g = uiGrid(c);
+
+	if (uiDarwinShouldStopSyncEnableState(uiDarwinControl(g), enabled))
+		return;
+	[g->view syncEnableStates:enabled];
+}
+
+uiDarwinControlDefaultSetSuperview(uiGrid, view)
+
+static BOOL uiGridHugsTrailingEdge(uiDarwinControl *c)
+{
+	uiGrid *g = uiGrid(c);
+
+	return [g->view hugsTrailing];
+}
+
+static BOOL uiGridHugsBottom(uiDarwinControl *c)
+{
+	uiGrid *g = uiGrid(c);
+
+	return [g->view hugsBottom];
+}
+
+static void uiGridChildEdgeHuggingChanged(uiDarwinControl *c)
+{
+	uiGrid *g = uiGrid(c);
+
+	[g->view establishOurConstraints];
+}
+
+uiDarwinControlDefaultHuggingPriority(uiGrid, view)
+uiDarwinControlDefaultSetHuggingPriority(uiGrid, view)
+
+static void uiGridChildVisibilityChanged(uiDarwinControl *c)
+{
+	uiGrid *g = uiGrid(c);
+
+	[g->view establishOurConstraints];
+}
+
+static gridChild *toChild(uiControl *c, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign, uiGrid *g)
+{
+	gridChild *gc;
+
+	if (xspan < 0)
+		userbug("You cannot have a negative xspan in a uiGrid cell.");
+	if (yspan < 0)
+		userbug("You cannot have a negative yspan in a uiGrid cell.");
+	gc = [gridChild new];
+	gc.xspan = xspan;
+	gc.yspan = yspan;
+	gc.hexpand = hexpand;
+	gc.halign = halign;
+	gc.vexpand = vexpand;
+	gc.valign = valign;
+	[gc setC:c grid:g];
+	return gc;
+}
+
+void uiGridAppend(uiGrid *g, uiControl *c, int left, int top, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign)
+{
+	gridChild *gc;
+
+	// LONGTERM on other platforms
+	// or at leat allow this and implicitly turn it into a spacer
+	if (c == NULL)
+		userbug("You cannot add NULL to a uiGrid.");
+	gc = toChild(c, xspan, yspan, hexpand, halign, vexpand, valign, g);
+	gc.left = left;
+	gc.top = top;
+	[g->view append:gc];
+}
+
+void uiGridInsertAt(uiGrid *g, uiControl *c, uiControl *existing, uiAt at, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign)
+{
+	gridChild *gc;
+
+	gc = toChild(c, xspan, yspan, hexpand, halign, vexpand, valign, g);
+	[g->view insert:gc after:existing at:at];
+}
+
+int uiGridPadded(uiGrid *g)
+{
+	return [g->view isPadded];
+}
+
+void uiGridSetPadded(uiGrid *g, int padded)
+{
+	[g->view setPadded:padded];
+}
+
+uiGrid *uiNewGrid(void)
+{
+	uiGrid *g;
+
+	uiDarwinNewControl(uiGrid, g);
+
+	g->view = [[gridView alloc] initWithG:g];
+
+	return g;
+}
diff --git a/src/libui_sdl/libui/darwin/group.m b/src/libui_sdl/libui/darwin/group.m
new file mode 100644
index 0000000..0050bbd
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/group.m
@@ -0,0 +1,194 @@
+// 14 august 2015
+#import "uipriv_darwin.h"
+
+struct uiGroup {
+	uiDarwinControl c;
+	NSBox *box;
+	uiControl *child;
+	NSLayoutPriority oldHorzHuggingPri;
+	NSLayoutPriority oldVertHuggingPri;
+	int margined;
+	struct singleChildConstraints constraints;
+	NSLayoutPriority horzHuggingPri;
+	NSLayoutPriority vertHuggingPri;
+};
+
+static void removeConstraints(uiGroup *g)
+{
+	// set to contentView instead of to the box itself, otherwise we get clipping underneath the label
+	singleChildConstraintsRemove(&(g->constraints), [g->box contentView]);
+}
+
+static void uiGroupDestroy(uiControl *c)
+{
+	uiGroup *g = uiGroup(c);
+
+	removeConstraints(g);
+	if (g->child != NULL) {
+		uiControlSetParent(g->child, NULL);
+		uiDarwinControlSetSuperview(uiDarwinControl(g->child), nil);
+		uiControlDestroy(g->child);
+	}
+	[g->box release];
+	uiFreeControl(uiControl(g));
+}
+
+uiDarwinControlDefaultHandle(uiGroup, box)
+uiDarwinControlDefaultParent(uiGroup, box)
+uiDarwinControlDefaultSetParent(uiGroup, box)
+uiDarwinControlDefaultToplevel(uiGroup, box)
+uiDarwinControlDefaultVisible(uiGroup, box)
+uiDarwinControlDefaultShow(uiGroup, box)
+uiDarwinControlDefaultHide(uiGroup, box)
+uiDarwinControlDefaultEnabled(uiGroup, box)
+uiDarwinControlDefaultEnable(uiGroup, box)
+uiDarwinControlDefaultDisable(uiGroup, box)
+
+static void uiGroupSyncEnableState(uiDarwinControl *c, int enabled)
+{
+	uiGroup *g = uiGroup(c);
+
+	if (uiDarwinShouldStopSyncEnableState(uiDarwinControl(g), enabled))
+		return;
+	if (g->child != NULL)
+		uiDarwinControlSyncEnableState(uiDarwinControl(g->child), enabled);
+}
+
+uiDarwinControlDefaultSetSuperview(uiGroup, box)
+
+static void groupRelayout(uiGroup *g)
+{
+	NSView *childView;
+
+	removeConstraints(g);
+	if (g->child == NULL)
+		return;
+	childView = (NSView *) uiControlHandle(g->child);
+	singleChildConstraintsEstablish(&(g->constraints),
+		[g->box contentView], childView,
+		uiDarwinControlHugsTrailingEdge(uiDarwinControl(g->child)),
+		uiDarwinControlHugsBottom(uiDarwinControl(g->child)),
+		g->margined,
+		@"uiGroup");
+	// needed for some very rare drawing errors...
+	jiggleViewLayout(g->box);
+}
+
+// TODO rename these since I'm starting to get confused by what they mean by hugging
+BOOL uiGroupHugsTrailingEdge(uiDarwinControl *c)
+{
+	uiGroup *g = uiGroup(c);
+
+	// TODO make a function?
+	return g->horzHuggingPri < NSLayoutPriorityWindowSizeStayPut;
+}
+
+BOOL uiGroupHugsBottom(uiDarwinControl *c)
+{
+	uiGroup *g = uiGroup(c);
+
+	return g->vertHuggingPri < NSLayoutPriorityWindowSizeStayPut;
+}
+
+static void uiGroupChildEdgeHuggingChanged(uiDarwinControl *c)
+{
+	uiGroup *g = uiGroup(c);
+
+	groupRelayout(g);
+}
+
+static NSLayoutPriority uiGroupHuggingPriority(uiDarwinControl *c, NSLayoutConstraintOrientation orientation)
+{
+	uiGroup *g = uiGroup(c);
+
+	if (orientation == NSLayoutConstraintOrientationHorizontal)
+		return g->horzHuggingPri;
+	return g->vertHuggingPri;
+}
+
+static void uiGroupSetHuggingPriority(uiDarwinControl *c, NSLayoutPriority priority, NSLayoutConstraintOrientation orientation)
+{
+	uiGroup *g = uiGroup(c);
+
+	if (orientation == NSLayoutConstraintOrientationHorizontal)
+		g->horzHuggingPri = priority;
+	else
+		g->vertHuggingPri = priority;
+	uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl(g));
+}
+
+static void uiGroupChildVisibilityChanged(uiDarwinControl *c)
+{
+	uiGroup *g = uiGroup(c);
+
+	groupRelayout(g);
+}
+
+char *uiGroupTitle(uiGroup *g)
+{
+	return uiDarwinNSStringToText([g->box title]);
+}
+
+void uiGroupSetTitle(uiGroup *g, const char *title)
+{
+	[g->box setTitle:toNSString(title)];
+}
+
+void uiGroupSetChild(uiGroup *g, uiControl *child)
+{
+	NSView *childView;
+
+	if (g->child != NULL) {
+		removeConstraints(g);
+		uiDarwinControlSetHuggingPriority(uiDarwinControl(g->child), g->oldHorzHuggingPri, NSLayoutConstraintOrientationHorizontal);
+		uiDarwinControlSetHuggingPriority(uiDarwinControl(g->child), g->oldVertHuggingPri, NSLayoutConstraintOrientationVertical);
+		uiControlSetParent(g->child, NULL);
+		uiDarwinControlSetSuperview(uiDarwinControl(g->child), nil);
+	}
+	g->child = child;
+	if (g->child != NULL) {
+		childView = (NSView *) uiControlHandle(g->child);
+		uiControlSetParent(g->child, uiControl(g));
+		uiDarwinControlSetSuperview(uiDarwinControl(g->child), [g->box contentView]);
+		uiDarwinControlSyncEnableState(uiDarwinControl(g->child), uiControlEnabledToUser(uiControl(g)));
+		// don't hug, just in case we're a stretchy group
+		g->oldHorzHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(g->child), NSLayoutConstraintOrientationHorizontal);
+		g->oldVertHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(g->child), NSLayoutConstraintOrientationVertical);
+		uiDarwinControlSetHuggingPriority(uiDarwinControl(g->child), NSLayoutPriorityDefaultLow, NSLayoutConstraintOrientationHorizontal);
+		uiDarwinControlSetHuggingPriority(uiDarwinControl(g->child), NSLayoutPriorityDefaultLow, NSLayoutConstraintOrientationVertical);
+	}
+	groupRelayout(g);
+}
+
+int uiGroupMargined(uiGroup *g)
+{
+	return g->margined;
+}
+
+void uiGroupSetMargined(uiGroup *g, int margined)
+{
+	g->margined = margined;
+	singleChildConstraintsSetMargined(&(g->constraints), g->margined);
+}
+
+uiGroup *uiNewGroup(const char *title)
+{
+	uiGroup *g;
+
+	uiDarwinNewControl(uiGroup, g);
+
+	g->box = [[NSBox alloc] initWithFrame:NSZeroRect];
+	[g->box setTitle:toNSString(title)];
+	[g->box setBoxType:NSBoxPrimary];
+	[g->box setBorderType:NSLineBorder];
+	[g->box setTransparent:NO];
+	[g->box setTitlePosition:NSAtTop];
+	// we can't use uiDarwinSetControlFont() because the selector is different
+	[g->box setTitleFont:[NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSSmallControlSize]]];
+
+	// default to low hugging to not hug edges
+	g->horzHuggingPri = NSLayoutPriorityDefaultLow;
+	g->vertHuggingPri = NSLayoutPriorityDefaultLow;
+
+	return g;
+}
diff --git a/src/libui_sdl/libui/darwin/image.m b/src/libui_sdl/libui/darwin/image.m
new file mode 100644
index 0000000..b62de31
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/image.m
@@ -0,0 +1,82 @@
+// 25 june 2016
+#import "uipriv_darwin.h"
+
+struct uiImage {
+	NSImage *i;
+	NSSize size;
+	NSMutableArray *swizzled;
+};
+
+uiImage *uiNewImage(double width, double height)
+{
+	uiImage *i;
+
+	i = uiNew(uiImage);
+	i->size = NSMakeSize(width, height);
+	i->i = [[NSImage alloc] initWithSize:i->size];
+	i->swizzled = [NSMutableArray new];
+	return i;
+}
+
+void uiFreeImage(uiImage *i)
+{
+	NSValue *v;
+
+	[i->i release];
+	// to be safe, do this after releasing the image
+	for (v in i->swizzled)
+		uiFree([v pointerValue]);
+	[i->swizzled release];
+	uiFree(i);
+}
+
+void uiImageAppend(uiImage *i, void *pixels, int pixelWidth, int pixelHeight, int pixelStride)
+{
+	NSBitmapImageRep *repCalibrated, *repsRGB;
+	uint8_t *swizzled, *bp, *sp;
+	int x, y;
+	unsigned char *pix[1];
+
+	// OS X demands that R and B are in the opposite order from what we expect
+	// we must swizzle :(
+	// LONGTERM test on a big-endian system
+	swizzled = (uint8_t *) uiAlloc((pixelStride * pixelHeight * 4) * sizeof (uint8_t), "uint8_t[]");
+	bp = (uint8_t *) pixels;
+	sp = swizzled;
+	for (y = 0; y < pixelHeight * pixelStride; y += pixelStride)
+		for (x = 0; x < pixelStride; x++) {
+			sp[0] = bp[2];
+			sp[1] = bp[1];
+			sp[2] = bp[0];
+			sp[3] = bp[3];
+			sp += 4;
+			bp += 4;
+		}
+
+	pix[0] = (unsigned char *) swizzled;
+	repCalibrated = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:pix
+		pixelsWide:pixelWidth
+		pixelsHigh:pixelHeight
+		bitsPerSample:8
+		samplesPerPixel:4
+		hasAlpha:YES
+		isPlanar:NO
+		colorSpaceName:NSCalibratedRGBColorSpace
+		bitmapFormat:0
+		bytesPerRow:pixelStride
+		bitsPerPixel:32];
+	repsRGB = [repCalibrated bitmapImageRepByRetaggingWithColorSpace:[NSColorSpace sRGBColorSpace]];
+	[repCalibrated release];
+
+	[i->i addRepresentation:repsRGB];
+	[repsRGB setSize:i->size];
+	[repsRGB release];
+
+	// we need to keep swizzled alive for NSBitmapImageRep
+	[i->swizzled addObject:[NSValue valueWithPointer:swizzled]];
+}
+
+NSImage *imageImage(uiImage *i)
+{
+	return i->i;
+}
diff --git a/src/libui_sdl/libui/darwin/label.m b/src/libui_sdl/libui/darwin/label.m
new file mode 100644
index 0000000..897bc3f
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/label.m
@@ -0,0 +1,43 @@
+// 14 august 2015
+#import "uipriv_darwin.h"
+
+struct uiLabel {
+	uiDarwinControl c;
+	NSTextField *textfield;
+};
+
+uiDarwinControlAllDefaults(uiLabel, textfield)
+
+char *uiLabelText(uiLabel *l)
+{
+	return uiDarwinNSStringToText([l->textfield stringValue]);
+}
+
+void uiLabelSetText(uiLabel *l, const char *text)
+{
+	[l->textfield setStringValue:toNSString(text)];
+}
+
+NSTextField *newLabel(NSString *str)
+{
+	NSTextField *tf;
+
+	tf = [[NSTextField alloc] initWithFrame:NSZeroRect];
+	[tf setStringValue:str];
+	[tf setEditable:NO];
+	[tf setSelectable:NO];
+	[tf setDrawsBackground:NO];
+	finishNewTextField(tf, NO);
+	return tf;
+}
+
+uiLabel *uiNewLabel(const char *text)
+{
+	uiLabel *l;
+
+	uiDarwinNewControl(uiLabel, l);
+
+	l->textfield = newLabel(toNSString(text));
+
+	return l;
+}
diff --git a/src/libui_sdl/libui/darwin/main.m b/src/libui_sdl/libui/darwin/main.m
new file mode 100644
index 0000000..59a8683
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/main.m
@@ -0,0 +1,239 @@
+// 6 april 2015
+#import "uipriv_darwin.h"
+
+static BOOL canQuit = NO;
+static NSAutoreleasePool *globalPool;
+static applicationClass *app;
+static appDelegate *delegate;
+
+static BOOL (^isRunning)(void);
+static BOOL stepsIsRunning;
+
+@implementation applicationClass
+
+- (void)sendEvent:(NSEvent *)e
+{
+	if (sendAreaEvents(e) != 0)
+		return;
+	[super sendEvent:e];
+}
+
+// NSColorPanel always sends changeColor: to the first responder regardless of whether there's a target set on it
+// we can override it here (see colorbutton.m)
+// thanks to mikeash in irc.freenode.net/#macdev for informing me this is how the first responder chain is initiated
+// it turns out NSFontManager also sends changeFont: through this; let's inhibit that here too (see fontbutton.m)
+- (BOOL)sendAction:(SEL)sel to:(id)to from:(id)from
+{
+	if (colorButtonInhibitSendAction(sel, from, to))
+		return NO;
+	if (fontButtonInhibitSendAction(sel, from, to))
+		return NO;
+	return [super sendAction:sel to:to from:from];
+}
+
+// likewise, NSFontManager also sends NSFontPanelValidation messages to the first responder, however it does NOT use sendAction:from:to:!
+// instead, it uses this one (thanks swillits in irc.freenode.net/#macdev)
+// we also need to override it (see fontbutton.m)
+- (id)targetForAction:(SEL)sel to:(id)to from:(id)from
+{
+	id override;
+
+	if (fontButtonOverrideTargetForAction(sel, from, to, &override))
+		return override;
+	return [super targetForAction:sel to:to from:from];
+}
+
+// hey look! we're overriding terminate:!
+// we're going to make sure we can go back to main() whether Cocoa likes it or not!
+// and just how are we going to do that, hm?
+// (note: this is called after applicationShouldTerminate:)
+- (void)terminate:(id)sender
+{
+	// yes that's right folks: DO ABSOLUTELY NOTHING.
+	// the magic is [NSApp run] will just... stop.
+
+	// well let's not do nothing; let's actually quit our graceful way
+	NSEvent *e;
+
+	if (!canQuit)
+		implbug("call to [NSApp terminate:] when not ready to terminate; definitely contact andlabs");
+
+	[realNSApp() stop:realNSApp()];
+	// stop: won't register until another event has passed; let's synthesize one
+	e = [NSEvent otherEventWithType:NSApplicationDefined
+		location:NSZeroPoint
+		modifierFlags:0
+		timestamp:[[NSProcessInfo processInfo] systemUptime]
+		windowNumber:0
+		context:[NSGraphicsContext currentContext]
+		subtype:0
+		data1:0
+		data2:0];
+	[realNSApp() postEvent:e atStart:NO];		// let pending events take priority (this is what PostQuitMessage() on Windows does so we have to do it here too for parity; thanks to mikeash in irc.freenode.net/#macdev for confirming that this parameter should indeed be NO)
+
+	// and in case uiMainSteps() was called
+	stepsIsRunning = NO;
+}
+
+@end
+
+@implementation appDelegate
+
+- (void)dealloc
+{
+	// Apple docs: "Don't Use Accessor Methods in Initializer Methods and dealloc"
+	[_menuManager release];
+	[super dealloc];
+}
+
+- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)app
+{
+	// for debugging
+	NSLog(@"in applicationShouldTerminate:");
+	if (shouldQuit()) {
+		canQuit = YES;
+		// this will call terminate:, which is the same as uiQuit()
+		return NSTerminateNow;
+	}
+	return NSTerminateCancel;
+}
+
+- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)app
+{
+	return NO;
+}
+
+@end
+
+uiInitOptions options;
+
+const char *uiInit(uiInitOptions *o)
+{
+	@autoreleasepool {
+		options = *o;
+		app = [[applicationClass sharedApplication] retain];
+		// don't check for a NO return; something (launch services?) causes running from application bundles to always return NO when asking to change activation policy, even if the change is to the same activation policy!
+		// see https://github.com/andlabs/ui/issues/6
+		[realNSApp() setActivationPolicy:NSApplicationActivationPolicyRegular];
+		delegate = [appDelegate new];
+		[realNSApp() setDelegate:delegate];
+
+		initAlloc();
+
+		// always do this so we always have an application menu
+		appDelegate().menuManager = [[menuManager new] autorelease];
+		[realNSApp() setMainMenu:[appDelegate().menuManager makeMenubar]];
+
+		setupFontPanel();
+	}
+
+	globalPool = [[NSAutoreleasePool alloc] init];
+
+	return NULL;
+}
+
+void uiUninit(void)
+{
+	if (!globalPool) {
+		userbug("You must call uiInit() first!");
+	}
+	[globalPool release];
+
+	@autoreleasepool {
+		[delegate release];
+		[realNSApp() setDelegate:nil];
+		[app release];
+		uninitAlloc();
+	}
+}
+
+void uiFreeInitError(const char *err)
+{
+}
+
+void uiMain(void)
+{
+	isRunning = ^{
+		return [realNSApp() isRunning];
+	};
+	[realNSApp() run];
+}
+
+void uiMainSteps(void)
+{
+	// SDL does this and it seems to be necessary for the menubar to work (see #182)
+	[realNSApp() finishLaunching];
+	isRunning = ^{
+		return stepsIsRunning;
+	};
+	stepsIsRunning = YES;
+}
+
+int uiMainStep(int wait)
+{
+	struct nextEventArgs nea;
+
+	nea.mask = NSAnyEventMask;
+
+	// ProPuke did this in his original PR requesting this
+	// I'm not sure if this will work, but I assume it will...
+	nea.duration = [NSDate distantPast];
+	if (wait)		// but this is normal so it will work
+		nea.duration = [NSDate distantFuture];
+
+	nea.mode = NSDefaultRunLoopMode;
+	nea.dequeue = YES;
+
+	return mainStep(&nea, ^(NSEvent *e) {
+		return NO;
+	});
+}
+
+// see also:
+// - http://www.cocoawithlove.com/2009/01/demystifying-nsapplication-by.html
+// - https://github.com/gnustep/gui/blob/master/Source/NSApplication.m
+int mainStep(struct nextEventArgs *nea, BOOL (^interceptEvent)(NSEvent *e))
+{
+	NSDate *expire;
+	NSEvent *e;
+	NSEventType type;
+
+	@autoreleasepool {
+		if (!isRunning())
+			return 0;
+
+		e = [realNSApp() nextEventMatchingMask:nea->mask
+			untilDate:nea->duration
+			inMode:nea->mode
+			dequeue:nea->dequeue];
+		if (e == nil)
+			return 1;
+
+		type = [e type];
+		if (!interceptEvent(e))
+			[realNSApp() sendEvent:e];
+		[realNSApp() updateWindows];
+
+		// GNUstep does this
+		// it also updates the Services menu but there doesn't seem to be a public API for that so
+		if (type != NSPeriodic && type != NSMouseMoved)
+			[[realNSApp() mainMenu] update];
+
+		return 1;
+	}
+}
+
+void uiQuit(void)
+{
+	canQuit = YES;
+	[realNSApp() terminate:realNSApp()];
+}
+
+// thanks to mikeash in irc.freenode.net/#macdev for suggesting the use of Grand Central Dispatch for this
+// LONGTERM will dispatch_get_main_queue() break after _CFRunLoopSetCurrent()?
+void uiQueueMain(void (*f)(void *data), void *data)
+{
+	// dispatch_get_main_queue() is a serial queue so it will not execute multiple uiQueueMain() functions concurrently
+	// the signature of f matches dispatch_function_t
+	dispatch_async_f(dispatch_get_main_queue(), data, f);
+}
diff --git a/src/libui_sdl/libui/darwin/map.m b/src/libui_sdl/libui/darwin/map.m
new file mode 100644
index 0000000..46a7b8d
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/map.m
@@ -0,0 +1,59 @@
+// 17 august 2015
+#import "uipriv_darwin.h"
+
+// unfortunately NSMutableDictionary copies its keys, meaning we can't use it for pointers
+// hence, this file
+// we could expose a NSMapTable directly, but let's treat all pointers as opaque and hide the implementation, just to be safe and prevent even more rewrites later
+struct mapTable {
+	NSMapTable *m;
+};
+
+struct mapTable *newMap(void)
+{
+	struct mapTable *m;
+
+	m = uiNew(struct mapTable);
+	m->m = [[NSMapTable alloc] initWithKeyOptions:(NSPointerFunctionsOpaqueMemory | NSPointerFunctionsOpaquePersonality)
+		valueOptions:(NSPointerFunctionsOpaqueMemory | NSPointerFunctionsOpaquePersonality)
+		capacity:0];
+	return m;
+}
+
+void mapDestroy(struct mapTable *m)
+{
+	if ([m->m count] != 0)
+		implbug("attempt to destroy map with items inside");
+	[m->m release];
+	uiFree(m);
+}
+
+void *mapGet(struct mapTable *m, void *key)
+{
+	return NSMapGet(m->m, key);
+}
+
+void mapSet(struct mapTable *m, void *key, void *value)
+{
+	NSMapInsert(m->m, key, value);
+}
+
+void mapDelete(struct mapTable *m, void *key)
+{
+	NSMapRemove(m->m, key);
+}
+
+void mapWalk(struct mapTable *m, void (*f)(void *key, void *value))
+{
+	NSMapEnumerator e = NSEnumerateMapTable(m->m);
+	void *k = NULL;
+	void *v = NULL;
+	while (NSNextMapEnumeratorPair(&e, &k, &v)) {
+		f(k, v);
+	}
+	NSEndMapTableEnumeration(&e);
+}
+
+void mapReset(struct mapTable *m)
+{
+	NSResetMapTable(m->m);
+}
diff --git a/src/libui_sdl/libui/darwin/menu.m b/src/libui_sdl/libui/darwin/menu.m
new file mode 100644
index 0000000..735cac5
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/menu.m
@@ -0,0 +1,368 @@
+// 28 april 2015
+#import "uipriv_darwin.h"
+
+static NSMutableArray *menus = nil;
+static BOOL menusFinalized = NO;
+
+struct uiMenu {
+	NSMenu *menu;
+	NSMenuItem *item;
+	NSMutableArray *items;
+};
+
+struct uiMenuItem {
+	NSMenuItem *item;
+	int type;
+	BOOL disabled;
+	void (*onClicked)(uiMenuItem *, uiWindow *, void *);
+	void *onClickedData;
+};
+
+enum {
+	typeRegular,
+	typeCheckbox,
+	typeQuit,
+	typePreferences,
+	typeAbout,
+	typeSeparator,
+};
+
+static void mapItemReleaser(void *key, void *value)
+{
+	uiMenuItem *item;
+ 
+	item = (uiMenuItem *)value;
+	[item->item release];
+}
+
+@implementation menuManager
+
+- (id)init
+{
+	self = [super init];
+	if (self) {
+		self->items = newMap();
+		self->hasQuit = NO;
+		self->hasPreferences = NO;
+		self->hasAbout = NO;
+	}
+	return self;
+}
+
+- (void)dealloc
+{
+	mapWalk(self->items, mapItemReleaser);
+	mapReset(self->items);
+	mapDestroy(self->items);
+	uninitMenus();
+	[super dealloc];
+}
+
+- (IBAction)onClicked:(id)sender
+{
+	uiMenuItem *item;
+
+	item = (uiMenuItem *) mapGet(self->items, sender);
+	if (item->type == typeCheckbox)
+		uiMenuItemSetChecked(item, !uiMenuItemChecked(item));
+	// use the key window as the source of the menu event; it's the active window
+	(*(item->onClicked))(item, windowFromNSWindow([realNSApp() keyWindow]), item->onClickedData);
+}
+
+- (IBAction)onQuitClicked:(id)sender
+{
+	if (shouldQuit())
+		uiQuit();
+}
+
+- (void)register:(NSMenuItem *)item to:(uiMenuItem *)smi
+{
+	switch (smi->type) {
+	case typeQuit:
+		if (self->hasQuit)
+			userbug("You can't have multiple Quit menu items in one program.");
+		self->hasQuit = YES;
+		break;
+	case typePreferences:
+		if (self->hasPreferences)
+			userbug("You can't have multiple Preferences menu items in one program.");
+		self->hasPreferences = YES;
+		break;
+	case typeAbout:
+		if (self->hasAbout)
+			userbug("You can't have multiple About menu items in one program.");
+		self->hasAbout = YES;
+		break;
+	}
+	mapSet(self->items, item, smi);
+}
+
+// on OS X there are two ways to handle menu items being enabled or disabled: automatically and manually
+// unfortunately, the application menu requires automatic menu handling for the Hide, Hide Others, and Show All items to work correctly
+// therefore, we have to handle enabling of the other options ourselves
+- (BOOL)validateMenuItem:(NSMenuItem *)item
+{
+	uiMenuItem *smi;
+
+	// disable the special items if they aren't present
+	if (item == self.quitItem && !self->hasQuit)
+		return NO;
+	if (item == self.preferencesItem && !self->hasPreferences)
+		return NO;
+	if (item == self.aboutItem && !self->hasAbout)
+		return NO;
+	// then poll the item's enabled/disabled state
+	smi = (uiMenuItem *) mapGet(self->items, item);
+	return !smi->disabled;
+}
+
+// Cocoa constructs the default application menu by hand for each program; that's what MainMenu.[nx]ib does
+- (void)buildApplicationMenu:(NSMenu *)menubar
+{
+	NSString *appName;
+	NSMenuItem *appMenuItem;
+	NSMenu *appMenu;
+	NSMenuItem *item;
+	NSString *title;
+	NSMenu *servicesMenu;
+
+	// note: no need to call setAppleMenu: on this anymore; see https://developer.apple.com/library/mac/releasenotes/AppKit/RN-AppKitOlderNotes/#X10_6Notes
+	appName = [[NSProcessInfo processInfo] processName];
+	appMenuItem = [[[NSMenuItem alloc] initWithTitle:appName action:NULL keyEquivalent:@""] autorelease];
+	appMenu = [[[NSMenu alloc] initWithTitle:appName] autorelease];
+	[appMenuItem setSubmenu:appMenu];
+	[menubar addItem:appMenuItem];
+
+	// first is About
+	title = [@"About " stringByAppendingString:appName];
+	item = [[[NSMenuItem alloc] initWithTitle:title action:@selector(onClicked:) keyEquivalent:@""] autorelease];
+	[item setTarget:self];
+	[appMenu addItem:item];
+	self.aboutItem = item;
+
+	[appMenu addItem:[NSMenuItem separatorItem]];
+
+	// next is Preferences
+	item = [[[NSMenuItem alloc] initWithTitle:@"Preferences…" action:@selector(onClicked:) keyEquivalent:@","] autorelease];
+	[item setTarget:self];
+	[appMenu addItem:item];
+	self.preferencesItem = item;
+
+	[appMenu addItem:[NSMenuItem separatorItem]];
+
+	// next is Services
+	item = [[[NSMenuItem alloc] initWithTitle:@"Services" action:NULL keyEquivalent:@""] autorelease];
+	servicesMenu = [[[NSMenu alloc] initWithTitle:@"Services"] autorelease];
+	[item setSubmenu:servicesMenu];
+	[realNSApp() setServicesMenu:servicesMenu];
+	[appMenu addItem:item];
+
+	[appMenu addItem:[NSMenuItem separatorItem]];
+
+	// next are the three hiding options
+	title = [@"Hide " stringByAppendingString:appName];
+	item = [[[NSMenuItem alloc] initWithTitle:title action:@selector(hide:) keyEquivalent:@"h"] autorelease];
+	// the .xib file says they go to -1 ("First Responder", which sounds wrong...)
+	// to do that, we simply leave the target as nil
+	[appMenu addItem:item];
+	item = [[[NSMenuItem alloc] initWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"] autorelease];
+	[item setKeyEquivalentModifierMask:(NSAlternateKeyMask | NSCommandKeyMask)];
+	[appMenu addItem:item];
+	item = [[[NSMenuItem alloc] initWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""] autorelease];
+	[appMenu addItem:item];
+
+	[appMenu addItem:[NSMenuItem separatorItem]];
+
+	// and finally Quit
+	// DON'T use @selector(terminate:) as the action; we handle termination ourselves
+	title = [@"Quit " stringByAppendingString:appName];
+	item = [[[NSMenuItem alloc] initWithTitle:title action:@selector(onQuitClicked:) keyEquivalent:@"q"] autorelease];
+	[item setTarget:self];
+	[appMenu addItem:item];
+	self.quitItem = item;
+}
+
+- (NSMenu *)makeMenubar
+{
+	NSMenu *menubar;
+
+	menubar = [[[NSMenu alloc] initWithTitle:@""] autorelease];
+	[self buildApplicationMenu:menubar];
+	return menubar;
+}
+
+@end
+
+static void defaultOnClicked(uiMenuItem *item, uiWindow *w, void *data)
+{
+	// do nothing
+}
+
+void uiMenuItemEnable(uiMenuItem *item)
+{
+	item->disabled = NO;
+	// we don't need to explicitly update the menus here; they'll be updated the next time they're opened (thanks mikeash in irc.freenode.net/#macdev)
+}
+
+void uiMenuItemDisable(uiMenuItem *item)
+{
+	item->disabled = YES;
+}
+
+void uiMenuItemOnClicked(uiMenuItem *item, void (*f)(uiMenuItem *, uiWindow *, void *), void *data)
+{
+	if (item->type == typeQuit)
+		userbug("You can't call uiMenuItemOnClicked() on a Quit item; use uiOnShouldQuit() instead.");
+	item->onClicked = f;
+	item->onClickedData = data;
+}
+
+int uiMenuItemChecked(uiMenuItem *item)
+{
+	return [item->item state] != NSOffState;
+}
+
+void uiMenuItemSetChecked(uiMenuItem *item, int checked)
+{
+	NSInteger state;
+
+	state = NSOffState;
+	if ([item->item state] == NSOffState)
+		state = NSOnState;
+	[item->item setState:state];
+}
+
+static uiMenuItem *newItem(uiMenu *m, int type, const char *name)
+{
+	@autoreleasepool {
+
+	uiMenuItem *item;
+
+	if (menusFinalized)
+		userbug("You can't create a new menu item after menus have been finalized.");
+
+	item = uiNew(uiMenuItem);
+
+	item->type = type;
+	switch (item->type) {
+	case typeQuit:
+		item->item = [appDelegate().menuManager.quitItem retain];
+		break;
+	case typePreferences:
+		item->item = [appDelegate().menuManager.preferencesItem retain];
+		break;
+	case typeAbout:
+		item->item = [appDelegate().menuManager.aboutItem retain];
+		break;
+	case typeSeparator:
+		item->item = [[NSMenuItem separatorItem] retain];
+		[m->menu addItem:item->item];
+		break;
+	default:
+		item->item = [[NSMenuItem alloc] initWithTitle:toNSString(name) action:@selector(onClicked:) keyEquivalent:@""];
+		[item->item setTarget:appDelegate().menuManager];
+		[m->menu addItem:item->item];
+		break;
+	}
+
+	[appDelegate().menuManager register:item->item to:item];
+	item->onClicked = defaultOnClicked;
+
+	[m->items addObject:[NSValue valueWithPointer:item]];
+
+	return item;
+
+	} // @autoreleasepool
+}
+
+uiMenuItem *uiMenuAppendItem(uiMenu *m, const char *name)
+{
+	return newItem(m, typeRegular, name);
+}
+
+uiMenuItem *uiMenuAppendCheckItem(uiMenu *m, const char *name)
+{
+	return newItem(m, typeCheckbox, name);
+}
+
+uiMenuItem *uiMenuAppendQuitItem(uiMenu *m)
+{
+	// duplicate check is in the register:to: selector
+	return newItem(m, typeQuit, NULL);
+}
+
+uiMenuItem *uiMenuAppendPreferencesItem(uiMenu *m)
+{
+	// duplicate check is in the register:to: selector
+	return newItem(m, typePreferences, NULL);
+}
+
+uiMenuItem *uiMenuAppendAboutItem(uiMenu *m)
+{
+	// duplicate check is in the register:to: selector
+	return newItem(m, typeAbout, NULL);
+}
+
+void uiMenuAppendSeparator(uiMenu *m)
+{
+	newItem(m, typeSeparator, NULL);
+}
+
+uiMenu *uiNewMenu(const char *name)
+{
+	@autoreleasepool {
+
+	uiMenu *m;
+
+	if (menusFinalized)
+		userbug("You can't create a new menu after menus have been finalized.");
+	if (menus == nil)
+		menus = [NSMutableArray new];
+
+	m = uiNew(uiMenu);
+
+	m->menu = [[NSMenu alloc] initWithTitle:toNSString(name)];
+	// use automatic menu item enabling for all menus for consistency's sake
+
+	m->item = [[NSMenuItem alloc] initWithTitle:toNSString(name) action:NULL keyEquivalent:@""];
+	[m->item setSubmenu:m->menu];
+
+	m->items = [NSMutableArray new];
+
+	[[realNSApp() mainMenu] addItem:m->item];
+
+	[menus addObject:[NSValue valueWithPointer:m]];
+
+	return m;
+
+	} // @autoreleasepool
+}
+
+void finalizeMenus(void)
+{
+	menusFinalized = YES;
+}
+
+void uninitMenus(void)
+{
+	if (menus == NULL)
+		return;
+	[menus enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) {
+		NSValue *v;
+		uiMenu *m;
+
+		v = (NSValue *) obj;
+		m = (uiMenu *) [v pointerValue];
+		[m->items enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) {
+			NSValue *v;
+			uiMenuItem *mi;
+
+			v = (NSValue *) obj;
+			mi = (uiMenuItem *) [v pointerValue];
+			uiFree(mi);
+		}];
+		[m->items release];
+		uiFree(m);
+	}];
+	[menus release];
+}
diff --git a/src/libui_sdl/libui/darwin/multilineentry.m b/src/libui_sdl/libui/darwin/multilineentry.m
new file mode 100644
index 0000000..605e900
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/multilineentry.m
@@ -0,0 +1,233 @@
+// 8 december 2015
+#import "uipriv_darwin.h"
+
+// NSTextView has no intrinsic content size by default, which wreaks havoc on a pure-Auto Layout system
+// we'll have to take over to get it to work
+// see also http://stackoverflow.com/questions/24210153/nstextview-not-properly-resizing-with-auto-layout and http://stackoverflow.com/questions/11237622/using-autolayout-with-expanding-nstextviews
+@interface intrinsicSizeTextView : NSTextView {
+	uiMultilineEntry *libui_e;
+}
+- (id)initWithFrame:(NSRect)r e:(uiMultilineEntry *)e;
+@end
+
+struct uiMultilineEntry {
+	uiDarwinControl c;
+	NSScrollView *sv;
+	intrinsicSizeTextView *tv;
+	struct scrollViewData *d;
+	void (*onChanged)(uiMultilineEntry *, void *);
+	void *onChangedData;
+	BOOL changing;
+};
+
+@implementation intrinsicSizeTextView
+
+- (id)initWithFrame:(NSRect)r e:(uiMultilineEntry *)e
+{
+	self = [super initWithFrame:r];
+	if (self)
+		self->libui_e = e;
+	return self;
+}
+
+- (NSSize)intrinsicContentSize
+{
+	NSTextContainer *textContainer;
+	NSLayoutManager *layoutManager;
+	NSRect rect;
+
+	textContainer = [self textContainer];
+	layoutManager = [self layoutManager];
+	[layoutManager ensureLayoutForTextContainer:textContainer];
+	rect = [layoutManager usedRectForTextContainer:textContainer];
+	return rect.size;
+}
+
+- (void)didChangeText
+{
+	[super didChangeText];
+	[self invalidateIntrinsicContentSize];
+	if (!self->libui_e->changing)
+		(*(self->libui_e->onChanged))(self->libui_e, self->libui_e->onChangedData);
+}
+
+@end
+
+uiDarwinControlAllDefaultsExceptDestroy(uiMultilineEntry, sv)
+
+static void uiMultilineEntryDestroy(uiControl *c)
+{
+	uiMultilineEntry *e = uiMultilineEntry(c);
+
+	scrollViewFreeData(e->sv, e->d);
+	[e->tv release];
+	[e->sv release];
+	uiFreeControl(uiControl(e));
+}
+
+static void defaultOnChanged(uiMultilineEntry *e, void *data)
+{
+	// do nothing
+}
+
+char *uiMultilineEntryText(uiMultilineEntry *e)
+{
+	return uiDarwinNSStringToText([e->tv string]);
+}
+
+void uiMultilineEntrySetText(uiMultilineEntry *e, const char *text)
+{
+	[[e->tv textStorage] replaceCharactersInRange:NSMakeRange(0, [[e->tv string] length])
+		withString:toNSString(text)];
+	// must be called explicitly according to the documentation of shouldChangeTextInRange:replacementString:
+	e->changing = YES;
+	[e->tv didChangeText];
+	e->changing = NO;
+}
+
+// TODO scroll to end?
+void uiMultilineEntryAppend(uiMultilineEntry *e, const char *text)
+{
+	[[e->tv textStorage] replaceCharactersInRange:NSMakeRange([[e->tv string] length], 0)
+		withString:toNSString(text)];
+	e->changing = YES;
+	[e->tv didChangeText];
+	e->changing = NO;
+}
+
+void uiMultilineEntryOnChanged(uiMultilineEntry *e, void (*f)(uiMultilineEntry *e, void *data), void *data)
+{
+	e->onChanged = f;
+	e->onChangedData = data;
+}
+
+int uiMultilineEntryReadOnly(uiMultilineEntry *e)
+{
+	return [e->tv isEditable] == NO;
+}
+
+void uiMultilineEntrySetReadOnly(uiMultilineEntry *e, int readonly)
+{
+	BOOL editable;
+
+	editable = YES;
+	if (readonly)
+		editable = NO;
+	[e->tv setEditable:editable];
+}
+
+static uiMultilineEntry *finishMultilineEntry(BOOL hscroll)
+{
+	uiMultilineEntry *e;
+	NSFont *font;
+	struct scrollViewCreateParams p;
+
+	uiDarwinNewControl(uiMultilineEntry, e);
+
+	e->tv = [[intrinsicSizeTextView alloc] initWithFrame:NSZeroRect e:e];
+
+	// verified against Interface Builder for a sufficiently customized text view
+
+	// NSText properties:
+	// this is what Interface Builder sets the background color to
+	[e->tv setBackgroundColor:[NSColor colorWithCalibratedWhite:1.0 alpha:1.0]];
+	[e->tv setDrawsBackground:YES];
+	[e->tv setEditable:YES];
+	[e->tv setSelectable:YES];
+	[e->tv setFieldEditor:NO];
+	[e->tv setRichText:NO];
+	[e->tv setImportsGraphics:NO];
+	[e->tv setUsesFontPanel:NO];
+	[e->tv setRulerVisible:NO];
+	// we'll handle font last
+	// while setAlignment: has been around since 10.0, the named constant "NSTextAlignmentNatural" seems to have only been introduced in 10.11
+#define ourNSTextAlignmentNatural 4
+	[e->tv setAlignment:ourNSTextAlignmentNatural];
+	// textColor is set to nil, just keep the dfault
+	[e->tv setBaseWritingDirection:NSWritingDirectionNatural];
+	[e->tv setHorizontallyResizable:NO];
+	[e->tv setVerticallyResizable:YES];
+
+	// NSTextView properties:
+	[e->tv setAllowsDocumentBackgroundColorChange:NO];
+	[e->tv setAllowsUndo:YES];
+	// default paragraph style is nil; keep default
+	[e->tv setAllowsImageEditing:NO];
+	[e->tv setAutomaticQuoteSubstitutionEnabled:NO];
+	[e->tv setAutomaticLinkDetectionEnabled:NO];
+	[e->tv setDisplaysLinkToolTips:YES];
+	[e->tv setUsesRuler:NO];
+	[e->tv setUsesInspectorBar:NO];
+	[e->tv setSelectionGranularity:NSSelectByCharacter];
+	// there is a dedicated named insertion point color but oh well
+	[e->tv setInsertionPointColor:[NSColor controlTextColor]];
+	// typing attributes is nil; keep default (we change it below for fonts though)
+	[e->tv setSmartInsertDeleteEnabled:NO];
+	[e->tv setContinuousSpellCheckingEnabled:NO];
+	[e->tv setGrammarCheckingEnabled:NO];
+	[e->tv setUsesFindPanel:YES];
+	[e->tv setEnabledTextCheckingTypes:0];
+	[e->tv setAutomaticDashSubstitutionEnabled:NO];
+	[e->tv setAutomaticDataDetectionEnabled:NO];
+	[e->tv setAutomaticSpellingCorrectionEnabled:NO];
+	[e->tv setAutomaticTextReplacementEnabled:NO];
+	[e->tv setUsesFindBar:NO];
+	[e->tv setIncrementalSearchingEnabled:NO];
+
+	// NSTextContainer properties:
+	[[e->tv textContainer] setWidthTracksTextView:YES];
+	[[e->tv textContainer] setHeightTracksTextView:NO];
+
+	// NSLayoutManager properties:
+	[[e->tv layoutManager] setAllowsNonContiguousLayout:YES];
+
+	// now just to be safe; this will do some of the above but whatever
+	disableAutocorrect(e->tv);
+
+	// see https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/TextUILayer/Tasks/TextInScrollView.html
+	// notice we don't use the Auto Layout code; see scrollview.m for more details
+	[e->tv setMaxSize:NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX)];
+	[e->tv setVerticallyResizable:YES];
+	[e->tv setHorizontallyResizable:hscroll];
+	if (hscroll) {
+		[e->tv setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
+		[[e->tv textContainer] setWidthTracksTextView:NO];
+	} else {
+		[e->tv setAutoresizingMask:NSViewWidthSizable];
+		[[e->tv textContainer] setWidthTracksTextView:YES];
+	}
+	[[e->tv textContainer] setContainerSize:NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX)];
+
+	// don't use uiDarwinSetControlFont() directly; we have to do a little extra work to set the font
+	font = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSRegularControlSize]];
+	[e->tv setTypingAttributes:[NSDictionary
+		dictionaryWithObject:font
+		forKey:NSFontAttributeName]];
+	// e->tv font from Interface Builder is nil, but setFont:nil throws an exception
+	// let's just set it to the standard control font anyway, just to be safe
+	[e->tv setFont:font];
+
+	memset(&p, 0, sizeof (struct scrollViewCreateParams));
+	p.DocumentView = e->tv;
+	// this is what Interface Builder sets it to
+	p.BackgroundColor = [NSColor colorWithCalibratedWhite:1.0 alpha:1.0];
+	p.DrawsBackground = YES;
+	p.Bordered = YES;
+	p.HScroll = hscroll;
+	p.VScroll = YES;
+	e->sv = mkScrollView(&p, &(e->d));
+
+	uiMultilineEntryOnChanged(e, defaultOnChanged, NULL);
+
+	return e;
+}
+
+uiMultilineEntry *uiNewMultilineEntry(void)
+{
+	return finishMultilineEntry(NO);
+}
+
+uiMultilineEntry *uiNewNonWrappingMultilineEntry(void)
+{
+	return finishMultilineEntry(YES);
+}
diff --git a/src/libui_sdl/libui/darwin/progressbar.m b/src/libui_sdl/libui/darwin/progressbar.m
new file mode 100644
index 0000000..b538228
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/progressbar.m
@@ -0,0 +1,78 @@
+// 14 august 2015
+#import "uipriv_darwin.h"
+
+// NSProgressIndicator has no intrinsic width by default; use the default width in Interface Builder
+#define progressIndicatorWidth 100
+
+@interface intrinsicWidthNSProgressIndicator : NSProgressIndicator
+@end
+
+@implementation intrinsicWidthNSProgressIndicator
+
+- (NSSize)intrinsicContentSize
+{
+	NSSize s;
+
+	s = [super intrinsicContentSize];
+	s.width = progressIndicatorWidth;
+	return s;
+}
+
+@end
+
+struct uiProgressBar {
+	uiDarwinControl c;
+	NSProgressIndicator *pi;
+};
+
+uiDarwinControlAllDefaults(uiProgressBar, pi)
+
+int uiProgressBarValue(uiProgressBar *p)
+{
+	if ([p->pi isIndeterminate])
+		return -1;
+	return [p->pi doubleValue];
+}
+
+void uiProgressBarSetValue(uiProgressBar *p, int value)
+{
+	if (value == -1) {
+		[p->pi setIndeterminate:YES];
+		[p->pi startAnimation:p->pi];
+		return;
+	}
+
+	if ([p->pi isIndeterminate]) {
+		[p->pi setIndeterminate:NO];
+		[p->pi stopAnimation:p->pi];
+	}
+
+	if (value < 0 || value > 100)
+		userbug("Value %d out of range for a uiProgressBar.", value);
+
+	// on 10.8 there's an animation when the progress bar increases, just like with Aero
+	if (value == 100) {
+		[p->pi setMaxValue:101];
+		[p->pi setDoubleValue:101];
+		[p->pi setDoubleValue:100];
+		[p->pi setMaxValue:100];
+		return;
+	}
+	[p->pi setDoubleValue:((double) (value + 1))];
+	[p->pi setDoubleValue:((double) value)];
+}
+
+uiProgressBar *uiNewProgressBar(void)
+{
+	uiProgressBar *p;
+
+	uiDarwinNewControl(uiProgressBar, p);
+
+	p->pi = [[intrinsicWidthNSProgressIndicator alloc] initWithFrame:NSZeroRect];
+	[p->pi setControlSize:NSRegularControlSize];
+	[p->pi setBezeled:YES];
+	[p->pi setStyle:NSProgressIndicatorBarStyle];
+	[p->pi setIndeterminate:NO];
+
+	return p;
+}
diff --git a/src/libui_sdl/libui/darwin/radiobuttons.m b/src/libui_sdl/libui/darwin/radiobuttons.m
new file mode 100644
index 0000000..25d773c
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/radiobuttons.m
@@ -0,0 +1,207 @@
+// 14 august 2015
+#import "uipriv_darwin.h"
+
+// TODO resizing the controlgallery vertically causes the third button to still resize :|
+
+// In the old days you would use a NSMatrix for this; as of OS X 10.8 this was deprecated and now you need just a bunch of NSButtons with the same superview AND same action method.
+// This is documented on the NSMatrix page, but the rest of the OS X documentation says to still use NSMatrix.
+// NSMatrix has weird quirks anyway...
+
+// LONGTERM 6 units of spacing between buttons, as suggested by Interface Builder?
+
+@interface radioButtonsDelegate : NSObject {
+	uiRadioButtons *libui_r;
+}
+- (id)initWithR:(uiRadioButtons *)r;
+- (IBAction)onClicked:(id)sender;
+@end
+
+struct uiRadioButtons {
+	uiDarwinControl c;
+	NSView *view;
+	NSMutableArray *buttons;
+	NSMutableArray *constraints;
+	NSLayoutConstraint *lastv;
+	radioButtonsDelegate *delegate;
+	void (*onSelected)(uiRadioButtons *, void *);
+	void *onSelectedData;
+};
+
+@implementation radioButtonsDelegate
+
+- (id)initWithR:(uiRadioButtons *)r
+{
+	self = [super init];
+	if (self)
+		self->libui_r = r;
+	return self;
+}
+
+- (IBAction)onClicked:(id)sender
+{
+	uiRadioButtons *r = self->libui_r;
+
+	(*(r->onSelected))(r, r->onSelectedData);
+}
+
+@end
+
+uiDarwinControlAllDefaultsExceptDestroy(uiRadioButtons, view)
+
+static void defaultOnSelected(uiRadioButtons *r, void *data)
+{
+	// do nothing
+}
+
+static void uiRadioButtonsDestroy(uiControl *c)
+{
+	uiRadioButtons *r = uiRadioButtons(c);
+	NSButton *b;
+
+	// drop the constraints
+	[r->view removeConstraints:r->constraints];
+	[r->constraints release];
+	if (r->lastv != nil)
+		[r->lastv release];
+	// destroy the buttons
+	for (b in r->buttons) {
+		[b setTarget:nil];
+		[b removeFromSuperview];
+	}
+	[r->buttons release];
+	// destroy the delegate
+	[r->delegate release];
+	// and destroy ourselves
+	[r->view release];
+	uiFreeControl(uiControl(r));
+}
+
+static NSButton *buttonAt(uiRadioButtons *r, int n)
+{
+	return (NSButton *) [r->buttons objectAtIndex:n];
+}
+
+void uiRadioButtonsAppend(uiRadioButtons *r, const char *text)
+{
+	NSButton *b, *b2;
+	NSLayoutConstraint *constraint;
+
+	b = [[NSButton alloc] initWithFrame:NSZeroRect];
+	[b setTitle:toNSString(text)];
+	[b setButtonType:NSRadioButton];
+	// doesn't seem to have an associated bezel style
+	[b setBordered:NO];
+	[b setTransparent:NO];
+	uiDarwinSetControlFont(b, NSRegularControlSize);
+	[b setTranslatesAutoresizingMaskIntoConstraints:NO];
+
+	[b setTarget:r->delegate];
+	[b setAction:@selector(onClicked:)];
+
+	[r->buttons addObject:b];
+	[r->view addSubview:b];
+
+	// pin horizontally to the edges of the superview
+	constraint = mkConstraint(b, NSLayoutAttributeLeading,
+		NSLayoutRelationEqual,
+		r->view, NSLayoutAttributeLeading,
+		1, 0,
+		@"uiRadioButtons button leading constraint");
+	[r->view addConstraint:constraint];
+	[r->constraints addObject:constraint];
+	constraint = mkConstraint(b, NSLayoutAttributeTrailing,
+		NSLayoutRelationEqual,
+		r->view, NSLayoutAttributeTrailing,
+		1, 0,
+		@"uiRadioButtons button trailing constraint");
+	[r->view addConstraint:constraint];
+	[r->constraints addObject:constraint];
+
+	// if this is the first view, pin it to the top
+	// otherwise pin to the bottom of the last
+	if ([r->buttons count] == 1)
+		constraint = mkConstraint(b, NSLayoutAttributeTop,
+			NSLayoutRelationEqual,
+			r->view, NSLayoutAttributeTop,
+			1, 0,
+			@"uiRadioButtons first button top constraint");
+	else {
+		b2 = buttonAt(r, [r->buttons count] - 2);
+		constraint = mkConstraint(b, NSLayoutAttributeTop,
+			NSLayoutRelationEqual,
+			b2, NSLayoutAttributeBottom,
+			1, 0,
+			@"uiRadioButtons non-first button top constraint");
+	}
+	[r->view addConstraint:constraint];
+	[r->constraints addObject:constraint];
+
+	// if there is a previous bottom constraint, remove it
+	if (r->lastv != nil) {
+		[r->view removeConstraint:r->lastv];
+		[r->constraints removeObject:r->lastv];
+		[r->lastv release];
+	}
+
+	// and make the new bottom constraint
+	r->lastv = mkConstraint(b, NSLayoutAttributeBottom,
+		NSLayoutRelationEqual,
+		r->view, NSLayoutAttributeBottom,
+		1, 0,
+		@"uiRadioButtons last button bottom constraint");
+	[r->view addConstraint:r->lastv];
+	[r->constraints addObject:r->lastv];
+	[r->lastv retain];
+}
+
+int uiRadioButtonsSelected(uiRadioButtons *r)
+{
+	NSButton *b;
+	NSUInteger i;
+
+	for (i = 0; i < [r->buttons count]; i++) {
+		b = (NSButton *) [r->buttons objectAtIndex:i];
+		if ([b state] == NSOnState)
+			return i;
+	}
+	return -1;
+}
+
+void uiRadioButtonsSetSelected(uiRadioButtons *r, int n)
+{
+	NSButton *b;
+	NSInteger state;
+
+	state = NSOnState;
+	if (n == -1) {
+		n = uiRadioButtonsSelected(r);
+		if (n == -1)		// from nothing to nothing; do nothing
+			return;
+		state = NSOffState;
+	}
+	b = (NSButton *) [r->buttons objectAtIndex:n];
+	[b setState:state];
+}
+
+void uiRadioButtonsOnSelected(uiRadioButtons *r, void (*f)(uiRadioButtons *, void *), void *data)
+{
+	r->onSelected = f;
+	r->onSelectedData = data;
+}
+
+uiRadioButtons *uiNewRadioButtons(void)
+{
+	uiRadioButtons *r;
+
+	uiDarwinNewControl(uiRadioButtons, r);
+
+	r->view = [[NSView alloc] initWithFrame:NSZeroRect];
+	r->buttons = [NSMutableArray new];
+	r->constraints = [NSMutableArray new];
+
+	r->delegate = [[radioButtonsDelegate alloc] initWithR:r];
+
+	uiRadioButtonsOnSelected(r, defaultOnSelected, NULL);
+
+	return r;
+}
diff --git a/src/libui_sdl/libui/darwin/scrollview.m b/src/libui_sdl/libui/darwin/scrollview.m
new file mode 100644
index 0000000..b0b4040
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/scrollview.m
@@ -0,0 +1,61 @@
+// 27 may 2016
+#include "uipriv_darwin.h"
+
+// see http://stackoverflow.com/questions/37979445/how-do-i-properly-set-up-a-scrolling-nstableview-using-auto-layout-what-ive-tr for why we don't use auto layout
+// TODO do the same with uiGroup and uiTab?
+
+struct scrollViewData {
+	BOOL hscroll;
+	BOOL vscroll;
+};
+
+NSScrollView *mkScrollView(struct scrollViewCreateParams *p, struct scrollViewData **dout)
+{
+	NSScrollView *sv;
+	NSBorderType border;
+	struct scrollViewData *d;
+
+	sv = [[NSScrollView alloc] initWithFrame:NSZeroRect];
+	if (p->BackgroundColor != nil)
+		[sv setBackgroundColor:p->BackgroundColor];
+	[sv setDrawsBackground:p->DrawsBackground];
+	border = NSNoBorder;
+	if (p->Bordered)
+		border = NSBezelBorder;
+	// document view seems to set the cursor properly
+	[sv setBorderType:border];
+	[sv setAutohidesScrollers:YES];
+	[sv setHasHorizontalRuler:NO];
+	[sv setHasVerticalRuler:NO];
+	[sv setRulersVisible:NO];
+	[sv setScrollerKnobStyle:NSScrollerKnobStyleDefault];
+	// the scroller style is documented as being set by default for us
+	// LONGTERM verify line and page for programmatically created NSTableView
+	[sv setScrollsDynamically:YES];
+	[sv setFindBarPosition:NSScrollViewFindBarPositionAboveContent];
+	[sv setUsesPredominantAxisScrolling:NO];
+	[sv setHorizontalScrollElasticity:NSScrollElasticityAutomatic];
+	[sv setVerticalScrollElasticity:NSScrollElasticityAutomatic];
+	[sv setAllowsMagnification:NO];
+
+	[sv setDocumentView:p->DocumentView];
+	d = uiNew(struct scrollViewData);
+	scrollViewSetScrolling(sv, d, p->HScroll, p->VScroll);
+
+	*dout = d;
+	return sv;
+}
+
+// based on http://blog.bjhomer.com/2014/08/nsscrollview-and-autolayout.html because (as pointed out there) Apple's official guide is really only for iOS
+void scrollViewSetScrolling(NSScrollView *sv, struct scrollViewData *d, BOOL hscroll, BOOL vscroll)
+{
+	d->hscroll = hscroll;
+	[sv setHasHorizontalScroller:d->hscroll];
+	d->vscroll = vscroll;
+	[sv setHasVerticalScroller:d->vscroll];
+}
+
+void scrollViewFreeData(NSScrollView *sv, struct scrollViewData *d)
+{
+	uiFree(d);
+}
diff --git a/src/libui_sdl/libui/darwin/separator.m b/src/libui_sdl/libui/darwin/separator.m
new file mode 100644
index 0000000..a37a376
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/separator.m
@@ -0,0 +1,45 @@
+// 14 august 2015
+#import "uipriv_darwin.h"
+
+// TODO make this intrinsic
+#define separatorWidth 96
+#define separatorHeight 96
+
+struct uiSeparator {
+	uiDarwinControl c;
+	NSBox *box;
+};
+
+uiDarwinControlAllDefaults(uiSeparator, box)
+
+uiSeparator *uiNewHorizontalSeparator(void)
+{
+	uiSeparator *s;
+
+	uiDarwinNewControl(uiSeparator, s);
+
+	// make the initial width >= initial height to force horizontal
+	s->box = [[NSBox alloc] initWithFrame:NSMakeRect(0, 0, 100, 1)];
+	[s->box setBoxType:NSBoxSeparator];
+	[s->box setBorderType:NSGrooveBorder];
+	[s->box setTransparent:NO];
+	[s->box setTitlePosition:NSNoTitle];
+
+	return s;
+}
+
+uiSeparator *uiNewVerticalSeparator(void)
+{
+	uiSeparator *s;
+
+	uiDarwinNewControl(uiSeparator, s);
+
+	// make the initial height >= initial width to force vertical
+	s->box = [[NSBox alloc] initWithFrame:NSMakeRect(0, 0, 1, 100)];
+	[s->box setBoxType:NSBoxSeparator];
+	[s->box setBorderType:NSGrooveBorder];
+	[s->box setTransparent:NO];
+	[s->box setTitlePosition:NSNoTitle];
+
+	return s;
+}
diff --git a/src/libui_sdl/libui/darwin/slider.m b/src/libui_sdl/libui/darwin/slider.m
new file mode 100644
index 0000000..f00da50
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/slider.m
@@ -0,0 +1,147 @@
+// 14 august 2015
+#import "uipriv_darwin.h"
+
+// Horizontal sliders have no intrinsic width; we'll use the default Interface Builder width for them.
+// This will also be used for the initial frame size, to ensure the slider is always horizontal (see below).
+#define sliderWidth 92
+
+@interface libui_intrinsicWidthNSSlider : NSSlider
+@end
+
+@implementation libui_intrinsicWidthNSSlider
+
+- (NSSize)intrinsicContentSize
+{
+	NSSize s;
+
+	s = [super intrinsicContentSize];
+	s.width = sliderWidth;
+	return s;
+}
+
+@end
+
+struct uiSlider {
+	uiDarwinControl c;
+	NSSlider *slider;
+	void (*onChanged)(uiSlider *, void *);
+	void *onChangedData;
+};
+
+@interface sliderDelegateClass : NSObject {
+	struct mapTable *sliders;
+}
+- (IBAction)onChanged:(id)sender;
+- (void)registerSlider:(uiSlider *)b;
+- (void)unregisterSlider:(uiSlider *)b;
+@end
+
+@implementation sliderDelegateClass
+
+- (id)init
+{
+	self = [super init];
+	if (self)
+		self->sliders = newMap();
+	return self;
+}
+
+- (void)dealloc
+{
+	mapDestroy(self->sliders);
+	[super dealloc];
+}
+
+- (IBAction)onChanged:(id)sender
+{
+	uiSlider *s;
+
+	s = (uiSlider *) mapGet(self->sliders, sender);
+	(*(s->onChanged))(s, s->onChangedData);
+}
+
+- (void)registerSlider:(uiSlider *)s
+{
+	mapSet(self->sliders, s->slider, s);
+	[s->slider setTarget:self];
+	[s->slider setAction:@selector(onChanged:)];
+}
+
+- (void)unregisterSlider:(uiSlider *)s
+{
+	[s->slider setTarget:nil];
+	mapDelete(self->sliders, s->slider);
+}
+
+@end
+
+static sliderDelegateClass *sliderDelegate = nil;
+
+uiDarwinControlAllDefaultsExceptDestroy(uiSlider, slider)
+
+static void uiSliderDestroy(uiControl *c)
+{
+	uiSlider *s = uiSlider(c);
+
+	[sliderDelegate unregisterSlider:s];
+	[s->slider release];
+	uiFreeControl(uiControl(s));
+}
+
+int uiSliderValue(uiSlider *s)
+{
+	return [s->slider integerValue];
+}
+
+void uiSliderSetValue(uiSlider *s, int value)
+{
+	[s->slider setIntegerValue:value];
+}
+
+void uiSliderOnChanged(uiSlider *s, void (*f)(uiSlider *, void *), void *data)
+{
+	s->onChanged = f;
+	s->onChangedData = data;
+}
+
+static void defaultOnChanged(uiSlider *s, void *data)
+{
+	// do nothing
+}
+
+uiSlider *uiNewSlider(int min, int max)
+{
+	uiSlider *s;
+	NSSliderCell *cell;
+	int temp;
+
+	if (min >= max) {
+		temp = min;
+		min = max;
+		max = temp;
+	}
+
+	uiDarwinNewControl(uiSlider, s);
+
+	// a horizontal slider is defined as one where the width > height, not by a flag
+	// to be safe, don't use NSZeroRect, but make it horizontal from the get-go
+	s->slider = [[libui_intrinsicWidthNSSlider alloc]
+		initWithFrame:NSMakeRect(0, 0, sliderWidth, 2)];
+	[s->slider setMinValue:min];
+	[s->slider setMaxValue:max];
+	[s->slider setAllowsTickMarkValuesOnly:NO];
+	[s->slider setNumberOfTickMarks:0];
+	[s->slider setTickMarkPosition:NSTickMarkAbove];
+
+	cell = (NSSliderCell *) [s->slider cell];
+	[cell setSliderType:NSLinearSlider];
+
+	if (sliderDelegate == nil) {
+		sliderDelegate = [[sliderDelegateClass new] autorelease];
+		[delegates addObject:sliderDelegate];
+	}
+	[sliderDelegate registerSlider:s];
+	uiSliderOnChanged(s, defaultOnChanged, NULL);
+
+	return s;
+}
diff --git a/src/libui_sdl/libui/darwin/spinbox.m b/src/libui_sdl/libui/darwin/spinbox.m
new file mode 100644
index 0000000..73474d0
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/spinbox.m
@@ -0,0 +1,214 @@
+// 14 august 2015
+#import "uipriv_darwin.h"
+
+@interface libui_spinbox : NSView<NSTextFieldDelegate> {
+	NSTextField *tf;
+	NSNumberFormatter *formatter;
+	NSStepper *stepper;
+
+	NSInteger value;
+	NSInteger minimum;
+	NSInteger maximum;
+
+	uiSpinbox *spinbox;
+}
+- (id)initWithFrame:(NSRect)r spinbox:(uiSpinbox *)sb;
+// see https://github.com/andlabs/ui/issues/82
+- (NSInteger)libui_value;
+- (void)libui_setValue:(NSInteger)val;
+- (void)setMinimum:(NSInteger)min;
+- (void)setMaximum:(NSInteger)max;
+- (IBAction)stepperClicked:(id)sender;
+- (void)controlTextDidChange:(NSNotification *)note;
+@end
+
+struct uiSpinbox {
+	uiDarwinControl c;
+	libui_spinbox *spinbox;
+	void (*onChanged)(uiSpinbox *, void *);
+	void *onChangedData;
+};
+
+// yes folks, this varies by operating system! woo!
+// 10.10 started drawing the NSStepper one point too low, so we have to fix it up conditionally
+// TODO test this; we'll probably have to substitute 10_9
+static CGFloat stepperYDelta(void)
+{
+	// via https://developer.apple.com/library/mac/releasenotes/AppKit/RN-AppKit/
+	if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_9)
+		return 0;
+	return -1;
+}
+
+@implementation libui_spinbox
+
+- (id)initWithFrame:(NSRect)r spinbox:(uiSpinbox *)sb
+{
+	self = [super initWithFrame:r];
+	if (self) {
+		self->tf = newEditableTextField();
+		[self->tf setTranslatesAutoresizingMaskIntoConstraints:NO];
+
+		self->formatter = [NSNumberFormatter new];
+		[self->formatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
+		[self->formatter setLocalizesFormat:NO];
+		[self->formatter setUsesGroupingSeparator:NO];
+		[self->formatter setHasThousandSeparators:NO];
+		[self->formatter setAllowsFloats:NO];
+		[self->tf setFormatter:self->formatter];
+
+		self->stepper = [[NSStepper alloc] initWithFrame:NSZeroRect];
+		[self->stepper setIncrement:1];
+		[self->stepper setValueWraps:NO];
+		[self->stepper setAutorepeat:YES];              // hold mouse button to step repeatedly
+		[self->stepper setTranslatesAutoresizingMaskIntoConstraints:NO];
+
+		[self->tf setDelegate:self];
+		[self->stepper setTarget:self];
+		[self->stepper setAction:@selector(stepperClicked:)];
+
+		[self addSubview:self->tf];
+		[self addSubview:self->stepper];
+
+		[self addConstraint:mkConstraint(self->tf, NSLayoutAttributeLeading,
+			NSLayoutRelationEqual,
+			self, NSLayoutAttributeLeading,
+			1, 0,
+			@"uiSpinbox left edge")];
+		[self addConstraint:mkConstraint(self->stepper, NSLayoutAttributeTrailing,
+			NSLayoutRelationEqual,
+			self, NSLayoutAttributeTrailing,
+			1, 0,
+			@"uiSpinbox right edge")];
+		[self addConstraint:mkConstraint(self->tf, NSLayoutAttributeTop,
+			NSLayoutRelationEqual,
+			self, NSLayoutAttributeTop,
+			1, 0,
+			@"uiSpinbox top edge text field")];
+		[self addConstraint:mkConstraint(self->tf, NSLayoutAttributeBottom,
+			NSLayoutRelationEqual,
+			self, NSLayoutAttributeBottom,
+			1, 0,
+			@"uiSpinbox bottom edge text field")];
+		[self addConstraint:mkConstraint(self->stepper, NSLayoutAttributeTop,
+			NSLayoutRelationEqual,
+			self, NSLayoutAttributeTop,
+			1, stepperYDelta(),
+			@"uiSpinbox top edge stepper")];
+		[self addConstraint:mkConstraint(self->stepper, NSLayoutAttributeBottom,
+			NSLayoutRelationEqual,
+			self, NSLayoutAttributeBottom,
+			1, stepperYDelta(),
+			@"uiSpinbox bottom edge stepper")];
+		[self addConstraint:mkConstraint(self->tf, NSLayoutAttributeTrailing,
+			NSLayoutRelationEqual,
+			self->stepper, NSLayoutAttributeLeading,
+			1, -3,		// arbitrary amount; good enough visually (and it seems to match NSDatePicker too, at least on 10.11, which is even better)
+			@"uiSpinbox space between text field and stepper")];
+
+		self->spinbox = sb;
+	}
+	return self;
+}
+
+- (void)dealloc
+{
+	[self->tf setDelegate:nil];
+	[self->tf removeFromSuperview];
+	[self->tf release];
+	[self->formatter release];
+	[self->stepper setTarget:nil];
+	[self->stepper removeFromSuperview];
+	[self->stepper release];
+	[super dealloc];
+}
+
+- (NSInteger)libui_value
+{
+	return self->value;
+}
+
+- (void)libui_setValue:(NSInteger)val
+{
+	self->value = val;
+	if (self->value < self->minimum)
+		self->value = self->minimum;
+	if (self->value > self->maximum)
+		self->value = self->maximum;
+	[self->tf setIntegerValue:self->value];
+	[self->stepper setIntegerValue:self->value];
+}
+
+- (void)setMinimum:(NSInteger)min
+{
+	self->minimum = min;
+	[self->formatter setMinimum:[NSNumber numberWithInteger:self->minimum]];
+	[self->stepper setMinValue:((double) (self->minimum))];
+}
+
+- (void)setMaximum:(NSInteger)max
+{
+	self->maximum = max;
+	[self->formatter setMaximum:[NSNumber numberWithInteger:self->maximum]];
+	[self->stepper setMaxValue:((double) (self->maximum))];
+}
+
+- (IBAction)stepperClicked:(id)sender
+{
+	[self libui_setValue:[self->stepper integerValue]];
+	(*(self->spinbox->onChanged))(self->spinbox, self->spinbox->onChangedData);
+}
+
+- (void)controlTextDidChange:(NSNotification *)note
+{
+	[self libui_setValue:[self->tf integerValue]];
+	(*(self->spinbox->onChanged))(self->spinbox, self->spinbox->onChangedData);
+}
+
+@end
+
+uiDarwinControlAllDefaults(uiSpinbox, spinbox)
+
+int uiSpinboxValue(uiSpinbox *s)
+{
+	return [s->spinbox libui_value];
+}
+
+void uiSpinboxSetValue(uiSpinbox *s, int value)
+{
+	[s->spinbox libui_setValue:value];
+}
+
+void uiSpinboxOnChanged(uiSpinbox *s, void (*f)(uiSpinbox *, void *), void *data)
+{
+	s->onChanged = f;
+	s->onChangedData = data;
+}
+
+static void defaultOnChanged(uiSpinbox *s, void *data)
+{
+	// do nothing
+}
+
+uiSpinbox *uiNewSpinbox(int min, int max)
+{
+	uiSpinbox *s;
+	int temp;
+
+	if (min >= max) {
+		temp = min;
+		min = max;
+		max = temp;
+	}
+
+	uiDarwinNewControl(uiSpinbox, s);
+
+	s->spinbox = [[libui_spinbox alloc] initWithFrame:NSZeroRect spinbox:s];
+	[s->spinbox setMinimum:min];
+	[s->spinbox setMaximum:max];
+	[s->spinbox libui_setValue:min];
+
+	uiSpinboxOnChanged(s, defaultOnChanged, NULL);
+
+	return s;
+}
diff --git a/src/libui_sdl/libui/darwin/stddialogs.m b/src/libui_sdl/libui/darwin/stddialogs.m
new file mode 100644
index 0000000..57ce959
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/stddialogs.m
@@ -0,0 +1,123 @@
+// 26 june 2015
+#import "uipriv_darwin.h"
+
+// LONGTERM restructure this whole file
+// LONGTERM explicitly document this works as we want
+// LONGTERM note that font and color buttons also do this
+
+#define windowWindow(w) ((NSWindow *) uiControlHandle(uiControl(w)))
+
+// source of code modal logic: http://stackoverflow.com/questions/604768/wait-for-nsalert-beginsheetmodalforwindow
+
+// note: whether extensions are actually shown depends on a user setting in Finder; we can't control it here
+static void setupSavePanel(NSSavePanel *s)
+{
+	[s setCanCreateDirectories:YES];
+	[s setShowsHiddenFiles:YES];
+	[s setExtensionHidden:NO];
+	[s setCanSelectHiddenExtension:NO];
+	[s setTreatsFilePackagesAsDirectories:YES];
+}
+
+static char *runSavePanel(NSWindow *parent, NSSavePanel *s)
+{
+	char *filename;
+
+	[s beginSheetModalForWindow:parent completionHandler:^(NSInteger result) {
+		[realNSApp() stopModalWithCode:result];
+	}];
+	if ([realNSApp() runModalForWindow:s] != NSFileHandlingPanelOKButton)
+		return NULL;
+	filename = uiDarwinNSStringToText([[s URL] path]);
+	return filename;
+}
+
+char *uiOpenFile(uiWindow *parent)
+{
+	NSOpenPanel *o;
+
+	o = [NSOpenPanel openPanel];
+	[o setCanChooseFiles:YES];
+	[o setCanChooseDirectories:NO];
+	[o setResolvesAliases:NO];
+	[o setAllowsMultipleSelection:NO];
+	setupSavePanel(o);
+	// panel is autoreleased
+	return runSavePanel(windowWindow(parent), o);
+}
+
+char *uiSaveFile(uiWindow *parent)
+{
+	NSSavePanel *s;
+
+	s = [NSSavePanel savePanel];
+	setupSavePanel(s);
+	// panel is autoreleased
+	return runSavePanel(windowWindow(parent), s);
+}
+
+// I would use a completion handler for NSAlert as well, but alas NSAlert's are 10.9 and higher only
+@interface libuiCodeModalAlertPanel : NSObject {
+	NSAlert *panel;
+	NSWindow *parent;
+}
+- (id)initWithPanel:(NSAlert *)p parent:(NSWindow *)w;
+- (NSInteger)run;
+- (void)panelEnded:(NSAlert *)panel result:(NSInteger)result data:(void *)data;
+@end
+
+@implementation libuiCodeModalAlertPanel
+
+- (id)initWithPanel:(NSAlert *)p parent:(NSWindow *)w
+{
+	self = [super init];
+	if (self) {
+		self->panel = p;
+		self->parent = w;
+	}
+	return self;
+}
+
+- (NSInteger)run
+{
+	[self->panel beginSheetModalForWindow:self->parent
+		modalDelegate:self
+		didEndSelector:@selector(panelEnded:result:data:)
+		contextInfo:NULL];
+	return [realNSApp() runModalForWindow:[self->panel window]];
+}
+
+- (void)panelEnded:(NSAlert *)panel result:(NSInteger)result data:(void *)data
+{
+	[realNSApp() stopModalWithCode:result];
+}
+
+@end
+
+static void msgbox(NSWindow *parent, const char *title, const char *description, NSAlertStyle style)
+{
+	NSAlert *a;
+	libuiCodeModalAlertPanel *cm;
+
+	a = [NSAlert new];
+	[a setAlertStyle:style];
+	[a setShowsHelp:NO];
+	[a setShowsSuppressionButton:NO];
+	[a setMessageText:toNSString(title)];
+	[a setInformativeText:toNSString(description)];
+	[a addButtonWithTitle:@"OK"];
+	cm = [[libuiCodeModalAlertPanel alloc] initWithPanel:a parent:parent];
+	[cm run];
+	[cm release];
+	[a release];
+}
+
+void uiMsgBox(uiWindow *parent, const char *title, const char *description)
+{
+	msgbox(windowWindow(parent), title, description, NSInformationalAlertStyle);
+}
+
+void uiMsgBoxError(uiWindow *parent, const char *title, const char *description)
+{
+	msgbox(windowWindow(parent), title, description, NSCriticalAlertStyle);
+}
diff --git a/src/libui_sdl/libui/darwin/tab.m b/src/libui_sdl/libui/darwin/tab.m
new file mode 100644
index 0000000..3d2ca9f
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/tab.m
@@ -0,0 +1,292 @@
+// 15 august 2015
+#import "uipriv_darwin.h"
+
+// TODO need to jiggle on tab change too (second page disabled tab label initially ambiguous)
+
+@interface tabPage : NSObject {
+	struct singleChildConstraints constraints;
+	int margined;
+	NSView *view;		// the NSTabViewItem view itself
+	NSObject *pageID;
+}
+@property uiControl *c;
+@property NSLayoutPriority oldHorzHuggingPri;
+@property NSLayoutPriority oldVertHuggingPri;
+- (id)initWithView:(NSView *)v pageID:(NSObject *)o;
+- (NSView *)childView;
+- (void)establishChildConstraints;
+- (void)removeChildConstraints;
+- (int)isMargined;
+- (void)setMargined:(int)m;
+@end
+
+struct uiTab {
+	uiDarwinControl c;
+	NSTabView *tabview;
+	NSMutableArray *pages;
+	NSLayoutPriority horzHuggingPri;
+	NSLayoutPriority vertHuggingPri;
+};
+
+@implementation tabPage
+
+- (id)initWithView:(NSView *)v pageID:(NSObject *)o
+{
+	self = [super init];
+	if (self != nil) {
+		self->view = [v retain];
+		self->pageID = [o retain];
+	}
+	return self;
+}
+
+- (void)dealloc
+{
+	[self removeChildConstraints];
+	[self->view release];
+	[self->pageID release];
+	[super dealloc];
+}
+
+- (NSView *)childView
+{
+	return (NSView *) uiControlHandle(self.c);
+}
+
+- (void)establishChildConstraints
+{
+	[self removeChildConstraints];
+	if (self.c == NULL)
+		return;
+	singleChildConstraintsEstablish(&(self->constraints),
+		self->view, [self childView],
+		uiDarwinControlHugsTrailingEdge(uiDarwinControl(self.c)),
+		uiDarwinControlHugsBottom(uiDarwinControl(self.c)),
+		self->margined,
+		@"uiTab page");
+}
+
+- (void)removeChildConstraints
+{
+	singleChildConstraintsRemove(&(self->constraints), self->view);
+}
+
+- (int)isMargined
+{
+	return self->margined;
+}
+
+- (void)setMargined:(int)m
+{
+	self->margined = m;
+	singleChildConstraintsSetMargined(&(self->constraints), self->margined);
+}
+
+@end
+
+static void uiTabDestroy(uiControl *c)
+{
+	uiTab *t = uiTab(c);
+	tabPage *page;
+
+	// first remove all tab pages so we can destroy all the children
+	while ([t->tabview numberOfTabViewItems] != 0)
+		[t->tabview removeTabViewItem:[t->tabview tabViewItemAtIndex:0]];
+	// then destroy all the children
+	for (page in t->pages) {
+		[page removeChildConstraints];
+		uiControlSetParent(page.c, NULL);
+		uiDarwinControlSetSuperview(uiDarwinControl(page.c), nil);
+		uiControlDestroy(page.c);
+	}
+	// and finally destroy ourselves
+	[t->pages release];
+	[t->tabview release];
+	uiFreeControl(uiControl(t));
+}
+
+uiDarwinControlDefaultHandle(uiTab, tabview)
+uiDarwinControlDefaultParent(uiTab, tabview)
+uiDarwinControlDefaultSetParent(uiTab, tabview)
+uiDarwinControlDefaultToplevel(uiTab, tabview)
+uiDarwinControlDefaultVisible(uiTab, tabview)
+uiDarwinControlDefaultShow(uiTab, tabview)
+uiDarwinControlDefaultHide(uiTab, tabview)
+uiDarwinControlDefaultEnabled(uiTab, tabview)
+uiDarwinControlDefaultEnable(uiTab, tabview)
+uiDarwinControlDefaultDisable(uiTab, tabview)
+
+static void uiTabSyncEnableState(uiDarwinControl *c, int enabled)
+{
+	uiTab *t = uiTab(c);
+	tabPage *page;
+
+	if (uiDarwinShouldStopSyncEnableState(uiDarwinControl(t), enabled))
+		return;
+	for (page in t->pages)
+		uiDarwinControlSyncEnableState(uiDarwinControl(page.c), enabled);
+}
+
+uiDarwinControlDefaultSetSuperview(uiTab, tabview)
+
+static void tabRelayout(uiTab *t)
+{
+	tabPage *page;
+
+	for (page in t->pages)
+		[page establishChildConstraints];
+	// and this gets rid of some weird issues with regards to box alignment
+	jiggleViewLayout(t->tabview);
+}
+
+BOOL uiTabHugsTrailingEdge(uiDarwinControl *c)
+{
+	uiTab *t = uiTab(c);
+
+	return t->horzHuggingPri < NSLayoutPriorityWindowSizeStayPut;
+}
+
+BOOL uiTabHugsBottom(uiDarwinControl *c)
+{
+	uiTab *t = uiTab(c);
+
+	return t->vertHuggingPri < NSLayoutPriorityWindowSizeStayPut;
+}
+
+static void uiTabChildEdgeHuggingChanged(uiDarwinControl *c)
+{
+	uiTab *t = uiTab(c);
+
+	tabRelayout(t);
+}
+
+static NSLayoutPriority uiTabHuggingPriority(uiDarwinControl *c, NSLayoutConstraintOrientation orientation)
+{
+	uiTab *t = uiTab(c);
+
+	if (orientation == NSLayoutConstraintOrientationHorizontal)
+		return t->horzHuggingPri;
+	return t->vertHuggingPri;
+}
+
+static void uiTabSetHuggingPriority(uiDarwinControl *c, NSLayoutPriority priority, NSLayoutConstraintOrientation orientation)
+{
+	uiTab *t = uiTab(c);
+
+	if (orientation == NSLayoutConstraintOrientationHorizontal)
+		t->horzHuggingPri = priority;
+	else
+		t->vertHuggingPri = priority;
+	uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl(t));
+}
+
+static void uiTabChildVisibilityChanged(uiDarwinControl *c)
+{
+	uiTab *t = uiTab(c);
+
+	tabRelayout(t);
+}
+
+void uiTabAppend(uiTab *t, const char *name, uiControl *child)
+{
+	uiTabInsertAt(t, name, [t->pages count], child);
+}
+
+void uiTabInsertAt(uiTab *t, const char *name, int n, uiControl *child)
+{
+	tabPage *page;
+	NSView *view;
+	NSTabViewItem *i;
+	NSObject *pageID;
+
+	uiControlSetParent(child, uiControl(t));
+
+	view = [[[NSView alloc] initWithFrame:NSZeroRect] autorelease];
+	// note: if we turn off the autoresizing mask, nothing shows up
+	uiDarwinControlSetSuperview(uiDarwinControl(child), view);
+	uiDarwinControlSyncEnableState(uiDarwinControl(child), uiControlEnabledToUser(uiControl(t)));
+
+	// the documentation says these can be nil but the headers say these must not be; let's be safe and make them non-nil anyway
+	pageID = [NSObject new];
+	page = [[[tabPage alloc] initWithView:view pageID:pageID] autorelease];
+	page.c = child;
+
+	// don't hug, just in case we're a stretchy tab
+	page.oldHorzHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(page.c), NSLayoutConstraintOrientationHorizontal);
+	page.oldVertHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(page.c), NSLayoutConstraintOrientationVertical);
+	uiDarwinControlSetHuggingPriority(uiDarwinControl(page.c), NSLayoutPriorityDefaultLow, NSLayoutConstraintOrientationHorizontal);
+	uiDarwinControlSetHuggingPriority(uiDarwinControl(page.c), NSLayoutPriorityDefaultLow, NSLayoutConstraintOrientationVertical);
+
+	[t->pages insertObject:page atIndex:n];
+
+	i = [[[NSTabViewItem alloc] initWithIdentifier:pageID] autorelease];
+	[i setLabel:toNSString(name)];
+	[i setView:view];
+	[t->tabview insertTabViewItem:i atIndex:n];
+
+	tabRelayout(t);
+}
+
+void uiTabDelete(uiTab *t, int n)
+{
+	tabPage *page;
+	uiControl *child;
+	NSTabViewItem *i;
+
+	page = (tabPage *) [t->pages objectAtIndex:n];
+
+	uiDarwinControlSetHuggingPriority(uiDarwinControl(page.c), page.oldHorzHuggingPri, NSLayoutConstraintOrientationHorizontal);
+	uiDarwinControlSetHuggingPriority(uiDarwinControl(page.c), page.oldVertHuggingPri, NSLayoutConstraintOrientationVertical);
+
+	child = page.c;
+	[page removeChildConstraints];
+	[t->pages removeObjectAtIndex:n];
+
+	uiControlSetParent(child, NULL);
+	uiDarwinControlSetSuperview(uiDarwinControl(child), nil);
+
+	i = [t->tabview tabViewItemAtIndex:n];
+	[t->tabview removeTabViewItem:i];
+
+	tabRelayout(t);
+}
+
+int uiTabNumPages(uiTab *t)
+{
+	return [t->pages count];
+}
+
+int uiTabMargined(uiTab *t, int n)
+{
+	tabPage *page;
+
+	page = (tabPage *) [t->pages objectAtIndex:n];
+	return [page isMargined];
+}
+
+void uiTabSetMargined(uiTab *t, int n, int margined)
+{
+	tabPage *page;
+
+	page = (tabPage *) [t->pages objectAtIndex:n];
+	[page setMargined:margined];
+}
+
+uiTab *uiNewTab(void)
+{
+	uiTab *t;
+
+	uiDarwinNewControl(uiTab, t);
+
+	t->tabview = [[NSTabView alloc] initWithFrame:NSZeroRect];
+	// also good for NSTabView (same selector and everything)
+	uiDarwinSetControlFont((NSControl *) (t->tabview), NSRegularControlSize);
+
+	t->pages = [NSMutableArray new];
+
+	// default to low hugging to not hug edges
+	t->horzHuggingPri = NSLayoutPriorityDefaultLow;
+	t->vertHuggingPri = NSLayoutPriorityDefaultLow;
+
+	return t;
+}
diff --git a/src/libui_sdl/libui/darwin/text.m b/src/libui_sdl/libui/darwin/text.m
new file mode 100644
index 0000000..f0d3dab
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/text.m
@@ -0,0 +1,19 @@
+// 10 april 2015
+#import "uipriv_darwin.h"
+
+char *uiDarwinNSStringToText(NSString *s)
+{
+	char *out;
+
+	out = strdup([s UTF8String]);
+	if (out == NULL) {
+		fprintf(stderr, "memory exhausted in uiDarwinNSStringToText()\n");
+		abort();
+	}
+	return out;
+}
+
+void uiFreeText(char *s)
+{
+	free(s);
+}
diff --git a/src/libui_sdl/libui/darwin/uipriv_darwin.h b/src/libui_sdl/libui/darwin/uipriv_darwin.h
new file mode 100644
index 0000000..6bca87b
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/uipriv_darwin.h
@@ -0,0 +1,146 @@
+// 6 january 2015
+#define MAC_OS_X_VERSION_MIN_REQUIRED MAC_OS_X_VERSION_10_8
+#define MAC_OS_X_VERSION_MAX_ALLOWED MAC_OS_X_VERSION_10_8
+#import <Cocoa/Cocoa.h>
+#import "../ui.h"
+#import "../ui_darwin.h"
+#import "../common/uipriv.h"
+
+#if __has_feature(objc_arc)
+#error Sorry, libui cannot be compiled with ARC.
+#endif
+
+#define toNSString(str) [NSString stringWithUTF8String:(str)]
+#define fromNSString(str) [(str) UTF8String]
+
+#ifndef NSAppKitVersionNumber10_9
+#define NSAppKitVersionNumber10_9 1265
+#endif
+
+/*TODO remove this*/typedef struct uiImage uiImage;
+
+// menu.m
+@interface menuManager : NSObject {
+	struct mapTable *items;
+	BOOL hasQuit;
+	BOOL hasPreferences;
+	BOOL hasAbout;
+}
+@property (strong) NSMenuItem *quitItem;
+@property (strong) NSMenuItem *preferencesItem;
+@property (strong) NSMenuItem *aboutItem;
+// NSMenuValidation is only informal
+- (BOOL)validateMenuItem:(NSMenuItem *)item;
+- (NSMenu *)makeMenubar;
+@end
+extern void finalizeMenus(void);
+extern void uninitMenus(void);
+
+// main.m
+@interface applicationClass : NSApplication
+@end
+// this is needed because NSApp is of type id, confusing clang
+#define realNSApp() ((applicationClass *) NSApp)
+@interface appDelegate : NSObject <NSApplicationDelegate>
+@property (strong) menuManager *menuManager;
+@end
+#define appDelegate() ((appDelegate *) [realNSApp() delegate])
+struct nextEventArgs {
+	NSEventMask mask;
+	NSDate *duration;
+	// LONGTERM no NSRunLoopMode?
+	NSString *mode;
+	BOOL dequeue;
+};
+extern int mainStep(struct nextEventArgs *nea, BOOL (^interceptEvent)(NSEvent *));
+
+// util.m
+extern void disableAutocorrect(NSTextView *);
+
+// entry.m
+extern void finishNewTextField(NSTextField *, BOOL);
+extern NSTextField *newEditableTextField(void);
+
+// window.m
+@interface libuiNSWindow : NSWindow
+- (void)libui_doMove:(NSEvent *)initialEvent;
+- (void)libui_doResize:(NSEvent *)initialEvent on:(uiWindowResizeEdge)edge;
+@end
+extern uiWindow *windowFromNSWindow(NSWindow *);
+
+// alloc.m
+extern NSMutableArray *delegates;
+extern void initAlloc(void);
+extern void uninitAlloc(void);
+
+// autolayout.m
+extern NSLayoutConstraint *mkConstraint(id view1, NSLayoutAttribute attr1, NSLayoutRelation relation, id view2, NSLayoutAttribute attr2, CGFloat multiplier, CGFloat c, NSString *desc);
+extern void jiggleViewLayout(NSView *view);
+struct singleChildConstraints {
+	NSLayoutConstraint *leadingConstraint;
+	NSLayoutConstraint *topConstraint;
+	NSLayoutConstraint *trailingConstraintGreater;
+	NSLayoutConstraint *trailingConstraintEqual;
+	NSLayoutConstraint *bottomConstraintGreater;
+	NSLayoutConstraint *bottomConstraintEqual;
+};
+extern void singleChildConstraintsEstablish(struct singleChildConstraints *c, NSView *contentView, NSView *childView, BOOL hugsTrailing, BOOL hugsBottom, int margined, NSString *desc);
+extern void singleChildConstraintsRemove(struct singleChildConstraints *c, NSView *cv);
+extern void singleChildConstraintsSetMargined(struct singleChildConstraints *c, int margined);
+
+// map.m
+extern struct mapTable *newMap(void);
+extern void mapDestroy(struct mapTable *m);
+extern void *mapGet(struct mapTable *m, void *key);
+extern void mapSet(struct mapTable *m, void *key, void *value);
+extern void mapDelete(struct mapTable *m, void *key);
+extern void mapWalk(struct mapTable *m, void (*f)(void *key, void *value));
+extern void mapReset(struct mapTable *m);
+
+// area.m
+extern int sendAreaEvents(NSEvent *);
+
+// areaevents.m
+extern BOOL fromKeycode(unsigned short keycode, uiAreaKeyEvent *ke);
+extern BOOL keycodeModifier(unsigned short keycode, uiModifiers *mod);
+
+// draw.m
+extern uiDrawContext *newContext(CGContextRef, CGFloat);
+extern void freeContext(uiDrawContext *);
+
+// drawtext.m
+extern uiDrawTextFont *mkTextFont(CTFontRef f, BOOL retain);
+extern uiDrawTextFont *mkTextFontFromNSFont(NSFont *f);
+extern void doDrawText(CGContextRef c, CGFloat cheight, double x, double y, uiDrawTextLayout *layout);
+
+// fontbutton.m
+extern BOOL fontButtonInhibitSendAction(SEL sel, id from, id to);
+extern BOOL fontButtonOverrideTargetForAction(SEL sel, id from, id to, id *override);
+extern void setupFontPanel(void);
+
+// colorbutton.m
+extern BOOL colorButtonInhibitSendAction(SEL sel, id from, id to);
+
+// scrollview.m
+struct scrollViewCreateParams {
+	NSView *DocumentView;
+	NSColor *BackgroundColor;
+	BOOL DrawsBackground;
+	BOOL Bordered;
+	BOOL HScroll;
+	BOOL VScroll;
+};
+struct scrollViewData;
+extern NSScrollView *mkScrollView(struct scrollViewCreateParams *p, struct scrollViewData **dout);
+extern void scrollViewSetScrolling(NSScrollView *sv, struct scrollViewData *d, BOOL hscroll, BOOL vscroll);
+extern void scrollViewFreeData(NSScrollView *sv, struct scrollViewData *d);
+
+// label.m
+extern NSTextField *newLabel(NSString *str);
+
+// image.m
+extern NSImage *imageImage(uiImage *);
+
+// winmoveresize.m
+extern void doManualMove(NSWindow *w, NSEvent *initialEvent);
+extern void doManualResize(NSWindow *w, NSEvent *initialEvent, uiWindowResizeEdge edge);
diff --git a/src/libui_sdl/libui/darwin/util.m b/src/libui_sdl/libui/darwin/util.m
new file mode 100644
index 0000000..ab87390
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/util.m
@@ -0,0 +1,15 @@
+// 7 april 2015
+#import "uipriv_darwin.h"
+
+// LONGTERM do we really want to do this? make it an option?
+void disableAutocorrect(NSTextView *tv)
+{
+	[tv setEnabledTextCheckingTypes:0];
+	[tv setAutomaticDashSubstitutionEnabled:NO];
+	// don't worry about automatic data detection; it won't change stringValue (thanks pretty_function in irc.freenode.net/#macdev)
+	[tv setAutomaticSpellingCorrectionEnabled:NO];
+	[tv setAutomaticTextReplacementEnabled:NO];
+	[tv setAutomaticQuoteSubstitutionEnabled:NO];
+	[tv setAutomaticLinkDetectionEnabled:NO];
+	[tv setSmartInsertDeleteEnabled:NO];
+}
diff --git a/src/libui_sdl/libui/darwin/window.m b/src/libui_sdl/libui/darwin/window.m
new file mode 100644
index 0000000..97c22e6
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/window.m
@@ -0,0 +1,407 @@
+// 15 august 2015
+#import "uipriv_darwin.h"
+
+#define defaultStyleMask (NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask)
+
+struct uiWindow {
+	uiDarwinControl c;
+	NSWindow *window;
+	uiControl *child;
+	int margined;
+	int (*onClosing)(uiWindow *, void *);
+	void *onClosingData;
+	struct singleChildConstraints constraints;
+	void (*onContentSizeChanged)(uiWindow *, void *);
+	void *onContentSizeChangedData;
+	BOOL suppressSizeChanged;
+	int fullscreen;
+	int borderless;
+};
+
+@implementation libuiNSWindow
+
+- (void)libui_doMove:(NSEvent *)initialEvent
+{
+	doManualMove(self, initialEvent);
+}
+
+- (void)libui_doResize:(NSEvent *)initialEvent on:(uiWindowResizeEdge)edge
+{
+	doManualResize(self, initialEvent, edge);
+}
+
+@end
+
+@interface windowDelegateClass : NSObject<NSWindowDelegate> {
+	struct mapTable *windows;
+}
+- (BOOL)windowShouldClose:(id)sender;
+- (void)windowDidResize:(NSNotification *)note;
+- (void)windowDidEnterFullScreen:(NSNotification *)note;
+- (void)windowDidExitFullScreen:(NSNotification *)note;
+- (void)registerWindow:(uiWindow *)w;
+- (void)unregisterWindow:(uiWindow *)w;
+- (uiWindow *)lookupWindow:(NSWindow *)w;
+@end
+
+@implementation windowDelegateClass
+
+- (id)init
+{
+	self = [super init];
+	if (self)
+		self->windows = newMap();
+	return self;
+}
+
+- (void)dealloc
+{
+	mapDestroy(self->windows);
+	[super dealloc];
+}
+
+- (BOOL)windowShouldClose:(id)sender
+{
+	uiWindow *w;
+
+	w = [self lookupWindow:((NSWindow *) sender)];
+	// w should not be NULL; we are only the delegate of registered windows
+	if ((*(w->onClosing))(w, w->onClosingData))
+		uiControlDestroy(uiControl(w));
+	return NO;
+}
+
+- (void)windowDidResize:(NSNotification *)note
+{
+	uiWindow *w;
+
+	w = [self lookupWindow:((NSWindow *) [note object])];
+	if (!w->suppressSizeChanged)
+		(*(w->onContentSizeChanged))(w, w->onContentSizeChangedData);
+}
+
+- (void)windowDidEnterFullScreen:(NSNotification *)note
+{
+	uiWindow *w;
+
+	w = [self lookupWindow:((NSWindow *) [note object])];
+	if (!w->suppressSizeChanged)
+		w->fullscreen = 1;
+}
+
+- (void)windowDidExitFullScreen:(NSNotification *)note
+{
+	uiWindow *w;
+
+	w = [self lookupWindow:((NSWindow *) [note object])];
+	if (!w->suppressSizeChanged)
+		w->fullscreen = 0;
+}
+
+- (void)registerWindow:(uiWindow *)w
+{
+	mapSet(self->windows, w->window, w);
+	[w->window setDelegate:self];
+}
+
+- (void)unregisterWindow:(uiWindow *)w
+{
+	[w->window setDelegate:nil];
+	mapDelete(self->windows, w->window);
+}
+
+- (uiWindow *)lookupWindow:(NSWindow *)w
+{
+	uiWindow *v;
+
+	v = uiWindow(mapGet(self->windows, w));
+	// this CAN (and IS ALLOWED TO) return NULL, just in case we're called with some OS X-provided window as the key window
+	return v;
+}
+
+@end
+
+static windowDelegateClass *windowDelegate = nil;
+
+static void removeConstraints(uiWindow *w)
+{
+	NSView *cv;
+
+	cv = [w->window contentView];
+	singleChildConstraintsRemove(&(w->constraints), cv);
+}
+
+static void uiWindowDestroy(uiControl *c)
+{
+	uiWindow *w = uiWindow(c);
+
+	// hide the window
+	[w->window orderOut:w->window];
+	removeConstraints(w);
+	if (w->child != NULL) {
+		uiControlSetParent(w->child, NULL);
+		uiDarwinControlSetSuperview(uiDarwinControl(w->child), nil);
+		uiControlDestroy(w->child);
+	}
+	[windowDelegate unregisterWindow:w];
+	[w->window release];
+	uiFreeControl(uiControl(w));
+}
+
+uiDarwinControlDefaultHandle(uiWindow, window)
+
+uiControl *uiWindowParent(uiControl *c)
+{
+	return NULL;
+}
+
+void uiWindowSetParent(uiControl *c, uiControl *parent)
+{
+	uiUserBugCannotSetParentOnToplevel("uiWindow");
+}
+
+static int uiWindowToplevel(uiControl *c)
+{
+	return 1;
+}
+
+static int uiWindowVisible(uiControl *c)
+{
+	uiWindow *w = uiWindow(c);
+
+	return [w->window isVisible];
+}
+
+static void uiWindowShow(uiControl *c)
+{
+	uiWindow *w = (uiWindow *) c;
+
+	[w->window makeKeyAndOrderFront:w->window];
+}
+
+static void uiWindowHide(uiControl *c)
+{
+	uiWindow *w = (uiWindow *) c;
+
+	[w->window orderOut:w->window];
+}
+
+uiDarwinControlDefaultEnabled(uiWindow, window)
+uiDarwinControlDefaultEnable(uiWindow, window)
+uiDarwinControlDefaultDisable(uiWindow, window)
+
+static void uiWindowSyncEnableState(uiDarwinControl *c, int enabled)
+{
+	uiWindow *w = uiWindow(c);
+
+	if (uiDarwinShouldStopSyncEnableState(uiDarwinControl(w), enabled))
+		return;
+	if (w->child != NULL)
+		uiDarwinControlSyncEnableState(uiDarwinControl(w->child), enabled);
+}
+
+static void uiWindowSetSuperview(uiDarwinControl *c, NSView *superview)
+{
+	// TODO
+}
+
+static void windowRelayout(uiWindow *w)
+{
+	NSView *childView;
+	NSView *contentView;
+
+	removeConstraints(w);
+	if (w->child == NULL)
+		return;
+	childView = (NSView *) uiControlHandle(w->child);
+	contentView = [w->window contentView];
+	singleChildConstraintsEstablish(&(w->constraints),
+		contentView, childView,
+		uiDarwinControlHugsTrailingEdge(uiDarwinControl(w->child)),
+		uiDarwinControlHugsBottom(uiDarwinControl(w->child)),
+		w->margined,
+		@"uiWindow");
+}
+
+uiDarwinControlDefaultHugsTrailingEdge(uiWindow, window)
+uiDarwinControlDefaultHugsBottom(uiWindow, window)
+
+static void uiWindowChildEdgeHuggingChanged(uiDarwinControl *c)
+{
+	uiWindow *w = uiWindow(c);
+
+	windowRelayout(w);
+}
+
+// TODO
+uiDarwinControlDefaultHuggingPriority(uiWindow, window)
+uiDarwinControlDefaultSetHuggingPriority(uiWindow, window)
+// end TODO
+
+static void uiWindowChildVisibilityChanged(uiDarwinControl *c)
+{
+	uiWindow *w = uiWindow(c);
+
+	windowRelayout(w);
+}
+
+char *uiWindowTitle(uiWindow *w)
+{
+	return uiDarwinNSStringToText([w->window title]);
+}
+
+void uiWindowSetTitle(uiWindow *w, const char *title)
+{
+	[w->window setTitle:toNSString(title)];
+}
+
+void uiWindowContentSize(uiWindow *w, int *width, int *height)
+{
+	NSRect r;
+
+	r = [w->window contentRectForFrameRect:[w->window frame]];
+	*width = r.size.width;
+	*height = r.size.height;
+}
+
+void uiWindowSetContentSize(uiWindow *w, int width, int height)
+{
+	w->suppressSizeChanged = YES;
+	[w->window setContentSize:NSMakeSize(width, height)];
+	w->suppressSizeChanged = NO;
+}
+
+int uiWindowFullscreen(uiWindow *w)
+{
+	return w->fullscreen;
+}
+
+void uiWindowSetFullscreen(uiWindow *w, int fullscreen)
+{
+	if (w->fullscreen && fullscreen)
+		return;
+	if (!w->fullscreen && !fullscreen)
+		return;
+	w->fullscreen = fullscreen;
+	if (w->fullscreen && w->borderless)		// borderless doesn't play nice with fullscreen; don't toggle while borderless
+		return;
+	w->suppressSizeChanged = YES;
+	[w->window toggleFullScreen:w->window];
+	w->suppressSizeChanged = NO;
+	if (!w->fullscreen && w->borderless)		// borderless doesn't play nice with fullscreen; restore borderless after removing
+		[w->window setStyleMask:NSBorderlessWindowMask];
+}
+
+void uiWindowOnContentSizeChanged(uiWindow *w, void (*f)(uiWindow *, void *), void *data)
+{
+	w->onContentSizeChanged = f;
+	w->onContentSizeChangedData = data;
+}
+
+void uiWindowOnClosing(uiWindow *w, int (*f)(uiWindow *, void *), void *data)
+{
+	w->onClosing = f;
+	w->onClosingData = data;
+}
+
+int uiWindowBorderless(uiWindow *w)
+{
+	return w->borderless;
+}
+
+void uiWindowSetBorderless(uiWindow *w, int borderless)
+{
+	w->borderless = borderless;
+	if (w->borderless) {
+		// borderless doesn't play nice with fullscreen; wait for later
+		if (!w->fullscreen)
+			[w->window setStyleMask:NSBorderlessWindowMask];
+	} else {
+		[w->window setStyleMask:defaultStyleMask];
+		// borderless doesn't play nice with fullscreen; restore state
+		if (w->fullscreen) {
+			w->suppressSizeChanged = YES;
+			[w->window toggleFullScreen:w->window];
+			w->suppressSizeChanged = NO;
+		}
+	}
+}
+
+void uiWindowSetChild(uiWindow *w, uiControl *child)
+{
+	NSView *childView;
+
+	if (w->child != NULL) {
+		childView = (NSView *) uiControlHandle(w->child);
+		[childView removeFromSuperview];
+		uiControlSetParent(w->child, NULL);
+	}
+	w->child = child;
+	if (w->child != NULL) {
+		uiControlSetParent(w->child, uiControl(w));
+		childView = (NSView *) uiControlHandle(w->child);
+		uiDarwinControlSetSuperview(uiDarwinControl(w->child), [w->window contentView]);
+		uiDarwinControlSyncEnableState(uiDarwinControl(w->child), uiControlEnabledToUser(uiControl(w)));
+	}
+	windowRelayout(w);
+}
+
+int uiWindowMargined(uiWindow *w)
+{
+	return w->margined;
+}
+
+void uiWindowSetMargined(uiWindow *w, int margined)
+{
+	w->margined = margined;
+	singleChildConstraintsSetMargined(&(w->constraints), w->margined);
+}
+
+static int defaultOnClosing(uiWindow *w, void *data)
+{
+	return 0;
+}
+
+static void defaultOnPositionContentSizeChanged(uiWindow *w, void *data)
+{
+	// do nothing
+}
+
+uiWindow *uiNewWindow(const char *title, int width, int height, int hasMenubar)
+{
+	uiWindow *w;
+
+	finalizeMenus();
+
+	uiDarwinNewControl(uiWindow, w);
+
+	w->window = [[libuiNSWindow alloc] initWithContentRect:NSMakeRect(0, 0, (CGFloat) width, (CGFloat) height)
+		styleMask:defaultStyleMask
+		backing:NSBackingStoreBuffered
+		defer:YES];
+	[w->window setTitle:toNSString(title)];
+
+	// do NOT release when closed
+	// we manually do this in uiWindowDestroy() above
+	[w->window setReleasedWhenClosed:NO];
+
+	if (windowDelegate == nil) {
+		windowDelegate = [[windowDelegateClass new] autorelease];
+		[delegates addObject:windowDelegate];
+	}
+	[windowDelegate registerWindow:w];
+	uiWindowOnClosing(w, defaultOnClosing, NULL);
+	uiWindowOnContentSizeChanged(w, defaultOnPositionContentSizeChanged, NULL);
+
+	return w;
+}
+
+// utility function for menus
+uiWindow *windowFromNSWindow(NSWindow *w)
+{
+	if (w == nil)
+		return NULL;
+	if (windowDelegate == nil)		// no windows were created yet; we're called with some OS X-provided window
+		return NULL;
+	return [windowDelegate lookupWindow:w];
+}
diff --git a/src/libui_sdl/libui/darwin/winmoveresize.m b/src/libui_sdl/libui/darwin/winmoveresize.m
new file mode 100644
index 0000000..9145b7b
--- /dev/null
+++ b/src/libui_sdl/libui/darwin/winmoveresize.m
@@ -0,0 +1,253 @@
+// 1 november 2016
+#import "uipriv_darwin.h"
+
+// because we are changing the window frame each time the mouse moves, the successive -[NSEvent locationInWindow]s cannot be meaningfully used together
+// make sure they are all following some sort of standard to avoid this problem; the screen is the most obvious possibility since it requires only one conversion (the only one that a NSWindow provides)
+static NSPoint makeIndependent(NSPoint p, NSWindow *w)
+{
+	NSRect r;
+
+	r.origin = p;
+	// mikeash in irc.freenode.net/#macdev confirms both that any size will do and that we can safely ignore the resultant size
+	r.size = NSZeroSize;
+	return [w convertRectToScreen:r].origin;
+}
+
+struct onMoveDragParams {
+	NSWindow *w;
+	// using the previous point causes weird issues like the mouse seeming to fall behind the window edge... so do this instead
+	// TODO will this make things like the menubar and dock easier too?
+	NSRect initialFrame;
+	NSPoint initialPoint;
+};
+
+void onMoveDrag(struct onMoveDragParams *p, NSEvent *e)
+{
+	NSPoint new;
+	NSRect frame;
+	CGFloat offx, offy;
+
+	new = makeIndependent([e locationInWindow], p->w);
+	frame = p->initialFrame;
+
+	offx = new.x - p->initialPoint.x;
+	offy = new.y - p->initialPoint.y;
+	frame.origin.x += offx;
+	frame.origin.y += offy;
+
+	// TODO handle the menubar
+	// TODO wait the system does this for us already?!
+
+	[p->w setFrameOrigin:frame.origin];
+}
+
+void doManualMove(NSWindow *w, NSEvent *initialEvent)
+{
+	__block struct onMoveDragParams mdp;
+	struct nextEventArgs nea;
+	BOOL (^handleEvent)(NSEvent *e);
+	__block BOOL done;
+
+	// this is only available on 10.11 and newer (LONGTERM FUTURE)
+	// but use it if available; this lets us use the real OS dragging code, which means we can take advantage of OS features like Spaces
+	if ([w respondsToSelector:@selector(performWindowDragWithEvent:)]) {
+		[((id) w) performWindowDragWithEvent:initialEvent];
+		return;
+	}
+
+	mdp.w = w;
+	mdp.initialFrame = [mdp.w frame];
+	mdp.initialPoint = makeIndependent([initialEvent locationInWindow], mdp.w);
+
+	nea.mask = NSLeftMouseDraggedMask | NSLeftMouseUpMask;
+	nea.duration = [NSDate distantFuture];
+	nea.mode = NSEventTrackingRunLoopMode;		// nextEventMatchingMask: docs suggest using this for manual mouse tracking
+	nea.dequeue = YES;
+	handleEvent = ^(NSEvent *e) {
+		if ([e type] == NSLeftMouseUp) {
+			done = YES;
+			return YES;	// do not send
+		}
+		onMoveDrag(&mdp, e);
+		return YES;		// do not send
+	};
+	done = NO;
+	while (mainStep(&nea, handleEvent))
+		if (done)
+			break;
+}
+
+// see http://stackoverflow.com/a/40352996/3408572
+static void minMaxAutoLayoutSizes(NSWindow *w, NSSize *min, NSSize *max)
+{
+	NSLayoutConstraint *cw, *ch;
+	NSView *contentView;
+	NSRect prevFrame;
+
+	// if adding these constraints causes the window to change size somehow, don't show it to the user and change it back afterwards
+	NSDisableScreenUpdates();
+	prevFrame = [w frame];
+
+	// minimum: encourage the window to be as small as possible
+	contentView = [w contentView];
+	cw = mkConstraint(contentView, NSLayoutAttributeWidth,
+		NSLayoutRelationEqual,
+		nil, NSLayoutAttributeNotAnAttribute,
+		0, 0,
+		@"window minimum width finding constraint");
+	[cw setPriority:NSLayoutPriorityDragThatCanResizeWindow];
+	[contentView addConstraint:cw];
+	ch = mkConstraint(contentView, NSLayoutAttributeHeight,
+		NSLayoutRelationEqual,
+		nil, NSLayoutAttributeNotAnAttribute,
+		0, 0,
+		@"window minimum height finding constraint");
+	[ch setPriority:NSLayoutPriorityDragThatCanResizeWindow];
+	[contentView addConstraint:ch];
+	*min = [contentView fittingSize];
+	[contentView removeConstraint:cw];
+	[contentView removeConstraint:ch];
+
+	// maximum: encourage the window to be as large as possible
+	contentView = [w contentView];
+	cw = mkConstraint(contentView, NSLayoutAttributeWidth,
+		NSLayoutRelationEqual,
+		nil, NSLayoutAttributeNotAnAttribute,
+		0, CGFLOAT_MAX,
+		@"window maximum width finding constraint");
+	[cw setPriority:NSLayoutPriorityDragThatCanResizeWindow];
+	[contentView addConstraint:cw];
+	ch = mkConstraint(contentView, NSLayoutAttributeHeight,
+		NSLayoutRelationEqual,
+		nil, NSLayoutAttributeNotAnAttribute,
+		0, CGFLOAT_MAX,
+		@"window maximum height finding constraint");
+	[ch setPriority:NSLayoutPriorityDragThatCanResizeWindow];
+	[contentView addConstraint:ch];
+	*max = [contentView fittingSize];
+	[contentView removeConstraint:cw];
+	[contentView removeConstraint:ch];
+
+	[w setFrame:prevFrame display:YES];		// TODO really YES?
+	NSEnableScreenUpdates();
+}
+
+static void handleResizeLeft(NSRect *frame, NSPoint old, NSPoint new)
+{
+	frame->origin.x += new.x - old.x;
+	frame->size.width -= new.x - old.x;
+}
+
+// TODO properly handle the menubar
+// TODO wait, OS X does it for us?!
+static void handleResizeTop(NSRect *frame, NSPoint old, NSPoint new)
+{
+	frame->size.height += new.y - old.y;
+}
+
+static void handleResizeRight(NSRect *frame, NSPoint old, NSPoint new)
+{
+	frame->size.width += new.x - old.x;
+}
+
+
+// TODO properly handle the menubar
+static void handleResizeBottom(NSRect *frame, NSPoint old, NSPoint new)
+{
+	frame->origin.y += new.y - old.y;
+	frame->size.height -= new.y - old.y;
+}
+
+struct onResizeDragParams {
+	NSWindow *w;
+	// using the previous point causes weird issues like the mouse seeming to fall behind the window edge... so do this instead
+	// TODO will this make things like the menubar and dock easier too?
+	NSRect initialFrame;
+	NSPoint initialPoint;
+	uiWindowResizeEdge edge;
+	NSSize min;
+	NSSize max;
+};
+
+static void onResizeDrag(struct onResizeDragParams *p, NSEvent *e)
+{
+	NSPoint new;
+	NSRect frame;
+
+	new = makeIndependent([e locationInWindow], p->w);
+	frame = p->initialFrame;
+
+	// horizontal
+	switch (p->edge) {
+	case uiWindowResizeEdgeLeft:
+	case uiWindowResizeEdgeTopLeft:
+	case uiWindowResizeEdgeBottomLeft:
+		handleResizeLeft(&frame, p->initialPoint, new);
+		break;
+	case uiWindowResizeEdgeRight:
+	case uiWindowResizeEdgeTopRight:
+	case uiWindowResizeEdgeBottomRight:
+		handleResizeRight(&frame, p->initialPoint, new);
+		break;
+	}
+	// vertical
+	switch (p->edge) {
+	case uiWindowResizeEdgeTop:
+	case uiWindowResizeEdgeTopLeft:
+	case uiWindowResizeEdgeTopRight:
+		handleResizeTop(&frame, p->initialPoint, new);
+		break;
+	case uiWindowResizeEdgeBottom:
+	case uiWindowResizeEdgeBottomLeft:
+	case uiWindowResizeEdgeBottomRight:
+		handleResizeBottom(&frame, p->initialPoint, new);
+		break;
+	}
+
+	// constrain
+	// TODO should we constrain against anything else as well? minMaxAutoLayoutSizes() already gives us nonnegative sizes, but...
+	if (frame.size.width < p->min.width)
+		frame.size.width = p->min.width;
+	if (frame.size.height < p->min.height)
+		frame.size.height = p->min.height;
+	// TODO > or >= ?
+	if (frame.size.width > p->max.width)
+		frame.size.width = p->max.width;
+	if (frame.size.height > p->max.height)
+		frame.size.height = p->max.height;
+
+	[p->w setFrame:frame display:YES];			// and do reflect the new frame immediately
+}
+
+// TODO do our events get fired with this? *should* they?
+void doManualResize(NSWindow *w, NSEvent *initialEvent, uiWindowResizeEdge edge)
+{
+	__block struct onResizeDragParams rdp;
+	struct nextEventArgs nea;
+	BOOL (^handleEvent)(NSEvent *e);
+	__block BOOL done;
+
+	rdp.w = w;
+	rdp.initialFrame = [rdp.w frame];
+	rdp.initialPoint = makeIndependent([initialEvent locationInWindow], rdp.w);
+	rdp.edge = edge;
+	// TODO what happens if these change during the loop?
+	minMaxAutoLayoutSizes(rdp.w, &(rdp.min), &(rdp.max));
+
+	nea.mask = NSLeftMouseDraggedMask | NSLeftMouseUpMask;
+	nea.duration = [NSDate distantFuture];
+	nea.mode = NSEventTrackingRunLoopMode;		// nextEventMatchingMask: docs suggest using this for manual mouse tracking
+	nea.dequeue = YES;
+	handleEvent = ^(NSEvent *e) {
+		if ([e type] == NSLeftMouseUp) {
+			done = YES;
+			return YES;	// do not send
+		}
+		onResizeDrag(&rdp, e);
+		return YES;		// do not send
+	};
+	done = NO;
+	while (mainStep(&nea, handleEvent))
+		if (done)
+			break;
+}
-- 
cgit v1.2.3