commit 2486c78f7cdc5a8a00c1b0af904989137b756127 Author: hippoz Date: Thu Dec 16 19:47:39 2021 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6cd342 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +ui + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..af05296 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +CFLAGS=-Wall -Wextra -std=c11 -pedantic `pkg-config --cflags cairo pangocairo` +LIBS=`pkg-config --libs cairo pangocairo` -lX11 + +ui: main.c + $(CC) $(CFLAGS) -o ui main.c $(LIBS) + diff --git a/main.c b/main.c new file mode 100644 index 0000000..0e46e60 --- /dev/null +++ b/main.c @@ -0,0 +1,384 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define M_PI 3.14159265358979323846 + +typedef struct Vec2 { + double x, y; +} Vec2; + +typedef struct RGB { + double r, g, b; +} RGB; + +typedef struct uui_row { + Vec2 starting_position, position, spacing, element_size; + bool is_vertical; +} uui_row_t; + +typedef struct uui_context { + int hot_id, active_id; + Vec2 mouse_pos; + bool mouse1_down; + cairo_t *cr; +} uui_context_t; + +typedef enum { + TEXT_CENTER_ALIGN, + TEXT_CENTER_ALIGN_LEFT, +} TextCenterAlign; + +typedef struct uui_text_style { + RGB text_color; + PangoFontDescription *font_description; + TextCenterAlign center_align; +} uui_text_style_t; + +typedef struct uui_button_style { + RGB normal_color, hot_color, active_color; + double border_radius; + int do_fill; + uui_text_style_t *text_style; +} uui_button_style_t; + +Vec2 vec2(double x, double y) { + return (Vec2) { + x, y + }; +} + +RGB rgb(double r, double g, double b) { + return (RGB) { + r, g, b + }; +} + +bool uui_collide_point_rect(Vec2 pos, Vec2 size, Vec2 point) { + return point.x >= pos.x && pos.x + size.x >= point.x && + point.y >= pos.y && pos.y + size.y >= point.y; +} + +void panic(int exit_code, const char *error) { + fputs(error, stderr); + exit(exit_code); +} + +cairo_surface_t *create_x11_surface(int x, int y) { + Display *dsp; + Drawable da; + int screen; + cairo_surface_t *sfc; + + if ((dsp = XOpenDisplay(NULL)) == NULL) + panic(1, "Failed to open display"); + + screen = DefaultScreen(dsp); + da = XCreateSimpleWindow(dsp, DefaultRootWindow(dsp), 0, 0, x, y, 0, 0, 0); + XSelectInput(dsp, da, ButtonPressMask | ButtonReleaseMask | KeyPressMask | PointerMotionMask); + XMapWindow(dsp, da); + + sfc = cairo_xlib_surface_create(dsp, da, DefaultVisual(dsp, screen), x, y); + cairo_xlib_surface_set_size(sfc, x, y); + + return sfc; +} + +void destroy_x11_surface(cairo_surface_t *sfc) { + Display *dsp = cairo_xlib_surface_get_display(sfc); + cairo_surface_destroy(sfc); + XCloseDisplay(dsp); +} + +void uui_handle_event(uui_context_t *uc, XEvent *e) { + char keybuf[8]; + KeySym key; + + switch (e->type) { + case ButtonPress: + if (e->xbutton.button == Button1) + uc->mouse1_down = true; + break; + case ButtonRelease: + if (e->xbutton.button == Button1) + uc->mouse1_down = false; + break; + case KeyPress: + XLookupString(&e->xkey, keybuf, sizeof(keybuf), &key, NULL); + break; + case MotionNotify: + uc->mouse_pos.x = e->xmotion.x; + uc->mouse_pos.y = e->xmotion.y; + break; + } +} + +void uui_draw_rect(cairo_t *cr, Vec2 pos, Vec2 size, double border_radius) +{ + double x = pos.x; + double y = pos.y; + double width = size.x; + double height = size.y; + if (border_radius > 0) { + double aspect = 1.0; + double corner_radius = border_radius; + double radius = corner_radius / aspect; + double degrees = M_PI / 180.0; + + cairo_new_sub_path(cr); + cairo_arc(cr, x + width - radius, y + radius, radius, -90 * degrees, 0 * degrees); + cairo_arc(cr, x + width - radius, y + height - radius, radius, 0 * degrees, 90 * degrees); + cairo_arc(cr, x + radius, y + height - radius, radius, 90 * degrees, 180 * degrees); + cairo_arc(cr, x + radius, y + radius, radius, 180 * degrees, 270 * degrees); + cairo_close_path(cr); + } else { + cairo_rectangle(cr, x, y, width, height); + } +} + +void uui_draw_text(cairo_t *cr, PangoFontDescription *desc, Vec2 pos, const char *text) { + PangoLayout *layout = pango_cairo_create_layout(cr); + pango_layout_set_font_description(layout, desc); + pango_layout_set_text(layout, text, -1); + cairo_move_to(cr, pos.x, pos.y); + pango_cairo_show_layout(cr, layout); + g_object_unref(layout); +} + +void uui_draw_centered_text(cairo_t *cr, PangoFontDescription *desc, TextCenterAlign align, Vec2 rect_pos, Vec2 rect_size, const char *text) { + PangoLayout *layout = pango_cairo_create_layout(cr); + int font_width; + int font_height; + + pango_layout_set_font_description(layout, desc); + pango_layout_set_text(layout, text, -1); + pango_layout_get_pixel_size(layout, &font_width, &font_height); + + double x = rect_pos.x + ((rect_size.x - font_width) / 2); + double y = rect_pos.y + ((rect_size.y - font_height) / 2); + + if (align == TEXT_CENTER_ALIGN_LEFT) { + // center the text only horizontally and add a hardcoded padding to the x position + // TODO: unhardcode the padding + x = rect_pos.x + 4; + } + + cairo_move_to(cr, x, y); + pango_cairo_show_layout(cr, layout); + g_object_unref(layout); +} + +bool uui_do_click_region(uui_context_t *uc, Vec2 pos, Vec2 size, int id) { + if (id != -1) { + /* if the element is hot (hovered) and mouse button 1 is down, it's clicked */ + if (uc->hot_id == id && uc->mouse1_down) { + /* this function only ever returns true once - when the click happens (unless id == -1, in which case it's checked each time, see below) */ + if (uc->active_id == id) { + return false; + } + uc->active_id = id; + return true; + } else { + uc->active_id = -1; + return false; + } + } else { + /* if the user provided -1 as an id, we will do the check independently of the hot/active system */ + return uc->mouse1_down && uui_collide_point_rect(pos, size, uc->mouse_pos); + } +} + +bool uui_do_hover_region(uui_context_t *uc, Vec2 pos, Vec2 size, int id) { + if (id != -1) { + if (uui_collide_point_rect(pos, size, uc->mouse_pos)) { + uc->hot_id = id; + return true; + } else { + uc->hot_id = -1; + return false; + } + } else { + /* if the user provided -1 as an id, we will do the check independently of the hot/active system */ + return uui_collide_point_rect(pos, size, uc->mouse_pos); + } +} + +void uui_do_text(uui_context_t *uc, uui_text_style_t *style, Vec2 pos, const char *text) { + cairo_save(uc->cr); + + cairo_set_source_rgb(uc->cr, style->text_color.r, style->text_color.g, style->text_color.b); + uui_draw_text(uc->cr, style->font_description, pos, text); + cairo_fill(uc->cr); + + cairo_restore(uc->cr); +} + +void uui_do_centered_text(uui_context_t *uc, uui_text_style_t *style, Vec2 pos, Vec2 size, const char *text) { + cairo_save(uc->cr); + + cairo_set_source_rgb(uc->cr, style->text_color.r, style->text_color.g, style->text_color.b); + uui_draw_centered_text(uc->cr, style->font_description, style->center_align, pos, size, text); + cairo_fill(uc->cr); + + cairo_restore(uc->cr); +} + +bool uui_do_button(uui_context_t *uc, uui_button_style_t *style, Vec2 pos, Vec2 size, const char *text, int id) { + bool clicked = false; + + cairo_save(uc->cr); + + if (uui_do_hover_region(uc, pos, size, id)) { + cairo_set_source_rgb(uc->cr, style->hot_color.r, style->hot_color.g, style->hot_color.b); + if (uui_do_click_region(uc, pos, size, id)) { + cairo_set_source_rgb(uc->cr, style->active_color.r, style->active_color.g, style->active_color.b); + clicked = true; + } + } else { + cairo_set_source_rgb(uc->cr, style->normal_color.r, style->normal_color.g, style->normal_color.b); + } + + uui_draw_rect(uc->cr, pos, size, style->border_radius); + + if (style->do_fill) { + cairo_fill(uc->cr); + } else { + cairo_stroke(uc->cr); + } + + if (text != NULL) { + uui_do_centered_text(uc, style->text_style, pos, size, text); + } + + cairo_restore(uc->cr); + + return clicked; +} + +uui_context_t *uui_context_create(cairo_t *cr) { + uui_context_t *uc = malloc(sizeof(uui_context_t)); + uc->mouse_pos = vec2(0, 0); + uc->mouse1_down = false; + uc->cr = cr; + return uc; +} + +void uui_context_destroy(uui_context_t *uc) { + free(uc); +} + +void uui_row_start(uui_row_t *row) { + row->position = row->starting_position; +} + +void uui_row_advance(uui_row_t *row, Vec2 add) { + /* advance the position, respecting the spacing, regardless of direction */ + row->position.x += add.x + row->spacing.x; + row->position.y += add.y + row->spacing.y; +} + +void uui_row_element(uui_row_t *row) { + /* increment position based on element size */ + /* the spacing is added regardless of the direction */ + if (row->is_vertical) { + row->position.y += row->element_size.y; + } else { + row->position.x += row->element_size.x; + } + row->position.x += row->spacing.x; + row->position.y += row->spacing.y; +} + +int main(void) { + cairo_surface_t *sfc = create_x11_surface(800, 600); + cairo_t *cr = cairo_create(sfc); + PangoFontDescription *font_description = pango_font_description_new(); + uui_context_t *uc = uui_context_create(cr); + uui_button_style_t button_style; + uui_text_style_t text_style; + uui_row_t row; + + XEvent e; + + pango_font_description_set_family(font_description, "sans-serif"); + pango_font_description_set_weight(font_description, PANGO_WEIGHT_NORMAL); + pango_font_description_set_absolute_size(font_description, 16 * PANGO_SCALE); + + button_style.border_radius = 8; + button_style.do_fill = 1; + button_style.normal_color = rgb(1.0, 0.0, 0.0); + button_style.hot_color = rgb(0.0, 1.0, 0.0); + button_style.active_color = rgb(0.0, 0.0, 1.0); + button_style.text_style = &text_style; + + text_style.text_color = rgb(1.0, 1.0, 1.0); + text_style.font_description = font_description; + text_style.center_align = TEXT_CENTER_ALIGN_LEFT; + + row.starting_position = vec2(20.0, 20.0); + row.position = vec2(0.0, 0.0); + row.spacing = vec2(20.0, 0.0); + row.element_size = vec2(100.0, 30.0); + row.is_vertical = false; + + int selected_tab = 0; + + for (;;) { + XNextEvent(cairo_xlib_surface_get_display(sfc), &e); + uui_handle_event(uc, &e); + + cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); + cairo_paint(cr); + + uui_row_start(&row); + if (uui_do_button(uc, &button_style, row.position, row.element_size, "one", 0)) { + selected_tab = 0; + } + uui_row_element(&row); + if (uui_do_button(uc, &button_style, row.position, row.element_size, "two", 1)) { + selected_tab = 1; + } + uui_row_element(&row); + if (uui_do_button(uc, &button_style, row.position, row.element_size, "three", 2)) { + selected_tab = 2; + } + uui_row_element(&row); + if (uui_do_button(uc, &button_style, row.position, row.element_size, "four", 3)) { + selected_tab = 3; + } + uui_row_element(&row); + + char *text = "no tab selected"; + + switch (selected_tab) { + case 0: + text = "tab 1"; + break; + case 1: + text = "tab 2"; + break; + case 2: + text = "tab 3"; + break; + case 3: + text = "tab 4"; + break; + } + + uui_do_text(uc, &text_style, row.position, text); + } + + pango_font_description_free(font_description); + cairo_destroy(cr); + destroy_x11_surface(sfc); + uui_context_destroy(uc); + return 0; +} +