fixed lastseen text (cosmetix)
[bioacid.git] / tkminiedit.d
blob7a9f1becd770420043f5ccdc8ad81889fc87c3a0
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;
18 private:
20 import arsd.color;
21 import arsd.simpledisplay;
23 import iv.cmdcon;
24 import iv.cmdcon.gl;
25 import iv.nanovega;
26 import iv.strex;
27 import iv.sdpyutil;
28 import iv.unarray;
29 import iv.utfutil;
30 import iv.vfs.io;
32 import fonts;
35 // ////////////////////////////////////////////////////////////////////////// //
36 private struct MiniEditKB { string evt; }
39 // ////////////////////////////////////////////////////////////////////////// //
40 public final class MiniEdit {
41 private import iv.utfutil : Utf8Decoder;
42 private:
43 dchar[] dtext;
44 Utf8Decoder ec; // encoder for clipboard gets
45 int curpos;
46 int lastWidth = -1;
48 private:
49 void setFont () nothrow {
50 fstash.size = 20;
51 fstash.fontId = "ui";
52 fstash.textAlign = NVGTextAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Top);
55 void setFont (NVGContext nvg) nothrow {
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 {
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 // `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) {
82 assert(dg !is null);
84 if (dtext.length == 0) return;
86 setFont();
87 int width = lastWidth;
88 if (width < 1) width = 1; // just for fun
90 auto tbi = FONSTextBoundsIterator(fstash);
91 uint linestart = 0;
92 uint pos = 0;
94 while (pos < dtext.length) {
95 // forced newline?
96 if (dtext[pos] == '\n') {
97 dg(Line(linestart, dtext[linestart..pos], false)); // not wrapped
98 tbi.reset(fstash);
99 linestart = ++pos;
100 continue;
102 // find word end
103 uint epos = pos;
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++]);
118 // does it fit?
119 if (tbi.advance < width) {
120 pos = epos;
121 continue;
123 // if we have some words, generate line
124 if (pos > linestart) {
125 dg(Line(linestart, dtext[linestart..pos], true)); // wrapped
126 tbi.reset(fstash);
127 linestart = pos;
128 continue;
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
134 tbi.reset(fstash);
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 {
146 int x, y; // pixels
147 int line; // line number
150 CurXY calcCurPixXY (float x=0, float y=0) {
151 CurXY res;
153 setFont();
155 immutable float lineh = fstash.fontHeight;
157 bool cursorDrawn = false;
159 void drawCursorAt (float cx, float cy) {
160 if (cursorDrawn) return;
161 cursorDrawn = true;
162 res.x = cast(int)cx;
163 res.y = cast(int)cy;
164 res.line = cast(int)(cy/lineh);
167 float ex = x;
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) {
174 // after hard EOL
175 drawCursorAt(x, y);
176 return;
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)) {
182 // in line
183 immutable float cx = x+fstash.getTextBounds(0, y, line.text[0..curpos-line.stpos], null);
184 drawCursorAt(cx, y);
185 return;
188 // go to next line
189 if (x != 0) ex = x+fstash.getTextBounds(0, y, line.text, null);
190 y += lineh;
191 lastWasHardEOL = !line.wrap;
194 // this will draw cursor which is after the last line char
195 if (!cursorDrawn) {
196 if (!lastWasHardEOL) y -= lineh; else ex = x;
197 drawCursorAt(ex, y);
200 return res;
203 private:
204 void insertChar (dchar ch) nothrow {
205 assert(curpos >= 0);
206 if (ch < ' ' && (ch != '\n' && ch != '\t')) return;
207 // append?
208 if (curpos >= dtext.length) {
209 dtext ~= ch;
210 curpos = cast(int)dtext.length;
211 } else {
212 // insert
213 dtext.length += 1;
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
225 private:
226 void doBackspace () nothrow {
227 if (dtext.length == 0 || curpos == 0) return;
228 --curpos;
229 foreach (immutable idx; curpos+1..dtext.length) dtext[idx-1] = dtext[idx];
230 dtext.length -= 1;
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];
237 dtext.length -= 1;
238 dtext.assumeSafeAppend;
241 void doLeftWord () nothrow {
242 if (curpos == 0) return;
243 --curpos;
244 if (dtext[curpos] <= ' ') {
245 while (curpos > 0 && dtext[curpos] <= ' ') --curpos;
246 if (dtext[curpos] > ' ') ++curpos;
247 } else {
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;
257 } else {
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();
266 } else {
267 while (curpos > 0 && dtext[curpos-1] > ' ') doBackspace();
271 void doUp () {
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!
276 while (curpos > 0) {
277 --curpos;
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;
283 ++curpos;
284 auto npos = calcCurPixXY();
285 if (abs(cpos.x-ppos.x) < abs(cpos.x-npos.x)) --curpos;
286 return;
291 void doDown () {
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) {
296 ++curpos;
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;
302 --curpos;
303 auto ppos = calcCurPixXY();
304 if (abs(cpos.x-npos.x) > abs(cpos.x-npos.x)) ++curpos;
305 return;
310 public:
311 this () nothrow {}
313 string text () const nothrow {
314 import iv.utfutil : utf8Encode;
315 string res;
316 res.reserve(dtext.length*4);
317 foreach (dchar dc; dtext) {
318 char[4] buf = void;
319 auto len = utf8Encode(buf[], dc);
320 res ~= buf[0..len];
322 return res;
325 void text (const(char)[] s) nothrow {
326 clear();
327 addText(s);
330 void addText (const(char)[] s) nothrow {
331 foreach (char ch; s) putChar(ch);
334 void clear () nothrow {
335 if (dtext.length) {
336 dtext.length = 0;
337 dtext.assumeSafeAppend;
339 curpos = 0;
340 ec.reset();
343 public:
344 void setWidth (int wdt) {
345 if (wdt < 1) wdt = 1;
346 lastWidth = wdt;
349 // for the given width
350 int calcHeight () {
351 setFont();
353 //if (dtext.length) conwriteln("text: <", text, ">");
354 bool lastWasHardEOL = true; // at least one line should be here
355 int lineCount = 0;
356 byLine(delegate (scope Line line) {
357 //if (dtext.length) conwriteln(" line(", line.stpos, "): text: <", line.utftext, ">");
358 ++lineCount;
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;
375 // draw text
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);
380 y += lineh;
383 // draw cursor
384 nvg.beginPath();
385 nvg.strokeColor = NVGColor.yellow;
386 nvg.rect(cpos.x, cpos.y, 1, cast(int)lineh); // ensure that cursor looks a little blurry
387 nvg.stroke();
389 // reset path
390 nvg.beginPath();
393 // find and call event handler
394 final bool processKeyEvent(ME=typeof(this)) (KeyEvent event) {
395 import std.traits;
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)) {
401 //pragma(msg, memn);
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;
414 return false;
417 bool onKey (KeyEvent event) {
418 // enter
419 if (event.key == Key.Enter) {
420 if (event.pressed) insertChar('\n');
421 return true;
424 if (processKeyEvent(event)) { glconPostScreenRepaint(); return true; }
426 return false;
429 bool onChar (dchar ch) {
430 // 127 is "delete"
431 if (/*ch == '\t' ||*/ (ch >= ' ' && ch != 127)) { insertChar(ch); return true; } // enter is processed in key handler
432 return false;
435 // keybindings
436 final public:
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(); }