1local degenerates = {} -- set of known visible regions
2local root        = {id = 0} -- tree of current stacking order
3local idt         = {} -- mapping between xids stacking tree nodes
4local native      = {} -- VIDs for arcan native clients
5local pending     = {} -- set of pending attributes to apply
6local paired      = {} -- lookup from XID to native
7local dirty       = true
8root.children     = {}
9
10local input_focus
11local xarcan_client
12local native_handler
13local input_grab
14
15local function set_txcos(vid, tbl)
16	local props = image_storage_properties(vid)
17
18-- convert to surface local coordinates, windows can lie in negative
19-- in coordinate space, so shrink the rectangle and clamp the coordinates
20	local ss = 1.0 / props.width
21	local st = 1.0 / props.height
22	local bx = tbl.rel_x * ss
23	if bx < 0.0 then
24		tbl.anchor_w = tbl.anchor_w + tbl.rel_x
25		tbl.rel_x = 0
26		bx = 0.0
27	end
28
29	local by = tbl.rel_y * st
30	if by < 0.0 then
31		tbl.anchor_h = tbl.anchor_h + tbl.rel_y
32		tbl.rel_y = 0
33		by = 0.0
34	end
35
36	local bw = tbl.anchor_w * ss
37	local bh = tbl.anchor_h * st
38
39	image_set_txcos(vid,
40	{
41		bx,    by,    bx+bw, by,
42		bx+bw, by+bh, bx,    by+bh
43	})
44end
45
46-- return the (sub)tree in processing order (DFS)
47local function flatten(tree)
48	local tmp = {}
49	local add
50
51	add = function(node)
52		table.insert(tmp, node)
53		for _,v in ipairs(node.children) do
54			add(v)
55		end
56	end
57
58	for _,v in ipairs(tree.children) do
59		add(v)
60	end
61
62	return tmp
63end
64
65-- we reparent on viewport and restack calls
66local function add_to_stack(xid)
67	if idt[xid] then
68		return
69	end
70
71	local new = {
72		parent = root,
73		id = xid,
74		children = {}
75	}
76
77	table.insert(root.children, new)
78	idt[xid] = new
79	dirty = true
80end
81
82local function drop_from_stack(xid)
83	local src = idt[xid]
84	if not src then
85		warning("attempt to destroy unknown: " .. tostring(xid))
86		return
87	end
88
89	idt[xid] = nil
90
91-- get rid of the visible scene graph node
92	if degenerates[xid] then
93		delete_image(degenerates[xid])
94		degenerates[xid] = nil
95	end
96
97-- it is a native window, get rid off it
98	if paired[xid] then
99		delete_image(paired[xid])
100		native[paired[xid]] = nil
101		paired[xid] = nil
102	end
103
104-- unlink from tree
105	local parent = src.parent
106	local pre = #parent.children
107	print("drop", src.id)
108	table.remove_match(parent.children, src)
109	local post = #parent.children
110	assert(pre ~= post, "could not remove")
111
112-- let parent adopt children, this might need to change to unlink
113	for _,v in ipairs(src.children) do
114		v.parent = parent
115		table.insert(parent.children, v)
116	end
117	dirty = true
118end
119
120local function restack(xid, parent, nextsib)
121	local src = idt[xid]
122	dirty = true
123
124	if not src then
125		warning("attempt to unknown source: " .. tostring(xid))
126		return
127	end
128
129-- become new root
130	local ptable
131	if not parent or parent == -1 then
132		ptable = root
133	elseif not idt[parent] then
134		warning("attempt to restack to unknown parent: " .. tostring(parent))
135		return
136	else
137		ptable = idt[parent]
138	end
139
140	local res = table.remove_match(src.parent.children, src)
141	if not res then
142		print("no match in parent")
143		for _,v in ipairs(src.parent.children) do
144			print(v.xid)
145		end
146	end
147
148	local sibindex = 1
149
150	if not nextsib or nextsib <= 0 then
151		sibindex = #ptable.children
152
153	elseif not idt[nextsib] then
154		warning("invalid next sibling sent: " .. tostring(nextsib))
155	else
156		for k,v in ipairs(ptable.children) do
157			if v.id == nextsib then
158				sibindex = k
159				break
160			end
161		end
162	end
163
164	src.parent = ptable
165	table.insert(ptable.children, sibindex, src)
166--	local out = {}
167--	for _,v in ipairs(ptable.children) do
168--		table.insert(out, v.id)
169--	end
170--	print(table.concat(out, " -> "))
171end
172
173local function apply_stack()
174	dirty = false
175	local lst = flatten(root)
176
177-- go through the current stack and match against pending updates
178	for i, v in ipairs(lst) do
179		local id = v.id
180
181		if not degenerates[id] and pending[id] and not pending[id].invisible then
182			local new = null_surface(32, 32)
183			if paired[id] then
184				image_sharestorage(paired[id], new)
185-- could / should apply some cropping if there is a disagreement on size
186			else
187				image_sharestorage(xarcan_client, new)
188			end
189			degenerates[id] = new
190		end
191
192-- synch changes
193		if degenerates[id] and pending[id] then
194			order_image(degenerates[id], i) --1 + pending[id].rel_order)
195			if paired[id] then
196				image_set_txcos_default(degenerates[id], false)
197			else
198				set_txcos(degenerates[id], pending[id])
199			end
200			resize_image(degenerates[id], pending[id].anchor_w, pending[id].anchor_h)
201			move_image(degenerates[id], pending[id].rel_x, pending[id].rel_y)
202			show_image(degenerates[id])
203			pending[id] = nil
204		end
205	end
206end
207
208local function cursor_handler(source, status)
209	print("cursor", status.kind)
210end
211
212local function clipboard_handler(source, status)
213	print("clipboard", status.kind, status.message)
214end
215
216local handler
217handler =
218function(source, status)
219	if status.kind == "terminated" then
220		delete_image(source)
221
222		xarcan_client = target_alloc("xarcan", handler)
223		for _,v in pairs(degenerates) do
224			if valid_vid(v.vid) then
225				delete_image(v.vid)
226			end
227			mouse_droplistener(v.vid)
228		end
229		degenerates = {}
230		stack = {}
231
232-- requesting initial screen properties
233	elseif status.kind == "preroll" then
234		target_displayhint(source, VRESW, VRESH)
235
236	elseif status.kind == "segment_request" then
237		if status.segkind == "cursor" then
238			accept_target(32, 32, cursor_handler)
239
240		elseif status.segkind == "clipboard" then
241			CLIPBOARD = accept_target(32, 32, clipboard_handler)
242		end
243
244	elseif status.kind == "frame" then
245		if dirty then
246			apply_stack()
247		end
248
249-- randr changed resolution
250	elseif status.kind == "resized" then
251		resize_image(source, status.width, status.height)
252
253-- remove immediately, scene-graph only contains currently visible
254	elseif status.kind == "viewport" then
255		local node = idt[status.ext_id]
256		if not node then
257			warning("viewport on unknown node: " .. tostring(status.ext_id))
258			return
259		end
260
261		if status.invisible then
262			if degenerates[status.ext_id] then
263				print("dropping degenerate")
264				delete_image(degenerates[status.ext_id])
265				degenerates[status.ext_id] = nil
266			end
267		else
268			dirty = true
269			pending[status.ext_id] = status
270		end
271
272		if idt[status.parent] and node.parent ~= idt[status.parent] then
273--			print("reparent", node.parent.id, status.parent)
274--			table.insert(idt[status.parent].children, node)
275--			table.remove_match(node.parent.children, node)
276--			node.parent = idt[status.parent]
277		end
278
279-- this is in the format used with ARCAN_ARG and so on as an env- packed argv
280	elseif status.kind == "message" then
281		local args = string.unpack_shmif_argstr(status.message)
282
283		if args.kind == "pair" then
284			local xid = tonumber(args.xid)
285			local vid = tonumber(args.vid)
286			if not xid or not vid then
287				warning("pair argument error, missing xid/vid")
288				return
289			end
290			if not native[vid] then
291				warning("pair error, no such vid")
292				return
293			end
294			if not idt[xid] then
295				add_to_stack(xid)
296			end
297			print("paired", xid, vid)
298			paired[xid] = vid
299
300		elseif args.kind == "restack" then
301			local xid = tonumber(args.xid)
302			local parent = tonumber(args.parent)
303			local sibling = tonumber(args.next)
304
305			print("restack", xid, parent, sibling)
306			restack(xid, parent, sibling)
307
308		elseif args.kind == "create" then
309			local id = tonumber(args.xid)
310			add_to_stack(id)
311
312		elseif args.kind == "destroy" then
313			local id = tonumber(args.xid)
314			drop_from_stack(id)
315
316			if not valid_vid(input_focus) then
317				input_focus = xarcan_client
318			end
319		end
320	end
321end
322
323function xwm(arguments)
324	symtable = system_load("builtin/keyboard.lua")()
325	system_load("builtin/string.lua")()
326	system_load("builtin/table.lua")()
327	system_load("builtin/mouse.lua")()
328	mouse_setup(fill_surface(8, 8, 0, 255, 0), 65535, 1, true, false)
329
330	if arguments[1] then
331		xarcan_client = launch_target(arguments[1], handler)
332	else
333		xarcan_client = target_alloc("xarcan", handler)
334	end
335
336	input_focus = xarcan_client
337	target_flags(xarcan_client, TARGET_VERBOSE) -- enable 'frame update' events
338	target_flags(xarcan_client, TARGET_DRAINQUEUE)
339	assert(TARGET_DRAINQUEUE > 0)
340end
341
342local function wnd_meta(wnd)
343	res = ""
344
345	if wnd.mark then
346		res = res .. " color=\"deepskyblue\""
347	end
348
349	if degenerates[wnd.id] then
350		res = res .. " shape=\"triangle\""
351	end
352
353	return res
354end
355
356local function dump_nodes(io, tree)
357	local id = tree.id
358	local shape = "square"
359	if degenerates[id] then
360		if paired[id] then
361			shape = "triangle"
362		else
363			shape = "circle"
364		end
365	end
366
367	io:write(
368		string.format(
369			"%.0f[label=\"%.0f\" shape=\"%s\"]\n",
370			id, id, shape
371		)
372	)
373
374	for _,v in ipairs(tree.children) do
375		dump_nodes(io, v)
376	end
377end
378
379local function dump_relations(io, tree)
380	local lst = {}
381	local visit
382
383	for _,v in ipairs(tree.children) do
384		io:write(string.format("%d->%d;\n", tree.id, v.id))
385		io:write(string.format("%d->%d;\n", v.id, v.parent.id))
386	end
387
388	for _,v in ipairs(tree.children) do
389		dump_relations(io, v)
390	end
391end
392
393local bindings = {
394	F1 =
395	function()
396		local new = target_alloc("demo", native_handler)
397		native[new] = {}
398		target_input(xarcan_client, "kind=new:x=100:y=100:w=640:h=480:id=" .. new)
399	end,
400	F2 =
401	function()
402		if valid_vid(CLIPBOARD) then
403			local io = open_nonblock(CLIPBOARD, false, "primary:utf-8")
404			xwm_clock_pulse = function()
405				local msg, ok = io:read()
406				if not ok then
407					xwn_clock_pulse = nil
408					io:close()
409				elseif msg and #msg > 0 then
410					print("read: ", msg)
411				end
412			end
413		end
414	end,
415	F3 =
416	function()
417		local new = launch_avfeed("", "terminal", native_handler)
418		native[new] = {}
419		target_input(xarcan_client, "kind=new:x=100:y=100:w=640:h=480:id=" .. new)
420	end,
421	F4 =
422	function()
423		print("creating dump.dot")
424		zap_resource("dump.dot")
425		local io = open_nonblock("dump.dot", true)
426		io:write("digraph g{\n")
427		dump_nodes(io, root)
428		dump_relations(io, root)
429		io:write("subgraph order {")
430		local lst = {}
431		for _,v in ipairs(flatten(root)) do
432			if degenerates[v.id] then
433				local ch = "o"
434				if paired[v.id] then
435					ch = "P"
436				end
437				local name = ch .. tostring(v.id)
438				io:write(string.format("%s[label=\"%s\" %s]\n", name, name, wnd_meta(v)))
439				table.insert(lst, name)
440			end
441		end
442		io:write(table.concat(lst, "->"))
443		io:write(";\n}}\n")
444		io:close()
445	end,
446	F5 =
447	function()
448		snapshot_target(xarcan_client, "xorg.dot", APPL_TEMP_RESOURCE, "dot")
449	end,
450	F6 =
451	function()
452		local x, y = mouse_xy()
453		local items = pick_items(x, y, 1, true)
454		if items[1] then
455			for k,v in pairs(degenerates) do
456				if v == items[1] then
457					print("matched xid:", k)
458					idt[k].mark = true
459					break
460				end
461			end
462		end
463	end,
464	F10 = shutdown,
465	F12 =
466	function()
467		input_grab = not input_grab
468	end
469}
470
471native_handler =
472function(source, status)
473	if status.kind == "resized" then
474-- resizes are driven by the x11 side here, so forward the information
475		local wnd = native[source]
476		if wnd and wnd.xid then
477			image_set_txcos_default(wnd.vid, status.origo_ll)
478			target_input(xarcan_client, string.format(
479				"kind=configure:w=%.0f:h=%.0f:id=%d", status.width, status.height, wnd.xid))
480		end
481
482	elseif status.kind == "connected" then
483
484	elseif status.kind == "preroll" then
485		target_displayhint(source, 640, 480)
486
487	elseif status.kind == "terminated" then
488	end
489end
490
491function xwm_input(iotbl)
492	local sym, lutsym
493	if iotbl.translated then
494		sym, lutsym = symtable:patch(iotbl)
495	end
496
497	if iotbl.mouse then
498		mouse_iotbl_input(iotbl)
499	end
500
501	if bindings[sym] then
502		if iotbl.active then
503			bindings[sym]()
504		end
505		return
506	end
507
508-- routing, keyboard always goes to focus target, mouse will just use helper routing
509	if not valid_vid(input_focus, TYPE_FRAMESERVER) then
510		return
511	end
512
513-- If the WM is completely window controlled, this will conflict with modifiers
514-- when a native arcan client is selected as keyboard focus won't be able to
515-- 'jump' without some kind of toggle on our level. While basic keys will have
516-- the mod mask set, the 'release' event won't have the modifiers, causing ghost
517-- releases being sent to X. For this demo we use F12 as an 'arcan client'
518-- toggle.
519	if iotbl.translated then
520		if input_focus ~= xarcan_client and input_grab then
521			target_input(input_focus, iotbl)
522		else
523			target_input(xarcan_client, iotbl)
524		end
525	else
526		target_input(xarcan_client, iotbl)
527	end
528end
529