The Birdfont Source Code


All Repositories / birdfont.git / blob – RSS feed

TextArea.vala in libbirdfont/Renderer

This file is a part of the Birdfont project.

Contributing

Send patches or pull requests to johan.mattsson.m@gmail.com.
Clone this repository: git clone https://github.com/johanmattssonm/birdfont.git

Revisions

View the latest version of libbirdfont/Renderer/TextArea.vala.
Platform independent tool tip
1 /* 2 Copyright (C) 2014 Johan Mattsson 3 4 This library is free software; you can redistribute it and/or modify 5 it under the terms of the GNU Lesser General Public License as 6 published by the Free Software Foundation; either version 3 of the 7 License, or (at your option) any later version. 8 9 This library is distributed in the hope that it will be useful, but 10 WITHOUT ANY WARRANTY; without even the implied warranty of 11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 Lesser General Public License for more details. 13 */ 14 15 using Cairo; 16 using Math; 17 18 namespace BirdFont { 19 20 public class TextArea : Widget { 21 22 public double min_width = 500; 23 public double min_height = 100; 24 public double font_size; 25 public double padding = 3.3; 26 public bool single_line = false; 27 28 public bool draw_carret { 29 get { return carret_is_visible; } 30 set { 31 carret_is_visible = value; 32 if (!value) { 33 update_selection = false; 34 selection_end = carret.copy (); 35 } 36 } 37 } 38 public bool carret_is_visible = false; 39 public bool draw_border = true; 40 41 public double width; 42 public double height; 43 44 Carret carret = new Carret (); 45 Carret selection_end = new Carret (); 46 bool update_selection = false; 47 public bool show_selection = false; 48 49 public signal void scroll (double pixels); 50 public signal void text_changed (string text); 51 public signal void enter (string text); 52 53 Gee.ArrayList<Paragraph> paragraphs = new Gee.ArrayList<Paragraph> (); 54 private static const int DONE = -2; 55 56 int64 cache_id = -1; 57 58 int last_paragraph = 0; 59 string text; 60 int text_length; 61 62 Gee.ArrayList<TextUndoItem> undo_items = new Gee.ArrayList<TextUndoItem> (); 63 Gee.ArrayList<TextUndoItem> redo_items = new Gee.ArrayList<TextUndoItem> (); 64 65 bool store_undo_state_at_next_event = false; 66 public bool editable; 67 68 public TextArea (double font_size = 20) { 69 this.font_size = font_size; 70 width = min_width; 71 height = min_height; 72 editable = true; 73 } 74 75 public override double get_height () { 76 return height + 2 * padding; 77 } 78 79 public override double get_width () { 80 return width + 2 * padding; 81 } 82 83 public void set_font_size (double z) { 84 font_size = z; 85 } 86 87 bool generate_paragraphs () { 88 Paragraph paragraph; 89 90 int next_paragraph = -1; 91 92 if (last_paragraph == DONE) { 93 return false; 94 } 95 96 next_paragraph = text.index_of ("\n", last_paragraph); 97 98 if (next_paragraph == -1) { 99 paragraph = new Paragraph (text.substring (last_paragraph), font_size, paragraphs.size); 100 paragraphs.add (paragraph); 101 last_paragraph = DONE; 102 } else { 103 next_paragraph += "\n".length; 104 paragraph = new Paragraph (text.substring (last_paragraph, next_paragraph - last_paragraph), font_size, paragraphs.size); 105 paragraphs.add (paragraph); 106 last_paragraph = next_paragraph; 107 } 108 109 return last_paragraph != DONE; 110 } 111 112 void generate_all_paragraphs () { 113 while (generate_paragraphs ()) { 114 } 115 } 116 117 public void key_press (uint keyval) { 118 unichar c; 119 TextUndoItem ui; 120 121 if (!editable) { 122 return; 123 } 124 125 c = (unichar) keyval; 126 127 switch (c) { 128 case ' ': 129 store_undo_edit_state (); 130 add_character (keyval); 131 break; 132 case 'a': 133 if (KeyBindings.has_ctrl ()) { 134 select_all (); 135 } else { 136 add_character (keyval); 137 } 138 break; 139 case 'c': 140 if (KeyBindings.has_ctrl ()) { 141 ClipTool.copy_text (this); 142 } else { 143 add_character (keyval); 144 } 145 break; 146 case 'v': 147 if (KeyBindings.has_ctrl ()) { 148 ClipTool.paste_text (this); 149 store_undo_state_at_next_event = true; 150 } else { 151 add_character (keyval); 152 } 153 break; 154 case 'y': 155 if (KeyBindings.has_ctrl ()) { 156 redo (); 157 } else { 158 add_character (keyval); 159 } 160 break; 161 case 'z': 162 if (KeyBindings.has_ctrl ()) { 163 undo (); 164 } else { 165 add_character (keyval); 166 } 167 break; 168 case Key.RIGHT: 169 check_selection (); 170 move_carret_next (); 171 break; 172 case Key.LEFT: 173 check_selection (); 174 move_carret_previous (); 175 break; 176 case Key.DOWN: 177 check_selection (); 178 move_carret_next_row (); 179 break; 180 case Key.UP: 181 check_selection (); 182 move_carret_previous_row (); 183 break; 184 case Key.END: 185 check_selection (); 186 move_carret_to_end_of_line (); 187 break; 188 case Key.HOME: 189 check_selection (); 190 move_carret_to_beginning_of_line (); 191 break; 192 case Key.BACK_SPACE: 193 if (has_selection ()) { 194 ui = delete_selected_text (); 195 undo_items.add (ui); 196 redo_items.clear (); 197 store_undo_state_at_next_event = true; 198 } else { 199 ui = remove_last_character (); 200 undo_items.add (ui); 201 redo_items.clear (); 202 store_undo_state_at_next_event = true; 203 } 204 text_changed (get_text ()); 205 break; 206 case Key.ENTER: 207 store_undo_edit_state (); 208 insert_text ("\n"); 209 210 if (single_line) { 211 enter (get_text ()); 212 } 213 break; 214 case Key.DEL: 215 if (has_selection ()) { 216 ui = delete_selected_text (); 217 undo_items.add (ui); 218 redo_items.clear (); 219 store_undo_state_at_next_event = true; 220 } else { 221 ui = remove_next_character (); 222 undo_items.add (ui); 223 redo_items.clear (); 224 store_undo_state_at_next_event = true; 225 } 226 text_changed (get_text ()); 227 break; 228 default: 229 add_character (keyval); 230 break; 231 } 232 233 GlyphCanvas.redraw (); 234 } 235 236 void check_selection () { 237 if (!has_selection () && KeyBindings.has_shift ()) { 238 show_selection = true; 239 selection_end = carret.copy (); 240 } 241 242 if (!KeyBindings.has_shift ()) { 243 show_selection = false; 244 } 245 } 246 247 private void add_character (uint keyval) { 248 unichar c = (unichar) keyval; 249 string s; 250 251 if (!is_modifier_key (keyval) 252 && !KeyBindings.has_ctrl () 253 && !KeyBindings.has_alt ()) { 254 255 s = (!) c.to_string (); 256 257 if (s.validate ()) { 258 if (store_undo_state_at_next_event) { 259 store_undo_edit_state (); 260 store_undo_state_at_next_event = false; 261 } 262 263 insert_text (s); 264 } 265 } 266 } 267 268 Paragraph get_current_paragraph () { 269 Paragraph p; 270 271 if (unlikely (!(0 <= carret.paragraph < paragraphs.size))) { 272 warning (@"No paragraph, index: $(carret.paragraph), size: $(paragraphs.size)"); 273 p = new Paragraph ("", 0, 0); 274 paragraphs.add (p); 275 return p; 276 } 277 278 p = paragraphs.get (carret.paragraph); 279 return p; 280 } 281 282 public void set_text (string t) { 283 int tl; 284 285 if (single_line) { 286 text = t.replace ("\n", "").replace ("\r", ""); 287 } else { 288 text = t; 289 } 290 291 tl = t.length; 292 text_length += tl; 293 294 paragraphs.clear (); 295 generate_paragraphs (); 296 297 return_if_fail (paragraphs.size != 0); 298 299 carret.paragraph = paragraphs.size - 1; 300 carret.character_index = paragraphs.get (paragraphs.size - 1).text.length; 301 selection_end = carret.copy (); 302 show_selection = false; 303 304 text_changed (get_text ()); 305 } 306 307 Carret get_selection_start () { 308 if (carret.paragraph == selection_end.paragraph) { 309 return carret.character_index < selection_end.character_index ? carret : selection_end; 310 } 311 312 return carret.paragraph < selection_end.paragraph ? carret : selection_end; 313 } 314 315 Carret get_selection_stop () { 316 if (carret.paragraph == selection_end.paragraph) { 317 return carret.character_index > selection_end.character_index ? carret : selection_end; 318 } 319 320 return carret.paragraph > selection_end.paragraph ? carret : selection_end; 321 } 322 323 public string get_selected_text () { 324 Carret selection_start, selection_stop; 325 int i; 326 Paragraph pg; 327 StringBuilder sb; 328 329 sb = new StringBuilder (); 330 331 if (!has_selection ()) { 332 return "".dup (); 333 } 334 335 selection_start = get_selection_start (); 336 selection_stop = get_selection_stop (); 337 338 if (selection_start.paragraph == selection_stop.paragraph) { 339 pg = paragraphs.get (selection_start.paragraph); 340 return pg.text.substring (selection_start.character_index, selection_stop.character_index - selection_start.character_index); 341 } 342 343 pg = paragraphs.get (selection_start.paragraph); 344 sb.append (pg.text.substring (selection_start.character_index)); 345 346 for (i = selection_start.paragraph + 1; i < selection_stop.paragraph; i++) { 347 return_if_fail (0 <= i < paragraphs.size); 348 pg = paragraphs.get (i); 349 sb.append (pg.text); 350 } 351 352 pg = paragraphs.get (selection_stop.paragraph); 353 sb.append (pg.text.substring (0, selection_stop.character_index)); 354 355 return sb.str; 356 } 357 358 public void select_all () { 359 while (last_paragraph != DONE) { 360 generate_paragraphs (); 361 } 362 363 if (paragraphs.size > 0) { 364 carret.paragraph = 0; 365 carret.character_index = 0; 366 selection_end.paragraph = paragraphs.size - 1; 367 selection_end.character_index = paragraphs.get (paragraphs.size - 1).text_length; 368 show_selection = true; 369 } 370 } 371 372 public TextUndoItem delete_selected_text () { 373 Carret selection_start, selection_stop; 374 int i; 375 Paragraph pg, pge; 376 string e, s, n; 377 bool same; 378 TextUndoItem ui; 379 380 ui = new TextUndoItem (carret); 381 382 e = ""; 383 s = ""; 384 n = ""; 385 386 if (!has_selection ()) { 387 warning ("No selected text."); 388 return ui; 389 } 390 391 selection_start = get_selection_start (); 392 selection_stop = get_selection_stop (); 393 394 same = selection_start.paragraph == selection_stop.paragraph; 395 396 if (!same) { 397 return_val_if_fail (0 <= selection_start.paragraph < paragraphs.size, ui); 398 pg = paragraphs.get (selection_start.paragraph); 399 s = pg.text.substring (0, selection_start.character_index); 400 401 return_val_if_fail (0 <= selection_stop.paragraph < paragraphs.size, ui); 402 pge = paragraphs.get (selection_stop.paragraph); 403 e = pge.text.substring (selection_stop.character_index); 404 405 if (!s.has_suffix ("\n")) { 406 ui.deleted.add (pge.copy ()); 407 ui.edited.add (pg.copy ()); 408 409 pg.set_text (s + e); 410 pge.set_text (""); 411 } else { 412 ui.edited.add (pg.copy ()); 413 ui.edited.add (pge.copy ()); 414 415 pg.set_text (s); 416 pge.set_text (e); 417 } 418 } else { 419 return_val_if_fail (0 <= selection_start.paragraph < paragraphs.size, ui); 420 421 pg = paragraphs.get (selection_start.paragraph); 422 n = pg.text.substring (0, selection_start.character_index); 423 n += pg.text.substring (selection_stop.character_index); 424 425 if (n == "") { 426 ui.deleted.add (pg.copy ()); 427 paragraphs.remove_at (selection_start.paragraph); 428 } else { 429 ui.edited.add (pg.copy ()); 430 } 431 432 pg.set_text (n); 433 } 434 435 if (e == "" && !same) { 436 paragraphs.remove_at (selection_stop.paragraph); 437 } 438 439 for (i = selection_stop.paragraph - 1; i > selection_start.paragraph; i--) { 440 return_val_if_fail (0 <= i < paragraphs.size, ui); 441 ui.deleted.add (paragraphs.get (i)); 442 paragraphs.remove_at (i); 443 } 444 445 if (s == "" && !same) { 446 return_val_if_fail (0 <= selection_start.paragraph < paragraphs.size, ui); 447 paragraphs.remove_at (selection_start.paragraph); 448 } 449 450 carret = selection_start.copy (); 451 selection_end = carret.copy (); 452 453 show_selection = false; 454 update_paragraph_index (); 455 layout (); 456 457 return ui; 458 } 459 460 void update_paragraph_index () { 461 int i = 0; 462 foreach (Paragraph p in paragraphs) { 463 p.index = i; 464 i++; 465 } 466 } 467 468 public TextUndoItem remove_last_character () { 469 TextUndoItem ui; 470 move_carret_previous (); 471 ui = remove_next_character (); 472 return ui; 473 } 474 475 public TextUndoItem remove_next_character () { 476 Paragraph paragraph; 477 Paragraph next_paragraph; 478 int index; 479 unichar c; 480 string np; 481 TextUndoItem ui; 482 483 ui = new TextUndoItem (carret); 484 485 return_val_if_fail (0 <= carret.paragraph < paragraphs.size, ui); 486 paragraph = paragraphs.get (carret.paragraph); 487 488 index = carret.character_index; 489 490 paragraph.text.get_next_char (ref index, out c); 491 492 if (index >= paragraph.text_length) { 493 np = paragraph.text.substring (0, carret.character_index); 494 495 if (carret.paragraph + 1 < paragraphs.size) { 496 next_paragraph = paragraphs.get (carret.paragraph + 1); 497 paragraphs.remove_at (carret.paragraph + 1); 498 499 np = np + next_paragraph.text; 500 501 ui.deleted.add (next_paragraph); 502 } 503 504 paragraph.set_text (np); 505 ui.edited.add (paragraph); 506 } else { 507 np = paragraph.text.substring (0, carret.character_index) + paragraph.text.substring (index); 508 paragraph.set_text (np); 509 510 if (np == "") { 511 return_if_fail (carret.paragraph > 0); 512 carret.paragraph--; 513 paragraph = paragraphs.get (carret.paragraph); 514 carret.character_index = paragraph.text_length; 515 516 ui.deleted.add (paragraphs.get (carret.paragraph + 1)); 517 518 paragraphs.remove_at (carret.paragraph + 1); 519 } else { 520 ui.edited.add (paragraph); 521 } 522 } 523 524 update_paragraph_index (); 525 layout (); 526 527 return ui; 528 } 529 530 public void move_carret_next () { 531 Paragraph paragraph; 532 int index; 533 unichar c; 534 535 return_if_fail (0 <= carret.paragraph < paragraphs.size); 536 paragraph = paragraphs.get (carret.paragraph); 537 538 index = carret.character_index; 539 paragraph.text.get_next_char (ref index, out c); 540 541 if (index >= paragraph.text_length && carret.paragraph + 1 < paragraphs.size) { 542 carret.paragraph++; 543 carret.character_index = 0; 544 } else { 545 carret.character_index = index; 546 } 547 } 548 549 public void move_carret_previous () { 550 Paragraph paragraph; 551 int index, last_index; 552 unichar c; 553 554 return_if_fail (0 <= carret.paragraph < paragraphs.size); 555 paragraph = paragraphs.get (carret.paragraph); 556 557 index = 0; 558 last_index = -1; 559 560 while (paragraph.text.get_next_char (ref index, out c) && index < carret.character_index) { 561 last_index = index; 562 } 563 564 if (last_index <= 0 && carret.paragraph > 0) { 565 carret.paragraph--; 566 567 return_if_fail (0 <= carret.paragraph < paragraphs.size); 568 paragraph = paragraphs.get (carret.paragraph); 569 carret.character_index = paragraph.text_length; 570 571 if (paragraph.text.has_suffix ("\n")) { 572 carret.character_index -= "\n".length; 573 } 574 } else { 575 carret.character_index = (last_index) > 0 ? last_index : 0; 576 } 577 578 return_if_fail (0 <= carret.paragraph < paragraphs.size); 579 } 580 581 public void move_carret_next_row () { 582 double nr = font_size; 583 584 if (carret.desired_y + 2 * font_size >= allocation.height) { 585 scroll (2 * font_size); 586 nr = -font_size; 587 } 588 589 if (carret.desired_y + nr < widget_y + height - padding) { 590 carret = get_carret_at (carret.desired_x - widget_x - padding, carret.desired_y + nr); 591 } 592 } 593 594 public void move_carret_to_end_of_line () { 595 carret = get_carret_at (widget_x + padding + width, carret.desired_y, false); 596 } 597 598 public void move_carret_to_beginning_of_line () { 599 carret = get_carret_at (0, carret.desired_y, false); 600 } 601 602 603 public void move_carret_previous_row () { 604 double nr = -font_size; 605 606 if (carret.desired_y - 2 * font_size < 0) { 607 scroll (-2 * font_size); 608 nr = font_size; 609 } 610 611 if (carret.desired_y + nr > widget_y + padding) { 612 carret = get_carret_at (carret.desired_x, carret.desired_y + nr); 613 } 614 } 615 616 public bool has_selection () { 617 return show_selection && selection_is_visible (); 618 } 619 620 private bool selection_is_visible () { 621 return carret.paragraph != selection_end.paragraph || carret.character_index != selection_end.character_index; 622 } 623 624 public void insert_text (string t) { 625 string s; 626 Paragraph paragraph; 627 TextUndoItem ui; 628 Gee.ArrayList<string> pgs; 629 bool u = false; 630 631 pgs = new Gee.ArrayList<string> (); 632 633 if (single_line) { 634 s = t.replace ("\n", "").replace ("\r", ""); 635 pgs.add (s); 636 } else { 637 if (t.last_index_of ("\n") > 0) { 638 string[] parts = t.split ("\n"); 639 int i; 640 for (i = 0; i < parts.length -1; i++) { 641 pgs.add (parts[i]); 642 pgs.add ("\n"); 643 } 644 645 pgs.add (parts[parts.length - 1]); 646 647 if (t.has_suffix ("\n")) { 648 pgs.add ("\n"); 649 } 650 } else { 651 s = t; 652 pgs.add (s); 653 } 654 } 655 656 if (has_selection () && show_selection) { 657 ui = delete_selected_text (); 658 u = true; 659 660 if (paragraphs.size == 0) { 661 paragraphs.add (new Paragraph ("", font_size, 0)); 662 } 663 } else { 664 ui = new TextUndoItem (carret); 665 } 666 667 return_if_fail (0 <= carret.paragraph < paragraphs.size); 668 paragraph = paragraphs.get (carret.paragraph); 669 670 if (pgs.size > 0) { 671 if (!u) { 672 ui.edited.add (paragraph.copy ()); 673 } 674 675 string first = pgs.get (0); 676 677 string end; 678 string nt = paragraph.text.substring (0, carret.character_index); 679 680 nt += first; 681 end = paragraph.text.substring (carret.character_index); 682 683 paragraph.set_text (nt); 684 685 int paragraph_index = carret.paragraph; 686 Paragraph next_paragraph = paragraph; 687 for (int i = 1; i < pgs.size; i++) { 688 paragraph_index++; 689 string next = pgs.get (i); 690 next_paragraph = new Paragraph (next, font_size, paragraph_index); 691 paragraphs.insert (paragraph_index, next_paragraph); 692 ui.added.add (next_paragraph); 693 u = true; 694 } 695 696 carret.paragraph = paragraph_index; 697 carret.character_index = next_paragraph.text.length; 698 699 next_paragraph.set_text (next_paragraph.text + end); 700 } 701 702 if (u) { 703 undo_items.add (ui); 704 redo_items.clear (); 705 } 706 707 update_paragraph_index (); 708 layout (); 709 710 text_changed (get_text ()); 711 show_selection = false; 712 } 713 714 // FIXME: corruption? 715 public string get_text () { 716 StringBuilder sb = new StringBuilder (); 717 718 generate_all_paragraphs (); 719 720 foreach (Paragraph p in paragraphs) { 721 sb.append (p.text); 722 } 723 724 return sb.str; 725 } 726 727 Carret get_carret_at (double click_x, double click_y, bool check_boundaries = true) { 728 int i = 0; 729 double tx, ty; 730 double p; 731 string w; 732 int ch_index; 733 double min_d = double.MAX; 734 Carret c = new Carret (); 735 double dt; 736 737 c.paragraph = -1; 738 c.desired_x = click_x; 739 c.desired_y = click_y; 740 741 foreach (Paragraph paragraph in paragraphs) { 742 if (!check_boundaries || paragraph.text_is_on_screen (allocation, widget_y)) { 743 ch_index = 0; 744 745 if (paragraph.start_y + widget_y - font_size <= click_y <= paragraph.end_y + widget_y + font_size) { 746 foreach (Text next_word in paragraph.words) { 747 double tt_click = click_y - widget_y - padding + font_size; // - next_word.get_baseline_to_bottom (); //- font_size + next_word.get_baseline_to_bottom (); 748 749 w = next_word.text; 750 if (next_word.widget_y <= tt_click <= next_word.widget_y + font_size) { 751 Theme.text_color (next_word, "Foreground 1"); 752 753 p = next_word.get_sidebearing_extent (); 754 755 if ((next_word.widget_y <= tt_click <= next_word.widget_y + font_size) 756 && (next_word.widget_x + widget_x <= click_x <= next_word.widget_x + widget_x + padding + next_word.get_sidebearing_extent ())) { 757 758 tx = widget_x + next_word.widget_x + padding; 759 ty = widget_y + next_word.widget_y + padding; 760 761 next_word.iterate ((glyph, kerning, last) => { 762 double cw; 763 int ci; 764 double d; 765 string gc = (!) glyph.get_unichar ().to_string (); 766 767 d = Math.fabs (click_x - tx); 768 769 if (d <= min_d) { 770 min_d = d; 771 c.character_index = ch_index; 772 c.paragraph = i; 773 } 774 775 cw = (glyph.get_width ()) * next_word.get_scale () + kerning; 776 ci = gc.length; 777 778 tx += cw; 779 ch_index += ci; 780 }); 781 782 dt = Math.fabs (click_x - (tx + widget_x + padding)); 783 if (dt < min_d) { 784 min_d = dt; 785 c.character_index = ch_index; 786 c.paragraph = i; 787 } 788 } else { 789 dt = Math.fabs (click_x - (next_word.widget_x + widget_x + padding + next_word.get_sidebearing_extent ())); 790 791 if (dt < min_d) { 792 min_d = dt; 793 c.character_index = ch_index + w.length; 794 795 if (w.has_suffix ("\n")) { 796 c.character_index -= "\n".length; 797 } 798 799 c.paragraph = i; 800 } 801 802 ch_index += w.length; 803 } 804 } else { 805 ch_index += w.length; 806 } 807 } 808 } 809 } 810 i++; 811 } 812 813 if (unlikely (c.paragraph < 0)) { 814 c.paragraph = 0; 815 c.character_index = 0; 816 } 817 818 store_undo_state_at_next_event = true; 819 820 return c; 821 } 822 823 /** @return offset to click in text. */ 824 public void layout () { 825 double p; 826 double tx, ty; 827 string w; 828 double xmax = 0; 829 int i = 0; 830 double dd; 831 832 tx = 0; 833 ty = font_size; 834 835 for (i = paragraphs.size - 1; i >= 0 && paragraphs.size > 1; i--) { 836 if (unlikely (paragraphs.get (i).is_empty ())) { 837 warning ("Empty paragraph."); 838 paragraphs.remove_at (i); 839 update_paragraph_index (); 840 } 841 } 842 843 i = 0; 844 foreach (Paragraph paragraph in paragraphs) { 845 if (paragraph.need_layout 846 || (paragraph.text_area_width != width 847 && paragraph.text_is_on_screen (allocation, widget_y))) { 848 849 paragraph.start_y = ty; 850 paragraph.start_x = tx; 851 852 paragraph.cached_surface = null; 853 854 foreach (Text next_word in paragraph.words) { 855 next_word.set_font_size (font_size); 856 857 w = next_word.text; 858 p = next_word.get_sidebearing_extent (); 859 860 if (unlikely (p == 0)) { 861 warning (@"Zero width word: $(w)"); 862 } 863 864 if (w == "") { 865 break; 866 } 867 868 if (w == "\n") { 869 next_word.widget_x = tx; 870 next_word.widget_y = ty; 871 872 tx = 0; 873 ty += next_word.font_size; 874 } else { 875 if (!single_line) { 876 if (tx + p + 2 * padding > width || w == "\n") { 877 tx = 0; 878 ty += next_word.font_size; 879 } 880 } 881 882 if (tx + p > xmax) { 883 xmax = tx + p; 884 } 885 886 next_word.widget_x = tx; 887 next_word.widget_y = ty; 888 889 if (w != "\n") { 890 tx += p; 891 } 892 } 893 } 894 895 if (tx > xmax) { 896 xmax = tx; 897 } 898 899 paragraph.text_area_width = width; 900 paragraph.width = xmax; 901 paragraph.end_x = tx; 902 paragraph.end_y = ty; 903 paragraph.need_layout = false; 904 } 905 906 if (xmax > width) { 907 break; 908 } 909 910 tx = paragraph.end_x; 911 ty = paragraph.end_y; 912 i++; 913 } 914 915 if (xmax > width) { 916 this.width = xmax + 2 * padding; 917 layout (); 918 return; 919 } 920 921 this.height = fmax (min_height, ty + 2 * padding); 922 923 if (last_paragraph != DONE) { 924 this.height = (text_length / (double) last_paragraph) * ty + 2 * padding; // estimate height 925 } 926 927 if (ty + widget_y < allocation.height && last_paragraph != DONE) { 928 generate_paragraphs (); 929 layout (); 930 return; 931 } 932 933 ty = font_size; 934 tx = 0; 935 936 foreach (Paragraph paragraph in paragraphs) { 937 dd = ty - paragraph.start_y; 938 939 if (dd != 0) { 940 paragraph.start_y += dd; 941 paragraph.end_y += dd; 942 foreach (Text word in paragraph.words) { 943 word.widget_y += dd; 944 } 945 } 946 947 ty = paragraph.end_y; 948 } 949 } 950 951 public void button_press (uint button, double x, double y) { 952 if (is_over (x, y)) { 953 carret = get_carret_at (x, y); 954 selection_end = carret.copy (); 955 update_selection = true; 956 } 957 } 958 959 public void button_release (uint button, double x, double y) { 960 update_selection = false; 961 show_selection = selection_is_visible (); 962 } 963 964 public bool motion (double x, double y) { 965 if (update_selection) { 966 selection_end = get_carret_at (x, y); 967 show_selection = selection_is_visible (); 968 } 969 970 return update_selection; 971 } 972 973 public override void draw (Context cr) { 974 Text word; 975 double tx, ty; 976 string w; 977 double scale; 978 double width; 979 double x = widget_x; 980 double y = widget_y; 981 Carret selection_start, selection_stop; 982 double carret_x; 983 double carret_y; 984 985 layout (); 986 987 if (draw_border) { 988 // background 989 cr.save (); 990 cr.set_line_width (1); 991 Theme.color (cr, "Foreground 2"); 992 draw_rounded_rectangle (cr, x, y, this.width, this.height, padding); 993 cr.fill (); 994 cr.restore (); 995 996 // border 997 cr.save (); 998 cr.set_line_width (1); 999 Theme.color (cr, "Foreground 1"); 1000 draw_rounded_rectangle (cr, x, y, this.width, this.height, padding); 1001 cr.stroke (); 1002 cr.restore (); 1003 } 1004 1005 cr.save (); 1006 1007 word = new Text (); 1008 1009 width = this.width - padding; 1010 x += padding; 1011 scale = word.get_scale (); 1012 y += font_size; 1013 1014 // draw selection background 1015 if (has_selection ()) { 1016 tx = 0; 1017 ty = 0; 1018 1019 selection_start = get_selection_start (); 1020 selection_stop = get_selection_stop (); 1021 1022 cr.save (); 1023 Theme.color (cr, "Highlighted 1"); 1024 1025 for (int i = selection_start.paragraph; i <= selection_stop.paragraph; i++) { 1026 return_if_fail (0 <= i < paragraphs.size); 1027 Paragraph pg = paragraphs.get (i); 1028 1029 if (pg.text_is_on_screen (allocation, widget_y)) { 1030 int char_index = 0; 1031 1032 foreach (Text next_word in pg.words) { 1033 double cw = next_word.get_sidebearing_extent (); 1034 bool paint_background = false; 1035 bool partial_start = false; 1036 bool partial_stop = false; 1037 int wl; 1038 1039 w = next_word.text; 1040 wl = w.length; 1041 scale = next_word.get_scale (); 1042 1043 if (selection_start.paragraph == selection_stop.paragraph) { 1044 partial_start = true; 1045 partial_stop = true; 1046 } else if (selection_start.paragraph < i < selection_stop.paragraph) { 1047 paint_background = true; 1048 } else if (selection_start.paragraph == i) { 1049 paint_background = true; 1050 partial_start = true; 1051 } else if (selection_stop.paragraph == i) { 1052 paint_background = char_index + wl < selection_stop.character_index; 1053 partial_stop = !paint_background; 1054 } 1055 1056 if (paint_background && !(partial_start || partial_stop)) { 1057 double selection_y = widget_y + next_word.widget_y + scale * -next_word.font.bottom_limit - font_size; 1058 cr.rectangle (widget_x + padding + next_word.widget_x - 1, selection_y, cw + 1, font_size); 1059 cr.fill (); 1060 } 1061 1062 if (partial_start || partial_stop) { 1063 int index = char_index; 1064 double bx = widget_x + padding + next_word.widget_x + (partial_start ? 0 : 1); 1065 1066 next_word.iterate ((glyph, kerning, last) => { 1067 double cwi; 1068 int ci; 1069 bool draw = (index >= selection_start.character_index && partial_start && !partial_stop) 1070 || (index < selection_stop.character_index && !partial_start && partial_stop) 1071 || (selection_start.character_index <= index < selection_stop.character_index && partial_start && partial_stop); 1072 1073 cwi = (glyph.get_width ()) * next_word.get_scale () + kerning; 1074 1075 if (draw) { 1076 double selection_y = widget_y + next_word.widget_y + scale * -next_word.font.bottom_limit - font_size; 1077 cr.rectangle (bx - 1, selection_y, cwi + 1, font_size); 1078 cr.fill (); 1079 } 1080 1081 bx += cwi; 1082 ci = ((!) glyph.get_unichar ().to_string ()).length; 1083 index += ci; 1084 }); 1085 } 1086 1087 char_index += w.length; 1088 } 1089 } 1090 } 1091 1092 cr.restore (); 1093 } 1094 1095 tx = 0; 1096 ty = 0; 1097 1098 int first_visible = 0; 1099 int last_visible; 1100 int paragraphs_size = paragraphs.size; 1101 while (first_visible < paragraphs_size) { 1102 if (paragraphs.get (first_visible).text_is_on_screen (allocation, widget_y)) { 1103 break; 1104 } 1105 first_visible++; 1106 } 1107 1108 last_visible = first_visible; 1109 while (last_visible < paragraphs_size) { 1110 if (!paragraphs.get (last_visible).text_is_on_screen (allocation, widget_y)) { 1111 last_visible++; 1112 break; 1113 } 1114 last_visible++; 1115 } 1116 1117 if (paragraphs_size == 0) { 1118 if (carret_is_visible) { 1119 draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding); 1120 } 1121 1122 return; 1123 } 1124 1125 Context cc; // cached context 1126 Paragraph paragraph; 1127 paragraph = paragraphs.get (0); 1128 1129 tx = paragraph.start_x; 1130 ty = paragraph.start_y; 1131 1132 if (cache_id == -1 && paragraphs.size > 0 && paragraphs.get (0).words.size > 0) { 1133 Text t = paragraphs.get (0).words.get (0); 1134 Theme.text_color (t, "Foreground 1"); 1135 cache_id = t.get_cache_id (); 1136 } 1137 1138 for (int i = first_visible; i < last_visible; i++) { 1139 paragraph = paragraphs.get (i); 1140 1141 tx = paragraph.start_x; 1142 ty = paragraph.start_y; 1143 1144 if (paragraph.cached_surface == null) { 1145 paragraph.cached_surface = new Surface.similar (cr.get_target (), Cairo.Content.COLOR_ALPHA, (int) width + 1, paragraph.get_height () + (int) font_size + 1); 1146 cc = new Context ((!) paragraph.cached_surface); 1147 1148 foreach (Text next_word in paragraph.words) { 1149 Theme.text_color (next_word, "Foreground 1"); 1150 1151 if (next_word.text != "\n") { 1152 next_word.draw_at_top (cc, next_word.widget_x, next_word.widget_y - ty, cache_id); 1153 } 1154 } 1155 } 1156 1157 if (likely (paragraph.cached_surface != null)) { 1158 cr.set_source_surface ((!) paragraph.cached_surface, x + tx, widget_y + paragraph.start_y - font_size + padding); 1159 cr.paint (); 1160 } else { 1161 warning ("No paragraph image."); 1162 } 1163 } 1164 1165 if (carret_is_visible) { 1166 get_carret_position (carret, out carret_x, out carret_y); 1167 1168 if (carret_y < 0) { 1169 draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding); 1170 } else { 1171 draw_carret_at (cr, carret_x, carret_y); 1172 } 1173 } 1174 1175 if (has_selection ()) { 1176 get_carret_position (selection_end, out carret_x, out carret_y); 1177 1178 if (carret_y < 0) { 1179 draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding); 1180 } else { 1181 draw_carret_at (cr, carret_x, carret_y); 1182 } 1183 } 1184 } 1185 1186 void get_carret_position (Carret carret, out double carret_x, out double carret_y) { 1187 Paragraph paragraph; 1188 double tx; 1189 double ty; 1190 int ch_index; 1191 int wl; 1192 double pos_x, pos_y; 1193 1194 ch_index = 0; 1195 1196 carret_x = -1; 1197 carret_y = -1; 1198 1199 return_if_fail (0 <= carret.paragraph < paragraphs.size); 1200 paragraph = paragraphs.get (carret.paragraph); 1201 1202 pos_x = -1; 1203 pos_y = -1; 1204 1205 foreach (Text next_word in paragraph.words) { 1206 string w = next_word.text; 1207 wl = w.length; 1208 1209 if (carret.character_index == ch_index) { 1210 pos_x = next_word.widget_x + widget_x + padding; 1211 pos_y = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom (); 1212 } else if (carret.character_index >= ch_index + wl) { 1213 pos_x = next_word.widget_x + next_word.get_sidebearing_extent () + widget_x + padding; 1214 pos_y = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom (); 1215 1216 if (next_word.text.has_suffix ("\n")) { 1217 pos_x = widget_x + padding; 1218 pos_y += next_word.font_size; 1219 } 1220 } else if (ch_index < carret.character_index <= ch_index + wl) { 1221 tx = widget_x + next_word.widget_x; 1222 ty = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom (); 1223 1224 if (carret.character_index <= ch_index) { 1225 pos_x = widget_x + padding; 1226 pos_y = ty; 1227 } 1228 1229 next_word.iterate ((glyph, kerning, last) => { 1230 double cw; 1231 int ci; 1232 1233 cw = (glyph.get_width ()) * next_word.get_scale () + kerning; 1234 ci = ((!) glyph.get_unichar ().to_string ()).length; 1235 1236 if (ch_index < carret.character_index <= ch_index + ci) { 1237 pos_x = tx + cw + padding; 1238 pos_y = ty; 1239 1240 if (glyph.get_unichar () == '\n') { 1241 pos_x = widget_x + padding; 1242 pos_y += next_word.font_size; 1243 } 1244 } 1245 1246 tx += cw; 1247 ch_index += ci; 1248 }); 1249 } 1250 1251 ch_index += wl; 1252 } 1253 1254 carret_x = pos_x; 1255 carret_y = pos_y; 1256 } 1257 1258 void draw_carret_at (Context cr, double x, double y) { 1259 cr.save (); 1260 cr.set_source_rgba (0, 0, 0, 0.5); 1261 cr.set_line_width (1); 1262 cr.move_to (x, y); 1263 cr.line_to (x, y - font_size); 1264 cr.stroke (); 1265 cr.restore (); 1266 } 1267 1268 public void store_undo_edit_state () { 1269 TextUndoItem ui = new TextUndoItem (carret); 1270 ui.edited.add (get_current_paragraph ().copy ()); 1271 undo_items.add (ui); 1272 redo_items.clear (); 1273 } 1274 1275 public void redo () { 1276 TextUndoItem i; 1277 TextUndoItem undo_item; 1278 1279 if (redo_items.size > 0) { 1280 i = redo_items.get (redo_items.size - 1); 1281 1282 undo_item = new TextUndoItem (i.carret); 1283 1284 i.deleted.sort ((a, b) => { 1285 Paragraph pa = (Paragraph) a; 1286 Paragraph pb = (Paragraph) b; 1287 return pb.index - pa.index; 1288 }); 1289 1290 i.added.sort ((a, b) => { 1291 Paragraph pa = (Paragraph) a; 1292 Paragraph pb = (Paragraph) b; 1293 return pa.index - pb.index; 1294 }); 1295 1296 foreach (Paragraph p in i.deleted) { 1297 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1298 warning ("Paragraph not found."); 1299 } else { 1300 undo_item.deleted.add (p.copy ()); 1301 paragraphs.remove_at (p.index); 1302 } 1303 } 1304 1305 foreach (Paragraph p in i.added) { 1306 if (p.index == paragraphs.size) { 1307 paragraphs.add (p.copy ()); 1308 } else { 1309 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1310 warning (@"Index: $(p.index) out of bounds, size: $(paragraphs.size)"); 1311 } else { 1312 undo_item.added.add (paragraphs.get (p.index).copy ()); 1313 paragraphs.insert (p.index, p.copy ()); 1314 } 1315 } 1316 } 1317 1318 foreach (Paragraph p in i.edited) { 1319 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1320 warning (@"Index: $(p.index ) out of bounds, size: $(paragraphs.size)"); 1321 return; 1322 } 1323 1324 undo_item.edited.add (paragraphs.get (p.index).copy ()); 1325 paragraphs.set (p.index, p.copy ()); 1326 } 1327 1328 redo_items.remove_at (redo_items.size - 1); 1329 undo_items.add (undo_item); 1330 1331 carret = i.carret.copy (); 1332 layout (); 1333 } 1334 } 1335 1336 public void undo () { 1337 TextUndoItem i; 1338 TextUndoItem redo_item; 1339 1340 if (undo_items.size > 0) { 1341 i = undo_items.get (undo_items.size - 1); 1342 redo_item = new TextUndoItem (i.carret); 1343 1344 i.deleted.sort ((a, b) => { 1345 Paragraph pa = (Paragraph) a; 1346 Paragraph pb = (Paragraph) b; 1347 return pa.index - pb.index; 1348 }); 1349 1350 i.added.sort ((a, b) => { 1351 Paragraph pa = (Paragraph) a; 1352 Paragraph pb = (Paragraph) b; 1353 return pb.index - pa.index; 1354 }); 1355 1356 foreach (Paragraph p in i.added) { 1357 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1358 warning ("Paragraph not found."); 1359 } else { 1360 redo_item.added.add (paragraphs.get (p.index).copy ()); 1361 paragraphs.remove_at (p.index); 1362 } 1363 } 1364 1365 foreach (Paragraph p in i.deleted) { 1366 if (p.index == paragraphs.size) { 1367 paragraphs.add (p.copy ()); 1368 } else { 1369 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1370 warning (@"Index: $(p.index) out of bounds, size: $(paragraphs.size)"); 1371 } else { 1372 redo_item.deleted.add (p.copy ()); 1373 paragraphs.insert (p.index, p.copy ()); 1374 } 1375 } 1376 } 1377 1378 foreach (Paragraph p in i.edited) { 1379 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1380 warning (@"Index: $(p.index ) out of bounds, size: $(paragraphs.size)"); 1381 return; 1382 } 1383 1384 redo_item.edited.add (paragraphs.get (p.index).copy ()); 1385 paragraphs.set (p.index, p.copy ()); 1386 } 1387 1388 undo_items.remove_at (undo_items.size - 1); 1389 redo_items.add (redo_item); 1390 1391 carret = i.carret.copy (); 1392 layout (); 1393 } 1394 } 1395 1396 public void set_editable (bool editable) { 1397 this.editable = editable; 1398 } 1399 1400 public class TextUndoItem : GLib.Object { 1401 public Carret carret; 1402 public Gee.ArrayList<Paragraph> added = new Gee.ArrayList<Paragraph> (); 1403 public Gee.ArrayList<Paragraph> edited = new Gee.ArrayList<Paragraph> (); 1404 public Gee.ArrayList<Paragraph> deleted = new Gee.ArrayList<Paragraph> (); 1405 1406 public TextUndoItem (Carret c) { 1407 carret = c.copy (); 1408 } 1409 } 1410 1411 public class Paragraph : GLib.Object { 1412 public double end_x = -10000; 1413 public double end_y = -10000; 1414 1415 public double start_x = -10000; 1416 public double start_y = -10000; 1417 1418 public double width = -10000; 1419 public double text_area_width = -10000; 1420 1421 public string text; 1422 1423 public Gee.ArrayList<Text> words { 1424 get { 1425 if (words_in_paragraph.size == 0) { 1426 generate_words (); 1427 } 1428 1429 return words_in_paragraph; 1430 } 1431 } 1432 1433 private Gee.ArrayList<Text> words_in_paragraph = new Gee.ArrayList<Text> (); 1434 1435 public int text_length; 1436 1437 public bool need_layout = true; 1438 1439 public Surface? cached_surface = null; 1440 1441 double font_size; 1442 1443 public int index; 1444 1445 public Paragraph (string text, double font_size, int index) { 1446 this.index = index; 1447 this.font_size = font_size; 1448 set_text (text); 1449 } 1450 1451 public Paragraph copy () { 1452 Paragraph p = new Paragraph (text.dup (), font_size, index); 1453 p.need_layout = true; 1454 return p; 1455 } 1456 1457 public bool is_empty () { 1458 return text == ""; 1459 } 1460 1461 public void set_text (string t) { 1462 this.text = t; 1463 text_length = t.length; 1464 need_layout = true; 1465 words.clear (); 1466 cached_surface = null; 1467 } 1468 1469 public int get_height () { 1470 return (int) (end_y - start_y) + 1; 1471 } 1472 1473 public int get_width () { 1474 return (int) width + 1; 1475 } 1476 1477 public bool text_is_on_screen (WidgetAllocation alloc, double widget_y) { 1478 bool v = (0 <= start_y + widget_y <= alloc.height) 1479 || (0 <= end_y + widget_y <= alloc.height) 1480 || (start_y + widget_y <= 0 && alloc.height <= end_y + widget_y); 1481 return v; 1482 } 1483 1484 private void generate_words () { 1485 string w; 1486 int p = 0; 1487 bool carret_at_word_end = false; 1488 Text word; 1489 int carret = 0; 1490 int iter_pos = 0; 1491 1492 return_if_fail (words_in_paragraph.size == 0); 1493 1494 while (p < text_length) { 1495 w = get_next_word (out carret_at_word_end, ref iter_pos, carret); 1496 1497 if (w == "") { 1498 break; 1499 } 1500 1501 word = new Text (w, font_size); 1502 words_in_paragraph.add (word); 1503 } 1504 } 1505 1506 string get_next_word (out bool carret_at_end_of_word, ref int iter_pos, int carret) { 1507 int i; 1508 int ni; 1509 int pi; 1510 string n; 1511 int nl; 1512 1513 carret_at_end_of_word = false; 1514 1515 if (iter_pos >= text_length) { 1516 carret_at_end_of_word = true; 1517 return "".dup (); 1518 } 1519 1520 if (text.get_char (iter_pos) == '\n') { 1521 iter_pos += "\n".length; 1522 carret_at_end_of_word = (iter_pos == carret); 1523 return "\n".dup (); 1524 } 1525 1526 i = text.index_of (" ", iter_pos); 1527 pi = i + " ".length; 1528 1529 ni = text.index_of ("\t", iter_pos); 1530 if (ni != -1 && ni < pi || i == -1) { 1531 i = ni; 1532 pi = i + "\t".length; 1533 } 1534 1535 ni = text.index_of ("\n", iter_pos); 1536 if (ni != -1 && ni < pi || i == -1) { 1537 i = ni; 1538 pi = i; 1539 } 1540 1541 if (iter_pos + iter_pos - pi > text_length || i == -1) { 1542 n = text.substring (iter_pos); 1543 } else { 1544 n = text.substring (iter_pos, pi - iter_pos); 1545 } 1546 1547 nl = n.length; 1548 if (iter_pos < carret < iter_pos + nl) { 1549 n = text.substring (iter_pos, carret - iter_pos); 1550 nl = n.length; 1551 carret_at_end_of_word = true; 1552 } 1553 1554 iter_pos += nl; 1555 1556 if (iter_pos == carret) { 1557 carret_at_end_of_word = true; 1558 } 1559 1560 return n; 1561 } 1562 } 1563 1564 public class Carret : GLib.Object { 1565 1566 public int paragraph = 0; 1567 1568 public int character_index { 1569 get { 1570 return ci; 1571 } 1572 1573 set { 1574 ci = value; 1575 } 1576 } 1577 1578 private int ci = 0; 1579 1580 public double desired_x = 0; 1581 public double desired_y = 0; 1582 1583 public Carret () { 1584 } 1585 1586 public void print () { 1587 stdout.printf (@"paragraph: $paragraph, character_index: $character_index\n"); 1588 } 1589 1590 public Carret copy () { 1591 Carret c = new Carret (); 1592 1593 c.paragraph = paragraph; 1594 c.character_index = character_index; 1595 1596 c.desired_x = desired_x; 1597 c.desired_y = desired_y; 1598 1599 return c; 1600 } 1601 } 1602 } 1603 1604 } 1605