#include "cairo.h" #include "rect.h" #include "timeutil.h" #include "window.h" #include "node.h" //#define PROF #include "prof.c" #include "time.h" #include #include #include #include #include #include void _window_process_xcb_event(UIWindow *window, xcb_generic_event_t *event); int window_invalidate_node(UIWindow *window, UINode *node) { if (node->flags & UI_NODE_COMPONENT) { if (!node->parent) { return -1; } node = node->parent; } UIRect local_rect = (UIRect){ .x = node->rect.x + node->window_rel_x, .y = node->rect.y + node->window_rel_y, .w = node->rect.w, .h = node->rect.h }; window->invalid_region = ui_rect_united(&window->invalid_region, &local_rect); return 0; } void window_free(UIWindow *window) { if (!window) return; if (window->_cairo_surface) cairo_surface_destroy(window->_cairo_surface); if (window->drw) cairo_destroy(window->drw); if (window->_xcb_connection) xcb_disconnect(window->_xcb_connection); node_free(window->root); free(window); } UIWindow *window_new(int width, int height) { bool fail = false; UIWindow *window = NULL; window = calloc(1, sizeof(UIWindow)); if (!window) { fail = true; goto done; } window->hovered = NULL; window->pressed = NULL; window->root = NULL; window->invalid_region = (UIRect){ 0, 0, 0, 0 }; window->_cairo_surface = NULL; window->_xcb_connection = NULL; window->_xcb_window_id = -1; window->drw = NULL; window->_xcb_connection = xcb_connect(NULL, NULL); if (xcb_connection_has_error(window->_xcb_connection) != 0) { fail = true; goto done; } const xcb_setup_t *setup = xcb_get_setup(window->_xcb_connection); xcb_screen_iterator_t iter = xcb_setup_roots_iterator(setup); xcb_screen_t *screen = iter.data; xcb_visualtype_t *visualtype = NULL; for (xcb_depth_iterator_t iter_depth = xcb_screen_allowed_depths_iterator(screen); iter_depth.rem; xcb_depth_next(&iter_depth)) { for (xcb_visualtype_iterator_t iter_visualtype = xcb_depth_visuals_iterator(iter_depth.data); iter_visualtype.rem; xcb_visualtype_next(&iter_visualtype)) { if (iter_visualtype.data->visual_id == screen->root_visual) { visualtype = iter_visualtype.data; break; } } if (visualtype) break; } if (!visualtype) { fail = true; goto done; } window->_xcb_window_id = xcb_generate_id(window->_xcb_connection); if (window->_xcb_window_id == (unsigned)-1) { fail = true; goto done; } xcb_create_window( window->_xcb_connection, // connection XCB_COPY_FROM_PARENT, // depth window->_xcb_window_id, // window id screen->root, // parent window 0, // x 0, // y width, // width height, // height 0, // border_width XCB_WINDOW_CLASS_INPUT_OUTPUT, // class screen->root_visual, // visual XCB_CW_EVENT_MASK, // value mask (uint32_t[]){ XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE } // value list ); xcb_map_window(window->_xcb_connection, window->_xcb_window_id); if (xcb_flush(window->_xcb_connection) <= 0) { fail = true; goto done; } window->_cairo_surface = cairo_xcb_surface_create(window->_xcb_connection, window->_xcb_window_id, visualtype, width, height); if (cairo_surface_status(window->_cairo_surface) != CAIRO_STATUS_SUCCESS) { fail = true; goto done; } window->drw = cairo_create(window->_cairo_surface); if (cairo_status(window->drw) != CAIRO_STATUS_SUCCESS) { fail = true; goto done; } done: if (fail) { window_free(window); return NULL; } return window; } bool window_flush_invalidated(UIWindow *window) { if (ui_rect_null(&window->invalid_region)) { return false; } cairo_push_group(window->drw); node_repaint(window->root, &window->invalid_region, true, 0, 0); window->invalid_region = (UIRect){ 0, 0, 0, 0 }; cairo_pop_group_to_source(window->drw); cairo_paint(window->drw); cairo_surface_flush(window->_cairo_surface); return true; } int window_attach_root(UIWindow *window, UINode *root) { if (!window || !root || window->root || !window->drw) { return -1; } root->window = window; root->drw = window->drw; root->width_policy = UI_SIZE_POLICY_STATIC; root->height_policy = UI_SIZE_POLICY_STATIC; window->root = root; return 0; } void _window_process_xcb_event(UIWindow *window, xcb_generic_event_t *event) { begin_clock("Process XCB event"); uint8_t event_type = event->response_type & ~0x80; switch (event_type) { case XCB_EXPOSE: { xcb_expose_event_t *ev = (xcb_expose_event_t*)event; UIRect region = { ev->x, ev->y, ev->width, ev->height }; window->invalid_region = ui_rect_united(&window->invalid_region, ®ion); window_invalidate_node(window, window->root); break; } case XCB_CONFIGURE_NOTIFY: { xcb_configure_notify_event_t *ev = (xcb_configure_notify_event_t*)event; UIRect new_rect = { 0, 0, ev->width, ev->height }; if (!ui_rect_equals(&window->root->rect, &new_rect)) { cairo_xcb_surface_set_size(window->_cairo_surface, ev->width, ev->height); window->root->rect.x = 0; window->root->rect.y = 0; window->root->rect.w = ev->width; window->root->rect.h = ev->height; node_dispatch(window->root, UI_EVENT_RELAYOUT, 0, NULL); node_dump(window->root, 0); } break; } case XCB_MOTION_NOTIFY: { xcb_motion_notify_event_t *ev = (xcb_motion_notify_event_t*)event; UINode *node = node_by_point(window->root, ev->event_x, ev->event_y); if (node && node != window->hovered) { UINode *previously_hovered = window->hovered; window->hovered = node; if (previously_hovered) { node_dispatch(previously_hovered, UI_EVENT_UNHOVERED, 0, NULL); } node_dispatch(node, UI_EVENT_HOVERED, 0, NULL); } break; } case XCB_BUTTON_RELEASE: /* through */ case XCB_BUTTON_PRESS: { xcb_button_press_event_t *ev = (xcb_button_press_event_t*)event; UINode *node = node_by_point(window->root, ev->event_x, ev->event_y); if (node) { enum UIEvent ui_event = UI_EVENT_BUTTON_LEFT_UPDATE; bool state = event_type == XCB_BUTTON_PRESS ? true : false; double delta = 0.0; switch (ev->detail) { case XCB_BUTTON_INDEX_3: { ui_event = UI_EVENT_BUTTON_RIGHT_UPDATE; break; } case XCB_BUTTON_INDEX_4: { // scroll up ui_event = UI_EVENT_SCROLL; delta = 20.0; break; } case XCB_BUTTON_INDEX_5: { // scroll down ui_event = UI_EVENT_SCROLL; delta = -20.0; break; } } UINode *n = node; while (n) { if (node_dispatch(n, ui_event, state, &delta) > 0) { break; } n = n->parent; } if (ui_event == UI_EVENT_BUTTON_LEFT_UPDATE) { if (state) { UINode *previously_pressed = window->pressed; window->pressed = node; if (previously_pressed) { node_dispatch(previously_pressed, UI_EVENT_UNPRESSED, 0, NULL); } node_dispatch(node, UI_EVENT_PRESSED, 0, NULL); } else { UINode *previously_pressed = window->pressed; window->pressed = NULL; if (previously_pressed) { node_dispatch(previously_pressed, UI_EVENT_UNPRESSED, 0, NULL); } } } } break; } default: { break; } } end_clock(); } int window_turn(UIWindow *window) { if (!window || !window->root || !window->_xcb_connection || !window->_cairo_surface || !window->drw) { return -1; } if (xcb_connection_has_error(window->_xcb_connection) != 0) { return -1; } if (cairo_surface_status(window->_cairo_surface) != CAIRO_STATUS_SUCCESS || cairo_status(window->drw) != CAIRO_STATUS_SUCCESS) { return -1; } bool has_looped_once = false; struct pollfd fds[UI_WINDOW_MAX_POLL] = {0}; for (int i = 0; i < UI_WINDOW_MAX_POLL; i++) { fds[i].fd = -1; fds[i].events = 0; fds[i].revents = 0; } fds[0].fd = xcb_get_file_descriptor(window->_xcb_connection); fds[0].events = POLLIN; int64_t frame_peak_ms = 0; int64_t frame_peak_last_measurement_ms = 0; for (;;) { int64_t poll_timeout; /* compute `poll_timeout` based on active timers */ { int64_t now = time_current_ms(); int64_t lowest = INT64_MAX; bool has_timer = false; for (int i = 0; i < UI_WINDOW_MAX_TIMERS; i++) { UIWindowTimer *timer = &window->timers[i]; if (timer->present) { has_timer = true; int64_t distance = (timer->started_at_ms + timer->duration_ms) - now; if (distance < lowest) { lowest = distance; } } } poll_timeout = has_timer ? lowest : -1; if (!has_looped_once) { // Makes sure that we handle any pending events before entering the loop for the first time poll_timeout = 0; } } { if (poll(fds, UI_WINDOW_MAX_POLL, poll_timeout) < 0) { fprintf(stderr, "err: window_turn(): poll() failed\n"); return -1; } } clear_summary(); int64_t frame_start_ms = time_current_ms(); for (int i = 0; i < UI_WINDOW_MAX_POLL; i++) { if (fds[i].revents & POLLNVAL) { fprintf(stderr, "err: window_turn(): poll got POLLNVAL\n"); return -1; } if (fds[i].revents & POLLERR) { fprintf(stderr, "err: window_turn(): poll got POLLERR\n"); return -1; } if (fds[i].revents & POLLHUP) { fprintf(stderr, "err: window_turn(): poll got POLLHUP\n"); return -1; } if ((i == 0 && fds[i].revents & POLLIN) || !has_looped_once) { xcb_generic_event_t *xcb_event; while ((xcb_event = xcb_poll_for_event(window->_xcb_connection))) { _window_process_xcb_event(window, xcb_event); free(xcb_event); } } } /* process finished timers */ { for (int j = 0; j < UI_WINDOW_MAX_TIMERS; j++) { UIWindowTimer *timer = &window->timers[j]; if (timer->present) { int64_t now = time_current_ms(); int64_t distance = (timer->started_at_ms + timer->duration_ms) - now; if (distance <= 0) { int64_t dt = time_current_ms() - timer->started_at_ms; UINode *target = timer->target; timer->present = false; timer->started_at_ms = 0; timer->duration_ms = 0; timer->target = NULL; if (target) { node_dispatch(target, UI_EVENT_TIMER_END, 0, &dt); } } } } } begin_clock("Flush invalidated widgets"); if (window_flush_invalidated(window)) { begin_clock("Flush painting results to XCB"); xcb_flush(window->_xcb_connection); end_clock(); } end_clock(); has_looped_once = true; int64_t frame_end_ms = time_current_ms(); int64_t frame_delta_ms = frame_end_ms - frame_start_ms; if (frame_delta_ms > frame_peak_ms || frame_end_ms - frame_peak_last_measurement_ms >= 3000) { frame_peak_last_measurement_ms = frame_end_ms; frame_peak_ms = frame_delta_ms; printf("peak frametime: %ldms\n", frame_peak_ms); } dump_summary(); } return 0; } UIWindowTimer *window_sched_timer(UIWindow *window, UINode *target, int64_t duration_ms) { for (int i = 0; i < UI_WINDOW_MAX_TIMERS; i++) { if (!window->timers[i].present) { UIWindowTimer *timer = &window->timers[i]; timer->present = true; timer->duration_ms = duration_ms; timer->target = target; timer->started_at_ms = time_current_ms(); return timer; } } return NULL; }