shithub: purgatorio

ref: fb7dd4b3a868cb8987049c95bb32e6425a73c8b9
dir: /appl/spree/clients/cards.b/

View raw version
implement Cards;
include "sys.m";
	sys: Sys;
include "draw.m";
	draw: Draw;
	Point, Rect, Display, Image, Font: import draw;
include "tk.m";
	tk: Tk;
include "tkclient.m";
	tkclient: Tkclient;
include "math.m";
	math: Math;

Cards: module {
	init: fn(ctxt: ref Draw->Context, argv: list of string);
};

# fairly general card clique client.
# inherent restrictions:
#	no dragging of cards visible over the net; it's unclear how
#		to handle the coordinate spaces involved

Object: adt {
	id:				int;
	pick {
	Card =>
		parentid:		int;
		face:			int;			# 1 is face up
		number:		int;
		rear:			int;
	Member =>
		cid:			int;
		name:		string;
	Stack =>
		o:			ref Layobject.Stack;
	Widget =>
		o:			ref Layobject.Widget;
	Menuentry =>
		parentid:		int;
		text:			string;
	Layoutframe =>
		lay:			ref Layout.Frame;
	Layoutobj =>
		lay:			ref Layout.Obj;
	Scoretable =>
		scores:		array of ref Object.Score;
	Score =>
		row:			array of (int, string);
		height:		int;
	Button =>
	Other =>
	}
};

# specify how an object is laid out.
Layout: adt {
	id:			int;
	parentid:		int;
	opts:			string;		# pack options
	orientation:	int;
	pick {
	Frame =>
		lays:		cyclic array of ref Layout;
	Obj =>
		layid:	int;			# reference to layid of laid-out object
	}
};

# an object which can be laid out on the canvas
Layobject: adt {
	id:			int;
	parentid:		int;
	w:			string;
	size:			Point;
	needrepack:	int;
	orientation:	int;
	layid:		int;
	pick {
	Stack =>
		style:		int;
		cards:		array of ref Object.Card;	# fake objects when invisible
		pos:			Point;		# top-left origin of first card in stack
		delta:		Point;		# card offset delta.
		animq:		ref Queue;	# queue of pending animations.
		actions:		int;
		maxcards:	int;
		title:			string;
		visible:		int;
		n:			int;			# for concealed stacks, n cards in stack.
		ownerid:		int;			# owner of selection
		sel:			ref Selection;
		showsize,
		hassize:		int;
	Widget =>
		wtype:		string;
		entries:		array of ref Object.Menuentry;
		cmd:			string;		# only used for entry widgets
		width:		int;
	}
};
	
Animation: adt {
	tag:		string;					# canvas tag common to cards being moved.
	srcpt:	Point;					# where cards are coming from.
	cards:	array of ref Object.Card;		# objects being transferred.
	dstid:	int;
	index:	int;
	waitch:	chan of ref Animation;		# notification comes on this chan when finished.
};

Selection: adt {
	pick {
	XRange =>
		r: Range;
	Indexes =>
		idxl: list of int;
	Empty =>
	}
};

MAXPLAYERS: con 4;

# layout actions
lFRAME, lOBJECT: con iota;

# possible actions on a card on a stack.
aCLICK: con 1<<iota;

# styles of stack display
styDISPLAY, styPILE: con iota;

# orientations
oLEFT, oRIGHT, oUP, oDOWN: con iota;

Range: adt {
	start, end: int;
};

T: type ref Animation;
Queue: adt {
	h, t: list of T; 
	put: fn(q: self ref Queue, s: T);
	get: fn(q: self ref Queue): T;
	isempty: fn(q: self ref Queue): int;
	peek: fn(q: self ref Queue): T;
};

configcmds := array[] of {
"frame .buts",
"frame .cf",
"canvas .c -width 400 -height 450 -bg green",
"label .status -text 0",
"checkbutton .buts.scores -text {Show scores} -command {send cmd scores}",
"button .buts.sizetofit -text {Fit} -command {send cmd sizetofit}",
"checkbutton .buts.debug -text {Debug} -variable debug -command {send cmd debug}",
"pack .buts.sizetofit .buts.debug .status -in .buts -side left",
"pack .buts -side top -fill x",
"pack  .c -in .cf -side top -fill both -expand 1",
"pack .cf -side top -fill both -expand 1",
"bind .c <Button-1> {send cmd b1 %X %Y}",
"bind .c <ButtonRelease-1} {send cmd b1r %X %Y}",
"bind .c <Button-2> {send cmd b2 %X %Y}",
"bind .c <ButtonRelease-2> {send cmd b2r %X %Y}",
"bind .c <ButtonPress-3> {send cmd b3 %X %Y}",
"bind .c <ButtonRelease-3> {send cmd b3r %X %Y}",
"bind . <Configure> {send cmd config}",
"pack propagate .buts 0",
".status configure -text {}",
"pack propagate . 0",
};

objects: 		array of ref Object;
layobjects := array[20] of list of ref Layobject;
members := array[8] of list of ref Object.Member;
win: 			ref Tk->Toplevel;
drawctxt:		ref Draw->Context;
me:			ref Object.Member;
layout:		ref Layout;
scoretable:	ref Object.Scoretable;
showingscores := 0;
debugging := 0;

stderr:		ref Sys->FD;
animfinishedch: chan of (ref Animation, chan of chan of ref Animation);
yieldch:		chan of int;
cardlockch: 	chan of int;
notifych:		chan of string;
tickregisterch, tickunregisterch: chan of chan of int;
starttime :=	0;
cvsfont: 		ref Font;

packwin:		ref Tk->Toplevel;	# invisible; used to steal tk's packing algorithms...
packobjs:		list of ref Layobject;
repackobjs:	list of ref Layobject;
needresize := 0;
needrepack := 0;

animid := 0;
fakeid := -2;		# ids allocated to "fake" cards in private hands; descending
nimages := 0;
Hiddenpos := Point(5000, 5000);

cliquefd: ref Sys->FD;

init(ctxt: ref Draw->Context, nil: list of string)
{
	sys = load Sys Sys->PATH;
	stderr = sys->fildes(2);
	draw = load Draw Draw->PATH;
	tk = load Tk Tk->PATH;
	math = load Math Math->PATH;
	tkclient = load Tkclient Tkclient->PATH;
	if (tkclient == nil) {
		sys->fprint(stderr, "cards: cannot load %s: %r\n", Tkclient->PATH);
		raise "fail:bad module";
	}
	tkclient->init();
	drawctxt = ctxt;
	client1();
}

# maximum number of rears (overridden by actual rear images)
rearcolours := array[] of {
	int 16r0000ccff,
	int 16rff0000ff,
	int 16rffff00ff,
	int 16r008000ff,
	int 16rffffffff,
	int 16rffaa00ff,
	int 16r00ffffff,
	int 16r808080ff,
	int 16r00ff00ff,
	int 16r800000ff,
	int 16r800080ff,
};
Rearborder := 3;
Border := 6;
Selectborder := 3;
cardsize: Point;
carddelta := Point(12, 15);		# offset in order to see card number/suit
Selectcolour := "red";
Textfont := "/fonts/pelm/unicode.8.font";

client1()
{
	cliquefd = sys->fildes(0);
	if (readconfig() == -1)
		raise "fail:error";

	winctl: chan of string;
	(win, winctl) = tkclient->toplevel(drawctxt, "-font " + Textfont,
		"Cards", Tkclient->Appl);
	cmd(win, ". unmap");
	bcmd := chan of string;
	tk->namechan(win, bcmd, "cmd");
	srvcmd := chan of string;
	tk->namechan(win, srvcmd, "srv");

	if (readcardimages() == -1)
		raise "fail:error";
	for (i := 0; i < len configcmds; i++)
		cmd(win, configcmds[i]);
	tkclient->onscreen(win, nil);
	tkclient->startinput(win, "kbd"::"ptr"::nil);

	fontname := cmd(win, ".c cget -font");
	cvsfont = Font.open(drawctxt.display, fontname);
	if (cvsfont == nil) {
		sys->fprint(stderr, "cards: cannot open font %s: %r\n", fontname);
		raise "fail:error";
	}
	fontname = nil;

	cardlockch = chan of int;
	spawn lockproc();

	yieldch = chan of int;
	spawn yieldproc();

	notifych = chan of string;
	spawn notifierproc();

	updatech := chan of array of byte;
	spawn readproc(cliquefd, updatech);

	spawn updateproc(updatech);
	b1down := 0;

	tickregisterch = chan of chan of int;
	tickunregisterch = chan of chan of int;
	spawn timeproc();
	spawn eventproc(win);

	for (;;) alt {
	c := <-bcmd =>
		(n, toks) := sys->tokenize(c, " ");
		case hd toks {
		"b3" =>
			curp := Point(int cmd(win, ".c canvasx " + hd tl toks),
				int cmd(win, ".c canvasy " + hd tl tl toks));
			b3raise(bcmd, curp);
		"b2" =>
			curp := Point(int cmd(win, ".c canvasx " + hd tl toks),
				int cmd(win, ".c canvasy " + hd tl tl toks));
			dopan(bcmd, "b2", curp);
		"b1" =>
			if (!b1down) {
				# b1 x y
				# x and y in screen coords
				curp := Point(int cmd(win, ".c canvasx " + hd tl toks),
					int cmd(win, ".c canvasy " + hd tl tl toks));
				b1down = b1action(bcmd, curp);
			}
		"b1r" =>
			b1down = 0;
		"entry" =>
			id := int hd tl toks;
			lock();
			cc := "";
			pick o := objects[id] {
			Widget =>
				cc = o.o.cmd;
			* =>
				sys->print("entry message from unknown obj: id %d\n", id);
			}
			unlock();
			if (cc != nil) {
				w := ".buts." + string id + ".b";
				s := cmd(win, w + " get");
				cardscmd(cc + " " + s);
				cmd(win, w + " selection range 0 end");
				cmd(win, "update");
			}
		"config" =>
			lock();
			needresize = 1;
			updatearena();
			unlock();
			cmd(win, "update");
		"scores" =>
			if (scoretable == nil)
				break;
			if (!showingscores) {
				cmd(win, ".c move score " + string -Hiddenpos.x + " " + string -Hiddenpos.y);
				cmd(win, ".c raise score");
			} else
				cmd(win, ".c move score " + p2s(Hiddenpos));
			cmd(win, "update");
			showingscores = !showingscores;
		"sizetofit" =>
			lock();
			sizetofit();
			unlock();
			cmd(win, "update");
		"debug" =>
			debugging = int cmd(win, "variable debug");
		}
	c := <-srvcmd =>		# from button or menu entry
		cardscmd(c);
	s := <-win.ctxt.ctl or
	s = <-win.wreq or
	s = <-winctl =>
		if (s == "exit")
			sys->write(cliquefd, array[0] of byte, 0);
		tkclient->wmctl(win, s);
	}
}

eventproc(win: ref Tk->Toplevel)
{
	for(;;)alt{
	s := <-win.ctxt.kbd =>
		tk->keyboard(win, s);
	s := <-win.ctxt.ptr =>
		tk->pointer(win, *s);
	}
}

readproc(fd: ref Sys->FD, updatech: chan of array of byte)
{
	buf := rest := array[Sys->ATOMICIO * 2] of byte;
	while ((n := sys->read(fd, rest, Sys->ATOMICIO)) > 0) {
		updatech <-= rest[0:n];
		rest = rest[n:];
		if (len rest < Sys->ATOMICIO)
			buf = rest = array[Sys->ATOMICIO * 2] of byte;
	}
	updatech <-= nil;
}


b1action(bcmd: chan of string, p: Point): int
{
	(hitsomething, id) := hitcard(p);
	if (!hitsomething) {
		dopan(bcmd, "b1", p);
		return 0;
	}
	if (id < 0) {		# either error, or someone else's private card
		sys->print("no card hit (%d)\n", id);
		return 1;
	}
	lock();
	if (objects[id] == nil) {
		notify("it's gone");
		unlock();
		return 1;
	}
	stack: ref Layobject.Stack;
	index := -1;
	pick o := objects[id] {
	Card =>
		card := o;
		parentid := card.parentid;
		stack = stackobj(parentid);
		for (index = 0; index < len stack.cards; index++)
			if (stack.cards[index] == card)
				break;
		if (index == len stack.cards)
			index = -1;
	Stack =>
		stack = o.o;
	* =>
		unlock();
		return 1;
	}
	actions := stack.actions;
	stackid := stack.id;
	unlock();
	# XXX potential problems when object ids get reused.
	# the object id that we saw before the unlock()
	# might now refer to a different object, so the user
	# might be performing a different action to the one intended.
	# this should be changed throughout... hmm.
	if (actions == 0) {
		notify("no way josé");
		sys->print("no way: stack %d, actions %d\n", stackid, actions);
		return 1;
	}
	cardscmd("click " + string stackid + " " + string index);
	return 1;
}

dopan(bcmd: chan of string, b: string, p: Point)
{
	r := b + "r";
	for (;;) {
		(n, toks) := sys->tokenize(<-bcmd, " ");
		if (hd toks == b) {
			pan(p, (int hd tl toks, int hd tl tl toks));
			p = Point(int cmd(win, ".c canvasx " + hd tl toks),
				int cmd(win, ".c canvasy " + hd tl tl toks));
			cmd(win, "update");
		} else if (hd toks == r)
			return;
	}
}

b3raise(bcmd: chan of string, p: Point)
{
	currcard := -1;
	above := "";
loop:	for (;;) {
		(nil, id) := hitcard(p);
		if (id != currcard) {
			if (currcard != -1 && above != nil)
				cmd(win, ".c lower i" + string currcard + " " + above);
			if (id == -1 || tagof(objects[id]) != tagof(Object.Card)) {
				above = nil;
				currcard = -1;
			} else {
				above = cmd(win, ".c find above i" + string id);
				cmd(win, ".c raise i" + string id);
				cmd(win, "update");
				currcard = id;
			}
		}
		(nil, toks) := sys->tokenize(<-bcmd, " ");
		case hd toks {
		"b3" =>
			p = Point(int cmd(win, ".c canvasx " + hd tl toks),
				int cmd(win, ".c canvasy " + hd tl tl toks));
		"b3r" =>
			break loop;
		}
	}
	if (currcard != -1 && above != nil) {
		cmd(win, ".c lower i" + string currcard + " " + above);
		cmd(win, "update");
	}
}

hitcard(p: Point): (int, int)
{
	(nil, hitids) := sys->tokenize(cmd(win, ".c find overlapping " + r2s((p, p))), " ");
	if (hitids == nil)
		return (0, -1);
	ids: list of string;
	for (; hitids != nil; hitids = tl hitids)
		ids = hd hitids :: ids;
	for (; ids != nil; ids = tl ids) {
		(nil, tags) := sys->tokenize(cmd(win, ".c gettags " + hd ids), " ");
		for (; tags != nil; tags = tl tags) {
			tag := hd tags;
			if (tag[0] == 'i' || tag[0] == 'r' || tag[0] == 'n' || tag[0] == 'N')
				return (1, int (hd tags)[1:]);
			if (tag[0] == 's')		# ignore selection
				break;
		}
		if (tags == nil)
			break;
	}
	return (1, -1);
}

cardscmd(s: string): int
{
	if (debugging)
		sys->print("cmd: %s\n", s);
	if (sys->fprint(cliquefd, "%s", s) == -1) {
		err := sys->sprint("%r");
		notify(err);
		sys->print("cmd error on '%s': %s\n", s, err);
		return 0;
	}
	return 1;
}

updateproc(updatech: chan of array of byte)
{
	wfd := sys->open("/prog/" + string sys->pctl(0, nil) + "/wait", Sys->OREAD);
	spawn updateproc1(updatech);
	buf := array[Sys->ATOMICIO] of byte;
	n := sys->read(wfd, buf, len buf);
	sys->print("updateproc process exited: %s\n", string buf[0:n]);
}

updateproc1(updatech: chan of array of byte)
{
	animfinishedch = chan of (ref Animation, chan of chan of ref Animation);
	first := 1;
	for (;;) {
		alt {
		v := <-animfinishedch =>
			lock();
			animterminated(v);
			updatearena();
			cmd(win, "update");
			unlock();
		u := <-updatech =>
			if (u == nil) {
				# XXX notify user that clique has been hung up somehow
				exit;
			}
			moretocome := 0;
			if (len u > 2 && u[len u-1] == byte '*' && u[len u-2] == byte '\n') {
				u = u[0:len u - 2];
				moretocome = 1;
			}
			(nil, lines) := sys->tokenize(string u, "\n");
			lock();
			starttime = sys->millisec();
			for (; lines != nil; lines = tl lines)
				applyupdate(hd lines);
			updatearena();
			if (!moretocome) {
				if (first) {
					sizetofit();
					first = 0;
				}
				cmd(win, "update");
			}
			unlock();
		}
	}
}

updatearena()
{
	if (needrepack)
		repackall();
	if (needresize)
		resizeall();
	for (pstk := repackobjs; pstk != nil; pstk = tl pstk)
		repackobj(hd pstk);
	repackobjs = nil;
}

applyupdate(s: string)
{
	if (debugging) {
		sys->print("update: %s\n", s);
#		showtk = 1;
	}
	(nt, toks) := sys->tokenize(s, " ");
	case hd toks {
	"create" =>
		# create id parentid vis type
		id := int hd tl toks;
		if (id >= len objects)
			objects = (array[len objects + 10] of ref Object)[0:] = objects;
		if (objects[id] != nil)
			panic(sys->sprint("object %d already exists!", id));
		parentid := int hd tl tl toks;
		vis := int hd tl tl tl toks;
		objtype := tl tl tl tl toks;
		case hd objtype {
		"stack" =>
			objects[id] = makestack(id, parentid, vis);
			needrepack = 1;
		"card" =>
			stk := stackobj(parentid);
			completeanim(stk);
			if (!stk.visible) {
				# if creating in a private stack, we assume
				# that the cards were there already, and
				# just make them real again.

				# first find a fake card.
				for (i := 0; i < len stk.cards; i++)
					if (stk.cards[i].id < 0)
						break;
				c: ref Object.Card;
				if (i == len stk.cards) {
					# no fake cards - we'll create one instead.
					# this can happen if we've entered halfway through
					# a clique, so don't know how many cards people
					# are holding.
					c = makecard(id, stk);
					insertcards(stk, array[] of {c}, len stk.cards);
				} else {
					c = stk.cards[i];
					changecardid(c, id);
				}
				objects[id] = c;
			} else {
				objects[id] = c := makecard(id, stk);
				insertcards(stk, array[] of {c}, len stk.cards);
			}
		"widget" =>
			objects[id] = makewidget(id, parentid, hd tl objtype);
		"menuentry" =>
			objects[id] = makemenuentry(id, parentid, tl objtype);
		"member" =>
			objects[id] = ref Object.Member(id, -1, "");
		"layframe" =>
			lay := ref Layout.Frame(id, parentid, "", -1, nil);
			objects[id] = ref Object.Layoutframe(id, lay);
			addlayout(lay);
		"layobj" =>
			lay := ref Layout.Obj(id, parentid, "", -1, -1);
			objects[id] = ref Object.Layoutobj(id, lay);
			addlayout(lay);
		"scoretable" =>
			if (scoretable != nil)
				panic("cannot make two scoretables");
			scoretable = objects[id] = ref Object.Scoretable(id, nil);
		"score" =>
			pick l := objects[parentid] {
			Scoretable =>
				nl := array[len l.scores + 1] of ref Object.Score;
				nl[0:] = l.scores;
				nl[len nl - 1] = objects[id] = ref Object.Score(id, nil, 0);
				l.scores = nl;
				cmd(win, "pack .buts.scores -side left");
			* =>
				panic("score created outside scoretable object");
			}
		"button" =>
			objects[id] = ref Object.Button(id);
			cmd(win, "button .buts." + string id);
			cmd(win, "pack .buts." + string id + " -side left");
		* =>
			if (parentid != -1)
				sys->print("cards: unknown objtype: '%s'\n", hd objtype);
			objects[id] = ref Object.Other(id);
		}

	"tx" =>
		# tx src dst start end dstindex
		src, dst: ref Layobject.Stack;
		index: int;
		r: Range;
		(src, toks) = (stackobj(int hd tl toks), tl tl toks);
		(dst, toks) = (stackobj(int hd toks), tl toks);
		(r.start, toks) =  (int hd toks, tl toks);
		(r.end, toks) =  (int hd toks, tl toks);
		(index, toks) = (int hd toks, tl toks);
		transfer(src, r, dst, index);

	"del" =>
		# del parent start end objs...
		oo := objects[int hd tl toks];	# parent
		r := Range(int hd tl tl toks, int hd tl tl tl toks);
		pick o := oo {
		Stack =>			# deleting cards from a stack.
			stk := o.o;
			completeanim(stk);
			if (!stk.visible) {
				# if deleting from a private area, we assume the cards aren't
				# actually being deleted at all, but merely becoming
				# invisible, so turn them into fakes.
				for (i := r.start; i < r.end; i++) {
					card := stk.cards[i];
					objects[card.id] = nil;
					changecardid(card, --fakeid);
					cardsetattr(card, "face", "0" :: nil);
				}
			} else {
				cards := extractcards(stk, r);
				for (i := 0; i < len cards; i++)
					destroy(cards[i]);
			}
		Layoutframe =>		# deleting the layout specification.
			lay := o.lay;
			if (r.start != 0 || r.end != len lay.lays)
				panic("cannot partially delete layouts");
			for (i := r.start; i < r.end; i++)
				destroy(objects[lay.lays[i].id]);
			lay.lays = nil;
			needrepack = 1;
		Widget =>
			# must be a menu widget
			cmd(win, ".buts." + string o.id + ".m delete " +
				string r.start + " " + string r.end);
		* =>
			for (objs := tl tl tl tl toks; objs != nil; objs = tl objs)
				destroy(objects[int hd objs]);
		}

	"set" =>
		# set obj attr val
		id := int hd tl toks;
		(attr, val) := (hd tl tl toks, tl tl tl toks);
		pick o := objects[id] {
		Card =>
			cardsetattr(o, attr, val);
		Widget =>
			widgetsetattr(o.o, attr, val);
		Stack =>
			stacksetattr(o.o, attr, val);
		Member =>
			membersetattr(o, attr, val);
		Layoutframe =>
			laysetattr(o.lay, attr, val);
		Layoutobj =>
			laysetattr(o.lay, attr, val);
		Score =>
			scoresetattr(o, attr, val);
		Button =>
			buttonsetattr(o, attr, val);
		Menuentry =>
			menuentrysetattr(o, attr, val);
		* =>
			sys->fprint(stderr, "unknown attr set on object(tag %d), %s\n", tagof(objects[id]), s);
		}

	"say" or
	"remark" =>
		notify(join(tl toks));
	* =>
		sys->fprint(stderr, "cards: unknown update message '%s'\n", s);
	}
}

addlayout(lay: ref Layout)
{
	pick lo := objects[lay.parentid] {
	Layoutframe =>
		l := lo.lay;
		nl := array[len l.lays + 1] of ref Layout;
		nl[0:] = l.lays;
		nl[len nl - 1] = lay;
		l.lays = nl;
	* =>
		if (layout == nil)
			layout = lay;
		else
			panic("cannot make two layout objects");
	}
}

makestack(id, parentid: int, vis: int): ref Object.Stack
{
	o := ref Object.Stack(
		id,
		ref Layobject.Stack(
			id,
			parentid,
			"",			# pack widget name
			(0, 0),		# size
			0,			# needrepack
			-1,			# orientation
			-1,			# layid
			-1,			# style
			nil,			# cards
			Hiddenpos,	# pos
			(0, 0),		# delta
			ref Queue,
			0,			# actions
			0,			# maxcards
			"",			# title
			vis,			# visible
			0,			# n
			-1,			# ownerid
			ref Selection.Empty,		# sel
			1,			# showsize
			0			# hassize
		)
	);
	cmd(win, ".c create rectangle -10 -10 -10 -10 -width 3 -tags r" + string id);
	return o;
}

makewidget(id, parentid: int, wtype: string): ref Object.Widget
{
	wctype := wtype;
	if (wtype == "menu")
		wctype = "menubutton";
	# XXX the widget is put in a frame 'cos of bugs in the canvas
	# to do with size propagation.
	w := cmd(win, "frame .buts." + string id + " -bg transparent");
	cmd(win, wctype + " " + w + ".b");
	cmd(win, "pack " + w + ".b -fill both -expand 1");
	case wtype {
	"menu" =>
		cmd(win, "menu " + w + ".m");
		cmd(win, w + ".b configure -menu " + w + ".m" +
			" -relief raised");
	"entry" =>
		cmd(win, "bind " + w + ".b <Key-\n> {send cmd entry " + string id + "}");
	}
	cmd(win, ".c create window -1000 -1000 -tags r" + string id +
		" -window " + w + " -anchor nw");
	o := ref Object.Widget(
		id,
		ref Layobject.Widget(
			id,
			parentid,
			nil,		# w
			(0, 0),	# size
			0,		# needrepack
			-1,		# orientation
			-1,		# style

			wtype,
			nil,		# entries
			"",		# cmd
			0		# width
		)
	);
	return o;
}

menutitleid := 0;		# hack to identify menu entries
makemenuentry(id, parentid: int, nil: list of string): ref Object.Menuentry
{
	m := ".buts." + string parentid + ".m";
	t := "@" + string menutitleid++;
	cmd(win, m + " add command -text " + t);
	return ref Object.Menuentry(id, parentid, t);
}

makecard(id: int, stack: ref Layobject.Stack): ref Object.Card
{
	cmd(win, ".c create image 5000 5000 -anchor nw -tags i" + string id);
	return ref Object.Card(id, stack.id, -1, -1, 0);
}

buttonsetattr(b: ref Object.Button, attr: string, val: list of string)
{
	w := ".buts." + string b.id;
	case attr {
	"text" =>
		cmd(win, w + " configure -text '" + join(val));
	"command" =>
		cmd(win, w + " configure -command 'send srv " + join(val));
	* =>
		sys->print("unknown attribute on button: %s\n", attr);
	}
}

widgetsetattr(b: ref Layobject.Widget, attr: string, val: list of string)
{
	w := ".buts." + string b.id + ".b";
	case attr {
	"text" =>
		t := join(val);
		if (b.wtype == "entry") {
			cmd(win, w + " delete 0 end");
			cmd(win, w + " insert 0 '" + t);
			cmd(win, w + " select 0 end");		# XXX ??
		} else {
			cmd(win, w + " configure -text '" + t);
			needresize = 1;
		}
	"command" =>
		case b.wtype {
		"button" =>
			cmd(win, w + " configure -command 'send srv " + join(val));
		"entry" =>
			b.cmd = join(val);
		}
	"width" =>		# width in characters
		b.width = int hd val;
		sys->print("configuring %s for width %s\n", w, hd val);
		cmd(win, w + " configure -width " + hd val + "w");
		needresize = 1;
	"layid" =>
		setlayid(b, int hd val);
	* =>
		sys->print("unknown attribute on button: %s\n", attr);
	}
}

findmenuentry(m: string, title: string): int
{
	end := int cmd(win, m + " index end");
	for (i := 0; i <= end; i++) {
		t := cmd(win, m + " entrycget " + string i + " -text");
		if (t == title)
			return i;
	}
	return -1;
}

menuentrysetattr(e: ref Object.Menuentry, attr: string, val: list of string)
{
	m := ".buts." + string e.parentid + ".m";
	idx := findmenuentry(m, e.text);
	if (idx == -1) {
		sys->print("couldn't find menu entry '%s'\n", e.text);
		return;
	}
	case attr {
	"text" =>
		t := join(val);
		cmd(win, m + " entryconfigure " + string idx +" -text '" + t);
		e.text = t;
	"command" =>
		cmd(win, m + " entryconfigure " + string idx +
				" -command 'send srv " + join(val));
	* =>
		sys->print("unknown attribute on menu entry: %s\n", attr);
	}
}

stacksetattr(stack: ref Layobject.Stack, attr: string, val: list of string)
{
	id := string stack.id;
	case attr {
	"maxcards" =>
		stack.maxcards = int hd val;
		needresize = 1;
	"layid" =>
		setlayid(stack, int hd val);
	"showsize" =>
		stack.showsize = int hd val;
		showsize(stack);
	"title" =>
		title := join(val);
		if (title != stack.title) {
			if (stack.title == nil) {
				cmd(win, ".c create text 5000 6000 -anchor n -tags t" + string id +
					" -fill #ffffaa");
				needresize = 1;
			} else if (title == nil) {
				cmd(win, ".c delete t" + string id);
				needresize = 1;
			}
			if (title != nil)
				cmd(win, ".c itemconfigure t" + string id + " -text '" + title);
			stack.title = title;
		}
	"n" =>
		# there are "n" cards in this stack, honest guv.
		n := int hd val;
		if (!stack.visible) {
			if (n > len stack.cards) {
				a := array[n - len stack.cards] of ref Object.Card;
				for (i := 0; i < len a; i++) {
					a[i] = makecard(--fakeid, stack);
					cardsetattr(a[i], "face", "0" :: nil);
				}
				insertcards(stack, a, len stack.cards);
			} else if (n < len stack.cards) {
				for (i := len stack.cards - 1; i >= n; i--)
					if (stack.cards[i].id >= 0)
						break;
				cards := extractcards(stack, (i + 1, len stack.cards));
				for (i = 0; i < len cards; i++)
					destroy(cards[i]);
			}
		}
		stack.n = n;
	"style" =>
		case hd val {
		"pile" =>
			stack.style = styPILE;
		"display" =>
			stack.style = styDISPLAY;
		* =>
			sys->print("unknown stack style '%s'\n", hd val);
		}
		needresize = 1;
	"owner" =>
		if (val != nil)
			stack.ownerid = int hd val;
		else
			stack.ownerid = -1;
		changesel(stack, stack.sel);
	"sel" =>
		sel: ref Selection;
		if (val == nil)
			sel = ref Selection.Empty;
		else if (tl val != nil && hd tl val == "-")
			sel = ref Selection.XRange((int hd val, int hd tl tl val));
		else {
			idxl: list of int;
			for (; val != nil; val = tl val)
				idxl = int hd val :: idxl;
			sel = ref Selection.Indexes(idxl);
		}
		changesel(stack, sel);
	* =>
		if (len attr >= len "actions" && attr[0:len "actions"] == "actions") {
			oldactions := stack.actions;
			act := 0;
			for (; val != nil; val = tl val) {
				case hd val {
				"click" =>
					act |= aCLICK;
				* =>
					sys->print("unknown action '%s'\n", hd val);
				}
			}
			stack.actions = act;
		} else
			sys->fprint(stderr, "bad stack attr '%s'\n", attr);
	}
}

showsize(stack: ref Layobject.Stack)
{
	id := string stack.id;
	needsize := stack.showsize && len stack.cards > 0 && stack.style == styPILE;
	if (needsize != stack.hassize) {
		if (stack.hassize)
			cmd(win, ".c delete n" + id + " N" + id);
		else {
			cmd(win, ".c create rectangle -5000 0 0 0  -fill #ffffaa -tags n" + id);
			cmd(win, ".c create text -5000 0 -anchor sw -fill red -tags N" + id);
		}
		stack.hassize = needsize;
	}
	if (needsize) {
		cmd(win, ".c itemconfigure N" + id + " -text " + string len stack.cards);
		sr := cardrect(stack, (len stack.cards - 1, len stack.cards));
		cmd(win, ".c coords N" + id + " " + p2s((sr.min.x, sr.max.y)));
		bbox := cmd(win, ".c bbox N" + id);
		cmd(win, ".c coords n" + id + " " + bbox);
		cmd(win, ".c raise n" + id + "; .c raise N" + id);
	}
}		

changesel(stack: ref Layobject.Stack, newsel: ref Selection)
{
	sid := "s" + string stack.id;
	cmd(win, ".c delete " + sid);

	if (me != nil && stack.ownerid == me.cid) {
		pick sel := newsel {
		Indexes =>
			for (l := sel.idxl; l != nil; l = tl l) {
				s := cmd(win, ".c create rectangle " +
					r2s(cardrect(stack, (hd l, hd l + 1)).inset(-1)) +
					" -width " + string Selectborder +
					" -outline " + Selectcolour +
					" -tags {" + sid + " " + sid + "." + string hd l + "}");
				cmd(win, ".c lower " + s + " i" + string stack.cards[hd l].id);
			}
		XRange =>
			cmd(win, ".c create rectangle " +
					r2s(cardrect(stack, sel.r).inset(-1)) +
					" -outline " + Selectcolour +
					" -width " + string Selectborder +
					" -tags " + sid);
		}
	}
	stack.sel = newsel;
}

cardsetattr(card: ref Object.Card, attr: string, val: list of string)
{
	id := string card.id;
	case attr {
	"face" =>
		card.face = int hd val;
		if (card.face) {
			if (card.number != -1)
				cmd(win, ".c itemconfigure i" + id + " -image c" + string card.number );
		} else
			cmd(win, ".c itemconfigure i" + id + " -image rear" + string card.rear);
	"number" =>
		card.number = int hd val;
		if (card.face)
			cmd(win, ".c itemconfigure i" + id + " -image c" + string card.number );
	"rear" =>
		card.rear = int hd val;
		if (card.face == 0)
			cmd(win, ".c itemconfigure i" + id + " -image rear" + string card.rear);
	* =>
		sys->print("unknown attribute on card: %s\n", attr);
	}
}

setlayid(layobj: ref Layobject, layid: int)
{
	if (layobj.layid != -1)
		panic("obj already has a layout id (" + string layobj.layid + ")");
	layobj.layid = layid;
	x := layobj.layid % len layobjects;
	layobjects[x] = layobj :: layobjects[x];
	needrepack = 1;
}

membersetattr(p: ref Object.Member, attr: string, val: list of string)
{
	case attr {
	"you" =>
		me = p;
		p.cid = int hd val;
		for (i := 0; i < len objects; i++) {
			if (objects[i] != nil) {
				pick o := objects[i] {
				Stack =>
					if (o.o.ownerid == p.cid)
						objneedsrepack(o.o);
				}
			}
		}
	"name" =>
		p.name = hd val;
	"id" =>
		p.cid = int hd val;
	"status" =>
		if (p == me)
			cmd(win, ".status configure -text '" + join(val));
	"cliquetitle" =>
		if (p == me)
			tkclient->settitle(win, join(val));
	* =>
		sys->print("unknown attribute on member: %s\n", attr);
	}
}

laysetattr(lay: ref Layout, attr: string, val: list of string)
{
	case attr {
	"opts" =>
		# orientation opts
		case hd val {
		"up" =>
			lay.orientation = oUP;
		"down" =>
			lay.orientation = oDOWN;
		"left" =>
			lay.orientation = oLEFT;
		"right" =>
			lay.orientation = oRIGHT;
		* =>
			sys->print("unknown orientation '%s'\n", hd val);
		}
		lay.opts = join(tl val);
	"layid" =>
#		sys->print("layout obj %d => layid %s\n", lay.id, hd val);
		pick l := lay {
		Obj =>
			l.layid = int hd val;
			needrepack = 1;
		* =>
			sys->print("cannot set layid on Layout.Frame!\n");
		}
	* =>
		sys->print("unknown attribute on lay: %s\n", attr);
	}
	needrepack = 1;
}

scoresetattr(score: ref Object.Score, attr: string, val: list of string)
{
	if (attr != "score")
		return;
	cmd(win, ".c delete score");

	Padx: con 10;		# padding to the right of each item
	Pady: con 6;		# padding below each item.

	n := len val;
	row := score.row = array[n] of (int, string);
	height := 0;

	# calculate values for this row
	for ((col, vl) := (0, val); vl != nil; (col, vl) = (col + 1, tl vl)) {
		v := hd vl;
		size := textsize(v);
		size.y += Pady;
		if (size.y > height)
			height = size.y;
		row[col] = (size.x + Padx, v);
	}
	score.height = height;
	totheight := 0;
	scores := scoretable.scores;

	# calculate number of columns
	ncols := 0;
	for (i := 0; i < len scores; i++)
		if (len scores[i].row > ncols)
			ncols = len scores[i].row;

	# calculate column widths
	colwidths := array[ncols] of {* => 0};
	for (i = 0; i < len scores; i++) {
		r := scores[i].row;
		for (j := 0; j < len r; j++) {
			(w, nil) := r[j];
			if (w > colwidths[j])
				colwidths[j] = w;
		}
		totheight += scores[i].height;
	}
	# create all table items
	p := Hiddenpos;
	for (i = 0; i < len scores; i++) {
		p.x = Hiddenpos.x;
		r := scores[i].row;
		for (j := 0; j < len r; j++) {
			(w, text) := r[j];
			cmd(win, ".c create text " + p2s(p) + " -anchor nw -tags {score scoreent}-text '" + text);
			p.x += colwidths[j];
		}
		p.y += scores[i].height;
	}
	r := Rect(Hiddenpos, p);
	r.min.x -= Padx;
	r.max.y -= Pady / 2;

	cmd(win, ".c create rectangle " + r2s(r) + " -fill #ffffaa -tags score");

	# horizontal lines
	y := 0;
	for (i = 0; i < len scores - 1; i++) {
		ly := y + scores[i].height - Pady / 2;
		cmd(win, ".c create line " + r2s(((r.min.x, ly), (r.max.x, ly))) + " -fill gray -tags score");
		y += scores[i].height;
	}

	cmd(win, ".c raise scoreent");
	cmd(win, ".c move score " + p2s(Hiddenpos.sub(r.min)));
}

textsize(s: string): Point
{
	return (cvsfont.width(s), cvsfont.height);
}

changecardid(c: ref Object.Card, newid: int)
{
	(nil, tags) := sys->tokenize(cmd(win, ".c gettags i" + string c.id), " ");
	for (; tags != nil; tags = tl tags) {
		tag := hd tags;
		if (tag[0] >= '0' && tag[0] <= '9')
			break;
	}
	cvsid := hd tags;
	cmd(win, ".c dtag " + cvsid + " i" + string c.id);
	c.id = newid;
	cmd(win, ".c addtag i" + string c.id + " withtag " + cvsid);
}

stackobj(id: int): ref Layobject.Stack
{
	obj := objects[id];
	if (obj == nil)
		panic("nil stack object");
	pick o := obj {
	Stack =>
		return o.o;
	* =>
		panic("expected obj " + string id + " to be a stack");
	}
	return nil;
}

# if there are updates pending on the stack,
# then wait for them all to finish before we can do
# any operations on the stack (e.g. insert, delete, create, etc)
completeanim(stk: ref Layobject.Stack)
{
	while (!stk.animq.isempty())
		animterminated(<-animfinishedch);
}

transfer(src: ref Layobject.Stack, r: Range, dst: ref Layobject.Stack, index: int)
{
	# we don't bother animating movement within a stack; maybe later?
	if (src == dst) {
		transfercards(src, r, dst, index);
		return;
	}
	completeanim(src);

	if (!src.visible) {
		# cards being transferred out of private area should
		# have already been created, but check anyway.
		if (r.start != 0)
			panic("bad transfer out of private");
		for (i := 0; i < r.end; i++)
			if (src.cards[i].id < 0)
				panic("cannot transfer fake card");
	}

	startanimating(newanimation(src, r), dst, index);
}

objneedsrepack(obj: ref Layobject)
{
	if (!obj.needrepack) {
		obj.needrepack = 1;
		repackobjs = obj :: repackobjs;
	}
}

repackobj(obj: ref Layobject)
{
	pick o := obj {
	Stack =>
		cards := o.cards;
		pos := o.pos;
		delta := o.delta;
		for (i := 0; i < len cards; i++) {
			p := pos.add(delta.mul(i));
			id := string cards[i].id;
			cmd(win, ".c coords i" + id + " " + p2s(p));
			cmd(win, ".c raise i" + id);		# XXX could be more efficient.
			cmd(win, ".c lower s" + string o.id + "." + string i + " i" + id);
		}
		changesel(o, o.sel);
		showsize(o);
	}
	obj.needrepack = 0;
}

cardrect(stack: ref Layobject.Stack, r: Range): Rect
{
	if (r.start == r.end)
		return ((-10, -10), (-10, -10));
	cr := Rect((0, 0), cardsize).addpt(stack.pos);
	delta := stack.delta;
	return union(cr.addpt(delta.mul(r.start)), cr.addpt(delta.mul(r.end - 1)));
}

repackall()
{
	sys->print("repackall()\n");
	needrepack = 0;
	if (layout == nil) {
		sys->print("no layout\n");
		return;
	}
	if (packwin == nil) {
		# use an unmapped tk window to do our packing arrangements
		packwin = tk->toplevel(drawctxt.display, "-bd 0");
		packwin.wreq = nil;			# stop window requests piling up.
	}
	cmd(packwin, "destroy " + cmd(packwin, "pack slaves ."));
	packobjs = nil;
	packit(layout, ".0");
	sys->print("%d packobjs\n", len packobjs);
	needresize = 1;
}

# make the frames for the objects to be laid out, in the
# offscreen window.
packit(lay: ref Layout, f: string)
{
	cmd(packwin, "frame " + f);
	cmd(packwin, "pack " + f + " " + lay.opts);
	pick l := lay {
	Frame =>
		for (i := 0; i < len l.lays; i++)
			packit(l.lays[i], f + "." + string i);
	Obj =>
		if ((obj := findlayobject(l.layid)) != nil) {
			obj.w = f;
			obj.orientation = l.orientation;
			packobjs = obj :: packobjs;
		} else
			sys->print("cannot find layobject %d\n", l.layid);
	}
}

sizetofit()
{
	if (packobjs == nil)
		return;
	cmd(packwin, "pack propagate . 1");
	cmd(packwin, ". configure -width 0 -height 0");	# make sure propagation works.
	csz := actsize(packwin, ".");
	cmd(win, "bind . <Configure> {}");
	cmd(win, "pack propagate . 1");
	cmd(win, ". configure -width 0 -height 0");

	cmd(win, ".c configure -width " + string csz.x + " -height " + string csz.y
			+ " -scrollregion {0 0 " + p2s(csz) + "}");
	winr := actrect(win, ".");
	screenr := win.image.screen.image.r;
	if (!winr.inrect(screenr)) {
		if (winr.dx() > screenr.dx())
			(winr.min.x, winr.max.x) = (screenr.min.x, screenr.max.x);
		if (winr.dy() > screenr.dy())
			(winr.min.y, winr.max.y) = (screenr.min.y, screenr.max.y);
		if (winr.max.x > screenr.max.x)
			(winr.min.x, winr.max.x) = (screenr.max.x - winr.dx(), screenr.max.x);
		if (winr.max.y > screenr.max.y)
			(winr.min.y, winr.max.y) = (screenr.max.y - winr.dy(), screenr.max.y);
	}
	cmd(win, "pack propagate . 0");
	cmd(win, ". configure " +
			" -x " + string winr.min.x +
			" -y " + string winr.min.y +
			" -width " + string winr.dx() +
			" -height " + string winr.dy());
	needresize = 1;
	updatearena();
	cmd(win, "bind . <Configure> {send cmd config}");
}

setorigin(r: Rect, p: Point): Rect
{
	sz := Point(r.max.x - r.min.x, r.max.y - r.min.y);
	return (p, p.add(sz));
}

resizeall()
{
	needresize = 0;
	if (packobjs == nil)
		return;
	cmd(packwin, "pack propagate . 1");
	cmd(packwin, ". configure -width 0 -height 0");	# make sure propagation works.
	for (sl := packobjs; sl != nil; sl = tl sl) {
		obj := hd sl;
		sizeobj(obj);
		cmd(packwin, obj.w + " configure -width " + string obj.size.x +
			" -height " + string obj.size.y);
	}
	csz := actsize(packwin, ".");
	sz := actsize(win, ".cf");
	if (sz.x > csz.x || sz.y > csz.y) {
		cmd(packwin, "pack propagate . 0");
		if (sz.x > csz.x) {
			cmd(packwin, ". configure -width " + string sz.x);
			cmd(win, ".c xview moveto 0");
			csz.x = sz.x;
		}
		if (sz.y > csz.y) {
			cmd(packwin, ". configure -height " + string sz.y);
			cmd(win, ".c yview moveto 0");
			csz.y = sz.y;
		}
	}
	cmd(win, ".c configure -width " + string csz.x + " -height " + string csz.y
			+ " -scrollregion {0 0 " + p2s(csz) + "}");
	onscreen();
	for (sl = packobjs; sl != nil; sl = tl sl) {
		obj := hd sl;
		r := actrect(packwin, obj.w);
		positionobj(obj, r);
	}
}

# make sure that there aren't any unnecessary blank
# bits in the scroll area.
onscreen()
{
	(n, toks) := sys->tokenize(cmd(win, ".c xview"), " ");
	cmd(win, ".c xview moveto " + hd toks);
	(n, toks) = sys->tokenize(cmd(win, ".c yview"), " ");
	cmd(win, ".c yview moveto " + hd toks);
}

# work out the size of an object to be laid out.
sizeobj(obj: ref Layobject)
{
	pick o := obj {
	Stack =>
		delta := Point(0, 0);
		case o.style {
		styDISPLAY =>
			case o.orientation {
			oRIGHT =>	delta.x = carddelta.x;
			oLEFT =>		delta.x = -carddelta.x;
			oDOWN =>	delta.y = carddelta.y;
			oUP =>		delta.y = -carddelta.y;
			}
		styPILE =>
			;	# no offset
		}
		o.delta = delta;
		r := Rect((0, 0), size(cardrect(o, (0, max(len o.cards, o.maxcards)))));
		if (o.title != nil) {
			p := Point(r.min.x + r.dx() / 2, r.min.y);
			tr := s2r(cmd(win, ".c bbox t" + string o.id));
			tbox := Rect((p.x - tr.dx() / 2, p.y - tr.dy()), (p.x + tr.dx() / 2, p.y));
			r = union(r, tbox);
		}
		o.size = r.max.sub(r.min).add((Border * 2, Border * 2));
#		sys->print("sized stack %d => %s\n", o.id, p2s(o.size));
	Widget =>
		w := ".buts." + string o.id;
		o.size.x = int cmd(win, w + " cget -width");
		o.size.y = int cmd(win, w + " cget -height");
#		sys->print("sized widget %d (%s) => %s\n", o.id,
#			cmd(win, "winfo class " + w + ".b"), p2s(o.size));
	}
}

# set a laid-out object's position on the canvas, given
# its allocated rectangle, r.
positionobj(obj: ref Layobject, r: Rect)
{
	pick o := obj {
	Stack =>
#		sys->print("positioning stack %d, r %s\n", o.id, r2s(r));
		delta := o.delta;
		sz := o.size.sub((Border * 2, Border * 2));
		r.min.x += (r.dx() - sz.x) / 2;
		r.min.y += (r.dy() - sz.y) / 2;
		r.max = r.min.add(sz);
		if (o.title != nil) {
			cmd(win, ".c coords t" +string o.id + " " +
				string (r.min.x + r.dx() / 2) + " " + string r.min.y);
			tr := s2r(cmd(win, ".c bbox t" + string o.id));
			r.min.y = tr.max.y;
			sz = size(cardrect(o, (0, max(len o.cards, o.maxcards))));
			r.min.x += (r.dx() - sz.x) / 2;
			r.min.y += (r.dy() - sz.y) / 2;
			r.max = r.min.add(sz);
		}
		o.pos = r.min;
		if (delta.x < 0)
			o.pos.x = r.max.x - cardsize.x;
		if (delta.y < 0)
			o.pos.y = r.max.y - cardsize.y;
		cmd(win, ".c coords r" + string o.id + " " + r2s(r.inset(-(Border / 2))));
		objneedsrepack(o);
	Widget =>
#		sys->print("positioning widget %d, r %s\n", o.id, r2s(r));
		cmd(win, ".c coords r" + string o.id + " " + p2s(r.min));
		bd := int cmd(win, ".buts." + string o.id + " cget -bd");
		cmd(win, ".c itemconfigure r" + string o.id +
			" -width " + string (r.dx() - bd * 2) +
			" -height " + string (r.dy() - bd * 2));
	}
}

size(r: Rect): Point
{
	return r.max.sub(r.min);
}

transfercards(src: ref Layobject.Stack, r: Range, dst: ref Layobject.Stack, index: int)
{
	cards := extractcards(src, r);
	n := r.end - r.start;
	# if we've just removed some cards from the destination,
	# then adjust the destination index accordingly.
	if (src == dst && index > r.start) {
		if (index < r.end)
			index = r.start;
		else
			index -= n;
	}
	insertcards(dst, cards, index);
}

extractcards(src: ref Layobject.Stack, r: Range): array of ref Object.Card
{
	if (len src.cards > src.maxcards)
		needresize = 1;
	deltag(src.cards[r.start:r.end], "c" + string src.id);
	n := r.end - r.start;
	cards := src.cards[r.start:r.end];
	newcards := array[len src.cards - n] of ref Object.Card;
	newcards[0:] = src.cards[0:r.start];
	newcards[r.start:] = src.cards[r.end:];
	src.cards = newcards;
	objneedsrepack(src);		# XXX not necessary if moving from top?
	return cards;
}

insertcards(dst: ref Layobject.Stack, cards: array of ref Object.Card, index: int)
{
	n := len cards;
	newcards := array[len dst.cards + n] of ref Object.Card;
	newcards[0:] = dst.cards[0:index];
	newcards[index + n:] = dst.cards[index:];
	newcards[index:] = cards;
	dst.cards = newcards;

	for (i := 0; i < len cards; i++)
		cards[i].parentid = dst.id;
	addtag(dst.cards[index:index + n], "c" + string dst.id);
	objneedsrepack(dst);		# XXX not necessary if adding to top?
	if (len dst.cards > dst.maxcards)
		needresize = 1;
}

destroy(obj: ref Object)
{
	if (obj.id >= 0)
		objects[obj.id] = nil;
	id := string obj.id;
	pick o := obj {
	Card =>
		cmd(win, ".c delete i" + id);	# XXX crashed here once...
	Widget =>
		cmd(win, ".c delete r" + id);
		w := ".buts." + id;
		cmd(win, "destroy " + w);
		dellayobject(o.o);
	Stack =>
		completeanim(o.o);
		cmd(win, ".c delete r" + id + " s" + id + " n" + id + " N" + id);
		if (o.o.title != nil)
			cmd(win, ".c delete t" + id);
		cmd(win, ".c delete c" + id);		# any remaining "fake" cards
		needrepack = 1;
		dellayobject(o.o);
	Button =>
		cmd(win, "destroy .buts." + string o.id);
	Member =>
		if (o.cid != -1) {
			# XXX remove member from members hash.
		}
	Layoutobj =>
		if ((l := findlayobject(o.lay.layid)) != nil) {
			# XXX are we sure they're not off-screen anyway?
			cmd(win, ".c move r" + string l.id + " 5000 5000");
			cmd(win, ".c move c" + string l.id + " 5000 5000");
			cmd(win, ".c move N" + string l.id + " 5000 5000");
			cmd(win, ".c move n" + string l.id + " 5000 5000");
			cmd(win, ".c move s" + string l.id + " 5000 5000");
		}
		if (layout == o.lay)
			layout = nil;
	Layoutframe =>
		if (layout == o.lay)
			layout = nil;
	}
}

dellayobject(lay: ref Layobject)
{
	if (lay.layid == -1)
		return;
	x := lay.layid % len layobjects;
	nl: list of ref Layobject;
	for (ll := layobjects[x]; ll != nil; ll = tl ll)
		if ((hd ll).layid != lay.layid)
			nl = hd ll :: nl;
	layobjects[x] = nl;
}

findlayobject(layid: int): ref Layobject
{
	if (layid == -1)
		return nil;
	for (ll := layobjects[layid % len layobjects]; ll != nil; ll = tl ll)
		if ((hd ll).layid == layid)
			return hd ll;
	return nil;
}

deltag(cards: array of ref Object.Card, tag: string)
{
	for (i := 0; i < len cards; i++)
		cmd(win, ".c dtag i" + string cards[i].id + " " + tag);
}

addtag(cards: array of ref Object.Card, tag: string)
{
	for (i := 0; i < len cards; i++)
		cmd(win, ".c addtag " + tag + " withtag i" + string cards[i].id);
}

join(v: list of string): string
{
	if (v == nil)
		return nil;
	s := hd v;
	for (v = tl v; v != nil; v = tl v)
		s += " " + hd v;
	return s;
}

notify(s: string)
{
	notifych <-= s;
}

notifierproc()
{
	notifypid := -1;
	sync := chan of int;
	for (;;) {
		s := <-notifych;
		kill(notifypid);
		spawn notifyproc(s, sync);
		notifypid = <-sync;
	}
}

notifyproc(s: string, sync: chan of int)
{
	sync <-= sys->pctl(0, nil);
	cmd(win, ".c delete notify");
	id := cmd(win, ".c create text " + p2s(visibleorigin()) + " -anchor nw -fill red -tags notify -text '" + s);
	bbox := cmd(win, ".c bbox " + id);
	cmd(win, ".c create rectangle " + bbox + " -fill #ffffaa -tags notify");
	cmd(win, ".c raise " + id);
	cmd(win, "update");
	sys->sleep(1500);
	cmd(win, ".c delete notify");
	cmd(win, "update");
}

# move canvas so that canvas point canvp lies under
# screen point scrp.
pan(canvp, scrp: Point)
{
	o := Point(int cmd(win, ".c cget -actx"), int cmd(win, ".c cget -acty"));
	co := canvp.sub(scrp.sub(o));
	sz := Point(int cmd(win, ".c cget -width"), int cmd(win, ".c cget -height"));

	cmd(win, ".c xview moveto " + string (real co.x / real sz.x));
	cmd(win, ".c yview moveto " + string (real co.y / real sz.y));
}

# return the top left point that's currently visible
# in the canvas, taking into account scrolling.
visibleorigin(): Point
{
	(scrx, scry) := (cmd(win, ".c cget -actx"), cmd(win, ".c cget -acty"));
	return Point (int cmd(win, ".c canvasx " + scrx),
		int cmd(win, ".c canvasy " + scry));
}

s2r(s: string): Rect
{
	r: Rect;
	(n, toks) := sys->tokenize(s, " ");
	if (n < 4)
		panic("malformed rectangle " + s);
	(r.min.x, toks) = (int hd toks, tl toks);
	(r.min.y, toks) = (int hd toks, tl toks);
	(r.max.x, toks) = (int hd toks, tl toks);
	(r.max.y, toks) = (int hd toks, tl toks);
	return r;
}

r2s(r: Rect): string
{
	return sys->sprint("%d %d %d %d", r.min.x, r.min.y, r.max.x, r.max.y);
}

p2s(p: Point): string
{
	return string p.x + " " + string p.y;
}

union(r1, r2: Rect): Rect
{
	if (r1.min.x > r2.min.x)
		r1.min.x = r2.min.x;
	if (r1.min.y > r2.min.y)
		r1.min.y = r2.min.y;

	if (r1.max.x < r2.max.x)
		r1.max.x = r2.max.x;
	if (r1.max.y < r2.max.y)
		r1.max.y = r2.max.y;
	return r1;
}
 
kill(pid: int)
{
	if ((fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE)) != nil)
		sys->write(fd, array of byte "kill", 4);
}

lockproc()
{
	for (;;) {
		<-cardlockch;
		cardlockch <-=1;
	}
}

lock()
{
	cardlockch <-= 1;
}

unlock()
{
	<-cardlockch;
}

openimage(file: string, id: string): Point
{
	if (tk->cmd(win, "image create bitmap " + id + " -file " + file)[0] == '!')
		return (0, 0);
	return (int tk->cmd(win, "image width " + id),
				int tk->cmd(win, "image height " + id));
}

# read images into tk.
readimages(dir: string, prefix: string): (int, Point)
{
	displ := drawctxt.display;
	if (cardsize.x > 0 && cardsize.y > 0 &&
			(img := displ.open(dir + "/" + prefix + ".all.bit")) != nil) {
		if (img.r.dx() % cardsize.x != 0 || img.r.dy() != cardsize.y)
			sys->fprint(stderr, "cards: inconsistent complete image, ignoring\n");
		else {
			n := img.r.dx() / cardsize.x;
			x := img.r.min.x;
			sys->print("found %d cards in complete image\n", n);
			for (i := 0; i < n; i++) {
				c := displ.newimage(((0, 0), cardsize), img.chans, 0, 0);
				c.draw(c.r, img, nil, (x, 0));
				id := prefix + string i;
				cmd(win, "image create bitmap " + id);
				tk->putimage(win, id, c, nil);
				x += cardsize.x;
			}
			return (n, cardsize);
		}
	}
				
	size := openimage("@" + dir + "/" + prefix + "0.bit", prefix + "0");
	if (size.x == 0) {
		sys->print("no first image (filename: '%s')\n", dir + "/" + prefix + "0.bit");
		return (0, (0, 0));
	}
	i := 1;
	for (;;) {
		nsize := openimage("@" + dir + "/" + prefix + string i + ".bit", prefix + string i);
		if (nsize.x == 0)
			break;
		if (!nsize.eq(size))
			sys->fprint(stderr, "warning: inconsistent image size in %s/%s%d.bit, " +
				"[%d %d] vs [%d %d]\n", dir, prefix, i, size.x, size.y, nsize.x, nsize.y);
		i++;
	}
	return (i, size);
}

newanimation(src: ref Layobject.Stack, r: Range): ref Animation
{
	a := ref Animation;
	a.srcpt = src.pos.add(src.delta.mul(r.start));
	cards := extractcards(src, r);
	a.cards = cards;
	a.waitch = chan of ref Animation;
	return a;
}

startanimating(a: ref Animation, dst: ref Layobject.Stack, index: int)
{
	q := dst.animq;
	if (q.isempty())
		spawn animqueueproc(a.waitch);

	a.tag = "a" + string animid++;
	addtag(a.cards, a.tag);
	q.put(a);
	a.dstid = dst.id;
	a.index = index;
	spawn animproc(a);
}

SPEED: con 1.5;			# animation speed in pixels/millisec

animproc(a: ref Animation)
{
	tick := chan of int;
	dst := stackobj(a.dstid);
	if (dst == nil)
		panic("animation destination has gone!");
	dstpt := dst.pos.add(dst.delta.mul(a.index));
	srcpt := a.srcpt;
	d := dstpt.sub(srcpt);
	# don't bother animating if moving to or from a hidden stack.
	if (!srcpt.eq(Hiddenpos) && !dst.pos.eq(Hiddenpos) && !d.eq((0, 0))) {
		mag := math->sqrt(real(d.x * d.x + d.y * d.y));
		(vx, vy) := (real d.x / mag, real d.y / mag);
		currpt := a.srcpt;		# current position of cards
		t0 := starttime;
		dt := int (mag / SPEED);
		t := 0;
		tickregister(tick);
		cmd(win, ".c raise " + a.tag);
		while (t < dt) {
			s := real t * SPEED;
			p := Point(srcpt.x + int (s * vx), srcpt.y + int (s * vy));
			dp := p.sub(currpt);
			cmd(win, ".c move " + a.tag + " " + string dp.x + " " + string dp.y);
			currpt = p;
			t = <-tick - t0;
		}
		tickunregister(tick);
		cmd(win, "update");
	}
	a.waitch <-= a;
}

tickregister(tick: chan of int)
{
	tickregisterch <-= tick;
}

tickunregister(tick: chan of int)
{
	tickunregisterch <-= tick;
}

tickproc(tick: chan of int)
{
	for (;;)
		tick <-= 1;
}

timeproc()
{
	reg: list of chan of int;
	dummytick := chan of int;
	realtick := chan of int;
	tick := dummytick;
	spawn tickproc(realtick);
	for (;;) {
		alt {
		c := <-tickregisterch =>
			if (reg == nil)
				tick = realtick;
			reg = c :: reg;
		c := <-tickunregisterch =>
			r: list of chan of int;
			for (; reg != nil; reg = tl reg)
				if (hd reg != c)
					r = hd reg :: r;
			reg = r;
			if (reg == nil)
				tick = dummytick;
		<-tick =>
			t := sys->millisec();
			for (r := reg; r != nil; r = tl r) {
				alt {
				hd r <-= t =>
					;
				* =>
					;
				}
			}
			cmd(win, "update");
		}
	}
}

yield()
{
	yieldch <-= 1;
}

yieldproc()
{
	for (;;)
		<-yieldch;
}


# send completed animations down animfinishedch;
# wait for a reply, which is either a new animation to wait
# for (the next in the queue) or nil, telling us to exit
animqueueproc(waitch: chan of ref Animation)
{
	rc := chan of chan of ref Animation;
	while (waitch != nil) {
		animfinishedch <-= (<-waitch, rc);
		waitch = <-rc;
	}
}

# an animation has finished.
# move the cards into their final place in the stack,
# remove the animation from the queue it's on,
# and inform the mediating process of the next animation process in the queue.
animterminated(v: (ref Animation, chan of chan of ref Animation))
{
	(a, rc) := v;
	deltag(a.cards, a.tag);
	dst := stackobj(a.dstid);
	insertcards(dst, a.cards, a.index);
	repackobj(dst);
	cmd(win, "update");
	q := dst.animq;
	q.get();
	if (q.isempty())
		rc <-= nil;
	else {
		a = q.peek();
		rc <-= a.waitch;
	}
}

actrect(win: ref Tk->Toplevel, w: string): Rect
{
	r: Rect;
	r.min.x = int cmd(win, w + " cget -actx") + int cmd(win, w + " cget -bd");
	r.min.y = int cmd(win, w + " cget -acty") + int cmd(win, w + " cget -bd");
	r.max.x = r.min.x + int cmd(win, w + " cget -actwidth");
	r.max.y = r.min.y + int cmd(win, w + " cget -actheight");
	return r;
}

actsize(win: ref Tk->Toplevel, w: string): Point
{
	return (int cmd(win, w + " cget -actwidth"), int cmd(win, w + " cget -actheight"));
}

Queue.put(q: self ref Queue, s: T)
{
	q.t = s :: q.t;
}

Queue.get(q: self ref Queue): T
{
	s: T;
	if(q.h == nil){
		q.h = revlist(q.t);
		q.t = nil;
	}
	if(q.h != nil){
		s = hd q.h;
		q.h = tl q.h;
	}
	return s;
}

Queue.peek(q: self ref Queue): T
{
	s: T;
	if (q.isempty())
		return s;
	s = q.get();
	q.h = s :: q.h;
	return s;
}

Queue.isempty(q: self ref Queue): int
{
	return q.h == nil && q.t == nil;
}

revlist(ls: list of T) : list of T
{
	rs: list of T;
	for (; ls != nil; ls = tl ls)
		rs = hd ls :: rs;
	return rs;
}

readconfig(): int
{
	for (lines := readconfigfile("/icons/cards/config"); lines != nil; lines = tl lines) {
		t := hd lines;
		case hd t {
		"rearborder" =>
			Rearborder = int hd tl t;
		"border" =>
			Border = int hd tl t;
		"selectborder" =>
			Selectborder = int hd tl t;
		"xdelta" =>
			carddelta.x = int hd tl t;
		"ydelta" =>
			carddelta.y = int hd tl t;
		"font" =>
			Textfont = hd tl t;
		"selectcolour" =>
			Selectcolour = hd tl t;
		"cardsize" =>
			if (len t != 3)
				sys->fprint(stderr, "cards: invalid value for cardsize attribute\n");
			else
				cardsize = (int hd tl t, int hd tl tl t);
		* =>
			sys->fprint(stderr, "cards: unknown config attribute: %s\n", hd t);
		}
	}
	return 0;
}

readcardimages(): int
{
	(nimages, cardsize) = readimages("/icons/cards", "c");
 	if (nimages == 0) {
		sys->fprint(stderr, "cards: no card images found\n");
		return -1;
	}
	sys->print("%d card images found\n", nimages);

	(nrears, rearsize) := readimages("/icons/cardrears", "rear");
	if (nrears > 0 && !rearsize.eq(cardsize)) {
		sys->fprint(stderr, "cards: card rear sizes don't match card sizes (%s vs %s)\n", p2s(rearsize), p2s(cardsize));
		return -1;
	}
	sys->print("%d card rear images found\n", nrears);
	cr := Rect((0, 0), cardsize);
	for (i := nrears; i < len rearcolours; i++) {
		cmd(win, "image create bitmap rear" + string i);
		img := drawctxt.display.newimage(cr, Draw->XRGB32, 0, Draw->Black);
		img.draw(cr.inset(Rearborder),
			drawctxt.display.color(rearcolours[i] - nrears), nil, (0, 0));
		tk->putimage(win, "rear" + string i, img, nil);
	}
	return 0;
}

readconfigfile(f: string): list of list of string
{
	sys->print("opening config file '%s'\n", f);
	fd := sys->open(f, Sys->OREAD);
	if (fd == nil)
		return nil;
	buf := array[Sys->ATOMICIO] of byte;
	nb := sys->read(fd, buf, len buf);
	if (nb <= 0)
		return nil;
	(nil, lines) := sys->tokenize(string buf[0:nb], "\r\n");
	r: list of list of string;
	for (; lines != nil; lines = tl lines) {
		(n, toks) := sys->tokenize(hd lines, " \t");
		if (n == 0)
			continue;
		if (n < 2)
			sys->fprint(stderr, "cards: invalid config line: %s\n", hd lines);
		else
			r = toks :: r;
	}
	return r;
}

fittoscreen(win: ref Tk->Toplevel)
{
	Point: import draw;
	if (win.image == nil || win.image.screen == nil)
		return;
	r := win.image.screen.image.r;
	scrsize := Point((r.max.x - r.min.x), (r.max.y - r.min.y));
	bd := int cmd(win, ". cget -bd");
	winsize := Point(int cmd(win, ". cget -actwidth") + bd * 2, int cmd(win, ". cget -actheight") + bd * 2);
	if (winsize.x > scrsize.x)
		cmd(win, ". configure -width " + string (scrsize.x - bd * 2));
	if (winsize.y > scrsize.y)
		cmd(win, ". configure -height " + string (scrsize.y - bd * 2));
	actr: Rect;
	actr.min = Point(int cmd(win, ". cget -actx"), int cmd(win, ". cget -acty"));
	actr.max = actr.min.add((int cmd(win, ". cget -actwidth") + bd*2,
				int cmd(win, ". cget -actheight") + bd*2));
	(dx, dy) := (actr.dx(), actr.dy());
	if (actr.max.x > r.max.x)
		(actr.min.x, actr.max.x) = (r.min.x - dx, r.max.x - dx);
	if (actr.max.y > r.max.y)
		(actr.min.y, actr.max.y) = (r.min.y - dy, r.max.y - dy);
	if (actr.min.x < r.min.x)
		(actr.min.x, actr.max.x) = (r.min.x, r.min.x + dx);
	if (actr.min.y < r.min.y)
		(actr.min.y, actr.max.y) = (r.min.y, r.min.y + dy);
	cmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y);
}

panic(s: string)
{
	sys->fprint(stderr, "cards: panic: %s\n", s);
	raise "panic";
}

showtk := 0;
cmd(top: ref Tk->Toplevel, s: string): string
{
	if (showtk)
		sys->print("tk: %s\n", s);
	e := tk->cmd(top, s);
	if (e != nil && e[0] == '!') {
		sys->fprint(stderr, "tk error %s on '%s'\n", e, s);
		raise "panic";
	}
	return e;
}

max(a, b: int): int
{
	if (a > b)
		return a;
	return b;
}