commit fcbc1ce6f1df72e293da709c8f5a29e30a1ac6ba
parent a69310e2abfdd32a215f15785b9c12cb47cbc32c
Author: lumidify <nobody@lumidify.org>
Date:   Fri,  3 May 2024 19:35:52 +0200
Add basic checkbutton
Diffstat:
10 files changed, 342 insertions(+), 24 deletions(-)
diff --git a/Makefile b/Makefile
@@ -57,6 +57,7 @@ OBJ_LTK = \
 	src/ltk/widget.o \
 	src/ltk/ltk.o \
 	src/ltk/button.o \
+	src/ltk/checkbutton.o \
 	src/ltk/graphics_xlib.o \
 	src/ltk/surface_cache.o \
 	src/ltk/event_xlib.o \
@@ -94,6 +95,7 @@ OBJ_TEST = examples/ltk/test.o
 # currently so short that I don't really care.
 HDR_LTK = \
 	src/ltk/button.h \
+	src/ltk/checkbutton.h \
 	src/ltk/color.h \
 	src/ltk/label.h \
 	src/ltk/rect.h \
diff --git a/examples/ltk/test.c b/examples/ltk/test.c
@@ -9,6 +9,7 @@
 #include <ltk/entry.h>
 #include <ltk/menu.h>
 #include <ltk/box.h>
+#include <ltk/checkbutton.h>
 
 int
 quit(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
@@ -81,6 +82,11 @@ main(int argc, char *argv[]) {
 	ltk_box_add(box, LTK_CAST_WIDGET(btn4), LTK_STICKY_LEFT);
 	ltk_box_add(box, LTK_CAST_WIDGET(btn5), LTK_STICKY_LEFT);
 
+	ltk_checkbutton *cbtn1 = ltk_checkbutton_create(window, "Checkbutton1", 0);
+	ltk_checkbutton *cbtn2 = ltk_checkbutton_create(window, "Checkbutton2", 1);
+	ltk_box_add(box, LTK_CAST_WIDGET(cbtn1), LTK_STICKY_LEFT);
+	ltk_box_add(box, LTK_CAST_WIDGET(cbtn2), LTK_STICKY_LEFT);
+
 	ltk_grid_add(grid, LTK_CAST_WIDGET(menu), 0, 0, 1, 2, LTK_STICKY_LEFT|LTK_STICKY_RIGHT);
 	ltk_grid_add(grid, LTK_CAST_WIDGET(button), 1, 0, 1, 1, LTK_STICKY_LEFT);
 	ltk_grid_add(grid, LTK_CAST_WIDGET(button1), 1, 1, 1, 1, LTK_STICKY_RIGHT);
diff --git a/src/ltk/button.c b/src/ltk/button.c
@@ -14,9 +14,9 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include <stdint.h>
 #include <stdio.h>
 
+#include "config.h"
 #include "button.h"
 #include "color.h"
 #include "graphics.h"
diff --git a/src/ltk/button.h b/src/ltk/button.h
@@ -17,7 +17,6 @@
 #ifndef LTK_BUTTON_H
 #define LTK_BUTTON_H
 
-#include "graphics.h"
 #include "text.h"
 #include "widget.h"
 #include "window.h"
diff --git a/src/ltk/checkbutton.c b/src/ltk/checkbutton.c
@@ -0,0 +1,270 @@
+/*
+ * Copyright (c) 2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdio.h>
+
+#include "config.h"
+#include "checkbutton.h"
+#include "color.h"
+#include "graphics.h"
+#include "ltk.h"
+#include "memory.h"
+#include "rect.h"
+#include "text.h"
+#include "util.h"
+#include "widget.h"
+
+#define MAX_CHECKBUTTON_BORDER_WIDTH 10000
+#define MAX_CHECKBUTTON_PADDING 50000
+#define MAX_CHECKBUTTON_BOX_SIZE 50000
+
+static void ltk_checkbutton_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip);
+static int ltk_checkbutton_release(ltk_widget *self);
+static void ltk_checkbutton_destroy(ltk_widget *self, int shallow);
+static void ltk_checkbutton_recalc_ideal_size(ltk_widget *self);
+
+static struct ltk_widget_vtable vtable = {
+	.key_press = NULL,
+	.key_release = NULL,
+	.mouse_press = NULL,
+	.mouse_release = NULL,
+	.release = <k_checkbutton_release,
+	.motion_notify = NULL,
+	.mouse_leave = NULL,
+	.mouse_enter = NULL,
+	.change_state = NULL,
+	.get_child_at_pos = NULL,
+	.resize = NULL,
+	.hide = NULL,
+	.draw = <k_checkbutton_draw,
+	.destroy = <k_checkbutton_destroy,
+	.child_size_change = NULL,
+	.remove_child = NULL,
+	.recalc_ideal_size = <k_checkbutton_recalc_ideal_size,
+	.type = LTK_WIDGET_CHECKBUTTON,
+	.flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS,
+	.invalid_signal = LTK_CHECKBUTTON_SIGNAL_INVALID,
+};
+
+static struct {
+	ltk_color *text_color;
+
+	ltk_color *fill;
+	ltk_color *fill_pressed;
+	ltk_color *fill_hover;
+	ltk_color *fill_active;
+	ltk_color *fill_disabled;
+
+	ltk_color *box_fill;
+	ltk_color *box_border;
+
+	ltk_color *box_fill_pressed;
+	ltk_color *box_border_pressed;
+
+	ltk_color *box_fill_hover;
+	ltk_color *box_border_hover;
+
+	ltk_color *box_fill_active;
+	ltk_color *box_border_active;
+
+	ltk_color *box_fill_disabled;
+	ltk_color *box_border_disabled;
+
+	ltk_color *box_fill_checked;
+	ltk_color *box_border_checked;
+
+	ltk_color *box_fill_pressed_checked;
+	ltk_color *box_border_pressed_checked;
+
+	ltk_color *box_fill_hover_checked;
+	ltk_color *box_border_hover_checked;
+
+	ltk_color *box_fill_active_checked;
+	ltk_color *box_border_active_checked;
+
+	ltk_color *box_fill_disabled_checked;
+	ltk_color *box_border_disabled_checked;
+
+	char *font;
+	ltk_size box_size;
+	ltk_size box_border_width;
+	ltk_size pad;
+	ltk_size font_size;
+} theme;
+
+static ltk_theme_parseinfo parseinfo[] = {
+	{"fill", THEME_COLOR, {.color = &theme.fill}, {.color = "#000000"}, 0, 0, 0},
+	{"fill-hover", THEME_COLOR, {.color = &theme.fill_hover}, {.color = "#222222"}, 0, 0, 0},
+	{"fill-active", THEME_COLOR, {.color = &theme.fill_active}, {.color = "#222222"}, 0, 0, 0},
+	{"fill-disabled", THEME_COLOR, {.color = &theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0},
+	{"fill-pressed", THEME_COLOR, {.color = &theme.fill_pressed}, {.color = "#222222"}, 0, 0, 0},
+
+	{"box-fill", THEME_COLOR, {.color = &theme.box_fill}, {.color = "#000000"}, 0, 0, 0},
+	{"box-fill-hover", THEME_COLOR, {.color = &theme.box_fill_hover}, {.color = "#222222"}, 0, 0, 0},
+	{"box-fill-active", THEME_COLOR, {.color = &theme.box_fill_active}, {.color = "#222222"}, 0, 0, 0},
+	{"box-fill-disabled", THEME_COLOR, {.color = &theme.box_fill_disabled}, {.color = "#292929"}, 0, 0, 0},
+	{"box-fill-pressed", THEME_COLOR, {.color = &theme.box_fill_pressed}, {.color = "#222222"}, 0, 0, 0},
+	{"box-border", THEME_COLOR, {.color = &theme.box_border}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"box-border-hover", THEME_COLOR, {.color = &theme.box_border_hover}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"box-border-active", THEME_COLOR, {.color = &theme.box_border_active}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"box-border-disabled", THEME_COLOR, {.color = &theme.box_border_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"box-border-pressed", THEME_COLOR, {.color = &theme.box_border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0},
+
+	{"box-fill-checked", THEME_COLOR, {.color = &theme.box_fill_checked}, {.color = "#113355"}, 0, 0, 0},
+	{"box-fill-hover-checked", THEME_COLOR, {.color = &theme.box_fill_hover_checked}, {.color = "#738194"}, 0, 0, 0},
+	{"box-fill-active-checked", THEME_COLOR, {.color = &theme.box_fill_active_checked}, {.color = "#113355"}, 0, 0, 0},
+	{"box-fill-disabled-checked", THEME_COLOR, {.color = &theme.box_fill_disabled_checked}, {.color = "#292929"}, 0, 0, 0},
+	{"box-fill-pressed-checked", THEME_COLOR, {.color = &theme.box_fill_pressed_checked}, {.color = "#113355"}, 0, 0, 0},
+	{"box-border-checked", THEME_COLOR, {.color = &theme.box_border_checked}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"box-border-hover-checked", THEME_COLOR, {.color = &theme.box_border_hover_checked}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"box-border-active-checked", THEME_COLOR, {.color = &theme.box_border_active_checked}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"box-border-disabled-checked", THEME_COLOR, {.color = &theme.box_border_disabled_checked}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"box-border-pressed-checked", THEME_COLOR, {.color = &theme.box_border_pressed_checked}, {.color = "#FFFFFF"}, 0, 0, 0},
+
+	{"box-size", THEME_SIZE, {.size = &theme.box_size}, {.size = {.val = 500, .unit = LTK_UNIT_MM}}, 0, MAX_CHECKBUTTON_BOX_SIZE, 0},
+	{"box-border-width", THEME_SIZE, {.size = &theme.box_border_width}, {.size = {.val = 25, .unit = LTK_UNIT_MM}}, 0, MAX_CHECKBUTTON_BORDER_WIDTH, 0},
+	{"pad", THEME_SIZE, {.size = &theme.pad}, {.size = {.val = 100, .unit = LTK_UNIT_MM}}, 0, MAX_CHECKBUTTON_PADDING, 0},
+	{"text-color", THEME_COLOR, {.color = &theme.text_color}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"font", THEME_STRING, {.str = &theme.font}, {.str = "Monospace"}, 0, 0, 0},
+	{"font-size", THEME_SIZE, {.size = &theme.font_size}, {.size = {.val = 1200, .unit = LTK_UNIT_PT}}, 0, 20000, 0},
+};
+
+void
+ltk_checkbutton_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) {
+	*p = parseinfo;
+	*len = LENGTH(parseinfo);
+}
+
+/* FIXME: a lot more theme settings */
+static void
+ltk_checkbutton_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
+	ltk_checkbutton *button = LTK_CAST_CHECKBUTTON(self);
+	ltk_rect lrect = self->lrect;
+	ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h});
+	if (clip_final.w <= 0 || clip_final.h <= 0)
+		return;
+
+	int box_size = ltk_size_to_pixel(theme.box_size, self->last_dpi);
+	int box_bw = ltk_size_to_pixel(theme.box_border_width, self->last_dpi);
+	int pad = ltk_size_to_pixel(theme.pad, self->last_dpi);
+	ltk_color *fill = NULL, *box_border = NULL, *box_fill = NULL;
+	if (self->state & LTK_DISABLED) {
+		fill = theme.fill_disabled;
+		box_border = button->checked ? theme.box_border_disabled_checked : theme.box_border_disabled;
+		box_fill = button->checked ? theme.box_fill_disabled_checked : theme.box_fill_disabled;
+	} else if (self->state & LTK_PRESSED) {
+		fill = theme.fill_pressed;
+		box_border = button->checked ? theme.box_border_pressed_checked : theme.box_border_pressed;
+		box_fill = button->checked ? theme.box_fill_pressed_checked : theme.box_fill_pressed;
+	} else if (self->state & LTK_HOVER) {
+		fill = theme.fill_hover;
+		box_border = button->checked ? theme.box_border_hover_checked : theme.box_border_hover;
+		box_fill = button->checked ? theme.box_fill_hover_checked : theme.box_fill_hover;
+	} else if (self->state & LTK_ACTIVE) {
+		fill = theme.fill_active;
+		box_border = button->checked ? theme.box_border_active_checked : theme.box_border_active;
+		box_fill = button->checked ? theme.box_fill_active_checked : theme.box_fill_active;
+	} else {
+		fill = theme.fill;
+		box_border = button->checked ? theme.box_border_checked : theme.box_border;
+		box_fill = button->checked ? theme.box_fill_checked : theme.box_fill;
+	}
+	ltk_rect box_rect = {x + pad, y + pad, box_size, box_size};
+	ltk_rect draw_clip = {x + clip_final.x, y + clip_final.y, clip_final.w, clip_final.h};
+	ltk_rect box_clip = ltk_rect_intersect(box_rect, draw_clip);
+	ltk_surface_fill_rect(draw_surf, fill, draw_clip);
+	ltk_surface_fill_rect(draw_surf, box_fill, box_clip);
+	if (box_bw > 0) {
+		ltk_surface_draw_border_clipped(
+			draw_surf, box_border, box_rect, box_clip, box_bw, LTK_BORDER_ALL
+		);
+	}
+	int text_w, text_h;
+	ltk_text_line_get_size(button->tl, &text_w, &text_h);
+	int text_x = x + 2 * pad + box_size;
+	int text_y = y + (lrect.h - text_h) / 2;
+	ltk_text_line_draw_clipped(button->tl, draw_surf, theme.text_color, text_x, text_y, draw_clip);
+	/* FIXME: only redraw if dirty (needs to be handled higher-up to only
+	   call draw when dirty or window rect invalidated */
+	self->dirty = 0;
+}
+
+static int
+ltk_checkbutton_release(ltk_widget *self) {
+	ltk_checkbutton *button = LTK_CAST_CHECKBUTTON(self);
+	button->checked = !button->checked;
+	ltk_widget_emit_signal(self, LTK_CHECKBUTTON_SIGNAL_CHANGED, LTK_EMPTY_ARGLIST);
+	return 1;
+}
+
+int
+ltk_checkbutton_get_checked(ltk_checkbutton *button) {
+	return button->checked;
+}
+
+void
+ltk_checkbutton_set_checked(ltk_checkbutton *button, int checked) {
+	button->checked = checked;
+	ltk_widget *self = LTK_CAST_WIDGET(button);
+	ltk_window_invalidate_widget_rect(self->window, self);
+}
+
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+
+static void
+recalc_ideal_size(ltk_checkbutton *button) {
+	int text_w, text_h;
+	ltk_text_line_get_size(button->tl, &text_w, &text_h);
+	int box_size = ltk_size_to_pixel(theme.box_size, LTK_CAST_WIDGET(button)->last_dpi);
+	int pad = ltk_size_to_pixel(theme.pad, LTK_CAST_WIDGET(button)->last_dpi);
+	button->widget.ideal_w = text_w + pad * 3 + box_size;
+	button->widget.ideal_h = MAX(text_h, box_size) + pad * 2;
+}
+
+static void
+ltk_checkbutton_recalc_ideal_size(ltk_widget *self) {
+	ltk_checkbutton *button = LTK_CAST_CHECKBUTTON(self);
+	int font_size = ltk_size_to_pixel(theme.font_size, self->last_dpi);
+	ltk_text_line_set_font_size(button->tl, font_size);
+	recalc_ideal_size(button);
+}
+
+ltk_checkbutton *
+ltk_checkbutton_create(ltk_window *window, const char *text, int checked) {
+	ltk_checkbutton *button = ltk_malloc(sizeof(ltk_checkbutton));
+	ltk_fill_widget_defaults(LTK_CAST_WIDGET(button), window, &vtable, 0, 0);
+	button->checked = checked;
+
+	button->tl = ltk_text_line_create_const_text_default(
+		theme.font, ltk_size_to_pixel(theme.font_size, button->widget.last_dpi), text, -1
+	);
+	recalc_ideal_size(button);
+	button->widget.dirty = 1;
+
+	return button;
+}
+
+static void
+ltk_checkbutton_destroy(ltk_widget *self, int shallow) {
+	(void)shallow;
+	ltk_checkbutton *button = LTK_CAST_CHECKBUTTON(self);
+	if (!button) {
+		ltk_warn("Tried to destroy NULL checkbutton.\n");
+		return;
+	}
+	ltk_text_line_destroy(button->tl);
+	ltk_free(button);
+}
diff --git a/src/ltk/checkbutton.h b/src/ltk/checkbutton.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LTK_CHECKBUTTON_H
+#define LTK_CHECKBUTTON_H
+
+#include "text.h"
+#include "widget.h"
+#include "window.h"
+
+#define LTK_CHECKBUTTON_SIGNAL_CHANGED -1
+#define LTK_CHECKBUTTON_SIGNAL_INVALID -2
+
+typedef struct {
+	ltk_widget widget;
+	ltk_text_line *tl;
+	int checked;
+} ltk_checkbutton;
+
+ltk_checkbutton *ltk_checkbutton_create(ltk_window *window, const char *text, int checked);
+int ltk_checkbutton_get_checked(ltk_checkbutton *button);
+void ltk_checkbutton_set_checked(ltk_checkbutton *button, int checked);
+
+#endif /* LTK_CHECKBUTTON_H */
diff --git a/src/ltk/config.c b/src/ltk/config.c
@@ -84,6 +84,7 @@ static struct theme_handlerinfo {
 	{"theme:menuentry", <k_menuentry_get_theme_parseinfo, "theme:window", 0},
 	{"theme:submenu", <k_submenu_get_theme_parseinfo, "theme:window", 0},
 	{"theme:submenuentry", <k_submenuentry_get_theme_parseinfo, "theme:window", 0},
+	{"theme:checkbutton", <k_checkbutton_get_theme_parseinfo, "theme:window", 0},
 };
 
 GEN_SORT_SEARCH_HELPERS(themehandler, struct theme_handlerinfo, name)
diff --git a/src/ltk/event_xlib.c b/src/ltk/event_xlib.c
@@ -335,12 +335,12 @@ next_event_base(ltk_renderdata *renderdata, ltk_renderwindow **windows, size_t n
 		button = xevent.xbutton.button;
 		/* FIXME: are the buttons really always defined as exactly these values? */
 		if (button >= 1 && button <= 3) {
-			if (xevent.xbutton.time - last_button_press[button] <= DOUBLECLICK_TIME &&
-			    DISTSQ(press_pos[button].x, press_pos[button].y, xevent.xbutton.x, xevent.xbutton.y) <= DOUBLECLICK_DISTSQ) {
-				if (was_2press[button]) {
+			if (xevent.xbutton.time - last_button_press[button - 1] <= DOUBLECLICK_TIME &&
+			    DISTSQ(press_pos[button - 1].x, press_pos[button - 1].y, xevent.xbutton.x, xevent.xbutton.y) <= DOUBLECLICK_DISTSQ) {
+				if (was_2press[button - 1]) {
 					/* reset so normal press is sent again next time */
-					was_2press[button] = 0;
-					last_button_press[button] = 0;
+					was_2press[button - 1] = 0;
+					last_button_press[button - 1] = 0;
 					ltk_array_append_event(local_event_stack, (ltk_event){.button = {
 						.type = LTK_3BUTTONPRESS_EVENT,
 						.window_id = window_id,
@@ -349,8 +349,8 @@ next_event_base(ltk_renderdata *renderdata, ltk_renderwindow **windows, size_t n
 						.y = xevent.xbutton.y
 					}});
 				} else {
-					was_2press[button] = 1;
-					last_button_press[button] = xevent.xbutton.time;
+					was_2press[button - 1] = 1;
+					last_button_press[button - 1] = xevent.xbutton.time;
 					ltk_array_append_event(local_event_stack, (ltk_event){.button = {
 						.type = LTK_2BUTTONPRESS_EVENT,
 						.window_id = window_id,
@@ -360,8 +360,8 @@ next_event_base(ltk_renderdata *renderdata, ltk_renderwindow **windows, size_t n
 					}});
 				}
 			} else {
-				last_button_press[button] = xevent.xbutton.time;
-				was_2press[button] = 0;
+				last_button_press[button - 1] = xevent.xbutton.time;
+				was_2press[button - 1] = 0;
 			}
 			*event = (ltk_event){.button = {
 				.type = LTK_BUTTONPRESS_EVENT,
@@ -370,8 +370,8 @@ next_event_base(ltk_renderdata *renderdata, ltk_renderwindow **windows, size_t n
 				.x = xevent.xbutton.x,
 				.y = xevent.xbutton.y
 			}};
-			press_pos[button].x = xevent.xbutton.x;
-			press_pos[button].y = xevent.xbutton.y;
+			press_pos[button - 1].x = xevent.xbutton.x;
+			press_pos[button - 1].y = xevent.xbutton.y;
 		} else if (button >= 4 && button <= 7) {
 			/* FIXME: compress multiple scroll events into one */
 			*event = (ltk_event){.scroll = {
@@ -404,12 +404,12 @@ next_event_base(ltk_renderdata *renderdata, ltk_renderwindow **windows, size_t n
 		ltk_assert(window_id < num_windows);
 		button = xevent.xbutton.button;
 		if (button >= 1 && button <= 3) {
-			if (xevent.xbutton.time - last_button_release[button] <= DOUBLECLICK_TIME &&
-			    DISTSQ(release_pos[button].x, release_pos[button].y, xevent.xbutton.x, xevent.xbutton.y) <= DOUBLECLICK_DISTSQ) {
-				if (was_2release[button]) {
+			if (xevent.xbutton.time - last_button_release[button - 1] <= DOUBLECLICK_TIME &&
+			    DISTSQ(release_pos[button - 1].x, release_pos[button - 1].y, xevent.xbutton.x, xevent.xbutton.y) <= DOUBLECLICK_DISTSQ) {
+				if (was_2release[button - 1]) {
 					/* reset so normal release is sent again next time */
-					was_2release[button] = 0;
-					last_button_release[button] = 0;
+					was_2release[button - 1] = 0;
+					last_button_release[button - 1] = 0;
 					ltk_array_append_event(local_event_stack, (ltk_event){.button = {
 						.type = LTK_3BUTTONRELEASE_EVENT,
 						.window_id = window_id,
@@ -418,8 +418,8 @@ next_event_base(ltk_renderdata *renderdata, ltk_renderwindow **windows, size_t n
 						.y = xevent.xbutton.y
 					}});
 				} else {
-					was_2release[button] = 1;
-					last_button_release[button] = xevent.xbutton.time;
+					was_2release[button - 1] = 1;
+					last_button_release[button - 1] = xevent.xbutton.time;
 					ltk_array_append_event(local_event_stack, (ltk_event){.button = {
 						.type = LTK_2BUTTONRELEASE_EVENT,
 						.window_id = window_id,
@@ -429,8 +429,8 @@ next_event_base(ltk_renderdata *renderdata, ltk_renderwindow **windows, size_t n
 					}});
 				}
 			} else {
-				last_button_release[button] = xevent.xbutton.time;
-				was_2release[button] = 0;
+				last_button_release[button - 1] = xevent.xbutton.time;
+				was_2release[button - 1] = 0;
 			}
 			*event = (ltk_event){.button = {
 				.type = LTK_BUTTONRELEASE_EVENT,
@@ -439,8 +439,8 @@ next_event_base(ltk_renderdata *renderdata, ltk_renderwindow **windows, size_t n
 				.x = xevent.xbutton.x,
 				.y = xevent.xbutton.y
 			}};
-			release_pos[button].x = xevent.xbutton.x;
-			release_pos[button].y = xevent.xbutton.y;
+			release_pos[button - 1].x = xevent.xbutton.x;
+			release_pos[button - 1].y = xevent.xbutton.y;
 		} else {
 			return 2;
 		}
diff --git a/src/ltk/widget.h b/src/ltk/widget.h
@@ -45,6 +45,7 @@ typedef enum {
 	LTK_WIDGET_IMAGE,
 	LTK_WIDGET_WINDOW,
 	LTK_WIDGET_SCROLLBAR,
+	LTK_WIDGET_CHECKBUTTON,
 	LTK_NUM_WIDGETS,
 } ltk_widget_type;
 
@@ -184,6 +185,7 @@ typedef struct {
 #define LTK_CAST_MENUENTRY(w) (ltk_assert(w->vtable->type == LTK_WIDGET_MENUENTRY), (ltk_menuentry *)(w))
 #define LTK_CAST_SCROLLBAR(w) (ltk_assert(w->vtable->type == LTK_WIDGET_SCROLLBAR), (ltk_scrollbar *)(w))
 #define LTK_CAST_BOX(w) (ltk_assert(w->vtable->type == LTK_WIDGET_BOX), (ltk_box *)(w))
+#define LTK_CAST_CHECKBUTTON(w) (ltk_assert(w->vtable->type == LTK_WIDGET_CHECKBUTTON), (ltk_checkbutton *)(w))
 
 /* FIXME: a bit weird because window never gets some of these signals */
 #define LTK_WIDGET_SIGNAL_KEY_PRESS          1
diff --git a/src/ltk/widget_internal.h b/src/ltk/widget_internal.h
@@ -26,6 +26,7 @@
 
 void ltk_window_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
 void ltk_button_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
+void ltk_checkbutton_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
 void ltk_label_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
 void ltk_menu_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
 void ltk_menuentry_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);