shithub: picker

ref: 72a65f4596a41a569ef02852c9ab638d6329a481
dir: /picker.c/

View raw version
#include <u.h>
#include <libc.h>
#include <thread.h>
#include <draw.h>
#include <memdraw.h>
#include <mouse.h>
#include <keyboard.h>
#include <bio.h>
#include <plumb.h>
#include "hsluv.h"

#define MAX(a,b) ((a)>=(b)?(a):(b))
#define MIN(a,b) ((a)<=(b)?(a):(b))
#define D2C(x) (int)MAX(0, MIN(0xff, x*256.0))

enum
{
	Ckey,
	Cmouse,
	Cresize,
	Cloaded,
	Numchan,

	Offset = 6,
	Sliderheight = 48,
};

typedef struct Color Color;
typedef struct Space Space;

struct Color {
	char *id;
	Image *i;
	double v[4];
	double rgba[4];
	double rgba0[4]; /* that's for reverting back to the original */
	Rectangle r;
	u32int u;
	int nchan;
	Color *next, *prev;
};

struct Space {
	char *name;
	char opt;
	char single;
	void (*torgb)(double *v, double *rgb);
	void (*fromrgb)(double *rgb, double *v);
	double max[4];
};

static void
_hsluv2rgb(double *v, double *rgb)
{
	hsluv2rgb(v[0], v[1], v[2], rgb+0, rgb+1, rgb+2);
}

static void
_rgb2hsluv(double *rgb, double *v)
{
	rgb2hsluv(rgb[0], rgb[1], rgb[2], v+0, v+1, v+2);
}

static void
_hpluv2rgb(double *v, double *rgb)
{
	hpluv2rgb(v[0], v[1], v[2], rgb+0, rgb+1, rgb+2);
}

static void
_rgb2hpluv(double *rgb, double *v)
{
	rgb2hpluv(rgb[0], rgb[1], rgb[2], v+0, v+1, v+2);
}

static void
_torgb(double *v, double *rgb)
{
	rgb[0] = v[0];
	rgb[1] = v[1];
	rgb[2] = v[2];
}

static void
_fromrgb(double *rgb, double *v)
{
	v[0] = rgb[0];
	v[1] = rgb[1];
	v[2] = rgb[2];
}

static Space spaces[] = {
	{
		.name = "HSLuv",
		.opt = 's',
		.single = 0,
		.torgb = _hsluv2rgb,
		.fromrgb = _rgb2hsluv,
		.max = {360.0, 100.0, 100.0, 1.0},
	},
	{
		.name = "HPLuv",
		.opt = 'l',
		.single = 0,
		.torgb = _hpluv2rgb,
		.fromrgb = _rgb2hpluv,
		.max = {360.0, 100.0, 100.0, 1.0},
	},
	{
		.name = "RGB",
		.opt = 'r',
		.single = 1,
		.torgb = _torgb,
		.fromrgb = _fromrgb,
		.max = {1.0, 1.0, 1.0, 1.0},
	},
};

static char *menu2i[nelem(spaces)+6];
static Menu menu2 = { .item = menu2i };

static Color *colors, *color, *last;
static Channel *loaded;
static int ncolors;
static Rectangle srects[3];
static Space *space;
static Image *bg;
static char hex[12];
static int once;

static ulong
rgba2u(double *rgba)
{
	return D2C(rgba[0])<<24 | D2C(rgba[1])<<16 | D2C(rgba[2])<<8 | D2C(rgba[3]);
}

#pragma varargck type "©" Color*
static int
colorfmt(Fmt *f)
{
	char s[16];
	Color *c;

	c = va_arg(f->args, Color*);
	if (c->nchan < 4)
		sprint(s, "%06ux", c->u>>8);
	else
		sprint(s, "%08ux", c->u);

	return fmtstrcpy(f, s);
}

static Image *
slider(int si, int w)
{
	static Image *s, *sliders[4];
	static u8int *b, *buf[4];
	double c[4], rgba[4], dt;
	Rectangle rect;
	int i, n, mi;
	ulong u;

	rect = Rect(0, 0, w, 1);
	s = sliders[si];
	if (s == nil || Dx(s->r) != w) {
		buf[si] = realloc(buf[si], 4*w);
		if (s != nil)
			freeimage(s);
		if ((s = sliders[si] = allocimage(display, rect, RGBA32, 1, DNofill)) == nil)
			sysfatal("allocimage: %r");
	}
	b = buf[si];

	memmove(c, color->v, sizeof(c));
	if (space->single) {
		memset(c, 0, 3*sizeof(double));
		c[si] = color->v[si];
	}
	dt = space->max[si] / w;
	mi = c[si] / dt;
	c[si] = 0.0;
	for (i = n = 0; i < w; i++, n += 4) {
		space->torgb(c, rgba);
		rgba[3] = c[3];
		u = rgba2u(rgba);
		u = setalpha(u, u&0xff);
		b[n] = 0xff;
		if (mi-2 == i)
			memset(b+n+1, 0, 3);
		else if (mi-1 == i)
			memset(b+n+1, 0xff, 3);
		else if (mi+1 == i)
			memset(b+n+1, 0xff, 3);
		else if (mi+2 == i)
			memset(b+n+1, 0, 3);
		else {
			b[n+0] = u & 0xff;
			b[n+1] = (u>>8) & 0xff;
			b[n+2] = (u>>16) & 0xff;
			b[n+3] = (u>>24) & 0xff;
		}

		c[si] = MIN(space->max[si], c[si] + dt);
	}
	loadimage(s, rect, b, 4*w);

	return s;
}

static void
redraw(void)
{
	Rectangle r, cr;
	Color *c;
	Image *im;
	int i, colw;

	lockdisplay(display);

	draw(screen, screen->r, display->white, nil, ZP);
	r = screen->r;

	r.min.x += Offset;
	r.min.y += Offset;
	r.max.x -= Offset;
	r.max.y = r.min.y + Sliderheight;

	/* sliders */
	for (i = 0; i < color->nchan; i++) {
		srects[i] = r;
		im = slider(i, Dx(r));
		draw(screen, r, bg, nil, ZP);
		draw(screen, r, im, nil, ZP);
		r.min.y += Sliderheight + Offset;
		r.max.y += Sliderheight + Offset;
	}

	/* current color is changed on redraw, always */
	freeimage(color->i);
	color->i = nil;

	/* palette */
	colw = MIN(Sliderheight, (Dx(r)-(ncolors-1)*Offset)/ncolors);
	cr = r;
	cr.min.x += (Dx(cr) - colw*ncolors - (ncolors-1)*Offset) / 2;
	for (c = colors; c != nil; c = c->next) {
		if (c->i == nil) {
			if ((c->i = allocimage(display, Rect(0, 0, 1, 1), RGBA32, 1, setalpha(c->u|0xff, c->u&0xff))) == nil)
				sysfatal("allocimage: %r");
		}

		cr.max.x = cr.min.x + colw;
		draw(screen, cr, bg, nil, ZP);
		draw(screen, cr, c->i, nil, ZP);
		border(screen, cr, 1, display->black, ZP);
		c->r = insetrect(cr, -3);
		if (c == color)
			border(screen, c->r, 3, display->black, ZP);
		cr.min.x += colw + Offset;
	}
	r.min.y += Sliderheight + Offset;

	/* current color */
	r.max.y = screen->r.max.y - Offset;
	draw(screen, r, bg, nil, ZP);
	draw(screen, r, color->i, nil, ZP);

	/* current color id */
	r.min.x += Dx(r)/2 - stringwidth(font, color->id)/2;
	stringbg(screen, r.min, display->white, ZP, font, color->id, display->black, ZP);

	/* current color in hex */
	r.min.y += font->height;
	sprint(hex, "%©", color);
	r.min.x = screen->r.min.x + Dx(screen->r)/2 - stringwidth(font, hex)/2;
	stringbg(screen, r.min, display->white, ZP, font, hex, display->black, ZP);

	flushimage(display, 1);
	unlockdisplay(display);
}

static void
usage(void)
{
	int i;

	print("usage: %s [-e] [-", argv0);
	for (i = 0; i < nelem(spaces); i++)
		print("%c", spaces[i].opt);
	print("]\n");

	threadexitsall("usage");
}

static void
loadbg(void)
{
	Rectangle r;
	u8int *d;
	int i, j;

	r = Rect(0, 0, Sliderheight, Sliderheight);
	d = calloc(1, Sliderheight*Sliderheight);
	for (i = 0; i < Sliderheight/2; i++) {
		for (j = 0; j < Sliderheight/2; j++)
			d[j*Sliderheight+i] = 0xff;
	}
	for (; i < Sliderheight; i++) {
		for (j = Sliderheight/2; j < Sliderheight; j++)
			d[j*Sliderheight+i] = 0xff;
	}
	if ((bg = allocimage(display, r, GREY8, 1, DNofill)) == nil)
		sysfatal("allocimage: %r");
	if (loadimage(bg, r, d, Sliderheight*Sliderheight) < 0)
		sysfatal("loadimage: %r");
	free(d);
}

static int
printcolor(int f, Color *c)
{
	char s[64];
	int n;

	n = snprint(s, sizeof(s), "%s\t%©\n", c->id, c);

	return write(f, s, n) == n ? 0 : -1;
}

static int
hex2color(char *h, Color *c)
{
	vlong v;
	int i, n;
	char *e;

	n = strlen(h);
	if (n != 6 && n != 8) {
		werrstr("components: %d", n);
		return -1;
	}
	c->nchan = n == 6 ? 3 : 4;
	if ((v = strtoll(h, &e, 16)) == 0 &&
		(e == h || *e || v < 0 || (n == 6 && v > 0xffffff) || (n == 8 && v > 0xffffffff))) {
		werrstr("color: '%s'", h);
		return -1;
	}
	if (c->nchan < 4) {
		v <<= 8;
		v |= 0xff;
	}
	c->u = v;

	for (i = 0; i < 4; i++) {
		c->rgba[i] = (double)((v>>24)&0xff) / 255.0;
		v <<= 8;
	}
	c->v[3] = c->rgba[3];
	space->fromrgb(c->rgba, c->v);

	return 0;
}

static int
line2color(char *s, Color *c)
{
	char *a[3];
	int n;

	if ((n = tokenize(s, a, nelem(a))) < 2) {
		werrstr("columns: %d", n);
		return -1;
	}
	if (hex2color(a[1], c) != 0)
		return -1;
	c->id = strdup(a[0]);

	return 0;
}

static void
readcolors(void *x)
{
	Color *c, *new;
	Biobuf *b;
	char *s;
	char tmp[64];
	int i, n;

	b = x;
	n = sprint(tmp, "readcolors ");
	fd2path(Bfildes(b), tmp+n, sizeof(tmp)-n);
	threadsetname(tmp);
	new = nil;
	for (i = 1; (s = Brdstr(b, '\n', 1)) != nil; i++) {
		if (new == nil)
			new = calloc(1, sizeof(Color));

		n = line2color(s, new);
		free(s);

		if (n != 0) {
			fprint(2, "%d: %r\n", i);
			continue;
		}

		lockdisplay(display);
		for (c = colors; c != nil; c = c->next) {
			if (strcmp(c->id, new->id) == 0) {
				free(c->id);
				new->prev = c->prev;
				new->next = c->next;
				memmove(new->rgba0, c->rgba0, sizeof(new->rgba0));
				memmove(c, new, sizeof(*c));
				break;
			}
		}

		if (c == nil) {
			c = new;
			memmove(c->rgba0, c->rgba, sizeof(c->rgba));
			if (last != nil)
				last->next = new;
			new->prev = last;
			last = new;

			if (colors == nil)
				colors = color = new;
			new = nil;
			ncolors++;
		}

		if (!once)
			printcolor(1, c);
		unlockdisplay(display);
		sendul(loaded, 0);
	}
	Bterm(b);
	threadexits(nil);
}

static void
dump(int f)
{
	Color *c;

	for (c = colors; c != nil; c = c->next)
		printcolor(f, c);
}

static void
loadtheme(char *filename)
{
	Biobuf *b;

	if ((b = Bopen(filename, OREAD)) != nil)
		proccreate(readcolors, b, 4096);
}

static void
plumbproc(void *)
{
	int f;
	Plumbmsg *m;

	threadsetname("plumb");
	if ((f = plumbopen("picker", OREAD)) >= 0) {
		while ((m = plumbrecv(f)) != nil) {
			loadtheme(m->data);
			plumbfree(m);
		}
	}

	threadexits(nil);
}

void
threadmain(int argc, char **argv)
{
	Mousectl *mctl;
	Keyboardctl *kctl;
	Biobuf *b;
	Color *c;
	Rune r;
	Mouse m;
	Alt a[Numchan+1] = {
		[Ckey] = { nil, &r, CHANRCV },
		[Cmouse] = { nil, &m, CHANRCV },
		[Cresize] = { nil, nil, CHANRCV },
		[Cloaded] = { nil, nil, CHANRCV },
		{ nil, nil, CHANEND },
	};
	int i, oldbuttons, slider, menusnarf, menusnarfall, menurevert, menurevertall, menuexit;
	ulong u;
	char buf[16];

	fmtinstall(L'©', colorfmt);

	space = &spaces[0];
	ARGBEGIN{
	case 'e':
		once = 1;
		break;
	default:
		space = nil;
		for (i = 0; i < nelem(spaces); i++) {
			if (spaces[i].opt == ARGC()) {
				space = &spaces[i];
				break;
			}
		}
		if (space == nil) {
			fprint(2, "unknown color space '%c'\n", ARGC());
			usage();
		}
		break;
	}ARGEND

	if (argc > 1)
		usage();

	b = argc == 1 ? Bopen(argv[0], OREAD) : Bfdopen(0, OREAD);
	if (b == nil)
		sysfatal("no colors: %r");
	loaded = chancreate(sizeof(ulong), 0);

	for (i = 0; i < nelem(spaces); i++)
		menu2i[i] = spaces[i].name;
	menu2i[menusnarf = i++] = "snarf";
	menu2i[menusnarfall = i++] = "snarf all";
	menu2i[menurevert = i++] = "revert";
	menu2i[menurevertall = i++] = "revert all";
	menu2i[menuexit = i] = "exit";

	if (initdraw(nil, nil, "picker") < 0)
		sysfatal("initdraw: %r");
	if ((kctl = initkeyboard(nil)) == nil)
		sysfatal("initkeyboard: %r");
	a[Ckey].c = kctl->c;
	if ((mctl = initmouse(nil, screen)) == nil)
		sysfatal("initmouse: %r");
	a[Cmouse].c = mctl->c;
	a[Cresize].c = mctl->resizec;
	a[Cloaded].c = loaded;
	display->locking = 1;
	unlockdisplay(display);
	loadbg();
	slider = -1;

	proccreate(readcolors, b, 4096);
	proccreate(plumbproc, nil, 4096);

	for (;;) {
next:
		c = color;
		oldbuttons = m.buttons;

		switch (alt(a)) {
		case -1:
			goto end;

		case Ckey:
			if (c == nil)
				break;
			switch (r) {
			case Kleft:
				if (c->prev != nil)
					color = c->prev;
				redraw();
				break;
			case Kright:
				if (c->next != nil)
					color = c->next;
				redraw();
				break;
			case Kdel:
				goto end;
			}
			break;

		case Cmouse:
			if (c == nil)
				break;
			if (m.buttons == 1) {
				m.xy.x = MAX(screen->r.min.x, MIN(screen->r.max.x, m.xy.x));
				for (i = 0; i < c->nchan; i++) {
					Rectangle r = srects[i];
					r.max.x += 1;
					if (oldbuttons == 0 && ptinrect(m.xy, r))
						slider = i;
					if (slider != i)
						continue;

					c->v[i] = MIN(space->max[i], (double)(m.xy.x - r.min.x) * space->max[i]/(double)(Dx(r)-1));
					c->rgba[3] = c->v[3];
changed:
					space->torgb(c->v, c->rgba);
					u = rgba2u(c->rgba);
					if (c->u != u) {
						c->u = u;
						if (!once)
							printcolor(1, c);
					}
					redraw();
					goto next;
				}

				for (c = colors; c != nil; c = c->next) {
					if (ptinrect(m.xy, c->r)) {
						color = c;
						space->fromrgb(c->rgba, c->v);
						for (i = 0; i < 3; i++)
							c->v[i] = MAX(0.0, MIN(space->max[i], c->v[i]));
						goto changed;
					}
				}
			} else if (m.buttons == 4 && (i = menuhit(3, mctl, &menu2, nil)) >= 0) {
				if (i == menuexit)
					goto end;
				if (i < nelem(spaces)) {
					space = &spaces[i];
					space->fromrgb(c->rgba, c->v);
					for (i = 0; i < 3; i++)
						c->v[i] = MAX(0.0, MIN(space->max[i], c->v[i]));
					goto changed;
				} else if (i == menusnarf || i == menusnarfall) {
					int f;
					if ((f = open("/dev/snarf", OWRITE)) >= 0) {
						if (i == menusnarf)
							write(f, hex, strlen(hex));
						else
							dump(f);
						close(f);
					}
				} else if (i == menurevert) {
					space->fromrgb(c->rgba0, c->v);
					goto changed;
				} else if (i == menurevertall) {
					for (c = colors; c != nil; c = c->next) {
						memmove(c->rgba, c->rgba0, sizeof(c->rgba));
						space->fromrgb(c->rgba, c->v);
						space->torgb(c->v, c->rgba);
						u = rgba2u(c->rgba);
						if (c->u != u) {
							c->u = u;
							if (!once)
								printcolor(1, c);
							freeimage(c->i);
							c->i = nil;
						}
					}
					redraw();
					break;
				}
			} else if (m.buttons == 2) {
				strcpy(buf, hex);
				if (enter("rgb(a):", buf, sizeof(buf), mctl, kctl, nil) > 0) {
					u = c->u;
					if (hex2color(buf, c) == 0 && c->u != u) {
						c->u = ~c->u; /* just for the update to kick in */
						goto changed;
					}
				}
			}
			slider = -1;
			break;

		case Cresize:
			getwindow(display, Refnone);
			if (color != nil)
				redraw();
			break;

		case Cloaded:
			redraw();
			break;
		}
	}

end:
	if (once)
		dump(1);

	threadexitsall(nil);
}