#include "teensy_ui.h" #include #include #include #include #include "teensy_list.h" #define TEXT_HEIGHT (8) #define MAX_LAYOUTS (16) #define MAX_WINDOWS (255) #define window() (uictx.active) #define layout() (window()->layout - 1) #define push_layout() (window()->layout++) #define pop_layout() (--window()->layout) #define padding() (uictx.style.padding) #define control_padding() (uictx.style.control_padding) #define title_bar_height() (uictx.style.title_bar_height) enum { CMD_RECT, CMD_TEXT, CMD_CLIP, CMD_IMAGE, }; typedef uint8_t Cmd_Kind; typedef struct { const char *text; ty_Recti rect; ty_Color color; ty_Image img; Cmd_Kind kind; tyui_Align align; } Cmd; typedef struct { int idx; int columns; float column_widths[TYUI_MAX_LAYOUT_SIZE]; int max_height; int total_height; int x; int width; } Layout; typedef struct { Cmd *cmds; Layout *layout; Layout layout_stack[MAX_LAYOUTS]; ty_Recti rect; uint32_t flags; bool focused; bool lmb_down; bool rmb_down; bool lmb_pressed; bool rmb_pressed; } Window; typedef struct { Window *active; Window root; Window windows[MAX_WINDOWS]; int window_order[MAX_WINDOWS]; int window_count; int layout_idx; int layout_size; tyui_Style style; const ty_Font *font; const float *layout; ty_Vec2i prev_mouse_pos; } Ctx; static tyui_Style default_style = { .fg_normal = ty_color(200, 200, 200), .bg_normal = ty_color(20, 20, 20), .fg_hover = ty_color(255, 255, 255), .bg_hover = ty_color(100, 100, 100), .fg_pressed = ty_color(150, 150, 150), .bg_pressed = ty_color(10, 10, 10), .win_bg = ty_color(48, 48, 48), .win_border = ty_color(24, 24, 24), .win_title = ty_color(64, 128, 128), .frame = ty_color(10, 10, 10), .title_bar_height = 12, .padding = 1, .control_padding = 2, .frame_size = 1, .grabber_size = 4, .slider_fmt = "%2d" }; static Ctx uictx; static void rect_cmd(ty_Recti rect, ty_Color color) { Cmd cmd = {}; cmd.kind = CMD_RECT; cmd.rect = rect; cmd.color = color; ty_list_append(window()->cmds, cmd); } static void text_cmd(const char *text, ty_Recti rect, tyui_Align align) { Cmd cmd = {}; cmd.kind = CMD_TEXT; cmd.text = text; cmd.rect = rect; cmd.align = align; ty_list_append(window()->cmds, cmd); } static void image_cmd(ty_Image img, ty_Recti rect) { Cmd cmd = {}; cmd.kind = CMD_IMAGE; cmd.img = img; cmd.rect = rect; ty_list_append(window()->cmds, cmd); } static void clip_cmd(ty_Recti rect) { Cmd cmd = {}; cmd.kind = CMD_CLIP; if (rect.w < 0 || rect.h < 0) rect = window()->rect; // This is a full screen clip cmd.rect = ty_recti_clamp(rect, window()->rect); ty_list_append(window()->cmds, cmd); } static void draw_frame(ty_Recti rect, ty_Color bg_col) { clip_cmd(TY_CLIP_NONE); rect_cmd(rect, uictx.style.frame); ty_Recti content_rect = ty_recti_shrink(rect, uictx.style.frame_size); rect_cmd(content_rect, bg_col); clip_cmd(content_rect); } static void advance_layout(void) { Layout *l = layout(); if (++l->idx < l->columns) return; l->total_height += l->max_height + padding(); l->idx = 0; l->max_height = 0; if (l == window()->layout_stack) l->x = padding(); else l->x = 0; } static int get_column_width(const Layout *l) { float width = l->column_widths[l->idx]; if (width < 0) return width + l->width - l->x + 1; if (width < 1) return (l->width - padding() * 2) * width; return width; } static ty_Recti next_rect(int height) { Layout *l = layout(); l->max_height = ty_max(l->max_height, height); float width = get_column_width(l); int x = 0; int y = 0; for (Layout *outer = window()->layout_stack; outer <= l; outer++) { x += outer->x; y += outer->total_height; } x += window()->rect.x; y += window()->rect.y; width = ty_max(width, 0); height = ty_max(height, 0); l->x += width + padding(); advance_layout(); return ty_recti(x, y, width, height); } static ty_Vec2i mouse_delta(void) { ty_Vec2i cur = ty_mouse_pos(); return ty_vec2i( cur.x - uictx.prev_mouse_pos.x, cur.y - uictx.prev_mouse_pos.y ); } static bool is_hovered(ty_Recti rect) { return ty_pointi_in_recti(ty_mouse_pos(), rect); } static Window create_window(ty_Recti rect, uint32_t flags) { Window win = {}; win.rect = rect; win.flags = flags; win.cmds = ty_list_create(); ty_list_reserve(win.cmds, 64); return win; } void tyui_init(const ty_Font *font) { assert(font); uint32_t root_flags = 0; root_flags |= TYUI_WIN_NORESIZE; root_flags |= TYUI_WIN_NOCLOSE; root_flags |= TYUI_WIN_NOMOVE; root_flags |= TYUI_WIN_INVISIBLE; uictx.root = create_window(ty_recti(0, 0, 256, 256), root_flags); uictx.style = default_style; uictx.font = font; uictx.active = &uictx.root; } void tyui_deinit(void) { for (int i = 0; i < uictx.window_count; i++) ty_list_free(uictx.windows[i].cmds); } void tyui_push_layout(const float *column_widths) { assert(column_widths); Layout *outer = layout(); Layout *new = push_layout(); memset(new, 0, sizeof(*new)); for (const float *width = column_widths; *width != 0; width++) new->columns++; memcpy( new->column_widths, column_widths, sizeof(*column_widths) * new->columns ); new->x = 0; if (new != window()->layout_stack) new->width = get_column_width(outer); else new->width = window()->rect.w; new->width -= padding(); } void tyui_pop_layout(void) { Layout *old = pop_layout(); Layout *top = layout(); top->max_height = ty_max(top->max_height, old->total_height - padding()); top->x += old->width + padding(); advance_layout(); } bool tyui_begin_window_ex(tyui_Window_Conf conf, bool *closed_ptr) { assert(conf.title); assert(conf.id); bool closed = false; if (closed_ptr) closed = *closed_ptr; if (window() != &uictx.root) { ty_log_err("cannot create window within another window"); return false; } if (closed) return !closed; if (*conf.id == 0) { *conf.id = ++uictx.window_count; uictx.windows[*conf.id - 1] = create_window(conf.rect, conf.flags); uictx.window_order[uictx.window_count - 1] = *conf.id; } Window *win = &uictx.windows[*conf.id - 1]; uictx.active = win; win->layout = win->layout_stack; if (win->flags & TYUI_WIN_INVISIBLE) goto done; ty_Vec2i mouse_pos = ty_mouse_pos(); ty_Vec2i md = mouse_delta(); mouse_pos.x -= md.x; mouse_pos.y -= md.y; bool hovering_resizer = false; // Grabbing resizer? { ty_Vec2i win_end = ty_recti_end(win->rect); ty_Recti resize_rect = ty_recti( win_end.x - 20, win_end.y - 20 + TEXT_HEIGHT, 20, 20 ); hovering_resizer = ty_pointi_in_recti(mouse_pos, resize_rect) || ty_pointi_in_recti(ty_mouse_pos(), resize_rect); if (win->lmb_down && hovering_resizer) { win->rect.w += md.x; win->rect.h += md.y; win->rect.w = ty_max(win->rect.w, conf.min_size.x); win->rect.h = ty_max(win->rect.h, conf.min_size.y); } } ty_Recti title_rect = win->rect; title_rect.h = title_bar_height(); // Grabbing title bar? { if ( win->lmb_down && (ty_pointi_in_recti(mouse_pos, title_rect) || ty_pointi_in_recti(ty_mouse_pos(), title_rect)) ) { win->rect.x += md.x; win->rect.y += md.y; title_rect = win->rect; title_rect.h = title_bar_height(); } } clip_cmd(TY_CLIP_NONE); rect_cmd(title_rect, uictx.style.win_title); text_cmd(conf.title, ty_recti_shrink(title_rect, 1), TYUI_ALIGN_LEFT); ty_Recti body_rect = win->rect; body_rect.y += title_bar_height(); body_rect.h -= title_bar_height(); rect_cmd(body_rect, uictx.style.win_border); rect_cmd(ty_recti_shrink(body_rect, 1), uictx.style.win_bg); if (hovering_resizer) { ty_Recti right_bar = ty_recti( win->rect.x + win->rect.w - 1, 0, 1, win->rect.y + win->rect.h ); rect_cmd(right_bar, uictx.style.win_title); ty_Recti bot_bar = ty_recti( 0, win->rect.y + win->rect.h - 1, win->rect.x + win->rect.w, 1 ); rect_cmd(bot_bar, uictx.style.win_title); } tyui_push_layout((float[]){-1, 0}); layout()->total_height = title_bar_height() + padding(); done: return !closed; } void tyui_end_window(void) { if (window() == &uictx.root) { ty_log_err("ending window with no window open"); return; } clip_cmd(TY_CLIP_NONE); uictx.active = &uictx.root; } void tyui_text_ex(tyui_Align align, const char *fmt, ...) { va_list args; va_start(args, fmt); char *text = ty_format_args(fmt, args); va_end(args); ty_Recti rect = next_rect(TEXT_HEIGHT); clip_cmd(rect); text_cmd(text, rect, align); } bool tyui_button(const char *text) { ty_Recti rect = next_rect(TEXT_HEIGHT + control_padding() * 2); ty_Color bg_col = uictx.style.bg_normal; bool clicked = false; if (is_hovered(rect)) { clicked = window()->lmb_pressed; bg_col = window()->lmb_down ? uictx.style.bg_pressed : uictx.style.bg_hover; } draw_frame(rect, bg_col); text_cmd(text, ty_recti_shrink(rect, control_padding()), TYUI_ALIGN_CENTER); return clicked; } void tyui_slider_ex( float min, float max, float *value_ptr, bool show_value, tyui_Align value_align ) { assert(value_ptr); const int grabber_size = uictx.style.grabber_size; ty_Recti rect = next_rect(TEXT_HEIGHT + control_padding() * 2); ty_Color bg_col = uictx.style.bg_normal; ty_Color fg_col = uictx.style.fg_normal; bool clicked = false; if (is_hovered(rect)) { clicked = window()->lmb_down; if (window()->lmb_down) { bg_col = uictx.style.bg_pressed; fg_col = uictx.style.fg_pressed; } else { bg_col = uictx.style.bg_hover; fg_col = uictx.style.fg_hover; } } if (clicked) { ty_Vec2i mp = ty_mouse_pos(); float x = mp.x - rect.x; float p = x / (rect.w - 1); *value_ptr = ty_clamp(p * (max - min) + min, min, max); } float value = *value_ptr; draw_frame(rect, bg_col); if (show_value) { text_cmd( ty_format(uictx.style.slider_fmt, (int)round(value)), ty_recti_shrink(rect, control_padding()), value_align ); } // Grabber { float p = (value - min) / max; int x = (rect.w - grabber_size) * p + rect.x; ty_Recti grabber_rect = ty_recti( x, rect.y, grabber_size, rect.h ); draw_frame(grabber_rect, fg_col); } } static void draw_window(Window *win) { for (size_t i = 0; i < ty_list_len(win->cmds); i++) { Cmd cmd = win->cmds[i]; switch (cmd.kind) { case CMD_RECT: ty_draw_rect(cmd.rect, cmd.color); break; case CMD_TEXT: { int text_width = ty_font_width(uictx.font, cmd.text); ty_Vec2i pos = ty_recti_start(cmd.rect); switch (cmd.align) { case TYUI_ALIGN_LEFT: break; case TYUI_ALIGN_RIGHT: pos.x += cmd.rect.w - text_width; break; case TYUI_ALIGN_CENTER: pos.x += (cmd.rect.w - text_width) / 2; break; } pos.y += (cmd.rect.h - ty_font_height(uictx.font)) / 2; ty_draw_text(uictx.font, pos, cmd.text); break; } case CMD_IMAGE: ty_draw_image(cmd.img, ty_recti_start(cmd.rect)); break; case CMD_CLIP: ty_draw_set_clip(cmd.rect); break; } } ty_draw_set_clip(TY_CLIP_NONE); ty_list_clear(win->cmds); } static void focus_window(tyui_Id win_id) { int idx = uictx.window_count - 1; for (; idx >= 0; idx--) { if (uictx.window_order[idx] == win_id) break; } if (idx < 0) ty_log_err("uh oh"); for (int i = idx; i < uictx.window_count - 1; i++) uictx.window_order[i] = uictx.window_order[i + 1]; uictx.window_order[uictx.window_count - 1] = win_id; uictx.windows[win_id - 1].focused = true; } static void update_windows(void) { for (int i = 0; i < uictx.window_count; i++) { Window *win = &uictx.windows[i]; win->lmb_down = false; win->rmb_down = false; win->lmb_pressed = false; win->rmb_pressed = false; win->focused = false; } for (int i = uictx.window_count - 1; i >= 0; i--) { tyui_Id win_id = uictx.window_order[i]; Window *win = &uictx.windows[win_id - 1]; if (!is_hovered(win->rect)) continue; win->lmb_down = ty_button_down(TY_BTN_DB_LMB); win->rmb_down = ty_button_down(TY_BTN_DB_RMB); win->lmb_pressed = ty_button_pressed(TY_BTN_DB_LMB); win->rmb_pressed = ty_button_pressed(TY_BTN_DB_RMB); if (win->lmb_down || win->rmb_down) focus_window(win_id); break; } } void tyui_draw(void) { draw_window(&uictx.root); for (int i = 0; i < uictx.window_count; i++) { tyui_Id win_id = uictx.window_order[i]; draw_window(&uictx.windows[win_id - 1]); } uictx.prev_mouse_pos = ty_mouse_pos(); update_windows(); }