ltk

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

commit d02312cfb88d3c52be0c9c4e421af28b224fd7b5
parent 55009a9b2fc3477235ddf5cae6c45446e0117802
Author: lumidify <nobody@lumidify.org>
Date:   Thu, 24 Dec 2020 14:06:27 +0100

Use sockets instead of stdin and stdout

Diffstat:
M.gitignore | 4+++-
MMakefile | 11++++++++---
MREADME.md | 7++++---
ATODO | 15+++++++++++++++
Mbutton.c | 2+-
Dltk.c | 646-------------------------------------------------------------------------------
Mltk.h | 30++++++++++++++++++------------
Altkc.c | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Altkd.c | 912+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msocket_format.txt | 4++--
Atest.sh | 27+++++++++++++++++++++++++++
Dtest_anim.sh | 29-----------------------------
Dtest_draw.gui | 10----------
13 files changed, 1144 insertions(+), 707 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,3 +1,5 @@ -ltk +ltkd +ltkc +ltk.sock *.o *.core diff --git a/Makefile b/Makefile @@ -1,14 +1,19 @@ include config.mk -OBJ += color.o util.o ltk.o ini.o grid.o button.o draw.o +OBJ += color.o util.o ltkd.o ini.o grid.o button.o draw.o -ltk: $(OBJ) $(COMPATOBJ) +all: ltkd ltkc + +ltkd: $(OBJ) $(COMPATOBJ) $(CC) -o $@ $(OBJ) $(COMPATOBJ) $(LDFLAGS) +ltkc: ltkc.o + $(CC) -o $@ ltkc.o + %.o: %.c $(CC) -c -o $@ $< $(CFLAGS) .PHONY: clean clean: - rm -f $(OBJ) ltk *.core + rm -f $(OBJ) ltkc.o ltkd ltkc ltk.sock *.core diff --git a/README.md b/README.md @@ -11,9 +11,10 @@ because it's a bit of a hack. To test: make -./ltk < test.gui -./ltk < test_draw.gui -./test_anim.sh | ./ltk (this requires sleep(1) to support fractional seconds) +./test.sh + +If you click the top button, it should exit. That's all it does now. +Also read the comment in './test.sh'. Note: you need to uncomment "COMPATOBJ = strtonum.c" in config.mk if you're not using OpenBSD. diff --git a/TODO b/TODO @@ -0,0 +1,15 @@ +Convert points to pixels for stb rendering (currently, the size between +pango and stb is completely different). + +Random stuff: +* This is not really a general-purpose GUI toolkit - imagine building + a complex GUI with this. Especially things like having to respond + directly to mouse movement wouldn't be very efficient due to all the + protocol overhead (or would it maybe not be as bad as I think?). + One idea would be to have a C API and the socket API. The C program + works more or less like a regular GUI program but the GUI mainloop + still opens the socket as usual and allows commands to be sent that + way. That way, you would get the advantages of both sides - the C + program can do the normal GUI stuff, but everything can still be + edited on-the-fly, and text can be retrieved (e.g. for a screen- + reader). diff --git a/button.c b/button.c @@ -154,7 +154,7 @@ ltk_button_change_state(ltk_button *button) { static void ltk_button_mouse_release(ltk_button *button, XEvent event) { - ltk_queue_event(button->widget.window, button->widget.id, "button_click"); + ltk_queue_event(button->widget.window, LTK_EVENT_BUTTON, button->widget.id, "button_click"); } static ltk_button * diff --git a/ltk.c b/ltk.c @@ -1,646 +0,0 @@ -/* - * This file is part of the Lumidify ToolKit (LTK) - * Copyright (c) 2016, 2017, 2018, 2020 lumidify <nobody@lumidify.org> - * - * 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. - */ - -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <stdint.h> -#include <fcntl.h> -#include <unistd.h> -#include <time.h> -#include <sys/stat.h> -#include <sys/select.h> -#include <X11/Xlib.h> -#include <X11/Xutil.h> -#include "util.h" -#include "khash.h" -#include "ini.h" -#include "text.h" -#include "ltk.h" -#include "grid.h" -#include "button.h" -#include "draw.h" - -static void ltk_load_theme(ltk_window *window, const char *path); -static void ltk_destroy_theme(ltk_theme *theme); -static ltk_rect ltk_rect_union(ltk_rect r1, ltk_rect r2); - -static int running = 1; -static char *cmd_input = NULL; -static char **tokens = NULL; -static size_t cmd_bufsize = 0; -static size_t tokens_bufsize = 0; -static size_t cmd_len = 0; -static size_t tokens_len = 0; - -static ltk_rect -ltk_rect_union(ltk_rect r1, ltk_rect r2) { - ltk_rect u; - u.x = r1.x < r2.x ? r1.x : r2.x; - u.y = r1.y < r2.y ? r1.y : r2.y; - int x2 = r1.x + r1.w < r2.x + r2.w ? r2.x + r2.w : r1.x + r1.w; - int y2 = r1.y + r1.h < r2.y + r2.h ? r2.y + r2.h : r1.y + r1.h; - u.w = x2 - u.x; - u.h = y2 - u.y; - return u; -} - -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_clean_up(ltk_window *window) { - ltk_destroy_theme(window->theme); - ltk_destroy_window(window); - if (tokens) free(tokens); - if (cmd_input) free(cmd_input); -} - -void -ltk_quit(ltk_window *window) { - ltk_clean_up(window); - running = 0; -} - -void -ltk_fatal(const char *msg) { - (void)fprintf(stderr, msg); - /* FIXME: clean up first */ - exit(1); -}; - -int -ltk_create_xcolor(ltk_window *window, const char *hex, XColor *col) { - if (!XParseColor(window->dpy, window->cm, hex, col)) { - (void)fprintf(stderr, "Invalid color: %s\n", hex); - return 0; - } - XAllocColor(window->dpy, window->cm, col); - - return 1; -} - -void -ltk_queue_event(ltk_window *window, const char *id, const char *name) { - struct ltk_event_queue *new = malloc(sizeof(struct ltk_event_queue)); - if (!new) ltk_fatal("Unable to queue event.\n"); - new->id = strdup(id); - new->name = strdup(name); - new->next = window->first_event; - window->first_event = new; - new->prev = NULL; - if (!window->last_event) - window->last_event = new; -} - -static void -ltk_set_root_widget_cmd( - ltk_window *window, - char **tokens, - int num_tokens) { - ltk_widget *widget; - if (num_tokens != 2) { - (void)fprintf(stderr, "set-root-widget: Invalid number of arguments.\n"); - return; - } - widget = ltk_get_widget(window, tokens[1], LTK_WIDGET, "set-root-widget"); - if (!widget) return; - window->root_widget = widget; - int w = widget->rect.w; - int h = widget->rect.h; - widget->rect.w = window->rect.w; - widget->rect.h = window->rect.h; - if (widget->resize) { - widget->resize(widget, w, h); - } -} - -/* copied from suckless ii */ -static int -read_cmdline(int fd) { - char c = '\0'; - char *tmp = NULL; - cmd_len = 0; - do { - if (cmd_len >= cmd_bufsize) { - cmd_bufsize = !cmd_bufsize ? 1 : cmd_bufsize; - tmp = realloc(cmd_input, cmd_bufsize * 2 * sizeof(char)); - if (!tmp) ltk_fatal("Out of memory while reading command.\n"); - cmd_input = tmp; - cmd_bufsize *= 2; - } - if (read(fd, &c, sizeof(char)) != sizeof(char)) - return -1; - cmd_input[cmd_len++] = c; - } while (c != '\n'); - cmd_input[cmd_len - 1] = '\0'; /* eliminates '\n' */ - return 0; -} - -/* FIXME: turn this into something that doesn't, you know, - explode at the slightest touch... */ -static int -tokenize(void) { - char *c; - tokens_len = 0; - if (!cmd_input || cmd_input[0] == '\0') return 0; - for (c = cmd_input; *c == ' '; c++) - ; - size_t cur_tok = 0; - int in_str = 0; - char **tmp; - if (tokens_bufsize) - tokens[0] = c; - while (*c != '\0') { - if (cur_tok >= tokens_bufsize) { - if (!tokens_bufsize) - tokens_bufsize = 1; - tmp = realloc(tokens, tokens_bufsize * 2 * sizeof(char *)); - if (!tmp) ltk_fatal("Out of memory while parsing command.\n"); - tokens = tmp; - tokens[cur_tok] = c; - tokens_bufsize *= 2; - } - if (*c == '"') { - if (!in_str) - tokens[cur_tok] = c + 1; - in_str = !in_str; - *c = '\0'; - } else if (*c == ' ' && !in_str) { - *c = '\0'; - cur_tok++; - tokens[cur_tok] = c + 1; - } - c++; - } - tokens_len = cur_tok + 1; -} - -static void -proc_cmds(ltk_window *window) { - if (!tokenize()) return; - if (!tokens_len) return; - if (strcmp(tokens[0], "grid") == 0) { - ltk_grid_cmd(window, tokens, tokens_len); - } else if (strcmp(tokens[0], "button") == 0) { - ltk_button_cmd(window, tokens, tokens_len); - } else if (strcmp(tokens[0], "set-root-widget") == 0) { - ltk_set_root_widget_cmd(window, tokens, tokens_len); - } else if (strcmp(tokens[0], "draw") == 0) { - ltk_draw_cmd(window, tokens, tokens_len); - } else if (strcmp(tokens[0], "quit") == 0) { - ltk_quit(window); - } else { - (void)fprintf(stderr, "Invalid command.\n"); - } -} - -int -ltk_mainloop(ltk_window *window) { - XEvent event; - struct stat st; - struct timespec tick; - struct timeval tv; - fd_set rfds; - int fd_in = fileno(stdin); - int retval; - tick.tv_sec = 0; - tick.tv_nsec = 10000; - tv.tv_sec = 0; - tv.tv_usec = 0; - - tokens = malloc(10 * sizeof(char *)); - cmd_input = malloc(200 * sizeof(char)); - if (!tokens || !cmd_input) ltk_fatal("Out of memory.\n"); - tokens_bufsize = 10; - cmd_bufsize = 200; - - while (running) { - FD_ZERO(&rfds); - FD_SET(fd_in, &rfds); - retval = select(fd_in + 1, &rfds, NULL, NULL, &tv); - while (XPending(window->dpy)) { - XNextEvent(window->dpy, &event); - ltk_handle_event(window, event); - } - if (retval > 0 && !read_cmdline(fd_in)) { - proc_cmds(window); - } else if (window->dirty_rect.w != 0 && window->dirty_rect.h != 0) { - ltk_redraw_window(window); - window->dirty_rect.w = 0; - window->dirty_rect.h = 0; - } - if (window->last_event && running) { - struct ltk_event_queue *cur = window->last_event; - struct ltk_event_queue *last; - do { - printf("%s %s\n", cur->id, cur->name); - free(cur->id); - free(cur->name); - last = cur; - cur = cur->prev; - free(last); - } while (cur); - window->first_event = window->last_event = NULL; - } - /* yes, this should be improved */ - nanosleep(&tick, NULL); - - } -} - -void -ltk_redraw_window(ltk_window *window) { - ltk_widget *ptr; - if (!window) return; - if (window->dirty_rect.x >= window->rect.w) return; - if (window->dirty_rect.y >= window->rect.h) return; - if (window->dirty_rect.x + window->dirty_rect.w > window->rect.w) - window->dirty_rect.w -= window->dirty_rect.x + window->dirty_rect.w - window->rect.w; - if (window->dirty_rect.y + window->dirty_rect.h > window->rect.h) - window->dirty_rect.h -= window->dirty_rect.y + window->dirty_rect.h - window->rect.h; - XClearArea(window->dpy, window->xwindow, window->dirty_rect.x, window->dirty_rect.y, window->dirty_rect.w, window->dirty_rect.h, False); - if (!window->root_widget) return; - /* FIXME: actually respect the dirty rect... */ - ptr = window->root_widget; - ptr->draw(ptr); -} - -ltk_window * -ltk_create_window(const char *theme_path, const char *title, int x, int y, unsigned int w, unsigned int h) { - ltk_window *window = malloc(sizeof(ltk_window)); - if (!window) - ltk_fatal("Not enough memory left for window!\n"); - - window->dpy = XOpenDisplay(NULL); - window->screen = DefaultScreen(window->dpy); - window->cm = DefaultColormap(window->dpy, window->screen); - /* FIXME: validate theme properly */ - ltk_load_theme(window, theme_path); - window->wm_delete_msg = XInternAtom(window->dpy, "WM_DELETE_WINDOW", False); - - ltk_window_theme *wtheme = window->theme->window; - window->xwindow = - XCreateSimpleWindow(window->dpy, DefaultRootWindow(window->dpy), x, y, - w, h, wtheme->border_width, - wtheme->fg.pixel, wtheme->bg.pixel); - window->gc = XCreateGC(window->dpy, window->xwindow, 0, 0); - XSetForeground(window->dpy, window->gc, wtheme->fg.pixel); - XSetBackground(window->dpy, window->gc, wtheme->bg.pixel); - XSetStandardProperties(window->dpy, window->xwindow, title, NULL, None, - NULL, 0, NULL); - XSetWMProtocols(window->dpy, window->xwindow, &window->wm_delete_msg, 1); - window->root_widget = NULL; - - ltk_init_text(wtheme->font, window->dpy, window->screen, window->cm); - - window->other_event = &ltk_window_other_event; - - window->rect.w = 0; - window->rect.h = 0; - 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->widget_hash = kh_init(widget); - - XClearWindow(window->dpy, window->xwindow); - XMapRaised(window->dpy, window->xwindow); - XSelectInput(window->dpy, window->xwindow, - ExposureMask | KeyPressMask | KeyReleaseMask | - ButtonPressMask | ButtonReleaseMask | - StructureNotifyMask | PointerMotionMask); - - return window; -} - -void -ltk_destroy_window(ltk_window *window) { - khint_t k; - ltk_widget *ptr; - XDestroyWindow(window->dpy, window->xwindow); - for (k = kh_begin(window->widget_hash); k != kh_end(window->widget_hash); k++) { - if (kh_exist(window->widget_hash, k)) { - ptr = kh_value(window->widget_hash, k); - ptr->destroy(ptr, 1); - } - } - kh_destroy(widget, window->widget_hash); - ltk_cleanup_text(); - XCloseDisplay(window->dpy); - free(window); -} - -void -ltk_window_other_event(ltk_window *window, XEvent event) { - ltk_widget *ptr = window->root_widget; - int retval = 0; - if (event.type == ConfigureNotify) { - unsigned int w, h; - w = event.xconfigure.width; - h = event.xconfigure.height; - 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; - if (ptr && ptr->resize) { - ptr->rect.w = w; - ptr->rect.h = h; - ptr->resize(ptr, orig_w, orig_h); - } - } - } else if (event.type == Expose && event.xexpose.count == 0) { - ltk_rect r; - r.x = event.xexpose.x; - r.y = event.xexpose.y; - r.w = event.xexpose.width; - r.h = event.xexpose.height; - ltk_window_invalidate_rect(window, r); - } else if (event.type == ClientMessage - && event.xclient.data.l[0] == window->wm_delete_msg) { - ltk_destroy_window(window); - exit(0); /* FIXME */ - } -} - -void -ltk_window_ini_handler(ltk_window *window, const char *prop, const char *value) { - /* FIXME: A whole lot more error checking! */ - if (strcmp(prop, "border_width") == 0) { - window->theme->window->border_width = atoi(value); - } else if (strcmp(prop, "bg") == 0) { - ltk_create_xcolor(window, value, &window->theme->window->bg); - } else if (strcmp(prop, "fg") == 0) { - ltk_create_xcolor(window, value, &window->theme->window->fg); - } else if (strcmp(prop, "font") == 0) { - window->theme->window->font = strdup(value); - } else if (strcmp(prop, "font_size") == 0) { - window->theme->window->font_size = atoi(value); - } -} - -int -ltk_ini_handler(ltk_window *window, const char *widget, const char *prop, const char *value) { - if (strcmp(widget, "window") == 0) { - ltk_window_ini_handler(window, prop, value); - } else if (strcmp(widget, "button") == 0) { - ltk_button_ini_handler(window, prop, value); - } -} - -static void -ltk_load_theme(ltk_window *window, const char *path) { - window->theme = malloc(sizeof(ltk_theme)); - if (!window->theme) ltk_fatal("Unable to allocate memory for theme.\n"); - window->theme->window = malloc(sizeof(ltk_window_theme)); - if (!window->theme->window) ltk_fatal("Unable to allocate memory for window theme.\n"); - window->theme->button = NULL; - if (ini_parse(path, ltk_ini_handler, window) < 0) { - (void)fprintf(stderr, "ERROR: Can't load theme %s\n.", path); - exit(1); - } -} - -static void -ltk_destroy_theme(ltk_theme *theme) { - free(theme->button); - free(theme->window); - free(theme); -} - -int -ltk_collide_rect(ltk_rect rect, int x, int y) { - return (rect.x <= x && (rect.x + rect.w) >= x && rect.y <= y - && (rect.y + rect.h) >= y); -} - -void -ltk_window_remove_active_widget(ltk_window *window) { - ltk_widget *widget = window->active_widget; - if (!widget) return; - while (widget) { - widget->state = LTK_NORMAL; - widget->active_widget = NULL; - if (widget->change_state) - widget->change_state(widget); - if (widget->needs_redraw) - ltk_window_invalidate_rect(window, widget->rect); - widget = widget->parent; - } - window->active_widget = NULL; -} - -void -ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget) { - window->active_widget = widget; - ltk_widget *parent = widget->parent; - widget->state = LTK_ACTIVE; - while (parent) { - widget->state = LTK_ACTIVE; - parent->active_widget = widget; - widget = parent; - parent = widget->parent; - } -} - -void -ltk_fill_widget_defaults(ltk_widget *widget, const char *id, ltk_window *window, - void (*draw) (void *), void (*change_state) (void *), - void (*destroy) (void *, int), unsigned int needs_redraw, - ltk_widget_type type) { - widget->id = strdup(id); - widget->window = window; - widget->active_widget = NULL; - widget->parent = NULL; - widget->type = type; - - widget->key_press = NULL; - widget->key_release = NULL; - widget->mouse_press = NULL; - widget->mouse_release = NULL; - widget->motion_notify = NULL; - widget->mouse_enter = NULL; - widget->mouse_leave = NULL; - - widget->resize = NULL; - widget->draw = draw; - widget->change_state = change_state; - widget->destroy = destroy; - - widget->needs_redraw = needs_redraw; - widget->state = LTK_NORMAL; - widget->row = 0; - widget->rect.x = 0; - widget->rect.y = 0; - widget->rect.w = 100; - widget->rect.h = 100; - - widget->row = NULL; - widget->column = NULL; - widget->row_span = NULL; - widget->column_span = NULL; - widget->sticky = 0; -} - -void -ltk_widget_mouse_press_event(ltk_widget *widget, XEvent event) { - if (!widget || widget->state == LTK_DISABLED) - return; - if (event.xbutton.button == 1) { - ltk_widget *parent = widget->parent; - widget->state = LTK_PRESSED; - if (widget->change_state) - widget->change_state(widget); - if (widget->needs_redraw) - ltk_window_invalidate_rect(widget->window, widget->rect); - } - if (widget->mouse_press) { - widget->mouse_press(widget, event); - } -} - -void -ltk_widget_mouse_release_event(ltk_widget *widget, XEvent event) { - if (!widget || widget->state == LTK_DISABLED) - return; - if (widget->state == LTK_PRESSED) { - widget->state = LTK_ACTIVE; - if (widget->change_state) - widget->change_state(widget); - if (widget->needs_redraw) - ltk_window_invalidate_rect(widget->window, widget->rect); - } - if (widget->mouse_release) { - widget->mouse_release(widget, event); - } -} - -void -ltk_widget_motion_notify_event(ltk_widget *widget, XEvent event) { - if (!widget) return; - short pressed = (event.xmotion.state & Button1Mask) == Button1Mask; - if ((widget->state == LTK_NORMAL) && !pressed) { - widget->state = LTK_ACTIVE; - if (widget->change_state) - widget->change_state(widget); - if (widget->mouse_enter) - widget->mouse_enter(widget, event); - /* FIXME: do this properly */ - if (widget->window->active_widget != widget && !widget->motion_notify) { - ltk_window_remove_active_widget(widget->window); - ltk_window_set_active_widget(widget->window, widget); - } - if (widget->needs_redraw) - ltk_window_invalidate_rect(widget->window, widget->rect); - } - if (widget->motion_notify) - widget->motion_notify(widget, event); -} - -void -ltk_handle_event(ltk_window *window, XEvent event) { - ltk_widget *root_widget = window->root_widget; - switch (event.type) { - case KeyPress: - break; - case KeyRelease: - break; - case ButtonPress: - if (root_widget) - ltk_widget_mouse_press_event(root_widget, event); - break; - case ButtonRelease: - if (root_widget) - ltk_widget_mouse_release_event(root_widget, event); - break; - case MotionNotify: - if (root_widget) - ltk_widget_motion_notify_event(root_widget, event); - break; - default: - if (window->other_event) - window->other_event(window, event); - } -} - -int -ltk_check_widget_id_free(ltk_window *window, const char *id, const char *caller) { - khint_t k; - k = kh_get(widget, window->widget_hash, id); - if (k != kh_end(window->widget_hash)) { - (void)fprintf(stderr, "%s: Widget \"%s\" already exists.\n", caller, id); - return 0; - } - return 1; -} - -ltk_widget * -ltk_get_widget(ltk_window *window, const char *id, ltk_widget_type type, - const char *caller) { - khint_t k; - ltk_widget *widget; - k = kh_get(widget, window->widget_hash, id); - if (k == kh_end(window->widget_hash)) { - (void)fprintf(stderr, "%s: Widget \"%s\" doesn't exist.\n", caller, id); - return NULL; - } - widget = kh_value(window->widget_hash, k); - if (type != LTK_WIDGET && widget->type != type) { - (void)fprintf(stderr, "%s: Widget \"%s\" has wrong type.\n", caller, id); - return NULL; - } - return widget; -} - -void -ltk_set_widget(ltk_window *window, ltk_widget *widget, const char *id) { - int ret; - khint_t k; - /* apparently, khash requires the string to stay accessible */ - char *tmp = strdup(id); - k = kh_put(widget, window->widget_hash, tmp, &ret); - kh_value(window->widget_hash, k) = widget; -} - -void -ltk_remove_widget(ltk_window *window, const char *id) { - khint_t k; - k = kh_get(widget, window->widget_hash, id); - if (k != kh_end(window->widget_hash)) { - kh_del(widget, window->widget_hash, k); - } -} - -int main(int argc, char *argv[]) { - ltk_window *window = ltk_create_window("theme.ini", "Demo", 0, 0, 500, 500); - ltk_mainloop(window); -} diff --git a/ltk.h b/ltk.h @@ -28,6 +28,12 @@ #include "khash.h" +typedef enum { + LTK_EVENT_RESIZE = 1 << 0, + LTK_EVENT_BUTTON = 1 << 1, + LTK_EVENT_KEY = 1 << 2 +} ltk_event_type; + typedef struct { int x; int y; @@ -59,10 +65,10 @@ typedef enum { typedef struct ltk_window ltk_window; typedef struct ltk_widget { + ltk_rect rect; ltk_window *window; struct ltk_widget *active_widget; struct ltk_widget *parent; - ltk_widget_type type; char *id; void (*key_press) (void *, XEvent event); @@ -78,14 +84,14 @@ typedef struct ltk_widget { void (*change_state) (void *); void (*destroy) (void *, int); - ltk_rect rect; - unsigned int row; - unsigned int column; - unsigned int row_span; - unsigned int column_span; - unsigned int needs_redraw; + ltk_widget_type type; ltk_widget_state state; - unsigned short sticky; + unsigned int sticky; + unsigned short row; + unsigned short column; + unsigned short row_span; + unsigned short column_span; + unsigned char needs_redraw; } ltk_widget; typedef struct { @@ -104,8 +110,8 @@ typedef struct { } ltk_theme; struct ltk_event_queue { - char *id; - char *name; + ltk_event_type event_type; + char *data; struct ltk_event_queue *prev; struct ltk_event_queue *next; }; @@ -121,7 +127,7 @@ typedef struct ltk_window { Window xwindow; ltk_widget *root_widget; ltk_widget *active_widget; - int (*other_event) (ltk_window *, XEvent event); + void (*other_event) (ltk_window *, XEvent event); ltk_rect rect; ltk_theme *theme; ltk_rect dirty_rect; @@ -136,7 +142,7 @@ void ltk_fatal(const char *msg); int ltk_create_xcolor(ltk_window *window, const char *hex, XColor *col); -void ltk_queue_event(ltk_window *window, const char *id, const char *name); +void ltk_queue_event(ltk_window *window, ltk_event_type type, const char *id, const char *data); int ltk_mainloop(ltk_window *window); diff --git a/ltkc.c b/ltkc.c @@ -0,0 +1,154 @@ +/* + * This file is part of the Lumidify ToolKit (LTK) + * Copyright (c) 2020 lumidify <nobody@lumidify.org> + * + * 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. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stdint.h> +#include <stddef.h> +#include <unistd.h> +#include <time.h> +#include <errno.h> +#include <sys/select.h> +#include <sys/socket.h> +#include <sys/un.h> + +#define BLK_SIZE 128 + +/* If `needed` is larger than `*alloc_size`, resize `*str` to `*alloc_size * 2`. */ +static int +grow_string(char **str, int *alloc_size, int needed) { + if (needed <= *alloc_size) return 0; + char *new = realloc(*str, *alloc_size * 2); + if (!new) return 1; + *str = new; + *alloc_size = *alloc_size * 2; + return 0; +} +static struct { + char *in_buffer; + int in_len; + int in_alloc; + char *out_buffer; + int out_len; + int out_alloc; +} io_buffers; + +int main(int argc, char *argv[]) { + int sockfd, maxrfd, maxwfd; + 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 = 15; + + if ((sockfd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { + perror("Socket error"); + return -1; + } + memset(&un, 0, sizeof(un)); + un.sun_family = AF_UNIX; + strcpy(un.sun_path, "ltk.sock"); + if (connect(sockfd, (struct sockaddr *)&un, offsetof(struct sockaddr_un, sun_path) + 9) < 0) { + perror("Socket error"); + return -2; + } + + io_buffers.in_buffer = malloc(BLK_SIZE); + if (!io_buffers.in_buffer) + return 1; + io_buffers.in_alloc = BLK_SIZE; + + io_buffers.out_buffer = malloc(BLK_SIZE); + if (!io_buffers.out_buffer) + return 1; + 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); + maxrfd = sockfd > infd ? sockfd : infd; + maxwfd = sockfd > outfd ? sockfd : outfd; + + while (1) { + if (!FD_ISSET(infd, &rallfds) && !FD_ISSET(sockfd, &rallfds) && io_buffers.in_len == 0 && io_buffers.out_len == 0) + break; + rfds = rallfds; + wfds = wallfds; + /* Separate this because the writing fds are *usually* always ready, + leading to the loop looping way too fast */ + select(maxrfd + 1, &rfds, NULL, NULL, &tv); + select(maxwfd + 1, NULL, &wfds, NULL, &tv); + if (FD_ISSET(sockfd, &rfds)) { + 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) { + return 2; + } else if (nread == 0) { + FD_CLR(sockfd, &rallfds); + FD_CLR(sockfd, &wallfds); + } else { + io_buffers.out_len += nread; + } + } + if (FD_ISSET(infd, &rfds)) { + grow_string(&io_buffers.in_buffer, &io_buffers.in_alloc, io_buffers.in_len + BLK_SIZE); + int nread = read(infd, io_buffers.in_buffer + io_buffers.in_len, BLK_SIZE); + if (nread < 0) { + return 2; + } else if (nread == 0) { + FD_CLR(infd, &rallfds); + } else { + io_buffers.in_len += nread; + } + } + if (FD_ISSET(sockfd, &wfds)) { + 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) { + return 2; + } 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)) { + 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) { + return 2; + } else { + memmove(io_buffers.out_buffer, io_buffers.out_buffer + nwritten, io_buffers.out_len - nwritten); + io_buffers.out_len -= nwritten; + } + } + } + + return 0; +} diff --git a/ltkd.c b/ltkd.c @@ -0,0 +1,912 @@ +/* + * This file is part of the Lumidify ToolKit (LTK) + * Copyright (c) 2016, 2017, 2018, 2020 lumidify <nobody@lumidify.org> + * + * 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. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stdint.h> +#include <fcntl.h> +#include <unistd.h> +#include <time.h> +#include <errno.h> +#include <sys/stat.h> +#include <sys/select.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include "util.h" +#include "khash.h" +#include "ini.h" +#include "text.h" +#include "ltk.h" +#include "grid.h" +#include "button.h" +#include "draw.h" + +#define MAX_SOCK_CONNS 20 +#define READ_BLK_SIZE 128 +#define WRITE_BLK_SIZE 128 + +struct token_list { + char **tokens; + int num_tokens; + int num_alloc; +}; + +static struct ltk_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 */ +} sockets[MAX_SOCK_CONNS]; + +static void ltk_load_theme(ltk_window *window, const char *path); +static void ltk_destroy_theme(ltk_theme *theme); +static ltk_rect ltk_rect_union(ltk_rect r1, ltk_rect r2); +static int read_sock(struct ltk_sock_info *sock); + +static int running = 1; + +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); + char **new = realloc(tl->tokens, new_size * sizeof(char *)); + if (!new) return -1; + tl->tokens = new; + tl->num_alloc = new_size; + } + tl->tokens[tl->num_tokens++] = token; + + return 0; +} + +/* If `needed` is larger than `*alloc_size`, resize `*str` to `*alloc_size * 2`. */ +static int +grow_string(char **str, int *alloc_size, int needed) { + if (needed <= *alloc_size) return 0; + int new_size = needed > (*alloc_size * 2) ? needed : (*alloc_size * 2); + char *new = realloc(*str, new_size); + if (!new) return 1; + *str = new; + *alloc_size = new_size; + return 0; +} +/* FIXME: non-blocking io? */ +/* Read up to READ_BLK_SIZE bytes from the socket. + Returns -1 if an error occurred, 0 if the connection was closed, 1 otherwise. */ +static int +read_sock(struct ltk_sock_info *sock) { + int nread; + char *old = sock->read; + int ret = grow_string(&sock->read, &sock->read_alloc, sock->read_len + READ_BLK_SIZE); + if (ret) return -1; /* fixme: errno? */ + /* 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] = sock->read + (sock->tokens.tokens[i] - 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 ltk_sock_info *sock) { + if (!sock->write_len) + 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; + return 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. */ +static int +queue_sock_write(struct ltk_sock_info *sock, const char *str, int len) { + 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; + } + + if (len < 0) + len = strlen(str); + + if (sock->write_alloc - sock->write_len < len && + grow_string(&sock->to_write, &sock->write_alloc, sock->write_len + len)) + return -1; + + (void)strncpy(sock->to_write + sock->write_len, str, len); + sock->write_len += len; + + return 0; +} + +/* Returns 0 if the end of a command was encountered, 1 otherwise */ +static int +tokenize_command(struct ltk_sock_info *sock) { + for (; sock->read_cur < sock->read_len; sock->read_cur++) { + 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++; + 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; + 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; + } else { + sock->read[sock->read_cur-sock->offset] = sock->read[sock->read_cur]; + sock->bs = 0; + } + } + + return 1; +} + +void +ltk_clean_up(ltk_window *window) { + ltk_destroy_theme(window->theme); + ltk_destroy_window(window); +} + +void +ltk_quit(ltk_window *window) { + ltk_clean_up(window); + running = 0; +} + +static void +ltk_set_root_widget_cmd( + ltk_window *window, + char **tokens, + int num_tokens) { + ltk_widget *widget; + if (num_tokens != 2) { + (void)fprintf(stderr, "set-root-widget: Invalid number of arguments.\n"); + return; + } + widget = ltk_get_widget(window, tokens[1], LTK_WIDGET, "set-root-widget"); + if (!widget) return; + window->root_widget = widget; + int w = widget->rect.w; + int h = widget->rect.h; + widget->rect.w = window->rect.w; + widget->rect.h = window->rect.h; + if (widget->resize) { + widget->resize(widget, w, h); + } +} + +static void +process_commands(ltk_window *window, struct ltk_sock_info *sock) { + char **tokens; + int num_tokens; + while (!tokenize_command(sock)) { + tokens = sock->tokens.tokens; + num_tokens = sock->tokens.num_tokens; + if (num_tokens < 1) + continue; + if (strcmp(tokens[0], "grid") == 0) { + ltk_grid_cmd(window, tokens, num_tokens); + } else if (strcmp(tokens[0], "button") == 0) { + ltk_button_cmd(window, tokens, num_tokens); + } else if (strcmp(tokens[0], "set-root-widget") == 0) { + ltk_set_root_widget_cmd(window, tokens, num_tokens); + } else if (strcmp(tokens[0], "draw") == 0) { + ltk_draw_cmd(window, tokens, num_tokens); + } else if (strcmp(tokens[0], "quit") == 0) { + ltk_quit(window); + } else { + /* FIXME... */ + (void)fprintf(stderr, "Invalid command.\n"); + } + sock->tokens.num_tokens = 0; + } + if (sock->tokens.num_tokens > 0 && sock->tokens.tokens[0] != sock->read) { + memmove(sock->read, sock->tokens.tokens[0], sock->read + sock->read_len - sock->tokens.tokens[0]); + ptrdiff_t offset = sock->tokens.tokens[0] - sock->read; + /* Hmm, seems a bit ugly... */ + for (int i = 0; i < sock->tokens.num_tokens; i++) { + sock->tokens.tokens[i] -= offset; + } + sock->read_len -= offset; + sock->read_cur -= offset; + } else if (sock->tokens.num_tokens == 0) { + sock->read_len = 0; + sock->read_cur = 0; + } +} + +static ltk_rect +ltk_rect_union(ltk_rect r1, ltk_rect r2) { + ltk_rect u; + u.x = r1.x < r2.x ? r1.x : r2.x; + u.y = r1.y < r2.y ? r1.y : r2.y; + int x2 = r1.x + r1.w < r2.x + r2.w ? r2.x + r2.w : r1.x + r1.w; + int y2 = r1.y + r1.h < r2.y + r2.h ? r2.y + r2.h : r1.y + r1.h; + u.w = x2 - u.x; + u.h = y2 - u.y; + return u; +} + +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_fatal(const char *msg) { + (void)fprintf(stderr, msg); + /* FIXME: clean up first */ + exit(1); +}; + +int +ltk_create_xcolor(ltk_window *window, const char *hex, XColor *col) { + if (!XParseColor(window->dpy, window->cm, hex, col)) { + (void)fprintf(stderr, "Invalid color: %s\n", hex); + return 0; + } + XAllocColor(window->dpy, window->cm, col); + + return 1; +} + +void +ltk_queue_event(ltk_window *window, ltk_event_type type, const char *id, const char *data) { + /* FIXME: make it nicer and safer */ + struct ltk_event_queue *new = malloc(sizeof(struct ltk_event_queue)); + if (!new) ltk_fatal("Unable to queue event.\n"); + new->event_type = type; + int id_len = strlen(id); + int data_len = strlen(data); + new->data = malloc(id_len + data_len + 3); + if (!new->data) ltk_fatal("Unable to queue event.\n"); + strcpy(new->data, id); + new->data[id_len] = ' '; + strcpy(new->data + id_len + 1, data); + new->data[id_len + data_len + 1] = '\n'; + new->data[id_len + data_len + 2] = '\0'; + new->next = window->first_event; + window->first_event = new; + new->prev = NULL; + if (!window->last_event) + window->last_event = new; +} + +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; + return i; + } + } + + return -1; +} + +/* largely copied from APUE */ +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; +} + +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; + } + + return clifd; +} + +int +ltk_mainloop(ltk_window *window) { + XEvent event; + fd_set rfds, wfds, rallfds, wallfds; + int maxi, maxfd, listenfd; + int rretval, wretval; + int clifd; + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 10; + + /* 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; + } + + FD_ZERO(&rallfds); + FD_ZERO(&wallfds); + + if ((listenfd = listen_sock("ltk.sock")) < 0) { + fprintf(stderr, "Error listening on ltk.sock\n"); + exit(1); /* FIXME: proper error handling */ + } + + FD_SET(listenfd, &rallfds); + maxfd = listenfd; + maxi = -1; + + while (running) { + rfds = rallfds; + wfds = wallfds; + /* separate these because the writing fds are usually + always ready for writing */ + rretval = select(maxfd + 1, &rfds, NULL, NULL, &tv); + wretval = select(maxfd + 1, NULL, &wfds, NULL, &tv); + while (XPending(window->dpy)) { + XNextEvent(window->dpy, &event); + ltk_handle_event(window, event); + } + /* FIXME: somehow keep track of whether anything has to be written, + otherwise it always has to loop over all fds to check - the writing + fds are usually always set, so this is really run on every loop + iteration, which is bad */ + if (rretval > 0 || wretval > 0) { + if (FD_ISSET(listenfd, &rfds)) { + if ((clifd = accept_sock(listenfd)) < 0) { + fprintf(stderr, "Error accepting socket connection\n"); + exit(1); /* FIXME: proper error handling */ + } + int i = add_client(clifd); + FD_SET(clifd, &rallfds); + FD_SET(clifd, &wallfds); + if (clifd > maxfd) + maxfd = clifd; + if (i > maxi) + maxi = i; + continue; + } + for (int i = 0; i <= maxi; i++) { + if ((clifd = sockets[i].fd) < 0) + continue; + if (FD_ISSET(clifd, &rfds)) { + if (read_sock(&sockets[i]) == 0) { + fprintf(stderr, "Closed socket fd %d\n", clifd); /* FIXME */ + FD_CLR(clifd, &rallfds); + FD_CLR(clifd, &wallfds); + sockets[i].fd = -1; + close(clifd); + } else { + process_commands(window, &sockets[i]); + } + } + if (FD_ISSET(clifd, &wfds)) { + write_sock(&sockets[i]); + } + } + } + if (window->dirty_rect.w != 0 && window->dirty_rect.h != 0) { + ltk_redraw_window(window); + window->dirty_rect.w = 0; + window->dirty_rect.h = 0; + } + if (window->last_event && running) { + struct ltk_event_queue *cur = window->last_event; + struct ltk_event_queue *last; + do { + int event_len = strlen(cur->data); + for (int i = 0; i <= maxi; i++) { + if (sockets[i].fd != -1 && sockets[i].event_mask & cur->event_type) { + if (queue_sock_write(&sockets[i], cur->data, event_len) < 0) + exit(1); /* FIXME: error handling */ + } + } + free(cur->data); + last = cur; + cur = cur->prev; + free(last); + } while (cur); + window->first_event = window->last_event = NULL; + } + + } + for (int i = 0; i < MAX_SOCK_CONNS; i++) { + if (sockets[i].read) + free(sockets[i].read); + if (sockets[i].to_write) + free(sockets[i].to_write); + if (sockets[i].tokens.tokens) + free(sockets[i].tokens.tokens); + } + + unlink("ltk.sock"); + + return 0; +} + +void +ltk_redraw_window(ltk_window *window) { + ltk_widget *ptr; + if (!window) return; + if (window->dirty_rect.x >= window->rect.w) return; + if (window->dirty_rect.y >= window->rect.h) return; + if (window->dirty_rect.x + window->dirty_rect.w > window->rect.w) + window->dirty_rect.w -= window->dirty_rect.x + window->dirty_rect.w - window->rect.w; + if (window->dirty_rect.y + window->dirty_rect.h > window->rect.h) + window->dirty_rect.h -= window->dirty_rect.y + window->dirty_rect.h - window->rect.h; + XClearArea(window->dpy, window->xwindow, window->dirty_rect.x, window->dirty_rect.y, window->dirty_rect.w, window->dirty_rect.h, False); + if (!window->root_widget) return; + /* FIXME: actually respect the dirty rect... */ + ptr = window->root_widget; + ptr->draw(ptr); +} + +void +ltk_window_other_event(ltk_window *window, XEvent event) { + ltk_widget *ptr = window->root_widget; + if (event.type == ConfigureNotify) { + unsigned int w, h; + w = event.xconfigure.width; + h = event.xconfigure.height; + 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; + if (ptr && ptr->resize) { + ptr->rect.w = w; + ptr->rect.h = h; + ptr->resize(ptr, orig_w, orig_h); + } + } + } else if (event.type == Expose && event.xexpose.count == 0) { + ltk_rect r; + r.x = event.xexpose.x; + r.y = event.xexpose.y; + r.w = event.xexpose.width; + r.h = event.xexpose.height; + ltk_window_invalidate_rect(window, r); + } else if (event.type == ClientMessage + && event.xclient.data.l[0] == window->wm_delete_msg) { + ltk_destroy_window(window); + exit(0); /* FIXME */ + } +} + +ltk_window * +ltk_create_window(const char *theme_path, const char *title, int x, int y, unsigned int w, unsigned int h) { + ltk_window *window = malloc(sizeof(ltk_window)); + if (!window) + ltk_fatal("Not enough memory left for window!\n"); + + window->dpy = XOpenDisplay(NULL); + window->screen = DefaultScreen(window->dpy); + window->cm = DefaultColormap(window->dpy, window->screen); + /* FIXME: validate theme properly */ + ltk_load_theme(window, theme_path); + window->wm_delete_msg = XInternAtom(window->dpy, "WM_DELETE_WINDOW", False); + + ltk_window_theme *wtheme = window->theme->window; + window->xwindow = + XCreateSimpleWindow(window->dpy, DefaultRootWindow(window->dpy), x, y, + w, h, wtheme->border_width, + wtheme->fg.pixel, wtheme->bg.pixel); + window->gc = XCreateGC(window->dpy, window->xwindow, 0, 0); + XSetForeground(window->dpy, window->gc, wtheme->fg.pixel); + XSetBackground(window->dpy, window->gc, wtheme->bg.pixel); + XSetStandardProperties(window->dpy, window->xwindow, title, NULL, None, + NULL, 0, NULL); + XSetWMProtocols(window->dpy, window->xwindow, &window->wm_delete_msg, 1); + window->root_widget = NULL; + + ltk_init_text(wtheme->font, window->dpy, window->screen, window->cm); + + window->other_event = &ltk_window_other_event; + + window->rect.w = 0; + window->rect.h = 0; + 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->widget_hash = kh_init(widget); + + XClearWindow(window->dpy, window->xwindow); + XMapRaised(window->dpy, window->xwindow); + XSelectInput(window->dpy, window->xwindow, + ExposureMask | KeyPressMask | KeyReleaseMask | + ButtonPressMask | ButtonReleaseMask | + StructureNotifyMask | PointerMotionMask); + + return window; +} + +void +ltk_destroy_window(ltk_window *window) { + khint_t k; + ltk_widget *ptr; + XDestroyWindow(window->dpy, window->xwindow); + for (k = kh_begin(window->widget_hash); k != kh_end(window->widget_hash); k++) { + if (kh_exist(window->widget_hash, k)) { + ptr = kh_value(window->widget_hash, k); + ptr->destroy(ptr, 1); + } + } + kh_destroy(widget, window->widget_hash); + ltk_cleanup_text(); + XCloseDisplay(window->dpy); + free(window); +} + +void +ltk_window_ini_handler(ltk_window *window, const char *prop, const char *value) { + /* FIXME: A whole lot more error checking! */ + if (strcmp(prop, "border_width") == 0) { + window->theme->window->border_width = atoi(value); + } else if (strcmp(prop, "bg") == 0) { + ltk_create_xcolor(window, value, &window->theme->window->bg); + } else if (strcmp(prop, "fg") == 0) { + ltk_create_xcolor(window, value, &window->theme->window->fg); + } else if (strcmp(prop, "font") == 0) { + window->theme->window->font = strdup(value); + } else if (strcmp(prop, "font_size") == 0) { + window->theme->window->font_size = atoi(value); + } +} + +int +ltk_ini_handler(void *window, const char *widget, const char *prop, const char *value) { + if (strcmp(widget, "window") == 0) { + ltk_window_ini_handler(window, prop, value); + } else if (strcmp(widget, "button") == 0) { + ltk_button_ini_handler(window, prop, value); + } else { + return 0; + } + return 1; +} + +static void +ltk_load_theme(ltk_window *window, const char *path) { + window->theme = malloc(sizeof(ltk_theme)); + if (!window->theme) ltk_fatal("Unable to allocate memory for theme.\n"); + window->theme->window = malloc(sizeof(ltk_window_theme)); + if (!window->theme->window) ltk_fatal("Unable to allocate memory for window theme.\n"); + window->theme->button = NULL; + if (ini_parse(path, ltk_ini_handler, window) < 0) { + (void)fprintf(stderr, "ERROR: Can't load theme %s\n.", path); + exit(1); + } +} + +static void +ltk_destroy_theme(ltk_theme *theme) { + free(theme->button); + free(theme->window); + free(theme); +} + +int +ltk_collide_rect(ltk_rect rect, int x, int y) { + return (rect.x <= x && (rect.x + rect.w) >= x && rect.y <= y + && (rect.y + rect.h) >= y); +} + +void +ltk_window_remove_active_widget(ltk_window *window) { + ltk_widget *widget = window->active_widget; + if (!widget) return; + while (widget) { + widget->state = LTK_NORMAL; + widget->active_widget = NULL; + if (widget->change_state) + widget->change_state(widget); + if (widget->needs_redraw) + ltk_window_invalidate_rect(window, widget->rect); + widget = widget->parent; + } + window->active_widget = NULL; +} + +void +ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget) { + window->active_widget = widget; + ltk_widget *parent = widget->parent; + widget->state = LTK_ACTIVE; + while (parent) { + widget->state = LTK_ACTIVE; + parent->active_widget = widget; + widget = parent; + parent = widget->parent; + } +} + +void +ltk_fill_widget_defaults(ltk_widget *widget, const char *id, ltk_window *window, + void (*draw) (void *), void (*change_state) (void *), + void (*destroy) (void *, int), unsigned int needs_redraw, + ltk_widget_type type) { + widget->id = strdup(id); + widget->window = window; + widget->active_widget = NULL; + widget->parent = NULL; + widget->type = type; + + widget->key_press = NULL; + widget->key_release = NULL; + widget->mouse_press = NULL; + widget->mouse_release = NULL; + widget->motion_notify = NULL; + widget->mouse_enter = NULL; + widget->mouse_leave = NULL; + + widget->resize = NULL; + widget->draw = draw; + widget->change_state = change_state; + widget->destroy = destroy; + + widget->needs_redraw = needs_redraw; + widget->state = LTK_NORMAL; + widget->row = 0; + widget->rect.x = 0; + widget->rect.y = 0; + widget->rect.w = 100; + widget->rect.h = 100; + + widget->row = 0; + widget->column = 0; + widget->row_span = 0; + widget->column_span = 0; + widget->sticky = 0; +} + +void +ltk_widget_mouse_press_event(ltk_widget *widget, XEvent event) { + if (!widget || widget->state == LTK_DISABLED) + return; + if (event.xbutton.button == 1) { + /* ltk_widget *parent = widget->parent; FIXME */ + widget->state = LTK_PRESSED; + if (widget->change_state) + widget->change_state(widget); + if (widget->needs_redraw) + ltk_window_invalidate_rect(widget->window, widget->rect); + } + if (widget->mouse_press) { + widget->mouse_press(widget, event); + } +} + +void +ltk_widget_mouse_release_event(ltk_widget *widget, XEvent event) { + if (!widget || widget->state == LTK_DISABLED) + return; + if (widget->state == LTK_PRESSED) { + widget->state = LTK_ACTIVE; + if (widget->change_state) + widget->change_state(widget); + if (widget->needs_redraw) + ltk_window_invalidate_rect(widget->window, widget->rect); + } + if (widget->mouse_release) { + widget->mouse_release(widget, event); + } +} + +void +ltk_widget_motion_notify_event(ltk_widget *widget, XEvent event) { + if (!widget) return; + short pressed = (event.xmotion.state & Button1Mask) == Button1Mask; + if ((widget->state == LTK_NORMAL) && !pressed) { + widget->state = LTK_ACTIVE; + if (widget->change_state) + widget->change_state(widget); + if (widget->mouse_enter) + widget->mouse_enter(widget, event); + /* FIXME: do this properly */ + if (widget->window->active_widget != widget && !widget->motion_notify) { + ltk_window_remove_active_widget(widget->window); + ltk_window_set_active_widget(widget->window, widget); + } + if (widget->needs_redraw) + ltk_window_invalidate_rect(widget->window, widget->rect); + } + if (widget->motion_notify) + widget->motion_notify(widget, event); +} + +void +ltk_handle_event(ltk_window *window, XEvent event) { + ltk_widget *root_widget = window->root_widget; + switch (event.type) { + case KeyPress: + break; + case KeyRelease: + break; + case ButtonPress: + if (root_widget) + ltk_widget_mouse_press_event(root_widget, event); + break; + case ButtonRelease: + if (root_widget) + ltk_widget_mouse_release_event(root_widget, event); + break; + case MotionNotify: + if (root_widget) + ltk_widget_motion_notify_event(root_widget, event); + break; + default: + if (window->other_event) + window->other_event(window, event); + } +} + +int +ltk_check_widget_id_free(ltk_window *window, const char *id, const char *caller) { + khint_t k; + k = kh_get(widget, window->widget_hash, id); + if (k != kh_end(window->widget_hash)) { + (void)fprintf(stderr, "%s: Widget \"%s\" already exists.\n", caller, id); + return 0; + } + return 1; +} + +ltk_widget * +ltk_get_widget(ltk_window *window, const char *id, ltk_widget_type type, + const char *caller) { + khint_t k; + ltk_widget *widget; + k = kh_get(widget, window->widget_hash, id); + if (k == kh_end(window->widget_hash)) { + (void)fprintf(stderr, "%s: Widget \"%s\" doesn't exist.\n", caller, id); + return NULL; + } + widget = kh_value(window->widget_hash, k); + if (type != LTK_WIDGET && widget->type != type) { + (void)fprintf(stderr, "%s: Widget \"%s\" has wrong type.\n", caller, id); + return NULL; + } + return widget; +} + +void +ltk_set_widget(ltk_window *window, ltk_widget *widget, const char *id) { + int ret; + khint_t k; + /* apparently, khash requires the string to stay accessible */ + char *tmp = strdup(id); + k = kh_put(widget, window->widget_hash, tmp, &ret); + kh_value(window->widget_hash, k) = widget; +} + +void +ltk_remove_widget(ltk_window *window, const char *id) { + khint_t k; + k = kh_get(widget, window->widget_hash, id); + if (k != kh_end(window->widget_hash)) { + kh_del(widget, window->widget_hash, k); + } +} + +int main(int argc, char *argv[]) { + ltk_window *window = ltk_create_window("theme.ini", "Demo", 0, 0, 500, 500); + return ltk_mainloop(window); +} diff --git a/socket_format.txt b/socket_format.txt @@ -1,5 +1,5 @@ -Note: This is not implemented yet; it is just here to collect -my thoughts while I keep working. +Note: This is not fully implemented yet; it is just here to +collect my thoughts while I keep working. <widget type> <widget id> <command> <args> > grid grd1 create 2 2 diff --git a/test.sh b/test.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +# This is very hacky. +# +# All events are still printed to the terminal curerntly because +# the second './ltkc' still prints everything - event masks aren't +# supported yet. +# +# Currently, everything just uses the socket 'ltk.sock'. My idea +# was to have a directory containing sockets for all instances of +# ltks, each of them named after the X Window ID used. This would +# allow other tools (screenreader, etc.) to determine which socket +# to use to communicate with a window. Probably ltkd would just +# print out its window ID when starting and then daemonize itself, +# so the calling script can just use save the output of ltkd and +# use that to communicate with the socket using ltkc. + +./ltkd& +sleep 0.2 + +cat test.gui | ./ltkc | while read cmd +do + case "$cmd" in + "btn1 button_click") echo "quit" + ;; + esac +done | ./ltkc diff --git a/test_anim.sh b/test_anim.sh @@ -1,29 +0,0 @@ -#!/bin/sh - -echo "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 -draw drw1 create 100 100 #fff -grid grd1 add drw1 0 1 1 1 15 -draw drw1 set-color #000 -" - -i=0 -while [ $i -lt 400 ]; do - j=0 - while [ $j -lt 20 ]; do - echo "draw drw1 line $((i+j)) $j $((i+j+2)) $((j+2))"; - sleep 0.05 - j=$((j+2)) - done - j=20 - while [ $j -gt 0 ]; do - echo "draw drw1 line $((i+40-j)) $j $((i+42-j)) $((j-2))"; - sleep 0.05 - j=$((j-2)) - done - i=$((i+40)); -done diff --git a/test_draw.gui b/test_draw.gui @@ -1,10 +0,0 @@ -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 -draw drw1 create 100 100 #fff -grid grd1 add drw1 0 1 1 1 15 -draw drw1 set-color #000 -draw drw1 line 0 0 100 100