commit d3b49ae1320664eeb8629e6c50be99642dc7f25e
parent 33cf30d1cfde0826ff45ce7b876ca2c1d8dbc9b9
Author: lumidify <nobody@lumidify.org>
Date: Sun, 22 May 2022 17:12:52 +0200
Add menus
Yes, I know a lot of other things were also changed.
Diffstat:
30 files changed, 2518 insertions(+), 234 deletions(-)
diff --git a/Makefile b/Makefile
@@ -4,34 +4,36 @@
NAME = ltk
VERSION = -999-prealpha0
+# Note: The stb backend should not be used with untrusted font files.
# FIXME: Using DEBUG here doesn't work because it somehow
# interferes with a predefined macro, at least on OpenBSD.
-DEV = 1
+DEV = 0
USE_PANGO = 0
-# FIXME: When using _POSIX_C_SOURCE on OpenBSD, strtonum isn't defined anymore -
-# should strtonum just only be used from the local copy?
-
-CFLAGS += -DUSE_PANGO=$(USE_PANGO) -DDEV=$(DEV) -Wall -Wextra -std=c99 `pkg-config --cflags x11 fontconfig xext` -D_POSIX_C_SOURCE=200809L
-LDFLAGS += -lm `pkg-config --libs x11 fontconfig xext`
-
# Note: this macro magic for debugging and pango rendering seems ugly; it should probably be changed
# debug
-DEV_1 = -g -Wall -Wextra -pedantic
-#-Werror
+DEV_CFLAGS_1 = -fsanitize=address -g -Wall -Wextra -pedantic
+DEV_LDFLAGS_1 = -fsanitize=address
+# don't include default flags when debugging so possible
+# optimization flags don't interfere with it
+DEV_CFLAGS_0 = $(CFLAGS)
+DEV_LDFLAGS_0 = $(LDFLAGS)
# stb rendering
EXTRA_OBJ_0 = src/stb_truetype.o src/text_stb.o
# pango rendering
EXTRA_OBJ_1 = src/text_pango.o
-EXTRA_CFLAGS_1 += -DUSE_PANGO `pkg-config --cflags pangoxft`
-EXTRA_LDFLAGS_1 += `pkg-config --libs pangoxft`
+EXTRA_CFLAGS_1 = `pkg-config --cflags pangoxft`
+EXTRA_LDFLAGS_1 = `pkg-config --libs pangoxft`
EXTRA_OBJ = $(EXTRA_OBJ_$(USE_PANGO))
-EXTRA_CFLAGS = $(EXTRA_CFLAGS_$(USE_PANGO)) $(DEV_$(DEV))
-EXTRA_LDFLAGS = $(EXTRA_LDFLAGS_$(USE_PANGO))
+EXTRA_CFLAGS = $(DEV_CFLAGS_$(DEV)) $(EXTRA_CFLAGS_$(USE_PANGO))
+EXTRA_LDFLAGS = $(DEV_LDFLAGS_$(DEV)) $(EXTRA_LDFLAGS_$(USE_PANGO))
+
+LTK_CFLAGS = $(EXTRA_CFLAGS) -DUSE_PANGO=$(USE_PANGO) -DDEV=$(DEV) -std=c99 `pkg-config --cflags x11 fontconfig xext` -D_POSIX_C_SOURCE=200809L
+LTK_LDFLAGS = $(EXTRA_LDFLAGS) -lm `pkg-config --libs x11 fontconfig xext`
OBJ = \
src/strtonum.o \
@@ -47,6 +49,7 @@ OBJ = \
src/scrollbar.o \
src/button.o \
src/label.o \
+ src/menu.o \
src/graphics_xlib.o \
src/surface_cache.o \
$(EXTRA_OBJ)
@@ -71,25 +74,24 @@ HDR = \
src/stb_truetype.h \
src/text.h \
src/util.h \
+ src/menu.h \
src/graphics.h \
- src/surface_cache.h
+ src/surface_cache.h \
+ src/macros.h
# src/draw.h \
-CFLAGS += $(EXTRA_CFLAGS)
-LDFLAGS += $(EXTRA_LDFLAGS)
-
all: src/ltkd src/ltkc
src/ltkd: $(OBJ)
- $(CC) -o $@ $(OBJ) $(LDFLAGS)
+ $(CC) -o $@ $(OBJ) $(LTK_LDFLAGS)
src/ltkc: src/ltkc.o src/util.o src/memory.o
- $(CC) -o $@ src/ltkc.o src/util.o src/memory.o
+ $(CC) -o $@ src/ltkc.o src/util.o src/memory.o $(LTK_LDFLAGS)
$(OBJ) : $(HDR)
.c.o:
- $(CC) -c -o $@ $< $(CFLAGS)
+ $(CC) -c -o $@ $< $(LTK_CFLAGS)
.PHONY: clean
diff --git a/README.md b/README.md
@@ -5,8 +5,7 @@ WILDEST FANTASIES, NOT ACTUAL WORKING CODE.
To build with or without pango: Follow instructions in config.mk.
-Note: The basic (non-pango) text doesn't work properly on my i386 machine
-because it's a bit of a hack.
+Note: The basic (non-pango) text doesn't work properly on all systems.
To test:
@@ -16,5 +15,8 @@ make
If you click the top button, it should exit. That's all it does now.
Also read the comment in './test.sh'.
-New:
./testbox.sh shows my gopherhole, but most buttons don't actually do anything.
+./test2.sh shows an example with menus.
+
+Note: I know the default theme is butt-ugly at the moment. It is mainly
+to test things, not to look pretty.
diff --git a/src/box.c b/src/box.c
@@ -117,15 +117,21 @@ static void
ltk_box_destroy(ltk_widget *self, int shallow) {
ltk_box *box = (ltk_box *)self;
ltk_widget *ptr;
- if (!shallow) {
- for (size_t i = 0; i < box->num_widgets; i++) {
- ptr = box->widgets[i];
+ char *errstr;
+ if (self->parent && self->parent->vtable->remove_child) {
+ self->parent->vtable->remove_child(
+ self->window, self, self->parent, &errstr
+ );
+ }
+ for (size_t i = 0; i < box->num_widgets; i++) {
+ ptr = box->widgets[i];
+ ptr->parent = NULL;
+ if (!shallow)
ptr->vtable->destroy(ptr, shallow);
- }
}
ltk_free(box->widgets);
- ltk_remove_widget(box->widget.id);
- ltk_free(box->widget.id);
+ ltk_remove_widget(self->id);
+ ltk_free(self->id);
box->sc->widget.vtable->destroy((ltk_widget *)box->sc, 0);
ltk_free(box);
}
@@ -341,6 +347,7 @@ ltk_box_mouse_press(ltk_widget *self, XEvent event) {
default_handler = widget->vtable->mouse_press(widget, event);
}
}
+ /* FIXME: configure scrollstep */
if (default_handler) {
int delta = event.xbutton.button == 4 ? -15 : 15;
ltk_scrollbar_scroll((ltk_widget *)box->sc, delta, 0);
diff --git a/src/button.c b/src/button.c
@@ -34,6 +34,9 @@
#include "graphics.h"
#include "surface_cache.h"
+#define MAX_BUTTON_BORDER_WIDTH 100
+#define MAX_BUTTON_PADDING 500
+
static void ltk_button_draw(ltk_widget *self, ltk_rect clip);
static int ltk_button_mouse_release(ltk_widget *self, XEvent event);
static ltk_button *ltk_button_create(ltk_window *window,
@@ -76,61 +79,57 @@ void
ltk_button_setup_theme_defaults(ltk_window *window) {
theme.border_width = 2;
theme.pad = 5;
- ltk_color_create(window->dpy, window->screen, window->cm,
- "#FFFFFF", &theme.text_color);
- ltk_color_create(window->dpy, window->screen, window->cm,
- "#339999", &theme.border);
- ltk_color_create(window->dpy, window->screen, window->cm,
- "#113355", &theme.fill);
- ltk_color_create(window->dpy, window->screen, window->cm,
- "#FFFFFF", &theme.border_pressed);
- ltk_color_create(window->dpy, window->screen, window->cm,
- "#113355", &theme.fill_pressed);
- ltk_color_create(window->dpy, window->screen, window->cm,
- "#FFFFFF", &theme.border_active);
- ltk_color_create(window->dpy, window->screen, window->cm,
- "#738194", &theme.fill_active);
- ltk_color_create(window->dpy, window->screen, window->cm,
- "#FFFFFF", &theme.border_disabled);
- ltk_color_create(window->dpy, window->screen, window->cm,
- "#292929", &theme.fill_disabled);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &theme.text_color);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#339999", &theme.border);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &theme.fill);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &theme.border_pressed);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &theme.fill_pressed);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &theme.border_active);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#738194", &theme.fill_active);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &theme.border_disabled);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#292929", &theme.fill_disabled);
}
void
ltk_button_ini_handler(ltk_window *window, const char *prop, const char *value) {
+ const char *errstr;
if (strcmp(prop, "border_width") == 0) {
- theme.border_width = atoi(value);
+ theme.border_width = ltk_strtonum(value, 0, MAX_BUTTON_BORDER_WIDTH, &errstr);
+ if (errstr)
+ ltk_warn("Invalid button border width '%s': %s.\n", value, errstr);
} else if (strcmp(prop, "pad") == 0) {
- theme.pad = atoi(value);
+ theme.pad = ltk_strtonum(value, 0, MAX_BUTTON_PADDING, &errstr);
+ if (errstr)
+ ltk_warn("Invalid button padding '%s': %s.\n", value, errstr);
} else if (strcmp(prop, "border") == 0) {
- ltk_color_create(window->dpy, window->screen, window->cm,
- value, &theme.border);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.border))
+ ltk_warn("Error setting button border color to '%s'.\n", value);
} else if (strcmp(prop, "fill") == 0) {
- ltk_color_create(window->dpy, window->screen, window->cm,
- value, &theme.fill);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.fill))
+ ltk_warn("Error setting button fill color to '%s'.\n", value);
} else if (strcmp(prop, "border_pressed") == 0) {
- ltk_color_create(window->dpy, window->screen, window->cm,
- value, &theme.border_pressed);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.border_pressed))
+ ltk_warn("Error setting button pressed border color to '%s'.\n", value);
} else if (strcmp(prop, "fill_pressed") == 0) {
- ltk_color_create(window->dpy, window->screen, window->cm,
- value, &theme.fill_pressed);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.fill_pressed))
+ ltk_warn("Error setting button pressed fill color to '%s'.\n", value);
} else if (strcmp(prop, "border_active") == 0) {
- ltk_color_create(window->dpy, window->screen, window->cm,
- value, &theme.border_active);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.border_active))
+ ltk_warn("Error setting button active border color to '%s'.\n", value);
} else if (strcmp(prop, "fill_active") == 0) {
- ltk_color_create(window->dpy, window->screen, window->cm,
- value, &theme.fill_active);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.fill_active))
+ ltk_warn("Error setting button active fill color to '%s'.\n", value);
} else if (strcmp(prop, "border_disabled") == 0) {
- ltk_color_create(window->dpy, window->screen, window->cm,
- value, &theme.border_disabled);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.border_disabled))
+ ltk_warn("Error setting button disabled border color to '%s'.\n", value);
} else if (strcmp(prop, "fill_disabled") == 0) {
- ltk_color_create(window->dpy, window->screen, window->cm,
- value, &theme.fill_disabled);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.fill_disabled))
+ ltk_warn("Error setting button disabled fill color to '%s'.\n", value);
} else if (strcmp(prop, "text_color") == 0) {
- ltk_color_create(window->dpy, window->screen, window->cm,
- value, &theme.text_color);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.text_color))
+ ltk_warn("Error setting button text color to '%s'.\n", value);
} else {
- ltk_warn("Unknown property \"%s\" for button style.\n", prop);
+ ltk_warn("Unknown property '%s' for button style.\n", prop);
}
}
@@ -192,7 +191,6 @@ ltk_button_change_state(ltk_widget *self) {
/* FIXME: only when pressed button was actually this one */
static int
ltk_button_mouse_release(ltk_widget *self, XEvent event) {
- (void)event;
ltk_button *button = (ltk_button *)self;
if (event.xbutton.button == 1) {
ltk_queue_event(button->widget.window, LTK_EVENT_BUTTON, button->widget.id, "button_click");
@@ -220,6 +218,12 @@ ltk_button_create(ltk_window *window, const char *id, char *text) {
static void
ltk_button_destroy(ltk_widget *self, int shallow) {
(void)shallow;
+ char *errstr;
+ if (self->parent && self->parent->vtable->remove_child) {
+ self->parent->vtable->remove_child(
+ self->window, self, self->parent, &errstr
+ );
+ }
ltk_button *button = (ltk_button *)self;
if (!button) {
ltk_warn("Tried to destroy NULL button.\n");
@@ -228,6 +232,7 @@ ltk_button_destroy(ltk_widget *self, int shallow) {
/* FIXME: this should be generic part of widget */
ltk_surface_cache_release_key(self->surface_key);
ltk_text_line_destroy(button->tl);
+ ltk_remove_widget(self->id);
ltk_remove_widget(button->widget.id);
ltk_free(button->widget.id);
ltk_free(button);
diff --git a/src/button.h b/src/button.h
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, 2017, 2018, 2020 lumidify <nobody@lumidify.org>
+ * Copyright (c) 2016, 2017, 2018, 2020, 2022 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
diff --git a/src/color.c b/src/color.c
@@ -20,18 +20,23 @@
#include "util.h"
#include "color.h"
+#include "compat.h"
-void
+/* FIXME: avoid initializing part of the struct and then error returning */
+/* FIXME: better error codes */
+/* FIXME: I think xcolor is unneeded when xft is enabled */
+int
ltk_color_create(Display *dpy, int screen, Colormap cm, const char *hex, ltk_color *col) {
- if (!XParseColor(dpy, cm, hex, &col->xcolor)) {
- /* FIXME: better error reporting!!! */
- ltk_fatal("ltk_color_create");
- }
- XAllocColor(dpy, cm, &col->xcolor);
- /* FIXME: replace with XftColorAllocValue; error checking */
- #if USE_PANGO == 1
- XftColorAllocName(dpy, DefaultVisual(dpy, screen), cm, hex, &col->xftcolor);
+ if (!XParseColor(dpy, cm, hex, &col->xcolor))
+ return 1;
+ if (!XAllocColor(dpy, cm, &col->xcolor))
+ return 1;
+ /* FIXME: replace with XftColorAllocValue */
+ #if USE_XFT == 1
+ if (!XftColorAllocName(dpy, DefaultVisual(dpy, screen), cm, hex, &col->xftcolor))
+ return 1;
#else
(void)screen;
#endif
+ return 0;
}
diff --git a/src/color.h b/src/color.h
@@ -30,6 +30,7 @@ typedef struct {
#endif
} ltk_color;
-void ltk_color_create(Display *dpy, int screen, Colormap cm, const char *hex, ltk_color *col);
+/* returns 1 on failure, 0 on success */
+int ltk_color_create(Display *dpy, int screen, Colormap cm, const char *hex, ltk_color *col);
#endif /* _LTK_COLOR_H_ */
diff --git a/src/compat.h b/src/compat.h
@@ -1,4 +1,4 @@
-#ifdef _LTK_COMPAT_H_
+#ifndef _LTK_COMPAT_H_
#define _LTK_COMPAT_H_
#if USE_PANGO == 1
diff --git a/src/draw.c b/src/draw.c
@@ -254,22 +254,22 @@ ltk_draw_cmd_line(
}
draw = (ltk_draw *)ltk_get_widget(tokens[1], LTK_DRAW, errstr);
if (!draw) return 1;
- x1 = strtonum(tokens[3], 0, 100000, &errstr_num);
+ x1 = ltk_strtonum(tokens[3], 0, 100000, &errstr_num);
if (errstr_num) {
*errstr = "Invalid x1.\n";
return 1;
}
- y1 = strtonum(tokens[4], 0, 100000, &errstr_num);
+ y1 = ltk_strtonum(tokens[4], 0, 100000, &errstr_num);
if (errstr_num) {
*errstr = "Invalid y1.\n";
return 1;
}
- x2 = strtonum(tokens[5], 0, 100000, &errstr_num);
+ x2 = ltk_strtonum(tokens[5], 0, 100000, &errstr_num);
if (errstr_num) {
*errstr = "Invalid x2.\n";
return 1;
}
- y2 = strtonum(tokens[6], 0, 100000, &errstr_num);
+ y2 = ltk_strtonum(tokens[6], 0, 100000, &errstr_num);
if (errstr_num) {
*errstr = "Invalid y2.\n";
return 1;
@@ -294,27 +294,27 @@ ltk_draw_cmd_rect(
}
draw = (ltk_draw *)ltk_get_widget(tokens[1], LTK_DRAW, errstr);
if (!draw) return 1;
- x = strtonum(tokens[3], 0, 100000, &errstr_num);
+ x = ltk_strtonum(tokens[3], 0, 100000, &errstr_num);
if (errstr_num) {
*errstr = "Invalid x.\n";
return 1;
}
- y = strtonum(tokens[4], 0, 100000, &errstr_num);
+ y = ltk_strtonum(tokens[4], 0, 100000, &errstr_num);
if (errstr_num) {
*errstr = "Invalid y.\n";
return 1;
}
- w = strtonum(tokens[5], 1, 100000, &errstr_num);
+ w = ltk_strtonum(tokens[5], 1, 100000, &errstr_num);
if (errstr_num) {
*errstr = "Invalid width.\n";
return 1;
}
- h = strtonum(tokens[6], 1, 100000, &errstr_num);
+ h = ltk_strtonum(tokens[6], 1, 100000, &errstr_num);
if (errstr_num) {
*errstr = "Invalid height.\n";
return 1;
}
- fill = strtonum(tokens[7], 0, 1, &errstr_num);
+ fill = ltk_strtonum(tokens[7], 0, 1, &errstr_num);
if (errstr_num) {
*errstr = "Invalid fill bool.\n";
return 1;
@@ -342,12 +342,12 @@ ltk_draw_cmd_create(
*errstr = "Widget ID already taken.\n";
return 1;
}
- w = strtonum(tokens[3], 1, 100000, &errstr_num);
+ w = ltk_strtonum(tokens[3], 1, 100000, &errstr_num);
if (errstr_num) {
*errstr = "Invalid width.\n";
return 1;
}
- h = strtonum(tokens[4], 1, 100000, &errstr_num);
+ h = ltk_strtonum(tokens[4], 1, 100000, &errstr_num);
if (errstr_num) {
*errstr = "Invalid height.\n";
return 1;
diff --git a/src/graphics.h b/src/graphics.h
@@ -28,6 +28,20 @@
#include "ltk.h"
#include "compat.h"
+typedef enum {
+ LTK_BORDER_NONE = 0,
+ LTK_BORDER_TOP = 1,
+ LTK_BORDER_RIGHT = 2,
+ LTK_BORDER_BOTTOM = 4,
+ LTK_BORDER_LEFT = 8,
+ LTK_BORDER_ALL = 0xF
+} ltk_border_sides;
+
+/* FIXME: X only supports 16-bit numbers */
+typedef struct {
+ int x, y;
+} ltk_point;
+
/* typedef struct ltk_surface ltk_surface; */
/* FIXME: graphics context */
@@ -42,6 +56,9 @@ void ltk_surface_get_size(ltk_surface *s, int *w, int *h);
void ltk_surface_copy(ltk_surface *src, ltk_surface *dst, ltk_rect src_rect, int dst_x, int dst_y);
void ltk_surface_draw_rect(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width);
void ltk_surface_fill_rect(ltk_surface *s, ltk_color *c, ltk_rect rect);
+/* FIXME: document properly, especiall difference to draw_rect with offsets and line_width */
+void ltk_surface_draw_border(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width, ltk_border_sides border_sides);
+void ltk_surface_fill_polygon(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints);
/* TODO */
/*
diff --git a/src/graphics_xlib.c b/src/graphics_xlib.c
@@ -107,22 +107,46 @@ ltk_surface_draw_rect(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_widt
}
void
-ltk_surface_fill_rect(ltk_surface *s, ltk_color *c, ltk_rect rect) {
+ltk_surface_draw_border(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width, ltk_border_sides border_sides) {
+ /* drawn as rectangles to have proper control over line width - I'm not sure how exactly
+ XDrawLine handles even line widths (i.e. on which side the extra pixel will be) */
XSetForeground(s->window->dpy, s->window->gc, c->xcolor.pixel);
- XFillRectangle(s->window->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, rect.h);
+ if (border_sides & LTK_BORDER_TOP)
+ XFillRectangle(s->window->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, line_width);
+ if (border_sides & LTK_BORDER_BOTTOM)
+ XFillRectangle(s->window->dpy, s->d, s->window->gc, rect.x, rect.y + rect.h - line_width, rect.w, line_width);
+ if (border_sides & LTK_BORDER_LEFT)
+ XFillRectangle(s->window->dpy, s->d, s->window->gc, rect.x, rect.y, line_width, rect.h);
+ if (border_sides & LTK_BORDER_RIGHT)
+ XFillRectangle(s->window->dpy, s->d, s->window->gc, rect.x + rect.w - line_width, rect.y, line_width, rect.h);
}
void
-ltk_window_draw_rect(ltk_window *window, ltk_color *c, ltk_rect rect, int line_width) {
- XSetForeground(window->dpy, window->gc, c->xcolor.pixel);
- XSetLineAttributes(window->dpy, window->gc, line_width, LineSolid, CapButt, JoinMiter);
- XDrawRectangle(window->dpy, window->drawable, window->gc, rect.x, rect.y, rect.w, rect.h);
+ltk_surface_fill_rect(ltk_surface *s, ltk_color *c, ltk_rect rect) {
+ XSetForeground(s->window->dpy, s->window->gc, c->xcolor.pixel);
+ XFillRectangle(s->window->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, rect.h);
}
void
-ltk_window_fill_rect(ltk_window *window, ltk_color *c, ltk_rect rect) {
- XSetForeground(window->dpy, window->gc, c->xcolor.pixel);
- XFillRectangle(window->dpy, window->drawable, window->gc, rect.x, rect.y, rect.w, rect.h);
+ltk_surface_fill_polygon(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints) {
+ /* FIXME: maybe make this statis since this won't be threaded anyways? */
+ XPoint tmp_points[6]; /* to avoid extra allocations when not necessary */
+ /* FIXME: this is ugly and inefficient */
+ XPoint *final_points;
+ if (npoints <= 6) {
+ final_points = tmp_points;
+ } else {
+ final_points = ltk_reallocarray(NULL, npoints, sizeof(XPoint));
+ }
+ /* FIXME: how to deal with ints that don't fit in short? */
+ for (size_t i = 0; i < npoints; i++) {
+ final_points[i].x = (short)points[i].x;
+ final_points[i].y = (short)points[i].y;
+ }
+ XSetForeground(s->window->dpy, s->window->gc, c->xcolor.pixel);
+ XFillPolygon(s->window->dpy, s->d, s->window->gc, final_points, (int)npoints, Complex, CoordModeOrigin);
+ if (npoints > 6)
+ free(final_points);
}
void
diff --git a/src/grid.c b/src/grid.c
@@ -1,3 +1,4 @@
+/* FIXME: sometimes, resizing doesn't work properly when running test.sh */
/*
* Copyright (c) 2016, 2017, 2018, 2020, 2021, 2022 lumidify <nobody@lumidify.org>
*
@@ -160,11 +161,18 @@ ltk_grid_create(ltk_window *window, const char *id, int rows, int columns) {
static void
ltk_grid_destroy(ltk_widget *self, int shallow) {
ltk_grid *grid = (ltk_grid *)self;
+ char *errstr; /* FIXME: unused */
+ if (self->parent && self->parent->vtable->remove_child) {
+ self->parent->vtable->remove_child(
+ self->window, self, self->parent, &errstr
+ );
+ }
ltk_widget *ptr;
- if (!shallow) {
- for (int i = 0; i < grid->rows * grid->columns; i++) {
- if (grid->widget_grid[i]) {
- ptr = grid->widget_grid[i];
+ for (int i = 0; i < grid->rows * grid->columns; i++) {
+ if (grid->widget_grid[i]) {
+ ptr = grid->widget_grid[i];
+ ptr->parent = NULL;
+ if (!shallow) {
/* required to avoid freeing a widget multiple times
if row_span or column_span is not 1 */
for (int r = ptr->row; r < ptr->row + ptr->row_span; r++) {
@@ -183,8 +191,8 @@ ltk_grid_destroy(ltk_widget *self, int shallow) {
ltk_free(grid->column_weights);
ltk_free(grid->row_pos);
ltk_free(grid->column_pos);
- ltk_remove_widget(grid->widget.id);
- ltk_free(grid->widget.id);
+ ltk_remove_widget(self->id);
+ ltk_free(self->id);
ltk_free(grid);
}
@@ -300,6 +308,10 @@ ltk_grid_child_size_change(ltk_widget *self, ltk_widget *widget) {
static int
ltk_grid_add(ltk_window *window, ltk_widget *widget, ltk_grid *grid,
int row, int column, int row_span, int column_span, unsigned short sticky, char **errstr) {
+ if (widget->parent) {
+ *errstr = "Widget already inside a container.\n";
+ return 1;
+ }
if (row + row_span > grid->rows || column + column_span > grid->columns) {
*errstr = "Invalid row or column.\n";
return 1;
@@ -439,22 +451,22 @@ ltk_grid_cmd_add(
*errstr = "Invalid widget ID.\n";
return 1;
}
- row = strtonum(tokens[4], 0, grid->rows - 1, &errstr_num);
+ row = ltk_strtonum(tokens[4], 0, grid->rows - 1, &errstr_num);
if (errstr_num) {
*errstr = "Invalid row number.\n";
return 1;
}
- column = strtonum(tokens[5], 0, grid->columns - 1, &errstr_num);
+ column = ltk_strtonum(tokens[5], 0, grid->columns - 1, &errstr_num);
if (errstr_num) {
*errstr = "Invalid row number.\n";
return 1;
}
- row_span = strtonum(tokens[6], 1, grid->rows, &errstr_num);
+ row_span = ltk_strtonum(tokens[6], 1, grid->rows, &errstr_num);
if (errstr_num) {
*errstr = "Invalid row span.\n";
return 1;
}
- column_span = strtonum(tokens[7], 1, grid->columns, &errstr_num);
+ column_span = ltk_strtonum(tokens[7], 1, grid->columns, &errstr_num);
if (errstr_num) {
*errstr = "Invalid column span.\n";
return 1;
@@ -517,12 +529,12 @@ ltk_grid_cmd_create(
*errstr = "Widget ID already taken.\n";
return 1;
}
- rows = strtonum(tokens[3], 1, 64, &errstr_num);
+ rows = ltk_strtonum(tokens[3], 1, 64, &errstr_num);
if (errstr_num) {
*errstr = "Invalid number of rows.\n";
return 1;
}
- columns = strtonum(tokens[4], 1, 64, &errstr_num);
+ columns = ltk_strtonum(tokens[4], 1, 64, &errstr_num);
if (errstr_num) {
*errstr = "Invalid number of columns.\n";
return 1;
@@ -550,12 +562,12 @@ ltk_grid_cmd_set_row_weight(
}
grid = (ltk_grid *)ltk_get_widget(tokens[1], LTK_GRID, errstr);
if (!grid) return 1;
- row = strtonum(tokens[3], 0, grid->rows, &errstr_num);
+ row = ltk_strtonum(tokens[3], 0, grid->rows, &errstr_num);
if (errstr_num) {
*errstr = "Invalid row number.\n";
return 1;
}
- weight = strtonum(tokens[4], 0, 64, &errstr_num);
+ weight = ltk_strtonum(tokens[4], 0, 64, &errstr_num);
if (errstr_num) {
*errstr = "Invalid row weight.\n";
return 1;
@@ -582,12 +594,12 @@ ltk_grid_cmd_set_column_weight(
}
grid = (ltk_grid *)ltk_get_widget(tokens[1], LTK_GRID, errstr);
if (!grid) return 1;
- column = strtonum(tokens[3], 0, grid->columns, &errstr_num);
+ column = ltk_strtonum(tokens[3], 0, grid->columns, &errstr_num);
if (errstr_num) {
*errstr = "Invalid column number.\n";
return 1;
}
- weight = strtonum(tokens[4], 0, 64, &errstr_num);
+ weight = ltk_strtonum(tokens[4], 0, 64, &errstr_num);
if (errstr_num) {
*errstr = "Invalid column weight.\n";
return 1;
diff --git a/src/label.c b/src/label.c
@@ -34,6 +34,8 @@
#include "graphics.h"
#include "surface_cache.h"
+#define MAX_LABEL_PADDING 500
+
static void ltk_label_draw(ltk_widget *self, ltk_rect clip);
static ltk_label *ltk_label_create(ltk_window *window,
const char *id, char *text);
@@ -57,24 +59,26 @@ static struct {
void
ltk_label_setup_theme_defaults(ltk_window *window) {
theme.pad = 5;
- ltk_color_create(window->dpy, window->screen, window->cm,
- "#FFFFFF", &theme.text_color);
- ltk_color_create(window->dpy, window->screen, window->cm,
- "#000000", &theme.bg_color);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &theme.text_color);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#000000", &theme.bg_color);
}
void
ltk_label_ini_handler(ltk_window *window, const char *prop, const char *value) {
+ const char *errstr;
+ /* FIXME: store generic max padding somewhere for all widgets? */
if (strcmp(prop, "pad") == 0) {
- theme.pad = atoi(value);
+ theme.pad = ltk_strtonum(value, 0, MAX_LABEL_PADDING, &errstr);
+ if (errstr)
+ ltk_warn("Invalid label padding '%s': %s.\n", value, errstr);
} else if (strcmp(prop, "text_color") == 0) {
- ltk_color_create(window->dpy, window->screen, window->cm,
- value, &theme.text_color);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.text_color))
+ ltk_warn("Error setting label text color to '%s'.\n", value);
} else if (strcmp(prop, "bg_color") == 0) {
- ltk_color_create(window->dpy, window->screen, window->cm,
- value, &theme.bg_color);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.bg_color))
+ ltk_warn("Error setting label background color to '%s'.\n", value);
} else {
- ltk_warn("Unknown property \"%s\" for label style.\n", prop);
+ ltk_warn("Unknown property '%s' for label style.\n", prop);
}
}
@@ -121,6 +125,12 @@ ltk_label_create(ltk_window *window, const char *id, char *text) {
static void
ltk_label_destroy(ltk_widget *self, int shallow) {
(void)shallow;
+ char *errstr;
+ if (self->parent && self->parent->vtable->remove_child) {
+ self->parent->vtable->remove_child(
+ self->window, self, self->parent, &errstr
+ );
+ }
ltk_label *label = (ltk_label *)self;
if (!label) {
ltk_warn("Tried to destroy NULL label.\n");
@@ -128,8 +138,8 @@ ltk_label_destroy(ltk_widget *self, int shallow) {
}
ltk_surface_cache_release_key(self->surface_key);
ltk_text_line_destroy(label->tl);
- ltk_remove_widget(label->widget.id);
- ltk_free(label->widget.id);
+ ltk_remove_widget(self->id);
+ ltk_free(self->id);
ltk_free(label);
}
diff --git a/src/ltk.h b/src/ltk.h
@@ -28,7 +28,8 @@
typedef enum {
LTK_EVENT_RESIZE = 1 << 0,
LTK_EVENT_BUTTON = 1 << 1,
- LTK_EVENT_KEY = 1 << 2
+ LTK_EVENT_KEY = 1 << 2,
+ LTK_EVENT_MENU = 1 << 3
} ltk_event_type;
typedef struct {
@@ -83,6 +84,14 @@ typedef struct ltk_window {
ltk_rect dirty_rect;
struct ltk_event_queue *first_event;
struct ltk_event_queue *last_event;
+ /* FIXME: generic array */
+ ltk_widget **popups;
+ size_t popups_num;
+ size_t popups_alloc;
+ /* This is a hack so ltk_window_unregister_all_popups can
+ call hide for all popup widgets even if the hide function
+ already calls ltk_window_unregister_popup */
+ char popups_locked;
} ltk_window;
void ltk_window_invalidate_rect(ltk_window *window, ltk_rect rect);
@@ -92,4 +101,10 @@ void ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget);
void ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget);
void ltk_quit(ltk_window *window);
+void ltk_unregister_timer(int timer_id);
+int ltk_register_timer(long first, long repeat, void (*callback)(void *), void *data);
+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);
+
#endif
diff --git a/src/ltkd.c b/src/ltkd.c
@@ -55,6 +55,11 @@
#include "label.h"
#include "scrollbar.h"
#include "box.h"
+#include "menu.h"
+#include "macros.h"
+
+#define MAX_WINDOW_BORDER_WIDTH 100
+#define MAX_FONT_SIZE 200
#define MAX_SOCK_CONNS 20
#define READ_BLK_SIZE 128
@@ -85,6 +90,18 @@ static struct ltk_sock_info {
struct token_list tokens; /* current tokens */
} sockets[MAX_SOCK_CONNS];
+typedef struct {
+ void (*callback)(void *);
+ void *data;
+ struct timespec repeat;
+ struct timespec remaining;
+ int id;
+} ltk_timer;
+
+static ltk_timer *timers = NULL;
+static size_t timers_num = 0;
+static size_t timers_alloc = 0;
+
static int ltk_mainloop(ltk_window *window);
static char *get_sock_path(char *basedir, Window id);
static FILE *open_log(char *dir);
@@ -179,9 +196,15 @@ ltk_mainloop(ltk_window *window) {
maxfd = listenfd;
printf("%lu", window->xwindow);
- /*fflush(stdout);*/
+ fflush(stdout);
daemonize();
+ /* FIXME: make time management smarter - maybe always figure out how long
+ it will take until the next timer is due and then sleep if no other events
+ are happening */
+ struct timespec now, elapsed, last;
+ clock_gettime(CLOCK_MONOTONIC, &last);
+
while (running) {
rfds = rallfds;
wfds = wallfds;
@@ -241,6 +264,29 @@ ltk_mainloop(ltk_window *window) {
}
}
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ ltk_timespecsub(&now, &last, &elapsed);
+ /* Note: it should be safe to give the same pointer as the first and
+ last argument, as long as ltk_timespecsub/add isn't changed incompatibly */
+ size_t i = 0;
+ while (i < timers_num) {
+ ltk_timespecsub(&timers[i].remaining, &elapsed, &timers[i].remaining);
+ if (timers[i].remaining.tv_sec < 0 ||
+ (timers[i].remaining.tv_sec == 0 && timers[i].remaining.tv_nsec == 0)) {
+ timers[i].callback(timers[i].data);
+ if (timers[i].repeat.tv_sec == 0 && timers[i].repeat.tv_nsec == 0) {
+ /* remove timers because it has no repeat */
+ memmove(timers + i, timers + i + 1, sizeof(ltk_timer) * (timers_num - i - 1));
+ } else {
+ ltk_timespecadd(&timers[i].remaining, &timers[i].repeat, &timers[i].remaining);
+ i++;
+ }
+ } else {
+ i++;
+ }
+ }
+ last = now;
+
if (window->dirty_rect.w != 0 && window->dirty_rect.h != 0) {
ltk_redraw_window(window);
window->dirty_rect.w = 0;
@@ -378,6 +424,7 @@ ltk_cleanup(void) {
ltk_widgets_cleanup();
if (main_window)
ltk_destroy_window(main_window);
+ main_window = NULL;
}
void
@@ -481,10 +528,15 @@ ltk_redraw_window(ltk_window *window) {
window->rect.x, window->rect.y,
window->rect.w, window->rect.h
);
- if (!window->root_widget) return;
- ptr = window->root_widget;
- if (ptr)
+ if (window->root_widget) {
+ ptr = window->root_widget;
ptr->vtable->draw(ptr, window->rect);
+ }
+ /* last popup is the newest one, so draw that last */
+ for (size_t i = 0; i < window->popups_num; i++) {
+ ptr = window->popups[i];
+ ptr->vtable->draw(ptr, window->rect);
+ }
XdbeSwapInfo swap_info;
swap_info.swap_window = window->xwindow;
swap_info.swap_action = XdbeBackground;
@@ -497,6 +549,7 @@ static void
ltk_window_other_event(ltk_window *window, XEvent event) {
ltk_widget *ptr = window->root_widget;
if (event.type == ConfigureNotify) {
+ ltk_window_unregister_all_popups(window);
int w, h;
w = event.xconfigure.width;
h = event.xconfigure.height;
@@ -526,6 +579,124 @@ ltk_window_other_event(ltk_window *window, XEvent event) {
}
}
+/* FIXME: optimize timer handling - maybe also a sort of priority queue */
+/* FIXME: JUST USE A GENERIC DYNAMIC ARRAY ALREADY!!!!! */
+void
+ltk_unregister_timer(int timer_id) {
+ for (size_t i = 0; i < timers_num; i++) {
+ if (timers[i].id == timer_id) {
+ memmove(
+ timers + i,
+ timers + i + 1,
+ sizeof(ltk_timer) * (timers_num - i - 1)
+ );
+ timers_num--;
+ size_t sz = ideal_array_size(timers_alloc, timers_num);
+ if (sz != timers_alloc) {
+ timers_alloc = sz;
+ timers = ltk_reallocarray(
+ timers, sz, sizeof(ltk_timer)
+ );
+ }
+ return;
+ }
+ }
+}
+
+/* repeat <= 0 means no repeat, first <= 0 means run as soon as possible */
+int
+ltk_register_timer(long first, long repeat, void (*callback)(void *), void *data) {
+ if (first < 0)
+ first = 0;
+ if (repeat < 0)
+ repeat = 0;
+ if (timers_num == timers_alloc) {
+ timers_alloc = ideal_array_size(timers_alloc, timers_num + 1);
+ timers = ltk_reallocarray(
+ timers, timers_alloc, sizeof(ltk_timer)
+ );
+ }
+ /* FIXME: better finding of id */
+ /* FIXME: maybe store sorted by id */
+ int id = 0;
+ for (size_t i = 0; i < timers_num; i++) {
+ if (timers[i].id >= id)
+ id = timers[i].id + 1;
+ }
+ ltk_timer *t = &timers[timers_num++];
+ t->callback = callback;
+ t->data = data;
+ t->repeat.tv_sec = repeat / 1000;
+ t->repeat.tv_nsec = (repeat % 1000) * 1000;
+ t->remaining.tv_sec = first / 1000;
+ t->remaining.tv_nsec = (first % 1000) * 1000;
+ t->id = id;
+ return id;
+}
+
+/* FIXME: check for duplicates? */
+void
+ltk_window_register_popup(ltk_window *window, ltk_widget *popup) {
+ if (window->popups_num == window->popups_alloc) {
+ window->popups_alloc = ideal_array_size(
+ window->popups_alloc, window->popups_num + 1
+ );
+ window->popups = ltk_reallocarray(
+ window->popups, window->popups_alloc, sizeof(ltk_widget *)
+ );
+ }
+ window->popups[window->popups_num++] = popup;
+}
+
+void
+ltk_window_unregister_popup(ltk_window *window, ltk_widget *popup) {
+ if (window->popups_locked)
+ return;
+ for (size_t i = 0; i < window->popups_num; i++) {
+ if (window->popups[i] == popup) {
+ memmove(
+ window->popups + i,
+ window->popups + i + 1,
+ sizeof(ltk_widget *) * (window->popups_num - i - 1)
+ );
+ window->popups_num--;
+ size_t sz = ideal_array_size(
+ window->popups_alloc, window->popups_num
+ );
+ if (sz != window->popups_alloc) {
+ window->popups_alloc = sz;
+ window->popups = ltk_reallocarray(
+ window->popups, sz, sizeof(ltk_widget *)
+ );
+ }
+ return;
+ }
+ }
+}
+
+/* FIXME: where should actual hiding happen? */
+void
+ltk_window_unregister_all_popups(ltk_window *window) {
+ window->popups_locked = 1;
+ for (size_t i = 0; i < window->popups_num; i++) {
+ if (window->popups[i]->vtable->hide) {
+ window->popups[i]->vtable->hide(window->popups[i]);
+ }
+ window->popups[i]->hidden = 1;
+ }
+ window->popups_num = 0;
+ /* somewhat arbitrary, but should be enough for most cases */
+ if (window->popups_num > 4) {
+ window->popups = ltk_reallocarray(
+ window->popups, 4, sizeof(ltk_widget *)
+ );
+ window->popups_alloc = 4;
+ }
+ window->popups_locked = 0;
+ /* I guess just invalidate everything instead of being smart */
+ ltk_window_invalidate_rect(window, window->rect);
+}
+
static ltk_window *
ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int h) {
char *theme_path;
@@ -533,6 +704,10 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int
ltk_window *window = ltk_malloc(sizeof(ltk_window));
+ window->popups = NULL;
+ window->popups_num = window->popups_alloc = 0;
+ window->popups_locked = 0;
+
window->dpy = XOpenDisplay(NULL);
window->screen = DefaultScreen(window->dpy);
/* based on http://wili.cc/blog/xdbe.html */
@@ -562,6 +737,9 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int
ltk_fatal("Couldn't match a Visual with double buffering.\n");
}
window->vis = xvisinfo_match->visual;
+ /* FIXME: is it legal to free this while keeping the visual? */
+ XFree(xvisinfo_match);
+ XdbeFreeVisualInfo(info);
found = 1;
} else {
window->vis = DefaultVisual(window->dpy, window->screen);
@@ -574,6 +752,7 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int
if (!theme_path)
ltk_fatal_errno("Not enough memory for theme path.\n");
ltk_load_theme(window, theme_path);
+ ltk_free(theme_path);
window->wm_delete_msg = XInternAtom(window->dpy, "WM_DELETE_WINDOW", False);
memset(&attrs, 0, sizeof(attrs));
@@ -641,10 +820,13 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int
static void
ltk_destroy_window(ltk_window *window) {
ltk_text_context_destroy(window->text_context);
+ if (window->popups)
+ ltk_free(window->popups);
+ XFreeGC(window->dpy, window->gc);
XDestroyWindow(window->dpy, window->xwindow);
XCloseDisplay(window->dpy);
- /* FIXME: This doesn't work because it can sometimes be a readonly
- string from ltk_window_setup_theme_defaults! */
+ ltk_surface_destroy(window->surface);
+ ltk_surface_cache_destroy(window->surface_cache);
if (window->theme.font)
ltk_free(window->theme.font);
ltk_free(window);
@@ -652,18 +834,23 @@ ltk_destroy_window(ltk_window *window) {
void
ltk_window_ini_handler(ltk_window *window, const char *prop, const char *value) {
+ const char *errstr;
if (strcmp(prop, "border_width") == 0) {
- window->theme.border_width = atoi(value);
+ window->theme.border_width = ltk_strtonum(value, 0, MAX_WINDOW_BORDER_WIDTH, &errstr);
+ if (errstr)
+ ltk_warn("Invalid window border width '%s': %s.\n", value, errstr);
} else if (strcmp(prop, "bg") == 0) {
- ltk_color_create(window->dpy, window->screen,
- window->cm, value, &window->theme.bg);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &window->theme.bg))
+ ltk_warn("Error setting window background color to '%s'.\n", value);
} else if (strcmp(prop, "fg") == 0) {
- ltk_color_create(window->dpy, window->screen,
- window->cm, value, &window->theme.fg);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &window->theme.fg))
+ ltk_warn("Error setting window foreground color to '%s'.\n", value);
} else if (strcmp(prop, "font") == 0) {
window->theme.font = ltk_strdup(value);
} else if (strcmp(prop, "font_size") == 0) {
- window->theme.font_size = atoi(value);
+ window->theme.font_size = ltk_strtonum(value, 0, MAX_FONT_SIZE, &errstr);
+ if (errstr)
+ ltk_warn("Invalid window font size '%s': %s.\n", value, errstr);
}
}
@@ -677,6 +864,10 @@ ltk_ini_handler(void *window, const char *widget, const char *prop, const char *
ltk_label_ini_handler(window, prop, value);
} else if (strcmp(widget, "scrollbar") == 0) {
ltk_scrollbar_ini_handler(window, prop, value);
+ } else if (strcmp(widget, "menu") == 0) {
+ ltk_menu_ini_handler(window, prop, value);
+ } else if (strcmp(widget, "submenu") == 0) {
+ ltk_submenu_ini_handler(window, prop, value);
} else {
return 0;
}
@@ -688,10 +879,8 @@ ltk_window_setup_theme_defaults(ltk_window *window) {
window->theme.border_width = 0;
window->theme.font_size = 15;
window->theme.font = ltk_strdup("Liberation Mono");
- ltk_color_create(window->dpy, window->screen,
- window->cm, "#000000", &window->theme.bg);
- ltk_color_create(window->dpy, window->screen,
- window->cm, "#FFFFFF", &window->theme.fg);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#000000", &window->theme.bg);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &window->theme.fg);
}
static void
@@ -701,6 +890,7 @@ ltk_load_theme(ltk_window *window, const char *path) {
ltk_button_setup_theme_defaults(window);
ltk_label_setup_theme_defaults(window);
ltk_scrollbar_setup_theme_defaults(window);
+ ltk_menu_setup_theme_defaults(window);
if (ini_parse(path, ltk_ini_handler, window) < 0) {
ltk_warn("Can't load theme.\n");
}
@@ -741,8 +931,18 @@ ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget) {
}
}
+static ltk_widget *
+get_hover_popup(ltk_window *window, int x, int y) {
+ for (size_t i = window->popups_num; i-- > 0;) {
+ if (ltk_collide_rect(window->popups[i]->rect, x, y))
+ return window->popups[i];
+ }
+ return NULL;
+}
+
static void
ltk_handle_event(ltk_window *window, XEvent event) {
+ ltk_widget *hover_popup;
ltk_widget *root_widget = window->root_widget;
switch (event.type) {
case KeyPress:
@@ -750,15 +950,26 @@ ltk_handle_event(ltk_window *window, XEvent event) {
case KeyRelease:
break;
case ButtonPress:
- if (root_widget)
+ hover_popup = get_hover_popup(window, event.xbutton.x, event.xbutton.y);
+ if (hover_popup) {
+ ltk_widget_mouse_press_event(hover_popup, event);
+ } else if (root_widget) {
+ ltk_window_unregister_all_popups(window);
ltk_widget_mouse_press_event(root_widget, event);
+ }
break;
case ButtonRelease:
- if (root_widget)
+ hover_popup = get_hover_popup(window, event.xbutton.x, event.xbutton.y);
+ if (hover_popup)
+ ltk_widget_mouse_release_event(hover_popup, event);
+ else if (root_widget)
ltk_widget_mouse_release_event(root_widget, event);
break;
case MotionNotify:
- if (root_widget)
+ hover_popup = get_hover_popup(window, event.xmotion.x, event.xmotion.y);
+ if (hover_popup)
+ ltk_widget_motion_notify_event(hover_popup, event);
+ else if (root_widget)
ltk_widget_motion_notify_event(root_widget, event);
break;
default:
@@ -1015,6 +1226,10 @@ process_commands(ltk_window *window, struct ltk_sock_info *sock) {
err = ltk_button_cmd(window, tokens, num_tokens, &errstr);
} else if (strcmp(tokens[0], "label") == 0) {
err = ltk_label_cmd(window, tokens, num_tokens, &errstr);
+ } else if (strcmp(tokens[0], "menu") == 0) {
+ err = ltk_menu_cmd(window, tokens, num_tokens, &errstr);
+ } else if (strcmp(tokens[0], "submenu") == 0) {
+ err = ltk_menu_cmd(window, tokens, num_tokens, &errstr);
} else if (strcmp(tokens[0], "set-root-widget") == 0) {
err = ltk_set_root_widget_cmd(window, tokens, num_tokens, &errstr);
/*
@@ -1024,7 +1239,7 @@ process_commands(ltk_window *window, struct ltk_sock_info *sock) {
} else if (strcmp(tokens[0], "quit") == 0) {
ltk_quit(window);
} else if (strcmp(tokens[0], "destroy") == 0) {
- err = ltk_widget_destroy(window, tokens, num_tokens, &errstr);
+ err = ltk_widget_destroy_cmd(window, tokens, num_tokens, &errstr);
} else {
errstr = "Invalid command.\n";
err = 1;
diff --git a/src/macros.h b/src/macros.h
@@ -0,0 +1,25 @@
+#ifndef _MACROS_H_
+#define _MACROS_H_
+
+/* stolen from OpenBSD */
+#define ltk_timespecadd(tsp, usp, vsp) \
+ do { \
+ (vsp)->tv_sec = (tsp)->tv_sec + (usp)->tv_sec; \
+ (vsp)->tv_nsec = (tsp)->tv_nsec + (usp)->tv_nsec; \
+ if ((vsp)->tv_nsec >= 1000000000L) { \
+ (vsp)->tv_sec++; \
+ (vsp)->tv_nsec -= 1000000000L; \
+ } \
+ } while (0)
+
+#define ltk_timespecsub(tsp, usp, vsp) \
+ do { \
+ (vsp)->tv_sec = (tsp)->tv_sec - (usp)->tv_sec; \
+ (vsp)->tv_nsec = (tsp)->tv_nsec - (usp)->tv_nsec; \
+ if ((vsp)->tv_nsec < 0) { \
+ (vsp)->tv_sec--; \
+ (vsp)->tv_nsec += 1000000000L; \
+ } \
+ } while (0)
+
+#endif
diff --git a/src/memory.c b/src/memory.c
@@ -129,6 +129,9 @@ ltk_reallocarray(void *optr, size_t nmemb, size_t size)
size_t
ideal_array_size(size_t old, size_t needed) {
size_t ret = old;
+ /* FIXME: the shrinking here only makes sense if not
+ many elements are removed at once - what would be
+ more sensible here? */
if (old < needed)
ret = old * 2 > needed ? old * 2 : needed;
else if (needed * 4 < old)
diff --git a/src/menu.c b/src/menu.c
@@ -0,0 +1,1772 @@
+/*
+ * Copyright (c) 2022 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 <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <stdarg.h>
+#include <math.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+
+#include "memory.h"
+#include "color.h"
+#include "rect.h"
+#include "widget.h"
+#include "ltk.h"
+#include "util.h"
+#include "text.h"
+#include "menu.h"
+#include "graphics.h"
+#include "surface_cache.h"
+
+#define MAX_MENU_BORDER_WIDTH 100
+#define MAX_MENU_PAD 500
+#define MAX_MENU_ARROW_SIZE 100
+
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+
+static struct theme {
+ int border_width;
+ int pad;
+ int text_pad;
+ int arrow_size;
+ int arrow_pad;
+ int compress_borders;
+ int menu_border_width;
+ /* FIXME: should border_sides actually factor into
+ size calculation? - probably useless and would
+ just make it more complicated */
+ /* FIXME: allow different values for different states? */
+ ltk_border_sides border_sides;
+
+ ltk_color background;
+ ltk_color scroll_background;
+ ltk_color scroll_arrow_color;
+ ltk_color menu_border;
+
+ ltk_color text;
+ ltk_color border;
+ ltk_color fill;
+
+ ltk_color text_pressed;
+ ltk_color border_pressed;
+ ltk_color fill_pressed;
+
+ ltk_color text_active;
+ ltk_color border_active;
+ ltk_color fill_active;
+
+ ltk_color text_disabled;
+ ltk_color border_disabled;
+ ltk_color fill_disabled;
+} menu_theme, submenu_theme;
+
+static void ini_handler(ltk_window *window, struct theme *t, const char *prop, const char *value);
+static void ltk_menu_resize(ltk_widget *self);
+static void ltk_menu_change_state(ltk_widget *self);
+static void ltk_menu_draw(ltk_widget *self, ltk_rect clip);
+static void ltk_menu_redraw_surface(ltk_menu *menu, ltk_surface *s);
+static void ltk_menu_get_max_scroll_offset(ltk_menu *menu, int *x_ret, int *y_ret);
+static void ltk_menu_scroll(ltk_menu *menu, char t, char b, char l, char r, int step);
+static void ltk_menu_scroll_callback(void *data);
+static void stop_scrolling(ltk_menu *menu);
+static size_t get_entry_at_point(ltk_menu *menu, int x, int y, ltk_rect *entry_rect_ret);
+static int set_scroll_timer(ltk_menu *menu, int x, int y);
+static int ltk_menu_mouse_release(ltk_widget *self, XEvent event);
+static int ltk_menu_mouse_press(ltk_widget *self, XEvent event);
+static void ltk_menu_hide(ltk_widget *self);
+static void popup_active_menu(ltk_menu *menu, ltk_rect r);
+static void unpopup_active_entry(ltk_menu *menu);
+static void handle_hover(ltk_menu *menu, int x, int y);
+static int ltk_menu_motion_notify(ltk_widget *self, XEvent event);
+static int ltk_menu_mouse_enter(ltk_widget *self, XEvent event);
+static int ltk_menu_mouse_leave(ltk_widget *self, XEvent event);
+static ltk_menu *ltk_menu_create(ltk_window *window, const char *id, int is_submenu);
+static ltk_menuentry *insert_entry(ltk_menu *menu, size_t idx);
+static void recalc_menu_size(ltk_menu *menu);
+static void shrink_entries(ltk_menu *menu);
+static size_t get_entry_with_id(ltk_menu *menu, const char *id);
+static void ltk_menu_destroy(ltk_widget *self, int shallow);
+
+static ltk_menuentry *ltk_menu_insert_entry(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, size_t idx, char **errstr);
+static ltk_menuentry *ltk_menu_add_entry(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, char **errstr);
+static ltk_menuentry *ltk_menu_insert_submenu(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, size_t idx, char **errstr);
+static ltk_menuentry *ltk_menu_add_submenu(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, char **errstr);
+static int ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx, int shallow, char **errstr);
+static int ltk_menu_remove_entry_id(ltk_menu *menu, const char *id, int shallow, char **errstr);
+static int ltk_menu_remove_all_entries(ltk_menu *menu, int shallow, char **errstr);
+static int ltk_menu_detach_submenu_from_entry_id(ltk_menu *menu, const char *id, char **errstr);
+static int ltk_menu_detach_submenu_from_entry_index(ltk_menu *menu, size_t idx, char **errstr);
+static int ltk_menu_disable_entry_index(ltk_menu *menu, size_t idx, char **errstr);
+static int ltk_menu_disable_entry_id(ltk_menu *menu, const char *id, char **errstr);
+static int ltk_menu_disable_all_entries(ltk_menu *menu, char **errstr);
+static int ltk_menu_enable_entry_index(ltk_menu *menu, size_t idx, char **errstr);
+static int ltk_menu_enable_entry_id(ltk_menu *menu, const char *id, char **errstr);
+static int ltk_menu_enable_all_entries(ltk_menu *menu, char **errstr);
+
+static struct ltk_widget_vtable vtable = {
+ .mouse_press = <k_menu_mouse_press,
+ .motion_notify = <k_menu_motion_notify,
+ .mouse_release = <k_menu_mouse_release,
+ .mouse_enter = <k_menu_mouse_enter,
+ .mouse_leave = <k_menu_mouse_leave,
+ .resize = <k_menu_resize,
+ .change_state = <k_menu_change_state,
+ .hide = <k_menu_hide,
+ .draw = <k_menu_draw,
+ .destroy = <k_menu_destroy,
+ .type = LTK_MENU,
+ .needs_redraw = 1,
+ .needs_surface = 1
+};
+
+/* FIXME: maybe just store colors as pointers and check after
+ ini handling if any are null */
+
+void
+ltk_menu_setup_theme_defaults(ltk_window *window) {
+ menu_theme.border_width = 2;
+ menu_theme.pad = 0;
+ menu_theme.text_pad = 5;
+ menu_theme.arrow_size = 10;
+ menu_theme.arrow_pad = 5;
+ menu_theme.compress_borders = 1;
+ menu_theme.border_sides = LTK_BORDER_ALL;
+ menu_theme.menu_border_width = 0;
+ ltk_color_create(window->dpy, window->screen, window->cm, "#555555", &menu_theme.background);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#333333", &menu_theme.scroll_background);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#000000", &menu_theme.scroll_arrow_color);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &menu_theme.text);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#339999", &menu_theme.border);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &menu_theme.fill);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &menu_theme.text_pressed);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &menu_theme.border_pressed);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &menu_theme.fill_pressed);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &menu_theme.text_active);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &menu_theme.border_active);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#738194", &menu_theme.fill_active);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &menu_theme.text_disabled);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &menu_theme.border_disabled);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#292929", &menu_theme.fill_disabled);
+
+ /* FIXME: actually unnecessary since border width is 0 */
+ ltk_color_create(window->dpy, window->screen, window->cm, "#000000", &menu_theme.menu_border);
+
+ submenu_theme.border_width = 0;
+ submenu_theme.pad = 5;
+ submenu_theme.text_pad = 5;
+ submenu_theme.arrow_size = 10;
+ submenu_theme.arrow_pad = 5;
+ submenu_theme.compress_borders = 0;
+ submenu_theme.border_sides = LTK_BORDER_NONE;
+ submenu_theme.menu_border_width = 1;
+ ltk_color_create(window->dpy, window->screen, window->cm, "#555555", &submenu_theme.background);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#333333", &submenu_theme.scroll_background);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#000000", &submenu_theme.scroll_arrow_color);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &submenu_theme.menu_border);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &submenu_theme.text);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &submenu_theme.fill);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#000000", &submenu_theme.text_pressed);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &submenu_theme.fill_pressed);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#000000", &submenu_theme.text_active);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &submenu_theme.fill_active);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &submenu_theme.text_disabled);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#292929", &submenu_theme.fill_disabled);
+
+ /* FIXME: make this unnecessary if border width is 0 */
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &submenu_theme.border);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &submenu_theme.border_pressed);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &submenu_theme.border_active);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &submenu_theme.border_disabled);
+}
+
+/* FIXME: use border-width, etc. */
+/* FIXME: make theme parsing more convenient */
+/* FIXME: DEALLOCATE COLORS INSTEAD OF OVERWRITING DEFAULTS! */
+static void
+ini_handler(ltk_window *window, struct theme *t, const char *prop, const char *value) {
+ const char *errstr;
+ if (strcmp(prop, "border_width") == 0) {
+ t->border_width = ltk_strtonum(value, 0, MAX_MENU_BORDER_WIDTH, &errstr);
+ if (errstr)
+ ltk_warn("Invalid menu border width '%s': %s.\n", value, errstr);
+ } else if (strcmp(prop, "menu_border_width") == 0) {
+ t->menu_border_width = ltk_strtonum(value, 0, MAX_MENU_BORDER_WIDTH, &errstr);
+ /* FIXME: clarify different types of border width in error message */
+ if (errstr)
+ ltk_warn("Invalid menu border width '%s': %s.\n", value, errstr);
+ } else if (strcmp(prop, "pad") == 0) {
+ t->pad = ltk_strtonum(value, 0, MAX_MENU_PAD, &errstr);
+ if (errstr)
+ ltk_warn("Invalid menu pad '%s': %s.\n", value, errstr);
+ } else if (strcmp(prop, "text_pad") == 0) {
+ t->text_pad = ltk_strtonum(value, 0, MAX_MENU_PAD, &errstr);
+ if (errstr)
+ ltk_warn("Invalid menu text pad '%s': %s.\n", value, errstr);
+ } else if (strcmp(prop, "arrow_size") == 0) {
+ /* FIXME: should be error when used for menu instead of submenu */
+ t->arrow_size = ltk_strtonum(value, 0, MAX_MENU_ARROW_SIZE, &errstr);
+ if (errstr)
+ ltk_warn("Invalid menu arrow size '%s': %s.\n", value, errstr);
+ } else if (strcmp(prop, "arrow_pad") == 0) {
+ /* FIXME: should be error when used for menu instead of submenu */
+ t->arrow_pad = ltk_strtonum(value, 0, MAX_MENU_PAD, &errstr);
+ if (errstr)
+ ltk_warn("Invalid menu arrow pad '%s': %s.\n", value, errstr);
+ } else if (strcmp(prop, "compress_borders") == 0) {
+ if (strcmp(value, "true") == 0)
+ t->compress_borders = 1;
+ else if (strcmp(value, "false") == 0)
+ t->compress_borders = 0;
+ else
+ ltk_warn("Invalid menu compress_borders '%s'.\n", value);
+ } else if (strcmp(prop, "border_sides") == 0) {
+ t->border_sides = LTK_BORDER_NONE;
+ for (const char *c = value; *c != '\0'; c++) {
+ switch (*c) {
+ case 't':
+ t->border_sides |= LTK_BORDER_TOP;
+ break;
+ case 'b':
+ t->border_sides |= LTK_BORDER_BOTTOM;
+ break;
+ case 'l':
+ t->border_sides |= LTK_BORDER_LEFT;
+ break;
+ case 'r':
+ t->border_sides |= LTK_BORDER_RIGHT;
+ break;
+ default:
+ ltk_warn("Invalid menu border_sides '%s'.\n", value);
+ return;
+ }
+ }
+ } else if (strcmp(prop, "background") == 0) {
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->background))
+ ltk_warn("Error setting menu background color to '%s'.\n", value);
+ } else if (strcmp(prop, "menu_border") == 0) {
+ /* FIXME: clarify different type of menu border color */
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->menu_border))
+ ltk_warn("Error setting menu border color to '%s'.\n", value);
+ } else if (strcmp(prop, "scroll_background") == 0) {
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->scroll_background))
+ ltk_warn("Error setting menu scroll background color to '%s'.\n", value);
+ } else if (strcmp(prop, "scroll_arrow_color") == 0) {
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->scroll_arrow_color))
+ ltk_warn("Error setting menu scroll arrow color to '%s'.\n", value);
+ } else if (strcmp(prop, "text") == 0) {
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->text))
+ ltk_warn("Error setting menu text color to '%s'.\n", value);
+ } else if (strcmp(prop, "border") == 0) {
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->border))
+ ltk_warn("Error setting menu border color to '%s'.\n", value);
+ } else if (strcmp(prop, "fill") == 0) {
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->fill))
+ ltk_warn("Error setting menu fill color to '%s'.\n", value);
+ } else if (strcmp(prop, "text_pressed") == 0) {
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->text_pressed))
+ ltk_warn("Error setting menu pressed text color to '%s'.\n", value);
+ } else if (strcmp(prop, "border_pressed") == 0) {
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->border_pressed))
+ ltk_warn("Error setting menu pressed border color to '%s'.\n", value);
+ } else if (strcmp(prop, "fill_pressed") == 0) {
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->fill_pressed))
+ ltk_warn("Error setting menu pressed fill color to '%s'.\n", value);
+ } else if (strcmp(prop, "text_active") == 0) {
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->text_active))
+ ltk_warn("Error setting menu active text color to '%s'.\n", value);
+ } else if (strcmp(prop, "border_active") == 0) {
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->border_active))
+ ltk_warn("Error setting menu active border color to '%s'.\n", value);
+ } else if (strcmp(prop, "fill_active") == 0) {
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->fill_active))
+ ltk_warn("Error setting menu active fill color to '%s'.\n", value);
+ } else if (strcmp(prop, "text_disabled") == 0) {
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->text_disabled))
+ ltk_warn("Error setting menu disabled text color to '%s'.\n", value);
+ } else if (strcmp(prop, "border_disabled") == 0) {
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->border_disabled))
+ ltk_warn("Error setting menu disabled border color to '%s'.\n", value);
+ } else if (strcmp(prop, "fill_disabled") == 0) {
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->fill_disabled))
+ ltk_warn("Error setting menu disabled fill color to '%s'.\n", value);
+ } else {
+ ltk_warn("Unknown property '%s' for button style.\n", prop);
+ }
+}
+
+void
+ltk_menu_ini_handler(ltk_window *window, const char *prop, const char *value) {
+ ini_handler(window, &menu_theme, prop, value);
+}
+
+void
+ltk_submenu_ini_handler(ltk_window *window, const char *prop, const char *value) {
+ ini_handler(window, &submenu_theme, prop, value);
+}
+
+static void
+ltk_menu_resize(ltk_widget *self) {
+ ltk_menu *menu = (ltk_menu *)self;
+ double x_old = menu->x_scroll_offset;
+ double y_old = menu->y_scroll_offset;
+ int max_x, max_y;
+ ltk_menu_get_max_scroll_offset(menu, &max_x, &max_y);
+ if (menu->x_scroll_offset > max_x)
+ menu->x_scroll_offset = max_x;
+ if (menu->y_scroll_offset > max_y)
+ menu->y_scroll_offset = max_y;
+ if (fabs(x_old - menu->x_scroll_offset) < 0.01 ||
+ fabs(y_old - menu->y_scroll_offset) < 0.01) {
+ menu->widget.dirty = 1;
+ ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
+ }
+}
+
+static void
+ltk_menu_change_state(ltk_widget *self) {
+ ltk_menu *menu = (ltk_menu *)self;
+ if (self->state != LTK_PRESSED && menu->pressed_entry < menu->num_entries) {
+ menu->pressed_entry = SIZE_MAX;
+ self->dirty = 1;
+ ltk_window_invalidate_rect(self->window, self->rect);
+ }
+}
+
+static void
+ltk_menu_draw(ltk_widget *self, ltk_rect clip) {
+ if (self->hidden)
+ return;
+ ltk_menu *menu = (ltk_menu *)self;
+ ltk_rect rect = self->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_menu_redraw_surface(menu, s);
+ ltk_surface_copy(s, self->window->surface, ltk_rect_relative(rect, clip_final), clip_final.x, clip_final.y);
+}
+
+/* FIXME: glitches when drawing text with stb backend while scrolling */
+static void
+ltk_menu_redraw_surface(ltk_menu *menu, ltk_surface *s) {
+ ltk_rect rect = menu->widget.rect;
+ int ideal_w = menu->widget.ideal_w, ideal_h = menu->widget.ideal_h;
+ struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
+
+ int arrow_size = t->arrow_pad * 2 + t->arrow_size;
+ int start_x = rect.w < ideal_w ? arrow_size : 0;
+ int start_y = rect.h < ideal_h ? arrow_size : 0;
+ start_x += t->menu_border_width;
+ start_y += t->menu_border_width;
+ int real_w = rect.w - start_x * 2;
+ int real_h = rect.h - start_y * 2;
+
+ int offset_x = (int)menu->x_scroll_offset;
+ int offset_y = (int)menu->y_scroll_offset;
+
+ ltk_surface_fill_rect(s, &t->background, (ltk_rect){0, 0, rect.w, rect.h});
+ int text_w, text_h;
+ ltk_color *text, *border, *fill;
+ int cur_abs_x = 0, cur_abs_y = 0;
+ if (menu->is_submenu)
+ cur_abs_y = t->pad;
+ else
+ cur_abs_x = t->pad;
+ int overlap = t->compress_borders ? t->border_width - t->pad : 0;
+ int bw_advance = t->compress_borders ? t->border_width : t->border_width * 2;
+ int mbw = t->menu_border_width;
+ for (size_t i = 0; i < menu->num_entries; i++) {
+ ltk_menuentry *e = &menu->entries[i];
+ ltk_text_line_get_size(e->text, &text_w, &text_h);
+ if (menu->is_submenu) {
+ if (cur_abs_y + t->border_width * 2 + t->text_pad * 2 + text_h <= offset_y) {
+ /* FIXME: ugly because repeated further down */
+ cur_abs_y += bw_advance + t->text_pad * 2 + text_h + t->pad;
+ continue;
+ } else if (cur_abs_y >= offset_y + real_h) {
+ break;
+ }
+ } else {
+ if (cur_abs_x + t->border_width * 2 + t->text_pad * 2 + text_w <= offset_x) {
+ cur_abs_x += bw_advance + t->text_pad * 2 + text_w + t->pad;
+ continue;
+ } else if (cur_abs_x >= offset_x + real_w) {
+ break;
+ }
+ }
+ /* FIXME: allow different border_sides for different states */
+ if (e->disabled) {
+ text = &t->text_disabled;
+ border = &t->border_disabled;
+ fill = &t->fill_disabled;
+ } else if (menu->pressed_entry == i) {
+ text = &t->text_pressed;
+ border = &t->border_pressed;
+ fill = &t->fill_pressed;
+ } else if (menu->active_entry == i) {
+ text = &t->text_active;
+ border = &t->border_active;
+ fill = &t->fill_active;
+ } else {
+ text = &t->text;
+ border = &t->border;
+ fill = &t->fill;
+ }
+ /* FIXME: how well-defined is it to give X drawing commands
+ with parts outside of the actual pixmap? */
+ /* FIXME: optimize drawing (avoid drawing pixels multiple times) */
+ int draw_x = cur_abs_x - offset_x + start_x;
+ int draw_y = cur_abs_y - offset_y + start_y;
+ int last_special = i > 0 && (menu->active_entry == i - 1 || menu->pressed_entry == i - 1);
+ if (menu->is_submenu) {
+ int extra_size = e->submenu ? t->arrow_pad * 2 + t->arrow_size : 0;
+ int height = MAX(text_h + t->text_pad * 2, extra_size) + t->border_width * 2;
+ ltk_rect r;
+ if (last_special && overlap > 0) {
+ r = (ltk_rect){
+ draw_x + overlap,
+ draw_y + t->pad, /* t->pad is the same as t->border_width - overlap */
+ ideal_w - t->pad * 2 - mbw * 2,
+ height - overlap
+ };
+ } else {
+ r = (ltk_rect){draw_x + t->pad, draw_y, ideal_w - t->pad * 2, height};
+ }
+ ltk_surface_fill_rect(s, fill, r);
+ ltk_text_line_draw(
+ e->text, s, text,
+ draw_x + t->pad + t->border_width + t->text_pad,
+ draw_y + height / 2 - text_h / 2
+ );
+ if (e->submenu) {
+ ltk_point arrow_points[3] = {
+ {draw_x + ideal_w - t->pad - t->arrow_pad, draw_y + height / 2},
+ {draw_x + ideal_w - t->pad - t->arrow_pad - t->arrow_size, draw_y + height / 2 - t->arrow_size / 2},
+ {draw_x + ideal_w - t->pad - t->arrow_pad - t->arrow_size, draw_y + height / 2 + t->arrow_size / 2}
+ };
+ ltk_surface_fill_polygon(s, text, arrow_points, 3);
+ }
+ if (last_special && overlap > 0) {
+ ltk_surface_draw_border(s, border, r, t->border_width, t->border_sides & ~LTK_BORDER_TOP);
+ if (t->border_sides & LTK_BORDER_TOP)
+ ltk_surface_draw_border(s, border, r, t->pad, LTK_BORDER_TOP);
+ } else {
+ ltk_surface_draw_border(s, border, r, t->border_width, t->border_sides);
+ }
+ cur_abs_y += bw_advance + t->text_pad * 2 + text_h + t->pad;
+ } else {
+ ltk_rect r;
+ if (last_special && overlap > 0) {
+ r = (ltk_rect){
+ draw_x + overlap,
+ draw_y + t->pad,
+ t->text_pad * 2 + t->border_width * 2 - overlap + text_w,
+ ideal_h - t->pad * 2 - mbw * 2
+ };
+ } else {
+ r = (ltk_rect){draw_x, draw_y + t->pad, t->text_pad * 2 + t->border_width * 2 + text_w, ideal_h - t->pad * 2};
+ }
+ ltk_surface_fill_rect(s, fill, r);
+ /* FIXME: should the text be bottom-aligned in case different
+ entries have different text height? */
+ ltk_text_line_draw(
+ e->text, s, text,
+ draw_x + t->border_width + t->text_pad,
+ draw_y + t->pad + t->border_width + t->text_pad
+ );
+ if (last_special && overlap > 0) {
+ ltk_surface_draw_border(s, border, r, t->border_width, t->border_sides & ~LTK_BORDER_LEFT);
+ if (t->border_sides & LTK_BORDER_LEFT)
+ ltk_surface_draw_border(s, border, r, t->pad, LTK_BORDER_LEFT);
+ } else {
+ ltk_surface_draw_border(s, border, r, t->border_width, t->border_sides);
+ }
+ cur_abs_x += bw_advance + t->text_pad * 2 + text_w + t->pad;
+ }
+ }
+ /* FIXME: active, pressed states */
+ int sz = t->arrow_size + t->arrow_pad * 2;
+ int ww = menu->widget.rect.w;
+ int wh = menu->widget.rect.h;
+ if (rect.w < ideal_w) {
+ ltk_surface_fill_rect(s, &t->scroll_background, (ltk_rect){mbw, mbw, sz, wh - mbw * 2});
+ ltk_surface_fill_rect(s, &t->scroll_background, (ltk_rect){ww - sz - mbw, mbw, sz, wh - mbw * 2});
+ ltk_point arrow_points[3] = {
+ {t->arrow_pad + mbw, wh / 2},
+ {t->arrow_pad + mbw + t->arrow_size, wh / 2 - t->arrow_size / 2},
+ {t->arrow_pad + mbw + t->arrow_size, wh / 2 + t->arrow_size / 2}
+ };
+ ltk_surface_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
+ arrow_points[0] = (ltk_point){ww - t->arrow_pad - mbw, wh / 2};
+ arrow_points[1] = (ltk_point){ww - t->arrow_pad - mbw - t->arrow_size, wh / 2 - t->arrow_size / 2};
+ arrow_points[2] = (ltk_point){ww - t->arrow_pad - mbw - t->arrow_size, wh / 2 + t->arrow_size / 2};
+ ltk_surface_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
+ }
+ if (rect.h < ideal_h) {
+ ltk_surface_fill_rect(s, &t->scroll_background, (ltk_rect){mbw, mbw, ww - mbw * 2, sz});
+ ltk_surface_fill_rect(s, &t->scroll_background, (ltk_rect){mbw, wh - sz - mbw, ww - mbw * 2, sz});
+ ltk_point arrow_points[3] = {
+ {ww / 2, t->arrow_pad + mbw},
+ {ww / 2 - t->arrow_size / 2, t->arrow_pad + mbw + t->arrow_size},
+ {ww / 2 + t->arrow_size / 2, t->arrow_pad + mbw + t->arrow_size}
+ };
+ ltk_surface_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
+ arrow_points[0] = (ltk_point){ww / 2, wh - t->arrow_pad - mbw};
+ arrow_points[1] = (ltk_point){ww / 2 - t->arrow_size / 2, wh - t->arrow_pad - mbw - t->arrow_size};
+ arrow_points[2] = (ltk_point){ww / 2 + t->arrow_size / 2, wh - t->arrow_pad - mbw - t->arrow_size};
+ ltk_surface_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
+ }
+ ltk_surface_draw_border(s, &t->menu_border, (ltk_rect){0, 0, ww, wh}, mbw, LTK_BORDER_ALL);
+
+ menu->widget.dirty = 0;
+}
+
+static void
+ltk_menu_get_max_scroll_offset(ltk_menu *menu, int *x_ret, int *y_ret) {
+ struct theme *theme = menu->is_submenu ? &submenu_theme : &menu_theme;
+ int extra_size = theme->arrow_size * 2 + theme->arrow_pad * 4;
+ *x_ret = 0;
+ *y_ret = 0;
+ if (menu->widget.rect.w < (int)menu->widget.ideal_w) {
+ *x_ret = menu->widget.ideal_w - (menu->widget.rect.w - extra_size);
+ }
+ if (menu->widget.rect.h < (int)menu->widget.ideal_h) {
+ *y_ret = menu->widget.ideal_h - (menu->widget.rect.h - extra_size);
+ }
+}
+
+static void
+ltk_menu_scroll(ltk_menu *menu, char t, char b, char l, char r, int step) {
+ int max_scroll_x, max_scroll_y;
+ ltk_menu_get_max_scroll_offset(menu, &max_scroll_x, &max_scroll_y);
+ double y_old = menu->y_scroll_offset;
+ double x_old = menu->x_scroll_offset;
+ if (t)
+ menu->y_scroll_offset -= step;
+ else if (b)
+ menu->y_scroll_offset += step;
+ else if (l)
+ menu->x_scroll_offset -= step;
+ else if (r)
+ menu->x_scroll_offset += step;
+ if (menu->x_scroll_offset < 0)
+ menu->x_scroll_offset = 0;
+ if (menu->y_scroll_offset < 0)
+ menu->y_scroll_offset = 0;
+ if (menu->x_scroll_offset > max_scroll_x)
+ menu->x_scroll_offset = max_scroll_x;
+ if (menu->y_scroll_offset > max_scroll_y)
+ menu->y_scroll_offset = max_scroll_y;
+ /* FIXME: sensible epsilon? */
+ if (fabs(x_old - menu->x_scroll_offset) < 0.01 ||
+ fabs(y_old - menu->y_scroll_offset) < 0.01) {
+ menu->widget.dirty = 1;
+ ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
+ }
+}
+
+/* FIXME: show scroll arrow disabled when nothing further */
+static void
+ltk_menu_scroll_callback(void *data) {
+ ltk_menu *menu = (ltk_menu *)data;
+ ltk_menu_scroll(
+ menu,
+ menu->scroll_top_hover, menu->scroll_bottom_hover,
+ menu->scroll_left_hover, menu->scroll_right_hover, 2
+ );
+}
+
+/* FIXME: HANDLE mouse scroll wheel! */
+
+static void
+stop_scrolling(ltk_menu *menu) {
+ menu->scroll_top_hover = 0;
+ menu->scroll_bottom_hover = 0;
+ menu->scroll_left_hover = 0;
+ menu->scroll_right_hover = 0;
+ if (menu->scroll_timer_id >= 0)
+ ltk_unregister_timer(menu->scroll_timer_id);
+}
+
+/* FIXME: should ideal_w, ideal_h just be int? */
+static size_t
+get_entry_at_point(ltk_menu *menu, int x, int y, ltk_rect *entry_rect_ret) {
+ struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
+ int arrow_size = t->arrow_size + t->arrow_pad * 2;
+ int mbw = t->menu_border_width;
+ int start_x = menu->widget.rect.x + mbw, end_x = menu->widget.rect.x + menu->widget.rect.w - mbw;
+ int start_y = menu->widget.rect.y + mbw, end_y = menu->widget.rect.y + menu->widget.rect.h - mbw;
+ if (menu->widget.rect.w < (int)menu->widget.ideal_w) {
+ start_x += arrow_size;
+ end_x -= arrow_size;
+ }
+ if (menu->widget.rect.h < (int)menu->widget.ideal_h) {
+ start_y += arrow_size;
+ end_y -= arrow_size;
+ }
+ if (!ltk_collide_rect((ltk_rect){start_x, start_y, end_x - start_x, end_y - start_y}, x, y))
+ return SIZE_MAX;
+
+ int bw_sub = t->compress_borders ? t->border_width : 0;
+ int cur_x = start_x - (int)menu->x_scroll_offset + t->pad;
+ int cur_y = start_y - (int)menu->y_scroll_offset + t->pad;
+ /* FIXME: could be optimized a bit */
+ for (size_t i = 0; i < menu->num_entries; i++) {
+ ltk_menuentry *e = &menu->entries[i];
+ int text_w, text_h;
+ ltk_text_line_get_size(e->text, &text_w, &text_h);
+ if (menu->is_submenu) {
+ int extra_size = e->submenu ? t->arrow_pad * 2 + t->arrow_size : 0;
+ int w = (int)menu->widget.ideal_w - t->pad * 2;
+ int h = MAX(text_h + t->text_pad * 2, extra_size) + t->border_width * 2;
+ if (x >= cur_x && x <= cur_x + w && y >= cur_y && y <= cur_y + h) {
+ if (entry_rect_ret) {
+ entry_rect_ret->x = cur_x;
+ entry_rect_ret->y = cur_y;
+ entry_rect_ret->w = w;
+ entry_rect_ret->h = h;
+ }
+ return i;
+ }
+ cur_y += h - bw_sub + t->pad;
+ } else {
+ int w = text_w + t->text_pad * 2 + t->border_width * 2;
+ int h = (int)menu->widget.ideal_h - t->pad * 2;
+ if (x >= cur_x && x <= cur_x + w && y >= cur_y && y <= cur_y + h) {
+ if (entry_rect_ret) {
+ entry_rect_ret->x = cur_x;
+ entry_rect_ret->y = cur_y;
+ entry_rect_ret->w = w;
+ entry_rect_ret->h = h;
+ }
+ return i;
+ }
+ cur_x += w - bw_sub + t->pad;
+ }
+ }
+ return SIZE_MAX;
+}
+
+/* FIXME: make sure timers are always destroyed when widget is destroyed */
+static int
+set_scroll_timer(ltk_menu *menu, int x, int y) {
+ if (!ltk_collide_rect(menu->widget.rect, x, y))
+ return 0;
+ int t = 0, b = 0, l = 0,r = 0;
+ struct theme *theme = menu->is_submenu ? &submenu_theme : &menu_theme;
+ int arrow_size = theme->arrow_size + theme->arrow_pad * 2;
+ if (menu->widget.rect.w < (int)menu->widget.ideal_w) {
+ if (x < menu->widget.rect.x + arrow_size)
+ l = 1;
+ else if (x > menu->widget.rect.x + menu->widget.rect.w - arrow_size)
+ r = 1;
+ }
+ if (menu->widget.rect.h < (int)menu->widget.ideal_h) {
+ if (y < menu->widget.rect.y + arrow_size)
+ t = 1;
+ else if (y > menu->widget.rect.y + menu->widget.rect.h - arrow_size)
+ b = 1;
+ }
+ if (t == menu->scroll_top_hover &&
+ b == menu->scroll_bottom_hover &&
+ l == menu->scroll_left_hover &&
+ r == menu->scroll_right_hover)
+ return 0;
+ stop_scrolling(menu);
+ menu->scroll_top_hover = t;
+ menu->scroll_bottom_hover = b;
+ menu->scroll_left_hover = l;
+ menu->scroll_right_hover = r;
+ ltk_menu_scroll_callback(menu);
+ menu->scroll_timer_id = ltk_register_timer(0, 300, <k_menu_scroll_callback, menu);
+ return 1;
+}
+
+static int
+ltk_menu_mouse_release(ltk_widget *self, XEvent event) {
+ ltk_menu *menu = (ltk_menu *)self;
+ size_t idx = get_entry_at_point(menu, event.xbutton.x, event.xbutton.y, NULL);
+ if (idx < menu->num_entries && idx == menu->pressed_entry) {
+ ltk_window_unregister_all_popups(self->window);
+ /* FIXME: give menu id and entry id */
+ ltk_queue_event(self->window, LTK_EVENT_MENU, menu->entries[idx].id, "menu_entry_click");
+ }
+ if (menu->pressed_entry < menu->num_entries && idx < menu->num_entries)
+ menu->active_entry = menu->pressed_entry;
+ else if (idx < menu->num_entries)
+ menu->active_entry = idx;
+ menu->pressed_entry = SIZE_MAX;
+ self->dirty = 1;
+ return 1;
+}
+
+static int
+ltk_menu_mouse_press(ltk_widget *self, XEvent event) {
+ ltk_menu *menu = (ltk_menu *)self;
+ size_t idx;
+ /* FIXME: configure scroll step */
+ switch (event.xbutton.button) {
+ case 1:
+ idx = get_entry_at_point(menu, event.xbutton.x, event.xbutton.y, NULL);
+ if (idx < menu->num_entries) {
+ menu->pressed_entry = idx;
+ self->dirty = 1;
+ }
+ break;
+ case 4:
+ ltk_menu_scroll(menu, 1, 0, 0, 0, 10);
+ handle_hover(menu, event.xbutton.x, event.xbutton.y);
+ break;
+ case 5:
+ ltk_menu_scroll(menu, 0, 1, 0, 0, 10);
+ handle_hover(menu, event.xbutton.x, event.xbutton.y);
+ break;
+ case 6:
+ ltk_menu_scroll(menu, 0, 0, 1, 0, 10);
+ handle_hover(menu, event.xbutton.x, event.xbutton.y);
+ break;
+ case 7:
+ ltk_menu_scroll(menu, 0, 0, 0, 1, 10);
+ handle_hover(menu, event.xbutton.x, event.xbutton.y);
+ break;
+ default:
+ break;
+ }
+ return 1;
+}
+
+static void
+ltk_menu_hide(ltk_widget *self) {
+ ltk_menu *menu = (ltk_menu *)self;
+ menu->active_entry = menu->pressed_entry = SIZE_MAX;
+ if (menu->scroll_timer_id >= 0)
+ ltk_unregister_timer(menu->scroll_timer_id);
+ menu->scroll_bottom_hover = menu->scroll_top_hover = 0;
+ menu->scroll_left_hover = menu->scroll_right_hover = 0;
+ ltk_window_unregister_popup(self->window, self);
+ ltk_window_invalidate_rect(self->window, self->rect);
+}
+
+/* FIXME: don't require passing rect */
+static void
+popup_active_menu(ltk_menu *menu, ltk_rect r) {
+ size_t idx = menu->active_entry;
+ if (idx >= menu->num_entries)
+ return;
+ int win_w = menu->widget.window->rect.w;
+ int win_h = menu->widget.window->rect.h;
+ if (menu->entries[idx].submenu) {
+ ltk_menu *submenu = menu->entries[idx].submenu;
+ int ideal_w = submenu->widget.ideal_w + 2;
+ int ideal_h = submenu->widget.ideal_h;
+ int x_final = 0, y_final = 0, w_final = ideal_w, h_final = ideal_h;
+ if (menu->is_submenu) {
+ int space_left = menu->widget.rect.x;
+ int space_right = win_w - (menu->widget.rect.x + menu->widget.rect.w);
+ int x_right = menu->widget.rect.x + menu->widget.rect.w;
+ int x_left = menu->widget.rect.x - ideal_w;
+ if (menu->was_opened_left) {
+ if (x_left >= 0) {
+ x_final = x_left;
+ submenu->was_opened_left = 1;
+ } else if (space_right >= ideal_w) {
+ x_final = x_right;
+ submenu->was_opened_left = 0;
+ } else {
+ x_final = 0;
+ if (win_w < ideal_w)
+ w_final = win_w;
+ submenu->was_opened_left = 1;
+ }
+ } else {
+ if (space_right >= ideal_w) {
+ x_final = x_right;
+ submenu->was_opened_left = 0;
+ } else if (space_left >= ideal_w) {
+ x_final = x_left;
+ submenu->was_opened_left = 1;
+ } else {
+ x_final = win_w - ideal_w;
+ if (x_final < 0) {
+ x_final = 0;
+ w_final = win_w;
+ }
+ submenu->was_opened_left = 0;
+ }
+ }
+ /* subtract padding and border width so the actual entries are at the right position */
+ y_final = r.y - submenu_theme.pad - submenu_theme.menu_border_width;
+ if (y_final + ideal_h > win_h)
+ y_final = win_h - ideal_h;
+ if (y_final < 0) {
+ y_final = 0;
+ h_final = win_h;
+ }
+ } else {
+ int space_top = menu->widget.rect.y;
+ int space_bottom = win_h - (menu->widget.rect.y + menu->widget.rect.h);
+ int y_top = menu->widget.rect.y - ideal_h;
+ int y_bottom = menu->widget.rect.y + menu->widget.rect.h;
+ if (space_top > space_bottom) {
+ y_final = y_top;
+ if (y_final < 0) {
+ y_final = 0;
+ h_final = menu->widget.rect.y;
+ }
+ } else {
+ y_final = y_bottom;
+ if (space_bottom < ideal_h)
+ h_final = space_bottom;
+ }
+ /* FIXME: maybe threshold so there's always at least a part of
+ the menu contents shown (instead of maybe just a few pixels) */
+ /* pathological case where window is way too small */
+ if (h_final <= 0) {
+ y_final = 0;
+ h_final = win_h;
+ }
+ x_final = r.x;
+ if (x_final + ideal_w > win_w)
+ x_final = win_w - ideal_w;
+ if (x_final < 0) {
+ x_final = 0;
+ w_final = win_w;
+ }
+ }
+ /* reset everything just in case */
+ submenu->x_scroll_offset = submenu->y_scroll_offset = 0;
+ submenu->active_entry = submenu->pressed_entry = SIZE_MAX;
+ submenu->scroll_top_hover = submenu->scroll_bottom_hover = 0;
+ submenu->scroll_left_hover = submenu->scroll_right_hover = 0;
+ submenu->widget.rect.x = x_final;
+ submenu->widget.rect.y = y_final;
+ submenu->widget.rect.w = w_final;
+ submenu->widget.rect.h = h_final;
+ ltk_surface_cache_request_surface_size(submenu->widget.surface_key, w_final, h_final);
+ submenu->widget.dirty = 1;
+ submenu->widget.hidden = 0;
+ ltk_window_register_popup(menu->widget.window, (ltk_widget *)submenu);
+ ltk_window_invalidate_rect(submenu->widget.window, submenu->widget.rect);
+ }
+}
+
+static void
+unpopup_active_entry(ltk_menu *menu) {
+ if (menu->active_entry >= menu->num_entries)
+ return;
+ ltk_menu *cur_menu = menu->entries[menu->active_entry].submenu;
+ menu->active_entry = SIZE_MAX;
+ while (cur_menu) {
+ ltk_menu *tmp = NULL;
+ if (cur_menu->active_entry < cur_menu->num_entries)
+ tmp = cur_menu->entries[cur_menu->active_entry].submenu;
+ ltk_menu_hide((ltk_widget *)cur_menu);
+ cur_menu = tmp;
+ }
+}
+
+static void
+handle_hover(ltk_menu *menu, int x, int y) {
+ if (set_scroll_timer(menu, x, y) || menu->pressed_entry < menu->num_entries)
+ return;
+ ltk_rect r;
+ size_t idx = get_entry_at_point(menu, x, y, &r);
+ if (idx >= menu->num_entries)
+ return;
+ ltk_menu *cur_submenu = menu->active_entry < menu->num_entries ? menu->entries[menu->active_entry].submenu : NULL;
+ if (idx != menu->active_entry) {
+ unpopup_active_entry(menu);
+ menu->active_entry = idx;
+ menu->widget.dirty = 1;
+ ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
+ popup_active_menu(menu, r);
+ } else if (cur_submenu && cur_submenu->widget.hidden) {
+ popup_active_menu(menu, r);
+ }
+}
+
+static int
+ltk_menu_motion_notify(ltk_widget *self, XEvent event) {
+ handle_hover((ltk_menu *)self, event.xmotion.x, event.xmotion.y);
+ return 1;
+}
+
+static int
+ltk_menu_mouse_enter(ltk_widget *self, XEvent event) {
+ handle_hover((ltk_menu *)self, event.xbutton.x, event.xbutton.y);
+ return 1;
+}
+
+static int
+ltk_menu_mouse_leave(ltk_widget *self, XEvent event) {
+ (void)event;
+ stop_scrolling((ltk_menu *)self);
+ return 1;
+}
+
+static ltk_menu *
+ltk_menu_create(ltk_window *window, const char *id, int is_submenu) {
+ ltk_menu *menu = ltk_malloc(sizeof(ltk_menu));
+ menu->widget.ideal_w = menu_theme.pad;
+ menu->widget.ideal_h = menu_theme.pad;
+ ltk_fill_widget_defaults(&menu->widget, id, window, &vtable, menu->widget.ideal_w, menu->widget.ideal_h);
+ menu->widget.dirty = 1;
+
+ menu->entries = NULL;
+ menu->num_entries = menu->num_alloc = 0;
+ menu->pressed_entry = menu->active_entry = SIZE_MAX;
+ menu->x_scroll_offset = menu->y_scroll_offset = 0;
+ menu->is_submenu = is_submenu;
+ menu->was_opened_left = 0;
+ menu->scroll_timer_id = -1;
+ menu->scroll_top_hover = menu->scroll_bottom_hover = 0;
+ menu->scroll_left_hover = menu->scroll_right_hover = 0;
+ /* FIXME: hide widget by default so recalc doesn't cause
+ unnecessary redrawing */
+ recalc_menu_size(menu);
+
+ return menu;
+}
+
+static ltk_menuentry *
+insert_entry(ltk_menu *menu, size_t idx) {
+ if (idx > menu->num_entries)
+ return NULL;
+ if (menu->num_entries == menu->num_alloc) {
+ menu->num_alloc = ideal_array_size(menu->num_alloc, menu->num_entries + 1);
+ menu->entries = ltk_reallocarray(menu->entries, menu->num_alloc, sizeof(ltk_menuentry));
+ }
+ memmove(
+ menu->entries + idx + 1,
+ menu->entries + idx,
+ sizeof(ltk_menuentry) * (menu->num_entries - idx)
+ );
+ if (menu->active_entry >= idx && menu->active_entry < menu->num_entries)
+ menu->active_entry++;
+ if (menu->pressed_entry >= idx && menu->pressed_entry < menu->num_entries)
+ menu->pressed_entry++;
+ menu->num_entries++;
+ return &menu->entries[idx];
+}
+
+/* FIXME: handle child_size_change - what if something added while menu shown?
+ -> I guess the scroll arrows will just be added when that's the case - it's
+ kind of pointless to spend time implementing the logic for recalculating
+ all submenu positions and sizes when it's such a corner case */
+static void
+recalc_menu_size(ltk_menu *menu) {
+ struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
+ menu->widget.ideal_w = menu->widget.ideal_h = t->pad + t->menu_border_width * 2;
+ ltk_menuentry *e;
+ int text_w, text_h, bw;
+ for (size_t i = 0; i < menu->num_entries; i++) {
+ e = &menu->entries[i];
+ ltk_text_line_get_size(e->text, &text_w, &text_h);
+ bw = t->border_width * 2;
+ if (t->compress_borders && i != 0)
+ bw = t->border_width;
+ if (menu->is_submenu) {
+ int extra_size = e->submenu ? t->arrow_pad * 2 + t->arrow_size : 0;
+ menu->widget.ideal_w =
+ MAX(text_w + extra_size + (t->pad + t->text_pad + t->border_width + t->menu_border_width) * 2, (int)menu->widget.ideal_w);
+ menu->widget.ideal_h += MAX(text_h + t->text_pad * 2, extra_size) + bw + t->pad;
+ } else {
+ menu->widget.ideal_h =
+ MAX(text_h + (t->pad + t->text_pad + t->border_width + t->menu_border_width) * 2, (int)menu->widget.ideal_h);
+ menu->widget.ideal_w += text_w + t->text_pad * 2 + bw + t->pad;
+ }
+ }
+ if (!menu->widget.hidden && menu->widget.parent && menu->widget.parent->vtable->child_size_change) {
+ menu->widget.parent->vtable->child_size_change(menu->widget.parent, (ltk_widget *)menu);
+ }
+ menu->widget.dirty = 1;
+ if (!menu->widget.hidden)
+ ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
+}
+
+static ltk_menuentry *
+ltk_menu_insert_entry(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, size_t idx, char **errstr) {
+ if (submenu && submenu->widget.parent) {
+ *errstr = "Submenu already part of other menu.\n";
+ return NULL;
+ }
+ ltk_menuentry *e = insert_entry(menu, idx);
+ if (!e) {
+ *errstr = "Unable to insert menu entry at given index.\n";
+ return NULL;
+ }
+ e->id = ltk_strdup(id);
+ ltk_window *w = menu->widget.window;
+ /* FIXME: pass const text */
+ e->text = ltk_text_line_create(w->text_context, w->theme.font_size, (char *)text, 0, -1);
+ e->submenu = submenu;
+ if (submenu)
+ submenu->widget.parent = (ltk_widget *)menu;
+ e->disabled = 0;
+ recalc_menu_size(menu);
+ menu->widget.dirty = 1;
+ return e;
+}
+
+static ltk_menuentry *
+ltk_menu_add_entry(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, char **errstr) {
+ return ltk_menu_insert_entry(menu, id, text, submenu, menu->num_entries, errstr);
+}
+
+/* FIXME: maybe allow any menu and just change is_submenu (also need to recalculate size then) */
+static ltk_menuentry *
+ltk_menu_insert_submenu(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, size_t idx, char **errstr) {
+ if (!submenu->is_submenu) {
+ *errstr = "Not a submenu.\n";
+ return NULL;
+ }
+ return ltk_menu_insert_entry(menu, id, text, submenu, idx, errstr);
+}
+
+static ltk_menuentry *
+ltk_menu_add_submenu(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, char **errstr) {
+ return ltk_menu_insert_submenu(menu, id, text, submenu, menu->num_entries, errstr);
+}
+
+static void
+shrink_entries(ltk_menu *menu) {
+ size_t new_alloc = ideal_array_size(menu->num_alloc, menu->num_entries);
+ if (new_alloc != menu->num_alloc) {
+ menu->entries = ltk_reallocarray(menu->entries, new_alloc, sizeof(ltk_menuentry));
+ menu->num_alloc = new_alloc;
+ }
+}
+
+static int
+ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx, int shallow, char **errstr) {
+ if (idx >= menu->num_entries) {
+ *errstr = "Invalid menu entry index.\n";
+ return 1;
+ }
+ ltk_menuentry *e = &menu->entries[idx];
+ ltk_free(e->id);
+ ltk_text_line_destroy(e->text);
+ if (e->submenu) {
+ e->submenu->widget.parent = NULL;
+ if (!shallow)
+ ltk_menu_destroy((ltk_widget *)e->submenu, shallow);
+ }
+ memmove(
+ menu->entries + idx,
+ menu->entries + idx + 1,
+ sizeof(ltk_menuentry) * (menu->num_entries - idx - 1)
+ );
+ menu->num_entries--;
+ shrink_entries(menu);
+ recalc_menu_size(menu);
+ return 0;
+}
+
+static int
+ltk_menu_remove_entry_id(ltk_menu *menu, const char *id, int shallow, char **errstr) {
+ size_t idx = get_entry_with_id(menu, id);
+ if (idx >= menu->num_entries) {
+ *errstr = "Invalid menu entry id.\n";
+ return 1;
+ }
+ ltk_menu_remove_entry_index(menu, idx, shallow, errstr);
+ return 0;
+}
+
+static int
+ltk_menu_remove_all_entries(ltk_menu *menu, int shallow, char **errstr) {
+ (void)errstr; /* FIXME: why is errstr given at all? */
+ for (size_t i = 0; i < menu->num_entries; i++) {
+ ltk_menuentry *e = &menu->entries[i];
+ ltk_free(e->id);
+ ltk_text_line_destroy(e->text);
+ if (e->submenu) {
+ e->submenu->widget.parent = NULL;
+ if (!shallow)
+ ltk_menu_destroy((ltk_widget *)e->submenu, shallow);
+ }
+ }
+ menu->num_entries = menu->num_alloc = 0;
+ ltk_free(menu->entries);
+ menu->entries = NULL;
+ recalc_menu_size(menu);
+ return 0;
+}
+
+/* FIXME: how to get rid of duplicate IDs? */
+
+static size_t
+get_entry_with_id(ltk_menu *menu, const char *id) {
+ for (size_t i = 0; i < menu->num_entries; i++) {
+ if (!strcmp(id, menu->entries[i].id))
+ return i;
+ }
+ return SIZE_MAX;
+}
+
+/* FIXME: unregister from window popups? */
+static int
+ltk_menu_detach_submenu_from_entry_id(ltk_menu *menu, const char *id, char **errstr) {
+ size_t idx = get_entry_with_id(menu, id);
+ if (idx >= menu->num_entries) {
+ *errstr = "Invalid menu entry id.\n";
+ return 1;
+ }
+ /* FIXME: error if submenu already NULL? */
+ menu->entries[idx].submenu = NULL;
+ recalc_menu_size(menu);
+ return 0;
+}
+
+static int
+ltk_menu_detach_submenu_from_entry_index(ltk_menu *menu, size_t idx, char **errstr) {
+ if (idx >= menu->num_entries) {
+ *errstr = "Invalid menu entry index.\n";
+ return 1;
+ }
+ menu->entries[idx].submenu = NULL;
+ recalc_menu_size(menu);
+ return 0;
+}
+
+static int
+ltk_menu_disable_entry_index(ltk_menu *menu, size_t idx, char **errstr) {
+ if (idx >= menu->num_entries) {
+ *errstr = "Invalid menu entry index.\n";
+ return 1;
+ }
+ menu->entries[idx].disabled = 1;
+ menu->widget.dirty = 1;
+ if (!menu->widget.hidden)
+ ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
+ return 0;
+}
+
+static int
+ltk_menu_disable_entry_id(ltk_menu *menu, const char *id, char **errstr) {
+ size_t idx = get_entry_with_id(menu, id);
+ if (idx >= menu->num_entries) {
+ *errstr = "Invalid menu entry id.\n";
+ return 1;
+ }
+ menu->entries[idx].disabled = 1;
+ menu->widget.dirty = 1;
+ if (!menu->widget.hidden)
+ ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
+ return 0;
+}
+
+static int
+ltk_menu_disable_all_entries(ltk_menu *menu, char **errstr) {
+ (void)errstr;
+ for (size_t i = 0; i < menu->num_entries; i++) {
+ menu->entries[i].disabled = 1;
+ }
+ menu->widget.dirty = 1;
+ if (!menu->widget.hidden)
+ ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
+ return 0;
+}
+
+static int
+ltk_menu_enable_entry_index(ltk_menu *menu, size_t idx, char **errstr) {
+ if (idx >= menu->num_entries) {
+ *errstr = "Invalid menu entry index.\n";
+ return 1;
+ }
+ menu->entries[idx].disabled = 0;
+ menu->widget.dirty = 1;
+ if (!menu->widget.hidden)
+ ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
+ return 0;
+}
+
+static int
+ltk_menu_enable_entry_id(ltk_menu *menu, const char *id, char **errstr) {
+ size_t idx = get_entry_with_id(menu, id);
+ if (idx >= menu->num_entries) {
+ *errstr = "Invalid menu entry id.\n";
+ return 1;
+ }
+ menu->entries[idx].disabled = 0;
+ menu->widget.dirty = 1;
+ if (!menu->widget.hidden)
+ ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
+ return 0;
+}
+
+static int
+ltk_menu_enable_all_entries(ltk_menu *menu, char **errstr) {
+ (void)errstr;
+ for (size_t i = 0; i < menu->num_entries; i++) {
+ menu->entries[i].disabled = 0;
+ }
+ menu->widget.dirty = 1;
+ if (!menu->widget.hidden)
+ ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
+ return 0;
+}
+
+static void
+ltk_menu_destroy(ltk_widget *self, int shallow) {
+ ltk_menu *menu = (ltk_menu *)self;
+ char *errstr;
+ if (self->parent && self->parent->vtable->remove_child) {
+ self->parent->vtable->remove_child(
+ self->window, self, self->parent, &errstr
+ );
+ }
+ if (!menu) {
+ ltk_warn("Tried to destroy NULL menu.\n");
+ return;
+ }
+ /* FIXME: this should be generic part of widget */
+ ltk_surface_cache_release_key(self->surface_key);
+ if (menu->scroll_timer_id >= 0)
+ ltk_unregister_timer(menu->scroll_timer_id);
+ ltk_menu_remove_all_entries(menu, shallow, NULL);
+ ltk_window_unregister_popup(self->window, self);
+ /* FIXME: what to do on error here? */
+ /* FIXME: maybe unregister popup in ltk_remove_widget? */
+ ltk_remove_widget(self->id);
+ ltk_free(self->id);
+ ltk_free(menu);
+}
+
+/* FIXME: simplify command handling to avoid all this boilerplate */
+/* TODO: get-index-for-id */
+
+/* [sub]menu <menu id> create */
+static int
+ltk_menu_cmd_create(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr) {
+ ltk_menu *menu;
+ if (num_tokens != 3) {
+ *errstr = "Invalid number of arguments.\n";
+ return 1;
+ }
+ if (!ltk_widget_id_free(tokens[1])) {
+ *errstr = "Widget ID already taken.\n";
+ return 1;
+ }
+ if (!strcmp(tokens[0], "menu")) {
+ menu = ltk_menu_create(window, tokens[1], 0);
+ } else {
+ menu = ltk_menu_create(window, tokens[1], 1);
+ }
+ ltk_set_widget((ltk_widget *)menu, tokens[1]);
+
+ return 0;
+}
+
+/* menu <menu id> insert-entry <entry id> <entry text> <index> */
+static int
+ltk_menu_cmd_insert_entry(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr) {
+ (void)window;
+ ltk_menu *menu;
+ const char *errstr_num;
+ if (num_tokens != 6) {
+ *errstr = "Invalid number of arguments.\n";
+ return 1;
+ }
+ /* FIXME: actually use this errstr */
+ menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
+ if (!menu) {
+ *errstr = "Invalid widget ID.\n";
+ return 1;
+ }
+ size_t idx = (size_t)ltk_strtonum(tokens[5], 0, (long long)menu->num_entries, &errstr_num);
+ if (errstr_num) {
+ *errstr = "Invalid index.\n";
+ return 1;
+ }
+ if (!ltk_menu_insert_entry(menu, tokens[3], tokens[4], NULL, idx, errstr))
+ return 1;
+
+ return 0;
+}
+
+/* menu <menu id> add-entry <entry id> <entry text> */
+static int
+ltk_menu_cmd_add_entry(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr) {
+ (void)window;
+ ltk_menu *menu;
+ if (num_tokens != 5) {
+ *errstr = "Invalid number of arguments.\n";
+ return 1;
+ }
+ menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
+ if (!menu) {
+ *errstr = "Invalid widget ID.\n";
+ return 1;
+ }
+ if (!ltk_menu_add_entry(menu, tokens[3], tokens[4], NULL, errstr))
+ return 1;
+
+ return 0;
+}
+
+/* menu <menu id> insert-submenu <entry id> <entry text> <submenu id> <index> */
+static int
+ltk_menu_cmd_insert_submenu(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr) {
+ (void)window;
+ ltk_menu *menu, *submenu;
+ const char *errstr_num;
+ if (num_tokens != 7) {
+ *errstr = "Invalid number of arguments.\n";
+ return 1;
+ }
+ menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
+ submenu = (ltk_menu *)ltk_get_widget(tokens[5], LTK_MENU, errstr);
+ if (!menu || !submenu) {
+ *errstr = "Invalid widget ID.\n";
+ return 1;
+ }
+ size_t idx = (size_t)ltk_strtonum(tokens[6], 0, (long long)menu->num_entries, &errstr_num);
+ if (errstr_num) {
+ *errstr = "Invalid index.\n";
+ return 1;
+ }
+ if (!ltk_menu_insert_submenu(menu, tokens[3], tokens[4], submenu, idx, errstr))
+ return 1;
+
+ return 0;
+}
+
+/* menu <menu id> add-submenu <entry id> <entry text> <submenu id> */
+static int
+ltk_menu_cmd_add_submenu(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr) {
+ (void)window;
+ ltk_menu *menu, *submenu;
+ if (num_tokens != 6) {
+ *errstr = "Invalid number of arguments.\n";
+ return 1;
+ }
+ menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
+ submenu = (ltk_menu *)ltk_get_widget(tokens[5], LTK_MENU, errstr);
+ if (!menu || !submenu) {
+ *errstr = "Invalid widget ID.\n";
+ return 1;
+ }
+ if (!ltk_menu_add_submenu(menu, tokens[3], tokens[4], submenu, errstr))
+ return 1;
+
+ return 0;
+}
+
+/* menu <menu id> remove-entry-index <entry index> [shallow|deep] */
+static int
+ltk_menu_cmd_remove_entry_index(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr) {
+ (void)window;
+ ltk_menu *menu;
+ const char *errstr_num;
+ if (num_tokens != 4 && num_tokens != 5) {
+ *errstr = "Invalid number of arguments.\n";
+ return 1;
+ }
+ menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
+ if (!menu) {
+ *errstr = "Invalid widget ID.\n";
+ return 1;
+ }
+ size_t idx = (size_t)ltk_strtonum(tokens[3], 0, (long long)menu->num_entries, &errstr_num);
+ if (errstr_num) {
+ *errstr = "Invalid index.\n";
+ return 1;
+ }
+ int shallow = 1;
+ if (num_tokens == 5) {
+ if (!strcmp(tokens[4], "shallow")) {
+ /* NOP */
+ } else if (!strcmp(tokens[4], "deep")) {
+ shallow = 0;
+ } else {
+ *errstr = "Invalid shallow specifier.\n";
+ return 1;
+ }
+ }
+ if (!ltk_menu_remove_entry_index(menu, idx, shallow, errstr))
+ return 1;
+
+ return 0;
+}
+
+/* menu <menu id> remove-entry-id <entry id> [shallow|deep] */
+static int
+ltk_menu_cmd_remove_entry_id(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr) {
+ (void)window;
+ ltk_menu *menu;
+ if (num_tokens != 4 && num_tokens != 5) {
+ *errstr = "Invalid number of arguments.\n";
+ return 1;
+ }
+ menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
+ if (!menu) {
+ *errstr = "Invalid widget ID.\n";
+ return 1;
+ }
+ int shallow = 1;
+ if (num_tokens == 5) {
+ if (!strcmp(tokens[4], "shallow")) {
+ /* NOP */
+ } else if (!strcmp(tokens[4], "deep")) {
+ shallow = 0;
+ } else {
+ *errstr = "Invalid shallow specifier.\n";
+ return 1;
+ }
+ }
+ if (!ltk_menu_remove_entry_id(menu, tokens[3], shallow, errstr))
+ return 1;
+
+ return 0;
+}
+
+/* menu <menu id> remove-all-entries [shallow|deep] */
+static int
+ltk_menu_cmd_remove_all_entries(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr) {
+ (void)window;
+ ltk_menu *menu;
+ if (num_tokens != 3 && num_tokens != 4) {
+ *errstr = "Invalid number of arguments.\n";
+ return 1;
+ }
+ menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
+ if (!menu) {
+ *errstr = "Invalid widget ID.\n";
+ return 1;
+ }
+ int shallow = 1;
+ if (num_tokens == 4) {
+ if (!strcmp(tokens[3], "shallow")) {
+ /* NOP */
+ } else if (!strcmp(tokens[3], "deep")) {
+ shallow = 0;
+ } else {
+ *errstr = "Invalid shallow specifier.\n";
+ return 1;
+ }
+ }
+ if (!ltk_menu_remove_all_entries(menu, shallow, errstr))
+ return 1;
+
+ return 0;
+}
+
+/* menu <menu id> detach-submenu-from-entry-id <entry id> */
+static int
+ltk_menu_cmd_detach_submenu_from_entry_id(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr) {
+ (void)window;
+ ltk_menu *menu;
+ if (num_tokens != 4) {
+ *errstr = "Invalid number of arguments.\n";
+ return 1;
+ }
+ menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
+ if (!menu) {
+ *errstr = "Invalid widget ID.\n";
+ return 1;
+ }
+
+ if (!ltk_menu_detach_submenu_from_entry_id(menu, tokens[3], errstr))
+ return 1;
+
+ return 0;
+}
+
+/* menu <menu id> detach-submenu-from-entry-index <entry index> */
+static int
+ltk_menu_cmd_detach_submenu_from_entry_index(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr) {
+ (void)window;
+ ltk_menu *menu;
+ const char *errstr_num;
+ if (num_tokens != 4) {
+ *errstr = "Invalid number of arguments.\n";
+ return 1;
+ }
+ menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
+ if (!menu) {
+ *errstr = "Invalid widget ID.\n";
+ return 1;
+ }
+ size_t idx = (size_t)ltk_strtonum(tokens[3], 0, (long long)menu->num_entries, &errstr_num);
+ if (errstr_num) {
+ *errstr = "Invalid index.\n";
+ return 1;
+ }
+
+ if (!ltk_menu_detach_submenu_from_entry_index(menu, idx, errstr))
+ return 1;
+
+ return 0;
+}
+
+/* menu <menu id> enable-entry-index <entry index> */
+static int
+ltk_menu_cmd_enable_entry_index(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr) {
+ (void)window;
+ ltk_menu *menu;
+ const char *errstr_num;
+ if (num_tokens != 4) {
+ *errstr = "Invalid number of arguments.\n";
+ return 1;
+ }
+ menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
+ if (!menu) {
+ *errstr = "Invalid widget ID.\n";
+ return 1;
+ }
+ size_t idx = (size_t)ltk_strtonum(tokens[3], 0, (long long)menu->num_entries, &errstr_num);
+ if (errstr_num) {
+ *errstr = "Invalid index.\n";
+ return 1;
+ }
+
+ if (!ltk_menu_enable_entry_index(menu, idx, errstr))
+ return 1;
+
+ return 0;
+}
+
+/* menu <menu id> enable-entry-id <entry id> */
+static int
+ltk_menu_cmd_enable_entry_id(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr) {
+ (void)window;
+ ltk_menu *menu;
+ if (num_tokens != 4) {
+ *errstr = "Invalid number of arguments.\n";
+ return 1;
+ }
+ menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
+ if (!menu) {
+ *errstr = "Invalid widget ID.\n";
+ return 1;
+ }
+
+ if (!ltk_menu_enable_entry_id(menu, tokens[3], errstr))
+ return 1;
+
+ return 0;
+}
+
+/* menu <menu id> enable-all-entries */
+static int
+ltk_menu_cmd_enable_all_entries(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr) {
+ (void)window;
+ ltk_menu *menu;
+ if (num_tokens != 3) {
+ *errstr = "Invalid number of arguments.\n";
+ return 1;
+ }
+ menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
+ if (!menu) {
+ *errstr = "Invalid widget ID.\n";
+ return 1;
+ }
+
+ if (!ltk_menu_enable_all_entries(menu, errstr))
+ return 1;
+
+ return 0;
+}
+
+/* menu <menu id> disable-entry-index <entry index> */
+static int
+ltk_menu_cmd_disable_entry_index(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr) {
+ (void)window;
+ ltk_menu *menu;
+ const char *errstr_num;
+ if (num_tokens != 4) {
+ *errstr = "Invalid number of arguments.\n";
+ return 1;
+ }
+ menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
+ if (!menu) {
+ *errstr = "Invalid widget ID.\n";
+ return 1;
+ }
+ size_t idx = (size_t)ltk_strtonum(tokens[3], 0, (long long)menu->num_entries, &errstr_num);
+ if (errstr_num) {
+ *errstr = "Invalid index.\n";
+ return 1;
+ }
+
+ if (!ltk_menu_disable_entry_index(menu, idx, errstr))
+ return 1;
+
+ return 0;
+}
+
+/* menu <menu id> disable-entry-id <entry id> */
+static int
+ltk_menu_cmd_disable_entry_id(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr) {
+ (void)window;
+ ltk_menu *menu;
+ if (num_tokens != 4) {
+ *errstr = "Invalid number of arguments.\n";
+ return 1;
+ }
+ menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
+ if (!menu) {
+ *errstr = "Invalid widget ID.\n";
+ return 1;
+ }
+
+ if (!ltk_menu_disable_entry_id(menu, tokens[3], errstr))
+ return 1;
+
+ return 0;
+}
+
+/* menu <menu id> disable-all-entries */
+static int
+ltk_menu_cmd_disable_all_entries(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr) {
+ (void)window;
+ ltk_menu *menu;
+ if (num_tokens != 3) {
+ *errstr = "Invalid number of arguments.\n";
+ return 1;
+ }
+ menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
+ if (!menu) {
+ *errstr = "Invalid widget ID.\n";
+ return 1;
+ }
+
+ if (!ltk_menu_disable_all_entries(menu, errstr))
+ return 1;
+
+ return 0;
+}
+
+/* FIXME: binary search for command handler */
+/* FIXME: distinguish between menu/submenu in commands other than create? */
+/* menu <menu id> <command> ... */
+int
+ltk_menu_cmd(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr) {
+ if (num_tokens < 3) {
+ *errstr = "Invalid number of arguments.\n";
+ return 1;
+ }
+ if (strcmp(tokens[2], "create") == 0) {
+ return ltk_menu_cmd_create(window, tokens, num_tokens, errstr);
+ } else if (strcmp(tokens[2], "insert-entry") == 0) {
+ return ltk_menu_cmd_insert_entry(window, tokens, num_tokens, errstr);
+ } else if (strcmp(tokens[2], "add-entry") == 0) {
+ return ltk_menu_cmd_add_entry(window, tokens, num_tokens, errstr);
+ } else if (strcmp(tokens[2], "insert-submenu") == 0) {
+ return ltk_menu_cmd_insert_submenu(window, tokens, num_tokens, errstr);
+ } else if (strcmp(tokens[2], "add-submenu") == 0) {
+ return ltk_menu_cmd_add_submenu(window, tokens, num_tokens, errstr);
+ } else if (strcmp(tokens[2], "remove-entry-index") == 0) {
+ return ltk_menu_cmd_remove_entry_index(window, tokens, num_tokens, errstr);
+ } else if (strcmp(tokens[2], "remove-entry-id") == 0) {
+ return ltk_menu_cmd_remove_entry_id(window, tokens, num_tokens, errstr);
+ } else if (strcmp(tokens[2], "remove-all-entries") == 0) {
+ return ltk_menu_cmd_remove_all_entries(window, tokens, num_tokens, errstr);
+ } else if (strcmp(tokens[2], "detach-submenu-from-entry-id") == 0) {
+ return ltk_menu_cmd_detach_submenu_from_entry_id(window, tokens, num_tokens, errstr);
+ } else if (strcmp(tokens[2], "detach-submenu-from-entry-index") == 0) {
+ return ltk_menu_cmd_detach_submenu_from_entry_index(window, tokens, num_tokens, errstr);
+ } else if (strcmp(tokens[2], "disable-entry-index") == 0) {
+ return ltk_menu_cmd_disable_entry_index(window, tokens, num_tokens, errstr);
+ } else if (strcmp(tokens[2], "disable-entry-id") == 0) {
+ return ltk_menu_cmd_disable_entry_id(window, tokens, num_tokens, errstr);
+ } else if (strcmp(tokens[2], "disable-all-entries") == 0) {
+ return ltk_menu_cmd_disable_all_entries(window, tokens, num_tokens, errstr);
+ } else if (strcmp(tokens[2], "enable-entry-index") == 0) {
+ return ltk_menu_cmd_enable_entry_index(window, tokens, num_tokens, errstr);
+ } else if (strcmp(tokens[2], "enable-entry-id") == 0) {
+ return ltk_menu_cmd_enable_entry_id(window, tokens, num_tokens, errstr);
+ } else if (strcmp(tokens[2], "enable-all-entries") == 0) {
+ return ltk_menu_cmd_enable_all_entries(window, tokens, num_tokens, errstr);
+ } else {
+ *errstr = "Invalid command.\n";
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/src/menu.h b/src/menu.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2022 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_MENU_H_
+#define _LTK_MENU_H_
+
+#include "ltk.h"
+#include "text.h"
+#include "widget.h"
+
+/* TODO: implement scrolling */
+
+typedef struct ltk_menuentry ltk_menuentry;
+
+typedef struct {
+ ltk_widget widget;
+ ltk_menuentry *entries;
+ size_t num_entries;
+ size_t num_alloc;
+ size_t pressed_entry;
+ size_t active_entry;
+ double x_scroll_offset;
+ double y_scroll_offset;
+ int scroll_timer_id;
+ char is_submenu;
+ char was_opened_left;
+ char scroll_top_hover;
+ char scroll_bottom_hover;
+ char scroll_left_hover;
+ char scroll_right_hover;
+} ltk_menu;
+
+struct ltk_menuentry {
+ char *id;
+ ltk_text_line *text;
+ ltk_menu *submenu;
+ int disabled;
+};
+
+void ltk_menu_setup_theme_defaults(ltk_window *window);
+void ltk_menu_ini_handler(ltk_window *window, const char *prop, const char *value);
+void ltk_submenu_ini_handler(ltk_window *window, const char *prop, const char *value);
+
+int ltk_menu_cmd(
+ ltk_window *window,
+ char **tokens,
+ size_t num_tokens,
+ char **errstr
+);
+
+#endif /* _LTK_MENU_H_ */
diff --git a/src/scrollbar.c b/src/scrollbar.c
@@ -1,3 +1,4 @@
+/* FIXME: make scrollbar a "real" widget that is also in widget hash */
/*
* Copyright (c) 2021, 2022 lumidify <nobody@lumidify.org>
*
@@ -31,6 +32,8 @@
#include "util.h"
#include "scrollbar.h"
+#define MAX_SCROLLBAR_WIDTH 100 /* completely arbitrary */
+
static void ltk_scrollbar_draw(ltk_widget *self, ltk_rect clip);
static int ltk_scrollbar_mouse_press(ltk_widget *self, XEvent event);
static int ltk_scrollbar_motion_notify(ltk_widget *self, XEvent event);
@@ -59,44 +62,43 @@ static struct {
void
ltk_scrollbar_setup_theme_defaults(ltk_window *window) {
theme.size = 15;
- ltk_color_create(window->dpy, window->screen, window->cm,
- "#000000", &theme.bg_normal);
- ltk_color_create(window->dpy, window->screen, window->cm,
- "#555555", &theme.bg_disabled);
- ltk_color_create(window->dpy, window->screen, window->cm,
- "#113355", &theme.fg_normal);
- ltk_color_create(window->dpy, window->screen, window->cm,
- "#738194", &theme.fg_active);
- ltk_color_create(window->dpy, window->screen, window->cm,
- "#113355", &theme.fg_pressed);
- ltk_color_create(window->dpy, window->screen, window->cm,
- "#292929", &theme.fg_disabled);
+ /* FIXME: error checking - but if these fail, there is probably a bigger
+ problem, so it might be best to just die completely */
+ ltk_color_create(window->dpy, window->screen, window->cm, "#000000", &theme.bg_normal);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#555555", &theme.bg_disabled);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &theme.fg_normal);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#738194", &theme.fg_active);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &theme.fg_pressed);
+ ltk_color_create(window->dpy, window->screen, window->cm, "#292929", &theme.fg_disabled);
}
void
ltk_scrollbar_ini_handler(ltk_window *window, const char *prop, const char *value) {
+ const char *errstr;
if (strcmp(prop, "size") == 0) {
- theme.size = atoi(value); /* FIXME: proper strtonum */
+ theme.size = ltk_strtonum(value, 1, MAX_SCROLLBAR_WIDTH, &errstr);
+ if (errstr)
+ ltk_warn("Invalid scrollbar size '%s': %s.\n", value, errstr);
} else if (strcmp(prop, "bg") == 0) {
- ltk_color_create(window->dpy, window->screen, window->cm,
- value, &theme.bg_normal);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.bg_normal))
+ ltk_warn("Error setting scrollbar background color to '%s'.\n", value);
} else if (strcmp(prop, "bg_disabled") == 0) {
- ltk_color_create(window->dpy, window->screen, window->cm,
- value, &theme.bg_disabled);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.bg_disabled))
+ ltk_warn("Error setting scrollbar disabled background color to '%s'.\n", value);
} else if (strcmp(prop, "fg") == 0) {
- ltk_color_create(window->dpy, window->screen, window->cm,
- value, &theme.fg_normal);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.fg_normal))
+ ltk_warn("Error setting scrollbar foreground color to '%s'.\n", value);
} else if (strcmp(prop, "fg_active") == 0) {
- ltk_color_create(window->dpy, window->screen, window->cm,
- value, &theme.fg_active);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.fg_active))
+ ltk_warn("Error setting scrollbar active foreground color to '%s'.\n", value);
} else if (strcmp(prop, "fg_pressed") == 0) {
- ltk_color_create(window->dpy, window->screen, window->cm,
- value, &theme.fg_pressed);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.fg_pressed))
+ ltk_warn("Error setting scrollbar pressed foreground color to '%s'.\n", value);
} else if (strcmp(prop, "fg_disabled") == 0) {
- ltk_color_create(window->dpy, window->screen, window->cm,
- value, &theme.fg_disabled);
+ if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.fg_disabled))
+ ltk_warn("Error setting scrollbar disabled foreground color to '%s'.\n", value);
} else {
- ltk_warn("Unknown property \"%s\" for scrollbar style.\n", prop);
+ ltk_warn("Unknown property '%s' for scrollbar style.\n", prop);
}
}
@@ -262,7 +264,12 @@ ltk_scrollbar_create(ltk_window *window, ltk_orientation orient, void (*callback
static void
ltk_scrollbar_destroy(ltk_widget *self, int shallow) {
(void)shallow;
+ char *errstr;
+ if (self->parent && self->parent->vtable->remove_child) {
+ self->parent->vtable->remove_child(
+ self->window, self, self->parent, &errstr
+ );
+ }
ltk_surface_cache_release_key(self->surface_key);
- ltk_scrollbar *scrollbar = (ltk_scrollbar *)self;
- ltk_free(scrollbar);
+ ltk_free(self);
}
diff --git a/src/strtonum.c b/src/strtonum.c
@@ -1,4 +1,6 @@
-/* $OpenBSD: strtonum.c,v 1.8 2015/09/13 08:31:48 guenther Exp $ */
+/* Note: Taken from OpenBSD:
+ * $OpenBSD: strtonum.c,v 1.8 2015/09/13 08:31:48 guenther Exp $
+ */
/*
* Copyright (c) 2004 Ted Unangst and Todd Miller
@@ -17,8 +19,6 @@
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* #ifndef __OpenBSD__ */
-
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
@@ -28,9 +28,9 @@
#define TOOLARGE 3
long long
-strtonum(const char *numstr, long long minval, long long maxval,
- const char **errstrp)
-{
+ltk_strtonum(
+ const char *numstr, long long minval,
+ long long maxval, const char **errstrp) {
long long ll = 0;
int error = 0;
char *ep;
@@ -65,7 +65,3 @@ strtonum(const char *numstr, long long minval, long long maxval,
return (ll);
}
-/* FIXME: What does this do? - lumidify */
-/* DEF_WEAK(strtonum); */
-
-/* #endif */
diff --git a/src/text.h b/src/text.h
@@ -36,6 +36,11 @@ void ltk_text_line_get_size(ltk_text_line *tl, int *w, int *h);
void ltk_text_line_destroy(ltk_text_line *tl);
/* Draw the entire line to a surface. */
+/* FIXME: Some widgets rely on this to not fail when negative coordinates are given or
+ the text goes outside of the surface boundaries - in the stb backend, this is taken
+ into account and the pango-xft backend doesn't *seem* to have any problems with it,
+ but I don't know if that's guaranteed. Proper clipping would be better, but Pango
+ can't do that. */
void ltk_text_line_draw(ltk_text_line *tl, ltk_surface *s, ltk_color *color, int x, int y);
/* Get the smallest rectangle of the line that can be drawn while covering 'clip'.
diff --git a/src/text_pango.c b/src/text_pango.c
@@ -48,20 +48,23 @@ struct ltk_text_context {
char *default_font;
};
-void
-ltk_text_context_create(ltk_window *window, const char *default_font) {
+ltk_text_context *
+ltk_text_context_create(ltk_window *window, char *default_font) {
ltk_text_context *ctx = ltk_malloc(sizeof(ltk_text_context));
ctx->window = window;
ctx->fontmap = pango_xft_get_font_map(window->dpy, window->screen);
ctx->context = pango_font_map_create_context(ctx->fontmap);
ctx->default_font = ltk_strdup(default_font);
+ return ctx;
}
void
ltk_text_context_destroy(ltk_text_context *ctx) {
ltk_free(ctx->default_font);
+ /* FIXME: if both are unref'd, there is a segfault - what is
+ the normal thing to do here? */
g_object_unref(ctx->fontmap);
- g_object_unref(ctx->context);
+ /*g_object_unref(ctx->context);*/
ltk_free(ctx);
}
diff --git a/src/text_stb.c b/src/text_stb.c
@@ -1,3 +1,4 @@
+/* FIXME: max cache size for glyphs */
/*
* Copyright (c) 2017, 2018, 2020, 2022 lumidify <nobody@lumidify.org>
*
@@ -99,7 +100,6 @@ struct ltk_text_context {
ltk_font **fonts;
int num_fonts;
int fonts_bufsize;
- FcPattern *fcpattern;
ltk_font *default_font;
uint16_t font_id_cur;
};
@@ -208,7 +208,6 @@ ltk_text_context_create(ltk_window *window, char *default_font) {
ctx->fonts = ltk_malloc(sizeof(ltk_font *));
ctx->num_fonts = 0;
ctx->fonts_bufsize = 1;
- ctx->fcpattern = NULL;
ltk_load_default_font(ctx, default_font);
ctx->font_id_cur = 1;
return ctx;
@@ -216,10 +215,10 @@ ltk_text_context_create(ltk_window *window, char *default_font) {
void
ltk_text_context_destroy(ltk_text_context *ctx) {
- /* FIXME: destroy fcpattern */
for (int i = 0; i < ctx->num_fonts; i++) {
ltk_destroy_font(ctx->fonts[i]);
}
+ ltk_free(ctx->fonts);
if (!ctx->glyph_cache) return;
for (khint_t k = kh_begin(ctx->glyph_cache); k != kh_end(ctx->glyph_cache); k++) {
if (kh_exist(ctx->glyph_cache, k)) {
@@ -227,6 +226,7 @@ ltk_text_context_destroy(ltk_text_context *ctx) {
}
}
kh_destroy(glyphcache, ctx->glyph_cache);
+ ltk_free(ctx);
}
static ltk_glyph_info *
@@ -302,18 +302,18 @@ ltk_destroy_glyph_cache(khash_t(glyphinfo) *cache) {
static void
ltk_load_default_font(ltk_text_context *ctx, char *name) {
- FcPattern *match;
+ FcPattern *match, *pat;
FcResult result;
char *file;
int index;
/* FIXME: Get rid of this stupid cast somehow */
- ctx->fcpattern = FcNameParse((const FcChar8 *)name);
- /*ctx->fcpattern = FcPatternCreate();*/
- FcPatternAddString(ctx->fcpattern, FC_FONTFORMAT, (const FcChar8 *)"truetype");
- FcConfigSubstitute(NULL, ctx->fcpattern, FcMatchPattern);
- FcDefaultSubstitute(ctx->fcpattern);
- match = FcFontMatch(NULL, ctx->fcpattern, &result);
+ pat = FcNameParse((const FcChar8 *)name);
+ FcPatternAddString(pat, FC_FONTFORMAT, (const FcChar8 *)"truetype");
+ FcConfigSubstitute(NULL, pat, FcMatchPattern);
+ FcDefaultSubstitute(pat);
+ /* FIXME look at result */
+ match = FcFontMatch(NULL, pat, &result);
FcPatternGetString(match, FC_FILE, 0, (FcChar8 **) &file);
FcPatternGetInteger(match, FC_INDEX, 0, &index);
@@ -321,6 +321,7 @@ ltk_load_default_font(ltk_text_context *ctx, char *name) {
ctx->default_font = ltk_get_font(ctx, file, index);
FcPatternDestroy(match);
+ FcPatternDestroy(pat);
}
static ltk_font *
@@ -331,6 +332,7 @@ ltk_create_font(char *path, uint16_t id, int index) {
if (!contents)
ltk_fatal_errno("Unable to read font file %s\n", path);
int offset = stbtt_GetFontOffsetForIndex((unsigned char *)contents, index);
+ font->info.data = NULL;
if (!stbtt_InitFont(&font->info, (unsigned char *)contents, offset))
ltk_fatal("Failed to load font %s\n", path);
font->id = id;
@@ -343,6 +345,7 @@ ltk_create_font(char *path, uint16_t id, int index) {
static void
ltk_destroy_font(ltk_font *font) {
+ ltk_free(font->path);
ltk_free(font->info.data);
ltk_free(font);
}
@@ -405,6 +408,7 @@ ltk_text_to_glyphs(ltk_text_context *ctx, ltk_glyph *glyphs, int num_glyphs, cha
/* Question: Why does this not work with FcPatternDuplicate? */
FcPattern *pat = FcPatternCreate();
FcPattern *match;
+ /* FIXME: use result */
FcResult result;
FcPatternAddBool(pat, FC_SCALABLE, 1);
FcConfigSubstitute(NULL, pat, FcMatchPattern);
@@ -506,6 +510,8 @@ ltk_text_line_draw_glyph(ltk_glyph *glyph, int x, int y, XImage *img, XColor fg)
int b;
for (int i = 0; i < glyph->info->h; i++) {
for (int j = 0; j < glyph->info->w; j++) {
+ /* FIXME: this check could be moved to the for loop condition and initialization */
+ /* -> not sure it that would *possibly* be a tiny bit faster */
if (y + i >= img->height || x + j >= img->width ||
y + i < 0 || x + i < 0)
continue;
@@ -569,8 +575,23 @@ void
ltk_text_line_draw(ltk_text_line *tl, ltk_surface *s, ltk_color *color, int x, int y) {
if (tl->dirty)
ltk_text_line_break_lines(tl);
+ int xoff = 0, yoff = 0;
+ if (x < 0) {
+ xoff = x;
+ x = 0;
+ }
+ if (y < 0) {
+ yoff = y;
+ y = 0;
+ }
+ int s_w, s_h;
+ ltk_surface_get_size(s, &s_w, &s_h);
+ int w = x + xoff + tl->w > s_w ? s_w - x : xoff + tl->w;
+ int h = y + yoff + tl->h > s_h ? s_h - y : yoff + tl->h;
+ if (w <= 0 || h <= 0)
+ return;
Drawable d = ltk_surface_get_drawable(s);
- XImage *img = XGetImage(tl->ctx->window->dpy, d, x, y, tl->w, tl->h, 0xFFFFFF, ZPixmap);
+ XImage *img = XGetImage(tl->ctx->window->dpy, d, x, y, w, h, 0xFFFFFF, ZPixmap);
int last_break = 0;
for (int i = 0; i < tl->lines; i++) {
@@ -580,13 +601,13 @@ ltk_text_line_draw(ltk_text_line *tl, ltk_surface *s, ltk_color *color, int x, i
else
next_break = tl->glyph_len;
for (int j = last_break; j < next_break; j++) {
- int x = tl->glyphs[j].x - tl->glyphs[last_break].x;
- int y = tl->glyphs[j].y - tl->y_min + tl->line_h * i;
- ltk_text_line_draw_glyph(&tl->glyphs[j], x, y, img, color->xcolor);
+ int g_x = tl->glyphs[j].x - tl->glyphs[last_break].x + xoff;
+ int g_y = tl->glyphs[j].y - tl->y_min + tl->line_h * i + yoff;
+ ltk_text_line_draw_glyph(&tl->glyphs[j], g_x, g_y, img, color->xcolor);
}
last_break = next_break;
}
- XPutImage(tl->ctx->window->dpy, d, tl->ctx->window->gc, img, 0, 0, x, y, tl->w, tl->h);
+ XPutImage(tl->ctx->window->dpy, d, tl->ctx->window->gc, img, 0, 0, x, y, w, h);
XDestroyImage(img);
}
diff --git a/src/util.h b/src/util.h
@@ -16,11 +16,10 @@
/* Requires: <stdarg.h> */
-/* #ifndef __OpenBSD__ */
-long long
-strtonum(const char *numstr, long long minval, long long maxval,
- const char **errstrp);
-/* #endif */
+long long ltk_strtonum(
+ const char *numstr, long long minval,
+ long long maxval, const char **errstrp
+);
char *ltk_read_file(const char *path, unsigned long *len);
int ltk_grow_string(char **str, int *alloc_size, int needed);
diff --git a/src/widget.c b/src/widget.c
@@ -1,5 +1,10 @@
/* FIXME: store coordinates relative to parent widget */
/* FIXME: Destroy function for widget to destroy pixmap! */
+/* FIXME/NOTE: maybe it would be better to do some sort of
+ inheritance where the generic widget destroy function is
+ called before the specific function for each widget type
+ so each widget doesn't have to manually remove itself from
+ its parent */
/*
* Copyright (c) 2021, 2022 lumidify <nobody@lumidify.org>
*
@@ -35,18 +40,25 @@ static void ltk_destroy_widget_hash(void);
KHASH_MAP_INIT_STR(widget, ltk_widget *)
static khash_t(widget) *widget_hash = NULL;
+/* Hack to make ltk_destroy_widget_hash work */
+/* FIXME: any better way to do this? */
+static int hash_locked = 0;
static void
ltk_destroy_widget_hash(void) {
+ hash_locked = 1;
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);
+ ltk_free((char *)kh_key(widget_hash, k));
ptr->vtable->destroy(ptr, 1);
}
}
kh_destroy(widget, widget_hash);
+ widget_hash = NULL;
+ hash_locked = 0;
}
void
@@ -90,6 +102,7 @@ ltk_fill_widget_defaults(ltk_widget *widget, const char *id, ltk_window *window,
widget->column_span = 0;
widget->sticky = 0;
widget->dirty = 1;
+ widget->hidden = 0;
}
/* FIXME: Maybe pass the new width as arg here?
@@ -137,15 +150,28 @@ ltk_widget_mouse_release_event(ltk_widget *widget, XEvent event) {
void
ltk_widget_motion_notify_event(ltk_widget *widget, XEvent event) {
- if (!widget || widget->state == LTK_DISABLED)
- return;
/* FIXME: THIS WHOLE STATE HANDLING IS STILL PARTIALLY BROKEN */
+ /* FIXME: need to bring back hover state to make enter/leave work properly */
+ /* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ */
+ /* (especially once keyboard navigation is added) */
+ /* Also, enter/leave should probably be called for all in hierarchy */
int set_active = 1;
if (widget->window->pressed_widget && widget->window->pressed_widget->vtable->motion_notify) {
widget->window->pressed_widget->vtable->motion_notify(widget->window->pressed_widget, event);
set_active = 0;
- } else if (widget->vtable->motion_notify) {
- set_active = widget->vtable->motion_notify(widget, event);
+ } else if (widget && widget->state != LTK_DISABLED) {
+ /* FIXME: because only the bottom widget of the hierarchy is stored,
+ this *really* does not work properly! */
+ if (widget != widget->window->active_widget) {
+ if (widget->window->active_widget && widget->window->active_widget->vtable->mouse_leave) {
+ widget->window->active_widget->vtable->mouse_leave(widget->window->active_widget, event);
+ }
+ if (widget->vtable->mouse_enter) {
+ widget->vtable->mouse_enter(widget, event);
+ }
+ }
+ if (widget->vtable->motion_notify)
+ set_active = widget->vtable->motion_notify(widget, event);
}
if (set_active)
ltk_window_set_active_widget(widget->window, widget);
@@ -182,8 +208,7 @@ void
ltk_set_widget(ltk_widget *widget, const char *id) {
int ret;
khint_t k;
- /* apparently, khash requires the string to stay accessible */
- /* FIXME: How is this freed? */
+ /* FIXME: make sure no widget is overwritten here */
char *tmp = ltk_strdup(id);
k = kh_put(widget, widget_hash, tmp, &ret);
kh_value(widget_hash, k) = widget;
@@ -191,20 +216,40 @@ ltk_set_widget(ltk_widget *widget, const char *id) {
void
ltk_remove_widget(const char *id) {
+ if (hash_locked)
+ return;
khint_t k;
k = kh_get(widget, widget_hash, id);
if (k != kh_end(widget_hash)) {
+ ltk_free((char *)kh_key(widget_hash, k));
kh_del(widget, widget_hash, k);
}
}
int
-ltk_widget_destroy(
+ltk_widget_destroy(ltk_widget *widget, int shallow, char **errstr) {
+ /* widget->parent->remove_child should never be NULL because of the fact that
+ the widget is set as parent, but let's just check anyways... */
+ int err = 0;
+ /* FIXME: why is window passed here? */
+ if (widget->parent && widget->parent->vtable->remove_child) {
+ err = widget->parent->vtable->remove_child(
+ widget->window, widget, widget->parent, errstr
+ );
+ }
+ widget->vtable->destroy(widget, shallow);
+
+ return err;
+}
+
+int
+ltk_widget_destroy_cmd(
ltk_window *window,
char **tokens,
size_t num_tokens,
char **errstr) {
- int err = 0, shallow = 1;
+ (void)window;
+ int shallow = 1;
if (num_tokens != 2 && num_tokens != 3) {
*errstr = "Invalid number of arguments.\n";
return 1;
@@ -224,15 +269,5 @@ ltk_widget_destroy(
*errstr = "Invalid widget ID.\n";
return 1;
}
- ltk_remove_widget(tokens[1]);
- /* widget->parent->remove_child should never be NULL because of the fact that
- the widget is set as parent, but let's just check anyways... */
- if (widget->parent && widget->parent->vtable->remove_child) {
- err = widget->parent->vtable->remove_child(
- window, widget, widget->parent, errstr
- );
- }
- widget->vtable->destroy(widget, shallow);
-
- return err;
+ return ltk_widget_destroy(widget, shallow, errstr);
}
diff --git a/src/widget.h b/src/widget.h
@@ -53,6 +53,7 @@ typedef enum {
LTK_LABEL,
LTK_WIDGET,
LTK_BOX,
+ LTK_MENU,
LTK_NUM_WIDGETS
} ltk_widget_type;
@@ -81,6 +82,7 @@ struct ltk_widget {
unsigned short row_span;
unsigned short column_span;
char dirty;
+ char hidden;
};
struct ltk_widget_vtable {
@@ -90,10 +92,11 @@ struct ltk_widget_vtable {
int (*mouse_release) (struct ltk_widget *, XEvent);
int (*mouse_wheel) (struct ltk_widget *, XEvent);
int (*motion_notify) (struct ltk_widget *, XEvent);
- void (*mouse_leave) (struct ltk_widget *, XEvent);
- void (*mouse_enter) (struct ltk_widget *, XEvent);
+ int (*mouse_leave) (struct ltk_widget *, XEvent);
+ int (*mouse_enter) (struct ltk_widget *, XEvent);
void (*resize) (struct ltk_widget *);
+ void (*hide) (struct ltk_widget *);
void (*draw) (struct ltk_widget *, ltk_rect);
void (*change_state) (struct ltk_widget *);
void (*destroy) (struct ltk_widget *, int);
@@ -106,7 +109,8 @@ struct ltk_widget_vtable {
char needs_surface;
};
-int ltk_widget_destroy(struct ltk_window *window, char **tokens, size_t num_tokens, char **errstr);
+int ltk_widget_destroy(ltk_widget *widget, int shallow, char **errstr);
+int ltk_widget_destroy_cmd(struct ltk_window *window, char **tokens, size_t num_tokens, char **errstr);
void ltk_fill_widget_defaults(ltk_widget *widget, const char *id, struct ltk_window *window,
struct ltk_widget_vtable *vtable, int w, int h);
void ltk_widget_change_state(ltk_widget *widget);
diff --git a/test.sh b/test.sh
@@ -2,7 +2,7 @@
# This is very hacky.
#
-# All events are still printed to the terminal curerntly because
+# All events are still printed to the terminal currently because
# the second './ltkc' still prints everything - event masks aren't
# supported yet.
diff --git a/test2.gui b/test2.gui
@@ -0,0 +1,25 @@
+grid grd1 create 2 1
+grid grd1 set-row-weight 1 1
+grid grd1 set-column-weight 0 1
+set-root-widget grd1
+menu menu1 create
+menu menu1 add-entry entry1 "Menu Entry"
+menu menu1 add-entry entrya1 "Menu Entry 2"
+submenu submenu1 create
+menu submenu1 add-entry entry2 "Submenu Entry 1"
+menu submenu1 add-entry entry6 "Submenu Entry 2"
+menu submenu1 add-entry entry7 "Submenu Entry 3"
+menu submenu1 add-entry entry8 "Submenu Entry 4"
+menu submenu1 add-entry entry9 "Submenu Entry 5"
+menu submenu1 add-entry entry10 "Submenu Entry 6"
+menu submenu1 add-entry entry11 "Submenu Entry 7"
+menu submenu1 add-entry entry12 "Submenu Entry 8"
+menu submenu1 add-entry entry13 "Submenu Entry 9"
+menu menu1 add-submenu entry3 "Submenu" submenu1
+submenu submenu2 create
+menu submenu2 add-entry entry4 "Submenu Entry"
+menu submenu1 add-submenu entry5 "Submenu" submenu2
+submenu submenu3 create
+menu submenu3 add-entry entrya3 "Submenu Entry"
+menu submenu2 add-submenu entrya2 "Submenu" submenu3
+grid grd1 add menu1 0 0 1 1 ew
diff --git a/test2.sh b/test2.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+export LTKDIR="`pwd`/.ltk"
+ltk_id=`./src/ltkd -t "Cool Window"`
+if [ $? -ne 0 ]; then
+ echo "Unable to start ltkd." >&2
+ exit 1
+fi
+
+cat test2.gui | ./src/ltkc $ltk_id