1 /* coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module tkminiedit
is aliced
;
21 import arsd
.simpledisplay
;
35 // ////////////////////////////////////////////////////////////////////////// //
36 private struct MiniEditKB
{ string evt
; }
39 // ////////////////////////////////////////////////////////////////////////// //
40 public final class MiniEdit
{
41 private import iv
.utfutil
: Utf8Decoder
;
44 Utf8Decoder ec
; // encoder for clipboard gets
49 void setFont () nothrow {
52 fstash
.textAlign
= NVGTextAlign(NVGTextAlign
.H
.Left
, NVGTextAlign
.V
.Top
);
55 void setFont (NVGContext nvg
) nothrow {
57 nvg
.setupCtxFrom(fstash
);
63 const(dchar)[] text
; // never has final EOL
64 bool wrap
; // true: this line was soft-wrapped
66 string
utftext () const nothrow {
67 import iv
.utfutil
: utf8Encode
;
69 res
.reserve(text
.length
*4);
70 foreach (dchar dc
; text
) {
72 auto len
= utf8Encode(buf
[], dc
);
79 // `line` in delegate cannot be empty
80 // if `line` ends with '\n', this is hard newline, otherwise it is a wrap
81 void byLine (scope void delegate (scope Line line
) dg
) {
84 if (dtext
.length
== 0) return;
87 int width
= lastWidth
;
88 if (width
< 1) width
= 1; // just for fun
90 auto tbi
= FONSTextBoundsIterator(fstash
);
94 while (pos
< dtext
.length
) {
96 if (dtext
[pos
] == '\n') {
97 dg(Line(linestart
, dtext
[linestart
..pos
], false)); // not wrapped
104 uint lastgoodpos
= epos
; // for faster wrapping
105 while (epos
< dtext
.length
&& dtext
[epos
] != '\n' && dtext
[epos
] <= ' ') {
106 if (tbi
.advance
< width
) lastgoodpos
= epos
;
107 tbi
.put(dtext
[epos
++]);
109 while (epos
< dtext
.length
&& dtext
[epos
] > ' ') {
110 if (tbi
.advance
< width
) lastgoodpos
= epos
;
111 tbi
.put(dtext
[epos
++]);
113 // if we have spaces, they should fit too
114 while (epos
< dtext
.length
&& dtext
[epos
] != '\n' && dtext
[epos
] <= ' ') {
115 if (tbi
.advance
< width
) lastgoodpos
= epos
;
116 tbi
.put(dtext
[epos
++]);
119 if (tbi
.advance
< width
) {
123 // if we have some words, generate line
124 if (pos
> linestart
) {
125 dg(Line(linestart
, dtext
[linestart
..pos
], true)); // wrapped
130 //conwriteln(" pos=", pos, "; epos=", epos, "; adv=", tbi.advance, "; width=", width);
131 // we need at least one char, and it is guaranteed by `+1`
132 epos
= lastgoodpos
+(lastgoodpos
== linestart ?
1 : 0);
133 dg(Line(linestart
, dtext
[linestart
..epos
], true)); // wrapped
135 linestart
= (pos
= epos
);
138 // last line (if any)
139 if (pos
> linestart
) {
140 assert(pos
== dtext
.length
);
141 dg(Line(linestart
, dtext
[linestart
..pos
], true)); // wrapped
145 static struct CurXY
{
147 int line
; // line number
150 CurXY
calcCurPixXY (float x
=0, float y
=0) {
155 immutable float lineh
= fstash
.fontHeight
;
157 bool cursorDrawn
= false;
159 void drawCursorAt (float cx
, float cy
) {
160 if (cursorDrawn
) return;
164 res
.line
= cast(int)(cy
/lineh
);
169 bool lastWasHardEOL
= true;
170 byLine(delegate (scope Line line
) {
171 if (cursorDrawn
) return;
172 // check if we should draw cursor
173 if (curpos
== line
.stpos
-1) {
178 if (curpos
>= line
.stpos
) {
179 // if cursor at EOL, and line ends with space, don't draw it
180 bool isEndsWithSpace
= (line
.text
.length
&& dtext
[line
.stpos
+line
.text
.length
-1] <= ' ' && dtext
[line
.stpos
+line
.text
.length
-1] != '\n');
181 if (curpos
<= line
.stpos
+line
.text
.length
-(isEndsWithSpace ?
1 : 0)) {
183 immutable float cx
= x
+fstash
.getTextBounds(0, y
, line
.text
[0..curpos
-line
.stpos
], null);
189 if (x
!= 0) ex
= x
+fstash
.getTextBounds(0, y
, line
.text
, null);
191 lastWasHardEOL
= !line
.wrap
;
194 // this will draw cursor which is after the last line char
196 if (!lastWasHardEOL
) y
-= lineh
; else ex
= x
;
204 void insertChar (dchar ch
) nothrow {
206 if (ch
< ' ' && (ch
!= '\n' && ch
!= '\t')) return;
208 if (curpos
>= dtext
.length
) {
210 curpos
= cast(int)dtext
.length
;
214 dtext
.assumeSafeAppend
;
215 foreach (immutable idx
; curpos
..dtext
.length
-1; reverse
) dtext
[idx
+1] = dtext
[idx
];
216 dtext
[curpos
++] = ch
;
220 void putChar (char ch
) nothrow {
221 dchar dc
= ec
.decode(cast(ubyte)ch
);
222 if (dc
<= dchar.max
) insertChar(dc
); // insert if decoded
226 void doBackspace () nothrow {
227 if (dtext
.length
== 0 || curpos
== 0) return;
229 foreach (immutable idx
; curpos
+1..dtext
.length
) dtext
[idx
-1] = dtext
[idx
];
231 dtext
.assumeSafeAppend
;
234 void doDelete () nothrow {
235 if (dtext
.length
== 0 || curpos
>= dtext
.length
) return;
236 foreach (immutable idx
; curpos
+1..dtext
.length
) dtext
[idx
-1] = dtext
[idx
];
238 dtext
.assumeSafeAppend
;
241 void doLeftWord () nothrow {
242 if (curpos
== 0) return;
244 if (dtext
[curpos
] <= ' ') {
245 while (curpos
> 0 && dtext
[curpos
] <= ' ') --curpos
;
246 if (dtext
[curpos
] > ' ') ++curpos
;
248 while (curpos
> 0 && dtext
[curpos
] > ' ') --curpos
;
249 if (dtext
[curpos
] <= ' ') ++curpos
;
253 void doRightWord () nothrow {
254 if (curpos
== dtext
.length
) return;
255 if (dtext
[curpos
] <= ' ') {
256 while (curpos
< dtext
.length
&& dtext
[curpos
] <= ' ') ++curpos
;
258 while (curpos
< dtext
.length
&& dtext
[curpos
] > ' ') ++curpos
;
262 void doDeleteWord () nothrow {
263 if (curpos
== 0) return;
264 if (dtext
[curpos
-1] <= ' ') {
265 while (curpos
> 0 && dtext
[curpos
-1] <= ' ') doBackspace();
267 while (curpos
> 0 && dtext
[curpos
-1] > ' ') doBackspace();
272 if (lastWidth
< 1) return;
273 auto cpos
= calcCurPixXY();
274 if (cpos
.line
== 0) { curpos
= 0; return; } // nothing more to do
275 // as we won't have really long texts here, let's use Shlemiel's algorithm. boo!
278 auto ppos
= calcCurPixXY();
279 if (ppos
.line
!= cpos
.line
-1) continue;
280 if (ppos
.x
== cpos
.x
) return; // i found her!
281 if (ppos
.x
< cpos
.x
) {
282 import std
.math
: abs
;
284 auto npos
= calcCurPixXY();
285 if (abs(cpos
.x
-ppos
.x
) < abs(cpos
.x
-npos
.x
)) --curpos
;
292 if (lastWidth
< 1) return;
293 auto cpos
= calcCurPixXY();
294 // as we won't have really long texts here, let's use Shlemiel's algorithm. boo!
295 while (curpos
< dtext
.length
) {
297 auto npos
= calcCurPixXY();
298 if (npos
.line
!= cpos
.line
+1) continue;
299 if (npos
.x
== cpos
.x
) return; // i found her!
300 if (npos
.x
> cpos
.x
) {
301 import std
.math
: abs
;
303 auto ppos
= calcCurPixXY();
304 if (abs(cpos
.x
-npos
.x
) > abs(cpos
.x
-npos
.x
)) ++curpos
;
313 string
text () const nothrow {
314 import iv
.utfutil
: utf8Encode
;
316 res
.reserve(dtext
.length
*4);
317 foreach (dchar dc
; dtext
) {
319 auto len
= utf8Encode(buf
[], dc
);
325 void text (const(char)[] s
) nothrow {
330 void addText (const(char)[] s
) nothrow {
331 foreach (char ch
; s
) putChar(ch
);
334 void clear () nothrow {
337 dtext
.assumeSafeAppend
;
344 void setWidth (int wdt
) {
345 if (wdt
< 1) wdt
= 1;
349 // for the given width
353 //if (dtext.length) conwriteln("text: <", text, ">");
354 bool lastWasHardEOL
= true; // at least one line should be here
356 byLine(delegate (scope Line line
) {
357 //if (dtext.length) conwriteln(" line(", line.stpos, "): text: <", line.utftext, ">");
359 lastWasHardEOL
= !line
.wrap
;
360 //if (lineCount >= 4) assert(0, "oops");
362 //if (dtext.length) conwriteln("===");
364 if (lastWasHardEOL
) ++lineCount
;
366 return cast(int)(fstash
.fontHeight
*lineCount
);
369 void draw (NVGContext nvg
, float x
, float y
) {
370 if (lastWidth
< 1) return;
372 auto cpos
= calcCurPixXY(x
, y
);
373 immutable float lineh
= fstash
.fontHeight
;
376 setFont(nvg
); // this sets `fstash` too
377 nvg
.fillColor
= NVGColor
.k8orange
; // text color
378 byLine(delegate (scope Line line
) {
379 nvg
.text(x
, y
, line
.text
);
385 nvg
.strokeColor
= NVGColor
.yellow
;
386 nvg
.rect(cpos
.x
, cpos
.y
, 1, cast(int)lineh
); // ensure that cursor looks a little blurry
393 // find and call event handler
394 final bool processKeyEvent(ME
=typeof(this)) (KeyEvent event
) {
396 foreach (string memn
; __traits(allMembers
, ME
)) {
397 static if (is(typeof(&__traits(getMember
, ME
, memn
)))) {
398 import std
.meta
: AliasSeq
;
399 alias mx
= AliasSeq
!(__traits(getMember
, ME
, memn
))[0];
400 static if (isCallable
!mx
&& hasUDA
!(mx
, MiniEditKB
)) {
402 foreach (const MiniEditKB attr
; getUDAs
!(mx
, MiniEditKB
)) {
403 //pragma(msg, " ", attr.evt);
404 if (event
== attr
.evt
) { mx(); return true; }
405 if (!event
.pressed
) {
406 event
.pressed
= true;
407 if (event
== attr
.evt
) return true;
408 event
.pressed
= false;
417 bool onKey (KeyEvent event
) {
419 if (event
.key
== Key
.Enter
) {
420 if (event
.pressed
) insertChar('\n');
424 if (processKeyEvent(event
)) { glconPostScreenRepaint(); return true; }
429 bool onChar (dchar ch
) {
431 if (/*ch == '\t' ||*/ (ch
>= ' ' && ch
!= 127)) { insertChar(ch
); return true; } // enter is processed in key handler
437 @MiniEditKB("D-S-Insert") void oeFromClip () { glconCtlWindow
.getClipboardText(delegate (str) { foreach (immutable char ch
; str) putChar(ch
); }); }
438 @MiniEditKB("D-C-Insert") void oeToClip () { glconCtlWindow
.setClipboardText(text
); }
440 @MiniEditKB("D-Backspace") void oeBackspace () { doBackspace(); }
441 @MiniEditKB("D-Delete") void oeDelete () { doDelete(); }
442 @MiniEditKB("D-C-A") @MiniEditKB("D-Home") @MiniEditKB("D-Pad7") void oeGoHome () { curpos
= 0; }
443 @MiniEditKB("D-C-E") @MiniEditKB("D-End") @MiniEditKB("D-Pad1") void oeGoEnd () { curpos
= cast(int)dtext
.length
; }
444 @MiniEditKB("D-C-Backspace") @MiniEditKB("D-M-Backspace") void oeDelWord () { doDeleteWord(); }
446 @MiniEditKB("D-C-Y") void oeKillAll () { clear(); }
448 @MiniEditKB("D-Left") @MiniEditKB("D-Pad4") void oeGoLeft () { if (curpos
> 0) --curpos
; }
449 @MiniEditKB("D-Right") @MiniEditKB("D-Pad6") void oeGoRight () { if (curpos
< dtext
.length
) ++curpos
; }
451 @MiniEditKB("D-Up") @MiniEditKB("D-Pad8") void oeGoUp () { doUp(); }
452 @MiniEditKB("D-Down") @MiniEditKB("D-Pad2") void oeGoDown () { doDown(); }
454 @MiniEditKB("D-C-Left") @MiniEditKB("D-C-Pad4") void oeGoLeftWord () { doLeftWord(); }
455 @MiniEditKB("D-C-Right") @MiniEditKB("D-C-Pad6") void oeGoRightWord () { doRightWord(); }