better quoting
[bioacid.git] / tkminiedit.d
bloba2826372bb68d6825808bee140f6a60155898eb4
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;
17 private:
19 import arsd.color;
20 import arsd.simpledisplay;
22 import iv.cmdcon;
23 import iv.cmdcon.gl;
24 import iv.nanovega;
25 import iv.strex;
26 import iv.sdpyutil;
27 import iv.unarray;
28 import iv.utfutil;
29 import iv.vfs.io;
31 import fonts;
34 // ////////////////////////////////////////////////////////////////////////// //
35 private struct MiniEditKB { string evt; }
38 // ////////////////////////////////////////////////////////////////////////// //
39 public final class MiniEdit {
40 private import iv.utfutil : Utf8Decoder;
41 private:
42 dchar[] dtext;
43 Utf8Decoder ec; // encoder for clipboard gets
44 int curpos;
45 int lastWidth = -1;
46 bool allowMultiline = true;
48 private:
49 void setFont () nothrow @trusted @nogc {
50 fstash.size = 20;
51 fstash.fontId = "ui";
52 fstash.textAlign = NVGTextAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Top);
55 void setFont (NVGContext nvg) nothrow @trusted @nogc {
56 setFont();
57 nvg.setupCtxFrom(fstash);
60 private:
61 static struct Line {
62 uint stpos;
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;
68 string res;
69 res.reserve(text.length*4);
70 foreach (dchar dc; text) {
71 char[4] buf = void;
72 auto len = utf8Encode(buf[], dc);
73 res ~= buf[0..len];
75 return res;
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) {
85 assert(dg !is null);
87 if (dtext.length == 0) return;
89 setFont();
90 int width = lastWidth;
91 if (width < 1) width = 1; // just for fun
93 auto tbi = FONSTextBoundsIterator(fstash);
94 uint linestart = 0;
95 uint pos = 0;
97 while (pos < dtext.length) {
98 // forced newline?
99 if (isNewlineChar(dtext[pos])) {
100 dg(Line(linestart, dtext[linestart..pos], false)); // not wrapped
101 tbi.reset(fstash);
102 linestart = ++pos;
103 continue;
105 // find word end
106 uint epos = pos;
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++]);
121 // does it fit?
122 if (tbi.advance < width) {
123 pos = epos;
124 continue;
126 // if we have some words, generate line
127 if (pos > linestart) {
128 dg(Line(linestart, dtext[linestart..pos], true)); // wrapped
129 tbi.reset(fstash);
130 linestart = pos;
131 continue;
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
137 tbi.reset(fstash);
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 {
149 int x, y; // pixels
150 int line; // line number
153 CurXY calcCurPixXY (float x=0, float y=0) nothrow @trusted {
154 CurXY res;
156 setFont();
158 immutable float lineh = fstash.fontHeight;
160 bool cursorDrawn = false;
162 void drawCursorAt (float cx, float cy) {
163 if (cursorDrawn) return;
164 cursorDrawn = true;
165 res.x = cast(int)cx;
166 res.y = cast(int)cy;
167 res.line = cast(int)(cy/lineh);
170 float ex = x;
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) {
177 // after hard EOL
178 drawCursorAt(x, y);
179 return;
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)) {
185 // in line
186 immutable float cx = x+fstash.getTextBounds(0, y, line.text[0..curpos-line.stpos], null);
187 drawCursorAt(cx, y);
188 return;
191 // go to next line
192 if (x != 0) ex = x+fstash.getTextBounds(0, y, line.text, null);
193 y += lineh;
194 lastWasHardEOL = !line.wrap;
197 // this will draw cursor which is after the last line char
198 if (!cursorDrawn) {
199 if (!lastWasHardEOL) y -= lineh; else ex = x;
200 drawCursorAt(ex, y);
203 return res;
206 private:
207 void insertChar (dchar ch) nothrow {
208 assert(curpos >= 0);
209 if (!allowMultiline && ch == '\n') ch = ' ';
210 if (ch < ' ' && (ch != '\n' && ch != '\t')) return;
211 // append?
212 if (curpos >= dtext.length) {
213 dtext ~= ch;
214 curpos = cast(int)dtext.length;
215 } else {
216 // insert
217 dtext.length += 1;
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
229 private:
230 void doBackspace () nothrow {
231 if (dtext.length == 0 || curpos == 0) return;
232 --curpos;
233 foreach (immutable idx; curpos+1..dtext.length) dtext[idx-1] = dtext[idx];
234 dtext.length -= 1;
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];
241 dtext.length -= 1;
242 dtext.assumeSafeAppend;
245 void doLeftWord () nothrow {
246 if (curpos == 0) return;
247 --curpos;
248 if (dtext[curpos] <= ' ') {
249 while (curpos > 0 && dtext[curpos] <= ' ') --curpos;
250 if (dtext[curpos] > ' ') ++curpos;
251 } else {
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;
261 } else {
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();
270 } else {
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!
280 while (curpos > 0) {
281 --curpos;
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;
287 ++curpos;
288 auto npos = calcCurPixXY();
289 if (abs(cpos.x-ppos.x) < abs(cpos.x-npos.x)) --curpos;
290 return;
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) {
300 ++curpos;
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;
306 --curpos;
307 auto ppos = calcCurPixXY();
308 if (abs(cpos.x-npos.x) > abs(cpos.x-npos.x)) ++curpos;
309 return;
314 public:
315 this () nothrow {}
317 string text () const nothrow {
318 import iv.utfutil : utf8Encode;
319 string res;
320 res.reserve(dtext.length*4);
321 foreach (dchar dc; dtext) {
322 char[4] buf = void;
323 auto len = utf8Encode(buf[], dc);
324 res ~= buf[0..len];
326 return res;
329 void text (const(char)[] s) nothrow {
330 clear();
331 addText(s);
334 void addText (const(char)[] s) nothrow {
335 foreach (char ch; s) putChar(ch);
338 void clear () nothrow {
339 if (dtext.length) {
340 dtext.length = 0;
341 dtext.assumeSafeAppend;
343 curpos = 0;
344 ec.reset();
347 public:
348 @property bool multiline () const nothrow @safe @nogc { pragma(inline, true); return allowMultiline; }
349 @property void multiline (bool v) {
350 if (allowMultiline == v) return;
351 allowMultiline = v;
352 //TODO: mark dirty
355 void setWidth (int wdt) nothrow @safe @nogc {
356 if (wdt < 1) wdt = 1;
357 lastWidth = wdt;
360 // for the given width
361 int calcHeight () nothrow @trusted {
362 setFont();
364 //if (dtext.length) conwriteln("text: <", text, ">");
365 bool lastWasHardEOL = true; // at least one line should be here
366 int lineCount = 0;
367 byLine(delegate (scope Line line) {
368 //if (dtext.length) conwriteln(" line(", line.stpos, "): text: <", line.utftext, ">");
369 ++lineCount;
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;
386 // draw text
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);
391 y += lineh;
394 // draw cursor
395 nvg.beginPath();
396 nvg.strokeColor = NVGColor.yellow;
397 nvg.rect(cpos.x, cpos.y, 1, cast(int)lineh); // ensure that cursor looks a little blurry
398 nvg.stroke();
400 // reset path
401 nvg.beginPath();
404 // find and call event handler
405 final bool processKeyEvent(ME=typeof(this)) (KeyEvent event) nothrow {
406 import std.traits;
407 try {
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)) {
413 //pragma(msg, memn);
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);
429 return false;
432 bool onKey (KeyEvent event) nothrow {
433 // enter
434 if (allowMultiline && event.key == Key.Enter) {
435 if (event.pressed) insertChar('\n');
436 return true;
439 if (processKeyEvent(event)) { try { glconPostScreenRepaint(); } catch (Exception e) {} return true; }
441 return false;
444 bool onChar (dchar ch) nothrow {
445 // 127 is "delete"
446 if (/*ch == '\t' ||*/ (ch >= ' ' && ch != 127)) { insertChar(ch); return true; } // enter is processed in key handler
447 return false;
450 // keybindings
451 final public:
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(); }