Plan 9 from Bell Labs’s /usr/web/sources/contrib/nemo/octopus/port/live/owptext.b

Copyright © 2021 Plan 9 Foundation.
Distributed under the MIT License.
Download the Plan 9 distribution.


#
# Text panel.
# Text is handled by the Tblks module.
# Undo/redo is handled by a separate Tundo module.
# Drawing is handled by Tframe, which is also in charge of maintaining the
# selection and the position of the text shown in the panel.
# This does more than needed and calls frame operations even when it's not
# necessary to do so.
implement Pimpl;
include "mods.m";
mods, debug, win, tree: import dat;
Menu: import menus;
setcursor, Waiting, Arrow,
getfont, Cpointer, cols, panelback, maxpt, cookclick, drawtag,
BACK,TEXT, readsnarf, Inset, terminate, writesnarf, 
SET, CLEAR, SHAD, BORD, CMtriple, CMdouble : import gui;
Ptag, Pline, Pedit, Pshown, Psync, Pdead, Pdirty, Ptbl, Predraw,
intag, escape, unescape, nth, Panel: import wpanel;
panelctl, panelkbd, panelmouse, tagmouse, Tree: import wtree;
usage: import arg;


include "tblks.m";
	tblks: Tblks;
	fixpos,  fixposins, fixposdel, dtxt, strstr, Maxline, Blks, strchr, Str: import tblks;
include "tundo.m";
	tundo: Tundo;
	Edit, Edits: import tundo;
include "tframe.m";
	tframe:	Tframe;
	Frame:	import tframe;

Ptext: adt {
	blks:		ref Tblks->Blks;		# sequence of text blocks being edited.
	edits:	ref Tundo->Edits;		# edit operations for sync, undo, and redo.
	f:		ref Tframe->Frame;		# nil or initialized frame
	s0:		int;					# offset in text for 1st selection point, to extend selection.
	fsss:		int;					# f.ss as last synced to o/mero
	fsse:		int;					# idem for f.se
	mlast:	int;					# last option used in its menu
	tabtext:	string;				# for tbls, the original text, as updated.
	nlines:	int;					# nb. of lines in text (to compute maxsz)

	new:		fn(): ref Ptext;
	gotopos:	fn(pi: self ref Ptext, npos: int): int;
	wordat:	fn(pi: self ref Ptext, pos: int, long: int, selok: int): (int, int);

	dump:	fn(pi: self ref Ptext);
};

panels: array of ref Ptext;
textmenu: ref Menu;

# To allow search from tag lines, anytime a text is selected it becomes
# an argument for search, open, etc. The argument is void when they
# keyboard is used or a single click is made.
seltext: string;

pimpl(p: ref Panel): ref Ptext
{
	if (p.implid < 0 || p.implid > len panels || panels[p.implid] == nil)
		panic("draw: bug: no impl");
	return panels[p.implid];
}

init(d: Livedat): string
{
	prefixes = list of {"text:", "button:", "label:", "tag:", "tbl:"};
	dat = d;
	initmods();
	dat = d;
	str = load String String->PATH;
	if (str == nil)
		return sprint("loading %s: %r", String->PATH);
	tblks = load Tblks Tblks->PATH;
	if (tblks == nil)
		return sprint("loading %s: %r", Tblks->PATH);
	tframe = load Tframe Tframe->PATH;
	if (tframe == nil)
		return sprint("loading %s: %r", Tframe->PATH);
	tundo= load Tundo Tundo->PATH;
	if (tundo == nil)
		return sprint("loading %s: %r", Tundo->PATH);
	tblks->init(sys, str, err, debug['T']);
	tframe->init(dat, tblks, debug['F']);
	tundo->init(sys, err, debug['T']);
	return nil;
}

nullptext: Ptext;
Ptext.new(): ref Ptext
{
	pi := ref nullptext;
	return pi;
}

Ptext.gotopos(pi: self ref Ptext, npos: int): int
{
	if (npos == pi.f.pos)
		return pi.f.pos;
	s := pi.blks.pack();
	npos = fixpos(npos, len s.s);
	if (npos > 0){
		n := s.findr(npos, '\n', Maxline);	# position just past the last \n
		if (n >= 0){
			if (n > 0 && n == '\n')
				n++;
			npos = n;
		} else if (npos < len s.s)		# only when scrolling within the first line.
			npos = 0;
	}
	return npos;
}

# Taken from acme
isalnum(c : int) : int
{
	#
	# Hard to get absolutely right.  Use what we know about ASCII
	# and assume anything above the Latin control characters is
	# potentially an alphanumeric.
	#
	if(c <= ' ')
		return 0;
	if(16r7F<=c && c<=16rA0)
		return 0;
	if(strchr("!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", c) >= 0)
		return 0;
	return 1;
}

iswordchar(r: int, long: int): int
{
	if (isalnum(r) || strchr("0123456789_", r) >= 0)
		return 1;
	if (long && strchr("|&?=.-+/:", r) >= 0)
		return 1;
	return 0;
}


isparen(set: string, r: int): int
{
	i := strchr(set, r);
	if (i >= 0)
		return i + 1;
	else
		return 0;
}

#  Returns the word at pos.
#  If we are looking at the end of line, we pretend we look right
#  before it. In any case:
# 	The word is the selection when it exists and selok.
#  	It is the longest set of <wordchar>s if pos at <wordchar>
#		(if long,  |&?=.-+/: are also considers as word chars)
#  	It is the text between {} [] '' "" () if pos is at delim.
#  	It is the current line otherwise (if pos at blank)
#
Ptext.wordat(pi: self ref Ptext, pos: int, long: int, selok: int): (int, int)
{
	ss := pi.f.ss;
	se := pi.f.se;
	lparen := "{[(«<“";
	rparen :=  "}])»>”";
	paren := "\"'`";
	s := pi.blks.pack();
	nr := len s.s;
	if (pos >nr)
		pos = nr;
	if (pos == nr && pos > 0)
		pos--;
	spos := epos := pos;
	if (nr == 0)
		return (spos, epos);
	if (selok && pos >= ss && pos <= se && ss != se)
		return (ss, se);
	if (iswordchar(s.s[pos], long)){
		while(spos > 0 && iswordchar(s.s[spos], long))
			spos--;
		if (spos > 0)
			spos++;
		while(epos < nr && iswordchar(s.s[epos], long))
			epos++;
	} else if (pp := isparen(paren, s.s[pos])){
		spos++;
		for(epos = spos; epos < nr; epos++)
			if (isparen(paren, s.s[epos]) == pp)
				break;
	} else if (pp = isparen(lparen, s.s[pos])){
		nparen := 1;
		spos++;
		for(epos = spos; epos < nr; epos++){
			if (isparen(lparen, s.s[epos]) == pp)
				nparen++;
			if (isparen(rparen, s.s[epos]) == pp)
				nparen--;
			if (nparen <= 0){
				break;
			}
		}
	} else if (pp = isparen(rparen, s.s[pos])){
		nparen := 1;
		if (spos > 0)
		for(spos--; spos > 0; spos--){
			if (isparen(rparen, s.s[spos]) == pp)
				nparen++;
			if (isparen(lparen, s.s[spos]) == pp)
				nparen--;
			if (nparen <= 0){
				spos++;
				break;
			}
		}
	} else { # pos at blank
		if (s.s[spos] == '\n' && spos > 0 && s.s[spos-1] != '\n'){
			# click at right part of line; step back
			# so that expanding leads to previous line
			spos--;
		}
		while(spos > 0 && s.s[spos-1] != '\n')
			spos--;
		while(epos < nr && s.s[epos] != '\n')
			epos++;
		if (epos < nr)
			epos++;	# include \n
	}
	if (spos < 0 || epos < 0 || epos < spos || epos > nr)
		panic(sprint("spos/epos bug: %d %d %d\n", spos, epos, nr));
	return (spos, epos);
}

Ptext.dump(pi: self ref Ptext)
{
	pi.blks.dump();
	if (debug['F'] > 1)
		pi.f.dump();
}

settextsize(p: ref Panel)
{
	pi := pimpl(p);
	p.maxsz.y = 0;
	s := pi.blks.pack();
	if (p.flags&Pline){
		p.minsz.y = p.font.height; # up/down margins
		if (!(p.flags&Pedit) && len s.s > 0)
			p.minsz.x = p.font.width(s.s);
	} else {
		p.minsz = Point(p.font.height*2,p.font.height*2);
		p.minsz.y += 1;
		if (pi.nlines == 0)
			p.maxsz.y = 0;
		else {
			n := pi.nlines + 1;
			if (n < 3)
				n = 3;
			p.maxsz.y =  p.font.height * n;
			p.maxsz.y += 1;
		}
	}
}

Mexec, Mfind, Mput, Mapply, Mpaste, Mcut, Mclose, Mopen: con iota;
pinit(p: ref Panel)
{
	if (tree == nil)
		tree = dat->tree;
	if (textmenu == nil)
		textmenu = Menu.new(array[] of {"Exec", "Find", "Put", "Apply", "Paste",  "Cut", "Close", "Open"});
	for (i := 0; i < len panels; i++)
		if (panels[i] == nil)
			break;
	if (i == len panels){
		npanels := array[i+16] of ref Ptext;
		npanels[0:] = panels;
		panels = npanels;
	}
	p.implid = i;
	pi := panels[i] = Ptext.new();
	pi.edits = Edits.new();
	pi.mlast = Mexec;
	p.flags &= ~(Pline|Pedit);
	tab := 4;
	if (len p.name > 3 && p.name[0:3] == "tbl"){
		p.flags |= Ptbl|Pedit;
		p.wants = Point(1,1);
		pi.tabtext = "[empty]";
		pi.mlast = Mopen;
		#tab = 3;
	} else if (len p.name > 4 && p.name[0:4] == "text"){
		p.flags |= Pedit;
		p.wants = Point(1,1);
		pi.mlast = Mfind;
	} else if (len p.name > 3 && p.name[0:3] == "tag"){
		p.flags |= Pline|Pedit;
		p.wants = Point(1,0);
	} else {
		p.flags |= Pline;
	}
	if (p.flags&Pline)
		p.font = getfont("B");
	else
		p.font = getfont("R");
	s := "";
	if (p.flags&Pline){
		(nil, n) := splitl(p.name, ":");
		if (n != nil){
			n = n[1:];
			(s, nil) = splitl(n, ".");
			if (s == nil)
				s = nil;
		}
	}
	pi.blks = Blks.new(s);
	settextsize(p);
	# create frame w/o image. it maintains selection, mostly.
	# following pdraw calls will attach an image to the frame and reset its size.
	pi.f = Frame.new(Rect((0, 0), p.minsz), nil, p.font, gui->cols, !(p.flags&Pline));
	pi.f.tabsz = tab;
	pi.f.showsel = p.flags&Pedit;
	pi.f.init(pi.blks, pi.f.pos);
	pi.s0 = pi.f.ss;
}

pterm(p: ref Panel)
{
	if (p.implid != -1){
		pi := pimpl(p);
		panels[p.implid] = nil; 
		p.implid = -1;
		pi.f = nil;
		pi.edits = nil;
		pi.blks = nil;
	}
}

dirty(p: ref Panel, set: int)
{
	if (set)
		p.flags |= Psync;
	else
		p.flags &= ~Psync;
	if (p.flags&Pline)	# lines do not get dirty
		return;
	old := p.flags;
	if (set)
		p.flags |= Pdirty;
	else
		p.flags &= ~Pdirty;
	if (old != p.flags){
		if (set)
			p.fsctl("dirty\n", 0);
		else
			p.fsctl("clean\n", 0);
		spawn tree.tags("/"); # we might get here called from tree; avoid deadlock.
	}
}

tab(p: ref Panel)
{
	pi := pimpl(p);
	(n, toks) := tokenize(pi.tabtext, "\t\n");
	if (n <= 0)
		return;
	wl := array[n] of string;
	wwl := array[n] of int;
	for (i := 0; i < n; i++){
		wl[i] = hd toks;
		toks = tl toks;
	}
	mint := pi.f.spwid;
	maxt := pi.f.tabwid;
	colw := 0;
	maxw := 0;
	for (i = 0; i < n; i++){
		wwl[i] = p.font.width(wl[i]);
		if (maxw < wwl[i])
			maxw = wwl[i];
	}
	spwid := p.font.width(" ");
	for (i = 0; i < n; i++)
		while(wwl[i] < maxw){
			wl[i] += " ";
			wwl[i] += spwid;
		}
	for(i=0; i<n; i++){
		w := p.font.width(wl[i]);
		wwl[i]  = w;
		if(maxt-w%maxt < mint)
			w += mint;
		if(w % maxt)
			w += maxt-(w%maxt);
		if(w > colw)
			colw = w;
	}
	ncol := 1;
	if (colw != 0)
		ncol = p.rect.dx()/colw;	# can't use pi.f.r, because
	if (ncol < 1)				# frame may be using an old rectangle.
		ncol = 1;
	nrow := (n+ncol-1)/ncol;

	ns := "";
	for(i=0; i<nrow; i++){
		for(j:=i; j<n; j+=nrow){
			ns += wl[j];
			if(j+nrow >= n)
				break;
			else
				ns += "\t";
		}
		ns += "\n";
	}
	pi.blks = Blks.new(ns);
	pi.f.init(pi.blks, pi.f.pos);
	pi.s0 = pi.f.ss;
}

pdraw(p: ref Panel)
{
	pi := pimpl(p);
	if (!(p.flags&Pshown)){
		pi.f.i = nil;
		return;
	}
	w := dat->win;
	if (p.flags&Ptbl)
		tab(p);
	if (p.rect.eq(pi.f.r)){
		pi.f.i = w.image;
		pi.f.redraw();
	} else if (p.rect.dx() == pi.f.r.dx() && p.rect.dy() == pi.f.r.dy()){
		pi.f.i = w.image;
		pi.f.move(p.rect.min);
		pi.f.redraw();
	} else
		pi.f.resize(p.rect, w.image);
	settextsize(p);
	if (p.flags&Ptag)
		drawtag(p);
}

stringlines(s: string): int
{
	nc := n := 0;
	for (i := 0; i < len s; i++)
		if (s[i] == '\n' || nc == Maxline){
			n++;
			nc = 0;
		} else
			nc++;
	return n;
}

# could put the update as a new del+ins event, but how would we
# sync out our current ongoing edit?
# The safe thing to do is to discard any edits and start again.
# The application knows best.
pupdate(p: ref Panel, d: array of byte)
{
	pi := pimpl(p);
	if (!(p.flags&Pshown))
		pi.f.i = nil;	# avoid drawing
	s := string d;
	pi.blks = Blks.new(s);
	pi.edits = Edits.new();
	if (p.flags&Ptbl)
		pi.tabtext = s;
	nsel := -1;
	if ( (p.flags&Pline) || !(p.flags&Pedit))
		nsel = len s;
	pi.nlines = stringlines(s);
	pi.f.init(pi.blks, pi.f.pos);
	if (nsel >= 0){
		pi.f.sel(nsel, nsel);
		pi.s0 = pi.f.ss;
	}
	p.flags |= Predraw;
}

printdiff(s: string, ftext: string)
{
	l := len s;
	if (l > len ftext)
		l = len ftext;
	for (i := 0; i < l - 1 && s[i] == ftext[i]; i++)
		;
	if (i > 15)
		i -= 15;
	else
		i = 0;
	sj := len s - 1;
	fj := len ftext - 1;
	while(sj > 0 && fj > 0 && s[sj] == ftext[fj]){
		sj--; fj--;
	}
	fprint(stderr, "o/live and o/mero text differs: pos %d:\n", i);
	fprint(stderr, "text[%s]\n\nfile[%s]\n\n", s[i:sj], ftext[i:fj]);
}

syncchk(p: ref Panel)
{
	if (debug['T']){
		# double check that we are really synced with the FS.
		pi := pimpl(p);
		fname := p.path + "/data";
		fd := open(fname, OREAD);
		data := readfile(fd);
		if (data != nil){
			ftext := string data;
			s := pi.blks.pack().s;
			if (s != ftext){
				pi.dump();
				printdiff(s, ftext);
				panic("insdel bug");
			}
		}
		# double check that the text in the frame is also in sync.
		pi.f.chk();
	}
}

# BUG: needs to be reworked. While we were editing, other o/lives could
# update the text. Thus, we better send the version of the text that changes
# are for, and make o/mero discard (and report as failed) those that do not
# match the version. When we see a discarded change, we may simply reload the
# text and undo our edit.
syncedits(p: ref Panel, edits: list of ref Edit)
{
	pi := pimpl(p);
	ctlstr := "";
	for(; edits != nil; edits = tl edits){
		ev : string;
		pick e := hd edits {
		Ins =>
			ev = sprint("ins %d %s\n", e.pos, e.s);
		Del =>
			ev = sprint("del %d %d\n", e.pos, len e.s);
		}
		ctlstr += escape(ev);
	}
	# This is a good time to update o/mero idea of our selection.
	if (ctlstr != "" ||  pi.f.ss != pi.f.se && (pi.fsss != pi.f.ss || pi.fsse != pi.f.se)){
		ctlstr += escape(sprint("sel %d %d\n", pi.f.ss, pi.f.se));
		pi.fsss = pi.f.ss;
		pi.fsse = pi.f.se;
	}
	# fsctl would scape our \n's, and we want a single write for the entire
	# set of updates, if feasible.
	if (ctlstr != ""){
		# a good time to update our selection in o/mero. do so.
		fname := p.path + "/ctl";
		fd := open(fname, OWRITE);
		if (fd != nil){
			if (debug['E'])
				fprint(stderr, "fsctl: %s: [%s]\n", p.path, ctlstr);
			seek(fd, big 0, 2);
			data := array of byte ctlstr;
			write(fd, data, len data);
		}
	}
	p.flags &= ~Psync;	# we're synced now.
}

applyedit(p: ref Panel, e: ref Edit): int
{
	if (e != nil){
		pi := pimpl(p);
		pick ep := e {
		Ins =>
			pi.blks.ins(ep.s, ep.pos);
			pi.f.ins(ep.pos, len ep.s);
			pi.s0 = pi.f.ss;
			return ep.pos + len ep.s;
		Del =>
			pi.blks.del(len ep.s, ep.pos);
			pi.f.del(ep.pos, len ep.s);
			pi.s0 = pi.f.ss;
			return ep.pos;
		}
	}
	return -1;
}

undo(p: ref Panel): int
{
	pi := pimpl(p);
	return applyedit(p, pi.edits.undo());
}

redo(p: ref Panel): int
{
	pi := pimpl(p);
	return applyedit(p, pi.edits.redo());
}

ins(p: ref Panel, s: string, pos: int)
{
	pi := pimpl(p);
	pi.blks.ins(s, pos);
	pi.s0 = fixposins(pi.s0, pos, len s);
	if (pi.edits.ins(s, pos) < 0){
		syncedits(p, pi.edits.sync());
		if (pi.edits.ins(s, pos) < 0){
			pi.dump();
			panic(sprint("text: ins [%s] %d failed", s, pos));
		}
	}
}

del(p: ref Panel, n: int, pos: int): string
{
	pi := pimpl(p);
	ds := pi.blks.del(n, pos);
	pi.s0 = fixposdel(pi.s0, pos, n);
	if (pi.edits.del(ds, pos) < 0){
		syncedits(p, pi.edits.sync());
		if (pi.edits.del(ds, pos) < 0){
			pi.dump();
			panic(sprint("text: del [%s] %d failed", ds, pos));
		}
	}
	return ds;
}

pctl(p: ref Panel, s: string)
{
	pi := pimpl(p);
	if (!(p.flags&Pshown))
		pi.f.i = nil;	# avoid drawing
	odirty := p.flags&Pdirty;
	ofont := p.font;
	if (panelctl(tree, p, s) < 0){
		(nargs, args) := tokenize(s, " \t\n");
		if (nargs > 0)
		case hd args {
		"sel" =>
			ss := fixpos(int nth(args, 1), pi.blks.blen());
			se := fixpos(int nth(args, 2), pi.blks.blen());
			if (se < ss)
				se = ss;
			pi.s0 = ss;
			if (ss != pi.f.ss || se != pi.f.se){
				pi.f.sel(ss, se);
				p.flags |= Predraw;
			}
			pi.fsss = pi.f.ss;
			pi.fsse = pi.f.se;
		"mark" =>
			pi.f.mark = fixpos(int nth(args, 1), pi.blks.blen());
		"tab" =>
			tab := int nth(args, 1);
			if (tab < 3)
				tab = 3;
			if (tab > 20)
				tab = 20;
			if (pi.f.tabsz != tab){
				pi.f.tabsz = tab;
				pi.f.resize(pi.f.r, pi.f.i);
				p.flags |= Predraw;
			}
		* =>
			; # ignore others
		}
	}
	if (odirty && !(p.flags&Pdirty)){
		pi.edits.cpos = pi.edits.pos;
		# BUG: potential race here: User puts, editor issues a clean ctl,
		# and the user adds an edit in the mean while.
		# If this happens, we could just set Pdirty again.
		# But let see if the race is a real one.
		eds := pi.edits.sync();
		if (eds != nil)
			panic("text pctl: clean while dirty edits");
	}
	if (ofont != p.font)
		pi.f.resize(pi.f.r, pi.f.i);	# recompute widths and init
}

# Any of:
# o/mero: /path/to/panel ins pos str
# o/mero: /path/to/panel del pos n
pevent(p: ref Panel, ev: string)
{
	pi := pimpl(p);
	if (!(p.flags&Pshown))
		pi.f.i = nil;	# avoid drawing
	n := strchr(ev, ' ');
	ev = ev[n+1:];
	n = strchr(ev, ' ');
	ev = ev[n+1:];		# ins|del ...
	if (len ev < 5){
		fprint(stderr, "o/live: pevent: bad event\n");
		return;
	}
	op := ev[0:3];
	ev = ev[3+1:];		# pos ...
	n = strchr(ev, ' ');
	if (n < 0){
		fprint(stderr, "o/live: pevent: short event\n");
		return;
	}
	pos := int ev[0:n];
	ev = ev[n+1:];		# n|str
	if (len ev == 0)
		return;
	ev = ev[0:len ev -1];	# remove \n
	if (op == "del"){
		n = int ev;
		del(p, n, pos);
		pi.f.del(pos, n);
	} else {
		s := unescape(ev);
		ins(p, s, pos);
		pi.f.ins(pos, len s);
		if ((p.flags&Pline) && pos + len s> pi.f.ss)
			pi.f.sel(pos, pos);
	}
	pi.s0 = pi.f.ss;
	p.flags |= Pdirty;
	pdraw(p);
}

writepsel(p: ref Panel)
{
	fd := open("/dev/sel", OWRITE|OTRUNC);
	if (fd != nil)
		fprint(fd, "%s\n", p.path);
}

movetoshow(p: ref Panel, pos: int)
{
	pi := pimpl(p);
	if (pos < pi.f.pos || (pos > pi.f.pos + pi.f.nr)){
		npos := pi.gotopos(pos);
		pi.f.init(pi.blks, npos);
		pi.f.scroll(-pi.f.sz.y/2);
	}
}

cut(p: ref Panel, putsnarf: int): int
{
	pi := pimpl(p);
	nr := pi.f.se - pi.f.ss;
	if (nr == 0)
		return 0;
	pos := pi.f.ss;
	s := "";
	if (!(p.flags&Pedit)){
		t := pi.blks.pack();
		s = t.s[pi.f.se:pi.f.se];
		pi.f.sel(pi.f.ss, pi.f.ss);
	} else {
		s = del(p, nr, pos);
		pi.f.del(pos, nr);
	}
	pi.s0 = pi.f.ss;
	if (putsnarf){
		writesnarf(s);
		writepsel(p);
	}
	return 1;
}

paste(p: ref Panel, pos: int): int
{
	if (!(p.flags&Pedit))
		return 0;
	pi := pimpl(p);
	s := readsnarf();
	if (len s == 0)
		return 0;
	ins(p, s, pos);
	pi.f.ins(pos, len s);
	pi.f.sel(pos, pos + len s);
	pi.s0 = pi.f.ss;
	writepsel(p);
	return 1;
}

lastcmd: string;

exec(p: ref Panel, pos: int)
{
	pi := pimpl(p);
	s := pi.blks.pack();
	(ws, we) := pi.wordat(pos, 1, 1);
	c := s.s[ws:we];
	lastcmd = c;
	pi.f.sel(ws, we);
	pi.s0 = pi.f.ss;
	if (c == "Exit")
		terminate();
	else
		p.fsctl("exec " + c + "\n", 1);
}

look(p: ref Panel, pos: int)
{
	if (seltext != nil){
		p.fsctl("look " + seltext + "\n", 1);
		return;
	}
	pi := pimpl(p);
	s := pi.blks.pack();
	(ws, we) := pi.wordat(pos, 1, 1);
	c := s.s[ws:we];
	pi.f.sel(ws, we);
	pi.s0 = pi.f.ss;
	p.fsctl("look " + c + "\n", 1);
}

apply(p: ref Panel, nil: int)
{
	if (p.flags&Pedit)
	if (lastcmd != nil){
		writepsel(p);
		p.fsctl("apply " + lastcmd + "\n", 1);
	}
}

search(p: ref Panel, pos: int)
{
	ws, we: int;
	txt: string;
	pi := pimpl(p);
	s := pi.blks.pack();
	if (seltext == nil){
		(ws, we) = pi.wordat(pos, 0, 1);
		txt = s.s[ws:we];
	} else {
		we = pos+1;
		txt = seltext;
	}
	i := strstr(s.s[we:], txt);
	if (i < 0)
		i = strstr(s.s, txt);
	else
		i += we;
	if (i < 0)
		return;
	if (debug['T'])
		fprint(stderr, "search: %s: pos %d\n", dtxt(txt), i);
	pi.f.sel(i, i + len txt);
	pi.s0 = pi.f.ss;
	movetoshow(p, pi.f.ss);
	pt := pi.f.pos2pt(pi.f.ss);
	pt = pt.add((p.font.width(txt[0:1])/2, p.font.height -2));
	win.wmctl("ptr " + string pt.x + " " + string pt.y);
}

# Select requires us to coallesce mouse events that would do nothing
# but update further our possition.
getmouse(mc: chan of ref Cpointer): ref Cpointer
{
	m := <-mc;
	for(;;){
		mm: ref Cpointer;
		mm = nil;
		# nbrecv
		alt {
		mm = <-mc => ;
		* => ;
		}
		if (mm == nil || mm.buttons != m.buttons)
			break;
		m = mm;
	}
	return m;
}

select(p: ref Panel, pos: int, m: ref Cpointer, mc: chan of ref Cpointer): ref Cpointer
{
	pi := pimpl(p);
	b := m.buttons;
	s := pi.blks.pack();
	pi.f.sel(pos, pos);
	if (m.flags&(CMdouble|CMtriple)){
		long := m.flags&CMtriple;
		(ws, we) := pi.wordat(pos, long, 0);
		if (debug['T'])
			fprint(stderr, "getword: pos %d long %d: %d %d %s\n",
				pos, long, ws, we, dtxt(s.s[ws:we]));
		pi.f.sel(ws, we);
		pi.s0 = pi.f.ss;
		writepsel(p);
		do {
			m = <-mc;
		} while (m.buttons == b);
	} else {
		pi.s0 = pos;
		do {
			if (m.xy.y < pi.f.r.min.y){		# scroll up
				pi.f.scroll(-1-(pi.f.r.min.y-m.xy.y)/p.font.height);
				pi.f.sel(pi.f.pos, pi.s0);
				sys->sleep(100);
			} else if (m.xy.y > pi.f.r.max.y){	# scroll down
				pi.f.scroll(1+(m.xy.y-pi.f.r.max.y)/p.font.height);
				pi.f.sel(pi.s0, pi.f.pos + pi.f.nr);
				sys->sleep(100);
			} else {
				pos = pi.f.pt2pos(m.xy);
				if (pos < pi.s0)
					pi.f.sel(pos, pi.s0);
				else
					pi.f.sel(pi.s0, pos);
			}
			m = getmouse(mc);
		} while (m.buttons == b);
	}
	if (pi.f.ss == pi.f.se)
		seltext = nil;
	else
		seltext = s.s[pi.f.ss:pi.f.se];
	return m;
}

pmouse1(p: ref Panel, pos: int, m: ref Cpointer, mc: chan of ref Cpointer)
{
	dirties := 0;
	pi := pimpl(p);
	m = select(p, pos, m, mc);
	case m.buttons {
	0 =>
		return;
	3 =>
		if (cut(p, 1))
			dirties = 1;
	5 =>
		if (cut(p, 0)){
			pos = pi.f.ss;
			dirties = 1;
		}
		if (paste(p, pos))
			dirties = 1;
	}
	if (dirties)
		dirty(p, 1);
	while(m.buttons)
		m = <-mc;
}

pmouse2(p: ref Panel, pos: int, m: ref Cpointer, mc: chan of ref Cpointer)
{
	pi := pimpl(p);
	if (pi.f.ss != pi.f.se && pos >= pi.f.ss && pos < pi.f.se){
		m = <-mc;
		if (m.buttons == 0)
			exec(p, pos);
	} else {
		m = select(p, pos, m, mc);
		if (m.buttons == 0)
			exec(p, pos);
	}
	while(m.buttons)
		m = <-mc;
}

# To avoid blinking, we use double buffering during
# scroll operations. This is because we redraw the whole frame,
# and do not shift rectangles around. This makes it easier to draw
# overlayed scroll bars.

scrdraw(p: ref Panel, i: ref Image): ref Image
{
	pi := pimpl(p);
	r := Rect((0,0), (p.rect.dx(), p.rect.dy()));
	if (i == nil){
		i = win.display.newimage(r, win.image.chans, 0, Draw->White);
		pi.f.resize(r, i);
	}
	drawscrollbar(p, i);
	win.image.draw(p.rect, i, nil, (0,0));
	return i;
}

scrdone(p: ref Panel)
{
	pi := pimpl(p);
	pi.f.resize(p.rect, win.image);
}

drawscrollbar(p: ref Panel, i: ref Image)
{
	Barwid: con 25;
	Barht: con 120;

	pi := pimpl(p);
	bar, r: Rect;
	bar.max.x = r.max.x = i.r.max.x - Inset - 3;
	bar.min.x = r.min.x = r.max.x - Barwid;
	r.min.y = i.r.min.y + Inset;
	ysz := Barht;
	r.max.y = r.min.y + ysz;
	if (r.max.y + 3 > i.r.max.y){
		r.max.y = i.r.max.y - 3;
		ysz = r.dy();
	}
	i.draw(r.addpt((2,2)), cols[TEXT], cols[SHAD], (0,0));
	i.draw(r, cols[CLEAR], cols[SHAD], (0,0));
	l := len pi.blks.b[0].s;
	y0 := dy := 0;
	if (l > 0){
		y0 = ysz * pi.f.pos / l;
		dy = ysz * pi.f.nr / l;
		if (dy < 3)
			dy = 3;
	} else {
		y0 = 0;
		dy = r.dy();
	}
	bar.min.y = r.min.y + y0;
	bar.max.y = bar.min.y + dy;
	if (bar.max.y > r.max.y)
		bar.max.y = r.max.y;
	if (bar.min.y > bar.max.y - 2)
		bar.min.y = bar.max.y - 2;
	i.draw(bar.addpt((3,3)), cols[TEXT], cols[SHAD], (0,0));
	i.draw(bar, cols[SET], nil, (0,0));
	i.border(r, 1, cols[BORD], (0,0));
}

jumpscale(p: ref Panel, xy: Point): real
{
	pi := pimpl(p);
	s := pi.blks.pack();
	dy := xy.y - p.rect.min.y;
	if (dy > p.rect.max.y - xy.y)
		dy = p.rect.max.y - xy.y;
	dc := len s.s - pi.f.pos;
	if (dc < pi.f.pos)
		dc = pi.f.pos;
	if (dy < 1)
		dy = 1;
	return (real dc) / (real dy);
}

pmouse3scrl(p: ref Panel, pos: int, m: ref Cpointer, mc: chan of ref Cpointer)
{
	pi := pimpl(p);
	xy := m.xy;
	jfactor := real 0;
	jy := xy.y;
	old := pi.f.pos;
	i := scrdraw(p, nil);
	b := m.buttons;
	do {
		if (jfactor <= real 0.00001){
			pos = pi.f.pos;
			jfactor = jumpscale(p, xy);
		}
		jpos := pos + int (real (xy.y - jy) * jfactor);
		if (jpos != old)
			jpos = pi.gotopos(jpos);
		if (jpos == old)
			; # sys->sleep(10);
		else {
			old = jpos;
			pi.f.init(pi.blks, jpos);
			scrdraw(p, i);
		}
		more := 0;
		m = <- mc;
		do {
			alt {
			m = <-mc =>
				more=1;
			* =>
				more = 0;
			}
		} while(more && m.buttons == b);
		xy = m.xy;
	} while(m.buttons);
	scrdone(p);
}

pmouse3(p: ref Panel, pos: int, m: ref Cpointer, mc: chan of ref Cpointer)
{
	pi := pimpl(p);
	m = <- mc;
	case m.buttons {
	0 =>
		textmenu.last = pi.mlast;
		cmd := textmenu.run(m, mc);
		if (debug['T'])
			fprint(stderr, "pmouse3: cmd: %s at %d\n", cmd, pos);
		case cmd {
		"scroll" =>
			pmouse3scrl(p, pos, m, mc);
		"Find" =>
			search(p, pos);
		"Open" =>
			look(p, pos);
		"Exec" =>
			exec(p, pos);
		"Apply" =>
			apply(p, pos);
		"Cut" =>
			if (cut(p, 1))
				dirty(p, 1);
			textmenu.last = Mpaste;
		"Paste" =>
			if (paste(p, pos))
				dirty(p, 1);
		* =>
			p.fsctl("exec " + cmd + "\n", 1);
		}
		pi.mlast = textmenu.last;
	4 =>
		pmouse3scrl(p, pos, m, mc);
	* =>
		do
			m = <-mc;
		while (m.buttons);
	}
}

pmouse(p: ref Panel, m: ref Cpointer, mc: chan of ref Cpointer)
{
	if ((p.flags&Ptag) && intag(p, m.xy)){
		tagmouse(tree, p, m, mc);
		return;
	}
	pi := pimpl(p);
	# mouse movement syncs any pending edit.
	eds := pi.edits.sync();
	if (eds != nil){
		syncedits(p, eds);
		syncchk(p);
	}
	pos := pi.f.pt2pos(m.xy);
	case m.buttons {
	1 =>
		# For tbls, this should probably allow drag&drop
		pmouse1(p, pos, m, mc);
		if (debug['T'] > 1)
			pi.dump();
	2 =>
		pmouse2(p, pos, m, mc);
		if (debug['T'] > 1)
			pi.dump();
	4 =>
		pmouse3(p, pos, m, mc);
		if (debug['T'] > 1)
			pi.dump();
	}
}

pkbd(p: ref Panel, k: int)
{
	Killword:	con 16r17;	# C-w
	pi := pimpl(p);
	pos := pi.f.ss;
	s := "";
	s[0] = k;
	if (!(p.flags&Pedit)){
		p.fsctl("keys " + s + "\n", 1);
		return;
	}
	case k {
	'\b' =>
		if (!cut(p, 1) && pos > 0){
			pos--;
			movetoshow(p, pos);
			del(p, 1, pos);
			pi.f.del(pos, 1);
			pi.s0= pi.f.ss;
		}
		dirty(p, 1);
	Killword =>
		(ws, we) := pi.wordat(pos, 1, 1);
		if (ws != we){
			pos = ws;
			n := we - ws;
			movetoshow(p, pos);
			del(p, n, pos);
			pi.f.del(pos, n);
			pi.s0 = pi.f.ss;
		}
	Keyboard->Del =>
		p.fsctl("interrupt\n", 1);
	Keyboard->Left or Keyboard->Right=>
		if (k == Keyboard->Left)
			pos = undo(p);
		else
			pos = redo(p);
		if (pos >= 0){
			dirty(p, pi.edits.pos != pi.edits.cpos);
			pi.f.sel(pos, pos);
			pi.s0 = pos;
			movetoshow(p, pos);
		}
	Keyboard->Up or Keyboard->Down =>
		if (p.flags&Pline)
			return;
		n := pi.f.sz.y / 3;
		if (n < 2)
			n = 2;
		if (k == Keyboard->Up)
			n = -n;
		pi.f.scroll(n);
	Keyboard->Esc =>
		if (pi.s0 < pos)
			pi.f.sel(pi.s0, pos);
		else
			pi.f.sel(pos, pi.s0);
	* =>
		if (k == '\n' && (p.flags&Pline)){
			if (pi.s0 < pos)
				pi.f.sel(pi.s0, pos);
			else
				pi.f.sel(pos, pi.s0);
			exec(p, pos);
		} else {
			cut(p, 1);
			if (!(p.flags&Pline))
				movetoshow(p, pos);
			ins(p, s, pos);
			pi.f.ins(pos, len s);
			pt := pi.f.pos2pt(pos);
			if (pt.y + p.font.height >= pi.f.r.max.y && !(p.flags&Pline))
				pi.f.scroll(1);
			dirty(p, 1);
			if ((p.flags&Pedit) && (k == '\n' || (p.flags&Pline)))
				settextsize(p);
		}
	}
	if (debug['T'] > 1)
		pi.dump();
}

psync(p: ref Panel)
{
fprint(stderr, "owptext: sync called\n");

	pi := pimpl(p);
	fd := open(p.path + "/data", OWRITE|OTRUNC);
	if (fd == nil)
		return;
	s := pi.blks.pack();
	data := array of byte s.s;
	if (write(fd, data, len data) != len data)
		fprint(stderr, "o/live: sync %s: %r\n", p.path);
}

Bell Labs OSI certified Powered by Plan 9

(Return to Plan 9 Home Page)

Copyright © 2021 Plan 9 Foundation. All Rights Reserved.
Comments to webmaster@9p.io.