shithub: purgatorio

ref: d4f4b292ca9c8665ef938036340ff32bf5128755
dir: /appl/collab/clients/whiteboard.b/

View raw version
implement Whiteboard;

include "sys.m";
	sys: Sys;

include "draw.m";
	draw: Draw;
	Screen, Display, Image, Rect, Point, Font: import draw;

include "tk.m";
	tk: Tk;

include "tkclient.m";
	tkclient: Tkclient;

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

ERASEWIDTH: con 6;


stderr: ref Sys->FD;
srvfd: ref Sys->FD;
disp: ref Display;
font: ref Draw->Font;
drawctxt: ref Draw->Context;

tksetup := array[] of {
	"frame .f -bd 2",
	"frame .c -bg white -width 234 -height 279",
	"menu .penmenu",
	".penmenu add command -command {send cmd pen 0} -bitmap @/icons/whiteboard/0.bit",
	".penmenu add command -command {send cmd pen 1} -bitmap @/icons/whiteboard/1.bit",
	".penmenu add command -command {send cmd pen 2} -bitmap @/icons/whiteboard/2.bit",
	".penmenu add command -command {send cmd pen erase} -bitmap @/icons/whiteboard/erase.bit",
	"menubutton .pen -menu .penmenu -bitmap @/icons/whiteboard/1.bit",
	"button .colour -bg black -activebackground black -command {send cmd getcolour}",
	"pack .c -in .f",
	"pack .f -side top -anchor center",
	"pack .pen -side left",
	"pack .colour -side left -fill both -expand 1",
	"update",
};

tkconnected := array[] of {
	"bind .c <Button-1> {send cmd down %x %y}",
	"bind .c <ButtonRelease-1> {send cmd up %x %y}",
	"update",
};

init(ctxt: ref Draw->Context, args: list of string)
{
	sys = load Sys Sys->PATH;
	sys->pctl(Sys->NEWPGRP, nil);
	stderr = sys->fildes(2);

	draw = load Draw Draw->PATH;
	tk = load Tk Tk->PATH;

	tkclient = load Tkclient Tkclient->PATH;
	if (tkclient == nil)
		badmod(Tkclient->PATH);

	if (len args < 2) {
		sys->fprint(stderr, "Usage: whiteboard [servicedir] id\n");
		raise "fail:init";
	}

	args = tl args;
	servicedir := "/n/remote/services";
	if(len args == 2)
		(servicedir, args) = (hd args, tl args);
	wbid := hd args;

	disp = ctxt.display;
	if (disp == nil) {
		sys->fprint(stderr, "bad Draw->Context\n");
		raise "fail:init";
	}
	drawctxt = ctxt;

	tkclient->init();
	(win, winctl) := tkclient->toplevel(ctxt, nil, "Whiteboard", 0);
	font = Font.open(disp, tkcmd(win, ". cget -font"));
	if(font == nil)
		font = Font.open(disp, "*default*");
	cmd := chan of string;
	tk->namechan(win, cmd, "cmd");
	tkcmds(win, tksetup);
	tkclient->onscreen(win, nil);
	tkclient->startinput(win, "kbd" :: "ptr" :: nil);
	cimage := makeimage(win);

	sc := chan of array of (Point, Point);
	cc := chan of (string, ref Image, ref Sys->FD, ref Sys->FD);
	connected := 0;
	sfd: ref Sys->FD;
	ctlfd: ref Sys->FD;	# must keep this open to keep service active

	showtext(cimage, "connecting...");
	spawn connect(servicedir, wbid, cc);

	err: string;
	strokeimg: ref Image;
Connect:
	for (;;) alt {
	(err, strokeimg, sfd, ctlfd) = <-cc =>
		if (err == nil)
			break Connect;
		else
			showtext(cimage, "Error: " + err);

	s := <-winctl or
	s = <-win.wreq or
	s = <-win.ctxt.ctl =>
		oldimg := win.image;
		err = tkclient->wmctl(win, s);
		if(s[0] == '!' && err == nil && win.image != oldimg){
			cimage = makeimage(win);
			showtext(cimage, "connecting...");
		}
	p := <-win.ctxt.ptr =>
		tk->pointer(win, *p);
	c := <-win.ctxt.kbd =>
		tk->keyboard(win, c);
	}

	tkcmd(win, ".c configure -width " + string strokeimg.r.dx());
	tkcmd(win, ".c configure -height " + string strokeimg.r.dy());
	tkcmds(win, tkconnected);
	tkcmd(win, "update");
	cimage.draw(cimage.r, strokeimg, nil, strokeimg.r.min);

	strokesin := chan of (int, int, array of Point);
	strokesout := chan of (int, int, Point, Point);
	spawn reader(sfd, strokesin);
	spawn writer(sfd, strokesout);

	pendown := 0;
	p0, p1: Point;

	getcolour := 0;
	white := disp.white;
	whitepen := disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, Draw->White);
	pencolour := Draw->Black;
	penwidth := 1;
	erase := 0;
	drawpen := disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, pencolour);

	for (;;) alt {
	s := <-winctl or
	s = <-win.ctxt.ctl or
	s = <-win.wreq =>
		oldimg := win.image;
		err = tkclient->wmctl(win, s);
		if(s[0] == '!' && err == nil && win.image != oldimg){
			cimage = makeimage(win);
			cimage.draw(cimage.r, strokeimg, nil, strokeimg.r.min);
		}
	p := <-win.ctxt.ptr =>
		tk->pointer(win, *p);
	c := <-win.ctxt.kbd =>
		tk->keyboard(win, c);
	(colour, width, strokes) := <-strokesin =>
		if (strokes == nil)
			tkclient->settitle(win, "Whiteboard (Disconnected)");
		else {
			pen := disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, colour);
			drawstrokes(cimage, cimage.r.min, pen, width, strokes);
			drawstrokes(strokeimg, strokeimg.r.min, pen, width, strokes);
		}

	c := <-cmd =>
		(nil, toks) := sys->tokenize(c, " ");
		case hd toks {
		"down" =>
			toks = tl toks;
			x := int hd toks;
			y := int hd tl toks;
			if (!pendown) {
				pendown = 1;
				p0 = Point(x, y);
				continue;
			}
			p1 = Point(x, y);
			if (p1.x == p0.x && p1.y == p0.y)
				continue;
			pen := drawpen;
			colour := pencolour;
			width := penwidth;
			if (erase) {
				pen = whitepen;
				colour = Draw->White;
				width = ERASEWIDTH;
			}
			drawstroke(cimage, cimage.r.min, p0, p1, pen, width);
			drawstroke(strokeimg, strokeimg.r.min, p0, p1, pen, width);
			strokesout <-= (colour, width, p0, p1);
			p0 = p1;
		"up" =>
			pendown = 0;

		"getcolour" =>
			pendown = 0;
			if (!getcolour)
				spawn colourmenu(cmd);
		"colour" =>
			pendown = 0;
			getcolour = 0;
			toks = tl toks;
			if (toks == nil)
				# colourmenu was dismissed
				continue;
			erase = 0;
			tkcmd(win, ".pen configure -bitmap @/icons/whiteboard/" + string penwidth + ".bit");
			tkcmd(win, "update");
			pencolour = int hd toks;
			toks = tl toks;
			tkcolour := hd toks;
			drawpen = disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, pencolour);
			tkcmd(win, ".colour configure -bg " + tkcolour + " -activebackground " + tkcolour);
			tkcmd(win, "update");

		"pen" =>
			pendown = 0;
			p := hd tl toks;
			i := "";
			if (p == "erase") {
				erase = 1;
				i = "erase.bit";
			} else {
				erase = 0;
				penwidth = int p;
				i = p + ".bit";
			}
			tkcmd(win, ".pen configure -bitmap @/icons/whiteboard/" + i);
			tkcmd(win, "update");
		}

	}
}

makeimage(win: ref Tk->Toplevel): ref Draw->Image
{
	if(win.image == nil)
		return nil;
	scr := Screen.allocate(win.image, win.image.display.white, 0);
	w := scr.newwindow(tk->rect(win, ".c", Tk->Local), Draw->Refnone, Draw->Nofill);
	return w;
}

showtext(img: ref Image, s: string)
{
	r := img.r;
	r.max.y = img.r.min.y + font.height;
	img.draw(r, disp.white, nil, (0, 0));
	img.text(r.min, disp.black, (0, 0), font, s);
}

penmenu(t: ref Tk->Toplevel, p: Point)
{
	topy := int tkcmd(t, ".penmenu yposition 0");
	boty := int tkcmd(t, ".penmenu yposition end");
	dy := boty - topy;
	p.y -= dy;
	tkcmd(t, ".penmenu post " + string p.x + " " + string p.y);
}

colourcmds := array[] of {
	"label .l -height 10",
	"frame .c -height 224 -width 224",
	"pack .l -fill x -expand 1",
	"pack .c -side bottom -fill both -expand 1",
	"pack propagate . 0",
	"bind .c <Button-1> {send cmd push %x %y}",
	"bind .c <ButtonRelease-1> {send cmd release}",
};

lastcolour := "255";
lasttkcolour := "#000000";

colourmenu(c: chan of string)
{
	(t, winctl) := tkclient->toplevel(drawctxt, nil, "Whiteboard", Tkclient->OK);
	cmd := chan of string;
	tk->namechan(t, cmd, "cmd");
	tkcmds(t, colourcmds);
	tkcmd(t, ".l configure -bg " + lasttkcolour);
	tkcmd(t, "update");
	tkclient->onscreen(t, "onscreen");
	tkclient->startinput(t, "kbd" :: "ptr" :: nil);

	drawcolours(t.image, tk->rect(t, ".c", Tk->Local));

	for(;;) alt {
	p := <-t.ctxt.ptr =>
		tk->pointer(t, *p);
	s := <-t.ctxt.kbd =>
		tk->keyboard(t, s);
	s := <-winctl or
	s = <-t.ctxt.ctl or
	s = <-t.wreq =>
		case s{
		"ok" =>
			c <-= "colour " + lastcolour + " " + lasttkcolour;
			return;
		"exit" =>
			c <-= "colour";
			return;
		* =>
			oldimage := t.image;
			e := tkclient->wmctl(t, s);
			if(s[0] == '!' && e == nil && oldimage != t.image)
				drawcolours(t.image, tk->rect(t, ".c", Tk->Local));
		}

	press := <-cmd =>
		(n, word) := sys->tokenize(press, " ");
		case hd word {
		"push" =>
			(lastcolour, lasttkcolour) = color(int hd tl word, int hd tl tl word, tk->rect(t, ".c", 0).size());
			tkcmd(t, ".l configure -bg " + lasttkcolour);
		}
	}
}

drawcolours(img: ref Image, cr: Rect)
{
	# use writepixels because it's much faster than allocating all those colors.
	tmp := disp.newimage(((0,0),(cr.dx(),cr.dy()/16+1)), Draw->CMAP8, 0, 0);
	if(tmp == nil)
		return;
	buf := array[tmp.r.dx()*tmp.r.dy()] of byte;
	dx := cr.dx();
	dy := cr.dy();
	for(y:=0; y<16; y++){
		for(i:=tmp.r.dx()-1; i>=0; --i)
			buf[i] = byte (16*y+(16*i)/dx);
		for(k:=tmp.r.dy()-1; k>=1; --k)
			buf[dx*k:] = buf[0:dx];
		tmp.writepixels(tmp.r, buf);
		r: Rect;
		r.min.x = cr.min.x;
		r.max.x = cr.max.x;
		r.min.y = cr.min.y+(dy*y)/16;
		r.max.y = cr.min.y+(dy*(y+1))/16;
		img.draw(r, tmp, nil, tmp.r.min);
	}
}

color(x, y: int, size: Point): (string, string)
{
	x = (16*x)/size.x;
	y = (16*y)/size.y;
	col := 16*y+x;
	(r, g, b) := disp.cmap2rgb(col);
	tks := sys->sprint("#%.2x%.2x%.2x", r, g, b);
	return (string disp.cmap2rgba(col), tks);
}

opensvc(dir: string, svc: string, name: string): (ref Sys->FD, string, string)
{
	ctlfd := sys->open(dir+"/ctl", Sys->ORDWR);
	if(ctlfd == nil)
		return (nil, nil, sys->sprint("can't open %s/ctl: %r", dir));
	if(sys->fprint(ctlfd, "%s %s", svc, name) <= 0)
		return (nil, nil, sys->sprint("can't access %s service %s: %r", svc, name));
	buf := array [32] of byte;
	sys->seek(ctlfd, big 0, Sys->SEEKSTART);
	n := sys->read(ctlfd, buf, len buf);
	if (n <= 0)
		return (nil, nil, sys->sprint("%s/ctl: protocol error: %r", dir));
	return (ctlfd, dir+"/"+string buf[0:n], nil);
}

connect(dir, name: string, res: chan of (string, ref Image, ref Sys->FD, ref Sys->FD))
{
	(ctlfd, srvdir, emsg) := opensvc(dir, "whiteboard", name);
	if(ctlfd == nil) {
		res <-= (emsg, nil, nil, nil);
		return;
	}

	bitpath := srvdir + "/wb.bit";
	strokepath := srvdir + "/strokes";

	sfd := sys->open(strokepath, Sys->ORDWR);
	if (sfd == nil) {
		err := sys->sprint("cannot open whiteboard data: %r");
		res <-= (err, nil, nil, nil);
		srvfd = nil;
		return;
	}

	bfd := sys->open(bitpath, Sys->OREAD);
	if (bfd == nil) {
		err := sys->sprint("cannot open whiteboard image: %r");
		res <-= (err, nil, nil, nil);
		srvfd = nil;
		return;
	}

	img := disp.readimage(bfd);
	if (img == nil) {
		err := sys->sprint("cannot read whiteboard image: %r");
		res <-= (err, nil, nil, nil);
		srvfd = nil;
		return;
	}
sys->print("read image ok\n");

	# make sure image is depth 8 (because of image.line() bug)
	if (img.depth != 8) {
sys->print("depth is %d, not 8\n", img.depth);
		nimg := disp.newimage(img.r, Draw->CMAP8, 0, 0);
		if (nimg == nil) {
			res <-= ("cannot allocate local image", nil, nil, nil);
			srvfd = nil;
			return;
		}
		nimg.draw(nimg.r, img, nil, img.r.min);
		img = nimg;
	}

	res <-= (nil, img, sfd, ctlfd);
}

reader(fd: ref Sys->FD, sc: chan of (int, int, array of Point))
{
	buf := array [Sys->ATOMICIO] of byte;

	for (;;) {
		n := sys->read(fd, buf, len buf);
		if (n <= 0) {
			sc <-= (0, 0, nil);
			return;
		}
		s := string buf[0:n];
		(npts, toks) := sys->tokenize(s, " ");
		if (npts & 1)
			# something wrong
			npts--;
		if (npts < 6)
			# ignore
			continue;

		colour, width: int;
		(colour, toks) = (int hd toks, tl toks);
		(width, toks) = (int hd toks, tl toks);
		pts := array [(npts - 2)/ 2] of Point;
		for (i := 0; toks != nil; i++) {
			x, y: int;
			(x, toks) = (int hd toks, tl toks);
			(y, toks) = (int hd toks, tl toks);
			pts[i] = Point(x, y);
		}
		sc <-= (colour, width, pts);
		pts = nil;
	}
}

Wmsg: adt {
	data: array of byte;
	datalen: int;
	next: cyclic ref Wmsg;
};

writer(fd: ref Sys->FD, sc: chan of (int, int, Point, Point))
{
	lastcol := -1;
	lastw := -1;
	lastpt := Point(-1, -1);
	curmsg: ref Wmsg;
	nextmsg: ref Wmsg;

	eofc := chan of int;
	wc := chan of ref Wmsg;
	wseof := 0;
	spawn wslave(fd, wc, eofc);

	for (;;) {
		colour := -1;
		width := 0;
		p0, p1: Point;

		if (curmsg == nil || wseof)
			(colour, width, p0, p1) = <-sc;
		else alt {
		wseof = <-eofc =>
			;

		(colour, width, p0, p1) = <-sc =>
			;

		wc <-= curmsg =>
			curmsg = curmsg.next;
			continue;
		}

		newseq := 0;
		if (curmsg == nil) {
			curmsg = ref Wmsg(array [Sys->ATOMICIO] of byte, 0, nil);
			nextmsg = curmsg;
			newseq = 1;
		}

		if (colour != lastcol || width != lastw || p0.x != lastpt.x || p0.y != lastpt.y)
			newseq = 1;

		d: array of byte = nil;
		if (!newseq) {
			d = sys->aprint(" %d %d", p1.x, p1.y);
			if (nextmsg.datalen + len d >= Sys->ATOMICIO) {
				nextmsg.next = ref Wmsg(array [Sys->ATOMICIO] of byte, 0, nil);
				nextmsg = nextmsg.next;
				newseq = 1;
			}
		}
		if (newseq) {
			d = sys->aprint(" %d %d %d %d %d %d", colour, width, p0.x, p0.y, p1.x, p1.y);
			if (nextmsg.datalen != 0) {
				nextmsg.next = ref Wmsg(array [Sys->ATOMICIO] of byte, 0, nil);
				nextmsg = nextmsg.next;
			}
		}
		nextmsg.data[nextmsg.datalen:] = d;
		nextmsg.datalen += len d;
		lastcol = colour;
		lastw = width;
		lastpt = p1;
	}
}

wslave(fd: ref Sys->FD, wc: chan of ref Wmsg, eof: chan of int)
{
	for (;;) {
		wm := <-wc;
		n := sys->write(fd, wm.data, wm.datalen);
		if (n != wm.datalen)
			break;
	}
	eof <-= 1;
}

drawstroke(img: ref Image, offset, p0, p1: Point, pen: ref Image, width: int)
{
	p0 = p0.add(offset);
	p1 = p1.add(offset);
	img.line(p0, p1, Draw->Endsquare, Draw->Endsquare, width, pen, p0);
}

drawstrokes(img: ref Image, offset: Point, pen: ref Image, width: int, pts: array of Point)
{
	if (len pts < 2)
		return;
	p0, p1: Point;
	p0 = pts[0].add(offset);
	for (i := 1; i < len pts; i++) {
		p1 = pts[i].add(offset);
		img.line(p0, p1, Draw->Endsquare, Draw->Endsquare, width, pen, p0);
		p0 = p1;
	}
}

badmod(mod: string)
{
	sys->fprint(stderr, "cannot load %s: %r\n", mod);
	raise "fail:bad module";
}

tkcmd(t: ref Tk->Toplevel, cmd: string): string
{
	s := tk->cmd(t, cmd);
	if (s != nil && s[0] == '!') {
		sys->fprint(stderr, "%s\n", cmd);
		sys->fprint(stderr, "tk error: %s\n", s);
	}
	return s;
}

tkcmds(t: ref Tk->Toplevel, cmds: array of string)
{
	for (i := 0; i < len cmds; i++)
		tkcmd(t, cmds[i]);
}