/* Pressure-sensitive drawing using only YASL's portable API. */ #include #include #include #include #include #define PAPER 0xfff5f2eau #define INK 0xff315fadu #define PANEL 0xff172338u #define TEXT 0xffe5edf8u #define ACCENT 0xff59d5b6u #define HEADER 104 struct app { bool smoke_test, failed, tablet_supported; bool down, visible, previous; struct yasl_surface *surface; enum yasl_backend backend; struct { struct yasl_vector2i pos; enum yasl_pointer_device device; struct yasl_pointer_pen pen; } pointer; double last_x, last_y, last_radius; uint32_t *canvas; struct yasl_geometry geometry; }; static const char *backend_name(enum yasl_backend backend) { switch (backend) { case YASL_BACKEND_WAYLAND: return "WAYLAND"; case YASL_BACKEND_X11: return "X11"; case YASL_BACKEND_COCOA: return "COCOA"; default: return "AUTO"; } } static const char *device_name(enum yasl_pointer_device device) { switch (device) { case YASL_PTR_DEV_PEN: return "PEN"; case YASL_PTR_DEV_ERASER: return "ERASER"; case YASL_PTR_DEV_TOUCH: return "TOUCH"; default: return "MOUSE"; } } static void cancel_stroke(struct app *app) { app->down = app->previous = app->visible = false; app->pointer.pen = (struct yasl_pointer_pen){0}; } static bool resize_canvas(struct app *app, struct yasl_geometry geometry) { int w = geometry.size.x, h = geometry.size.y; if (w <= 0 || h <= 0 || !geometry.scale_120 || (size_t)w > SIZE_MAX / sizeof(uint32_t) / (size_t)h) return false; if (app->canvas && w == app->geometry.size.x && h == app->geometry.size.y && geometry.scale_120 == app->geometry.scale_120) return true; uint32_t *pixels = malloc((size_t)w * h * sizeof(*pixels)); if (!pixels) return false; double ratio = (double)app->geometry.scale_120 / geometry.scale_120; for (int y = 0; y < h; y++) { int old_y = (int)(y * ratio); for (int x = 0; x < w; x++) { int old_x = (int)(x * ratio); pixels[(size_t)y * w + x] = app->canvas && old_x < app->geometry.size.x && old_y < app->geometry.size.y ? app->canvas[(size_t)old_y * app->geometry.size.x + old_x] : PAPER; } } free(app->canvas); app->canvas = pixels; app->geometry = geometry; cancel_stroke(app); return true; } static void clear_canvas(struct app *app) { for (size_t i = 0, n = (size_t)app->geometry.size.x * app->geometry.size.y; i < n; i++) app->canvas[i] = PAPER; app->previous = false; } static double brush_radius(const struct app *app) { return ((double)app->geometry.scale_120 / YASL_SCALE_UNIT) * ((app->pointer.device == YASL_PTR_DEV_PEN || app->pointer.device == YASL_PTR_DEV_ERASER) ? 2 + 24 * ((double)app->pointer.pen.pressure / 65535) : 6); } static void stamp(struct app *app, double cx, double cy, double radius) { int w = app->geometry.size.x, h = app->geometry.size.y; int top = yasl_scale_to_device(HEADER, app->geometry.scale_120); int x0 = (int)fmax(0, fmin(w, floor(cx - radius))); int y0 = (int)fmax(top, fmin(h, floor(cy - radius))); int x1 = (int)fmax(0, fmin(w, ceil(cx + radius))); int y1 = (int)fmax(0, fmin(h, ceil(cy + radius))); uint32_t color = app->pointer.device == YASL_PTR_DEV_ERASER ? PAPER : INK; for (int y = y0; y < y1; y++) for (int x = x0; x < x1; x++) { double dx = x + 0.5 - cx, dy = y + 0.5 - cy; if (dx * dx + dy * dy <= radius * radius) app->canvas[(size_t)y * w + x] = color; } } static void stroke(struct app *app) { double x = app->pointer.pos.x, y = app->pointer.pos.y; double radius = brush_radius(app); /* Leaving the canvas breaks the line so re-entry cannot bridge the UI. */ if (x < 0 || x >= app->geometry.size.x || y < HEADER * ((double)app->geometry.scale_120 / YASL_SCALE_UNIT) || y >= app->geometry.size.y) { app->previous = false; return; } if (app->previous) { double dx = x - app->last_x, dy = y - app->last_y; int steps = (int)ceil(hypot(dx, dy)); for (int i = 1; i <= steps; i++) { double t = (double)i / steps; stamp(app, app->last_x + dx * t, app->last_y + dy * t, app->last_radius + (radius - app->last_radius) * t); } } /* Also stamp pressure-only motion, even when position has not changed. */ stamp(app, x, y, radius); app->last_x = x; app->last_y = y; app->last_radius = radius; app->previous = true; } static int input(void *ud, const struct yasl_event *event) { struct app *app = ud; if (event->type == YASL_EVENT_WINDOW_CLOSE_REQUEST) return YASL_APP_QUIT; if (event->type == YASL_EVENT_WINDOW_RESIZE) { if (!resize_canvas(app, (struct yasl_geometry){event->resize.size, event->resize.scale_120})) { app->failed = true; return YASL_APP_QUIT; } } else if (event->type == YASL_EVENT_INPUT_RESET || event->type == YASL_EVENT_MOUSE_LEAVE || (event->type == YASL_EVENT_WINDOW_FOCUS && !event->focus.focused)) { cancel_stroke(app); } else if (event->type == YASL_EVENT_KEY && event->key.action == YASL_KEY_PRESS) { if (event->key.key == YASL_KEY_ESCAPE) return YASL_APP_QUIT; if (event->key.key == YASL_KEY_C || event->key.key == YASL_KEY_SPACE) clear_canvas(app); } else if (event->type == YASL_EVENT_MOUSE_BUTTON) { if (app->pointer.device != event->mouse_button.device) cancel_stroke(app); app->pointer.pos = event->mouse_button.pos; app->pointer.device = event->mouse_button.device; app->pointer.pen = event->mouse_button.pen; app->visible = true; if (event->mouse_button.pressed && event->mouse_button.button == 0) { app->down = true; app->previous = false; stroke(app); } else if (!event->mouse_button.pressed && event->mouse_button.button == 0) { app->down = app->previous = false; app->pointer.pen.pressure = 0; } } else if (event->type == YASL_EVENT_MOUSE_MOTION) { if (app->pointer.device != event->mouse_motion.device) cancel_stroke(app); app->pointer.pos = event->mouse_motion.pos; app->pointer.device = event->mouse_motion.device; app->pointer.pen = event->mouse_motion.pen; app->visible = true; if (app->down) stroke(app); } else { return YASL_APP_CONTINUE; } yasl_surface_request_frame(event->surface); return YASL_APP_CONTINUE; } /* Tiny built-in 5x7 font keeps this example independent of a text library. */ static const uint8_t glyphs[128][7] = { ['A']={14,17,17,31,17,17,17}, ['B']={30,17,17,30,17,17,30}, ['C']={14,17,16,16,16,17,14}, ['D']={30,17,17,17,17,17,30}, ['E']={31,16,16,30,16,16,31}, ['F']={31,16,16,30,16,16,16}, ['G']={14,17,16,23,17,17,14}, ['H']={17,17,17,31,17,17,17}, ['I']={14,4,4,4,4,4,14}, ['J']={7,2,2,2,2,18,12}, ['K']={17,18,20,24,20,18,17}, ['L']={16,16,16,16,16,16,31}, ['M']={17,27,21,21,17,17,17}, ['N']={17,25,21,19,17,17,17}, ['O']={14,17,17,17,17,17,14}, ['P']={30,17,17,30,16,16,16}, ['Q']={14,17,17,17,21,18,13}, ['R']={30,17,17,30,20,18,17}, ['S']={15,16,16,14,1,1,30}, ['T']={31,4,4,4,4,4,4}, ['U']={17,17,17,17,17,17,14}, ['V']={17,17,17,17,17,10,4}, ['W']={17,17,17,21,21,21,10}, ['X']={17,17,10,4,10,17,17}, ['Y']={17,17,10,4,4,4,4}, ['Z']={31,1,2,4,8,16,31}, ['0']={14,17,19,21,25,17,14}, ['1']={4,12,4,4,4,4,14}, ['2']={14,17,1,2,4,8,31}, ['3']={30,1,1,14,1,1,30}, ['4']={2,6,10,18,31,2,2}, ['5']={31,16,16,30,1,1,30}, ['6']={14,16,16,30,17,17,14}, ['7']={31,1,2,4,8,8,8}, ['8']={14,17,17,14,17,17,14}, ['9']={14,17,17,15,1,1,14}, ['-']={0,0,0,31,0,0,0}, ['+']={0,4,4,31,4,4,0}, ['/']={1,1,2,4,8,16,16}, [':']={0,4,4,0,4,4,0}, ['.']={0,0,0,0,0,4,4}, ['%']={17,2,4,4,8,16,17}, }; static void rect(struct yasl_framebuffer *fb, int x, int y, int w, int h, uint32_t color) { int right = x + w < fb->size.x ? x + w : fb->size.x; int bottom = y + h < fb->size.y ? y + h : fb->size.y; for (int j = y > 0 ? y : 0; j < bottom; j++) for (int i = x > 0 ? x : 0; i < right; i++) fb->pixels[(size_t)j * fb->size.x + i] = color; } static void text(struct yasl_framebuffer *fb, double dpi, int x, int y, const char *str, uint32_t color) { for (; *str; str++, x += 12) { unsigned ch = (unsigned char)*str; if (ch >= 128) continue; for (int row = 0; row < 7; row++) for (int col = 0; col < 5; col++) { if (!(glyphs[ch][row] & (1 << (4 - col)))) continue; int left = (int)lround((x + col * 2) * dpi); int top = (int)lround((y + row * 2) * dpi); rect(fb, left, top, (int)lround((x + col * 2 + 2) * dpi) - left, (int)lround((y + row * 2 + 2) * dpi) - top, color); } } } static void draw(void *ud, struct yasl_surface *surface, struct yasl_frame_context *frame) { struct app *app = ud; struct yasl_geometry geometry = {frame->size, frame->scale_120}; if (!resize_canvas(app, geometry)) { fprintf(stderr, "Cannot allocate drawing canvas\n"); app->failed = true; yasl_surface_destroy(surface); app->surface = NULL; return; } struct yasl_framebuffer target = {.size = frame->size, .pixels = frame->cpu.pixels}; struct yasl_framebuffer *fb = ⌖ memcpy(fb->pixels, app->canvas, (size_t)geometry.size.x * geometry.size.y * sizeof(uint32_t)); double dpi = (double)app->geometry.scale_120 / YASL_SCALE_UNIT; rect(fb, 0, 0, fb->size.x, (int)lround(HEADER * dpi), PANEL); char line[128]; snprintf(line, sizeof(line), "YASL TABLET / %s / %s / %s", backend_name(app->backend), device_name(app->pointer.device), app->down ? "CONTACT" : "HOVER"); text(fb, dpi, 16, 12, line, TEXT); if (app->tablet_supported) snprintf(line, sizeof(line), "PRESSURE %5u / 65535 %5.1f%%", app->pointer.pen.pressure, app->pointer.pen.pressure * 100.0 / 65535); else snprintf(line, sizeof(line), "TABLET PROTOCOL UNAVAILABLE / MOUSE DRAWING"); text(fb, dpi, 16, 36, line, ACCENT); int bar_width = (int)lround(220 * dpi); rect(fb, (int)(16 * dpi), (int)(58 * dpi), bar_width, (int)(6 * dpi), 0xff34455eu); rect(fb, (int)(16 * dpi), (int)(58 * dpi), (int)((uint32_t)app->pointer.pen.pressure * (uint64_t)bar_width / 65535), (int)(6 * dpi), ACCENT); snprintf(line, sizeof(line), "TILT X %+d / Y %+d", app->pointer.pen.tilt_x, app->pointer.pen.tilt_y); text(fb, dpi, 260, 54, line, TEXT); text(fb, dpi, 16, 82, "DRAW WITH TIP / FLIP TO ERASE / C CLEAR / ESC QUIT", TEXT); if (app->visible && app->pointer.pos.x >= 0 && app->pointer.pos.x < fb->size.x && app->pointer.pos.y >= HEADER * dpi && app->pointer.pos.y < fb->size.y) { double radius = brush_radius(app); double cx = app->pointer.pos.x, cy = app->pointer.pos.y; /* Brush outline and tilt vector are overlays, never stored in canvas. */ for (int i = 0; i < 180; i++) { double angle = i * (6.283185307179586 / 180); rect(fb, (int)lround(cx + cos(angle) * radius), (int)lround(cy + sin(angle) * radius), 1, 1, PANEL); } double tx = app->pointer.pen.tilt_x * dpi / 2; double ty = app->pointer.pen.tilt_y * dpi / 2; for (int i = 0; i <= 64; i++) rect(fb, (int)lround(cx + tx * i / 64), (int)lround(cy + ty * i / 64), 2, 2, ACCENT); } if (!yasl_frame_present_cpu(frame, NULL, 0)) { app->failed = true; } if (app->failed || app->smoke_test) { yasl_surface_destroy(surface); app->surface = NULL; } } static int init(void *ud, struct yasl_display *display) { struct app *app = ud; app->backend = yasl_display_backend(display); app->tablet_supported = (yasl_display_caps(display) & YASL_CAP_TABLET) != 0; struct yasl_window_spec spec = { .title = "YASL Tablet - pressure and tilt", .size = {900, 640}, }; app->surface = yasl_surface_create_toplevel(display, &spec); if (!app->surface || !resize_canvas(app, yasl_surface_geometry(app->surface))) { fprintf(stderr, "Cannot create drawing surface\n"); app->failed = true; return YASL_APP_QUIT; } yasl_surface_set_mouse_mode(app->surface, YASL_MOUSE_HIDDEN); yasl_surface_request_frame(app->surface); printf("YASL tablet on %s: pressure changes brush width; eraser removes ink.\n" "Mouse: fixed-width drawing. C/Space: clear. Escape: quit.\n", backend_name(app->backend)); if (!app->tablet_supported) printf("This display server does not expose tablet input.\n"); return YASL_APP_CONTINUE; } int main(int argc, char **argv) { struct app app = {0}; enum yasl_backend backend = YASL_BACKEND_AUTO; for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "--smoke-test")) app.smoke_test = true; else if (!strcmp(argv[i], "--backend") && i + 1 < argc) { const char *name = argv[++i]; if (!strcmp(name, "auto")) backend = YASL_BACKEND_AUTO; else if (!strcmp(name, "wayland")) backend = YASL_BACKEND_WAYLAND; else if (!strcmp(name, "x11")) backend = YASL_BACKEND_X11; else if (!strcmp(name, "cocoa")) backend = YASL_BACKEND_COCOA; else { fprintf(stderr, "Unknown backend: %s\n", name); return 1; } } else { bool help = !strcmp(argv[i], "--help"); fprintf(help ? stdout : stderr, "Usage: %s [--backend auto|wayland|x11|cocoa] [--smoke-test]\n", argv[0]); return help ? 0 : 1; } } struct yasl_app_desc desc = { .app_id = "org.yasl.tablet", .backend = backend, .userdata = &app, .init = init, .event = input, .frame = draw, }; int result = yasl_run(&desc); if (result) perror("yasl_run"); free(app.canvas); return result || app.failed ? 1 : 0; }