canvas.c (15263B)
1 /* sdiagram - simple diagram tool 2 * See LICENSE file for copyright and license details. */ 3 4 #include <math.h> 5 #include <stdio.h> 6 #include <string.h> 7 8 #include "sdiagram.h" 9 #include "config.h" 10 11 #define PBUF_SZ 4096 12 13 /* append to pango buffer with bounds check */ 14 static int 15 papp(char *o, char *end, const char *s) 16 { 17 int n; 18 19 n = snprintf(o, end - o, "%s", s); 20 return (n > 0 && o + n < end) ? n : 0; 21 } 22 23 /* convert markdown to Pango markup. 24 * supports: **bold**, *italic*, `code`, 25 * # h1, ## h2, - lists, --- rules */ 26 static void 27 md_to_pango(const char *md, char *out, size_t outsz) 28 { 29 char *o, *end; 30 int in_b, in_i, in_c, sol; 31 32 o = out; 33 end = out + outsz - 1; 34 in_b = in_i = in_c = 0; 35 sol = 1; 36 37 while (*md && o < end) { 38 /* line-level at start of line */ 39 if (sol) { 40 if (md[0] == '#' && md[1] == '#' 41 && md[2] == ' ') { 42 o += papp(o, end, "<b>"); 43 md += 3; 44 while (*md && *md != '\n' && o < end) { 45 if (*md == '&') 46 o += papp(o, end, "&"); 47 else if (*md == '<') 48 o += papp(o, end, "<"); 49 else 50 *o++ = *md; 51 md++; 52 } 53 o += papp(o, end, "</b>"); 54 if (*md == '\n') { *o++ = '\n'; md++; } 55 sol = 1; 56 continue; 57 } 58 if (md[0] == '#' && md[1] == ' ') { 59 o += papp(o, end, 60 "<span size=\"large\">" 61 "<b>"); 62 md += 2; 63 while (*md && *md != '\n' && o < end) { 64 if (*md == '&') 65 o += papp(o, end, "&"); 66 else if (*md == '<') 67 o += papp(o, end, "<"); 68 else 69 *o++ = *md; 70 md++; 71 } 72 o += papp(o, end, 73 "</b></span>"); 74 if (*md == '\n') { *o++ = '\n'; md++; } 75 sol = 1; 76 continue; 77 } 78 if ((md[0] == '-' || md[0] == '*') 79 && md[1] == ' ' 80 && !(md[0] == '*' && md[1] == '*')) { 81 o += papp(o, end, " \xe2\x80\xa2 "); 82 md += 2; 83 sol = 0; 84 continue; 85 } 86 if (md[0] == '-' && md[1] == '-' 87 && md[2] == '-') { 88 o += papp(o, end, 89 "\xe2\x94\x80\xe2\x94\x80" 90 "\xe2\x94\x80\xe2\x94\x80" 91 "\xe2\x94\x80\xe2\x94\x80" 92 "\xe2\x94\x80\xe2\x94\x80"); 93 while (*md == '-') md++; 94 if (*md == '\n') { *o++ = '\n'; md++; } 95 sol = 1; 96 continue; 97 } 98 sol = 0; 99 } 100 101 /* inside code: no markdown processing */ 102 if (in_c) { 103 if (*md == '`') { 104 o += papp(o, end, "</tt>"); 105 in_c = 0; 106 md++; 107 continue; 108 } 109 if (*md == '&') 110 o += papp(o, end, "&"); 111 else if (*md == '<') 112 o += papp(o, end, "<"); 113 else 114 *o++ = *md; 115 if (*md == '\n') sol = 1; 116 md++; 117 continue; 118 } 119 120 /* backtick: start code */ 121 if (*md == '`') { 122 o += papp(o, end, "<tt>"); 123 in_c = 1; 124 md++; 125 continue; 126 } 127 128 /* bold ** */ 129 if (md[0] == '*' && md[1] == '*') { 130 if (in_b) 131 o += papp(o, end, "</b>"); 132 else 133 o += papp(o, end, "<b>"); 134 in_b = !in_b; 135 md += 2; 136 continue; 137 } 138 139 /* italic * */ 140 if (md[0] == '*' && md[1] != '*') { 141 if (in_i) 142 o += papp(o, end, "</i>"); 143 else 144 o += papp(o, end, "<i>"); 145 in_i = !in_i; 146 md++; 147 continue; 148 } 149 150 /* escape XML */ 151 if (*md == '&') 152 o += papp(o, end, "&"); 153 else if (*md == '<') 154 o += papp(o, end, "<"); 155 else 156 *o++ = *md; 157 158 if (*md == '\n') 159 sol = 1; 160 md++; 161 } 162 163 /* close unclosed tags */ 164 if (in_c) o += papp(o, end, "</tt>"); 165 if (in_b) o += papp(o, end, "</b>"); 166 if (in_i) o += papp(o, end, "</i>"); 167 *o = '\0'; 168 } 169 170 static void 171 set_color(cairo_t *cr, const double *c) 172 { 173 cairo_set_source_rgba(cr, c[0], c[1], c[2], c[3]); 174 } 175 176 static void 177 rect_edge(double cx, double cy, double w, double h, 178 double tx, double ty, double *ex, double *ey) 179 { 180 double dx, dy, sx, sy, s; 181 182 dx = tx - cx; 183 dy = ty - cy; 184 if (dx == 0 && dy == 0) { 185 *ex = cx; 186 *ey = cy; 187 return; 188 } 189 sx = (dx != 0) ? fabs((w / 2) / dx) : 1e9; 190 sy = (dy != 0) ? fabs((h / 2) / dy) : 1e9; 191 s = (sx < sy) ? sx : sy; 192 *ex = cx + dx * s; 193 *ey = cy + dy * s; 194 } 195 196 static void 197 draw_grid(cairo_t *cr, Diagram *d, int w, int h) 198 { 199 double gs, x0, y0, x, y; 200 201 if (!grid_on) 202 return; 203 204 gs = grid_size * d->zoom; 205 if (gs < 8.0) 206 return; 207 208 set_color(cr, d->theme->grid); 209 cairo_set_line_width(cr, 0.5); 210 211 x0 = fmod(d->panx * d->zoom, gs); 212 y0 = fmod(d->pany * d->zoom, gs); 213 if (x0 > 0) x0 -= gs; 214 if (y0 > 0) y0 -= gs; 215 216 for (x = x0; x < w; x += gs) { 217 cairo_move_to(cr, x, 0); 218 cairo_line_to(cr, x, h); 219 } 220 for (y = y0; y < h; y += gs) { 221 cairo_move_to(cr, 0, y); 222 cairo_line_to(cr, w, y); 223 } 224 cairo_stroke(cr); 225 } 226 227 static void 228 draw_conn(cairo_t *cr, Diagram *d, Conn *c, int sel) 229 { 230 int fi, ti; 231 Node *fn, *tn; 232 double x1, y1, x2, y2, fh, th; 233 double fcx, fcy, tcx, tcy; 234 const Theme *t; 235 236 fi = node_by_id(d, c->from); 237 ti = node_by_id(d, c->to); 238 if (fi < 0 || ti < 0) 239 return; 240 241 t = d->theme; 242 fn = &d->nodes[fi]; 243 tn = &d->nodes[ti]; 244 fh = node_height(fn); 245 th = node_height(tn); 246 247 fcx = fn->x + fn->w / 2; 248 fcy = fn->y + fh / 2; 249 tcx = tn->x + tn->w / 2; 250 tcy = tn->y + th / 2; 251 252 rect_edge(fcx, fcy, fn->w, fh, tcx, tcy, &x1, &y1); 253 rect_edge(tcx, tcy, tn->w, th, fcx, fcy, &x2, &y2); 254 255 if (sel) 256 set_color(cr, t->sel); 257 else 258 set_color(cr, t->conn); 259 cairo_set_line_width(cr, conn_lw); 260 cairo_move_to(cr, x1, y1); 261 cairo_line_to(cr, x2, y2); 262 cairo_stroke(cr); 263 264 /* arrowhead */ 265 { 266 double dx, dy, len, ax, ay, px, py, sz; 267 268 dx = x2 - x1; 269 dy = y2 - y1; 270 len = hypot(dx, dy); 271 if (len < 1.0) 272 return; 273 dx /= len; 274 dy /= len; 275 sz = 8.0; 276 ax = x2 - dx * sz; 277 ay = y2 - dy * sz; 278 px = -dy; 279 py = dx; 280 cairo_move_to(cr, x2, y2); 281 cairo_line_to(cr, 282 ax + px * sz * 0.35, 283 ay + py * sz * 0.35); 284 cairo_line_to(cr, 285 ax - px * sz * 0.35, 286 ay - py * sz * 0.35); 287 cairo_close_path(cr); 288 cairo_fill(cr); 289 } 290 291 /* label */ 292 if (c->label[0]) { 293 PangoLayout *layout; 294 PangoFontDescription *fd; 295 int tw, tth; 296 297 fd = pango_font_description_from_string(font_body); 298 layout = pango_cairo_create_layout(cr); 299 pango_layout_set_font_description(layout, fd); 300 pango_layout_set_text(layout, c->label, -1); 301 pango_layout_get_pixel_size(layout, &tw, &tth); 302 set_color(cr, t->fg); 303 cairo_move_to(cr, (x1 + x2) / 2 - tw / 2, 304 (y1 + y2) / 2 - tth / 2); 305 pango_cairo_show_layout(cr, layout); 306 g_object_unref(layout); 307 pango_font_description_free(fd); 308 } 309 } 310 311 /* draw a bento-style node: header | image | comment sections 312 * separated by borders, all sharp corners */ 313 static void 314 draw_node(cairo_t *cr, Diagram *d, Node *n) 315 { 316 PangoLayout *layout; 317 PangoFontDescription *fd; 318 const Theme *t; 319 double h, yoff; 320 int tw, th; 321 322 t = d->theme; 323 h = node_height(n); 324 n->h = h; 325 326 /* card background */ 327 cairo_rectangle(cr, n->x, n->y, n->w, h); 328 set_color(cr, t->card); 329 cairo_fill(cr); 330 331 /* header background */ 332 cairo_rectangle(cr, n->x, n->y, n->w, node_hdr_h); 333 cairo_set_source_rgb(cr, n->hdr[0], n->hdr[1], n->hdr[2]); 334 cairo_fill(cr); 335 336 /* title text */ 337 fd = pango_font_description_from_string(font_title); 338 pango_font_description_set_weight(fd, PANGO_WEIGHT_MEDIUM); 339 layout = pango_cairo_create_layout(cr); 340 pango_layout_set_font_description(layout, fd); 341 pango_layout_set_text(layout, n->text, -1); 342 pango_layout_set_width(layout, 343 (int)((n->w - node_pad * 2) * PANGO_SCALE)); 344 pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); 345 pango_layout_get_pixel_size(layout, &tw, &th); 346 set_color(cr, t->primary_fg); 347 cairo_move_to(cr, n->x + node_pad, 348 n->y + (node_hdr_h - th) / 2); 349 pango_cairo_show_layout(cr, layout); 350 g_object_unref(layout); 351 pango_font_description_free(fd); 352 353 yoff = n->y + node_hdr_h; 354 355 /* header bottom border */ 356 set_color(cr, t->border); 357 cairo_set_line_width(cr, 1.0); 358 cairo_move_to(cr, n->x, yoff); 359 cairo_line_to(cr, n->x + n->w, yoff); 360 cairo_stroke(cr); 361 362 /* image section: flush, natural size */ 363 if (n->thumb) { 364 double ih; 365 366 ih = gdk_pixbuf_get_height(n->thumb); 367 gdk_cairo_set_source_pixbuf(cr, 368 n->thumb, n->x, yoff); 369 cairo_paint(cr); 370 yoff += ih; 371 372 /* border below image */ 373 if (n->desc[0]) { 374 set_color(cr, t->border); 375 cairo_set_line_width(cr, 1.0); 376 cairo_move_to(cr, n->x, yoff); 377 cairo_line_to(cr, n->x + n->w, yoff); 378 cairo_stroke(cr); 379 } 380 } 381 382 /* description: rendered markdown */ 383 if (n->desc[0]) { 384 char pbuf[PBUF_SZ]; 385 386 md_to_pango(n->desc, pbuf, sizeof(pbuf)); 387 fd = pango_font_description_from_string(font_body); 388 layout = pango_cairo_create_layout(cr); 389 pango_layout_set_font_description(layout, fd); 390 pango_layout_set_markup(layout, pbuf, -1); 391 pango_layout_set_width(layout, 392 (int)((n->w - node_pad * 2) 393 * PANGO_SCALE)); 394 pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR); 395 set_color(cr, t->muted_fg); 396 cairo_move_to(cr, n->x + node_pad, 397 yoff + 4.0); 398 pango_cairo_show_layout(cr, layout); 399 g_object_unref(layout); 400 pango_font_description_free(fd); 401 } 402 403 /* outer border */ 404 cairo_rectangle(cr, n->x, n->y, n->w, h); 405 if (n->selected) { 406 set_color(cr, t->sel); 407 cairo_set_line_width(cr, 2.0); 408 } else { 409 set_color(cr, t->border); 410 cairo_set_line_width(cr, 1.0); 411 } 412 cairo_stroke(cr); 413 } 414 415 void 416 canvas_draw(GtkDrawingArea *area, cairo_t *cr, 417 int w, int h, gpointer data) 418 { 419 Diagram *d; 420 int i; 421 422 (void)area; 423 d = (Diagram *)data; 424 425 /* background */ 426 set_color(cr, d->theme->bg); 427 cairo_paint(cr); 428 429 draw_grid(cr, d, w, h); 430 431 /* pan + zoom */ 432 cairo_save(cr); 433 cairo_translate(cr, d->panx * d->zoom, 434 d->pany * d->zoom); 435 cairo_scale(cr, d->zoom, d->zoom); 436 437 for (i = 0; i < d->nconns; i++) 438 draw_conn(cr, d, &d->conns[i], 439 i == d->selconn); 440 441 for (i = 0; i < d->nnodes; i++) 442 draw_node(cr, d, &d->nodes[i]); 443 444 cairo_restore(cr); 445 446 /* connect mode indicator */ 447 if (d->mode == MODE_CONNECT) { 448 PangoLayout *layout; 449 PangoFontDescription *fd; 450 451 fd = pango_font_description_from_string( 452 "Sans Bold 10"); 453 layout = pango_cairo_create_layout(cr); 454 pango_layout_set_font_description(layout, fd); 455 pango_layout_set_text(layout, 456 "CONNECT MODE click two nodes", -1); 457 set_color(cr, d->theme->muted_fg); 458 cairo_move_to(cr, 10, h - 24); 459 pango_cairo_show_layout(cr, layout); 460 g_object_unref(layout); 461 pango_font_description_free(fd); 462 } 463 } 464 465 void 466 canvas_click(GtkGestureClick *g, int np, 467 double x, double y, gpointer data) 468 { 469 Diagram *d; 470 double cx, cy; 471 int hit, i; 472 473 (void)g; 474 d = (Diagram *)data; 475 screen_to_canvas(d, x, y, &cx, &cy); 476 477 for (i = 0; i < d->nnodes; i++) 478 d->nodes[i].selected = 0; 479 d->sel = -1; 480 d->selconn = -1; 481 482 hit = node_at(d, cx, cy); 483 484 if (d->mode == MODE_CONNECT) { 485 if (hit >= 0) { 486 if (d->connfrom < 0) { 487 d->connfrom = hit; 488 d->nodes[hit].selected = 1; 489 } else if (hit != d->connfrom) { 490 conn_add(d, 491 d->nodes[d->connfrom].id, 492 d->nodes[hit].id, ""); 493 d->mode = MODE_NORMAL; 494 d->connfrom = -1; 495 } 496 } 497 redraw(d); 498 status_update(d); 499 return; 500 } 501 502 if (hit >= 0) { 503 d->sel = hit; 504 d->nodes[hit].selected = 1; 505 if (np == 2) 506 edit_node_text(d, hit); 507 } else { 508 d->selconn = conn_at(d, cx, cy); 509 if (d->selconn < 0 && np == 2) { 510 hit = node_add(d, cx - node_min_w / 2, 511 cy - node_min_h / 2, NULL); 512 d->sel = hit; 513 d->nodes[hit].selected = 1; 514 edit_node_text(d, hit); 515 } 516 } 517 redraw(d); 518 status_update(d); 519 } 520 521 void 522 canvas_rclick(GtkGestureClick *g, int np, 523 double x, double y, gpointer data) 524 { 525 Diagram *d; 526 double cx, cy; 527 int hit, i; 528 529 (void)g; 530 (void)np; 531 d = (Diagram *)data; 532 screen_to_canvas(d, x, y, &cx, &cy); 533 534 /* select what was right-clicked */ 535 for (i = 0; i < d->nnodes; i++) 536 d->nodes[i].selected = 0; 537 d->selconn = -1; 538 539 hit = node_at(d, cx, cy); 540 if (hit >= 0) { 541 d->sel = hit; 542 d->nodes[hit].selected = 1; 543 } else { 544 d->sel = -1; 545 d->selconn = conn_at(d, cx, cy); 546 } 547 548 d->ctx_cx = cx; 549 d->ctx_cy = cy; 550 show_context_menu(d, x, y); 551 redraw(d); 552 } 553 554 void 555 canvas_drag_begin(GtkGestureDrag *g, 556 double x, double y, gpointer data) 557 { 558 Diagram *d; 559 double cx, cy; 560 int hit; 561 GdkModifierType mods; 562 GdkEvent *ev; 563 564 d = (Diagram *)data; 565 screen_to_canvas(d, x, y, &cx, &cy); 566 567 ev = gtk_gesture_get_last_event( 568 GTK_GESTURE(g), NULL); 569 mods = ev ? gdk_event_get_modifier_state(ev) : 0; 570 571 hit = node_at(d, cx, cy); 572 573 if ((mods & GDK_CONTROL_MASK) || hit < 0) { 574 d->panning = 1; 575 d->pansx = d->panx; 576 d->pansy = d->pany; 577 } else { 578 d->dragging = 1; 579 d->dragox = cx - d->nodes[hit].x; 580 d->dragoy = cy - d->nodes[hit].y; 581 d->sel = hit; 582 { 583 int i; 584 for (i = 0; i < d->nnodes; i++) 585 d->nodes[i].selected = 0; 586 } 587 d->nodes[hit].selected = 1; 588 } 589 } 590 591 void 592 canvas_drag_update(GtkGestureDrag *g, 593 double ox, double oy, gpointer data) 594 { 595 Diagram *d; 596 double sx, sy, cx, cy; 597 598 d = (Diagram *)data; 599 sx = sy = cx = cy = 0; 600 601 if (d->panning) { 602 d->panx = d->pansx + ox / d->zoom; 603 d->pany = d->pansy + oy / d->zoom; 604 } 605 606 if (d->dragging && d->sel >= 0) { 607 gtk_gesture_drag_get_start_point( 608 GTK_GESTURE_DRAG(g), &sx, &sy); 609 screen_to_canvas(d, sx + ox, sy + oy, 610 &cx, &cy); 611 d->nodes[d->sel].x = cx - d->dragox; 612 d->nodes[d->sel].y = cy - d->dragoy; 613 d->modified = 1; 614 } 615 616 redraw(d); 617 } 618 619 void 620 canvas_drag_end(GtkGestureDrag *g, 621 double ox, double oy, gpointer data) 622 { 623 Diagram *d; 624 625 (void)g; 626 (void)ox; 627 (void)oy; 628 d = (Diagram *)data; 629 d->dragging = 0; 630 d->panning = 0; 631 status_update(d); 632 } 633 634 gboolean 635 canvas_scroll(GtkEventControllerScroll *c, 636 double dx, double dy, gpointer data) 637 { 638 Diagram *d; 639 double oz; 640 641 (void)c; 642 (void)dx; 643 d = (Diagram *)data; 644 645 oz = d->zoom; 646 d->zoom -= dy * zoom_step; 647 if (d->zoom < zoom_min) d->zoom = zoom_min; 648 if (d->zoom > zoom_max) d->zoom = zoom_max; 649 650 if (d->zoom != oz) { 651 redraw(d); 652 status_update(d); 653 } 654 return TRUE; 655 } 656 657 gboolean 658 canvas_key(GtkEventControllerKey *c, 659 guint kv, guint kc, GdkModifierType st, gpointer data) 660 { 661 Diagram *d; 662 663 (void)c; 664 (void)kc; 665 d = (Diagram *)data; 666 667 if (st & GDK_CONTROL_MASK) { 668 switch (kv) { 669 case GDK_KEY_s: 670 do_save(d); 671 return TRUE; 672 case GDK_KEY_o: 673 do_open(d); 674 return TRUE; 675 case GDK_KEY_n: 676 do_new(d); 677 return TRUE; 678 } 679 } 680 681 switch (kv) { 682 case GDK_KEY_n: 683 if (!(st & GDK_CONTROL_MASK)) { 684 int idx; 685 idx = node_add(d, -d->panx + 200, 686 -d->pany + 200, NULL); 687 d->sel = idx; 688 d->nodes[idx].selected = 1; 689 edit_node_text(d, idx); 690 redraw(d); 691 } 692 return TRUE; 693 case GDK_KEY_c: 694 if (d->mode == MODE_CONNECT) { 695 d->mode = MODE_NORMAL; 696 d->connfrom = -1; 697 } else { 698 d->mode = MODE_CONNECT; 699 d->connfrom = -1; 700 } 701 redraw(d); 702 status_update(d); 703 return TRUE; 704 case GDK_KEY_e: 705 if (d->sel >= 0) 706 edit_node_text(d, d->sel); 707 return TRUE; 708 case GDK_KEY_i: 709 if (d->sel >= 0) 710 do_import_image(d); 711 return TRUE; 712 case GDK_KEY_Delete: /* FALLTHROUGH */ 713 case GDK_KEY_x: 714 if (d->sel >= 0) { 715 node_del(d, d->sel); 716 redraw(d); 717 status_update(d); 718 } else if (d->selconn >= 0) { 719 conn_del(d, d->selconn); 720 redraw(d); 721 status_update(d); 722 } 723 return TRUE; 724 case GDK_KEY_Escape: 725 d->mode = MODE_NORMAL; 726 d->connfrom = -1; 727 { 728 int i; 729 for (i = 0; i < d->nnodes; i++) 730 d->nodes[i].selected = 0; 731 } 732 d->sel = -1; 733 d->selconn = -1; 734 redraw(d); 735 status_update(d); 736 return TRUE; 737 case GDK_KEY_plus: /* FALLTHROUGH */ 738 case GDK_KEY_equal: 739 d->zoom += zoom_step; 740 if (d->zoom > zoom_max) 741 d->zoom = zoom_max; 742 redraw(d); 743 status_update(d); 744 return TRUE; 745 case GDK_KEY_minus: 746 d->zoom -= zoom_step; 747 if (d->zoom < zoom_min) 748 d->zoom = zoom_min; 749 redraw(d); 750 status_update(d); 751 return TRUE; 752 case GDK_KEY_0: 753 d->zoom = 1.0; 754 d->panx = 0; 755 d->pany = 0; 756 redraw(d); 757 status_update(d); 758 return TRUE; 759 } 760 return FALSE; 761 }