ltk

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

commit 9de2d4ac1734343f1de02c9ec29e408ccc05689c
parent 43bb385257c126c200662ed207f27a7a285f113e
Author: lumidify <nobody@lumidify.org>
Date:   Mon, 25 Mar 2024 19:16:53 +0100

Re-add client-server functionality

Diffstat:
M.gitignore | 1-
MLICENSE | 4++--
MMakefile | 190+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
MREADME.md | 17++++++++++++++++-
Dexamples/.gitignore | 2--
Aexamples/ltk/.gitignore | 2++
Aexamples/ltk/test.c | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rexamples/test.jpg -> examples/ltk/test.jpg | 0
Aexamples/ltkd/.gitignore | 1+
Aexamples/ltkd/test.gui | 27+++++++++++++++++++++++++++
Aexamples/ltkd/test.sh | 23+++++++++++++++++++++++
Aexamples/ltkd/test2.gui | 45+++++++++++++++++++++++++++++++++++++++++++++
Aexamples/ltkd/test2.sh | 11+++++++++++
Aexamples/ltkd/test3.gui | 16++++++++++++++++
Aexamples/ltkd/test3.sh | 21+++++++++++++++++++++
Aexamples/ltkd/testbox.sh | 30++++++++++++++++++++++++++++++
Aexamples/ltkd/testimg.sh | 14++++++++++++++
Dexamples/test.c | 87-------------------------------------------------------------------------------
Dsrc/box.c | 470-------------------------------------------------------------------------------
Dsrc/graphics.h | 76----------------------------------------------------------------------------
Dsrc/graphics_xlib.c | 600-------------------------------------------------------------------------------
Dsrc/grid.c | 546-------------------------------------------------------------------------------
Dsrc/ltk.c | 672-------------------------------------------------------------------------------
Dsrc/ltk.h | 51---------------------------------------------------
Rsrc/.gitignore -> src/ltk/.gitignore | 0
Rsrc/array.h -> src/ltk/array.h | 0
Asrc/ltk/box.c | 470+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/box.h -> src/ltk/box.h | 0
Rsrc/button.c -> src/ltk/button.c | 0
Rsrc/button.h -> src/ltk/button.h | 0
Rsrc/clipboard.h -> src/ltk/clipboard.h | 0
Rsrc/clipboard_xlib.c -> src/ltk/clipboard_xlib.c | 0
Rsrc/clipboard_xlib.h -> src/ltk/clipboard_xlib.h | 0
Rsrc/color.h -> src/ltk/color.h | 0
Rsrc/color_xlib.c -> src/ltk/color_xlib.c | 0
Rsrc/config.c -> src/ltk/config.c | 0
Rsrc/config.h -> src/ltk/config.h | 0
Rsrc/ctrlsel.c -> src/ltk/ctrlsel.c | 0
Rsrc/ctrlsel.h -> src/ltk/ctrlsel.h | 0
Rsrc/entry.c -> src/ltk/entry.c | 0
Rsrc/entry.h -> src/ltk/entry.h | 0
Rsrc/event.h -> src/ltk/event.h | 0
Rsrc/event_xlib.c -> src/ltk/event_xlib.c | 0
Rsrc/eventdefs.h -> src/ltk/eventdefs.h | 0
Asrc/ltk/graphics.h | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltk/graphics_xlib.c | 600+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/graphics_xlib.h -> src/ltk/graphics_xlib.h | 0
Asrc/ltk/grid.c | 546+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/grid.h -> src/ltk/grid.h | 0
Rsrc/image.h -> src/ltk/image.h | 0
Rsrc/image_imlib.c -> src/ltk/image_imlib.c | 0
Rsrc/image_widget.c -> src/ltk/image_widget.c | 0
Rsrc/image_widget.h -> src/ltk/image_widget.h | 0
Rsrc/ini.c -> src/ltk/ini.c | 0
Rsrc/ini.h -> src/ltk/ini.h | 0
Rsrc/keys.h -> src/ltk/keys.h | 0
Rsrc/label.c -> src/ltk/label.c | 0
Rsrc/label.h -> src/ltk/label.h | 0
Asrc/ltk/ltk.c | 667+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltk/ltk.h | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/macros.h -> src/ltk/macros.h | 0
Rsrc/memory.c -> src/ltk/memory.c | 0
Rsrc/memory.h -> src/ltk/memory.h | 0
Rsrc/menu.c -> src/ltk/menu.c | 0
Rsrc/menu.h -> src/ltk/menu.h | 0
Rsrc/rect.c -> src/ltk/rect.c | 0
Rsrc/rect.h -> src/ltk/rect.h | 0
Rsrc/scrollbar.c -> src/ltk/scrollbar.c | 0
Rsrc/scrollbar.h -> src/ltk/scrollbar.h | 0
Rsrc/stb_truetype.c -> src/ltk/stb_truetype.c | 0
Rsrc/stb_truetype.h -> src/ltk/stb_truetype.h | 0
Rsrc/strtonum.c -> src/ltk/strtonum.c | 0
Rsrc/surface_cache.c -> src/ltk/surface_cache.c | 0
Rsrc/surface_cache.h -> src/ltk/surface_cache.h | 0
Rsrc/text.h -> src/ltk/text.h | 0
Rsrc/text_pango.c -> src/ltk/text_pango.c | 0
Rsrc/text_stb.c -> src/ltk/text_stb.c | 0
Rsrc/theme.c -> src/ltk/theme.c | 0
Rsrc/theme.h -> src/ltk/theme.h | 0
Rsrc/txtbuf.c -> src/ltk/txtbuf.c | 0
Rsrc/txtbuf.h -> src/ltk/txtbuf.h | 0
Asrc/ltk/util.c | 394+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltk/util.h | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltk/widget.c | 295+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltk/widget.h | 346+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltk/window.c | 1319+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/window.h -> src/ltk/window.h | 0
Asrc/ltkd/.gitignore | 4++++
Asrc/ltkd/box.c | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/button.c | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/cmd.c | 185+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/cmd.h | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/cmd_helpers.h | 31+++++++++++++++++++++++++++++++
Asrc/ltkd/entry.c | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/err.c | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/err.h | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/grid.c | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/image_widget.c | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/khash.h | 627+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/label.c | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/ltkc.c | 272+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/ltkc_img.c | 42++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/ltkd.c | 1097+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/ltkd.h | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/menu.c | 272+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/proto_types.h | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/socket_format.txt | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/util.c | 31+++++++++++++++++++++++++++++++
Asrc/ltkd/util.h | 22++++++++++++++++++++++
Asrc/ltkd/widget.c | 553+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ltkd/widget.h | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/util.c | 436-------------------------------------------------------------------------------
Dsrc/util.h | 62--------------------------------------------------------------
Dsrc/widget.c | 241-------------------------------------------------------------------------------
Dsrc/widget.h | 320-------------------------------------------------------------------------------
Dsrc/window.c | 1270-------------------------------------------------------------------------------
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 = &ltk_box_draw, - .destroy = &ltk_box_destroy, - .resize = &ltk_recalculate_box, - .child_size_change = &ltk_box_child_size_change, - .remove_child = &ltk_box_remove_child, - .key_press = NULL, - .key_release = NULL, - .mouse_press = NULL, - .mouse_scroll = &ltk_box_mouse_scroll, - .mouse_release = NULL, - .motion_notify = NULL, - .get_child_at_pos = &ltk_box_get_child_at_pos, - .mouse_leave = NULL, - .mouse_enter = NULL, - .prev_child = &ltk_box_prev_child, - .next_child = &ltk_box_next_child, - .first_child = &ltk_box_first_child, - .last_child = &ltk_box_last_child, - .nearest_child = &ltk_box_nearest_child, - .nearest_child_left = &ltk_box_nearest_child_left, - .nearest_child_right = &ltk_box_nearest_child_right, - .nearest_child_above = &ltk_box_nearest_child_above, - .nearest_child_below = &ltk_box_nearest_child_below, - .ensure_rect_shown = &ltk_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, - &ltk_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 = &ltk_grid_draw, - .destroy = &ltk_grid_destroy, - .resize = &ltk_recalculate_grid, - .hide = NULL, - .change_state = NULL, - .child_size_change = &ltk_grid_child_size_change, - .remove_child = &ltk_grid_remove_child, - .mouse_press = NULL, - .mouse_scroll = NULL, - .mouse_release = NULL, - .motion_notify = NULL, - .get_child_at_pos = &ltk_grid_get_child_at_pos, - .mouse_leave = NULL, - .mouse_enter = NULL, - .key_press = NULL, - .key_release = NULL, - .prev_child = &ltk_grid_prev_child, - .next_child = &ltk_grid_next_child, - .first_child = &ltk_grid_first_child, - .last_child = &ltk_grid_last_child, - .nearest_child = &ltk_grid_nearest_child, - .nearest_child_left = &ltk_grid_nearest_child_left, - .nearest_child_right = &ltk_grid_nearest_child_right, - .nearest_child_above = &ltk_grid_nearest_child_above, - .nearest_child_below = &ltk_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 = &ltk_button_ini_handler, - .fill_theme_defaults = &ltk_button_fill_theme_defaults, - .uninitialize_theme = &ltk_button_uninitialize_theme, - .register_keypress = NULL, - .register_keyrelease = NULL, - .cleanup = NULL, - }, - { - .name = "entry", - .ini_handler = &ltk_entry_ini_handler, - .fill_theme_defaults = &ltk_entry_fill_theme_defaults, - .uninitialize_theme = &ltk_entry_uninitialize_theme, - .register_keypress = &ltk_entry_register_keypress, - .register_keyrelease = &ltk_entry_register_keyrelease, - .cleanup = &ltk_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 = &ltk_label_ini_handler, - .fill_theme_defaults = &ltk_label_fill_theme_defaults, - .uninitialize_theme = &ltk_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 = &ltk_menu_ini_handler, - .fill_theme_defaults = &ltk_menu_fill_theme_defaults, - .uninitialize_theme = &ltk_menu_uninitialize_theme, - .register_keypress = NULL, - .register_keyrelease = NULL, - .cleanup = NULL, - }, - { - .name = "menuentry", - .ini_handler = &ltk_menuentry_ini_handler, - .fill_theme_defaults = &ltk_menuentry_fill_theme_defaults, - .uninitialize_theme = &ltk_menuentry_uninitialize_theme, - .register_keypress = NULL, - .register_keyrelease = NULL, - .cleanup = NULL, - }, - { - .name = "submenu", - .ini_handler = &ltk_submenu_ini_handler, - .fill_theme_defaults = &ltk_submenu_fill_theme_defaults, - .uninitialize_theme = &ltk_submenu_uninitialize_theme, - .register_keypress = NULL, - .register_keyrelease = NULL, - .cleanup = NULL, - }, - { - .name = "submenuentry", - .ini_handler = &ltk_submenuentry_ini_handler, - .fill_theme_defaults = &ltk_submenuentry_fill_theme_defaults, - .uninitialize_theme = &ltk_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 = &ltk_scrollbar_ini_handler, - .fill_theme_defaults = &ltk_scrollbar_fill_theme_defaults, - .uninitialize_theme = &ltk_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 = &ltk_window_ini_handler, - .fill_theme_defaults = &ltk_window_fill_theme_defaults, - .uninitialize_theme = &ltk_window_uninitialize_theme, - .register_keypress = &ltk_window_register_keypress, - .register_keyrelease = &ltk_window_register_keyrelease, - .cleanup = &ltk_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 = &ltk_box_draw, + .destroy = &ltk_box_destroy, + .resize = &ltk_recalculate_box, + .child_size_change = &ltk_box_child_size_change, + .remove_child = &ltk_box_remove_child, + .key_press = NULL, + .key_release = NULL, + .mouse_press = NULL, + .mouse_scroll = &ltk_box_mouse_scroll, + .mouse_release = NULL, + .motion_notify = NULL, + .get_child_at_pos = &ltk_box_get_child_at_pos, + .mouse_leave = NULL, + .mouse_enter = NULL, + .prev_child = &ltk_box_prev_child, + .next_child = &ltk_box_next_child, + .first_child = &ltk_box_first_child, + .last_child = &ltk_box_last_child, + .nearest_child = &ltk_box_nearest_child, + .nearest_child_left = &ltk_box_nearest_child_left, + .nearest_child_right = &ltk_box_nearest_child_right, + .nearest_child_above = &ltk_box_nearest_child_above, + .nearest_child_below = &ltk_box_nearest_child_below, + .ensure_rect_shown = &ltk_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, + &ltk_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 = &ltk_grid_draw, + .destroy = &ltk_grid_destroy, + .resize = &ltk_recalculate_grid, + .hide = NULL, + .change_state = NULL, + .child_size_change = &ltk_grid_child_size_change, + .remove_child = &ltk_grid_remove_child, + .mouse_press = NULL, + .mouse_scroll = NULL, + .mouse_release = NULL, + .motion_notify = NULL, + .get_child_at_pos = &ltk_grid_get_child_at_pos, + .mouse_leave = NULL, + .mouse_enter = NULL, + .key_press = NULL, + .key_release = NULL, + .prev_child = &ltk_grid_prev_child, + .next_child = &ltk_grid_next_child, + .first_child = &ltk_grid_first_child, + .last_child = &ltk_grid_last_child, + .nearest_child = &ltk_grid_nearest_child, + .nearest_child_left = &ltk_grid_nearest_child_left, + .nearest_child_right = &ltk_grid_nearest_child_right, + .nearest_child_above = &ltk_grid_nearest_child_above, + .nearest_child_below = &ltk_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 = &ltk_button_ini_handler, + .fill_theme_defaults = &ltk_button_fill_theme_defaults, + .uninitialize_theme = &ltk_button_uninitialize_theme, + .register_keypress = NULL, + .register_keyrelease = NULL, + .cleanup = NULL, + }, + { + .name = "entry", + .ini_handler = &ltk_entry_ini_handler, + .fill_theme_defaults = &ltk_entry_fill_theme_defaults, + .uninitialize_theme = &ltk_entry_uninitialize_theme, + .register_keypress = &ltk_entry_register_keypress, + .register_keyrelease = &ltk_entry_register_keyrelease, + .cleanup = &ltk_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 = &ltk_label_ini_handler, + .fill_theme_defaults = &ltk_label_fill_theme_defaults, + .uninitialize_theme = &ltk_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 = &ltk_menu_ini_handler, + .fill_theme_defaults = &ltk_menu_fill_theme_defaults, + .uninitialize_theme = &ltk_menu_uninitialize_theme, + .register_keypress = NULL, + .register_keyrelease = NULL, + .cleanup = NULL, + }, + { + .name = "menuentry", + .ini_handler = &ltk_menuentry_ini_handler, + .fill_theme_defaults = &ltk_menuentry_fill_theme_defaults, + .uninitialize_theme = &ltk_menuentry_uninitialize_theme, + .register_keypress = NULL, + .register_keyrelease = NULL, + .cleanup = NULL, + }, + { + .name = "submenu", + .ini_handler = &ltk_submenu_ini_handler, + .fill_theme_defaults = &ltk_submenu_fill_theme_defaults, + .uninitialize_theme = &ltk_submenu_uninitialize_theme, + .register_keypress = NULL, + .register_keyrelease = NULL, + .cleanup = NULL, + }, + { + .name = "submenuentry", + .ini_handler = &ltk_submenuentry_ini_handler, + .fill_theme_defaults = &ltk_submenuentry_fill_theme_defaults, + .uninitialize_theme = &ltk_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 = &ltk_scrollbar_ini_handler, + .fill_theme_defaults = &ltk_scrollbar_fill_theme_defaults, + .uninitialize_theme = &ltk_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 = &ltk_window_ini_handler, + .fill_theme_defaults = &ltk_window_fill_theme_defaults, + .uninitialize_theme = &ltk_window_uninitialize_theme, + .register_keypress = &ltk_window_register_keypress, + .register_keyrelease = &ltk_window_register_keyrelease, + .cleanup = &ltk_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 = &ltk_window_key_press_event, + .key_release = &ltk_window_key_release_event, + .mouse_press = &ltk_window_mouse_press_event, + .mouse_release = &ltk_window_mouse_release_event, + .release = NULL, + .motion_notify = &ltk_window_motion_notify_event, + .mouse_leave = NULL, + .mouse_enter = NULL, + .change_state = NULL, + .get_child_at_pos = NULL, + .resize = NULL, + .hide = NULL, + .draw = &ltk_window_redraw, + .destroy = &ltk_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 = &ltk_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 = &ltk_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", &ltkd_box_cmd_add, 0}, + {"create", &ltkd_box_cmd_create, 1}, + {"remove", &ltkd_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[] = { + {&ltkd_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", &ltkd_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", &ltkd_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", &ltkd_grid_cmd_add, 0}, + {"create", &ltkd_grid_cmd_create, 1}, + {"remove", &ltkd_grid_cmd_ungrid, 0}, + {"set-column-weight", &ltkd_grid_cmd_set_column_weight, 0}, + {"set-row-weight", &ltkd_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", &ltkd_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", &ltkd_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 = &ltkd_box_cmd + }, + { + .name = "button", + .cmd = &ltkd_button_cmd + }, + { + .name = "entry", + .cmd = &ltkd_entry_cmd + }, + { + .name = "grid", + .cmd = &ltkd_grid_cmd + }, + { + .name = "label", + .cmd = &ltkd_label_cmd + }, + { + .name = "image", + .cmd = &ltkd_image_widget_cmd + }, + { + .name = "menu", + .cmd = &ltkd_menu_cmd + }, + { + .name = "menuentry", + .cmd = &ltkd_menuentry_cmd + }, + { + .name = "submenu", + .cmd = &ltkd_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, &ltkd_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[] = { + {&ltkd_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", &ltkd_menu_cmd_add_entry, 0}, + {"create", &ltkd_menu_cmd_create, 1}, + {"insert-entry", &ltkd_menu_cmd_insert_entry, 0}, + {"remove-all-entries", &ltkd_menu_cmd_remove_all_entries, 0}, + {"remove-entry-index", &ltkd_menu_cmd_remove_entry_index, 0}, + {"remove-entry-id", &ltkd_menu_cmd_remove_entry_id, 0}, +}; + +static ltkd_cmd_info menuentry_cmds[] = { + {"attach-submenu", &ltkd_menuentry_cmd_attach_submenu, 0}, + {"create", &ltkd_menuentry_cmd_create, 1}, + {"detach-submenu", &ltkd_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[] = { + {&ltkd_widget_button_event, LTK_WIDGET_SIGNAL_MOUSE_PRESS}, + {&ltkd_widget_button_event, LTK_WIDGET_SIGNAL_MOUSE_RELEASE}, + {&ltkd_widget_motion_notify, LTK_WIDGET_SIGNAL_MOTION_NOTIFY}, + {&ltkd_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}, + {&ltkd_widget_resize, LTK_WIDGET_SIGNAL_RESIZE}, + {&ltkd_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, b