diff --git a/src/utils/meson.build b/src/utils/meson.build index c1bf78a640..e93c6f06cb 100644 --- a/src/utils/meson.build +++ b/src/utils/meson.build @@ -7,5 +7,6 @@ srcs += [ 'misc.c', 'statistics.c', 'str.c', + 'ui.c', ), ] diff --git a/src/utils/misc.h b/src/utils/misc.h index 1475a1964a..72469a1ad9 100644 --- a/src/utils/misc.h +++ b/src/utils/misc.h @@ -126,7 +126,7 @@ safe_isinf(double a) { #define to_i16_checked(val) \ ({ \ - int64_t __to_tmp = (val); \ + int64_t __to_tmp = (int64_t)(val); \ ASSERT_IN_RANGE(__to_tmp, INT16_MIN, INT16_MAX); \ (int16_t) __to_tmp; \ }) @@ -166,12 +166,16 @@ static inline uint16_t i64_to_u16_saturated(int64_t val) { } return (uint16_t)val; } +static inline uint16_t int_to_u16_saturated(int val) { + return i64_to_u16_saturated(val); +} #define to_u16_saturated(val) \ _Generic((val), \ double: double_to_u16_saturated, \ float: double_to_u16_saturated, \ uint64_t: u64_to_u16_saturated, \ + int: int_to_u16_saturated, \ default: i64_to_u16_saturated)((val)) static inline int32_t double_to_i32_saturated(double val) { @@ -375,4 +379,14 @@ static inline int timespec_get(struct timespec *ts, int base) { } #endif +static inline int long_cmp(const long a, const long b) { + return a < b ? -1 : a > b; +} + +/// Compare 2 timespecs. Return 1 if `x` is greater, -1 if `y` is greater, 0 if +/// they are equal. +static inline int timespec_cmp(struct timespec x, struct timespec y) { + return long_cmp(x.tv_sec, y.tv_sec) ?: long_cmp(x.tv_nsec, y.tv_nsec); +} + // vim: set noet sw=8 ts=8 : diff --git a/src/utils/ui.c b/src/utils/ui.c new file mode 100644 index 0000000000..a0e03e9339 --- /dev/null +++ b/src/utils/ui.c @@ -0,0 +1,302 @@ +#include +#include +#include +#include +#include + +#include "log.h" +#include "misc.h" +#include "ui.h" +#include "x.h" + +struct ui { + xcb_fontable_t normal_font; + xcb_fontable_t bold_font; +}; + +static xcb_pixmap_t +ui_message_box_draw_text(struct ui *ui, struct x_connection *c, xcb_window_t window, + struct ui_message_box_content *content) { + xcb_pixmap_t pixmap = x_new_id(c); + uint16_t width = + content->size.width > UINT16_MAX ? UINT16_MAX : (uint16_t)content->size.width; + uint16_t height = + content->size.height > UINT16_MAX ? UINT16_MAX : (uint16_t)content->size.height; + if (!XCB_AWAIT_VOID(xcb_create_pixmap, c->c, c->screen_info->root_depth, pixmap, + window, width, height)) { + return XCB_NONE; + } + + xcb_gcontext_t gc = x_new_id(c); + { + uint32_t mask = XCB_GC_FOREGROUND | XCB_GC_BACKGROUND; + uint32_t value_list[3] = {c->screen_info->black_pixel, + c->screen_info->black_pixel}; + + if (!XCB_AWAIT_VOID(xcb_create_gc, c->c, gc, pixmap, mask, value_list)) { + return XCB_NONE; + } + } + + xcb_poly_fill_rectangle( + c->c, pixmap, gc, 1, + &(xcb_rectangle_t){.x = 0, .y = 0, .width = width, .height = height}); + + const char yellow_name[] = "yellow"; + const char red_name[] = "red"; + auto r = XCB_AWAIT(xcb_alloc_named_color, c->c, c->screen_info->default_colormap, + ARR_SIZE(yellow_name) - 1, yellow_name); + if (r == NULL) { + return XCB_NONE; + } + auto yellow_pixel = r->pixel; + free(r); + r = XCB_AWAIT(xcb_alloc_named_color, c->c, c->screen_info->default_colormap, + ARR_SIZE(red_name) - 1, red_name); + if (r == NULL) { + return XCB_NONE; + } + auto red_pixel = r->pixel; + free(r); + + for (unsigned i = 0; i < content->num_lines; i++) { + auto line = &content->lines[i]; + uint32_t color = 0; + switch (line->color) { + case UI_COLOR_WHITE: color = c->screen_info->white_pixel; break; + case UI_COLOR_YELLOW: color = yellow_pixel; break; + case UI_COLOR_RED: color = red_pixel; break; + } + uint32_t mask = XCB_GC_FOREGROUND | XCB_GC_FONT; + uint32_t value_list[2] = { + color, line->style == UI_STYLE_BOLD ? ui->bold_font : ui->normal_font}; + int16_t x = (int16_t)(line->position.x > INT16_MAX ? INT16_MAX + : line->position.x), + y = (int16_t)(line->position.y > INT16_MAX ? INT16_MAX + : line->position.y); + xcb_change_gc(c->c, gc, mask, value_list); + xcb_image_text_8(c->c, (uint8_t)strlen(line->text), pixmap, gc, x, y, + line->text); + } + xcb_free_gc(c->c, gc); + return pixmap; +} +const int64_t FPS = 60; +bool ui_message_box_show(struct ui *ui, struct x_connection *c, + struct ui_message_box_content *content, unsigned timeout) { + struct timespec next_render; + if (clock_gettime(CLOCK_MONOTONIC, &next_render) < 0) { + log_error("Failed to get current time"); + return false; + } + struct timespec close_time = next_render; + close_time.tv_sec += (long)timeout; + + ivec2 size = ivec2_add( + content->size, (ivec2){(int)content->margin * 2, (int)content->margin * 2}); + size.width *= (int)content->scale; + size.height *= (int)content->scale; + + xcb_window_t win = x_new_id(c); + uint16_t width = to_u16_saturated(size.width), + height = to_u16_saturated(size.height), + inner_width = to_u16_saturated(content->size.width * (int)content->scale), + inner_height = to_u16_saturated(content->size.height * (int)content->scale); + int16_t margin = to_i16_checked(content->margin * content->scale); + + uint32_t mask = XCB_CW_BACK_PIXEL | XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK; + uint32_t values[3] = {c->screen_info->black_pixel, 1, + XCB_EVENT_MASK_KEY_RELEASE | XCB_EVENT_MASK_BUTTON_PRESS | + XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_POINTER_MOTION | + XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW}; + bool success = XCB_AWAIT_VOID(xcb_create_window, c->c, c->screen_info->root_depth, + win, c->screen_info->root, + /*x=*/1, /*y=*/1, width, height, + /*border_width=*/0, XCB_WINDOW_CLASS_INPUT_OUTPUT, + c->screen_info->root_visual, mask, values); + if (!success) { + return success; + } + auto rendered_content = ui_message_box_draw_text(ui, c, win, content); + xcb_render_picture_t content_picture = x_create_picture_with_visual_and_pixmap( + c, c->screen_info->root_visual, rendered_content, 0, NULL); + xcb_render_picture_t target_picture = x_create_picture_with_visual_and_pixmap( + c, c->screen_info->root_visual, win, 0, NULL); + if (content_picture == XCB_NONE || target_picture == XCB_NONE) { + return false; + } + + xcb_render_transform_t transform = { + .matrix11 = DOUBLE_TO_XFIXED(1.0F / (double)content->scale), + .matrix22 = DOUBLE_TO_XFIXED(1.0F / (double)content->scale), + .matrix33 = DOUBLE_TO_XFIXED(1.0), + }; + if (!XCB_AWAIT_VOID(xcb_render_set_picture_transform, c->c, content_picture, transform)) { + return false; + } + + if (!XCB_AWAIT_VOID(xcb_map_window, c->c, win)) { + xcb_destroy_window(c->c, win); + return false; + } + + xcb_generic_event_t *event; + bool quit = false; + while (!quit) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + + int wait_time = 0; + bool should_render = false; + if (timespec_cmp(now, next_render) < 0) { + wait_time = (int)((next_render.tv_sec - now.tv_sec) * 1000 + + (next_render.tv_nsec - now.tv_nsec) / 1000000); + } + struct pollfd fds = {.fd = xcb_get_file_descriptor(c->c), .events = POLLIN}; + poll(&fds, 1, wait_time); + clock_gettime(CLOCK_MONOTONIC, &now); + if (timespec_cmp(next_render, now) <= 0) { + should_render = true; + } + if (timespec_cmp(close_time, now) <= 0) { + quit = true; + } + while ((event = xcb_poll_for_event(c->c)) != NULL) { + switch (XCB_EVENT_RESPONSE_TYPE(event)) { + case XCB_EXPOSE: + xcb_render_fill_rectangles( + c->c, XCB_RENDER_PICT_OP_SRC, target_picture, + (xcb_render_color_t){.alpha = 0xffff}, 1, + (const xcb_rectangle_t[]){ + {.x = 0, .y = 0, .width = width, .height = height}}); + xcb_render_composite(c->c, XCB_RENDER_PICT_OP_SRC, + content_picture, XCB_NONE, + target_picture, 0, 0, 0, 0, margin, + margin, inner_width, inner_height); + break; + case XCB_KEY_RELEASE:; + xcb_key_release_event_t *kr = (xcb_key_release_event_t *)event; + + switch (kr->detail) { + case /*ESC*/ 9: quit = true; break; + } + break; + case XCB_ENTER_NOTIFY: + xcb_grab_keyboard(c->c, 0, win, XCB_CURRENT_TIME, + XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC); + break; + case XCB_LEAVE_NOTIFY: + xcb_ungrab_keyboard(c->c, XCB_CURRENT_TIME); + break; + } + free(event); + } + if (should_render) { + next_render.tv_sec = now.tv_sec; + next_render.tv_nsec = now.tv_nsec + 1000000000 / FPS; + if (next_render.tv_nsec >= 1000000000) { + next_render.tv_sec++; + next_render.tv_nsec -= 1000000000; + } + } + xcb_flush(c->c); + } + return true; +} + +static bool ui_message_box_line_extent(struct ui *ui, struct x_connection *c, + struct ui_message_box_line *line) { + auto len = (uint32_t)strlen(line->text); + if (len > UINT8_MAX) { + return false; + } + auto text16 = ccalloc(len, xcb_char2b_t); + auto font = line->style == UI_STYLE_BOLD ? ui->bold_font : ui->normal_font; + for (uint32_t i = 0; i < len; i++) { + text16[i].byte1 = 0; + text16[i].byte2 = (uint8_t)line->text[i]; + } + auto r = XCB_AWAIT(xcb_query_text_extents, c->c, font, len, text16); + free(text16); + + if (!r) { + return false; + } + line->size.width = r->overall_width; + line->size.height = r->font_ascent + r->font_descent; + line->position.y = r->font_ascent; + free(r); + return true; +} + +bool ui_message_box_content_plan(struct ui *ui, struct x_connection *c, + struct ui_message_box_content *content) { + if (content->margin > INT_MAX) { + log_error("Margin is too large"); + return false; + } + ivec2 size = {}; + for (unsigned i = 0; i < content->num_lines; i++) { + if (!ui_message_box_line_extent(ui, c, &content->lines[i])) { + return false; + } + content->lines[i].position.y += size.height; + size.height += + content->lines[i].size.height + (int)content->lines[i].pad_bottom; + if (content->lines[i].size.width > size.width) { + size.width = content->lines[i].size.width; + } + } + + content->size = size; + + for (unsigned i = 0; i < content->num_lines; i++) { + switch (content->lines[i].justify) { + case UI_JUSTIFY_LEFT: content->lines[i].position.x = 0; break; + case UI_JUSTIFY_CENTER: + content->lines[i].position.x = + (size.width - content->lines[i].size.width) / 2; + break; + case UI_JUSTIFY_RIGHT: + content->lines[i].position.x = + size.width - content->lines[i].size.width; + break; + } + } + return true; +} + +struct ui *ui_new(struct x_connection *c) { + const char normal_font[] = "fixed"; + const char bold_font[] = "-*-fixed-bold-*"; + auto ui = ccalloc(1, struct ui); + ui->normal_font = x_new_id(c); + ui->bold_font = x_new_id(c); + auto cookie1 = xcb_open_font_checked(c->c, ui->normal_font, + ARR_SIZE(normal_font) - 1, normal_font); + auto cookie2 = + xcb_open_font_checked(c->c, ui->bold_font, ARR_SIZE(bold_font) - 1, bold_font); + + xcb_generic_error_t *e = xcb_request_check(c->c, cookie1); + if (e != NULL) { + log_error_x_error(e, "Cannot open the fixed font"); + free(e); + return NULL; + } + e = xcb_request_check(c->c, cookie2); + if (e != NULL) { + ui->bold_font = ui->normal_font; + log_error_x_error(e, "Cannot open the bold font, falling back to normal " + "font"); + free(e); + } + return ui; +} + +void ui_destroy(struct ui *ui, struct x_connection *c) { + xcb_close_font(c->c, ui->normal_font); + if (ui->bold_font != ui->normal_font) { + xcb_close_font(c->c, ui->bold_font); + } + xcb_flush(c->c); +} diff --git a/src/utils/ui.h b/src/utils/ui.h new file mode 100644 index 0000000000..f74cd08b4f --- /dev/null +++ b/src/utils/ui.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2024 Yuxuan Shui + +#include + +struct x_connection; +struct ui; + +enum ui_colors { + UI_COLOR_WHITE, + UI_COLOR_YELLOW, + UI_COLOR_RED, +}; + +enum ui_style { UI_STYLE_NORMAL, UI_STYLE_BOLD }; + +enum ui_justify { UI_JUSTIFY_LEFT, UI_JUSTIFY_CENTER, UI_JUSTIFY_RIGHT }; + +struct ui_message_box_line { + enum ui_colors color; + enum ui_style style; + enum ui_justify justify; + ivec2 position; + ivec2 size; + unsigned pad_bottom; + const char *text; +}; + +struct ui_message_box_content { + unsigned num_lines; + ivec2 size; + unsigned margin; + unsigned scale; + struct ui_message_box_line lines[]; +}; + +/// Layout the content of a message box. +/// @return true if the layout is successful, false if an error occurred. +bool ui_message_box_content_plan(struct ui *ui, struct x_connection *c, + struct ui_message_box_content *content); +bool ui_message_box_show(struct ui *ui, struct x_connection *c, + struct ui_message_box_content *content, unsigned timeout); +/// Initialize necessary resources for displaying UI. +struct ui *ui_new(struct x_connection *c); +void ui_destroy(struct ui *ui, struct x_connection *c);