Compare commits

..

8 commits

Author SHA1 Message Date
hippoz
023c876641
update single header distribution 2023-05-24 23:03:05 +03:00
hippoz
ce685968b2
add small example program 2023-05-24 23:02:58 +03:00
hippoz
98288a60cd
add gitignore for example program 2023-05-24 23:02:00 +03:00
hippoz
ae80e1776f
fix text node extent invalidation 2023-05-24 23:00:17 +03:00
hippoz
f4fcca122b
allow for centering on box layout 2023-05-24 22:59:25 +03:00
hippoz
8b56dcd3dd
add convenience function to box layout to set all margins 2023-05-24 21:11:00 +03:00
hippoz
fd482ff455
add single-header bundler 2023-05-24 19:02:03 +03:00
hippoz
2a07dc1061
fix delete key for text editing 2023-05-21 20:10:47 +03:00
11 changed files with 2870 additions and 21 deletions

5
.gitignore vendored
View file

@ -1,2 +1,5 @@
compile_flags.txt compile_flags.txt
build/ build/
example/libraven.h
example/main
libraven.h

5
example/build.sh Normal file
View file

@ -0,0 +1,5 @@
#!/bin/sh
set -xe
gcc -Wall -Wextra -std=gnu99 $(pkg-config --cflags --libs xcb xcb-xkb cairo pangocairo xkbcommon xkbcommon-x11) -lm main.c -o main

58
example/main.c Normal file
View file

@ -0,0 +1,58 @@
#include "pango/pango-font.h"
#define RAVEN_IMPLEMENTATION
#include "libraven.h"
typedef struct AppState {
PangoFontDescription *desc;
} AppState;
UINode *main_view_scaffold(UINode *root, AppState *state)
{
{
background_node_new(root, UISlate50, 0);
UIBoxLayoutNode *layout = box_layout_new(root, UI_DIRECTION_VERTICAL);
box_layout_set_margins(layout, 36);
layout->gap = 12;
layout->justify_secondary_dimension = UI_BOX_LAYOUT_JUSTIFY_CENTER;
}
{
UINode *input = (UINode*)text_input_new(root);
background_node_new(input, UISlate200, 10);
input->width_policy = UI_SIZE_POLICY_STATIC;
input->height_policy = UI_SIZE_POLICY_GROW;
input->rect.w = 38;
UIBoxLayoutNode *layout = box_layout_new(input, UI_DIRECTION_HORIZONTAL);
layout->justify_secondary_dimension = UI_BOX_LAYOUT_JUSTIFY_CENTER;
layout->margin_left = 0;
UINode *input_text_container = node_new(input, "input_text_container");
UITextNode *input_text = text_node_new(input_text_container, state->desc, UINeutral950, NULL);
((UITextInputNode*)input)->text_node = input_text;
}
return root;
}
int main(void)
{
UIWindow *window = window_new(800, 600);
if (!window) {
fprintf(stderr, "err: failed to create window\n");
return EXIT_FAILURE;
}
AppState state ={
.desc = pango_font_description_from_string("Roboto:size=16")
};
UINode *root = node_new(NULL, "[root]");
window_attach_root(window, root);
main_view_scaffold(root, &state);
window_turn(window);
window_free(window);
return 0;
}

145
headerify.py Normal file
View file

@ -0,0 +1,145 @@
from pathlib import Path, PurePath
from collections import OrderedDict
import sys
bundle_header = """
// This is an automatically generated bundle of the Raven library. Do not edit this file directly.
// Please see the end of the file for licensing information.
//
// This is a header-only bundle of the Raven library. Add this file to your project and
// use `#define RAVEN_IMPLEMENTATION` before including to create the implementation.
// Raven depends on these libraries, please make sure you include them before building:
// xcb xcb-xkb cairo pangocairo xkbcommon xkbcommon-x11
// Compile command example:
// gcc -Wall -Wextra -std=gnu99 $(pkg-config --cflags --libs xcb xcb-xkb cairo pangocairo xkbcommon xkbcommon-x11) -lm main.c -o main
//
// For more information, please see: https://git.hippoz.xyz/hippoz/raven
//
"""
bundle_footer = """
// MIT License
// Copyright (c) 2023 hippoz
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
def get_local_includes(lines):
includes = []
for line_index, line_content in enumerate(lines):
line = line_content.strip()
if line.startswith("#include"):
inside_string = False
token = ""
for char in line:
if inside_string:
if char == '"':
break
else:
token += char
else:
if char == '"':
inside_string = True
if len(token):
includes.append({
"line_index": line_index,
"include_path": token
})
return includes
def recurse_header_dependencies(header_to_deps, header_name):
result = []
header_deps = header_to_deps[header_name]
for dep in header_deps:
result += recurse_header_dependencies(header_to_deps, dep)
result.append(header_name)
return result
def main(argc, argv) -> int:
if argc != 2:
print("usage: headerify source_dir")
return 1
source_directory = Path(argv[1])
header_files = list(source_directory.glob("*.h"))
source_files = list(source_directory.glob("*.c"))
# We need a mapping between the name of a header file and a list of header files that it depends on,
# since we will compute the order in which the header files need to be appended afterwards. We
# also need a mapping between the name of a header file and the content of that header file,
# because we need to comment out the lines that have `#include`s relative to the project, since
# we are bundling all of them together.
header_to_deps = {}
header_to_content = {}
for header_file in header_files:
header_file_name = header_file.name
with header_file.open() as f:
lines = f.readlines()
local_includes = get_local_includes(lines)
header_to_deps[header_file_name] = []
for local_include in local_includes:
# We keep track of the header files that this header file depends on
header_to_deps[header_file_name].append(local_include["include_path"])
# We'll comment out the includes because they're not needed in a single-header bundle
lines[local_include["line_index"]] = f"// {lines[local_include['line_index']].strip()} // -- commented because of single-header bundle\n"
header_to_content[header_file_name] = "".join(lines)
# Find the order we need to append the header files in based on their dependencies
recursed_deps = []
for header_file in header_files:
recursed_deps += recurse_header_dependencies(header_to_deps, header_file.name)
header_file_order = list(OrderedDict.fromkeys(recursed_deps))
# Find the order we need to append the source files in, based on their respective header files, for consistency
source_file_order = []
for header_file_name in header_file_order:
for source_file in source_files:
if source_file.stem == PurePath(header_file_name).stem:
source_file_order.append(source_file)
break
# Make sure any source files without a corresponding header are added at the start
for source_file in source_files:
if source_file not in source_file_order and source_file.stem != "main":
source_file_order.insert(0, source_file)
# We're now ready to stitch the final bundle together
bundle = ""
bundle += bundle_header
# Header files
bundle += "// ----- header files -----"
for header_file_name in header_file_order:
bundle += f"\n\n\n// --- {header_file_name} ---\n\n" + header_to_content[header_file_name]
bundle += "\n\n// ----- end header files -----\n\n\n\n"
# Source files
bundle += "// ----- source files -----\n#ifdef RAVEN_IMPLEMENTATION\n#define RAVEN_IMPLEMENTATION\n\n"
for source_file in source_file_order:
with source_file.open() as f:
lines = f.readlines()
local_includes = get_local_includes(lines)
for inc in local_includes:
# We'll comment out the includes because they're not needed in a single-header bundle
lines[inc["line_index"]] = f"// {lines[inc['line_index']].strip()} // -- commented because of single-header bundle\n"
bundle += f"\n\n\n// --- {source_file.name} ---\n\n" + "".join(lines)
bundle += "\n\n#endif\n// ----- end source files -----\n\n"
bundle += bundle_footer
print(bundle)
if __name__ == "__main__":
sys.exit(main(len(sys.argv), sys.argv))

2613
libraven.h Normal file

File diff suppressed because it is too large Load diff

View file

@ -118,7 +118,11 @@ int box_layout_handle(UINode *component, enum UIEvent ev, size_t d, void *p)
x += gap; x += gap;
} }
current->rect.x = x; current->rect.x = x;
current->rect.y = y; if (box_layout_node->justify_secondary_dimension == UI_BOX_LAYOUT_JUSTIFY_CENTER) {
current->rect.y = margin_top + (maximum_secondary_position - current->rect.h) / 2;
} else {
current->rect.y = y;
}
x += current->rect.w; x += current->rect.w;
} else { } else {
if (current->height_policy == UI_SIZE_POLICY_GROW) { if (current->height_policy == UI_SIZE_POLICY_GROW) {
@ -130,7 +134,11 @@ int box_layout_handle(UINode *component, enum UIEvent ev, size_t d, void *p)
if (current_index) { if (current_index) {
y += gap; y += gap;
} }
current->rect.x = x; if (box_layout_node->justify_secondary_dimension == UI_BOX_LAYOUT_JUSTIFY_CENTER) {
current->rect.x = margin_left + (maximum_secondary_position - current->rect.w) / 2;
} else {
current->rect.x = x;
}
current->rect.y = y; current->rect.y = y;
y += current->rect.h; y += current->rect.h;
} }
@ -143,6 +151,14 @@ int box_layout_handle(UINode *component, enum UIEvent ev, size_t d, void *p)
return 0; return 0;
} }
void box_layout_set_margins(UIBoxLayoutNode *layout, double margin)
{
layout->margin_top = margin;
layout->margin_left = margin;
layout->margin_bottom = margin;
layout->margin_right = margin;
}
UIBoxLayoutNode *box_layout_new(UINode *parent, enum UIDirection direction) UIBoxLayoutNode *box_layout_new(UINode *parent, enum UIDirection direction)
{ {
UIBoxLayoutNode *n = malloc(sizeof(UIBoxLayoutNode)); UIBoxLayoutNode *n = malloc(sizeof(UIBoxLayoutNode));

View file

@ -3,13 +3,20 @@
#include "node.h" #include "node.h"
enum UIBoxLayoutJustify {
UI_BOX_LAYOUT_JUSTIFY_START,
UI_BOX_LAYOUT_JUSTIFY_CENTER
};
typedef struct UIBoxLayoutNode { typedef struct UIBoxLayoutNode {
UINode node; UINode node;
enum UIDirection direction; enum UIDirection direction;
enum UIBoxLayoutJustify justify_secondary_dimension;
double margin_top, margin_left, margin_bottom, margin_right, gap; double margin_top, margin_left, margin_bottom, margin_right, gap;
} UIBoxLayoutNode; } UIBoxLayoutNode;
UIBoxLayoutNode *box_layout_new(UINode *parent, enum UIDirection direction); UIBoxLayoutNode *box_layout_new(UINode *parent, enum UIDirection direction);
void box_layout_set_margins(UIBoxLayoutNode *layout, double margin);
int box_layout_handle(UINode *component, enum UIEvent ev, size_t d, void *p); int box_layout_handle(UINode *component, enum UIEvent ev, size_t d, void *p);
#endif // _UI__BOX_LAYOUT_NODE_H #endif // _UI__BOX_LAYOUT_NODE_H

View file

@ -183,7 +183,7 @@ int app_handle(struct UINode *node, void *data, int event_type, size_t d, void *
case UPDATE_COUNT: { case UPDATE_COUNT: {
state->count = d; state->count = d;
snprintf(state->counter_text, sizeof(state->counter_text), "%d", state->count); snprintf(state->counter_text, sizeof(state->counter_text), "%d", state->count);
state->counter_text_node->pending_text = state->counter_text; text_node_set_text(state->counter_text_node, state->counter_text);
node_request_relayout((UINode*)state->counter_text_node); node_request_relayout((UINode*)state->counter_text_node);
break; break;
} }

View file

@ -26,10 +26,6 @@ int text_input_handle(UINode *node, enum UIEvent ev, size_t d, void *p)
UITextInputNode *n = (UITextInputNode *)node; UITextInputNode *n = (UITextInputNode *)node;
switch (ev) { switch (ev) {
case UI_EVENT_ATTACHED: {
node_notify_subscribe(node->parent, node, (enum UIEvent[]){ UI_EVENT_KEY_DOWN, UI_EVENT_PRESSED, UI_EVENT_UNPRESSED, 0 });
break;
}
case UI_EVENT_KEY_DOWN: { case UI_EVENT_KEY_DOWN: {
xkb_keycode_t keycode = d; xkb_keycode_t keycode = d;
struct xkb_state *xkb_state = (struct xkb_state*)p; struct xkb_state *xkb_state = (struct xkb_state*)p;
@ -43,10 +39,10 @@ int text_input_handle(UINode *node, enum UIEvent ev, size_t d, void *p)
break; break;
} }
case XKB_KEY_Delete: { case XKB_KEY_Delete: {
if (n->text_cursor_index > 0 && n->text_cursor_index + 1 <= n->text.size) { if (n->text.size > 0 && n->text_cursor_index < n->text.size) {
ui_string_delete(&n->text, n->text_cursor_index + 1, 1); ui_string_delete(&n->text, n->text_cursor_index, 1);
} }
if (n->text_cursor_index > n->text.size) { if (n->text_cursor_index > 0 && n->text_cursor_index > n->text.size) {
n->text_cursor_index = n->text.size - 1; n->text_cursor_index = n->text.size - 1;
} }
break; break;
@ -87,7 +83,7 @@ int text_input_handle(UINode *node, enum UIEvent ev, size_t d, void *p)
} }
if (n->text_node) { if (n->text_node) {
n->text_node->pending_text = n->text.data; text_node_set_text(n->text_node, n->text.data);
n->text_node->caret_index = n->text_cursor_index; n->text_node->caret_index = n->text_cursor_index;
n->text_node->wrap = true; n->text_node->wrap = true;
node_request_relayout(&n->text_node->node); node_request_relayout(&n->text_node->node);

View file

@ -12,7 +12,7 @@ UITextNode *text_node_new(UINode *parent, PangoFontDescription *desc, UIRGBA col
UITextNode *n = malloc(sizeof(UITextNode)); UITextNode *n = malloc(sizeof(UITextNode));
node_init(&n->node); node_init(&n->node);
n->node.flags = UI_NODE_COMPONENT; n->node.flags = UI_NODE_COMPONENT;
n->pending_text = text; n->_pending_text = text;
n->desc = desc; n->desc = desc;
n->layout = NULL; n->layout = NULL;
n->color = color; n->color = color;
@ -25,6 +25,12 @@ UITextNode *text_node_new(UINode *parent, PangoFontDescription *desc, UIRGBA col
return n; return n;
} }
void text_node_set_text(UITextNode *text_node, char *text)
{
text_node->_pending_text = text;
text_node->node.cached_computed_extents = false;
}
int text_node_handle(UINode *node, enum UIEvent ev, size_t d, void *p) int text_node_handle(UINode *node, enum UIEvent ev, size_t d, void *p)
{ {
(void)d; (void)d;
@ -36,11 +42,11 @@ int text_node_handle(UINode *node, enum UIEvent ev, size_t d, void *p)
UITextNode *n = (UITextNode*)node; UITextNode *n = (UITextNode*)node;
if (n->pending_text) { if (n->_pending_text) {
if (n->layout) { if (n->layout) {
pango_layout_set_text(n->layout, n->pending_text, -1); pango_layout_set_text(n->layout, n->_pending_text, -1);
pango_cairo_update_layout(node->drw, n->layout); pango_cairo_update_layout(node->drw, n->layout);
n->pending_text = NULL; n->_pending_text = NULL;
} else { } else {
PangoLayout *layout = pango_cairo_create_layout(n->node.drw); PangoLayout *layout = pango_cairo_create_layout(n->node.drw);
pango_layout_set_font_description(layout, n->desc); pango_layout_set_font_description(layout, n->desc);
@ -49,11 +55,10 @@ int text_node_handle(UINode *node, enum UIEvent ev, size_t d, void *p)
} else { } else {
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
} }
pango_layout_set_text(layout, n->pending_text, -1); pango_layout_set_text(layout, n->_pending_text, -1);
n->layout = layout; n->layout = layout;
n->pending_text = NULL; n->_pending_text = NULL;
} }
node->parent->cached_computed_extents = false;
} }
switch (ev) { switch (ev) {
@ -118,7 +123,7 @@ int text_node_handle(UINode *node, enum UIEvent ev, size_t d, void *p)
g_object_unref(n->layout); g_object_unref(n->layout);
} }
n->layout = NULL; n->layout = NULL;
n->pending_text = NULL; n->_pending_text = NULL;
break; break;
} }
default: { default: {

View file

@ -11,7 +11,7 @@ typedef struct UITextNode {
UINode node; UINode node;
PangoFontDescription *desc; PangoFontDescription *desc;
PangoLayout *layout; PangoLayout *layout;
char *pending_text; char *_pending_text;
UIRGBA color; UIRGBA color;
ssize_t caret_index; ssize_t caret_index;
UINode *caret_node; UINode *caret_node;
@ -19,6 +19,7 @@ typedef struct UITextNode {
} UITextNode; } UITextNode;
int text_node_handle(UINode *node, enum UIEvent ev, size_t d, void *p); int text_node_handle(UINode *node, enum UIEvent ev, size_t d, void *p);
void text_node_set_text(UITextNode *text_node, char *text);
UITextNode *text_node_new(UINode *parent, PangoFontDescription *desc, UIRGBA color, char *text); UITextNode *text_node_new(UINode *parent, PangoFontDescription *desc, UIRGBA color, char *text);
#endif // _UI__TEXT_NODE_H #endif // _UI__TEXT_NODE_H