diff options
Diffstat (limited to 'examples/tablet.c')
| -rw-r--r-- | examples/tablet.c | 355 |
1 files changed, 355 insertions, 0 deletions
diff --git a/examples/tablet.c b/examples/tablet.c new file mode 100644 index 0000000..4d7a8bd --- /dev/null +++ b/examples/tablet.c @@ -0,0 +1,355 @@ +/* Pressure-sensitive drawing using only YASL's portable API. */ +#include <yasl/yasl.h> + +#include <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#define PAPER 0xfff5f2eau +#define INK 0xff315fadu +#define PANEL 0xff172338u +#define TEXT 0xffe5edf8u +#define ACCENT 0xff59d5b6u +#define HEADER 104 + +struct app { + bool running, smoke_test, failed, tablet_supported; + bool down, visible, previous; + enum yasl_backend backend; + struct yasl_mouse_event 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 bool is_pen(enum yasl_pointer_device device) +{ + return device == YASL_PTR_DEV_PEN || device == YASL_PTR_DEV_ERASER; +} + +static double scale(const struct app *app) +{ + return (double)app->geometry.scale_120 / YASL_SCALE_UNIT; +} + +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 scale(app) * (is_pen(app->pointer.device) ? + 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 * scale(app) || 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 void input(struct app *app, const struct yasl_input_event *event) +{ + if (event->kind == YASL_INPUT_RESET || + (event->kind == YASL_INPUT_FOCUS && !event->focus.focused)) { + cancel_stroke(app); + } else if (event->kind == YASL_INPUT_KEY && event->key.action == YASL_KEY_PRESS) { + if (event->key.key == 0xff1b) app->running = false; + if (event->key.key == 'c' || event->key.key == 'C' || event->key.key == ' ') + clear_canvas(app); + } else if (event->kind == YASL_INPUT_MOUSE) { + const struct yasl_mouse_event *pointer = &event->mouse; + if (pointer->kind == YASL_PTR_SCROLL) return; + if (app->pointer.device != pointer->device) cancel_stroke(app); + app->pointer = *pointer; + app->visible = pointer->kind != YASL_PTR_LEAVE; + if (pointer->kind == YASL_PTR_LEAVE) cancel_stroke(app); + else if (pointer->kind == YASL_PTR_PRESS && pointer->button == 0) { + app->down = true; + app->previous = false; + stroke(app); + } else if (pointer->kind == YASL_PTR_RELEASE && pointer->button == 0) { + app->down = app->previous = false; + app->pointer.pen.pressure = 0; + } else if (pointer->kind == YASL_PTR_MOTION && app->down) stroke(app); + } +} + +/* 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(struct yasl_surface *surface, struct yasl_frame *frame, void *ud) +{ + struct app *app = ud; + struct yasl_geometry geometry = yasl_surface_geometry(surface); + geometry.size = frame->target.size; + if (!resize_canvas(app, geometry)) { + fprintf(stderr, "Cannot allocate drawing canvas\n"); + app->running = false; + app->failed = true; + return; + } + struct yasl_framebuffer *fb = &frame->target; + memcpy(fb->pixels, app->canvas, (size_t)geometry.size.x * geometry.size.y * sizeof(uint32_t)); + double dpi = scale(app); + 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); + } + struct yasl_damage_list damage = {.n = -1}; + if (!yasl_surface_present(surface, fb, &damage)) { + app->failed = true; + app->running = false; + } + if (app->smoke_test) app->running = false; +} + +static void configure(struct yasl_surface *surface, struct yasl_geometry geometry, void *ud) +{ + struct app *app = ud; + if (!resize_canvas(app, geometry)) { + app->failed = true; + app->running = false; + } + yasl_surface_schedule_redraw(surface); +} + +static void close_window(struct yasl_surface *surface, void *ud) +{ + (void)surface; + ((struct app *)ud)->running = false; +} + +static void quit(void *ud) { ((struct app *)ud)->running = false; } +static void lost(void *ud) +{ + struct app *app = ud; + app->failed = true; + app->running = false; +} + +int main(int argc, char **argv) +{ + struct app app = {.running = true}; + 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_display_cbs display_cbs = {.quit = quit, .lost = lost}; + struct yasl_display_spec display_spec = { + .app_id = "org.yasl.tablet", .backend = backend, .cbs = &display_cbs, .ud = &app, + }; + struct yasl_display *display = yasl_display_open(NULL, &display_spec); + if (!display) { perror("Cannot open display"); return 1; } + app.backend = yasl_display_backend(display); + app.tablet_supported = (yasl_display_caps(display) & YASL_CAP_TABLET) != 0; + struct yasl_surface_spec spec = {.title = "YASL Tablet - pressure and tilt", .size = {900, 640}}; + struct yasl_surface *surface = yasl_surface_create_toplevel(display, &spec); + if (!surface) { perror("Cannot create surface"); yasl_display_close(display); return 1; } + struct yasl_surface_cbs callbacks = {.draw = draw, .configure = configure, .close = close_window}; + yasl_surface_set_callbacks(surface, &callbacks, &app); + yasl_surface_set_cursor(surface, YASL_CURSOR_HIDDEN); + yasl_surface_schedule_redraw(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"); + while (app.running) { + if (yasl_display_dispatch(display, -1) < 0) { app.failed = true; break; } + struct yasl_input_event event; + while (yasl_surface_input_poll(surface, &event)) { + input(&app, &event); + yasl_surface_schedule_redraw(surface); + } + yasl_display_flush(display); + } + free(app.canvas); + yasl_surface_destroy(surface); + yasl_display_close(display); + return app.failed ? 1 : 0; +} |
