egra: added slightly faster sorter to agg mini
[iv.d.git] / egtui / tui.d
blob7d843cc8599d027d7ddd18e6b08dfb10ecca0d60
1 /* Invisible Vector Library
2 * simple FlexBox-based TUI engine
4 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
5 * Understanding is not required. Only obedience.
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, version 3 of the License ONLY.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 module iv.egtui.tui /*is aliced*/;
21 import iv.alice;
22 import iv.strex;
23 import iv.rawtty;
25 import iv.egtui.editor;
26 public import iv.egtui.layout;
27 import iv.egtui.tty;
28 import iv.egtui.types;
29 import iv.egtui.utils;
30 import iv.egtui.dialogs : dialogHistory;
33 // ////////////////////////////////////////////////////////////////////////// //
34 enum FuiContinue = -666;
37 // ////////////////////////////////////////////////////////////////////////// //
38 struct Palette {
39 uint def; // default color
40 uint sel; // sel is also focus
41 uint mark; // marked text
42 uint marksel; // active marked text
43 uint gauge; // unused
44 uint input; // input field
45 uint inputmark; // input field marked text (?)
46 uint inputunchanged; // unchanged input field
47 uint reverse; // reversed text
48 uint title; // window title
49 uint disabled; // disabled text
50 // hotkey
51 uint hot;
52 uint hotsel;
56 // ////////////////////////////////////////////////////////////////////////// //
57 enum TuiPaletteNormal = 0;
58 enum TuiPaletteError = 1;
60 __gshared Palette[2] tuiPalette; // default palette
62 shared static this () {
65 tuiPalette[TuiPaletteNormal].def = XtColorFB!(ttyRgb2Color(0xd0, 0xd0, 0xd0), ttyRgb2Color(0x4e, 0x4e, 0x4e)); // 252,239
66 // sel is also focus
67 tuiPalette[TuiPaletteNormal].sel = XtColorFB!(ttyRgb2Color(0xda, 0xda, 0xda), ttyRgb2Color(0x00, 0x5f, 0x5f)); // 253,23
68 tuiPalette[TuiPaletteNormal].mark = XtColorFB!(ttyRgb2Color(0xff, 0xff, 0x00), ttyRgb2Color(0x5f, 0x5f, 0x5f)); // 226,59
69 tuiPalette[TuiPaletteNormal].marksel = XtColorFB!(ttyRgb2Color(0xff, 0xff, 0x87), ttyRgb2Color(0x00, 0x5f, 0x87)); // 228,24
70 tuiPalette[TuiPaletteNormal].gauge = XtColorFB!(ttyRgb2Color(0xbc, 0xbc, 0xbc), ttyRgb2Color(0x5f, 0x87, 0x87)); // 250,66
71 tuiPalette[TuiPaletteNormal].input = XtColorFB!(ttyRgb2Color(0xd7, 0xd7, 0xaf), ttyRgb2Color(0x26, 0x26, 0x26)); // 187,235
72 tuiPalette[TuiPaletteNormal].inputmark = XtColorFB!(ttyRgb2Color(0xff, 0xff, 0x87), ttyRgb2Color(0x00, 0x5f, 0x5f)); // 228,23
73 //tuiPalette[TuiPaletteNormal].inputunchanged = XtColorFB!(ttyRgb2Color(0xff, 0xff, 0xff), ttyRgb2Color(0x26, 0x26, 0x26)); // 144,235
74 tuiPalette[TuiPaletteNormal].inputunchanged = XtColorFB!(ttyRgb2Color(0xff, 0xff, 0xff), ttyRgb2Color(0x00, 0x00, 0x40));
75 tuiPalette[TuiPaletteNormal].reverse = XtColorFB!(ttyRgb2Color(0xe4, 0xe4, 0xe4), ttyRgb2Color(0x5f, 0x87, 0x87)); // 254,66
76 tuiPalette[TuiPaletteNormal].title = XtColorFB!(ttyRgb2Color(0xd7, 0xaf, 0x87), ttyRgb2Color(0x4e, 0x4e, 0x4e)); // 180,239
77 tuiPalette[TuiPaletteNormal].disabled = XtColorFB!(ttyRgb2Color(0x94, 0x94, 0x94), ttyRgb2Color(0x4e, 0x4e, 0x4e)); // 246,239
78 // hotkey
79 tuiPalette[TuiPaletteNormal].hot = XtColorFB!(ttyRgb2Color(0xff, 0xaf, 0x00), ttyRgb2Color(0x4e, 0x4e, 0x4e)); // 214,239
80 tuiPalette[TuiPaletteNormal].hotsel = XtColorFB!(ttyRgb2Color(0xff, 0xaf, 0x00), ttyRgb2Color(0x00, 0x5f, 0x5f)); // 214,23
82 tuiPalette[TuiPaletteError] = tuiPalette[TuiPaletteNormal];
83 tuiPalette[TuiPaletteError].def = XtColorFB!(ttyRgb2Color(0xff, 0xff, 0xd7), ttyRgb2Color(0x5f, 0x00, 0x00)); // 230,52
84 tuiPalette[TuiPaletteError].sel = XtColorFB!(ttyRgb2Color(0xe4, 0xe4, 0xe4), ttyRgb2Color(0x00, 0x5f, 0x5f)); // 254,23
85 tuiPalette[TuiPaletteError].hot = XtColorFB!(ttyRgb2Color(0xff, 0x5f, 0x5f), ttyRgb2Color(0x5f, 0x00, 0x00)); // 203,52
86 tuiPalette[TuiPaletteError].hotsel = XtColorFB!(ttyRgb2Color(0xff, 0x5f, 0x5f), ttyRgb2Color(0x00, 0x5f, 0x5f)); // 203,23
87 tuiPalette[TuiPaletteError].title = XtColorFB!(ttyRgb2Color(0xff, 0xff, 0x5f), ttyRgb2Color(0x5f, 0x00, 0x00)); // 227,52
89 if (termType == TermType.linux) {
90 //ttyBeep();
91 tuiPalette[TuiPaletteNormal].def = XtColorFB!(ttyRgb2Color(0xd0, 0xd0, 0xd0), ttyRgb2Color(0x18, 0x18, 0xb2));
92 tuiPalette[TuiPaletteNormal].title = XtColorFB!(ttyRgb2Color(0xd7, 0xaf, 0x87), ttyRgb2Color(0x18, 0x18, 0xb2));
93 tuiPalette[TuiPaletteNormal].disabled = XtColorFB!(ttyRgb2Color(0x94, 0x94, 0x94), ttyRgb2Color(0x18, 0x18, 0xb2));
94 tuiPalette[TuiPaletteNormal].hot = XtColorFB!(ttyRgb2Color(0xff, 0xaf, 0x00), ttyRgb2Color(0x18, 0x18, 0xb2));
99 // ////////////////////////////////////////////////////////////////////////// //
100 private int visStrLen(bool dohot=true) (const(char)[] s) nothrow @trusted @nogc {
101 if (s.length > int.max) s = s[0..int.max];
102 int res = cast(int)s.length;
103 if (res && s.ptr[0] == '\x01') --res; // left
104 else if (res && s.ptr[0] == '\x02') --res; // right
105 else if (res && s.ptr[0] == '\x03') --res; // center (default)
106 static if (dohot) {
107 for (;;) {
108 auto ampos = s.indexOf('&');
109 if (ampos < 0 || s.length-ampos < 2) break;
110 if (s.ptr[ampos+1] != '&') { --res; break; }
111 s = s[ampos+2..$];
114 return res;
118 private char visHotChar (const(char)[] s) nothrow @trusted @nogc {
119 bool prevWasAmp = false;
120 foreach (char ch; s) {
121 if (ch == '&') {
122 prevWasAmp = !prevWasAmp;
123 } else if (prevWasAmp) {
124 return ch;
127 return 0;
131 // ////////////////////////////////////////////////////////////////////////// //
132 private const(char)[] getz (const(char)[] s) nothrow @trusted @nogc {
133 foreach (immutable idx, char ch; s) if (ch == 0) return s[0..idx];
134 return s;
138 private void setz (char[] dest, const(char)[] s) nothrow @trusted @nogc {
139 dest[] = 0;
140 if (s.length >= dest.length) s = s[0..dest.length-1];
141 if (s.length) dest[0..s.length] = s[];
145 // ////////////////////////////////////////////////////////////////////////// //
146 enum FuiCtlUserFlags : ushort {
147 Default = 1<<0,
151 // ////////////////////////////////////////////////////////////////////////// //
152 enum FuiCtlType : ubyte {
153 Invisible,
154 HLine,
155 Box,
156 Panel,
157 EditLine,
158 EditText,
159 TextView,
160 ListBox,
161 CustomBox,
162 //WARNING: button-likes should be last!
163 Label,
164 Button,
165 Check,
166 Radio,
169 // called when dialog is closed; -1 means "esc"; you can change `modalLastResult` to change dialog return value
170 alias TuiDialogCloseCB = void delegate (FuiContext ctx, int res);
171 // onchange callback; return FuiContinue to continue, -1 to exit via "esc", or control id to return from `modalDialog()`
172 alias TuiActionCB = int delegate (FuiContext ctx, int self);
173 // draw widget; rc is in global space
174 alias TuiDrawCB = void delegate (FuiContext ctx, int self, FuiRect rc);
175 // process event; return `true` if event was eaten
176 alias TuiEventCB = bool delegate (FuiContext ctx, int self, FuiEvent ev);
178 mixin template FuiCtlHeader() {
179 FuiCtlType type;
180 Palette pal;
181 char[64] id = 0; // widget it
182 char[128] caption = 0; // widget caption
183 align(8):
184 // onchange callback; return FuiContinue to continue, -1 to exit via "esc", or control id to return from `modalDialog()`
185 TuiActionCB actcb;
186 // draw widget; rc is in global space
187 TuiDrawCB drawcb;
188 // process event; return `true` if event was eaten
189 TuiEventCB eventcb;
193 struct FuiCtlHead {
194 mixin FuiCtlHeader;
196 static assert(FuiCtlHead.actcb.offsetof%8 == 0);
199 //TODO: make delegate this scoped?
200 bool setActionCB (FuiContext ctx, int item, TuiActionCB cb) {
201 if (auto data = ctx.itemIntr!FuiCtlHead(item)) {
202 data.actcb = cb;
203 return true;
205 return false;
209 //TODO: make delegate this scoped?
210 bool setDrawCB (FuiContext ctx, int item, TuiDrawCB cb) {
211 if (auto data = ctx.itemIntr!FuiCtlHead(item)) {
212 data.drawcb = cb;
213 return true;
215 return false;
219 //TODO: make delegate this scoped?
220 bool setEventCB (FuiContext ctx, int item, TuiEventCB cb) {
221 if (auto data = ctx.itemIntr!FuiCtlHead(item)) {
222 data.eventcb = cb;
223 return true;
225 return false;
229 bool setCaption (FuiContext ctx, int item, const(char)[] caption) {
230 if (auto data = ctx.item!FuiCtlHead(item)) {
231 data.caption.setz(caption);
232 auto cpt = data.caption.getz;
233 auto lp = ctx.layprops(item);
234 /*if (lp.minSize.w < cpt.length)*/ lp.minSize.w = cast(int)cpt.length;
235 return true;
237 return false;
241 // ////////////////////////////////////////////////////////////////////////// //
242 void postClose (FuiContext ctx, int res) {
243 if (!ctx.valid) return;
244 ctx.queueEvent(0, FuiEvent.Type.Close, res);
248 // ////////////////////////////////////////////////////////////////////////// //
249 enum FuiDialogFrameType : ubyte {
250 Normal,
251 Small,
252 //None,
255 struct FuiCtlRootPanel {
256 mixin FuiCtlHeader;
257 FuiDialogFrameType frame;
258 bool enterclose;
259 bool moving;
260 // history manager for this dialog
261 FuiHistoryManager hisman;
262 TuiDialogCloseCB closecb;
266 bool dialogCloseCB (FuiContext ctx, TuiDialogCloseCB dcb) {
267 if (auto data = ctx.itemIntr!FuiCtlRootPanel(0)) {
268 data.closecb = dcb;
269 return true;
271 return false;
274 TuiDialogCloseCB dialogCloseCB (FuiContext ctx) {
275 if (auto data = ctx.itemIntr!FuiCtlRootPanel(0)) return data.closecb;
276 return null;
280 // purely cosmetic
281 void dialogMoving (FuiContext ctx, bool v) {
282 if (!ctx.valid) return;
283 auto data = ctx.itemIntr!FuiCtlRootPanel(0);
284 data.moving = v;
287 bool dialogMoving (FuiContext ctx) {
288 if (!ctx.valid) return false;
289 auto data = ctx.itemIntr!FuiCtlRootPanel(0);
290 return data.moving;
294 void dialogCaption (FuiContext ctx, const(char)[] text) {
295 if (!ctx.valid) return;
296 auto data = ctx.itemIntr!FuiCtlRootPanel(0);
297 data.caption.setz(text);
301 FuiDialogFrameType dialogFrame (FuiContext ctx) {
302 if (!ctx.valid) return FuiDialogFrameType.Normal;
303 auto data = ctx.itemIntr!FuiCtlRootPanel(0);
304 return data.frame;
307 void dialogFrame (FuiContext ctx, FuiDialogFrameType t) {
308 if (!ctx.valid) return;
309 auto data = ctx.itemIntr!FuiCtlRootPanel(0);
310 auto lp = ctx.layprops(0);
311 final switch (t) {
312 case FuiDialogFrameType.Normal:
313 data.frame = FuiDialogFrameType.Normal;
314 with (lp.padding) {
315 left = 3;
316 right = 3;
317 top = 2;
318 bottom = 2;
320 break;
321 case FuiDialogFrameType.Small:
322 data.frame = FuiDialogFrameType.Small;
323 with (lp.padding) {
324 left = 1;
325 right = 1;
326 top = 1;
327 bottom = 1;
329 break;
334 void dialogEnterClose (FuiContext ctx, bool enterclose) {
335 if (!ctx.valid) return;
336 auto data = ctx.itemIntr!FuiCtlRootPanel(0);
337 data.enterclose = enterclose;
341 bool dialogEnterClose (FuiContext ctx) {
342 if (!ctx.valid) return false;
343 auto data = ctx.itemIntr!FuiCtlRootPanel(0);
344 return data.enterclose;
348 FuiHistoryManager dialogHistoryManager (FuiContext ctx) {
349 if (auto data = ctx.itemIntr!FuiCtlRootPanel(0)) return data.hisman;
350 return null;
354 void dialogHistoryManager (FuiContext ctx, FuiHistoryManager hisman) {
355 if (auto data = ctx.itemIntr!FuiCtlRootPanel(0)) data.hisman = hisman;
359 // ////////////////////////////////////////////////////////////////////////// //
360 struct FuiCtlCustomBox {
361 mixin FuiCtlHeader;
362 align(8) void* udata;
366 int custombox (FuiContext ctx, int parent, const(char)[] id=null) {
367 if (!ctx.valid) return -1;
368 auto item = ctx.addItem!FuiCtlSpan(parent);
369 with (ctx.layprops(item)) {
370 //flex = 1;
371 visible = true;
372 horizontal = true;
373 aligning = FuiLayoutProps.Align.Stretch;
374 minSize.w = 1;
375 minSize.h = 1;
376 //clickMask |= FuiLayoutProps.Buttons.Left;
377 //canBeFocused = true;
378 wantTab = false;
380 auto data = ctx.item!FuiCtlCustomBox(item);
381 data.type = FuiCtlType.CustomBox;
382 data.id.setz(id);
384 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
385 auto win = XtWindow(rc.x, rc.y, rc.w, rc.h);
386 win.color = ctx.palColor!"def"(item);
387 win.bg = 1;
388 win.fill(0, 0, rc.w, rc.h);
391 return item;
395 // ////////////////////////////////////////////////////////////////////////// //
396 struct FuiCtlSpan {
397 mixin FuiCtlHeader;
401 int span (FuiContext ctx, int parent, bool ahorizontal) {
402 if (!ctx.valid) return -1;
403 auto item = ctx.addItem!FuiCtlSpan(parent);
404 with (ctx.layprops(item)) {
405 flex = 1;
406 visible = false;
407 horizontal = ahorizontal;
408 aligning = (ahorizontal ? FuiLayoutProps.Align.Stretch : FuiLayoutProps.Align.Start);
410 auto data = ctx.item!FuiCtlSpan(item);
411 data.type = FuiCtlType.Invisible;
412 //data.pal = Palette.init;
413 return item;
416 int hspan (FuiContext ctx, int parent) { return ctx.span(parent, true); }
417 int vspan (FuiContext ctx, int parent) { return ctx.span(parent, false); }
420 int spacer (FuiContext ctx, int parent) {
421 if (!ctx.valid) return -1;
422 auto item = ctx.addItem!FuiCtlSpan(parent);
423 with (ctx.layprops(item)) {
424 flex = 0;
425 visible = false;
426 horizontal = true;
427 aligning = FuiLayoutProps.Align.Start;
429 auto data = ctx.item!FuiCtlSpan(item);
430 data.type = FuiCtlType.Invisible;
431 //data.pal = Palette.init;
432 return item;
436 int hline (FuiContext ctx, int parent) {
437 if (!ctx.valid) return -1;
438 auto item = ctx.addItem!FuiCtlSpan(parent);
439 with (ctx.layprops(item)) {
440 flex = 0;
441 visible = true;
442 horizontal = true;
443 lineBreak = true;
444 aligning = FuiLayoutProps.Align.Stretch;
445 minSize.h = maxSize.h = 1;
447 auto data = ctx.item!FuiCtlSpan(item);
448 data.type = FuiCtlType.HLine;
449 //data.pal = Palette.init;
450 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
451 auto lp = ctx.layprops(self);
452 auto win = XtWindow.fullscreen;
453 win.color = ctx.palColor!"def"(self);
454 if (lp.parent == 0) {
455 auto rlp = ctx.layprops(0);
456 int x0, x1;
457 final switch (ctx.dialogFrame) {
458 case FuiDialogFrameType.Normal:
459 x0 = rc.x-2;
460 x1 = x0+(rlp.position.w-2);
461 break;
462 case FuiDialogFrameType.Small:
463 x0 = rc.x-1;
464 x1 = x0+rlp.position.w;
465 break;
467 win.hline(x0, rc.y, x1-x0);
468 } else {
469 win.hline(rc.x, rc.y, rc.w);
472 return item;
475 // ////////////////////////////////////////////////////////////////////////// //
476 struct FuiCtlBox {
477 mixin FuiCtlHeader;
481 int box (FuiContext ctx, int parent, bool ahorizontal, const(char)[] id=null) {
482 if (!ctx.valid) return -1;
483 auto item = ctx.addItem!FuiCtlBox(parent);
484 with (ctx.layprops(item)) {
485 flex = (ahorizontal ? 0 : 1);
486 horizontal = ahorizontal;
487 aligning = (ahorizontal ? FuiLayoutProps.Align.Stretch : FuiLayoutProps.Align.Start);
488 visible = true;
490 auto data = ctx.item!FuiCtlBox(item);
491 data.type = FuiCtlType.Box;
492 //data.pal = Palette.init;
493 data.id.setz(id);
494 return item;
497 int hbox (FuiContext ctx, int parent, const(char)[] id=null) { return ctx.box(parent, true, id); }
498 int vbox (FuiContext ctx, int parent, const(char)[] id=null) { return ctx.box(parent, false, id); }
501 // ////////////////////////////////////////////////////////////////////////// //
502 struct FuiCtlPanel {
503 mixin FuiCtlHeader;
507 int panel (FuiContext ctx, int parent, const(char)[] caption, bool ahorizontal, const(char)[] id=null) {
508 if (!ctx.valid) return -1;
509 auto item = ctx.addItem!FuiCtlPanel(parent);
510 with (ctx.layprops(item)) {
511 flex = (ahorizontal ? 0 : 1);
512 horizontal = ahorizontal;
513 aligning = (ahorizontal ? FuiLayoutProps.Align.Stretch : FuiLayoutProps.Align.Start);
514 minSize = FuiSize(visStrLen(caption)+4, 2);
515 padding.left = 1;
516 padding.right = 1;
517 padding.top = 1;
518 padding.bottom = 1;
520 auto data = ctx.item!FuiCtlPanel(item);
521 data.type = FuiCtlType.Panel;
522 //data.pal = Palette.init;
523 data.id.setz(id);
524 data.caption.setz(caption);
525 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
526 auto data = ctx.item!FuiCtlPanel(self);
527 auto win = XtWindow.fullscreen;
528 win.color = ctx.palColor!"def"(self);
529 win.frame!true(rc.x, rc.y, rc.w, rc.h);
530 win.tuiWriteStr!("center", true)(rc.x+1, rc.y, rc.w-2, data.caption.getz, ctx.palColor!"title"(self), ctx.palColor!"hot"(self));
532 return item;
535 int hpanel (FuiContext ctx, int parent, const(char)[] caption, const(char)[] id=null) { return ctx.panel(parent, caption, true, id); }
536 int vpanel (FuiContext ctx, int parent, const(char)[] caption, const(char)[] id=null) { return ctx.panel(parent, caption, false, id); }
539 // ////////////////////////////////////////////////////////////////////////// //
540 struct FuiCtlEditLine {
541 mixin FuiCtlHeader;
542 align(8) TtyEditor ed;
544 static assert(FuiCtlEditLine.ed.offsetof%4 == 0);
547 private int editlinetext(bool text) (FuiContext ctx, int parent, const(char)[] id, const(char)[] deftext=null, bool utfuck=false) {
548 if (!ctx.valid) return -1;
549 auto item = ctx.addItem!FuiCtlEditLine(parent);
550 with (ctx.layprops(item)) {
551 flex = 0;
552 horizontal = true;
553 clickMask |= FuiLayoutProps.Buttons.Left;
554 canBeFocused = true;
555 wantTab = false;
556 //aligning = (ahorizontal ? FuiLayoutProps.Align.Stretch : FuiLayoutProps.Align.Start);
557 int mw = (deftext.length < 10 ? 10 : deftext.length > 50 ? 50 : cast(int)deftext.length);
558 minSize = FuiSize(mw, 1);
559 maxSize = FuiSize(int.max-1024, 1);
561 auto data = ctx.item!FuiCtlEditLine(item);
562 static if (text) {
563 data.type = FuiCtlType.EditText;
564 } else {
565 data.type = FuiCtlType.EditLine;
567 //data.pal = Palette.init;
568 data.id.setz(id);
569 data.ed = new TtyEditor(0, 0, 10, 1, !text); // size will be fixed later
570 data.ed.utfuck = utfuck;
571 data.ed.setNewText(deftext);
572 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
573 auto data = ctx.item!FuiCtlEditLine(self);
574 auto lp = ctx.layprops(self);
575 auto ed = data.ed;
576 ed.hideStatus = true;
577 ed.moveResize(rc.x+(ed.singleline ? 0 : 1), rc.y, rc.w, rc.h);
578 ed.fullDirty;
579 ed.dontSetCursor = (self != ctx.focused);
580 if (lp.enabled) {
581 if (self == ctx.focused) {
582 ed.clrBlock = ctx.palColor!"inputmark"(self);
583 ed.clrText = ctx.palColor!"input"(self);
584 ed.clrTextUnchanged = ctx.palColor!"inputunchanged"(self);
585 } else {
586 ed.clrBlock = ctx.palColor!"inputmark"(self);
587 ed.clrText = ctx.palColor!"input"(self);
588 ed.clrTextUnchanged = ctx.palColor!"inputunchanged"(self);
590 } else {
591 ed.clrBlock = ctx.palColor!"disabled"(self);
592 ed.clrText = ctx.palColor!"disabled"(self);
593 ed.clrTextUnchanged = ctx.palColor!"disabled"(self);
595 //auto oldcolor = xtGetColor();
596 //scope(exit) xtSetColor(oldcolor);
597 ed.drawPage();
599 data.eventcb = delegate (FuiContext ctx, int self, FuiEvent ev) {
600 if (ev.item != self) return false;
601 auto eld = ctx.item!FuiCtlEditLine(self);
602 final switch (ev.type) {
603 case FuiEvent.Type.None:
604 return false;
605 case FuiEvent.Type.Char: // param0: dchar; param1: mods&buttons
606 TtyEvent k;
607 k.key = TtyEvent.Key.Char;
608 k.ch = ev.ch;
609 if (eld.ed.processKey(k)) {
610 if (eld.actcb !is null) {
611 auto rr = eld.actcb(ctx, ev.item);
612 if (rr >= -1) ctx.postClose(rr);
614 return true;
616 return false;
617 case FuiEvent.Type.Key: // param0: sdpy keycode; param1: mods&buttons
618 // history
619 if (eld.ed.singleline) {
620 if (auto hisman = ctx.dialogHistoryManager) {
621 if (auto data = ctx.item!FuiCtlEditLine(self)) {
622 auto eid = data.id.getz;
623 if (eid.length && hisman.has(eid)) {
624 if (ev.key == "M-H") {
625 // history dialog
626 if (auto lp = ctx.layprops(self)) {
627 auto pt = ctx.toGlobal(self, FuiPoint(0, 0));
628 auto hidx = dialogHistory(hisman, eid, pt.x, pt.y);
629 if (hidx >= 0) {
630 auto s = hisman.item(eid, hidx);
631 eld.ed.setNewText(s, false); // don't clear on type
632 hisman.activate(eid, hidx);
633 if (eld.actcb !is null) {
634 auto rr = eld.actcb(ctx, ev.item);
635 if (rr >= -1) ctx.postClose(rr);
638 return true;
645 if (eld.ed.singleline && ev.key == "S-M-M") {
646 eld.ed.setNewText(`\s+([-+*/%^&|]|<<|>>)\s+`, false); // don't clear on type
647 return true;
649 // editline
650 if (eld.ed.processKey(ev.key)) {
651 if (eld.actcb !is null) {
652 auto rr = eld.actcb(ctx, ev.item);
653 if (rr >= -1) ctx.postClose(rr);
655 return true;
657 return false;
658 case FuiEvent.Type.Click: // mouse click; param0: buttton index; param1: mods&buttons
659 if (eld.ed.processClick(ev.bidx, ev.x, ev.y)) return true;
660 return true;
661 case FuiEvent.Type.Double: // mouse double-click; param0: buttton index; param1: mods&buttons
662 return true;
663 case FuiEvent.Type.Close: // close dialog; param0: return id
664 return false;
667 return item;
671 // `actcb` will be called *after* editor was changed (or not changed, who knows)
672 int editline (FuiContext ctx, int parent, const(char)[] id, const(char)[] deftext=null, bool utfuck=false) {
673 return editlinetext!false(ctx, parent, id, deftext, utfuck);
677 // `actcb` will be called *after* editor was changed (or not changed, who knows)
678 int edittext (FuiContext ctx, int parent, const(char)[] id, const(char)[] deftext=null, bool utfuck=false) {
679 return editlinetext!true(ctx, parent, id, deftext, utfuck);
683 TtyEditor editlineEditor (FuiContext ctx, int item) {
684 if (auto data = ctx.item!FuiCtlEditLine(item)) {
685 if (data.type == FuiCtlType.EditLine) return data.ed;
687 return null;
691 TtyEditor edittextEditor (FuiContext ctx, int item) {
692 if (auto data = ctx.item!FuiCtlEditLine(item)) {
693 if (data.type == FuiCtlType.EditText) return data.ed;
695 return null;
699 char[] editlineGetText (FuiContext ctx, int item) {
700 if (auto edl = ctx.itemAs!"editline"(item)) {
701 auto ed = edl.ed;
702 if (ed is null) return null;
703 char[] res;
704 auto rng = ed[];
705 res.reserve(rng.length);
706 foreach (char ch; rng) res ~= ch;
707 return res;
709 return null;
713 char[] edittextGetText (FuiContext ctx, int item) {
714 if (auto edl = ctx.itemAs!"edittext"(item)) {
715 auto ed = edl.ed;
716 if (ed is null) return null;
717 char[] res;
718 auto rng = ed[];
719 res.reserve(rng.length);
720 foreach (char ch; rng) res ~= ch;
721 return res;
723 return null;
727 // ////////////////////////////////////////////////////////////////////////// //
728 mixin template FuiCtlBtnLike() {
729 mixin FuiCtlHeader;
730 char hotchar = 0;
731 bool spaceClicks = true;
732 bool enterClicks = false;
733 align(8):
734 // internal function, does some action on "click"
735 // will be called before `actcb`
736 // return `false` if click should not be processed further (no `actcb` will be called)
737 bool delegate (FuiContext ctx, int self) doclickcb;
740 struct FuiCtlBtnLikeHead {
741 mixin FuiCtlBtnLike;
745 private bool btnlikeClick (FuiContext ctx, int item, int clickButton=-1) {
746 if (auto lp = ctx.layprops(item)) {
747 if (!lp.visible || lp.disabled || (clickButton >= 0 && lp.clickMask == 0)) return false;
748 auto data = ctx.item!FuiCtlBtnLikeHead(item);
749 bool clicked = false;
750 if (clickButton >= 0) {
751 foreach (ubyte shift; 0..8) {
752 if (shift == clickButton && (lp.clickMask&(1<<shift)) != 0) { clicked = true; break; }
754 } else {
755 clicked = true;
757 if (clicked) {
758 if (data.doclickcb !is null) {
759 if (!data.doclickcb(ctx, item)) return false;
761 if (data.actcb !is null) {
762 auto rr = data.actcb(ctx, item);
763 if (rr >= -1) {
764 ctx.postClose(rr);
765 return true;
768 return true;
771 return false;
775 private int buttonLike(T, FuiCtlType type) (FuiContext ctx, int parent, const(char)[] id, const(char)[] text) {
776 if (!ctx.valid) return -1;
777 auto item = ctx.addItem!T(parent);
778 auto data = ctx.item!T(item);
779 data.type = type;
780 //data.pal = Palette.init;
781 if (text.length > 255) text = text[0..255];
782 if (id.length > 255) id = id[0..255];
783 data.id.setz(id);
784 data.caption.setz(text);
785 if (text.length) {
786 data.hotchar = visHotChar(text).toupper;
787 } else {
788 data.hotchar = 0;
790 with (ctx.layprops(item)) {
791 flex = 0;
792 minSize = FuiSize(visStrLen(data.caption.getz), 1);
793 clickMask |= FuiLayoutProps.Buttons.Left;
795 data.eventcb = delegate (FuiContext ctx, int self, FuiEvent ev) {
796 final switch (ev.type) {
797 case FuiEvent.Type.None:
798 return false;
799 case FuiEvent.Type.Char: // param0: dchar; param1: mods&buttons
800 if (ev.item != self) return false;
801 auto data = ctx.item!FuiCtlBtnLikeHead(self);
802 if (!data.spaceClicks) return false;
803 if (ev.ch != ' ') return false;
804 if (auto lp = ctx.layprops(self)) {
805 if (!lp.visible || lp.disabled) return false;
806 if (lp.canBeFocused) ctx.focused = self;
807 return ctx.btnlikeClick(self);
809 return false;
810 case FuiEvent.Type.Key: // param0: sdpy keycode; param1: mods&buttons
811 auto data = ctx.item!FuiCtlBtnLikeHead(self);
812 auto lp = ctx.layprops(self);
813 if (!lp.visible || lp.disabled) return false;
814 if (data.enterClicks && ev.item == self && ev.key == "Enter") {
815 if (lp.canBeFocused) ctx.focused = self;
816 return ctx.btnlikeClick(self);
818 if (ev.key.key != TtyEvent.Key.ModChar || ev.key.ctrl || !ev.key.alt || ev.key.shift) return false;
819 if (data.hotchar != ev.key.ch) return false;
820 if (lp.canBeFocused) ctx.focused = self;
821 ctx.btnlikeClick(self);
822 return true;
823 case FuiEvent.Type.Click: // mouse click; param0: buttton index; param1: mods&buttons
824 if (ev.item == self) return ctx.btnlikeClick(self, ev.bidx);
825 return false;
826 case FuiEvent.Type.Double: // mouse double-click; param0: buttton index; param1: mods&buttons
827 return false;
828 case FuiEvent.Type.Close: // close dialog; param0: return id
829 return false;
832 return item;
836 // ////////////////////////////////////////////////////////////////////////// //
837 struct FuiCtlLabel {
838 mixin FuiCtlBtnLike;
839 ubyte destlen;
840 char[256] dest;
843 int label (FuiContext ctx, int parent, const(char)[] id, const(char)[] text, const(char)[] destid=null) {
844 auto res = ctx.buttonLike!(FuiCtlLabel, FuiCtlType.Label)(parent, id, text);
845 with (ctx.layprops(res)) {
846 clickMask = 0;
847 canBeFocused = false;
849 auto data = ctx.item!FuiCtlLabel(res);
850 data.dest.setz(destid);
851 if (destid.length == 0) {
852 data.hotchar = 0;
853 ctx.layprops(res).minSize.w = visStrLen!false(data.caption.getz);
854 ctx.layprops(res).clickMask = 0;
856 data.spaceClicks = data.enterClicks = false;
857 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
858 auto data = ctx.item!FuiCtlLabel(self);
859 auto lp = ctx.layprops(self);
860 auto win = XtWindow.fullscreen;
861 uint anorm, ahot;
862 if (lp.enabled) {
863 anorm = ctx.palColor!"def"(self);
864 ahot = ctx.palColor!"hot"(self);
865 } else {
866 anorm = ahot = ctx.palColor!"disabled"(self);
868 if (data.hotchar) {
869 win.tuiWriteStr!("right", false)(rc.x, rc.y, rc.w, data.caption.getz, anorm, ahot);
870 } else {
871 win.tuiWriteStr!("left", false, false)(rc.x, rc.y, rc.w, data.caption.getz, anorm, ahot);
874 data.doclickcb = delegate (FuiContext ctx, int self) {
875 auto data = ctx.item!FuiCtlLabel(self);
876 auto did = ctx[data.dest.getz];
877 if (did <= 0) return false;
878 if (auto lp = ctx.layprops(did)) {
879 if (lp.canBeFocused && lp.visible && lp.enabled) {
880 ctx.focused = did;
881 return true;
884 return false;
886 return res;
890 // ////////////////////////////////////////////////////////////////////////// //
891 struct FuiCtlButton {
892 mixin FuiCtlBtnLike;
895 int button (FuiContext ctx, int parent, const(char)[] id, const(char)[] text) {
896 auto item = ctx.buttonLike!(FuiCtlButton, FuiCtlType.Button)(parent, id, text);
897 if (item >= 0) {
898 with (ctx.layprops(item)) {
899 //clickMask |= FuiLayoutProps.Buttons.Left;
900 canBeFocused = true;
901 minSize.w += 4;
903 auto data = ctx.item!FuiCtlButton(item);
904 data.spaceClicks = data.enterClicks = true;
905 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
906 auto data = ctx.item!FuiCtlButton(self);
907 auto lp = ctx.layprops(self);
908 auto win = XtWindow.fullscreen;
909 uint anorm, ahot;
910 int hotx = rc.x;
911 if (lp.enabled) {
912 if (ctx.focused != self) {
913 anorm = ctx.palColor!"def"(self);
914 ahot = ctx.palColor!"hot"(self);
915 } else {
916 anorm = ctx.palColor!"sel"(self);
917 ahot = ctx.palColor!"hotsel"(self);
919 } else {
920 anorm = ahot = ctx.palColor!"disabled"(self);
922 win.color = anorm;
923 bool def = ((lp.userFlags&FuiCtlUserFlags.Default) != 0);
924 if (rc.w == 1) {
925 win.writeCharsAt!true(rc.x, rc.y, 1, '`');
926 } else if (rc.w == 2) {
927 win.writeStrAt(rc.x, rc.y, (def ? "<>" : "[]"));
928 } else if (rc.w > 2) {
929 win.writeCharsAt(rc.x, rc.y, rc.w, ' ');
930 if (def) {
931 win.writeCharsAt(rc.x+1, rc.y, 1, '<');
932 win.writeCharsAt(rc.x+rc.w-2, rc.y, 1, '>');
934 win.writeCharsAt(rc.x, rc.y, 1, '[');
935 win.writeCharsAt(rc.x+rc.w-1, rc.y, 1, ']');
936 hotx = rc.x+1;
937 win.tuiWriteStr!("center", false)(rc.x+2, rc.y, rc.w-4, data.caption.getz, anorm, ahot, &hotx);
939 if (ctx.focused == self) win.gotoXY(hotx, rc.y);
941 data.doclickcb = delegate (FuiContext ctx, int self) {
942 // send "close" to root
943 ctx.postClose(self);
944 return true;
947 return item;
951 // ////////////////////////////////////////////////////////////////////////// //
952 private void drawCheckRadio(string type) (FuiContext ctx, int self, FuiRect rc, const(char)[] text, bool marked) {
953 static assert(type == "checkbox" || type == "radio");
954 auto lp = ctx.layprops(self);
955 auto win = XtWindow.fullscreen;
956 uint anorm, ahot;
957 int hotx = rc.x;
958 if (lp.enabled) {
959 if (ctx.focused != self) {
960 anorm = ctx.palColor!"def"(self);
961 ahot = ctx.palColor!"hot"(self);
962 } else {
963 anorm = ctx.palColor!"sel"(self);
964 ahot = ctx.palColor!"hotsel"(self);
966 } else {
967 anorm = ahot = ctx.palColor!"disabled"(self);
969 win.color = anorm;
970 win.writeCharsAt(rc.x, rc.y, rc.w, ' ');
971 char markCh = ' ';
972 if (marked) {
973 static if (type == "checkbox") markCh = 'x';
974 else static if (type == "radio") markCh = '*';
975 else static assert(0, "wtf?!");
977 if (rc.w == 1) {
978 win.writeCharsAt(rc.x, rc.y, 1, markCh);
979 } else if (rc.w == 2) {
980 win.writeCharsAt(rc.x, rc.y, 1, markCh);
981 win.writeCharsAt(rc.x+1, rc.y, 1, ' ');
982 } else if (rc.w > 2) {
983 static if (type == "checkbox") win.writeStrAt(rc.x, rc.y, "[ ]");
984 else static if (type == "radio") win.writeStrAt(rc.x, rc.y, "( )");
985 else static assert(0, "wtf?!");
986 if (markCh != ' ') win.writeCharsAt(rc.x+1, rc.y, 1, markCh);
987 hotx = rc.x+1;
988 win.tuiWriteStr!("left", false)(rc.x+4, rc.y, rc.w-4, text, anorm, ahot);
990 if (ctx.focused == self) win.gotoXY(hotx, rc.y);
994 struct FuiCtlCheck {
995 mixin FuiCtlBtnLike;
996 bool* var;
999 int checkbox (FuiContext ctx, int parent, const(char)[] id, const(char)[] text, bool* var) {
1000 auto item = ctx.buttonLike!(FuiCtlCheck, FuiCtlType.Check)(parent, id, text);
1001 if (item >= 0) {
1002 auto data = ctx.item!FuiCtlCheck(item);
1003 data.spaceClicks = true;
1004 data.enterClicks = false;
1005 data.var = var;
1006 with (ctx.layprops(item)) {
1007 //clickMask |= FuiLayoutProps.Buttons.Left;
1008 canBeFocused = true;
1009 minSize.w += 4;
1011 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
1012 auto data = ctx.item!FuiCtlCheck(self);
1013 bool marked = (data.var !is null ? *data.var : false);
1014 ctx.drawCheckRadio!"checkbox"(self, rc, data.caption.getz, marked);
1016 data.doclickcb = delegate (FuiContext ctx, int self) {
1017 auto data = ctx.item!FuiCtlCheck(self);
1018 if (data.var !is null) *data.var = !*data.var;
1019 return true;
1022 return item;
1026 // ////////////////////////////////////////////////////////////////////////// //
1027 struct FuiCtlRadio {
1028 mixin FuiCtlBtnLike;
1029 int gid; // radio group index; <0: standalone
1030 int* var;
1033 private int countRadio (FuiContext ctx, int* var) {
1034 if (!ctx.valid || var is null) return -1;
1035 int res = 0;
1036 foreach (int fid; 1..ctx.length) {
1037 if (auto data = ctx.item!FuiCtlRadio(fid)) {
1038 if (data.type == FuiCtlType.Radio) {
1039 if (data.var is var) ++res;
1043 return res;
1046 int radio (FuiContext ctx, int parent, const(char)[] id, const(char)[] text, int* var) {
1047 auto item = ctx.buttonLike!(FuiCtlRadio, FuiCtlType.Radio)(parent, id, text);
1048 if (item >= 0) {
1049 auto gid = ctx.countRadio(var);
1050 auto data = ctx.item!FuiCtlRadio(item);
1051 data.spaceClicks = true;
1052 data.enterClicks = false;
1053 data.var = var;
1054 data.gid = gid;
1055 with (ctx.layprops(item)) {
1056 flex = 0;
1057 //clickMask |= FuiLayoutProps.Buttons.Left;
1058 canBeFocused = true;
1059 minSize.w += 4;
1061 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
1062 auto data = ctx.item!FuiCtlRadio(self);
1063 bool marked = (data.var ? (*data.var == data.gid) : false);
1064 ctx.drawCheckRadio!"radio"(self, rc, data.caption.getz, marked);
1066 data.doclickcb = delegate (FuiContext ctx, int self) {
1067 auto data = ctx.item!FuiCtlRadio(self);
1068 if (data.var !is null) *data.var = data.gid;
1069 return true;
1072 return item;
1076 // ////////////////////////////////////////////////////////////////////////// //
1077 struct FuiCtlTextView {
1078 mixin FuiCtlHeader;
1079 uint textlen;
1080 int topline;
1081 char[0] text; // this *will* be modified by wrapper; '\1' is "soft wrap"; '\0' is EOT
1085 // width (postition.w) must be already determined
1086 int textviewHeight (FuiContext ctx, int item) {
1087 if (auto lp = ctx.layprops(item)) {
1088 int w = lp.position.w;
1089 if (w < 1) w = lp.minSize.w;
1090 if (w < 1) w = lp.maxSize.w;
1091 if (w < 1) return 0;
1092 if (auto data = ctx.itemAs!"textview"(item)) {
1093 if (data.textlen) {
1094 int c, r;
1095 data.textlen = calcTextBoundsEx(c, r, data.text.ptr[0..data.textlen], w);
1096 return r;
1100 return 0;
1104 // `actcb` will be called *after* editor was changed (or not changed, who knows)
1105 int textview (FuiContext ctx, int parent, const(char)[] id, const(char)[] text) {
1106 if (!ctx.valid) return -1;
1107 if (text.length > 256*1024) throw new Exception("text view: text too long");
1108 auto item = ctx.addItem!FuiCtlTextView(parent, cast(int)text.length+256);
1109 with (ctx.layprops(item)) {
1110 flex = 1;
1111 aligning = FuiLayoutProps.Align.Stretch;
1112 clickMask |= FuiLayoutProps.Buttons.WheelUp|FuiLayoutProps.Buttons.WheelDown;
1113 //canBeFocused = true;
1115 auto data = ctx.item!FuiCtlTextView(item);
1116 data.type = FuiCtlType.TextView;
1117 //data.pal = Palette.init;
1118 data.id.setz(id);
1119 data.textlen = cast(uint)text.length;
1120 if (text.length > 0) {
1121 data.text.ptr[0..text.length] = text[];
1122 int c, r;
1123 data.textlen = calcTextBoundsEx(c, r, data.text.ptr[0..data.textlen], ttyw-8);
1124 //!//with (ctx.layprops(item).maxSize) { w = c; h = r; }
1125 //!//with (ctx.layprops(item).minSize) { w = 2; h = 1; }
1126 //with (ctx.layprops(item).minSize) { w = c+2; h = r; }
1127 //int minsz = r;
1128 //if (minsz > ttyh-ttyh/3) minsz = ttyh-ttyh/3;
1129 with (ctx.layprops(item).minSize) { w = c+2; h = 3; }
1130 with (ctx.layprops(item).maxSize) { w = c+2; h = r; }
1132 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
1133 if (rc.w < 1 || rc.h < 1) return;
1134 auto data = ctx.item!FuiCtlTextView(self);
1135 auto lp = ctx.layprops(self);
1136 //auto win = XtWindow.fullscreen;
1137 auto win = XtWindow(rc.x, rc.y, rc.w, rc.h);
1138 if (lp.enabled) {
1139 win.color = ctx.palColor!"def"(self);
1140 } else {
1141 win.color = ctx.palColor!"disabled"(self);
1143 //win.fill(rc.x, rc.y, rc.w, rc.h);
1144 win.fill(0, 0, rc.w, rc.h);
1145 if (data.textlen) {
1146 int c, r;
1147 data.textlen = calcTextBoundsEx(c, r, data.text.ptr[0..data.textlen], rc.w/*-lp.padding.left-lp.padding.right*/);
1148 if (data.topline < 0) data.topline = 0;
1149 if (data.topline+rc.h >= r) {
1150 data.topline = r-rc.h;
1151 if (data.topline < 0) data.topline = 0;
1153 bool wantSBar = (r > rc.h);
1154 int xofs = (wantSBar ? 2 : 1);
1155 uint tpos = 0;
1156 int ty = -data.topline;
1157 int talign = 0; // 0: left; 1: right; 2: center;
1158 if (data.textlen > 0 && data.text.ptr[0] >= 1 && data.text.ptr[0] <= 3) {
1159 talign = data.text.ptr[tpos]-1;
1160 ++tpos;
1162 while (tpos < data.textlen && ty < rc.h) {
1163 uint epos = tpos;
1164 while (epos < data.textlen && data.text.ptr[epos] != '\n' && data.text.ptr[epos] != '\6') {
1165 if (epos-tpos == rc.w) break;
1166 ++epos;
1168 final switch (talign) {
1169 case 0: // left
1170 win.writeStrAt(xofs, ty, data.text.ptr[tpos..epos]);
1171 break;
1172 case 1: // right
1173 win.writeStrAt(rc.w-xofs-(epos-tpos), ty, data.text.ptr[tpos..epos]);
1174 break;
1175 case 2: // center
1176 win.writeStrAt(xofs+(rc.w-xofs-(epos-tpos))/2, ty, data.text.ptr[tpos..epos]);
1177 break;
1179 if (epos < data.textlen && data.text.ptr[epos] <= ' ') {
1180 if (data.textlen-epos > 1 && data.text.ptr[epos] == '\n' && data.text.ptr[epos+1] >= 1 && data.text.ptr[epos+1] <= 3) {
1181 ++epos;
1182 talign = data.text.ptr[epos]-1;
1183 } else {
1184 // keep it
1185 //talign = 0;
1187 ++epos;
1189 tpos = epos;
1190 ++ty;
1192 // draw scrollbar
1193 if (wantSBar) {
1194 //win.color = atext;
1195 win.vline(1, 0, rc.h);
1196 int last = data.topline+rc.h;
1197 if (last > r) last = r;
1198 last = rc.h*last/r;
1199 foreach (int yy; 0..rc.h) win.writeCharsAt!true(0, yy, 1, (yy <= last ? 'a' : ' '));
1203 data.eventcb = delegate (FuiContext ctx, int self, FuiEvent ev) {
1204 if (ev.item != self) return false;
1205 final switch (ev.type) {
1206 case FuiEvent.Type.None:
1207 return false;
1208 case FuiEvent.Type.Char: // param0: dchar; param1: mods&buttons
1209 return false;
1210 case FuiEvent.Type.Key: // param0: sdpy keycode; param1: mods&buttons
1211 auto lp = ctx.layprops(self);
1212 if (lp !is null) {
1213 if (!lp.visible || lp.disabled) return false;
1214 } else {
1215 return false;
1217 if (auto tv = ctx.itemAs!"textview"(self)) {
1218 if (ev.key == "Up") { --tv.topline; return true; }
1219 if (ev.key == "Down") { ++tv.topline; return true; }
1220 if (ev.key == "Home") { tv.topline = 0; return true; }
1221 if (ev.key == "End") { tv.topline = int.max/2; return true; }
1222 int pgstep = lp.position.h-1;
1223 if (pgstep < 1) pgstep = 1;
1224 if (ev.key == "PageUp") { tv.topline -= pgstep; return true; }
1225 if (ev.key == "PageDown") { tv.topline += pgstep; return true; }
1227 return false;
1228 case FuiEvent.Type.Click: // mouse click; param0: buttton index; param1: mods&buttons
1229 return false;
1230 case FuiEvent.Type.Double: // mouse double-click; param0: buttton index; param1: mods&buttons
1231 return false;
1232 case FuiEvent.Type.Close: // close dialog; param0: return id
1233 return false;
1236 return item;
1240 // ////////////////////////////////////////////////////////////////////////// //
1241 struct FuiCtlListBoxItem {
1242 uint nextofs;
1243 uint length;
1244 char[0] text;
1247 struct FuiCtlListBox {
1248 mixin FuiCtlHeader;
1249 int itemCount; // total number of items
1250 int topItem;
1251 int curItem;
1252 int maxWidth;
1253 uint firstItemOffset; // in layout buffer
1254 uint lastItemOffset; // in layout buffer
1255 uint topItemOffset; // in layout buffer
1259 // return `true` if it has scrollbar
1260 bool listboxNormPage (FuiContext ctx, int item) {
1261 auto data = ctx.item!FuiCtlListBox(item);
1262 if (data is null) return false;
1263 auto lp = ctx.layprops(item);
1264 // make current item visible
1265 if (data.itemCount == 0) return false;
1266 // sanitize current item (just in case, it should be sane always)
1267 if (data.curItem < 0) data.curItem = 0;
1268 if (data.curItem >= data.itemCount) data.curItem = data.itemCount-1;
1269 int oldtop = data.topItem;
1270 if (data.topItem > data.itemCount-lp.position.h) data.topItem = data.itemCount-lp.position.h;
1271 if (data.topItem < 0) data.topItem = 0;
1272 if (data.curItem < data.topItem) {
1273 data.topItem = data.curItem;
1274 } else if (data.topItem+lp.position.h <= data.curItem) {
1275 data.topItem = data.curItem-lp.position.h+1;
1276 if (data.topItem < 0) data.topItem = 0;
1278 if (data.topItem != oldtop || data.topItemOffset == 0) {
1279 data.topItemOffset = ctx.listboxItemOffset(item, data.topItem);
1280 assert(data.topItemOffset != 0);
1282 bool wantSBar = false;
1283 // should i draw a scrollbar?
1284 if (lp.position.w > 2 && (data.topItem > 0 || data.topItem+lp.position.h < data.itemCount)) {
1285 // yes
1286 wantSBar = true;
1288 return wantSBar;
1292 // `actcb` will be called *after* editor was changed (or not changed, who knows)
1293 int listbox (FuiContext ctx, int parent, const(char)[] id) {
1294 if (!ctx.valid) return -1;
1295 auto item = ctx.addItem!FuiCtlListBox(parent);
1296 if (item == -1) return -1;
1297 with (ctx.layprops(item)) {
1298 flex = 1;
1299 aligning = FuiLayoutProps.Align.Stretch;
1300 clickMask |= FuiLayoutProps.Buttons.Left|FuiLayoutProps.Buttons.WheelUp|FuiLayoutProps.Buttons.WheelDown;
1301 canBeFocused = true;
1302 minSize = FuiSize(5, 2);
1304 auto data = ctx.item!FuiCtlListBox(item);
1305 data.type = FuiCtlType.ListBox;
1306 //data.pal = Palette.init;
1307 data.id.setz(id);
1308 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
1309 auto data = ctx.item!FuiCtlListBox(self);
1310 auto lp = ctx.layprops(self);
1311 auto win = XtWindow.fullscreen;
1312 // get colors
1313 uint atext, asel, agauge;
1314 if (lp.enabled) {
1315 atext = ctx.palColor!"def"(self);
1316 asel = ctx.palColor!"sel"(self);
1317 agauge = ctx.palColor!"gauge"(self);
1318 } else {
1319 atext = asel = agauge = ctx.palColor!"disabled"(self);
1321 win.color = atext;
1322 win.fill(rc.x, rc.y, rc.w, rc.h);
1323 // make current item visible
1324 if (data.itemCount == 0) return;
1325 // sanitize current item (just in case, it should be sane always)
1326 if (data.curItem < 0) data.curItem = 0;
1327 if (data.curItem >= data.itemCount) data.curItem = data.itemCount-1;
1328 int oldtop = data.topItem;
1329 if (data.topItem > data.itemCount-rc.h) data.topItem = data.itemCount-rc.h;
1330 if (data.topItem < 0) data.topItem = 0;
1331 if (data.curItem < data.topItem) {
1332 data.topItem = data.curItem;
1333 } else if (data.topItem+rc.h <= data.curItem) {
1334 data.topItem = data.curItem-rc.h+1;
1335 if (data.topItem < 0) data.topItem = 0;
1337 if (data.topItem != oldtop || data.topItemOffset == 0) {
1338 data.topItemOffset = ctx.listboxItemOffset(self, data.topItem);
1339 assert(data.topItemOffset != 0);
1341 bool wantSBar = false;
1342 int wdt = rc.w;
1343 int x = 0, y = 0;
1344 // should i draw a scrollbar?
1345 if (wdt > 2 && (data.topItem > 0 || data.topItem+rc.h < data.itemCount)) {
1346 // yes
1347 wantSBar = true;
1348 wdt -= 2;
1349 x += 2;
1350 } else {
1351 x += 1;
1352 wdt -= 1;
1354 x += rc.x;
1355 // draw items
1356 auto itofs = data.topItemOffset;
1357 auto curit = data.topItem;
1358 while (itofs != 0 && y < rc.h) {
1359 auto it = ctx.structAtOfs!FuiCtlListBoxItem(itofs);
1360 auto t = it.text.ptr[0..it.length];
1361 if (t.length > wdt) t = t[0..wdt];
1362 if (curit == data.curItem) {
1363 win.color = asel;
1364 // fill cursor
1365 win.writeCharsAt(x-(wantSBar ? 0 : 1), rc.y+y, wdt+(wantSBar ? 0 : 1), ' ');
1366 if (self == ctx.focused) win.gotoXY(x-(wantSBar ? 0 : 1), rc.y+y);
1367 } else {
1368 win.color = atext;
1370 win.writeStrAt(x, rc.y+y, t);
1371 itofs = it.nextofs;
1372 ++y;
1373 ++curit;
1375 // draw scrollbar
1376 if (wantSBar) {
1377 x -= 2;
1378 win.color = atext;
1379 win.vline(x+1, rc.y, rc.h);
1380 win.color = agauge; //atext;
1381 int last = data.topItem+rc.h;
1382 if (last > data.itemCount) last = data.itemCount;
1383 last = rc.h*last/data.itemCount;
1384 foreach (int yy; 0..rc.h) win.writeCharsAt!true(x, rc.y+yy, 1, (yy <= last ? 'a' : ' '));
1387 data.eventcb = delegate (FuiContext ctx, int self, FuiEvent ev) {
1388 if (ev.item != self) return false;
1389 final switch (ev.type) {
1390 case FuiEvent.Type.None:
1391 return false;
1392 case FuiEvent.Type.Char: // param0: dchar; param1: mods&buttons
1393 return false;
1394 case FuiEvent.Type.Key: // param0: sdpy keycode; param1: mods&buttons
1395 if (auto lp = ctx.layprops(self)) {
1396 if (!lp.visible || lp.disabled) return false;
1397 } else {
1398 return false;
1400 if (auto lbox = ctx.itemAs!"listbox"(self)) {
1401 bool procIt() () {
1402 auto lp = ctx.layprops(ev.item);
1403 if (ev.key == "Up") {
1404 if (--lbox.curItem < 0) lbox.curItem = 0;
1405 return true;
1407 if (ev.key == "S-Up") {
1408 if (--lbox.topItem < 0) lbox.topItem = 0;
1409 lbox.topItemOffset = 0; // invalidate
1410 return true;
1412 if (ev.key == "Down") {
1413 if (lbox.itemCount > 0) {
1414 if (++lbox.curItem >= lbox.itemCount) lbox.curItem = lbox.itemCount-1;
1416 return true;
1418 if (ev.key == "S-Down") {
1419 if (lbox.topItem+lp.position.h < lbox.itemCount) {
1420 ++lbox.topItem;
1421 lbox.topItemOffset = 0; // invalidate
1423 return true;
1425 if (ev.key == "Home") {
1426 lbox.curItem = 0;
1427 return true;
1429 if (ev.key == "End") {
1430 if (lbox.itemCount > 0) lbox.curItem = lbox.itemCount-1;
1431 return true;
1433 if (ev.key == "PageUp") {
1434 if (lbox.curItem > lbox.topItem) {
1435 lbox.curItem = lbox.topItem;
1436 } else if (lp.position.h > 1) {
1437 if ((lbox.curItem -= lp.position.h-1) < 0) lbox.curItem = 0;
1439 return true;
1441 if (ev.key == "PageDown") {
1442 if (lbox.curItem < lbox.topItem+lp.position.h-1) {
1443 lbox.curItem = lbox.topItem+lp.position.h-1;
1444 } else if (lp.position.h > 1 && lbox.itemCount > 0) {
1445 if ((lbox.curItem += lp.position.h-1) >= lbox.itemCount) lbox.curItem = lbox.itemCount-1;
1447 return true;
1449 return false;
1451 ctx.listboxNormPage(self);
1452 auto oldCI = lbox.curItem;
1453 if (procIt()) {
1454 ctx.listboxNormPage(self);
1455 if (oldCI != lbox.curItem && lbox.actcb !is null) {
1456 auto rr = lbox.actcb(ctx, self);
1457 if (rr >= -1) ctx.postClose(rr);
1459 return true;
1462 return false;
1463 case FuiEvent.Type.Click: // mouse click; param0: buttton index; param1: mods&buttons
1464 if (auto lbox = ctx.itemAs!"listbox"(self)) {
1465 ctx.listboxNormPage(self);
1466 auto oldCI = lbox.curItem;
1467 if (ev.bidx == FuiLayoutProps.Button.WheelUp) {
1468 if (--lbox.curItem < 0) lbox.curItem = 0;
1469 } else if (ev.bidx == FuiLayoutProps.Button.WheelDown) {
1470 if (lbox.itemCount > 0) {
1471 if (++lbox.curItem >= lbox.itemCount) lbox.curItem = lbox.itemCount-1;
1473 } else if (ev.x > 0) {
1474 int it = lbox.topItem+ev.y;
1475 lbox.curItem = it;
1477 ctx.listboxNormPage(self);
1478 if (oldCI != lbox.curItem && lbox.actcb !is null) {
1479 auto rr = lbox.actcb(ctx, self);
1480 if (rr >= -1) ctx.postClose(rr);
1483 return true;
1484 case FuiEvent.Type.Double: // mouse double-click; param0: buttton index; param1: mods&buttons
1485 return false;
1486 case FuiEvent.Type.Close: // close dialog; param0: return id
1487 return false;
1490 return item;
1494 void listboxItemAdd (FuiContext ctx, int item, const(char)[] text) {
1495 if (auto data = ctx.itemAs!"listbox"(item)) {
1496 int len = (text.length < 255 ? cast(int)text.length : 255);
1497 uint itofs;
1498 auto it = ctx.addStruct!FuiCtlListBoxItem(itofs, len);
1499 it.length = len;
1500 if (text.length <= 255) {
1501 it.text.ptr[0..text.length] = text[];
1502 } else {
1503 it.text.ptr[0..256] = text[0..256];
1504 it.text.ptr[253..256] = '.';
1506 if (data.maxWidth < len) data.maxWidth = len;
1507 if (data.firstItemOffset == 0) {
1508 data.firstItemOffset = itofs;
1509 data.topItemOffset = itofs;
1510 } else {
1511 auto prev = ctx.structAtOfs!FuiCtlListBoxItem(data.lastItemOffset);
1512 prev.nextofs = itofs;
1514 data.lastItemOffset = itofs;
1515 ++data.itemCount;
1516 auto lp = ctx.layprops(item);
1517 if (lp.parent >= 0) {
1518 auto pp = ctx.layprops(lp.parent);
1519 assert(pp !is null);
1520 int maxW = pp.maxSize.w-(lp.parent == 0 ? 2 : 0);
1521 if (lp.minSize.w < len+3) lp.minSize.w = len+3;
1522 if (maxW > 0 && lp.minSize.w > maxW) lp.minSize.w = maxW;
1523 int maxH = pp.maxSize.h-(lp.parent == 0 ? 2 : 0);
1524 if (lp.minSize.h < data.itemCount) lp.minSize.h = data.itemCount;
1525 if (maxH > 0 && lp.minSize.h > maxH) lp.minSize.h = maxH;
1526 if (lp.minSize.h > lp.maxSize.h) lp.minSize.h = lp.maxSize.h;
1527 } else {
1528 if (lp.minSize.w < len+3) lp.minSize.w = len+3;
1529 if (lp.minSize.w > lp.maxSize.w) lp.minSize.w = lp.maxSize.w;
1530 if (lp.minSize.h < data.itemCount) lp.minSize.h = data.itemCount;
1531 if (lp.minSize.h > lp.maxSize.h) lp.minSize.h = lp.maxSize.h;
1537 int listboxMaxItemWidth (FuiContext ctx, int item) {
1538 if (auto data = ctx.itemAs!"listbox"(item)) return data.maxWidth+2;
1539 return 0;
1543 int listboxItemCount (FuiContext ctx, int item) {
1544 if (auto data = ctx.itemAs!"listbox"(item)) return data.itemCount;
1545 return 0;
1549 int listboxItemCurrent (FuiContext ctx, int item) {
1550 if (auto data = ctx.itemAs!"listbox"(item)) return data.curItem;
1551 return 0;
1555 void listboxItemSetCurrent (FuiContext ctx, int item, int cur) {
1556 if (auto data = ctx.itemAs!"listbox"(item)) {
1557 if (data.itemCount > 0) {
1558 if (cur < 0) cur = 0;
1559 if (cur > data.itemCount) cur = data.itemCount-1;
1560 data.curItem = cur;
1566 private uint listboxItemOffset (FuiContext ctx, int item, int inum) {
1567 if (auto data = ctx.itemAs!"listbox"(item)) {
1568 if (inum > data.itemCount) inum = data.itemCount-1;
1569 if (inum < 0) inum = 0;
1570 uint itofs = data.firstItemOffset;
1571 while (inum-- > 0) {
1572 assert(itofs > 0);
1573 auto it = ctx.structAtOfs!FuiCtlListBoxItem(itofs);
1574 if (it.nextofs == 0) break;
1575 itofs = it.nextofs;
1577 return itofs;
1579 return 0;
1583 // ////////////////////////////////////////////////////////////////////////// //
1584 // `id` is element id
1585 public class FuiHistoryManager {
1586 public:
1587 this () {}
1588 abstract bool has (const(char)[] id);
1589 abstract int count (const(char)[] id);
1590 abstract const(char)[] item (const(char)[] id, int idx); // 0: oldest
1591 abstract void add (const(char)[] id, const(char)[] value); // this can shrink history; should correctly process duplicates
1592 abstract void clear (const(char)[] id);
1593 abstract void activate (const(char)[] id, int idx); // usually moves item to bottom
1597 // ////////////////////////////////////////////////////////////////////////// //
1598 // returned value valid until first layout change
1599 const(char)[] itemId (FuiContext ctx, int item) nothrow @trusted @nogc {
1600 if (auto lp = ctx.item!FuiCtlHead(item)) return lp.id.getz;
1601 return null;
1605 // ////////////////////////////////////////////////////////////////////////// //
1606 // return item id or -1
1607 int findById (FuiContext ctx, const(char)[] id) nothrow @trusted @nogc {
1608 foreach (int fid; 0..ctx.length) {
1609 if (auto data = ctx.item!FuiCtlHead(fid)) {
1610 if (data.id.getz == id) return fid;
1613 return -1;
1617 auto itemAs(string type) (FuiContext ctx, int item) nothrow @trusted @nogc {
1618 if (!ctx.valid) return null;
1619 static if (type.strEquCI("hline")) {
1620 enum ctp = FuiCtlType.HLine;
1621 alias tp = FuiCtlSpan;
1622 } else static if (type.strEquCI("box") || type.strEquCI("vbox") || type.strEquCI("hbox")) {
1623 enum ctp = FuiCtlType.Box;
1624 alias tp = FuiCtlBox;
1625 } else static if (type.strEquCI("panel") || type.strEquCI("vpanel") || type.strEquCI("hpanel")) {
1626 enum ctp = FuiCtlType.Panel;
1627 alias tp = FuiCtlPanel;
1628 } else static if (type.strEquCI("editline")) {
1629 enum ctp = FuiCtlType.EditLine;
1630 alias tp = FuiCtlEditLine;
1631 } else static if (type.strEquCI("edittext")) {
1632 enum ctp = FuiCtlType.EditText;
1633 alias tp = FuiCtlEditLine;
1634 } else static if (type.strEquCI("label")) {
1635 enum ctp = FuiCtlType.Label;
1636 alias tp = FuiCtlLabel;
1637 } else static if (type.strEquCI("button")) {
1638 enum ctp = FuiCtlType.Button;
1639 alias tp = FuiCtlButton;
1640 } else static if (type.strEquCI("check") || type.strEquCI("checkbox")) {
1641 enum ctp = FuiCtlType.Check;
1642 alias tp = FuiCtlCheck;
1643 } else static if (type.strEquCI("radio") || type.strEquCI("radio")) {
1644 enum ctp = FuiCtlType.Radio;
1645 alias tp = FuiCtlRadio;
1646 } else static if (type.strEquCI("textview")) {
1647 enum ctp = FuiCtlType.TextView;
1648 alias tp = FuiCtlTextView;
1649 } else static if (type.strEquCI("listbox")) {
1650 enum ctp = FuiCtlType.ListBox;
1651 alias tp = FuiCtlListBox;
1652 } else static if (type.strEquCI("custombox")) {
1653 enum ctp = FuiCtlType.CustomBox;
1654 alias tp = FuiCtlCustomBox;
1655 } else {
1656 static assert(0, "invalid control type: '"~type~"'");
1658 if (auto data = ctx.item!FuiCtlHead(item)) {
1659 if (data.type == ctp) return ctx.item!tp(item);
1661 return null;
1665 auto itemAs(string type) (FuiContext ctx, const(char)[] id) nothrow @trusted @nogc {
1666 if (!ctx.valid) return null;
1667 foreach (int fid; 0..ctx.length) {
1668 if (auto data = ctx.itemAs!type(fid)) {
1669 if (data.id.getz == id) return data;
1672 return null;
1676 auto firstItemOfType(string type) (FuiContext ctx) nothrow @trusted @nogc {
1677 if (!ctx.valid) return null;
1678 foreach (int fid; 0..ctx.length) {
1679 if (auto it = ctx.itemAs!type(fid)) return it;
1681 return null;
1685 FuiCtlType itemType (FuiContext ctx, int item) nothrow @trusted @nogc {
1686 if (!ctx.valid) return FuiCtlType.Invisible;
1687 if (auto data = ctx.itemIntr!FuiCtlHead(item)) return data.type;
1688 return FuiCtlType.Invisible;
1692 // ////////////////////////////////////////////////////////////////////////// //
1693 void focusFirst (FuiContext ctx) nothrow @trusted @nogc {
1694 if (!ctx.valid) return;
1695 auto fid = ctx.focused;
1696 if (fid < 1 || fid >= ctx.length) fid = 0;
1697 auto lp = ctx.layprops(fid);
1698 if (fid < 1 || fid >= ctx.length || !lp.visible || !lp.enabled || !lp.canBeFocused) {
1699 for (fid = 1; fid < ctx.length; ++fid) {
1700 lp = ctx.layprops(fid);
1701 if (lp is null) continue;
1702 if (lp.visible && lp.enabled && lp.canBeFocused) {
1703 ctx.focused = fid;
1704 return;
1711 int findDefault (FuiContext ctx) nothrow @trusted @nogc {
1712 if (!ctx.valid) return -1;
1713 foreach (int fid; 0..ctx.length) {
1714 if (auto lp = ctx.layprops(fid)) {
1715 if (lp.visible && lp.enabled && lp.canBeFocused && (lp.userFlags&FuiCtlUserFlags.Default) != 0) return fid;
1718 return -1;
1722 // ////////////////////////////////////////////////////////////////////////// //
1723 void dialogPalette (FuiContext ctx, int palidx) {
1724 if (palidx < 0 || palidx >= tuiPalette.length) palidx = 0;
1725 if (auto data = ctx.itemIntr!FuiCtlHead(0)) {
1726 data.pal = tuiPalette[palidx];
1731 void palColor(string name) (FuiContext ctx, int itemid, uint clr) {
1732 if (auto data = ctx.item!FuiCtlHead(itemid)) {
1733 mixin("data.pal."~name~" = clr;");
1738 // ////////////////////////////////////////////////////////////////////////// //
1739 uint palColor(string name) (FuiContext ctx, int itemid) {
1740 uint res;
1741 for (;;) {
1742 if (auto data = ctx.itemIntr!FuiCtlHead(itemid)) {
1743 res = mixin("data.pal."~name);
1744 if (res) break;
1745 itemid = ctx.layprops(itemid).parent;
1746 } else {
1747 res = mixin("tuiPalette[TuiPaletteNormal]."~name);
1748 break;
1751 return (res ? res : XtColorFB!(ttyRgb2Color(0xd0, 0xd0, 0xd0), ttyRgb2Color(0x4e, 0x4e, 0x4e))); // 252,239
1755 // ////////////////////////////////////////////////////////////////////////// //
1756 private void tuiWriteStr(string defcenter, bool spaces, bool dohot=true) (auto ref XtWindow win, int x, int y, int w, const(char)[] s, uint attr, uint hotattr, int* hotx=null) {
1757 static assert(defcenter == "left" || defcenter == "right" || defcenter == "center");
1758 if (hotx !is null) *hotx = x;
1759 if (w < 1) return;
1760 win.color = attr;
1761 static if (spaces) { x += 1; w -= 2; }
1762 if (w < 1 || s.length == 0) return;
1763 int sx = x, ex = x+w-1;
1764 auto vislen = visStrLen!dohot(s);
1765 if (s.ptr[0] == '\x01') {
1766 // left, nothing to do
1767 s = s[1..$];
1768 } else if (s.ptr[0] == '\x02') {
1769 // right
1770 x += (w-vislen);
1771 s = s[1..$];
1772 } else if (s.ptr[0] == '\x03') {
1773 // center
1774 auto len = visStrLen(s);
1775 x += (w-len)/2;
1776 s = s[1..$];
1777 } else {
1778 static if (defcenter == "left") {
1779 } else static if (defcenter == "right") {
1780 x += (w-vislen);
1781 } else static if (defcenter == "center") {
1782 // center
1783 x += (w-vislen)/2;
1786 bool wasHot;
1787 static if (spaces) {
1788 win.writeCharsAt(x-1, y, 1, ' ');
1789 int ee = x+vislen;
1790 if (ee <= ex+1) win.writeCharsAt(ee, y, 1, ' ');
1792 while (s.length > 0) {
1793 if (dohot && s.length > 1 && s.ptr[0] == '&' && s.ptr[1] != '&') {
1794 if (!wasHot && hotx !is null) *hotx = x;
1795 if (x >= sx && x <= ex) {
1796 if (hotattr && !wasHot) win.color = hotattr;
1797 win.writeCharsAt(x, y, 1, s.ptr[1]);
1798 win.color = attr;
1800 s = s[2..$];
1801 wasHot = true;
1802 } else if (dohot && s.length > 1 && s.ptr[0] == '&' && s.ptr[1] != '&') {
1803 if (x >= sx && x <= ex) win.writeCharsAt(x, y, 1, s.ptr[0]);
1804 s = s[2..$];
1805 } else {
1806 if (x >= sx && x <= ex) win.writeCharsAt(x, y, 1, s.ptr[0]);
1807 s = s[1..$];
1809 ++x;
1814 // ////////////////////////////////////////////////////////////////////////// //
1815 // we have to draw shadow separately, so it won't get darken each time
1816 void drawShadow (FuiContext ctx) nothrow @trusted @nogc {
1817 if (!ctx.valid) return;
1818 auto lp = ctx.layprops(0);
1819 if (lp is null) return;
1820 auto rc = lp.position;
1821 //xtShadowBox(rc.x+rc.w, rc.y+1, 2, rc.h-1);
1822 //xtHShadow(rc.x+2, rc.y+rc.h, rc.w);
1823 auto win = XtWindow(rc.x, rc.y, rc.w+2, rc.h+1);
1824 win.shadowBox(rc.w, 1, 2, rc.h-1);
1825 win.hshadow(2, rc.h, rc.w);
1829 void draw (FuiContext ctx) {
1830 if (!ctx.valid) return;
1832 void drawItem (int item, FuiPoint g) {
1833 auto lp = ctx.layprops(item);
1834 if (lp is null) return;
1835 if (!lp.visible) return;
1837 // convert local coords to global coords
1838 auto rc = lp.position;
1839 // don't shift root panes, it is already shifted
1840 if (item != 0) {
1841 rc.xp += g.x;
1842 rc.yp += g.y;
1845 void drawRootFrame (bool secondpass=false) {
1846 auto dlgdata = ctx.itemIntr!FuiCtlRootPanel(0);
1847 auto anorm = ctx.palColor!"def"(item);
1848 auto atitle = ctx.palColor!"title"(item);
1849 auto win = XtWindow.fullscreen;
1850 win.color = (!dlgdata.moving ? anorm : atitle);
1851 if (rc.w > 0 && rc.h > 0) {
1852 auto data = ctx.itemIntr!FuiCtlRootPanel(0);
1853 if (!secondpass) win.fill(rc.x, rc.y, rc.w, rc.h);
1854 final switch (data.frame) {
1855 case FuiDialogFrameType.Normal:
1856 win.frame!false(rc.x+1, rc.y+1, rc.w-2, rc.h-2);
1857 win.tuiWriteStr!("center", true)(rc.x+1, rc.y+1, rc.w-2, data.caption.getz, atitle, atitle);
1858 break;
1859 case FuiDialogFrameType.Small:
1860 win.frame!false(rc.x, rc.y, rc.w, rc.h);
1861 win.tuiWriteStr!("center", true)(rc.x+1, rc.y, rc.w-2, data.caption.getz, atitle, atitle);
1862 break;
1867 bool root = (item == 0);
1868 if (item == 0) drawRootFrame();
1870 auto head = ctx.itemIntr!FuiCtlHead(item);
1871 if (head.drawcb !is null) head.drawcb(ctx, item, rc);
1874 // draw children
1875 item = lp.firstChild;
1876 lp = ctx.layprops(item);
1877 if (lp !is null) {
1878 while (lp !is null) {
1879 drawItem(item, rc.pos);
1880 item = lp.nextSibling;
1881 lp = ctx.layprops(item);
1885 if (root) {
1886 auto dlgdata = ctx.itemIntr!FuiCtlRootPanel(0);
1887 if (dlgdata.moving) drawRootFrame(true);
1891 drawItem(0, ctx.layprops(0).position.pos);
1895 // ////////////////////////////////////////////////////////////////////////// //
1896 // returns `true` if event was consumed
1897 bool processEvent (FuiContext ctx, FuiEvent ev) {
1898 if (!ctx.valid) return false;
1900 if (auto rd = ctx.itemIntr!FuiCtlHead(0)) {
1901 if (rd.eventcb !is null) {
1902 if (rd.eventcb(ctx, ev.item, ev)) return true;
1906 if (ev.item > 0) {
1907 if (auto lp = ctx.layprops(ev.item)) {
1908 if (lp.visible && !lp.disabled) {
1909 auto data = ctx.itemIntr!FuiCtlHead(ev.item);
1910 assert(data !is null);
1911 if (data.eventcb !is null) {
1912 //{ import iv.vfs.io; VFile("z00.log", "a").writeln("ev.item=", ev.item); }
1913 if (data.eventcb(ctx, ev.item, ev)) return true;
1919 // event is not processed
1920 if (ev.type == FuiEvent.Type.Char) {
1921 // broadcast char as ModChar
1922 auto ch = ev.ch;
1923 if (ch > ' ' && ch < 256) ch = toupper(cast(char)ch);
1924 ev.type = FuiEvent.Type.Key;
1925 ev.keyp = TtyEvent.init;
1926 ev.keyp.key = TtyEvent.Key.ModChar;
1927 ev.keyp.ctrl = false;
1928 ev.keyp.alt = true;
1929 ev.keyp.shift = false;
1930 ev.keyp.ch = ch;
1931 //assert(ev.type == FuiEvent.Type.Key);
1932 } else {
1933 if (ev.type != FuiEvent.Type.Key) return false;
1935 // do navigation
1936 if (ev.key == "Enter") {
1937 // either current or default
1938 auto def = ctx.findDefault;
1939 if (def >= 0) return ctx.btnlikeClick(def);
1940 if (auto rd = ctx.itemIntr!FuiCtlRootPanel(0)) {
1941 if (rd.enterclose) {
1942 // send "close" to root with root as result
1943 ctx.postClose(0);
1944 return true;
1947 return false;
1949 if (ev.key == "Up" || ev.key == "Left") { ctx.focusPrev(); return true; }
1950 if (ev.key == "Down" || ev.key == "Right") { ctx.focusNext(); return true; }
1951 // broadcast ModChar, so widgets can process hotkeys
1952 if (ev.key.key == TtyEvent.Key.ModChar && !ev.key.ctrl && ev.key.alt && !ev.key.shift) {
1953 auto res = ctx.findNextEx(0, (int id) {
1954 if (auto lp = ctx.layprops(id)) {
1955 if (lp.visible && !lp.disabled) {
1956 auto data = ctx.item!FuiCtlHead(id);
1957 if (data.eventcb !is null) {
1958 if (data.eventcb(ctx, id, ev)) return true;
1962 return false;
1964 if (res >= 0) return true;
1966 return false;
1970 // ////////////////////////////////////////////////////////////////////////// //
1971 private __gshared bool windowMovingMouse, windowMovingKeys;
1972 private __gshared int wmX, wmY;
1973 private __gshared bool screenSaved;
1974 public __gshared int modalLastResult = -1;
1976 private __gshared FuiContext[] modalStack;
1978 @property bool modalHasOpenDialogs () nothrow @trusted @nogc { return (modalStack.length > 0); }
1981 // ////////////////////////////////////////////////////////////////////////// //
1982 void modalDialogRestoreScreen () {
1983 if (screenSaved) {
1984 screenSaved = false;
1985 xtPopArea();
1990 // this will restore old screen before saving it again
1991 void modalDialogSaveScreen () {
1992 modalDialogRestoreScreen();
1994 if (modalStack.length == 0) return;
1995 if (!ctx.valid) return false;
1996 auto lp = ctx.layprops(0);
1997 if (lp is null) return false;
1998 xtPushArea(
1999 lp.position.x, lp.position.y,
2000 lp.position.w+2, lp.position.h+1
2003 screenSaved = true;
2004 xtPushArea(0, 0, ttyw, ttyh);
2008 // redraw all dialogs
2009 void modalDialogDraw () {
2010 modalDialogSaveScreen();
2011 foreach (immutable idx, FuiContext ctx; modalStack) {
2012 if (!ctx.valid) continue;
2013 ctx.drawShadow();
2014 if (idx == 0 && (windowMovingMouse || windowMovingKeys)) ctx.dialogMoving = true;
2015 scope(exit) if (idx == 0 && (windowMovingMouse || windowMovingKeys)) ctx.dialogMoving = false;
2016 ctx.draw();
2021 // modalLastResult should be set
2022 private void closeTopDialog () {
2023 modalDialogRestoreScreen();
2024 if (modalStack.length == 0) return;
2025 auto ctx = modalStack[$-1];
2026 windowMovingMouse = false;
2027 windowMovingKeys = false;
2028 modalStack[$-1] = FuiContext.init;
2029 modalStack.length -= 1;
2030 modalStack.assumeSafeAppend;
2031 if (auto data = ctx.itemIntr!FuiCtlRootPanel(0)) {
2032 if (data.closecb !is null) data.closecb(ctx, modalLastResult);
2034 { import core.memory : GC; GC.collect; GC.minimize; }
2038 // returns `true` if dialog was closed, and then `modalLastResult` will contain result
2039 // return FuiContinue, clicked item index or -1 for esc
2040 int modalDialogProcessKey (TtyEvent key, bool* closed=null) {
2041 FuiContext ctx;
2043 if (closed !is null) *closed = false;
2045 while (modalStack.length > 0) {
2046 if (modalStack[$-1].valid) break;
2047 modalStack.length -= 1;
2048 modalStack.assumeSafeAppend;
2051 if (modalStack.length == 0) {
2052 { import core.memory : GC; GC.collect; GC.minimize; }
2053 windowMovingMouse = false;
2054 windowMovingKeys = false;
2055 modalLastResult = -1;
2056 if (closed !is null) *closed = true;
2057 return FuiContinue;
2060 ctx = modalStack[$-1];
2062 int processContextEvents () {
2063 ctx.update();
2064 while (ctx.hasEvents) {
2065 auto ev = ctx.getEvent();
2066 if (ev.type == FuiEvent.Type.Close) return ev.result;
2067 if (ctx.processEvent(ev)) continue;
2068 if (ev.type == FuiEvent.Type.Key && (ev.key == "^F3" || ev.key == "S-F3")) windowMovingKeys = true;
2070 return FuiContinue;
2073 int res = processContextEvents();
2074 if (res >= -1) {
2075 if (closed !is null) *closed = true;
2076 modalLastResult = res;
2077 closeTopDialog();
2078 return modalLastResult;
2081 //auto key = ttyReadKey(-1, TtyDefaultEscWait);
2082 if (key.key == TtyEvent.Key.Error) return FuiContinue;
2083 if (key.key == TtyEvent.Key.Unknown) return FuiContinue;
2084 if (!windowMovingKeys && key.key == TtyEvent.Key.Escape) {
2085 if (closed !is null) *closed = true;
2086 modalLastResult = -1;
2087 closeTopDialog();
2088 return modalLastResult;
2091 //if (key == "^L") { xtFullRefresh(); continue; }
2093 int dx = 0, dy = 0;
2095 // move dialog with keys
2096 if (key == "M-Left") dx = -1;
2097 if (key == "M-Right") dx = 1;
2098 if (key == "M-Up") dy = -1;
2099 if (key == "M-Down") dy = 1;
2101 if (windowMovingKeys) {
2102 if (key == "Left") dx = -1;
2103 if (key == "Right") dx = 1;
2104 if (key == "Up") dy = -1;
2105 if (key == "Down") dy = 1;
2108 // move dialog with mouse
2109 if (windowMovingMouse && key.mouse) {
2110 if (key.mrelease && key.button == TtyEvent.MButton.Left) {
2111 windowMovingMouse = false;
2112 return FuiContinue;
2114 dx = key.x-wmX;
2115 dy = key.y-wmY;
2116 wmX = key.x;
2117 wmY = key.y;
2120 // do move
2121 if (dx || dy) {
2122 ctx.layprops(0).position.x = ctx.layprops(0).position.x+dx;
2123 ctx.layprops(0).position.y = ctx.layprops(0).position.y+dy;
2124 //saveArea();
2125 //ctx.drawShadow();
2126 return FuiContinue;
2129 if (windowMovingKeys) {
2130 if (key == "Escape") windowMovingKeys = false;
2131 return FuiContinue;
2134 if (windowMovingMouse) return FuiContinue;
2136 if (key.mouse) {
2137 //TODO: check for no frame when we'll get that
2138 if (key.mpress && key.button == TtyEvent.MButton.Left && key.x >= ctx.layprops(0).position.x && key.x < ctx.layprops(0).position.x+ctx.layprops(0).position.w) {
2139 if ((key.y == ctx.layprops(0).position.y) || (ctx.dialogFrame == FuiDialogFrameType.Normal && key.y == ctx.layprops(0).position.y+1)) {
2140 windowMovingMouse = true;
2141 wmX = key.x;
2142 wmY = key.y;
2143 return FuiContinue;
2148 ctx.keyboardEvent(key);
2150 res = processContextEvents();
2151 if (res >= -1) {
2152 if (closed !is null) *closed = true;
2153 modalLastResult = res;
2154 closeTopDialog();
2155 return modalLastResult;
2158 return FuiContinue;
2162 // initialize and push dialog
2163 // return `false` if dialog was not initialized and pushed
2164 bool modalDialogInit(bool docenter=true) (FuiContext ctx) {
2165 if (!ctx.valid) return false;
2167 static if (docenter) {
2168 if (auto lp = ctx.layprops(0)) {
2169 if (lp.position.x == 0) lp.position.x = (ttyw-lp.position.w)/2;
2170 if (lp.position.y == 0) lp.position.y = (ttyh-lp.position.h)/2;
2173 ctx.focusFirst();
2174 modalStack ~= ctx;
2175 return true;
2179 // close top-level dialog
2180 // return `false` if there are no open dialogs
2181 bool modalCloseDialog () {
2182 if (modalStack.length) {
2183 modalLastResult = -1;
2184 closeTopDialog();
2185 return true;
2187 return false;
2191 // ////////////////////////////////////////////////////////////////////////// //
2192 // returns clicked item or -1 for esc
2193 int modalDialog(bool docenter=true) (FuiContext ctx) {
2194 if (!ctx.modalDialogInit!docenter()) return -1;
2195 scope(exit) modalDialogRestoreScreen();
2196 bool closed;
2197 for (;;) {
2198 modalDialogDraw();
2199 xtFlush();
2200 do {
2201 auto key = ttyReadKey(-1, TtyDefaultEscWait);
2202 if (key == "^L") { modalDialogRestoreScreen(); xtFullRefresh(); break; }
2203 if (key.key == TtyEvent.Key.Error) { modalCloseDialog(); return modalLastResult; }
2204 modalDialogProcessKey(key, &closed);
2205 if (closed) return modalLastResult;
2206 } while (ttyIsKeyHit);