commit 99531db6c1821736ff1a975ec9872fc033d94df7
parent 488ed473efaa6153752374f9c27f67fe45dc4823
Author: lumidify <nobody@lumidify.org>
Date:   Fri, 24 Jun 2022 14:31:59 +0200
Add event masks
This is very weird and buggy right now.
See socket_format.txt for some open problems.
Diffstat:
21 files changed, 703 insertions(+), 162 deletions(-)
diff --git a/Makefile b/Makefile
@@ -55,8 +55,8 @@ OBJ = \
 	src/graphics_xlib.o \
 	src/surface_cache.o \
 	src/event_xlib.o \
+	src/err.c \
 	$(EXTRA_OBJ)
-#	src/draw.o \
 
 # Note: This could be improved so a change in a header only causes the .c files
 # which include that header to be recompiled, but the compile times are
@@ -83,8 +83,9 @@ HDR = \
 	src/surface_cache.h \
 	src/macros.h \
 	src/event.h \
-	src/xlib_shared.h
-#	src/draw.h \
+	src/xlib_shared.h \
+	src/err.h \
+	src/proto_types.h
 
 all: src/ltkd src/ltkc
 
diff --git a/README.md b/README.md
@@ -20,3 +20,5 @@ Also read the comment in './test.sh'.
 
 Note: I know the default theme is butt-ugly at the moment. It is mainly
 to test things, not to look pretty.
+
+Note: Read 'socket_format.txt' for some documentation and open problems.
diff --git a/socket_format.txt b/socket_format.txt
@@ -1,5 +1,4 @@
-Note: This is not fully implemented yet; it is just here to
-collect my thoughts while I keep working.
+Requests:
 
 <widget type> <widget id> <command> <args>
 > grid grd1 create 2 2
@@ -14,12 +13,73 @@ within a string.
 Double quotes must be escaped in strings, like so:
 > button btn1 create "Bla\"bla"
 
-When the server sends a reply, the format is the same, but
-there are some special cases, such as "get-text". When the
-client asks to get the text for a widget, only the text is
-sent back, but still inside double quotes, with double quotes
-belonging to the text escaped.
-
 Essentially, the individual messages are separated by line
 breaks (\n), but line breaks within strings don't break the
 message.
+
+Replies:
+
+Not properly implemented yet.
+The requests will probably have to include a sequence number eventually, which
+the replies will also give back so it's possible to know when the appropriate
+reply has been received.
+
+Errors:
+
+err <error number> <number of bad argument or -1> <string description of error>
+
+Events:
+
+event[l] <widget id> <widget type or "widget" for generic events> <event name> [further data]
+
+By default, no events are reported. An event mask has to be set first:
+
+mask-add <widget id> <type> <event name> [lock]
+mask-remove <widget id> <type> <event name> [lock]
+mask-set <widget id> <type> <event name> [lock]
+
+<type> is either "widget" for generic events (mousepress, mouserelease,
+mousemotion, configure, statechange) or the widget type for specific events.
+The only specific event currently supported is "press" for both "button"
+and "menuentry". Note that currently, only a single mask type can be
+added/removed at once instead of combining multiple in one request
+(i.e. it isn't possible to do something like "mousepress|mouserelease" yet).
+
+If "lock" is set, the "lock mask" is manipulated instead of the normal one.
+If an event occurs that is included in the lock mask, the event will start
+with "eventl" instead of "event", and the ltk server blocks until it gets the
+request "event-unlock [true/false]" from the client that received the event.
+Note that if multiple clients have an event in their lock mask, all of them will
+receive the event, but only one of them is chosen to listen for the event-unlock
+(basically, this is unspecified behavior that should be avoided). If event-unlock
+includes "true", the event is not processed further by the ltk server. If "false"
+is given instead, the event is processed as usual by the ltk server.
+
+This was added to allow functionality like in regular GUI toolkits where it is
+possible to override events completely. The problem is that it currently isn't
+really clear where exactly the command should be emitted and whether it really
+makes sense to block all further processing (some processing has to be done
+even now for it to make any sense at all). That could possibly lead to very
+weird bugs. It also currently isn't possible to do much after locking because
+no useful low-level functions for widgets exist (yet?). All in all, I'm not
+entirely sure how to make this work nicely so it is actually useful.
+Since all of this is pushed over a socket and will probably be able to run
+over a network connection eventually, it will also cause problems with latency.
+
+Miscellaneous:
+
+It probably isn't too great for security when anyone can do anything with the
+window. Maybe it would be better to allow different clients to have different
+permissions? For instance, maybe only the main client could change things, but
+other clients could have readonly permissions for things like screenreaders.
+That would probably get very over-complicated, though.
+
+I'm also seriously considering switching to a binary socket format. It's nice
+to have a text format, but it's an absolute pain to process, because everything
+has to be converted from/to text. It also isn't nearly as efficient, especially
+if more complicated things are done, such as listening for all mousemotion events.
+Of course, it could be made much more efficient with various lookup tables
+(it isn't implemented very efficiently currently), but still not nearly as good
+as a binary protocol. The idea would be to have a binary protocol, but to still
+have something like ltkc that converts the protocol to a text format so simple
+shell clients can still exist, but more complicated programs aren't hindered by it.
diff --git a/src/box.c b/src/box.c
@@ -59,7 +59,7 @@ static struct ltk_widget_vtable vtable = {
 	.get_child_at_pos = <k_box_get_child_at_pos,
 	.mouse_leave = NULL,
 	.mouse_enter = NULL,
-	.type = LTK_BOX,
+	.type = LTK_WIDGET_BOX,
 	.flags = 0,
 };
 
@@ -358,8 +358,8 @@ ltk_box_cmd_add(
 		err->arg = -1;
 		return 1;
 	}
-	box = (ltk_box *)ltk_get_widget(tokens[1], LTK_BOX, err);
-	widget = ltk_get_widget(tokens[3], LTK_WIDGET, err);
+	box = (ltk_box *)ltk_get_widget(tokens[1], LTK_WIDGET_BOX, err);
+	widget = ltk_get_widget(tokens[3], LTK_WIDGET_ANY, err);
 	if (!box) {
 		err->arg = 1;
 		return 1;
@@ -409,8 +409,8 @@ ltk_box_cmd_remove(
 		err->arg = -1;
 		return 1;
 	}
-	box = (ltk_box *)ltk_get_widget(tokens[1], LTK_BOX, err);
-	widget = ltk_get_widget(tokens[3], LTK_WIDGET, err);
+	box = (ltk_box *)ltk_get_widget(tokens[1], LTK_WIDGET_BOX, err);
+	widget = ltk_get_widget(tokens[3], LTK_WIDGET_ANY, err);
 	if (!box) {
 		err->arg = 1;
 		return 1;
@@ -441,7 +441,6 @@ ltk_box_cmd_create(
 		err->arg = -1;
 		return 1;
 	}
-	/* FIXME: race condition */
 	if (!ltk_widget_id_free(tokens[1])) {
 		err->type = ERR_WIDGET_ID_IN_USE;
 		err->arg = 1;
diff --git a/src/button.c b/src/button.c
@@ -20,6 +20,7 @@
 #include <string.h>
 #include <stdarg.h>
 
+#include "proto_types.h"
 #include "event.h"
 #include "memory.h"
 #include "color.h"
@@ -59,8 +60,8 @@ static struct ltk_widget_vtable vtable = {
 	.destroy = <k_button_destroy,
 	.child_size_change = NULL,
 	.remove_child = NULL,
-	.type = LTK_BUTTON,
-	.flags = LTK_NEEDS_REDRAW | LTK_NEEDS_SURFACE | LTK_ACTIVATABLE_ALWAYS,
+	.type = LTK_WIDGET_BUTTON,
+	.flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS,
 };
 
 static struct {
@@ -116,13 +117,15 @@ ltk_button_uninitialize_theme(ltk_window *window) {
 	ltk_theme_uninitialize(window, parseinfo, LENGTH(parseinfo));
 }
 
+/* FIXME: only keep text in surface to avoid large surface */
 static void
 ltk_button_draw(ltk_widget *self, ltk_rect clip) {
 	ltk_button *button = (ltk_button *)self;
 	ltk_rect rect = button->widget.rect;
 	ltk_rect clip_final = ltk_rect_intersect(clip, rect);
 	ltk_surface *s;
-	if (!ltk_surface_cache_get_surface(self->surface_key, &s) || self->dirty)
+	ltk_surface_cache_request_surface_size(button->key, self->rect.w, self->rect.h);
+	if (!ltk_surface_cache_get_surface(button->key, &s) || self->dirty)
 		ltk_button_redraw_surface(button, s);
 	ltk_surface_copy(s, self->window->surface, ltk_rect_relative(rect, clip_final), clip_final.x, clip_final.y);
 }
@@ -165,9 +168,8 @@ ltk_button_redraw_surface(ltk_button *button, ltk_surface *s) {
 
 static int
 ltk_button_mouse_release(ltk_widget *self, ltk_button_event *event) {
-	ltk_button *button = (ltk_button *)self;
 	if ((self->state & LTK_PRESSED) && event->button == LTK_BUTTONL && ltk_collide_rect(self->rect, event->x, event->y)) {
-		ltk_queue_event(button->widget.window, LTK_EVENT_BUTTON, button->widget.id, "button_click");
+		ltk_queue_specific_event(self, "button", LTK_PWEVENTMASK_BUTTON_PRESS, "press");
 		return 1;
 	}
 	return 0;
@@ -184,6 +186,7 @@ ltk_button_create(ltk_window *window, const char *id, char *text) {
 	button->widget.ideal_w = text_w + theme.border_width * 2 + theme.pad * 2;
 	button->widget.ideal_h = text_h + theme.border_width * 2 + theme.pad * 2;
 	ltk_fill_widget_defaults(&button->widget, id, window, &vtable, button->widget.ideal_w, button->widget.ideal_h);
+	button->key = ltk_surface_cache_get_unnamed_key(window->surface_cache, button->widget.ideal_w, button->widget.ideal_h);
 	button->widget.dirty = 1;
 
 	return button;
@@ -197,8 +200,7 @@ ltk_button_destroy(ltk_widget *self, int shallow) {
 		ltk_warn("Tried to destroy NULL button.\n");
 		return;
 	}
-	/* FIXME: this should be generic part of widget */
-	ltk_surface_cache_release_key(self->surface_key);
+	ltk_surface_cache_release_key(button->key);
 	ltk_text_line_destroy(button->tl);
 	ltk_remove_widget(self->id);
 	ltk_remove_widget(button->widget.id);
diff --git a/src/button.h b/src/button.h
@@ -14,8 +14,8 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#ifndef _LTK_BUTTON_H_
-#define _LTK_BUTTON_H_
+#ifndef LTK_BUTTON_H
+#define LTK_BUTTON_H
 
 /* Requires the following includes: <X11/Xlib.h>, "rect.h", "widget.h", "ltk.h", "color.h", "text.h" */
 
@@ -24,6 +24,7 @@
 typedef struct {
 	ltk_widget widget;
 	ltk_text_line *tl;
+	ltk_surface_cache_key *key;
 } ltk_button;
 
 int ltk_button_ini_handler(ltk_window *window, const char *prop, const char *value);
@@ -37,4 +38,4 @@ int ltk_button_cmd(
     ltk_error *
 );
 
-#endif /* _LTK_BUTTON_H_ */
+#endif /* LTK_BUTTON_H */
diff --git a/src/grid.c b/src/grid.c
@@ -68,7 +68,7 @@ static struct ltk_widget_vtable vtable = {
 	.mouse_enter = NULL,
 	.key_press = NULL,
 	.key_release = NULL,
-	.type = LTK_GRID,
+	.type = LTK_WIDGET_GRID,
 	.flags = 0,
 };
 
@@ -414,8 +414,8 @@ ltk_grid_cmd_add(
 		err->arg = -1;
 		return 1;
 	}
-	grid = (ltk_grid *)ltk_get_widget(tokens[1], LTK_GRID, err);
-	widget = ltk_get_widget(tokens[3], LTK_WIDGET, err);
+	grid = (ltk_grid *)ltk_get_widget(tokens[1], LTK_WIDGET_GRID, err);
+	widget = ltk_get_widget(tokens[3], LTK_WIDGET_ANY, err);
 	if (!grid) {
 		err->arg = 1;
 		return 1;
@@ -490,8 +490,8 @@ ltk_grid_cmd_ungrid(
 		err->arg = -1;
 		return 1;
 	}
-	grid = (ltk_grid *)ltk_get_widget(tokens[1], LTK_GRID, err);
-	widget = ltk_get_widget(tokens[3], LTK_WIDGET, err);
+	grid = (ltk_grid *)ltk_get_widget(tokens[1], LTK_WIDGET_GRID, err);
+	widget = ltk_get_widget(tokens[3], LTK_WIDGET_ANY, err);
 	if (!grid) {
 		err->arg = 1;
 		return 1;
@@ -560,7 +560,7 @@ ltk_grid_cmd_set_row_weight(
 		err->arg = -1;
 		return 1;
 	}
-	grid = (ltk_grid *)ltk_get_widget(tokens[1], LTK_GRID, err);
+	grid = (ltk_grid *)ltk_get_widget(tokens[1], LTK_WIDGET_GRID, err);
 	if (!grid) {
 		err->arg = 1;
 		return 1;
@@ -598,7 +598,7 @@ ltk_grid_cmd_set_column_weight(
 		err->arg = -1;
 		return 1;
 	}
-	grid = (ltk_grid *)ltk_get_widget(tokens[1], LTK_GRID, err);
+	grid = (ltk_grid *)ltk_get_widget(tokens[1], LTK_WIDGET_GRID, err);
 	if (!grid) {
 		err->arg = 1;
 		return 1;
diff --git a/src/label.c b/src/label.c
@@ -57,8 +57,8 @@ static struct ltk_widget_vtable vtable = {
 	.motion_notify = NULL,
 	.mouse_leave = NULL,
 	.mouse_enter = NULL,
-	.type = LTK_LABEL,
-	.flags = LTK_NEEDS_REDRAW | LTK_NEEDS_SURFACE,
+	.type = LTK_WIDGET_LABEL,
+	.flags = LTK_NEEDS_REDRAW,
 };
 
 static struct {
@@ -96,7 +96,8 @@ ltk_label_draw(ltk_widget *self, ltk_rect clip) {
 	ltk_rect rect = label->widget.rect;
 	ltk_rect clip_final = ltk_rect_intersect(clip, rect);
 	ltk_surface *s;
-	if (!ltk_surface_cache_get_surface(self->surface_key, &s) || self->dirty)
+	ltk_surface_cache_request_surface_size(label->key, self->rect.w, self->rect.h);
+	if (!ltk_surface_cache_get_surface(label->key, &s) || self->dirty)
 		ltk_label_redraw_surface(label, s);
 	ltk_surface_copy(s, self->window->surface, ltk_rect_relative(rect, clip_final), clip_final.x, clip_final.y);
 }
@@ -126,6 +127,7 @@ ltk_label_create(ltk_window *window, const char *id, char *text) {
 	label->widget.ideal_w = text_w + theme.pad * 2;
 	label->widget.ideal_h = text_h + theme.pad * 2;
 	ltk_fill_widget_defaults(&label->widget, id, window, &vtable, label->widget.ideal_w, label->widget.ideal_h);
+	label->key = ltk_surface_cache_get_unnamed_key(window->surface_cache, label->widget.ideal_w, label->widget.ideal_h);
 
 	return label;
 }
@@ -138,7 +140,7 @@ ltk_label_destroy(ltk_widget *self, int shallow) {
 		ltk_warn("Tried to destroy NULL label.\n");
 		return;
 	}
-	ltk_surface_cache_release_key(self->surface_key);
+	ltk_surface_cache_release_key(label->key);
 	ltk_text_line_destroy(label->tl);
 	ltk_remove_widget(self->id);
 	ltk_free(self->id);
diff --git a/src/label.h b/src/label.h
@@ -24,6 +24,7 @@
 typedef struct {
 	ltk_widget widget;
 	ltk_text_line *tl;
+	ltk_surface_cache_key *key;
 } ltk_label;
 
 int ltk_label_ini_handler(ltk_window *window, const char *prop, const char *value);
diff --git a/src/ltk.h b/src/ltk.h
@@ -18,6 +18,7 @@
 #define _LTK_H_
 
 #include <stdint.h>
+#include "proto_types.h"
 
 typedef enum {
 	LTK_EVENT_RESIZE = 1 << 0,
@@ -100,5 +101,9 @@ int ltk_register_timer(long first, long repeat, void (*callback)(void *), void *
 void ltk_window_register_popup(ltk_window *window, ltk_widget *popup);
 void ltk_window_unregister_popup(ltk_window *window, ltk_widget *popup);
 void ltk_window_unregister_all_popups(ltk_window *window);
+int ltk_handle_lock_client(ltk_window *window, int client);
+int ltk_queue_specific_event(ltk_widget *widget, const char *type, uint32_t mask, const char *data);
+int ltk_queue_sock_write(int client, const char *str, int len);
+int ltk_queue_sock_write_fmt(int client, const char *fmt, ...);
 
 #endif
diff --git a/src/ltkd.c b/src/ltkd.c
@@ -1,6 +1,5 @@
 /* FIXME: backslashes should be parsed properly! */
 /* FIXME: Figure out how to properly print window id */
-/* FIXME: PROPERLY CLEANUP ALL THEMES */
 /* FIXME: error checking in tokenizer (is this necessary?) */
 /* FIXME: parsing doesn't work properly with bs? */
 /* FIXME: strip whitespace at end of lines in socket format */
@@ -125,11 +124,9 @@ static int read_sock(struct ltk_sock_info *sock);
 static int push_token(struct token_list *tl, char *token);
 static int read_sock(struct ltk_sock_info *sock);
 static int write_sock(struct ltk_sock_info *sock);
-static int queue_sock_write(struct ltk_sock_info *sock, const char *str, int len);
-static int queue_sock_write_fmt(struct ltk_sock_info *sock, const char *fmt, ...);
 static int tokenize_command(struct ltk_sock_info *sock);
 static int ltk_set_root_widget_cmd(ltk_window *window, char **tokens, int num_tokens, ltk_error *err);
-static void process_commands(ltk_window *window, struct ltk_sock_info *sock);
+static int process_commands(ltk_window *window, int client);
 static int add_client(int fd);
 static int listen_sock(const char *sock_path);
 static int accept_sock(int listenfd);
@@ -137,7 +134,6 @@ static int accept_sock(int listenfd);
 static short maxsocket = -1;
 static short running = 1;
 static short sock_write_available = 0;
-static int listenfd = -1;
 static char *ltk_dir = NULL;
 static FILE *ltk_logfile = NULL;
 static char *sock_path = NULL;
@@ -187,25 +183,91 @@ int main(int argc, char *argv[]) {
 	return ltk_mainloop(main_window);
 }
 
+/* FIXME: need to recalculate maxfd when removing client */
+static struct {
+	fd_set rallfds, wallfds;
+	int maxfd;
+	int listenfd;
+} sock_state;
+
+int
+ltk_handle_lock_client(ltk_window *window, int client) {
+	if (client < 0 || client >= MAX_SOCK_CONNS || sockets[client].fd == -1)
+		return 0;
+	fd_set rfds, wfds, rallfds, wallfds;
+	int clifd = sockets[client].fd;
+	FD_ZERO(&rallfds);
+	FD_ZERO(&wallfds);
+	FD_SET(clifd, &rallfds);
+	FD_SET(clifd, &wallfds);
+	int rretval, wretval;
+	struct timeval tv, tv_master;
+	tv_master.tv_sec = 0;
+	tv_master.tv_usec = 20000;
+	while (1) {
+		rfds = rallfds;
+		wfds = wallfds;
+		/* separate these because the writing fds are usually
+		   always ready for writing */
+		tv = tv_master;
+		rretval = select(clifd + 1, &rfds, NULL, NULL, &tv);
+		/* value of tv doesn't really matter anymore here because the
+		   necessary framerate-limiting delay is already done */
+		wretval = select(clifd + 1, NULL, &wfds, NULL, &tv);
+
+		if (rretval > 0 || ((sockets[client].write_cur != sockets[client].write_len) && wretval > 0)) {
+			if (FD_ISSET(clifd, &rfds)) {
+				if (read_sock(&sockets[client]) == 0) {
+					FD_CLR(clifd, &sock_state.rallfds);
+					FD_CLR(clifd, &sock_state.wallfds);
+					ltk_widget_remove_client(client);
+					sockets[clifd].fd = -1;
+					close(clifd);
+					int newmaxsocket = -1;
+					for (int j = 0; j <= maxsocket; j++) {
+						if (sockets[j].fd >= 0)
+							newmaxsocket = j;
+					}
+					maxsocket = newmaxsocket;
+					if (maxsocket == -1) {
+						ltk_quit(window);
+						break;
+					}
+					return 0;
+				} else {
+					int ret;
+					if ((ret = process_commands(window, client)) == 1)
+						return 1;
+					else if (ret == -1)
+						return 0;
+				}
+			}
+			if (FD_ISSET(clifd, &wfds)) {
+				write_sock(&sockets[client]);
+			}
+		}
+	}
+	return 0;
+}
+
 static int
 ltk_mainloop(ltk_window *window) {
 	ltk_event event;
-	fd_set rfds, wfds, rallfds, wallfds;
-	int maxfd;
+	fd_set rfds, wfds;
 	int rretval, wretval;
 	int clifd;
 	struct timeval tv, tv_master;
 	tv_master.tv_sec = 0;
 	tv_master.tv_usec = 20000;
 
-	FD_ZERO(&rallfds);
-	FD_ZERO(&wallfds);
+	FD_ZERO(&sock_state.rallfds);
+	FD_ZERO(&sock_state.wallfds);
 
-	if ((listenfd = listen_sock(sock_path)) < 0)
+	if ((sock_state.listenfd = listen_sock(sock_path)) < 0)
 		ltk_fatal_errno("Error listening on socket.\n");
 
-	FD_SET(listenfd, &rallfds);
-	maxfd = listenfd;
+	FD_SET(sock_state.listenfd, &sock_state.rallfds);
+	sock_state.maxfd = sock_state.listenfd;
 
 	printf("%lu", renderer_get_window_id(main_window->renderdata));
 	fflush(stdout);
@@ -221,31 +283,31 @@ ltk_mainloop(ltk_window *window) {
 	/* FIXME: framerate limiting for draw */
 
 	while (running) {
-		rfds = rallfds;
-		wfds = wallfds;
+		rfds = sock_state.rallfds;
+		wfds = sock_state.wallfds;
 		/* separate these because the writing fds are usually
 		   always ready for writing */
 		tv = tv_master;
-		rretval = select(maxfd + 1, &rfds, NULL, NULL, &tv);
+		rretval = select(sock_state.maxfd + 1, &rfds, NULL, NULL, &tv);
 		/* value of tv doesn't really matter anymore here because the
 		   necessary framerate-limiting delay is already done */
-		wretval = select(maxfd + 1, NULL, &wfds, NULL, &tv);
+		wretval = select(sock_state.maxfd + 1, NULL, &wfds, NULL, &tv);
 		while (ltk_events_pending(window->renderdata)) {
 			ltk_next_event(window->renderdata, &event);
 			ltk_handle_event(window, &event);
 		}
 
 		if (rretval > 0 || (sock_write_available && wretval > 0)) {
-			if (FD_ISSET(listenfd, &rfds)) {
-				if ((clifd = accept_sock(listenfd)) < 0) {
+			if (FD_ISSET(sock_state.listenfd, &rfds)) {
+				if ((clifd = accept_sock(sock_state.listenfd)) < 0) {
 					/* FIXME: Just log this! */
 					ltk_fatal_errno("Error accepting socket connection.\n");
 				}
 				int i = add_client(clifd);
-				FD_SET(clifd, &rallfds);
-				FD_SET(clifd, &wallfds);
-				if (clifd > maxfd)
-					maxfd = clifd;
+				FD_SET(clifd, &sock_state.rallfds);
+				FD_SET(clifd, &sock_state.wallfds);
+				if (clifd > sock_state.maxfd)
+					sock_state.maxfd = clifd;
 				if (i > maxsocket)
 					maxsocket = i;
 				continue;
@@ -255,8 +317,9 @@ ltk_mainloop(ltk_window *window) {
 					continue;
 				if (FD_ISSET(clifd, &rfds)) {
 					if (read_sock(&sockets[i]) == 0) {
-						FD_CLR(clifd, &rallfds);
-						FD_CLR(clifd, &wallfds);
+						ltk_widget_remove_client(i);
+						FD_CLR(clifd, &sock_state.rallfds);
+						FD_CLR(clifd, &sock_state.wallfds);
 						sockets[i].fd = -1;
 						close(clifd);
 						int newmaxsocket = -1;
@@ -270,7 +333,7 @@ ltk_mainloop(ltk_window *window) {
 							break;
 						}
 					} else {
-						process_commands(window, &sockets[i]);
+						process_commands(window, i);
 					}
 				}
 				if (FD_ISSET(clifd, &wfds)) {
@@ -315,7 +378,7 @@ ltk_mainloop(ltk_window *window) {
 				int event_len = strlen(cur->data);
 				for (int i = 0; i <= maxsocket; i++) {
 					if (sockets[i].fd != -1 && sockets[i].event_mask & cur->event_type) {
-						if (queue_sock_write(&sockets[i], cur->data, event_len) < 0)
+						if (ltk_queue_sock_write(i, cur->data, event_len) < 0)
 							ltk_fatal_errno("Unable to queue event.\n");
 					}
 				}
@@ -414,8 +477,8 @@ open_log(char *dir) {
 
 void
 ltk_cleanup(void) {
-	if (listenfd >= 0)
-		close(listenfd);
+	if (sock_state.listenfd >= 0)
+		close(sock_state.listenfd);
 	if (ltk_dir)
 		ltk_free(ltk_dir);
 	if (ltk_logfile)
@@ -479,7 +542,7 @@ ltk_set_root_widget_cmd(
 		err->arg = -1;
 		return 1;
 	}
-	widget = ltk_get_widget(tokens[1], LTK_WIDGET, err);
+	widget = ltk_get_widget(tokens[1], LTK_WIDGET_ANY, err);
 	if (!widget) {
 		err->arg = 1;
 		return 1;
@@ -501,24 +564,29 @@ ltk_window_invalidate_rect(ltk_window *window, ltk_rect rect) {
 		window->dirty_rect = ltk_rect_union(rect, window->dirty_rect);
 }
 
-void
-ltk_queue_event(ltk_window *window, ltk_userevent_type type, const char *id, const char *data) {
-	/* FIXME: make it nicer and safer */
-	struct ltk_event_queue *new = ltk_malloc(sizeof(struct ltk_event_queue));
-	new->event_type = type;
-	int id_len = strlen(id);
-	int data_len = strlen(data);
-	new->data = ltk_malloc(id_len + data_len + 3);
-	strcpy(new->data, id);
-	new->data[id_len] = ' ';
-	strcpy(new->data + id_len + 1, data);
-	new->data[id_len + data_len + 1] = '\n';
-	new->data[id_len + data_len + 2] = '\0';
-	new->next = window->first_event;
-	window->first_event = new;
-	new->prev = NULL;
-	if (!window->last_event)
-		window->last_event = new;
+/* FIXME: generic event handling functions that take the actual uint32_t event type, etc. */
+int
+ltk_queue_specific_event(ltk_widget *widget, const char *type, uint32_t mask, const char *data) {
+	int lock_client = -1;
+	for (size_t i = 0; i < widget->masks_num; i++) {
+		if (widget->event_masks[i].lwmask & mask) {
+			ltk_queue_sock_write_fmt(
+			    widget->event_masks[i].client,
+			    "eventl %s %s %s\n", widget->id, type, data
+			);
+			lock_client = widget->event_masks[i].client;
+		} else if (widget->event_masks[i].wmask & mask) {
+			ltk_queue_sock_write_fmt(
+			    widget->event_masks[i].client,
+			    "event %s %s %s\n", widget->id, type, data
+			);
+		}
+	}
+	if (lock_client >= 0) {
+		if (ltk_handle_lock_client(widget->window, lock_client))
+			return 1;
+	}
+	return 0;
 }
 
 static void
@@ -1115,8 +1183,11 @@ move_write_pos(struct ltk_sock_info *sock) {
    Returns -1 on error, 0 otherwise.
    Note: The string must include all '\n', etc. as defined in the protocol. This
    function just adds the given string verbatim. */
-static int
-queue_sock_write(struct ltk_sock_info *sock, const char *str, int len) {
+int
+ltk_queue_sock_write(int client, const char *str, int len) {
+	if (client < 0 || client >= MAX_SOCK_CONNS || sockets[client].fd == -1)
+		return 1;
+	struct ltk_sock_info *sock = &sockets[client];
 	move_write_pos(sock);
 	if (len < 0)
 		len = strlen(str);
@@ -1132,8 +1203,11 @@ queue_sock_write(struct ltk_sock_info *sock, const char *str, int len) {
 	return 0;
 }
 
-static int
-queue_sock_write_fmt(struct ltk_sock_info *sock, const char *fmt, ...) {
+int
+ltk_queue_sock_write_fmt(int client, const char *fmt, ...) {
+	if (client < 0 || client >= MAX_SOCK_CONNS || sockets[client].fd == -1)
+		return 1;
+	struct ltk_sock_info *sock = &sockets[client];
 	move_write_pos(sock);
 	va_list args;
 	va_start(args, fmt);
@@ -1193,13 +1267,136 @@ tokenize_command(struct ltk_sock_info *sock) {
 	return 1;
 }
 
+/* FIXME: currently no type-checking when setting specific widget mask */
+/* FIXME: this is really ugly and inefficient right now - it will be replaced with something
+   more generic at some point (or maybe just with a binary protocol?) */
+static int
+handle_mask_command(int client, char **tokens, size_t num_tokens, ltk_error *err) {
+	if (num_tokens != 4 && num_tokens != 5) {
+		err->type = ERR_INVALID_NUMBER_OF_ARGUMENTS;
+		err->arg = -1;
+		return 1;
+	}
+	uint32_t mask = 0;
+	int lock = 0;
+	int special = 0;
+	ltk_widget *widget = ltk_get_widget(tokens[1], LTK_WIDGET_ANY, err);
+	if (!widget) {
+		err->arg = 1;
+		return 1;
+	}
+	if (!strcmp(tokens[2], "widget")) {
+		if (!strcmp(tokens[3], "mousepress")) {
+			mask = LTK_PEVENTMASK_MOUSEPRESS;
+		} else if (!strcmp(tokens[3], "mouserelease")) {
+			mask = LTK_PEVENTMASK_MOUSERELEASE;
+		} else if (!strcmp(tokens[3], "mousemotion")) {
+			mask = LTK_PEVENTMASK_MOUSEMOTION;
+		} else if (!strcmp(tokens[3], "configure")) {
+			mask = LTK_PEVENTMASK_CONFIGURE;
+		} else if (!strcmp(tokens[3], "statechange")) {
+			mask = LTK_PEVENTMASK_STATECHANGE;
+		} else if (!strcmp(tokens[3], "none")) {
+			mask = LTK_PEVENTMASK_NONE;
+		} else {
+			err->type = ERR_INVALID_ARGUMENT;
+			err->arg = 3;
+			return 1;
+		}
+	} else if (!strcmp(tokens[2], "menuentry")) {
+		if (!strcmp(tokens[3], "press")) {
+			mask = LTK_PWEVENTMASK_MENUENTRY_PRESS;
+		} else if (!strcmp(tokens[3], "none")) {
+			mask = LTK_PWEVENTMASK_MENUENTRY_NONE;
+		} else {
+			err->type = ERR_INVALID_ARGUMENT;
+			err->arg = 3;
+			return 1;
+		}
+		special = 1;
+	} else if (!strcmp(tokens[2], "button")) {
+		if (!strcmp(tokens[3], "press")) {
+			mask = LTK_PWEVENTMASK_BUTTON_PRESS;
+		} else if (!strcmp(tokens[3], "none")) {
+			mask = LTK_PWEVENTMASK_BUTTON_NONE;
+		} else {
+			err->type = ERR_INVALID_ARGUMENT;
+			err->arg = 3;
+			return 1;
+		}
+		special = 1;
+	} else {
+		err->type = ERR_INVALID_ARGUMENT;
+		err->arg = 2;
+	}
+	if (num_tokens == 5) {
+		if (!strcmp(tokens[4], "lock")) {
+			lock = 1;
+		} else {
+			err->type = ERR_INVALID_ARGUMENT;
+			err->arg = 4;
+			return 1;
+		}
+	}
+
+	if (!strcmp(tokens[0], "mask-add")) {
+		if (lock) {
+			if (special)
+				ltk_widget_add_to_event_lwmask(widget, client, mask);
+			else
+				ltk_widget_add_to_event_lmask(widget, client, mask);
+		} else {
+			if (special)
+				ltk_widget_add_to_event_wmask(widget, client, mask);
+			else
+				ltk_widget_add_to_event_mask(widget, client, mask);
+		}
+	} else if (!strcmp(tokens[0], "mask-set")) {
+		if (lock) {
+			if (special)
+				ltk_widget_set_event_lwmask(widget, client, mask);
+			else
+				ltk_widget_set_event_lmask(widget, client, mask);
+		} else {
+			if (special)
+				ltk_widget_set_event_wmask(widget, client, mask);
+			else
+				ltk_widget_set_event_mask(widget, client, mask);
+		}
+	} else if (!strcmp(tokens[0], "mask-remove")) {
+		if (lock) {
+			if (special)
+				ltk_widget_remove_from_event_lwmask(widget, client, mask);
+			else
+				ltk_widget_remove_from_event_lmask(widget, client, mask);
+		} else {
+			if (special)
+				ltk_widget_remove_from_event_wmask(widget, client, mask);
+			else
+				ltk_widget_remove_from_event_mask(widget, client, mask);
+		}
+	} else {
+		err->type = ERR_INVALID_COMMAND;
+		err->arg = 0;
+		return 1;
+	}
+	return 0;
+}
+
 /* Process the commands as they are read from the socket. */
-static void
-process_commands(ltk_window *window, struct ltk_sock_info *sock) {
+/* Returns 1 if command was 'event-unlock true',
+   -1 if command was 'event-unlock false', 0 otherwise. */
+static int
+process_commands(ltk_window *window, int client) {
+	if (client < 0 || client >= MAX_SOCK_CONNS || sockets[client].fd == -1)
+		return 0;
+	struct ltk_sock_info *sock = &sockets[client];
 	char **tokens;
 	int num_tokens;
 	ltk_error errdetail = {ERR_NONE, -1};
 	int err;
+	int retval = 0;
+	int last = 0;
 	while (!tokenize_command(sock)) {
 		err = 0;
 		tokens = sock->tokens.tokens;
@@ -1226,6 +1423,22 @@ process_commands(ltk_window *window, struct ltk_sock_info *sock) {
 			ltk_quit(window);
 		} else if (strcmp(tokens[0], "destroy") == 0) {
 			err = ltk_widget_destroy_cmd(window, tokens, num_tokens, &errdetail);
+		} else if (strncmp(tokens[0], "mask", 4) == 0) {
+			err = handle_mask_command(client, tokens, num_tokens, &errdetail);
+		} else if (strcmp(tokens[0], "event-unlock") == 0) {
+			if (num_tokens != 2) {
+				errdetail.type = ERR_INVALID_NUMBER_OF_ARGUMENTS;
+				err = 1;
+			} else if (strcmp(tokens[1], "true") == 0) {
+				retval = 1;
+			} else if (strcmp(tokens[1], "false") == 0) {
+				retval = -1;
+			} else {
+				err = 1;
+				errdetail.type = ERR_INVALID_ARGUMENT;
+				errdetail.arg = 1;
+			}
+			last = 1;
 		} else {
 			errdetail.type = ERR_INVALID_COMMAND;
 			errdetail.arg = -1;
@@ -1234,9 +1447,11 @@ process_commands(ltk_window *window, struct ltk_sock_info *sock) {
 		sock->tokens.num_tokens = 0;
 		if (err) {
 			const char *errmsg = errtype_to_string(errdetail.type);
-			if (queue_sock_write_fmt(sock, "err %d arg %d msg \"%s\"\n", errdetail.type, errdetail.arg, errmsg) < 0)
+			if (ltk_queue_sock_write_fmt(client, "err %d %d \"%s\"\n", errdetail.type, errdetail.arg, errmsg) < 0)
 				ltk_fatal("Unable to queue socket write.\n");
 		}
+		if (last)
+			break;
 	}
 	if (sock->tokens.num_tokens > 0 && sock->tokens.tokens[0] != sock->read) {
 		memmove(sock->read, sock->tokens.tokens[0], sock->read + sock->read_len - sock->tokens.tokens[0]);
@@ -1251,4 +1466,5 @@ process_commands(ltk_window *window, struct ltk_sock_info *sock) {
 		sock->read_len = 0;
 		sock->read_cur = 0;
 	}
+	return retval;
 }
diff --git a/src/menu.c b/src/menu.c
@@ -23,6 +23,7 @@
 #include <stdarg.h>
 #include <math.h>
 
+#include "proto_types.h"
 #include "event.h"
 #include "memory.h"
 #include "color.h"
@@ -116,7 +117,7 @@ static void ltk_menuentry_detach_submenu(ltk_menuentry *e);
 
 static int ltk_menu_remove_child(ltk_widget *widget, ltk_widget *self, ltk_error *err);
 
-#define IN_SUBMENU(e) (e->widget.parent && e->widget.parent->vtable->type == LTK_MENU && ((ltk_menu *)e->widget.parent)->is_submenu)
+#define IN_SUBMENU(e) (e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_MENU && ((ltk_menu *)e->widget.parent)->is_submenu)
 
 static struct ltk_widget_vtable vtable = {
 	.key_press = NULL,
@@ -134,7 +135,7 @@ static struct ltk_widget_vtable vtable = {
 	.destroy = <k_menu_destroy,
 	.child_size_change = &recalc_ideal_menu_size,
 	.remove_child = <k_menu_remove_child,
-	.type = LTK_MENU,
+	.type = LTK_WIDGET_MENU,
 	.flags = LTK_NEEDS_REDRAW,
 };
 
@@ -154,7 +155,7 @@ static struct ltk_widget_vtable entry_vtable = {
 	.destroy = <k_menuentry_destroy,
 	.child_size_change = NULL,
 	.remove_child = NULL,
-	.type = LTK_MENUENTRY,
+	.type = LTK_WIDGET_MENUENTRY,
 	.flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS | LTK_HOVER_IS_ACTIVE,
 };
 
@@ -294,7 +295,7 @@ static void
 ltk_menuentry_change_state(ltk_widget *self, ltk_widget_state old_state) {
 	ltk_menuentry *e = (ltk_menuentry *)self;
 	int in_submenu = IN_SUBMENU(e);
-	int submenus_opened = self->parent && self->parent->vtable->type == LTK_MENU && ((ltk_menu *)self->parent)->popup_submenus;
+	int submenus_opened = self->parent && self->parent->vtable->type == LTK_WIDGET_MENU && ((ltk_menu *)self->parent)->popup_submenus;
 	if (!(self->state & (LTK_ACTIVE | LTK_PRESSED))) {
 		/* Note: This only has to take care of the submenu that is the direct child
 		   of e because ltk_window_set_active_widget already calls change_state for
@@ -306,7 +307,7 @@ ltk_menuentry_change_state(ltk_widget *self, ltk_widget_state old_state) {
 	           ((self->state & LTK_ACTIVE) && (in_submenu || submenus_opened))) &&
 		   e->submenu && e->submenu->widget.hidden) {
 		popup_active_menu(e);
-		if (self->parent && self->parent->vtable->type == LTK_MENU)
+		if (self->parent && self->parent->vtable->type == LTK_WIDGET_MENU)
 			((ltk_menu *)self->parent)->popup_submenus = 1;
 	}
 }
@@ -614,15 +615,16 @@ ltk_menuentry_mouse_release(ltk_widget *self, ltk_button_event *event) {
 	(void)event;
 	ltk_menuentry *e = (ltk_menuentry *)self;
 	int in_submenu = IN_SUBMENU(e);
-	int keep_popup = self->parent && self->parent->vtable->type == LTK_MENU && ((ltk_menu *)self->parent)->popup_submenus;
+	int keep_popup = self->parent && self->parent->vtable->type == LTK_WIDGET_MENU && ((ltk_menu *)self->parent)->popup_submenus;
 	/* FIXME: problem when scrolling because actual shown rect may not be entire rect */
 	if ((self->state & LTK_PRESSED) && event->button == LTK_BUTTONL && ltk_collide_rect(self->rect, event->x, event->y)) {
 		if (in_submenu || !keep_popup) {
 			ltk_window_unregister_all_popups(self->window);
 		}
-		ltk_queue_event(self->window, LTK_EVENT_MENU, self->id, "menu_entry_click");
+		ltk_queue_specific_event(self, "menu", LTK_PWEVENTMASK_MENUENTRY_PRESS, "press");
+		return 1;
 	}
-	return 1;
+	return 0;
 }
 
 static int
@@ -662,8 +664,8 @@ ltk_menu_hide(ltk_widget *self) {
 	ltk_window_unregister_popup(self->window, self);
 	ltk_window_invalidate_rect(self->window, self->rect);
 	/* FIXME: this is really ugly/hacky */
-	if (menu->unpopup_submenus_on_hide && self->parent && self->parent->vtable->type == LTK_MENUENTRY &&
-	    self->parent->parent && self->parent->parent->vtable->type == LTK_MENU) {
+	if (menu->unpopup_submenus_on_hide && self->parent && self->parent->vtable->type == LTK_WIDGET_MENUENTRY &&
+	    self->parent->parent && self->parent->parent->vtable->type == LTK_WIDGET_MENU) {
 		((ltk_menu *)self->parent->parent)->popup_submenus = 0;
 	}
 	menu->unpopup_submenus_on_hide = 1;
@@ -677,7 +679,7 @@ popup_active_menu(ltk_menuentry *e) {
 	int in_submenu = 0, was_opened_left = 0;
 	ltk_rect menu_rect = e->widget.rect;
 	ltk_rect entry_rect = e->widget.rect;
-	if (e->widget.parent && e->widget.parent->vtable->type == LTK_MENU) {
+	if (e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_MENU) {
 		ltk_menu *menu = (ltk_menu *)e->widget.parent;
 		in_submenu = menu->is_submenu;
 		was_opened_left = menu->was_opened_left;
@@ -861,7 +863,7 @@ static void
 recalc_ideal_menu_size(ltk_widget *self, ltk_widget *widget) {
 	ltk_menu *menu = (ltk_menu *)self;
 	/* If widget with size change is submenu, it doesn't affect this menu */
-	if (widget && widget->vtable->type == LTK_MENU) {
+	if (widget && widget->vtable->type == LTK_WIDGET_MENU) {
 		ltk_widget_resize(widget);
 		return;
 	}
@@ -1143,12 +1145,12 @@ ltk_menu_cmd_insert_entry(
 		err->arg = -1;
 		return 1;
 	}
-	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, err);
+	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_WIDGET_MENU, err);
 	if (!menu) {
 		err->arg = 1;
 		return 1;
 	}
-	e = (ltk_menuentry *)ltk_get_widget(tokens[3], LTK_MENUENTRY, err);
+	e = (ltk_menuentry *)ltk_get_widget(tokens[3], LTK_WIDGET_MENUENTRY, err);
 	if (!e) {
 		err->arg = 3;
 		return 1;
@@ -1181,12 +1183,12 @@ ltk_menu_cmd_add_entry(
 		err->arg = -1;
 		return 1;
 	}
-	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, err);
+	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_WIDGET_MENU, err);
 	if (!menu) {
 		err->arg = 1;
 		return 1;
 	}
-	e = (ltk_menuentry *)ltk_get_widget(tokens[3], LTK_MENUENTRY, err);
+	e = (ltk_menuentry *)ltk_get_widget(tokens[3], LTK_WIDGET_MENUENTRY, err);
 	if (!e) {
 		err->arg = 3;
 		return 1;
@@ -1213,7 +1215,7 @@ ltk_menu_cmd_remove_entry_index(
 		err->arg = -1;
 		return 1;
 	}
-	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, err);
+	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_WIDGET_MENU, err);
 	if (!menu) {
 		err->arg = 1;
 		return 1;
@@ -1246,7 +1248,7 @@ ltk_menu_cmd_remove_entry_id(
 		err->arg = -1;
 		return 1;
 	}
-	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, err);
+	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_WIDGET_MENU, err);
 	if (!menu) {
 		err->arg = 1;
 		return 1;
@@ -1273,7 +1275,7 @@ ltk_menu_cmd_remove_all_entries(
 		err->arg = -1;
 		return 1;
 	}
-	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, err);
+	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_WIDGET_MENU, err);
 	if (!menu) {
 		err->arg = 1;
 		return 1;
@@ -1322,12 +1324,12 @@ ltk_menuentry_cmd_attach_submenu(
 		err->arg = -1;
 		return 1;
 	}
-	e = (ltk_menuentry *)ltk_get_widget(tokens[1], LTK_MENUENTRY, err);
+	e = (ltk_menuentry *)ltk_get_widget(tokens[1], LTK_WIDGET_MENUENTRY, err);
 	if (!e) {
 		err->arg = 1;
 		return 1;
 	}
-	submenu = (ltk_menu *)ltk_get_widget(tokens[3], LTK_MENU, err);
+	submenu = (ltk_menu *)ltk_get_widget(tokens[3], LTK_WIDGET_MENU, err);
 	if (!submenu) {
 		err->arg = 3;
 		return 1;
@@ -1354,7 +1356,7 @@ ltk_menuentry_cmd_detach_submenu(
 		err->arg = -1;
 		return 1;
 	}
-	e = (ltk_menuentry *)ltk_get_widget(tokens[1], LTK_MENUENTRY, err);
+	e = (ltk_menuentry *)ltk_get_widget(tokens[1], LTK_WIDGET_MENUENTRY, err);
 	if (!e) {
 		err->arg = 1;
 		return 1;
diff --git a/src/proto_types.h b/src/proto_types.h
@@ -0,0 +1,51 @@
+#ifndef LTK_PROTO_TYPES_H
+#define LTK_PROTO_TYPES_H
+
+#define LTK_WIDGET_UNKNOWN   0
+#define LTK_WIDGET_ANY       1
+#define LTK_WIDGET_GRID      2
+#define LTK_WIDGET_BUTTON    3
+#define LTK_WIDGET_LABEL     4
+#define LTK_WIDGET_BOX       5
+#define LTK_WIDGET_MENU      6
+#define LTK_WIDGET_MENUENTRY 7
+#define LTK_NUM_WIDGETS      8
+
+#define LTK_WIDGETMASK_UNKNOWN    (UINT32_C(1) << LTK_WIDGET_UNKNOWN)
+#define LTK_WIDGETMASK_ANY        (UINT32_C(0xFFFF))
+#define LTK_WIDGETMASK_GRID       (UINT32_C(1) << LTK_WIDGET_GRID)
+#define LTK_WIDGETMASK_BUTTON     (UINT32_C(1) << LTK_WIDGET_BUTTON)
+#define LTK_WIDGETMASK_LABEL      (UINT32_C(1) << LTK_WIDGET_LABEL)
+#define LTK_WIDGETMASK_BOX        (UINT32_C(1) << LTK_WIDGET_BOX)
+#define LTK_WIDGETMASK_MENU       (UINT32_C(1) << LTK_WIDGET_MENU)
+#define LTK_WIDGETMASK_MENUENTRY  (UINT32_C(1) << LTK_WIDGET_MENUENTRY)
+
+/* P == protocol; W == widget */
+
+#define LTK_PEVENT_MOUSEPRESS    0
+#define LTK_PEVENT_MOUSERELEASE  1
+#define LTK_PEVENT_MOUSEMOTION   2
+#define LTK_PEVENT_KEYPRESS      3
+#define LTK_PEVENT_KEYRELEASE    4
+#define LTK_PEVENT_CONFIGURE     5
+#define LTK_PEVENT_STATECHANGE   6
+
+#define LTK_PEVENTMASK_NONE          (UINT32_C(0))
+#define LTK_PEVENTMASK_MOUSEPRESS    (UINT32_C(1) << LTK_PEVENT_MOUSEPRESS)
+#define LTK_PEVENTMASK_MOUSERELEASE  (UINT32_C(1) << LTK_PEVENT_MOUSERELEASE)
+#define LTK_PEVENTMASK_MOUSEMOTION   (UINT32_C(1) << LTK_PEVENT_MOUSEMOTION)
+#define LTK_PEVENTMASK_KEYPRESS      (UINT32_C(1) << LTK_PEVENT_KEYPRESS)
+#define LTK_PEVENTMASK_KEYRELEASE    (UINT32_C(1) << LTK_PEVENT_KEYRELEASE)
+#define LTK_PEVENTMASK_CONFIGURE     (UINT32_C(1) << LTK_PEVENT_CONFIGURE)
+#define LTK_PEVENTMASK_EXPOSE        (UINT32_C(1) << LTK_PEVENT_EXPOSE)
+#define LTK_PEVENTMASK_STATECHANGE   (UINT32_C(1) << LTK_PEVENT_STATECHANGE)
+
+#define LTK_PWEVENT_MENUENTRY_PRESS     0
+#define LTK_PWEVENTMASK_MENUENTRY_NONE  (UINT32_C(0))
+#define LTK_PWEVENTMASK_MENUENTRY_PRESS (UINT32_C(1) << LTK_PWEVENT_MENUENTRY_PRESS)
+
+#define LTK_PWEVENT_BUTTON_PRESS     0
+#define LTK_PWEVENTMASK_BUTTON_NONE  (UINT32_C(0))
+#define LTK_PWEVENTMASK_BUTTON_PRESS (UINT32_C(1) << LTK_PWEVENT_BUTTON_PRESS)
+
+#endif /* LTK_PROTO_TYPES_H */
diff --git a/src/scrollbar.c b/src/scrollbar.c
@@ -52,9 +52,9 @@ static struct ltk_widget_vtable vtable = {
 	.mouse_enter = NULL,
 	.child_size_change = NULL,
 	.remove_child = NULL,
-	.type = LTK_UNKNOWN, /* FIXME */
+	.type = LTK_WIDGET_UNKNOWN, /* FIXME */
 	/* FIXME: need different activatable state so arrow keys don't move onto scrollbar */
-	.flags = LTK_NEEDS_REDRAW | LTK_NEEDS_SURFACE | LTK_ACTIVATABLE_ALWAYS,
+	.flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS,
 };
 
 static struct {
@@ -147,7 +147,8 @@ ltk_scrollbar_draw(ltk_widget *self, ltk_rect clip) {
 		fg = &theme.fg_normal;
 	}
 	ltk_surface *s;
-	ltk_surface_cache_get_surface(self->surface_key, &s);
+	ltk_surface_cache_request_surface_size(scrollbar->key, self->rect.w, self->rect.h);
+	ltk_surface_cache_get_surface(scrollbar->key, &s);
 	ltk_surface_fill_rect(s, bg, (ltk_rect){0, 0, rect.w, rect.h});
 	/* FIXME: maybe too much calculation in draw function - move to
 	   resizing function? */
@@ -242,6 +243,7 @@ ltk_scrollbar_create(ltk_window *window, ltk_orientation orient, void (*callback
 		sc->widget.rect.w = theme.size;
 	sc->callback = callback;
 	sc->callback_data = data;
+	sc->key = ltk_surface_cache_get_unnamed_key(window->surface_cache, sc->widget.ideal_w, sc->widget.ideal_h);
 
 	return sc;
 }
@@ -249,6 +251,7 @@ ltk_scrollbar_create(ltk_window *window, ltk_orientation orient, void (*callback
 static void
 ltk_scrollbar_destroy(ltk_widget *self, int shallow) {
 	(void)shallow;
-	ltk_surface_cache_release_key(self->surface_key);
+	ltk_scrollbar *sc = (ltk_scrollbar *)self;
+	ltk_surface_cache_release_key(sc->key);
 	ltk_free(self);
 }
diff --git a/src/scrollbar.h b/src/scrollbar.h
@@ -28,6 +28,7 @@ typedef struct {
 	int last_mouse_x;
 	int last_mouse_y;
 	ltk_orientation orient;
+	ltk_surface_cache_key *key;
 } ltk_scrollbar;
 
 void ltk_scrollbar_set_virtual_size(ltk_scrollbar *scrollbar, int virtual_size);
diff --git a/src/surface_cache.c b/src/surface_cache.c
@@ -143,7 +143,7 @@ ltk_surface_cache_get_unnamed_key(ltk_surface_cache *cache, int min_w, int min_h
 	key->min_w = min_w;
 	key->min_h = min_h;
 	key->is_named = 0;
-	key->widget_type = LTK_UNKNOWN;
+	key->widget_type = LTK_WIDGET_UNKNOWN;
 	key->id = -1;
 	key->refcount = 1;
 	return key;
diff --git a/src/widget.c b/src/widget.c
@@ -55,6 +55,117 @@ ltk_destroy_widget_hash(void) {
 	hash_locked = 0;
 }
 
+/* FIXME: any way to optimize the whole event mask handling a bit? */
+void
+ltk_widget_remove_client(int client) {
+	khint_t k;
+	ltk_widget *ptr;
+	for (k = kh_begin(widget_hash); k != kh_end(widget_hash); k++) {
+		if (kh_exist(widget_hash, k)) {
+			ptr = kh_value(widget_hash, k);
+			for (size_t i = 0; i < ptr->masks_num; i++) {
+				if (ptr->event_masks[i].client == client) {
+					memmove(ptr->event_masks + i, ptr->event_masks + i + 1, ptr->masks_num - i - 1);
+					ptr->masks_num--;
+					size_t sz = ideal_array_size(ptr->masks_alloc, ptr->masks_num - 1);
+					if (sz != ptr->masks_alloc) {
+						ptr->masks_alloc = sz;
+						ptr->event_masks = ltk_reallocarray(ptr->event_masks, sz, sizeof(client_event_mask));
+					}
+					break;
+				}
+			}
+		}
+	}
+}
+
+static client_event_mask *
+get_mask_struct(ltk_widget *widget, int client) {
+	for (size_t i = 0; i < widget->masks_num; i++) {
+		if (widget->event_masks[i].client == client)
+			return &widget->event_masks[i];
+	}
+	widget->masks_alloc = ideal_array_size(widget->masks_alloc, widget->masks_num + 1);
+	widget->event_masks = ltk_reallocarray(widget->event_masks, widget->masks_alloc, sizeof(client_event_mask));
+	client_event_mask *m = &widget->event_masks[widget->masks_num];
+	widget->masks_num++;
+	m->client = client;
+	m->mask = m->lmask = m->wmask = m->lwmask = 0;
+	return m;
+}
+
+void
+ltk_widget_set_event_mask(ltk_widget *widget, int client, uint32_t mask) {
+	client_event_mask *m = get_mask_struct(widget, client);
+	m->mask = mask;
+}
+
+void
+ltk_widget_set_event_lmask(ltk_widget *widget, int client, uint32_t mask) {
+	client_event_mask *m = get_mask_struct(widget, client);
+	m->lmask = mask;
+}
+
+void
+ltk_widget_set_event_wmask(ltk_widget *widget, int client, uint32_t mask) {
+	client_event_mask *m = get_mask_struct(widget, client);
+	m->wmask = mask;
+}
+
+void
+ltk_widget_set_event_lwmask(ltk_widget *widget, int client, uint32_t mask) {
+	client_event_mask *m = get_mask_struct(widget, client);
+	m->lwmask = mask;
+}
+
+void
+ltk_widget_add_to_event_mask(ltk_widget *widget, int client, uint32_t mask) {
+	client_event_mask *m = get_mask_struct(widget, client);
+	m->mask |= mask;
+}
+
+void
+ltk_widget_add_to_event_lmask(ltk_widget *widget, int client, uint32_t mask) {
+	client_event_mask *m = get_mask_struct(widget, client);
+	m->lmask |= mask;
+}
+
+void
+ltk_widget_add_to_event_wmask(ltk_widget *widget, int client, uint32_t mask) {
+	client_event_mask *m = get_mask_struct(widget, client);
+	m->wmask |= mask;
+}
+
+void
+ltk_widget_add_to_event_lwmask(ltk_widget *widget, int client, uint32_t mask) {
+	client_event_mask *m = get_mask_struct(widget, client);
+	m->lwmask |= mask;
+}
+
+void
+ltk_widget_remove_from_event_mask(ltk_widget *widget, int client, uint32_t mask) {
+	client_event_mask *m = get_mask_struct(widget, client);
+	m->mask &= ~mask;
+}
+
+void
+ltk_widget_remove_from_event_lmask(ltk_widget *widget, int client, uint32_t mask) {
+	client_event_mask *m = get_mask_struct(widget, client);
+	m->lmask &= ~mask;
+}
+
+void
+ltk_widget_remove_from_event_wmask(ltk_widget *widget, int client, uint32_t mask) {
+	client_event_mask *m = get_mask_struct(widget, client);
+	m->wmask &= ~mask;
+}
+
+void
+ltk_widget_remove_from_event_lwmask(ltk_widget *widget, int client, uint32_t mask) {
+	client_event_mask *m = get_mask_struct(widget, client);
+	m->lwmask &= ~mask;
+}
+
 void
 ltk_widgets_init() {
 	widget_hash = kh_init(widget);
@@ -77,11 +188,6 @@ ltk_fill_widget_defaults(ltk_widget *widget, const char *id, ltk_window *window,
 	widget->window = window;
 	widget->parent = NULL;
 
-	if (vtable->flags & LTK_NEEDS_SURFACE)
-		widget->surface_key = ltk_surface_cache_get_unnamed_key(window->surface_cache, w, h);
-	else
-		widget->surface_key = NULL;
-
 	/* FIXME: possibly check that draw and destroy aren't NULL */
 	widget->vtable = vtable;
 
@@ -92,6 +198,9 @@ ltk_fill_widget_defaults(ltk_widget *widget, const char *id, ltk_window *window,
 	widget->rect.w = w;
 	widget->rect.h = h;
 
+	widget->event_masks = NULL;
+	widget->masks_num = widget->masks_alloc = 0;
+
 	widget->row = 0;
 	widget->column = 0;
 	widget->row_span = 0;
@@ -146,17 +255,56 @@ ltk_widget_hide(ltk_widget *widget) {
    That would make a bit more sense */
 void
 ltk_widget_resize(ltk_widget *widget) {
-	/* FIXME: should surface maybe be resized first? */
+	int lock_client = -1;
+	for (size_t i = 0; i < widget->masks_num; i++) {
+		if (widget->event_masks[i].lmask & LTK_PEVENTMASK_CONFIGURE) {
+			ltk_queue_sock_write_fmt(
+			    widget->event_masks[i].client,
+			    "eventl %s widget configure %d %d %d %d\n",
+			    widget->id, widget->rect.x, widget->rect.y,
+			    widget->rect.w, widget->rect.h
+			);
+			lock_client = widget->event_masks[i].client;
+		} else if (widget->event_masks[i].mask & LTK_PEVENTMASK_CONFIGURE) {
+			ltk_queue_sock_write_fmt(
+			    widget->event_masks[i].client,
+			    "event %s widget configure %d %d %d %d\n",
+			    widget->id, widget->rect.x, widget->rect.y,
+			    widget->rect.w, widget->rect.h
+			);
+		}
+	}
+	if (lock_client >= 0) {
+		if (ltk_handle_lock_client(widget->window, lock_client))
+			return;
+	}
 	if (widget->vtable->resize)
 		widget->vtable->resize(widget);
-	if (!(widget->vtable->flags & LTK_NEEDS_SURFACE))
-		return;
-	ltk_surface_cache_request_surface_size(widget->surface_key, widget->rect.w, widget->rect.h);
 	widget->dirty = 1;
 }
 
 void
 ltk_widget_change_state(ltk_widget *widget, ltk_widget_state old_state) {
+	int lock_client = -1;
+	/* FIXME: give old and new state in event */
+	for (size_t i = 0; i < widget->masks_num; i++) {
+		if (widget->event_masks[i].lmask & LTK_PEVENTMASK_CONFIGURE) {
+			ltk_queue_sock_write_fmt(
+			    widget->event_masks[i].client,
+			    "eventl %s widget statechange\n", widget->id
+			);
+			lock_client = widget->event_masks[i].client;
+		} else if (widget->event_masks[i].mask & LTK_PEVENTMASK_CONFIGURE) {
+			ltk_queue_sock_write_fmt(
+			    widget->event_masks[i].client,
+			    "event %s widget statechange\n", widget->id
+			);
+		}
+	}
+	if (lock_client >= 0) {
+		if (ltk_handle_lock_client(widget->window, lock_client))
+			return;
+	}
 	if (widget->vtable->change_state)
 		widget->vtable->change_state(widget, old_state);
 	if (widget->vtable->flags & LTK_NEEDS_REDRAW) {
@@ -195,6 +343,34 @@ is_parent(ltk_widget *parent, ltk_widget *child) {
 	return child != NULL;
 }
 
+static int
+queue_mouse_event(ltk_widget *widget, char *type, uint32_t mask, int x, int y) {
+	int lock_client = -1;
+	for (size_t i = 0; i < widget->masks_num; i++) {
+		if (widget->event_masks[i].lmask & mask) {
+			ltk_queue_sock_write_fmt(
+			    widget->event_masks[i].client,
+			    "eventl %s widget %s %d %d %d %d\n",
+			    widget->id, type, x, y,
+			    x - widget->rect.x, y - widget->rect.y
+			);
+			lock_client = widget->event_masks[i].client;
+		} else if (widget->event_masks[i].mask & mask) {
+			ltk_queue_sock_write_fmt(
+			    widget->event_masks[i].client,
+			    "event %s widget %s %d %d %d %d\n",
+			    widget->id, type, x, y,
+			    x - widget->rect.x, y - widget->rect.y
+			);
+		}
+	}
+	if (lock_client >= 0) {
+		if (ltk_handle_lock_client(widget->window, lock_client))
+			return 1;
+	}
+	return 0;
+}
+
 /* FIXME: This is still weird. */
 void
 ltk_window_mouse_press_event(ltk_window *window, ltk_button_event *event) {
@@ -228,7 +404,9 @@ ltk_window_mouse_press_event(ltk_window *window, ltk_button_event *event) {
 		if (cur_widget->state != LTK_DISABLED) {
 			/* FIXME: figure out whether this makes sense - currently, all widgets (unless disabled)
 			   get mouse press, but they are only set to pressed if they are activatable */
-			if (cur_widget->vtable->mouse_press)
+			if (queue_mouse_event(cur_widget, "mousepress", LTK_PEVENTMASK_MOUSEPRESS, event->x, event->y))
+				handled = 1;
+			else if (cur_widget->vtable->mouse_press)
 				handled = cur_widget->vtable->mouse_press(cur_widget, event);
 			/* set first non-disabled widget to pressed widget */
 			if (first && event->button == LTK_BUTTONL && (cur_widget->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
@@ -257,8 +435,11 @@ ltk_window_mouse_release_event(ltk_window *window, ltk_button_event *event) {
 		widget = get_widget_under_pointer(widget, event->x, event->y);
 	}
 	/* FIXME: loop up to top of hierarchy if not handled */
-	if (widget && widget->vtable->mouse_release)
+	if (widget && queue_mouse_event(widget, "mouserelease", LTK_PEVENTMASK_MOUSERELEASE, event->x, event->y)) {
+		/* NOP */
+	} else if (widget && widget->vtable->mouse_release) {
 		widget->vtable->mouse_release(widget, event);
+	}
 	if (event->button == LTK_BUTTONL) {
 		ltk_window_set_pressed_widget(window, NULL);
 		/* send motion notify to widget under pointer */
@@ -290,7 +471,9 @@ ltk_window_motion_notify_event(ltk_window *window, ltk_motion_event *event) {
 	while (cur_widget) {
 		int handled = 0;
 		if (cur_widget->state != LTK_DISABLED) {
-			if (cur_widget->vtable->motion_notify)
+			if (queue_mouse_event(cur_widget, "mousemotion", LTK_PEVENTMASK_MOUSEMOTION, event->x, event->y))
+				handled = 1;
+			else if (cur_widget->vtable->motion_notify)
 				handled = cur_widget->vtable->motion_notify(cur_widget, event);
 			/* set first non-disabled widget to hover widget */
 			/* FIXME: should enter/leave event be sent to parent
@@ -329,7 +512,7 @@ ltk_get_widget(const char *id, ltk_widget_type type, ltk_error *err) {
 		return NULL;
 	}
 	widget = kh_value(widget_hash, k);
-	if (type != LTK_WIDGET && widget->vtable->type != type) {
+	if (type != LTK_WIDGET_ANY && widget->vtable->type != type) {
 		err->type = ERR_INVALID_WIDGET_TYPE;
 		return NULL;
 	}
@@ -397,7 +580,7 @@ ltk_widget_destroy_cmd(
 			return 1;
 		}
 	}
-	ltk_widget *widget = ltk_get_widget(tokens[1], LTK_WIDGET, err);
+	ltk_widget *widget = ltk_get_widget(tokens[1], LTK_WIDGET_ANY, err);
 	if (!widget) {
 		err->arg = 1;
 		return 1;
diff --git a/src/widget.h b/src/widget.h
@@ -25,6 +25,7 @@
 
 
 typedef struct ltk_widget ltk_widget;
+typedef uint32_t ltk_widget_type;
 
 typedef enum {
 	LTK_ACTIVATABLE_NORMAL = 1,
@@ -32,8 +33,7 @@ typedef enum {
 	LTK_ACTIVATABLE_ALWAYS = 1|2,
 	LTK_GRABS_INPUT = 4,
 	LTK_NEEDS_REDRAW = 8,
-	LTK_NEEDS_SURFACE = 16, /* FIXME: let widgets handle this themselves */
-	LTK_HOVER_IS_ACTIVE = 32,
+	LTK_HOVER_IS_ACTIVE = 16,
 } ltk_widget_flags;
 
 typedef enum {
@@ -49,47 +49,44 @@ typedef enum {
 } ltk_orientation;
 
 typedef enum {
-	/* FIXME: maybe remove normal or set it to 0 */
-	LTK_NORMAL = 1,
-	LTK_HOVER = 2,
-	LTK_PRESSED = 4,
-	LTK_ACTIVE = 8,
-	LTK_HOVERACTIVE = 2 | 8,
-	LTK_DISABLED = 16,
+	LTK_NORMAL = 0,
+	LTK_HOVER = 1,
+	LTK_PRESSED = 2,
+	LTK_ACTIVE = 4,
+	LTK_HOVERACTIVE = 1 | 4,
+	LTK_DISABLED = 8,
 } ltk_widget_state;
 
-typedef enum {
-	/* for e.g. scrollbar, which can't be directly accessed by users */
-	LTK_UNKNOWN = 0,
-	LTK_GRID,
-	LTK_BUTTON,
-	LTK_DRAW,
-	LTK_LABEL,
-	LTK_WIDGET,
-	LTK_BOX,
-	LTK_MENU,
-	LTK_MENUENTRY,
-	LTK_NUM_WIDGETS
-} ltk_widget_type;
-
 #include "surface_cache.h"
 
 struct ltk_window;
 
 struct ltk_widget_vtable;
 
+typedef struct {
+	int client;      /* index of client */
+	uint32_t mask;   /* generic event mask */
+	uint32_t lmask;  /* generic lock mask */
+	uint32_t wmask;  /* event mask for specific widget type */
+	uint32_t lwmask; /* lock event mask for specific widget type */
+} client_event_mask;
+
 struct ltk_widget {
 	struct ltk_window *window;
 	struct ltk_widget *parent;
 	char *id;
 
-	ltk_surface_cache_key *surface_key;
 	struct ltk_widget_vtable *vtable;
 
 	ltk_rect rect;
 	unsigned int ideal_w;
 	unsigned int ideal_h;
 
+	client_event_mask *event_masks;
+	/* FIXME: kind of a waste of space to use size_t here */
+	size_t masks_num;
+	size_t masks_alloc;
+
 	ltk_widget_state state;
 	unsigned int sticky;
 	unsigned short row;
@@ -152,5 +149,18 @@ void ltk_remove_widget(const char *id);
 void ltk_widgets_cleanup();
 void ltk_widgets_init();
 void ltk_widget_resize(ltk_widget *widget);
+void ltk_widget_remove_client(int client);
+void ltk_widget_set_event_mask(ltk_widget *widget, int client, uint32_t mask);
+void ltk_widget_set_event_lmask(ltk_widget *widget, int client, uint32_t mask);
+void ltk_widget_set_event_wmask(ltk_widget *widget, int client, uint32_t mask);
+void ltk_widget_set_event_lwmask(ltk_widget *widget, int client, uint32_t mask);
+void ltk_widget_add_to_event_mask(ltk_widget *widget, int client, uint32_t mask);
+void ltk_widget_add_to_event_lmask(ltk_widget *widget, int client, uint32_t mask);
+void ltk_widget_add_to_event_wmask(ltk_widget *widget, int client, uint32_t mask);
+void ltk_widget_add_to_event_lwmask(ltk_widget *widget, int client, uint32_t mask);
+void ltk_widget_remove_from_event_mask(ltk_widget *widget, int client, uint32_t mask);
+void ltk_widget_remove_from_event_lmask(ltk_widget *widget, int client, uint32_t mask);
+void ltk_widget_remove_from_event_wmask(ltk_widget *widget, int client, uint32_t mask);
+void ltk_widget_remove_from_event_lwmask(ltk_widget *widget, int client, uint32_t mask);
 
 #endif /* LTK_WIDGET_H */
diff --git a/test.gui b/test.gui
@@ -19,3 +19,4 @@ button btn6 create "2 I'm another boring button."
 box box2 add btn4 ew
 box box2 add btn5 e
 box box2 add btn6
+mask-add btn1 button press
diff --git a/test.sh b/test.sh
@@ -16,7 +16,7 @@ fi
 cat test.gui | ./src/ltkc $ltk_id | while read cmd
 do
 	case "$cmd" in
-	"btn1 button_click")
+	"event btn1 button press")
 		echo "quit"
 		;;
 	*)
diff --git a/testbox.sh b/testbox.sh
@@ -7,18 +7,19 @@ if [ $? -ne 0 ]; then
 	exit 1
 fi
 
-cmds="box box1 create vertical\nset-root-widget box1\nbutton exit_btn create \"Exit\"\nbox box1 add exit_btn
+cmds="box box1 create vertical\nset-root-widget box1\nbutton exit_btn create \"Exit\"\nmask-add exit_btn button press\nbox box1 add exit_btn
 $(curl -s gopher://lumidify.org | awk -F'\t' '
 BEGIN {btn = 0; lbl = 0;}
 /^i/ { printf "label lbl%s create \"%s\"\nbox box1 add lbl%s w\n", lbl, substr($1, 2), lbl; lbl++ }
-/^[1gI]/ { printf "button btn%s create \"%s\"\nbox box1 add btn%s w\n", btn, substr($1, 2), btn; btn++ }')"
+/^[1gI]/ { printf "button btn%s create \"%s\"\nbox box1 add btn%s w\n", btn, substr($1, 2), btn; btn++ }')
+mask-add btn0 button press"
 echo "$cmds" | ./src/ltkc $ltk_id | while read cmd
 do
 	case "$cmd" in
-	"exit_btn button_click")
+	"event exit_btn button press")
 		echo "quit"
 		;;
-	"btn0 button_click")
+	"event btn0 button press")
 		echo "button bla create \"safhaskfldshk\"\nbox box1 add bla w"
 		;;
 	*)