├── .gitignore ├── README.md ├── TODO.md ├── c ├── Makefile └── src │ ├── lhelp.c │ ├── lhelp.h │ ├── platform.c │ ├── sane.h │ ├── util.c │ ├── util.h │ └── xcb │ ├── context.c │ ├── context.h │ ├── event.c │ ├── event.h │ ├── scairo.c │ ├── spixmap.c │ ├── swin.c │ ├── terra_xkb.c │ ├── terra_xkb.h │ ├── vidata.c │ ├── vidata.h │ ├── window.c │ ├── window.h │ ├── xcb_ctx.c │ ├── xcb_ctx.h │ ├── xlhelp.c │ ├── xlhelp.h │ ├── xutil.c │ └── xutil.h ├── l ├── _retired.lua ├── abonaments │ └── tcp │ │ └── client.lua ├── element.lua ├── input │ ├── click.lua │ ├── clickbind.lua │ ├── clickmap.lua │ ├── key.lua │ ├── keybind.lua │ └── keymap.lua ├── oak │ ├── align.lua │ ├── border.lua │ ├── elements │ │ ├── _retired.lua │ │ ├── branches │ │ │ ├── _retired.lua │ │ │ ├── branch.lua │ │ │ ├── el.lua │ │ │ ├── horizontal.lua │ │ │ ├── internal.lua │ │ │ ├── root.lua │ │ │ └── vertical.lua │ │ ├── element.lua │ │ ├── internal.lua │ │ └── leaves │ │ │ ├── bg.lua │ │ │ ├── leaf.lua │ │ │ ├── svg.lua │ │ │ └── text.lua │ ├── internal.lua │ ├── padding.lua │ ├── shape.lua │ ├── size.lua │ └── source.lua ├── object.lua ├── orchard.lua ├── platforms │ ├── common │ │ └── window.lua │ └── xcb │ │ ├── app.lua │ │ └── window.lua ├── puv.lua ├── sigtools.lua └── tools │ ├── color.lua │ ├── enum.lua │ ├── promise.lua │ ├── shapers.lua │ ├── sstr.lua │ ├── table.lua │ ├── tracker.lua │ └── urn.lua ├── notes.md ├── run_tests.sh ├── shell.nix ├── showcase ├── green_background_red_ball.png ├── made_with_terra.png ├── mathgraph.png └── whether.png ├── terra-dev-1.rockspec └── tests └── tools ├── sstr_spec.lua └── urn ├── basics_spec.lua └── json_spec.lua /.gitignore: -------------------------------------------------------------------------------- 1 | terra/ 2 | *.o 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## IMPORTANT 2 | This project is currently unmaintained. 3 | 4 | # Terra 5 | *What if you didn't have to write electron apps anymore?* 6 | 7 | Terra offers a platform-independent, featureful toolkit that allows you to easily and quickly build your application, and then flexibly go as low-level as you need to optimize your application. 8 | This toolkit was built based on my experience with [AwesomeWM](https://github.com/awesomeWM/awesome), [Elm](https://elm-lang.org/), and [Elm-ui](https://github.com/mdgriffith/elm-ui), and other projects. I hope you find it useful. 9 |

Whether weather app image

10 | 11 | ## Build gorgeous, high-performance applications 12 | Terra aims to make developing cross platform applications an experience that is both fast and pleasant, without sacrificing on aesthetics or performance. 13 |

Mathgraph app image

14 | 15 | ## Current state 16 | **Warning: Terra is experimental software and very early in development. We push to the main branch and live life day to day. :)** 17 | Also, I'm working on this whenever I have time and whenever I feel like it. I may be slow to solve issues. 18 | Terra currently only works with luajit on linux under Xorg. Support for Wayland, Windows and Mac is planned. 19 | If you experience any issues with installing, bugs, etc., you can open an issue or contact me directly. 20 | 21 | ## Example 22 | This code creates an application, creates a window, creates a UI tree with Oak, terra's built-in UI library, paints a green background, creates a red ball, draws "hello world" on it, and animates the ball to spin in a circle. 23 | ```lua 24 | #!/usr/bin/env luajit 25 | 26 | local t_app = require("terra.app") 27 | local t_window = require("terra.window." .. t_app.get_platform()) 28 | 29 | local tt_color = require("terra.tools.color") 30 | 31 | local to_size = require("terra.oak.size") 32 | local to_align = require("terra.oak.align") 33 | 34 | local toeb_root = require("terra.oak.elements.branches.root") 35 | local toeb_el = require("terra.oak.elements.branches.el") 36 | 37 | local toel_bg = require("terra.oak.elements.leaves.bg") 38 | local toel_text = require("terra.oak.elements.leaves.text") 39 | 40 | local function init_app(app) 41 | 42 | local model = {} 43 | app.model = model 44 | 45 | -- create the window 46 | model.main_window = t_window.create(app, 320, 420, 200, 160, { 47 | 48 | tree = toeb_root.new({ -- the root of the UI tree 49 | toeb_el.new({ -- the background of the window 50 | width = to_size.FILL, 51 | height = to_size.FILL, 52 | bg = toel_bg.new({ 53 | source = tt_color.rgb(0.17, 0.42, 0.21), -- green 54 | }), 55 | toeb_el.new({ -- the red ball 56 | halign = to_align.CENTER, 57 | valign = to_align.CENTER, 58 | width = 60, 59 | height = 60, 60 | bg = toel_bg.new({ 61 | source = tt_color.rgb(0.8, 0.1, 0), -- red 62 | border_radius = 30, 63 | }), 64 | -- declaratively subscribe to signals on elements 65 | subscribe_on_root = { 66 | ["AnimationEvent"] = function(self, time) 67 | local spin_push = 20 68 | self:set_offset_x(math.sin(time) * spin_push) 69 | self:set_offset_y(-math.cos(time) * spin_push) 70 | end 71 | }, 72 | toel_text.new({ -- "hello world" text 73 | family = "Roboto", 74 | size = 11, 75 | width = 40, -- constrain the width so the text wraps 76 | halign = to_align.CENTER, 77 | valign = to_align.CENTER, 78 | text = "hello world", 79 | fg = tt_color.rgb(1, 1, 1), 80 | }) 81 | }), 82 | }), 83 | }), 84 | }) 85 | 86 | t_window.request_raise(model.main_window) 87 | end 88 | 89 | t_app.desktop(init_app, t_app.make_default_event_handler(function(app, event_type, ...) 90 | end)) 91 | ``` 92 | 93 | The above code produces the following output (without the titlebar): 94 |

green background red ball image

95 | 96 | ## Installing 97 | 1. Clone the repo 98 | `git clone https://github.com/chris-montero/terra.git` 99 | 2. Build the project and install the luarock 100 | `sudo luarocks make` 101 | 3. That's it. Now you should be able to successfully run the code in the [Example](#Example) section. 102 | 103 | # Credits 104 | * [Uli Schlachter](https://github.com/psychon), for promptly and elaborately answering my questions about Xorg on stack overflow. 105 | * My mom, for sponsoring this project. Thanks mom. 106 | 107 | # Contributing 108 | You are welcome to contribute by opening issues, comitting code, or through donations. 109 | Any support is sincerely appreciated. 110 | 111 | https://ko-fi.com/chrismontero 112 | 113 | US DOLLAR IBAN: `RO75BTRLUSDCRT0323524101` 114 | EURO IBAN: `RO71BTRLEURCRT0323524101` 115 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | 2 | # TODO 3 | [ ] maybe have separate `set_bounding_geometry`, `set_drawing_geometry`, `get_bounding_geometry` and `get_drawing_geometry` functions, instead of just having `set_geometry` and `get_geometry`. This could be very useful in cases where you want to layer children in a container, you want one of the children to be "anchored" to the relative x & y coordinates of the parent "el", you want this child to draw something, but you don't want it to take up any space in the "el" parent. In this case, you could override the `get_bounding_geometry` and make it `return x, y, 0, 0`. This way, the element would still be allowed to be anchored to the parent and draw, but would not take up any space in the layout. 4 | 5 | [ ] we need a good way to set and get the shape of an element. Additionally, we need to find a way to clip the sub-children of the element to its shape so that they can't draw outside of it. Additionally, we need to find a way to "punch holes" into an element, and be able to "see through" that hole AND click through it. I think the way I'm going to make all of this work is by having a "shape" property. I'm pretty sure cairo lets save a shape, and also lets you do "is_mouse_inside" or something like that, which would basically solve all of our issues. I'm also thinking that it would be nice to have a "clip_through_parent" property which would essentially "punch" a hole through the parent in the shape of the child. The reason this might be a good idea is because later on, if I'm going to be optimizing everything to only redraw its before/after geometry, moving the child (and thus moving the "hole") would also be optimized with this system. 6 | 7 | [ ] write documentation. Maybe use [Sphinx](https://www.sphinx-doc.org/en/master/)? 8 | -------------------------------------------------------------------------------- /c/Makefile: -------------------------------------------------------------------------------- 1 | CC = clang 2 | # CC = gcc 3 | 4 | BUILD_DIR = build 5 | 6 | LIB_NAME = application 7 | LIB_NAME_DEBUG = $(LIB_NAME) 8 | 9 | SRC_DIR = src 10 | APP_SRC_FILE_NAMES = \ 11 | app \ 12 | application \ 13 | lhelp \ 14 | terra_xkb \ 15 | event \ 16 | util \ 17 | xdraw 18 | 19 | APP_FULL_C_FILE_PATHS = $(patsubst %, $(SRC_DIR)/%.c, $(APP_SRC_FILE_NAMES)) 20 | APP_FULL_O_FILE_PATHS = $(patsubst %, %.o, $(APP_SRC_FILE_NAMES)) 21 | 22 | WINDOWS_DIR = $(SRC_DIR)/windows 23 | SWIN_SRC_FILE_NAMES = swin 24 | 25 | SWIN_FULL_C_FILE_PATHS = $(patsubst %, $(SRC_DIR)/%.c, $(SWIN_SRC_FILE_NAMES)) $(WINDOWS_DIR)/xcb.c 26 | SWIN_FULL_O_FILE_PATHS = $(patsubst %, %.o, $(SWIN_SRC_FILE_NAMES)) xcb.o 27 | 28 | SCAIRO = scairo 29 | SCAIRO_FULL_C_FILE_PATHS = $(SRC_DIR)/$(SCAIRO).c 30 | SCAIRO_FULL_O_FILE_PATHS = $(SCAIRO).o 31 | 32 | SPIXMAP = spixmap 33 | SPIXMAP_FULL_C_FILE_PATHS = $(SRC_DIR)/$(SPIXMAP).c 34 | SPIXMAP_FULL_O_FILE_PATHS = $(SPIXMAP).o 35 | 36 | PACKAGES = \ 37 | luajit \ 38 | xcb xcb-keysyms xcb-cursor \ 39 | x11 \ 40 | xkbcommon xkbcommon-x11 \ 41 | cairo 42 | PKGCONFIG = pkg-config 43 | COMPILER_FLAGS = -std=c99 -Wall -Wextra 44 | CFLAGS = $(shell $(PKGCONFIG) --cflags $(PACKAGES)) -Isrc 45 | LIBS = $(shell $(PKGCONFIG) --libs $(PACKAGES)) -lev 46 | 47 | all: application swin scairo spixmap 48 | 49 | application: compile_application build_application 50 | swin : compile_swin build_swin 51 | scairo : compile_scairo build_scairo 52 | spixmap : compile_spixmap build_spixmap 53 | 54 | compile_application: $(APP_FULL_C_SRC_PATHS) 55 | $(CC) \ 56 | $(CFLAGS) \ 57 | $(APP_FULL_C_FILE_PATHS) \ 58 | $(COMPILER_FLAGS) \ 59 | -c -fpic 60 | 61 | build_application: $(FULL_SRC_FILES) 62 | $(CC) \ 63 | $(LIBS) \ 64 | $(APP_FULL_O_FILE_PATHS) \ 65 | $(COMPILER_FLAGS) \ 66 | -shared \ 67 | -o $(LIB_NAME).so 68 | 69 | compile_swin : 70 | $(CC) \ 71 | $(CFLAGS) \ 72 | $(SWIN_FULL_C_FILE_PATHS) \ 73 | $(COMPILER_FLAGS) \ 74 | -c -fpic 75 | 76 | build_swin : 77 | $(CC) \ 78 | $(LIBS) \ 79 | $(SWIN_FULL_O_FILE_PATHS) \ 80 | $(COMPILER_FLAGS) \ 81 | -shared \ 82 | -o swin.so 83 | 84 | compile_scairo : 85 | $(CC) \ 86 | $(CFLAGS) \ 87 | $(SCAIRO_FULL_C_FILE_PATHS) \ 88 | $(COMPILER_FLAGS) \ 89 | -c -fpic 90 | 91 | build_scairo : 92 | $(CC) \ 93 | $(LIBS) \ 94 | $(SCAIRO_FULL_O_FILE_PATHS) \ 95 | $(COMPILER_FLAGS) \ 96 | -shared \ 97 | -o scairo.so 98 | 99 | compile_spixmap : 100 | $(CC) \ 101 | $(CFLAGS) \ 102 | $(SPIXMAP_FULL_C_FILE_PATHS) \ 103 | $(COMPILER_FLAGS) \ 104 | -c -fpic 105 | 106 | build_spixmap : 107 | $(CC) \ 108 | $(LIBS) \ 109 | $(SPIXMAP_FULL_O_FILE_PATHS) \ 110 | $(COMPILER_FLAGS) \ 111 | -shared \ 112 | -o spixmap.so 113 | 114 | debug: $(FULL_SRC_FILES) 115 | $(CC) \ 116 | --verbose -g \ 117 | $(FULL_SRC_FILES) \ 118 | $(COMPILER_FLAGS) \ 119 | $(CFLAGS) \ 120 | $(LIBS) \ 121 | -o $(LIB_NAME) 122 | 123 | clean: 124 | $(RM) *.o *.so core* 125 | 126 | .PHONY: all application swin scairo spixmap clean 127 | -------------------------------------------------------------------------------- /c/src/lhelp.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | #include "lhelp.h" 6 | 7 | #include "sane.h" 8 | 9 | // from luajit source code. Copyright Mike Pall. (probably copy pasted from lua5.2 lol) 10 | void luaL_setfuncs(lua_State *L, const luaL_Reg *l, int nup) 11 | { 12 | luaL_checkstack(L, nup, "too many upvalues"); 13 | for (; l->name; l++) { 14 | int i; 15 | for (i = 0; i < nup; i++) /* Copy upvalues to the top. */ 16 | lua_pushvalue(L, -nup); 17 | lua_pushcclosure(L, l->func, nup); 18 | lua_setfield(L, -(nup + 2), l->name); 19 | } 20 | lua_pop(L, nup); /* Remove upvalues. */ 21 | } 22 | 23 | void lhelp_dump_stack(lua_State *L) 24 | { 25 | uint i = lua_gettop(L); 26 | fprintf(stderr, "LUA STACK DUMP START: \n"); 27 | while (i != 0) { 28 | uint t = lua_type(L, i); 29 | switch (t) { 30 | case LUA_TSTRING: 31 | fprintf(stderr, "%d\t string: %s\n", i, lua_tostring(L, i)); 32 | break; 33 | case LUA_TBOOLEAN: 34 | fprintf(stderr, "%d\t boolean: %s", i, lua_toboolean(L, i) ? "true\n" : "false\n"); 35 | break; 36 | case LUA_TNUMBER: 37 | fprintf(stderr, "%d\t number: %g\n", i, lua_tonumber(L, i)); 38 | break; 39 | case LUA_TNIL: 40 | fprintf(stderr, "%d\t nil\n", i); 41 | break; 42 | case LUA_TTABLE: 43 | fprintf(stderr, "%d\t table(%p); length: %lu\n", i, lua_topointer(L, i), lua_objlen(L, i)); 44 | break; 45 | default: 46 | fprintf( 47 | stderr, 48 | "%d\t %s(%p); length: %lu\n", 49 | i, 50 | lua_typename(L, t), 51 | lua_topointer(L, i), 52 | lua_objlen(L, i) 53 | ); 54 | break; 55 | } 56 | i--; 57 | } 58 | fprintf(stderr, "\nLUA STACK DUMP END \n"); 59 | } 60 | 61 | // the default runtime error function 62 | int lhelp_function_on_runtime_error(lua_State *L) 63 | { 64 | // push the `deubg.traceback` function on the stack 65 | lua_getglobal(L, "debug"); 66 | lua_pushstring(L, "traceback"); 67 | lua_rawget(L, -2); 68 | lua_remove(L, -2); // remove the debug library 69 | 70 | // push the return value of `debug.traceback(, 1)` on the stack 71 | lua_tostring(L, 1); // make sure the error is a string 72 | lua_pushvalue(L, 1); 73 | lua_pushinteger(L, 1); 74 | lua_call(L, 2, 1); 75 | 76 | // remove the original message 77 | lua_remove(L, 1); 78 | 79 | // now create a message like: 80 | // ----------------- RUNTIME ERROR ----------------- 81 | // 82 | lua_pushstring(L, "\n----------------- RUNTIME ERROR -----------------\n"); 83 | lua_insert(L, 1); 84 | lua_pushstring(L, "\n\n"); 85 | lua_concat(L, 3); 86 | 87 | // the error string we created is still on the stack 88 | return 1; 89 | } 90 | 91 | // does t[`name`] = `b` where "t" is the table on top of the stack 92 | void lhelp_set_bool(lua_State *L, char *name, bool b) 93 | { 94 | lua_pushstring(L, name); 95 | lua_pushboolean(L, b); 96 | lua_rawset(L, -3); 97 | } 98 | 99 | // does t[`name`] = `i` where "t" is the table on top of the stack 100 | void lhelp_set_int(lua_State *L, char *name, int i) 101 | { 102 | lua_pushstring(L, name); 103 | lua_pushinteger(L, i); 104 | lua_rawset(L, -3); 105 | } 106 | 107 | // does t[`name`] = `value` where "t" is the table on top of the stack 108 | void lhelp_set_string(lua_State *L, char *name, char *value) 109 | { 110 | lua_pushstring(L, name); 111 | lua_pushstring(L, value); 112 | lua_rawset(L, -3); 113 | } 114 | -------------------------------------------------------------------------------- /c/src/lhelp.h: -------------------------------------------------------------------------------- 1 | #ifndef TERRA_LHELP_H 2 | #define TERRA_LHELP_H 3 | 4 | #include 5 | #include 6 | 7 | #include "sane.h" 8 | 9 | // from lua5.2. In spite of the fact that we use luajit, which defines these 10 | // macros, we use luarocks to compile the project, which uses lua5.1's 11 | // headers. What this means is that we have to define these ourselves to 12 | // ensure compatibility with lua5.1. 13 | void luaL_setfuncs(lua_State *L, const luaL_Reg *l, int nup); 14 | #define luaL_newlibtable(L, l) \ 15 | lua_createtable(L, 0, sizeof(l)/sizeof((l)[0]) - 1) 16 | #define luaL_newlib(L, l) (luaL_newlibtable(L, l), luaL_setfuncs(L, l, 0)) 17 | 18 | void lhelp_dump_stack(lua_State *L); 19 | int lhelp_function_on_runtime_error(lua_State *L); 20 | 21 | void lhelp_set_bool(lua_State *L, char *name, bool b); 22 | void lhelp_set_int(lua_State *L, char *name, int i); 23 | void lhelp_set_string(lua_State *L, char *name, char *value); 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /c/src/platform.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | // #define PLATFORM "xcb" // TODO: parameterize this 4 | // 5 | // int terra_get_platform(lua_State *L) 6 | // { 7 | // lua_pushstring(L, PLATFORM); 8 | // return 1; 9 | // } 10 | // 11 | // static const struct luaL_Reg lib_terra_platform[] = { 12 | // 13 | // { "get", terra_get_platform }, 14 | // { NULL, NULL }, 15 | // }; 16 | // 17 | // int luaopen_terra_platform(lua_State *L) 18 | // { 19 | // luaL_newlib(L, lib_terra_platform); 20 | // 21 | // return 1; 22 | // } 23 | 24 | #define PLATFORM "xcb" // TODO: parameterize this 25 | 26 | int luaopen_terra_platform(lua_State *L) 27 | { 28 | lua_pushstring(L, PLATFORM); 29 | 30 | return 1; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /c/src/sane.h: -------------------------------------------------------------------------------- 1 | #ifndef SANE_LIB_H 2 | #define SANE_LIB_H 3 | 4 | #include 5 | 6 | // macros are normally defined in uppercase. why should "true" and "false" 7 | // be different? 8 | #define FALSE 0 9 | #define TRUE 1 10 | 11 | #ifndef NULL 12 | #define NULL ((void *)0) 13 | #endif 14 | 15 | // much easier to type 16 | #define i8 int8_t 17 | #define i16 int16_t 18 | #define i32 int32_t 19 | #define i64 int64_t 20 | #define u8 uint8_t 21 | #define u16 uint16_t 22 | #define u32 uint32_t 23 | #define u64 uint64_t 24 | 25 | // what people should be using as default instead of "int" 26 | #define uint unsigned int 27 | 28 | typedef uint8_t byte8; 29 | typedef uint16_t byte16; 30 | #define bool unsigned int 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /c/src/util.c: -------------------------------------------------------------------------------- 1 | 2 | #include // for `fprintf`, `stderr` 3 | #include 4 | #include // for backtrace 5 | 6 | #include "util.h" 7 | 8 | #define MAX_BACKTRACE_STACK_SIZE 32 9 | 10 | void util_backtrace_print(void) 11 | { 12 | void *stack[MAX_BACKTRACE_STACK_SIZE]; 13 | char **symbols; 14 | int stack_size; 15 | 16 | stack_size = backtrace(stack, NUMBEROF(stack)); 17 | symbols = backtrace_symbols(stack, stack_size); 18 | 19 | if(symbols == NULL) return; // TODO: maybe I can remove this 20 | 21 | fprintf(stderr, "Dumping backtrace:\n"); 22 | for(int i = 0; i < stack_size; i++) { 23 | fprintf(stderr, "\t%s\n", symbols[i]); 24 | } 25 | free(symbols); 26 | } 27 | 28 | -------------------------------------------------------------------------------- /c/src/util.h: -------------------------------------------------------------------------------- 1 | #ifndef TERRA_UTIL_H 2 | #define TERRA_UTIL_H 3 | 4 | #define UNUSED(x) (void)(x) 5 | #define NUMBEROF(arr) (sizeof((arr)) / sizeof((arr[0]))) 6 | 7 | #ifdef DEBUG 8 | // ##__VA_ARGS__ makes it so it removes the last ',' if there's no more args 9 | #define DLOG(fmt, ...) fprintf(stderr, "%d %s %s : " fmt, __LINE__, __FILE__, __FUNCTION__, ##__VA_ARGS__) 10 | #else 11 | #define DLOG(...) 12 | #endif 13 | 14 | void util_backtrace_print(void); 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /c/src/xcb/context.h: -------------------------------------------------------------------------------- 1 | #ifndef TERRA_XCB_CONTEXT_H 2 | #define TERRA_XCB_CONTEXT_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | // #include 9 | 10 | #include 11 | 12 | #include "sane.h" 13 | 14 | // TODO: organize these properly 15 | struct XcbContext { 16 | lua_State *L; 17 | 18 | struct xcb_connection_t *connection; 19 | // TODO: the screen seems to hold information about width in px and mm. 20 | // Use this information to figure out the dpi of each screen 21 | struct xcb_screen_t *screen; 22 | 23 | struct xkb_context *xkb_ctx; 24 | struct xkb_state *xkb_state; 25 | 26 | xcb_visualtype_t *visual; 27 | u8 visual_depth; 28 | i32 default_screen_number; 29 | xcb_colormap_t default_colormap_id; 30 | 31 | // keyboard support 32 | xcb_key_symbols_t *keysyms; 33 | 34 | // cursor support 35 | xcb_cursor_context_t *cursor_ctx; 36 | xcb_cursor_t current_cursor; 37 | 38 | xcb_window_t gc_window; 39 | xcb_gcontext_t default_gc_id; 40 | 41 | #ifdef DEBUG 42 | struct xcb_errors_context_t *xcb_error_ctx; 43 | #endif 44 | }; 45 | 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /c/src/xcb/event.h: -------------------------------------------------------------------------------- 1 | #ifndef TERRA_XCB_EVENT_H 2 | #define TERRA_XCB_EVENT_H 3 | 4 | void event_handle(xcb_generic_event_t *e); 5 | #endif 6 | -------------------------------------------------------------------------------- /c/src/xcb/scairo.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | #include "lhelp.h" 8 | 9 | #include "xcb/xlhelp.h" 10 | #include "xcb/context.h" 11 | 12 | int luaH_scairo_surface_create_from_pixmap(lua_State *L) 13 | { 14 | struct XcbContext *xc = xlhelp_check_xcb_ctx(L, 1); 15 | xcb_pixmap_t pixmap_id = (xcb_pixmap_t)xlhelp_check_id(L, 2); 16 | u16 width = luaL_checkinteger(L, 3); 17 | u16 height = luaL_checkinteger(L, 4); 18 | 19 | cairo_surface_t *cairo_surface = cairo_xcb_surface_create( 20 | xc->connection, 21 | pixmap_id, 22 | xc->visual, 23 | width, 24 | height 25 | ); 26 | 27 | // push the pointer directly as a lua number. TODO: is this safe? 28 | // Also: http://lua-users.org/wiki/LightUserData says 29 | // "Light userdata are intended to store C pointers in Lua (note: 30 | // Lua numbers may or may not be suitable for this purpose depending 31 | // on the data types on the platform)." 32 | // My problem was that I couldn't give lgi a lightuserdata and have 33 | // it understand it was a pointer, but it worked if I just gave it 34 | // a number, hence the use of an integer here. 35 | lua_pushinteger(L, (u64)cairo_surface); 36 | return 1; 37 | } 38 | 39 | int luaH_scairo_surface_set_pixmap(lua_State *L) 40 | { 41 | cairo_surface_t *cairo_surface = (cairo_surface_t *) lua_tointeger(L, 1); 42 | xcb_pixmap_t pixmap_id = (xcb_pixmap_t)xlhelp_check_id(L, 2); 43 | u16 width = luaL_checkinteger(L, 3); 44 | u16 height = luaL_checkinteger(L, 4); 45 | 46 | cairo_xcb_surface_set_drawable(cairo_surface, pixmap_id, width, height); 47 | 48 | return 0; 49 | } 50 | 51 | int luaH_scairo_surface_destroy(lua_State *L) 52 | { 53 | cairo_surface_t *cairo_surface = (cairo_surface_t *) lua_tointeger(L, 1); 54 | cairo_surface_finish(cairo_surface); 55 | cairo_surface_destroy(cairo_surface); 56 | return 0; 57 | } 58 | 59 | static const struct luaL_Reg lib_scairo[] = { 60 | { "create_from_pixmap", luaH_scairo_surface_create_from_pixmap }, 61 | { "set_pixmap", luaH_scairo_surface_set_pixmap }, 62 | { "destroy", luaH_scairo_surface_destroy }, 63 | { NULL, NULL } 64 | }; 65 | 66 | int luaopen_terra_platforms_xcb_scairo(lua_State *L) 67 | { 68 | luaL_newlib(L, lib_scairo); 69 | return 1; 70 | } 71 | 72 | -------------------------------------------------------------------------------- /c/src/xcb/spixmap.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | #include "lhelp.h" 8 | 9 | #include "xcb/xlhelp.h" 10 | #include "xcb/context.h" 11 | 12 | int luaH_spixmap_create(lua_State *L) 13 | { 14 | struct XcbContext *xc = xlhelp_check_xcb_ctx(L, 1); 15 | u16 width = luaL_checkint(L, 2); 16 | u16 height = luaL_checkint(L, 3); 17 | 18 | xcb_pixmap_t pixmap_id = xcb_generate_id(xc->connection); 19 | xcb_create_pixmap( 20 | xc->connection, 21 | xc->visual_depth, 22 | pixmap_id, 23 | xc->screen->root, // drawable to get the screen from 24 | width, 25 | height 26 | ); 27 | 28 | xlhelp_push_id(L, pixmap_id); 29 | 30 | return 1; 31 | } 32 | 33 | int luaH_spixmap_draw_portion_to_window(lua_State *L) 34 | { 35 | struct XcbContext *xc = xlhelp_check_xcb_ctx(L, 1); 36 | xcb_pixmap_t pix_id = (xcb_pixmap_t)xlhelp_check_id(L, 2); 37 | xcb_window_t win_id = (xcb_window_t)xlhelp_check_id(L, 3); 38 | 39 | u16 x = luaL_checkinteger(L, 4); 40 | u16 y = luaL_checkinteger(L, 5); 41 | u16 width = luaL_checkinteger(L, 6); 42 | u16 height = luaL_checkinteger(L, 7); 43 | 44 | xcb_copy_area( 45 | xc->connection, 46 | pix_id, 47 | win_id, 48 | xc->default_gc_id, // do we even need gcs in x anymore? 49 | x, x, 50 | y, y, 51 | width, height 52 | ); 53 | 54 | return 0; 55 | } 56 | 57 | int luaH_spixmap_destroy(lua_State *L) 58 | { 59 | struct XcbContext *xc = xlhelp_check_xcb_ctx(L, 1); 60 | xcb_pixmap_t pix_id = (xcb_pixmap_t)xlhelp_check_id(L, 2); 61 | xcb_free_pixmap(xc->connection, pix_id); 62 | return 0; 63 | } 64 | 65 | static const struct luaL_Reg lib_spixmap[] = { 66 | { "create", luaH_spixmap_create }, 67 | { "draw_portion_to_window", luaH_spixmap_draw_portion_to_window }, 68 | { "destroy", luaH_spixmap_destroy }, 69 | { NULL, NULL } 70 | }; 71 | 72 | int luaopen_terra_platforms_xcb_spixmap(lua_State *L) 73 | { 74 | luaL_newlib(L, lib_spixmap); 75 | return 1; 76 | } 77 | 78 | -------------------------------------------------------------------------------- /c/src/xcb/swin.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | 6 | #include "lhelp.h" 7 | 8 | #include "xcb/window.h" 9 | #include "xlhelp.h" 10 | // #include "click.h" 11 | // #include "key.h" 12 | 13 | int luaH_swin_create(lua_State *L) 14 | { 15 | struct XcbContext *xc = xlhelp_check_xcb_ctx(L, 1); 16 | // TODO: maybe check that these don't go over bounds 17 | i16 x = luaL_checkint(L, 2); 18 | i16 y = luaL_checkint(L, 3); 19 | u16 width = luaL_checkint(L, 4); 20 | u16 height = luaL_checkint(L, 5); 21 | luaL_checktype(L, 6, LUA_TBOOLEAN); 22 | u8 override_redirect = lua_toboolean(L, 6); 23 | printf("override_redirect: %b\n", override_redirect); 24 | 25 | // these can never be 0 26 | if (width == 0) width = 1; 27 | if (height == 0) height = 1; 28 | 29 | // TODO: make this platform independent 30 | xcb_window_t x_window_id = terra_xcb_window_create(xc, x, y, width, height, override_redirect); 31 | xlhelp_push_id(L, x_window_id); 32 | xcb_flush(xc->connection); 33 | return 1; 34 | } 35 | 36 | int luaH_swin_map_request(lua_State *L) { 37 | struct XcbContext *xc = xlhelp_check_xcb_ctx(L, 1); 38 | xcb_window_t x_win_id = (xcb_window_t)xlhelp_check_id(L, 2); 39 | terra_xcb_window_map_request(xc, x_win_id); 40 | xcb_flush(xc->connection); 41 | return 0; 42 | } 43 | 44 | int luaH_swin_unmap(lua_State *L) { 45 | struct XcbContext *xc = xlhelp_check_xcb_ctx(L, 1); 46 | xcb_window_t x_win_id = (xcb_window_t)xlhelp_check_id(L, 2); 47 | terra_xcb_window_unmap(xc, x_win_id); 48 | return 0; 49 | } 50 | 51 | int luaH_swin_set_geometry_request(lua_State *L) { 52 | struct XcbContext *xc = xlhelp_check_xcb_ctx(L, 1); 53 | xcb_window_t x_win_id = (xcb_window_t)xlhelp_check_id(L, 2); 54 | 55 | // TODO: maybe check that these don't go over bounds 56 | i16 x = (i16)luaL_checkint(L, 3); 57 | i16 y = (i16)luaL_checkint(L, 4); 58 | u16 width = (u16)luaL_checkint(L, 5); 59 | u16 height = (u16)luaL_checkint(L, 6); 60 | 61 | terra_xcb_window_set_geometry_request(xc, x_win_id, x, y, width, height); 62 | return 0; 63 | } 64 | 65 | int luaH_swin_set_coordinates_request(lua_State *L) { 66 | struct XcbContext *xc = xlhelp_check_xcb_ctx(L, 1); 67 | xcb_window_t x_win_id = (xcb_window_t)xlhelp_check_id(L, 2); 68 | 69 | // TODO: maybe check that these don't go over bounds 70 | i16 x = (i16)luaL_checkint(L, 3); 71 | i16 y = (i16)luaL_checkint(L, 4); 72 | 73 | terra_xcb_window_set_coordinates_request(xc, x_win_id, x, y); 74 | return 0; 75 | } 76 | 77 | int luaH_swin_set_sizes_request(lua_State *L) { 78 | struct XcbContext *xc = xlhelp_check_xcb_ctx(L, 1); 79 | xcb_window_t x_win_id = (xcb_window_t)xlhelp_check_id(L, 2); 80 | 81 | // TODO: maybe check that these don't go over bounds 82 | u16 width = (u16)luaL_checkint(L, 3); 83 | u16 height = (u16)luaL_checkint(L, 4); 84 | 85 | terra_xcb_window_set_sizes_request(xc, x_win_id, width, height); 86 | return 0; 87 | } 88 | 89 | int luaH_swin_destroy(lua_State *L) 90 | { 91 | struct XcbContext *xc = xlhelp_check_xcb_ctx(L, 1); 92 | xcb_window_t x_win_id = (xcb_window_t)xlhelp_check_id(L, 2); 93 | terra_xcb_window_destroy(xc, x_win_id); 94 | return 0; 95 | } 96 | 97 | // TODO: use properties for this (I think ICCCM had something for this) 98 | // int luaH_swin_focus_request(lua_State *L) 99 | // { 100 | // xcb_window_t x_win_id = (xcb_window_t)xlhelp_check_id(L, 1); 101 | // window_set_focus(x_win_id); 102 | // return 0; 103 | // } 104 | 105 | 106 | // // TODO: move this documentation somewhere else, or maybe just put a 107 | // // manual together and also include this. 108 | // // you should be able to use this from the lua side as such: 109 | // // `swin.subscribe_key(, )` 110 | // // where is a table of the form: 111 | // // { 112 | // // key: string, 113 | // // is_press: TRUE (for press) or FALSE (for release), 114 | // // modifiers : number (which is a 16 bit bitmask denoting which modifiers have to be pressed for this key to fire) 115 | // // } 116 | // int luaH_swin_subscribe_key(lua_State *L) 117 | // { 118 | // xcb_window_t x_win_id = (xcb_window_t)xlhelp_check_id(L, 1); 119 | // struct Key k = xlhelp_key_from_table(L); // expects table to be on top 120 | // window_subscribe_key(x_win_id, k); 121 | // 122 | // // printf("Subscribing Key on window %d:\n", win); 123 | // // printf("\tkeycode: %d\n", k.keycode); 124 | // // printf("\tkey_event: %d\n", k.key_event); 125 | // // printf("\tmodifiers: %b\n", k.modifiers); 126 | // 127 | // return 0; 128 | // } 129 | // 130 | // int luaH_swin_unsubscribe_key(lua_State *L) 131 | // { 132 | // xcb_window_t x_win_id = (xcb_window_t)xlhelp_check_id(L, 1); 133 | // // TODO: maybe rework this into `xlhelp_check_key` 134 | // struct Key k = xlhelp_key_from_table(L); // expects table to be on top 135 | // window_unsubscribe_key(x_win_id, k); 136 | // return 0; 137 | // } 138 | // 139 | // int luaH_swin_subscribe_click(lua_State *L) 140 | // { 141 | // xcb_window_t x_win_id = (xcb_window_t)xlhelp_check_id(L, 1); 142 | // struct Click c = xlhelp_click_from_table(L); // expects table to be on top 143 | // window_subscribe_click(x_win_id, c); 144 | // return 0; 145 | // } 146 | // 147 | // int luaH_swin_unsubscribe_click(lua_State *L) 148 | // { 149 | // xcb_window_t x_win_id = (xcb_window_t)xlhelp_check_id(L, 1); 150 | // // TODO: maybe rework this into `xlhelp_check_click` 151 | // struct Click c = xlhelp_click_from_table(L); // expects table to be on top 152 | // window_unsubscribe_click(x_win_id, c); 153 | // return 0; 154 | // } 155 | // 156 | // int luaH_swin_grab_pointer(lua_State *L) 157 | // { 158 | // xcb_window_t x_win_id = (xcb_window_t)xlhelp_check_id(L, 1); 159 | // const xcb_event_mask_t event_mask = (xcb_event_mask_t)luaL_checkinteger(L, 2); 160 | // window_grab_pointer(x_win_id, event_mask); 161 | // return 0; 162 | // } 163 | // 164 | // int luaH_swin_ungrab_pointer(lua_State *L) 165 | // { 166 | // window_ungrab_pointer(); 167 | // return 0; 168 | // } 169 | // 170 | // int luaH_swin_window_stack_above(lua_State *L) 171 | // { 172 | // xcb_window_t below = (xcb_window_t)xlhelp_check_id(L, 1); 173 | // xcb_window_t win_id = (xcb_window_t)xlhelp_check_id(L, 2); 174 | // 175 | // window_stack_above(below, win_id); 176 | // return 0; 177 | // } 178 | 179 | static const struct luaL_Reg lib_swin[] = { 180 | { "create", luaH_swin_create }, 181 | { "destroy", luaH_swin_destroy }, 182 | { "map_request", luaH_swin_map_request }, 183 | { "unmap", luaH_swin_unmap }, 184 | { "set_geometry_request", luaH_swin_set_geometry_request }, 185 | { "set_coordinates_request", luaH_swin_set_coordinates_request }, 186 | { "set_sizes_request", luaH_swin_set_sizes_request }, 187 | // { "change_event_mask", luaH_swin_change_event_mask }, 188 | // { "set_focus", luaH_swin_set_focus }, 189 | // { "map_request", luaH_swin_map_request }, 190 | // { "subscribe_key", luaH_swin_subscribe_key }, 191 | // { "unsubscribe_key", luaH_swin_unsubscribe_key }, 192 | // { "subscribe_click", luaH_swin_subscribe_click }, 193 | // { "unsubscribe_click", luaH_swin_unsubscribe_click }, 194 | // { "grab_pointer", luaH_swin_grab_pointer }, 195 | // { "ungrab_pointer", luaH_swin_ungrab_pointer }, 196 | // { "stack_above", luaH_swin_window_stack_above }, 197 | { NULL, NULL } 198 | }; 199 | 200 | int luaopen_terra_platforms_xcb_swin(lua_State *L) 201 | { 202 | luaL_newlib(L, lib_swin); 203 | return 1; 204 | } 205 | 206 | 207 | -------------------------------------------------------------------------------- /c/src/xcb/terra_xkb.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include // for `memcpy` 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "sane.h" 10 | 11 | #include "xcb/terra_xkb.h" 12 | #include "xcb/context.h" 13 | #include "xcb/xcb_ctx.h" 14 | 15 | 16 | // returns true if the given utf-8 string is a control character such as 17 | // Control or Shift, etc. 18 | bool _is_control_character(char *str) 19 | { 20 | if (str[0] == (0x7f)/*(127)*/) return TRUE; 21 | if (str[0] >= 0 && str[0] < 0x20) return TRUE; 22 | return FALSE; 23 | } 24 | 25 | // NOTE: we use this "keybuffer" struct because for some reason we can't just 26 | // return a simple 64 byte buffer, even though C has ways of specifying how 27 | // large this buffer return value is going to be. 28 | struct Keybuffer terra_xkb_keycode_to_string(xcb_keycode_t keycode) 29 | { 30 | // XXX: for now, we just use 64 bytes which is usually enough. The output 31 | // may get truncated, but its a sacrifice I am willing to make. 32 | // If someone complains, we'll fix this then 33 | static char str[KEY_NAME_BUFFER_LENGTH]; // xkbcommon/xkbcommon.h recommends a 34 | // length of at least 64 bytes, so lets try that. 35 | 36 | // int string_length = xkb_state_key_get_utf8( 37 | xkb_state_key_get_utf8( 38 | xcb_ctx.xkb_state, 39 | keycode, 40 | str, 41 | KEY_NAME_BUFFER_LENGTH 42 | ); 43 | 44 | // if the key pressed is a control character, just push the key name, 45 | // like "Control", "Shift", etc. 46 | if (_is_control_character(str)) { 47 | xcb_keysym_t keysym = xcb_key_symbols_get_keysym( 48 | xcb_ctx.keysyms, 49 | keycode, 50 | 0 // col ?? (thats what xcb/xcb_keysyms.h says) 51 | ); 52 | // the same truncation rules as above apply here. We'll fix it 53 | // if we need to 54 | xkb_keysym_get_name(keysym, str, KEY_NAME_BUFFER_LENGTH); 55 | } 56 | 57 | struct Keybuffer kbf; 58 | memcpy(&kbf.key_str, str, KEY_NAME_BUFFER_LENGTH); 59 | 60 | return kbf; 61 | } 62 | 63 | 64 | // initialize xkb 65 | void terra_xkb_init(void) 66 | { 67 | // TRUE on success, FALSE on error 68 | int xkb_success = xkb_x11_setup_xkb_extension( 69 | xcb_ctx.connection, 70 | XKB_X11_MIN_MAJOR_XKB_VERSION, 71 | XKB_X11_MIN_MINOR_XKB_VERSION, 72 | 0, // extension flags 73 | NULL, // major_xkb_version_out 74 | NULL, // minor_xkb_version_out 75 | NULL, // base_event_out 76 | NULL // base_error_out 77 | ); 78 | 79 | if (xkb_success == FALSE) { 80 | fprintf(stderr, "COULDN'T INITIALIZE XKB. EXITING.\n"); 81 | exit(1); 82 | } 83 | 84 | u16 event_mask = 85 | XCB_XKB_EVENT_TYPE_STATE_NOTIFY 86 | | XCB_XKB_EVENT_TYPE_MAP_NOTIFY 87 | | XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY; 88 | 89 | // maps used to allow key remapping in terra 90 | u16 remapping_mask = 91 | XCB_XKB_MAP_PART_KEY_TYPES 92 | | XCB_XKB_MAP_PART_KEY_SYMS 93 | | XCB_XKB_MAP_PART_MODIFIER_MAP 94 | | XCB_XKB_MAP_PART_EXPLICIT_COMPONENTS 95 | | XCB_XKB_MAP_PART_KEY_ACTIONS 96 | | XCB_XKB_MAP_PART_KEY_BEHAVIORS 97 | | XCB_XKB_MAP_PART_VIRTUAL_MODS 98 | | XCB_XKB_MAP_PART_VIRTUAL_MOD_MAP; 99 | 100 | // enable detectable auto-repeat, but ignore failures // TODO: what exactly does this mean 101 | xcb_xkb_per_client_flags_cookie_t pclient_flags; 102 | pclient_flags = xcb_xkb_per_client_flags( 103 | xcb_ctx.connection, 104 | XCB_XKB_ID_USE_CORE_KBD, // deviceSpec 105 | XCB_XKB_PER_CLIENT_FLAG_DETECTABLE_AUTO_REPEAT, // change 106 | XCB_XKB_PER_CLIENT_FLAG_DETECTABLE_AUTO_REPEAT, // value 107 | 0, // ctrlsToChange 108 | 0, // autoCtrls 109 | 0 // autoCtrlsValues 110 | ); 111 | xcb_discard_reply( 112 | xcb_ctx.connection, 113 | pclient_flags.sequence 114 | ); 115 | 116 | xcb_xkb_select_events( 117 | xcb_ctx.connection, 118 | XCB_XKB_ID_USE_CORE_KBD, // deviceSpec 119 | event_mask, // affectWhich 120 | 0, // clear 121 | event_mask, // selectAll 122 | remapping_mask, // affectMap 123 | remapping_mask, // map 124 | 0 // details // TODO: I think this should be NULL 125 | ); 126 | 127 | // Init keymap 128 | 129 | // The steps go as follows: 130 | // (1) you create an xkb_context, which you use to 131 | // (2) create an xkb_keymap, which you use to 132 | // (3) create an xkb_state 133 | // and finally, in the event handling function, you 134 | // (4) use the xkb_state in a function like `xkb_state_key_get_utf8` to 135 | // get the string representation of the key pressed or released, etc. 136 | xcb_ctx.xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); 137 | if (xcb_ctx.xkb_ctx == NULL) { 138 | fprintf(stderr, "Could not create XKB context used for resolving keypresses. Exiting.\n"); 139 | exit(1); 140 | } 141 | 142 | // TODO: learn about xkb concepts like layouts, rules, devices, keymaps, etc. 143 | // And xkb in general, and provide lua APIs for working with them 144 | 145 | // try to get id of the current keyboard 146 | i32 device_id = xkb_x11_get_core_keyboard_device_id(xcb_ctx.connection); 147 | if (device_id == -1) { 148 | fprintf(stderr, "Could not get XKB device id. Exiting."); 149 | exit(1); 150 | } 151 | 152 | struct xkb_keymap *xkb_keymap = xkb_x11_keymap_new_from_device( 153 | xcb_ctx.xkb_ctx, 154 | xcb_ctx.connection, 155 | device_id, 156 | XKB_KEYMAP_COMPILE_NO_FLAGS 157 | ); 158 | 159 | if (xkb_keymap == NULL) { 160 | fprintf(stderr, "Could not get XKB keymap from device. Exiting."); 161 | exit(1); 162 | } 163 | 164 | xcb_ctx.xkb_state = xkb_x11_state_new_from_device( 165 | xkb_keymap, 166 | xcb_ctx.connection, 167 | device_id 168 | ); 169 | 170 | // we're done using this keymap, so unref it 171 | xkb_keymap_unref(xkb_keymap); 172 | 173 | if (xcb_ctx.xkb_state == NULL) { 174 | fprintf(stderr, "Could not get XKB state from device. Exiting."); 175 | exit(1); 176 | } 177 | 178 | } 179 | 180 | 181 | void terra_xkb_free(void) 182 | { 183 | // unsubscribe from all events 184 | xcb_xkb_select_events( 185 | xcb_ctx.connection, 186 | XCB_XKB_ID_USE_CORE_KBD, 187 | 0, 188 | 0, 189 | 0, 190 | 0, 191 | 0, 192 | 0 193 | ); 194 | 195 | // free keymap related data 196 | xkb_state_unref(xcb_ctx.xkb_state); 197 | xkb_context_unref(xcb_ctx.xkb_ctx); 198 | } 199 | 200 | 201 | -------------------------------------------------------------------------------- /c/src/xcb/terra_xkb.h: -------------------------------------------------------------------------------- 1 | #ifndef TERRA_XCB_TERRA_XKB_H 2 | #define TERRA_XCB_TERRA_XKB_H 3 | 4 | #define KEY_NAME_BUFFER_LENGTH 64 5 | 6 | struct Keybuffer { 7 | char key_str[KEY_NAME_BUFFER_LENGTH]; 8 | }; 9 | 10 | void terra_xkb_init(void); 11 | void terra_xkb_free(void); 12 | 13 | struct Keybuffer terra_xkb_keycode_to_string(xcb_keycode_t keycode); 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /c/src/xcb/vidata.c: -------------------------------------------------------------------------------- 1 | 2 | #include // for `fprintf`, `stderr` 3 | #include // for `exit` 4 | 5 | #include 6 | 7 | #include "sane.h" 8 | 9 | #include "xcb/vidata.h" 10 | 11 | struct VisualData vidata_find_argb(const xcb_screen_t *s) 12 | { 13 | struct VisualData vdata; 14 | vdata.visual = NULL; // used to check for failure 15 | vdata.visual_depth = 0; 16 | 17 | xcb_depth_iterator_t depth_iter = xcb_screen_allowed_depths_iterator(s); 18 | 19 | if(depth_iter.data == NULL) { 20 | return vdata; 21 | } 22 | 23 | while(depth_iter.rem != 0) { 24 | 25 | if (depth_iter.data->depth != 32) { 26 | printf( 27 | "Found depth_iterator with depth %d. Skipping.\n", 28 | depth_iter.data->depth 29 | ); 30 | xcb_depth_next(&depth_iter); 31 | continue; 32 | } 33 | 34 | printf( 35 | "Successfully found depth_iterator with depth %d.\n", 36 | depth_iter.data->depth 37 | ); 38 | 39 | xcb_visualtype_iterator_t visual_iter = xcb_depth_visuals_iterator(depth_iter.data); 40 | while (visual_iter.rem != 0) { 41 | vdata.visual = visual_iter.data; 42 | vdata.visual_depth = depth_iter.data->depth; 43 | // TODO: are these any different from each other? 44 | // printf("Found Visual:\n"); 45 | // printf("\tbits_per_rgb_value: %d\n", visual_iter.data->bits_per_rgb_value); 46 | // printf("\t_class: %d\n", visual_iter.data->_class); 47 | // printf("\tcolormap_entries: %d\n", visual_iter.data->colormap_entries); 48 | // printf("\tred_mask: %d\n", visual_iter.data->red_mask); 49 | // printf("\tgreen_mask: %d\n", visual_iter.data->green_mask); 50 | // printf("\tblue_mask: %d\n", visual_iter.data->blue_mask); 51 | return vdata; 52 | xcb_visualtype_next(&visual_iter); 53 | } 54 | } 55 | return vdata; 56 | } 57 | 58 | struct VisualData vidata_find_default(const xcb_screen_t *s) 59 | { 60 | struct VisualData vdata; 61 | vdata.visual = NULL; // used to check for failure 62 | vdata.visual_depth = 0; 63 | 64 | xcb_depth_iterator_t depth_iter = xcb_screen_allowed_depths_iterator(s); 65 | 66 | if(depth_iter.data == NULL) { 67 | return vdata; 68 | } 69 | 70 | printf( 71 | "Found depth_iterator with depth %d. Using this one as default.\n", 72 | depth_iter.data->depth 73 | ); 74 | 75 | xcb_visualtype_iterator_t visual_iter = xcb_depth_visuals_iterator(depth_iter.data); 76 | while (visual_iter.rem != 0) { 77 | vdata.visual = visual_iter.data; 78 | vdata.visual_depth = depth_iter.data->depth; 79 | // TODO: are these any different from each other? 80 | // printf("Found Visual:\n"); 81 | // printf("\tbits_per_rgb_value: %d\n", visual_iter.data->bits_per_rgb_value); 82 | // printf("\t_class: %d\n", visual_iter.data->_class); 83 | // printf("\tcolormap_entries: %d\n", visual_iter.data->colormap_entries); 84 | // printf("\tred_mask: %d\n", visual_iter.data->red_mask); 85 | // printf("\tgreen_mask: %d\n", visual_iter.data->green_mask); 86 | // printf("\tblue_mask: %d\n", visual_iter.data->blue_mask); 87 | return vdata; 88 | xcb_visualtype_next(&visual_iter); 89 | } 90 | 91 | return vdata; 92 | } 93 | 94 | bool vidata_is_invalid(struct VisualData vidata) 95 | { 96 | if (vidata.visual == NULL) { 97 | return TRUE; 98 | } else { 99 | return FALSE; 100 | } 101 | } 102 | 103 | // u8 vidata_find_visual_depth(const xcb_screen_t *s, xcb_visualid_t vid) 104 | // { 105 | // xcb_depth_iterator_t depth_iter = xcb_screen_allowed_depths_iterator(s); 106 | // 107 | // if(!depth_iter.data) { // TODO: check 108 | // goto abort; 109 | // } 110 | // while (depth_iter.rem) { // TODO: check 111 | // xcb_visualtype_iterator_t visual_iter = xcb_depth_visuals_iterator(depth_iter.data); 112 | // while (visual_iter.rem) { // TODO: check 113 | // if(vid == visual_iter.data->visual_id) { 114 | // return depth_iter.data->depth; 115 | // } 116 | // xcb_visualtype_next(&visual_iter); 117 | // } 118 | // xcb_depth_next(&depth_iter); 119 | // } 120 | // goto abort; // if everything failed, just close everything 121 | // 122 | // abort: 123 | // fprintf(stderr, "FATAL: Could not find a visual's depth"); 124 | // exit(1); // TODO: exit with another code once we have all of them figured out? 125 | // } 126 | 127 | -------------------------------------------------------------------------------- /c/src/xcb/vidata.h: -------------------------------------------------------------------------------- 1 | #ifndef TERRA_XCB_VIDATA_H 2 | #define TERRA_XCB_VIDATA_H 3 | 4 | #include 5 | 6 | #include "sane.h" 7 | 8 | struct VisualData { 9 | xcb_visualtype_t *visual; 10 | u8 visual_depth; 11 | }; 12 | 13 | bool vidata_is_invalid(struct VisualData vidata); 14 | struct VisualData vidata_find_argb(const xcb_screen_t *s); 15 | struct VisualData vidata_find_default(const xcb_screen_t *s); 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /c/src/xcb/window.c: -------------------------------------------------------------------------------- 1 | 2 | // #include // for `free` // TODO: remove 3 | #include // for `printf` TODO: remove 4 | 5 | #include 6 | 7 | #include "xcb/context.h" 8 | 9 | 10 | xcb_window_t terra_xcb_window_create(struct XcbContext *xc, i16 x, i16 y, u16 width, u16 height, u8 override_redirect) 11 | { 12 | // lhelp_dump_stack(L); 13 | 14 | xcb_event_mask_t ev_mask = 15 | XCB_EVENT_MASK_KEY_PRESS 16 | | XCB_EVENT_MASK_KEY_RELEASE 17 | | XCB_EVENT_MASK_BUTTON_PRESS 18 | | XCB_EVENT_MASK_BUTTON_RELEASE 19 | | XCB_EVENT_MASK_ENTER_WINDOW 20 | | XCB_EVENT_MASK_LEAVE_WINDOW 21 | | XCB_EVENT_MASK_POINTER_MOTION 22 | | XCB_EVENT_MASK_EXPOSURE 23 | | XCB_EVENT_MASK_VISIBILITY_CHANGE 24 | | XCB_EVENT_MASK_STRUCTURE_NOTIFY 25 | | XCB_EVENT_MASK_FOCUS_CHANGE 26 | | XCB_EVENT_MASK_PROPERTY_CHANGE; 27 | 28 | // | XCB_EVENT_MASK_KEYMAP_STATE 29 | // | XCB_EVENT_MASK_RESIZE_REDIRECT 30 | // | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY 31 | // | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT 32 | // | XCB_EVENT_MASK_COLOR_MAP_CHANGE 33 | // | XCB_EVENT_MASK_OWNER_GRAB_BUTTON 34 | 35 | xcb_window_t x_window_id = xcb_generate_id(xc->connection); 36 | 37 | // NOTE: apparently we MUST specify values for BACK_PIXEL and 38 | // BORDER_PIXEL, otherwise the window doesn't show up? I don't know why. 39 | u32 x_window_value_mask = 40 | XCB_CW_BACK_PIXEL 41 | | XCB_CW_BORDER_PIXEL 42 | | XCB_CW_BIT_GRAVITY 43 | | XCB_CW_OVERRIDE_REDIRECT 44 | | XCB_CW_EVENT_MASK 45 | | XCB_CW_COLORMAP; 46 | // XCB_CW_CURSOR; // TODO: select cursor 47 | u32 x_window_value_list[] = { 48 | xc->screen->black_pixel, 49 | xc->screen->black_pixel, 50 | XCB_GRAVITY_NORTH_WEST, 51 | override_redirect, 52 | ev_mask, 53 | xc->default_colormap_id 54 | }; 55 | xcb_create_window( 56 | xc->connection, 57 | xc->visual_depth, 58 | x_window_id, 59 | xc->screen->root, // parent id 60 | x, 61 | y, 62 | width, 63 | height, 64 | 0, // border width (the window manager will take care of this) 65 | XCB_WINDOW_CLASS_INPUT_OUTPUT, 66 | // XCB_WINDOW_CLASS_COPY_FROM_PARENT, 67 | xc->visual->visual_id, // TODO: learn more about visuals 68 | x_window_value_mask, 69 | x_window_value_list 70 | ); 71 | 72 | return x_window_id; 73 | } 74 | 75 | void terra_xcb_window_change_cursor(struct XcbContext *xc, xcb_window_t x_window_id, char *cursor_str) 76 | { 77 | xcb_cursor_t new_cursor = xcb_cursor_load_cursor(xc->cursor_ctx, cursor_str); 78 | xcb_cursor_t old_cursor = xc->current_cursor; 79 | xcb_change_window_attributes( 80 | xc->connection, 81 | x_window_id, 82 | XCB_CW_CURSOR, 83 | (u32[]){ new_cursor } 84 | ); 85 | xcb_free_cursor(xc->connection, old_cursor); 86 | xc->current_cursor = new_cursor; 87 | } 88 | 89 | void terra_xcb_window_set_geometry_request( 90 | struct XcbContext *xc, 91 | xcb_window_t x_window_id, 92 | i16 x, 93 | i16 y, 94 | u16 width, 95 | u16 height 96 | ) { 97 | u32 window_config_mask = 98 | XCB_CONFIG_WINDOW_X 99 | | XCB_CONFIG_WINDOW_Y 100 | | XCB_CONFIG_WINDOW_WIDTH 101 | | XCB_CONFIG_WINDOW_HEIGHT; 102 | 103 | const i32 window_config_values[] = { x, y, width, height }; 104 | 105 | xcb_configure_window( 106 | xc->connection, 107 | x_window_id, 108 | window_config_mask, 109 | window_config_values 110 | ); 111 | // xcb_flush(xc->connection); 112 | } 113 | 114 | void terra_xcb_window_set_sizes_request(struct XcbContext *xc, xcb_window_t x_window_id, u16 width, u16 height) 115 | { 116 | u32 client_config_mask = XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT; 117 | const u32 client_config_values[] = { width, height }; 118 | xcb_configure_window( 119 | xc->connection, 120 | x_window_id, 121 | client_config_mask, 122 | client_config_values 123 | ); 124 | } 125 | 126 | void terra_xcb_window_set_coordinates_request(struct XcbContext *xc, xcb_window_t x_window_id, i16 x, i16 y) 127 | { 128 | u32 client_config_mask = XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y; 129 | const u32 client_config_values[] = { x, y }; 130 | 131 | xcb_configure_window( 132 | xc->connection, 133 | x_window_id, 134 | client_config_mask, 135 | client_config_values 136 | ); 137 | } 138 | 139 | 140 | // void terra_xcb_window_map_request(struct XcbContext *xc, xcb_window_t x_window_id) 141 | // { 142 | // xcb_map_request_event_t mr; 143 | // 144 | // mr.response_type = XCB_MAP_REQUEST; 145 | // mr.parent = xc->screen->root; 146 | // mr.window = x_window_id; 147 | // xcb_send_event( 148 | // xc->connection, 149 | // FALSE, // override_redirect 150 | // xc->screen->root, 151 | // XCB_EVENT_MASK_STRUCTURE_NOTIFY, 152 | // (char *) &mr 153 | // ); 154 | // // xcb_flush(xc->connection); 155 | // } 156 | 157 | void terra_xcb_window_map_request(struct XcbContext *xc, xcb_window_t x_window_id) 158 | { 159 | xcb_map_window(xc->connection, x_window_id); 160 | } 161 | 162 | void terra_xcb_window_unmap(struct XcbContext *xc, xcb_window_t x_window_id) 163 | { 164 | xcb_unmap_window(xc->connection, x_window_id); 165 | } 166 | 167 | void terra_xcb_window_destroy(struct XcbContext *xc, xcb_window_t x_window_id) 168 | { 169 | xcb_destroy_window(xc->connection, x_window_id); 170 | } 171 | 172 | // // TODO: make this work based on properties 173 | // void terra_xcb_window_set_focus_request(struct XcbContext *xc, xcb_window_t window_id) { 174 | // if (window_id == XCB_NONE) return; 175 | // xcb_set_input_focus( 176 | // xc->connection, 177 | // XCB_INPUT_FOCUS_POINTER_ROOT, 178 | // // XCB_INPUT_FOCUS_NONE, 179 | // window_id, 180 | // XCB_CURRENT_TIME 181 | // ); 182 | // } 183 | 184 | 185 | // // TODO: maybe I should just let users only rely on automatic grabs? 186 | // void terra_xcb_window_grab_pointer(xcb_window_t x_win_id, xcb_event_mask_t event_mask) 187 | // { 188 | // xcb_grab_pointer( 189 | // xc->connection, 190 | // FALSE, // "owner_events" (if the grab window should still get the events) 191 | // x_win_id, 192 | // event_mask, 193 | // XCB_GRAB_MODE_ASYNC, 194 | // XCB_GRAB_MODE_ASYNC, 195 | // XCB_NONE, // confine_to 196 | // XCB_NONE, // cursor 197 | // XCB_CURRENT_TIME 198 | // ); 199 | // } 200 | // void terra_xcb_window_ungrab_pointer() 201 | // { 202 | // xcb_ungrab_pointer(xc->connection, XCB_CURRENT_TIME); 203 | // } 204 | // 205 | 206 | // void terra_xcb_window_subscribe_key(xcb_window_t x_win_id, struct Key key) 207 | // { 208 | // // xcb_keycode_t *keycode = util_xcb_keysym_to_keycode(keybindings[i].keysym); // TODO: dont I have to free this?? 209 | // xcb_grab_key( 210 | // xc->connection, 211 | // FALSE, // owner_events. Should the window still get the event for this key? 212 | // x_win_id, 213 | // key.modifiers, 214 | // key.keycode, 215 | // XCB_GRAB_MODE_ASYNC, 216 | // XCB_GRAB_MODE_ASYNC 217 | // ); 218 | // } 219 | // 220 | // void terra_xcb_window_unsubscribe_key(xcb_window_t x_win_id, struct Key key) 221 | // { 222 | // xcb_ungrab_key( 223 | // xc->connection, 224 | // key.keycode, 225 | // x_win_id, 226 | // key.modifiers 227 | // ); 228 | // } 229 | // 230 | // void terra_xcb_window_subscribe_click(xcb_window_t x_win_id, struct Click click) 231 | // { 232 | // xcb_grab_button( 233 | // xc->connection, 234 | // TRUE, // owner events. should the grabbing window still get events? 235 | // x_win_id, 236 | // XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE, 237 | // XCB_GRAB_MODE_ASYNC, 238 | // XCB_GRAB_MODE_ASYNC, 239 | // xc->screen->root, // confine_to window 240 | // // x_win_id, 241 | // XCB_NONE, // cursor 242 | // click.button, 243 | // click.modifiers 244 | // ); 245 | // } 246 | // 247 | // void terra_xcb_window_unsubscribe_click(xcb_window_t x_win_id, struct Click click) 248 | // { 249 | // xcb_ungrab_button( 250 | // xc->connection, 251 | // click.button, 252 | // x_win_id, 253 | // click.modifiers 254 | // ); 255 | // } 256 | 257 | -------------------------------------------------------------------------------- /c/src/xcb/window.h: -------------------------------------------------------------------------------- 1 | #ifndef TERRA_XCB_WINDOW_H 2 | #define TERRA_XCB_WINDOW_H 3 | 4 | #include 5 | 6 | #include "xcb/context.h" 7 | #include "sane.h" 8 | 9 | xcb_window_t terra_xcb_window_create(struct XcbContext *xc, i16 x, i16 y, u16 width, u16 height, u8 override_redirect); 10 | void terra_xcb_window_change_cursor(struct XcbContext *xc, xcb_window_t x_window_id, char *cursor_str); 11 | void terra_xcb_window_set_geometry_request(struct XcbContext *xc, xcb_window_t x_window_id, i16 x, i16 y, u16 width, u16 height); 12 | void terra_xcb_window_set_sizes_request(struct XcbContext *xc, xcb_window_t x_window_id, u16 width, u16 height); 13 | void terra_xcb_window_set_coordinates_request(struct XcbContext *xc, xcb_window_t x_window_id, i16 x, i16 y); 14 | void terra_xcb_window_map_request(struct XcbContext *xc, xcb_window_t x_window_id); 15 | void terra_xcb_window_unmap(struct XcbContext *xc, xcb_window_t x_window_id); 16 | void terra_xcb_window_destroy(struct XcbContext *xc, xcb_window_t x_window_id); 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /c/src/xcb/xcb_ctx.c: -------------------------------------------------------------------------------- 1 | 2 | #include "context.h" 3 | 4 | struct XcbContext xcb_ctx; 5 | 6 | -------------------------------------------------------------------------------- /c/src/xcb/xcb_ctx.h: -------------------------------------------------------------------------------- 1 | #ifndef TERRA_XCB_XCB_CTX_H 2 | #define TERRA_XCB_XCB_CTX_H 3 | 4 | #include "xcb_ctx.h" 5 | 6 | extern struct XcbContext xcb_ctx; 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /c/src/xcb/xlhelp.c: -------------------------------------------------------------------------------- 1 | #ifndef XCB_XLHELP_H 2 | #define XCB_XLHELP_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "lhelp.h" 10 | #include "util.h" 11 | #include "sane.h" 12 | 13 | #include "xcb/xlhelp.h" 14 | 15 | struct XcbContext *xlhelp_check_xcb_ctx(lua_State *L, int ind) 16 | { 17 | struct XcbContext *xc = (struct XcbContext *)lua_touserdata(L, ind); 18 | if (xc == NULL) { 19 | printf("TERRA ERROR - supplied argument is not an 'XcbContext': %s.\n", lua_tostring(L, ind)); 20 | util_backtrace_print(); 21 | exit(1); 22 | } 23 | return xc; 24 | } 25 | 26 | void xlhelp_push_id(lua_State *L, u32 x_id) 27 | { 28 | lua_pushinteger(L, x_id); 29 | lua_tostring(L, -1); // convert the id to a lua string 30 | } 31 | 32 | u32 xlhelp_check_id(lua_State *L, i32 ind) 33 | { 34 | u32 id = lua_tointeger(L, ind); 35 | if (id == 0) { 36 | // TODO: dostring("debug.traceback") 37 | printf("ERROR - invalid X11 id: %s. (TODO: print a traceback)\n", lua_tostring(L, ind)); 38 | util_backtrace_print(); 39 | exit(1); 40 | } 41 | return id; 42 | } 43 | 44 | // int lhelp_store_event_handler(lua_State *L) 45 | // { 46 | // // the top function should be the event handler supplied to us by the user. 47 | // // we don't need it now, so put it in the registry. 48 | // lua_setfield(L, LUA_REGISTRYINDEX, TERRA_LUA_REGISTRY_EVENT_HANDLER_KEY); 49 | // 50 | // // by this point, we should only be left with one function on the stack: 51 | // // the model initializing function 52 | // 53 | // // push the on_runtime_error function at the beginning of the stack 54 | // lua_pushcfunction(L, lhelp_function_on_runtime_error); 55 | // lua_insert(L, 1); 56 | // 57 | // // finally, run the model initializing function, which should return 58 | // // to us with the model. 59 | // lua_pushlightuserdata(L, &app); 60 | // int runtime_error = lua_pcall(L, 1, 1, 1); 61 | // if (runtime_error != 0) return runtime_error; 62 | // 63 | // // then keep the model safe by storing it into the lua registry 64 | // lua_setfield(L, LUA_REGISTRYINDEX, TERRA_LUA_REGISTRY_MODEL_KEY); 65 | // 66 | // // remove `lhelp_function_on_runtime_error` function 67 | // lua_remove(L, 1); 68 | // return 0; 69 | // } 70 | 71 | // setup the lua equivalent of `event_handler(xcb_ctx, model, ` 72 | // the should be pushed by the user of this function. This is 73 | // usually done in the event handling portion of the C code 74 | void xlhelp_setup_event_handler(lua_State *L) 75 | { 76 | lua_pushcfunction(L, lhelp_function_on_runtime_error); 77 | lua_getfield(L, LUA_REGISTRYINDEX, TERRA_LUA_REGISTRY_EVENT_HANDLER_KEY); 78 | } 79 | 80 | // after calling `lhelp_setup_event_handler`, and pushing onto the stack the 81 | // desired event, you can call this function to have it automatically call 82 | // the event handler properly 83 | void xlhelp_call_event_handler(lua_State *L, uint nr_params) 84 | { 85 | int runtime_error = lua_pcall(L, nr_params, 0, 1); 86 | if (runtime_error != 0) { 87 | fprintf(stderr, "%s\n", lua_tostring(L, -1)); 88 | lua_pop(L, 1); // pop error message 89 | } 90 | lua_pop(L, 1); // pop the error function 91 | } 92 | 93 | #endif 94 | -------------------------------------------------------------------------------- /c/src/xcb/xlhelp.h: -------------------------------------------------------------------------------- 1 | #ifndef TERRA_XCB_XLHELP_H 2 | #define TERRA_XCB_XLHELP_H 3 | 4 | #include 5 | #include 6 | 7 | #include "sane.h" 8 | 9 | #define TERRA_LUA_REGISTRY_EVENT_HANDLER_KEY "terra_lua_event_handler" 10 | 11 | struct XcbContext *xlhelp_check_xcb_ctx(lua_State *L, int ind); 12 | u32 xlhelp_check_id(lua_State *L, i32 ind); 13 | void xlhelp_push_id(lua_State *L, u32 x_id); 14 | 15 | void xlhelp_setup_event_handler(lua_State *L); 16 | void xlhelp_call_event_handler(lua_State *L, uint nr_parameters); 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /c/src/xcb/xutil.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #ifdef DEBUG 8 | #include 9 | #endif 10 | #include // for `XStringToKeysym()` 11 | 12 | #include "xcb/context.h" 13 | #include "xcb/xcb_ctx.h" 14 | 15 | xcb_keycode_t xutil_string_to_keycode(const char *str) 16 | { 17 | xcb_keysym_t keysym = XStringToKeysym(str); 18 | // TODO: in "/usr/include/xcb/xcb_keysyms.h" it says that this function 19 | // can be slow. Maybe we can do it some other way? 20 | xcb_keycode_t *keycodes = xcb_key_symbols_get_keycode(xcb_ctx.keysyms, keysym); 21 | 22 | if(keycodes == NULL) return 0; 23 | 24 | // TODO: returning only the first is probably not ok 25 | xcb_keycode_t keycode = keycodes[0]; 26 | free(keycodes); // we are responsible for freeing the keycodes 27 | 28 | return keycode; 29 | } 30 | 31 | xcb_keycode_t *xutil_xcb_keysym_to_keycode(xcb_keysym_t keysym) { 32 | // FIXME: memory leak. this should be freed 33 | xcb_keycode_t *keycode = xcb_key_symbols_get_keycode(xcb_ctx.keysyms, keysym); 34 | return keycode; 35 | } 36 | 37 | xcb_keysym_t xutil_xcb_keycode_to_keysym(xcb_keycode_t keycode) { 38 | xcb_keysym_t keysym = xcb_key_symbols_get_keysym(xcb_ctx.keysyms, keycode, 0); 39 | return keysym; 40 | } 41 | 42 | void xutil_xerror_handle(xcb_generic_error_t *error) 43 | { 44 | #ifdef DEBUG 45 | // ignore this 46 | // if(e->error_code == XCB_WINDOW 47 | // || (e->error_code == XCB_MATCH 48 | // && e->major_code == XCB_SET_INPUT_FOCUS) 49 | // || (e->error_code == XCB_VALUE 50 | // && e->major_code == XCB_KILL_CLIENT) 51 | // || (e->error_code == XCB_MATCH 52 | // && e->major_code == XCB_CONFIGURE_WINDOW)) 53 | // return; 54 | 55 | const char *major = xcb_errors_get_name_for_major_code( 56 | xcb_ctx.xcb_error_ctx, 57 | error->major_code 58 | ); 59 | const char *minor = xcb_errors_get_name_for_minor_code( 60 | xcb_ctx.xcb_error_ctx, 61 | error->major_code, 62 | error->minor_code 63 | ); 64 | const char *extension = NULL; 65 | const char *error_str = xcb_errors_get_name_for_error( 66 | xcb_ctx.xcb_error_ctx, 67 | error->error_code, 68 | &extension 69 | ); 70 | 71 | fprintf(stderr, "X error: request - %s%s%s (major %d, minor %d); \terror - (%d) %s%s%s; \tresource_id - (%d)\n", 72 | major, 73 | minor == NULL ? "" : "-", 74 | minor == NULL ? "" : minor, 75 | error->major_code, 76 | error->minor_code, 77 | error->error_code, 78 | extension == NULL ? "" : extension, 79 | extension == NULL ? "" : "-", 80 | error_str, 81 | error->resource_id 82 | ); 83 | #else 84 | fprintf(stderr, "XCB ERROR:\n"); 85 | fprintf(stderr, "\terror_code: %d\n", error->error_code); 86 | fprintf(stderr, "\tsequence: %d\n", error->sequence); 87 | fprintf(stderr, "\tresource_id: %d\n", error->resource_id); 88 | fprintf(stderr, "\tminor_code: %d\n", error->minor_code); 89 | fprintf(stderr, "\tmajor_code: %d\n", error->major_code); 90 | #endif 91 | } 92 | 93 | -------------------------------------------------------------------------------- /c/src/xcb/xutil.h: -------------------------------------------------------------------------------- 1 | #ifndef TERRA_XCB_XUTIL_H 2 | #define TERRA_XCB_XUTIL_H 3 | 4 | #include 5 | 6 | xcb_keycode_t xutil_string_to_keycode(const char *str); 7 | xcb_keycode_t *xutil_xcb_keysym_to_keycode(xcb_keysym_t keysym); 8 | xcb_keysym_t xutil_xcb_keycode_to_keysym(xcb_keycode_t keycode); 9 | 10 | void xutil_xerror_handle(xcb_generic_error_t *error); 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /l/_retired.lua: -------------------------------------------------------------------------------- 1 | 2 | -- TODO: make it so that the user has the option to either handle platform specific events directly or abstract them away automatically 3 | -- LEFTOFF: make signals work on windows properly 4 | 5 | local te_xcb = require("terra.events") 6 | 7 | local events = te_xcb.events 8 | 9 | -- -- TODO: this should automatically clean up after itself 10 | -- local function destroy(window) 11 | -- 12 | -- twindow.destroy(window) 13 | -- tsoil.destroy(window.soil) 14 | -- 15 | -- if window.branch == nil then return end 16 | -- 17 | -- -- TODO: maybe the user should manually detach the branch? 18 | -- ou_internal.element_recursively_detach(window.branch) 19 | -- 20 | -- _root_unsubscribe_functions(root, root.model) 21 | -- 22 | -- end 23 | 24 | local function _handle_configure_notify_event(window, event_type, window_id, x, y, width, height) 25 | 26 | local size_changed = false 27 | 28 | if window.width ~= event.width or window.height ~= event.height then 29 | size_changed = true 30 | end 31 | 32 | window.x = event.x 33 | window.y = event.y 34 | window.width = event.width 35 | window.height = event.height 36 | 37 | local tree = window.tree 38 | if tree == nil then return end 39 | 40 | if size_changed then 41 | tstation.emit(tree.station, tree, t_i_e_tree.ParentWindowSizeChanged, width, height) 42 | end 43 | end 44 | 45 | local function _handle_click_event(window, event_type, window_id, is_press, button, modifiers, x, y) 46 | 47 | local tree = window.tree 48 | if tree == nil then return end 49 | if x > tree.width or y > tree.height then return end 50 | 51 | tstation.emit(tree.station, tree, t_i_e_tree.MouseClickEvent, is_press, button, modifiers, x, y) 52 | end 53 | 54 | local function _handle_create_event(app, ...) 55 | -- this can only happen if someone creates a window with us as the parent. 56 | -- this is currently not supported, so we do nothing. 57 | end 58 | 59 | local function _handle_destroy_event(window, event_type, parent_id, window_id) 60 | 61 | _window_drawing_context_destroy(window) 62 | 63 | local tree = window.tree 64 | if tree == nil then return end 65 | 66 | -- TODO: also let each child element know about this 67 | 68 | tstation.emit(window.station, window, event_type, window_id) 69 | end 70 | 71 | local function _handle_enter_event(app, event_type, window_id, ...) 72 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 73 | tstation.emit(window.station, window, event_type, window_id, ...) 74 | end 75 | 76 | local function _handle_expose_event(app, event_type, window_id, ...) 77 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 78 | tstation.emit(window.station, window, event_type, window_id, ...) 79 | end 80 | 81 | local function _handle_focus_in_event(app, event_type, window_id) 82 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 83 | -- window.focused = true -- TODO: move this out of here 84 | tstation.emit(window.station, window, event_type, window_id) 85 | end 86 | 87 | local function _handle_focus_out_event(app, event_type, window_id) 88 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 89 | -- window.focused = false -- TODO: move this out of here 90 | tstation.emit(window.station, window, event_type, window_id) 91 | end 92 | 93 | local function _handle_key_event(app, event_type, window_id, ...) 94 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 95 | -- TODO: set keybindings on windows directly 96 | 97 | tstation.emit(window.station, window, event_type, window_id, ...) 98 | end 99 | 100 | local function _handle_leave_event(app, event_type, window_id, ...) 101 | 102 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 103 | 104 | tstation.emit(window.station, window, event_type, window_id, ...) 105 | end 106 | 107 | local function _handle_motion_event(app, event_type, window_id, ...) 108 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 109 | tstation.emit(window.station, window, event_type, window_id, ...) 110 | 111 | -- TODO: move this code where it belongs 112 | -- local hit_children = ou_internal.get_approved_mouse_hit_children( 113 | -- window, 114 | -- event_type, 115 | -- event.x, 116 | -- event.y 117 | -- ) 118 | -- 119 | -- for _, child in ipairs(hit_children) do 120 | -- local geom = child.oak_geometry 121 | -- tstation.emit(child.station, { 122 | -- type = event_type, 123 | -- -- translate the coordinates so the child gets relative coordinates 124 | -- x = event.x - geom.x, 125 | -- y = event.y - geom.y, 126 | -- }) 127 | -- end 128 | end 129 | 130 | local function _handle_map_event(app, event_type, window_id, ...) 131 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 132 | -- TODO: move this out of here 133 | -- window.visibility = tw_internal.visibility.RAISED 134 | 135 | tstation.emit(window.station, window, event_type, window_id, ...) 136 | end 137 | 138 | local function _handle_map_request(app, event_type, parent_id, window_id) 139 | -- this could only happen if we had child windows and one of them 140 | -- would request to be mapped. This is currently not supported. 141 | end 142 | 143 | local function _handle_property_event(app, event_type, window_id, ...) 144 | -- TODO: implement this properly 145 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 146 | tstation.emit(window.station, window, event_type, window_id, ...) 147 | end 148 | 149 | local function _handle_reparent_event(app, event) 150 | -- this happens when a window is reparented to us, or when our window 151 | -- is reparented onto another. I'm not sure what to do about this yet, 152 | -- so let's just mark everything for relayout and redraw. 153 | -- ou_internal.element_mark_relayout(window) 154 | -- ou_internal.element_mark_redraw(window) 155 | end 156 | 157 | local function _handle_visibility_event(app, event_type, window_id, visibility) 158 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 159 | 160 | -- TODO: move this out of here 161 | -- window.visibility = tw_internal.visibility.RAISED_AND_SHOWING 162 | 163 | tstation.emit(window.station, window, event_type, window_id, visibility) 164 | end 165 | 166 | local function _handle_unmap_event(app, event_type, window_id) 167 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 168 | 169 | -- TODO: move this out of here 170 | -- window.visibility = tw_internal.visibility.HIDDEN 171 | 172 | tstation.emit(window.station, window, event_type, window_id) 173 | end 174 | 175 | 176 | 177 | 178 | local default_event_handler_map = { 179 | 180 | -- platform specific events 181 | [events.X_ClickEvent] = _handle_click_event, 182 | [events.X_ConfigureNotify] = _handle_configure_notify_event, 183 | [events.X_CreateNotify] = _handle_create_event, 184 | [events.X_DestroyNotify] = _handle_destroy_event, 185 | [events.X_EnterNotify] = _handle_enter_event, 186 | [events.X_ExposeEvent] = _handle_expose_event, 187 | [events.X_FocusIn] = _handle_focus_in_event, 188 | [events.X_FocusOut] = _handle_focus_out_event, 189 | [events.X_KeyEvent] = _handle_key_event, 190 | [events.X_LeaveNotify] = _handle_leave_event, 191 | [events.X_MotionEvent] = _handle_motion_event, 192 | [events.X_MapNotify] = _handle_map_event, 193 | [events.X_MapRequest] = _handle_map_request, 194 | [events.X_PropertyNotify] = _handle_property_event, 195 | [events.X_ReparentNotify] = _handle_reparent_event, 196 | [events.X_VisibilityNotify] = _handle_visibility_event, 197 | [events.X_UnmapNotify] = _handle_unmap_event, 198 | } 199 | 200 | 201 | return { 202 | events = events, 203 | default_event_handler_map = default_event_handler_map, 204 | } 205 | -------------------------------------------------------------------------------- /l/abonaments/tcp/client.lua: -------------------------------------------------------------------------------- 1 | 2 | local luv = require("luv") 3 | local tstation = require("tstation") 4 | 5 | local tt_promise = require("terra.tools.promise") 6 | local t_puv = require("terra.puv") 7 | 8 | local function _make_id(host, port) 9 | return tostring(host) .. '/' .. tostring(port) 10 | end 11 | 12 | local function _ensure_connection(app, host, port) 13 | 14 | local id = _make_id(host, port) 15 | local conn = app._tcp_conns[id] 16 | if conn == nil then 17 | return t_puv.tcp_new() 18 | :next(function(tcp) 19 | app._tcp_conns[id] = tcp 20 | return t_puv.tcp_connect(tcp, host, port) 21 | end) 22 | else 23 | local p = tt_promise.new() 24 | p:resolve(conn) 25 | return p 26 | end 27 | end 28 | 29 | local function listen_by_line(app, host, port, sig_succ, sig_fail) 30 | _ensure_connection(app, host, port) 31 | :next(function(tcp) 32 | luv.read_start(tcp, function(err, data) 33 | if err ~= nil then 34 | return tstation.emit(app.station, sig_fail, err) 35 | else 36 | return tstation.emit(app.station, sig_succ, data) 37 | end 38 | end) 39 | end) 40 | end 41 | 42 | local function stop_listening(app, host, port) 43 | end 44 | 45 | local function write(app, host, port, data) 46 | _ensure_connection(app, host, port) 47 | :next(function(tcp) 48 | return t_puv.tcp_write(tcp, data) 49 | end) 50 | end 51 | 52 | return { 53 | listen_by_line = listen_by_line, 54 | stop_listening = stop_listening, 55 | write = write, 56 | } 57 | 58 | -------------------------------------------------------------------------------- /l/element.lua: -------------------------------------------------------------------------------- 1 | 2 | local t_object = require("terra.object") 3 | local tt_table = require("terra.tools.table") 4 | -- local tstation = require("tstation") 5 | 6 | local events = { 7 | MouseEnterEvent = "MouseEnterEvent", 8 | MouseLeaveEvent = "MouseLeaveEvent", 9 | MouseMotionEvent = "MouseMotionEvent", 10 | MouseClickEvent = "MouseClickEvent", 11 | } 12 | 13 | local function default_get_geometry(element) 14 | return element.geometry 15 | end 16 | 17 | local function default_set_geometry(element, x, y, width, height) 18 | element.geometry.x = x 19 | element.geometry.y = y 20 | element.geometry.width = width 21 | element.geometry.height = height 22 | end 23 | 24 | -- returns true if the given point exists inside the geometry of the given element. 25 | local function contains_point(element, point_x, point_y) 26 | local geom = element.geometry 27 | if point_x < geom.x then return false end 28 | if point_x > geom.x + geom.width then return false end 29 | if point_y < geom.y then return false end 30 | if point_y > geom.y + geom.height then return false end 31 | return true 32 | end 33 | 34 | -- geometries can have floating point values. clip areas shouldn't. Given the 35 | -- geometry of an element, this function returns a table with x, y, width, height 36 | -- as integers 37 | local function geometry_to_clip_area(geom) -- TODO: maybe rename this to "get_bounding_box" 38 | 39 | -- note: when clipping, these values should always be integers, in 40 | -- order to have the rectangle be on pixel aligned coordinates. We do 41 | -- this because the cairo docs suggest this would be fastest. 42 | -- https://www.cairographics.org/FAQ/#clipping_performance 43 | local element_x_floor, element_x_fractional_part = math.modf( 44 | geom.x 45 | ) 46 | local element_y_floor, element_y_fractional_part = math.modf( 47 | geom.y 48 | ) 49 | 50 | return { -- TODO: dont return a table here 51 | x = element_x_floor, 52 | y = element_y_floor, 53 | width = math.ceil(geom.width + element_x_fractional_part), 54 | height = math.ceil(geom.height + element_y_fractional_part) 55 | } 56 | end 57 | 58 | local function new() 59 | 60 | local element_defaults = { 61 | geometry = {}, -- TODO: maybe make this `nil` at first until an element is attached 62 | get_geometry = default_get_geometry, 63 | set_geometry = default_set_geometry, 64 | } 65 | 66 | local element = tt_table.crush(t_object.new(), element_defaults) 67 | 68 | return element 69 | end 70 | 71 | 72 | return { 73 | new = new, 74 | 75 | events = events, 76 | 77 | default_get_geometry = default_get_geometry, 78 | default_set_geometry = default_set_geometry, 79 | 80 | contains_point = contains_point, 81 | 82 | geometry_to_clip_area = geometry_to_clip_area, 83 | } 84 | -------------------------------------------------------------------------------- /l/input/click.lua: -------------------------------------------------------------------------------- 1 | 2 | local lib = { 3 | ANY = 0, 4 | LEFT = 1, 5 | RIGHT = 2, 6 | MIDDLE = 3, 7 | SCROLL_UP = 4, 8 | SCROLL_DOWN = 5, 9 | } 10 | 11 | return lib 12 | 13 | -------------------------------------------------------------------------------- /l/input/clickbind.lua: -------------------------------------------------------------------------------- 1 | 2 | -- `on_press` or `on_release` can be nil, but not both 3 | local function new(button, modifiers, on_press, on_release) 4 | 5 | if on_press == nil and on_release == nil then 6 | error("cannot create a clickbind where both `on_press` and `on_release` are nil.") 7 | end 8 | 9 | return { 10 | button = button, 11 | modifiers = modifiers, 12 | on_press = on_press, 13 | on_release = on_release, 14 | } 15 | end 16 | 17 | return { 18 | new = new 19 | } 20 | -------------------------------------------------------------------------------- /l/input/clickmap.lua: -------------------------------------------------------------------------------- 1 | 2 | local i_click = require("input.click") 3 | local i_key = require("input.key") 4 | 5 | -- Glossary: 6 | -- * click_id: 7 | -- - a table used to identify a specific modifier + click combination 8 | -- - also called a "cid" 9 | -- For example: { 10 | -- "button" : , (the mouse button number. e.g. 1 for left click, 2 for right click, etc.) 11 | -- "modifiers" : (a 16 bit bitmap where each bit represents one modifier. Except for the special "MOD_ANY" modifier) 12 | -- "is_press : 13 | -- } 14 | -- * clickbind: 15 | -- - just like a `click_id` but with a "callback" field to denote the 16 | -- callback to be called when the click combination matches 17 | -- * clickbindings: 18 | -- _ a contiguous array of `clickbind` values 19 | -- * clickmap: 20 | -- - a table of the form { : { : } }. 21 | -- In the first table the index is a modifier mask turned 22 | -- into string form. In the second, nested table, the key is an 23 | -- to denote the "button". The is the callback 24 | -- that exists at the given modifier + button combination. 25 | -- - normally used to index into with a `click_id`, in order to 26 | -- execute a clickbinding 27 | 28 | 29 | -- create a "clickmap" from "clickbindings" 30 | local function from_clickbindings(clickbindings) 31 | local clickmap = {} 32 | for _, clickbind in ipairs(clickbindings) do 33 | 34 | local str_mods = tostring(clickbind.modifiers) 35 | local buttons = clickmap[str_mods] 36 | if buttons == nil then 37 | buttons = {} 38 | clickmap[str_mods] = buttons 39 | end 40 | 41 | buttons[clickbind.button] = { 42 | [true] = clickbind.on_press, 43 | [false] = clickbind.on_release, 44 | } 45 | end 46 | return clickmap 47 | end 48 | 49 | -- indexes the given "clickmap" with the given "click_id", to give back a 50 | -- callback, if there is one 51 | local function index(clickmap, cid) 52 | local buttons = clickmap[tostring(cid.modifiers)] 53 | if buttons == nil then 54 | -- buttons are nil, so try with "any modifiers" 55 | buttons = clickmap[tostring(i_key.MOD_ANY)] 56 | end 57 | if buttons == nil then return end 58 | local press_or_release = buttons[cid.button] 59 | if press_or_release == nil then 60 | press_or_release = buttons[i_click.ANY] 61 | end 62 | if press_or_release == nil then return end 63 | local cb = press_or_release[cid.is_press] 64 | return cb 65 | end 66 | 67 | return { 68 | from_clickbindings = from_clickbindings, 69 | index = index, 70 | } 71 | 72 | -------------------------------------------------------------------------------- /l/input/key.lua: -------------------------------------------------------------------------------- 1 | 2 | local lib = { 3 | ANY = 0, -- literal keycode to signify "any key" 4 | 5 | MOD_NONE = 0, 6 | MOD_SHIFT = 1, -- 1 << 0 7 | MOD_LOCK = 2, -- 1 << 1 8 | MOD_CONTROL = 4, -- 1 << 2 9 | MOD_1 = 8, -- 1 << 3 10 | MOD_2 = 16, -- 1 << 4 11 | MOD_3 = 32, -- 1 << 5 12 | MOD_4 = 64, -- 1 << 6 13 | MOD_5 = 128, -- 1 << 7 14 | MOD_ANY = 32768 -- 1 << 15 15 | } 16 | 17 | return lib 18 | 19 | -------------------------------------------------------------------------------- /l/input/keybind.lua: -------------------------------------------------------------------------------- 1 | 2 | -- on_press or on_release can be nil, but not both 3 | local function new(keyname, modifiers, on_press, on_release) 4 | 5 | if on_press == nil and on_release == nil then 6 | error("cannot create a keybind without either `on_press` or `on_release`") 7 | end 8 | 9 | return { 10 | key = keyname, 11 | modifiers = modifiers, 12 | on_press = on_press, 13 | on_release = on_release, 14 | } 15 | end 16 | 17 | return { 18 | new = new 19 | } 20 | 21 | -------------------------------------------------------------------------------- /l/input/keymap.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Glossary: 3 | -- * key_id: 4 | -- - a table used to identify a specific modifier + key combination 5 | -- - also called a "kid" 6 | -- For example: { 7 | -- "key" : , 8 | -- "modifiers" : (a 16 bit bitmap where each bit represents one modifier. Except for the special "MOD_ANY" modifier) 9 | -- "is_press" : 10 | -- } 11 | -- * keybind: 12 | -- - just like a `key_id` but with a "callback" field to denote the 13 | -- callback to be called when the key combination matches 14 | -- * keybindings: 15 | -- - a contiguous array of `keybind` values 16 | -- * keymap: 17 | -- - a table of the form { : { | : { : } } }. 18 | -- In the first table the index is a modifier mask turned 19 | -- into string form. In the second, nested table, the key is a 20 | -- to denote the "key" name, or, it can be an integer 21 | -- index which is treated as a literal keycode. The third nested 22 | -- table is the table that uses a boolean to determine whether 23 | -- the callback is for a key press event (true), or for a key 24 | -- release event (false). The is the callback that 25 | -- exists at the given modifier + key + is_press combination. 26 | 27 | -- create a "keymap" from "keybindings". 28 | local function from_keybindings(keybindings) 29 | local keymap = {} 30 | for _, keybind in ipairs(keybindings) do 31 | local str_mods = tostring(keybind.modifiers) 32 | local keys = keymap[str_mods] 33 | if keys == nil then 34 | keys = {} 35 | keymap[str_mods] = keys 36 | end 37 | keys[keybind.key] = { 38 | [true] = keybind.on_press, 39 | [false] = keybind.on_release, 40 | } 41 | end 42 | return keymap 43 | end 44 | 45 | -- indexes the given "keymap" with the given "key_id", to give back a 46 | -- callback, if there is one 47 | local function index(keymap, kid) 48 | local keys = keymap[tostring(kid.modifiers)] 49 | if keys == nil then return end 50 | local key = kid.key 51 | 52 | -- TODO: I don't think this applies anymore 53 | -- FIXME: the "`" or "grave" key doesn't work properly. When using 54 | -- `subscribe_key` to subscribe to events of this key, using "grave" 55 | -- as the `.key` name works to subscribe, but when we get an event 56 | -- from the C side, instead of giving us "grave" back as the `.key`, 57 | -- it gives us '`'. This fixes the issue, but I'd like to fix it 58 | -- properly from the C side. 59 | if key == '`' then key = "grave" end 60 | local press_or_release = keys[key] 61 | local cb = press_or_release[kid.is_press] 62 | return cb 63 | end 64 | 65 | return { 66 | from_keybindings = from_keybindings, 67 | index = index, 68 | } 69 | -------------------------------------------------------------------------------- /l/oak/align.lua: -------------------------------------------------------------------------------- 1 | 2 | -- TODO: maybe replace these with "ALIGN_START", "ALIGN_CENTER", "ALIGN_END" 3 | local ALIGN_CENTER = 1 4 | local ALIGN_LEFT = 2 5 | local ALIGN_RIGHT = 3 6 | local ALIGN_TOP = 4 7 | local ALIGN_BOTTOM = 5 8 | 9 | local lib = { 10 | CENTER = ALIGN_CENTER, 11 | LEFT = ALIGN_LEFT, 12 | RIGHT = ALIGN_RIGHT, 13 | TOP = ALIGN_TOP, 14 | BOTTOM = ALIGN_BOTTOM, 15 | } 16 | 17 | return lib 18 | 19 | -------------------------------------------------------------------------------- /l/oak/border.lua: -------------------------------------------------------------------------------- 1 | 2 | local _BORDER_EACH = 1 3 | 4 | -- TODO: I think it'd be better to make the border an element like any other 5 | local function get_border_width(element) 6 | local bg = element.bg 7 | if element.bg == nil then return 0 end 8 | if bg.border_width == nil then return 0 end 9 | return bg.border_width 10 | end 11 | 12 | local function get_border_radius(element) 13 | local bg = element.bg 14 | if element.bg == nil then return 0 end 15 | return bg.border_radius or 0 16 | end 17 | 18 | local function radius_each(args) 19 | 20 | args.top_left = args.top_left or 0 21 | args.top_right = args.top_right or 0 22 | args.bottom_right = args.bottom_right or 0 23 | args.bottom_left = args.bottom_left or 0 24 | args.border_type = _BORDER_EACH 25 | 26 | assert(type(args.top_left) == "number", "'.top_left' should be number, got: " .. tostring(top_left)) 27 | assert(type(args.top_right) == "number", "'.top_right' should be number, got: " .. tostring(top_right)) 28 | assert(type(args.bottom_right) == "number", "'.bottom_right' should be number, got: " .. tostring(bottom_right)) 29 | assert(type(args.bottom_left) == "number", "'.bottom_left' should be number, got: " .. tostring(bottom_left)) 30 | 31 | return args 32 | end 33 | 34 | -- TODO: just return numbers here instead of a table 35 | local function standardize_radius(border_radius) 36 | 37 | if border_radius == nil then 38 | return { 39 | top_left = 0, 40 | top_right = 0, 41 | bottom_right = 0, 42 | bottom_left = 0, 43 | } 44 | elseif type(border_radius) == "number" then 45 | return { 46 | top_left = border_radius, 47 | top_right = border_radius, 48 | bottom_right = border_radius, 49 | bottom_left = border_radius, 50 | } 51 | else -- border_each 52 | return { 53 | top_left = border_radius.top_left, 54 | top_right = border_radius.top_right, 55 | bottom_right = border_radius.bottom_right, 56 | bottom_left = border_radius.bottom_left, 57 | } 58 | end 59 | end 60 | 61 | return { 62 | get_width = get_border_width, 63 | get_radius = get_border_radius, 64 | 65 | radius_each = radius_each, 66 | standardize_radius = standardize_radius, 67 | } 68 | 69 | -------------------------------------------------------------------------------- /l/oak/elements/_retired.lua: -------------------------------------------------------------------------------- 1 | 2 | local function needs_relayout(element) 3 | return element._oak_private.needs_relayout 4 | end 5 | 6 | local function needs_redraw(element) 7 | return element._oak_private.needs_redraw 8 | end 9 | 10 | local function mark_relayout(element) 11 | 12 | -- if this element is not attached to a root, don't mark it because all 13 | -- freshly attached elements automatically get relayouted and redrawn. 14 | local root = element.scope.root 15 | if root == nil then return end 16 | 17 | -- is the element already marked? 18 | if element._oak_private.needs_relayout then return end 19 | 20 | root.nr_of_elements_that_need_relayout = root.nr_of_elements_that_need_relayout + 1 21 | 22 | element._oak_private.needs_relayout = true 23 | end 24 | 25 | local function mark_redraw(element) 26 | 27 | -- if this element is not attached to a root, don't mark it because all 28 | -- freshly attached elements automatically get relayouted and redrawn. 29 | local root = element.scope.root 30 | if root == nil then return end 31 | 32 | -- is the element already marked? 33 | if element._oak_private.needs_redraw then return end 34 | 35 | root.nr_of_elements_that_need_redraw = root.nr_of_elements_that_need_redraw + 1 36 | 37 | element._oak_private.needs_redraw = true 38 | end 39 | 40 | local function mark_dont_relayout(element) 41 | 42 | local root = element.scope.root 43 | if root == nil then return end 44 | 45 | if element._oak_private.needs_relayout == false then return end 46 | 47 | root.nr_of_elements_that_need_relayout = root.nr_of_elements_that_need_relayout - 1 48 | 49 | element._oak_private.needs_relayout = false 50 | end 51 | 52 | local function mark_dont_redraw(element) 53 | 54 | local root = element.scope.root 55 | if root == nil then return end 56 | 57 | if element._oak_private.needs_redraw == false then return end 58 | 59 | root.nr_of_elements_that_need_redraw = root.nr_of_elements_that_need_redraw - 1 60 | 61 | element._oak_private.needs_redraw = false 62 | end 63 | 64 | -- local function element_mark_redraw_all_subchildren(element) 65 | -- element_mark_redraw(element) 66 | -- if element.oak_children_iter == nil then return end -- not a branch 67 | -- for _, c in element:oak_children_iter() do 68 | -- element_mark_redraw_all_subchildren(c) 69 | -- end 70 | -- end 71 | 72 | -- -- returns a list of the fewest elements that need a relayout 73 | -- -- for example: if 'b' is the child of 'a', and they're both marked as requiring 74 | -- -- a relayout, only { a } will be returned because 'b' will automatically be 75 | -- -- relayouted when 'a' will be, since all children of a relayoued element are 76 | -- -- automatically relayouted 77 | -- local function get_least_elements_to_relayout(element) 78 | -- 79 | -- local function dig(storage, current_element) 80 | -- 81 | -- if current_element.oak_children_iter == nil then return end -- not a branch 82 | -- for _, c in current_element:oak_children_iter() do 83 | -- 84 | -- if element_needs_relayout(c) then 85 | -- table.insert(storage, c) 86 | -- else 87 | -- dig(storage, c) 88 | -- end 89 | -- 90 | -- end 91 | -- 92 | -- end 93 | -- 94 | -- local elements_needing_relayout = {} 95 | -- dig(elements_needing_relayout, element) 96 | -- return elements_needing_relayout 97 | -- end 98 | -- 99 | -- -- traverses the whole tree and returns a list of all elements that need to 100 | -- -- be redrawn 101 | -- local function get_all_elements_to_redraw(element) 102 | -- 103 | -- local function dig(storage, current_element) 104 | -- 105 | -- if current_element.oak_children_iter == nil then return end -- not a branch 106 | -- for _, c in current_element:oak_children_iter() do 107 | -- if element_needs_redraw(c) then 108 | -- table.insert(storage, c) 109 | -- dig(storage, c) 110 | -- end 111 | -- end 112 | -- end 113 | -- 114 | -- local elements_needing_redraw = {} 115 | -- dig(elements_needing_redraw, element) 116 | -- return elements_needing_redraw 117 | -- end 118 | 119 | -- TODO: TEST 120 | -- TODO: try to turn this an iterator and not create tables anymore. 121 | local function element_get_children_to_relayout_and_redraw(element) 122 | 123 | local function dig( 124 | current, 125 | least_relayout_storage, -- the "least" elements that need to be relayouted 126 | all_relayout_storage, -- all elements that were marked to be relayouted 127 | redraw_storage, -- all elements that need to be redrawn 128 | should_still_check_for_relayout, 129 | should_redraw_from_here_on 130 | ) 131 | if element_needs_relayout(current) then 132 | should_redraw_from_here_on = true 133 | if should_still_check_for_relayout then 134 | table.insert(least_relayout_storage, current) 135 | should_still_check_for_relayout = false 136 | end 137 | table.insert(all_relayout_storage, current) 138 | end 139 | 140 | if should_redraw_from_here_on then 141 | table.insert(redraw_storage, current) 142 | else 143 | if element_needs_redraw(current) then 144 | table.insert(redraw_storage, current) 145 | end 146 | end 147 | 148 | if current.oak_children_iter == nil then return end 149 | for _, c in current:oak_children_iter() do 150 | dig( 151 | c, 152 | least_relayout_storage, 153 | all_relayout_storage, 154 | redraw_storage, 155 | should_still_check_for_relayout, 156 | should_redraw_from_here_on 157 | ) 158 | end 159 | end 160 | 161 | local least_elements_to_relayout = {} 162 | local all_elements_to_relayout = {} 163 | local elements_to_redraw = {} 164 | dig( 165 | element, 166 | least_elements_to_relayout, 167 | all_elements_to_relayout, 168 | elements_to_redraw, 169 | true, 170 | false 171 | ) 172 | 173 | return least_elements_to_relayout, all_elements_to_relayout, elements_to_redraw 174 | end 175 | 176 | -- TODO: either make this work, or add something like a "shape" 177 | -- property to all elements. 178 | local function apply_clip_shape(branch, cr, width, height) 179 | 180 | local clip_shape = branch.clip_shape 181 | if clip_shape ~= nil then 182 | clip_shape(branch, cr, width, height) 183 | return 184 | end 185 | 186 | local bg = branch.bg 187 | if bg == nil then 188 | return 189 | end 190 | 191 | local border_radius = bg.border_radius or 0 192 | if border_radius == 0 then 193 | return 194 | end 195 | 196 | if branch.clip_to_background == true then 197 | 198 | local border_width = bg.border_width or 0 199 | cr:translate(border_width, border_width) 200 | to_shape.rounded_rectangle( 201 | cr, 202 | width - (border_width * 2), 203 | height - (border_width * 2), 204 | border_radius 205 | ) 206 | 207 | end 208 | 209 | end 210 | 211 | -------------------------------------------------------------------------------- /l/oak/elements/branches/_retired.lua: -------------------------------------------------------------------------------- 1 | 2 | -- NOTE: this is a very outdated drawing function that was optimized at the time. 3 | -- local function draw(root, cr) 4 | -- 5 | -- local something_was_drawn = true -- TODO: use this properly 6 | -- 7 | -- -- if no element needs to be relayouted or redrawn, just return 8 | -- if root.nr_of_elements_that_need_relayout == 0 9 | -- and root.nr_of_elements_that_need_redraw == 0 10 | -- then return false end 11 | -- 12 | -- -- TODO: make sure not to return tables here. Just store them in the root. 13 | -- -- This should massively optimize everything since these tables would 14 | -- -- not need to be gc'd. 15 | -- local 16 | -- least_elements_to_relayout, 17 | -- all_elements_to_relayout, 18 | -- elements_to_redraw 19 | -- = toe_internal.element_get_children_to_relayout_and_redraw(root) 20 | -- 21 | -- -- TODO: rewrite the draw function to make it start out with a "nil" 22 | -- -- clip region, that gets progressively larger as the regions get marked 23 | -- -- for redraw. 24 | -- -- ALSO: write a "draw_debug" function 25 | -- -- local something_needs_to_be_drawn = elements_to_redraw[1] ~= nil 26 | -- local dirty_region = lgi.cairo.Region.create() -- TODO: replace lgi 27 | -- 28 | -- -- mark all regions of the redraw elements as dirty BEFORE relayouting 29 | -- 30 | -- print('redraw elements that need clipping before relayout:') 31 | -- for _, element in ipairs(elements_to_redraw) do 32 | -- -- TODO: check for oak_geometry == nil 33 | -- print("element to redraw", element) 34 | -- local elem_geom = element:get_geometry() 35 | -- if elem_geom.x ~= nil then 36 | -- dirty_region:union_rectangle( 37 | -- lgi.cairo.RectangleInt( 38 | -- toe_internal.geometry_to_clip_area(element.geometry) 39 | -- ) 40 | -- ) 41 | -- end 42 | -- end 43 | -- 44 | -- -- relayout all elements that it makes sense to relayout 45 | -- for _, element in ipairs(least_elements_to_relayout) do 46 | -- print("element to relayout", element) 47 | -- local geom = element:get_geometry() 48 | -- toe_internal.element_recursively_process( 49 | -- element, 50 | -- geom.x, 51 | -- geom.y, 52 | -- geom.width, 53 | -- geom.height 54 | -- ) 55 | -- end 56 | -- 57 | -- -- was something relayouted? 58 | -- if #least_elements_to_relayout > 0 then 59 | -- -- if something was relayouted, mark all regions of the redraw 60 | -- -- elements as dirty AFTER RELAYOUTING AGAIN, because their geometry 61 | -- -- might have changed 62 | -- 63 | -- print('redraw elements that need clipping after relayout:') 64 | -- for _, element in ipairs(elements_to_redraw) do 65 | -- dirty_region:union_rectangle( 66 | -- lgi.cairo.RectangleInt( 67 | -- toe_internal.geometry_to_clip_area(element.geometry) 68 | -- ) 69 | -- ) 70 | -- end 71 | -- end 72 | -- 73 | -- -- reset the clip so we can draw anywhere on the window 74 | -- cr:reset_clip() 75 | -- 76 | -- -- clip the whole region marked 77 | -- for i=0, dirty_region:num_rectangles() - 1 do 78 | -- local rect = dirty_region:get_rectangle(i) 79 | -- print('CLIPPING THE FOLLOWING RECTS:') 80 | -- print("rect: ", rect.x, rect.y, rect.width, rect.height) 81 | -- cr:rectangle(rect.x, rect.y, rect.width, rect.height) 82 | -- end 83 | -- cr:clip() 84 | -- 85 | -- -- draw the background first, so we don't get "solitaire trails" from 86 | -- -- previous drawings if the background is transparent 87 | -- cr:save() -- save because we don't want to use this operator for everything. 88 | -- -- We use the CLEAR operator to make sure that all previous data 89 | -- -- that used to exist in memory where our surface now exists gets cleared. 90 | -- -- Otherwise we can get random artifacts and trash in our drawing. 91 | -- -- This will also automatically draw transparency if a compositor 92 | -- -- is running. 93 | -- cr:set_operator(lgi.cairo.Operator.CLEAR) 94 | -- cr:paint() 95 | -- cr:restore() 96 | -- 97 | -- -- draw the whole tree onto the pixmap. We clipped to the dirty region 98 | -- -- earlier, which should ensure that only that portion is redrawn. 99 | -- toe_internal.element_recursively_draw_on_context(root, cr) 100 | -- 101 | -- -- cr:rectangle(100, 100, 40, 40) 102 | -- -- cr:set_source_rgb(0.1, 0.9, 0.1) 103 | -- -- cr:fill() 104 | -- 105 | -- -- reset the marked changes so that changes dont persist across frames 106 | -- for _, element in ipairs(all_elements_to_relayout) do 107 | -- element._oak_private.needs_relayout = false 108 | -- end 109 | -- for _, element in ipairs(elements_to_redraw) do 110 | -- element._oak_private.needs_redraw = false 111 | -- end 112 | -- root.nr_of_elements_that_need_relayout = 0 113 | -- root.nr_of_elements_that_need_redraw = 0 114 | -- 115 | -- print("DRAWING DONE IN OAK ROOT") 116 | -- 117 | -- return something_was_drawn 118 | -- end 119 | 120 | 121 | -------------------------------------------------------------------------------- /l/oak/elements/branches/branch.lua: -------------------------------------------------------------------------------- 1 | 2 | local tt_table = require("terra.tools.table") 3 | 4 | local toe_internal = require("terra.oak.elements.internal") 5 | local toe_element = require("terra.oak.elements.element") 6 | 7 | local function oak_handle_attach_to_parent_element(branch, parent, root, window, app) 8 | toe_element.default_oak_handle_attach_to_parent_element(branch, parent, root, window, app) 9 | for key, child in branch:oak_children_iter() do 10 | branch.oak_private.child_id_to_index[child.oak_private.id] = key 11 | child:oak_handle_attach_to_parent_element(branch, root, window, app) 12 | end 13 | end 14 | 15 | local function oak_handle_detach_from_parent_element(branch, parent, root, window, app) 16 | toe_element.default_oak_handle_detach_from_parent_element(branch, parent, root, window, app) 17 | for _, child in branch:oak_children_iter() do 18 | child:oak_handle_detach_from_parent_element(branch, root, window, app) 19 | end 20 | end 21 | 22 | local function default_oak_children_iter(branch) 23 | 24 | local co = coroutine.create(function() 25 | if branch.bg ~= nil then 26 | coroutine.yield("bg", branch.bg) 27 | end 28 | if branch.shadow ~= nil then 29 | coroutine.yield("shadow", branch.shadow) 30 | end 31 | for i=1, #branch do 32 | coroutine.yield(i, branch[i]) 33 | end 34 | end) 35 | 36 | return function() 37 | local is_not_finished, k, v = coroutine.resume(co) 38 | if is_not_finished then 39 | return k, v 40 | else 41 | return nil, nil 42 | end 43 | end 44 | end 45 | 46 | 47 | local function set_halign(branch, halign) 48 | toe_element.default_oak_prop_set(branch, "halign", halign) 49 | end 50 | 51 | local function set_valign(branch, valign) 52 | toe_element.default_oak_prop_set(branch, "valign", valign) 53 | end 54 | 55 | local function set_padding(branch, padding) 56 | toe_element.default_oak_prop_set(branch, "padding", padding) 57 | end 58 | 59 | local function new() 60 | 61 | local branch_defaults = { 62 | 63 | -- part of the interface to be a 64 | oak_children_iter = default_oak_children_iter, 65 | 66 | -- branch specific attach/detach functions 67 | oak_handle_attach_to_parent_element = oak_handle_attach_to_parent_element, 68 | oak_handle_detach_from_parent_element = oak_handle_detach_from_parent_element, 69 | 70 | -- default branch-specific property setters 71 | set_bg = toe_internal.set_bg, 72 | set_shadow = toe_internal.set_shadow, 73 | set_child_n = toe_internal.set_child_n, 74 | insert_child_n = toe_internal.insert_child_n, 75 | remove_child_n = toe_internal.remove_child_n, 76 | remove = toe_internal.element_remove, 77 | 78 | set_valign = set_valign, 79 | set_halign = set_halign, 80 | set_padding = set_padding, 81 | } 82 | 83 | local branch = tt_table.crush(toe_element.new(), branch_defaults) 84 | -- we use this to keep track of the index of each child set. This makes 85 | -- it easier to have a "remove_element" function where you only have to 86 | -- supply the element itself. 87 | branch.oak_private.child_id_to_index = {} 88 | return branch 89 | end 90 | 91 | return { 92 | new = new, 93 | 94 | oak_handle_attach_to_parent_element = oak_handle_attach_to_parent_element, 95 | oak_handle_detach_from_parent_element = oak_handle_detach_from_parent_element, 96 | 97 | default_oak_children_iter = default_oak_children_iter, 98 | 99 | set_bg = toe_internal.set_bg, 100 | set_shadow = toe_internal.set_shadow, 101 | set_child_n = toe_internal.set_child_n, 102 | insert_child_n = toe_internal.insert_child_n, 103 | remove_child_n = toe_internal.remove_child_n, 104 | remove = toe_internal.element_remove, 105 | 106 | set_valign = set_valign, 107 | set_halign = set_halign, 108 | set_padding = set_padding, 109 | } 110 | 111 | -------------------------------------------------------------------------------- /l/oak/elements/branches/el.lua: -------------------------------------------------------------------------------- 1 | 2 | local tt_table = require("terra.tools.table") 3 | 4 | local to_padding = require("terra.oak.padding") 5 | local to_size = require("terra.oak.size") 6 | local to_align = require("terra.oak.align") 7 | local to_border = require("terra.oak.border") 8 | local to_internal = require("terra.oak.internal") 9 | 10 | local toe_element = require("terra.oak.elements.element") 11 | local toe_internal = require("terra.oak.elements.internal") 12 | 13 | local toeb_internal = require("terra.oak.elements.branches.internal") 14 | local toeb_branch = require("terra.oak.elements.branches.branch") 15 | 16 | local function el_calculate_minimum_dimensions(el, constraint_w, constraint_h) 17 | 18 | local el_bw = to_border.get_width(el) 19 | local standardized_padding = to_padding.standardize(el.padding or 0) 20 | 21 | local min_w = standardized_padding.left + standardized_padding.right + (el_bw * 2) 22 | local min_h = standardized_padding.top + standardized_padding.bottom + (el_bw * 2) 23 | 24 | local max_w = 0 25 | local max_h = 0 26 | 27 | -- NOTE: only go through the children in the array portion of the table because 28 | -- we don't want the shadow or the bg to take up horizontal space 29 | for _, child in ipairs(el) do 30 | 31 | local child_bw = to_border.get_width(child) 32 | local child_standardized_padding = to_padding.standardize(child.padding or 0) 33 | local child_w, child_h = child.width, child.height 34 | 35 | if type(child_w) == "number" and type(child_h) == "number" then 36 | max_w = math.max( 37 | max_w, 38 | child_w 39 | + (child_bw * 2) 40 | + child_standardized_padding.left 41 | + child_standardized_padding.right 42 | ) 43 | max_h = math.max( 44 | max_h, 45 | child_h 46 | + (child_bw * 2) 47 | + child_standardized_padding.top 48 | + child_standardized_padding.bottom 49 | ) 50 | elseif type(child_w) == "number" and type(child_h) ~= "number" then 51 | local _, min_child_h = child:oak_calculate_minimum_dimensions(constraint_w, constraint_h) 52 | max_w = math.max( 53 | max_w, 54 | child_w 55 | + (child_bw * 2) 56 | + child_standardized_padding.left 57 | + child_standardized_padding.right 58 | ) 59 | max_h = math.max(max_h, min_child_h) 60 | elseif type(child_w) ~= "number" and type(child_h) == "number" then 61 | local min_child_w, _ = child:oak_calculate_minimum_dimensions(constraint_w, constraint_h) 62 | max_w = math.max(max_w, min_child_w) 63 | max_h = math.max( 64 | max_h, 65 | child_h 66 | + (child_bw * 2) 67 | + child_standardized_padding.top 68 | + child_standardized_padding.bottom 69 | ) 70 | else -- both are not numbers 71 | local min_child_w, min_child_h = child:oak_calculate_minimum_dimensions(constraint_w, constraint_h) 72 | max_w = math.max(max_w, min_child_w) 73 | max_h = math.max(max_h, min_child_h) 74 | end 75 | end 76 | 77 | return min_w + max_w, min_h + max_h 78 | end 79 | 80 | 81 | local function _dimensionate_single_child_el(el, child, avail_w, avail_h) 82 | 83 | local padd = el.padding or 0 84 | local standardized_padding = to_padding.standardize(padd) 85 | local padding_top = standardized_padding.top 86 | local padding_right = standardized_padding.right 87 | local padding_bottom = standardized_padding.bottom 88 | local padding_left = standardized_padding.left 89 | local child_bw = to_border.get_width(child) 90 | 91 | local child_w = 0 92 | local child_h = 0 93 | 94 | local function _calculate_non_shrink_width(c) 95 | if type(c.width) == "number" then 96 | return c.width + (child_bw * 2) 97 | else -- child.width == "fill" 98 | local remaining_w = avail_w - (padding_left + padding_right) 99 | if remaining_w - (child_bw * 2) > 0 then 100 | return remaining_w 101 | else 102 | return child_bw * 2 103 | end 104 | end 105 | end 106 | 107 | local function _calculate_non_shrink_height(c) 108 | if type(c.height) == "number" then 109 | return c.height + (child_bw * 2) 110 | else -- child.height == "fill" 111 | local remaining_h = avail_h - (padding_top + padding_bottom) 112 | if remaining_h - (child_bw * 2) > 0 then 113 | return remaining_h 114 | else 115 | return child_bw * 2 116 | end 117 | end 118 | end 119 | 120 | if to_size.is_shrink(child.width) and to_size.is_shrink(child.height) then 121 | child_w, child_h = child:oak_calculate_minimum_dimensions(nil, nil) 122 | 123 | elseif to_size.is_shrink(child.width) and not to_size.is_shrink(child.height) then 124 | local min_w, _ = child:oak_calculate_minimum_dimensions(nil, child_h) 125 | child_h = _calculate_non_shrink_height(child) 126 | child_w = min_w 127 | 128 | elseif not to_size.is_shrink(child.width) and to_size.is_shrink(child.height) then 129 | child_w = _calculate_non_shrink_width(child) 130 | local _, min_h = child:oak_calculate_minimum_dimensions(child_w, nil) 131 | child_h = min_h 132 | 133 | else -- neither are of type "shrink" 134 | child_w = _calculate_non_shrink_width(child) 135 | child_h = _calculate_non_shrink_height(child) 136 | 137 | end 138 | 139 | return { 140 | element = child, 141 | valign = child.valign or to_align.TOP, 142 | halign = child.halign or to_align.LEFT, 143 | width = child_w, 144 | height = child_h, 145 | offset_x = child.offset_x or 0, 146 | offset_y = child.offset_y or 0, 147 | } 148 | end 149 | 150 | local function el_dimensionate_children(el, avail_w, avail_h) 151 | 152 | local dimensionated_children_data = { 153 | available_width = avail_w, 154 | available_height = avail_h, 155 | standardized_padding = to_padding.standardize(el.padding or 0), 156 | parent_border_width = to_border.get_width(el) 157 | } 158 | 159 | do 160 | local shadow = el.shadow 161 | local bg = el.bg 162 | if shadow ~= nil then dimensionated_children_data.shadow = shadow end 163 | if bg ~= nil then dimensionated_children_data.bg = bg end 164 | end 165 | 166 | for _, child in ipairs(el) do 167 | table.insert( 168 | dimensionated_children_data, 169 | _dimensionate_single_child_el(el, child, avail_w, avail_h) 170 | ) 171 | end 172 | 173 | return dimensionated_children_data 174 | end 175 | 176 | local function el_position_children(dimensionated_children_data) 177 | local available_width = dimensionated_children_data.available_width 178 | local available_height = dimensionated_children_data.available_height 179 | 180 | local padding_top, padding_right, padding_bottom, padding_left 181 | do 182 | local standardized_padding = dimensionated_children_data.standardized_padding 183 | padding_top = standardized_padding.top 184 | padding_right = standardized_padding.right 185 | padding_bottom = standardized_padding.bottom 186 | padding_left = standardized_padding.left 187 | end 188 | 189 | local parent_bw = dimensionated_children_data.parent_border_width 190 | 191 | local positioned_children_data = {} 192 | 193 | do -- add shadow and bg elements first 194 | 195 | -- Note: normally, elements should not be dimensionated here, only 196 | -- positioned. but it's such a trivial task that we just dimensionate 197 | -- and position the shadow and bg here 198 | local shadow = dimensionated_children_data.shadow 199 | if shadow ~= nil then 200 | table.insert(positioned_children_data, toe_internal.shadow_dimensionate_and_position( 201 | shadow, 202 | available_width, 203 | available_height 204 | )) 205 | end 206 | 207 | local bg = dimensionated_children_data.bg 208 | if bg ~= nil then 209 | table.insert(positioned_children_data, { 210 | x = bg.offset_x or 0, 211 | y = bg.offset_y or 0, 212 | width = available_width, 213 | height = available_height, 214 | element = bg 215 | }) 216 | end 217 | end 218 | 219 | for _, dimensionated_child in ipairs(dimensionated_children_data) do 220 | 221 | local child_w = dimensionated_child.width 222 | local child_h = dimensionated_child.height 223 | table.insert(positioned_children_data, { 224 | element = dimensionated_child.element, -- a reference to the child 225 | x = to_internal.align_on_secondary_axis( 226 | padding_left + parent_bw, 227 | padding_right + parent_bw, 228 | dimensionated_child.halign, 229 | available_width, 230 | child_w 231 | ) + dimensionated_child.offset_x, 232 | y = to_internal.align_on_secondary_axis( 233 | padding_top + parent_bw, 234 | padding_bottom + parent_bw, 235 | dimensionated_child.valign, 236 | available_height, 237 | child_h 238 | ) + dimensionated_child.offset_y, 239 | width = child_w - (parent_bw * 2), 240 | height = child_h - (parent_bw * 2), 241 | }) 242 | end 243 | 244 | return positioned_children_data 245 | end 246 | 247 | -- TODO: make this function set the geometries of its subchildren directly 248 | local function el_oak_geometrize_children(el, avail_w, avail_h) 249 | 250 | -- NOTE: this will return nil if this el has no shadow, no bg, 251 | -- and no sub-children 252 | if el.bg == nil and el.shadow == nil and #el == 0 then return nil end 253 | 254 | -- TODO: merge these two functions into one 255 | return el_position_children( 256 | el_dimensionate_children(el, avail_w, avail_h) 257 | ) 258 | end 259 | 260 | local function new(args) 261 | if args == nil then args = {} end 262 | 263 | local el_defaults = { 264 | -- part of the interface to be a 265 | oak_geometrize_children = el_geometrize_children, 266 | oak_calculate_minimum_dimensions = el_calculate_minimum_dimensions, 267 | } 268 | 269 | return tt_table.crush(toeb_branch.new(), el_defaults, args) 270 | end 271 | 272 | return { 273 | new = new, 274 | 275 | dimensionate_children = el_dimensionate_children, 276 | position_children = el_position_children, 277 | 278 | oak_geometrize_children = el_oak_geometrize_children, 279 | oak_calculate_minimum_dimensions = oak_el_calculate_minimum_dimensions, 280 | } 281 | 282 | -------------------------------------------------------------------------------- /l/oak/elements/branches/internal.lua: -------------------------------------------------------------------------------- 1 | 2 | local t_element = require("terra.element") 3 | local toe_element = require("terra.oak.elements.element") 4 | 5 | local function get_spacing_between_children(children_amt, spacing) 6 | -- if we have 0 or 1 children, there's 0 spacing 7 | -- if there's 2 or more, we have (num_children - 1) * spacing 8 | if children_amt >= 2 then 9 | return spacing * (children_amt - 1) 10 | end 11 | return 0 12 | end 13 | 14 | local function shadow_dimensionate_and_position(shadow, avail_w, avail_h) 15 | 16 | local edge_width = shadow.edge_width or 0 17 | local shadow_w = avail_w + (edge_width * 2) 18 | local shadow_h = avail_h + (edge_width * 2) 19 | 20 | -- TODO: make sure the shadow x and y is always on integer coordinates 21 | local shadow_edge_width = shadow.edge_width or 0 22 | -- always place shadow in the center of the parent geometry, regardless of 23 | -- what halign/valign the shadow has 24 | local shadow_x = - shadow_edge_width 25 | local shadow_y = - shadow_edge_width 26 | 27 | if shadow.offset_x ~= nil then shadow_x = shadow_x + shadow.offset_x end 28 | if shadow.offset_y ~= nil then shadow_y = shadow_y + shadow.offset_y end 29 | 30 | return { 31 | x = shadow_x, 32 | y = shadow_y, 33 | width = shadow_w, 34 | height = shadow_h, 35 | element = shadow, 36 | } 37 | end 38 | 39 | local function set_spacing(elem, value) 40 | toe_element.default_oak_prop_set(elem, "spacing", value) 41 | end 42 | 43 | return { 44 | POSITION_START = 1, 45 | POSITION_START_END = 2, 46 | POSITION_START_CENTER_END = 3, 47 | 48 | get_spacing_between_children = get_spacing_between_children, 49 | shadow_dimensionate_and_position = shadow_dimensionate_and_position, 50 | 51 | set_spacing = set_spacing, 52 | } 53 | -------------------------------------------------------------------------------- /l/oak/elements/element.lua: -------------------------------------------------------------------------------- 1 | 2 | local tt_table = require("terra.tools.table") 3 | 4 | local t_element = require("terra.element") 5 | local t_sigtools = require("terra.sigtools") 6 | local toe_internal = require("terra.oak.elements.internal") 7 | 8 | local function set_offset_x(element, value) 9 | toe_internal.default_oak_prop_set(element, "offset_x", value) 10 | end 11 | 12 | local function set_offset_y(element, value) 13 | toe_internal.default_oak_prop_set(element, "offset_y", value) 14 | end 15 | 16 | local function set_oak_draw(element, value) 17 | toe_internal.default_oak_prop_set(element, "oak_draw", value) 18 | end 19 | 20 | local function set_opacity(element, value) 21 | toe_internal.default_oak_prop_set(element, "opacity", value) 22 | end 23 | 24 | local function new() 25 | 26 | local element_defaults = { 27 | oak_private = { 28 | -- use an empty function as an ID. TODO: check if this is efficient 29 | id = function() end, 30 | needs_redraw = false, 31 | }, 32 | scope = { 33 | -- will be set when the element gets attached to an attached parent: 34 | -- root : 35 | -- window : 36 | -- parent : 37 | -- self : 38 | -- app : 39 | }, 40 | 41 | -- TODO: make it so that these should only ever be user-defined. 42 | subscribe_on_self = {}, 43 | subscribe_on_parent = {}, 44 | subscribe_on_root = {}, 45 | subscribe_on_window = {}, 46 | subscribe_on_app = {}, 47 | 48 | -- transform-related property setters 49 | set_offset_x = set_offset_x, 50 | set_offset_y = set_offset_y, 51 | -- TODO: implement scale_x, scale_y, rotate, origin 52 | 53 | -- drawing related property setters 54 | set_oak_draw = set_oak_draw, 55 | set_opacity = set_opacity, 56 | } 57 | 58 | return tt_table.crush(t_element.new(), element_defaults) 59 | end 60 | 61 | local function element_setup_signals(element, parent_element, parent_root, parent_window, parent_app) 62 | t_sigtools.setup_subscribe_on_object_signals(element, "self", element) 63 | t_sigtools.setup_subscribe_on_object_signals(parent_element, "parent", element) 64 | t_sigtools.setup_subscribe_on_object_signals(parent_root, "root", element) 65 | t_sigtools.setup_subscribe_on_object_signals(parent_window, "window", element) 66 | t_sigtools.setup_subscribe_on_object_signals(parent_app, "app", element) 67 | end 68 | 69 | local function element_teardown_signals(element, parent_element, parent_root, parent_window, parent_app) 70 | t_sigtools.teardown_subscribe_on_object_signals(element, "self", element) 71 | t_sigtools.teardown_subscribe_on_object_signals(parent_element, "parent", element) 72 | t_sigtools.teardown_subscribe_on_object_signals(parent_root, "root", element) 73 | t_sigtools.teardown_subscribe_on_object_signals(parent_window, "window", element) 74 | t_sigtools.teardown_subscribe_on_object_signals(parent_app, "app", element) 75 | end 76 | 77 | local function default_oak_handle_attach_to_parent_element(element, parent, root, window, app) 78 | 79 | element.scope.self = element 80 | element.scope.parent = parent 81 | element.scope.root = root 82 | element.scope.window = window 83 | element.scope.app = app 84 | 85 | element_setup_signals(element, parent, root, window, app) 86 | end 87 | 88 | local function default_oak_handle_detach_from_parent_element(element, parent, root, window, app) 89 | 90 | element.scope.self = nil 91 | element.scope.parent = nil 92 | element.scope.root = nil 93 | element.scope.window = nil 94 | element.scope.app = nil 95 | 96 | element_teardown_signals(element, parent, root, window, app) 97 | end 98 | 99 | 100 | return { 101 | -- constructor 102 | new = new, 103 | 104 | set_offset_x = set_offset_x, 105 | set_offset_y = set_offset_y, 106 | set_oak_draw = set_oak_draw, 107 | set_opacity = set_opacity, 108 | 109 | default_oak_handle_attach_to_parent_element = default_oak_handle_attach_to_parent_element, 110 | default_oak_handle_detach_from_parent_element = default_oak_handle_detach_from_parent_element, 111 | } 112 | 113 | -------------------------------------------------------------------------------- /l/oak/elements/leaves/bg.lua: -------------------------------------------------------------------------------- 1 | 2 | local tt_table = require("terra.tools.table") 3 | local tt_color = require("terra.tools.color") 4 | 5 | local to_border = require("terra.oak.border") 6 | local to_source = require("terra.oak.source") 7 | local to_shape = require("terra.oak.shape") 8 | 9 | local toe_internal = require("terra.oak.elements.internal") 10 | local toe_element = require("terra.oak.elements.element") 11 | 12 | local toel_leaf = require("terra.oak.elements.leaves.leaf") 13 | 14 | 15 | local function draw_background(bg_elem, cr, width, height) 16 | local bg_source = bg_elem.source 17 | 18 | if bg_source == nil then return end 19 | 20 | local br_top_left, br_top_right, br_bottom_right, br_bottom_left 21 | do 22 | local border_radius = to_border.standardize_radius(bg_elem.border_radius or 0) 23 | br_top_left = border_radius.top_left or 0 24 | br_top_right = border_radius.top_right or 0 25 | br_bottom_right = border_radius.bottom_right or 0 26 | br_bottom_left = border_radius.bottom_left or 0 27 | end 28 | local border_width = bg_elem.border_width or 0 29 | 30 | if br_top_left > 0 or 31 | br_top_right > 0 or 32 | br_bottom_right > 0 or 33 | br_bottom_left > 0 34 | then 35 | -- we have rounded borders, so use a rounded rectangle as the path 36 | cr:save() 37 | cr:translate(border_width, border_width) 38 | to_shape.rounded_rectangle_each( 39 | cr, 40 | width - (border_width * 2), 41 | height - (border_width * 2), 42 | br_top_left, 43 | br_top_right, 44 | br_bottom_right, 45 | br_bottom_left 46 | ) 47 | cr:restore() 48 | else 49 | cr:rectangle( 50 | border_width, 51 | border_width, 52 | width - (border_width * 2), 53 | height - (border_width * 2) 54 | ) 55 | end 56 | 57 | cr:set_source(to_source.to_cairo_source(bg_source)) 58 | cr:fill() 59 | end 60 | 61 | local function draw_border(bg_elem, cr, width, height) 62 | 63 | local border_width = bg_elem.border_width 64 | 65 | if border_width == nil then return end 66 | 67 | local br_top_left, br_top_right, br_bottom_right, br_bottom_left 68 | do 69 | local border_radius = to_border.standardize_radius(bg_elem.border_radius or 0) 70 | br_top_left = border_radius.top_left or 0 71 | br_top_right = border_radius.top_right or 0 72 | br_bottom_right = border_radius.bottom_right or 0 73 | br_bottom_left = border_radius.bottom_left or 0 74 | end 75 | 76 | -- local border_radius = bg_elem.border_radius or 0 77 | local border_source = bg_elem.border_source or tt_color.rgb(0, 0, 0) -- black default color for border 78 | cr:push_group_with_content(lgi.cairo.Content.ALPHA) 79 | cr.fill_rule = lgi.cairo.FillRule.EVEN_ODD 80 | 81 | if br_top_left > 0 or 82 | br_top_right > 0 or 83 | br_bottom_right > 0 or 84 | br_bottom_left > 0 85 | then 86 | 87 | to_shape.rounded_rectangle_each( 88 | cr, 89 | width, 90 | height, 91 | br_top_left + border_width, 92 | br_top_right + border_width, 93 | br_bottom_right + border_width, 94 | br_bottom_left + border_width 95 | ) 96 | 97 | -- to_shape.rounded_rectangle(cr, width, height, border_radius + border_width) 98 | cr:translate(border_width, border_width) 99 | -- to_shape.rounded_rectangle(cr, width - (border_width * 2), height - (border_width * 2), border_radius) 100 | to_shape.rounded_rectangle_each( 101 | cr, 102 | width - (border_width * 2), 103 | height - (border_width * 2), 104 | br_top_left, 105 | br_top_right, 106 | br_bottom_right, 107 | br_bottom_left 108 | ) 109 | cr:fill() 110 | else 111 | cr:rectangle(0, 0, width, height) 112 | cr:rectangle( 113 | border_width, 114 | border_width, 115 | width - (border_width * 2), 116 | height - (border_width * 2) 117 | ) 118 | cr:fill() 119 | end 120 | local msk = cr:pop_group() 121 | cr:set_source(to_source.to_cairo_source(border_source)) 122 | cr:mask(msk) 123 | finish_pattern_surface(msk) 124 | end 125 | 126 | local function bg_draw(bg, cr, width, height) 127 | cr:save() 128 | draw_border(bg, cr, width, height) 129 | cr:restore() 130 | draw_background(bg, cr, width, height) 131 | end 132 | 133 | local function set_source(bg, source) 134 | toe_element.default_oak_prop_set(bg, "source", source) 135 | end 136 | 137 | local function new(args) 138 | 139 | local bg_defaults = { 140 | 141 | -- TODO: uncomment these 142 | -- width = ou_internal.SIZE_FILL, 143 | -- height = ou_internal.SIZE_FILL, 144 | 145 | -- part of the interface to be a 146 | oak_handle_attach_to_parent_element = toe_internal.element_common_handle_attach_to_parent_element, 147 | oak_handle_detach_from_parent_element = toe_internal.element_common_handle_detach_from_parent_element, 148 | 149 | -- this element happens to draw something 150 | oak_draw = bg_draw, 151 | 152 | -- TODO: implement this 153 | set_source = set_source, 154 | 155 | -- TODO: make bg also work with halign and valign 156 | } 157 | 158 | return tt_table.crush(toel_leaf.new(), bg_defaults, args) 159 | end 160 | 161 | return { 162 | new = new, 163 | 164 | oak_draw = bg_draw, 165 | set_source = set_source, 166 | } 167 | 168 | -------------------------------------------------------------------------------- /l/oak/elements/leaves/leaf.lua: -------------------------------------------------------------------------------- 1 | 2 | local tt_table = require("terra.tools.table") 3 | 4 | local toe_element = require("terra.oak.elements.element") 5 | local toe_internal = require("terra.oak.elements.internal") 6 | 7 | local function new() 8 | 9 | local leaf_defaults = { 10 | oak_handle_attach_to_parent_element = toe_element.default_oak_handle_attach_to_parent_element, 11 | oak_handle_detach_from_parent_element = toe_element.default_oak_handle_detach_from_parent_element, 12 | 13 | -- convenience element self-removal method 14 | remove = toe_internal.element_remove, 15 | } 16 | 17 | return tt_table.crush(toe_element.new(), leaf_defaults) 18 | end 19 | 20 | return { 21 | new = new, 22 | 23 | remove = toe_internal.element_remove, 24 | } 25 | 26 | -------------------------------------------------------------------------------- /l/oak/elements/leaves/svg.lua: -------------------------------------------------------------------------------- 1 | 2 | local lgi = require("lgi") 3 | 4 | local tt_color = require("terra.tools.color") 5 | local tt_table = require("terra.tools.table") 6 | 7 | local to_source = require("terra.oak.source") 8 | 9 | local toel_leaf = require("terra.oak.elements.leaves.leaf") 10 | 11 | -- TODO: check if it still makes sense to keep this code this way 12 | local rsvg_handle_cache = setmetatable({}, { __mode = 'k' }) 13 | 14 | ---Load rsvg handle form image file 15 | -- @tparam string file Path to svg file or svg content directly as a lua string. 16 | -- @return Rsvg handle 17 | -- @treturn table A table where cached data can be stored. 18 | local function load_rsvg_handle(file) 19 | 20 | local cache = (rsvg_handle_cache[file] or {})["handle"] 21 | 22 | if cache then 23 | return cache, rsvg_handle_cache[file] 24 | end 25 | 26 | local handle, err 27 | 28 | if file:match("<[?]?xml") or file:match("= text_el._text_layout:get_character_count() then 272 | pango_caret_pos = lgi.Pango.Layout.get_caret_pos( 273 | text_el._text_layout, 274 | text_el._text_layout:get_character_count() 275 | ) 276 | else 277 | pango_caret_pos = lgi.Pango.Layout.get_caret_pos(text_el._text_layout, graphene_index) 278 | end 279 | 280 | -- local a ,_ = lgi.Pango.Layout.move_cursor_visually( 281 | -- text_el._text_layout, 282 | -- true, -- move "strong" cursor. (whatever that means to pango. why do you need 2 cursors?) 283 | -- graphene_index - 1, -- "I am 'here' in the layout" (apparently saying 284 | -- -- you're at index '1' puts your caret after the 285 | -- -- SECOND letter. stupid) 286 | -- 0, -- "from 'here', move this many graphenes to the right or left". see NOTE 287 | -- 0 -- this has to do with things like graphenes formed from a bunch of 288 | -- -- different unicode characters. Setting this to 0 normally works, but 289 | -- -- will probably break when encountering graphenes formed of multiple 290 | -- -- unicode characters, but we'll fix that later lol 291 | -- ) 292 | 293 | -- use a rect for caret position, because carets can be sloped if we have 294 | -- an italic layout. The width of this rect would tell you how much the 295 | -- caret is sloped 296 | return { 297 | x = pango_caret_pos.x / lgi.Pango.SCALE, 298 | y = pango_caret_pos.y / lgi.Pango.SCALE, 299 | width = pango_caret_pos.width / lgi.Pango.SCALE, 300 | height = pango_caret_pos.height / lgi.Pango.SCALE, 301 | } 302 | end 303 | 304 | return { 305 | new = new, 306 | 307 | get_caret_geometry = get_caret_geometry, 308 | set_text = set_text, 309 | set_text_halign = set_text_halign, 310 | set_size = set_size, 311 | set_family = set_family, 312 | set_weight = set_weight, 313 | 314 | oak_draw = oak_draw, 315 | oak_calculate_minimum_dimensions = text_calculate_minimum_dimensions, 316 | } 317 | 318 | -------------------------------------------------------------------------------- /l/oak/internal.lua: -------------------------------------------------------------------------------- 1 | 2 | local to_align = require("terra.oak.align") 3 | 4 | local function align_on_secondary_axis(padding_start, padding_end, align_, context_size, element_size) 5 | if align_ == to_align.BOTTOM or align_ == to_align.RIGHT then 6 | return context_size - (element_size + padding_end) 7 | elseif align_ == to_align.CENTER then 8 | return (context_size / 2) - (element_size / 2) 9 | else 10 | return padding_start 11 | end 12 | end 13 | 14 | return { 15 | align_on_secondary_axis = align_on_secondary_axis, 16 | } 17 | -------------------------------------------------------------------------------- /l/oak/padding.lua: -------------------------------------------------------------------------------- 1 | local _PADDING_AXIS = 1 2 | local _PADDING_EACH = 2 3 | 4 | local function axis(args) 5 | 6 | local x = args.x or 0 7 | local y = args.y or 0 8 | assert(type(x) == "number", "key 'x' should be number, got: " .. tostring(x)) 9 | assert(type(y) == "number", "key 'y' should be number, got: " .. tostring(y)) 10 | 11 | return { 12 | type = _PADDING_AXIS, 13 | x = x, 14 | y = y 15 | } 16 | end 17 | 18 | local function each(args) 19 | 20 | local top = args.top or 0 21 | local right = args.right or 0 22 | local bottom = args.bottom or 0 23 | local left = args.left or 0 24 | 25 | assert(type(top) == "number", "key 'top' should be number, got: " .. tostring(top)) 26 | assert(type(right) == "number", "key 'right' should be number, got: " .. tostring(right)) 27 | assert(type(bottom) == "number", "key 'bottom' should be number, got: " .. tostring(bottom)) 28 | assert(type(left) == "number", "key 'left' should be number, got: " .. tostring(left)) 29 | 30 | return { 31 | type = _PADDING_EACH, 32 | top = top, 33 | right = right, 34 | bottom = bottom, 35 | left = left, 36 | } 37 | end 38 | 39 | local function is_each(p) 40 | if p.type == _PADDING_EACH then return true end 41 | return false 42 | end 43 | local function is_axis(p) 44 | if p.type == _PADDING_AXIS then return true end 45 | return false 46 | end 47 | 48 | -- TODO: just return numbers here, instead of tables 49 | local function standardize(pad) 50 | if type(pad) == "number" then 51 | return { 52 | top = pad, 53 | right = pad, 54 | bottom = pad, 55 | left = pad, 56 | } 57 | elseif is_axis(pad) then 58 | return { 59 | top = pad.y, 60 | right = pad.x, 61 | bottom = pad.y, 62 | left = pad.x, 63 | } 64 | else -- pad.type == PADDING_EACH 65 | return { 66 | top = pad.top, 67 | right = pad.right, 68 | bottom = pad.bottom, 69 | left = pad.left, 70 | } 71 | end 72 | end 73 | 74 | return { 75 | is_each = is_each, 76 | is_axis = is_axis, 77 | each = each, 78 | axis = axis, 79 | standardize = standardize, 80 | } 81 | -------------------------------------------------------------------------------- /l/oak/shape.lua: -------------------------------------------------------------------------------- 1 | 2 | local function circle(cr, x, y, radius) 3 | cr:arc(x, y, radius, 0, 2*math.pi) 4 | cr:close_path() 5 | end 6 | 7 | local function rounded_rectangle(cr, width, height, radius) 8 | local constrained_rad = math.min(math.floor(math.min(width, height)/2), radius) 9 | 10 | local quarter_pi = math.pi/2 11 | local h_point_a = constrained_rad 12 | local h_point_b = width - constrained_rad 13 | local v_point_a = constrained_rad 14 | local v_point_b = height - constrained_rad 15 | 16 | cr:move_to(h_point_a, 0) 17 | cr:line_to(h_point_b, 0) 18 | cr:arc(h_point_b, v_point_a, constrained_rad, -quarter_pi, 0) 19 | cr:line_to(width, v_point_b) 20 | cr:arc(h_point_b, v_point_b, constrained_rad, 0, quarter_pi) 21 | cr:line_to(h_point_a, height) 22 | cr:arc(h_point_a, v_point_b, constrained_rad, quarter_pi, math.pi) 23 | cr:line_to(0, v_point_a) 24 | cr:arc(h_point_a, v_point_a, constrained_rad, math.pi, math.pi + quarter_pi) 25 | cr:close_path() 26 | 27 | end 28 | 29 | local function rounded_rectangle_each(cr, width, height, tl, tr, br, bl) 30 | 31 | local max_rad = math.floor(math.min(width, height) / 2) 32 | tl = math.min(tl, max_rad) 33 | tr = math.min(tr, max_rad) 34 | br = math.min(br, max_rad) 35 | bl = math.min(bl, max_rad) 36 | 37 | local quarter_pi = math.pi/2 38 | 39 | cr:move_to(tl, 0) 40 | cr:line_to(width - tr, 0) 41 | if tr > 0 then cr:arc(width - tr, tr, tr, -quarter_pi, 0) end 42 | cr:line_to(width, height - br) 43 | if br > 0 then cr:arc(width - br, height - br, br, 0, quarter_pi) end 44 | cr:line_to(bl, height) 45 | if bl > 0 then cr:arc(bl, height - bl, bl, quarter_pi, math.pi) end 46 | cr:line_to(0, tl) 47 | if tl > 0 then cr:arc(tl, tl, tl, math.pi, math.pi + quarter_pi) end 48 | cr:close_path() 49 | 50 | end 51 | 52 | return { 53 | circle = circle, 54 | rounded_rectangle = rounded_rectangle, 55 | rounded_rectangle_each = rounded_rectangle_each, 56 | } 57 | -------------------------------------------------------------------------------- /l/oak/size.lua: -------------------------------------------------------------------------------- 1 | 2 | local SIZE_SHRINK = { type = 1 } 3 | local SIZE_FILL = { type = 2 } 4 | 5 | local lib = { 6 | FILL = SIZE_FILL, 7 | SHRINK = SIZE_SHRINK, 8 | } 9 | 10 | local function is_shrink(v) 11 | if v == nil then return true end -- nil always means "shrink" 12 | return type(v) == "table" and v.type == 1 13 | end 14 | 15 | local function is_fill(v) 16 | return type(v) == "table" and v.type == 2 17 | end 18 | 19 | lib.is_shrink = is_shrink 20 | lib.is_fill = is_fill 21 | 22 | return lib 23 | 24 | -------------------------------------------------------------------------------- /l/oak/source.lua: -------------------------------------------------------------------------------- 1 | 2 | local lgi = require("lgi") 3 | local tt_color = require("terra.tools.color") 4 | 5 | local SOURCE_LINEAR_GRADIENT = 3 6 | local SOURCE_RADIAL_GRADIENT = 4 7 | local SOURCE_IMAGE = 5 8 | 9 | local function _check_point(args) 10 | assert(type(args.x) == "number", "x should be a number") 11 | assert(type(args.y) == "number", "y should be a number") 12 | end 13 | 14 | local function stop(offset, color) 15 | assert(tt_color.is_color(color)) 16 | 17 | return { 18 | offset = offset, 19 | color = color, 20 | } 21 | end 22 | 23 | local function linear_gradient(point1, point2, stops) 24 | 25 | _check_point(point1) 26 | _check_point(point2) 27 | 28 | return { 29 | _source_type = SOURCE_LINEAR_GRADIENT, 30 | begin = point1, 31 | finish = point2, 32 | stops = stops, 33 | } 34 | end 35 | 36 | local function _add_color_stop(linpat, stp) 37 | 38 | if stp.color._color_type == tt_color.COLOR_RGB then 39 | lgi.cairo.GradientPattern.add_color_stop_rgb( 40 | linpat, 41 | stp.offset, 42 | stp.color.r, 43 | stp.color.g, 44 | stp.color.b 45 | ) 46 | elseif stp.color._color_type == tt_color.COLOR_RGBA then 47 | lgi.cairo.GradientPattern.add_color_stop_rgba( 48 | linpat, 49 | stp.offset, 50 | stp.color.r, 51 | stp.color.g, 52 | stp.color.b, 53 | stp.color.a 54 | ) 55 | elseif stp.color._color_type == tt_color.COLOR_HSL then 56 | local conv = tt_color.hsl_to_rgb(stp.color) 57 | lgi.cairo.GradientPattern.add_color_stop_rgb( 58 | linpat, 59 | stp.offset, 60 | conv.r, 61 | conv.g, 62 | conv.b 63 | ) 64 | elseif stp.color._color_type == tt_color.COLOR_HSLA then 65 | local conv = tt_color.hsla_to_rgba(stp.color) 66 | lgi.cairo.GradientPattern.add_color_stop_rgba( 67 | linpat, 68 | stp.offset, 69 | conv.r, 70 | conv.g, 71 | conv.b, 72 | conv.a 73 | ) 74 | end 75 | end 76 | 77 | local function to_cairo_source(src) 78 | 79 | if src._color_type ~= nil then 80 | 81 | if src._color_type == tt_color.COLOR_RGB then 82 | return lgi.cairo.Pattern.create_rgb(src.r, src.g, src.b) 83 | elseif src._color_type == tt_color.COLOR_RGBA then 84 | return lgi.cairo.Pattern.create_rgba(src.r, src.g, src.b, src.a) 85 | elseif src._color_type == tt_color.COLOR_HSL then 86 | local conv = tt_color.hsl_to_rgb(src) 87 | return lgi.cairo.Pattern.create_rgb(conv.r, conv.g, conv.b) 88 | elseif src._color_type == tt_color.COLOR_HSLA then 89 | local conv = tt_color.hsla_to_rgba(src) 90 | return lgi.cairo.Pattern.create_rgba(conv.r, conv.g, conv.b, conv.a) 91 | end 92 | 93 | elseif src._source_type ~= nil then 94 | 95 | if src._source_type == SOURCE_LINEAR_GRADIENT then 96 | local linpat = lgi.cairo.Pattern.create_linear( 97 | src.begin.x, src.begin.y, 98 | src.finish.x, src.finish.y 99 | ) 100 | for _, s in ipairs(src.stops) do 101 | _add_color_stop(linpat, s) 102 | end 103 | return linpat 104 | end 105 | 106 | end 107 | end 108 | 109 | return { 110 | linear_gradient = linear_gradient, 111 | stop = stop, 112 | to_cairo_source = to_cairo_source, 113 | } 114 | 115 | -------------------------------------------------------------------------------- /l/object.lua: -------------------------------------------------------------------------------- 1 | 2 | local tstation = require("tstation") 3 | 4 | -- create a new terra object. This object has the potential to have events 5 | -- emitted on its station. 6 | local function new() 7 | return { 8 | station = tstation.new() 9 | } 10 | end 11 | 12 | return { 13 | new = new, 14 | } 15 | -------------------------------------------------------------------------------- /l/orchard.lua: -------------------------------------------------------------------------------- 1 | -- TODO: move this to terra.internal 2 | 3 | local function new() 4 | -- a mapping of s to s 5 | return {} 6 | end 7 | 8 | local function add_window(orchard, window) 9 | orchard[window.window_id] = window 10 | end 11 | 12 | local function get_window_by_id(orchard, window_id) 13 | return orchard[window_id] 14 | end 15 | 16 | local function remove_window_by_id(orchard, window_id) 17 | orchard[window_id] = nil 18 | end 19 | 20 | return { 21 | new = new, 22 | add_window = add_window, 23 | get_window_by_id = get_window_by_id, 24 | remove_window_by_id = remove_window_by_id, 25 | } 26 | 27 | -------------------------------------------------------------------------------- /l/platforms/common/window.lua: -------------------------------------------------------------------------------- 1 | 2 | local luv = require("luv") 3 | local tstation = require("tstation") 4 | 5 | local t_object = require("terra.object") 6 | local t_sigtools = require("terra.sigtools") 7 | local t_element = require("terra.element") 8 | 9 | local tt_table = require("terra.tools.table") 10 | 11 | local visibility = { 12 | HIDDEN = 0, 13 | RAISED = 1, 14 | RAISED_AND_SHOWING = 2, 15 | } 16 | 17 | local function set_tree(window, new_tree) 18 | 19 | if window.tree == tree then return end -- nothing changed 20 | 21 | local old_tree = window.tree 22 | if old_tree == nil then 23 | if tree == nil then -- nothing to do 24 | return 25 | else -- old tree is nil, set new tree 26 | window.tree = tree 27 | tree:handle_attach_to_window(window, window.scope.app) 28 | end 29 | else 30 | -- remove the old tree 31 | old_tree:handle_detach_from_window(window, window.scope.app) 32 | if tree == nil then 33 | window.tree = nil 34 | else -- set the new tree 35 | tree:handle_attach_to_window(window, window.scope.app) 36 | window.tree = tree 37 | end 38 | end 39 | end 40 | 41 | local function set_max_fps(window, fps) 42 | if fps < 0 then fps = 0 end 43 | if fps == nil then fps = 300 end -- TODO: add support for unlimited fps 44 | 45 | if fps == window.max_fps then return end 46 | window.max_fps = fps 47 | 48 | if fps == 0 then 49 | window._draw_every = 0 50 | luv.timer_stop(window.frame_timer) 51 | else 52 | window._draw_every = 1/fps 53 | luv.timer_set_repeat(window.frame_timer, window._draw_every * 1000) 54 | 55 | -- draw it instantly after setting the fps. the draw function 56 | -- should take care to reset the timer properly. 57 | window:draw() 58 | end 59 | end 60 | 61 | local function reset_frame_timer(window) 62 | luv.timer_again(window.frame_timer) 63 | end 64 | 65 | local function setup_signals(window, parent_app) 66 | -- TODO: make these work so that the user can set them to nil 67 | t_sigtools.setup_subscribe_on_object_signals(window, "self", window) 68 | t_sigtools.setup_subscribe_on_object_signals(parent_app, "app", window) 69 | end 70 | 71 | local function teardown_signals(window, parent_app) 72 | t_sigtools.teardown_subscribe_on_object_signals(window, "self", window) 73 | t_sigtools.teardown_subscribe_on_object_signals(parent_app, "app", window) 74 | end 75 | 76 | local function check_valid(app, x, y, width, height) 77 | assert(app ~= nil, "You must provide a valid .") 78 | assert(x ~= nil, "You must provide a x coord.") 79 | assert(y ~= nil, "You must provide a y coord.") 80 | assert(width ~= nil, "You must provide a width property.") 81 | assert(height ~= nil, "You must provide a height property.") 82 | end 83 | 84 | local function window_common_new(app, x, y, width, height, args) 85 | 86 | -- TODO: document supported fields 87 | local window_defaults = { 88 | -- model = model -- NOTE: the user provides a model if he wants 89 | 90 | -- tree = nil, -- to be set by the user 91 | 92 | -- TODO: check which screen this window is on, and try to get the 93 | -- refresh rate and use that by default 94 | max_fps = 144, 95 | _draw_every = nil, 96 | 97 | -- signal handling 98 | subscribe_on_self = {}, 99 | subscribe_on_app = {}, 100 | 101 | scope = { 102 | app = app, 103 | }, 104 | 105 | set_max_fps = set_max_fps, 106 | reset_frame_timer = reset_frame_timer, 107 | 108 | -- by default, all windows are "hidden" 109 | visibility = visibility.HIDDEN, 110 | } 111 | local window = tt_table.crush(t_element.new(), window_defaults, args) 112 | check_valid(window.scope.app, x, y, width, height) 113 | 114 | -- set the timer callback 115 | window._timer_cb = function() 116 | window:draw() 117 | end 118 | 119 | -- create the frame timer 120 | window.frame_timer = luv.new_timer() 121 | if window.max_fps == 0 then 122 | window._draw_every = 0 123 | else 124 | window._draw_every = 1/window.max_fps 125 | -- window.frame_timer = lev.Timer.new(window._timer_cb, window._draw_every, window._draw_every) 126 | luv.timer_start( 127 | window.frame_timer, 128 | 0, 129 | window._draw_every * 1000, -- miliseconds 130 | window._timer_cb 131 | ) 132 | end 133 | 134 | -- the scope should contain a reference to self 135 | window.scope.self = window 136 | 137 | -- always set the initial window.geometry 138 | window:set_geometry(x, y, width, height) 139 | 140 | -- setup the signals 141 | setup_signals(window, app) 142 | 143 | -- set the tree if there is one. 144 | if window.tree ~= nil then 145 | window.tree:handle_attach_to_window(window, window.scope.app) 146 | end 147 | 148 | -- TODO: maybe add functionality to allow users to move a window from one 149 | -- to another? If so, we need to handle signals properly. 150 | 151 | return window 152 | end 153 | 154 | return { 155 | common_new = window_common_new, 156 | check_valid = check_valid, 157 | visibility = visibility, 158 | 159 | setup_signals = setup_signals, 160 | teardown_signals = teardown_signals, 161 | 162 | set_max_fps = set_max_fps, 163 | reset_frame_timer = reset_frame_timer, 164 | } 165 | -------------------------------------------------------------------------------- /l/platforms/xcb/app.lua: -------------------------------------------------------------------------------- 1 | 2 | local luv = require("luv") 3 | 4 | local t_platform = require("terra.platform") 5 | local t_object = require("terra.object") 6 | local t_orchard = require("terra.orchard") 7 | 8 | local tt_table = require("terra.tools.table") 9 | local tpx_ctx = require("terra.platforms.xcb.ctx") 10 | 11 | local events = { 12 | X_ClickEvent = "X_ClickEvent", 13 | X_ConfigureNotify = "X_ConfigureNotify", 14 | X_CreateNotify = "X_CreateNotify", 15 | X_DestroyNotify = "X_DestroyNotify", 16 | X_EnterNotify = "X_EnterNotify", 17 | X_ExposeEvent = "X_ExposeEvent", 18 | X_FocusIn = "X_FocusIn", 19 | X_FocusOut = "X_FocusOut", 20 | X_KeyEvent = "X_KeyEvent", 21 | X_LeaveNotify = "X_LeaveNotify", 22 | X_MotionEvent = "X_MotionEvent", 23 | X_MapNotify = "X_MapNotify", 24 | X_MapRequest = "X_MapRequest", 25 | X_PropertyNotify = "X_PropertyNotify", 26 | X_ReparentNotify = "X_ReparentNotify", 27 | X_VisibilityNotify = "X_VisibilityNotify", 28 | X_UnmapNotify = "X_UnmapNotify", 29 | } 30 | 31 | local function handle_configure_notify_event(app, event_type, window_id, x, y, width, height) 32 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 33 | window:handle_configure_notify_event(x, y, width, height) 34 | end 35 | 36 | local function handle_mouse_click_event(app, event_type, window_id, is_press, button, modifiers, x, y) 37 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 38 | window:handle_mouse_click_event(is_press, button, modifiers, x, y) 39 | end 40 | 41 | local function handle_create_event(app, event_type, parent_id, window_id, x, y, width, height) 42 | local window = t_orchard.get_window_by_id(app.orchard, parent_id) 43 | window:handle_create_event() 44 | end 45 | 46 | local function handle_destroy_event(window, event_type, parent_id, window_id) 47 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 48 | window:handle_destroy_event() 49 | end 50 | 51 | local function handle_mouse_enter_event(app, event_type, window_id, button, modifiers, x, y) 52 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 53 | window:handle_mouse_enter_event(button, modifiers, x, y) 54 | end 55 | 56 | local function handle_expose_event(app, event_type, window_id, x, y, width, height, count) 57 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 58 | window:handle_expose_event(x, y, width, height, count) 59 | end 60 | 61 | local function handle_focus_in_event(app, event_type, window_id) 62 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 63 | window:handle_focus_in_event() 64 | end 65 | 66 | local function handle_focus_out_event(app, event_type, window_id) 67 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 68 | window:handle_focus_out_event() 69 | end 70 | 71 | local function handle_key_event(app, event_type, window_id, is_press, key, modifiers) 72 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 73 | window:handle_key_event(is_press, key, modifiers) 74 | end 75 | 76 | local function handle_mouse_leave_event(app, event_type, window_id, button, modifiers, x, y) 77 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 78 | window:handle_mouse_leave_event(button, modifiers, x, y) 79 | end 80 | 81 | local function handle_mouse_motion_event(app, event_type, window_id, modifiers, x, y) 82 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 83 | window:handle_mouse_motion_event(modifiers, x, y) 84 | end 85 | 86 | local function handle_map_event(app, event_type, window_id) 87 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 88 | window:handle_map_event() 89 | end 90 | 91 | local function handle_map_request(app, event_type, parent_id, window_id) 92 | local window = t_orchard.get_window_by_id(app.orchard, parent_id) 93 | window:handle_map_request(window_id) 94 | end 95 | 96 | local function handle_property_event(app, event_type, window_id, atom, time, state) 97 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 98 | window:handle_property_event(atom, time, state) 99 | end 100 | 101 | local function handle_reparent_event(app, event_type, event_window_id, parent_id, window_id, x, y) 102 | -- this happens when a window is reparented to us, or when our window 103 | -- is reparented onto another. I'm not sure what to do about this yet, 104 | -- so let's just mark everything for relayout and redraw. 105 | -- ou_internal.element_mark_relayout(window) 106 | -- ou_internal.element_mark_redraw(window) 107 | end 108 | 109 | local function handle_visibility_event(app, event_type, window_id, visibility) 110 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 111 | window:handle_visibility_event(visibility) 112 | end 113 | 114 | local function handle_unmap_event(app, event_type, window_id) 115 | local window = t_orchard.get_window_by_id(app.orchard, window_id) 116 | window:handle_unmap_event() 117 | end 118 | 119 | local default_event_handler_map = { 120 | [events.X_ClickEvent] = handle_mouse_click_event, 121 | [events.X_ConfigureNotify] = handle_configure_notify_event, 122 | [events.X_CreateNotify] = handle_create_event, 123 | [events.X_DestroyNotify] = handle_destroy_event, 124 | [events.X_EnterNotify] = handle_mouse_enter_event, 125 | [events.X_ExposeEvent] = handle_expose_event, 126 | [events.X_FocusIn] = handle_focus_in_event, 127 | [events.X_FocusOut] = handle_focus_out_event, 128 | [events.X_KeyEvent] = handle_key_event, 129 | [events.X_LeaveNotify] = handle_mouse_leave_event, 130 | [events.X_MotionEvent] = handle_mouse_motion_event, 131 | [events.X_MapNotify] = handle_map_event, 132 | [events.X_MapRequest] = handle_map_request, 133 | [events.X_PropertyNotify] = handle_property_event, 134 | [events.X_ReparentNotify] = handle_reparent_event, 135 | [events.X_VisibilityNotify] = handle_visibility_event, 136 | [events.X_UnmapNotify] = handle_unmap_event, 137 | } 138 | 139 | local function make_default_event_handler(user_event_handler) 140 | return function(app, event_type, ...) 141 | local cb = default_event_handler_map[event_type] 142 | if cb == nil then 143 | return user_event_handler(app, event_type, ...) 144 | else 145 | return cb(app, event_type, ...) 146 | end 147 | end 148 | end 149 | 150 | local function desktop(init_func, event_handler) 151 | local app = tt_table.crush(t_object.new(), { 152 | -- we use a to keep track of all of the windows 153 | orchard = t_orchard.new(), 154 | -- user can set a ".model" field if he wants 155 | 156 | -- a "host/port" : mapping 157 | _tcp_conns = {}, 158 | }) 159 | 160 | -- the xcb_ctx is a bunch of stuff from the C side that we need in 161 | -- order to have the application work 162 | app.xcb_ctx = tpx_ctx.create(function(...) 163 | return event_handler(app, ...) 164 | end) 165 | 166 | -- create the X11 file descriptor watcher 167 | app.xcb_watcher = luv.new_poll(tpx_ctx.get_file_descriptor(app.xcb_ctx)) 168 | luv.poll_start(app.xcb_watcher, "r", function(err, events) 169 | tpx_ctx.handle_events() 170 | end) 171 | 172 | -- let the user initialize his data 173 | init_func(app) 174 | 175 | -- start the event loop 176 | luv.run() 177 | 178 | -- this only runs after the event loop is stopped. 179 | -- normally, when the application is done. 180 | tpx_ctx.destroy() 181 | end 182 | 183 | -- -- function that allows the user to listen to events on a user-supplied 184 | -- -- file descriptor. 185 | -- -- @fun: (loop, watcher, revents) -> nil 186 | -- local function watch_io(app, fd, fun) 187 | -- -- The way to stop an io watcher created with this function is by 188 | -- -- calling a ":stop(loop)" method on the return value. I dislike 189 | -- -- APIs like this. TODO: Maybe fork lua-ev and write my own api. 190 | -- local watcher = lev.IO.new(fun, fd, ev.READ) -- TODO: use bitlib 191 | -- watcher:start(app.event_loop) 192 | -- return watcher 193 | -- end 194 | 195 | -- -- TODO: implement this. This function lets the user simulate an event 196 | -- -- directly onto the event loop and have it handled as a regular event. 197 | -- local function event(app, name, ...) 198 | -- end 199 | 200 | -- local function desktop(init_func, event_handler) 201 | -- t_i_application.desktop( 202 | -- function(terra_data) -- initialization function 203 | -- 204 | -- -- Inheritance usually sucks but since we always need to have a 205 | -- -- "station" field on each , , and 206 | -- -- element, it makes sense to make our lives easier this way. 207 | -- local app = tt_table.crush(t_object.new(), { 208 | -- -- we use a to keep track of all of the windows 209 | -- orchard = t_orchard.new(), 210 | -- -- the terra data is a bunch of stuff from the C side that we need in 211 | -- -- order to have the application work 212 | -- terra_data = terra_data, 213 | -- -- the ".model" should be set by the user if it makes sense to have one 214 | -- }) 215 | -- 216 | -- init_func(app) 217 | -- 218 | -- return app 219 | -- end, 220 | -- event_handler 221 | -- ) 222 | -- end 223 | 224 | 225 | return { 226 | -- supported events names 227 | events = events, 228 | 229 | default_event_handler_map = default_event_handler_map, 230 | -- app default xcb handlers 231 | handle_mouse_click_event = handle_mouse_click_event, 232 | handle_configure_notify_event = handle_configure_notify_event, 233 | handle_create_event = handle_create_event, 234 | handle_destroy_event = handle_destroy_event, 235 | handle_mouse_enter_event = handle_mouse_enter_event, 236 | handle_expose_event = handle_expose_event, 237 | handle_focus_in_event = handle_focus_in_event, 238 | handle_focus_out_event = handle_focus_out_event, 239 | handle_key_event = handle_key_event, 240 | handle_mouse_leave_event = handle_mouse_leave_event, 241 | handle_mouse_motion_event = handle_mouse_motion_event, 242 | handle_map_event = handle_map_event, 243 | handle_map_request = handle_map_request, 244 | handle_property_event = handle_property_event, 245 | handle_reparent_event = handle_reparent_event, 246 | handle_visibility_event = handle_visibility_event, 247 | handle_unmap_event = handle_unmap_event, 248 | 249 | make_default_event_handler = make_default_event_handler, 250 | 251 | -- create and run the app 252 | desktop = desktop, 253 | 254 | -- -- app tools 255 | -- watch_io = watch_io, 256 | 257 | -- TODO: see if I can get rid of these things 258 | -- sync = t_i_application.sync, 259 | -- flush = t_i_application.flush, 260 | } 261 | -------------------------------------------------------------------------------- /l/puv.lua: -------------------------------------------------------------------------------- 1 | -- promise-based wrapper around luv 2 | 3 | local luv = require("luv") 4 | local tt_promise = require("terra.tools.promise") 5 | 6 | local function mkdir(path) 7 | local prom = tt_promise.new() 8 | 9 | luv.fs_mkdir(path, tonumber('755', 8), function(err, success) 10 | if err ~= nil then 11 | prom:reject(err) 12 | else 13 | prom:resolve(success) 14 | end 15 | end) 16 | return prom 17 | end 18 | 19 | local function tcp_new() 20 | local prom = tt_promise.new() 21 | local tcp, err_msg, err_type = luv.new_tcp() 22 | if tcp == nil then 23 | prom:reject(err_msg) 24 | else 25 | prom:resolve(tcp) 26 | end 27 | return prom 28 | end 29 | 30 | local function tcp_connect(tcp, host, port) 31 | 32 | local p = tt_promise.new() 33 | luv.tcp_connect(tcp, host, port, function(err) 34 | if err ~= nil then 35 | p:reject(err) 36 | else 37 | p:resolve(tcp) 38 | end 39 | end) 40 | return p 41 | end 42 | 43 | local function tcp_write(tcp, data) 44 | 45 | local p = tt_promise.new() 46 | 47 | luv.write(tcp, data, function(err) 48 | if err ~= nil then 49 | p:reject(err) 50 | else 51 | p:resolve(tcp) 52 | end 53 | end) 54 | 55 | return p 56 | end 57 | 58 | return { 59 | mkdir = mkdir, 60 | tcp_new = tcp_new, 61 | tcp_connect = tcp_connect, 62 | tcp_write = tcp_write 63 | } 64 | -------------------------------------------------------------------------------- /l/sigtools.lua: -------------------------------------------------------------------------------- 1 | 2 | local tstation = require("tstation") 3 | 4 | local function setup_subscribe_on_object_signals(obj_a, name, obj_b) 5 | 6 | -- NOTE: we keep track of this function because we're going to need it 7 | -- in order to unsubscribe upon destruction. 8 | obj_b["subscribe_on_" .. name .. "_event_handler"] = function(event_type, ...) 9 | -- print(event_type) 10 | local handler = obj_b["subscribe_on_" .. name][event_type] 11 | -- print("handler:", handler) 12 | if handler == nil then return end 13 | return handler(obj_b, ...) 14 | end 15 | tstation.subscribe_function(obj_a.station, obj_b["subscribe_on_" .. name .. "_event_handler"]) 16 | end 17 | 18 | local function teardown_subscribe_on_object_signals(obj_a, name, obj_b) 19 | tstation.unsubscribe_function(obj_a.station, obj_b["subscribe_on_" .. name .. "_event_handler"]) 20 | end 21 | 22 | return { 23 | setup_subscribe_on_object_signals = setup_subscribe_on_object_signals, 24 | teardown_subscribe_on_object_signals = teardown_subscribe_on_object_signals, 25 | } 26 | -------------------------------------------------------------------------------- /l/tools/color.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2024 Chris Montero 2 | 3 | local COLOR_RGB = 1 4 | local COLOR_RGBA = 2 5 | local COLOR_HSL = 3 6 | local COLOR_HSLA = 4 7 | 8 | local function _clamp(val, min, max) 9 | if val < min then return min end 10 | if val > max then return max end 11 | return val 12 | end 13 | 14 | local function _parse_string(str) 15 | assert(string.match(str, "^#%x+$") ~= nil, 16 | [[the string supplied to should have one '#' character, followed by six 17 | or eight hexadecimal digits. Got: ]] .. str 18 | ) 19 | 20 | local str_numbers = string.sub(str, 2, string.len(str)) 21 | 22 | -- make sure the values stay between 0 and 1 23 | local r = _clamp(tonumber(string.sub(str_numbers, 1, 2), 16) / 255, 0, 1) 24 | local g = _clamp(tonumber(string.sub(str_numbers, 3, 4), 16) / 255, 0, 1) 25 | local b = _clamp(tonumber(string.sub(str_numbers, 5, 6), 16) / 255, 0, 1) 26 | local a 27 | if string.len(str_numbers) == 8 then 28 | a = _clamp(tonumber(string.sub(str_numbers, 7, 8), 16) / 255, 0, 1) 29 | end 30 | 31 | return { 32 | r = r, 33 | g = g, 34 | b = b, 35 | a = a 36 | } 37 | 38 | end 39 | 40 | local function rgb(r, g, b) 41 | 42 | return { 43 | _color_type = COLOR_RGB, 44 | r = _clamp(r, 0, 1), 45 | g = _clamp(g, 0, 1), 46 | b = _clamp(b, 0, 1), 47 | } 48 | end 49 | 50 | 51 | local function rgba(r, g, b, a) 52 | 53 | return { 54 | _color_type = COLOR_RGBA, 55 | r = _clamp(r, 0, 1), 56 | g = _clamp(g, 0, 1), 57 | b = _clamp(b, 0, 1), 58 | a = _clamp(a, 0, 1), 59 | } 60 | end 61 | 62 | local function hsl(h, s, l) 63 | 64 | return { 65 | _color_type = COLOR_HSL, 66 | -- TODO: fix. the user first sets the hue value from 0 to 360, but then 67 | -- he'll be surprised to see that the value internally goes from 0 to 1 68 | h = _clamp(h, 0, 360) / 360, 69 | s = _clamp(s, 0, 1), 70 | l = _clamp(l, 0, 1), 71 | } 72 | end 73 | 74 | local function hsla(h, s, l, a) 75 | 76 | return { 77 | _color_type = COLOR_HSLA, 78 | -- TODO: fix. the user first sets the hue value from 0 to 360, but then 79 | -- he'll be surprised to see that the value internally goes from 0 to 1 80 | h = _clamp(h, 0, 360) / 360, 81 | s = _clamp(s, 0, 1), 82 | l = _clamp(l, 0, 1), 83 | a = _clamp(a, 0, 1) 84 | } 85 | end 86 | 87 | local function rgb_to_hsl(color) 88 | local r = color.r 89 | local g = color.g 90 | local b = color.b 91 | 92 | local max, min = math.max(r, g, b), math.min(r, g, b) 93 | local h, s, l 94 | 95 | l = (max + min) / 2 96 | 97 | if max == min then 98 | h, s = 0, 0 -- achromatic 99 | else 100 | local d = max - min 101 | if l > 0.5 then s = d / (2 - max - min) else s = d / (max + min) end 102 | if max == r then 103 | h = (g - b) / d 104 | if g < b then h = h + 6 end 105 | elseif max == g then h = (b - r) / d + 2 106 | elseif max == b then h = (r - g) / d + 4 107 | end 108 | h = h / 6 109 | end 110 | 111 | return { 112 | _color_type = COLOR_HSL, 113 | h = h, 114 | s = s, 115 | l = l 116 | } 117 | end 118 | 119 | local function rgba_to_hsla(color) 120 | local conv = rgb_to_hsl(color) 121 | conv.a = color.a 122 | return conv 123 | end 124 | 125 | local function hsl_to_rgb(color) 126 | local r, g, b 127 | local h, s, l = color.h, color.s, color.l 128 | 129 | if s == 0 then 130 | r, g, b = l, l, l -- achromatic 131 | else 132 | local function hue2rgb(p, q, t) 133 | if t < 0 then t = t + 1 end 134 | if t > 1 then t = t - 1 end 135 | if t < 1/6 then return p + (q - p) * 6 * t end 136 | if t < 1/2 then return q end 137 | if t < 2/3 then return p + (q - p) * (2/3 - t) * 6 end 138 | return p 139 | end 140 | 141 | local q 142 | if l < 0.5 then q = l * (1 + s) else q = l + s - l * s end 143 | local p = 2 * l - q 144 | 145 | r = hue2rgb(p, q, h + 1/3) 146 | g = hue2rgb(p, q, h) 147 | b = hue2rgb(p, q, h - 1/3) 148 | end 149 | 150 | return { 151 | _color_type = COLOR_RGB, 152 | r = r, 153 | g = g, 154 | b = b 155 | } 156 | end 157 | 158 | local function hsla_to_rgba(color) 159 | local conv = hsl_to_rgb(color) 160 | conv.a = color.a 161 | return conv 162 | end 163 | 164 | -- local function lighten(color, amt) 165 | -- return { 166 | -- _color_type = COLOR_RGB, 167 | -- r = _clamp(color.r + amt, 0, 1), 168 | -- g = _clamp(color.g + amt, 0, 1), 169 | -- b = _clamp(color.b + amt, 0, 1) 170 | -- } 171 | -- end 172 | 173 | local function rgb_from_string(str) 174 | 175 | local parsed_string = _parse_string(str) 176 | return { 177 | _color_type = COLOR_RGB, 178 | r = parsed_string.r, 179 | g = parsed_string.g, 180 | b = parsed_string.b, 181 | } 182 | 183 | end 184 | 185 | local function rgba_from_string(str) 186 | 187 | local parsed = _parse_string(str) 188 | assert(parsed.a ~= nil, 189 | [[the string given must have a '#' character, followed by 8 hexadecimal 190 | digits. Got: ]] .. str 191 | ) 192 | 193 | return { 194 | _color_type = COLOR_RGBA, 195 | r = parsed.r, 196 | g = parsed.g, 197 | b = parsed.b, 198 | a = parsed.a 199 | } 200 | end 201 | 202 | local function is_color(val) 203 | if val._color_type == nil then 204 | return false 205 | end 206 | if val._color_type < COLOR_RGB then 207 | return false 208 | end 209 | if val._color_type > COLOR_HSLA then 210 | return false 211 | end 212 | return true 213 | end 214 | 215 | return { 216 | 217 | COLOR_RGB = COLOR_RGB, 218 | COLOR_RGBA = COLOR_RGBA, 219 | COLOR_HSL = COLOR_HSL, 220 | COLOR_HSLA = COLOR_HSLA, 221 | 222 | rgb = rgb, 223 | rgba = rgba, 224 | hsl = hsl, 225 | hsla = hsla, 226 | 227 | is_color = is_color, 228 | -- lighten = lighten, 229 | 230 | rgb_to_hsl = rgb_to_hsl, 231 | rgba_to_hsla = rgba_to_hsla, 232 | hsl_to_rgb = hsl_to_rgb, 233 | hsla_to_rgba = hsla_to_rgba, 234 | 235 | rgb_from_string = rgb_from_string, 236 | rgba_from_string = rgba_from_string, 237 | 238 | } 239 | -------------------------------------------------------------------------------- /l/tools/enum.lua: -------------------------------------------------------------------------------- 1 | 2 | -- very simple enum tool 3 | 4 | -- from a list of strings, it creates a : mapping. 5 | local function new(strings) 6 | local ret = {} 7 | for _, v in ipairs(strings) do 8 | ret[v] = v 9 | end 10 | 11 | setmetatable(ret, { 12 | __index = function(t, k) 13 | if t[k] == nil then 14 | error("value " .. tostring(k) .. " does not exist in enum" .. tostring(k) .. ".") 15 | end 16 | end, 17 | __newindex = function(t, k, v) 18 | print("cannot add key/value (" .. tostring(k) .. "-" .. tostring(v) .. ") to enum " .. tostring(t) .. ".") 19 | end, 20 | }) 21 | 22 | return ret 23 | end 24 | 25 | local function iter(enum) 26 | return pairs(enum) 27 | end 28 | 29 | return { 30 | new = new, 31 | iter = iter, 32 | } 33 | -------------------------------------------------------------------------------- /l/tools/promise.lua: -------------------------------------------------------------------------------- 1 | --- A+ promises in Lua. 2 | --- @module deferred 3 | --- 4 | 5 | --The MIT License (MIT) 6 | -- Copyright (c) 2015 Serge Zaitsev 7 | -- 8 | -- Permission is hereby granted, free of charge, to any person obtaining a copy 9 | -- of this software and associated documentation files (the "Software"), to deal 10 | -- in the Software without restriction, including without limitation the rights 11 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | -- copies of the Software, and to permit persons to whom the Software is 13 | -- furnished to do so, subject to the following conditions: 14 | -- 15 | -- The above copyright notice and this permission notice shall be included in all 16 | -- copies or substantial portions of the Software. 17 | -- 18 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | -- SOFTWARE. 25 | 26 | local M = {} 27 | 28 | local deferred = {} 29 | deferred.__index = deferred 30 | 31 | local PENDING = 0 32 | local RESOLVING = 1 33 | local REJECTING = 2 34 | local RESOLVED = 3 35 | local REJECTED = 4 36 | 37 | local function finish(deferred, state) 38 | state = state or REJECTED 39 | for i, f in ipairs(deferred.queue) do 40 | if state == RESOLVED then 41 | f:resolve(deferred.value) 42 | else 43 | f:reject(deferred.value) 44 | end 45 | end 46 | deferred.state = state 47 | end 48 | 49 | local function isfunction(f) 50 | if type(f) == 'table' then 51 | local mt = getmetatable(f) 52 | return mt ~= nil and type(mt.__call) == 'function' 53 | end 54 | return type(f) == 'function' 55 | end 56 | 57 | local function promise(deferred, next, success, failure, nonpromisecb) 58 | if type(deferred) == 'table' and type(deferred.value) == 'table' and isfunction(next) then 59 | local called = false 60 | local ok, err = pcall(next, deferred.value, function(v) 61 | if called then return end 62 | called = true 63 | deferred.value = v 64 | success() 65 | end, function(v) 66 | if called then return end 67 | called = true 68 | deferred.value = v 69 | failure() 70 | end) 71 | if not ok and not called then 72 | deferred.value = err 73 | failure() 74 | end 75 | else 76 | nonpromisecb() 77 | end 78 | end 79 | 80 | local function fire(deferred) 81 | local next 82 | if type(deferred.value) == 'table' then 83 | next = deferred.value.next 84 | end 85 | promise(deferred, next, function() 86 | deferred.state = RESOLVING 87 | fire(deferred) 88 | end, function() 89 | deferred.state = REJECTING 90 | fire(deferred) 91 | end, function() 92 | local ok 93 | local v 94 | if deferred.state == RESOLVING and isfunction(deferred.success) then 95 | ok, v = pcall(deferred.success, deferred.value) 96 | elseif deferred.state == REJECTING and isfunction(deferred.failure) then 97 | ok, v = pcall(deferred.failure, deferred.value) 98 | if ok then 99 | deferred.state = RESOLVING 100 | end 101 | end 102 | 103 | if ok ~= nil then 104 | if ok then 105 | deferred.value = v 106 | else 107 | deferred.value = v 108 | return finish(deferred) 109 | end 110 | end 111 | 112 | if deferred.value == deferred then 113 | deferred.value = pcall(error, 'resolving promise with itself') 114 | return finish(deferred) 115 | else 116 | promise(deferred, next, function() 117 | finish(deferred, RESOLVED) 118 | end, function(state) 119 | finish(deferred, state) 120 | end, function() 121 | finish(deferred, deferred.state == RESOLVING and RESOLVED) 122 | end) 123 | end 124 | end) 125 | end 126 | 127 | local function resolve(deferred, state, value) 128 | if deferred.state == 0 then 129 | deferred.value = value 130 | deferred.state = state 131 | fire(deferred) 132 | end 133 | return deferred 134 | end 135 | 136 | -- 137 | -- PUBLIC API 138 | -- 139 | function deferred:resolve(value) 140 | return resolve(self, RESOLVING, value) 141 | end 142 | 143 | function deferred:reject(value) 144 | return resolve(self, REJECTING, value) 145 | end 146 | 147 | --- Returns a new promise object. 148 | --- @treturn Promise New promise 149 | --- @usage 150 | --- local deferred = require('deferred') 151 | --- 152 | --- -- 153 | --- -- Converting callback-based API into promise-based is very straightforward: 154 | --- -- 155 | --- -- 1) Create promise object 156 | --- -- 2) Start your asynchronous action 157 | --- -- 3) Resolve promise object whenever action is finished (only first resolution 158 | --- -- is accepted, others are ignored) 159 | --- -- 4) Reject promise object whenever action is failed (only first rejection is 160 | --- -- accepted, others are ignored) 161 | --- -- 5) Return promise object letting calling side to add a chain of callbacks to 162 | --- -- your asynchronous function 163 | --- 164 | --- function read(f) 165 | --- local d = deferred.new() 166 | --- readasync(f, function(contents, err) 167 | --- if err == nil then 168 | --- d:resolve(contents) 169 | --- else 170 | --- d:reject(err) 171 | --- end 172 | --- end) 173 | --- return d 174 | --- end 175 | --- 176 | --- -- You can now use read() like this: 177 | --- read('file.txt'):next(function(s) 178 | --- print('File.txt contents: ', s) 179 | --- end, function(err) 180 | --- print('Error', err) 181 | --- end) 182 | function M.new(options) 183 | if isfunction(options) then 184 | local d = M.new() 185 | local ok, err = pcall(options, d) 186 | if not ok then 187 | d:reject(err) 188 | end 189 | return d 190 | end 191 | options = options or {} 192 | local d 193 | d = { 194 | next = function(self, success, failure) 195 | local next = M.new({success = success, failure = failure, extend = options.extend}) 196 | if d.state == RESOLVED then 197 | next:resolve(d.value) 198 | elseif d.state == REJECTED then 199 | next:reject(d.value) 200 | else 201 | table.insert(d.queue, next) 202 | end 203 | return next 204 | end, 205 | state = 0, 206 | queue = {}, 207 | success = options.success, 208 | failure = options.failure, 209 | } 210 | d = setmetatable(d, deferred) 211 | if isfunction(options.extend) then 212 | options.extend(d) 213 | end 214 | return d 215 | end 216 | 217 | --- Returns a new promise object that is resolved when all promises are resolved/rejected. 218 | --- @param args list of promise 219 | --- @treturn Promise New promise 220 | --- @usage 221 | --- deferred.all({ 222 | --- http.get('http://example.com/first'), 223 | --- http.get('http://example.com/second'), 224 | --- http.get('http://example.com/third'), 225 | --- }):next(function(results) 226 | --- -- handle results here (all requests are finished and there has been 227 | --- -- no errors) 228 | --- end, function(results) 229 | --- -- handle errors here (all requests are finished and there has been 230 | --- -- at least one error) 231 | --- end) 232 | function M.all(args) 233 | local d = M.new() 234 | if #args == 0 then 235 | return d:resolve({}) 236 | end 237 | local method = "resolve" 238 | local pending = #args 239 | local results = {} 240 | 241 | local function synchronizer(i, resolved) 242 | return function(value) 243 | results[i] = value 244 | if not resolved then 245 | method = "reject" 246 | end 247 | pending = pending - 1 248 | if pending == 0 then 249 | d[method](d, results) 250 | end 251 | return value 252 | end 253 | end 254 | 255 | for i = 1, pending do 256 | args[i]:next(synchronizer(i, true), synchronizer(i, false)) 257 | end 258 | return d 259 | end 260 | 261 | --- Returns a new promise object that is resolved with the values of sequential application of function fn to each element in the list. fn is expected to return promise object. 262 | --- @function map 263 | --- @param args list of promise 264 | --- @param fn promise used to resolve the list of promise 265 | --- @return a new promise 266 | --- @usage 267 | --- local items = {'a.txt', 'b.txt', 'c.txt'} 268 | --- -- Read 3 files, one by one 269 | --- deferred.map(items, read):next(function(files) 270 | --- -- here files is an array of file contents for each of the files 271 | --- end, function(err) 272 | --- -- handle reading error 273 | --- end) 274 | function M.map(args, fn) 275 | local d = M.new() 276 | local results = {} 277 | local function donext(i) 278 | if i > #args then 279 | d:resolve(results) 280 | else 281 | fn(args[i]):next(function(res) 282 | table.insert(results, res) 283 | donext(i+1) 284 | end, function(err) 285 | d:reject(err) 286 | end) 287 | end 288 | end 289 | donext(1) 290 | return d 291 | end 292 | 293 | --- Returns a new promise object that is resolved as soon as the first of the promises gets resolved/rejected. 294 | --- @param args list of promise 295 | --- @treturn Promise New promise 296 | --- @usage 297 | --- -- returns a promise that gets rejected after a certain timeout 298 | --- function timeout(sec) 299 | --- local d = deferred.new() 300 | --- settimeout(function() 301 | --- d:reject('Timeout') 302 | --- end, sec) 303 | --- return d 304 | --- end 305 | --- 306 | --- deferred.first({ 307 | --- read(somefile), -- resolves promise with contents, or rejects with error 308 | --- timeout(5), 309 | --- }):next(function(result) 310 | --- -- file was read successfully... 311 | --- end, function(err) 312 | --- -- either timeout or I/O error... 313 | --- end) 314 | function M.first(args) 315 | local d = M.new() 316 | for _, v in ipairs(args) do 317 | v:next(function(res) 318 | d:resolve(res) 319 | end, function(err) 320 | d:reject(err) 321 | end) 322 | end 323 | return d 324 | end 325 | 326 | --- A promise is an object that can store a value to be retrieved by a future object. 327 | --- @type Promise 328 | 329 | --- Wait for the promise object. 330 | --- @function next 331 | --- @tparam function cb resolve callback (function(value) end) 332 | --- @tparam[opt] function errcb rejection callback (function(reject_value) end) 333 | --- @usage 334 | --- -- Reading two files sequentially: 335 | --- read('first.txt'):next(function(s) 336 | --- print('File file:', s) 337 | --- return read('second.txt') 338 | --- end):next(function(s) 339 | --- print('Second file:', s) 340 | --- end):next(nil, function(err) 341 | --- -- error while reading first or second file 342 | --- print('Error', err) 343 | --- end) 344 | 345 | --- Resolve promise object with value. 346 | --- @function resolve 347 | --- @param value promise value 348 | --- @return resolved future result 349 | 350 | --- Reject promise object with value. 351 | --- @function reject 352 | --- @param value promise value 353 | --- @return rejected future result 354 | 355 | return M 356 | -------------------------------------------------------------------------------- /l/tools/shapers.lua: -------------------------------------------------------------------------------- 1 | 2 | local EPSILON = 0.00001 3 | 4 | local function exponential_ease(x, a) 5 | 6 | local min_a = 0 + EPSILON 7 | local max_a = 1 - EPSILON 8 | a = math.max(min_a, math.min(a, max_a)) 9 | 10 | if a < 0.5 then 11 | a = 2 * a 12 | return math.pow(x, a) 13 | else 14 | a = 2 * (a - 0.5) 15 | return math.pow(x, 1/(1-a)) 16 | end 17 | 18 | end 19 | 20 | return { 21 | exponential_ease = exponential_ease, 22 | } 23 | -------------------------------------------------------------------------------- /l/tools/sstr.lua: -------------------------------------------------------------------------------- 1 | 2 | -- a simple library for working with strings in lua 3 | 4 | -- always use this table to represent the empty sstr 5 | -- TODO: is this safe? 6 | local sstr_empty = {} 7 | 8 | local function sstr_from_string(str) 9 | -- store the string as a list because it's costly in lua to keep 10 | -- splitting and splicing and joining etc regular strings. 11 | local ret = {} 12 | for i=1, #str do 13 | ret[i] = string.sub(str, i, i) 14 | end 15 | return ret 16 | end 17 | 18 | local function sstr_to_string(sstr) 19 | return table.concat(sstr) 20 | end 21 | 22 | local function sstr_length(sstr) 23 | return #sstr 24 | end 25 | 26 | local function sstr_print(sstr) 27 | for _, c in ipairs(sstr) do 28 | io.stdout:write(c) 29 | end 30 | io.stdout:write("\n") 31 | end 32 | 33 | -- returns a new value of type "sstr" that is composed of the characters 34 | -- that exist in `sstr` between `p1` and `p2`. returns nil if it can't. 35 | local function sstr_slice(sstr, p1, p2) 36 | 37 | if p1 > p2 then return nil end 38 | if p1 > sstr_length(sstr) then return nil end 39 | 40 | if p2 > sstr_length(sstr) then p2 = #sstr end -- TODO: should this loop? 41 | 42 | local ret = {} 43 | 44 | for i=p1, p2 do 45 | table.insert(ret, sstr[i]) 46 | end 47 | 48 | return ret 49 | end 50 | 51 | local function _matches(sstr1, start, sstr2) 52 | if sstr_length(sstr2) > sstr_length(sstr1) then return false end 53 | if start > sstr_length(sstr1) - sstr_length(sstr2) + 1 then return false end 54 | -- print("checking matches:", start) 55 | 56 | -- print("does it match:", sstr_to_string(sstr1), start, sstr_to_string(sstr2)) 57 | -- print("start, end", start, start+sstr_length(sstr2)) 58 | -- for i=start, start + sstr_length(sstr2) do 59 | for i=1, sstr_length(sstr2) do 60 | -- print(sstr1[i + start - 1], sstr2[i]) 61 | if sstr1[i + start - 1] ~= sstr2[i] then 62 | -- print("doesnt") 63 | return false 64 | end 65 | end 66 | -- print("MATCH") 67 | return true 68 | end 69 | 70 | -- returns true if `sstr` starts with `sstr_with` 71 | local function sstr_starts_with(sstr, sstr_with) 72 | return _matches(sstr, 1, sstr_with) 73 | end 74 | 75 | local function sstr_ends_with(sstr, sstr_with) 76 | return _matches(sstr, sstr_length(sstr) - sstr_length(sstr_with) + 1, sstr_with) 77 | end 78 | 79 | local function sstr_find_iter(sstr, sstr_thing) 80 | 81 | local p = 1 82 | local max = sstr_length(sstr) - sstr_length(sstr_thing) + 1 83 | 84 | return function() 85 | while p <= max do 86 | if _matches(sstr, p, sstr_thing) then 87 | local ret = p 88 | p = p + sstr_length(sstr_thing) 89 | return ret 90 | else 91 | p = p + 1 92 | end 93 | end 94 | end 95 | end 96 | 97 | -- split `sstr` in a list of sstrs based on pattern `sstr_sep` 98 | local function sstr_split_iter(sstr, sstr_sep) 99 | 100 | local is_first = true 101 | local from = 1 102 | 103 | local find_next = sstr_find_iter(sstr, sstr_sep) 104 | local first_run = true 105 | local last_run = false 106 | 107 | return function() 108 | if last_run == true then return nil end -- exit the iterator 109 | 110 | local i = find_next() 111 | 112 | if first_run then 113 | first_run = false 114 | if i == 1 then 115 | from = i + sstr_length(sstr_sep) 116 | i = find_next() 117 | end 118 | end 119 | 120 | if i == nil then 121 | -- this can return nil, which will also correctly exit the iterator 122 | local s = sstr_slice(sstr, from, sstr_length(sstr)) 123 | last_run = true 124 | return s 125 | else 126 | local s = sstr_slice(sstr, from, i-1) or sstr_empty 127 | from = i + sstr_length(sstr_sep) 128 | return s 129 | end 130 | end 131 | end 132 | 133 | -- returns an iterator, which, upon being called, returns 134 | -- (index, character) pairs 135 | local function sstr_iter_char(sstr_value) 136 | return ipairs(sstr_value) 137 | -- local i = 1 138 | -- return function() 139 | -- while i <= sstr_length(sstr_value) do 140 | -- local c = sstr_value[i] 141 | -- i = i + 1 142 | -- return c 143 | -- end 144 | -- end 145 | end 146 | 147 | local function concat(sstr_list) 148 | local concatenated = {} 149 | for _, sstr_value in ipairs(sstr_list) do 150 | for _, c in sstr_iter_char(sstr_value) do 151 | table.insert(concatenated, c) 152 | end 153 | end 154 | return concatenated 155 | end 156 | 157 | return { 158 | empty = sstr_empty, 159 | 160 | from_string = sstr_from_string, 161 | to_string = sstr_to_string, 162 | 163 | concat = concat, 164 | starts_with = sstr_starts_with, 165 | ends_with = sstr_ends_with, 166 | slice = sstr_slice, 167 | length = sstr_length, 168 | print = sstr_print, 169 | find_iter = sstr_find_iter, 170 | split_iter = sstr_split_iter, 171 | } 172 | -------------------------------------------------------------------------------- /l/tools/table.lua: -------------------------------------------------------------------------------- 1 | 2 | -- takes a variable number of tables and crushes each one onto the previous one. 3 | -- Example: 4 | -- * we have t1, t2, t3 5 | -- * create t0 6 | -- * all properties of t1 go into t0 7 | -- * all properties of t2 go into t0 8 | -- * all properties of t3 go into t0 9 | -- * return t0 10 | local function crush(...) 11 | local t = {} 12 | 13 | local tbls = {...} 14 | for i=#tbls, 1, -1 do 15 | local tbl = tbls[i] 16 | 17 | for k, v in pairs(tbl) do 18 | if t[k] == nil then 19 | t[k] = v 20 | end 21 | end 22 | 23 | end 24 | 25 | return t 26 | end 27 | 28 | 29 | return { 30 | crush = crush, 31 | } 32 | 33 | -------------------------------------------------------------------------------- /l/tools/tracker.lua: -------------------------------------------------------------------------------- 1 | 2 | -- A tool for tracking elements. Most notably used in tracking elements 3 | -- under the mouse for emitting MouseEnterEvent and MouseLeaveEvent events. 4 | 5 | local function new() 6 | return { 7 | list = {}, 8 | mapping = {}, 9 | } 10 | end 11 | 12 | local function track(tracker, elem) 13 | table.insert(tracker.list, elem) 14 | tracker.mapping[elem.oak_private.id] = elem 15 | end 16 | 17 | local function reset(tracker) 18 | for k, elem in ipairs(tracker.list) do 19 | tracker.list[k] = nil 20 | tracker.mapping[elem.oak_private.id] = nil 21 | end 22 | end 23 | 24 | local function iter(tracker) 25 | local i = 0 26 | return function() 27 | i = i + 1 28 | return tracker.list[i] 29 | end 30 | end 31 | 32 | local function contains(tracker, elem) 33 | return tracker.mapping[elem.oak_private.id] ~= nil 34 | end 35 | 36 | return { 37 | new = new, 38 | 39 | track = track, 40 | reset = reset, 41 | iter = iter, 42 | contains = contains, 43 | } 44 | 45 | -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | 2 | # notes 3 | 4 | ### Cairo optimization idea 5 | as far as I know, cairo works in the way that: you draw something, then, if some part of the drawing changes, you mark that portion as "dirty", you draw your whole drawing again, and now cairo is going to take care to only draw in that specific portion that you marked. It's essentially an optimized painter's algorithm. 6 | The problem with this is with a scenario like the following: let's say you have two things you want to draw: a red circle in the middle of the screen, and then a blue rectangle that takes up the whole size of the screen. Then, you want to move the circle around. How would you update your drawing? 7 | * In the most unoptimized scenario, you would just start the whole drawing all over again: draw the circle, draw the rectangle, and now your drawing is updated. 8 | * In a slightly more optimized scenario, you would be able to mark as dirty the region of the screen where the circle previously WAS, and the region of the screen where the circle currently IS. Then, draw your whole drawing again, and the graphics library that you're using should only draw in the portions of the screen that you marked as dirty. This is what cairo does. 9 | * In the most optimized scenario, you would realize that since the rectangle takes up the whole size of the screen, there's no reason to even bother with the circle, so you DRAW NOTHING. How would you be able to optimize your graphics library to identify a scenario like this and correctly figure out which portions of the screen to update? I think you could do it this way: 10 | Instead of having a "linear" algorithm for drawing where you give your drawing library a list of commands to perform (as cairo does), you instead give it a tree of shapes and configurations. The graphics library stores one thing: the tree of the current drawing, and, if you want to have a new drawing, you create a new tree, give it that, and the graphics library should take care of figuring out what changed, and only drawing the portions of the screen that changed. This way you don't even need cairo's usual "clip" functionality to mark out portions of the screen that changed. Then, the graphics library starts comparing the trees from the TOP-MOST element down. If the trees are identical, the screen is not updated at all. In this way, the library would perfectly optimize away the aforementioned scenario by noticing that there's a big blue rectangle obstructing the whole screen. Therefore, in spite of the fact that the circle's x, y coordinates changed, draw nothing. 11 | 12 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #/usr/bin/bash 2 | busted --lua=lua5.1 tests 3 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {}, recompile ? false }: 2 | 3 | pkgs.mkShell { 4 | buildInputs = with pkgs; [ 5 | luarocks 6 | luajit 7 | cairo 8 | pango # needed for lgi 9 | libev 10 | pkg-config 11 | xorg.libX11 12 | xorg.libxcb 13 | xorg.xcbutil 14 | xcb-util-cursor 15 | xorg.xcbutilerrors 16 | xorg.xcbutilkeysyms 17 | libxkbcommon 18 | 19 | luajitPackages.lgi 20 | gobject-introspection 21 | ]; 22 | 23 | shellHook = '' 24 | # Have to do this jank to avoid escaping every quote in the build 25 | ${if recompile then "export RECOMPILE=1" else "export RECOMPILE=0" } 26 | 27 | if ! luarocks list --local --lua-version=5.1 | grep -q "terra"; then 28 | echo "Terra not installed, compiling" 29 | export RECOMPILE=1 30 | fi 31 | 32 | if [ "$RECOMPILE" = 1 ]; then 33 | # Set environment variables for the libraries 34 | export CAIRO_DIR=${pkgs.cairo} 35 | export EV_DIR=${pkgs.libev} 36 | export LUAJIT_5_1_DIR=${pkgs.luajit} 37 | export X11_DIR=${pkgs.xorg.libX11} 38 | export XCB_DIR=${pkgs.xorg.libxcb} 39 | 40 | # Some jank to extract the header directory to pass to the includes 41 | export XCB_UTIL_DIR=${pkgs.xorg.xcbutil} 42 | export XCB_UTIL_DRV_DIR=$(nix-store --query --deriver $XCB_UTIL_DIR) 43 | export XCB_HEADER_DIR=$(nix-store --query --outputs $XCB_UTIL_DRV_DIR | grep "dev") 44 | 45 | export XCB_CURSOR_DIR=${pkgs.xcb-util-cursor} 46 | export XCB_ERRORS_DIR=${pkgs.xorg.xcbutilerrors} 47 | export XCB_KEYSYMS_DIR=${pkgs.xorg.xcbutilkeysyms} 48 | export XKBCOMMON_DIR=${pkgs.libxkbcommon} 49 | export XKBCOMMON_X11_DIR=${pkgs.libxkbcommon} 50 | 51 | # Add LuaJIT include directory, 52 | export LUA_INCDIR=${pkgs.luajit}/include 53 | 54 | PKG_CONFIG_PATH=${pkgs.gobject-introspection} 55 | 56 | luarocks install --server=https://luarocks.org/dev tstation --lua-version=5.1 --local 57 | 58 | luarocks --lua-version=5.1 make --local \ 59 | CAIRO_DIR="$CAIRO_DIR" EV_DIR="$EV_DIR" \ 60 | LUAJIT_5_1_DIR="$LUAJIT_5_1_DIR" \ 61 | X11_DIR="$X11_DIR" XCB_DIR="$XCB_DIR" \ 62 | XCB_CURSOR_DIR="$XCB_CURSOR_DIR" \ 63 | XCB_ERRORS_DIR="$XCB_ERRORS_DIR" \ 64 | XCB_KEYSYMS_DIR="$XCB_KEYSYMS_DIR" \ 65 | XKBCOMMON_DIR="$XKBCOMMON_DIR" \ 66 | XKBCOMMON_X11_DIR="$XKBCOMMON_X11_DIR" \ 67 | LUA_INCDIR="$LUA_INCDIR" \ 68 | CFLAGS="-I$XCB_HEADER_DIR/include" 69 | else 70 | echo -e "\033[33mLibrary not recompiled, to recompile run 'nix-shell --arg recompile true'\033[0m" 71 | fi 72 | 73 | # As long as luajit stays on 5.1 we're safe (pretty likely) 74 | export LUA_PATH="$HOME/.luarocks/share/lua/5.1/?.lua;;" 75 | export LUA_CPATH="$HOME/.luarocks/lib/lua/5.1/?.so;;" 76 | ''; 77 | } 78 | -------------------------------------------------------------------------------- /showcase/green_background_red_ball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-montero/terra/1d6772532aa815445db0d35a36fba8765de3991e/showcase/green_background_red_ball.png -------------------------------------------------------------------------------- /showcase/made_with_terra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-montero/terra/1d6772532aa815445db0d35a36fba8765de3991e/showcase/made_with_terra.png -------------------------------------------------------------------------------- /showcase/mathgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-montero/terra/1d6772532aa815445db0d35a36fba8765de3991e/showcase/mathgraph.png -------------------------------------------------------------------------------- /showcase/whether.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-montero/terra/1d6772532aa815445db0d35a36fba8765de3991e/showcase/whether.png -------------------------------------------------------------------------------- /terra-dev-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "terra" 2 | version = "dev-1" 3 | source = { 4 | url = "git://github.com/chris-montero/terra.git" 5 | } 6 | description = { 7 | homepage = "http://github.com/chris-montero/terra", 8 | summary = "What if you didn't have to write Electron apps anymore?", 9 | detailed = "A sane, desktop-native, application development framework.", 10 | license = "MIT", 11 | } 12 | dependencies = { 13 | "lua ~> 5.1", 14 | "lgi ~> 0.9.2-1", 15 | "tstation ~> dev-1", 16 | "luv", 17 | } 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["terra.orchard"] = "l/orchard.lua", 22 | ["terra.sigtools"] = "l/sigtools.lua", 23 | ["terra.object"] = "l/object.lua", 24 | ["terra.element"] = "l/element.lua", 25 | 26 | -- luv abstraction 27 | -- TODO: should I go this route? 28 | -- ["terra.suv.tcp"] = "l/suv/tcp.lua", 29 | -- ["terra.suv.internal.util"] = "l/suv/internal/util.lua", 30 | -- ["terra.suv.internal.stream"] = "l/suv/internal/stream.lua", 31 | -- ["terra.suv.internal.handle"] = "l/suv/internal/handle.lua", 32 | 33 | -- EXPERIMENTAL: try to use plenary's async abstraction 34 | -- ["terra.async.uv_async"] = "l/async/uv_async.lua", 35 | -- ["terra.async.async"] = "l/async/async.lua", 36 | -- ["terra.async.rotate"] = "l/async/rotate.lua", 37 | 38 | -- EXPERIMENTAL: an api that just does what people actually want 39 | -- to do with tcp connections: 40 | -- * as a server: write data and listen for and accept connections. 41 | -- * as a client: write data to a host and port, and listen for data. 42 | ["terra.abonaments.tcp.client"] = "l/abonaments/tcp/client.lua", 43 | 44 | ["terra.puv"] = "l/puv.lua", 45 | 46 | -- terra input 47 | ["terra.input.click"] = "l/input/click.lua", 48 | ["terra.input.clickmap"] = "l/input/clickmap.lua", 49 | ["terra.input.clickbind"] = "l/input/clickbind.lua", 50 | ["terra.input.key"] = "l/input/key.lua", 51 | ["terra.input.keymap"] = "l/input/keymap.lua", 52 | ["terra.input.keybind"] = "l/input/keybind.lua", 53 | 54 | -- oak tools 55 | ["terra.oak.shape"] = "l/oak/shape.lua", 56 | ["terra.oak.source"] = "l/oak/source.lua", 57 | ["terra.oak.size"] = "l/oak/size.lua", 58 | ["terra.oak.align"] = "l/oak/align.lua", 59 | ["terra.oak.padding"] = "l/oak/padding.lua", 60 | ["terra.oak.border"] = "l/oak/border.lua", 61 | ["terra.oak.internal"] = "l/oak/internal.lua", 62 | 63 | -- oak element internals 64 | -- TODO: move all l/oak/elements/element.lua to l/oak/elements/internal.lua 65 | -- TODO: same with branches and leaves 66 | ["terra.oak.elements.internal"] = "l/oak/elements/internal.lua", 67 | ["terra.oak.elements.element"] = "l/oak/elements/element.lua", 68 | 69 | -- oak branches 70 | ["terra.oak.elements.branches.internal"] = "l/oak/elements/branches/internal.lua", 71 | ["terra.oak.elements.branches.branch"] = "l/oak/elements/branches/branch.lua", 72 | ["terra.oak.elements.branches.el"] = "l/oak/elements/branches/el.lua", 73 | ["terra.oak.elements.branches.horizontal"] = "l/oak/elements/branches/horizontal.lua", 74 | ["terra.oak.elements.branches.vertical"] = "l/oak/elements/branches/vertical.lua", 75 | ["terra.oak.elements.branches.root"] = "l/oak/elements/branches/root.lua", 76 | 77 | -- oak leaves 78 | ["terra.oak.elements.leaves.leaf"] = "l/oak/elements/leaves/leaf.lua", 79 | ["terra.oak.elements.leaves.bg"] = "l/oak/elements/leaves/bg.lua", 80 | ["terra.oak.elements.leaves.text"] = "l/oak/elements/leaves/text.lua", 81 | ["terra.oak.elements.leaves.svg"] = "l/oak/elements/leaves/svg.lua", 82 | 83 | -- terra tools 84 | ["terra.tools.table"] = "l/tools/table.lua", 85 | ["terra.tools.tracker"] = "l/tools/tracker.lua", 86 | ["terra.tools.shapers"] = "l/tools/shapers.lua", 87 | ["terra.tools.color"] = "l/tools/color.lua", 88 | ["terra.tools.enum"] = "l/tools/enum.lua", 89 | ["terra.tools.urn"] = "l/tools/urn.lua", 90 | ["terra.tools.promise"] = "l/tools/promise.lua", 91 | ["terra.tools.sstr"] = "l/tools/sstr.lua", 92 | 93 | -- platform-independent common code 94 | ["terra.platforms.common.window"] = "l/platforms/common/window.lua", 95 | ["terra.platform"] = { 96 | sources = { 97 | "c/src/platform.c", 98 | }, 99 | libraries = { 100 | }, 101 | incdirs = { 102 | "c/src", 103 | }, 104 | }, 105 | 106 | -- xcb platform code 107 | ["terra.platforms.xcb.app"] = "l/platforms/xcb/app.lua", 108 | ["terra.platforms.xcb.window"] = "l/platforms/xcb/window.lua", 109 | ["terra.platforms.xcb.scairo"] = { 110 | sources = { 111 | "c/src/lhelp.c", 112 | "c/src/util.c", 113 | 114 | "c/src/xcb/scairo.c", 115 | "c/src/xcb/xlhelp.c", 116 | "c/src/xcb/xcb_ctx.c", 117 | -- "c/src/util.c", 118 | }, 119 | libraries = { 120 | "xcb", 121 | "xcb-keysyms", 122 | "cairo", 123 | }, 124 | incdirs = { 125 | "c/src", 126 | }, 127 | }, 128 | ["terra.platforms.xcb.spixmap"] = { 129 | sources = { 130 | "c/src/lhelp.c", 131 | "c/src/util.c", 132 | 133 | "c/src/xcb/spixmap.c", 134 | "c/src/xcb/xlhelp.c", 135 | "c/src/xcb/xcb_ctx.c", 136 | }, 137 | libraries = { 138 | "xcb", 139 | "xcb-keysyms", 140 | "X11", 141 | }, 142 | incdirs = { 143 | "c/src", 144 | }, 145 | }, 146 | ["terra.platforms.xcb.swin"] = { 147 | sources = { 148 | "c/src/lhelp.c", 149 | "c/src/util.c", 150 | 151 | "c/src/xcb/swin.c", 152 | -- "c/src/xcb/util.c", 153 | "c/src/xcb/xcb_ctx.c", 154 | "c/src/xcb/xlhelp.c", 155 | "c/src/xcb/window.c" 156 | }, 157 | libraries = { 158 | "xcb", 159 | "xcb-keysyms", 160 | "xcb-cursor", 161 | "X11", 162 | }, 163 | incdirs = { 164 | "c/src", 165 | }, 166 | }, 167 | ["terra.platforms.xcb.ctx"] = { 168 | -- defines = { 169 | -- "DEBUG=1", 170 | -- }, 171 | sources = { 172 | "c/src/lhelp.c", 173 | "c/src/util.c", 174 | 175 | "c/src/xcb/context.c", 176 | "c/src/xcb/xcb_ctx.c", 177 | "c/src/xcb/xlhelp.c", 178 | "c/src/xcb/terra_xkb.c", 179 | "c/src/xcb/event.c", 180 | "c/src/xcb/xutil.c", 181 | "c/src/xcb/vidata.c", 182 | }, 183 | libraries = { 184 | "luajit-5.1", 185 | "xcb", 186 | "xcb-keysyms", 187 | "xcb-cursor", 188 | "xcb-errors", 189 | "cairo", 190 | "xkbcommon", 191 | "xkbcommon-x11", 192 | }, 193 | incdirs = { 194 | "c/src", 195 | }, 196 | }, 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /tests/tools/sstr_spec.lua: -------------------------------------------------------------------------------- 1 | 2 | local tt_sstr = require("terra.tools.sstr") 3 | 4 | describe("sstr", function() 5 | 6 | describe("from_string", function() 7 | it("correctly creates a sstr from a lua string", function() 8 | assert.is_same(tt_sstr.from_string("hello"), {'h', 'e', 'l', 'l', 'o'}) 9 | end) 10 | end) 11 | 12 | describe("to_string", function() 13 | it("correctly converts a 'sstr' back into a lua string", function() 14 | assert.is_same(tt_sstr.to_string({'h', 'e', 'l', 'l', 'o'}), "hello") 15 | end) 16 | end) 17 | 18 | describe("length", function() 19 | it("returns the correct length", function() 20 | assert.equals(tt_sstr.length(tt_sstr.from_string("hello wordl")), 11) 21 | end) 22 | end) 23 | 24 | describe("slice", function() 25 | it("returns nil if p2 is smaller than p1", function() 26 | assert.is_true(tt_sstr.slice(tt_sstr.from_string("hello"), 4, 2) == nil) 27 | end) 28 | it("returns nil if p1 is bigger than the length of the value", function() 29 | assert.is_true(tt_sstr.slice(tt_sstr.from_string("hello"), 8, 9) == nil) 30 | end) 31 | it("clamps p2 if it's longer than the length of the string", function() 32 | assert.is_same(tt_sstr.slice(tt_sstr.from_string("hello"), 2, 8), {'e', 'l', 'l', 'o'}) 33 | end) 34 | it("returns the correct result when it should", function() 35 | assert.is_same(tt_sstr.slice(tt_sstr.from_string("hello"), 2, 3), {'e', 'l'}) 36 | end) 37 | end) 38 | 39 | describe("starts_with", function() 40 | it("returns false if second parameter is longer than the first", function() 41 | assert.is_false(tt_sstr.starts_with(tt_sstr.from_string("/home"), tt_sstr.from_string("/home/dir"))) 42 | end) 43 | it("returns true when it should", function() 44 | assert.is_true(tt_sstr.starts_with(tt_sstr.from_string("/home/dir"), tt_sstr.from_string("/home"))) 45 | end) 46 | it("returns false when it should", function() 47 | assert.is_false(tt_sstr.starts_with(tt_sstr.from_string("/home/dir"), tt_sstr.from_string("no"))) 48 | end) 49 | end) 50 | 51 | describe("ends_with", function() 52 | it("returns false if second parameter is longer than the first", function() 53 | assert.is_false(tt_sstr.ends_with(tt_sstr.from_string("/home"), tt_sstr.from_string("/home/dir"))) 54 | end) 55 | it("returns true when it should", function() 56 | assert.is_true(tt_sstr.ends_with(tt_sstr.from_string("/home/dir"), tt_sstr.from_string("dir"))) 57 | end) 58 | it("returns false when it should", function() 59 | assert.is_false(tt_sstr.ends_with(tt_sstr.from_string("/home/dir"), tt_sstr.from_string("no"))) 60 | end) 61 | end) 62 | 63 | describe("find_iter", function() 64 | it("do", function() 65 | local to_compare = {1, 11, 17, 24} 66 | local result = {} 67 | for i in tt_sstr.find_iter(tt_sstr.from_string("__grabbing__your__gyatt__"), tt_sstr.from_string('__')) do 68 | table.insert(result, i) 69 | end 70 | assert.is_same(to_compare, result) 71 | end) 72 | end) 73 | 74 | describe("split_iter", function() 75 | it("returns the original sstr if there's no separator", function() 76 | local res = {} 77 | for sstr_value in tt_sstr.split_iter(tt_sstr.from_string("lmao"), tt_sstr.from_string('/')) do 78 | table.insert(res, sstr_value) 79 | end 80 | assert.is_same(res, {tt_sstr.from_string("lmao")}) 81 | end) 82 | it("correctly discards the separator if the sstr starts with it", function() 83 | local res = {} 84 | for sstr_value in tt_sstr.split_iter(tt_sstr.from_string("/home"), tt_sstr.from_string('/')) do 85 | table.insert(res, sstr_value) 86 | end 87 | assert.is_same(res, {tt_sstr.from_string("home")}) 88 | end) 89 | it("correctly discards the separator if the sstr starts with it multiple consecutive times", function() 90 | local res = {} 91 | for sstr_value in tt_sstr.split_iter(tt_sstr.from_string("//home"), tt_sstr.from_string('/')) do 92 | table.insert(res, sstr_value) 93 | end 94 | assert.is_same(res, {{}, tt_sstr.from_string("home")}) 95 | end) 96 | it("correctly splits a sstr even if it ends with the separator", function() 97 | local res = {} 98 | for sstr_value in tt_sstr.split_iter(tt_sstr.from_string("home/"), tt_sstr.from_string('/')) do 99 | table.insert(res, sstr_value) 100 | end 101 | assert.is_same(res, {tt_sstr.from_string("home")}) 102 | end) 103 | it("correctly splits a sstr even if it ends with the separator multiple consecutive times", function() 104 | local res = {} 105 | for sstr_value in tt_sstr.split_iter(tt_sstr.from_string("home//"), tt_sstr.from_string('/')) do 106 | table.insert(res, sstr_value) 107 | end 108 | assert.is_same(res, {tt_sstr.from_string("home"), {}}) 109 | end) 110 | it("correctly splits a simple sstr", function() 111 | local res = {} 112 | for sstr_value in tt_sstr.split_iter(tt_sstr.from_string("__a__"), tt_sstr.from_string('__')) do 113 | table.insert(res, sstr_value) 114 | end 115 | assert.is_same(res, {tt_sstr.from_string("a")}) 116 | end) 117 | it("correctly splits a more complex sstr", function() 118 | local res = {} 119 | for sstr_value in tt_sstr.split_iter(tt_sstr.from_string("__a__b__"), tt_sstr.from_string('__')) do 120 | table.insert(res, sstr_value) 121 | end 122 | assert.is_same(res, {tt_sstr.from_string("a"), tt_sstr.from_string("b")}) 123 | end) 124 | it("correctly splits a complex sstr", function() 125 | local res = {} 126 | for sstr_value in tt_sstr.split_iter(tt_sstr.from_string("__ab_b__c__d_eb_"), tt_sstr.from_string('_')) do 127 | table.insert(res, sstr_value) 128 | end 129 | assert.is_same(res, { 130 | {}, 131 | tt_sstr.from_string("ab"), 132 | tt_sstr.from_string("b"), 133 | {}, 134 | tt_sstr.from_string("c"), 135 | {}, 136 | tt_sstr.from_string("d"), 137 | tt_sstr.from_string("eb"), 138 | }) 139 | end) 140 | end) 141 | 142 | end) 143 | 144 | --------------------------------------------------------------------------------