summaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
Diffstat (limited to 'examples')
-rw-r--r--examples/tablet.c355
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;
+}