commit 9de2d4ac1734343f1de02c9ec29e408ccc05689c
parent 43bb385257c126c200662ed207f27a7a285f113e
Author: lumidify <nobody@lumidify.org>
Date: Mon, 25 Mar 2024 19:16:53 +0100
Re-add client-server functionality
Diffstat:
116 files changed, 9514 insertions(+), 4909 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,3 +1,2 @@
*.o
*.core
-ltkd
diff --git a/LICENSE b/LICENSE
@@ -1,5 +1,5 @@
-See src/khash.h, src/ini.*, src/stb_truetype.*, src/strtonum.c,
-src/ctrlsel.*, and src/macros.h for third-party licenses.
+See src/ltkd/khash.h, src/ltk/ini.*, src/ltk/stb_truetype.*, src/ltk/strtonum.c,
+src/ltk/ctrlsel.*, and src/ltk/macros.h for third-party licenses.
ISC License
diff --git a/Makefile b/Makefile
@@ -1,3 +1,5 @@
+# Yes, I know this is a mess.
+
.POSIX:
.SUFFIXES: .c .o
@@ -24,10 +26,10 @@ DEV_CFLAGS_0 = $(CFLAGS)
DEV_LDFLAGS_0 = $(LDFLAGS)
# stb rendering
-EXTRA_OBJ_0 = src/stb_truetype.o src/text_stb.o
+EXTRA_OBJ_0 = src/ltk/stb_truetype.o src/ltk/text_stb.o
# pango rendering
-EXTRA_OBJ_1 = src/text_pango.o
+EXTRA_OBJ_1 = src/ltk/text_pango.o
EXTRA_CFLAGS_1 = `pkg-config --cflags pangoxft`
EXTRA_LDFLAGS_1 = `pkg-config --libs pangoxft`
@@ -38,80 +40,124 @@ EXTRA_LDFLAGS = $(SANITIZE_FLAGS_$(SANITIZE)) $(DEV_LDFLAGS_$(DEV)) $(EXTRA_LDFL
LTK_CFLAGS = $(EXTRA_CFLAGS) -DUSE_PANGO=$(USE_PANGO) -DDEV=$(DEV) -DMEMDEBUG=$(MEMDEBUG) -I ./src -std=c99 `pkg-config --cflags x11 fontconfig xext xcursor imlib2` -D_POSIX_C_SOURCE=200809L
LTK_LDFLAGS = $(EXTRA_LDFLAGS) -lm `pkg-config --libs x11 fontconfig xext xcursor imlib2`
-OBJ = \
- examples/test.o \
- src/strtonum.o \
- src/util.o \
- src/memory.o \
- src/window.o \
- src/color_xlib.o \
- src/rect.o \
- src/widget.o \
- src/ltk.o \
- src/ini.o \
- src/button.o \
- src/theme.o \
- src/graphics_xlib.o \
- src/surface_cache.o \
- src/event_xlib.o \
- src/grid.o \
- src/config.o \
- src/clipboard_xlib.o \
- src/txtbuf.o \
- src/ctrlsel.o \
- src/label.o \
- src/image_imlib.o \
- src/image_widget.o \
- src/entry.o \
- src/menu.o \
- src/box.o \
- src/scrollbar.o \
+OBJ_LTK = \
+ src/ltk/strtonum.o \
+ src/ltk/util.o \
+ src/ltk/memory.o \
+ src/ltk/window.o \
+ src/ltk/color_xlib.o \
+ src/ltk/rect.o \
+ src/ltk/widget.o \
+ src/ltk/ltk.o \
+ src/ltk/ini.o \
+ src/ltk/button.o \
+ src/ltk/theme.o \
+ src/ltk/graphics_xlib.o \
+ src/ltk/surface_cache.o \
+ src/ltk/event_xlib.o \
+ src/ltk/grid.o \
+ src/ltk/config.o \
+ src/ltk/clipboard_xlib.o \
+ src/ltk/txtbuf.o \
+ src/ltk/ctrlsel.o \
+ src/ltk/label.o \
+ src/ltk/image_imlib.o \
+ src/ltk/image_widget.o \
+ src/ltk/entry.o \
+ src/ltk/menu.o \
+ src/ltk/box.o \
+ src/ltk/scrollbar.o \
$(EXTRA_OBJ)
+OBJ_LTKD = \
+ src/ltkd/box.o \
+ src/ltkd/button.o \
+ src/ltkd/cmd.o \
+ src/ltkd/entry.o \
+ src/ltkd/err.o \
+ src/ltkd/grid.o \
+ src/ltkd/image_widget.o \
+ src/ltkd/label.o \
+ src/ltkd/ltkd.o \
+ src/ltkd/menu.o \
+ src/ltkd/util.o \
+ src/ltkd/widget.o
+
+OBJ_TEST = examples/ltk/test.o
# Note: This could be improved so a change in a header only causes the .c files
# which include that header to be recompiled, but the compile times are
# currently so short that I don't really care.
-HDR = \
- src/button.h \
- src/color.h \
- src/ini.h \
- src/label.h \
- src/rect.h \
- src/widget.h \
- src/ltk.h \
- src/grid.h \
- src/memory.h \
- src/stb_truetype.h \
- src/text.h \
- src/util.h \
- src/theme.h \
- src/graphics.h \
- src/surface_cache.h \
- src/macros.h \
- src/event.h \
- src/eventdefs.h \
- src/graphics_xlib.h \
- src/label.h \
- src/config.h \
- src/array.h \
- src/keys.h \
- src/clipboard_xlib.h \
- src/clipboard.h \
- src/txtbuf.h \
- src/ctrlsel.h \
- src/image.h \
- src/image_widget.h \
- src/entry.h \
- src/menu.h \
- src/box.h \
- src/scrollbar.h
-
-all: examples/test
-
-examples/test: $(OBJ)
- $(CC) -o $@ $(OBJ) $(LTK_LDFLAGS)
-
-$(OBJ) : $(HDR)
+HDR_LTK = \
+ src/ltk/button.h \
+ src/ltk/color.h \
+ src/ltk/ini.h \
+ src/ltk/label.h \
+ src/ltk/rect.h \
+ src/ltk/widget.h \
+ src/ltk/ltk.h \
+ src/ltk/grid.h \
+ src/ltk/memory.h \
+ src/ltk/stb_truetype.h \
+ src/ltk/text.h \
+ src/ltk/util.h \
+ src/ltk/theme.h \
+ src/ltk/graphics.h \
+ src/ltk/surface_cache.h \
+ src/ltk/macros.h \
+ src/ltk/event.h \
+ src/ltk/eventdefs.h \
+ src/ltk/graphics_xlib.h \
+ src/ltk/label.h \
+ src/ltk/config.h \
+ src/ltk/array.h \
+ src/ltk/keys.h \
+ src/ltk/clipboard_xlib.h \
+ src/ltk/clipboard.h \
+ src/ltk/txtbuf.h \
+ src/ltk/ctrlsel.h \
+ src/ltk/image.h \
+ src/ltk/image_widget.h \
+ src/ltk/entry.h \
+ src/ltk/menu.h \
+ src/ltk/box.h \
+ src/ltk/scrollbar.h
+
+HDR_LTKD = \
+ src/ltkd/cmd.h \
+ src/ltkd/cmd_helpers.h \
+ src/ltkd/err.h \
+ src/ltkd/khash.h \
+ src/ltkd/ltkd.h \
+ src/ltkd/proto_types.h \
+ src/ltkd/widget.h
+
+all: examples/ltk/test src/ltkd/ltkd src/ltkd/ltkc src/ltkd/ltkc_img
+
+test: examples/ltk/test
+
+ltkd: src/ltkd/ltkd
+
+ltkc: src/ltkd/ltkc
+
+ltkc_img: src/ltkd/ltkc_img
+
+examples/ltk/test: $(OBJ_LTK) $(OBJ_TEST)
+ $(CC) -o $@ $(OBJ_LTK) $(OBJ_TEST) $(LTK_LDFLAGS)
+
+src/ltkd/ltkd: $(OBJ_LTK) $(OBJ_LTKD)
+ $(CC) -o $@ $(OBJ_LTK) $(OBJ_LTKD) $(LTK_LDFLAGS)
+
+src/ltkd/ltkc: $(OBJ_LTK) src/ltkd/ltkc.o src/ltkd/util.o
+ $(CC) -o $@ $(OBJ_LTK) src/ltkd/ltkc.o src/ltkd/util.o $(LTK_LDFLAGS)
+
+src/ltkd/ltkc_img: $(OBJ_LTK) src/ltkd/ltkc_img.o
+ $(CC) -o $@ $(OBJ_LTK) src/ltkd/ltkc_img.o $(LTK_LDFLAGS)
+
+$(OBJ_LTK) : $(HDR_LTK)
+
+$(OBJ_TEST) : $(HDR_LTK)
+
+$(OBJ_LTKD) : $(HDR_LTK) $(HDR_LTKD)
.c.o:
$(CC) -c -o $@ $< $(LTK_CFLAGS)
@@ -119,4 +165,4 @@ $(OBJ) : $(HDR)
.PHONY: clean
clean:
- rm -f src/*.o examples/test examples/*.o
+ rm -f src/ltkd/*.o src/ltk/*.o src/ltkd/ltkd src/ltkd/ltkc src/ltkd/ltkc_img examples/ltk/test examples/ltk/*.o
diff --git a/README.md b/README.md
@@ -8,10 +8,25 @@ To build with or without pango: Follow instructions in Makefile.
Note: The basic (non-pango) text doesn't work properly on all systems.
Note: The basic (non-pango) text is currently completely broken.
+The toolkit has now been split into two parts:
+
+* ltk is a regular GUI toolkit.
+* ltkd is the client-server based toolkit that was previously the only
+ way to use ltk.
+
+The current plan is to focus on ltk before continuing work on ltkd.
+
To test:
make
-cd examples && LTKDIR=../config.example/ ./test
+cd examples/ltk && LTKDIR=../../config.example/ ./test
+
+Alternatively, run one of the shell scripts in examples/ltkd to test the
+client-server functionality.
+
+You can also run 'make test' to only compile the test that does not use
+the client-server functionality or 'make ltkd ltkc ltkc_img' to compile
+only the binaries needed for the client-server functionality.
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/examples/.gitignore b/examples/.gitignore
@@ -1,2 +0,0 @@
-*.o
-test
diff --git a/examples/ltk/.gitignore b/examples/ltk/.gitignore
@@ -0,0 +1,2 @@
+test
+*.o
diff --git a/examples/ltk/test.c b/examples/ltk/test.c
@@ -0,0 +1,100 @@
+#include <stdio.h>
+
+#include <ltk/ltk.h>
+#include <ltk/label.h>
+#include <ltk/button.h>
+#include <ltk/image.h>
+#include <ltk/image_widget.h>
+#include <ltk/grid.h>
+#include <ltk/entry.h>
+#include <ltk/menu.h>
+#include <ltk/box.h>
+
+int
+quit(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
+ (void)self;
+ (void)args;
+ (void)data;
+ ltk_mainloop_quit();
+ return 1;
+}
+
+int
+printstuff(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
+ (void)self;
+ (void)args;
+ printf("%d\n", LTK_CAST_ARG_INT(data));
+ return 1;
+}
+
+int
+printstate(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
+ (void)self;
+ (void)data;
+ int state = LTK_GET_ARG_INT(args, 0);
+ printf("%d\n", state);
+ return 0;
+}
+
+int
+main(int argc, char *argv[]) {
+ (void)argc;
+ (void)argv;
+ ltk_init();
+
+ ltk_window *window = ltk_window_create("Hi", 0, 0, 500, 500);
+ ltk_grid *grid = ltk_grid_create(window, 5, 2);
+ ltk_grid_set_column_weight(grid, 0, 1);
+ ltk_grid_set_column_weight(grid, 1, 1);
+ ltk_grid_set_row_weight(grid, 4, 1);
+ ltk_button *button = ltk_button_create(window, "I'm a button!");
+ ltk_button *button1 = ltk_button_create(window, "I'm also a button!");
+ ltk_label *label = ltk_label_create(window, "I'm a label!");
+ ltk_image *img = ltk_image_create_from_path("test.jpg");
+ if (!img) {
+ fprintf(stderr, "Unable to load image.\n");
+ return 1;
+ }
+ ltk_image_widget *iw = ltk_image_widget_create(window, img);
+ ltk_entry *entry = ltk_entry_create(window, "");
+ ltk_menu *menu = ltk_menu_create(window);
+ ltk_menuentry *e1 = ltk_menuentry_create(window, "Hi");
+ ltk_menuentry *e2 = ltk_menuentry_create(window, "I'm a submenu");
+ ltk_menu_add_entry(menu, e1);
+ ltk_menu_add_entry(menu, e2);
+ ltk_menu *submenu = ltk_submenu_create(window);
+ ltk_menuentry *e3 = ltk_menuentry_create(window, "Menu Entry 1");
+ ltk_menuentry *e4 = ltk_menuentry_create(window, "Quit");
+ ltk_menu_add_entry(submenu, e3);
+ ltk_menu_add_entry(submenu, e4);
+ ltk_menuentry_attach_submenu(e2, submenu);
+
+ ltk_box *box = ltk_box_create(window, LTK_VERTICAL);
+ ltk_button *btn1 = ltk_button_create(window, "Bla1");
+ ltk_button *btn2 = ltk_button_create(window, "Bla2");
+ ltk_button *btn3 = ltk_button_create(window, "Bla3");
+ ltk_button *btn4 = ltk_button_create(window, "Bla4");
+ ltk_button *btn5 = ltk_button_create(window, "Bla5");
+ ltk_box_add(box, LTK_CAST_WIDGET(btn1), LTK_STICKY_LEFT);
+ ltk_box_add(box, LTK_CAST_WIDGET(btn2), LTK_STICKY_LEFT);
+ ltk_box_add(box, LTK_CAST_WIDGET(btn3), LTK_STICKY_LEFT);
+ ltk_box_add(box, LTK_CAST_WIDGET(btn4), LTK_STICKY_LEFT);
+ ltk_box_add(box, LTK_CAST_WIDGET(btn5), LTK_STICKY_LEFT);
+
+ ltk_grid_add(grid, LTK_CAST_WIDGET(menu), 0, 0, 1, 2, LTK_STICKY_LEFT|LTK_STICKY_RIGHT);
+ ltk_grid_add(grid, LTK_CAST_WIDGET(button), 1, 0, 1, 1, LTK_STICKY_LEFT);
+ ltk_grid_add(grid, LTK_CAST_WIDGET(button1), 1, 1, 1, 1, LTK_STICKY_RIGHT);
+ ltk_grid_add(grid, LTK_CAST_WIDGET(label), 2, 0, 1, 1, LTK_STICKY_RIGHT);
+ ltk_grid_add(grid, LTK_CAST_WIDGET(iw), 2, 1, 1, 1, LTK_STICKY_LEFT|LTK_STICKY_RIGHT|LTK_STICKY_PRESERVE_ASPECT_RATIO);
+ ltk_grid_add(grid, LTK_CAST_WIDGET(entry), 3, 0, 1, 1, LTK_STICKY_LEFT|LTK_STICKY_RIGHT);
+ ltk_grid_add(grid, LTK_CAST_WIDGET(box), 4, 0, 1, 2, LTK_STICKY_LEFT|LTK_STICKY_RIGHT|LTK_STICKY_TOP|LTK_STICKY_BOTTOM);
+ ltk_window_set_root_widget(window, LTK_CAST_WIDGET(grid));
+ ltk_widget_register_signal_handler(LTK_CAST_WIDGET(button), LTK_BUTTON_SIGNAL_PRESSED, &quit, LTK_ARG_VOID);
+ ltk_widget_register_signal_handler(LTK_CAST_WIDGET(e4), LTK_BUTTON_SIGNAL_PRESSED, &quit, LTK_ARG_VOID);
+ ltk_widget_register_signal_handler(LTK_CAST_WIDGET(button1), LTK_BUTTON_SIGNAL_PRESSED, &printstuff, LTK_MAKE_ARG_INT(5));
+ ltk_widget_register_signal_handler(LTK_CAST_WIDGET(window), LTK_WINDOW_SIGNAL_CLOSE, &quit, LTK_ARG_VOID);
+ ltk_widget_register_signal_handler(LTK_CAST_WIDGET(button1), LTK_WIDGET_SIGNAL_CHANGE_STATE, &printstate, LTK_ARG_VOID);
+
+ ltk_mainloop();
+ return 0;
+}
diff --git a/examples/test.jpg b/examples/ltk/test.jpg
Binary files differ.
diff --git a/examples/ltkd/.gitignore b/examples/ltkd/.gitignore
@@ -0,0 +1 @@
+.ltkd
diff --git a/examples/ltkd/test.gui b/examples/ltkd/test.gui
@@ -0,0 +1,27 @@
+grid grd1 create 2 2
+grid grd1 set-row-weight 0 1
+grid grd1 set-row-weight 1 1
+grid grd1 set-column-weight 0 1
+grid grd1 set-column-weight 1 1
+set-root-widget grd1
+box box1 create vertical
+grid grd1 add box1 0 0 1 1 lrtb
+button btn1 create "I'm a button!"
+button btn2 create "I'm also a button!"
+button btn3 create "I'm another boring button."
+box box1 add btn1 lr
+box box1 add btn2 r
+box box1 add btn3
+box box2 create vertical
+grid grd1 add box2 1 0 1 1 lrtb
+button btn4 create "2 I'm a button!"
+button btn5 create "2 I'm also a button!"
+button btn6 create "2 I'm another boring button."
+box box2 add btn4 lr
+box box2 add btn5 r
+box box2 add btn6
+button btn7 create "Button 7"
+button btn8 create "Button 8"
+grid grd1 add btn7 0 1 1 1 lrtb
+grid grd1 add btn8 1 1 1 1 lr
+mask-add btn1 button press
diff --git a/examples/ltkd/test.sh b/examples/ltkd/test.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+# This is very hacky.
+
+export LTKDDIR=".ltkd"
+export LTKDIR="../../config.example"
+ltk_id=`../../src/ltkd/ltkd -t "Cool Window"`
+if [ $? -ne 0 ]; then
+ echo "Unable to start ltkd." >&2
+ exit 1
+fi
+
+cat test.gui | ../../src/ltkd/ltkc $ltk_id | while read cmd
+do
+ case "$cmd" in
+ *"event btn1 button press")
+ echo "quit"
+ ;;
+ *)
+ printf "%s\n" "$cmd" >&2
+ ;;
+ esac
+done | ../../src/ltkd/ltkc $ltk_id
diff --git a/examples/ltkd/test2.gui b/examples/ltkd/test2.gui
@@ -0,0 +1,45 @@
+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
+menuentry entry1 create "Entry 1"
+menuentry entry2 create "Entry 2"
+menuentry entry3 create "Entry 3"
+menuentry entry4 create "Entry 4"
+menuentry entry5 create "Entry 5"
+menuentry entry6 create "Entry 6"
+menuentry entry7 create "Entry 7"
+menuentry entry8 create "Entry 8"
+menuentry entry9 create "Entry 9"
+menuentry entry10 create "Entry 10"
+menuentry entry11 create "Entry 11"
+menuentry entry12 create "Entry 12"
+menuentry entry13 create "Entry 13"
+menuentry entry14 create "Entry 14"
+menuentry entry15 create "Entry 15"
+menuentry entry16 create "Entry 16"
+menu menu1 add-entry entry1
+menu menu1 add-entry entry2
+menu menu1 add-entry entry12
+submenu submenu1 create
+menu submenu1 add-entry entry3
+menu submenu1 add-entry entry4
+menu submenu1 add-entry entry5
+menu submenu1 add-entry entry6
+menu submenu1 add-entry entry7
+menu submenu1 add-entry entry8
+menu submenu1 add-entry entry9
+menu submenu1 add-entry entry10
+menu submenu1 add-entry entry11
+menuentry entry12 attach-submenu submenu1
+submenu submenu2 create
+menu submenu2 add-entry entry13
+menu submenu2 add-entry entry15
+menu submenu1 add-entry entry14
+menuentry entry14 attach-submenu submenu2
+submenu submenu3 create
+menu submenu3 add-entry entry16
+menuentry entry15 attach-submenu submenu3
+grid grd1 add menu1 0 0 1 1 lr
+mask-add entry10 menuentry press
diff --git a/examples/ltkd/test2.sh b/examples/ltkd/test2.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+export LTKDDIR=".ltkd"
+export LTKDIR="../../config.example"
+ltk_id=`../../src/ltkd/ltkd -t "Cool Window"`
+if [ $? -ne 0 ]; then
+ echo "Unable to start ltkd." >&2
+ exit 1
+fi
+
+cat test2.gui | ../../src/ltkd/ltkc $ltk_id
diff --git a/examples/ltkd/test3.gui b/examples/ltkd/test3.gui
@@ -0,0 +1,16 @@
+grid grd1 create 4 1
+grid grd1 set-row-weight 0 1
+grid grd1 set-row-weight 1 1
+grid grd1 set-row-weight 2 1
+grid grd1 set-row-weight 3 1
+grid grd1 set-column-weight 0 1
+set-root-widget grd1
+button btn1 create "I'm a button!"
+button btn2 create "I'm also a button!"
+button btn3 create "I'm another boring button."
+grid grd1 add btn1 0 0 1 1
+grid grd1 add btn2 1 0 1 1
+grid grd1 add btn3 2 0 1 1
+mask-add btn1 button press
+entry entry1 create "Hi"
+grid grd1 add entry1 3 0 1 1 w
diff --git a/examples/ltkd/test3.sh b/examples/ltkd/test3.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+export LTKDDIR=".ltkd"
+export LTKDIR="../../config.example"
+ltk_id=`../../src/ltkd/ltkd -t "Cool Window"`
+#if [ $? -ne 0 ]; then
+# echo "Unable to start ltkd." >&2
+# exit 1
+#fi
+
+cat test3.gui | ../../src/ltkd/ltkc $ltk_id | while read cmd
+do
+ case "$cmd" in
+ *"event btn1 button press")
+ echo "quit"
+ ;;
+ *)
+ printf "client1: %s\n" "$cmd" >&2
+ ;;
+ esac
+done | ../../src/ltkd/ltkc $ltk_id
diff --git a/examples/ltkd/testbox.sh b/examples/ltkd/testbox.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+export LTKDDIR=".ltkd"
+export LTKDIR="../../config.example"
+ltk_id=`../../src/ltkd/ltkd -t "Cool Window"`
+if [ $? -ne 0 ]; then
+ echo "Unable to start ltkd." >&2
+ exit 1
+fi
+
+cmds="box box1 create vertical\nset-root-widget box1\nlabel lblbla create \"Hi\"\nbox box1 add lblbla w\nbutton exit_btn create \"Exit\"\nmask-add exit_btn button press\nbox box1 add exit_btn
+$(curl -s gopher://lumidify.org | awk -F'\t' '
+BEGIN {btn = 0; lbl = 0;}
+/^i/ { printf "label lbl%s create \"%s\"\nbox box1 add lbl%s w\n", lbl, substr($1, 2), lbl; lbl++ }
+/^[1gI]/ { printf "button btn%s create \"%s\"\nbox box1 add btn%s w\n", btn, substr($1, 2), btn; btn++ }')
+mask-add btn0 button press"
+echo "$cmds" | ../../src/ltkd/ltkc $ltk_id | while read cmd
+do
+ case "$cmd" in
+ *"event exit_btn button press")
+ echo "quit"
+ ;;
+ *"event btn0 button press")
+ echo "button bla create \"safhaskfldshk\"\nbox box1 add bla w"
+ ;;
+ *)
+ printf "%s\n" "$cmd" >&2
+ ;;
+ esac
+done | ../../src/ltkd/ltkc $ltk_id > /dev/null
diff --git a/examples/ltkd/testimg.sh b/examples/ltkd/testimg.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+export LTKDDIR=".ltkd"
+export LTKDIR="../../config.example"
+ltk_id=`../../src/ltkd/ltkd -t "Cool Window"`
+#if [ $? -ne 0 ]; then
+# echo "Unable to start ltkd." >&2
+# exit 1
+#fi
+
+{ printf "grid grd1 create 2 1\ngrid grd1 set-row-weight 0 1\ngrid grd1 set-row-weight 1 1\ngrid grd1 set-column-weight 0 1\nset-root-widget grd1\nimage img1 create test.jpg \""; ../../src/ltkd/ltkc_img < ../ltk/test.jpg; printf "\"\ngrid grd1 add img1 1 0 1 1 lrp\n"; } |../../src/ltkd/ltkc $ltk_id | while read cmd
+do
+ printf "%s\n" "$cmd" >&2
+done | ../../src/ltkd/ltkc $ltk_id
diff --git a/examples/test.c b/examples/test.c
@@ -1,87 +0,0 @@
-#include <stdio.h>
-
-#include "ltk.h"
-#include "label.h"
-#include "button.h"
-#include "image.h"
-#include "image_widget.h"
-#include "grid.h"
-#include "entry.h"
-#include "menu.h"
-#include "box.h"
-
-int
-quit(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
- (void)self;
- (void)args;
- (void)data;
- ltk_quit();
- return 1;
-}
-
-int
-printstuff(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
- (void)self;
- (void)args;
- printf("%d\n", LTK_CAST_ARG_INT(data));
- return 1;
-}
-
-int
-main(int argc, char *argv[]) {
- (void)argc;
- (void)argv;
- ltk_init();
- ltk_window *window = ltk_window_create("Hi", 0, 0, 500, 500);
- ltk_grid *grid = ltk_grid_create(window, 5, 2);
- ltk_grid_set_column_weight(grid, 0, 1);
- ltk_grid_set_column_weight(grid, 1, 1);
- ltk_grid_set_row_weight(grid, 4, 1);
- ltk_button *button = ltk_button_create(window, "I'm a button!");
- ltk_button *button1 = ltk_button_create(window, "I'm also a button!");
- ltk_label *label = ltk_label_create(window, "I'm a label!");
- ltk_image *img = ltk_image_create_from_path("test.jpg");
- if (!img) {
- fprintf(stderr, "Unable to load image.\n");
- return 1;
- }
- ltk_image_widget *iw = ltk_image_widget_create(window, img);
- ltk_entry *entry = ltk_entry_create(window, "");
- ltk_menu *menu = ltk_menu_create(window);
- ltk_menuentry *e1 = ltk_menuentry_create(window, "Hi");
- ltk_menuentry *e2 = ltk_menuentry_create(window, "I'm a submenu");
- ltk_menu_add_entry(menu, e1);
- ltk_menu_add_entry(menu, e2);
- ltk_menu *submenu = ltk_submenu_create(window);
- ltk_menuentry *e3 = ltk_menuentry_create(window, "Menu Entry 1");
- ltk_menuentry *e4 = ltk_menuentry_create(window, "Quit");
- ltk_menu_add_entry(submenu, e3);
- ltk_menu_add_entry(submenu, e4);
- ltk_menuentry_attach_submenu(e2, submenu);
-
- ltk_box *box = ltk_box_create(window, LTK_VERTICAL);
- ltk_button *btn1 = ltk_button_create(window, "Bla1");
- ltk_button *btn2 = ltk_button_create(window, "Bla2");
- ltk_button *btn3 = ltk_button_create(window, "Bla3");
- ltk_button *btn4 = ltk_button_create(window, "Bla4");
- ltk_button *btn5 = ltk_button_create(window, "Bla5");
- ltk_box_add(box, LTK_CAST_WIDGET(btn1), LTK_STICKY_LEFT);
- ltk_box_add(box, LTK_CAST_WIDGET(btn2), LTK_STICKY_LEFT);
- ltk_box_add(box, LTK_CAST_WIDGET(btn3), LTK_STICKY_LEFT);
- ltk_box_add(box, LTK_CAST_WIDGET(btn4), LTK_STICKY_LEFT);
- ltk_box_add(box, LTK_CAST_WIDGET(btn5), LTK_STICKY_LEFT);
-
- ltk_grid_add(grid, LTK_CAST_WIDGET(menu), 0, 0, 1, 2, LTK_STICKY_LEFT|LTK_STICKY_RIGHT);
- ltk_grid_add(grid, LTK_CAST_WIDGET(button), 1, 0, 1, 1, LTK_STICKY_LEFT);
- ltk_grid_add(grid, LTK_CAST_WIDGET(button1), 1, 1, 1, 1, LTK_STICKY_RIGHT);
- ltk_grid_add(grid, LTK_CAST_WIDGET(label), 2, 0, 1, 1, LTK_STICKY_RIGHT);
- ltk_grid_add(grid, LTK_CAST_WIDGET(iw), 2, 1, 1, 1, LTK_STICKY_LEFT|LTK_STICKY_RIGHT|LTK_STICKY_PRESERVE_ASPECT_RATIO);
- ltk_grid_add(grid, LTK_CAST_WIDGET(entry), 3, 0, 1, 1, LTK_STICKY_LEFT|LTK_STICKY_RIGHT);
- ltk_grid_add(grid, LTK_CAST_WIDGET(box), 4, 0, 1, 2, LTK_STICKY_LEFT|LTK_STICKY_RIGHT|LTK_STICKY_TOP|LTK_STICKY_BOTTOM);
- ltk_window_set_root_widget(window, LTK_CAST_WIDGET(grid));
- ltk_widget_register_signal_handler(LTK_CAST_WIDGET(button), LTK_BUTTON_SIGNAL_PRESSED, &quit, LTK_ARG_VOID);
- ltk_widget_register_signal_handler(LTK_CAST_WIDGET(e4), LTK_BUTTON_SIGNAL_PRESSED, &quit, LTK_ARG_VOID);
- ltk_widget_register_signal_handler(LTK_CAST_WIDGET(button1), LTK_BUTTON_SIGNAL_PRESSED, &printstuff, LTK_MAKE_ARG_INT(5));
- ltk_widget_register_signal_handler(LTK_CAST_WIDGET(window), LTK_WINDOW_SIGNAL_CLOSE, &quit, LTK_ARG_VOID);
- ltk_mainloop();
-}
diff --git a/src/box.c b/src/box.c
@@ -1,470 +0,0 @@
-/*
- * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-/* FIXME: implement other sticky options now supported by grid */
-
-#include <limits.h>
-#include <string.h>
-
-#include "box.h"
-#include "event.h"
-#include "graphics.h"
-#include "memory.h"
-#include "rect.h"
-#include "scrollbar.h"
-#include "widget.h"
-
-static void ltk_box_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip);
-static void ltk_box_destroy(ltk_widget *self, int shallow);
-static void ltk_recalculate_box(ltk_widget *self);
-static void ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget);
-static int ltk_box_remove_child(ltk_widget *self, ltk_widget *widget);
-/* static int ltk_box_clear(ltk_window *window, ltk_box *box, int shallow); */
-static int ltk_box_scroll_cb(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data);
-static int ltk_box_mouse_scroll(ltk_widget *self, ltk_scroll_event *event);
-static ltk_widget *ltk_box_get_child_at_pos(ltk_widget *self, int x, int y);
-static void ltk_box_ensure_rect_shown(ltk_widget *self, ltk_rect r);
-
-static ltk_widget *ltk_box_prev_child(ltk_widget *self, ltk_widget *child);
-static ltk_widget *ltk_box_next_child(ltk_widget *self, ltk_widget *child);
-static ltk_widget *ltk_box_first_child(ltk_widget *self);
-static ltk_widget *ltk_box_last_child(ltk_widget *self);
-
-static ltk_widget *ltk_box_nearest_child(ltk_widget *self, ltk_rect rect);
-static ltk_widget *ltk_box_nearest_child_left(ltk_widget *self, ltk_widget *widget);
-static ltk_widget *ltk_box_nearest_child_right(ltk_widget *self, ltk_widget *widget);
-static ltk_widget *ltk_box_nearest_child_above(ltk_widget *self, ltk_widget *widget);
-static ltk_widget *ltk_box_nearest_child_below(ltk_widget *self, ltk_widget *widget);
-
-static struct ltk_widget_vtable vtable = {
- .change_state = NULL,
- .hide = NULL,
- .draw = <k_box_draw,
- .destroy = <k_box_destroy,
- .resize = <k_recalculate_box,
- .child_size_change = <k_box_child_size_change,
- .remove_child = <k_box_remove_child,
- .key_press = NULL,
- .key_release = NULL,
- .mouse_press = NULL,
- .mouse_scroll = <k_box_mouse_scroll,
- .mouse_release = NULL,
- .motion_notify = NULL,
- .get_child_at_pos = <k_box_get_child_at_pos,
- .mouse_leave = NULL,
- .mouse_enter = NULL,
- .prev_child = <k_box_prev_child,
- .next_child = <k_box_next_child,
- .first_child = <k_box_first_child,
- .last_child = <k_box_last_child,
- .nearest_child = <k_box_nearest_child,
- .nearest_child_left = <k_box_nearest_child_left,
- .nearest_child_right = <k_box_nearest_child_right,
- .nearest_child_above = <k_box_nearest_child_above,
- .nearest_child_below = <k_box_nearest_child_below,
- .ensure_rect_shown = <k_box_ensure_rect_shown,
- .type = LTK_WIDGET_BOX,
- .flags = 0,
- .invalid_signal = LTK_BOX_SIGNAL_INVALID,
-};
-
-static void
-ltk_box_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) {
- ltk_box *box = LTK_CAST_BOX(self);
- ltk_widget *ptr;
- /* FIXME: clip out scrollbar */
- ltk_rect real_clip = ltk_rect_intersect((ltk_rect){0, 0, self->lrect.w, self->lrect.h}, clip);
- for (size_t i = 0; i < box->num_widgets; i++) {
- ptr = box->widgets[i];
- /* FIXME: Maybe continue immediately if widget is
- obviously outside of clipping rect */
- ptr->vtable->draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, real_clip));
- }
- box->sc->widget.vtable->draw(
- LTK_CAST_WIDGET(box->sc), s,
- x + box->sc->widget.lrect.x,
- y + box->sc->widget.lrect.y,
- ltk_rect_relative(box->sc->widget.lrect, real_clip)
- );
-}
-
-ltk_box *
-ltk_box_create(ltk_window *window, ltk_orientation orient) {
- ltk_box *box = ltk_malloc(sizeof(ltk_box));
- ltk_widget *self = LTK_CAST_WIDGET(box);
-
- ltk_fill_widget_defaults(self, window, &vtable, 0, 0);
-
- box->sc = ltk_scrollbar_create(window, orient);
- box->sc->widget.parent = self;
- ltk_widget_register_signal_handler(
- LTK_CAST_WIDGET(box->sc), LTK_SCROLLBAR_SIGNAL_SCROLL,
- <k_box_scroll_cb, LTK_MAKE_ARG_WIDGET(self)
- );
- box->widgets = NULL;
- box->num_alloc = 0;
- box->num_widgets = 0;
- box->orient = orient;
- if (orient == LTK_HORIZONTAL)
- box->widget.ideal_h = box->sc->widget.ideal_h;
- else
- box->widget.ideal_w = box->sc->widget.ideal_w;
- ltk_recalculate_box(self);
-
- return box;
-}
-
-static void
-ltk_box_ensure_rect_shown(ltk_widget *self, ltk_rect r) {
- ltk_box *box = LTK_CAST_BOX(self);
- int delta = 0;
- if (box->orient == LTK_HORIZONTAL) {
- if (r.x + r.w > self->lrect.w && r.w <= self->lrect.w)
- delta = r.x - (self->lrect.w - r.w);
- else if (r.x < 0 || r.w > self->lrect.w)
- delta = r.x;
- } else {
- if (r.y + r.h > self->lrect.h && r.h <= self->lrect.h)
- delta = r.y - (self->lrect.h - r.h);
- else if (r.y < 0 || r.h > self->lrect.h)
- delta = r.y;
- }
- if (delta)
- ltk_scrollbar_scroll(LTK_CAST_WIDGET(box->sc), delta, 0);
-}
-
-static void
-ltk_box_destroy(ltk_widget *self, int shallow) {
- ltk_box *box = LTK_CAST_BOX(self);
- ltk_widget *ptr;
- for (size_t i = 0; i < box->num_widgets; i++) {
- ptr = box->widgets[i];
- ptr->parent = NULL;
- if (!shallow)
- ltk_widget_destroy(ptr, shallow);
- }
- ltk_free(box->widgets);
- box->sc->widget.parent = NULL;
- ltk_widget_destroy(LTK_CAST_WIDGET(box->sc), 0);
- ltk_free(box);
-}
-
-/* FIXME: Make this function name more consistent */
-/* FIXME: The widget positions are set with the old scrollbar->cur_pos, before the
- virtual_size is set - this can cause problems when a widget changes its size
- (in the scrolled direction) when resized. */
-/* FIXME: avoid complete recalculation when just scrolling (only position updated) */
-static void
-ltk_recalculate_box(ltk_widget *self) {
- ltk_box *box = LTK_CAST_BOX(self);
- ltk_widget *ptr;
- ltk_rect *sc_rect = &box->sc->widget.lrect;
- int cur_pos = 0;
- if (box->orient == LTK_HORIZONTAL)
- sc_rect->h = box->sc->widget.ideal_h;
- else
- sc_rect->w = box->sc->widget.ideal_w;
- for (size_t i = 0; i < box->num_widgets; i++) {
- ptr = box->widgets[i];
- if (box->orient == LTK_HORIZONTAL) {
- ptr->lrect.x = cur_pos - box->sc->cur_pos;
- if (ptr->sticky & LTK_STICKY_TOP && ptr->sticky & LTK_STICKY_BOTTOM)
- ptr->lrect.h = box->widget.lrect.h - sc_rect->h;
- if (ptr->sticky & LTK_STICKY_TOP)
- ptr->lrect.y = 0;
- else if (ptr->sticky & LTK_STICKY_BOTTOM)
- ptr->lrect.y = box->widget.lrect.h - ptr->lrect.h - sc_rect->h;
- else
- ptr->lrect.y = (box->widget.lrect.h - ptr->lrect.h) / 2;
- cur_pos += ptr->lrect.w;
- } else {
- ptr->lrect.y = cur_pos - box->sc->cur_pos;
- if (ptr->sticky & LTK_STICKY_LEFT && ptr->sticky & LTK_STICKY_RIGHT)
- ptr->lrect.w = box->widget.lrect.w - sc_rect->w;
- if (ptr->sticky & LTK_STICKY_LEFT)
- ptr->lrect.x = 0;
- else if (ptr->sticky & LTK_STICKY_RIGHT)
- ptr->lrect.x = box->widget.lrect.w - ptr->lrect.w - sc_rect->w;
- else
- ptr->lrect.x = (box->widget.lrect.w - ptr->lrect.w) / 2;
- cur_pos += ptr->lrect.h;
- }
- ptr->crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, ptr->lrect);
- ltk_widget_resize(ptr);
- }
- ltk_scrollbar_set_virtual_size(box->sc, cur_pos);
- if (box->orient == LTK_HORIZONTAL) {
- sc_rect->x = 0;
- sc_rect->y = box->widget.lrect.h - sc_rect->h;
- sc_rect->w = box->widget.lrect.w;
- } else {
- sc_rect->x = box->widget.lrect.w - sc_rect->w;
- sc_rect->y = 0;
- sc_rect->h = box->widget.lrect.h;
- }
- *sc_rect = ltk_rect_intersect(*sc_rect, (ltk_rect){0, 0, box->widget.lrect.w, box->widget.lrect.h});
- box->sc->widget.crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, *sc_rect);
- ltk_widget_resize(LTK_CAST_WIDGET(box->sc));
-}
-
-/* FIXME: This entire resizing thing is a bit weird. For instance, if a label
- in a vertical box increases its height because its width has been decreased
- and it is forced to wrap, should that just change the rect or also the
- ideal size? Ideal size wouldn't really make sense here, but then the box
- might be forced to add a scrollbar even though the parent widget would
- actually give it more space if it knew that it needed it. */
-
-static void
-ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget) {
- ltk_box *box = LTK_CAST_BOX(self);
- short size_changed = 0;
- /* This is always reset here - if it needs to be changed,
- the resize function called by the last child_size_change
- function will fix it */
- /* Note: This seems a bit weird, but if each widget set its rect itself,
- that would also lead to weird things. For instance, if a butten is
- added to after a box after being ungridded, and its rect was changed
- by the grid (e.g. because of a column weight), who should reset the
- rect if it doesn't have sticky set? Of course, the resize function
- could also set all widgets even if they don't have any sticky
- settings, but there'd probably be some catch as well. */
- /* FIXME: the same comment as in grid.c applies */
- int orig_w = widget->lrect.w;
- int orig_h = widget->lrect.h;
- widget->lrect.w = widget->ideal_w;
- widget->lrect.h = widget->ideal_h;
- int sc_w = box->sc->widget.lrect.w;
- int sc_h = box->sc->widget.lrect.h;
- if (box->orient == LTK_HORIZONTAL && widget->ideal_h + sc_h > box->widget.ideal_h) {
- box->widget.ideal_h = widget->ideal_h + sc_h;
- size_changed = 1;
- } else if (box->orient == LTK_VERTICAL && widget->ideal_w + sc_w > box->widget.ideal_h) {
- box->widget.ideal_w = widget->ideal_w + sc_w;
- size_changed = 1;
- }
-
- if (size_changed && box->widget.parent && box->widget.parent->vtable->child_size_change)
- box->widget.parent->vtable->child_size_change(box->widget.parent, (ltk_widget *)box);
- else
- ltk_recalculate_box(LTK_CAST_WIDGET(box));
- if (orig_w != widget->lrect.w || orig_h != widget->lrect.h)
- ltk_widget_resize(widget);
-}
-
-int
-ltk_box_add(ltk_box *box, ltk_widget *widget, ltk_sticky_mask sticky) {
- if (widget->parent)
- return 1;
- if (box->num_widgets >= box->num_alloc) {
- size_t new_size = box->num_alloc > 0 ? box->num_alloc * 2 : 4;
- ltk_widget **new = ltk_realloc(box->widgets, new_size * sizeof(ltk_widget *));
- box->num_alloc = new_size;
- box->widgets = new;
- }
-
- int sc_w = box->sc->widget.lrect.w;
- int sc_h = box->sc->widget.lrect.h;
-
- box->widgets[box->num_widgets++] = widget;
- if (box->orient == LTK_HORIZONTAL) {
- box->widget.ideal_w += widget->ideal_w;
- if (widget->ideal_h + sc_h > box->widget.ideal_h)
- box->widget.ideal_h = widget->ideal_h + sc_h;
- } else {
- box->widget.ideal_h += widget->ideal_h;
- if (widget->ideal_w + sc_w > box->widget.ideal_w)
- box->widget.ideal_w = widget->ideal_w + sc_w;
- }
- widget->parent = LTK_CAST_WIDGET(box);
- widget->sticky = sticky;
- ltk_box_child_size_change(LTK_CAST_WIDGET(box), widget);
- ltk_window_invalidate_widget_rect(box->widget.window, LTK_CAST_WIDGET(box));
-
- return 0;
-}
-
-int
-ltk_box_remove_index(ltk_box *box, size_t index) {
- if (index >= box->num_widgets)
- return 1;
- ltk_widget *self = LTK_CAST_WIDGET(box);
- ltk_widget *widget = box->widgets[index];
- int sc_w = box->sc->widget.lrect.w;
- int sc_h = box->sc->widget.lrect.h;
- if (index < box->num_widgets - 1)
- memmove(box->widgets + index, box->widgets + index + 1,
- (box->num_widgets - index - 1) * sizeof(ltk_widget *));
- box->num_widgets--;
- ltk_window_invalidate_widget_rect(self->window, self);
- /* search for new ideal width/height */
- /* FIXME: make this all a bit nicer and break the lines better */
- /* FIXME: other part of ideal size not updated */
- if (box->orient == LTK_HORIZONTAL && widget->ideal_h + sc_h == self->ideal_h) {
- self->ideal_h = 0;
- for (size_t j = 0; j < box->num_widgets; j++) {
- if (box->widgets[j]->ideal_h + sc_h > self->ideal_h)
- self->ideal_h = box->widgets[j]->ideal_h + sc_h;
- }
- if (self->parent)
- ltk_widget_resize(self->parent);
- } else if (box->orient == LTK_VERTICAL && widget->ideal_w + sc_w == self->ideal_w) {
- self->ideal_w = 0;
- for (size_t j = 0; j < box->num_widgets; j++) {
- if (box->widgets[j]->ideal_w + sc_w > self->ideal_w)
- self->ideal_w = box->widgets[j]->ideal_w + sc_w;
- }
- if (self->parent)
- ltk_widget_resize(self->parent);
- }
- return 0;
-}
-
-int
-ltk_box_remove(ltk_box *box, ltk_widget *widget) {
- if (widget->parent != LTK_CAST_WIDGET(box))
- return 1;
- widget->parent = NULL;
- for (size_t i = 0; i < box->num_widgets; i++) {
- if (box->widgets[i] == widget) {
- return ltk_box_remove_index(box, i);
- }
- }
-
- return 1;
-}
-
-static int
-ltk_box_remove_child(ltk_widget *self, ltk_widget *widget) {
- return ltk_box_remove(LTK_CAST_BOX(self), widget);
-}
-
-/* FIXME: maybe come up with a more efficient method */
-static ltk_widget *
-ltk_box_nearest_child(ltk_widget *self, ltk_rect rect) {
- ltk_box *box = LTK_CAST_BOX(self);
- ltk_widget *minw = NULL;
- int min_dist = INT_MAX;
- for (size_t i = 0; i < box->num_widgets; i++) {
- ltk_rect r = box->widgets[i]->lrect;
- int dist = ltk_rect_fakedist(rect, r);
- if (dist < min_dist) {
- min_dist = dist;
- minw = box->widgets[i];
- }
- }
- return minw;
-}
-
-static ltk_widget *
-ltk_box_nearest_child_left(ltk_widget *self, ltk_widget *widget) {
- ltk_box *box = LTK_CAST_BOX(self);
- if (box->orient == LTK_VERTICAL)
- return NULL;
- return ltk_box_prev_child(self, widget);
-}
-
-static ltk_widget *
-ltk_box_nearest_child_right(ltk_widget *self, ltk_widget *widget) {
- ltk_box *box = LTK_CAST_BOX(self);
- if (box->orient == LTK_VERTICAL)
- return NULL;
- return ltk_box_next_child(self, widget);
-}
-
-static ltk_widget *
-ltk_box_nearest_child_above(ltk_widget *self, ltk_widget *widget) {
- ltk_box *box = LTK_CAST_BOX(self);
- if (box->orient == LTK_HORIZONTAL)
- return NULL;
- return ltk_box_prev_child(self, widget);
-}
-
-static ltk_widget *
-ltk_box_nearest_child_below(ltk_widget *self, ltk_widget *widget) {
- ltk_box *box = LTK_CAST_BOX(self);
- if (box->orient == LTK_HORIZONTAL)
- return NULL;
- return ltk_box_next_child(self, widget);
-}
-
-static ltk_widget *
-ltk_box_prev_child(ltk_widget *self, ltk_widget *child) {
- ltk_box *box = LTK_CAST_BOX(self);
- for (size_t i = box->num_widgets; i-- > 0;) {
- if (box->widgets[i] == child)
- return i > 0 ? box->widgets[i-1] : NULL;
- }
- return NULL;
-}
-
-static ltk_widget *
-ltk_box_next_child(ltk_widget *self, ltk_widget *child) {
- ltk_box *box = LTK_CAST_BOX(self);
- for (size_t i = 0; i < box->num_widgets; i++) {
- if (box->widgets[i] == child)
- return i < box->num_widgets - 1 ? box->widgets[i+1] : NULL;
- }
- return NULL;
-}
-
-static ltk_widget *
-ltk_box_first_child(ltk_widget *self) {
- ltk_box *box = LTK_CAST_BOX(self);
- return box->num_widgets > 0 ? box->widgets[0] : NULL;
-}
-
-static ltk_widget *
-ltk_box_last_child(ltk_widget *self) {
- ltk_box *box = LTK_CAST_BOX(self);
- return box->num_widgets > 0 ? box->widgets[box->num_widgets-1] : NULL;
-}
-
-static int
-ltk_box_scroll_cb(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
- (void)self;
- (void)args;
- ltk_widget *boxw = LTK_CAST_ARG_WIDGET(data);
- ltk_recalculate_box(boxw);
- ltk_window_invalidate_widget_rect(boxw->window, boxw);
- return 1;
-}
-
-static ltk_widget *
-ltk_box_get_child_at_pos(ltk_widget *self, int x, int y) {
- ltk_box *box = LTK_CAST_BOX(self);
- if (ltk_collide_rect(box->sc->widget.crect, x, y))
- return (ltk_widget *)box->sc;
- for (size_t i = 0; i < box->num_widgets; i++) {
- if (ltk_collide_rect(box->widgets[i]->crect, x, y))
- return box->widgets[i];
- }
- return NULL;
-}
-
-static int
-ltk_box_mouse_scroll(ltk_widget *self, ltk_scroll_event *event) {
- ltk_box *box = LTK_CAST_BOX(self);
- if (event->dy) {
- /* FIXME: horizontal scrolling, etc. */
- /* FIXME: configure scrollstep */
- int delta = event->dy * -15;
- ltk_scrollbar_scroll(LTK_CAST_WIDGET(box->sc), delta, 0);
- ltk_point glob = ltk_widget_pos_to_global(self, event->x, event->y);
- ltk_window_fake_motion_event(self->window, glob.x, glob.y);
- return 1;
- }
- return 0;
-}
diff --git a/src/graphics.h b/src/graphics.h
@@ -1,76 +0,0 @@
-/*
- * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef LTK_GRAPHICS_H
-#define LTK_GRAPHICS_H
-
-typedef struct ltk_renderdata ltk_renderdata;
-typedef struct ltk_renderwindow ltk_renderwindow;
-
-#include <stddef.h>
-
-#include "rect.h"
-#include "color.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;
-
-typedef struct ltk_surface ltk_surface;
-
-/* FIXME: graphics context */
-ltk_surface *ltk_surface_create(ltk_renderwindow *window, int w, int h);
-void ltk_surface_destroy(ltk_surface *s);
-/* returns 0 if successful, 1 if not resizable */
-int ltk_surface_resize(ltk_surface *s, int w, int h);
-/* FIXME: kind of hacky */
-void ltk_surface_update_size(ltk_surface *s, int w, int h);
-ltk_surface *ltk_surface_from_window(ltk_renderwindow *window, int w, int h);
-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, especially 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_draw_border_clipped(ltk_surface *s, ltk_color *c, ltk_rect rect, ltk_rect clip_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);
-void ltk_surface_fill_polygon_clipped(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints, ltk_rect clip);
-
-/* TODO */
-/*
-void ltk_surface_draw_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2, int line_width);
-void ltk_surface_fill_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2);
-void ltk_surface_draw_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r, int line_width);
-void ltk_surface_fill_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r);
-*/
-
-void renderer_set_imspot(ltk_renderwindow *window, int x, int y);
-ltk_renderdata *renderer_create(void);
-ltk_renderwindow *renderer_create_window(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h);
-void renderer_destroy_window(ltk_renderwindow *window);
-void renderer_destroy(ltk_renderdata *data);
-void renderer_set_window_properties(ltk_renderwindow *window, ltk_color *bg);
-/* FIXME: this is kind of out of place */
-void renderer_swap_buffers(ltk_renderwindow *window);
-/* FIXME: this is just for the socket name in ltkd and is a bit weird */
-unsigned long renderer_get_window_id(ltk_renderwindow *window);
-
-#endif /* LTK_GRAPHICS_H */
diff --git a/src/graphics_xlib.c b/src/graphics_xlib.c
@@ -1,600 +0,0 @@
-/*
- * Copyright (c) 2022-2024 lumidify <nobody@lumidify.org>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <stdio.h>
-#include <string.h>
-
-#include <X11/XKBlib.h>
-#include <X11/Xlib.h>
-#include <X11/Xutil.h>
-#include <X11/extensions/XKB.h>
-#include <X11/extensions/Xdbe.h>
-#include <X11/extensions/dbe.h>
-
-#include "graphics_xlib.h"
-#include "color.h"
-#include "memory.h"
-#include "rect.h"
-#include "util.h"
-
-struct ltk_surface {
- int w, h;
- ltk_renderwindow *window;
- Drawable d;
- #if USE_XFT == 1
- XftDraw *xftdraw;
- #endif
- char resizable;
-};
-
-ltk_surface *
-ltk_surface_create(ltk_renderwindow *window, int w, int h) {
- ltk_surface *s = ltk_malloc(sizeof(ltk_surface));
- if (w <= 0)
- w = 1;
- if (h <= 0)
- h = 1;
- s->w = w;
- s->h = h;
- s->window = window;
- s->d = XCreatePixmap(window->renderdata->dpy, window->xwindow, w, h, window->renderdata->depth);
- #if USE_XFT == 1
- s->xftdraw = XftDrawCreate(window->renderdata->dpy, s->d, window->renderdata->vis, window->renderdata->cm);
- #endif
- s->resizable = 1;
- return s;
-}
-
-ltk_surface *
-ltk_surface_from_window(ltk_renderwindow *window, int w, int h) {
- ltk_surface *s = ltk_malloc(sizeof(ltk_surface));
- s->w = w;
- s->h = h;
- s->window = window;
- s->d = window->drawable;
- #if USE_XFT == 1
- s->xftdraw = XftDrawCreate(window->renderdata->dpy, s->d, window->renderdata->vis, window->renderdata->cm);
- #endif
- s->resizable = 0;
- return s;
-}
-
-void
-ltk_surface_destroy(ltk_surface *s) {
- #if USE_XFT == 1
- XftDrawDestroy(s->xftdraw);
- #endif
- if (s->resizable)
- XFreePixmap(s->window->renderdata->dpy, (Pixmap)s->d);
- ltk_free(s);
-}
-
-void
-ltk_surface_update_size(ltk_surface *s, int w, int h) {
- /* FIXME: maybe return directly if surface is resizable? */
- s->w = w;
- s->h = h;
- /* FIXME: sort of hacky (this is only used by window surface) */
- #if USE_XFT == 1
- XftDrawChange(s->xftdraw, s->d);
- #endif
-}
-
-int
-ltk_surface_resize(ltk_surface *s, int w, int h) {
- if (!s->resizable)
- return 1;
- s->w = w;
- s->h = h;
- XFreePixmap(s->window->renderdata->dpy, (Pixmap)s->d);
- s->d = XCreatePixmap(s->window->renderdata->dpy, s->window->xwindow, w, h, s->window->renderdata->depth);
- #if USE_XFT == 1
- XftDrawChange(s->xftdraw, s->d);
- #endif
- return 0;
-}
-
-void
-ltk_surface_get_size(ltk_surface *s, int *w, int *h) {
- *w = s->w;
- *h = s->h;
-}
-
-void
-ltk_surface_draw_rect(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width) {
- XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
- XSetLineAttributes(s->window->renderdata->dpy, s->window->gc, line_width, LineSolid, CapButt, JoinMiter);
- XDrawRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, rect.h);
-}
-
-void
-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->renderdata->dpy, s->window->gc, c->xcolor.pixel);
- if (border_sides & LTK_BORDER_TOP)
- XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, line_width);
- if (border_sides & LTK_BORDER_BOTTOM)
- XFillRectangle(s->window->renderdata->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->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, line_width, rect.h);
- if (border_sides & LTK_BORDER_RIGHT)
- XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x + rect.w - line_width, rect.y, line_width, rect.h);
-}
-
-void
-ltk_surface_draw_border_clipped(ltk_surface *s, ltk_color *c, ltk_rect rect, ltk_rect clip_rect, int line_width, ltk_border_sides border_sides) {
- if (line_width <= 0)
- return;
- XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
- int width;
- ltk_rect final_rect = ltk_rect_intersect(rect, clip_rect);
- if (border_sides & LTK_BORDER_TOP) {
- width = rect.y - final_rect.y;
- if (width > -line_width) {
- width = line_width + width;
- XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, final_rect.x, final_rect.y, final_rect.w, width);
- }
- }
- if (border_sides & LTK_BORDER_BOTTOM) {
- width = (final_rect.y + final_rect.h) - (rect.y + rect.h);
- if (width > -line_width) {
- width = line_width + width;
- XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, final_rect.x, final_rect.y + final_rect.h - width, final_rect.w, width);
- }
- }
- if (border_sides & LTK_BORDER_LEFT) {
- width = rect.x - final_rect.x;
- if (width > -line_width) {
- width = line_width + width;
- XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, final_rect.x, final_rect.y, width, final_rect.h);
- }
- }
- if (border_sides & LTK_BORDER_RIGHT) {
- width = (final_rect.x + final_rect.w) - (rect.x + rect.w);
- if (width > -line_width) {
- width = line_width + width;
- XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, final_rect.x + final_rect.w - width, final_rect.y, width, final_rect.h);
- }
- }
-}
-
-void
-ltk_surface_fill_rect(ltk_surface *s, ltk_color *c, ltk_rect rect) {
- XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
- XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, rect.h);
-}
-
-void
-ltk_surface_fill_polygon(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints) {
- /* FIXME: maybe make this static 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->renderdata->dpy, s->window->gc, c->xcolor.pixel);
- XFillPolygon(s->window->renderdata->dpy, s->d, s->window->gc, final_points, (int)npoints, Complex, CoordModeOrigin);
- if (npoints > 6)
- ltk_free(final_points);
-}
-
-static inline void
-swap_ptr(void **ptr1, void **ptr2) {
- void *tmp = *ptr1;
- *ptr1 = *ptr2;
- *ptr2 = tmp;
-}
-
-#define check_size(cond) if (!(cond)) ltk_fatal("Unable to perform polygon clipping. This is a bug, tell lumidify about it.\n")
-
-/* FIXME: xlib already includes clipping... */
-/* FIXME: this can probably be optimized */
-/* This is basically Sutherland-Hodgman, but only the special case for clipping rectangles. */
-void
-ltk_surface_fill_polygon_clipped(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints, ltk_rect clip) {
- /* FIXME: is this even more efficient? */
- XPoint tmp_points1[12]; /* to avoid extra allocations when not necessary */
- XPoint tmp_points2[12];
- XPoint *points1;
- XPoint *points2;
- /* FIXME: be a bit smarter about this */
- if (npoints <= 6) {
- points1 = tmp_points1;
- points2 = tmp_points2;
- } else {
- /* FIXME: I'm pretty sure there can never be more points than this
- since we're only clipping against a rectangle, right?
- If I can be sure about that, I can remove all the check_size's below. */
- points1 = ltk_reallocarray(NULL, npoints, sizeof(XPoint) * 2);
- points2 = ltk_reallocarray(NULL, npoints, sizeof(XPoint) * 2);
- }
-
- size_t num1 = npoints;
- size_t num2 = 0;
- for (size_t i = 0; i < npoints; i++) {
- points1[i].x = (short)points[i].x;
- points1[i].y = (short)points[i].y;
- }
-
- for (size_t i = 0; i < num1; i++) {
- XPoint p1 = points1[i];
- XPoint p2 = points1[(i + 1) % num1];
- if (p1.x >= clip.x) {
- check_size(num2 < npoints * 2);
- points2[num2++] = p1;
- if (p2.x < clip.x) {
- check_size(num2 < npoints * 2);
- points2[num2++] = (XPoint){.x = (short)clip.x, .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x - p1.x) / (p2.x - p1.x))};
- }
- } else if (p2.x >= clip.x) {
- check_size(num2 < npoints * 2);
- points2[num2++] = (XPoint){.x = (short)clip.x, .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x - p1.x) / (p2.x - p1.x))};
- }
- }
- num1 = num2;
- num2 = 0;
- swap_ptr((void**)&points1, (void**)&points2);
-
- for (size_t i = 0; i < num1; i++) {
- XPoint p1 = points1[i];
- XPoint p2 = points1[(i + 1) % num1];
- if (p1.x <= clip.x + clip.w) {
- check_size(num2 < npoints * 2);
- points2[num2++] = p1;
- if (p2.x > clip.x + clip.w) {
- check_size(num2 < npoints * 2);
- points2[num2++] = (XPoint){.x = (short)(clip.x + clip.w), .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x + clip.w - p1.x) / (p2.x - p1.x))};
- }
- } else if (p2.x <= clip.x + clip.w) {
- check_size(num2 < npoints * 2);
- points2[num2++] = (XPoint){.x = (short)(clip.x + clip.w), .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x + clip.w - p1.x) / (p2.x - p1.x))};
- }
- }
- num1 = num2;
- num2 = 0;
- swap_ptr((void**)&points1, (void**)&points2);
-
- for (size_t i = 0; i < num1; i++) {
- XPoint p1 = points1[i];
- XPoint p2 = points1[(i + 1) % num1];
- if (p1.y >= clip.y) {
- check_size(num2 < npoints * 2);
- points2[num2++] = p1;
- if (p2.y < clip.y) {
- check_size(num2 < npoints * 2);
- points2[num2++] = (XPoint){.y = (short)clip.y, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y - p1.y) / (p2.y - p1.y))};
- }
- } else if (p2.y >= clip.y) {
- check_size(num2 < npoints * 2);
- points2[num2++] = (XPoint){.y = (short)clip.y, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y - p1.y) / (p2.y - p1.y))};
- }
- }
- num1 = num2;
- num2 = 0;
- swap_ptr((void**)&points1, (void**)&points2);
-
- for (size_t i = 0; i < num1; i++) {
- XPoint p1 = points1[i];
- XPoint p2 = points1[(i + 1) % num1];
- if (p1.y <= clip.y + clip.h) {
- check_size(num2 < npoints * 2);
- points2[num2++] = p1;
- if (p2.y > clip.y + clip.h) {
- check_size(num2 < npoints * 2);
- points2[num2++] = (XPoint){.y = (short)clip.y + clip.h, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y + clip.h - p1.y) / (p2.y - p1.y))};
- }
- } else if (p2.y <= clip.y + clip.h) {
- check_size(num2 < npoints * 2);
- points2[num2++] = (XPoint){.y = (short)clip.y + clip.h, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y + clip.h - p1.y) / (p2.y - p1.y))};
- }
- }
-
- if (num2 > 0) {
- XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
- XFillPolygon(s->window->renderdata->dpy, s->d, s->window->gc, points2, (int)num2, Complex, CoordModeOrigin);
- }
- if (npoints > 6) {
- ltk_free(points1);
- ltk_free(points2);
- }
-}
-
-void
-ltk_surface_copy(ltk_surface *src, ltk_surface *dst, ltk_rect src_rect, int dst_x, int dst_y) {
- XCopyArea(
- src->window->renderdata->dpy, src->d, dst->d, src->window->gc,
- src_rect.x, src_rect.y, src_rect.w, src_rect.h, dst_x, dst_y
- );
-}
-
-/* TODO */
-/*
-void
-ltk_surface_draw_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2, int line_width) {
-}
-
-void
-ltk_surface_fill_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2) {
-}
-
-void
-ltk_surface_draw_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r, int line_width) {
-}
-
-void
-ltk_surface_fill_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r) {
-}
-*/
-
-#if USE_XFT == 1
-XftDraw *
-ltk_surface_get_xft_draw(ltk_surface *s) {
- return s->xftdraw;
-}
-#endif
-
-Drawable
-ltk_surface_get_drawable(ltk_surface *s) {
- return s->d;
-}
-
-/* FIXME: move this to a file where it makes more sense */
-/* blatantly stolen from st */
-static void ximinstantiate(Display *dpy, XPointer client, XPointer call);
-static void ximdestroy(XIM xim, XPointer client, XPointer call);
-static int xicdestroy(XIC xim, XPointer client, XPointer call);
-static int ximopen(ltk_renderwindow *window);
-
-static void
-ximdestroy(XIM xim, XPointer client, XPointer call) {
- (void)xim;
- (void)call;
- ltk_renderwindow *window = (ltk_renderwindow *)client;
- window->xim = NULL;
- XRegisterIMInstantiateCallback(
- window->renderdata->dpy, NULL, NULL, NULL, ximinstantiate, (XPointer)window
- );
- XFree(window->spotlist);
-}
-
-static int
-xicdestroy(XIC xim, XPointer client, XPointer call) {
- (void)xim;
- (void)call;
- ltk_renderwindow *window = (ltk_renderwindow *)client;
- window->xic = NULL;
- return 1;
-}
-
-static int
-ximopen(ltk_renderwindow *window) {
- XIMCallback imdestroy = { .client_data = (XPointer)window, .callback = ximdestroy };
- XICCallback icdestroy = { .client_data = (XPointer)window, .callback = xicdestroy };
-
- window->xim = XOpenIM(window->renderdata->dpy, NULL, NULL, NULL);
- if (window->xim == NULL)
- return 0;
-
- if (XSetIMValues(window->xim, XNDestroyCallback, &imdestroy, NULL))
- ltk_warn("XSetIMValues: Could not set XNDestroyCallback.\n");
-
- window->spotlist = XVaCreateNestedList(0, XNSpotLocation, &window->spot, NULL);
-
- if (window->xic == NULL) {
- window->xic = XCreateIC(
- window->xim, XNInputStyle,
- XIMPreeditNothing | XIMStatusNothing,
- XNClientWindow, window->xwindow,
- XNDestroyCallback, &icdestroy, NULL
- );
- }
- if (window->xic == NULL)
- ltk_warn("XCreateIC: Could not create input context.\n");
-
- return 1;
-}
-
-static void
-ximinstantiate(Display *dpy, XPointer client, XPointer call) {
- (void)call;
- ltk_renderwindow *window = (ltk_renderwindow *)client;
- if (ximopen(window)) {
- XUnregisterIMInstantiateCallback(
- dpy, NULL, NULL, NULL, ximinstantiate, (XPointer)window
- );
- }
-}
-
-void
-renderer_set_imspot(ltk_renderwindow *window, int x, int y) {
- if (window->xic == NULL)
- return;
- window->spot.x = x;
- window->spot.y = y;
- XSetICValues(window->xic, XNPreeditAttributes, window->spotlist, NULL);
-}
-
-ltk_renderdata *
-renderer_create(void) {
- /* FIXME: this might not be the best place for this */
- XSetLocaleModifiers("");
- ltk_renderdata *renderdata = ltk_malloc(sizeof(ltk_renderdata));
- renderdata->dpy = XOpenDisplay(NULL);
- renderdata->screen = DefaultScreen(renderdata->dpy);
- renderdata->db_enabled = 0;
- /* based on http://wili.cc/blog/xdbe.html */
- int major, minor;
- if (XdbeQueryExtension(renderdata->dpy, &major, &minor)) {
- int num_screens = 1;
- Drawable screens[] = {DefaultRootWindow(renderdata->dpy)};
- XdbeScreenVisualInfo *info = XdbeGetVisualInfo(
- renderdata->dpy, screens, &num_screens
- );
- if (!info || num_screens < 1 || info->count < 1) {
- ltk_fatal("No visuals support Xdbe.");
- }
- XVisualInfo xvisinfo_templ;
- /* we know there's at least one */
- xvisinfo_templ.visualid = info->visinfo[0].visual;
- /* FIXME: proper screen number? */
- xvisinfo_templ.screen = 0;
- xvisinfo_templ.depth = info->visinfo[0].depth;
- int matches;
- XVisualInfo *xvisinfo_match = XGetVisualInfo(
- renderdata->dpy,
- VisualIDMask | VisualScreenMask | VisualDepthMask,
- &xvisinfo_templ, &matches
- );
- if (!xvisinfo_match || matches < 1) {
- ltk_fatal("Couldn't match a Visual with double buffering.\n");
- }
- renderdata->vis = xvisinfo_match->visual;
- /* FIXME: is it legal to free this while keeping the visual? */
- XFree(xvisinfo_match);
- XdbeFreeVisualInfo(info);
- renderdata->db_enabled = 1;
- } else {
- renderdata->vis = DefaultVisual(renderdata->dpy, renderdata->screen);
- ltk_warn("No Xdbe support.\n");
- }
- renderdata->cm = DefaultColormap(renderdata->dpy, renderdata->screen);
- renderdata->wm_delete_msg = XInternAtom(renderdata->dpy, "WM_DELETE_WINDOW", False);
- renderdata->depth = DefaultDepth(renderdata->dpy, renderdata->screen);
- renderdata->xkb_supported = 1;
- renderdata->xkb_event_type = 0;
- if (!XkbQueryExtension(renderdata->dpy, 0, &renderdata->xkb_event_type, NULL, &major, &minor)) {
- ltk_warn("XKB not supported.\n");
- renderdata->xkb_supported = 0;
- } else {
- /* This should select the events when the keyboard mapping changes.
- * When e.g. 'setxkbmap us' is executed, two events are sent, but I
- * haven't figured out how to change that. When the xkb layout
- * switching is used (e.g. 'setxkbmap -option grp:shifts_toggle'),
- * this issue does not occur because only a state event is sent. */
- XkbSelectEvents(
- renderdata->dpy, XkbUseCoreKbd,
- XkbNewKeyboardNotifyMask, XkbNewKeyboardNotifyMask
- );
- XkbSelectEventDetails(
- renderdata->dpy, XkbUseCoreKbd, XkbStateNotify,
- XkbAllStateComponentsMask, XkbGroupStateMask
- );
- }
- return renderdata;
-}
-
-ltk_renderwindow *
-renderer_create_window(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h) {
- XSetWindowAttributes attrs;
- ltk_renderwindow *window = ltk_malloc(sizeof(ltk_renderwindow));
- window->renderdata = data;
- memset(&attrs, 0, sizeof(attrs));
- attrs.background_pixel = BlackPixel(data->dpy, data->screen);
- attrs.colormap = data->cm;
- attrs.border_pixel = WhitePixel(data->dpy, data->screen);
- /* this causes the window contents to be kept
- * when it is resized, leading to less flicker */
- attrs.bit_gravity = NorthWestGravity;
- attrs.event_mask =
- ExposureMask | KeyPressMask | KeyReleaseMask |
- ButtonPressMask | ButtonReleaseMask |
- StructureNotifyMask | PointerMotionMask;
- /* FIXME: set border width */
- window->xwindow = XCreateWindow(
- data->dpy, DefaultRootWindow(data->dpy), x, y,
- w, h, 0, data->depth,
- InputOutput, data->vis,
- CWBackPixel | CWColormap | CWBitGravity | CWEventMask | CWBorderPixel, &attrs
- );
-
- if (data->db_enabled) {
- window->back_buf = XdbeAllocateBackBufferName(
- data->dpy, window->xwindow, XdbeBackground
- );
- } else {
- window->back_buf = window->xwindow;
- }
- window->drawable = window->back_buf;
- window->gc = XCreateGC(data->dpy, window->xwindow, 0, 0);
- XSetStandardProperties(
- data->dpy, window->xwindow,
- title, NULL, None, NULL, 0, NULL
- );
- /* FIXME: check return value */
- XSetWMProtocols(data->dpy, window->xwindow, &data->wm_delete_msg, 1);
-
- window->xic = NULL;
- window->xim = NULL;
- if (!ximopen(window)) {
- XRegisterIMInstantiateCallback(
- window->renderdata->dpy, NULL, NULL, NULL,
- ximinstantiate, (XPointer)window
- );
- }
-
- XClearWindow(window->renderdata->dpy, window->xwindow);
- XMapRaised(window->renderdata->dpy, window->xwindow);
-
- return window;
-}
-
-void
-renderer_destroy_window(ltk_renderwindow *window) {
- XFreeGC(window->renderdata->dpy, window->gc);
- if (window->spotlist)
- XFree(window->spotlist);
- /* FIXME: destroy xim/xic? */
- XDestroyWindow(window->renderdata->dpy, window->xwindow);
- ltk_free(window);
-}
-
-void
-renderer_destroy(ltk_renderdata *renderdata) {
- XCloseDisplay(renderdata->dpy);
- /* FIXME: destroy visual, wm_delete_msg, etc.? */
- ltk_free(renderdata);
-}
-
-/* FIXME: this is a completely random collection of properties and should be
- changed to a more sensible list */
-void
-renderer_set_window_properties(ltk_renderwindow *window, ltk_color *bg) {
- XSetWindowBackground(window->renderdata->dpy, window->xwindow, bg->xcolor.pixel);
-}
-
-void
-renderer_swap_buffers(ltk_renderwindow *window) {
- XdbeSwapInfo swap_info;
- swap_info.swap_window = window->xwindow;
- swap_info.swap_action = XdbeBackground;
- if (!XdbeSwapBuffers(window->renderdata->dpy, &swap_info, 1))
- ltk_fatal("Unable to swap buffers.\n");
- XFlush(window->renderdata->dpy);
-}
-
-unsigned long
-renderer_get_window_id(ltk_renderwindow *window) {
- return (unsigned long)window->xwindow;
-}
diff --git a/src/grid.c b/src/grid.c
@@ -1,546 +0,0 @@
-/* FIXME: sometimes, resizing doesn't work properly when running test.sh */
-
-/*
- * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-/* TODO: make ungrid function also adjust static row/column width/height
- -> also, how should the grid deal with a widget spanning over multiple
- rows/columns with static size - if all are static, it could just
- divide the widget size (it would complicate things, though), but
- what should happen if some rows/columns under the span do have a
- positive weight? */
-
-#include <stddef.h>
-#include <limits.h>
-
-#include "memory.h"
-#include "rect.h"
-#include "widget.h"
-#include "util.h"
-#include "grid.h"
-#include "graphics.h"
-
-void ltk_grid_set_row_weight(ltk_grid *grid, int row, int weight);
-void ltk_grid_set_column_weight(ltk_grid *grid, int column, int weight);
-ltk_grid *ltk_grid_create(ltk_window *window, int rows, int columns);
-int ltk_grid_add(
- ltk_grid *grid, ltk_widget *widget,
- int row, int column, int row_span, int column_span,
- ltk_sticky_mask sticky
-);
-/* just a wrapper around ltk_grid_remove to make types match */
-static int ltk_grid_remove_child(ltk_widget *self, ltk_widget *widget);
-int ltk_grid_remove(ltk_grid *grid, ltk_widget *widget);
-
-static void ltk_grid_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip);
-static void ltk_grid_destroy(ltk_widget *self, int shallow);
-static void ltk_recalculate_grid(ltk_widget *self);
-static void ltk_grid_child_size_change(ltk_widget *self, ltk_widget *widget);
-static int ltk_grid_find_nearest_column(ltk_grid *grid, int x);
-static int ltk_grid_find_nearest_row(ltk_grid *grid, int y);
-static ltk_widget *ltk_grid_get_child_at_pos(ltk_widget *self, int x, int y);
-
-static ltk_widget *ltk_grid_prev_child(ltk_widget *self, ltk_widget *child);
-static ltk_widget *ltk_grid_next_child(ltk_widget *self, ltk_widget *child);
-static ltk_widget *ltk_grid_first_child(ltk_widget *self);
-static ltk_widget *ltk_grid_last_child(ltk_widget *self);
-
-static ltk_widget *ltk_grid_nearest_child(ltk_widget *self, ltk_rect rect);
-static ltk_widget *ltk_grid_nearest_child_left(ltk_widget *self, ltk_widget *widget);
-static ltk_widget *ltk_grid_nearest_child_right(ltk_widget *self, ltk_widget *widget);
-static ltk_widget *ltk_grid_nearest_child_above(ltk_widget *self, ltk_widget *widget);
-static ltk_widget *ltk_grid_nearest_child_below(ltk_widget *self, ltk_widget *widget);
-
-static struct ltk_widget_vtable vtable = {
- .draw = <k_grid_draw,
- .destroy = <k_grid_destroy,
- .resize = <k_recalculate_grid,
- .hide = NULL,
- .change_state = NULL,
- .child_size_change = <k_grid_child_size_change,
- .remove_child = <k_grid_remove_child,
- .mouse_press = NULL,
- .mouse_scroll = NULL,
- .mouse_release = NULL,
- .motion_notify = NULL,
- .get_child_at_pos = <k_grid_get_child_at_pos,
- .mouse_leave = NULL,
- .mouse_enter = NULL,
- .key_press = NULL,
- .key_release = NULL,
- .prev_child = <k_grid_prev_child,
- .next_child = <k_grid_next_child,
- .first_child = <k_grid_first_child,
- .last_child = <k_grid_last_child,
- .nearest_child = <k_grid_nearest_child,
- .nearest_child_left = <k_grid_nearest_child_left,
- .nearest_child_right = <k_grid_nearest_child_right,
- .nearest_child_above = <k_grid_nearest_child_above,
- .nearest_child_below = <k_grid_nearest_child_below,
- .type = LTK_WIDGET_GRID,
- .flags = 0,
- .invalid_signal = LTK_GRID_SIGNAL_INVALID,
-};
-
-/* FIXME: only set "dirty" bit to avoid constand recalculation when
- setting multiple row/column weights? */
-void
-ltk_grid_set_row_weight(ltk_grid *grid, int row, int weight) {
- ltk_assert(row < grid->rows);
- grid->row_weights[row] = weight;
- ltk_recalculate_grid(LTK_CAST_WIDGET(grid));
-}
-
-void
-ltk_grid_set_column_weight(ltk_grid *grid, int column, int weight) {
- ltk_assert(column < grid->columns);
- grid->column_weights[column] = weight;
- ltk_recalculate_grid(LTK_CAST_WIDGET(grid));
-}
-
-static void
-ltk_grid_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) {
- ltk_grid *grid = LTK_CAST_GRID(self);
- int i;
- ltk_rect real_clip = ltk_rect_intersect((ltk_rect){0, 0, self->lrect.w, self->lrect.h}, clip);
- for (i = 0; i < grid->rows * grid->columns; i++) {
- if (!grid->widget_grid[i])
- continue;
- ltk_widget *ptr = grid->widget_grid[i];
- int max_w = grid->column_pos[ptr->column + ptr->column_span] - grid->column_pos[ptr->column];
- int max_h = grid->row_pos[ptr->row + ptr->row_span] - grid->row_pos[ptr->row];
- ltk_rect r = ltk_rect_intersect(
- (ltk_rect){grid->column_pos[ptr->column], grid->row_pos[ptr->row], max_w, max_h}, real_clip
- );
- if (ptr->vtable->draw)
- ptr->vtable->draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, r));
- }
-}
-
-ltk_grid *
-ltk_grid_create(ltk_window *window, int rows, int columns) {
- ltk_grid *grid = ltk_malloc(sizeof(ltk_grid));
-
- ltk_fill_widget_defaults(LTK_CAST_WIDGET(grid), window, &vtable, 0, 0);
-
- grid->rows = rows;
- grid->columns = columns;
- grid->widget_grid = ltk_malloc(rows * columns * sizeof(ltk_widget));
- grid->row_heights = ltk_malloc(rows * sizeof(int));
- grid->column_widths = ltk_malloc(rows * sizeof(int));
- grid->row_weights = ltk_malloc(rows * sizeof(int));
- grid->column_weights = ltk_malloc(columns * sizeof(int));
- /* Positions have one extra for the end */
- grid->row_pos = ltk_malloc((rows + 1) * sizeof(int));
- grid->column_pos = ltk_malloc((columns + 1) * sizeof(int));
- /* FIXME: wow, that's horrible, this should just use memset */
- int i;
- for (i = 0; i < rows; i++) {
- grid->row_heights[i] = 0;
- grid->row_weights[i] = 0;
- grid->row_pos[i] = 0;
- }
- grid->row_pos[rows] = 0;
- for (i = 0; i < columns; i++) {
- grid->column_widths[i] = 0;
- grid->column_weights[i] = 0;
- grid->column_pos[i] = 0;
- }
- grid->column_pos[columns] = 0;
- for (i = 0; i < rows * columns; i++) {
- grid->widget_grid[i] = NULL;
- }
-
- ltk_recalculate_grid(LTK_CAST_WIDGET(grid));
- return grid;
-}
-
-static void
-ltk_grid_destroy(ltk_widget *self, int shallow) {
- ltk_grid *grid = LTK_CAST_GRID(self);
- ltk_widget *ptr;
- 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++) {
- for (int c = ptr->column; c < ptr->column + ptr->column_span; c++) {
- grid->widget_grid[r * grid->columns + c] = NULL;
- }
- }
- ltk_widget_destroy(ptr, shallow);
- }
- }
- }
- ltk_free(grid->widget_grid);
- ltk_free(grid->row_heights);
- ltk_free(grid->column_widths);
- ltk_free(grid->row_weights);
- ltk_free(grid->column_weights);
- ltk_free(grid->row_pos);
- ltk_free(grid->column_pos);
- ltk_free(grid);
-}
-
-static void
-ltk_recalculate_grid(ltk_widget *self) {
- ltk_grid *grid = LTK_CAST_GRID(self);
- unsigned int height_static = 0, width_static = 0;
- unsigned int total_row_weight = 0, total_column_weight = 0;
- float height_unit = 0, width_unit = 0;
- unsigned int currentx = 0, currenty = 0;
- int i, j;
- for (i = 0; i < grid->rows; i++) {
- total_row_weight += grid->row_weights[i];
- if (grid->row_weights[i] == 0) {
- height_static += grid->row_heights[i];
- }
- }
- for (i = 0; i < grid->columns; i++) {
- total_column_weight += grid->column_weights[i];
- if (grid->column_weights[i] == 0) {
- width_static += grid->column_widths[i];
- }
- }
- /* FIXME: what should be done when static height or width is larger than grid? */
- if (total_row_weight > 0) {
- height_unit = (float) (self->lrect.h - height_static) / (float) total_row_weight;
- }
- if (total_column_weight > 0) {
- width_unit = (float) (self->lrect.w - width_static) / (float) total_column_weight;
- }
- for (i = 0; i < grid->rows; i++) {
- grid->row_pos[i] = currenty;
- if (grid->row_weights[i] > 0) {
- grid->row_heights[i] = grid->row_weights[i] * height_unit;
- }
- currenty += grid->row_heights[i];
- }
- grid->row_pos[grid->rows] = currenty;
- for (i = 0; i < grid->columns; i++) {
- grid->column_pos[i] = currentx;
- if (grid->column_weights[i] > 0) {
- grid->column_widths[i] = grid->column_weights[i] * width_unit;
- }
- currentx += grid->column_widths[i];
- }
- grid->column_pos[grid->columns] = currentx;
- /*int orig_width, orig_height;*/
- int end_column, end_row;
- for (i = 0; i < grid->rows; i++) {
- for (j = 0; j < grid->columns; j++) {
- ltk_widget *ptr = grid->widget_grid[i * grid->columns + j];
- if (!ptr || ptr->row != i || ptr->column != j)
- continue;
- /*orig_width = ptr->lrect.w;
- orig_height = ptr->lrect.h;*/
- ptr->lrect.w = ptr->ideal_w;
- ptr->lrect.h = ptr->ideal_h;
- end_row = i + ptr->row_span;
- end_column = j + ptr->column_span;
- int max_w = grid->column_pos[end_column] - grid->column_pos[j];
- int max_h = grid->row_pos[end_row] - grid->row_pos[i];
- int stretch_width = (ptr->sticky & LTK_STICKY_LEFT) && (ptr->sticky & LTK_STICKY_RIGHT);
- int shrink_width = (ptr->sticky & LTK_STICKY_SHRINK_WIDTH) && ptr->lrect.w > max_w;
- int stretch_height = (ptr->sticky & LTK_STICKY_TOP) && (ptr->sticky & LTK_STICKY_BOTTOM);
- int shrink_height = (ptr->sticky & LTK_STICKY_SHRINK_HEIGHT) && ptr->lrect.h > max_h;
- if (stretch_width || shrink_width)
- ptr->lrect.w = max_w;
- if (stretch_height || shrink_height)
- ptr->lrect.h = max_h;
- if (ptr->sticky & LTK_STICKY_PRESERVE_ASPECT_RATIO) {
- if (!stretch_width && !shrink_width) {
- ptr->lrect.w = (int)(((double)ptr->lrect.h / ptr->ideal_h) * ptr->ideal_w);
- } else if (!stretch_height && !shrink_height) {
- ptr->lrect.h = (int)(((double)ptr->lrect.w / ptr->ideal_w) * ptr->ideal_h);
- } else {
- double scale_w = (double)ptr->lrect.w / ptr->ideal_w;
- double scale_h = (double)ptr->lrect.h / ptr->ideal_h;
- if (scale_w * ptr->ideal_h > ptr->lrect.h)
- ptr->lrect.w = (int)(scale_h * ptr->ideal_w);
- else if (scale_h * ptr->ideal_w > ptr->lrect.w)
- ptr->lrect.h = (int)(scale_w * ptr->ideal_h);
- }
- }
- /* FIXME: Figure out a better system for this - it would be nice to make it more
- efficient by not doing anything if nothing changed, but that doesn't work when
- this function was called because of a child_size_change. In that case, if a
- container widget is nested inside another container widget and another widget
- inside the nested container sends a child_size_change but the toplevel container
- doesn't change the size of the container, the position/size of the widget at the
- bottom of the hierarchy will never be updated. That's why updates are forced
- here even if seemingly nothing changed, but there probably is a better way. */
- /*if (orig_width != ptr->lrect.w || orig_height != ptr->lrect.h)*/
- ltk_widget_resize(ptr);
-
- /* the "default" case needs to come first because the widget may be stretched
- with aspect ratio preserving, and in that case it should still be centered */
- if (stretch_width || !(ptr->sticky & (LTK_STICKY_RIGHT|LTK_STICKY_LEFT))) {
- ptr->lrect.x = grid->column_pos[j] + (grid->column_pos[end_column] - grid->column_pos[j] - ptr->lrect.w) / 2;
- } else if (ptr->sticky & LTK_STICKY_RIGHT) {
- ptr->lrect.x = grid->column_pos[end_column] - ptr->lrect.w;
- } else if (ptr->sticky & LTK_STICKY_LEFT) {
- ptr->lrect.x = grid->column_pos[j];
- }
-
- if (stretch_height || !(ptr->sticky & (LTK_STICKY_TOP|LTK_STICKY_BOTTOM))) {
- ptr->lrect.y = grid->row_pos[i] + (grid->row_pos[end_row] - grid->row_pos[i] - ptr->lrect.h) / 2;
- } else if (ptr->sticky & LTK_STICKY_BOTTOM) {
- ptr->lrect.y = grid->row_pos[end_row] - ptr->lrect.h;
- } else if (ptr->sticky & LTK_STICKY_TOP) {
- ptr->lrect.y = grid->row_pos[i];
- }
- /* intersect both with the grid rect and with the rect of the covered cells since there may be
- weird cases where the layout doesn't work properly and the cells are partially outside the grid */
- ptr->crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, ptr->lrect);
- ptr->crect = ltk_rect_intersect((ltk_rect){grid->column_pos[j], grid->row_pos[i], max_w, max_h}, ptr->crect);
- }
- }
-}
-
-/* FIXME: Maybe add debug stuff to check that grid is actually parent of widget */
-static void
-ltk_grid_child_size_change(ltk_widget *self, ltk_widget *widget) {
- ltk_grid *grid = LTK_CAST_GRID(self);
- short size_changed = 0;
- int orig_w = widget->lrect.w;
- int orig_h = widget->lrect.h;
- widget->lrect.w = widget->ideal_w;
- widget->lrect.h = widget->ideal_h;
- if (grid->column_weights[widget->column] == 0 &&
- widget->lrect.w > grid->column_widths[widget->column]) {
- self->ideal_w += widget->lrect.w - grid->column_widths[widget->column];
- grid->column_widths[widget->column] = widget->lrect.w;
- size_changed = 1;
- }
- if (grid->row_weights[widget->row] == 0 &&
- widget->lrect.h > grid->row_heights[widget->row]) {
- self->ideal_h += widget->lrect.h - grid->row_heights[widget->row];
- grid->row_heights[widget->row] = widget->lrect.h;
- size_changed = 1;
- }
- if (size_changed && self->parent && self->parent->vtable->child_size_change)
- self->parent->vtable->child_size_change(self->parent, LTK_CAST_WIDGET(grid));
- else
- ltk_recalculate_grid(LTK_CAST_WIDGET(grid));
- if (widget->lrect.w != orig_w || widget->lrect.h != orig_h)
- ltk_widget_resize(widget);
-}
-
-/* FIXME: Check if widget already exists at position */
-int
-ltk_grid_add(
- ltk_grid *grid, ltk_widget *widget,
- int row, int column, int row_span, int column_span,
- ltk_sticky_mask sticky
-) {
- if (widget->parent)
- return 1;
- /* FIXME: decide which checks should be asserts and which should be error returns */
- /* the client-server version of ltk shouldn't abort on errors like these */
- ltk_assert(row >= 0 && row + row_span <= grid->rows);
- ltk_assert(column >= 0 && column + column_span <= grid->columns);
-
- widget->sticky = sticky;
- widget->row = row;
- widget->column = column;
- widget->row_span = row_span;
- widget->column_span = column_span;
- for (int i = row; i < row + row_span; i++) {
- for (int j = column; j < column + column_span; j++) {
- grid->widget_grid[i * grid->columns + j] = widget;
- }
- }
- widget->parent = LTK_CAST_WIDGET(grid);
- ltk_grid_child_size_change(LTK_CAST_WIDGET(grid), widget);
- ltk_window_invalidate_widget_rect(LTK_CAST_WIDGET(grid)->window, LTK_CAST_WIDGET(grid));
-
- return 0;
-}
-
-int
-ltk_grid_remove(ltk_grid *grid, ltk_widget *widget) {
- if (widget->parent != LTK_CAST_WIDGET(grid))
- return 1;
- widget->parent = NULL;
- for (int i = widget->row; i < widget->row + widget->row_span; i++) {
- for (int j = widget->column; j < widget->column + widget->column_span; j++) {
- grid->widget_grid[i * grid->columns + j] = NULL;
- }
- }
- ltk_window_invalidate_widget_rect(LTK_CAST_WIDGET(grid)->window, LTK_CAST_WIDGET(grid));
-
- return 0;
-}
-
-static int
-ltk_grid_remove_child(ltk_widget *self, ltk_widget *widget) {
- return ltk_grid_remove(LTK_CAST_GRID(self), widget);
-}
-
-static int
-ltk_grid_find_nearest_column(ltk_grid *grid, int x) {
- int i;
- for (i = 0; i < grid->columns; i++) {
- if (grid->column_pos[i] <= x && grid->column_pos[i + 1] >= x) {
- return i;
- }
- }
- return -1;
-}
-
-static int
-ltk_grid_find_nearest_row(ltk_grid *grid, int y) {
- int i;
- for (i = 0; i < grid->rows; i++) {
- if (grid->row_pos[i] <= y && grid->row_pos[i + 1] >= y) {
- return i;
- }
- }
- return -1;
-}
-
-/* FIXME: maybe come up with a more efficient method */
-static ltk_widget *
-ltk_grid_nearest_child(ltk_widget *self, ltk_rect rect) {
- ltk_grid *grid = LTK_CAST_GRID(self);
- ltk_widget *minw = NULL;
- int min_dist = INT_MAX;
- /* FIXME: rows and columns shouldn't be int */
- for (size_t i = 0; i < (size_t)(grid->rows * grid->columns); i++) {
- if (!grid->widget_grid[i])
- continue;
- /* FIXME: this checks widgets with row/columnspan > 1 multiple times */
- ltk_rect r = grid->widget_grid[i]->lrect;
- int dist = ltk_rect_fakedist(rect, r);
- if (dist < min_dist) {
- min_dist = dist;
- minw = grid->widget_grid[i];
- }
- }
- return minw;
-}
-
-/* FIXME: assertions to check that widget row/column are legal */
-static ltk_widget *
-ltk_grid_nearest_child_left(ltk_widget *self, ltk_widget *widget) {
- ltk_grid *grid = LTK_CAST_GRID(self);
- unsigned int col = widget->column;
- ltk_widget *cur = NULL;
- while (col-- > 0) {
- cur = grid->widget_grid[widget->row * grid->columns + col];
- if (cur && cur != widget)
- return cur;
- }
- return NULL;
-}
-
-static ltk_widget *
-ltk_grid_nearest_child_right(ltk_widget *self, ltk_widget *widget) {
- ltk_grid *grid = LTK_CAST_GRID(self);
- ltk_widget *cur = NULL;
- for (int col = widget->column + 1; col < grid->columns; col++) {
- cur = grid->widget_grid[widget->row * grid->columns + col];
- if (cur && cur != widget)
- return cur;
- }
- return NULL;
-}
-
-/* FIXME: maybe these should also fall back to widgets in other columns if those
- exist but no widgets exist in the same column */
-static ltk_widget *
-ltk_grid_nearest_child_above(ltk_widget *self, ltk_widget *widget) {
- ltk_grid *grid = LTK_CAST_GRID(self);
- unsigned int row = widget->row;
- ltk_widget *cur = NULL;
- while (row-- > 0) {
- cur = grid->widget_grid[row * grid->columns + widget->column];
- if (cur && cur != widget)
- return cur;
- }
- return NULL;
-}
-
-static ltk_widget *
-ltk_grid_nearest_child_below(ltk_widget *self, ltk_widget *widget) {
- ltk_grid *grid = LTK_CAST_GRID(self);
- ltk_widget *cur = NULL;
- for (int row = widget->row + 1; row < grid->rows; row++) {
- cur = grid->widget_grid[row * grid->columns + widget->column];
- if (cur && cur != widget)
- return cur;
- }
- return NULL;
-}
-
-static ltk_widget *
-ltk_grid_get_child_at_pos(ltk_widget *self, int x, int y) {
- ltk_grid *grid = LTK_CAST_GRID(self);
- int row = ltk_grid_find_nearest_row(grid, y);
- int column = ltk_grid_find_nearest_column(grid, x);
- if (row == -1 || column == -1)
- return 0;
- ltk_widget *ptr = grid->widget_grid[row * grid->columns + column];
- if (ptr && ltk_collide_rect(ptr->crect, x, y))
- return ptr;
- return NULL;
-}
-
-static ltk_widget *
-ltk_grid_prev_child(ltk_widget *self, ltk_widget *child) {
- ltk_grid *grid = LTK_CAST_GRID(self);
- unsigned int start = child->row * grid->columns + child->column;
- while (start-- > 0) {
- if (grid->widget_grid[start])
- return grid->widget_grid[start];
- }
- return NULL;
-}
-
-static ltk_widget *
-ltk_grid_next_child(ltk_widget *self, ltk_widget *child) {
- ltk_grid *grid = LTK_CAST_GRID(self);
- unsigned int start = child->row * grid->columns + child->column;
- while (++start < (unsigned int)(grid->rows * grid->columns)) {
- if (grid->widget_grid[start] && grid->widget_grid[start] != child)
- return grid->widget_grid[start];
- }
- return NULL;
-}
-
-static ltk_widget *
-ltk_grid_first_child(ltk_widget *self) {
- ltk_grid *grid = LTK_CAST_GRID(self);
- for (unsigned int i = 0; i < (unsigned int)(grid->rows * grid->columns); i++) {
- if (grid->widget_grid[i])
- return grid->widget_grid[i];
- }
- return NULL;
-}
-
-static ltk_widget *
-ltk_grid_last_child(ltk_widget *self) {
- ltk_grid *grid = LTK_CAST_GRID(self);
- for (unsigned int i = grid->rows * grid->columns; i-- > 0;) {
- if (grid->widget_grid[i])
- return grid->widget_grid[i];
- }
- return NULL;
-}
diff --git a/src/ltk.c b/src/ltk.c
@@ -1,672 +0,0 @@
-/*
- * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <locale.h>
-#include <pwd.h>
-#include <stdint.h>
-#include <stdlib.h>
-#include <string.h>
-#include <time.h>
-#include <unistd.h>
-
-#include <sys/wait.h>
-
-#include "ltk.h"
-#include "array.h"
-#include "button.h"
-#include "config.h"
-#include "entry.h"
-#include "event.h"
-#include "eventdefs.h"
-#include "graphics.h"
-#include "image.h"
-#include "ini.h"
-#include "label.h"
-#include "macros.h"
-#include "memory.h"
-#include "menu.h"
-#include "rect.h"
-#include "scrollbar.h"
-#include "text.h"
-#include "util.h"
-#include "widget.h"
-
-#define MAX_WINDOW_FONT_SIZE 200
-
-typedef struct {
- char *tmpfile;
- ltk_widget *caller;
- int pid;
-} ltk_cmdinfo;
-
-LTK_ARRAY_INIT_DECL_STATIC(window, ltk_window *)
-LTK_ARRAY_INIT_IMPL_STATIC(window, ltk_window *)
-LTK_ARRAY_INIT_DECL_STATIC(rwindow, ltk_renderwindow *)
-LTK_ARRAY_INIT_IMPL_STATIC(rwindow, ltk_renderwindow *)
-LTK_ARRAY_INIT_DECL_STATIC(cmd, ltk_cmdinfo)
-LTK_ARRAY_INIT_IMPL_STATIC(cmd, ltk_cmdinfo)
-
-static struct {
- ltk_renderdata *renderdata;
- ltk_text_context *text_context;
- ltk_clipboard *clipboard;
- ltk_array(window) *windows;
- ltk_array(rwindow) *rwindows;
- /* PID of external command called e.g. by text widget to edit text.
- ON exit, cmd_caller->vtable->cmd_return is called with the text
- the external command wrote to a file. */
- /*IMPORTANT: this needs to be checked whenever a widget is destroyed!
- FIXME: allow option to instead return output of command */
- ltk_array(cmd) *cmds;
- size_t cur_kbd;
-} shared_data = {NULL, NULL, NULL, NULL, NULL, NULL, 0};
-
-typedef struct {
- void (*callback)(ltk_callback_arg data);
- ltk_callback_arg 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 void ltk_handle_event(ltk_event *event);
-static void ltk_load_theme(const char *path);
-static void ltk_uninitialize_theme(void);
-static int ltk_ini_handler(void *renderdata, const char *widget, const char *prop, const char *value);
-static int handle_keypress_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keypress_binding b);
-static int handle_keyrelease_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keyrelease_binding b);
-
-static short running = 1;
-
-typedef struct {
- char *name;
- int (*ini_handler)(ltk_renderdata *, const char *, const char *);
- int (*fill_theme_defaults)(ltk_renderdata *);
- void (*uninitialize_theme)(ltk_renderdata *);
- int (*register_keypress)(const char *, size_t, ltk_keypress_binding);
- int (*register_keyrelease)(const char *, size_t, ltk_keyrelease_binding);
- void (*cleanup)(void);
-} ltk_widget_funcs;
-
-/* FIXME: use binary search when searching for the widget */
-ltk_widget_funcs widget_funcs[] = {
- {
- .name = "box",
- .ini_handler = NULL,
- .fill_theme_defaults = NULL,
- .uninitialize_theme = NULL,
- .register_keypress = NULL,
- .register_keyrelease = NULL,
- .cleanup = NULL,
- },
- {
- .name = "button",
- .ini_handler = <k_button_ini_handler,
- .fill_theme_defaults = <k_button_fill_theme_defaults,
- .uninitialize_theme = <k_button_uninitialize_theme,
- .register_keypress = NULL,
- .register_keyrelease = NULL,
- .cleanup = NULL,
- },
- {
- .name = "entry",
- .ini_handler = <k_entry_ini_handler,
- .fill_theme_defaults = <k_entry_fill_theme_defaults,
- .uninitialize_theme = <k_entry_uninitialize_theme,
- .register_keypress = <k_entry_register_keypress,
- .register_keyrelease = <k_entry_register_keyrelease,
- .cleanup = <k_entry_cleanup,
- },
- {
- .name = "grid",
- .ini_handler = NULL,
- .fill_theme_defaults = NULL,
- .uninitialize_theme = NULL,
- .register_keypress = NULL,
- .register_keyrelease = NULL,
- .cleanup = NULL,
- },
- {
- .name = "label",
- .ini_handler = <k_label_ini_handler,
- .fill_theme_defaults = <k_label_fill_theme_defaults,
- .uninitialize_theme = <k_label_uninitialize_theme,
- .register_keypress = NULL,
- .register_keyrelease = NULL,
- .cleanup = NULL,
- },
- {
- /* FIXME: this is actually image_widget */
- .name = "image",
- .ini_handler = NULL,
- .fill_theme_defaults = NULL,
- .uninitialize_theme = NULL,
- .register_keypress = NULL,
- .register_keyrelease = NULL,
- .cleanup = NULL,
- },
- {
- .name = "menu",
- .ini_handler = <k_menu_ini_handler,
- .fill_theme_defaults = <k_menu_fill_theme_defaults,
- .uninitialize_theme = <k_menu_uninitialize_theme,
- .register_keypress = NULL,
- .register_keyrelease = NULL,
- .cleanup = NULL,
- },
- {
- .name = "menuentry",
- .ini_handler = <k_menuentry_ini_handler,
- .fill_theme_defaults = <k_menuentry_fill_theme_defaults,
- .uninitialize_theme = <k_menuentry_uninitialize_theme,
- .register_keypress = NULL,
- .register_keyrelease = NULL,
- .cleanup = NULL,
- },
- {
- .name = "submenu",
- .ini_handler = <k_submenu_ini_handler,
- .fill_theme_defaults = <k_submenu_fill_theme_defaults,
- .uninitialize_theme = <k_submenu_uninitialize_theme,
- .register_keypress = NULL,
- .register_keyrelease = NULL,
- .cleanup = NULL,
- },
- {
- .name = "submenuentry",
- .ini_handler = <k_submenuentry_ini_handler,
- .fill_theme_defaults = <k_submenuentry_fill_theme_defaults,
- .uninitialize_theme = <k_submenuentry_uninitialize_theme,
- .register_keypress = NULL,
- .register_keyrelease = NULL,
- .cleanup = NULL,
- /*
- This "widget" is only needed to have separate styles for regular
- menu entries and submenu entries. "submenu" is just an alias for
- "menu" in most cases - it's just needed when creating a menu to
- decide if it's a submenu or not.
- FIXME: is that even necessary? Why can't it just decide if it's
- a submenu based on whether it has a parent or not?
- -> I guess right-click menus are also just submenus, so they
- need to set it explicitly, but wasn't there another reason?
- */
- },
- {
- .name = "scrollbar",
- .ini_handler = <k_scrollbar_ini_handler,
- .fill_theme_defaults = <k_scrollbar_fill_theme_defaults,
- .uninitialize_theme = <k_scrollbar_uninitialize_theme,
- .register_keypress = NULL,
- .register_keyrelease = NULL,
- .cleanup = NULL,
- },
- {
- /* Handler for general widget key bindings. */
- .name = "widget",
- .ini_handler = NULL,
- .fill_theme_defaults = NULL,
- .uninitialize_theme = NULL,
- .register_keypress = NULL,
- .register_keyrelease = NULL,
- .cleanup = NULL,
- },
- {
- /* Handler for window theme. */
- .name = "window",
- .ini_handler = <k_window_ini_handler,
- .fill_theme_defaults = <k_window_fill_theme_defaults,
- .uninitialize_theme = <k_window_uninitialize_theme,
- .register_keypress = <k_window_register_keypress,
- .register_keyrelease = <k_window_register_keyrelease,
- .cleanup = <k_window_cleanup,
- }
-};
-
-/* Get the directory to search for ltk.cfg in.
- This first checks the environment variable LTKDIR and,
- if that doesn't exist, the home directory with "/.ltk" appended.
- Returns NULL on error. */
-static char *
-ltk_get_dir(void) {
- char *dir, *dir_orig;
- struct passwd *pw;
- uid_t uid;
- int len;
-
- dir_orig = getenv("LTKDIR");
- if (dir_orig) {
- dir = ltk_strdup(dir_orig);
- } else {
- uid = getuid();
- pw = getpwuid(uid);
- if (!pw)
- return NULL;
- len = strlen(pw->pw_dir);
- dir = ltk_malloc(len + 6);
- if (!dir)
- return NULL;
- strcpy(dir, pw->pw_dir);
- strcpy(dir + len, "/.ltk");
- }
-
- return dir;
-}
-
-int
-ltk_init(void) {
- /* FIXME: should ltk set this? probably not */
- setlocale(LC_CTYPE, "");
- char *ltk_dir = ltk_get_dir();
- /* FIXME: return error instead of dying */
- if (!ltk_dir)
- ltk_fatal_errno("Unable to setup ltk directory.\n");
- shared_data.cur_kbd = 0;
-
- /* FIXME: search different directories for config */
- char *config_path = ltk_strcat_useful(ltk_dir, "/ltk.cfg");
- char *theme_path;
- char *errstr = NULL;
- if (ltk_config_parsefile(config_path, &handle_keypress_binding, &handle_keyrelease_binding, &errstr)) {
- if (errstr) {
- ltk_warn("Unable to load config: %s\n", errstr);
- ltk_free0(errstr);
- }
- if (ltk_config_load_default(&handle_keypress_binding, &handle_keyrelease_binding, &errstr)) {
- /* FIXME: I guess errstr isn't freed here, but whatever */
- /* FIXME: return error instead of dying */
- ltk_fatal("Unable to load default config: %s\n", errstr);
- }
- }
- ltk_free0(config_path);
- theme_path = ltk_strcat_useful(ltk_dir, "/theme.ini");
- ltk_free0(ltk_dir);
- shared_data.renderdata = renderer_create();
- if (!shared_data.renderdata)
- return 1; /* FIXME: clean up */
- ltk_load_theme(theme_path);
- ltk_free0(theme_path);
- /* FIXME: maybe "general" theme instead of window theme? */
- ltk_window_theme *window_theme = ltk_window_get_theme();
- shared_data.text_context = ltk_text_context_create(shared_data.renderdata, window_theme->font);
- shared_data.clipboard = ltk_clipboard_create(shared_data.renderdata);
- /* FIXME: configure cache size; check for overflow */
- ltk_image_init(shared_data.renderdata, 1024 * 1024 * 4);
- shared_data.windows = ltk_array_create(window, 1);
- shared_data.rwindows = ltk_array_create(rwindow, 1);
- shared_data.cmds = ltk_array_create(cmd, 1);
- return 0; /* FIXME: or maybe 1? */
-}
-
-/* FIXME: need to remove event masks from all widgets when removing client */
-int
-ltk_mainloop(void) {
- ltk_event event;
-
- /* 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, lasttimer, sleep_time;
- clock_gettime(CLOCK_MONOTONIC, &last);
- lasttimer = last;
- sleep_time.tv_sec = 0;
-
- /* initialize keyboard mapping */
- ltk_generate_keyboard_event(shared_data.renderdata, &event);
- ltk_handle_event(&event);
-
- int pid = -1;
- int wstatus = 0;
- /* FIXME: kill all children on exit? */
- while (running) {
- if ((pid = waitpid(-1, &wstatus, WNOHANG)) > 0) {
- //ltk_error err;
- ltk_cmdinfo *info;
- /* FIXME: should commands be split into read/write and block write commands during external editing? */
- for (size_t i = 0; i < ltk_array_len(shared_data.cmds); i++) {
- info = &(ltk_array_get(shared_data.cmds, i));
- if (info->pid == pid) {
- if (!info->caller) {
- ltk_warn("Widget disappeared while text was being edited in external program\n");
- /* FIXME: call overwritten cmd_return! */
- } else if (info->caller->vtable->cmd_return) {
- size_t file_len = 0;
- char *errstr = NULL;
- char *contents = ltk_read_file(info->tmpfile, &file_len, &errstr);
- if (!contents) {
- ltk_warn("Unable to read file '%s' written by external command: %s\n", info->tmpfile, errstr);
- } else {
- info->caller->vtable->cmd_return(info->caller, contents, file_len);
- ltk_free0(contents);
- }
- }
- ltk_free0(info->tmpfile);
- ltk_array_delete(cmd, shared_data.cmds, i, 1);
- break;
- }
- }
- }
- while (!ltk_next_event(
- shared_data.renderdata,
- ltk_array_get_buf(shared_data.rwindows),
- ltk_array_len(shared_data.rwindows),
- shared_data.clipboard, shared_data.cur_kbd, &event)) {
- ltk_handle_event(&event);
- }
-
- clock_gettime(CLOCK_MONOTONIC, &now);
- ltk_timespecsub(&now, &lasttimer, &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 timer 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++;
- }
- }
- lasttimer = now;
-
- for (size_t i = 0; i < shared_data.windows->len; i++) {
- ltk_window *window = shared_data.windows->buf[i];
- if (window->dirty_rect.w != 0 && window->dirty_rect.h != 0) {
- window->widget.vtable->draw(LTK_CAST_WIDGET(window), NULL, 0, 0, (ltk_rect){0, 0, 0, 0});
- }
- }
-
- clock_gettime(CLOCK_MONOTONIC, &now);
- ltk_timespecsub(&now, &last, &elapsed);
- /* FIXME: configure framerate */
- if (elapsed.tv_sec == 0 && elapsed.tv_nsec < 20000000LL) {
- sleep_time.tv_nsec = 20000000LL - elapsed.tv_nsec;
- nanosleep(&sleep_time, NULL);
- }
- last = now;
- }
-
- ltk_deinit();
-
- return 0;
-}
-
-void
-ltk_deinit(void) {
- if (running)
- return;
- if (shared_data.cmds) {
- for (size_t i = 0; i < ltk_array_len(shared_data.cmds); i++) {
- /* FIXME: maybe kill child processes? */
- ltk_free((ltk_array_get(shared_data.cmds, i)).tmpfile);
- }
- ltk_array_destroy(cmd, shared_data.cmds);
- }
- shared_data.cmds = NULL;
- /* FIXME: also check for overwritten methods everywhere! */
- if (shared_data.windows) {
- for (size_t i = 0; i < ltk_array_len(shared_data.windows); i++) {
- ltk_window *window = ltk_array_get(shared_data.windows, i);
- ltk_widget_destroy(LTK_CAST_WIDGET(window), 0);
- }
- ltk_array_destroy(window, shared_data.windows);
- }
- shared_data.windows = NULL;
- if (shared_data.rwindows)
- ltk_array_destroy(rwindow, shared_data.rwindows);
- shared_data.rwindows = NULL;
- ltk_config_cleanup();
- for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
- if (widget_funcs[i].cleanup)
- widget_funcs[i].cleanup();
- }
- if (shared_data.text_context)
- ltk_text_context_destroy(shared_data.text_context);
- shared_data.text_context = NULL;
- if (shared_data.clipboard)
- ltk_clipboard_destroy(shared_data.clipboard);
- shared_data.clipboard = NULL;
- ltk_events_cleanup();
- if (shared_data.renderdata) {
- ltk_uninitialize_theme();
- renderer_destroy(shared_data.renderdata);
- }
- shared_data.renderdata = NULL;
-}
-
-void
-ltk_quit(void) {
- /* FIXME: maybe prevent other events from running? */
- running = 0;
-}
-
-/* FIXME: check everywhere if initialized already */
-ltk_window *
-ltk_window_create(const char *title, int x, int y, unsigned int w, unsigned int h) {
- /* FIXME: more asserts, or maybe global "initialized" flag */
- ltk_assert(shared_data.renderdata != NULL);
- ltk_assert(shared_data.windows != NULL);
- ltk_assert(shared_data.rwindows != NULL);
- ltk_window *window = ltk_window_create_intern(shared_data.renderdata, title, x, y, w, h);
- ltk_array_append(window, shared_data.windows, window);
- ltk_array_append(rwindow, shared_data.rwindows, window->renderwindow);
- return window;
-}
-
-void
-ltk_window_destroy(ltk_widget *self, int shallow) {
- /* FIXME: would it make sense to do something with 'shallow' here? */
- (void)shallow;
- ltk_window *window = LTK_CAST_WINDOW(self);
- for (size_t i = 0; i < ltk_array_len(shared_data.windows); i++) {
- if (ltk_array_get(shared_data.windows, i) == window) {
- ltk_array_delete(window, shared_data.windows, i, 1);
- ltk_array_delete(rwindow, shared_data.rwindows, i, 1);
- break;
- }
- }
- ltk_window_destroy_intern(window);
-}
-
-ltk_clipboard *
-ltk_get_clipboard(void) {
- /* FIXME: what to do when not initialized? */
- return shared_data.clipboard;
-}
-
-/* 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)(ltk_callback_arg data), ltk_callback_arg 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: standardize return codes - usually, 0 is returned on success, but ini.h
- uses 1 on success, so this is all a bit confusing */
-/* FIXME: switch away from ini.h */
-static int
-ltk_ini_handler(void *renderdata, const char *widget, const char *prop, const char *value) {
- for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
- if (widget_funcs[i].ini_handler && !strcmp(widget, widget_funcs[i].name)) {
- widget_funcs[i].ini_handler(renderdata, prop, value);
- return 1;
- }
- }
- return 0;
-}
-
-/* FIXME: don't call ltk_fatal, instead return error from ltk_init */
-static void
-ltk_load_theme(const char *path) {
- /* FIXME: give line number in error message */
- if (ini_parse(path, ltk_ini_handler, shared_data.renderdata) != 0) {
- ltk_warn("Unable to load theme.\n");
- }
- for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
- if (widget_funcs[i].fill_theme_defaults) {
- if (widget_funcs[i].fill_theme_defaults(shared_data.renderdata)) {
- ltk_uninitialize_theme();
- ltk_fatal("Unable to load theme defaults.\n");
- }
- }
- }
-}
-
-static void
-ltk_uninitialize_theme(void) {
- for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
- if (widget_funcs[i].uninitialize_theme)
- widget_funcs[i].uninitialize_theme(shared_data.renderdata);
- }
-}
-
-static int
-handle_keypress_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keypress_binding b) {
- for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
- if (str_array_equal(widget_funcs[i].name, widget_name, wlen)) {
- if (!widget_funcs[i].register_keypress)
- return 1;
- return widget_funcs[i].register_keypress(name, nlen, b);
- }
- }
- return 1;
-}
-
-static int
-handle_keyrelease_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keyrelease_binding b) {
- for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
- if (str_array_equal(widget_funcs[i].name, widget_name, wlen)) {
- if (!widget_funcs[i].register_keyrelease)
- return 1;
- return widget_funcs[i].register_keyrelease(name, nlen, b);
- }
- }
- return 1;
-}
-
-int
-ltk_call_cmd(ltk_widget *caller, const char *cmd, size_t cmdlen, const char *text, size_t textlen) {
- /* FIXME: support environment variable $TMPDIR */
- ltk_cmdinfo info = {NULL, NULL, -1};
- info.tmpfile = ltk_strdup("/tmp/ltk.XXXXXX");
- int fd = mkstemp(info.tmpfile);
- if (fd == -1) {
- ltk_warn_errno("Unable to create temporary file while trying to run command '%.*s'\n", (int)cmdlen, cmd);
- ltk_free0(info.tmpfile);
- return 1;
- }
- close(fd);
- /* FIXME: give file descriptor directly to modified version of ltk_write_file */
- char *errstr = NULL;
- if (ltk_write_file(info.tmpfile, text, textlen, &errstr)) {
- ltk_warn("Unable to write to file '%s' while trying to run command '%.*s': %s\n", info.tmpfile, (int)cmdlen, cmd, errstr);
- unlink(info.tmpfile);
- ltk_free0(info.tmpfile);
- return 1;
- }
- int pid = -1;
- if ((pid = ltk_parse_run_cmd(cmd, cmdlen, info.tmpfile)) <= 0) {
- /* FIXME: errno */
- ltk_warn("Unable to run command '%.*s'\n", (int)cmdlen, cmd);
- unlink(info.tmpfile);
- ltk_free0(info.tmpfile);
- return 1;
- }
- info.pid = pid;
- info.caller = caller;
- ltk_array_append(cmd, shared_data.cmds, info);
- return 0;
-}
-
-static void
-ltk_handle_event(ltk_event *event) {
- size_t kbd_idx;
- if (event->type == LTK_KEYBOARDCHANGE_EVENT) {
- /* FIXME: emit event */
- if (ltk_config_get_language_index(event->keyboard.new_kbd, &kbd_idx))
- ltk_warn("No language mapping for language \"%s\".\n", event->keyboard.new_kbd);
- else
- shared_data.cur_kbd = kbd_idx;
- } else {
- if (event->any.window_id < ltk_array_len(shared_data.windows)) {
- ltk_window_handle_event(ltk_array_get(shared_data.windows, event->any.window_id), event);
- }
- }
-}
-
-ltk_text_line *
-ltk_text_line_create_default(uint16_t font_size, char *text, int take_over_text, int width) {
- return ltk_text_line_create(shared_data.text_context, font_size, text, take_over_text, width);
-}
diff --git a/src/ltk.h b/src/ltk.h
@@ -1,51 +0,0 @@
-/*
- * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef LTK_H
-#define LTK_H
-
-#include <stddef.h>
-#include <stdint.h>
-
-#include "clipboard.h"
-#include "widget.h"
-#include "window.h"
-#include "text.h"
-
-int ltk_init(void);
-void ltk_deinit(void);
-void ltk_quit(void);
-int ltk_mainloop(void);
-
-void ltk_unregister_timer(int timer_id);
-int ltk_register_timer(long first, long repeat, void (*callback)(ltk_callback_arg data), ltk_callback_arg data);
-
-/* These are here so they can be added to the global array in ltk.c */
-ltk_window *ltk_window_create(const char *title, int x, int y, unsigned int w, unsigned int h);
-void ltk_window_destroy(ltk_widget *self, int shallow);
-
-/* FIXME: allow piping text instead of writing to temporary file */
-/* FIXME: how to avoid bad things happening while external program open? maybe store cmd widget somewhere (but could be multiple!) and check if widget to destroy is one of those
--> alternative: store all widgets in array and only give out IDs, then when returning from cmd, widget is already destroyed and can be ignored
--> first option maybe just set callback, etc. of current cmd to NULL so widget can still be destroyed */
-int ltk_call_cmd(ltk_widget *caller, const char *cmd, size_t cmdlen, const char *text, size_t textlen);
-
-/* convenience function to use the default text context */
-ltk_text_line *ltk_text_line_create_default(uint16_t font_size, char *text, int take_over_text, int width);
-
-ltk_clipboard *ltk_get_clipboard(void);
-
-#endif /* LTK_H */
diff --git a/src/.gitignore b/src/ltk/.gitignore
diff --git a/src/array.h b/src/ltk/array.h
diff --git a/src/ltk/box.c b/src/ltk/box.c
@@ -0,0 +1,470 @@
+/*
+ * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/* FIXME: implement other sticky options now supported by grid */
+
+#include <limits.h>
+#include <string.h>
+
+#include "box.h"
+#include "event.h"
+#include "graphics.h"
+#include "memory.h"
+#include "rect.h"
+#include "scrollbar.h"
+#include "widget.h"
+
+static void ltk_box_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip);
+static void ltk_box_destroy(ltk_widget *self, int shallow);
+static void ltk_recalculate_box(ltk_widget *self);
+static void ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget);
+static int ltk_box_remove_child(ltk_widget *self, ltk_widget *widget);
+/* static int ltk_box_clear(ltk_window *window, ltk_box *box, int shallow); */
+static int ltk_box_scroll_cb(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data);
+static int ltk_box_mouse_scroll(ltk_widget *self, ltk_scroll_event *event);
+static ltk_widget *ltk_box_get_child_at_pos(ltk_widget *self, int x, int y);
+static void ltk_box_ensure_rect_shown(ltk_widget *self, ltk_rect r);
+
+static ltk_widget *ltk_box_prev_child(ltk_widget *self, ltk_widget *child);
+static ltk_widget *ltk_box_next_child(ltk_widget *self, ltk_widget *child);
+static ltk_widget *ltk_box_first_child(ltk_widget *self);
+static ltk_widget *ltk_box_last_child(ltk_widget *self);
+
+static ltk_widget *ltk_box_nearest_child(ltk_widget *self, ltk_rect rect);
+static ltk_widget *ltk_box_nearest_child_left(ltk_widget *self, ltk_widget *widget);
+static ltk_widget *ltk_box_nearest_child_right(ltk_widget *self, ltk_widget *widget);
+static ltk_widget *ltk_box_nearest_child_above(ltk_widget *self, ltk_widget *widget);
+static ltk_widget *ltk_box_nearest_child_below(ltk_widget *self, ltk_widget *widget);
+
+static struct ltk_widget_vtable vtable = {
+ .change_state = NULL,
+ .hide = NULL,
+ .draw = <k_box_draw,
+ .destroy = <k_box_destroy,
+ .resize = <k_recalculate_box,
+ .child_size_change = <k_box_child_size_change,
+ .remove_child = <k_box_remove_child,
+ .key_press = NULL,
+ .key_release = NULL,
+ .mouse_press = NULL,
+ .mouse_scroll = <k_box_mouse_scroll,
+ .mouse_release = NULL,
+ .motion_notify = NULL,
+ .get_child_at_pos = <k_box_get_child_at_pos,
+ .mouse_leave = NULL,
+ .mouse_enter = NULL,
+ .prev_child = <k_box_prev_child,
+ .next_child = <k_box_next_child,
+ .first_child = <k_box_first_child,
+ .last_child = <k_box_last_child,
+ .nearest_child = <k_box_nearest_child,
+ .nearest_child_left = <k_box_nearest_child_left,
+ .nearest_child_right = <k_box_nearest_child_right,
+ .nearest_child_above = <k_box_nearest_child_above,
+ .nearest_child_below = <k_box_nearest_child_below,
+ .ensure_rect_shown = <k_box_ensure_rect_shown,
+ .type = LTK_WIDGET_BOX,
+ .flags = 0,
+ .invalid_signal = LTK_BOX_SIGNAL_INVALID,
+};
+
+static void
+ltk_box_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) {
+ ltk_box *box = LTK_CAST_BOX(self);
+ ltk_widget *ptr;
+ /* FIXME: clip out scrollbar */
+ ltk_rect real_clip = ltk_rect_intersect((ltk_rect){0, 0, self->lrect.w, self->lrect.h}, clip);
+ for (size_t i = 0; i < box->num_widgets; i++) {
+ ptr = box->widgets[i];
+ /* FIXME: Maybe continue immediately if widget is
+ obviously outside of clipping rect */
+ ltk_widget_draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, real_clip));
+ }
+ ltk_widget_draw(
+ LTK_CAST_WIDGET(box->sc), s,
+ x + box->sc->widget.lrect.x,
+ y + box->sc->widget.lrect.y,
+ ltk_rect_relative(box->sc->widget.lrect, real_clip)
+ );
+}
+
+ltk_box *
+ltk_box_create(ltk_window *window, ltk_orientation orient) {
+ ltk_box *box = ltk_malloc(sizeof(ltk_box));
+ ltk_widget *self = LTK_CAST_WIDGET(box);
+
+ ltk_fill_widget_defaults(self, window, &vtable, 0, 0);
+
+ box->sc = ltk_scrollbar_create(window, orient);
+ box->sc->widget.parent = self;
+ ltk_widget_register_signal_handler(
+ LTK_CAST_WIDGET(box->sc), LTK_SCROLLBAR_SIGNAL_SCROLL,
+ <k_box_scroll_cb, LTK_MAKE_ARG_WIDGET(self)
+ );
+ box->widgets = NULL;
+ box->num_alloc = 0;
+ box->num_widgets = 0;
+ box->orient = orient;
+ if (orient == LTK_HORIZONTAL)
+ box->widget.ideal_h = box->sc->widget.ideal_h;
+ else
+ box->widget.ideal_w = box->sc->widget.ideal_w;
+ ltk_recalculate_box(self);
+
+ return box;
+}
+
+static void
+ltk_box_ensure_rect_shown(ltk_widget *self, ltk_rect r) {
+ ltk_box *box = LTK_CAST_BOX(self);
+ int delta = 0;
+ if (box->orient == LTK_HORIZONTAL) {
+ if (r.x + r.w > self->lrect.w && r.w <= self->lrect.w)
+ delta = r.x - (self->lrect.w - r.w);
+ else if (r.x < 0 || r.w > self->lrect.w)
+ delta = r.x;
+ } else {
+ if (r.y + r.h > self->lrect.h && r.h <= self->lrect.h)
+ delta = r.y - (self->lrect.h - r.h);
+ else if (r.y < 0 || r.h > self->lrect.h)
+ delta = r.y;
+ }
+ if (delta)
+ ltk_scrollbar_scroll(LTK_CAST_WIDGET(box->sc), delta, 0);
+}
+
+static void
+ltk_box_destroy(ltk_widget *self, int shallow) {
+ ltk_box *box = LTK_CAST_BOX(self);
+ ltk_widget *ptr;
+ for (size_t i = 0; i < box->num_widgets; i++) {
+ ptr = box->widgets[i];
+ ptr->parent = NULL;
+ if (!shallow)
+ ltk_widget_destroy(ptr, shallow);
+ }
+ ltk_free(box->widgets);
+ box->sc->widget.parent = NULL;
+ ltk_widget_destroy(LTK_CAST_WIDGET(box->sc), 0);
+ ltk_free(box);
+}
+
+/* FIXME: Make this function name more consistent */
+/* FIXME: The widget positions are set with the old scrollbar->cur_pos, before the
+ virtual_size is set - this can cause problems when a widget changes its size
+ (in the scrolled direction) when resized. */
+/* FIXME: avoid complete recalculation when just scrolling (only position updated) */
+static void
+ltk_recalculate_box(ltk_widget *self) {
+ ltk_box *box = LTK_CAST_BOX(self);
+ ltk_widget *ptr;
+ ltk_rect *sc_rect = &box->sc->widget.lrect;
+ int cur_pos = 0;
+ if (box->orient == LTK_HORIZONTAL)
+ sc_rect->h = box->sc->widget.ideal_h;
+ else
+ sc_rect->w = box->sc->widget.ideal_w;
+ for (size_t i = 0; i < box->num_widgets; i++) {
+ ptr = box->widgets[i];
+ if (box->orient == LTK_HORIZONTAL) {
+ ptr->lrect.x = cur_pos - box->sc->cur_pos;
+ if (ptr->sticky & LTK_STICKY_TOP && ptr->sticky & LTK_STICKY_BOTTOM)
+ ptr->lrect.h = box->widget.lrect.h - sc_rect->h;
+ if (ptr->sticky & LTK_STICKY_TOP)
+ ptr->lrect.y = 0;
+ else if (ptr->sticky & LTK_STICKY_BOTTOM)
+ ptr->lrect.y = box->widget.lrect.h - ptr->lrect.h - sc_rect->h;
+ else
+ ptr->lrect.y = (box->widget.lrect.h - ptr->lrect.h) / 2;
+ cur_pos += ptr->lrect.w;
+ } else {
+ ptr->lrect.y = cur_pos - box->sc->cur_pos;
+ if (ptr->sticky & LTK_STICKY_LEFT && ptr->sticky & LTK_STICKY_RIGHT)
+ ptr->lrect.w = box->widget.lrect.w - sc_rect->w;
+ if (ptr->sticky & LTK_STICKY_LEFT)
+ ptr->lrect.x = 0;
+ else if (ptr->sticky & LTK_STICKY_RIGHT)
+ ptr->lrect.x = box->widget.lrect.w - ptr->lrect.w - sc_rect->w;
+ else
+ ptr->lrect.x = (box->widget.lrect.w - ptr->lrect.w) / 2;
+ cur_pos += ptr->lrect.h;
+ }
+ ptr->crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, ptr->lrect);
+ ltk_widget_resize(ptr);
+ }
+ ltk_scrollbar_set_virtual_size(box->sc, cur_pos);
+ if (box->orient == LTK_HORIZONTAL) {
+ sc_rect->x = 0;
+ sc_rect->y = box->widget.lrect.h - sc_rect->h;
+ sc_rect->w = box->widget.lrect.w;
+ } else {
+ sc_rect->x = box->widget.lrect.w - sc_rect->w;
+ sc_rect->y = 0;
+ sc_rect->h = box->widget.lrect.h;
+ }
+ *sc_rect = ltk_rect_intersect(*sc_rect, (ltk_rect){0, 0, box->widget.lrect.w, box->widget.lrect.h});
+ box->sc->widget.crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, *sc_rect);
+ ltk_widget_resize(LTK_CAST_WIDGET(box->sc));
+}
+
+/* FIXME: This entire resizing thing is a bit weird. For instance, if a label
+ in a vertical box increases its height because its width has been decreased
+ and it is forced to wrap, should that just change the rect or also the
+ ideal size? Ideal size wouldn't really make sense here, but then the box
+ might be forced to add a scrollbar even though the parent widget would
+ actually give it more space if it knew that it needed it. */
+
+static void
+ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget) {
+ ltk_box *box = LTK_CAST_BOX(self);
+ short size_changed = 0;
+ /* This is always reset here - if it needs to be changed,
+ the resize function called by the last child_size_change
+ function will fix it */
+ /* Note: This seems a bit weird, but if each widget set its rect itself,
+ that would also lead to weird things. For instance, if a butten is
+ added to after a box after being ungridded, and its rect was changed
+ by the grid (e.g. because of a column weight), who should reset the
+ rect if it doesn't have sticky set? Of course, the resize function
+ could also set all widgets even if they don't have any sticky
+ settings, but there'd probably be some catch as well. */
+ /* FIXME: the same comment as in grid.c applies */
+ int orig_w = widget->lrect.w;
+ int orig_h = widget->lrect.h;
+ widget->lrect.w = widget->ideal_w;
+ widget->lrect.h = widget->ideal_h;
+ int sc_w = box->sc->widget.lrect.w;
+ int sc_h = box->sc->widget.lrect.h;
+ if (box->orient == LTK_HORIZONTAL && widget->ideal_h + sc_h > box->widget.ideal_h) {
+ box->widget.ideal_h = widget->ideal_h + sc_h;
+ size_changed = 1;
+ } else if (box->orient == LTK_VERTICAL && widget->ideal_w + sc_w > box->widget.ideal_h) {
+ box->widget.ideal_w = widget->ideal_w + sc_w;
+ size_changed = 1;
+ }
+
+ if (size_changed && box->widget.parent && box->widget.parent->vtable->child_size_change)
+ box->widget.parent->vtable->child_size_change(box->widget.parent, (ltk_widget *)box);
+ else
+ ltk_recalculate_box(LTK_CAST_WIDGET(box));
+ if (orig_w != widget->lrect.w || orig_h != widget->lrect.h)
+ ltk_widget_resize(widget);
+}
+
+int
+ltk_box_add(ltk_box *box, ltk_widget *widget, ltk_sticky_mask sticky) {
+ if (widget->parent)
+ return 1;
+ if (box->num_widgets >= box->num_alloc) {
+ size_t new_size = box->num_alloc > 0 ? box->num_alloc * 2 : 4;
+ ltk_widget **new = ltk_realloc(box->widgets, new_size * sizeof(ltk_widget *));
+ box->num_alloc = new_size;
+ box->widgets = new;
+ }
+
+ int sc_w = box->sc->widget.lrect.w;
+ int sc_h = box->sc->widget.lrect.h;
+
+ box->widgets[box->num_widgets++] = widget;
+ if (box->orient == LTK_HORIZONTAL) {
+ box->widget.ideal_w += widget->ideal_w;
+ if (widget->ideal_h + sc_h > box->widget.ideal_h)
+ box->widget.ideal_h = widget->ideal_h + sc_h;
+ } else {
+ box->widget.ideal_h += widget->ideal_h;
+ if (widget->ideal_w + sc_w > box->widget.ideal_w)
+ box->widget.ideal_w = widget->ideal_w + sc_w;
+ }
+ widget->parent = LTK_CAST_WIDGET(box);
+ widget->sticky = sticky;
+ ltk_box_child_size_change(LTK_CAST_WIDGET(box), widget);
+ ltk_window_invalidate_widget_rect(box->widget.window, LTK_CAST_WIDGET(box));
+
+ return 0;
+}
+
+int
+ltk_box_remove_index(ltk_box *box, size_t index) {
+ if (index >= box->num_widgets)
+ return 1;
+ ltk_widget *self = LTK_CAST_WIDGET(box);
+ ltk_widget *widget = box->widgets[index];
+ int sc_w = box->sc->widget.lrect.w;
+ int sc_h = box->sc->widget.lrect.h;
+ if (index < box->num_widgets - 1)
+ memmove(box->widgets + index, box->widgets + index + 1,
+ (box->num_widgets - index - 1) * sizeof(ltk_widget *));
+ box->num_widgets--;
+ ltk_window_invalidate_widget_rect(self->window, self);
+ /* search for new ideal width/height */
+ /* FIXME: make this all a bit nicer and break the lines better */
+ /* FIXME: other part of ideal size not updated */
+ if (box->orient == LTK_HORIZONTAL && widget->ideal_h + sc_h == self->ideal_h) {
+ self->ideal_h = 0;
+ for (size_t j = 0; j < box->num_widgets; j++) {
+ if (box->widgets[j]->ideal_h + sc_h > self->ideal_h)
+ self->ideal_h = box->widgets[j]->ideal_h + sc_h;
+ }
+ if (self->parent)
+ ltk_widget_resize(self->parent);
+ } else if (box->orient == LTK_VERTICAL && widget->ideal_w + sc_w == self->ideal_w) {
+ self->ideal_w = 0;
+ for (size_t j = 0; j < box->num_widgets; j++) {
+ if (box->widgets[j]->ideal_w + sc_w > self->ideal_w)
+ self->ideal_w = box->widgets[j]->ideal_w + sc_w;
+ }
+ if (self->parent)
+ ltk_widget_resize(self->parent);
+ }
+ return 0;
+}
+
+int
+ltk_box_remove(ltk_box *box, ltk_widget *widget) {
+ if (widget->parent != LTK_CAST_WIDGET(box))
+ return 1;
+ widget->parent = NULL;
+ for (size_t i = 0; i < box->num_widgets; i++) {
+ if (box->widgets[i] == widget) {
+ return ltk_box_remove_index(box, i);
+ }
+ }
+
+ return 1;
+}
+
+static int
+ltk_box_remove_child(ltk_widget *self, ltk_widget *widget) {
+ return ltk_box_remove(LTK_CAST_BOX(self), widget);
+}
+
+/* FIXME: maybe come up with a more efficient method */
+static ltk_widget *
+ltk_box_nearest_child(ltk_widget *self, ltk_rect rect) {
+ ltk_box *box = LTK_CAST_BOX(self);
+ ltk_widget *minw = NULL;
+ int min_dist = INT_MAX;
+ for (size_t i = 0; i < box->num_widgets; i++) {
+ ltk_rect r = box->widgets[i]->lrect;
+ int dist = ltk_rect_fakedist(rect, r);
+ if (dist < min_dist) {
+ min_dist = dist;
+ minw = box->widgets[i];
+ }
+ }
+ return minw;
+}
+
+static ltk_widget *
+ltk_box_nearest_child_left(ltk_widget *self, ltk_widget *widget) {
+ ltk_box *box = LTK_CAST_BOX(self);
+ if (box->orient == LTK_VERTICAL)
+ return NULL;
+ return ltk_box_prev_child(self, widget);
+}
+
+static ltk_widget *
+ltk_box_nearest_child_right(ltk_widget *self, ltk_widget *widget) {
+ ltk_box *box = LTK_CAST_BOX(self);
+ if (box->orient == LTK_VERTICAL)
+ return NULL;
+ return ltk_box_next_child(self, widget);
+}
+
+static ltk_widget *
+ltk_box_nearest_child_above(ltk_widget *self, ltk_widget *widget) {
+ ltk_box *box = LTK_CAST_BOX(self);
+ if (box->orient == LTK_HORIZONTAL)
+ return NULL;
+ return ltk_box_prev_child(self, widget);
+}
+
+static ltk_widget *
+ltk_box_nearest_child_below(ltk_widget *self, ltk_widget *widget) {
+ ltk_box *box = LTK_CAST_BOX(self);
+ if (box->orient == LTK_HORIZONTAL)
+ return NULL;
+ return ltk_box_next_child(self, widget);
+}
+
+static ltk_widget *
+ltk_box_prev_child(ltk_widget *self, ltk_widget *child) {
+ ltk_box *box = LTK_CAST_BOX(self);
+ for (size_t i = box->num_widgets; i-- > 0;) {
+ if (box->widgets[i] == child)
+ return i > 0 ? box->widgets[i-1] : NULL;
+ }
+ return NULL;
+}
+
+static ltk_widget *
+ltk_box_next_child(ltk_widget *self, ltk_widget *child) {
+ ltk_box *box = LTK_CAST_BOX(self);
+ for (size_t i = 0; i < box->num_widgets; i++) {
+ if (box->widgets[i] == child)
+ return i < box->num_widgets - 1 ? box->widgets[i+1] : NULL;
+ }
+ return NULL;
+}
+
+static ltk_widget *
+ltk_box_first_child(ltk_widget *self) {
+ ltk_box *box = LTK_CAST_BOX(self);
+ return box->num_widgets > 0 ? box->widgets[0] : NULL;
+}
+
+static ltk_widget *
+ltk_box_last_child(ltk_widget *self) {
+ ltk_box *box = LTK_CAST_BOX(self);
+ return box->num_widgets > 0 ? box->widgets[box->num_widgets-1] : NULL;
+}
+
+static int
+ltk_box_scroll_cb(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
+ (void)self;
+ (void)args;
+ ltk_widget *boxw = LTK_CAST_ARG_WIDGET(data);
+ ltk_recalculate_box(boxw);
+ ltk_window_invalidate_widget_rect(boxw->window, boxw);
+ return 1;
+}
+
+static ltk_widget *
+ltk_box_get_child_at_pos(ltk_widget *self, int x, int y) {
+ ltk_box *box = LTK_CAST_BOX(self);
+ if (ltk_collide_rect(box->sc->widget.crect, x, y))
+ return (ltk_widget *)box->sc;
+ for (size_t i = 0; i < box->num_widgets; i++) {
+ if (ltk_collide_rect(box->widgets[i]->crect, x, y))
+ return box->widgets[i];
+ }
+ return NULL;
+}
+
+static int
+ltk_box_mouse_scroll(ltk_widget *self, ltk_scroll_event *event) {
+ ltk_box *box = LTK_CAST_BOX(self);
+ if (event->dy) {
+ /* FIXME: horizontal scrolling, etc. */
+ /* FIXME: configure scrollstep */
+ int delta = event->dy * -15;
+ ltk_scrollbar_scroll(LTK_CAST_WIDGET(box->sc), delta, 0);
+ ltk_point glob = ltk_widget_pos_to_global(self, event->x, event->y);
+ ltk_window_fake_motion_event(self->window, glob.x, glob.y);
+ return 1;
+ }
+ return 0;
+}
diff --git a/src/box.h b/src/ltk/box.h
diff --git a/src/button.c b/src/ltk/button.c
diff --git a/src/button.h b/src/ltk/button.h
diff --git a/src/clipboard.h b/src/ltk/clipboard.h
diff --git a/src/clipboard_xlib.c b/src/ltk/clipboard_xlib.c
diff --git a/src/clipboard_xlib.h b/src/ltk/clipboard_xlib.h
diff --git a/src/color.h b/src/ltk/color.h
diff --git a/src/color_xlib.c b/src/ltk/color_xlib.c
diff --git a/src/config.c b/src/ltk/config.c
diff --git a/src/config.h b/src/ltk/config.h
diff --git a/src/ctrlsel.c b/src/ltk/ctrlsel.c
diff --git a/src/ctrlsel.h b/src/ltk/ctrlsel.h
diff --git a/src/entry.c b/src/ltk/entry.c
diff --git a/src/entry.h b/src/ltk/entry.h
diff --git a/src/event.h b/src/ltk/event.h
diff --git a/src/event_xlib.c b/src/ltk/event_xlib.c
diff --git a/src/eventdefs.h b/src/ltk/eventdefs.h
diff --git a/src/ltk/graphics.h b/src/ltk/graphics.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LTK_GRAPHICS_H
+#define LTK_GRAPHICS_H
+
+typedef struct ltk_renderdata ltk_renderdata;
+typedef struct ltk_renderwindow ltk_renderwindow;
+
+#include <stddef.h>
+
+#include "rect.h"
+#include "color.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;
+
+typedef struct ltk_surface ltk_surface;
+
+/* FIXME: graphics context */
+ltk_surface *ltk_surface_create(ltk_renderwindow *window, int w, int h);
+void ltk_surface_destroy(ltk_surface *s);
+/* returns 0 if successful, 1 if not resizable */
+int ltk_surface_resize(ltk_surface *s, int w, int h);
+/* FIXME: kind of hacky */
+void ltk_surface_update_size(ltk_surface *s, int w, int h);
+ltk_surface *ltk_surface_from_window(ltk_renderwindow *window, int w, int h);
+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, especially 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_draw_border_clipped(ltk_surface *s, ltk_color *c, ltk_rect rect, ltk_rect clip_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);
+void ltk_surface_fill_polygon_clipped(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints, ltk_rect clip);
+
+/* TODO */
+/*
+void ltk_surface_draw_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2, int line_width);
+void ltk_surface_fill_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2);
+void ltk_surface_draw_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r, int line_width);
+void ltk_surface_fill_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r);
+*/
+
+/* FIXME: rename some of these functions */
+void ltk_renderer_set_imspot(ltk_renderwindow *window, int x, int y);
+ltk_renderdata *ltk_renderer_create(void);
+ltk_renderwindow *ltk_renderer_create_window(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h);
+void ltk_renderer_destroy_window(ltk_renderwindow *window);
+void ltk_renderer_destroy(ltk_renderdata *data);
+void ltk_renderer_set_window_properties(ltk_renderwindow *window, ltk_color *bg);
+/* FIXME: this is kind of out of place */
+void ltk_renderer_swap_buffers(ltk_renderwindow *window);
+/* FIXME: this is just for the socket name in ltkd and is a bit weird */
+unsigned long ltk_renderer_get_window_id(ltk_renderwindow *window);
+
+#endif /* LTK_GRAPHICS_H */
diff --git a/src/ltk/graphics_xlib.c b/src/ltk/graphics_xlib.c
@@ -0,0 +1,600 @@
+/*
+ * Copyright (c) 2022-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <string.h>
+
+#include <X11/XKBlib.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/extensions/XKB.h>
+#include <X11/extensions/Xdbe.h>
+#include <X11/extensions/dbe.h>
+
+#include "graphics_xlib.h"
+#include "color.h"
+#include "memory.h"
+#include "rect.h"
+#include "util.h"
+
+struct ltk_surface {
+ int w, h;
+ ltk_renderwindow *window;
+ Drawable d;
+ #if USE_XFT == 1
+ XftDraw *xftdraw;
+ #endif
+ char resizable;
+};
+
+ltk_surface *
+ltk_surface_create(ltk_renderwindow *window, int w, int h) {
+ ltk_surface *s = ltk_malloc(sizeof(ltk_surface));
+ if (w <= 0)
+ w = 1;
+ if (h <= 0)
+ h = 1;
+ s->w = w;
+ s->h = h;
+ s->window = window;
+ s->d = XCreatePixmap(window->renderdata->dpy, window->xwindow, w, h, window->renderdata->depth);
+ #if USE_XFT == 1
+ s->xftdraw = XftDrawCreate(window->renderdata->dpy, s->d, window->renderdata->vis, window->renderdata->cm);
+ #endif
+ s->resizable = 1;
+ return s;
+}
+
+ltk_surface *
+ltk_surface_from_window(ltk_renderwindow *window, int w, int h) {
+ ltk_surface *s = ltk_malloc(sizeof(ltk_surface));
+ s->w = w;
+ s->h = h;
+ s->window = window;
+ s->d = window->drawable;
+ #if USE_XFT == 1
+ s->xftdraw = XftDrawCreate(window->renderdata->dpy, s->d, window->renderdata->vis, window->renderdata->cm);
+ #endif
+ s->resizable = 0;
+ return s;
+}
+
+void
+ltk_surface_destroy(ltk_surface *s) {
+ #if USE_XFT == 1
+ XftDrawDestroy(s->xftdraw);
+ #endif
+ if (s->resizable)
+ XFreePixmap(s->window->renderdata->dpy, (Pixmap)s->d);
+ ltk_free(s);
+}
+
+void
+ltk_surface_update_size(ltk_surface *s, int w, int h) {
+ /* FIXME: maybe return directly if surface is resizable? */
+ s->w = w;
+ s->h = h;
+ /* FIXME: sort of hacky (this is only used by window surface) */
+ #if USE_XFT == 1
+ XftDrawChange(s->xftdraw, s->d);
+ #endif
+}
+
+int
+ltk_surface_resize(ltk_surface *s, int w, int h) {
+ if (!s->resizable)
+ return 1;
+ s->w = w;
+ s->h = h;
+ XFreePixmap(s->window->renderdata->dpy, (Pixmap)s->d);
+ s->d = XCreatePixmap(s->window->renderdata->dpy, s->window->xwindow, w, h, s->window->renderdata->depth);
+ #if USE_XFT == 1
+ XftDrawChange(s->xftdraw, s->d);
+ #endif
+ return 0;
+}
+
+void
+ltk_surface_get_size(ltk_surface *s, int *w, int *h) {
+ *w = s->w;
+ *h = s->h;
+}
+
+void
+ltk_surface_draw_rect(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width) {
+ XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
+ XSetLineAttributes(s->window->renderdata->dpy, s->window->gc, line_width, LineSolid, CapButt, JoinMiter);
+ XDrawRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, rect.h);
+}
+
+void
+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->renderdata->dpy, s->window->gc, c->xcolor.pixel);
+ if (border_sides & LTK_BORDER_TOP)
+ XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, line_width);
+ if (border_sides & LTK_BORDER_BOTTOM)
+ XFillRectangle(s->window->renderdata->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->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, line_width, rect.h);
+ if (border_sides & LTK_BORDER_RIGHT)
+ XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x + rect.w - line_width, rect.y, line_width, rect.h);
+}
+
+void
+ltk_surface_draw_border_clipped(ltk_surface *s, ltk_color *c, ltk_rect rect, ltk_rect clip_rect, int line_width, ltk_border_sides border_sides) {
+ if (line_width <= 0)
+ return;
+ XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
+ int width;
+ ltk_rect final_rect = ltk_rect_intersect(rect, clip_rect);
+ if (border_sides & LTK_BORDER_TOP) {
+ width = rect.y - final_rect.y;
+ if (width > -line_width) {
+ width = line_width + width;
+ XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, final_rect.x, final_rect.y, final_rect.w, width);
+ }
+ }
+ if (border_sides & LTK_BORDER_BOTTOM) {
+ width = (final_rect.y + final_rect.h) - (rect.y + rect.h);
+ if (width > -line_width) {
+ width = line_width + width;
+ XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, final_rect.x, final_rect.y + final_rect.h - width, final_rect.w, width);
+ }
+ }
+ if (border_sides & LTK_BORDER_LEFT) {
+ width = rect.x - final_rect.x;
+ if (width > -line_width) {
+ width = line_width + width;
+ XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, final_rect.x, final_rect.y, width, final_rect.h);
+ }
+ }
+ if (border_sides & LTK_BORDER_RIGHT) {
+ width = (final_rect.x + final_rect.w) - (rect.x + rect.w);
+ if (width > -line_width) {
+ width = line_width + width;
+ XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, final_rect.x + final_rect.w - width, final_rect.y, width, final_rect.h);
+ }
+ }
+}
+
+void
+ltk_surface_fill_rect(ltk_surface *s, ltk_color *c, ltk_rect rect) {
+ XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
+ XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, rect.h);
+}
+
+void
+ltk_surface_fill_polygon(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints) {
+ /* FIXME: maybe make this static 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->renderdata->dpy, s->window->gc, c->xcolor.pixel);
+ XFillPolygon(s->window->renderdata->dpy, s->d, s->window->gc, final_points, (int)npoints, Complex, CoordModeOrigin);
+ if (npoints > 6)
+ ltk_free(final_points);
+}
+
+static inline void
+swap_ptr(void **ptr1, void **ptr2) {
+ void *tmp = *ptr1;
+ *ptr1 = *ptr2;
+ *ptr2 = tmp;
+}
+
+#define check_size(cond) if (!(cond)) ltk_fatal("Unable to perform polygon clipping. This is a bug, tell lumidify about it.\n")
+
+/* FIXME: xlib already includes clipping... */
+/* FIXME: this can probably be optimized */
+/* This is basically Sutherland-Hodgman, but only the special case for clipping rectangles. */
+void
+ltk_surface_fill_polygon_clipped(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints, ltk_rect clip) {
+ /* FIXME: is this even more efficient? */
+ XPoint tmp_points1[12]; /* to avoid extra allocations when not necessary */
+ XPoint tmp_points2[12];
+ XPoint *points1;
+ XPoint *points2;
+ /* FIXME: be a bit smarter about this */
+ if (npoints <= 6) {
+ points1 = tmp_points1;
+ points2 = tmp_points2;
+ } else {
+ /* FIXME: I'm pretty sure there can never be more points than this
+ since we're only clipping against a rectangle, right?
+ If I can be sure about that, I can remove all the check_size's below. */
+ points1 = ltk_reallocarray(NULL, npoints, sizeof(XPoint) * 2);
+ points2 = ltk_reallocarray(NULL, npoints, sizeof(XPoint) * 2);
+ }
+
+ size_t num1 = npoints;
+ size_t num2 = 0;
+ for (size_t i = 0; i < npoints; i++) {
+ points1[i].x = (short)points[i].x;
+ points1[i].y = (short)points[i].y;
+ }
+
+ for (size_t i = 0; i < num1; i++) {
+ XPoint p1 = points1[i];
+ XPoint p2 = points1[(i + 1) % num1];
+ if (p1.x >= clip.x) {
+ check_size(num2 < npoints * 2);
+ points2[num2++] = p1;
+ if (p2.x < clip.x) {
+ check_size(num2 < npoints * 2);
+ points2[num2++] = (XPoint){.x = (short)clip.x, .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x - p1.x) / (p2.x - p1.x))};
+ }
+ } else if (p2.x >= clip.x) {
+ check_size(num2 < npoints * 2);
+ points2[num2++] = (XPoint){.x = (short)clip.x, .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x - p1.x) / (p2.x - p1.x))};
+ }
+ }
+ num1 = num2;
+ num2 = 0;
+ swap_ptr((void**)&points1, (void**)&points2);
+
+ for (size_t i = 0; i < num1; i++) {
+ XPoint p1 = points1[i];
+ XPoint p2 = points1[(i + 1) % num1];
+ if (p1.x <= clip.x + clip.w) {
+ check_size(num2 < npoints * 2);
+ points2[num2++] = p1;
+ if (p2.x > clip.x + clip.w) {
+ check_size(num2 < npoints * 2);
+ points2[num2++] = (XPoint){.x = (short)(clip.x + clip.w), .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x + clip.w - p1.x) / (p2.x - p1.x))};
+ }
+ } else if (p2.x <= clip.x + clip.w) {
+ check_size(num2 < npoints * 2);
+ points2[num2++] = (XPoint){.x = (short)(clip.x + clip.w), .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x + clip.w - p1.x) / (p2.x - p1.x))};
+ }
+ }
+ num1 = num2;
+ num2 = 0;
+ swap_ptr((void**)&points1, (void**)&points2);
+
+ for (size_t i = 0; i < num1; i++) {
+ XPoint p1 = points1[i];
+ XPoint p2 = points1[(i + 1) % num1];
+ if (p1.y >= clip.y) {
+ check_size(num2 < npoints * 2);
+ points2[num2++] = p1;
+ if (p2.y < clip.y) {
+ check_size(num2 < npoints * 2);
+ points2[num2++] = (XPoint){.y = (short)clip.y, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y - p1.y) / (p2.y - p1.y))};
+ }
+ } else if (p2.y >= clip.y) {
+ check_size(num2 < npoints * 2);
+ points2[num2++] = (XPoint){.y = (short)clip.y, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y - p1.y) / (p2.y - p1.y))};
+ }
+ }
+ num1 = num2;
+ num2 = 0;
+ swap_ptr((void**)&points1, (void**)&points2);
+
+ for (size_t i = 0; i < num1; i++) {
+ XPoint p1 = points1[i];
+ XPoint p2 = points1[(i + 1) % num1];
+ if (p1.y <= clip.y + clip.h) {
+ check_size(num2 < npoints * 2);
+ points2[num2++] = p1;
+ if (p2.y > clip.y + clip.h) {
+ check_size(num2 < npoints * 2);
+ points2[num2++] = (XPoint){.y = (short)clip.y + clip.h, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y + clip.h - p1.y) / (p2.y - p1.y))};
+ }
+ } else if (p2.y <= clip.y + clip.h) {
+ check_size(num2 < npoints * 2);
+ points2[num2++] = (XPoint){.y = (short)clip.y + clip.h, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y + clip.h - p1.y) / (p2.y - p1.y))};
+ }
+ }
+
+ if (num2 > 0) {
+ XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
+ XFillPolygon(s->window->renderdata->dpy, s->d, s->window->gc, points2, (int)num2, Complex, CoordModeOrigin);
+ }
+ if (npoints > 6) {
+ ltk_free(points1);
+ ltk_free(points2);
+ }
+}
+
+void
+ltk_surface_copy(ltk_surface *src, ltk_surface *dst, ltk_rect src_rect, int dst_x, int dst_y) {
+ XCopyArea(
+ src->window->renderdata->dpy, src->d, dst->d, src->window->gc,
+ src_rect.x, src_rect.y, src_rect.w, src_rect.h, dst_x, dst_y
+ );
+}
+
+/* TODO */
+/*
+void
+ltk_surface_draw_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2, int line_width) {
+}
+
+void
+ltk_surface_fill_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2) {
+}
+
+void
+ltk_surface_draw_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r, int line_width) {
+}
+
+void
+ltk_surface_fill_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r) {
+}
+*/
+
+#if USE_XFT == 1
+XftDraw *
+ltk_surface_get_xft_draw(ltk_surface *s) {
+ return s->xftdraw;
+}
+#endif
+
+Drawable
+ltk_surface_get_drawable(ltk_surface *s) {
+ return s->d;
+}
+
+/* FIXME: move this to a file where it makes more sense */
+/* blatantly stolen from st */
+static void ximinstantiate(Display *dpy, XPointer client, XPointer call);
+static void ximdestroy(XIM xim, XPointer client, XPointer call);
+static int xicdestroy(XIC xim, XPointer client, XPointer call);
+static int ximopen(ltk_renderwindow *window);
+
+static void
+ximdestroy(XIM xim, XPointer client, XPointer call) {
+ (void)xim;
+ (void)call;
+ ltk_renderwindow *window = (ltk_renderwindow *)client;
+ window->xim = NULL;
+ XRegisterIMInstantiateCallback(
+ window->renderdata->dpy, NULL, NULL, NULL, ximinstantiate, (XPointer)window
+ );
+ XFree(window->spotlist);
+}
+
+static int
+xicdestroy(XIC xim, XPointer client, XPointer call) {
+ (void)xim;
+ (void)call;
+ ltk_renderwindow *window = (ltk_renderwindow *)client;
+ window->xic = NULL;
+ return 1;
+}
+
+static int
+ximopen(ltk_renderwindow *window) {
+ XIMCallback imdestroy = { .client_data = (XPointer)window, .callback = ximdestroy };
+ XICCallback icdestroy = { .client_data = (XPointer)window, .callback = xicdestroy };
+
+ window->xim = XOpenIM(window->renderdata->dpy, NULL, NULL, NULL);
+ if (window->xim == NULL)
+ return 0;
+
+ if (XSetIMValues(window->xim, XNDestroyCallback, &imdestroy, NULL))
+ ltk_warn("XSetIMValues: Could not set XNDestroyCallback.\n");
+
+ window->spotlist = XVaCreateNestedList(0, XNSpotLocation, &window->spot, NULL);
+
+ if (window->xic == NULL) {
+ window->xic = XCreateIC(
+ window->xim, XNInputStyle,
+ XIMPreeditNothing | XIMStatusNothing,
+ XNClientWindow, window->xwindow,
+ XNDestroyCallback, &icdestroy, NULL
+ );
+ }
+ if (window->xic == NULL)
+ ltk_warn("XCreateIC: Could not create input context.\n");
+
+ return 1;
+}
+
+static void
+ximinstantiate(Display *dpy, XPointer client, XPointer call) {
+ (void)call;
+ ltk_renderwindow *window = (ltk_renderwindow *)client;
+ if (ximopen(window)) {
+ XUnregisterIMInstantiateCallback(
+ dpy, NULL, NULL, NULL, ximinstantiate, (XPointer)window
+ );
+ }
+}
+
+void
+ltk_renderer_set_imspot(ltk_renderwindow *window, int x, int y) {
+ if (window->xic == NULL)
+ return;
+ window->spot.x = x;
+ window->spot.y = y;
+ XSetICValues(window->xic, XNPreeditAttributes, window->spotlist, NULL);
+}
+
+ltk_renderdata *
+ltk_renderer_create(void) {
+ /* FIXME: this might not be the best place for this */
+ XSetLocaleModifiers("");
+ ltk_renderdata *renderdata = ltk_malloc(sizeof(ltk_renderdata));
+ renderdata->dpy = XOpenDisplay(NULL);
+ renderdata->screen = DefaultScreen(renderdata->dpy);
+ renderdata->db_enabled = 0;
+ /* based on http://wili.cc/blog/xdbe.html */
+ int major, minor;
+ if (XdbeQueryExtension(renderdata->dpy, &major, &minor)) {
+ int num_screens = 1;
+ Drawable screens[] = {DefaultRootWindow(renderdata->dpy)};
+ XdbeScreenVisualInfo *info = XdbeGetVisualInfo(
+ renderdata->dpy, screens, &num_screens
+ );
+ if (!info || num_screens < 1 || info->count < 1) {
+ ltk_fatal("No visuals support Xdbe.");
+ }
+ XVisualInfo xvisinfo_templ;
+ /* we know there's at least one */
+ xvisinfo_templ.visualid = info->visinfo[0].visual;
+ /* FIXME: proper screen number? */
+ xvisinfo_templ.screen = 0;
+ xvisinfo_templ.depth = info->visinfo[0].depth;
+ int matches;
+ XVisualInfo *xvisinfo_match = XGetVisualInfo(
+ renderdata->dpy,
+ VisualIDMask | VisualScreenMask | VisualDepthMask,
+ &xvisinfo_templ, &matches
+ );
+ if (!xvisinfo_match || matches < 1) {
+ ltk_fatal("Couldn't match a Visual with double buffering.\n");
+ }
+ renderdata->vis = xvisinfo_match->visual;
+ /* FIXME: is it legal to free this while keeping the visual? */
+ XFree(xvisinfo_match);
+ XdbeFreeVisualInfo(info);
+ renderdata->db_enabled = 1;
+ } else {
+ renderdata->vis = DefaultVisual(renderdata->dpy, renderdata->screen);
+ ltk_warn("No Xdbe support.\n");
+ }
+ renderdata->cm = DefaultColormap(renderdata->dpy, renderdata->screen);
+ renderdata->wm_delete_msg = XInternAtom(renderdata->dpy, "WM_DELETE_WINDOW", False);
+ renderdata->depth = DefaultDepth(renderdata->dpy, renderdata->screen);
+ renderdata->xkb_supported = 1;
+ renderdata->xkb_event_type = 0;
+ if (!XkbQueryExtension(renderdata->dpy, 0, &renderdata->xkb_event_type, NULL, &major, &minor)) {
+ ltk_warn("XKB not supported.\n");
+ renderdata->xkb_supported = 0;
+ } else {
+ /* This should select the events when the keyboard mapping changes.
+ * When e.g. 'setxkbmap us' is executed, two events are sent, but I
+ * haven't figured out how to change that. When the xkb layout
+ * switching is used (e.g. 'setxkbmap -option grp:shifts_toggle'),
+ * this issue does not occur because only a state event is sent. */
+ XkbSelectEvents(
+ renderdata->dpy, XkbUseCoreKbd,
+ XkbNewKeyboardNotifyMask, XkbNewKeyboardNotifyMask
+ );
+ XkbSelectEventDetails(
+ renderdata->dpy, XkbUseCoreKbd, XkbStateNotify,
+ XkbAllStateComponentsMask, XkbGroupStateMask
+ );
+ }
+ return renderdata;
+}
+
+ltk_renderwindow *
+ltk_renderer_create_window(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h) {
+ XSetWindowAttributes attrs;
+ ltk_renderwindow *window = ltk_malloc(sizeof(ltk_renderwindow));
+ window->renderdata = data;
+ memset(&attrs, 0, sizeof(attrs));
+ attrs.background_pixel = BlackPixel(data->dpy, data->screen);
+ attrs.colormap = data->cm;
+ attrs.border_pixel = WhitePixel(data->dpy, data->screen);
+ /* this causes the window contents to be kept
+ * when it is resized, leading to less flicker */
+ attrs.bit_gravity = NorthWestGravity;
+ attrs.event_mask =
+ ExposureMask | KeyPressMask | KeyReleaseMask |
+ ButtonPressMask | ButtonReleaseMask |
+ StructureNotifyMask | PointerMotionMask;
+ /* FIXME: set border width */
+ window->xwindow = XCreateWindow(
+ data->dpy, DefaultRootWindow(data->dpy), x, y,
+ w, h, 0, data->depth,
+ InputOutput, data->vis,
+ CWBackPixel | CWColormap | CWBitGravity | CWEventMask | CWBorderPixel, &attrs
+ );
+
+ if (data->db_enabled) {
+ window->back_buf = XdbeAllocateBackBufferName(
+ data->dpy, window->xwindow, XdbeBackground
+ );
+ } else {
+ window->back_buf = window->xwindow;
+ }
+ window->drawable = window->back_buf;
+ window->gc = XCreateGC(data->dpy, window->xwindow, 0, 0);
+ XSetStandardProperties(
+ data->dpy, window->xwindow,
+ title, NULL, None, NULL, 0, NULL
+ );
+ /* FIXME: check return value */
+ XSetWMProtocols(data->dpy, window->xwindow, &data->wm_delete_msg, 1);
+
+ window->xic = NULL;
+ window->xim = NULL;
+ if (!ximopen(window)) {
+ XRegisterIMInstantiateCallback(
+ window->renderdata->dpy, NULL, NULL, NULL,
+ ximinstantiate, (XPointer)window
+ );
+ }
+
+ XClearWindow(window->renderdata->dpy, window->xwindow);
+ XMapRaised(window->renderdata->dpy, window->xwindow);
+
+ return window;
+}
+
+void
+ltk_renderer_destroy_window(ltk_renderwindow *window) {
+ XFreeGC(window->renderdata->dpy, window->gc);
+ if (window->spotlist)
+ XFree(window->spotlist);
+ /* FIXME: destroy xim/xic? */
+ XDestroyWindow(window->renderdata->dpy, window->xwindow);
+ ltk_free(window);
+}
+
+void
+ltk_renderer_destroy(ltk_renderdata *renderdata) {
+ XCloseDisplay(renderdata->dpy);
+ /* FIXME: destroy visual, wm_delete_msg, etc.? */
+ ltk_free(renderdata);
+}
+
+/* FIXME: this is a completely random collection of properties and should be
+ changed to a more sensible list */
+void
+ltk_renderer_set_window_properties(ltk_renderwindow *window, ltk_color *bg) {
+ XSetWindowBackground(window->renderdata->dpy, window->xwindow, bg->xcolor.pixel);
+}
+
+void
+ltk_renderer_swap_buffers(ltk_renderwindow *window) {
+ XdbeSwapInfo swap_info;
+ swap_info.swap_window = window->xwindow;
+ swap_info.swap_action = XdbeBackground;
+ if (!XdbeSwapBuffers(window->renderdata->dpy, &swap_info, 1))
+ ltk_fatal("Unable to swap buffers.\n");
+ XFlush(window->renderdata->dpy);
+}
+
+unsigned long
+ltk_renderer_get_window_id(ltk_renderwindow *window) {
+ return (unsigned long)window->xwindow;
+}
diff --git a/src/graphics_xlib.h b/src/ltk/graphics_xlib.h
diff --git a/src/ltk/grid.c b/src/ltk/grid.c
@@ -0,0 +1,546 @@
+/* FIXME: sometimes, resizing doesn't work properly when running test.sh */
+
+/*
+ * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/* TODO: make ungrid function also adjust static row/column width/height
+ -> also, how should the grid deal with a widget spanning over multiple
+ rows/columns with static size - if all are static, it could just
+ divide the widget size (it would complicate things, though), but
+ what should happen if some rows/columns under the span do have a
+ positive weight? */
+
+#include <stddef.h>
+#include <limits.h>
+
+#include "memory.h"
+#include "rect.h"
+#include "widget.h"
+#include "util.h"
+#include "grid.h"
+#include "graphics.h"
+
+void ltk_grid_set_row_weight(ltk_grid *grid, int row, int weight);
+void ltk_grid_set_column_weight(ltk_grid *grid, int column, int weight);
+ltk_grid *ltk_grid_create(ltk_window *window, int rows, int columns);
+int ltk_grid_add(
+ ltk_grid *grid, ltk_widget *widget,
+ int row, int column, int row_span, int column_span,
+ ltk_sticky_mask sticky
+);
+/* just a wrapper around ltk_grid_remove to make types match */
+static int ltk_grid_remove_child(ltk_widget *self, ltk_widget *widget);
+int ltk_grid_remove(ltk_grid *grid, ltk_widget *widget);
+
+static void ltk_grid_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip);
+static void ltk_grid_destroy(ltk_widget *self, int shallow);
+static void ltk_recalculate_grid(ltk_widget *self);
+static void ltk_grid_child_size_change(ltk_widget *self, ltk_widget *widget);
+static int ltk_grid_find_nearest_column(ltk_grid *grid, int x);
+static int ltk_grid_find_nearest_row(ltk_grid *grid, int y);
+static ltk_widget *ltk_grid_get_child_at_pos(ltk_widget *self, int x, int y);
+
+static ltk_widget *ltk_grid_prev_child(ltk_widget *self, ltk_widget *child);
+static ltk_widget *ltk_grid_next_child(ltk_widget *self, ltk_widget *child);
+static ltk_widget *ltk_grid_first_child(ltk_widget *self);
+static ltk_widget *ltk_grid_last_child(ltk_widget *self);
+
+static ltk_widget *ltk_grid_nearest_child(ltk_widget *self, ltk_rect rect);
+static ltk_widget *ltk_grid_nearest_child_left(ltk_widget *self, ltk_widget *widget);
+static ltk_widget *ltk_grid_nearest_child_right(ltk_widget *self, ltk_widget *widget);
+static ltk_widget *ltk_grid_nearest_child_above(ltk_widget *self, ltk_widget *widget);
+static ltk_widget *ltk_grid_nearest_child_below(ltk_widget *self, ltk_widget *widget);
+
+static struct ltk_widget_vtable vtable = {
+ .draw = <k_grid_draw,
+ .destroy = <k_grid_destroy,
+ .resize = <k_recalculate_grid,
+ .hide = NULL,
+ .change_state = NULL,
+ .child_size_change = <k_grid_child_size_change,
+ .remove_child = <k_grid_remove_child,
+ .mouse_press = NULL,
+ .mouse_scroll = NULL,
+ .mouse_release = NULL,
+ .motion_notify = NULL,
+ .get_child_at_pos = <k_grid_get_child_at_pos,
+ .mouse_leave = NULL,
+ .mouse_enter = NULL,
+ .key_press = NULL,
+ .key_release = NULL,
+ .prev_child = <k_grid_prev_child,
+ .next_child = <k_grid_next_child,
+ .first_child = <k_grid_first_child,
+ .last_child = <k_grid_last_child,
+ .nearest_child = <k_grid_nearest_child,
+ .nearest_child_left = <k_grid_nearest_child_left,
+ .nearest_child_right = <k_grid_nearest_child_right,
+ .nearest_child_above = <k_grid_nearest_child_above,
+ .nearest_child_below = <k_grid_nearest_child_below,
+ .type = LTK_WIDGET_GRID,
+ .flags = 0,
+ .invalid_signal = LTK_GRID_SIGNAL_INVALID,
+};
+
+/* FIXME: only set "dirty" bit to avoid constand recalculation when
+ setting multiple row/column weights? */
+void
+ltk_grid_set_row_weight(ltk_grid *grid, int row, int weight) {
+ ltk_assert(row < grid->rows);
+ grid->row_weights[row] = weight;
+ ltk_recalculate_grid(LTK_CAST_WIDGET(grid));
+}
+
+void
+ltk_grid_set_column_weight(ltk_grid *grid, int column, int weight) {
+ ltk_assert(column < grid->columns);
+ grid->column_weights[column] = weight;
+ ltk_recalculate_grid(LTK_CAST_WIDGET(grid));
+}
+
+static void
+ltk_grid_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) {
+ ltk_grid *grid = LTK_CAST_GRID(self);
+ int i;
+ ltk_rect real_clip = ltk_rect_intersect((ltk_rect){0, 0, self->lrect.w, self->lrect.h}, clip);
+ for (i = 0; i < grid->rows * grid->columns; i++) {
+ if (!grid->widget_grid[i])
+ continue;
+ ltk_widget *ptr = grid->widget_grid[i];
+ int max_w = grid->column_pos[ptr->column + ptr->column_span] - grid->column_pos[ptr->column];
+ int max_h = grid->row_pos[ptr->row + ptr->row_span] - grid->row_pos[ptr->row];
+ ltk_rect r = ltk_rect_intersect(
+ (ltk_rect){grid->column_pos[ptr->column], grid->row_pos[ptr->row], max_w, max_h}, real_clip
+ );
+ ltk_widget_draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, r));
+ }
+}
+
+ltk_grid *
+ltk_grid_create(ltk_window *window, int rows, int columns) {
+ ltk_grid *grid = ltk_malloc(sizeof(ltk_grid));
+
+ ltk_fill_widget_defaults(LTK_CAST_WIDGET(grid), window, &vtable, 0, 0);
+
+ grid->rows = rows;
+ grid->columns = columns;
+ grid->widget_grid = ltk_malloc(rows * columns * sizeof(ltk_widget));
+ grid->row_heights = ltk_malloc(rows * sizeof(int));
+ grid->column_widths = ltk_malloc(rows * sizeof(int));
+ grid->row_weights = ltk_malloc(rows * sizeof(int));
+ grid->column_weights = ltk_malloc(columns * sizeof(int));
+ /* Positions have one extra for the end */
+ grid->row_pos = ltk_malloc((rows + 1) * sizeof(int));
+ grid->column_pos = ltk_malloc((columns + 1) * sizeof(int));
+ /* FIXME: wow, that's horrible, this should just use memset */
+ int i;
+ for (i = 0; i < rows; i++) {
+ grid->row_heights[i] = 0;
+ grid->row_weights[i] = 0;
+ grid->row_pos[i] = 0;
+ }
+ grid->row_pos[rows] = 0;
+ for (i = 0; i < columns; i++) {
+ grid->column_widths[i] = 0;
+ grid->column_weights[i] = 0;
+ grid->column_pos[i] = 0;
+ }
+ grid->column_pos[columns] = 0;
+ for (i = 0; i < rows * columns; i++) {
+ grid->widget_grid[i] = NULL;
+ }
+
+ ltk_recalculate_grid(LTK_CAST_WIDGET(grid));
+ return grid;
+}
+
+static void
+ltk_grid_destroy(ltk_widget *self, int shallow) {
+ ltk_grid *grid = LTK_CAST_GRID(self);
+ ltk_widget *ptr;
+ 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++) {
+ for (int c = ptr->column; c < ptr->column + ptr->column_span; c++) {
+ grid->widget_grid[r * grid->columns + c] = NULL;
+ }
+ }
+ ltk_widget_destroy(ptr, shallow);
+ }
+ }
+ }
+ ltk_free(grid->widget_grid);
+ ltk_free(grid->row_heights);
+ ltk_free(grid->column_widths);
+ ltk_free(grid->row_weights);
+ ltk_free(grid->column_weights);
+ ltk_free(grid->row_pos);
+ ltk_free(grid->column_pos);
+ ltk_free(grid);
+}
+
+static void
+ltk_recalculate_grid(ltk_widget *self) {
+ ltk_grid *grid = LTK_CAST_GRID(self);
+ unsigned int height_static = 0, width_static = 0;
+ unsigned int total_row_weight = 0, total_column_weight = 0;
+ float height_unit = 0, width_unit = 0;
+ unsigned int currentx = 0, currenty = 0;
+ int i, j;
+ for (i = 0; i < grid->rows; i++) {
+ total_row_weight += grid->row_weights[i];
+ if (grid->row_weights[i] == 0) {
+ height_static += grid->row_heights[i];
+ }
+ }
+ for (i = 0; i < grid->columns; i++) {
+ total_column_weight += grid->column_weights[i];
+ if (grid->column_weights[i] == 0) {
+ width_static += grid->column_widths[i];
+ }
+ }
+ /* FIXME: what should be done when static height or width is larger than grid? */
+ if (total_row_weight > 0) {
+ height_unit = (float) (self->lrect.h - height_static) / (float) total_row_weight;
+ }
+ if (total_column_weight > 0) {
+ width_unit = (float) (self->lrect.w - width_static) / (float) total_column_weight;
+ }
+ for (i = 0; i < grid->rows; i++) {
+ grid->row_pos[i] = currenty;
+ if (grid->row_weights[i] > 0) {
+ grid->row_heights[i] = grid->row_weights[i] * height_unit;
+ }
+ currenty += grid->row_heights[i];
+ }
+ grid->row_pos[grid->rows] = currenty;
+ for (i = 0; i < grid->columns; i++) {
+ grid->column_pos[i] = currentx;
+ if (grid->column_weights[i] > 0) {
+ grid->column_widths[i] = grid->column_weights[i] * width_unit;
+ }
+ currentx += grid->column_widths[i];
+ }
+ grid->column_pos[grid->columns] = currentx;
+ /*int orig_width, orig_height;*/
+ int end_column, end_row;
+ for (i = 0; i < grid->rows; i++) {
+ for (j = 0; j < grid->columns; j++) {
+ ltk_widget *ptr = grid->widget_grid[i * grid->columns + j];
+ if (!ptr || ptr->row != i || ptr->column != j)
+ continue;
+ /*orig_width = ptr->lrect.w;
+ orig_height = ptr->lrect.h;*/
+ ptr->lrect.w = ptr->ideal_w;
+ ptr->lrect.h = ptr->ideal_h;
+ end_row = i + ptr->row_span;
+ end_column = j + ptr->column_span;
+ int max_w = grid->column_pos[end_column] - grid->column_pos[j];
+ int max_h = grid->row_pos[end_row] - grid->row_pos[i];
+ int stretch_width = (ptr->sticky & LTK_STICKY_LEFT) && (ptr->sticky & LTK_STICKY_RIGHT);
+ int shrink_width = (ptr->sticky & LTK_STICKY_SHRINK_WIDTH) && ptr->lrect.w > max_w;
+ int stretch_height = (ptr->sticky & LTK_STICKY_TOP) && (ptr->sticky & LTK_STICKY_BOTTOM);
+ int shrink_height = (ptr->sticky & LTK_STICKY_SHRINK_HEIGHT) && ptr->lrect.h > max_h;
+ if (stretch_width || shrink_width)
+ ptr->lrect.w = max_w;
+ if (stretch_height || shrink_height)
+ ptr->lrect.h = max_h;
+ if (ptr->sticky & LTK_STICKY_PRESERVE_ASPECT_RATIO) {
+ if (!stretch_width && !shrink_width) {
+ ptr->lrect.w = (int)(((double)ptr->lrect.h / ptr->ideal_h) * ptr->ideal_w);
+ } else if (!stretch_height && !shrink_height) {
+ ptr->lrect.h = (int)(((double)ptr->lrect.w / ptr->ideal_w) * ptr->ideal_h);
+ } else {
+ double scale_w = (double)ptr->lrect.w / ptr->ideal_w;
+ double scale_h = (double)ptr->lrect.h / ptr->ideal_h;
+ if (scale_w * ptr->ideal_h > ptr->lrect.h)
+ ptr->lrect.w = (int)(scale_h * ptr->ideal_w);
+ else if (scale_h * ptr->ideal_w > ptr->lrect.w)
+ ptr->lrect.h = (int)(scale_w * ptr->ideal_h);
+ }
+ }
+
+ /* the "default" case needs to come first because the widget may be stretched
+ with aspect ratio preserving, and in that case it should still be centered */
+ if (stretch_width || !(ptr->sticky & (LTK_STICKY_RIGHT|LTK_STICKY_LEFT))) {
+ ptr->lrect.x = grid->column_pos[j] + (grid->column_pos[end_column] - grid->column_pos[j] - ptr->lrect.w) / 2;
+ } else if (ptr->sticky & LTK_STICKY_RIGHT) {
+ ptr->lrect.x = grid->column_pos[end_column] - ptr->lrect.w;
+ } else if (ptr->sticky & LTK_STICKY_LEFT) {
+ ptr->lrect.x = grid->column_pos[j];
+ }
+
+ if (stretch_height || !(ptr->sticky & (LTK_STICKY_TOP|LTK_STICKY_BOTTOM))) {
+ ptr->lrect.y = grid->row_pos[i] + (grid->row_pos[end_row] - grid->row_pos[i] - ptr->lrect.h) / 2;
+ } else if (ptr->sticky & LTK_STICKY_BOTTOM) {
+ ptr->lrect.y = grid->row_pos[end_row] - ptr->lrect.h;
+ } else if (ptr->sticky & LTK_STICKY_TOP) {
+ ptr->lrect.y = grid->row_pos[i];
+ }
+ /* intersect both with the grid rect and with the rect of the covered cells since there may be
+ weird cases where the layout doesn't work properly and the cells are partially outside the grid */
+ ptr->crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, ptr->lrect);
+ ptr->crect = ltk_rect_intersect((ltk_rect){grid->column_pos[j], grid->row_pos[i], max_w, max_h}, ptr->crect);
+
+ /* FIXME: Figure out a better system for this - it would be nice to make it more
+ efficient by not doing anything if nothing changed, but that doesn't work when
+ this function was called because of a child_size_change. In that case, if a
+ container widget is nested inside another container widget and another widget
+ inside the nested container sends a child_size_change but the toplevel container
+ doesn't change the size of the container, the position/size of the widget at the
+ bottom of the hierarchy will never be updated. That's why updates are forced
+ here even if seemingly nothing changed, but there probably is a better way. */
+ /*if (orig_width != ptr->lrect.w || orig_height != ptr->lrect.h)*/
+ ltk_widget_resize(ptr);
+ }
+ }
+}
+
+/* FIXME: Maybe add debug stuff to check that grid is actually parent of widget */
+static void
+ltk_grid_child_size_change(ltk_widget *self, ltk_widget *widget) {
+ ltk_grid *grid = LTK_CAST_GRID(self);
+ short size_changed = 0;
+ int orig_w = widget->lrect.w;
+ int orig_h = widget->lrect.h;
+ widget->lrect.w = widget->ideal_w;
+ widget->lrect.h = widget->ideal_h;
+ if (grid->column_weights[widget->column] == 0 &&
+ widget->lrect.w > grid->column_widths[widget->column]) {
+ self->ideal_w += widget->lrect.w - grid->column_widths[widget->column];
+ grid->column_widths[widget->column] = widget->lrect.w;
+ size_changed = 1;
+ }
+ if (grid->row_weights[widget->row] == 0 &&
+ widget->lrect.h > grid->row_heights[widget->row]) {
+ self->ideal_h += widget->lrect.h - grid->row_heights[widget->row];
+ grid->row_heights[widget->row] = widget->lrect.h;
+ size_changed = 1;
+ }
+ if (size_changed && self->parent && self->parent->vtable->child_size_change)
+ self->parent->vtable->child_size_change(self->parent, LTK_CAST_WIDGET(grid));
+ else
+ ltk_recalculate_grid(LTK_CAST_WIDGET(grid));
+ if (widget->lrect.w != orig_w || widget->lrect.h != orig_h)
+ ltk_widget_resize(widget);
+}
+
+/* FIXME: Check if widget already exists at position */
+int
+ltk_grid_add(
+ ltk_grid *grid, ltk_widget *widget,
+ int row, int column, int row_span, int column_span,
+ ltk_sticky_mask sticky
+) {
+ if (widget->parent)
+ return 1;
+ /* FIXME: decide which checks should be asserts and which should be error returns */
+ /* the client-server version of ltk shouldn't abort on errors like these */
+ ltk_assert(row >= 0 && row + row_span <= grid->rows);
+ ltk_assert(column >= 0 && column + column_span <= grid->columns);
+
+ widget->sticky = sticky;
+ widget->row = row;
+ widget->column = column;
+ widget->row_span = row_span;
+ widget->column_span = column_span;
+ for (int i = row; i < row + row_span; i++) {
+ for (int j = column; j < column + column_span; j++) {
+ grid->widget_grid[i * grid->columns + j] = widget;
+ }
+ }
+ widget->parent = LTK_CAST_WIDGET(grid);
+ ltk_grid_child_size_change(LTK_CAST_WIDGET(grid), widget);
+ ltk_window_invalidate_widget_rect(LTK_CAST_WIDGET(grid)->window, LTK_CAST_WIDGET(grid));
+
+ return 0;
+}
+
+int
+ltk_grid_remove(ltk_grid *grid, ltk_widget *widget) {
+ if (widget->parent != LTK_CAST_WIDGET(grid))
+ return 1;
+ widget->parent = NULL;
+ for (int i = widget->row; i < widget->row + widget->row_span; i++) {
+ for (int j = widget->column; j < widget->column + widget->column_span; j++) {
+ grid->widget_grid[i * grid->columns + j] = NULL;
+ }
+ }
+ ltk_window_invalidate_widget_rect(LTK_CAST_WIDGET(grid)->window, LTK_CAST_WIDGET(grid));
+
+ return 0;
+}
+
+static int
+ltk_grid_remove_child(ltk_widget *self, ltk_widget *widget) {
+ return ltk_grid_remove(LTK_CAST_GRID(self), widget);
+}
+
+static int
+ltk_grid_find_nearest_column(ltk_grid *grid, int x) {
+ int i;
+ for (i = 0; i < grid->columns; i++) {
+ if (grid->column_pos[i] <= x && grid->column_pos[i + 1] >= x) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+static int
+ltk_grid_find_nearest_row(ltk_grid *grid, int y) {
+ int i;
+ for (i = 0; i < grid->rows; i++) {
+ if (grid->row_pos[i] <= y && grid->row_pos[i + 1] >= y) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+/* FIXME: maybe come up with a more efficient method */
+static ltk_widget *
+ltk_grid_nearest_child(ltk_widget *self, ltk_rect rect) {
+ ltk_grid *grid = LTK_CAST_GRID(self);
+ ltk_widget *minw = NULL;
+ int min_dist = INT_MAX;
+ /* FIXME: rows and columns shouldn't be int */
+ for (size_t i = 0; i < (size_t)(grid->rows * grid->columns); i++) {
+ if (!grid->widget_grid[i])
+ continue;
+ /* FIXME: this checks widgets with row/columnspan > 1 multiple times */
+ ltk_rect r = grid->widget_grid[i]->lrect;
+ int dist = ltk_rect_fakedist(rect, r);
+ if (dist < min_dist) {
+ min_dist = dist;
+ minw = grid->widget_grid[i];
+ }
+ }
+ return minw;
+}
+
+/* FIXME: assertions to check that widget row/column are legal */
+static ltk_widget *
+ltk_grid_nearest_child_left(ltk_widget *self, ltk_widget *widget) {
+ ltk_grid *grid = LTK_CAST_GRID(self);
+ unsigned int col = widget->column;
+ ltk_widget *cur = NULL;
+ while (col-- > 0) {
+ cur = grid->widget_grid[widget->row * grid->columns + col];
+ if (cur && cur != widget)
+ return cur;
+ }
+ return NULL;
+}
+
+static ltk_widget *
+ltk_grid_nearest_child_right(ltk_widget *self, ltk_widget *widget) {
+ ltk_grid *grid = LTK_CAST_GRID(self);
+ ltk_widget *cur = NULL;
+ for (int col = widget->column + 1; col < grid->columns; col++) {
+ cur = grid->widget_grid[widget->row * grid->columns + col];
+ if (cur && cur != widget)
+ return cur;
+ }
+ return NULL;
+}
+
+/* FIXME: maybe these should also fall back to widgets in other columns if those
+ exist but no widgets exist in the same column */
+static ltk_widget *
+ltk_grid_nearest_child_above(ltk_widget *self, ltk_widget *widget) {
+ ltk_grid *grid = LTK_CAST_GRID(self);
+ unsigned int row = widget->row;
+ ltk_widget *cur = NULL;
+ while (row-- > 0) {
+ cur = grid->widget_grid[row * grid->columns + widget->column];
+ if (cur && cur != widget)
+ return cur;
+ }
+ return NULL;
+}
+
+static ltk_widget *
+ltk_grid_nearest_child_below(ltk_widget *self, ltk_widget *widget) {
+ ltk_grid *grid = LTK_CAST_GRID(self);
+ ltk_widget *cur = NULL;
+ for (int row = widget->row + 1; row < grid->rows; row++) {
+ cur = grid->widget_grid[row * grid->columns + widget->column];
+ if (cur && cur != widget)
+ return cur;
+ }
+ return NULL;
+}
+
+static ltk_widget *
+ltk_grid_get_child_at_pos(ltk_widget *self, int x, int y) {
+ ltk_grid *grid = LTK_CAST_GRID(self);
+ int row = ltk_grid_find_nearest_row(grid, y);
+ int column = ltk_grid_find_nearest_column(grid, x);
+ if (row == -1 || column == -1)
+ return 0;
+ ltk_widget *ptr = grid->widget_grid[row * grid->columns + column];
+ if (ptr && ltk_collide_rect(ptr->crect, x, y))
+ return ptr;
+ return NULL;
+}
+
+static ltk_widget *
+ltk_grid_prev_child(ltk_widget *self, ltk_widget *child) {
+ ltk_grid *grid = LTK_CAST_GRID(self);
+ unsigned int start = child->row * grid->columns + child->column;
+ while (start-- > 0) {
+ if (grid->widget_grid[start])
+ return grid->widget_grid[start];
+ }
+ return NULL;
+}
+
+static ltk_widget *
+ltk_grid_next_child(ltk_widget *self, ltk_widget *child) {
+ ltk_grid *grid = LTK_CAST_GRID(self);
+ unsigned int start = child->row * grid->columns + child->column;
+ while (++start < (unsigned int)(grid->rows * grid->columns)) {
+ if (grid->widget_grid[start] && grid->widget_grid[start] != child)
+ return grid->widget_grid[start];
+ }
+ return NULL;
+}
+
+static ltk_widget *
+ltk_grid_first_child(ltk_widget *self) {
+ ltk_grid *grid = LTK_CAST_GRID(self);
+ for (unsigned int i = 0; i < (unsigned int)(grid->rows * grid->columns); i++) {
+ if (grid->widget_grid[i])
+ return grid->widget_grid[i];
+ }
+ return NULL;
+}
+
+static ltk_widget *
+ltk_grid_last_child(ltk_widget *self) {
+ ltk_grid *grid = LTK_CAST_GRID(self);
+ for (unsigned int i = grid->rows * grid->columns; i-- > 0;) {
+ if (grid->widget_grid[i])
+ return grid->widget_grid[i];
+ }
+ return NULL;
+}
diff --git a/src/grid.h b/src/ltk/grid.h
diff --git a/src/image.h b/src/ltk/image.h
diff --git a/src/image_imlib.c b/src/ltk/image_imlib.c
diff --git a/src/image_widget.c b/src/ltk/image_widget.c
diff --git a/src/image_widget.h b/src/ltk/image_widget.h
diff --git a/src/ini.c b/src/ltk/ini.c
diff --git a/src/ini.h b/src/ltk/ini.h
diff --git a/src/keys.h b/src/ltk/keys.h
diff --git a/src/label.c b/src/ltk/label.c
diff --git a/src/label.h b/src/ltk/label.h
diff --git a/src/ltk/ltk.c b/src/ltk/ltk.c
@@ -0,0 +1,667 @@
+/*
+ * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <locale.h>
+#include <pwd.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <sys/wait.h>
+
+#include "ltk.h"
+#include "array.h"
+#include "button.h"
+#include "config.h"
+#include "entry.h"
+#include "event.h"
+#include "eventdefs.h"
+#include "graphics.h"
+#include "image.h"
+#include "ini.h"
+#include "label.h"
+#include "macros.h"
+#include "memory.h"
+#include "menu.h"
+#include "rect.h"
+#include "scrollbar.h"
+#include "text.h"
+#include "util.h"
+#include "widget.h"
+
+#define MAX_WINDOW_FONT_SIZE 200
+
+typedef struct {
+ char *tmpfile;
+ ltk_widget *caller;
+ int pid;
+} ltk_cmdinfo;
+
+LTK_ARRAY_INIT_DECL_STATIC(window, ltk_window *)
+LTK_ARRAY_INIT_IMPL_STATIC(window, ltk_window *)
+LTK_ARRAY_INIT_DECL_STATIC(rwindow, ltk_renderwindow *)
+LTK_ARRAY_INIT_IMPL_STATIC(rwindow, ltk_renderwindow *)
+LTK_ARRAY_INIT_DECL_STATIC(cmd, ltk_cmdinfo)
+LTK_ARRAY_INIT_IMPL_STATIC(cmd, ltk_cmdinfo)
+
+static struct {
+ ltk_renderdata *renderdata;
+ ltk_text_context *text_context;
+ ltk_clipboard *clipboard;
+ ltk_array(window) *windows;
+ ltk_array(rwindow) *rwindows;
+ /* PID of external command called e.g. by text widget to edit text.
+ ON exit, cmd_caller->vtable->cmd_return is called with the text
+ the external command wrote to a file. */
+ /*IMPORTANT: this needs to be checked whenever a widget is destroyed!
+ FIXME: allow option to instead return output of command */
+ ltk_array(cmd) *cmds;
+ size_t cur_kbd;
+} shared_data = {NULL, NULL, NULL, NULL, NULL, NULL, 0};
+
+typedef struct {
+ void (*callback)(ltk_callback_arg data);
+ ltk_callback_arg 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 void ltk_handle_event(ltk_event *event);
+static void ltk_load_theme(const char *path);
+static void ltk_uninitialize_theme(void);
+static int ltk_ini_handler(void *renderdata, const char *widget, const char *prop, const char *value);
+static int handle_keypress_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keypress_binding b);
+static int handle_keyrelease_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keyrelease_binding b);
+
+static short running = 1;
+
+typedef struct {
+ char *name;
+ int (*ini_handler)(ltk_renderdata *, const char *, const char *);
+ int (*fill_theme_defaults)(ltk_renderdata *);
+ void (*uninitialize_theme)(ltk_renderdata *);
+ int (*register_keypress)(const char *, size_t, ltk_keypress_binding);
+ int (*register_keyrelease)(const char *, size_t, ltk_keyrelease_binding);
+ void (*cleanup)(void);
+} ltk_widget_funcs;
+
+/* FIXME: use binary search when searching for the widget */
+static ltk_widget_funcs widget_funcs[] = {
+ {
+ .name = "box",
+ .ini_handler = NULL,
+ .fill_theme_defaults = NULL,
+ .uninitialize_theme = NULL,
+ .register_keypress = NULL,
+ .register_keyrelease = NULL,
+ .cleanup = NULL,
+ },
+ {
+ .name = "button",
+ .ini_handler = <k_button_ini_handler,
+ .fill_theme_defaults = <k_button_fill_theme_defaults,
+ .uninitialize_theme = <k_button_uninitialize_theme,
+ .register_keypress = NULL,
+ .register_keyrelease = NULL,
+ .cleanup = NULL,
+ },
+ {
+ .name = "entry",
+ .ini_handler = <k_entry_ini_handler,
+ .fill_theme_defaults = <k_entry_fill_theme_defaults,
+ .uninitialize_theme = <k_entry_uninitialize_theme,
+ .register_keypress = <k_entry_register_keypress,
+ .register_keyrelease = <k_entry_register_keyrelease,
+ .cleanup = <k_entry_cleanup,
+ },
+ {
+ .name = "grid",
+ .ini_handler = NULL,
+ .fill_theme_defaults = NULL,
+ .uninitialize_theme = NULL,
+ .register_keypress = NULL,
+ .register_keyrelease = NULL,
+ .cleanup = NULL,
+ },
+ {
+ .name = "label",
+ .ini_handler = <k_label_ini_handler,
+ .fill_theme_defaults = <k_label_fill_theme_defaults,
+ .uninitialize_theme = <k_label_uninitialize_theme,
+ .register_keypress = NULL,
+ .register_keyrelease = NULL,
+ .cleanup = NULL,
+ },
+ {
+ /* FIXME: this is actually image_widget */
+ .name = "image",
+ .ini_handler = NULL,
+ .fill_theme_defaults = NULL,
+ .uninitialize_theme = NULL,
+ .register_keypress = NULL,
+ .register_keyrelease = NULL,
+ .cleanup = NULL,
+ },
+ {
+ .name = "menu",
+ .ini_handler = <k_menu_ini_handler,
+ .fill_theme_defaults = <k_menu_fill_theme_defaults,
+ .uninitialize_theme = <k_menu_uninitialize_theme,
+ .register_keypress = NULL,
+ .register_keyrelease = NULL,
+ .cleanup = NULL,
+ },
+ {
+ .name = "menuentry",
+ .ini_handler = <k_menuentry_ini_handler,
+ .fill_theme_defaults = <k_menuentry_fill_theme_defaults,
+ .uninitialize_theme = <k_menuentry_uninitialize_theme,
+ .register_keypress = NULL,
+ .register_keyrelease = NULL,
+ .cleanup = NULL,
+ },
+ {
+ .name = "submenu",
+ .ini_handler = <k_submenu_ini_handler,
+ .fill_theme_defaults = <k_submenu_fill_theme_defaults,
+ .uninitialize_theme = <k_submenu_uninitialize_theme,
+ .register_keypress = NULL,
+ .register_keyrelease = NULL,
+ .cleanup = NULL,
+ },
+ {
+ .name = "submenuentry",
+ .ini_handler = <k_submenuentry_ini_handler,
+ .fill_theme_defaults = <k_submenuentry_fill_theme_defaults,
+ .uninitialize_theme = <k_submenuentry_uninitialize_theme,
+ .register_keypress = NULL,
+ .register_keyrelease = NULL,
+ .cleanup = NULL,
+ /*
+ This "widget" is only needed to have separate styles for regular
+ menu entries and submenu entries. "submenu" is just an alias for
+ "menu" in most cases - it's just needed when creating a menu to
+ decide if it's a submenu or not.
+ FIXME: is that even necessary? Why can't it just decide if it's
+ a submenu based on whether it has a parent or not?
+ -> I guess right-click menus are also just submenus, so they
+ need to set it explicitly, but wasn't there another reason?
+ */
+ },
+ {
+ .name = "scrollbar",
+ .ini_handler = <k_scrollbar_ini_handler,
+ .fill_theme_defaults = <k_scrollbar_fill_theme_defaults,
+ .uninitialize_theme = <k_scrollbar_uninitialize_theme,
+ .register_keypress = NULL,
+ .register_keyrelease = NULL,
+ .cleanup = NULL,
+ },
+ {
+ /* Handler for general widget key bindings. */
+ .name = "widget",
+ .ini_handler = NULL,
+ .fill_theme_defaults = NULL,
+ .uninitialize_theme = NULL,
+ .register_keypress = NULL,
+ .register_keyrelease = NULL,
+ .cleanup = NULL,
+ },
+ {
+ /* Handler for window theme. */
+ .name = "window",
+ .ini_handler = <k_window_ini_handler,
+ .fill_theme_defaults = <k_window_fill_theme_defaults,
+ .uninitialize_theme = <k_window_uninitialize_theme,
+ .register_keypress = <k_window_register_keypress,
+ .register_keyrelease = <k_window_register_keyrelease,
+ .cleanup = <k_window_cleanup,
+ }
+};
+
+ltk_renderdata *
+ltk_get_renderer(void) {
+ /* FIXME: check if initialized? */
+ return shared_data.renderdata;
+}
+
+int
+ltk_init(void) {
+ /* FIXME: should ltk set this? probably not */
+ setlocale(LC_CTYPE, "");
+ char *ltk_dir = ltk_setup_directory("LTKDIR", ".ltk", 0);
+ if (!ltk_dir)
+ ltk_fatal_errno("Unable to setup ltk directory.\n");
+ shared_data.cur_kbd = 0;
+
+ /* FIXME: search different directories for config */
+ /* FIXME: don't print error if config or theme file doesn't exist */
+ char *config_path = ltk_strcat_useful(ltk_dir, "/ltk.cfg");
+ char *theme_path;
+ char *errstr = NULL;
+ if (ltk_config_parsefile(config_path, &handle_keypress_binding, &handle_keyrelease_binding, &errstr)) {
+ if (errstr) {
+ ltk_warn("Unable to load config: %s\n", errstr);
+ ltk_free0(errstr);
+ }
+ if (ltk_config_load_default(&handle_keypress_binding, &handle_keyrelease_binding, &errstr)) {
+ /* FIXME: I guess errstr isn't freed here, but whatever */
+ /* FIXME: return error instead of dying */
+ ltk_fatal("Unable to load default config: %s\n", errstr);
+ }
+ }
+ ltk_free0(config_path);
+ theme_path = ltk_strcat_useful(ltk_dir, "/theme.ini");
+ ltk_free0(ltk_dir);
+ shared_data.renderdata = ltk_renderer_create();
+ if (!shared_data.renderdata)
+ return 1; /* FIXME: clean up */
+ ltk_load_theme(theme_path);
+ ltk_free0(theme_path);
+ /* FIXME: maybe "general" theme instead of window theme? */
+ ltk_window_theme *window_theme = ltk_window_get_theme();
+ shared_data.text_context = ltk_text_context_create(shared_data.renderdata, window_theme->font);
+ shared_data.clipboard = ltk_clipboard_create(shared_data.renderdata);
+ /* FIXME: configure cache size; check for overflow */
+ ltk_image_init(shared_data.renderdata, 1024 * 1024 * 4);
+ shared_data.windows = ltk_array_create(window, 1);
+ shared_data.rwindows = ltk_array_create(rwindow, 1);
+ shared_data.cmds = ltk_array_create(cmd, 1);
+ return 0; /* FIXME: or maybe 1? */
+}
+
+static struct {
+ struct timespec last;
+ struct timespec lasttimer;
+} mainloop_data;
+
+void
+ltk_mainloop_init(void) {
+ clock_gettime(CLOCK_MONOTONIC, &mainloop_data.last);
+ mainloop_data.lasttimer = mainloop_data.last;
+
+ /* initialize keyboard mapping */
+ ltk_event event;
+ ltk_generate_keyboard_event(shared_data.renderdata, &event);
+ ltk_handle_event(&event);
+}
+
+/* FIXME: maybe split this up into multiple stages */
+void
+ltk_mainloop_step(int limit_framerate) {
+ ltk_event event;
+
+ /* 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 (would need separate parameter to turn that off when a
+ different mainloop is used) */
+ struct timespec now, elapsed, sleep_time;
+ sleep_time.tv_sec = 0;
+
+ int pid = -1;
+ int wstatus = 0;
+ /* FIXME: kill all children on exit? */
+ if ((pid = waitpid(-1, &wstatus, WNOHANG)) > 0) {
+ ltk_cmdinfo *info;
+ /* FIXME: should commands be split into read/write and block write commands during external editing? */
+ for (size_t i = 0; i < ltk_array_len(shared_data.cmds); i++) {
+ info = &(ltk_array_get(shared_data.cmds, i));
+ if (info->pid == pid) {
+ if (!info->caller) {
+ ltk_warn("Widget disappeared while text was being edited in external program\n");
+ /* FIXME: call overwritten cmd_return! */
+ } else if (info->caller->vtable->cmd_return) {
+ size_t file_len = 0;
+ char *errstr = NULL;
+ char *contents = ltk_read_file(info->tmpfile, &file_len, &errstr);
+ if (!contents) {
+ ltk_warn("Unable to read file '%s' written by external command: %s\n", info->tmpfile, errstr);
+ } else {
+ info->caller->vtable->cmd_return(info->caller, contents, file_len);
+ ltk_free0(contents);
+ }
+ }
+ ltk_free0(info->tmpfile);
+ ltk_array_delete(cmd, shared_data.cmds, i, 1);
+ break;
+ }
+ }
+ }
+ while (!ltk_next_event(
+ shared_data.renderdata,
+ ltk_array_get_buf(shared_data.rwindows),
+ ltk_array_len(shared_data.rwindows),
+ shared_data.clipboard, shared_data.cur_kbd, &event)) {
+ ltk_handle_event(&event);
+ }
+
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ ltk_timespecsub(&now, &mainloop_data.lasttimer, &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 timer 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++;
+ }
+ }
+ mainloop_data.lasttimer = now;
+
+ for (size_t i = 0; i < shared_data.windows->len; i++) {
+ ltk_window *window = shared_data.windows->buf[i];
+ if (window->dirty_rect.w != 0 && window->dirty_rect.h != 0) {
+ ltk_widget_draw(LTK_CAST_WIDGET(window), NULL, 0, 0, (ltk_rect){0, 0, 0, 0});
+ }
+ }
+
+ if (limit_framerate) {
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ ltk_timespecsub(&now, &mainloop_data.last, &elapsed);
+ /* FIXME: configure framerate */
+ if (elapsed.tv_sec == 0 && elapsed.tv_nsec < 20000000LL) {
+ sleep_time.tv_nsec = 20000000LL - elapsed.tv_nsec;
+ nanosleep(&sleep_time, NULL);
+ }
+ mainloop_data.last = now;
+ }
+}
+
+void
+ltk_mainloop_quit(void) {
+ /* FIXME: maybe prevent other events from running? */
+ running = 0;
+}
+
+void
+ltk_mainloop_restartable(void) {
+ ltk_mainloop_init();
+ while (running) {
+ ltk_mainloop_step(1);
+ }
+}
+
+void
+ltk_mainloop(void) {
+ ltk_mainloop_restartable();
+ ltk_deinit();
+}
+
+void
+ltk_deinit(void) {
+ if (running)
+ return;
+ if (shared_data.cmds) {
+ for (size_t i = 0; i < ltk_array_len(shared_data.cmds); i++) {
+ /* FIXME: maybe kill child processes? */
+ ltk_free((ltk_array_get(shared_data.cmds, i)).tmpfile);
+ }
+ ltk_array_destroy(cmd, shared_data.cmds);
+ }
+ shared_data.cmds = NULL;
+ if (shared_data.windows) {
+ for (size_t i = 0; i < ltk_array_len(shared_data.windows); i++) {
+ ltk_window *window = ltk_array_get(shared_data.windows, i);
+ ltk_widget_destroy(LTK_CAST_WIDGET(window), 0);
+ }
+ ltk_array_destroy(window, shared_data.windows);
+ }
+ shared_data.windows = NULL;
+ if (shared_data.rwindows)
+ ltk_array_destroy(rwindow, shared_data.rwindows);
+ shared_data.rwindows = NULL;
+ ltk_config_cleanup();
+ for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
+ if (widget_funcs[i].cleanup)
+ widget_funcs[i].cleanup();
+ }
+ if (shared_data.text_context)
+ ltk_text_context_destroy(shared_data.text_context);
+ shared_data.text_context = NULL;
+ if (shared_data.clipboard)
+ ltk_clipboard_destroy(shared_data.clipboard);
+ shared_data.clipboard = NULL;
+ ltk_events_cleanup();
+ if (shared_data.renderdata) {
+ ltk_uninitialize_theme();
+ ltk_renderer_destroy(shared_data.renderdata);
+ }
+ shared_data.renderdata = NULL;
+}
+
+/* FIXME: check everywhere if initialized already */
+ltk_window *
+ltk_window_create(const char *title, int x, int y, unsigned int w, unsigned int h) {
+ /* FIXME: more asserts, or maybe global "initialized" flag */
+ ltk_assert(shared_data.renderdata != NULL);
+ ltk_assert(shared_data.windows != NULL);
+ ltk_assert(shared_data.rwindows != NULL);
+ ltk_window *window = ltk_window_create_intern(shared_data.renderdata, title, x, y, w, h);
+ ltk_array_append(window, shared_data.windows, window);
+ ltk_array_append(rwindow, shared_data.rwindows, window->renderwindow);
+ return window;
+}
+
+void
+ltk_window_destroy(ltk_widget *self, int shallow) {
+ /* FIXME: would it make sense to do something with 'shallow' here? */
+ (void)shallow;
+ ltk_window *window = LTK_CAST_WINDOW(self);
+ for (size_t i = 0; i < ltk_array_len(shared_data.windows); i++) {
+ if (ltk_array_get(shared_data.windows, i) == window) {
+ ltk_array_delete(window, shared_data.windows, i, 1);
+ ltk_array_delete(rwindow, shared_data.rwindows, i, 1);
+ break;
+ }
+ }
+ ltk_window_destroy_intern(window);
+}
+
+ltk_clipboard *
+ltk_get_clipboard(void) {
+ /* FIXME: what to do when not initialized? */
+ return shared_data.clipboard;
+}
+
+/* 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)(ltk_callback_arg data), ltk_callback_arg 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: standardize return codes - usually, 0 is returned on success, but ini.h
+ uses 1 on success, so this is all a bit confusing */
+/* FIXME: switch away from ini.h */
+static int
+ltk_ini_handler(void *renderdata, const char *widget, const char *prop, const char *value) {
+ for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
+ if (widget_funcs[i].ini_handler && !strcmp(widget, widget_funcs[i].name)) {
+ widget_funcs[i].ini_handler(renderdata, prop, value);
+ return 1;
+ }
+ }
+ return 0;
+}
+
+/* FIXME: don't call ltk_fatal, instead return error from ltk_init */
+static void
+ltk_load_theme(const char *path) {
+ /* FIXME: give line number in error message */
+ if (ini_parse(path, ltk_ini_handler, shared_data.renderdata) != 0) {
+ ltk_warn("Unable to load theme.\n");
+ }
+ for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
+ if (widget_funcs[i].fill_theme_defaults) {
+ if (widget_funcs[i].fill_theme_defaults(shared_data.renderdata)) {
+ ltk_uninitialize_theme();
+ ltk_fatal("Unable to load theme defaults.\n");
+ }
+ }
+ }
+}
+
+static void
+ltk_uninitialize_theme(void) {
+ for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
+ if (widget_funcs[i].uninitialize_theme)
+ widget_funcs[i].uninitialize_theme(shared_data.renderdata);
+ }
+}
+
+static int
+handle_keypress_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keypress_binding b) {
+ for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
+ if (str_array_equal(widget_funcs[i].name, widget_name, wlen)) {
+ if (!widget_funcs[i].register_keypress)
+ return 1;
+ return widget_funcs[i].register_keypress(name, nlen, b);
+ }
+ }
+ return 1;
+}
+
+static int
+handle_keyrelease_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keyrelease_binding b) {
+ for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
+ if (str_array_equal(widget_funcs[i].name, widget_name, wlen)) {
+ if (!widget_funcs[i].register_keyrelease)
+ return 1;
+ return widget_funcs[i].register_keyrelease(name, nlen, b);
+ }
+ }
+ return 1;
+}
+
+int
+ltk_call_cmd(ltk_widget *caller, const char *cmd, size_t cmdlen, const char *text, size_t textlen) {
+ /* FIXME: support environment variable $TMPDIR */
+ ltk_cmdinfo info = {NULL, NULL, -1};
+ info.tmpfile = ltk_strdup("/tmp/ltk.XXXXXX");
+ int fd = mkstemp(info.tmpfile);
+ if (fd == -1) {
+ ltk_warn_errno("Unable to create temporary file while trying to run command '%.*s'\n", (int)cmdlen, cmd);
+ ltk_free0(info.tmpfile);
+ return 1;
+ }
+ close(fd);
+ /* FIXME: give file descriptor directly to modified version of ltk_write_file */
+ char *errstr = NULL;
+ if (ltk_write_file(info.tmpfile, text, textlen, &errstr)) {
+ ltk_warn("Unable to write to file '%s' while trying to run command '%.*s': %s\n", info.tmpfile, (int)cmdlen, cmd, errstr);
+ unlink(info.tmpfile);
+ ltk_free0(info.tmpfile);
+ return 1;
+ }
+ int pid = -1;
+ if ((pid = ltk_parse_run_cmd(cmd, cmdlen, info.tmpfile)) <= 0) {
+ /* FIXME: errno */
+ ltk_warn("Unable to run command '%.*s'\n", (int)cmdlen, cmd);
+ unlink(info.tmpfile);
+ ltk_free0(info.tmpfile);
+ return 1;
+ }
+ info.pid = pid;
+ info.caller = caller;
+ ltk_array_append(cmd, shared_data.cmds, info);
+ return 0;
+}
+
+static void
+ltk_handle_event(ltk_event *event) {
+ size_t kbd_idx;
+ if (event->type == LTK_KEYBOARDCHANGE_EVENT) {
+ /* FIXME: emit event */
+ if (ltk_config_get_language_index(event->keyboard.new_kbd, &kbd_idx))
+ ltk_warn("No language mapping for language \"%s\".\n", event->keyboard.new_kbd);
+ else
+ shared_data.cur_kbd = kbd_idx;
+ } else {
+ if (event->any.window_id < ltk_array_len(shared_data.windows)) {
+ ltk_window_handle_event(ltk_array_get(shared_data.windows, event->any.window_id), event);
+ }
+ }
+}
+
+ltk_text_line *
+ltk_text_line_create_default(uint16_t font_size, char *text, int take_over_text, int width) {
+ return ltk_text_line_create(shared_data.text_context, font_size, text, take_over_text, width);
+}
diff --git a/src/ltk/ltk.h b/src/ltk/ltk.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LTK_H
+#define LTK_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "clipboard.h"
+#include "widget.h"
+#include "window.h"
+#include "text.h"
+
+int ltk_init(void);
+void ltk_deinit(void);
+
+void ltk_mainloop_init(void);
+void ltk_mainloop_step(int limit_framerate);
+
+void ltk_mainloop(void);
+/* FIXME: maybe better name */
+void ltk_mainloop_restartable(void);
+void ltk_mainloop_quit(void);
+
+void ltk_unregister_timer(int timer_id);
+int ltk_register_timer(long first, long repeat, void (*callback)(ltk_callback_arg data), ltk_callback_arg data);
+
+/* These are here so they can be added to the global array in ltk.c */
+ltk_window *ltk_window_create(const char *title, int x, int y, unsigned int w, unsigned int h);
+void ltk_window_destroy(ltk_widget *self, int shallow);
+
+/* FIXME: allow piping text instead of writing to temporary file */
+/* FIXME: how to avoid bad things happening while external program open? maybe store cmd widget somewhere (but could be multiple!) and check if widget to destroy is one of those
+-> alternative: store all widgets in array and only give out IDs, then when returning from cmd, widget is already destroyed and can be ignored
+-> first option maybe just set callback, etc. of current cmd to NULL so widget can still be destroyed */
+int ltk_call_cmd(ltk_widget *caller, const char *cmd, size_t cmdlen, const char *text, size_t textlen);
+
+/* convenience function to use the default text context */
+ltk_text_line *ltk_text_line_create_default(uint16_t font_size, char *text, int take_over_text, int width);
+
+ltk_clipboard *ltk_get_clipboard(void);
+ltk_renderdata *ltk_get_renderer(void);
+
+#endif /* LTK_H */
diff --git a/src/macros.h b/src/ltk/macros.h
diff --git a/src/memory.c b/src/ltk/memory.c
diff --git a/src/memory.h b/src/ltk/memory.h
diff --git a/src/menu.c b/src/ltk/menu.c
diff --git a/src/menu.h b/src/ltk/menu.h
diff --git a/src/rect.c b/src/ltk/rect.c
diff --git a/src/rect.h b/src/ltk/rect.h
diff --git a/src/scrollbar.c b/src/ltk/scrollbar.c
diff --git a/src/scrollbar.h b/src/ltk/scrollbar.h
diff --git a/src/stb_truetype.c b/src/ltk/stb_truetype.c
diff --git a/src/stb_truetype.h b/src/ltk/stb_truetype.h
diff --git a/src/strtonum.c b/src/ltk/strtonum.c
diff --git a/src/surface_cache.c b/src/ltk/surface_cache.c
diff --git a/src/surface_cache.h b/src/ltk/surface_cache.h
diff --git a/src/text.h b/src/ltk/text.h
diff --git a/src/text_pango.c b/src/ltk/text_pango.c
diff --git a/src/text_stb.c b/src/ltk/text_stb.c
diff --git a/src/theme.c b/src/ltk/theme.c
diff --git a/src/theme.h b/src/ltk/theme.h
diff --git a/src/txtbuf.c b/src/ltk/txtbuf.c
diff --git a/src/txtbuf.h b/src/ltk/txtbuf.h
diff --git a/src/ltk/util.c b/src/ltk/util.c
@@ -0,0 +1,394 @@
+/*
+ * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <pwd.h>
+#include <time.h>
+#include <ctype.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+#include "ltk.h"
+#include "util.h"
+#include "array.h"
+#include "memory.h"
+#include "txtbuf.h"
+
+/* FIXME: Should these functions really fail on memory error? */
+
+char *
+ltk_read_file(const char *filename, size_t *len_ret, char **errstr_ret) {
+ long len;
+ char *file_contents;
+ FILE *file;
+
+ /* FIXME: https://wiki.sei.cmu.edu/confluence/display/c/FIO19-C.+Do+not+use+fseek()+and+ftell()+to+compute+the+size+of+a+regular+file */
+ file = fopen(filename, "r");
+ if (!file) goto error;
+ if (fseek(file, 0, SEEK_END)) goto errorclose;
+ len = ftell(file);
+ if (len < 0) goto errorclose;
+ if (fseek(file, 0, SEEK_SET)) goto errorclose;
+ file_contents = ltk_malloc((size_t)len + 1);
+ clearerr(file);
+ fread(file_contents, 1, (size_t)len, file);
+ if (ferror(file)) goto errorclose;
+ file_contents[len] = '\0';
+ if (fclose(file)) goto error;
+ *len_ret = (size_t)len;
+ return file_contents;
+error:
+ if (errstr_ret)
+ *errstr_ret = strerror(errno);
+ return NULL;
+errorclose:
+ if (errstr_ret)
+ *errstr_ret = strerror(errno);
+ fclose(file);
+ return NULL;
+}
+
+/* FIXME: not sure if errno actually is set usefully after all these functions */
+int
+ltk_write_file(const char *path, const char *data, size_t len, char **errstr_ret) {
+ FILE *file = fopen(path, "w");
+ if (!file) goto error;
+ clearerr(file);
+ if (fwrite(data, 1, len, file) < len) goto errorclose;
+ if (fclose(file)) goto error;
+ return 0;
+error:
+ if (errstr_ret)
+ *errstr_ret = strerror(errno);
+ return 1;
+errorclose:
+ if (errstr_ret)
+ *errstr_ret = strerror(errno);
+ fclose(file);
+ return 1;
+}
+
+/* FIXME: maybe have a few standard array types defined somewhere else */
+LTK_ARRAY_INIT_DECL_STATIC(cmd, char *)
+LTK_ARRAY_INIT_IMPL_STATIC(cmd, char *)
+
+static void
+free_helper(char *ptr) {
+ ltk_free(ptr);
+}
+
+/* FIXME: this is really ugly */
+/* FIXME: parse command only once in beginning instead of each time it is run? */
+/* FIXME: this handles double-quote, but the config parser already uses that, so
+ it's kind of weird because it's parsed twice (also backslashes are parsed twice). */
+int
+ltk_parse_run_cmd(const char *cmdtext, size_t len, const char *filename) {
+ int bs = 0;
+ int in_sqstr = 0;
+ int in_dqstr = 0;
+ int in_ws = 1;
+ char c;
+ size_t cur_start = 0;
+ int offset = 0;
+ txtbuf *cur_arg = txtbuf_new();
+ ltk_array(cmd) *cmd = ltk_array_create(cmd, 4);
+ char *cmdcopy = ltk_strndup(cmdtext, len);
+ for (size_t i = 0; i < len; i++) {
+ c = cmdcopy[i];
+ if (c == '\\') {
+ if (bs) {
+ offset++;
+ bs = 0;
+ } else {
+ bs = 1;
+ }
+ } else if (isspace(c)) {
+ if (!in_sqstr && !in_dqstr) {
+ if (bs) {
+ if (in_ws) {
+ in_ws = 0;
+ cur_start = i;
+ offset = 0;
+ } else {
+ offset++;
+ }
+ bs = 0;
+ } else if (!in_ws) {
+ /* FIXME: shouldn't this be < instead of <=? */
+ if (cur_start <= i - offset)
+ txtbuf_appendn(cur_arg, cmdcopy + cur_start, i - cur_start - offset);
+ /* FIXME: cmd is named horribly */
+ ltk_array_append(cmd, cmd, txtbuf_get_textcopy(cur_arg));
+ txtbuf_clear(cur_arg);
+ in_ws = 1;
+ offset = 0;
+ }
+ /* FIXME: parsing weird here - bs just ignored */
+ } else if (bs) {
+ bs = 0;
+ }
+ } else if (c == '%') {
+ if (bs) {
+ if (in_ws) {
+ cur_start = i;
+ offset = 0;
+ } else {
+ offset++;
+ }
+ bs = 0;
+ } else if (!in_sqstr && filename && i < len - 1 && cmdcopy[i + 1] == 'f') {
+ if (!in_ws && cur_start < i - offset)
+ txtbuf_appendn(cur_arg, cmdcopy + cur_start, i - cur_start - offset);
+ txtbuf_append(cur_arg, filename);
+ i++;
+ cur_start = i + 1;
+ offset = 0;
+ } else if (in_ws) {
+ cur_start = i;
+ offset = 0;
+ }
+ in_ws = 0;
+ } else if (c == '"') {
+ if (in_sqstr) {
+ bs = 0;
+ } else if (bs) {
+ if (in_ws) {
+ cur_start = i;
+ offset = 0;
+ } else {
+ offset++;
+ }
+ bs = 0;
+ } else if (in_dqstr) {
+ offset++;
+ in_dqstr = 0;
+ continue;
+ } else {
+ in_dqstr = 1;
+ if (in_ws) {
+ cur_start = i + 1;
+ offset = 0;
+ } else {
+ offset++;
+ continue;
+ }
+ }
+ in_ws = 0;
+ } else if (c == '\'') {
+ if (in_dqstr) {
+ bs = 0;
+ } else if (bs) {
+ if (in_ws) {
+ cur_start = i;
+ offset = 0;
+ } else {
+ offset++;
+ }
+ bs = 0;
+ } else if (in_sqstr) {
+ offset++;
+ in_sqstr = 0;
+ continue;
+ } else {
+ in_sqstr = 1;
+ if (in_ws) {
+ cur_start = i + 1;
+ offset = 0;
+ } else {
+ offset++;
+ continue;
+ }
+ }
+ in_ws = 0;
+ } else if (bs) {
+ if (!in_sqstr && !in_dqstr) {
+ if (in_ws) {
+ cur_start = i;
+ offset = 0;
+ } else {
+ offset++;
+ }
+ }
+ bs = 0;
+ in_ws = 0;
+ } else {
+ if (in_ws) {
+ cur_start = i;
+ offset = 0;
+ }
+ in_ws = 0;
+ }
+ cmdcopy[i - offset] = cmdcopy[i];
+ }
+ if (in_sqstr || in_dqstr) {
+ ltk_warn("Unterminated string in command\n");
+ goto error;
+ }
+ if (!in_ws) {
+ if (cur_start <= len - offset)
+ txtbuf_appendn(cur_arg, cmdcopy + cur_start, len - cur_start - offset);
+ ltk_array_append(cmd, cmd, txtbuf_get_textcopy(cur_arg));
+ }
+ if (cmd->len == 0) {
+ ltk_warn("Empty command\n");
+ goto error;
+ }
+ ltk_array_append(cmd, cmd, NULL); /* necessary for execvp */
+ int fret = -1;
+ if ((fret = fork()) < 0) {
+ ltk_warn("Unable to fork\n");
+ goto error;
+ } else if (fret == 0) {
+ if (execvp(cmd->buf[0], cmd->buf) == -1) {
+ /* FIXME: what to do on error here? */
+ exit(1);
+ }
+ } else {
+ ltk_free(cmdcopy);
+ txtbuf_destroy(cur_arg);
+ ltk_array_destroy_deep(cmd, cmd, &free_helper);
+ return fret;
+ }
+error:
+ ltk_free(cmdcopy);
+ txtbuf_destroy(cur_arg);
+ ltk_array_destroy_deep(cmd, cmd, &free_helper);
+ return -1;
+}
+
+/* If `needed` is larger than `*alloc_size`, resize `*str` to
+ `max(needed, *alloc_size * 2)`. Aborts program on error. */
+void
+ltk_grow_string(char **str, int *alloc_size, int needed) {
+ if (needed <= *alloc_size) return;
+ int new_size = needed > (*alloc_size * 2) ? needed : (*alloc_size * 2);
+ char *new = ltk_realloc(*str, new_size);
+ *str = new;
+ *alloc_size = new_size;
+}
+
+/* Get the directory to store ltk files in and optionally create it if it
+ doesn't exist yet and `create` is set.
+ This first checks the environment variable `env` and, if that doesn't
+ exist, the home directory with "/" and `default` appended.
+ Returns NULL on error. */
+char *
+ltk_setup_directory(const char *envname, const char *defaultname, int create) {
+ char *dir, *dir_orig;
+ struct passwd *pw;
+ uid_t uid;
+
+ dir_orig = getenv(envname);
+ if (dir_orig) {
+ dir = ltk_strdup(dir_orig);
+ } else {
+ uid = getuid();
+ pw = getpwuid(uid);
+ if (!pw)
+ return NULL;
+ size_t len = strlen(pw->pw_dir);
+ size_t dlen = strlen(defaultname);
+ dir = ltk_malloc(len + dlen + 2);
+ strcpy(dir, pw->pw_dir);
+ dir[len] = '/';
+ strcpy(dir + len + 1, defaultname);
+ }
+
+ if (create && mkdir(dir, 0700) < 0) {
+ if (errno != EEXIST)
+ return NULL;
+ }
+
+ return dir;
+}
+
+/* Concatenate the two given strings and return the result.
+ This allocates new memory for the result string, unlike
+ the actual strcat. Aborts program on error */
+char *
+ltk_strcat_useful(const char *str1, const char *str2) {
+ int len1, len2;
+ char *ret;
+
+ len1 = strlen(str1);
+ len2 = strlen(str2);
+ ret = ltk_malloc(len1 + len2 + 1);
+ strcpy(ret, str1);
+ strcpy(ret + len1, str2);
+
+ return ret;
+}
+
+static void
+ltk_log_msg(const char *mode, const char *format, va_list args) {
+ char logtime[25]; /* FIXME: This should always be big enough, right? */
+ time_t clock;
+ struct tm *timeptr;
+
+ time(&clock);
+ timeptr = localtime(&clock);
+ strftime(logtime, 25, "%Y-%m-%d %H:%M:%S", timeptr);
+
+ fprintf(stderr, "%s ltk %s: ", logtime, mode);
+ vfprintf(stderr, format, args);
+}
+
+LTK_GEN_LOG_FUNCS(ltk, ltk_log_msg, ltk_deinit)
+
+int
+str_array_equal(const char *terminated, const char *array, size_t len) {
+ if (!strncmp(terminated, array, len)) {
+ /* this is kind of inefficient, but there's no way to know
+ otherwise if strncmp just stopped comparing after a '\0' */
+ return strlen(terminated) == len;
+ }
+ return 0;
+}
+
+size_t
+prev_utf8(char *text, size_t index) {
+ if (index == 0)
+ return 0;
+ size_t i = index - 1;
+ /* find valid utf8 char - this probably needs to be improved */
+ while (i > 0 && ((text[i] & 0xC0) == 0x80))
+ i--;
+ return i;
+}
+
+size_t
+next_utf8(char *text, size_t len, size_t index) {
+ if (index >= len)
+ return len;
+ size_t i = index + 1;
+ while (i < len && ((text[i] & 0xC0) == 0x80))
+ i++;
+ return i;
+}
+
+void
+ltk_assert_impl(const char *file, int line, const char *func, const char *failedexpr)
+{
+ (void)fprintf(stderr,
+ "assertion \"%s\" failed: file \"%s\", line %d, function \"%s\"\n",
+ failedexpr, file, line, func);
+ abort();
+ /* NOTREACHED */
+}
diff --git a/src/ltk/util.h b/src/ltk/util.h
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LTK_UTIL_H
+#define LTK_UTIL_H
+
+#include <stdarg.h>
+#include <stddef.h>
+
+long long ltk_strtonum(
+ const char *numstr, long long minval,
+ long long maxval, const char **errstrp
+);
+
+char *ltk_read_file(const char *filename, size_t *len_ret, char **errstr_ret);
+int ltk_write_file(const char *path, const char *data, size_t len, char **errstr_ret);
+int ltk_parse_run_cmd(const char *cmdtext, size_t len, const char *filename);
+void ltk_grow_string(char **str, int *alloc_size, int needed);
+char *ltk_setup_directory(const char *envname, const char *defaultname, int create);
+char *ltk_strcat_useful(const char *str1, const char *str2);
+
+/*
+ * Compare the nul-terminated string 'terminated' with the char
+ * array 'array' with length 'len'.
+ * Returns non-zero if they are equal, 0 otherwise.
+ */
+/* Note: this doesn't work if array contains '\0'. */
+int str_array_equal(const char *terminated, const char *array, size_t len);
+
+size_t prev_utf8(char *text, size_t index);
+size_t next_utf8(char *text, size_t len, size_t index);
+
+/* based on the assert found in OpenBSD */
+void ltk_assert_impl(const char *file, int line, const char *func, const char *failedexpr);
+#define ltk_assert(e) ((e) ? (void)0 : ltk_assert_impl(__FILE__, __LINE__, __func__, #e))
+
+#define LENGTH(X) (sizeof(X) / sizeof(X[0]))
+
+#define LTK_GEN_LOG_FUNC_PROTO(prefix) \
+void prefix##_warn_errno(const char *format, ...); \
+void prefix##_fatal_errno(const char *format, ...); \
+void prefix##_fatal(const char *format, ...); \
+void prefix##_warn(const char *format, ...);
+
+#define LTK_GEN_LOG_FUNCS(prefix, log_func, cleanup_func) \
+void \
+prefix##_warn(const char *format, ...) { \
+ va_list args; \
+ va_start(args, format); \
+ log_func("Warning", format, args); \
+ va_end(args); \
+} \
+ \
+void \
+prefix##_fatal(const char *format, ...) { \
+ va_list args; \
+ va_start(args, format); \
+ log_func("Fatal", format, args); \
+ va_end(args); \
+ cleanup_func(); \
+ \
+ exit(1); \
+} \
+ \
+void \
+prefix##_warn_errno(const char *format, ...) { \
+ va_list args; \
+ char *errstr = strerror(errno); \
+ va_start(args, format); \
+ log_func("Warning", format, args); \
+ va_end(args); \
+ prefix##_warn("system error: %s\n", errstr); \
+} \
+ \
+void \
+prefix##_fatal_errno(const char *format, ...) { \
+ va_list args; \
+ char *errstr = strerror(errno); \
+ va_start(args, format); \
+ log_func("Fatal", format, args); \
+ va_end(args); \
+ prefix##_fatal("system error: %s\n", errstr); \
+}
+
+LTK_GEN_LOG_FUNC_PROTO(ltk)
+
+#endif /* LTK_UTIL_H */
diff --git a/src/ltk/widget.c b/src/ltk/widget.c
@@ -0,0 +1,295 @@
+/*
+ * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <string.h>
+
+#include "rect.h"
+#include "widget.h"
+#include "window.h"
+#include "memory.h"
+#include "array.h"
+
+LTK_ARRAY_INIT_FUNC_DECL_STATIC(signal, ltk_signal_callback_info)
+LTK_ARRAY_INIT_IMPL_STATIC(signal, ltk_signal_callback_info)
+
+void
+ltk_fill_widget_defaults(ltk_widget *widget, ltk_window *window,
+ struct ltk_widget_vtable *vtable, int w, int h) {
+ widget->window = window;
+ widget->parent = NULL;
+
+ /* FIXME: possibly check that draw and destroy aren't NULL */
+ widget->vtable = vtable;
+
+ widget->state = LTK_NORMAL;
+ widget->row = 0;
+ widget->lrect.x = 0;
+ widget->lrect.y = 0;
+ widget->lrect.w = w;
+ widget->lrect.h = h;
+ widget->crect.x = 0;
+ widget->crect.y = 0;
+ widget->crect.w = w;
+ widget->crect.h = h;
+ widget->popup = 0;
+
+ widget->ideal_w = widget->ideal_h = 0;
+
+ widget->row = 0;
+ widget->column = 0;
+ widget->row_span = 0;
+ widget->column_span = 0;
+ widget->sticky = 0;
+ widget->dirty = 1;
+ widget->hidden = 0;
+ widget->vtable_copied = 0;
+ widget->signal_cbs = NULL;
+ /* FIXME: null other members! */
+}
+
+void
+ltk_widget_hide(ltk_widget *widget) {
+ /* FIXME: it may not make sense to call this here */
+ if (ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_HIDE, LTK_EMPTY_ARGLIST))
+ return;
+ if (widget->vtable->hide)
+ widget->vtable->hide(widget);
+ widget->hidden = 1;
+ /* remove hover state */
+ /* FIXME: this needs to call change_state but that might cause issues */
+ ltk_widget *hover = widget->window->hover_widget;
+ while (hover) {
+ if (hover == widget) {
+ widget->window->hover_widget->state &= ~LTK_HOVER;
+ widget->window->hover_widget = NULL;
+ break;
+ }
+ hover = hover->parent;
+ }
+ ltk_widget *pressed = widget->window->pressed_widget;
+ while (pressed) {
+ if (pressed == widget) {
+ widget->window->pressed_widget->state &= ~LTK_PRESSED;
+ widget->window->pressed_widget = NULL;
+ break;
+ }
+ pressed = pressed->parent;
+ }
+ ltk_widget *active = widget->window->active_widget;
+ /* if current active widget is child, set active widget to widget above in hierarchy */
+ int set_next = 0;
+ while (active) {
+ if (active == widget) {
+ set_next = 1;
+ /* FIXME: use config values for all_activatable */
+ } else if (set_next && (active->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
+ ltk_window_set_active_widget(active->window, active);
+ break;
+ }
+ active = active->parent;
+ }
+ if (set_next && !active)
+ ltk_window_set_active_widget(active->window, NULL);
+}
+
+/* FIXME: Maybe pass the new width as arg here?
+ That would make a bit more sense */
+/* FIXME: maybe give global and local position in event */
+void
+ltk_widget_resize(ltk_widget *widget) {
+ if (ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_RESIZE, LTK_EMPTY_ARGLIST))
+ return;
+ if (widget->vtable->resize)
+ widget->vtable->resize(widget);
+ widget->dirty = 1;
+}
+
+void
+ltk_widget_draw(ltk_widget *widget, ltk_surface *draw_surf, int x, int y, ltk_rect clip_rect) {
+ ltk_callback_arg args[] = {
+ LTK_MAKE_ARG_SURFACE(draw_surf),
+ LTK_MAKE_ARG_INT(x),
+ LTK_MAKE_ARG_INT(y),
+ LTK_MAKE_ARG_RECT(clip_rect)
+ };
+ if (ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_DRAW, (ltk_callback_arglist){args, LENGTH(args)}))
+ return;
+ if (widget->vtable->draw)
+ widget->vtable->draw(widget, draw_surf, x, y, clip_rect);
+}
+
+void
+ltk_widget_change_state(ltk_widget *widget, ltk_widget_state old_state) {
+ if (old_state == widget->state)
+ return;
+ ltk_callback_arg args[] = {LTK_MAKE_ARG_INT(old_state)};
+ if (ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_CHANGE_STATE, (ltk_callback_arglist){args, LENGTH(args)}))
+ return;
+ if (widget->vtable->change_state)
+ widget->vtable->change_state(widget, old_state);
+ if (widget->vtable->flags & LTK_NEEDS_REDRAW) {
+ widget->dirty = 1;
+ ltk_window_invalidate_widget_rect(widget->window, widget);
+ }
+}
+
+/* FIXME: document that it's really dangerous to overwrite remove_child or destroy */
+int
+ltk_widget_destroy(ltk_widget *widget, int shallow) {
+ ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_DESTROY, LTK_EMPTY_ARGLIST);
+ /* 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 invalid = 0;
+ if (widget->parent) {
+ if (widget->parent->vtable->remove_child)
+ invalid = widget->parent->vtable->remove_child(widget->parent, widget);
+ }
+ if (widget->vtable_copied) {
+ ltk_free(widget->vtable);
+ widget->vtable = NULL;
+ }
+ if (widget->signal_cbs) {
+ ltk_array_destroy(signal, widget->signal_cbs);
+ widget->signal_cbs = NULL;
+ }
+ widget->vtable->destroy(widget, shallow);
+
+ return invalid;
+}
+
+ltk_point
+ltk_widget_pos_to_global(ltk_widget *widget, int x, int y) {
+ ltk_widget *cur = widget;
+ while (cur) {
+ x += cur->lrect.x;
+ y += cur->lrect.y;
+ if (cur->popup)
+ break;
+ cur = cur->parent;
+ }
+ return (ltk_point){x, y};
+}
+
+ltk_point
+ltk_global_to_widget_pos(ltk_widget *widget, int x, int y) {
+ ltk_widget *cur = widget;
+ while (cur) {
+ x -= cur->lrect.x;
+ y -= cur->lrect.y;
+ if (cur->popup)
+ break;
+ cur = cur->parent;
+ }
+ return (ltk_point){x, y};
+}
+
+int
+ltk_widget_register_signal_handler(ltk_widget *widget, int type, ltk_signal_callback callback, ltk_callback_arg data) {
+ if ((type >= LTK_WIDGET_SIGNAL_INVALID) || type <= widget->vtable->invalid_signal)
+ return 1;
+ if (!widget->signal_cbs) {
+ widget->signal_cbs = ltk_array_create(signal, 1);
+ }
+ ltk_array_append_signal(widget->signal_cbs, (ltk_signal_callback_info){callback, data, type});
+ return 0;
+}
+
+int
+ltk_widget_emit_signal(ltk_widget *widget, int type, ltk_callback_arglist args) {
+ if (!widget->signal_cbs)
+ return 0;
+ int handled = 0;
+ for (size_t i = 0; i < ltk_array_len(widget->signal_cbs); i++) {
+ if (ltk_array_get(widget->signal_cbs, i).type == type) {
+ handled |= ltk_array_get(widget->signal_cbs, i).callback(widget, args, ltk_array_get(widget->signal_cbs, i).data);
+ }
+ }
+ return handled;
+}
+
+static int
+filter_by_type(ltk_signal_callback_info *info, void *data) {
+ return info->type == *(int *)data;
+}
+
+size_t
+ltk_widget_remove_signal_handler_by_type(ltk_widget *widget, int type) {
+ if (!widget->signal_cbs)
+ return 0;
+ return ltk_array_remove_if(signal, widget->signal_cbs, &filter_by_type, &type);
+}
+
+struct func_wrapper {
+ ltk_signal_callback callback;
+};
+
+static int
+filter_by_callback(ltk_signal_callback_info *info, void *data) {
+ return info->callback == ((struct func_wrapper *)data)->callback;
+}
+
+size_t
+ltk_widget_remove_signal_handler_by_callback(ltk_widget *widget, ltk_signal_callback callback) {
+ if (!widget->signal_cbs)
+ return 0;
+ /* callback can't be passed directly because ISO C forbids
+ conversion of object pointer to function pointer */
+ struct func_wrapper data = {callback};
+ return ltk_array_remove_if(signal, widget->signal_cbs, &filter_by_callback, &data);
+}
+
+struct delete_wrapper {
+ int (*filter_func)(ltk_signal_callback_info *, ltk_signal_callback_info *);
+ ltk_signal_callback_info *info;
+};
+
+static int
+filter_by_info(ltk_signal_callback_info *info, void *data) {
+ struct delete_wrapper *w = data;
+ return w->filter_func(info, w->info);
+}
+
+size_t
+ltk_widget_remove_signal_handler_by_info(
+ ltk_widget *widget,
+ int (*filter_func)(ltk_signal_callback_info *to_check, ltk_signal_callback_info *info),
+ ltk_signal_callback_info *info) {
+
+ if (!widget->signal_cbs)
+ return 0;
+ struct delete_wrapper data = {filter_func, info};
+ return ltk_array_remove_if(signal, widget->signal_cbs, &filter_by_info, &data);
+}
+
+void
+ltk_widget_remove_all_signal_handlers(ltk_widget *widget) {
+ if (!widget->signal_cbs)
+ return;
+ ltk_array_destroy(signal, widget->signal_cbs);
+ widget->signal_cbs = NULL;
+}
+
+int ltk_widget_register_type(void); /* FIXME */
+
+ltk_widget_vtable *
+ltk_widget_get_editable_vtable(ltk_widget *widget) {
+ if (!widget->vtable_copied) {
+ ltk_widget_vtable *vtable = ltk_malloc(sizeof(ltk_widget_vtable));
+ memcpy(vtable, widget->vtable, sizeof(ltk_widget_vtable));
+ widget->vtable_copied = 1;
+ }
+ return widget->vtable;
+}
diff --git a/src/ltk/widget.h b/src/ltk/widget.h
@@ -0,0 +1,346 @@
+/*
+ * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/* need to check what happens when registering signal for destroy, but then calling ltk_destroy_widget from
+that handler - maybe loop if container widget deletes children but they call parent->delete_child again */
+#ifndef LTK_WIDGET_H
+#define LTK_WIDGET_H
+
+/* FIXME: destroy signal for widgets (also window) */
+
+#include <stddef.h>
+#include "array.h"
+#include "event.h"
+#include "graphics.h"
+#include "rect.h"
+#include "util.h"
+
+struct ltk_widget;
+struct ltk_window;
+typedef struct ltk_widget ltk_widget;
+
+typedef enum {
+ LTK_WIDGET_UNKNOWN = 0,
+ LTK_WIDGET_ANY,
+ LTK_WIDGET_GRID,
+ LTK_WIDGET_BUTTON,
+ LTK_WIDGET_LABEL,
+ LTK_WIDGET_BOX,
+ LTK_WIDGET_MENU,
+ LTK_WIDGET_MENUENTRY,
+ LTK_WIDGET_ENTRY,
+ LTK_WIDGET_IMAGE,
+ LTK_WIDGET_WINDOW,
+ LTK_WIDGET_SCROLLBAR,
+ LTK_NUM_WIDGETS,
+} ltk_widget_type;
+
+typedef enum {
+ LTK_ACTIVATABLE_NORMAL = 1,
+ /* only activatable when "all-activatable"
+ is set to true in the config */
+ LTK_ACTIVATABLE_SPECIAL = 2,
+ LTK_ACTIVATABLE_ALWAYS = 1|2,
+ /* FIXME: redundant or needs better name - is implied by entries in vtable
+ - if there are widgets that have keyboard functions in the vtable but
+ shouldn't have this set, then it's a bad name */
+ LTK_NEEDS_KEYBOARD = 4,
+ LTK_NEEDS_REDRAW = 8,
+ LTK_HOVER_IS_ACTIVE = 16,
+} ltk_widget_flags;
+
+/* FIXME: "sticky" is maybe not the correct name anymore */
+typedef enum {
+ LTK_STICKY_NONE = 0,
+ LTK_STICKY_LEFT = 1 << 0,
+ LTK_STICKY_RIGHT = 1 << 1,
+ LTK_STICKY_TOP = 1 << 2,
+ LTK_STICKY_BOTTOM = 1 << 3,
+ LTK_STICKY_SHRINK_WIDTH = 1 << 4,
+ LTK_STICKY_SHRINK_HEIGHT = 1 << 5,
+ LTK_STICKY_PRESERVE_ASPECT_RATIO = 1 << 6,
+} ltk_sticky_mask;
+
+typedef enum {
+ LTK_VERTICAL,
+ LTK_HORIZONTAL
+} ltk_orientation;
+
+typedef enum {
+ LTK_NORMAL = 0,
+ LTK_HOVER = 1,
+ LTK_PRESSED = 2,
+ LTK_ACTIVE = 4,
+ LTK_HOVERACTIVE = 1 | 4,
+ LTK_FOCUSED = 8,
+ LTK_DISABLED = 16,
+} ltk_widget_state;
+
+/* FIXME: need "ltk_register_type" just to get unique integer for type checking */
+
+typedef struct {
+ union {
+ int i;
+ size_t sz;
+ char c;
+ ltk_widget *widget;
+ char *str;
+ const char *cstr;
+ ltk_key_event *key_event;
+ ltk_button_event *button_event;
+ ltk_scroll_event *scroll_event;
+ ltk_motion_event *motion_event;
+ ltk_surface *surface;
+ void *v;
+ /* FIXME: maybe rewrite the functions to take
+ pointers instead so this doesn't increase
+ the size of the union (thereby increasing
+ the size of every arg in the arglist) */
+ ltk_rect rect;
+ } arg;
+ enum {
+ LTK_TYPE_INT,
+ LTK_TYPE_SIZE_T,
+ LTK_TYPE_CHAR,
+ LTK_TYPE_WIDGET,
+ LTK_TYPE_STRING,
+ LTK_TYPE_CONST_STRING,
+ LTK_TYPE_KEY_EVENT,
+ LTK_TYPE_BUTTON_EVENT,
+ LTK_TYPE_SCROLL_EVENT,
+ LTK_TYPE_MOTION_EVENT,
+ LTK_TYPE_SURFACE,
+ LTK_TYPE_RECT,
+ LTK_TYPE_VOID,
+ LTK_TYPE_VOIDP,
+ } type;
+} ltk_callback_arg;
+
+/* FIXME: STRING should be CHARP for consistency */
+#define LTK_MAKE_ARG_INT(data) ((ltk_callback_arg){.type = LTK_TYPE_INT, .arg = {.i = (data)}})
+#define LTK_MAKE_ARG_SIZE_T(data) ((ltk_callback_arg){.type = LTK_TYPE_SIZE_T, .arg = {.sz = (data)}})
+#define LTK_MAKE_ARG_CHAR(data) ((ltk_callback_arg){.type = LTK_TYPE_CHAR, .arg = {.c = (data)}})
+#define LTK_MAKE_ARG_WIDGET(data) ((ltk_callback_arg){.type = LTK_TYPE_WIDGET, .arg = {.widget = (data)}})
+#define LTK_MAKE_ARG_STRING(data) ((ltk_callback_arg){.type = LTK_TYPE_STRING, .arg = {.str = (data)}})
+#define LTK_MAKE_ARG_CONST_STRING(data) ((ltk_callback_arg){.type = LTK_TYPE_CONST_STRING, .arg = {.cstr = (data)}})
+#define LTK_MAKE_ARG_KEY_EVENT(data) ((ltk_callback_arg){.type = LTK_TYPE_KEY_EVENT, .arg = {.key_event = (data)}})
+#define LTK_MAKE_ARG_BUTTON_EVENT(data) ((ltk_callback_arg){.type = LTK_TYPE_BUTTON_EVENT, .arg = {.button_event = (data)}})
+#define LTK_MAKE_ARG_SCROLL_EVENT(data) ((ltk_callback_arg){.type = LTK_TYPE_SCROLL_EVENT, .arg = {.scroll_event = (data)}})
+#define LTK_MAKE_ARG_MOTION_EVENT(data) ((ltk_callback_arg){.type = LTK_TYPE_MOTION_EVENT, .arg = {.motion_event = (data)}})
+#define LTK_MAKE_ARG_SURFACE(data) ((ltk_callback_arg){.type = LTK_TYPE_SURFACE, .arg = {.surface = (data)}})
+#define LTK_MAKE_ARG_RECT(data) ((ltk_callback_arg){.type = LTK_TYPE_RECT, .arg = {.rect = (data)}})
+#define LTK_MAKE_ARG_VOIDP(data) ((ltk_callback_arg){.type = LTK_TYPE_VOIDP, .arg = {.v = (data)}})
+
+#define LTK_ARG_VOID ((ltk_callback_arg){.type = LTK_TYPE_VOID})
+
+#define LTK_CAST_ARG_INT(carg) (ltk_assert(carg.type == LTK_TYPE_INT), carg.arg.i)
+#define LTK_CAST_ARG_SIZE_T(carg) (ltk_assert(carg.type == LTK_TYPE_SIZE_T), carg.arg.sz)
+#define LTK_CAST_ARG_CHAR(carg) (ltk_assert(carg.type == LTK_TYPE_CHAR), carg.arg.c)
+#define LTK_CAST_ARG_WIDGET(carg) (ltk_assert(carg.type == LTK_TYPE_WIDGET), carg.arg.widget)
+#define LTK_CAST_ARG_STRING(carg) (ltk_assert(carg.type == LTK_TYPE_STRING), carg.arg.str)
+#define LTK_CAST_ARG_CONST_STRING(carg) (ltk_assert(carg.type == LTK_TYPE_CONST_STRING), carg.arg.cstr)
+#define LTK_CAST_ARG_KEY_EVENT(carg) (ltk_assert(carg.type == LTK_TYPE_KEY_EVENT), carg.arg.key_event)
+#define LTK_CAST_ARG_BUTTON_EVENT(carg) (ltk_assert(carg.type == LTK_TYPE_BUTTON_EVENT), carg.arg.button_event)
+#define LTK_CAST_ARG_SCROLL_EVENT(carg) (ltk_assert(carg.type == LTK_TYPE_SCROLL_EVENT), carg.arg.scroll_event)
+#define LTK_CAST_ARG_MOTION_EVENT(carg) (ltk_assert(carg.type == LTK_TYPE_MOTION_EVENT), carg.arg.motion_event)
+#define LTK_CAST_ARG_SURFACE(carg) (ltk_assert(carg.type == LTK_TYPE_SURFACE), carg.arg.surface)
+#define LTK_CAST_ARG_RECT(carg) (ltk_assert(carg.type == LTK_TYPE_RECT), carg.arg.rect)
+#define LTK_CAST_ARG_VOIDP(carg) (ltk_assert(carg.type == LTK_TYPE_VOIDP), carg.arg.v)
+
+#define LTK_GET_ARG_INT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_INT(cargs.args[i]))
+#define LTK_GET_ARG_SIZE_T(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_SIZE_T(cargs.args[i]))
+#define LTK_GET_ARG_CHAR(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_CHAR(cargs.args[i]))
+#define LTK_GET_ARG_WIDGET(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_WIDGET(cargs.args[i]))
+#define LTK_GET_ARG_STRING(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_STRING(cargs.args[i]))
+#define LTK_GET_ARG_CONST_STRING(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_CONST_STRING(cargs.args[i]))
+#define LTK_GET_ARG_KEY_EVENT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_KEY_EVENT(cargs.args[i]))
+#define LTK_GET_ARG_BUTTON_EVENT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_BUTTON_EVENT(cargs.args[i]))
+#define LTK_GET_ARG_SCROLL_EVENT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_SCROLL_EVENT(cargs.args[i]))
+#define LTK_GET_ARG_MOTION_EVENT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_MOTION_EVENT(cargs.args[i]))
+#define LTK_GET_ARG_SURFACE(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_SURFACE(cargs.args[i]))
+#define LTK_GET_ARG_RECT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_RECT(cargs.args[i]))
+
+#define LTK_CAST_WIDGET(w) (&(w)->widget)
+#define LTK_CAST_WINDOW(w) (ltk_assert(w->vtable->type == LTK_WIDGET_WINDOW), (ltk_window *)(w))
+#define LTK_CAST_LABEL(w) (ltk_assert(w->vtable->type == LTK_WIDGET_LABEL), (ltk_label *)(w))
+#define LTK_CAST_BUTTON(w) (ltk_assert(w->vtable->type == LTK_WIDGET_BUTTON), (ltk_button *)(w))
+#define LTK_CAST_GRID(w) (ltk_assert(w->vtable->type == LTK_WIDGET_GRID), (ltk_grid *)(w))
+#define LTK_CAST_IMAGE_WIDGET(w) (ltk_assert(w->vtable->type == LTK_WIDGET_IMAGE), (ltk_image_widget *)(w))
+#define LTK_CAST_ENTRY(w) (ltk_assert(w->vtable->type == LTK_WIDGET_ENTRY), (ltk_entry *)(w))
+#define LTK_CAST_MENU(w) (ltk_assert(w->vtable->type == LTK_WIDGET_MENU), (ltk_menu *)(w))
+#define LTK_CAST_MENUENTRY(w) (ltk_assert(w->vtable->type == LTK_WIDGET_MENUENTRY), (ltk_menuentry *)(w))
+#define LTK_CAST_SCROLLBAR(w) (ltk_assert(w->vtable->type == LTK_WIDGET_SCROLLBAR), (ltk_scrollbar *)(w))
+#define LTK_CAST_BOX(w) (ltk_assert(w->vtable->type == LTK_WIDGET_BOX), (ltk_box *)(w))
+
+/* FIXME: a bit weird because window never gets some of these signals */
+#define LTK_WIDGET_SIGNAL_KEY_PRESS 1
+#define LTK_WIDGET_SIGNAL_KEY_RELEASE 2
+#define LTK_WIDGET_SIGNAL_MOUSE_PRESS 3
+#define LTK_WIDGET_SIGNAL_MOUSE_RELEASE 4
+#define LTK_WIDGET_SIGNAL_MOUSE_SCROLL 5
+#define LTK_WIDGET_SIGNAL_MOTION_NOTIFY 6
+#define LTK_WIDGET_SIGNAL_MOUSE_ENTER 7
+#define LTK_WIDGET_SIGNAL_MOUSE_LEAVE 8
+#define LTK_WIDGET_SIGNAL_PRESS 9
+#define LTK_WIDGET_SIGNAL_RELEASE 10
+#define LTK_WIDGET_SIGNAL_RESIZE 11
+#define LTK_WIDGET_SIGNAL_HIDE 12
+#define LTK_WIDGET_SIGNAL_DRAW 13
+#define LTK_WIDGET_SIGNAL_CHANGE_STATE 14
+/* The return value for this is ignored, i.e.
+ the widget destroy function is always called.
+ Also, it doesn't receive the 'shallow' argument
+ that the widget method receives. */
+#define LTK_WIDGET_SIGNAL_DESTROY 15
+#define LTK_WIDGET_SIGNAL_INVALID 16
+
+typedef struct {
+ ltk_callback_arg *args;
+ size_t num;
+} ltk_callback_arglist;
+
+#define LTK_EMPTY_ARGLIST ((ltk_callback_arglist){NULL, 0})
+
+typedef int (*ltk_signal_callback)(ltk_widget *widget, ltk_callback_arglist args, ltk_callback_arg data);
+typedef struct {
+ ltk_signal_callback callback;
+ ltk_callback_arg data;
+ int type;
+} ltk_signal_callback_info;
+
+int ltk_widget_register_signal_handler(ltk_widget *widget, int type, ltk_signal_callback callback, ltk_callback_arg data);
+int ltk_widget_emit_signal(ltk_widget *widget, int type, ltk_callback_arglist args);
+size_t ltk_widget_remove_signal_handler_by_type(ltk_widget *widget, int type);
+size_t ltk_widget_remove_signal_handler_by_callback(ltk_widget *widget, ltk_signal_callback callback);
+size_t ltk_widget_remove_signal_handler_by_info(
+ ltk_widget *widget,
+ int (*filter_func)(ltk_signal_callback_info *to_check, ltk_signal_callback_info *info),
+ ltk_signal_callback_info *info
+);
+void ltk_widget_remove_all_signal_handlers(ltk_widget *widget);
+int ltk_widget_register_type(void);
+
+LTK_ARRAY_INIT_STRUCT_DECL(signal, ltk_signal_callback_info)
+
+struct ltk_widget {
+ struct ltk_window *window;
+ struct ltk_widget *parent;
+
+ struct ltk_widget_vtable *vtable;
+
+ /* FIXME: crect and lrect are a bit weird still */
+ /* FIXME: especially the relative positioning is really weird for
+ popups because they're positioned globally but still have a
+ parent-child relationship - weird things can probably happen */
+ /* both rects relative to parent (except for popups) */
+ /* collision rect is only part that is actually shown and used for
+ collision with mouse (but may still not be drawn if hidden by
+ something else) - e.g. in a box with scrolling, a widget that
+ is half cut off by a side of the box will have the logical rect
+ going past the side of the box, but the collision rect will only
+ be the part inside the box */
+ ltk_rect crect; /* collision rect */
+ ltk_rect lrect; /* logical rect */
+ unsigned int ideal_w;
+ unsigned int ideal_h;
+
+ /* maybe mask to determine quickly which callbacks are included?
+ default signals only allowed to have one callback? */
+ /* could maybe have just uint32_t mask so at least the lower signals
+ can be easily checked */
+ ltk_array(signal) *signal_cbs;
+
+ ltk_widget_state state;
+ /* FIXME: store this in grid/box - for row_span, column_span the other cells could be marked with "not top left cell of widget" so they can be skipped */
+ ltk_sticky_mask sticky;
+ unsigned short row;
+ unsigned short column;
+ unsigned short row_span;
+ unsigned short column_span;
+ /* ALSO NEED SIGNALS LIKE ADD-TEXT (called *before* text is inserted to check validity) - these would need argument
+ FIGURE OUT HOW TO DO KEY MAPPINGS - should reuse parts of builtin mapping handling
+ -> maybe something like tk
+ -> or maybe just say everyone needs to override event handler? but makes simple stuff more difficult
+ -> also need "global" mappings/key event handler for global shortcuts */
+ /* needed to properly handle handle local coordinates since
+ popups are positioned globally instead of locally */
+ char popup;
+ char dirty;
+ char hidden;
+ char vtable_copied;
+};
+
+typedef struct ltk_widget_vtable {
+ int (*key_press)(struct ltk_widget *, ltk_key_event *);
+ int (*key_release)(struct ltk_widget *, ltk_key_event *);
+ /* press/release also receive double/triple-click/release */
+ int (*mouse_press)(struct ltk_widget *, ltk_button_event *);
+ int (*mouse_release)(struct ltk_widget *, ltk_button_event *);
+ int (*mouse_scroll)(struct ltk_widget *, ltk_scroll_event *);
+ int (*motion_notify)(struct ltk_widget *, ltk_motion_event *);
+ int (*mouse_leave)(struct ltk_widget *, ltk_motion_event *);
+ int (*mouse_enter)(struct ltk_widget *, ltk_motion_event *);
+ int (*press)(struct ltk_widget *);
+ int (*release)(struct ltk_widget *);
+ void (*cmd_return)(struct ltk_widget *self, char *text, size_t len);
+
+ void (*resize)(struct ltk_widget *);
+ void (*hide)(struct ltk_widget *);
+ /* draw_surface: surface to draw it on
+ x, y: position of logical rectangle on surface
+ clip: clipping rectangle, relative to logical rectangle */
+ void (*draw)(struct ltk_widget *self, ltk_surface *draw_surface, int x, int y, ltk_rect clip);
+ void (*change_state)(struct ltk_widget *, ltk_widget_state);
+ void (*destroy)(struct ltk_widget *, int);
+
+ /* rect is in self's coordinate system */
+ struct ltk_widget *(*nearest_child)(struct ltk_widget *self, ltk_rect rect);
+ struct ltk_widget *(*nearest_child_left)(struct ltk_widget *self, ltk_widget *widget);
+ struct ltk_widget *(*nearest_child_right)(struct ltk_widget *self, ltk_widget *widget);
+ struct ltk_widget *(*nearest_child_above)(struct ltk_widget *self, ltk_widget *widget);
+ struct ltk_widget *(*nearest_child_below)(struct ltk_widget *self, ltk_widget *widget);
+ struct ltk_widget *(*next_child)(struct ltk_widget *self, ltk_widget *child);
+ struct ltk_widget *(*prev_child)(struct ltk_widget *self, ltk_widget *child);
+ struct ltk_widget *(*first_child)(struct ltk_widget *self);
+ struct ltk_widget *(*last_child)(struct ltk_widget *self);
+
+ void (*child_size_change)(struct ltk_widget *, struct ltk_widget *);
+ int (*remove_child)(struct ltk_widget *, struct ltk_widget *);
+ /* x and y relative to widget's lrect! */
+ struct ltk_widget *(*get_child_at_pos)(struct ltk_widget *, int x, int y);
+ /* r is in self's coordinate system */
+ void (*ensure_rect_shown)(struct ltk_widget *self, ltk_rect r);
+
+ ltk_widget_type type;
+ ltk_widget_flags flags;
+ int invalid_signal;
+} ltk_widget_vtable;
+
+void ltk_widget_hide(ltk_widget *widget);
+int ltk_widget_destroy(ltk_widget *widget, int shallow);
+void ltk_fill_widget_defaults(
+ ltk_widget *widget, struct ltk_window *window,
+ struct ltk_widget_vtable *vtable, int w, int h
+);
+void ltk_widget_change_state(ltk_widget *widget, ltk_widget_state old_state);
+void ltk_widget_resize(ltk_widget *widget);
+void ltk_widget_draw(ltk_widget *widget, ltk_surface *draw_surf, int x, int y, ltk_rect clip_rect);
+ltk_point ltk_widget_pos_to_global(ltk_widget *widget, int x, int y);
+ltk_point ltk_global_to_widget_pos(ltk_widget *widget, int x, int y);
+
+ltk_widget_vtable *ltk_widget_get_editable_vtable(ltk_widget *widget);
+
+#endif /* LTK_WIDGET_H */
diff --git a/src/ltk/window.c b/src/ltk/window.c
@@ -0,0 +1,1319 @@
+/* FIXME: signal handling is really ugly and inconsistent at the moment */
+/*
+ * Copyright (c) 2020-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "ltk.h"
+#include "util.h"
+#include "keys.h"
+#include "array.h"
+#include "theme.h"
+#include "widget.h"
+#include "window.h"
+#include "memory.h"
+#include "eventdefs.h"
+
+#define MAX_WINDOW_FONT_SIZE 200
+
+static void gen_widget_stack(ltk_widget *bottom);
+static ltk_widget *get_hover_popup(ltk_window *window, int x, int y);
+static int is_parent(ltk_widget *parent, ltk_widget *child);
+static ltk_widget *get_widget_under_pointer(ltk_widget *widget, int x, int y, int *local_x_ret, int *local_y_ret);
+
+static int ltk_window_key_press_event(ltk_widget *self, ltk_key_event *event);
+static int ltk_window_key_release_event(ltk_widget *self, ltk_key_event *event);
+static int ltk_window_mouse_press_event(ltk_widget *self, ltk_button_event *event);
+static int ltk_window_mouse_scroll_event(ltk_widget *self, ltk_scroll_event *event);
+static int ltk_window_mouse_release_event(ltk_widget *self, ltk_button_event *event);
+static int ltk_window_motion_notify_event(ltk_widget *self, ltk_motion_event *event);
+static void ltk_window_redraw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip);
+
+/* FIXME: actually use this properly */
+static struct ltk_widget_vtable vtable = {
+ .key_press = <k_window_key_press_event,
+ .key_release = <k_window_key_release_event,
+ .mouse_press = <k_window_mouse_press_event,
+ .mouse_release = <k_window_mouse_release_event,
+ .release = NULL,
+ .motion_notify = <k_window_motion_notify_event,
+ .mouse_leave = NULL,
+ .mouse_enter = NULL,
+ .change_state = NULL,
+ .get_child_at_pos = NULL,
+ .resize = NULL,
+ .hide = NULL,
+ .draw = <k_window_redraw,
+ .destroy = <k_window_destroy,
+ .child_size_change = NULL,
+ .remove_child = NULL,
+ .type = LTK_WIDGET_WINDOW,
+ .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS,
+ .invalid_signal = LTK_WINDOW_SIGNAL_INVALID,
+};
+
+static int cb_focus_active(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_unfocus_active(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_move_prev(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_move_next(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_move_left(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_move_right(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_move_up(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_move_down(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_set_pressed(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_unset_pressed(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_remove_popups(ltk_window *window, ltk_key_event *event, int handled);
+
+struct key_cb {
+ char *func_name;
+ int (*callback)(ltk_window *, ltk_key_event *, int handled);
+};
+
+static struct key_cb cb_map[] = {
+ {"focus-active", &cb_focus_active},
+ {"move-down", &cb_move_down},
+ {"move-left", &cb_move_left},
+ {"move-next", &cb_move_next},
+ {"move-prev", &cb_move_prev},
+ {"move-right", &cb_move_right},
+ {"move-up", &cb_move_up},
+ {"remove-popups", &cb_remove_popups},
+ {"set-pressed", &cb_set_pressed},
+ {"unfocus-active", &cb_unfocus_active},
+ {"unset-pressed", &cb_unset_pressed},
+};
+
+struct keypress_cfg {
+ ltk_keypress_binding b;
+ struct key_cb cb;
+};
+
+struct keyrelease_cfg {
+ ltk_keyrelease_binding b;
+ struct key_cb cb;
+};
+
+LTK_ARRAY_INIT_DECL_STATIC(keypress, struct keypress_cfg)
+LTK_ARRAY_INIT_IMPL_STATIC(keypress, struct keypress_cfg)
+LTK_ARRAY_INIT_DECL_STATIC(keyrelease, struct keyrelease_cfg)
+LTK_ARRAY_INIT_IMPL_STATIC(keyrelease, struct keyrelease_cfg)
+
+static ltk_array(keypress) *keypresses = NULL;
+static ltk_array(keyrelease) *keyreleases = NULL;
+
+GEN_CB_MAP_HELPERS(cb_map, struct key_cb, func_name)
+
+/* needed for passing keyboard events down the hierarchy */
+static ltk_widget **widget_stack = NULL;
+static size_t widget_stack_alloc = 0;
+static size_t widget_stack_len = 0;
+
+static ltk_window_theme theme;
+static ltk_theme_parseinfo theme_parseinfo[] = {
+ {"bg", THEME_COLOR, {.color = &theme.bg}, {.color = "#000000"}, 0, 0, 0},
+ {"fg", THEME_COLOR, {.color = &theme.fg}, {.color = "#FFFFFF"}, 0, 0, 0},
+ {"font", THEME_STRING, {.str = &theme.font}, {.str = "Monospace"}, 0, 0, 0},
+ {"font-size", THEME_INT, {.i = &theme.font_size}, {.i = 15}, 0, MAX_WINDOW_FONT_SIZE, 0},
+};
+static int theme_parseinfo_sorted = 0;
+
+int
+ltk_window_fill_theme_defaults(ltk_renderdata *data) {
+ return ltk_theme_fill_defaults(data, "window", theme_parseinfo, LENGTH(theme_parseinfo));
+}
+
+int
+ltk_window_ini_handler(ltk_renderdata *data, const char *prop, const char *value) {
+ return ltk_theme_handle_value(data, "window", prop, value, theme_parseinfo, LENGTH(theme_parseinfo), &theme_parseinfo_sorted);
+}
+
+void
+ltk_window_uninitialize_theme(ltk_renderdata *data) {
+ ltk_theme_uninitialize(data, theme_parseinfo, LENGTH(theme_parseinfo));
+}
+
+/* FIXME: maybe ltk_fatal if ltk not initialized? */
+ltk_window_theme *
+ltk_window_get_theme(void) {
+ return &theme;
+}
+
+/* FIXME: most of this is duplicated code */
+
+int
+ltk_window_register_keypress(const char *func_name, size_t func_len, ltk_keypress_binding b) {
+ if (!keypresses)
+ keypresses = ltk_array_create(keypress, 1);
+ struct key_cb *cb = cb_map_get_entry(func_name, func_len);
+ if (!cb)
+ return 1;
+ struct keypress_cfg cfg = {b, *cb};
+ ltk_array_append(keypress, keypresses, cfg);
+ return 0;
+}
+
+int
+ltk_window_register_keyrelease(const char *func_name, size_t func_len, ltk_keyrelease_binding b) {
+ if (!keyreleases)
+ keyreleases = ltk_array_create(keyrelease, 1);
+ struct key_cb *cb = cb_map_get_entry(func_name, func_len);
+ if (!cb)
+ return 1;
+ struct keyrelease_cfg cfg = {b, *cb};
+ ltk_array_append(keyrelease, keyreleases, cfg);
+ return 0;
+}
+
+static void
+destroy_keypress_cfg(struct keypress_cfg cfg) {
+ ltk_keypress_binding_destroy(cfg.b);
+}
+
+void
+ltk_window_cleanup(void) {
+ ltk_array_destroy_deep(keypress, keypresses, &destroy_keypress_cfg);
+ ltk_array_destroy(keyrelease, keyreleases);
+ free(widget_stack);
+ keypresses = NULL;
+ keyreleases = NULL;
+ widget_stack = NULL;
+}
+
+static void
+ensure_active_widget_shown(ltk_window *window) {
+ ltk_widget *widget = window->active_widget;
+ if (!widget)
+ return;
+ ltk_rect r = widget->lrect;
+ while (widget->parent) {
+ if (widget->parent->vtable->ensure_rect_shown)
+ widget->parent->vtable->ensure_rect_shown(widget->parent, r);
+ widget = widget->parent;
+ r.x += widget->lrect.x;
+ r.y += widget->lrect.y;
+ /* FIXME: this currently just aborts if a widget is positioned
+ absolutely because I'm not sure what the best action would
+ be in that case */
+ if (widget->popup)
+ break;
+ }
+ ltk_window_invalidate_widget_rect(window, widget);
+}
+
+/* FIXME: should keyrelease events be ignored if the corresponding keypress event
+ was consumed for movement? */
+/* FIXME: check if there's any weirdness when combining return and mouse press */
+/* FIXME: maybe it doesn't really make sense to make e.g. text entry pressed when enter is pressed? */
+/* FIXME: implement key binding flag to run before widget handler is called */
+static int
+ltk_window_key_press_event(ltk_widget *self, ltk_key_event *event) {
+ ltk_window *window = LTK_CAST_WINDOW(self);
+ int handled = 0;
+ ltk_callback_arg args[] = {LTK_MAKE_ARG_KEY_EVENT(event)};
+ if (window->active_widget && (window->active_widget->state & LTK_FOCUSED)) {
+ gen_widget_stack(window->active_widget);
+ for (size_t i = widget_stack_len; i-- > 0 && !handled;) {
+ if (ltk_widget_emit_signal(widget_stack[i], LTK_WIDGET_SIGNAL_KEY_PRESS, (ltk_callback_arglist){args, LENGTH(args)}) ||
+ (widget_stack[i]->vtable->key_press && widget_stack[i]->vtable->key_press(widget_stack[i], event))) {
+ handled = 1;
+ break;
+ }
+ }
+ }
+ if (!keypresses)
+ return 1;
+ ltk_keypress_binding *b = NULL;
+ for (size_t i = 0; i < ltk_array_len(keypresses); i++) {
+ b = <k_array_get(keypresses, i).b;
+ if (b->mods != event->modmask || (!(b->flags & LTK_KEY_BINDING_RUN_ALWAYS) && handled)) {
+ continue;
+ } else if (b->text) {
+ if (event->mapped && !strcmp(b->text, event->mapped))
+ handled |= ltk_array_get(keypresses, i).cb.callback(window, event, handled);
+ } else if (b->rawtext) {
+ if (event->text && !strcmp(b->text, event->text))
+ handled |= ltk_array_get(keypresses, i).cb.callback(window, event, handled);
+ } else if (b->sym != LTK_KEY_NONE) {
+ if (event->sym == b->sym)
+ handled |= ltk_array_get(keypresses, i).cb.callback(window, event, handled);
+ }
+ }
+ return 1;
+}
+
+/* FIXME: need to actually check if any of parent widgets are focused and still pass to them even if bottom widget not focused? */
+static int
+ltk_window_key_release_event(ltk_widget *self, ltk_key_event *event) {
+ ltk_window *window = LTK_CAST_WINDOW(self);
+ int handled = 0;
+ ltk_callback_arg args[] = {LTK_MAKE_ARG_KEY_EVENT(event)};
+ if (window->active_widget && (window->active_widget->state & LTK_FOCUSED)) {
+ gen_widget_stack(window->active_widget);
+ for (size_t i = widget_stack_len; i-- > 0 && !handled;) {
+ if (ltk_widget_emit_signal(widget_stack[i], LTK_WIDGET_SIGNAL_KEY_RELEASE, (ltk_callback_arglist){args, LENGTH(args)}) ||
+ (widget_stack[i]->vtable->key_release && widget_stack[i]->vtable->key_release(widget_stack[i], event))) {
+ handled = 1;
+ break;
+ }
+ }
+ }
+ if (!keyreleases)
+ return 1;
+ ltk_keyrelease_binding *b = NULL;
+ for (size_t i = 0; i < ltk_array_len(keyreleases); i++) {
+ b = <k_array_get(keyreleases, i).b;
+ if (b->mods != event->modmask || (!(b->flags & LTK_KEY_BINDING_RUN_ALWAYS) && handled)) {
+ continue;
+ } else if (b->sym != LTK_KEY_NONE && event->sym == b->sym) {
+ handled |= ltk_array_get(keyreleases, i).cb.callback(window, event, handled);
+ }
+ }
+ return 1;
+}
+
+/* FIXME: This is still weird. */
+static int
+ltk_window_mouse_press_event(ltk_widget *self, ltk_button_event *event) {
+ ltk_window *window = LTK_CAST_WINDOW(self);
+ ltk_widget *widget = get_hover_popup(window, event->x, event->y);
+ int check_hide = 0;
+ if (!widget) {
+ widget = window->root_widget;
+ check_hide = 1;
+ }
+ if (!widget) {
+ ltk_window_unregister_all_popups(window);
+ return 1;
+ }
+ int orig_x = event->x, orig_y = event->y;
+ ltk_widget *cur_widget = get_widget_under_pointer(widget, event->x, event->y, &event->x, &event->y);
+ /* FIXME: need to add more flags for more fine-grained control
+ -> also, should the widget still get mouse_press even if state doesn't change? */
+ /* FIXME: doesn't work with e.g. disabled menu entries */
+ if (!(cur_widget->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
+ ltk_window_unregister_all_popups(window);
+ }
+
+ /* FIXME: this doesn't make much sense if the popups aren't a
+ hierarchy (right now, they're just menus, so that's always
+ a hierarchy */
+ /* don't hide popups if they are children of the now pressed widget */
+ if (check_hide && !(window->popups_num > 0 && is_parent(cur_widget, window->popups[0])))
+ ltk_window_unregister_all_popups(window);
+
+ /* FIXME: popups don't always have their children geometrically contained within parents,
+ so this won't work properly in all cases */
+ int first = 1;
+ ltk_callback_arg args[] = {LTK_MAKE_ARG_BUTTON_EVENT(event)};
+ while (cur_widget) {
+ int handled = 0;
+ ltk_point local = ltk_global_to_widget_pos(cur_widget, orig_x, orig_y);
+ event->x = local.x;
+ event->y = local.y;
+ if (cur_widget->state != LTK_DISABLED) {
+ /* FIXME: figure out whether this makes sense - currently, all widgets (unless disabled)
+ get mouse press, but they are only set to pressed if they are activatable */
+ handled = ltk_widget_emit_signal(cur_widget, LTK_WIDGET_SIGNAL_MOUSE_PRESS, (ltk_callback_arglist){args, LENGTH(args)});
+ if (!handled && cur_widget->vtable->mouse_press)
+ handled = cur_widget->vtable->mouse_press(cur_widget, event);
+ /* set first non-disabled widget to pressed widget */
+ /* FIXME: use config values for all_activatable */
+ if (first && event->button == LTK_BUTTONL && event->type == LTK_BUTTONPRESS_EVENT && (cur_widget->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
+ ltk_window_set_pressed_widget(window, cur_widget, 0);
+ first = 0;
+ }
+ }
+ if (!handled)
+ cur_widget = cur_widget->parent;
+ else
+ break;
+ }
+ return 1;
+}
+
+static int
+ltk_window_mouse_scroll_event(ltk_widget *self, ltk_scroll_event *event) {
+ ltk_window *window = LTK_CAST_WINDOW(self);
+ /* FIXME: should it first be sent to pressed widget? */
+ ltk_widget *widget = get_hover_popup(window, event->x, event->y);
+ if (!widget)
+ widget = window->root_widget;
+ if (!widget)
+ return 1;
+ int orig_x = event->x, orig_y = event->y;
+ ltk_widget *cur_widget = get_widget_under_pointer(widget, event->x, event->y, &event->x, &event->y);
+ ltk_callback_arg args[] = {LTK_MAKE_ARG_SCROLL_EVENT(event)};
+ /* FIXME: same issue with popups like in mouse_press above */
+ while (cur_widget) {
+ int handled = 0;
+ ltk_point local = ltk_global_to_widget_pos(cur_widget, orig_x, orig_y);
+ event->x = local.x;
+ event->y = local.y;
+ if (cur_widget->state != LTK_DISABLED) {
+ /* FIXME: see function above
+ if (queue_scroll_event(cur_widget, event->x, event->y, event->dx, event->dy))
+ handled = 1; */
+ handled = ltk_widget_emit_signal(cur_widget, LTK_WIDGET_SIGNAL_MOUSE_SCROLL, (ltk_callback_arglist){args, LENGTH(args)});
+ if (!handled && cur_widget->vtable->mouse_scroll)
+ handled = cur_widget->vtable->mouse_scroll(cur_widget, event);
+ }
+ if (!handled)
+ cur_widget = cur_widget->parent;
+ else
+ break;
+ }
+ return 1;
+}
+
+void
+ltk_window_fake_motion_event(ltk_window *window, int x, int y) {
+ ltk_widget *self = LTK_CAST_WIDGET(window);
+ ltk_motion_event e = {.type = LTK_MOTION_EVENT, .x = x, .y = y};
+ ltk_callback_arg args[] = {LTK_MAKE_ARG_MOTION_EVENT(&e)};
+ if (!ltk_widget_emit_signal(self, LTK_WIDGET_SIGNAL_MOTION_NOTIFY, (ltk_callback_arglist){args, LENGTH(args)})) {
+ self->vtable->motion_notify(LTK_CAST_WIDGET(window), &e);
+ }
+}
+
+static int
+ltk_window_mouse_release_event(ltk_widget *self, ltk_button_event *event) {
+ ltk_window *window = LTK_CAST_WINDOW(self);
+ ltk_widget *widget = window->pressed_widget;
+ int orig_x = event->x, orig_y = event->y;
+ /* FIXME: why does this only take pressed widget and popups into account? */
+ if (!widget) {
+ widget = get_hover_popup(window, event->x, event->y);
+ widget = get_widget_under_pointer(widget, event->x, event->y, &event->x, &event->y);
+ }
+ /* FIXME: loop up to top of hierarchy if not handled */
+ /* FIXME: see functions above
+ if (widget && queue_mouse_event(widget, event->type, event->x, event->y)) { */
+ /* NOP */
+ if (widget) {
+ ltk_callback_arg args[] = {LTK_MAKE_ARG_BUTTON_EVENT(event)};
+ if (!ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_MOUSE_RELEASE, (ltk_callback_arglist){args, LENGTH(args)})) {
+ if (widget->vtable->mouse_release)
+ widget->vtable->mouse_release(widget, event);
+ }
+ }
+ if (event->button == LTK_BUTTONL && event->type == LTK_BUTTONRELEASE_EVENT) {
+ int release = 0;
+ if (window->pressed_widget) {
+ ltk_rect prect = window->pressed_widget->lrect;
+ ltk_point pglob = ltk_widget_pos_to_global(window->pressed_widget, 0, 0);
+ if (ltk_collide_rect((ltk_rect){pglob.x, pglob.y, prect.w, prect.h}, orig_x, orig_y))
+ release = 1;
+ }
+ ltk_window_set_pressed_widget(window, NULL, release);
+ /* send motion notify to widget under pointer */
+ /* FIXME: only when not collide with rect? */
+ ltk_window_fake_motion_event(window, orig_x, orig_y);
+ }
+ return 1;
+}
+
+static int
+ltk_window_motion_notify_event(ltk_widget *self, ltk_motion_event *event) {
+ ltk_window *window = LTK_CAST_WINDOW(self);
+ ltk_widget *widget = get_hover_popup(window, event->x, event->y);
+ int orig_x = event->x, orig_y = event->y;
+ ltk_callback_arg args[] = {LTK_MAKE_ARG_MOTION_EVENT(event)};
+ if (!widget) {
+ widget = window->pressed_widget;
+ if (widget) {
+ ltk_point local = ltk_global_to_widget_pos(widget, event->x, event->y);
+ event->x = local.x;
+ event->y = local.y;
+ if (!ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_MOTION_NOTIFY, (ltk_callback_arglist){args, LENGTH(args)})) {
+ if (widget->vtable->motion_notify)
+ widget->vtable->motion_notify(widget, event);
+ }
+ return 1;
+ }
+ widget = window->root_widget;
+ }
+ if (!widget)
+ return 1;
+ ltk_point local = ltk_global_to_widget_pos(widget, event->x, event->y);
+ if (!ltk_collide_rect((ltk_rect){0, 0, widget->lrect.w, widget->lrect.h}, local.x, local.y)) {
+ ltk_window_set_hover_widget(widget->window, NULL, event);
+ return 1;
+ }
+ ltk_widget *cur_widget = get_widget_under_pointer(widget, event->x, event->y, &event->x, &event->y);
+ int first = 1;
+ while (cur_widget) {
+ int handled = 0;
+ ltk_point local = ltk_global_to_widget_pos(cur_widget, orig_x, orig_y);
+ event->x = local.x;
+ event->y = local.y;
+ if (cur_widget->state != LTK_DISABLED) {
+ /* FIXME: see functions above
+ if (queue_mouse_event(cur_widget, LTK_MOTION_EVENT, event->x, event->y))
+ handled = 1; */
+ handled = ltk_widget_emit_signal(cur_widget, LTK_WIDGET_SIGNAL_MOTION_NOTIFY, (ltk_callback_arglist){args, LENGTH(args)});
+ if (!handled && cur_widget->vtable->motion_notify)
+ handled = cur_widget->vtable->motion_notify(cur_widget, event);
+ /* set first non-disabled widget to hover widget */
+ /* FIXME: should enter/leave event be sent to parent
+ when moving from/to widget nested in parent? */
+ /* FIXME: use config values for all_activatable */
+ if (first && (cur_widget->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
+ event->x = orig_x;
+ event->y = orig_y;
+ ltk_window_set_hover_widget(window, cur_widget, event);
+ first = 0;
+ }
+ }
+ if (!handled)
+ cur_widget = cur_widget->parent;
+ else
+ break;
+ }
+ if (first) {
+ event->x = orig_x;
+ event->y = orig_y;
+ ltk_window_set_hover_widget(window, NULL, event);
+ }
+ return 1;
+}
+
+void
+ltk_window_set_root_widget(ltk_window *window, ltk_widget *widget) {
+ window->root_widget = widget;
+ widget->lrect.x = 0;
+ widget->lrect.y = 0;
+ widget->lrect.w = window->rect.w;
+ widget->lrect.h = window->rect.h;
+ widget->crect = widget->lrect;
+ ltk_window_invalidate_rect(window, widget->lrect);
+ ltk_widget_resize(widget);
+}
+
+void
+ltk_window_invalidate_rect(ltk_window *window, ltk_rect rect) {
+ if (window->dirty_rect.w == 0 && window->dirty_rect.h == 0)
+ window->dirty_rect = rect;
+ else
+ window->dirty_rect = ltk_rect_union(rect, window->dirty_rect);
+}
+
+void
+ltk_window_invalidate_widget_rect(ltk_window *window, ltk_widget *widget) {
+ ltk_point glob = ltk_widget_pos_to_global(widget, 0, 0);
+ ltk_window_invalidate_rect(window, (ltk_rect){glob.x, glob.y, widget->lrect.w, widget->lrect.h});
+}
+
+static void
+ltk_window_redraw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
+ (void)draw_surf;
+ (void)x;
+ (void)y;
+ (void)clip;
+ if (!self) return;
+ ltk_window *window = LTK_CAST_WINDOW(self);
+ ltk_widget *ptr;
+ if (window->dirty_rect.x >= window->rect.w) return;
+ if (window->dirty_rect.y >= window->rect.h) return;
+ if (window->dirty_rect.x + window->dirty_rect.w > window->rect.w)
+ window->dirty_rect.w -= window->dirty_rect.x + window->dirty_rect.w - window->rect.w;
+ if (window->dirty_rect.y + window->dirty_rect.h > window->rect.h)
+ window->dirty_rect.h -= window->dirty_rect.y + window->dirty_rect.h - window->rect.h;
+ /* FIXME: this should use window->dirty_rect, but that doesn't work
+ properly with double buffering */
+ ltk_surface_fill_rect(window->surface, window->theme->bg, (ltk_rect){0, 0, window->rect.w, window->rect.h});
+ if (window->root_widget) {
+ ptr = window->root_widget;
+ ltk_widget_draw(ptr, window->surface, 0, 0, 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];
+ ltk_widget_draw(ptr, window->surface, ptr->lrect.x, ptr->lrect.y, ltk_rect_relative(ptr->lrect, window->rect));
+ }
+ ltk_renderer_swap_buffers(window->renderwindow);
+ window->dirty_rect.w = 0;
+ window->dirty_rect.h = 0;
+}
+
+static void
+ltk_window_other_event(ltk_window *window, ltk_event *event) {
+ ltk_widget *ptr = window->root_widget;
+ /* FIXME: decide whether this should be moved to separate resize function in window vtable */
+ if (event->type == LTK_CONFIGURE_EVENT) {
+ ltk_window_unregister_all_popups(window);
+ int w, h;
+ w = event->configure.w;
+ h = event->configure.h;
+ int orig_w = window->rect.w;
+ int orig_h = window->rect.h;
+ if (orig_w != w || orig_h != h) {
+ window->rect.w = w;
+ window->rect.h = h;
+ ltk_window_invalidate_rect(window, window->rect);
+ ltk_surface_update_size(window->surface, w, h);
+ if (ltk_widget_emit_signal(LTK_CAST_WIDGET(window), LTK_WIDGET_SIGNAL_RESIZE, LTK_EMPTY_ARGLIST))
+ return;
+ if (ptr) {
+ ptr->lrect.w = w;
+ ptr->lrect.h = h;
+ ptr->crect = ptr->lrect;
+ ltk_widget_resize(ptr);
+ }
+ }
+ } else if (event->type == LTK_EXPOSE_EVENT) {
+ ltk_rect r;
+ r.x = event->expose.x;
+ r.y = event->expose.y;
+ r.w = event->expose.w;
+ r.h = event->expose.h;
+ ltk_window_invalidate_rect(window, r);
+ } else if (event->type == LTK_WINDOWCLOSE_EVENT) {
+ ltk_widget_emit_signal(LTK_CAST_WIDGET(window), LTK_WINDOW_SIGNAL_CLOSE, LTK_EMPTY_ARGLIST);
+ }
+}
+
+/* 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;
+ popup->popup = 1;
+}
+
+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) {
+ popup->popup = 0;
+ 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++) {
+ window->popups[i]->hidden = 1;
+ window->popups[i]->popup = 0;
+ ltk_widget_hide(window->popups[i]);
+ }
+ 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);
+}
+
+/* FIXME: support more options like child windows */
+ltk_window *
+ltk_window_create_intern(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h) {
+ ltk_window *window = ltk_malloc(sizeof(ltk_window));
+ /* this is a bit weird because the window entry points to itself */
+ /* the ideal width isn't needed for a window */
+ ltk_fill_widget_defaults(&window->widget, window, &vtable, 0, 0);
+
+ window->popups = NULL;
+ window->popups_num = window->popups_alloc = 0;
+ window->popups_locked = 0;
+
+ window->renderwindow = ltk_renderer_create_window(data, title, x, y, w, h);
+ ltk_renderer_set_window_properties(window->renderwindow, theme.bg);
+ window->theme = &theme;
+
+ window->root_widget = NULL;
+ window->hover_widget = NULL;
+ window->active_widget = NULL;
+ window->pressed_widget = NULL;
+
+ //FIXME: use widget rect
+ window->rect.w = w;
+ window->rect.h = h;
+ window->rect.x = 0;
+ window->rect.y = 0;
+ window->dirty_rect.w = 0;
+ window->dirty_rect.h = 0;
+ window->dirty_rect.x = 0;
+ window->dirty_rect.y = 0;
+
+ window->surface_cache = ltk_surface_cache_create(window->renderwindow);
+ window->surface = ltk_surface_from_window(window->renderwindow, w, h);
+
+ return window;
+}
+
+/* FIXME: check if widget window matches in all public functions */
+
+void
+ltk_window_destroy_intern(ltk_window *window) {
+ if (window->root_widget) {
+ ltk_widget_destroy(window->root_widget, 0);
+ }
+ if (window->popups)
+ ltk_free(window->popups);
+ ltk_surface_cache_destroy(window->surface_cache);
+ ltk_surface_destroy(window->surface);
+ ltk_renderer_destroy_window(window->renderwindow);
+ ltk_free(window);
+}
+
+/* event must have global coordinates! */
+void
+ltk_window_set_hover_widget(ltk_window *window, ltk_widget *widget, ltk_motion_event *event) {
+ ltk_widget *old = window->hover_widget;
+ if (old == widget)
+ return;
+ int orig_x = event->x, orig_y = event->y;
+ ltk_callback_arg args[] = {LTK_MAKE_ARG_MOTION_EVENT(event)};
+ if (old) {
+ ltk_widget_state old_state = old->state;
+ old->state &= ~LTK_HOVER;
+ ltk_widget_change_state(old, old_state);
+ ltk_point local = ltk_global_to_widget_pos(old, event->x, event->y);
+ event->x = local.x;
+ event->y = local.y;
+ if (!ltk_widget_emit_signal(old, LTK_WIDGET_SIGNAL_MOUSE_LEAVE, (ltk_callback_arglist){args, LENGTH(args)})) {
+ if (old->vtable->mouse_leave)
+ old->vtable->mouse_leave(old, event);
+ }
+ event->x = orig_x;
+ event->y = orig_y;
+ }
+ window->hover_widget = widget;
+ if (widget) {
+ ltk_point local = ltk_global_to_widget_pos(widget, event->x, event->y);
+ event->x = local.x;
+ event->y = local.y;
+ if (!ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_MOUSE_ENTER, (ltk_callback_arglist){args, LENGTH(args)})) {
+ if (widget->vtable->mouse_enter)
+ widget->vtable->mouse_enter(widget, event);
+ }
+ ltk_widget_state old_state = widget->state;
+ widget->state |= LTK_HOVER;
+ ltk_widget_change_state(widget, old_state);
+ if ((widget->vtable->flags & LTK_HOVER_IS_ACTIVE) && widget != window->active_widget)
+ ltk_window_set_active_widget(window, widget);
+ }
+}
+
+void
+ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget) {
+ if (window->active_widget == widget) {
+ return;
+ }
+ ltk_widget *old = window->active_widget;
+ /* Note: this has to be set at the beginning to
+ avoid infinite recursion in some cases */
+ window->active_widget = widget;
+ ltk_widget *common_parent = NULL;
+ if (widget) {
+ ltk_widget *cur = widget;
+ while (cur) {
+ if (cur->state & LTK_ACTIVE) {
+ common_parent = cur;
+ break;
+ }
+ ltk_widget_state old_state = cur->state;
+ cur->state |= LTK_ACTIVE;
+ /* FIXME: should all be set focused? */
+ if (cur == widget && !(cur->vtable->flags & LTK_NEEDS_KEYBOARD))
+ widget->state |= LTK_FOCUSED;
+ ltk_widget_change_state(cur, old_state);
+ cur = cur->parent;
+ }
+ }
+ /* FIXME: better variable names; generally make this nicer */
+ /* special case if old is parent of new active widget */
+ ltk_widget *tmp = common_parent;
+ while (tmp) {
+ if (tmp == old)
+ return;
+ tmp = tmp->parent;
+ }
+ if (old) {
+ old->state &= ~LTK_FOCUSED;
+ ltk_widget *cur = old;
+ while (cur) {
+ if (cur == common_parent)
+ break;
+ ltk_widget_state old_state = cur->state;
+ cur->state &= ~LTK_ACTIVE;
+ ltk_widget_change_state(cur, old_state);
+ cur = cur->parent;
+ }
+ }
+}
+
+void
+ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget, int release) {
+ if (window->pressed_widget == widget)
+ return;
+ if (window->pressed_widget) {
+ ltk_widget_state old_state = window->pressed_widget->state;
+ window->pressed_widget->state &= ~LTK_PRESSED;
+ ltk_widget_change_state(window->pressed_widget, old_state);
+ ltk_window_set_active_widget(window, window->pressed_widget);
+ /* FIXME: this is a bit weird because the release handler for menuentry
+ indirectly calls ltk_widget_hide, which messes with the pressed widget */
+ /* FIXME: isn't it redundant to check that state is pressed? */
+ if (release && (old_state & LTK_PRESSED)) {
+ if (!ltk_widget_emit_signal(window->pressed_widget, LTK_WIDGET_SIGNAL_RELEASE, LTK_EMPTY_ARGLIST)) {
+ if (window->pressed_widget->vtable->release)
+ window->pressed_widget->vtable->release(window->pressed_widget);
+ }
+ }
+ }
+ window->pressed_widget = widget;
+ if (widget) {
+ if (!ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_PRESS, LTK_EMPTY_ARGLIST)) {
+ if (widget->vtable->press)
+ widget->vtable->press(widget);
+ }
+ ltk_widget_state old_state = widget->state;
+ widget->state |= LTK_PRESSED;
+ ltk_widget_change_state(widget, old_state);
+ }
+}
+
+void
+ltk_window_handle_event(ltk_window *window, ltk_event *event) {
+ ltk_widget *self = LTK_CAST_WIDGET(window);
+ ltk_callback_arg args[1];
+ switch (event->type) {
+ case LTK_KEYPRESS_EVENT:
+ args[0] = LTK_MAKE_ARG_KEY_EVENT(&event->key);
+ if (!ltk_widget_emit_signal(self, LTK_WIDGET_SIGNAL_KEY_PRESS, (ltk_callback_arglist){args, LENGTH(args)})) {
+ ltk_window_key_press_event(self, &event->key);
+ }
+ break;
+ case LTK_KEYRELEASE_EVENT:
+ args[0] = LTK_MAKE_ARG_KEY_EVENT(&event->key);
+ if (!ltk_widget_emit_signal(self, LTK_WIDGET_SIGNAL_KEY_RELEASE, (ltk_callback_arglist){args, LENGTH(args)})) {
+ ltk_window_key_release_event(self, &event->key);
+ }
+ break;
+ case LTK_BUTTONPRESS_EVENT:
+ case LTK_2BUTTONPRESS_EVENT:
+ case LTK_3BUTTONPRESS_EVENT:
+ args[0] = LTK_MAKE_ARG_BUTTON_EVENT(&event->button);
+ if (!ltk_widget_emit_signal(self, LTK_WIDGET_SIGNAL_MOUSE_PRESS, (ltk_callback_arglist){args, LENGTH(args)})) {
+ ltk_window_mouse_press_event(self, &event->button);
+ }
+ break;
+ case LTK_SCROLL_EVENT:
+ args[0] = LTK_MAKE_ARG_SCROLL_EVENT(&event->scroll);
+ if (!ltk_widget_emit_signal(self, LTK_WIDGET_SIGNAL_MOUSE_SCROLL, (ltk_callback_arglist){args, LENGTH(args)})) {
+ ltk_window_mouse_scroll_event(self, &event->scroll);
+ }
+ break;
+ case LTK_BUTTONRELEASE_EVENT:
+ case LTK_2BUTTONRELEASE_EVENT:
+ case LTK_3BUTTONRELEASE_EVENT:
+ args[0] = LTK_MAKE_ARG_BUTTON_EVENT(&event->button);
+ if (!ltk_widget_emit_signal(self, LTK_WIDGET_SIGNAL_MOUSE_RELEASE, (ltk_callback_arglist){args, LENGTH(args)})) {
+ ltk_window_mouse_release_event(self, &event->button);
+ }
+ break;
+ case LTK_MOTION_EVENT:
+ args[0] = LTK_MAKE_ARG_MOTION_EVENT(&event->motion);
+ if (!ltk_widget_emit_signal(self, LTK_WIDGET_SIGNAL_MOTION_NOTIFY, (ltk_callback_arglist){args, LENGTH(args)})) {
+ ltk_window_motion_notify_event(self, &event->motion);
+ }
+ break;
+ default:
+ ltk_window_other_event(window, event);
+ }
+}
+
+/* x and y are global! */
+static ltk_widget *
+get_widget_under_pointer(ltk_widget *widget, int x, int y, int *local_x_ret, int *local_y_ret) {
+ ltk_point glob = ltk_widget_pos_to_global(widget, 0, 0);
+ ltk_widget *next = NULL;
+ *local_x_ret = x - glob.x;
+ *local_y_ret = y - glob.y;
+ while (widget && widget->vtable->get_child_at_pos) {
+ next = widget->vtable->get_child_at_pos(widget, *local_x_ret, *local_y_ret);
+ if (!next) {
+ break;
+ } else {
+ widget = next;
+ if (next->popup) {
+ *local_x_ret = x - next->lrect.x;
+ *local_y_ret = y - next->lrect.y;
+ } else {
+ *local_x_ret -= next->lrect.x;
+ *local_y_ret -= next->lrect.y;
+ }
+ }
+ }
+ return 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]->crect, x, y))
+ return window->popups[i];
+ }
+ return NULL;
+}
+
+static int
+is_parent(ltk_widget *parent, ltk_widget *child) {
+ while (child && child != parent) {
+ child = child->parent;
+ }
+ return child != NULL;
+}
+
+/* FIXME: come up with a more elegant way to handle this? */
+/* FIXME: Handle hidden state here instead of in widgets */
+/* FIXME: handle disabled state */
+static int
+prev_child(ltk_window *window) {
+ if (!window->root_widget)
+ return 0;
+ ltk_config *config = ltk_config_get();
+ ltk_widget_flags act_flags = config->general.all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
+ ltk_widget *new, *cur = window->active_widget;
+ int changed = 0;
+ ltk_widget *prevcur = cur;
+ while (1) {
+ if (cur) {
+ while (cur->parent) {
+ new = NULL;
+ if (cur->parent->vtable->prev_child)
+ new = cur->parent->vtable->prev_child(cur->parent, cur);
+ if (new) {
+ cur = new;
+ ltk_widget *last_activatable = (cur->vtable->flags & act_flags) ? cur : NULL;
+ while (cur->vtable->last_child && (new = cur->vtable->last_child(cur))) {
+ cur = new;
+ if (cur->vtable->flags & act_flags)
+ last_activatable = cur;
+ }
+ if (last_activatable) {
+ cur = last_activatable;
+ changed = 1;
+ break;
+ }
+ } else {
+ cur = cur->parent;
+ if (cur->vtable->flags & act_flags) {
+ changed = 1;
+ break;
+ }
+ }
+ }
+ }
+ if (!changed) {
+ cur = window->root_widget;
+ ltk_widget *last_activatable = (cur->vtable->flags & act_flags) ? cur : NULL;
+ while (cur->vtable->last_child && (new = cur->vtable->last_child(cur))) {
+ cur = new;
+ if (cur->vtable->flags & act_flags)
+ last_activatable = cur;
+ }
+ if (last_activatable)
+ cur = last_activatable;
+ }
+ if (prevcur == cur || (cur && (cur->vtable->flags & act_flags)))
+ break;
+ prevcur = cur;
+ }
+ /* FIXME: What exactly should be done if no activatable widget exists? */
+ if (cur != window->active_widget) {
+ ltk_window_set_active_widget(window, cur);
+ ensure_active_widget_shown(window);
+ return 1;
+ }
+ return 0;
+}
+
+static int
+next_child(ltk_window *window) {
+ if (!window->root_widget)
+ return 0;
+ ltk_config *config = ltk_config_get();
+ ltk_widget_flags act_flags = config->general.all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
+ ltk_widget *new, *cur = window->active_widget;
+ int changed = 0;
+ ltk_widget *prevcur = cur;
+ while (1) {
+ if (cur) {
+
+ while (cur->vtable->first_child && (new = cur->vtable->first_child(cur))) {
+ cur = new;
+ if (cur->vtable->flags & act_flags) {
+ changed = 1;
+ break;
+ }
+ }
+ if (!changed) {
+ while (cur->parent) {
+ new = NULL;
+ if (cur->parent->vtable->next_child)
+ new = cur->parent->vtable->next_child(cur->parent, cur);
+ if (new) {
+ cur = new;
+ if (cur->vtable->flags & act_flags) {
+ changed = 1;
+ break;
+ }
+ while (cur->vtable->first_child && (new = cur->vtable->first_child(cur))) {
+ cur = new;
+ if (cur->vtable->flags & act_flags) {
+ changed = 1;
+ break;
+ }
+ }
+ if (changed)
+ break;
+ } else {
+ cur = cur->parent;
+ }
+ }
+ }
+ }
+ if (!changed) {
+ cur = window->root_widget;
+ if (!(cur->vtable->flags & act_flags)) {
+ while (cur->vtable->first_child && (new = cur->vtable->first_child(cur))) {
+ cur = new;
+ if (cur->vtable->flags & act_flags)
+ break;
+ }
+ }
+ if (!(cur->vtable->flags & act_flags))
+ cur = window->root_widget;
+ }
+ if (prevcur == cur || (cur && (cur->vtable->flags & act_flags)))
+ break;
+ prevcur = cur;
+ }
+ if (cur != window->active_widget) {
+ ltk_window_set_active_widget(window, cur);
+ ensure_active_widget_shown(window);
+ return 1;
+ }
+ return 0;
+}
+
+/* FIXME: moving up/down/left/right needs to be rethought
+ it generally is a bit weird, and in particular, nearest_child always searches for the child
+ that has the smallest distance to the given rect, so it may not be the child that the user
+ expects when going down (e.g. a vertical box with one widget closer vertically but on the
+ other side horizontally, thus possibly leading to a different widget that is farther away
+ vertically to be chosen instead) - what would be logical here? */
+static ltk_widget *
+nearest_child(ltk_widget *widget, ltk_rect r) {
+ ltk_point local = ltk_global_to_widget_pos(widget, r.x, r.y);
+ ltk_rect rect = {local.x, local.y, r.w, r.h};
+ if (widget->vtable->nearest_child)
+ return widget->vtable->nearest_child(widget, rect);
+ return NULL;
+}
+
+/* FIXME: maybe wrap around in these two functions? */
+static int
+left_top_child(ltk_window *window, int left) {
+ if (!window->root_widget)
+ return 0;
+ ltk_config *config = ltk_config_get();
+ ltk_widget_flags act_flags = config->general.all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
+ ltk_widget *new, *cur = window->active_widget;
+ ltk_rect old_rect = {0, 0, 0, 0};
+ ltk_widget *last_activatable = NULL;
+ if (!cur) {
+ cur = window->root_widget;
+ if (cur->vtable->flags & act_flags)
+ last_activatable = cur;
+ ltk_rect r = {cur->lrect.w, cur->lrect.h, 0, 0};
+ while ((new = nearest_child(cur, r))) {
+ cur = new;
+ if (cur->vtable->flags & act_flags)
+ last_activatable = cur;
+ }
+ }
+ if (last_activatable) {
+ cur = last_activatable;
+ } else if (cur) {
+ ltk_point glob = cur->parent ? ltk_widget_pos_to_global(cur->parent, cur->lrect.x, cur->lrect.y) : (ltk_point){cur->lrect.x, cur->lrect.y};
+ old_rect = (ltk_rect){glob.x, glob.y, cur->lrect.w, cur->lrect.h};
+ while (cur->parent) {
+ new = NULL;
+ if (left) {
+ if (cur->parent->vtable->nearest_child_left)
+ new = cur->parent->vtable->nearest_child_left(cur->parent, cur);
+ } else {
+ if (cur->parent->vtable->nearest_child_above)
+ new = cur->parent->vtable->nearest_child_above(cur->parent, cur);
+ }
+ if (new) {
+ cur = new;
+ ltk_widget *last_activatable = (cur->vtable->flags & act_flags) ? cur : NULL;
+ while ((new = nearest_child(cur, old_rect))) {
+ cur = new;
+ if (cur->vtable->flags & act_flags)
+ last_activatable = cur;
+ }
+ if (last_activatable) {
+ cur = last_activatable;
+ break;
+ }
+ } else {
+ cur = cur->parent;
+ if (cur->vtable->flags & act_flags) {
+ break;
+ }
+ }
+ }
+ }
+ /* FIXME: What exactly should be done if no activatable widget exists? */
+ if (cur && cur != window->active_widget && (cur->vtable->flags & act_flags)) {
+ ltk_window_set_active_widget(window, cur);
+ ensure_active_widget_shown(window);
+ return 1;
+ }
+ return 0;
+}
+
+static int
+right_bottom_child(ltk_window *window, int right) {
+ if (!window->root_widget)
+ return 0;
+ ltk_config *config = ltk_config_get();
+ ltk_widget_flags act_flags = config->general.all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
+ ltk_widget *new, *cur = window->active_widget;
+ int changed = 0;
+ ltk_rect old_rect = {0, 0, 0, 0};
+ ltk_rect corner = {0, 0, 0, 0};
+ int found_activatable = 0;
+ if (!cur) {
+ cur = window->root_widget;
+ if (!(cur->vtable->flags & act_flags)) {
+ while ((new = nearest_child(cur, (ltk_rect){0, 0, 0, 0}))) {
+ cur = new;
+ if (cur->vtable->flags & act_flags) {
+ found_activatable = 1;
+ break;
+ }
+ }
+ }
+ }
+ if (!found_activatable) {
+ ltk_point glob = cur->parent ? ltk_widget_pos_to_global(cur->parent, cur->lrect.x, cur->lrect.y) : (ltk_point){cur->lrect.x, cur->lrect.y};
+ corner = (ltk_rect){glob.x, glob.y, 0, 0};
+ old_rect = (ltk_rect){glob.x, glob.y, cur->lrect.w, cur->lrect.h};
+ while ((new = nearest_child(cur, corner))) {
+ cur = new;
+ if (cur->vtable->flags & act_flags) {
+ changed = 1;
+ break;
+ }
+ }
+ if (!changed) {
+ while (cur->parent) {
+ new = NULL;
+ if (right) {
+ if (cur->parent->vtable->nearest_child_right)
+ new = cur->parent->vtable->nearest_child_right(cur->parent, cur);
+ } else {
+ if (cur->parent->vtable->nearest_child_below)
+ new = cur->parent->vtable->nearest_child_below(cur->parent, cur);
+ }
+ if (new) {
+ cur = new;
+ if (cur->vtable->flags & act_flags) {
+ changed = 1;
+ break;
+ }
+ while ((new = nearest_child(cur, old_rect))) {
+ cur = new;
+ if (cur->vtable->flags & act_flags) {
+ changed = 1;
+ break;
+ }
+ }
+ if (changed)
+ break;
+ } else {
+ cur = cur->parent;
+ }
+ }
+ }
+ }
+ if (cur && cur != window->active_widget && (cur->vtable->flags & act_flags)) {
+ ltk_window_set_active_widget(window, cur);
+ ensure_active_widget_shown(window);
+ return 1;
+ }
+ return 0;
+}
+
+/* FIXME: maybe just set this when active widget changes */
+/* -> but would also need to change it when widgets are created/destroyed or parents change */
+static void
+gen_widget_stack(ltk_widget *bottom) {
+ widget_stack_len = 0;
+ while (bottom) {
+ if (widget_stack_len + 1 > widget_stack_alloc) {
+ widget_stack_alloc = ideal_array_size(widget_stack_alloc, widget_stack_len + 1);
+ widget_stack = ltk_reallocarray(widget_stack, widget_stack_alloc, sizeof(ltk_widget *));
+ }
+ widget_stack[widget_stack_len++] = bottom;
+ bottom = bottom->parent;
+ }
+}
+
+/* FIXME: The focus behavior needs to be rethought. It's currently hard-coded in the vtable for each
+ widget type, but what if the program using ltk wants to catch keyboard events even if the widget
+ doesn't do that by default? */
+static int
+cb_focus_active(ltk_window *window, ltk_key_event *event, int handled) {
+ (void)event;
+ (void)handled;
+ if (window->active_widget && !(window->active_widget->state & LTK_FOCUSED)) {
+ /* FIXME: maybe also set widgets above in hierarchy? */
+ ltk_widget_state old_state = window->active_widget->state;
+ window->active_widget->state |= LTK_FOCUSED;
+ ltk_widget_change_state(window->active_widget, old_state);
+ return 1;
+ }
+ return 0;
+}
+
+static int
+cb_unfocus_active(ltk_window *window, ltk_key_event *event, int handled) {
+ (void)event;
+ (void)handled;
+ if (window->active_widget && (window->active_widget->state & LTK_FOCUSED) && (window->active_widget->vtable->flags & LTK_NEEDS_KEYBOARD)) {
+ ltk_widget_state old_state = window->active_widget->state;
+ window->active_widget->state &= ~LTK_FOCUSED;
+ ltk_widget_change_state(window->active_widget, old_state);
+ return 1;
+ }
+ return 0;
+}
+
+static int
+cb_move_prev(ltk_window *window, ltk_key_event *event, int handled) {
+ (void)event;
+ (void)handled;
+ return prev_child(window);
+}
+
+static int
+cb_move_next(ltk_window *window, ltk_key_event *event, int handled) {
+ (void)event;
+ (void)handled;
+ return next_child(window);
+}
+
+static int
+cb_move_left(ltk_window *window, ltk_key_event *event, int handled) {
+ (void)event;
+ (void)handled;
+ return left_top_child(window, 1);
+}
+
+static int
+cb_move_right(ltk_window *window, ltk_key_event *event, int handled) {
+ (void)event;
+ (void)handled;
+ return right_bottom_child(window, 1);
+}
+
+static int
+cb_move_up(ltk_window *window, ltk_key_event *event, int handled) {
+ (void)event;
+ (void)handled;
+ return left_top_child(window, 0);
+}
+
+static int
+cb_move_down(ltk_window *window, ltk_key_event *event, int handled) {
+ (void)event;
+ (void)handled;
+ return right_bottom_child(window, 0);
+}
+
+static int
+cb_set_pressed(ltk_window *window, ltk_key_event *event, int handled) {
+ (void)event;
+ (void)handled;
+ if (window->active_widget && (window->active_widget->state & LTK_FOCUSED)) {
+ /* FIXME: only set pressed if needs keyboard? */
+ ltk_window_set_pressed_widget(window, window->active_widget, 0);
+ return 1;
+ }
+ return 0;
+}
+
+static int
+cb_unset_pressed(ltk_window *window, ltk_key_event *event, int handled) {
+ (void)event;
+ (void)handled;
+ if (window->pressed_widget) {
+ ltk_window_set_pressed_widget(window, NULL, 1);
+ return 1;
+ }
+ return 0;
+}
+
+static int
+cb_remove_popups(ltk_window *window, ltk_key_event *event, int handled) {
+ (void)event;
+ (void)handled;
+ if (window->popups_num > 0) {
+ ltk_window_unregister_all_popups(window);
+ return 1;
+ }
+ return 0;
+}
diff --git a/src/window.h b/src/ltk/window.h
diff --git a/src/ltkd/.gitignore b/src/ltkd/.gitignore
@@ -0,0 +1,4 @@
+*.o
+ltkd
+ltkc
+ltkc_img
diff --git a/src/ltkd/box.c b/src/ltkd/box.c
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stddef.h>
+
+#include "err.h"
+#include "ltkd.h"
+#include "widget.h"
+#include "cmd.h"
+
+#include <ltk/ltk.h>
+#include <ltk/util.h>
+#include <ltk/box.h>
+
+/* box <box id> add <widget id> [sticky] */
+static int
+ltkd_box_cmd_add(
+ ltk_window *window,
+ ltkd_widget *widget,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)window;
+ ltk_box *box = LTK_CAST_BOX(widget->widget);
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_UNKNOWN, .optional = 0},
+ {.type = CMDARG_STICKY, .optional = 1},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ if (ltk_box_add(box, cmd[0].val.widget->widget, cmd[1].initialized ? cmd[1].val.sticky : 0)) {
+ err->type = ERR_WIDGET_IN_CONTAINER;
+ err->arg = 0;
+ return 1;
+ }
+ return 0;
+}
+
+/* box <box id> remove <widget id> */
+static int
+ltkd_box_cmd_remove(
+ ltk_window *window,
+ ltkd_widget *widget,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)window;
+ ltk_box *box = LTK_CAST_BOX(widget->widget);
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_UNKNOWN, .optional = 0},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ if (ltk_box_remove(box, cmd[0].val.widget->widget)) {
+ err->type = ERR_WIDGET_NOT_IN_CONTAINER;
+ err->arg = 0;
+ return 1;
+ }
+ return 0;
+}
+
+/* box <box id> create <orientation> */
+static int
+ltkd_box_cmd_create(
+ ltk_window *window,
+ ltkd_widget *widget_unneeded,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)widget_unneeded;
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_IGNORE, .optional = 0},
+ {.type = CMDARG_STRING, .optional = 0},
+ {.type = CMDARG_IGNORE, .optional = 0},
+ {.type = CMDARG_ORIENTATION, .optional = 0},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ ltk_box *box = ltk_box_create(window, cmd[3].val.orient);
+ if (!ltkd_widget_create(LTK_CAST_WIDGET(box), cmd[1].val.str, NULL, 0, err)) {
+ ltk_widget_destroy(LTK_CAST_WIDGET(box), 1);
+ err->arg = 1;
+ return 1;
+ }
+
+ return 0;
+}
+
+static ltkd_cmd_info box_cmds[] = {
+ {"add", <kd_box_cmd_add, 0},
+ {"create", <kd_box_cmd_create, 1},
+ {"remove", <kd_box_cmd_remove, 0},
+};
+
+GEN_CMD_HELPERS(ltkd_box_cmd, LTK_WIDGET_BOX, box_cmds)
diff --git a/src/ltkd/button.c b/src/ltkd/button.c
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stddef.h>
+
+#include "err.h"
+#include "ltkd.h"
+#include "widget.h"
+#include "cmd.h"
+#include "proto_types.h"
+
+#include <ltk/ltk.h>
+#include <ltk/util.h>
+#include <ltk/button.h>
+
+static int
+ltkd_button_press(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg) {
+ (void)widget_unused;
+ (void)args;
+ ltkd_widget *widget = LTK_CAST_ARG_VOIDP(arg);
+ return ltkd_widget_queue_specific_event(widget, "button", LTKD_PWEVENTMASK_BUTTON_PRESS, "press");
+}
+
+static ltkd_event_handler handlers[] = {
+ {<kd_button_press, LTK_BUTTON_SIGNAL_PRESSED},
+};
+
+/* button <button id> create <text> */
+static int
+ltkd_button_cmd_create(
+ ltk_window *window,
+ ltkd_widget *widget_unneeded,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)widget_unneeded;
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_IGNORE, .optional = 0},
+ {.type = CMDARG_STRING, .optional = 0},
+ {.type = CMDARG_IGNORE, .optional = 0},
+ {.type = CMDARG_STRING, .optional = 0},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ ltk_button *button = ltk_button_create(window, cmd[3].val.str);
+ if (!ltkd_widget_create(LTK_CAST_WIDGET(button), cmd[1].val.str, handlers, LENGTH(handlers), err)) {
+ ltk_widget_destroy(LTK_CAST_WIDGET(button), 1);
+ err->arg = 1;
+ return 1;
+ }
+
+ return 0;
+}
+
+static ltkd_cmd_info button_cmds[] = {
+ {"create", <kd_button_cmd_create, 1},
+};
+
+GEN_CMD_HELPERS(ltkd_button_cmd, LTK_WIDGET_BUTTON, button_cmds)
diff --git a/src/ltkd/cmd.c b/src/ltkd/cmd.c
@@ -0,0 +1,185 @@
+/*
+ * Copyright (c) 2023-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "cmd.h"
+
+#include <string.h>
+
+#include "err.h"
+#include "ltkd.h"
+#include "widget.h"
+
+#include <ltk/ltk.h>
+#include <ltk/util.h>
+#include <ltk/color.h>
+#include <ltk/graphics.h>
+
+int
+ltkd_parse_cmd(
+ ltkd_cmd_token *tokens, size_t num_tokens,
+ ltkd_cmdarg_parseinfo *parseinfo, size_t num_arg, ltkd_error *err) {
+ const char *errstr = NULL;
+ ltk_renderdata *renderdata = ltk_get_renderer();
+ if (num_tokens > num_arg || (num_tokens < num_arg && !parseinfo[num_tokens].optional)) {
+ err->type = ERR_INVALID_NUMBER_OF_ARGUMENTS;
+ err->arg = -1;
+ return 1;
+ }
+ size_t i = 0;
+ for (; i < num_tokens; i++) {
+ if (parseinfo[i].type != CMDARG_DATA && tokens[i].contains_nul) {
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = i;
+ goto error;
+ }
+ switch (parseinfo[i].type) {
+ case CMDARG_INT:
+ parseinfo[i].val.i = ltk_strtonum(tokens[i].text, parseinfo[i].min, parseinfo[i].max, &errstr);
+ if (errstr) {
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = i;
+ goto error;
+ }
+ parseinfo[i].initialized = 1;
+ break;
+ case CMDARG_STICKY:
+ parseinfo[i].val.sticky = LTK_STICKY_NONE;
+ for (const char *c = tokens[i].text; *c != '\0'; c++) {
+ switch (*c) {
+ case 't':
+ parseinfo[i].val.sticky |= LTK_STICKY_TOP;
+ break;
+ case 'b':
+ parseinfo[i].val.sticky |= LTK_STICKY_BOTTOM;
+ break;
+ case 'r':
+ parseinfo[i].val.sticky |= LTK_STICKY_RIGHT;
+ break;
+ case 'l':
+ parseinfo[i].val.sticky |= LTK_STICKY_LEFT;
+ break;
+ case 'w':
+ parseinfo[i].val.sticky |= LTK_STICKY_SHRINK_WIDTH;
+ break;
+ case 'h':
+ parseinfo[i].val.sticky |= LTK_STICKY_SHRINK_HEIGHT;
+ break;
+ case 'p':
+ parseinfo[i].val.sticky |= LTK_STICKY_PRESERVE_ASPECT_RATIO;
+ break;
+ default:
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = i;
+ goto error;
+ }
+ }
+ parseinfo[i].initialized = 1;
+ break;
+ case CMDARG_WIDGET:
+ parseinfo[i].val.widget = ltkd_get_widget(tokens[i].text, parseinfo[i].widget_type, err);
+ if (!parseinfo[i].val.widget) {
+ err->arg = i;
+ goto error;
+ }
+ parseinfo[i].initialized = 1;
+ break;
+ case CMDARG_STRING:
+ parseinfo[i].val.str = tokens[i].text;
+ parseinfo[i].initialized = 1;
+ break;
+ case CMDARG_DATA:
+ parseinfo[i].val.data = tokens[i].text;
+ parseinfo[i].initialized = 1;
+ parseinfo[i].len = tokens[i].len;
+ break;
+ case CMDARG_COLOR:
+ if (!(parseinfo[i].val.color = ltk_color_create(renderdata, tokens[i].text))) {
+ /* FIXME: this could fail even if the argument is fine */
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = i;
+ goto error;
+ }
+ parseinfo[i].initialized = 1;
+ break;
+ case CMDARG_BOOL:
+ if (strcmp(tokens[i].text, "true") == 0) {
+ parseinfo[i].val.b = 1;
+ } else if (strcmp(tokens[i].text, "false") == 0) {
+ parseinfo[i].val.b = 0;
+ } else {
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = i;
+ goto error;
+ }
+ parseinfo[i].initialized = 1;
+ break;
+ case CMDARG_ORIENTATION:
+ if (strcmp(tokens[i].text, "horizontal") == 0) {
+ parseinfo[i].val.orient = LTK_HORIZONTAL;
+ } else if (strcmp(tokens[i].text, "vertical") == 0) {
+ parseinfo[i].val.orient = LTK_VERTICAL;
+ } else {
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = i;
+ goto error;
+ }
+ parseinfo[i].initialized = 1;
+ break;
+ case CMDARG_BORDERSIDES:
+ parseinfo[i].val.border = LTK_BORDER_NONE;
+ for (const char *c = tokens[i].text; *c != '\0'; c++) {
+ switch (*c) {
+ case 't':
+ parseinfo[i].val.border |= LTK_BORDER_TOP;
+ break;
+ case 'b':
+ parseinfo[i].val.border |= LTK_BORDER_BOTTOM;
+ break;
+ case 'l':
+ parseinfo[i].val.border |= LTK_BORDER_LEFT;
+ break;
+ case 'r':
+ parseinfo[i].val.border |= LTK_BORDER_RIGHT;
+ break;
+ default:
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = i;
+ goto error;
+ }
+ }
+ parseinfo[i].initialized = 1;
+ break;
+ case CMDARG_IGNORE:
+ parseinfo[i].initialized = 1;
+ break;
+ default:
+ ltkd_fatal("Invalid command argument type. This should not happen.\n");
+ /* TODO: ltk_assert(0); */
+ }
+ }
+ for (; i < num_arg; i++) {
+ parseinfo[i].initialized = 0;
+ }
+ return 0;
+error:
+ for (; i-- > 0;) {
+ if (parseinfo[i].type == CMDARG_COLOR) {
+ ltk_color_destroy(renderdata, parseinfo[i].val.color);
+ }
+ parseinfo[i].initialized = 0;
+ }
+ return 1;
+}
diff --git a/src/ltkd/cmd.h b/src/ltkd/cmd.h
@@ -0,0 +1,157 @@
+/*
+ * Copyright (c) 2023-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LTKD_CMD_H
+#define LTKD_CMD_H
+
+#include <stddef.h>
+
+#include "err.h"
+#include "ltkd.h"
+#include "widget.h"
+
+#include <ltk/ltk.h>
+#include <ltk/util.h>
+#include <ltk/color.h>
+#include <ltk/graphics.h>
+
+typedef struct {
+ char *name;
+ int (*func)(ltk_window *, ltkd_widget *, ltkd_cmd_token *, size_t, ltkd_error *);
+ int needs_all;
+} ltkd_cmd_info;
+
+typedef enum {
+ CMDARG_IGNORE,
+ CMDARG_STRING, /* nul-terminated string */
+ CMDARG_DATA, /* also char*, but may contain nul */
+ CMDARG_COLOR,
+ CMDARG_INT,
+ CMDARG_BOOL,
+ CMDARG_BORDERSIDES,
+ CMDARG_STICKY,
+ CMDARG_WIDGET,
+ CMDARG_ORIENTATION
+} ltkd_cmdarg_datatype;
+
+/* color needs to be destroyed by cmd handling function if it is not needed anymore */
+/* str must *not* be freed because it is a pointer to the original argument
+ -> it must be copied if it needs to be kept around */
+typedef struct {
+ ltkd_cmdarg_datatype type;
+ /* Note: Bool and int are both integers, but they are
+ separate just to make it a bit clearer (same goes for str/data) */
+ union {
+ char *str;
+ char *data;
+ ltk_color *color;
+ int i;
+ int b;
+ ltk_border_sides border;
+ ltk_sticky_mask sticky;
+ ltk_orientation orient;
+ ltkd_widget *widget;
+ } val;
+ size_t len; /* only for data */
+ int min, max; /* only for integers */ /* FIXME: which integer type is sensible here? */
+ ltk_widget_type widget_type; /* only for widgets */
+ int optional;
+ int initialized;
+} ltkd_cmdarg_parseinfo;
+
+/* Returns 1 on error, 0 on success */
+/* All optional arguments must be in one block at the end */
+int ltkd_parse_cmd(
+ ltkd_cmd_token *tokens, size_t num_tokens,
+ ltkd_cmdarg_parseinfo *parseinfo, size_t num_arg, ltkd_error *err
+);
+
+/* FIXME: This doesn't really need to be a macro anymore since it doesn't have to be generic for different types anymore */
+#define GEN_CMD_HELPERS_PROTO(func_name) \
+int func_name(ltk_window *window, ltkd_cmd_token *tokens, size_t num_tokens, ltkd_error *err);
+
+
+#define GEN_CMD_HELPERS(func_name, widget_type, array_name) \
+/* FIXME: maybe just get rid of this and rely on array already being sorted? */ \
+static int array_name##_sorted = 0; \
+ \
+static int \
+array_name##_search_helper(const void *keyv, const void *entryv) { \
+ const char *key = (const char *)keyv; \
+ ltkd_cmd_info *entry = (ltkd_cmd_info *)entryv; \
+ return strcmp(key, entry->name); \
+} \
+ \
+static int \
+array_name##_sort_helper(const void *entry1v, const void *entry2v) { \
+ ltkd_cmd_info *entry1 = (ltkd_cmd_info *)entry1v; \
+ ltkd_cmd_info *entry2 = (ltkd_cmd_info *)entry2v; \
+ return strcmp(entry1->name, entry2->name); \
+} \
+ \
+int \
+func_name( \
+ ltk_window *window, \
+ ltkd_cmd_token *tokens, \
+ size_t num_tokens, \
+ ltkd_error *err) { \
+ if (num_tokens < 3) { \
+ err->type = ERR_INVALID_NUMBER_OF_ARGUMENTS; \
+ err->arg = -1; \
+ return 1; \
+ } \
+ /* just in case */ \
+ if (!array_name##_sorted) { \
+ qsort( \
+ array_name, LENGTH(array_name), \
+ sizeof(array_name[0]), &array_name##_sort_helper); \
+ array_name##_sorted = 1; \
+ } \
+ if (tokens[1].contains_nul) { \
+ err->type = ERR_INVALID_ARGUMENT; \
+ err->arg = 1; \
+ return 1; \
+ } else if (tokens[2].contains_nul) { \
+ err->type = ERR_INVALID_ARGUMENT; \
+ err->arg = 2; \
+ return 1; \
+ } \
+ ltkd_cmd_info *e = bsearch( \
+ tokens[2].text, array_name, LENGTH(array_name), \
+ sizeof(array_name[0]), &array_name##_search_helper \
+ ); \
+ if (!e) { \
+ err->type = ERR_INVALID_COMMAND; \
+ err->arg = -1; \
+ return 1; \
+ } \
+ if (e->needs_all) { \
+ return e->func(window, NULL, tokens, num_tokens, err); \
+ } else { \
+ ltkd_widget *widget = ltkd_get_widget(tokens[1].text, widget_type, err); \
+ if (!widget) { \
+ err->arg = 1; \
+ return 1; \
+ } \
+ int ret = e->func(window, widget, tokens + 3, num_tokens - 3, err); \
+ if (ret && err->arg >= 0) \
+ err->arg += 3; \
+ return ret; \
+ } \
+ return 0; /* Well, I guess this is impossible anyways... */ \
+}
+
+#endif /* LTKD_CMD_H */
diff --git a/src/ltkd/cmd_helpers.h b/src/ltkd/cmd_helpers.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LTKD_CMD_HELPERS_H
+#define LTKD_CMD_HELPERS_H
+
+#include "cmd.h"
+
+GEN_CMD_HELPERS_PROTO(ltkd_box_cmd)
+GEN_CMD_HELPERS_PROTO(ltkd_button_cmd)
+GEN_CMD_HELPERS_PROTO(ltkd_entry_cmd)
+GEN_CMD_HELPERS_PROTO(ltkd_grid_cmd)
+GEN_CMD_HELPERS_PROTO(ltkd_image_widget_cmd)
+GEN_CMD_HELPERS_PROTO(ltkd_label_cmd)
+GEN_CMD_HELPERS_PROTO(ltkd_menu_cmd)
+GEN_CMD_HELPERS_PROTO(ltkd_menuentry_cmd)
+
+#endif /* LTKD_CMD_HELPERS_H */
diff --git a/src/ltkd/entry.c b/src/ltkd/entry.c
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2022-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stddef.h>
+
+#include "err.h"
+#include "ltkd.h"
+#include "widget.h"
+#include "cmd.h"
+
+#include <ltk/ltk.h>
+#include <ltk/util.h>
+#include <ltk/entry.h>
+
+/* FIXME: make text optional, command set-text */
+/* entry <entry id> create <text> */
+static int
+ltkd_entry_cmd_create(
+ ltk_window *window,
+ ltkd_widget *widget_unneeded,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)widget_unneeded;
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_IGNORE, .optional = 0},
+ {.type = CMDARG_STRING, .optional = 0},
+ {.type = CMDARG_IGNORE, .optional = 0},
+ {.type = CMDARG_STRING, .optional = 0},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ ltk_entry *entry = ltk_entry_create(window, cmd[3].val.str);
+ if (!ltkd_widget_create(LTK_CAST_WIDGET(entry), cmd[1].val.str, NULL, 0, err)) {
+ ltk_widget_destroy(LTK_CAST_WIDGET(entry), 1);
+ err->arg = 1;
+ return 1;
+ }
+
+ return 0;
+}
+
+static ltkd_cmd_info entry_cmds[] = {
+ {"create", <kd_entry_cmd_create, 1},
+};
+
+GEN_CMD_HELPERS(ltkd_entry_cmd, LTK_WIDGET_ENTRY, entry_cmds)
diff --git a/src/ltkd/err.c b/src/ltkd/err.c
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2022-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "err.h"
+
+static const char *errtable[] = {
+ "None",
+ "Widget already in container",
+ "Widget not in container",
+ "Widget id already in use",
+ "Invalid number of arguments",
+ "Invalid argument",
+ "Invalid index",
+ "Invalid widget id",
+ "Invalid widget type",
+ "Invalid command",
+ "Unknown error",
+ "Menu is not submenu",
+ "Menu entry already contains submenu",
+ "Invalid grid position",
+ "Invalid sequence number",
+};
+
+#define LENGTH(X) (sizeof(X) / sizeof(X[0]))
+
+const char *
+errtype_to_string(ltkd_errtype type) {
+ if (type < 0 || type >= LENGTH(errtable)) {
+ /* that's a funny error, now isn't it? */
+ return "Invalid error";
+ }
+ return errtable[type];
+}
diff --git a/src/ltkd/err.h b/src/ltkd/err.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2022-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LTKD_ERR_H
+#define LTKD_ERR_H
+
+/* WARNING: THIS NEEDS TO BE KEPT IN SYNC WITH THE TABLE IN err.c! */
+/* (also, the explicit value setting is redundant, but just in case) */
+typedef enum {
+ ERR_NONE = 0,
+ ERR_WIDGET_IN_CONTAINER = 1,
+ ERR_WIDGET_NOT_IN_CONTAINER = 2,
+ ERR_WIDGET_ID_IN_USE = 3,
+ ERR_INVALID_NUMBER_OF_ARGUMENTS = 4,
+ ERR_INVALID_ARGUMENT = 5,
+ ERR_INVALID_INDEX = 6,
+ ERR_INVALID_WIDGET_ID = 7,
+ ERR_INVALID_WIDGET_TYPE = 8,
+ ERR_INVALID_COMMAND = 9,
+ ERR_UNKNOWN = 10,
+ /* widget specific */
+ ERR_MENU_NOT_SUBMENU = 11,
+ ERR_MENU_ENTRY_CONTAINS_SUBMENU = 12,
+ ERR_GRID_INVALID_POSITION = 13,
+ ERR_INVALID_SEQNUM = 14,
+} ltkd_errtype;
+
+typedef struct {
+ ltkd_errtype type;
+ /* corresponding argument, -1 if none */
+ int arg;
+} ltkd_error;
+
+const char *errtype_to_string(ltkd_errtype type);
+
+#endif /* LTKD_ERR_H */
diff --git a/src/ltkd/grid.c b/src/ltkd/grid.c
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "ltkd.h"
+#include "widget.h"
+#include "cmd.h"
+
+#include <ltk/ltk.h>
+#include <ltk/grid.h>
+
+/* grid <grid id> add <widget id> <row> <column> <row_span> <column_span> [sticky] */
+static int
+ltkd_grid_cmd_add(
+ ltk_window *window,
+ ltkd_widget *widget,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)window;
+ ltk_grid *grid = LTK_CAST_GRID(widget->widget);
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_UNKNOWN, .optional = 0},
+ {.type = CMDARG_INT, .min = 0, .max = grid->rows - 1, .optional = 0},
+ {.type = CMDARG_INT, .min = 0, .max = grid->columns - 1, .optional = 0},
+ {.type = CMDARG_INT, .min = 0, .max = grid->rows, .optional = 0},
+ {.type = CMDARG_INT, .min = 0, .max = grid->columns, .optional = 0},
+ {.type = CMDARG_STICKY, .optional = 1},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ int row = cmd[1].val.i, col = cmd[2].val.i;
+ int rowspan = cmd[3].val.i, colspan = cmd[4].val.i;
+ if (row + rowspan > grid->rows) {
+ err->type = ERR_GRID_INVALID_POSITION;
+ err->arg = 1;
+ }
+ if (col + colspan > grid->columns) {
+ err->type = ERR_GRID_INVALID_POSITION;
+ err->arg = 2;
+ }
+ /* FIXME: better error reporting for invalid grid position */
+ /* FIXME: check if recalculation deals properly with rowspan/columnspan
+ that goes over the edge of the grid */
+ if (ltk_grid_add(
+ grid, cmd[0].val.widget->widget,
+ row, col, rowspan, colspan,
+ cmd[5].initialized ? cmd[5].val.sticky : 0)) {
+ err->type = ERR_WIDGET_IN_CONTAINER;
+ err->arg = 0;
+ return 1;
+ }
+ return 0;
+}
+
+/* grid <grid id> remove <widget id> */
+static int
+ltkd_grid_cmd_ungrid(
+ ltk_window *window,
+ ltkd_widget *widget,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)window;
+ ltk_grid *grid = LTK_CAST_GRID(widget->widget);
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_UNKNOWN, .optional = 0}
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ if (ltk_grid_remove(grid, cmd[0].val.widget->widget)) {
+ err->type = ERR_WIDGET_NOT_IN_CONTAINER;
+ err->arg = 0;
+ return 1;
+ }
+ return 0;
+}
+
+/* FIXME: max size of 64 is completely arbitrary! */
+/* grid <grid id> create <rows> <columns> */
+static int
+ltkd_grid_cmd_create(
+ ltk_window *window,
+ ltkd_widget *widget_unneeded,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)widget_unneeded;
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_IGNORE, .optional = 0},
+ {.type = CMDARG_STRING, .optional = 0},
+ {.type = CMDARG_IGNORE, .optional = 0},
+ {.type = CMDARG_INT, .min = 0, .max = 64, .optional = 0},
+ {.type = CMDARG_INT, .min = 0, .max = 64, .optional = 0},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ ltk_grid *grid = ltk_grid_create(window, cmd[3].val.i, cmd[4].val.i);
+ if (!ltkd_widget_create(LTK_CAST_WIDGET(grid), cmd[1].val.str, NULL, 0, err)) {
+ ltk_widget_destroy(LTK_CAST_WIDGET(grid), 1);
+ err->arg = 1;
+ return 1;
+ }
+
+ return 0;
+}
+
+/* FIXME: 64 is completely arbitrary */
+/* grid <grid id> set-row-weight <row> <weight> */
+static int
+ltkd_grid_cmd_set_row_weight(
+ ltk_window *window,
+ ltkd_widget *widget,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)window;
+ ltk_grid *grid = LTK_CAST_GRID(widget->widget);
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_INT, .min = 0, .max = grid->rows - 1, .optional = 0},
+ {.type = CMDARG_INT, .min = 0, .max = 64, .optional = 0},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ ltk_grid_set_row_weight(grid, cmd[0].val.i, cmd[1].val.i);
+
+ return 0;
+}
+
+
+/* FIXME: 64 is completely arbitrary */
+/* FIXME: check for overflows in various grid calculations (at least when larger values are allowed) */
+/* grid <grid id> set-column-weight <column> <weight> */
+static int
+ltkd_grid_cmd_set_column_weight(
+ ltk_window *window,
+ ltkd_widget *widget,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)window;
+ ltk_grid *grid = LTK_CAST_GRID(widget->widget);
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_INT, .min = 0, .max = grid->columns - 1, .optional = 0},
+ {.type = CMDARG_INT, .min = 0, .max = 64, .optional = 0},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ ltk_grid_set_column_weight(grid, cmd[0].val.i, cmd[1].val.i);
+
+ return 0;
+}
+
+static ltkd_cmd_info grid_cmds[] = {
+ {"add", <kd_grid_cmd_add, 0},
+ {"create", <kd_grid_cmd_create, 1},
+ {"remove", <kd_grid_cmd_ungrid, 0},
+ {"set-column-weight", <kd_grid_cmd_set_column_weight, 0},
+ {"set-row-weight", <kd_grid_cmd_set_row_weight, 0},
+};
+
+GEN_CMD_HELPERS(ltkd_grid_cmd, LTK_WIDGET_GRID, grid_cmds)
diff --git a/src/ltkd/image_widget.c b/src/ltkd/image_widget.c
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2023-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stddef.h>
+
+#include "err.h"
+#include "ltkd.h"
+#include "widget.h"
+#include "cmd.h"
+
+#include <ltk/ltk.h>
+#include <ltk/util.h>
+#include <ltk/image.h>
+#include <ltk/image_widget.h>
+
+/* image <image id> create <filename> <data> */
+static int
+ltkd_image_widget_cmd_create(
+ ltk_window *window,
+ ltkd_widget *widget_unneeded,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)widget_unneeded;
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_IGNORE, .optional = 0},
+ {.type = CMDARG_STRING, .optional = 0},
+ {.type = CMDARG_IGNORE, .optional = 0},
+ {.type = CMDARG_STRING, .optional = 0},
+ {.type = CMDARG_DATA, .optional = 0},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ ltk_image *img = ltk_image_create_from_mem(cmd[3].val.str, cmd[4].val.data, cmd[4].len);
+ if (!img) {
+ /* FIXME: more sensible error name */
+ err->type = ERR_UNKNOWN;
+ err->arg = -1;
+ return 1;
+ }
+ ltk_image_widget *imgw = ltk_image_widget_create(window, img);
+ if (!ltkd_widget_create(LTK_CAST_WIDGET(imgw), cmd[1].val.str, NULL, 0, err)) {
+ ltk_widget_destroy(LTK_CAST_WIDGET(imgw), 1);
+ err->arg = 1;
+ return 1;
+ }
+
+ return 0;
+}
+
+static ltkd_cmd_info image_widget_cmds[] = {
+ {"create", <kd_image_widget_cmd_create, 1},
+};
+
+GEN_CMD_HELPERS(ltkd_image_widget_cmd, LTK_WIDGET_IMAGE, image_widget_cmds)
diff --git a/src/ltkd/khash.h b/src/ltkd/khash.h
@@ -0,0 +1,627 @@
+/* The MIT License
+
+ Copyright (c) 2008, 2009, 2011 by Attractive Chaos <attractor@live.co.uk>
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+/*
+ An example:
+
+#include "khash.h"
+KHASH_MAP_INIT_INT(32, char)
+int main() {
+ int ret, is_missing;
+ khiter_t k;
+ khash_t(32) *h = kh_init(32);
+ k = kh_put(32, h, 5, &ret);
+ kh_value(h, k) = 10;
+ k = kh_get(32, h, 10);
+ is_missing = (k == kh_end(h));
+ k = kh_get(32, h, 5);
+ kh_del(32, h, k);
+ for (k = kh_begin(h); k != kh_end(h); ++k)
+ if (kh_exist(h, k)) kh_value(h, k) = 1;
+ kh_destroy(32, h);
+ return 0;
+}
+*/
+
+/*
+ 2013-05-02 (0.2.8):
+
+ * Use quadratic probing. When the capacity is power of 2, stepping function
+ i*(i+1)/2 guarantees to traverse each bucket. It is better than double
+ hashing on cache performance and is more robust than linear probing.
+
+ In theory, double hashing should be more robust than quadratic probing.
+ However, my implementation is probably not for large hash tables, because
+ the second hash function is closely tied to the first hash function,
+ which reduce the effectiveness of double hashing.
+
+ Reference: http://research.cs.vt.edu/AVresearch/hashing/quadratic.php
+
+ 2011-12-29 (0.2.7):
+
+ * Minor code clean up; no actual effect.
+
+ 2011-09-16 (0.2.6):
+
+ * The capacity is a power of 2. This seems to dramatically improve the
+ speed for simple keys. Thank Zilong Tan for the suggestion. Reference:
+
+ - http://code.google.com/p/ulib/
+ - http://nothings.org/computer/judy/
+
+ * Allow to optionally use linear probing which usually has better
+ performance for random input. Double hashing is still the default as it
+ is more robust to certain non-random input.
+
+ * Added Wang's integer hash function (not used by default). This hash
+ function is more robust to certain non-random input.
+
+ 2011-02-14 (0.2.5):
+
+ * Allow to declare global functions.
+
+ 2009-09-26 (0.2.4):
+
+ * Improve portability
+
+ 2008-09-19 (0.2.3):
+
+ * Corrected the example
+ * Improved interfaces
+
+ 2008-09-11 (0.2.2):
+
+ * Improved speed a little in kh_put()
+
+ 2008-09-10 (0.2.1):
+
+ * Added kh_clear()
+ * Fixed a compiling error
+
+ 2008-09-02 (0.2.0):
+
+ * Changed to token concatenation which increases flexibility.
+
+ 2008-08-31 (0.1.2):
+
+ * Fixed a bug in kh_get(), which has not been tested previously.
+
+ 2008-08-31 (0.1.1):
+
+ * Added destructor
+*/
+
+
+#ifndef __AC_KHASH_H
+#define __AC_KHASH_H
+
+/*!
+ @header
+
+ Generic hash table library.
+ */
+
+#define AC_VERSION_KHASH_H "0.2.8"
+
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+
+/* compiler specific configuration */
+
+#if UINT_MAX == 0xffffffffu
+typedef unsigned int khint32_t;
+#elif ULONG_MAX == 0xffffffffu
+typedef unsigned long khint32_t;
+#endif
+
+#if ULONG_MAX == ULLONG_MAX
+typedef unsigned long khint64_t;
+#else
+typedef unsigned long long khint64_t;
+#endif
+
+#ifndef kh_inline
+#ifdef _MSC_VER
+#define kh_inline __inline
+#else
+#define kh_inline inline
+#endif
+#endif /* kh_inline */
+
+#ifndef klib_unused
+#if (defined __clang__ && __clang_major__ >= 3) || (defined __GNUC__ && __GNUC__ >= 3)
+#define klib_unused __attribute__ ((__unused__))
+#else
+#define klib_unused
+#endif
+#endif /* klib_unused */
+
+typedef khint32_t khint_t;
+typedef khint_t khiter_t;
+
+#define __ac_isempty(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&2)
+#define __ac_isdel(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&1)
+#define __ac_iseither(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&3)
+#define __ac_set_isdel_false(flag, i) (flag[i>>4]&=~(1ul<<((i&0xfU)<<1)))
+#define __ac_set_isempty_false(flag, i) (flag[i>>4]&=~(2ul<<((i&0xfU)<<1)))
+#define __ac_set_isboth_false(flag, i) (flag[i>>4]&=~(3ul<<((i&0xfU)<<1)))
+#define __ac_set_isdel_true(flag, i) (flag[i>>4]|=1ul<<((i&0xfU)<<1))
+
+#define __ac_fsize(m) ((m) < 16? 1 : (m)>>4)
+
+#ifndef kroundup32
+#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x))
+#endif
+
+#ifndef kcalloc
+#define kcalloc(N,Z) calloc(N,Z)
+#endif
+#ifndef kmalloc
+#define kmalloc(Z) malloc(Z)
+#endif
+#ifndef krealloc
+#define krealloc(P,Z) realloc(P,Z)
+#endif
+#ifndef kfree
+#define kfree(P) free(P)
+#endif
+
+static const double __ac_HASH_UPPER = 0.77;
+
+#define __KHASH_TYPE(name, khkey_t, khval_t) \
+ typedef struct kh_##name##_s { \
+ khint_t n_buckets, size, n_occupied, upper_bound; \
+ khint32_t *flags; \
+ khkey_t *keys; \
+ khval_t *vals; \
+ } kh_##name##_t;
+
+#define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \
+ extern kh_##name##_t *kh_init_##name(void); \
+ extern void kh_destroy_##name(kh_##name##_t *h); \
+ extern void kh_clear_##name(kh_##name##_t *h); \
+ extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); \
+ extern int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \
+ extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \
+ extern void kh_del_##name(kh_##name##_t *h, khint_t x);
+
+#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
+ SCOPE kh_##name##_t *kh_init_##name(void) { \
+ return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \
+ } \
+ SCOPE void kh_destroy_##name(kh_##name##_t *h) \
+ { \
+ if (h) { \
+ kfree((void *)h->keys); kfree(h->flags); \
+ kfree((void *)h->vals); \
+ kfree(h); \
+ } \
+ } \
+ SCOPE void kh_clear_##name(kh_##name##_t *h) \
+ { \
+ if (h && h->flags) { \
+ memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \
+ h->size = h->n_occupied = 0; \
+ } \
+ } \
+ SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \
+ { \
+ if (h->n_buckets) { \
+ khint_t k, i, last, mask, step = 0; \
+ mask = h->n_buckets - 1; \
+ k = __hash_func(key); i = k & mask; \
+ last = i; \
+ while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \
+ i = (i + (++step)) & mask; \
+ if (i == last) return h->n_buckets; \
+ } \
+ return __ac_iseither(h->flags, i)? h->n_buckets : i; \
+ } else return 0; \
+ } \
+ SCOPE int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \
+ { /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \
+ khint32_t *new_flags = 0; \
+ khint_t j = 1; \
+ { \
+ kroundup32(new_n_buckets); \
+ if (new_n_buckets < 4) new_n_buckets = 4; \
+ if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0; /* requested size is too small */ \
+ else { /* hash table size to be changed (shrink or expand); rehash */ \
+ new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t)); \
+ if (!new_flags) return -1; \
+ memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \
+ if (h->n_buckets < new_n_buckets) { /* expand */ \
+ khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \
+ if (!new_keys) { kfree(new_flags); return -1; } \
+ h->keys = new_keys; \
+ if (kh_is_map) { \
+ khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \
+ if (!new_vals) { kfree(new_flags); return -1; } \
+ h->vals = new_vals; \
+ } \
+ } /* otherwise shrink */ \
+ } \
+ } \
+ if (j) { /* rehashing is needed */ \
+ for (j = 0; j != h->n_buckets; ++j) { \
+ if (__ac_iseither(h->flags, j) == 0) { \
+ khkey_t key = h->keys[j]; \
+ khval_t val; \
+ khint_t new_mask; \
+ new_mask = new_n_buckets - 1; \
+ if (kh_is_map) val = h->vals[j]; \
+ __ac_set_isdel_true(h->flags, j); \
+ while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \
+ khint_t k, i, step = 0; \
+ k = __hash_func(key); \
+ i = k & new_mask; \
+ while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \
+ __ac_set_isempty_false(new_flags, i); \
+ if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \
+ { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \
+ if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \
+ __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \
+ } else { /* write the element and jump out of the loop */ \
+ h->keys[i] = key; \
+ if (kh_is_map) h->vals[i] = val; \
+ break; \
+ } \
+ } \
+ } \
+ } \
+ if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \
+ h->keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \
+ if (kh_is_map) h->vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \
+ } \
+ kfree(h->flags); /* free the working space */ \
+ h->flags = new_flags; \
+ h->n_buckets = new_n_buckets; \
+ h->n_occupied = h->size; \
+ h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \
+ } \
+ return 0; \
+ } \
+ SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \
+ { \
+ khint_t x; \
+ if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \
+ if (h->n_buckets > (h->size<<1)) { \
+ if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \
+ *ret = -1; return h->n_buckets; \
+ } \
+ } else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \
+ *ret = -1; return h->n_buckets; \
+ } \
+ } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \
+ { \
+ khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \
+ x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \
+ if (__ac_isempty(h->flags, i)) x = i; /* for speed up */ \
+ else { \
+ last = i; \
+ while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \
+ if (__ac_isdel(h->flags, i)) site = i; \
+ i = (i + (++step)) & mask; \
+ if (i == last) { x = site; break; } \
+ } \
+ if (x == h->n_buckets) { \
+ if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \
+ else x = i; \
+ } \
+ } \
+ } \
+ if (__ac_isempty(h->flags, x)) { /* not present at all */ \
+ h->keys[x] = key; \
+ __ac_set_isboth_false(h->flags, x); \
+ ++h->size; ++h->n_occupied; \
+ *ret = 1; \
+ } else if (__ac_isdel(h->flags, x)) { /* deleted */ \
+ h->keys[x] = key; \
+ __ac_set_isboth_false(h->flags, x); \
+ ++h->size; \
+ *ret = 2; \
+ } else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \
+ return x; \
+ } \
+ SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \
+ { \
+ if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \
+ __ac_set_isdel_true(h->flags, x); \
+ --h->size; \
+ } \
+ }
+
+#define KHASH_DECLARE(name, khkey_t, khval_t) \
+ __KHASH_TYPE(name, khkey_t, khval_t) \
+ __KHASH_PROTOTYPES(name, khkey_t, khval_t)
+
+#define KHASH_INIT2(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
+ __KHASH_TYPE(name, khkey_t, khval_t) \
+ __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal)
+
+#define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
+ KHASH_INIT2(name, static kh_inline klib_unused, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal)
+
+/* --- BEGIN OF HASH FUNCTIONS --- */
+
+/*! @function
+ @abstract Integer hash function
+ @param key The integer [khint32_t]
+ @return The hash value [khint_t]
+ */
+#define kh_int_hash_func(key) (khint32_t)(key)
+/*! @function
+ @abstract Integer comparison function
+ */
+#define kh_int_hash_equal(a, b) ((a) == (b))
+/*! @function
+ @abstract 64-bit integer hash function
+ @param key The integer [khint64_t]
+ @return The hash value [khint_t]
+ */
+#define kh_int64_hash_func(key) (khint32_t)((key)>>33^(key)^(key)<<11)
+/*! @function
+ @abstract 64-bit integer comparison function
+ */
+#define kh_int64_hash_equal(a, b) ((a) == (b))
+/*! @function
+ @abstract const char* hash function
+ @param s Pointer to a null terminated string
+ @return The hash value
+ */
+static kh_inline khint_t __ac_X31_hash_string(const char *s)
+{
+ khint_t h = (khint_t)*s;
+ if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s;
+ return h;
+}
+/*! @function
+ @abstract Another interface to const char* hash function
+ @param key Pointer to a null terminated string [const char*]
+ @return The hash value [khint_t]
+ */
+#define kh_str_hash_func(key) __ac_X31_hash_string(key)
+/*! @function
+ @abstract Const char* comparison function
+ */
+#define kh_str_hash_equal(a, b) (strcmp(a, b) == 0)
+
+static kh_inline khint_t __ac_Wang_hash(khint_t key)
+{
+ key += ~(key << 15);
+ key ^= (key >> 10);
+ key += (key << 3);
+ key ^= (key >> 6);
+ key += ~(key << 11);
+ key ^= (key >> 16);
+ return key;
+}
+#define kh_int_hash_func2(key) __ac_Wang_hash((khint_t)key)
+
+/* --- END OF HASH FUNCTIONS --- */
+
+/* Other convenient macros... */
+
+/*!
+ @abstract Type of the hash table.
+ @param name Name of the hash table [symbol]
+ */
+#define khash_t(name) kh_##name##_t
+
+/*! @function
+ @abstract Initiate a hash table.
+ @param name Name of the hash table [symbol]
+ @return Pointer to the hash table [khash_t(name)*]
+ */
+#define kh_init(name) kh_init_##name()
+
+/*! @function
+ @abstract Destroy a hash table.
+ @param name Name of the hash table [symbol]
+ @param h Pointer to the hash table [khash_t(name)*]
+ */
+#define kh_destroy(name, h) kh_destroy_##name(h)
+
+/*! @function
+ @abstract Reset a hash table without deallocating memory.
+ @param name Name of the hash table [symbol]
+ @param h Pointer to the hash table [khash_t(name)*]
+ */
+#define kh_clear(name, h) kh_clear_##name(h)
+
+/*! @function
+ @abstract Resize a hash table.
+ @param name Name of the hash table [symbol]
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param s New size [khint_t]
+ */
+#define kh_resize(name, h, s) kh_resize_##name(h, s)
+
+/*! @function
+ @abstract Insert a key to the hash table.
+ @param name Name of the hash table [symbol]
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param k Key [type of keys]
+ @param r Extra return code: -1 if the operation failed;
+ 0 if the key is present in the hash table;
+ 1 if the bucket is empty (never used); 2 if the element in
+ the bucket has been deleted [int*]
+ @return Iterator to the inserted element [khint_t]
+ */
+#define kh_put(name, h, k, r) kh_put_##name(h, k, r)
+
+/*! @function
+ @abstract Retrieve a key from the hash table.
+ @param name Name of the hash table [symbol]
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param k Key [type of keys]
+ @return Iterator to the found element, or kh_end(h) if the element is absent [khint_t]
+ */
+#define kh_get(name, h, k) kh_get_##name(h, k)
+
+/*! @function
+ @abstract Remove a key from the hash table.
+ @param name Name of the hash table [symbol]
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param k Iterator to the element to be deleted [khint_t]
+ */
+#define kh_del(name, h, k) kh_del_##name(h, k)
+
+/*! @function
+ @abstract Test whether a bucket contains data.
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param x Iterator to the bucket [khint_t]
+ @return 1 if containing data; 0 otherwise [int]
+ */
+#define kh_exist(h, x) (!__ac_iseither((h)->flags, (x)))
+
+/*! @function
+ @abstract Get key given an iterator
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param x Iterator to the bucket [khint_t]
+ @return Key [type of keys]
+ */
+#define kh_key(h, x) ((h)->keys[x])
+
+/*! @function
+ @abstract Get value given an iterator
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param x Iterator to the bucket [khint_t]
+ @return Value [type of values]
+ @discussion For hash sets, calling this results in segfault.
+ */
+#define kh_val(h, x) ((h)->vals[x])
+
+/*! @function
+ @abstract Alias of kh_val()
+ */
+#define kh_value(h, x) ((h)->vals[x])
+
+/*! @function
+ @abstract Get the start iterator
+ @param h Pointer to the hash table [khash_t(name)*]
+ @return The start iterator [khint_t]
+ */
+#define kh_begin(h) (khint_t)(0)
+
+/*! @function
+ @abstract Get the end iterator
+ @param h Pointer to the hash table [khash_t(name)*]
+ @return The end iterator [khint_t]
+ */
+#define kh_end(h) ((h)->n_buckets)
+
+/*! @function
+ @abstract Get the number of elements in the hash table
+ @param h Pointer to the hash table [khash_t(name)*]
+ @return Number of elements in the hash table [khint_t]
+ */
+#define kh_size(h) ((h)->size)
+
+/*! @function
+ @abstract Get the number of buckets in the hash table
+ @param h Pointer to the hash table [khash_t(name)*]
+ @return Number of buckets in the hash table [khint_t]
+ */
+#define kh_n_buckets(h) ((h)->n_buckets)
+
+/*! @function
+ @abstract Iterate over the entries in the hash table
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param kvar Variable to which key will be assigned
+ @param vvar Variable to which value will be assigned
+ @param code Block of code to execute
+ */
+#define kh_foreach(h, kvar, vvar, code) { khint_t __i; \
+ for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \
+ if (!kh_exist(h,__i)) continue; \
+ (kvar) = kh_key(h,__i); \
+ (vvar) = kh_val(h,__i); \
+ code; \
+ } }
+
+/*! @function
+ @abstract Iterate over the values in the hash table
+ @param h Pointer to the hash table [khash_t(name)*]
+ @param vvar Variable to which value will be assigned
+ @param code Block of code to execute
+ */
+#define kh_foreach_value(h, vvar, code) { khint_t __i; \
+ for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \
+ if (!kh_exist(h,__i)) continue; \
+ (vvar) = kh_val(h,__i); \
+ code; \
+ } }
+
+/* More conenient interfaces */
+
+/*! @function
+ @abstract Instantiate a hash set containing integer keys
+ @param name Name of the hash table [symbol]
+ */
+#define KHASH_SET_INIT_INT(name) \
+ KHASH_INIT(name, khint32_t, char, 0, kh_int_hash_func, kh_int_hash_equal)
+
+/*! @function
+ @abstract Instantiate a hash map containing integer keys
+ @param name Name of the hash table [symbol]
+ @param khval_t Type of values [type]
+ */
+#define KHASH_MAP_INIT_INT(name, khval_t) \
+ KHASH_INIT(name, khint32_t, khval_t, 1, kh_int_hash_func, kh_int_hash_equal)
+
+/*! @function
+ @abstract Instantiate a hash map containing 64-bit integer keys
+ @param name Name of the hash table [symbol]
+ */
+#define KHASH_SET_INIT_INT64(name) \
+ KHASH_INIT(name, khint64_t, char, 0, kh_int64_hash_func, kh_int64_hash_equal)
+
+/*! @function
+ @abstract Instantiate a hash map containing 64-bit integer keys
+ @param name Name of the hash table [symbol]
+ @param khval_t Type of values [type]
+ */
+#define KHASH_MAP_INIT_INT64(name, khval_t) \
+ KHASH_INIT(name, khint64_t, khval_t, 1, kh_int64_hash_func, kh_int64_hash_equal)
+
+typedef const char *kh_cstr_t;
+/*! @function
+ @abstract Instantiate a hash map containing const char* keys
+ @param name Name of the hash table [symbol]
+ */
+#define KHASH_SET_INIT_STR(name) \
+ KHASH_INIT(name, kh_cstr_t, char, 0, kh_str_hash_func, kh_str_hash_equal)
+
+/*! @function
+ @abstract Instantiate a hash map containing const char* keys
+ @param name Name of the hash table [symbol]
+ @param khval_t Type of values [type]
+ */
+#define KHASH_MAP_INIT_STR(name, khval_t) \
+ KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal)
+
+#endif /* __AC_KHASH_H */
diff --git a/src/ltkd/label.c b/src/ltkd/label.c
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stddef.h>
+
+#include "err.h"
+#include "ltkd.h"
+#include "widget.h"
+#include "cmd.h"
+
+#include <ltk/ltk.h>
+#include <ltk/util.h>
+#include <ltk/label.h>
+
+/* label <label id> create <text> */
+static int
+ltkd_label_cmd_create(
+ ltk_window *window,
+ ltkd_widget *widget_unneeded,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)widget_unneeded;
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_IGNORE, .optional = 0},
+ {.type = CMDARG_STRING, .optional = 0},
+ {.type = CMDARG_IGNORE, .optional = 0},
+ {.type = CMDARG_STRING, .optional = 0},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ ltk_label *label = ltk_label_create(window, cmd[3].val.str);
+ if (!ltkd_widget_create(LTK_CAST_WIDGET(label), cmd[1].val.str, NULL, 0, err)) {
+ ltk_widget_destroy(LTK_CAST_WIDGET(label), 1);
+ err->arg = 1;
+ return 1;
+ }
+
+ return 0;
+}
+
+static ltkd_cmd_info label_cmds[] = {
+ {"create", <kd_label_cmd_create, 1},
+};
+
+GEN_CMD_HELPERS(ltkd_label_cmd, LTK_WIDGET_LABEL, label_cmds)
diff --git a/src/ltkd/ltkc.c b/src/ltkd/ltkc.c
@@ -0,0 +1,272 @@
+/*
+ * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <time.h>
+#include <inttypes.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include "util.h"
+
+#include <ltk/util.h>
+#include <ltk/memory.h>
+#include <ltk/macros.h>
+
+#define BLK_SIZE 128
+char tmp_buf[BLK_SIZE];
+
+static struct {
+ char *in_buffer; /* text that is read from stdin and written to the socket */
+ int in_len;
+ int in_alloc;
+ char *out_buffer; /* text that is read from the socket and written to stdout */
+ int out_len;
+ int out_alloc;
+} io_buffers;
+
+static char *ltkd_dir = NULL;
+static char *sock_path = NULL;
+static int sockfd = -1;
+
+void
+ltkc_log_msg(const char *mode, const char *format, va_list args) {
+ fprintf(stderr, "ltkc %s: ", mode);
+ vfprintf(stderr, format, args);
+}
+
+static void
+ltkc_cleanup() {
+ if (sockfd >= 0)
+ close(sockfd);
+ if (ltkd_dir)
+ ltk_free(ltkd_dir);
+ if (sock_path)
+ ltk_free(sock_path);
+ if (io_buffers.in_buffer)
+ ltk_free(io_buffers.in_buffer);
+ if (io_buffers.out_buffer)
+ ltk_free(io_buffers.out_buffer);
+}
+
+LTK_GEN_LOG_FUNC_PROTO(ltkc)
+LTK_GEN_LOG_FUNCS(ltkc, ltkc_log_msg, ltkc_cleanup)
+
+int main(int argc, char *argv[]) {
+ char num[12];
+ int bs = 0;
+ int last_newline = 1;
+ int in_str = 0;
+ uint32_t seq = 0;
+ int maxfd;
+ int infd = fileno(stdin);
+ int outfd = fileno(stdout);
+ struct sockaddr_un un;
+ fd_set rfds, wfds, rallfds, wallfds;
+ struct timeval tv;
+ tv.tv_sec = 0;
+ tv.tv_usec = 20000;
+ size_t path_size;
+
+ if (argc != 2) {
+ (void)fprintf(stderr, "USAGE: ltkc <socket id>\n");
+ return 1;
+ }
+
+ ltkd_dir = ltk_setup_directory("LTKDDIR", ".ltkd", 1);
+ if (!ltkd_dir) {
+ (void)fprintf(stderr, "Unable to setup ltk directory.\n");
+ return 1;
+ }
+
+ /* 7 because of "/", ".sock", and '\0' */
+ path_size = strlen(ltkd_dir) + strlen(argv[1]) + 7;
+ sock_path = ltk_malloc(path_size);
+ snprintf(sock_path, path_size, "%s/%s.sock", ltkd_dir, argv[1]);
+
+ if ((sockfd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
+ perror("Socket error");
+ return -1;
+ }
+ memset(&un, 0, sizeof(un));
+ un.sun_family = AF_UNIX;
+ if (path_size > sizeof(un.sun_path)) {
+ (void)fprintf(stderr, "Socket path too long.\n");
+ return 1;
+ }
+ strcpy(un.sun_path, sock_path);
+ if (connect(sockfd, (struct sockaddr *)&un, offsetof(struct sockaddr_un, sun_path) + path_size) < 0) {
+ perror("Socket error");
+ return -2;
+ }
+ if (ltkd_set_nonblock(sockfd)) {
+ (void)fprintf(stderr, "Unable to set socket to non-blocking mode.\n");
+ return 1;
+ } else if (ltkd_set_nonblock(infd)) {
+ (void)fprintf(stderr, "Unable to set stdin to non-blocking mode.\n");
+ return 1;
+ } else if (ltkd_set_nonblock(outfd)) {
+ (void)fprintf(stderr, "Unable to set stdout to non-blocking mode.\n");
+ return 1;
+ }
+
+ io_buffers.in_buffer = ltk_malloc(BLK_SIZE);
+ io_buffers.in_alloc = BLK_SIZE;
+
+ io_buffers.out_buffer = ltk_malloc(BLK_SIZE);
+ io_buffers.out_alloc = BLK_SIZE;
+
+ FD_ZERO(&rallfds);
+ FD_ZERO(&wallfds);
+
+ FD_SET(sockfd, &rallfds);
+ FD_SET(infd, &rallfds);
+ FD_SET(sockfd, &wallfds);
+ FD_SET(outfd, &wallfds);
+ maxfd = sockfd > infd ? sockfd : infd;
+ if (maxfd < outfd)
+ maxfd = outfd;
+
+ struct timespec now, elapsed, last, sleep_time;
+ clock_gettime(CLOCK_MONOTONIC, &last);
+ sleep_time.tv_sec = 0;
+
+ while (1) {
+ if (!FD_ISSET(sockfd, &rallfds) && io_buffers.out_len == 0)
+ break;
+ rfds = rallfds;
+ wfds = wallfds;
+ select(maxfd + 1, &rfds, &wfds, NULL, &tv);
+
+ /* FIXME: make all this buffer handling a bit more intelligent */
+ if (FD_ISSET(sockfd, &rfds)) {
+ while (1) {
+ ltk_grow_string(&io_buffers.out_buffer,
+ &io_buffers.out_alloc,
+ io_buffers.out_len + BLK_SIZE);
+ int nread = read(sockfd,
+ io_buffers.out_buffer + io_buffers.out_len,
+ BLK_SIZE);
+ if (nread < 0) {
+ /* FIXME: distinguish errors */
+ break;
+ } else if (nread == 0) {
+ FD_CLR(sockfd, &rallfds);
+ FD_CLR(sockfd, &wallfds);
+ break;
+ } else {
+ io_buffers.out_len += nread;
+ }
+ }
+ }
+
+ if (FD_ISSET(infd, &rfds)) {
+ while (1) {
+ int nread = read(infd, tmp_buf, BLK_SIZE);
+ if (nread < 0) {
+ break;
+ } else if (nread == 0) {
+ FD_CLR(infd, &rallfds);
+ break;
+ } else {
+ for (int i = 0; i < nread; i++) {
+ if (last_newline) {
+ int numlen = snprintf(num, sizeof(num), "%"PRIu32" ", seq);
+ if (numlen < 0 || (unsigned)numlen >= sizeof(num))
+ ltkc_fatal("There's a bug in the universe.\n");
+ ltk_grow_string(
+ &io_buffers.in_buffer,
+ &io_buffers.in_alloc,
+ io_buffers.in_len + numlen
+ );
+ memcpy(io_buffers.in_buffer + io_buffers.in_len, num, numlen);
+ io_buffers.in_len += numlen;
+ last_newline = 0;
+ seq++;
+ }
+ if (tmp_buf[i] == '\\') {
+ bs++;
+ bs %= 2;
+ } else if (tmp_buf[i] == '"' && !bs) {
+ in_str = !in_str;
+ } else if (tmp_buf[i] == '\n' && !in_str) {
+ last_newline = 1;
+ } else {
+ bs = 0;
+ }
+ if (io_buffers.in_len == io_buffers.in_alloc) {
+ ltk_grow_string(
+ &io_buffers.in_buffer,
+ &io_buffers.in_alloc,
+ io_buffers.in_len + 1
+ );
+ }
+ io_buffers.in_buffer[io_buffers.in_len++] = tmp_buf[i];
+ }
+ }
+ }
+ }
+
+ if (FD_ISSET(sockfd, &wfds)) {
+ while (io_buffers.in_len > 0) {
+ int maxwrite = BLK_SIZE > io_buffers.in_len ?
+ io_buffers.in_len : BLK_SIZE;
+ int nwritten = write(sockfd, io_buffers.in_buffer, maxwrite);
+ if (nwritten <= 0) {
+ break;
+ } else {
+ memmove(io_buffers.in_buffer,
+ io_buffers.in_buffer + nwritten,
+ io_buffers.in_len - nwritten);
+ io_buffers.in_len -= nwritten;
+ }
+ }
+ }
+
+ if (FD_ISSET(outfd, &wfds)) {
+ while (io_buffers.out_len > 0) {
+ int maxwrite = BLK_SIZE > io_buffers.out_len ?
+ io_buffers.out_len : BLK_SIZE;
+ int nwritten = write(outfd, io_buffers.out_buffer, maxwrite);
+ if (nwritten <= 0) {
+ break;
+ } else {
+ memmove(io_buffers.out_buffer,
+ io_buffers.out_buffer + nwritten,
+ io_buffers.out_len - nwritten);
+ io_buffers.out_len -= nwritten;
+ }
+ }
+ }
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ ltk_timespecsub(&now, &last, &elapsed);
+ /* FIXME: configure framerate */
+ if (elapsed.tv_sec == 0 && elapsed.tv_nsec < 20000000LL) {
+ sleep_time.tv_nsec = 20000000LL - elapsed.tv_nsec;
+ nanosleep(&sleep_time, NULL);
+ }
+ last = now;
+ }
+
+ ltkc_cleanup();
+
+ return 0;
+}
diff --git a/src/ltkd/ltkc_img.c b/src/ltkd/ltkc_img.c
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2023-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/* This is just a temporary hack to preprocess an image for sending it to
+ ltkd. The nicer way for this would be to have a special case for the
+ "image create" command in ltkc, but I was too lazy to implement that
+ right now. */
+
+#include <stdio.h>
+
+int main(int argc, char *argv[]) {
+ (void)argc;
+ (void)argv;
+ int c;
+ while ((c = getchar()) != EOF) {
+ switch (c) {
+ case '\\':
+ fputs("\\\\", stdout);
+ break;
+ case '"':
+ fputs("\\\"", stdout);
+ break;
+ default:
+ putchar(c);
+ }
+ }
+
+ return 0;
+}
diff --git a/src/ltkd/ltkd.c b/src/ltkd/ltkd.c
@@ -0,0 +1,1097 @@
+/* FIXME: Figure out how to properly print window id */
+/* FIXME: error checking in tokenizer (is this necessary?) */
+/* FIXME: strip whitespace at end of lines in socket format */
+/*
+ * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <time.h>
+#include <stdio.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+#include <unistd.h>
+#include <signal.h>
+#include <locale.h>
+#include <inttypes.h>
+
+#include <sys/un.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+
+#include "err.h"
+#include "ltkd.h"
+#include "util.h"
+#include "widget.h"
+#include "cmd_helpers.h"
+#include "proto_types.h"
+
+#include <ltk/memory.h>
+#include <ltk/ltk.h>
+#include <ltk/util.h>
+#include <ltk/text.h>
+#include <ltk/macros.h>
+#include <ltk/graphics.h>
+
+#define MAX_SOCK_CONNS 20
+#define READ_BLK_SIZE 128
+#define WRITE_BLK_SIZE 128
+
+struct token_list {
+ ltkd_cmd_token *tokens;
+ /* FIXME: size_t everywhere */
+ int num_tokens;
+ int num_alloc;
+};
+
+/* FIXME: switch to size_t */
+static struct ltkd_sock_info {
+ int fd; /* file descriptor for socket connection */
+ int event_mask; /* events to send to socket */
+ char *read; /* text read from socket */
+ int read_len; /* length of text in read buffer */
+ int read_alloc; /* size of read buffer */
+ char *to_write; /* text to be written to socket */
+ int write_len; /* length of text in write buffer */
+ int write_cur; /* length of text already written */
+ int write_alloc; /* size of write buffer */
+ /* stuff for tokenizing */
+ int in_token; /* last read char is inside token */
+ int offset; /* offset from removing backslashes */
+ int in_str; /* last read char is inside string */
+ int read_cur; /* length of text already tokenized */
+ int bs; /* last char was non-escaped backslash */
+ struct token_list tokens; /* current tokens */
+ uint32_t last_seq; /* sequence number of last request processed */
+} sockets[MAX_SOCK_CONNS];
+
+static int daemonize_flag = 1;
+
+static void ltkd_mainloop(ltk_window *window);
+static char *get_sock_path(char *basedir, unsigned long id);
+static FILE *open_log(char *dir);
+static void daemonize(void);
+static int read_sock(struct ltkd_sock_info *sock);
+static int push_token(struct token_list *tl, char *token);
+static int read_sock(struct ltkd_sock_info *sock);
+static int write_sock(struct ltkd_sock_info *sock);
+static int tokenize_command(struct ltkd_sock_info *sock);
+static int ltkd_set_root_widget_cmd(ltk_window *window, ltkd_cmd_token *tokens, int num_tokens, ltkd_error *err);
+static int process_commands(ltk_window *window, int client);
+static int add_client(int fd);
+static int listen_sock(const char *sock_path);
+static int accept_sock(int listenfd);
+static void ltkd_quit(void);
+static void ltkd_cleanup(void);
+
+static short maxsocket = -1;
+static short running = 1;
+static short sock_write_available = 0;
+static int base_dir_fd = -1;
+static char *ltkd_dir = NULL;
+static FILE *ltkd_logfile = NULL;
+static char *sock_path = NULL;
+/* Note: Most functions still take this explicitly because it wasn't
+ global originally, but that's just the way it is. */
+static ltk_window *main_window = NULL;
+
+typedef struct {
+ char *name;
+ int (*cmd)(ltk_window *, ltkd_cmd_token *, size_t, ltkd_error *);
+} ltkd_widget_funcs;
+
+/* FIXME: use binary search when searching for the widget */
+ltkd_widget_funcs widget_funcs[] = {
+ {
+ .name = "box",
+ .cmd = <kd_box_cmd
+ },
+ {
+ .name = "button",
+ .cmd = <kd_button_cmd
+ },
+ {
+ .name = "entry",
+ .cmd = <kd_entry_cmd
+ },
+ {
+ .name = "grid",
+ .cmd = <kd_grid_cmd
+ },
+ {
+ .name = "label",
+ .cmd = <kd_label_cmd
+ },
+ {
+ .name = "image",
+ .cmd = <kd_image_widget_cmd
+ },
+ {
+ .name = "menu",
+ .cmd = <kd_menu_cmd
+ },
+ {
+ .name = "menuentry",
+ .cmd = <kd_menuentry_cmd
+ },
+ {
+ .name = "submenu",
+ .cmd = <kd_menu_cmd
+ },
+};
+
+static int
+ltkd_window_close(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
+ (void)self;
+ (void)args;
+ (void)data;
+ ltkd_quit();
+ return 1;
+}
+
+int
+main(int argc, char *argv[]) {
+ setlocale(LC_CTYPE, "");
+ /* FIXME: decide where to add this (currently called in ltk, but kind of weird) */
+ /*XSetLocaleModifiers("");*/
+ int ch;
+ char *title = "LTK Window";
+ while ((ch = getopt(argc, argv, "dt:")) != -1) {
+ switch (ch) {
+ case 't':
+ title = optarg;
+ break;
+ case 'd':
+ daemonize_flag = 0;
+ break;
+ default:
+ ltkd_fatal("USAGE: ltkd [-t title]\n");
+ }
+ }
+
+ ltkd_dir = ltk_setup_directory("LTKDDIR", ".ltkd", 1);
+ if (!ltkd_dir) ltkd_fatal_errno("Unable to setup ltkd directory.\n");
+ /* FIXME: this is only used to use unlinkat for deleting the socket in
+ the end in case ltkd_dir is relative. It would probably be better
+ to just use getcwd to turn that into an absolute path instead.
+ Or maybe ltkd should just stay in the current directory instead of
+ moving to / in daemonize(). */
+ base_dir_fd = open(".", O_RDONLY);
+ if (base_dir_fd < 0)
+ ltkd_fatal_errno("Unable to open current directory.\n");
+ ltkd_logfile = open_log(ltkd_dir);
+ if (!ltkd_logfile) ltkd_fatal_errno("Unable to open log file.\n");
+
+ ltk_init();
+ ltkd_widgets_init();
+
+ /* FIXME: set window size properly - I only run it in a tiling WM
+ anyways, so it doesn't matter, but still... */
+ main_window = ltk_window_create(title, 0, 0, 500, 500);
+ ltk_widget_register_signal_handler(LTK_CAST_WIDGET(main_window), LTK_WINDOW_SIGNAL_CLOSE, <kd_window_close, LTK_ARG_VOID);
+
+ /* This hack is necessary to make the daemonization work properly when using Pango.
+ This may not be entirely accurate, but from what I gather, newer versions of Pango
+ initialize Fontconfig in a separate thread to avoid startup overhead. This leads
+ to non-deterministic behavior because the Fontconfig initialization doesn't work
+ properly after daemonization. Creating a text line and getting the size waits until
+ Fontconfig is initialized. Getting the size is important because Pango doesn't
+ actually do much until you try to use the line for something. */
+ /* FIXME: I guess just calling FcInit manually in the text backend could work as well. */
+ /* FIXME: Maybe just call this when actually daemonizing. */
+ ltk_text_line *tmp = ltk_text_line_create_default(10, "hi", 0, -1);
+ int tw, th;
+ ltk_text_line_get_size(tmp, &tw, &th);
+ ltk_text_line_destroy(tmp);
+
+ sock_path = get_sock_path(ltkd_dir, ltk_renderer_get_window_id(main_window->renderwindow));
+ if (!sock_path) ltkd_fatal_errno("Unable to allocate memory for socket path.\n");
+
+ /* Note: sockets should be initialized to 0 because it is static */
+ for (int i = 0; i < MAX_SOCK_CONNS; i++) {
+ sockets[i].fd = -1; /* socket unused */
+ /* initialize these just because I'm paranoid */
+ sockets[i].read = NULL;
+ sockets[i].to_write = NULL;
+ sockets[i].tokens.tokens = NULL;
+ }
+
+ ltkd_mainloop(main_window);
+ return 0;
+}
+
+/* FIXME: need to recalculate maxfd when removing client */
+static struct {
+ fd_set rallfds, wallfds;
+ int maxfd;
+ int listenfd;
+} sock_state;
+
+/* FIXME: this is extremely dangerous right now because pretty much any command
+ can be executed, so for instance the widget that caused the lock could also
+ be destroyed, causing issues when this function returns */
+int
+ltkd_handle_lock_client(ltk_window *window, int client) {
+ if (client < 0 || client >= MAX_SOCK_CONNS || sockets[client].fd == -1)
+ return 0;
+ fd_set rfds, wfds, rallfds, wallfds;
+ int clifd = sockets[client].fd;
+ FD_ZERO(&rallfds);
+ FD_ZERO(&wallfds);
+ FD_SET(clifd, &rallfds);
+ FD_SET(clifd, &wallfds);
+ int retval;
+ struct timeval tv;
+ tv.tv_sec = 0;
+ tv.tv_usec = 0;
+ struct timespec now, elapsed, last, sleep_time;
+ clock_gettime(CLOCK_MONOTONIC, &last);
+ sleep_time.tv_sec = 0;
+ while (1) {
+ rfds = rallfds;
+ wfds = wallfds;
+ retval = select(clifd + 1, &rfds, &wfds, NULL, &tv);
+
+ if (retval > 0) {
+ if (FD_ISSET(clifd, &rfds)) {
+ int ret;
+ while ((ret = read_sock(&sockets[client])) == 1) {
+ int pret;
+ if ((pret = process_commands(window, client)) == 1)
+ return 1;
+ else if (pret == -1)
+ return 0;
+ }
+ /* FIXME: maybe also return on read error? or would that be dangerous? */
+ if (ret == 0) {
+ FD_CLR(clifd, &sock_state.rallfds);
+ FD_CLR(clifd, &sock_state.wallfds);
+ ltkd_widget_remove_client(client);
+ sockets[clifd].fd = -1;
+ close(clifd);
+ int newmaxsocket = -1;
+ for (int j = 0; j <= maxsocket; j++) {
+ if (sockets[j].fd >= 0)
+ newmaxsocket = j;
+ }
+ maxsocket = newmaxsocket;
+ if (maxsocket == -1) {
+ ltkd_quit();
+ break;
+ }
+ return 0;
+ }
+ }
+ if (FD_ISSET(clifd, &wfds)) {
+ /* FIXME: call in loop like above */
+ write_sock(&sockets[client]);
+ }
+ }
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ ltk_timespecsub(&now, &last, &elapsed);
+ /* FIXME: configure framerate */
+ if (elapsed.tv_sec == 0 && elapsed.tv_nsec < 20000000LL) {
+ sleep_time.tv_nsec = 20000000LL - elapsed.tv_nsec;
+ nanosleep(&sleep_time, NULL);
+ }
+ last = now;
+ }
+ return 0;
+}
+
+/* FIXME: need to remove event masks from all widgets when removing client */
+static void
+ltkd_mainloop(ltk_window *window) {
+ fd_set rfds, wfds;
+ int retval;
+ int clifd;
+ struct timeval tv;
+ tv.tv_sec = 0;
+ tv.tv_usec = 0;
+
+ FD_ZERO(&sock_state.rallfds);
+ FD_ZERO(&sock_state.wallfds);
+
+ if ((sock_state.listenfd = listen_sock(sock_path)) < 0)
+ ltkd_fatal_errno("Error listening on socket.\n");
+
+ FD_SET(sock_state.listenfd, &sock_state.rallfds);
+ sock_state.maxfd = sock_state.listenfd;
+
+ printf("%lu", ltk_renderer_get_window_id(main_window->renderwindow));
+ fflush(stdout);
+ if (daemonize_flag)
+ daemonize();
+
+ ltk_mainloop_init();
+
+ while (running) {
+ rfds = sock_state.rallfds;
+ wfds = sock_state.wallfds;
+ retval = select(sock_state.maxfd + 1, &rfds, &wfds, NULL, &tv);
+ if (retval > 0) {
+ if (FD_ISSET(sock_state.listenfd, &rfds)) {
+ if ((clifd = accept_sock(sock_state.listenfd)) < 0) {
+ /* FIXME: Just log this! */
+ ltkd_fatal_errno("Error accepting socket connection.\n");
+ }
+ int i = add_client(clifd);
+ FD_SET(clifd, &sock_state.rallfds);
+ FD_SET(clifd, &sock_state.wallfds);
+ if (clifd > sock_state.maxfd)
+ sock_state.maxfd = clifd;
+ if (i > maxsocket)
+ maxsocket = i;
+ continue;
+ }
+ for (int i = 0; i <= maxsocket; i++) {
+ if ((clifd = sockets[i].fd) < 0)
+ continue;
+ if (FD_ISSET(clifd, &rfds)) {
+ /* FIXME: better error handling - this assumes error
+ is always because read would block */
+ /* FIXME: maybe maximum number of iterations here to
+ avoid choking on a lot of data? although such a
+ large amount of data would probably cause other
+ problems anyways */
+ /* or maybe measure time and break after max time? */
+ int ret;
+ while ((ret = read_sock(&sockets[i])) == 1) {
+ process_commands(window, i);
+ }
+ if (ret == 0) {
+ ltkd_widget_remove_client(i);
+ FD_CLR(clifd, &sock_state.rallfds);
+ FD_CLR(clifd, &sock_state.wallfds);
+ sockets[i].fd = -1;
+ /* FIXME: what to do on error? */
+ close(clifd);
+ int newmaxsocket = -1;
+ for (int j = 0; j <= maxsocket; j++) {
+ if (sockets[j].fd >= 0)
+ newmaxsocket = j;
+ }
+ maxsocket = newmaxsocket;
+ if (maxsocket == -1) {
+ ltkd_quit();
+ break;
+ }
+ }
+ }
+ /* FIXME: maybe ignore SIGPIPE signal - then don't call FD_CLR
+ for wallfds above but rather when write fails with EPIPE */
+ /* -> this would possibly allow data to be written still in the
+ hypothetical scenario that only the writing end of the socket
+ is closed (and ltkd wouldn't crash if only the reading end is
+ closed) */
+ if (FD_ISSET(clifd, &wfds)) {
+ /* FIXME: also call in loop like reading above */
+ write_sock(&sockets[i]);
+ }
+ }
+ }
+
+ ltk_mainloop_step(1);
+ }
+
+ ltkd_cleanup();
+}
+
+/* largely copied from APUE */
+static void
+daemonize(void) {
+ pid_t pid;
+ struct sigaction sa;
+
+ fflush(stdout);
+ fflush(stderr);
+ fflush(ltkd_logfile);
+
+ if ((pid = fork()) < 0)
+ ltkd_fatal_errno("Can't fork.\n");
+ else if (pid != 0)
+ exit(0);
+ setsid();
+
+ sa.sa_handler = SIG_IGN;
+ sigemptyset(&sa.sa_mask);
+ sa.sa_flags = 0;
+ if (sigaction(SIGHUP, &sa, NULL) < 0)
+ ltkd_fatal_errno("Unable to ignore SIGHUP.\n");
+ if ((pid = fork()) < 0)
+ ltkd_fatal_errno("Can't fork.\n");
+ else if (pid != 0)
+ exit(0);
+
+ if (chdir("/") < 0)
+ ltkd_fatal_errno("Can't change directory to root.\n");
+
+ /* FIXME: error handling */
+ int devnull = open("/dev/null", O_RDONLY);
+ if (devnull >= 0)
+ dup2(devnull, fileno(stdin));
+ dup2(fileno(ltkd_logfile), fileno(stdout));
+ dup2(fileno(ltkd_logfile), fileno(stderr));
+}
+
+static char *
+get_sock_path(char *basedir, unsigned long id) {
+ int len;
+ char *path;
+
+ len = strlen(basedir);
+ /* FIXME: MAKE SURE THIS IS ACTUALLY BIG ENOUGH! */
+ path = ltk_malloc(len + 20);
+ /* FIXME: also check for less than 0 */
+ if (snprintf(path, len + 20, "%s/%lu.sock", basedir, id) >= len + 20)
+ ltkd_fatal("Tell lumidify to fix his code.\n");
+
+ return path;
+}
+
+static FILE *
+open_log(char *dir) {
+ FILE *f;
+ char *path;
+
+ path = ltk_strcat_useful(dir, "/ltkd.log");
+ if (!path)
+ return NULL;
+ f = fopen(path, "a");
+ if (!f) {
+ ltk_free(path);
+ return NULL;
+ }
+ ltk_free(path);
+
+ return f;
+}
+
+static void
+ltkd_cleanup(void) {
+ if (sock_path) {
+ /* FIXME: somewhat misleading warning message */
+ if (base_dir_fd >= 0)
+ unlinkat(base_dir_fd, sock_path, 0);
+ else
+ ltk_warn("Unable to remove socket file!\n");
+ ltk_free(sock_path);
+ }
+ if (base_dir_fd >= 0)
+ close(base_dir_fd);
+ if (sock_state.listenfd >= 0)
+ close(sock_state.listenfd);
+ if (ltkd_dir)
+ ltk_free(ltkd_dir);
+ if (ltkd_logfile)
+ fclose(ltkd_logfile);
+
+ for (int i = 0; i < MAX_SOCK_CONNS; i++) {
+ if (sockets[i].fd >= 0)
+ close(sockets[i].fd);
+ if (sockets[i].read)
+ ltk_free(sockets[i].read);
+ if (sockets[i].to_write)
+ ltk_free(sockets[i].to_write);
+ if (sockets[i].tokens.tokens)
+ ltk_free(sockets[i].tokens.tokens);
+ }
+
+ ltkd_widgets_cleanup();
+ main_window = NULL;
+ ltk_deinit();
+}
+
+static void
+ltkd_quit(void) {
+ running = 0;
+}
+
+static void
+ltkd_log_msg(const char *mode, const char *format, va_list args) {
+ char logtime[25]; /* FIXME: This should always be big enough, right? */
+ time_t clock;
+ struct tm *timeptr;
+
+ time(&clock);
+ timeptr = localtime(&clock);
+ strftime(logtime, 25, "%Y-%m-%d %H:%M:%S", timeptr);
+
+ if (main_window)
+ fprintf(stderr, "%s ltkd(%lu) %s: ", logtime, ltk_renderer_get_window_id(main_window->renderwindow), mode);
+ else
+ fprintf(stderr, "%s ltkd(?) %s: ", logtime, mode);
+ vfprintf(stderr, format, args);
+}
+
+static int
+ltkd_set_root_widget_cmd(
+ ltk_window *window,
+ ltkd_cmd_token *tokens,
+ int num_tokens,
+ ltkd_error *err) {
+ ltkd_widget *widget;
+ if (num_tokens != 2) {
+ err->type = ERR_INVALID_NUMBER_OF_ARGUMENTS;
+ err->arg = -1;
+ return 1;
+ } else if (tokens[1].contains_nul) {
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = 1;
+ return 1;
+ }
+ widget = ltkd_get_widget(tokens[1].text, LTK_WIDGET_UNKNOWN, err);
+ if (!widget) {
+ err->arg = 1;
+ return 1;
+ }
+ ltk_window_set_root_widget(window, widget->widget);
+
+ return 0;
+}
+
+/* Push a token onto `token_list`, resizing the buffer if necessary.
+ Returns -1 on error, 0 otherwise.
+ Note: The token is not copied, it is only added directly. */
+static int
+push_token(struct token_list *tl, char *token) {
+ int new_size;
+ if (tl->num_tokens >= tl->num_alloc) {
+ new_size = (tl->num_alloc * 2) > (tl->num_tokens + 1) ?
+ (tl->num_alloc * 2) : (tl->num_tokens + 1);
+ ltkd_cmd_token *new = ltk_reallocarray(tl->tokens, new_size, sizeof(ltkd_cmd_token));
+ if (!new) return -1;
+ tl->tokens = new;
+ tl->num_alloc = new_size;
+ }
+ tl->tokens[tl->num_tokens].text = token;
+ tl->tokens[tl->num_tokens].len = 0;
+ tl->tokens[tl->num_tokens++].contains_nul = 0;
+
+ return 0;
+}
+
+/* Add a new client to the socket list and return the index in `sockets`.
+ Returns -1 if there is no space for a new client. */
+static int
+add_client(int fd) {
+ for (int i = 0; i < MAX_SOCK_CONNS; i++) {
+ if (sockets[i].fd == -1) {
+ sockets[i].fd = fd;
+ sockets[i].event_mask = ~0; /* FIXME */
+ sockets[i].read_len = 0;
+ sockets[i].write_len = 0;
+ sockets[i].write_cur = 0;
+ sockets[i].offset = 0;
+ sockets[i].in_str = 0;
+ sockets[i].read_cur = 0;
+ sockets[i].bs = 0;
+ sockets[i].tokens.num_tokens = 0;
+ sockets[i].last_seq = 0;
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+/* largely copied from APUE */
+/* Listen on the socket at `sock_path`.
+ Returns the file descriptor of the opened socket on success.
+ Returns -1 if `sock_path` is too long
+ -2 if the socket could not be created
+ -3 if the socket could not be bound to the path
+ -4 if the socket could not be listened on */
+static int
+listen_sock(const char *sock_path) {
+ int fd, len, err, rval;
+ struct sockaddr_un un;
+
+ if (strlen(sock_path) >= sizeof(un.sun_path)) {
+ errno = ENAMETOOLONG;
+ return -1;
+ }
+
+ if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
+ return -2;
+
+ unlink(sock_path);
+
+ memset(&un, 0, sizeof(un));
+ un.sun_family = AF_UNIX;
+ strcpy(un.sun_path, sock_path);
+ len = offsetof(struct sockaddr_un, sun_path) + strlen(sock_path);
+ if (bind(fd, (struct sockaddr *)&un, len) < 0) {
+ rval = -3;
+ goto errout;
+ }
+
+ if (listen(fd, 10) < 0) {
+ rval = -4;
+ goto errout;
+ }
+
+ return fd;
+
+errout:
+ err = errno;
+ close(fd);
+ errno = err;
+ return rval;
+}
+
+/* Accept a socket connection on the listening socket `listenfd`.
+ Returns the file descriptor of the accepted client on success.
+ Returns -1 if there was an error. */
+static int
+accept_sock(int listenfd) {
+ int clifd;
+ socklen_t len;
+ struct sockaddr_un un;
+
+ len = sizeof(un);
+ if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len)) < 0) {
+ return -1;
+ }
+ if (ltkd_set_nonblock(clifd)) {
+ /* FIXME: what could even be done if close fails? */
+ close(clifd);
+ return -1;
+ }
+
+ return clifd;
+}
+
+/* Read up to READ_BLK_SIZE bytes from the socket `sock`.
+ Returns -1 if an error occurred, 0 if the connection was closed, 1 otherwise.
+ Note: Returning 1 on success is weird, but it could also be confusing to
+ return 0 on success when `read` returns that to mean that the connection
+ was closed. */
+static int
+read_sock(struct ltkd_sock_info *sock) {
+ int nread;
+ char *old = sock->read;
+ ltk_grow_string(&sock->read, &sock->read_alloc, sock->read_len + READ_BLK_SIZE);
+ /* move tokens to new addresses - this was added as an
+ afterthought and really needs to be cleaned up */
+ if (sock->read != old) {
+ for (int i = 0; i < sock->tokens.num_tokens; i++) {
+ sock->tokens.tokens[i].text = sock->read + (sock->tokens.tokens[i].text - old);
+ }
+ }
+ nread = read(sock->fd, sock->read + sock->read_len, READ_BLK_SIZE);
+ if (nread == -1 || nread == 0)
+ return nread;
+ sock->read_len += nread;
+
+ return 1;
+}
+
+/* Write up to WRITE_BLK_SIZE bytes to the socket.
+ Returns -1 on error, 0 otherwise. */
+static int
+write_sock(struct ltkd_sock_info *sock) {
+ if (sock->write_len == sock->write_cur)
+ return 0;
+ int write_len = WRITE_BLK_SIZE > sock->write_len - sock->write_cur ?
+ sock->write_len - sock->write_cur : WRITE_BLK_SIZE;
+ int nwritten = write(sock->fd, sock->to_write + sock->write_cur, write_len);
+ if (nwritten == -1)
+ return nwritten;
+ sock->write_cur += nwritten;
+
+ /* check if any sockets have text to write */
+ if (sock->write_cur == sock->write_len) {
+ int found = 0;
+ for (int i = 0; i < maxsocket; i++) {
+ if (sockets[i].fd != -1 &&
+ sockets[i].write_cur != sockets[i].write_len) {
+ found = 1;
+ break;
+ }
+ }
+ if (!found)
+ sock_write_available = 0;
+ }
+
+ return 0;
+}
+
+static void
+move_write_pos(struct ltkd_sock_info *sock) {
+ /* FIXME: also resize if too large */
+ if (sock->write_cur > 0) {
+ memmove(sock->to_write, sock->to_write + sock->write_cur,
+ sock->write_len - sock->write_cur);
+ sock->write_len -= sock->write_cur;
+ sock->write_cur = 0;
+ }
+}
+
+/* Queue `str` to be written to the socket. If len is < 0, it is set to `strlen(str)`.
+ Returns -1 on error, 0 otherwise.
+ Note: The string must include all '\n', etc. as defined in the protocol. This
+ function just adds the given string verbatim. */
+int
+ltkd_queue_sock_write(int client, const char *str, int len) {
+ if (client < 0 || client >= MAX_SOCK_CONNS || sockets[client].fd == -1)
+ return 1;
+ /* this is always large enough to hold a uint32_t and " \0" */
+ char num[12];
+ struct ltkd_sock_info *sock = &sockets[client];
+ move_write_pos(sock);
+ if (len < 0)
+ len = strlen(str);
+
+ int numlen = snprintf(num, sizeof(num), "%"PRIu32" ", sock->last_seq);
+ if (numlen < 0 || (unsigned)numlen >= sizeof(num))
+ ltkd_fatal("There's a bug in the universe.\n");
+ if (sock->write_alloc - sock->write_len < len + numlen)
+ ltk_grow_string(&sock->to_write, &sock->write_alloc, sock->write_len + len + numlen);
+
+ (void)strncpy(sock->to_write + sock->write_len, num, numlen);
+ (void)strncpy(sock->to_write + sock->write_len + numlen, str, len);
+ sock->write_len += len + numlen;
+
+ sock_write_available = 1;
+
+ return 0;
+}
+
+int
+ltkd_queue_sock_write_fmt(int client, const char *fmt, ...) {
+ if (client < 0 || client >= MAX_SOCK_CONNS || sockets[client].fd == -1)
+ return 1;
+ struct ltkd_sock_info *sock = &sockets[client];
+ /* just to print the sequence number */
+ ltkd_queue_sock_write(client, "", 0);
+ va_list args;
+ va_start(args, fmt);
+ int len = vsnprintf(sock->to_write + sock->write_len, sock->write_alloc - sock->write_len, fmt, args);
+ if (len < 0) {
+ ltkd_fatal("Unable to print formatted text to socket.\n");
+ } else if (len >= sock->write_alloc - sock->write_len) {
+ va_end(args);
+ va_start(args, fmt);
+ /* snprintf always writes '\0', even though we don't actually need it here */
+ ltk_grow_string(&sock->to_write, &sock->write_alloc, sock->write_len + len + 1);
+ vsnprintf(sock->to_write + sock->write_len, sock->write_alloc - sock->write_len, fmt, args);
+ }
+ va_end(args);
+ sock->write_len += len;
+ sock_write_available = 1;
+
+ return 0;
+}
+
+/* Tokenize the current read buffer in `sock`.
+ Returns 0 immediately if the end of a command was encountered, 1 otherwise. */
+static int
+tokenize_command(struct ltkd_sock_info *sock) {
+ for (; sock->read_cur < sock->read_len; sock->read_cur++) {
+ /* FIXME: strip extra whitespace? Or maybe just have a "hard" protocol where it always has to be one space. */
+ if (!sock->in_token) {
+ push_token(&sock->tokens, sock->read + sock->read_cur - sock->offset);
+ sock->in_token = 1;
+ }
+ if (sock->read[sock->read_cur] == '\\') {
+ sock->bs++;
+ if (sock->bs / 2)
+ sock->offset++;
+ else
+ sock->tokens.tokens[sock->tokens.num_tokens - 1].len++;
+ sock->bs %= 2;
+ sock->read[sock->read_cur-sock->offset] = '\\';
+ } else if (sock->read[sock->read_cur] == '\n' && !sock->in_str) {
+ sock->read[sock->read_cur-sock->offset] = '\0';
+ sock->read_cur++;
+ sock->offset = 0;
+ sock->in_token = 0;
+ sock->bs = 0;
+ return 0;
+ } else if (sock->read[sock->read_cur] == '"') {
+ sock->offset++;
+ if (sock->bs) {
+ sock->read[sock->read_cur-sock->offset] = '"';
+ sock->bs = 0;
+ } else {
+ sock->in_str = !sock->in_str;
+ }
+ } else if (sock->read[sock->read_cur] == ' ' && !sock->in_str) {
+ sock->read[sock->read_cur-sock->offset] = '\0';
+ sock->in_token = !sock->in_token;
+ sock->bs = 0;
+ } else {
+ sock->read[sock->read_cur-sock->offset] = sock->read[sock->read_cur];
+ /* FIXME: assert that num_tokens > 0 */
+ sock->tokens.tokens[sock->tokens.num_tokens - 1].len++;
+ if (sock->read[sock->read_cur] == '\0')
+ sock->tokens.tokens[sock->tokens.num_tokens - 1].contains_nul = 1;
+ sock->bs = 0;
+ }
+ }
+
+ return 1;
+}
+
+/* FIXME: currently no type-checking when setting specific widget mask */
+/* FIXME: this is really ugly and inefficient right now - it will be replaced with something
+ more generic at some point (or maybe just with a binary protocol?) */
+static int
+handle_mask_command(int client, ltkd_cmd_token *tokens, size_t num_tokens, ltkd_error *err) {
+ if (num_tokens != 4 && num_tokens != 5) {
+ err->type = ERR_INVALID_NUMBER_OF_ARGUMENTS;
+ err->arg = -1;
+ return 1;
+ }
+ /* FIXME: make this nicer */
+ /* -> use generic cmd handling like the widgets */
+ if (tokens[1].contains_nul) {
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = 1;
+ return 1;
+ } else if (tokens[2].contains_nul) {
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = 2;
+ return 1;
+ } else if (tokens[3].contains_nul) {
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = 3;
+ return 1;
+ } else if (num_tokens == 5 && tokens[4].contains_nul) {
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = 4;
+ return 1;
+ }
+ uint32_t mask = 0;
+ int lock = 0;
+ int special = 0;
+ ltkd_widget *widget = ltkd_get_widget(tokens[1].text, LTK_WIDGET_UNKNOWN, err);
+ if (!widget) {
+ err->arg = 1;
+ return 1;
+ }
+ if (!strcmp(tokens[2].text, "widget")) {
+ if (!strcmp(tokens[3].text, "mousepress")) {
+ mask = LTKD_PEVENTMASK_MOUSEPRESS;
+ } else if (!strcmp(tokens[3].text, "mouserelease")) {
+ mask = LTKD_PEVENTMASK_MOUSERELEASE;
+ } else if (!strcmp(tokens[3].text, "mousemotion")) {
+ mask = LTKD_PEVENTMASK_MOUSEMOTION;
+ } else if (!strcmp(tokens[3].text, "resize")) {
+ mask = LTKD_PEVENTMASK_RESIZE;
+ } else if (!strcmp(tokens[3].text, "statechange")) {
+ mask = LTKD_PEVENTMASK_STATECHANGE;
+ } else if (!strcmp(tokens[3].text, "none")) {
+ mask = LTKD_PEVENTMASK_NONE;
+ } else {
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = 3;
+ return 1;
+ }
+ } else if (!strcmp(tokens[2].text, "menuentry")) {
+ if (!strcmp(tokens[3].text, "press")) {
+ mask = LTKD_PWEVENTMASK_MENUENTRY_PRESS;
+ } else if (!strcmp(tokens[3].text, "none")) {
+ mask = LTKD_PWEVENTMASK_MENUENTRY_NONE;
+ } else {
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = 3;
+ return 1;
+ }
+ special = 1;
+ } else if (!strcmp(tokens[2].text, "button")) {
+ if (!strcmp(tokens[3].text, "press")) {
+ mask = LTKD_PWEVENTMASK_BUTTON_PRESS;
+ } else if (!strcmp(tokens[3].text, "none")) {
+ mask = LTKD_PWEVENTMASK_BUTTON_NONE;
+ } else {
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = 3;
+ return 1;
+ }
+ special = 1;
+ } else {
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = 2;
+ return 1;
+ }
+ if (num_tokens == 5) {
+ if (!strcmp(tokens[4].text, "lock")) {
+ lock = 1;
+ } else {
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = 4;
+ return 1;
+ }
+ }
+
+ if (!strcmp(tokens[0].text, "mask-add")) {
+ if (lock) {
+ if (special)
+ ltkd_widget_add_to_event_lwmask(widget, client, mask);
+ else
+ ltkd_widget_add_to_event_lmask(widget, client, mask);
+ } else {
+ if (special)
+ ltkd_widget_add_to_event_wmask(widget, client, mask);
+ else
+ ltkd_widget_add_to_event_mask(widget, client, mask);
+ }
+ } else if (!strcmp(tokens[0].text, "mask-set")) {
+ if (lock) {
+ if (special)
+ ltkd_widget_set_event_lwmask(widget, client, mask);
+ else
+ ltkd_widget_set_event_lmask(widget, client, mask);
+ } else {
+ if (special)
+ ltkd_widget_set_event_wmask(widget, client, mask);
+ else
+ ltkd_widget_set_event_mask(widget, client, mask);
+ }
+ } else if (!strcmp(tokens[0].text, "mask-remove")) {
+ if (lock) {
+ if (special)
+ ltkd_widget_remove_from_event_lwmask(widget, client, mask);
+ else
+ ltkd_widget_remove_from_event_lmask(widget, client, mask);
+ } else {
+ if (special)
+ ltkd_widget_remove_from_event_wmask(widget, client, mask);
+ else
+ ltkd_widget_remove_from_event_mask(widget, client, mask);
+ }
+ } else {
+ err->type = ERR_INVALID_COMMAND;
+ err->arg = 0;
+ return 1;
+ }
+ return 0;
+}
+
+/* Process the commands as they are read from the socket. */
+/* Returns 1 if command was 'event-unlock true',
+ -1 if command was 'event-unlock false', 0 otherwise. */
+static int
+process_commands(ltk_window *window, int client) {
+ if (client < 0 || client >= MAX_SOCK_CONNS || sockets[client].fd == -1)
+ return 0;
+ struct ltkd_sock_info *sock = &sockets[client];
+ ltkd_cmd_token *tokens;
+ int num_tokens;
+ ltkd_error errdetail = {ERR_NONE, -1};
+ int err;
+ int retval = 0;
+ int last = 0;
+ uint32_t seq;
+ const char *errstr;
+ int contains_nul = 0;
+ while (!tokenize_command(sock)) {
+ contains_nul = 0;
+ err = 0;
+ tokens = sock->tokens.tokens;
+ num_tokens = sock->tokens.num_tokens;
+ if (num_tokens < 2) {
+ errdetail.type = ERR_INVALID_COMMAND;
+ errdetail.arg = -1;
+ err = 1;
+ } else {
+ contains_nul = tokens[0].contains_nul;
+ seq = (uint32_t)ltk_strtonum(tokens[0].text, 0, UINT32_MAX, &errstr);
+ tokens++;
+ num_tokens--;
+ if (errstr || contains_nul) {
+ errdetail.type = ERR_INVALID_SEQNUM;
+ errdetail.arg = -1;
+ err = 1;
+ seq = sock->last_seq;
+ } else if (tokens[0].contains_nul) {
+ errdetail.type = ERR_INVALID_ARGUMENT;
+ errdetail.arg = 0;
+ err = 1;
+ seq = sock->last_seq;
+ } else if (strcmp(tokens[0].text, "set-root-widget") == 0) {
+ err = ltkd_set_root_widget_cmd(window, tokens, num_tokens, &errdetail);
+ } else if (strcmp(tokens[0].text, "quit") == 0) {
+ ltkd_quit();
+ last = 1;
+ } else if (strcmp(tokens[0].text, "destroy") == 0) {
+ err = ltkd_widget_destroy_cmd(window, tokens, num_tokens, &errdetail);
+ } else if (strncmp(tokens[0].text, "mask", 4) == 0) {
+ err = handle_mask_command(client, tokens, num_tokens, &errdetail);
+ } else if (strcmp(tokens[0].text, "event-unlock") == 0) {
+ if (num_tokens != 2) {
+ errdetail.type = ERR_INVALID_NUMBER_OF_ARGUMENTS;
+ errdetail.arg = -1;
+ err = 1;
+ } else if (!tokens[1].contains_nul && strcmp(tokens[1].text, "true") == 0) {
+ retval = 1;
+ } else if (!tokens[1].contains_nul && strcmp(tokens[1].text, "false") == 0) {
+ retval = -1;
+ } else {
+ err = 1;
+ errdetail.type = ERR_INVALID_ARGUMENT;
+ errdetail.arg = 1;
+ }
+ last = 1;
+ } else {
+ int found = 0;
+ for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
+ if (widget_funcs[i].cmd && !strcmp(tokens[0].text, widget_funcs[i].name)) {
+ err = widget_funcs[i].cmd(window, tokens, num_tokens, &errdetail);
+ found = 1;
+ }
+ }
+ if (!found) {
+ errdetail.type = ERR_INVALID_COMMAND;
+ errdetail.arg = -1;
+ err = 1;
+ }
+ }
+ sock->tokens.num_tokens = 0;
+ sock->last_seq = seq;
+ }
+ if (err) {
+ const char *errmsg = errtype_to_string(errdetail.type);
+ if (ltkd_queue_sock_write_fmt(client, "err %d %d \"%s\"\n", errdetail.type, errdetail.arg, errmsg))
+ ltkd_fatal("Unable to queue socket write.\n");
+ } else {
+ if (ltkd_queue_sock_write(client, "res ok\n", -1)) {
+ ltkd_fatal("Unable to queue socket write.\n");
+ }
+ }
+ if (last)
+ break;
+ }
+ if (sock->tokens.num_tokens > 0 && sock->tokens.tokens[0].text != sock->read) {
+ memmove(sock->read, sock->tokens.tokens[0].text, sock->read + sock->read_len - sock->tokens.tokens[0].text);
+ ptrdiff_t offset = sock->tokens.tokens[0].text - sock->read;
+ /* Hmm, seems a bit ugly... */
+ for (int i = 0; i < sock->tokens.num_tokens; i++) {
+ sock->tokens.tokens[i].text -= offset;
+ }
+ sock->read_len -= offset;
+ sock->read_cur -= offset;
+ } else if (sock->tokens.num_tokens == 0) {
+ sock->read_len = 0;
+ sock->read_cur = 0;
+ }
+ return retval;
+}
+
+LTK_GEN_LOG_FUNCS(ltkd, ltkd_log_msg, ltkd_cleanup)
diff --git a/src/ltkd/ltkd.h b/src/ltkd/ltkd.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LTKD_H
+#define LTKD_H
+
+#include <stddef.h>
+#include <ltk/ltk.h>
+
+typedef struct {
+ char *text;
+ size_t len;
+ int contains_nul;
+} ltkd_cmd_token;
+
+typedef enum {
+ LTK_EVENT_RESIZE = 1 << 0,
+ LTK_EVENT_BUTTON = 1 << 1,
+ LTK_EVENT_KEY = 1 << 2,
+ LTK_EVENT_MENU = 1 << 3
+} ltkd_userevent_type;
+
+void ltkd_queue_event(ltk_window *window, ltkd_userevent_type type, const char *id, const char *data);
+int ltkd_handle_lock_client(ltk_window *window, int client);
+int ltkd_queue_sock_write(int client, const char *str, int len);
+int ltkd_queue_sock_write_fmt(int client, const char *fmt, ...);
+
+LTK_GEN_LOG_FUNC_PROTO(ltkd)
+
+#endif /* LTKD_H */
diff --git a/src/ltkd/menu.c b/src/ltkd/menu.c
@@ -0,0 +1,272 @@
+/*
+ * Copyright (c) 2022-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <string.h>
+
+#include "err.h"
+#include "ltkd.h"
+#include "widget.h"
+#include "cmd.h"
+#include "proto_types.h"
+
+#include <ltk/ltk.h>
+#include <ltk/util.h>
+#include <ltk/menu.h>
+
+/* [sub]menu <menu id> create */
+static int
+ltkd_menu_cmd_create(
+ ltk_window *window,
+ ltkd_widget *widget_unneeded,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)widget_unneeded;
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_STRING, .optional = 0},
+ {.type = CMDARG_STRING, .optional = 0},
+ {.type = CMDARG_IGNORE, .optional = 0},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ ltk_menu *menu;
+ if (!strcmp(cmd[0].val.str, "menu")) {
+ menu = ltk_menu_create(window);
+ } else {
+ menu = ltk_submenu_create(window);
+ }
+ if (!ltkd_widget_create(LTK_CAST_WIDGET(menu), cmd[1].val.str, NULL, 0, err)) {
+ ltk_widget_destroy(LTK_CAST_WIDGET(menu), 1);
+ err->arg = 1;
+ return 1;
+ }
+ return 0;
+}
+
+/* menu <menu id> insert-entry <entry widget id> <index> */
+static int
+ltkd_menu_cmd_insert_entry(
+ ltk_window *window,
+ ltkd_widget *widget,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)window;
+ ltk_menu *menu = LTK_CAST_MENU(widget->widget);
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_MENUENTRY, .optional = 0},
+ {.type = CMDARG_INT, .min = 0, .max = menu->num_entries, .optional = 0},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ int ret;
+ if ((ret = ltk_menu_insert_entry(menu, LTK_CAST_MENUENTRY(cmd[0].val.widget->widget), cmd[1].val.i))) {
+ err->type = ret == 1 ? ERR_WIDGET_IN_CONTAINER : ERR_INVALID_INDEX;
+ err->arg = err->type == ERR_WIDGET_IN_CONTAINER ? 0 : 1;
+ return 1;
+ }
+ return 0;
+}
+
+/* menu <menu id> add-entry <entry widget id> */
+static int
+ltkd_menu_cmd_add_entry(
+ ltk_window *window,
+ ltkd_widget *widget,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)window;
+ ltk_menu *menu = LTK_CAST_MENU(widget->widget);
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_MENUENTRY, .optional = 0},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ if (ltk_menu_add_entry(menu, LTK_CAST_MENUENTRY(cmd[0].val.widget->widget))) {
+ err->type = ERR_WIDGET_IN_CONTAINER;
+ err->arg = 0;
+ return 1;
+ }
+ return 0;
+}
+
+/* menu <menu id> remove-entry-index <entry index> */
+static int
+ltkd_menu_cmd_remove_entry_index(
+ ltk_window *window,
+ ltkd_widget *widget,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)window;
+ ltk_menu *menu = LTK_CAST_MENU(widget->widget);
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_INT, .min = 0, .max = menu->num_entries - 1, .optional = 0},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ if (!ltk_menu_remove_entry_index(menu, cmd[0].val.i)) {
+ err->type = ERR_WIDGET_NOT_IN_CONTAINER;
+ err->arg = 0;
+ return 1;
+ }
+ return 0;
+}
+
+/* menu <menu id> remove-entry-id <entry id> */
+static int
+ltkd_menu_cmd_remove_entry_id(
+ ltk_window *window,
+ ltkd_widget *widget,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)window;
+ ltk_menu *menu = LTK_CAST_MENU(widget->widget);
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_MENUENTRY, .optional = 0},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ ltk_menuentry *entry = LTK_CAST_MENUENTRY(cmd[0].val.widget->widget);
+ if (!ltk_menu_remove_entry(menu, entry)) {
+ err->type = ERR_WIDGET_NOT_IN_CONTAINER;
+ err->arg = 0;
+ return 1;
+ }
+ return 0;
+}
+
+/* menu <menu id> remove-all-entries */
+static int
+ltkd_menu_cmd_remove_all_entries(
+ ltk_window *window,
+ ltkd_widget *widget,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)window;
+ (void)tokens;
+ (void)num_tokens;
+ (void)err;
+ ltk_menu *menu = LTK_CAST_MENU(widget->widget);
+ ltk_menu_remove_all_entries(menu);
+ return 0;
+}
+
+static int
+ltkd_menuentry_press(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg) {
+ (void)widget_unused;
+ (void)args;
+ ltkd_widget *widget = LTK_CAST_ARG_VOIDP(arg);
+ return ltkd_widget_queue_specific_event(widget, "menuentry", LTKD_PWEVENTMASK_MENUENTRY_PRESS, "press");
+}
+
+static ltkd_event_handler entry_handlers[] = {
+ {<kd_menuentry_press, LTK_MENUENTRY_SIGNAL_PRESSED},
+};
+
+/* menuentry <id> create <text> */
+static int
+ltkd_menuentry_cmd_create(
+ ltk_window *window,
+ ltkd_widget *widget_unneeded,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)widget_unneeded;
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_STRING, .optional = 0},
+ {.type = CMDARG_STRING, .optional = 0},
+ {.type = CMDARG_IGNORE, .optional = 0},
+ {.type = CMDARG_STRING, .optional = 0},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ ltk_menuentry *e = ltk_menuentry_create(window, cmd[3].val.str);
+ if (!ltkd_widget_create(LTK_CAST_WIDGET(e), cmd[1].val.str, entry_handlers, LENGTH(entry_handlers), err)) {
+ ltk_widget_destroy(LTK_CAST_WIDGET(e), 1);
+ err->arg = 1;
+ return 1;
+ }
+ return 0;
+}
+
+/* menuentry <menuentry id> attach-submenu <submenu id> */
+static int
+ltkd_menuentry_cmd_attach_submenu(
+ ltk_window *window,
+ ltkd_widget *widget,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)window;
+ ltk_menuentry *e = LTK_CAST_MENUENTRY(widget->widget);
+ ltkd_cmdarg_parseinfo cmd[] = {
+ {.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_MENU, .optional = 0},
+ };
+ if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
+ return 1;
+ int ret;
+ if ((ret = ltk_menuentry_attach_submenu(e, LTK_CAST_MENU(cmd[0].val.widget->widget)))) {
+ /* FIXME: allow setting err->arg to arg before the args given to function */
+ /*err->arg = err->type == ERR_MENU_NOT_SUBMENU ? 0 : -2;*/
+ err->type = ret == 1 ? ERR_MENU_NOT_SUBMENU : ERR_MENU_ENTRY_CONTAINS_SUBMENU;
+ err->arg = 0;
+ return 1;
+ }
+ return 0;
+}
+
+/* menuentry <menuentry id> detach-submenu */
+static int
+ltkd_menuentry_cmd_detach_submenu(
+ ltk_window *window,
+ ltkd_widget *widget,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)window;
+ (void)tokens;
+ (void)num_tokens;
+ (void)err;
+ ltk_menuentry *e = LTK_CAST_MENUENTRY(widget->widget);
+ ltk_menuentry_detach_submenu(e);
+ return 0;
+}
+
+/* FIXME: sort out menu/submenu - it's weird right now */
+/* FIXME: distinguish between menu/submenu in commands other than create? */
+
+static ltkd_cmd_info menu_cmds[] = {
+ {"add-entry", <kd_menu_cmd_add_entry, 0},
+ {"create", <kd_menu_cmd_create, 1},
+ {"insert-entry", <kd_menu_cmd_insert_entry, 0},
+ {"remove-all-entries", <kd_menu_cmd_remove_all_entries, 0},
+ {"remove-entry-index", <kd_menu_cmd_remove_entry_index, 0},
+ {"remove-entry-id", <kd_menu_cmd_remove_entry_id, 0},
+};
+
+static ltkd_cmd_info menuentry_cmds[] = {
+ {"attach-submenu", <kd_menuentry_cmd_attach_submenu, 0},
+ {"create", <kd_menuentry_cmd_create, 1},
+ {"detach-submenu", <kd_menuentry_cmd_detach_submenu, 0},
+};
+
+GEN_CMD_HELPERS(ltkd_menu_cmd, LTK_WIDGET_MENU, menu_cmds)
+GEN_CMD_HELPERS(ltkd_menuentry_cmd, LTK_WIDGET_MENUENTRY, menuentry_cmds)
diff --git a/src/ltkd/proto_types.h b/src/ltkd/proto_types.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2022-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LTKD_PROTO_TYPES_H
+#define LTKD_PROTO_TYPES_H
+
+/* P == protocol; W == widget */
+
+#define LTKD_PEVENT_MOUSEPRESS 0
+#define LTKD_PEVENT_MOUSERELEASE 1
+#define LTKD_PEVENT_MOUSEMOTION 2
+#define LTKD_PEVENT_MOUSESCROLL 3
+#define LTKD_PEVENT_KEYPRESS 4
+#define LTKD_PEVENT_KEYRELEASE 5
+#define LTKD_PEVENT_RESIZE 6
+#define LTKD_PEVENT_STATECHANGE 7
+
+/* FIXME: standardize names - internally, buttonpress is used, here it's mousepress... */
+#define LTKD_PEVENTMASK_NONE (UINT32_C(0))
+#define LTKD_PEVENTMASK_MOUSEPRESS (UINT32_C(1) << LTKD_PEVENT_MOUSEPRESS)
+#define LTKD_PEVENTMASK_MOUSERELEASE (UINT32_C(1) << LTKD_PEVENT_MOUSERELEASE)
+#define LTKD_PEVENTMASK_MOUSEMOTION (UINT32_C(1) << LTKD_PEVENT_MOUSEMOTION)
+#define LTKD_PEVENTMASK_KEYPRESS (UINT32_C(1) << LTKD_PEVENT_KEYPRESS)
+#define LTKD_PEVENTMASK_KEYRELEASE (UINT32_C(1) << LTKD_PEVENT_KEYRELEASE)
+#define LTKD_PEVENTMASK_RESIZE (UINT32_C(1) << LTKD_PEVENT_RESIZE)
+#define LTKD_PEVENTMASK_EXPOSE (UINT32_C(1) << LTKD_PEVENT_EXPOSE)
+#define LTKD_PEVENTMASK_STATECHANGE (UINT32_C(1) << LTKD_PEVENT_STATECHANGE)
+#define LTKD_PEVENTMASK_MOUSESCROLL (UINT32_C(1) << LTKD_PEVENT_MOUSESCROLL)
+
+#define LTKD_PWEVENT_MENUENTRY_PRESS 0
+#define LTKD_PWEVENTMASK_MENUENTRY_NONE (UINT32_C(0))
+#define LTKD_PWEVENTMASK_MENUENTRY_PRESS (UINT32_C(1) << LTKD_PWEVENT_MENUENTRY_PRESS)
+
+#define LTKD_PWEVENT_BUTTON_PRESS 0
+#define LTKD_PWEVENTMASK_BUTTON_NONE (UINT32_C(0))
+#define LTKD_PWEVENTMASK_BUTTON_PRESS (UINT32_C(1) << LTKD_PWEVENT_BUTTON_PRESS)
+
+#endif /* LTKD_PROTO_TYPES_H */
diff --git a/src/ltkd/socket_format.txt b/src/ltkd/socket_format.txt
@@ -0,0 +1,106 @@
+General:
+
+All requests, responses, errors, and events start with a sequence number.
+This number starts at 0 and is incremented by the client with each request.
+When the server sends a response, error, or event, it starts with the last
+sequence number that the client sent. The client 'ltkc' adds sequence
+numbers automatically (perhaps it should hide them in its output as well,
+but I'm too lazy to implement that right now). A more advanced client could
+use the numbers to properly check that requests really were received.
+It isn't clear yet what should happen in the pathological case that the
+uint32_t used to store the sequence number overflows before any of the
+requests have been handled (i.e. it isn't clear anymore which request a
+reply is for). I guess this could technically happen when running over a
+broken connection or something, but I don't have a solution right now.
+It doesn't seem like a very realistic scenario, though, considering that
+all the requests/responses would need to be buffered somewhere, which
+would be somewhat unrealistic considering the size of uint32_t.
+
+Requests:
+
+<widget type> <widget id> <command> <args>
+> grid grd1 create 2 2
+
+If the command takes a string, the string may contain newlines:
+> button btn1 create "I'm a
+> button!"
+
+The command line is read until the first newline that is not
+within a string.
+
+Double quotes must be escaped in strings, like so:
+> button btn1 create "Bla\"bla"
+
+Essentially, the individual messages are separated by line
+breaks (\n), but line breaks within strings don't break the
+message.
+
+Responses:
+
+Not properly implemented yet.
+Currently, all requests that don't generate errors just get the response
+"<sequence> res ok". Of course, this will be changed once other response
+types are available (e.g. get-text and others).
+
+It might be good to allow ignoring responses to avoid lots of useless traffic.
+On the client side, the usage could be similar to XCB's checked/unchecked.
+
+Errors:
+
+err <error number> <number of bad argument or -1> <string description of error>
+
+Events:
+
+event[l] <widget id> <widget type or "widget" for generic events> <event name> [further data]
+
+By default, no events are reported. An event mask has to be set first:
+
+mask-add <widget id> <type> <event name> [lock]
+mask-remove <widget id> <type> <event name> [lock]
+mask-set <widget id> <type> <event name> [lock]
+
+<type> is either "widget" for generic events (mousepress, mouserelease,
+mousemotion, resize, statechange) or the widget type for specific events.
+The only specific event currently supported is "press" for both "button"
+and "menuentry". Note that currently, only a single mask type can be
+added/removed at once instead of combining multiple in one request
+(i.e. it isn't possible to do something like "mousepress|mouserelease" yet).
+
+If "lock" is set, the "lock mask" is manipulated instead of the normal one.
+If an event occurs that is included in the lock mask, the event will start
+with "eventl" instead of "event", and the ltk server blocks until it gets the
+request "event-unlock [true/false]" from the client that received the event.
+Note that if multiple clients have an event in their lock mask, all of them will
+receive the event, but only one of them is chosen to listen for the event-unlock
+(basically, this is unspecified behavior that should be avoided). If event-unlock
+includes "true", the event is not processed further by the ltk server. If "false"
+is given instead, the event is processed as usual by the ltk server.
+
+This was added to allow functionality like in regular GUI toolkits where it is
+possible to override events completely. The problem is that it currently isn't
+really clear where exactly the command should be emitted and whether it really
+makes sense to block all further processing (some processing has to be done
+even now for it to make any sense at all). That could possibly lead to very
+weird bugs. It also currently isn't possible to do much after locking because
+no useful low-level functions for widgets exist (yet?). All in all, I'm not
+entirely sure how to make this work nicely so it is actually useful.
+Since all of this is pushed over a socket and will probably be able to run
+over a network connection eventually, it will also cause problems with latency.
+
+Miscellaneous:
+
+It probably isn't too great for security when anyone can do anything with the
+window. Maybe it would be better to allow different clients to have different
+permissions? For instance, maybe only the main client could change things, but
+other clients could have readonly permissions for things like screenreaders.
+That would probably get very over-complicated, though.
+
+I'm also seriously considering switching to a binary socket format. It's nice
+to have a text format, but it's an absolute pain to process, because everything
+has to be converted from/to text. It also isn't nearly as efficient, especially
+if more complicated things are done, such as listening for all mousemotion events.
+Of course, it could be made much more efficient with various lookup tables
+(it isn't implemented very efficiently currently), but still not nearly as good
+as a binary protocol. The idea would be to have a binary protocol, but to still
+have something like ltkc that converts the protocol to a text format so simple
+shell clients can still exist, but more complicated programs aren't hindered by it.
diff --git a/src/ltkd/util.c b/src/ltkd/util.c
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2023-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <fcntl.h>
+#include <errno.h>
+
+#include "ltkd.h"
+#include "util.h"
+
+int
+ltkd_set_nonblock(int fd) {
+ int flags = fcntl(fd, F_GETFL, 0);
+ if (flags == -1)
+ return -1;
+ if (fcntl(fd, F_SETFL, flags | O_NONBLOCK))
+ return -1;
+ return 0;
+}
diff --git a/src/ltkd/util.h b/src/ltkd/util.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2023-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LTKD_UTIL_H
+#define LTKD_UTIL_H
+
+int ltkd_set_nonblock(int fd);
+
+#endif /* LTKD_UTIL_H */
diff --git a/src/ltkd/widget.c b/src/ltkd/widget.c
@@ -0,0 +1,553 @@
+/*
+ * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <string.h>
+
+#include "khash.h"
+
+#include "err.h"
+#include "ltkd.h"
+#include "widget.h"
+#include "proto_types.h"
+
+#include <ltk/ltk.h>
+#include <ltk/event.h>
+#include <ltk/eventdefs.h>
+#include <ltk/memory.h>
+#include <ltk/rect.h>
+#include <ltk/util.h>
+
+KHASH_MAP_INIT_STR(widget, ltkd_widget *)
+static khash_t(widget) *widget_hash = NULL;
+/* Hack to make ltkd_destroy_widget_hash work */
+/* FIXME: any better way to do this? */
+static int hash_locked = 0;
+
+static int ltkd_widget_button_event(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg);
+static int ltkd_widget_motion_notify(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg);
+static int ltkd_widget_scroll_event(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg);
+static int ltkd_widget_resize(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg);
+static int ltkd_widget_change_state(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg);
+
+/* FIXME: generic event handling functions that take the actual uint32_t event type, etc. */
+int
+ltkd_widget_queue_specific_event(ltkd_widget *widget, const char *type, uint32_t mask, const char *data) {
+ for (size_t i = 0; i < widget->masks_num; i++) {
+ if (widget->event_masks[i].lwmask & mask) {
+ ltkd_queue_sock_write_fmt(
+ widget->event_masks[i].client,
+ "eventl %s %s %s\n", widget->id, type, data
+ );
+ if (ltkd_handle_lock_client(widget->widget->window, widget->event_masks[i].client))
+ return 1;
+ } else if (widget->event_masks[i].wmask & mask) {
+ ltkd_queue_sock_write_fmt(
+ widget->event_masks[i].client,
+ "event %s %s %s\n", widget->id, type, data
+ );
+ }
+ }
+ return 0;
+}
+
+static int
+ltkd_widget_resize(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg) {
+ (void)widget_unused;
+ (void)args;
+ ltkd_widget *widget = LTK_CAST_ARG_VOIDP(arg);
+ for (size_t i = 0; i < widget->masks_num; i++) {
+ if (widget->event_masks[i].lmask & LTKD_PEVENTMASK_RESIZE) {
+ ltkd_queue_sock_write_fmt(
+ widget->event_masks[i].client,
+ "eventl %s widget configure %d %d %d %d\n",
+ widget->id, widget->widget->lrect.x, widget->widget->lrect.y,
+ widget->widget->lrect.w, widget->widget->lrect.h
+ );
+ if (ltkd_handle_lock_client(widget->widget->window, widget->event_masks[i].client))
+ return 1;
+ } else if (widget->event_masks[i].mask & LTKD_PEVENTMASK_RESIZE) {
+ ltkd_queue_sock_write_fmt(
+ widget->event_masks[i].client,
+ "event %s widget configure %d %d %d %d\n",
+ widget->id, widget->widget->lrect.x, widget->widget->lrect.y,
+ widget->widget->lrect.w, widget->widget->lrect.h
+ );
+ }
+ }
+ return 0;
+}
+
+static int
+ltkd_widget_change_state(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg) {
+ (void)widget_unused;
+ (void)args;
+ ltkd_widget *widget = LTK_CAST_ARG_VOIDP(arg);
+ /* FIXME: give old and new state in event */
+ for (size_t i = 0; i < widget->masks_num; i++) {
+ if (widget->event_masks[i].lmask & LTKD_PEVENTMASK_STATECHANGE) {
+ ltkd_queue_sock_write_fmt(
+ widget->event_masks[i].client,
+ "eventl %s widget statechange\n", widget->id
+ );
+ if (ltkd_handle_lock_client(widget->widget->window, widget->event_masks[i].client))
+ return 1;
+ } else if (widget->event_masks[i].mask & LTKD_PEVENTMASK_STATECHANGE) {
+ ltkd_queue_sock_write_fmt(
+ widget->event_masks[i].client,
+ "event %s widget statechange\n", widget->id
+ );
+ }
+ }
+ return 0;
+}
+
+static void
+ltkd_destroy_widget_hash(void) {
+ hash_locked = 1;
+ khint_t k;
+ ltkd_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));
+ ltkd_widget_destroy(ptr, 1);
+ }
+ }
+ kh_destroy(widget, widget_hash);
+ widget_hash = NULL;
+ hash_locked = 0;
+}
+
+void
+ltkd_widgets_cleanup(void) {
+ if (widget_hash)
+ ltkd_destroy_widget_hash();
+}
+
+static client_event_mask *
+get_mask_struct(ltkd_widget *widget, int client) {
+ for (size_t i = 0; i < widget->masks_num; i++) {
+ if (widget->event_masks[i].client == client)
+ return &widget->event_masks[i];
+ }
+ widget->masks_alloc = ideal_array_size(widget->masks_alloc, widget->masks_num + 1);
+ widget->event_masks = ltk_reallocarray(widget->event_masks, widget->masks_alloc, sizeof(client_event_mask));
+ client_event_mask *m = &widget->event_masks[widget->masks_num];
+ widget->masks_num++;
+ m->client = client;
+ m->mask = m->lmask = m->wmask = m->lwmask = 0;
+ return m;
+}
+
+static ltkd_event_handler widget_handlers[] = {
+ {<kd_widget_button_event, LTK_WIDGET_SIGNAL_MOUSE_PRESS},
+ {<kd_widget_button_event, LTK_WIDGET_SIGNAL_MOUSE_RELEASE},
+ {<kd_widget_motion_notify, LTK_WIDGET_SIGNAL_MOTION_NOTIFY},
+ {<kd_widget_scroll_event, LTK_WIDGET_SIGNAL_MOUSE_SCROLL},
+ {NULL, LTK_WIDGET_SIGNAL_KEY_PRESS}, /* FIXME: add key press here */
+ {NULL, LTK_WIDGET_SIGNAL_KEY_RELEASE},
+ {<kd_widget_resize, LTK_WIDGET_SIGNAL_RESIZE},
+ {<kd_widget_change_state, LTK_WIDGET_SIGNAL_CHANGE_STATE},
+};
+
+static uint32_t
+get_widget_mask(ltkd_widget *widget) {
+ uint32_t cur_mask = 0;
+ for (size_t i = 0; i < widget->masks_num; i++) {
+ cur_mask |= widget->event_masks[i].mask;
+ cur_mask |= widget->event_masks[i].lmask;
+ }
+ return cur_mask;
+}
+
+static uint32_t
+get_widget_special_mask(ltkd_widget *widget) {
+ uint32_t cur_mask = 0;
+ for (size_t i = 0; i < widget->masks_num; i++) {
+ cur_mask |= widget->event_masks[i].wmask;
+ cur_mask |= widget->event_masks[i].lwmask;
+ }
+ return cur_mask;
+}
+
+static void
+set_event_handlers(ltkd_widget *widget, uint32_t before, uint32_t after, ltkd_event_handler *handlers, size_t num_handlers) {
+ for (size_t i = 0; i < num_handlers; i++) {
+ if (!(before & 1) && (after & 1)) {
+ if (handlers[i].callback) {
+ ltk_widget_register_signal_handler(
+ widget->widget, handlers[i].type,
+ handlers[i].callback, LTK_MAKE_ARG_VOIDP(widget)
+ );
+ }
+ } else if ((before & 1) && !(after & 1)) {
+ ltk_widget_remove_signal_handler_by_callback(widget->widget, handlers[i].callback);
+ }
+ before >>= 1;
+ after >>= 1;
+ }
+}
+
+static void
+ltkd_widget_set_event_handlers(ltkd_widget *widget, uint32_t *set_mask, uint32_t new_mask) {
+ uint32_t before = get_widget_mask(widget);
+ *set_mask = new_mask;
+ uint32_t after = get_widget_mask(widget);
+ set_event_handlers(widget, before, after, widget_handlers, LENGTH(widget_handlers));
+}
+
+static void
+ltkd_widget_set_special_event_handlers(ltkd_widget *widget, uint32_t *set_mask, uint32_t new_mask) {
+ uint32_t before = get_widget_special_mask(widget);
+ *set_mask = new_mask;
+ uint32_t after = get_widget_special_mask(widget);
+ set_event_handlers(widget, before, after, widget->event_handlers, widget->num_event_handlers);
+}
+
+void
+ltkd_widget_set_event_mask(ltkd_widget *widget, int client, uint32_t mask) {
+ client_event_mask *m = get_mask_struct(widget, client);
+ ltkd_widget_set_event_handlers(widget, &m->mask, mask);
+}
+
+void
+ltkd_widget_set_event_lmask(ltkd_widget *widget, int client, uint32_t mask) {
+ client_event_mask *m = get_mask_struct(widget, client);
+ ltkd_widget_set_event_handlers(widget, &m->lmask, mask);
+}
+
+void
+ltkd_widget_set_event_wmask(ltkd_widget *widget, int client, uint32_t mask) {
+ client_event_mask *m = get_mask_struct(widget, client);
+ ltkd_widget_set_special_event_handlers(widget, &m->wmask, mask);
+}
+
+void
+ltkd_widget_set_event_lwmask(ltkd_widget *widget, int client, uint32_t mask) {
+ client_event_mask *m = get_mask_struct(widget, client);
+ ltkd_widget_set_special_event_handlers(widget, &m->lwmask, mask);
+}
+
+void
+ltkd_widget_add_to_event_mask(ltkd_widget *widget, int client, uint32_t mask) {
+ client_event_mask *m = get_mask_struct(widget, client);
+ ltkd_widget_set_event_handlers(widget, &m->mask, m->mask | mask);
+}
+
+void
+ltkd_widget_add_to_event_lmask(ltkd_widget *widget, int client, uint32_t mask) {
+ client_event_mask *m = get_mask_struct(widget, client);
+ ltkd_widget_set_event_handlers(widget, &m->lmask, m->lmask | mask);
+}
+
+void
+ltkd_widget_add_to_event_wmask(ltkd_widget *widget, int client, uint32_t mask) {
+ client_event_mask *m = get_mask_struct(widget, client);
+ ltkd_widget_set_special_event_handlers(widget, &m->wmask, m->wmask | mask);
+}
+
+void
+ltkd_widget_add_to_event_lwmask(ltkd_widget *widget, int client, uint32_t mask) {
+ client_event_mask *m = get_mask_struct(widget, client);
+ ltkd_widget_set_special_event_handlers(widget, &m->lwmask, m->lwmask | mask);
+}
+
+void
+ltkd_widget_remove_from_event_mask(ltkd_widget *widget, int client, uint32_t mask) {
+ client_event_mask *m = get_mask_struct(widget, client);
+ ltkd_widget_set_event_handlers(widget, &m->mask, m->mask & ~mask);
+}
+
+void
+ltkd_widget_remove_from_event_lmask(ltkd_widget *widget, int client, uint32_t mask) {
+ client_event_mask *m = get_mask_struct(widget, client);
+ ltkd_widget_set_event_handlers(widget, &m->lmask, m->lmask & ~mask);
+}
+
+void
+ltkd_widget_remove_from_event_wmask(ltkd_widget *widget, int client, uint32_t mask) {
+ client_event_mask *m = get_mask_struct(widget, client);
+ ltkd_widget_set_special_event_handlers(widget, &m->wmask, m->wmask & ~mask);
+}
+
+void
+ltkd_widget_remove_from_event_lwmask(ltkd_widget *widget, int client, uint32_t mask) {
+ client_event_mask *m = get_mask_struct(widget, client);
+ ltkd_widget_set_special_event_handlers(widget, &m->lwmask, m->lwmask & ~mask);
+}
+
+/* FIXME: any way to optimize the whole event mask handling a bit? */
+void
+ltkd_widget_remove_client(int client) {
+ khint_t k;
+ ltkd_widget *ptr;
+ for (k = kh_begin(widget_hash); k != kh_end(widget_hash); k++) {
+ if (kh_exist(widget_hash, k)) {
+ ptr = kh_value(widget_hash, k);
+ for (size_t i = 0; i < ptr->masks_num; i++) {
+ if (ptr->event_masks[i].client == client) {
+ uint32_t before = get_widget_mask(ptr);
+ uint32_t befores = get_widget_special_mask(ptr);
+ memmove(ptr->event_masks + i, ptr->event_masks + i + 1, ptr->masks_num - i - 1);
+ ptr->masks_num--;
+ /* FIXME: maybe reset to NULL in that case? */
+ if (ptr->masks_num > 0) {
+ size_t sz = ideal_array_size(ptr->masks_alloc, ptr->masks_num);
+ if (sz != ptr->masks_alloc) {
+ ptr->masks_alloc = sz;
+ ptr->event_masks = ltk_reallocarray(ptr->event_masks, sz, sizeof(client_event_mask));
+ }
+ }
+ uint32_t after = get_widget_mask(ptr);
+ uint32_t afters = get_widget_special_mask(ptr);
+ set_event_handlers(ptr, before, after, widget_handlers, LENGTH(widget_handlers));
+ set_event_handlers(ptr, befores, afters, ptr->event_handlers, ptr->num_event_handlers);
+ break;
+ }
+ }
+ }
+ }
+}
+
+void
+ltkd_widgets_init() {
+ widget_hash = kh_init(widget);
+ if (!widget_hash) ltkd_fatal_errno("Unable to initialize widget hash table.\n");
+}
+
+/* FIXME: fix global and local coordinates! */
+static int
+queue_mouse_event(ltkd_widget *widget, ltk_event_type type, int x, int y) {
+ uint32_t mask;
+ char *typename;
+ switch (type) {
+ case LTK_MOTION_EVENT:
+ mask = LTKD_PEVENTMASK_MOUSEMOTION;
+ typename = "mousemotion";
+ break;
+ case LTK_2BUTTONPRESS_EVENT:
+ mask = LTKD_PEVENTMASK_MOUSEPRESS;
+ typename = "2mousepress";
+ break;
+ case LTK_3BUTTONPRESS_EVENT:
+ mask = LTKD_PEVENTMASK_MOUSEPRESS;
+ typename = "3mousepress";
+ break;
+ case LTK_BUTTONRELEASE_EVENT:
+ mask = LTKD_PEVENTMASK_MOUSERELEASE;
+ typename = "mouserelease";
+ break;
+ case LTK_2BUTTONRELEASE_EVENT:
+ mask = LTKD_PEVENTMASK_MOUSERELEASE;
+ typename = "2mouserelease";
+ break;
+ case LTK_3BUTTONRELEASE_EVENT:
+ mask = LTKD_PEVENTMASK_MOUSERELEASE;
+ typename = "3mouserelease";
+ break;
+ case LTK_BUTTONPRESS_EVENT:
+ default:
+ mask = LTKD_PEVENTMASK_MOUSEPRESS;
+ typename = "mousepress";
+ break;
+ }
+ for (size_t i = 0; i < widget->masks_num; i++) {
+ if (widget->event_masks[i].lmask & mask) {
+ ltkd_queue_sock_write_fmt(
+ widget->event_masks[i].client,
+ "eventl %s widget %s %d %d %d %d\n",
+ widget->id, typename, x, y, x, y
+ /* x - widget->rect.x, y - widget->rect.y */
+ );
+ if (ltkd_handle_lock_client(widget->widget->window, widget->event_masks[i].client))
+ return 1;
+ } else if (widget->event_masks[i].mask & mask) {
+ ltkd_queue_sock_write_fmt(
+ widget->event_masks[i].client,
+ "event %s widget %s %d %d %d %d\n",
+ widget->id, typename, x, y, x, y
+ /* x - widget->rect.x, y - widget->rect.y */
+ );
+ }
+ }
+ return 0;
+}
+
+static int
+ltkd_widget_button_event(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg) {
+ (void)widget_unused;
+ ltkd_widget *widget = LTK_CAST_ARG_VOIDP(arg);
+ ltk_button_event *event = LTK_GET_ARG_BUTTON_EVENT(args, 0);
+ return queue_mouse_event(widget, event->type, event->x, event->y);
+}
+
+static int
+ltkd_widget_motion_notify(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg) {
+ (void)widget_unused;
+ ltkd_widget *widget = LTK_CAST_ARG_VOIDP(arg);
+ ltk_motion_event *event = LTK_GET_ARG_MOTION_EVENT(args, 0);
+ return queue_mouse_event(widget, event->type, event->x, event->y);
+}
+
+/* FIXME: global/local coords (like above) */
+static int
+ltkd_widget_scroll_event(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg) {
+ (void)widget_unused;
+ ltk_scroll_event *event = LTK_GET_ARG_SCROLL_EVENT(args, 0);
+ ltkd_widget *widget = LTK_CAST_ARG_VOIDP(arg);
+ uint32_t mask = LTKD_PEVENTMASK_MOUSESCROLL;
+ for (size_t i = 0; i < widget->masks_num; i++) {
+ if (widget->event_masks[i].lmask & mask) {
+ ltkd_queue_sock_write_fmt(
+ widget->event_masks[i].client,
+ "eventl %s widget %s %d %d %d %d %d %d\n",
+ widget->id, "mousescroll", event->x, event->y, event->x, event->y, event->dx, event->dy
+ /* x - widget->widget->rect.x, y - widget->widget->rect.y */
+ );
+ if (ltkd_handle_lock_client(widget->widget->window, widget->event_masks[i].client))
+ return 1;
+ } else if (widget->event_masks[i].mask & mask) {
+ ltkd_queue_sock_write_fmt(
+ widget->event_masks[i].client,
+ "event %s widget %s %d %d %d %d %d %d\n",
+ widget->id, "mousescroll", event->x, event->y, event->x, event->y, event->dx, event->dy
+ /* x - widget->widget->rect.x, y - widget->widget->rect.y */
+ );
+ }
+ }
+ return 0;
+}
+
+static int
+ltkd_widget_id_free(const char *id) {
+ khint_t k;
+ k = kh_get(widget, widget_hash, id);
+ if (k != kh_end(widget_hash)) {
+ return 0;
+ }
+ return 1;
+}
+
+ltkd_widget *
+ltkd_get_widget(const char *id, ltk_widget_type type, ltkd_error *err) {
+ khint_t k;
+ ltkd_widget *widget;
+ k = kh_get(widget, widget_hash, id);
+ if (k == kh_end(widget_hash)) {
+ err->type = ERR_INVALID_WIDGET_ID;
+ return NULL;
+ }
+ widget = kh_value(widget_hash, k);
+ if (type != LTK_WIDGET_UNKNOWN && widget->widget->vtable->type != type) {
+ err->type = ERR_INVALID_WIDGET_TYPE;
+ return NULL;
+ }
+ return widget;
+}
+
+static void
+ltkd_set_widget(ltkd_widget *widget, const char *id) {
+ int ret;
+ khint_t k;
+ /* 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;
+}
+
+static void
+ltkd_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);
+ }
+}
+
+ltkd_widget *
+ltkd_widget_create(
+ ltk_widget *widget, const char *id,
+ ltkd_event_handler *event_handlers, size_t num_event_handlers, ltkd_error *err){
+ if (!ltkd_widget_id_free(id)) {
+ err->type = ERR_WIDGET_ID_IN_USE;
+ return NULL;
+ }
+ ltkd_widget *w = ltk_malloc(sizeof(ltkd_widget));
+ w->widget = widget;
+ w->id = ltk_strdup(id);
+ w->event_masks = NULL;
+ w->masks_num = w->masks_alloc = 0;
+ w->event_handlers = event_handlers;
+ w->num_event_handlers = num_event_handlers;
+ ltkd_set_widget(w, id);
+ return w;
+}
+
+void
+ltkd_widget_destroy(ltkd_widget *widget, int shallow) {
+ ltkd_remove_widget(widget->id);
+ ltk_free(widget->id);
+ widget->id = NULL;
+ ltk_free(widget->event_masks);
+ widget->event_masks = NULL;
+ ltk_widget_destroy(widget->widget, shallow);
+ ltk_free(widget);
+}
+
+int
+ltkd_widget_destroy_cmd(
+ ltk_window *window,
+ ltkd_cmd_token *tokens,
+ size_t num_tokens,
+ ltkd_error *err) {
+ (void)window;
+ int shallow = 1;
+ if (num_tokens != 2 && num_tokens != 3) {
+ err->type = ERR_INVALID_NUMBER_OF_ARGUMENTS;
+ err->arg = -1;
+ return 1;
+ }
+ if (tokens[1].contains_nul) {
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = 1;
+ return 1;
+ } else if (num_tokens == 3 && tokens[2].contains_nul) {
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = 2;
+ return 1;
+ }
+ if (num_tokens == 3) {
+ if (strcmp(tokens[2].text, "deep") == 0) {
+ shallow = 0;
+ } else if (strcmp(tokens[2].text, "shallow") == 0) {
+ shallow = 1;
+ } else {
+ err->type = ERR_INVALID_ARGUMENT;
+ err->arg = 2;
+ return 1;
+ }
+ }
+ ltkd_widget *widget = ltkd_get_widget(tokens[1].text, LTK_WIDGET_UNKNOWN, err);
+ if (!widget) {
+ err->arg = 1;
+ return 1;
+ }
+ ltkd_widget_destroy(widget, shallow);
+ return 0;
+}
diff --git a/src/ltkd/widget.h b/src/ltkd/widget.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LTKD_WIDGET_H
+#define LTKD_WIDGET_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "ltkd.h"
+#include "err.h"
+
+#include <ltk/ltk.h>
+
+typedef struct {
+ int client; /* index of client */
+ uint32_t mask; /* generic event mask */
+ uint32_t lmask; /* generic lock mask */
+ uint32_t wmask; /* event mask for specific widget type */
+ uint32_t lwmask; /* lock event mask for specific widget type */
+} client_event_mask;
+
+typedef struct {
+ ltk_signal_callback callback;
+ int type;
+} ltkd_event_handler;
+
+typedef struct {
+ ltk_widget *widget;
+ char *id;
+ client_event_mask *event_masks;
+ /* FIXME: kind of a waste of space to use size_t here */
+ size_t masks_num;
+ size_t masks_alloc;
+ ltkd_event_handler *event_handlers;
+ size_t num_event_handlers;
+} ltkd_widget;
+
+/* FIXME: document that multiple clients locking on event is undefined
+ (or at least order is undefined) */
+
+void ltkd_widgets_init();
+ltkd_widget *ltkd_widget_create(
+ ltk_widget *widget, const char *id,
+ ltkd_event_handler *event_handlers, size_t num_event_handlers, ltkd_error *err
+);
+ltkd_widget *ltkd_get_widget(const char *id, ltk_widget_type type, ltkd_error *err);
+
+int ltkd_widget_queue_specific_event(ltkd_widget *widget, const char *type, uint32_t mask, const char *data);
+
+/*int ltkd_widget_id_free(const char *id);
+void ltkd_set_widget(ltkd_widget *widget, const char *id);
+void ltkd_remove_widget(const char *id);*/
+
+void ltkd_widget_destroy(ltkd_widget *widget, int shallow);
+int ltkd_widget_destroy_cmd(ltk_window *window, ltkd_cmd_token *tokens, size_t num_tokens, ltkd_error *err);
+void ltkd_widget_remove_client(int client);
+
+void ltkd_widgets_cleanup(void);
+
+void ltkd_widget_set_event_mask(ltkd_widget *widget, int client, uint32_t mask);
+void ltkd_widget_set_event_lmask(ltkd_widget *widget, int client, uint32_t mask);
+void ltkd_widget_set_event_wmask(ltkd_widget *widget, int client, uint32_t mask);
+void ltkd_widget_set_event_lwmask(ltkd_widget *widget, int client, uint32_t mask);
+void ltkd_widget_add_to_event_mask(ltkd_widget *widget, int client, uint32_t mask);
+void ltkd_widget_add_to_event_lmask(ltkd_widget *widget, int client, uint32_t mask);
+void ltkd_widget_add_to_event_wmask(ltkd_widget *widget, int client, uint32_t mask);
+void ltkd_widget_add_to_event_lwmask(ltkd_widget *widget, int client, uint32_t mask);
+void ltkd_widget_remove_from_event_mask(ltkd_widget *widget, int client, uint32_t mask);
+void ltkd_widget_remove_from_event_lmask(ltkd_widget *widget, int client, uint32_t mask);
+void ltkd_widget_remove_from_event_wmask(ltkd_widget *widget, int client, uint32_t mask);
+void ltkd_widget_remove_from_event_lwmask(ltkd_widget *widget, int client, uint32_t mask);
+
+#endif /* LTKD_WIDGET_H */
diff --git a/src/util.c b/src/util.c
@@ -1,436 +0,0 @@
-/*
- * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <pwd.h>
-#include <time.h>
-#include <ctype.h>
-#include <errno.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <stdarg.h>
-#include <unistd.h>
-#include <sys/stat.h>
-
-#include "util.h"
-#include "array.h"
-#include "memory.h"
-#include "txtbuf.h"
-
-/* FIXME: Should these functions really fail on memory error? */
-
-char *
-ltk_read_file(const char *filename, size_t *len_ret, char **errstr_ret) {
- long len;
- char *file_contents;
- FILE *file;
-
- /* FIXME: https://wiki.sei.cmu.edu/confluence/display/c/FIO19-C.+Do+not+use+fseek()+and+ftell()+to+compute+the+size+of+a+regular+file */
- file = fopen(filename, "r");
- if (!file) goto error;
- if (fseek(file, 0, SEEK_END)) goto errorclose;
- len = ftell(file);
- if (len < 0) goto errorclose;
- if (fseek(file, 0, SEEK_SET)) goto errorclose;
- file_contents = ltk_malloc((size_t)len + 1);
- clearerr(file);
- fread(file_contents, 1, (size_t)len, file);
- if (ferror(file)) goto errorclose;
- file_contents[len] = '\0';
- if (fclose(file)) goto error;
- *len_ret = (size_t)len;
- return file_contents;
-error:
- if (errstr_ret)
- *errstr_ret = strerror(errno);
- return NULL;
-errorclose:
- if (errstr_ret)
- *errstr_ret = strerror(errno);
- fclose(file);
- return NULL;
-}
-
-/* FIXME: not sure if errno actually is set usefully after all these functions */
-int
-ltk_write_file(const char *path, const char *data, size_t len, char **errstr_ret) {
- FILE *file = fopen(path, "w");
- if (!file) goto error;
- clearerr(file);
- if (fwrite(data, 1, len, file) < len) goto errorclose;
- if (fclose(file)) goto error;
- return 0;
-error:
- if (errstr_ret)
- *errstr_ret = strerror(errno);
- return 1;
-errorclose:
- if (errstr_ret)
- *errstr_ret = strerror(errno);
- fclose(file);
- return 1;
-}
-
-/* FIXME: maybe have a few standard array types defined somewhere else */
-LTK_ARRAY_INIT_DECL_STATIC(cmd, char *)
-LTK_ARRAY_INIT_IMPL_STATIC(cmd, char *)
-
-static void
-free_helper(char *ptr) {
- ltk_free(ptr);
-}
-
-/* FIXME: this is really ugly */
-/* FIXME: parse command only once in beginning instead of each time it is run? */
-/* FIXME: this handles double-quote, but the config parser already uses that, so
- it's kind of weird because it's parsed twice (also backslashes are parsed twice). */
-int
-ltk_parse_run_cmd(const char *cmdtext, size_t len, const char *filename) {
- int bs = 0;
- int in_sqstr = 0;
- int in_dqstr = 0;
- int in_ws = 1;
- char c;
- size_t cur_start = 0;
- int offset = 0;
- txtbuf *cur_arg = txtbuf_new();
- ltk_array(cmd) *cmd = ltk_array_create(cmd, 4);
- char *cmdcopy = ltk_strndup(cmdtext, len);
- for (size_t i = 0; i < len; i++) {
- c = cmdcopy[i];
- if (c == '\\') {
- if (bs) {
- offset++;
- bs = 0;
- } else {
- bs = 1;
- }
- } else if (isspace(c)) {
- if (!in_sqstr && !in_dqstr) {
- if (bs) {
- if (in_ws) {
- in_ws = 0;
- cur_start = i;
- offset = 0;
- } else {
- offset++;
- }
- bs = 0;
- } else if (!in_ws) {
- /* FIXME: shouldn't this be < instead of <=? */
- if (cur_start <= i - offset)
- txtbuf_appendn(cur_arg, cmdcopy + cur_start, i - cur_start - offset);
- /* FIXME: cmd is named horribly */
- ltk_array_append(cmd, cmd, txtbuf_get_textcopy(cur_arg));
- txtbuf_clear(cur_arg);
- in_ws = 1;
- offset = 0;
- }
- /* FIXME: parsing weird here - bs just ignored */
- } else if (bs) {
- bs = 0;
- }
- } else if (c == '%') {
- if (bs) {
- if (in_ws) {
- cur_start = i;
- offset = 0;
- } else {
- offset++;
- }
- bs = 0;
- } else if (!in_sqstr && filename && i < len - 1 && cmdcopy[i + 1] == 'f') {
- if (!in_ws && cur_start < i - offset)
- txtbuf_appendn(cur_arg, cmdcopy + cur_start, i - cur_start - offset);
- txtbuf_append(cur_arg, filename);
- i++;
- cur_start = i + 1;
- offset = 0;
- } else if (in_ws) {
- cur_start = i;
- offset = 0;
- }
- in_ws = 0;
- } else if (c == '"') {
- if (in_sqstr) {
- bs = 0;
- } else if (bs) {
- if (in_ws) {
- cur_start = i;
- offset = 0;
- } else {
- offset++;
- }
- bs = 0;
- } else if (in_dqstr) {
- offset++;
- in_dqstr = 0;
- continue;
- } else {
- in_dqstr = 1;
- if (in_ws) {
- cur_start = i + 1;
- offset = 0;
- } else {
- offset++;
- continue;
- }
- }
- in_ws = 0;
- } else if (c == '\'') {
- if (in_dqstr) {
- bs = 0;
- } else if (bs) {
- if (in_ws) {
- cur_start = i;
- offset = 0;
- } else {
- offset++;
- }
- bs = 0;
- } else if (in_sqstr) {
- offset++;
- in_sqstr = 0;
- continue;
- } else {
- in_sqstr = 1;
- if (in_ws) {
- cur_start = i + 1;
- offset = 0;
- } else {
- offset++;
- continue;
- }
- }
- in_ws = 0;
- } else if (bs) {
- if (!in_sqstr && !in_dqstr) {
- if (in_ws) {
- cur_start = i;
- offset = 0;
- } else {
- offset++;
- }
- }
- bs = 0;
- in_ws = 0;
- } else {
- if (in_ws) {
- cur_start = i;
- offset = 0;
- }
- in_ws = 0;
- }
- cmdcopy[i - offset] = cmdcopy[i];
- }
- if (in_sqstr || in_dqstr) {
- ltk_warn("Unterminated string in command\n");
- goto error;
- }
- if (!in_ws) {
- if (cur_start <= len - offset)
- txtbuf_appendn(cur_arg, cmdcopy + cur_start, len - cur_start - offset);
- ltk_array_append(cmd, cmd, txtbuf_get_textcopy(cur_arg));
- }
- if (cmd->len == 0) {
- ltk_warn("Empty command\n");
- goto error;
- }
- ltk_array_append(cmd, cmd, NULL); /* necessary for execvp */
- int fret = -1;
- if ((fret = fork()) < 0) {
- ltk_warn("Unable to fork\n");
- goto error;
- } else if (fret == 0) {
- if (execvp(cmd->buf[0], cmd->buf) == -1) {
- /* FIXME: what to do on error here? */
- exit(1);
- }
- } else {
- ltk_free(cmdcopy);
- txtbuf_destroy(cur_arg);
- ltk_array_destroy_deep(cmd, cmd, &free_helper);
- return fret;
- }
-error:
- ltk_free(cmdcopy);
- txtbuf_destroy(cur_arg);
- ltk_array_destroy_deep(cmd, cmd, &free_helper);
- return -1;
-}
-
-/* If `needed` is larger than `*alloc_size`, resize `*str` to
- `max(needed, *alloc_size * 2)`. Aborts program on error. */
-void
-ltk_grow_string(char **str, int *alloc_size, int needed) {
- if (needed <= *alloc_size) return;
- int new_size = needed > (*alloc_size * 2) ? needed : (*alloc_size * 2);
- char *new = ltk_realloc(*str, new_size);
- *str = new;
- *alloc_size = new_size;
-}
-
-/* Get the directory to store ltk files in and create it if it doesn't exist yet.
- This first checks the environment variable LTKDIR and, if that doesn't
- exist, the home directory with "/.ltk" appended.
- Returns NULL on error. */
-char *
-ltk_setup_directory(void) {
- char *dir, *dir_orig;
- struct passwd *pw;
- uid_t uid;
- int len;
-
- dir_orig = getenv("LTKDIR");
- if (dir_orig) {
- dir = ltk_strdup(dir_orig);
- /*
- if (!dir)
- return NULL;
- */
- } else {
- uid = getuid();
- pw = getpwuid(uid);
- if (!pw)
- return NULL;
- len = strlen(pw->pw_dir);
- dir = ltk_malloc(len + 6);
- /*
- if (!dir)
- return NULL;
- */
- strcpy(dir, pw->pw_dir);
- strcpy(dir + len, "/.ltk");
- }
-
- if (mkdir(dir, 0770) < 0) {
- if (errno != EEXIST)
- return NULL;
- }
-
- return dir;
-}
-
-/* Concatenate the two given strings and return the result.
- This allocates new memory for the result string, unlike
- the actual strcat. Aborts program on error */
-char *
-ltk_strcat_useful(const char *str1, const char *str2) {
- int len1, len2;
- char *ret;
-
- len1 = strlen(str1);
- len2 = strlen(str2);
- ret = ltk_malloc(len1 + len2 + 1);
- strcpy(ret, str1);
- strcpy(ret + len1, str2);
-
- return ret;
-}
-
-void
-ltk_log_msg(const char *mode, const char *format, va_list args) {
- char logtime[25]; /* FIXME: This should always be big enough, right? */
- time_t clock;
- struct tm *timeptr;
-
- time(&clock);
- timeptr = localtime(&clock);
- strftime(logtime, 25, "%Y-%m-%d %H:%M:%S", timeptr);
-
- fprintf(stderr, "%s ltk %s: ", logtime, mode);
- vfprintf(stderr, format, args);
-}
-
-void
-ltk_warn(const char *format, ...) {
- va_list args;
- va_start(args, format);
- ltk_log_msg("Warning", format, args);
- va_end(args);
-}
-
-void
-ltk_fatal(const char *format, ...) {
- va_list args;
- va_start(args, format);
- ltk_log_msg("Fatal", format, args);
- va_end(args);
- ltk_deinit();
-
- exit(1);
-}
-
-void
-ltk_warn_errno(const char *format, ...) {
- va_list args;
- char *errstr = strerror(errno);
- va_start(args, format);
- ltk_log_msg("Warning", format, args);
- va_end(args);
- ltk_warn("system error: %s\n", errstr);
-}
-
-void
-ltk_fatal_errno(const char *format, ...) {
- va_list args;
- char *errstr = strerror(errno);
- va_start(args, format);
- ltk_log_msg("Fatal", format, args);
- va_end(args);
- ltk_fatal("system error: %s\n", errstr);
-}
-
-int
-str_array_equal(const char *terminated, const char *array, size_t len) {
- if (!strncmp(terminated, array, len)) {
- /* this is kind of inefficient, but there's no way to know
- otherwise if strncmp just stopped comparing after a '\0' */
- return strlen(terminated) == len;
- }
- return 0;
-}
-
-size_t
-prev_utf8(char *text, size_t index) {
- if (index == 0)
- return 0;
- size_t i = index - 1;
- /* find valid utf8 char - this probably needs to be improved */
- while (i > 0 && ((text[i] & 0xC0) == 0x80))
- i--;
- return i;
-}
-
-size_t
-next_utf8(char *text, size_t len, size_t index) {
- if (index >= len)
- return len;
- size_t i = index + 1;
- while (i < len && ((text[i] & 0xC0) == 0x80))
- i++;
- return i;
-}
-
-void
-ltk_assert_impl(const char *file, int line, const char *func, const char *failedexpr)
-{
- (void)fprintf(stderr,
- "assertion \"%s\" failed: file \"%s\", line %d, function \"%s\"\n",
- failedexpr, file, line, func);
- abort();
- /* NOTREACHED */
-}
diff --git a/src/util.h b/src/util.h
@@ -1,62 +0,0 @@
-/*
- * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef LTK_UTIL_H
-#define LTK_UTIL_H
-
-#include <stdarg.h>
-#include <stddef.h>
-
-long long ltk_strtonum(
- const char *numstr, long long minval,
- long long maxval, const char **errstrp
-);
-
-char *ltk_read_file(const char *filename, size_t *len_ret, char **errstr_ret);
-int ltk_write_file(const char *path, const char *data, size_t len, char **errstr_ret);
-int ltk_parse_run_cmd(const char *cmdtext, size_t len, const char *filename);
-void ltk_grow_string(char **str, int *alloc_size, int needed);
-char *ltk_setup_directory(void);
-char *ltk_strcat_useful(const char *str1, const char *str2);
-
-/* Note: this is actually implemented in ltk.c (it is just
- declared here so it can be used by the utility functions */
-void ltk_deinit(void);
-
-void ltk_log_msg(const char *mode, const char *format, va_list args);
-void ltk_fatal_errno(const char *format, ...);
-void ltk_warn_errno(const char *format, ...);
-void ltk_fatal(const char *format, ...);
-void ltk_warn(const char *format, ...);
-
-/*
- * Compare the nul-terminated string 'terminated' with the char
- * array 'array' with length 'len'.
- * Returns non-zero if they are equal, 0 otherwise.
- */
-/* Note: this doesn't work if array contains '\0'. */
-int str_array_equal(const char *terminated, const char *array, size_t len);
-
-size_t prev_utf8(char *text, size_t index);
-size_t next_utf8(char *text, size_t len, size_t index);
-
-/* based on the assert found in OpenBSD */
-void ltk_assert_impl(const char *file, int line, const char *func, const char *failedexpr);
-#define ltk_assert(e) ((e) ? (void)0 : ltk_assert_impl(__FILE__, __LINE__, __func__, #e))
-
-#define LENGTH(X) (sizeof(X) / sizeof(X[0]))
-
-#endif /* LTK_UTIL_H */
diff --git a/src/widget.c b/src/widget.c
@@ -1,241 +0,0 @@
-/*
- * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <string.h>
-
-#include "rect.h"
-#include "widget.h"
-#include "window.h"
-#include "memory.h"
-#include "array.h"
-
-LTK_ARRAY_INIT_FUNC_DECL_STATIC(signal, ltk_signal_callback_info)
-LTK_ARRAY_INIT_IMPL_STATIC(signal, ltk_signal_callback_info)
-
-void
-ltk_fill_widget_defaults(ltk_widget *widget, ltk_window *window,
- struct ltk_widget_vtable *vtable, int w, int h) {
- widget->window = window;
- widget->parent = NULL;
-
- /* FIXME: possibly check that draw and destroy aren't NULL */
- widget->vtable = vtable;
-
- widget->state = LTK_NORMAL;
- widget->row = 0;
- widget->lrect.x = 0;
- widget->lrect.y = 0;
- widget->lrect.w = w;
- widget->lrect.h = h;
- widget->crect.x = 0;
- widget->crect.y = 0;
- widget->crect.w = w;
- widget->crect.h = h;
- widget->popup = 0;
-
- widget->ideal_w = widget->ideal_h = 0;
-
- widget->row = 0;
- widget->column = 0;
- widget->row_span = 0;
- widget->column_span = 0;
- widget->sticky = 0;
- widget->dirty = 1;
- widget->hidden = 0;
- widget->vtable_copied = 0;
- widget->signal_cbs = NULL;
- /* FIXME: null other members! */
-}
-
-void
-ltk_widget_hide(ltk_widget *widget) {
- if (widget->vtable->hide)
- widget->vtable->hide(widget);
- widget->hidden = 1;
- /* remove hover state */
- /* FIXME: this needs to call change_state but that might cause issues */
- ltk_widget *hover = widget->window->hover_widget;
- while (hover) {
- if (hover == widget) {
- widget->window->hover_widget->state &= ~LTK_HOVER;
- widget->window->hover_widget = NULL;
- break;
- }
- hover = hover->parent;
- }
- ltk_widget *pressed = widget->window->pressed_widget;
- while (pressed) {
- if (pressed == widget) {
- widget->window->pressed_widget->state &= ~LTK_PRESSED;
- widget->window->pressed_widget = NULL;
- break;
- }
- pressed = pressed->parent;
- }
- ltk_widget *active = widget->window->active_widget;
- /* if current active widget is child, set active widget to widget above in hierarchy */
- int set_next = 0;
- while (active) {
- if (active == widget) {
- set_next = 1;
- /* FIXME: use config values for all_activatable */
- } else if (set_next && (active->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
- ltk_window_set_active_widget(active->window, active);
- break;
- }
- active = active->parent;
- }
- if (set_next && !active)
- ltk_window_set_active_widget(active->window, NULL);
-}
-
-/* FIXME: Maybe pass the new width as arg here?
- That would make a bit more sense */
-/* FIXME: maybe give global and local position in event */
-void
-ltk_widget_resize(ltk_widget *widget) {
- if (widget->vtable->resize)
- widget->vtable->resize(widget);
- widget->dirty = 1;
-}
-
-void
-ltk_widget_change_state(ltk_widget *widget, ltk_widget_state old_state) {
- if (old_state == widget->state)
- return;
- if (widget->vtable->change_state)
- widget->vtable->change_state(widget, old_state);
- if (widget->vtable->flags & LTK_NEEDS_REDRAW) {
- widget->dirty = 1;
- ltk_window_invalidate_widget_rect(widget->window, widget);
- }
-}
-
-/* FIXME: document that it's really dangerous to overwrite remove_child or destroy */
-int
-ltk_widget_destroy(ltk_widget *widget, int shallow) {
- /* 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 invalid = 0;
- if (widget->parent) {
- if (widget->parent->vtable->remove_child)
- invalid = widget->parent->vtable->remove_child(widget->parent, widget);
- }
- if (widget->vtable_copied) {
- ltk_free(widget->vtable);
- widget->vtable = NULL;
- }
- if (widget->signal_cbs) {
- ltk_array_destroy(signal, widget->signal_cbs);
- widget->signal_cbs = NULL;
- }
- widget->vtable->destroy(widget, shallow);
-
- return invalid;
-}
-
-ltk_point
-ltk_widget_pos_to_global(ltk_widget *widget, int x, int y) {
- ltk_widget *cur = widget;
- while (cur) {
- x += cur->lrect.x;
- y += cur->lrect.y;
- if (cur->popup)
- break;
- cur = cur->parent;
- }
- return (ltk_point){x, y};
-}
-
-ltk_point
-ltk_global_to_widget_pos(ltk_widget *widget, int x, int y) {
- ltk_widget *cur = widget;
- while (cur) {
- x -= cur->lrect.x;
- y -= cur->lrect.y;
- if (cur->popup)
- break;
- cur = cur->parent;
- }
- return (ltk_point){x, y};
-}
-
-int
-ltk_widget_register_signal_handler(ltk_widget *widget, int type, ltk_signal_callback callback, ltk_callback_arg data) {
- if ((type >= LTK_WIDGET_SIGNAL_INVALID) || type <= widget->vtable->invalid_signal)
- return 1;
- if (!widget->signal_cbs) {
- widget->signal_cbs = ltk_array_create(signal, 1);
- }
- ltk_array_append_signal(widget->signal_cbs, (ltk_signal_callback_info){callback, data, type});
- return 0;
-}
-
-int
-ltk_widget_emit_signal(ltk_widget *widget, int type, ltk_callback_arglist args) {
- if (!widget->signal_cbs)
- return 0;
- int handled = 0;
- for (size_t i = 0; i < ltk_array_len(widget->signal_cbs); i++) {
- if (ltk_array_get(widget->signal_cbs, i).type == type) {
- handled |= ltk_array_get(widget->signal_cbs, i).callback(widget, args, ltk_array_get(widget->signal_cbs, i).data);
- }
- }
- return handled;
-}
-
-static int
-filter_by_type(ltk_signal_callback_info *info, void *data) {
- return info->type == *(int *)data;
-}
-
-size_t
-ltk_widget_remove_signal_handler_by_type(ltk_widget *widget, int type) {
- if (!widget->signal_cbs)
- return 0;
- return ltk_array_remove_if(signal, widget->signal_cbs, &filter_by_type, &type);
-}
-
-struct func_wrapper {
- ltk_signal_callback callback;
-};
-
-static int
-filter_by_callback(ltk_signal_callback_info *info, void *data) {
- return info->callback == ((struct func_wrapper *)data)->callback;
-}
-
-size_t
-ltk_widget_remove_signal_handler_by_callback(ltk_widget *widget, ltk_signal_callback callback) {
- if (!widget->signal_cbs)
- return 0;
- /* callback can't be passed directly because ISO C forbids
- conversion of object pointer to function pointer */
- struct func_wrapper data = {callback};
- return ltk_array_remove_if(signal, widget->signal_cbs, &filter_by_callback, &data);
-}
-
-int ltk_widget_register_type(void); /* FIXME */
-
-ltk_widget_vtable *
-ltk_widget_get_editable_vtable(ltk_widget *widget) {
- if (!widget->vtable_copied) {
- ltk_widget_vtable *vtable = ltk_malloc(sizeof(ltk_widget_vtable));
- memcpy(vtable, widget->vtable, sizeof(ltk_widget_vtable));
- widget->vtable_copied = 1;
- }
- return widget->vtable;
-}
diff --git a/src/widget.h b/src/widget.h
@@ -1,320 +0,0 @@
-/*
- * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-/* need to check what happens when registering signal for destroy, but then calling ltk_destroy_widget from
-that handler - maybe loop if container widget deletes children but they call parent->delete_child again */
-#ifndef LTK_WIDGET_H
-#define LTK_WIDGET_H
-
-/* FIXME: destroy signal for widgets (also window) */
-
-#include <stddef.h>
-#include "array.h"
-#include "event.h"
-#include "graphics.h"
-#include "rect.h"
-#include "util.h"
-
-struct ltk_widget;
-struct ltk_window;
-typedef struct ltk_widget ltk_widget;
-
-typedef enum {
- LTK_WIDGET_UNKNOWN = 0,
- LTK_WIDGET_ANY,
- LTK_WIDGET_GRID,
- LTK_WIDGET_BUTTON,
- LTK_WIDGET_LABEL,
- LTK_WIDGET_BOX,
- LTK_WIDGET_MENU,
- LTK_WIDGET_MENUENTRY,
- LTK_WIDGET_ENTRY,
- LTK_WIDGET_IMAGE,
- LTK_WIDGET_WINDOW,
- LTK_WIDGET_SCROLLBAR,
- LTK_NUM_WIDGETS,
-} ltk_widget_type;
-
-/* FIXME: SORT OUT INCLUDES PROPERLY! */
-
-typedef enum {
- LTK_ACTIVATABLE_NORMAL = 1,
- /* only activatable when "all-activatable"
- is set to true in the config */
- LTK_ACTIVATABLE_SPECIAL = 2,
- LTK_ACTIVATABLE_ALWAYS = 1|2,
- /* FIXME: redundant or needs better name - is implied by entries in vtable
- - if there are widgets that have keyboard functions in the vtable but
- shouldn't have this set, then it's a bad name */
- LTK_NEEDS_KEYBOARD = 4,
- LTK_NEEDS_REDRAW = 8,
- LTK_HOVER_IS_ACTIVE = 16,
-} ltk_widget_flags;
-
-/* FIXME: "sticky" is maybe not the correct name anymore */
-typedef enum {
- LTK_STICKY_NONE = 0,
- LTK_STICKY_LEFT = 1 << 0,
- LTK_STICKY_RIGHT = 1 << 1,
- LTK_STICKY_TOP = 1 << 2,
- LTK_STICKY_BOTTOM = 1 << 3,
- LTK_STICKY_SHRINK_WIDTH = 1 << 4,
- LTK_STICKY_SHRINK_HEIGHT = 1 << 5,
- LTK_STICKY_PRESERVE_ASPECT_RATIO = 1 << 6,
-} ltk_sticky_mask;
-
-typedef enum {
- LTK_VERTICAL,
- LTK_HORIZONTAL
-} ltk_orientation;
-
-typedef enum {
- LTK_NORMAL = 0,
- LTK_HOVER = 1,
- LTK_PRESSED = 2,
- LTK_ACTIVE = 4,
- LTK_HOVERACTIVE = 1 | 4,
- LTK_FOCUSED = 8,
- LTK_DISABLED = 16,
-} ltk_widget_state;
-
-/* FIXME: need "ltk_register_type" just to get unique integer for type checking */
-
-typedef struct {
- union {
- int i;
- size_t sz;
- char c;
- ltk_widget *widget;
- char *str;
- const char *cstr;
- ltk_key_event *key_event;
- ltk_button_event *button_event;
- ltk_scroll_event *scroll_event;
- ltk_motion_event *motion_event;
- ltk_surface *surface;
- void *v;
- /* FIXME: maybe rewrite the functions to take
- pointers instead so this doesn't increase
- the size of the union (thereby increasing
- the size of every arg in the arglist) */
- ltk_rect rect;
- } arg;
- enum {
- LTK_TYPE_INT,
- LTK_TYPE_SIZE_T,
- LTK_TYPE_CHAR,
- LTK_TYPE_WIDGET,
- LTK_TYPE_STRING,
- LTK_TYPE_CONST_STRING,
- LTK_TYPE_KEY_EVENT,
- LTK_TYPE_BUTTON_EVENT,
- LTK_TYPE_SCROLL_EVENT,
- LTK_TYPE_MOTION_EVENT,
- LTK_TYPE_SURFACE,
- LTK_TYPE_RECT,
- LTK_TYPE_VOID,
- LTK_TYPE_VOIDP,
- } type;
-} ltk_callback_arg;
-
-/* FIXME: STRING should be CHARP for consistency */
-#define LTK_MAKE_ARG_INT(data) ((ltk_callback_arg){.type = LTK_TYPE_INT, .arg = {.i = (data)}})
-#define LTK_MAKE_ARG_SIZE_T(data) ((ltk_callback_arg){.type = LTK_TYPE_SIZE_T, .arg = {.sz = (data)}})
-#define LTK_MAKE_ARG_CHAR(data) ((ltk_callback_arg){.type = LTK_TYPE_CHAR, .arg = {.c = (data)}})
-#define LTK_MAKE_ARG_WIDGET(data) ((ltk_callback_arg){.type = LTK_TYPE_WIDGET, .arg = {.widget = (data)}})
-#define LTK_MAKE_ARG_STRING(data) ((ltk_callback_arg){.type = LTK_TYPE_STRING, .arg = {.str = (data)}})
-#define LTK_MAKE_ARG_CONST_STRING(data) ((ltk_callback_arg){.type = LTK_TYPE_CONST_STRING, .arg = {.cstr = (data)}})
-#define LTK_MAKE_ARG_KEY_EVENT(data) ((ltk_callback_arg){.type = LTK_TYPE_KEY_EVENT, .arg = {.key_event = (data)}})
-#define LTK_MAKE_ARG_BUTTON_EVENT(data) ((ltk_callback_arg){.type = LTK_TYPE_BUTTON_EVENT, .arg = {.button_event = (data)}})
-#define LTK_MAKE_ARG_SCROLL_EVENT(data) ((ltk_callback_arg){.type = LTK_TYPE_SCROLL_EVENT, .arg = {.scroll_event = (data)}})
-#define LTK_MAKE_ARG_MOTION_EVENT(data) ((ltk_callback_arg){.type = LTK_TYPE_MOTION_EVENT, .arg = {.motion_event = (data)}})
-#define LTK_MAKE_ARG_SURFACE(data) ((ltk_callback_arg){.type = LTK_TYPE_SURFACE, .arg = {.surface = (data)}})
-#define LTK_MAKE_ARG_RECT(data) ((ltk_callback_arg){.type = LTK_TYPE_RECT, .arg = {.rect = (data)}})
-#define LTK_MAKE_ARG_VOIDP(data) ((ltk_callback_arg){.type = LTK_TYPE_VOIDP, .arg = {.v = (data)}})
-
-#define LTK_ARG_VOID ((ltk_callback_arg){.type = LTK_TYPE_VOID})
-
-#define LTK_CAST_ARG_INT(carg) (ltk_assert(carg.type == LTK_TYPE_INT), carg.arg.i)
-#define LTK_CAST_ARG_SIZE_T(carg) (ltk_assert(carg.type == LTK_TYPE_SIZE_T), carg.arg.sz)
-#define LTK_CAST_ARG_CHAR(carg) (ltk_assert(carg.type == LTK_TYPE_CHAR), carg.arg.c)
-#define LTK_CAST_ARG_WIDGET(carg) (ltk_assert(carg.type == LTK_TYPE_WIDGET), carg.arg.widget)
-#define LTK_CAST_ARG_STRING(carg) (ltk_assert(carg.type == LTK_TYPE_STRING), carg.arg.str)
-#define LTK_CAST_ARG_CONST_STRING(carg) (ltk_assert(carg.type == LTK_TYPE_CONST_STRING), carg.arg.cstr)
-#define LTK_CAST_ARG_KEY_EVENT(carg) (ltk_assert(carg.type == LTK_TYPE_KEY_EVENT), carg.arg.key_event)
-#define LTK_CAST_ARG_BUTTON_EVENT(carg) (ltk_assert(carg.type == LTK_TYPE_BUTTON_EVENT), carg.arg.button_event)
-#define LTK_CAST_ARG_SCROLL_EVENT(carg) (ltk_assert(carg.type == LTK_TYPE_SCROLL_EVENT), carg.arg.scroll_event)
-#define LTK_CAST_ARG_MOTION_EVENT(carg) (ltk_assert(carg.type == LTK_TYPE_MOTION_EVENT), carg.arg.motion_event)
-#define LTK_CAST_ARG_SURFACE(carg) (ltk_assert(carg.type == LTK_TYPE_SURFACE), carg.arg.surface)
-#define LTK_CAST_ARG_RECT(carg) (ltk_assert(carg.type == LTK_TYPE_RECT), carg.arg.rect)
-#define LTK_CAST_ARG_VOIDP(carg) (ltk_assert(carg.type == LTK_TYPE_VOIDP), carg.arg.v)
-
-#define LTK_GET_ARG_INT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_INT(cargs.args[i]))
-#define LTK_GET_ARG_SIZE_T(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_SIZE_T(cargs.args[i]))
-#define LTK_GET_ARG_CHAR(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_CHAR(cargs.args[i]))
-#define LTK_GET_ARG_WIDGET(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_WIDGET(cargs.args[i]))
-#define LTK_GET_ARG_STRING(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_STRING(cargs.args[i]))
-#define LTK_GET_ARG_CONST_STRING(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_CONST_STRING(cargs.args[i]))
-#define LTK_GET_ARG_KEY_EVENT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_KEY_EVENT(cargs.args[i]))
-#define LTK_GET_ARG_BUTTON_EVENT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_BUTTON_EVENT(cargs.args[i]))
-#define LTK_GET_ARG_SCROLL_EVENT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_SCROLL_EVENT(cargs.args[i]))
-#define LTK_GET_ARG_MOTION_EVENT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_MOTION_EVENT(cargs.args[i]))
-#define LTK_GET_ARG_SURFACE(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_SURFACE(cargs.args[i]))
-#define LTK_GET_ARG_RECT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_RECT(cargs.args[i]))
-
-#define LTK_CAST_WIDGET(w) (&(w)->widget)
-#define LTK_CAST_WINDOW(w) (ltk_assert(w->vtable->type == LTK_WIDGET_WINDOW), (ltk_window *)(w))
-#define LTK_CAST_LABEL(w) (ltk_assert(w->vtable->type == LTK_WIDGET_LABEL), (ltk_label *)(w))
-#define LTK_CAST_BUTTON(w) (ltk_assert(w->vtable->type == LTK_WIDGET_BUTTON), (ltk_button *)(w))
-#define LTK_CAST_GRID(w) (ltk_assert(w->vtable->type == LTK_WIDGET_GRID), (ltk_grid *)(w))
-#define LTK_CAST_IMAGE_WIDGET(w) (ltk_assert(w->vtable->type == LTK_WIDGET_IMAGE), (ltk_image_widget *)(w))
-#define LTK_CAST_ENTRY(w) (ltk_assert(w->vtable->type == LTK_WIDGET_ENTRY), (ltk_entry *)(w))
-#define LTK_CAST_MENU(w) (ltk_assert(w->vtable->type == LTK_WIDGET_MENU), (ltk_menu *)(w))
-#define LTK_CAST_MENUENTRY(w) (ltk_assert(w->vtable->type == LTK_WIDGET_MENUENTRY), (ltk_menuentry *)(w))
-#define LTK_CAST_SCROLLBAR(w) (ltk_assert(w->vtable->type == LTK_WIDGET_SCROLLBAR), (ltk_scrollbar *)(w))
-#define LTK_CAST_BOX(w) (ltk_assert(w->vtable->type == LTK_WIDGET_BOX), (ltk_box *)(w))
-
-#define LTK_WIDGET_SIGNAL_INVALID 0
-
-typedef struct {
- ltk_callback_arg *args;
- size_t num;
-} ltk_callback_arglist;
-
-#define LTK_EMPTY_ARGLIST ((ltk_callback_arglist){NULL, 0})
-
-/* FIXME: should signals just return int to make it simpler? */
-typedef int (*ltk_signal_callback)(ltk_widget *widget, ltk_callback_arglist args, ltk_callback_arg data);
-typedef struct {
- ltk_signal_callback callback;
- ltk_callback_arg data;
- int type;
-} ltk_signal_callback_info;
-
-int ltk_widget_register_signal_handler(ltk_widget *widget, int type, ltk_signal_callback callback, ltk_callback_arg data);
-int ltk_widget_emit_signal(ltk_widget *widget, int type, ltk_callback_arglist args);
-size_t ltk_widget_remove_signal_handler_by_type(ltk_widget *widget, int type);
-size_t ltk_widget_remove_signal_handler_by_callback(ltk_widget *widget, ltk_signal_callback callback);
-int ltk_widget_register_type(void);
-
-LTK_ARRAY_INIT_STRUCT_DECL(signal, ltk_signal_callback_info)
-
-struct ltk_widget {
- struct ltk_window *window;
- struct ltk_widget *parent;
-
- struct ltk_widget_vtable *vtable;
-
- /* FIXME: crect and lrect are a bit weird still */
- /* FIXME: especially the relative positioning is really weird for
- popups because they're positioned globally but still have a
- parent-child relationship - weird things can probably happen */
- /* both rects relative to parent (except for popups) */
- /* collision rect is only part that is actually shown and used for
- collision with mouse (but may still not be drawn if hidden by
- something else) - e.g. in a box with scrolling, a widget that
- is half cut off by a side of the box will have the logical rect
- going past the side of the box, but the collision rect will only
- be the part inside the box */
- ltk_rect crect; /* collision rect */
- ltk_rect lrect; /* logical rect */
- unsigned int ideal_w;
- unsigned int ideal_h;
-
- /* maybe mask to determine quickly which callbacks are included?
- default signals only allowed to have one callback? */
- ltk_array(signal) *signal_cbs;
-
- ltk_widget_state state;
- /* FIXME: store this in grid/box - for row_span, column_span the other cells could be marked with "not top left cell of widget" so they can be skipped */
- ltk_sticky_mask sticky;
- unsigned short row;
- unsigned short column;
- unsigned short row_span;
- unsigned short column_span;
- /* ALSO NEED SIGNALS LIKE ADD-TEXT (called *before* text is inserted to check validity) - these would need argument
- FIGURE OUT HOW TO DO KEY MAPPINGS - should reuse parts of builtin mapping handling
- -> maybe something like tk
- -> or maybe just say everyone needs to override event handler? but makes simple stuff more difficult
- -> also need "global" mappings/key event handler for global shortcuts */
- /* needed to properly handle handle local coordinates since
- popups are positioned globally instead of locally */
- char popup;
- char dirty;
- char hidden;
- char vtable_copied;
-};
-
-typedef struct ltk_widget_vtable {
- int (*key_press)(struct ltk_widget *, ltk_key_event *);
- int (*key_release)(struct ltk_widget *, ltk_key_event *);
- /* press/release also receive double/triple-click/release */
- int (*mouse_press)(struct ltk_widget *, ltk_button_event *);
- int (*mouse_release)(struct ltk_widget *, ltk_button_event *);
- int (*mouse_scroll)(struct ltk_widget *, ltk_scroll_event *);
- int (*motion_notify)(struct ltk_widget *, ltk_motion_event *);
- int (*mouse_leave)(struct ltk_widget *, ltk_motion_event *);
- int (*mouse_enter)(struct ltk_widget *, ltk_motion_event *);
- int (*press)(struct ltk_widget *);
- int (*release)(struct ltk_widget *);
- void (*cmd_return)(struct ltk_widget *self, char *text, size_t len);
-
- void (*resize)(struct ltk_widget *);
- void (*hide)(struct ltk_widget *);
- /* draw_surface: surface to draw it on
- x, y: position of logical rectangle on surface
- clip: clipping rectangle, relative to logical rectangle */
- void (*draw)(struct ltk_widget *self, ltk_surface *draw_surface, int x, int y, ltk_rect clip);
- void (*change_state)(struct ltk_widget *, ltk_widget_state);
- void (*destroy)(struct ltk_widget *, int);
-
- /* rect is in self's coordinate system */
- struct ltk_widget *(*nearest_child)(struct ltk_widget *self, ltk_rect rect);
- struct ltk_widget *(*nearest_child_left)(struct ltk_widget *self, ltk_widget *widget);
- struct ltk_widget *(*nearest_child_right)(struct ltk_widget *self, ltk_widget *widget);
- struct ltk_widget *(*nearest_child_above)(struct ltk_widget *self, ltk_widget *widget);
- struct ltk_widget *(*nearest_child_below)(struct ltk_widget *self, ltk_widget *widget);
- struct ltk_widget *(*next_child)(struct ltk_widget *self, ltk_widget *child);
- struct ltk_widget *(*prev_child)(struct ltk_widget *self, ltk_widget *child);
- struct ltk_widget *(*first_child)(struct ltk_widget *self);
- struct ltk_widget *(*last_child)(struct ltk_widget *self);
-
- void (*child_size_change)(struct ltk_widget *, struct ltk_widget *);
- int (*remove_child)(struct ltk_widget *, struct ltk_widget *);
- /* x and y relative to widget's lrect! */
- struct ltk_widget *(*get_child_at_pos)(struct ltk_widget *, int x, int y);
- /* r is in self's coordinate system */
- void (*ensure_rect_shown)(struct ltk_widget *self, ltk_rect r);
-
- ltk_widget_type type;
- ltk_widget_flags flags;
- int invalid_signal;
-} ltk_widget_vtable;
-
-void ltk_widget_hide(ltk_widget *widget);
-int ltk_widget_destroy(ltk_widget *widget, int shallow);
-void ltk_fill_widget_defaults(
- ltk_widget *widget, struct ltk_window *window,
- struct ltk_widget_vtable *vtable, int w, int h
-);
-void ltk_widget_change_state(ltk_widget *widget, ltk_widget_state old_state);
-void ltk_widget_resize(ltk_widget *widget);
-ltk_point ltk_widget_pos_to_global(ltk_widget *widget, int x, int y);
-ltk_point ltk_global_to_widget_pos(ltk_widget *widget, int x, int y);
-
-ltk_widget_vtable *ltk_widget_get_editable_vtable(ltk_widget *widget);
-
-#endif /* LTK_WIDGET_H */
diff --git a/src/window.c b/src/window.c
@@ -1,1270 +0,0 @@
-/*
- * Copyright (c) 2020-2024 lumidify <nobody@lumidify.org>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <stdlib.h>
-#include <string.h>
-
-#include "ltk.h"
-#include "util.h"
-#include "keys.h"
-#include "array.h"
-#include "theme.h"
-#include "widget.h"
-#include "window.h"
-#include "memory.h"
-#include "eventdefs.h"
-
-#define MAX_WINDOW_FONT_SIZE 200
-
-static void gen_widget_stack(ltk_widget *bottom);
-static ltk_widget *get_hover_popup(ltk_window *window, int x, int y);
-static int is_parent(ltk_widget *parent, ltk_widget *child);
-static ltk_widget *get_widget_under_pointer(ltk_widget *widget, int x, int y, int *local_x_ret, int *local_y_ret);
-
-static int ltk_window_key_press_event(ltk_widget *self, ltk_key_event *event);
-static int ltk_window_key_release_event(ltk_widget *self, ltk_key_event *event);
-static int ltk_window_mouse_press_event(ltk_widget *self, ltk_button_event *event);
-static int ltk_window_mouse_scroll_event(ltk_widget *self, ltk_scroll_event *event);
-static int ltk_window_mouse_release_event(ltk_widget *self, ltk_button_event *event);
-static int ltk_window_motion_notify_event(ltk_widget *self, ltk_motion_event *event);
-static void ltk_window_redraw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip);
-
-/* FIXME: actually use this properly */
-static struct ltk_widget_vtable vtable = {
- .key_press = <k_window_key_press_event,
- .key_release = <k_window_key_release_event,
- .mouse_press = <k_window_mouse_press_event,
- .mouse_release = <k_window_mouse_release_event,
- .release = NULL,
- .motion_notify = <k_window_motion_notify_event,
- .mouse_leave = NULL,
- .mouse_enter = NULL,
- .change_state = NULL,
- .get_child_at_pos = NULL,
- .resize = NULL,
- .hide = NULL,
- .draw = <k_window_redraw,
- .destroy = <k_window_destroy,
- .child_size_change = NULL,
- .remove_child = NULL,
- .type = LTK_WIDGET_WINDOW,
- .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS,
- .invalid_signal = LTK_WINDOW_SIGNAL_INVALID,
-};
-
-static int cb_focus_active(ltk_window *window, ltk_key_event *event, int handled);
-static int cb_unfocus_active(ltk_window *window, ltk_key_event *event, int handled);
-static int cb_move_prev(ltk_window *window, ltk_key_event *event, int handled);
-static int cb_move_next(ltk_window *window, ltk_key_event *event, int handled);
-static int cb_move_left(ltk_window *window, ltk_key_event *event, int handled);
-static int cb_move_right(ltk_window *window, ltk_key_event *event, int handled);
-static int cb_move_up(ltk_window *window, ltk_key_event *event, int handled);
-static int cb_move_down(ltk_window *window, ltk_key_event *event, int handled);
-static int cb_set_pressed(ltk_window *window, ltk_key_event *event, int handled);
-static int cb_unset_pressed(ltk_window *window, ltk_key_event *event, int handled);
-static int cb_remove_popups(ltk_window *window, ltk_key_event *event, int handled);
-
-struct key_cb {
- char *func_name;
- int (*callback)(ltk_window *, ltk_key_event *, int handled);
-};
-
-static struct key_cb cb_map[] = {
- {"focus-active", &cb_focus_active},
- {"move-down", &cb_move_down},
- {"move-left", &cb_move_left},
- {"move-next", &cb_move_next},
- {"move-prev", &cb_move_prev},
- {"move-right", &cb_move_right},
- {"move-up", &cb_move_up},
- {"remove-popups", &cb_remove_popups},
- {"set-pressed", &cb_set_pressed},
- {"unfocus-active", &cb_unfocus_active},
- {"unset-pressed", &cb_unset_pressed},
-};
-
-struct keypress_cfg {
- ltk_keypress_binding b;
- struct key_cb cb;
-};
-
-struct keyrelease_cfg {
- ltk_keyrelease_binding b;
- struct key_cb cb;
-};
-
-LTK_ARRAY_INIT_DECL_STATIC(keypress, struct keypress_cfg)
-LTK_ARRAY_INIT_IMPL_STATIC(keypress, struct keypress_cfg)
-LTK_ARRAY_INIT_DECL_STATIC(keyrelease, struct keyrelease_cfg)
-LTK_ARRAY_INIT_IMPL_STATIC(keyrelease, struct keyrelease_cfg)
-
-static ltk_array(keypress) *keypresses = NULL;
-static ltk_array(keyrelease) *keyreleases = NULL;
-
-GEN_CB_MAP_HELPERS(cb_map, struct key_cb, func_name)
-
-/* needed for passing keyboard events down the hierarchy */
-static ltk_widget **widget_stack = NULL;
-static size_t widget_stack_alloc = 0;
-static size_t widget_stack_len = 0;
-
-static ltk_window_theme theme;
-static ltk_theme_parseinfo theme_parseinfo[] = {
- {"bg", THEME_COLOR, {.color = &theme.bg}, {.color = "#000000"}, 0, 0, 0},
- {"fg", THEME_COLOR, {.color = &theme.fg}, {.color = "#FFFFFF"}, 0, 0, 0},
- {"font", THEME_STRING, {.str = &theme.font}, {.str = "Monospace"}, 0, 0, 0},
- {"font-size", THEME_INT, {.i = &theme.font_size}, {.i = 15}, 0, MAX_WINDOW_FONT_SIZE, 0},
-};
-static int theme_parseinfo_sorted = 0;
-
-int
-ltk_window_fill_theme_defaults(ltk_renderdata *data) {
- return ltk_theme_fill_defaults(data, "window", theme_parseinfo, LENGTH(theme_parseinfo));
-}
-
-int
-ltk_window_ini_handler(ltk_renderdata *data, const char *prop, const char *value) {
- return ltk_theme_handle_value(data, "window", prop, value, theme_parseinfo, LENGTH(theme_parseinfo), &theme_parseinfo_sorted);
-}
-
-void
-ltk_window_uninitialize_theme(ltk_renderdata *data) {
- ltk_theme_uninitialize(data, theme_parseinfo, LENGTH(theme_parseinfo));
-}
-
-/* FIXME: maybe ltk_fatal if ltk not initialized? */
-ltk_window_theme *
-ltk_window_get_theme(void) {
- return &theme;
-}
-
-/* FIXME: most of this is duplicated code */
-
-int
-ltk_window_register_keypress(const char *func_name, size_t func_len, ltk_keypress_binding b) {
- if (!keypresses)
- keypresses = ltk_array_create(keypress, 1);
- struct key_cb *cb = cb_map_get_entry(func_name, func_len);
- if (!cb)
- return 1;
- struct keypress_cfg cfg = {b, *cb};
- ltk_array_append(keypress, keypresses, cfg);
- return 0;
-}
-
-int
-ltk_window_register_keyrelease(const char *func_name, size_t func_len, ltk_keyrelease_binding b) {
- if (!keyreleases)
- keyreleases = ltk_array_create(keyrelease, 1);
- struct key_cb *cb = cb_map_get_entry(func_name, func_len);
- if (!cb)
- return 1;
- struct keyrelease_cfg cfg = {b, *cb};
- ltk_array_append(keyrelease, keyreleases, cfg);
- return 0;
-}
-
-static void
-destroy_keypress_cfg(struct keypress_cfg cfg) {
- ltk_keypress_binding_destroy(cfg.b);
-}
-
-void
-ltk_window_cleanup(void) {
- ltk_array_destroy_deep(keypress, keypresses, &destroy_keypress_cfg);
- ltk_array_destroy(keyrelease, keyreleases);
- free(widget_stack);
- keypresses = NULL;
- keyreleases = NULL;
- widget_stack = NULL;
-}
-
-static void
-ensure_active_widget_shown(ltk_window *window) {
- ltk_widget *widget = window->active_widget;
- if (!widget)
- return;
- ltk_rect r = widget->lrect;
- while (widget->parent) {
- if (widget->parent->vtable->ensure_rect_shown)
- widget->parent->vtable->ensure_rect_shown(widget->parent, r);
- widget = widget->parent;
- r.x += widget->lrect.x;
- r.y += widget->lrect.y;
- /* FIXME: this currently just aborts if a widget is positioned
- absolutely because I'm not sure what the best action would
- be in that case */
- if (widget->popup)
- break;
- }
- ltk_window_invalidate_widget_rect(window, widget);
-}
-
-/* FIXME: should keyrelease events be ignored if the corresponding keypress event
- was consumed for movement? */
-/* FIXME: check if there's any weirdness when combining return and mouse press */
-/* FIXME: maybe it doesn't really make sense to make e.g. text entry pressed when enter is pressed? */
-/* FIXME: implement key binding flag to run before widget handler is called */
-static int
-ltk_window_key_press_event(ltk_widget *self, ltk_key_event *event) {
- ltk_window *window = LTK_CAST_WINDOW(self);
- int handled = 0;
- if (window->active_widget && (window->active_widget->state & LTK_FOCUSED)) {
- gen_widget_stack(window->active_widget);
- for (size_t i = widget_stack_len; i-- > 0 && !handled;) {
- if (widget_stack[i]->vtable->key_press && widget_stack[i]->vtable->key_press(widget_stack[i], event)) {
- handled = 1;
- break;
- }
- }
- }
- if (!keypresses)
- return 1;
- ltk_keypress_binding *b = NULL;
- for (size_t i = 0; i < ltk_array_len(keypresses); i++) {
- b = <k_array_get(keypresses, i).b;
- if (b->mods != event->modmask || (!(b->flags & LTK_KEY_BINDING_RUN_ALWAYS) && handled)) {
- continue;
- } else if (b->text) {
- if (event->mapped && !strcmp(b->text, event->mapped))
- handled |= ltk_array_get(keypresses, i).cb.callback(window, event, handled);
- } else if (b->rawtext) {
- if (event->text && !strcmp(b->text, event->text))
- handled |= ltk_array_get(keypresses, i).cb.callback(window, event, handled);
- } else if (b->sym != LTK_KEY_NONE) {
- if (event->sym == b->sym)
- handled |= ltk_array_get(keypresses, i).cb.callback(window, event, handled);
- }
- }
- return 1;
-}
-
-/* FIXME: need to actually check if any of parent widgets are focused and still pass to them even if bottom widget not focused? */
-static int
-ltk_window_key_release_event(ltk_widget *self, ltk_key_event *event) {
- ltk_window *window = LTK_CAST_WINDOW(self);
- /* FIXME: emit event */
- int handled = 0;
- if (window->active_widget && (window->active_widget->state & LTK_FOCUSED)) {
- gen_widget_stack(window->active_widget);
- for (size_t i = widget_stack_len; i-- > 0 && !handled;) {
- if (widget_stack[i]->vtable->key_release && widget_stack[i]->vtable->key_release(widget_stack[i], event)) {
- handled = 1;
- break;
- }
- }
- }
- if (!keyreleases)
- return 1;
- ltk_keyrelease_binding *b = NULL;
- for (size_t i = 0; i < ltk_array_len(keyreleases); i++) {
- b = <k_array_get(keyreleases, i).b;
- if (b->mods != event->modmask || (!(b->flags & LTK_KEY_BINDING_RUN_ALWAYS) && handled)) {
- continue;
- } else if (b->sym != LTK_KEY_NONE && event->sym == b->sym) {
- handled |= ltk_array_get(keyreleases, i).cb.callback(window, event, handled);
- }
- }
- return 1;
-}
-
-/* FIXME: This is still weird. */
-static int
-ltk_window_mouse_press_event(ltk_widget *self, ltk_button_event *event) {
- ltk_window *window = LTK_CAST_WINDOW(self);
- ltk_widget *widget = get_hover_popup(window, event->x, event->y);
- int check_hide = 0;
- if (!widget) {
- widget = window->root_widget;
- check_hide = 1;
- }
- if (!widget) {
- ltk_window_unregister_all_popups(window);
- return 1;
- }
- int orig_x = event->x, orig_y = event->y;
- ltk_widget *cur_widget = get_widget_under_pointer(widget, event->x, event->y, &event->x, &event->y);
- /* FIXME: need to add more flags for more fine-grained control
- -> also, should the widget still get mouse_press even if state doesn't change? */
- /* FIXME: doesn't work with e.g. disabled menu entries */
- if (!(cur_widget->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
- ltk_window_unregister_all_popups(window);
- }
-
- /* FIXME: this doesn't make much sense if the popups aren't a
- hierarchy (right now, they're just menus, so that's always
- a hierarchy */
- /* don't hide popups if they are children of the now pressed widget */
- if (check_hide && !(window->popups_num > 0 && is_parent(cur_widget, window->popups[0])))
- ltk_window_unregister_all_popups(window);
-
- /* FIXME: popups don't always have their children geometrically contained within parents,
- so this won't work properly in all cases */
- int first = 1;
- while (cur_widget) {
- int handled = 0;
- ltk_point local = ltk_global_to_widget_pos(cur_widget, orig_x, orig_y);
- event->x = local.x;
- event->y = local.y;
- if (cur_widget->state != LTK_DISABLED) {
- /* FIXME: figure out whether this makes sense - currently, all widgets (unless disabled)
- get mouse press, but they are only set to pressed if they are activatable */
- if (cur_widget->vtable->mouse_press)
- handled = cur_widget->vtable->mouse_press(cur_widget, event);
- /* set first non-disabled widget to pressed widget */
- /* FIXME: use config values for all_activatable */
- if (first && event->button == LTK_BUTTONL && event->type == LTK_BUTTONPRESS_EVENT && (cur_widget->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
- ltk_window_set_pressed_widget(window, cur_widget, 0);
- first = 0;
- }
- }
- if (!handled)
- cur_widget = cur_widget->parent;
- else
- break;
- }
- return 1;
-}
-
-static int
-ltk_window_mouse_scroll_event(ltk_widget *self, ltk_scroll_event *event) {
- ltk_window *window = LTK_CAST_WINDOW(self);
- /* FIXME: should it first be sent to pressed widget? */
- ltk_widget *widget = get_hover_popup(window, event->x, event->y);
- if (!widget)
- widget = window->root_widget;
- if (!widget)
- return 1;
- int orig_x = event->x, orig_y = event->y;
- ltk_widget *cur_widget = get_widget_under_pointer(widget, event->x, event->y, &event->x, &event->y);
- /* FIXME: same issue with popups like in mouse_press above */
- while (cur_widget) {
- int handled = 0;
- ltk_point local = ltk_global_to_widget_pos(cur_widget, orig_x, orig_y);
- event->x = local.x;
- event->y = local.y;
- if (cur_widget->state != LTK_DISABLED) {
- /* FIXME: see function above
- if (queue_scroll_event(cur_widget, event->x, event->y, event->dx, event->dy))
- handled = 1; */
- if (cur_widget->vtable->mouse_scroll)
- handled = cur_widget->vtable->mouse_scroll(cur_widget, event);
- }
- if (!handled)
- cur_widget = cur_widget->parent;
- else
- break;
- }
- return 1;
-}
-
-void
-ltk_window_fake_motion_event(ltk_window *window, int x, int y) {
- ltk_motion_event e = {.type = LTK_MOTION_EVENT, .x = x, .y = y};
- /* FIXME: call overwritten method */
- window->widget.vtable->motion_notify(LTK_CAST_WIDGET(window), &e);
-}
-
-static int
-ltk_window_mouse_release_event(ltk_widget *self, ltk_button_event *event) {
- ltk_window *window = LTK_CAST_WINDOW(self);
- ltk_widget *widget = window->pressed_widget;
- int orig_x = event->x, orig_y = event->y;
- /* FIXME: why does this only take pressed widget and popups into account? */
- if (!widget) {
- widget = get_hover_popup(window, event->x, event->y);
- widget = get_widget_under_pointer(widget, event->x, event->y, &event->x, &event->y);
- }
- /* FIXME: loop up to top of hierarchy if not handled */
- /* FIXME: see functions above
- if (widget && queue_mouse_event(widget, event->type, event->x, event->y)) { */
- /* NOP */
- if (widget) {
- if (widget->vtable->mouse_release)
- widget->vtable->mouse_release(widget, event);
- }
- if (event->button == LTK_BUTTONL && event->type == LTK_BUTTONRELEASE_EVENT) {
- int release = 0;
- if (window->pressed_widget) {
- ltk_rect prect = window->pressed_widget->lrect;
- ltk_point pglob = ltk_widget_pos_to_global(window->pressed_widget, 0, 0);
- if (ltk_collide_rect((ltk_rect){pglob.x, pglob.y, prect.w, prect.h}, orig_x, orig_y))
- release = 1;
- }
- ltk_window_set_pressed_widget(window, NULL, release);
- /* send motion notify to widget under pointer */
- /* FIXME: only when not collide with rect? */
- ltk_window_fake_motion_event(window, orig_x, orig_y);
- }
- return 1;
-}
-
-static int
-ltk_window_motion_notify_event(ltk_widget *self, ltk_motion_event *event) {
- ltk_window *window = LTK_CAST_WINDOW(self);
- ltk_widget *widget = get_hover_popup(window, event->x, event->y);
- int orig_x = event->x, orig_y = event->y;
- if (!widget) {
- widget = window->pressed_widget;
- if (widget) {
- ltk_point local = ltk_global_to_widget_pos(widget, event->x, event->y);
- event->x = local.x;
- event->y = local.y;
- if (widget->vtable->motion_notify)
- widget->vtable->motion_notify(widget, event);
- return 1;
- }
- widget = window->root_widget;
- }
- if (!widget)
- return 1;
- ltk_point local = ltk_global_to_widget_pos(widget, event->x, event->y);
- if (!ltk_collide_rect((ltk_rect){0, 0, widget->lrect.w, widget->lrect.h}, local.x, local.y)) {
- ltk_window_set_hover_widget(widget->window, NULL, event);
- return 1;
- }
- ltk_widget *cur_widget = get_widget_under_pointer(widget, event->x, event->y, &event->x, &event->y);
- int first = 1;
- while (cur_widget) {
- int handled = 0;
- ltk_point local = ltk_global_to_widget_pos(cur_widget, orig_x, orig_y);
- event->x = local.x;
- event->y = local.y;
- if (cur_widget->state != LTK_DISABLED) {
- /* FIXME: see functions above
- if (queue_mouse_event(cur_widget, LTK_MOTION_EVENT, event->x, event->y))
- handled = 1; */
- if (cur_widget->vtable->motion_notify)
- handled = cur_widget->vtable->motion_notify(cur_widget, event);
- /* set first non-disabled widget to hover widget */
- /* FIXME: should enter/leave event be sent to parent
- when moving from/to widget nested in parent? */
- /* FIXME: use config values for all_activatable */
- if (first && (cur_widget->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
- event->x = orig_x;
- event->y = orig_y;
- ltk_window_set_hover_widget(window, cur_widget, event);
- first = 0;
- }
- }
- if (!handled)
- cur_widget = cur_widget->parent;
- else
- break;
- }
- if (first) {
- event->x = orig_x;
- event->y = orig_y;
- ltk_window_set_hover_widget(window, NULL, event);
- }
- return 1;
-}
-
-void
-ltk_window_set_root_widget(ltk_window *window, ltk_widget *widget) {
- window->root_widget = widget;
- widget->lrect.x = 0;
- widget->lrect.y = 0;
- widget->lrect.w = window->rect.w;
- widget->lrect.h = window->rect.h;
- widget->crect = widget->lrect;
- ltk_window_invalidate_rect(window, widget->lrect);
- ltk_widget_resize(widget);
-}
-
-void
-ltk_window_invalidate_rect(ltk_window *window, ltk_rect rect) {
- if (window->dirty_rect.w == 0 && window->dirty_rect.h == 0)
- window->dirty_rect = rect;
- else
- window->dirty_rect = ltk_rect_union(rect, window->dirty_rect);
-}
-
-void
-ltk_window_invalidate_widget_rect(ltk_window *window, ltk_widget *widget) {
- ltk_point glob = ltk_widget_pos_to_global(widget, 0, 0);
- ltk_window_invalidate_rect(window, (ltk_rect){glob.x, glob.y, widget->lrect.w, widget->lrect.h});
-}
-
-static void
-ltk_window_redraw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
- (void)draw_surf;
- (void)x;
- (void)y;
- (void)clip;
- ltk_window *window = LTK_CAST_WINDOW(self);
- ltk_widget *ptr;
- if (!window) return;
- if (window->dirty_rect.x >= window->rect.w) return;
- if (window->dirty_rect.y >= window->rect.h) return;
- if (window->dirty_rect.x + window->dirty_rect.w > window->rect.w)
- window->dirty_rect.w -= window->dirty_rect.x + window->dirty_rect.w - window->rect.w;
- if (window->dirty_rect.y + window->dirty_rect.h > window->rect.h)
- window->dirty_rect.h -= window->dirty_rect.y + window->dirty_rect.h - window->rect.h;
- /* FIXME: this should use window->dirty_rect, but that doesn't work
- properly with double buffering */
- ltk_surface_fill_rect(window->surface, window->theme->bg, (ltk_rect){0, 0, window->rect.w, window->rect.h});
- if (window->root_widget) {
- ptr = window->root_widget;
- ptr->vtable->draw(ptr, window->surface, 0, 0, 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->surface, ptr->lrect.x, ptr->lrect.y, ltk_rect_relative(ptr->lrect, window->rect));
- }
- renderer_swap_buffers(window->renderwindow);
- window->dirty_rect.w = 0;
- window->dirty_rect.h = 0;
-}
-
-static void
-ltk_window_other_event(ltk_window *window, ltk_event *event) {
- ltk_widget *ptr = window->root_widget;
- /* FIXME: decide whether this should be moved to separate resize function in window vtable */
- if (event->type == LTK_CONFIGURE_EVENT) {
- ltk_window_unregister_all_popups(window);
- int w, h;
- w = event->configure.w;
- h = event->configure.h;
- int orig_w = window->rect.w;
- int orig_h = window->rect.h;
- if (orig_w != w || orig_h != h) {
- window->rect.w = w;
- window->rect.h = h;
- ltk_window_invalidate_rect(window, window->rect);
- ltk_surface_update_size(window->surface, w, h);
- if (ptr) {
- ptr->lrect.w = w;
- ptr->lrect.h = h;
- ptr->crect = ptr->lrect;
- ltk_widget_resize(ptr);
- }
- }
- } else if (event->type == LTK_EXPOSE_EVENT) {
- ltk_rect r;
- r.x = event->expose.x;
- r.y = event->expose.y;
- r.w = event->expose.w;
- r.h = event->expose.h;
- ltk_window_invalidate_rect(window, r);
- } else if (event->type == LTK_WINDOWCLOSE_EVENT) {
- ltk_widget_emit_signal(LTK_CAST_WIDGET(window), LTK_WINDOW_SIGNAL_CLOSE, LTK_EMPTY_ARGLIST);
- }
-}
-
-/* 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;
- popup->popup = 1;
-}
-
-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) {
- popup->popup = 0;
- 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++) {
- window->popups[i]->hidden = 1;
- window->popups[i]->popup = 0;
- ltk_widget_hide(window->popups[i]);
- }
- 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);
-}
-
-/* FIXME: support more options like child windows */
-ltk_window *
-ltk_window_create_intern(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h) {
- ltk_window *window = ltk_malloc(sizeof(ltk_window));
- /* this is a bit weird because the window entry points to itself */
- /* the ideal width isn't needed for a window */
- ltk_fill_widget_defaults(&window->widget, window, &vtable, 0, 0);
-
- window->popups = NULL;
- window->popups_num = window->popups_alloc = 0;
- window->popups_locked = 0;
-
- window->renderwindow = renderer_create_window(data, title, x, y, w, h);
- renderer_set_window_properties(window->renderwindow, theme.bg);
- window->theme = &theme;
-
- window->root_widget = NULL;
- window->hover_widget = NULL;
- window->active_widget = NULL;
- window->pressed_widget = NULL;
-
- //FIXME: use widget rect
- window->rect.w = w;
- window->rect.h = h;
- window->rect.x = 0;
- window->rect.y = 0;
- window->dirty_rect.w = 0;
- window->dirty_rect.h = 0;
- window->dirty_rect.x = 0;
- window->dirty_rect.y = 0;
-
- window->surface_cache = ltk_surface_cache_create(window->renderwindow);
- window->surface = ltk_surface_from_window(window->renderwindow, w, h);
-
- return window;
-}
-
-/* FIXME: check if widget window matches in all public functions */
-
-void
-ltk_window_destroy_intern(ltk_window *window) {
- if (window->root_widget) {
- ltk_widget_destroy(window->root_widget, 0);
- }
- if (window->popups)
- ltk_free(window->popups);
- ltk_surface_cache_destroy(window->surface_cache);
- ltk_surface_destroy(window->surface);
- renderer_destroy_window(window->renderwindow);
- ltk_free(window);
-}
-
-/* event must have global coordinates! */
-void
-ltk_window_set_hover_widget(ltk_window *window, ltk_widget *widget, ltk_motion_event *event) {
- ltk_widget *old = window->hover_widget;
- if (old == widget)
- return;
- int orig_x = event->x, orig_y = event->y;
- if (old) {
- ltk_widget_state old_state = old->state;
- old->state &= ~LTK_HOVER;
- ltk_widget_change_state(old, old_state);
- ltk_point local = ltk_global_to_widget_pos(old, event->x, event->y);
- event->x = local.x;
- event->y = local.y;
- if (old->vtable->mouse_leave)
- old->vtable->mouse_leave(old, event);
- event->x = orig_x;
- event->y = orig_y;
- }
- window->hover_widget = widget;
- if (widget) {
- ltk_point local = ltk_global_to_widget_pos(widget, event->x, event->y);
- event->x = local.x;
- event->y = local.y;
- if (widget->vtable->mouse_enter)
- widget->vtable->mouse_enter(widget, event);
- ltk_widget_state old_state = widget->state;
- widget->state |= LTK_HOVER;
- ltk_widget_change_state(widget, old_state);
- if ((widget->vtable->flags & LTK_HOVER_IS_ACTIVE) && widget != window->active_widget)
- ltk_window_set_active_widget(window, widget);
- }
-}
-
-void
-ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget) {
- if (window->active_widget == widget) {
- return;
- }
- ltk_widget *old = window->active_widget;
- /* Note: this has to be set at the beginning to
- avoid infinite recursion in some cases */
- window->active_widget = widget;
- ltk_widget *common_parent = NULL;
- if (widget) {
- ltk_widget *cur = widget;
- while (cur) {
- if (cur->state & LTK_ACTIVE) {
- common_parent = cur;
- break;
- }
- ltk_widget_state old_state = cur->state;
- cur->state |= LTK_ACTIVE;
- /* FIXME: should all be set focused? */
- if (cur == widget && !(cur->vtable->flags & LTK_NEEDS_KEYBOARD))
- widget->state |= LTK_FOCUSED;
- ltk_widget_change_state(cur, old_state);
- cur = cur->parent;
- }
- }
- /* FIXME: better variable names; generally make this nicer */
- /* special case if old is parent of new active widget */
- ltk_widget *tmp = common_parent;
- while (tmp) {
- if (tmp == old)
- return;
- tmp = tmp->parent;
- }
- if (old) {
- old->state &= ~LTK_FOCUSED;
- ltk_widget *cur = old;
- while (cur) {
- if (cur == common_parent)
- break;
- ltk_widget_state old_state = cur->state;
- cur->state &= ~LTK_ACTIVE;
- ltk_widget_change_state(cur, old_state);
- cur = cur->parent;
- }
- }
-}
-
-void
-ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget, int release) {
- if (window->pressed_widget == widget)
- return;
- if (window->pressed_widget) {
- ltk_widget_state old_state = window->pressed_widget->state;
- window->pressed_widget->state &= ~LTK_PRESSED;
- ltk_widget_change_state(window->pressed_widget, old_state);
- ltk_window_set_active_widget(window, window->pressed_widget);
- /* FIXME: this is a bit weird because the release handler for menuentry
- indirectly calls ltk_widget_hide, which messes with the pressed widget */
- /* FIXME: isn't it redundant to check that state is pressed? */
- if (release && (old_state & LTK_PRESSED)) {
- if (window->pressed_widget->vtable->release)
- window->pressed_widget->vtable->release(window->pressed_widget);
- }
- }
- window->pressed_widget = widget;
- if (widget) {
- if (widget->vtable->press)
- widget->vtable->press(widget);
- ltk_widget_state old_state = widget->state;
- widget->state |= LTK_PRESSED;
- ltk_widget_change_state(widget, old_state);
- }
-}
-
-void
-ltk_window_handle_event(ltk_window *window, ltk_event *event) {
- switch (event->type) {
- case LTK_KEYPRESS_EVENT:
- ltk_window_key_press_event(LTK_CAST_WIDGET(window), &event->key);
- break;
- case LTK_KEYRELEASE_EVENT:
- ltk_window_key_release_event(LTK_CAST_WIDGET(window), &event->key);
- break;
- case LTK_BUTTONPRESS_EVENT:
- case LTK_2BUTTONPRESS_EVENT:
- case LTK_3BUTTONPRESS_EVENT:
- ltk_window_mouse_press_event(LTK_CAST_WIDGET(window), &event->button);
- break;
- case LTK_SCROLL_EVENT:
- ltk_window_mouse_scroll_event(LTK_CAST_WIDGET(window), &event->scroll);
- break;
- case LTK_BUTTONRELEASE_EVENT:
- case LTK_2BUTTONRELEASE_EVENT:
- case LTK_3BUTTONRELEASE_EVENT:
- ltk_window_mouse_release_event(LTK_CAST_WIDGET(window), &event->button);
- break;
- case LTK_MOTION_EVENT:
- ltk_window_motion_notify_event(LTK_CAST_WIDGET(window), &event->motion);
- break;
- default:
- ltk_window_other_event(window, event);
- }
-}
-
-/* x and y are global! */
-static ltk_widget *
-get_widget_under_pointer(ltk_widget *widget, int x, int y, int *local_x_ret, int *local_y_ret) {
- ltk_point glob = ltk_widget_pos_to_global(widget, 0, 0);
- ltk_widget *next = NULL;
- *local_x_ret = x - glob.x;
- *local_y_ret = y - glob.y;
- while (widget && widget->vtable->get_child_at_pos) {
- next = widget->vtable->get_child_at_pos(widget, *local_x_ret, *local_y_ret);
- if (!next) {
- break;
- } else {
- widget = next;
- if (next->popup) {
- *local_x_ret = x - next->lrect.x;
- *local_y_ret = y - next->lrect.y;
- } else {
- *local_x_ret -= next->lrect.x;
- *local_y_ret -= next->lrect.y;
- }
- }
- }
- return 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]->crect, x, y))
- return window->popups[i];
- }
- return NULL;
-}
-
-static int
-is_parent(ltk_widget *parent, ltk_widget *child) {
- while (child && child != parent) {
- child = child->parent;
- }
- return child != NULL;
-}
-
-/* FIXME: come up with a more elegant way to handle this? */
-/* FIXME: Handle hidden state here instead of in widgets */
-/* FIXME: handle disabled state */
-static int
-prev_child(ltk_window *window) {
- if (!window->root_widget)
- return 0;
- ltk_config *config = ltk_config_get();
- ltk_widget_flags act_flags = config->general.all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
- ltk_widget *new, *cur = window->active_widget;
- int changed = 0;
- ltk_widget *prevcur = cur;
- while (1) {
- if (cur) {
- while (cur->parent) {
- new = NULL;
- if (cur->parent->vtable->prev_child)
- new = cur->parent->vtable->prev_child(cur->parent, cur);
- if (new) {
- cur = new;
- ltk_widget *last_activatable = (cur->vtable->flags & act_flags) ? cur : NULL;
- while (cur->vtable->last_child && (new = cur->vtable->last_child(cur))) {
- cur = new;
- if (cur->vtable->flags & act_flags)
- last_activatable = cur;
- }
- if (last_activatable) {
- cur = last_activatable;
- changed = 1;
- break;
- }
- } else {
- cur = cur->parent;
- if (cur->vtable->flags & act_flags) {
- changed = 1;
- break;
- }
- }
- }
- }
- if (!changed) {
- cur = window->root_widget;
- ltk_widget *last_activatable = (cur->vtable->flags & act_flags) ? cur : NULL;
- while (cur->vtable->last_child && (new = cur->vtable->last_child(cur))) {
- cur = new;
- if (cur->vtable->flags & act_flags)
- last_activatable = cur;
- }
- if (last_activatable)
- cur = last_activatable;
- }
- if (prevcur == cur || (cur && (cur->vtable->flags & act_flags)))
- break;
- prevcur = cur;
- }
- /* FIXME: What exactly should be done if no activatable widget exists? */
- if (cur != window->active_widget) {
- ltk_window_set_active_widget(window, cur);
- ensure_active_widget_shown(window);
- return 1;
- }
- return 0;
-}
-
-static int
-next_child(ltk_window *window) {
- if (!window->root_widget)
- return 0;
- ltk_config *config = ltk_config_get();
- ltk_widget_flags act_flags = config->general.all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
- ltk_widget *new, *cur = window->active_widget;
- int changed = 0;
- ltk_widget *prevcur = cur;
- while (1) {
- if (cur) {
-
- while (cur->vtable->first_child && (new = cur->vtable->first_child(cur))) {
- cur = new;
- if (cur->vtable->flags & act_flags) {
- changed = 1;
- break;
- }
- }
- if (!changed) {
- while (cur->parent) {
- new = NULL;
- if (cur->parent->vtable->next_child)
- new = cur->parent->vtable->next_child(cur->parent, cur);
- if (new) {
- cur = new;
- if (cur->vtable->flags & act_flags) {
- changed = 1;
- break;
- }
- while (cur->vtable->first_child && (new = cur->vtable->first_child(cur))) {
- cur = new;
- if (cur->vtable->flags & act_flags) {
- changed = 1;
- break;
- }
- }
- if (changed)
- break;
- } else {
- cur = cur->parent;
- }
- }
- }
- }
- if (!changed) {
- cur = window->root_widget;
- if (!(cur->vtable->flags & act_flags)) {
- while (cur->vtable->first_child && (new = cur->vtable->first_child(cur))) {
- cur = new;
- if (cur->vtable->flags & act_flags)
- break;
- }
- }
- if (!(cur->vtable->flags & act_flags))
- cur = window->root_widget;
- }
- if (prevcur == cur || (cur && (cur->vtable->flags & act_flags)))
- break;
- prevcur = cur;
- }
- if (cur != window->active_widget) {
- ltk_window_set_active_widget(window, cur);
- ensure_active_widget_shown(window);
- return 1;
- }
- return 0;
-}
-
-/* FIXME: moving up/down/left/right needs to be rethought
- it generally is a bit weird, and in particular, nearest_child always searches for the child
- that has the smallest distance to the given rect, so it may not be the child that the user
- expects when going down (e.g. a vertical box with one widget closer vertically but on the
- other side horizontally, thus possibly leading to a different widget that is farther away
- vertically to be chosen instead) - what would be logical here? */
-static ltk_widget *
-nearest_child(ltk_widget *widget, ltk_rect r) {
- ltk_point local = ltk_global_to_widget_pos(widget, r.x, r.y);
- ltk_rect rect = {local.x, local.y, r.w, r.h};
- if (widget->vtable->nearest_child)
- return widget->vtable->nearest_child(widget, rect);
- return NULL;
-}
-
-/* FIXME: maybe wrap around in these two functions? */
-static int
-left_top_child(ltk_window *window, int left) {
- if (!window->root_widget)
- return 0;
- ltk_config *config = ltk_config_get();
- ltk_widget_flags act_flags = config->general.all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
- ltk_widget *new, *cur = window->active_widget;
- ltk_rect old_rect = {0, 0, 0, 0};
- ltk_widget *last_activatable = NULL;
- if (!cur) {
- cur = window->root_widget;
- if (cur->vtable->flags & act_flags)
- last_activatable = cur;
- ltk_rect r = {cur->lrect.w, cur->lrect.h, 0, 0};
- while ((new = nearest_child(cur, r))) {
- cur = new;
- if (cur->vtable->flags & act_flags)
- last_activatable = cur;
- }
- }
- if (last_activatable) {
- cur = last_activatable;
- } else if (cur) {
- ltk_point glob = cur->parent ? ltk_widget_pos_to_global(cur->parent, cur->lrect.x, cur->lrect.y) : (ltk_point){cur->lrect.x, cur->lrect.y};
- old_rect = (ltk_rect){glob.x, glob.y, cur->lrect.w, cur->lrect.h};
- while (cur->parent) {
- new = NULL;
- if (left) {
- if (cur->parent->vtable->nearest_child_left)
- new = cur->parent->vtable->nearest_child_left(cur->parent, cur);
- } else {
- if (cur->parent->vtable->nearest_child_above)
- new = cur->parent->vtable->nearest_child_above(cur->parent, cur);
- }
- if (new) {
- cur = new;
- ltk_widget *last_activatable = (cur->vtable->flags & act_flags) ? cur : NULL;
- while ((new = nearest_child(cur, old_rect))) {
- cur = new;
- if (cur->vtable->flags & act_flags)
- last_activatable = cur;
- }
- if (last_activatable) {
- cur = last_activatable;
- break;
- }
- } else {
- cur = cur->parent;
- if (cur->vtable->flags & act_flags) {
- break;
- }
- }
- }
- }
- /* FIXME: What exactly should be done if no activatable widget exists? */
- if (cur && cur != window->active_widget && (cur->vtable->flags & act_flags)) {
- ltk_window_set_active_widget(window, cur);
- ensure_active_widget_shown(window);
- return 1;
- }
- return 0;
-}
-
-static int
-right_bottom_child(ltk_window *window, int right) {
- if (!window->root_widget)
- return 0;
- ltk_config *config = ltk_config_get();
- ltk_widget_flags act_flags = config->general.all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
- ltk_widget *new, *cur = window->active_widget;
- int changed = 0;
- ltk_rect old_rect = {0, 0, 0, 0};
- ltk_rect corner = {0, 0, 0, 0};
- int found_activatable = 0;
- if (!cur) {
- cur = window->root_widget;
- if (!(cur->vtable->flags & act_flags)) {
- while ((new = nearest_child(cur, (ltk_rect){0, 0, 0, 0}))) {
- cur = new;
- if (cur->vtable->flags & act_flags) {
- found_activatable = 1;
- break;
- }
- }
- }
- }
- if (!found_activatable) {
- ltk_point glob = cur->parent ? ltk_widget_pos_to_global(cur->parent, cur->lrect.x, cur->lrect.y) : (ltk_point){cur->lrect.x, cur->lrect.y};
- corner = (ltk_rect){glob.x, glob.y, 0, 0};
- old_rect = (ltk_rect){glob.x, glob.y, cur->lrect.w, cur->lrect.h};
- while ((new = nearest_child(cur, corner))) {
- cur = new;
- if (cur->vtable->flags & act_flags) {
- changed = 1;
- break;
- }
- }
- if (!changed) {
- while (cur->parent) {
- new = NULL;
- if (right) {
- if (cur->parent->vtable->nearest_child_right)
- new = cur->parent->vtable->nearest_child_right(cur->parent, cur);
- } else {
- if (cur->parent->vtable->nearest_child_below)
- new = cur->parent->vtable->nearest_child_below(cur->parent, cur);
- }
- if (new) {
- cur = new;
- if (cur->vtable->flags & act_flags) {
- changed = 1;
- break;
- }
- while ((new = nearest_child(cur, old_rect))) {
- cur = new;
- if (cur->vtable->flags & act_flags) {
- changed = 1;
- break;
- }
- }
- if (changed)
- break;
- } else {
- cur = cur->parent;
- }
- }
- }
- }
- if (cur && cur != window->active_widget && (cur->vtable->flags & act_flags)) {
- ltk_window_set_active_widget(window, cur);
- ensure_active_widget_shown(window);
- return 1;
- }
- return 0;
-}
-
-/* FIXME: maybe just set this when active widget changes */
-/* -> but would also need to change it when widgets are created/destroyed or parents change */
-static void
-gen_widget_stack(ltk_widget *bottom) {
- widget_stack_len = 0;
- while (bottom) {
- if (widget_stack_len + 1 > widget_stack_alloc) {
- widget_stack_alloc = ideal_array_size(widget_stack_alloc, widget_stack_len + 1);
- widget_stack = ltk_reallocarray(widget_stack, widget_stack_alloc, sizeof(ltk_widget *));
- }
- widget_stack[widget_stack_len++] = bottom;
- bottom = bottom->parent;
- }
-}
-
-/* FIXME: The focus behavior needs to be rethought. It's currently hard-coded in the vtable for each
- widget type, but what if the program using ltk wants to catch keyboard events even if the widget
- doesn't do that by default? */
-static int
-cb_focus_active(ltk_window *window, ltk_key_event *event, int handled) {
- (void)event;
- (void)handled;
- if (window->active_widget && !(window->active_widget->state & LTK_FOCUSED)) {
- /* FIXME: maybe also set widgets above in hierarchy? */
- ltk_widget_state old_state = window->active_widget->state;
- window->active_widget->state |= LTK_FOCUSED;
- ltk_widget_change_state(window->active_widget, old_state);
- return 1;
- }
- return 0;
-}
-
-static int
-cb_unfocus_active(ltk_window *window, ltk_key_event *event, int handled) {
- (void)event;
- (void)handled;
- if (window->active_widget && (window->active_widget->state & LTK_FOCUSED) && (window->active_widget->vtable->flags & LTK_NEEDS_KEYBOARD)) {
- ltk_widget_state old_state = window->active_widget->state;
- window->active_widget->state &= ~LTK_FOCUSED;
- ltk_widget_change_state(window->active_widget, old_state);
- return 1;
- }
- return 0;
-}
-
-static int
-cb_move_prev(ltk_window *window, ltk_key_event *event, int handled) {
- (void)event;
- (void)handled;
- return prev_child(window);
-}
-
-static int
-cb_move_next(ltk_window *window, ltk_key_event *event, int handled) {
- (void)event;
- (void)handled;
- return next_child(window);
-}
-
-static int
-cb_move_left(ltk_window *window, ltk_key_event *event, int handled) {
- (void)event;
- (void)handled;
- return left_top_child(window, 1);
-}
-
-static int
-cb_move_right(ltk_window *window, ltk_key_event *event, int handled) {
- (void)event;
- (void)handled;
- return right_bottom_child(window, 1);
-}
-
-static int
-cb_move_up(ltk_window *window, ltk_key_event *event, int handled) {
- (void)event;
- (void)handled;
- return left_top_child(window, 0);
-}
-
-static int
-cb_move_down(ltk_window *window, ltk_key_event *event, int handled) {
- (void)event;
- (void)handled;
- return right_bottom_child(window, 0);
-}
-
-static int
-cb_set_pressed(ltk_window *window, ltk_key_event *event, int handled) {
- (void)event;
- (void)handled;
- if (window->active_widget && (window->active_widget->state & LTK_FOCUSED)) {
- /* FIXME: only set pressed if needs keyboard? */
- ltk_window_set_pressed_widget(window, window->active_widget, 0);
- return 1;
- }
- return 0;
-}
-
-static int
-cb_unset_pressed(ltk_window *window, ltk_key_event *event, int handled) {
- (void)event;
- (void)handled;
- if (window->pressed_widget) {
- ltk_window_set_pressed_widget(window, NULL, 1);
- return 1;
- }
- return 0;
-}
-
-static int
-cb_remove_popups(ltk_window *window, ltk_key_event *event, int handled) {
- (void)event;
- (void)handled;
- if (window->popups_num > 0) {
- ltk_window_unregister_all_popups(window);
- return 1;
- }
- return 0;
-}