#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; }