changed status popup background
[bioacid.git] / tkminiedit.d
blob8cd8acc184e6d1798d84897c2930a7b73f1022a2
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;
33 // ////////////////////////////////////////////////////////////////////////// //
34 private struct MiniEditKB { string evt; }
37 // ////////////////////////////////////////////////////////////////////////// //
38 public final class MiniEdit {
39 private import iv.utfutil : Utf8Decoder;
40 private:
41 dchar[] dtext;
42 Utf8Decoder ec; // encoder for clipboard gets
43 int curpos;
45 private:
46 void insertChar (dchar ch) nothrow {
47 assert(curpos >= 0);
48 if (ch < ' ' && (ch != '\n' && ch != '\t')) return;
49 // append?
50 if (curpos >= dtext.length) {
51 dtext ~= ch;
52 curpos = cast(int)dtext.length;
53 } else {
54 // insert
55 dtext.length += 1;
56 dtext.assumeSafeAppend;
57 foreach (immutable idx; curpos..dtext.length-1; reverse) dtext[idx+1] = dtext[idx];
58 dtext[curpos++] = ch;
62 void putChar (char ch) nothrow {
63 dchar dc = ec.decode(cast(ubyte)ch);
64 if (dc <= dchar.max) insertChar(dc); // insert if decoded
67 private:
68 void doBackspace () nothrow {
69 if (dtext.length == 0 || curpos == 0) return;
70 --curpos;
71 foreach (immutable idx; curpos+1..dtext.length) dtext[idx-1] = dtext[idx];
72 dtext.length -= 1;
73 dtext.assumeSafeAppend;
76 void doDelete () nothrow {
77 if (dtext.length == 0 || curpos >= dtext.length) return;
78 foreach (immutable idx; curpos+1..dtext.length) dtext[idx-1] = dtext[idx];
79 dtext.length -= 1;
80 dtext.assumeSafeAppend;
83 void doLeftWord () nothrow {
84 if (curpos == 0) return;
85 --curpos;
86 if (dtext[curpos] <= ' ') {
87 while (curpos > 0 && dtext[curpos] <= ' ') --curpos;
88 if (dtext[curpos] > ' ') ++curpos;
89 } else {
90 while (curpos > 0 && dtext[curpos] > ' ') --curpos;
91 if (dtext[curpos] <= ' ') ++curpos;
95 void doRightWord () nothrow {
96 if (curpos == dtext.length) return;
97 if (dtext[curpos] <= ' ') {
98 while (curpos < dtext.length && dtext[curpos] <= ' ') ++curpos;
99 } else {
100 while (curpos < dtext.length && dtext[curpos] > ' ') ++curpos;
104 void doDeleteWord () nothrow {
105 if (curpos == 0) return;
106 if (dtext[curpos-1] <= ' ') {
107 while (curpos > 0 && dtext[curpos-1] <= ' ') doBackspace();
108 } else {
109 while (curpos > 0 && dtext[curpos-1] > ' ') doBackspace();
113 public:
114 this () nothrow {}
116 string text () const nothrow {
117 import iv.utfutil : utf8Encode;
118 string res;
119 res.reserve(dtext.length*4);
120 foreach (dchar dc; dtext) {
121 char[4] buf = void;
122 auto len = utf8Encode(buf[], dc);
123 res ~= buf[0..len];
125 return res;
128 void text (const(char)[] s) nothrow {
129 clear();
130 addText(s);
133 void addText (const(char)[] s) nothrow {
134 foreach (char ch; s) putChar(ch);
137 void clear () nothrow {
138 if (dtext.length) {
139 dtext.length = 0;
140 dtext.assumeSafeAppend;
142 curpos = 0;
143 ec.reset();
146 void setFont (NVGContext nvg) nothrow {
147 nvg.fontSize = 20;
148 nvg.fontFace = "ui";
149 nvg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Top);
152 public:
153 static struct HeightInfo {
154 float height = 0;
155 int lines = 0;
156 float lineh = 0;
158 void newline () nothrow @safe @nogc { ++lines; height += lineh; }
161 static struct Line {
162 uint stpos;
163 const(dchar)[] text; // never has final EOL
164 bool wrap; // true: this line was soft-wrapped
167 // `line` in delegate cannot be empty
168 // if `line` ends with '\n', this is hard newline, otherwise it is a wrap
169 void byLine (NVGContext nvg, int width, scope void delegate (scope Line line) dg) {
170 assert(nvg !is null);
171 assert(dg !is null);
173 setFont(nvg);
174 if (width < 1) width = 1; // just for fun
176 if (dtext.length == 0) return;
178 auto tbi = TextBoundsIterator(nvg);
179 uint linestart = 0;
180 uint pos = 0;
182 while (pos < dtext.length) {
183 // forced newline?
184 if (dtext[pos] == '\n') {
185 dg(Line(linestart, dtext[linestart..pos], false)); // not wrapped
186 tbi.restart();
187 linestart = ++pos;
188 continue;
190 // find word end
191 auto tbisaved = tbi;
192 uint epos = pos;
193 while (epos < dtext.length && dtext[epos] != '\n' && dtext[epos] <= ' ') tbi.put(dtext[epos++]);
194 while (epos < dtext.length && dtext[epos] > ' ') tbi.put(dtext[epos++]);
195 // if we have spaces, they should fit too
196 while (epos < dtext.length && dtext[epos] != '\n' && dtext[epos] <= ' ') tbi.put(dtext[epos++]);
197 // does it fit?
198 if (tbi.advance < width) {
199 pos = epos;
200 continue;
202 // if we have some words, generate line
203 if (pos > linestart) {
204 dg(Line(linestart, dtext[linestart..pos], true)); // wrapped
205 tbi.restart();
206 linestart = pos;
207 continue;
209 // oops, it doesn't; try it char-by-char
210 // epos will contain the char at which we should break
211 tbi = tbisaved;
212 epos = pos;
213 while (epos < dtext.length && dtext[epos] != '\n' && dtext[epos] <= ' ') {
214 tbi.put(dtext[epos]);
215 if (epos != linestart && tbi.advance >= width) break;
216 ++epos;
218 while (epos < dtext.length && dtext[epos] > ' ') {
219 tbi.put(dtext[epos]);
220 if (epos != linestart && tbi.advance >= width) break;
221 ++epos;
223 // generate line
224 dg(Line(linestart, dtext[linestart..pos], true)); // wrapped
225 tbi.restart();
226 linestart = pos;
229 // last line (if any)
230 if (pos > linestart) {
231 assert(pos == dtext.length);
232 dg(Line(linestart, dtext[linestart..pos], true)); // wrapped
236 public:
237 // for the given width
238 HeightInfo calcHeight (NVGContext nvg, int width) {
239 setFont(nvg);
241 HeightInfo res;
242 nvg.textMetrics(null, null, &res.lineh);
244 bool lastWasHardEOL = true; // at least one line should be here
245 byLine(nvg, width, delegate (scope Line line) {
246 res.newline();
247 lastWasHardEOL = !line.wrap;
250 if (lastWasHardEOL) res.newline();
252 return res;
255 void draw (NVGContext nvg, float x, float y, int width, int height) {
256 if (height < 1) return;
258 setFont(nvg);
260 float lineh;
261 nvg.textMetrics(null, null, &lineh);
263 //nvg.beginPath();
264 nvg.fillColor = NVGColor.k8orange;
265 //nvg.text(x, y, text[0..repos]);
267 bool cursorDrawn = false;
269 void drawCursorAt (float cx, float cy) {
270 if (cursorDrawn) return;
271 cursorDrawn = true;
272 nvg.beginPath();
273 nvg.strokeColor = NVGColor.yellow;
274 nvg.rect(cast(int)cx, cast(int)cy, 1, cast(int)lineh); // ensure that cursor looks a little blurry
275 nvg.stroke();
278 bool lastWasHardEOL = true;
279 float ex = x;
280 byLine(nvg, width, delegate (scope Line line) {
281 // draw text
282 ex = nvg.text(x, y, line.text);
283 // check if we should draw cursor
284 if (!cursorDrawn) {
285 if (curpos >= line.stpos && curpos <= line.stpos+line.text.length) {
286 // in line
287 float cx = x+nvg.textBounds(x, y, line.text[0..curpos-line.stpos], null);
288 drawCursorAt(cx, y);
289 } else if (curpos == line.stpos-1) {
290 // after hard EOL
291 drawCursorAt(x, y);
294 // go to next line
295 y += lineh;
296 lastWasHardEOL = !line.wrap;
299 // this will draw cursor which is after the last line char
300 if (!cursorDrawn) {
301 if (!lastWasHardEOL) y -= lineh; else ex = x;
302 drawCursorAt(ex, y);
305 // reset path
306 nvg.beginPath();
309 // find and call event handler
310 final bool processKeyEvent(ME=typeof(this)) (KeyEvent event) {
311 import std.traits;
312 foreach (string memn; __traits(allMembers, ME)) {
313 static if (is(typeof(&__traits(getMember, ME, memn)))) {
314 import std.meta : AliasSeq;
315 alias mx = AliasSeq!(__traits(getMember, ME, memn))[0];
316 static if (isCallable!mx && hasUDA!(mx, MiniEditKB)) {
317 //pragma(msg, memn);
318 foreach (const MiniEditKB attr; getUDAs!(mx, MiniEditKB)) {
319 //pragma(msg, " ", attr.evt);
320 if (event == attr.evt) { mx(); return true; }
321 if (!event.pressed) {
322 event.pressed = true;
323 if (event == attr.evt) return true;
324 event.pressed = false;
330 return false;
333 bool onKey (KeyEvent event) {
334 // enter
335 if (event.key == Key.Enter) {
336 if (event.pressed) insertChar('\n');
337 return true;
340 if (processKeyEvent(event)) { glconPostScreenRepaint(); return true; }
342 return false;
345 bool onChar (dchar ch) {
346 // 127 is "delete"
347 if (/*ch == '\t' ||*/ (ch >= ' ' && ch != 127)) { insertChar(ch); return true; } // enter is processed in key handler
348 return false;
351 // keybindings
352 final public:
353 @MiniEditKB("D-S-Insert") void oeFromClip () { glconCtlWindow.getClipboardText(delegate (str) { foreach (immutable char ch; str) putChar(ch); }); }
354 @MiniEditKB("D-C-Insert") void oeToClip () { glconCtlWindow.setClipboardText(text); }
356 @MiniEditKB("D-Backspace") void oeBackspace () { doBackspace(); }
357 @MiniEditKB("D-Delete") void oeDelete () { doDelete(); }
358 @MiniEditKB("D-C-A") @MiniEditKB("D-Home") @MiniEditKB("D-Pad7") void oeGoHome () { curpos = 0; }
359 @MiniEditKB("D-C-E") @MiniEditKB("D-End") @MiniEditKB("D-Pad1") void oeGoEnd () { curpos = cast(int)dtext.length; }
360 @MiniEditKB("D-C-Backspace") @MiniEditKB("D-M-Backspace") void oeDelWord () { doDeleteWord(); }
362 @MiniEditKB("D-C-Y") void oeKillAll () { clear(); }
364 @MiniEditKB("D-Left") @MiniEditKB("D-Pad4") void oeGoLeft () { if (curpos > 0) --curpos; }
365 @MiniEditKB("D-Right") @MiniEditKB("D-Pad6") void oeGoRight () { if (curpos < dtext.length) ++curpos; }
367 @MiniEditKB("D-C-Left") @MiniEditKB("D-C-Pad4") void oeGoLeftWord () { doLeftWord(); }
368 @MiniEditKB("D-C-Right") @MiniEditKB("D-C-Pad6") void oeGoRightWord () { doRightWord(); }