Plan 9 from Bell Labs’s /sys/src/libcontrol/edit.c

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


#include <u.h>
#include <libc.h>
#include <thread.h>
#include <draw.h>
#include <mouse.h>
#include <keyboard.h>
#include <control.h>
#include <editcontrol.h>
#include "string.h"

#define	FRTICKW	3
#define	FRTICKH	24

int debug;
enum{
	EAppend,
	EAddrune,
	EBorder,
	EBordercolor,
	EClear,
	EContext,
	ECut,
	EDelete,
	EDelrune,
EDump,
	EFocus,
	EHide,
	EImage,
	EInsert,
	EJoin,
	EPaste,
	ERect,
	ERedo,
	ERedraw,
	EReveal,
	EScroll,
	ESelect,
	ESelectcolor,
	EShow,
	ESize,
	ESnarf,
	ESpacing,
	ESplit,
	EStringset,
	ETextcolor,
	ETop,
	EUndo,
	EValue,
};

static char *cmds[] = {
	[EAppend] =		"append",	//	runes
	[EAddrune] =		"addrune",	//	runes
	[EBorder] =		"border",
	[EBordercolor] =	"bordercolor",
	[EClear] =		"clear",
	[EContext] =		"context",	//	name attr=value ...
	[ECut] =		"cut",		//	selection #
	[EDelete] =		"delete",	//	string#
	[EDelrune] =		"delrune",	//	# of runes
	[EDump] =		"dump",		//	debugging
	[EFocus] = 		"focus",
	[EHide] =		"hide",
	[EImage] =		"image",
	[EInsert] =		"insert",	//	string#, { runes }
	[EJoin] =		"join",		//	position
	[EPaste] =		"paste",	//	selection #
	[ERect] =		"rect",
	[ERedo] =		"redo",
	[ERedraw] =		"redraw",
	[EReveal] =		"reveal",
	[EScroll] =		"scroll",
	[ESelect] =		"select",
	[ESelectcolor] =	"selectcolor",	//	color, color, color (empty names allowed)
	[EShow] =		"show",
	[ESize] =		"size",
	[ESnarf] =		"snarf",	//	selection #
	[ESpacing] =		"spacing",	//	[ font | npixels ]
	[ESplit] =		"split",	//	position
	[EStringset] =		"stringset",	//	0x%p, address of stringset
	[ETextcolor] =		"textcolor",
	[ETop] =		"top",		//	string#, rune#
	[EUndo] =		"undo",
	[EValue] =		"value",	//	string#, fontname, runes
	nil
};

/*	The edit control manages the display of a series of Strings.  Strings are
	numbered.  The value of a String is a Font, and a series of zero or more
	Runes.  A Position is a String number and a Rune number within the String.
	Top is the Position of the first Rune visible in the edit control.

	The edit control need not know the value of each String.  When a String
	whose value is not known becomes visible in the edit control, the control
	will generate an event requesting the value.

	Keystrokes generate events; they do not directly affect the display, nor the
	contents of any String.

	The mouse can be used to make selections and generate events.  Each button
	event is reported in an event which reports which buttons are down after the
	event.  Position is reported in terms of String/Rune number.
	In addition, text selection colors can be associated with mouse buttons
	1, 2 and 3.  If a color is associated with button n, then pressing that button
	by itself will initiate a selection in the associated color.  Releasing the button
	or pressing any other button will freeze the selection (and, as mentioned, will
	report the button event).  The three selections may also be set via a select
	command.
	Creating a selection will remove the previous selection for the same button.

	If line-spacing is defined, it determines the height given to each line.  If it
	is not defined, each line is given the highest font height occurring on the line.
	Lines are broken either at white space, or after the last rune that fits, depending
	on the line-break setting.  A line break always occurs at a newline character.

	The Addrune (Delrune) commands replace (delete) the given selection,
	or they insert characters after (delete the character _posbefore) the typing tick;
	the selection is replaced by a  null selection (typing tick) after the character(s)
	inserted (_posbefore the character deleted).  Addrune adds its characters to
	the String containing the beginning of the Selection.
	Delrune at the beginning of the first String is a no-op.
	When Scroll is on, an Addrune or Delrune command will automatically set
	Top to make the cursor visible.
*/

Context* context;

static void scroll(Edit *e, Position top);

static void
editclean(Edit *e)
{
	Selection *s;
	Line *l, *ll;

	debugprint(DBGCLEAN, "editclean\n");
	for (s = e->sel; s < e->sel + nelem(e->sel); s++){
		s->mpl.f = nil;
		s->mpr.f = nil;
		s->mpl.p = Pt(-1, -1);
		s->mpr.p = Pt(-1, -1);
	}
	for (l = e->l; l; l = ll){
		freefrag(l->f);
		ll = l->next;
		freeline(l);
	}
	e->l = nil;
}

static void
editclear(Edit *e)
{
	editclean(e);
	freestringset(e->ss);
	e->ss = newstringset("default");
}

static void
editfree(Control *c)
{
	int i;
	Edit *e;

	e = (Edit*)c;
	_putctlimage(e->image);
	_putctlimage(e->textcolor);
	_putctlimage(e->bordercolor);
	for(i=0; i<nelem(e->sel); i++)
		if (e->sel[i].color)
			_putctlimage(e->sel[i].color);
	editclear(e);
	free(e->ss->string);
}

void
adjustlines(Edit *e, Undo *c)
{
	Line *l;
	Frag *f;

	for (l = e->l; l; l = l->next){
		for (f = l->f; f; f = f->next){
			_posadjust(c, &f->spos);
			_posadjust(c, &f->epos);
			assert(!_posbefore(f->epos, f->spos));
			if (f->epos.str > f->spos.str){
				f->epos.str = f->spos.str;
				f->epos.rn = e->ss->string[f->epos.str]->n;
			}
		}
	}
}

void
adjustselections(Edit *e, Undo *c)
{
	Selection *sel;

	for (sel = e->sel; sel < e->sel + nelem(e->sel); sel++){
		if ((sel->state & (Dragging|Set)) == 0)
			continue;
		_posadjust(c, &sel->mpl.pos);
		_posadjust(c, &sel->mpr.pos);
	}
}

static void
showselect(Edit *e, Mpos *p1, Mpos *p2, int which, int sel)
{
	Frag *f;
	Rune *rp, *ep;
	Stringset *ss;
	String *s;
	Context *c;
	Position lpos, rpos;
	Image *color;
	Point p;
	Rectangle r;
	int i;
	Line *l;

	debugprint(DBGSELECT, "select: %d/%d %d/%d %d\n",
			p1->pos.str, p1->pos.rn, p2->pos.str, p2->pos.rn, sel);
	ss = e->ss;
	if (_posequal(p1->pos, p2->pos))
		return;
	assert(_posbefore(p1->pos, p2->pos));
	if (!_posbefore(e->top, p2->pos) || !_posbefore(p2->pos, e->bot))
		return;	// that was easy
	if (_posbefore(p1->pos, e->top))
		p1->pos = e->top;
	if (_posbefore(e->bot, p2->pos))
		p2->pos = e->bot;
	if (sel)
		color = e->sel[which].color->image;
	else{
		// deselecting, check for clashes with other selections
		for (i = 0; i < nelem(e->sel); i++) {
			if (i == which || (e->sel[i].state & Set) == 0)
				continue;
			if (!_posbefore(p1->pos, e->sel[i].mpr.pos) || !_posbefore(e->sel[i].mpl.pos, p2->pos))
				continue;
			debugprint(DBGSELECT, "select: clash with selection %d\n", i);
			if (!_posbefore(p1->pos, e->sel[i].mpl.pos)){
				if (_posbefore(e->sel[i].mpr.pos, p2->pos)){
					debugprint(DBGSELECT, "select: left clash\n");
					showselect(e, p1, &e->sel[i].mpr, i, 1);
					showselect(e, &e->sel[i].mpr, p2, which, 0);
				}else{
					debugprint(DBGSELECT, "select: complete clash\n");
					showselect(e, p1, p2, i, 1);
				}
			}else{
				if (_posbefore(e->sel[i].mpr.pos, p2->pos)){
					debugprint(DBGSELECT, "select: internal clash\n");
					showselect(e, p1, &e->sel[i].mpl, which, 0);
					showselect(e, &e->sel[i].mpl, &e->sel[i].mpr, i, 1);
					showselect(e, &e->sel[i].mpr, p2, which, 0);
				}else{
					debugprint(DBGSELECT, "select: right clash\n");
					showselect(e, p1, &e->sel[i].mpl, which, 0);
					showselect(e, &e->sel[i].mpl, p2, i, 1);
				}
			}
			return;
		}
		color = e->image->image;
	}
	f = findfrag(e, p1->pos);
	assert(f);
	for(;;) {
		debugprint(DBGSELECT, "select: new frag %d/%d-%d/%d %R\n", f->spos.str, f->spos.rn, f->epos.str, f->epos.rn, fragr(f));
		s = stringof(ss, f->spos);
		if (s == nil){
			SET(s);
			abort();
		}
		c = _contextnamed(s->context);
		r = fragr(f);
		if (_posbefore(f->spos, p1->pos)){
			debugprint(DBGSELECT, "select: start halfway\n");
			// start half way
			assert(f->spos.str == p1->pos.str);
			lpos = p1->pos;
			p = p1->p;
			r.min.x = p1->p.x;
		}else{
			lpos = f->spos;
			p = Pt(f->minx, f->l->r.min.y);
		}
		rp = s->r + lpos.rn;
		rpos = f->epos;
		if (_posbefore(p2->pos, rpos)){
			rpos = p2->pos;
			r.max.x = p2->p.x;
		}
		ep = s->r + rpos.rn;
		// selection is from rune ptr rp to ep, and from position lpos to rpos
		debugprint(DBGSELECT, "select: %P %R %ld\n", p, r, ep-rp);
		assert(ep >= rp);
		if (rp != ep){
			if (*rp < ' '){
				assert(ep - rp == 1);
				draw(e->screen, fragr(f), color, nil, ZP);
			}else
				_string(e->screen, p, e->textcolor->image, ZP, c->f->font, nil, rp, ep-rp, r, color, ZP);
		}
		if (!_posbefore(rpos, p2->pos)){
			debugprint(DBGSELECT, "select: finished\n");
			break;
		}
		if (f->next){
			debugprint(DBGSELECT, "select: next frag in line\n");
			f = f->next;
		}else {
			debugprint(DBGSELECT, "select: frag in line after 0x%p\n", f->l);
			l = f->l;
			f = nil;
			if (l = l->next)
				while (l){
					if (f = l->f)
						break;
					debugprint(DBGSELECT, "select: no frag in line 0x%p\n", l);
					l = l->next;
				}
			if (f == nil){
				debugprint(DBGSELECT, "select: no more frags\n");
				break;
			}
		}
	}
}

static Mpos
p2pos(Edit *e, Point p)
{
	Mpos m;
	Line *l, *ll;
	Frag *f;
	int dx, w;
	Rune *rp;
	Stringset *ss;
	String *s;
	Context *c;
	Rectangle r;

	ss = e->ss;
	r = insetrect(e->rect, e->border);
	m.p = r.min;
	m.pos.str = -1;
	m.f = nil;
	m.flags = 0;
	if (p.x >= r.max.x) p.x = r.max.x - 1;
	if (p.x < r.min.x) p.x = r.min.x;
	if (p.y >= r.max.y) p.y = r.max.y - 1;
	if (p.y < r.min.y) p.y = r.min.y;
	ll = nil;
	for (l = e->l; l; l = l->next){
		ll = l;
		if (ptinrect(p, l->r)){
			m.p = l->r.min;
			for (f = l->f; f; f = f->next){
				m.pos = f->spos;
				m.f = f;
				if (ptinrect(p, fragr(f))){
					if ((s = stringof(ss, m.pos)) == nil)
						abort();
					c = _contextnamed(s->context);
					rp = s->r + m.pos.rn;
					dx= 0;
					if (*rp >= ' ')
						for( ; rp < s->r + f->epos.rn; rp++){
							w = runestringnwidth(c->f->font, rp, 1);
							if (f->minx + dx + w >= p.x)
								break;
							dx += w;
						}
					m.pos.rn = rp - s->r;
					m.p.x = f->minx + dx;
					return m;
				}
				m.p.x = f->maxx;
				m.pos = f->epos;
			}
			m.flags = Eol;
			return m;
		}
		m.f = nil;
	}
	if (ll){
		m.p = ll->rplay.min;
		for (f = ll->f; f; f = f->next)
			m.f = f;
	}
	m.pos = e->bot;
	m.flags = Bof;
	return m;
}

static int
pos2mpos(Edit *e, Position pos, Mpos *mp)
{
	String *s;
	Stringset *ss;
	Context *c;

	ss = e->ss;
	mp->pos = pos;
	mp->p = Pt(-1, -1);
	mp->f = nil;
	if ((mp->f = findfrag(e, pos)) == nil){
		mp->flags = Outside;
		return 0;
	}
	mp->flags = 0;
	assert(mp->f->spos.str == pos.str);
	s = stringof(ss, pos);
	assert(s);
	c = _contextnamed(s->context);
	mp->p = Pt(mp->f->minx, mp->f->l->r.min.y);
	mp->p.x += runestringnwidth(c->f->font, s->r + mp->f->spos.rn, pos.rn - mp->f->spos.rn);
	return 1;
}

void
layoutselections(Edit *e)
{
	Selection *sel;

	if (e->ss == nil)
		return;
	for (sel = e->sel; sel < e->sel + nelem(e->sel); sel++){
		if ((sel->state & (Dragging|Set)) == 0)
			continue;
		pos2mpos(e, sel->mpl.pos, &sel->mpl);
		pos2mpos(e, sel->mpr.pos, &sel->mpr);
	}
}

static void
frinittick(Edit *e)
{
	/* Shamelessly stolen from libframe */
	Image *b;
	Selection *s;

	b = display->image;
	for (s = e->sel; s < e->sel + nelem(e->sel); s++){
		if(s->tick)
			free(s->tick);
		s->tick = allocimage(display, Rect(0, 0, FRTICKW, FRTICKH), b->chan, 0, DWhite);
		if(s->tick == nil)
			return;
		s->tickback = allocimage(display, s->tick->r, b->chan, 0, DWhite);
		if(s->tickback == 0){
			freeimage(s->tick);
			s->tick = 0;
			return;
		}
		s->tickh = 0;
		s->state &= ~Ticked;
	}
}

static void
frtick(Edit *e, Selection *sel, int ticked)
{
	Rectangle r;
	Stringset *ss;
	String *s;
	Context *c;
	int h;
	Mpos *mp;

	if ((ss = e->ss) == nil)
		return;
	/* Shamelessly stolen from libframe */
	mp = &sel->mpl;
	debugprint(DBGTICK, "frtick: %P %d\n", mp->p, ticked);
	if((sel->state & Ticked) == ticked || sel->tick==0)
		return;
	if (pos2mpos(e, mp->pos, mp) == 0)
		return;
	r = Rect(mp->p.x-1, mp->p.y, mp->p.x+FRTICKW-1, mp->p.y + FRTICKH);
	if(ticked){
		if(_posbefore(mp->pos, e->top) || _posbefore(e->bot, mp->pos))
			return;
		draw(sel->tickback, sel->tickback->r, screen, nil, r.min);
		s = stringof(ss, mp->f->spos);
		assert(s);
		c = _contextnamed(s->context);
		h = c->f->font->height;
		if (h > FRTICKH) h = FRTICKH;
		if (h != sel->tickh){
			debugprint(DBGTICK, "frtick: new cursor ht %d\n", h);
			/* background color */
			draw(sel->tick, sel->tick->r, e->image->image, nil, ZP);
			/* vertical line */
			draw(sel->tick, Rect(FRTICKW/2, 0, FRTICKW/2+1, h), display->black, nil, ZP);
			/* box on each end */
			draw(sel->tick, Rect(0, 0, FRTICKW, FRTICKW), e->textcolor->image, nil, ZP);
			draw(sel->tick, Rect(0, h-FRTICKW, FRTICKW, h), e->textcolor->image, nil, ZP);
			sel->tickh = h;
		}
		draw(e->screen, r, sel->tick, nil, ZP);
		sel->state |= ticked;
	}else{
		draw(e->screen, r, sel->tickback, nil, ZP);
		sel->state &= ~Ticked;
	}
}

static void
showticks(Edit *e)
{
	Selection *s;

	for (s = e->sel; s < e->sel + nelem(e->sel); s++)
		if ((s->state & (Dragging|Set)) && _posequal(s->mpl.pos, s->mpr.pos)){
			s->state &= ~Ticked;
			frtick(e, s, Ticked);
		}
}

static void
hideticks(Edit *e)
{
	Selection *s;

	for (s = e->sel; s < e->sel + nelem(e->sel); s++)
		if (s->state & Ticked)
			frtick(e, s, 0);
}

static void
drag(Edit *e, Mouse *m, int cursel){
	Selection *s;
	Mpos mp;
	Point p;
	Rectangle r;
	Position pos;

	if (debug) fprint(2, "drag\n");
	r = insetrect(e->rect, e->border);
	s = e->sel + cursel;
	assert(s->state & Dragging);
	p = m->xy;
	if (p.y >= r.max.y) {
		p.y = r.max.y - 1;
		pos = e->top;
		if (_posinc(e->ss, &pos) && _stringfindrune(e->ss, &pos, '\n')){
			if (debug) fprint(2, "scroll down from %d/%d to %d/%d\n",
				e->top.str, e->top.rn, pos.str, pos.rn);
			scroll(e, pos);
		}
	}
	if (p.y < r.min.y)  {
		pos = e->top;
		p.y = r.min.y;
		if (!_posdec(e->ss, &pos) || !_stringfindrrune(e->ss, &pos, '\n')){
			pos.str = 0;
			pos.rn = 0;
		}
		if (debug) fprint(2, "scroll up from %d/%d to %d/%d\n",
			e->top.str, e->top.rn, pos.str, pos.rn);
		scroll(e, pos);
	}
	mp = p2pos(e, m->xy);
	if (mp.flags & Outside)
		return;
	if (s->state & Drag1){
		if (_posequal(s->mpr.pos,  mp.pos))
			return;	// nothing changed
		if (_posbefore(s->mpr.pos, mp.pos)){
			// selection became bigger
			showselect(e, &s->mpr, &mp, cursel, 1);
			s->mpr = mp;
		}else if (_posbefore(mp.pos, s->mpl.pos)){
			// went past beginning of selection
			showselect(e, &s->mpl, &s->mpr, cursel, 0);
			showselect(e, &mp, &s->mpl, cursel, 1);
			s->mpr = s->mpl;
			s->mpl = mp;
			s->state &= ~Drag1;
		} else {
			// selection became smaller
			showselect(e, &mp, &s->mpr, cursel, 0);
			s->mpr = mp;
		}
	}else{
		if (_posequal(s->mpl.pos, mp.pos))
			return;	// nothing changed
		if (_posbefore(mp.pos, s->mpl.pos)){
			// selection became bigger
			showselect(e, &mp, &s->mpl, cursel, 1);
			s->mpl = mp;
		}else if (_posbefore(s->mpr.pos, mp.pos)){
			// went past beginning of selection
			showselect(e, &s->mpl, &s->mpr, cursel, 0);
			showselect(e, &s->mpr, &mp, cursel, 1);
			s->mpl = s->mpr;
			s->mpr = mp;
			s->state |= Drag1;
		} else {
			// selection became smaller
			showselect(e, &s->mpl, &mp, cursel, 0);
			s->mpl = mp;
		}
	}
}

static void
editkey(Control *c, Rune *r)
{
	while (*r){
		switch(*r){
		case 0x000d:	//end
		case 0x0080:	//down arrow
		case 0xf00d:	//home
		case 0xf00e:	//up arrow
		case 0xf00f:	//page up
		case 0xf011:	//left arrow
		case 0xf012:	//right arrow
		case 0xf013:	//page down
			break;
		default:
			chanprint(c->event, "%q: key %d", c->name, *r);
		}
		r++;
	}
}

static void
editmouse(Control *c, Mouse *m)
{
	Edit *e;
	Mpos mp;
	static int cursel = -1;
	int sel;
	Selection *s;

	e = (Edit*)c;
	if (e->lastbut == 0 && (m->buttons & 7) == 0)
		return;
	if (cursel >= 0){
		s = e->sel + cursel;
		if (_posequal(s->mpl.pos, s->mpr.pos))
			frtick(e, s, 0);
		drag(e, m, cursel);
		if (_posequal(s->mpl.pos, s->mpr.pos))
			frtick(e, s, Ticked);
		flushimage(display, 1);
		if (e->lastbut != (m->buttons & 0x7)){
			// end selection, buttons changed
			s->state &= ~Dragging;
			s->state |= Set;
			chanprint(e->event, "%q: selection %d %d %d %d %d %d", e->name, cursel,
				s->mpl.pos.str, s->mpl.pos.rn, s->mpr.pos.str, s->mpr.pos.rn,
				m->buttons & 7);
			cursel = -1;
		}
		e->lastbut = m->buttons & 7;
		return;
	}
	sel = 0;
	mp = p2pos(e, m->xy);
	if (mp.flags & Outside)
		return;
	if (e->lastbut == 0)
		switch (m->buttons&7){
		case 4:
			sel++;
		case 2:
			sel++;
		case 1:
			s = e->sel + sel;
			if(s->state & Enabled){
				debugprint(DBGMOUSE, "start select\n");
				assert((s->state & Dragging) == 0);
				if (s->state & Set){
					if (_posequal(s->mpl.pos, s->mpr.pos))
						frtick(e, s, 0);
					else
						showselect(e, &s->mpl, &s->mpr, sel, 0);
				}
				s->state &= ~(Set|Drag1);
				s->state |= Dragging;
				s->mpl = mp;
				s->mpr = mp;
				cursel = sel;
				e->lastbut = m->buttons & 7;
				frtick(e, s, Ticked);
				flushimage(display, 1);
				return;
			}
		}
	if (e->lastbut != (m->buttons & 7)){
		chanprint(e->event, "%q: button %d %d %d", e->name,
				mp.pos.str, mp.pos.rn, m->buttons & 7);
		e->lastbut = m->buttons & 7;
	}
}

static void
editlayout(Edit *e)
{
	Line *l;

	debugprint(DBGLAYOUT, "editlayout\n");
	editclean(e);
	assert(e->l == nil);
	l = allocline();
	l->r = insetrect(e->rect, e->border);
	l->rplay = insetrect(e->rect, e->border);
	l->pos = e->top;
	e->l = l;
	e->lredraw = e->l;
	layoutrect(e);
	layoutselections(e);
	e->dirty = 0;
}

static void
scroll(Edit *e, Position top)
{
	Line *l;

	debugprint(DBGLAYOUT, "scroll: from %d/%d to %d/%d\n",
		e->top.str, e->top.rn, top.str, top.rn);
	hideticks(e);
	if (_posequal(e->top, top))
		return;
	e->top = top;
	l = allocline();
	l->r = insetrect(e->rect, e->border);
	l->rplay = insetrect(e->rect, e->border);
	l->pos = e->top;
	l->next = e->l;
	e->l = l;
	e->lredraw = e->l;
	layoutrect(e);
	layoutselections(e);
	showticks(e);
	flushimage(display, 1);
	e->dirty = 0;
}

static void
editshow(Edit *e)
{
	Rectangle r;
	Position pos;
	Frag *f;
	int lineno;
	Line *l;

	if (e->hidden){
		debugprint(DBGSHOW, "editshow: returns: hidden");
		return;
	}
	r = e->rect;
	debugprint(DBGSHOW, "editshow: clear rect %R", r);
	draw(e->screen, r, e->image->image, nil, e->image->image->r.min);
	if(e->border > 0){
		border(e->screen, r, e->border, e->bordercolor->image, e->bordercolor->image->r.min);
		r = insetrect(r, e->border);
		debugprint(DBGSHOW, "editshow: after border rect %R", r);
	}
	if (e->dirty){
		debugprint(DBGSHOW, ": dirty, using editlayout\n");
		editlayout(e);
	}else{
		debugprint(DBGSHOW, ": clean\n");
		if (pos.str < 0){
			debugprint(DBGSHOW, "editshow: early return\n");
			return;
		}
		lineno = 0;
		for (l = e->l; l; l = l->next) {
			debugprint(DBGSHOW, "editshow: line %d\n", lineno++);
			for (f = l->f; f; f = f->next)
				showfrag(e, f);
		}
	}
	showticks(e);
	flushimage(display, 1);
}

static void
editctl(Control *ctl, CParse *cp)
{
	int cmd, i, n, thistab, lasttab;
	Rectangle r;
	Position p1, p2;
	Stringset *ss;
	String *s;
	Edit *e;
	Selection *sel;
	Rune rune;
	Context *c;

	e = (Edit*)ctl;
	if (ss = e->ss)
		qlock(ss);
	cmd = _ctllookup(cp->args[0], cmds, nelem(cmds));
	switch(cmd){
	case EAddrune:
		_ctlargcount(e, cp, 2);
		sel = e->sel;
		if ((sel->state & Set) == 0)
			ctlerror("%q: inactive selection: %d", e->name, 0);
		rune = cp->iargs[1];
		if (ss == nil){
			ss = newstringset("default");
			ss->c = (Control*)e;
			qlock(ss);
			e->ss = ss;
		}
		hideticks(e);
		cut(ss, sel->mpl.pos, sel->mpr.pos);
		if (ss->nstring == 0){
			char buf[UTFmax+1];

			snprint(buf, sizeof buf, "%C", rune);
			s = allocstring("default", buf);
			_stringadd(ss, s, 0);
			freestring(s);
			sel->mpr.pos.rn++;
			sel->mpl.pos.rn++;
		}else{
			sel->mpr.pos = _addonerune(ss, sel->mpl.pos, rune);
			sel->mpl.pos = sel->mpr.pos;
		}
		e->dirty = 1;
		layoutrect(e);
		layoutselections(e);
		showticks(e);
		flushimage(display, 1);
		break;
	case EAppend:
		_ctlargcount(e, cp, 2);
		s = ctlmalloc(sizeof(String));
		s->context = strdup(e->curcontext->name);
		s->r = _ctlrunestr(cp->args[1]);
		s->n = runestrlen(s->r);
		s->ref = 1;
		debugprint(DBGSTRING, "allocate string: 0x%p %s [%d]", s, e->curcontext->name, s->n);
		if (ss == nil){
			ss = newstringset("default");
			ss->c = (Control*)e;
			qlock(ss);
			e->ss = ss;
		}
		_stringspace(ss, ss->nstring+1);
		ss->string[ss->nstring] = s;
		ss->nstring++;
		break;
	case EBorder:
		_ctlargcount(e, cp, 2);
		if(cp->iargs[1] < 0)
			ctlerror("%q: bad border: %c", e->name, cp->str);
		e->border = cp->iargs[1];
		e->dirty = 1;
		break;
	case EBordercolor:
		_ctlargcount(e, cp, 2);
		_setctlimage(e, &e->bordercolor, cp->args[1]);
		break;
	case EClear:
		_ctlargcount(e, cp, 1);
		editclear(e);
		e->dirty = 1;
		editshow(e);
		break;
	case EContext:
		if (cp->nargs < 2)
			ctlerror("%q: context: argcount", e->name);
		for (c = context; c; c = c->next)
			if (strcmp(c->name, cp->args[1]) == 0)
				break;
		if (c == nil){
			if (cp->nargs == 2)
				ctlerror("%q: context: non-existent context %q",
					e->name, cp->args[1]);
			c = _newcontext(cp->args[1]);
		}
		context = c;
		if (cp->nargs == 2)
			break;
		_setctlfont(e, &c->f, cp->args[2]);
		if (cp->nargs == 3)
			break;
		c->wordbreak = cp->iargs[3];
		if (cp->nargs == 4 && c->tabs[0] > 0)
			break;
		if (cp->nargs-4 >= nelem(c->tabs))
			ctlerror("%q: context: maximum number of tab settings is %d",
				e->name, nelem(c->tabs));
		lasttab = 0;
		for (i = 4; i < cp->nargs; i++){
			thistab = cp->iargs[i];
			if (thistab <= lasttab)
				ctlerror("%q: context: tab settings must monotonically increase",
					e->name);
			c->tabs[i-4] = thistab;
			lasttab = thistab;
		}
		// set remaining tabs
		for (i = i-4; i < nelem(c->tabs); i++){
			thistab = lasttab + stringwidth(c->f->font, "0000");
			c->tabs[i] = thistab;
			lasttab = thistab;
		}
		c->next = context;
		context = c;
		break;
	case ECut:
		_ctlargcount(e, cp, 2);
		i = cp->iargs[1];
		if (i < 0 || i >= nelem(e->sel))
			ctlerror("%q: nonexistent selection: %d", e->name, i);
		sel = e->sel + i;
		if ((sel->state & Set) == 0)
			ctlerror("%q: inactive selection: %d", e->name, i);
		if (ss == nil)
			ctlerror("%q: no stringset to cut from");
		hideticks(e);
		cut(ss, sel->mpl.pos, sel->mpr.pos);
		layoutrect(e);
		layoutselections(e);
		showticks(e);
		flushimage(display, 1);
		break;
	case EDelete:
		_ctlargcount(e, cp, 2);
		n = cp->iargs[1];
		if (n < 0 || n >= ss->nstring)
			ctlerror("%q: no such string: %d", e->name, n);
		hideticks(e);
		delete(ss, n);
		layoutrect(e);
		layoutselections(e);
		showticks(e);
		flushimage(display, 1);
		break;
	case EDelrune:
		_ctlargcount(e, cp, 1);
		if (ss == nil)
			break;
		sel = e->sel;
		hideticks(e);
		if (_posequal(sel->mpl.pos, sel->mpr.pos)){
			sel->mpr.pos = _delonerune(ss, sel->mpl.pos);
			sel->mpl.pos = sel->mpr.pos;
		}else
			cut(ss, sel->mpl.pos, sel->mpr.pos);
		layoutrect(e);
		layoutselections(e);
		showticks(e);
		flushimage(display, 1);
		break;
	case EDump:
		if (ss){
			dumpstrings(ss);
			dumpundo(ss);
		}
		dumplines(e);
		dumpselections(e);
		break;
	case EFocus:
		break;
	case EHide:
		_ctlargcount(e, cp, 1);
		e->hidden = 1;
		break;
	case EImage:
		_ctlargcount(e, cp, 2);
		_setctlimage(e, &e->image, cp->args[1]);
		break;
	case EInsert:
		_ctlargcount(e, cp, 3);
		n = cp->iargs[1];
		s = ctlmalloc(sizeof(String));
		s->context = strdup(e->curcontext->name);
		s->r = _ctlrunestr(cp->args[2]);
		s->n = runestrlen(ss->string[n]->r);
		s->ref = 1;
		if (ss == nil){
			ss = newstringset("default");
			ss->c = (Control*)e;
			qlock(ss);
			e->ss = ss;
		}
		insert(ss, n, s);
		e->dirty = 1;
		layoutrect(e);
		layoutselections(e);
		showticks(e);
		flushimage(display, 1);
		break;
	case EJoin:
		_ctlargcount(e, cp, 3);
		p1.str = cp->iargs[1];
		p1.rn = cp->iargs[2];
		if (p1.str < 0 || p1.str >= ss->nstring-1)
			ctlerror("%q: no such string: %d", e->name, p1.str);
		hideticks(e);
		join(ss, p1);
		layoutrect(e);
		layoutselections(e);
		showticks(e);
		flushimage(display, 1);
		break;
	case EPaste:
		_ctlargcount(e, cp, 2);
		i = cp->iargs[1];
		if (i < 0 || i >= nelem(e->sel))
			ctlerror("%q: nonexistent selection: %d", e->name, i);
		sel = e->sel + i;
		if ((sel->state & Set) == 0)
			ctlerror("%q: inactive selection: %d", e->name, i);
		if (ss == nil)
			ctlerror("%q: no stringset to paste from");
		p1 = sel->mpl.pos;
		hideticks(e);
		cut(ss, sel->mpl.pos, sel->mpr.pos);
		paste(ss, sel->mpl.pos, ss->snarf, ss->nsnarf);
		sel->mpl.pos = p1;
		layoutrect(e);
		layoutselections(e);
		showticks(e);
		flushimage(display, 1);
		break;
	case ERect:
		_ctlargcount(e, cp, 5);
		r.min.x = cp->iargs[1];
		r.min.y = cp->iargs[2];
		r.max.x = cp->iargs[3];
		r.max.y = cp->iargs[4];
		if(Dx(r)<=0 || Dy(r)<=0)
			ctlerror("%q: bad rectangle: %s", e->name, cp->str);
		e->rect = r;
		e->dirty =1;
		break;
	case ERedo:
		_ctlargcount(e, cp, 1);
		if (ss == nil)
			break;
		hideticks(e);
		redo(ss);
		layoutrect(e);
		layoutselections(e);
		showticks(e);
		flushimage(display, 1);
		break;
	case ERedraw:
		_ctlargcount(e, cp, 1);
		editshow(e);
		break;
	case EReveal:
		_ctlargcount(e, cp, 1);
		e->hidden = 0;
		editshow(e);
		break;
	case EScroll:
		break;
	case ESelect:
		_ctlargcount(e, cp, 6);
		i = cp->iargs[1];
		if (i < 0 || i >= nelem(e->sel))
			ctlerror("%q: select index: %s", e->name, cp->str);
		sel = e->sel + i;
		if ((sel->state & Enabled) == 0)
			ctlerror("%q: select selection %d not enabled", e->name, i);
		hideticks(e);
		p1.str = cp->iargs[2];
		p1.rn = cp->iargs[3];
		p2.str = cp->iargs[4];
		p2.rn = cp->iargs[5];
		sel = e->sel + i;
		if ((sel->state & (Set|Dragging)) && ! _posequal(sel->mpl.pos, sel->mpr.pos))
			layoutprep(e, sel->mpl.pos, sel->mpr.pos);
		if (_posbefore(p2, p1)){
			sel->mpl.pos = p2;
			sel->mpr.pos = p1;
		}else{
			sel->mpl.pos = p1;
			sel->mpr.pos = p2;
		}
		sel->state = Enabled | Set;
		if (!_posequal(sel->mpl.pos, sel->mpr.pos))
			layoutprep(e, sel->mpl.pos, sel->mpr.pos);
		layoutrect(e);
		pos2mpos(e, sel->mpl.pos, &sel->mpl);
		pos2mpos(e, sel->mpr.pos, &sel->mpr);
		showticks(e);
		flushimage(display, 1);
		break;
	case ESelectcolor:
		_ctlargcount(e, cp, 3);
		i = cp->iargs[1];
		if (i < 0 || i >= nelem(e->sel))
			ctlerror("%q: selectcolor index: %s", e->name, cp->str);
		_setctlimage(e, &e->sel[i].color, cp->args[2]);
		e->sel[i].state |= Enabled;
		break;
	case EShow:
		_ctlargcount(e, cp, 1);
		if (e->dirty && e->lredraw == nil)
			editshow(e);
		else{
			layoutrect(e);
			layoutselections(e);
			showticks(e);
			flushimage(display, 1);
		}
		break;
	case ESize:
		if (cp->nargs == 3)
			r.max = Pt(10000, 10000);
		else{
			_ctlargcount(e, cp, 5);
			r.max.x = cp->iargs[3];
			r.max.y = cp->iargs[4];
		}
		r.min.x = cp->iargs[1];
		r.min.y = cp->iargs[2];
		if(r.min.x<=0 || r.min.y<=0 || r.max.x<=0 || r.max.y<=0 || r.max.x < r.min.x || r.max.y < r.min.y)
			ctlerror("%q: bad sizes: %s", e->name, cp->str);
		e->size.min = r.min;
		e->size.max = r.max;
		break;
	case ESnarf:
		_ctlargcount(e, cp, 2);
		i = cp->iargs[1];
		if (i < 0 || i >= nelem(e->sel))
			ctlerror("%q: nonexistent selection: %d", e->name, i);
		sel = e->sel + i;
		if ((sel->state & Set) == 0)
			ctlerror("%q: inactive selection: %d", e->name, i);
		if (ss == nil)
			ctlerror("%q: no stringset to paste into", e->name);
		snarf(ss, sel->mpl.pos, sel->mpr.pos);
		break;
	case ESpacing:
		_ctlargcount(e, cp, 2);
		if(cp->iargs[1] < 0)
			ctlerror("%q: bad spacing: %c", e->name, cp->str);
		e->spacing = cp->iargs[1];
		break;
	case ESplit:
		_ctlargcount(e, cp, 3);
		p1.str = cp->iargs[1];
		p1.rn = cp->iargs[2];
		if ((s = stringof(ss, p1)) == nil)
			ctlerror("%q: no such string: %d", e->name, p1.str);
		if (p1.rn <= 0 || p1.rn >= s->n)
			ctlerror("%q: split outside string: %d/%d", e->name, p1.str, p1.rn);
		hideticks(e);
		split(ss, p1);
		layoutrect(e);
		layoutselections(e);
		showticks(e);
		flushimage(display, 1);
		break;
	case EStringset:
		_ctlargcount(e, cp, 2);
		ss = stringsetnamed(cp->args[1]);
		if (ss == nil)
			ctlerror("%q: no such stringset: %s", e->name, cp->args[1]);
		e->ss = ss;
		ss->c = (Control*)e;
		qlock(ss);
		e->dirty = 1;
		break;
	case ETextcolor:
		_ctlargcount(e, cp, 2);
		_setctlimage(e, &e->textcolor, cp->args[1]);
		break;
	case ETop:
		_ctlargcount(e, cp, 3);
		if (ss == nil)
			break;
		p1.str = cp->iargs[1];
		p1.rn = cp->iargs[2];
		if (_stringfindrrune(ss, &p1, '\n'))
			p1.rn++;
		debugprint(DBGLAYOUT, "top command %d/%d\n", p1.str, p1.rn);
		scroll(e, p1);
		break;
	case EUndo:
		_ctlargcount(e, cp, 1);
		if (ss == nil)
			break;
		hideticks(e);
		undo(ss);
		layoutrect(e);
		layoutselections(e);
		showticks(e);
		flushimage(display, 1);
		break;
	case EValue:
		_ctlargcount(e, cp, 3);
		n = cp->iargs[1];
		s = ctlmalloc(sizeof(String));
		s->context = strdup(e->curcontext->name);
		s->r = _ctlrunestr(cp->args[2]);
		s->n = runestrlen(ss->string[n]->r);
		s->ref = 1;
		debugprint(DBGSTRING, "allocate string: 0x%p %s [%d]", ss->string[n], e->curcontext->name, ss->string[n]->n);
		if (ss == nil){
			ss = newstringset("default");
			ss->c = (Control*)e;
			qlock(ss);
			e->ss = ss;
		}
		_stringspace(ss, n+1);
		if (ss->string[n]){
			p1.str = p2.str = n;
			p1.rn = 0;
			p2.rn = ss->string[n]->n;
			layoutprep(e, p1, p2);
			freestring(ss->string[n]);
		}
		ss->string[n] = s;
		if (ss->nstring <= n)
			ss->nstring = n+1;
		e->dirty = 1;
		layoutrect(e);
		layoutselections(e);
		showticks(e);
		flushimage(display, 1);
		break;
	}
	if (e->ss)
		qunlock(e->ss);
}

Control*
createedit(Controlset *cs, char *name)
{
	Edit *e;
	int i, thistab;
	Selection *sel;
	Context *c;

	e = (Edit*)_createctl(cs, "edit", sizeof(Edit), name);
	e->border = 0;
	e->top.str = 0;
	e->top.rn = 0;
	e->bot.str = 0;
	e->bot.rn = 0;
	e->dirty = 0;
	e->scroll = 0;
	e->spacing = 0;
	e->lastbut = 0;
	e->image = _getctlimage("white");
	e->textcolor = _getctlimage("black");
	e->bordercolor = _getctlimage("black");
	for (i = 0; i < nelem(e->sel); i++){
		e->sel[i].state = 0;
		e->sel[i].color = nil;
	}
	sel = e->sel;
	sel->color = _getctlimage("paleyellow");
	sel->state = Enabled|Set;
	sel->mpl.pos.str = 0;
	sel->mpl.pos.rn = 0;
	sel->mpr.pos.str = 0;
	sel->mpr.pos.rn = 0;
	e->l = nil;
	e->ss = nil;
	e->mouse = editmouse;
	e->key = editkey;
	e->ctl = editctl;
	e->exit = editfree;
	if ((c = _contextnamed("default")) == nil){
		c = _newcontext("default");
		_setctlfont(e, &c->f, "font");
		c->wordbreak = 0;
		thistab = 0;
		for (i = 0; i < nelem(c->tabs); i++){
			thistab += stringwidth(c->f->font, "0000");
			c->tabs[i] = thistab;
		}
	}
	e->curcontext = c;
	frinittick(e);
	return (Control *)e;
}

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@plan9.bell-labs.com.