zymosis: cosmetix
[iv.d.git] / egtui / tui.d
blobe27967a67776bba079a7b78d7ebc26b285d03932
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, either version 3 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 module iv.egtui.tui /*is aliced*/;
22 import iv.alice;
23 import iv.strex;
24 import iv.rawtty;
26 import iv.egtui.editor;
27 public import iv.egtui.layout;
28 import iv.egtui.tty;
29 import iv.egtui.types;
30 import iv.egtui.utils;
31 import iv.egtui.dialogs : dialogHistory;
34 // ////////////////////////////////////////////////////////////////////////// //
35 enum FuiContinue = -666;
38 // ////////////////////////////////////////////////////////////////////////// //
39 struct Palette {
40 uint def; // default color
41 uint sel; // sel is also focus
42 uint mark; // marked text
43 uint marksel; // active marked text
44 uint gauge; // unused
45 uint input; // input field
46 uint inputmark; // input field marked text (?)
47 uint inputunchanged; // unchanged input field
48 uint reverse; // reversed text
49 uint title; // window title
50 uint disabled; // disabled text
51 // hotkey
52 uint hot;
53 uint hotsel;
57 // ////////////////////////////////////////////////////////////////////////// //
58 enum TuiPaletteNormal = 0;
59 enum TuiPaletteError = 1;
61 __gshared Palette[2] tuiPalette; // default palette
63 shared static this () {
66 tuiPalette[TuiPaletteNormal].def = XtColorFB!(ttyRgb2Color(0xd0, 0xd0, 0xd0), ttyRgb2Color(0x4e, 0x4e, 0x4e)); // 252,239
67 // sel is also focus
68 tuiPalette[TuiPaletteNormal].sel = XtColorFB!(ttyRgb2Color(0xda, 0xda, 0xda), ttyRgb2Color(0x00, 0x5f, 0x5f)); // 253,23
69 tuiPalette[TuiPaletteNormal].mark = XtColorFB!(ttyRgb2Color(0xff, 0xff, 0x00), ttyRgb2Color(0x5f, 0x5f, 0x5f)); // 226,59
70 tuiPalette[TuiPaletteNormal].marksel = XtColorFB!(ttyRgb2Color(0xff, 0xff, 0x87), ttyRgb2Color(0x00, 0x5f, 0x87)); // 228,24
71 tuiPalette[TuiPaletteNormal].gauge = XtColorFB!(ttyRgb2Color(0xbc, 0xbc, 0xbc), ttyRgb2Color(0x5f, 0x87, 0x87)); // 250,66
72 tuiPalette[TuiPaletteNormal].input = XtColorFB!(ttyRgb2Color(0xd7, 0xd7, 0xaf), ttyRgb2Color(0x26, 0x26, 0x26)); // 187,235
73 tuiPalette[TuiPaletteNormal].inputmark = XtColorFB!(ttyRgb2Color(0xff, 0xff, 0x87), ttyRgb2Color(0x00, 0x5f, 0x5f)); // 228,23
74 //tuiPalette[TuiPaletteNormal].inputunchanged = XtColorFB!(ttyRgb2Color(0xff, 0xff, 0xff), ttyRgb2Color(0x26, 0x26, 0x26)); // 144,235
75 tuiPalette[TuiPaletteNormal].inputunchanged = XtColorFB!(ttyRgb2Color(0xff, 0xff, 0xff), ttyRgb2Color(0x00, 0x00, 0x40));
76 tuiPalette[TuiPaletteNormal].reverse = XtColorFB!(ttyRgb2Color(0xe4, 0xe4, 0xe4), ttyRgb2Color(0x5f, 0x87, 0x87)); // 254,66
77 tuiPalette[TuiPaletteNormal].title = XtColorFB!(ttyRgb2Color(0xd7, 0xaf, 0x87), ttyRgb2Color(0x4e, 0x4e, 0x4e)); // 180,239
78 tuiPalette[TuiPaletteNormal].disabled = XtColorFB!(ttyRgb2Color(0x94, 0x94, 0x94), ttyRgb2Color(0x4e, 0x4e, 0x4e)); // 246,239
79 // hotkey
80 tuiPalette[TuiPaletteNormal].hot = XtColorFB!(ttyRgb2Color(0xff, 0xaf, 0x00), ttyRgb2Color(0x4e, 0x4e, 0x4e)); // 214,239
81 tuiPalette[TuiPaletteNormal].hotsel = XtColorFB!(ttyRgb2Color(0xff, 0xaf, 0x00), ttyRgb2Color(0x00, 0x5f, 0x5f)); // 214,23
83 tuiPalette[TuiPaletteError] = tuiPalette[TuiPaletteNormal];
84 tuiPalette[TuiPaletteError].def = XtColorFB!(ttyRgb2Color(0xff, 0xff, 0xd7), ttyRgb2Color(0x5f, 0x00, 0x00)); // 230,52
85 tuiPalette[TuiPaletteError].sel = XtColorFB!(ttyRgb2Color(0xe4, 0xe4, 0xe4), ttyRgb2Color(0x00, 0x5f, 0x5f)); // 254,23
86 tuiPalette[TuiPaletteError].hot = XtColorFB!(ttyRgb2Color(0xff, 0x5f, 0x5f), ttyRgb2Color(0x5f, 0x00, 0x00)); // 203,52
87 tuiPalette[TuiPaletteError].hotsel = XtColorFB!(ttyRgb2Color(0xff, 0x5f, 0x5f), ttyRgb2Color(0x00, 0x5f, 0x5f)); // 203,23
88 tuiPalette[TuiPaletteError].title = XtColorFB!(ttyRgb2Color(0xff, 0xff, 0x5f), ttyRgb2Color(0x5f, 0x00, 0x00)); // 227,52
90 if (termType == TermType.linux) {
91 //ttyBeep();
92 tuiPalette[TuiPaletteNormal].def = XtColorFB!(ttyRgb2Color(0xd0, 0xd0, 0xd0), ttyRgb2Color(0x18, 0x18, 0xb2));
93 tuiPalette[TuiPaletteNormal].title = XtColorFB!(ttyRgb2Color(0xd7, 0xaf, 0x87), ttyRgb2Color(0x18, 0x18, 0xb2));
94 tuiPalette[TuiPaletteNormal].disabled = XtColorFB!(ttyRgb2Color(0x94, 0x94, 0x94), ttyRgb2Color(0x18, 0x18, 0xb2));
95 tuiPalette[TuiPaletteNormal].hot = XtColorFB!(ttyRgb2Color(0xff, 0xaf, 0x00), ttyRgb2Color(0x18, 0x18, 0xb2));
100 // ////////////////////////////////////////////////////////////////////////// //
101 private int visStrLen(bool dohot=true) (const(char)[] s) nothrow @trusted @nogc {
102 if (s.length > int.max) s = s[0..int.max];
103 int res = cast(int)s.length;
104 if (res && s.ptr[0] == '\x01') --res; // left
105 else if (res && s.ptr[0] == '\x02') --res; // right
106 else if (res && s.ptr[0] == '\x03') --res; // center (default)
107 static if (dohot) {
108 for (;;) {
109 auto ampos = s.indexOf('&');
110 if (ampos < 0 || s.length-ampos < 2) break;
111 if (s.ptr[ampos+1] != '&') { --res; break; }
112 s = s[ampos+2..$];
115 return res;
119 private char visHotChar (const(char)[] s) nothrow @trusted @nogc {
120 bool prevWasAmp = false;
121 foreach (char ch; s) {
122 if (ch == '&') {
123 prevWasAmp = !prevWasAmp;
124 } else if (prevWasAmp) {
125 return ch;
128 return 0;
132 // ////////////////////////////////////////////////////////////////////////// //
133 private const(char)[] getz (const(char)[] s) nothrow @trusted @nogc {
134 foreach (immutable idx, char ch; s) if (ch == 0) return s[0..idx];
135 return s;
139 private void setz (char[] dest, const(char)[] s) nothrow @trusted @nogc {
140 dest[] = 0;
141 if (s.length >= dest.length) s = s[0..dest.length-1];
142 if (s.length) dest[0..s.length] = s[];
146 // ////////////////////////////////////////////////////////////////////////// //
147 enum FuiCtlUserFlags : ushort {
148 Default = 1<<0,
152 // ////////////////////////////////////////////////////////////////////////// //
153 enum FuiCtlType : ubyte {
154 Invisible,
155 HLine,
156 Box,
157 Panel,
158 EditLine,
159 EditText,
160 TextView,
161 ListBox,
162 CustomBox,
163 //WARNING: button-likes should be last!
164 Label,
165 Button,
166 Check,
167 Radio,
170 // called when dialog is closed; -1 means "esc"; you can change `modalLastResult` to change dialog return value
171 alias TuiDialogCloseCB = void delegate (FuiContext ctx, int res);
172 // onchange callback; return FuiContinue to continue, -1 to exit via "esc", or control id to return from `modalDialog()`
173 alias TuiActionCB = int delegate (FuiContext ctx, int self);
174 // draw widget; rc is in global space
175 alias TuiDrawCB = void delegate (FuiContext ctx, int self, FuiRect rc);
176 // process event; return `true` if event was eaten
177 alias TuiEventCB = bool delegate (FuiContext ctx, int self, FuiEvent ev);
179 mixin template FuiCtlHeader() {
180 FuiCtlType type;
181 Palette pal;
182 char[64] id = 0; // widget it
183 char[128] caption = 0; // widget caption
184 align(8):
185 // onchange callback; return FuiContinue to continue, -1 to exit via "esc", or control id to return from `modalDialog()`
186 TuiActionCB actcb;
187 // draw widget; rc is in global space
188 TuiDrawCB drawcb;
189 // process event; return `true` if event was eaten
190 TuiEventCB eventcb;
194 struct FuiCtlHead {
195 mixin FuiCtlHeader;
197 static assert(FuiCtlHead.actcb.offsetof%8 == 0);
200 //TODO: make delegate this scoped?
201 bool setActionCB (FuiContext ctx, int item, TuiActionCB cb) {
202 if (auto data = ctx.itemIntr!FuiCtlHead(item)) {
203 data.actcb = cb;
204 return true;
206 return false;
210 //TODO: make delegate this scoped?
211 bool setDrawCB (FuiContext ctx, int item, TuiDrawCB cb) {
212 if (auto data = ctx.itemIntr!FuiCtlHead(item)) {
213 data.drawcb = cb;
214 return true;
216 return false;
220 //TODO: make delegate this scoped?
221 bool setEventCB (FuiContext ctx, int item, TuiEventCB cb) {
222 if (auto data = ctx.itemIntr!FuiCtlHead(item)) {
223 data.eventcb = cb;
224 return true;
226 return false;
230 bool setCaption (FuiContext ctx, int item, const(char)[] caption) {
231 if (auto data = ctx.item!FuiCtlHead(item)) {
232 data.caption.setz(caption);
233 auto cpt = data.caption.getz;
234 auto lp = ctx.layprops(item);
235 /*if (lp.minSize.w < cpt.length)*/ lp.minSize.w = cast(int)cpt.length;
236 return true;
238 return false;
242 // ////////////////////////////////////////////////////////////////////////// //
243 void postClose (FuiContext ctx, int res) {
244 if (!ctx.valid) return;
245 ctx.queueEvent(0, FuiEvent.Type.Close, res);
249 // ////////////////////////////////////////////////////////////////////////// //
250 enum FuiDialogFrameType : ubyte {
251 Normal,
252 Small,
253 //None,
256 struct FuiCtlRootPanel {
257 mixin FuiCtlHeader;
258 FuiDialogFrameType frame;
259 bool enterclose;
260 bool moving;
261 // history manager for this dialog
262 FuiHistoryManager hisman;
263 TuiDialogCloseCB closecb;
267 bool dialogCloseCB (FuiContext ctx, TuiDialogCloseCB dcb) {
268 if (auto data = ctx.itemIntr!FuiCtlRootPanel(0)) {
269 data.closecb = dcb;
270 return true;
272 return false;
275 TuiDialogCloseCB dialogCloseCB (FuiContext ctx) {
276 if (auto data = ctx.itemIntr!FuiCtlRootPanel(0)) return data.closecb;
277 return null;
281 // purely cosmetic
282 void dialogMoving (FuiContext ctx, bool v) {
283 if (!ctx.valid) return;
284 auto data = ctx.itemIntr!FuiCtlRootPanel(0);
285 data.moving = v;
288 bool dialogMoving (FuiContext ctx) {
289 if (!ctx.valid) return false;
290 auto data = ctx.itemIntr!FuiCtlRootPanel(0);
291 return data.moving;
295 void dialogCaption (FuiContext ctx, const(char)[] text) {
296 if (!ctx.valid) return;
297 auto data = ctx.itemIntr!FuiCtlRootPanel(0);
298 data.caption.setz(text);
302 FuiDialogFrameType dialogFrame (FuiContext ctx) {
303 if (!ctx.valid) return FuiDialogFrameType.Normal;
304 auto data = ctx.itemIntr!FuiCtlRootPanel(0);
305 return data.frame;
308 void dialogFrame (FuiContext ctx, FuiDialogFrameType t) {
309 if (!ctx.valid) return;
310 auto data = ctx.itemIntr!FuiCtlRootPanel(0);
311 auto lp = ctx.layprops(0);
312 final switch (t) {
313 case FuiDialogFrameType.Normal:
314 data.frame = FuiDialogFrameType.Normal;
315 with (lp.padding) {
316 left = 3;
317 right = 3;
318 top = 2;
319 bottom = 2;
321 break;
322 case FuiDialogFrameType.Small:
323 data.frame = FuiDialogFrameType.Small;
324 with (lp.padding) {
325 left = 1;
326 right = 1;
327 top = 1;
328 bottom = 1;
330 break;
335 void dialogEnterClose (FuiContext ctx, bool enterclose) {
336 if (!ctx.valid) return;
337 auto data = ctx.itemIntr!FuiCtlRootPanel(0);
338 data.enterclose = enterclose;
342 bool dialogEnterClose (FuiContext ctx) {
343 if (!ctx.valid) return false;
344 auto data = ctx.itemIntr!FuiCtlRootPanel(0);
345 return data.enterclose;
349 FuiHistoryManager dialogHistoryManager (FuiContext ctx) {
350 if (auto data = ctx.itemIntr!FuiCtlRootPanel(0)) return data.hisman;
351 return null;
355 void dialogHistoryManager (FuiContext ctx, FuiHistoryManager hisman) {
356 if (auto data = ctx.itemIntr!FuiCtlRootPanel(0)) data.hisman = hisman;
360 // ////////////////////////////////////////////////////////////////////////// //
361 struct FuiCtlCustomBox {
362 mixin FuiCtlHeader;
363 align(8) void* udata;
367 int custombox (FuiContext ctx, int parent, const(char)[] id=null) {
368 if (!ctx.valid) return -1;
369 auto item = ctx.addItem!FuiCtlSpan(parent);
370 with (ctx.layprops(item)) {
371 //flex = 1;
372 visible = true;
373 horizontal = true;
374 aligning = FuiLayoutProps.Align.Stretch;
375 minSize.w = 1;
376 minSize.h = 1;
377 //clickMask |= FuiLayoutProps.Buttons.Left;
378 //canBeFocused = true;
379 wantTab = false;
381 auto data = ctx.item!FuiCtlCustomBox(item);
382 data.type = FuiCtlType.CustomBox;
383 data.id.setz(id);
385 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
386 auto win = XtWindow(rc.x, rc.y, rc.w, rc.h);
387 win.color = ctx.palColor!"def"(item);
388 win.bg = 1;
389 win.fill(0, 0, rc.w, rc.h);
392 return item;
396 // ////////////////////////////////////////////////////////////////////////// //
397 struct FuiCtlSpan {
398 mixin FuiCtlHeader;
402 int span (FuiContext ctx, int parent, bool ahorizontal) {
403 if (!ctx.valid) return -1;
404 auto item = ctx.addItem!FuiCtlSpan(parent);
405 with (ctx.layprops(item)) {
406 flex = 1;
407 visible = false;
408 horizontal = ahorizontal;
409 aligning = (ahorizontal ? FuiLayoutProps.Align.Stretch : FuiLayoutProps.Align.Start);
411 auto data = ctx.item!FuiCtlSpan(item);
412 data.type = FuiCtlType.Invisible;
413 //data.pal = Palette.init;
414 return item;
417 int hspan (FuiContext ctx, int parent) { return ctx.span(parent, true); }
418 int vspan (FuiContext ctx, int parent) { return ctx.span(parent, false); }
421 int spacer (FuiContext ctx, int parent) {
422 if (!ctx.valid) return -1;
423 auto item = ctx.addItem!FuiCtlSpan(parent);
424 with (ctx.layprops(item)) {
425 flex = 0;
426 visible = false;
427 horizontal = true;
428 aligning = FuiLayoutProps.Align.Start;
430 auto data = ctx.item!FuiCtlSpan(item);
431 data.type = FuiCtlType.Invisible;
432 //data.pal = Palette.init;
433 return item;
437 int hline (FuiContext ctx, int parent) {
438 if (!ctx.valid) return -1;
439 auto item = ctx.addItem!FuiCtlSpan(parent);
440 with (ctx.layprops(item)) {
441 flex = 0;
442 visible = true;
443 horizontal = true;
444 lineBreak = true;
445 aligning = FuiLayoutProps.Align.Stretch;
446 minSize.h = maxSize.h = 1;
448 auto data = ctx.item!FuiCtlSpan(item);
449 data.type = FuiCtlType.HLine;
450 //data.pal = Palette.init;
451 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
452 auto lp = ctx.layprops(self);
453 auto win = XtWindow.fullscreen;
454 win.color = ctx.palColor!"def"(self);
455 if (lp.parent == 0) {
456 auto rlp = ctx.layprops(0);
457 int x0, x1;
458 final switch (ctx.dialogFrame) {
459 case FuiDialogFrameType.Normal:
460 x0 = rc.x-2;
461 x1 = x0+(rlp.position.w-2);
462 break;
463 case FuiDialogFrameType.Small:
464 x0 = rc.x-1;
465 x1 = x0+rlp.position.w;
466 break;
468 win.hline(x0, rc.y, x1-x0);
469 } else {
470 win.hline(rc.x, rc.y, rc.w);
473 return item;
476 // ////////////////////////////////////////////////////////////////////////// //
477 struct FuiCtlBox {
478 mixin FuiCtlHeader;
482 int box (FuiContext ctx, int parent, bool ahorizontal, const(char)[] id=null) {
483 if (!ctx.valid) return -1;
484 auto item = ctx.addItem!FuiCtlBox(parent);
485 with (ctx.layprops(item)) {
486 flex = (ahorizontal ? 0 : 1);
487 horizontal = ahorizontal;
488 aligning = (ahorizontal ? FuiLayoutProps.Align.Stretch : FuiLayoutProps.Align.Start);
489 visible = true;
491 auto data = ctx.item!FuiCtlBox(item);
492 data.type = FuiCtlType.Box;
493 //data.pal = Palette.init;
494 data.id.setz(id);
495 return item;
498 int hbox (FuiContext ctx, int parent, const(char)[] id=null) { return ctx.box(parent, true, id); }
499 int vbox (FuiContext ctx, int parent, const(char)[] id=null) { return ctx.box(parent, false, id); }
502 // ////////////////////////////////////////////////////////////////////////// //
503 struct FuiCtlPanel {
504 mixin FuiCtlHeader;
508 int panel (FuiContext ctx, int parent, const(char)[] caption, bool ahorizontal, const(char)[] id=null) {
509 if (!ctx.valid) return -1;
510 auto item = ctx.addItem!FuiCtlPanel(parent);
511 with (ctx.layprops(item)) {
512 flex = (ahorizontal ? 0 : 1);
513 horizontal = ahorizontal;
514 aligning = (ahorizontal ? FuiLayoutProps.Align.Stretch : FuiLayoutProps.Align.Start);
515 minSize = FuiSize(visStrLen(caption)+4, 2);
516 padding.left = 1;
517 padding.right = 1;
518 padding.top = 1;
519 padding.bottom = 1;
521 auto data = ctx.item!FuiCtlPanel(item);
522 data.type = FuiCtlType.Panel;
523 //data.pal = Palette.init;
524 data.id.setz(id);
525 data.caption.setz(caption);
526 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
527 auto data = ctx.item!FuiCtlPanel(self);
528 auto win = XtWindow.fullscreen;
529 win.color = ctx.palColor!"def"(self);
530 win.frame!true(rc.x, rc.y, rc.w, rc.h);
531 win.tuiWriteStr!("center", true)(rc.x+1, rc.y, rc.w-2, data.caption.getz, ctx.palColor!"title"(self), ctx.palColor!"hot"(self));
533 return item;
536 int hpanel (FuiContext ctx, int parent, const(char)[] caption, const(char)[] id=null) { return ctx.panel(parent, caption, true, id); }
537 int vpanel (FuiContext ctx, int parent, const(char)[] caption, const(char)[] id=null) { return ctx.panel(parent, caption, false, id); }
540 // ////////////////////////////////////////////////////////////////////////// //
541 struct FuiCtlEditLine {
542 mixin FuiCtlHeader;
543 align(8) TtyEditor ed;
545 static assert(FuiCtlEditLine.ed.offsetof%4 == 0);
548 private int editlinetext(bool text) (FuiContext ctx, int parent, const(char)[] id, const(char)[] deftext=null, bool utfuck=false) {
549 if (!ctx.valid) return -1;
550 auto item = ctx.addItem!FuiCtlEditLine(parent);
551 with (ctx.layprops(item)) {
552 flex = 0;
553 horizontal = true;
554 clickMask |= FuiLayoutProps.Buttons.Left;
555 canBeFocused = true;
556 wantTab = false;
557 //aligning = (ahorizontal ? FuiLayoutProps.Align.Stretch : FuiLayoutProps.Align.Start);
558 int mw = (deftext.length < 10 ? 10 : deftext.length > 50 ? 50 : cast(int)deftext.length);
559 minSize = FuiSize(mw, 1);
560 maxSize = FuiSize(int.max-1024, 1);
562 auto data = ctx.item!FuiCtlEditLine(item);
563 static if (text) {
564 data.type = FuiCtlType.EditText;
565 } else {
566 data.type = FuiCtlType.EditLine;
568 //data.pal = Palette.init;
569 data.id.setz(id);
570 data.ed = new TtyEditor(0, 0, 10, 1, !text); // size will be fixed later
571 data.ed.utfuck = utfuck;
572 data.ed.setNewText(deftext);
573 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
574 auto data = ctx.item!FuiCtlEditLine(self);
575 auto lp = ctx.layprops(self);
576 auto ed = data.ed;
577 ed.hideStatus = true;
578 ed.moveResize(rc.x+(ed.singleline ? 0 : 1), rc.y, rc.w, rc.h);
579 ed.fullDirty;
580 ed.dontSetCursor = (self != ctx.focused);
581 if (lp.enabled) {
582 if (self == ctx.focused) {
583 ed.clrBlock = ctx.palColor!"inputmark"(self);
584 ed.clrText = ctx.palColor!"input"(self);
585 ed.clrTextUnchanged = ctx.palColor!"inputunchanged"(self);
586 } else {
587 ed.clrBlock = ctx.palColor!"inputmark"(self);
588 ed.clrText = ctx.palColor!"input"(self);
589 ed.clrTextUnchanged = ctx.palColor!"inputunchanged"(self);
591 } else {
592 ed.clrBlock = ctx.palColor!"disabled"(self);
593 ed.clrText = ctx.palColor!"disabled"(self);
594 ed.clrTextUnchanged = ctx.palColor!"disabled"(self);
596 //auto oldcolor = xtGetColor();
597 //scope(exit) xtSetColor(oldcolor);
598 ed.drawPage();
600 data.eventcb = delegate (FuiContext ctx, int self, FuiEvent ev) {
601 if (ev.item != self) return false;
602 auto eld = ctx.item!FuiCtlEditLine(self);
603 final switch (ev.type) {
604 case FuiEvent.Type.None:
605 return false;
606 case FuiEvent.Type.Char: // param0: dchar; param1: mods&buttons
607 TtyEvent k;
608 k.key = TtyEvent.Key.Char;
609 k.ch = ev.ch;
610 if (eld.ed.processKey(k)) {
611 if (eld.actcb !is null) {
612 auto rr = eld.actcb(ctx, ev.item);
613 if (rr >= -1) ctx.postClose(rr);
615 return true;
617 return false;
618 case FuiEvent.Type.Key: // param0: sdpy keycode; param1: mods&buttons
619 // history
620 if (eld.ed.singleline) {
621 if (auto hisman = ctx.dialogHistoryManager) {
622 if (auto data = ctx.item!FuiCtlEditLine(self)) {
623 auto eid = data.id.getz;
624 if (eid.length && hisman.has(eid)) {
625 if (ev.key == "M-H") {
626 // history dialog
627 if (auto lp = ctx.layprops(self)) {
628 auto pt = ctx.toGlobal(self, FuiPoint(0, 0));
629 auto hidx = dialogHistory(hisman, eid, pt.x, pt.y);
630 if (hidx >= 0) {
631 auto s = hisman.item(eid, hidx);
632 eld.ed.setNewText(s, false); // don't clear on type
633 hisman.activate(eid, hidx);
634 if (eld.actcb !is null) {
635 auto rr = eld.actcb(ctx, ev.item);
636 if (rr >= -1) ctx.postClose(rr);
639 return true;
646 // editline
647 if (eld.ed.processKey(ev.key)) {
648 if (eld.actcb !is null) {
649 auto rr = eld.actcb(ctx, ev.item);
650 if (rr >= -1) ctx.postClose(rr);
652 return true;
654 return false;
655 case FuiEvent.Type.Click: // mouse click; param0: buttton index; param1: mods&buttons
656 if (eld.ed.processClick(ev.bidx, ev.x, ev.y)) return true;
657 return true;
658 case FuiEvent.Type.Double: // mouse double-click; param0: buttton index; param1: mods&buttons
659 return true;
660 case FuiEvent.Type.Close: // close dialog; param0: return id
661 return false;
664 return item;
668 // `actcb` will be called *after* editor was changed (or not changed, who knows)
669 int editline (FuiContext ctx, int parent, const(char)[] id, const(char)[] deftext=null, bool utfuck=false) {
670 return editlinetext!false(ctx, parent, id, deftext, utfuck);
674 // `actcb` will be called *after* editor was changed (or not changed, who knows)
675 int edittext (FuiContext ctx, int parent, const(char)[] id, const(char)[] deftext=null, bool utfuck=false) {
676 return editlinetext!true(ctx, parent, id, deftext, utfuck);
680 TtyEditor editlineEditor (FuiContext ctx, int item) {
681 if (auto data = ctx.item!FuiCtlEditLine(item)) {
682 if (data.type == FuiCtlType.EditLine) return data.ed;
684 return null;
688 TtyEditor edittextEditor (FuiContext ctx, int item) {
689 if (auto data = ctx.item!FuiCtlEditLine(item)) {
690 if (data.type == FuiCtlType.EditText) return data.ed;
692 return null;
696 char[] editlineGetText (FuiContext ctx, int item) {
697 if (auto edl = ctx.itemAs!"editline"(item)) {
698 auto ed = edl.ed;
699 if (ed is null) return null;
700 char[] res;
701 auto rng = ed[];
702 res.reserve(rng.length);
703 foreach (char ch; rng) res ~= ch;
704 return res;
706 return null;
710 char[] edittextGetText (FuiContext ctx, int item) {
711 if (auto edl = ctx.itemAs!"edittext"(item)) {
712 auto ed = edl.ed;
713 if (ed is null) return null;
714 char[] res;
715 auto rng = ed[];
716 res.reserve(rng.length);
717 foreach (char ch; rng) res ~= ch;
718 return res;
720 return null;
724 // ////////////////////////////////////////////////////////////////////////// //
725 mixin template FuiCtlBtnLike() {
726 mixin FuiCtlHeader;
727 char hotchar = 0;
728 bool spaceClicks = true;
729 bool enterClicks = false;
730 align(8):
731 // internal function, does some action on "click"
732 // will be called before `actcb`
733 // return `false` if click should not be processed further (no `actcb` will be called)
734 bool delegate (FuiContext ctx, int self) doclickcb;
737 struct FuiCtlBtnLikeHead {
738 mixin FuiCtlBtnLike;
742 private bool btnlikeClick (FuiContext ctx, int item, int clickButton=-1) {
743 if (auto lp = ctx.layprops(item)) {
744 if (!lp.visible || lp.disabled || (clickButton >= 0 && lp.clickMask == 0)) return false;
745 auto data = ctx.item!FuiCtlBtnLikeHead(item);
746 bool clicked = false;
747 if (clickButton >= 0) {
748 foreach (ubyte shift; 0..8) {
749 if (shift == clickButton && (lp.clickMask&(1<<shift)) != 0) { clicked = true; break; }
751 } else {
752 clicked = true;
754 if (clicked) {
755 if (data.doclickcb !is null) {
756 if (!data.doclickcb(ctx, item)) return false;
758 if (data.actcb !is null) {
759 auto rr = data.actcb(ctx, item);
760 if (rr >= -1) {
761 ctx.postClose(rr);
762 return true;
765 return true;
768 return false;
772 private int buttonLike(T, FuiCtlType type) (FuiContext ctx, int parent, const(char)[] id, const(char)[] text) {
773 if (!ctx.valid) return -1;
774 auto item = ctx.addItem!T(parent);
775 auto data = ctx.item!T(item);
776 data.type = type;
777 //data.pal = Palette.init;
778 if (text.length > 255) text = text[0..255];
779 if (id.length > 255) id = id[0..255];
780 data.id.setz(id);
781 data.caption.setz(text);
782 if (text.length) {
783 data.hotchar = visHotChar(text).toupper;
784 } else {
785 data.hotchar = 0;
787 with (ctx.layprops(item)) {
788 flex = 0;
789 minSize = FuiSize(visStrLen(data.caption.getz), 1);
790 clickMask |= FuiLayoutProps.Buttons.Left;
792 data.eventcb = delegate (FuiContext ctx, int self, FuiEvent ev) {
793 final switch (ev.type) {
794 case FuiEvent.Type.None:
795 return false;
796 case FuiEvent.Type.Char: // param0: dchar; param1: mods&buttons
797 if (ev.item != self) return false;
798 auto data = ctx.item!FuiCtlBtnLikeHead(self);
799 if (!data.spaceClicks) return false;
800 if (ev.ch != ' ') return false;
801 if (auto lp = ctx.layprops(self)) {
802 if (!lp.visible || lp.disabled) return false;
803 if (lp.canBeFocused) ctx.focused = self;
804 return ctx.btnlikeClick(self);
806 return false;
807 case FuiEvent.Type.Key: // param0: sdpy keycode; param1: mods&buttons
808 auto data = ctx.item!FuiCtlBtnLikeHead(self);
809 auto lp = ctx.layprops(self);
810 if (!lp.visible || lp.disabled) return false;
811 if (data.enterClicks && ev.item == self && ev.key == "Enter") {
812 if (lp.canBeFocused) ctx.focused = self;
813 return ctx.btnlikeClick(self);
815 if (ev.key.key != TtyEvent.Key.ModChar || ev.key.ctrl || !ev.key.alt || ev.key.shift) return false;
816 if (data.hotchar != ev.key.ch) return false;
817 if (lp.canBeFocused) ctx.focused = self;
818 ctx.btnlikeClick(self);
819 return true;
820 case FuiEvent.Type.Click: // mouse click; param0: buttton index; param1: mods&buttons
821 if (ev.item == self) return ctx.btnlikeClick(self, ev.bidx);
822 return false;
823 case FuiEvent.Type.Double: // mouse double-click; param0: buttton index; param1: mods&buttons
824 return false;
825 case FuiEvent.Type.Close: // close dialog; param0: return id
826 return false;
829 return item;
833 // ////////////////////////////////////////////////////////////////////////// //
834 struct FuiCtlLabel {
835 mixin FuiCtlBtnLike;
836 ubyte destlen;
837 char[256] dest;
840 int label (FuiContext ctx, int parent, const(char)[] id, const(char)[] text, const(char)[] destid=null) {
841 auto res = ctx.buttonLike!(FuiCtlLabel, FuiCtlType.Label)(parent, id, text);
842 with (ctx.layprops(res)) {
843 clickMask = 0;
844 canBeFocused = false;
846 auto data = ctx.item!FuiCtlLabel(res);
847 data.dest.setz(destid);
848 if (destid.length == 0) {
849 data.hotchar = 0;
850 ctx.layprops(res).minSize.w = visStrLen!false(data.caption.getz);
851 ctx.layprops(res).clickMask = 0;
853 data.spaceClicks = data.enterClicks = false;
854 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
855 auto data = ctx.item!FuiCtlLabel(self);
856 auto lp = ctx.layprops(self);
857 auto win = XtWindow.fullscreen;
858 uint anorm, ahot;
859 if (lp.enabled) {
860 anorm = ctx.palColor!"def"(self);
861 ahot = ctx.palColor!"hot"(self);
862 } else {
863 anorm = ahot = ctx.palColor!"disabled"(self);
865 if (data.hotchar) {
866 win.tuiWriteStr!("right", false)(rc.x, rc.y, rc.w, data.caption.getz, anorm, ahot);
867 } else {
868 win.tuiWriteStr!("left", false, false)(rc.x, rc.y, rc.w, data.caption.getz, anorm, ahot);
871 data.doclickcb = delegate (FuiContext ctx, int self) {
872 auto data = ctx.item!FuiCtlLabel(self);
873 auto did = ctx[data.dest.getz];
874 if (did <= 0) return false;
875 if (auto lp = ctx.layprops(did)) {
876 if (lp.canBeFocused && lp.visible && lp.enabled) {
877 ctx.focused = did;
878 return true;
881 return false;
883 return res;
887 // ////////////////////////////////////////////////////////////////////////// //
888 struct FuiCtlButton {
889 mixin FuiCtlBtnLike;
892 int button (FuiContext ctx, int parent, const(char)[] id, const(char)[] text) {
893 auto item = ctx.buttonLike!(FuiCtlButton, FuiCtlType.Button)(parent, id, text);
894 if (item >= 0) {
895 with (ctx.layprops(item)) {
896 //clickMask |= FuiLayoutProps.Buttons.Left;
897 canBeFocused = true;
898 minSize.w += 4;
900 auto data = ctx.item!FuiCtlButton(item);
901 data.spaceClicks = data.enterClicks = true;
902 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
903 auto data = ctx.item!FuiCtlButton(self);
904 auto lp = ctx.layprops(self);
905 auto win = XtWindow.fullscreen;
906 uint anorm, ahot;
907 int hotx = rc.x;
908 if (lp.enabled) {
909 if (ctx.focused != self) {
910 anorm = ctx.palColor!"def"(self);
911 ahot = ctx.palColor!"hot"(self);
912 } else {
913 anorm = ctx.palColor!"sel"(self);
914 ahot = ctx.palColor!"hotsel"(self);
916 } else {
917 anorm = ahot = ctx.palColor!"disabled"(self);
919 win.color = anorm;
920 bool def = ((lp.userFlags&FuiCtlUserFlags.Default) != 0);
921 if (rc.w == 1) {
922 win.writeCharsAt!true(rc.x, rc.y, 1, '`');
923 } else if (rc.w == 2) {
924 win.writeStrAt(rc.x, rc.y, (def ? "<>" : "[]"));
925 } else if (rc.w > 2) {
926 win.writeCharsAt(rc.x, rc.y, rc.w, ' ');
927 if (def) {
928 win.writeCharsAt(rc.x+1, rc.y, 1, '<');
929 win.writeCharsAt(rc.x+rc.w-2, rc.y, 1, '>');
931 win.writeCharsAt(rc.x, rc.y, 1, '[');
932 win.writeCharsAt(rc.x+rc.w-1, rc.y, 1, ']');
933 hotx = rc.x+1;
934 win.tuiWriteStr!("center", false)(rc.x+2, rc.y, rc.w-4, data.caption.getz, anorm, ahot, &hotx);
936 if (ctx.focused == self) win.gotoXY(hotx, rc.y);
938 data.doclickcb = delegate (FuiContext ctx, int self) {
939 // send "close" to root
940 ctx.postClose(self);
941 return true;
944 return item;
948 // ////////////////////////////////////////////////////////////////////////// //
949 private void drawCheckRadio(string type) (FuiContext ctx, int self, FuiRect rc, const(char)[] text, bool marked) {
950 static assert(type == "checkbox" || type == "radio");
951 auto lp = ctx.layprops(self);
952 auto win = XtWindow.fullscreen;
953 uint anorm, ahot;
954 int hotx = rc.x;
955 if (lp.enabled) {
956 if (ctx.focused != self) {
957 anorm = ctx.palColor!"def"(self);
958 ahot = ctx.palColor!"hot"(self);
959 } else {
960 anorm = ctx.palColor!"sel"(self);
961 ahot = ctx.palColor!"hotsel"(self);
963 } else {
964 anorm = ahot = ctx.palColor!"disabled"(self);
966 win.color = anorm;
967 win.writeCharsAt(rc.x, rc.y, rc.w, ' ');
968 char markCh = ' ';
969 if (marked) {
970 static if (type == "checkbox") markCh = 'x';
971 else static if (type == "radio") markCh = '*';
972 else static assert(0, "wtf?!");
974 if (rc.w == 1) {
975 win.writeCharsAt(rc.x, rc.y, 1, markCh);
976 } else if (rc.w == 2) {
977 win.writeCharsAt(rc.x, rc.y, 1, markCh);
978 win.writeCharsAt(rc.x+1, rc.y, 1, ' ');
979 } else if (rc.w > 2) {
980 static if (type == "checkbox") win.writeStrAt(rc.x, rc.y, "[ ]");
981 else static if (type == "radio") win.writeStrAt(rc.x, rc.y, "( )");
982 else static assert(0, "wtf?!");
983 if (markCh != ' ') win.writeCharsAt(rc.x+1, rc.y, 1, markCh);
984 hotx = rc.x+1;
985 win.tuiWriteStr!("left", false)(rc.x+4, rc.y, rc.w-4, text, anorm, ahot);
987 if (ctx.focused == self) win.gotoXY(hotx, rc.y);
991 struct FuiCtlCheck {
992 mixin FuiCtlBtnLike;
993 bool* var;
996 int checkbox (FuiContext ctx, int parent, const(char)[] id, const(char)[] text, bool* var) {
997 auto item = ctx.buttonLike!(FuiCtlCheck, FuiCtlType.Check)(parent, id, text);
998 if (item >= 0) {
999 auto data = ctx.item!FuiCtlCheck(item);
1000 data.spaceClicks = true;
1001 data.enterClicks = false;
1002 data.var = var;
1003 with (ctx.layprops(item)) {
1004 //clickMask |= FuiLayoutProps.Buttons.Left;
1005 canBeFocused = true;
1006 minSize.w += 4;
1008 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
1009 auto data = ctx.item!FuiCtlCheck(self);
1010 bool marked = (data.var !is null ? *data.var : false);
1011 ctx.drawCheckRadio!"checkbox"(self, rc, data.caption.getz, marked);
1013 data.doclickcb = delegate (FuiContext ctx, int self) {
1014 auto data = ctx.item!FuiCtlCheck(self);
1015 if (data.var !is null) *data.var = !*data.var;
1016 return true;
1019 return item;
1023 // ////////////////////////////////////////////////////////////////////////// //
1024 struct FuiCtlRadio {
1025 mixin FuiCtlBtnLike;
1026 int gid; // radio group index; <0: standalone
1027 int* var;
1030 private int countRadio (FuiContext ctx, int* var) {
1031 if (!ctx.valid || var is null) return -1;
1032 int res = 0;
1033 foreach (int fid; 1..ctx.length) {
1034 if (auto data = ctx.item!FuiCtlRadio(fid)) {
1035 if (data.type == FuiCtlType.Radio) {
1036 if (data.var is var) ++res;
1040 return res;
1043 int radio (FuiContext ctx, int parent, const(char)[] id, const(char)[] text, int* var) {
1044 auto item = ctx.buttonLike!(FuiCtlRadio, FuiCtlType.Radio)(parent, id, text);
1045 if (item >= 0) {
1046 auto gid = ctx.countRadio(var);
1047 auto data = ctx.item!FuiCtlRadio(item);
1048 data.spaceClicks = true;
1049 data.enterClicks = false;
1050 data.var = var;
1051 data.gid = gid;
1052 with (ctx.layprops(item)) {
1053 flex = 0;
1054 //clickMask |= FuiLayoutProps.Buttons.Left;
1055 canBeFocused = true;
1056 minSize.w += 4;
1058 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
1059 auto data = ctx.item!FuiCtlRadio(self);
1060 bool marked = (data.var ? (*data.var == data.gid) : false);
1061 ctx.drawCheckRadio!"radio"(self, rc, data.caption.getz, marked);
1063 data.doclickcb = delegate (FuiContext ctx, int self) {
1064 auto data = ctx.item!FuiCtlRadio(self);
1065 if (data.var !is null) *data.var = data.gid;
1066 return true;
1069 return item;
1073 // ////////////////////////////////////////////////////////////////////////// //
1074 struct FuiCtlTextView {
1075 mixin FuiCtlHeader;
1076 uint textlen;
1077 int topline;
1078 char[0] text; // this *will* be modified by wrapper; '\1' is "soft wrap"; '\0' is EOT
1082 // width (postition.w) must be already determined
1083 int textviewHeight (FuiContext ctx, int item) {
1084 if (auto lp = ctx.layprops(item)) {
1085 int w = lp.position.w;
1086 if (w < 1) w = lp.minSize.w;
1087 if (w < 1) w = lp.maxSize.w;
1088 if (w < 1) return 0;
1089 if (auto data = ctx.itemAs!"textview"(item)) {
1090 if (data.textlen) {
1091 int c, r;
1092 data.textlen = calcTextBoundsEx(c, r, data.text.ptr[0..data.textlen], w);
1093 return r;
1097 return 0;
1101 // `actcb` will be called *after* editor was changed (or not changed, who knows)
1102 int textview (FuiContext ctx, int parent, const(char)[] id, const(char)[] text) {
1103 if (!ctx.valid) return -1;
1104 if (text.length > 256*1024) throw new Exception("text view: text too long");
1105 auto item = ctx.addItem!FuiCtlTextView(parent, cast(int)text.length+256);
1106 with (ctx.layprops(item)) {
1107 flex = 1;
1108 aligning = FuiLayoutProps.Align.Stretch;
1109 clickMask |= FuiLayoutProps.Buttons.WheelUp|FuiLayoutProps.Buttons.WheelDown;
1110 //canBeFocused = true;
1112 auto data = ctx.item!FuiCtlTextView(item);
1113 data.type = FuiCtlType.TextView;
1114 //data.pal = Palette.init;
1115 data.id.setz(id);
1116 data.textlen = cast(uint)text.length;
1117 if (text.length > 0) {
1118 data.text.ptr[0..text.length] = text[];
1119 int c, r;
1120 data.textlen = calcTextBoundsEx(c, r, data.text.ptr[0..data.textlen], ttyw-8);
1121 //!//with (ctx.layprops(item).maxSize) { w = c; h = r; }
1122 //!//with (ctx.layprops(item).minSize) { w = 2; h = 1; }
1123 //with (ctx.layprops(item).minSize) { w = c+2; h = r; }
1124 //int minsz = r;
1125 //if (minsz > ttyh-ttyh/3) minsz = ttyh-ttyh/3;
1126 with (ctx.layprops(item).minSize) { w = c+2; h = 3; }
1127 with (ctx.layprops(item).maxSize) { w = c+2; h = r; }
1129 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
1130 if (rc.w < 1 || rc.h < 1) return;
1131 auto data = ctx.item!FuiCtlTextView(self);
1132 auto lp = ctx.layprops(self);
1133 //auto win = XtWindow.fullscreen;
1134 auto win = XtWindow(rc.x, rc.y, rc.w, rc.h);
1135 if (lp.enabled) {
1136 win.color = ctx.palColor!"def"(self);
1137 } else {
1138 win.color = ctx.palColor!"disabled"(self);
1140 //win.fill(rc.x, rc.y, rc.w, rc.h);
1141 win.fill(0, 0, rc.w, rc.h);
1142 if (data.textlen) {
1143 int c, r;
1144 data.textlen = calcTextBoundsEx(c, r, data.text.ptr[0..data.textlen], rc.w/*-lp.padding.left-lp.padding.right*/);
1145 if (data.topline < 0) data.topline = 0;
1146 if (data.topline+rc.h >= r) {
1147 data.topline = r-rc.h;
1148 if (data.topline < 0) data.topline = 0;
1150 bool wantSBar = (r > rc.h);
1151 int xofs = (wantSBar ? 2 : 1);
1152 uint tpos = 0;
1153 int ty = -data.topline;
1154 int talign = 0; // 0: left; 1: right; 2: center;
1155 if (data.textlen > 0 && data.text.ptr[0] >= 1 && data.text.ptr[0] <= 3) {
1156 talign = data.text.ptr[tpos]-1;
1157 ++tpos;
1159 while (tpos < data.textlen && ty < rc.h) {
1160 uint epos = tpos;
1161 while (epos < data.textlen && data.text.ptr[epos] != '\n' && data.text.ptr[epos] != '\6') {
1162 if (epos-tpos == rc.w) break;
1163 ++epos;
1165 final switch (talign) {
1166 case 0: // left
1167 win.writeStrAt(xofs, ty, data.text.ptr[tpos..epos]);
1168 break;
1169 case 1: // right
1170 win.writeStrAt(rc.w-xofs-(epos-tpos), ty, data.text.ptr[tpos..epos]);
1171 break;
1172 case 2: // center
1173 win.writeStrAt(xofs+(rc.w-xofs-(epos-tpos))/2, ty, data.text.ptr[tpos..epos]);
1174 break;
1176 if (epos < data.textlen && data.text.ptr[epos] <= ' ') {
1177 if (data.textlen-epos > 1 && data.text.ptr[epos] == '\n' && data.text.ptr[epos+1] >= 1 && data.text.ptr[epos+1] <= 3) {
1178 ++epos;
1179 talign = data.text.ptr[epos]-1;
1180 } else {
1181 // keep it
1182 //talign = 0;
1184 ++epos;
1186 tpos = epos;
1187 ++ty;
1189 // draw scrollbar
1190 if (wantSBar) {
1191 //win.color = atext;
1192 win.vline(1, 0, rc.h);
1193 int last = data.topline+rc.h;
1194 if (last > r) last = r;
1195 last = rc.h*last/r;
1196 foreach (int yy; 0..rc.h) win.writeCharsAt!true(0, yy, 1, (yy <= last ? 'a' : ' '));
1200 data.eventcb = delegate (FuiContext ctx, int self, FuiEvent ev) {
1201 if (ev.item != self) return false;
1202 final switch (ev.type) {
1203 case FuiEvent.Type.None:
1204 return false;
1205 case FuiEvent.Type.Char: // param0: dchar; param1: mods&buttons
1206 return false;
1207 case FuiEvent.Type.Key: // param0: sdpy keycode; param1: mods&buttons
1208 auto lp = ctx.layprops(self);
1209 if (lp !is null) {
1210 if (!lp.visible || lp.disabled) return false;
1211 } else {
1212 return false;
1214 if (auto tv = ctx.itemAs!"textview"(self)) {
1215 if (ev.key == "Up") { --tv.topline; return true; }
1216 if (ev.key == "Down") { ++tv.topline; return true; }
1217 if (ev.key == "Home") { tv.topline = 0; return true; }
1218 if (ev.key == "End") { tv.topline = int.max/2; return true; }
1219 int pgstep = lp.position.h-1;
1220 if (pgstep < 1) pgstep = 1;
1221 if (ev.key == "PageUp") { tv.topline -= pgstep; return true; }
1222 if (ev.key == "PageDown") { tv.topline += pgstep; return true; }
1224 return false;
1225 case FuiEvent.Type.Click: // mouse click; param0: buttton index; param1: mods&buttons
1226 return false;
1227 case FuiEvent.Type.Double: // mouse double-click; param0: buttton index; param1: mods&buttons
1228 return false;
1229 case FuiEvent.Type.Close: // close dialog; param0: return id
1230 return false;
1233 return item;
1237 // ////////////////////////////////////////////////////////////////////////// //
1238 struct FuiCtlListBoxItem {
1239 uint nextofs;
1240 uint length;
1241 char[0] text;
1244 struct FuiCtlListBox {
1245 mixin FuiCtlHeader;
1246 int itemCount; // total number of items
1247 int topItem;
1248 int curItem;
1249 int maxWidth;
1250 uint firstItemOffset; // in layout buffer
1251 uint lastItemOffset; // in layout buffer
1252 uint topItemOffset; // in layout buffer
1256 // return `true` if it has scrollbar
1257 bool listboxNormPage (FuiContext ctx, int item) {
1258 auto data = ctx.item!FuiCtlListBox(item);
1259 if (data is null) return false;
1260 auto lp = ctx.layprops(item);
1261 // make current item visible
1262 if (data.itemCount == 0) return false;
1263 // sanitize current item (just in case, it should be sane always)
1264 if (data.curItem < 0) data.curItem = 0;
1265 if (data.curItem >= data.itemCount) data.curItem = data.itemCount-1;
1266 int oldtop = data.topItem;
1267 if (data.topItem > data.itemCount-lp.position.h) data.topItem = data.itemCount-lp.position.h;
1268 if (data.topItem < 0) data.topItem = 0;
1269 if (data.curItem < data.topItem) {
1270 data.topItem = data.curItem;
1271 } else if (data.topItem+lp.position.h <= data.curItem) {
1272 data.topItem = data.curItem-lp.position.h+1;
1273 if (data.topItem < 0) data.topItem = 0;
1275 if (data.topItem != oldtop || data.topItemOffset == 0) {
1276 data.topItemOffset = ctx.listboxItemOffset(item, data.topItem);
1277 assert(data.topItemOffset != 0);
1279 bool wantSBar = false;
1280 // should i draw a scrollbar?
1281 if (lp.position.w > 2 && (data.topItem > 0 || data.topItem+lp.position.h < data.itemCount)) {
1282 // yes
1283 wantSBar = true;
1285 return wantSBar;
1289 // `actcb` will be called *after* editor was changed (or not changed, who knows)
1290 int listbox (FuiContext ctx, int parent, const(char)[] id) {
1291 if (!ctx.valid) return -1;
1292 auto item = ctx.addItem!FuiCtlListBox(parent);
1293 if (item == -1) return -1;
1294 with (ctx.layprops(item)) {
1295 flex = 1;
1296 aligning = FuiLayoutProps.Align.Stretch;
1297 clickMask |= FuiLayoutProps.Buttons.Left|FuiLayoutProps.Buttons.WheelUp|FuiLayoutProps.Buttons.WheelDown;
1298 canBeFocused = true;
1299 minSize = FuiSize(5, 2);
1301 auto data = ctx.item!FuiCtlListBox(item);
1302 data.type = FuiCtlType.ListBox;
1303 //data.pal = Palette.init;
1304 data.id.setz(id);
1305 data.drawcb = delegate (FuiContext ctx, int self, FuiRect rc) {
1306 auto data = ctx.item!FuiCtlListBox(self);
1307 auto lp = ctx.layprops(self);
1308 auto win = XtWindow.fullscreen;
1309 // get colors
1310 uint atext, asel, agauge;
1311 if (lp.enabled) {
1312 atext = ctx.palColor!"def"(self);
1313 asel = ctx.palColor!"sel"(self);
1314 agauge = ctx.palColor!"gauge"(self);
1315 } else {
1316 atext = asel = agauge = ctx.palColor!"disabled"(self);
1318 win.color = atext;
1319 win.fill(rc.x, rc.y, rc.w, rc.h);
1320 // make current item visible
1321 if (data.itemCount == 0) return;
1322 // sanitize current item (just in case, it should be sane always)
1323 if (data.curItem < 0) data.curItem = 0;
1324 if (data.curItem >= data.itemCount) data.curItem = data.itemCount-1;
1325 int oldtop = data.topItem;
1326 if (data.topItem > data.itemCount-rc.h) data.topItem = data.itemCount-rc.h;
1327 if (data.topItem < 0) data.topItem = 0;
1328 if (data.curItem < data.topItem) {
1329 data.topItem = data.curItem;
1330 } else if (data.topItem+rc.h <= data.curItem) {
1331 data.topItem = data.curItem-rc.h+1;
1332 if (data.topItem < 0) data.topItem = 0;
1334 if (data.topItem != oldtop || data.topItemOffset == 0) {
1335 data.topItemOffset = ctx.listboxItemOffset(self, data.topItem);
1336 assert(data.topItemOffset != 0);
1338 bool wantSBar = false;
1339 int wdt = rc.w;
1340 int x = 0, y = 0;
1341 // should i draw a scrollbar?
1342 if (wdt > 2 && (data.topItem > 0 || data.topItem+rc.h < data.itemCount)) {
1343 // yes
1344 wantSBar = true;
1345 wdt -= 2;
1346 x += 2;
1347 } else {
1348 x += 1;
1349 wdt -= 1;
1351 x += rc.x;
1352 // draw items
1353 auto itofs = data.topItemOffset;
1354 auto curit = data.topItem;
1355 while (itofs != 0 && y < rc.h) {
1356 auto it = ctx.structAtOfs!FuiCtlListBoxItem(itofs);
1357 auto t = it.text.ptr[0..it.length];
1358 if (t.length > wdt) t = t[0..wdt];
1359 if (curit == data.curItem) {
1360 win.color = asel;
1361 // fill cursor
1362 win.writeCharsAt(x-(wantSBar ? 0 : 1), rc.y+y, wdt+(wantSBar ? 0 : 1), ' ');
1363 if (self == ctx.focused) win.gotoXY(x-(wantSBar ? 0 : 1), rc.y+y);
1364 } else {
1365 win.color = atext;
1367 win.writeStrAt(x, rc.y+y, t);
1368 itofs = it.nextofs;
1369 ++y;
1370 ++curit;
1372 // draw scrollbar
1373 if (wantSBar) {
1374 x -= 2;
1375 win.color = atext;
1376 win.vline(x+1, rc.y, rc.h);
1377 win.color = agauge; //atext;
1378 int last = data.topItem+rc.h;
1379 if (last > data.itemCount) last = data.itemCount;
1380 last = rc.h*last/data.itemCount;
1381 foreach (int yy; 0..rc.h) win.writeCharsAt!true(x, rc.y+yy, 1, (yy <= last ? 'a' : ' '));
1384 data.eventcb = delegate (FuiContext ctx, int self, FuiEvent ev) {
1385 if (ev.item != self) return false;
1386 final switch (ev.type) {
1387 case FuiEvent.Type.None:
1388 return false;
1389 case FuiEvent.Type.Char: // param0: dchar; param1: mods&buttons
1390 return false;
1391 case FuiEvent.Type.Key: // param0: sdpy keycode; param1: mods&buttons
1392 if (auto lp = ctx.layprops(self)) {
1393 if (!lp.visible || lp.disabled) return false;
1394 } else {
1395 return false;
1397 if (auto lbox = ctx.itemAs!"listbox"(self)) {
1398 bool procIt() () {
1399 auto lp = ctx.layprops(ev.item);
1400 if (ev.key == "Up") {
1401 if (--lbox.curItem < 0) lbox.curItem = 0;
1402 return true;
1404 if (ev.key == "S-Up") {
1405 if (--lbox.topItem < 0) lbox.topItem = 0;
1406 lbox.topItemOffset = 0; // invalidate
1407 return true;
1409 if (ev.key == "Down") {
1410 if (lbox.itemCount > 0) {
1411 if (++lbox.curItem >= lbox.itemCount) lbox.curItem = lbox.itemCount-1;
1413 return true;
1415 if (ev.key == "S-Down") {
1416 if (lbox.topItem+lp.position.h < lbox.itemCount) {
1417 ++lbox.topItem;
1418 lbox.topItemOffset = 0; // invalidate
1420 return true;
1422 if (ev.key == "Home") {
1423 lbox.curItem = 0;
1424 return true;
1426 if (ev.key == "End") {
1427 if (lbox.itemCount > 0) lbox.curItem = lbox.itemCount-1;
1428 return true;
1430 if (ev.key == "PageUp") {
1431 if (lbox.curItem > lbox.topItem) {
1432 lbox.curItem = lbox.topItem;
1433 } else if (lp.position.h > 1) {
1434 if ((lbox.curItem -= lp.position.h-1) < 0) lbox.curItem = 0;
1436 return true;
1438 if (ev.key == "PageDown") {
1439 if (lbox.curItem < lbox.topItem+lp.position.h-1) {
1440 lbox.curItem = lbox.topItem+lp.position.h-1;
1441 } else if (lp.position.h > 1 && lbox.itemCount > 0) {
1442 if ((lbox.curItem += lp.position.h-1) >= lbox.itemCount) lbox.curItem = lbox.itemCount-1;
1444 return true;
1446 return false;
1448 ctx.listboxNormPage(self);
1449 auto oldCI = lbox.curItem;
1450 if (procIt()) {
1451 ctx.listboxNormPage(self);
1452 if (oldCI != lbox.curItem && lbox.actcb !is null) {
1453 auto rr = lbox.actcb(ctx, self);
1454 if (rr >= -1) ctx.postClose(rr);
1456 return true;
1459 return false;
1460 case FuiEvent.Type.Click: // mouse click; param0: buttton index; param1: mods&buttons
1461 if (auto lbox = ctx.itemAs!"listbox"(self)) {
1462 ctx.listboxNormPage(self);
1463 auto oldCI = lbox.curItem;
1464 if (ev.bidx == FuiLayoutProps.Button.WheelUp) {
1465 if (--lbox.curItem < 0) lbox.curItem = 0;
1466 } else if (ev.bidx == FuiLayoutProps.Button.WheelDown) {
1467 if (lbox.itemCount > 0) {
1468 if (++lbox.curItem >= lbox.itemCount) lbox.curItem = lbox.itemCount-1;
1470 } else if (ev.x > 0) {
1471 int it = lbox.topItem+ev.y;
1472 lbox.curItem = it;
1474 ctx.listboxNormPage(self);
1475 if (oldCI != lbox.curItem && lbox.actcb !is null) {
1476 auto rr = lbox.actcb(ctx, self);
1477 if (rr >= -1) ctx.postClose(rr);
1480 return true;
1481 case FuiEvent.Type.Double: // mouse double-click; param0: buttton index; param1: mods&buttons
1482 return false;
1483 case FuiEvent.Type.Close: // close dialog; param0: return id
1484 return false;
1487 return item;
1491 void listboxItemAdd (FuiContext ctx, int item, const(char)[] text) {
1492 if (auto data = ctx.itemAs!"listbox"(item)) {
1493 int len = (text.length < 255 ? cast(int)text.length : 255);
1494 uint itofs;
1495 auto it = ctx.addStruct!FuiCtlListBoxItem(itofs, len);
1496 it.length = len;
1497 if (text.length <= 255) {
1498 it.text.ptr[0..text.length] = text[];
1499 } else {
1500 it.text.ptr[0..256] = text[0..256];
1501 it.text.ptr[253..256] = '.';
1503 if (data.maxWidth < len) data.maxWidth = len;
1504 if (data.firstItemOffset == 0) {
1505 data.firstItemOffset = itofs;
1506 data.topItemOffset = itofs;
1507 } else {
1508 auto prev = ctx.structAtOfs!FuiCtlListBoxItem(data.lastItemOffset);
1509 prev.nextofs = itofs;
1511 data.lastItemOffset = itofs;
1512 ++data.itemCount;
1513 auto lp = ctx.layprops(item);
1514 if (lp.parent >= 0) {
1515 auto pp = ctx.layprops(lp.parent);
1516 assert(pp !is null);
1517 int maxW = pp.maxSize.w-(lp.parent == 0 ? 2 : 0);
1518 if (lp.minSize.w < len+3) lp.minSize.w = len+3;
1519 if (maxW > 0 && lp.minSize.w > maxW) lp.minSize.w = maxW;
1520 int maxH = pp.maxSize.h-(lp.parent == 0 ? 2 : 0);
1521 if (lp.minSize.h < data.itemCount) lp.minSize.h = data.itemCount;
1522 if (maxH > 0 && lp.minSize.h > maxH) lp.minSize.h = maxH;
1523 if (lp.minSize.h > lp.maxSize.h) lp.minSize.h = lp.maxSize.h;
1524 } else {
1525 if (lp.minSize.w < len+3) lp.minSize.w = len+3;
1526 if (lp.minSize.w > lp.maxSize.w) lp.minSize.w = lp.maxSize.w;
1527 if (lp.minSize.h < data.itemCount) lp.minSize.h = data.itemCount;
1528 if (lp.minSize.h > lp.maxSize.h) lp.minSize.h = lp.maxSize.h;
1534 int listboxMaxItemWidth (FuiContext ctx, int item) {
1535 if (auto data = ctx.itemAs!"listbox"(item)) return data.maxWidth+2;
1536 return 0;
1540 int listboxItemCount (FuiContext ctx, int item) {
1541 if (auto data = ctx.itemAs!"listbox"(item)) return data.itemCount;
1542 return 0;
1546 int listboxItemCurrent (FuiContext ctx, int item) {
1547 if (auto data = ctx.itemAs!"listbox"(item)) return data.curItem;
1548 return 0;
1552 void listboxItemSetCurrent (FuiContext ctx, int item, int cur) {
1553 if (auto data = ctx.itemAs!"listbox"(item)) {
1554 if (data.itemCount > 0) {
1555 if (cur < 0) cur = 0;
1556 if (cur > data.itemCount) cur = data.itemCount-1;
1557 data.curItem = cur;
1563 private uint listboxItemOffset (FuiContext ctx, int item, int inum) {
1564 if (auto data = ctx.itemAs!"listbox"(item)) {
1565 if (inum > data.itemCount) inum = data.itemCount-1;
1566 if (inum < 0) inum = 0;
1567 uint itofs = data.firstItemOffset;
1568 while (inum-- > 0) {
1569 assert(itofs > 0);
1570 auto it = ctx.structAtOfs!FuiCtlListBoxItem(itofs);
1571 if (it.nextofs == 0) break;
1572 itofs = it.nextofs;
1574 return itofs;
1576 return 0;
1580 // ////////////////////////////////////////////////////////////////////////// //
1581 // `id` is element id
1582 public class FuiHistoryManager {
1583 public:
1584 this () {}
1585 abstract bool has (const(char)[] id);
1586 abstract int count (const(char)[] id);
1587 abstract const(char)[] item (const(char)[] id, int idx); // 0: oldest
1588 abstract void add (const(char)[] id, const(char)[] value); // this can shrink history; should correctly process duplicates
1589 abstract void clear (const(char)[] id);
1590 abstract void activate (const(char)[] id, int idx); // usually moves item to bottom
1594 // ////////////////////////////////////////////////////////////////////////// //
1595 // returned value valid until first layout change
1596 const(char)[] itemId (FuiContext ctx, int item) nothrow @trusted @nogc {
1597 if (auto lp = ctx.item!FuiCtlHead(item)) return lp.id.getz;
1598 return null;
1602 // ////////////////////////////////////////////////////////////////////////// //
1603 // return item id or -1
1604 int findById (FuiContext ctx, const(char)[] id) nothrow @trusted @nogc {
1605 foreach (int fid; 0..ctx.length) {
1606 if (auto data = ctx.item!FuiCtlHead(fid)) {
1607 if (data.id.getz == id) return fid;
1610 return -1;
1614 auto itemAs(string type) (FuiContext ctx, int item) nothrow @trusted @nogc {
1615 if (!ctx.valid) return null;
1616 static if (type.strEquCI("hline")) {
1617 enum ctp = FuiCtlType.HLine;
1618 alias tp = FuiCtlSpan;
1619 } else static if (type.strEquCI("box") || type.strEquCI("vbox") || type.strEquCI("hbox")) {
1620 enum ctp = FuiCtlType.Box;
1621 alias tp = FuiCtlBox;
1622 } else static if (type.strEquCI("panel") || type.strEquCI("vpanel") || type.strEquCI("hpanel")) {
1623 enum ctp = FuiCtlType.Panel;
1624 alias tp = FuiCtlPanel;
1625 } else static if (type.strEquCI("editline")) {
1626 enum ctp = FuiCtlType.EditLine;
1627 alias tp = FuiCtlEditLine;
1628 } else static if (type.strEquCI("edittext")) {
1629 enum ctp = FuiCtlType.EditText;
1630 alias tp = FuiCtlEditLine;
1631 } else static if (type.strEquCI("label")) {
1632 enum ctp = FuiCtlType.Label;
1633 alias tp = FuiCtlLabel;
1634 } else static if (type.strEquCI("button")) {
1635 enum ctp = FuiCtlType.Button;
1636 alias tp = FuiCtlButton;
1637 } else static if (type.strEquCI("check") || type.strEquCI("checkbox")) {
1638 enum ctp = FuiCtlType.Check;
1639 alias tp = FuiCtlCheck;
1640 } else static if (type.strEquCI("radio") || type.strEquCI("radio")) {
1641 enum ctp = FuiCtlType.Radio;
1642 alias tp = FuiCtlRadio;
1643 } else static if (type.strEquCI("textview")) {
1644 enum ctp = FuiCtlType.TextView;
1645 alias tp = FuiCtlTextView;
1646 } else static if (type.strEquCI("listbox")) {
1647 enum ctp = FuiCtlType.ListBox;
1648 alias tp = FuiCtlListBox;
1649 } else static if (type.strEquCI("custombox")) {
1650 enum ctp = FuiCtlType.CustomBox;
1651 alias tp = FuiCtlCustomBox;
1652 } else {
1653 static assert(0, "invalid control type: '"~type~"'");
1655 if (auto data = ctx.item!FuiCtlHead(item)) {
1656 if (data.type == ctp) return ctx.item!tp(item);
1658 return null;
1662 auto itemAs(string type) (FuiContext ctx, const(char)[] id) nothrow @trusted @nogc {
1663 if (!ctx.valid) return null;
1664 foreach (int fid; 0..ctx.length) {
1665 if (auto data = ctx.itemAs!type(fid)) {
1666 if (data.id.getz == id) return data;
1669 return null;
1673 auto firstItemOfType(string type) (FuiContext ctx) nothrow @trusted @nogc {
1674 if (!ctx.valid) return null;
1675 foreach (int fid; 0..ctx.length) {
1676 if (auto it = ctx.itemAs!type(fid)) return it;
1678 return null;
1682 FuiCtlType itemType (FuiContext ctx, int item) nothrow @trusted @nogc {
1683 if (!ctx.valid) return FuiCtlType.Invisible;
1684 if (auto data = ctx.itemIntr!FuiCtlHead(item)) return data.type;
1685 return FuiCtlType.Invisible;
1689 // ////////////////////////////////////////////////////////////////////////// //
1690 void focusFirst (FuiContext ctx) nothrow @trusted @nogc {
1691 if (!ctx.valid) return;
1692 auto fid = ctx.focused;
1693 if (fid < 1 || fid >= ctx.length) fid = 0;
1694 auto lp = ctx.layprops(fid);
1695 if (fid < 1 || fid >= ctx.length || !lp.visible || !lp.enabled || !lp.canBeFocused) {
1696 for (fid = 1; fid < ctx.length; ++fid) {
1697 lp = ctx.layprops(fid);
1698 if (lp is null) continue;
1699 if (lp.visible && lp.enabled && lp.canBeFocused) {
1700 ctx.focused = fid;
1701 return;
1708 int findDefault (FuiContext ctx) nothrow @trusted @nogc {
1709 if (!ctx.valid) return -1;
1710 foreach (int fid; 0..ctx.length) {
1711 if (auto lp = ctx.layprops(fid)) {
1712 if (lp.visible && lp.enabled && lp.canBeFocused && (lp.userFlags&FuiCtlUserFlags.Default) != 0) return fid;
1715 return -1;
1719 // ////////////////////////////////////////////////////////////////////////// //
1720 void dialogPalette (FuiContext ctx, int palidx) {
1721 if (palidx < 0 || palidx >= tuiPalette.length) palidx = 0;
1722 if (auto data = ctx.itemIntr!FuiCtlHead(0)) {
1723 data.pal = tuiPalette[palidx];
1728 void palColor(string name) (FuiContext ctx, int itemid, uint clr) {
1729 if (auto data = ctx.item!FuiCtlHead(itemid)) {
1730 mixin("data.pal."~name~" = clr;");
1735 // ////////////////////////////////////////////////////////////////////////// //
1736 uint palColor(string name) (FuiContext ctx, int itemid) {
1737 uint res;
1738 for (;;) {
1739 if (auto data = ctx.itemIntr!FuiCtlHead(itemid)) {
1740 res = mixin("data.pal."~name);
1741 if (res) break;
1742 itemid = ctx.layprops(itemid).parent;
1743 } else {
1744 res = mixin("tuiPalette[TuiPaletteNormal]."~name);
1745 break;
1748 return (res ? res : XtColorFB!(ttyRgb2Color(0xd0, 0xd0, 0xd0), ttyRgb2Color(0x4e, 0x4e, 0x4e))); // 252,239
1752 // ////////////////////////////////////////////////////////////////////////// //
1753 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) {
1754 static assert(defcenter == "left" || defcenter == "right" || defcenter == "center");
1755 if (hotx !is null) *hotx = x;
1756 if (w < 1) return;
1757 win.color = attr;
1758 static if (spaces) { x += 1; w -= 2; }
1759 if (w < 1 || s.length == 0) return;
1760 int sx = x, ex = x+w-1;
1761 auto vislen = visStrLen!dohot(s);
1762 if (s.ptr[0] == '\x01') {
1763 // left, nothing to do
1764 s = s[1..$];
1765 } else if (s.ptr[0] == '\x02') {
1766 // right
1767 x += (w-vislen);
1768 s = s[1..$];
1769 } else if (s.ptr[0] == '\x03') {
1770 // center
1771 auto len = visStrLen(s);
1772 x += (w-len)/2;
1773 s = s[1..$];
1774 } else {
1775 static if (defcenter == "left") {
1776 } else static if (defcenter == "right") {
1777 x += (w-vislen);
1778 } else static if (defcenter == "center") {
1779 // center
1780 x += (w-vislen)/2;
1783 bool wasHot;
1784 static if (spaces) {
1785 win.writeCharsAt(x-1, y, 1, ' ');
1786 int ee = x+vislen;
1787 if (ee <= ex+1) win.writeCharsAt(ee, y, 1, ' ');
1789 while (s.length > 0) {
1790 if (dohot && s.length > 1 && s.ptr[0] == '&' && s.ptr[1] != '&') {
1791 if (!wasHot && hotx !is null) *hotx = x;
1792 if (x >= sx && x <= ex) {
1793 if (hotattr && !wasHot) win.color = hotattr;
1794 win.writeCharsAt(x, y, 1, s.ptr[1]);
1795 win.color = attr;
1797 s = s[2..$];
1798 wasHot = true;
1799 } else if (dohot && s.length > 1 && s.ptr[0] == '&' && s.ptr[1] != '&') {
1800 if (x >= sx && x <= ex) win.writeCharsAt(x, y, 1, s.ptr[0]);
1801 s = s[2..$];
1802 } else {
1803 if (x >= sx && x <= ex) win.writeCharsAt(x, y, 1, s.ptr[0]);
1804 s = s[1..$];
1806 ++x;
1811 // ////////////////////////////////////////////////////////////////////////// //
1812 // we have to draw shadow separately, so it won't get darken each time
1813 void drawShadow (FuiContext ctx) nothrow @trusted @nogc {
1814 if (!ctx.valid) return;
1815 auto lp = ctx.layprops(0);
1816 if (lp is null) return;
1817 auto rc = lp.position;
1818 //xtShadowBox(rc.x+rc.w, rc.y+1, 2, rc.h-1);
1819 //xtHShadow(rc.x+2, rc.y+rc.h, rc.w);
1820 auto win = XtWindow(rc.x, rc.y, rc.w+2, rc.h+1);
1821 win.shadowBox(rc.w, 1, 2, rc.h-1);
1822 win.hshadow(2, rc.h, rc.w);
1826 void draw (FuiContext ctx) {
1827 if (!ctx.valid) return;
1829 void drawItem (int item, FuiPoint g) {
1830 auto lp = ctx.layprops(item);
1831 if (lp is null) return;
1832 if (!lp.visible) return;
1834 // convert local coords to global coords
1835 auto rc = lp.position;
1836 // don't shift root panes, it is already shifted
1837 if (item != 0) {
1838 rc.xp += g.x;
1839 rc.yp += g.y;
1842 void drawRootFrame (bool secondpass=false) {
1843 auto dlgdata = ctx.itemIntr!FuiCtlRootPanel(0);
1844 auto anorm = ctx.palColor!"def"(item);
1845 auto atitle = ctx.palColor!"title"(item);
1846 auto win = XtWindow.fullscreen;
1847 win.color = (!dlgdata.moving ? anorm : atitle);
1848 if (rc.w > 0 && rc.h > 0) {
1849 auto data = ctx.itemIntr!FuiCtlRootPanel(0);
1850 if (!secondpass) win.fill(rc.x, rc.y, rc.w, rc.h);
1851 final switch (data.frame) {
1852 case FuiDialogFrameType.Normal:
1853 win.frame!false(rc.x+1, rc.y+1, rc.w-2, rc.h-2);
1854 win.tuiWriteStr!("center", true)(rc.x+1, rc.y+1, rc.w-2, data.caption.getz, atitle, atitle);
1855 break;
1856 case FuiDialogFrameType.Small:
1857 win.frame!false(rc.x, rc.y, rc.w, rc.h);
1858 win.tuiWriteStr!("center", true)(rc.x+1, rc.y, rc.w-2, data.caption.getz, atitle, atitle);
1859 break;
1864 bool root = (item == 0);
1865 if (item == 0) drawRootFrame();
1867 auto head = ctx.itemIntr!FuiCtlHead(item);
1868 if (head.drawcb !is null) head.drawcb(ctx, item, rc);
1871 // draw children
1872 item = lp.firstChild;
1873 lp = ctx.layprops(item);
1874 if (lp !is null) {
1875 while (lp !is null) {
1876 drawItem(item, rc.pos);
1877 item = lp.nextSibling;
1878 lp = ctx.layprops(item);
1882 if (root) {
1883 auto dlgdata = ctx.itemIntr!FuiCtlRootPanel(0);
1884 if (dlgdata.moving) drawRootFrame(true);
1888 drawItem(0, ctx.layprops(0).position.pos);
1892 // ////////////////////////////////////////////////////////////////////////// //
1893 // returns `true` if event was consumed
1894 bool processEvent (FuiContext ctx, FuiEvent ev) {
1895 if (!ctx.valid) return false;
1897 if (auto rd = ctx.itemIntr!FuiCtlHead(0)) {
1898 if (rd.eventcb !is null) {
1899 if (rd.eventcb(ctx, ev.item, ev)) return true;
1903 if (ev.item > 0) {
1904 if (auto lp = ctx.layprops(ev.item)) {
1905 if (lp.visible && !lp.disabled) {
1906 auto data = ctx.itemIntr!FuiCtlHead(ev.item);
1907 assert(data !is null);
1908 if (data.eventcb !is null) {
1909 //{ import iv.vfs.io; VFile("z00.log", "a").writeln("ev.item=", ev.item); }
1910 if (data.eventcb(ctx, ev.item, ev)) return true;
1916 // event is not processed
1917 if (ev.type == FuiEvent.Type.Char) {
1918 // broadcast char as ModChar
1919 auto ch = ev.ch;
1920 if (ch > ' ' && ch < 256) ch = toupper(cast(char)ch);
1921 ev.type = FuiEvent.Type.Key;
1922 ev.keyp = TtyEvent.init;
1923 ev.keyp.key = TtyEvent.Key.ModChar;
1924 ev.keyp.ctrl = false;
1925 ev.keyp.alt = true;
1926 ev.keyp.shift = false;
1927 ev.keyp.ch = ch;
1928 //assert(ev.type == FuiEvent.Type.Key);
1929 } else {
1930 if (ev.type != FuiEvent.Type.Key) return false;
1932 // do navigation
1933 if (ev.key == "Enter") {
1934 // either current or default
1935 auto def = ctx.findDefault;
1936 if (def >= 0) return ctx.btnlikeClick(def);
1937 if (auto rd = ctx.itemIntr!FuiCtlRootPanel(0)) {
1938 if (rd.enterclose) {
1939 // send "close" to root with root as result
1940 ctx.postClose(0);
1941 return true;
1944 return false;
1946 if (ev.key == "Up" || ev.key == "Left") { ctx.focusPrev(); return true; }
1947 if (ev.key == "Down" || ev.key == "Right") { ctx.focusNext(); return true; }
1948 // broadcast ModChar, so widgets can process hotkeys
1949 if (ev.key.key == TtyEvent.Key.ModChar && !ev.key.ctrl && ev.key.alt && !ev.key.shift) {
1950 auto res = ctx.findNextEx(0, (int id) {
1951 if (auto lp = ctx.layprops(id)) {
1952 if (lp.visible && !lp.disabled) {
1953 auto data = ctx.item!FuiCtlHead(id);
1954 if (data.eventcb !is null) {
1955 if (data.eventcb(ctx, id, ev)) return true;
1959 return false;
1961 if (res >= 0) return true;
1963 return false;
1967 // ////////////////////////////////////////////////////////////////////////// //
1968 private __gshared bool windowMovingMouse, windowMovingKeys;
1969 private __gshared int wmX, wmY;
1970 private __gshared bool screenSaved;
1971 public __gshared int modalLastResult = -1;
1973 private __gshared FuiContext[] modalStack;
1975 @property bool modalHasOpenDialogs () nothrow @trusted @nogc { return (modalStack.length > 0); }
1978 // ////////////////////////////////////////////////////////////////////////// //
1979 void modalDialogRestoreScreen () {
1980 if (screenSaved) {
1981 screenSaved = false;
1982 xtPopArea();
1987 // this will restore old screen before saving it again
1988 void modalDialogSaveScreen () {
1989 modalDialogRestoreScreen();
1991 if (modalStack.length == 0) return;
1992 if (!ctx.valid) return false;
1993 auto lp = ctx.layprops(0);
1994 if (lp is null) return false;
1995 xtPushArea(
1996 lp.position.x, lp.position.y,
1997 lp.position.w+2, lp.position.h+1
2000 screenSaved = true;
2001 xtPushArea(0, 0, ttyw, ttyh);
2005 // redraw all dialogs
2006 void modalDialogDraw () {
2007 modalDialogSaveScreen();
2008 foreach (immutable idx, FuiContext ctx; modalStack) {
2009 if (!ctx.valid) continue;
2010 ctx.drawShadow();
2011 if (idx == 0 && (windowMovingMouse || windowMovingKeys)) ctx.dialogMoving = true;
2012 scope(exit) if (idx == 0 && (windowMovingMouse || windowMovingKeys)) ctx.dialogMoving = false;
2013 ctx.draw();
2018 // modalLastResult should be set
2019 private void closeTopDialog () {
2020 modalDialogRestoreScreen();
2021 if (modalStack.length == 0) return;
2022 auto ctx = modalStack[$-1];
2023 windowMovingMouse = false;
2024 windowMovingKeys = false;
2025 modalStack[$-1] = FuiContext.init;
2026 modalStack.length -= 1;
2027 modalStack.assumeSafeAppend;
2028 if (auto data = ctx.itemIntr!FuiCtlRootPanel(0)) {
2029 if (data.closecb !is null) data.closecb(ctx, modalLastResult);
2031 { import core.memory : GC; GC.collect; GC.minimize; }
2035 // returns `true` if dialog was closed, and then `modalLastResult` will contain result
2036 // return FuiContinue, clicked item index or -1 for esc
2037 int modalDialogProcessKey (TtyEvent key, bool* closed=null) {
2038 FuiContext ctx;
2040 if (closed !is null) *closed = false;
2042 while (modalStack.length > 0) {
2043 if (modalStack[$-1].valid) break;
2044 modalStack.length -= 1;
2045 modalStack.assumeSafeAppend;
2048 if (modalStack.length == 0) {
2049 { import core.memory : GC; GC.collect; GC.minimize; }
2050 windowMovingMouse = false;
2051 windowMovingKeys = false;
2052 modalLastResult = -1;
2053 if (closed !is null) *closed = true;
2054 return FuiContinue;
2057 ctx = modalStack[$-1];
2059 int processContextEvents () {
2060 ctx.update();
2061 while (ctx.hasEvents) {
2062 auto ev = ctx.getEvent();
2063 if (ev.type == FuiEvent.Type.Close) return ev.result;
2064 if (ctx.processEvent(ev)) continue;
2065 if (ev.type == FuiEvent.Type.Key && (ev.key == "^F3" || ev.key == "S-F3")) windowMovingKeys = true;
2067 return FuiContinue;
2070 int res = processContextEvents();
2071 if (res >= -1) {
2072 if (closed !is null) *closed = true;
2073 modalLastResult = res;
2074 closeTopDialog();
2075 return modalLastResult;
2078 //auto key = ttyReadKey(-1, TtyDefaultEscWait);
2079 if (key.key == TtyEvent.Key.Error) return FuiContinue;
2080 if (key.key == TtyEvent.Key.Unknown) return FuiContinue;
2081 if (!windowMovingKeys && key.key == TtyEvent.Key.Escape) {
2082 if (closed !is null) *closed = true;
2083 modalLastResult = -1;
2084 closeTopDialog();
2085 return modalLastResult;
2088 //if (key == "^L") { xtFullRefresh(); continue; }
2090 int dx = 0, dy = 0;
2092 // move dialog with keys
2093 if (key == "M-Left") dx = -1;
2094 if (key == "M-Right") dx = 1;
2095 if (key == "M-Up") dy = -1;
2096 if (key == "M-Down") dy = 1;
2098 if (windowMovingKeys) {
2099 if (key == "Left") dx = -1;
2100 if (key == "Right") dx = 1;
2101 if (key == "Up") dy = -1;
2102 if (key == "Down") dy = 1;
2105 // move dialog with mouse
2106 if (windowMovingMouse && key.mouse) {
2107 if (key.mrelease && key.button == TtyEvent.MButton.Left) {
2108 windowMovingMouse = false;
2109 return FuiContinue;
2111 dx = key.x-wmX;
2112 dy = key.y-wmY;
2113 wmX = key.x;
2114 wmY = key.y;
2117 // do move
2118 if (dx || dy) {
2119 ctx.layprops(0).position.x = ctx.layprops(0).position.x+dx;
2120 ctx.layprops(0).position.y = ctx.layprops(0).position.y+dy;
2121 //saveArea();
2122 //ctx.drawShadow();
2123 return FuiContinue;
2126 if (windowMovingKeys) {
2127 if (key == "Escape") windowMovingKeys = false;
2128 return FuiContinue;
2131 if (windowMovingMouse) return FuiContinue;
2133 if (key.mouse) {
2134 //TODO: check for no frame when we'll get that
2135 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) {
2136 if ((key.y == ctx.layprops(0).position.y) || (ctx.dialogFrame == FuiDialogFrameType.Normal && key.y == ctx.layprops(0).position.y+1)) {
2137 windowMovingMouse = true;
2138 wmX = key.x;
2139 wmY = key.y;
2140 return FuiContinue;
2145 ctx.keyboardEvent(key);
2147 res = processContextEvents();
2148 if (res >= -1) {
2149 if (closed !is null) *closed = true;
2150 modalLastResult = res;
2151 closeTopDialog();
2152 return modalLastResult;
2155 return FuiContinue;
2159 // initialize and push dialog
2160 // return `false` if dialog was not initialized and pushed
2161 bool modalDialogInit(bool docenter=true) (FuiContext ctx) {
2162 if (!ctx.valid) return false;
2164 static if (docenter) {
2165 if (auto lp = ctx.layprops(0)) {
2166 if (lp.position.x == 0) lp.position.x = (ttyw-lp.position.w)/2;
2167 if (lp.position.y == 0) lp.position.y = (ttyh-lp.position.h)/2;
2170 ctx.focusFirst();
2171 modalStack ~= ctx;
2172 return true;
2176 // close top-level dialog
2177 // return `false` if there are no open dialogs
2178 bool modalCloseDialog () {
2179 if (modalStack.length) {
2180 modalLastResult = -1;
2181 closeTopDialog();
2182 return true;
2184 return false;
2188 // ////////////////////////////////////////////////////////////////////////// //
2189 // returns clicked item or -1 for esc
2190 int modalDialog(bool docenter=true) (FuiContext ctx) {
2191 if (!ctx.modalDialogInit!docenter()) return -1;
2192 scope(exit) modalDialogRestoreScreen();
2193 bool closed;
2194 for (;;) {
2195 modalDialogDraw();
2196 xtFlush();
2197 do {
2198 auto key = ttyReadKey(-1, TtyDefaultEscWait);
2199 if (key == "^L") { modalDialogRestoreScreen(); xtFullRefresh(); break; }
2200 if (key.key == TtyEvent.Key.Error) { modalCloseDialog(); return modalLastResult; }
2201 modalDialogProcessKey(key, &closed);
2202 if (closed) return modalLastResult;
2203 } while (ttyIsKeyHit);