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*/;
25 import iv
.egtui
.editor
;
26 public import iv
.egtui
.layout
;
28 import iv
.egtui
.types
;
29 import iv
.egtui
.utils
;
30 import iv
.egtui
.dialogs
: dialogHistory
;
33 // ////////////////////////////////////////////////////////////////////////// //
34 enum FuiContinue
= -666;
37 // ////////////////////////////////////////////////////////////////////////// //
39 uint def
; // default color
40 uint sel
; // sel is also focus
41 uint mark
; // marked text
42 uint marksel
; // active marked text
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
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
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
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
) {
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)
108 auto ampos
= s
.indexOf('&');
109 if (ampos
< 0 || s
.length
-ampos
< 2) break;
110 if (s
.ptr
[ampos
+1] != '&') { --res
; break; }
118 private char visHotChar (const(char)[] s
) nothrow @trusted @nogc {
119 bool prevWasAmp
= false;
120 foreach (char ch
; s
) {
122 prevWasAmp
= !prevWasAmp
;
123 } else if (prevWasAmp
) {
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
];
138 private void setz (char[] dest
, const(char)[] s
) nothrow @trusted @nogc {
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 {
151 // ////////////////////////////////////////////////////////////////////////// //
152 enum FuiCtlType
: ubyte {
162 //WARNING: button-likes should be last!
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() {
181 char[64] id
= 0; // widget it
182 char[128] caption
= 0; // widget caption
184 // onchange callback; return FuiContinue to continue, -1 to exit via "esc", or control id to return from `modalDialog()`
186 // draw widget; rc is in global space
188 // process event; return `true` if event was eaten
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
)) {
209 //TODO: make delegate this scoped?
210 bool setDrawCB (FuiContext ctx
, int item
, TuiDrawCB cb
) {
211 if (auto data
= ctx
.itemIntr
!FuiCtlHead(item
)) {
219 //TODO: make delegate this scoped?
220 bool setEventCB (FuiContext ctx
, int item
, TuiEventCB cb
) {
221 if (auto data
= ctx
.itemIntr
!FuiCtlHead(item
)) {
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
;
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 {
255 struct FuiCtlRootPanel
{
257 FuiDialogFrameType frame
;
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)) {
274 TuiDialogCloseCB
dialogCloseCB (FuiContext ctx
) {
275 if (auto data
= ctx
.itemIntr
!FuiCtlRootPanel(0)) return data
.closecb
;
281 void dialogMoving (FuiContext ctx
, bool v
) {
282 if (!ctx
.valid
) return;
283 auto data
= ctx
.itemIntr
!FuiCtlRootPanel(0);
287 bool dialogMoving (FuiContext ctx
) {
288 if (!ctx
.valid
) return false;
289 auto data
= ctx
.itemIntr
!FuiCtlRootPanel(0);
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);
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);
312 case FuiDialogFrameType
.Normal
:
313 data
.frame
= FuiDialogFrameType
.Normal
;
321 case FuiDialogFrameType
.Small
:
322 data
.frame
= FuiDialogFrameType
.Small
;
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
;
354 void dialogHistoryManager (FuiContext ctx
, FuiHistoryManager hisman
) {
355 if (auto data
= ctx
.itemIntr
!FuiCtlRootPanel(0)) data
.hisman
= hisman
;
359 // ////////////////////////////////////////////////////////////////////////// //
360 struct FuiCtlCustomBox
{
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
)) {
373 aligning
= FuiLayoutProps
.Align
.Stretch
;
376 //clickMask |= FuiLayoutProps.Buttons.Left;
377 //canBeFocused = true;
380 auto data
= ctx
.item
!FuiCtlCustomBox(item
);
381 data
.type
= FuiCtlType
.CustomBox
;
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);
388 win.fill(0, 0, rc.w, rc.h);
395 // ////////////////////////////////////////////////////////////////////////// //
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
)) {
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;
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
)) {
427 aligning
= FuiLayoutProps
.Align
.Start
;
429 auto data
= ctx
.item
!FuiCtlSpan(item
);
430 data
.type
= FuiCtlType
.Invisible
;
431 //data.pal = Palette.init;
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
)) {
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);
457 final switch (ctx
.dialogFrame
) {
458 case FuiDialogFrameType
.Normal
:
460 x1
= x0
+(rlp
.position
.w
-2);
462 case FuiDialogFrameType
.Small
:
464 x1
= x0
+rlp
.position
.w
;
467 win
.hline(x0
, rc
.y
, x1
-x0
);
469 win
.hline(rc
.x
, rc
.y
, rc
.w
);
475 // ////////////////////////////////////////////////////////////////////////// //
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
);
490 auto data
= ctx
.item
!FuiCtlBox(item
);
491 data
.type
= FuiCtlType
.Box
;
492 //data.pal = Palette.init;
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 // ////////////////////////////////////////////////////////////////////////// //
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);
520 auto data
= ctx
.item
!FuiCtlPanel(item
);
521 data
.type
= FuiCtlType
.Panel
;
522 //data.pal = Palette.init;
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
));
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
{
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
)) {
553 clickMask |
= FuiLayoutProps
.Buttons
.Left
;
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
);
563 data
.type
= FuiCtlType
.EditText
;
565 data
.type
= FuiCtlType
.EditLine
;
567 //data.pal = Palette.init;
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
);
576 ed
.hideStatus
= true;
577 ed
.moveResize(rc
.x
+(ed
.singleline ?
0 : 1), rc
.y
, rc
.w
, rc
.h
);
579 ed
.dontSetCursor
= (self
!= ctx
.focused
);
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
);
586 ed
.clrBlock
= ctx
.palColor
!"inputmark"(self
);
587 ed
.clrText
= ctx
.palColor
!"input"(self
);
588 ed
.clrTextUnchanged
= ctx
.palColor
!"inputunchanged"(self
);
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);
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
:
605 case FuiEvent
.Type
.Char
: // param0: dchar; param1: mods&buttons
607 k
.key
= TtyEvent
.Key
.Char
;
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
);
617 case FuiEvent
.Type
.Key
: // param0: sdpy keycode; param1: mods&buttons
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") {
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
);
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
);
645 if (eld
.ed
.singleline
&& ev
.key
== "S-M-M") {
646 eld
.ed
.setNewText(`\s+([-+*/%^&|]|<<|>>)\s+`, false); // don't clear on type
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
);
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;
661 case FuiEvent
.Type
.Double
: // mouse double-click; param0: buttton index; param1: mods&buttons
663 case FuiEvent
.Type
.Close
: // close dialog; param0: return id
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
;
691 TtyEditor
edittextEditor (FuiContext ctx
, int item
) {
692 if (auto data
= ctx
.item
!FuiCtlEditLine(item
)) {
693 if (data
.type
== FuiCtlType
.EditText
) return data
.ed
;
699 char[] editlineGetText (FuiContext ctx
, int item
) {
700 if (auto edl
= ctx
.itemAs
!"editline"(item
)) {
702 if (ed
is null) return null;
705 res
.reserve(rng
.length
);
706 foreach (char ch
; rng
) res
~= ch
;
713 char[] edittextGetText (FuiContext ctx
, int item
) {
714 if (auto edl
= ctx
.itemAs
!"edittext"(item
)) {
716 if (ed
is null) return null;
719 res
.reserve(rng
.length
);
720 foreach (char ch
; rng
) res
~= ch
;
727 // ////////////////////////////////////////////////////////////////////////// //
728 mixin template FuiCtlBtnLike() {
731 bool spaceClicks
= true;
732 bool enterClicks
= false;
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
{
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; }
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
);
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
);
780 //data.pal = Palette.init;
781 if (text
.length
> 255) text
= text
[0..255];
782 if (id
.length
> 255) id
= id
[0..255];
784 data
.caption
.setz(text
);
786 data
.hotchar
= visHotChar(text
).toupper
;
790 with (ctx
.layprops(item
)) {
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
:
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
);
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
);
823 case FuiEvent
.Type
.Click
: // mouse click; param0: buttton index; param1: mods&buttons
824 if (ev
.item
== self
) return ctx
.btnlikeClick(self
, ev
.bidx
);
826 case FuiEvent
.Type
.Double
: // mouse double-click; param0: buttton index; param1: mods&buttons
828 case FuiEvent
.Type
.Close
: // close dialog; param0: return id
836 // ////////////////////////////////////////////////////////////////////////// //
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
)) {
847 canBeFocused
= false;
849 auto data
= ctx
.item
!FuiCtlLabel(res
);
850 data
.dest
.setz(destid
);
851 if (destid
.length
== 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
;
863 anorm
= ctx
.palColor
!"def"(self
);
864 ahot
= ctx
.palColor
!"hot"(self
);
866 anorm
= ahot
= ctx
.palColor
!"disabled"(self
);
869 win
.tuiWriteStr
!("right", false)(rc
.x
, rc
.y
, rc
.w
, data
.caption
.getz
, anorm
, ahot
);
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
) {
890 // ////////////////////////////////////////////////////////////////////////// //
891 struct FuiCtlButton
{
895 int button (FuiContext ctx
, int parent
, const(char)[] id
, const(char)[] text
) {
896 auto item
= ctx
.buttonLike
!(FuiCtlButton
, FuiCtlType
.Button
)(parent
, id
, text
);
898 with (ctx
.layprops(item
)) {
899 //clickMask |= FuiLayoutProps.Buttons.Left;
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
;
912 if (ctx
.focused
!= self
) {
913 anorm
= ctx
.palColor
!"def"(self
);
914 ahot
= ctx
.palColor
!"hot"(self
);
916 anorm
= ctx
.palColor
!"sel"(self
);
917 ahot
= ctx
.palColor
!"hotsel"(self
);
920 anorm
= ahot
= ctx
.palColor
!"disabled"(self
);
923 bool def
= ((lp
.userFlags
&FuiCtlUserFlags
.Default
) != 0);
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
, ' ');
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, ']');
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
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
;
959 if (ctx
.focused
!= self
) {
960 anorm
= ctx
.palColor
!"def"(self
);
961 ahot
= ctx
.palColor
!"hot"(self
);
963 anorm
= ctx
.palColor
!"sel"(self
);
964 ahot
= ctx
.palColor
!"hotsel"(self
);
967 anorm
= ahot
= ctx
.palColor
!"disabled"(self
);
970 win
.writeCharsAt(rc
.x
, rc
.y
, rc
.w
, ' ');
973 static if (type
== "checkbox") markCh
= 'x';
974 else static if (type
== "radio") markCh
= '*';
975 else static assert(0, "wtf?!");
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
);
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
);
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
);
1002 auto data
= ctx
.item
!FuiCtlCheck(item
);
1003 data
.spaceClicks
= true;
1004 data
.enterClicks
= false;
1006 with (ctx
.layprops(item
)) {
1007 //clickMask |= FuiLayoutProps.Buttons.Left;
1008 canBeFocused
= true;
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
;
1026 // ////////////////////////////////////////////////////////////////////////// //
1027 struct FuiCtlRadio
{
1028 mixin FuiCtlBtnLike
;
1029 int gid
; // radio group index; <0: standalone
1033 private int countRadio (FuiContext ctx
, int* var
) {
1034 if (!ctx
.valid || var
is null) return -1;
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
;
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
);
1049 auto gid
= ctx
.countRadio(var
);
1050 auto data
= ctx
.item
!FuiCtlRadio(item
);
1051 data
.spaceClicks
= true;
1052 data
.enterClicks
= false;
1055 with (ctx
.layprops(item
)) {
1057 //clickMask |= FuiLayoutProps.Buttons.Left;
1058 canBeFocused
= true;
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
;
1076 // ////////////////////////////////////////////////////////////////////////// //
1077 struct FuiCtlTextView
{
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
)) {
1095 data
.textlen
= calcTextBoundsEx(c
, r
, data
.text
.ptr
[0..data
.textlen
], w
);
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
)) {
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;
1119 data
.textlen
= cast(uint)text
.length
;
1120 if (text
.length
> 0) {
1121 data
.text
.ptr
[0..text
.length
] = text
[];
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; }
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
);
1139 win
.color
= ctx
.palColor
!"def"(self
);
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
);
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);
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;
1162 while (tpos
< data
.textlen
&& ty
< rc
.h
) {
1164 while (epos
< data
.textlen
&& data
.text
.ptr
[epos
] != '\n' && data
.text
.ptr
[epos
] != '\6') {
1165 if (epos
-tpos
== rc
.w
) break;
1168 final switch (talign
) {
1170 win
.writeStrAt(xofs
, ty
, data
.text
.ptr
[tpos
..epos
]);
1173 win
.writeStrAt(rc
.w
-xofs
-(epos
-tpos
), ty
, data
.text
.ptr
[tpos
..epos
]);
1176 win
.writeStrAt(xofs
+(rc
.w
-xofs
-(epos
-tpos
))/2, ty
, data
.text
.ptr
[tpos
..epos
]);
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) {
1182 talign
= data
.text
.ptr
[epos
]-1;
1194 //win.color = atext;
1195 win
.vline(1, 0, rc
.h
);
1196 int last
= data
.topline
+rc
.h
;
1197 if (last
> r
) 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
:
1208 case FuiEvent
.Type
.Char
: // param0: dchar; param1: mods&buttons
1210 case FuiEvent
.Type
.Key
: // param0: sdpy keycode; param1: mods&buttons
1211 auto lp
= ctx
.layprops(self
);
1213 if (!lp
.visible || lp
.disabled
) 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; }
1228 case FuiEvent
.Type
.Click
: // mouse click; param0: buttton index; param1: mods&buttons
1230 case FuiEvent
.Type
.Double
: // mouse double-click; param0: buttton index; param1: mods&buttons
1232 case FuiEvent
.Type
.Close
: // close dialog; param0: return id
1240 // ////////////////////////////////////////////////////////////////////////// //
1241 struct FuiCtlListBoxItem
{
1247 struct FuiCtlListBox
{
1249 int itemCount
; // total number of items
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
)) {
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
)) {
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;
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
;
1313 uint atext
, asel
, agauge
;
1315 atext
= ctx
.palColor
!"def"(self
);
1316 asel
= ctx
.palColor
!"sel"(self
);
1317 agauge
= ctx
.palColor
!"gauge"(self
);
1319 atext
= asel
= agauge
= ctx
.palColor
!"disabled"(self
);
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;
1344 // should i draw a scrollbar?
1345 if (wdt
> 2 && (data
.topItem
> 0 || data
.topItem
+rc
.h
< data
.itemCount
)) {
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
) {
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
);
1370 win
.writeStrAt(x
, rc
.y
+y
, t
);
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
:
1392 case FuiEvent
.Type
.Char
: // param0: dchar; param1: mods&buttons
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;
1400 if (auto lbox
= ctx
.itemAs
!"listbox"(self
)) {
1402 auto lp
= ctx
.layprops(ev
.item
);
1403 if (ev
.key
== "Up") {
1404 if (--lbox
.curItem
< 0) lbox
.curItem
= 0;
1407 if (ev
.key
== "S-Up") {
1408 if (--lbox
.topItem
< 0) lbox
.topItem
= 0;
1409 lbox
.topItemOffset
= 0; // invalidate
1412 if (ev
.key
== "Down") {
1413 if (lbox
.itemCount
> 0) {
1414 if (++lbox
.curItem
>= lbox
.itemCount
) lbox
.curItem
= lbox
.itemCount
-1;
1418 if (ev
.key
== "S-Down") {
1419 if (lbox
.topItem
+lp
.position
.h
< lbox
.itemCount
) {
1421 lbox
.topItemOffset
= 0; // invalidate
1425 if (ev
.key
== "Home") {
1429 if (ev
.key
== "End") {
1430 if (lbox
.itemCount
> 0) lbox
.curItem
= lbox
.itemCount
-1;
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;
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;
1451 ctx
.listboxNormPage(self
);
1452 auto oldCI
= lbox
.curItem
;
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
);
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
;
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
);
1484 case FuiEvent
.Type
.Double
: // mouse double-click; param0: buttton index; param1: mods&buttons
1486 case FuiEvent
.Type
.Close
: // close dialog; param0: return id
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);
1498 auto it
= ctx
.addStruct
!FuiCtlListBoxItem(itofs
, len
);
1500 if (text
.length
<= 255) {
1501 it
.text
.ptr
[0..text
.length
] = text
[];
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
;
1511 auto prev
= ctx
.structAtOfs
!FuiCtlListBoxItem(data
.lastItemOffset
);
1512 prev
.nextofs
= itofs
;
1514 data
.lastItemOffset
= itofs
;
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
;
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;
1543 int listboxItemCount (FuiContext ctx
, int item
) {
1544 if (auto data
= ctx
.itemAs
!"listbox"(item
)) return data
.itemCount
;
1549 int listboxItemCurrent (FuiContext ctx
, int item
) {
1550 if (auto data
= ctx
.itemAs
!"listbox"(item
)) return data
.curItem
;
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;
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) {
1573 auto it
= ctx
.structAtOfs
!FuiCtlListBoxItem(itofs
);
1574 if (it
.nextofs
== 0) break;
1583 // ////////////////////////////////////////////////////////////////////////// //
1584 // `id` is element id
1585 public class FuiHistoryManager
{
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
;
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
;
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
;
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
);
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
;
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
;
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
) {
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
;
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
) {
1742 if (auto data
= ctx
.itemIntr
!FuiCtlHead(itemid
)) {
1743 res
= mixin("data.pal."~name
);
1745 itemid
= ctx
.layprops(itemid
).parent
;
1747 res
= mixin("tuiPalette[TuiPaletteNormal]."~name
);
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
;
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
1768 } else if (s
.ptr
[0] == '\x02') {
1772 } else if (s
.ptr
[0] == '\x03') {
1774 auto len
= visStrLen(s
);
1778 static if (defcenter
== "left") {
1779 } else static if (defcenter
== "right") {
1781 } else static if (defcenter
== "center") {
1787 static if (spaces
) {
1788 win
.writeCharsAt(x
-1, y
, 1, ' ');
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]);
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]);
1806 if (x
>= sx
&& x
<= ex
) win
.writeCharsAt(x
, y
, 1, s
.ptr
[0]);
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
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
);
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
);
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
);
1875 item
= lp
.firstChild
;
1876 lp
= ctx
.layprops(item
);
1878 while (lp
!is null) {
1879 drawItem(item
, rc
.pos
);
1880 item
= lp
.nextSibling
;
1881 lp
= ctx
.layprops(item
);
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;
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
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;
1929 ev
.keyp
.shift
= false;
1931 //assert(ev.type == FuiEvent.Type.Key);
1933 if (ev
.type
!= FuiEvent
.Type
.Key
) return false;
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
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;
1964 if (res
>= 0) return true;
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 () {
1984 screenSaved
= false;
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;
1999 lp.position.x, lp.position.y,
2000 lp.position.w+2, lp.position.h+1
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;
2014 if (idx
== 0 && (windowMovingMouse || windowMovingKeys
)) ctx
.dialogMoving
= true;
2015 scope(exit
) if (idx
== 0 && (windowMovingMouse || windowMovingKeys
)) ctx
.dialogMoving
= false;
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) {
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;
2060 ctx
= modalStack
[$-1];
2062 int processContextEvents () {
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;
2073 int res
= processContextEvents();
2075 if (closed
!is null) *closed
= true;
2076 modalLastResult
= res
;
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;
2088 return modalLastResult
;
2091 //if (key == "^L") { xtFullRefresh(); continue; }
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;
2122 ctx
.layprops(0).position
.x
= ctx
.layprops(0).position
.x
+dx
;
2123 ctx
.layprops(0).position
.y
= ctx
.layprops(0).position
.y
+dy
;
2129 if (windowMovingKeys
) {
2130 if (key
== "Escape") windowMovingKeys
= false;
2134 if (windowMovingMouse
) return FuiContinue
;
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;
2148 ctx
.keyboardEvent(key
);
2150 res
= processContextEvents();
2152 if (closed
!is null) *closed
= true;
2153 modalLastResult
= res
;
2155 return modalLastResult
;
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;
2179 // close top-level dialog
2180 // return `false` if there are no open dialogs
2181 bool modalCloseDialog () {
2182 if (modalStack
.length
) {
2183 modalLastResult
= -1;
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();
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
);