The Birdfont Source Code


All Repositories / birdfont.git / blob – RSS feed

TextArea.vala in libbirdfont/TextRendering

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/TextRendering/TextArea.vala.
Merge ../birdfont-2.x
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 protected Color text_color = Color.black (); 29 30 public bool draw_carret { 31 get { return carret_is_visible; } 32 set { 33 carret_is_visible = value; 34 if (!value) { 35 update_selection = false; 36 selection_end = carret.copy (); 37 } 38 } 39 } 40 public bool carret_is_visible = false; 41 public bool draw_border = true; 42 43 public double width; 44 public double height; 45 46 Carret carret = new Carret (); 47 Carret selection_end = new Carret (); 48 bool update_selection = false; 49 public bool show_selection = false; 50 51 public signal void scroll (double pixels); 52 public signal void text_changed (string text); 53 public signal void enter (string text); 54 55 Gee.ArrayList<Paragraph> paragraphs = new Gee.ArrayList<Paragraph> (); 56 private static const int DONE = -2; 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 67 public bool editable; 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 paragraphs.remove_at (i); 901 update_paragraph_index (); 902 } 903 } 904 905 i = 0; 906 foreach (Paragraph paragraph in paragraphs) { 907 if (paragraph.need_layout 908 || (paragraph.text_area_width != width 909 && paragraph.text_is_on_screen (allocation, widget_y))) { 910 911 paragraph.start_y = ty; 912 paragraph.start_x = tx; 913 914 paragraph.cached_surface = null; 915 916 foreach (Text next_word in paragraph.words) { 917 next_word.set_font_size (font_size); 918 919 w = next_word.text; 920 p = next_word.get_sidebearing_extent (); 921 922 if (unlikely (p == 0)) { 923 warning (@"Zero width word: $(w)"); 924 } 925 926 if (w == "") { 927 break; 928 } 929 930 if (w == "\n") { 931 next_word.widget_x = tx; 932 next_word.widget_y = ty; 933 934 tx = 0; 935 ty += next_word.font_size; 936 } else { 937 if (!single_line) { 938 if (tx + p + 2 * padding > width || w == "\n") { 939 tx = 0; 940 ty += next_word.font_size; 941 } 942 } 943 944 if (tx + p > xmax) { 945 xmax = tx + p; 946 } 947 948 next_word.widget_x = tx; 949 next_word.widget_y = ty; 950 951 if (w != "\n") { 952 tx += p; 953 } 954 } 955 } 956 957 if (tx > xmax) { 958 xmax = tx; 959 } 960 961 paragraph.text_area_width = width; 962 paragraph.width = xmax; 963 paragraph.end_x = tx; 964 paragraph.end_y = ty; 965 paragraph.need_layout = false; 966 } 967 968 if (xmax > width) { 969 break; 970 } 971 972 tx = paragraph.end_x; 973 ty = paragraph.end_y; 974 i++; 975 } 976 977 if (xmax > width) { 978 this.width = xmax + 2 * padding; 979 layout (); 980 return; 981 } 982 983 this.height = fmax (min_height, ty + 2 * padding); 984 985 if (last_paragraph != DONE) { 986 this.height = (text_length / (double) last_paragraph) * ty + 2 * padding; // estimate height 987 } 988 989 if (ty + widget_y < allocation.height && last_paragraph != DONE) { 990 generate_paragraphs (); 991 layout (); 992 return; 993 } 994 995 ty = font_size; 996 tx = 0; 997 998 foreach (Paragraph paragraph in paragraphs) { 999 dd = ty - paragraph.start_y; 1000 1001 if (dd != 0) { 1002 paragraph.start_y += dd; 1003 paragraph.end_y += dd; 1004 foreach (Text word in paragraph.words) { 1005 word.widget_y += dd; 1006 } 1007 } 1008 1009 ty = paragraph.end_y; 1010 } 1011 } 1012 1013 public override void button_press (uint button, double x, double y) { 1014 if (is_over (x, y)) { 1015 carret = get_carret_at (x, y); 1016 selection_end = carret.copy (); 1017 update_selection = true; 1018 } 1019 } 1020 1021 public override void button_release (uint button, double x, double y) { 1022 update_selection = false; 1023 show_selection = selection_is_visible (); 1024 } 1025 1026 public override bool motion (double x, double y) { 1027 if (update_selection) { 1028 selection_end = get_carret_at (x, y); 1029 show_selection = selection_is_visible (); 1030 } 1031 1032 return update_selection; 1033 } 1034 1035 public override void draw (Context cr) { 1036 Text word; 1037 double tx, ty; 1038 string w; 1039 double scale; 1040 double width; 1041 double x = widget_x; 1042 double y = widget_y; 1043 Carret selection_start, selection_stop; 1044 double carret_x; 1045 double carret_y; 1046 1047 layout (); 1048 1049 if (draw_border) { 1050 // background 1051 cr.save (); 1052 cr.set_line_width (1); 1053 Theme.color (cr, "Text Area Background"); 1054 draw_rounded_rectangle (cr, x, y, this.width, this.height - padding, padding); 1055 cr.fill (); 1056 cr.restore (); 1057 1058 // border 1059 cr.save (); 1060 cr.set_line_width (1); 1061 Theme.color (cr, "Foreground 1"); 1062 draw_rounded_rectangle (cr, x, y, this.width, this.height - padding, padding); 1063 cr.stroke (); 1064 cr.restore (); 1065 } 1066 1067 cr.save (); 1068 1069 word = new Text (); 1070 1071 width = this.width - padding; 1072 x += padding; 1073 scale = word.get_font_scale (); 1074 y += font_size; 1075 1076 // draw selection background 1077 if (has_selection ()) { 1078 tx = 0; 1079 ty = 0; 1080 1081 selection_start = get_selection_start (); 1082 selection_stop = get_selection_stop (); 1083 1084 cr.save (); 1085 Theme.color (cr, "Highlighted 1"); 1086 1087 for (int i = selection_start.paragraph; i <= selection_stop.paragraph; i++) { 1088 return_if_fail (0 <= i < paragraphs.size); 1089 Paragraph pg = paragraphs.get (i); 1090 1091 if (pg.text_is_on_screen (allocation, widget_y)) { 1092 int char_index = 0; 1093 1094 foreach (Text next_word in pg.words) { 1095 double cw = next_word.get_sidebearing_extent (); 1096 bool paint_background = false; 1097 bool partial_start = false; 1098 bool partial_stop = false; 1099 int wl; 1100 1101 w = next_word.text; 1102 wl = w.length; 1103 scale = next_word.get_font_scale (); 1104 1105 if (selection_start.paragraph == selection_stop.paragraph) { 1106 partial_start = true; 1107 partial_stop = true; 1108 } else if (selection_start.paragraph < i < selection_stop.paragraph) { 1109 paint_background = true; 1110 } else if (selection_start.paragraph == i) { 1111 paint_background = true; 1112 partial_start = true; 1113 } else if (selection_stop.paragraph == i) { 1114 paint_background = char_index + wl < selection_stop.character_index; 1115 partial_stop = !paint_background; 1116 } 1117 1118 if (paint_background && !(partial_start || partial_stop)) { 1119 double selection_y = widget_y + next_word.widget_y + scale * -next_word.cached_font.bottom_limit - font_size; 1120 cr.rectangle (widget_x + padding + next_word.widget_x - 1, selection_y, cw + 1, font_size); 1121 cr.fill (); 1122 } 1123 1124 if (partial_start || partial_stop) { 1125 int index = char_index; 1126 double bx = widget_x + padding + next_word.widget_x + (partial_start ? 0 : 1); 1127 1128 next_word.iterate ((glyph, kerning, last) => { 1129 double cwi; 1130 int ci; 1131 bool draw = (index >= selection_start.character_index && partial_start && !partial_stop) 1132 || (index < selection_stop.character_index && !partial_start && partial_stop) 1133 || (selection_start.character_index <= index < selection_stop.character_index && partial_start && partial_stop); 1134 1135 cwi = (glyph.get_width ()) * next_word.get_font_scale () + kerning; 1136 1137 if (draw) { 1138 double selection_y = widget_y + next_word.widget_y + scale * -next_word.cached_font.bottom_limit - font_size; 1139 cr.rectangle (bx - 1, selection_y, cwi + 1, font_size); 1140 cr.fill (); 1141 } 1142 1143 bx += cwi; 1144 ci = ((!) glyph.get_unichar ().to_string ()).length; 1145 index += ci; 1146 }); 1147 } 1148 1149 char_index += w.length; 1150 } 1151 } 1152 } 1153 1154 cr.restore (); 1155 } 1156 1157 tx = 0; 1158 ty = 0; 1159 1160 int first_visible = 0; 1161 int last_visible; 1162 int paragraphs_size = paragraphs.size; 1163 while (first_visible < paragraphs_size) { 1164 if (paragraphs.get (first_visible).text_is_on_screen (allocation, widget_y)) { 1165 break; 1166 } 1167 first_visible++; 1168 } 1169 1170 last_visible = first_visible; 1171 while (last_visible < paragraphs_size) { 1172 if (!paragraphs.get (last_visible).text_is_on_screen (allocation, widget_y)) { 1173 last_visible++; 1174 break; 1175 } 1176 last_visible++; 1177 } 1178 1179 if (paragraphs_size == 0) { 1180 if (carret_is_visible) { 1181 draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding); 1182 } 1183 1184 return; 1185 } 1186 1187 Context cc; // cached context 1188 Paragraph paragraph; 1189 paragraph = paragraphs.get (0); 1190 1191 tx = paragraph.start_x; 1192 ty = paragraph.start_y; 1193 1194 for (int i = first_visible; i < last_visible; i++) { 1195 paragraph = paragraphs.get (i); 1196 1197 tx = paragraph.start_x; 1198 ty = paragraph.start_y; 1199 1200 if (paragraph.cached_surface == null) { 1201 paragraph.cached_surface = Screen.create_background_surface ((int) width + 2, paragraph.get_height () + (int) font_size + 2); 1202 cc = new Context ((!) paragraph.cached_surface); 1203 cc.scale (Screen.get_scale(), Screen.get_scale()); 1204 1205 foreach (Text next_word in paragraph.words) { 1206 if (next_word.text != "\n") { 1207 next_word.draw_at_top (cc, next_word.widget_x, next_word.widget_y - ty); 1208 } 1209 } 1210 } 1211 1212 if (likely (paragraph.cached_surface != null)) { 1213 // FIXME: subpixel offset in text area 1214 Screen.paint_background_surface(cr, 1215 (!) paragraph.cached_surface, 1216 (int) (x + tx), 1217 (int) (widget_y + paragraph.start_y - font_size + padding)); 1218 } else { 1219 warning ("No paragraph image."); 1220 } 1221 } 1222 1223 if (carret_is_visible) { 1224 get_carret_position (carret, out carret_x, out carret_y); 1225 1226 if (carret_y < 0) { 1227 draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding); 1228 } else { 1229 draw_carret_at (cr, carret_x, carret_y); 1230 } 1231 } 1232 1233 if (has_selection ()) { 1234 get_carret_position (selection_end, out carret_x, out carret_y); 1235 1236 if (carret_y < 0) { 1237 draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding); 1238 } else { 1239 draw_carret_at (cr, carret_x, carret_y); 1240 } 1241 } 1242 } 1243 1244 void get_carret_position (Carret carret, out double carret_x, out double carret_y) { 1245 Paragraph paragraph; 1246 double tx; 1247 double ty; 1248 int ch_index; 1249 int wl; 1250 double pos_x, pos_y; 1251 1252 ch_index = 0; 1253 1254 carret_x = -1; 1255 carret_y = -1; 1256 1257 return_if_fail (0 <= carret.paragraph < paragraphs.size); 1258 paragraph = paragraphs.get (carret.paragraph); 1259 1260 pos_x = -1; 1261 pos_y = -1; 1262 1263 foreach (Text next_word in paragraph.words) { 1264 string w = next_word.text; 1265 wl = w.length; 1266 1267 if (carret.character_index == ch_index) { 1268 pos_x = next_word.widget_x + widget_x + padding; 1269 pos_y = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom_for_font (); 1270 } else if (carret.character_index >= ch_index + wl) { 1271 pos_x = next_word.widget_x + next_word.get_sidebearing_extent () + widget_x + padding; 1272 pos_y = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom_for_font (); 1273 1274 if (next_word.text.has_suffix ("\n")) { 1275 pos_x = widget_x + padding; 1276 pos_y += next_word.font_size; 1277 } 1278 } else if (ch_index < carret.character_index <= ch_index + wl) { 1279 tx = widget_x + next_word.widget_x; 1280 ty = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom_for_font (); 1281 1282 if (carret.character_index <= ch_index) { 1283 pos_x = widget_x + padding; 1284 pos_y = ty; 1285 } 1286 1287 next_word.iterate ((glyph, kerning, last) => { 1288 double cw; 1289 int ci; 1290 1291 cw = (glyph.get_width ()) * next_word.get_font_scale () + kerning; 1292 ci = ((!) glyph.get_unichar ().to_string ()).length; 1293 1294 if (ch_index < carret.character_index <= ch_index + ci) { 1295 pos_x = tx + cw + padding; 1296 pos_y = ty; 1297 1298 if (glyph.get_unichar () == '\n') { 1299 pos_x = widget_x + padding; 1300 pos_y += next_word.font_size; 1301 } 1302 } 1303 1304 tx += cw; 1305 ch_index += ci; 1306 }); 1307 } 1308 1309 ch_index += wl; 1310 } 1311 1312 carret_x = pos_x; 1313 carret_y = pos_y; 1314 } 1315 1316 void draw_carret_at (Context cr, double x, double y) { 1317 cr.save (); 1318 cr.set_source_rgba (0, 0, 0, 0.5); 1319 cr.set_line_width (1); 1320 cr.move_to (x, y); 1321 cr.line_to (x, y - font_size); 1322 cr.stroke (); 1323 cr.restore (); 1324 } 1325 1326 public void store_undo_edit_state () { 1327 TextUndoItem ui = new TextUndoItem (carret); 1328 ui.edited.add (get_current_paragraph ().copy ()); 1329 undo_items.add (ui); 1330 redo_items.clear (); 1331 } 1332 1333 public void redo () { 1334 TextUndoItem i; 1335 TextUndoItem undo_item; 1336 1337 if (redo_items.size > 0) { 1338 i = redo_items.get (redo_items.size - 1); 1339 1340 undo_item = new TextUndoItem (i.carret); 1341 1342 i.deleted.sort ((a, b) => { 1343 Paragraph pa = (Paragraph) a; 1344 Paragraph pb = (Paragraph) b; 1345 return pb.index - pa.index; 1346 }); 1347 1348 i.added.sort ((a, b) => { 1349 Paragraph pa = (Paragraph) a; 1350 Paragraph pb = (Paragraph) b; 1351 return pa.index - pb.index; 1352 }); 1353 1354 foreach (Paragraph p in i.deleted) { 1355 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1356 warning ("Paragraph not found."); 1357 } else { 1358 undo_item.deleted.add (p.copy ()); 1359 paragraphs.remove_at (p.index); 1360 } 1361 } 1362 1363 foreach (Paragraph p in i.added) { 1364 if (p.index == paragraphs.size) { 1365 paragraphs.add (p.copy ()); 1366 } else { 1367 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1368 warning (@"Index: $(p.index) out of bounds, size: $(paragraphs.size)"); 1369 } else { 1370 undo_item.added.add (paragraphs.get (p.index).copy ()); 1371 paragraphs.insert (p.index, p.copy ()); 1372 } 1373 } 1374 } 1375 1376 foreach (Paragraph p in i.edited) { 1377 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1378 warning (@"Index: $(p.index ) out of bounds, size: $(paragraphs.size)"); 1379 return; 1380 } 1381 1382 undo_item.edited.add (paragraphs.get (p.index).copy ()); 1383 paragraphs.set (p.index, p.copy ()); 1384 } 1385 1386 redo_items.remove_at (redo_items.size - 1); 1387 undo_items.add (undo_item); 1388 1389 carret = i.carret.copy (); 1390 layout (); 1391 } 1392 } 1393 1394 public void undo () { 1395 TextUndoItem i; 1396 TextUndoItem redo_item; 1397 1398 if (undo_items.size > 0) { 1399 i = undo_items.get (undo_items.size - 1); 1400 redo_item = new TextUndoItem (i.carret); 1401 1402 i.deleted.sort ((a, b) => { 1403 Paragraph pa = (Paragraph) a; 1404 Paragraph pb = (Paragraph) b; 1405 return pa.index - pb.index; 1406 }); 1407 1408 i.added.sort ((a, b) => { 1409 Paragraph pa = (Paragraph) a; 1410 Paragraph pb = (Paragraph) b; 1411 return pb.index - pa.index; 1412 }); 1413 1414 foreach (Paragraph p in i.added) { 1415 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1416 warning ("Paragraph not found."); 1417 } else { 1418 redo_item.added.add (paragraphs.get (p.index).copy ()); 1419 paragraphs.remove_at (p.index); 1420 } 1421 } 1422 1423 foreach (Paragraph p in i.deleted) { 1424 if (p.index == paragraphs.size) { 1425 paragraphs.add (p.copy ()); 1426 } else { 1427 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1428 warning (@"Index: $(p.index) out of bounds, size: $(paragraphs.size)"); 1429 } else { 1430 redo_item.deleted.add (p.copy ()); 1431 paragraphs.insert (p.index, p.copy ()); 1432 } 1433 } 1434 } 1435 1436 foreach (Paragraph p in i.edited) { 1437 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1438 warning (@"Index: $(p.index ) out of bounds, size: $(paragraphs.size)"); 1439 return; 1440 } 1441 1442 redo_item.edited.add (paragraphs.get (p.index).copy ()); 1443 paragraphs.set (p.index, p.copy ()); 1444 } 1445 1446 undo_items.remove_at (undo_items.size - 1); 1447 redo_items.add (redo_item); 1448 1449 carret = i.carret.copy (); 1450 layout (); 1451 } 1452 } 1453 1454 public void set_editable (bool editable) { 1455 this.editable = editable; 1456 } 1457 1458 public class TextUndoItem : GLib.Object { 1459 public Carret carret; 1460 public Gee.ArrayList<Paragraph> added = new Gee.ArrayList<Paragraph> (); 1461 public Gee.ArrayList<Paragraph> edited = new Gee.ArrayList<Paragraph> (); 1462 public Gee.ArrayList<Paragraph> deleted = new Gee.ArrayList<Paragraph> (); 1463 1464 public TextUndoItem (Carret c) { 1465 carret = c.copy (); 1466 } 1467 } 1468 1469 public class Paragraph : GLib.Object { 1470 public double end_x = -10000; 1471 public double end_y = -10000; 1472 1473 public double start_x = -10000; 1474 public double start_y = -10000; 1475 1476 public double width = -10000; 1477 public double text_area_width = -10000; 1478 1479 public string text; 1480 1481 public Gee.ArrayList<Text> words { 1482 get { 1483 if (words_in_paragraph.size == 0) { 1484 generate_words (); 1485 } 1486 1487 return words_in_paragraph; 1488 } 1489 } 1490 1491 private Gee.ArrayList<Text> words_in_paragraph = new Gee.ArrayList<Text> (); 1492 public int text_length; 1493 public bool need_layout = true; 1494 public Surface? cached_surface = null; 1495 double font_size; 1496 public int index; 1497 Color text_color; 1498 1499 public Paragraph (string text, double font_size, int index, Color c) { 1500 this.index = index; 1501 this.font_size = font_size; 1502 text_color = c; 1503 set_text (text); 1504 } 1505 1506 public Paragraph copy () { 1507 Paragraph p = new Paragraph (text.dup (), font_size, index, text_color); 1508 p.need_layout = true; 1509 return p; 1510 } 1511 1512 public bool is_empty () { 1513 return text == ""; 1514 } 1515 1516 public void set_text (string t) { 1517 this.text = t; 1518 text_length = t.length; 1519 need_layout = true; 1520 words.clear (); 1521 cached_surface = null; 1522 } 1523 1524 public int get_height () { 1525 return (int) (end_y - start_y) + 1; 1526 } 1527 1528 public int get_width () { 1529 return (int) width + 1; 1530 } 1531 1532 public bool text_is_on_screen (WidgetAllocation alloc, double widget_y) { 1533 bool v = (0 <= start_y + widget_y <= alloc.height) 1534 || (0 <= end_y + widget_y <= alloc.height) 1535 || (start_y + widget_y <= 0 && alloc.height <= end_y + widget_y); 1536 return v; 1537 } 1538 1539 private void generate_words () { 1540 string w; 1541 int p = 0; 1542 bool carret_at_word_end = false; 1543 Text word; 1544 int carret = 0; 1545 int iter_pos = 0; 1546 1547 return_if_fail (words_in_paragraph.size == 0); 1548 1549 while (p < text_length) { 1550 w = get_next_word (out carret_at_word_end, ref iter_pos, carret); 1551 1552 if (w == "") { 1553 break; 1554 } 1555 1556 word = new Text (w, font_size, 0, text_color); 1557 words_in_paragraph.add (word); 1558 } 1559 } 1560 1561 string get_next_word (out bool carret_at_end_of_word, ref int iter_pos, int carret) { 1562 int i; 1563 int ni; 1564 int pi; 1565 string n; 1566 int nl; 1567 1568 carret_at_end_of_word = false; 1569 1570 if (iter_pos >= text_length) { 1571 carret_at_end_of_word = true; 1572 return "".dup (); 1573 } 1574 1575 if (text.get_char (iter_pos) == '\n') { 1576 iter_pos += "\n".length; 1577 carret_at_end_of_word = (iter_pos == carret); 1578 return "\n".dup (); 1579 } 1580 1581 i = text.index_of (" ", iter_pos); 1582 pi = i + " ".length; 1583 1584 ni = text.index_of ("\t", iter_pos); 1585 if (ni != -1 && ni < pi || i == -1) { 1586 i = ni; 1587 pi = i + "\t".length; 1588 } 1589 1590 ni = text.index_of ("\n", iter_pos); 1591 if (ni != -1 && ni < pi || i == -1) { 1592 i = ni; 1593 pi = i; 1594 } 1595 1596 if (iter_pos + iter_pos - pi > text_length || i == -1) { 1597 n = text.substring (iter_pos); 1598 } else { 1599 n = text.substring (iter_pos, pi - iter_pos); 1600 } 1601 1602 nl = n.length; 1603 if (iter_pos < carret < iter_pos + nl) { 1604 n = text.substring (iter_pos, carret - iter_pos); 1605 nl = n.length; 1606 carret_at_end_of_word = true; 1607 } 1608 1609 iter_pos += nl; 1610 1611 if (iter_pos == carret) { 1612 carret_at_end_of_word = true; 1613 } 1614 1615 return n; 1616 } 1617 } 1618 1619 public class Carret : GLib.Object { 1620 1621 public int paragraph = 0; 1622 1623 public int character_index { 1624 get { 1625 return ci; 1626 } 1627 1628 set { 1629 ci = value; 1630 } 1631 } 1632 1633 private int ci = 0; 1634 1635 public double desired_x = 0; 1636 public double desired_y = 0; 1637 1638 public Carret () { 1639 } 1640 1641 public void print () { 1642 stdout.printf (@"paragraph: $paragraph, character_index: $character_index\n"); 1643 } 1644 1645 public Carret copy () { 1646 Carret c = new Carret (); 1647 1648 c.paragraph = paragraph; 1649 c.character_index = character_index; 1650 1651 c.desired_x = desired_x; 1652 c.desired_y = desired_y; 1653 1654 return c; 1655 } 1656 } 1657 } 1658 1659 } 1660