Skip to content

Commit

Permalink
utils/ui: add utils for displaying message boxes
Browse files Browse the repository at this point in the history
This is work-in-progress and is not being used at the moment.

Signed-off-by: Yuxuan Shui <[email protected]>
  • Loading branch information
yshui committed Oct 16, 2024
1 parent 1006e95 commit a1b4a49
Show file tree
Hide file tree
Showing 4 changed files with 363 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/utils/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ srcs += [
'misc.c',
'statistics.c',
'str.c',
'ui.c',
),
]
16 changes: 15 additions & 1 deletion src/utils/misc.h
Original file line number Diff line number Diff line change
Expand Up @@ -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; \
})
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 :
302 changes: 302 additions & 0 deletions src/utils/ui.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <xcb/xcb_event.h>

#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);
}
45 changes: 45 additions & 0 deletions src/utils/ui.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2024 Yuxuan Shui <[email protected]>

#include <picom/types.h>

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);

0 comments on commit a1b4a49

Please sign in to comment.