.
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 int last_paragraph = 0;
57 string text;
58 int text_length;
59
60 Gee.ArrayList<TextUndoItem> undo_items = new Gee.ArrayList<TextUndoItem> ();
61 Gee.ArrayList<TextUndoItem> redo_items = new Gee.ArrayList<TextUndoItem> ();
62
63 bool store_undo_state_at_next_event = false;
64
65 public bool editable;
66 public bool use_cache = true;
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 () || KeyBindings.has_logo ()) {
134 select_all ();
135 } else {
136 add_character (keyval);
137 }
138 break;
139 case 'c':
140 if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) {
141 ClipTool.copy_text (this);
142 } else {
143 add_character (keyval);
144 }
145 break;
146 case 'v':
147 if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) {
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 () || KeyBindings.has_logo ()) {
156 redo ();
157 } else {
158 add_character (keyval);
159 }
160 break;
161 case 'z':
162 if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) {
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 unichar c;
532
533 move_carret_one_character ();
534
535 if (KeyBindings.has_ctrl ()) {
536 while (true) {
537 c = move_carret_one_character ();
538
539 if (c == '\0' || c == ' ') {
540 break;
541 }
542 }
543 }
544 }
545
546 unichar move_carret_one_character () {
547 Paragraph paragraph;
548 int index;
549 unichar c;
550
551 return_if_fail (0 <= carret.paragraph < paragraphs.size);
552 paragraph = paragraphs.get (carret.paragraph);
553
554 index = carret.character_index;
555
556 paragraph.text.get_next_char (ref index, out c);
557
558 if (index >= paragraph.text_length && carret.paragraph + 1 < paragraphs.size) {
559 carret.paragraph++;
560 carret.character_index = 0;
561 c = ' ';
562 } else {
563 carret.character_index = index;
564 }
565
566 return c;
567 }
568
569 public void move_carret_previous () {
570 unichar c;
571
572 move_carret_back_one_character ();
573
574 if (KeyBindings.has_ctrl ()) {
575 while (true) {
576 c = move_carret_back_one_character ();
577
578 if (c == '\0' || c == ' ') {
579 break;
580 }
581 }
582 }
583 }
584
585 unichar move_carret_back_one_character () {
586 Paragraph paragraph;
587 int index, last_index;
588 unichar c;
589
590 return_if_fail (0 <= carret.paragraph < paragraphs.size);
591 paragraph = paragraphs.get (carret.paragraph);
592
593 index = 0;
594 last_index = -1;
595
596 while (paragraph.text.get_next_char (ref index, out c) && index < carret.character_index) {
597 last_index = index;
598 }
599
600 if (last_index <= 0 && carret.paragraph > 0) {
601 carret.paragraph--;
602
603 return_if_fail (0 <= carret.paragraph < paragraphs.size);
604 paragraph = paragraphs.get (carret.paragraph);
605 carret.character_index = paragraph.text_length;
606
607 if (paragraph.text.has_suffix ("\n")) {
608 carret.character_index -= "\n".length;
609 }
610
611 c = ' ';
612 } else if (last_index > 0) {
613 carret.character_index = last_index;
614 } else {
615 carret.character_index = 0;
616 c = ' ';
617 }
618
619 return_if_fail (0 <= carret.paragraph < paragraphs.size);
620
621 return c;
622 }
623
624 public void move_carret_next_row () {
625 double nr = font_size;
626
627 if (carret.desired_y + 2 * font_size >= allocation.height) {
628 scroll (2 * font_size);
629 nr = -font_size;
630 }
631
632 if (carret.desired_y + nr < widget_y + height - padding) {
633 carret = get_carret_at (carret.desired_x - widget_x - padding, carret.desired_y + nr);
634 }
635 }
636
637 public void move_carret_to_end_of_line () {
638 carret = get_carret_at (widget_x + padding + width, carret.desired_y, false);
639 }
640
641 public void move_carret_to_beginning_of_line () {
642 carret = get_carret_at (0, carret.desired_y, false);
643 }
644
645
646 public void move_carret_previous_row () {
647 double nr = -font_size;
648
649 if (carret.desired_y - 2 * font_size < 0) {
650 scroll (-2 * font_size);
651 nr = font_size;
652 }
653
654 if (carret.desired_y + nr > widget_y + padding) {
655 carret = get_carret_at (carret.desired_x, carret.desired_y + nr);
656 }
657 }
658
659 public bool has_selection () {
660 return show_selection && selection_is_visible ();
661 }
662
663 private bool selection_is_visible () {
664 return carret.paragraph != selection_end.paragraph || carret.character_index != selection_end.character_index;
665 }
666
667 public void insert_text (string t) {
668 string s;
669 Paragraph paragraph;
670 TextUndoItem ui;
671 Gee.ArrayList<string> pgs;
672 bool u = false;
673
674 pgs = new Gee.ArrayList<string> ();
675
676 if (single_line) {
677 s = t.replace ("\n", "").replace ("\r", "");
678 pgs.add (s);
679 } else {
680 if (t.last_index_of ("\n") > 0) {
681 string[] parts = t.split ("\n");
682 int i;
683 for (i = 0; i < parts.length -1; i++) {
684 pgs.add (parts[i]);
685 pgs.add ("\n");
686 }
687
688 pgs.add (parts[parts.length - 1]);
689
690 if (t.has_suffix ("\n")) {
691 pgs.add ("\n");
692 }
693 } else {
694 s = t;
695 pgs.add (s);
696 }
697 }
698
699 if (has_selection () && show_selection) {
700 ui = delete_selected_text ();
701 u = true;
702
703 if (paragraphs.size == 0) {
704 paragraphs.add (new Paragraph ("", font_size, 0));
705 }
706 } else {
707 ui = new TextUndoItem (carret);
708 }
709
710 return_if_fail (0 <= carret.paragraph < paragraphs.size);
711 paragraph = paragraphs.get (carret.paragraph);
712
713 if (pgs.size > 0) {
714 if (!u) {
715 ui.edited.add (paragraph.copy ());
716 }
717
718 string first = pgs.get (0);
719
720 string end;
721 string nt = paragraph.text.substring (0, carret.character_index);
722
723 nt += first;
724 end = paragraph.text.substring (carret.character_index);
725
726 paragraph.set_text (nt);
727
728 int paragraph_index = carret.paragraph;
729 Paragraph next_paragraph = paragraph;
730 for (int i = 1; i < pgs.size; i++) {
731 paragraph_index++;
732 string next = pgs.get (i);
733 next_paragraph = new Paragraph (next, font_size, paragraph_index);
734 paragraphs.insert (paragraph_index, next_paragraph);
735 ui.added.add (next_paragraph);
736 u = true;
737 }
738
739 carret.paragraph = paragraph_index;
740 carret.character_index = next_paragraph.text.length;
741
742 next_paragraph.set_text (next_paragraph.text + end);
743 }
744
745 if (u) {
746 undo_items.add (ui);
747 redo_items.clear ();
748 }
749
750 update_paragraph_index ();
751 layout ();
752
753 text_changed (get_text ());
754 show_selection = false;
755 }
756
757 public string get_text () {
758 StringBuilder sb = new StringBuilder ();
759
760 generate_all_paragraphs ();
761
762 foreach (Paragraph p in paragraphs) {
763 sb.append (p.text);
764 }
765
766 return sb.str;
767 }
768
769 Carret get_carret_at (double click_x, double click_y, bool check_boundaries = true) {
770 int i = 0;
771 double tx, ty;
772 double p;
773 string w;
774 int ch_index;
775 double min_d = double.MAX;
776 Carret c = new Carret ();
777 double dt;
778
779 c.paragraph = -1;
780 c.desired_x = click_x;
781 c.desired_y = click_y;
782
783 foreach (Paragraph paragraph in paragraphs) {
784 if (!check_boundaries || paragraph.text_is_on_screen (allocation, widget_y)) {
785 ch_index = 0;
786
787 if (paragraph.start_y + widget_y - font_size <= click_y <= paragraph.end_y + widget_y + font_size) {
788 foreach (Text next_word in paragraph.words) {
789 double tt_click = click_y - widget_y - padding + font_size; // - next_word.get_baseline_to_bottom (); //- font_size + next_word.get_baseline_to_bottom ();
790
791 w = next_word.text;
792 if (next_word.widget_y <= tt_click <= next_word.widget_y + font_size) {
793 Theme.text_color (next_word, "Foreground 1");
794
795 p = next_word.get_sidebearing_extent ();
796
797 if ((next_word.widget_y <= tt_click <= next_word.widget_y + font_size)
798 && (next_word.widget_x + widget_x <= click_x <= next_word.widget_x + widget_x + padding + next_word.get_sidebearing_extent ())) {
799
800 tx = widget_x + next_word.widget_x + padding;
801 ty = widget_y + next_word.widget_y + padding;
802
803 next_word.iterate ((glyph, kerning, last) => {
804 double cw;
805 int ci;
806 double d;
807 string gc = (!) glyph.get_unichar ().to_string ();
808
809 d = Math.fabs (click_x - tx);
810
811 if (d <= min_d) {
812 min_d = d;
813 c.character_index = ch_index;
814 c.paragraph = i;
815 }
816
817 cw = (glyph.get_width ()) * next_word.get_scale () + kerning;
818 ci = gc.length;
819
820 tx += cw;
821 ch_index += ci;
822 });
823
824 dt = Math.fabs (click_x - (tx + widget_x + padding));
825 if (dt < min_d) {
826 min_d = dt;
827 c.character_index = ch_index;
828 c.paragraph = i;
829 }
830 } else {
831 dt = Math.fabs (click_x - (next_word.widget_x + widget_x + padding + next_word.get_sidebearing_extent ()));
832
833 if (dt < min_d) {
834 min_d = dt;
835 c.character_index = ch_index + w.length;
836
837 if (w.has_suffix ("\n")) {
838 c.character_index -= "\n".length;
839 }
840
841 c.paragraph = i;
842 }
843
844 ch_index += w.length;
845 }
846 } else {
847 ch_index += w.length;
848 }
849 }
850 }
851 }
852 i++;
853 }
854
855 if (unlikely (c.paragraph < 0)) {
856 c.paragraph = paragraphs.size > 0 ? paragraphs.size - 1 : 0;
857 c.character_index = paragraphs.size > 0 ? paragraphs.get (c.paragraph).text.length : 0;
858 }
859
860 store_undo_state_at_next_event = true;
861
862 return c;
863 }
864
865 /** @return offset to click in text. */
866 public void layout () {
867 double p;
868 double tx, ty;
869 string w;
870 double xmax = 0;
871 int i = 0;
872 double dd;
873
874 tx = 0;
875 ty = font_size;
876
877 if (allocation.width <= 0 || allocation.height <= 0) {
878 warning ("Parent widget allocation is not set.");
879 }
880
881 for (i = paragraphs.size - 1; i >= 0 && paragraphs.size > 1; i--) {
882 if (unlikely (paragraphs.get (i).is_empty ())) {
883 warning ("Empty paragraph.");
884 paragraphs.remove_at (i);
885 update_paragraph_index ();
886 }
887 }
888
889 i = 0;
890 foreach (Paragraph paragraph in paragraphs) {
891 if (paragraph.need_layout
892 || (paragraph.text_area_width != width
893 && paragraph.text_is_on_screen (allocation, widget_y))) {
894
895 paragraph.start_y = ty;
896 paragraph.start_x = tx;
897
898 paragraph.cached_surface = null;
899
900 foreach (Text next_word in paragraph.words) {
901 next_word.set_font_size (font_size);
902
903 w = next_word.text;
904 p = next_word.get_sidebearing_extent ();
905
906 if (unlikely (p == 0)) {
907 warning (@"Zero width word: $(w)");
908 }
909
910 if (w == "") {
911 break;
912 }
913
914 if (w == "\n") {
915 next_word.widget_x = tx;
916 next_word.widget_y = ty;
917
918 tx = 0;
919 ty += next_word.font_size;
920 } else {
921 if (!single_line) {
922 if (tx + p + 2 * padding > width || w == "\n") {
923 tx = 0;
924 ty += next_word.font_size;
925 }
926 }
927
928 if (tx + p > xmax) {
929 xmax = tx + p;
930 }
931
932 next_word.widget_x = tx;
933 next_word.widget_y = ty;
934
935 if (w != "\n") {
936 tx += p;
937 }
938 }
939 }
940
941 if (tx > xmax) {
942 xmax = tx;
943 }
944
945 paragraph.text_area_width = width;
946 paragraph.width = xmax;
947 paragraph.end_x = tx;
948 paragraph.end_y = ty;
949 paragraph.need_layout = false;
950 }
951
952 if (xmax > width) {
953 break;
954 }
955
956 tx = paragraph.end_x;
957 ty = paragraph.end_y;
958 i++;
959 }
960
961 if (xmax > width) {
962 this.width = xmax + 2 * padding;
963 layout ();
964 return;
965 }
966
967 this.height = fmax (min_height, ty + 2 * padding);
968
969 if (last_paragraph != DONE) {
970 this.height = (text_length / (double) last_paragraph) * ty + 2 * padding; // estimate height
971 }
972
973 if (ty + widget_y < allocation.height && last_paragraph != DONE) {
974 generate_paragraphs ();
975 layout ();
976 return;
977 }
978
979 ty = font_size;
980 tx = 0;
981
982 foreach (Paragraph paragraph in paragraphs) {
983 dd = ty - paragraph.start_y;
984
985 if (dd != 0) {
986 paragraph.start_y += dd;
987 paragraph.end_y += dd;
988 foreach (Text word in paragraph.words) {
989 word.widget_y += dd;
990 }
991 }
992
993 ty = paragraph.end_y;
994 }
995 }
996
997 public void button_press (uint button, double x, double y) {
998 if (is_over (x, y)) {
999 carret = get_carret_at (x, y);
1000 selection_end = carret.copy ();
1001 update_selection = true;
1002 }
1003 }
1004
1005 public void button_release (uint button, double x, double y) {
1006 update_selection = false;
1007 show_selection = selection_is_visible ();
1008 }
1009
1010 public bool motion (double x, double y) {
1011 if (update_selection) {
1012 selection_end = get_carret_at (x, y);
1013 show_selection = selection_is_visible ();
1014 }
1015
1016 return update_selection;
1017 }
1018
1019 public override void draw (Context cr) {
1020 Text word;
1021 double tx, ty;
1022 string w;
1023 double scale;
1024 double width;
1025 double x = widget_x;
1026 double y = widget_y;
1027 Carret selection_start, selection_stop;
1028 double carret_x;
1029 double carret_y;
1030
1031 layout ();
1032
1033 if (draw_border) {
1034 // background
1035 cr.save ();
1036 cr.set_line_width (1);
1037 Theme.color (cr, "Text Area Background");
1038 draw_rounded_rectangle (cr, x, y, this.width, this.height - padding, padding);
1039 cr.fill ();
1040 cr.restore ();
1041
1042 // border
1043 cr.save ();
1044 cr.set_line_width (1);
1045 Theme.color (cr, "Foreground 1");
1046 draw_rounded_rectangle (cr, x, y, this.width, this.height - padding, padding);
1047 cr.stroke ();
1048 cr.restore ();
1049 }
1050
1051 cr.save ();
1052
1053 word = new Text ();
1054 word.use_cache (use_cache);
1055
1056 width = this.width - padding;
1057 x += padding;
1058 scale = word.get_scale ();
1059 y += font_size;
1060
1061 // draw selection background
1062 if (has_selection ()) {
1063 tx = 0;
1064 ty = 0;
1065
1066 selection_start = get_selection_start ();
1067 selection_stop = get_selection_stop ();
1068
1069 cr.save ();
1070 Theme.color (cr, "Highlighted 1");
1071
1072 for (int i = selection_start.paragraph; i <= selection_stop.paragraph; i++) {
1073 return_if_fail (0 <= i < paragraphs.size);
1074 Paragraph pg = paragraphs.get (i);
1075
1076 if (pg.text_is_on_screen (allocation, widget_y)) {
1077 int char_index = 0;
1078
1079 foreach (Text next_word in pg.words) {
1080 double cw = next_word.get_sidebearing_extent ();
1081 bool paint_background = false;
1082 bool partial_start = false;
1083 bool partial_stop = false;
1084 int wl;
1085
1086 w = next_word.text;
1087 wl = w.length;
1088 scale = next_word.get_scale ();
1089
1090 if (selection_start.paragraph == selection_stop.paragraph) {
1091 partial_start = true;
1092 partial_stop = true;
1093 } else if (selection_start.paragraph < i < selection_stop.paragraph) {
1094 paint_background = true;
1095 } else if (selection_start.paragraph == i) {
1096 paint_background = true;
1097 partial_start = true;
1098 } else if (selection_stop.paragraph == i) {
1099 paint_background = char_index + wl < selection_stop.character_index;
1100 partial_stop = !paint_background;
1101 }
1102
1103 if (paint_background && !(partial_start || partial_stop)) {
1104 double selection_y = widget_y + next_word.widget_y + scale * -next_word.font.bottom_limit - font_size;
1105 cr.rectangle (widget_x + padding + next_word.widget_x - 1, selection_y, cw + 1, font_size);
1106 cr.fill ();
1107 }
1108
1109 if (partial_start || partial_stop) {
1110 int index = char_index;
1111 double bx = widget_x + padding + next_word.widget_x + (partial_start ? 0 : 1);
1112
1113 next_word.iterate ((glyph, kerning, last) => {
1114 double cwi;
1115 int ci;
1116 bool draw = (index >= selection_start.character_index && partial_start && !partial_stop)
1117 || (index < selection_stop.character_index && !partial_start && partial_stop)
1118 || (selection_start.character_index <= index < selection_stop.character_index && partial_start && partial_stop);
1119
1120 cwi = (glyph.get_width ()) * next_word.get_scale () + kerning;
1121
1122 if (draw) {
1123 double selection_y = widget_y + next_word.widget_y + scale * -next_word.font.bottom_limit - font_size;
1124 cr.rectangle (bx - 1, selection_y, cwi + 1, font_size);
1125 cr.fill ();
1126 }
1127
1128 bx += cwi;
1129 ci = ((!) glyph.get_unichar ().to_string ()).length;
1130 index += ci;
1131 });
1132 }
1133
1134 char_index += w.length;
1135 }
1136 }
1137 }
1138
1139 cr.restore ();
1140 }
1141
1142 tx = 0;
1143 ty = 0;
1144
1145 int first_visible = 0;
1146 int last_visible;
1147 int paragraphs_size = paragraphs.size;
1148 while (first_visible < paragraphs_size) {
1149 if (paragraphs.get (first_visible).text_is_on_screen (allocation, widget_y)) {
1150 break;
1151 }
1152 first_visible++;
1153 }
1154
1155 last_visible = first_visible;
1156 while (last_visible < paragraphs_size) {
1157 if (!paragraphs.get (last_visible).text_is_on_screen (allocation, widget_y)) {
1158 last_visible++;
1159 break;
1160 }
1161 last_visible++;
1162 }
1163
1164 if (paragraphs_size == 0) {
1165 if (carret_is_visible) {
1166 draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding);
1167 }
1168
1169 return;
1170 }
1171
1172 Context cc; // cached context
1173 Paragraph paragraph;
1174 paragraph = paragraphs.get (0);
1175
1176 tx = paragraph.start_x;
1177 ty = paragraph.start_y;
1178
1179 if (paragraphs.size > 0 && paragraphs.get (0).words.size > 0) {
1180 Text t = paragraphs.get (0).words.get (0);
1181 Theme.text_color (t, "Foreground 1");
1182 }
1183
1184 for (int i = first_visible; i < last_visible; i++) {
1185 paragraph = paragraphs.get (i);
1186
1187 tx = paragraph.start_x;
1188 ty = paragraph.start_y;
1189
1190 if (paragraph.cached_surface == null) {
1191 paragraph.cached_surface = new Surface.similar (cr.get_target (), Cairo.Content.COLOR_ALPHA, (int) width + 2, paragraph.get_height () + (int) font_size + 2);
1192 cc = new Context ((!) paragraph.cached_surface);
1193
1194 foreach (Text next_word in paragraph.words) {
1195 Theme.text_color (next_word, "Foreground 1");
1196
1197 if (next_word.text != "\n") {
1198 next_word.draw_at_top (cc, next_word.widget_x, next_word.widget_y - ty);
1199 }
1200 }
1201 }
1202
1203 if (likely (paragraph.cached_surface != null)) {
1204 // FIXME: subpixel offset in text area
1205 cr.set_source_surface ((!) paragraph.cached_surface, (int) (x + tx), (int) (widget_y + paragraph.start_y - font_size + padding));
1206 cr.paint ();
1207 } else {
1208 warning ("No paragraph image.");
1209 }
1210 }
1211
1212 if (carret_is_visible) {
1213 get_carret_position (carret, out carret_x, out carret_y);
1214
1215 if (carret_y < 0) {
1216 draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding);
1217 } else {
1218 draw_carret_at (cr, carret_x, carret_y);
1219 }
1220 }
1221
1222 if (has_selection ()) {
1223 get_carret_position (selection_end, out carret_x, out carret_y);
1224
1225 if (carret_y < 0) {
1226 draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding);
1227 } else {
1228 draw_carret_at (cr, carret_x, carret_y);
1229 }
1230 }
1231 }
1232
1233 void get_carret_position (Carret carret, out double carret_x, out double carret_y) {
1234 Paragraph paragraph;
1235 double tx;
1236 double ty;
1237 int ch_index;
1238 int wl;
1239 double pos_x, pos_y;
1240
1241 ch_index = 0;
1242
1243 carret_x = -1;
1244 carret_y = -1;
1245
1246 return_if_fail (0 <= carret.paragraph < paragraphs.size);
1247 paragraph = paragraphs.get (carret.paragraph);
1248
1249 pos_x = -1;
1250 pos_y = -1;
1251
1252 foreach (Text next_word in paragraph.words) {
1253 string w = next_word.text;
1254 wl = w.length;
1255
1256 if (carret.character_index == ch_index) {
1257 pos_x = next_word.widget_x + widget_x + padding;
1258 pos_y = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom ();
1259 } else if (carret.character_index >= ch_index + wl) {
1260 pos_x = next_word.widget_x + next_word.get_sidebearing_extent () + widget_x + padding;
1261 pos_y = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom ();
1262
1263 if (next_word.text.has_suffix ("\n")) {
1264 pos_x = widget_x + padding;
1265 pos_y += next_word.font_size;
1266 }
1267 } else if (ch_index < carret.character_index <= ch_index + wl) {
1268 tx = widget_x + next_word.widget_x;
1269 ty = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom ();
1270
1271 if (carret.character_index <= ch_index) {
1272 pos_x = widget_x + padding;
1273 pos_y = ty;
1274 }
1275
1276 next_word.iterate ((glyph, kerning, last) => {
1277 double cw;
1278 int ci;
1279
1280 cw = (glyph.get_width ()) * next_word.get_scale () + kerning;
1281 ci = ((!) glyph.get_unichar ().to_string ()).length;
1282
1283 if (ch_index < carret.character_index <= ch_index + ci) {
1284 pos_x = tx + cw + padding;
1285 pos_y = ty;
1286
1287 if (glyph.get_unichar () == '\n') {
1288 pos_x = widget_x + padding;
1289 pos_y += next_word.font_size;
1290 }
1291 }
1292
1293 tx += cw;
1294 ch_index += ci;
1295 });
1296 }
1297
1298 ch_index += wl;
1299 }
1300
1301 carret_x = pos_x;
1302 carret_y = pos_y;
1303 }
1304
1305 void draw_carret_at (Context cr, double x, double y) {
1306 cr.save ();
1307 cr.set_source_rgba (0, 0, 0, 0.5);
1308 cr.set_line_width (1);
1309 cr.move_to (x, y);
1310 cr.line_to (x, y - font_size);
1311 cr.stroke ();
1312 cr.restore ();
1313 }
1314
1315 public void store_undo_edit_state () {
1316 TextUndoItem ui = new TextUndoItem (carret);
1317 ui.edited.add (get_current_paragraph ().copy ());
1318 undo_items.add (ui);
1319 redo_items.clear ();
1320 }
1321
1322 public void redo () {
1323 TextUndoItem i;
1324 TextUndoItem undo_item;
1325
1326 if (redo_items.size > 0) {
1327 i = redo_items.get (redo_items.size - 1);
1328
1329 undo_item = new TextUndoItem (i.carret);
1330
1331 i.deleted.sort ((a, b) => {
1332 Paragraph pa = (Paragraph) a;
1333 Paragraph pb = (Paragraph) b;
1334 return pb.index - pa.index;
1335 });
1336
1337 i.added.sort ((a, b) => {
1338 Paragraph pa = (Paragraph) a;
1339 Paragraph pb = (Paragraph) b;
1340 return pa.index - pb.index;
1341 });
1342
1343 foreach (Paragraph p in i.deleted) {
1344 if (unlikely (!(0 <= p.index < paragraphs.size))) {
1345 warning ("Paragraph not found.");
1346 } else {
1347 undo_item.deleted.add (p.copy ());
1348 paragraphs.remove_at (p.index);
1349 }
1350 }
1351
1352 foreach (Paragraph p in i.added) {
1353 if (p.index == paragraphs.size) {
1354 paragraphs.add (p.copy ());
1355 } else {
1356 if (unlikely (!(0 <= p.index < paragraphs.size))) {
1357 warning (@"Index: $(p.index) out of bounds, size: $(paragraphs.size)");
1358 } else {
1359 undo_item.added.add (paragraphs.get (p.index).copy ());
1360 paragraphs.insert (p.index, p.copy ());
1361 }
1362 }
1363 }
1364
1365 foreach (Paragraph p in i.edited) {
1366 if (unlikely (!(0 <= p.index < paragraphs.size))) {
1367 warning (@"Index: $(p.index ) out of bounds, size: $(paragraphs.size)");
1368 return;
1369 }
1370
1371 undo_item.edited.add (paragraphs.get (p.index).copy ());
1372 paragraphs.set (p.index, p.copy ());
1373 }
1374
1375 redo_items.remove_at (redo_items.size - 1);
1376 undo_items.add (undo_item);
1377
1378 carret = i.carret.copy ();
1379 layout ();
1380 }
1381 }
1382
1383 public void undo () {
1384 TextUndoItem i;
1385 TextUndoItem redo_item;
1386
1387 if (undo_items.size > 0) {
1388 i = undo_items.get (undo_items.size - 1);
1389 redo_item = new TextUndoItem (i.carret);
1390
1391 i.deleted.sort ((a, b) => {
1392 Paragraph pa = (Paragraph) a;
1393 Paragraph pb = (Paragraph) b;
1394 return pa.index - pb.index;
1395 });
1396
1397 i.added.sort ((a, b) => {
1398 Paragraph pa = (Paragraph) a;
1399 Paragraph pb = (Paragraph) b;
1400 return pb.index - pa.index;
1401 });
1402
1403 foreach (Paragraph p in i.added) {
1404 if (unlikely (!(0 <= p.index < paragraphs.size))) {
1405 warning ("Paragraph not found.");
1406 } else {
1407 redo_item.added.add (paragraphs.get (p.index).copy ());
1408 paragraphs.remove_at (p.index);
1409 }
1410 }
1411
1412 foreach (Paragraph p in i.deleted) {
1413 if (p.index == paragraphs.size) {
1414 paragraphs.add (p.copy ());
1415 } else {
1416 if (unlikely (!(0 <= p.index < paragraphs.size))) {
1417 warning (@"Index: $(p.index) out of bounds, size: $(paragraphs.size)");
1418 } else {
1419 redo_item.deleted.add (p.copy ());
1420 paragraphs.insert (p.index, p.copy ());
1421 }
1422 }
1423 }
1424
1425 foreach (Paragraph p in i.edited) {
1426 if (unlikely (!(0 <= p.index < paragraphs.size))) {
1427 warning (@"Index: $(p.index ) out of bounds, size: $(paragraphs.size)");
1428 return;
1429 }
1430
1431 redo_item.edited.add (paragraphs.get (p.index).copy ());
1432 paragraphs.set (p.index, p.copy ());
1433 }
1434
1435 undo_items.remove_at (undo_items.size - 1);
1436 redo_items.add (redo_item);
1437
1438 carret = i.carret.copy ();
1439 layout ();
1440 }
1441 }
1442
1443 public void set_editable (bool editable) {
1444 this.editable = editable;
1445 }
1446
1447 public class TextUndoItem : GLib.Object {
1448 public Carret carret;
1449 public Gee.ArrayList<Paragraph> added = new Gee.ArrayList<Paragraph> ();
1450 public Gee.ArrayList<Paragraph> edited = new Gee.ArrayList<Paragraph> ();
1451 public Gee.ArrayList<Paragraph> deleted = new Gee.ArrayList<Paragraph> ();
1452
1453 public TextUndoItem (Carret c) {
1454 carret = c.copy ();
1455 }
1456 }
1457
1458 public class Paragraph : GLib.Object {
1459 public double end_x = -10000;
1460 public double end_y = -10000;
1461
1462 public double start_x = -10000;
1463 public double start_y = -10000;
1464
1465 public double width = -10000;
1466 public double text_area_width = -10000;
1467
1468 public string text;
1469
1470 public Gee.ArrayList<Text> words {
1471 get {
1472 if (words_in_paragraph.size == 0) {
1473 generate_words ();
1474 }
1475
1476 return words_in_paragraph;
1477 }
1478 }
1479
1480 private Gee.ArrayList<Text> words_in_paragraph = new Gee.ArrayList<Text> ();
1481
1482 public int text_length;
1483
1484 public bool need_layout = true;
1485
1486 public Surface? cached_surface = null;
1487
1488 double font_size;
1489
1490 public int index;
1491
1492 public Paragraph (string text, double font_size, int index) {
1493 this.index = index;
1494 this.font_size = font_size;
1495 set_text (text);
1496 }
1497
1498 public Paragraph copy () {
1499 Paragraph p = new Paragraph (text.dup (), font_size, index);
1500 p.need_layout = true;
1501 return p;
1502 }
1503
1504 public bool is_empty () {
1505 return text == "";
1506 }
1507
1508 public void set_text (string t) {
1509 this.text = t;
1510 text_length = t.length;
1511 need_layout = true;
1512 words.clear ();
1513 cached_surface = null;
1514 }
1515
1516 public int get_height () {
1517 return (int) (end_y - start_y) + 1;
1518 }
1519
1520 public int get_width () {
1521 return (int) width + 1;
1522 }
1523
1524 public bool text_is_on_screen (WidgetAllocation alloc, double widget_y) {
1525 bool v = (0 <= start_y + widget_y <= alloc.height)
1526 || (0 <= end_y + widget_y <= alloc.height)
1527 || (start_y + widget_y <= 0 && alloc.height <= end_y + widget_y);
1528 return v;
1529 }
1530
1531 private void generate_words () {
1532 string w;
1533 int p = 0;
1534 bool carret_at_word_end = false;
1535 Text word;
1536 int carret = 0;
1537 int iter_pos = 0;
1538
1539 return_if_fail (words_in_paragraph.size == 0);
1540
1541 while (p < text_length) {
1542 w = get_next_word (out carret_at_word_end, ref iter_pos, carret);
1543
1544 if (w == "") {
1545 break;
1546 }
1547
1548 word = new Text (w, font_size);
1549 words_in_paragraph.add (word);
1550 }
1551 }
1552
1553 string get_next_word (out bool carret_at_end_of_word, ref int iter_pos, int carret) {
1554 int i;
1555 int ni;
1556 int pi;
1557 string n;
1558 int nl;
1559
1560 carret_at_end_of_word = false;
1561
1562 if (iter_pos >= text_length) {
1563 carret_at_end_of_word = true;
1564 return "".dup ();
1565 }
1566
1567 if (text.get_char (iter_pos) == '\n') {
1568 iter_pos += "\n".length;
1569 carret_at_end_of_word = (iter_pos == carret);
1570 return "\n".dup ();
1571 }
1572
1573 i = text.index_of (" ", iter_pos);
1574 pi = i + " ".length;
1575
1576 ni = text.index_of ("\t", iter_pos);
1577 if (ni != -1 && ni < pi || i == -1) {
1578 i = ni;
1579 pi = i + "\t".length;
1580 }
1581
1582 ni = text.index_of ("\n", iter_pos);
1583 if (ni != -1 && ni < pi || i == -1) {
1584 i = ni;
1585 pi = i;
1586 }
1587
1588 if (iter_pos + iter_pos - pi > text_length || i == -1) {
1589 n = text.substring (iter_pos);
1590 } else {
1591 n = text.substring (iter_pos, pi - iter_pos);
1592 }
1593
1594 nl = n.length;
1595 if (iter_pos < carret < iter_pos + nl) {
1596 n = text.substring (iter_pos, carret - iter_pos);
1597 nl = n.length;
1598 carret_at_end_of_word = true;
1599 }
1600
1601 iter_pos += nl;
1602
1603 if (iter_pos == carret) {
1604 carret_at_end_of_word = true;
1605 }
1606
1607 return n;
1608 }
1609 }
1610
1611 public class Carret : GLib.Object {
1612
1613 public int paragraph = 0;
1614
1615 public int character_index {
1616 get {
1617 return ci;
1618 }
1619
1620 set {
1621 ci = value;
1622 }
1623 }
1624
1625 private int ci = 0;
1626
1627 public double desired_x = 0;
1628 public double desired_y = 0;
1629
1630 public Carret () {
1631 }
1632
1633 public void print () {
1634 stdout.printf (@"paragraph: $paragraph, character_index: $character_index\n");
1635 }
1636
1637 public Carret copy () {
1638 Carret c = new Carret ();
1639
1640 c.paragraph = paragraph;
1641 c.character_index = character_index;
1642
1643 c.desired_x = desired_x;
1644 c.desired_y = desired_y;
1645
1646 return c;
1647 }
1648 }
1649 }
1650
1651 }
1652