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