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