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, version 3 of the License ONLY.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 module tkminiedit
is aliced
;
20 import arsd
.simpledisplay
;
34 // ////////////////////////////////////////////////////////////////////////// //
35 private struct MiniEditKB
{ string evt
; }
38 // ////////////////////////////////////////////////////////////////////////// //
39 public final class MiniEdit
{
40 private import iv
.utfutil
: Utf8Decoder
;
43 Utf8Decoder ec
; // encoder for clipboard gets
46 bool allowMultiline
= true;
49 void setFont () nothrow @trusted @nogc {
52 fstash
.textAlign
= NVGTextAlign(NVGTextAlign
.H
.Left
, NVGTextAlign
.V
.Top
);
55 void setFont (NVGContext nvg
) nothrow @trusted @nogc {
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 @safe {
67 import iv
.utfutil
: utf8Encode
;
69 res
.reserve(text
.length
*4);
70 foreach (dchar dc
; text
) {
72 auto len
= utf8Encode(buf
[], dc
);
79 bool isNewlineChar (char ch
) const nothrow @safe @nogc { pragma(inline
, true); return (allowMultiline
&& ch
== '\n'); }
80 bool isNewlineChar (dchar ch
) const nothrow @safe @nogc { pragma(inline
, true); return (allowMultiline
&& ch
== '\n'); }
82 // `line` in delegate cannot be empty
83 // if `line` ends with '\n', this is hard newline, otherwise it is a wrap
84 void byLine() (scope void delegate (scope Line line
) nothrow dg
) {
87 if (dtext
.length
== 0) return;
90 int width
= lastWidth
;
91 if (width
< 1) width
= 1; // just for fun
93 auto tbi
= FONSTextBoundsIterator(fstash
);
97 while (pos
< dtext
.length
) {
99 if (isNewlineChar(dtext
[pos
])) {
100 dg(Line(linestart
, dtext
[linestart
..pos
], false)); // not wrapped
107 uint lastgoodpos
= epos
; // for faster wrapping
108 while (epos
< dtext
.length
&& !isNewlineChar(dtext
[epos
]) && dtext
[epos
] <= ' ') {
109 if (tbi
.advance
< width
) lastgoodpos
= epos
;
110 tbi
.put(dtext
[epos
++]);
112 while (epos
< dtext
.length
&& dtext
[epos
] > ' ') {
113 if (tbi
.advance
< width
) lastgoodpos
= epos
;
114 tbi
.put(dtext
[epos
++]);
116 // if we have spaces, they should fit too
117 while (epos
< dtext
.length
&& !isNewlineChar(dtext
[epos
]) && dtext
[epos
] <= ' ') {
118 if (tbi
.advance
< width
) lastgoodpos
= epos
;
119 tbi
.put(dtext
[epos
++]);
122 if (tbi
.advance
< width
) {
126 // if we have some words, generate line
127 if (pos
> linestart
) {
128 dg(Line(linestart
, dtext
[linestart
..pos
], true)); // wrapped
133 //conwriteln(" pos=", pos, "; epos=", epos, "; adv=", tbi.advance, "; width=", width);
134 // we need at least one char, and it is guaranteed by `+1`
135 epos
= lastgoodpos
+(lastgoodpos
== linestart ?
1 : 0);
136 dg(Line(linestart
, dtext
[linestart
..epos
], true)); // wrapped
138 linestart
= (pos
= epos
);
141 // last line (if any)
142 if (pos
> linestart
) {
143 assert(pos
== dtext
.length
);
144 dg(Line(linestart
, dtext
[linestart
..pos
], true)); // wrapped
148 static struct CurXY
{
150 int line
; // line number
153 CurXY
calcCurPixXY (float x
=0, float y
=0) nothrow @trusted {
158 immutable float lineh
= fstash
.fontHeight
;
160 bool cursorDrawn
= false;
162 void drawCursorAt (float cx
, float cy
) {
163 if (cursorDrawn
) return;
167 res
.line
= cast(int)(cy
/lineh
);
172 bool lastWasHardEOL
= true;
173 byLine(delegate (scope Line line
) {
174 if (cursorDrawn
) return;
175 // check if we should draw cursor
176 if (curpos
== line
.stpos
-1) {
181 if (curpos
>= line
.stpos
) {
182 // if cursor at EOL, and line ends with space, don't draw it
183 bool isEndsWithSpace
= (line
.text
.length
&& dtext
[line
.stpos
+line
.text
.length
-1] <= ' ' && !isNewlineChar(dtext
[line
.stpos
+line
.text
.length
-1]));
184 if (curpos
<= line
.stpos
+line
.text
.length
-(isEndsWithSpace ?
1 : 0)) {
186 immutable float cx
= x
+fstash
.getTextBounds(0, y
, line
.text
[0..curpos
-line
.stpos
], null);
192 if (x
!= 0) ex
= x
+fstash
.getTextBounds(0, y
, line
.text
, null);
194 lastWasHardEOL
= !line
.wrap
;
197 // this will draw cursor which is after the last line char
199 if (!lastWasHardEOL
) y
-= lineh
; else ex
= x
;
207 void insertChar (dchar ch
) nothrow {
209 if (!allowMultiline
&& ch
== '\n') ch
= ' ';
210 if (ch
< ' ' && (ch
!= '\n' && ch
!= '\t')) return;
212 if (curpos
>= dtext
.length
) {
214 curpos
= cast(int)dtext
.length
;
218 dtext
.assumeSafeAppend
;
219 foreach (immutable idx
; curpos
..dtext
.length
-1; reverse
) dtext
[idx
+1] = dtext
[idx
];
220 dtext
[curpos
++] = ch
;
224 void putChar (char ch
) nothrow {
225 dchar dc
= ec
.decode(cast(ubyte)ch
);
226 if (dc
<= dchar.max
) insertChar(dc
); // insert if decoded
230 void doBackspace () nothrow {
231 if (dtext
.length
== 0 || curpos
== 0) return;
233 foreach (immutable idx
; curpos
+1..dtext
.length
) dtext
[idx
-1] = dtext
[idx
];
235 dtext
.assumeSafeAppend
;
238 void doDelete () nothrow {
239 if (dtext
.length
== 0 || curpos
>= dtext
.length
) return;
240 foreach (immutable idx
; curpos
+1..dtext
.length
) dtext
[idx
-1] = dtext
[idx
];
242 dtext
.assumeSafeAppend
;
245 void doLeftWord () nothrow {
246 if (curpos
== 0) return;
248 if (dtext
[curpos
] <= ' ') {
249 while (curpos
> 0 && dtext
[curpos
] <= ' ') --curpos
;
250 if (dtext
[curpos
] > ' ') ++curpos
;
252 while (curpos
> 0 && dtext
[curpos
] > ' ') --curpos
;
253 if (dtext
[curpos
] <= ' ') ++curpos
;
257 void doRightWord () nothrow {
258 if (curpos
== dtext
.length
) return;
259 if (dtext
[curpos
] <= ' ') {
260 while (curpos
< dtext
.length
&& dtext
[curpos
] <= ' ') ++curpos
;
262 while (curpos
< dtext
.length
&& dtext
[curpos
] > ' ') ++curpos
;
266 void doDeleteWord () nothrow {
267 if (curpos
== 0) return;
268 if (dtext
[curpos
-1] <= ' ') {
269 while (curpos
> 0 && dtext
[curpos
-1] <= ' ') doBackspace();
271 while (curpos
> 0 && dtext
[curpos
-1] > ' ') doBackspace();
275 void doUp () nothrow {
276 if (lastWidth
< 1) return;
277 auto cpos
= calcCurPixXY();
278 if (cpos
.line
== 0) { curpos
= 0; return; } // nothing more to do
279 // as we won't have really long texts here, let's use Shlemiel's algorithm. boo!
282 auto ppos
= calcCurPixXY();
283 if (ppos
.line
!= cpos
.line
-1) continue;
284 if (ppos
.x
== cpos
.x
) return; // i found her!
285 if (ppos
.x
< cpos
.x
) {
286 import std
.math
: abs
;
288 auto npos
= calcCurPixXY();
289 if (abs(cpos
.x
-ppos
.x
) < abs(cpos
.x
-npos
.x
)) --curpos
;
295 void doDown () nothrow {
296 if (lastWidth
< 1) return;
297 auto cpos
= calcCurPixXY();
298 // as we won't have really long texts here, let's use Shlemiel's algorithm. boo!
299 while (curpos
< dtext
.length
) {
301 auto npos
= calcCurPixXY();
302 if (npos
.line
!= cpos
.line
+1) continue;
303 if (npos
.x
== cpos
.x
) return; // i found her!
304 if (npos
.x
> cpos
.x
) {
305 import std
.math
: abs
;
307 auto ppos
= calcCurPixXY();
308 if (abs(cpos
.x
-npos
.x
) > abs(cpos
.x
-npos
.x
)) ++curpos
;
317 string
text () const nothrow {
318 import iv
.utfutil
: utf8Encode
;
320 res
.reserve(dtext
.length
*4);
321 foreach (dchar dc
; dtext
) {
323 auto len
= utf8Encode(buf
[], dc
);
329 void text (const(char)[] s
) nothrow {
334 void addText (const(char)[] s
) nothrow {
335 foreach (char ch
; s
) putChar(ch
);
338 void clear () nothrow {
341 dtext
.assumeSafeAppend
;
348 @property bool multiline () const nothrow @safe @nogc { pragma(inline
, true); return allowMultiline
; }
349 @property void multiline (bool v
) {
350 if (allowMultiline
== v
) return;
355 void setWidth (int wdt
) nothrow @safe @nogc {
356 if (wdt
< 1) wdt
= 1;
360 // for the given width
361 int calcHeight () nothrow @trusted {
364 //if (dtext.length) conwriteln("text: <", text, ">");
365 bool lastWasHardEOL
= true; // at least one line should be here
367 byLine(delegate (scope Line line
) {
368 //if (dtext.length) conwriteln(" line(", line.stpos, "): text: <", line.utftext, ">");
370 lastWasHardEOL
= !line
.wrap
;
371 //if (lineCount >= 4) assert(0, "oops");
373 //if (dtext.length) conwriteln("===");
375 if (lastWasHardEOL
) ++lineCount
;
377 return cast(int)(fstash
.fontHeight
*lineCount
);
380 void draw (NVGContext nvg
, float x
, float y
) {
381 if (lastWidth
< 1) return;
383 auto cpos
= calcCurPixXY(x
, y
);
384 immutable float lineh
= fstash
.fontHeight
;
387 setFont(nvg
); // this sets `fstash` too
388 nvg
.fillColor
= NVGColor
.k8orange
; // text color
389 byLine(delegate (scope Line line
) {
390 nvg
.text(x
, y
, line
.text
);
396 nvg
.strokeColor
= NVGColor
.yellow
;
397 nvg
.rect(cpos
.x
, cpos
.y
, 1, cast(int)lineh
); // ensure that cursor looks a little blurry
404 // find and call event handler
405 final bool processKeyEvent(ME
=typeof(this)) (KeyEvent event
) nothrow {
408 foreach (string memn
; __traits(allMembers
, ME
)) {
409 static if (is(typeof(&__traits(getMember
, ME
, memn
)))) {
410 import std
.meta
: AliasSeq
;
411 alias mx
= AliasSeq
!(__traits(getMember
, ME
, memn
))[0];
412 static if (isCallable
!mx
&& hasUDA
!(mx
, MiniEditKB
)) {
414 foreach (const MiniEditKB attr
; getUDAs
!(mx
, MiniEditKB
)) {
415 //pragma(msg, " ", attr.evt);
416 if (event
== attr
.evt
) { mx(); return true; }
417 if (!event
.pressed
) {
418 event
.pressed
= true;
419 if (event
== attr
.evt
) return true;
420 event
.pressed
= false;
426 } catch (Exception e
) {
427 conwriteln("processKeyEvent EXCEPTION: ", e
.msg
);
432 bool onKey (KeyEvent event
) nothrow {
434 if (allowMultiline
&& event
.key
== Key
.Enter
) {
435 if (event
.pressed
) insertChar('\n');
439 if (processKeyEvent(event
)) { try { glconPostScreenRepaint(); } catch (Exception e
) {} return true; }
444 bool onChar (dchar ch
) nothrow {
446 if (/*ch == '\t' ||*/ (ch
>= ' ' && ch
!= 127)) { insertChar(ch
); return true; } // enter is processed in key handler
452 @MiniEditKB("D-S-Insert") void oeFromClip () { glconCtlWindow
.getClipboardText(delegate (str) { foreach (immutable char ch
; str) putChar(ch
); }); }
453 @MiniEditKB("D-C-Insert") void oeToClip () { glconCtlWindow
.setClipboardText(text
); }
455 @MiniEditKB("D-Backspace") void oeBackspace () { doBackspace(); }
456 @MiniEditKB("D-Delete") void oeDelete () { doDelete(); }
457 @MiniEditKB("D-C-A") @MiniEditKB("D-Home") @MiniEditKB("D-Pad7") void oeGoHome () { curpos
= 0; }
458 @MiniEditKB("D-C-E") @MiniEditKB("D-End") @MiniEditKB("D-Pad1") void oeGoEnd () { curpos
= cast(int)dtext
.length
; }
459 @MiniEditKB("D-C-Backspace") @MiniEditKB("D-M-Backspace") void oeDelWord () { doDeleteWord(); }
461 @MiniEditKB("D-C-Y") void oeKillAll () { clear(); }
463 @MiniEditKB("D-Left") @MiniEditKB("D-Pad4") void oeGoLeft () { if (curpos
> 0) --curpos
; }
464 @MiniEditKB("D-Right") @MiniEditKB("D-Pad6") void oeGoRight () { if (curpos
< dtext
.length
) ++curpos
; }
466 @MiniEditKB("D-Up") @MiniEditKB("D-Pad8") void oeGoUp () { doUp(); }
467 @MiniEditKB("D-Down") @MiniEditKB("D-Pad2") void oeGoDown () { doDown(); }
469 @MiniEditKB("D-C-Left") @MiniEditKB("D-C-Pad4") void oeGoLeftWord () { doLeftWord(); }
470 @MiniEditKB("D-C-Right") @MiniEditKB("D-C-Pad6") void oeGoRightWord () { doRightWord(); }