#include #include #include #include #include #include #include #include #include #include #include #include enum CommandType { COMMAND_NOTHING, COMMAND_DONE, COMMAND_BLOCK, COMMAND_SPAN, }; union Command { int type; struct { int type; int length; const char * data; } block; struct { int type; int start; int end; const char * uri; } span; }; struct State { int width; int height; int pointer_x; int pointer_y; int follow; // TODO: Rethink how pointer events are handled. xcb_connection_t * c; xcb_screen_t * s; xcb_visualtype_t * v; xcb_window_t w; cairo_t * ctx; // TODO-maybe: separate drawing context, application state and xcb state? cairo_surface_t * surface; }; struct Keyboard { struct xkb_context * ctx; struct xkb_keymap * keymap; int32_t device_id; struct xkb_state * state; }; enum Actions { ACTION_REDRAW = 1 << 0, ACTION_ARRANGE = 1 << 1, ACTION_CLOSE = 1 << 2, ACTION_POINTER = 1 << 3, ACTION_CLICK = 1 << 4, }; struct Layout { PangoFontDescription * desc; int size; int * offsets; PangoLayout ** v; }; int setup(struct State *); int setup_keyboard(struct State *, struct Keyboard *); void finalize(struct State *, struct Layout *); int handle(struct State *, struct Keyboard *, xcb_generic_event_t *); xcb_visualtype_t * find_visual(xcb_screen_t *); PangoAttribute * find_link_under(struct State *, PangoLayout *, PangoAttrList *, int, int); void arrange(struct State *, struct Layout *, union Command[]); void draw(struct State *, struct Layout *); PangoAttribute * attr_hyperlink_new(const char *); PangoAttribute * attr_string_copy(const PangoAttribute *); void attr_string_destroy(PangoAttribute *); gboolean attr_string_equal(const PangoAttribute *, const PangoAttribute *); PangoAttribute * attr_string_new(const PangoAttrClass *, const char *); static const int MARGIN = 20; static const char * const FONT = "Serif 16"; static PangoAttrType PANGO_CUSTOM_ATTR_HYPERLINK; static PangoAttrClass hyperlink_klass; // TODO: Separate almost-pango-internals somewhere to minimize clutter. #define _BLOCK(_txt) {.block = {.type = COMMAND_BLOCK, .length = sizeof(_txt) - 1, .data = _txt}} union Command body[] = { {.span = {.type = COMMAND_SPAN, .start = 28, .end = 39, .uri = "https://ignore.pl/consectetur"}}, _BLOCK( "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam auctor porta eros at tempus. Aliquam " "elementum lectus id mi fermentum, non consectetur urna lobortis." ), {.span = {.type = COMMAND_SPAN, .start = 10, .end = 19, .uri = "https://ignore.pl/dignissim"}}, _BLOCK("Phasellus dignissim rhoncus magna at imperdiet."), {.type = COMMAND_DONE}, }; #undef _BLOCK int main(int argc, const char ** argv) { (void) argc; (void) argv; PANGO_CUSTOM_ATTR_HYPERLINK = pango_attr_type_register("hyperlink"); hyperlink_klass = (PangoAttrClass){ PANGO_CUSTOM_ATTR_HYPERLINK, attr_string_copy, attr_string_destroy, attr_string_equal, }; struct State state; if (-1 == setup(&state)) { dprintf(2, "Could not connect to X server\n"); return 1; } struct Keyboard keys; if (-1 == setup_keyboard(&state, &keys)) { dprintf(2, "Could not set up XKB\n"); return 1; } struct Layout layout = {.desc = NULL, .size = 0, .v = NULL}; arrange(&state, &layout, body); for (;;) { xcb_generic_event_t * e = xcb_wait_for_event(state.c); // TODO: All events first, then flagged actions. int actions = handle(&state, &keys, e); // Obviously, right now this may have at most one action. if (ACTION_CLOSE & actions) break; // TODO: Arranging, rearranging and (obviously) reacting to pointer events are all different things. if ((ACTION_ARRANGE | ACTION_POINTER | ACTION_CLICK) & actions) arrange(&state, &layout, body); if ((ACTION_REDRAW | ACTION_POINTER) & actions) draw(&state, &layout); state.follow = 0; } finalize(&state, &layout); } int setup(struct State * state) { static const int initial_width = 800; static const int initial_height = 600; state->c = xcb_connect(NULL, NULL); if (xcb_connection_has_error(state->c)) return -1; state->s = xcb_setup_roots_iterator(xcb_get_setup(state->c)).data; state->w = xcb_generate_id(state->c); state->v = find_visual(state->s); const uint32_t mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK; const uint32_t values[] = { state->s->white_pixel, XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_BUTTON_PRESS, }; xcb_create_window( state->c, XCB_COPY_FROM_PARENT, state->w, state->s->root, 0, 0, initial_width, initial_height, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, state->s->root_visual, mask, values); state->surface = cairo_xcb_surface_create(state->c, state->w, state->v, initial_width, initial_height); state->ctx = cairo_create(state->surface); xcb_map_window(state->c, state->w); xcb_flush(state->c); state->pointer_x = 0; state->pointer_y = 0; state->follow = 0; return 0; } int setup_keyboard(struct State * state, struct Keyboard * keys) { unsigned char event; if (!xkb_x11_setup_xkb_extension( state->c, XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION, XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS, NULL, NULL, &event, NULL)) return -1; keys->ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); if (NULL == keys->ctx) return -1; keys->device_id = xkb_x11_get_core_keyboard_device_id(state->c); if (-1 == keys->device_id) return -1; keys->keymap = xkb_x11_keymap_new_from_device(keys->ctx, state->c, keys->device_id, XKB_KEYMAP_COMPILE_NO_FLAGS); if (NULL == keys->keymap) return -1; keys->state = xkb_x11_state_new_from_device(keys->keymap, state->c, keys->device_id); if (NULL == keys->state) return -1; return 0; } void finalize(struct State * state, struct Layout * layout) { pango_font_description_free(layout->desc); for (int i = 0; i < layout->size; ++i) g_object_unref(layout->v[i]); free(layout->v); free(layout->offsets); cairo_destroy(state->ctx); cairo_surface_destroy(state->surface); xcb_disconnect(state->c); } int handle(struct State * state, struct Keyboard * keys, xcb_generic_event_t * e) { int actions = 0; switch (XCB_EVENT_RESPONSE_TYPE(e)) { case XCB_KEY_PRESS: ; xcb_key_press_event_t * key = (xcb_key_press_event_t *) e; xkb_keysym_t sym = xkb_state_key_get_one_sym(keys->state, (xkb_keycode_t) key->detail); if (XKB_KEY_q == sym) actions = ACTION_CLOSE; break; case XCB_EXPOSE: actions |= ACTION_REDRAW; break; case XCB_MOTION_NOTIFY: ; actions |= ACTION_POINTER; xcb_motion_notify_event_t * motion = (xcb_motion_notify_event_t *) e; state->pointer_x = motion->event_x; state->pointer_y = motion->event_y; break; case XCB_BUTTON_PRESS: break; case XCB_BUTTON_RELEASE: ; xcb_button_release_event_t * button = (xcb_button_release_event_t *) e; if (1 == button->detail) state->follow = 1; actions |= ACTION_CLICK; break; case XCB_CONFIGURE_NOTIFY: ; actions |= ACTION_ARRANGE; xcb_configure_notify_event_t * conf = (xcb_configure_notify_event_t *) e; if (state->width != conf->width || state->height != conf->height) { cairo_xcb_surface_set_size(state->surface, conf->width, conf->height); state->width = conf->width; state->height = conf->height; } break; default: break; } free(e); return actions; } xcb_visualtype_t * find_visual(xcb_screen_t * screen) { xcb_depth_iterator_t d = xcb_screen_allowed_depths_iterator(screen); for (; d.rem; xcb_depth_next(&d)) { xcb_visualtype_iterator_t v = xcb_depth_visuals_iterator(d.data); for (; v.rem; xcb_visualtype_next(&v)) if (v.data->visual_id == screen->root_visual) return v.data; } return NULL; } PangoAttribute * find_link_under(struct State * state, PangoLayout * layout, PangoAttrList * attrs, int x, int y) { int index; int trailing; int found = pango_layout_xy_to_index(layout, x * PANGO_SCALE, y * PANGO_SCALE, &index, &trailing); PangoAttribute * attr = NULL; if (!found) return NULL; PangoAttrIterator * iter = pango_attr_list_get_iterator(attrs); for (int next = TRUE; next; next = pango_attr_iterator_next(iter)) { attr = pango_attr_iterator_get(iter, PANGO_ATTR_FOREGROUND); if (NULL == attr) continue; int start, end; pango_attr_iterator_range(iter, &start, &end); if (start > index || index > end) { attr = NULL; continue; } attr = pango_attr_foreground_new(0xffff, 0x0000, 0x0000); attr->start_index = start; attr->end_index = end; if (state->follow) { state->follow = 0; PangoAttribute * link = pango_attr_iterator_get(iter, PANGO_CUSTOM_ATTR_HYPERLINK); if (NULL != link) printf("%s\n", ((PangoAttrString *) link)->value); } goto done; } done: pango_attr_iterator_destroy(iter); return attr; } void arrange(struct State * state, struct Layout * layout, union Command body[]) { if (NULL == layout->desc) layout->desc = pango_font_description_from_string(FONT); int size = 0; for (union Command * c = body; COMMAND_DONE != c->type; ++c) if (COMMAND_BLOCK == c->type) size++; if (size != layout->size) { for (int i = 0; i < layout->size; ++i) g_object_unref(layout->v[i]); layout->v = malloc(sizeof(PangoLayout *) * size); // TODO: Free when changing data layout->offsets = malloc(sizeof(int) * size); if (NULL == layout->v || NULL == layout->offsets) abort(); // TODO: Be more graceful? for (int i = 0; i < size; ++i) layout->v[i] = pango_cairo_create_layout(state->ctx); layout->size = size; } PangoAttrList * attrs = pango_attr_list_new(); int i = 0; int offset = MARGIN; for (union Command * c = body; COMMAND_DONE != c->type; ++c) { switch (c->type) { case COMMAND_SPAN: ; PangoAttribute * attr = attr_hyperlink_new(c->span.uri); attr->start_index = c->span.start; attr->end_index = c->span.end; pango_attr_list_change(attrs, attr); attr = pango_attr_foreground_new(0x0000, 0x0000, 0xffff); attr->start_index = c->span.start; attr->end_index = c->span.end; pango_attr_list_change(attrs, attr); break; case COMMAND_BLOCK: ; attr = find_link_under(state, layout->v[i], attrs, state->pointer_x - MARGIN, state->pointer_y - MARGIN); if (NULL != attr) pango_attr_list_change(attrs, attr); pango_layout_set_attributes(layout->v[i], attrs); pango_attr_list_unref(attrs); attrs = pango_attr_list_new(); pango_layout_set_font_description(layout->v[i], layout->desc); pango_layout_set_wrap(layout->v[i], PANGO_WRAP_WORD); pango_layout_set_width(layout->v[i], (state->width - 2 * MARGIN) * PANGO_SCALE); pango_layout_set_text(layout->v[i], c->block.data, c->block.length); int height; pango_layout_get_pixel_size(layout->v[i], NULL, &height); layout->offsets[i] = offset; offset += height + MARGIN; i++; break; case COMMAND_NOTHING: default: break; } } pango_attr_list_unref(attrs); } void draw(struct State * state, struct Layout * layout) { cairo_set_source_rgb(state->ctx, 1., 1., 1.); cairo_paint(state->ctx); cairo_set_source_rgb(state->ctx, 0., 0., 0.); for (int i = 0; i < layout->size; ++i) { cairo_move_to(state->ctx, MARGIN, layout->offsets[i]); pango_cairo_update_layout(state->ctx, layout->v[i]); pango_cairo_show_layout(state->ctx, layout->v[i]); } cairo_surface_flush(state->surface); xcb_flush(state->c); } PangoAttribute * attr_hyperlink_new(const char * uri) { return attr_string_new(&hyperlink_klass, uri); } PangoAttribute * attr_string_copy(const PangoAttribute * src) { return attr_string_new(src->klass, ((PangoAttrString *) src)->value); } void attr_string_destroy(PangoAttribute * attr) { PangoAttrString * str = (PangoAttrString *) attr; g_free(str->value); g_slice_free(PangoAttrString, str); } gboolean attr_string_equal(const PangoAttribute * lhs, const PangoAttribute * rhs) { return 0 == strcmp(((PangoAttrString *) lhs)->value, ((PangoAttrString *) rhs)->value); } PangoAttribute * attr_string_new(const PangoAttrClass * klass, const char * uri) { PangoAttrString * str = g_slice_new(PangoAttrString); pango_attribute_init(&str->attr, klass); str->value = g_strdup(uri); return (PangoAttribute *) str; }