io.stdout:setvbuf'no'
io.stderr:setvbuf'no'

require'strict'
local nw = require'nw'
local glue = require'glue'
local ffi = require'ffi'
local bit = require'bit'
local box2d = require'box2d'
local bitmap = require'bitmap'
local pp = require'pp'
local time = require'time'
local sleep = time.sleep

--local objc = require'objc'
--objc.debug.logtopics.refcount = true

local app --global app object
local seetime = 1
local dotime = 5

--helpers / collecting and running tests -------------------------------------

local tests = {} --{name = test} also {test1, ...} also {test = name}

--add a named test to the tests collection
local function add(name, test)
	table.insert(tests, name)
	tests[name] = test
end

local function run_test(name)
	app = app or nw:app()
	tests[name]()
end

--run all tests whose names match a pattern, in order
local function run_all_matching(patt)
	for i,name in ipairs(tests) do
		if name:match(patt) then
			print()
			print(name)
			print(('-'):rep(70))
			run_test(name)
		end
	end
end

--helpers / window position generators ---------------------------------------

local x = 100
local y = 100
local function winpos(t, same_pos)
	if not same_pos then
		if y > 600 then
			x = x + 150
			y = 100
		else
			y = y + 100
		end
	end
	return glue.update({x = x, y = y, w = 140, h = 90}, t)
end

local function cascadepos(t)
	x = x + 50
	y = y + 50
	return glue.update({x = x, y = y, w = 240, h = 190}, t)
end

--helpers / event recorder/checker -------------------------------------------

local function recorder()
	local t = {n = 0}
	local function record(...)
		print(...)
		for i = 1, select('#', ...) do
			t[t.n + i] = select(i, ...)
		end
		t.n = t.n + select('#', ...)
	end
	local function check(expected)
		assert(t.n == (expected.n or #expected))
		for i=1,t.n do
			assert(t[i] == expected[i])
		end
		print'ok'
	end
	return function(e, ...)
		if type(e) == 'table' then
			check(e)
		else
			record(e, ...)
		end
	end
end

--helpers / rendering --------------------------------------------------------

function whiteband_animation()
	local i = 0
	return function(bmp)
		local _, setpixel = bitmap.pixel_interface(bmp)
		for y = 0, bmp.h-1 do
			for x = 0, bmp.w-1 do
				local i = (i % bmp.w)
				local c = x >= i and x <= i + 50 and 255 or 100
				if x <= 10 or x >= bmp.w - 10 or y <= 10 or y >= bmp.h - 10 then
					c = 20
				end
				setpixel(x, y, c, c, c, 255)
			end
		end
		i = i + 5
	end
end

function arrows_animation()
	local r = 0
	return function(cr, w, h)
		local cairo = require'cairo'
		cr:rgba(0,0,0,1)
		cr:paint()

		cr:identity_matrix()
		cr:translate(w/2, h/2)
		cr:rotate_around(0, 0, r)
		cr:translate(-128, -128)
		r = r + 0.02

		cr:rgba(0,0.7,0,1)

		cr:line_width(40.96)
		cr:move_to(76.8, 84.48)
		cr:rel_line_to(51.2, -51.2)
		cr:rel_line_to(51.2, 51.2)
		cr:line_join'miter'
		cr:stroke()

		cr:move_to(76.8, 161.28)
		cr:rel_line_to(51.2, -51.2)
		cr:rel_line_to(51.2, 51.2)
		cr:line_join'bevel'
		cr:stroke()

		cr:move_to(76.8, 238.08)
		cr:rel_line_to(51.2, -51.2)
		cr:rel_line_to(51.2, 51.2)
		cr:line_join'round'
		cr:stroke()
	end
end

local function cube_animation()
	local r = 30
	return function(gl, w, h)
		gl.glViewport(0, 0, w, h)
		gl.glMatrixMode(gl.GL_PROJECTION)
		gl.glLoadIdentity()
		gl.glFrustum(-1, 1, -1, 1, 1, 100) --so fov is 90 deg
		gl.glScaled(1, w/h, 1)

		gl.glClearColor(0, 0, 0, 1)
		gl.glClear(bit.bor(gl.GL_COLOR_BUFFER_BIT, gl.GL_DEPTH_BUFFER_BIT))
		gl.glEnable(gl.GL_BLEND)
		gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_SRC_ALPHA)
		gl.glDisable(gl.GL_DEPTH_TEST)
		gl.glDisable(gl.GL_CULL_FACE)
		gl.glDisable(gl.GL_LIGHTING)
		gl.glMatrixMode(gl.GL_MODELVIEW)
		gl.glLoadIdentity()
		gl.glTranslated(0,0,-1)

		r = r + 1
		gl.glPushMatrix()
		gl.glTranslated(0,0,-3)
		gl.glScaled(1, 1, 1)
		gl.glRotated(r,1,r,r)
		gl.glTranslated(0,0,2)

		local function face(c)
			gl.glBegin(gl.GL_QUADS)
			gl.glColor4d(c,0,0,.5)
			gl.glVertex3d(-1, -1, -1)
			gl.glColor4d(0,c,0,.5)
			gl.glVertex3d(1, -1, -1)
			gl.glColor4d(0,0,c,.5)
			gl.glVertex3d(1, 1, -1)
			gl.glColor4d(c,0,c,.5)
			gl.glVertex3d(-1, 1, -1)
			gl.glEnd()
		end
		gl.glTranslated(0,0,-2)
		face(1)
		gl.glTranslated(0,0,2)
		face(1)
		gl.glTranslated(0,0,-2)
		gl.glRotated(-90,0,1,0)
		face(1)
		gl.glTranslated(0,0,2)
		face(1)
		gl.glRotated(-90,1,0,0)
		gl.glTranslated(0,2,0)
		face(1)
		gl.glTranslated(0,0,2)
		face(1)
		gl.glPopMatrix()
	end
end

--version checks -------------------------------------------------------------

add('ver', function()
	assert(app:ver(ffi.os:upper())) --case-insensitive match
	assert(not app:ver'XXX')
	if ffi.os == 'OSX' then
		assert(app:ver'osx')
		assert(app:ver'OSX 1')
		assert(app:ver'OSX 10')
		assert(app:ver'OSX 10.2')
		assert(not app:ver'OSX 55')
		assert(not app:ver'OS')
	elseif ffi.os == 'Windows' then
		assert(app:ver'windows')
		assert(app:ver'WINDOWS 1')
		assert(app:ver'WINDOWS 5')
		assert(app:ver'WINDOWS 5.0')
		assert(app:ver'WINDOWS 5.0.1')
		assert(not app:ver'WINDOWS 55.0.1')
		assert(not app:ver'Window')
	elseif ffi.os == 'Linux' then
		assert(app:ver'X 11')
		assert(app:ver'linux')
		assert(app:ver'Linux')
		print(app:ver'linux')
	end
	print'ok'
end)

--timers ---------------------------------------------------------------------

--runafter() works, i.e.:
--negative intervals are clamped.
--timers don't start before the loop starts.
--timer interval is respected more/less.
--timers don't fire more than once.
add('timer-runafter', function()
	local rec = recorder()
	app:runafter(-1, function() --clamped to 0
		rec(0)
	end)
	app:runafter(0.4, function()
		rec(4)
		app:quit()
	end)
	app:runafter(0.1, function()
		rec(1)
		app:runafter(0.2, function()
			rec(3)
		end)
		app:runafter(0.1, function()
			rec(2)
		end)
	end)
	rec'start' --timers start after run
	app:run()
	rec{'start', 0, 1, 2, 3, 4}
end)

--runevery() works, i.e.:
--negative intervals are clamped.
--timers don't start before the loop starts.
--timers fire continuously, but stop if false is returned.
add('timer-runevery', function()
	local rec = recorder()
	local i = 1
	app:runevery(0, function()
		rec(i)
		local stop = i == 3
		i = i + 1
		if stop then
			app:runafter(0.2, function()
				rec'quit'
				app:quit()
			end)
			rec'stop'
			return false
		end
	end)
	rec'start'
	app:run()
	rec{'start', 1, 2, 3, 'stop', 'quit'}
end)

--ensure that timers that are too fast don't starve the loop
add('timer-fast', function()
	local rec = recorder()
	local i = 1
	app:runevery(0, function()
		rec(i)
		i = i + 1
		if i > 3 then
			--stopping is done on the next loop. if the app stops
			--then it means that it reached the top of the loop.
			app:stop()
			return false
		end
	end)
	app:run()
	rec{1, 2, 3}
end)

--timers stop when the loop is stopped, and resume after the loop is resumed.
add('timer-resume', function()
	local rec = recorder()
	local i, j = 1, 1
	app:runevery(0, function()
		rec(i)
		i = i + 1
		if i > 3 then
			app:stop()
			j = j + 1
			return j < 3 --remove the timer on the second app run.
		end
	end)
	app:run()
	i = 1
	app:run()
	rec{1, 2, 3, 1, 2, 3}
end)

--app:sleep() works with an async func.
add('timer-sleep', function()
	local rec = recorder()
	app:run(function()
		for i=1,5 do
			rec(i)
			local t0 = time.clock()
			local d = i/10 -- 0.1s -> 1s
			app:sleep(d)
			print(string.format('deviation: %d%%', (time.clock() - t0 - d) / d * 100))
		end
	end)
	rec{1, 2, 3, 4, 5}
end)

add('timer-sleep-forever', function()
	local rec = recorder()
	app:runafter(0.2, function()
		app:stop()
		rec'stopped'
	end)
	app:run(function()
		rec'forever'
		app:sleep()
		error'never here'
	end)
	rec{'forever', 'stopped'}
end)

--app running and stopping ---------------------------------------------------

--run() starts the loop even if there are no windows.
--run() is ignored while running.
--run() returns.
add('loop-run-stop', function()
	local rec = recorder()
	app:runafter(0, function()
		app:run() --ignored, already running
		app:stop()
		rec'after-stop'
	end)
	app:run() --not returning immediately, stopped by timer
	rec{'after-stop'}
end)

--running() is true while app is running.
--running() is true after app:stop() is called.
add('loop-running', function()
	local rec = recorder()
	app:runafter(0, function()
		assert(app:running())
		app:stop()
		assert(app:running())
		app:stop() --ignored if called a second time
		assert(app:running())
		rec'after-stop'
	end)
	assert(not app:running())
	app:run()
	assert(not app:running())
	rec{'after-stop'}
end)

--stop() works after running a second time.
add('loop-run-stop-run', function()
	local rec = recorder()
	app:runafter(0, function()
		rec'run1'
		app:stop()
	end)
	app:run()
	app:runafter(0, function()
		rec'run2'
		app:stop()
	end)
	app:run()
	rec{'run1', 'run2'}
end)

--app quitting ---------------------------------------------------------------

--quit() is ignored if app not running.
--quit() stops the loop.
--quit() returns.
add('quit-quit', function()
	local rec = recorder()
	app:runafter(0, function()
		rec'before-quit'
		app:quit()
		rec'after-quit'
	end)
	app:quit() --ignored, not running
	app:run()
	rec{'before-quit', 'after-quit'}
end)

--quitting() event works, even if there are no windows
add('quit-quitting', function()
	local rec = recorder()
	local allow
	function app:quitting()
		if not allow then --don't allow the first time
			allow = true
			rec'not allowing'
			return false
		else --allow the second time
			rec'allowing'
		end
	end
	app:autoquit(false)
	app:runafter(0, function()
		app:quit()
		app:quit()
	end)
	app:run()
	rec{'not allowing', 'allowing'}
end)

--quitting() comes before closing() on all windows.
--closing() called in creation order.
add('quit-quitting-before-closing', function()
	local rec = recorder()
	local win1 = app:window(winpos())
	local win2 = app:window(winpos())
	function app:quitting() rec'quitting' end
	function win1:closing() rec'closing1' end
	function win2:closing() rec'closing2' end
	app:autoquit(false)
	app:runafter(0, function() app:quit() end)
	app:run()
	rec{'quitting', 'closing1', 'closing2'}
end)

--app:autoquit(true) works.
add('quit-autoquit-app', function()
	local rec = recorder()
	local win1 = app:window(winpos())
	local win2 = app:window(winpos())
	function app:quitting() rec'quitting' end
	function win1:closing() rec'closing1' end
	function win2:closing() rec'closing2' end
	app:autoquit(true)
	app:runafter(0, function()
		win1:close()
		win2:close()
	end)
	app:run()
	rec{'closing1', 'quitting', 'closing2'}
end)

--window:autoquit(true) works.
add('quit-autoquit-window', function()
	local rec = recorder()
	local win1 = app:window(winpos())
	local win2 = app:window(winpos())
	function app:quitting() rec'quitting' end
	function win1:closing() rec'closing1' end
	function win2:closing() rec'closing2' end
	app:autoquit(false)
	win2:autoquit(true)
	app:runafter(0, function()
		win2:close()
	end)
	app:run()
	rec{'quitting', 'closing1', 'closing2'}
end)

--closing() and closed() are splitted out.
--closed() happen in reverse creation order.
add('quit-quitting-sequence', function()
	local rec = recorder()
	local win1 = app:window(winpos())
	local win2 = app:window(winpos())
	function app:quitting() rec'quitting' end
	function win1:closing() rec'closing1' end
	function win2:closing() rec'closing2' end
	function win1:closed() rec'closed1' end
	function win2:closed() rec'closed2' end
	app:autoquit(false)
	app:runafter(0, function()
		app:quit()
	end)
	app:run()
	rec{'quitting', 'closing1', 'closing2', 'closed2', 'closed1'}
end)

--quit() rejected because closing() rejected.
add('quit-quitting-closing-query', function()
	local rec = recorder()
	local allow
	local win = app:window(winpos())
	function win:closing()
		if not allow then --don't allow the first time
			allow = true
			rec'not allowing'
			return false
		else --allow the second time
			rec'allowing'
		end
	end
	app:autoquit(false)
	app:runafter(0, function()
		app:quit() --not allowed
		app:quit() --allowed
	end)
	app:run()
	rec{'not allowing', 'allowing'}
end)

--quit() rejected while closing().
add('quit-quitting-while-closing', function()
	local rec = recorder()
	local allow
	local win = app:window(winpos())
	function win:closing()
		app:quit() --ignored because closing() is rejected
		rec'ignored'
	end
	app:autoquit(false)
	app:runafter(0, function()
		win:close()
		rec'closed'
		app:quit()
	end)
	app:run()
	rec{'ignored', 'closed'}
end)

--window closing -------------------------------------------------------------

--closed() event works, even before the app starts.
--dead() not yet true in the closed() event (we can still use the window).
add('close-closed-not-dead', function()
	local rec = recorder()
	local win = app:window(winpos())
	function win:closed()
		assert(not self:dead()) --not dead yet
		rec'closed'
	end
	app:autoquit(false)
	assert(not win:dead())
	win:close()
	assert(win:dead()) --dead now
	rec{'closed'}
end)

--closing() event works, even before the app starts.
add('close-closing-query', function()
	local rec = recorder()
	local win = app:window(winpos())
	local allow
	function win:closing()
		if not allow then --don't allow the first time
			rec'not allowing'
			allow = true
			return false
		else --allow the second time
			rec'allowing'
			return true
		end
	end
	app:autoquit(false)
	win:close() --not allowed
	assert(not win:dead())
	win:close() --allowed
	rec{'not allowing', 'allowing'}
end)

--close() is ignored from closed().
add('close-while-closed', function()
	local rec = recorder()
	local win = app:window(winpos())
	function win:closed()
		self:close() --ignored because not dead yet
		assert(not self:dead()) --still not dead
		rec'closed'
	end
	app:autoquit(false)
	win:close()
	assert(win:dead())
	rec{'closed'}
end)

--close() is ignored from closing().
add('close-while-closing', function()
	local rec = recorder()
	local win = app:window(winpos())
	function win:closing()
		self:close() --ignored
		assert(not self:dead())
		rec'closing'
	end
	app:autoquit(false)
	win:close()
	assert(win:dead())
	rec{'closing'}
end)

--child can close its parent from closed() event.
add('close-parent-from-closed', function()
	app:autoquit(false)
	local win1 = app:window(winpos{title = 'win1'})
	local win2 = app:window(winpos{title = 'win2', parent = win1})
	function win1:closed()
		print'win1 closed'
	end
	function win2:closed()
		print'win2 closed'
		win1:close()
	end
	win2:close()
	assert(win1:dead())
	assert(win2:dead())
	print'ok'
end)

--children close too when parent closes.
add('close-children', function()
	app:autoquit(false)
	local win1 = app:window(winpos{title = 'win1'})
	local win2 = app:window(winpos{title = 'win2', parent = win1})
	function win1:closed() print'win1 closed' end
	function win2:closed() print'win2 closed' end
	win1:close()
	app:runafter(0, function()
		assert(win1:dead())
		assert(win2:dead())
		app:quit()
	end)
	app:run()
	print'ok'
end)

--ask children too when parent closes.
add('close-children-ask', function()
	app:autoquit(false)
	local win1 = app:window(winpos{title = 'win1'})
	local win2 = app:window(winpos{title = 'win2', parent = win1})
	function win2:closing() return false end
	win1:close()
	assert(not win1:dead())
	assert(not win2:dead())
	print'ok'
end)

--window & app activaton -----------------------------------------------------

--the app is activated if a window is shown.
add('activation-app-activated', function()
	local rec = recorder()
	function app:activated() rec'app-activated' end
	local win = app:window(winpos())
	app:run(function()
		app:sleep(0.1)
	end)
	rec{'app-activated'}
end)

--the app is dactivated after the last window is hidden.
--this is an interactive test because you have to start the app with it.
add('demo-activation-app-deactivated', function()
	local rec = recorder()
	function app:deactivated() rec'app-deactivated' end
	local win = app:window(winpos())
	app:run(function()
		app:sleep(0.2)
		win:hide()
		app:sleep(0.1)
	end)
	rec{'app-deactivated'}
end)

--app:active() works (returns true only if the app is active).
--app:active_window() works (always returns nil if the app is not active).
--this is an interactive test: you must activate another app to see it.
add('demo-activation-app-active', function()
	local win = app:window(winpos())
	app:runevery(0.1, function()
		if app:active() then
			print('app is active', 'app:active_window() -> ', app:active_window())
		else
			print('app is not active', 'app:active_window() -> ', app:active_window())
		end
	end)
	app:run()
	print'ok'
end)

--in Windows, app:activate() does not activate the window immediately.
--instead, it flashes the window on the taskbar waiting for the user
--to click on it (or alt-tab to it) and activate it.
--Likewise in OSX it flashes the dock icon.
--In Linux it does nothing (only activate'force' works).
--this is an interactive test: you must activate another app to see it.
local function activation_test(mode)
	return function()
		local win = app:window(winpos())
		function app:activated() print'app-activated' end
		function app:deactivated()
			print'app-deactivated'
			app:runafter(1, function()
				app:activate(mode)
				--check that the window is not considered active until the user
				--clicks on the flashing taskbar button.
				app:runevery(0.1, function()
					print('window is active:', win:active(), 'active window:', app:active_window())
					if win:active() then
						--user clicked on the taskbar button
						app:stop()
						return false
					end
				end)
			end)
		end
		app:run()
	end
end
add('demo-activation-app-alert', activation_test())
add('demo-activation-app-info', activation_test'info')
add('demo-activation-app-force', activation_test'force')

--In Windows the app can be activated programatically even if there are no
--visible windows, but there must be at least one (hidden) window.
--In OSX the app can always be activated even without windows (the main
--menu is activated anyway).
--In Linux the app can't be activated if there isn't at least one visible
--window (because there's no concept of apps in X really).
--this is an interactive test: you must activate another app to see it.
add('demo-activation-app-activate-no-windows', function()
	function app:activated() print'activated' end
	function app:deactivated() print'deactivated' end
	local win = app:window(winpos{visible = false})
	app:runevery(1, function()
		print('app:active() -> ', app:active(), 'app:active_window() -> ', app:active_window())
		app:activate'force'
	end)
	app:run()
end)

--when the first window is shown, it is not activated if another app
--was activated in the meantime (Windows, OSX, not Linux).
--this is an interactive test: you must activate another app to see it.
add('demo-activation-window-activate-hidden', function()
	local rec = recorder()
	local win = app:window(winpos{visible = false})
	function win:activated() rec'win-activated' end
	app:runafter(0, function()
		win:activate() --ignored
		print'Right-click on this terminal window NOW! You have 2 SECONDS!'
		app:runafter(2, function()
			win:show()
			app:runafter(1, function()
				app:quit()
			end)
		end)
	end)
	app:run()
	rec{}
end)

--activable flag works for child toolbox windows.
--this is an interactive test: move the child window and it doesn't activate.
add('demo-activation-window-non-activable', function()
	local win1 = app:window{x = 100, y = 100, w = 500, h = 200}
	local win2 = app:window{x = 200, y = 130, w = 200, h = 300,
		activable = false, frame = 'toolbox', parent = win1}
	function win1:activated() rec'win1-activated' end
	function win1:deactivated() rec'win1-deactivated' end
	function win2:activated() rec'win2-activated' end
	function win2:deactivated() rec'win2-deactivated' end
	app:runevery(0.1, function() print('win1 active', win1:active()) end)
	app:run()
end)

--app visibility (OSX only) --------------------------------------------------

add('app-hide', function()
	local rec = recorder()
	function app:hidden() rec'hide' end
	function app:unhidden() rec'unhide' end
	assert(app:visible())
	app:run(function()
		app:sleep(0.2)
		app:hide()
		app:sleep(0.2)
		if app:ver'OSX' then
			assert(not app:visible())
		end
		app:unhide()
		app:sleep(0.2)
		assert(app:visible())
		if app:ver'OSX' then
			rec{'hide', 'unhide'}
		end
	end)
end)

--default initial properties -------------------------------------------------

add('init-defaults', function()
	local win = app:window(winpos())
	app:runafter(0.2, function()
		assert(win:visible())
		assert(not win:isminimized())
		assert(not win:fullscreen())
		assert(not win:ismaximized())
		assert(win:title() == '')
		assert(win:frame() == 'normal')
		assert(not win:topmost())
		assert(win:minimizable())
		assert(win:maximizable())
		assert(win:closeable())
		assert(win:resizeable())
		assert(win:fullscreenable())
		assert(not win:autoquit())
		assert(win:edgesnapping() == 'screen')
		app:quit()
	end)
	app:run()
end)

--interactive tests ----------------------------------------------------------

--[[
What you should know about window states:
- there are five state flags making up the window state:
	- v (visible)
	- m (minimized)
	- M (maximized)
	- F (fullscreen)
	- A (active)
- a window can be created with any combination of (v, m, M) but not F.
- you can't change v, m or M from F (you can only restore from fullscreen).
- state-changing methods can change one or more of the state flags at once
  depending on the initial state and the method.
- state-changing methods can behave synchronously or asynchronously.
- some state-changing methods may not always succeed in changing the state.
- state changes trigger specific state-changing events, and can also trigger:
	- window activation and/or deactivation.
	- window moving and/or resizing.
]]

local function state_string(win)
	if win:dead() then return 'x' end
	local w,h = win:client_size()
	local fx,fy,fw,fh = win:frame_rect()
	local nx,ny,nw,nh = win:normal_frame_rect()
	return
		(app:visible() and ' ' or 'h')..
		(win:visible() and 'v' or ' ')..
		(win:isminimized() and 'm' or ' ')..
		(win:ismaximized() and 'M' or ' ')..
		(win:fullscreen() and 'F' or ' ')..
		(win:active() and 'A' or ' ')..' | '..
		(app:active() and 'a' or ' ')..' | '..
		(string.format('  (%4s x%4s)',w,h))..
		(string.format('  (%4s, %4s :%4s x%4s)',fx,fy,fw,fh))..
		(string.format('  (%4s, %4s :%4s x%4s)',nx,ny,nw,nh))
end

local function mkdemo(t, child)
	return function()

		local function help()
			print[[

	F1       help
	H        hide
	S        show
	N        minimize
	M        maximize
	F        fullscreen toggle
	G        fullscreen on
	shift+G  fullscreen off
	esc      restore
	D        shownormal
	A        activate cwin
	B        activate win
	F9       activate app after 2s
	Z        resize+
	X        resize-
	shift+Z  resize+ (normal rect)
	shift+X  resize- (normal rect)
	arrows   move
	4        toggle minsize
	5        toggle maxsize
	1        toggle enabled
	2        toggle allow close
	3        toggle autoquit
	[        lower
	]        raise
	<        lower relative to win1
	>        raise relative to win1
	T        set title
	C        close / create
	Q        quit
	0        app hide (OSX)
	9        app hide and unhide after 1s (OSX)
	enter    print state
			]]
		end

		help()

		app:autoquit(true)

		local cwin, win

		local function print_state(s, ...)
			if not win or win:dead() then return end
			print(string.format('%-16s', s), state_string(win), ...)
		end

		function app:quitting()              print_state'quitting'; return true end
		function app:window_created(win)
			print(string.format('%-16s', 'window_created'), state_string(win)) end
		function app:window_closed(win)
			print(string.format('%-16s', 'window_closed'), state_string(win))
		end
		function app:unhidden()          print_state'app unhidden' end
		function app:hidden()            print_state'app hidden' end
		function app:displays_changed()      print_state'displays_changed' end
		function app:deactivated()           print_state'    app deactivated' end
		function app:activated()             print_state'    app activated' end
		function app:changed(old, new)       print_state('app changed', old..' -> '..new) end

		--make a control window that receives key presses when win gets hidden
		cwin = app:window(winpos{w = 200, h = 50, title = 'cwin'})
		cwin.name = 'cwin'

		t.parent = child and cwin or nil
		t.cx = 500
		t.cy = 300
		t.cw = 700
		t.ch = 300
		--t.edgesnapping = false
		t.autoquit = true
		t.title = 'win'

		local function create()

			win = app:window(t)
			win.name = 'win'

			function cwin:keypress(key)
				win:keypress(key)
			end

			local function short(s)
				return s:gsub('%w+', {visible = 'v', minimized = 'm',
				maximized = 'M', fullscreen = 'F', active = 'A'}):gsub(' ', '')
			end
			function win:changed(old, new)    print_state('changed', short(old)..' -> '..short(new)) end

			--synthetic changed events
			function win:minimized()      print_state'  minimized' end
			function win:maximized()      print_state'  maximized' end
			function win:unminimized()    print_state'  unminimized' end
			function win:unmaximized()    print_state'  unmaximized' end
			function win:entered_fullscreen() print_state'  entered_fullscreen' end
			function win:exited_fullscreen()  print_state'  exited_fullscreen' end
			function win:shown()              print_state'  shown' end
			function win:hidden()             print_state'  hidden' end
			function win:activated()          print_state'    activated' end
			function win:deactivated()        print_state'    deactivated' end
			function win:frame_resized(...)   print_state('      frame_resized', ...) end
			function win:frame_moved(...)     print_state('      frame_moved', ...) end
			function win:client_resized(...)  print_state('      client_resized', ...) end
			function win:client_moved(...)    print_state('      client_moved', ...) end

			function win:closed()             print_state'closed' end
			function win:sizing(...)          print_state('  sizing', ...) end

			local allow_close = true

			local function next_cursor()
				return cursors[i]
			end

			function win:keypress(key)
				if key == 'H' then
					self:hide()
				elseif key == 'S' then
					self:show()
				elseif key == 'D' then
					self:shownormal()
				elseif key == 'esc' then
					self:restore()
				elseif key == 'F' then
					local fs = not self:fullscreen()
					self:fullscreen(fs)
					if app:key'shift' then --toggle it back immediately
						self:fullscreen(not fs)
					end
					if app:key'ctrl' then --now toggle it back again
						self:fullscreen(fs)
					end
				elseif key == 'G' then
					self:fullscreen(not app:key'shift')
				elseif key == 'M' then
					self:maximize()
				elseif key == 'N' then
					self:minimize()
				elseif key == 'A' then
					cwin:activate()
				elseif key == 'B' then
					self:activate()
				elseif key == 'F9' then
					app:runafter(2, function()
						app:activate()
					end)
				elseif key == 'X' or key == 'Z' then
					local ofs = key == 'Z' and 10 or -10
					if self.app:key'shift' then
						if not win:client_rect() then return end
						win:client_rect(box2d.offset(ofs, win:client_rect()))
					else
						if not win:frame_rect() then return end
						win:frame_rect(box2d.offset(ofs, win:frame_rect()))
					end
				elseif key == 'left' or key == 'right' or key == 'up' or key == 'down' then
					if app:key'shift' then
						local x, y = win:client_rect()
						if not x then return end
						win:client_rect(
							x + (key == 'left' and -10 or key == 'right' and 10 or 0),
							y + (key == 'up'   and -10 or key == 'down'  and 10 or 0))
					else
						local x, y = win:frame_rect()
						if not x then return end
						win:frame_rect(
							x + (key == 'left' and -10 or key == 'right' and 10 or 0),
							y + (key == 'up'   and -10 or key == 'down'  and 10 or 0))
					end
				elseif key == '1' then
					win:enabled(not win:enabled())
				elseif key == 'C' then
					if self:dead() then
						create()
					else
						self:close()
					end
				elseif key == '2' then
					allow_close = not allow_close
				elseif key == '3' then
					win:autoquit(not win:autoquit())
				elseif key == '4' then
					if win:minsize() then
						win:minsize(false)
					else
						win:minsize(200, 200)
					end
				elseif key == '5' then
					if win:maxsize() then
						win:maxsize(false)
					else
						win:maxsize(400, 400)
					end
				elseif key == ',' then
					win:lower(cwin)
				elseif key == '.' then
					win:raise(cwin)
				elseif key == '[' then
					win:lower()
				elseif key == ']' then
					win:raise()
				elseif key == 'T' then
					win:title(win:title() .. '!')
				elseif key == 'tab' then
					win:cursor(next_cursor())
				elseif key == 'Q' then
					self.app:quit()
				elseif key == '0' then
					self.app:hide()
				elseif key == '9' then
					self.app:hide()
					self.app:runafter(1, function()
						self.app:show()
					end)
				elseif key == 'F1' then
					help()
				elseif key == 'enter' then
					print(('-'):rep(100))
					print('state             ', state_string(win))
					print''
					print('active            ', win:active())
					print('active window     ', app:active_window() and app:active_window().name)
					print('enabled           ', win:enabled())
					print('sticky            ', win:sticky())
					print('frame_rect        ', win:frame_rect())
					print('client_rect       ', win:client_rect())
					--TODO: implement this
					--print('normal_client_rect', win:normal_client_rect())
					print('normal_frame_rect ', win:normal_frame_rect())
					print('client_size       ', win:client_size())
					print('minsize           ', win:minsize())
					print('maxsize           ', win:maxsize())
					print('cursor            ', win:cursor())
					print('edgesnapping      ', win:edgesnapping())
					print('autoquit          ', win:autoquit())
					print''
					print('app active        ', app:active())
					print('app visible       ', app:visible())
					print('app autoquit      ', app:autoquit())
					print''
					print('display           ', pp.format(win:display()))
					print('displays"#"       ', app:displays'#')
					print('main_display      ', pp.format(app:main_display()))
					print('active_display    ', pp.format(app:active_display()))
					print(('-'):rep(100))
				end
			end

			function win:closing(...)
				print_state('closing', ...)
				return allow_close
			end
		end
		create()
		app:run()
	end
end

--initial states
add('demo', mkdemo{})
add('demo-hidden', mkdemo{visible = false})
add('demo-minimized', mkdemo{visible = true, minimized = true})
add('demo-maximized', mkdemo{visible = true, maximized = true})
add('demo-minimized-maximized', mkdemo{visible = true, minimized = true, maximized = true})
add('demo-hidden-minimized', mkdemo{visible = false, minimized = true})
add('demo-hidden-maximized', mkdemo{visible = false, maximized = true})
add('demo-hidden-minimized-maximized', mkdemo{visible = false, minimized = true, maximized = true})
add('demo-disabled', mkdemo({enabled = false}))

--restrictions
add('demo-non-minimizable', mkdemo{minimizable = false})
add('demo-non-maximizable', mkdemo{maximizable = false})
add('demo-non-closeable', mkdemo{closeable = false})
add('demo-non-resizeable', mkdemo{resizeable = false}) --implies non-maximizable non-fullscreenable
add('demo-non-activable', mkdemo({activable = false, frame = 'toolbox'}, true))
--this combination makes disabled buttons hidden rather than disabled
add('demo-non-minimizable-non-maximizable', mkdemo{minimizable = false, maximizable = false})
--this combination shows no buttons
add('demo-non-minimizable-non-maximizable-non-closeable', mkdemo{minimizable = false, maximizable = false})
--this combination allows maxsize()
add('demo-non-maximizable-non-fullscreenable', mkdemo{maximizable = false, fullscreenable = false})

--other read-only properties
add('demo-topmost', mkdemo({topmost = true}))
add('demo-parent', mkdemo({}, true))
add('demo-parent-non-sticky', mkdemo({sticky = false}, true))
add('demo-frame=none', mkdemo({frame = 'none'}))
add('demo-frame=toolbox', mkdemo({frame = 'toolbox'}, true))
add('demo-frame=toolbox-non-activable', mkdemo({frame = 'toolbox', activable = false}, true))
add('demo-frame=none-transparent', mkdemo({frame = 'none', transparent = true}, true))
add('demo-round-corners', mkdemo({corner_radius = 20}))
add('demo-frame=none-round-corners', mkdemo({frame = 'none', resizeable = true, corner_radius = 20}))

--state automated tests ------------------------------------------------------

local checkactive = true --test the active flag too!

local function parse_initial_state_string(s)
	local visible
	if s:match'h' then visible = false elseif s:match'v' then visible = true end
	return {
		visible = visible,
		minimized = s:match'm' and true or nil,
		maximized = s:match'M' and true or nil,
		fullscreen = s:match'F' and true or nil,
		active = checkactive and s:match'A' and true or nil,
	}
end

local function state_string(win)
	if win:dead() then return 'x' end
	return
		(win:visible() and 'v' or 'h')..
		(win:isminimized() and 'm' or '')..
		(win:ismaximized() and 'M' or '')..
		(win:fullscreen() and 'F' or '')..
		(checkactive and win:active() and 'A' or '')
end

--wait for a predicate on a timeout.
--uses app:sleep() instead of time.sleep() so that events can be recorded while waiting.
local function waitfor(func, timeout)
	timeout = timeout or 5 --seconds to wait for animations to complete
	local t0 = time.clock()
	while not func() do
		if time.clock() - t0 > timeout then return end --give up after timeout
		app:sleep(0.1)
	end
	return true
end

--make a state-changing test from a test spec which has the form:
--		{initial_state, actions, expected_state, actions, expected_state, ...}
local function state_test(t)
	return function()
		app:run(function()

			local t0 = time.clock()

			--parse initial state string
			local initial_state
			if type(t[1]) == 'string' then
				initial_state = parse_initial_state_string(t[1])
			else
				initial_state = glue.update(parse_initial_state_string(t[1][1] or ''), t[1])
			end

			--create a window
			local win = app:window(winpos(initial_state))

			--catch events
			local events = {}
			function win:event(event_name)
				local t1 = time.clock()
				print(string.format('   %4dms | %-5s | EVENT: %s', (t1 - t0) * 1000, state_string(self), event_name))
				t0 = t1
				events[#events+1] = event_name
				events[event_name] = (events[event_name] or 0) + 1
			end

			--wait for the window to get to its initial state
			waitfor(function()

				--previous test might have left the app inactive. fix that.
				if not win:isminimized() and win:visible() and not app:active() then
					app:activate()
				end

				return
					    (initial_state.visible ~= nil or win:visible())
					and (initial_state.visible ~= true or win:visible())
					and (initial_state.visible ~= false or not win:visible())
					and (not not initial_state.minimized == win:isminimized())
					and (not not initial_state.maximized == win:ismaximized())
					and (not not initial_state.fullscreen == win:fullscreen())
					and (not checkactive or (not not initial_state.active == win:active()))
			end)

			app:sleep(0.1)

			--run the actions
			for i=2,#t,2 do
				local actions = t[i] --actions: 'action1 action2 ...'
				local state = t[i+1] --state: '[vmMf] event1 event2...'
				local expected_state = state:match'^[hvmMFA]*'
				if not checkactive then
					expected_state = expected_state:gsub('A', '')
				end
				local expected_events = state:match'%s+(.*)' or ''
				print(state_string(win)..' -> '..actions..' -> '..expected_state..
					(expected_events ~= '' and ' ('..expected_events..')' or ''))

				--perform all the actions and record all events
				events = {}
				for action in glue.gsplit(actions, ' ') do
					local t1 = time.clock()
					print(string.format('   %4dms | %-5s | ACTION: %s', (t1 - t0) * 1000, state_string(win), action))
					t0 = t1
					if action == 'enter_fullscreen' then
						win:fullscreen(true)
					elseif action == 'exit_fullscreen' then
						win:fullscreen(false)
					else
						win[action](win)
					end

					if app:ver'Linux' then
						--Linux backend does not have proper semantics for queued async operations
						app:sleep(0.1)
					end
				end

				--poll the window until it reaches the expected state or a timeout expires.
				waitfor(function()
					--print('', state_string(win), expected_state)
					return state_string(win) == expected_state
				end)

				--wait a little more so that events announcing the state change can fire.
				app:sleep(0.1)

				--check that it did reach expected state.
				if state_string(win) ~= expected_state then
					error(state_string(win) .. ', expected ' .. expected_state .. ' after ' .. actions)
				end

				--check that all expected events were fired.
				local i = 0
				for event in glue.gsplit(expected_events, ' ') do
					if event ~= '' then
						i = i + 1
						if not events[event] then
							error(table.concat(events, ' ') .. ', expected ' .. expected_events .. ', missing '..event)
						end
						if events[event] > 1 then
							--TODO: fix these problems in the Linux backend
							if app:ver'Linux' then
								print('\n\n\n\nWARNING multiple '..event..'\n\n\n\n')
							else
								error('multiple '..event)
							end
						end
					end
				end
			end

			--close the window
			app:autoquit(false) --but don't exit the loop!
			win:close()

			--wait for it to be closed
			assert(waitfor(function() return win:dead() end), 'window not dead')

			print'  --------+---------------------------------'
		end)
	end
end

for i,t in ipairs{

	--basic check: check single transitions fron initial hidden state
	{'h', 'show', 'vA shown activated'},
	{'h', 'hide', 'h'},
	{'h', 'maximize', 'vMA shown maximized activated'},
	{'h', 'minimize', 'vm minimized'},
	{'h', 'restore', 'vA shown activated'},
	{'h', 'shownormal', 'vA shown activated'},

	--basic check: check single transitions fron initial normal state
	{'vA', 'show', 'vA'},
	{'vA', 'hide', 'h hidden deactivated'},
	{'vA', 'maximize', 'vMA maximized'},
	{'vA', 'minimize', 'vm minimized deactivated'},
	{'vA', 'restore', 'vA'},
	{'vA', 'shownormal', 'vA'},

	--basic check: check single transitions fron initial minimized state
	{'vm', 'show', 'vm'},
	{'vm', 'hide', 'hm hidden'},
	{'vm', 'maximize', 'vMA unminimized maximized activated'},
	{'vm', 'minimize', 'vm'},
	{'vm', 'restore', 'vA unminimized activated'},
	{'vm', 'shownormal', 'vA unminimized activated'},

	--basic check: check single transitions fron initial hidden minimized state
	{'hm', 'show', 'vm shown'},
	{'hm', 'hide', 'hm'},
	{'hm', 'maximize', 'vMA shown unminimized maximized activated'},
	{'hm', 'minimize', 'vm shown'},
	{'hm', 'restore', 'vA shown unminimized activated'},
	{'hm', 'shownormal', 'vA shown unminimized activated'},

	--basic check: check single transitions fron initial maximized state
	{'vMA', 'show', 'vMA'},
	{'vMA', 'hide', 'hM hidden deactivated'},
	{'vMA', 'maximize', 'vMA'},
	{'vMA', 'minimize', 'vmM minimized deactivated'},
	{'vMA', 'restore', 'vA unmaximized'},
	{'vMA', 'shownormal', 'vA unmaximized'},

	--basic check: check single transitions fron initial hidden maximized state
	{'hM', 'show', 'vMA shown activated'},
	{'hM', 'hide', 'hM'},
	{'hM', 'maximize', 'vMA shown activated'},
	{'hM', 'minimize', 'vmM minimized'},
	{'hM', 'restore', 'vA unmaximized'},
	{'hM', 'shownormal', 'vA unmaximized'},

	--basic check: check single transitions fron initial minimized maximized state
	{'vmM', 'show', 'vmM'},
	{'vmM', 'hide', 'hmM hidden'},
	{'vmM', 'maximize', 'vMA unminimized'},
	{'vmM', 'minimize', 'vmM'},
	{'vmM', 'restore', 'vMA unminimized'},
	{'vmM', 'shownormal', 'vA unminimized unmaximized'},

	--basic check: check single transitions fron initial hidden minimized maximized state
	{'hmM', 'show', 'vmM shown'},
	{'hmM', 'hide', 'hmM'},
	{'hmM', 'maximize', 'vMA unminimized activated'},
	{'hmM', 'minimize', 'vmM shown'},
	{'hmM', 'restore', 'vMA unminimized activated'},
	{'hmM', 'shownormal', 'vA unminimized unmaximized activated'},

	--basic check: transitions from fullscreen
	{'vA', 'enter_fullscreen', 'vFA', 'show', 'vFA'},
	{'vA', 'enter_fullscreen', 'vFA', 'hide', 'vFA'},
	{'vA', 'enter_fullscreen', 'vFA', 'maximize', 'vFA'},
	{'vA', 'enter_fullscreen', 'vFA', 'minimize', 'vFA'},
	{'vA', 'enter_fullscreen', 'vFA', 'restore', 'vA'},
	{'vA', 'enter_fullscreen', 'vFA', 'shownormal', 'vFA'},

	--combined checks: check sequences of commands because some commands perform
	--asynchronously which may reveal bad assumptions made by other commands
	--that look at the current state to decide how to perform the operation.
	--1. check that calls are not merged (i.e. that all relevant events fire).
	--2. check that subsequent commands are not ignored while other commands perform.
	--3. check that the final state is correct.

	--basic check: duplicate commands
	{'vA',  'hide hide hide', 'h hidden'},
	{'h',   'show show show', 'vA shown'},
	{'vA',  'maximize maximize maximize', 'vMA maximized'},
	{'vA',  'minimize minimize minimize', 'vm minimized'},
	{'vm',  'restore restore restore', 'vA unminimized'},
	{'vMA', 'restore restore restore', 'vA unmaximized'},
	{'vMA', 'shownormal shownormal shownormal', 'vA unmaximized'},
	{'vm',  'shownormal shownormal shownormal', 'vA unminimized'},
	{'vmM', 'shownormal shownormal shownormal', 'vA unminimized unmaximized'},

	--combined check: check more complex combinations
	{'vA',  'hide show', 'vA hidden shown'},
	{'h',   'show hide', 'h shown hidden'},
	{'vA',  'maximize restore', 'vA maximized unmaximized'},
	{'vA',  'minimize restore', 'vA minimized deactivated unminimized activated'},
	{'vA',  'maximize minimize restore', 'vMA maximized minimized deactivated unminimized activated'},
	{'vA',  'maximize minimize restore restore', 'vA maximized minimized deactivated unminimized unmaximized activated'},
	{'vA',  'maximize hide', 'hM maximized hidden'},
	{'vA',  'minimize hide', 'hm minimized hidden'},
	{'vA',  'maximize minimize hide', 'hmM maximized minimized deactivated hidden'},

	--even more complex combinations (particularly minimized->hide->shownormal doesn't activate the window on Windows)
	{'vA',  'maximize hide restore',    'vA maximized deactivated hidden unmaximized'},
	{'vA',  'maximize hide shownormal', 'vA maximized deactivated hidden unmaximized'},

	{'vA',  'minimize hide show',       'vm minimized deactivated hidden shown'},
	{'vA',  'minimize hide restore',    'vA minimized deactivated hidden shown unminimized activated'},
	{'vA',  'minimize hide shownormal', 'vA minimized deactivated hidden shown unminimized activated'},
	{'vA',  'maximize minimize hide show',           'vmM maximized minimized deactivated hidden shown'},
	{'vA',  'maximize minimize hide restore',        'vMA maximized minimized deactivated hidden shown unminimized activated'},
	{'vA',  'maximize minimize hide shownormal',      'vA maximized minimized deactivated hidden shown unminimized unmaximized activated'},
	{'vA',  'maximize minimize hide restore restore', 'vA maximized minimized deactivated hidden shown unminimized unmaximized activated'},
	{'hM',  'restore',         'vA shown unmaximized activated'},
	{'hm',  'restore',         'vA shown unminimized activated'},
	{'hmM', 'restore',        'vMA shown unminimized activated'},
	{'hmM', 'restore restore', 'vA shown unminimized activated unmaximized'},

	--transtions in and out of fullscreen

	--transitions from maximized fullscreen
	{'vA', 'maximize enter_fullscreen', 'vMFA', 'show', 'vMFA'},
	{'vA', 'maximize enter_fullscreen', 'vMFA', 'hide', 'vMFA'},
	{'vA', 'maximize enter_fullscreen', 'vMFA', 'maximize', 'vMFA'},
	{'vA', 'maximize enter_fullscreen', 'vMFA', 'minimize', 'vMFA'},
	{'vA', 'maximize enter_fullscreen', 'vMFA', 'restore', 'vMA exited_fullscreen'},
	{'vA', 'maximize enter_fullscreen', 'vMFA', 'shownormal', 'vMFA'},

	--transitions to fullscreen
	{'vA',  'enter_fullscreen', 'vFA entered_fullscreen'},
	{'h',   'enter_fullscreen', 'vFA shown entered_fullscreen activated'},
	{'vm',  'enter_fullscreen', 'vFA unminimized entered_fullscreen activated'},
	{'vMA', 'enter_fullscreen', 'vMFA entered_fullscreen'},
	{'hm',  'enter_fullscreen', 'vFA shown unminimized activated entered_fullscreen'},
	{'vmM', 'enter_fullscreen', 'vMFA unminimized entered_fullscreen activated'},
	{'hmM', 'enter_fullscreen', 'vMFA shown unminimized entered_fullscreen activated'},
	{'vA',  'enter_fullscreen', 'vFA', 'enter_fullscreen', 'vFA'},
	{'vMA', 'enter_fullscreen', 'vMFA', 'enter_fullscreen', 'vMFA'},

	--transitions to exit fullscreen
	{'vA',  'exit_fullscreen', 'vA'},
	{'h',   'exit_fullscreen', 'h'},
	{'vm',  'exit_fullscreen', 'vm'},
	{'vMA', 'exit_fullscreen', 'vMA'},
	{'hm',  'exit_fullscreen', 'hm'},
	{'vmM', 'exit_fullscreen', 'vmM'},
	{'hmM', 'exit_fullscreen', 'hmM'},
	{'vA',  'enter_fullscreen', 'vFA', 'exit_fullscreen', 'vA exited_fullscreen'},
	{'vMA', 'enter_fullscreen', 'vMFA', 'exit_fullscreen', 'vMA exited_fullscreen'},

} do

	--make up a name for the test
	local nt = {}
	for i = 2, #t, 2 do
		glue.append(nt, t[i] and t[i]:gsub(' ', '-'))
	end
	local test_name = (t[1] == 'vA' and '' or t[1]..'-')..table.concat(nt, '-')

	add('state-'..test_name, state_test(t))
end

--parent/child relationship --------------------------------------------------

add('demo-children', function()
	local w1 = app:window(winpos{x = 100, y = 100, w = 500, h = 300})
	local w2 = app:window(winpos{x = 200, y = 200, w = 500, h = 300, parent = w1})
	function w2:closing()
		print'w2 closing'
	end
	function w1:closed()
		--w2:show()
		--w2.backend.nswin:makeKeyAndOrderFront(nil)
		print(w2:visible())
	end
	app:run()
end)

--hiding the parent ----------------------------------------------------------

--check that a hidden parent results in a visible window that doesn't show on taskbar.
--bonus: check that autoquit works on child windows.
add('demo-parent-hidden', function()
	local win1 = app:window(winpos{visible = false})
	local win2 = app:window(winpos{parent = win1, autoquit = true})
	app:run()
end)

--check that hiding the parent results in a visible window that doesn't show on taskbar.
add('demo-parent-hide', function()
	local win1 = app:window(winpos())
	local win2 = app:window(winpos{parent = win1, autoquit = true})
	app:run(function()
		app:sleep(0.1) --for Linux
		win1:hide()
		app:sleep(true)
	end)
end)

--check that showing back the parent preserves the parent-child relationship.
add('demo-parent-hide-show', function()
	local win1 = app:window(winpos())
	local x, y = win1:client_rect()
	local win2 = app:window(winpos{x = x + 20, y = y + 20, parent = win1, autoquit = true})
	app:run(function()
		app:sleep(0.5)
		win1:hide()
		app:sleep(0.5)
		win1:show()
		app:sleep(true)
	end)
end)

--sticky children ------------------------------------------------------------

--children are sticky: they follow parent on move (but not on resize, maximize, etc).
--interactive test: move the parent to see child moving too.
add('demo-children-sticky', function()
	local win1 = app:window{x = 100, y = 100, w = 500, h = 200}
	local win2 = app:window{x = 200, y = 130, w = 200, h = 300, parent = win1, sticky = true}
	app:run()
end)

--children are not sticky: they don't follow parent on move or resize or maximize.
--interactive test: move the parent to see child moving too.
add('demo-children-non-sticky', function()
	local win1 = app:window{x = 100, y = 100, w = 500, h = 200}
	local win2 = app:window{x = 200, y = 130, w = 200, h = 300, parent = win1, sticky = false}
	app:run()
end)

--state/enabled --------------------------------------------------------------

--interactive test showing modal operation based on `enabled` and `parent` properties.
add('demo-enabled', function()
	local win1 = app:window(winpos{x = 100, y = 100, w = 500, h = 300, enabled = false})
	local win2 = app:window(winpos{x = 200, y = 150, w = 300, h = 200, parent = win1,
		minimizable = false, maximizable = false, resizeable = false})
	function win2:closing()
		win1:enabled(true)
	end
	function win1:event(...)
		print(...)
	end
	app:run()
end)

--positioning/app-level frame-client conversions -----------------------------

add('pos-client-to-frame', function()
	local function test_frame(frame, has_menu)
		local cx, cy, cw, ch = 100, 200, 300, 400
		local x, y, w, h = app:client_to_frame(frame, has_menu, cx, cy, cw, ch)
		assert(x <= cx)
		assert(y <= cy)
		assert(w >= cw)
		assert(h >= ch)
		print(frame, has_menu, '', cx, cy, cw, ch, '->', x, y, w, h)
	end
	for _,frame in ipairs{'normal', 'toolbox', 'none'} do
		test_frame(frame, false)
		test_frame(frame, true)
	end
end)

add('pos-frame-to-client', function()
	local x, y, w, h = 100, 200, 300, 400

	local cx, cy, cw, ch = app:frame_to_client('normal', false, x, y, w, h)
	assert(x <= cx)
	assert(y <= cy)
	assert(w >= cw)
	assert(h >= ch)

	--the minimum client rect for a zero-sized frame rect is 1.
	local cx, cy, cw, ch = app:frame_to_client('normal', false, 0, 0, 0, 0)
	assert(cw == 1)
	assert(ch == 1)

	--if no frame, frame rect and client rect match, even at size 1.
	local cx, cy, cw, ch = app:frame_to_client('none', false, 0, 0, 0, 0)
	assert(cx == 0)
	assert(cy == 0)
	assert(cw == 1)
	assert(ch == 1)

	print'ok'
end)

--positioning/initial values -------------------------------------------------

--check that a window is spanning the entire workspace -2px on all sides.
add('demo-pos-init-client', function()
	local sx, sy, sw, sh = app:main_display():desktop_rect()
	local fw1, fh1, fw2, fh2 = app:frame_extents'normal'
	local b = 2
	local win = app:window{
		cx = sx + fw1 + b,
		cy = sy + fh1 + b,
		cw = sw - fw1 - fw2 - 2*b,
		ch = sh - fh1 - fh2 - 2*b,
	}
	app:run()
end)

--check that a window spanning the entire workspace -2px on all sides.
add('demo-pos-init-frame', function()
	local sx, sy, sw, sh = app:main_display():desktop_rect()
	local b = 2
	local win = app:window{
		x = sx + b,
		y = sy + b,
		w = sw - 2*b,
		h = sh - 2*b,
	}
	app:run()
end)

--test if x,y,w,h mixed with cx,cy,cw,ch works.
--this is an eye-test for framed windows.
add('demo-pos-init-mixed', function()
	app:window{cx = 200, cy = 200, cw = 200, ch = 200}
	app:window{x = 200, cy = 200, w = 200, ch = 200}
	app:window{cx = 200, y = 200, cw = 200, h = 200}
	app:window{x = 200, y = 200, w = 200, h = 200}
	app:run()
end)

--check that frame_rect() and initial client rect values match for frameless windows.
add('pos-init-client-noframe', function()
	local win = app:window{cx = 100, cy = 100, cw = 500, ch = 500, frame = 'none'}
	local x, y, w, h = win:frame_rect()
	assert(x == 100)
	assert(y == 100)
	assert(w == 500)
	assert(h == 500)
	print'ok'
end)

--check that the default window position is cascaded.
add('demo-pos-init-cascade', function()
	for i = 1,30 do
		app:window{w = 500, h = 300}
	end
	app:run()
end)

--check that negative sizes are accepted (and are clamped to (1,1)).
add('pos-init-neg-size', function()
	app:run(function()
		local win = app:window({cw = -1, ch = -1})
		app:sleep(0.1)
		local cw, ch = win:client_size()
		assert(cw >= 1)
		assert(ch >= 1)
	end)
end)

--positioning/screen-client conversions --------------------------------------

--to_screen() and to_screen() conversions work.
local function pos_conv_test(visible)
	return function()
		app:run(function()
			local win = app:window{cx = 100, cy = 100, cw = 400, ch = 400, visible = visible}
			app:sleep(0.1)
			local x, y = win:to_screen(100, 100)
			assert(x == 200)
			assert(y == 200)
			local x, y = win:to_client(x, y)
			assert(x == 100)
			assert(y == 100)
			print'ok'
		end)
	end
end
add('pos-conversions', pos_conv_test(true))
add('pos-conversions-hidden', pos_conv_test(false))

--positioning/window states --------------------------------------------------

--TODO: check that frame_rect() and client_rect() return nil in hidden
--and minimized states; check that setting them is ignored in all but
--normal state.

--TODO: check that {client|frame}_{changed|moved|resized} events are triggered,
--including on state changes; check for nil args on `minimized` and `hidden` events.

--normal_frame_rect ----------------------------------------------------------

--TODO: check that normal_frame_rect() returns correct values in the following cases:
--		initial normal state
--		initial maximized state
--		after maximized
--		after fullscreen
--		after minimized
--		after maximized and minimized

--edge snapping --------------------------------------------------------------

--interactive test: move and resize windows around.
add('demo-pos-snap', function()
	app:window(winpos{w = 300, title = 'no snap', edgesnapping = false})
	app:window(winpos{w = 300, title = 'snap to: default'})
	app:window(winpos{w = 300, title = 'snap to: screen', edgesnapping = 'screen'})
	app:window(winpos{w = 300, title = 'snap to: app', edgesnapping = 'app'})
	app:window(winpos{w = 300, title = 'snap to: other', edgesnapping = 'other'}) --NYI
	app:window(winpos{w = 300, title = 'snap to: app screen', edgesnapping = 'app screen'})
	local win = app:window(winpos{w = 300, title = 'snap to: all', edgesnapping = 'all'})
	local child1 = app:window(winpos{w = 300, title = 'child1 snap to: parent', edgesnapping = 'parent', parent = win})
	local child2 = app:window(winpos{w = 300, title = 'child2 snap to: siblings', edgesnapping = 'siblings', parent = win})
	app:run()
end)

--size constraints -----------------------------------------------------------

--resize the windows to see the constraints in effect.
add('pos-minmax', function()
app:run(function()

	app:autoquit(false)

	--check that initial constraints are set and the window size respects them.
	local win = app:window{w = 800, h = 800, min_cw = 200, min_ch = 200, max_cw = 400, max_ch = 400,
		maximizable = false, fullscreenable = false}

	app:sleep(0.1)

	local minw, minh = win:minsize()
	assert(minw == 200)
	assert(minh == 200)

	local maxw, maxh = win:maxsize()
	assert(maxw == 400)
	assert(maxh == 400)

	local w, h = win:client_size()
	assert(w == 400)
	assert(h == 400)

	win:close()

	--check that minsize() is set and that it resizes the window.
	local win = app:window{w = 100, h = 100}

	local rec = recorder()
	function win:sizing() rec'error' end
	function win:frame_resized() rec'resized' end

	win:minsize(200, 200)
	app:sleep(0.1)

	rec{'resized'}

	local minw, minh = win:minsize()
	assert(minw == 200)
	assert(minh == 200)

	local w, h = win:client_size()
	assert(w == 200)
	assert(h == 200)

	win:close()

	--check that maxsize() is set and that it resizes the window.
	local win = app:window{w = 800, h = 800, maximizable = false, fullscreenable = false}
	app:sleep(0.1)

	local rec = recorder()
	function win:sizing() rec'error' end
	function win:frame_resized() rec'resized' end

	win:maxsize(400, 400)
	app:sleep(0.1)

	rec{'resized'}

	local maxw, maxh = win:maxsize()
	assert(maxw == 400)
	assert(maxh == 400)

	local w, h = win:client_size()
	assert(w == 400)
	assert(h == 400)

	win:close()

	--check that initial partial constraints work too.
	local win = app:window{w = 800, h = 100, max_cw = 400, min_ch = 200,
		maximizable = false, fullscreenable = false}
	app:sleep(0.1)

	local minw, minh = win:minsize()
	assert(minw == 1)
	assert(minh == 200)

	local maxw, maxh = win:maxsize()
	assert(maxw == 400)
	assert(maxh == nil)

	local w, h = win:client_size()
	assert(w == 400)
	assert(h == 200)

	win:close()

	--check that runtime partial constraints work too.
	local win = app:window{w = 100, h = 800, maximizable = false, fullscreenable = false}

	win:minsize(200, nil)
	app:sleep(0.1)
	local minw, minh = win:minsize()
	assert(minw == 200)
	assert(minh == 1)

	win:maxsize(nil, 400)
	app:sleep(0.1)
	local maxw, maxh = win:maxsize()
	assert(maxw == nil)
	assert(maxh == 400)

	local w, h = win:client_size()
	assert(w == 200)
	assert(h == 400)

	win:close()

	--frame_rect() is constrained too.
	local win = app:window{w = 100, h = 100, min_cw = 200, max_cw = 500, min_ch = 200, max_ch = 500,
		maximizable = false, fullscreenable = false}

	win:frame_rect(nil, nil, 100, 100)
	app:sleep(0.1)
	local w, h = win:client_size()
	assert(w == 200)
	assert(h == 200)

	win:frame_rect(nil, nil, 600, 600)
	app:sleep(0.1)
	local w, h = win:client_size()
	assert(w == 500)
	assert(h == 500)

	win:close()
	--TODO: check that setting minsize/maxize inside sizing() event works.
	--TODO: check that minsize is itself constrained to previously set maxsize and viceversa.
end) --app:run()
end)

--setting maxsize > screen size constrains the window to screen size,
--but the window can be resized to larger than screen size manually.
add('demo-pos-minmax-large-max', function()
	local win = app:window{x = 100, y = 100, w = 10000, h = 10000, max_cw = 10000, max_ch = 10000,
		maximizable = false, fullscreenable = false}
	app:run()
end)

--setting minsize > screen size works.
--it's buggy/slow on both Windows and OSX for very large sizes.
add('demo-pos-minmax-large-min', function()
	local win = app:window{x = 100, y = 100, w = 10000, h = 10000, min_cw = 10000, min_ch = 10000}
	app:run()
end)

--title ----------------------------------------------------------------------

add('title', function()
	local win = app:window(winpos{title = 'with title'})
	assert(win:title() == 'with title')
	win:title'changed'
	assert(win:title() == 'changed')
	win:close()
end)

--z-order --------------------------------------------------------------------

--interactive test showing topmost.
add('demo-topmost', function()
	local win = app:window(winpos{title = 'top1', x = 100, y = 100, topmost = true, autoquit = true})
	assert(win:topmost())
	win:topmost(false)
	assert(not win:topmost())
	win:topmost(true)
	assert(win:topmost())

	local win2 = app:window(winpos{title = 'top2', x = 120, y = 140, autoquit = true})
	win2:topmost(true)
	assert(win:topmost())

	local win3 = app:window(winpos{title = 'normal1', x = 40, y = 160, autoquit = true})
	local win4 = app:window(winpos{title = 'normal2', x = 160, y = 200, topmost = true, autoquit = true})
	win4:topmost(false)

	app:run()
end)

add('zorder', function()
	local ix, iy = x, y
	for i = 1,5 do
		app:window(cascadepos{title = 'window'..i})
	end
	x, y = ix + 500, iy
	for i = 1,5 do
		app:window(cascadepos{title = 'window'..i..'-top', topmost = true})
	end
	function app:activated()
		app:runafter(0.5, function()
			app:windows()[3]:raise(); assert(not app:windows()[3]:topmost())
			app:windows()[5]:lower(); assert(not app:windows()[5]:topmost())
			app:windows()[8]:raise(); assert(app:windows()[8]:topmost())
			app:windows()[10]:lower(); assert(app:windows()[10]:topmost())
		end)
	end
	app:run()
end)

--displays -------------------------------------------------------------------

--client rect is fully enclosed in screen rect and has a sane size
--client rect has a sane size
local function test_display(display)

	local x, y, w, h = display:screen_rect()
	print('screen_rect ',  x, y, w, h)

	local cx, cy, cw, ch = display:desktop_rect()
	print('desktop_rect', cx, cy, cw, ch)

	--client rect has a sane size
	assert(cw > 100)
	assert(ch > 100)

	--client rect must be fully enclosed in screen rect
	assert(cx >= x)
	assert(cy >= y)
	assert(cw <= w)
	assert(ch <= h)
end

--there's at least one display and its values are sane.
--first display is the main display with (x, y) at (0, 0).
add('display-list', function()
	local n = 0
	for i,display in ipairs(app:displays()) do
		n = n + 1
		print(string.format('# display %d', i))
		test_display(display)
		if i == 1 then
			--main screen is first, and at (0, 0)
			local x, y = display:screen_rect()
			assert(x == 0)
			assert(y == 0)
		end
	end
	assert(n > 0) --there must be at least 1 display
	assert(n == app:displays'#')
end)

--main_display() returns a valid display.
add('display-main', function()
	local display = app:main_display()
	test_display(display)
end)

--active_display() returns a valid display.
add('display-active', function()
	local display = app:active_display()
	test_display(display)
end)

--display is available on a hidden (but with on-screen coordinates) window.
add('display-hidden', function()
	local win = app:window(winpos{visible = false})
	assert(win:display())
	win:close()

	local win = app:window(winpos{})
	win:hide()
	local d = win:display()
	assert(d)
	win:close()

	print'ok'
end)

--display is nil on an off-screen window.
--NOTE: visible=false is because X11 moves off-screen windows back on screen.
add('display-out', function()
	local win = app:window(winpos{x = -5000, y = -5000, frame = 'none', visible = false})
	local x, y = win:frame_rect()
	assert(x == -5000)
	assert(y == -5000)
	assert(not win:display())
	win:close()
	print'ok'
end)

local function test_autoscaling(scaling)
	app:autoscaling(scaling)
	print('wanted autoscaling: ', scaling and 'on' or 'off')
	print('actual autoscaling: ', app:autoscaling() and 'on' or 'off',
		app:autoscaling() ~= scaling and '(failed)' or '')

	for i,d in ipairs(app:displays()) do

		print(string.format('display %d scaling factor:', i), d.scalingfactor)
		print(string.format('display %d rectangle:     ', i), d:screen_rect())

		--create a window on this display and check its dimensions
		local x = d.x + 100
		local y = d.y + 100
		local cw0, ch0 = 300, 200
		local win = app:window{x = x, y = y, cw = cw0, ch = ch0, visible = false}

		--check that the window was indeed created on that monitor even if hidden.
		local d1 = win:display()
		assert(d1.x == d.x)
		assert(d1.y == d.y)

		--check that autoscaling does not affect window's client size.
		local cw, ch = win:client_size()
		assert(cw == cw0)
		assert(ch == ch0)
		print(string.format('window at (%d,%d):', x, y))
		print('', 'display:    ', d:screen_rect())
		print('', 'client size:', win:client_size())

		win:close()
	end
end

add('display-autoscaling-off', function() test_autoscaling(false) end)
add('display-autoscaling-on', function() test_autoscaling(true) end)

--move the window between screens with different scaling factors to see the event.
add('demo-display-scalingfactor-changed', function()
	app:autoscaling(false)
	local win = app:window{cw = 300, ch = 200}
	function win:scalingfactor_changed(factor)
		print('scalingfactor_changed', factor)
	end
	app:runevery(1, function()
		print(string.format('scaling factor: %d, client size: %d x %d', win:display().scalingfactor, win:client_size()))
	end)
	app:run()
end)

--cursors --------------------------------------------------------------------

local cursors = {'arrow', 'text', 'hand', 'cross', 'forbidden', 'size_diag1',
	'size_diag2', 'size_v', 'size_h', 'move', 'busy_arrow'}

add('cursors', function()
	local win = app:window(winpos{resizeable = true})
	function win:mousemove(x, y)
		local cursor = cursors[math.min(math.max(math.floor(x / 10), 1), #cursors)]
		win:cursor(cursor)
	end
	app:run()
end)

--frame flags ----------------------------------------------------------------

--closeable

add('closeable', function()
	local win = app:window(winpos{title = 'cannot close', closeable = false})
	assert(not win:closeable())
end)

--resizeable

add('resizeable', function()
	local win = app:window(winpos{title = 'fixed size', resizeable = false})
	assert(not win:resizeable())
end)

--frame types

add('frameless', function()
	local win = app:window(winpos{frame = 'none'})
	assert(win:frame() == 'none')
end)

add('transparent', function()
	local win = app:window(winpos{frame = 'none', transparent = true})
	assert(win:frame() == 'none')
	assert(win:transparent())
end)

--mouse ----------------------------------------------------------------------

--double click time is sane
add('mouse-click-time', function()
	local t = app.backend:double_click_time()
	print('double_click_time', t)
	assert(t > 0 and t < 5)
end)

--target area is sane
add('mouse-click-area', function()
	local w, h = app.backend:double_click_target_area()
	print('double_click_target_area', w, h)
	assert(w > 0 and w < 100)
	assert(h > 0 and h < 100)
end)

local function checkmouse_funcs(win, name)
	function win:mouseenter(x, y)        print(string.format('mouseenter %-10s', name), x, y) end
	function win:mouseleave()            print(string.format('mouseleave %-10s', name)) end
	function win:mousemove(x, y)         print(string.format('mousemove  %-10s', name), x, y) end
	function win:mousedown(button, x, y) print(string.format('mousedown  %-10s', name), button, x, y) end
	function win:mouseup(button, x, y)   print(string.format('mouseup    %-10s', name), button, x, y) end
	function win:click(button, click_count, x, y)
		print(string.format('click      %-10s', name), button, click_count, x, y)
		if click_count == 2 then return true end
	end
	function win:mousewheel(delta, x, y) print('wheel '..name, delta, x, y) end
end

--check that mousemove() event is triggered inside the client area of the active window.
--check that mousemove() event continues outside the client area while at least one mouse button is held.
--check that mouseenter() and mouseleave() events are muted while mouse buttons are pressed.
--[the order of events when moving the mouse between windows is undefined.]
--check that mouseenter() is triggered if the mouse was over the window when the window is shown.
--check that mouseleave() is triggered if the mouse was over the window when the window is hidden.
add('demo-mouse', function()
	local win1 = app:window{x = 100, y = 100, w = 300, h = 200}
	local win2 = app:window{x = 150, y = 150, w = 300, h = 200}
	checkmouse_funcs(win1, 'win1')
	checkmouse_funcs(win2, 'win2')
	app:run()
end)

add('demo-mouse-view', function()
	local function mkwin(name)
		local cw, ch = 400, 200
		local win = app:window{cw=cw, ch=ch}
		local view = win:view{
			x = 10, y = 10,
			w = cw - 20,
			h = ch - 20,
		}
		view:bitmap()
		checkmouse_funcs(win, name)
		checkmouse_funcs(view, name..'.view')
	end
	mkwin'win1'
	mkwin'win2'
	app:run()
end)

--keyboard -------------------------------------------------------------------


add('demo-input', function()
	local win1 = app:window(winpos())
	local win2 = app:window(winpos())

	--mouse
	checkmouse_funcs(win1, 'win1')
	checkmouse_funcs(win2, 'win2')

	--keyboard
	function win1:printkey(title, key, vkey)
		print(string.format('%-16s %-16s %-16s %-16s %-16s',
			title, key, vkey, app:key(key), app:key(vkey)))
	end
	win2.printkey = win1.printkey
	function win1:keydown(key, ...)
		if key == 'N' then
			app:ignore_numlock(not app:ignore_numlock())
		elseif key == 'enter' then
			print(string.format('keyboard state: capslock: %s, numlock: %s, scrolllock: %s',
				tostring(app:key'^capslock'),
				tostring(app:key'^numlock'),
				tostring(app:key'^scrolllock')))
		end
		self:printkey('keydown', key, ...)
		--print(app:key('ctrl+shift+F10'))
	end
	function win1:keypress(...)
		self:printkey('   keypress', ...)
	end
	function win1:keyup(...)
		self:printkey('keyup', ...)
	end
	function win1:keychar(s)
		print(string.format('%-16s %s', '      keychar', s))
	end
	win2.keydown = win1.keydown
	win2.keypress = win1.keypress
	win2.keyup = win1.keyup
	win2.keychar = win1.keychar

	--win2:close()

	app:run()
end)

--views ----------------------------------------------------------------------

add('demo-view-mouse', function()
	local win = app:window{cw = 700, ch = 500}
	local view = win:view{x = 50, y = 50, w = 600, h = 400, anchors = 'ltrb', visible = false}
	function view:repaint()
		view:bitmap():clear()
	end
	view:show()
	function view:event(...)
		print(...)
	end
	app:run()
end)

--NOTE: this indirectly checks get/set rect() too when resizing the window.
add('demo-view-anchors', function()
	local win = app:window{cw = 340, ch = 340, min_cw = 150, min_ch = 150, max_cw = 450, max_ch = 450,
		maximizable = false, fullscreenable = false}
	win:view{x = 10, y = 10, w = 100, h = 100,   anchors = 'tl'}
	win:view{x = 120, y = 10, w = 100, h = 100,  anchors = 'tlr'}
	win:view{x = 230, y = 10, w = 100, h = 100,  anchors = 'tr'}
	win:view{x = 10, y = 120, w = 100, h = 100,  anchors = 'tlb'}
	win:view{x = 120, y = 120, w = 100, h = 100, anchors = 'tlrb'}
	win:view{x = 230, y = 120, w = 100, h = 100, anchors = 'trb'}
	win:view{x = 10, y = 230, w = 100, h = 100,  anchors = 'lb'}
	win:view{x = 120, y = 230, w = 100, h = 100, anchors = 'lrb'}
	win:view{x = 230, y = 230, w = 100, h = 100, anchors = 'br'}
	for i,v in ipairs(win:views()) do
		function v:repaint() self:bitmap():clear() end
	end
	app:run()
end)

--to_screen() and to_screen() conversions work.
local function pos_conv_test(visible)
	return function()
		app:run(function()
			local win = app:window{cx = 100, cy = 100, cw = 400, ch = 400, visible = visible}
			local view = win:view{x = 100, y = 100, w = 200, h = 200}
			app:sleep(0.1)
			local x, y = view:to_screen(100, 100)
			assert(x == 300)
			assert(y == 300)
			local x, y = view:to_client(x, y)
			assert(x == 100)
			assert(y == 100)
			print'ok'
		end)
	end
end
add('pos-view-conversions', pos_conv_test(true))
add('pos-view-conversions-hidden', pos_conv_test(false))

--check that negative sizes are accepted (don't crash)
add('pos-view-init-neg-size', function()
	app:run(function()
		local win = app:window{w = 100, h = 100, visible = false}
		local view = win:view{x = 0, y = 0, w = -1, h = -1}
		app:sleep(0.1)
		print(select(3, view:rect()))
	end)
end)

--rendering ------------------------------------------------------------------

add('demo-render-window-bitmap', function()
	local whiteband = whiteband_animation()
	local win = app:window{x = 200, y = 200, w = 600, h = 300}
	function win:repaint() whiteband(self:bitmap()) end
	app:runevery(1/30, function() win:invalidate() end)
	app:run()
end)

add('demo-render-window-cairo', function()
	local arrows = arrows_animation()
	local win = app:window{x = 200, y = 200, w = 600, h = 300}
	function win:repaint() arrows(self:bitmap():cairo(), self:client_size()) end
	app:runevery(1/30, function() win:invalidate() end)
	app:run()
end)

add('demo-render-window-gl', function()
	local cube = cube_animation()
	local win = app:window{x = 200, y = 200, w = 600, h = 300,
		opengl = {antialiasing = 'multisample', samples = 16}}
	function win:repaint() cube(self:gl(), self:client_size()) end
	app:runevery(1/30, function() win:invalidate() end)
	app:run()
end)

local function render_view_test(gen_view, opengl)
	local cw, ch, q = 600, 600, 300
	local win = app:window{cw = cw, ch = ch, opengl = opengl}
	for y = 0,cw-1,q do
		for x = 0,ch-1,q do
			local r = x == cw-q and 'r' or ''
			local b = y == ch-q and 'b' or ''
			local repaint, opengl = gen_view()
			local view = win:view{x = x, y = y, w = q, h = q, anchors = 'lt'..r..b, opengl = opengl}
			view.repaint = repaint
		end
	end
	app:runevery(1/30, function()
		for i,view in ipairs(win:views()) do
			view:invalidate()
		end
	end)
	app:run()
end

add('demo-render-view-bitmap', function()
	render_view_test(function()
		local whiteband = whiteband_animation()
		return function(self) whiteband(self:bitmap()) end
	end)
end)

add('demo-render-view-cairo', function()
	render_view_test(function()
		local arrows = arrows_animation()
		return function(self) arrows(self:bitmap():cairo(), self:size()) end
	end)
end)

add('demo-render-view-gl', function()
	render_view_test(function()
		local cube = cube_animation()
		return function(self) cube(self:gl(), self:size()) end, true
	end)
end)

add('demo-render-view-mixed', function()
	local i = 0
	render_view_test(function()
		i = i + 1
		if i == 1 then
			local whiteband = whiteband_animation()
			return function(self) whiteband(self:bitmap()) end
		elseif i == 2 then
			local arrows = arrows_animation()
			return function(self) arrows(self:bitmap():cairo(), self:size()) end
		elseif i == 3 then
			local cube = cube_animation()
			return function(self) cube(self:gl(), self:size()) end, true
		end
	end)
end)

--menus ----------------------------------------------------------------------

add('menu', function()

	local function setmenu()
		local win = app:window(winpos{w = 500, h = 300})
		local winmenu = app:ver'Windows' and win:menubar() or app:menubar()
		local menu1 = app:menu()
		menu1:add('Option1\tCtrl+G', function() print'Option1' end)
		menu1:add('Option2', function() print'Option2' end)
		menu1:set(2, 'Option2-changed', function() print'Option2-changed' end, {checked = true})
		menu1:add(2, 'Dead Option')
		menu1:remove(2)
		menu1:add(2, '') --separator
		menu1:checked(1, true)
		assert(menu1:checked(3))
		menu1:checked(3, false)
		assert(not menu1:checked(3))
		--assert(menu1:enabled(3))
		menu1:enabled(3, false)
		assert(not menu1:enabled(3))
		winmenu:add('Menu1', menu1)
		winmenu:add'---' --separator: not for menu bar items
		local menu2 = app:menu()
		winmenu:add('Menu2', menu2)
		local menu3 = app:menu()
		menu2:add('Menu3', menu3)
		local menu4 = app:menu()
		menu3:add('Menu4', menu4)
		menu4:add('Option41', function() print'Option41' end)
		menu4:add('Option42', function() print'Option42' end)
		local pmenu = app:menu()
		pmenu:add'Option1'
		pmenu:add'Option2'
		function win:mouseup(button, x, y)
			if button == 'right' then
				win:popup(pmenu, x, y)
			end
		end

		assert(winmenu:items'#' == 3)
		assert(winmenu:get(1).action == menu1)
		assert(winmenu:get(3, 'action') == menu2)
		assert(#winmenu:items() == 3)
		assert(winmenu:items()[3].action == menu2)
	end

	app:runafter(1, function()
		setmenu()
	end)

	app:run()
end)

--notification icons ---------------------------------------------------------

add('notify', function()
	local icon = app:notifyicon{text = 'hello', length = 80}

	local menu = app:menu()
	menu:add'Option1'
	menu:add'Option2'

	icon:tooltip'Hey imma tooltip'
	icon:menu(menu)

	print(icon:rect())

	--make one icon that increases alpha from 0% to 100% once per second.
	--do it inside repaint().

	local i = 1
	function icon:repaint()
		i = (i + 1/30) % 1
		local bmp = icon:bitmap()
		fill_bmp(bmp, 1, 0, 0, i)
	end

	app:runevery(1/30, function()
		icon:invalidate()
	end)

	--make a second icon that's yellow.
	--do it outside repaint().

	local icon2 = app:notifyicon()
	local bmp = icon2:bitmap()
	fill_bmp(bmp, 1, 1, 0, 1)
	icon2:invalidate()

	app:runafter(3, function()
		app:quit()
	end)

	app:run()
end)

--window icon (windows only) -------------------------------------------------

add('window-icon', function()
	local win = app:window(winpos{w = 500, h = 300, visible = false})

	--make the alt-tab icon yellow. do it in repaint().
	local bigicon = win:icon'big'
	function bigicon:repaint()
		local bmp = self:bitmap()
		fill_bmp(bmp, 1, 1, 0, 1)
	end
	bigicon:invalidate()

	--make the taskbar icon red. do it outside repaint().
	--note: if the small icon is not set explicitly, the normal icon will be
	--scaled down and used instead.
	local smallicon = win:icon'small'
	local bmp = smallicon:bitmap()
	fill_bmp(bmp, 1, 0, 0, 1)
	smallicon:invalidate()

	win:show()

	app:run()
end)

--dock icon (osx only) -------------------------------------------------------

add('dock-icon', function()
	local rec = recorder()
	local icon = app:dockicon()

	function icon:repaint()
		animate_bmp(icon:bitmap())
	end

	function icon:free_bitmap(bitmap)
		print(bitmap.w, bitmap.h)
		rec'free_bitmap'
	end

	app:runevery(1/30, function()
		icon:invalidate()
	end)

	app:runafter(5, function()
		app:quit()
	end)

	app:run()
	rec{'free_bitmap'}
end)

--file dialogs ---------------------------------------------------------------

add('dialog-open-default', function()
	print(app:opendialog())
end)

add('dialog-open-custom', function()
	--custom title, custom file types, default file type, multiselect.
	local path = app:ver'OSX' and '/' or app:ver'Windows' and 'C:\\'
	local paths = app:opendialog{
		title = 'What do you mean "open him up"?',
		filetypes = {'png', 'jpeg'}, --only if files = true
		multiselect = true,
		path = path,
	}
	pp(paths)
end)

add('dialog-save-default', function()
	print(app:savedialog())
end)

add('dialog-save-custom', function()
	--custom title, custom file types, default file type.
	local path = app:ver'OSX' and '/' or app:ver'Windows' and 'C:\\'
	local path = app:savedialog{
		title = 'Save time!',
		filetypes = {'png', 'jpeg'},
		filename = 'example',
		path = path,
	}
	print(path)
end)

--filetypes option can't be an empty list.
add('dialog-notypes', function()
	assert(not pcall(function() app:opendialog{filetypes = {}} end))
	assert(not pcall(function() app:savedialog{filetypes = {}} end))
end)

--clipboard ------------------------------------------------------------------

add('clipboard-text', function()
	local s = 'I am The Ocean'
	app:setclipboard(s)
	assert(#app:clipboard() == 1)
	assert(app:clipboard()[1] == 'text')
	assert(app:clipboard'text' == s)
	assert(not app:clipboard'files')
	print(app:clipboard'text')
end)

add('clipboard-files', function()
	local files =
		app:ver'OSX' and {'/home', '/na-file1', '/na-dir2/'} or
		app:ver'Windows' and {'C:\\Windows', 'Q:\\na-file1', 'O:\\na-dir2\\'}
	app:setclipboard(files, 'files')
	assert(#app:clipboard() == 1)
	assert(app:clipboard()[1] == 'files')
	assert(#app:clipboard('files') == #files)
	assert(app:clipboard('files')[1] == files[1])
	assert(app:clipboard('files')[2] == files[2])
	assert(not app:clipboard'text')
end)

add('clipboard-bitmap', function()
	local bmp = app:clipboard'bitmap'
	if bmp then
		app:setclipboard(bmp)
	end
end)

add('clipboard-inspect', function()

	for i,name in ipairs(app:clipboard()) do
		print(name)
		pp(app:clipboard(name))
	end

	local bmp = app:clipboard'bitmap'
	if bmp then
		local bitmap = require'bitmap'
		local margin = 50
		local win = app:window{cw = bmp.w + 2*margin, ch = bmp.h + 2*margin}
		local x, y = margin, margin
		function win:repaint()
			local wbmp = win:bitmap()
			checkerboard_bmp(wbmp)
			local x, y, w, h = box2d.clip(x, y, bmp.w, bmp.h, 0, 0, wbmp.w, wbmp.h)
			local src = bitmap.sub(bmp, 0, 0, w, h)
			local dst = bitmap.sub(wbmp, x, y, w, h)
			if src and dst then
				bitmap.blend(src, dst)
			end
		end
		win:invalidate()
		app:run()
	end

end)

--drag & drop ----------------------------------------------------------------

add('drop-files', function()
	local win = app:window(winpos())
	function win:dropfiles(x, y, files)
		pp(x, y, files)
	end
	app:run()
end)

add('dragging', function()
	local t = {'abort', 'none', 'copy', 'link'}
	local win = app:window(winpos{x = 100, y = 100, w = #t * 100, h = 100})
	function win:dragging(how, data, x, y)
		print(how, x, y)
		pp(data)
		if x then
			local ret = t[math.floor((x % (100 * #t)) / 100) + 1]
			print(ret)
			return ret
		end
		--return true
	end
	app:run()
end)

--run tests ------------------------------------------------------------------

local name = ...
if not name then
	print(string.format('Usage: %s name | prefix*', arg[0]))
	print'Available tests:'
	for i,name in ipairs(tests) do
		print('', name)
	end
elseif name:match'%*$' then
	run_all_matching('^'..glue.esc(name:gsub('%*', ''))..'.*')
elseif tests[name] then
	run_test(name)
else
	print'What test was that?'
end

