]> gitweb.factorcode.org Git - factor.git/blob - basis/ui/gadgets/editors/editors.factor
74ffbd46954689ba97ad3757afb0e66a2c022c2c
[factor.git] / basis / ui / gadgets / editors / editors.factor
1 ! Copyright (C) 2006, 2011 Slava Pestov
2 ! See http://factorcode.org/license.txt for BSD license.
3 USING: accessors arrays assocs calendar colors.constants combinators
4 combinators.short-circuit documents documents.elements fonts fry
5 grouping kernel locals make math math.functions math.order math.ranges
6 math.rectangles math.vectors models models.arrow namespaces opengl
7 sequences sorting splitting timers ui.baseline-alignment ui.clipboards
8 ui.commands ui.gadgets ui.gadgets.borders ui.gadgets.line-support
9 ui.gadgets.menus ui.gadgets.scrollers ui.gestures ui.pens.solid
10 ui.render ui.text ui.theme unicode ;
11 IN: ui.gadgets.editors
12
13 TUPLE: editor < line-gadget
14     caret-color
15     caret mark
16     focused? blink blink-timer
17     default-text ;
18
19 <PRIVATE
20
21 : <loc> ( -- loc ) { 0 0 } <model> ;
22
23 : init-editor-locs ( editor -- editor )
24     <loc> >>caret
25     <loc> >>mark ; inline
26
27 : editor-theme ( editor -- editor )
28     COLOR: red >>caret-color
29     monospace-font >>font ; inline
30
31 PRIVATE>
32
33 : new-editor ( class -- editor )
34     new-line-gadget
35         <document> >>model
36         init-editor-locs
37         editor-theme ; inline
38
39 : <editor> ( -- editor )
40     editor new-editor ;
41
42 <PRIVATE
43
44 : activate-editor-model ( editor model -- )
45     [ add-connection ]
46     [ nip activate-model ]
47     [ swap model>> add-loc ] 2tri ;
48
49 : deactivate-editor-model ( editor model -- )
50     [ remove-connection ]
51     [ nip deactivate-model ]
52     [ swap model>> remove-loc ] 2tri ;
53
54 : blink-caret ( editor -- )
55     [ not ] change-blink relayout-1 ;
56
57 SYMBOL: blink-interval
58
59 750 milliseconds blink-interval set-global
60
61 : stop-blinking ( editor -- )
62     blink-timer>> [ stop-timer ] when* ;
63
64 : start-blinking ( editor -- )
65     t >>blink
66     blink-timer>> [ restart-timer ] when* ;
67
68 : restart-blinking ( editor -- )
69     dup focused?>> [
70         [ start-blinking ]
71         [ relayout-1 ]
72         bi
73     ] [ drop ] if ;
74
75 PRIVATE>
76
77 M: editor graft*
78     [ dup caret>> activate-editor-model ]
79     [ dup mark>> activate-editor-model ]
80     [
81         [
82             '[ _ blink-caret ] blink-interval get dup <timer>
83         ] keep blink-timer<<
84     ] tri ;
85
86 M: editor ungraft*
87     [ [ stop-blinking ] [ f >>blink-timer drop ] bi ]
88     [ dup caret>> deactivate-editor-model ]
89     [ dup mark>> deactivate-editor-model ] tri ;
90
91 : editor-caret ( editor -- loc ) caret>> value>> ;
92
93 : editor-mark ( editor -- loc ) mark>> value>> ;
94
95 : set-caret ( loc editor -- )
96     [ model>> validate-loc ] [ caret>> ] bi set-model ;
97
98 : change-caret ( editor quot: ( loc document -- newloc ) -- )
99     [ [ [ editor-caret ] [ model>> ] bi ] dip call ] [ drop ] 2bi
100     set-caret ; inline
101
102 : mark>caret ( editor -- )
103     [ editor-caret ] [ mark>> ] bi set-model ;
104
105 : change-caret&mark ( editor quot: ( loc document -- newloc ) -- )
106     [ change-caret ] [ drop mark>caret ] 2bi ; inline
107
108 : editor-line ( n editor -- str ) control-value nth ;
109
110 :: point>loc ( point editor -- loc )
111     point second editor y>line {
112         { [ dup 0 < ] [ drop { 0 0 } ] }
113         { [ dup editor model>> last-line# > ] [ drop editor model>> doc-end ] }
114         [| n |
115             n
116             point first
117             editor font>>
118             n editor editor-line
119             x>offset 2array
120         ]
121     } cond ;
122
123 : clicked-loc ( editor -- loc )
124     [ hand-rel ] keep point>loc ;
125
126 : click-loc ( editor model -- )
127     [ clicked-loc ] dip set-model ;
128
129 : focus-editor ( editor -- )
130     [ start-blinking ] [ t >>focused? relayout-1 ] bi ;
131
132 : unfocus-editor ( editor -- )
133     [ stop-blinking ] [ f >>focused? relayout-1 ] bi ;
134
135 : loc>x ( loc editor -- x )
136     [ first2 swap ] dip [ editor-line ] [ font>> ] bi swap offset>x round ;
137
138 : loc>point ( loc editor -- loc )
139     [ loc>x ] [ [ first ] dip line>y ceiling ] 2bi 2array ;
140
141 : caret-loc ( editor -- loc )
142     [ editor-caret ] keep loc>point ;
143
144 : caret-dim ( editor -- dim )
145     [ 0 ] dip line-height 2array ;
146
147 : scroll>caret ( editor -- )
148     dup graft-state>> second [
149         [
150             [ caret-loc ] [ caret-dim { 2 1 } v+ ] bi <rect>
151         ] keep scroll>rect
152     ] [ drop ] if ;
153
154 <PRIVATE
155
156 : draw-caret? ( editor -- ? )
157     { [ focused?>> ] [ blink>> ] } 1&& ;
158
159 : draw-caret ( editor -- )
160     dup draw-caret? [
161         [ caret-color>> gl-color ]
162         [
163             [ caret-loc ] [ caret-dim ] bi
164             over v+ gl-line
165         ] bi
166     ] [ drop ] if ;
167
168 : selection-start/end ( editor -- start end )
169     [ editor-mark ] [ editor-caret ] bi sort-pair ;
170
171 SYMBOL: selected-lines
172
173 TUPLE: selected-line start end first? last? ;
174
175 : compute-selection ( editor -- assoc )
176     dup gadget-selection? [
177         [ selection-start/end [ [ first ] bi@ [a,b] ] [ ] 2bi ]
178         [ model>> ] bi
179         '[ [ _ _ ] [ _ start/end-on-line ] bi 2array ] H{ } map>assoc
180     ] [ drop f ] if ;
181
182 :: draw-selection ( line pair editor -- )
183     pair [ editor font>> line offset>x ] map :> pair
184     editor selection-color>> gl-color
185     pair first 0 2array
186     pair second pair first - round 1 max editor line-height 2array
187     gl-fill-rect ;
188
189 : draw-unselected-line ( line editor -- )
190     font>> swap draw-text ;
191
192 : draw-selected-line ( line pair editor -- )
193     over all-equal? [
194         [ nip draw-unselected-line ] [ draw-selection ] 3bi
195     ] [
196         [ draw-selection ]
197         [
198             [ [ first2 ] [ selection-color>> ] bi* <selection> ]
199             [ draw-unselected-line ] bi
200         ] 3bi
201     ] if ;
202
203 : draw-default-text? ( editor -- ? )
204     { [ default-text>> ] [ model>> doc-string empty? ] } 1&& ;
205
206 : draw-default-text ( editor -- )
207     [ font>> clone line-color >>foreground ]
208     [ default-text>> ] bi draw-text ;
209
210 PRIVATE>
211
212 M: editor draw-line ( line index editor -- )
213     [ selected-lines get at ] dip over
214     [ draw-selected-line ] [ nip draw-unselected-line ] if ;
215
216 M: editor draw-gadget*
217     dup draw-default-text? [
218         [ draw-default-text ] [ draw-caret ] bi
219     ] [
220         dup compute-selection selected-lines [
221             [ draw-lines ] [ draw-caret ] bi
222         ] with-variable
223     ] if ;
224
225 M: editor pref-dim*
226     ! Add some space for the caret.
227     [ font>> ] keep dup draw-default-text?
228     [ default-text>> ] [ control-value ] if
229     text-dim { 1 0 } v+ ;
230
231 M: editor baseline font>> font-metrics ascent>> ;
232
233 M: editor cap-height font>> font-metrics cap-height>> ;
234
235 <PRIVATE
236
237 : contents-changed ( model editor -- )
238     [ [ nip caret>> ] [ drop ] 2bi '[ _ validate-loc ] (change-model) ]
239     [ [ nip mark>> ] [ drop ] 2bi '[ _ validate-loc ] (change-model) ]
240     [ nip relayout ] 2tri ;
241
242 : caret/mark-changed ( editor -- )
243     [ restart-blinking ] keep scroll>caret ;
244
245 PRIVATE>
246
247 M: editor model-changed
248     {
249         { [ 2dup model>> eq? ] [ contents-changed ] }
250         { [ 2dup caret>> eq? ] [ nip caret/mark-changed ] }
251         { [ 2dup mark>> eq? ] [ nip caret/mark-changed ] }
252     } cond ;
253
254 M: editor gadget-selection?
255     selection-start/end = not ;
256
257 M: editor gadget-selection
258     [ selection-start/end ] [ model>> ] bi doc-range ;
259
260 : remove-selection ( editor -- )
261     [ selection-start/end ] [ model>> ] bi remove-doc-range ;
262
263 M: editor user-input*
264     [ selection-start/end ] [ model>> ] bi set-doc-range t ;
265
266 : editor-string ( editor -- string )
267     model>> doc-string ;
268
269 : set-editor-string ( string editor -- )
270     model>> set-doc-string ;
271
272 M: editor gadget-text* editor-string % ;
273
274 : extend-selection ( editor -- )
275     [ request-focus ]
276     [ restart-blinking ]
277     [ dup caret>> click-loc ] tri ;
278
279 : mouse-elt ( -- element )
280     hand-click# get {
281         { 1 one-char-elt }
282         { 2 one-word-elt }
283     } at one-line-elt or ;
284
285 : drag-direction? ( loc editor -- ? )
286     editor-mark before? ;
287
288 : drag-selection-caret ( loc editor element -- loc )
289     [
290         [ drag-direction? ] [ model>> ] 2bi
291     ] dip prev/next-elt ? ;
292
293 : drag-selection-mark ( loc editor element -- loc )
294     [
295         [ drag-direction? not ]
296         [ editor-mark ]
297         [ model>> ] tri
298     ] dip prev/next-elt ? ;
299
300 : drag-caret&mark ( editor -- caret mark )
301     [ clicked-loc ] [ mouse-elt ] bi
302     [ drag-selection-caret ]
303     [ drag-selection-mark ] 3bi ;
304
305 : drag-selection ( editor -- )
306     [ drag-caret&mark ]
307     [ mark>> set-model ]
308     [ caret>> set-model ] tri ;
309
310 : editor-cut ( editor clipboard -- )
311     [ gadget-copy ] [ drop remove-selection ] 2bi ;
312
313 : delete/backspace ( editor quot -- )
314     over gadget-selection? [
315         drop remove-selection
316     ] [
317         [ [ [ editor-caret ] [ model>> ] bi ] dip call ]
318         [ drop model>> ]
319         2bi remove-doc-range
320     ] if ; inline
321
322 : editor-delete ( editor elt -- )
323     '[ dupd _ next-elt ] delete/backspace ;
324
325 : editor-backspace ( editor elt -- )
326     '[ over [ _ prev-elt ] dip ] delete/backspace ;
327
328 : editor-select-prev ( editor elt -- )
329     '[ _ prev-elt ] change-caret ;
330
331 : editor-prev ( editor elt -- )
332     [ editor-select-prev ] [ drop mark>caret ] 2bi ;
333
334 : editor-select-next ( editor elt -- )
335     '[ _ next-elt ] change-caret ;
336
337 : editor-next ( editor elt -- )
338     dupd editor-select-next mark>caret ;
339
340 : editor-select ( from to editor -- )
341     [ mark>> set-model ] [ caret>> set-model ] bi-curry bi* ;
342
343 : select-elt ( editor elt -- )
344     [ [ [ editor-caret ] [ model>> ] bi ] dip prev/next-elt ] [ drop ] 2bi
345     editor-select ;
346
347 : start-of-document ( editor -- ) doc-elt editor-prev ;
348
349 : end-of-document ( editor -- ) doc-elt editor-next ;
350
351 : position-caret ( editor -- )
352     mouse-elt dup one-char-elt =
353     [ drop dup extend-selection dup mark>> click-loc ]
354     [ select-elt ] if ;
355
356 : delete-previous-character ( editor -- )
357     char-elt editor-backspace ;
358
359 : delete-next-character ( editor -- )
360     char-elt editor-delete ;
361
362 : delete-previous-word ( editor -- )
363     word-elt editor-backspace ;
364
365 : delete-next-word ( editor -- )
366     word-elt editor-delete ;
367
368 : delete-to-start-of-line ( editor -- )
369     one-line-elt editor-backspace ;
370
371 : delete-to-end-of-line ( editor -- )
372     one-line-elt editor-delete ;
373
374 : delete-to-start-of-document ( editor -- )
375     doc-elt editor-delete ;
376
377 : delete-to-end-of-document ( editor -- )
378     doc-elt editor-delete ;
379
380 : com-undo ( editor -- ) model>> undo ;
381
382 : com-redo ( editor -- ) model>> redo ;
383
384 editor "editing" f {
385     { undo-action com-undo }
386     { redo-action com-redo }
387     { T{ key-down f f "DELETE" } delete-next-character }
388     { T{ key-down f f "BACKSPACE" } delete-previous-character }
389     { T{ key-down f { S+ } "BACKSPACE" } delete-previous-character }
390     { T{ key-down f { C+ } "DELETE" } delete-next-word }
391     { T{ key-down f { C+ } "BACKSPACE" } delete-previous-word }
392     { T{ key-down f { A+ } "DELETE" } delete-to-end-of-line }
393     { T{ key-down f { A+ } "BACKSPACE" } delete-to-start-of-line }
394 } define-command-map
395
396 : com-paste ( editor -- ) clipboard get paste-clipboard ;
397
398 : paste-selection ( editor -- ) ui.clipboards:selection get paste-clipboard ;
399
400 : com-cut ( editor -- ) clipboard get editor-cut ;
401
402 editor "clipboard" f {
403     { cut-action com-cut }
404     { copy-action com-copy }
405     { paste-action com-paste }
406     { T{ button-up } com-copy-selection }
407     { T{ button-up f f 2 } paste-selection }
408 } define-command-map
409
410 : previous-character ( editor -- )
411     dup gadget-selection? [
412         dup selection-start/end drop
413         over set-caret mark>caret
414     ] [
415         char-elt editor-prev
416     ] if ;
417
418 : next-character ( editor -- )
419     dup gadget-selection? [
420         dup selection-start/end nip
421         over set-caret mark>caret
422     ] [
423         char-elt editor-next
424     ] if ;
425
426 : previous-word ( editor -- ) word-elt editor-prev ;
427
428 : next-word ( editor -- ) word-elt editor-next ;
429
430 : start-of-line ( editor -- ) one-line-elt editor-prev ;
431
432 : end-of-line ( editor -- ) one-line-elt editor-next ;
433
434 editor "caret-motion" f {
435     { T{ button-down } position-caret }
436     { T{ key-down f f "LEFT" } previous-character }
437     { T{ key-down f f "RIGHT" } next-character }
438     { T{ key-down f { C+ } "LEFT" } previous-word }
439     { T{ key-down f { C+ } "RIGHT" } next-word }
440     { T{ key-down f f "HOME" } start-of-line }
441     { T{ key-down f f "END" } end-of-line }
442     { T{ key-down f { C+ } "HOME" } start-of-document }
443     { T{ key-down f { C+ } "END" } end-of-document }
444 } define-command-map
445
446 : clear-editor ( editor -- )
447     model>> clear-doc ;
448
449 : select-all ( editor -- ) doc-elt select-elt ;
450
451 : select-line ( editor -- ) one-line-elt select-elt ;
452
453 : select-word ( editor -- ) one-word-elt select-elt ;
454
455 : selected-token ( editor -- string )
456     dup gadget-selection?
457     [ dup select-word ] unless
458     gadget-selection ;
459
460 : select-previous-character ( editor -- )
461     char-elt editor-select-prev ;
462
463 : select-next-character ( editor -- )
464     char-elt editor-select-next ;
465
466 : select-previous-word ( editor -- )
467     word-elt editor-select-prev ;
468
469 : select-next-word ( editor -- )
470     word-elt editor-select-next ;
471
472 : select-start-of-line ( editor -- )
473     one-line-elt editor-select-prev ;
474
475 : select-end-of-line ( editor -- )
476     one-line-elt editor-select-next ;
477
478 : select-start-of-document ( editor -- )
479     doc-elt editor-select-prev ;
480
481 : select-end-of-document ( editor -- )
482     doc-elt editor-select-next ;
483
484 editor "selection" f {
485     { T{ button-down f { S+ } 1 } extend-selection }
486     { T{ button-up f { S+ } 1 } com-copy-selection }
487     { T{ drag { # 1 } } drag-selection }
488     { gain-focus focus-editor }
489     { lose-focus unfocus-editor }
490     { delete-action remove-selection }
491     { select-all-action select-all }
492     { T{ key-down f { C+ } "l" } select-line }
493     { T{ key-down f { S+ } "LEFT" } select-previous-character }
494     { T{ key-down f { S+ } "RIGHT" } select-next-character }
495     { T{ key-down f { S+ C+ } "LEFT" } select-previous-word }
496     { T{ key-down f { S+ C+ } "RIGHT" } select-next-word }
497     { T{ key-down f { S+ } "HOME" } select-start-of-line }
498     { T{ key-down f { S+ } "END" } select-end-of-line }
499     { T{ key-down f { S+ C+ } "HOME" } select-start-of-document }
500     { T{ key-down f { S+ C+ } "END" } select-end-of-document }
501 } define-command-map
502
503 : editor-menu ( editor -- )
504     {
505         com-undo
506         com-redo
507         ----
508         com-cut
509         com-copy
510         com-paste
511     } show-commands-menu ;
512
513 editor "misc" f {
514     ! { T{ button-down f f 2 } paste-selection }
515     { T{ button-down f f 3 } editor-menu }
516 } define-command-map
517
518 ! Multi-line editors
519 TUPLE: multiline-editor < editor ;
520
521 : <multiline-editor> ( -- editor )
522     multiline-editor new-editor ;
523
524 : previous-line ( editor -- ) line-elt editor-prev ;
525
526 : next-line ( editor -- ) line-elt editor-next ;
527
528 <PRIVATE
529
530 : page-elt ( editor -- editor element ) dup visible-lines 1 - <page-elt> ;
531
532 PRIVATE>
533
534 : previous-page ( editor -- ) page-elt editor-prev ;
535
536 : next-page ( editor -- ) page-elt editor-next ;
537
538 : select-previous-line ( editor -- ) line-elt editor-select-prev ;
539
540 : select-next-line ( editor -- ) line-elt editor-select-next ;
541
542 : select-previous-page ( editor -- ) page-elt editor-select-prev ;
543
544 : select-next-page ( editor -- ) page-elt editor-select-next ;
545
546 : insert-newline ( editor -- )
547     "\n" swap user-input* drop ;
548
549 : change-selection ( editor quot -- )
550     '[ gadget-selection @ ] [ user-input* drop ] bi ; inline
551
552 <PRIVATE
553
554 : join-lines ( string -- string' )
555     "\n" split
556     [ rest-slice [ [ blank? ] trim-head-slice ] map! drop ]
557     [ but-last-slice [ [ blank? ] trim-tail-slice ] map! drop ]
558     [ " " join ]
559     tri ;
560
561 : last-line? ( document line -- ? )
562     [ last-line# ] dip = ;
563
564 : prev-line-and-this ( document line -- start end )
565     swap [ drop 1 - 0 2array ] [ line-end ] 2bi ;
566
567 : join-with-prev ( document line -- )
568     [ prev-line-and-this ] [ drop ] 2bi
569     [ join-lines ] change-doc-range ;
570
571 : this-line-and-next ( document line -- start end )
572     swap [ drop 0 2array ] [ [ 1 + ] dip line-end ] 2bi ;
573
574 : join-with-next ( document line -- )
575     [ this-line-and-next ] [ drop ] 2bi
576     [ join-lines ] change-doc-range ;
577
578 PRIVATE>
579
580 : com-join-lines ( editor -- )
581     dup gadget-selection?
582     [ [ join-lines ] change-selection ] [
583         [ model>> ] [ editor-caret first ] bi {
584             { [ over last-line# 0 = ] [ 2drop ] }
585             { [ 2dup last-line? ] [ join-with-prev ] }
586             [ join-with-next ]
587         } cond
588     ] if ;
589
590 multiline-editor "multiline" f {
591     { T{ key-down f f "UP" } previous-line }
592     { T{ key-down f f "DOWN" } next-line }
593     { T{ key-down f { S+ } "UP" } select-previous-line }
594     { T{ key-down f { S+ } "DOWN" } select-next-line }
595     { T{ key-down f f "PAGE_UP" } previous-page }
596     { T{ key-down f f "PAGE_DOWN" } next-page }
597     { T{ key-down f { S+ } "PAGE_UP" } select-previous-page }
598     { T{ key-down f { S+ } "PAGE_DOWN" } select-next-page }
599     { T{ key-down f f "RET" } insert-newline }
600     { T{ key-down f { S+ } "RET" } insert-newline }
601     { T{ key-down f f "ENTER" } insert-newline }
602     { T{ key-down f { C+ } "j" } com-join-lines }
603 } define-command-map
604
605 TUPLE: source-editor < multiline-editor ;
606
607 : <source-editor> ( -- editor )
608     source-editor new-editor ;
609
610 ! A useful model
611 : <element-model> ( editor element -- model )
612     [ [ caret>> ] [ model>> ] bi ] dip
613     '[ _ _ elt-string ] <arrow> ;
614
615 ! Fields wrap an editor
616 TUPLE: field < border editor min-cols max-cols ;
617
618 <PRIVATE
619
620 : field-theme ( gadget -- gadget )
621     { 2 2 } >>size
622     { 1 0 } >>fill
623     field-border-color <solid> >>boundary ; inline
624
625 : <field-border> ( gadget -- border )
626     border new-border field-theme ;
627
628 PRIVATE>
629
630 : new-field ( class -- gadget )
631     [ <editor> ] dip new-border
632         dup gadget-child >>editor
633         field-theme ; inline
634
635 ! For line-gadget-width
636 M: field font>> editor>> font>> ;
637
638 M: field pref-dim*
639     [ ]
640     [ editor>> pref-dim ]
641     [ [ line-gadget-width ] [ drop second ] 2bi 2array ]
642     tri border-pref-dim ;
643
644 M: field default-text>> editor>> default-text>> ;
645
646 M: field default-text<< editor>> default-text<< ;
647
648 TUPLE: model-field < field field-model ;
649
650 : <model-field> ( model -- gadget )
651     model-field new-field
652         swap >>field-model ;
653
654 M: model-field graft*
655     [ [ field-model>> value>> ] [ editor>> ] bi set-editor-string ]
656     [ dup editor>> model>> add-connection ]
657     bi ;
658
659 M: model-field ungraft*
660     dup editor>> model>> remove-connection ;
661
662 M: model-field model-changed
663     nip [ editor>> editor-string ] [ field-model>> ] bi set-model ;
664
665 TUPLE: action-field < field quot ;
666
667 : <action-field> ( quot: ( string -- ) -- gadget )
668     action-field [ <editor> ] dip new-border
669         dup gadget-child >>editor
670         field-theme
671         swap >>quot ;
672
673 : invoke-action-field ( field -- )
674     [ editor>> editor-string ]
675     [ editor>> clear-editor ]
676     [ quot>> ]
677     tri call( string -- ) ;
678
679 action-field H{
680     { T{ key-down f f "RET" } [ invoke-action-field ] }
681 } set-gestures