ltk

Socket-based GUI for X11 (WIP)
git clone git://lumidify.org/ltk.git (fast, but not encrypted)
git clone https://lumidify.org/git/ltk.git (encrypted, but very slow)
Log | Files | Refs | README | LICENSE

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:
MMakefile | 42++++++++++++++++++++++--------------------
MREADME.md | 8+++++---
Msrc/box.c | 19+++++++++++++------
Msrc/button.c | 85++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/button.h | 2+-
Msrc/color.c | 23++++++++++++++---------
Msrc/color.h | 3++-
Msrc/compat.h | 2+-
Msrc/draw.c | 22+++++++++++-----------
Msrc/graphics.h | 17+++++++++++++++++
Msrc/graphics_xlib.c | 42+++++++++++++++++++++++++++++++++---------
Msrc/grid.c | 44++++++++++++++++++++++++++++----------------
Msrc/label.c | 34++++++++++++++++++++++------------
Msrc/ltk.h | 17++++++++++++++++-
Msrc/ltkd.c | 255++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Asrc/macros.h | 25+++++++++++++++++++++++++
Msrc/memory.c | 3+++
Asrc/menu.c | 1772+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/menu.h | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/scrollbar.c | 63+++++++++++++++++++++++++++++++++++----------------------------
Msrc/strtonum.c | 16++++++----------
Msrc/text.h | 5+++++
Msrc/text_pango.c | 9++++++---
Msrc/text_stb.c | 51++++++++++++++++++++++++++++++++++++---------------
Msrc/util.h | 9++++-----
Msrc/widget.c | 73++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msrc/widget.h | 10+++++++---
Mtest.sh | 2+-
Atest2.gui | 25+++++++++++++++++++++++++
Atest2.sh | 10++++++++++
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 = &ltk_menu_mouse_press, + .motion_notify = &ltk_menu_motion_notify, + .mouse_release = &ltk_menu_mouse_release, + .mouse_enter = &ltk_menu_mouse_enter, + .mouse_leave = &ltk_menu_mouse_leave, + .resize = &ltk_menu_resize, + .change_state = &ltk_menu_change_state, + .hide = &ltk_menu_hide, + .draw = &ltk_menu_draw, + .destroy = &ltk_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, &ltk_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