shithub: purgatorio

ref: c116550e6a41572796e4db65e4f6acbcb3d9d6f8
dir: /appl/charon/layout.b/

View raw version
implement Layout;

include "common.m";
include "keyboard.m";

sys: Sys;
CU: CharonUtils;
	ByteSource, MaskedImage, CImage, ImageCache, max, min,
	White, Black, Grey, DarkGrey, LightGrey, Blue, Navy, Red, Green, DarkRed: import CU;

D: Draw;
	Point, Rect, Font, Image, Display: import D;
S: String;
T: StringIntTab;
U: Url;
	Parsedurl: import U;
I: Img;
	ImageSource: import I;
J: Script;
E: Events;
	Event: import E;
G: Gui;
	Popup: import G;
B: Build;

# B : Build, declared in layout.m so main program can use it
	Item, ItemSource,
	IFbrk, IFbrksp, IFnobrk, IFcleft, IFcright, IFwrap, IFhang,
	IFrjust, IFcjust, IFsmap, IFindentshift, IFindentmask,
	IFhangmask,
	Voffbias,
	ISPnull, ISPvline, ISPhspace, ISPgeneral,
	Align, Dimen, Formfield, Option, Form,
	Table, Tablecol, Tablerow, Tablecell,
	Anchor, DestAnchor, Map, Area, Kidinfo, Docinfo,
	Anone, Aleft, Acenter, Aright, Ajustify, Achar, Atop, Amiddle,
	Abottom, Abaseline,
	Dnone, Dpixels, Dpercent, Drelative,
	Ftext, Fpassword, Fcheckbox, Fradio, Fsubmit, Fhidden, Fimage,
	Freset, Ffile, Fbutton, Fselect, Ftextarea,
	Background,
	FntR, FntI, FntB, FntT, NumStyle,
	Tiny, Small, Normal, Large, Verylarge, NumSize, NumFnt, DefFnt,
	ULnone, ULunder, ULmid,
	FRnoresize, FRnoscroll, FRhscroll, FRvscroll,
	FRhscrollauto, FRvscrollauto
    : import B;

# font stuff
Fontinfo : adt {
	name:	string;
	f:	ref Font;
	spw:	int;			# width of a space in this font
};

fonts := array[NumFnt] of {
	FntR*NumSize+Tiny => Fontinfo("/fonts/charon/plain.tiny.font", nil, 0),
	FntR*NumSize+Small => ("/fonts/charon/plain.small.font", nil, 0),
	FntR*NumSize+Normal => ("/fonts/charon/plain.normal.font", nil, 0),
	FntR*NumSize+Large => ("/fonts/charon/plain.large.font", nil, 0),
	FntR*NumSize+Verylarge => ("/fonts/charon/plain.vlarge.font", nil, 0),
	
	FntI*NumSize+Tiny => ("/fonts/charon/italic.tiny.font", nil, 0),
	FntI*NumSize+Small => ("/fonts/charon/italic.small.font", nil, 0),
	FntI*NumSize+Normal => ("/fonts/charon/italic.normal.font", nil, 0),
	FntI*NumSize+Large => ("/fonts/charon/italic.large.font", nil, 0),
	FntI*NumSize+Verylarge => ("/fonts/charon/italic.vlarge.font", nil, 0),
	
	FntB*NumSize+Tiny => ("/fonts/charon/bold.tiny.font", nil, 0),
	FntB*NumSize+Small => ("/fonts/charon/bold.small.font", nil, 0),
	FntB*NumSize+Normal => ("/fonts/charon/bold.normal.font", nil, 0),
	FntB*NumSize+Large => ("/fonts/charon/bold.large.font", nil, 0),
	FntB*NumSize+Verylarge => ("/fonts/charon/bold.vlarge.font", nil, 0),
	
	FntT*NumSize+Tiny => ("/fonts/charon/cw.tiny.font", nil, 0),
	FntT*NumSize+Small => ("/fonts/charon/cw.small.font", nil, 0),
	FntT*NumSize+Normal => ("/fonts/charon/cw.normal.font", nil, 0),
	FntT*NumSize+Large => ("/fonts/charon/cw.large.font", nil, 0),
	FntT*NumSize+Verylarge => ("/fonts/charon/cw.vlarge.font", nil, 0)
};

# Seems better to use a slightly smaller font in Controls, to match other browsers
CtlFnt: con (FntR*NumSize+Small);

# color stuff.  have hash table mapping RGB values to D->Image for that color
Colornode : adt {
	rgb:	int;
	im:	ref Image;
	next:	ref Colornode;
};

# Source of info for page (html, image, etc.)
Source: adt {
	bs:	ref ByteSource;
	redirects:	int;
	pick {
		Srequired or
		Shtml =>
			itsrc: ref ItemSource;
		Simage =>
			ci: ref CImage;
			itl: list of ref Item;
			imsrc: ref ImageSource;
	}
};

Sources: adt {
	main: ref Source;
	reqd: ref Source;
	srcs: list of ref Source;

	new: fn(m : ref Source) : ref Sources;
	add: fn(srcs: self ref Sources, s: ref Source, required: int);
	done: fn(srcs: self ref Sources, s: ref Source);
	waitsrc: fn(srcs : self ref Sources) : ref Source;
};

NCOLHASH : con 19;	# 19 checked for standard colors: only 1 collision
colorhashtab := array[NCOLHASH] of ref Colornode;

# No line break should happen between adjacent characters if
# they are 'wordchars' : set in this array, or outside the array range.
# We include certain punctuation characters that are not traditionally
# regarded as 'word' characters.
wordchar := array[16rA0] of {
	'!' => byte 1, 
	'0'=>byte 1, '1'=>byte 1, '2'=>byte 1, '3'=>byte 1, '4'=>byte 1,
	'5'=>byte 1, '6'=>byte 1, '7'=>byte 1, '8'=>byte 1, '9'=>byte 1,
	':'=>byte 1, ';' => byte 1,
	'?' => byte 1,
	'A'=>byte 1, 'B'=>byte 1, 'C'=>byte 1, 'D'=>byte 1, 'E'=>byte 1, 'F'=>byte 1,
	'G'=>byte 1, 'H'=>byte 1, 'I'=>byte 1, 'J'=>byte 1, 'K'=>byte 1, 'L'=>byte 1,
	'M'=>byte 1, 'N'=>byte 1, 'O'=>byte 1, 'P'=>byte 1, 'Q'=>byte 1, 'R'=>byte 1,
	'S'=>byte 1, 'T'=>byte 1, 'U'=>byte 1, 'V'=>byte 1, 'W'=>byte 1, 'X'=>byte 1,
	'Y'=>byte 1, 'Z'=>byte 1,
	'a'=>byte 1, 'b'=>byte 1, 'c'=>byte 1, 'd'=>byte 1, 'e'=>byte 1, 'f'=>byte 1,
	'g'=>byte 1, 'h'=>byte 1, 'i'=>byte 1, 'j'=>byte 1, 'k'=>byte 1, 'l'=>byte 1,
	'm'=>byte 1, 'n'=>byte 1, 'o'=>byte 1, 'p'=>byte 1, 'q'=>byte 1, 'r'=>byte 1,
	's'=>byte 1, 't'=>byte 1, 'u'=>byte 1, 'v'=>byte 1, 'w'=>byte 1, 'x'=>byte 1,
	'y'=>byte 1, 'z'=>byte 1,
	'_'=>byte 1,
	'\''=>byte 1, '"'=>byte 1, '.'=>byte 1, ','=>byte 1, '('=>byte 1, ')'=>byte 1,
	* => byte 0
};

TABPIX: con 30;		# number of pixels in a tab
CAPSEP: con 5;			# number of pixels separating tab from caption
SCRBREADTH: con 14;	# scrollbar breadth (normal)
SCRFBREADTH: con 14;	# scrollbar breadth (inside child frame or select control)
FRMARGIN: con 0;		# default margin around frames
RULESP: con 7;			# extra space before and after rules
POPUPLINES: con 12;	# number of lines in popup select list
MINSCR: con 6;			# min size in pixels of scrollbar drag widget
SCRDELTASF: con 10000;	# fixed-point scale factor for scrollbar per-pixel step

# all of the following include room for relief
CBOXWID: con 14;		# check box width
CBOXHT: con 12;		# check box height
ENTVMARGIN : con 4;	# vertical margin inside entry box
ENTHMARGIN : con 6;	# horizontal margin inside entry box
SELMARGIN : con 4;		# margin inside select control
BUTMARGIN: con 4;		# margin inside button control
PBOXWID: con 10;		# progress box width
PBOXHT: con 16;		# progress box height
PBOXBD: con 2;		# progress box border width

TABLEMAXTARGET: con 2000;	# targetwidth to get max width of table cell
TABLEFLOATTARGET: con 1;	# targetwidth for floating tables

SELBG: con 16r00FFFF;	# aqua

ARPAUSE : con 500;			# autorepeat initial delay (ms)
ARTICK : con 100;			# autorepeat tick delay (ms)

display: ref D->Display;

dbg := 0;
dbgtab := 0;
dbgev := 0;
linespace := 0;
lineascent := 0;
charspace := 0;
spspace := 0;
ctllinespace := 0;
ctllineascent := 0;
ctlcharspace := 0;
ctlspspace := 0;
frameid := 0;
zp := Point(0,0);

init(cu: CharonUtils)
{
	CU = cu;
	sys = load Sys Sys->PATH;
	D = load Draw Draw->PATH;
	S = load String String->PATH;
	T = load StringIntTab StringIntTab->PATH;
	U = load Url Url->PATH;
	if (U != nil)
		U->init();
	E = cu->E;
	G = cu->G;
	I = cu->I;
	J = cu->J;
	B = cu->B;
	display = G->display;

	# make sure default and control fonts are loaded
	getfont(DefFnt);
	fnt := fonts[DefFnt].f;
	linespace = fnt.height;
	lineascent = fnt.ascent;
	charspace = fnt.width("a");	# a kind of average char width
	spspace = fonts[DefFnt].spw;
	getfont(CtlFnt);
	fnt = fonts[CtlFnt].f;
	ctllinespace = fnt.height;
	ctllineascent = fnt.ascent;
	ctlcharspace = fnt.width("a");
	ctlspspace = fonts[CtlFnt].spw;
}

stringwidth(s: string): int
{
	return fonts[DefFnt].f.width(s)/charspace;
}

# Use bsmain to fill frame f.
# Return buffer containing source when done.
layout(f: ref Frame, bsmain: ref ByteSource, linkclick: int) : array of byte
{
	dbg = int (CU->config).dbg['l'];
	dbgtab = int (CU->config).dbg['t'];
	dbgev = int (CU->config).dbg['e'];
	if(dbgev)
		CU->event("LAYOUT", 0);
	sources : ref Sources;
	hdr := bsmain.hdr;
	auth := "";
	url : ref Parsedurl;
	if (bsmain.req != nil) {
		auth = bsmain.req.auth;
		url = bsmain.req.url;
	}
#	auth := bsmain.req.auth;
	ans : array of byte = nil;
	di := Docinfo.new();
	if(linkclick && f.doc != nil)
		di.referrer = f.doc.src;
	f.reset();
	f.doc = di;
	di.frameid = f.id;
	di.src = hdr.actual;
	di.base = hdr.base;
	di.refresh = hdr.refresh;
	if (hdr.chset != nil)
		di.chset = hdr.chset;
	di.lastModified = hdr.lastModified;
	if(J != nil)
		J->havenewdoc(f);
	oclipr := f.cim.clipr;
	f.cim.clipr = f.cr;
	if(f.framebd != 0) {
		f.cr = f.r.inset(2);
		drawborder(f.cim, f.cr, 2, DarkGrey);
	}
	fillbg(f, f.cr);
	G->flush(f.cr);
	f.cim.clipr = oclipr;
	if(f.flags&FRvscroll)
		createvscroll(f);
	if(f.flags&FRhscroll)
		createhscroll(f);
	l := Lay.new(f.cr.dx(), Aleft, f.marginw, di.background);
	f.layout = l;
	anyanim := 0;
	if(hdr.mtype == CU->TextHtml || hdr.mtype == CU->TextPlain) {
		itsrc := ItemSource.new(bsmain, f, hdr.mtype);
		sources = Sources.new(ref Source.Shtml(bsmain, 0, itsrc));
	}
	else {
		# for now, must be supported image type
		if(!I->supported(hdr.mtype)) {
			sys->print("Need to implement something: source isn't supported image type\n");
			return nil;
		}
		imsrc := I->ImageSource.new(bsmain, 0, 0);
		ci := CImage.new(url, nil, 0, 0);
		simage := ref Source.Simage(bsmain, 0, ci, nil, imsrc);
		sources = Sources.new(simage);
		it := ref Item.Iimage(nil, 0, 0, 0, 0, 0, nil, len di.images, ci, 0, 0, "", nil, nil, -1, Abottom, byte 0, byte 0, byte 0);
		di.images = it :: nil;
		appenditems(f, l, it);
		simage.itl = it :: nil;
	}
	while ((src := sources.waitsrc()) != nil) {
		if(dbgev)
			CU->event("LAYOUT GETSOMETHING", 0);
		bs := src.bs;
		freeit := 0;
		if(bs.err != "") {
			if(dbg)
				sys->print("error getting %s: %s\n", bs.req.url.tostring(), bs.err);
			pick s := src {
			Srequired =>
				s.itsrc.reqddata = array [0] of byte;
				sources.done(src);
				CU->freebs(bs);
				src.bs = nil;
				continue;
			}
			freeit = 1;
		}
		else {
			if(bs.hdr != nil && !bs.seenhdr) {
				(use, error, challenge, newurl) := CU->hdraction(bs, 0, src.redirects);
				if(challenge != nil) {
					sys->print("Need to implement authorization credential dialog\n");
					error = "Need authorization";
					use = 0;
				}
				if(error != "" && dbg)
					sys->print("subordinate error: %s\n", error);
				if(newurl != nil) {
					s := ref *src;
					freeit = 1;
					pick ps := src {
					Shtml or Srequired =>
						sys->print("unexpected redirect of subord\n");
					Simage =>
						newci := CImage.new(newurl, nil, ps.ci.width, ps.ci.height);
						for(itl := ps.itl; itl != nil ; itl = tl itl) {
							pick imi := hd itl {
							Iimage =>
								imi.ci = newci;
							}
						}
						news := ref Source.Simage(nil, 0, newci, ps.itl, nil);
						sources.add(news, 0);
						startimreq(news, auth);
					}
				}
				if(!use)
					freeit = 1;
			}
			if(!freeit) {
				pick s := src {
				Srequired or
				Shtml =>
					if (tagof src == tagof Source.Srequired) {
						s.itsrc.reqddata = bs.data;
						sources.done(src);
						CU->freebs(bs);
						src.bs = nil;
						continue;
#						src = sources.main;
#						CU->assert(src != nil);
					}
					itl := s.itsrc.getitems();
					if(di.kidinfo != nil) {
						if(s.itsrc.kidstk == nil) {
							layframeset(f, di.kidinfo);
							G->flush(f.r);
							freeit = 1;
						}
					}
					else {
						l.background = di.background;
						anyanim |= addsubords(sources, di, auth);
						if(itl != nil) {
							appenditems(f, l, itl);
							fixframegeom(f);
							if(dbgev)
								CU->event("LAYOUT_DRAWALL", 0);
							f.dirty(f.totalr);
							drawall(f);
						}
					}
					if (s.itsrc.reqdurl != nil) {
						news := ref Source.Srequired(nil, 0, s.itsrc);
						sources.add(news, 1);
						rbs := CU->startreq(ref CU->ReqInfo(s.itsrc.reqdurl, CU->HGet, nil, "", ""));
						news.bs = rbs;
					} else {
						if (bs.eof && bs.lim == bs.edata && s.itsrc.toks == nil)
							freeit = 1;
					}
				Simage =>
					(ret, mim) := s.imsrc.getmim();
					# mark it done even if error
					s.ci.complete = ret;
					if(ret == I->Mimerror) {
						bs.err = s.imsrc.err;
						freeit = 1;
					}
					else if(ret != I->Mimnone) {
						if(s.ci.mims == nil) {
							s.ci.mims = array[1] of { mim };
							s.ci.width = s.imsrc.width;
							s.ci.height = s.imsrc.height;
							if(ret == I->Mimdone && (CU->config).imagelvl <= CU->ImgNoAnim)
								freeit = 1;
						}
						else {
							n := len s.ci.mims;
							if(mim != s.ci.mims[n-1]) {
								newmims := array[n + 1] of ref MaskedImage;
								newmims[0:] = s.ci.mims;
								newmims[n] = mim;
								s.ci.mims = newmims;
								anyanim = 1;
							}
						}
						if(s.ci.mims[0] == mim)
							haveimage(f, s.ci, s.itl);
						if(bs.eof && bs.lim == bs.edata)
							(CU->imcache).add(s.ci);
					}
					if(!freeit && bs.eof && bs.lim == bs.edata)
						freeit = 1;
				}
			}
		}
		if(freeit) {
			if(bs == bsmain)
				ans = bs.data[0:bs.edata];
			CU->freebs(bs);
			src.bs = nil;
			sources.done(src);
		}
	}
	if(anyanim && (CU->config).imagelvl > CU->ImgNoAnim)
		spawn animproc(f);
	if(dbgev)
		CU->event("LAYOUT_END", 0);
	return ans;
}

# return value is 1 if found any existing images needed animation
addsubords(sources: ref Sources, di: ref Docinfo, auth: string) : int
{
	anyanim := 0;
	if((CU->config).imagelvl == CU->ImgNone)
		return anyanim;
	newsims: list of ref Source.Simage = nil;
	for(il := di.images; il != nil; il = tl il) {
		it := hd il;
		pick i := it {
		Iimage =>
			if(i.ci.mims == nil) {
				cachedci := (CU->imcache).look(i.ci);
				if(cachedci != nil) {
					i.ci = cachedci;
					if(i.imwidth == 0)
						i.imwidth = i.ci.width;
					if(i.imheight == 0)
						i.imheight = i.ci.height;
					anyanim |= (len cachedci.mims > 1);
				}
				else {
				    sloop:
					for(sl := sources.srcs; sl != nil; sl = tl sl) {
						pick s := hd sl {
						Simage =>
							if(s.ci.match(i.ci)) {
								s.itl = it :: s.itl;
								# want all items on list to share same ci;
								# want most-specific dimension specs
								iciw := i.ci.width;
								icih := i.ci.height;
								i.ci = s.ci;
								if(s.ci.width == 0 && s.ci.height == 0) {
									s.ci.width = iciw;
									s.ci.height = icih;
								}
								break sloop;
							}
						}
					}
					if(sl == nil) {
						# didn't find existing Source for this image
						s := ref Source.Simage(nil, 0, i.ci, it:: nil, nil);
						newsims = s :: newsims;
						sources.add(s, 0);
					}
				}
			}
		}
	}
	# Start requests for new newsources.
	# di.images are in last-in-document-first order,
	# so newsources is in first-in-document-first order (good order to load in).
	for(sl := newsims; sl != nil; sl = tl sl)
		startimreq(hd sl, auth);
	return anyanim;
}

startimreq(s: ref Source.Simage, auth: string)
{
	if(dbgev)
		CU->event(sys->sprint("LAYOUT STARTREQ %s", s.ci.src.tostring()), 0);
	bs := CU->startreq(ref CU->ReqInfo(s.ci.src, CU->HGet, nil, auth, ""));
	s.bs = bs;
	s.imsrc = I->ImageSource.new(bs, s.ci.width, s.ci.height);
}

createvscroll(f: ref Frame)
{
	breadth := SCRBREADTH;
	if(f.parent != nil)
		breadth = SCRFBREADTH;
	length := f.cr.dy();
	if(f.flags&FRhscroll)
		length -= breadth;
	f.vscr = Control.newscroll(f, 1, length, breadth);
	f.vscr.r = f.vscr.r.addpt(Point(f.cr.max.x-breadth, f.cr.min.y));
	f.cr.max.x -= breadth;
	if(f.cr.dx() <= 2*f.marginw)
		raise "EXInternal: frame too small for layout";
	f.vscr.draw(1);
}

createhscroll(f: ref Frame)
{
	breadth := SCRBREADTH;
	if(f.parent != nil)
		breadth = SCRFBREADTH;
	length := f.cr.dx();
	x := f.cr.min.x;
	f.hscr = Control.newscroll(f, 0, length, breadth);
	f.hscr.r = f.hscr.r.addpt(Point(x,f.cr.max.y-breadth));
	f.cr.max.y -= breadth;
	if(f.cr.dy() <= 2*f.marginh)
		raise "EXInternal: frame too small for layout";
	f.hscr.draw(1);
}

# Call after a change to f.layout or f.viewr.min to fix totalr and viewr
# (We are to leave viewr.min unchanged, if possible, as
# user might be scrolling).
fixframegeom(f: ref Frame)
{
	l := f.layout;
	if(dbg)
		sys->print("fixframegeom, layout width=%d, height=%d\n", l.width, l.height);
	crwidth := f.cr.dx();
	crheight := f.cr.dy();
	layw := max(l.width, crwidth);
	layh := max(l.height, crheight);
	f.totalr.max = Point(layw, layh);
	crchanged := 0;
	n := l.height+l.margin-crheight;
	if(n > 0 && f.vscr == nil && (f.flags&FRvscrollauto)) {
		createvscroll(f);
		crchanged = 1;
		crwidth = f.cr.dx();
	}
	if(f.viewr.min.y > n)
		f.viewr.min.y = max(0, n);
	n = l.width+l.margin-crwidth;
	if(!crchanged && n > 0 && f.hscr == nil && (f.flags&FRhscrollauto)) {
		createhscroll(f);
		crchanged = 1;
		crheight = f.cr.dy();
	}
	if(crchanged) {
		relayout(f, l, crwidth, l.just);
		fixframegeom(f);
		return;
	}
	if(f.viewr.min.x > n)
		f.viewr.min.x = max(0, n);
	f.viewr.max.x = min(f.viewr.min.x+crwidth, layw);
	f.viewr.max.y = min(f.viewr.min.y+crheight, layh);
	if(f.vscr != nil)
		f.vscr.scrollset(f.viewr.min.y, f.viewr.max.y, f.totalr.max.y, 0, 1);
	if(f.hscr != nil)
		f.hscr.scrollset(f.viewr.min.x, f.viewr.max.x, f.totalr.max.x, f.viewr.dx()/5, 1);
}

# The items its within f are Iimage items,
# and its image, ci, now has at least a ci.mims[0], which may be partially
# or fully filled.
haveimage(f: ref Frame, ci: ref CImage, itl: list of ref Item)
{
	if(dbgev)
		CU->event("HAVEIMAGE", 0);
	if(dbg)
		sys->print("\nHAVEIMAGE src=%s w=%d h=%d\n", ci.src.tostring(), ci.width, ci.height);
	# make all base images repl'd - makes handling backgrounds much easier
	im := ci.mims[0].im;
	im.repl = 1;
	im.clipr = Rect((-16rFFFFFFF, -16r3FFFFFFF), (16r3FFFFFFF, 16r3FFFFFFF));
	dorelayout := 0;
	for( ; itl != nil; itl = tl itl) {
		it := hd itl;
		pick i := it {
		Iimage =>
			if (!(it.state & B->IFbkg)) {
				# If i.imwidth and i.imheight are not both 0, the HTML specified the dimens.
				# If one of them is 0, the other is to be scaled by the same factor;
				# we have to relay the line in that case too.
				if(i.imwidth == 0 || i.imheight == 0) {
					i.imwidth = ci.width;
					i.imheight = ci.height;
					setimagedims(i);
					loc := f.find(zp, it);
					# sometimes the image was added to doc image list, but
					# never made it to layout (e.g., because html bug prevented
					# a table from being added).
					# also, script-created images won't have items
					if(loc != nil) {
						f.layout.flags |= Lchanged;
						markchanges(loc);
						dorelayout = 1;
						# Floats are assumed to be premeasured, so if there
						# are any floats in the loc list, remeasure them
						for(k := loc.n-1; k > 0; k--) {
							if(loc.le[k].kind == LEitem) {
								locit := loc.le[k].item;
								pick fit := locit {
								Ifloat =>
									pick xi := fit.item {
									Iimage =>
										fit.height = fit.item.height;
									Itable =>
										checktabsize(f, xi, TABLEFLOATTARGET);
									}
								}
							}
						}
					}
				}
			}
			if(dbg > 1) {
				sys->print("\nhaveimage item: ");
				it.print();
			}
		}
	}
	if(dorelayout) {
		relayout(f, f.layout, f.layout.targetwidth, f.layout.just);
		fixframegeom(f);
	}
	f.dirty(f.totalr);
	drawall(f);
	if(dbgev)
		CU->event("HAVEIMAGE_END", 0);
}
# For first layout of subelements, such as table cells.
# After this, content items will be dispersed throughout resulting lay.
# Return index into f.sublays.
# (This roundabout way of storing sublayouts avoids pointers to Lay
# in Build, so that all of the layout-related stuff can be in Layout
# where it belongs.)
sublayout(f: ref Frame, targetwidth: int, just: byte, bg: Background, content: ref Item) : int
{
	if(dbg)
		sys->print("sublayout, targetwidth=%d\n", targetwidth);
	l := Lay.new(targetwidth, just, 0, bg);
	if(f.sublayid >= len f.sublays) {
		newsublays := array[len f.sublays + 30] of ref Lay;
		newsublays[0:] = f.sublays;
		f.sublays = newsublays;
	}
	id := f.sublayid;
	f.sublays[id] = l;
	f.sublayid++;
	appenditems(f, l, content);
	l.flags &= ~Lchanged;
	if(dbg)
		sys->print("after sublayout, width=%d\n", l.width);
	return id;
}

# Relayout of lay, given a new target width or if something changed inside
# or if the global justification for the layout changed.
# Floats are hard: for now, just relay everything with floats temporarily
# moved way down, if there are any floats.
relayout(f: ref Frame, lay: ref Lay, targetwidth: int, just: byte)
{
	if(dbg)
		sys->print("relayout, targetwidth=%d, old target=%d, changed=%d\n",
			targetwidth, lay.targetwidth, (lay.flags&Lchanged) != byte 0);
	changeall := (lay.targetwidth != targetwidth || lay.just != just);
	if(!changeall && !int(lay.flags&Lchanged))
		return;
	if(lay.floats != nil) {
		# move the current y positions of floats to a big value,
		# so they don't contribute to floatw until after they've
		# been encountered in current fixgeom
		for(flist := lay.floats; flist != nil; flist = tl flist) {
			ff := hd flist;
			ff.y = 16r6fffffff;
		}
		changeall = 1;
	}
	lay.targetwidth = targetwidth;
	lay.just = just;
	lay.height = 0;
	lay.width = 0;
	if(changeall)
		changelines(lay.start.next, lay.end);
	fixgeom(f, lay, lay.start.next);
	lay.flags &= ~Lchanged;
	if(dbg)
		sys->print("after relayout, width=%d\n", lay.width);
}

# Measure and append the items to the end of layout lay,
# and fix the geometry.
appenditems(f: ref Frame, lay: ref Lay, items: ref Item)
{
	measure(f, items);
	if(dbg)
		items.printlist("appenditems, after measure");
	it := items;
	if(it == nil)
		return;
	lprev := lay.end.prev;
	l : ref Line;
	lit := lastitem(lprev.items);
	if(lit == nil || (it.state&IFbrk)) {
		# start a new line after existing last line
		l = Line.new();
		appendline(lprev, l);
		l.items = it;
	}
	else {
		# start appending items to existing last line
		l = lprev;
		lit.next = it;
	}
	l.flags |= Lchanged;
	while(it != nil) {
		nexti := it.next;
		if(nexti == nil || (nexti.state&IFbrk)) {
			it.next = nil;
			fixgeom(f, lay, l);
			if(nexti == nil)
				break;
			# now there may be multiple lines containing the
			# items from l, but the one after the last is lay.end
			l = Line.new();
			appendline(lay.end.prev, l);
			l.flags |= Lchanged;
			it = nexti;
			l.items = it;
		}
		else
			it = nexti;
	}
}

# Fix up the geometry of line l and successors.
# Assume geometry of previous line is correct.
fixgeom(f: ref Frame, lay: ref Lay, l: ref Line)
{
	while(l != nil) {
		fixlinegeom(f, lay, l);
		mergetext(l);
		l = l.next;
	}
	lay.height = max(lay.height, lay.end.pos.y);
}

mergetext(l: ref Line)
{
	lastit : ref Item;
	for (it := l.items; it != nil; it = it.next) {
		pick i := it {
		Itext =>
			if (lastit == nil)
				break; #pick
			pick pi := lastit {
			Itext =>
				# ignore item state flags as fixlinegeom() 
				# will have taken account of them.
				if (pi.anchorid == i.anchorid &&
				pi.fnt == i.fnt && pi.fg == i.fg && pi.voff == i.voff && pi.ul == i.ul) {
					# compatible - merge
					pi.s += i.s;
					pi.width += i.width;
					pi.next = i.next;
					continue;
				}
			}
		}
		lastit = it;
	}
}

# Fix geom for one line.
# This may change the overall lay.width, if there is no way
# to fit the line into the target width. 
fixlinegeom(f: ref Frame, lay: ref Lay, l: ref Line)
{
	lprev := l.prev;
	y := lprev.pos.y + lprev.height;
	it := l.items;
	state := it.state;
	if(dbg > 1) {
		sys->print("\nfixlinegeom start, y=prev.y+prev.height=%d+%d=%d, changed=%d\n",
				l.prev.pos.y, lprev.height, y, int (l.flags&Lchanged));
		if(dbg > 2)
			it.printlist("items");
		else {
			sys->print("first item: ");
			it.print();
		}
	}
	if(state&IFbrk) {
		y = pastbrk(lay, y, state);
		if(dbg > 1 && y != lprev.pos.y + lprev.height)
			sys->print("after pastbrk, line y is now %d\n", y);
	}
	l.pos.y = y;
	lineh := max(l.height, linespace);
	lfloatw := floatw(y, y+lineh, lay.floats, Aleft);
	rfloatw := floatw(y, y+lineh, lay.floats, Aright);
	if((l.flags&Lchanged) == byte 0) {
		# possibly adjust lay.width
		n := (lay.width-rfloatw)-(l.pos.x-lay.margin+l.width);
		if(n < 0)
			lay.width += -n;
		return;
	}
	hang := (state&IFhangmask)*TABPIX/10;
	linehang := hang;
	hangtogo := hang;
	indent := ((state&IFindentmask)>>IFindentshift)*TABPIX;
	just := (state&(IFcjust|IFrjust));
	if(just == 0 && lay.just != Aleft) {
		if(lay.just == byte Acenter)
			just = IFcjust;
		else if(lay.just == Aright)
			just = IFrjust;
	}
	right := lay.targetwidth - lay.margin;
	lwid := right - (lfloatw+rfloatw+indent+lay.margin);
	if(lwid < 0) {
		if (right - lwid > lay.width)
			lay.width = right - lwid;
		right += -lwid;
		lwid = 0;
	}
	lwid += hang;
	if(dbg > 1) {
		sys->print("fixlinegeom, now y=%d, lfloatw=%d, rfloatw=%d, indent=%d, hang=%d, lwid=%d\n",
				y, lfloatw, rfloatw, indent, hang, lwid);
	}
	w := 0;
	lineh = 0;
	linea := 0;
	lastit: ref Item = nil;
	nextfloats: list of ref Item.Ifloat = nil;
	anystuff := 0;
	eol := 0;
	while(it != nil && !eol) {
		if(dbg > 2) {
			sys->print("fixlinegeom loop head, w=%d, loop item:\n", w);
			it.print();
		}
		state = it.state;
		wrapping := int (state&IFwrap);
		if(anystuff && (state&IFbrk))
			break;
		checkw := 1;
		if(hang && !(state&IFhangmask)) {
			lwid -= hang;
			hang = 0;
			if(hangtogo > 0) {
				# insert a null spacer item
				spaceit := Item.newspacer(ISPgeneral, 0);
				spaceit.width = hangtogo;
				if(lastit != nil) {
					spaceit.state = lastit.state & ~(IFbrk|IFbrksp|IFnobrk|IFcleft|IFcright);
					lastit.next = spaceit;
				}
				else
					lastit = spaceit;
				spaceit.next = it;
			}
		}
		pick i := it {
		Ifloat =>
			if(anystuff) {
				# float will go after this line
				nextfloats = i :: nextfloats;
			}
			else {
				# add float beside current line, adjust widths
				fixfloatxy(lay, y, i);
				# TODO: only do following if y and/or height changed
				changelines(l.next, lay.end);
				newlfloatw := floatw(y, y+lineh, lay.floats, Aleft);
				newrfloatw := floatw(y, y+lineh, lay.floats, Aright);
				lwid -= (newlfloatw-lfloatw) + (newrfloatw-rfloatw);
				if (lwid < 0) {
					right += -lwid;
					lwid = 0;
				}
				lfloatw = newlfloatw;
				rfloatw = newrfloatw;
			}
			checkw = 0;
		Itable =>
			# When just doing layout for cell dimensions, don't
			# want a "100%" spec to make the table really wide
			kindspec := 0;
			if(lay.targetwidth == TABLEMAXTARGET && i.table.width.kind() == Dpercent) {
				kindspec = i.table.width.kindspec;
				i.table.width = Dimen.make(Dnone, 0);
			}
			checktabsize(f, i, lwid-w);
			if(kindspec != 0)
				i.table.width.kindspec = kindspec;
		Irule =>
			avail := lwid-w;
			# When just doing layout for cell dimensions, don't
			# want a "100%" spec to make the rule really wide
			if(lay.targetwidth == TABLEMAXTARGET)
				avail = min(10, avail);
			i.width = widthfromspec(i.wspec, avail);
		Iformfield =>
			checkffsize(f, i, i.formfield);
		}
		if(checkw) {
			iw := it.width;
			if(wrapping && w + iw > lwid) {
				# it doesn't fit; see if it can be broken
				takeit: int;
				noneok := (anystuff || lfloatw != 0 || rfloatw != 0) && !(state&IFnobrk);
				(takeit, iw) = trybreak(it, lwid-w, iw, noneok);
				eol = 1;
				if(!takeit) {
					if(lastit == nil) {
						# Nothing added because one of the float widths
						# is nonzero, and not enough room for anything else.
						# Move y down until there's more room and try again.
						CU->assert(lfloatw != 0 || rfloatw != 0);
						oldy := y;
						y = pastbrk(lay, y, IFcleft|IFcright);
						if(dbg > 1)
							sys->print("moved y past %d, now y=%d\n", oldy, y);
						CU->assert(y > oldy);	# else infinite recurse
						# Do the move down by artificially increasing the
						# height of the previous line
						lprev.height += y-oldy;
						fixlinegeom(f, lay, l);
						return;
					} else
						break;
				}
			}
			w += iw;
			if(hang)
				hangtogo -= w;
			(lineh, linea) = lgeom(lineh, linea, it);
			if(!anystuff) {
				anystuff = 1;
				# don't count an ordinary space as 'stuff' if wrapping
				pick t := it {
				Itext =>
					if(wrapping && t.s == " ")
						anystuff = 0;
				}
			}
		}
		lastit = it;
		it = it.next;
		if(it == nil && !eol) {
			# perhaps next lines items can now fit on this line
			nextl := l.next;
			nit := nextl.items;
			if(nextl != lay.end && !(nit.state&IFbrk)) {
				lastit.next = nit;
				# remove nextl
				l.next = nextl.next;
				l.next.prev = l;
				it = nit;
			}
		}
	}
	# line is complete, next line will start with it (or it is nil)
	rest := it;
	if(lastit == nil)
		raise "EXInternal: no items on line";
	lastit.next = nil;

	l.width = w;
	x := lfloatw + lay.margin + indent - linehang;
	# shift line if it begins with a space or a rule
	pick pi := l.items {
	Itext =>
		if(pi.s != nil && pi.s[0] == ' ')
			x -= fonts[pi.fnt].spw;
	Irule =>
		# note: build ensures that rules appear on lines
		# by themselves
		if(pi.align == Acenter)
			just = IFcjust;
		else if(pi.align == Aright)
			just = IFrjust;
	Ifloat =>
		if(pi.next != nil) {
			pick qi := pi.next {
			Itext =>
				if(qi.s != nil && qi.s[0] == ' ')
					x -= fonts[qi.fnt].spw;
			}
		}
	}
	xright := x+w;
	if (xright + rfloatw > lay.width)
		lay.width = xright+rfloatw;
	n := lay.targetwidth-(lay.margin+rfloatw+xright);
	if(n > 0 && just) {
		if(just&IFcjust)
			x += n/2;
		else
			x += n;
	}
	if(dbg > 1) {
		sys->print("line geometry fixed, (x,y)=(%d,%d), w=%d, h=%d, a=%d, lfloatw=%d, rfloatw=%d, lay.width=%d\n",
			x, l.pos.y, w, lineh, linea, lfloatw, rfloatw, lay.width);
		if(dbg > 2)
			l.items.printlist("final line items");
	}
	l.pos.x = x;
	l.height = lineh;
	l.ascent = linea;
	l.flags &= ~Lchanged;

	if(nextfloats != nil)
		fixfloatsafter(lay, l, nextfloats);

	if(rest != nil) {
		nextl := l.next;
		if(nextl == lay.end || (nextl.items.state&IFbrk)) {
			nextl = Line.new();
			appendline(l, nextl);
		}
		li := lastitem(rest);
		li.next = nextl.items;
		nextl.items = rest;
		nextl.flags |= Lchanged;
	}
}

# Return y coord after y due to a break.
pastbrk(lay: ref Lay, y, state: int) : int
{
	nextralines := 0;
	if(state&IFbrksp)
		nextralines = 1;
	ynext := y;
	if(state&IFcleft)
		ynext = floatclry(lay.floats, Aleft, ynext);
	if(state&IFcright)
		ynext = max(ynext, floatclry(lay.floats, Aright, ynext));
	ynext += nextralines*linespace;
	return ynext;
}

# Add line l after lprev (and before lprev's current successor)
appendline(lprev, l: ref Line)
{
	l.next = lprev.next;
	l.prev = lprev;
	l.next.prev = l;
	lprev.next = l;
}

# Mark lines l up to but not including lend as changed
changelines(l, lend: ref Line)
{
	for( ; l != lend; l = l.next)
		l.flags |= Lchanged;
}

# Return a ref Font for font number num = (style*NumSize + size)
getfont(num: int) : ref Font
{
	f := fonts[num].f;
	if(f == nil) {
		f = Font.open(display, fonts[num].name);
		if(f == nil) {
			if(num == DefFnt)
				raise sys->sprint("exLayout: can't open default font %s: %r", fonts[num].name);
			else {
				if(int (CU->config).dbg['w'])
					sys->print("warning: substituting default for font %s\n",
						fonts[num].name);
				f = fonts[DefFnt].f;
			}
		}
		fonts[num].f = f;
		fonts[num].spw = f.width(" ");
	}
	return f;
}

# Set the width, height and ascent fields of all items, getting any necessary fonts.
# Some widths and heights depend on the available width on the line, and may be
# wrong until checked during fixlinegeom.
# Don't do tables here at all (except floating tables).
# Configure Controls for form fields.
measure(fr: ref Frame, items: ref Item)
{
	for(it := items; it != nil; it = it.next) {
		pick t := it {
		Itext =>
			f := getfont(t.fnt);
			it.width = f.width(t.s);
			a := f.ascent;
			h := f.height;
			if(t.voff != byte Voffbias) {
				a -= (int t.voff) - Voffbias;
				if(a > h)
					h = a;
			}
			it.height = h;
			it.ascent = a;
		Irule =>
			it.height =  t.size + 2*RULESP;
			it.ascent = t.size + RULESP;
		Iimage =>
			setimagedims(t);
		Iformfield =>
			c := Control.newff(fr, t.formfield);
			if(c != nil) {
				t.formfield.ctlid = fr.addcontrol(c);
				it.width = c.r.dx();
				it.height = c.r.dy();
				it.ascent = it.height;
				pick pc := c {
				Centry =>
					it.ascent = lineascent + ENTVMARGIN;
				Cselect =>
					it.ascent = lineascent + SELMARGIN;
				Cbutton =>
					if(pc.dorelief)
						it.ascent -= BUTMARGIN;
				}
			}
		Ifloat =>
			# Leave w at zero, so it doesn't contribute to line width in normal way
			# (Can find its width in t.item.width).
			pick i := t.item {
			Iimage =>
				setimagedims(i);
				it.height = t.item.height;
			Itable =>
				checktabsize(fr, i, TABLEFLOATTARGET);
			* =>
				CU->assert(0);
			}
			it.ascent = it.height;
		Ispacer =>
			case t.spkind {
			ISPvline =>
				f := getfont(t.fnt);
				it.height = f.height;
				it.ascent = f.ascent;
			ISPhspace =>
				getfont(t.fnt);
				it.width = fonts[t.fnt].spw;
			}
		}
	}
}

# Set the dimensions of an image item
setimagedims(i: ref Item.Iimage)
{
	i.width = i.imwidth + 2*(int i.hspace + int i.border);
	i.height = i.imheight + 2*(int i.vspace + int i.border);
	i.ascent = i.height - (int i.vspace + int i.border);
	if((CU->config).imagelvl == CU->ImgNone && i.altrep != "") {
		f := fonts[DefFnt].f;
		i.width = max(i.width, f.width(i.altrep));
		i.height = max(i.height, f.height);
		i.ascent = f.ascent;
	}
}

# Line geometry function:
# Given current line height (H) and ascent (distance from top to baseline) (A),
# and an item, see if that item changes height and ascent.
# Return (H', A'), the updated line height and ascent.
lgeom(H, A: int, it: ref Item) : (int, int)
{
	h := it.height;
	a := it.ascent;
	atype := Abaseline;
	pick i := it {
	Iimage =>
		atype = i.align;
	Itable =>
		atype = Atop;
	Ifloat =>
		return (H, A);
	}
	d := h-a;
	Hnew := H;
	Anew := A;
	case int atype {
	int Abaseline or int Abottom =>
		if(a > A) {
			Anew = a;
			Hnew += (Anew - A);
		}
		if(d > Hnew - Anew)
			Hnew = Anew + d;
	int Atop =>
		# OK to ignore what comes after in the line
		if(h > H)
			Hnew = h;
	int Amiddle or int Acenter =>
		# supposed to align middle with baseline
		hhalf := h/2;
		if(hhalf > A)
			Anew = hhalf;
		if(hhalf > H-Anew)
			Hnew = Anew + hhalf;
	}
	return (Hnew, Anew);
}

# Try breaking item bit to make it fit in availw.
# If that is possible, change bit to be the part that fits
# and insert the rest between bit and bit.next.
# iw is the current width of bit.
# If noneok is 0, break off the minimum size word
# even if it exceeds availw.
# Return (1 if supposed to take bit, iw' = new width of bit)
trybreak(bit: ref Item, availw, iw, noneok: int) : (int, int)
{
	if(iw <= 0)
		return (1, iw);
	if(availw < 0) {
		if(noneok)
			return (0, iw);
		else
			availw = 0;
	}
	pick t := bit {
	Itext =>
		if(len t.s < 2)
			return (!noneok, iw);
		(s1, w1, s2, w2) := breakstring(t.s, iw, fonts[t.fnt].f, availw, noneok);
		if(w1 == 0)
			return (0, iw);
		itn := Item.newtext(s2, t.fnt, t.fg, int t.voff, t.ul);
		itn.width = w2;
		itn.height = t.height;
		itn.ascent = t.ascent;
		itn.anchorid = t.anchorid;
		itn.state = t.state & ~(IFbrk|IFbrksp|IFnobrk|IFcleft|IFcright);
		itn.next = t.next;
		t.next = itn;
		t.s = s1;
		t.width = w1;
		return (1, w1);
	}
	return (!noneok, iw);
}

# s has width sw when drawn in fnt.
# Break s into s1 and s2 so that s1 fits in availw.
# If noneok is true, it is ok for s1 to be nil, otherwise might
# have to return an s1 that overflows availw somewhat.
# Return (s1, w1, s2, w2) where w1 and w2 are widths of s1 and s2.
# Assume caller has already checked that sw > availw.
breakstring(s: string, sw: int, fnt: ref Font, availw, noneok: int) : (string, int, string, int)
{
	slen := len s;
	if(slen < 2) {
		if(noneok)
			return (nil, 0, s, sw);
		else
			return (s, sw, nil, 0);
	}

	# Use linear interpolation to guess break point.
	# We know avail < iw by conditions of trybreak call.
	i := slen*availw / sw - 1;
	if(i < 0)
		i = 0;
	i = breakpoint(s, i, -1);
	(ss, ww) := tryw(fnt, s, i);
	if(ww > availw) {
		while(ww > availw) {
			i = breakpoint(s, i-1, -1);
			if(i <= 0)
				break;
			(ss, ww) = tryw(fnt, s, i);
		}
	}
	else {
		oldi := i;
		oldss := ss;
		oldww := ww;
		while(ww < availw) {
			oldi = i;
			oldss = ss;
			oldww = ww;
			i = breakpoint(s, i+1, 1);
			if(i >= slen)
				break;
			(ss, ww) = tryw(fnt, s, i);
		}
		i = oldi;
		ss = oldss;
		ww = oldww;
	}
	if(i <= 0 || i >= slen) {
		if(noneok)
			return (nil, 0, s, sw);
		i = breakpoint(s, 1, 1);
		(ss,ww) = tryw(fnt, s, i);
	}
	return (ss, ww, s[i:slen], sw-ww);
}

# If can break between s[i-1] and s[i], return i.
# Else move i in direction incr until this is true.
# (Might end up returning 0 or len s).
breakpoint(s: string, i, incr: int) : int
{
	slen := len s;
	ans := 0;
	while(i > 0 && i < slen) {
		ci := s[i];
		di := s[i-1];
		
		# ASCII rules
		if ((ci < 16rA0 && !int wordchar[ci]) || (di < 16rA0 && !int wordchar[di])) {
			ans = i;
			break;
		}

		# Treat all ideographs as breakable.
		# The following range includes unassigned unicode code points.
		# All assigned code points in the range are class ID (ideograph) as defined
		# by the Unicode consortium's LineBreak data.
		# There are many other class ID code points outside of this range.
		# For details on how to do unicode line breaking properly see:
		# Unicode Standard Annex #14 (http://www.unicode.org/unicode/reports/tr14/)

		if ((ci >= 16r30E && ci <= 16r9FA5) || (di >= 16r30E && di <= 16r9FA5)) {
			ans = i;
			break;
		}

		# consider all other characters as unbreakable
		i += incr;
	}
	if(i == slen)
		ans = slen;
	return ans;
}

# Return (s[0:i], width of that slice in font fnt)
tryw(fnt: ref Font, s: string, i: int) : (string, int)
{
	if(i == 0)
		return ("", 0);
	ss := s[0:i];
	return (ss, fnt.width(ss));
}

# Return max width of a float that overlaps [ymin, ymax) on given side.
# Floats are in reverse order of addition, so each float's y is <= that of
# preceding floats in list.  Floats from both sides are intermixed.
floatw(ymin, ymax: int, flist: list of ref Item.Ifloat, side: byte) : int
{
	ans := 0;
	for( ; flist != nil; flist = tl flist) {
		fl := hd flist;
		if(fl.side != side)
			continue;
		fymin := fl.y;
		fymax := fymin + fl.item.height;
		if((fymin <= ymin && ymin < fymax) ||
		   (ymin <= fymin && fymin < ymax)) {
			w := fl.x;
			if(side == Aleft)
				w += fl.item.width;
			if(ans < w)
				ans = w;
		}
	}
	return ans;
}

# Float f is to be at vertical position >= y.
# Fix its (x,y) pos and add it to lay.floats, if not already there.
fixfloatxy(lay: ref Lay, y: int, f: ref Item.Ifloat)
{
	height := f.item.height;
	width := f.item.width;
	f.y = y;
	flist := lay.floats;
	if(f.infloats != byte 0) {
		# only take previous floats into account for width
		while(flist != nil) {
			x := hd flist;
			flist = tl flist;
			if(x == f)
				break;
		}
	}
	f.x = floatw(y, y+height, flist, f.side);
	endx := f.x + width + lay.margin;
	if (endx > lay.width)
		lay.width = endx;
	if (f.side == Aright)
		f.x += width;
	endy := f.y + height + lay.margin;
	if (endy > lay.height)
		lay.height = endy;
	if(f.infloats == byte 0) {
		lay.floats = f :: lay.floats;
		f.infloats = byte 1;
	}
}

# Floats in flist are to go after line l.
fixfloatsafter(lay: ref Lay, l: ref Line, flist: list of ref Item.Ifloat)
{
	change := 0;
	y := l.pos.y + l.height;
	for(itl := Item.revlist(flist); itl != nil; itl = tl itl) {
		pick fl := hd itl {
		Ifloat =>
			oldy := fl.y;
			fixfloatxy(lay, y, fl);
			if(fl.y != oldy)
				change = 1;
			y += fl.item.height;
		}
	}
#	if(change)
# TODO only change if y and/or height changed
		changelines(l.next, lay.end);
}

# If there's a float on given side that starts on or before y and
# ends after y, return ending y of that float, else return original y.
# Assume float list is bottom up.
floatclry(flist: list of ref Item.Ifloat, side: byte, y: int) : int
{
	ymax := y;
	for( ; flist != nil; flist = tl flist) {
		fl := hd flist;
		if(fl.side == side) {
			if(fl.y <= y) {
				flymax := fl.y + fl.item.height;
				if (fl.item.height == 0)
					# assume it will have some height later
					flymax++;
				if(flymax > ymax)
					ymax = flymax;
			}
		}
	}
	return ymax;
}

# Do preliminaries to laying out table tab in target width linewidth,
# setting total height and width.
sizetable(f: ref Frame, tab: ref Table, availwidth: int)
{
	if(dbgtab)
		sys->print("sizetable %d, availwidth=%d, nrow=%d, ncol=%d, changed=%x, tab.availw=%d\n",
			tab.tableid, availwidth, tab.nrow, tab.ncol, int (tab.flags&Lchanged), tab.availw);
	if(tab.ncol == 0 || tab.nrow == 0)
		return;
	if(tab.availw == availwidth && (tab.flags&Lchanged) == byte 0)
		return;
	(hsp, vsp, pad, bd, cbd, hsep, vsep) := tableparams(tab);
	totw := widthfromspec(tab.width, availwidth);
	# reduce totw by spacing, padding, and rule widths
	# to leave amount left for contents
	totw -= (tab.ncol-1)*hsep+ 2*(hsp+bd+pad+cbd);
	if(totw <= 0)
		totw = 1;
	if(dbgtab)
		sys->print("\nsizetable %d, totw=%d, hsp=%d, vsp=%d, pad=%d, bd=%d, cbd=%d, hsep=%d, vsep=%d\n",
			tab.tableid, totw, hsp, vsp, pad, bd, cbd, hsep, vsep);
	for(cl := tab.cells; cl != nil; cl = tl cl) {
		c := hd cl;
		clay : ref Lay = nil;
		if(c.layid >= 0)
			clay = f.sublays[c.layid];
		if(clay == nil || (clay.flags&Lchanged) != byte 0) {
			c.minw = -1;
			tw := TABLEMAXTARGET;
			if(c.wspec.kind() != Dnone)
				tw = widthfromspec(c.wspec, totw);

			# When finding max widths, want to lay out using Aleft alignment,
			# because we don't yet know final width for proper justification.
			# If the max widths are accepted, we'll redo those needing other justification.
			if(clay == nil) {
				if(dbg)
					sys->print("Initial layout for cell %d.%d\n", tab.tableid, c.cellid);
				c.layid = sublayout(f, tw, Aleft, c.background, c.content);
				clay = f.sublays[c.layid];
				c.content = nil;
			}
			else {
				if(dbg)
					sys->print("Relayout (for max) for cell %d.%d\n", tab.tableid, c.cellid);
				relayout(f, clay, tw, Aleft);
			}
			clay.flags |= Lchanged;	# for min test, below
			c.maxw = clay.width;
			if(dbgtab)
				sys->print("sizetable %d for cell %d max layout done, targw=%d, c.maxw=%d\n",
						tab.tableid, c.cellid, tw, c.maxw);
			if(c.wspec.kind() == Dpixels) {
				# Other browsers don't make the following adjustment for
				# percentage and relative widths
				if(c.maxw <= tw)
					c.maxw = tw;
				if(dbgtab)
					sys->print("after spec adjustment, c.maxw=%d\n", c.maxw);
			}
		}
	}

	# calc max column widths
	colmaxw := array[tab.ncol] of { * => 0};
	maxw := widthcalc(tab, colmaxw, hsep, 1);

	if(dbgtab)
		sys->print("sizetable %d maxw=%d, totw=%d\n", tab.tableid, maxw, totw);
	ci: int;
	if(maxw <= totw) {
		# trial layouts are fine,
		# but if table width was specified, add more space
		d := 0;
		adjust := (totw > maxw && tab.width.kind() != Dnone);
		for(ci = 0; ci < tab.ncol; ci++) {
			if (adjust) {
				delta := (totw-maxw);
				d = delta / (tab.ncol - ci);
				if (d <= 0) {
					d = delta;
					adjust = 0;
				}
				maxw += d;
			}
			tab.cols[ci].width = colmaxw[ci] + d;
		}
	}
	else {
		# calc min column widths and  apportion out
		# differences
		if(dbgtab)
			sys->print("sizetable %d, availwidth %d, need min widths too\n", tab.tableid, availwidth);
		for(cl = tab.cells; cl != nil; cl = tl cl) {
			c := hd cl;
			clay := f.sublays[c.layid];
			if(c.minw == -1 || (clay.flags&Lchanged) != byte 0) {
				if(dbg)
					sys->print("Relayout (for min) for cell %d.%d\n", tab.tableid, c.cellid);
				relayout(f, clay, 1, Aleft);
				c.minw = clay.width;
				if(dbgtab)
					sys->print("sizetable %d for cell %d min layout done, c.min=%d\n",
						tab.tableid, c.cellid, clay.width);
			}
		}
		colminw := array[tab.ncol] of { * => 0};
		minw := widthcalc(tab, colminw, hsep, 0);
		w := totw - minw;
		d := maxw - minw;
		if(dbgtab)
			sys->print("sizetable %d minw=%d, w=%d, d=%d\n", tab.tableid, minw, w, d);
		for(ci = 0; ci < tab.ncol; ci++) {
			wd : int;
			if(w < 0 || d < 0)
				wd = colminw[ci];
			else
				wd = colminw[ci] + (colmaxw[ci] - colminw[ci])*w/d;
			if(dbgtab)
				sys->print("sizetable %d col[%d].width = %d\n", tab.tableid, ci, wd);
			tab.cols[ci].width = wd;
		}

		if(dbgtab)
			sys->print("sizetable %d, availwidth %d, doing final layouts\n", tab.tableid, availwidth);
	}

	# now have col widths; set actual cell dimensions
	# and relayout (note: relayout will do no work if the target width
	# and just haven't changed from last layout)
	for(cl = tab.cells; cl != nil; cl = tl cl) {
		c := hd cl;
		clay := f.sublays[c.layid];
		wd := cellwidth(tab, c, hsep);
		if(dbgtab)
			sys->print("sizetable %d for cell %d, clay.width=%d, cellwidth=%d\n",
					tab.tableid, c.cellid, clay.width, wd);
		if(dbg)
			sys->print("Relayout (final) for cell %d.%d\n", tab.tableid, c.cellid);
		relayout(f, clay, wd, c.align.halign);
		if(dbgtab)
			sys->print("sizetable %d for cell %d, final width %d, got width %d, height %d\n",
					tab.tableid, c.cellid, wd, clay.width, clay.height);
	}

	# set row heights and ascents
	# first pass: ignore cells with rowspan > 1
	for(ri := 0; ri < tab.nrow; ri++) {
		row := tab.rows[ri];
		h := 0;
		a := 0;
		n : int;
		for(rcl := row.cells; rcl != nil; rcl = tl rcl) {
			c := hd rcl;
			if(c.rowspan > 1 || c.layid < 0)
				continue;
			al := c.align.valign;
			if(al == Anone)
				al = tab.rows[c.row].align.valign;
			clay := f.sublays[c.layid];
			if(al == Abaseline) {
				n = c.ascent;
				if(n > a) {
					h += (n - a);
					a = n;
				}
				n = clay.height - c.ascent;
				if(n > h-a)
					h = a + n;
			}
			else {
				n = clay.height;
				if(n > h)
					h = n;
			}
		}
		row.height = h;
		row.ascent = a;
	}
	# second pass: take care of rowspan > 1
	# (this algorithm isn't quite right -- it might add more space
	# than is needed in the presence of multiple overlapping rowspans)
	for(cl = tab.cells; cl != nil; cl = tl cl) {
		c := hd cl;
		if(c.rowspan > 1) {
			spanht := 0;
			for(i := 0; i < c.rowspan && c.row+i < tab.nrow; i++)
				spanht += tab.rows[c.row+i].height;
			if(c.layid < 0)
				continue;
			clay := f.sublays[c.layid];
			ht := clay.height - (c.rowspan-1)*vsep;
			if(ht > spanht) {
				# add extra space to last spanned row
				i = c.row+c.rowspan-1;
				if(i >= tab.nrow)
					i = tab.nrow - 1;
				tab.rows[i].height += ht - spanht;
				if(dbgtab)
					sys->print("sizetable %d, row %d height %d\n", tab.tableid, i, tab.rows[i].height);
			}
		}
	}
	# get total width, heights, and col x / row y positions
	totw = bd + hsp + cbd + pad;
	for(ci = 0; ci < tab.ncol; ci++) {
		tab.cols[ci].pos.x = totw;
		if(dbgtab)
			sys->print("sizetable %d, col %d at x=%d\n", tab.tableid, ci, totw);
		totw += tab.cols[ci].width + hsep;
	}
	totw = totw - (cbd+pad) + bd;
	toth := bd + vsp + cbd + pad;
	# first time: move tab.caption items into layout
	if(tab.caption != nil) {
		# lay caption with Aleft; drawing will center it over the table width
		tab.caption_lay = sublayout(f, availwidth, Aleft, f.layout.background, tab.caption);
		caplay := f.sublays[tab.caption_lay];
		tab.caph = caplay.height + CAPSEP;
		tab.caption = nil;
	}
	else if(tab.caption_lay >= 0) {
		caplay := f.sublays[tab.caption_lay];
		if(tab.availw != availwidth || (caplay.flags&Lchanged) != byte 0) {
			relayout(f, caplay, availwidth, Aleft);
			tab.caph = caplay.height + CAPSEP;
		}
	}
	if(tab.caption_place == Atop)
		toth += tab.caph;
	for(ri = 0; ri < tab.nrow; ri++) {
		tab.rows[ri].pos.y = toth;
		if(dbgtab)
			sys->print("sizetable %d, row %d at y=%d\n", tab.tableid, ri, toth);
		toth += tab.rows[ri].height + vsep;
	}
	toth = toth - (cbd+pad) + bd;
	if(tab.caption_place == Abottom)
		toth += tab.caph;
	tab.totw = totw;
	tab.toth = toth;
	tab.availw = availwidth;
	tab.flags &= ~Lchanged;
	if(dbgtab)
		sys->print("\ndone sizetable %d, availwidth %d, totw=%d, toth=%d\n\n",
			tab.tableid, availwidth, totw, toth);
}

# Calculate various table spacing parameters
tableparams(tab: ref Table) : (int, int, int, int, int, int, int)
{
	bd := tab.border;
	hsp := tab.cellspacing;
	vsp := hsp;
	pad := tab.cellpadding;
	if(bd != 0)
		cbd := 1;
	else
		cbd = 0;
	hsep := 2*(cbd+pad)+hsp;
	vsep := 2*(cbd+pad)+vsp;
	return (hsp, vsp, pad, bd, cbd, hsep, vsep);
}

# return cell width, taking multicol spanning into account
cellwidth(tab: ref Table, c: ref Tablecell, hsep: int) : int
{
	if(c.colspan == 1)
		return tab.cols[c.col].width;
	wd := (c.colspan-1)*hsep;
	for(i := 0; i < c.colspan && c.col + i < tab.ncol; i++)
		wd += tab.cols[c.col + i].width;
	return wd;
}

# return cell height, taking multirow spanning into account
cellheight(tab: ref Table, c: ref Tablecell, vsep: int) : int
{
	if(c.rowspan == 1)
		return tab.rows[c.row].height;
	ht := (c.rowspan-1)*vsep;
	for(i := 0; i < c.rowspan && c.row + i < tab.nrow; i++)
		ht += tab.rows[c.row + i].height;
	return ht;
}

# Calculate the column widths w as the max of the cells
# maxw or minw (as domax is 1 or 0).
# Return the total of all w.
# (hseps were accounted for by the adjustment that got
# totw from availwidth).
# hsep is amount of free space available between columns
# where there is multicolumn spanning.
# This is a two-pass algorithm.  The first pass ignores
# cells that span multiple columns.  The second pass
# sees if those multispanners need still more space, and
# if so, apportions the space out.
widthcalc(tab: ref Table, w: array of int, hsep, domax: int) : int
{
	anyspan := 0;
	totw := 0;
	for(pass := 1; pass <= 2; pass++) {
		if(pass==2 && !anyspan)
			break;
		totw = 0;
		for(ci := 0; ci < tab.ncol; ci++) {
			for(ri := 0; ri < tab.nrow; ri++) {
				c := tab.grid[ri][ci];
				if(c == nil)
					continue;
				if(domax)
					cwd := c.maxw;
				else
					cwd = c.minw;
				if(pass == 1) {
					if(c.colspan > 1) {
						anyspan = 1;
						continue;
					}
					if(cwd > w[ci])
						w[ci] = cwd;
				}
				else {
					if(c.colspan == 1 || !(ci==c.col && ri==c.row))
						continue;
					curw := 0;
					iend := ci+c.colspan;
					if(iend > tab.ncol)
						iend = tab.ncol;
					for(i:=ci; i < iend; i++)
						curw += w[i];
				
					# padding between spanned cols is free
					cwd -= hsep*(c.colspan-1);
					diff := cwd-curw;
					if(diff <= 0)
						continue;
					# doesn't fit: apportion diff among cols
					# in proportion to their current w
					for(i = ci; i < iend; i++) {
						if(curw == 0)
							w[i] = diff/c.colspan;
						else
							w[i] += diff*w[i]/curw;
					}
				}
			}
			totw += w[ci];
		}
	}
	return totw;
}

layframeset(f: ref Frame, ki: ref Kidinfo)
{
	fwid := f.cr.dx();
	fht := f.cr.dy();
	if(dbg)
		sys->print("layframeset, configuring frame %d wide by %d high\n", fwid, fht);
	(nrow, rowh) := frdimens(ki.rows, fht);
	(ncol, colw) := frdimens(ki.cols, fwid);
	l := ki.kidinfos;
	y := f.cr.min.y;
	for(i := 0; i < nrow; i++) {
		x := f.cr.min.x;
		for(j := 0; j < ncol; j++) {
			if(l == nil)
				return;
			r := Rect(Point(x,y), Point(x+colw[j],y+rowh[i]));
			if(dbg)
				sys->print("kid gets rect (%d,%d)(%d,%d)\n", r.min.x, r.min.y, r.max.x, r.max.y);
			kidki := hd l;
			l = tl l;
			kidf := Frame.newkid(f, kidki, r);
			if(!kidki.isframeset)
				f.kids = kidf :: f.kids;
			if(kidf.framebd != 0) {
				kidf.cr = kidf.r.inset(2);
				drawborder(kidf.cim, kidf.cr, 2, DarkGrey);
			}
			if(kidki.isframeset) {
				layframeset(kidf, kidki);
				for(al := kidf.kids; al != nil; al = tl al)
					f.kids = (hd al) :: f.kids;
			}
			x += colw[j];
		}
		y += rowh[i];
	}
}

# Use the dimension specs in dims to allocate total space t.
# Return (number of dimens, array of allocated space)
frdimens(dims: array of B->Dimen, t: int): (int, array of int)
{
	n := len dims;
	if(n == 1)
		return (1, array[] of {t});
	totpix := 0;
	totpcnt := 0;
	totrel := 0;
	for(i := 0; i < n; i++) {
		v := dims[i].spec();
		kind := dims[i].kind();
		if(v < 0) {
			v = 0;
			dims[i] = Dimen.make(kind, v);
		}
		case kind {
			B->Dpixels => totpix += v;
			B->Dpercent => totpcnt += v;
			B->Drelative => totrel += v;
			B->Dnone => totrel++;
		}
	}
	spix := 1.0;
	spcnt := 1.0;
	min_relu := 0;
	if(totrel > 0)
		min_relu = 30;	# allow for scrollbar (14) and a bit
	relu := real min_relu;
	tt := totpix + (t*totpcnt/100) + totrel*min_relu;
	# want
	#  t ==  totpix*spix + (totpcnt/100)*spcnt*t + totrel*relu
	if(tt < t) {
		# need to expand one of spix, spcnt, relu
		if(totrel == 0) {
			if(totpcnt != 0)
				# spix==1.0, relu==0, solve for spcnt
				spcnt = real ((t-totpix) * 100)/ real (t*totpcnt);
			else
				# relu==0, totpcnt==0, solve for spix
				spix = real t/ real totpix;
		}
		else
			# spix=1.0, spcnt=1.0, solve for relu
			relu += real (t-tt)/ real totrel;
	}
	else {
		# need to contract one or more of spix, spcnt, and have relu==min_relu
		totpixrel := totpix+totrel*min_relu;
		if(totpixrel < t) {
			# spix==1.0, solve for spcnt
			spcnt = real ((t-totpixrel) * 100)/ real (t*totpcnt);
		}
		else {
			# let spix==spcnt, solve
			trest := t - totrel*min_relu;
			if(trest > 0) {
				spcnt = real trest/real (totpix+(t*totpcnt/100));
			}
			else {
				spcnt = real t / real tt;
				relu = 0.0;
			}
			spix = spcnt;
		}
	}
	x := array[n] of int;
	tt = 0;
	for(i = 0; i < n-1; i++) {
		vr := real dims[i].spec();
		case dims[i].kind() {
			B->Dpixels => vr = vr * spix;
			B->Dpercent => vr = vr * real t * spcnt / 100.0;
			B->Drelative => vr = vr * relu;
			B->Dnone => vr = relu;
		}
		x[i] = int vr;
		tt += x[i];
	}
	x[n-1] = t - tt;
	return (n, x);
}

# Return last item of list of items, or nil if no items
lastitem(it: ref Item) : ref Item
{
	ans : ref Item = it;
	for( ; it != nil; it = it.next)
		ans = it;
	return ans;
}

# Lay out table if availw changed or tab changed
checktabsize(f: ref Frame, t: ref Item.Itable, availw: int)
{
	tab := t.table;
	if (dbgtab)
		sys->print("checktabsize %d, availw %d, tab.availw %d, changed %d\n", tab.tableid, availw, tab.availw, (tab.flags&Lchanged)>byte 0);
	if(availw != tab.availw || int (tab.flags&Lchanged)) {
		sizetable(f, tab, availw);
		t.width = tab.totw + 2*tab.border;
		t.height = tab.toth + 2*tab.border;
		t.ascent = t.height;
	}
}

widthfromspec(wspec: Dimen, availw: int) : int
{
	w := availw;
	spec := wspec.spec();
	case wspec.kind() {
		Dpixels => w = spec;
		Dpercent => w = spec*w/100;
	}
	return w;
}

# An image may have arrived for an image input field
checkffsize(f: ref Frame, i: ref Item, ff: ref Formfield)
{
	if(ff.ftype == Fimage && ff.image != nil) {
		pick imi := ff.image {
		Iimage =>
			if(imi.ci.mims != nil && ff.ctlid >= 0) {
				pick b := f.controls[ff.ctlid] {
				Cbutton =>
					if(b.pic == nil) {
						b.pic = imi.ci.mims[0].im;
						b.picmask = imi.ci.mims[0].mask;
						w := b.pic.r.dx();
						h := b.pic.r.dy();
						b.r.max.x = b.r.min.x + w;
						b.r.max.y = b.r.min.y + h;
						i.width = w;
						i.height = h;
						i.ascent = h;
					}
				}
			}
		}
	}
	else if(ff.ftype == Fselect) {
		opts := ff.options;
		if(ff.ctlid >=0) {
			pick c := f.controls[ff.ctlid] {
			Cselect =>
				if(len opts != len c.options) {
					nc := Control.newff(f, ff);
					f.controls[ff.ctlid] = nc;
					i.width = nc.r.dx();
					i.height = nc.r.dy();
					i.ascent = lineascent + SELMARGIN;
				}
			}
		}
	}
}

drawall(f: ref Frame)
{
	oclipr := f.cim.clipr;
	origin := f.lptosp(zp);
	clipr := f.dirtyr.addpt(origin);
	f.cim.clipr = clipr;
	fillbg(f, clipr);
	if(dbg > 1)
		sys->print("drawall, cr=(%d,%d,%d,%d), viewr=(%d,%d,%d,%d), origin=(%d,%d)\n",
			f.cr.min.x, f.cr.min.y, f.cr.max.x, f.cr.max.y,
			f.viewr.min.x, f.viewr.min.y, f.viewr.max.x, f.viewr.max.y,
			origin.x, origin.y);
	if(f.layout != nil)
		drawlay(f, f.layout, origin);
	f.cim.clipr = oclipr;
	G->flush(f.cr);
	f.isdirty = 0;
}

drawlay(f: ref Frame, lay: ref Lay, origin: Point)
{
	for(l := lay.start.next; l != lay.end; l = l.next)
		drawline(f, origin, l, lay);
}

# Draw line l in frame f, assuming that content's (0,0)
# aligns with layorigin in f.cim.
drawline(f : ref Frame, layorigin : Point, l: ref Line, lay: ref Lay)
{
	im := f.cim;
	o := layorigin.add(l.pos);
	x := o.x;
	y := o.y;
	lr := Rect(zp, Point(l.width, l.height)).addpt(o);
	isdirty := f.isdirty && lr.Xrect(f.dirtyr.addpt(f.lptosp(zp)));
	inview := lr.Xrect(f.cr) && isdirty;

	# note: drawimg must always be called to update
	# draw point of animated images
	for(it := l.items; it != nil; it = it.next) {
		pick i := it {
		Itext =>
			if (!inview || i.s == nil)
				break;
			fnt := fonts[i.fnt];
			width := i.width;
			yy := y+l.ascent - fnt.f.ascent + (int i.voff) - Voffbias;
			if (f.prctxt != nil) {
				if (yy < f.cr.min.y)
					continue;
				endy := yy + fnt.f.height;
				if (endy > f.cr.max.y) {
					# do not draw
					if (yy < f.prctxt.endy)
						f.prctxt.endy = yy;
					continue;
				}
			}
			fgi := colorimage(i.fg);
			im.text(Point(x, yy), fgi, zp, fnt.f, i.s);
			if(i.ul != ULnone) {
				if(i.ul == ULmid)
					yy += 2*i.ascent/3;
				else
					yy += i.height - 1;
				# don't underline leading space
				# have already adjusted x pos in fixlinegeom()
				ulx := x;
				ulw := width;
				if (i.s[0] == ' ') {
					ulx += fnt.spw;
					ulw -= fnt.spw;
				}
				if (i.s[len i.s - 1] == ' ')
					ulw -= fnt.spw;
				if (ulw < 1)
					continue;
				im.drawop(Rect(Point(ulx,yy),Point(ulx+ulw,yy+1)), fgi, nil, zp, Draw->S);
			}
		Irule =>
			if (!inview)
				break;
			yy := y + RULESP;
			im.draw(Rect(Point(x,yy),Point(x+i.width,yy+i.size)),
					display.black, nil, zp);
		Iimage =>
			yy := y;
			if(i.align == Abottom)
				# bottom aligns with baseline
				yy += l.ascent - i.imheight;
			else if(i.align == Amiddle)
				yy += l.ascent - (i.imheight/2);
			drawimg(f, Point(x,yy), i);
		Iformfield =>
			ff := i.formfield;
			if(ff.ctlid >= 0 && ff.ctlid < len f.controls) {
				ctl := f.controls[ff.ctlid];
				dims := ctl.r.max.sub(ctl.r.min);
				# align as text
				yy := y + l.ascent - i.ascent;
				p := Point(x,yy);
				ctl.r = Rect(p, p.add(dims));
				if (!inview)
					break;
				if (f.prctxt != nil) {
					if (yy < f.cr.min.y)
						continue;
					if (ctl.r.max.y > f.cr.max.y) {
						# do not draw
						if (yy < f.prctxt.endy)
							f.prctxt.endy = yy;
						continue;
					}
				}
				ctl.draw(0);
			}
		Itable =>
			# don't check inview - table can contain images
			drawtable(f, lay, Point(x,y), i.table);
			t := i.table;
		Ifloat =>
			xx := layorigin.x + lay.margin;
			if(i.side == Aright) {
				xx -= i.x;
#				# for main layout of frame, floats hug
#				# right edge of frame, not layout
#				# (other browsers do that)
#				if(f.layout == lay)
					xx += lay.targetwidth;
#				else
#					xx += lay.width;
			}
			else
				xx += i.x;
			pick fi := i.item {
			Iimage =>
				drawimg(f, Point(xx, layorigin.y + i.y + (int fi.border + int fi.vspace)), fi);
			Itable =>
				drawtable(f, lay, Point(xx, layorigin.y + i.y), fi.table);
			}
		}
		x += it.width;
	}
}

drawimg(f: ref Frame, iorigin: Point, i: ref Item.Iimage)
{
	ci := i.ci;
	im := f.cim;
	iorigin.x += int i.hspace + int i.border;
	# y coord is already adjusted for border and vspace
	if(ci.mims != nil) {
		r := Rect(iorigin, iorigin.add(Point(i.imwidth,i.imheight)));
		inview := r.Xrect(f.cr);
		if(i.ctlid >= 0) {
			# animated
			c := f.controls[i.ctlid];
			dims := c.r.max.sub(c.r.min);
			c.r = Rect(iorigin, iorigin.add(dims));
			if (inview) {
				pick ac := c {
				Canimimage =>
					ac.redraw = 1;
					ac.bg = f.layout.background;
				}
				c.draw(0);
			}
		}
		else if (inview) {
			mim := ci.mims[0];
			iorigin = iorigin.add(mim.origin);
			im.draw(r, mim.im, mim.mask, zp);
		}
		if(inview && i.border != byte 0) {
			if(i.anchorid != 0)
				bdcol := f.doc.link;
			else
				bdcol = Black;
			drawborder(im, r, int i.border, bdcol);
		}
	}
	else if((CU->config).imagelvl == CU->ImgNone && i.altrep != "") {
		fnt := fonts[DefFnt].f;
		yy := iorigin.y+(i.imheight-fnt.height)/2;
		xx := iorigin.x + (i.width-fnt.width(i.altrep))/2;
		if(i.anchorid != 0)
			col := f.doc.link;
		else
			col = DarkGrey;
		fgi := colorimage(col);
		im.text(Point(xx, yy), fgi, zp, fnt, i.altrep);
	}
}

drawtable(f : ref Frame, parentlay: ref Lay, torigin: Point, tab: ref Table)
{
	if (dbgtab)
		sys->print("drawtable %d\n", tab.tableid);
	if(tab.ncol == 0 || tab.nrow == 0)
		return;
	im := f.cim;
	(hsp, vsp, pad, bd, cbd, hsep, vsep) := tableparams(tab);
	x := torigin.x;
	y := torigin.y;
	capy := y;
	boxy := y;
	if(tab.caption_place == Abottom)
		capy = y+tab.toth-tab.caph+vsp;
	else
		boxy = y+tab.caph;
	if (tab.background.color != -1 && tab.background.color != parentlay.background.color) {
#	if(tab.background.image != parentlay.background.image ||
#	   tab.background.color != parentlay.background.color) {
		bgi := colorimage(tab.background.color);
		im.draw(((x,boxy),(x+tab.totw,boxy+tab.toth-tab.caph)),
			bgi, nil, zp);
	}
	if(bd != 0)
		drawborder(im, ((x+bd,boxy+bd),(x+tab.totw-bd,boxy+tab.toth-tab.caph-bd)),
			1, Black);
	for(cl := tab.cells; cl != nil; cl = tl cl) {
		c := hd cl;
		if (c.layid == -1 || c.layid >= len f.sublays) {
			# for some reason (usually scrolling)
			# we are drawing this cell before it has been layed out
			continue;
		}
		clay := f.sublays[c.layid];
		if(clay == nil)
			continue;
		if(c.col >= len tab.cols)
			continue;
		cx := x + tab.cols[c.col].pos.x;
		cy := y + tab.rows[c.row].pos.y;
		wd := cellwidth(tab, c, hsep);
		ht := cellheight(tab, c, vsep);
		if(c.background.image != nil && c.background.image.ci != nil && c.background.image.ci.mims != nil) {
			cellr := Rect((cx-pad,cy-pad),(cx+wd+pad,cy+ht+pad));
			ci := c.background.image.ci;
			bgi := ci.mims[0].im;
			bgmask := ci.mims[0].mask;
			im.draw(cellr, bgi, bgmask, bgi.r.min);
		} else if(c.background.color != -1 && c.background.color != tab.background.color) {
			bgi := colorimage(c.background.color);
			im.draw(((cx-pad,cy-pad),(cx+wd+pad,cy+ht+pad)),
				bgi, nil, zp);
		}
		if(bd != 0)
			drawborder(im, ((cx-pad+1,cy-pad+1),(cx+wd+pad-1,cy+ht+pad-1)),
				1, Black);
		if(c.align.valign != Atop && c.align.valign != Abaseline) {
			n := ht - clay.height;
			if(c.align.valign == Amiddle)
				cy += n/2;
			else if(c.align.valign == Abottom)
				cy += n;
		}
		if(dbgtab)
			sys->print("drawtable %d cell %d at (%d,%d)\n",
				tab.tableid, c.cellid, cx, cy);
		drawlay(f, clay, Point(cx,cy));
	}
	if(tab.caption_lay >= 0) {
		caplay := f.sublays[tab.caption_lay];
		capx := x;
		if(caplay.width < tab.totw)
			capx += (tab.totw-caplay.width) / 2;
		drawlay(f, caplay, Point(capx,capy));
	}
}

# Draw border of width n just outside r, using src color
drawborder(im: ref Image, r: Rect, n, color: int)
{
	x := r.min.x-n;
	y := r.min.y - n;
	xr := r.max.x+n;
	ybi := r.max.y;
	src := colorimage(color);
	im.draw((Point(x,y),Point(xr,y+n)), src, nil, zp);				# top
	im.draw((Point(x,ybi),Point(xr,ybi+n)), src, nil, zp);			# bottom
	im.draw((Point(x,y+n),Point(x+n,ybi)), src, nil, zp);			# left
	im.draw((Point(xr-n,y+n),Point(xr,ybi)), src, nil, zp);			# right
}

# Draw relief border just outside r, width 2 border,
# colors white/lightgrey/darkgrey/black
# to give raised relief (if raised != 0) or sunken.
drawrelief(im: ref Image, r: Rect, raised: int)
{
	# ((x,y),(xr,yb)) == r
	x := r.min.x;
	x1 := x-1;
	x2 := x-2;
	xr := r.max.x;
	xr1 := xr+1;
	xr2 := xr+2;
	y := r.min.y;
	y1 := y-1;
	y2 := y-2;
	yb := r.max.y;
	yb1 := yb+1;
	yb2 := yb+2;

	# colors for top/left outside, top/left inside, bottom/right outside, bottom/right inside
	tlo, tli, bro, bri: ref Image;
	if(raised) {
		tlo = colorimage(Grey);
		tli = colorimage(White);
		bro = colorimage(Black);
		bri = colorimage(DarkGrey);
	}
	else {
		tlo = colorimage(DarkGrey);
		tli = colorimage(Black);
		bro = colorimage(White);
		bri = colorimage(Grey);
	}

	im.draw((Point(x2,y2), Point(xr1,y1)), tlo, nil, zp);		# top outside
	im.draw((Point(x1,y1), Point(xr,y)), tli, nil, zp);			# top inside
	im.draw((Point(x2,y1), Point(x1,yb1)), tlo, nil, zp);		# left outside
	im.draw((Point(x1,y), Point(x,yb)), tli, nil, zp);			# left inside
	im.draw((Point(xr,y1),Point(xr1,yb)), bri, nil, zp);		# right inside
	im.draw((Point(xr1,y),Point(xr2,yb1)), bro, nil, zp);		# right outside
	im.draw((Point(x1,yb),Point(xr1,yb1)), bri, nil, zp);		# bottom inside
	im.draw((Point(x,yb1),Point(xr2,yb2)), bro, nil, zp);		# bottom outside
}

# Fill r with color
drawfill(im: ref Image, r: Rect, color: int)
{
	im.draw(r, colorimage(color), nil, zp);
}

# Draw string in default font at p
drawstring(im: ref Image, p: Point, s: string)
{
	im.text(p, colorimage(Black), zp, fonts[DefFnt].f, s);
}

# Return (width, height) of string in default font
measurestring(s: string) : Point
{
	f := fonts[DefFnt].f;
	return (f.width(s), f.height);
}

# Mark as "changed" everything with change flags on the loc path
markchanges(loc: ref Loc)
{
	lastf : ref Frame = nil;
	for(i := 0; i < loc.n; i++) {
		case loc.le[i].kind {
		LEframe =>
			lastf = loc.le[i].frame;
			lastf.layout.flags |= Lchanged;
		LEline =>
			loc.le[i].line.flags |= Lchanged;
		LEitem =>
			pick it := loc.le[i].item {
			Itable =>
				it.table.flags |= Lchanged;
			Ifloat =>
				# whole layout will be redone if layout changes
				# and there are any floats
				;
			}
		LEtablecell =>
			if(lastf == nil)
				raise "EXInternal: markchanges no lastf";
			c := loc.le[i].tcell;
			clay := lastf.sublays[c.layid];
			if(clay != nil)
				clay.flags |= Lchanged;
		}
	}
}

# one-item cache for colorimage
prevrgb := -1;
prevrgbimage : ref Image = nil;

colorimage(rgb: int) : ref Image
{
	if(rgb == prevrgb)
		return prevrgbimage;
	prevrgb = rgb;
	if(rgb == Black)
		prevrgbimage = display.black;
	else if(rgb == White)
		prevrgbimage = display.white;
	else {
		hv := rgb % NCOLHASH;
		if (hv < 0)
			hv = -hv;
		xhd := colorhashtab[hv];
		x := xhd;
		while(x != nil && x.rgb  != rgb)
			x = x.next;
		if(x == nil) {
#			pix := I->closest_rgbpix((rgb>>16)&255, (rgb>>8)&255, rgb&255);
#			im := display.color(pix);
			im := display.rgb((rgb>>16)&255, (rgb>>8)&255, rgb&255);
			if(im == nil)
				raise sys->sprint("exLayout: can't allocate color #%8.8ux: %r", rgb);
			x = ref Colornode(rgb, im, xhd);
			colorhashtab[hv] = x;
		}
		prevrgbimage = x.im;
	}
	return prevrgbimage;
}

# Use f.background.image (if not nil) or f.background.color to fill r (in cim coord system)
# with background color.
fillbg(f: ref Frame, r: Rect)
{
	bgi: ref Image;
	ii := f.doc.background.image;
	if (ii != nil && ii.ci != nil && ii.ci.mims != nil)
		bgi = ii.ci.mims[0].im;
	if(bgi == nil)
		bgi = colorimage(f.doc.background.color);
	f.cim.drawop(r, bgi, nil, f.viewr.min, Draw->S);
}

TRIup, TRIdown, TRIleft, TRIright: con iota;
# Assume r is a square
drawtriangle(im: ref Image, r: Rect, kind, style: int)
{
	drawfill(im, r, Grey);
	b := r.max.x - r.min.x;
	if(b < 4)
		return;
	b2 := b/2;
	bm2 := b-ReliefBd;
	p := array[3] of Point;
	col012, col20 : ref Image;
	d := colorimage(DarkGrey);
	l := colorimage(White);
	case kind {
	TRIup =>
		p[0] = Point(b2, ReliefBd);
		p[1] = Point(bm2,bm2);
		p[2] = Point(ReliefBd,bm2);
		col012 = d;
		col20 = l;
	TRIdown =>
		p[0] = Point(b2,bm2);
		p[1] = Point(ReliefBd,ReliefBd);
		p[2] = Point(bm2,ReliefBd);
		col012 = l;
		col20 = d;
	TRIleft =>
		p[0] = Point(bm2, ReliefBd);
		p[1] = Point(bm2, bm2);
		p[2] = Point(ReliefBd,b2);
		col012 = d;
		col20 = l;
	TRIright =>
		p[0] = Point(ReliefBd,bm2);
		p[1] = Point(ReliefBd,ReliefBd);
		p[2] = Point(bm2,b2);
		col012 = l;
		col20 = d;
	}
	if(style == ReliefSunk) {
		t := col012;
		col012 = col20;
		col20 = t;
	}
	for(i := 0; i < 3; i++)
		p[i] = p[i].add(r.min);
	im.fillpoly(p, ~0, colorimage(Grey), zp);
	im.line(p[0], p[1], 0, 0, ReliefBd/2, col012, zp);
	im.line(p[1], p[2], 0, 0, ReliefBd/2, col012, zp);
	im.line(p[2], p[0], 0, 0, ReliefBd/2, col20, zp);
}

abs(a: int) : int
{
	if(a < 0)
		return -a;
	return a;
}

Frame.new() : ref Frame
{
	f := ref Frame;
	f.parent = nil;
	f.cim = nil;
	f.r = Rect(zp, zp);
	f.animpid = 0;
	f.reset();
	return f;
}

Frame.newkid(parent: ref Frame, ki: ref Kidinfo, r: Rect) : ref Frame
{
	f := ref Frame;
	f.parent = parent;
	f.cim = parent.cim;
	f.r = r;
	f.animpid = 0;
	f.reset();
	f.src = ki.src;
	f.name = ki.name;
	f.marginw = ki.marginw;
	f.marginh = ki.marginh;
	f.framebd = ki.framebd;
	f.flags = ki.flags;
	return f;
}

# Note: f.parent, f.cim and f.r should not be reset
# And if f.parent is true, don't reset params set in frameset.
Frame.reset(f: self ref Frame)
{
	f.id = ++frameid;
	f.doc = nil;
	if(f.parent == nil) {
		f.src = nil;
		f.name = "";
		f.marginw = FRMARGIN;
		f.marginh = FRMARGIN;
		f.framebd = 0;
		f.flags = FRvscrollauto | FRhscrollauto;
	}
	f.layout = nil;
	f.sublays = nil;
	f.sublayid = 0;
	f.controls = nil;
	f.controlid = 0;
	f.cr = f.r;
	f.isdirty = 1;
	f.dirtyr = f.cr;
	f.viewr = Rect(zp, zp);
	f.totalr = f.viewr;
	f.vscr = nil;
	f.hscr = nil;
	hadkids := (f.kids != nil);
	f.kids = nil;
	if(f.animpid != 0)
		CU->kill(f.animpid, 0);
	if(J != nil && hadkids)
		J->frametreechanged(f);
	f.animpid = 0;
}

Frame.dirty(f: self ref Frame, r: Draw->Rect)
{
	if (f.isdirty)
		f.dirtyr= f.dirtyr.combine(r);
	else {
		f.dirtyr = r;
		f.isdirty = 1;
	}
}

Frame.addcontrol(f: self ref Frame, c: ref Control) : int
{
	if(len f.controls <= f.controlid) {
		newcontrols := array[len f.controls + 30] of ref Control;
		newcontrols[0:] = f.controls;
		f.controls = newcontrols;
	}
	f.controls[f.controlid] = c;
	ans := f.controlid++;
	return ans;
}

Frame.xscroll(f: self ref Frame, kind, val: int)
{
	newx := f.viewr.min.x;
	case kind {
	CAscrollpage =>
		newx += val*(f.cr.dx()*8/10);
	CAscrollline =>
		newx += val*f.cr.dx()/10;
	CAscrolldelta =>
		newx += val;
	CAscrollabs =>
		newx = val;
	}
	f.scrollabs(Point(newx, f.viewr.min.y));
}

# Don't actually scroll by "page" and "line",
# But rather, 80% and 10%, which give more
# context in the first case, and more motion
# in the second.
Frame.yscroll(f: self ref Frame, kind, val: int)
{
	newy := f.viewr.min.y;
	case kind {
	CAscrollpage =>
		newy += val*(f.cr.dy()*8/10);
	CAscrollline =>
		newy += val*f.cr.dy()/20;
	CAscrolldelta =>
		newy += val;
	CAscrollabs =>
		newy = val;
	}
	f.scrollabs(Point(f.viewr.min.x, newy));
}

Frame.scrollrel(f : self ref Frame, p : Point)
{
	(x, y) := p;
	x += f.viewr.min.x;
	y += f.viewr.min.y;
	f.scrollabs(f.viewr.min.add(p));
}

Frame.scrollabs(f : self ref Frame, p : Point)
{
	(x, y) := p;
	lay := f.layout;
	margin := 0;
	if (lay != nil)
		margin = lay.margin;
	x = max(0, min(x, f.totalr.max.x));
	y = max(0, min(y, f.totalr.max.y + margin - f.cr.dy()));
	(oldx, oldy) := f.viewr.min;
	if (oldx != x || oldy != y) {
		f.viewr.min = (x, y);
		fixframegeom(f);
		# blit scroll
		dx := f.viewr.min.x - oldx;
		dy := f.viewr.min.y - oldy;
		origin := f.lptosp(zp);
		destr := f.viewr.addpt(origin);
		srcpt := destr.min.add((dx, dy));
		oclipr := f.cim.clipr;
		f.cim.clipr = f.cr;
		f.cim.drawop(destr, f.cim, nil, srcpt, Draw->S);
		if (dx > 0)
			f.dirty(Rect((f.viewr.max.x - dx, f.viewr.min.y), f.viewr.max));
		else if (dx < 0)
			f.dirty(Rect(f.viewr.min, (f.viewr.min.x - dx, f.viewr.max.y)));

		if (dy > 0)
			f.dirty(Rect((f.viewr.min.x, f.viewr.max.y-dy), f.viewr.max));
		else if (dy < 0)
			f.dirty(Rect(f.viewr.min, (f.viewr.max.x, f.viewr.min.y-dy)));
#f.cim.draw(destr, display.white, nil, zp);
		drawall(f);
		f.cim.clipr = oclipr;
	}
}

# Convert layout coords (where (0,0) is top left of layout)
# to screen coords (i.e., coord system of mouse, f.cr, etc.)
Frame.sptolp(f: self ref Frame, sp: Point) : Point
{
	return f.viewr.min.add(sp.sub(f.cr.min));
}

# Reverse translation of sptolp
Frame.lptosp(f: self ref Frame, lp: Point) : Point
{
	return lp.add(f.cr.min.sub(f.viewr.min));
}

# Return Loc of Item or Scrollbar containing p (p in screen coords)
# or item it, if that is not nil.
Frame.find(f: self ref Frame, p: Point, it: ref Item) : ref Loc
{
	return framefind(Loc.new(), f, p, it);
}

# Find it (if non-nil) or place where p is (known to be inside f's layout).
framefind(loc: ref Loc, f: ref Frame, p: Point, it: ref Item) : ref Loc
{
	loc.add(LEframe, f.r.min);
	loc.le[loc.n-1].frame = f;
	if(it == nil) {
		if(f.vscr != nil && p.in(f.vscr.r)) {
			loc.add(LEcontrol, f.vscr.r.min);
			loc.le[loc.n-1].control = f.vscr;
			loc.pos = p.sub(f.vscr.r.min);
			return loc;
		}
		if(f.hscr != nil && p.in(f.hscr.r)) {
			loc.add(LEcontrol, f.hscr.r.min);
			loc.le[loc.n-1].control = f.hscr;
			loc.pos = p.sub(f.hscr.r.min);
			return loc;
		}
	}
	if(it != nil || p.in(f.cr)) {
		lay := f.layout;
		if(f.kids != nil) {
			for(fl := f.kids; fl != nil; fl = tl fl) {
				kf := hd fl;
				try := framefind(loc, kf, p, it);
				if(try != nil)
					return try;
			}
		}
		else if(lay != nil)
			return layfind(loc, f, lay, f.lptosp(zp), p, it);
	}
	return nil;
}

# Find it (if non-nil) or place where p is (known to be inside f's layout).
# p (in screen coords), lay offset by origin also in screen coords
layfind(loc: ref Loc, f: ref Frame, lay: ref Lay, origin, p: Point, it: ref Item) : ref Loc
{
	for(flist := lay.floats; flist != nil; flist = tl flist) {
		fl := hd flist;
		fymin := fl.y+origin.y;
		fymax := fymin + fl.item.height;
		inside := 0;
		xx : int;
		if(it != nil || (fymin <= p.y && p.y < fymax)) {
			xx = origin.x + lay.margin;
			if(fl.side == Aright) {
				xx -= fl.x;
				xx += lay.targetwidth;
#				if(lay == f.layout)
#					xx = origin.x + (f.cr.dx() - lay.margin) - fl.x;
##					xx += f.cr.dx() - fl.x;
#				else
#					xx += lay.width - fl.x;
			}
			else
				xx += fl.x;
			if(p.x >= xx && p.x < xx+fl.item.width)
					inside = 1;
		}
		fp := Point(xx,fymin);
		match := 0;
		if(it != nil) {
			pick fi := fl.item {
			Itable =>
				loc.add(LEitem, fp);
				loc.le[loc.n-1].item = fl;
				loc.pos = p.sub(fp);
				lloc := tablefind(loc, f, fi, fp, p, it);
				if(lloc != nil)
					return lloc;
			Iimage =>
				match = (it == fl || it == fl.item);
			}
		}
		if(match || inside) {
			loc.add(LEitem, fp);
			loc.le[loc.n-1].item = fl;
			loc.pos = p.sub(fp);
			if(it == fl.item) {
				loc.add(LEitem, fp);
				loc.le[loc.n-1].item = fl.item;
			}
			if(inside) {
				pick fi := fl.item {
				Itable =>
					loc = tablefind(loc, f, fi, fp, p, it);
				}
			}
			return loc;
		}
	}
	for(l :=lay.start; l != nil; l = l.next) {
		o := origin.add(l.pos);
		if(it != nil || (o.y <= p.y && p.y < o.y+l.height)) {
			lloc := linefind(loc, f, l, o, p, it);
			if(lloc != nil)
				return lloc;
			if(it == nil && o.y + l.height >= p.y)
				break;
		}
	}
	return nil;
}

# p (in screen coords), line at o, also in screen coords
linefind(loc: ref Loc, f: ref Frame, l: ref Line, o, p: Point, it: ref Item) : ref Loc
{
	loc.add(LEline, o);
	loc.le[loc.n-1].line = l;
	x := o.x;
	y := o.y;
	inside := 0;
	for(i := l.items; i != nil; i = i.next) {
		if(it != nil || (x <= p.x && p.x < x+i.width)) {
			yy := y;
			h := 0;
			pick pi := i {
			Itext =>
				fnt := fonts[pi.fnt].f;
				yy += l.ascent - fnt.ascent + (int pi.voff) - Voffbias;
				h = fnt.height;
			Irule =>
				h = pi.size;
			Iimage =>
				yy = y;
				if(pi.align == Abottom)
					yy += l.ascent - pi.imheight;
				else if(pi.align == Amiddle)
					yy += l.ascent - (pi.imheight/2);
				h = pi.imheight;
			Iformfield =>
				h = pi.height;
				yy += l.ascent - pi.ascent;
				if(it != nil) {
					if(it == pi.formfield.image) {
						loc.add(LEitem, Point(x,yy));
						loc.le[loc.n-1].item = i;
						loc.add(LEitem, Point(x,yy));
						loc.le[loc.n-1].item = it;
						loc.pos = zp;	# doesn't matter, its an 'it' test
						return loc;
					}
				}
				else if(yy < p.y && p.y < yy+h && pi.formfield.ctlid >= 0) {
					loc.add(LEcontrol, Point(x,yy));
					loc.le[loc.n-1].control = f.controls[pi.formfield.ctlid];
					loc.pos = p.sub(Point(x,yy));
					return loc;
				}
			Itable =>
				lloc := tablefind(loc, f, pi, Point(x,y), p, it);
				if(lloc != nil)
					return lloc;
				# else leave h==0 so p test will fail

			# floats were handled separately. nulls can be picked by 'it' test
			# leave h==0, so p test will fail
			}
			if(it == i || (it == nil && yy <= p.y && p.y < yy+h)) {
				loc.add(LEitem, Point(x,yy));
				loc.le[loc.n-1].item = i;
				loc.pos = p.sub(Point(x,yy));
				return loc;
			}
			if(it == nil)
				return nil;
		}
		x += i.width;
		if(it == nil && x >= p.x)
			break;
	}
	loc.n--;
	return nil;
}

tablefind(loc: ref Loc, f: ref Frame, ti: ref Item.Itable, torigin: Point, p: Point, it: ref Item) : ref Loc
{
	loc.add(LEitem, torigin);
	loc.le[loc.n-1].item = ti;
	t := ti.table;
	(hsp, vsp, pad, bd, cbd, hsep, vsep) := tableparams(t);
	if(t.caption_lay >= 0) {
		caplay := f.sublays[t.caption_lay];
		capy := torigin.y;
		if(t.caption_place == Abottom)
			capy += t.toth-t.caph+vsp;
		lloc := layfind(loc, f, caplay, Point(torigin.x,capy), p, it);
		if(lloc != nil)
			return lloc;
	}
	for(cl := t.cells; cl != nil; cl = tl cl) {
		c := hd cl;
		if(c.layid == -1 || c.layid >= len f.sublays)
			continue;
		clay := f.sublays[c.layid];
		if(clay == nil)
			continue;
		cx := torigin.x + t.cols[c.col].pos.x;
		cy := torigin.y + t.rows[c.row].pos.y;
		wd := cellwidth(t, c, hsep);
		ht := cellheight(t, c, vsep);
		if(it == nil && !p.in(Rect(Point(cx,cy),Point(cx+wd,cy+ht))))
			continue;
		if(c.align.valign != Atop && c.align.valign != Abaseline) {
			n := ht - clay.height;
			if(c.align.valign == Amiddle)
				cy += n/2;
			else if(c.align.valign == Abottom)
				cy += n;
		}
		loc.add(LEtablecell, Point(cx,cy));
		loc.le[loc.n-1].tcell = c;
		lloc := layfind(loc, f, clay, Point(cx,cy), p, it);
		if(lloc != nil)
			return lloc;
		loc.n--;
		if(it == nil)
			return nil;
	}
	loc.n--;
	return nil;
}

# (called from jscript)
# 'it' is an Iimage item in frame f whose image is to be switched
# to come from the src URL.
#
# For now, assume this is called only after the entire build process
# has finished.  Also, only handle the case where the image has
# been preloaded and is in the cache now.  This isn't right (BUG), but will
# cover most of the cases of extant image swapping, and besides,
# image swapping is mostly cosmetic anyway.
# 
# For now, pay no attention to scaling issues or animation issues.
Frame.swapimage(f: self ref Frame, im: ref Item.Iimage, src: string)
{
	u := U->parse(src);
	if(u.scheme == "")
		return;
	u = U->mkabs(u, f.doc.base);
	# width=height=0 finds u if in cache
	newci := CImage.new(u, nil, 0, 0);
	cachedci := (CU->imcache).look(newci);
	if(cachedci == nil || cachedci.mims == nil)
		return;
	im.ci = cachedci;

	# we're assuming image will have same dimensions
	# as one that is replaced, so no relayout is needed;
	# otherwise need to call haveimage() instead of drawall()
	# Netscape scales replacement image to size of replaced image

	f.dirty(f.totalr);
	drawall(f);
}

Frame.focus(f : self ref Frame, focus, raisex : int)
{
	di := f.doc;
	if (di == nil || (CU->config).doscripts == 0)
		return;
	if (di.evmask && raisex) {
		kind := E->SEonfocus;
		if (!focus)
			kind = E->SEonblur;
		if(di.evmask & kind)
			se := ref E->ScriptEvent(kind, f.id, -1, -1, -1, -1, -1, -1, 0, nil, nil, 0);
	}
}

Control.newff(f: ref Frame, ff: ref B->Formfield) : ref Control
{
	ans : ref Control = nil;
	case ff.ftype {
	Ftext or Fpassword or Ftextarea =>
		nh := ff.size;
		nv := 1;
		linewrap := 0;
		if(ff.ftype == Ftextarea) {
			nh = ff.cols;
			nv = ff.rows;
			linewrap = 1;
		}
		ans = Control.newentry(f, nh, nv, linewrap);
		if(ff.ftype == Fpassword)
			ans.flags |= CFsecure;
		ans.entryset(ff.value);
	Fcheckbox or Fradio =>
		ans = Control.newcheckbox(f, ff.ftype==Fradio);
		if((ff.flags&B->FFchecked) != byte 0)
			ans.flags |= CFactive;
	Fsubmit or Fimage or Freset or Fbutton =>
		if(ff.image == nil)
			ans = Control.newbutton(f, nil, nil, ff.value, nil, 0, 1);
		else {
			pick i := ff.image {
			Iimage =>
				pic, picmask : ref Image;
				if(i.ci.mims != nil) {
					pic = i.ci.mims[0].im;
					picmask = i.ci.mims[0].mask;
				}
				lab := "";
				if((CU->config).imagelvl == CU->ImgNone) {
					lab = i.altrep;
					i = nil;
				}
				ans = Control.newbutton(f, pic, picmask, lab, i, 0, 0);
			}
		}
	Fselect =>
		n := len ff.options;
		if(n > 0) {
			ao := array[n] of Option;
			l := ff.options;
			for(i := 0; i < n; i++) {
				o := hd l;
				# these are copied, so selected can be used for current state
				ao[i] = *o;
				l = tl l;
			}
			nvis := ff.size;
			ans = Control.newselect(f, nvis, ao);
		}
	Ffile =>
		if(dbg)
			sys->print("warning: unimplemented file form field\n");
	}
	if(ans != nil)
		ans.ff = ff;
	return ans;
}

Control.newscroll(f: ref Frame, isvert, length, breadth: int) : ref Control
{
	# need room for at least two squares and 2 borders of size 2
	if(length < 12) {
		breadth = 0;
		length = 0;
	}
	else if(breadth*2 + 4 > length)
		breadth = (length - 4) / 2;
	maxpt : Point;
	flags := CFenabled;
	if(isvert) {
		maxpt = Point(breadth, length);
		flags |= CFscrvert;
	}
	else
		maxpt = Point(length, breadth);
	return ref Control.Cscrollbar(f, nil, Rect(zp,maxpt), flags, nil, 0, 0, 1, 0, nil, (0, 0));
}

Control.newentry(f: ref Frame, nh, nv, linewrap: int) : ref Control
{
	w := ctlcharspace*nh + 2*ENTHMARGIN;
	h := ctllinespace*nv + 2*ENTVMARGIN;
	scr : ref Control;
	if (linewrap) {
		scr = Control.newscroll(f, 1, h-4, SCRFBREADTH);
		scr.r.addpt(Point(w,0));
		w += SCRFBREADTH;
	}
	ans := ref Control.Centry(f, nil, Rect(zp,Point(w,h)), CFenabled, nil, scr, "", (0, 0), 0, linewrap, 0);
	if (scr != nil) {
		pick pscr := scr {
		Cscrollbar =>
			pscr.ctl = ans;
		}
		scr.scrollset(0, 1, 1, 0, 0);
	}
	return ans;
}

Control.newbutton(f: ref Frame, pic, picmask: ref Image, lab: string, it: ref Item.Iimage, candisable, dorelief: int) : ref Control
{
	dpic, dpicmask: ref Image;
	w := 0;
	h := 0;
	if(pic != nil) {
		w = pic.r.dx();
		h = pic.r.dy();
	}
	else if(it != nil) {
		w = it.imwidth;
		h = it.imheight;
	}
	else {
		w = fonts[CtlFnt].f.width(lab);
		h = ctllinespace;
	}
	if(dorelief) {
		# form image buttons are shown without margins in other browsers
		w += 2*BUTMARGIN;
		h += 2*BUTMARGIN;
	}
	r := Rect(zp, Point(w,h));
	if(candisable && pic != nil) {
		# make "greyed out" image:
		#	- convert pic to monochrome (ones where pic is non-white)
		#	- draw pic in White, then DarkGrey shifted (-1,-1) and use
		#	    union of those two areas as mask
		dpicmask = display.newimage(pic.r, Draw->GREY1, 0, D->White);
		dpic = display.newimage(pic.r, pic.chans, 0, D->White);
		dpic.draw(dpic.r, colorimage(White), pic, zp);
		dpicmask.draw(dpicmask.r, display.black, pic, zp);
		dpic.draw(dpic.r.addpt(Point(-1,-1)), colorimage(DarkGrey), pic, zp);
		dpicmask.draw(dpicmask.r.addpt(Point(-1,-1)), display.black, pic, zp);
	}
	b := ref Control.Cbutton(f, nil, r, CFenabled, nil, pic, picmask, dpic, dpicmask, lab, dorelief);
	return b;
}

Control.newcheckbox(f: ref Frame, isradio: int) : ref Control
{
	return ref Control.Ccheckbox(f, nil, Rect((0,0),(CBOXWID,CBOXHT)), CFenabled, nil, isradio);
}

Control.newselect(f: ref Frame, nvis: int, options: array of B->Option) : ref Control
{
	nvis = min(5, len options);
	if (nvis < 1)
		nvis = 1;
	fnt := fonts[CtlFnt].f;
	w := 0;
	first := -1;
	for(i := 0; i < len options; i++) {
		if (first == -1 && options[i].selected)
			first = i;
		w = max(w, fnt.width(options[i].display));
	}
	if (first == -1)
		first = 0;
	if (len options -nvis > 0 && len options - nvis < first)
		first = len options - nvis;
	w += 2*SELMARGIN;
	h := ctllinespace*nvis + 2*SELMARGIN;
	scr: ref Control;
	if (nvis > 1 && nvis < len options) {
		scr = Control.newscroll(f, 1, h, SCRFBREADTH);
		scr.r.addpt(Point(w,0));
	}
	if (nvis < len options)
		w += SCRFBREADTH;
	ans := ref Control.Cselect(f, nil, Rect(zp, Point(w,h)), CFenabled, nil, nil, scr, nvis, first, options);
	if(scr != nil) {
		pick pscr := scr {
		Cscrollbar =>
			pscr.ctl = ans;
		}
		scr.scrollset(first, first+nvis, len options, len options, 0);
	}
	return ans;
}

Control.newlistbox(f: ref Frame, nrow, ncol: int, options: array of B->Option) : ref Control
{
	fnt := fonts[CtlFnt].f;
	w := charspace*ncol + 2*SELMARGIN;
	h := fnt.height*nrow + 2*SELMARGIN;

	vscr: ref Control = nil;
	#if(nrow < len options) {
		vscr = Control.newscroll(f, 1, (h-4)+SCRFBREADTH, SCRFBREADTH);
		vscr.r.addpt(Point(w-SCRFBREADTH,0));
		w += SCRFBREADTH;
	#}

	maxw := 0;
	for(i := 0; i < len options; i++)
		maxw = max(maxw, fnt.width(options[i].display));

	hscr: ref Control = nil;
	#if(w < maxw) {
		# allow for border (inset(2))
		hscr = Control.newscroll(f, 0, (w-4)-SCRFBREADTH, SCRFBREADTH);
		hscr.r.addpt(Point(0, h-SCRBREADTH));
		h += SCRFBREADTH;
	#}

	ans := ref Control.Clistbox(f, nil, Rect(zp, Point(w,h)), CFenabled, nil, hscr, vscr, nrow, 0, 0, maxw/charspace, options, nil);
	if(vscr != nil) {
		pick pscr := vscr {
		Cscrollbar =>
			pscr.ctl = ans;
		}
		vscr.scrollset(0, nrow, len options, len options, 0);
	}
	if(hscr != nil) {
		pick pscr := hscr {
		Cscrollbar =>
			pscr.ctl = ans;
		}
		hscr.scrollset(0, w-SCRFBREADTH, maxw, 0, 0);
	}
	return ans;	
}

Control.newanimimage(f: ref Frame, cim: ref CU->CImage, bg: Background) : ref Control
{
	return ref Control.Canimimage(f, nil, Rect((0,0),(cim.width,cim.height)), 0, nil, cim, 0, 0, big 0, bg);
}

Control.newlabel(f: ref Frame, s: string) : ref Control
{
	w := fonts[DefFnt].f.width(s);
	h := ctllinespace + 2*ENTVMARGIN;	# give it same height as an entry box
	return ref Control.Clabel(f, nil, Rect(zp,Point(w,h)), 0, nil, s);
}

Control.disable(c: self ref Control)
{
	if(c.flags & CFenabled) {
		win := c.f.cim;
		c.flags &= ~CFenabled;
		if(c.f.cim != nil)
			c.draw(1);
	}
}

Control.enable(c: self ref Control)
{
	if(!(c.flags & CFenabled)) {
		c.flags |= CFenabled;
		if(c.f.cim != nil)
			c.draw(1);
	}
}

changeevent(c: ref Control)
{
	onchange := 0;
	pick pc := c {
	Centry =>
		onchange = pc.onchange;
		pc.onchange = 0;
# this code reproduced Navigator 2 bug
# changes to Select Formfield selection only resulted in onchange event upon
# loss of focus.  Now handled by domouse() code so event can be raised
# immediately
#	Cselect =>
#		onchange = pc.onchange;
#		pc.onchange = 0;
	}
	if(onchange && (c.ff.evmask & E->SEonchange)) {
		se := ref E->ScriptEvent(E->SEonchange, c.f.id, c.ff.form.formid, c.ff.fieldid, -1, -1, -1, -1, 1, nil, nil, 0);
		J->jevchan <-= se;
	}
}

blurfocusevent(c: ref Control, kind, raisex: int)
{
	if((CU->config).doscripts && c.ff != nil && c.ff.evmask) {
		if(kind == E->SEonblur)
			changeevent(c);
		if (!raisex || !(c.ff.evmask & kind))
			return;
		se := ref E->ScriptEvent(kind, c.f.id, c.ff.form.formid, c.ff.fieldid, -1, -1, -1, -1, 0, nil, nil, 0);
		J->jevchan <-= se;
	}
}

Control.losefocus(c: self ref Control, raisex: int)
{
	if(c.flags & CFhasfocus) {
		c.flags &= ~CFhasfocus;
		if(c.f.cim != nil) {
			blurfocusevent(c, E->SEonblur, raisex);
			c.draw(1);
		}
	}
}

Control.gainfocus(c: self ref Control, raisex: int)
{
	if(!(c.flags & CFhasfocus)) {
		c.flags |= CFhasfocus;
		if(c.f.cim != nil) {
			blurfocusevent(c, E->SEonfocus, raisex);
			c.draw(1);
		}
		G->clientfocus();
	}
}

Control.scrollset(c: self ref Control, v1, v2, vmax, nsteps, draw: int)
{
	pick sc := c {
	Cscrollbar =>
		if(v1 < 0)
			v1 = 0;
		if(v2 > vmax)
			v2 = vmax;
		if(v1 > v2)
			v1 = v2;
		if(v1 == 0 && v2 == vmax) {
			sc.mindelta = 1;
			sc.deltaval = 0;
			sc.top = 0;
			sc.bot = 0;
		}
		else {
			length, breadth: int;
			if(sc.flags&CFscrvert) {
				length = sc.r.max.y - sc.r.min.y;
				breadth = sc.r.max.x - sc.r.min.x;
			}
			else {
				length = sc.r.max.x - sc.r.min.x;
				breadth = sc.r.max.y - sc.r.min.y;
			}
			l := length - (2*breadth + MINSCR);
			if(l <= 0)
				l = 1;
			if(l < 0)
				raise "EXInternal: negative scrollbar trough";
			sc.top = l*v1/vmax;
			sc.bot = l*(vmax-v2)/vmax;
			if (nsteps == 0)
				sc.mindelta = 1;
			else
				sc.mindelta = max(1, length/nsteps);
			sc.deltaval = max(1, vmax/(l/sc.mindelta))*SCRDELTASF;
		}
		if(sc.f.cim != nil && draw)
			sc.draw(1);
	}
}

SPECMASK : con 16rf000;
CTRLMASK : con 16r1f;
DEL : con 16r7f;
TAB : con '\t';
CR: con '\n';

Control.dokey(ctl: self ref Control, keychar: int) : int
{
	if(!(ctl.flags&CFenabled))
		return CAnone;
	ans := CAnone;
	pick c := ctl {
	Centry =>
		olds := c.s;
		slen := len c.s;
		(sels, sele) := normalsel(c.sel);
		modified := 0;
		(osels, osele) := (sels, sele);
		case keychar {
			('a' & CTRLMASK) or Keyboard->Home =>
				(sels, sele) = (0, 0);
			('e' & CTRLMASK) or Keyboard->End =>
				(sels, sele) = (slen, slen);
			'f' & CTRLMASK or Keyboard->Right =>
				if(sele < slen)
					(sels, sele) = (sele+1, sele+1);
			'b' & CTRLMASK or Keyboard->Left =>
				if(sels > 0)
					(sels, sele) = (sels-1, sels-1);
			Keyboard->Up =>
				if (c.linewrap)
					sels = sele = entryupdown(c, sels, -1);
			Keyboard->Down =>
				if (c.linewrap)
					sels = sele = entryupdown(c, sele, 1);
			'u' & CTRLMASK =>
				entrydelrange(c, 0, slen);
				modified = 1;
				(sels, sele) = c.sel;
			'c' & CTRLMASK =>
				entrysetsnarf(c);
			'v' & CTRLMASK =>
				entryinsertsnarf(c);
				modified = 1;
				(sels, sele) = c.sel;
			'h' & CTRLMASK or DEL=>
				if (sels != sele) {
					entrydelrange(c, sels, sele);
					modified = 1;
				} else if(sels > 0) {
					entrydelrange(c, sels-1, sels);
					modified = 1;
				}
				(sels, sele) = c.sel;
			Keyboard->Del =>
				if (sels != sele) {
					entrydelrange(c, sels, sele);
					modified = 1;
				} else if(sels < len c.s) {
					entrydelrange(c, sels, sels+1);
					modified = 1;
				}
				(sels, sele) = c.sel;
			TAB =>
				ans = CAtabkey;
			* =>
				if ((keychar & SPECMASK) == Keyboard->Spec)
					# ignore all other special keys
					break;
				if(keychar == CR) {
					if(c.linewrap)
						keychar = '\n';
					else
						ans = CAreturnkey;
				}
				if(keychar > CTRLMASK || (keychar == '\n' && c.linewrap)) {
					if (sels != sele) {
						entrydelrange(c, sels, sele);
						(sels, sele) = c.sel;
					}
					slen = len c.s;
					c.s[slen] = 0;	# expand string by 1 char
					for(k := slen; k > sels; k--)
						c.s[k] = c.s[k-1];
					c.s[sels] = keychar;
					(sels, sele) = (sels+1, sels+1);
					modified = 1;
				}
		}
		c.sel = (sels, sele);
		if(osels != sels || osele != sele || modified) {
			entryscroll(c);
			c.draw(1);
		}
		if (c.s != olds)
			c.onchange = 1;
	}
	return ans;
}

Control.domouse(ctl: self ref Control, p: Point, mtype: int, oldgrab : ref Control) : (int, ref Control)
{
	up := (mtype == E->Mlbuttonup || mtype == E->Mldrop);
	down := (mtype == E->Mlbuttondown);
	drag := (mtype == E->Mldrag);
	hold := (mtype == E->Mhold);
	move := (mtype == E->Mmove);

	# any button actions stop auto-repeat
	# it's up to the individual controls to re-instate it
	if (!move)
		E->autorepeat(nil, 0, 0);

	if(!(ctl.flags&CFenabled))
		return (CAnone, nil);
	ans := CAnone;
	changed := 0;
	newgrab : ref Control;
	grabbed := oldgrab != nil;
	pick c := ctl {
	Cbutton =>
		if(down) {
			c.flags |= CFactive;
			newgrab = c;
			changed = 1;
		}
		else if(move && c.ff == nil) {
			ans = CAflyover;
		}
		else if (drag && grabbed) {
			newgrab = c;
			active := 0;
			if (p.in(c.r))
				active = CFactive;
			if ((c.flags & CFactive) != active)
				changed = 1;
			c.flags = (c.flags & ~CFactive) | active;
		}
		else if(up) {
			if (c.flags & CFactive)
				ans = CAbuttonpush;
			c.flags &= ~CFactive;
			changed = 1;
		}
	Centry =>
		if(c.scr != nil && !grabbed && p.x >= c.r.max.x-SCRFBREADTH) {
			pick scr := c.scr {
			Cscrollbar =>
				return scr.domouse(p, mtype, oldgrab);
			}
		}
		(sels, sele) := c.sel;
		if(mtype == E->Mlbuttonup && grabbed) {
			if (sels != sele)
				ans = CAselected;
		}
		if(down || (drag && grabbed)) {
			newgrab = c;
			x := c.r.min.x+ENTHMARGIN;
			fnt := fonts[CtlFnt].f;
			s := c.s;
			if(c.flags&CFsecure) {
				for(i := 0; i < len s; i++)
					s[i] = '*';
			}
			(osels, osele) := c.sel;
			s1 := " ";
			i := 0;
			iend := len s - 1;
			if(c.linewrap) {
				(lines, linestarts, topline, cursline) := entrywrapcalc(c);
				if(len lines > 1) {
					lineno := topline + (p.y - (c.r.min.y+ENTVMARGIN)) / ctllinespace;
					lineno = min(lineno, len lines -1);
					lineno = max(lineno, 0);

					i = linestarts[lineno];
					iend = i + len lines[lineno] -1;
				}
			} else
				x -= fnt.width(s[:c.left]);
			for(; i <= iend; i++) {
				s1[0] = s[i];
				cx := fnt.width(s1);
				if(p.x < x + cx)
					break;
				x += cx;
			}
			sele = i;

			if (down)
				sels = sele;
			c.sel = (sels, sele);

			if (sels != osels || sele != osele) {
				changed = 1;
				entryscroll(c);
				if (p.x < c.r.min.x + ENTHMARGIN || p.x > c.r.max.x - ENTHMARGIN
				|| p.y < c.r.min.y + ENTVMARGIN || p.y > c.r.max.y - ENTVMARGIN) {
					E->autorepeat(ref (Event.Emouse)(p, mtype), ARTICK, ARTICK);
				}
			}

			if(!(c.flags&CFhasfocus))
				ans = CAkeyfocus;
		}
	Ccheckbox=>
		if(up) {
			if(c.isradio) {
				if(!(c.flags&CFactive)) {
					c.flags |= CFactive;
					changed = 1;
					ans = CAbuttonpush;
					# turn off other radio button
					frm := c.ff.form;
					for(lf := frm.fields; lf != nil; lf = tl lf) {
						ff := hd lf;
						if(ff == c.ff)
							continue;
						if(ff.ftype == Fradio && ff.name==c.ff.name && ff.ctlid >= 0) {
							d := c.f.controls[ff.ctlid];
							if(d.flags&CFactive) {
								d.flags &= ~CFactive;
								d.draw(1);
								break;		# at most one other should be on
							}
						}
					}
				}
			}
			else {
				c.flags ^= CFactive;
				changed = 1;
			}
		}
	Cselect =>
		if (c.nvis == 1 && up && c.popup == nil && c.r.contains(p))
			return (CAdopopup, nil);
		if(c.scr != nil && (grabbed || p.x >= c.r.max.x-SCRFBREADTH)) {
			pick scr := c.scr {
			Cscrollbar =>
				(a, grab) := scr.domouse(p, mtype, oldgrab);
				if (grab != nil)
					grab = c;
				return (a, grab);
			}
			return (ans, nil);
		}
		n := (p.y - (c.r.min.y+SELMARGIN))/ctllinespace + c.first;
		if (n >= c.first && n < c.first+c.nvis) {
			if ((c.ff.flags&B->FFmultiple) != byte 0) {
				if (down) {
					c.options[n].selected ^= 1;
					changed = 1;
				}
			} else if (up || drag) {
				changed = c.options[n].selected == 0;
				c.options[n].selected = 1;
				for(i := 0; i < len c.options; i++) {
					if(i != n)
						c.options[i].selected = 0;
				}
			}
		}
		if (up) {
			if (c.popup != nil)
				ans = CAdonepopup;
			else
				ans = CAchanged;
		}
	Clistbox =>
		if(c.vscr != nil && (c.grab == c.vscr || (!grabbed && p.x >= c.r.max.x-SCRFBREADTH))) {
			c.grab = nil;
			pick vscr := c.vscr {
			Cscrollbar =>
				(a, grab) := vscr.domouse(p, mtype, oldgrab);
				if (grab != nil) {
					c.grab = c.vscr;
					grab = c;
				}
				return (a, grab);
			}
		}
		else if(c.hscr != nil && (c.grab == c.hscr || (!grabbed && p.y >= c.r.max.y-SCRFBREADTH))) {
			c.grab = nil;
			pick hscr := c.hscr {
			Cscrollbar =>
				(a, grab) := hscr.domouse(p, mtype, oldgrab);
				if (grab != nil) {
					c.grab = c.hscr;
					grab = c;
				}
				return (a, grab);
			}
		}
		else if(up) {
			fnt := fonts[CtlFnt].f;
			n := (p.y - (c.r.min.y+SELMARGIN))/fnt.height + c.first;
			if(n >= 0 && n < len c.options) {
				c.options[n].selected ^= 1;
				# turn off other selections
				for(i := 0; i < len c.options; i++) {
					if(i != n)
						c.options[i].selected = 0;
				}
				ans = CAchanged;
				changed = 1;
			}
		}
	Cscrollbar =>
		val := 0;
		v, vmin, vmax, b: int;
		if(c.flags&CFscrvert) {
			v = p.y;
			vmin = c.r.min.y;
			vmax = c.r.max.y;
			b = c.r.dx();
		}
		else {
			v = p.x;
			vmin = c.r.min.x;
			vmax = c.r.max.x;
			b = c.r.dy();
		}
		vsltop := vmin+b+c.top;
		vslbot := vmax-b-c.bot;
		actflags := 0;
		oldactflags := c.flags&CFscrallact;

		if ((down || drag) && !up && !hold)
			newgrab = c;

		if (down) {
			newgrab = c;
			holdval := 0;
			repeat := 1;
			if (v >= vsltop && v < vslbot) {
				holdval = v - vsltop;
				actflags = CFactive;
				repeat = 0;
			}
			if(v < vmin+b) {
				holdval = -1;
				actflags = CFscracta1;
			}
			else if(v < vsltop) {
				holdval = -1;
				actflags = CFscracttr1;
			}
			else if(v >= vmax-b) {
				holdval = 1;
				actflags = CFscracta2;
			}
			else if(v >= vslbot) {
				holdval = 1;
				actflags = CFscracttr2;
			}
			c.holdstate = (actflags, holdval);
			if (repeat) {
				E->autorepeat(ref (Event.Emouse)(p, E->Mhold), ARPAUSE, ARTICK);
			}
		}
		if (drag) {
			(actflags, val) = c.holdstate;
			if (actflags == CFactive) {
				# dragging main scroll widget (relative to top of drag block)
				val = (v - vsltop) - val;
				if(abs(val) >= c.mindelta) {
					ans = CAscrolldelta;
					val = (c.deltaval * (val / c.mindelta))/SCRDELTASF;
				}
			} else {
				E->autorepeat(ref (Event.Emouse)(p, E->Mhold), ARTICK, ARTICK);
			}
		}
		if (up || hold) {
			# set the action according to the hold state
			# Note: main widget (case CFactive) handled by drag
			act := 0;
			(act, val) = c.holdstate;
			case act {
			CFscracta1 or
			CFscracta2 =>
				ans = CAscrollline;
			CFscracttr1 or
			CFscracttr2 =>
				ans = CAscrollpage;
			}
			if (up) {
				c.holdstate = (0, 0);
			} else { # hold
				(actflags, nil) = c.holdstate;
				if (ans != CAnone) {
					E->autorepeat(ref (Event.Emouse)(p, E->Mhold), ARTICK, ARTICK);
					newgrab = c;
				}
			}
		}
		c.flags = (c.flags & ~CFscrallact) | actflags;
		if(ans != CAnone) {
			ftoscroll := c.f;
			if(c.ctl != nil) {
				pick cff := c.ctl {
				Centry =>
					ny := (cff.r.dy() - 2 * ENTVMARGIN) / ctllinespace;
					(nil, linestarts, topline, nil) := entrywrapcalc(cff);
					nlines := len linestarts;
					case ans {
					CAscrollpage =>
						topline += val*ny;
					CAscrollline =>
						topline += val;
					CAscrolldelta =>
#						# insufficient for large number of lines
						topline += val;
#						if(val > 0)
#							topline++;
#						else
#							topline--;
					}
					if (topline+ny >= nlines)
						topline = (nlines-1) - ny;
					if (topline < 0)
						topline = 0;
					cff.left = linestarts[topline];
					c.scrollset(topline, topline+ny, nlines - 1, nlines, 1);
					cff.draw(1);
					return (ans, newgrab);
				Cselect =>
					newfirst := cff.first;
					case ans {
					CAscrollpage =>
						newfirst += val*cff.nvis;
					CAscrollline =>
						newfirst += val;
					CAscrolldelta =>
#						# insufficient for very long select lists
						newfirst += val;
#						if(val > 0)
#							newfirst++;
#						else
#							newfirst--;
					}
					newfirst = max(0, min(newfirst, len cff.options - cff.nvis));
					cff.first = newfirst;
					nopt := len cff.options;
					c.scrollset(newfirst, newfirst+cff.nvis, nopt, nopt, 0);
					cff.draw(1);
					return (ans, newgrab);
				Clistbox =>
					if(c.flags&CFscrvert) {
						newfirst := cff.first;
						case ans {
						CAscrollpage =>
							newfirst += val*cff.nvis;
						CAscrollline =>
							newfirst += val;
						CAscrolldelta =>
							newfirst += val;
#							if(val > 0)
#								newfirst++;
#							else
#								newfirst--;
						}
						newfirst = max(0, min(newfirst, len cff.options - cff.nvis));
						cff.first = newfirst;
						c.scrollset(newfirst, newfirst+cff.nvis, len cff.options, 0, 1);
						# TODO: need redraw only vscr and content
					}
					else {
						hw := cff.maxcol;
						w := (c.r.max.x - c.r.min.x - SCRFBREADTH)/charspace;
						newstart := cff.start;
						case ans {
						CAscrollpage =>
								newstart += val*hw;
						CAscrollline =>
								newstart += val;
						CAscrolldelta =>
							if(val > 0)
								newstart++;
							else
								newstart--;
						}
						if(hw < w)
							newstart = 0;
						else
							newstart = max(0, min(newstart, hw - w));
						cff.start = newstart;
						c.scrollset(newstart, w+newstart, hw, 0, 1);
						# TODO: need redraw only hscr and content
					}
					cff.draw(1);
					return (ans, newgrab);
				}
			}
			else {
				if(c.flags&CFscrvert)
					c.f.yscroll(ans, val);
				else
					c.f.xscroll(ans, val);
			}
			changed = 1;
		}
		else if(actflags != oldactflags) {
			changed = 1;
		}
	}
	if(changed)
		ctl.draw(1);
	return (ans, newgrab);
}

# returns a new popup control
Control.dopopup(ctl: self ref Control): ref Control
{
	sel : ref Control.Cselect;
	pick c := ctl {
	Cselect =>
		if (c.nvis > 1)
			return nil;
		sel = c;
	* =>
		return nil;
	}

	w := sel.r.dx();
	nopt := len sel.options;
	nvis := min(nopt, POPUPLINES);
	first := sel.first;
	if (first + nvis > nopt)
		first = nopt - nvis;
	h := ctllinespace*nvis + 2*SELMARGIN;
	r := Rect(sel.r.min, sel.r.min.add(Point(w, h)));
	popup := G->getpopup(r);
	if (popup == nil)
		return nil;
	scr : ref Control;
	if (nvis < nopt) {
		scr = Control.newscroll(sel.f, 1, h, SCRFBREADTH);
		scr.r.addpt(Point(w,0));
	}
	newsel := ref Control.Cselect(sel.f, sel.ff, r, sel.flags, popup, sel, scr, nvis, first, sel.options);
	if(scr != nil) {
		pick pscr := scr {
		Cscrollbar =>
			pscr.ctl = newsel;
		}
		scr.popup = popup;
		scr.scrollset(first, first+nvis, nopt, nopt, 0);
	}
	newsel.draw(1);
	return newsel;
}

# returns original control for which this was a popup
Control.donepopup(ctl: self ref Control): ref Control
{
	owner: ref Control;
	pick c := ctl {
	Cselect =>
		if (c.owner == nil)
			return nil;
		owner = c.owner;
	* =>
		return nil;
	}
	G->cancelpopup();
	pick c := owner {
	Cselect =>
		for (first := 0; first < len c.options; first++)
			if (c.options[first].selected)
				break;
		if (first == len c.options)
			first = 0;
		c.first = first;
	}
	owner.draw(1);
	return owner;
}


Control.reset(ctl: self ref Control)
{
	pick c := ctl {
	Cbutton =>
		c.flags &= ~CFactive;
	Centry =>
		c.s = "";
		c.sel = (0, 0);
		c.left = 0;
		if(c.ff != nil && c.ff.value != "")
			c.s = c.ff.value;
		if (c.scr != nil)
			c.scr.scrollset(0, 1, 1, 0, 0);
	Ccheckbox=>
		c.flags &= ~CFactive;
		if(c.ff != nil && (c.ff.flags&B->FFchecked) != byte 0)
			c.flags |= CFactive;
	Cselect =>
		nopt := len c.options;
		if(c.ff != nil) {
			l := c.ff.options;
			for(i := 0; i < nopt; i++) {
				o := hd l;
				c.options[i].selected = o.selected;
				l = tl l;
			}
		}
		c.first = 0;
		if(c.scr != nil) {
			c.scr.scrollset(0, c.nvis, nopt, nopt, 0);
		}
	Clistbox =>
		c.first = 0;
		nopt := len c.options;
		if(c.vscr != nil) {
			c.vscr.scrollset(0, c.nvis, nopt, nopt, 0);
		}
		hw := 0;
		for(i := 0; i < len c.options; i++)
			hw = max(hw, fonts[DefFnt].f.width(c.options[i].display)); 
		if(c.hscr != nil) {
			c.hscr.scrollset(0, c.r.max.x, hw, 0, 0); 
		}
	Canimimage =>
		c.cur = 0;
	}
	ctl.draw(0);
}

Control.draw(ctl: self ref Control, flush: int)
{
	win := ctl.f.cim;
	if (ctl.popup != nil)
		win = ctl.popup.image;
	if (win == nil)
		return;
	oclipr := win.clipr;
	clipr := oclipr;
	any: int;
	if (ctl.popup == nil) {
		(clipr, any) = ctl.r.clip(ctl.f.cr);
		if(!any && ctl != ctl.f.vscr && ctl != ctl.f.hscr)
			return;
		win.clipr = clipr;
	}
	pick c := ctl {
	Cbutton =>
		if(c.ff != nil && c.ff.image != nil && c.pic == nil) {
			# check to see if image arrived
			# (dimensions will have been set by checkffsize, if needed;
			# this code is only for when the HTML specified the dimensions)
			pick imi := c.ff.image {
			Iimage =>
				if(imi.ci.mims != nil) {
					c.pic = imi.ci.mims[0].im;
					c.picmask = imi.ci.mims[0].mask;
				}
			}
		}
		if(c.dorelief || c.pic == nil)
			win.draw(c.r, colorimage(Grey), nil, zp);
		if(c.pic != nil) {
			p, m: ref Image;
			if(c.flags & CFenabled) {
				p = c.pic;
				m = c.picmask;
			}
			else {
				p = c.dpic;
				m = c.dpicmask;
			}
			w := p.r.dx();
			h := p.r.dy();
			x := c.r.min.x + (c.r.dx() - w) / 2;
			y := c.r.min.y + (c.r.dy() - h) / 2;
			if((c.flags & CFactive) && c.dorelief) {
				x++;
				y++;
			}
			win.draw(Rect((x,y),(x+w,y+h)), p, m, zp);
		}
		else if(c.label != "") {
			p := c.r.min.add(Point(BUTMARGIN, BUTMARGIN));
			if(c.flags & CFactive)
				p = p.add(Point(1,1));
			win.text(p, colorimage(Black), zp, fonts[CtlFnt].f, c.label);
		}
		if(c.dorelief) {
			relief := ReliefRaised;
			if(c.flags & CFactive)
				relief = ReliefSunk;
			drawrelief(win, c.r.inset(2), relief);
		}
	Centry =>
		win.draw(c.r, colorimage(White), nil, zp);
		insetr := c.r.inset(2);
		drawrelief(win,insetr, ReliefSunk);
		eclipr := c.r;
		eclipr.min.x += ENTHMARGIN;
		eclipr.max.x -= ENTHMARGIN;
		eclipr.min.y += ENTVMARGIN;
		eclipr.max.y -= ENTVMARGIN;
#		if (c.scr != nil)
#			eclipr.max.x -= SCRFBREADTH;
		(eclipr, any) = clipr.clip(eclipr);
		win.clipr = eclipr;
		p := c.r.min.add(Point(ENTHMARGIN,ENTVMARGIN));
		s := c.s;
		fnt := fonts[CtlFnt].f;
		if(c.left > 0)
			s = s[c.left:];
		if(c.flags&CFsecure) {
			for(i := 0; i < len s; i++)
				s[i] = '*';
		}

		(sels, sele) := normalsel(c.sel);
		(sels, sele) = (sels-c.left, sele-c.left);

		lines : array of string;
		linestarts : array of int;
		textw := c.r.dx()-2*ENTHMARGIN;
		if (c.scr != nil) {
			textw -= SCRFBREADTH;
			c.scr.r = c.scr.r.subpt(c.scr.r.min);
			c.scr.r = c.scr.r.addpt(Point(insetr.max.x-SCRFBREADTH,insetr.min.y));
			c.scr.draw(0);
		}
		if (c.linewrap)
			(lines, linestarts) = wrapstring(fnt, s, textw);
		else
			(lines, linestarts) = (array [] of {s}, array [] of {0});

		q := p;
		black := colorimage(Black);
		white := colorimage(White);
		navy := colorimage(Navy);
		nlines := len lines;
		for (n := 0; n < nlines; n++) {
			segs : list of (int, int, int);
			# only show cursor or selection if we have focus
			if (c.flags & CFhasfocus)
				segs = selsegs(len lines[n], sels-linestarts[n], sele-linestarts[n]);
			else
				segs = (0, len lines[n], 0) :: nil;
			for (; segs != nil; segs = tl segs) {
				(ss, se, sel) := hd segs;
				txt := lines[n][ss:se];
				w := fnt.width(txt);
				txtcol : ref Image;
				if (!sel)
					txtcol = black;
				else {
					txtcol = white;
					bgcol := navy;
					if (n < nlines-1 && sele >= linestarts[n+1])
						w = (p.x-q.x) + textw;
					selr := Rect((q.x, q.y-1), (q.x+w, q.y+ctllinespace+1));
					if (selr.dx() == 0) {
						# empty selection - assume cursor
						bgcol = black;
						selr.max.x = selr.min.x + 2;
					}
					win.draw(selr, bgcol, nil, zp);
				}
				if (se > ss)
					win.text(q, txtcol, zp, fnt, txt);
				q.x += w;
			}
			q = (p.x, q.y + ctllinespace);
		}
	Ccheckbox=>
		win.draw(c.r, colorimage(White), nil, zp);
		if(c.isradio) {
			a := CBOXHT/2;
			a1 := a-1;
			cen := Point(c.r.min.x+a,c.r.min.y+a);
			win.ellipse(cen, a1, a1, 1, colorimage(DarkGrey), zp);
			win.arc(cen, a, a, 0, colorimage(Black), zp, 45, 180);
			win.arc(cen, a, a, 0, colorimage(Grey), zp, 225, 180);
			if(c.flags&CFactive)
				win.fillellipse(cen, 2, 2, colorimage(Black), zp);
		}
		else {
			ir := c.r.inset(2);
			ir.min.x += CBOXWID-CBOXHT;
			ir.max.x -= CBOXWID-CBOXHT;
			drawrelief(win, ir, ReliefSunk);
			if(c.flags&CFactive) {
				p1 := Point(ir.min.x, ir.min.y);
				p2 := Point(ir.max.x, ir.max.y);
				p3 := Point(ir.max.x, ir.min.y);
				p4 := Point(ir.min.x, ir.max.y);
				win.line(p1, p2, D->Endsquare, D->Endsquare, 0, colorimage(Black), zp);
				win.line(p3, p4, D->Endsquare, D->Endsquare, 0, colorimage(Black), zp);
			}
		}
	Cselect =>
		black := colorimage(Black);
		white := colorimage(White);
		navy := colorimage(Navy);
		win.draw(c.r, white, nil, zp);
		drawrelief(win, c.r.inset(2), ReliefSunk);
		ir := c.r.inset(SELMARGIN);
		p := ir.min;
		fnt := fonts[CtlFnt].f;
		drawsel := c.nvis > 1;
		for(i := c.first; i < len c.options && i < c.first+c.nvis; i++) {
			if(drawsel && c.options[i].selected) {
				maxx := ir.max.x;
				if (c.scr != nil)
					maxx -= SCRFBREADTH;
				r := Rect((p.x-SELMARGIN,p.y),(maxx,p.y+ctllinespace));
				win.draw(r, navy, nil, zp);
				win.text(p, white, zp, fnt, c.options[i].display);
			}
			else {
				win.text(p, black, zp, fnt, c.options[i].display);
			}
			p.y += ctllinespace;
		}
		if (c.nvis == 1 && len c.options > 1) {
			# drop down select list - draw marker (must be same width as scroll bar)
			r := Rect((ir.max.x - SCRFBREADTH, ir.min.y), ir.max);
			drawtriangle(win, r, TRIdown, ReliefRaised);
		} 
		if(c.scr != nil) {
			c.scr.r = Rect((ir.max.x - SCRFBREADTH, ir.min.y), ir.max);
			c.scr.draw(0);
		}
	Clistbox =>
		black := colorimage(Black);
		white := colorimage(White);
		navy := colorimage(Navy);
		win.draw(c.r, white, nil, zp);
		insetr := c.r.inset(2);
		#drawrelief(win, c.r.inset(2), ReliefSunk);
		ir := c.r.inset(SELMARGIN);
		p := ir.min;
		fnt := fonts[CtlFnt].f;
		for(i := c.first; i < len c.options && i < c.first+c.nvis; i++) {
			txt := "";
			if (c.start < len c.options[i].display)
				txt = c.options[i].display[c.start:];
			if(c.options[i].selected) {
				r := Rect((p.x-SELMARGIN,p.y),(c.r.max.x-SCRFBREADTH,p.y+fnt.height));
				win.draw(r, navy, nil, zp);
				win.text(p, white, zp, fnt, txt);
			}
			else {
 				win.text(p, black, zp, fnt, txt);
			}
			p.y +=fnt.height;
		}
		if(c.vscr != nil) {
			c.vscr.r = c.vscr.r.subpt(c.vscr.r.min);
			c.vscr.r = c.vscr.r.addpt(Point(insetr.max.x-SCRFBREADTH,insetr.min.y));
			c.vscr.draw(0);
 		}
 		if(c.hscr != nil) {
			c.hscr.r = c.hscr.r.subpt(c.hscr.r.min);
			c.hscr.r = c.hscr.r.addpt(Point(insetr.min.x, insetr.max.y-SCRFBREADTH));
 			c.hscr.draw(0);
		}
		drawrelief(win, insetr, ReliefSunk);

	Cscrollbar =>
		# Scrollbar components: arrow 1 (a1), trough 1 (t1), slider (s), trough 2 (t2), arrow 2 (a2)
		x := c.r.min.x;
		y := c.r.min.y;
		ra1, rt1, rs, rt2, ra2: Rect;
		b, l, a1kind, a2kind: int;
		if(c.flags&CFscrvert) {
			l = c.r.max.y - c.r.min.y;
			b = c.r.max.x - c.r.min.x;
			xr := x+b;
			yt1 := y+b;
			ys := yt1+c.top;
			yb := y+l;
			ya2 := yb-b;
			yt2 := ya2-c.bot;
			ra1 = Rect(Point(x,y),Point(xr,yt1));
			rt1 = Rect(Point(x,yt1),Point(xr,ys));
			rs = Rect(Point(x,ys),Point(xr,yt2));
			rt2 = Rect(Point(x,yt2),Point(xr,ya2));
			ra2 = Rect(Point(x,ya2),Point(xr,yb));
			a1kind = TRIup;
			a2kind = TRIdown;
		}
		else {
			l = c.r.max.x - c.r.min.x;
			b = c.r.max.y - c.r.min.y;
			yb := y+b;
			xt1 := x+b;
			xs := xt1+c.top;
			xr := x+l;
			xa2 := xr-b;
			xt2 := xa2-c.bot;
			ra1 = Rect(Point(x,y),Point(xt1,yb));
			rt1 = Rect(Point(xt1,y),Point(xs,yb));
			rs = Rect(Point(xs,y),Point(xt2,yb));
			rt2 = Rect(Point(xt2,y),Point(xa2,yb));
			ra2 = Rect(Point(xa2,y),Point(xr,yb));
			a1kind = TRIleft;
			a2kind = TRIright;
		}
		a1relief := ReliefRaised;
		if(c.flags&CFscracta1)
			a1relief = ReliefSunk;
		a2relief := ReliefRaised;
		if(c.flags&CFscracta2)
			a2relief = ReliefSunk;
		drawtriangle(win, ra1, a1kind, a1relief);
		drawtriangle(win, ra2, a2kind, a2relief);
		drawfill(win, rt1, Grey);
		rs = rs.inset(2);
		drawfill(win, rs, Grey);
		rsrelief := ReliefRaised;
		if(c.flags&CFactive)
			rsrelief = ReliefSunk;
		drawrelief(win, rs, rsrelief);
		drawfill(win, rt2, Grey);
	Canimimage =>
		i := c.cur;
		if(c.redraw)
			i = 0;
		else if(i > 0) {
			iprev := i-1;
			if(c.cim.mims[iprev].bgcolor != -1) {
				i = iprev;
				# get i back to before all "reset to previous"
				# images (which will be skipped in following
				# image drawing loop)
				while(i > 0 && c.cim.mims[i].bgcolor == -2)
					i--;
			}
		}
		bgi := colorimage(c.bg.color);
		if(c.bg.image != nil && c.bg.image.ci != nil && len c.bg.image.ci.mims > 0)
			bgi = c.bg.image.ci.mims[0].im;
		for( ; i <= c.cur; i++) {
			mim := c.cim.mims[i];
			if(i > 0 && i < c.cur && mim.bgcolor == -2)
				continue;
			p := c.r.min.add(mim.origin);
			r := mim.im.r;
			r = Rect(p, p.add(Point(r.dx(), r.dy())));

			# IE takes "clear-to-background" disposal method to mean
			# clear to background of HTML page, ignoring any background
			# color specified in the GIF.
			# IE clears to background before frame 0
			if(i == 0)
				win.draw(c.r, bgi, nil, zp);

			if(i != c.cur && mim.bgcolor >= 0)
				win.draw(r, bgi, nil, zp);
			else
				win.draw(r, mim.im, mim.mask, zp);
		}
	Clabel =>
		p := c.r.min.add(Point(0,ENTVMARGIN));
		win.text(p, colorimage(Black), zp, fonts[DefFnt].f, c.s);
	}
	if(flush) {
		if (ctl.popup != nil)
			ctl.popup.flush(ctl.r);
		else
			G->flush(ctl.r);
	}
	win.clipr = oclipr;
}

# Break s up into substrings that fit in width availw
# when printing with font fnt.
# The second returned array contains the indexes into the original
# string where the corresponding line starts (which might not be simply
# the sum of the preceding lines because of cr/lf's in the original string
# which are omitted from the lines array.
# Empty lines (ending in cr) get put into the array as empty strings.
# The start indices array has an entry for the phantom next line, to avoid
# the need for special cases in the rest of the code.
wrapstring(fnt: ref Font, s: string, availw: int) : (array of string, array of int)
{
	sl : list of (string, int) = nil;
	sw := fnt.width(s);
	n := 0;
	k := 0;	# index into original s where current s starts
	origlen := len s;
	done := 0;
	while(!done) {
		kincr := 0;
		s1, s2: string;
		if(s == "") {
			s1 = s;
			done = 1;
		}
		else {
			# if any newlines in s1, it's a forced break
			# (and newlines aren't to appear in result)
			(s1, s2) = S->splitl(s, "\n");
			if(s2 != nil && fnt.width(s1) <= availw) {
				s = s2[1:];
				sw = fnt.width(s);
				kincr = (len s1) + 1;
			}
			else if(sw <= availw) {
				s1 = s;
				done = 1;
			}
			else {
				(s1, nil, s, sw) = breakstring(s, sw, fnt, availw, 0);
				kincr = len s1;
				if(s == "")
					done = 1;
			}
		}
		sl = (s1, k) :: sl;
		k += kincr;
		n++;
	}
	# reverse sl back to original order
	lines := array[n] of string;
	linestarts := array[n+1] of int;
	linestarts[n] = origlen;
	while(sl != nil) {
		(ss, nn) := hd sl;
		lines[--n] = ss;
		linestarts[n] = nn;
		sl = tl sl;
	}
	return (lines, linestarts);
}

normalsel(sel : (int, int)) : (int, int)
{
	(s, e) := sel;
	if (s > e)
		(e, s) = sel;
	return (s, e);
}

selsegs(n, s, e : int) : list of (int, int, int)
{
	if (e < 0 || s > n)
		# selection is not in 0..n
		return (0, n, 0) :: nil;

	if (e > n) {
		# second half of string is selected
		if (s <= 0)
			return (0, n, 1) :: nil;
		return (0, s, 0) :: (s, n, 1) :: nil;
	}

	if (s < 0) {
		# first half of string is selected
		if (e >= n)
			return (0, n, 1) :: nil;
		return (0, e, 1) :: (e, n, 0) :: nil;
	}
	# middle section of string is selected
	return (0, s, 0) :: (s, e, 1) :: (e, n, 0) :: nil;
}

# Figure out in which area of scrollbar, if any, p lies.
# Then use p and mtype from mouse event to return desired action.
Control.entryset(c: self ref Control, s: string)
{
	pick e := c {
	Centry =>
		e.s = s;
		e.sel = (0, 0);
		e.left = 0;
		# calculate scroll bar settings
		if (e.linewrap && e.scr != nil) {
			(lines, nil, nil, nil) := entrywrapcalc(e);
			nlines := len lines;
			ny := (e.r.dy() - 2 * ENTVMARGIN)/ctllinespace;
			e.scr.scrollset(0, ny, (nlines - 1), nlines, 0);
		}
	}
}

entryupdown(e: ref Control.Centry, cur : int, delta : int) : int
{
	e.sel = (cur, cur);
	(lines, linestarts, topline, cursline) := entrywrapcalc(e);
	newl := cursline + delta;
	if (newl < 0 || newl >= len lines)
		return cur;

	fnt := fonts[CtlFnt].f;
	x := cur - linestarts[cursline];
	w := fnt.width(lines[cursline][0:x]);
	l := lines[newl];
	if (len l == 0)
		return linestarts[newl];
	prevw := fnt.width(l);
	curw := prevw;
	for (ix := len l - 1; ix > 0 ; ix--) {
		prevw = curw;
		curw = fnt.width(l[:ix]);
		if (curw < w)
			break;
	}
	# decide on closest (curw <= w <= prevw)
	if (prevw-w <= w - curw)
		# closer to rhs
		ix++;
	return linestarts[newl]+ix;
}

# delete given range of characters, and redraw
entrydelrange(e: ref Control.Centry, istart, iend: int)
{
	n := iend - istart;
	(sels, sele) := normalsel(e.sel);
	if(n > 0) {
		e.s = e.s[0:istart] + e.s[iend:];

		if(sels > istart) {
			if(sels < iend)
				sels = istart;
			else
				sels -= n;
		}
		if (sele > istart) {
			if (sele < iend)
				sele = istart;
			else
				sele -= n;
		}

		if(e.left > istart)
			e.left = max(istart-1, 0);
		e.sel = (sels, sele);
		entryscroll(e);
	}
}

snarf : string;
entrysetsnarf(e: ref Control.Centry)
{
	if (e.s == nil)
		return;
	s := e.s;
	(sels, sele) := normalsel(e.sel);
	if (sels != sele)
		s = e.s[sels:sele];
		
	f := sys->open("/chan/snarf", sys->OWRITE);
	if (f == nil)
		snarf = s;
	else {
		data := array of byte s;
		sys->write(f, data, len data);
	}
}

entryinsertsnarf(e: ref Control.Centry)
{
	f := sys->open("/chan/snarf", sys->OREAD);
	if(f != nil) {
		buf := array[sys->ATOMICIO] of byte;
		n := sys->read(f, buf, len buf);
		if(n > 0) {
			# trim a trailing newline, as a service...
			if(buf[n-1] == byte '\n')
				n--;
		}
		snarf = "";
		if (n > 0)
			snarf = string buf[:n];
	}

	if (snarf != nil) {
		(sels, sele) := normalsel(e.sel);
		if (sels != sele) {
			entrydelrange(e, sels, sele);
			(sels, sele) = e.sel;
		}
		lhs, rhs : string;
		if (sels > 0)
			lhs = e.s[:sels];
		if (sels < len e.s)
			rhs  = e.s[sels:];
		e.entryset(lhs + snarf + rhs);
		e.sel = (len lhs, len lhs + len snarf);
	}
}

# make sure can see cursor and following char or two
entryscroll(e: ref Control.Centry)
{
	s := e.s;
	slen := len s;
	if(e.flags&CFsecure) {
		for(i := 0; i < slen; i++)
			s[i] = '*';
	}
	if(e.linewrap) {
		# For multiple line entries, c.left is the char
		# at the beginning of the topmost visible line,
		# and we just want to scroll to make sure that
		# the line with the cursor is visible
		(lines, linestarts, topline, cursline) := entrywrapcalc(e);
		vislines := (e.r.dy()-2*ENTVMARGIN) / ctllinespace;
		nlines := len linestarts;
		if(cursline < topline)
			topline = cursline;
		else {
			if(cursline >= topline+vislines)
				topline = cursline-vislines+1;
			if (topline + vislines >= nlines)
				topline = max(0, (nlines-1) - vislines);
		}
		e.left = linestarts[topline];
		if (e.scr != nil)
			e.scr.scrollset(topline, topline+vislines, nlines-1, nlines, 1);
	}
	else {
		(nil, sele) := e.sel;
		# sele is always the drag point
		if(sele < e.left)
			e.left = sele;
		else if(sele > e.left) {
			fnt := fonts[CtlFnt].f;
			wantw := e.r.dx() -2*ENTHMARGIN; # - 2*ctlspspace;
			while(e.left < sele-1) {
				w := fnt.width(e.s[e.left:sele]);
				if(w < wantw)
					break;
				e.left++;
			}
		}
	}
}

# Given e, a Centry with line wrapping,
# return (wrapped lines, line start indices, line# of top displayed line, line# containing cursor).
entrywrapcalc(e: ref Control.Centry) : (array of string, array of int, int, int)
{
	s := e.s;
	if(e.flags&CFsecure) {
		for(i := 0; i < len s; i++)
			s[i] = '*';
	}
	(nil, sele) := e.sel;
	textw := e.r.dx()-2*ENTHMARGIN;
	if (e.scr != nil)
		textw -= SCRFBREADTH;
	(lines, linestarts) := wrapstring(fonts[CtlFnt].f, s, textw);
	topline := 0;
	cursline := 0;
	for(i := 0; i < len lines; i++) {
		s = lines[i];
		i1 := linestarts[i];
		i2 := linestarts[i+1];
		if(e.left >= i1 && e.left < i2)
			topline = i;
		if(sele >= i1 && sele < i2)
			cursline = i;
	}
	if(sele == linestarts[len lines])
		cursline = len lines - 1;
	return (lines, linestarts, topline, cursline);
}

Lay.new(targwidth: int, just: byte, margin: int, bg: Background) : ref Lay
{
	ans := ref Lay(Line.new(), Line.new(),
			targwidth, 0, 0, margin, nil, bg, just, byte 0);
	if(ans.targetwidth < 0)
		ans.targetwidth = 0;
	ans.start.pos = Point(margin, margin);
	ans.start.next = ans.end;
	ans.end.prev = ans.start;
	# dummy item at end so ans.end will have correct y coord
	it := Item.newspacer(ISPnull, 0);
	it.state = IFbrk|IFcleft|IFcright;
	ans.end.items = it;
	return ans;
}

Line.new() : ref Line
{
	return ref Line(
			nil, nil, nil,	# items, next, prev
			zp,		# pos
			0, 0, 0,	# width, height, ascent
			byte 0);	# flags
}

Loc.new() : ref Loc
{
	return ref Loc(array[10] of Locelem, 0, zp);	# le, n, pos
}

Loc.add(loc: self ref Loc, kind: int, pos: Point)
{
	if(loc.n == len loc.le) {
		newa := array[len loc.le + 10] of Locelem;
		newa[0:] = loc.le;
		loc.le = newa;
	}
	loc.le[loc.n].kind = kind;
	loc.le[loc.n].pos = pos;
	loc.n++;
}

# return last frame in loc's path
Loc.lastframe(loc: self ref Loc) : ref Frame
{
	if (loc == nil)
		return nil;
	for(i := loc.n-1; i >=0; i--)
		if(loc.le[i].kind == LEframe)
			return loc.le[i].frame;
	return nil;
}

Loc.print(loc: self ref Loc, msg: string)
{
	sys->print("%s: Loc with %d components, pos=(%d,%d)\n", msg, loc.n, loc.pos.x, loc.pos.y);
	for(i := 0; i < loc.n; i++) {
		case loc.le[i].kind {
		LEframe =>
			sys->print("frame %x\n",  loc.le[i].frame);
		LEline =>
			sys->print("line %x\n", loc.le[i].line);
		LEitem =>
			sys->print("item: %x", loc.le[i].item);
			loc.le[i].item.print();
		LEtablecell =>
			sys->print("tablecell: %x, cellid=%d\n", loc.le[i].tcell, loc.le[i].tcell.cellid);
		LEcontrol =>
			sys->print("control %x\n", loc.le[i].control);
		}
	}
}

Sources.new(m : ref Source) : ref Sources
{
	srcs := ref Sources;
	srcs.main = m;
	return srcs;
}

Sources.add(srcs: self ref Sources, s: ref Source, required: int)
{
	if (required) {
		CU->assert(srcs.reqd == nil);
		srcs.reqd = s;
	} else
		srcs.srcs = s :: srcs.srcs;
}

Sources.done(srcs: self ref Sources, s: ref Source)
{
	if (s == srcs.main) {
		if (srcs.reqd != nil) {
			sys->print("FREEING MAIN WHEN REQD != nil\n");
			if (s.bs == nil)
				sys->print("s.bs == nil\n");
			else
				sys->print("main.eof = %d main.lim = %d, main.edata = %d\n", s.bs.eof, s.bs.lim, s.bs.edata);
		}
		srcs.main = nil;
	}
	else if (s == srcs.reqd)
		srcs.reqd = nil;
	else {
		new : list of ref Source;
		for (old := srcs.srcs; old != nil; old = tl old) {
			src := hd old;
			if (src == s)
				continue;
			new = src :: new;
		}
		srcs.srcs = new;
	}
}

Sources.waitsrc(srcs: self ref Sources) : ref Source
{
	if (srcs == nil)
		return nil;

	bsl : list of ref ByteSource;

	if (srcs.reqd == nil && srcs.main != nil) {
		pick s := srcs.main {
		Shtml =>
			if (s.itsrc.toks != nil || s.itsrc.reqddata != nil)
				return s;
		}
	}

	# always check for subordinates
	for (sl := srcs.srcs; sl != nil; sl = tl sl)
		bsl = (hd sl).bs :: bsl;
	# reqd is taken in preference to main source as main
	# cannot be processed until we have the whole of reqd
	if (srcs.reqd != nil)
		bsl = srcs.reqd.bs :: bsl;
	else if (srcs.main != nil)
		bsl = srcs.main.bs :: bsl;
	if (bsl == nil)
		return nil;
	bs : ref ByteSource;
	for (;;) {
		bs = CU->waitreq(bsl);
		if (srcs.reqd == nil || srcs.reqd.bs != bs)
			break;
		# only interested in reqd if we have got it all
		if (bs.err != "" || bs.eof)
			return srcs.reqd;
	}
	if (srcs.main != nil && srcs.main.bs == bs)
		return srcs.main;
	found : ref Source;
	for(sl = srcs.srcs; sl != nil; sl = tl sl) {
		s := hd sl;
		if(s.bs == bs) {
			found = s;
			break;
		}
	}
	CU->assert(found != nil);
	return found;
}

# spawned to animate images in frame f
animproc(f: ref Frame)
{
	f.animpid = sys->pctl(0, nil);
	aits : list of ref Item = nil;
	# let del be millisecs to sleep before next frame change
	del := 10000000;
	d : int;
	for(il := f.doc.images; il != nil; il = tl il) {
		it := hd il;
		pick i := it {
		Iimage =>
			ms := i.ci.mims;
			if(len ms > 1) {
				loc := f.find(zp, it);
				if(loc == nil) {
					# could be background, I suppose -- don't animate it
					if(dbg)
						sys->print("couldn't find item for animated image\n");
					continue;
				}
				p := loc.le[loc.n-1].pos;
				p.x += int i.hspace + int i.border;
				# BUG: should get background from least enclosing layout
				ctl := Control.newanimimage(f, i.ci, f.layout.background);
				ctl.r = ctl.r.addpt(p);
				i.ctlid = f.addcontrol(ctl);
				d = ms[0].delay;
				if(dbg)
					sys->print("added anim ctl %d for image %s, initial delay %d\n",
						i.ctlid, i.ci.src.tostring(), d);
				aits = it :: aits;
				if(d < del)
					del = d;
			}
		}
	}
	if(aits == nil)
		return;
	tot := big 0;
	for(;;) {
		sys->sleep(del);
		tot = tot + big del;
		newdel := 10000000;
		for(al := aits; al != nil; al = tl al) {
			it := hd al;
			pick i := hd al {
			Iimage =>
				ms := i.ci.mims;
				pick c := f.controls[i.ctlid] {
				Canimimage =>
					m := ms[c.cur];
					d = m.delay;
					if(d > 0)
						d -= int (tot - c.ts);
					if(d == 0) {
						# advance to next frame and show it
						c.cur++;
						if(c.cur == len ms)
							c.cur = 0;
						d = ms[c.cur].delay;
						c.ts = tot;
						c.draw(1);
					}
					if(d < newdel)
						newdel = d;
				}
			}
		}
		del = newdel;
	}
}